bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
173 lines
5.7 KiB
Go
173 lines
5.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package sheets
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// ─── lark_sheet_search_replace ────────────────────────────────────────
|
|
//
|
|
// Wraps search_data (read) and replace_data (write). Both tools take an
|
|
// `options` sub-object; the CLI flattens its common booleans
|
|
// (--match-case / --match-entire-cell / --regex / --include-formulas) into
|
|
// independent flags per the铁律.
|
|
|
|
// CellsSearch wraps search_data: find cell coordinates matching --find,
|
|
// with optional case / regex / whole-cell / formula-text controls.
|
|
var CellsSearch = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+cells-search",
|
|
Description: "Find cells matching --find in a spreadsheet (case / regex / whole-cell / formula-text controls).",
|
|
Risk: "read",
|
|
Scopes: []string{"sheets:spreadsheet:read"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
Flags: flagsFor("+cells-search"),
|
|
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
if _, err := resolveSpreadsheetToken(runtime); err != nil {
|
|
return err
|
|
}
|
|
if _, _, err := resolveSheetSelector(runtime); err != nil {
|
|
return err
|
|
}
|
|
if strings.TrimSpace(runtime.Str("find")) == "" {
|
|
return sheetsValidationForFlag("find", "--find is required")
|
|
}
|
|
return nil
|
|
},
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := resolveSpreadsheetToken(runtime)
|
|
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
|
return invokeToolDryRun(token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, err := resolveSpreadsheetTokenExec(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := callTool(ctx, runtime, token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
func searchInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} {
|
|
input := map[string]interface{}{
|
|
"excel_id": token,
|
|
"search_term": runtime.Str("find"),
|
|
}
|
|
sheetSelectorForToolInput(input, sheetID, sheetName)
|
|
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
|
|
input["range"] = r
|
|
}
|
|
if runtime.Changed("offset") && runtime.Int("offset") > 0 {
|
|
input["offset"] = runtime.Int("offset")
|
|
}
|
|
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
|
|
input["options"] = opts
|
|
}
|
|
if n := runtime.Int("max-matches"); n > 0 {
|
|
input["max_matches"] = n
|
|
}
|
|
return input
|
|
}
|
|
|
|
// searchReplaceOptions packs the four shared boolean flags into the tool's
|
|
// `options` sub-object. Empty result → caller should omit the field.
|
|
func searchReplaceOptions(runtime flagView) map[string]interface{} {
|
|
opts := map[string]interface{}{}
|
|
if runtime.Bool("match-case") {
|
|
opts["match_case"] = true
|
|
}
|
|
if runtime.Bool("match-entire-cell") {
|
|
opts["match_entire_cell"] = true
|
|
}
|
|
if runtime.Bool("regex") {
|
|
opts["use_regex"] = true
|
|
}
|
|
if runtime.Bool("include-formulas") {
|
|
opts["match_formulas"] = true
|
|
}
|
|
return opts
|
|
}
|
|
|
|
// CellsReplace wraps replace_data: find and replace text across a
|
|
// spreadsheet, with the same option controls as +cells-search.
|
|
var CellsReplace = common.Shortcut{
|
|
Service: "sheets",
|
|
Command: "+cells-replace",
|
|
Description: "Find and replace text in a spreadsheet (case / regex / whole-cell / formula-text controls).",
|
|
Risk: "write",
|
|
Scopes: []string{"sheets:spreadsheet:write_only"},
|
|
AuthTypes: []string{"user", "bot"},
|
|
HasFormat: true,
|
|
Flags: flagsFor("+cells-replace"),
|
|
Validate: validateViaInput(replaceInput),
|
|
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
|
token, _ := resolveSpreadsheetToken(runtime)
|
|
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
|
input, _ := replaceInput(runtime, token, sheetID, sheetName)
|
|
return invokeToolDryRun(token, ToolKindWrite, "replace_data", input)
|
|
},
|
|
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
|
token, err := resolveSpreadsheetTokenExec(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sheetID, sheetName, err := resolveSheetSelector(runtime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input, err := replaceInput(runtime, token, sheetID, sheetName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := callTool(ctx, runtime, token, ToolKindWrite, "replace_data", input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
runtime.Out(out, nil)
|
|
return nil
|
|
},
|
|
Tips: []string{
|
|
"Always preview with --dry-run before running — replace can mutate every matching cell across the sheet.",
|
|
},
|
|
}
|
|
|
|
func replaceInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
|
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(runtime.Str("find")) == "" {
|
|
return nil, sheetsValidationForFlag("find", "--find is required")
|
|
}
|
|
if !runtime.Changed("replacement") {
|
|
return nil, sheetsValidationForFlag("replacement", "--replacement is required (pass an empty string to delete matches)")
|
|
}
|
|
input := map[string]interface{}{
|
|
"excel_id": token,
|
|
"search_term": runtime.Str("find"),
|
|
"replace_term": runtime.Str("replacement"),
|
|
}
|
|
sheetSelectorForToolInput(input, sheetID, sheetName)
|
|
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
|
|
input["range"] = r
|
|
}
|
|
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
|
|
input["options"] = opts
|
|
}
|
|
return input, nil
|
|
}
|