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 @@ -7,6 +7,7 @@
- Photos: add an explicit-opt-in Google Photos Picker workflow for creating selection sessions, waiting for completion, listing chosen media, and downloading selected files. (#754)
- Docs: add persisted, revision-locked request batches for composing supported mutations locally and submitting them atomically, with explicit split and partial-recovery modes. (#755)
- CLI: remove the separate `gog agent` and `exit-codes` helpers; expose stable exit codes and effective automation safety state through `gog schema --json`, and summarize the contract in root help. (#677)
- CLI: add Git-style `gog help <command>`, make explicit output flags override environment defaults, validate color and JSON-only transforms before command execution, report early usage errors on stderr, and reject contradictory schema plain output.

## 0.24.0 - 2026-06-11

Expand Down
11 changes: 11 additions & 0 deletions docs/automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ Root help summarizes the human-facing contract:

```bash
gog --help
gog help drive ls
```

`gog help <command>` and `gog <command> --help` are equivalent. Once a help
flag is present, trailing arguments are ignored so recovery help remains
available after a malformed command attempt.

The machine-readable contract is:

```bash
Expand All @@ -34,6 +39,12 @@ gog --json gmail search 'newer_than:7d'
gog --plain calendar events --today
```

`--results-only` and `--select` transform JSON and therefore require
`--json`. Contradictory output flags fail with usage exit code 2 instead of
being silently ignored. Explicit output flags override `GOG_JSON` and
`GOG_PLAIN` environment defaults. `gog schema` always emits JSON and rejects
`--plain`.

Use `--no-input` in CI and unattended processes. Use `--wrap-untrusted` when
Google-hosted free text will be consumed by an LLM or another instruction-aware
system.
Expand Down
4 changes: 2 additions & 2 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ Generated from `gog schema --json`.
- [`gog classroom (class) students (student) add (create,new) <courseId> <userId> [flags]`](commands/gog-classroom-students-add.md) - Add a student
- [`gog classroom (class) students (student) get (info,show) <courseId> <userId>`](commands/gog-classroom-students-get.md) - Get a student
- [`gog classroom (class) students (student) list (ls) <courseId> [flags]`](commands/gog-classroom-students-list.md) - List students
- [`gog classroom (class) students (student) remove (delete,rm,del,remove) <courseId> <userId>`](commands/gog-classroom-students-remove.md) - Remove a student
- [`gog classroom (class) students (student) remove (delete,rm,del) <courseId> <userId>`](commands/gog-classroom-students-remove.md) - Remove a student
- [`gog classroom (class) submissions (submission) <command>`](commands/gog-classroom-submissions.md) - Student submissions
- [`gog classroom (class) submissions (submission) get (info,show) <courseId> <courseworkId> <submissionId>`](commands/gog-classroom-submissions-get.md) - Get a student submission
- [`gog classroom (class) submissions (submission) grade (set,edit) <courseId> <courseworkId> <submissionId> [flags]`](commands/gog-classroom-submissions-grade.md) - Set draft/assigned grades
Expand All @@ -186,7 +186,7 @@ Generated from `gog schema --json`.
- [`gog classroom (class) teachers (teacher) add (create,new) <courseId> <userId>`](commands/gog-classroom-teachers-add.md) - Add a teacher
- [`gog classroom (class) teachers (teacher) get (info,show) <courseId> <userId>`](commands/gog-classroom-teachers-get.md) - Get a teacher
- [`gog classroom (class) teachers (teacher) list (ls) <courseId> [flags]`](commands/gog-classroom-teachers-list.md) - List teachers
- [`gog classroom (class) teachers (teacher) remove (delete,rm,del,remove) <courseId> <userId>`](commands/gog-classroom-teachers-remove.md) - Remove a teacher
- [`gog classroom (class) teachers (teacher) remove (delete,rm,del) <courseId> <userId>`](commands/gog-classroom-teachers-remove.md) - Remove a teacher
- [`gog classroom (class) topics (topic) <command>`](commands/gog-classroom-topics.md) - Topics
- [`gog classroom (class) topics (topic) create (add,new) --name=STRING <courseId>`](commands/gog-classroom-topics-create.md) - Create a topic
- [`gog classroom (class) topics (topic) delete (rm,del,remove) <courseId> <topicId>`](commands/gog-classroom-topics-delete.md) - Delete a topic
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/gog-classroom-students-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Remove a student
## Usage

```bash
gog classroom (class) students (student) remove (delete,rm,del,remove) <courseId> <userId>
gog classroom (class) students (student) remove (delete,rm,del) <courseId> <userId>
```

## Parent
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/gog-classroom-teachers-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Remove a teacher
## Usage

```bash
gog classroom (class) teachers (teacher) remove (delete,rm,del,remove) <courseId> <userId>
gog classroom (class) teachers (teacher) remove (delete,rm,del) <courseId> <userId>
```

## Parent
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/classroom_rosters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ClassroomStudentsCmd struct {
List ClassroomStudentsListCmd `cmd:"" default:"withargs" aliases:"ls" help:"List students"`
Get ClassroomStudentsGetCmd `cmd:"" aliases:"info,show" help:"Get a student"`
Add ClassroomStudentsAddCmd `cmd:"" aliases:"create,new" help:"Add a student"`
Remove ClassroomStudentsRemoveCmd `cmd:"" aliases:"delete,rm,del,remove" help:"Remove a student"`
Remove ClassroomStudentsRemoveCmd `cmd:"" aliases:"delete,rm,del" help:"Remove a student"`
}

type ClassroomStudentsListCmd struct {
Expand Down Expand Up @@ -244,7 +244,7 @@ type ClassroomTeachersCmd struct {
List ClassroomTeachersListCmd `cmd:"" default:"withargs" aliases:"ls" help:"List teachers"`
Get ClassroomTeachersGetCmd `cmd:"" aliases:"info,show" help:"Get a teacher"`
Add ClassroomTeachersAddCmd `cmd:"" aliases:"create,new" help:"Add a teacher"`
Remove ClassroomTeachersRemoveCmd `cmd:"" aliases:"delete,rm,del,remove" help:"Remove a teacher"`
Remove ClassroomTeachersRemoveCmd `cmd:"" aliases:"delete,rm,del" help:"Remove a teacher"`
}

type ClassroomTeachersListCmd struct {
Expand Down
22 changes: 22 additions & 0 deletions internal/cmd/desire_paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ func TestDesirePaths_GlobalFlagAliases(t *testing.T) {
}
}

func TestDesirePaths_RewriteHelp(t *testing.T) {
tests := []struct {
name string
in []string
want []string
}{
{name: "root", in: []string{"help"}, want: []string{"--help"}},
{name: "command", in: []string{"help", "drive", "ls"}, want: []string{"drive", "ls", "--help"}},
{name: "global flag", in: []string{"--color", "never", "help", "gmail"}, want: []string{"--color", "never", "gmail", "--help"}},
{name: "help ignores trailing args", in: []string{"drive", "--help", "nonsense"}, want: []string{"drive", "--help"}},
{name: "help after delimiter is data", in: []string{"open", "--", "--help"}, want: []string{"open", "--", "--help"}},
{name: "global value named help", in: []string{"--account", "help", "version"}, want: []string{"--account", "help", "version"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := rewriteHelpArgs(tt.in); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("rewriteHelpArgs(%v) = %v, want %v", tt.in, got, tt.want)
}
})
}
}

func TestDesirePaths_DryRunAlias_ExitsBeforeAuth(t *testing.T) {
out := captureStdout(t, func() {
_ = captureStderr(t, func() {
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/help_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func injectAutomationHelp(out string, selected *kong.Node) string {

const section = `Automation:
Use --json or --plain for stable output; --no-input disables prompts.
Use "gog help <command>" or "gog <command> --help" for command help.
Exit codes: 0 success, 1 error, 2 usage, 3 empty, 4 auth, 5 not found,
6 denied, 7 rate limited, 8 retryable, 10 config, 11 orphaned,
130 interrupted.
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/help_snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func TestHelpSnapshot_RootAutomationContract(t *testing.T) {
requireHelpContains(t, out,
"\nAutomation:\n",
"Use --json or --plain for stable output",
`Use "gog help <command>"`,
"Exit codes: 0 success",
`Run "gog schema --json"`,
)
Expand Down
111 changes: 95 additions & 16 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,23 @@ func Execute(args []string) (err error) {
if len(args) == 0 {
args = []string{"--help"}
}
args = rewriteHelpArgs(args)
args = rewriteDesirePathArgs(args)
args = rewriteDocsCellUpdateContentArgs(args)

preHomeApplied := false
if home, ok := preScanHomeArg(args); ok {
restoreHome, homeErr := config.SetHomeOverride(home)
if homeErr != nil {
return newUsageError(homeErr)
return reportEarlyError(newUsageError(homeErr))
}
preHomeApplied = true
defer restoreHome()
}

parser, cli, err := newParser(helpDescription())
if err != nil {
return err
return reportEarlyError(err)
}

defer func() {
Expand All @@ -142,33 +143,28 @@ func Execute(args []string) (err error) {

kctx, err := parser.Parse(args)
if err != nil {
parsedErr := wrapParseError(err)
_, _ = fmt.Fprintln(os.Stderr, errfmt.Format(parsedErr))
return parsedErr
return reportEarlyError(wrapParseError(err))
}
applyExplicitOutputModePrecedence(kctx, &cli.RootFlags)
if !preHomeApplied && strings.TrimSpace(cli.Home) != "" {
restoreHome, homeErr := config.SetHomeOverride(cli.Home)
if homeErr != nil {
return newUsageError(homeErr)
return reportEarlyError(newUsageError(homeErr))
}
defer restoreHome()
}

if err = enforceBakedSafetyProfile(kctx); err != nil {
_, _ = fmt.Fprintln(os.Stderr, errfmt.Format(err))
return err
return reportEarlyError(err)
}
if err = enforceEnabledCommands(kctx, cli.EnableCommands, cli.EnableCommandsExact); err != nil {
_, _ = fmt.Fprintln(os.Stderr, errfmt.Format(err))
return err
return reportEarlyError(err)
}
if err = enforceDisabledCommands(kctx, cli.DisableCommands); err != nil {
_, _ = fmt.Fprintln(os.Stderr, errfmt.Format(err))
return err
return reportEarlyError(err)
}
if err = enforceGmailNoSend(kctx, &cli.RootFlags); err != nil {
_, _ = fmt.Fprintln(os.Stderr, errfmt.Format(err))
return err
return reportEarlyError(err)
}

logLevel := slog.LevelWarn
Expand All @@ -187,7 +183,11 @@ func Execute(args []string) (err error) {

mode, err := outfmt.FromFlags(cli.JSON, cli.Plain)
if err != nil {
return newUsageError(err)
return reportEarlyError(newUsageError(err))
}
err = validateJSONTransformFlags(mode, &cli.RootFlags)
if err != nil {
return reportEarlyError(err)
}

ctx := context.Background()
Expand Down Expand Up @@ -216,7 +216,7 @@ func Execute(args []string) (err error) {
Color: uiColor,
})
if err != nil {
return err
return reportEarlyError(newUsageError(err))
}
ctx = ui.WithUI(ctx, u)

Expand Down Expand Up @@ -247,6 +247,85 @@ func Execute(args []string) (err error) {
return err
}

func rewriteHelpArgs(args []string) []string {
for i, arg := range args {
if arg == "--" {
break
}
if arg == "--help" || arg == "-h" {
return append([]string(nil), args[:i+1]...)
}
}

for i := 0; i < len(args); i++ {
arg := args[i]
if arg == "--" {
return args
}
if strings.HasPrefix(arg, "-") {
if globalFlagTakesValue(arg) && i+1 < len(args) {
i++
}
continue
}
if arg != "help" {
return args
}

out := make([]string, 0, len(args))
out = append(out, args[:i]...)
out = append(out, args[i+1:]...)
out = append(out, "--help")
Comment on lines +277 to +278

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not append help after malformed command flags

When a user runs a Git-style help command with a partial or malformed flag, e.g. gog help drive ls --max nope or gog help send --body, this rewrite preserves those tokens before the synthetic --help. Kong will parse/validate them first, or may consume --help as the missing flag value, so this path can error or enter command validation instead of reliably showing help. This makes gog help <command> less robust than the new gog <command> --help truncation; drop trailing non-path arguments or insert the help flag before them.

Useful? React with 👍 / 👎.

return out
}
return args
}

func validateJSONTransformFlags(mode outfmt.Mode, flags *RootFlags) error {
if flags == nil || mode.JSON {
return nil
}

hasResultsOnly := flags.ResultsOnly
hasSelect := strings.TrimSpace(flags.Select) != ""
switch {
case hasResultsOnly && hasSelect:
return usage("--results-only and --select require --json")
case hasResultsOnly:
return usage("--results-only requires --json")
case hasSelect:
return usage("--select requires --json")
default:
return nil
}
}

func applyExplicitOutputModePrecedence(kctx *kong.Context, flags *RootFlags) {
if flags == nil {
return
}

jsonSet := flagProvided(kctx, "json")
plainSet := flagProvided(kctx, "plain")
switch {
case jsonSet && !plainSet:
flags.Plain = false
case plainSet && !jsonSet:
flags.JSON = false
}
}

func reportEarlyError(err error) error {
if err == nil {
return nil
}
msg := strings.TrimSpace(errfmt.Format(err))
if msg != "" {
_, _ = fmt.Fprintln(os.Stderr, msg)
}
return err
}

func rewriteDesirePathArgs(args []string) []string {
// Some commands use `--fields` for API field masks. Agents also frequently
// guess `--fields` to mean "select output fields", so we squat it everywhere
Expand Down
Loading
Loading