Skip to content
Open
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
74 changes: 74 additions & 0 deletions shortcuts/sheets/data/flag-defs.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,78 @@
{
"+undo": {
"risk": "write",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet token (XOR with `--url`)"
},
{
"name": "steps",
"kind": "own",
"type": "int",
"required": "optional",
"desc": "Undo the most recent N edits made through this CLI link (default 1); XOR with `--op`",
"default": "1"
},
{
"name": "op",
"kind": "own",
"type": "string",
"required": "optional",
"desc": "Undo a specific operation by its operation_id (from a prior write's undo handle); XOR with `--steps`"
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
},
"+recover": {
"risk": "write",
"flags": [
{
"name": "url",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet URL (XOR with `--spreadsheet-token`)"
},
{
"name": "spreadsheet-token",
"kind": "public",
"type": "string",
"required": "xor",
"desc": "Spreadsheet token (XOR with `--url`)"
},
{
"name": "to-revision",
"kind": "own",
"type": "int",
"required": "required",
"desc": "Restore the whole spreadsheet to this revision (a revision number returned by a prior write)"
},
{
"name": "dry-run",
"kind": "system",
"type": "bool",
"required": "optional",
"desc": ""
}
]
},
"+workbook-info": {
"risk": "read",
"flags": [
Expand Down
19 changes: 19 additions & 0 deletions shortcuts/sheets/flag_defs_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion shortcuts/sheets/lark_sheet_object_crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ func newFloatImageWriteShortcut(command, description, op string, withIDFlag, isH
// With a local --image, Execute first uploads the file; surface that
// extra step in the preview (mirrors +cells-set-image's dry-run).
if img := strings.TrimSpace(runtime.Str("image")); img != "" {
manageBody, _ := buildToolBody("manage_float_image_object", input)
manageBody, _ := buildToolBody(ToolKindWrite, "manage_float_image_object", input)
return common.NewDryRunAPI().
POST("/open-apis/drive/v1/medias/upload_all").
Desc("upload local image to drive (parent_type=sheet_image)").
Expand Down
85 changes: 85 additions & 0 deletions shortcuts/sheets/lark_sheet_recover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package sheets

import (
"context"

"github.com/larksuite/cli/shortcuts/common"
)

// ─── lark_sheet_recover ───────────────────────────────────────────────
//
// Wraps:
// - recover_to_revision (write) — powers +recover
//
// Rolls the WHOLE spreadsheet back to a past revision (the undo design doc's
// "方案 B"). Unlike +undo — which is precise, per-edit, and scoped to this CLI
// link — +recover is a full-document version restore. The facade gateway
// already owns this capability (the same revert-by-revision path the web
// "history" panel drives): it submits a single RECOVER changeset that reverts
// every sheet to the target revision and produces a new revision. The CLI only
// passes the target revision; all the work stays server-side.
//
// ⚠️ Full-table overwrite: +recover discards EVERY change made after
// --to-revision, including other collaborators' (and the web UI's) edits. Use
// it only on agent scratch spreadsheets, or when a whole-document rollback is
// acceptable. For precise, this-link-only undo, use +undo instead.
var Recover = common.Shortcut{
Service: "sheets",
Command: "+recover",
Description: "Roll the whole spreadsheet back to a past revision (full-document restore; discards all later edits).",
Risk: "write",
Scopes: []string{"sheets:spreadsheet:write_only"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+recover"),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
_, err = recoverInput(runtime, token)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
input, _ := recoverInput(runtime, token)
return invokeToolDryRun(token, ToolKindWrite, "recover_to_revision", input)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
input, err := recoverInput(runtime, token)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindWrite, "recover_to_revision", input)
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
Tips: []string{
"+recover is a FULL-DOCUMENT rollback — it discards every edit made after --to-revision, including other collaborators'. For precise, this-link-only undo, use +undo instead.",
"--to-revision takes a revision number returned by a prior write (the `revision` field in the response).",
"Use --dry-run to preview the recover request before running it.",
},
}

// recoverInput builds the recover_to_revision tool body. Network-free; shared
// by Validate, DryRun, and Execute.
func recoverInput(runtime flagView, token string) (map[string]interface{}, error) {
rev := runtime.Int("to-revision")
if rev < 1 {
return nil, common.FlagErrorf("--to-revision must be a positive revision number")
}
return map[string]interface{}{
"excel_id": token,
"to_revision": rev,
}, nil
}
6 changes: 3 additions & 3 deletions shortcuts/sheets/lark_sheet_table_io.go
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,7 @@ func tablePutDryRun(runtime *common.RuntimeContext) *common.DryRunAPI {
if s.AllowOverwrite != nil && !*s.AllowOverwrite {
input["allow_overwrite"] = false
}
wireBody, _ := buildToolBody("set_cell_range", input)
wireBody, _ := buildToolBody(ToolKindWrite, "set_cell_range", input)
dry.POST(toolInvokePath(token, ToolKindWrite)).Desc(desc).Body(wireBody)
}
return dry
Expand Down Expand Up @@ -777,14 +777,14 @@ var TableGet = common.Shortcut{
token, _ := resolveSpreadsheetToken(runtime)
dry := common.NewDryRunAPI()
if strings.TrimSpace(runtime.Str("sheet-id")) == "" && strings.TrimSpace(runtime.Str("sheet-name")) == "" {
body, _ := buildToolBody("get_workbook_structure", map[string]interface{}{"excel_id": token})
body, _ := buildToolBody(ToolKindRead, "get_workbook_structure", map[string]interface{}{"excel_id": token})
dry.POST(toolInvokePath(token, ToolKindRead)).Desc("list sub-sheets via get_workbook_structure").Body(body)
}
rng := strings.TrimSpace(runtime.Str("range"))
if rng == "" {
rng = "<each sheet's current region>"
}
body, _ := buildToolBody("get_cell_ranges", map[string]interface{}{
body, _ := buildToolBody(ToolKindRead, "get_cell_ranges", map[string]interface{}{
"excel_id": token, "ranges": []string{rng},
"include_styles": true, "value_render_option": "raw_value",
})
Expand Down
108 changes: 108 additions & 0 deletions shortcuts/sheets/lark_sheet_undo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package sheets

import (
"context"
"strings"

"github.com/larksuite/cli/internal/validate"
"github.com/larksuite/cli/shortcuts/common"
)

// ─── lark_sheet_undo ──────────────────────────────────────────────────
//
// Wraps:
// - undo_last (write) — powers +undo
//
// Reverses the most recent edits this CLI link made to a spreadsheet. The
// backend already records an inverse changeset for every write (see the
// undo design doc, "方案 A"); +undo asks the backend executor to read that
// changeset back and re-apply it in reverse order on the node Workbook, then
// push the result upstream as a collaboration change. The CLI only triggers
// the tool — the read-back endpoint is space-internal and not reachable
// through the /open-apis gateway, so all the heavy lifting stays server-side.
//
// +undo carries no sheet selector: undo is scoped to the spreadsheet + this
// link's edit history, not a single sub-sheet. The two selection modes are
// XOR:
// - --steps N : undo the last N edits (default 1)
// - --op <id> : undo one specific operation_id surfaced by a prior write's
// undo handle

// Undo wraps undo_last: reverse the most recent edits made through this CLI
// link, either the last N steps (--steps) or one specific operation (--op).
var Undo = common.Shortcut{
Service: "sheets",
Command: "+undo",
Description: "Undo the most recent edits this CLI link made to a spreadsheet (last N steps, or a specific operation).",
Risk: "write",
Scopes: []string{"sheets:spreadsheet:write_only"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+undo"),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
_, err = undoInput(runtime, token)
return err
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
input, _ := undoInput(runtime, token)
return invokeToolDryRun(token, ToolKindWrite, "undo_last", input)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetToken(runtime)
if err != nil {
return err
}
input, err := undoInput(runtime, token)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindWrite, "undo_last", input)
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
Tips: []string{
"Undo is scoped to edits made through this CLI link — it never touches changes other collaborators (or the web UI) made to the same range.",
"Use --dry-run to preview which steps would be undone before running it.",
},
}

// undoInput builds the undo_last tool body and enforces the --steps / --op
// XOR. --steps carries a default of 1, so the mutual-exclusion check keys off
// Changed("steps") (whether the user actually passed it) rather than its
// value. Network-free; shared by Validate, DryRun, and Execute.
func undoInput(runtime flagView, token string) (map[string]interface{}, error) {
op := strings.TrimSpace(runtime.Str("op"))
stepsSet := runtime.Changed("steps")

if op != "" && stepsSet {
return nil, common.FlagErrorf("--steps and --op are mutually exclusive")
}

input := map[string]interface{}{"excel_id": token}

if op != "" {
if err := validate.RejectControlChars(op, "op"); err != nil {
return nil, common.FlagErrorf("%v", err)
}
input["operation_id"] = op
return input, nil
}

steps := runtime.Int("steps")
if steps < 1 {
return nil, common.FlagErrorf("--steps must be >= 1")
}
input["steps"] = steps
return input, nil
}
Loading
Loading