Skip to content

Commit 9d8a674

Browse files
committed
feat(manifest): add 'list' command to display all flags in the manifest
Signed-off-by: Georges Dugué <[email protected]>
1 parent 3138f07 commit 9d8a674

File tree

6 files changed

+560
-10
lines changed

6 files changed

+560
-10
lines changed

internal/cmd/manifest.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ func GetManifestCmd() *cobra.Command {
2020

2121
// Add subcommands
2222
manifestCmd.AddCommand(GetManifestAddCmd())
23+
manifestCmd.AddCommand(GetManifestListCmd())
2324

2425
addStabilityInfo(manifestCmd)
2526

2627
return manifestCmd
27-
}
28+
}

internal/cmd/manifest_add.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import (
88
"strings"
99

1010
"github.com/open-feature/cli/internal/config"
11+
"github.com/open-feature/cli/internal/filesystem"
1112
"github.com/open-feature/cli/internal/flagset"
1213
"github.com/open-feature/cli/internal/logger"
1314
"github.com/open-feature/cli/internal/manifest"
1415
"github.com/pterm/pterm"
16+
"github.com/spf13/afero"
1517
"github.com/spf13/cobra"
1618
)
1719

@@ -67,16 +69,23 @@ Examples:
6769
}
6870

6971
// Load existing manifest
70-
fs, err := manifest.LoadFlagSet(manifestPath)
72+
var fs *flagset.Flagset
73+
exists, err := afero.Exists(filesystem.FileSystem(), manifestPath)
74+
7175
if err != nil {
72-
// If manifest doesn't exist, create a new one
73-
if strings.Contains(err.Error(), "error reading contents") {
74-
fs = &flagset.Flagset{
75-
Flags: []flagset.Flag{},
76-
}
77-
} else {
76+
return fmt.Errorf("failed to check manifest existence: %w", err)
77+
}
78+
79+
if exists {
80+
fs, err = manifest.LoadFlagSet(manifestPath)
81+
if err != nil {
7882
return fmt.Errorf("failed to load manifest: %w", err)
7983
}
84+
} else {
85+
// If manifest doesn't exist, create a new one
86+
fs = &flagset.Flagset{
87+
Flags: []flagset.Flag{},
88+
}
8089
}
8190

8291
// Check if flag already exists
@@ -105,6 +114,11 @@ Examples:
105114
logger.Default.Debug(fmt.Sprintf("Added flag: name=%s, type=%s, defaultValue=%v, description=%s",
106115
flagName, flagType, defaultValue, description))
107116

117+
// Display all current flags
118+
displayFlagList(fs, manifestPath)
119+
pterm.Println("Use the 'generate' command to update type-safe clients with the new flag.")
120+
pterm.Println()
121+
108122
return nil
109123
},
110124
}
@@ -169,4 +183,4 @@ func parseDefaultValue(value string, flagType flagset.FlagType) (interface{}, er
169183
default:
170184
return nil, fmt.Errorf("unsupported flag type: %v", flagType)
171185
}
172-
}
186+
}

internal/cmd/manifest_add_test.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"testing"
67

78
"github.com/open-feature/cli/internal/config"
89
"github.com/open-feature/cli/internal/filesystem"
10+
"github.com/pterm/pterm"
911
"github.com/spf13/afero"
1012
"github.com/stretchr/testify/assert"
1113
"github.com/stretchr/testify/require"
@@ -360,4 +362,68 @@ func TestManifestAddCmd_CreateNewManifest(t *testing.T) {
360362
assert.Equal(t, "boolean", flag["flagType"])
361363
assert.Equal(t, true, flag["defaultValue"])
362364
assert.Equal(t, "The first flag in a new manifest", flag["description"])
363-
}
365+
}
366+
367+
func TestManifestAddCmd_DisplaysListAfterAdd(t *testing.T) {
368+
// Setup
369+
fs := afero.NewMemMapFs()
370+
filesystem.SetFileSystem(fs)
371+
372+
// Create existing manifest with one flag
373+
existingManifest := `{
374+
"$schema": "https://raw.githubusercontent.com/open-feature/cli/refs/heads/main/schema/v0/flag-manifest.json",
375+
"flags": {
376+
"existing-flag": {
377+
"flagType": "string",
378+
"defaultValue": "test",
379+
"description": "An existing flag"
380+
}
381+
}
382+
}`
383+
err := afero.WriteFile(fs, "flags.json", []byte(existingManifest), 0644)
384+
require.NoError(t, err)
385+
386+
// Enable pterm output and capture it
387+
pterm.EnableOutput()
388+
defer pterm.DisableOutput()
389+
390+
buf := &bytes.Buffer{}
391+
oldStdout := pterm.DefaultTable.Writer
392+
oldSection := pterm.DefaultSection.Writer
393+
oldInfo := pterm.Info.Writer
394+
oldSuccess := pterm.Success.Writer
395+
pterm.DefaultTable.Writer = buf
396+
pterm.DefaultSection.Writer = buf
397+
pterm.Info.Writer = buf
398+
pterm.Success.Writer = buf
399+
defer func() {
400+
pterm.DefaultTable.Writer = oldStdout
401+
pterm.DefaultSection.Writer = oldSection
402+
pterm.Info.Writer = oldInfo
403+
pterm.Success.Writer = oldSuccess
404+
}()
405+
406+
// Create command and execute
407+
cmd := GetManifestCmd()
408+
config.AddRootFlags(cmd)
409+
410+
cmd.SetArgs([]string{
411+
"add", "new-flag",
412+
"--default-value", "true",
413+
"--description", "A new flag",
414+
"-m", "flags.json",
415+
})
416+
417+
// Execute command
418+
err = cmd.Execute()
419+
require.NoError(t, err)
420+
421+
// Validate output contains list of all flags
422+
output := buf.String()
423+
assert.Contains(t, output, "existing-flag", "Output should contain existing flag")
424+
assert.Contains(t, output, "new-flag", "Output should contain newly added flag")
425+
assert.Contains(t, output, "(2)", "Output should show total count of 2 flags")
426+
assert.Contains(t, output, "string", "Output should show flag types")
427+
assert.Contains(t, output, "boolean", "Output should show flag types")
428+
}
429+

