diff --git a/cmd/build.go b/cmd/build.go index 9668e3f20..57ef5210c 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -210,14 +210,18 @@ func (cmd *BuildCmd) build( ctx context.Context, workspaceClient client.WorkspaceClient, ) error { - err := workspaceClient.Lock(ctx) + mode, err := output.ResolveMode(cmd.ResultFormat) if err != nil { return err } + emitJSON := mode == output.ModeJSON + + if err = workspaceClient.Lock(ctx); err != nil { + return err + } defer workspaceClient.Unlock() - err = clientimplementation.StartWait(ctx, workspaceClient, true) - if err != nil { + if err = clientimplementation.StartWait(ctx, workspaceClient, true); err != nil { return err } @@ -226,7 +230,6 @@ func (cmd *BuildCmd) build( log.Debugf("done building devcontainer") log.Infof("cleaning up temporary workspace") }() - emitJSON := output.ResolveMode(cmd.ResultFormat) == output.ModeJSON result, err := clientimplementation.BuildAgentClient( ctx, diff --git a/cmd/context/list.go b/cmd/context/list.go index 1e602dbcc..036e5ab9c 100644 --- a/cmd/context/list.go +++ b/cmd/context/list.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -16,8 +17,6 @@ import ( // ListCmd holds the list cmd flags. type ListCmd struct { *flags.GlobalFlags - - Output string } // NewListCmd creates a new command. @@ -34,8 +33,6 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return listCmd } @@ -52,8 +49,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for contextName := range devsyConfig.Contexts { tableEntries = append(tableEntries, []string{ @@ -69,7 +70,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { "Name", "Default", }, tableEntries) - case "json": + case output.ModeJSON: ides := []ContextWithDefault{} for contextName := range devsyConfig.Contexts { ides = append(ides, ContextWithDefault{ @@ -83,11 +84,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/context/options.go b/cmd/context/options.go index 55db46a3a..165411e0d 100644 --- a/cmd/context/options.go +++ b/cmd/context/options.go @@ -8,6 +8,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -15,8 +16,6 @@ import ( // OptionsCmd holds the options cmd flags. type OptionsCmd struct { *flags.GlobalFlags - - Output string } // NewOptionsCmd creates a new command. @@ -32,8 +31,6 @@ func NewOptionsCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - optionsCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return optionsCmd } @@ -55,8 +52,12 @@ func (cmd *OptionsCmd) Run(ctx context.Context, args []string) error { entryOptions = map[string]config.OptionValue{} } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for _, entry := range config.ContextOptions { value := entryOptions[entry.Name].Value @@ -78,7 +79,7 @@ func (cmd *OptionsCmd) Run(ctx context.Context, args []string) error { "Default", "Value", }, tableEntries) - case "json": + case output.ModeJSON: options := map[string]optionWithValue{} for _, entry := range config.ContextOptions { options[entry.Name] = optionWithValue{ @@ -92,11 +93,6 @@ func (cmd *OptionsCmd) Run(ctx context.Context, args []string) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/exec.go b/cmd/exec.go index 53dd84116..5baba6a42 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -132,6 +132,10 @@ func (cmd *ExecCmd) Run(ctx context.Context, args []string) error { return err } + if _, err := output.ResolveMode(cmd.ResultFormat); err != nil { + return err + } + if cmd.ContainerID != "" { return cmd.runWithContainerID(ctx, args) } @@ -173,7 +177,11 @@ func (cmd *ExecCmd) Run(ctx context.Context, args []string) error { probedEnv := probeContainerEnv(ctx, target, userEnvProbe) envMap := buildExecEnv(result, cmd.RemoteEnv, probedEnv) - emitJSON := output.ResolveMode(cmd.ResultFormat) == output.ModeJSON + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + emitJSON := mode == output.ModeJSON err = cmd.execInContainer(ctx, execOpts{ target: target, @@ -228,7 +236,11 @@ func (cmd *ExecCmd) runWithContainerID(ctx context.Context, args []string) error workdir := containerDetails.Config.WorkingDir - emitJSON := output.ResolveMode(cmd.ResultFormat) == output.ModeJSON + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + emitJSON := mode == output.ModeJSON err = cmd.execInContainer(ctx, execOpts{ target: target, diff --git a/cmd/features/generatedocs.go b/cmd/features/generatedocs.go index 70289cdf7..d7262adcb 100644 --- a/cmd/features/generatedocs.go +++ b/cmd/features/generatedocs.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/spf13/cobra" ) @@ -18,7 +19,6 @@ type GenerateDocsCmd struct { ProjectFolder string OutputFolder string Namespace string - Output string } func NewGenerateDocsCmd(globalFlags *flags.GlobalFlags) *cobra.Command { @@ -44,16 +44,14 @@ documentation for each feature based on its devcontainer-feature.json.`, generateDocsCmd.Flags().StringVar( &cmd.Namespace, "namespace", "", "Registry namespace for linking (e.g. ghcr.io/myorg/features)", ) - generateDocsCmd.Flags().StringVar( - &cmd.Output, "output", "text", "Output format (text or json)", - ) _ = generateDocsCmd.MarkFlagRequired("project-folder") return generateDocsCmd } func (cmd *GenerateDocsCmd) Run() error { - if err := validateOutputFormat(cmd.Output); err != nil { + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { return err } @@ -72,11 +70,13 @@ func (cmd *GenerateDocsCmd) Run() error { return err } - if cmd.Output == outputJSON { + switch mode { + case output.ModeJSON: return cmd.writeJSON(features) + case output.ModePlain: + return cmd.writeDocs(features, outputFolder) } - - return cmd.writeDocs(features, outputFolder) + return nil } func (cmd *GenerateDocsCmd) resolveOutputFolder( diff --git a/cmd/features/generatedocs_test.go b/cmd/features/generatedocs_test.go index a7e28e33d..028db4756 100644 --- a/cmd/features/generatedocs_test.go +++ b/cmd/features/generatedocs_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "testing" + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -40,7 +42,7 @@ func TestGenerateDocsCmd_Run(t *testing.T) { ProjectFolder: projectDir, OutputFolder: outputDir, Namespace: "ghcr.io/test/features", - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() @@ -78,7 +80,7 @@ func TestGenerateDocsCmd_NoFeatures(t *testing.T) { cmd := &GenerateDocsCmd{ ProjectFolder: projectDir, - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() @@ -110,7 +112,7 @@ func TestGenerateDocsCmd_MultipleFeatures(t *testing.T) { cmd := &GenerateDocsCmd{ ProjectFolder: projectDir, OutputFolder: outputDir, - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() diff --git a/cmd/features/info.go b/cmd/features/info.go index e1555d03e..7b0022942 100644 --- a/cmd/features/info.go +++ b/cmd/features/info.go @@ -7,6 +7,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -17,7 +18,6 @@ import ( type InfoCmd struct { *flags.GlobalFlags - Output string ShowTags bool ShowDependencies bool } @@ -55,7 +55,6 @@ to display metadata.`, }, } - infoCmd.Flags().StringVar(&cmd.Output, "output", "text", "Output format (text or json)") infoCmd.Flags().BoolVar( &cmd.ShowTags, "show-tags", false, "List available tags from the registry", ) @@ -70,10 +69,6 @@ to display metadata.`, } func (cmd *InfoCmd) Run(featureID string) error { - if err := validateOutputFormat(cmd.Output); err != nil { - return err - } - ref, err := name.ParseReference(featureID) if err != nil { return fmt.Errorf("invalid feature reference %q: %w", featureID, err) @@ -92,10 +87,17 @@ func (cmd *InfoCmd) Run(featureID string) error { info.Tags = tags } - if cmd.Output == outputJSON { + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModeJSON: return writeJSON(os.Stdout, info) + case output.ModePlain: + return cmd.printText(info) } - return cmd.printText(info) + return nil } func (cmd *InfoCmd) fetchInfo( diff --git a/cmd/features/info_manifest.go b/cmd/features/info_manifest.go index 1a59c12df..30433cd65 100644 --- a/cmd/features/info_manifest.go +++ b/cmd/features/info_manifest.go @@ -7,6 +7,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -15,8 +16,6 @@ import ( type InfoManifestCmd struct { *flags.GlobalFlags - - Output string } func NewInfoManifestCmd(globalFlags *flags.GlobalFlags) *cobra.Command { @@ -36,16 +35,10 @@ ghcr.io/devcontainers/features/go:1 and outputs the OCI image manifest.`, }, } - manifestCmd.Flags().StringVar(&cmd.Output, "output", "json", "Output format (text or json)") - return manifestCmd } func (cmd *InfoManifestCmd) Run(featureID string) error { - if err := validateOutputFormat(cmd.Output); err != nil { - return err - } - ref, err := name.ParseReference(featureID) if err != nil { return fmt.Errorf("invalid feature reference %q: %w", featureID, err) @@ -56,10 +49,17 @@ func (cmd *InfoManifestCmd) Run(featureID string) error { return fmt.Errorf("fetch manifest: %w", err) } - if cmd.Output == outputJSON { + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModeJSON: return writeJSON(os.Stdout, manifest) + case output.ModePlain: + return cmd.printText(manifest) } - return cmd.printText(manifest) + return nil } func (cmd *InfoManifestCmd) printText(manifest *v1.Manifest) error { diff --git a/cmd/features/info_manifest_test.go b/cmd/features/info_manifest_test.go index a05910dc7..6116f4d9c 100644 --- a/cmd/features/info_manifest_test.go +++ b/cmd/features/info_manifest_test.go @@ -3,18 +3,12 @@ package features import ( "testing" + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestInfoManifestCmd_FlagDefaults(t *testing.T) { - cmd := NewInfoManifestCmd(nil) - - outputFlag := cmd.Flags().Lookup("output") - require.NotNil(t, outputFlag) - assert.Equal(t, "json", outputFlag.DefValue) -} - func TestInfoManifestCmd_RequiresExactlyOneArg(t *testing.T) { cmd := NewInfoManifestCmd(nil) assert.NotNil(t, cmd.Args) @@ -40,15 +34,8 @@ func TestInfoManifestCmd_RequiresExactlyOneArg(t *testing.T) { } } -func TestInfoManifestCmd_InvalidOutputFormat(t *testing.T) { - cmd := &InfoManifestCmd{Output: "xml"} - err := cmd.Run("ghcr.io/devcontainers/features/go:1") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") -} - func TestInfoManifestCmd_InvalidFeatureReference(t *testing.T) { - cmd := &InfoManifestCmd{Output: outputJSON} + cmd := &InfoManifestCmd{GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModeJSON}} err := cmd.Run("not a valid reference!!!") require.Error(t, err) assert.Contains(t, err.Error(), "invalid feature reference") diff --git a/cmd/features/info_tags.go b/cmd/features/info_tags.go index f89ae5392..0f21ef962 100644 --- a/cmd/features/info_tags.go +++ b/cmd/features/info_tags.go @@ -5,6 +5,7 @@ import ( "os" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" @@ -12,8 +13,6 @@ import ( type InfoTagsCmd struct { *flags.GlobalFlags - - Output string } type tagsOutput struct { @@ -37,16 +36,10 @@ queries the registry for all available image tags.`, }, } - tagsCmd.Flags().StringVar(&cmd.Output, "output", "text", "Output format (text or json)") - return tagsCmd } func (cmd *InfoTagsCmd) Run(featureID string) error { - if err := validateOutputFormat(cmd.Output); err != nil { - return err - } - ref, err := name.ParseReference(featureID) if err != nil { return fmt.Errorf("invalid feature reference %q: %w", featureID, err) @@ -57,20 +50,26 @@ func (cmd *InfoTagsCmd) Run(featureID string) error { return fmt.Errorf("list tags: %w", err) } - if cmd.Output == outputJSON { - return writeJSON(os.Stdout, &tagsOutput{Tags: tags}) + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err } + switch mode { + case output.ModeJSON: + return writeJSON(os.Stdout, &tagsOutput{Tags: tags}) + case output.ModePlain: + if len(tags) == 0 { + _, _ = fmt.Fprintln(os.Stdout, "No tags found.") + return nil + } - if len(tags) == 0 { - _, _ = fmt.Fprintln(os.Stdout, "No tags found.") + _, _ = fmt.Fprintln(os.Stdout, "Available Tags:") + rows := make([][]string, 0, len(tags)) + for _, tag := range tags { + rows = append(rows, []string{tag}) + } + table.Print([]string{"Tag"}, rows) return nil } - - _, _ = fmt.Fprintln(os.Stdout, "Available Tags:") - rows := make([][]string, 0, len(tags)) - for _, tag := range tags { - rows = append(rows, []string{tag}) - } - table.Print([]string{"Tag"}, rows) return nil } diff --git a/cmd/features/info_tags_test.go b/cmd/features/info_tags_test.go index 962553c97..e8d7d8805 100644 --- a/cmd/features/info_tags_test.go +++ b/cmd/features/info_tags_test.go @@ -3,18 +3,12 @@ package features import ( "testing" + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestInfoTagsCmd_FlagDefaults(t *testing.T) { - cmd := NewInfoTagsCmd(nil) - - outputFlag := cmd.Flags().Lookup("output") - require.NotNil(t, outputFlag) - assert.Equal(t, outputText, outputFlag.DefValue) -} - func TestInfoTagsCmd_RequiresExactlyOneArg(t *testing.T) { cmd := NewInfoTagsCmd(nil) assert.NotNil(t, cmd.Args) @@ -23,15 +17,8 @@ func TestInfoTagsCmd_RequiresExactlyOneArg(t *testing.T) { assert.Error(t, cmd.Args(cmd, []string{"first", "second"})) } -func TestInfoTagsCmd_InvalidOutputFormat(t *testing.T) { - cmd := &InfoTagsCmd{Output: "csv"} - err := cmd.Run("ghcr.io/devcontainers/features/go:1") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") -} - func TestInfoTagsCmd_InvalidFeatureReference(t *testing.T) { - cmd := &InfoTagsCmd{Output: outputText} + cmd := &InfoTagsCmd{GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}} err := cmd.Run("not a valid reference!!!") require.Error(t, err) assert.Contains(t, err.Error(), "invalid feature reference") diff --git a/cmd/features/info_test.go b/cmd/features/info_test.go index d311657b5..0e10dc741 100644 --- a/cmd/features/info_test.go +++ b/cmd/features/info_test.go @@ -3,6 +3,8 @@ package features import ( "testing" + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -10,10 +12,6 @@ import ( func TestInfoCmd_FlagDefaults(t *testing.T) { cmd := NewInfoCmd(nil) - outputFlag := cmd.Flags().Lookup("output") - require.NotNil(t, outputFlag) - assert.Equal(t, "text", outputFlag.DefValue) - showTagsFlag := cmd.Flags().Lookup("show-tags") require.NotNil(t, showTagsFlag) assert.Equal(t, "false", showTagsFlag.DefValue) @@ -25,7 +23,7 @@ func TestInfoCmd_FlagDefaults(t *testing.T) { func TestInfoCmd_AllFlagsRegistered(t *testing.T) { cmd := NewInfoCmd(nil) - expected := []string{"output", "show-tags", "show-dependencies"} + expected := []string{"show-tags", "show-dependencies"} for _, name := range expected { assert.NotNil(t, cmd.Flags().Lookup(name), "flag %q should be registered", name) } @@ -39,19 +37,9 @@ func TestInfoCmd_RequiresExactlyOneArg(t *testing.T) { assert.Error(t, cmd.Args(cmd, []string{"one", "two"})) } -func TestInfoCmd_InvalidOutputFormat(t *testing.T) { - infoCmd := &InfoCmd{ - Output: "yaml", - } - err := infoCmd.Run("ghcr.io/devcontainers/features/go:1") - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") - assert.Contains(t, err.Error(), "yaml") -} - func TestInfoCmd_InvalidFeatureReference(t *testing.T) { infoCmd := &InfoCmd{ - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := infoCmd.Run("not a valid reference!!!") require.Error(t, err) diff --git a/cmd/features/output.go b/cmd/features/output.go index 19d5dce20..a34f1f9cc 100644 --- a/cmd/features/output.go +++ b/cmd/features/output.go @@ -7,26 +7,10 @@ import ( ) const ( - outputJSON = "json" - outputText = "text" - outputYAML = "yaml" - headerFeature = "Feature" headerValue = "Value" ) -func validateOutputFormat(format string) error { - if format != outputText && format != outputJSON { - return fmt.Errorf( - "invalid output format %q: must be %q or %q", - format, - outputText, - outputJSON, - ) - } - return nil -} - func writeJSON(w io.Writer, v any) error { data, err := json.MarshalIndent(v, "", " ") if err != nil { diff --git a/cmd/features/package.go b/cmd/features/package.go index ae9a7b1fe..0b687dfe6 100644 --- a/cmd/features/package.go +++ b/cmd/features/package.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/extract" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -20,7 +21,6 @@ type PackageCmd struct { Target string OutputFolder string ForceCleanOutputFolder bool - Output string } type packageResult struct { @@ -58,17 +58,14 @@ and creates gzipped tar archives suitable for OCI distribution.`, &cmd.ForceCleanOutputFolder, "force-clean-output-folder", false, "Clean output folder before writing", ) - packageCmd.Flags().StringVar( - &cmd.Output, "output", "text", - "Output format (text or json)", - ) _ = packageCmd.MarkFlagRequired("target") return packageCmd } func (cmd *PackageCmd) Run() error { - if err := validateOutputFormat(cmd.Output); err != nil { + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { return err } @@ -96,7 +93,7 @@ func (cmd *PackageCmd) Run() error { results = append(results, result) } - if cmd.Output == outputJSON { + if mode == output.ModeJSON { return writeJSON(os.Stdout, results) } diff --git a/cmd/features/package_test.go b/cmd/features/package_test.go index 0475abf24..c53713399 100644 --- a/cmd/features/package_test.go +++ b/cmd/features/package_test.go @@ -8,7 +8,9 @@ import ( "path/filepath" "testing" + "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -29,10 +31,6 @@ func TestPackageCmd_FlagDefaults(t *testing.T) { forceCleanFlag := cmd.Flags().Lookup("force-clean-output-folder") require.NotNil(t, forceCleanFlag) assert.Equal(t, "false", forceCleanFlag.DefValue) - - outputFlag := cmd.Flags().Lookup("output") - require.NotNil(t, outputFlag) - assert.Equal(t, "text", outputFlag.DefValue) } func TestPackageCmd_AllFlagsRegistered(t *testing.T) { @@ -41,7 +39,6 @@ func TestPackageCmd_AllFlagsRegistered(t *testing.T) { "target", "output-folder", "force-clean-output-folder", - "output", } for _, name := range expected { assert.NotNil(t, cmd.Flags().Lookup(name), "flag %q should be registered", name) @@ -164,7 +161,7 @@ func TestPackageCmd_ForceCleanOutputFolder(t *testing.T) { Target: targetDir, OutputFolder: outputDir, ForceCleanOutputFolder: true, - Output: outputText, + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() @@ -176,16 +173,6 @@ func TestPackageCmd_ForceCleanOutputFolder(t *testing.T) { assert.FileExists(t, filepath.Join(outputDir, "devcontainer-feature-feat.tgz")) } -func TestPackageCmd_InvalidOutputFormat(t *testing.T) { - cmd := &PackageCmd{ - Target: "/tmp", - Output: outputYAML, - } - err := cmd.Run() - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") -} - func TestPackageCmd_InvalidFeatureID(t *testing.T) { tests := []struct { name string diff --git a/cmd/features/resolvedeps.go b/cmd/features/resolvedeps.go index 622de36d8..fc4cebfa9 100644 --- a/cmd/features/resolvedeps.go +++ b/cmd/features/resolvedeps.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -18,7 +19,6 @@ type ResolveDepsCmd struct { WorkspaceFolder string Config string - Output string } type resolvedFeature struct { @@ -51,16 +51,14 @@ install order based on dependency declarations and install ordering.`, &cmd.Config, "config", "", "Path to specific devcontainer.json (optional)", ) - resolveDepsCmd.Flags().StringVar( - &cmd.Output, "output", "text", "Output format (text or json)", - ) _ = resolveDepsCmd.MarkFlagRequired("workspace-folder") return resolveDepsCmd } func (cmd *ResolveDepsCmd) Run() error { - if err := validateOutputFormat(cmd.Output); err != nil { + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { return err } @@ -76,7 +74,7 @@ func (cmd *ResolveDepsCmd) Run() error { } if len(devContainerConfig.Features) == 0 { - return cmd.printEmpty() + return cmd.printEmpty(mode) } sorted, err := feature.ResolveFeatureOrder(devContainerConfig) @@ -86,14 +84,14 @@ func (cmd *ResolveDepsCmd) Run() error { resolved := buildResolvedList(sorted) - if cmd.Output == outputJSON { + if mode == output.ModeJSON { return writeJSON(os.Stdout, resolved) } return cmd.printText(resolved) } -func (cmd *ResolveDepsCmd) printEmpty() error { - if cmd.Output == outputJSON { +func (cmd *ResolveDepsCmd) printEmpty(mode string) error { + if mode == output.ModeJSON { _, err := fmt.Fprintln(os.Stdout, "[]") return err } diff --git a/cmd/features/resolvedeps_test.go b/cmd/features/resolvedeps_test.go index a972dce1f..d805cfbb9 100644 --- a/cmd/features/resolvedeps_test.go +++ b/cmd/features/resolvedeps_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "testing" + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,7 +27,7 @@ func TestResolveDepsCmd_NoFeatures(t *testing.T) { cmd := &ResolveDepsCmd{ WorkspaceFolder: workspaceDir, - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() @@ -35,7 +37,7 @@ func TestResolveDepsCmd_NoFeatures(t *testing.T) { func TestResolveDepsCmd_MissingWorkspace(t *testing.T) { cmd := &ResolveDepsCmd{ WorkspaceFolder: "/nonexistent/path/12345", - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() @@ -55,25 +57,13 @@ func TestResolveDepsCmd_ExplicitConfig(t *testing.T) { cmd := &ResolveDepsCmd{ WorkspaceFolder: tmpDir, Config: configPath, - Output: outputJSON, + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModeJSON}, } err := cmd.Run() require.NoError(t, err) } -func TestResolveDepsCmd_InvalidOutputFormat(t *testing.T) { - cmd := &ResolveDepsCmd{ - WorkspaceFolder: t.TempDir(), - Output: "yaml", - } - - err := cmd.Run() - assert.Error(t, err) - assert.Contains(t, err.Error(), "invalid output format") - assert.Contains(t, err.Error(), "yaml") -} - func TestResolveDepsCmd_WithOptions(t *testing.T) { workspaceDir := t.TempDir() devcontainerDir := filepath.Join(workspaceDir, ".devcontainer") @@ -103,7 +93,7 @@ func TestResolveDepsCmd_WithOptions(t *testing.T) { cmd := &ResolveDepsCmd{ WorkspaceFolder: workspaceDir, - Output: "text", + GlobalFlags: &flags.GlobalFlags{ResultFormat: output.ModePlain}, } err := cmd.Run() diff --git a/cmd/flags/flags.go b/cmd/flags/flags.go index f4e90adbc..b7568391a 100644 --- a/cmd/flags/flags.go +++ b/cmd/flags/flags.go @@ -33,8 +33,8 @@ func SetGlobalFlags(flags *flag.FlagSet) *GlobalFlags { flags.StringVar( &globalFlags.ResultFormat, "result-format", - "json", - "The result output format. Can be json, plain, or auto", + "auto", + "The result output format. Can be json, plain, or auto (auto picks plain on a TTY and json when piped)", ) flags.StringVar( &globalFlags.LogOutput, diff --git a/cmd/ide/list.go b/cmd/ide/list.go index 3304ff7a8..5d57e47a7 100644 --- a/cmd/ide/list.go +++ b/cmd/ide/list.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/ide/ideparse" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -17,8 +18,6 @@ import ( // ListCmd holds the list cmd flags. type ListCmd struct { *flags.GlobalFlags - - Output string } // NewListCmd creates a new command. @@ -36,8 +35,6 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return listCmd } @@ -54,8 +51,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for _, entry := range ideparse.AllowedIDEs { tableEntries = append(tableEntries, []string{ @@ -71,7 +72,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { "Name", "Default", }, tableEntries) - case "json": + case output.ModeJSON: ides := []IDEWithDefault{} for _, entry := range ideparse.AllowedIDEs { ides = append(ides, IDEWithDefault{ @@ -85,11 +86,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/ide/options.go b/cmd/ide/options.go index 30b49c68a..dbcedd98f 100644 --- a/cmd/ide/options.go +++ b/cmd/ide/options.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/ide" "github.com/devsy-org/devsy/pkg/ide/ideparse" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -17,8 +18,6 @@ import ( // OptionsCmd holds the options cmd flags. type OptionsCmd struct { *flags.GlobalFlags - - Output string } // NewOptionsCmd creates a new command. @@ -38,8 +37,6 @@ func NewOptionsCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - optionsCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return optionsCmd } @@ -62,8 +59,12 @@ func (cmd *OptionsCmd) Run(ctx context.Context, ide string) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for optionName, entry := range ideOptions { value := values[optionName].Value @@ -84,7 +85,7 @@ func (cmd *OptionsCmd) Run(ctx context.Context, ide string) error { "Default", "Value", }, tableEntries) - case "json": + case output.ModeJSON: options := map[string]optionWithValue{} for optionName, entry := range ideOptions { options[optionName] = optionWithValue{ @@ -98,11 +99,6 @@ func (cmd *OptionsCmd) Run(ctx context.Context, ide string) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/list.go b/cmd/list.go index c5a490ca4..f6c9ce279 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -18,7 +19,6 @@ import ( type ListCmd struct { *flags.GlobalFlags - Output string SkipPro bool } @@ -37,8 +37,6 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") listCmd.Flags().BoolVar(&cmd.SkipPro, "skip-pro", false, "Don't list pro workspaces") return listCmd } @@ -55,8 +53,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } - switch cmd.Output { - case "json": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModeJSON: sort.SliceStable(workspaces, func(i, j int) bool { return workspaces[i].LastUsedTimestamp.Unix() > workspaces[j].LastUsedTimestamp.Unix() }) @@ -65,7 +67,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - case "plain": + case output.ModePlain: tableEntries := [][]string{} sort.SliceStable(workspaces, func(i, j int) bool { return workspaces[i].LastUsedTimestamp.Unix() > workspaces[j].LastUsedTimestamp.Unix() @@ -97,11 +99,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { "Age", "Pro", }, tableEntries) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/machine/list.go b/cmd/machine/list.go index be4975f39..23ad0bed1 100644 --- a/cmd/machine/list.go +++ b/cmd/machine/list.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -18,8 +19,6 @@ import ( // ListCmd holds the configuration. type ListCmd struct { *flags.GlobalFlags - - Output string } // NewListCmd creates a new list command. @@ -36,8 +35,6 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return listCmd } @@ -58,8 +55,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for _, entry := range entries { machineConfig, err := provider.LoadMachineConfig( @@ -85,7 +86,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { "Provider", "Age", }, tableEntries) - case "json": + case output.ModeJSON: tableEntries := []*provider.Machine{} for _, entry := range entries { machineConfig, err := provider.LoadMachineConfig( @@ -106,11 +107,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/machine/status.go b/cmd/machine/status.go index acf8882f6..bfb5a729f 100644 --- a/cmd/machine/status.go +++ b/cmd/machine/status.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -16,8 +17,6 @@ import ( // StatusCmd holds the configuration. type StatusCmd struct { *flags.GlobalFlags - - Output string } // NewStatusCmd creates a new status command. @@ -33,7 +32,6 @@ func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - statusCmd.Flags().StringVar(&cmd.Output, "output", "plain", "Output format: plain or json") return statusCmd } @@ -55,8 +53,12 @@ func (cmd *StatusCmd) Run(ctx context.Context, args []string) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: switch machineStatus { case client.StatusStopped: log.Infof( @@ -77,7 +79,7 @@ func (cmd *StatusCmd) Run(ctx context.Context, args []string) error { default: log.Infof("Machine '%s' is '%s'", machineClient.Machine(), machineStatus) } - case "json": + case output.ModeJSON: out, err := json.Marshal(struct { ID string `json:"id,omitempty"` Context string `json:"context,omitempty"` @@ -94,11 +96,6 @@ func (cmd *StatusCmd) Run(ctx context.Context, args []string) error { } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/pro/list.go b/cmd/pro/list.go index d9e47c99b..b8674b955 100644 --- a/cmd/pro/list.go +++ b/cmd/pro/list.go @@ -9,6 +9,7 @@ import ( proflags "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/workspace" @@ -19,8 +20,7 @@ import ( type ListCmd struct { proflags.GlobalFlags - Output string - Login bool + Login bool } // NewListCmd creates a new command. @@ -38,8 +38,6 @@ func NewListCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") listCmd.Flags(). BoolVar(&cmd.Login, "login", false, "Check if the user is logged into the pro instance") return listCmd @@ -57,8 +55,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for _, proInstance := range proInstances { entry := []string{ @@ -87,7 +89,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { } table.Print(tableHeaders, tableEntries) - case "json": + case output.ModeJSON: tableEntries := []*proTableEntry{} for _, proInstance := range proInstances { entry := &proTableEntry{ @@ -112,11 +114,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/provider/list.go b/cmd/provider/list.go index 9a0a6c6e4..658134334 100644 --- a/cmd/provider/list.go +++ b/cmd/provider/list.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/types" "github.com/devsy-org/devsy/pkg/workspace" @@ -18,8 +19,6 @@ import ( // ListCmd holds the list cmd flags. type ListCmd struct { *flags.GlobalFlags - - Output string } // NewListCmd creates a new command. @@ -37,8 +36,6 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return listCmd } @@ -65,8 +62,12 @@ func (cmd *ListCmd) Run(ctx context.Context) error { configuredProviders = map[string]*config.ProviderConfig{} } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: tableEntries := [][]string{} for _, entry := range providers { tableEntries = append(tableEntries, []string{ @@ -88,7 +89,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error { "Initialized", "Description", }, tableEntries) - case "json": + case output.ModeJSON: retMap := map[string]ProviderWithDefault{} for k, entry := range providers { var dynamicOptions map[string]*types.Option @@ -109,11 +110,6 @@ func (cmd *ListCmd) Run(ctx context.Context) error { return err } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/provider/options.go b/cmd/provider/options.go index 3a8ef97d3..ecd829c2c 100644 --- a/cmd/provider/options.go +++ b/cmd/provider/options.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "maps" + "os" "sort" "strconv" @@ -12,6 +13,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/types" "github.com/devsy-org/devsy/pkg/workspace" @@ -23,7 +25,6 @@ type OptionsCmd struct { *flags.GlobalFlags Hidden bool - Output string } // NewOptionsCmd creates a new command. @@ -51,8 +52,6 @@ func NewOptionsCmd(flags *flags.GlobalFlags) *cobra.Command { optionsCmd.Flags(). BoolVar(&cmd.Hidden, "hidden", false, "If true, will also show hidden options.") - optionsCmd.Flags(). - StringVar(&cmd.Output, "output", "plain", "The output format to use. Can be json or plain") return optionsCmd } @@ -93,7 +92,11 @@ func (cmd *OptionsCmd) Run(ctx context.Context, args []string) error { return err } - return printOptions(devsyConfig, providerWithOptions, cmd.Output, cmd.Hidden) + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + return printOptions(devsyConfig, providerWithOptions, mode, cmd.Hidden) } func printOptions( @@ -105,60 +108,76 @@ func printOptions( entryOptions := devsyConfig.ProviderOptions(provider.Config.Name) dynamicOptions := devsyConfig.DynamicProviderOptionDefinitions(provider.Config.Name) srcOptions := MergeDynamicOptions(provider.Config.Options, dynamicOptions) - if format == "plain" { - tableEntries := [][]string{} - for optionName, entry := range srcOptions { - if !showHidden && entry.Hidden { - continue - } - - value := entryOptions[optionName].Value - if value != "" && entry.Password { - value = "********" - } - - tableEntries = append(tableEntries, []string{ - optionName, - strconv.FormatBool(entry.Required), - entry.Description, - entry.Default, - value, - }) + switch format { + case output.ModePlain: + printOptionsPlain(srcOptions, entryOptions, showHidden) + case output.ModeJSON: + return printOptionsJSON(srcOptions, entryOptions, showHidden) + } + + return nil +} + +func printOptionsPlain( + srcOptions map[string]*types.Option, + entryOptions map[string]config.OptionValue, + showHidden bool, +) { + tableEntries := [][]string{} + for optionName, entry := range srcOptions { + if !showHidden && entry.Hidden { + continue } - sort.SliceStable(tableEntries, func(i, j int) bool { - return tableEntries[i][0] < tableEntries[j][0] + + value := entryOptions[optionName].Value + if value != "" && entry.Password { + value = "********" + } + + tableEntries = append(tableEntries, []string{ + optionName, + strconv.FormatBool(entry.Required), + entry.Description, + entry.Default, + value, }) + } + sort.SliceStable(tableEntries, func(i, j int) bool { + return tableEntries[i][0] < tableEntries[j][0] + }) + + table.Print([]string{ + "Name", + "Required", + "Description", + "Default", + "Value", + }, tableEntries) +} - table.Print([]string{ - "Name", - "Required", - "Description", - "Default", - "Value", - }, tableEntries) - } else if format == "json" { - options := map[string]optionWithValue{} - for optionName, entry := range srcOptions { - if !showHidden && entry.Hidden { - continue - } - - options[optionName] = optionWithValue{ - Option: *entry, - Children: entryOptions[optionName].Children, - Value: entryOptions[optionName].Value, - } +func printOptionsJSON( + srcOptions map[string]*types.Option, + entryOptions map[string]config.OptionValue, + showHidden bool, +) error { + options := map[string]optionWithValue{} + for optionName, entry := range srcOptions { + if !showHidden && entry.Hidden { + continue } - out, err := json.MarshalIndent(options, "", " ") - if err != nil { - return err + options[optionName] = optionWithValue{ + Option: *entry, + Children: entryOptions[optionName].Children, + Value: entryOptions[optionName].Value, } - fmt.Print(string(out)) - } else { - return fmt.Errorf("unexpected output format, choose either json or plain. Got %s", format) } + out, err := json.MarshalIndent(options, "", " ") + if err != nil { + return err + } + _, _ = os.Stdout.Write(out) return nil } diff --git a/cmd/setup.go b/cmd/setup.go index 0c0676c10..33a273c05 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -70,7 +70,11 @@ func NewSetUpCmd(f *flags.GlobalFlags) *cobra.Command { // Run executes the set-up command logic. func (cmd *SetUpCmd) Run(ctx context.Context) error { - emitJSON := output.ResolveMode(cmd.ResultFormat) == output.ModeJSON + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + emitJSON := mode == output.ModeJSON helper := &docker.DockerHelper{DockerCommand: cmd.resolveDockerPath()} diff --git a/cmd/status.go b/cmd/status.go index f3b20a0c2..0abdf0816 100644 --- a/cmd/status.go +++ b/cmd/status.go @@ -12,6 +12,7 @@ import ( "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/log" + "github.com/devsy-org/devsy/pkg/output" workspace2 "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -21,7 +22,6 @@ type StatusCmd struct { *flags.GlobalFlags client2.StatusOptions - Output string Timeout string } @@ -73,7 +73,6 @@ func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command { statusCmd.Flags(). BoolVar(&cmd.ContainerStatus, "container-status", true, "If enabled shows the workspace container status as well") - statusCmd.Flags().StringVar(&cmd.Output, "output", "plain", "Status shows the workspace status") statusCmd.Flags(). StringVar(&cmd.Timeout, "timeout", "30s", "The timeout to wait until the status can be retrieved") return statusCmd @@ -102,8 +101,12 @@ func (cmd *StatusCmd) Run( return err } - switch cmd.Output { - case "plain": + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + switch mode { + case output.ModePlain: switch instanceStatus { case client2.StatusStopped: log.Infof( @@ -129,7 +132,7 @@ func (cmd *StatusCmd) Run( default: log.Infof("Workspace '%s' is '%s'", client.Workspace(), instanceStatus) } - case "json": + case output.ModeJSON: out, err := json.Marshal(&client2.WorkspaceStatus{ ID: client.Workspace(), Context: client.Context(), @@ -141,11 +144,6 @@ func (cmd *StatusCmd) Run( } fmt.Print(string(out)) - default: - return fmt.Errorf( - "unexpected output format, choose either json or plain. Got %s", - cmd.Output, - ) } return nil diff --git a/cmd/up/up.go b/cmd/up/up.go index 9ba00bf35..8ffc1becd 100644 --- a/cmd/up/up.go +++ b/cmd/up/up.go @@ -69,7 +69,11 @@ func (cmd *UpCmd) Run( //nolint:cyclop ) error { cmd.prepareWorkspace(client) - emitJSON := output.ResolveMode(cmd.ResultFormat) == output.ModeJSON + mode, err := output.ResolveMode(cmd.ResultFormat) + if err != nil { + return err + } + emitJSON := mode == output.ModeJSON wctx, err := cmd.executeDevsyUp(ctx, devsyConfig, client) if err != nil { diff --git a/desktop/e2e/fixtures/mock-devsy.cjs b/desktop/e2e/fixtures/mock-devsy.cjs index 541148a5d..c3e465d93 100755 --- a/desktop/e2e/fixtures/mock-devsy.cjs +++ b/desktop/e2e/fixtures/mock-devsy.cjs @@ -2,7 +2,7 @@ "use strict" // Mock devsy binary for e2e tests (cross-platform Node.js version) // Returns canned JSON responses for each subcommand -// Handles --output json, --skip-pro, and other flags the app sends +// Handles --result-format json, --skip-pro, and other flags the app sends // Persists state to a JSON file so provider/workspace CRUD works across calls const fs = require("node:fs") @@ -104,7 +104,7 @@ let nameFlag = "" let i = 0 while (i < args.length) { const arg = args[i] - if (arg === "--output") { + if (arg === "--result-format") { i += 2 continue } diff --git a/desktop/src/main/__tests__/cli.test.ts b/desktop/src/main/__tests__/cli.test.ts index 5eefb215f..8ea522d9d 100644 --- a/desktop/src/main/__tests__/cli.test.ts +++ b/desktop/src/main/__tests__/cli.test.ts @@ -35,7 +35,7 @@ describe("CliRunner", () => { expect(result).toEqual([{ id: "ws-1" }]) expect(mockExecFile).toHaveBeenCalledWith( "/usr/local/bin/devsy", - ["list", "--skip-pro", "--output", "json"], + ["list", "--skip-pro", "--result-format", "json"], expect.objectContaining({ env: expect.any(Object) }), expect.any(Function), ) @@ -92,7 +92,7 @@ describe("CliRunner", () => { await jsCli.run(["list"]) expect(mockExecFile).toHaveBeenCalledWith( "node", - ["/tmp/mock.cjs", "list", "--output", "json"], + ["/tmp/mock.cjs", "list", "--result-format", "json"], expect.objectContaining({ env: expect.any(Object) }), expect.any(Function), ) diff --git a/desktop/src/main/cli.ts b/desktop/src/main/cli.ts index 608271f96..0d2061b81 100644 --- a/desktop/src/main/cli.ts +++ b/desktop/src/main/cli.ts @@ -71,7 +71,7 @@ export class CliRunner { async run(args: string[]): Promise { await this.acquire() try { - const fullArgs = [...this.prefixArgs, ...args, "--output", "json"] + const fullArgs = [...this.prefixArgs, ...args, "--result-format", "json"] const { stdout } = await execFile(this.execPath, fullArgs, { env: this.env, }) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 5c746810a..0caff7ec0 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -59,7 +59,7 @@ export function registerIpcHandlers(deps: IpcDependencies): { tunnelProcesses: M return cli.runRaw([ "status", args.workspaceId, - "--output", + "--result-format", "json", "--timeout", "5s", @@ -219,7 +219,7 @@ export function registerIpcHandlers(deps: IpcDependencies): { tunnelProcesses: M }) ipcMain.handle("machine_status", async (_event, args: { id: string }) => { - return cli.runRaw(["machine", "status", args.id, "--output", "json"]) + return cli.runRaw(["machine", "status", args.id, "--result-format", "json"]) }) // ── Contexts ── diff --git a/e2e/framework/command.go b/e2e/framework/command.go index 6d92b2542..bf4199523 100644 --- a/e2e/framework/command.go +++ b/e2e/framework/command.go @@ -13,6 +13,12 @@ import ( "github.com/devsy-org/devsy/pkg/workspace" ) +const ( + flagResultFormat = "--result-format" + formatJSON = "json" + cmdList = "list" +) + func (f *Framework) FindWorkspace(ctx context.Context, id string) (*provider2.Workspace, error) { list, err := f.DevsyListParsed(ctx) if err != nil { @@ -46,7 +52,7 @@ func (f *Framework) DevsyListParsed(ctx context.Context) ([]*provider2.Workspace // DevsyList executes the `devsy list` command in the test framework. func (f *Framework) DevsyList(ctx context.Context) (string, error) { - listArgs := []string{"list", "--output", "json"} + listArgs := []string{cmdList, flagResultFormat, formatJSON} out, _, err := f.ExecCommandCapture(ctx, listArgs) if err != nil { @@ -184,7 +190,7 @@ func (f *Framework) DevsyProviderOptionsCheckNamespaceDescription( } func (f *Framework) DevsyProviderList(ctx context.Context, extraArgs ...string) error { - baseArgs := []string{"provider", "list"} + baseArgs := []string{"provider", cmdList} err := f.ExecCommand(ctx, false, true, "", append(baseArgs, extraArgs...)) if err != nil { return fmt.Errorf("devsy provider list failed: %s", err.Error()) @@ -209,7 +215,7 @@ func (f *Framework) DevsyStatus( ctx context.Context, extraArgs ...string, ) (client.WorkspaceStatus, error) { - baseArgs := []string{"status", "--output", "json"} + baseArgs := []string{"status", flagResultFormat, formatJSON} baseArgs = append(baseArgs, extraArgs...) stdout, err := f.ExecCommandOutput(ctx, baseArgs) if err != nil { @@ -308,7 +314,7 @@ func (f *Framework) DevsyProviderOptionsJSON( ctx context.Context, providerName string, ) (string, error) { - args := []string{"provider", "options", providerName, "--output", "json"} + args := []string{"provider", "options", providerName, flagResultFormat, formatJSON} stdout, _, err := f.ExecCommandCapture(ctx, args) if err != nil { return "", fmt.Errorf("devsy provider options failed: %s", err.Error()) @@ -500,7 +506,7 @@ func (f *Framework) DevsyLogs(ctx context.Context, workspace string) (string, er } func (f *Framework) DevsyIDEList(ctx context.Context, extraArgs ...string) (string, error) { - baseArgs := []string{"ide", "list"} + baseArgs := []string{"ide", cmdList} return f.ExecCommandOutput(ctx, append(baseArgs, extraArgs...)) } diff --git a/e2e/tests/context/context.go b/e2e/tests/context/context.go index e99cbb95e..18a0615ed 100644 --- a/e2e/tests/context/context.go +++ b/e2e/tests/context/context.go @@ -74,7 +74,7 @@ var _ = ginkgo.Describe( err = f.DevsyIDEUse(ctx, ideIntelliJ, "--context", contextB) framework.ExpectNoError(err) - output, err := f.DevsyIDEList(ctx, "--output", "json") + output, err := f.DevsyIDEList(ctx, "--result-format", "json") framework.ExpectNoError(err) var ides []map[string]any @@ -90,7 +90,7 @@ var _ = ginkgo.Describe( } } - output, err = f.DevsyIDEList(ctx, "--context", contextB, "--output", "json") + output, err = f.DevsyIDEList(ctx, "--context", contextB, "--result-format", "json") framework.ExpectNoError(err) err = json.Unmarshal([]byte(output), &ides) @@ -113,7 +113,7 @@ var _ = ginkgo.Describe( ginkgo.GinkgoT().Setenv("DEVSY_CONTEXT", contextB) - output, err = f.DevsyIDEList(ctx, "--output", "json") + output, err = f.DevsyIDEList(ctx, "--result-format", "json") framework.ExpectNoError(err) err = json.Unmarshal([]byte(output), &ides) diff --git a/e2e/tests/features/features.go b/e2e/tests/features/features.go index 5ca3665ad..4842316aa 100644 --- a/e2e/tests/features/features.go +++ b/e2e/tests/features/features.go @@ -83,6 +83,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ "features", "resolve-dependencies", "--workspace-folder", workspaceDir, + flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -122,29 +123,13 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ "features", "resolve-dependencies", "--workspace-folder", workspaceDir, - "--output", "json", + flagOutput, outputJSON, }) framework.ExpectNoError(err) var result []map[string]any gomega.Expect(json.Unmarshal([]byte(stdout), &result)).To(gomega.Succeed()) }, ginkgo.SpecTimeout(framework.TimeoutShort())) - - ginkgo.It("rejects invalid output format", func(ctx context.Context) { - f := framework.NewDefaultFramework(initialDir + "/bin") - - workspaceDir, err := os.MkdirTemp("", "e2e-resolve-deps-invalid-*") - framework.ExpectNoError(err) - ginkgo.DeferCleanup(func() { _ = os.RemoveAll(workspaceDir) }) - - _, stderr, err := f.ExecCommandCapture(ctx, []string{ - "features", "resolve-dependencies", - "--workspace-folder", workspaceDir, - "--output", "yaml", - }) - gomega.Expect(err).To(gomega.HaveOccurred()) - gomega.Expect(stderr).To(gomega.ContainSubstring("invalid output format")) - }, ginkgo.SpecTimeout(framework.TimeoutShort())) }) ginkgo.Describe("features generate-docs", func() { @@ -178,6 +163,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { "features", "generate-docs", "--project-folder", projectDir, "--output-folder", outputDir, + flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -225,6 +211,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { "--project-folder", projectDir, "--output-folder", outputDir, "--namespace", "ghcr.io/test/features", + flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -253,6 +240,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ "features", "info", featureRef, + flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -275,7 +263,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { }) stdout, _, err := f.ExecCommandCapture(ctx, []string{ - "features", "info", featureRef, "--output", "json", + "features", "info", featureRef, flagOutput, outputJSON, }) framework.ExpectNoError(err) @@ -333,7 +321,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { gomega.Expect(manifest).To(gomega.HaveKey("layers")) }, ginkgo.SpecTimeout(framework.TimeoutShort())) - ginkgo.It("returns text output with --output=text", func(ctx context.Context) { + ginkgo.It("returns text output with --result-format=plain", func(ctx context.Context) { f := framework.NewDefaultFramework(initialDir + "/bin") srv := httptest.NewServer(registry.New()) @@ -345,7 +333,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { pushFeatureWithAnnotations(featureRef, nil) stdout, _, err := f.ExecCommandCapture(ctx, []string{ - "features", "info", subCmdManifest, featureRef, "--output", "text", + "features", "info", subCmdManifest, featureRef, flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -427,6 +415,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ "features", "info", subCmdTags, featureRepo + ":1.0.0", + flagOutput, outputPlain, }) framework.ExpectNoError(err) @@ -450,7 +439,7 @@ var _ = ginkgo.Describe("features commands", ginkgo.Label("features"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ "features", "info", subCmdTags, featureRepo + ":1.0.0", - "--output", "json", + flagOutput, outputJSON, }) framework.ExpectNoError(err) diff --git a/e2e/tests/features/features_package_cmd.go b/e2e/tests/features/features_package_cmd.go index 919dd48bc..7b0cf3e68 100644 --- a/e2e/tests/features/features_package_cmd.go +++ b/e2e/tests/features/features_package_cmd.go @@ -19,8 +19,9 @@ const ( flagTarget = "--target" flagOutputFolder = "--output-folder" flagForceCleanOutput = "--force-clean-output-folder" - flagOutput = "--output" + flagOutput = "--result-format" outputJSON = "json" + outputPlain = "plain" featureNameGo = "go" featureNameNode = "node" featureVersion100 = "1.0.0" diff --git a/pkg/output/mode.go b/pkg/output/mode.go index c4166fd44..a1e81986c 100644 --- a/pkg/output/mode.go +++ b/pkg/output/mode.go @@ -1,24 +1,31 @@ package output -import "github.com/devsy-org/devsy/pkg/terminal" +import ( + "fmt" + + "github.com/devsy-org/devsy/pkg/terminal" +) const ( ModeJSON = "json" ModePlain = "plain" ) -func ResolveMode(flagValue string) string { +func ResolveMode(flagValue string) (string, error) { switch flagValue { case ModeJSON: - return ModeJSON + return ModeJSON, nil case ModePlain: - return ModePlain + return ModePlain, nil case "auto": if !terminal.IsTerminalOut { - return ModeJSON + return ModeJSON, nil } - return ModePlain + return ModePlain, nil default: - return ModeJSON + return "", fmt.Errorf( + "unexpected output format, choose json, plain, or auto. Got %q", + flagValue, + ) } } diff --git a/pkg/output/mode_test.go b/pkg/output/mode_test.go index fb7b85b53..880e7e44a 100644 --- a/pkg/output/mode_test.go +++ b/pkg/output/mode_test.go @@ -7,14 +7,20 @@ import ( ) func TestResolveMode_JSON(t *testing.T) { - got := ResolveMode("json") + got, err := ResolveMode("json") + if err != nil { + t.Fatalf("ResolveMode(\"json\") returned error: %v", err) + } if got != ModeJSON { t.Errorf("ResolveMode(\"json\") = %q, want %q", got, ModeJSON) } } func TestResolveMode_Plain(t *testing.T) { - got := ResolveMode("plain") + got, err := ResolveMode("plain") + if err != nil { + t.Fatalf("ResolveMode(\"plain\") returned error: %v", err) + } if got != ModePlain { t.Errorf("ResolveMode(\"plain\") = %q, want %q", got, ModePlain) } @@ -25,7 +31,10 @@ func TestResolveMode_Auto_NonTTY(t *testing.T) { terminal.IsTerminalOut = false defer func() { terminal.IsTerminalOut = orig }() - got := ResolveMode("auto") + got, err := ResolveMode("auto") + if err != nil { + t.Fatalf("ResolveMode(\"auto\") returned error: %v", err) + } if got != ModeJSON { t.Errorf("ResolveMode(\"auto\") with non-TTY = %q, want %q", got, ModeJSON) } @@ -36,15 +45,21 @@ func TestResolveMode_Auto_TTY(t *testing.T) { terminal.IsTerminalOut = true defer func() { terminal.IsTerminalOut = orig }() - got := ResolveMode("auto") + got, err := ResolveMode("auto") + if err != nil { + t.Fatalf("ResolveMode(\"auto\") returned error: %v", err) + } if got != ModePlain { t.Errorf("ResolveMode(\"auto\") with TTY = %q, want %q", got, ModePlain) } } func TestResolveMode_InvalidValue(t *testing.T) { - got := ResolveMode("bogus") - if got != ModeJSON { - t.Errorf("ResolveMode(\"bogus\") = %q, want %q", got, ModeJSON) + got, err := ResolveMode("bogus") + if err == nil { + t.Fatalf("ResolveMode(\"bogus\") expected error, got nil (value=%q)", got) + } + if got != "" { + t.Errorf("ResolveMode(\"bogus\") = %q, want empty string", got) } }