diff --git a/cli/autostart.go b/cli/autostart.go index fed05689e2..b3929cac9a 100644 --- a/cli/autostart.go +++ b/cli/autostart.go @@ -18,11 +18,12 @@ The default schedule is at 09:00 in your local timezone (TZ env, UTC by default) func autostart() *cobra.Command { autostartCmd := &cobra.Command{ - Use: "autostart enable ", - Short: "schedule a workspace to automatically start at a regular time", - Long: autostartDescriptionLong, - Example: "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin", - Hidden: true, + Annotations: workspaceCommand, + Use: "autostart enable ", + Short: "schedule a workspace to automatically start at a regular time", + Long: autostartDescriptionLong, + Example: "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin", + Hidden: true, } autostartCmd.AddCommand(autostartEnable()) diff --git a/cli/configssh.go b/cli/configssh.go index 5e32f8c779..d63892f97a 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -36,7 +36,9 @@ func configSSH() *cobra.Command { skipProxyCommand bool ) cmd := &cobra.Command{ - Use: "config-ssh", + Annotations: workspaceCommand, + Use: "config-ssh", + Short: "Populate your SSH config with Host entries for all of your workspaces", RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/create.go b/cli/create.go index e1d028511d..6a274afa4f 100644 --- a/cli/create.go +++ b/cli/create.go @@ -20,8 +20,9 @@ func create() *cobra.Command { templateName string ) cmd := &cobra.Command{ - Use: "create [name]", - Short: "Create a workspace from a template", + Annotations: workspaceCommand, + Use: "create [name]", + Short: "Create a workspace from a template", RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/delete.go b/cli/delete.go index 7fa183b2b1..0f795c06c5 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -13,9 +13,11 @@ import ( // nolint func delete() *cobra.Command { return &cobra.Command{ - Use: "delete ", - Aliases: []string{"rm"}, - Args: cobra.ExactArgs(1), + Annotations: workspaceCommand, + Use: "delete ", + Short: "Delete a workspace", + Aliases: []string{"rm"}, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/list.go b/cli/list.go index bb690b8c82..78bc106c62 100644 --- a/cli/list.go +++ b/cli/list.go @@ -13,8 +13,10 @@ import ( func list() *cobra.Command { return &cobra.Command{ - Use: "list", - Aliases: []string{"ls"}, + Annotations: workspaceCommand, + Use: "list", + Short: "List all workspaces", + Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/login.go b/cli/login.go index 7ef42a6e4e..908ec812ff 100644 --- a/cli/login.go +++ b/cli/login.go @@ -37,8 +37,9 @@ func init() { func login() *cobra.Command { return &cobra.Command{ - Use: "login ", - Args: cobra.ExactArgs(1), + Use: "login ", + Short: "Authenticate with a Coder deployment", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { rawURL := args[0] diff --git a/cli/parametercreate.go b/cli/parametercreate.go deleted file mode 100644 index 4cd285311f..0000000000 --- a/cli/parametercreate.go +++ /dev/null @@ -1,75 +0,0 @@ -package cli - -import ( - "fmt" - - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "github.com/coder/coder/coderd/database" - "github.com/coder/coder/codersdk" -) - -func parameterCreate() *cobra.Command { - var ( - name string - value string - scheme string - ) - cmd := &cobra.Command{ - Use: "create [name]", - Aliases: []string{"mk"}, - Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - organization, err := currentOrganization(cmd, client) - if err != nil { - return err - } - - scopeName := "" - if len(args) >= 2 { - scopeName = args[1] - } - scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], scopeName) - if err != nil { - return err - } - scheme, err := parseParameterScheme(scheme) - if err != nil { - return err - } - _, err = client.CreateParameter(cmd.Context(), scope, scopeID, codersdk.CreateParameterRequest{ - Name: name, - SourceValue: value, - SourceScheme: database.ParameterSourceSchemeData, - DestinationScheme: scheme, - }) - if err != nil { - return err - } - _, _ = fmt.Printf("Created!\n") - return nil - }, - } - cmd.Flags().StringVarP(&name, "name", "n", "", "Name for a parameter.") - _ = cmd.MarkFlagRequired("name") - cmd.Flags().StringVarP(&value, "value", "v", "", "Value for a parameter.") - _ = cmd.MarkFlagRequired("value") - cmd.Flags().StringVarP(&scheme, "scheme", "s", "var", `Scheme for the parameter ("var" or "env").`) - - return cmd -} - -func parseParameterScheme(scheme string) (database.ParameterDestinationScheme, error) { - switch scheme { - case "env": - return database.ParameterDestinationSchemeEnvironmentVariable, nil - case "var": - return database.ParameterDestinationSchemeProvisionerVariable, nil - } - return database.ParameterDestinationSchemeNone, xerrors.Errorf("scheme %q not recognized", scheme) -} diff --git a/cli/parameterdelete.go b/cli/parameterdelete.go deleted file mode 100644 index f30859a0a4..0000000000 --- a/cli/parameterdelete.go +++ /dev/null @@ -1,13 +0,0 @@ -package cli - -import "github.com/spf13/cobra" - -func parameterDelete() *cobra.Command { - return &cobra.Command{ - Use: "delete", - Aliases: []string{"rm"}, - RunE: func(cmd *cobra.Command, args []string) error { - return nil - }, - } -} diff --git a/cli/parameterlist.go b/cli/parameterlist.go deleted file mode 100644 index b1c57a16c1..0000000000 --- a/cli/parameterlist.go +++ /dev/null @@ -1,50 +0,0 @@ -package cli - -import ( - "fmt" - "text/tabwriter" - - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -func parameterList() *cobra.Command { - return &cobra.Command{ - Use: "list ", - RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - organization, err := currentOrganization(cmd, client) - if err != nil { - return err - } - - name := "" - if len(args) >= 2 { - name = args[1] - } - scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], name) - if err != nil { - return err - } - params, err := client.Parameters(cmd.Context(), scope, scopeID) - if err != nil { - return err - } - writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0) - _, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n", - color.HiBlackString("Parameter"), - color.HiBlackString("Created"), - color.HiBlackString("Scheme")) - for _, param := range params { - _, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n", - color.New(color.FgHiCyan).Sprint(param.Name), - color.WhiteString(param.UpdatedAt.Format("January 2, 2006")), - color.New(color.FgHiWhite).Sprint(param.DestinationScheme)) - } - return writer.Flush() - }, - } -} diff --git a/cli/parameters.go b/cli/parameters.go deleted file mode 100644 index a7f15719ee..0000000000 --- a/cli/parameters.go +++ /dev/null @@ -1,78 +0,0 @@ -package cli - -import ( - "context" - - "github.com/google/uuid" - "github.com/spf13/cobra" - "golang.org/x/xerrors" - - "github.com/coder/coder/codersdk" -) - -func parameters() *cobra.Command { - cmd := &cobra.Command{ - Use: "parameters", - Aliases: []string{"params"}, - } - - cmd.AddCommand(parameterCreate(), parameterList(), parameterDelete()) - - return cmd -} - -func parseScopeAndID(ctx context.Context, client *codersdk.Client, organization codersdk.Organization, rawScope string, name string) (codersdk.ParameterScope, uuid.UUID, error) { - scope, err := parseParameterScope(rawScope) - if err != nil { - return scope, uuid.Nil, err - } - - var scopeID uuid.UUID - switch scope { - case codersdk.ParameterOrganization: - if name == "" { - scopeID = organization.ID - } else { - org, err := client.OrganizationByName(ctx, codersdk.Me, name) - if err != nil { - return scope, uuid.Nil, err - } - scopeID = org.ID - } - case codersdk.ParameterTemplate: - template, err := client.TemplateByName(ctx, organization.ID, name) - if err != nil { - return scope, uuid.Nil, err - } - scopeID = template.ID - case codersdk.ParameterUser: - uid, _ := uuid.Parse(name) - user, err := client.User(ctx, uid) - if err != nil { - return scope, uuid.Nil, err - } - scopeID = user.ID - case codersdk.ParameterWorkspace: - workspace, err := client.WorkspaceByOwnerAndName(ctx, organization.ID, codersdk.Me, name) - if err != nil { - return scope, uuid.Nil, err - } - scopeID = workspace.ID - } - - return scope, scopeID, nil -} - -func parseParameterScope(scope string) (codersdk.ParameterScope, error) { - switch scope { - case string(codersdk.ParameterOrganization): - return codersdk.ParameterOrganization, nil - case string(codersdk.ParameterTemplate): - return codersdk.ParameterTemplate, nil - case string(codersdk.ParameterUser): - return codersdk.ParameterUser, nil - case string(codersdk.ParameterWorkspace): - return codersdk.ParameterWorkspace, nil - } - return codersdk.ParameterOrganization, xerrors.Errorf("no scope found by name %q", scope) -} diff --git a/cli/publickey.go b/cli/publickey.go index d8d59a292c..ad6c016b9f 100644 --- a/cli/publickey.go +++ b/cli/publickey.go @@ -14,6 +14,7 @@ func publickey() *cobra.Command { return &cobra.Command{ Use: "publickey", Aliases: []string{"pubkey"}, + Short: "Output your public key for Git operations", RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/root.go b/cli/root.go index 4a38d5895b..97c6f121cc 100644 --- a/cli/root.go +++ b/cli/root.go @@ -3,10 +3,8 @@ package cli import ( "net/url" "os" - "strings" "time" - "github.com/charmbracelet/lipgloss" "github.com/kirsle/configdir" "github.com/mattn/go-isatty" "github.com/spf13/cobra" @@ -20,6 +18,12 @@ import ( var ( caret = cliui.Styles.Prompt.String() + + // Applied as annotations to workspace commands + // so they display in a separated "help" section. + workspaceCommand = map[string]string{ + "workspaces": " ", + } ) const ( @@ -38,33 +42,14 @@ func Root() *cobra.Command { Version: buildinfo.Version(), SilenceErrors: true, SilenceUsage: true, - Long: ` ▄█▀ ▀█▄ - ▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█ - ▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀ -█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █ - ██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀ - ` + lipgloss.NewStyle().Underline(true).Render("Self-hosted developer workspaces on your infra") + ` - + Long: `Coder — A tool for provisioning self-hosted development environments. `, - Example: cliui.Styles.Paragraph.Render(`Start Coder in "dev" mode. This dev-mode requires no further setup, and your local `+cliui.Styles.Code.Render("coder")+` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.`) + ` + Example: ` Start Coder in "dev" mode. This dev-mode requires no further setup, and your local ` + cliui.Styles.Code.Render("coder") + ` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder. + ` + cliui.Styles.Code.Render("$ coder server --dev") + ` - ` + cliui.Styles.Code.Render("$ coder server --dev") + ` - ` + cliui.Styles.Paragraph.Render("Get started by creating a template from an example.") + ` - - ` + cliui.Styles.Code.Render("$ coder templates init"), + Get started by creating a template from an example. + ` + cliui.Styles.Code.Render("$ coder templates init"), } - // Customizes the color of headings to make subcommands - // more visually appealing. - header := cliui.Styles.Placeholder - cmd.SetUsageTemplate(strings.NewReplacer( - `Usage:`, header.Render("Usage:"), - `Examples:`, header.Render("Examples:"), - `Available Commands:`, header.Render("Commands:"), - `Global Flags:`, header.Render("Global Flags:"), - `Flags:`, header.Render("Flags:"), - `Additional help topics:`, header.Render("Additional help:"), - ).Replace(cmd.UsageTemplate())) - cmd.SetVersionTemplate(versionTemplate()) cmd.AddCommand( autostart(), @@ -75,7 +60,6 @@ func Root() *cobra.Command { gitssh(), list(), login(), - parameters(), publickey(), server(), show(), @@ -90,6 +74,9 @@ func Root() *cobra.Command { workspaceAgent(), ) + cmd.SetUsageTemplate(usageTemplate()) + cmd.SetVersionTemplate(versionTemplate()) + cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.") cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.") cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.") @@ -188,6 +175,27 @@ func isTTY(cmd *cobra.Command) bool { return isatty.IsTerminal(file.Fd()) } +func usageTemplate() string { + // Customizes the color of headings to make subcommands + // more visually appealing. + header := cliui.Styles.Placeholder + + return `{{if .HasExample}}` + header.Render("Get Started:") + ` +{{.Example}} + +{{end}}{{if .HasAvailableLocalFlags}}` + header.Render("Flags:") + ` +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableSubCommands}} + +` + header.Render("Commands:") + `{{range .Commands}}{{if and .IsAvailableCommand (eq (len .Annotations) 0)}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .HasParent }} + +` + header.Render("Workspace Commands:") + `{{range .Commands}}{{if and .IsAvailableCommand (ne (index .Annotations "workspaces") "")}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}} + +Use "{{.CommandPath}} [command] --help" for more information about a command. +` +} + func versionTemplate() string { template := `Coder {{printf "%s" .Version}}` buildTime, valid := buildinfo.Time() diff --git a/cli/server.go b/cli/server.go index 8a6dc99492..0188394ccd 100644 --- a/cli/server.go +++ b/cli/server.go @@ -89,7 +89,8 @@ func server() *cobra.Command { ) root := &cobra.Command{ - Use: "server", + Use: "server", + Short: "Start a Coder server", RunE: func(cmd *cobra.Command, args []string) error { logger := slog.Make(sloghuman.Sink(os.Stderr)) if verbose { diff --git a/cli/show.go b/cli/show.go index 9b8a67abe4..2c51de16b6 100644 --- a/cli/show.go +++ b/cli/show.go @@ -10,8 +10,10 @@ import ( func show() *cobra.Command { return &cobra.Command{ - Use: "show", - Args: cobra.ExactArgs(1), + Annotations: workspaceCommand, + Use: "show", + Short: "Show details of a workspace's resources and agents", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/ssh.go b/cli/ssh.go index 5595143968..937b64226e 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -26,8 +26,10 @@ func ssh() *cobra.Command { stdio bool ) cmd := &cobra.Command{ - Use: "ssh ", - Args: cobra.MinimumNArgs(1), + Annotations: workspaceCommand, + Use: "ssh ", + Short: "SSH into a workspace", + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/start.go b/cli/start.go index 52a5d25f40..223731045e 100644 --- a/cli/start.go +++ b/cli/start.go @@ -12,8 +12,10 @@ import ( func start() *cobra.Command { return &cobra.Command{ - Use: "start ", - Args: cobra.ExactArgs(1), + Annotations: workspaceCommand, + Use: "start ", + Short: "Build a workspace with the start state", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/state.go b/cli/state.go index 4f8ea345f0..ec43a27115 100644 --- a/cli/state.go +++ b/cli/state.go @@ -13,7 +13,8 @@ import ( func state() *cobra.Command { cmd := &cobra.Command{ - Use: "state", + Use: "state", + Short: "Manually manage Terraform state to fix broken workspaces", } cmd.AddCommand(statePull(), statePush()) return cmd diff --git a/cli/stop.go b/cli/stop.go index f0c99539af..1125901608 100644 --- a/cli/stop.go +++ b/cli/stop.go @@ -12,8 +12,10 @@ import ( func stop() *cobra.Command { return &cobra.Command{ - Use: "stop ", - Args: cobra.ExactArgs(1), + Annotations: workspaceCommand, + Use: "stop ", + Short: "Build a workspace with the stop state", + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/templates.go b/cli/templates.go index 9ec6dbef10..5341df1d1c 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -8,6 +8,7 @@ import ( func templates() *cobra.Command { cmd := &cobra.Command{ Use: "templates", + Short: "Create, manage, and deploy templates", Aliases: []string{"template"}, Example: ` - Create a template for developers to create workspaces diff --git a/cli/tunnel.go b/cli/tunnel.go index 7f50abcd7d..887d766a09 100644 --- a/cli/tunnel.go +++ b/cli/tunnel.go @@ -4,7 +4,9 @@ import "github.com/spf13/cobra" func tunnel() *cobra.Command { return &cobra.Command{ - Use: "tunnel", + Annotations: workspaceCommand, + Use: "tunnel", + Short: "Forward ports to your local machine", RunE: func(cmd *cobra.Command, args []string) error { return nil }, diff --git a/cli/update.go b/cli/update.go index c3de8c2a36..f444ac441c 100644 --- a/cli/update.go +++ b/cli/update.go @@ -11,7 +11,9 @@ import ( func update() *cobra.Command { return &cobra.Command{ - Use: "update", + Annotations: workspaceCommand, + Use: "update", + Short: "Update a workspace to the latest template version", RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { diff --git a/cli/users.go b/cli/users.go index 9ffabfae0b..dcc03c8ddf 100644 --- a/cli/users.go +++ b/cli/users.go @@ -4,7 +4,8 @@ import "github.com/spf13/cobra" func users() *cobra.Command { cmd := &cobra.Command{ - Use: "users", + Short: "Create, remove, and list users", + Use: "users", } cmd.AddCommand(userCreate(), userList()) return cmd