Skip to content

Commit 5d3b1c0

Browse files
authored
feat(sheets): append table rows
Adds table-aware row appends for Google Sheets tables, including docs, generated command page, width validation, and live Google smoke verification.
1 parent 322695f commit 5d3b1c0

11 files changed

Lines changed: 347 additions & 7 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Calendar: add `calendar move` / `calendar transfer` to move an event to another calendar and change its organizer. (#448) — thanks @markusbkoch.
1515
- Docs: add `docs add-tab`, `docs rename-tab`, and `docs delete-tab` for managing Google Docs tabs. (#547) — thanks @chopenhauer.
1616
- Docs: support tab-scoped Markdown append and find-replace flows. (#541) — thanks @donbowman.
17+
- Sheets: add `sheets table append` for appending rows to structured Sheets tables without targeting headers directly.
1718

1819
### Fixed
1920
- Agent safety: compile baked safety profile policies into generated hash switches so raw allow/deny rule strings are not embedded as patchable YAML. (#540) — thanks @drewburchfield.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,7 @@ gog sheets named-ranges delete <spreadsheetId> MyNamedRange2
13911391
# Tables
13921392
gog sheets table list <spreadsheetId>
13931393
gog sheets table create <spreadsheetId> 'Sheet1!A1:C4' --name Tasks --columns-json '[{"columnName":"Task","columnType":"TEXT"},{"columnName":"Amount","columnType":"DOUBLE"},{"columnName":"Done","columnType":"BOOLEAN"}]'
1394+
gog sheets table append <spreadsheetId> <tableId> --values-json '[["Write docs",2,true]]'
13941395
gog sheets table get <spreadsheetId> <tableId>
13951396
gog sheets table delete <spreadsheetId> <tableId> --force
13961397
# See docs/sheets-tables.md for valid column types and current command scope.

docs/commands.generated.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ Generated from `gog schema --json`.
415415
- [`gog sheets (sheet) resize-columns <spreadsheetId> <columns> [flags]`](commands/gog-sheets-resize-columns.md) - Resize sheet columns
416416
- [`gog sheets (sheet) resize-rows <spreadsheetId> <rows> [flags]`](commands/gog-sheets-resize-rows.md) - Resize sheet rows
417417
- [`gog sheets (sheet) table (tables) <command>`](commands/gog-sheets-table.md) - Manage Google Sheets tables
418+
- [`gog sheets (sheet) table (tables) append (add-row,add-rows) <spreadsheetId> <tableId> [<values> ...] [flags]`](commands/gog-sheets-table-append.md) - Append rows to a table
418419
- [`gog sheets (sheet) table (tables) create (add,new) --name=STRING --columns-json=STRING <spreadsheetId> <range>`](commands/gog-sheets-table-create.md) - Create a table
419420
- [`gog sheets (sheet) table (tables) delete (rm,remove,del) <spreadsheetId> <tableId>`](commands/gog-sheets-table-delete.md) - Delete a table
420421
- [`gog sheets (sheet) table (tables) get (show,info) <spreadsheetId> <tableId>`](commands/gog-sheets-table-get.md) - Get a table

docs/commands/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
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.
44

5-
Generated pages: 456.
5+
Generated pages: 457.
66

77
## Top-level Commands
88

@@ -458,6 +458,7 @@ Generated pages: 456.
458458
- [gog sheets resize-columns](gog-sheets-resize-columns.md) - Resize sheet columns
459459
- [gog sheets resize-rows](gog-sheets-resize-rows.md) - Resize sheet rows
460460
- [gog sheets table](gog-sheets-table.md) - Manage Google Sheets tables
461+
- [gog sheets table append](gog-sheets-table-append.md) - Append rows to a table
461462
- [gog sheets table create](gog-sheets-table-create.md) - Create a table
462463
- [gog sheets table delete](gog-sheets-table-delete.md) - Delete a table
463464
- [gog sheets table get](gog-sheets-table-get.md) - Get a table
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# `gog sheets table append`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Append rows to a table
6+
7+
## Usage
8+
9+
```bash
10+
gog sheets (sheet) table (tables) append (add-row,add-rows) <spreadsheetId> <tableId> [<values> ...] [flags]
11+
```
12+
13+
## Parent
14+
15+
- [gog sheets table](gog-sheets-table.md)
16+
17+
## Flags
18+
19+
| Flag | Type | Default | Help |
20+
| --- | --- | --- | --- |
21+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
22+
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
23+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
24+
| `--color` | `string` | auto | Color output: auto\|always\|never |
25+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
26+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
27+
| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
28+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
29+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
30+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
31+
| `--input` | `string` | USER_ENTERED | Value input option: RAW or USER_ENTERED |
32+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
33+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
34+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
35+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
36+
| `--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. |
37+
| `--values-json` | `string` | | Values as JSON 2D array |
38+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
39+
| `--version` | `kong.VersionFlag` | | Print version and exit |
40+
41+
## See Also
42+
43+
- [gog sheets table](gog-sheets-table.md)
44+
- [Command index](README.md)

docs/commands/gog-sheets-table.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ gog sheets (sheet) table (tables) <command>
1616

1717
## Subcommands
1818

19+
- [gog sheets table append](gog-sheets-table-append.md) - Append rows to a table
1920
- [gog sheets table create](gog-sheets-table-create.md) - Create a table
2021
- [gog sheets table delete](gog-sheets-table-delete.md) - Delete a table
2122
- [gog sheets table get](gog-sheets-table-get.md) - Get a table

docs/sheets-tables.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,29 @@ gog sheets table get "$spreadsheet_id" Tasks --json
7878
JSON output includes the table ID, table name, sheet title, A1 range, raw
7979
`GridRange`, and typed columns.
8080

81+
## Append Rows
82+
83+
Append rows by table ID or name:
84+
85+
```bash
86+
gog sheets table append "$spreadsheet_id" "$table_id" \
87+
--values-json '[["Write docs",2,true]]'
88+
```
89+
90+
Positional values use the same comma-separated row, pipe-separated cell syntax
91+
as `gog sheets append`:
92+
93+
```bash
94+
gog sheets table append "$spreadsheet_id" Tasks 'Write docs|2|true'
95+
gog sheets table append "$spreadsheet_id" Tasks 'One|1|false,Two|2|true'
96+
```
97+
98+
`sheets table append` resolves the table first, then calls the Sheets append API
99+
against the table's bounded A1 range with `INSERT_ROWS`. This lets Sheets place
100+
new rows after the current table data and expand the table, without targeting
101+
the header row directly. Rows wider than the table's column count are rejected
102+
before the mutation is sent.
103+
81104
## Delete A Table
82105

83106
Deleting removes the table object. Use `--force` for non-interactive runs:
@@ -94,15 +117,16 @@ gog sheets table delete "$spreadsheet_id" "$table_id" --dry-run --json
94117

95118
## Current Scope
96119

97-
This first table command set intentionally covers list, get, create, and delete
98-
only. Row append, table update, footer handling, and table-aware clear behavior
99-
need separate semantics because the plain Sheets range APIs can touch table
100-
headers or footer rows if used blindly.
120+
This table command set intentionally covers list, get, create, append, and
121+
delete. Table update, footer editing, and table-aware clear behavior need
122+
separate semantics because the plain Sheets range APIs can touch table headers
123+
or footer rows if used blindly.
101124

102125
## Command Pages
103126

104127
- [`gog sheets table`](commands/gog-sheets-table.md)
105128
- [`gog sheets table list`](commands/gog-sheets-table-list.md)
106129
- [`gog sheets table get`](commands/gog-sheets-table-get.md)
107130
- [`gog sheets table create`](commands/gog-sheets-table-create.md)
131+
- [`gog sheets table append`](commands/gog-sheets-table-append.md)
108132
- [`gog sheets table delete`](commands/gog-sheets-table-delete.md)

internal/cmd/sheets.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818

1919
var newSheetsService = googleapi.NewSheets
2020

21+
const sheetsDefaultValueInputOption = "USER_ENTERED"
22+
2123
// cleanRange removes shell escape sequences from range arguments.
2224
// Some shells escape ! to \! (bash history expansion), which breaks Google Sheets API calls.
2325
func cleanRange(r string) string {
@@ -201,7 +203,7 @@ func (c *SheetsUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
201203

202204
valueInputOption := strings.TrimSpace(c.ValueInput)
203205
if valueInputOption == "" {
204-
valueInputOption = "USER_ENTERED"
206+
valueInputOption = sheetsDefaultValueInputOption
205207
}
206208

207209
if err := dryRunExit(ctx, flags, "sheets.update", map[string]any{
@@ -309,7 +311,7 @@ func (c *SheetsAppendCmd) Run(ctx context.Context, flags *RootFlags) error {
309311

310312
valueInputOption := strings.TrimSpace(c.ValueInput)
311313
if valueInputOption == "" {
312-
valueInputOption = "USER_ENTERED"
314+
valueInputOption = sheetsDefaultValueInputOption
313315
}
314316
insertDataOption := strings.TrimSpace(c.Insert)
315317

internal/cmd/sheets_table.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type SheetsTableCmd struct {
1717
List SheetsTableListCmd `cmd:"" default:"withargs" help:"List tables in a spreadsheet"`
1818
Get SheetsTableGetCmd `cmd:"" name:"get" aliases:"show,info" help:"Get a table"`
1919
Create SheetsTableCreateCmd `cmd:"" name:"create" aliases:"add,new" help:"Create a table"`
20+
Append SheetsTableAppendCmd `cmd:"" name:"append" aliases:"add-row,add-rows" help:"Append rows to a table"`
2021
Delete SheetsTableDeleteCmd `cmd:"" name:"delete" aliases:"rm,remove,del" help:"Delete a table"`
2122
}
2223

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"strings"
9+
10+
"google.golang.org/api/sheets/v4"
11+
12+
"github.com/steipete/gogcli/internal/outfmt"
13+
"github.com/steipete/gogcli/internal/ui"
14+
)
15+
16+
type SheetsTableAppendCmd struct {
17+
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
18+
TableID string `arg:"" name:"tableId" help:"Table ID or table name"`
19+
Values []string `arg:"" optional:"" name:"values" help:"Values (comma-separated rows, pipe-separated cells)"`
20+
ValueInput string `name:"input" help:"Value input option: RAW or USER_ENTERED" default:"USER_ENTERED"`
21+
ValuesJSON string `name:"values-json" help:"Values as JSON 2D array"`
22+
}
23+
24+
func (c *SheetsTableAppendCmd) Run(ctx context.Context, flags *RootFlags) error {
25+
u := ui.FromContext(ctx)
26+
spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
27+
in := strings.TrimSpace(c.TableID)
28+
if spreadsheetID == "" {
29+
return usage("empty spreadsheetId")
30+
}
31+
if in == "" {
32+
return usage("empty tableId")
33+
}
34+
35+
values, err := parseSheetsAppendValues(c.ValuesJSON, c.Values)
36+
if err != nil {
37+
return err
38+
}
39+
valueInputOption := strings.TrimSpace(c.ValueInput)
40+
if valueInputOption == "" {
41+
valueInputOption = sheetsDefaultValueInputOption
42+
}
43+
44+
account, err := requireAccount(flags)
45+
if err != nil {
46+
return err
47+
}
48+
49+
svc, err := newSheetsService(ctx, account)
50+
if err != nil {
51+
return err
52+
}
53+
54+
tables, err := fetchSpreadsheetTables(ctx, svc, spreadsheetID)
55+
if err != nil {
56+
return err
57+
}
58+
table, found, err := resolveSheetsTable(in, tables)
59+
if err != nil {
60+
return err
61+
}
62+
if !found {
63+
return usagef("unknown table %q", in)
64+
}
65+
if strings.TrimSpace(table.A1) == "" {
66+
return fmt.Errorf("table %q has no bounded A1 range", table.TableID)
67+
}
68+
if widthErr := validateSheetsTableAppendWidth(table, values); widthErr != nil {
69+
return widthErr
70+
}
71+
72+
if dryRunErr := dryRunExit(ctx, flags, "sheets.table.append", map[string]any{
73+
"spreadsheet_id": spreadsheetID,
74+
"table_id": table.TableID,
75+
"name": table.Name,
76+
"range": table.A1,
77+
"values": values,
78+
"value_input_option": valueInputOption,
79+
"insert_data_option": "INSERT_ROWS",
80+
}); dryRunErr != nil {
81+
return dryRunErr
82+
}
83+
84+
resp, err := svc.Spreadsheets.Values.Append(spreadsheetID, table.A1, &sheets.ValueRange{Values: values}).
85+
ValueInputOption(valueInputOption).
86+
InsertDataOption("INSERT_ROWS").
87+
Do()
88+
if err != nil {
89+
return err
90+
}
91+
if resp == nil || resp.Updates == nil {
92+
return fmt.Errorf("append response missing update metadata")
93+
}
94+
95+
if outfmt.IsJSON(ctx) {
96+
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
97+
"tableId": table.TableID,
98+
"name": table.Name,
99+
"tableRange": table.A1,
100+
"updatedRange": resp.Updates.UpdatedRange,
101+
"updatedRows": resp.Updates.UpdatedRows,
102+
"updatedColumns": resp.Updates.UpdatedColumns,
103+
"updatedCells": resp.Updates.UpdatedCells,
104+
})
105+
}
106+
107+
u.Out().Printf("Appended %d cells to %s", resp.Updates.UpdatedCells, resp.Updates.UpdatedRange)
108+
return nil
109+
}
110+
111+
func parseSheetsAppendValues(valuesJSON string, values []string) ([][]interface{}, error) {
112+
switch {
113+
case strings.TrimSpace(valuesJSON) != "":
114+
b, err := resolveInlineOrFileBytes(valuesJSON)
115+
if err != nil {
116+
return nil, fmt.Errorf("read --values-json: %w", err)
117+
}
118+
var parsed [][]interface{}
119+
if err := json.Unmarshal(b, &parsed); err != nil {
120+
return nil, fmt.Errorf("invalid JSON values: %w", err)
121+
}
122+
if len(parsed) == 0 {
123+
return nil, fmt.Errorf("provide at least one row")
124+
}
125+
return parsed, nil
126+
case len(values) > 0:
127+
rawValues := strings.Join(values, " ")
128+
rows := strings.Split(rawValues, ",")
129+
parsed := make([][]interface{}, 0, len(rows))
130+
for _, row := range rows {
131+
cells := strings.Split(strings.TrimSpace(row), "|")
132+
rowData := make([]interface{}, len(cells))
133+
for i, cell := range cells {
134+
rowData[i] = strings.TrimSpace(cell)
135+
}
136+
parsed = append(parsed, rowData)
137+
}
138+
return parsed, nil
139+
default:
140+
return nil, fmt.Errorf("provide values as args or via --values-json")
141+
}
142+
}
143+
144+
func validateSheetsTableAppendWidth(table sheetsTableItem, values [][]interface{}) error {
145+
if len(table.Columns) == 0 {
146+
return nil
147+
}
148+
width := len(table.Columns)
149+
for i, row := range values {
150+
if len(row) > width {
151+
return usagef("row %d has %d cells, but table %q has %d columns", i+1, len(row), table.Name, width)
152+
}
153+
}
154+
return nil
155+
}

0 commit comments

Comments
 (0)