Skip to content

Commit 7e7f716

Browse files
authored
feat(base): add base block shortcuts (#1044)
* feat(base): add base block shortcuts * fix(base): use block scopes for base block shortcuts * fix(base): split base block shortcut scopes * docs(base): consolidate base block help * docs(base): simplify block help wording * test(base): cover base block shortcut execution * feat(base): filter base block list by type * docs(base): clarify base block ids * docs(base): simplify docx block help * docs(base): refine base block agent help
1 parent 1670a79 commit 7e7f716

13 files changed

Lines changed: 775 additions & 4 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
9+
"github.com/larksuite/cli/shortcuts/common"
10+
)
11+
12+
var BaseBaseBlockCreate = common.Shortcut{
13+
Service: "base",
14+
Command: "+base-block-create",
15+
Description: "Create a block",
16+
Risk: "write",
17+
Scopes: []string{"base:block:create"},
18+
AuthTypes: authTypes(),
19+
Flags: []common.Flag{
20+
baseTokenFlag(true),
21+
{Name: "type", Desc: "resource type", Required: true, Enum: baseBlockTypeEnums},
22+
{Name: "name", Desc: "block name", Required: true},
23+
{Name: "parent-id", Desc: "folder block id; when omitted, create at root"},
24+
},
25+
Tips: []string{
26+
"Example: lark-cli base +base-block-create --base-token <base_token> --type folder --name \"Project Docs\"",
27+
"Example: lark-cli base +base-block-create --base-token <base_token> --type table --name \"Tasks\"",
28+
"Example: lark-cli base +base-block-create --base-token <base_token> --type docx --name \"Spec\" --parent-id <folder_block_id>",
29+
"Example: lark-cli base +base-block-create --base-token <base_token> --type dashboard --name \"Metrics\"",
30+
"Example: lark-cli base +base-block-create --base-token <base_token> --type workflow --name \"Approval Flow\"",
31+
"Creates a folder, table, docx, dashboard, or workflow entry.",
32+
"Do not pass null for --parent-id. Omit it to create at the root level.",
33+
"Created resources still use their own commands for content operations, such as table/field/record/docx/dashboard/workflow commands.",
34+
},
35+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
36+
return validateBaseBlockCreate(runtime)
37+
},
38+
DryRun: dryRunBaseBlockCreate,
39+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
40+
return executeBaseBlockCreate(runtime)
41+
},
42+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
9+
"github.com/larksuite/cli/shortcuts/common"
10+
)
11+
12+
var BaseBaseBlockDelete = common.Shortcut{
13+
Service: "base",
14+
Command: "+base-block-delete",
15+
Description: "Delete a block",
16+
Risk: "high-risk-write",
17+
Scopes: []string{"base:block:delete"},
18+
AuthTypes: authTypes(),
19+
Flags: []common.Flag{
20+
baseTokenFlag(true),
21+
baseBlockIDFlag(true),
22+
},
23+
Tips: []string{
24+
"Example: lark-cli base +base-block-delete --base-token <base_token> --block-id <block_id> --yes",
25+
"Deletes the block identified by --block-id.",
26+
"Recursive folder deletion is not supported. If a folder is not empty, move or delete its children first.",
27+
"Different block types may have independent backing resources; deletion follows backend semantics.",
28+
"Use +base-block-list first when you need to confirm the target block id.",
29+
"If the user already explicitly confirmed this exact delete target, pass --yes without asking again.",
30+
},
31+
DryRun: dryRunBaseBlockDelete,
32+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
33+
return executeBaseBlockDelete(runtime)
34+
},
35+
}

