Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Calendar: add `calendar events --sort=start|end|summary|calendar` and `--order=asc|desc` so `--all` output can be returned chronologically across calendars instead of per-calendar API iteration order. Also documents `now` in the `--from`/`--to` help strings (already accepted by `timeparse`) — the relative form agents need when planning "from now on" — thanks @gado-ships-it.
- Calendar: add `calendar events --location` to include event locations in table output. Embedded newlines in the location string are collapsed so multi-line addresses still render on one row — thanks @gado-ships-it.
- Auth: add `gog auth import --client --email` with `--refresh-token-stdin`, `--refresh-token-file`, or `--refresh-token-env` for non-interactive token import without exposing secrets in argv — thanks @jcarnegie.
- Drive: add `drive share --notify` for invite targets that require a Drive notification email.
- Calendar: keep `calendar appointments` as an explicit diagnostic because the Calendar API still rejects `eventTypes=appointmentSchedule`. (#329)
- CLI: add nested `docs tabs ...` and `forms questions ...` aliases for consistent sub-item command patterns while preserving existing flat commands. (#433)
Expand Down
1 change: 1 addition & 0 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Generated from `gog schema --json`.
- [`gog auth credentials remove [<client>]`](commands/gog-auth-credentials-remove.md) - Remove stored OAuth client credentials
- [`gog auth credentials set <credentials> [flags]`](commands/gog-auth-credentials-set.md) - Store OAuth client credentials
- [`gog auth doctor [flags]`](commands/gog-auth-doctor.md) - Diagnose auth, keyring, and refresh-token issues
- [`gog auth import --email=STRING [flags]`](commands/gog-auth-import.md) - Import a refresh token non-interactively from stdin, file, or env
- [`gog auth keep --key=STRING <email>`](commands/gog-auth-keep.md) - Configure service account for Google Keep (Workspace only)
- [`gog auth keyring [<backend> [<backend2>]]`](commands/gog-auth-keyring.md) - Configure keyring backend
- [`gog auth list [flags]`](commands/gog-auth-list.md) - List stored accounts
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.

Generated pages: 559.
Generated pages: 560.

## Top-level Commands

Expand Down Expand Up @@ -93,6 +93,7 @@ Generated pages: 559.
- [gog auth credentials remove](gog-auth-credentials-remove.md) - Remove stored OAuth client credentials
- [gog auth credentials set](gog-auth-credentials-set.md) - Store OAuth client credentials
- [gog auth doctor](gog-auth-doctor.md) - Diagnose auth, keyring, and refresh-token issues
- [gog auth import](gog-auth-import.md) - Import a refresh token non-interactively from stdin, file, or env
- [gog auth keep](gog-auth-keep.md) - Configure service account for Google Keep (Workspace only)
- [gog auth keyring](gog-auth-keyring.md) - Configure keyring backend
- [gog auth list](gog-auth-list.md) - List stored accounts
Expand Down
48 changes: 48 additions & 0 deletions docs/commands/gog-auth-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# `gog auth import`

> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.

Import a refresh token non-interactively from stdin, file, or env

## Usage

```bash
gog auth import --email=STRING [flags]
```

## Parent

- [gog auth](gog-auth.md)

## Flags

| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/drivelabels/docs/slides/contacts/tasks/people/sheets/forms/sites/appscript/analytics/searchconsole/ads/photos) |
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
| `--color` | `string` | auto | Color output: auto\|always\|never |
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
| `--email` | `string` | | Account email |
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--refresh-token-env` | `string` | | Read OAuth refresh token from the named environment variable |
| `--refresh-token-file` | `string` | | Read OAuth refresh token from file |
| `--refresh-token-stdin` | `bool` | | Read OAuth refresh token from stdin |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `--services` | `string` | | Comma-separated services to record on the token (informational; does not affect scopes) |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |

## See Also

- [gog auth](gog-auth.md)
- [Command index](README.md)
1 change: 1 addition & 0 deletions docs/commands/gog-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ gog auth <command> [flags]
- [gog auth alias](gog-auth-alias.md) - Manage account aliases
- [gog auth credentials](gog-auth-credentials.md) - Manage OAuth client credentials
- [gog auth doctor](gog-auth-doctor.md) - Diagnose auth, keyring, and refresh-token issues
- [gog auth import](gog-auth-import.md) - Import a refresh token non-interactively from stdin, file, or env
- [gog auth keep](gog-auth-keep.md) - Configure service account for Google Keep (Workspace only)
- [gog auth keyring](gog-auth-keyring.md) - Configure keyring backend
- [gog auth list](gog-auth-list.md) - List stored accounts
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const (
type AuthCmd struct {
Credentials AuthCredentialsCmd `cmd:"" name:"credentials" help:"Manage OAuth client credentials"`
Add AuthAddCmd `cmd:"" name:"add" help:"Authorize and store a refresh token"`
Import AuthImportCmd `cmd:"" name:"import" help:"Import a refresh token non-interactively from stdin, file, or env"`
Services AuthServicesCmd `cmd:"" name:"services" help:"List supported auth services and scopes"`
List AuthListCmd `cmd:"" name:"list" help:"List stored accounts"`
Doctor AuthDoctorCmd `cmd:"" name:"doctor" help:"Diagnose auth, keyring, and refresh-token issues"`
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/auth_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (c *AuthStatusCmd) Run(ctx context.Context, flags *RootFlags) error {
if flags != nil {
if a, err := requireAccount(flags); err == nil {
account = a
resolvedClient, resolveErr := resolveClientForEmail(account, flags, "")
resolvedClient, resolveErr := resolveClientForEmail(account, flags)
if resolveErr != nil {
return resolveErr
}
Expand Down Expand Up @@ -222,7 +222,7 @@ func (c *AuthRemoveCmd) Run(ctx context.Context, flags *RootFlags) error {
if err != nil {
return err
}
client, err := resolveClientForEmail(email, flags, "")
client, err := resolveClientForEmail(email, flags)
if err != nil {
return err
}
Expand Down
160 changes: 160 additions & 0 deletions internal/cmd/auth_import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package cmd

import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"

"github.com/99designs/keyring"

"github.com/steipete/gogcli/internal/config"
"github.com/steipete/gogcli/internal/secrets"
"github.com/steipete/gogcli/internal/ui"
)

var readAuthImportStdin = func() ([]byte, error) {
return io.ReadAll(os.Stdin)
}

type AuthImportCmd struct {
Email string `name:"email" required:"" help:"Account email"`
RefreshTokenStdin bool `name:"refresh-token-stdin" help:"Read OAuth refresh token from stdin"`
RefreshTokenFile string `name:"refresh-token-file" type:"path" help:"Read OAuth refresh token from file"`
RefreshTokenEnv string `name:"refresh-token-env" help:"Read OAuth refresh token from the named environment variable"`
ServicesCSV string `name:"services" help:"Comma-separated services to record on the token (informational; does not affect scopes)"`
}

func (c *AuthImportCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)

email := normalizeEmail(c.Email)
if email == "" {
return usage("--email is required")
}

refreshToken, tokenErr := c.resolveRefreshToken()
if tokenErr != nil {
return tokenErr
}

override := ""
if flags != nil {
override = flags.Client
}
client, clientErr := resolveClientForEmail(email, flags)
if clientErr != nil {
return clientErr
}

services := splitCommaList(c.ServicesCSV)
force := flags != nil && flags.Force

if err := dryRunExit(ctx, flags, "auth.import", map[string]any{
"client": client,
"email": email,
"services": services,
"force": force,
}); err != nil {
return err
}

if err := ensureKeychainAccessIfNeeded(); err != nil {
return fmt.Errorf("keychain access: %w", err)
}

store, err := openSecretsStore()
if err != nil {
return err
}

if !force {
if _, getErr := store.GetToken(client, email); getErr == nil {
return usagef("entry already exists for client=%q email=%q (use --force to overwrite)", client, email)
} else if !errors.Is(getErr, keyring.ErrKeyNotFound) {
return getErr
}
}

if err := store.SetToken(client, email, secrets.Token{
Client: client,
Email: email,
Services: services,
RefreshToken: refreshToken,
}); err != nil {
return err
}
if strings.TrimSpace(override) != "" {
cfg, err := config.ReadConfig()
if err != nil {
return err
}
if err := config.SetAccountClient(&cfg, email, client); err != nil {
return err
}
if err := config.WriteConfig(cfg); err != nil {
return err
}
}

return writeResult(ctx, u,
kv("imported", true),
kv("client", client),
kv("email", email),
)
}

func (c *AuthImportCmd) resolveRefreshToken() (string, error) {
sources := 0
if c.RefreshTokenStdin {
sources++
}
if strings.TrimSpace(c.RefreshTokenFile) != "" {
sources++
}
if strings.TrimSpace(c.RefreshTokenEnv) != "" {
sources++
}
if sources == 0 {
return "", usage("provide refresh token with --refresh-token-stdin, --refresh-token-file, or --refresh-token-env")
}
if sources > 1 {
return "", usage("provide exactly one refresh token source")
}

var (
raw []byte
err error
)
switch {
case c.RefreshTokenStdin:
raw, err = readAuthImportStdin()
if err != nil {
return "", fmt.Errorf("read --refresh-token-stdin: %w", err)
}
case strings.TrimSpace(c.RefreshTokenFile) != "":
path, expandErr := config.ExpandPath(strings.TrimSpace(c.RefreshTokenFile))
if expandErr != nil {
return "", fmt.Errorf("expand --refresh-token-file: %w", expandErr)
}
raw, err = os.ReadFile(path) //nolint:gosec // user-provided token file path
if err != nil {
return "", fmt.Errorf("read --refresh-token-file: %w", err)
}
case strings.TrimSpace(c.RefreshTokenEnv) != "":
envName := strings.TrimSpace(c.RefreshTokenEnv)
value, ok := os.LookupEnv(envName)
if !ok {
return "", usagef("environment variable %s is not set", envName)
}
raw = []byte(value)
}

token := strings.TrimSpace(string(raw))
if token == "" {
return "", usage("refresh token must not be empty")
}
return token, nil
}
Loading