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 @@ -10,6 +10,7 @@
- Docs: add `gog docs insert-page-break <docId> [--index N | --at-end] [--tab=STRING]` to insert a Google Docs page break directly via `InsertPageBreakRequest` — markdown has no native page-break construct, so this is the only path for multi-page deliverables. Aliases: `page-break`, `pb`. (#604)
- Docs: add `--heading-level N` (1..6 shortcut) and `--named-style NAME` (full enum) to `gog docs format` so existing paragraphs can be promoted to `HEADING_1`..`HEADING_6`, `TITLE`, `SUBTITLE`, or `NORMAL_TEXT`. Both set `paragraphStyle.namedStyleType` on the existing UpdateParagraphStyle request and compose cleanly with `--alignment` / `--line-spacing`. (#605)
- Sheets: add `gog sheets reorder-tab <spreadsheetId> --tab=<name|sheetId> --to=N` to move a tab to a specific 0-based position via `updateSheetProperties` with field mask `index`. `--tab` accepts a title or a numeric sheet ID; `--to=0` is force-sent so the leftmost target reaches the wire as `"index":0`. Aliases: `move-tab`, `reorder-sheet`, `move-sheet`. (#603)
- Docs: add `gog docs insert-table <docId> --rows N --cols M [--index N | --at-end] [--values-json [[...]]] [--tab=STRING]` to insert a native Google Docs table directly via `InsertTableRequest`, bypassing the markdown writer. `--values-json` takes a JSON 2D string array whose dimensions must match `--rows`x`--cols`. Empty `--values-json` produces an empty table structure. (#602)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ Docs: [Google Docs editing](docs/docs-editing.md),
gog docs write <docId> --append --markdown --text '## Status'
gog docs format <docId> --match Status --bold --font-size 18
gog docs insert-page-break <docId> --at-end
gog docs insert-table <docId> --rows 3 --cols 2 --at-end
gog docs add-tab <docId> --title "Notes"
gog docs tabs add <docId> --title "Notes"
gog docs find-replace <docId> old new --tab "Notes" --dry-run
Expand Down
1 change: 1 addition & 0 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ Generated from `gog schema --json`.
- [`gog docs (doc) info (get,show) <docId>`](commands/gog-docs-info.md) - Get Google Doc metadata
- [`gog docs (doc) insert <docId> [<content>] [flags]`](commands/gog-docs-insert.md) - Insert text at a specific position
- [`gog docs (doc) insert-page-break (page-break,pb) <docId> [flags]`](commands/gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
- [`gog docs (doc) insert-table --rows=INT --cols=INT <docId> [flags]`](commands/gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
- [`gog docs (doc) list-tabs <docId>`](commands/gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [`gog docs (doc) raw <docId> [flags]`](commands/gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [`gog docs (doc) rename-tab <docId> [flags]`](commands/gog-docs-rename-tab.md) - Rename a tab in a Google Doc
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: 566.
Generated pages: 567.

## Top-level Commands

Expand Down Expand Up @@ -290,6 +290,7 @@ Generated pages: 566.
- [gog docs info](gog-docs-info.md) - Get Google Doc metadata
- [gog docs insert](gog-docs-insert.md) - Insert text at a specific position
- [gog docs insert-page-break](gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
- [gog docs insert-table](gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
Expand Down
49 changes: 49 additions & 0 deletions docs/commands/gog-docs-insert-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# `gog docs insert-table`

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

Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json

## Usage

```bash
gog docs (doc) insert-table --rows=INT --cols=INT <docId> [flags]
```

## Parent

- [gog docs](gog-docs.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) |
| `--at-end` | `bool` | | Insert at end-of-doc/tab (mutually exclusive with --index) |
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
| `--color` | `string` | auto | Color output: auto\|always\|never |
| `--cols` | `int` | | Number of columns (>=1) |
| `--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 |
| `--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. |
| `--index` | `*int64` | | Character index to insert at (1 = beginning). Omit or use --at-end for end-of-doc. |
| `-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) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--rows` | `int` | | Number of rows (>=1) |
| `--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. |
| `--tab` | `string` | | Target a specific tab by title or ID (see docs list-tabs) |
| `--values-json` | `string` | | Cell values as a JSON 2D string array; dimensions must match --rows x --cols when supplied |
| `-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 docs](gog-docs.md)
- [Command index](README.md)
1 change: 1 addition & 0 deletions docs/commands/gog-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ gog docs (doc) <command> [flags]
- [gog docs info](gog-docs-info.md) - Get Google Doc metadata
- [gog docs insert](gog-docs-insert.md) - Insert text at a specific position
- [gog docs insert-page-break](gog-docs-insert-page-break.md) - Insert a page break at a specific position (or end-of-doc with --at-end)
- [gog docs insert-table](gog-docs-insert-table.md) - Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
Expand Down
22 changes: 22 additions & 0 deletions docs/docs-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ Command page:

- [`gog docs insert-page-break`](commands/gog-docs-insert-page-break.md)

## Tables

Insert a native Google Docs table directly via the Docs API, bypassing the
Markdown writer:

```bash
gog docs insert-table <docId> --rows 3 --cols 2 --at-end
gog docs insert-table <docId> --rows 2 --cols 2 --index 1 \
--values-json '[["A","B"],["C","D"]]'
```

`--values-json` takes a JSON 2D string array whose dimensions must match
`--rows`x`--cols`; omit it to insert an empty table structure. Use `--at-end`
to append at the end of the document (or the selected `--tab`), or `--index N`
to insert at a specific document index. Prefer this primitive when you want a
guaranteed native table rather than relying on the Markdown writer's table
rendering (see `gog docs write --markdown`).

Command page:

- [`gog docs insert-table`](commands/gog-docs-insert-table.md)

## Tabs

Manage Google Docs tabs:
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type DocsCmd struct {
ListTabs DocsListTabsCmd `cmd:"" name:"list-tabs" help:"List all tabs in a Google Doc"`
Write DocsWriteCmd `cmd:"" name:"write" help:"Write content to a Google Doc"`
Insert DocsInsertCmd `cmd:"" name:"insert" help:"Insert text at a specific position"`
InsertTable DocsInsertTableCmd `cmd:"" name:"insert-table" help:"Insert a native table at a specific position (or end-of-doc with --at-end), optionally populated via --values-json"`
InsertPageBreak DocsInsertPageBreakCmd `cmd:"" name:"insert-page-break" aliases:"page-break,pb" help:"Insert a page break at a specific position (or end-of-doc with --at-end)"`
Delete DocsDeleteCmd `cmd:"" name:"delete" help:"Delete text range from document"`
FindReplace DocsFindReplaceCmd `cmd:"" name:"find-replace" help:"Find and replace text. Supports plain text or markdown with images; use --first for a single occurrence."`
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/docs_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root
}
c.Tab = tab

var index any = "end"
var index any = docsAtIndexEnd
if c.Index > 0 {
index = c.Index
}
Expand Down Expand Up @@ -522,7 +522,7 @@ func (c *DocsInsertCmd) Run(ctx context.Context, flags *RootFlags) error {
if c.Index != nil {
dryRunPayload["atIndex"] = *c.Index
} else {
dryRunPayload["atIndex"] = "end"
dryRunPayload["atIndex"] = docsAtIndexEnd
}
if dryRunErr := dryRunExit(ctx, flags, "docs.insert", dryRunPayload); dryRunErr != nil {
return dryRunErr
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/docs_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/steipete/gogcli/internal/config"
)

const docsAtIndexEnd = "end"

func resolveContentInput(content, filePath string) (string, error) {
if content != "" {
return content, nil
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/docs_insert_page_break.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *DocsInsertPageBreakCmd) Run(ctx context.Context, flags *RootFlags) erro
"tab": c.Tab,
}
if resolveEnd {
dryRunPayload["atIndex"] = "end"
dryRunPayload["atIndex"] = docsAtIndexEnd
} else {
dryRunPayload["atIndex"] = *c.Index
}
Expand Down
158 changes: 158 additions & 0 deletions internal/cmd/docs_insert_table.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cmd

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)

// DocsInsertTableCmd inserts a native Google Docs table at a specific
// character index (or at end-of-doc) and optionally populates it from a JSON
// 2D string array. The markdown writer can already render tables, but it
// drops them mid-insert in some scenarios — see #592/#607/#608/#609 — and
// agents needed a path that bypasses the markdown converter entirely (#602).
type DocsInsertTableCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Rows int `name:"rows" required:"" help:"Number of rows (>=1)"`
Cols int `name:"cols" required:"" help:"Number of columns (>=1)"`
Index *int64 `name:"index" help:"Character index to insert at (1 = beginning). Omit or use --at-end for end-of-doc."`
AtEnd bool `name:"at-end" help:"Insert at end-of-doc/tab (mutually exclusive with --index)"`
ValuesJSON string `name:"values-json" help:"Cell values as a JSON 2D string array; dimensions must match --rows x --cols when supplied"`
Tab string `name:"tab" help:"Target a specific tab by title or ID (see docs list-tabs)"`
TabID string `name:"tab-id" hidden:"" help:"(deprecated) Use --tab"`
}

func (c *DocsInsertTableCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
docID := strings.TrimSpace(c.DocID)
if docID == "" {
return usage("empty docId")
}
if c.Rows < 1 {
return usage("--rows must be >= 1")
}
if c.Cols < 1 {
return usage("--cols must be >= 1")
}
if c.AtEnd && c.Index != nil {
return usage("--at-end and --index are mutually exclusive")
}
if c.Index != nil && *c.Index < 1 {
return usage("--index must be >= 1 (index 0 is reserved)")
}

cells, err := parseTableValuesJSON(c.ValuesJSON, c.Rows, c.Cols)
if err != nil {
return err
}

tab, tabErr := resolveTabArg(ctx, c.Tab, c.TabID)
if tabErr != nil {
return tabErr
}
c.Tab = tab

resolveEnd := c.AtEnd || c.Index == nil

dryRunPayload := map[string]any{
"documentId": docID,
"rows": c.Rows,
"cols": c.Cols,
"tab": c.Tab,
}
if resolveEnd {
dryRunPayload["atIndex"] = docsAtIndexEnd
} else {
dryRunPayload["atIndex"] = *c.Index
}
if dryRunErr := dryRunExit(ctx, flags, "docs.insert-table", dryRunPayload); dryRunErr != nil {
return dryRunErr
}

svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}

var insertIndex int64
if resolveEnd {
endIndex, tabID, endErr := docsTargetEndIndexAndTabID(ctx, svc, docID, c.Tab)
if endErr != nil {
return endErr
}
c.Tab = tabID
insertIndex = docsAppendIndex(endIndex)
} else {
insertIndex = *c.Index
if c.Tab != "" {
tabID, tabErr := resolveDocsTabID(ctx, svc, docID, c.Tab)
if tabErr != nil {
return tabErr
}
c.Tab = tabID
}
}

inserter := NewTableInserter(svc, docID)
tableEnd, err := inserter.InsertNativeTable(ctx, insertIndex, cells, c.Tab)
if err != nil {
return fmt.Errorf("insert table: %w", err)
}

if outfmt.IsJSON(ctx) {
payload := map[string]any{
"documentId": docID,
"atIndex": insertIndex,
"rows": c.Rows,
"cols": c.Cols,
"tableEnd": tableEnd,
}
if c.Tab != "" {
payload["tabId"] = c.Tab
}
return outfmt.WriteJSON(ctx, os.Stdout, payload)
}

u.Out().Linef("documentId\t%s", docID)
u.Out().Linef("atIndex\t%d", insertIndex)
u.Out().Linef("rows\t%d", c.Rows)
u.Out().Linef("cols\t%d", c.Cols)
u.Out().Linef("tableEnd\t%d", tableEnd)
if c.Tab != "" {
u.Out().Linef("tabId\t%s", c.Tab)
}
return nil
}

// parseTableValuesJSON converts a JSON 2D string array into a rows x cols cell
// matrix. When the input is empty, returns an all-empty rows x cols matrix
// suitable for inserting an empty table structure. Validates that the supplied
// JSON exactly matches the requested dimensions.
func parseTableValuesJSON(raw string, rows, cols int) ([][]string, error) {
if strings.TrimSpace(raw) == "" {
cells := make([][]string, rows)
for i := range cells {
cells[i] = make([]string, cols)
}
return cells, nil
}

var parsed [][]string
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
return nil, usagef("--values-json must be a JSON 2D string array: %v", err)
}
if len(parsed) != rows {
return nil, usagef("--values-json row count %d does not match --rows %d", len(parsed), rows)
}
for i, row := range parsed {
if len(row) != cols {
return nil, usagef("--values-json row %d has %d columns, want %d", i, len(row), cols)
}
}
return parsed, nil
}
Loading
Loading