shortcuts/base/base_block_list.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
9+
"github.com/larksuite/cli/shortcuts/common"
10+
)
11+
12+
var BaseBaseBlockList = common.Shortcut{
13+
Service: "base",
14+
Command: "+base-block-list",
15+
Description: "List blocks in a base",
16+
Risk: "read",
17+
Scopes: []string{"base:block:read"},
18+
AuthTypes: authTypes(),
19+
Flags: []common.Flag{
20+
baseTokenFlag(true),
21+
{Name: "type", Desc: "filter by resource type", Enum: baseBlockTypeEnums},
22+
{Name: "parent-id", Desc: "folder block id; when omitted, list all blocks"},
23+
},
24+
Tips: []string{
25+
"Example: lark-cli base +base-block-list --base-token <base_token>",
26+
"Example: lark-cli base +base-block-list --base-token <base_token> --type table",
27+
"Example: lark-cli base +base-block-list --base-token <base_token> --parent-id <folder_block_id>",
28+
`JQ crop: lark-cli base +base-block-list --base-token <base_token> | jq '.blocks[] | {type, name, block_id: .id, parent_id}'`,
29+
`JQ crop docx: lark-cli base +base-block-list --base-token <base_token> --type docx | jq '.blocks[] | {name, docx_token}'`,
30+
"Blocks are resources managed directly by the base, such as folder, table, docx, dashboard, and workflow.",
31+
"For table, dashboard, and workflow blocks, returned id is the table-id, dashboard-id, or workflow-id used by the corresponding commands.",
32+
"For docx blocks, use the returned docx_token with docx commands.",
33+
"For folder blocks, pass the returned id as --parent-id when creating, listing, or moving blocks inside that folder.",
34+
"This command returns the full backend list. It intentionally does not expose limit or offset.",
35+
"Pass --type to list only one resource type.",
36+
"Pass --parent-id to list only direct children of a folder.",
37+
"Dashboard blocks are chart/widget blocks inside a dashboard; use +dashboard-block-* for those.",
38+
},
39+
DryRun: dryRunBaseBlockList,
40+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
41+
return executeBaseBlockList(runtime)
42+
},
43+
}

shortcuts/base/base_block_move.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
9+
"github.com/larksuite/cli/shortcuts/common"
10+
)
11+
12+
var BaseBaseBlockMove = common.Shortcut{
13+
Service: "base",
14+
Command: "+base-block-move",
15+
Description: "Move a block",
16+
Risk: "write",
17+
Scopes: []string{"base:block:update"},
18+
AuthTypes: authTypes(),
19+
Flags: []common.Flag{
20+
baseTokenFlag(true),
21+
baseBlockIDFlag(true),
22+
{Name: "parent-id", Desc: "target folder block id; when omitted, move to root"},
23+
{Name: "before-id", Desc: "sibling block id; move the block before this sibling in the target folder/root order"},
24+
{Name: "after-id", Desc: "sibling block id; move the block after this sibling in the target folder/root order"},
25+
},
26+
Tips: []string{
27+
"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --parent-id <folder_block_id>",
28+
"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --after-id <sibling_block_id>",
29+
"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --before-id <sibling_block_id>",
30+
"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id>",
31+
"Omit --parent-id to move the block to root; do not pass null.",
32+
"--before-id and --after-id are mutually exclusive.",
33+
"When moving a folder, its children remain under that folder.",
34+
},
35+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
36+
return validateBaseBlockMove(runtime)
37+
},
38+
DryRun: dryRunBaseBlockMove,
39+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
40+
return executeBaseBlockMove(runtime)
41+
},
42+
}

