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 @@ -14,6 +14,7 @@
- Calendar: add `calendar move` / `calendar transfer` to move an event to another calendar and change its organizer. (#448) — thanks @markusbkoch.
- Docs: add `docs add-tab`, `docs rename-tab`, and `docs delete-tab` for managing Google Docs tabs. (#547) — thanks @chopenhauer.
- Docs: support tab-scoped Markdown append and find-replace flows. (#541) — thanks @donbowman.
- Sheets: add `sheets table append` for appending rows to structured Sheets tables without targeting headers directly.

### Fixed
- 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.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@ gog sheets named-ranges delete <spreadsheetId> MyNamedRange2
# Tables
gog sheets table list <spreadsheetId>
gog sheets table create <spreadsheetId> 'Sheet1!A1:C4' --name Tasks --columns-json '[{"columnName":"Task","columnType":"TEXT"},{"columnName":"Amount","columnType":"DOUBLE"},{"columnName":"Done","columnType":"BOOLEAN"}]'
gog sheets table append <spreadsheetId> <tableId> --values-json '[["Write docs",2,true]]'
gog sheets table get <spreadsheetId> <tableId>
gog sheets table delete <spreadsheetId> <tableId> --force
# See docs/sheets-tables.md for valid column types and current command scope.
Expand Down
1 change: 1 addition & 0 deletions docs/commands.generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ Generated from `gog schema --json`.
- [`gog sheets (sheet) resize-columns <spreadsheetId> <columns> [flags]`](commands/gog-sheets-resize-columns.md) - Resize sheet columns
- [`gog sheets (sheet) resize-rows <spreadsheetId> <rows> [flags]`](commands/gog-sheets-resize-rows.md) - Resize sheet rows
- [`gog sheets (sheet) table (tables) <command>`](commands/gog-sheets-table.md) - Manage Google Sheets tables
- [`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
- [`gog sheets (sheet) table (tables) create (add,new) --name=STRING --columns-json=STRING <spreadsheetId> <range>`](commands/gog-sheets-table-create.md) - Create a table
- [`gog sheets (sheet) table (tables) delete (rm,remove,del) <spreadsheetId> <tableId>`](commands/gog-sheets-table-delete.md) - Delete a table
- [`gog sheets (sheet) table (tables) get (show,info) <spreadsheetId> <tableId>`](commands/gog-sheets-table-get.md) - Get a table
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: 456.
Generated pages: 457.

## Top-level Commands

Expand Down Expand Up @@ -458,6 +458,7 @@ Generated pages: 456.
- [gog sheets resize-columns](gog-sheets-resize-columns.md) - Resize sheet columns
- [gog sheets resize-rows](gog-sheets-resize-rows.md) - Resize sheet rows
- [gog sheets table](gog-sheets-table.md) - Manage Google Sheets tables
- [gog sheets table append](gog-sheets-table-append.md) - Append rows to a table
- [gog sheets table create](gog-sheets-table-create.md) - Create a table
- [gog sheets table delete](gog-sheets-table-delete.md) - Delete a table
- [gog sheets table get](gog-sheets-table-get.md) - Get a table
Expand Down
44 changes: 44 additions & 0 deletions docs/commands/gog-sheets-table-append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# `gog sheets table append`

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

Append rows to a table

## Usage

```bash
gog sheets (sheet) table (tables) append (add-row,add-rows) <spreadsheetId> <tableId> [<values> ...] [flags]
```

## Parent

- [gog sheets table](gog-sheets-table.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/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
| `--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 |
| `--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. |
| `--input` | `string` | USER_ENTERED | Value input option: RAW or USER_ENTERED |
| `-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) |
| `--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. |
| `--values-json` | `string` | | Values as JSON 2D array |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |

## See Also

- [gog sheets table](gog-sheets-table.md)
- [Command index](README.md)
1 change: 1 addition & 0 deletions docs/commands/gog-sheets-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ gog sheets (sheet) table (tables) <command>

## Subcommands

- [gog sheets table append](gog-sheets-table-append.md) - Append rows to a table
- [gog sheets table create](gog-sheets-table-create.md) - Create a table
- [gog sheets table delete](gog-sheets-table-delete.md) - Delete a table
- [gog sheets table get](gog-sheets-table-get.md) - Get a table
Expand Down
32 changes: 28 additions & 4 deletions docs/sheets-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,29 @@ gog sheets table get "$spreadsheet_id" Tasks --json
JSON output includes the table ID, table name, sheet title, A1 range, raw
`GridRange`, and typed columns.

## Append Rows

Append rows by table ID or name:

```bash
gog sheets table append "$spreadsheet_id" "$table_id" \
--values-json '[["Write docs",2,true]]'
```

Positional values use the same comma-separated row, pipe-separated cell syntax
as `gog sheets append`:

```bash
gog sheets table append "$spreadsheet_id" Tasks 'Write docs|2|true'
gog sheets table append "$spreadsheet_id" Tasks 'One|1|false,Two|2|true'
```

`sheets table append` resolves the table first, then calls the Sheets append API
against the table's bounded A1 range with `INSERT_ROWS`. This lets Sheets place
new rows after the current table data and expand the table, without targeting
the header row directly. Rows wider than the table's column count are rejected
before the mutation is sent.

## Delete A Table

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

## Current Scope

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

## Command Pages

- [`gog sheets table`](commands/gog-sheets-table.md)
- [`gog sheets table list`](commands/gog-sheets-table-list.md)
- [`gog sheets table get`](commands/gog-sheets-table-get.md)
- [`gog sheets table create`](commands/gog-sheets-table-create.md)
- [`gog sheets table append`](commands/gog-sheets-table-append.md)
- [`gog sheets table delete`](commands/gog-sheets-table-delete.md)
6 changes: 4 additions & 2 deletions internal/cmd/sheets.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

var newSheetsService = googleapi.NewSheets

const sheetsDefaultValueInputOption = "USER_ENTERED"

// cleanRange removes shell escape sequences from range arguments.
// Some shells escape ! to \! (bash history expansion), which breaks Google Sheets API calls.
func cleanRange(r string) string {
Expand Down Expand Up @@ -201,7 +203,7 @@ func (c *SheetsUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {

valueInputOption := strings.TrimSpace(c.ValueInput)
if valueInputOption == "" {
valueInputOption = "USER_ENTERED"
valueInputOption = sheetsDefaultValueInputOption
}

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

valueInputOption := strings.TrimSpace(c.ValueInput)
if valueInputOption == "" {
valueInputOption = "USER_ENTERED"
valueInputOption = sheetsDefaultValueInputOption
}
insertDataOption := strings.TrimSpace(c.Insert)

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/sheets_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type SheetsTableCmd struct {
List SheetsTableListCmd `cmd:"" default:"withargs" help:"List tables in a spreadsheet"`
Get SheetsTableGetCmd `cmd:"" name:"get" aliases:"show,info" help:"Get a table"`
Create SheetsTableCreateCmd `cmd:"" name:"create" aliases:"add,new" help:"Create a table"`
Append SheetsTableAppendCmd `cmd:"" name:"append" aliases:"add-row,add-rows" help:"Append rows to a table"`
Delete SheetsTableDeleteCmd `cmd:"" name:"delete" aliases:"rm,remove,del" help:"Delete a table"`
}

Expand Down
155 changes: 155 additions & 0 deletions internal/cmd/sheets_table_append.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package cmd

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

"google.golang.org/api/sheets/v4"

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

type SheetsTableAppendCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
TableID string `arg:"" name:"tableId" help:"Table ID or table name"`
Values []string `arg:"" optional:"" name:"values" help:"Values (comma-separated rows, pipe-separated cells)"`
ValueInput string `name:"input" help:"Value input option: RAW or USER_ENTERED" default:"USER_ENTERED"`
ValuesJSON string `name:"values-json" help:"Values as JSON 2D array"`
}

func (c *SheetsTableAppendCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
in := strings.TrimSpace(c.TableID)
if spreadsheetID == "" {
return usage("empty spreadsheetId")
}
if in == "" {
return usage("empty tableId")
}

values, err := parseSheetsAppendValues(c.ValuesJSON, c.Values)
if err != nil {
return err
}
valueInputOption := strings.TrimSpace(c.ValueInput)
if valueInputOption == "" {
valueInputOption = sheetsDefaultValueInputOption
}

account, err := requireAccount(flags)
if err != nil {
return err
}

svc, err := newSheetsService(ctx, account)
if err != nil {
return err
}

tables, err := fetchSpreadsheetTables(ctx, svc, spreadsheetID)
if err != nil {
return err
}
table, found, err := resolveSheetsTable(in, tables)
if err != nil {
return err
}
if !found {
return usagef("unknown table %q", in)
}
if strings.TrimSpace(table.A1) == "" {
return fmt.Errorf("table %q has no bounded A1 range", table.TableID)
}
if widthErr := validateSheetsTableAppendWidth(table, values); widthErr != nil {
return widthErr
}

if dryRunErr := dryRunExit(ctx, flags, "sheets.table.append", map[string]any{
"spreadsheet_id": spreadsheetID,
"table_id": table.TableID,
"name": table.Name,
"range": table.A1,
"values": values,
"value_input_option": valueInputOption,
"insert_data_option": "INSERT_ROWS",
}); dryRunErr != nil {
return dryRunErr
}

resp, err := svc.Spreadsheets.Values.Append(spreadsheetID, table.A1, &sheets.ValueRange{Values: values}).
ValueInputOption(valueInputOption).
InsertDataOption("INSERT_ROWS").
Do()
if err != nil {
return err
}
if resp == nil || resp.Updates == nil {
return fmt.Errorf("append response missing update metadata")
}

if outfmt.IsJSON(ctx) {
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
"tableId": table.TableID,
"name": table.Name,
"tableRange": table.A1,
"updatedRange": resp.Updates.UpdatedRange,
"updatedRows": resp.Updates.UpdatedRows,
"updatedColumns": resp.Updates.UpdatedColumns,
"updatedCells": resp.Updates.UpdatedCells,
})
}

u.Out().Printf("Appended %d cells to %s", resp.Updates.UpdatedCells, resp.Updates.UpdatedRange)
return nil
}

func parseSheetsAppendValues(valuesJSON string, values []string) ([][]interface{}, error) {
switch {
case strings.TrimSpace(valuesJSON) != "":
b, err := resolveInlineOrFileBytes(valuesJSON)
if err != nil {
return nil, fmt.Errorf("read --values-json: %w", err)
}
var parsed [][]interface{}
if err := json.Unmarshal(b, &parsed); err != nil {
return nil, fmt.Errorf("invalid JSON values: %w", err)
}
if len(parsed) == 0 {
return nil, fmt.Errorf("provide at least one row")
}
return parsed, nil
case len(values) > 0:
rawValues := strings.Join(values, " ")
rows := strings.Split(rawValues, ",")
parsed := make([][]interface{}, 0, len(rows))
for _, row := range rows {
cells := strings.Split(strings.TrimSpace(row), "|")
rowData := make([]interface{}, len(cells))
for i, cell := range cells {
rowData[i] = strings.TrimSpace(cell)
}
parsed = append(parsed, rowData)
}
return parsed, nil
default:
return nil, fmt.Errorf("provide values as args or via --values-json")
}
}

func validateSheetsTableAppendWidth(table sheetsTableItem, values [][]interface{}) error {
if len(table.Columns) == 0 {
return nil
}
width := len(table.Columns)
for i, row := range values {
if len(row) > width {
return usagef("row %d has %d cells, but table %q has %d columns", i+1, len(row), table.Name, width)
}
}
return nil
}
Loading
Loading