From cf8af6c027881c1cb0aba8d0a2834ff21f9c4ded Mon Sep 17 00:00:00 2001 From: jonathanpopham Date: Wed, 29 Apr 2026 18:19:17 -0400 Subject: [PATCH 1/2] Auto-launch setup wizard on first run of bare 'supermodel' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user runs 'supermodel' with no API key configured, decide what to do based on whether stdin is interactive: - TTY: hand off to setup.Run (the wizard already ends in shards.Watch, so onboarding flows straight into watch). - Non-TTY: return a clean 'not authenticated' error so CI scripts can't hang on a browser-auth prompt. Together this collapses the canonical first-run flow from three commands to one: curl -fsSL https://supermodeltools.com/install.sh | sh cd /path/to/repo supermodel # ← was: supermodel setup 'supermodel setup' remains as the explicit reconfigure entry point. Subcommands keep their existing pre-run gate. The decision is extracted as pickRootAction(hasAPIKey, interactive) so the matrix is unit-testable without invoking the wizard or the daemon. cmd/root_test.go covers all four cases. Refs #151. --- cmd/root.go | 94 ++++++++++++++++++++++++++++++++++++------------ cmd/root_test.go | 26 ++++++++++++++ 2 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 cmd/root_test.go diff --git a/cmd/root.go b/cmd/root.go index 0414980..2a001e9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,14 +3,74 @@ package cmd import ( "fmt" "os" + "syscall" "time" "github.com/spf13/cobra" + "golang.org/x/term" "github.com/supermodeltools/cli/internal/config" + "github.com/supermodeltools/cli/internal/setup" "github.com/supermodeltools/cli/internal/shards" ) +// stdinIsTerminal reports whether stdin is connected to an interactive +// terminal. Pulled into a var so tests can stub it. +var stdinIsTerminal = func() bool { + return term.IsTerminal(int(syscall.Stdin)) //nolint:unconvert // syscall.Stdin is uintptr on Windows +} + +// rootAction enumerates the three branches the bare `supermodel` command +// can take based on auth state and whether stdin is interactive. +type rootAction int + +const ( + // runWatch: an API key is configured; start the watch daemon. + runWatch rootAction = iota + // runSetup: no key, but stdin is a TTY — drop into the setup wizard. + runSetup + // errNotAuthenticated: no key and we can't prompt; surface a clean error. + errNotAuthenticated +) + +// pickRootAction is the decision logic for bare `supermodel`. Pulled into +// a pure function so the dispatch matrix can be unit-tested without +// invoking the wizard or the watch daemon (issue #151). +func pickRootAction(hasAPIKey, interactive bool) rootAction { + switch { + case hasAPIKey: + return runWatch + case interactive: + return runSetup + default: + return errNotAuthenticated + } +} + +// persistentPreRunE gates every subcommand on having an API key, except +// for the bare root command (which handles its own dispatch — see issue +// #151) and noConfigCommands. +func persistentPreRunE(cmd *cobra.Command, args []string) error { + if noConfigCommands[cmd.Name()] { + return nil + } + // The root command (bare `supermodel`) has no parent. Skip the + // pre-run check and let RunE handle interactive vs CI dispatch. + if cmd.Parent() == nil { + return nil + } + cfg, err := config.Load() + 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) + } + return nil +} + // noConfigCommands are subcommands that work without a config file or API key. // Includes Cobra's internal shell-completion helpers to avoid crashing them. var noConfigCommands = map[string]bool{ @@ -40,33 +100,23 @@ enters daemon mode. Listens for file-change notifications from the Press Ctrl+C to stop and remove graph files. See https://supermodeltools.com for documentation.`, - Args: cobra.NoArgs, - 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 { - 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) - } - return nil - }, + Args: cobra.NoArgs, + SilenceUsage: true, + PersistentPreRunE: persistentPreRunE, RunE: func(cmd *cobra.Command, args []string) error { cfg, err := config.Load() if err != nil { return err } - if err := cfg.RequireAPIKey(); err != nil { - return err + // First-run dispatch (issue #151): pick a branch based on auth + // state and whether stdin is interactive. Non-interactive callers + // (CI, scripts, piped stdin) get a clean error so they can't hang + // on a browser-auth prompt. + switch pickRootAction(cfg.APIKey != "", stdinIsTerminal()) { + case runSetup: + return setup.Run(cmd.Context(), cfg) + case errNotAuthenticated: + return fmt.Errorf("not authenticated — run `supermodel setup` or set SUPERMODEL_API_KEY") } dir := watchDir opts := shards.WatchOptions{ diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..bceac9a --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,26 @@ +package cmd + +import "testing" + +func TestPickRootAction(t *testing.T) { + cases := []struct { + name string + hasAPIKey bool + interactive bool + want rootAction + }{ + {"key + tty starts watch", true, true, runWatch}, + {"key + non-tty starts watch (CI happy path)", true, false, runWatch}, + {"no key + tty drops into setup wizard", false, true, runSetup}, + {"no key + non-tty errors instead of hanging", false, false, errNotAuthenticated}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := pickRootAction(tc.hasAPIKey, tc.interactive) + if got != tc.want { + t.Errorf("pickRootAction(hasAPIKey=%v, interactive=%v) = %v, want %v", + tc.hasAPIKey, tc.interactive, got, tc.want) + } + }) + } +} From 918898e2abf3bb3f598da5dff90ffa559eea0e21 Mon Sep 17 00:00:00 2001 From: Grey Newell Date: Thu, 30 Apr 2026 10:01:02 -0400 Subject: [PATCH 2/2] fix(root): address CodeRabbit review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Walk full command ancestry for noConfigCommands check so nested subcommands like 'completion bash' are correctly exempted - Return errors from persistentPreRunE instead of calling os.Exit - Update README quickstart to single-command flow (supermodel auto- launches setup wizard on first run) - Update install URLs: /install.sh → /install Co-Authored-By: Claude Sonnet 4.6 --- README.md | 9 ++++----- cmd/root.go | 12 ++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e8d0a20..5f438b0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Supermodel maps every file, function, and call relationship in your repo and wri ## Linux / Mac ```bash -curl -fsSL https://supermodeltools.com/install.sh | sh +curl -fsSL https://supermodeltools.com/install | sh ``` ## npm (cross-platform) @@ -78,10 +78,10 @@ brew install supermodeltools/tap/supermodel ### Linux / macOS (curl) ```bash -curl -fsSL https://supermodeltools.com/install.sh | sh +curl -fsSL https://supermodeltools.com/install | sh ``` -Runs the setup wizard automatically on first install when attached to a terminal. +On first run, `supermodel` launches the setup wizard automatically when attached to a terminal. ### From source @@ -96,9 +96,8 @@ go build -o supermodel . ## Quick start ```bash -supermodel setup # authenticate + configure (runs automatically after install) cd your/repo -supermodel # generate graph files and keep them updated +supermodel # first run: launches setup wizard, then watches for changes ``` --- diff --git a/cmd/root.go b/cmd/root.go index 2a001e9..374d1c6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -51,8 +51,10 @@ func pickRootAction(hasAPIKey, interactive bool) rootAction { // for the bare root command (which handles its own dispatch — see issue // #151) and noConfigCommands. func persistentPreRunE(cmd *cobra.Command, args []string) error { - if noConfigCommands[cmd.Name()] { - return nil + for c := cmd; c != nil; c = c.Parent() { + if noConfigCommands[c.Name()] { + return nil + } } // The root command (bare `supermodel`) has no parent. Skip the // pre-run check and let RunE handle interactive vs CI dispatch. @@ -61,12 +63,10 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { } cfg, err := config.Load() if err != nil { - fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) - os.Exit(1) + return fmt.Errorf("error loading config: %w", err) } if cfg.APIKey == "" { - fmt.Fprintln(os.Stderr, "Run 'supermodel setup' to get started.") - os.Exit(1) + return fmt.Errorf("run 'supermodel setup' to get started") } return nil }