shortcuts/base/base_block_ops.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
"strings"
9+
10+
"github.com/larksuite/cli/shortcuts/common"
11+
)
12+
13+
var baseBlockTypeEnums = []string{"folder", "table", "docx", "dashboard", "workflow"}
14+
15+
func baseBlockIDFlag(required bool) common.Flag {
16+
return common.Flag{Name: "block-id", Desc: "block id", Required: required}
17+
}
18+
19+
func dryRunBaseBlockList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
20+
return common.NewDryRunAPI().
21+
POST("/open-apis/base/v3/bases/:base_token/blocks/list").
22+
Body(buildBaseBlockListBody(runtime)).
23+
Set("base_token", runtime.Str("base-token"))
24+
}
25+
26+
func dryRunBaseBlockCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
27+
return common.NewDryRunAPI().
28+
POST("/open-apis/base/v3/bases/:base_token/blocks").
29+
Body(buildBaseBlockCreateBody(runtime)).
30+
Set("base_token", runtime.Str("base-token"))
31+
}
32+
33+
func dryRunBaseBlockMove(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
34+
return common.NewDryRunAPI().
35+
POST("/open-apis/base/v3/bases/:base_token/blocks/:block_id/move").
36+
Body(buildBaseBlockMoveBody(runtime)).
37+
Set("base_token", runtime.Str("base-token")).
38+
Set("block_id", runtime.Str("block-id"))
39+
}
40+
41+
func dryRunBaseBlockRename(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
42+
return common.NewDryRunAPI().
43+
POST("/open-apis/base/v3/bases/:base_token/blocks/:block_id/rename").
44+
Body(map[string]interface{}{"name": strings.TrimSpace(runtime.Str("name"))}).
45+
Set("base_token", runtime.Str("base-token")).
46+
Set("block_id", runtime.Str("block-id"))
47+
}
48+
49+
func dryRunBaseBlockDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
50+
return common.NewDryRunAPI().
51+
DELETE("/open-apis/base/v3/bases/:base_token/blocks/:block_id").
52+
Set("base_token", runtime.Str("base-token")).
53+
Set("block_id", runtime.Str("block-id"))
54+
}
55+
56+
func validateBaseBlockCreate(runtime *common.RuntimeContext) error {
57+
if strings.TrimSpace(runtime.Str("name")) == "" {
58+
return common.FlagErrorf("--name must not be blank")
59+
}
60+
if strings.TrimSpace(runtime.Str("type")) == "" {
61+
return common.FlagErrorf("--type must not be blank")
62+
}
63+
return nil
64+
}
65+
66+
func validateBaseBlockMove(runtime *common.RuntimeContext) error {
67+
if strings.TrimSpace(runtime.Str("before-id")) != "" && strings.TrimSpace(runtime.Str("after-id")) != "" {
68+
return common.FlagErrorf("--before-id and --after-id are mutually exclusive")
69+
}
70+
return nil
71+
}
72+
73+
func validateBaseBlockRename(runtime *common.RuntimeContext) error {
74+
if strings.TrimSpace(runtime.Str("name")) == "" {
75+
return common.FlagErrorf("--name must not be blank")
76+
}
77+
return nil
78+
}
79+
80+
func executeBaseBlockList(runtime *common.RuntimeContext) error {
81+
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "blocks", "list"), nil, buildBaseBlockListBody(runtime))
82+
if err != nil {
83+
return err
84+
}
85+
filterBaseBlockListData(data, strings.TrimSpace(runtime.Str("type")))
86+
runtime.Out(data, nil)
87+
return nil
88+
}
89+
90+
func executeBaseBlockCreate(runtime *common.RuntimeContext) error {
91+
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "blocks"), nil, buildBaseBlockCreateBody(runtime))
92+
if err != nil {
93+
return err
94+
}
95+
runtime.Out(map[string]interface{}{"block": data, "created": true}, nil)
96+
return nil
97+
}
98+
99+
func executeBaseBlockMove(runtime *common.RuntimeContext) error {
100+
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "blocks", runtime.Str("block-id"), "move"), nil, buildBaseBlockMoveBody(runtime))
101+
if err != nil {
102+
return err
103+
}
104+
runtime.Out(map[string]interface{}{"block": data, "moved": true}, nil)
105+
return nil
106+
}
107+
108+
func executeBaseBlockRename(runtime *common.RuntimeContext) error {
109+
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "blocks", runtime.Str("block-id"), "rename"), nil, map[string]interface{}{
110+
"name": strings.TrimSpace(runtime.Str("name")),
111+
})
112+
if err != nil {
113+
return err
114+
}
115+
runtime.Out(map[string]interface{}{"block": data, "renamed": true}, nil)
116+
return nil
117+
}
118+
119+
func executeBaseBlockDelete(runtime *common.RuntimeContext) error {
120+
data, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "blocks", runtime.Str("block-id")), nil, nil)
121+
if err != nil {
122+
return err
123+
}
124+
runtime.Out(map[string]interface{}{"block": data, "deleted": true}, nil)
125+
return nil
126+
}
127+
128+
func buildBaseBlockListBody(runtime *common.RuntimeContext) map[string]interface{} {
129+
body := map[string]interface{}{}
130+
if parentID := strings.TrimSpace(runtime.Str("parent-id")); parentID != "" {
131+
body["parent_id"] = parentID
132+
}
133+
return body
134+
}
135+
136+
func filterBaseBlockListData(data map[string]interface{}, blockType string) {
137+
if blockType == "" {
138+
return
139+
}
140+
blocks, ok := data["blocks"].([]interface{})
141+
if !ok {
142+
return
143+
}
144+
filtered := make([]interface{}, 0, len(blocks))
145+
for _, block := range blocks {
146+
blockMap, ok := block.(map[string]interface{})
147+
if !ok || blockMap["type"] != blockType {
148+
continue
149+
}
150+
filtered = append(filtered, block)
151+
}
152+
data["blocks"] = filtered
153+
data["total"] = len(filtered)
154+
}
155+
156+
func buildBaseBlockCreateBody(runtime *common.RuntimeContext) map[string]interface{} {
157+
body := map[string]interface{}{
158+
"type": strings.TrimSpace(runtime.Str("type")),
159+
"name": strings.TrimSpace(runtime.Str("name")),
160+
}
161+
if parentID := strings.TrimSpace(runtime.Str("parent-id")); parentID != "" {
162+
body["parent_id"] = parentID
163+
}
164+
return body
165+
}
166+
167+
func buildBaseBlockMoveBody(runtime *common.RuntimeContext) map[string]interface{} {
168+
body := map[string]interface{}{"parent_id": nil}
169+
if parentID := strings.TrimSpace(runtime.Str("parent-id")); parentID != "" {
170+
body["parent_id"] = parentID
171+
}
172+
if beforeID := strings.TrimSpace(runtime.Str("before-id")); beforeID != "" {
173+
body["before_id"] = beforeID
174+
}
175+
if afterID := strings.TrimSpace(runtime.Str("after-id")); afterID != "" {
176+
body["after_id"] = afterID
177+
}
178+
return body
179+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
2+
// SPDX-License-Identifier: MIT
3+
4+
package base
5+
6+
import (
7+
"context"
8+
9+
"github.com/larksuite/cli/shortcuts/common"
10+
)
11+
12+
var BaseBaseBlockRename = common.Shortcut{
13+
Service: "base",
14+
Command: "+base-block-rename",
15+
Description: "Rename a block",
16+
Risk: "write",
17+
Scopes: []string{"base:block:update"},
18+
AuthTypes: authTypes(),
19+
Flags: []common.Flag{
20+
baseTokenFlag(true),
21+
baseBlockIDFlag(true),
22+
{Name: "name", Desc: "new unique block name; must not duplicate another block name in this base", Required: true},
23+
},
24+
Tips: []string{
25+
"Example: lark-cli base +base-block-rename --base-token <base_token> --block-id <block_id> --name \"New name\"",
26+
"Renames the block identified by --block-id.",
27+
"Block names must be unique in the base; use +base-block-list first when you need to check existing names.",
28+
"Use +base-block-list first when you need to resolve the target block id from a visible name.",
29+
},
30+
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
31+
return validateBaseBlockRename(runtime)
32+
},
33+
DryRun: dryRunBaseBlockRename,
34+
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
35+
return executeBaseBlockRename(runtime)
36+
},
37+
}

0 commit comments

Comments
 (0)