package cmd import ( "context" "errors" "fmt" "log/slog" "os" "github.com/alecthomas/kong" "github.com/steipete/gogcli/internal/config" "github.com/steipete/gogcli/internal/errfmt" "github.com/steipete/gogcli/internal/googleauth" "github.com/steipete/gogcli/internal/outfmt" "github.com/steipete/gogcli/internal/secrets" "github.com/steipete/gogcli/internal/ui" ) const ( colorAuto = "auto" colorNever = "never" ) type RootFlags struct { Color string `help:"Color output: auto|always|never" default:"${color}"` Account string `help:"Account email for API commands (gmail/calendar/drive/docs/slides/contacts/tasks/people/sheets)"` JSON bool `help:"Output JSON to stdout (best for scripting)" default:"${json}"` Plain bool `help:"Output stable, parseable text to stdout (TSV; no colors)" default:"${plain}"` Force bool `help:"Skip confirmations for destructive commands"` NoInput bool `help:"Never prompt; fail instead (useful for CI)"` Verbose bool `help:"Enable verbose logging"` } type CLI struct { RootFlags `embed:""` Version kong.VersionFlag `help:"Print version and exit"` Auth AuthCmd `cmd:"" help:"Auth and credentials"` Groups GroupsCmd `cmd:"" help:"Google Groups"` Drive DriveCmd `cmd:"" help:"Google Drive"` Docs DocsCmd `cmd:"" help:"Google Docs (export via Drive)"` Slides SlidesCmd `cmd:"" help:"Google Slides"` Calendar CalendarCmd `cmd:"" help:"Google Calendar"` Gmail GmailCmd `cmd:"" aliases:"mail,email" help:"Gmail"` Contacts ContactsCmd `cmd:"" help:"Google Contacts"` Tasks TasksCmd `cmd:"" help:"Google Tasks"` People PeopleCmd `cmd:"" help:"Google People"` Keep KeepCmd `cmd:"" help:"Google Keep (Workspace only)"` Sheets SheetsCmd `cmd:"" help:"Google Sheets"` VersionCmd VersionCmd `cmd:"" name:"version" help:"Print version"` Completion CompletionCmd `cmd:"" help:"Generate shell completion scripts"` } type exitPanic struct{ code int } func Execute(args []string) (err error) { envMode := outfmt.FromEnv() vars := kong.Vars{ "auth_services": googleauth.UserServiceCSV(), "color": envOr("GOG_COLOR", "auto"), "json": boolString(envMode.JSON), "plain": boolString(envMode.Plain), "version": VersionString(), } cli := &CLI{} parser, err := kong.New( cli, kong.Name("gog"), kong.Description(helpDescription()), kong.ConfigureHelp(helpOptions()), kong.Help(helpPrinter), kong.Vars(vars), kong.Writers(os.Stdout, os.Stderr), kong.Exit(func(code int) { panic(exitPanic{code: code}) }), ) if err != nil { return err } defer func() { if r := recover(); r != nil { if ep, ok := r.(exitPanic); ok { if ep.code == 0 { err = nil return } err = &ExitError{Code: ep.code, Err: errors.New("exited")} return } panic(r) } }() kctx, err := parser.Parse(args) if err != nil { parsedErr := wrapParseError(err) _, _ = fmt.Fprintln(os.Stderr, errfmt.Format(parsedErr)) return parsedErr } logLevel := slog.LevelWarn if cli.Verbose { logLevel = slog.LevelDebug } slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: logLevel, }))) mode, err := outfmt.FromFlags(cli.JSON, cli.Plain) if err != nil { return newUsageError(err) } ctx := context.Background() ctx = outfmt.WithMode(ctx, mode) uiColor := cli.Color if outfmt.IsJSON(ctx) || outfmt.IsPlain(ctx) { uiColor = colorNever } u, err := ui.New(ui.Options{ Stdout: os.Stdout, Stderr: os.Stderr, Color: uiColor, }) if err != nil { return err } ctx = ui.WithUI(ctx, u) kctx.BindTo(ctx, (*context.Context)(nil)) kctx.Bind(&cli.RootFlags) err = kctx.Run() if err == nil { return nil } if u := ui.FromContext(ctx); u != nil { u.Err().Error(errfmt.Format(err)) return err } _, _ = fmt.Fprintln(os.Stderr, errfmt.Format(err)) return err } func wrapParseError(err error) error { if err == nil { return nil } var parseErr *kong.ParseError if errors.As(err, &parseErr) { return &ExitError{Code: 2, Err: parseErr} } return err } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func boolString(v bool) string { if v { return "true" } return "false" } func helpDescription() string { desc := "Google CLI for Gmail/Calendar/Drive/Contacts/Tasks/Sheets/Docs/Slides/People" configPath, err := config.ConfigPath() configLine := "unknown" if err != nil { configLine = fmt.Sprintf("error: %v", err) } else if configPath != "" { configLine = configPath } backendInfo, err := secrets.ResolveKeyringBackendInfo() var backendLine string if err != nil { backendLine = fmt.Sprintf("error: %v", err) } else if backendInfo.Value != "" { backendLine = fmt.Sprintf("%s (source: %s)", backendInfo.Value, backendInfo.Source) } return fmt.Sprintf("%s\n\nConfig:\n file: %s\n keyring backend: %s", desc, configLine, backendLine) } // newUsageError wraps errors in a way main() can map to exit code 2. func newUsageError(err error) error { if err == nil { return nil } return &ExitError{Code: 2, Err: err} }