205 lines
5.1 KiB
Go
205 lines
5.1 KiB
Go
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}
|
|
}
|