package cli import ( "fmt" "io" "os" "github.com/spf13/cobra" "github.com/kotoyuuko/cc-switch-cli/internal/config" ) // appState is threaded through every subcommand via cobra's persistent // pre-run. Subcommands read from app.cfg, mutate it in place, and call // app.save() to persist. type appState struct { // Streams. Tests substitute these; production uses os.Std*. stdin io.Reader stdout io.Writer stderr io.Writer // Flags. verbose bool configFlag string // --config; beats env var precedence // Loaded state. configPath string cfg config.Config // requestedExit lets subcommands like `use` hand back a specific exit // code (claude's own) that bypasses cobra's 0/1 mapping. requestedExit *int } func (a *appState) tracef(format string, args ...any) { if !a.verbose { return } fmt.Fprintf(a.stderr, "[cc-switch] "+format+"\n", args...) } // exitCode collapses (requestedExit, cobra err) into a single process exit // code. requestedExit takes precedence — subcommands use it to propagate // claude's own exit code even when we also returned an error for logging. func (a *appState) exitCode(err error) int { if a.requestedExit != nil { return *a.requestedExit } if err != nil { return 1 } return 0 } func (a *appState) save() error { return config.Save(a.configPath, a.cfg) } func newRootCmd(app *appState) *cobra.Command { root := &cobra.Command{ Use: "cc-switch", Short: "Switch between Claude Code provider subscriptions", Long: "cc-switch manages multiple Claude Code provider env profiles and launches the `claude` CLI with the right environment.", SilenceUsage: true, SilenceErrors: false, // Bare run -> `use` (interactive when tty). RunE: func(cmd *cobra.Command, args []string) error { return runUse(cmd, app, args) }, } root.PersistentFlags().BoolVarP(&app.verbose, "verbose", "v", false, "print trace output to stderr") root.PersistentFlags().StringVar(&app.configFlag, "config", "", "path to config file (overrides $CC_SWITCH_CONFIG)") root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // Compute effective config path. if app.configFlag != "" { expanded, err := config.ExpandUser(app.configFlag) if err != nil { return err } app.configPath = expanded } else { p, err := config.ResolvePath() if err != nil { return err } app.configPath = p } app.tracef("config path: %s", app.configPath) res, err := config.Load(app.configPath) if err != nil { return err } if res.Warning != "" { fmt.Fprintln(app.stderr, res.Warning) } app.cfg = res.Config return nil } root.AddCommand( newAddCmd(app), newListCmd(app), newEditCmd(app), newRemoveCmd(app), newUseCmd(app), newConfigCmd(app), newTemplatesCmd(app), newVersionCmd(app), ) return root } // Build-time injected values (see Makefile LDFLAGS). var ( version = "dev" commit = "none" date = "unknown" ) func newVersionCmd(app *appState) *cobra.Command { return &cobra.Command{ Use: "version", Short: "Print version information", RunE: func(cmd *cobra.Command, args []string) error { _, err := fmt.Fprintf(app.stdout, "cc-switch %s (commit %s, built %s)\n", version, commit, date) return err }, } } // ensure os.Stdin satisfies io.Reader — helps static tools. var _ io.Reader = os.Stdin