internal/cmd/manifest_list.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/open-feature/cli/internal/config"
8+
"github.com/open-feature/cli/internal/flagset"
9+
"github.com/open-feature/cli/internal/manifest"
10+
"github.com/pterm/pterm"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func GetManifestListCmd() *cobra.Command {
15+
manifestListCmd := &cobra.Command{
16+
Use: "list",
17+
Short: "List all flags in the manifest",
18+
Long: `Display all flags defined in the manifest file with their configuration.`,
19+
Args: cobra.NoArgs,
20+
PreRunE: func(cmd *cobra.Command, args []string) error {
21+
return initializeConfig(cmd, "manifest.list")
22+
},
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
manifestPath := config.GetManifestPath(cmd)
25+
26+
// Load existing manifest
27+
fs, err := manifest.LoadFlagSet(manifestPath)
28+
if err != nil {
29+
return fmt.Errorf("failed to load manifest: %w", err)
30+
}
31+
32+
displayFlagList(fs, manifestPath)
33+
return nil
34+
},
35+
}
36+
37+
// Add command-specific flags
38+
config.AddManifestListFlags(manifestListCmd)
39+
addStabilityInfo(manifestListCmd)
40+
41+
return manifestListCmd
42+
}
43+
44+
// displayFlagList prints a formatted table of all flags in the flagset
45+
func displayFlagList(fs *flagset.Flagset, manifestPath string) {
46+
if len(fs.Flags) == 0 {
47+
pterm.Info.Println("No flags found in manifest")
48+
return
49+
}
50+
51+
// Print header
52+
pterm.DefaultSection.Println(fmt.Sprintf("Flags in %s (%d)", manifestPath, len(fs.Flags)))
53+
54+
// Create table data
55+
tableData := pterm.TableData{
56+
{"Key", "Type", "Default Value", "Description"},
57+
}
58+
59+
for _, flag := range fs.Flags {
60+
// Format default value for display
61+
defaultValueStr := formatValue(flag.DefaultValue)
62+
63+
// Truncate description if too long
64+
description := flag.Description
65+
const maxDescriptionLength = 50
66+
67+
if len(description) > maxDescriptionLength {
68+
description = description[:maxDescriptionLength-3] + "..."
69+
}
70+
71+
tableData = append(tableData, []string{
72+
flag.Key,
73+
flag.Type.String(),
74+
defaultValueStr,
75+
description,
76+
})
77+
}
78+
79+
// Render table
80+
pterm.DefaultTable.WithHasHeader().WithData(tableData).Render()
81+
}
82+
83+
// formatValue converts a value to a string representation suitable for display
84+
func formatValue(value interface{}) string {
85+
switch v := value.(type) {
86+
case string:
87+
if len(v) > 30 {
88+
return fmt.Sprintf("\"%s...\"", v[:27])
89+
}
90+
return fmt.Sprintf("\"%s\"", v)
91+
case bool, int, float64:
92+
return fmt.Sprintf("%v", v)
93+
case map[string]interface{}, []interface{}:
94+
jsonBytes, err := json.Marshal(v)
95+
if err != nil {
96+
return fmt.Sprintf("%v", v)
97+
}
98+
jsonStr := string(jsonBytes)
99+
if len(jsonStr) > 30 {
100+
return jsonStr[:27] + "..."
101+
}
102+
return jsonStr
103+
default:
104+
return fmt.Sprintf("%v", v)
105+
}
106+
}

0 commit comments

Comments
 (0)