From 266e5372e75ecabdba91319841581bd0ca06fd54 Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 14:57:06 -0400 Subject: [PATCH 1/5] Add first-run detection and install-time setup wizard - install.sh: run `supermodel setup` automatically after install when attached to a TTY (skips piped/CI installs) - cmd/root.go: PersistentPreRunE nudges users without a config to run `supermodel setup`; no-op for setup/login/logout/version/help/completion Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 26 ++++++++++++++++++++++++++ install.sh | 6 ++++++ 2 files changed, 32 insertions(+) diff --git a/cmd/root.go b/cmd/root.go index 368c025..76ab9fe 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,8 +5,20 @@ import ( "os" "github.com/spf13/cobra" + + "github.com/supermodeltools/cli/internal/config" ) +// noConfigCommands are subcommands that work without a config file. +var noConfigCommands = map[string]bool{ + "setup": true, + "login": true, + "logout": true, + "version": true, + "help": true, + "completion": true, +} + var rootCmd = &cobra.Command{ Use: "supermodel", Short: "Give your AI coding agent a map of your codebase", @@ -15,6 +27,20 @@ providing call graphs, dead code detection, and blast radius analysis. See https://supermodeltools.com for documentation.`, SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Walk up to the root command name to get the subcommand. + name := cmd.Name() + if noConfigCommands[name] { + return nil + } + + cfg, err := config.Load() + if err != nil || cfg.APIKey == "" { + fmt.Fprintln(os.Stderr, "Run 'supermodel setup' to get started.") + os.Exit(1) + } + return nil + }, } // Execute is the entry point called by main. diff --git a/install.sh b/install.sh index 54c2429..6268a1e 100644 --- a/install.sh +++ b/install.sh @@ -72,3 +72,9 @@ install -m755 "$TMP/$BINARY" "$INSTALL_DIR/$BINARY" echo "Installed: $INSTALL_DIR/$BINARY" "$INSTALL_DIR/$BINARY" version + +# Run the setup wizard when attached to a terminal (skip in piped/CI installs). +if [ -t 1 ]; then + echo "" + "$INSTALL_DIR/$BINARY" setup +fi From d2ae6b65cbda436cd18c8e499b13b9950c5e8e42 Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 15:04:59 -0400 Subject: [PATCH 2/5] Redesign setup wizard: inline auth, 3 focused steps, no dead ends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Authentication is now inline: if no API key, opens browser OAuth flow immediately instead of redirecting user to run `supermodel login` first - Removed the file mode toggle step — file mode is the default, no question needed; the product is explained in the header instead - 3 clean steps: Authenticate → Repository → Claude Code hook → Analyze - Punchier copy, fewer words per step - Removed unused boolPtr and selectMenu helpers Co-Authored-By: Claude Sonnet 4.6 --- internal/setup/wizard.go | 158 ++++++++++++--------------------------- 1 file changed, 48 insertions(+), 110 deletions(-) diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 4c3f766..5cc3c14 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -12,6 +12,7 @@ import ( "github.com/manifoldco/promptui" "github.com/supermodeltools/cli/internal/analyze" + "github.com/supermodeltools/cli/internal/auth" "github.com/supermodeltools/cli/internal/config" ) @@ -38,9 +39,8 @@ func Run(ctx context.Context, cfg *config.Config) error { fmt.Printf(" %sMake your coding agents %s3× faster%s, %s50%%+ cheaper%s, and %smore accurate%s%s.\n", reset, bWhite, reset, bWhite, reset, bWhite, reset, reset) fmt.Println() - fmt.Printf(" %sInjects a live code graph next to your source files so agents pick it%s\n", dWhite, reset) - fmt.Printf(" %sup automatically through their native grep, cat, and rg calls — no%s\n", dWhite, reset) - fmt.Printf(" %sprompt engineering, no extra context windows, no new tools to learn.%s\n", dWhite, reset) + fmt.Printf(" %s.graph files appear next to your source code. Your agent reads them%s\n", dWhite, reset) + fmt.Printf(" %sautomatically via grep and cat — no prompt changes, no new tools.%s\n", dWhite, reset) fmt.Println() // ── Step 1: Authentication ────────────────────────────────────── @@ -48,11 +48,18 @@ func Run(ctx context.Context, cfg *config.Config) error { fmt.Println() if cfg.APIKey == "" { - fmt.Printf(" %sRun 'supermodel login' first, then re-run 'supermodel setup'.%s\n\n", yellow, reset) - return nil + fmt.Printf(" %sOpening your browser to sign in and generate an API key…%s\n\n", dWhite, reset) + if err := auth.Login(ctx); err != nil { + return fmt.Errorf("authentication failed — run 'supermodel login' to try again") + } + // Reload config to pick up the saved key. + if reloaded, loadErr := config.Load(); loadErr == nil { + cfg = reloaded + } } - fmt.Printf(" %sUsing key%s %s%s%s\n", dim, reset, bWhite, maskKey(cfg.APIKey), reset) - fmt.Printf(" %s✓%s Authentication\n", green, reset) + + fmt.Printf(" %sKey%s %s%s%s\n", dim, reset, bWhite, maskKey(cfg.APIKey), reset) + fmt.Printf(" %s✓%s Authenticated\n", green, reset) fmt.Println() // ── Step 2: Repository ───────────────────────────────────────── @@ -73,92 +80,54 @@ func Run(ctx context.Context, cfg *config.Config) error { fmt.Printf(" %s✓%s Repository\n", green, reset) fmt.Println() - // ── Step 3: File mode ────────────────────────────────────────── - fmt.Printf(" %s◆%s File mode\n", cyan, reset) - fmt.Println() - fmt.Printf(" %sFile mode writes a .graph file next to each source file in your repo.%s\n", dWhite, reset) - fmt.Printf(" %sAI agents pick these up automatically through grep, cat, and rg — no%s\n", dWhite, reset) - fmt.Printf(" %sprompt engineering, no extra context windows, no new tools to learn.%s\n", dWhite, reset) - fmt.Println() - fmt.Printf(" %sKeep files updated with 'supermodel watch' in the background, or run%s\n", dWhite, reset) - fmt.Printf(" %s'supermodel analyze' once to generate them on demand.%s\n", dWhite, reset) - fmt.Println() - fmt.Printf(" %sDisable at any time with: supermodel clean%s\n", dWhite, reset) - fmt.Println() - - filesEnabled := confirmYN("Enable file mode?", true) - fmt.Println() - - // Persist file mode setting - cfg.Files = boolPtr(filesEnabled) - if err := cfg.Save(); err != nil { - fmt.Fprintf(os.Stderr, " %sWarning: could not save config: %v%s\n", yellow, err, reset) - } + // ── Step 3: Claude Code hook ─────────────────────────────────── + hookNote := "" - if filesEnabled { - fmt.Printf(" %s✓%s File mode enabled\n", green, reset) - } else { - fmt.Printf(" %s✓%s File mode disabled\n", green, reset) - } + fmt.Printf(" %s◆%s Claude Code hook\n", cyan, reset) fmt.Println() - // ── Step 4: Claude Code hook (only if file mode enabled) ─────── - hookInstalled := false - hookNote := "not installed" - - if filesEnabled { - fmt.Printf(" %s◆%s Claude Code hook\n", cyan, reset) + switch detectClaude() { + case true: + fmt.Printf(" %sInstalls a PostToolUse hook that regenerates .graph files every%s\n", dWhite, reset) + fmt.Printf(" %stime Claude writes or edits a file — keeps context always fresh.%s\n", dWhite, reset) fmt.Println() - switch detectClaude() { - case true: - fmt.Printf(" %sInstalling a PostToolUse hook keeps your .graph files updated every%s\n", dWhite, reset) - fmt.Printf(" %stime Claude Code writes or edits a file — no manual re-runs needed.%s\n", dWhite, reset) - fmt.Println() - - if confirmYN("Install Claude Code hook?", true) { - installed, err := installHook(repoDir) - switch { - case err != nil: - fmt.Fprintf(os.Stderr, " %sWarning: could not install hook: %v%s\n", yellow, err, reset) - case installed: - hookInstalled = true - hookNote = "installed in .claude/settings.json" - fmt.Printf(" %s✓%s Hook installed\n", green, reset) - default: - fmt.Printf(" %s✓%s Hook already installed\n", green, reset) - hookInstalled = true - hookNote = "already in .claude/settings.json" - } + if confirmYN("Install Claude Code hook?", true) { + installed, err := installHook(repoDir) + switch { + case err != nil: + fmt.Fprintf(os.Stderr, " %sWarning: could not install hook: %v%s\n", yellow, err, reset) + case installed: + hookNote = "installed in .claude/settings.json" + fmt.Printf(" %s✓%s Hook installed\n", green, reset) + default: + hookNote = "already in .claude/settings.json" + fmt.Printf(" %s✓%s Hook already installed\n", green, reset) } - default: - fmt.Printf(" %sClaude Code not detected. You can install the hook later by adding%s\n", dWhite, reset) - fmt.Printf(" %sthis to .claude/settings.json in your repo:%s\n", dWhite, reset) - fmt.Println() - fmt.Printf(" %s{%s\n", dim, reset) - fmt.Printf(" %s \"hooks\": {%s\n", dim, reset) - fmt.Printf(" %s \"PostToolUse\": [{%s\n", dim, reset) - fmt.Printf(" %s \"matcher\": \"Write|Edit\",%s\n", dim, reset) - fmt.Printf(" %s \"hooks\": [{\"type\": \"command\", \"command\": \"supermodel hook\"}]%s\n", dim, reset) - fmt.Printf(" %s }]%s\n", dim, reset) - fmt.Printf(" %s }%s\n", dim, reset) - fmt.Printf(" %s}%s\n", dim, reset) + } else { + fmt.Printf(" %s–%s Skipped\n", dim, reset) } + default: + fmt.Printf(" %sClaude Code not detected. Add this to .claude/settings.json:%s\n", dWhite, reset) fmt.Println() + fmt.Printf(" %s{%s\n", dim, reset) + fmt.Printf(" %s \"hooks\": {%s\n", dim, reset) + fmt.Printf(" %s \"PostToolUse\": [{%s\n", dim, reset) + fmt.Printf(" %s \"matcher\": \"Write|Edit\",%s\n", dim, reset) + fmt.Printf(" %s \"hooks\": [{\"type\": \"command\", \"command\": \"supermodel hook\"}]%s\n", dim, reset) + fmt.Printf(" %s }]%s\n", dim, reset) + fmt.Printf(" %s }%s\n", dim, reset) + fmt.Printf(" %s}%s\n", dim, reset) } + fmt.Println() // ── Summary ──────────────────────────────────────────────────── fmt.Printf(" %s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n", dim, reset) fmt.Println() fmt.Printf(" %s✓%s Setup complete\n", bGreen, reset) fmt.Println() - - fileModeStr := "disabled" - if filesEnabled { - fileModeStr = "enabled" - } - fmt.Printf(" %sFile mode%s %s%s%s\n", dim, reset, bWhite, fileModeStr, reset) - if filesEnabled { + fmt.Printf(" %sFile mode%s %senabled%s\n", dim, reset, bWhite, reset) + if hookNote != "" { fmt.Printf(" %sHook%s %s%s%s\n", dim, reset, bWhite, hookNote, reset) } fmt.Println() @@ -170,8 +139,6 @@ func Run(ctx context.Context, cfg *config.Config) error { fmt.Printf(" %s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n", dim, reset) fmt.Println() - _ = hookInstalled - if confirmYN("Run 'supermodel analyze' now?", true) { fmt.Println() return analyze.Run(ctx, cfg, repoDir, analyze.Options{}) @@ -234,7 +201,7 @@ func installHook(repoDir string) (bool, error) { const hookCmd = "supermodel hook" - // Check if already installed + // Check if already installed. if hooks, ok := settings["hooks"].(map[string]interface{}); ok { if existing, ok := hooks["PostToolUse"].([]interface{}); ok { for _, entry := range existing { @@ -284,30 +251,6 @@ func installHook(repoDir string) (bool, error) { // ── UI Helpers ────────────────────────────────────────────────────── -// selectMenu shows an arrow-key navigable list and returns the selected index. -func selectMenu(label string, items []string, cursorPos int) int { - sel := promptui.Select{ - Label: label, - Items: items, - CursorPos: cursorPos, - Size: len(items), - HideHelp: true, - Templates: &promptui.SelectTemplates{ - Label: fmt.Sprintf(" %s{{ . }}%s", dim, reset), - Active: fmt.Sprintf(" %s▸%s {{ . | cyan }}", green, reset), - Inactive: " {{ . }}", - Selected: fmt.Sprintf(" %s✔%s {{ . | cyan }}", green, reset), - }, - } - - idx, _, err := sel.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "\n %sCancelled.%s\n\n", dim, reset) - os.Exit(0) - } - return idx -} - // confirmYN shows a Y/N prompt navigable with arrow keys. func confirmYN(label string, defaultYes bool) bool { items := []string{"Yes", "No"} @@ -358,8 +301,3 @@ func promptText(label, defaultVal string) string { } return strings.TrimSpace(result) } - -func boolPtr(b bool) *bool { return &b } - -// keep selectMenu referenced to avoid unused import if callers don't use it directly -var _ = selectMenu From 59e103126b56fceaa2ffcbcf6b5f8632f40c6aed Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 15:08:42 -0400 Subject: [PATCH 3/5] Wizard: add file mode step, end with watch instead of analyze MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add back the file mode Y/N step with a concise explanation of what .graph files are and how agents use them - Replace the "run analyze now?" prompt with an unconditional supermodel watch launch — explains what watch does and how to stop/restart before handing over the terminal Co-Authored-By: Claude Sonnet 4.6 --- internal/setup/wizard.go | 56 +++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/internal/setup/wizard.go b/internal/setup/wizard.go index 5cc3c14..93ec807 100644 --- a/internal/setup/wizard.go +++ b/internal/setup/wizard.go @@ -11,9 +11,9 @@ import ( "github.com/manifoldco/promptui" - "github.com/supermodeltools/cli/internal/analyze" "github.com/supermodeltools/cli/internal/auth" "github.com/supermodeltools/cli/internal/config" + "github.com/supermodeltools/cli/internal/files" ) // ANSI color codes @@ -121,32 +121,64 @@ func Run(ctx context.Context, cfg *config.Config) error { } fmt.Println() + // ── Step 4: File mode ───────────────────────────────────────── + fmt.Printf(" %s◆%s File mode\n", cyan, reset) + fmt.Println() + fmt.Printf(" %sWrites a .graph file next to each source file in your repo.%s\n", dWhite, reset) + fmt.Printf(" %sAgents read them automatically via grep and cat — no extra%s\n", dWhite, reset) + fmt.Printf(" %sprompt changes, no new tools to learn.%s\n", dWhite, reset) + fmt.Println() + fmt.Printf(" %sDisable at any time with:%s %ssupermodel clean%s\n", dWhite, reset, bWhite, reset) + fmt.Println() + + filesEnabled := confirmYN("Enable file mode?", true) + fmt.Println() + + cfg.Files = boolPtr(filesEnabled) + if err := cfg.Save(); err != nil { + fmt.Fprintf(os.Stderr, " %sWarning: could not save config: %v%s\n", yellow, err, reset) + } + + if filesEnabled { + fmt.Printf(" %s✓%s File mode enabled\n", green, reset) + } else { + fmt.Printf(" %s–%s File mode disabled\n", dim, reset) + } + fmt.Println() + // ── Summary ──────────────────────────────────────────────────── fmt.Printf(" %s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n", dim, reset) fmt.Println() fmt.Printf(" %s✓%s Setup complete\n", bGreen, reset) fmt.Println() - fmt.Printf(" %sFile mode%s %senabled%s\n", dim, reset, bWhite, reset) + fileModeStr := "disabled" + if filesEnabled { + fileModeStr = "enabled" + } + fmt.Printf(" %sFile mode%s %s%s%s\n", dim, reset, bWhite, fileModeStr, reset) if hookNote != "" { fmt.Printf(" %sHook%s %s%s%s\n", dim, reset, bWhite, hookNote, reset) } fmt.Println() - fmt.Printf(" %sNext steps:%s\n", dWhite, reset) - fmt.Println() - fmt.Printf(" %ssupermodel analyze%s %sgenerate graph files now%s\n", bWhite, reset, dim, reset) - fmt.Printf(" %ssupermodel watch%s %skeep files updated as you code%s\n", bWhite, reset, dim, reset) - fmt.Println() fmt.Printf(" %s━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━%s\n", dim, reset) fmt.Println() - if confirmYN("Run 'supermodel analyze' now?", true) { - fmt.Println() - return analyze.Run(ctx, cfg, repoDir, analyze.Options{}) - } + // ── Start watch ──────────────────────────────────────────────── + fmt.Printf(" %sStarting watch mode…%s\n", bWhite, reset) + fmt.Println() + fmt.Printf(" %sGenerates your graph now, then stays running to keep it%s\n", dWhite, reset) + fmt.Printf(" %supdated as you edit files. Your agent reads the result via%s\n", dWhite, reset) + fmt.Printf(" %sgrep and cat — no extra steps needed.%s\n", dWhite, reset) + fmt.Println() + fmt.Printf(" %sPress %sCtrl+C%s%s to stop.%s\n", dWhite, bWhite, reset, dWhite, reset) + fmt.Printf(" %sRun %ssupermodel watch%s%s to restart at any time.%s\n", dWhite, bWhite, reset, dWhite, reset) + fmt.Println() - return nil + return files.Watch(ctx, cfg, repoDir, files.WatchOptions{}) } +func boolPtr(b bool) *bool { return &b } + // maskKey returns a display-safe version of the API key. func maskKey(key string) string { if len(key) <= 12 { From a4fe319d893c0c44ee9fa793a8275246682b94b4 Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Tue, 7 Apr 2026 15:23:13 -0400 Subject: [PATCH 4/5] Fix CodeRabbit review comments on first-run setup PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cmd/root.go: split config load error from missing-key nudge so real errors (e.g. corrupt YAML) show the actual message rather than the setup prompt - cmd/root.go: add __complete and __completeNoDesc to noConfigCommands so Cobra's shell completion internals don't crash without a config - install.sh: replace [ -t 1 ] with [ -r /dev/tty ] and redirect /dev/tty as stdin for setup invocation, so interactive prompts work correctly in piped installs (curl … | sh) Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 21 ++++++++++++++------- install.sh | 8 +++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 76ab9fe..612eb23 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,13 +10,16 @@ import ( ) // noConfigCommands are subcommands that work without a config file. +// Includes Cobra's internal shell-completion helpers to avoid crashing them. var noConfigCommands = map[string]bool{ - "setup": true, - "login": true, - "logout": true, - "version": true, - "help": true, - "completion": true, + "setup": true, + "login": true, + "logout": true, + "version": true, + "help": true, + "completion": true, + "__complete": true, + "__completeNoDesc": true, } var rootCmd = &cobra.Command{ @@ -35,7 +38,11 @@ See https://supermodeltools.com for documentation.`, } cfg, err := config.Load() - if err != nil || cfg.APIKey == "" { + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + if cfg.APIKey == "" { fmt.Fprintln(os.Stderr, "Run 'supermodel setup' to get started.") os.Exit(1) } diff --git a/install.sh b/install.sh index 6268a1e..a67b15d 100644 --- a/install.sh +++ b/install.sh @@ -73,8 +73,10 @@ install -m755 "$TMP/$BINARY" "$INSTALL_DIR/$BINARY" echo "Installed: $INSTALL_DIR/$BINARY" "$INSTALL_DIR/$BINARY" version -# Run the setup wizard when attached to a terminal (skip in piped/CI installs). -if [ -t 1 ]; then +# Run the setup wizard when a controlling terminal is available. +# Use /dev/tty as stdin so interactive prompts work even in piped installs +# (e.g. curl … | sh), where stdin is the pipe rather than the terminal. +if [ -r /dev/tty ]; then echo "" - "$INSTALL_DIR/$BINARY" setup + "$INSTALL_DIR/$BINARY" setup Date: Tue, 7 Apr 2026 15:25:00 -0400 Subject: [PATCH 5/5] Fix goimports formatting in noConfigCommands map Co-Authored-By: Claude Sonnet 4.6 --- cmd/root.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 612eb23..a659689 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,14 +12,14 @@ import ( // noConfigCommands are subcommands that work without a config file. // Includes Cobra's internal shell-completion helpers to avoid crashing them. var noConfigCommands = map[string]bool{ - "setup": true, - "login": true, - "logout": true, - "version": true, - "help": true, - "completion": true, - "__complete": true, - "__completeNoDesc": true, + "setup": true, + "login": true, + "logout": true, + "version": true, + "help": true, + "completion": true, + "__complete": true, + "__completeNoDesc": true, } var rootCmd = &cobra.Command{