From fa2773b91346d282faa6d9925b43a54031f1ae29 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 01:26:00 -0500 Subject: [PATCH 1/7] refactor(flags): centralize CLI flag names and introduce a flag builder Introduce pkg/flags/names as the single source of truth for every devsy-owned CLI flag name, and pkg/flags builder for uniform flag registration. Flag names are now compile-checked constants rather than raw strings scattered across command definitions and self-invocation sites, so a rename is a single edit the compiler propagates everywhere. - pkg/flags/names: dependency-light leaf (imports only fmt) holding all flag-name constants plus Flag/FlagValue/FlagTrue/FlagFalse helpers, usable from both cmd/ and pkg/ self-invocation builders without cobra. names_test.go pins every constant to its literal wire string so an accidental value change fails the build rather than silently altering the CLI contract. - pkg/flags builder: String/Bool/Int/Int64/Duration/StringSlice/ StringArray/Value constructors with chainable Shorthand()/Hidden(), registered via Add/AddPersistent. - Convert all cmd/ and pkg/ flag registrations and devsy self-invocation command lines to the constants; extract GPG signing helpers into pkg/gpg/signing.go. BREAKING CHANGE: renames several flags for consistency (ignoring backward compatibility) and removes hidden legacy aliases on `up`: additional-features -> features configure-ssh -> ssh-configure default-user-env-probe -> user-env-probe extra-devcontainer-path -> devcontainer-overlay git-clone-recursive-submodules -> git-recurse-submodules gpg-agent-forwarding -> ssh-gpg-forwarding prebuild-repository -> prebuild-repo ssh-tunnel-mode -> ssh-tunnel update-remote-user-uid-default -> update-remote-user-uid removed aliases: --config, --override-config, --dotfiles-repository, --remove-existing-container (use the canonical flag names). Other wire names are unchanged; devsy self-invocation command lines were verified byte-identical. --- cmd/completion/suggestions.go | 5 +- cmd/config/apply.go | 19 +- cmd/config/read.go | 69 +--- cmd/context/create.go | 9 +- cmd/context/set_options.go | 9 +- cmd/context/use.go | 9 +- cmd/env_flags_test.go | 11 +- cmd/feature/generatedocs.go | 32 +- cmd/feature/info.go | 21 +- cmd/feature/info_test.go | 5 +- cmd/feature/outdated.go | 16 +- cmd/feature/package.go | 35 +- cmd/feature/package_test.go | 7 +- cmd/feature/publish.go | 30 +- cmd/feature/publish_test.go | 9 +- cmd/feature/resolvedeps.go | 25 +- cmd/feature/test.go | 45 +- cmd/feature/test_test.go | 15 +- cmd/feature/upgrade.go | 19 +- cmd/feature/upgrade_test.go | 7 +- cmd/flag_aliases_test.go | 103 +---- cmd/flags/flags.go | 46 ++- cmd/ide/set.go | 9 +- cmd/ide/use.go | 9 +- cmd/internal/agent_daemon.go | 17 +- cmd/internal/agent_docker_credentials.go | 10 +- cmd/internal/agentcontainer/browser_async.go | 9 +- .../agentcontainer/credentials_server.go | 36 +- cmd/internal/agentcontainer/daemon.go | 26 +- cmd/internal/agentcontainer/deferred_hooks.go | 32 +- cmd/internal/agentcontainer/post_attach.go | 17 +- cmd/internal/agentcontainer/setup.go | 100 +++-- cmd/internal/agentcontainer/ssh_server.go | 29 +- cmd/internal/agentcontainer/vscode_async.go | 20 +- cmd/internal/agentworkspace/build.go | 9 +- cmd/internal/agentworkspace/clean.go | 21 +- cmd/internal/agentworkspace/delete.go | 30 +- .../agentworkspace/install_dotfiles.go | 25 +- cmd/internal/agentworkspace/logs.go | 6 +- cmd/internal/agentworkspace/logs_daemon.go | 6 +- cmd/internal/agentworkspace/setup_gpg.go | 19 +- cmd/internal/agentworkspace/status.go | 9 +- cmd/internal/agentworkspace/stop.go | 9 +- cmd/internal/agentworkspace/up.go | 9 +- cmd/internal/agentworkspace/update_config.go | 10 +- cmd/internal/browser_tunnel.go | 42 +- cmd/internal/browser_tunnel_test.go | 7 +- cmd/internal/container_tunnel.go | 12 +- cmd/internal/fleet_server.go | 12 +- cmd/internal/get.go | 10 +- cmd/internal/get_workspace_config.go | 25 +- cmd/internal/git_credentials.go | 7 +- cmd/internal/request.go | 21 +- cmd/internal/runusercommands.go | 119 +++--- cmd/internal/runusercommands_test.go | 51 +-- cmd/internal/sh.go | 10 +- cmd/internal/ssh_client.go | 12 +- cmd/internal/ssh_git_clone.go | 10 +- cmd/internal/ssh_server.go | 15 +- cmd/machine/create.go | 13 +- cmd/machine/delete.go | 12 +- cmd/machine/ssh.go | 18 +- cmd/mcp/serve.go | 29 +- cmd/minor_flags_test.go | 43 +- cmd/pro/check_update.go | 8 +- cmd/pro/cluster/add.go | 84 +++- cmd/pro/cluster/list.go | 14 +- cmd/pro/daemon/netcheck.go | 10 +- cmd/pro/daemon/start.go | 10 +- cmd/pro/daemon/status.go | 10 +- cmd/pro/flags/flags.go | 3 +- cmd/pro/health.go | 8 +- cmd/pro/list.go | 13 +- cmd/pro/login.go | 63 ++- cmd/pro/logout.go | 13 +- cmd/pro/project/list.go | 8 +- cmd/pro/provider/rebuild.go | 8 +- cmd/pro/self.go | 8 +- cmd/pro/start.go | 130 ++++-- cmd/pro/template/list.go | 14 +- cmd/pro/update_provider.go | 8 +- cmd/pro/user/reset_password.go | 18 +- cmd/pro/version.go | 8 +- cmd/pro/workspace/create.go | 15 +- cmd/pro/workspace/import.go | 28 +- cmd/pro/workspace/list.go | 8 +- cmd/pro/workspace/rebuild.go | 14 +- cmd/pro/workspace/sleep.go | 25 +- cmd/pro/workspace/update.go | 15 +- cmd/pro/workspace/wakeup.go | 14 +- cmd/pro/workspace/watch.go | 25 +- cmd/provider/add.go | 36 +- cmd/provider/delete.go | 14 +- cmd/provider/get.go | 8 +- cmd/provider/init.go | 21 +- cmd/provider/list.go | 13 +- cmd/provider/set.go | 20 +- cmd/provider/set_source.go | 16 +- cmd/provider/versions.go | 10 +- cmd/root.go | 5 +- cmd/self/update.go | 31 +- cmd/self/update_test.go | 7 +- cmd/template/apply.go | 60 ++- cmd/template/generate_docs.go | 35 +- cmd/template/metadata.go | 15 +- cmd/template/publish.go | 30 +- cmd/workspace/build.go | 182 ++++---- cmd/workspace/build_test.go | 15 +- cmd/workspace/delete.go | 30 +- cmd/workspace/describe.go | 10 +- cmd/workspace/exec.go | 80 +--- cmd/workspace/exec_test.go | 26 +- cmd/workspace/export.go | 6 +- cmd/workspace/import.go | 28 +- cmd/workspace/list.go | 12 +- cmd/workspace/logs.go | 6 +- cmd/workspace/ping.go | 6 +- cmd/workspace/set_ide.go | 17 +- cmd/workspace/ssh.go | 179 ++++---- cmd/workspace/status.go | 18 +- cmd/workspace/stop.go | 2 +- cmd/workspace/troubleshoot.go | 2 +- cmd/workspace/up/configure.go | 2 +- cmd/workspace/up/up.go | 17 +- cmd/workspace/up/up_client.go | 11 +- .../up/up_devcontainer_source_test.go | 49 +++ cmd/workspace/up/up_flags.go | 390 ++++++++---------- cmd/workspace/up/up_test.go | 51 +-- .../up/up_update_remote_user_uid_test.go | 15 +- cmd/workspace/up/up_validate.go | 39 +- e2e/framework/command.go | 61 ++- e2e/tests/exec/exec.go | 5 +- e2e/tests/feature/features_test_cmd.go | 3 +- e2e/tests/ide/browser_returns.go | 5 +- e2e/tests/ssh/ssh.go | 3 +- e2e/tests/ssh/ssh_tunnel_mode_test.go | 11 +- e2e/tests/template/template.go | 3 +- e2e/tests/up-docker-compose/build.go | 5 +- e2e/tests/up/dockerfile_build.go | 5 +- e2e/tests/up/provider_docker.go | 19 +- e2e/tests/up/provider_podman.go | 47 ++- e2e/tests/up/up_behaviors.go | 9 +- pkg/agent/delivery/workspace_seed.go | 4 +- pkg/config/config.go | 5 + pkg/daemon/agent/daemon.go | 7 +- pkg/devcontainer/compose.go | 7 +- pkg/devcontainer/config.go | 3 +- pkg/devcontainer/config/build.go | 7 +- pkg/devcontainer/config/result.go | 8 + pkg/devcontainer/config_test.go | 2 +- pkg/devcontainer/setup.go | 60 ++- pkg/devcontainer/source.go | 30 +- pkg/devcontainer/source_test.go | 15 +- pkg/dotfiles/dotfiles.go | 29 +- pkg/flags/builder.go | 232 +++++++++++ pkg/flags/devcontainer.go | 43 ++ pkg/flags/names/names.go | 350 ++++++++++++++++ pkg/flags/names/names_test.go | 284 +++++++++++++ pkg/gpg/forward.go | 15 +- pkg/gpg/forward_test.go | 2 +- pkg/gpg/signing.go | 52 +++ .../ssh_test.go => pkg/gpg/signing_test.go | 28 +- pkg/ide/opener/browser_tunnel.go | 21 +- pkg/ide/opener/browser_tunnel_unix.go | 7 +- pkg/ide/opener/opener.go | 3 +- pkg/options/options.go | 12 + pkg/ssh/config.go | 18 +- pkg/ssh/config_test.go | 4 +- pkg/tunnel/browser.go | 33 +- pkg/tunnel/dial.go | 12 +- sites/docs-devsy-sh/package.json | 2 +- sites/docs-devsy-sh/yarn.lock | 21 +- 172 files changed, 3418 insertions(+), 1651 deletions(-) create mode 100644 cmd/workspace/up/up_devcontainer_source_test.go create mode 100644 pkg/flags/builder.go create mode 100644 pkg/flags/devcontainer.go create mode 100644 pkg/flags/names/names.go create mode 100644 pkg/flags/names/names_test.go create mode 100644 pkg/gpg/signing.go rename cmd/workspace/ssh_test.go => pkg/gpg/signing_test.go (63%) diff --git a/cmd/completion/suggestions.go b/cmd/completion/suggestions.go index 3c0ecb57b..c98eae075 100644 --- a/cmd/completion/suggestions.go +++ b/cmd/completion/suggestions.go @@ -5,6 +5,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -12,7 +13,7 @@ import ( func RegisterFlagCompletionFuns(rootCmd *cobra.Command, globalFlags *flags.GlobalFlags) error { if err := rootCmd.RegisterFlagCompletionFunc( - "provider", + names.Provider, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return GetProviderSuggestions( rootCmd, @@ -28,7 +29,7 @@ func RegisterFlagCompletionFuns(rootCmd *cobra.Command, globalFlags *flags.Globa } if err := rootCmd.RegisterFlagCompletionFunc( - "context", + names.Context, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return GetContextSuggestions( rootCmd, diff --git a/cmd/config/apply.go b/cmd/config/apply.go index dac8f5bc3..ea878b622 100644 --- a/cmd/config/apply.go +++ b/cmd/config/apply.go @@ -15,6 +15,7 @@ import ( devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/feature" "github.com/devsy-org/devsy/pkg/docker" + cliflags "github.com/devsy-org/devsy/pkg/flags" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/types" @@ -54,15 +55,17 @@ func NewApplyCmd(f *flags.GlobalFlags) *cobra.Command { }, } - applyCmd.Flags().StringVar(&cmd.Container, flagApplyContainer, "", - "The container ID or name to apply configuration to (required)") + cliflags.Add(applyCmd, + cliflags.String(&cmd.Container, flagApplyContainer, "", + "The container ID or name to apply configuration to (required)"), + cliflags.String(&cmd.Config, flagApplyConfig, "", + "Path to devcontainer.json (defaults to auto-detection in current workspace)"), + cliflags.String(&cmd.WorkspaceFolder, flagApplyWorkspaceFolder, "", + "Workspace folder path inside the container"), + cliflags.String(&cmd.DockerPath, flagApplyDockerPath, "", + "Path to the docker/podman executable (defaults to 'docker')"), + ) _ = applyCmd.MarkFlagRequired(flagApplyContainer) - applyCmd.Flags().StringVar(&cmd.Config, flagApplyConfig, "", - "Path to devcontainer.json (defaults to auto-detection in current workspace)") - applyCmd.Flags().StringVar(&cmd.WorkspaceFolder, flagApplyWorkspaceFolder, "", - "Workspace folder path inside the container") - applyCmd.Flags().StringVar(&cmd.DockerPath, flagApplyDockerPath, "", - "Path to the docker/podman executable (defaults to 'docker')") return applyCmd } diff --git a/cmd/config/read.go b/cmd/config/read.go index 139880602..243e66041 100644 --- a/cmd/config/read.go +++ b/cmd/config/read.go @@ -11,6 +11,8 @@ import ( devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/metadata" "github.com/devsy-org/devsy/pkg/docker" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" pkgworkspace "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -37,55 +39,25 @@ func NewReadCmd(f *flags.GlobalFlags) *cobra.Command { RunE: cmd.Run, } - readConfigCmd.Flags(). - StringVar( - &cmd.WorkspaceFolder, - "workspace-folder", - "", - "Path to the workspace folder", - ) - readConfigCmd.Flags(). - StringVar( - &cmd.ContainerID, - "container-id", - "", - "Read configuration from a running container with the given ID", - ) - readConfigCmd.Flags(). - StringVar( - &cmd.DockerPath, - "docker-path", - "", - "Path to the docker/podman executable (defaults to 'docker')", - ) - readConfigCmd.Flags(). - StringVar( - &cmd.Config, - "config", - "", - "Path to a specific devcontainer.json", - ) - readConfigCmd.Flags(). - StringArrayVar( + cliflags.Add(readConfigCmd, + cliflags.String(&cmd.WorkspaceFolder, names.WorkspaceFolder, "", + "Path to the workspace folder"), + cliflags.String(&cmd.ContainerID, names.ContainerID, "", + "Read configuration from a running container with the given ID"), + cliflags.String(&cmd.DockerPath, names.DockerPath, "", + "Path to the docker/podman executable (defaults to 'docker')"), + cliflags.String(&cmd.Config, names.Config, "", "Path to a specific devcontainer.json"), + cliflags.StringArray( &cmd.IDLabels, - "id-label", + names.IDLabel, nil, "Override the default container identification labels (format: key=value, can be specified multiple times)", - ) - readConfigCmd.Flags(). - BoolVar( - &cmd.IncludeFeaturesConfiguration, - "include-features-configuration", - false, - "Include features in the output", - ) - readConfigCmd.Flags(). - BoolVar( - &cmd.IncludeMergedConfiguration, - "include-merged-configuration", - false, - "Include the merged configuration in the output", - ) + ), + cliflags.Bool(&cmd.IncludeFeaturesConfiguration, names.IncludeFeaturesConfiguration, false, + "Include features in the output"), + cliflags.Bool(&cmd.IncludeMergedConfiguration, names.IncludeMergedConfiguration, false, + "Include the merged configuration in the output"), + ) return readConfigCmd } @@ -152,7 +124,10 @@ func (cmd *ReadCmd) resolve(ctx context.Context) ( ) { if cmd.ContainerID == "" && cmd.WorkspaceFolder == "" && len(cmd.IDLabels) == 0 { return nil, "", fmt.Errorf( - "either --workspace-folder, --container-id, or --id-label must be provided", + "either %s, %s, or %s must be provided", + names.Flag(names.WorkspaceFolder), + names.Flag(names.ContainerID), + names.Flag(names.IDLabel), ) } if cmd.ContainerID != "" { diff --git a/cmd/context/create.go b/cmd/context/create.go index 511d7e2da..91b891e5c 100644 --- a/cmd/context/create.go +++ b/cmd/context/create.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" ) @@ -34,8 +36,11 @@ func NewCreateCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - createCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "context option in the form KEY=VALUE") + cliflags.Add( + createCmd, + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "context option in the form KEY=VALUE"). + Shorthand("o"), + ) return createCmd } diff --git a/cmd/context/set_options.go b/cmd/context/set_options.go index ede9ffaae..300bc4e43 100644 --- a/cmd/context/set_options.go +++ b/cmd/context/set_options.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -38,8 +40,11 @@ func NewSetOptionsCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - setOptionsCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "context option in the form KEY=VALUE") + cliflags.Add( + setOptionsCmd, + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "context option in the form KEY=VALUE"). + Shorthand("o"), + ) return setOptionsCmd } diff --git a/cmd/context/use.go b/cmd/context/use.go index ec00a1aa1..0c7223c6a 100644 --- a/cmd/context/use.go +++ b/cmd/context/use.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -33,8 +35,11 @@ func NewUseCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - useCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "context option in the form KEY=VALUE") + cliflags.Add( + useCmd, + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "context option in the form KEY=VALUE"). + Shorthand("o"), + ) return useCmd } diff --git a/cmd/env_flags_test.go b/cmd/env_flags_test.go index 57e18dea4..e607c6d1a 100644 --- a/cmd/env_flags_test.go +++ b/cmd/env_flags_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -77,7 +78,7 @@ func TestOptInEnvFlags_NoEnvLeavesDefault(t *testing.T) { t.Setenv(name, "") } rootCmd, _ := BuildRoot() - f := rootCmd.PersistentFlags().Lookup("context") + f := rootCmd.PersistentFlags().Lookup(names.Context) require.NotNil(t, f) assert.False(t, f.Changed) assert.Equal(t, "", f.Value.String()) @@ -91,7 +92,7 @@ func TestOptInEnvFlags_EmptyEnvIsNoOp(t *testing.T) { t.Setenv(envDevsyHost, "") rootCmd, _ := BuildRoot() cmd := resolveCommand(t, rootCmd, "pro workspace list") - f := cmd.Flags().Lookup("host") + f := cmd.Flags().Lookup(names.Host) require.NotNil(t, f) assert.False(t, f.Changed, "empty env value must not mark flag as Changed") } @@ -109,7 +110,7 @@ func TestOptInEnvFlags_CLIOverridesEnv(t *testing.T) { leaf := resolveCommand(t, rootCmd, "pro workspace list") var seen string leaf.RunE = func(c *cobra.Command, _ []string) error { - seen, _ = c.Flags().GetString("host") + seen, _ = c.Flags().GetString(names.Host) return nil } rootCmd.SetArgs([]string{"pro", cmdWorkspace, cmdList, "--host", "cli-value"}) @@ -119,7 +120,7 @@ func TestOptInEnvFlags_CLIOverridesEnv(t *testing.T) { // TestOptInEnvFlags_EnvSatisfiesRequired drives cobra's full parse + // ValidateRequiredFlags pipeline to prove DEVSY_HOST satisfies -// MarkFlagRequired("host") at execution time, not just in static inspection. +// MarkFlagRequired(names.Host) at execution time, not just in static inspection. func TestOptInEnvFlags_EnvSatisfiesRequired(t *testing.T) { t.Setenv(envProEnabled, "true") t.Setenv(envDevsyHost, "from-env.example.com") @@ -127,7 +128,7 @@ func TestOptInEnvFlags_EnvSatisfiesRequired(t *testing.T) { leaf := resolveCommand(t, rootCmd, "pro workspace list") var seen string leaf.RunE = func(c *cobra.Command, _ []string) error { - seen, _ = c.Flags().GetString("host") + seen, _ = c.Flags().GetString(names.Host) return nil } rootCmd.SetArgs([]string{"pro", cmdWorkspace, cmdList}) diff --git a/cmd/feature/generatedocs.go b/cmd/feature/generatedocs.go index 824be5c95..ea21284f1 100644 --- a/cmd/feature/generatedocs.go +++ b/cmd/feature/generatedocs.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -42,16 +44,28 @@ documentation for each feature based on its devcontainer-feature.json.`, }, } - generateDocsCmd.Flags().StringVar( - &cmd.ProjectFolder, "project-folder", "", "Path to feature project containing src/ directory", + cliflags.Add( + generateDocsCmd, + cliflags.String( + &cmd.ProjectFolder, + names.ProjectFolder, + "", + "Path to feature project containing src/ directory", + ), + cliflags.String( + &cmd.OutputFolder, + names.OutputFolder, + "", + "Where to write generated docs (default: project-folder)", + ), + cliflags.String( + &cmd.Namespace, + names.Namespace, + "", + "Registry namespace for linking (e.g.ghcr.io/myorg/features)", + ), ) - generateDocsCmd.Flags().StringVar( - &cmd.OutputFolder, "output-folder", "", "Where to write generated docs (default: project-folder)", - ) - generateDocsCmd.Flags().StringVar( - &cmd.Namespace, "namespace", "", "Registry namespace for linking (e.g. ghcr.io/myorg/features)", - ) - _ = generateDocsCmd.MarkFlagRequired("project-folder") + _ = generateDocsCmd.MarkFlagRequired(names.ProjectFolder) return generateDocsCmd } diff --git a/cmd/feature/info.go b/cmd/feature/info.go index 2e90738f2..977d6f226 100644 --- a/cmd/feature/info.go +++ b/cmd/feature/info.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/authn" @@ -55,11 +57,20 @@ to display metadata.`, }, } - infoCmd.Flags().BoolVar( - &cmd.ShowTags, "show-tags", false, "List available tags from the registry", - ) - infoCmd.Flags().BoolVar( - &cmd.ShowDependencies, "show-dependencies", false, "Show declared dependencies", + cliflags.Add( + infoCmd, + cliflags.Bool( + &cmd.ShowTags, + names.ShowTags, + false, + "List available tags from the registry", + ), + cliflags.Bool( + &cmd.ShowDependencies, + names.ShowDependencies, + false, + "Show declared dependencies", + ), ) infoCmd.AddCommand(NewInfoManifestCmd(globalFlags)) diff --git a/cmd/feature/info_test.go b/cmd/feature/info_test.go index a4372a552..c8fff7ef2 100644 --- a/cmd/feature/info_test.go +++ b/cmd/feature/info_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -12,11 +13,11 @@ import ( func TestInfoCmd_FlagDefaults(t *testing.T) { cmd := NewInfoCmd(nil) - showTagsFlag := cmd.Flags().Lookup("show-tags") + showTagsFlag := cmd.Flags().Lookup(names.ShowTags) require.NotNil(t, showTagsFlag) assert.Equal(t, "false", showTagsFlag.DefValue) - showDepsFlag := cmd.Flags().Lookup("show-dependencies") + showDepsFlag := cmd.Flags().Lookup(names.ShowDependencies) require.NotNil(t, showDepsFlag) assert.Equal(t, "false", showDepsFlag.DefValue) } diff --git a/cmd/feature/outdated.go b/cmd/feature/outdated.go index 2f5aa09ee..2646a2615 100644 --- a/cmd/feature/outdated.go +++ b/cmd/feature/outdated.go @@ -11,6 +11,8 @@ import ( "github.com/blang/semver/v4" "github.com/devsy-org/devsy/cmd/flags" devconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/table" "github.com/google/go-containerregistry/pkg/authn" @@ -45,10 +47,16 @@ func NewOutdatedCmd(f *flags.GlobalFlags) *cobra.Command { }, } - outdatedCmd.Flags(). - StringVar(&cmd.WorkspaceFolder, "workspace-folder", "", "Path to the workspace folder") - outdatedCmd.Flags(). - StringVar(&cmd.Config, "config", "", "Path to a specific devcontainer.json") + cliflags.Add( + outdatedCmd, + cliflags.String( + &cmd.WorkspaceFolder, + names.WorkspaceFolder, + "", + "Path to the workspace folder", + ), + cliflags.String(&cmd.Config, names.Config, "", "Path to a specific devcontainer.json"), + ) return outdatedCmd } diff --git a/cmd/feature/package.go b/cmd/feature/package.go index 5ef3b8218..4bb115119 100644 --- a/cmd/feature/package.go +++ b/cmd/feature/package.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" @@ -46,19 +48,28 @@ and creates gzipped tar archives suitable for OCI distribution.`, }, } - packageCmd.Flags().StringVar( - &cmd.Target, "target", "", - "Path to directory containing feature source subdirectories", + cliflags.Add( + packageCmd, + cliflags.String( + &cmd.Target, + names.Target, + "", + "Path to directory containing feature source subdirectories", + ), + cliflags.String( + &cmd.OutputFolder, + names.OutputFolder, + ".", + "Where to write the .tgz files", + ), + cliflags.Bool( + &cmd.ForceCleanOutputFolder, + names.ForceCleanOutputFolder, + false, + "Clean output folder before writing", + ), ) - packageCmd.Flags().StringVar( - &cmd.OutputFolder, "output-folder", ".", - "Where to write the .tgz files", - ) - packageCmd.Flags().BoolVar( - &cmd.ForceCleanOutputFolder, "force-clean-output-folder", false, - "Clean output folder before writing", - ) - _ = packageCmd.MarkFlagRequired("target") + _ = packageCmd.MarkFlagRequired(names.Target) return packageCmd } diff --git a/cmd/feature/package_test.go b/cmd/feature/package_test.go index c740f4450..76b84ed0c 100644 --- a/cmd/feature/package_test.go +++ b/cmd/feature/package_test.go @@ -10,6 +10,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,15 +21,15 @@ const testFeatureIDPkg = "my-feature" func TestPackageCmd_FlagDefaults(t *testing.T) { cmd := NewPackageCmd(nil) - targetFlag := cmd.Flags().Lookup("target") + targetFlag := cmd.Flags().Lookup(names.Target) require.NotNil(t, targetFlag) assert.Equal(t, "", targetFlag.DefValue) - outputFolderFlag := cmd.Flags().Lookup("output-folder") + outputFolderFlag := cmd.Flags().Lookup(names.OutputFolder) require.NotNil(t, outputFolderFlag) assert.Equal(t, ".", outputFolderFlag.DefValue) - forceCleanFlag := cmd.Flags().Lookup("force-clean-output-folder") + forceCleanFlag := cmd.Flags().Lookup(names.ForceCleanOutputFolder) require.NotNil(t, forceCleanFlag) assert.Equal(t, "false", forceCleanFlag.DefValue) } diff --git a/cmd/feature/publish.go b/cmd/feature/publish.go index 3f967db0e..c82873b28 100644 --- a/cmd/feature/publish.go +++ b/cmd/feature/publish.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -46,19 +48,23 @@ OCI artifact.`, }, } - publishCmd.Flags().StringVar( - &publishFlags.Target, "target", "", - "Path to packaged feature directory", + cliflags.Add( + publishCmd, + cliflags.String( + &publishFlags.Target, + names.Target, + "", + "Path to packaged feature directory", + ), + cliflags.String(&publishFlags.Registry, names.Registry, "ghcr.io", "Target OCI registry"), + cliflags.String( + &publishFlags.Namespace, + names.Namespace, + "", + "Registry namespace (e.g., devcontainers/features)", + ), ) - publishCmd.Flags().StringVar( - &publishFlags.Registry, "registry", "ghcr.io", - "Target OCI registry", - ) - publishCmd.Flags().StringVar( - &publishFlags.Namespace, "namespace", "", - "Registry namespace (e.g., devcontainers/features)", - ) - _ = publishCmd.MarkFlagRequired("target") + _ = publishCmd.MarkFlagRequired(names.Target) return publishCmd } diff --git a/cmd/feature/publish_test.go b/cmd/feature/publish_test.go index 027e8be1c..aa258917b 100644 --- a/cmd/feature/publish_test.go +++ b/cmd/feature/publish_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,15 +19,15 @@ const ( func TestPublishCmd_FlagDefaults(t *testing.T) { cmd := NewPublishCmd(nil) - targetFlag := cmd.Flags().Lookup("target") + targetFlag := cmd.Flags().Lookup(names.Target) require.NotNil(t, targetFlag) assert.Equal(t, "", targetFlag.DefValue) - registryFlag := cmd.Flags().Lookup("registry") + registryFlag := cmd.Flags().Lookup(names.Registry) require.NotNil(t, registryFlag) assert.Equal(t, testRegistry, registryFlag.DefValue) - namespaceFlag := cmd.Flags().Lookup("namespace") + namespaceFlag := cmd.Flags().Lookup(names.Namespace) require.NotNil(t, namespaceFlag) assert.Equal(t, "", namespaceFlag.DefValue) } @@ -41,7 +42,7 @@ func TestPublishCmd_AllFlagsRegistered(t *testing.T) { func TestPublishCmd_TargetRequired(t *testing.T) { cmd := NewPublishCmd(nil) - flag := cmd.Flags().Lookup("target") + flag := cmd.Flags().Lookup(names.Target) require.NotNil(t, flag) annotations := flag.Annotations diff --git a/cmd/feature/resolvedeps.go b/cmd/feature/resolvedeps.go index 079b6e7ff..1c45305c1 100644 --- a/cmd/feature/resolvedeps.go +++ b/cmd/feature/resolvedeps.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/feature" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -44,15 +46,22 @@ install order based on dependency declarations and install ordering.`, }, } - resolveDepsCmd.Flags().StringVar( - &cmd.WorkspaceFolder, "workspace-folder", "", - "Path to workspace containing devcontainer.json", + cliflags.Add( + resolveDepsCmd, + cliflags.String( + &cmd.WorkspaceFolder, + names.WorkspaceFolder, + "", + "Path to workspace containing devcontainer.json", + ), + cliflags.String( + &cmd.Config, + names.Config, + "", + "Path to specific devcontainer.json (optional)", + ), ) - resolveDepsCmd.Flags().StringVar( - &cmd.Config, "config", "", - "Path to specific devcontainer.json (optional)", - ) - _ = resolveDepsCmd.MarkFlagRequired("workspace-folder") + _ = resolveDepsCmd.MarkFlagRequired(names.WorkspaceFolder) return resolveDepsCmd } diff --git a/cmd/feature/test.go b/cmd/feature/test.go index a11cf02d5..7cb5df0fb 100644 --- a/cmd/feature/test.go +++ b/cmd/feature/test.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" ) @@ -55,35 +57,22 @@ test scripts from the test/ directory.`, }, } - testCmd.Flags().StringVar( - &cmd.ProjectFolder, "project-folder", "", - "Path to feature project containing src/ and test/ directories", + cliflags.Add(testCmd, + cliflags.String(&cmd.ProjectFolder, names.ProjectFolder, "", + "Path to feature project containing src/ and test/ directories"), + cliflags.String(&cmd.Features, names.Features, "", + "Comma-separated list of feature IDs to test (default: all)"), + cliflags.String(&cmd.BaseImage, names.BaseImage, defaultBaseImage, + "Base Docker image for test containers"), + cliflags.String(&cmd.RemoteUser, names.RemoteUser, defaultRemoteUser, + "User to run tests as"), + cliflags.Bool(&cmd.SkipScenarios, names.SkipScenarios, false, + "Only run the global test script, skip per-feature scenario tests"), + cliflags.Bool(&cmd.Quiet, names.Quiet, false, "Suppress verbose build output"), + cliflags.Bool(&cmd.PreserveTestContainers, names.PreserveTestContainers, false, + "Don't remove test containers after run (for debugging)"), ) - testCmd.Flags().StringVar( - &cmd.Features, "features", "", - "Comma-separated list of feature IDs to test (default: all)", - ) - testCmd.Flags().StringVar( - &cmd.BaseImage, "base-image", defaultBaseImage, - "Base Docker image for test containers", - ) - testCmd.Flags().StringVar( - &cmd.RemoteUser, "remote-user", defaultRemoteUser, - "User to run tests as", - ) - testCmd.Flags().BoolVar( - &cmd.SkipScenarios, "skip-scenarios", false, - "Only run the global test script, skip per-feature scenario tests", - ) - testCmd.Flags().BoolVar( - &cmd.Quiet, "quiet", false, - "Suppress verbose build output", - ) - testCmd.Flags().BoolVar( - &cmd.PreserveTestContainers, "preserve-test-containers", false, - "Don't remove test containers after run (for debugging)", - ) - _ = testCmd.MarkFlagRequired("project-folder") + _ = testCmd.MarkFlagRequired(names.ProjectFolder) return testCmd } diff --git a/cmd/feature/test_test.go b/cmd/feature/test_test.go index 84305ff88..97779ab86 100644 --- a/cmd/feature/test_test.go +++ b/cmd/feature/test_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,31 +19,31 @@ const ( func TestTestCmd_FlagDefaults(t *testing.T) { cmd := NewTestCmd(nil) - projectFolderFlag := cmd.Flags().Lookup("project-folder") + projectFolderFlag := cmd.Flags().Lookup(names.ProjectFolder) require.NotNil(t, projectFolderFlag) assert.Equal(t, "", projectFolderFlag.DefValue) - featuresFlag := cmd.Flags().Lookup("features") + featuresFlag := cmd.Flags().Lookup(names.Features) require.NotNil(t, featuresFlag) assert.Equal(t, "", featuresFlag.DefValue) - baseImageFlag := cmd.Flags().Lookup("base-image") + baseImageFlag := cmd.Flags().Lookup(names.BaseImage) require.NotNil(t, baseImageFlag) assert.Equal(t, defaultBaseImage, baseImageFlag.DefValue) - remoteUserFlag := cmd.Flags().Lookup("remote-user") + remoteUserFlag := cmd.Flags().Lookup(names.RemoteUser) require.NotNil(t, remoteUserFlag) assert.Equal(t, defaultRemoteUser, remoteUserFlag.DefValue) - skipScenariosFlag := cmd.Flags().Lookup("skip-scenarios") + skipScenariosFlag := cmd.Flags().Lookup(names.SkipScenarios) require.NotNil(t, skipScenariosFlag) assert.Equal(t, "false", skipScenariosFlag.DefValue) - quietFlag := cmd.Flags().Lookup("quiet") + quietFlag := cmd.Flags().Lookup(names.Quiet) require.NotNil(t, quietFlag) assert.Equal(t, "false", quietFlag.DefValue) - preserveFlag := cmd.Flags().Lookup("preserve-test-containers") + preserveFlag := cmd.Flags().Lookup(names.PreserveTestContainers) require.NotNil(t, preserveFlag) assert.Equal(t, "false", preserveFlag.DefValue) } diff --git a/cmd/feature/upgrade.go b/cmd/feature/upgrade.go index 2d9b84360..b373275b8 100644 --- a/cmd/feature/upgrade.go +++ b/cmd/feature/upgrade.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" devconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" ) @@ -35,12 +37,17 @@ features are upgraded. Otherwise, all outdated features are upgraded.`, }, } - upgradeCmd.Flags(). - StringVar(&cmd.WorkspaceFolder, "workspace-folder", "", "Path to the workspace folder") - upgradeCmd.Flags(). - StringVar(&cmd.Config, "config", "", "Path to a specific devcontainer.json") - upgradeCmd.Flags(). - BoolVar(&cmd.DryRun, "dry-run", false, "Preview upgrades without applying them") + cliflags.Add( + upgradeCmd, + cliflags.String( + &cmd.WorkspaceFolder, + names.WorkspaceFolder, + "", + "Path to the workspace folder", + ), + cliflags.String(&cmd.Config, names.Config, "", "Path to a specific devcontainer.json"), + cliflags.Bool(&cmd.DryRun, names.DryRun, false, "Preview upgrades without applying them"), + ) return upgradeCmd } diff --git a/cmd/feature/upgrade_test.go b/cmd/feature/upgrade_test.go index 9b6c4488b..797295d82 100644 --- a/cmd/feature/upgrade_test.go +++ b/cmd/feature/upgrade_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "testing" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,20 +24,20 @@ func TestNewUpgradeCmd_CreatesCommand(t *testing.T) { func TestNewUpgradeCmd_HasDryRunFlag(t *testing.T) { cmd := NewUpgradeCmd(nil) - flag := cmd.Flags().Lookup("dry-run") + flag := cmd.Flags().Lookup(names.DryRun) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } func TestNewUpgradeCmd_HasWorkspaceFolderFlag(t *testing.T) { cmd := NewUpgradeCmd(nil) - flag := cmd.Flags().Lookup("workspace-folder") + flag := cmd.Flags().Lookup(names.WorkspaceFolder) require.NotNil(t, flag) } func TestNewUpgradeCmd_HasConfigFlag(t *testing.T) { cmd := NewUpgradeCmd(nil) - flag := cmd.Flags().Lookup("config") + flag := cmd.Flags().Lookup(names.Config) require.NotNil(t, flag) } diff --git a/cmd/flag_aliases_test.go b/cmd/flag_aliases_test.go index 5e1a8d919..56399bfd8 100644 --- a/cmd/flag_aliases_test.go +++ b/cmd/flag_aliases_test.go @@ -3,44 +3,12 @@ package cmd import ( "testing" - "github.com/devsy-org/devsy/cmd/flags" - "github.com/devsy-org/devsy/cmd/workspace" - "github.com/devsy-org/devsy/cmd/workspace/up" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -const ( - flagConfig = "--config" - flagDevcontainerPath = "devcontainer-path" - flagOverrideConfig = "--override-config" - flagExtraDevContainerPath = "extra-devcontainer-path" - flagDotfilesRepository = "--dotfiles-repository" - flagDotfiles = "dotfiles" - flagRemoveExistingContainer = "--remove-existing-container" - flagRecreate = "recreate" - formatJSON = "json" -) - -func TestUpCmd_ConfigAlias(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{flagConfig, ".devcontainer/custom.json"}) - require.NoError(t, err) - - val, err := upCmd.Flags().GetString(flagDevcontainerPath) - require.NoError(t, err) - assert.Equal(t, ".devcontainer/custom.json", val) -} - -func TestBuildCmd_ConfigAlias(t *testing.T) { - buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) - err := buildCmd.ParseFlags([]string{flagConfig, "path/to/devcontainer.json"}) - require.NoError(t, err) - - val, err := buildCmd.Flags().GetString(flagDevcontainerPath) - require.NoError(t, err) - assert.Equal(t, "path/to/devcontainer.json", val) -} +const formatJSON = "json" func TestGlobalFlags_LogFormatAlias(t *testing.T) { rootCmd, globalFlags := BuildRoot() @@ -50,74 +18,9 @@ func TestGlobalFlags_LogFormatAlias(t *testing.T) { assert.Equal(t, formatJSON, globalFlags.LogOutput) } -func TestConfigAlias_IsHidden(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - f := upCmd.Flags().Lookup("config") - require.NotNil(t, f) - assert.True(t, f.Hidden, flagConfig+" alias should be hidden") -} - func TestLogFormatAlias_IsHidden(t *testing.T) { rootCmd, _ := BuildRoot() - f := rootCmd.PersistentFlags().Lookup("log-format") + f := rootCmd.PersistentFlags().Lookup(names.LogFormat) require.NotNil(t, f) assert.True(t, f.Hidden, flagLogFormat+" alias should be hidden") } - -func TestConfigAlias_E2E(t *testing.T) { - rootCmd, _ := BuildRoot() - rootCmd.SetArgs([]string{cmdWorkspace, "up", flagConfig, "/tmp/test.json", "--help"}) - err := rootCmd.Execute() - require.NoError(t, err) -} - -func TestUpCmd_OverrideConfigAlias(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{flagOverrideConfig, "/tmp/override.json"}) - require.NoError(t, err) - - val, err := upCmd.Flags().GetString(flagExtraDevContainerPath) - require.NoError(t, err) - assert.Equal(t, "/tmp/override.json", val) -} - -func TestOverrideConfigAlias_IsHidden(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - f := upCmd.Flags().Lookup("override-config") - require.NotNil(t, f) - assert.True(t, f.Hidden, flagOverrideConfig+" alias should be hidden") -} - -func TestUpCmd_DotfilesRepositoryAlias(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{flagDotfilesRepository, "https://github.com/user/dotfiles"}) - require.NoError(t, err) - - val, err := upCmd.Flags().GetString(flagDotfiles) - require.NoError(t, err) - assert.Equal(t, "https://github.com/user/dotfiles", val) -} - -func TestDotfilesRepositoryAlias_IsHidden(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - f := upCmd.Flags().Lookup("dotfiles-repository") - require.NotNil(t, f) - assert.True(t, f.Hidden, flagDotfilesRepository+" alias should be hidden") -} - -func TestUpCmd_RemoveExistingContainerAlias(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{flagRemoveExistingContainer}) - require.NoError(t, err) - - val, err := upCmd.Flags().GetBool(flagRecreate) - require.NoError(t, err) - assert.True(t, val) -} - -func TestRemoveExistingContainerAlias_IsHidden(t *testing.T) { - upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - f := upCmd.Flags().Lookup("remove-existing-container") - require.NotNil(t, f) - assert.True(t, f.Hidden, flagRemoveExistingContainer+" alias should be hidden") -} diff --git a/cmd/flags/flags.go b/cmd/flags/flags.go index b7568391a..27dd92032 100644 --- a/cmd/flags/flags.go +++ b/cmd/flags/flags.go @@ -1,6 +1,7 @@ package flags import ( + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" flag "github.com/spf13/pflag" ) @@ -26,62 +27,67 @@ func SetGlobalFlags(flags *flag.FlagSet) *GlobalFlags { flags.StringVar( &globalFlags.DevsyHome, - "home", + names.Home, "", "If defined will override the default devsy home", ) flags.StringVar( &globalFlags.ResultFormat, - "result-format", + names.ResultFormat, "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, - "log-output", + names.LogOutput, "text", "The log format to use. Can be text, json, or logfmt", ) - flags.StringVar(&globalFlags.LogOutput, "log-format", "text", "Alias for --log-output") - _ = flags.MarkHidden("log-format") - flags.StringVar(&globalFlags.Context, "context", "", "The context to use") + flags.StringVar(&globalFlags.LogOutput, names.LogFormat, "text", "Alias for --log-output") + _ = flags.MarkHidden(names.LogFormat) + flags.StringVar(&globalFlags.Context, names.Context, "", "The context to use") flags.StringVar( &globalFlags.Provider, - "provider", + names.Provider, "", "The provider to use. Needs to be configured for the selected context", ) flags.CountVarP( &globalFlags.Verbosity, - "verbose", + names.Verbose, "v", "Increase log verbosity (-v=info, -vv=debug, -vvv=trace)", ) flags.BoolVarP( &globalFlags.Quiet, - "quiet", + names.Quiet, "q", false, "Suppress all log output except fatal errors", ) - flags.BoolVar(&globalFlags.Debug, "debug", false, "Enable debug logging (equivalent to -vv)") + flags.BoolVar( + &globalFlags.Debug, + names.Debug, + false, + "Enable debug logging (equivalent to -vv)", + ) - flags.Var(&globalFlags.Owner, "owner", "Show pro workspaces for owner") - _ = flags.MarkHidden("owner") - flags.StringVar(&globalFlags.UID, "uid", "", "Set UID for workspace") - _ = flags.MarkHidden("uid") + flags.Var(&globalFlags.Owner, names.Owner, "Show pro workspaces for owner") + _ = flags.MarkHidden(names.Owner) + flags.StringVar(&globalFlags.UID, names.UID, "", "Set UID for workspace") + _ = flags.MarkHidden(names.UID) flags.StringVar( &globalFlags.AgentDir, - "agent-dir", + names.AgentDir, "", "The data folder where agent data is stored.", ) - _ = flags.MarkHidden("agent-dir") + _ = flags.MarkHidden(names.AgentDir) - BindEnv(flags, "home") - BindEnv(flags, "context") - BindEnv(flags, "provider") - BindEnv(flags, "debug") + BindEnv(flags, names.Home) + BindEnv(flags, names.Context) + BindEnv(flags, names.Provider) + BindEnv(flags, names.Debug) return globalFlags } diff --git a/cmd/ide/set.go b/cmd/ide/set.go index 062a2a146..b9e563f77 100644 --- a/cmd/ide/set.go +++ b/cmd/ide/set.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/ideparse" "github.com/spf13/cobra" ) @@ -43,8 +45,11 @@ with 'devsy ide list'.`, }, } - setCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "IDE option in the form KEY=VALUE") + cliflags.Add( + setCmd, + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "IDE option in the form KEY=VALUE"). + Shorthand("o"), + ) return setCmd } diff --git a/cmd/ide/use.go b/cmd/ide/use.go index db10b8403..01d261b89 100644 --- a/cmd/ide/use.go +++ b/cmd/ide/use.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide" "github.com/devsy-org/devsy/pkg/ide/ideparse" "github.com/devsy-org/devsy/pkg/log" @@ -40,8 +42,11 @@ Available IDEs can be listed with 'devsy ide list'`, }, } - useCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "IDE option in the form KEY=VALUE") + cliflags.Add( + useCmd, + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "IDE option in the form KEY=VALUE"). + Shorthand("o"), + ) return useCmd } diff --git a/cmd/internal/agent_daemon.go b/cmd/internal/agent_daemon.go index 67e2813fa..2ef6a5654 100644 --- a/cmd/internal/agent_daemon.go +++ b/cmd/internal/agent_daemon.go @@ -16,6 +16,8 @@ import ( agentconfig "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/driver/custom" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -43,13 +45,16 @@ func NewDaemonCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - daemonCmd.Flags(). - StringVar(&cmd.Interval, "interval", "", "The interval how to poll workspaces") - daemonCmd.Flags(). - StringVar( - &cmd.ShutdownAction, "shutdown-action", "", + cliflags.Add( + daemonCmd, + cliflags.String(&cmd.Interval, names.Interval, "", "The interval how to poll workspaces"), + cliflags.String( + &cmd.ShutdownAction, + names.ShutdownAction, + "", "The shutdown action (none, stopContainer, or stopCompose)", - ) + ), + ) return daemonCmd } diff --git a/cmd/internal/agent_docker_credentials.go b/cmd/internal/agent_docker_credentials.go index 82898830d..30bca0216 100644 --- a/cmd/internal/agent_docker_credentials.go +++ b/cmd/internal/agent_docker_credentials.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/dockercredentials" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/docker/docker-credential-helpers/credentials" "github.com/spf13/cobra" @@ -30,9 +32,11 @@ func NewDockerCredentialsCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context(), args) }, } - dockerCredentialsCmd.Flags(). - IntVar(&cmd.Port, "port", 0, "If specified, will use the given port") - _ = dockerCredentialsCmd.MarkFlagRequired("port") + cliflags.Add( + dockerCredentialsCmd, + cliflags.Int(&cmd.Port, names.Port, 0, "If specified, will use the given port"), + ) + _ = dockerCredentialsCmd.MarkFlagRequired(names.Port) return dockerCredentialsCmd } diff --git a/cmd/internal/agentcontainer/browser_async.go b/cmd/internal/agentcontainer/browser_async.go index 9ed03532b..50fafe854 100644 --- a/cmd/internal/agentcontainer/browser_async.go +++ b/cmd/internal/agentcontainer/browser_async.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/pkg/compress" config2 "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/codeserver" "github.com/devsy-org/devsy/pkg/ide/openvscode" "github.com/devsy-org/devsy/pkg/ide/vscodeweb" @@ -123,8 +125,11 @@ func newBrowserAsyncCmd(b browserIDE) *cobra.Command { Args: cobra.NoArgs, RunE: cmd.Run, } - cobraCmd.Flags().StringVar(&cmd.SetupInfo, "setup-info", "", "The container setup info") - _ = cobraCmd.MarkFlagRequired("setup-info") + cliflags.Add( + cobraCmd, + cliflags.String(&cmd.SetupInfo, names.SetupInfo, "", "The container setup info"), + ) + _ = cobraCmd.MarkFlagRequired(names.SetupInfo) return cobraCmd } diff --git a/cmd/internal/agentcontainer/credentials_server.go b/cmd/internal/agentcontainer/credentials_server.go index 718e11807..8ff96ccae 100644 --- a/cmd/internal/agentcontainer/credentials_server.go +++ b/cmd/internal/agentcontainer/credentials_server.go @@ -16,6 +16,8 @@ import ( "github.com/devsy-org/devsy/pkg/credentials" devconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/dockercredentials" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/gitcredentials" "github.com/devsy-org/devsy/pkg/gitsshsigning" "github.com/devsy-org/devsy/pkg/log" @@ -57,16 +59,30 @@ func NewCredentialsServerCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(c.Context(), port) }, } - credentialsServerCmd.Flags(). - BoolVar(&cmd.ConfigureGitHelper, "configure-git-helper", false, "If true will configure git helper") - credentialsServerCmd.Flags(). - BoolVar(&cmd.ConfigureDockerHelper, "configure-docker-helper", false, "If true will configure docker helper") - credentialsServerCmd.Flags(). - BoolVar(&cmd.ForwardPorts, "forward-ports", false, - "If true will automatically try to forward open ports within the container") - credentialsServerCmd.Flags().StringVar(&cmd.GitUserSigningKey, "git-user-signing-key", "", "") - credentialsServerCmd.Flags().StringVar(&cmd.User, "user", "", "The user to use") - _ = credentialsServerCmd.MarkFlagRequired("user") + cliflags.Add( + credentialsServerCmd, + cliflags.Bool( + &cmd.ConfigureGitHelper, + names.ConfigureGitHelper, + false, + "If true will configure git helper", + ), + cliflags.Bool( + &cmd.ConfigureDockerHelper, + names.ConfigureDockerHelper, + false, + "If true will configure docker helper", + ), + cliflags.Bool( + &cmd.ForwardPorts, + names.ForwardPorts, + false, + "If true will automatically try to forward open ports within the container", + ), + cliflags.String(&cmd.GitUserSigningKey, names.GitUserSigningKey, "", ""), + cliflags.String(&cmd.User, names.User, "", "The user to use"), + ) + _ = credentialsServerCmd.MarkFlagRequired(names.User) return credentialsServerCmd } diff --git a/cmd/internal/agentcontainer/daemon.go b/cmd/internal/agentcontainer/daemon.go index ac6d73212..8e1962dd5 100644 --- a/cmd/internal/agentcontainer/daemon.go +++ b/cmd/internal/agentcontainer/daemon.go @@ -16,6 +16,8 @@ import ( config2 "github.com/devsy-org/devsy/pkg/config" agentd "github.com/devsy-org/devsy/pkg/daemon/agent" "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform/client" "github.com/devsy-org/devsy/pkg/ts" @@ -43,13 +45,21 @@ func NewDaemonCmd() *cobra.Command { Args: cobra.NoArgs, RunE: cmd.Run, } - daemonCmd.Flags(). - StringVar(&cmd.Config.Timeout, "timeout", "", "The timeout to stop the container after") - daemonCmd.Flags(). - StringVar( - &cmd.Config.ShutdownAction, "shutdown-action", "", + cliflags.Add( + daemonCmd, + cliflags.String( + &cmd.Config.Timeout, + names.Timeout, + "", + "The timeout to stop the container after", + ), + cliflags.String( + &cmd.Config.ShutdownAction, + names.ShutdownAction, + "", "The shutdown action (none, stopContainer, or stopCompose)", - ) + ), + ) return daemonCmd } @@ -249,10 +259,10 @@ func runSshServer(ctx context.Context, cmd *DaemonCmd) error { args := []string{cmdInternal, cmdAgent, cmdContainer, "ssh-server"} if cmd.Config.Ssh.Workdir != "" { - args = append(args, "--workdir", cmd.Config.Ssh.Workdir) + args = append(args, names.Flag(names.Workdir), cmd.Config.Ssh.Workdir) } if cmd.Config.Ssh.User != "" { - args = append(args, "--remote-user", cmd.Config.Ssh.User) + args = append(args, names.Flag(names.RemoteUser), cmd.Config.Ssh.User) } sshCmd := exec.CommandContext(ctx, binaryPath, args...) // #nosec G204 diff --git a/cmd/internal/agentcontainer/deferred_hooks.go b/cmd/internal/agentcontainer/deferred_hooks.go index b47694258..5a32c0544 100644 --- a/cmd/internal/agentcontainer/deferred_hooks.go +++ b/cmd/internal/agentcontainer/deferred_hooks.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/pkg/compress" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/setup" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" ) @@ -38,17 +40,25 @@ func NewDeferredHooksCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - deferredCmd.Flags(). - StringVar(&cmd.SetupInfo, "setup-info", "", "The container setup info") - deferredCmd.Flags(). - BoolVar(&cmd.Prebuild, "prebuild", false, "If true, prebuild lifecycle mode") - deferredCmd.Flags(). - StringVar(&cmd.DotfilesRepo, "dotfiles-repo", "", "Dotfiles repository URL") - deferredCmd.Flags(). - StringVar(&cmd.DotfilesScript, "dotfiles-script", "", "Dotfiles install script path") - deferredCmd.Flags(). - StringSliceVar(&cmd.SecretsEnv, "secrets-env", []string{}, "Secrets to inject into lifecycle commands (KEY=VALUE)") - _ = deferredCmd.MarkFlagRequired("setup-info") + cliflags.Add( + deferredCmd, + cliflags.String(&cmd.SetupInfo, names.SetupInfo, "", "The container setup info"), + cliflags.Bool(&cmd.Prebuild, names.Prebuild, false, "If true, prebuild lifecycle mode"), + cliflags.String(&cmd.DotfilesRepo, names.DotfilesRepo, "", "Dotfiles repository URL"), + cliflags.String( + &cmd.DotfilesScript, + names.DotfilesScript, + "", + "Dotfiles install script path", + ), + cliflags.StringSlice( + &cmd.SecretsEnv, + names.SecretsEnv, + []string{}, + "Secrets to inject into lifecycle commands (KEY=VALUE)", + ), + ) + _ = deferredCmd.MarkFlagRequired(names.SetupInfo) return deferredCmd } diff --git a/cmd/internal/agentcontainer/post_attach.go b/cmd/internal/agentcontainer/post_attach.go index 8e6675f2b..012493280 100644 --- a/cmd/internal/agentcontainer/post_attach.go +++ b/cmd/internal/agentcontainer/post_attach.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/pkg/compress" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/setup" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" ) @@ -34,10 +36,17 @@ func NewPostAttachCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - postAttachCmd.Flags().StringVar(&cmd.SetupInfo, "setup-info", "", "The container setup info") - postAttachCmd.Flags(). - StringSliceVar(&cmd.SecretsEnv, "secrets-env", []string{}, "Secrets to inject into lifecycle commands (KEY=VALUE)") - _ = postAttachCmd.MarkFlagRequired("setup-info") + cliflags.Add( + postAttachCmd, + cliflags.String(&cmd.SetupInfo, names.SetupInfo, "", "The container setup info"), + cliflags.StringSlice( + &cmd.SecretsEnv, + names.SecretsEnv, + []string{}, + "Secrets to inject into lifecycle commands (KEY=VALUE)", + ), + ) + _ = postAttachCmd.MarkFlagRequired(names.SetupInfo) return postAttachCmd } diff --git a/cmd/internal/agentcontainer/setup.go b/cmd/internal/agentcontainer/setup.go index eebc6531c..a0d23309d 100644 --- a/cmd/internal/agentcontainer/setup.go +++ b/cmd/internal/agentcontainer/setup.go @@ -30,6 +30,8 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer/setup" "github.com/devsy-org/devsy/pkg/dockercredentials" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/git" "github.com/devsy-org/devsy/pkg/ide/fleet" "github.com/devsy-org/devsy/pkg/ide/jetbrains" @@ -73,32 +75,54 @@ func NewSetupContainerCmd(globalFlags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - setupContainerCmd.Flags(). - BoolVar(&cmd.StreamMounts, "stream-mounts", false, "If true, will try to stream the bind mounts from the host") - setupContainerCmd.Flags(). - BoolVar(&cmd.Prebuild, "prebuild", false, - "If true, only run prebuild lifecycle hooks (onCreateCommand + updateContentCommand)") - setupContainerCmd.Flags(). - BoolVar(&cmd.ChownWorkspace, "chown-workspace", false, "If Devsy should chown the workspace to the remote user") - setupContainerCmd.Flags(). - BoolVar(&cmd.InjectGitCredentials, "inject-git-credentials", false, - "If Devsy should inject git credentials during setup") - setupContainerCmd.Flags(). - StringVar(&cmd.ContainerWorkspaceInfo, "container-workspace-info", "", "The container workspace info") - setupContainerCmd.Flags(). - StringVar(&cmd.SetupInfo, "setup-info", "", "The container setup info") - setupContainerCmd.Flags().StringVar(&cmd.AccessKey, "access-key", "", "Access Key to use") - setupContainerCmd.Flags(). - StringVar(&cmd.WorkspaceHost, "workspace-host", "", "Workspace hostname to use") - setupContainerCmd.Flags().StringVar(&cmd.PlatformHost, "platform-host", "", "Platform host") - setupContainerCmd.Flags(). - StringVar(&cmd.DotfilesRepo, "dotfiles-repo", "", "Dotfiles repository URL") - setupContainerCmd.Flags(). - StringVar(&cmd.DotfilesScript, "dotfiles-script", "", "Dotfiles install script path") - _ = setupContainerCmd.MarkFlagRequired("setup-info") - - flags.BindEnv(setupContainerCmd.Flags(), "access-key") - flags.BindEnv(setupContainerCmd.Flags(), "platform-host") + cliflags.Add( + setupContainerCmd, + cliflags.Bool( + &cmd.StreamMounts, + names.StreamMounts, + false, + "If true, will try to stream the bind mounts from the host", + ), + cliflags.Bool( + &cmd.Prebuild, + names.Prebuild, + false, + "If true, only run prebuild lifecycle hooks (onCreateCommand + updateContentCommand)", + ), + cliflags.Bool( + &cmd.ChownWorkspace, + names.ChownWorkspace, + false, + "If Devsy should chown the workspace to the remote user", + ), + cliflags.Bool( + &cmd.InjectGitCredentials, + names.InjectGitCredentials, + false, + "If Devsy should inject git credentials during setup", + ), + cliflags.String( + &cmd.ContainerWorkspaceInfo, + names.ContainerWorkspaceInfo, + "", + "The container workspace info", + ), + cliflags.String(&cmd.SetupInfo, names.SetupInfo, "", "The container setup info"), + cliflags.String(&cmd.AccessKey, names.AccessKey, "", "Access Key to use"), + cliflags.String(&cmd.WorkspaceHost, names.WorkspaceHost, "", "Workspace hostname to use"), + cliflags.String(&cmd.PlatformHost, names.PlatformHost, "", "Platform host"), + cliflags.String(&cmd.DotfilesRepo, names.DotfilesRepo, "", "Dotfiles repository URL"), + cliflags.String( + &cmd.DotfilesScript, + names.DotfilesScript, + "", + "Dotfiles install script path", + ), + ) + _ = setupContainerCmd.MarkFlagRequired(names.SetupInfo) + + flags.BindEnv(setupContainerCmd.Flags(), names.AccessKey) + flags.BindEnv(setupContainerCmd.Flags(), names.PlatformHost) return setupContainerCmd } @@ -310,19 +334,19 @@ func buildDeferredHooksCmd( args := []string{ cmdInternal, cmdAgent, cmdContainer, "deferred-hooks", - "--setup-info", setupInfo, + names.Flag(names.SetupInfo), setupInfo, } if prebuild { - args = append(args, "--prebuild") + args = append(args, names.Flag(names.Prebuild)) } if dotfilesRepo != "" { - args = append(args, "--dotfiles-repo", dotfilesRepo) + args = append(args, names.Flag(names.DotfilesRepo), dotfilesRepo) } if dotfilesScript != "" { - args = append(args, "--dotfiles-script", dotfilesScript) + args = append(args, names.Flag(names.DotfilesScript), dotfilesScript) } if len(secretsEnv) > 0 { - args = append(args, "--secrets-env", strings.Join(secretsEnv, ",")) + args = append(args, names.Flag(names.SecretsEnv), strings.Join(secretsEnv, ",")) } return &exec.Cmd{ @@ -478,10 +502,10 @@ func (cmd *SetupContainerCmd) startContainerDaemon( args := []string{ cmdInternal, cmdAgent, cmdContainer, "daemon", - "--timeout", workspaceInfo.ContainerTimeout, + names.Flag(names.Timeout), workspaceInfo.ContainerTimeout, } if shutdownAction != "" { - args = append(args, "--shutdown-action", shutdownAction) + args = append(args, names.Flag(names.ShutdownAction), shutdownAction) } daemonCmd := &exec.Cmd{ @@ -506,12 +530,12 @@ func (cmd *SetupContainerCmd) startPostAttachHooks(sctx *setupContext) error { args := []string{ cmdInternal, cmdAgent, cmdContainer, "post-attach", - "--setup-info", cmd.SetupInfo, + names.Flag(names.SetupInfo), cmd.SetupInfo, } if len(sctx.workspaceInfo.CLIOptions.SecretsEnv) > 0 { args = append( args, - "--secrets-env", + names.Flag(names.SecretsEnv), strings.Join(sctx.workspaceInfo.CLIOptions.SecretsEnv, ","), ) } @@ -696,8 +720,8 @@ func (cmd *SetupContainerCmd) setupVSCode( args := []string{ cmdInternal, cmdAgent, cmdContainer, "vscode-async", - "--setup-info", cmd.SetupInfo, - "--flavor", string(flavor), + names.Flag(names.SetupInfo), cmd.SetupInfo, + names.Flag(names.Flavor), string(flavor), } //nolint:gosec // binaryPath is from os.Executable(), not user input @@ -773,7 +797,7 @@ func (cmd *SetupContainerCmd) startBrowserExtensionsInstall( cmdAgent, cmdContainer, b.asyncCmd, - "--setup-info", + names.Flag(names.SetupInfo), cmd.SetupInfo, ), nil }) diff --git a/cmd/internal/agentcontainer/ssh_server.go b/cmd/internal/agentcontainer/ssh_server.go index 298117ab6..6e73f4026 100644 --- a/cmd/internal/agentcontainer/ssh_server.go +++ b/cmd/internal/agentcontainer/ssh_server.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" sshserver "github.com/devsy-org/devsy/pkg/ssh/server" "github.com/devsy-org/devsy/pkg/ssh/server/port" @@ -31,12 +33,27 @@ func NewSSHServerCmd(flags *flags.GlobalFlags) *cobra.Command { RunE: cmd.Run, } - sshCmd.Flags(). - StringVar(&cmd.Address, "address", fmt.Sprintf("127.0.0.1:%d", sshserver.DefaultUserPort), "Address to listen to") - sshCmd.Flags(). - StringVar(&cmd.RemoteUser, "remote-user", "", "The remote user for this workspace") - sshCmd.Flags(). - StringVar(&cmd.Workdir, "workdir", "", "Directory where commands will run on the host") + cliflags.Add( + sshCmd, + cliflags.String( + &cmd.Address, + names.Address, + fmt.Sprintf("127.0.0.1:%d", sshserver.DefaultUserPort), + "Address to listen to", + ), + cliflags.String( + &cmd.RemoteUser, + names.RemoteUser, + "", + "The remote user for this workspace", + ), + cliflags.String( + &cmd.Workdir, + names.Workdir, + "", + "Directory where commands will run on the host", + ), + ) return sshCmd } diff --git a/cmd/internal/agentcontainer/vscode_async.go b/cmd/internal/agentcontainer/vscode_async.go index 9008cd826..6818c9ee0 100644 --- a/cmd/internal/agentcontainer/vscode_async.go +++ b/cmd/internal/agentcontainer/vscode_async.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/compress" "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/vscode" "github.com/spf13/cobra" ) @@ -27,11 +29,21 @@ func NewVSCodeAsyncCmd() *cobra.Command { Args: cobra.NoArgs, RunE: cmd.Run, } - vsCodeAsyncCmd.Flags().StringVar(&cmd.SetupInfo, "setup-info", "", "The container setup info") - _ = vsCodeAsyncCmd.MarkFlagRequired("setup-info") + cliflags.Add( + vsCodeAsyncCmd, + cliflags.String(&cmd.SetupInfo, names.SetupInfo, "", "The container setup info"), + ) + _ = vsCodeAsyncCmd.MarkFlagRequired(names.SetupInfo) - vsCodeAsyncCmd.Flags(). - StringVar(&cmd.Flavor, "flavor", string(vscode.FlavorStable), "The flavor of the VSCode distribution") + cliflags.Add( + vsCodeAsyncCmd, + cliflags.String( + &cmd.Flavor, + names.Flavor, + string(vscode.FlavorStable), + "The flavor of the VSCode distribution", + ), + ) return vsCodeAsyncCmd } diff --git a/cmd/internal/agentworkspace/build.go b/cmd/internal/agentworkspace/build.go index 3ad66ee7c..ee7c43e29 100644 --- a/cmd/internal/agentworkspace/build.go +++ b/cmd/internal/agentworkspace/build.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -32,8 +34,11 @@ func NewBuildCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - buildCmd.Flags().StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = buildCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + buildCmd, + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = buildCmd.MarkFlagRequired(names.WorkspaceInfo) return buildCmd } diff --git a/cmd/internal/agentworkspace/clean.go b/cmd/internal/agentworkspace/clean.go index 7259c31d1..71a472cc0 100644 --- a/cmd/internal/agentworkspace/clean.go +++ b/cmd/internal/agentworkspace/clean.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" ) @@ -42,10 +44,21 @@ This forces a fresh binary injection on the next workspace start.`, return cmd.Run(cobraCmd.Context(), args[0]) }, } - cleanCmd.Flags(). - StringVar(&cmd.DockerCommand, "docker-command", cleanDefaultDockerCmd, "Docker command to use") - cleanCmd.Flags(). - StringVar(&cmd.HelperImage, "helper-image", cleanHelperImage, "Helper image for volume operations") + cliflags.Add( + cleanCmd, + cliflags.String( + &cmd.DockerCommand, + names.DockerCommand, + cleanDefaultDockerCmd, + "Docker command to use", + ), + cliflags.String( + &cmd.HelperImage, + names.HelperImage, + cleanHelperImage, + "Helper image for volume operations", + ), + ) return cleanCmd } diff --git a/cmd/internal/agentworkspace/delete.go b/cmd/internal/agentworkspace/delete.go index 6f895dae0..5d267c7c5 100644 --- a/cmd/internal/agentworkspace/delete.go +++ b/cmd/internal/agentworkspace/delete.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/pkg/agent" agentdaemon "github.com/devsy-org/devsy/pkg/daemon/agent" "github.com/devsy-org/devsy/pkg/devcontainer" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -41,16 +43,24 @@ func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - deleteCmd.Flags(). - BoolVar(&cmd.Container, "container", true, "If enabled, cleans up the Devsy container") - deleteCmd.Flags(). - BoolVar(&cmd.Daemon, "daemon", false, "If enabled, cleans up the Devsy daemon") - - deleteCmd.Flags(). - BoolVar(&cmd.RemoveVolumes, "remove-volumes", false, "Remove named volumes associated with the workspace") - - deleteCmd.Flags().StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = deleteCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + deleteCmd, + cliflags.Bool( + &cmd.Container, + names.Container, + true, + "If enabled, cleans up the Devsy container", + ), + cliflags.Bool(&cmd.Daemon, names.Daemon, false, "If enabled, cleans up the Devsy daemon"), + cliflags.Bool( + &cmd.RemoveVolumes, + names.RemoveVolumes, + false, + "Remove named volumes associated with the workspace", + ), + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = deleteCmd.MarkFlagRequired(names.WorkspaceInfo) return deleteCmd } diff --git a/cmd/internal/agentworkspace/install_dotfiles.go b/cmd/internal/agentworkspace/install_dotfiles.go index 2e5c94abe..eeb0b508b 100644 --- a/cmd/internal/agentworkspace/install_dotfiles.go +++ b/cmd/internal/agentworkspace/install_dotfiles.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/git" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" @@ -37,13 +39,22 @@ func NewInstallDotfilesCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - installDotfilesCmd.Flags(). - StringVar(&cmd.Repository, "repository", "", "The dotfiles repository") - installDotfilesCmd.Flags(). - StringVar(&cmd.InstallScript, "install-script", "", "The dotfiles install command to execute") - installDotfilesCmd.Flags(). - BoolVar(&cmd.StrictHostKeyChecking, "strict-host-key-checking", false, - "Set to enable strict host key checking for git cloning via SSH") + cliflags.Add( + installDotfilesCmd, + cliflags.String(&cmd.Repository, names.Repository, "", "The dotfiles repository"), + cliflags.String( + &cmd.InstallScript, + names.InstallScript, + "", + "The dotfiles install command to execute", + ), + cliflags.Bool( + &cmd.StrictHostKeyChecking, + names.StrictHostKeyChecking, + false, + "Set to enable strict host key checking for git cloning via SSH", + ), + ) return installDotfilesCmd } diff --git a/cmd/internal/agentworkspace/logs.go b/cmd/internal/agentworkspace/logs.go index 630db0551..2d72bead9 100644 --- a/cmd/internal/agentworkspace/logs.go +++ b/cmd/internal/agentworkspace/logs.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/pkg/agent" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -32,8 +34,8 @@ func NewLogsCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - c.Flags().StringVar(&cmd.ID, "id", "", "The workspace id") - _ = c.MarkFlagRequired("id") + cliflags.Add(c, cliflags.String(&cmd.ID, names.ID, "", "The workspace id")) + _ = c.MarkFlagRequired(names.ID) return c } diff --git a/cmd/internal/agentworkspace/logs_daemon.go b/cmd/internal/agentworkspace/logs_daemon.go index d1b0905d1..2d757b566 100644 --- a/cmd/internal/agentworkspace/logs_daemon.go +++ b/cmd/internal/agentworkspace/logs_daemon.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -32,8 +34,8 @@ func NewLogsDaemonCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - logsDaemonCmd.Flags().StringVar(&cmd.ID, "id", "", "The workspace id") - _ = logsDaemonCmd.MarkFlagRequired("id") + cliflags.Add(logsDaemonCmd, cliflags.String(&cmd.ID, names.ID, "", "The workspace id")) + _ = logsDaemonCmd.MarkFlagRequired(names.ID) return logsDaemonCmd } diff --git a/cmd/internal/agentworkspace/setup_gpg.go b/cmd/internal/agentworkspace/setup_gpg.go index d8ba76706..f8c3808ea 100644 --- a/cmd/internal/agentworkspace/setup_gpg.go +++ b/cmd/internal/agentworkspace/setup_gpg.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/credentials" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/gitcredentials" "github.com/devsy-org/devsy/pkg/gpg" "github.com/devsy-org/devsy/pkg/log" @@ -35,12 +37,17 @@ func NewSetupGPGCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - setupGPGCmd.Flags(). - StringVar(&cmd.OwnerTrust, "ownertrust", "", "GPG Owner trust to import in armor form") - setupGPGCmd.Flags(). - StringVar(&cmd.SocketPath, "socketpath", "", "path to the gpg socket forwarded") - setupGPGCmd.Flags(). - StringVar(&cmd.GitKey, "gitkey", "", "gpg key to use for git commit signing") + cliflags.Add( + setupGPGCmd, + cliflags.String( + &cmd.OwnerTrust, + names.OwnerTrust, + "", + "GPG Owner trust to import in armor form", + ), + cliflags.String(&cmd.SocketPath, names.SocketPath, "", "path to the gpg socket forwarded"), + cliflags.String(&cmd.GitKey, names.GitKey, "", "gpg key to use for git commit signing"), + ) return setupGPGCmd } diff --git a/cmd/internal/agentworkspace/status.go b/cmd/internal/agentworkspace/status.go index 6b0f2a732..cb9e087bc 100644 --- a/cmd/internal/agentworkspace/status.go +++ b/cmd/internal/agentworkspace/status.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" "github.com/devsy-org/devsy/pkg/client" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -31,8 +33,11 @@ func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - statusCmd.Flags().StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = statusCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + statusCmd, + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = statusCmd.MarkFlagRequired(names.WorkspaceInfo) return statusCmd } diff --git a/cmd/internal/agentworkspace/stop.go b/cmd/internal/agentworkspace/stop.go index 00686e3c0..6dd137ec1 100644 --- a/cmd/internal/agentworkspace/stop.go +++ b/cmd/internal/agentworkspace/stop.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -31,8 +33,11 @@ func NewStopCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - stopCmd.Flags().StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = stopCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + stopCmd, + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = stopCmd.MarkFlagRequired(names.WorkspaceInfo) return stopCmd } diff --git a/cmd/internal/agentworkspace/up.go b/cmd/internal/agentworkspace/up.go index 39519d20a..c8b54d307 100644 --- a/cmd/internal/agentworkspace/up.go +++ b/cmd/internal/agentworkspace/up.go @@ -26,6 +26,8 @@ import ( "github.com/devsy-org/devsy/pkg/dockercredentials" "github.com/devsy-org/devsy/pkg/dockerinstall" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/util" @@ -52,8 +54,11 @@ func NewUpCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - upCmd.Flags().StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = upCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + upCmd, + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = upCmd.MarkFlagRequired(names.WorkspaceInfo) return upCmd } diff --git a/cmd/internal/agentworkspace/update_config.go b/cmd/internal/agentworkspace/update_config.go index 8754a562c..7ac9e2bca 100644 --- a/cmd/internal/agentworkspace/update_config.go +++ b/cmd/internal/agentworkspace/update_config.go @@ -5,6 +5,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/agent" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -28,9 +30,11 @@ func NewUpdateConfigCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - updateConfigCmd.Flags(). - StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = updateConfigCmd.MarkFlagRequired("workspace-info") + cliflags.Add( + updateConfigCmd, + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = updateConfigCmd.MarkFlagRequired(names.WorkspaceInfo) return updateConfigCmd } diff --git a/cmd/internal/browser_tunnel.go b/cmd/internal/browser_tunnel.go index c9cf0c84e..baaf0e0d5 100644 --- a/cmd/internal/browser_tunnel.go +++ b/cmd/internal/browser_tunnel.go @@ -12,6 +12,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/opener" pkglog "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/tunnel" @@ -50,25 +52,27 @@ func NewBrowserTunnelCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Workspace, "workspace", "", "Workspace name or ID") - c.Flags().StringVar(&cmd.TargetURL, "target-url", "", "Target URL for the browser IDE") - c.Flags().StringVar(&cmd.AuthSockID, "auth-sock-id", "", "Reused SSH_AUTH_SOCK id") - c.Flags().BoolVar(&cmd.ForwardPorts, "forward-ports", false, "Whether to forward ports") - c.Flags().StringArrayVar(&cmd.ExtraPorts, "extra-ports", nil, "Extra ports to forward") - c.Flags().StringVar(&cmd.User, "user", "", "Remote user") - c.Flags(). - StringVar(&cmd.GitSSHSigningKey, "git-ssh-signing-key", "", "Git SSH signing key") - c.Flags().StringArrayVar( - &cmd.InheritListeners, - "inherit-listener", - nil, - "Inherited listener fd, format host:port=fd (repeatable, unix only)", - ) - c.Flags().BoolVar( - &cmd.OpenBrowser, - "open-browser", - false, - "Open a host browser pointing at --target-url once the local listener is reachable", + cliflags.Add( + c, + cliflags.String(&cmd.Workspace, names.Workspace, "", "Workspace name or ID"), + cliflags.String(&cmd.TargetURL, names.TargetURL, "", "Target URL for the browser IDE"), + cliflags.String(&cmd.AuthSockID, names.AuthSockID, "", "Reused SSH_AUTH_SOCK id"), + cliflags.Bool(&cmd.ForwardPorts, names.ForwardPorts, false, "Whether to forward ports"), + cliflags.StringArray(&cmd.ExtraPorts, names.ExtraPorts, nil, "Extra ports to forward"), + cliflags.String(&cmd.User, names.User, "", "Remote user"), + cliflags.String(&cmd.GitSSHSigningKey, names.GitSSHSigningKey, "", "Git SSH signing key"), + cliflags.StringArray( + &cmd.InheritListeners, + names.InheritListener, + nil, + "Inherited listener fd, format host:port=fd (repeatable, unix only)", + ), + cliflags.Bool( + &cmd.OpenBrowser, + names.OpenBrowser, + false, + "Open a host browser pointing at --target-url once the local listener is reachable", + ), ) return c } diff --git a/cmd/internal/browser_tunnel_test.go b/cmd/internal/browser_tunnel_test.go index d0a5ed1e1..acfa47b66 100644 --- a/cmd/internal/browser_tunnel_test.go +++ b/cmd/internal/browser_tunnel_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" ) // TestNewBrowserTunnelCmd_OpenBrowserFlag locks down the flag plumbing for @@ -20,7 +21,7 @@ func TestNewBrowserTunnelCmd_OpenBrowserFlag(t *testing.T) { }); err != nil { t.Fatalf("ParseFlags: %v", err) } - openFlag := c.Flags().Lookup("open-browser") + openFlag := c.Flags().Lookup(names.OpenBrowser) if openFlag == nil { t.Fatal("--open-browser flag not registered") } @@ -34,9 +35,9 @@ func TestNewBrowserTunnelCmd_OpenBrowserFlag(t *testing.T) { if err := c2.ParseFlags([]string{"--workspace", "ws"}); err != nil { t.Fatalf("ParseFlags (default): %v", err) } - if c2.Flags().Lookup("open-browser").Value.String() != "false" { + if c2.Flags().Lookup(names.OpenBrowser).Value.String() != "false" { t.Errorf("--open-browser default = %q, want false", - c2.Flags().Lookup("open-browser").Value.String()) + c2.Flags().Lookup(names.OpenBrowser).Value.String()) } } diff --git a/cmd/internal/container_tunnel.go b/cmd/internal/container_tunnel.go index f4aa52cc5..4f63acf18 100644 --- a/cmd/internal/container_tunnel.go +++ b/cmd/internal/container_tunnel.go @@ -15,6 +15,8 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/encoding" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -42,11 +44,11 @@ func NewContainerTunnelCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - containerTunnelCmd.Flags(). - StringVar(&cmd.User, "user", "", "The user to create the tunnel with") - containerTunnelCmd.Flags(). - StringVar(&cmd.WorkspaceInfo, "workspace-info", "", "The workspace info") - _ = containerTunnelCmd.MarkFlagRequired("workspace-info") + cliflags.Add(containerTunnelCmd, + cliflags.String(&cmd.User, names.User, "", "The user to create the tunnel with"), + cliflags.String(&cmd.WorkspaceInfo, names.WorkspaceInfo, "", "The workspace info"), + ) + _ = containerTunnelCmd.MarkFlagRequired(names.WorkspaceInfo) return containerTunnelCmd } diff --git a/cmd/internal/fleet_server.go b/cmd/internal/fleet_server.go index bdea9cf64..734b766cc 100644 --- a/cmd/internal/fleet_server.go +++ b/cmd/internal/fleet_server.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" ) @@ -31,7 +33,15 @@ func NewFleetServerCmd(flags *flags.GlobalFlags) *cobra.Command { RunE: cmd.Run, } - fleetCmd.Flags().StringVar(&cmd.WorkspaceID, "workspaceid", "", "Fleet WorkspaceID to monitor") + cliflags.Add( + fleetCmd, + cliflags.String( + &cmd.WorkspaceID, + names.FleetWorkspaceID, + "", + "Fleet WorkspaceID to monitor", + ), + ) return fleetCmd } diff --git a/cmd/internal/get.go b/cmd/internal/get.go index f988854d8..37b1820a8 100644 --- a/cmd/internal/get.go +++ b/cmd/internal/get.go @@ -8,6 +8,8 @@ import ( "regexp" "strings" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" "github.com/tidwall/gjson" ) @@ -29,8 +31,12 @@ func NewGetCmd() *cobra.Command { }, } - getCmd.Flags().StringVarP(&cmd.File, "file", "f", "", "Parse this json file instead of STDIN") - getCmd.Flags().BoolVar(&cmd.Fail, "fail", false, "Fail if value is not found") + cliflags.Add( + getCmd, + cliflags.String(&cmd.File, names.File, "", "Parse this json file instead of STDIN"). + Shorthand("f"), + cliflags.Bool(&cmd.Fail, names.Fail, false, "Fail if value is not found"), + ) return getCmd } diff --git a/cmd/internal/get_workspace_config.go b/cmd/internal/get_workspace_config.go index eb9875abe..f5901f9c7 100644 --- a/cmd/internal/get_workspace_config.go +++ b/cmd/internal/get_workspace_config.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/spf13/cobra" ) @@ -50,10 +52,21 @@ func NewGetWorkspaceConfigCommand(flags *flags.GlobalFlags) *cobra.Command { }, } - shellCmd.Flags(). - DurationVar(&cmd.timeout, "timeout", 10*time.Second, "Timeout for the command, 10 seconds by default") - shellCmd.Flags(). - IntVar(&cmd.maxDepth, "max-depth", 3, "Maximum depth to search for devcontainer files") + cliflags.Add( + shellCmd, + cliflags.Duration( + &cmd.timeout, + names.Timeout, + 10*time.Second, + "Timeout for the command, 10 seconds by default", + ), + cliflags.Int( + &cmd.maxDepth, + names.MaxDepth, + 3, + "Maximum depth to search for devcontainer files", + ), + ) return shellCmd } @@ -89,9 +102,7 @@ func (cmd *GetWorkspaceConfigCommand) Run( rawSource, tmpDir, cmd.maxDepth, - devsyConfig.ContextOption( - config.ContextOptionSSHStrictHostKeyChecking, - ) == config.BoolTrue, + devsyConfig.ContextOptionBool(config.ContextOptionSSHStrictHostKeyChecking), ) if err != nil { errChan <- err diff --git a/cmd/internal/git_credentials.go b/cmd/internal/git_credentials.go index 3d1e81b46..6e630e98d 100644 --- a/cmd/internal/git_credentials.go +++ b/cmd/internal/git_credentials.go @@ -14,6 +14,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/cmd/internal/agentcontainer" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/gitcredentials" devsyhttp "github.com/devsy-org/devsy/pkg/http" "github.com/devsy-org/devsy/pkg/ts" @@ -39,7 +41,10 @@ func NewGitCredentialsCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context(), args) }, } - gitCredentialsCmd.Flags().IntVar(&cmd.Port, "port", 0, "If specified, will use the given port") + cliflags.Add( + gitCredentialsCmd, + cliflags.Int(&cmd.Port, names.Port, 0, "If specified, will use the given port"), + ) return gitCredentialsCmd } diff --git a/cmd/internal/request.go b/cmd/internal/request.go index ee5cc72ec..0568e0aba 100644 --- a/cmd/internal/request.go +++ b/cmd/internal/request.go @@ -8,6 +8,8 @@ import ( "os" "strings" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" devsyhttp "github.com/devsy-org/devsy/pkg/http" "github.com/spf13/cobra" ) @@ -31,12 +33,19 @@ func NewRequestCmd() *cobra.Command { }, } - requestCmd.Flags().StringVarP(&cmd.Data, "data", "d", "", "Request Data") - requestCmd.Flags().StringVarP(&cmd.Method, "request", "X", "GET", "Request Type") - requestCmd.Flags().StringSliceVarP(&cmd.Headers, "header", "H", []string{}, "Extra Headers") - requestCmd.Flags(). - BoolVar(&cmd.FailOnErrorCode, "fail-on-error-code", true, - "Let this command fail if the remote is returning an error code") + cliflags.Add( + requestCmd, + cliflags.String(&cmd.Data, names.Data, "", "Request Data").Shorthand("d"), + cliflags.String(&cmd.Method, names.Request, "GET", "Request Type").Shorthand("X"), + cliflags.StringSlice(&cmd.Headers, names.Header, []string{}, "Extra Headers"). + Shorthand("H"), + cliflags.Bool( + &cmd.FailOnErrorCode, + names.FailOnErrorCode, + true, + "Let this command fail if the remote is returning an error code", + ), + ) return requestCmd } diff --git a/cmd/internal/runusercommands.go b/cmd/internal/runusercommands.go index 0421e4546..f8bd26c73 100644 --- a/cmd/internal/runusercommands.go +++ b/cmd/internal/runusercommands.go @@ -13,6 +13,8 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer" devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/docker" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/types" workspace2 "github.com/devsy-org/devsy/pkg/workspace" @@ -52,71 +54,88 @@ func NewRunUserCommandsCmd(f *flags.GlobalFlags) *cobra.Command { RunE: runE, } - runCmd.Flags(). - StringVar( + cliflags.Add( + runCmd, + cliflags.String( &cmd.WorkspaceFolder, - "workspace-folder", + names.WorkspaceFolder, "", "Path to the workspace folder", - ) - runCmd.Flags(). - StringVar( + ), + cliflags.String( &cmd.ContainerID, - "container-id", + names.ContainerID, "", "Target a specific container by ID", - ) - runCmd.Flags(). - StringVar( + ), + cliflags.String( &cmd.DockerPath, - "docker-path", + names.DockerPath, "", "Path to the docker/podman executable (defaults to 'docker')", - ) - runCmd.Flags(). - StringVar( + ), + cliflags.String( &cmd.Config, - "config", + names.Config, "", "Path to the devcontainer.json configuration file", - ) - runCmd.Flags(). - StringVar( + ), + cliflags.String( &cmd.OverrideConfig, - "override-config", + names.OverrideConfig, "", "Path to an additional devcontainer.json file to override the primary configuration", - ) - runCmd.Flags(). - StringArrayVar( + ), + cliflags.StringArray( &cmd.RemoteEnv, - "remote-env", + names.RemoteEnv, []string{}, "Environment variables to set in the container (KEY=VALUE format, can be specified multiple times)", - ) - runCmd.Flags(). - StringArrayVar( + ), + cliflags.StringArray( &cmd.IDLabels, - "id-label", + names.IDLabel, []string{}, "Override the default container identification labels (format: key=value, can be specified multiple times)", - ) - runCmd.Flags(). - BoolVar(&cmd.Prebuild, "prebuild", false, - "Stop lifecycle execution after onCreateCommand and updateContentCommand") - runCmd.Flags(). - BoolVar(&cmd.SkipNonBlockingCommands, "skip-non-blocking-commands", false, - "Skip non-blocking lifecycle commands (stop after the waitFor-configured command)") - runCmd.Flags(). - BoolVar(&cmd.SkipPostCreate, "skip-post-create", false, "Skip running postCreateCommand") - runCmd.Flags(). - BoolVar(&cmd.SkipPostStart, "skip-post-start", false, "Skip running postStartCommand") - runCmd.Flags(). - BoolVar(&cmd.SkipPostAttach, "skip-post-attach", false, "Skip running postAttachCommand") - runCmd.Flags(). - BoolVar(&cmd.SkipOnCreate, "skip-on-create", false, "Skip running onCreateCommand") - runCmd.Flags(). - BoolVar(&cmd.SkipUpdateContent, "skip-update-content", false, "Skip running updateContentCommand") + ), + cliflags.Bool( + &cmd.Prebuild, + names.Prebuild, + false, + "Stop lifecycle execution after onCreateCommand and updateContentCommand", + ), + cliflags.Bool( + &cmd.SkipNonBlockingCommands, + names.SkipNonBlockingCommands, + false, + "Skip non-blocking lifecycle commands (stop after the waitFor-configured command)", + ), + cliflags.Bool( + &cmd.SkipPostCreate, + names.SkipPostCreate, + false, + "Skip running postCreateCommand", + ), + cliflags.Bool( + &cmd.SkipPostStart, + names.SkipPostStart, + false, + "Skip running postStartCommand", + ), + cliflags.Bool( + &cmd.SkipPostAttach, + names.SkipPostAttach, + false, + "Skip running postAttachCommand", + ), + cliflags.Bool(&cmd.SkipOnCreate, names.SkipOnCreate, false, "Skip running onCreateCommand"), + cliflags.Bool( + &cmd.SkipUpdateContent, + names.SkipUpdateContent, + false, + "Skip running updateContentCommand", + ), + ) return runCmd } @@ -162,7 +181,11 @@ func (cmd *RunUserCommandsCmd) Run(ctx context.Context) error { func (cmd *RunUserCommandsCmd) validate() error { if cmd.WorkspaceFolder == "" && cmd.ContainerID == "" { - return fmt.Errorf("either --workspace-folder or --container-id must be provided") + return fmt.Errorf( + "either %s or %s must be provided", + names.Flag(names.WorkspaceFolder), + names.Flag(names.ContainerID), + ) } if cmd.ContainerID != "" && cmd.WorkspaceFolder == "" && cmd.Config == "" { return fmt.Errorf( @@ -405,7 +428,11 @@ func (cmd *RunUserCommandsCmd) runLifecycleHooks( for i, hook := range hooks { if cmd.Prebuild && i >= 2 { - log.Infof("stopping lifecycle execution (--prebuild: after %s)", updateContentCommand) + log.Infof( + "stopping lifecycle execution (%s: after %s)", + names.Flag(names.Prebuild), + updateContentCommand, + ) return nil } if cmd.SkipNonBlockingCommands && i > waitForBoundary { diff --git a/cmd/internal/runusercommands_test.go b/cmd/internal/runusercommands_test.go index e0d100da0..142f031ca 100644 --- a/cmd/internal/runusercommands_test.go +++ b/cmd/internal/runusercommands_test.go @@ -6,6 +6,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/cmd/workspace" devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -69,91 +70,91 @@ func TestNewRunUserCommandsCmd_ContainerIDWithoutConfigFails(t *testing.T) { func TestNewRunUserCommandsCmd_ContainerIDFlagExists(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("container-id") + f := cmd.Flags().Lookup(names.ContainerID) require.NotNil(t, f) assert.Equal(t, "", f.DefValue) } func TestNewRunUserCommandsCmd_IDLabelFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("id-label") + f := cmd.Flags().Lookup(names.IDLabel) require.NotNil(t, f) assert.Equal(t, "stringArray", f.Value.Type()) } func TestNewRunUserCommandsCmd_DockerPathFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("docker-path") + f := cmd.Flags().Lookup(names.DockerPath) require.NotNil(t, f) assert.Equal(t, "", f.DefValue) } func TestNewRunUserCommandsCmd_ConfigFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("config") + f := cmd.Flags().Lookup(names.Config) require.NotNil(t, f) assert.Equal(t, "", f.DefValue) } func TestNewRunUserCommandsCmd_OverrideConfigFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("override-config") + f := cmd.Flags().Lookup(names.OverrideConfig) require.NotNil(t, f) assert.Equal(t, "", f.DefValue) } func TestNewRunUserCommandsCmd_RemoteEnvFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("remote-env") + f := cmd.Flags().Lookup(names.RemoteEnv) require.NotNil(t, f) assert.Equal(t, "stringArray", f.Value.Type()) } func TestNewRunUserCommandsCmd_PrebuildFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("prebuild") + f := cmd.Flags().Lookup(names.Prebuild) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestNewRunUserCommandsCmd_SkipNonBlockingCommandsFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-non-blocking-commands") + f := cmd.Flags().Lookup(names.SkipNonBlockingCommands) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestRunUserCommandsCmd_SkipPostCreateFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-post-create") + f := cmd.Flags().Lookup(names.SkipPostCreate) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestRunUserCommandsCmd_SkipPostStartFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-post-start") + f := cmd.Flags().Lookup(names.SkipPostStart) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestRunUserCommandsCmd_SkipPostAttachFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-post-attach") + f := cmd.Flags().Lookup(names.SkipPostAttach) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestRunUserCommandsCmd_SkipOnCreateFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-on-create") + f := cmd.Flags().Lookup(names.SkipOnCreate) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } func TestRunUserCommandsCmd_SkipUpdateContentFlag(t *testing.T) { cmd := NewRunUserCommandsCmd(&flags.GlobalFlags{}) - f := cmd.Flags().Lookup("skip-update-content") + f := cmd.Flags().Lookup(names.SkipUpdateContent) require.NotNil(t, f) assert.Equal(t, "false", f.DefValue) } @@ -170,23 +171,23 @@ func TestRunUserCommandsCmd_SkipFlagsParseValues(t *testing.T) { }) require.NoError(t, err) - val, err := cmd.Flags().GetBool("skip-post-create") + val, err := cmd.Flags().GetBool(names.SkipPostCreate) require.NoError(t, err) assert.True(t, val) - val, err = cmd.Flags().GetBool("skip-post-start") + val, err = cmd.Flags().GetBool(names.SkipPostStart) require.NoError(t, err) assert.True(t, val) - val, err = cmd.Flags().GetBool("skip-post-attach") + val, err = cmd.Flags().GetBool(names.SkipPostAttach) require.NoError(t, err) assert.True(t, val) - val, err = cmd.Flags().GetBool("skip-on-create") + val, err = cmd.Flags().GetBool(names.SkipOnCreate) require.NoError(t, err) assert.True(t, val) - val, err = cmd.Flags().GetBool("skip-update-content") + val, err = cmd.Flags().GetBool(names.SkipUpdateContent) require.NoError(t, err) assert.True(t, val) } @@ -206,31 +207,31 @@ func TestRunUserCommandsCmd_NewFlagsParseValues(t *testing.T) { }) require.NoError(t, err) - dockerPath, err := cmd.Flags().GetString("docker-path") + dockerPath, err := cmd.Flags().GetString(names.DockerPath) require.NoError(t, err) assert.Equal(t, "/usr/local/bin/podman", dockerPath) - configPath, err := cmd.Flags().GetString("config") + configPath, err := cmd.Flags().GetString(names.Config) require.NoError(t, err) assert.Equal(t, ".devcontainer/devcontainer.json", configPath) - overridePath, err := cmd.Flags().GetString("override-config") + overridePath, err := cmd.Flags().GetString(names.OverrideConfig) require.NoError(t, err) assert.Equal(t, "/tmp/override.json", overridePath) - remoteEnv, err := cmd.Flags().GetStringArray("remote-env") + remoteEnv, err := cmd.Flags().GetStringArray(names.RemoteEnv) require.NoError(t, err) assert.Equal(t, []string{testEnvFoo, testEnvBaz}, remoteEnv) - prebuild, err := cmd.Flags().GetBool("prebuild") + prebuild, err := cmd.Flags().GetBool(names.Prebuild) require.NoError(t, err) assert.True(t, prebuild) - skipNonBlocking, err := cmd.Flags().GetBool("skip-non-blocking-commands") + skipNonBlocking, err := cmd.Flags().GetBool(names.SkipNonBlockingCommands) require.NoError(t, err) assert.True(t, skipNonBlocking) - containerID, err := cmd.Flags().GetString("container-id") + containerID, err := cmd.Flags().GetString(names.ContainerID) require.NoError(t, err) assert.Equal(t, testContainerIDHex, containerID) } diff --git a/cmd/internal/sh.go b/cmd/internal/sh.go index 9f612bbae..e1834594a 100644 --- a/cmd/internal/sh.go +++ b/cmd/internal/sh.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/shell" "github.com/spf13/cobra" ) @@ -25,8 +27,12 @@ func NewShellCmd() *cobra.Command { }, } - shellCmd.Flags().BoolVarP(&cmd.Login, "login", "l", false, "If login shell should be used") - shellCmd.Flags().StringVarP(&cmd.Command, "command", "c", "", "Command to execute") + cliflags.Add( + shellCmd, + cliflags.Bool(&cmd.Login, names.Login, false, "If login shell should be used"). + Shorthand("l"), + cliflags.String(&cmd.Command, names.Command, "", "Command to execute").Shorthand("c"), + ) return shellCmd } diff --git a/cmd/internal/ssh_client.go b/cmd/internal/ssh_client.go index 601f09b14..08b4fd6e2 100644 --- a/cmd/internal/ssh_client.go +++ b/cmd/internal/ssh_client.go @@ -6,6 +6,8 @@ import ( "os" command2 "github.com/devsy-org/devsy/pkg/command" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" ) @@ -27,10 +29,12 @@ func NewSSHClientCmd() *cobra.Command { }, } - sshCmd.Flags().StringVar(&cmd.KeyFile, "key-file", "", "SSH Key file to use") - sshCmd.Flags().StringVar(&cmd.Address, "address", "", "Address to connect to") - sshCmd.Flags().StringVar(&cmd.User, "user", "root", "User to connect as") - _ = sshCmd.MarkFlagRequired("address") + cliflags.Add(sshCmd, + cliflags.String(&cmd.KeyFile, names.KeyFile, "", "SSH Key file to use"), + cliflags.String(&cmd.Address, names.Address, "", "Address to connect to"), + cliflags.String(&cmd.User, names.User, "root", "User to connect as"), + ) + _ = sshCmd.MarkFlagRequired(names.Address) return sshCmd } diff --git a/cmd/internal/ssh_git_clone.go b/cmd/internal/ssh_git_clone.go index 0d14156e0..2f0d538b8 100644 --- a/cmd/internal/ssh_git_clone.go +++ b/cmd/internal/ssh_git_clone.go @@ -8,6 +8,8 @@ import ( "strings" command2 "github.com/devsy-org/devsy/pkg/command" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" "golang.org/x/crypto/ssh" ) @@ -27,9 +29,11 @@ func NewSSHGitCloneCmd() *cobra.Command { }, } - sshCmd.Flags().StringArrayVar(&cmd.KeyFiles, "key-file", []string{}, "SSH Key file to use") - sshCmd.Flags().StringVar(&cmd.Port, "port", "22", "SSH port to use, defaults to 22") - _ = sshCmd.MarkFlagRequired("key-file") + cliflags.Add(sshCmd, + cliflags.StringArray(&cmd.KeyFiles, names.KeyFile, []string{}, "SSH Key file to use"), + cliflags.String(&cmd.Port, names.Port, "22", "SSH port to use, defaults to 22"), + ) + _ = sshCmd.MarkFlagRequired(names.KeyFile) return sshCmd } diff --git a/cmd/internal/ssh_server.go b/cmd/internal/ssh_server.go index 4eb3d342a..187c69d60 100644 --- a/cmd/internal/ssh_server.go +++ b/cmd/internal/ssh_server.go @@ -11,6 +11,7 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" sshserver "github.com/devsy-org/devsy/pkg/ssh/server" "github.com/devsy-org/devsy/pkg/ssh/server/port" @@ -54,18 +55,18 @@ func NewSSHServerCmd(globalFlags *flags.GlobalFlags) *cobra.Command { } f := sshCmd.Flags() - f.StringVar(&cmd.address, "address", + f.StringVar(&cmd.address, names.Address, fmt.Sprintf("0.0.0.0:%d", sshserver.DefaultPort), "Address to listen to") - f.BoolVar(&cmd.stdio, "stdio", false, + f.BoolVar(&cmd.stdio, names.Stdio, false, "Listen on stdin/stdout instead of an address") - f.BoolVar(&cmd.trackActivity, "track-activity", false, + f.BoolVar(&cmd.trackActivity, names.TrackActivity, false, "Touch the activity file every "+activityHeartbeatInterval.String()+" (only with --stdio)") - f.StringVar(&cmd.reuseSSHAuthSock, "reuse-ssh-auth-sock", "", + f.StringVar(&cmd.reuseSSHAuthSock, names.ReuseSSHAuthSock, "", "If set, reuse a pre-existing SSH_AUTH_SOCK in the workspace under /tmp") - _ = f.MarkHidden("reuse-ssh-auth-sock") - f.StringVar(&cmd.token, "token", "", "Base64 encoded token to use") - f.StringVar(&cmd.workdir, "workdir", "", + _ = f.MarkHidden(names.ReuseSSHAuthSock) + f.StringVar(&cmd.token, names.Token, "", "Base64 encoded token to use") + f.StringVar(&cmd.workdir, names.Workdir, "", "Directory where commands will run on the host") return sshCmd } diff --git a/cmd/machine/create.go b/cmd/machine/create.go index 71080b0ed..5486f6187 100644 --- a/cmd/machine/create.go +++ b/cmd/machine/create.go @@ -5,6 +5,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -28,8 +30,15 @@ func NewCreateCmd(flags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context(), args) }, } - createCmd.Flags(). - StringSliceVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE") + cliflags.Add( + createCmd, + cliflags.StringSlice( + &cmd.ProviderOptions, + names.ProviderOption, + []string{}, + "Provider option in the form KEY=VALUE", + ), + ) return createCmd } diff --git a/cmd/machine/delete.go b/cmd/machine/delete.go index 99d2de123..1005ea8c6 100644 --- a/cmd/machine/delete.go +++ b/cmd/machine/delete.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -34,10 +36,12 @@ func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - deleteCmd.Flags(). - StringVar(&cmd.GracePeriod, "grace-period", "", "The amount of time to give the command to delete the workspace") - deleteCmd.Flags(). - BoolVar(&cmd.Force, "force", false, "Delete workspace even if it is not found remotely anymore") + cliflags.Add(deleteCmd, + cliflags.String(&cmd.GracePeriod, names.GracePeriod, "", + "The amount of time to give the command to delete the workspace"), + cliflags.Bool(&cmd.Force, names.Force, false, + "Delete workspace even if it is not found remotely anymore"), + ) return deleteCmd } diff --git a/cmd/machine/ssh.go b/cmd/machine/ssh.go index 29a9b9e23..7b50191f8 100644 --- a/cmd/machine/ssh.go +++ b/cmd/machine/ssh.go @@ -14,6 +14,8 @@ import ( devagent "github.com/devsy-org/devsy/pkg/agent" "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/pty" devssh "github.com/devsy-org/devsy/pkg/ssh" @@ -87,14 +89,16 @@ func NewSSHCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - sshCmd.Flags(). - StringVar(&cmd.Command, "command", "", "The command to execute on the remote machine") - sshCmd.Flags(). - BoolVar(&cmd.AgentForwarding, "agent-forwarding", false, "If true, will forward the local ssh keys") - sshCmd.Flags().StringVar(&cmd.TermMode, "term-mode", TermModeAuto, termModeUsage) - sshCmd.Flags().BoolVar(&cmd.InstallTerminfo, "install-terminfo", false, installUsage) + cliflags.Add(sshCmd, + cliflags.String(&cmd.Command, names.Command, "", + "The command to execute on the remote machine"), + cliflags.Bool(&cmd.AgentForwarding, names.AgentForwarding, false, + "If true, will forward the local ssh keys"), + cliflags.String(&cmd.TermMode, names.TermMode, TermModeAuto, termModeUsage), + cliflags.Bool(&cmd.InstallTerminfo, names.InstallTerminfo, false, installUsage), + ) _ = sshCmd.RegisterFlagCompletionFunc( - "term-mode", + names.TermMode, func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { return []string{ TermModeAuto, diff --git a/cmd/mcp/serve.go b/cmd/mcp/serve.go index 7f3ddcedb..de5ca2ca1 100644 --- a/cmd/mcp/serve.go +++ b/cmd/mcp/serve.go @@ -6,6 +6,8 @@ import ( "time" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/version" sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp" @@ -32,12 +34,27 @@ func NewServeCmd(globalFlags *flags.GlobalFlags) *cobra.Command { return cmd.Run(cobraCmd.Context()) }, } - cobraCmd.Flags().DurationVar(&cmd.ExecTimeoutDefault, "exec-timeout-default", 5*time.Minute, - "Default timeout for workspace_exec calls") - cobraCmd.Flags().DurationVar(&cmd.ExecTimeoutMax, "exec-timeout-max", 30*time.Minute, - "Maximum timeout for workspace_exec calls (caller values are clamped)") - cobraCmd.Flags().IntVar(&cmd.ExecOutputCap, "exec-output-cap", 100*1024, - "Per-stream byte cap for workspace_exec output; excess is replaced with a truncation marker") + cliflags.Add( + cobraCmd, + cliflags.Duration( + &cmd.ExecTimeoutDefault, + names.ExecTimeoutDefault, + 5*time.Minute, + "Default timeout for workspace_exec calls", + ), + cliflags.Duration( + &cmd.ExecTimeoutMax, + names.ExecTimeoutMax, + 30*time.Minute, + "Maximum timeout for workspace_exec calls (caller values are clamped)", + ), + cliflags.Int( + &cmd.ExecOutputCap, + names.ExecOutputCap, + 100*1024, + "Per-stream byte cap for workspace_exec output; excess is replaced with a truncation marker", + ), + ) return cobraCmd } diff --git a/cmd/minor_flags_test.go b/cmd/minor_flags_test.go index 6ca00e0f3..dd09c5097 100644 --- a/cmd/minor_flags_test.go +++ b/cmd/minor_flags_test.go @@ -6,13 +6,14 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/cmd/workspace" "github.com/devsy-org/devsy/cmd/workspace/up" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUpCmd_ContainerDataFolderFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("container-data-folder") + flag := upCmd.Flags().Lookup(names.ContainerDataFolder) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -21,14 +22,14 @@ func TestUpCmd_ContainerDataFolderFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--container-data-folder", "/tmp/data"}) require.NoError(t, err) - val, err := upCmd.Flags().GetString("container-data-folder") + val, err := upCmd.Flags().GetString(names.ContainerDataFolder) require.NoError(t, err) assert.Equal(t, "/tmp/data", val) } func TestUpCmd_MountWorkspaceGitRootFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("mount-workspace-git-root") + flag := upCmd.Flags().Lookup(names.MountWorkspaceGitRoot) require.NotNil(t, flag) assert.Equal(t, "true", flag.DefValue) } @@ -37,14 +38,14 @@ func TestUpCmd_MountWorkspaceGitRootFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--mount-workspace-git-root=false"}) require.NoError(t, err) - val, err := upCmd.Flags().GetBool("mount-workspace-git-root") + val, err := upCmd.Flags().GetBool(names.MountWorkspaceGitRoot) require.NoError(t, err) assert.False(t, val) } func TestUpCmd_TerminalColumnsFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("terminal-columns") + flag := upCmd.Flags().Lookup(names.TerminalColumns) require.NotNil(t, flag) assert.Equal(t, "0", flag.DefValue) } @@ -53,14 +54,14 @@ func TestUpCmd_TerminalColumnsFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--terminal-columns", "120"}) require.NoError(t, err) - val, err := upCmd.Flags().GetInt("terminal-columns") + val, err := upCmd.Flags().GetInt(names.TerminalColumns) require.NoError(t, err) assert.Equal(t, 120, val) } func TestUpCmd_TerminalRowsFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("terminal-rows") + flag := upCmd.Flags().Lookup(names.TerminalRows) require.NotNil(t, flag) assert.Equal(t, "0", flag.DefValue) } @@ -69,7 +70,7 @@ func TestUpCmd_TerminalRowsFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--terminal-rows", "40"}) require.NoError(t, err) - val, err := upCmd.Flags().GetInt("terminal-rows") + val, err := upCmd.Flags().GetInt(names.TerminalRows) require.NoError(t, err) assert.Equal(t, 40, val) } @@ -78,14 +79,14 @@ func TestUpCmd_SkipPostCreateFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--skip-post-create"}) require.NoError(t, err) - val, err := upCmd.Flags().GetBool("skip-post-create") + val, err := upCmd.Flags().GetBool(names.SkipPostCreate) require.NoError(t, err) assert.True(t, val) } func TestUpCmd_SkipNonBlockingCommandsFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("skip-non-blocking-commands") + flag := upCmd.Flags().Lookup(names.SkipNonBlockingCommands) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } @@ -94,14 +95,14 @@ func TestUpCmd_SkipNonBlockingCommandsFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--skip-non-blocking-commands"}) require.NoError(t, err) - val, err := upCmd.Flags().GetBool("skip-non-blocking-commands") + val, err := upCmd.Flags().GetBool(names.SkipNonBlockingCommands) require.NoError(t, err) assert.True(t, val) } func TestUpCmd_DotfilesTargetPathFlag(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("dotfiles-target-path") + flag := upCmd.Flags().Lookup(names.DotfilesTargetPath) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -110,14 +111,14 @@ func TestUpCmd_DotfilesTargetPathFlagParsesValue(t *testing.T) { upCmd := up.NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{"--dotfiles-target-path", "~/dotfiles"}) require.NoError(t, err) - val, err := upCmd.Flags().GetString("dotfiles-target-path") + val, err := upCmd.Flags().GetString(names.DotfilesTargetPath) require.NoError(t, err) assert.Equal(t, "~/dotfiles", val) } func TestBuildCmd_LabelFlag(t *testing.T) { buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("label") + flag := buildCmd.Flags().Lookup(names.Label) require.NotNil(t, flag) } @@ -126,14 +127,14 @@ func TestBuildCmd_LabelFlagParsesValue(t *testing.T) { labelVal := "org.opencontainers.image.source=https://github.com/example" err := buildCmd.ParseFlags([]string{"--label", labelVal}) require.NoError(t, err) - val, err := buildCmd.Flags().GetStringArray("label") + val, err := buildCmd.Flags().GetStringArray(names.Label) require.NoError(t, err) assert.Equal(t, []string{labelVal}, val) } func TestBuildCmd_OutputFlag(t *testing.T) { buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("output") + flag := buildCmd.Flags().Lookup(names.Output) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -142,14 +143,14 @@ func TestBuildCmd_OutputFlagParsesValue(t *testing.T) { buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) err := buildCmd.ParseFlags([]string{"--output", "oci"}) require.NoError(t, err) - val, err := buildCmd.Flags().GetString("output") + val, err := buildCmd.Flags().GetString(names.Output) require.NoError(t, err) assert.Equal(t, "oci", val) } func TestBuildCmd_ExperimentalLockfileFlag(t *testing.T) { buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("experimental-lockfile") + flag := buildCmd.Flags().Lookup(names.ExperimentalLockfile) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -158,14 +159,14 @@ func TestBuildCmd_ExperimentalLockfileFlagParsesValue(t *testing.T) { buildCmd := workspace.NewBuildCmd(&flags.GlobalFlags{}) err := buildCmd.ParseFlags([]string{"--experimental-lockfile", "/path/to/lockfile"}) require.NoError(t, err) - val, err := buildCmd.Flags().GetString("experimental-lockfile") + val, err := buildCmd.Flags().GetString(names.ExperimentalLockfile) require.NoError(t, err) assert.Equal(t, "/path/to/lockfile", val) } func TestExecCmd_ContainerIDFlag(t *testing.T) { execCmd := workspace.NewExecCmd(&flags.GlobalFlags{}) - flag := execCmd.Flags().Lookup("container-id") + flag := execCmd.Flags().Lookup(names.ContainerID) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -174,7 +175,7 @@ func TestExecCmd_ContainerIDFlagParsesValue(t *testing.T) { execCmd := workspace.NewExecCmd(&flags.GlobalFlags{}) err := execCmd.ParseFlags([]string{"--container-id", "abc123"}) require.NoError(t, err) - val, err := execCmd.Flags().GetString("container-id") + val, err := execCmd.Flags().GetString(names.ContainerID) require.NoError(t, err) assert.Equal(t, "abc123", val) } diff --git a/cmd/pro/check_update.go b/cmd/pro/check_update.go index 27953762e..b1c01e57f 100644 --- a/cmd/pro/check_update.go +++ b/cmd/pro/check_update.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/telemetry" @@ -57,9 +59,9 @@ func NewCheckUpdateCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/cluster/add.go b/cmd/pro/cluster/add.go index c4eb96b92..174bb12c2 100644 --- a/cmd/pro/cluster/add.go +++ b/cmd/pro/cluster/add.go @@ -13,6 +13,8 @@ import ( storagev1 "github.com/devsy-org/api/pkg/apis/storage/v1" proflags "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/client" @@ -57,28 +59,66 @@ func NewAddCmd(globalFlags *proflags.GlobalFlags) *cobra.Command { }, } - c.Flags(). - StringVar(&cmd.Namespace, "namespace", "loft", - "The namespace to generate the service account in. The namespace will be created if it does not exist") - c.Flags(). - StringVar(&cmd.ServiceAccount, "service-account", "loft-admin", "The service account name to create") - c.Flags(). - StringVar(&cmd.DisplayName, "display-name", "", "The display name to show in the UI for this cluster") - c.Flags(). - BoolVar(&cmd.Wait, "wait", false, "If true, will wait until the cluster is initialized") - c.Flags(). - BoolVar(&cmd.Insecure, "insecure", false, "If true, deploys the agent in insecure mode") - c.Flags(). - StringVar(&cmd.HelmChartVersion, "helm-chart-version", "", "The agent chart version to deploy") - c.Flags().StringVar(&cmd.HelmChartPath, "helm-chart-path", "", "The agent chart to deploy") - c.Flags(). - StringArrayVar(&cmd.HelmSet, "helm-set", []string{}, "Extra helm values for the agent chart") - c.Flags(). - StringArrayVar(&cmd.HelmValues, "helm-values", []string{}, "Extra helm values for the agent chart") - c.Flags(). - StringVar(&cmd.KubeContext, "kube-context", "", "The kube context to use for installation") - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - proflags.BindEnv(c.Flags(), "host") + cliflags.Add( + c, + cliflags.String( + &cmd.Namespace, + names.Namespace, + "loft", + "The namespace to generate the service account in. The namespace will be created if it does not exist", + ), + cliflags.String( + &cmd.ServiceAccount, + names.ServiceAccount, + "loft-admin", + "The service account name to create", + ), + cliflags.String( + &cmd.DisplayName, + names.DisplayName, + "", + "The display name to show in the UI for this cluster", + ), + cliflags.Bool( + &cmd.Wait, + names.Wait, + false, + "If true, will wait until the cluster is initialized", + ), + cliflags.Bool( + &cmd.Insecure, + names.Insecure, + false, + "If true, deploys the agent in insecure mode", + ), + cliflags.String( + &cmd.HelmChartVersion, + names.HelmChartVersion, + "", + "The agent chart version to deploy", + ), + cliflags.String(&cmd.HelmChartPath, names.HelmChartPath, "", "The agent chart to deploy"), + cliflags.StringArray( + &cmd.HelmSet, + names.HelmSet, + []string{}, + "Extra helm values for the agent chart", + ), + cliflags.StringArray( + &cmd.HelmValues, + names.HelmValues, + []string{}, + "Extra helm values for the agent chart", + ), + cliflags.String( + &cmd.KubeContext, + names.KubeContext, + "", + "The kube context to use for installation", + ), + cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use"), + ) + proflags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/cluster/list.go b/cmd/pro/cluster/list.go index 9e84fbbf6..c6561e74a 100644 --- a/cmd/pro/cluster/list.go +++ b/cmd/pro/cluster/list.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" @@ -49,12 +51,12 @@ func NewListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) return c } diff --git a/cmd/pro/daemon/netcheck.go b/cmd/pro/daemon/netcheck.go index 7b874eee8..1f791de9a 100644 --- a/cmd/pro/daemon/netcheck.go +++ b/cmd/pro/daemon/netcheck.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/config" daemon "github.com/devsy-org/devsy/pkg/daemon/platform" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" providerpkg "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -56,11 +58,11 @@ func NewNetcheckCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - proflags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + proflags.BindEnv(c.Flags(), names.Host) _ = c.RegisterFlagCompletionFunc( - "host", + names.Host, func(rootCmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.GetPlatformHostSuggestions( rootCmd, diff --git a/cmd/pro/daemon/start.go b/cmd/pro/daemon/start.go index 87c925d00..136bb4f56 100644 --- a/cmd/pro/daemon/start.go +++ b/cmd/pro/daemon/start.go @@ -15,6 +15,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/config" daemon "github.com/devsy-org/devsy/pkg/daemon/platform" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform/client" providerpkg "github.com/devsy-org/devsy/pkg/provider" @@ -51,11 +53,11 @@ func NewStartCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - proflags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + proflags.BindEnv(c.Flags(), names.Host) _ = c.RegisterFlagCompletionFunc( - "host", + names.Host, func(rootCmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.GetPlatformHostSuggestions( rootCmd, diff --git a/cmd/pro/daemon/status.go b/cmd/pro/daemon/status.go index 62dd5d4b2..84be6a2f5 100644 --- a/cmd/pro/daemon/status.go +++ b/cmd/pro/daemon/status.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/config" platformdaemon "github.com/devsy-org/devsy/pkg/daemon/platform" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" providerpkg "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" ) @@ -54,11 +56,11 @@ func NewStatusCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - proflags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + proflags.BindEnv(c.Flags(), names.Host) _ = c.RegisterFlagCompletionFunc( - "host", + names.Host, func(rootCmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return completion.GetPlatformHostSuggestions( rootCmd, diff --git a/cmd/pro/flags/flags.go b/cmd/pro/flags/flags.go index 3993bac8b..d56d084c0 100644 --- a/cmd/pro/flags/flags.go +++ b/cmd/pro/flags/flags.go @@ -2,6 +2,7 @@ package flags import ( rootflags "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform/client" flag "github.com/spf13/pflag" ) @@ -25,7 +26,7 @@ func SetGlobalFlags(flags *flag.FlagSet) *GlobalFlags { flags.StringVar( &globalFlags.Config, - "config", + names.Config, client.DefaultCacheConfig, "The config to use (will be created if it does not exist)", ) diff --git a/cmd/pro/health.go b/cmd/pro/health.go index c7527960a..7632596c9 100644 --- a/cmd/pro/health.go +++ b/cmd/pro/health.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/telemetry" @@ -59,9 +61,9 @@ func NewHealthCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/list.go b/cmd/pro/list.go index 9c83719d1..ad3bad0a5 100644 --- a/cmd/pro/list.go +++ b/cmd/pro/list.go @@ -9,6 +9,8 @@ import ( proflags "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" @@ -40,8 +42,15 @@ func NewListCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - BoolVar(&cmd.Login, "login", false, "Check if the user is logged into the pro instance") + cliflags.Add( + listCmd, + cliflags.Bool( + &cmd.Login, + names.Login, + false, + "Check if the user is logged into the pro instance", + ), + ) return listCmd } diff --git a/cmd/pro/login.go b/cmd/pro/login.go index 313eef832..ff316ea96 100644 --- a/cmd/pro/login.go +++ b/cmd/pro/login.go @@ -10,6 +10,8 @@ import ( proflags "github.com/devsy-org/devsy/cmd/pro/flags" providercmd "github.com/devsy-org/devsy/cmd/provider" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/client" @@ -54,24 +56,49 @@ func NewLoginCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - loginCmd.Flags(). - StringVar(&cmd.AccessKey, "access-key", "", "If defined will use the given access key to login") - loginCmd.Flags(). - BoolVar(&cmd.Login, "login", true, "If enabled will automatically try to log into Devsy Pro") - loginCmd.Flags(). - BoolVar(&cmd.Use, "use", true, "If enabled will automatically activate the provider") - loginCmd.Flags(). - StringVar(&cmd.Provider, "provider", "", "Optional name how the Devsy Pro provider will be named") - loginCmd.Flags(). - StringVar(&cmd.Version, "version", "", "The version to use for the Devsy provider") - loginCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE") - loginCmd.Flags(). - BoolVar(&cmd.ForceBrowser, "force-browser", false, "Force login through browser") - - loginCmd.Flags(). - StringVar(&cmd.ProviderSource, "provider-source", "", "The source of the provider") - _ = loginCmd.Flags().MarkHidden("provider-source") + cliflags.Add( + loginCmd, + cliflags.String( + &cmd.AccessKey, + names.AccessKey, + "", + "If defined will use the given access key to login", + ), + cliflags.Bool( + &cmd.Login, + names.Login, + true, + "If enabled will automatically try to log into Devsy Pro", + ), + cliflags.Bool( + &cmd.Use, + names.Use, + true, + "If enabled will automatically activate the provider", + ), + cliflags.String( + &cmd.Provider, + names.Provider, + "", + "Optional name how the Devsy Pro provider will be named", + ), + cliflags.String( + &cmd.Version, + names.Version, + "", + "The version to use for the Devsy provider", + ), + cliflags.StringArray(&cmd.Options, names.Option, []string{}, "Provider option in the form KEY=VALUE"). + Shorthand("o"), + cliflags.Bool(&cmd.ForceBrowser, names.ForceBrowser, false, "Force login through browser"), + cliflags.String( + &cmd.ProviderSource, + names.ProviderSource, + "", + "The source of the provider", + ), + ) + _ = loginCmd.Flags().MarkHidden(names.ProviderSource) return loginCmd } diff --git a/cmd/pro/logout.go b/cmd/pro/logout.go index 74dad6e1c..9e79f17e0 100644 --- a/cmd/pro/logout.go +++ b/cmd/pro/logout.go @@ -12,6 +12,8 @@ import ( "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" daemon "github.com/devsy-org/devsy/pkg/daemon/platform" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" @@ -40,8 +42,15 @@ func NewLogoutCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - logoutCmd.Flags(). - BoolVar(&cmd.IgnoreNotFound, "ignore-not-found", false, "Treat \"pro instance not found\" as a successful logout") + cliflags.Add( + logoutCmd, + cliflags.Bool( + &cmd.IgnoreNotFound, + names.IgnoreNotFound, + false, + "Treat \"pro instance not found\" as a successful logout", + ), + ) return logoutCmd } diff --git a/cmd/pro/project/list.go b/cmd/pro/project/list.go index 610b3b7b3..4bb79642a 100644 --- a/cmd/pro/project/list.go +++ b/cmd/pro/project/list.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -47,9 +49,9 @@ func NewListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/provider/rebuild.go b/cmd/pro/provider/rebuild.go index d25ec6702..fc3e8a9b1 100644 --- a/cmd/pro/provider/rebuild.go +++ b/cmd/pro/provider/rebuild.go @@ -8,6 +8,8 @@ import ( "os" "github.com/devsy-org/devsy/cmd/pro/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/client" "github.com/devsy-org/devsy/pkg/platform/remotecommand" @@ -36,9 +38,9 @@ func NewRebuildCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) return c } diff --git a/cmd/pro/self.go b/cmd/pro/self.go index 126c33959..1dc7e098d 100644 --- a/cmd/pro/self.go +++ b/cmd/pro/self.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" ) @@ -44,9 +46,9 @@ func NewSelfCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/start.go b/cmd/pro/start.go index 32a90b84f..d302c4091 100644 --- a/cmd/pro/start.go +++ b/cmd/pro/start.go @@ -24,6 +24,8 @@ import ( loftclientset "github.com/devsy-org/api/pkg/clientset/versioned" proflags "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/hash" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/machineid" @@ -102,41 +104,99 @@ func NewStartCmd(flags *proflags.GlobalFlags) *cobra.Command { }, } - startCmd.Flags(). - BoolVar(&cmd.Docker, "docker", false, "If enabled will try to deploy Devsy Pro to the local docker installation.") - startCmd.Flags().StringVar(&cmd.DockerImage, "docker-image", "", "The docker image to install.") - startCmd.Flags().StringArrayVar(&cmd.DockerArgs, "docker-arg", []string{}, "Extra docker args") - startCmd.Flags(). - StringVar(&cmd.Context, "context", "", "The kube context to use for installation") - startCmd.Flags(). - StringVar(&cmd.Namespace, "namespace", config.ProReleaseName, "The namespace to install into") - startCmd.Flags(). - StringVar(&cmd.Host, "host", "", "Provide a hostname to enable ingress and configure its hostname") - startCmd.Flags(). - StringVar(&cmd.Password, "password", "", - "The password to use for the admin account. (If empty this will be the namespace UID)") - startCmd.Flags().StringVar(&cmd.Version, "version", "", "The version to install") - startCmd.Flags(). - StringVar(&cmd.Values, "values", "", "Path to a file for extra helm chart values") - startCmd.Flags(). - BoolVar(&cmd.ReuseValues, "reuse-values", true, "Reuse previous helm values on upgrade") - startCmd.Flags(). - BoolVar(&cmd.Upgrade, "upgrade", false, "If true, will try to upgrade the release") - startCmd.Flags().StringVar(&cmd.Email, "email", "", "The email to use for the installation") - startCmd.Flags(). - BoolVar(&cmd.Reset, "reset", false, "If true, an existing instance will be deleted before installing Devsy Pro") - startCmd.Flags(). - BoolVar(&cmd.NoWait, "no-wait", false, "If true, will not wait after installing it") - startCmd.Flags(). - BoolVar(&cmd.NoTunnel, "no-tunnel", false, "If true, will not create a loft.host tunnel for this installation") - startCmd.Flags(). - BoolVar(&cmd.NoLogin, "no-login", false, "If true, will not login to a Devsy Pro instance on start") - startCmd.Flags(). - StringVar(&cmd.ChartPath, "chart-path", "", "The local chart path to deploy Devsy Pro") - startCmd.Flags(). - StringVar(&cmd.ChartRepo, "chart-repo", "https://charts.devsy.sh/", "The chart repo to deploy Devsy Pro") - - proflags.BindEnv(startCmd.Flags(), "host") + cliflags.Add( + startCmd, + cliflags.Bool( + &cmd.Docker, + names.Docker, + false, + "If enabled will try to deploy Devsy Pro to the local docker installation.", + ), + cliflags.String(&cmd.DockerImage, names.DockerImage, "", "The docker image to install."), + cliflags.StringArray(&cmd.DockerArgs, names.DockerArg, []string{}, "Extra docker args"), + cliflags.String( + &cmd.Context, + names.Context, + "", + "The kube context to use for installation", + ), + cliflags.String( + &cmd.Namespace, + names.Namespace, + config.ProReleaseName, + "The namespace to install into", + ), + cliflags.String( + &cmd.Host, + names.Host, + "", + "Provide a hostname to enable ingress and configure its hostname", + ), + cliflags.String( + &cmd.Password, + names.Password, + "", + "The password to use for the admin account. (If empty this will be the namespace UID)", + ), + cliflags.String(&cmd.Version, names.Version, "", "The version to install"), + cliflags.String( + &cmd.Values, + names.Values, + "", + "Path to a file for extra helm chart values", + ), + cliflags.Bool( + &cmd.ReuseValues, + names.ReuseValues, + true, + "Reuse previous helm values on upgrade", + ), + cliflags.Bool( + &cmd.Upgrade, + names.Upgrade, + false, + "If true, will try to upgrade the release", + ), + cliflags.String(&cmd.Email, names.Email, "", "The email to use for the installation"), + cliflags.Bool( + &cmd.Reset, + names.Reset, + false, + "If true, an existing instance will be deleted before installing Devsy Pro", + ), + cliflags.Bool( + &cmd.NoWait, + names.NoWait, + false, + "If true, will not wait after installing it", + ), + cliflags.Bool( + &cmd.NoTunnel, + names.NoTunnel, + false, + "If true, will not create a loft.host tunnel for this installation", + ), + cliflags.Bool( + &cmd.NoLogin, + names.NoLogin, + false, + "If true, will not login to a Devsy Pro instance on start", + ), + cliflags.String( + &cmd.ChartPath, + names.ChartPath, + "", + "The local chart path to deploy Devsy Pro", + ), + cliflags.String( + &cmd.ChartRepo, + names.ChartRepo, + "https://charts.devsy.sh/", + "The chart repo to deploy Devsy Pro", + ), + ) + + proflags.BindEnv(startCmd.Flags(), names.Host) return startCmd } diff --git a/cmd/pro/template/list.go b/cmd/pro/template/list.go index 9cc80f457..252b24efc 100644 --- a/cmd/pro/template/list.go +++ b/cmd/pro/template/list.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" @@ -49,12 +51,12 @@ func NewListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) return c } diff --git a/cmd/pro/update_provider.go b/cmd/pro/update_provider.go index 3918f1199..c51f74974 100644 --- a/cmd/pro/update_provider.go +++ b/cmd/pro/update_provider.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/flags" providercmd "github.com/devsy-org/devsy/cmd/provider" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -34,9 +36,9 @@ func NewUpdateProviderCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/user/reset_password.go b/cmd/pro/user/reset_password.go index 39e723bdd..259601b8e 100644 --- a/cmd/pro/user/reset_password.go +++ b/cmd/pro/user/reset_password.go @@ -8,6 +8,8 @@ import ( storagev1 "github.com/devsy-org/api/pkg/apis/storage/v1" "github.com/devsy-org/devsy/cmd/pro/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform/kube" "github.com/devsy-org/devsy/pkg/random" @@ -52,10 +54,18 @@ devsy pro user reset-password --user admin }, } - c.Flags().StringVar(&cmd.User, "user", "admin", "The name of the user to reset the password") - c.Flags().StringVar(&cmd.Password, "password", "", "The new password to use") - c.Flags().BoolVar(&cmd.Create, "create", false, "Creates the user if it does not exist") - c.Flags().BoolVar(&cmd.Force, "force", false, "If user had no password will create one") + cliflags.Add( + c, + cliflags.String( + &cmd.User, + names.User, + "admin", + "The name of the user to reset the password", + ), + cliflags.String(&cmd.Password, names.Password, "", "The new password to use"), + cliflags.Bool(&cmd.Create, names.Create, false, "Creates the user if it does not exist"), + cliflags.Bool(&cmd.Force, names.Force, false, "If user had no password will create one"), + ) return c } diff --git a/cmd/pro/version.go b/cmd/pro/version.go index b4438ca54..04adcd1e2 100644 --- a/cmd/pro/version.go +++ b/cmd/pro/version.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" ) @@ -44,9 +46,9 @@ func NewVersionCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/workspace/create.go b/cmd/pro/workspace/create.go index 30bb6270e..570b5179b 100644 --- a/cmd/pro/workspace/create.go +++ b/cmd/pro/workspace/create.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" @@ -49,11 +51,14 @@ func NewCreateCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") - c.Flags().StringVar(&cmd.Instance, "instance", "", "The workspace instance to create") - _ = c.MarkFlagRequired("instance") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) + cliflags.Add( + c, + cliflags.String(&cmd.Instance, names.Instance, "", "The workspace instance to create"), + ) + _ = c.MarkFlagRequired(names.Instance) return c } diff --git a/cmd/pro/workspace/import.go b/cmd/pro/workspace/import.go index 83703e057..f72354f14 100644 --- a/cmd/pro/workspace/import.go +++ b/cmd/pro/workspace/import.go @@ -10,6 +10,8 @@ import ( proflags "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/cmd/pro/provider/list" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/options" "github.com/devsy-org/devsy/pkg/platform" @@ -47,14 +49,24 @@ func NewImportCmd(globalFlags *proflags.GlobalFlags) *cobra.Command { }, } - importCmd.Flags().StringVar(&cmd.WorkspaceId, "workspace-id", "", "ID of a workspace to import") - importCmd.Flags(). - StringVar(&cmd.WorkspaceUid, "workspace-uid", "", "UID of a workspace to import") - importCmd.Flags(). - StringVar(&cmd.WorkspaceProject, "workspace-project", "", "Project of the workspace to import") - importCmd.Flags(). - BoolVar(&cmd.Own, "own", false, "If true, will behave as if workspace was not imported") - _ = importCmd.MarkFlagRequired("workspace-uid") + cliflags.Add( + importCmd, + cliflags.String(&cmd.WorkspaceId, names.WorkspaceID, "", "ID of a workspace to import"), + cliflags.String(&cmd.WorkspaceUid, names.WorkspaceUID, "", "UID of a workspace to import"), + cliflags.String( + &cmd.WorkspaceProject, + names.WorkspaceProject, + "", + "Project of the workspace to import", + ), + cliflags.Bool( + &cmd.Own, + names.Own, + false, + "If true, will behave as if workspace was not imported", + ), + ) + _ = importCmd.MarkFlagRequired(names.WorkspaceUID) return importCmd } diff --git a/cmd/pro/workspace/list.go b/cmd/pro/workspace/list.go index c90a0d3a4..02639275b 100644 --- a/cmd/pro/workspace/list.go +++ b/cmd/pro/workspace/list.go @@ -12,6 +12,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -48,9 +50,9 @@ func NewListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/workspace/rebuild.go b/cmd/pro/workspace/rebuild.go index 928c9fbd6..9095b7e35 100644 --- a/cmd/pro/workspace/rebuild.go +++ b/cmd/pro/workspace/rebuild.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/remotecommand" "github.com/spf13/cobra" @@ -35,12 +37,12 @@ func NewRebuildCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/workspace/sleep.go b/cmd/pro/workspace/sleep.go index 6e22e21d7..e6caf15e0 100644 --- a/cmd/pro/workspace/sleep.go +++ b/cmd/pro/workspace/sleep.go @@ -10,6 +10,8 @@ import ( storagev1 "github.com/devsy-org/api/pkg/apis/storage/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/project" @@ -41,17 +43,22 @@ func NewSleepCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - c.Flags(). - Int64Var(&cmd.ForceDuration, "prevent-wakeup", -1, + cliflags.Add(c, + cliflags.String(&cmd.Project, names.Project, "", "The project to use"), + cliflags.Int64( + &cmd.ForceDuration, + names.PreventWakeup, + -1, "The amount of seconds this workspace should sleep until it can be woken up again (use 0 for infinite sleeping). "+ "During this time the space can only be woken up by `devsy pro workspace wakeup`, "+ - "manually deleting the annotation on the namespace or through the UI") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + "manually deleting the annotation on the namespace or through the UI", + ), + ) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/workspace/update.go b/cmd/pro/workspace/update.go index 6529df246..75e4017ae 100644 --- a/cmd/pro/workspace/update.go +++ b/cmd/pro/workspace/update.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/provider" @@ -49,11 +51,14 @@ func NewUpdateCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") - c.Flags().StringVar(&cmd.Instance, "instance", "", "The workspace instance to update") - _ = c.MarkFlagRequired("instance") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) + cliflags.Add( + c, + cliflags.String(&cmd.Instance, names.Instance, "", "The workspace instance to update"), + ) + _ = c.MarkFlagRequired(names.Instance) return c } diff --git a/cmd/pro/workspace/wakeup.go b/cmd/pro/workspace/wakeup.go index 72be8c5a8..29fa377b2 100644 --- a/cmd/pro/workspace/wakeup.go +++ b/cmd/pro/workspace/wakeup.go @@ -10,6 +10,8 @@ import ( storagev1 "github.com/devsy-org/api/pkg/apis/storage/v1" "github.com/devsy-org/devsy/cmd/pro/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" "github.com/devsy-org/devsy/pkg/platform/project" @@ -40,12 +42,12 @@ func NewWakeupCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) return c } diff --git a/cmd/pro/workspace/watch.go b/cmd/pro/workspace/watch.go index 1d7e64b7b..ac6c5219d 100644 --- a/cmd/pro/workspace/watch.go +++ b/cmd/pro/workspace/watch.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro/proutil" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" "github.com/spf13/cobra" @@ -49,14 +51,21 @@ func NewWatchCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - c.Flags().StringVar(&cmd.Host, "host", "", "The pro instance to use") - _ = c.MarkFlagRequired("host") - flags.BindEnv(c.Flags(), "host") - c.Flags().StringVar(&cmd.Project, "project", "", "The project to use") - _ = c.MarkFlagRequired("project") - flags.BindEnv(c.Flags(), "project") - c.Flags(). - BoolVar(&cmd.FilterByOwner, "filter-by-owner", true, "If true only shows workspaces of current owner") + cliflags.Add(c, cliflags.String(&cmd.Host, names.Host, "", "The pro instance to use")) + _ = c.MarkFlagRequired(names.Host) + flags.BindEnv(c.Flags(), names.Host) + cliflags.Add(c, cliflags.String(&cmd.Project, names.Project, "", "The project to use")) + _ = c.MarkFlagRequired(names.Project) + flags.BindEnv(c.Flags(), names.Project) + cliflags.Add( + c, + cliflags.Bool( + &cmd.FilterByOwner, + names.FilterByOwner, + true, + "If true only shows workspaces of current owner", + ), + ) return c } diff --git a/cmd/provider/add.go b/cmd/provider/add.go index abe24fec4..3ae20f716 100644 --- a/cmd/provider/add.go +++ b/cmd/provider/add.go @@ -7,6 +7,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/types" @@ -37,7 +39,7 @@ func NewAddCmd(f *flags.GlobalFlags) *cobra.Command { Args: cobra.MaximumNArgs(1), PreRunE: func(cobraCommand *cobra.Command, args []string) error { if cmd.FromExisting != "" { - return cobraCommand.MarkFlagRequired("name") + return cobraCommand.MarkFlagRequired(names.Name) } return nil @@ -52,18 +54,26 @@ func NewAddCmd(f *flags.GlobalFlags) *cobra.Command { }, } - addCmd.Flags(). - BoolVar(&cmd.SingleMachine, "single-machine", false, "If enabled will use a single machine for all workspaces") - addCmd.Flags(). - StringVar(&cmd.Name, "name", "", - "The name for the new provider. If not specified, the name from the provider's configuration file will be used") - addCmd.Flags(). - StringVar(&cmd.FromExisting, "from-existing", "", - "The name of an existing provider to use as a template. Needs to be used in conjunction with the --name flag") - addCmd.Flags(). - BoolVar(&cmd.Use, "use", true, "If enabled will automatically activate the provider") - addCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE") + cliflags.Add(addCmd, + cliflags.Bool(&cmd.SingleMachine, names.SingleMachine, false, + "If enabled will use a single machine for all workspaces"), + cliflags.String( + &cmd.Name, + names.Name, + "", + "The name for the new provider. If not specified, the name from the provider's configuration file will be used", + ), + cliflags.String( + &cmd.FromExisting, + names.FromExisting, + "", + "The name of an existing provider to use as a template. Needs to be used in conjunction with the --name flag", + ), + cliflags.Bool(&cmd.Use, names.Use, true, + "If enabled will automatically activate the provider"), + cliflags.StringArray(&cmd.Options, names.Option, nil, + "Provider option in the form KEY=VALUE").Shorthand("o"), + ) return addCmd } diff --git a/cmd/provider/delete.go b/cmd/provider/delete.go index f4f94f74e..8323c3af9 100644 --- a/cmd/provider/delete.go +++ b/cmd/provider/delete.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/platform" provider2 "github.com/devsy-org/devsy/pkg/provider" @@ -48,11 +50,13 @@ func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - deleteCmd.Flags(). - BoolVar(&cmd.IgnoreNotFound, "ignore-not-found", false, "Treat \"provider not found\" as a successful delete") - deleteCmd.Flags(). - BoolVar(&cmd.Force, "force", false, "Force delete the provider and ignore provider is already used") - _ = deleteCmd.Flags().MarkHidden("force") + cliflags.Add(deleteCmd, + cliflags.Bool(&cmd.IgnoreNotFound, names.IgnoreNotFound, false, + "Treat \"provider not found\" as a successful delete"), + cliflags.Bool(&cmd.Force, names.Force, false, + "Force delete the provider and ignore provider is already used"), + ) + _ = deleteCmd.Flags().MarkHidden(names.Force) return deleteCmd } diff --git a/cmd/provider/get.go b/cmd/provider/get.go index 61cb01f53..d6dc0a404 100644 --- a/cmd/provider/get.go +++ b/cmd/provider/get.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/types" @@ -49,8 +51,10 @@ func NewGetCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - getCmd.Flags(). - BoolVar(&cmd.Hidden, "hidden", false, "If true, will also show hidden options.") + cliflags.Add( + getCmd, + cliflags.Bool(&cmd.Hidden, names.Hidden, false, "If true, will also show hidden options."), + ) return getCmd } diff --git a/cmd/provider/init.go b/cmd/provider/init.go index aa9913816..857cf687e 100644 --- a/cmd/provider/init.go +++ b/cmd/provider/init.go @@ -4,6 +4,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" ) @@ -61,14 +63,15 @@ func NewInitCmd(f *flags.GlobalFlags) *cobra.Command { ) }, } - initCmd.Flags(). - BoolVar(&cmd.Reset, "reset", false, "Discard previously stored option answers and re-prompt from scratch") - initCmd.Flags(). - BoolVar(&cmd.SingleMachine, "single-machine", false, "Use a single machine for all workspaces") - initCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE") - initCmd.Flags(). - BoolVar(&cmd.SkipInit, "skip-init", false, "Skip provider init (testing only)") - _ = initCmd.Flags().MarkHidden("skip-init") + cliflags.Add(initCmd, + cliflags.Bool(&cmd.Reset, names.Reset, false, + "Discard previously stored option answers and re-prompt from scratch"), + cliflags.Bool(&cmd.SingleMachine, names.SingleMachine, false, + "Use a single machine for all workspaces"), + cliflags.StringArray(&cmd.Options, names.Option, nil, + "Provider option in the form KEY=VALUE").Shorthand("o"), + cliflags.Bool(&cmd.SkipInit, names.SkipInit, false, + "Skip provider init (testing only)").Hidden(), + ) return initCmd } diff --git a/cmd/provider/list.go b/cmd/provider/list.go index 65de3f82e..6e0e9f99e 100644 --- a/cmd/provider/list.go +++ b/cmd/provider/list.go @@ -13,6 +13,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" devsyhttp "github.com/devsy-org/devsy/pkg/http" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" @@ -44,8 +46,15 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags(). - BoolVar(&cmd.Available, "available", false, "List providers available for installation rather than installed ones") + cliflags.Add( + listCmd, + cliflags.Bool( + &cmd.Available, + names.Available, + false, + "List providers available for installation rather than installed ones", + ), + ) return listCmd } diff --git a/cmd/provider/set.go b/cmd/provider/set.go index 7ed0aac43..9469f1d05 100644 --- a/cmd/provider/set.go +++ b/cmd/provider/set.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -47,14 +49,16 @@ func NewSetCmd(f *flags.GlobalFlags) *cobra.Command { }, } - setCmd.Flags(). - BoolVar(&cmd.SingleMachine, "single-machine", false, "If enabled will use a single machine for all workspaces") - setCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE") - setCmd.Flags(). - BoolVar(&cmd.Dry, "dry", false, "Dry will not persist the options to file and instead return the new filled options") - setCmd.Flags(). - BoolVar(&cmd.SkipInit, "skip-init", false, "If true will skip running the provider init command") + cliflags.Add(setCmd, + cliflags.Bool(&cmd.SingleMachine, names.SingleMachine, false, + "If enabled will use a single machine for all workspaces"), + cliflags.StringArray(&cmd.Options, names.Option, nil, + "Provider option in the form KEY=VALUE").Shorthand("o"), + cliflags.Bool(&cmd.Dry, names.Dry, false, + "Dry will not persist the options to file and instead return the new filled options"), + cliflags.Bool(&cmd.SkipInit, names.SkipInit, false, + "If true will skip running the provider init command"), + ) return setCmd } diff --git a/cmd/provider/set_source.go b/cmd/provider/set_source.go index d06bef79a..149e4bd6b 100644 --- a/cmd/provider/set_source.go +++ b/cmd/provider/set_source.go @@ -6,6 +6,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -39,12 +41,14 @@ func NewSetSourceCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - setSourceCmd.Flags(). - BoolVar(&cmd.Use, "use", true, "If enabled will automatically activate the provider") - setSourceCmd.Flags(). - StringVar(&cmd.Version, "version", "", "Pin the provider to a specific version tag") - setSourceCmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "Provider option in the form KEY=VALUE") + cliflags.Add(setSourceCmd, + cliflags.Bool(&cmd.Use, names.Use, true, + "If enabled will automatically activate the provider"), + cliflags.String(&cmd.Version, names.Version, "", + "Pin the provider to a specific version tag"), + cliflags.StringArray(&cmd.Options, names.Option, nil, + "Provider option in the form KEY=VALUE").Shorthand("o"), + ) return setSourceCmd } diff --git a/cmd/provider/versions.go b/cmd/provider/versions.go index 7932f3ebc..aafae8b1a 100644 --- a/cmd/provider/versions.go +++ b/cmd/provider/versions.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -57,9 +59,11 @@ func NewVersionsCmd(f *flags.GlobalFlags) *cobra.Command { }, ValidArgsFunction: completeProviderName(cmd), } - versionsCmd.Flags().BoolVar(&cmd.JSON, "json", false, "Output JSON") - versionsCmd.Flags().BoolVar(&cmd.IncludePrerelease, "prerelease", false, "Include prereleases") - versionsCmd.Flags().BoolVar(&cmd.NoCache, "no-cache", false, "Bypass the version cache") + cliflags.Add(versionsCmd, + cliflags.Bool(&cmd.JSON, names.JSON, false, "Output JSON"), + cliflags.Bool(&cmd.IncludePrerelease, names.Prerelease, false, "Include prereleases"), + cliflags.Bool(&cmd.NoCache, names.NoCache, false, "Bypass the version cache"), + ) return versionsCmd } diff --git a/cmd/root.go b/cmd/root.go index 01bddb3be..29ac60e98 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -26,6 +26,7 @@ import ( "github.com/devsy-org/devsy/pkg/clierr" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/exitcode" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/flatpak" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/telemetry" @@ -43,8 +44,8 @@ const ( logOutputLogfmt = "logfmt" logOutputText = "text" - flagLogOutput = "--log-output" - flagLogFormat = "--log-format" + flagLogOutput = "--" + names.LogOutput + flagLogFormat = "--" + names.LogFormat groupCore = "core" groupConfig = "config" diff --git a/cmd/self/update.go b/cmd/self/update.go index 1c47fe52a..7356aba50 100644 --- a/cmd/self/update.go +++ b/cmd/self/update.go @@ -3,6 +3,8 @@ package self import ( "fmt" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/selfupdate" "github.com/spf13/cobra" ) @@ -53,13 +55,26 @@ func NewUpdateCmd() *cobra.Command { }, } - selfUpdateCmd.Flags(). - StringVar(&cmd.Version, "version", "", - "The version to update to. Defaults to the latest stable version available") - selfUpdateCmd.Flags(). - StringVar(&cmd.Channel, "channel", channelStable, - "Release channel: 'stable' for production releases, 'beta' for pre-release versions") - selfUpdateCmd.Flags(). - BoolVar(&cmd.DryRun, "dry-run", false, "Show which version would be downloaded without actually updating") + cliflags.Add( + selfUpdateCmd, + cliflags.String( + &cmd.Version, + names.Version, + "", + "The version to update to. Defaults to the latest stable version available", + ), + cliflags.String( + &cmd.Channel, + names.Channel, + channelStable, + "Release channel: 'stable' for production releases, 'beta' for pre-release versions", + ), + cliflags.Bool( + &cmd.DryRun, + names.DryRun, + false, + "Show which version would be downloaded without actually updating", + ), + ) return selfUpdateCmd } diff --git a/cmd/self/update_test.go b/cmd/self/update_test.go index decf77a21..dbeec68fb 100644 --- a/cmd/self/update_test.go +++ b/cmd/self/update_test.go @@ -3,6 +3,7 @@ package self import ( "testing" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,21 +15,21 @@ func TestNewUpdateCmd_CommandName(t *testing.T) { func TestNewUpdateCmd_HasVersionFlag(t *testing.T) { cmd := NewUpdateCmd() - f := cmd.Flags().Lookup("version") + f := cmd.Flags().Lookup(names.Version) require.NotNil(t, f, "--version flag must exist") assert.Equal(t, "", f.DefValue) } func TestNewUpdateCmd_HasDryRunFlag(t *testing.T) { cmd := NewUpdateCmd() - f := cmd.Flags().Lookup("dry-run") + f := cmd.Flags().Lookup(names.DryRun) require.NotNil(t, f, "--dry-run flag must exist") assert.Equal(t, "false", f.DefValue) } func TestNewUpdateCmd_HasChannelFlag(t *testing.T) { cmd := NewUpdateCmd() - f := cmd.Flags().Lookup("channel") + f := cmd.Flags().Lookup(names.Channel) require.NotNil(t, f, "--channel flag must exist") assert.Equal(t, channelStable, f.DefValue) } diff --git a/cmd/template/apply.go b/cmd/template/apply.go index 2a5f21f33..07e5fd960 100644 --- a/cmd/template/apply.go +++ b/cmd/template/apply.go @@ -10,6 +10,8 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/image" "github.com/devsy-org/devsy/pkg/log" "github.com/google/go-containerregistry/pkg/authn" @@ -40,27 +42,40 @@ func NewApplyCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - applyCmd.Flags().StringVar( - &applyFlags.WorkspaceFolder, "workspace-folder", ".", - "Target workspace folder to scaffold into", + cliflags.Add( + applyCmd, + cliflags.String( + &applyFlags.WorkspaceFolder, + names.WorkspaceFolder, + ".", + "Target workspace folder to scaffold into", + ), + cliflags.String( + &applyFlags.TemplateID, + names.TemplateID, + "", + "OCI reference of the template", + ), + cliflags.StringSlice( + &applyFlags.TemplateArgs, + names.TemplateArgs, + nil, + "Template variable arguments as key=value pairs", + ), + cliflags.StringSlice( + &applyFlags.Features, + names.Features, + nil, + "Feature IDs to add to the generated devcontainer.json", + ), + cliflags.StringSlice( + &applyFlags.OmitPaths, + names.OmitPaths, + nil, + "Glob patterns for paths to skip during scaffolding", + ), ) - applyCmd.Flags().StringVar( - &applyFlags.TemplateID, "template-id", "", - "OCI reference of the template", - ) - applyCmd.Flags().StringSliceVar( - &applyFlags.TemplateArgs, "template-args", nil, - "Template variable arguments as key=value pairs", - ) - applyCmd.Flags().StringSliceVar( - &applyFlags.Features, "features", nil, - "Feature IDs to add to the generated devcontainer.json", - ) - applyCmd.Flags().StringSliceVar( - &applyFlags.OmitPaths, "omit-paths", nil, - "Glob patterns for paths to skip during scaffolding", - ) - _ = applyCmd.MarkFlagRequired("template-id") + _ = applyCmd.MarkFlagRequired(names.TemplateID) return applyCmd } @@ -357,7 +372,10 @@ func shouldOmit(relPath string, omitPaths []string) bool { func addFeaturesToDevcontainer(workspaceFolder string, features []string) error { devcontainerPath := findDevcontainerJSON(workspaceFolder) if devcontainerPath == "" { - log.Warnf("no devcontainer.json found in workspace, --features not applied") + log.Warnf( + "no devcontainer.json found in workspace, %s not applied", + names.Flag(names.Features), + ) return nil } diff --git a/cmd/template/generate_docs.go b/cmd/template/generate_docs.go index bfcfe83d8..ca27081aa 100644 --- a/cmd/template/generate_docs.go +++ b/cmd/template/generate_docs.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/table" "github.com/spf13/cobra" @@ -30,19 +32,28 @@ func NewGenerateDocsCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - docsCmd.Flags().StringVar( - &docsFlags.ProjectFolder, "project-folder", "", - "Path to project folder containing template sources", + cliflags.Add( + docsCmd, + cliflags.String( + &docsFlags.ProjectFolder, + names.ProjectFolder, + "", + "Path to project folder containing template sources", + ), + cliflags.String( + &docsFlags.GithubOwner, + names.GitHubOwner, + "", + "GitHub owner for documentation links", + ), + cliflags.String( + &docsFlags.GithubRepo, + names.GitHubRepo, + "", + "GitHub repo for documentation links", + ), ) - docsCmd.Flags().StringVar( - &docsFlags.GithubOwner, "github-owner", "", - "GitHub owner for documentation links", - ) - docsCmd.Flags().StringVar( - &docsFlags.GithubRepo, "github-repo", "", - "GitHub repo for documentation links", - ) - _ = docsCmd.MarkFlagRequired("project-folder") + _ = docsCmd.MarkFlagRequired(names.ProjectFolder) return docsCmd } diff --git a/cmd/template/metadata.go b/cmd/template/metadata.go index 874554713..b432d4b3f 100644 --- a/cmd/template/metadata.go +++ b/cmd/template/metadata.go @@ -6,6 +6,8 @@ import ( "os" "github.com/devsy-org/devsy/cmd/flags" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" @@ -25,11 +27,16 @@ func NewMetadataCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - metadataCmd.Flags().StringVar( - &metadataFlags.TemplateID, "template-id", "", - "OCI reference of the template", + cliflags.Add( + metadataCmd, + cliflags.String( + &metadataFlags.TemplateID, + names.TemplateID, + "", + "OCI reference of the template", + ), ) - _ = metadataCmd.MarkFlagRequired("template-id") + _ = metadataCmd.MarkFlagRequired(names.TemplateID) return metadataCmd } diff --git a/cmd/template/publish.go b/cmd/template/publish.go index 34327e810..ba8c49b71 100644 --- a/cmd/template/publish.go +++ b/cmd/template/publish.go @@ -10,6 +10,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -38,19 +40,23 @@ func NewPublishCmd(globalFlags *flags.GlobalFlags) *cobra.Command { }, } - publishCmd.Flags().StringVar( - &publishFlags.Target, "target", "", - "Path to template source directory", + cliflags.Add( + publishCmd, + cliflags.String( + &publishFlags.Target, + names.Target, + "", + "Path to template source directory", + ), + cliflags.String(&publishFlags.Registry, names.Registry, "ghcr.io", "Target OCI registry"), + cliflags.String( + &publishFlags.Namespace, + names.Namespace, + "", + "Registry namespace (e.g., devcontainers/templates)", + ), ) - publishCmd.Flags().StringVar( - &publishFlags.Registry, "registry", "ghcr.io", - "Target OCI registry", - ) - publishCmd.Flags().StringVar( - &publishFlags.Namespace, "namespace", "", - "Registry namespace (e.g., devcontainers/templates)", - ) - _ = publishCmd.MarkFlagRequired("target") + _ = publishCmd.MarkFlagRequired(names.Target) return publishCmd } diff --git a/cmd/workspace/build.go b/cmd/workspace/build.go index 6e58252bb..b59802212 100644 --- a/cmd/workspace/build.go +++ b/cmd/workspace/build.go @@ -5,11 +5,13 @@ import ( "fmt" "os" - "github.com/devsy-org/devsy/cmd/flags" + cmdflags "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/image" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/output" @@ -20,7 +22,7 @@ import ( // BuildCmd holds the cmd flags. type BuildCmd struct { - *flags.GlobalFlags + *cmdflags.GlobalFlags provider.CLIOptions ProviderOptions []string @@ -30,89 +32,19 @@ type BuildCmd struct { } // NewBuildCmd creates a new command. -func NewBuildCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewBuildCmd(flags *cmdflags.GlobalFlags) *cobra.Command { cmd := &BuildCmd{ GlobalFlags: flags, } buildCmd := &cobra.Command{ Use: "build [flags] [workspace-path|workspace-name]", - Short: "Builds a workspace", + Short: "Build a workspace image", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.execute(cobraCmd.Context(), args) }, } - buildCmd.Flags(). - StringVar(&cmd.DevContainerImage, "devcontainer-image", "", - "The container image to use, this will override the devcontainer.json value in the project") - buildCmd.Flags(). - StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project") - buildCmd.Flags().StringVar(&cmd.DevContainerPath, "config", "", "Alias for --devcontainer-path") - _ = buildCmd.Flags().MarkHidden("config") - buildCmd.Flags(). - StringVar(&cmd.AdditionalFeatures, "additional-features", "", - `Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)`) - buildCmd.Flags(). - StringSliceVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE") - buildCmd.Flags(). - BoolVar(&cmd.SkipDelete, "skip-delete", false, "If true will not delete the workspace after building it") - buildCmd.Flags(). - StringVar(&cmd.Machine, "machine", "", - "The machine to use for this workspace. The machine needs to exist beforehand or the "+ - "command will fail. If the workspace already exists, this option has no effect") - buildCmd.Flags().StringVar(&cmd.Repository, "repository", "", "The repository to push to") - buildCmd.Flags(). - StringSliceVar(&cmd.Tag, "tag", []string{}, - "Image Tag(s) in the form of a comma separated list --tag latest,arm64 or "+ - "multiple flags --tag latest --tag arm64") - buildCmd.Flags(). - StringSliceVar(&cmd.Platforms, "platform", []string{}, "Set target platform for build") - buildCmd.Flags(). - BoolVar(&cmd.SkipPush, "skip-push", false, "If true will not push the image to the repository, useful for testing") - buildCmd.Flags(). - BoolVar(&cmd.PushDuringBuild, "push", false, - "Push image directly to registry during build, skipping load to local daemon") - buildCmd.Flags(). - StringArrayVar(&cmd.CacheFrom, "cache-from", []string{}, - "Cache sources for the build (e.g., myregistry.io/cache:latest or type=registry,ref=...). "+ - "Takes priority over devcontainer.json build.cacheFrom") - buildCmd.Flags(). - BoolVar(&cmd.NoCache, "no-cache", false, - "Disable Docker build cache") - buildCmd.Flags(). - StringArrayVar(&cmd.Labels, "label", []string{}, - "Add labels to the built image (format: key=value, can be specified multiple times)") - buildCmd.Flags(). - StringVar(&cmd.Output, "output", "", - "Build output type (docker or oci)") - buildCmd.Flags(). - StringVar(&cmd.ExperimentalLockfile, "experimental-lockfile", "", - "Lockfile path for reproducible builds") - buildCmd.Flags(). - Var(&cmd.GitCloneStrategy, "git-clone-strategy", - "The git clone strategy Devsy uses to checkout git based workspaces. "+ - "Can be full (default), blobless, treeless or shallow") - buildCmd.Flags(). - BoolVar(&cmd.GitCloneRecursiveSubmodules, "git-clone-recursive-submodules", false, - "If true will clone git submodule repositories recursively") - buildCmd.Flags(). - Var(&cmd.GitLFSMode, "git-lfs-mode", - "How Devsy handles Git LFS after cloning. Can be full (default, download LFS "+ - "content), setup-only (configure LFS but leave pointer files) or skip (ignore LFS)") - - buildCmd.Flags(). - StringVar(&cmd.ImageName, "image-name", "", "Alternative name for the built image") - buildCmd.Flags(). - BoolVar(&cmd.NoBuild, "no-build", false, "Fail if the image must be built (enforce pre-built images only)") - buildCmd.Flags(). - BoolVar(&cmd.Pull, "pull", false, "Always attempt to pull a newer version of the base image when building") - - // TESTING - buildCmd.Flags().BoolVar(&cmd.ForceBuild, "force-build", false, "TESTING ONLY") - buildCmd.Flags(). - BoolVar(&cmd.ForceInternalBuildKit, "force-internal-buildkit", false, "TESTING ONLY") - _ = buildCmd.Flags().MarkHidden("force-build") - _ = buildCmd.Flags().MarkHidden("force-internal-buildkit") + cmd.registerFlags(buildCmd) return buildCmd } @@ -120,6 +52,85 @@ func (cmd *BuildCmd) Run(ctx context.Context, client client.WorkspaceClient) err return cmd.build(ctx, client) } +func (cmd *BuildCmd) registerFlags(buildCmd *cobra.Command) { + cmd.registerDevContainerFlags(buildCmd) + cmd.registerImageFlags(buildCmd) + cmd.registerTestingFlags(buildCmd) +} + +func (cmd *BuildCmd) registerDevContainerFlags(buildCmd *cobra.Command) { + cliflags.RegisterDevContainerModifierFlags(buildCmd.Flags(), cliflags.DevContainerModifierFlags{ + Image: &cmd.DevContainerImage, + Features: &cmd.AdditionalFeatures, + }) + cliflags.Add(buildCmd, + cliflags.String(&cmd.DevContainerPath, names.DevContainerPath, "", + "The path to the devcontainer.json relative to the project"), + cliflags.StringSlice(&cmd.ProviderOptions, names.ProviderOption, nil, + "Provider option in the form KEY=VALUE"), + cliflags.Value(&cmd.GitCloneStrategy, names.GitCloneStrategy, + "The git clone strategy Devsy uses to checkout git based workspaces. "+ + "Can be full (default), blobless, treeless or shallow"), + cliflags.Bool(&cmd.GitCloneRecursiveSubmodules, names.GitRecurseSubmodules, false, + "Clone submodules recursively"), + cliflags.Value(&cmd.GitLFSMode, names.GitLFSMode, + "How Devsy handles Git LFS after cloning. Can be full (default, download LFS "+ + "content), setup-only (configure LFS but leave pointer files) or skip (ignore LFS)"), + ) +} + +func (cmd *BuildCmd) registerImageFlags(buildCmd *cobra.Command) { + cliflags.Add( + buildCmd, + cliflags.Bool(&cmd.SkipDelete, names.SkipDelete, false, + "If true will not delete the workspace after building it"), + cliflags.String(&cmd.Machine, names.Machine, "", + "The machine to use for this workspace. The machine needs to exist beforehand or the "+ + "command will fail. If the workspace already exists, this option has no effect"), + cliflags.String(&cmd.Repository, names.Repository, "", "The repository to push to"), + cliflags.StringSlice(&cmd.Tag, names.Tag, nil, + "Image Tag(s) in the form of a comma separated list --tag latest,arm64 or "+ + "multiple flags --tag latest --tag arm64"), + cliflags.StringSlice(&cmd.Platforms, names.Platform, nil, "Set target platform for build"), + cliflags.Bool(&cmd.SkipPush, names.SkipPush, false, + "If true will not push the image to the repository, useful for testing"), + cliflags.Bool(&cmd.PushDuringBuild, names.Push, false, + "Push image directly to registry during build, skipping load to local daemon"), + cliflags.StringArray( + &cmd.CacheFrom, + names.CacheFrom, + nil, + "Cache sources for the build (e.g., myregistry.io/cache:latest or type=registry,ref=...). "+ + "Takes priority over devcontainer.json build.cacheFrom", + ), + cliflags.Bool(&cmd.NoCache, names.NoCache, false, "Disable Docker build cache"), + cliflags.StringArray(&cmd.Labels, names.Label, nil, + "Add labels to the built image (format: key=value, can be specified multiple times)"), + cliflags.String(&cmd.Output, names.Output, "", "Build output type (docker or oci)"), + cliflags.String(&cmd.ExperimentalLockfile, names.ExperimentalLockfile, "", + "Lockfile path for reproducible builds"), + cliflags.String( + &cmd.ImageName, + names.ImageName, + "", + "Alternative name for the built image", + ), + cliflags.Bool(&cmd.NoBuild, names.NoBuild, false, + "Fail if the image must be built (enforce pre-built images only)"), + cliflags.Bool(&cmd.Pull, names.Pull, false, + "Always attempt to pull a newer version of the base image when building"), + ) +} + +func (cmd *BuildCmd) registerTestingFlags(buildCmd *cobra.Command) { + cliflags.Add( + buildCmd, + cliflags.Bool(&cmd.ForceBuild, names.ForceBuild, false, "TESTING ONLY").Hidden(), + cliflags.Bool(&cmd.ForceInternalBuildKit, names.ForceInternalBuildKit, false, "TESTING ONLY"). + Hidden(), + ) +} + func (cmd *BuildCmd) execute(ctx context.Context, args []string) error { devsyConfig, err := cmd.prepareBuild(ctx) if err != nil { @@ -176,9 +187,7 @@ func (cmd *BuildCmd) prepareBuild(ctx context.Context) (*config.Config, error) { return nil, fmt.Errorf("cannot build image: %w", err) } } - if devsyConfig.ContextOption( - config.ContextOptionSSHStrictHostKeyChecking, - ) == config.BoolTrue { + if devsyConfig.ContextOptionBool(config.ContextOptionSSHStrictHostKeyChecking) { cmd.StrictHostKeyChecking = true } return devsyConfig, nil @@ -186,10 +195,12 @@ func (cmd *BuildCmd) prepareBuild(ctx context.Context) (*config.Config, error) { func (cmd *BuildCmd) validateBuildFlags() error { if cmd.PushDuringBuild && cmd.SkipPush { - return fmt.Errorf("cannot use --push and --skip-push together") + return fmt.Errorf("cannot use %s and %s together", + names.Flag(names.Push), names.Flag(names.SkipPush)) } if cmd.PushDuringBuild && cmd.Repository == "" { - return fmt.Errorf("--push requires --repository to be specified") + return fmt.Errorf("%s requires %s to be specified", + names.Flag(names.Push), names.Flag(names.Repository)) } return nil } @@ -269,15 +280,10 @@ func (cmd *BuildCmd) build( return nil } - containerID := "" + containerID := devcconfig.GetContainerID(result) workdir := "" - if result != nil { - if result.ContainerDetails != nil { - containerID = result.ContainerDetails.ID - } - if result.SubstitutionContext != nil { - workdir = result.SubstitutionContext.ContainerWorkspaceFolder - } + if result != nil && result.SubstitutionContext != nil { + workdir = result.SubstitutionContext.ContainerWorkspaceFolder } user := devcconfig.GetRemoteUser(result) _ = devcconfig.WriteResultJSON(os.Stdout, devcconfig.ResultEnvelope{ diff --git a/cmd/workspace/build_test.go b/cmd/workspace/build_test.go index 6cd6c780e..a43a1f5f5 100644 --- a/cmd/workspace/build_test.go +++ b/cmd/workspace/build_test.go @@ -4,13 +4,14 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuildCmd_NoCacheFlag(t *testing.T) { buildCmd := NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("no-cache") + flag := buildCmd.Flags().Lookup(names.NoCache) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } @@ -20,14 +21,14 @@ func TestBuildCmd_NoCacheFlagParsesValue(t *testing.T) { err := buildCmd.ParseFlags([]string{"--no-cache"}) require.NoError(t, err) - val, err := buildCmd.Flags().GetBool("no-cache") + val, err := buildCmd.Flags().GetBool(names.NoCache) require.NoError(t, err) assert.True(t, val) } func TestBuildCmd_ImageNameFlag(t *testing.T) { buildCmd := NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("image-name") + flag := buildCmd.Flags().Lookup(names.ImageName) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -37,13 +38,13 @@ func TestBuildCmd_ImageNameFlagParsesValue(t *testing.T) { err := buildCmd.ParseFlags([]string{"--image-name", "my-custom-image:latest"}) require.NoError(t, err) - flag := buildCmd.Flags().Lookup("image-name") + flag := buildCmd.Flags().Lookup(names.ImageName) assert.Equal(t, "my-custom-image:latest", flag.Value.String()) } func TestBuildCmd_NoBuildFlag(t *testing.T) { buildCmd := NewBuildCmd(&flags.GlobalFlags{}) - flag := buildCmd.Flags().Lookup("no-build") + flag := buildCmd.Flags().Lookup(names.NoBuild) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } @@ -53,7 +54,7 @@ func TestBuildCmd_NoBuildFlagParsesValue(t *testing.T) { err := buildCmd.ParseFlags([]string{"--no-build"}) require.NoError(t, err) - val, err := buildCmd.Flags().GetBool("no-build") + val, err := buildCmd.Flags().GetBool(names.NoBuild) require.NoError(t, err) assert.True(t, val) } @@ -63,7 +64,7 @@ func TestBuildCmd_NoCacheDefaultFalse(t *testing.T) { err := buildCmd.ParseFlags([]string{}) require.NoError(t, err) - val, err := buildCmd.Flags().GetBool("no-cache") + val, err := buildCmd.Flags().GetBool(names.NoCache) require.NoError(t, err) assert.False(t, val) } diff --git a/cmd/workspace/delete.go b/cmd/workspace/delete.go index defa6b283..36ba94992 100644 --- a/cmd/workspace/delete.go +++ b/cmd/workspace/delete.go @@ -9,6 +9,8 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/telemetry" "github.com/devsy-org/devsy/pkg/workspace" @@ -22,16 +24,16 @@ type DeleteCmd struct { } // NewDeleteCmd creates a new command. -func NewDeleteCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewDeleteCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &DeleteCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } deleteCmd := &cobra.Command{ Use: "delete [flags] [workspace-path|workspace-name]", Aliases: []string{"rm"}, - Short: "Deletes an existing workspace", - Long: `Deletes an existing workspace. You can specify the workspace by its path or name. -If the workspace is not found, you can use the --ignore-not-found flag to treat it as a successful delete.`, + Short: "Delete a workspace", + Long: `Delete a workspace by path or name. +Use --ignore-not-found to treat a missing workspace as success.`, RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd, args) }, @@ -49,14 +51,16 @@ If the workspace is not found, you can use the --ignore-not-found flag to treat }, } - deleteCmd.Flags(). - BoolVar(&cmd.IgnoreNotFound, "ignore-not-found", false, "Treat \"workspace not found\" as a successful delete") - deleteCmd.Flags(). - StringVar(&cmd.GracePeriod, "grace-period", "", "The amount of time to give the command to delete the workspace") - deleteCmd.Flags(). - BoolVar(&cmd.Force, "force", false, "Delete workspace even if it is not found remotely anymore") - deleteCmd.Flags(). - BoolVar(&cmd.RemoveVolumes, "remove-volumes", false, "Remove named volumes associated with the workspace") + cliflags.Add(deleteCmd, + cliflags.Bool(&cmd.IgnoreNotFound, names.IgnoreNotFound, false, + "Treat \"workspace not found\" as a successful delete"), + cliflags.String(&cmd.GracePeriod, names.GracePeriod, "", + "The amount of time to give the command to delete the workspace"), + cliflags.Bool(&cmd.Force, names.Force, false, + "Delete workspace even if it is not found remotely anymore"), + cliflags.Bool(&cmd.RemoveVolumes, names.RemoveVolumes, false, + "Remove named volumes associated with the workspace"), + ) return deleteCmd } diff --git a/cmd/workspace/describe.go b/cmd/workspace/describe.go index 8f3f2ea2f..f4ac6910d 100644 --- a/cmd/workspace/describe.go +++ b/cmd/workspace/describe.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/table" @@ -38,15 +40,17 @@ func NewDescribeCmd(globalFlags *flags.GlobalFlags) *cobra.Command { } describeCmd := &cobra.Command{ Use: "describe [flags] [workspace-path|workspace-name]", - Short: "Shows the full details of a workspace", + Short: "Show workspace details", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.execute(cobraCmd.Context(), args) }, ValidArgsFunction: cmd.validArgs, } - describeCmd.Flags(). - StringVar(&cmd.Timeout, "timeout", "30s", "The timeout to wait until the status can be retrieved") + cliflags.Add(describeCmd, + cliflags.String(&cmd.Timeout, names.Timeout, "30s", + "The timeout to wait until the status can be retrieved"), + ) return describeCmd } diff --git a/cmd/workspace/exec.go b/cmd/workspace/exec.go index c48db4289..ec2944dbd 100644 --- a/cmd/workspace/exec.go +++ b/cmd/workspace/exec.go @@ -12,6 +12,8 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer" devcconfig "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/docker" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/output" workspace2 "github.com/devsy-org/devsy/pkg/workspace" @@ -37,7 +39,7 @@ func NewExecCmd(f *flags.GlobalFlags) *cobra.Command { cmd := &ExecCmd{GlobalFlags: f} execCmd := &cobra.Command{ Use: "exec [workspace-name] [flags] -- [args...]", - Short: "Executes a command in a running workspace container", + Short: "Run a command in a workspace", Args: cobra.MinimumNArgs(1), ValidArgsFunction: func( cobraCmd *cobra.Command, args []string, toComplete string, @@ -72,62 +74,23 @@ func NewExecCmd(f *flags.GlobalFlags) *cobra.Command { }, } - execCmd.Flags(). - StringVar( - &cmd.WorkspaceFolder, - "workspace-folder", - "", - "Path to the workspace folder", - ) - execCmd.Flags(). - StringVar( - &cmd.ContainerID, - "container-id", - "", - "Target a specific container by ID", - ) - execCmd.Flags(). - StringVar( - &cmd.DockerPath, - "docker-path", - "", - "Path to the docker/podman executable (defaults to 'docker')", - ) - execCmd.Flags(). - StringSliceVar( - &cmd.RemoteEnv, - "remote-env", - []string{}, - "Environment variables to set in the container (KEY=VALUE format)", - ) - execCmd.Flags(). - StringVar( - &cmd.DefaultUserEnvProbe, - "default-user-env-probe", - "", - "Override userEnvProbe from config (loginInteractiveShell, loginShell, interactiveShell, none)", - ) - execCmd.Flags(). - StringArrayVar( - &cmd.IDLabels, - "id-label", - []string{}, - "Override the default container identification labels (format: key=value, can be specified multiple times)", - ) - execCmd.Flags(). - StringVar( - &cmd.ContainerDataFolder, - "container-data-folder", - "", - "Override the default container data folder path", - ) - execCmd.Flags(). - BoolVar( - &cmd.SkipPostCreate, - "skip-post-create", - false, - "Skip running postCreateCommand", - ) + cliflags.Add(execCmd, + cliflags.String(&cmd.WorkspaceFolder, names.WorkspaceFolder, "", + "Path to the workspace folder"), + cliflags.String(&cmd.ContainerID, names.ContainerID, "", + "Target a specific container by ID"), + cliflags.String(&cmd.DockerPath, names.DockerPath, "", + "Path to the docker/podman executable (defaults to 'docker')"), + cliflags.StringSlice(&cmd.RemoteEnv, names.RemoteEnv, nil, + "Environment variables to set in the container (KEY=VALUE format)"), + cliflags.Bool(&cmd.SkipPostCreate, names.SkipPostCreate, false, + "Skip running postCreateCommand"), + ) + cliflags.RegisterDevContainerModifierFlags(execCmd.Flags(), cliflags.DevContainerModifierFlags{ + UserEnvProbe: &cmd.DefaultUserEnvProbe, + IDLabels: &cmd.IDLabels, + ContainerDataFolder: &cmd.ContainerDataFolder, + }) return execCmd } @@ -299,7 +262,8 @@ func (cmd *ExecCmd) runWithContainerID(ctx context.Context, args []string) error } var errFolderNameConflict = fmt.Errorf( - "specify either a workspace name or --workspace-folder/--container-id, not both", + "specify either a workspace name or %s/%s, not both", + names.Flag(names.WorkspaceFolder), names.Flag(names.ContainerID), ) // resolveExecTarget decides what string is handed to workspace.Get. diff --git a/cmd/workspace/exec_test.go b/cmd/workspace/exec_test.go index 29be0de92..349d684da 100644 --- a/cmd/workspace/exec_test.go +++ b/cmd/workspace/exec_test.go @@ -4,18 +4,22 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" workspace2 "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var ( + flagSkipPostCreate = names.Flag(names.SkipPostCreate) + flagWorkspaceFolder = names.Flag(names.WorkspaceFolder) +) + const ( - flagSkipPostCreate = "--skip-post-create" - flagWorkspaceFolder = "--workspace-folder" - testTmpDir = "/tmp" - testCmdEcho = "echo" - testCmdHello = "hello" + testTmpDir = "/tmp" + testCmdEcho = "echo" + testCmdHello = "hello" ) func TestValidateRemoteEnv_Valid(t *testing.T) { @@ -103,7 +107,7 @@ func TestResolveDockerCommand_OverrideBeatsDefault(t *testing.T) { func TestExecCmd_DockerPathFlag(t *testing.T) { execCmd := NewExecCmd(&flags.GlobalFlags{}) - flag := execCmd.Flags().Lookup("docker-path") + flag := execCmd.Flags().Lookup(names.DockerPath) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -133,7 +137,7 @@ func TestExecCmd_NonExistentContainerID(t *testing.T) { func TestExecCmd_ContainerDataFolderFlag(t *testing.T) { execCmd := NewExecCmd(&flags.GlobalFlags{}) - flag := execCmd.Flags().Lookup("container-data-folder") + flag := execCmd.Flags().Lookup(names.ContainerDataFolder) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -142,17 +146,17 @@ func TestExecCmd_ContainerDataFolderFlagParsesValue(t *testing.T) { execCmd := NewExecCmd(&flags.GlobalFlags{}) err := execCmd.ParseFlags([]string{ flagWorkspaceFolder, testTmpDir, - "--container-data-folder", "/custom/data", + names.Flag(names.ContainerDataFolder), "/custom/data", }) require.NoError(t, err) - flag := execCmd.Flags().Lookup("container-data-folder") + flag := execCmd.Flags().Lookup(names.ContainerDataFolder) assert.Equal(t, "/custom/data", flag.Value.String()) } func TestExecCmd_SkipPostCreateFlag(t *testing.T) { execCmd := NewExecCmd(&flags.GlobalFlags{}) - flag := execCmd.Flags().Lookup("skip-post-create") + flag := execCmd.Flags().Lookup(names.SkipPostCreate) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } @@ -165,7 +169,7 @@ func TestExecCmd_SkipPostCreateFlagParsesValue(t *testing.T) { }) require.NoError(t, err) - val, err := execCmd.Flags().GetBool("skip-post-create") + val, err := execCmd.Flags().GetBool(names.SkipPostCreate) require.NoError(t, err) assert.True(t, val) } diff --git a/cmd/workspace/export.go b/cmd/workspace/export.go index 44786001e..f64f3a136 100644 --- a/cmd/workspace/export.go +++ b/cmd/workspace/export.go @@ -19,13 +19,13 @@ type ExportCmd struct { } // NewExportCmd creates a new command. -func NewExportCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewExportCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &ExportCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } exportCmd := &cobra.Command{ Use: "export [flags] [workspace-path|workspace-name]", - Short: "Exports a workspace configuration", + Short: "Export workspace configuration", Hidden: true, RunE: func(cobraCmd *cobra.Command, args []string) error { ctx := cobraCmd.Context() diff --git a/cmd/workspace/import.go b/cmd/workspace/import.go index de58f3148..0c2ce37e7 100644 --- a/cmd/workspace/import.go +++ b/cmd/workspace/import.go @@ -11,6 +11,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/extract" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/workspace" @@ -33,13 +35,13 @@ type ImportCmd struct { } // NewImportCmd creates a new command. -func NewImportCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewImportCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &ImportCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } importCmd := &cobra.Command{ Use: "import", - Short: "Imports a workspace configuration", + Short: "Import workspace configuration", Args: cobra.NoArgs, Hidden: true, RunE: func(cobraCmd *cobra.Command, _ []string) error { @@ -47,15 +49,17 @@ func NewImportCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - importCmd.Flags().StringVar(&cmd.WorkspaceID, "workspace-id", "", "To workspace id to use") - importCmd.Flags().StringVar(&cmd.MachineID, "machine-id", "", "The machine id to use") - importCmd.Flags(). - BoolVar(&cmd.MachineReuse, "machine-reuse", false, "If machine already exists, reuse existing machine") - importCmd.Flags().StringVar(&cmd.ProviderID, "provider-id", "", "The provider id to use") - importCmd.Flags(). - BoolVar(&cmd.ProviderReuse, "provider-reuse", false, "If provider already exists, reuse existing provider") - importCmd.Flags().StringVar(&cmd.Data, "data", "", "The data to import as raw json") - _ = importCmd.MarkFlagRequired("data") + cliflags.Add(importCmd, + cliflags.String(&cmd.WorkspaceID, names.WorkspaceID, "", "To workspace id to use"), + cliflags.String(&cmd.MachineID, names.MachineID, "", "The machine id to use"), + cliflags.Bool(&cmd.MachineReuse, names.MachineReuse, false, + "If machine already exists, reuse existing machine"), + cliflags.String(&cmd.ProviderID, names.ProviderID, "", "The provider id to use"), + cliflags.Bool(&cmd.ProviderReuse, names.ProviderReuse, false, + "If provider already exists, reuse existing provider"), + cliflags.String(&cmd.Data, names.Data, "", "The data to import as raw json"), + ) + _ = importCmd.MarkFlagRequired(names.Data) return importCmd } diff --git a/cmd/workspace/list.go b/cmd/workspace/list.go index 97f76c9ac..dd04d13c9 100644 --- a/cmd/workspace/list.go +++ b/cmd/workspace/list.go @@ -9,6 +9,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" "github.com/devsy-org/devsy/pkg/table" "github.com/devsy-org/devsy/pkg/telemetry" @@ -24,14 +26,14 @@ type ListCmd struct { } // NewListCmd creates a new destroy command. -func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewListCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &ListCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } listCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, - Short: "Lists existing workspaces", + Short: "List workspaces", Args: cobra.NoArgs, Annotations: telemetry.SkipInUIAnnotation(), RunE: func(cobraCmd *cobra.Command, _ []string) error { @@ -39,7 +41,9 @@ func NewListCmd(flags *flags.GlobalFlags) *cobra.Command { }, } - listCmd.Flags().BoolVar(&cmd.SkipPro, "skip-pro", false, "Don't list pro workspaces") + cliflags.Add(listCmd, + cliflags.Bool(&cmd.SkipPro, names.SkipPro, false, "Don't list pro workspaces"), + ) return listCmd } diff --git a/cmd/workspace/logs.go b/cmd/workspace/logs.go index f871cfede..ccc9c4326 100644 --- a/cmd/workspace/logs.go +++ b/cmd/workspace/logs.go @@ -24,13 +24,13 @@ type LogsCmd struct { } // NewLogsCmd creates a new destroy command. -func NewLogsCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewLogsCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &LogsCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } startCmd := &cobra.Command{ Use: "logs [flags] [workspace-path|workspace-name]", - Short: "Prints the workspace logs on the machine", + Short: "Print workspace logs", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd.Context(), args) }, diff --git a/cmd/workspace/ping.go b/cmd/workspace/ping.go index 97f7bbd71..4703be8e9 100644 --- a/cmd/workspace/ping.go +++ b/cmd/workspace/ping.go @@ -17,13 +17,13 @@ type PingCmd struct { *flags.GlobalFlags } -func NewPingCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewPingCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &PingCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } troubleshootCmd := &cobra.Command{ Use: "ping [workspace-path|workspace-name]", - Short: "Pings the Devsy Pro workspace", + Short: "Ping a Devsy Pro workspace", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd.Context(), args) }, diff --git a/cmd/workspace/set_ide.go b/cmd/workspace/set_ide.go index ff5f4fa88..94d01ad48 100644 --- a/cmd/workspace/set_ide.go +++ b/cmd/workspace/set_ide.go @@ -8,6 +8,8 @@ import ( "github.com/devsy-org/devsy/cmd/completion" "github.com/devsy-org/devsy/cmd/flags" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/ideparse" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" @@ -31,11 +33,9 @@ func NewSetIDECmd(globalFlags *flags.GlobalFlags) *cobra.Command { setIDECmd := &cobra.Command{ Use: "set-ide ", - Short: "Assign an IDE to an existing workspace", - Long: `Assign an IDE to an existing workspace without starting it. - -The change is persisted to the workspace config and used on the next -'devsy workspace up'. Available IDEs can be listed with 'devsy ide list'.`, + Short: "Set a workspace's IDE", + Long: `Set a workspace's IDE, persisted to its config and applied on the next +'devsy workspace up'. List available IDEs with 'devsy ide list'.`, Args: cobra.ExactArgs(2), RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.Run(cobraCmd.Context(), args[0], args[1]) @@ -57,8 +57,11 @@ The change is persisted to the workspace config and used on the next }, } - setIDECmd.Flags(). - StringArrayVarP(&cmd.Options, "option", "o", []string{}, "IDE option in the form KEY=VALUE") + cliflags.Add( + setIDECmd, + cliflags.StringArray(&cmd.Options, names.Option, nil, "IDE option in the form KEY=VALUE"). + Shorthand("o"), + ) return setIDECmd } diff --git a/cmd/workspace/ssh.go b/cmd/workspace/ssh.go index 052b93f9d..c7f6d08d1 100644 --- a/cmd/workspace/ssh.go +++ b/cmd/workspace/ssh.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "os" - "os/exec" "path" "strings" "time" @@ -19,7 +18,8 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" - "github.com/devsy-org/devsy/pkg/git" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/gpg" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/port" @@ -72,7 +72,7 @@ func NewSSHCmd(f *flags.GlobalFlags) *cobra.Command { } sshCmd := &cobra.Command{ Use: "ssh [flags] [workspace-folder|workspace-name]", - Short: "Starts a new ssh session to a workspace", + Short: "Open an SSH session to a workspace", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.execute(cobraCmd.Context(), args) }, @@ -88,58 +88,66 @@ func NewSSHCmd(f *flags.GlobalFlags) *cobra.Command { }, } - sshCmd.Flags(). - StringArrayVarP(&cmd.ForwardPorts, "forward-ports", "L", []string{}, + cliflags.Add( + sshCmd, + cliflags.StringArray( + &cmd.ForwardPorts, + names.ForwardPorts, + nil, "Specifies that connections to the given TCP port or Unix socket on the local (client) "+ - "host are to be forwarded to the given host and port, or Unix socket, on the remote side.") - sshCmd.Flags(). - StringArrayVarP(&cmd.ReverseForwardPorts, "reverse-forward-ports", "R", []string{}, + "host are to be forwarded to the given host and port, or Unix socket, on the remote side.", + ). + Shorthand("L"), + cliflags.StringArray( + &cmd.ReverseForwardPorts, + names.ReverseForwardPorts, + nil, "Specifies that connections to the given TCP port or Unix socket on the local (client) "+ - "host are to be reverse forwarded to the given host and port, or Unix socket, on the remote side.") - sshCmd.Flags(). - StringArrayVarP(&cmd.SendEnvVars, "send-env", "", []string{}, - "Specifies which local env variables shall be sent to the container.") - sshCmd.Flags(). - StringArrayVarP(&cmd.SetEnvVars, "set-env", "", []string{}, "Specifies env variables to be set in the container.") - sshCmd.Flags(). - StringVar(&cmd.ForwardPortsTimeout, "forward-ports-timeout", "", - "Specifies the timeout after which the command should terminate when the ports are unused.") - sshCmd.Flags(). - StringVar(&cmd.Command, "command", "", "The command to execute within the workspace") - sshCmd.Flags().StringVar(&cmd.User, "user", "", "The user of the workspace to use") - sshCmd.Flags().StringVar(&cmd.WorkDir, "workdir", "", "The working directory in the container") - sshCmd.Flags(). - BoolVar(&cmd.AgentForwarding, "agent-forwarding", true, "If true forward the local ssh keys to the remote machine") - sshCmd.Flags(). - StringVar(&cmd.ReuseSSHAuthSock, "reuse-ssh-auth-sock", "", + "host are to be reverse forwarded to the given host and port, or Unix socket, on the remote side.", + ). + Shorthand("R"), + cliflags.StringArray(&cmd.SendEnvVars, names.SendEnv, nil, + "Specifies which local env variables shall be sent to the container."), + cliflags.StringArray(&cmd.SetEnvVars, names.SetEnv, nil, + "Specifies env variables to be set in the container."), + cliflags.String( + &cmd.ForwardPortsTimeout, + names.ForwardPortsTimeout, + "", + "Specifies the timeout after which the command should terminate when the ports are unused.", + ), + cliflags.String( + &cmd.Command, + names.Command, + "", + "The command to execute within the workspace", + ), + cliflags.String(&cmd.User, names.User, "", "The user of the workspace to use"), + cliflags.String(&cmd.WorkDir, names.Workdir, "", "The working directory in the container"), + cliflags.Bool(&cmd.AgentForwarding, names.AgentForwarding, true, + "If true forward the local ssh keys to the remote machine"), + cliflags.String(&cmd.ReuseSSHAuthSock, names.ReuseSSHAuthSock, "", "If set, the SSH_AUTH_SOCK is expected to already be available in the workspace "+ - "(under /tmp using the key provided) and the connection reuses this instead of creating a new one") - _ = sshCmd.Flags().MarkHidden("reuse-ssh-auth-sock") - sshCmd.Flags(). - BoolVar(&cmd.GPGAgentForwarding, "gpg-agent-forwarding", false, - "If true forward the local gpg-agent to the remote machine") - sshCmd.Flags(). - BoolVar(&cmd.Stdio, "stdio", false, "If true will tunnel connection through stdout and stdin") - sshCmd.Flags(). - BoolVar(&cmd.StartServices, "start-services", true, - "If false will not start any port-forwarding or git / docker credentials helper") - sshCmd.Flags(). - DurationVar(&cmd.SSHKeepAliveInterval, "ssh-keepalive-interval", 55*time.Second, - "How often should keepalive request be made (55s)") - sshCmd.Flags(). - StringVar(&cmd.GitSSHSigningKey, "git-ssh-signing-key", "", - "The SSH signing key to use for git commit signing inside the workspace") - sshCmd.Flags().StringVar( - &cmd.TermMode, - "term-mode", - machine.TermModeAuto, - "PTY TERM selection mode: auto, strict, fallback", - ) - sshCmd.Flags().BoolVar( - &cmd.InstallTerminfo, - "install-terminfo", - false, - "Install local TERM terminfo on remote before PTY", + "(under /tmp using the key provided) and the connection reuses this instead of creating a new one"). + Hidden(), + cliflags.Bool(&cmd.GPGAgentForwarding, names.SSHGPGForwarding, false, + "Forward the local gpg-agent to the remote machine"), + cliflags.Bool( + &cmd.Stdio, + names.Stdio, + false, + "If true will tunnel connection through stdout and stdin", + ), + cliflags.Bool(&cmd.StartServices, names.StartServices, true, + "If false will not start any port-forwarding or git / docker credentials helper"), + cliflags.Duration(&cmd.SSHKeepAliveInterval, names.SSHKeepAliveInterval, 55*time.Second, + "How often should keepalive request be made (55s)"), + cliflags.String(&cmd.GitSSHSigningKey, names.GitSSHSigningKey, "", + "The SSH signing key to use for git commit signing inside the workspace"), + cliflags.String(&cmd.TermMode, names.TermMode, machine.TermModeAuto, + "PTY TERM selection mode: auto, strict, fallback"), + cliflags.Bool(&cmd.InstallTerminfo, names.InstallTerminfo, false, + "Install local TERM terminfo on remote before PTY"), ) return sshCmd @@ -265,7 +273,7 @@ func (cmd *SSHCmd) jumpContainerTailscale( // Handle GPG agent forwarding if cmd.GPGAgentForwarding || - devsyConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == config.BoolTrue { + devsyConfig.ContextOptionBool(config.ContextOptionGPGAgentForwarding) { if gpg.IsGpgTunnelRunning(ctx, cmd.User, toolSSHClient) { log.Debugf("[GPG] exporting already running, skipping") } else if err := cmd.setupGPGAgent(ctx, toolSSHClient); err != nil { @@ -526,7 +534,7 @@ func (cmd *SSHCmd) startTunnel( // check if we should do gpg agent forwarding if cmd.GPGAgentForwarding || - devsyConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == config.BoolTrue { + devsyConfig.ContextOptionBool(config.ContextOptionGPGAgentForwarding) { // Check if a forwarding is already enabled and running, in that case // we skip the forwarding and keep using the original one if gpg.IsGpgTunnelRunning(ctx, cmd.User, containerClient) { @@ -546,17 +554,21 @@ func (cmd *SSHCmd) startTunnel( config.ContainerDevsyHelperLocation, "internal", "ssh-server", - "--track-activity", - "--stdio", - "--workdir", + names.Flag(names.TrackActivity), + names.Flag(names.Stdio), + names.Flag(names.Workdir), workdir, } if cmd.ReuseSSHAuthSock != "" { log.Debug("Reusing SSH_AUTH_SOCK") - commandArgs = append(commandArgs, "--reuse-ssh-auth-sock", cmd.ReuseSSHAuthSock) + commandArgs = append( + commandArgs, + names.Flag(names.ReuseSSHAuthSock), + cmd.ReuseSSHAuthSock, + ) } if cmd.Debug { - commandArgs = append(commandArgs, "--debug") + commandArgs = append(commandArgs, names.Flag(names.Debug)) } command := shellescape.QuoteCommand(commandArgs) if cmd.User != "" && cmd.User != "root" { @@ -690,16 +702,13 @@ func (cmd *SSHCmd) setupGPGAgent( log.Debugf("[GPG] detecting gpg-agent socket path on host") // Detect local agent extra socket, this will be forwarded to the remote and // symlinked in multiple paths - gpgExtraSocketBytes, err := exec.Command("gpgconf", []string{"--list-dir", "agent-extra-socket"}...). - Output() + gpgExtraSocketPath, err := gpg.DetectAgentSocketPath() if err != nil { return err } - - gpgExtraSocketPath := strings.TrimSpace(string(gpgExtraSocketBytes)) log.Debugf("[GPG] detected gpg-agent socket path %s", gpgExtraSocketPath) - gitKey := gpgSigningKey(ctx) + gitKey := gpg.SigningKey(ctx) cmd.ReverseForwardPorts = append(cmd.ReverseForwardPorts, gpgExtraSocketPath) @@ -710,18 +719,18 @@ func (cmd *SSHCmd) setupGPGAgent( "agent", "workspace", "setup-gpg", - "--ownertrust", + names.Flag(names.OwnerTrust), ownerTrustArgument, - "--socketpath", + names.Flag(names.SocketPath), gpgExtraSocketPath, } if log.DebugEnabled() { - forwardAgent = append(forwardAgent, "--debug") + forwardAgent = append(forwardAgent, names.Flag(names.Debug)) } if gitKey != "" { - forwardAgent = append(forwardAgent, "--gitkey") + forwardAgent = append(forwardAgent, names.Flag(names.GitKey)) forwardAgent = append(forwardAgent, gitKey) } @@ -754,40 +763,6 @@ func (cmd *SSHCmd) setupGPGAgent( return nil } -// gpgSigningKey returns the user's GPG signing key from git config, -// or empty string if no key is configured or the signing format is SSH -// (SSH signing keys are handled by the separate SSH signature helper). -func gpgSigningKey(ctx context.Context) string { - config := git.At("").Config() - formatStr, _ := config.Get(ctx, "gpg.format", git.ScopeDefault) - if formatStr == "ssh" { - log.Debugf( - "gpg.format is ssh, skipping GPG signing key", - ) - return "" - } - - result, err := config.Get(ctx, "user.signingKey", git.ScopeDefault) - if err != nil { - log.Debugf("no git signkey detected, skipping") - return "" - } - - // GPG key IDs are hex fingerprints, not file paths. If the signing key - // looks like a file path and the format isn't x509 (which legitimately - // uses certificate file paths via gpgsm), it's an SSH key. - if (strings.HasPrefix(result, "/") || strings.HasPrefix(result, "~")) && formatStr != "x509" { - log.Debugf( - "signing key %s looks like a file path, skipping", - result, - ) - return "" - } - - log.Debugf("detected git sign key %s", result) - return result -} - func startSSHKeepAlive( ctx context.Context, client *ssh.Client, diff --git a/cmd/workspace/status.go b/cmd/workspace/status.go index 5b538c29b..56f4f152f 100644 --- a/cmd/workspace/status.go +++ b/cmd/workspace/status.go @@ -12,6 +12,8 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/client/clientimplementation" "github.com/devsy-org/devsy/pkg/config" + cliflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/output" workspace2 "github.com/devsy-org/devsy/pkg/workspace" "github.com/spf13/cobra" @@ -26,13 +28,13 @@ type StatusCmd struct { } // NewStatusCmd creates a new command. -func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command { +func NewStatusCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &StatusCmd{ - GlobalFlags: flags, + GlobalFlags: globalFlags, } statusCmd := &cobra.Command{ Use: "status [flags] [workspace-path|workspace-name]", - Short: "Shows the status of a workspace", + Short: "Show workspace status", RunE: func(cobraCmd *cobra.Command, args []string) error { return cmd.execute(cobraCmd.Context(), args) }, @@ -48,10 +50,12 @@ 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.Timeout, "timeout", "30s", "The timeout to wait until the status can be retrieved") + cliflags.Add(statusCmd, + cliflags.Bool(&cmd.ContainerStatus, names.ContainerStatus, true, + "If enabled shows the workspace container status as well"), + cliflags.String(&cmd.Timeout, names.Timeout, "30s", + "The timeout to wait until the status can be retrieved"), + ) return statusCmd } diff --git a/cmd/workspace/stop.go b/cmd/workspace/stop.go index 83d58ea65..dd0fd58df 100644 --- a/cmd/workspace/stop.go +++ b/cmd/workspace/stop.go @@ -28,7 +28,7 @@ func NewStopCmd(flags *flags.GlobalFlags) *cobra.Command { } stopCmd := &cobra.Command{ Use: "stop [flags] [workspace-path|workspace-name]", - Short: "Stops an existing workspace", + Short: "Stop a workspace", RunE: func(cobraCmd *cobra.Command, args []string) error { ctx := cobraCmd.Context() devsyConfig, err := config.LoadConfig(cmd.Context, cmd.Provider) diff --git a/cmd/workspace/troubleshoot.go b/cmd/workspace/troubleshoot.go index 47550b6d4..e292a0d0c 100644 --- a/cmd/workspace/troubleshoot.go +++ b/cmd/workspace/troubleshoot.go @@ -31,7 +31,7 @@ func NewTroubleshootCmd(flags *flags.GlobalFlags) *cobra.Command { } troubleshootCmd := &cobra.Command{ Use: "troubleshoot [workspace-path|workspace-name]", - Short: "Prints the workspaces troubleshooting information", + Short: "Print workspace troubleshooting information", Run: func(cobraCmd *cobra.Command, args []string) { cmd.Run(cobraCmd.Context(), args) }, diff --git a/cmd/workspace/up/configure.go b/cmd/workspace/up/configure.go index b3d994184..d8c4a68dc 100644 --- a/cmd/workspace/up/configure.go +++ b/cmd/workspace/up/configure.go @@ -25,7 +25,7 @@ func (cmd *UpCmd) configureWorkspace( devsyHome = envDevsyHome } setupGPGAgentForwarding := cmd.GPGAgentForwarding || - devsyConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == config.BoolTrue + devsyConfig.ContextOptionBool(config.ContextOptionGPGAgentForwarding) sshConfigIncludePath := devsyConfig.ContextOption(config.ContextOptionSSHConfigIncludePath) if err := configureSSH(client, configureSSHParams{ diff --git a/cmd/workspace/up/up.go b/cmd/workspace/up/up.go index 0421148d7..3f8b7d44e 100644 --- a/cmd/workspace/up/up.go +++ b/cmd/workspace/up/up.go @@ -12,8 +12,8 @@ import ( "github.com/devsy-org/devsy/cmd/flags" client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" - "github.com/devsy-org/devsy/pkg/devcontainer" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide" "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/devsy-org/devsy/pkg/log" @@ -86,10 +86,6 @@ func RunFromOptions(ctx context.Context, g *flags.GlobalFlags, opts Options) err cmd.Context) } - if _, err := devcontainer.ParseSourceSpec(cmd.DevContainerSource); err != nil { - return err - } - args := []string{opts.Source} client, err := cmd.prepareClient(ctx, devsyConfig, args) if err != nil { @@ -148,7 +144,7 @@ func NewUpCmd(f *flags.GlobalFlags) *cobra.Command { cmd := &UpCmd{GlobalFlags: f} upCmd := &cobra.Command{ Use: "up [flags] [workspace-path|workspace-name]", - Short: "Starts a new workspace", + Short: "Start a workspace", RunE: cmd.execute, } cmd.registerFlags(upCmd) @@ -188,7 +184,7 @@ func (cmd *UpCmd) Run( } func (cmd *UpCmd) applyConfig(devsyConfig *config.Config) { - if devsyConfig.ContextOption(config.ContextOptionSSHStrictHostKeyChecking) == config.BoolTrue { + if devsyConfig.ContextOptionBool(config.ContextOptionSSHStrictHostKeyChecking) { cmd.StrictHostKeyChecking = true } cmd.resolveDotfilesOptions(devsyConfig) @@ -277,12 +273,9 @@ func reportErr(err error, emitJSON bool, out io.Writer) error { // emitUpResult writes the JSON result envelope for a completed `up` invocation. func emitUpResult(wctx *workspaceContext, ideURL string, out io.Writer) { - containerID := "" + containerID := config2.GetContainerID(wctx.result) var warnings []string if wctx.result != nil { - if wctx.result.ContainerDetails != nil { - containerID = wctx.result.ContainerDetails.ID - } warnings = wctx.result.HostWarnings } _ = config2.WriteResultJSON(out, config2.ResultEnvelope{ @@ -333,7 +326,7 @@ type workspaceContext struct { } func (cmd *UpCmd) applyPullFromInsideContainerOverride(cobraCmd *cobra.Command) { - if !cobraCmd.Flags().Changed("pull-from-inside-container") { + if !cobraCmd.Flags().Changed(names.PullFromInsideContainer) { return } value := cmd.pullFromInsideContainerFlag diff --git a/cmd/workspace/up/up_client.go b/cmd/workspace/up/up_client.go index 539782622..9f0fcddc1 100644 --- a/cmd/workspace/up/up_client.go +++ b/cmd/workspace/up/up_client.go @@ -55,15 +55,6 @@ func mergeEnvFromFiles(baseOptions *provider2.CLIOptions) error { return nil } -var inheritedEnvironmentVariables = []string{ - "GIT_AUTHOR_NAME", - "GIT_AUTHOR_EMAIL", - "GIT_AUTHOR_DATE", - "GIT_COMMITTER_NAME", - "GIT_COMMITTER_EMAIL", - "GIT_COMMITTER_DATE", -} - func (cmd *UpCmd) prepareClient( ctx context.Context, devsyConfig *config.Config, @@ -152,7 +143,7 @@ func (cmd *UpCmd) prepareSecrets() error { cmd.WorkspaceEnv = options2.InheritFromEnvironment( cmd.WorkspaceEnv, - inheritedEnvironmentVariables, + options2.GitIdentityEnvVars, "", ) diff --git a/cmd/workspace/up/up_devcontainer_source_test.go b/cmd/workspace/up/up_devcontainer_source_test.go new file mode 100644 index 000000000..99f560e50 --- /dev/null +++ b/cmd/workspace/up/up_devcontainer_source_test.go @@ -0,0 +1,49 @@ +package up + +import ( + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const srcNone = "none" + +func TestResolveDevContainerSource_ID(t *testing.T) { + cmd := &UpCmd{GlobalFlags: &flags.GlobalFlags{}} + cmd.DevContainerSource = "id:default" + + require.NoError(t, cmd.resolveDevContainerSource()) + assert.Equal(t, "default", cmd.DevContainerID) + assert.Empty(t, cmd.DevContainerSource) +} + +func TestResolveDevContainerSource_Path(t *testing.T) { + cmd := &UpCmd{GlobalFlags: &flags.GlobalFlags{}} + cmd.DevContainerSource = ".devcontainer/custom.json" + + require.NoError(t, cmd.resolveDevContainerSource()) + assert.Equal(t, ".devcontainer/custom.json", cmd.DevContainerPath) + assert.Empty(t, cmd.DevContainerSource) +} + +func TestResolveDevContainerSource_NoneAndImagePassThrough(t *testing.T) { + for _, spec := range []string{srcNone, "image:python"} { + cmd := &UpCmd{GlobalFlags: &flags.GlobalFlags{}} + cmd.DevContainerSource = spec + + require.NoError(t, cmd.resolveDevContainerSource()) + // none/image specs stay for agent-side handling. + assert.Equal(t, spec, cmd.DevContainerSource) + assert.Empty(t, cmd.DevContainerID) + assert.Empty(t, cmd.DevContainerPath) + } +} + +func TestResolveDevContainerSource_Invalid(t *testing.T) { + cmd := &UpCmd{GlobalFlags: &flags.GlobalFlags{}} + cmd.DevContainerSource = "image:" + + require.Error(t, cmd.resolveDevContainerSource()) +} diff --git a/cmd/workspace/up/up_flags.go b/cmd/workspace/up/up_flags.go index c0d293ed8..a8dbd2751 100644 --- a/cmd/workspace/up/up_flags.go +++ b/cmd/workspace/up/up_flags.go @@ -1,6 +1,8 @@ package up import ( + "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/spf13/cobra" ) @@ -17,37 +19,31 @@ func (cmd *UpCmd) registerFlags(upCmd *cobra.Command) { } func (cmd *UpCmd) registerSSHFlags(upCmd *cobra.Command) { - upCmd.Flags(). - BoolVar(&cmd.ConfigureSSH, "configure-ssh", true, - "If true will configure the ssh config to include the Devsy workspace") - upCmd.Flags(). - BoolVar(&cmd.GPGAgentForwarding, "gpg-agent-forwarding", false, - "If true forward the local gpg-agent to the Devsy workspace") - upCmd.Flags(). - StringVar(&cmd.SSHConfigPath, "ssh-config", "", - "The path to the ssh config to modify, if empty will use ~/.ssh/config") - upCmd.Flags(). - BoolVar(&cmd.SSHTunnelMode, "ssh-tunnel-mode", false, - "If true will use a local TCP tunnel instead of ProxyCommand for SSH connections") + flags.Add(upCmd, + flags.Bool(&cmd.ConfigureSSH, names.SSHConfigure, true, + "Add the workspace to your SSH config"), + flags.Bool(&cmd.GPGAgentForwarding, names.SSHGPGForwarding, false, + "Forward the local gpg-agent into the workspace"), + flags.String(&cmd.SSHConfigPath, names.SSHConfig, "", + "Path to the SSH config to modify (default ~/.ssh/config)"), + flags.Bool(&cmd.SSHTunnelMode, names.SSHTunnel, false, + "Use a local TCP tunnel instead of ProxyCommand for SSH connections"), + ) } func (cmd *UpCmd) registerDotfilesFlags(upCmd *cobra.Command) { - upCmd.Flags(). - StringVar(&cmd.DotfilesSource, "dotfiles", "", "The path or url to the dotfiles to use in the container") - upCmd.Flags().StringVar(&cmd.DotfilesSource, "dotfiles-repository", "", "Alias for --dotfiles") - _ = upCmd.Flags().MarkHidden("dotfiles-repository") - upCmd.Flags(). - StringVar(&cmd.DotfilesScript, "dotfiles-script", "", - "The path in dotfiles directory to use to install the dotfiles, if empty will try to guess") - upCmd.Flags(). - StringVar(&cmd.DotfilesTargetPath, "dotfiles-target-path", "", - "The target path inside the container to install dotfiles to (e.g., ~/dotfiles)") - upCmd.Flags(). - StringSliceVar(&cmd.DotfilesScriptEnv, "dotfiles-script-env", []string{}, - "Extra environment variables to put into the dotfiles install script, e.g. MY_ENV_VAR=MY_VALUE") - upCmd.Flags(). - StringSliceVar(&cmd.DotfilesScriptEnvFile, "dotfiles-script-env-file", []string{}, - "The path to files containing environment variables to set for the dotfiles install script") + flags.Add(upCmd, + flags.String(&cmd.DotfilesSource, names.Dotfiles, "", + "Path or URL to dotfiles to install in the container"), + flags.String(&cmd.DotfilesScript, names.DotfilesScript, "", + "Script within the dotfiles to run (auto-detected if unset)"), + flags.String(&cmd.DotfilesTargetPath, names.DotfilesTargetPath, "", + "Path inside the container to install dotfiles to (e.g. ~/dotfiles)"), + flags.StringSlice(&cmd.DotfilesScriptEnv, names.DotfilesScriptEnv, nil, + "Env var for the dotfiles script (KEY=VALUE, repeatable)"), + flags.StringSlice(&cmd.DotfilesScriptEnvFile, names.DotfilesScriptEnvFile, nil, + "File of env vars for the dotfiles script"), + ) } func (cmd *UpCmd) registerDevContainerFlags(upCmd *cobra.Command) { @@ -57,213 +53,187 @@ func (cmd *UpCmd) registerDevContainerFlags(upCmd *cobra.Command) { } func (cmd *UpCmd) registerBuildFlags(upCmd *cobra.Command) { - upCmd.Flags(). - StringVar(&cmd.DevContainerSource, "devcontainer", "", - "Override where the devcontainer config comes from, ignoring the project's: "+ - `"none" (ignore it; use --devcontainer-image or language detection) or `+ - `"image:" (use only that image)`) - upCmd.Flags(). - StringVar(&cmd.DevContainerImage, "devcontainer-image", "", - "The container image to use, this overrides the image in the resolved devcontainer config") - upCmd.Flags(). - StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project") - upCmd.Flags().StringVar(&cmd.DevContainerPath, "config", "", "Alias for --devcontainer-path") - _ = upCmd.Flags().MarkHidden("config") - upCmd.Flags(). - StringVar(&cmd.DevContainerID, "devcontainer-id", "", - "The ID of the devcontainer to use when multiple exist "+ - "(e.g., folder name in .devcontainer/FOLDER/devcontainer.json)") - upCmd.Flags(). - StringVar(&cmd.ExtraDevContainerPath, "extra-devcontainer-path", "", - "The path to an additional devcontainer.json file to override original devcontainer.json") - upCmd.Flags(). - StringVar(&cmd.ExtraDevContainerPath, "override-config", "", "Alias for --extra-devcontainer-path") - _ = upCmd.Flags().MarkHidden("override-config") - upCmd.Flags(). - StringVar(&cmd.FallbackImage, "fallback-image", "", - "The fallback image to use if no devcontainer configuration has been detected") - upCmd.Flags(). - StringVar(&cmd.AdditionalFeatures, "additional-features", "", - `Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)`) - upCmd.Flags(). - StringArrayVar(&cmd.IDLabels, "id-label", []string{}, - "Override the default container identification labels (format: key=value, can be specified multiple times)") - upCmd.Flags(). - StringVar(&cmd.DefaultUserEnvProbe, "default-user-env-probe", "", - "Override userEnvProbe from devcontainer.json (loginInteractiveShell, loginShell, interactiveShell, none)") - upCmd.Flags(). - StringVar(&cmd.GPUAvailability, "gpu-availability", "", - "Override GPU availability detection (detect, true, false)") - upCmd.Flags(). - StringVar(&cmd.UpdateRemoteUserUIDDefault, "update-remote-user-uid-default", "", - "Default for updateRemoteUserUID when not set in devcontainer.json (on, off)") - upCmd.Flags(). - StringVar(&cmd.ContainerDataFolder, "container-data-folder", "", - "Custom path for container-specific data") defaultMountGitRoot := true cmd.MountWorkspaceGitRoot = &defaultMountGitRoot - upCmd.Flags(). - BoolVar(cmd.MountWorkspaceGitRoot, "mount-workspace-git-root", true, - "Mount the workspace git root as the workspace folder") + flags.Add(upCmd, + flags.String(&cmd.DevContainerSource, names.DevContainer, "", + "Select the devcontainer config source, overriding project discovery: "+ + `"none" (ignore the project config), "image:" (use only that image), `+ + `"id:" (a named .devcontainer/ profile), or a path to a devcontainer.json`), + flags.String(&cmd.ExtraDevContainerPath, names.DevContainerOverlay, "", + "Path to a devcontainer.json whose values merge on top of the resolved config"), + flags.String(&cmd.FallbackImage, names.FallbackImage, "", + "Image to use when no devcontainer config is found"), + flags.String(&cmd.GPUAvailability, names.GPUAvailability, "", + "Override GPU detection (detect, true, false)"), + flags.String(&cmd.UpdateRemoteUserUIDDefault, names.UpdateRemoteUserUID, "", + "Default for updateRemoteUserUID when unset in the config (on, off)"), + flags.Bool(cmd.MountWorkspaceGitRoot, names.MountWorkspaceGitRoot, true, + "Mount the workspace git root as the workspace folder"), + ) + flags.RegisterDevContainerModifierFlags(upCmd.Flags(), flags.DevContainerModifierFlags{ + Image: &cmd.DevContainerImage, + Features: &cmd.AdditionalFeatures, + UserEnvProbe: &cmd.DefaultUserEnvProbe, + IDLabels: &cmd.IDLabels, + ContainerDataFolder: &cmd.ContainerDataFolder, + }) } func (cmd *UpCmd) registerLifecycleFlags(upCmd *cobra.Command) { - upCmd.Flags(). - IntVar(&cmd.TerminalColumns, "terminal-columns", 0, - "Terminal column count for lifecycle scripts") - upCmd.Flags(). - IntVar(&cmd.TerminalRows, "terminal-rows", 0, - "Terminal row count for lifecycle scripts") - upCmd.Flags(). - BoolVar(&cmd.SkipPostCreate, "skip-post-create", false, - "Skip the postCreateCommand lifecycle hook") - upCmd.Flags(). - BoolVar(&cmd.SkipNonBlockingCommands, "skip-non-blocking-commands", false, - "Skip non-blocking lifecycle commands") - upCmd.Flags(). - BoolVar(&cmd.SkipPostStart, "skip-post-start", false, - "Skip running postStartCommand") - upCmd.Flags(). - BoolVar(&cmd.SkipPostAttach, "skip-post-attach", false, - "Skip running postAttachCommand") - upCmd.Flags(). - BoolVar(&cmd.SkipHostRequirements, "skip-host-requirements", false, - "Skip host requirements validation and allow container creation even if the host does not meet minimum requirements") + flags.Add(upCmd, + flags.Int(&cmd.TerminalColumns, names.TerminalColumns, 0, + "Terminal column count for lifecycle scripts"), + flags.Int(&cmd.TerminalRows, names.TerminalRows, 0, + "Terminal row count for lifecycle scripts"), + flags.Bool(&cmd.SkipPostCreate, names.SkipPostCreate, false, + "Skip the postCreateCommand hook"), + flags.Bool(&cmd.SkipNonBlockingCommands, names.SkipNonBlockingCommands, false, + "Skip non-blocking lifecycle commands"), + flags.Bool(&cmd.SkipPostStart, names.SkipPostStart, false, + "Skip the postStartCommand hook"), + flags.Bool(&cmd.SkipPostAttach, names.SkipPostAttach, false, + "Skip the postAttachCommand hook"), + flags.Bool(&cmd.SkipHostRequirements, names.SkipHostRequirements, false, + "Skip host requirements validation"), + ) } func (cmd *UpCmd) registerContainerOverrideFlags(upCmd *cobra.Command) { - upCmd.Flags(). - StringVar(&cmd.ContainerUser, "container-user", "", - "Override the user in the container") - upCmd.Flags(). - StringVar(&cmd.RemoteUser, "remote-user", "", - "Override the remoteUser setting") + flags.Add(upCmd, + flags.String(&cmd.ContainerUser, names.ContainerUser, "", + "Override the container user"), + flags.String(&cmd.RemoteUser, names.RemoteUser, "", + "Override the remoteUser setting"), + ) } func (cmd *UpCmd) registerIDEFlags(upCmd *cobra.Command) { - upCmd.Flags(). - StringVar(&cmd.IDE, "ide", "", "The IDE to open the workspace in. If empty will use vscode locally or in browser") - upCmd.Flags(). - StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE") cmd.IDELaunch = opener.LaunchAuto - upCmd.Flags().Var( - &cmd.IDELaunch, - "ide-launch", - "How to launch the IDE: auto opens it (default), headless skips the host browser/app, skip does not launch.", + flags.Add( + upCmd, + flags.String(&cmd.IDE, names.IDE, "", + "IDE to open the workspace in (defaults to vscode, locally or in browser)"), + flags.StringArray( + &cmd.IDEOptions, + names.IDEOption, + nil, + "IDE option (KEY=VALUE, repeatable)", + ), + flags.Value( + &cmd.IDELaunch, + names.IDELaunch, + "How to launch the IDE: auto opens it (default), headless skips the host browser/app, skip does not launch", + ), + flags.String(&cmd.WorkspaceFolder, names.WorkspaceFolder, "", + "Folder to open in the IDE (absolute path inside the container)"), ) - upCmd.Flags(). - StringVar(&cmd.WorkspaceFolder, "workspace-folder", "", - "Override the folder path opened in the IDE (absolute path inside the container)") } func (cmd *UpCmd) registerGitFlags(upCmd *cobra.Command) { - upCmd.Flags(). - Var(&cmd.GitCloneStrategy, "git-clone-strategy", - "The git clone strategy Devsy uses to checkout git based workspaces. "+ - "Can be full (default), blobless, treeless or shallow") - upCmd.Flags(). - BoolVar(&cmd.GitCloneRecursiveSubmodules, "git-clone-recursive-submodules", false, - "If true will clone git submodule repositories recursively") - upCmd.Flags(). - Var(&cmd.GitLFSMode, "git-lfs-mode", - "How Devsy handles Git LFS after cloning. Can be full (default, download LFS "+ - "content), setup-only (configure LFS but leave pointer files) or skip (ignore LFS)") - upCmd.Flags(). - StringVar(&cmd.GitSSHSigningKey, "git-ssh-signing-key", "", - "The ssh key to use when signing git commits. Used to explicitly setup Devsy's ssh signature "+ - "forwarding with given key. Should be same format as value of `git config user.signingkey`") + flags.Add(upCmd, + flags.Value(&cmd.GitCloneStrategy, names.GitCloneStrategy, + "Clone strategy for git workspaces: full (default), blobless, treeless or shallow"), + flags.Bool(&cmd.GitCloneRecursiveSubmodules, names.GitRecurseSubmodules, false, + "Clone submodules recursively"), + flags.Value(&cmd.GitLFSMode, names.GitLFSMode, + "Git LFS handling after cloning: full (default, download content), "+ + "setup-only (configure LFS, leave pointer files) or skip (ignore LFS)"), + flags.String(&cmd.GitSSHSigningKey, names.GitSSHSigningKey, "", + "SSH key to sign git commits with (same format as git config user.signingkey)"), + ) } func (cmd *UpCmd) registerPodmanFlags(upCmd *cobra.Command) { - upCmd.Flags(). - StringVar(&cmd.Userns, "userns", "", - "User namespace to use for the container (Podman only; e.g. \"keep-id\", \"host\", or \"auto\")") - upCmd.Flags(). - StringSliceVar(&cmd.UidMap, "uidmap", []string{}, + flags.Add(upCmd, + flags.String( + &cmd.Userns, + names.Userns, + "", + "User namespace to use for the container (Podman only; e.g. \"keep-id\", \"host\", or \"auto\")", + ), + flags.StringSlice(&cmd.UidMap, names.UIDMap, nil, "UID mapping for Podman user namespace "+ - "(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")") - upCmd.Flags(). - StringSliceVar(&cmd.GidMap, "gidmap", []string{}, + "(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")"), + flags.StringSlice(&cmd.GidMap, names.GIDMap, nil, "GID mapping for Podman user namespace "+ - "(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")") + "(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")"), + ) + upCmd.MarkFlagsMutuallyExclusive(names.Userns, names.UIDMap) + upCmd.MarkFlagsMutuallyExclusive(names.Userns, names.GIDMap) } func (cmd *UpCmd) registerWorkspaceFlags(upCmd *cobra.Command) { - upCmd.Flags().StringVar(&cmd.ID, "id", "", "The id to use for the workspace") - upCmd.Flags(). - StringVar(&cmd.Machine, "machine", "", - "The machine to use for this workspace. The machine needs to exist beforehand or the "+ - "command will fail. If the workspace already exists, this option has no effect") - upCmd.Flags(). - StringVar(&cmd.Source, "source", "", "Optional source for the workspace, e.g. git:https://github.com/my-org/my-repo") - upCmd.Flags(). - StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE") - upCmd.Flags(). - BoolVar(&cmd.Reconfigure, "reconfigure", false, - "Reconfigure the options for this workspace. Only supported in Devsy Pro right now.") - upCmd.Flags(). - BoolVar(&cmd.Prebuild, "prebuild", false, - "If true will only run the prebuild lifecycle (onCreateCommand + updateContentCommand) then stop") - upCmd.Flags(). - BoolVar(&cmd.Pull, "pull", false, - "Always attempt to pull a newer version of the base image when building") - upCmd.Flags(). - BoolVar(&cmd.NoCache, "no-cache", false, - "Do not use the build cache when building the image") - upCmd.Flags(). - BoolVar(&cmd.Recreate, "recreate", false, "If true will remove any existing containers and recreate them") - upCmd.Flags().BoolVar(&cmd.Recreate, "remove-existing-container", false, "Alias for --recreate") - _ = upCmd.Flags().MarkHidden("remove-existing-container") - upCmd.Flags(). - BoolVar(&cmd.Reset, "reset", false, - "If true will remove any existing containers including sources, and recreate them") - upCmd.Flags(). - StringSliceVar(&cmd.PrebuildRepositories, "prebuild-repository", []string{}, - "Docker repository that hosts devsy prebuilds for this workspace") - upCmd.Flags(). - StringArrayVar(&cmd.WorkspaceEnv, "workspace-env", []string{}, - "Extra env variables to put into the workspace, e.g. MY_ENV_VAR=MY_VALUE") - upCmd.Flags(). - StringSliceVar(&cmd.WorkspaceEnvFile, "workspace-env-file", []string{}, - "The path to files containing a list of extra env variables to put into the workspace, "+ - "e.g. MY_ENV_VAR=MY_VALUE") - upCmd.Flags(). - StringVar(&cmd.SecretsFile, "secrets-file", "", - "Path to a dotenv-style file containing KEY=VALUE secrets injected into lifecycle commands") - upCmd.Flags(). - StringVar(&cmd.FeatureSecretsFile, "feature-secrets-file", "", - "Path to a JSON file containing secret values for features, format: "+ - `{"featureId": {"optionName": "value"}}`) - upCmd.Flags(). - StringArrayVar(&cmd.InitEnv, "init-env", []string{}, - "Extra env variables to inject during the initialization of the workspace, e.g. MY_ENV_VAR=MY_VALUE") - upCmd.Flags(). - BoolVar(&cmd.DisableDaemon, "disable-daemon", false, - "If enabled, will not install a daemon into the target machine to track activity") - upCmd.Flags(). - StringArrayVar(&cmd.CacheFrom, "cache-from", []string{}, - "Cache sources for the build (e.g., myregistry.io/cache:latest or type=registry,ref=...). "+ - "Takes priority over devcontainer.json build.cacheFrom") - upCmd.Flags(). - StringVar(&cmd.WorkspaceMountConsistency, "workspace-mount-consistency", "", - "Consistency mode for the workspace bind mount (consistent, cached, delegated)") - upCmd.Flags(). - StringArrayVar(&cmd.Mounts, "mount", []string{}, - "Additional mount to add to the container (format: type=bind,source=/host/path,target=/container/path). "+ - "Can be specified multiple times") - upCmd.Flags(). - StringVar(&cmd.RunPlatform, "platform", "", - "Run the container under a specific platform via emulation (e.g. linux/amd64). "+ - "Empty uses the host's native platform") - upCmd.Flags(). - BoolVar(&cmd.pullFromInsideContainerFlag, "pull-from-inside-container", false, - "Clone the workspace source inside the container instead of bind-mounting "+ - "from the host. Unset = auto-detect.") + flags.Add( + upCmd, + flags.String(&cmd.ID, names.ID, "", "ID for the workspace"), + flags.String( + &cmd.Machine, + names.Machine, + "", + "Existing machine to use for this workspace; no effect if the workspace already exists", + ), + flags.String(&cmd.Source, names.Source, "", + "Workspace source (e.g. git:https://github.com/my-org/my-repo)"), + flags.StringArray(&cmd.ProviderOptions, names.ProviderOption, nil, + "Provider option (KEY=VALUE, repeatable)"), + flags.Bool(&cmd.Reconfigure, names.Reconfigure, false, + "Reconfigure this workspace's options (Devsy Pro only)"), + flags.Bool(&cmd.Prebuild, names.Prebuild, false, + "Run only the prebuild lifecycle (onCreateCommand + updateContentCommand), then stop"), + flags.Bool(&cmd.Pull, names.Pull, false, "Pull a newer base image when building"), + flags.Bool(&cmd.NoCache, names.NoCache, false, "Build without the image cache"), + flags.Bool( + &cmd.Recreate, + names.Recreate, + false, + "Remove and recreate existing containers", + ), + flags.Bool(&cmd.Reset, names.Reset, false, + "Remove and recreate existing containers, including their sources"), + flags.StringSlice(&cmd.PrebuildRepositories, names.PrebuildRepo, nil, + "Docker repository hosting prebuilds for this workspace"), + flags.StringArray(&cmd.WorkspaceEnv, names.WorkspaceEnv, nil, + "Env var for the workspace (KEY=VALUE, repeatable)"), + flags.StringSlice(&cmd.WorkspaceEnvFile, names.WorkspaceEnvFile, nil, + "File of env vars for the workspace"), + flags.String(&cmd.SecretsFile, names.SecretsFile, "", + "Dotenv file of KEY=VALUE secrets for lifecycle commands"), + flags.String(&cmd.FeatureSecretsFile, names.FeatureSecretsFile, "", + `JSON file of feature secret values, format: {"featureId": {"optionName": "value"}}`), + flags.StringArray(&cmd.InitEnv, names.InitEnv, nil, + "Env var for workspace initialization (KEY=VALUE, repeatable)"), + flags.Bool(&cmd.DisableDaemon, names.DisableDaemon, false, + "Do not install the activity-tracking daemon on the target machine"), + flags.StringArray(&cmd.CacheFrom, names.CacheFrom, nil, + "Build cache source (e.g. myregistry.io/cache:latest or type=registry,ref=...); "+ + "takes priority over devcontainer.json build.cacheFrom"), + flags.String(&cmd.WorkspaceMountConsistency, names.WorkspaceMountConsistency, "", + "Consistency mode for the workspace bind mount (consistent, cached, delegated)"), + flags.StringArray( + &cmd.Mounts, + names.Mount, + nil, + "Extra container mount (type=bind,source=/host/path,target=/container/path; repeatable)", + ), + flags.String( + &cmd.RunPlatform, + names.Platform, + "", + "Run under a specific platform via emulation (e.g. linux/amd64); empty uses the host platform", + ), + flags.Bool( + &cmd.pullFromInsideContainerFlag, + names.PullFromInsideContainer, + false, + "Clone the source inside the container instead of bind-mounting from the host (unset = auto-detect)", + ), + ) } func (cmd *UpCmd) registerTestingFlags(upCmd *cobra.Command) { - upCmd.Flags().StringVar(&cmd.DaemonInterval, "daemon-interval", "", "TESTING ONLY") - _ = upCmd.Flags().MarkHidden("daemon-interval") - upCmd.Flags().BoolVar(&cmd.ForceDockerless, "force-dockerless", false, "TESTING ONLY") - _ = upCmd.Flags().MarkHidden("force-dockerless") + flags.Add(upCmd, + flags.String(&cmd.DaemonInterval, names.DaemonInterval, "", "TESTING ONLY").Hidden(), + flags.Bool(&cmd.ForceDockerless, names.ForceDockerless, false, "TESTING ONLY").Hidden(), + ) } diff --git a/cmd/workspace/up/up_test.go b/cmd/workspace/up/up_test.go index 7c756d289..a6e920729 100644 --- a/cmd/workspace/up/up_test.go +++ b/cmd/workspace/up/up_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -48,23 +49,23 @@ func TestUpCmd_ValidateDefaultUserEnvProbe(t *testing.T) { func TestUpCmd_FlagRegistered(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("default-user-env-probe") + flag := upCmd.Flags().Lookup(names.UserEnvProbe) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } func TestUpCmd_FlagParsesValue(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--default-user-env-probe", probeNone}) + err := upCmd.ParseFlags([]string{names.Flag(names.UserEnvProbe), probeNone}) require.NoError(t, err) - flag := upCmd.Flags().Lookup("default-user-env-probe") + flag := upCmd.Flags().Lookup(names.UserEnvProbe) assert.Equal(t, probeNone, flag.Value.String()) } func TestUpCmd_WorkspaceMountConsistencyFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("workspace-mount-consistency") + flag := upCmd.Flags().Lookup(names.WorkspaceMountConsistency) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } @@ -81,10 +82,12 @@ func TestUpCmd_WorkspaceMountConsistencyFlagParsesValue(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--workspace-mount-consistency", tt.value}) + err := upCmd.ParseFlags( + []string{names.Flag(names.WorkspaceMountConsistency), tt.value}, + ) require.NoError(t, err) - flag := upCmd.Flags().Lookup("workspace-mount-consistency") + flag := upCmd.Flags().Lookup(names.WorkspaceMountConsistency) assert.Equal(t, tt.value, flag.Value.String()) }) } @@ -120,28 +123,28 @@ func TestUpCmd_ValidateWorkspaceMountConsistency(t *testing.T) { func TestUpCmd_SkipPostCreateFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("skip-post-create") + flag := upCmd.Flags().Lookup(names.SkipPostCreate) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } func TestUpCmd_SkipPostStartFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("skip-post-start") + flag := upCmd.Flags().Lookup(names.SkipPostStart) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } func TestUpCmd_SkipPostAttachFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("skip-post-attach") + flag := upCmd.Flags().Lookup(names.SkipPostAttach) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } func TestUpCmd_SkipHostRequirementsFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("skip-host-requirements") + flag := upCmd.Flags().Lookup(names.SkipHostRequirements) require.NotNil(t, flag) assert.Equal(t, "false", flag.DefValue) } @@ -149,59 +152,59 @@ func TestUpCmd_SkipHostRequirementsFlag(t *testing.T) { func TestUpCmd_SkipFlagsParseValues(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) err := upCmd.ParseFlags([]string{ - "--skip-post-create", - "--skip-post-start", - "--skip-post-attach", - "--skip-host-requirements", + names.Flag(names.SkipPostCreate), + names.Flag(names.SkipPostStart), + names.Flag(names.SkipPostAttach), + names.Flag(names.SkipHostRequirements), }) require.NoError(t, err) - val, err := upCmd.Flags().GetBool("skip-post-create") + val, err := upCmd.Flags().GetBool(names.SkipPostCreate) require.NoError(t, err) assert.True(t, val) - val, err = upCmd.Flags().GetBool("skip-post-start") + val, err = upCmd.Flags().GetBool(names.SkipPostStart) require.NoError(t, err) assert.True(t, val) - val, err = upCmd.Flags().GetBool("skip-post-attach") + val, err = upCmd.Flags().GetBool(names.SkipPostAttach) require.NoError(t, err) assert.True(t, val) - val, err = upCmd.Flags().GetBool("skip-host-requirements") + val, err = upCmd.Flags().GetBool(names.SkipHostRequirements) require.NoError(t, err) assert.True(t, val) } func TestUpCmd_ContainerUserFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("container-user") + flag := upCmd.Flags().Lookup(names.ContainerUser) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } func TestUpCmd_ContainerUserFlagParsesValue(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--container-user", "devuser"}) + err := upCmd.ParseFlags([]string{names.Flag(names.ContainerUser), "devuser"}) require.NoError(t, err) - flag := upCmd.Flags().Lookup("container-user") + flag := upCmd.Flags().Lookup(names.ContainerUser) assert.Equal(t, "devuser", flag.Value.String()) } func TestUpCmd_RemoteUserFlag(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - flag := upCmd.Flags().Lookup("remote-user") + flag := upCmd.Flags().Lookup(names.RemoteUser) require.NotNil(t, flag) assert.Equal(t, "", flag.DefValue) } func TestUpCmd_RemoteUserFlagParsesValue(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--remote-user", "vscode"}) + err := upCmd.ParseFlags([]string{names.Flag(names.RemoteUser), "vscode"}) require.NoError(t, err) - flag := upCmd.Flags().Lookup("remote-user") + flag := upCmd.Flags().Lookup(names.RemoteUser) assert.Equal(t, "vscode", flag.Value.String()) } diff --git a/cmd/workspace/up/up_update_remote_user_uid_test.go b/cmd/workspace/up/up_update_remote_user_uid_test.go index bdc2776a2..0cea3765a 100644 --- a/cmd/workspace/up/up_update_remote_user_uid_test.go +++ b/cmd/workspace/up/up_update_remote_user_uid_test.go @@ -4,26 +4,31 @@ import ( "testing" "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUpCmd_UpdateRemoteUserUIDDefault_On(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--update-remote-user-uid-default", UpdateRemoteUserUIDOn}) + err := upCmd.ParseFlags( + []string{names.Flag(names.UpdateRemoteUserUID), UpdateRemoteUserUIDOn}, + ) require.NoError(t, err) - val, err := upCmd.Flags().GetString("update-remote-user-uid-default") + val, err := upCmd.Flags().GetString(names.UpdateRemoteUserUID) require.NoError(t, err) assert.Equal(t, UpdateRemoteUserUIDOn, val) } func TestUpCmd_UpdateRemoteUserUIDDefault_Off(t *testing.T) { upCmd := NewUpCmd(&flags.GlobalFlags{}) - err := upCmd.ParseFlags([]string{"--update-remote-user-uid-default", UpdateRemoteUserUIDOff}) + err := upCmd.ParseFlags( + []string{names.Flag(names.UpdateRemoteUserUID), UpdateRemoteUserUIDOff}, + ) require.NoError(t, err) - val, err := upCmd.Flags().GetString("update-remote-user-uid-default") + val, err := upCmd.Flags().GetString(names.UpdateRemoteUserUID) require.NoError(t, err) assert.Equal(t, UpdateRemoteUserUIDOff, val) } @@ -33,7 +38,7 @@ func TestUpCmd_UpdateRemoteUserUIDDefault_Validate_Invalid(t *testing.T) { cmd.UpdateRemoteUserUIDDefault = "invalid" err := cmd.validate() require.Error(t, err) - assert.Contains(t, err.Error(), "invalid --update-remote-user-uid-default value") + assert.Contains(t, err.Error(), "invalid --update-remote-user-uid value") } func TestUpCmd_UpdateRemoteUserUIDDefault_Validate_Empty(t *testing.T) { diff --git a/cmd/workspace/up/up_validate.go b/cmd/workspace/up/up_validate.go index 435599e65..a576b1140 100644 --- a/cmd/workspace/up/up_validate.go +++ b/cmd/workspace/up/up_validate.go @@ -6,7 +6,9 @@ import ( "strconv" "strings" + "github.com/devsy-org/devsy/pkg/devcontainer" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" ) const ( @@ -19,6 +21,9 @@ const ( ) func (cmd *UpCmd) validate() error { + if err := cmd.resolveDevContainerSource(); err != nil { + return err + } if err := validatePodmanFlags(cmd); err != nil { return err } @@ -43,6 +48,26 @@ func (cmd *UpCmd) validate() error { return validateRemoteUserUID(cmd.UpdateRemoteUserUIDDefault) } +func (cmd *UpCmd) resolveDevContainerSource() error { + spec, err := devcontainer.ParseSourceSpec(cmd.DevContainerSource) + if err != nil { + return err + } + if spec == nil { + return nil + } + switch spec.Kind { + case devcontainer.SourceID: + cmd.DevContainerID = spec.ID + cmd.DevContainerSource = "" + case devcontainer.SourcePath: + cmd.DevContainerPath = spec.Path + cmd.DevContainerSource = "" + case devcontainer.SourceNone, devcontainer.SourceImage: + } + return nil +} + func (cmd *UpCmd) resolveExtraDevContainerPath() error { if cmd.ExtraDevContainerPath == "" { return nil @@ -77,7 +102,8 @@ func validateMounts(mounts []string) error { parsed := config2.ParseMount(m) if parsed.Target == "" { return fmt.Errorf( - "invalid --mount value %q: target (dst/destination/target) is required", + "invalid %s value %q: target (dst/destination/target) is required", + names.Flag(names.Mount), m, ) } @@ -94,18 +120,17 @@ func validateRemoteUserUID(value string) error { return nil default: return fmt.Errorf( - "invalid --update-remote-user-uid-default value %q: must be \"on\" or \"off\"", + "invalid %s value %q: must be \"on\" or \"off\"", + names.Flag(names.UpdateRemoteUserUID), value, ) } } +// validatePodmanFlags validates UID/GID mapping formats. The --userns vs +// --uidmap/--gidmap exclusivity is enforced declaratively via +// MarkFlagsMutuallyExclusive at registration. func validatePodmanFlags(cmd *UpCmd) error { - if cmd.Userns != "" && (len(cmd.UidMap) > 0 || len(cmd.GidMap) > 0) { - return fmt.Errorf( - "--userns cannot be combined with --uidmap or --gidmap (mutually exclusive)", - ) - } for _, m := range cmd.UidMap { if !isValidMapping(m) { return fmt.Errorf( diff --git a/e2e/framework/command.go b/e2e/framework/command.go index c34ac7dd7..8952fdab8 100644 --- a/e2e/framework/command.go +++ b/e2e/framework/command.go @@ -9,24 +9,28 @@ import ( "strings" "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/flags/names" provider2 "github.com/devsy-org/devsy/pkg/provider" "github.com/devsy-org/devsy/pkg/workspace" ) +var ( + flagResultFormat = names.Flag(names.ResultFormat) + flagIDE = names.Flag(names.IDE) + flagDebug = names.Flag(names.Debug) + flagCommand = names.Flag(names.Command) +) + const ( - flagResultFormat = "--result-format" - flagIDE = "--ide" - flagDebug = "--debug" - flagCommand = "--command" - formatJSON = "json" - cmdList = "list" - cmdGet = "get" - cmdDelete = "delete" - cmdSSH = "ssh" - cmdProvider = "provider" - cmdWorkspace = "workspace" - ideNone = "none" - cmdUse = "use" + formatJSON = "json" + cmdList = "list" + cmdGet = "get" + cmdDelete = "delete" + cmdSSH = "ssh" + cmdProvider = "provider" + cmdWorkspace = "workspace" + ideNone = "none" + cmdUse = "use" ) func (f *Framework) FindWorkspace(ctx context.Context, id string) (*provider2.Workspace, error) { @@ -154,7 +158,14 @@ func (f *Framework) DevsyUp(ctx context.Context, additionalArgs ...string) error } func (f *Framework) DevsyUpRecreate(ctx context.Context, additionalArgs ...string) error { - upArgs := []string{cmdWorkspace, "up", "--recreate", flagDebug, flagIDE, ideNone} + upArgs := []string{ + cmdWorkspace, + "up", + names.Flag(names.Recreate), + flagDebug, + flagIDE, + ideNone, + } upArgs = append(upArgs, additionalArgs...) _, _, err := execWithDockerRetry(ctx, func(ctx context.Context) (string, string, error) { @@ -167,7 +178,14 @@ func (f *Framework) DevsyUpRecreate(ctx context.Context, additionalArgs ...strin } func (f *Framework) DevsyUpReset(ctx context.Context, additionalArgs ...string) error { - upArgs := []string{cmdWorkspace, "up", "--reset", flagDebug, flagIDE, ideNone} + upArgs := []string{ + cmdWorkspace, + "up", + names.Flag(names.Reset), + flagDebug, + flagIDE, + ideNone, + } upArgs = append(upArgs, additionalArgs...) _, _, err := execWithDockerRetry(ctx, func(ctx context.Context) (string, string, error) { @@ -407,7 +425,12 @@ func (f *Framework) DevsyWorkspaceDelete( workspace string, extraArgs ...string, ) error { - baseArgs := []string{cmdWorkspace, cmdDelete, workspace, "--ignore-not-found"} + baseArgs := []string{ + cmdWorkspace, + cmdDelete, + workspace, + names.Flag(names.IgnoreNotFound), + } baseArgs = append(baseArgs, extraArgs...) return f.ExecCommand(ctx, false, true, fmt.Sprintf("deleted workspace %s", workspace), baseArgs) @@ -456,8 +479,8 @@ func (f *Framework) DevsySSHGpgTestKey(ctx context.Context, workspace string) er stdout, _, err := f.ExecCommandCapture(ctx, []string{ cmdWorkspace, cmdSSH, - "--agent-forwarding", - "--gpg-agent-forwarding", + names.Flag(names.AgentForwarding), + names.Flag(names.SSHGPGForwarding), flagCommand, "gpg -k --with-colons 2>/dev/null |grep sec | base64 -w0", workspace, }) @@ -480,7 +503,7 @@ func (f *Framework) DevsyPortTest(ctx context.Context, port string, workspace st _, err := execWithSSHRetry(ctx, workspace, func(ctx context.Context) (string, string, error) { return f.ExecCommandCapture( ctx, - []string{cmdWorkspace, cmdSSH, "--forward-ports", port, workspace}, + []string{cmdWorkspace, cmdSSH, names.Flag(names.ForwardPorts), port, workspace}, ) }) return err diff --git a/e2e/tests/exec/exec.go b/e2e/tests/exec/exec.go index 179eff06f..99e9c5085 100644 --- a/e2e/tests/exec/exec.go +++ b/e2e/tests/exec/exec.go @@ -8,6 +8,7 @@ import ( "github.com/devsy-org/devsy/e2e/framework" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/workspace" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -139,7 +140,7 @@ var _ = ginkgo.Describe("devsy exec test suite", ginkgo.Label("exec"), ginkgo.Or gomega.Expect(stdout).To(gomega.ContainSubstring("/usr/local/bin")) }, ginkgo.SpecTimeout(framework.TimeoutShort())) - ginkgo.It("should skip env probe when --default-user-env-probe is none", + ginkgo.It("should skip env probe when --user-env-probe is none", func(ctx context.Context) { tempDir, f, err := setupWorkspaceAndUp(ctx, "tests/exec/testdata/envprobe", initialDir) framework.ExpectNoError(err) @@ -147,7 +148,7 @@ var _ = ginkgo.Describe("devsy exec test suite", ginkgo.Label("exec"), ginkgo.Or _, _, err = f.ExecCommandCapture(ctx, []string{ cmdWorkspace, execCommand, workspaceFolderFlag, tempDir, - "--default-user-env-probe", "none", + names.Flag(names.UserEnvProbe), "none", "--", echoCommand, "-n", "ok", }) framework.ExpectNoError(err) diff --git a/e2e/tests/feature/features_test_cmd.go b/e2e/tests/feature/features_test_cmd.go index b6f8cd1cd..069f284b6 100644 --- a/e2e/tests/feature/features_test_cmd.go +++ b/e2e/tests/feature/features_test_cmd.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -121,7 +122,7 @@ var _ = ginkgo.Describe("feature test", ginkgo.Label("feature"), func() { stdout, _, err := f.ExecCommandCapture(ctx, []string{ cmdFeatures, cmdTest, flagProjectFolder, projectDir, - "--features", featureNameA, + names.Flag(names.Features), featureNameA, }) framework.ExpectNoError(err) diff --git a/e2e/tests/ide/browser_returns.go b/e2e/tests/ide/browser_returns.go index 67d85778f..94f5c4140 100644 --- a/e2e/tests/ide/browser_returns.go +++ b/e2e/tests/ide/browser_returns.go @@ -15,6 +15,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" @@ -44,7 +45,7 @@ func setupBrowserIDE(ctx context.Context, initialDir string) (*framework.Framewo } // upBrowserIDE runs `devsy up --ide=openvscode --ide-launch=headless` against -// tempDir, optionally with extra args (e.g. "--recreate"). It returns the +// tempDir, optionally with extra args (e.g. names.Flag(names.Recreate)). It returns the // resolved workspace's tunnel state, which is asserted non-nil. func upBrowserIDE( ctx context.Context, f *framework.Framework, tempDir string, extraArgs ...string, @@ -172,7 +173,7 @@ var _ = ginkgo.Describe( // Recreate the workspace; the existing browser tunnel must be // killed before a new helper is spawned. - state2 := upBrowserIDE(ctx, f, tempDir, "--recreate") + state2 := upBrowserIDE(ctx, f, tempDir, names.Flag(names.Recreate)) pid2 := state2.PID gomega.Expect(pid2).NotTo(gomega.Equal(pid1), diff --git a/e2e/tests/ssh/ssh.go b/e2e/tests/ssh/ssh.go index 090532576..a5823330b 100644 --- a/e2e/tests/ssh/ssh.go +++ b/e2e/tests/ssh/ssh.go @@ -14,6 +14,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -108,7 +109,7 @@ var _ = ginkgo.Describe("devsy ssh test suite", ginkgo.Label("ssh"), ginkgo.Orde framework.ExpectNoError(err) ginkgo.GinkgoT().Setenv("GIT_CONFIG_GLOBAL", gitConfigPath) - err = f.DevsyUp(ctx, tempDir, "--gpg-agent-forwarding") + err = f.DevsyUp(ctx, tempDir, names.Flag(names.SSHGPGForwarding)) framework.ExpectNoError(err) devsySSHDeadline := time.Now().Add(20 * time.Second) diff --git a/e2e/tests/ssh/ssh_tunnel_mode_test.go b/e2e/tests/ssh/ssh_tunnel_mode_test.go index 07dbd8f49..09df6df4a 100644 --- a/e2e/tests/ssh/ssh_tunnel_mode_test.go +++ b/e2e/tests/ssh/ssh_tunnel_mode_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -27,7 +28,7 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) }) - ginkgo.It("should start workspace with --ssh-tunnel-mode and SSH into it", + ginkgo.It("should start workspace with --ssh-tunnel and SSH into it", ginkgo.SpecTimeout(framework.TimeoutModerate()), func(ctx context.Context) { if runtime.GOOS == osWindows { @@ -49,7 +50,7 @@ var _ = ginkgo.Describe( devsyUpCtx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute)) defer cancel() - err = f.DevsyUp(devsyUpCtx, tempDir, "--ssh-tunnel-mode") + err = f.DevsyUp(devsyUpCtx, tempDir, names.Flag(names.SSHTunnel)) framework.ExpectNoError(err) devsySSHCtx, cancelSSH := context.WithDeadline(ctx, time.Now().Add(20*time.Second)) @@ -87,7 +88,7 @@ var _ = ginkgo.Describe( err = f.DevsyUp( devsyUpCtx, tempDir, - "--ssh-tunnel-mode", + names.Flag(names.SSHTunnel), "--ssh-config", sshConfigPath, ) @@ -140,7 +141,7 @@ var _ = ginkgo.Describe( err = f.DevsyUp( devsyUpCtx, tempDir, - "--ssh-tunnel-mode", + names.Flag(names.SSHTunnel), "--ssh-config", sshConfigPath, ) @@ -191,7 +192,7 @@ var _ = ginkgo.Describe( devsyUpCtx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Minute)) defer cancel() - err = f.DevsyUp(devsyUpCtx, tempDir, "--ssh-tunnel-mode") + err = f.DevsyUp(devsyUpCtx, tempDir, names.Flag(names.SSHTunnel)) framework.ExpectNoError(err) for i := range 3 { diff --git a/e2e/tests/template/template.go b/e2e/tests/template/template.go index e073f2f60..db3ebc8f0 100644 --- a/e2e/tests/template/template.go +++ b/e2e/tests/template/template.go @@ -7,6 +7,7 @@ import ( "path/filepath" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" "github.com/tailscale/hujson" @@ -69,7 +70,7 @@ var _ = ginkgo.Describe("templates command", ginkgo.Label("templates"), func() { cmdTemplates, subCmdApply, flagTemplateID, testTemplateRef, flagWorkspaceFolder, tempDir, - "--features", "ghcr.io/devcontainers/features/node:1", + names.Flag(names.Features), "ghcr.io/devcontainers/features/node:1", }) framework.ExpectNoError(err) diff --git a/e2e/tests/up-docker-compose/build.go b/e2e/tests/up-docker-compose/build.go index c7b4a3f37..1dd5fa8b7 100644 --- a/e2e/tests/up-docker-compose/build.go +++ b/e2e/tests/up-docker-compose/build.go @@ -14,6 +14,7 @@ import ( "github.com/devsy-org/devsy/pkg/compose" pkgconfig "github.com/devsy-org/devsy/pkg/config" docker "github.com/devsy-org/devsy/pkg/docker" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -110,7 +111,7 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) ginkgo.By("Starting Devsy again with --recreate") - err = f.DevsyUp(ctx, tempDir, "--debug", "--recreate") + err = f.DevsyUp(ctx, tempDir, "--debug", names.Flag(names.Recreate)) framework.ExpectError(err) ginkgo.By("Should leave original container running") @@ -162,7 +163,7 @@ var _ = ginkgo.Describe( Should(gomega.Equal(1), "1 compose container to be created") ginkgo.By("Starting Devsy again with --recreate") - err = f.DevsyUp(ctx, tempDir, "--debug", "--recreate") + err = f.DevsyUp(ctx, tempDir, "--debug", names.Flag(names.Recreate)) framework.ExpectNoError(err) ginkgo.By("Should start a new docker-compose container on rebuild") diff --git a/e2e/tests/up/dockerfile_build.go b/e2e/tests/up/dockerfile_build.go index 985628eda..ef7d6614c 100644 --- a/e2e/tests/up/dockerfile_build.go +++ b/e2e/tests/up/dockerfile_build.go @@ -9,6 +9,7 @@ import ( "github.com/devsy-org/devsy/e2e/framework" pkgconfig "github.com/devsy-org/devsy/pkg/config" docker "github.com/devsy-org/devsy/pkg/docker" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -91,7 +92,7 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) ginkgo.By("Starting Devsy again with --recreate") - err = f.DevsyUp(ctx, tempDir, "--debug", "--recreate") + err = f.DevsyUp(ctx, tempDir, "--debug", names.Flag(names.Recreate)) framework.ExpectNoError(err) container, err = dockerHelper.FindDevContainer(ctx, []string{ @@ -138,7 +139,7 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) ginkgo.By("Starting Devsy again with --recreate") - err = f.DevsyUp(ctx, tempDir, "--debug", "--recreate") + err = f.DevsyUp(ctx, tempDir, "--debug", names.Flag(names.Recreate)) framework.ExpectNoError(err) container, err = dockerHelper.FindDevContainer(ctx, []string{ diff --git a/e2e/tests/up/provider_docker.go b/e2e/tests/up/provider_docker.go index e637a6173..654e02dde 100644 --- a/e2e/tests/up/provider_docker.go +++ b/e2e/tests/up/provider_docker.go @@ -12,6 +12,7 @@ import ( "github.com/devsy-org/devsy/e2e/framework" docker "github.com/devsy-org/devsy/pkg/docker" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/onsi/ginkgo/v2" @@ -305,7 +306,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "extra.json") - err = dtc.f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = dtc.f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := dtc.execSSH(ctx, tempDir, "bash -l -c 'echo -n $BASE_VAR'") @@ -329,7 +335,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "override.json") - err = dtc.f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = dtc.f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := dtc.execSSH(ctx, tempDir, "cat /tmp/test-var.out") @@ -695,7 +706,7 @@ var _ = ginkgo.Describe( ) framework.ExpectNoError(err) - err = dtc.f.DevsyUp(ctx, tempDir, "--devcontainer-id", "python") + err = dtc.f.DevsyUp(ctx, tempDir, names.Flag(names.DevContainer), "id:python") framework.ExpectNoError(err) out, err := dtc.execSSH(ctx, tempDir, "bash -l -c 'echo -n $DEVCONTAINER_TYPE'") @@ -705,7 +716,7 @@ var _ = ginkgo.Describe( err = dtc.f.DevsyWorkspaceDelete(ctx, tempDir) framework.ExpectNoError(err) - err = dtc.f.DevsyUp(ctx, tempDir, "--devcontainer-id", "go") + err = dtc.f.DevsyUp(ctx, tempDir, names.Flag(names.DevContainer), "id:go") framework.ExpectNoError(err) out, err = dtc.execSSH(ctx, tempDir, "bash -l -c 'echo -n $DEVCONTAINER_TYPE'") diff --git a/e2e/tests/up/provider_podman.go b/e2e/tests/up/provider_podman.go index b38c7c37f..27500a671 100644 --- a/e2e/tests/up/provider_podman.go +++ b/e2e/tests/up/provider_podman.go @@ -10,6 +10,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -543,7 +544,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "extra.json") - err = f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := f.DevsySSH(ctx, tempDir, "bash -l -c 'echo -n $BASE_VAR'") @@ -569,7 +575,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "override.json") - err = f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := f.DevsySSH(ctx, tempDir, "cat /tmp/test-var.out") @@ -590,7 +601,12 @@ var _ = ginkgo.Describe( ) framework.ExpectNoError(err) - err = f.DevsyUp(ctx, tempDir, "--devcontainer-id", "python") + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainer), + "id:python", + ) framework.ExpectNoError(err) out, err := f.DevsySSH( @@ -602,7 +618,7 @@ var _ = ginkgo.Describe( err = f.DevsyWorkspaceDelete(ctx, tempDir) framework.ExpectNoError(err) - err = f.DevsyUp(ctx, tempDir, "--devcontainer-id", "go") + err = f.DevsyUp(ctx, tempDir, names.Flag(names.DevContainer), "id:go") framework.ExpectNoError(err) out, err = f.DevsySSH( @@ -1177,7 +1193,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "extra.json") - err = f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := f.DevsySSH(ctx, tempDir, "bash -l -c 'echo -n $BASE_VAR'") @@ -1203,7 +1224,12 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err) extraPath := path.Join(tempDir, "override.json") - err = f.DevsyUp(ctx, tempDir, "--extra-devcontainer-path", extraPath) + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainerOverlay), + extraPath, + ) framework.ExpectNoError(err) out, err := f.DevsySSH(ctx, tempDir, "cat /tmp/test-var.out") @@ -1224,7 +1250,12 @@ var _ = ginkgo.Describe( ) framework.ExpectNoError(err) - err = f.DevsyUp(ctx, tempDir, "--devcontainer-id", "python") + err = f.DevsyUp( + ctx, + tempDir, + names.Flag(names.DevContainer), + "id:python", + ) framework.ExpectNoError(err) out, err := f.DevsySSH( @@ -1236,7 +1267,7 @@ var _ = ginkgo.Describe( err = f.DevsyWorkspaceDelete(ctx, tempDir) framework.ExpectNoError(err) - err = f.DevsyUp(ctx, tempDir, "--devcontainer-id", "go") + err = f.DevsyUp(ctx, tempDir, names.Flag(names.DevContainer), "id:go") framework.ExpectNoError(err) out, err = f.DevsySSH( diff --git a/e2e/tests/up/up_behaviors.go b/e2e/tests/up/up_behaviors.go index 2ab7422ce..8ad62e2a9 100644 --- a/e2e/tests/up/up_behaviors.go +++ b/e2e/tests/up/up_behaviors.go @@ -12,6 +12,7 @@ import ( "github.com/devsy-org/devsy/e2e/framework" "github.com/devsy-org/devsy/pkg/devcontainer/config" docker "github.com/devsy-org/devsy/pkg/docker" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) @@ -116,21 +117,21 @@ var _ = ginkgo.Describe("up command behaviors", ginkgo.Label("up-behaviors"), fu ginkgo.SpecTimeout(framework.TimeoutShort()), ) - ginkgo.It("should accept --update-remote-user-uid-default=on", func(ctx context.Context) { + ginkgo.It("should accept --update-remote-user-uid=on", func(ctx context.Context) { if runtime.GOOS != "linux" { ginkgo.Skip("updateRemoteUserUID only applies on Linux") } _, err := dtc.setupAndUp(ctx, "tests/up/testdata/docker", - "--update-remote-user-uid-default", "on") + names.Flag(names.UpdateRemoteUserUID), "on") framework.ExpectNoError(err) }, ginkgo.SpecTimeout(framework.TimeoutShort())) - ginkgo.It("should accept --update-remote-user-uid-default=off", func(ctx context.Context) { + ginkgo.It("should accept --update-remote-user-uid=off", func(ctx context.Context) { if runtime.GOOS != "linux" { ginkgo.Skip("updateRemoteUserUID only applies on Linux") } tempDir, err := dtc.setupAndUp(ctx, "tests/up/testdata/docker", - "--update-remote-user-uid-default", "off") + names.Flag(names.UpdateRemoteUserUID), "off") framework.ExpectNoError(err) out, err := dtc.execSSH(ctx, tempDir, "id -u") diff --git a/pkg/agent/delivery/workspace_seed.go b/pkg/agent/delivery/workspace_seed.go index 953a08ebf..8e1c9ffda 100644 --- a/pkg/agent/delivery/workspace_seed.go +++ b/pkg/agent/delivery/workspace_seed.go @@ -8,6 +8,7 @@ import ( pkgconfig "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" ) @@ -95,8 +96,9 @@ func (d *LocalDockerDelivery) prepareSeedTarget( if opts.Reset && managed { log.Warnf( - "--reset: removing workspace volume %s and re-seeding from source; "+ + "%s: removing workspace volume %s and re-seeding from source; "+ "any changes made inside the volume will be lost", + names.Flag(names.Reset), opts.VolumeName, ) if err := d.removeVolume(ctx, opts.VolumeName); err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index b00fd9a2d..ffb0dfb88 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -137,6 +137,11 @@ func (c *Config) ContextOption(option string) string { return "" } +// ContextOptionBool reports whether the given context option is set to true. +func (c *Config) ContextOptionBool(option string) bool { + return c.ContextOption(option) == BoolTrue +} + func (c *ContextConfig) IsSingleMachine(provider string) bool { if c.Providers == nil || c.Providers[provider] == nil { return false diff --git a/pkg/daemon/agent/daemon.go b/pkg/daemon/agent/daemon.go index 4645f5cf3..95da451d8 100644 --- a/pkg/daemon/agent/daemon.go +++ b/pkg/daemon/agent/daemon.go @@ -15,6 +15,7 @@ import ( "github.com/devsy-org/devsy/pkg/command" pkgconfig "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" ) @@ -193,13 +194,13 @@ func InstallDaemon(agentDir, interval, shutdownAction string) error { func buildDaemonArgs(executable, agentDir, interval, shutdownAction string) []string { args := []string{executable, "internal", "agent", "daemon"} if agentDir != "" { - args = append(args, "--agent-dir", agentDir) + args = append(args, names.Flag(names.AgentDir), agentDir) } if interval != "" { - args = append(args, "--interval", interval) + args = append(args, names.Flag(names.Interval), interval) } if shutdownAction != "" { - args = append(args, "--shutdown-action", shutdownAction) + args = append(args, names.Flag(names.ShutdownAction), shutdownAction) } return args } diff --git a/pkg/devcontainer/compose.go b/pkg/devcontainer/compose.go index 26d170c67..9dc768ec8 100644 --- a/pkg/devcontainer/compose.go +++ b/pkg/devcontainer/compose.go @@ -14,6 +14,7 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/metadata" "github.com/devsy-org/devsy/pkg/driver" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/joho/godotenv" ) @@ -871,7 +872,11 @@ func (r *runner) recreateDevContainer( ctx context.Context, container *config.ContainerDetails, ) error { - log.Debugf("Deleting dev container %s due to --recreate", container.ID) + log.Debugf( + "Deleting dev container %s due to %s", + container.ID, + names.Flag(names.Recreate), + ) if err := r.driver.StopDevContainer(ctx, r.id); err != nil { return fmt.Errorf("stop dev container: %w", err) diff --git a/pkg/devcontainer/config.go b/pkg/devcontainer/config.go index 4c22230e3..9165217fa 100644 --- a/pkg/devcontainer/config.go +++ b/pkg/devcontainer/config.go @@ -13,6 +13,7 @@ import ( pkgconfig "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/devcontainer/config" "github.com/devsy-org/devsy/pkg/devcontainer/crane" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/language" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/provider" @@ -357,7 +358,7 @@ func mergeAdditionalFeatures(parsedConfig *config.DevContainerConfig, raw string additionalFeatures := make(map[string]any) if err := json.Unmarshal([]byte(raw), &additionalFeatures); err != nil { - return fmt.Errorf("parse --additional-features JSON: %w", err) + return fmt.Errorf("parse %s JSON: %w", names.Flag(names.Features), err) } if parsedConfig.Features == nil { diff --git a/pkg/devcontainer/config/build.go b/pkg/devcontainer/config/build.go index b5f2897ee..30bb952f8 100644 --- a/pkg/devcontainer/config/build.go +++ b/pkg/devcontainer/config/build.go @@ -6,6 +6,7 @@ import ( pkgconfig "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/dockerfile" + "github.com/devsy-org/devsy/pkg/flags/names" ) const ( @@ -29,7 +30,11 @@ func ValidateIDLabels(labels []string) error { for _, label := range labels { k, _, ok := strings.Cut(label, "=") if !ok || k == "" { - return fmt.Errorf("invalid --id-label %q: must be in key=value format", label) + return fmt.Errorf( + "invalid %s %q: must be in key=value format", + names.Flag(names.IDLabel), + label, + ) } } return nil diff --git a/pkg/devcontainer/config/result.go b/pkg/devcontainer/config/result.go index e52fe8b1d..4263f4d17 100644 --- a/pkg/devcontainer/config/result.go +++ b/pkg/devcontainer/config/result.go @@ -61,6 +61,14 @@ func GetMounts(result *Result) []*Mount { return mounts } +// GetContainerID returns the container ID from a result, or "" when absent. +func GetContainerID(result *Result) string { + if result != nil && result.ContainerDetails != nil { + return result.ContainerDetails.ID + } + return "" +} + // GetRemoteUser determines the remote user using DevContainer specification priority order: // 1. remoteUser from configuration // 2. devsy.user label from container diff --git a/pkg/devcontainer/config_test.go b/pkg/devcontainer/config_test.go index a84b0bf83..8671d34ec 100644 --- a/pkg/devcontainer/config_test.go +++ b/pkg/devcontainer/config_test.go @@ -233,7 +233,7 @@ func (s *SubstituteTestSuite) TestSubstitute_AdditionalFeaturesInvalidJSON() { _, _, err := s.runner.substitute(options, rawConfig) s.Error(err) - s.Contains(err.Error(), "--additional-features") + s.Contains(err.Error(), "--features") } func (s *SubstituteTestSuite) TestSubstitute_AdditionalFeaturesEmpty() { diff --git a/pkg/devcontainer/setup.go b/pkg/devcontainer/setup.go index 361058d44..38fd71243 100644 --- a/pkg/devcontainer/setup.go +++ b/pkg/devcontainer/setup.go @@ -22,6 +22,7 @@ import ( "github.com/devsy-org/devsy/pkg/devcontainer/sshtunnel" "github.com/devsy-org/devsy/pkg/docker" "github.com/devsy-org/devsy/pkg/driver" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide" "github.com/devsy-org/devsy/pkg/log" provider2 "github.com/devsy-org/devsy/pkg/provider" @@ -315,9 +316,16 @@ func (r *runner) buildSetupCommand(compressed, workspaceConfigCompressed string) log.Infof("setting up container") args := []string{ shellescape.Quote(pkgconfig.ContainerDevsyHelperLocation), - "internal", "agent", "container", "setup", - "--setup-info", shellescape.Quote(compressed), - "--container-workspace-info", shellescape.Quote(workspaceConfigCompressed), + "internal", + "agent", + "container", + "setup", + names.Flag(names.SetupInfo), + shellescape.Quote(compressed), + names.Flag( + names.ContainerWorkspaceInfo, + ), + shellescape.Quote(workspaceConfigCompressed), } r.addSetupFlags(&args) @@ -337,7 +345,7 @@ func (r *runner) addSetupFlags(args *[]string) { func (r *runner) addChownFlag(args *[]string, isDockerDriver bool) { if shouldChownWorkspace(runtime.GOOS, isDockerDriver, r.isPodmanRuntime()) { - *args = append(*args, "--chown-workspace") + *args = append(*args, names.Flag(names.ChownWorkspace)) } } @@ -357,45 +365,65 @@ func (r *runner) isPodmanRuntime() bool { func (r *runner) addDriverFlags(args *[]string, isDockerDriver bool) { if !isDockerDriver { - *args = append(*args, "--stream-mounts") + *args = append(*args, names.Flag(names.StreamMounts)) } if r.workspaceConfig.Agent.InjectGitCredentials != stringFalse { - *args = append(*args, "--inject-git-credentials") + *args = append(*args, names.Flag(names.InjectGitCredentials)) } } func (r *runner) addPlatformFlags(args *[]string) { platform := r.workspaceConfig.CLIOptions.Platform if platform.AccessKey != "" { - *args = append(*args, "--access-key", shellescape.Quote(platform.AccessKey)) + *args = append( + *args, + names.Flag(names.AccessKey), + shellescape.Quote(platform.AccessKey), + ) } if platform.WorkspaceHost != "" { - *args = append(*args, "--workspace-host", shellescape.Quote(platform.WorkspaceHost)) + *args = append( + *args, + names.Flag(names.WorkspaceHost), + shellescape.Quote(platform.WorkspaceHost), + ) } if platform.PlatformHost != "" { - *args = append(*args, "--platform-host", shellescape.Quote(platform.PlatformHost)) + *args = append( + *args, + names.Flag(names.PlatformHost), + shellescape.Quote(platform.PlatformHost), + ) } } func (r *runner) addDotfilesFlags(args *[]string) { cli := r.workspaceConfig.CLIOptions if cli.DotfilesRepo != "" { - *args = append(*args, "--dotfiles-repo", shellescape.Quote(cli.DotfilesRepo)) + *args = append( + *args, + names.Flag(names.DotfilesRepo), + shellescape.Quote(cli.DotfilesRepo), + ) } if cli.DotfilesScript != "" { - *args = append(*args, "--dotfiles-script", shellescape.Quote(cli.DotfilesScript)) + *args = append( + *args, + names.Flag(names.DotfilesScript), + shellescape.Quote(cli.DotfilesScript), + ) } } func (r *runner) addPrebuildFlag(args *[]string) { if r.workspaceConfig.CLIOptions.Prebuild { - *args = append(*args, "--prebuild") + *args = append(*args, names.Flag(names.Prebuild)) } } func (r *runner) addDebugFlag(args *[]string) { if r.isDebugMode() { - *args = append(*args, "--debug") + *args = append(*args, names.Flag(names.Debug)) } } @@ -451,18 +479,18 @@ func (r *runner) executeSetup( func (r *runner) buildSSHTunnelCommand() string { args := []string{ shellescape.Quote(pkgconfig.ContainerDevsyHelperLocation), - "internal", "ssh-server", "--stdio", + "internal", "ssh-server", names.Flag(names.Stdio), } if ide.ReusesAuthSock(r.workspaceConfig.Workspace.IDE.Name) { args = append( args, - "--reuse-ssh-auth-sock", + names.Flag(names.ReuseSSHAuthSock), shellescape.Quote(r.workspaceConfig.CLIOptions.SSHAuthSockID), ) } if r.isDebugMode() { - args = append(args, "--debug") + args = append(args, names.Flag(names.Debug)) } return strings.Join(args, " ") } diff --git a/pkg/devcontainer/source.go b/pkg/devcontainer/source.go index d227fa9e1..4f2efdbf1 100644 --- a/pkg/devcontainer/source.go +++ b/pkg/devcontainer/source.go @@ -3,11 +3,15 @@ package devcontainer import ( "fmt" "strings" + + "github.com/devsy-org/devsy/pkg/flags/names" ) type SourceSpec struct { Kind SourceKind Image string + ID string + Path string } type SourceKind string @@ -15,9 +19,14 @@ type SourceKind string const ( SourceNone SourceKind = "none" SourceImage SourceKind = "image" + SourceID SourceKind = "id" + SourcePath SourceKind = "path" ) -const sourceImagePrefix = "image:" +const ( + sourceImagePrefix = "image:" + sourceIDPrefix = "id:" +) func ParseSourceSpec(value string) (*SourceSpec, error) { value = strings.TrimSpace(value) @@ -30,16 +39,25 @@ func ParseSourceSpec(value string) (*SourceSpec, error) { image := strings.TrimSpace(strings.TrimPrefix(value, sourceImagePrefix)) if image == "" { return nil, fmt.Errorf( - "--devcontainer %q: missing image reference after %q", + "%s %q: missing image reference after %q", + names.Flag(names.DevContainer), value, sourceImagePrefix, ) } return &SourceSpec{Kind: SourceImage, Image: image}, nil + case strings.HasPrefix(value, sourceIDPrefix): + id := strings.TrimSpace(strings.TrimPrefix(value, sourceIDPrefix)) + if id == "" { + return nil, fmt.Errorf( + "%s %q: missing devcontainer id after %q", + names.Flag(names.DevContainer), + value, + sourceIDPrefix, + ) + } + return &SourceSpec{Kind: SourceID, ID: id}, nil default: - return nil, fmt.Errorf( - "--devcontainer %q: unsupported value, expected \"none\" or \"image:\"", - value, - ) + return &SourceSpec{Kind: SourcePath, Path: value}, nil } } diff --git a/pkg/devcontainer/source_test.go b/pkg/devcontainer/source_test.go index 682a76472..87baecba5 100644 --- a/pkg/devcontainer/source_test.go +++ b/pkg/devcontainer/source_test.go @@ -13,6 +13,8 @@ func TestParseSourceSpec(t *testing.T) { in string wantKind SourceKind wantImage string + wantID string + wantPath string wantNil bool wantErr bool }{ @@ -26,9 +28,16 @@ func TestParseSourceSpec(t *testing.T) { wantImage: "mcr.microsoft.com/devcontainers/python:3", }, {in: "image: python ", wantKind: SourceImage, wantImage: "python"}, + {in: "id:default", wantKind: SourceID, wantID: "default"}, + {in: "id: claude ", wantKind: SourceID, wantID: "claude"}, + { + in: ".devcontainer/custom.json", + wantKind: SourcePath, + wantPath: ".devcontainer/custom.json", + }, + {in: "/abs/path.json", wantKind: SourcePath, wantPath: "/abs/path.json"}, {in: "image:", wantErr: true}, - {in: "id:default", wantErr: true}, - {in: "bogus", wantErr: true}, + {in: "id:", wantErr: true}, } for _, c := range cases { spec, err := ParseSourceSpec(c.in) @@ -43,5 +52,7 @@ func TestParseSourceSpec(t *testing.T) { } assert.Equal(t, c.wantKind, spec.Kind, "input %q kind", c.in) assert.Equal(t, c.wantImage, spec.Image, "input %q image", c.in) + assert.Equal(t, c.wantID, spec.ID, "input %q id", c.in) + assert.Equal(t, c.wantPath, spec.Path, "input %q path", c.in) } } diff --git a/pkg/dotfiles/dotfiles.go b/pkg/dotfiles/dotfiles.go index 40e7680c9..b7e2e428d 100644 --- a/pkg/dotfiles/dotfiles.go +++ b/pkg/dotfiles/dotfiles.go @@ -9,6 +9,7 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" devssh "github.com/devsy-org/devsy/pkg/ssh" ) @@ -62,7 +63,7 @@ func Setup(p SetupParams) error { return err } if log.DebugEnabled() { - dotCmd.Args = append(dotCmd.Args, "--debug") + dotCmd.Args = append(dotCmd.Args, names.Flag(names.Debug)) } log.Debugf("Running dotfiles setup command: %v", dotCmd.Args) @@ -91,20 +92,24 @@ func buildDotCmdAgentArguments( cmdAgent, cmdWorkspace, cmdInstallDotfs, - "--repository", + names.Flag(names.Repository), dotfilesRepo, } if strictHostKey { - agentArguments = append(agentArguments, "--strict-host-key-checking") + agentArguments = append(agentArguments, names.Flag(names.StrictHostKeyChecking)) } if debug { - agentArguments = append(agentArguments, "--debug") + agentArguments = append(agentArguments, names.Flag(names.Debug)) } if dotfilesScript != "" { - agentArguments = append(agentArguments, "--install-script", dotfilesScript) + agentArguments = append( + agentArguments, + names.Flag(names.InstallScript), + dotfilesScript, + ) } return agentArguments @@ -123,8 +128,8 @@ func buildDotCmd(p buildDotCmdParams) (*exec.Cmd, error) { sshCmd := []string{ "workspace", "ssh", - "--agent-forwarding=true", - "--start-services=true", + names.FlagTrue(names.AgentForwarding), + names.FlagTrue(names.StartServices), } envFilesKeyValuePairs, err := collectDotfilesScriptEnvKeyValuePairs(p.envFiles) @@ -137,7 +142,7 @@ func buildDotCmd(p buildDotCmdParams) (*exec.Cmd, error) { allEnvKeyValuesPairs := slices.Concat(envFilesKeyValuePairs, p.envKeyValuePairs) allEnvKeys := extractKeysFromEnvKeyValuePairs(allEnvKeyValuesPairs) for _, envKey := range allEnvKeys { - sshCmd = append(sshCmd, "--send-env", envKey) + sshCmd = append(sshCmd, names.Flag(names.SendEnv), envKey) } remoteUser, err := devssh.GetUser( @@ -162,13 +167,13 @@ func buildDotCmd(p buildDotCmdParams) (*exec.Cmd, error) { } sshCmd = append(sshCmd, - "--user", + names.Flag(names.User), remoteUser, - "--context", + names.Flag(names.Context), p.client.Context(), p.client.Workspace(), - "--log-output=raw", - "--command", + names.FlagValue(names.LogOutput, "raw"), + names.Flag(names.Command), config.ContainerDevsyHelperLocation+" "+strings.Join(agentArguments, " "), ) execPath, err := os.Executable() diff --git a/pkg/flags/builder.go b/pkg/flags/builder.go new file mode 100644 index 000000000..1933c7435 --- /dev/null +++ b/pkg/flags/builder.go @@ -0,0 +1,232 @@ +package flags + +import ( + "time" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" +) + +// Flag is a self-contained flag definition: its name, default, usage text, and +// the destination it binds to. Definitions are attached to a command with Add, +// which keeps registration uniform and lets shared flags be declared once. +type Flag interface { + register(fs *flag.FlagSet) +} + +// Add registers the given flag definitions on the command's local flags. +func Add(cmd *cobra.Command, defs ...Flag) { + fs := cmd.Flags() + for _, d := range defs { + d.register(fs) + } +} + +// AddPersistent registers the given flag definitions on the command's +// persistent flags (inherited by subcommands). +func AddPersistent(cmd *cobra.Command, defs ...Flag) { + fs := cmd.PersistentFlags() + for _, d := range defs { + d.register(fs) + } +} + +// options are the settings common to every flag kind, tweaked via With* chaining. +type options struct { + shorthand string + hidden bool +} + +func (o options) applyMeta(fs *flag.FlagSet, name string) { + if o.hidden { + _ = fs.MarkHidden(name) + } +} + +// StringFlag defines a string flag. +type StringFlag struct { + dest *string + name string + def string + usage string + options +} + +// String binds a string flag to dest. +func String(dest *string, name, def, usage string) *StringFlag { + return &StringFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Shorthand sets a single-letter shorthand (e.g. "L" for -L). +func (f *StringFlag) Shorthand(s string) *StringFlag { f.shorthand = s; return f } + +// Hidden marks the flag hidden from help output. +func (f *StringFlag) Hidden() *StringFlag { f.hidden = true; return f } + +func (f *StringFlag) register(fs *flag.FlagSet) { + fs.StringVarP(f.dest, f.name, f.shorthand, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// BoolFlag defines a bool flag. +type BoolFlag struct { + dest *bool + name string + def bool + usage string + options +} + +// Bool binds a bool flag to dest. +func Bool(dest *bool, name string, def bool, usage string) *BoolFlag { + return &BoolFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Shorthand sets a single-letter shorthand. +func (f *BoolFlag) Shorthand(s string) *BoolFlag { f.shorthand = s; return f } + +// Hidden marks the flag hidden from help output. +func (f *BoolFlag) Hidden() *BoolFlag { f.hidden = true; return f } + +func (f *BoolFlag) register(fs *flag.FlagSet) { + fs.BoolVarP(f.dest, f.name, f.shorthand, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// IntFlag defines an int flag. +type IntFlag struct { + dest *int + name string + def int + usage string + options +} + +// Int binds an int flag to dest. +func Int(dest *int, name string, def int, usage string) *IntFlag { + return &IntFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Hidden marks the flag hidden from help output. +func (f *IntFlag) Hidden() *IntFlag { f.hidden = true; return f } + +func (f *IntFlag) register(fs *flag.FlagSet) { + fs.IntVar(f.dest, f.name, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// Int64Flag defines an int64 flag. +type Int64Flag struct { + dest *int64 + name string + def int64 + usage string + options +} + +// Int64 binds an int64 flag to dest. +func Int64(dest *int64, name string, def int64, usage string) *Int64Flag { + return &Int64Flag{dest: dest, name: name, def: def, usage: usage} +} + +// Hidden marks the flag hidden from help output. +func (f *Int64Flag) Hidden() *Int64Flag { f.hidden = true; return f } + +func (f *Int64Flag) register(fs *flag.FlagSet) { + fs.Int64Var(f.dest, f.name, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// StringSliceFlag defines a comma-separated / repeatable string slice flag. +type StringSliceFlag struct { + dest *[]string + name string + def []string + usage string + options +} + +// StringSlice binds a comma-separated string slice flag to dest. +func StringSlice(dest *[]string, name string, def []string, usage string) *StringSliceFlag { + return &StringSliceFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Shorthand sets a single-letter shorthand. +func (f *StringSliceFlag) Shorthand(s string) *StringSliceFlag { f.shorthand = s; return f } + +// Hidden marks the flag hidden from help output. +func (f *StringSliceFlag) Hidden() *StringSliceFlag { f.hidden = true; return f } + +func (f *StringSliceFlag) register(fs *flag.FlagSet) { + fs.StringSliceVarP(f.dest, f.name, f.shorthand, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// StringArrayFlag defines a repeatable (not comma-split) string array flag. +type StringArrayFlag struct { + dest *[]string + name string + def []string + usage string + options +} + +// StringArray binds a repeatable string array flag to dest (values are not +// comma-split, unlike StringSlice). +func StringArray(dest *[]string, name string, def []string, usage string) *StringArrayFlag { + return &StringArrayFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Shorthand sets a single-letter shorthand. +func (f *StringArrayFlag) Shorthand(s string) *StringArrayFlag { f.shorthand = s; return f } + +// Hidden marks the flag hidden from help output. +func (f *StringArrayFlag) Hidden() *StringArrayFlag { f.hidden = true; return f } + +func (f *StringArrayFlag) register(fs *flag.FlagSet) { + fs.StringArrayVarP(f.dest, f.name, f.shorthand, f.def, f.usage) + f.applyMeta(fs, f.name) +} + +// ValueFlag defines a flag backed by a custom pflag.Value implementation. +type ValueFlag struct { + value flag.Value + name string + usage string + options +} + +// Value binds a custom pflag.Value flag. +func Value(value flag.Value, name, usage string) *ValueFlag { + return &ValueFlag{value: value, name: name, usage: usage} +} + +// Hidden marks the flag hidden from help output. +func (f *ValueFlag) Hidden() *ValueFlag { f.hidden = true; return f } + +func (f *ValueFlag) register(fs *flag.FlagSet) { + fs.Var(f.value, f.name, f.usage) + f.applyMeta(fs, f.name) +} + +// DurationFlag defines a time.Duration flag. +type DurationFlag struct { + dest *time.Duration + name string + def time.Duration + usage string + options +} + +// Duration binds a time.Duration flag to dest. +func Duration(dest *time.Duration, name string, def time.Duration, usage string) *DurationFlag { + return &DurationFlag{dest: dest, name: name, def: def, usage: usage} +} + +// Hidden marks the flag hidden from help output. +func (f *DurationFlag) Hidden() *DurationFlag { f.hidden = true; return f } + +func (f *DurationFlag) register(fs *flag.FlagSet) { + fs.DurationVar(f.dest, f.name, f.def, f.usage) + f.applyMeta(fs, f.name) +} diff --git a/pkg/flags/devcontainer.go b/pkg/flags/devcontainer.go new file mode 100644 index 000000000..e692e9658 --- /dev/null +++ b/pkg/flags/devcontainer.go @@ -0,0 +1,43 @@ +package flags + +import ( + "github.com/devsy-org/devsy/pkg/flags/names" + flag "github.com/spf13/pflag" +) + +// DevContainerModifierFlags holds the destinations a command binds the shared +// devcontainer modifier flags to. A nil pointer skips that flag, letting each +// command opt into only the modifiers it supports. +type DevContainerModifierFlags struct { + Image *string + Features *string + UserEnvProbe *string + IDLabels *[]string + ContainerDataFolder *string +} + +// RegisterDevContainerModifierFlags registers the devcontainer modifier flags +// shared across the up/build/exec commands so their names and descriptions +// stay in sync. +func RegisterDevContainerModifierFlags(fs *flag.FlagSet, opts DevContainerModifierFlags) { + if opts.Image != nil { + fs.StringVar(opts.Image, names.DevContainerImage, "", + "Override the image in the resolved devcontainer config") + } + if opts.Features != nil { + fs.StringVar(opts.Features, names.Features, "", + `Extra features to add (JSON, as in the "features" section of devcontainer.json)`) + } + if opts.UserEnvProbe != nil { + fs.StringVar(opts.UserEnvProbe, names.UserEnvProbe, "", + "Override userEnvProbe (loginInteractiveShell, loginShell, interactiveShell, none)") + } + if opts.IDLabels != nil { + fs.StringArrayVar(opts.IDLabels, names.IDLabel, nil, + "Container identity label (key=value, repeatable)") + } + if opts.ContainerDataFolder != nil { + fs.StringVar(opts.ContainerDataFolder, names.ContainerDataFolder, "", + "Path for container-specific data") + } +} diff --git a/pkg/flags/names/names.go b/pkg/flags/names/names.go new file mode 100644 index 000000000..22d3ce6f0 --- /dev/null +++ b/pkg/flags/names/names.go @@ -0,0 +1,350 @@ +// Package names is the single source of truth for devsy CLI flag names. +// +// Flags are registered in cmd/ but many are also used to re-invoke devsy from +// pkg/ (e.g. building a "devsy workspace ssh ..." command line). This package +// is a dependency-light leaf (imports only fmt) so both the cobra-coupled +// pkg/flags builder and the pkg/ command builders can import it without +// pulling in cobra. Defining each name once means a rename is a single edit the +// compiler propagates to every registration and construction site. +// +// Names are the bare flag name (no leading "--"). Use Flag or FlagTrue/FlagFalse +// to build a command-line token. +// +// Every name here is a devsy-owned CLI flag and can be renamed by editing its +// value once. Some names deliberately mirror an external tool or spec — docker +// build (--pull, --no-cache, --platform), podman (--userns, --uidmap, --gidmap), +// git (--git-recurse-submodules), or the devcontainer.json schema (--features, +// --user-env-probe, --update-remote-user-uid, --id-label). Renaming those is a +// UX/compatibility decision, not a free change. (The literal flags passed +// through to docker/podman themselves live at their construction sites, e.g. +// pkg/driver/docker, not here.) +package names + +import "fmt" + +// Flag renders a flag name as a command-line token, e.g. Flag(SSHGPGForwarding) +// -> "--ssh-gpg-forwarding". +func Flag(name string) string { return "--" + name } + +// FlagValue renders "--name=value". +func FlagValue(name, value string) string { return fmt.Sprintf("--%s=%s", name, value) } + +// FlagTrue renders "--name=true". +func FlagTrue(name string) string { return FlagValue(name, "true") } + +// FlagFalse renders "--name=false". +func FlagFalse(name string) string { return FlagValue(name, "false") } + +// Workspace up / build / exec — devcontainer source & modifiers. +const ( + DevContainer = "devcontainer" + DevContainerImage = "devcontainer-image" + DevContainerPath = "devcontainer-path" + DevContainerID = "devcontainer-id" + DevContainerOverlay = "devcontainer-overlay" + FallbackImage = "fallback-image" + Features = "features" + IDLabel = "id-label" + UserEnvProbe = "user-env-probe" + GPUAvailability = "gpu-availability" + UpdateRemoteUserUID = "update-remote-user-uid" + ContainerDataFolder = "container-data-folder" + MountWorkspaceGitRoot = "mount-workspace-git-root" + ContainerUser = "container-user" + RemoteUser = "remote-user" +) + +// SSH. +const ( + SSHConfigure = "ssh-configure" + SSHGPGForwarding = "ssh-gpg-forwarding" + SSHConfig = "ssh-config" + SSHTunnel = "ssh-tunnel" + AgentForwarding = "agent-forwarding" + ReuseSSHAuthSock = "reuse-ssh-auth-sock" + Stdio = "stdio" + StartServices = "start-services" + SSHKeepAliveInterval = "ssh-keepalive-interval" + ForwardPorts = "forward-ports" + ReverseForwardPorts = "reverse-forward-ports" + SendEnv = "send-env" + SetEnv = "set-env" + ForwardPortsTimeout = "forward-ports-timeout" + TermMode = "term-mode" + InstallTerminfo = "install-terminfo" + TrackActivity = "track-activity" +) + +// Dotfiles. +const ( + Dotfiles = "dotfiles" + DotfilesScript = "dotfiles-script" + DotfilesTargetPath = "dotfiles-target-path" + DotfilesScriptEnv = "dotfiles-script-env" + DotfilesScriptEnvFile = "dotfiles-script-env-file" + DotfilesRepo = "dotfiles-repo" +) + +// Git. +const ( + GitCloneStrategy = "git-clone-strategy" + GitRecurseSubmodules = "git-recurse-submodules" + GitLFSMode = "git-lfs-mode" + GitSSHSigningKey = "git-ssh-signing-key" +) + +// Lifecycle. +const ( + SkipPostCreate = "skip-post-create" + SkipPostStart = "skip-post-start" + SkipPostAttach = "skip-post-attach" + SkipNonBlockingCommands = "skip-non-blocking-commands" + SkipHostRequirements = "skip-host-requirements" + TerminalColumns = "terminal-columns" + TerminalRows = "terminal-rows" +) + +// Workspace behavior & identity. +const ( + ID = "id" + Machine = "machine" + Source = "source" + ProviderOption = "provider-option" + Reconfigure = "reconfigure" + Prebuild = "prebuild" + Pull = "pull" + NoCache = "no-cache" + Recreate = "recreate" + Reset = "reset" + PrebuildRepo = "prebuild-repo" + WorkspaceEnv = "workspace-env" + WorkspaceEnvFile = "workspace-env-file" + SecretsFile = "secrets-file" + FeatureSecretsFile = "feature-secrets-file" + InitEnv = "init-env" + DisableDaemon = "disable-daemon" + CacheFrom = "cache-from" + WorkspaceMountConsistency = "workspace-mount-consistency" + Mount = "mount" + Platform = "platform" + PullFromInsideContainer = "pull-from-inside-container" + WorkspaceFolder = "workspace-folder" + IgnoreNotFound = "ignore-not-found" + GracePeriod = "grace-period" + Force = "force" + RemoveVolumes = "remove-volumes" +) + +// IDE. +const ( + IDE = "ide" + IDEOption = "ide-option" + IDELaunch = "ide-launch" + Option = "option" +) + +// Podman. +const ( + Userns = "userns" + UIDMap = "uidmap" + GIDMap = "gidmap" +) + +// Exec. +const ( + ContainerID = "container-id" + DockerPath = "docker-path" + RemoteEnv = "remote-env" +) + +// Agent container setup (internal self-invocation). +const ( + ContainerWorkspaceInfo = "container-workspace-info" + ChownWorkspace = "chown-workspace" + StreamMounts = "stream-mounts" + InjectGitCredentials = "inject-git-credentials" //nolint:gosec // G101: flag name, not a credential + AccessKey = "access-key" + WorkspaceHost = "workspace-host" + PlatformHost = "platform-host" +) + +// Dotfiles install (internal self-invocation). +const ( + Repository = "repository" + StrictHostKeyChecking = "strict-host-key-checking" + InstallScript = "install-script" +) + +// Workspace list / status / describe / import. +const ( + SkipPro = "skip-pro" + ContainerStatus = "container-status" + WorkspaceID = "workspace-id" + MachineID = "machine-id" + MachineReuse = "machine-reuse" + ProviderID = "provider-id" + ProviderReuse = "provider-reuse" + Data = "data" +) + +// Build command. +const ( + SkipDelete = "skip-delete" + Tag = "tag" + SkipPush = "skip-push" + Push = "push" + Label = "label" + Output = "output" + ExperimentalLockfile = "experimental-lockfile" + ImageName = "image-name" + NoBuild = "no-build" + ForceBuild = "force-build" + ForceInternalBuildKit = "force-internal-buildkit" +) + +// Testing-only (hidden). +const ( + DaemonInterval = "daemon-interval" + ForceDockerless = "force-dockerless" +) + +// Common / global. +const ( + Debug = "debug" + Command = "command" + Context = "context" + User = "user" + Workdir = "workdir" + Home = "home" + LogOutput = "log-output" + LogFormat = "log-format" + ResultFormat = "result-format" + SetupInfo = "setup-info" + SecretsEnv = "secrets-env" + OwnerTrust = "ownertrust" + SocketPath = "socketpath" + GitKey = "gitkey" + ShutdownAction = "shutdown-action" + Timeout = "timeout" + Flavor = "flavor" + Verbose = "verbose" + Quiet = "quiet" + Owner = "owner" + AgentDir = "agent-dir" + UID = "uid" + OpenBrowser = "open-browser" + InheritListener = "inherit-listener" + Config = "config" + JSON = "json" + Name = "name" + DryRun = "dry-run" +) + +// Feature / template commands. +const ( + BaseImage = "base-image" + ForceCleanOutputFolder = "force-clean-output-folder" + GitHubOwner = "github-owner" + GitHubRepo = "github-repo" + IncludeFeaturesConfiguration = "include-features-configuration" + IncludeMergedConfiguration = "include-merged-configuration" + OmitPaths = "omit-paths" + OutputFolder = "output-folder" + OverrideConfig = "override-config" + PreserveTestContainers = "preserve-test-containers" + ProjectFolder = "project-folder" + Registry = "registry" + ShowDependencies = "show-dependencies" + ShowTags = "show-tags" + SkipScenarios = "skip-scenarios" + Target = "target" + TemplateArgs = "template-args" + TemplateID = "template-id" +) + +// Provider command. +const ( + Available = "available" + Create = "create" + FromExisting = "from-existing" + Prerelease = "prerelease" + ProviderSource = "provider-source" + Use = "use" + Version = "version" +) + +// Pro / platform commands. +const ( + Channel = "channel" + ChartPath = "chart-path" + ChartRepo = "chart-repo" + DisplayName = "display-name" + Email = "email" + Header = "header" + HelmChartPath = "helm-chart-path" + HelmChartVersion = "helm-chart-version" + HelmSet = "helm-set" + HelmValues = "helm-values" + Host = "host" + Insecure = "insecure" + Instance = "instance" + KubeContext = "kube-context" + Login = "login" + NoLogin = "no-login" + NoTunnel = "no-tunnel" + NoWait = "no-wait" + Own = "own" + Password = "password" + PreventWakeup = "prevent-wakeup" + Project = "project" + Provider = "provider" + ReuseValues = "reuse-values" + ServiceAccount = "service-account" + Token = "token" + Upgrade = "upgrade" + Values = "values" + Wait = "wait" +) + +// Internal / agent commands (self-invocation, daemons, tunnels, servers). +const ( + Address = "address" + AuthSockID = "auth-sock-id" + ConfigureDockerHelper = "configure-docker-helper" + ConfigureGitHelper = "configure-git-helper" + Container = "container" + Daemon = "daemon" + Docker = "docker" + DockerArg = "docker-arg" + DockerCommand = "docker-command" + DockerImage = "docker-image" + Dry = "dry" + ExecOutputCap = "exec-output-cap" + ExecTimeoutDefault = "exec-timeout-default" + ExecTimeoutMax = "exec-timeout-max" + ExtraPorts = "extra-ports" + Fail = "fail" + FailOnErrorCode = "fail-on-error-code" + File = "file" + FilterByOwner = "filter-by-owner" + FleetWorkspaceID = "workspaceid" + ForceBrowser = "force-browser" + GitUserSigningKey = "git-user-signing-key" + HelperImage = "helper-image" + Hidden = "hidden" + Interval = "interval" + KeyFile = "key-file" + MaxDepth = "max-depth" + Namespace = "namespace" + Port = "port" + Request = "request" + SingleMachine = "single-machine" + SkipInit = "skip-init" + SkipOnCreate = "skip-on-create" + SkipUpdateContent = "skip-update-content" + TargetURL = "target-url" + Workspace = "workspace" + WorkspaceInfo = "workspace-info" + WorkspaceProject = "workspace-project" + WorkspaceUID = "workspace-uid" +) diff --git a/pkg/flags/names/names_test.go b/pkg/flags/names/names_test.go new file mode 100644 index 000000000..8f983ba67 --- /dev/null +++ b/pkg/flags/names/names_test.go @@ -0,0 +1,284 @@ +package names_test + +import ( + "testing" + + "github.com/devsy-org/devsy/pkg/flags/names" +) + +// wireNames pins every flag-name constant to its literal command-line string. +// The literals here are written out independently of the constants so that any +// accidental change to a value in names.go breaks this test, forcing the wire +// change to be intentional. This is the source of truth for the CLI's flag-name +// contract and for devsy self-invocation command lines. +var wireNames = []struct { + got string + want string +}{ + {names.DevContainer, "devcontainer"}, + {names.DevContainerImage, "devcontainer-image"}, + {names.DevContainerPath, "devcontainer-path"}, + {names.DevContainerID, "devcontainer-id"}, + {names.DevContainerOverlay, "devcontainer-overlay"}, + {names.FallbackImage, "fallback-image"}, + {names.Features, "features"}, + {names.IDLabel, "id-label"}, + {names.UserEnvProbe, "user-env-probe"}, + {names.GPUAvailability, "gpu-availability"}, + {names.UpdateRemoteUserUID, "update-remote-user-uid"}, + {names.ContainerDataFolder, "container-data-folder"}, + {names.MountWorkspaceGitRoot, "mount-workspace-git-root"}, + {names.ContainerUser, "container-user"}, + {names.RemoteUser, "remote-user"}, + {names.SSHConfigure, "ssh-configure"}, + {names.SSHGPGForwarding, "ssh-gpg-forwarding"}, + {names.SSHConfig, "ssh-config"}, + {names.SSHTunnel, "ssh-tunnel"}, + {names.AgentForwarding, "agent-forwarding"}, + {names.ReuseSSHAuthSock, "reuse-ssh-auth-sock"}, + {names.Stdio, "stdio"}, + {names.StartServices, "start-services"}, + {names.SSHKeepAliveInterval, "ssh-keepalive-interval"}, + {names.ForwardPorts, "forward-ports"}, + {names.ReverseForwardPorts, "reverse-forward-ports"}, + {names.SendEnv, "send-env"}, + {names.SetEnv, "set-env"}, + {names.ForwardPortsTimeout, "forward-ports-timeout"}, + {names.TermMode, "term-mode"}, + {names.InstallTerminfo, "install-terminfo"}, + {names.TrackActivity, "track-activity"}, + {names.Dotfiles, "dotfiles"}, + {names.DotfilesScript, "dotfiles-script"}, + {names.DotfilesTargetPath, "dotfiles-target-path"}, + {names.DotfilesScriptEnv, "dotfiles-script-env"}, + {names.DotfilesScriptEnvFile, "dotfiles-script-env-file"}, + {names.DotfilesRepo, "dotfiles-repo"}, + {names.GitCloneStrategy, "git-clone-strategy"}, + {names.GitRecurseSubmodules, "git-recurse-submodules"}, + {names.GitLFSMode, "git-lfs-mode"}, + {names.GitSSHSigningKey, "git-ssh-signing-key"}, + {names.SkipPostCreate, "skip-post-create"}, + {names.SkipPostStart, "skip-post-start"}, + {names.SkipPostAttach, "skip-post-attach"}, + {names.SkipNonBlockingCommands, "skip-non-blocking-commands"}, + {names.SkipHostRequirements, "skip-host-requirements"}, + {names.TerminalColumns, "terminal-columns"}, + {names.TerminalRows, "terminal-rows"}, + {names.ID, "id"}, + {names.Machine, "machine"}, + {names.Source, "source"}, + {names.ProviderOption, "provider-option"}, + {names.Reconfigure, "reconfigure"}, + {names.Prebuild, "prebuild"}, + {names.Pull, "pull"}, + {names.NoCache, "no-cache"}, + {names.Recreate, "recreate"}, + {names.Reset, "reset"}, + {names.PrebuildRepo, "prebuild-repo"}, + {names.WorkspaceEnv, "workspace-env"}, + {names.WorkspaceEnvFile, "workspace-env-file"}, + {names.SecretsFile, "secrets-file"}, + {names.FeatureSecretsFile, "feature-secrets-file"}, + {names.InitEnv, "init-env"}, + {names.DisableDaemon, "disable-daemon"}, + {names.CacheFrom, "cache-from"}, + {names.WorkspaceMountConsistency, "workspace-mount-consistency"}, + {names.Mount, "mount"}, + {names.Platform, "platform"}, + {names.PullFromInsideContainer, "pull-from-inside-container"}, + {names.WorkspaceFolder, "workspace-folder"}, + {names.IgnoreNotFound, "ignore-not-found"}, + {names.GracePeriod, "grace-period"}, + {names.Force, "force"}, + {names.RemoveVolumes, "remove-volumes"}, + {names.IDE, "ide"}, + {names.IDEOption, "ide-option"}, + {names.IDELaunch, "ide-launch"}, + {names.Option, "option"}, + {names.Userns, "userns"}, + {names.UIDMap, "uidmap"}, + {names.GIDMap, "gidmap"}, + {names.ContainerID, "container-id"}, + {names.DockerPath, "docker-path"}, + {names.RemoteEnv, "remote-env"}, + {names.ContainerWorkspaceInfo, "container-workspace-info"}, + {names.ChownWorkspace, "chown-workspace"}, + {names.StreamMounts, "stream-mounts"}, + {names.InjectGitCredentials, "inject-git-credentials"}, + {names.AccessKey, "access-key"}, + {names.WorkspaceHost, "workspace-host"}, + {names.PlatformHost, "platform-host"}, + {names.Repository, "repository"}, + {names.StrictHostKeyChecking, "strict-host-key-checking"}, + {names.InstallScript, "install-script"}, + {names.SkipPro, "skip-pro"}, + {names.ContainerStatus, "container-status"}, + {names.WorkspaceID, "workspace-id"}, + {names.MachineID, "machine-id"}, + {names.MachineReuse, "machine-reuse"}, + {names.ProviderID, "provider-id"}, + {names.ProviderReuse, "provider-reuse"}, + {names.Data, "data"}, + {names.SkipDelete, "skip-delete"}, + {names.Tag, "tag"}, + {names.SkipPush, "skip-push"}, + {names.Push, "push"}, + {names.Label, "label"}, + {names.Output, "output"}, + {names.ExperimentalLockfile, "experimental-lockfile"}, + {names.ImageName, "image-name"}, + {names.NoBuild, "no-build"}, + {names.ForceBuild, "force-build"}, + {names.ForceInternalBuildKit, "force-internal-buildkit"}, + {names.DaemonInterval, "daemon-interval"}, + {names.ForceDockerless, "force-dockerless"}, + {names.Debug, "debug"}, + {names.Command, "command"}, + {names.Context, "context"}, + {names.User, "user"}, + {names.Workdir, "workdir"}, + {names.Home, "home"}, + {names.LogOutput, "log-output"}, + {names.LogFormat, "log-format"}, + {names.ResultFormat, "result-format"}, + {names.SetupInfo, "setup-info"}, + {names.SecretsEnv, "secrets-env"}, + {names.OwnerTrust, "ownertrust"}, + {names.SocketPath, "socketpath"}, + {names.GitKey, "gitkey"}, + {names.ShutdownAction, "shutdown-action"}, + {names.Timeout, "timeout"}, + {names.Flavor, "flavor"}, + {names.Verbose, "verbose"}, + {names.Quiet, "quiet"}, + {names.Owner, "owner"}, + {names.AgentDir, "agent-dir"}, + {names.UID, "uid"}, + {names.OpenBrowser, "open-browser"}, + {names.InheritListener, "inherit-listener"}, + {names.Config, "config"}, + {names.JSON, "json"}, + {names.Name, "name"}, + {names.DryRun, "dry-run"}, + {names.BaseImage, "base-image"}, + {names.ForceCleanOutputFolder, "force-clean-output-folder"}, + {names.GitHubOwner, "github-owner"}, + {names.GitHubRepo, "github-repo"}, + {names.IncludeFeaturesConfiguration, "include-features-configuration"}, + {names.IncludeMergedConfiguration, "include-merged-configuration"}, + {names.OmitPaths, "omit-paths"}, + {names.OutputFolder, "output-folder"}, + {names.OverrideConfig, "override-config"}, + {names.PreserveTestContainers, "preserve-test-containers"}, + {names.ProjectFolder, "project-folder"}, + {names.Registry, "registry"}, + {names.ShowDependencies, "show-dependencies"}, + {names.ShowTags, "show-tags"}, + {names.SkipScenarios, "skip-scenarios"}, + {names.Target, "target"}, + {names.TemplateArgs, "template-args"}, + {names.TemplateID, "template-id"}, + {names.Available, "available"}, + {names.Create, "create"}, + {names.FromExisting, "from-existing"}, + {names.Prerelease, "prerelease"}, + {names.ProviderSource, "provider-source"}, + {names.Use, "use"}, + {names.Version, "version"}, + {names.Channel, "channel"}, + {names.ChartPath, "chart-path"}, + {names.ChartRepo, "chart-repo"}, + {names.DisplayName, "display-name"}, + {names.Email, "email"}, + {names.Header, "header"}, + {names.HelmChartPath, "helm-chart-path"}, + {names.HelmChartVersion, "helm-chart-version"}, + {names.HelmSet, "helm-set"}, + {names.HelmValues, "helm-values"}, + {names.Host, "host"}, + {names.Insecure, "insecure"}, + {names.Instance, "instance"}, + {names.KubeContext, "kube-context"}, + {names.Login, "login"}, + {names.NoLogin, "no-login"}, + {names.NoTunnel, "no-tunnel"}, + {names.NoWait, "no-wait"}, + {names.Own, "own"}, + {names.Password, "password"}, + {names.PreventWakeup, "prevent-wakeup"}, + {names.Project, "project"}, + {names.Provider, "provider"}, + {names.ReuseValues, "reuse-values"}, + {names.ServiceAccount, "service-account"}, + {names.Token, "token"}, + {names.Upgrade, "upgrade"}, + {names.Values, "values"}, + {names.Wait, "wait"}, + {names.Address, "address"}, + {names.AuthSockID, "auth-sock-id"}, + {names.ConfigureDockerHelper, "configure-docker-helper"}, + {names.ConfigureGitHelper, "configure-git-helper"}, + {names.Container, "container"}, + {names.Daemon, "daemon"}, + {names.Docker, "docker"}, + {names.DockerArg, "docker-arg"}, + {names.DockerCommand, "docker-command"}, + {names.DockerImage, "docker-image"}, + {names.Dry, "dry"}, + {names.ExecOutputCap, "exec-output-cap"}, + {names.ExecTimeoutDefault, "exec-timeout-default"}, + {names.ExecTimeoutMax, "exec-timeout-max"}, + {names.ExtraPorts, "extra-ports"}, + {names.Fail, "fail"}, + {names.FailOnErrorCode, "fail-on-error-code"}, + {names.File, "file"}, + {names.FilterByOwner, "filter-by-owner"}, + {names.FleetWorkspaceID, "workspaceid"}, + {names.ForceBrowser, "force-browser"}, + {names.GitUserSigningKey, "git-user-signing-key"}, + {names.HelperImage, "helper-image"}, + {names.Hidden, "hidden"}, + {names.Interval, "interval"}, + {names.KeyFile, "key-file"}, + {names.MaxDepth, "max-depth"}, + {names.Namespace, "namespace"}, + {names.Port, "port"}, + {names.Request, "request"}, + {names.SingleMachine, "single-machine"}, + {names.SkipInit, "skip-init"}, + {names.SkipOnCreate, "skip-on-create"}, + {names.SkipUpdateContent, "skip-update-content"}, + {names.TargetURL, "target-url"}, + {names.Workspace, "workspace"}, + {names.WorkspaceInfo, "workspace-info"}, + {names.WorkspaceProject, "workspace-project"}, + {names.WorkspaceUID, "workspace-uid"}, +} + +func TestConstantWireNames(t *testing.T) { + seen := make(map[string]struct{}, len(wireNames)) + for _, c := range wireNames { + if c.got != c.want { + t.Errorf("flag constant wire name = %q, want %q", c.got, c.want) + } + if _, dup := seen[c.got]; dup { + t.Errorf("duplicate flag value %q shared by two constants", c.got) + } + seen[c.got] = struct{}{} + } +} + +func TestFlagHelpers(t *testing.T) { + if got := names.Flag("stdio"); got != "--stdio" { + t.Errorf("Flag = %q, want --stdio", got) + } + if got := names.FlagValue("log-output", "raw"); got != "--log-output=raw" { + t.Errorf("FlagValue = %q, want --log-output=raw", got) + } + if got := names.FlagTrue("agent-forwarding"); got != "--agent-forwarding=true" { + t.Errorf("FlagTrue = %q, want --agent-forwarding=true", got) + } + if got := names.FlagFalse("start-services"); got != "--start-services=false" { + t.Errorf("FlagFalse = %q, want --start-services=false", got) + } +} diff --git a/pkg/gpg/forward.go b/pkg/gpg/forward.go index 248139fa4..2d6d30e6c 100644 --- a/pkg/gpg/forward.go +++ b/pkg/gpg/forward.go @@ -5,6 +5,7 @@ import ( "os/exec" client2 "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" devssh "github.com/devsy-org/devsy/pkg/ssh" ) @@ -45,15 +46,15 @@ func buildForwardArgs(user, context, workspace string) []string { return []string{ "workspace", "ssh", - "--gpg-agent-forwarding=true", - "--agent-forwarding=true", - "--start-services=true", - "--user", + names.FlagTrue(names.SSHGPGForwarding), + names.FlagTrue(names.AgentForwarding), + names.FlagTrue(names.StartServices), + names.Flag(names.User), user, - "--context", + names.Flag(names.Context), context, workspace, - "--log-output=raw", - "--command", "sleep infinity", + names.FlagValue(names.LogOutput, "raw"), + names.Flag(names.Command), "sleep infinity", } } diff --git a/pkg/gpg/forward_test.go b/pkg/gpg/forward_test.go index cf765b771..0c534e8ee 100644 --- a/pkg/gpg/forward_test.go +++ b/pkg/gpg/forward_test.go @@ -11,7 +11,7 @@ func TestBuildForwardArgs(t *testing.T) { expected := []string{ "workspace", "ssh", - "--gpg-agent-forwarding=true", + "--ssh-gpg-forwarding=true", "--agent-forwarding=true", "--start-services=true", "--user", "root", diff --git a/pkg/gpg/signing.go b/pkg/gpg/signing.go new file mode 100644 index 000000000..2e3f072e2 --- /dev/null +++ b/pkg/gpg/signing.go @@ -0,0 +1,52 @@ +package gpg + +import ( + "context" + "os/exec" + "strings" + + "github.com/devsy-org/devsy/pkg/git" + "github.com/devsy-org/devsy/pkg/log" +) + +// DetectAgentSocketPath returns the host gpg-agent extra socket path, used to +// reverse-forward the agent into a workspace. +func DetectAgentSocketPath() (string, error) { + out, err := exec.Command("gpgconf", "--list-dir", "agent-extra-socket").Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil +} + +// gitFormatSSH is the git gpg.format value indicating SSH-based commit signing. +const gitFormatSSH = "ssh" + +// SigningKey returns the user's GPG signing key from git config, or "" when no +// key is configured or the signing format is SSH (SSH signing keys are handled +// by the separate SSH signature helper). +func SigningKey(ctx context.Context) string { + config := git.At("").Config() + formatStr, _ := config.Get(ctx, "gpg.format", git.ScopeDefault) + if formatStr == gitFormatSSH { + log.Debugf("gpg.format is %s, skipping GPG signing key", gitFormatSSH) + return "" + } + + result, err := config.Get(ctx, "user.signingKey", git.ScopeDefault) + if err != nil { + log.Debugf("no git signkey detected, skipping") + return "" + } + + // GPG key IDs are hex fingerprints, not file paths. If the signing key + // looks like a file path and the format isn't x509 (which legitimately + // uses certificate file paths via gpgsm), it's an SSH key. + if (strings.HasPrefix(result, "/") || strings.HasPrefix(result, "~")) && formatStr != "x509" { + log.Debugf("signing key %s looks like a file path, skipping", result) + return "" + } + + log.Debugf("detected git sign key %s", result) + return result +} diff --git a/cmd/workspace/ssh_test.go b/pkg/gpg/signing_test.go similarity index 63% rename from cmd/workspace/ssh_test.go rename to pkg/gpg/signing_test.go index 05b10ccd5..26906b0e7 100644 --- a/cmd/workspace/ssh_test.go +++ b/pkg/gpg/signing_test.go @@ -1,4 +1,4 @@ -package workspace +package gpg import ( "context" @@ -15,7 +15,7 @@ func writeGitConfig(t *testing.T, content string) { t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", home) t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(home, ".gitconfig")) - t.Setenv("GIT_CONFIG_SYSTEM", "/dev/null") + t.Setenv("GIT_CONFIG_SYSTEM", os.DevNull) t.Setenv("GIT_DIR", filepath.Join(home, ".git")) err := os.MkdirAll(filepath.Join(home, ".git"), 0o750) assert.NoError(t, err) @@ -23,41 +23,41 @@ func writeGitConfig(t *testing.T, content string) { assert.NoError(t, err) } -func TestGpgSigningKey_GPGFormat(t *testing.T) { +func TestSigningKey_GPGFormat(t *testing.T) { writeGitConfig(t, "[user]\n\tsigningKey = TESTKEY123\n") - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Equal(t, "TESTKEY123", result) } -func TestGpgSigningKey_SSHFormat_Skipped(t *testing.T) { +func TestSigningKey_SSHFormat_Skipped(t *testing.T) { writeGitConfig( t, "[gpg]\n\tformat = ssh\n[user]\n\tsigningKey = /home/user/.ssh/id_ed25519.pub\n", ) - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Empty(t, result) } -func TestGpgSigningKey_NoKeyConfigured(t *testing.T) { +func TestSigningKey_NoKeyConfigured(t *testing.T) { writeGitConfig(t, "[user]\n\tname = Test\n") - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Empty(t, result) } -func TestGpgSigningKey_X509Format_Returned(t *testing.T) { +func TestSigningKey_X509Format_Returned(t *testing.T) { writeGitConfig(t, "[gpg]\n\tformat = x509\n[user]\n\tsigningKey = /path/to/cert\n") - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Equal(t, "/path/to/cert", result) } -func TestGpgSigningKey_SSHKeyPath_Skipped(t *testing.T) { +func TestSigningKey_SSHKeyPath_Skipped(t *testing.T) { writeGitConfig(t, "[user]\n\tsigningKey = /home/user/.ssh/id_ed25519.pub\n") - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Empty(t, result) } -func TestGpgSigningKey_TildeKeyPath_Skipped(t *testing.T) { +func TestSigningKey_TildeKeyPath_Skipped(t *testing.T) { writeGitConfig(t, "[user]\n\tsigningKey = ~/.ssh/id_ed25519.pub\n") - result := gpgSigningKey(context.Background()) + result := SigningKey(context.Background()) assert.Empty(t, result) } diff --git a/pkg/ide/opener/browser_tunnel.go b/pkg/ide/opener/browser_tunnel.go index c470aa782..b6900e3b8 100644 --- a/pkg/ide/opener/browser_tunnel.go +++ b/pkg/ide/opener/browser_tunnel.go @@ -13,6 +13,7 @@ import ( "time" "github.com/devsy-org/devsy/pkg/client" + "github.com/devsy-org/devsy/pkg/flags/names" pkglog "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/open" "github.com/devsy-org/devsy/pkg/provider" @@ -374,24 +375,24 @@ func buildHelperArgs( ) []string { args := []string{ "internal", "browser-tunnel", - "--context", contextName, - "--workspace", workspaceID, - "--target-url", tunnelParams.TargetURL, - "--auth-sock-id", tunnelParams.AuthSockID, - "--user", tunnelParams.User, - "--git-ssh-signing-key", tunnelParams.GitSSHSigningKey, + names.Flag(names.Context), contextName, + names.Flag(names.Workspace), workspaceID, + names.Flag(names.TargetURL), tunnelParams.TargetURL, + names.Flag(names.AuthSockID), tunnelParams.AuthSockID, + names.Flag(names.User), tunnelParams.User, + names.Flag(names.GitSSHSigningKey), tunnelParams.GitSSHSigningKey, } if tunnelParams.ForwardPorts { - args = append(args, "--forward-ports") + args = append(args, names.Flag(names.ForwardPorts)) } for _, p := range tunnelParams.ExtraPorts { - args = append(args, "--extra-ports", p) + args = append(args, names.Flag(names.ExtraPorts), p) } if openBrowser { - args = append(args, "--open-browser") + args = append(args, names.Flag(names.OpenBrowser)) } if pkglog.DebugEnabled() { - args = append(args, "--debug") + args = append(args, names.Flag(names.Debug)) } return args } diff --git a/pkg/ide/opener/browser_tunnel_unix.go b/pkg/ide/opener/browser_tunnel_unix.go index 12da269ad..a3733e139 100644 --- a/pkg/ide/opener/browser_tunnel_unix.go +++ b/pkg/ide/opener/browser_tunnel_unix.go @@ -7,6 +7,7 @@ import ( "net" "os" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/docker/go-connections/nat" ) @@ -59,7 +60,11 @@ func prepareInheritedListeners(extraPorts []string) (inheritedListenerSetup, err // fd 3 is the first ExtraFiles entry in the child. childFD := 3 + len(files) files = append(files, h.f) - args = append(args, "--inherit-listener", fmt.Sprintf("%s=%d", hostAddr, childFD)) + args = append( + args, + names.Flag(names.InheritListener), + fmt.Sprintf("%s=%d", hostAddr, childFD), + ) holds = append(holds, h) } } diff --git a/pkg/ide/opener/opener.go b/pkg/ide/opener/opener.go index 4e8e7dd7a..e7a7ab3cd 100644 --- a/pkg/ide/opener/opener.go +++ b/pkg/ide/opener/opener.go @@ -14,6 +14,7 @@ import ( "github.com/devsy-org/devsy/pkg/command" "github.com/devsy-org/devsy/pkg/config" config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/gpg" "github.com/devsy-org/devsy/pkg/ide/codeserver" "github.com/devsy-org/devsy/pkg/ide/fleet" @@ -469,7 +470,7 @@ func startFleet(ctx context.Context, params IDEParams) (string, error) { sshCmd, err := tunnel.CreateSSHCommand( ctx, params.Client, - []string{"--command", "cat " + fleet.FleetURLFileName}, + []string{names.Flag(names.Command), "cat " + fleet.FleetURLFileName}, ) if err != nil { return "", err diff --git a/pkg/options/options.go b/pkg/options/options.go index da78c892e..a389671cb 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -7,6 +7,18 @@ import ( "strings" ) +// GitIdentityEnvVars are the git author/committer environment variables +// inherited from the host so commits made inside the workspace carry the +// user's identity. +var GitIdentityEnvVars = []string{ + "GIT_AUTHOR_NAME", + "GIT_AUTHOR_EMAIL", + "GIT_AUTHOR_DATE", + "GIT_COMMITTER_NAME", + "GIT_COMMITTER_EMAIL", + "GIT_COMMITTER_DATE", +} + // Takes a list of assignments in KEY=VALUE format, a map of option to propagate, and an environment variable prefix, // and returns a new list with additional assignments from environment variables for any options not already assigned. func InheritOptionsFromEnvironment[Map ~map[string]V, V any]( diff --git a/pkg/ssh/config.go b/pkg/ssh/config.go index 66b72294d..a144b9655 100644 --- a/pkg/ssh/config.go +++ b/pkg/ssh/config.go @@ -12,6 +12,7 @@ import ( "sync" "github.com/devsy-org/devsy/pkg/config" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" "github.com/devsy-org/devsy/pkg/scanner" "github.com/devsy-org/devsy/pkg/util" @@ -111,9 +112,12 @@ type proxyCommandBuilder struct { func newProxyCommandBuilder(execPath, context, user, workspace string) *proxyCommandBuilder { return &proxyCommandBuilder{ baseCommand: fmt.Sprintf( - "\"%s\" workspace ssh --stdio --context %s --user %s %s", + "\"%s\" workspace ssh %s %s %s %s %s %s", execPath, + names.Flag(names.Stdio), + names.Flag(names.Context), context, + names.Flag(names.User), user, workspace, ), @@ -122,21 +126,27 @@ func newProxyCommandBuilder(execPath, context, user, workspace string) *proxyCom func (b *proxyCommandBuilder) withDevsyHome(home string) *proxyCommandBuilder { if home != "" { - b.options = append(b.options, fmt.Sprintf("--home \"%s\"", home)) + b.options = append( + b.options, + fmt.Sprintf("%s \"%s\"", names.Flag(names.Home), home), + ) } return b } func (b *proxyCommandBuilder) withWorkdir(workdir string) *proxyCommandBuilder { if workdir != "" { - b.options = append(b.options, fmt.Sprintf("--workdir \"%s\"", workdir)) + b.options = append( + b.options, + fmt.Sprintf("%s \"%s\"", names.Flag(names.Workdir), workdir), + ) } return b } func (b *proxyCommandBuilder) withGPGAgent(enabled bool) *proxyCommandBuilder { if enabled { - b.options = append(b.options, "--gpg-agent-forwarding") + b.options = append(b.options, names.Flag(names.SSHGPGForwarding)) } return b } diff --git a/pkg/ssh/config_test.go b/pkg/ssh/config_test.go index eb7b2504e..03dfe74c0 100644 --- a/pkg/ssh/config_test.go +++ b/pkg/ssh/config_test.go @@ -151,7 +151,7 @@ Host testhost StrictHostKeyChecking no UserKnownHostsFile /dev/null HostKeyAlgorithms rsa-sha2-256,rsa-sha2-512,ssh-rsa - ProxyCommand "/path/to/exec" workspace ssh --stdio --context testcontext --user testuser testworkspace --gpg-agent-forwarding + ProxyCommand "/path/to/exec" workspace ssh --stdio --context testcontext --user testuser testworkspace --ssh-gpg-forwarding User testuser # Devsy End testhost`, }, @@ -324,7 +324,7 @@ Host testhost } if tt.gpgagent { - assert.Contains(s.T(), result, "--gpg-agent-forwarding") + assert.Contains(s.T(), result, "--ssh-gpg-forwarding") } if tt.config != "" { diff --git a/pkg/tunnel/browser.go b/pkg/tunnel/browser.go index cda2aa91a..ef80f5130 100644 --- a/pkg/tunnel/browser.go +++ b/pkg/tunnel/browser.go @@ -13,6 +13,7 @@ import ( client2 "github.com/devsy-org/devsy/pkg/client" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/exitcode" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" devssh "github.com/devsy-org/devsy/pkg/ssh" "golang.org/x/crypto/ssh" @@ -71,9 +72,9 @@ func startBrowserTunnelSSH(ctx context.Context, p BrowserTunnelParams) error { defer func() { _ = writer.Close() }() sshCmd, err := CreateSSHCommand(ctx, p.Client, []string{ - "--log-output=raw", - fmt.Sprintf("--reuse-ssh-auth-sock=%s", p.AuthSockID), - "--stdio", + names.FlagValue(names.LogOutput, "raw"), + names.FlagValue(names.ReuseSSHAuthSock, p.AuthSockID), + names.Flag(names.Stdio), }) if err != nil { return err @@ -158,20 +159,20 @@ func SetupBackhaul( execPath, "workspace", "ssh", - "--agent-forwarding=true", - fmt.Sprintf("--reuse-ssh-auth-sock=%s", authSockID), - "--start-services=false", - "--user", + names.FlagTrue(names.AgentForwarding), + names.FlagValue(names.ReuseSSHAuthSock, authSockID), + names.FlagFalse(names.StartServices), + names.Flag(names.User), remoteUser, - "--context", + names.Flag(names.Context), client.Context(), client.Workspace(), - "--log-output=raw", - "--command", + names.FlagValue(names.LogOutput, "raw"), + names.Flag(names.Command), "while true; do sleep 6000000; done", // sleep infinity is not available on all systems ) if log.DebugEnabled() { - cmd.Args = append(cmd.Args, "--debug") + cmd.Args = append(cmd.Args, names.Flag(names.Debug)) } cmd.Stdout = writer cmd.Stderr = writer @@ -250,15 +251,15 @@ func buildSSHCommandArgs(clientContext, workspace string, debug bool, extraArgs args := []string{ "workspace", "ssh", - "--user=root", - "--agent-forwarding=false", - "--start-services=false", - "--context", + names.FlagValue(names.User, "root"), + names.FlagFalse(names.AgentForwarding), + names.FlagFalse(names.StartServices), + names.Flag(names.Context), clientContext, workspace, } if debug { - args = append(args, "--debug") + args = append(args, names.Flag(names.Debug)) } args = append(args, extraArgs...) return args diff --git a/pkg/tunnel/dial.go b/pkg/tunnel/dial.go index 5457211b9..e7596e18e 100644 --- a/pkg/tunnel/dial.go +++ b/pkg/tunnel/dial.go @@ -9,13 +9,13 @@ import ( "sync" "time" + "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/log" ) const ( workspaceSubcommand = "workspace" sshSubcommand = "ssh" - sshStdioFlag = "--stdio" ) type WorkspaceDialer struct { @@ -81,18 +81,18 @@ func (d *WorkspaceDialer) buildCommand() (string, []string, error) { } } - args := []string{workspaceSubcommand, sshSubcommand, sshStdioFlag} + args := []string{workspaceSubcommand, sshSubcommand, names.Flag(names.Stdio)} if d.Context != "" { - args = append(args, "--context", d.Context) + args = append(args, names.Flag(names.Context), d.Context) } if d.User != "" { - args = append(args, "--user", d.User) + args = append(args, names.Flag(names.User), d.User) } if d.Workdir != "" { - args = append(args, "--workdir", d.Workdir) + args = append(args, names.Flag(names.Workdir), d.Workdir) } if d.GPGAgent { - args = append(args, "--gpg-agent-forwarding") + args = append(args, names.Flag(names.SSHGPGForwarding)) } args = append(args, d.Workspace) diff --git a/sites/docs-devsy-sh/package.json b/sites/docs-devsy-sh/package.json index f3144e251..936d4b003 100644 --- a/sites/docs-devsy-sh/package.json +++ b/sites/docs-devsy-sh/package.json @@ -16,7 +16,7 @@ "docusaurus-gtm-plugin": "^0.0.2", "mdx-link-checker": "^0.1.1", "react": "^19.0.0", - "react-dom": "^18.2.0" + "react-dom": "^19.0.0" }, "browserslist": { "production": [ diff --git a/sites/docs-devsy-sh/yarn.lock b/sites/docs-devsy-sh/yarn.lock index a8e3208ac..346022732 100644 --- a/sites/docs-devsy-sh/yarn.lock +++ b/sites/docs-devsy-sh/yarn.lock @@ -13256,13 +13256,12 @@ react-dom@^16.8.4: prop-types "^15.6.2" scheduler "^0.19.1" -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== +react-dom@^19.0.0: + version "19.2.8" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.8.tgz#3b46b9eeda877cdff2cf13d2770fff4ae36c2ec2" + integrity sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ== dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" + scheduler "^0.27.0" react-error-overlay@^6.0.9: version "6.0.11" @@ -14009,12 +14008,10 @@ scheduler@^0.19.1: loose-envify "^1.1.0" object-assign "^4.1.1" -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== schema-dts@^1.1.2: version "1.1.5" From 247347ccfb7b2663b25a9530a9a8021f86e3590e Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 09:58:08 -0500 Subject: [PATCH 2/7] fix(docs): bump Docusaurus to ^3.10.2 for React 19 compatibility The docs site declared @docusaurus/core/preset-classic at ^3.5.2 while react/react-dom are at ^19. Docusaurus gained official React 19 support in 3.6; pin the range to ^3.10.2 (already the resolved version) so the declared dependency honestly reflects React 19 support rather than relying on the caret range happening to resolve high. Verified with a clean `yarn install --frozen-lockfile && yarn build`. --- sites/docs-devsy-sh/package.json | 4 ++-- sites/docs-devsy-sh/yarn.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sites/docs-devsy-sh/package.json b/sites/docs-devsy-sh/package.json index 936d4b003..76dfe9ac5 100644 --- a/sites/docs-devsy-sh/package.json +++ b/sites/docs-devsy-sh/package.json @@ -9,8 +9,8 @@ "deploy": "docusaurus deploy" }, "dependencies": { - "@docusaurus/core": "^3.5.2", - "@docusaurus/preset-classic": "^3.5.2", + "@docusaurus/core": "^3.10.2", + "@docusaurus/preset-classic": "^3.10.2", "classnames": "^2.2.6", "docusaurus": "^1.14.7", "docusaurus-gtm-plugin": "^0.0.2", diff --git a/sites/docs-devsy-sh/yarn.lock b/sites/docs-devsy-sh/yarn.lock index 346022732..6a37f7aa5 100644 --- a/sites/docs-devsy-sh/yarn.lock +++ b/sites/docs-devsy-sh/yarn.lock @@ -2653,7 +2653,7 @@ webpack "^5.95.0" webpackbar "^7.0.0" -"@docusaurus/core@3.10.2", "@docusaurus/core@^3.5.2": +"@docusaurus/core@3.10.2", "@docusaurus/core@^3.10.2": version "3.10.2" resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.2.tgz#349cae728fc3769b3f8aef4cf538ccb79aace8d0" integrity sha512-EYByj6nk+aD9KeVxV6Hmo2/nAAT79P21Y82ycTBOBtrmqilloIbIEhgL2/8Xpt2Jz/pgNqHAwyusOGwmbKeJmA== @@ -2907,7 +2907,7 @@ tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/preset-classic@^3.5.2": +"@docusaurus/preset-classic@^3.10.2": version "3.10.2" resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.2.tgz#e4419c811723ab913a946c63efcc15e7f5f60a0a" integrity sha512-a4B3VczmDl99zK0EufDQYomdJ186WDingjmDXxhN2PNPS9Ty/Y2M5CLFX1KQMRKqRTLiRDKfutzG5IY1FC/ceg== From 72a33a5d79be72fd3d999f6c93d3d5cb3b60e3b8 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 10:12:30 -0500 Subject: [PATCH 3/7] fix: propagate renamed flags to desktop app and docs The flag refactor renames several flags; update the non-Go consumers that reference them by literal string: - desktop/src/main/ipc.ts: --prebuild-repository -> --prebuild-repo (the desktop app shells out to the devsy CLI; the old name would fail with "unknown flag" whenever a prebuild repository was set). - docs: --prebuild-repository -> --prebuild-repo and --gpg-agent-forwarding -> --ssh-gpg-forwarding in the workspace guides. --- desktop/src/main/ipc.ts | 2 +- .../pages/developing-in-workspaces/credentials.mdx | 2 +- .../pages/developing-in-workspaces/prebuild-a-workspace.mdx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index 73df625fb..a71095a92 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -625,7 +625,7 @@ export function registerIpcHandlers(deps: IpcDependencies): { if (args.devcontainerPath) cliArgs.push("--devcontainer-path", args.devcontainerPath) if (args.prebuildRepository) - cliArgs.push("--prebuild-repository", args.prebuildRepository) + cliArgs.push("--prebuild-repo", args.prebuildRepository) if (args.platform) cliArgs.push("--platform", args.platform) const wsId = args.workspaceId ?? args.source diff --git a/sites/docs-devsy-sh/pages/developing-in-workspaces/credentials.mdx b/sites/docs-devsy-sh/pages/developing-in-workspaces/credentials.mdx index 520549a3f..8d1c39cb5 100644 --- a/sites/docs-devsy-sh/pages/developing-in-workspaces/credentials.mdx +++ b/sites/docs-devsy-sh/pages/developing-in-workspaces/credentials.mdx @@ -37,5 +37,5 @@ devsy context set default -o GPG_AGENT_FORWARDING=true Or when creating a workspace using: ``` -devsy workspace up --gpg-agent-forwarding my-workspace +devsy workspace up --ssh-gpg-forwarding my-workspace ``` diff --git a/sites/docs-devsy-sh/pages/developing-in-workspaces/prebuild-a-workspace.mdx b/sites/docs-devsy-sh/pages/developing-in-workspaces/prebuild-a-workspace.mdx index fe3506635..17b838f20 100644 --- a/sites/docs-devsy-sh/pages/developing-in-workspaces/prebuild-a-workspace.mdx +++ b/sites/docs-devsy-sh/pages/developing-in-workspaces/prebuild-a-workspace.mdx @@ -36,9 +36,9 @@ If a prebuild cannot be found in the given repository or credentials are missing ### Reference Prebuild Repository as Flag -When creating a new workspace, you can define the prebuild repository via the `--prebuild-repository` flag: +When creating a new workspace, you can define the prebuild repository via the `--prebuild-repo` flag: ``` -devsy workspace up github.com/my-org/my-repo --prebuild-repository ghcr.io/my-org/my-repo +devsy workspace up github.com/my-org/my-repo --prebuild-repo ghcr.io/my-org/my-repo ``` ### Reference Prebuild Repository in devcontainer.json From ea3bb6e7df9369233fb0db4ca318155a5072aa13 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 10:33:51 -0500 Subject: [PATCH 4/7] feat(desktop): expose devcontainer source override in the wizard Wire the CLI's --devcontainer flag into the workspace wizard. The Advanced section's single "Dev container config" path field becomes a "Dev container source" mode selector that maps 1:1 to the flag forms: Auto-detect (default) -> flag omitted (project discovery) Config file path -> --devcontainer Container image -> --devcontainer image: Named profile -> --devcontainer id: Ignore project config -> --devcontainer none The value input only appears for modes that need one, and blank values for path/image/id fall back to omitting the flag so an invalid "image:"/"id:" token is never emitted. buildDevcontainerArg centralizes the mode->flag mapping and is covered by unit tests; the IPC layer now forwards a single `devcontainer` string instead of `devcontainerPath`. --- desktop/src/main/ipc.ts | 6 +- .../workspace/WorkspaceWizard.svelte | 67 +++++++++++++---- .../workspace/WorkspaceWizard.test.ts | 12 ++-- .../src/renderer/src/lib/ipc/commands.test.ts | 6 +- desktop/src/renderer/src/lib/ipc/commands.ts | 2 +- .../src/lib/utils/workspace-source.test.ts | 72 +++++++++++++++---- .../src/lib/utils/workspace-source.ts | 49 +++++++++++-- 7 files changed, 166 insertions(+), 48 deletions(-) diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index a71095a92..0ce2637b0 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -605,7 +605,7 @@ export function registerIpcHandlers(deps: IpcDependencies): { ideLaunch?: "auto" | "headless" | "skip" debug?: boolean workspaceFolder?: string - devcontainerPath?: string + devcontainer?: string prebuildRepository?: string platform?: string }, @@ -622,8 +622,8 @@ export function registerIpcHandlers(deps: IpcDependencies): { if (args.debug) cliArgs.push("--debug") if (args.workspaceFolder) cliArgs.push("--workspace-folder", args.workspaceFolder) - if (args.devcontainerPath) - cliArgs.push("--devcontainer-path", args.devcontainerPath) + if (args.devcontainer) + cliArgs.push("--devcontainer", args.devcontainer) if (args.prebuildRepository) cliArgs.push("--prebuild-repo", args.prebuildRepository) if (args.platform) cliArgs.push("--platform", args.platform) diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte index e9c6aa482..38bd4e14d 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.svelte @@ -38,6 +38,7 @@ import { buildWorkspaceSource } from "$lib/utils/workspace-source.js" import type { WorkspaceSourceType, GitRefType, + DevcontainerMode, } from "$lib/utils/workspace-source.js" import { onCommandProgress } from "$lib/ipc/events.js" import type { CommandProgress } from "$lib/types/index.js" @@ -171,7 +172,8 @@ let imageRef = $state("") let refType = $state("branch") let refValue = $state("") let subPath = $state("") -let devcontainerPath = $state("") +let devcontainerMode = $state("auto") +let devcontainerValue = $state("") let prebuildRepository = $state("") let workspaceFolder = $state("") let advancedOpen = $state(false) @@ -185,7 +187,7 @@ let assembled = $derived( ? buildWorkspaceSource({ sourceType: "local", localPath, - devcontainerPath, + devcontainer: { mode: devcontainerMode, value: devcontainerValue }, prebuildRepository, }) : buildWorkspaceSource({ @@ -194,7 +196,7 @@ let assembled = $derived( refType, refValue, subPath, - devcontainerPath, + devcontainer: { mode: devcontainerMode, value: devcontainerValue }, prebuildRepository, }), ) @@ -336,7 +338,8 @@ function reset() { refType = "branch" refValue = "" subPath = "" - devcontainerPath = "" + devcontainerMode = "auto" + devcontainerValue = "" prebuildRepository = "" workspaceFolder = "" advancedOpen = false @@ -505,7 +508,7 @@ async function handleLaunch() { ide: selectedIde, ideLaunch: "auto", workspaceFolder: workspaceFolder.trim() || undefined, - devcontainerPath: assembled.devcontainerPath, + devcontainer: assembled.devcontainer, prebuildRepository: assembled.prebuildRepository, platform: imageIncompatible && emulationEnabled && emulationTarget @@ -700,14 +703,48 @@ function selectTemplate(t: { name: string; source: string }) {
- - (devcontainerPath = e.currentTarget.value)} - /> + + + + {devcontainerMode === "auto" + ? "Auto-detect (default)" + : devcontainerMode === "path" + ? "Config file path" + : devcontainerMode === "image" + ? "Container image" + : devcontainerMode === "id" + ? "Named profile" + : "Ignore project config"} + + + Auto-detect (default) + Config file path + Container image + Named profile + Ignore project config + + + {#if devcontainerMode === "path" || devcontainerMode === "image" || devcontainerMode === "id"} + (devcontainerValue = e.currentTarget.value)} + /> + {/if}

- Path to the devcontainer.json to build from. Leave blank to auto-detect. + {devcontainerMode === "auto" + ? "Use the devcontainer.json discovered in the project." + : devcontainerMode === "path" + ? "Build from a devcontainer.json at this path, overriding project discovery." + : devcontainerMode === "image" + ? "Ignore the project config and run this image directly." + : devcontainerMode === "id" + ? "Use the named .devcontainer/ profile in the project." + : "Ignore the project config; falls back to the language default."}

@@ -1033,10 +1070,10 @@ function selectTemplate(t: { name: string; source: string }) { {subPath} {/if} - {#if assembled.devcontainerPath} + {#if assembled.devcontainer}
- Dev container config - {assembled.devcontainerPath} + Dev container source + {assembled.devcontainer}
{/if} {#if assembled.prebuildRepository} diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts index 256e8cf09..9cbed5f3d 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts @@ -463,12 +463,6 @@ describe("WorkspaceWizard", () => { 'input[placeholder="/workspaces/app"]', ) as HTMLInputElement await fireEvent.input(wsFolderInput, { target: { value: "/workspaces/app" } }) - const devcontainerInput = document.querySelector( - 'input[placeholder*="devcontainer.json"]', - ) as HTMLInputElement - await fireEvent.input(devcontainerInput, { - target: { value: ".devcontainer/devcontainer.json" }, - }) const prebuildInput = document.querySelector( 'input[placeholder*="ghcr.io/org/prebuilds"]', ) as HTMLInputElement @@ -488,11 +482,13 @@ describe("WorkspaceWizard", () => { await fireEvent.click(launchBtn) await flushAsync() + // Devcontainer source defaults to auto-detect (flag omitted); the value + // mapping for each mode is covered by workspace-source.test.ts. expect(workspaceUp).toHaveBeenCalledWith( expect.objectContaining({ source: "github.com/org/repo@subpath:pkg/api", workspaceFolder: "/workspaces/app", - devcontainerPath: ".devcontainer/devcontainer.json", + devcontainer: undefined, prebuildRepository: "ghcr.io/org/prebuilds", }), ) @@ -547,7 +543,7 @@ describe("WorkspaceWizard", () => { expect.objectContaining({ source: "ubuntu:22.04", workspaceFolder: "/workspaces/app", - devcontainerPath: undefined, + devcontainer: undefined, prebuildRepository: undefined, }), ) diff --git a/desktop/src/renderer/src/lib/ipc/commands.test.ts b/desktop/src/renderer/src/lib/ipc/commands.test.ts index 19f5df9c6..7c4cfd043 100644 --- a/desktop/src/renderer/src/lib/ipc/commands.test.ts +++ b/desktop/src/renderer/src/lib/ipc/commands.test.ts @@ -98,18 +98,18 @@ describe("IPC commands", () => { expect(result).toBe('{"state":"Running"}') }) - it("workspaceUp forwards devcontainerPath and prebuildRepository", async () => { + it("workspaceUp forwards devcontainer and prebuildRepository", async () => { mockInvoke.mockResolvedValue("cmd-id") await workspaceUp({ source: "github.com/org/repo", - devcontainerPath: ".devcontainer/devcontainer.json", + devcontainer: "image:python:3", prebuildRepository: "ghcr.io/org/prebuilds", }) expect(mockInvoke).toHaveBeenCalledWith( "workspace_up", expect.objectContaining({ source: "github.com/org/repo", - devcontainerPath: ".devcontainer/devcontainer.json", + devcontainer: "image:python:3", prebuildRepository: "ghcr.io/org/prebuilds", }), ) diff --git a/desktop/src/renderer/src/lib/ipc/commands.ts b/desktop/src/renderer/src/lib/ipc/commands.ts index cf55b95cb..3dc247784 100644 --- a/desktop/src/renderer/src/lib/ipc/commands.ts +++ b/desktop/src/renderer/src/lib/ipc/commands.ts @@ -27,7 +27,7 @@ export async function workspaceUp(params: { ideLaunch?: "auto" | "headless" | "skip" debug?: boolean workspaceFolder?: string - devcontainerPath?: string + devcontainer?: string prebuildRepository?: string platform?: string }): Promise { diff --git a/desktop/src/renderer/src/lib/utils/workspace-source.test.ts b/desktop/src/renderer/src/lib/utils/workspace-source.test.ts index 8c635e41f..12afcf6da 100644 --- a/desktop/src/renderer/src/lib/utils/workspace-source.test.ts +++ b/desktop/src/renderer/src/lib/utils/workspace-source.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect } from "vitest" -import { buildWorkspaceSource } from "./workspace-source.js" +import { + buildWorkspaceSource, + buildDevcontainerArg, +} from "./workspace-source.js" import type { GitSourceForm, LocalSourceForm, @@ -13,7 +16,7 @@ function gitForm(overrides: Partial): GitSourceForm { refType: "branch", refValue: "", subPath: "", - devcontainerPath: "", + devcontainer: { mode: "auto", value: "" }, prebuildRepository: "", ...overrides, } @@ -23,7 +26,7 @@ function localForm(overrides: Partial): LocalSourceForm { return { sourceType: "local", localPath: "", - devcontainerPath: "", + devcontainer: { mode: "auto", value: "" }, prebuildRepository: "", ...overrides, } @@ -41,7 +44,7 @@ describe("buildWorkspaceSource", () => { it("git: bare repo url, no suffixes", () => { const out = buildWorkspaceSource(gitForm({ repoUrl: "github.com/org/repo" })) expect(out.source).toBe("github.com/org/repo") - expect(out.devcontainerPath).toBeUndefined() + expect(out.devcontainer).toBeUndefined() expect(out.prebuildRepository).toBeUndefined() }) @@ -108,9 +111,13 @@ describe("buildWorkspaceSource", () => { it("git: whitespace-only optional fields become undefined", () => { const out = buildWorkspaceSource( - gitForm({ repoUrl: "github.com/org/repo", devcontainerPath: " ", prebuildRepository: " " }), + gitForm({ + repoUrl: "github.com/org/repo", + devcontainer: { mode: "path", value: " " }, + prebuildRepository: " ", + }), ) - expect(out.devcontainerPath).toBeUndefined() + expect(out.devcontainer).toBeUndefined() expect(out.prebuildRepository).toBeUndefined() }) @@ -128,15 +135,15 @@ describe("buildWorkspaceSource", () => { expect(out.source).toBe("github.com/org/repo@main") }) - it("git: forwards devcontainerPath and prebuildRepository", () => { + it("git: forwards devcontainer path and prebuildRepository", () => { const out = buildWorkspaceSource( gitForm({ repoUrl: "github.com/org/repo", - devcontainerPath: ".devcontainer/devcontainer.json", + devcontainer: { mode: "path", value: ".devcontainer/devcontainer.json" }, prebuildRepository: "ghcr.io/org/prebuilds", }), ) - expect(out.devcontainerPath).toBe(".devcontainer/devcontainer.json") + expect(out.devcontainer).toBe(".devcontainer/devcontainer.json") expect(out.prebuildRepository).toBe("ghcr.io/org/prebuilds") }) @@ -155,15 +162,15 @@ describe("buildWorkspaceSource", () => { expect(out.source).not.toContain("@subpath:") }) - it("local: forwards devcontainerPath and prebuildRepository", () => { + it("local: forwards devcontainer path and prebuildRepository", () => { const out = buildWorkspaceSource( localForm({ localPath: "/home/me/proj", - devcontainerPath: ".devcontainer/devcontainer.json", + devcontainer: { mode: "path", value: ".devcontainer/devcontainer.json" }, prebuildRepository: "ghcr.io/org/prebuilds", }), ) - expect(out.devcontainerPath).toBe(".devcontainer/devcontainer.json") + expect(out.devcontainer).toBe(".devcontainer/devcontainer.json") expect(out.prebuildRepository).toBe("ghcr.io/org/prebuilds") }) @@ -172,7 +179,7 @@ describe("buildWorkspaceSource", () => { imageForm({ imageRef: "mcr.microsoft.com/devcontainers/python:3.12" }), ) expect(out.source).toBe("mcr.microsoft.com/devcontainers/python:3.12") - expect(out.devcontainerPath).toBeUndefined() + expect(out.devcontainer).toBeUndefined() expect(out.prebuildRepository).toBeUndefined() }) @@ -183,3 +190,42 @@ describe("buildWorkspaceSource", () => { expect(out.source).toBe("mcr.microsoft.com/devcontainers/go:1") }) }) + +describe("buildDevcontainerArg", () => { + it("auto omits the flag", () => { + expect(buildDevcontainerArg({ mode: "auto", value: "" })).toBeUndefined() + expect(buildDevcontainerArg({ mode: "auto", value: "ignored" })).toBeUndefined() + }) + + it("none maps to the none token", () => { + expect(buildDevcontainerArg({ mode: "none", value: "" })).toBe("none") + }) + + it("path passes the raw path", () => { + expect( + buildDevcontainerArg({ mode: "path", value: ".devcontainer/devcontainer.json" }), + ).toBe(".devcontainer/devcontainer.json") + }) + + it("image prefixes with image:", () => { + expect(buildDevcontainerArg({ mode: "image", value: "python:3" })).toBe( + "image:python:3", + ) + }) + + it("id prefixes with id:", () => { + expect(buildDevcontainerArg({ mode: "id", value: "backend" })).toBe("id:backend") + }) + + it("trims the value", () => { + expect(buildDevcontainerArg({ mode: "image", value: " python " })).toBe( + "image:python", + ) + }) + + it("value-requiring modes omit the flag when the value is blank", () => { + expect(buildDevcontainerArg({ mode: "path", value: " " })).toBeUndefined() + expect(buildDevcontainerArg({ mode: "image", value: "" })).toBeUndefined() + expect(buildDevcontainerArg({ mode: "id", value: " " })).toBeUndefined() + }) +}) diff --git a/desktop/src/renderer/src/lib/utils/workspace-source.ts b/desktop/src/renderer/src/lib/utils/workspace-source.ts index 859ce3da0..03c0a17bb 100644 --- a/desktop/src/renderer/src/lib/utils/workspace-source.ts +++ b/desktop/src/renderer/src/lib/utils/workspace-source.ts @@ -1,20 +1,35 @@ export type WorkspaceSourceType = "git" | "local" | "image" export type GitRefType = "branch" | "commit" | "pr" +// DevcontainerMode selects how the workspace resolves its devcontainer config, +// mapping 1:1 to the CLI's --devcontainer flag forms: +// auto -> flag omitted (project discovery) +// path -> --devcontainer +// image -> --devcontainer image: +// id -> --devcontainer id: +// none -> --devcontainer none +export type DevcontainerMode = "auto" | "path" | "image" | "id" | "none" + +export interface DevcontainerConfig { + mode: DevcontainerMode + // value backs the path/image/id modes; ignored for auto and none. + value: string +} + export interface GitSourceForm { sourceType: "git" repoUrl: string refType: GitRefType refValue: string subPath: string - devcontainerPath: string + devcontainer: DevcontainerConfig prebuildRepository: string } export interface LocalSourceForm { sourceType: "local" localPath: string - devcontainerPath: string + devcontainer: DevcontainerConfig prebuildRepository: string } @@ -30,10 +45,34 @@ export type WorkspaceSourceForm = export interface WorkspaceSourceResult { source: string - devcontainerPath?: string + // devcontainer is the value for the --devcontainer flag, or undefined when + // the flag should be omitted (auto-detect). + devcontainer?: string prebuildRepository?: string } +// buildDevcontainerArg renders a DevcontainerConfig into the --devcontainer +// flag value, or undefined when the flag should be omitted. Modes that require +// a value (path/image/id) fall back to omitting the flag when the value is +// blank, so an empty input never produces an invalid "image:" / "id:" token. +export function buildDevcontainerArg( + config: DevcontainerConfig, +): string | undefined { + const value = config.value.trim() + switch (config.mode) { + case "none": + return "none" + case "path": + return value || undefined + case "image": + return value ? `image:${value}` : undefined + case "id": + return value ? `id:${value}` : undefined + case "auto": + return undefined + } +} + // hostFromUrl extracts the hostname from SSH (git@host:path) and URL // (scheme://[user@]host/path) forms. function hostFromUrl(repoUrl: string): string { @@ -91,7 +130,7 @@ export function buildWorkspaceSource( if (form.sourceType === "local") { return { source: form.localPath.trim(), - devcontainerPath: optional(form.devcontainerPath), + devcontainer: buildDevcontainerArg(form.devcontainer), prebuildRepository: optional(form.prebuildRepository), } } @@ -101,7 +140,7 @@ export function buildWorkspaceSource( return { source, - devcontainerPath: optional(form.devcontainerPath), + devcontainer: buildDevcontainerArg(form.devcontainer), prebuildRepository: optional(form.prebuildRepository), } } From 28a04ad06eeb25328e4bcc31e7c9e06dddf01637 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 12:38:27 -0500 Subject: [PATCH 5/7] test: increase timeout due to slow apt downloads --- e2e/tests/configread/configread.go | 2 +- e2e/tests/up-features/up_features.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/e2e/tests/configread/configread.go b/e2e/tests/configread/configread.go index 226e12c6e..a677ff8a8 100644 --- a/e2e/tests/configread/configread.go +++ b/e2e/tests/configread/configread.go @@ -344,7 +344,7 @@ var _ = ginkgo.Describe("config read command", ginkgo.Label("config-read"), func ws, ok := result["workspace"].(map[string]any) gomega.Expect(ok).To(gomega.BeTrue(), "workspace should be an object") gomega.Expect(ws).To(gomega.HaveKey("workspaceFolder")) - }, ginkgo.SpecTimeout(framework.TimeoutShort())) + }, ginkgo.SpecTimeout(framework.TimeoutModerate())) ginkgo.It("reads configuration from a running container via --container-id", func(ctx context.Context) { diff --git a/e2e/tests/up-features/up_features.go b/e2e/tests/up-features/up_features.go index e0d203ccf..64f712933 100644 --- a/e2e/tests/up-features/up_features.go +++ b/e2e/tests/up-features/up_features.go @@ -492,17 +492,15 @@ var _ = ginkgo.Describe("testing up command", ginkgo.Label("up-features"), func( wsName := filepath.Base(tempDir) ginkgo.DeferCleanup(f.DevsyWorkspaceDelete, wsName) - // This should not fail with "Parent does not exist" error err = f.DevsyUp(ctx, tempDir) framework.ExpectNoError(err) - // Test that both features are installed correctly out, err := f.DevsySSH(ctx, wsName, "python3 --version") framework.ExpectNoError(err) gomega.Expect(out).To(gomega.ContainSubstring("Python 3.11")) }, ginkgo.SpecTimeout(framework.TimeoutLong()), - ) // This test compiles Python + ) ginkgo.It( "should handle same feature in dependsOn and installsAfter", From 148e497369b636fe7c771c3ec0c1ad2dfd896526 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 13:38:49 -0500 Subject: [PATCH 6/7] style: update comments --- cmd/env_flags_test.go | 3 - .../workspace/WorkspaceWizard.test.ts | 2 - .../src/lib/utils/workspace-source.ts | 15 -- e2e/tests/ide/browser_returns.go | 5 +- pkg/devcontainer/compose.go | 2 +- pkg/flags/builder.go | 9 +- pkg/flags/devcontainer.go | 6 - pkg/flags/names/names.go | 147 +++++++----------- pkg/flags/names/names_test.go | 5 - pkg/gpg/signing.go | 2 +- 10 files changed, 62 insertions(+), 134 deletions(-) diff --git a/cmd/env_flags_test.go b/cmd/env_flags_test.go index e607c6d1a..ca293f62f 100644 --- a/cmd/env_flags_test.go +++ b/cmd/env_flags_test.go @@ -118,9 +118,6 @@ func TestOptInEnvFlags_CLIOverridesEnv(t *testing.T) { assert.Equal(t, "cli-value", seen, "CLI flag must override env value") } -// TestOptInEnvFlags_EnvSatisfiesRequired drives cobra's full parse + -// ValidateRequiredFlags pipeline to prove DEVSY_HOST satisfies -// MarkFlagRequired(names.Host) at execution time, not just in static inspection. func TestOptInEnvFlags_EnvSatisfiesRequired(t *testing.T) { t.Setenv(envProEnabled, "true") t.Setenv(envDevsyHost, "from-env.example.com") diff --git a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts index 9cbed5f3d..f7b500230 100644 --- a/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts +++ b/desktop/src/renderer/src/lib/components/workspace/WorkspaceWizard.test.ts @@ -482,8 +482,6 @@ describe("WorkspaceWizard", () => { await fireEvent.click(launchBtn) await flushAsync() - // Devcontainer source defaults to auto-detect (flag omitted); the value - // mapping for each mode is covered by workspace-source.test.ts. expect(workspaceUp).toHaveBeenCalledWith( expect.objectContaining({ source: "github.com/org/repo@subpath:pkg/api", diff --git a/desktop/src/renderer/src/lib/utils/workspace-source.ts b/desktop/src/renderer/src/lib/utils/workspace-source.ts index 03c0a17bb..fd981ccf1 100644 --- a/desktop/src/renderer/src/lib/utils/workspace-source.ts +++ b/desktop/src/renderer/src/lib/utils/workspace-source.ts @@ -1,18 +1,9 @@ export type WorkspaceSourceType = "git" | "local" | "image" export type GitRefType = "branch" | "commit" | "pr" - -// DevcontainerMode selects how the workspace resolves its devcontainer config, -// mapping 1:1 to the CLI's --devcontainer flag forms: -// auto -> flag omitted (project discovery) -// path -> --devcontainer -// image -> --devcontainer image: -// id -> --devcontainer id: -// none -> --devcontainer none export type DevcontainerMode = "auto" | "path" | "image" | "id" | "none" export interface DevcontainerConfig { mode: DevcontainerMode - // value backs the path/image/id modes; ignored for auto and none. value: string } @@ -45,16 +36,10 @@ export type WorkspaceSourceForm = export interface WorkspaceSourceResult { source: string - // devcontainer is the value for the --devcontainer flag, or undefined when - // the flag should be omitted (auto-detect). devcontainer?: string prebuildRepository?: string } -// buildDevcontainerArg renders a DevcontainerConfig into the --devcontainer -// flag value, or undefined when the flag should be omitted. Modes that require -// a value (path/image/id) fall back to omitting the flag when the value is -// blank, so an empty input never produces an invalid "image:" / "id:" token. export function buildDevcontainerArg( config: DevcontainerConfig, ): string | undefined { diff --git a/e2e/tests/ide/browser_returns.go b/e2e/tests/ide/browser_returns.go index 94f5c4140..2e0cecd08 100644 --- a/e2e/tests/ide/browser_returns.go +++ b/e2e/tests/ide/browser_returns.go @@ -45,8 +45,7 @@ func setupBrowserIDE(ctx context.Context, initialDir string) (*framework.Framewo } // upBrowserIDE runs `devsy up --ide=openvscode --ide-launch=headless` against -// tempDir, optionally with extra args (e.g. names.Flag(names.Recreate)). It returns the -// resolved workspace's tunnel state, which is asserted non-nil. +// tempDir, optionally with extra args. func upBrowserIDE( ctx context.Context, f *framework.Framework, tempDir string, extraArgs ...string, ) *opener.TunnelState { @@ -171,8 +170,6 @@ var _ = ginkgo.Describe( gomega.Expect(syscall.Kill(pid1, 0)).NotTo(gomega.HaveOccurred(), "expected helper PID %d to be alive before --recreate", pid1) - // Recreate the workspace; the existing browser tunnel must be - // killed before a new helper is spawned. state2 := upBrowserIDE(ctx, f, tempDir, names.Flag(names.Recreate)) pid2 := state2.PID diff --git a/pkg/devcontainer/compose.go b/pkg/devcontainer/compose.go index 9dc768ec8..87ed16822 100644 --- a/pkg/devcontainer/compose.go +++ b/pkg/devcontainer/compose.go @@ -873,7 +873,7 @@ func (r *runner) recreateDevContainer( container *config.ContainerDetails, ) error { log.Debugf( - "Deleting dev container %s due to %s", + "deleting dev container %s due to %s", container.ID, names.Flag(names.Recreate), ) diff --git a/pkg/flags/builder.go b/pkg/flags/builder.go index 1933c7435..da1770be2 100644 --- a/pkg/flags/builder.go +++ b/pkg/flags/builder.go @@ -7,9 +7,6 @@ import ( flag "github.com/spf13/pflag" ) -// Flag is a self-contained flag definition: its name, default, usage text, and -// the destination it binds to. Definitions are attached to a command with Add, -// which keeps registration uniform and lets shared flags be declared once. type Flag interface { register(fs *flag.FlagSet) } @@ -31,7 +28,6 @@ func AddPersistent(cmd *cobra.Command, defs ...Flag) { } } -// options are the settings common to every flag kind, tweaked via With* chaining. type options struct { shorthand string hidden bool @@ -162,7 +158,7 @@ func (f *StringSliceFlag) register(fs *flag.FlagSet) { f.applyMeta(fs, f.name) } -// StringArrayFlag defines a repeatable (not comma-split) string array flag. +// StringArrayFlag defines a repeatable string array flag. type StringArrayFlag struct { dest *[]string name string @@ -171,8 +167,7 @@ type StringArrayFlag struct { options } -// StringArray binds a repeatable string array flag to dest (values are not -// comma-split, unlike StringSlice). +// StringArray binds a repeatable string array flag. func StringArray(dest *[]string, name string, def []string, usage string) *StringArrayFlag { return &StringArrayFlag{dest: dest, name: name, def: def, usage: usage} } diff --git a/pkg/flags/devcontainer.go b/pkg/flags/devcontainer.go index e692e9658..1fddac7e1 100644 --- a/pkg/flags/devcontainer.go +++ b/pkg/flags/devcontainer.go @@ -5,9 +5,6 @@ import ( flag "github.com/spf13/pflag" ) -// DevContainerModifierFlags holds the destinations a command binds the shared -// devcontainer modifier flags to. A nil pointer skips that flag, letting each -// command opt into only the modifiers it supports. type DevContainerModifierFlags struct { Image *string Features *string @@ -16,9 +13,6 @@ type DevContainerModifierFlags struct { ContainerDataFolder *string } -// RegisterDevContainerModifierFlags registers the devcontainer modifier flags -// shared across the up/build/exec commands so their names and descriptions -// stay in sync. func RegisterDevContainerModifierFlags(fs *flag.FlagSet, opts DevContainerModifierFlags) { if opts.Image != nil { fs.StringVar(opts.Image, names.DevContainerImage, "", diff --git a/pkg/flags/names/names.go b/pkg/flags/names/names.go index 22d3ce6f0..e7f8e5549 100644 --- a/pkg/flags/names/names.go +++ b/pkg/flags/names/names.go @@ -1,57 +1,67 @@ -// Package names is the single source of truth for devsy CLI flag names. -// -// Flags are registered in cmd/ but many are also used to re-invoke devsy from -// pkg/ (e.g. building a "devsy workspace ssh ..." command line). This package -// is a dependency-light leaf (imports only fmt) so both the cobra-coupled -// pkg/flags builder and the pkg/ command builders can import it without -// pulling in cobra. Defining each name once means a rename is a single edit the -// compiler propagates to every registration and construction site. -// -// Names are the bare flag name (no leading "--"). Use Flag or FlagTrue/FlagFalse -// to build a command-line token. -// -// Every name here is a devsy-owned CLI flag and can be renamed by editing its -// value once. Some names deliberately mirror an external tool or spec — docker -// build (--pull, --no-cache, --platform), podman (--userns, --uidmap, --gidmap), -// git (--git-recurse-submodules), or the devcontainer.json schema (--features, -// --user-env-probe, --update-remote-user-uid, --id-label). Renaming those is a -// UX/compatibility decision, not a free change. (The literal flags passed -// through to docker/podman themselves live at their construction sites, e.g. -// pkg/driver/docker, not here.) package names import "fmt" -// Flag renders a flag name as a command-line token, e.g. Flag(SSHGPGForwarding) -// -> "--ssh-gpg-forwarding". func Flag(name string) string { return "--" + name } -// FlagValue renders "--name=value". func FlagValue(name, value string) string { return fmt.Sprintf("--%s=%s", name, value) } -// FlagTrue renders "--name=true". func FlagTrue(name string) string { return FlagValue(name, "true") } -// FlagFalse renders "--name=false". func FlagFalse(name string) string { return FlagValue(name, "false") } -// Workspace up / build / exec — devcontainer source & modifiers. +// Workspace. const ( - DevContainer = "devcontainer" - DevContainerImage = "devcontainer-image" - DevContainerPath = "devcontainer-path" - DevContainerID = "devcontainer-id" - DevContainerOverlay = "devcontainer-overlay" - FallbackImage = "fallback-image" - Features = "features" - IDLabel = "id-label" - UserEnvProbe = "user-env-probe" - GPUAvailability = "gpu-availability" - UpdateRemoteUserUID = "update-remote-user-uid" - ContainerDataFolder = "container-data-folder" - MountWorkspaceGitRoot = "mount-workspace-git-root" - ContainerUser = "container-user" - RemoteUser = "remote-user" + CacheFrom = "cache-from" + ContainerDataFolder = "container-data-folder" + ContainerStatus = "container-status" + ContainerUser = "container-user" + Data = "data" + DevContainer = "devcontainer" + DevContainerID = "devcontainer-id" + DevContainerImage = "devcontainer-image" + DevContainerOverlay = "devcontainer-overlay" + DevContainerPath = "devcontainer-path" + DisableDaemon = "disable-daemon" + FallbackImage = "fallback-image" + Features = "features" + FeatureSecretsFile = "feature-secrets-file" + Force = "force" + GPUAvailability = "gpu-availability" + GracePeriod = "grace-period" + ID = "id" + IDLabel = "id-label" + IgnoreNotFound = "ignore-not-found" + InitEnv = "init-env" + Machine = "machine" + MachineID = "machine-id" + MachineReuse = "machine-reuse" + Mount = "mount" + MountWorkspaceGitRoot = "mount-workspace-git-root" + NoCache = "no-cache" + Platform = "platform" + Prebuild = "prebuild" + PrebuildRepo = "prebuild-repo" + ProviderID = "provider-id" + ProviderOption = "provider-option" + ProviderReuse = "provider-reuse" + Pull = "pull" + PullFromInsideContainer = "pull-from-inside-container" + Reconfigure = "reconfigure" + Recreate = "recreate" + RemoteUser = "remote-user" + RemoveVolumes = "remove-volumes" + Reset = "reset" + SecretsFile = "secrets-file" + Source = "source" + UpdateRemoteUserUID = "update-remote-user-uid" + UserEnvProbe = "user-env-probe" + WorkspaceEnv = "workspace-env" + WorkspaceEnvFile = "workspace-env-file" + WorkspaceFolder = "workspace-folder" + WorkspaceID = "workspace-id" + WorkspaceMountConsistency = "workspace-mount-consistency" + SkipPro = "skip-pro" ) // SSH. @@ -104,37 +114,6 @@ const ( TerminalRows = "terminal-rows" ) -// Workspace behavior & identity. -const ( - ID = "id" - Machine = "machine" - Source = "source" - ProviderOption = "provider-option" - Reconfigure = "reconfigure" - Prebuild = "prebuild" - Pull = "pull" - NoCache = "no-cache" - Recreate = "recreate" - Reset = "reset" - PrebuildRepo = "prebuild-repo" - WorkspaceEnv = "workspace-env" - WorkspaceEnvFile = "workspace-env-file" - SecretsFile = "secrets-file" - FeatureSecretsFile = "feature-secrets-file" - InitEnv = "init-env" - DisableDaemon = "disable-daemon" - CacheFrom = "cache-from" - WorkspaceMountConsistency = "workspace-mount-consistency" - Mount = "mount" - Platform = "platform" - PullFromInsideContainer = "pull-from-inside-container" - WorkspaceFolder = "workspace-folder" - IgnoreNotFound = "ignore-not-found" - GracePeriod = "grace-period" - Force = "force" - RemoveVolumes = "remove-volumes" -) - // IDE. const ( IDE = "ide" @@ -157,7 +136,7 @@ const ( RemoteEnv = "remote-env" ) -// Agent container setup (internal self-invocation). +// Agent container. const ( ContainerWorkspaceInfo = "container-workspace-info" ChownWorkspace = "chown-workspace" @@ -168,26 +147,14 @@ const ( PlatformHost = "platform-host" ) -// Dotfiles install (internal self-invocation). +// Dotfiles. const ( Repository = "repository" StrictHostKeyChecking = "strict-host-key-checking" InstallScript = "install-script" ) -// Workspace list / status / describe / import. -const ( - SkipPro = "skip-pro" - ContainerStatus = "container-status" - WorkspaceID = "workspace-id" - MachineID = "machine-id" - MachineReuse = "machine-reuse" - ProviderID = "provider-id" - ProviderReuse = "provider-reuse" - Data = "data" -) - -// Build command. +// Build. const ( SkipDelete = "skip-delete" Tag = "tag" @@ -202,13 +169,13 @@ const ( ForceInternalBuildKit = "force-internal-buildkit" ) -// Testing-only (hidden). +// Testing. const ( DaemonInterval = "daemon-interval" ForceDockerless = "force-dockerless" ) -// Common / global. +// Miscellaneous. const ( Debug = "debug" Command = "command" @@ -306,7 +273,7 @@ const ( Wait = "wait" ) -// Internal / agent commands (self-invocation, daemons, tunnels, servers). +// Internal / agent commands. const ( Address = "address" AuthSockID = "auth-sock-id" diff --git a/pkg/flags/names/names_test.go b/pkg/flags/names/names_test.go index 8f983ba67..79e52d973 100644 --- a/pkg/flags/names/names_test.go +++ b/pkg/flags/names/names_test.go @@ -6,11 +6,6 @@ import ( "github.com/devsy-org/devsy/pkg/flags/names" ) -// wireNames pins every flag-name constant to its literal command-line string. -// The literals here are written out independently of the constants so that any -// accidental change to a value in names.go breaks this test, forcing the wire -// change to be intentional. This is the source of truth for the CLI's flag-name -// contract and for devsy self-invocation command lines. var wireNames = []struct { got string want string diff --git a/pkg/gpg/signing.go b/pkg/gpg/signing.go index 2e3f072e2..6fca5887b 100644 --- a/pkg/gpg/signing.go +++ b/pkg/gpg/signing.go @@ -40,7 +40,7 @@ func SigningKey(ctx context.Context) string { } // GPG key IDs are hex fingerprints, not file paths. If the signing key - // looks like a file path and the format isn't x509 (which legitimately + // looks like a file path and the format is not x509 (which legitimately // uses certificate file paths via gpgsm), it's an SSH key. if (strings.HasPrefix(result, "/") || strings.HasPrefix(result, "~")) && formatStr != "x509" { log.Debugf("signing key %s looks like a file path, skipping", result) From dc84f9b889630e37cfcddfbac25a76a5306816c9 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 22 Jul 2026 13:46:02 -0500 Subject: [PATCH 7/7] style: format line --- pkg/flags/names/names.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/flags/names/names.go b/pkg/flags/names/names.go index e7f8e5549..08202c4bb 100644 --- a/pkg/flags/names/names.go +++ b/pkg/flags/names/names.go @@ -53,6 +53,7 @@ const ( RemoveVolumes = "remove-volumes" Reset = "reset" SecretsFile = "secrets-file" + SkipPro = "skip-pro" Source = "source" UpdateRemoteUserUID = "update-remote-user-uid" UserEnvProbe = "user-env-probe" @@ -61,7 +62,6 @@ const ( WorkspaceFolder = "workspace-folder" WorkspaceID = "workspace-id" WorkspaceMountConsistency = "workspace-mount-consistency" - SkipPro = "skip-pro" ) // SSH.