From d3cd70fb4e808eea7e5263df6c2778fad1fc1299 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 28 Jul 2026 23:21:42 -0500 Subject: [PATCH 1/5] feat(cli): redesign help output with Devsy purple accent Replace cobra's default help with a renderer that reads top-down: uppercase section headers, grouped subcommand listings, and flags shown above their wrapped descriptions rather than crammed into a single column. Flag names and environment variables carry the Devsy purple accent so the two things a reader scans for stand out. This moves the env var out of the usage string and onto a pflag annotation, letting it be styled independently of the description text. --- cmd/env_flags_test.go | 8 +- cmd/root.go | 37 ++++- pkg/clihelp/help.go | 345 +++++++++++++++++++++++++++++++++++++++ pkg/clihelp/help_test.go | 149 +++++++++++++++++ pkg/flags/env.go | 16 +- pkg/theme/theme.go | 29 ++++ 6 files changed, 569 insertions(+), 15 deletions(-) create mode 100644 pkg/clihelp/help.go create mode 100644 pkg/clihelp/help_test.go create mode 100644 pkg/theme/theme.go diff --git a/cmd/env_flags_test.go b/cmd/env_flags_test.go index ca293f62f..daf7ba61c 100644 --- a/cmd/env_flags_test.go +++ b/cmd/env_flags_test.go @@ -4,6 +4,7 @@ import ( "strings" "testing" + pkgflags "github.com/devsy-org/devsy/pkg/flags" "github.com/devsy-org/devsy/pkg/flags/names" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" @@ -68,7 +69,12 @@ func TestOptInEnvFlags_AppliesEnvValueToFlag(t *testing.T) { require.NotNil(t, f, "flag --%s not found on %q", tc.flagName, tc.cmdPath) assert.Equal(t, tc.want, f.Value.String()) assert.True(t, f.Changed, "Changed must be true so MarkFlagRequired passes") - assert.Contains(t, f.Usage, tc.envName, "usage should advertise env var") + assert.Equal( + t, + []string{tc.envName}, + f.Annotations[pkgflags.EnvAnnotation], + "env var should be annotated for the help renderer", + ) }) } } diff --git a/cmd/root.go b/cmd/root.go index 7a166abda..a1c33f4ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,6 +27,7 @@ import ( "github.com/devsy-org/devsy/cmd/template" wsCmdPkg "github.com/devsy-org/devsy/cmd/workspace" "github.com/devsy-org/devsy/pkg/clierr" + "github.com/devsy-org/devsy/pkg/clihelp" "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/exitcode" "github.com/devsy-org/devsy/pkg/flags/names" @@ -62,6 +63,21 @@ const ( envProEnabled = "DEVSY_PRO_ENABLED" internalCommand = "internal" + + rootLong = "Devsy — standardized development workspaces built on devcontainers, " + + "running on Docker, Kubernetes, cloud providers, and SSH remote hosts." + + rootExample = `- Start a workspace from the current directory: + + $ devsy workspace up . + +- Open an SSH session to a workspace: + + $ devsy workspace ssh my-workspace + +- Configure a provider: + + $ devsy provider add docker` ) func proEnabled() bool { @@ -250,13 +266,17 @@ func renderCLIError(cliErr *clierr.CLIError, machineMode bool) { // without reaching for package-level mutable state. func BuildRoot() (*cobra.Command, *flags.GlobalFlags) { rootCmd := &cobra.Command{ - Use: config.BinaryName, - Short: "Devsy", - Version: version.GetVersion(), + Use: config.BinaryName, + Short: "Devsy", + Long: rootLong, + Example: rootExample, + Version: version.GetVersion(), + SilenceUsage: true, SilenceErrors: true, } rootCmd.SetVersionTemplate("{{.Version}}\n") + clihelp.Install(rootCmd) persistentFlags := rootCmd.PersistentFlags() globalFlags := flags.SetGlobalFlags(persistentFlags) _ = completion.RegisterFlagCompletionFuns(rootCmd, globalFlags) @@ -293,6 +313,13 @@ func BuildRoot() (*cobra.Command, *flags.GlobalFlags) { return nil } + registerGroups(rootCmd) + registerSubcommands(rootCmd, globalFlags) + + return rootCmd, globalFlags +} + +func registerGroups(rootCmd *cobra.Command) { groups := []*cobra.Group{ {ID: groupCore, Title: "Core commands:"}, {ID: groupConfig, Title: "Configuration commands:"}, @@ -303,10 +330,6 @@ func BuildRoot() (*cobra.Command, *flags.GlobalFlags) { groups = append(groups, &cobra.Group{ID: groupPlatform, Title: "Platform commands:"}) } rootCmd.AddGroup(groups...) - - registerSubcommands(rootCmd, globalFlags) - - return rootCmd, globalFlags } func registerSubcommands(rootCmd *cobra.Command, globalFlags *flags.GlobalFlags) { diff --git a/pkg/clihelp/help.go b/pkg/clihelp/help.go new file mode 100644 index 000000000..dd4c3d57b --- /dev/null +++ b/pkg/clihelp/help.go @@ -0,0 +1,345 @@ +// Package clihelp renders `devsy --help` output: uppercase section headers, +// grouped subcommand listings, and flags shown above their wrapped descriptions +// with the Devsy purple accent on flag names and environment variables. +package clihelp + +import ( + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/charmbracelet/colorprofile" + "github.com/charmbracelet/x/ansi" + "github.com/devsy-org/devsy/pkg/flags" + "github.com/devsy-org/devsy/pkg/theme" + "github.com/spf13/cobra" + pflag "github.com/spf13/pflag" + "golang.org/x/term" +) + +const ( + // indent is the left margin for entries inside a section. + indent = " " + // descIndent is the left margin for a flag's description line, set one + // level deeper than the flag itself so the two read as a pair. + descIndent = " " + // nameColumn is the width reserved for a subcommand name before its + // short description. Devsy's longest command name is well inside this. + nameColumn = 12 + // defaultWidth is the wrap width used when stdout is not a terminal. + defaultWidth = 80 + // maxWidth keeps prose readable on very wide terminals. + maxWidth = 100 + // minWidth is a floor so wrapping never degenerates on tiny terminals. + minWidth = 40 +) + +// Install makes this renderer the help and usage output for cmd and every +// command beneath it. Cobra propagates help/usage functions to children, so a +// single call on the root command covers the whole tree. +func Install(cmd *cobra.Command) { + cmd.SetHelpFunc(func(c *cobra.Command, _ []string) { + render(c, c.OutOrStdout()) + }) + cmd.SetUsageFunc(func(c *cobra.Command) error { + render(c, c.ErrOrStderr()) + return nil + }) +} + +// render writes the full help body for cmd to out, downsampling color to +// whatever the destination supports (and stripping it entirely when the output +// is redirected or NO_COLOR is set). +func render(cmd *cobra.Command, out io.Writer) { + // os.Environ() is passed explicitly: colorprofile.NewWriter documents a nil + // environ as meaning os.Environ(), but v0.4.3 builds an empty map instead, + // so TERM reads as unset and every profile collapses to NoTTY (no color). + w := colorprofile.NewWriter(out, os.Environ()) + width := wrapWidth(out) + + var b strings.Builder + writeIntro(&b, cmd, width) + writeUsage(&b, cmd) + writeCommands(&b, cmd, width) + writeFlags(&b, cmd, width) + writeFooter(&b, cmd) + + _, _ = io.WriteString(w, b.String()) +} + +// wrapWidth returns the column at which prose should wrap, based on the +// terminal size when out is a terminal. +func wrapWidth(out io.Writer) int { + f, ok := out.(interface{ Fd() uintptr }) + if !ok { + return defaultWidth + } + cols, _, err := term.GetSize(int(f.Fd())) //nolint:gosec // fd fits in int + if err != nil || cols <= 0 { + return defaultWidth + } + return clamp(cols, minWidth, maxWidth) +} + +func clamp(v, low, high int) int { + if v < low { + return low + } + if v > high { + return high + } + return v +} + +// writeIntro emits the command's long description, or its short one when no +// long form is set. +func writeIntro(b *strings.Builder, cmd *cobra.Command, width int) { + desc := cmd.Long + if desc == "" { + desc = cmd.Short + } + if desc == "" { + return + } + b.WriteString(wrapIndent(desc, width, "")) + b.WriteString("\n\n") +} + +// writeUsage emits the USAGE section, plus any Example block the command +// carries. +func writeUsage(b *strings.Builder, cmd *cobra.Command) { + section(b, "USAGE") + if cmd.Runnable() { + b.WriteString(indent) + b.WriteString(cmd.UseLine()) + b.WriteString("\n") + } + if cmd.HasAvailableSubCommands() { + b.WriteString(indent) + b.WriteString(cmd.CommandPath()) + b.WriteString(" [global-flags] \n") + } + if cmd.Example != "" { + b.WriteString("\n") + b.WriteString(indentBlock(strings.TrimRight(cmd.Example, "\n"), indent)) + b.WriteString("\n") + } + b.WriteString("\n") +} + +// writeCommands lists available subcommands, preserving the command groups +// declared on the root command and collecting ungrouped ones under a trailing +// section. +func writeCommands(b *strings.Builder, cmd *cobra.Command, width int) { + if !cmd.HasAvailableSubCommands() { + return + } + + grouped, ungrouped := partitionCommands(cmd) + + for _, group := range cmd.Groups() { + subs := grouped[group.ID] + if len(subs) == 0 { + continue + } + section(b, sectionTitle(group.Title)) + writeCommandList(b, subs, width) + b.WriteString("\n") + } + + if len(ungrouped) > 0 { + title := "SUBCOMMANDS" + if len(grouped) > 0 { + title = "ADDITIONAL COMMANDS" + } + section(b, title) + writeCommandList(b, ungrouped, width) + b.WriteString("\n") + } +} + +// partitionCommands splits cmd's visible subcommands into those belonging to a +// declared group, keyed by group ID, and those without one. +func partitionCommands(cmd *cobra.Command) (map[string][]*cobra.Command, []*cobra.Command) { + grouped := map[string][]*cobra.Command{} + var ungrouped []*cobra.Command + for _, sub := range cmd.Commands() { + if !sub.IsAvailableCommand() && !sub.IsAdditionalHelpTopicCommand() { + continue + } + if sub.GroupID != "" && cmd.ContainsGroup(sub.GroupID) { + grouped[sub.GroupID] = append(grouped[sub.GroupID], sub) + continue + } + ungrouped = append(ungrouped, sub) + } + return grouped, ungrouped +} + +// sectionTitle normalizes a cobra group title ("Core commands:") into a section +// header ("CORE COMMANDS"). +func sectionTitle(groupTitle string) string { + return strings.ToUpper(strings.TrimRight(strings.TrimSpace(groupTitle), ":")) +} + +func writeCommandList(b *strings.Builder, subs []*cobra.Command, width int) { + sort.Slice(subs, func(i, j int) bool { return subs[i].Name() < subs[j].Name() }) + for _, sub := range subs { + name := sub.Name() + b.WriteString(indent) + b.WriteString(theme.Command.Render(name)) + if sub.Short == "" { + b.WriteString("\n") + continue + } + // Pad from the plain name — the styled string carries escape bytes + // that would otherwise be counted as visible columns. + b.WriteString(strings.Repeat(" ", max(1, nameColumn-len(name)))) + descCol := len(indent) + nameColumn + b.WriteString(wrapHanging(sub.Short, width, descCol)) + b.WriteString("\n") + } +} + +// writeFlags emits the command's own flags under OPTIONS and the global flags +// under GLOBAL OPTIONS. Globals are the ones this command declares as +// persistent (on the root) plus everything inherited from ancestors — both +// apply to subcommands, so they belong in the same section. +func writeFlags(b *strings.Builder, cmd *cobra.Command, width int) { + persistent := cmd.PersistentFlags() + var local, global []*pflag.Flag + cmd.NonInheritedFlags().VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + if persistent.Lookup(f.Name) != nil { + global = append(global, f) + return + } + local = append(local, f) + }) + cmd.InheritedFlags().VisitAll(func(f *pflag.Flag) { + if !f.Hidden { + global = append(global, f) + } + }) + + if len(local) > 0 { + section(b, "OPTIONS") + writeFlagList(b, local, width) + } + if len(global) > 0 { + section(b, "GLOBAL OPTIONS") + b.WriteString(wrapIndent( + "Global options apply to every subcommand. Each may be set by flag "+ + "or by its environment variable.", + width, indent, + )) + b.WriteString("\n\n") + writeFlagList(b, global, width) + } +} + +// writeFlagList renders each flag as a signature line — name, value type, env +// var, default — followed by its indented, wrapped description. +func writeFlagList(b *strings.Builder, list []*pflag.Flag, width int) { + sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) + for _, f := range list { + b.WriteString(indent) + b.WriteString(flagSignature(f)) + b.WriteString("\n") + if usage := flagUsage(f); usage != "" { + b.WriteString(wrapIndent(usage, width, descIndent)) + b.WriteString("\n") + } + b.WriteString("\n") + } +} + +// flagSignature builds the styled first line for a flag, e.g. +// "-v, --verbose count, $DEVSY_VERBOSE (default: 0)". +func flagSignature(f *pflag.Flag) string { + var parts []string + if f.Shorthand != "" { + parts = append(parts, theme.Flag.Render("-"+f.Shorthand)) + } + name := theme.Flag.Render("--" + f.Name) + if valueType, _ := pflag.UnquoteUsage(f); valueType != "" { + name += " " + theme.Muted.Render(valueType) + } + parts = append(parts, name) + + sig := strings.Join(parts, ", ") + if env := f.Annotations[flags.EnvAnnotation]; len(env) > 0 { + sig += theme.Muted.Render(", ") + theme.EnvVar.Render("$"+env[0]) + } + if def := defaultText(f); def != "" { + sig += " " + theme.Muted.Render(def) + } + return sig +} + +// defaultText renders a flag's default as "(default: x)", omitting zero values +// that carry no information. +func defaultText(f *pflag.Flag) string { + switch f.DefValue { + case "", "false", "0", "[]", "0s": + return "" + } + return fmt.Sprintf("(default: %s)", f.DefValue) +} + +// flagUsage returns a flag's description with the value-type backquotes that +// pflag uses for naming stripped out. +func flagUsage(f *pflag.Flag) string { + _, usage := pflag.UnquoteUsage(f) + return strings.TrimSpace(usage) +} + +func writeFooter(b *strings.Builder, cmd *cobra.Command) { + if !cmd.HasAvailableSubCommands() { + return + } + b.WriteString(theme.Muted.Render(fmt.Sprintf( + "Run %q for more information about a subcommand.", + cmd.CommandPath()+" --help", + ))) + b.WriteString("\n") +} + +func section(b *strings.Builder, title string) { + b.WriteString(theme.Heading.Render(title + ":")) + b.WriteString("\n") +} + +// wrapIndent wraps text to width and prefixes every resulting line with prefix. +func wrapIndent(text string, width int, prefix string) string { + limit := max(minWidth/2, width-len(prefix)) + wrapped := ansi.Wordwrap(strings.TrimSpace(text), limit, " -") + return indentBlock(wrapped, prefix) +} + +// wrapHanging wraps text to width assuming the first line already begins at +// column col, indenting continuation lines to line up under it. +func wrapHanging(text string, width, col int) string { + limit := max(minWidth/2, width-col) + lines := strings.Split(ansi.Wordwrap(strings.TrimSpace(text), limit, " -"), "\n") + pad := strings.Repeat(" ", col) + for i := 1; i < len(lines); i++ { + lines[i] = pad + lines[i] + } + return strings.Join(lines, "\n") +} + +// indentBlock prefixes every non-empty line of text with prefix. +func indentBlock(text, prefix string) string { + lines := strings.Split(text, "\n") + for i, line := range lines { + if strings.TrimSpace(line) != "" { + lines[i] = prefix + line + } + } + return strings.Join(lines, "\n") +} diff --git a/pkg/clihelp/help_test.go b/pkg/clihelp/help_test.go new file mode 100644 index 000000000..7f72330a1 --- /dev/null +++ b/pkg/clihelp/help_test.go @@ -0,0 +1,149 @@ +package clihelp + +import ( + "bytes" + "strings" + "testing" + + pkgflags "github.com/devsy-org/devsy/pkg/flags" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newTestRoot builds a small command tree that mirrors the real one: grouped +// subcommands, a persistent flag with an env var, and a local flag on a leaf. +func newTestRoot(t *testing.T) *cobra.Command { + t.Helper() + root := &cobra.Command{Use: "devsy", Short: "Devsy", Long: "Devsy long description."} + root.AddGroup(&cobra.Group{ID: "core", Title: "Core commands:"}) + + pf := root.PersistentFlags() + pf.String("provider", "", "The provider to use") + pkgflags.BindEnv(pf, "provider") + pf.Bool("hidden-global", false, "should not appear") + require.NoError(t, pf.MarkHidden("hidden-global")) + + noop := func(*cobra.Command, []string) {} + ws := &cobra.Command{Use: "workspace", Short: "Manage workspaces", GroupID: "core"} + list := &cobra.Command{Use: "list", Short: "List workspaces", Run: noop} + list.Flags().Bool("skip-pro", false, "Don't list pro workspaces") + ws.AddCommand(list) + root.AddCommand(ws) + root.AddCommand(&cobra.Command{Use: "loose", Short: "Ungrouped command", Run: noop}) + + Install(root) + return root +} + +// renderHelp captures help output for the command at path. Output goes to a +// bytes.Buffer, which is not a terminal, so colorprofile strips all styling and +// assertions can match plain text. +func renderHelp(t *testing.T, root *cobra.Command, path ...string) string { + t.Helper() + target := root + if len(path) > 0 { + found, _, err := root.Find(path) + require.NoError(t, err) + target = found + } + var buf bytes.Buffer + target.SetOut(&buf) + target.SetErr(&buf) + require.NoError(t, target.Help()) + return buf.String() +} + +func TestRender_RootSectionsAndGroups(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + + assert.Contains(t, out, "Devsy long description.") + assert.Contains(t, out, "USAGE:") + assert.Contains(t, out, "devsy [global-flags] ") + assert.Contains(t, out, "CORE COMMANDS:") + assert.Contains(t, out, "workspace") + // Commands with no group land in a trailing section, not the group listing. + assert.Contains(t, out, "ADDITIONAL COMMANDS:") + assert.Contains(t, out, "loose") + assert.Contains(t, out, "GLOBAL OPTIONS:") + assert.Contains(t, out, "Run \"devsy --help\"") +} + +// The env var belongs on the flag's signature line, beside the flag name, so it +// can carry the purple accent independently of the description text. +func TestRender_EnvVarOnSignatureLine(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + + require.Contains(t, out, "--provider string, $DEVSY_PROVIDER") + line := signatureLine(t, out, "--provider") + assert.NotContains(t, line, "The provider to use", "description belongs on its own line") +} + +func TestRender_HidesHiddenFlags(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + assert.NotContains(t, out, "hidden-global") +} + +// A root command's own persistent flags are global to the whole tree, so they +// must be listed under GLOBAL OPTIONS rather than the root's local OPTIONS. +func TestRender_RootPersistentFlagsAreGlobal(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + assert.Contains(t, sectionBody(t, out, "GLOBAL OPTIONS:"), "--provider") +} + +func TestRender_LeafSeparatesLocalFromGlobal(t *testing.T) { + out := renderHelp(t, newTestRoot(t), "workspace", "list") + + assert.Contains(t, out, "devsy workspace list [flags]") + assert.Contains(t, sectionBody(t, out, "OPTIONS:"), "--skip-pro") + assert.Contains(t, sectionBody(t, out, "GLOBAL OPTIONS:"), "--provider") + // A leaf has no subcommands, so the footer hint would be a dead end. + assert.NotContains(t, out, "for more information about a subcommand") +} + +// Defaults that carry no information are omitted so signature lines stay scannable. +func TestDefaultText_OmitsZeroValues(t *testing.T) { + root := &cobra.Command{Use: "x", Run: func(*cobra.Command, []string) {}} + fs := root.Flags() + fs.String("empty", "", "empty default") + fs.Bool("off", false, "false default") + fs.Int("zero", 0, "zero default") + fs.String("set", "text", "real default") + Install(root) + + out := renderHelp(t, root) + for _, flagName := range []string{"--empty", "--off", "--zero"} { + assert.NotContains(t, signatureLine(t, out, flagName), "(default:") + } + assert.Contains(t, signatureLine(t, out, "--set"), "(default: text)") +} + +func TestRender_NoColorWhenNotATerminal(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + assert.NotContains(t, out, "\x1b[", "styling must be stripped for non-terminal output") +} + +// signatureLine returns the rendered line containing the given flag name. +func signatureLine(t *testing.T, out, flagName string) string { + t.Helper() + for line := range strings.SplitSeq(out, "\n") { + if strings.Contains(line, flagName) { + return line + } + } + t.Fatalf("no line containing %q in:\n%s", flagName, out) + return "" +} + +// section returns the body of a named section, up to the next section header. +func sectionBody(t *testing.T, out, header string) string { + t.Helper() + _, rest, found := strings.Cut(out, header) + require.True(t, found, "section %q not found in:\n%s", header, out) + for _, next := range []string{"OPTIONS:", "GLOBAL OPTIONS:", "COMMANDS:"} { + if body, _, cut := strings.Cut(rest, next); cut { + rest = body + } + } + return rest +} diff --git a/pkg/flags/env.go b/pkg/flags/env.go index 9d8c7bf90..bf0f0bd03 100644 --- a/pkg/flags/env.go +++ b/pkg/flags/env.go @@ -18,10 +18,15 @@ func EnvName(flagName string) string { return config.EnvPrefix + strings.ToUpper(strings.ReplaceAll(flagName, "-", "_")) } +// EnvAnnotation is the pflag annotation key under which BindEnv records a +// flag's canonical env var name. The help renderer reads it to display the env +// var beside the flag instead of burying it in the description text. +const EnvAnnotation = "devsy_env" + // BindEnv wires the canonical DEVSY_* env var into a cobra flag. Call it // immediately after the flag's *Var registration. If the env var is set, the // value is applied to the flag (Changed=true, satisfying MarkFlagRequired) and -// the usage string advertises the env var. +// the env var is recorded under [EnvAnnotation] for the help renderer. // // Failure modes are loud by design: // - panics if the flag is not registered (programmer bug at startup); @@ -35,13 +40,10 @@ func BindEnv(fs *pflag.FlagSet, flagName string) { panic("flags.BindEnv: flag --" + flagName + " is not registered") } envName := EnvName(flagName) - // "[$DEVSY_FOO]" — $VAR convention, bracket avoids clashing with - // parentheses inside the existing description. - suffix := " [$" + envName + "]" - desc := strings.TrimRight(f.Usage, " .") - if !strings.HasSuffix(desc, suffix) { - f.Usage = desc + suffix + if f.Annotations == nil { + f.Annotations = map[string][]string{} } + f.Annotations[EnvAnnotation] = []string{envName} value, ok := os.LookupEnv(envName) if !ok || value == "" { return diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go new file mode 100644 index 000000000..8767be4cd --- /dev/null +++ b/pkg/theme/theme.go @@ -0,0 +1,29 @@ +// Package theme holds the Devsy CLI color palette and the text styles built +// from it, so terminal output stays consistent with the desktop app. +package theme + +import "charm.land/lipgloss/v2" + +// Accent is the Devsy purple, matching the desktop app's `.theme-purple` +// primary (hsl(252 100% 68%)). It is a single tone rather than a light/dark +// pair because resolving a pair requires querying the terminal background, +// which needs raw mode — too costly for a `--help` render. This tone clears +// 4.3:1 contrast against both black and white backgrounds. +var Accent = lipgloss.Color("#7C5CFF") + +var ( + // Heading styles section headers such as "GLOBAL OPTIONS:". + Heading = lipgloss.NewStyle().Bold(true) + + // Flag styles a flag name, e.g. "--provider". + Flag = lipgloss.NewStyle().Foreground(Accent).Bold(true) + + // EnvVar styles a flag's environment variable, e.g. "$DEVSY_PROVIDER". + EnvVar = lipgloss.NewStyle().Foreground(Accent) + + // Command styles a subcommand name in a command listing. + Command = lipgloss.NewStyle().Bold(true) + + // Muted styles secondary detail: value types, defaults, footer hints. + Muted = lipgloss.NewStyle().Faint(true) +) From 1369d8655698fd1ee9885d314d40d249abbc30df Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 28 Jul 2026 23:32:54 -0500 Subject: [PATCH 2/5] refactor(cli): flatten help listing and drop redundant "Devsy" prefix List every subcommand in one alphabetical SUBCOMMANDS section. At 14 top-level commands the grouped variant spent four headers and three blank lines on a list that fits on one screen either way. This retires the cobra command groups, which existed only to drive those headers, collapsing registerSubcommands to a single AddCommand call. Also rewrite the "Devsy X commands" descriptions as verb phrases. The binary name is already on every line the reader is scanning, so the prefix crowded out the part that distinguishes one command from another. --- cmd/context/context.go | 2 +- cmd/env/env.go | 2 +- cmd/ide/ide.go | 2 +- cmd/machine/machine.go | 2 +- cmd/mcp/mcp.go | 2 +- cmd/provider/provider.go | 2 +- cmd/root.go | 81 +++++++++------------------------------- cmd/secrets/secrets.go | 2 +- cmd/self/self.go | 2 +- pkg/clihelp/help.go | 60 +++++------------------------ pkg/clihelp/help_test.go | 41 +++++++++++++------- 11 files changed, 61 insertions(+), 137 deletions(-) diff --git a/cmd/context/context.go b/cmd/context/context.go index 39cfb1d06..0afc97762 100644 --- a/cmd/context/context.go +++ b/cmd/context/context.go @@ -9,7 +9,7 @@ import ( func NewContextCmd(flags *flags.GlobalFlags) *cobra.Command { contextCmd := &cobra.Command{ Use: "context", - Short: "Devsy Context commands", + Short: "Manage contexts", } contextCmd.AddCommand(NewCreateCmd(flags)) diff --git a/cmd/env/env.go b/cmd/env/env.go index bfd24811b..7d6389bb9 100644 --- a/cmd/env/env.go +++ b/cmd/env/env.go @@ -10,7 +10,7 @@ import ( func NewEnvCmd(globalFlags *flags.GlobalFlags) *cobra.Command { envCmd := &cobra.Command{ Use: "env", - Short: "Devsy managed environment variables", + Short: "Manage environment variables", Long: `Manage named environment variables stored per context and injected into workspaces. Values are stored in plaintext in the Devsy config directory; use "devsy secret" for sensitive values.`, diff --git a/cmd/ide/ide.go b/cmd/ide/ide.go index bcfb39150..3b6309646 100644 --- a/cmd/ide/ide.go +++ b/cmd/ide/ide.go @@ -10,7 +10,7 @@ import ( func NewIDECmd(flags *flags.GlobalFlags) *cobra.Command { ideCmd := &cobra.Command{ Use: "ide", - Short: "Devsy IDE commands", + Short: "Manage IDE preferences", } ideCmd.AddCommand(NewUseCmd(flags)) diff --git a/cmd/machine/machine.go b/cmd/machine/machine.go index 172448ef8..3bebdab71 100644 --- a/cmd/machine/machine.go +++ b/cmd/machine/machine.go @@ -9,7 +9,7 @@ import ( func NewMachineCmd(flags *flags.GlobalFlags) *cobra.Command { machineCmd := &cobra.Command{ Use: "machine", - Short: "Devsy Machine commands", + Short: "Manage provider-hosted machines", } machineCmd.AddCommand(NewListCmd(flags)) diff --git a/cmd/mcp/mcp.go b/cmd/mcp/mcp.go index d1629410e..c00fb2b1d 100644 --- a/cmd/mcp/mcp.go +++ b/cmd/mcp/mcp.go @@ -9,7 +9,7 @@ import ( func NewMCPCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &cobra.Command{ Use: "mcp", - Short: "Run Devsy as a Model Context Protocol server", + Short: "Run as a Model Context Protocol server", } cmd.AddCommand(NewServeCmd(globalFlags)) return cmd diff --git a/cmd/provider/provider.go b/cmd/provider/provider.go index 0dfda7145..7b268bb33 100644 --- a/cmd/provider/provider.go +++ b/cmd/provider/provider.go @@ -9,7 +9,7 @@ import ( func NewProviderCmd(flags *flags.GlobalFlags) *cobra.Command { providerCmd := &cobra.Command{ Use: "provider", - Short: "Devsy Provider commands", + Short: "Manage providers", } providerCmd.AddCommand(NewAddCmd(flags)) diff --git a/cmd/root.go b/cmd/root.go index a1c33f4ce..e5535d62f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,12 +51,6 @@ const ( flagLogOutput = "--" + names.LogOutput flagLogFormat = "--" + names.LogFormat - groupCore = "core" - groupConfig = "config" - groupPlatform = "platform" - groupDevcontainer = "devcontainer" - groupMeta = "meta" - // envProEnabled gates registration of the `pro` command tree. The pro // feature is not ready for general use; set DEVSY_PRO_ENABLED=true to // expose it (e.g. for internal testing). @@ -313,70 +307,29 @@ func BuildRoot() (*cobra.Command, *flags.GlobalFlags) { return nil } - registerGroups(rootCmd) registerSubcommands(rootCmd, globalFlags) return rootCmd, globalFlags } -func registerGroups(rootCmd *cobra.Command) { - groups := []*cobra.Group{ - {ID: groupCore, Title: "Core commands:"}, - {ID: groupConfig, Title: "Configuration commands:"}, - {ID: groupDevcontainer, Title: "Devcontainer commands:"}, - {ID: groupMeta, Title: "Meta:"}, - } - if proEnabled() { - groups = append(groups, &cobra.Group{ID: groupPlatform, Title: "Platform commands:"}) - } - rootCmd.AddGroup(groups...) -} - func registerSubcommands(rootCmd *cobra.Command, globalFlags *flags.GlobalFlags) { - providerCmd := provider.NewProviderCmd(globalFlags) - providerCmd.GroupID = groupConfig - rootCmd.AddCommand(providerCmd) - ideCmd := ide.NewIDECmd(globalFlags) - ideCmd.GroupID = groupConfig - rootCmd.AddCommand(ideCmd) - machineCmd := machine.NewMachineCmd(globalFlags) - machineCmd.GroupID = groupCore - rootCmd.AddCommand(machineCmd) - contextCmd := context.NewContextCmd(globalFlags) - contextCmd.GroupID = groupConfig - rootCmd.AddCommand(contextCmd) - secretsCmd := secretscmd.NewSecretsCmd(globalFlags) - secretsCmd.GroupID = groupConfig - rootCmd.AddCommand(secretsCmd) - envCmd := envcmd.NewEnvCmd(globalFlags) - envCmd.GroupID = groupConfig - rootCmd.AddCommand(envCmd) + rootCmd.AddCommand( + ci.NewCICmd(globalFlags), + cliconfig.NewConfigCmd(globalFlags), + context.NewContextCmd(globalFlags), + envcmd.NewEnvCmd(globalFlags), + feature.NewFeatureCmd(globalFlags), + ide.NewIDECmd(globalFlags), + machine.NewMachineCmd(globalFlags), + mcp.NewMCPCmd(globalFlags), + provider.NewProviderCmd(globalFlags), + secretscmd.NewSecretsCmd(globalFlags), + self.NewSelfCmd(globalFlags), + template.NewTemplateCmd(globalFlags), + wsCmdPkg.NewWorkspaceCmd(globalFlags), + cmdinternal.NewInternalCmd(globalFlags), + ) if proEnabled() { - proCmd := pro.NewProCmd(globalFlags) - proCmd.GroupID = groupPlatform - rootCmd.AddCommand(proCmd) + rootCmd.AddCommand(pro.NewProCmd(globalFlags)) } - wsCmd := wsCmdPkg.NewWorkspaceCmd(globalFlags) - wsCmd.GroupID = groupCore - rootCmd.AddCommand(wsCmd) - - selfCmd := self.NewSelfCmd(globalFlags) - selfCmd.GroupID = groupMeta - rootCmd.AddCommand(selfCmd) - mcpCmd := mcp.NewMCPCmd(globalFlags) - mcpCmd.GroupID = groupMeta - rootCmd.AddCommand(mcpCmd) - configCmd := cliconfig.NewConfigCmd(globalFlags) - configCmd.GroupID = groupDevcontainer - rootCmd.AddCommand(configCmd) - featureCmd := feature.NewFeatureCmd(globalFlags) - featureCmd.GroupID = groupDevcontainer - rootCmd.AddCommand(featureCmd) - templateCmd := template.NewTemplateCmd(globalFlags) - templateCmd.GroupID = groupDevcontainer - rootCmd.AddCommand(templateCmd) - ciCmd := ci.NewCICmd(globalFlags) - ciCmd.GroupID = groupDevcontainer - rootCmd.AddCommand(ciCmd) - rootCmd.AddCommand(cmdinternal.NewInternalCmd(globalFlags)) } diff --git a/cmd/secrets/secrets.go b/cmd/secrets/secrets.go index d0403f60c..0a8acf3ed 100644 --- a/cmd/secrets/secrets.go +++ b/cmd/secrets/secrets.go @@ -10,7 +10,7 @@ import ( func NewSecretsCmd(flags *flags.GlobalFlags) *cobra.Command { secretsCmd := &cobra.Command{ Use: "secret", - Short: "Devsy Secrets commands", + Short: "Manage secrets", Long: `Manage named secrets stored locally and injected into workspaces. Secret values are kept in the OS keyring (macOS Keychain, Windows Credential diff --git a/cmd/self/self.go b/cmd/self/self.go index 809549e53..c116a386b 100644 --- a/cmd/self/self.go +++ b/cmd/self/self.go @@ -9,7 +9,7 @@ import ( func NewSelfCmd(globalFlags *flags.GlobalFlags) *cobra.Command { cmd := &cobra.Command{ Use: "self", - Short: "Manage the devsy CLI itself", + Short: "Manage the CLI itself", } cmd.AddCommand(NewUpdateCmd()) return cmd diff --git a/pkg/clihelp/help.go b/pkg/clihelp/help.go index dd4c3d57b..df0b2f88a 100644 --- a/pkg/clihelp/help.go +++ b/pkg/clihelp/help.go @@ -129,62 +129,19 @@ func writeUsage(b *strings.Builder, cmd *cobra.Command) { b.WriteString("\n") } -// writeCommands lists available subcommands, preserving the command groups -// declared on the root command and collecting ungrouped ones under a trailing -// section. +// writeCommands lists every visible subcommand in one alphabetical section. func writeCommands(b *strings.Builder, cmd *cobra.Command, width int) { - if !cmd.HasAvailableSubCommands() { - return - } - - grouped, ungrouped := partitionCommands(cmd) - - for _, group := range cmd.Groups() { - subs := grouped[group.ID] - if len(subs) == 0 { - continue - } - section(b, sectionTitle(group.Title)) - writeCommandList(b, subs, width) - b.WriteString("\n") - } - - if len(ungrouped) > 0 { - title := "SUBCOMMANDS" - if len(grouped) > 0 { - title = "ADDITIONAL COMMANDS" - } - section(b, title) - writeCommandList(b, ungrouped, width) - b.WriteString("\n") - } -} - -// partitionCommands splits cmd's visible subcommands into those belonging to a -// declared group, keyed by group ID, and those without one. -func partitionCommands(cmd *cobra.Command) (map[string][]*cobra.Command, []*cobra.Command) { - grouped := map[string][]*cobra.Command{} - var ungrouped []*cobra.Command + var subs []*cobra.Command for _, sub := range cmd.Commands() { - if !sub.IsAvailableCommand() && !sub.IsAdditionalHelpTopicCommand() { - continue - } - if sub.GroupID != "" && cmd.ContainsGroup(sub.GroupID) { - grouped[sub.GroupID] = append(grouped[sub.GroupID], sub) - continue + if sub.IsAvailableCommand() || sub.IsAdditionalHelpTopicCommand() { + subs = append(subs, sub) } - ungrouped = append(ungrouped, sub) } - return grouped, ungrouped -} - -// sectionTitle normalizes a cobra group title ("Core commands:") into a section -// header ("CORE COMMANDS"). -func sectionTitle(groupTitle string) string { - return strings.ToUpper(strings.TrimRight(strings.TrimSpace(groupTitle), ":")) -} + if len(subs) == 0 { + return + } -func writeCommandList(b *strings.Builder, subs []*cobra.Command, width int) { + section(b, "SUBCOMMANDS") sort.Slice(subs, func(i, j int) bool { return subs[i].Name() < subs[j].Name() }) for _, sub := range subs { name := sub.Name() @@ -201,6 +158,7 @@ func writeCommandList(b *strings.Builder, subs []*cobra.Command, width int) { b.WriteString(wrapHanging(sub.Short, width, descCol)) b.WriteString("\n") } + b.WriteString("\n") } // writeFlags emits the command's own flags under OPTIONS and the global flags diff --git a/pkg/clihelp/help_test.go b/pkg/clihelp/help_test.go index 7f72330a1..d13584ebe 100644 --- a/pkg/clihelp/help_test.go +++ b/pkg/clihelp/help_test.go @@ -11,12 +11,11 @@ import ( "github.com/stretchr/testify/require" ) -// newTestRoot builds a small command tree that mirrors the real one: grouped +// newTestRoot builds a small command tree that mirrors the real one: // subcommands, a persistent flag with an env var, and a local flag on a leaf. func newTestRoot(t *testing.T) *cobra.Command { t.Helper() root := &cobra.Command{Use: "devsy", Short: "Devsy", Long: "Devsy long description."} - root.AddGroup(&cobra.Group{ID: "core", Title: "Core commands:"}) pf := root.PersistentFlags() pf.String("provider", "", "The provider to use") @@ -25,7 +24,7 @@ func newTestRoot(t *testing.T) *cobra.Command { require.NoError(t, pf.MarkHidden("hidden-global")) noop := func(*cobra.Command, []string) {} - ws := &cobra.Command{Use: "workspace", Short: "Manage workspaces", GroupID: "core"} + ws := &cobra.Command{Use: "workspace", Short: "Manage workspaces"} list := &cobra.Command{Use: "list", Short: "List workspaces", Run: noop} list.Flags().Bool("skip-pro", false, "Don't list pro workspaces") ws.AddCommand(list) @@ -54,21 +53,30 @@ func renderHelp(t *testing.T, root *cobra.Command, path ...string) string { return buf.String() } -func TestRender_RootSectionsAndGroups(t *testing.T) { +func TestRender_RootSections(t *testing.T) { out := renderHelp(t, newTestRoot(t)) assert.Contains(t, out, "Devsy long description.") assert.Contains(t, out, "USAGE:") assert.Contains(t, out, "devsy [global-flags] ") - assert.Contains(t, out, "CORE COMMANDS:") - assert.Contains(t, out, "workspace") - // Commands with no group land in a trailing section, not the group listing. - assert.Contains(t, out, "ADDITIONAL COMMANDS:") - assert.Contains(t, out, "loose") + assert.Contains(t, out, "SUBCOMMANDS:") assert.Contains(t, out, "GLOBAL OPTIONS:") assert.Contains(t, out, "Run \"devsy --help\"") } +// Every subcommand lands in one alphabetical section, regardless of grouping. +func TestRender_SubcommandsAreFlatAndSorted(t *testing.T) { + out := renderHelp(t, newTestRoot(t)) + + body := sectionBody(t, out, "SUBCOMMANDS:") + assert.Contains(t, body, "loose") + assert.Contains(t, body, "workspace") + assert.Less(t, strings.Index(body, "loose"), strings.Index(body, "workspace")) + // Group headers must not survive the switch to a flat listing. + assert.NotContains(t, out, "CORE COMMANDS:") + assert.NotContains(t, out, "ADDITIONAL COMMANDS:") +} + // The env var belongs on the flag's signature line, beside the flag name, so it // can carry the purple accent independently of the description text. func TestRender_EnvVarOnSignatureLine(t *testing.T) { @@ -135,15 +143,20 @@ func signatureLine(t *testing.T, out, flagName string) string { return "" } -// section returns the body of a named section, up to the next section header. +// sectionBody returns the body of a named section, stopping at the next section +// header. Headers are the only unindented, uppercase, colon-terminated lines. func sectionBody(t *testing.T, out, header string) string { t.Helper() _, rest, found := strings.Cut(out, header) require.True(t, found, "section %q not found in:\n%s", header, out) - for _, next := range []string{"OPTIONS:", "GLOBAL OPTIONS:", "COMMANDS:"} { - if body, _, cut := strings.Cut(rest, next); cut { - rest = body + + var body []string + for line := range strings.SplitSeq(rest, "\n") { + if strings.HasSuffix(line, ":") && line == strings.ToUpper(line) && + !strings.HasPrefix(line, " ") && strings.TrimSpace(line) != "" { + break } + body = append(body, line) } - return rest + return strings.Join(body, "\n") } From 4f8186077a89ca1f1aa778db10e1659625702973 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 28 Jul 2026 23:37:24 -0500 Subject: [PATCH 3/5] refactor(cli)!: promote `self update` to top-level `update` The `self` parent held exactly one subcommand, so it added a level of nesting without grouping anything. Moving update to the top level makes it reachable as `devsy update`. BREAKING CHANGE: `devsy self update` is no longer recognized. Use `devsy update`. --- cmd/root.go | 4 ++-- cmd/self/self.go | 16 ---------------- cmd/{self => update}/update.go | 12 ++++++------ cmd/{self => update}/update_test.go | 2 +- e2e/tests/self/self.go | 4 ++-- 5 files changed, 11 insertions(+), 27 deletions(-) delete mode 100644 cmd/self/self.go rename cmd/{self => update}/update.go (88%) rename cmd/{self => update}/update_test.go (99%) diff --git a/cmd/root.go b/cmd/root.go index e5535d62f..96f1854f7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,8 +23,8 @@ import ( "github.com/devsy-org/devsy/cmd/pro" "github.com/devsy-org/devsy/cmd/provider" secretscmd "github.com/devsy-org/devsy/cmd/secrets" - "github.com/devsy-org/devsy/cmd/self" "github.com/devsy-org/devsy/cmd/template" + "github.com/devsy-org/devsy/cmd/update" wsCmdPkg "github.com/devsy-org/devsy/cmd/workspace" "github.com/devsy-org/devsy/pkg/clierr" "github.com/devsy-org/devsy/pkg/clihelp" @@ -324,8 +324,8 @@ func registerSubcommands(rootCmd *cobra.Command, globalFlags *flags.GlobalFlags) mcp.NewMCPCmd(globalFlags), provider.NewProviderCmd(globalFlags), secretscmd.NewSecretsCmd(globalFlags), - self.NewSelfCmd(globalFlags), template.NewTemplateCmd(globalFlags), + update.NewUpdateCmd(), wsCmdPkg.NewWorkspaceCmd(globalFlags), cmdinternal.NewInternalCmd(globalFlags), ) diff --git a/cmd/self/self.go b/cmd/self/self.go deleted file mode 100644 index c116a386b..000000000 --- a/cmd/self/self.go +++ /dev/null @@ -1,16 +0,0 @@ -package self - -import ( - "github.com/devsy-org/devsy/cmd/flags" - "github.com/spf13/cobra" -) - -// NewSelfCmd builds the 'devsy self' parent command for managing the devsy CLI itself. -func NewSelfCmd(globalFlags *flags.GlobalFlags) *cobra.Command { - cmd := &cobra.Command{ - Use: "self", - Short: "Manage the CLI itself", - } - cmd.AddCommand(NewUpdateCmd()) - return cmd -} diff --git a/cmd/self/update.go b/cmd/update/update.go similarity index 88% rename from cmd/self/update.go rename to cmd/update/update.go index 7356aba50..22c5a3d11 100644 --- a/cmd/self/update.go +++ b/cmd/update/update.go @@ -1,4 +1,4 @@ -package self +package update import ( "fmt" @@ -14,7 +14,7 @@ const ( channelBeta = "beta" ) -// UpdateCmd is a struct that defines a command call for "self update". +// UpdateCmd is a struct that defines a command call for "update". type UpdateCmd struct { Version string Channel string @@ -24,9 +24,9 @@ type UpdateCmd struct { // NewUpdateCmd creates a new update command. func NewUpdateCmd() *cobra.Command { cmd := &UpdateCmd{} - selfUpdateCmd := &cobra.Command{ + updateCmd := &cobra.Command{ Use: "update", - Short: "Update the Devsy CLI to the newest version", + Short: "Update the CLI to the newest version", Args: cobra.NoArgs, PreRunE: func(_ *cobra.Command, _ []string) error { switch cmd.Channel { @@ -56,7 +56,7 @@ func NewUpdateCmd() *cobra.Command { } cliflags.Add( - selfUpdateCmd, + updateCmd, cliflags.String( &cmd.Version, names.Version, @@ -76,5 +76,5 @@ func NewUpdateCmd() *cobra.Command { "Show which version would be downloaded without actually updating", ), ) - return selfUpdateCmd + return updateCmd } diff --git a/cmd/self/update_test.go b/cmd/update/update_test.go similarity index 99% rename from cmd/self/update_test.go rename to cmd/update/update_test.go index dbeec68fb..36070501f 100644 --- a/cmd/self/update_test.go +++ b/cmd/update/update_test.go @@ -1,4 +1,4 @@ -package self +package update import ( "testing" diff --git a/e2e/tests/self/self.go b/e2e/tests/self/self.go index 743752c7e..141a63891 100644 --- a/e2e/tests/self/self.go +++ b/e2e/tests/self/self.go @@ -21,8 +21,8 @@ var _ = ginkgo.Describe( framework.ExpectNoError(err, "getting current working directory should not error") f := framework.NewDefaultFramework(initialDir + "/bin") - output, err := f.ExecCommandOutput(ctx, []string{"self", "update", "--dry-run"}) - framework.ExpectNoError(err, "self update --dry-run should not error") + output, err := f.ExecCommandOutput(ctx, []string{"update", "--dry-run"}) + framework.ExpectNoError(err, "update --dry-run should not error") ginkgo.By("Parsing dry-run key=value output") lines := strings.Split(strings.TrimSpace(output), "\n") From 4cc5d16d69e528c04aab2735091cb3f74ae4568a Mon Sep 17 00:00:00 2001 From: Samuel K Date: Tue, 28 Jul 2026 23:41:59 -0500 Subject: [PATCH 4/5] style(cli): trim help renderer comments to non-obvious rationale Drop the doc comments that restated their own function names and the per-style comments in theme, keeping only the ones a reader can't recover from the code: the colorprofile nil-environ bug, why Accent is a single tone, and why persistent flags count as global. Help output is byte-identical. --- pkg/clihelp/help.go | 65 +++++++++++----------------------------- pkg/clihelp/help_test.go | 18 ++--------- pkg/flags/env.go | 5 ++-- pkg/theme/theme.go | 27 +++++------------ 4 files changed, 30 insertions(+), 85 deletions(-) diff --git a/pkg/clihelp/help.go b/pkg/clihelp/help.go index df0b2f88a..7319090a1 100644 --- a/pkg/clihelp/help.go +++ b/pkg/clihelp/help.go @@ -1,6 +1,4 @@ -// Package clihelp renders `devsy --help` output: uppercase section headers, -// grouped subcommand listings, and flags shown above their wrapped descriptions -// with the Devsy purple accent on flag names and environment variables. +// Package clihelp renders `devsy --help` output. package clihelp import ( @@ -20,25 +18,16 @@ import ( ) const ( - // indent is the left margin for entries inside a section. - indent = " " - // descIndent is the left margin for a flag's description line, set one - // level deeper than the flag itself so the two read as a pair. - descIndent = " " - // nameColumn is the width reserved for a subcommand name before its - // short description. Devsy's longest command name is well inside this. - nameColumn = 12 - // defaultWidth is the wrap width used when stdout is not a terminal. + indent = " " + descIndent = " " + nameColumn = 12 defaultWidth = 80 - // maxWidth keeps prose readable on very wide terminals. - maxWidth = 100 - // minWidth is a floor so wrapping never degenerates on tiny terminals. - minWidth = 40 + maxWidth = 100 + minWidth = 40 ) -// Install makes this renderer the help and usage output for cmd and every -// command beneath it. Cobra propagates help/usage functions to children, so a -// single call on the root command covers the whole tree. +// Install applies this renderer to cmd and, since cobra propagates help and +// usage functions to children, every command beneath it. func Install(cmd *cobra.Command) { cmd.SetHelpFunc(func(c *cobra.Command, _ []string) { render(c, c.OutOrStdout()) @@ -49,13 +38,10 @@ func Install(cmd *cobra.Command) { }) } -// render writes the full help body for cmd to out, downsampling color to -// whatever the destination supports (and stripping it entirely when the output -// is redirected or NO_COLOR is set). func render(cmd *cobra.Command, out io.Writer) { - // os.Environ() is passed explicitly: colorprofile.NewWriter documents a nil - // environ as meaning os.Environ(), but v0.4.3 builds an empty map instead, - // so TERM reads as unset and every profile collapses to NoTTY (no color). + // colorprofile.NewWriter documents a nil environ as meaning os.Environ(), + // but v0.4.3 builds an empty map instead, so TERM reads as unset and every + // profile collapses to NoTTY, silently stripping all color. w := colorprofile.NewWriter(out, os.Environ()) width := wrapWidth(out) @@ -69,8 +55,6 @@ func render(cmd *cobra.Command, out io.Writer) { _, _ = io.WriteString(w, b.String()) } -// wrapWidth returns the column at which prose should wrap, based on the -// terminal size when out is a terminal. func wrapWidth(out io.Writer) int { f, ok := out.(interface{ Fd() uintptr }) if !ok { @@ -93,8 +77,6 @@ func clamp(v, low, high int) int { return v } -// writeIntro emits the command's long description, or its short one when no -// long form is set. func writeIntro(b *strings.Builder, cmd *cobra.Command, width int) { desc := cmd.Long if desc == "" { @@ -107,8 +89,6 @@ func writeIntro(b *strings.Builder, cmd *cobra.Command, width int) { b.WriteString("\n\n") } -// writeUsage emits the USAGE section, plus any Example block the command -// carries. func writeUsage(b *strings.Builder, cmd *cobra.Command) { section(b, "USAGE") if cmd.Runnable() { @@ -129,7 +109,6 @@ func writeUsage(b *strings.Builder, cmd *cobra.Command) { b.WriteString("\n") } -// writeCommands lists every visible subcommand in one alphabetical section. func writeCommands(b *strings.Builder, cmd *cobra.Command, width int) { var subs []*cobra.Command for _, sub := range cmd.Commands() { @@ -161,10 +140,8 @@ func writeCommands(b *strings.Builder, cmd *cobra.Command, width int) { b.WriteString("\n") } -// writeFlags emits the command's own flags under OPTIONS and the global flags -// under GLOBAL OPTIONS. Globals are the ones this command declares as -// persistent (on the root) plus everything inherited from ancestors — both -// apply to subcommands, so they belong in the same section. +// A command's own persistent flags apply to its subcommands just as inherited +// ones do, so both are listed as global. func writeFlags(b *strings.Builder, cmd *cobra.Command, width int) { persistent := cmd.PersistentFlags() var local, global []*pflag.Flag @@ -200,8 +177,6 @@ func writeFlags(b *strings.Builder, cmd *cobra.Command, width int) { } } -// writeFlagList renders each flag as a signature line — name, value type, env -// var, default — followed by its indented, wrapped description. func writeFlagList(b *strings.Builder, list []*pflag.Flag, width int) { sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) for _, f := range list { @@ -216,8 +191,6 @@ func writeFlagList(b *strings.Builder, list []*pflag.Flag, width int) { } } -// flagSignature builds the styled first line for a flag, e.g. -// "-v, --verbose count, $DEVSY_VERBOSE (default: 0)". func flagSignature(f *pflag.Flag) string { var parts []string if f.Shorthand != "" { @@ -239,8 +212,8 @@ func flagSignature(f *pflag.Flag) string { return sig } -// defaultText renders a flag's default as "(default: x)", omitting zero values -// that carry no information. +// Zero-ish defaults carry no information, so they are omitted to keep +// signature lines scannable. func defaultText(f *pflag.Flag) string { switch f.DefValue { case "", "false", "0", "[]", "0s": @@ -249,8 +222,6 @@ func defaultText(f *pflag.Flag) string { return fmt.Sprintf("(default: %s)", f.DefValue) } -// flagUsage returns a flag's description with the value-type backquotes that -// pflag uses for naming stripped out. func flagUsage(f *pflag.Flag) string { _, usage := pflag.UnquoteUsage(f) return strings.TrimSpace(usage) @@ -272,15 +243,14 @@ func section(b *strings.Builder, title string) { b.WriteString("\n") } -// wrapIndent wraps text to width and prefixes every resulting line with prefix. func wrapIndent(text string, width int, prefix string) string { limit := max(minWidth/2, width-len(prefix)) wrapped := ansi.Wordwrap(strings.TrimSpace(text), limit, " -") return indentBlock(wrapped, prefix) } -// wrapHanging wraps text to width assuming the first line already begins at -// column col, indenting continuation lines to line up under it. +// wrapHanging assumes the first line already begins at column col, so only +// continuation lines are padded to line up under it. func wrapHanging(text string, width, col int) string { limit := max(minWidth/2, width-col) lines := strings.Split(ansi.Wordwrap(strings.TrimSpace(text), limit, " -"), "\n") @@ -291,7 +261,6 @@ func wrapHanging(text string, width, col int) string { return strings.Join(lines, "\n") } -// indentBlock prefixes every non-empty line of text with prefix. func indentBlock(text, prefix string) string { lines := strings.Split(text, "\n") for i, line := range lines { diff --git a/pkg/clihelp/help_test.go b/pkg/clihelp/help_test.go index d13584ebe..d2791ef35 100644 --- a/pkg/clihelp/help_test.go +++ b/pkg/clihelp/help_test.go @@ -11,8 +11,6 @@ import ( "github.com/stretchr/testify/require" ) -// newTestRoot builds a small command tree that mirrors the real one: -// subcommands, a persistent flag with an env var, and a local flag on a leaf. func newTestRoot(t *testing.T) *cobra.Command { t.Helper() root := &cobra.Command{Use: "devsy", Short: "Devsy", Long: "Devsy long description."} @@ -29,14 +27,13 @@ func newTestRoot(t *testing.T) *cobra.Command { list.Flags().Bool("skip-pro", false, "Don't list pro workspaces") ws.AddCommand(list) root.AddCommand(ws) - root.AddCommand(&cobra.Command{Use: "loose", Short: "Ungrouped command", Run: noop}) + root.AddCommand(&cobra.Command{Use: "loose", Short: "A loose command", Run: noop}) Install(root) return root } -// renderHelp captures help output for the command at path. Output goes to a -// bytes.Buffer, which is not a terminal, so colorprofile strips all styling and +// A bytes.Buffer is not a terminal, so colorprofile strips styling and // assertions can match plain text. func renderHelp(t *testing.T, root *cobra.Command, path ...string) string { t.Helper() @@ -64,7 +61,6 @@ func TestRender_RootSections(t *testing.T) { assert.Contains(t, out, "Run \"devsy --help\"") } -// Every subcommand lands in one alphabetical section, regardless of grouping. func TestRender_SubcommandsAreFlatAndSorted(t *testing.T) { out := renderHelp(t, newTestRoot(t)) @@ -72,13 +68,10 @@ func TestRender_SubcommandsAreFlatAndSorted(t *testing.T) { assert.Contains(t, body, "loose") assert.Contains(t, body, "workspace") assert.Less(t, strings.Index(body, "loose"), strings.Index(body, "workspace")) - // Group headers must not survive the switch to a flat listing. assert.NotContains(t, out, "CORE COMMANDS:") assert.NotContains(t, out, "ADDITIONAL COMMANDS:") } -// The env var belongs on the flag's signature line, beside the flag name, so it -// can carry the purple accent independently of the description text. func TestRender_EnvVarOnSignatureLine(t *testing.T) { out := renderHelp(t, newTestRoot(t)) @@ -92,8 +85,6 @@ func TestRender_HidesHiddenFlags(t *testing.T) { assert.NotContains(t, out, "hidden-global") } -// A root command's own persistent flags are global to the whole tree, so they -// must be listed under GLOBAL OPTIONS rather than the root's local OPTIONS. func TestRender_RootPersistentFlagsAreGlobal(t *testing.T) { out := renderHelp(t, newTestRoot(t)) assert.Contains(t, sectionBody(t, out, "GLOBAL OPTIONS:"), "--provider") @@ -109,7 +100,6 @@ func TestRender_LeafSeparatesLocalFromGlobal(t *testing.T) { assert.NotContains(t, out, "for more information about a subcommand") } -// Defaults that carry no information are omitted so signature lines stay scannable. func TestDefaultText_OmitsZeroValues(t *testing.T) { root := &cobra.Command{Use: "x", Run: func(*cobra.Command, []string) {}} fs := root.Flags() @@ -131,7 +121,6 @@ func TestRender_NoColorWhenNotATerminal(t *testing.T) { assert.NotContains(t, out, "\x1b[", "styling must be stripped for non-terminal output") } -// signatureLine returns the rendered line containing the given flag name. func signatureLine(t *testing.T, out, flagName string) string { t.Helper() for line := range strings.SplitSeq(out, "\n") { @@ -143,8 +132,7 @@ func signatureLine(t *testing.T, out, flagName string) string { return "" } -// sectionBody returns the body of a named section, stopping at the next section -// header. Headers are the only unindented, uppercase, colon-terminated lines. +// Section headers are the only unindented, uppercase, colon-terminated lines. func sectionBody(t *testing.T, out, header string) string { t.Helper() _, rest, found := strings.Cut(out, header) diff --git a/pkg/flags/env.go b/pkg/flags/env.go index bf0f0bd03..27da2062b 100644 --- a/pkg/flags/env.go +++ b/pkg/flags/env.go @@ -18,9 +18,8 @@ func EnvName(flagName string) string { return config.EnvPrefix + strings.ToUpper(strings.ReplaceAll(flagName, "-", "_")) } -// EnvAnnotation is the pflag annotation key under which BindEnv records a -// flag's canonical env var name. The help renderer reads it to display the env -// var beside the flag instead of burying it in the description text. +// EnvAnnotation keys the flag's env var name for the help renderer, which shows +// it beside the flag rather than buried in the description. const EnvAnnotation = "devsy_env" // BindEnv wires the canonical DEVSY_* env var into a cobra flag. Call it diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go index 8767be4cd..274856820 100644 --- a/pkg/theme/theme.go +++ b/pkg/theme/theme.go @@ -1,29 +1,18 @@ -// Package theme holds the Devsy CLI color palette and the text styles built -// from it, so terminal output stays consistent with the desktop app. +// Package theme holds the Devsy CLI color palette and text styles. package theme import "charm.land/lipgloss/v2" -// Accent is the Devsy purple, matching the desktop app's `.theme-purple` -// primary (hsl(252 100% 68%)). It is a single tone rather than a light/dark -// pair because resolving a pair requires querying the terminal background, -// which needs raw mode — too costly for a `--help` render. This tone clears -// 4.3:1 contrast against both black and white backgrounds. +// Accent mirrors the desktop app's `.theme-purple` primary, hsl(252 100% 68%). +// A single tone rather than a light/dark pair: resolving a pair means querying +// the terminal background, which needs raw mode — too costly for a --help +// render. This tone clears 4.3:1 contrast on both black and white. var Accent = lipgloss.Color("#7C5CFF") var ( - // Heading styles section headers such as "GLOBAL OPTIONS:". Heading = lipgloss.NewStyle().Bold(true) - - // Flag styles a flag name, e.g. "--provider". - Flag = lipgloss.NewStyle().Foreground(Accent).Bold(true) - - // EnvVar styles a flag's environment variable, e.g. "$DEVSY_PROVIDER". - EnvVar = lipgloss.NewStyle().Foreground(Accent) - - // Command styles a subcommand name in a command listing. + Flag = lipgloss.NewStyle().Foreground(Accent).Bold(true) + EnvVar = lipgloss.NewStyle().Foreground(Accent) Command = lipgloss.NewStyle().Bold(true) - - // Muted styles secondary detail: value types, defaults, footer hints. - Muted = lipgloss.NewStyle().Faint(true) + Muted = lipgloss.NewStyle().Faint(true) ) From 5afa000d6dc678df656a5d8ca518373ab91276fb Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 29 Jul 2026 00:54:49 -0500 Subject: [PATCH 5/5] style: update comments --- pkg/theme/theme.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/theme/theme.go b/pkg/theme/theme.go index 274856820..2cf11e863 100644 --- a/pkg/theme/theme.go +++ b/pkg/theme/theme.go @@ -3,10 +3,6 @@ package theme import "charm.land/lipgloss/v2" -// Accent mirrors the desktop app's `.theme-purple` primary, hsl(252 100% 68%). -// A single tone rather than a light/dark pair: resolving a pair means querying -// the terminal background, which needs raw mode — too costly for a --help -// render. This tone clears 4.3:1 contrast on both black and white. var Accent = lipgloss.Color("#7C5CFF") var (