base

package
v1.0.89 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 34 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BaseAdvpermDisable = common.Shortcut{
	Service:     "base",
	Command:     "+advperm-disable",
	Description: "Disable advanced permissions for a Base",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:app:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
	},
	Tips: []string{
		baseHighRiskYesTip,
		"Disabling advanced permissions invalidates existing custom roles; confirm the target Base before passing --yes.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PUT("/open-apis/base/v3/bases/:base_token/advperm/enable?enable=false").
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("enable", "false")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod:  http.MethodPut,
			ApiPath:     fmt.Sprintf("/open-apis/base/v3/bases/%s/advperm/enable", validate.EncodePathSegment(baseToken)),
			QueryParams: queryParams,
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "disable advanced permissions failed")
	},
}
View Source
var BaseAdvpermEnable = common.Shortcut{
	Service:     "base",
	Command:     "+advperm-enable",
	Description: "Enable advanced permissions for a Base",
	Risk:        "write",
	Scopes:      []string{"base:app:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
	},
	Tips: []string{
		"Caller must be a Base admin; enable advanced permissions before creating or updating roles.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PUT("/open-apis/base/v3/bases/:base_token/advperm/enable?enable=true").
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")

		queryParams := make(larkcore.QueryParams)
		queryParams.Set("enable", "true")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod:  http.MethodPut,
			ApiPath:     fmt.Sprintf("/open-apis/base/v3/bases/%s/advperm/enable", validate.EncodePathSegment(baseToken)),
			QueryParams: queryParams,
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "enable advanced permissions failed")
	},
}
View Source
var BaseAppBlockCreate = common.Shortcut{
	Service:     "base",
	Command:     "+app-block-create",
	Description: "Create a block on a BaseApp page",
	Risk:        "write",
	Scopes:      []string{"base:appmode_block:create", "base:appmode_block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
		{Name: "name", Desc: "block name", Required: true},
		{Name: "type", Desc: "block type: chart(column|bar|line|pie|ring|area|combo|scatter|funnel|wordCloud|radar|statistics) | text | list. Read lark-base-app-block-data-config.md before creating.", Required: true, Enum: appBlockTypes()},
		{Name: "sub-type", Desc: "list subtype: standard|grouped|collapsible|card|detail; defaults to standard", Enum: appListSubTypes},
		{Name: "data-config", Desc: "data_config JSON object; read lark-base-app-block-data-config.md for the SSOT"},
		{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation and normalization; send data_config as-is"},
	},
	Tips: []string{
		`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Order Count" --type statistics --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","count_all":true}]}'`,
		`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Monthly sales" --type column --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","series":[{"field_name":"Amount","rollup":"SUM"}],"group_by":[{"field_name":"Month","sort":{"type":"group","order":"asc"}}]}]}'`,
		"Chart blocks use multi-datasource data_config: one top-level base_token shared by all sources, with table_name/series/count_all/group_by/filter inside each data_sources[] element (text needs none). App block commands carry no --base-token.",
		`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Notes" --type text --data-config '{"text":"# Sales overview"}'`,
		`lark-cli base +app-block-create --app-token <app_token> --page-id <page_id> --name "Open orders" --type list --sub-type standard --data-config '{"base_token":"basxxx","table_name":"Orders"}'`,
		"For list creates, omit optional columns/fields to use the product defaults. The CLI sends them only when explicitly provided.",
		"Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.",
		"A list accepts exactly one base_token, and that Base must be in the same Workspace as the App.",
		"Read lark-base-app-block-data-config.md as the SSOT for chart, list and text config; do not invent data_config from natural language.",
		"Block type cannot be changed after creation and this phase has no delete command, so a wrong --type can only be fixed in the UI. Confirm the type before creating.",
		"Widget layout, position, size and display settings are not part of the public create/update protocol; the platform applies product defaults.",
		"Record block_id for +app-block-update. For chart data reads, pass the returned chart_token to +app-block-get-data --block-id.",
		"Block names must be unique within the page; the CLI checks every existing block before creation.",
		"Create blocks sequentially; do not parallelize multiple block creates for the same page.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		blockType := strings.TrimSpace(runtime.Str("type"))
		if !isAppBlockType(blockType) {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--type %q 不在支持的 block 类型内: %s", blockType, strings.Join(appBlockTypes(), ", ")).WithParam("--type")
		}
		raw := strings.TrimSpace(runtime.Str("data-config"))
		noValidate := runtime.Bool("no-validate")
		var cfg map[string]interface{}
		if raw != "" && !noValidate {
			var err error
			cfg, err = parseJSONObject(newParseCtx(runtime), raw, "data-config")
			if err != nil {
				return err
			}
		}
		if strings.EqualFold(blockType, "list") {
			subType, ok := normalizeAppListSubType(runtime.Str("sub-type"))
			if !ok {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--sub-type 仅支持 %s", strings.Join(appListSubTypes, "|")).WithParam("--sub-type")
			}
			if cfg != nil {
				if problems := validateAppListDataConfig(subType, cfg); len(problems) > 0 {
					return formatDataConfigErrors(problems)
				}
			}
		} else if strings.TrimSpace(runtime.Str("sub-type")) != "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--sub-type 仅适用于 list 类型组件").WithParam("--sub-type")
		}
		if raw == "" {
			if strings.EqualFold(blockType, "list") || isChartBlockType(blockType) {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "%s 类型组件必须提供 data-config", blockType).WithParam("--data-config")
			}
			return nil
		}
		if noValidate {
			return nil
		}
		norm := cfg
		if !strings.EqualFold(blockType, "list") {

			if isChartBlockType(blockType) {
				norm = normalizeAppChartDataConfig(cfg)
			} else {
				norm = normalizeDataConfig(cfg)
			}
			if problems := validateAppBlockDataConfig(blockType, norm); len(problems) > 0 {
				return formatDataConfigErrors(problems)
			}
		}
		b, _ := json.Marshal(norm)
		_ = runtime.Cmd.Flags().Set("data-config", string(b))
		return nil
	},
	DryRun: dryRunAppBlockCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeAppBlockCreate(runtime)
	},
}
View Source
var BaseAppBlockGet = common.Shortcut{
	Service:     "base",
	Command:     "+app-block-get",
	Description: "Get a BaseApp page block by ID",
	Risk:        "read",
	Scopes:      []string{"base:appmode_block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
		appBlockIDFlag(true),
	},
	Tips: []string{
		"lark-cli base +app-block-get --app-token <app_token> --page-id <page_id> --block-id <block_id>",
		"Do not call this command for a component whose +app-block-list result has type=unsupported; the API will return an error.",
		"Returns WidgetDetail: widget_id, name, type, optional chart_token/list sub_type, and data_config.",
		"For a text block, the Markdown content is in data_config.text — read it here; text has no +app-block-get-data endpoint.",
		"For a chart's computed result, pass its chart_token to +app-block-get-data --block-id together with --app-token and --base-token.",
		"Read the current data_config here before replacing nested values with +app-block-update.",
	},
	DryRun: dryRunAppBlockGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeAppBlockGet(runtime)
	},
}
View Source
var BaseAppBlockGetData = common.Shortcut{
	Service:     "base",
	Command:     "+app-block-get-data",
	Description: "Get computed data for a BaseApp page chart block",
	Risk:        "read",
	Scopes:      []string{"base:appmode_block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		baseTokenFlag(true),
		{Name: "block-id", Desc: "chart_token (cht… prefix) returned by +app-block-create, +app-block-list, or +app-block-get", Required: true},
	},
	Tips: []string{
		"lark-cli base +app-block-get-data --app-token <app_token> --base-token <base_token> --block-id <chart_token>",
		"Do not call this command for a component whose +app-block-list result has type=unsupported; the API will return an error.",
		"--block-id must be a chart_token, not a widget_id.",
		"Read --base-token from the chart block data_config.base_token; do not choose an arbitrary +app-get ref key when the app references multiple Bases.",
		"The response uses the same computed chart data protocol as +dashboard-block-get-data.",
		"List and text blocks have no computed data; use +app-block-get for their metadata instead. For text specifically, +app-block-get returns the Markdown content in data_config.text.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return dryRunAppBlockGetData(ctx, runtime)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeAppBlockGetData(runtime)
	},
}
View Source
var BaseAppBlockList = common.Shortcut{
	Service:     "base",
	Command:     "+app-block-list",
	Description: "List blocks on a BaseApp page",
	Risk:        "read",
	Scopes:      []string{"base:appmode_block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
		{Name: "page-size", Type: "int", Default: "20", Desc: "page size; must be positive"},
		{Name: "page-token", Desc: "pagination token"},
	},
	Tips: []string{
		"lark-cli base +app-block-list --app-token <app_token> --page-id <page_id>",
		"A returned component with type=unsupported can only be identified in this list; +app-block-get, +app-block-get-data, and +app-block-update do not support it and will return an error.",
		"Use block_id for +app-block-get/update. For chart data, pass chart_token to +app-block-get-data --block-id.",
		"These are page blocks, not dashboard blocks: do not pass a block_id from here to +dashboard-block-get.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 20, 1, int(^uint(0)>>1))
		return err
	},
	DryRun: dryRunAppBlockList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeAppBlockList(runtime)
	},
}
View Source
var BaseAppBlockUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+app-block-update",
	Description: "Update a block on a BaseApp page",
	Risk:        "write",
	Scopes:      []string{"base:appmode_block:update", "base:appmode_block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
		appBlockIDFlag(true),
		{Name: "name", Desc: "new block name"},
		{Name: "data-config", Desc: "data_config JSON object; read lark-base-app-block-data-config.md for the SSOT"},
		{Name: "no-validate", Type: "bool", Desc: "skip local data_config normalization; send data_config as-is"},
	},
	Tips: []string{
		`lark-cli base +app-block-update --app-token <app_token> --page-id <page_id> --block-id <block_id> --name "Monthly sales"`,
		`lark-cli base +app-block-update --app-token <app_token> --page-id <page_id> --block-id <block_id> --data-config '{"base_token":"basxxx","data_sources":[{"table_name":"Orders","count_all":true,"filter":{"conjunction":"and","conditions":[{"field_name":"Status","operator":"is","value":"Closed"}]}}]}'`,
		"Do not call this command for a component whose +app-block-list result has type=unsupported; the API will return an error.",
		"Read lark-base-app-block-data-config.md as the SSOT; do not invent data_config from natural language.",
		"Use +app-block-get first to inspect the current data_config before replacing nested values.",
		"The type and sub_type of an existing Block are immutable after creation and are not part of data_config; +app-block-update accepts only the name and data_config fields. If a user asks to change type/sub_type, read the current Block and always state this constraint in the final answer, even when it already matches and no write is needed; if it differs, it can only be fixed in the UI.",
		"Only explicitly provided data_config fields are sent; omitted fields stay unchanged. For charts, passing data_sources replaces the whole ordered array, and changing base_token requires sending the full data_sources.",
		"Widget layout, position, size and display settings are not part of the public create/update protocol.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		name := strings.TrimSpace(runtime.Str("name"))
		raw := strings.TrimSpace(runtime.Str("data-config"))
		if name == "" && raw == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--name 与 --data-config 至少提供一个").WithParam("--name")
		}
		if runtime.Bool("no-validate") {
			return nil
		}
		if raw == "" {
			return nil
		}
		pc := newParseCtx(runtime)
		cfg, err := parseJSONObject(pc, raw, "data-config")
		if err != nil {
			return err
		}
		if containsJSONNull(cfg) {
			return formatDataConfigErrors([]string{"Update 不接受 null 作为清空标记"})
		}
		if problems := validateAppBlockUpdateTopLevelFields(cfg); len(problems) > 0 {
			return formatDataConfigErrors(problems)
		}

		norm := normalizeAppChartDataConfig(cfg)
		if sources, exists := norm["data_sources"]; exists {
			items, ok := sources.([]interface{})
			if !ok || len(items) == 0 {
				return formatDataConfigErrors([]string{"data_sources 一旦传入,必须是至少包含一项的完整有序数组"})
			}
			var problems []string
			for i, rawSource := range items {
				source, ok := rawSource.(map[string]interface{})
				if !ok {
					problems = append(problems, fmt.Sprintf("data_sources[%d] 必须是对象", i))
					continue
				}
				for _, problem := range validateAppChartDataSourceConfig(source) {
					problems = append(problems, fmt.Sprintf("data_sources[%d]: %s", i, problem))
				}
			}
			if len(problems) > 0 {
				return formatDataConfigErrors(problems)
			}
		}
		b, _ := json.Marshal(norm)
		_ = runtime.Cmd.Flags().Set("data-config", string(b))
		return nil
	},
	DryRun: dryRunAppBlockUpdate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeAppBlockUpdate(runtime)
	},
}
View Source
var BaseAppCreate = common.Shortcut{
	Service:     "base",
	Command:     "+app-create",
	Description: "Create a new BaseApp in a Workspace (not a copy)",
	Risk:        "write",
	Scopes: []string{
		"base:appmode:create",
		"base:workspace:update",
	},
	AuthTypes: authTypes(),
	Flags: []common.Flag{
		{Name: "name", Desc: "BaseApp name", Required: true},
		workspaceTokenFlag(true),
		{Name: "theme-style", Desc: "theme style", Enum: []string{"default", "cloudBlue", "fresh", "softLight", "future", "technology"}},
	},
	Tips: []string{
		`lark-cli base +app-create --name "Sales app" --workspace-token <workspace_token>`,
		`lark-cli base +app-create --name "Sales app" --workspace-token <workspace_token> --theme-style cloudBlue`,
		"This command creates a new empty BaseApp; it does not copy an existing BaseApp or its pages and blocks.",
		"Create or select a Base separately when the app needs data.",
		"Record the returned app_token; page and block commands require it.",
	},
	DryRun: dryRunBaseappCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappCreate(runtime)
	},
}
View Source
var BaseAppGet = common.Shortcut{
	Service:     "base",
	Command:     "+app-get",
	Description: "Get BaseApp info, page summaries, and the referenced Base/Table map",
	Risk:        "read",
	Scopes:      []string{"base:appmode:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
	},
	Tips: []string{
		"lark-cli base +app-get --app-token <app_token>",
		"ref maps each Base token currently referenced by app widgets to the names of its referenced tables; table/field/record commands take the Base token keys.",
		"The response includes page summaries. Use +app-page-get or +app-block-list for component details.",
	},
	DryRun: dryRunBaseappGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappGet(runtime)
	},
}
View Source
var BaseAppPageCreate = common.Shortcut{
	Service:     "base",
	Command:     "+app-page-create",
	Description: "Create a page in a BaseApp",
	Risk:        "write",
	Scopes:      []string{"base:appmode_page:create", "base:appmode_page:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		{Name: "name", Desc: "page name", Required: true},
	},
	Tips: []string{
		`lark-cli base +app-page-create --app-token <app_token> --name "Overview"`,
		"Page names must be unique within an app; the CLI checks existing pages before creation.",
		"Record the returned page_id; every +app-block-* command needs it.",
		"This release creates top-level pages only; PageGroup placement is not supported.",
	},
	DryRun: dryRunBaseappPageCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappPageCreate(runtime)
	},
}
View Source
var BaseAppPageDelete = common.Shortcut{
	Service:     "base",
	Command:     "+app-page-delete",
	Description: "Delete a BaseApp page",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:appmode_page:delete"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
	},
	Tips: []string{
		"lark-cli base +app-page-delete --app-token <app_token> --page-id <page_id> --yes",
		"Deleting a page also deletes its blocks and cannot be recovered; the base data behind the blocks is untouched.",
		baseHighRiskYesTip,
	},
	DryRun: dryRunBaseappPageDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappPageDelete(runtime)
	},
}
View Source
var BaseAppPageGet = common.Shortcut{
	Service:     "base",
	Command:     "+app-page-get",
	Description: "Get a BaseApp page by ID",
	Risk:        "read",
	Scopes:      []string{"base:appmode_page:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
	},
	Tips: []string{
		"lark-cli base +app-page-get --app-token <app_token> --page-id <page_id>",
		"The response is PageDetail and always includes widget summaries. Use +app-block-list when you need paginated widget details.",
	},
	DryRun: dryRunBaseappPageGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappPageGet(runtime)
	},
}
View Source
var BaseAppPageList = common.Shortcut{
	Service:     "base",
	Command:     "+app-page-list",
	Description: "List pages in a BaseApp",
	Risk:        "read",
	Scopes:      []string{"base:appmode_page:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		{Name: "page-size", Type: "int", Default: "20", Desc: "page size; must be positive"},
		{Name: "page-token", Desc: "pagination token"},
	},
	Tips: []string{
		"lark-cli base +app-page-list --app-token <app_token>",
		"Use the returned page_id for +app-page-get/update/delete and every +app-block-* command.",
		"If a returned page has name=\"\", the current user has no permission to that page; do not treat it as an untitled page or use it as an operation target.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 20, 1, int(^uint(0)>>1))
		return err
	},
	DryRun: dryRunBaseappPageList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappPageList(runtime)
	},
}
View Source
var BaseAppPageRename = common.Shortcut{
	Service:     "base",
	Command:     "+app-page-update",
	Description: "Rename a BaseApp page",
	Risk:        "write",
	Scopes:      []string{"base:appmode_page:update", "base:appmode_page:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		appTokenFlag(true),
		pageIDFlag(true),
		{Name: "name", Desc: "new page name", Required: true},
	},
	Tips: []string{
		`lark-cli base +app-page-update --app-token <app_token> --page-id <page_id> --name "Overview"`,
		"Page names must be unique within an app; the CLI excludes the current page while checking.",
		"Renaming does not move the page; ordering and parent stay unchanged.",
	},
	DryRun: dryRunBaseappPageRename,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseappPageRename(runtime)
	},
}
View Source
var BaseBaseBlockCreate = common.Shortcut{
	Service:     "base",
	Command:     "+base-block-create",
	Description: "Create a block",
	Risk:        "write",
	Scopes:      []string{"base:block:create"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "type", Desc: "resource type", Required: true, Enum: baseBlockTypeEnums},
		{Name: "name", Desc: "block name", Required: true},
		{Name: "parent-id", Desc: "folder block id; when omitted, create at root"},
	},
	Tips: []string{
		"Example: lark-cli base +base-block-create --base-token <base_token> --type folder --name \"Project Docs\"",
		"Example: lark-cli base +base-block-create --base-token <base_token> --type table --name \"Tasks\"",
		"Example: lark-cli base +base-block-create --base-token <base_token> --type docx --name \"Spec\" --parent-id <folder_block_id>",
		"Example: lark-cli base +base-block-create --base-token <base_token> --type dashboard --name \"Metrics\"",
		"Example: lark-cli base +base-block-create --base-token <base_token> --type workflow --name \"Approval Flow\"",
		"Creates a folder, table, docx, dashboard, or workflow entry.",
		"Do not pass null for --parent-id. Omit it to create at the root level.",
		"Created resources still use their own commands for content operations, such as table/field/record/docx/dashboard/workflow commands.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateBaseBlockCreate(runtime)
	},
	DryRun: dryRunBaseBlockCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseBlockCreate(runtime)
	},
}
View Source
var BaseBaseBlockDelete = common.Shortcut{
	Service:     "base",
	Command:     "+base-block-delete",
	Description: "Delete a block",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:block:delete"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		baseBlockIDFlag(true),
	},
	Tips: []string{
		"Example: lark-cli base +base-block-delete --base-token <base_token> --block-id <block_id> --yes",
		"Deletes the block identified by --block-id.",
		"Recursive folder deletion is not supported. If a folder is not empty, move or delete its children first.",
		"Different block types may have independent backing resources; deletion follows backend semantics.",
		"Use +base-block-list first when you need to confirm the target block id.",
		"If the user already explicitly confirmed this exact delete target, pass --yes without asking again.",
	},
	DryRun: dryRunBaseBlockDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseBlockDelete(runtime)
	},
}
View Source
var BaseBaseBlockList = common.Shortcut{
	Service:     "base",
	Command:     "+base-block-list",
	Description: "List blocks in a base",
	Risk:        "read",
	Scopes:      []string{"base:block:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "type", Desc: "filter by resource type", Enum: baseBlockTypeEnums},
		{Name: "parent-id", Desc: "folder block id; when omitted, list all blocks"},
	},
	Tips: []string{
		"Example: lark-cli base +base-block-list --base-token <base_token>",
		"Example: lark-cli base +base-block-list --base-token <base_token> --type table",
		"Example: lark-cli base +base-block-list --base-token <base_token> --parent-id <folder_block_id>",
		`JQ crop: lark-cli base +base-block-list --base-token <base_token> | jq '.blocks[] | {type, name, block_id: .id, parent_id}'`,
		`JQ crop docx: lark-cli base +base-block-list --base-token <base_token> --type docx | jq '.blocks[] | {name, docx_token}'`,
		"Blocks are resources managed directly by the base, such as folder, table, docx, dashboard, and workflow.",
		"For table, dashboard, and workflow blocks, returned id is the table-id, dashboard-id, or workflow-id used by the corresponding commands.",
		"For docx blocks, use the returned docx_token with docx commands.",
		"For folder blocks, pass the returned id as --parent-id when creating, listing, or moving blocks inside that folder.",
		"This command returns the full backend list. It intentionally does not expose limit or offset.",
		"Pass --type to list only one resource type.",
		"Pass --parent-id to list only direct children of a folder.",
		"Dashboard blocks are chart/widget blocks inside a dashboard; use +dashboard-block-* for those.",
	},
	DryRun: dryRunBaseBlockList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseBlockList(runtime)
	},
}
View Source
var BaseBaseBlockMove = common.Shortcut{
	Service:     "base",
	Command:     "+base-block-move",
	Description: "Move a block",
	Risk:        "write",
	Scopes:      []string{"base:block:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		baseBlockIDFlag(true),
		{Name: "parent-id", Desc: "target folder block id; when omitted, move to root"},
		{Name: "before-id", Desc: "sibling block id; move the block before this sibling in the target folder/root order"},
		{Name: "after-id", Desc: "sibling block id; move the block after this sibling in the target folder/root order"},
	},
	Tips: []string{
		"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --parent-id <folder_block_id>",
		"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --after-id <sibling_block_id>",
		"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id> --before-id <sibling_block_id>",
		"Example: lark-cli base +base-block-move --base-token <base_token> --block-id <block_id>",
		"Omit --parent-id to move the block to root; do not pass null.",
		"--before-id and --after-id are mutually exclusive.",
		"When moving a folder, its children remain under that folder.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateBaseBlockMove(runtime)
	},
	DryRun: dryRunBaseBlockMove,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseBlockMove(runtime)
	},
}
View Source
var BaseBaseBlockRename = common.Shortcut{
	Service:     "base",
	Command:     "+base-block-rename",
	Description: "Rename a block",
	Risk:        "write",
	Scopes:      []string{"base:block:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		baseBlockIDFlag(true),
		{Name: "name", Desc: "new unique block name; must not duplicate another block name in this base", Required: true},
	},
	Tips: []string{
		"Example: lark-cli base +base-block-rename --base-token <base_token> --block-id <block_id> --name \"New name\"",
		"Renames the block identified by --block-id.",
		"Block names must be unique in the base; use +base-block-list first when you need to check existing names.",
		"Use +base-block-list first when you need to resolve the target block id from a visible name.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateBaseBlockRename(runtime)
	},
	DryRun: dryRunBaseBlockRename,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseBlockRename(runtime)
	},
}
View Source
var BaseBaseCopy = common.Shortcut{
	Service:     "base",
	Command:     "+base-copy",
	Description: "Copy a Base resource (not a BaseApp)",
	Risk:        "write",
	UserScopes:  []string{"base:app:copy"},
	BotScopes:   []string{"base:app:copy", "docs:permission.member:create"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "name", Desc: "new base name"},
		{Name: "folder-token", Desc: "folder token for destination"},
		{Name: "without-content", Type: "bool", Desc: "copy structure only"},
		{Name: "time-zone", Desc: "time zone, e.g. Asia/Shanghai"},
	},
	Tips: []string{
		"BaseApp/AppMode copy is unsupported. Do not pass an app_token or use this command as a substitute.",
		`Example: lark-cli base +base-copy --base-token <base_token> --name "Copy of Project Tracker"`,
		"Use --without-content when the user wants only structure.",
		"If copied as bot, output may include permission_grant; report it so the user knows whether they can open the new Base.",
	},
	DryRun: dryRunBaseCopy,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseCopy(runtime)
	},
}
View Source
var BaseBaseCreate = common.Shortcut{
	Service:     "base",
	Command:     "+base-create",
	Description: "Create a new base resource",
	Risk:        "write",
	UserScopes: []string{
		"base:app:create",
		"base:table:read",
		"base:table:create",
		"base:table:update",
		"base:table:delete",
	},
	BotScopes: []string{
		"base:app:create",
		"base:table:read",
		"base:table:create",
		"base:table:update",
		"base:table:delete",
		"docs:permission.member:create",
	},
	AuthTypes: authTypes(),
	Flags: []common.Flag{
		{Name: "name", Desc: "base name", Required: true},
		{Name: "folder-token", Desc: "folder token for destination"},
		{Name: "time-zone", Desc: "time zone, e.g. Asia/Shanghai"},
		{Name: "fields", Desc: `field JSON array for the first table schema; use with --table-name, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]`},
		{Name: "table-name", Desc: "first table name for the custom first table schema; use with --fields"},
	},
	Tips: []string{
		`Example: lark-cli base +base-create --name "Project Tracker" --time-zone Asia/Shanghai`,
		`Strongly recommended initial table schema: lark-cli base +base-create --name "Project Tracker" --table-name "Tasks" --fields '[{"name":"Title","type":"text"},{"name":"Status","type":"select","options":[{"name":"Todo"},{"name":"Done"}]}]'`,
		"Before using --fields, read lark-base-field-schema.md or rely on the same field JSON shape used by +field-create; do not invent field properties.",
		"If --table-name and --fields are both omitted, Base creates one initial table with the platform default schema.",
		"If created as bot, output may include permission_grant; report it so the user knows whether they can open the new Base.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateBaseCreate(runtime)
	},
	DryRun: dryRunBaseCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseCreate(runtime)
	},
}
View Source
var BaseBaseGet = common.Shortcut{
	Service:     "base",
	Command:     "+base-get",
	Description: "Get a base resource",
	Risk:        "read",
	Scopes:      []string{"base:app:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true)},
	Tips: []string{
		"Use a real Base token; workspace tokens and wiki tokens are not accepted by this command.",
	},
	DryRun: dryRunBaseGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseGet(runtime)
	},
}
View Source
var BaseButtonRuleBind = common.Shortcut{
	Service:     "base",
	Command:     "+button-rule-bind",
	Description: "Bind a button field to a workflow",
	Risk:        "write",
	Scopes:      []string{"base:field:read", "base:field:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		fieldRefFlag(true),
		{Name: "workflow-id", Desc: "public workflow ID returned by +workflow-create or +workflow-list (wkf prefix)", Required: true},
	},
	Tips: []string{
		"Use this after +workflow-create and +field-create; do not put workflow_id in the field JSON.",
		"workflow-id must be the public wkf ID returned by workflow commands; never pass an internal numeric workflow ID.",
		"Binding is independent from workflow enablement. Query with +button-rule-get, then call +workflow-enable only if the user wants it active.",
		"If binding fails after workflow and field creation, keep both IDs and retry this command instead of recreating them.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateButtonRuleWorkflowID(runtime)
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return buttonRuleDryRun(runtime, "PUT", strings.TrimSpace(runtime.Str("workflow-id")))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fieldID, err := resolveButtonRuleFieldID(runtime)
		if err != nil {
			return err
		}
		body := map[string]interface{}{"workflow_id": strings.TrimSpace(runtime.Str("workflow-id"))}
		data, err := baseV3Call(runtime, "PUT", buttonRulePath(runtime, fieldID), nil, body)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseButtonRuleGet = common.Shortcut{
	Service:     "base",
	Command:     "+button-rule-get",
	Description: "Get the target bound to a button field",
	Risk:        "read",
	Scopes:      []string{"base:field:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		fieldRefFlag(true),
	},
	Tips: []string{
		"Returns bound=false and target=null when the button field has no binding.",
		"When target.type is workflow, target.id is a public wkf ID suitable for +workflow-get, +workflow-enable, and +button-rule-bind.",
		"Use this after +button-rule-bind before enabling a newly created workflow.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateButtonRuleLocator(runtime)
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return buttonRuleDryRun(runtime, "GET", "")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fieldID, err := resolveButtonRuleFieldID(runtime)
		if err != nil {
			return err
		}
		data, err := baseV3Call(runtime, "GET", buttonRulePath(runtime, fieldID), nil, nil)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseButtonRuleUnbind = common.Shortcut{
	Service:     "base",
	Command:     "+button-rule-unbind",
	Description: "Remove the workflow binding from a button field",
	Risk:        "write",
	Scopes:      []string{"base:field:read", "base:field:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		fieldRefFlag(true),
	},
	Tips: []string{
		"Unbind removes only the ButtonRule relation; it does not delete the field or workflow.",
		"Repeat unbind is safe and should leave the button field with bound=false.",
		"Use +button-rule-get after unbind when the agent needs readback evidence.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateButtonRuleLocator(runtime)
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return buttonRuleDryRun(runtime, "PUT", "")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		fieldID, err := resolveButtonRuleFieldID(runtime)
		if err != nil {
			return err
		}
		data, err := baseV3Call(runtime, "PUT", buttonRulePath(runtime, fieldID), nil, map[string]interface{}{"workflow_id": ""})
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseDashboardArrange = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-arrange",
	Description: "Auto-arrange dashboard blocks layout (server-side smart layout)",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:update"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		{Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"},
	},
	Tips: []string{
		"Server-side smart layout is not deterministic or position-specific; use only when the user asks to arrange or beautify a dashboard, or to tidy up a dashboard created from scratch in this session.",
		"For exact placement, set --position on +dashboard-block-create / +dashboard-block-update instead; do not run this command as a substitute for coordinates the user asked for.",
	},
	DryRun: dryRunDashboardArrange,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardArrange(runtime)
	},
}
View Source
var BaseDashboardBlockCreate = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-create",
	Description: "Create a block in a dashboard",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:create"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		{Name: "name", Desc: "block name", Required: true},
		{Name: "type", Desc: "block type: column(柱状图)|bar(条形图)|line(折线图)|pie(饼图)|ring(环形图)|area(面积图)|combo(组合图)|scatter(散点图)|funnel(漏斗图)|wordCloud(词云)|radar(雷达图)|statistics(指标卡)|text(文本). Read lark-base-dashboard-block-config.md before creating.", Required: true},
		{Name: "data-config", Desc: "data_config JSON object; read lark-base-dashboard-block-config.md for the SSOT"},
		{Name: "position", Desc: `optional. component position+size in 12-col grid, JSON {"x","y","w","h"}; all four keys required and numeric (position is submitted whole, so a partial object cannot express a complete placement). Advisory bounds x/y>=0, 1<=w<=12 and x+w<=12, h>=1 — coordinate VALUES are not validated locally and pass through as given; the server auto-arranges out-of-range or overlapping positions. Omit for server auto-layout`},
		{Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"},
		{Name: "no-validate", Type: "bool", Desc: "skip local SEMANTIC validation: data_config checks + normalization, and the --position x/y/w/h completeness check. JSON syntax is still parsed (a malformed value never silently vanishes from the preview). Sends data_config and position as-is"},
	},
	Tips: []string{
		`lark-cli base +dashboard-block-create --base-token <base_token> --dashboard-id <dashboard_id> --name "Order Count" --type statistics --data-config '{"table_name":"Orders","count_all":true}'`,
		`lark-cli base +dashboard-block-create --base-token <base_token> --dashboard-id <dashboard_id> --name "Revenue" --type statistics --data-config '{"table_name":"Orders","series":[{"field_name":"Amount","rollup":"SUM"}],"number_format":{"formatName":"dollar_rounded","precision":2}}'`,
		`lark-cli base +dashboard-block-create --base-token <base_token> --dashboard-id <dashboard_id> --name "Dashboard Note" --type text --data-config '{"text":"# Sales Dashboard"}'`,
		`lark-cli base +dashboard-block-create --base-token <base_token> --dashboard-id <dashboard_id> --name "Order Count" --type statistics --data-config '{"table_name":"Orders","count_all":true}' --position '{"x":0,"y":0,"w":6,"h":4}'`,
		"Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.",
		"data_config uses table and field names, not table_id or field_id.",
		"Read lark-base-dashboard-block-config.md as the SSOT for chart templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.",
		"--position is optional precise layout in a 12-col grid; omit it to let the server auto-layout. Coordinate values are not validated locally; the server auto-arranges out-of-range or overlapping positions. To re-tidy an existing dashboard use +dashboard-arrange instead.",
		"For funnel/stage charts backed by ordered helper data, set the intended group_by.sort in the initial create request; do not create first and then issue a second update just to fix sorting.",
		"Record the returned block_id; block update/delete/get-data commands need it.",
		"Create dashboard blocks sequentially; do not parallelize multiple block creates for the same dashboard.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		pc := newParseCtx(runtime)
		if err := validateDashboardBlockPosition(pc, runtime); err != nil {
			return err
		}
		raw := strings.TrimSpace(runtime.Str("data-config"))
		if raw == "" {

			if !runtime.Bool("no-validate") && strings.EqualFold(strings.TrimSpace(runtime.Str("type")), "text") {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "text 类型组件必须提供 data-config,包含必填字段 text").WithParam("--data-config")
			}
			return nil
		}
		cfg, err := parseJSONObject(pc, raw, "data-config")
		if err != nil {
			return err
		}
		effective := cfg
		if !runtime.Bool("no-validate") {
			effective = normalizeDataConfig(cfg)
			if errs := validateBlockDataConfig(runtime.Str("type"), effective); len(errs) > 0 {
				return formatDataConfigErrors(errs)
			}
		}

		b, _ := json.Marshal(effective)
		_ = runtime.Cmd.Flags().Set("data-config", string(b))
		return nil
	},
	DryRun: dryRunDashboardBlockCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockCreate(runtime)
	},
}
View Source
var BaseDashboardBlockDelete = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-delete",
	Description: "Delete a dashboard block",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:dashboard:delete"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		blockIDFlag(true),
	},
	Tips: []string{
		"lark-cli base +dashboard-block-delete --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id> --yes",
		baseHighRiskYesTip,
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id").
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id")).
			Set("block_id", runtime.Str("block-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockDelete(runtime)
	},
}
View Source
var BaseDashboardBlockGet = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-get",
	Description: "Get a dashboard block by ID",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:read"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		blockIDFlag(true),
		{Name: "user-id-type", Desc: "user ID type: open_id / union_id / user_id"},
	},
	Tips: []string{
		"lark-cli base +dashboard-block-get --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id>",
		"Use this command for block metadata such as name, type, layout, and data_config.",
		"Text block content is stored in data_config.text; include it when the user asks for all dashboard content.",
		"Use +dashboard-block-get-data when you need the computed chart result instead of metadata.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		params := map[string]interface{}{}
		if uid := strings.TrimSpace(runtime.Str("user-id-type")); uid != "" {
			params["user_id_type"] = uid
		}
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id").
			Params(params).
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id")).
			Set("block_id", runtime.Str("block-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockGet(runtime)
	},
}
View Source
var BaseDashboardBlockGetData = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-get-data",
	Description: "Get computed data for a dashboard chart block",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:read"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		blockIDFlag(true),
		{Name: "dashboard-id", Desc: "hidden compatibility flag accepted by dashboard block commands; ignored by get-data", Hidden: true},
	},
	Tips: []string{
		"lark-cli base +dashboard-block-get-data --base-token <base_token> --block-id <block_id>",
		"This command does not need --dashboard-id.",
		"Use +dashboard-block-get first when you need block metadata like name, type, or data_config.",
		"This command returns computed chart protocol JSON directly, not wrapped block metadata.",
		"For a complete dashboard export, read text blocks with +dashboard-block-get; their content is in data_config.text.",
		"If a chart type does not support computed data, inspect its data_config with +dashboard-block-get, then use +data-query with the same real table, dimensions, measures, and filters; do not omit the block or guess values.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return dryRunDashboardBlockGetData(ctx, runtime)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockGetData(runtime)
	},
}
View Source
var BaseDashboardBlockList = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-list",
	Description: "List blocks in a dashboard",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:read"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		{Name: "page-size", Type: "int", Default: "20", Desc: "page size, range 1-100"},
		{Name: "page-token", Desc: "pagination token"},
	},
	Tips: []string{
		"lark-cli base +dashboard-block-list --base-token <base_token> --dashboard-id <dashboard_id>",
		"Use returned block_id and type values for +dashboard-block-get/update/delete/get-data.",
		"For a complete dashboard, use --page-size 100; while has_more=true, pass the returned page_token to --page-token and continue until has_more=false.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 20, 1, 100)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		params := map[string]interface{}{}
		params["page_size"] = runtime.Int("page-size")
		if pt := strings.TrimSpace(runtime.Str("page-token")); pt != "" {
			params["page_token"] = pt
		}
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks").
			Params(params).
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockList(runtime)
	},
}
View Source
var BaseDashboardBlockUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-block-update",
	Description: "Update a dashboard block",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:update"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		blockIDFlag(true),
		{Name: "name", Desc: "new block name"},
		{Name: "data-config", Desc: "data_config JSON object; read lark-base-dashboard-block-config.md for the SSOT"},
		{Name: "position", Desc: `optional. component position+size in 12-col grid, JSON {"x","y","w","h"}; all four keys required and numeric (position is submitted whole, so a partial object cannot express a complete placement). Advisory bounds x/y>=0, 1<=w<=12 and x+w<=12, h>=1 — coordinate VALUES are not validated locally and pass through as given; the server auto-arranges out-of-range or overlapping positions. Omit to leave layout unchanged`},
		{Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"},
		{Name: "no-validate", Type: "bool", Desc: "skip local SEMANTIC validation: data_config checks + normalization, and the --position x/y/w/h completeness check. JSON syntax is still parsed (a malformed value never silently vanishes from the preview). Sends data_config and position as-is"},
	},
	Tips: []string{
		`lark-cli base +dashboard-block-update --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id> --name "Total Sales"`,
		`lark-cli base +dashboard-block-update --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id> --data-config '{"series":[{"field_name":"Amount","rollup":"SUM"}]}'`,
		`lark-cli base +dashboard-block-update --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id> --data-config '{"number_format":{"formatName":"dollar_rounded","precision":0}}'`,
		`lark-cli base +dashboard-block-update --base-token <base_token> --dashboard-id <dashboard_id> --block-id <block_id> --position '{"x":6,"y":0,"w":6,"h":4}'`,
		"Read lark-base-dashboard-block-config.md as the SSOT for data_config templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.",
		"Use +dashboard-block-get first to inspect the current data_config before replacing nested values.",
		"Block type cannot be changed; delete and recreate the block to change chart type.",
		"data_config update merges top-level keys; each provided key is normally replaced as a whole, except number_format, whose subfields merge server-side.",
		"--position is optional precise layout in a 12-col grid; omit it to leave the current layout unchanged. Coordinate values are not validated locally; the server auto-arranges out-of-range or overlapping positions. To re-tidy an existing dashboard use +dashboard-arrange instead.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		pc := newParseCtx(runtime)
		if err := validateDashboardBlockPosition(pc, runtime); err != nil {
			return err
		}
		raw := strings.TrimSpace(runtime.Str("data-config"))
		if raw == "" {
			return nil
		}
		cfg, err := parseJSONObject(pc, raw, "data-config")
		if err != nil {
			return err
		}
		effective := cfg
		if !runtime.Bool("no-validate") {
			effective = normalizeDataConfig(cfg)

			if rawNumberFormat, hasNumberFormat := effective["number_format"]; hasNumberFormat {
				if problems := validateNumberFormat(rawNumberFormat); len(problems) > 0 {
					return formatDataConfigErrors(problems)
				}
			}
		}

		b, _ := json.Marshal(effective)
		_ = runtime.Cmd.Flags().Set("data-config", string(b))
		return nil
	},
	DryRun: dryRunDashboardBlockUpdate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardBlockUpdate(runtime)
	},
}
View Source
var BaseDashboardCreate = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-create",
	Description: "Create a dashboard in a base",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:create"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "name", Desc: "dashboard name", Required: true},
		{Name: "theme-style", Desc: "theme style, defaults to platform default when omitted"},
	},
	Tips: []string{
		"Record the returned dashboard_id; dashboard block create/get/update/delete/arrange commands need it.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := map[string]interface{}{}
		if name := runtime.Str("name"); name != "" {
			body["name"] = name
		}
		if themeStyle := runtime.Str("theme-style"); themeStyle != "" {
			body["theme"] = map[string]interface{}{"theme_style": themeStyle}
		}
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/dashboards").
			Body(body).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardCreate(runtime)
	},
}
View Source
var BaseDashboardDelete = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-delete",
	Description: "Delete a dashboard",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:dashboard:delete"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
	},
	Tips: []string{
		"lark-cli base +dashboard-delete --base-token <base_token> --dashboard-id <dashboard_id> --yes",
		"Deleting a dashboard also deletes its blocks and cannot be recovered.",
		baseHighRiskYesTip,
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id").
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardDelete(runtime)
	},
}
View Source
var BaseDashboardGet = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-get",
	Description: "Get a dashboard by ID",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:read"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
	},
	Tips: []string{
		"Use +dashboard-block-list or +dashboard-block-get when you need block-level details.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id").
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardGet(runtime)
	},
}
View Source
var BaseDashboardList = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-list",
	Description: "List dashboards in a base",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:read"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "page-size", Type: "int", Default: "100", Desc: "page size, range 1-100"},
		{Name: "page-token", Desc: "pagination token"},
	},
	Tips: []string{
		"Use returned dashboard_id values for +dashboard-get, +dashboard-block-list, and +dashboard-block-create.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 100, 1, 100)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		params := map[string]interface{}{}
		params["page_size"] = runtime.Int("page-size")
		if pt := strings.TrimSpace(runtime.Str("page-token")); pt != "" {
			params["page_token"] = pt
		}
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/dashboards").
			Params(params).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardList(runtime)
	},
}
View Source
var BaseDashboardShareGet = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-share-get",
	Description: "Get dashboard share status and settings",
	Risk:        "read",
	Scopes:      []string{"base:dashboard:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
	},
	DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/share").
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "GET", baseV3Path(
			"bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "share",
		), nil, nil)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseDashboardShareUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-share-update",
	Description: "Update dashboard share status and settings",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		{Name: "enabled", Type: "bool", Desc: "enable or disable dashboard sharing"},
		{Name: "access-scope", Desc: "share access scope", Enum: shareAccessScopeEnums},
		{Name: "show-source", Type: "bool", Desc: "show the entry back to the source Base"},
		{Name: "enable-auto-analysis", Type: "bool", Desc: "enable intelligent analysis on the shared dashboard"},
	},
	Tips: []string{
		"Boolean settings use PATCH semantics: pass --show-source=false or --enable-auto-analysis=false to explicitly turn them off.",
		"Update exactly one field per invocation; run separate commands to change multiple share fields.",
	},
	Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
		return validateSingleShareUpdate(runtime, dashboardShareUpdateFlagNames...)
	},
	DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/share").
			Body(buildDashboardShareUpdateBody(runtime)).
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "PATCH", baseV3Path(
			"bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "share",
		), nil, buildDashboardShareUpdateBody(runtime))
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseDashboardUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+dashboard-update",
	Description: "Update a dashboard",
	Risk:        "write",
	Scopes:      []string{"base:dashboard:update"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		dashboardIDFlag(true),
		{Name: "name", Desc: "new dashboard name"},
		{Name: "theme-style", Desc: "theme style, leave empty to keep current theme"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := map[string]interface{}{}
		if name := runtime.Str("name"); name != "" {
			body["name"] = name
		}
		if themeStyle := runtime.Str("theme-style"); themeStyle != "" {
			body["theme"] = map[string]interface{}{"theme_style": themeStyle}
		}
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id").
			Body(body).
			Set("base_token", runtime.Str("base-token")).
			Set("dashboard_id", runtime.Str("dashboard-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeDashboardUpdate(runtime)
	},
}
View Source
var BaseDataQuery = common.Shortcut{
	Service:     "base",
	Command:     "+data-query",
	Description: "Query and analyze Base data with JSON DSL (aggregation, filter, sort)",
	Risk:        "read",
	Scopes:      []string{"base:table:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "dsl", Desc: "query JSON DSL; first follow lark-base-record-query-and-analysis-sop.md, then read lark-base-data-query.md only if that SOP selects +data-query", Required: true},
	},
	Tips: []string{
		"Read lark-base-record-query-and-analysis-sop.md before using this command; use +data-query only when that SOP selects the Cloud aggregation path.",
		"After the SOP selects +data-query, read lark-base-data-query.md for its fewshots and DSL contract.",
		"`dimensions` and `measures` cannot both be empty.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		var dsl map[string]interface{}
		dec := json.NewDecoder(bytes.NewReader([]byte(runtime.Str("dsl"))))
		dec.UseNumber()
		if err := dec.Decode(&dsl); err != nil {
			return baseFlagErrorf("--dsl invalid JSON: %v", err)
		}
		_, hasDim := dsl["dimensions"]
		_, hasMeas := dsl["measures"]
		if !hasDim && !hasMeas {
			return baseFlagErrorf("--dsl must contain at least one of 'dimensions' or 'measures'")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		var dsl map[string]interface{}
		dec := json.NewDecoder(bytes.NewReader([]byte(runtime.Str("dsl"))))
		dec.UseNumber()
		dec.Decode(&dsl)
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/data/query").
			Body(dsl).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")

		var dsl map[string]interface{}
		dec := json.NewDecoder(bytes.NewReader([]byte(runtime.Str("dsl"))))
		dec.UseNumber()
		dec.Decode(&dsl)

		data, err := baseV3Call(runtime, "POST", baseV3Path("bases", baseToken, "data/query"), nil, dsl)
		if err != nil {
			return err
		}

		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseFieldCreate = common.Shortcut{
	Service:     "base",
	Command:     "+field-create",
	Description: "Create one or more fields",
	Risk:        "write",
	Scopes:      []string{"base:field:create"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "json", Desc: "field property JSON object or non-empty array of field objects; supports @file", Required: true},
		{Name: "i-have-read-guide", Type: "bool", Desc: "set only after you have read the formula/lookup guide for those field types", Hidden: true},
	},
	Tips: []string{
		`Example text: lark-cli base +field-create --base-token <base_token> --table-id <table_id> --json '{"name":"Status","type":"text"}'`,
		`Example select: lark-cli base +field-create --base-token <base_token> --table-id <table_id> --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}'`,
		`+field-create defines storage schema only: choose a documented field type from explicit stored-value requirements and the user's semantics. Treat the field name or business purpose only as a clue to confirm; do not use it to invent derived behavior. Use style only to format the chosen type.`,
		`For explicitly requested derived, automatic, synchronized, or backfilled behavior, use documented formula, lookup, link, workflow, or automation only. If unsupported, do not probe code/web/OpenAPI, create a storage placeholder, or claim completion; report the boundary and alternatives.`,
		"Agent hint: for multiple fields in one table, prefer one array; array items are created sequentially.",
		"For generated arrays, prefer --json @file or an argv-safe subprocess call; do not double-escape JSON inside shell command substitution.",
		`For large arrays, bound successful stdout with --jq 'if .ok then (.data | {created,total,field_get_recommended,next_step,verification_hint}) else . end'; this preserves the full partial-failure envelope. Omit the projection when individual field IDs are needed.`,
		"On successful simple fields, next_step:done means stop: do not list/get fields unless readback is explicitly requested; if needed, filter +field-list with --jq instead of printing every field.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateFieldCreate(runtime)
	},
	DryRun: dryRunFieldCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldCreate(runtime)
	},
}
View Source
var BaseFieldDelete = common.Shortcut{
	Service:     "base",
	Command:     "+field-delete",
	Description: "Delete a field by ID or name",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:field:delete"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)},
	Tips: []string{
		baseHighRiskYesTip,
		`Example: lark-cli base +field-delete --base-token <base_token> --table-id <table_id> --field-id "Status" --yes`,
	},
	DryRun: dryRunFieldDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldDelete(runtime)
	},
}
View Source
var BaseFieldGet = common.Shortcut{
	Service:     "base",
	Command:     "+field-get",
	Description: "Get a field by ID or name",
	Risk:        "read",
	Scopes:      []string{"base:field:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), fieldRefFlag(true)},
	Tips: []string{
		`Example: lark-cli base +field-get --base-token <base_token> --table-id <table_id> --field-id "Status"`,
		"field-id accepts a field ID (fld...) or the field name from the current table.",
		"Returns full field configuration; use it as the baseline before +field-update.",
	},
	DryRun: dryRunFieldGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldGet(runtime)
	},
}
View Source
var BaseFieldList = common.Shortcut{
	Service:     "base",
	Command:     "+field-list",
	Description: "List fields in a table",
	Risk:        "read",
	Scopes:      []string{"base:field:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Default: "100", Desc: "pagination size, range 1-200"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "limit", 100, 1, 200)
		return err
	},
	DryRun: dryRunFieldList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldList(runtime)
	},
}
View Source
var BaseFieldSearchOptions = common.Shortcut{
	Service:     "base",
	Command:     "+field-search-options",
	Description: "Search select options of a field",
	Risk:        "read",
	Scopes:      []string{"base:field:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		fieldRefFlag(true),
		{Name: "keyword", Desc: "keyword for option query"},
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Default: "30", Desc: "pagination size, range 1-200"},
	},
	Tips: []string{
		`Example: lark-cli base +field-search-options --base-token <base_token> --table-id <table_id> --field-id "Status" --keyword "Do"`,
		"Use only for select fields, whether multiple is false or true.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "limit", 30, 1, 200)
		return err
	},
	DryRun: dryRunFieldSearchOptions,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldSearchOptions(runtime)
	},
}
View Source
var BaseFieldUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+field-update",
	Description: "Update a field by ID or name",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:field:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		fieldRefFlag(true),
		{Name: "json", Desc: "complete field definition JSON object; update uses full PUT semantics, not a patch", Required: true},
		{Name: "i-have-read-guide", Type: "bool", Desc: "acknowledge reading formula/lookup guide before creating or updating those field types", Hidden: true},
	},
	Tips: []string{
		baseHighRiskYesTip,
		`Example text: lark-cli base +field-update --base-token <base_token> --table-id <table_id> --field-id "Status" --json '{"name":"Status","type":"text"}' --yes`,
		`Example select: lark-cli base +field-update --base-token <base_token> --table-id <table_id> --field-id "Status" --json '{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}' --yes`,
		`Example auto_number update: lark-cli base +field-update --base-token <base_token> --table-id <table_id> --field-id "编号" --json '{"name":"编号","type":"auto_number","style":{"rules":[{"type":"text","text":"TASK-"},{"type":"created_time","date_format":"yyyyMM"},{"type":"text","text":"-"},{"type":"incremental_number","length":4}]}}' --yes`,
		"Update uses full field-definition PUT semantics. Read the current field first with +field-get, then send the target state.",
		`When --json.type is "auto_number", updating the numbering rules also reapplies them to existing numbers; just submit the target field definition and do not add extra low-level parameters.`,
		"Type conversion is allowlist-based: only use CLI for safe conversions; otherwise migrate through a new field, or ask the user to finish high-risk conversions in the web UI.",
		"Formula and lookup updates require reading the corresponding guide first.",
		"Agent hint: use the lark-base skill's field-update guide for JSON shape, type-conversion rules, and limits.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateFieldUpdate(runtime)
	},
	DryRun: dryRunFieldUpdate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFieldUpdate(runtime)
	},
}
View Source
var BaseFormCreate = common.Shortcut{
	Service:     "base",
	Command:     "+form-create",
	Description: "Create a form in a Base table",
	Risk:        "write",
	Scopes:      []string{"base:form:create"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "name", Desc: "form name", Required: true},
		{Name: "description", Desc: `form description (plain text or markdown link like [text](https://example.com))`},
	},
	Tips: []string{
		"Record the returned form_id; form question create/list/update/delete commands need it.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		name := runtime.Str("name")
		description := runtime.Str("description")

		body := map[string]interface{}{"name": name}
		if description != "" {
			body["description"] = description
		}

		data, err := baseV3Call(runtime, "POST",
			baseV3Path("bases", baseToken, "tables", tableId, "forms"), nil, body)
		if err != nil {
			return err
		}

		runtime.OutFormat(data, nil, func(w io.Writer) {
			output.PrintTable(w, []map[string]interface{}{
				{
					"id":          data["id"],
					"name":        data["name"],
					"description": data["description"],
				},
			})
		})
		return nil
	},
}
View Source
var BaseFormDelete = common.Shortcut{
	Service:     "base",
	Command:     "+form-delete",
	Description: "Delete a form in a Base table",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:form:delete"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
	},
	Tips: []string{
		"Use +form-list or +form-get first when the form target is ambiguous.",
		baseHighRiskYesTip,
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")

		_, err := baseV3Call(runtime, "DELETE",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId), nil, nil)
		if err != nil {
			return err
		}

		runtime.Out(map[string]interface{}{"deleted": true, "form_id": formId}, nil)
		return nil
	},
}
View Source
var BaseFormDetail = common.Shortcut{
	Service:     "base",
	Command:     "+form-detail",
	Description: "Get form detail by share token",
	Risk:        "read",
	Scopes:      []string{"base:form:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "share-token", Desc: "Form share token (share_token)", Required: true},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/tables/forms/detail").
			Body(map[string]interface{}{
				"share_token": runtime.Str("share-token"),
			})
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		body := map[string]interface{}{
			"share_token": runtime.Str("share-token"),
		}

		data, err := baseV3Call(runtime, "POST",
			baseV3Path("bases", "tables", "forms", "detail"), nil, body)
		if err != nil {
			return err
		}

		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseFormGet = common.Shortcut{
	Service:     "base",
	Command:     "+form-get",
	Description: "Get a form in a Base table",
	Risk:        "read",
	Scopes:      []string{"base:form:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")

		data, err := baseV3Call(runtime, "GET",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId), nil, nil)
		if err != nil {
			return err
		}

		runtime.OutFormat(data, nil, func(w io.Writer) {
			output.PrintTable(w, []map[string]interface{}{
				{
					"id":          data["id"],
					"name":        data["name"],
					"description": data["description"],
				},
			})
		})
		return nil
	},
}
View Source
var BaseFormQuestionsCreate = common.Shortcut{
	Service:     "base",
	Command:     "+form-questions-create",
	Description: "Create questions for a form in a Base table",
	Risk:        "write",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
		{Name: "questions", Desc: `questions JSON array, max 10 items. Supports two shapes: create a new field question with "title"(field title) and "type"(text/number/select/datetime/user/attachment/location), or add an existing field as a question with "use_existing_field":true and "field_id"(field ID/name). Optional form fields: "description"(plain text or markdown link like [text](https://example.com)),"required","option_display_mode"(0=dropdown/1=vertical/2=horizontal,select only),"visible_rule"(display condition; same shape as view filter {"logic":"and","conditions":[["前序题目","==","是"]]}, field references another question's title/id, empty/absent = always shown). New field questions also support "multiple","options","style". E.g. '[{"type":"text","title":"Your name","required":true}]' or '[{"use_existing_field":true,"field_id":"fldEmail","title":"Email"}]'`, Required: true},
	},
	Tips: []string{
		"If the form may already contain questions and has not been checked, run +form-questions-list for the same --base-token, --table-id, and --form-id. A verified empty form can create directly.",
		"New field questions create fields in the form's table; question IDs are field IDs. Use use_existing_field=true with field_id to add an existing field without creating another field.",
		"Unless the user explicitly requests a separate same-title question, update an existing title with +form-questions-update instead of creating a duplicate.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := parseFormQuestionsCreate(runtime.Str("questions"))
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		questions, _ := parseFormQuestionsCreate(runtime.Str("questions"))
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id")).
			Body(map[string]interface{}{"questions": questions})
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")
		questionsJSON := runtime.Str("questions")

		questions, err := parseFormQuestionsCreate(questionsJSON)
		if err != nil {
			return err
		}

		data, err := baseV3Call(runtime, "POST",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId, "questions"),
			nil, map[string]interface{}{"questions": questions})
		if err != nil {
			return err
		}

		items, _ := data["questions"].([]interface{})
		outData := map[string]interface{}{"questions": items}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			var rows []map[string]interface{}
			for _, item := range items {
				m, _ := item.(map[string]interface{})
				rows = append(rows, map[string]interface{}{
					"id":       m["id"],
					"title":    m["title"],
					"required": m["required"],
				})
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%d question(s) created\n", len(items))
		})
		return nil
	},
}
View Source
var BaseFormQuestionsDelete = common.Shortcut{
	Service:     "base",
	Command:     "+form-questions-delete",
	Description: "Delete questions from a form in a Base table",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
		{Name: "question-ids", Desc: `JSON array of question IDs (field IDs) to remove from the form, max 10 items. Default behavior also deletes the underlying fields and their record data; add --keep-field to preserve fields. E.g. '["q_001","q_002"]'`, Required: true},
		{Name: "keep-field", Type: "bool", Desc: "Only remove/hide the questions from the form; keep the underlying fields and existing record data so they can be added back later with +form-questions-create using use_existing_field=true and field_id. Default false deletes fields and data."},
	},
	Tips: []string{
		"Run +form-questions-list first and use returned question IDs; question IDs are field IDs.",
		"Default behavior is destructive: it deletes the underlying fields and all record data in those fields.",
		"Use --keep-field when you only want to remove questions from the form while preserving fields and data; those fields can be added back with +form-questions-create using use_existing_field=true and field_id.",
		baseHighRiskYesTip,
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, _, err := buildFormQuestionsDeleteBody(runtime)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body, _, err := buildFormQuestionsDeleteBody(runtime)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		return common.NewDryRunAPI().
			DELETE("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id")).
			Body(body)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")

		body, questionIds, err := buildFormQuestionsDeleteBody(runtime)
		if err != nil {
			return err
		}

		_, err = baseV3Call(runtime, "DELETE",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId, "questions"),
			nil, body)
		if err != nil {
			return err
		}

		runtime.Out(map[string]interface{}{
			"deleted":      true,
			"question_ids": questionIds,
			"keep_field":   runtime.Bool("keep-field"),
		}, nil)
		return nil
	},
}
View Source
var BaseFormQuestionsList = common.Shortcut{
	Service:     "base",
	Command:     "+form-questions-list",
	Description: "List questions of a form in a Base table",
	Risk:        "read",
	Scopes:      []string{"base:form:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
	},
	Tips: []string{
		"Use returned question id values for +form-questions-update and +form-questions-delete.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")

		data, err := baseV3Call(runtime, "GET",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId, "questions"), nil, nil)
		if err != nil {
			return err
		}

		items, _ := data["questions"].([]interface{})
		outData := map[string]interface{}{
			"questions": items,
			"total":     data["total"],
		}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(items) == 0 {
				fmt.Fprintln(w, "No questions found.")
				return
			}
			var rows []map[string]interface{}
			for _, item := range items {
				m, _ := item.(map[string]interface{})
				rows = append(rows, map[string]interface{}{
					"id":          m["id"],
					"title":       m["title"],
					"description": m["description"],
					"required":    m["required"],
				})
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%v question(s) total\n", data["total"])
		})
		return nil
	},
}
View Source
var BaseFormQuestionsUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+form-questions-update",
	Description: "Update questions of a form in a Base table",
	Risk:        "write",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
		{Name: "questions", Desc: `questions JSON array, max 10 items, each item must include "id". Update uses full question overwrite semantics: omitted/empty fields are written as defaults/empty, so run +form-questions-list first and include existing values you want to keep. Supported fields: "id"(required),"title","description"(plain text or markdown link like [text](https://example.com)),"required","option_display_mode"(0=dropdown,1=vertical,2=horizontal,select only),"visible_rule"(display condition; same shape as view filter {"logic":"and","conditions":[["前序题目","==","是"]]}, field references another question's title/id; pass null or omit to clear). E.g. '[{"id":"q_001","title":"Updated?","required":true}]'`, Required: true},
	},
	Tips: []string{
		"Update uses full question overwrite semantics, not a patch.",
		"Run +form-questions-list first and include existing title/description/required/option_display_mode/visible_rule values you want to keep.",
		"Omitted fields reset to defaults; empty strings, null, and empty arrays are written as empty/clear when accepted by the API.",
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		api := common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/questions").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
		// Transcribe the questions body verbatim so the preview shows exactly
		// what would be sent (including optional fields like visible_rule).
		var questions []interface{}
		if err := json.Unmarshal([]byte(runtime.Str("questions")), &questions); err == nil {
			api.Body(map[string]interface{}{"questions": questions})
		}
		return api
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")
		questionsJSON := runtime.Str("questions")

		var questions []interface{}
		if err := json.Unmarshal([]byte(questionsJSON), &questions); err != nil {
			return baseValidationErrorf("--questions must be a valid JSON array: %s", err)
		}

		data, err := baseV3Call(runtime, "PATCH",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId, "questions"),
			nil, map[string]interface{}{"questions": questions})
		if err != nil {
			return err
		}

		items, _ := data["items"].([]interface{})
		if len(items) == 0 {
			items, _ = data["questions"].([]interface{})
		}
		outData := map[string]interface{}{"questions": items}

		runtime.OutFormat(outData, nil, func(w io.Writer) {
			var rows []map[string]interface{}
			for _, item := range items {
				m, _ := item.(map[string]interface{})
				rows = append(rows, map[string]interface{}{
					"id":       m["id"],
					"title":    m["title"],
					"required": m["required"],
				})
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%d question(s) updated\n", len(items))
		})
		return nil
	},
}
View Source
var BaseFormShareGet = common.Shortcut{
	Service:     "base",
	Command:     "+form-share-get",
	Description: "Get form share status and settings",
	Risk:        "read",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
	},
	DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/share").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "GET", baseV3Path(
			"bases", runtime.Str("base-token"), "tables", runtime.Str("table-id"), "forms", runtime.Str("form-id"), "share",
		), nil, nil)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseFormShareUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+form-share-update",
	Description: "Update form share status and settings",
	Risk:        "write",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
		{Name: "enabled", Type: "bool", Desc: "enable or disable form sharing"},
		{Name: "access-scope", Desc: "share access scope", Enum: shareAccessScopeEnums},
		{Name: "allow-anonymous", Type: "bool", Desc: "anonymize the submitter identity"},
		{Name: "require-login", Type: "bool", Desc: "require submitters to sign in before submitting"},
	},
	Tips: []string{
		"Boolean settings use PATCH semantics: pass --allow-anonymous=false or another boolean flag with =false to explicitly turn it off.",
		"--allow-anonymous controls submitter identity and --require-login controls sign-in; run separate update commands to change both settings.",
		"Update exactly one field per invocation; run separate commands to change multiple share fields.",
	},
	Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
		return validateFormShareUpdate(runtime)
	},
	DryRun: func(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id/share").
			Body(buildFormShareUpdateBody(runtime)).
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(_ context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "PATCH", baseV3Path(
			"bases", runtime.Str("base-token"), "tables", runtime.Str("table-id"), "forms", runtime.Str("form-id"), "share",
		), nil, buildFormShareUpdateBody(runtime))
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseFormSubmit = common.Shortcut{
	Service:     "base",
	Command:     "+form-submit",
	Description: "Submit a form (fill and submit form data)",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:form:update", "docs:document.media:upload"},
	AuthTypes:   authTypes(),
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "share-token", Desc: "Form share token (required), extracted from the form share link", Required: true},
		{Name: "base-token", Desc: "Base token (required when --json contains attachments, used for uploading attachments to Base Drive Media)"},
		{Name: "json", Desc: `JSON object containing "fields" (field values) and "attachments" (attachment file paths). Example: '{"fields":{"Rating":5,"Review":"Good"},"attachments":{"Attachment":["./a.pdf","./b.png"]}}'`, Required: true},
	},
	Tips: []string{
		`Example (no attachments): --share-token shrXXXX --json '{"fields":{"Service Rating":5,"Review":"Good service"}}'`,
		`Example (with attachments): --share-token shrXXXX --base-token basXXX --json '{"fields":{"Service Rating":5},"attachments":{"Attachment":["./report.pdf"]}}'`,
		`Cell values in "fields" follow lark-base-cell-value.md conventions; "attachments" maps field names to local file path arrays — the CLI uploads them in parallel and merges them into the submission.`,
		baseHighRiskYesTip,
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateFormSubmit(runtime)
	},
	DryRun: dryRunFormSubmit,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeFormSubmit(runtime)
	},
}
View Source
var BaseFormUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+form-update",
	Description: "Update a form in a Base table",
	Risk:        "write",
	Scopes:      []string{"base:form:update"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "form-id", Desc: "form ID", Required: true},
		{Name: "name", Desc: "new form name"},
		{Name: "description", Desc: "new form description (plain text or markdown link like [text](https://example.com))"},
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms/:form_id").
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id")).
			Set("form_id", runtime.Str("form-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")
		formId := runtime.Str("form-id")
		name := runtime.Str("name")
		description := runtime.Str("description")

		body := map[string]interface{}{}
		if name != "" {
			body["name"] = name
		}
		if description != "" {
			body["description"] = description
		}

		data, err := baseV3Call(runtime, "PATCH",
			baseV3Path("bases", baseToken, "tables", tableId, "forms", formId), nil, body)
		if err != nil {
			return err
		}

		runtime.OutFormat(data, nil, func(w io.Writer) {
			output.PrintTable(w, []map[string]interface{}{
				{
					"id":          data["id"],
					"name":        data["name"],
					"description": data["description"],
				},
			})
		})
		return nil
	},
}
View Source
var BaseFormsList = common.Shortcut{
	Service:     "base",
	Command:     "+form-list",
	Description: "List all forms in a Base table (auto-paginated)",
	Risk:        "read",
	Scopes:      []string{"base:form:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "Base token (base_token)", Required: true},
		{Name: "table-id", Desc: "table ID", Required: true},
		{Name: "page-size", Type: "int", Default: "100", Desc: "page size per request, range 1-100"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 100, 1, 100)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/tables/:table_id/forms").
			Params(map[string]interface{}{"page_size": runtime.Int("page-size")}).
			Set("base_token", runtime.Str("base-token")).
			Set("table_id", runtime.Str("table-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		tableId := runtime.Str("table-id")

		var allForms []interface{}
		pageToken := ""
		for {
			params := map[string]interface{}{
				"page_size": runtime.Int("page-size"),
			}
			if pageToken != "" {
				params["page_token"] = pageToken
			}

			data, err := baseV3Call(runtime, "GET",
				baseV3Path("bases", baseToken, "tables", tableId, "forms"), params, nil)
			if err != nil {
				return err
			}

			forms, _ := data["forms"].([]interface{})
			allForms = append(allForms, forms...)

			hasMore, _ := data["has_more"].(bool)
			if !hasMore {
				break
			}
			nextToken, _ := data["page_token"].(string)
			if nextToken == "" {
				break
			}
			pageToken = nextToken
		}

		outData := map[string]interface{}{
			"forms": allForms,
			"total": len(allForms),
		}
		runtime.OutFormat(outData, nil, func(w io.Writer) {
			if len(allForms) == 0 {
				fmt.Fprintln(w, "No forms found.")
				return
			}
			var rows []map[string]interface{}
			for _, item := range allForms {
				m, _ := item.(map[string]interface{})
				rows = append(rows, map[string]interface{}{
					"id":          m["id"],
					"name":        m["name"],
					"description": m["description"],
				})
			}
			output.PrintTable(w, rows)
			fmt.Fprintf(w, "\n%d form(s) total\n", len(allForms))
		})
		return nil
	},
}
View Source
var BaseRecordBatchCreate = common.Shortcut{
	Service:     "base",
	Command:     "+record-batch-create",
	Description: "Batch create records",
	Risk:        "write",
	Scopes:      []string{"base:record:create"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "json", Desc: `batch create JSON object; create_records contains one field map per record, e.g. {"create_records":[{"Name":"Task A","Status":["Todo"]},{"Name":"Task B","Score":20}]}`, Required: true},
	},
	Tips: append([]string{
		"Happy path field: create_records is an array of independent record field maps.",
		`Example: {"create_records":[{"Name":"Task A","Status":["Todo"]},{"Name":"Task B","Score":20}]}.`,
		"Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.",
		"Batch create supports max 200 records per call.",
		"After batch-creating known helper rows, use the returned record IDs and your submitted rows; do not immediately +record-list the same table unless you need server-normalized formula/lookup values or failure diagnosis.",
		"Use the record-batch-create guide for command limits and edge cases.",
	}, recordCellValueHappyPathTips...),
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordJSON(runtime)
	},
	DryRun: dryRunRecordBatchCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordBatchCreate(runtime)
	},
}
View Source
var BaseRecordBatchUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+record-batch-update",
	Description: "Batch update records with record-specific fields",
	Risk:        "write",
	Scopes:      []string{"base:record:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "json", Desc: `batch update JSON object; update_records maps each record ID to its field map, e.g. {"update_records":{"recA":{"Status":["Done"]},"recB":{"Score":20}}}`, Required: true},
	},
	Tips: append([]string{
		"Happy path field: update_records maps each record ID to its own field map.",
		`Example: {"update_records":{"recA":{"Status":["Done"]},"recB":{"Score":20}}}.`,
		"The response contains only optional ignored_fields and does not check whether record IDs exist; read records back when confirmation is required.",
		"Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.",
		"Batch update supports max 200 records per call; use the record-batch-update guide for command limits and edge cases.",
	}, recordCellValueHappyPathTips...),
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordJSON(runtime)
	},
	DryRun: dryRunRecordBatchUpdate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordBatchUpdate(runtime)
	},
}
View Source
var BaseRecordDelete = common.Shortcut{
	Service:     "base",
	Command:     "+record-delete",
	Description: "Delete one or more records by ID",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:record:delete"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "record-id", Type: "string_array", Desc: "record ID (repeatable)"},
		{Name: "json", Desc: `JSON object with record_id_list, e.g. {"record_id_list":["rec_xxx"]}`},
	},
	Tips: []string{
		baseHighRiskYesTip,
		`Example: lark-cli base +record-delete --base-token <base_token> --table-id <table_id> --record-id <record_id_1> --record-id <record_id_2> --yes`,
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordSelection(runtime)
	},
	DryRun: dryRunRecordDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordDelete(runtime)
	},
}
View Source
var BaseRecordDownloadAttachment = common.Shortcut{
	Service:     "base",
	Command:     "+record-download-attachment",
	Description: "Download Base record attachments by record-id, optionally filtering by file-token",
	Risk:        "read",
	Scopes:      []string{"base:record:read", "docs:document.media:download"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordRefFlag(true),
		{Name: "file-token", Type: "string_array", Desc: "attachment file_token returned by Base; repeat to download selected files; omit to download all attachments in the record", Required: false},
		{Name: "output", Desc: "local save path; with exactly one file token this may be a file path; with multiple or omitted file tokens this must be an existing directory", Required: true},
		{Name: "overwrite", Type: "bool", Desc: "overwrite existing output file"},
	},
	Tips: []string{
		`Example: lark-cli base +record-download-attachment --base-token <base_token> --table-id <table_id> --record-id <record_id> --file-token <file_token> --output ./downloads/`,
		`Omit --file-token to download every attachment in the record.`,
		`Base attachments should be downloaded with this command; other download commands may fail for Base attachment files.`,
		`With one --file-token, --output may be a file path or directory; with multiple or omitted --file-token values, --output must be an existing directory.`,
	},
	DryRun: dryRunRecordDownloadAttachment,
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordDownloadAttachment(runtime)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordDownloadAttachment(ctx, runtime)
	},
}
View Source
var BaseRecordGet = common.Shortcut{
	Service:     "base",
	Command:     "+record-get",
	Description: "Get one or more records by ID",
	Risk:        "read",
	Scopes:      []string{"base:record:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "record-id", Type: "string_array", Desc: "record ID (repeatable)"},
		recordProjectionFieldFlag("field ID or name to project; repeat to keep only needed columns"),
		recordProjectionAliasFlag("fields"),
		recordProjectionAliasFlag("field-names"),
		{Name: "json", Desc: `JSON object with record_id_list, e.g. {"record_id_list":["rec_xxx"]}`},
		recordReadFormatFlag(),
		recordOutputFlag(),
		recordMinimalStdoutFlag(),
		recordJQRecordsFlag(),
		recordOverwriteFlag(),
	},
	Normalize: normalizeRecordReadOutput,
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if err := validateRecordReadFormat(runtime); err != nil {
			return err
		}
		if err := validateRecordExportFlags(runtime); err != nil {
			return err
		}
		return validateRecordSelection(runtime)
	},
	Tips: []string{
		"Example: lark-cli base +record-get --base-token <base_token> --table-id <table_id> --record-id <record_id>",
		"Example with projection: lark-cli base +record-get --base-token <base_token> --table-id <table_id> --record-id rec_001 --record-id rec_002 --field-id Name --field-id Status",
		"Example for analysis input: lark-cli base +record-get --base-token <base_token> --table-id <table_id> --record-id <record_id> --field-id <field> --format ndjson --output ./record.ndjson",
		recordAnalysisOutputTip,
		"Use --field-id as a projection boundary to avoid loading large cell values into context when they are not needed.",
		"Use +record-get when record_id is already known; otherwise use +record-search or +record-list.",
	},
	DryRun: dryRunRecordGet,
	PostMount: func(cmd *cobra.Command) {
		preserveFlagOrder(cmd)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordGet(runtime)
	},
}
View Source
var BaseRecordHistoryList = common.Shortcut{
	Service:     "base",
	Command:     "+record-history-list",
	Description: "List record change history",
	Risk:        "read",
	Scopes:      []string{"base:history:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordRefFlag(true),
		{Name: "max-version", Type: "int", Desc: "max version for next page"},
		{Name: "page-size", Type: "int", Default: "30", Desc: "pagination size, range 1-50"},
	},
	Tips: []string{
		`Example: lark-cli base +record-history-list --base-token <base_token> --table-id <table_id> --record-id <record_id>`,
		"This reads one record's history only; it is not a table-wide audit scan.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if _, err := common.ValidatePageSizeTyped(runtime, "page-size", 30, 1, 50); err != nil {
			return err
		}
		if runtime.Changed("max-version") && runtime.Int("max-version") <= 0 {
			return errs.NewValidationError(
				errs.SubtypeInvalidArgument,
				"--max-version must be greater than 0",
			).WithParam("--max-version")
		}
		return nil
	},
	DryRun: dryRunRecordHistoryList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		params := map[string]interface{}{
			"table_id":  baseTableID(runtime),
			"record_id": runtime.Str("record-id"),
			"page_size": runtime.Int("page-size"),
		}
		if value := runtime.Int("max-version"); value > 0 {
			params["max_version"] = value
		}
		data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "record_history"), params, nil)
		if err != nil {
			return err
		}
		var pretty string
		if runtime.Format == "pretty" && runtime.JqExpr == "" {
			pretty, err = formatRecordHistoryPretty(data, time.Local)
			if err != nil {
				return err
			}
		}
		runtime.OutFormat(data, nil, func(w io.Writer) {
			_, _ = io.WriteString(w, pretty)
		})
		return nil
	},
}
View Source
var BaseRecordList = common.Shortcut{
	Service:     "base",
	Command:     "+record-list",
	Description: "List records in a table",
	Risk:        "read",
	Scopes:      []string{"base:record:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordProjectionFieldFlag("field ID or name to include; repeat to project only needed fields"),
		recordProjectionAliasFlag("fields"),
		recordProjectionAliasFlag("field-names"),
		recordListViewRefFlag(),
		recordFilterFlag(),
		recordSortFlag(),
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Default: "100", Desc: "maximum records to return; range 1-200, or 1-2000 for ndjson; omitted limit uses 2000 for ndjson"},
		recordReadFormatFlag(),
		recordOutputFlag(),
		recordMinimalStdoutFlag(),
		recordJQRecordsFlag(),
		recordOverwriteFlag(),
	},
	Tips: []string{
		"Example: lark-cli base +record-list --base-token <base_token> --table-id <table_id> --limit 50",
		"Example with projection: lark-cli base +record-list --base-token <base_token> --table-id <table_id> --field-id Name --field-id Status --limit 50",
		"Example for analysis: lark-cli base +record-list --base-token <base_token> --table-id <table_id> --field-id Name --field-id Status --format ndjson --output ./records.ndjson",
		`Text equality filter: --filter-json '{"logic":"and","conditions":[["Title","==","Launch plan"]]}'`,
		`Text contains/like filter: --filter-json '{"logic":"and","conditions":[["Title","intersects","urgent"]]}'`,
		`Number equality filter: --filter-json '{"logic":"and","conditions":[["Score","==",95]]}'`,
		`Date equality filter: --filter-json '{"logic":"and","conditions":[["Due Date","==","ExactDate(2026-06-02)"]]}'`,
		`Option intersection filter: --filter-json '{"logic":"and","conditions":[["Tags","intersects",["P0","Blocked"]]]}'`,
		`Sort priority follows --sort-json array order: --sort-json '[{"field":"Updated","desc":true},{"field":"Title","desc":false}]'`,
		formatRecordQueryPriorityTip(),
		recordAnalysisOutputTip,
		"Use --field-id repeatedly to keep output small and aligned with the task.",
	},
	Normalize: common.ChainNormalizers(normalizeRecordReadOutput, normalizeRecordNDJSONLimit),
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if err := validateRecordReadFormat(runtime); err != nil {
			return err
		}
		if err := validateRecordExportFlags(runtime); err != nil {
			return err
		}
		if err := validateRecordReadLimit(runtime, 100); err != nil {
			return err
		}
		if _, err := recordProjectionFields(runtime); err != nil {
			return err
		}
		return validateRecordQueryOptions(runtime)
	},
	DryRun: dryRunRecordList,
	PostMount: func(cmd *cobra.Command) {
		preserveFlagOrder(cmd)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordList(runtime)
	},
}
View Source
var BaseRecordRemoveAttachment = common.Shortcut{
	Service:     "base",
	Command:     "+record-remove-attachment",
	Description: "Remove one or more file_token values from a Base record attachment cell",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:record:update", "base:field:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordRefFlag(true),
		fieldRefFlag(true),
		{Name: "file-token", Type: "string_array", Desc: "attachment file_token to remove from the target cell; repeat to remove multiple attachments; max 50 tokens", Required: true},
	},
	Tips: []string{
		baseHighRiskYesTip,
		`Example: lark-cli base +record-remove-attachment --base-token <base_token> --table-id <table_id> --record-id <record_id> --field-id <attachment_field_id> --file-token <file_token> --yes`,
		`Repeat --file-token to remove multiple attachments from the same cell in one call.`,
		`This is a high-risk write command and requires --yes.`,
	},
	DryRun: dryRunRecordRemoveAttachment,
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordRemoveAttachment(runtime)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordRemoveAttachment(runtime)
	},
}
View Source
var BaseRecordSearch = common.Shortcut{
	Service:     "base",
	Command:     "+record-search",
	Description: "Search records in a table",
	Risk:        "read",
	Scopes:      []string{"base:record:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "json", Desc: `record search JSON object for the full request body, e.g. {"keyword":"Alice","search_fields":["Name"],"select_fields":["Name","Status"],"filter":{"logic":"and","conditions":[]},"sort":[{"field":"Updated","desc":true}],"limit":50}; escape hatch for advanced cases`},
		{Name: "keyword", Desc: "keyword for record search; required unless --json is used"},
		{Name: "search-field", Type: "string_array", Desc: "field ID or name to search; repeat for multiple fields; required unless --json is used"},
		recordProjectionFieldFlag("field ID or name to include; repeat to project only needed fields"),
		recordProjectionAliasFlag("fields"),
		recordProjectionAliasFlag("field-names"),
		recordListViewRefFlag(),
		recordFilterFlag(),
		recordSortFlag(),
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Desc: "maximum records to return; range 1-200, or 1-2000 for ndjson; omitted limit uses 10 inline or 2000 for ndjson"},
		recordReadFormatFlag(),
		recordOutputFlag(),
		recordMinimalStdoutFlag(),
		recordJQRecordsFlag(),
		recordOverwriteFlag(),
	},
	Tips: []string{
		`Happy path fields: keyword (string), search_fields (1-20 field names/ids), select_fields (optional projection, <=50), view_id (optional), offset (default 0), limit (default 10 inline or 2000 for ndjson; inline range 1-200, ndjson range 1-2000).`,
		"JSON constraints: keyword length >=1; search_fields length 1-20; select_fields length <=50; offset >=0 defaults to 0; omitted limit uses 10 inline or 2000 for ndjson.",
		"view_id scopes search to records in that view; when select_fields is omitted, returned fields follow that view's visible fields.",
		`Example: lark-cli base +record-search --base-token <base_token> --table-id <table_id> --keyword Alice --search-field Name --field-id Name --field-id Status --limit 20`,
		`Example with filter/sort JSON: lark-cli base +record-search --base-token <base_token> --table-id <table_id> --keyword Alice --search-field Name --filter-json @filter.json --sort-json '[{"field":"Updated","desc":true}]'`,
		`Example for analysis: lark-cli base +record-search --base-token <base_token> --table-id <table_id> --keyword Alice --search-field Name --field-id Name --field-id Status --format ndjson --output ./records.ndjson`,
		`Text equality filter: --filter-json '{"logic":"and","conditions":[["Title","==","Launch plan"]]}'`,
		`Text contains/like filter: --filter-json '{"logic":"and","conditions":[["Title","intersects","urgent"]]}'`,
		`Option intersection filter: --filter-json '{"logic":"and","conditions":[["Tags","intersects",["P0","Blocked"]]]}'`,
		`Sort priority follows --sort-json array order.`,
		formatRecordQueryPriorityTip(),
		"Use +record-search for keyword matching; use --filter-json for structured conditions and --sort-json for result ordering.",
		"Use --json only when you need to pass the full search body directly.",
		recordAnalysisOutputTip,
	},
	Normalize: normalizeRecordSearchOutput,
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordSearchFlags(runtime)
	},
	DryRun: dryRunRecordSearch,
	PostMount: func(cmd *cobra.Command) {
		preserveFlagOrder(cmd)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordSearch(runtime)
	},
}
View Source
var BaseRecordShareLinkCreate = common.Shortcut{
	Service:     "base",
	Command:     "+record-share-link-create",
	Description: "Generate share links for one or more records (max 100 per request)",
	Risk:        "read",
	Scopes:      []string{"base:record:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "record-ids", Type: "string_slice", Desc: "record IDs to generate share links for (comma-separated or repeatable, max 100)", Required: true},
	},
	Tips: []string{
		`Example: lark-cli base +record-share-link-create --base-token <base_token> --table-id <table_id> --record-ids <record_id>`,
		"Max 100 record IDs per call; duplicate IDs are ignored.",
		"Output record_share_links maps record_id to URL; records without permission or missing records may be absent.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordShareBatch(runtime)
	},
	DryRun: dryRunRecordShareBatch,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordShareBatch(runtime)
	},
}
View Source
var BaseRecordUploadAttachment = common.Shortcut{
	Service:     "base",
	Command:     "+record-upload-attachment",
	Description: "Upload one or more local files and append the returned file_token values to a Base attachment cell",
	Risk:        "write",
	Scopes:      []string{"base:record:update", "base:field:read", "docs:document.media:upload"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordRefFlag(true),
		fieldRefFlag(true),
		{Name: "file", Type: "string_array", Desc: "local file path; repeat to append multiple attachments in one cell; max 50 files, max 2GB each; files > 20MB use multipart upload automatically", Required: true},
		{Name: "name", Desc: "deprecated; attachment names are derived from local file basenames", Hidden: true},
	},
	Tips: []string{
		`Example: lark-cli base +record-upload-attachment --base-token <base_token> --table-id <table_id> --record-id <record_id> --field-id <attachment_field_id> --file ./report.pdf`,
		`Repeat --file to append multiple attachments: --file ./report.pdf --file ./screenshot.png`,
		`Reuse returned file_token values for download/remove`,
	},
	DryRun: dryRunRecordUploadAttachment,
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordUploadAttachment(runtime)
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordUploadAttachment(runtime)
	},
}
View Source
var BaseRecordUpsert = common.Shortcut{
	Service:     "base",
	Command:     "+record-upsert",
	Description: "Create or update a record",
	Risk:        "write",
	Scopes:      []string{"base:record:create", "base:record:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		recordRefFlag(false),
		{Name: "json", Desc: `record field map JSON object, e.g. {"Name":"Alice","Status":["Todo"]}; do not wrap in fields`, Required: true},
	},
	Tips: append([]string{
		"Happy path JSON is a top-level field map: each key is a real field name or field ID, each value is that field's CellValue.",
		"Without --record-id this creates a record; with --record-id this updates that record. It does not auto-upsert by business key.",
		"Before writing, use +field-list to confirm real writable fields; do not write system fields, formula, lookup, or attachment fields as normal CellValue.",
		"Sub-record/child-record path: when a one-way/two-way link field represents hierarchy, create a normal record and set that link field to a parent record reference array, e.g. {\"Parent Link\":[{\"id\":\"rec_xxx\"}]}; do not look for parent_record_id or a separate child-record API.",
	}, recordCellValueHappyPathTips...),
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateRecordJSON(runtime)
	},
	DryRun: dryRunRecordUpsert,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeRecordUpsert(runtime)
	},
}
View Source
var BaseRoleCreate = common.Shortcut{
	Service:     "base",
	Command:     "+role-create",
	Description: "Create a custom role in a Base",
	Risk:        "write",
	Scopes:      []string{"base:role:create"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "json", Desc: "role config JSON; read lark-base-advanced-permission-and-role.md and lark-base-role-config.md before constructing permissions", Required: true},
	},
	Tips: []string{
		"Requires advanced permissions to be enabled and the caller to be a Base admin.",
		"Use lark-base-advanced-permission-and-role.md as the module entry and lark-base-role-config.md as the role permission schema.",
		"Create supports custom_role only.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		var body map[string]any
		if err := json.Unmarshal([]byte(runtime.Str("json")), &body); err != nil {
			return baseFlagErrorf("--json must be valid JSON: %v", err)
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		var body map[string]any
		json.Unmarshal([]byte(runtime.Str("json")), &body)
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/roles").
			Body(body).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		var body map[string]any
		json.Unmarshal([]byte(runtime.Str("json")), &body)

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodPost,
			ApiPath:    fmt.Sprintf("/open-apis/base/v3/bases/%s/roles", validate.EncodePathSegment(baseToken)),
			Body:       body,
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "create role failed")
	},
}
View Source
var BaseRoleDelete = common.Shortcut{
	Service:     "base",
	Command:     "+role-delete",
	Description: "Delete a custom role (system roles cannot be deleted)",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:role:delete"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true},
	},
	Tips: []string{
		baseHighRiskYesTip,
		"Requires advanced permissions to be enabled and the caller to be a Base admin.",
		"Only custom roles can be deleted; system roles cannot be deleted.",
		"Use +role-get first if the role target is ambiguous, then pass --yes to confirm deletion.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("role-id")) == "" {
			return baseFlagErrorf("--role-id must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			DELETE("/open-apis/base/v3/bases/:base_token/roles/:role_id").
			Set("base_token", runtime.Str("base-token")).
			Set("role_id", runtime.Str("role-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		roleId := runtime.Str("role-id")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodDelete,
			ApiPath:    fmt.Sprintf("/open-apis/base/v3/bases/%s/roles/%s", validate.EncodePathSegment(baseToken), validate.EncodePathSegment(roleId)),
			Body:       map[string]any{},
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "delete role failed")
	},
}
View Source
var BaseRoleGet = common.Shortcut{
	Service:     "base",
	Command:     "+role-get",
	Description: "Get full config of a role",
	Risk:        "read",
	Scopes:      []string{"base:role:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true},
	},
	Tips: []string{
		"Requires advanced permissions to be enabled and the caller to be a Base admin.",
		"Use before +role-update to inspect the current full permission config.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("role-id")) == "" {
			return baseFlagErrorf("--role-id must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/roles/:role_id").
			Set("base_token", runtime.Str("base-token")).
			Set("role_id", runtime.Str("role-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		roleId := runtime.Str("role-id")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodGet,
			ApiPath:    fmt.Sprintf("/open-apis/base/v3/bases/%s/roles/%s", validate.EncodePathSegment(baseToken), validate.EncodePathSegment(roleId)),
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "get role failed")
	},
}
View Source
var BaseRoleList = common.Shortcut{
	Service:     "base",
	Command:     "+role-list",
	Description: "List all roles in a Base",
	Risk:        "read",
	Scopes:      []string{"base:role:read"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
	},
	Tips: []string{
		"Requires advanced permissions to be enabled and the caller to be a Base admin.",
		"Returns role summaries; use +role-get for the full permission config.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/roles").
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodGet,
			ApiPath:    fmt.Sprintf("/open-apis/base/v3/bases/%s/roles", validate.EncodePathSegment(baseToken)),
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "list roles failed")
	},
}
View Source
var BaseRoleUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+role-update",
	Description: "Update a role config (delta merge, only changed fields needed)",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:role:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "role-id", Desc: "role ID (e.g. rolxxxxxx4)", Required: true},
		{Name: "json", Desc: "delta role config JSON; read lark-base-advanced-permission-and-role.md and lark-base-role-config.md before changing permissions", Required: true},
	},
	Tips: []string{
		baseHighRiskYesTip,
		"Requires advanced permissions to be enabled and the caller to be a Base admin.",
		"Update is a delta merge: only changed fields are updated, others remain unchanged.",
		"Use lark-base-advanced-permission-and-role.md as the module entry and lark-base-role-config.md as the role permission schema.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("role-id")) == "" {
			return baseFlagErrorf("--role-id must not be blank")
		}
		var body map[string]any
		if err := json.Unmarshal([]byte(runtime.Str("json")), &body); err != nil {
			return baseFlagErrorf("--json must be valid JSON: %v", err)
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		var body map[string]any
		json.Unmarshal([]byte(runtime.Str("json")), &body)
		return common.NewDryRunAPI().
			Desc("Delta merge: only changed fields are updated, others remain unchanged").
			PUT("/open-apis/base/v3/bases/:base_token/roles/:role_id").
			Body(body).
			Set("base_token", runtime.Str("base-token")).
			Set("role_id", runtime.Str("role-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		baseToken := runtime.Str("base-token")
		roleId := runtime.Str("role-id")
		var body map[string]any
		json.Unmarshal([]byte(runtime.Str("json")), &body)

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: http.MethodPut,
			ApiPath:    fmt.Sprintf("/open-apis/base/v3/bases/%s/roles/%s", validate.EncodePathSegment(baseToken), validate.EncodePathSegment(roleId)),
			Body:       body,
		})
		if err != nil {
			return err
		}

		return handleRoleAPIResponse(runtime, apiResp, "update role failed")
	},
}
View Source
var BaseTableCopy = common.Shortcut{
	Service:     "base",
	Command:     "+table-copy",
	Description: "Copy a table by ID or name; structure only by default",
	Risk:        "write",
	Scopes:      []string{tableCopyScope},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "name", Desc: "target table name", Required: true},
		{Name: "range", Default: tableCopyRangeSchema, Desc: "copy range; defaults to schema, use all only to include records", Enum: []string{tableCopyRangeSchema, tableCopyRangeAll}},
		{Name: "wait", Type: "bool", Desc: "wait for an all-range copy task to finish"},
	},
	Tips: []string{
		`Example: lark-cli base +table-copy --base-token <base_token> --table-id "Tasks" --name "Tasks copy"`,
		"table-id accepts a table ID or name in the current Base.",
		"The default copies schema only; use --range all only when records must also be copied.",
		"Use --wait with --range all to wait locally; otherwise continue with the returned next_command.",
	},
	DryRun: dryRunTableCopy,
	PostMount: func(cmd *cobra.Command) {
		cmd.Flags().Duration("timeout", 5*time.Minute, "maximum time to wait for an asynchronous copy task (max 30m)")
	},
	Validate: validateTableCopy,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableCopy(ctx, runtime)
	},
}
View Source
var BaseTableCopyStatus = common.Shortcut{
	Service:     "base",
	Command:     "+table-copy-status",
	Description: "Get one table copy task status",
	Risk:        "read",
	Scopes:      []string{tableCopyScope},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "task-id", Desc: "opaque table copy task ID", Required: true},
	},
	Tips: []string{
		"Use the opaque task_id returned by base +table-copy; this command queries status once.",
		"If state is init or process, run the returned next_command later.",
	},
	DryRun: dryRunTableCopyStatus,
	Validate: func(_ context.Context, runtime *common.RuntimeContext) error {
		taskID := runtime.Str("task-id")
		if strings.TrimSpace(taskID) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--task-id cannot be blank").WithParam("--task-id")
		}
		if len(taskID) > tableCopyTaskIDMax {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--task-id must not exceed %d bytes", tableCopyTaskIDMax).WithParam("--task-id")
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableCopyStatus(ctx, runtime)
	},
}
View Source
var BaseTableCreate = common.Shortcut{
	Service:     "base",
	Command:     "+table-create",
	Description: "Create a table with an explicit field schema, plus optional views",
	Risk:        "write",
	Scopes:      []string{"base:table:create", "base:field:read", "base:field:create", "base:field:update", "base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "name", Desc: "table name", Required: true},
		{Name: "view", Desc: "view JSON object/array for create"},
		{Name: "fields", Required: true, Desc: `field JSON array defining the table schema; must hold at least one field, e.g. [{"name":"Title","type":"text"},{"name":"Status","type":"select","multiple":false,"options":[{"name":"Todo"},{"name":"Done"}]}]`},
	},
	Tips: []string{
		"Before using --fields, read lark-base-field-schema.md or rely on the same field JSON shape used by +field-create; do not invent field properties.",
		"The first --fields item becomes the primary field.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateTableCreate(runtime)
	},
	DryRun: dryRunTableCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableCreate(runtime)
	},
}

BaseTableCreate creates a table with an explicit schema. --fields is required at the cli surface (cobra MarkFlagRequired); a missing flag fails before Validate runs with cobra's standard "required flag(s)" error (which the dispatcher classifies as a typed *errs.ValidationError). validateTableCreate still rejects blank, non-array and empty-array values, because cobra accepts --fields "" and --fields "[]" — both of which would reach the API without a fields body and get the platform default schema instead of the caller's.

View Source
var BaseTableDelete = common.Shortcut{
	Service:     "base",
	Command:     "+table-delete",
	Description: "Delete a table by ID or name",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:table:delete"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true)},
	Tips: []string{
		`Example: lark-cli base +table-delete --base-token <base_token> --table-id "Old Tasks" --yes`,
		"table-id accepts a table ID (tbl...) or the table name in the current Base.",
		baseHighRiskYesTip,
	},
	DryRun: dryRunTableDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableDelete(runtime)
	},
}
View Source
var BaseTableGet = common.Shortcut{
	Service:     "base",
	Command:     "+table-get",
	Description: "Get a table by ID or name",
	Risk:        "read",
	Scopes:      []string{"base:table:read", "base:field:read", "base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true)},
	Tips: []string{
		`Example: lark-cli base +table-get --base-token <base_token> --table-id "Tasks"`,
		"table-id accepts a table ID (tbl...) or the table name in the current Base.",
	},
	DryRun: dryRunTableGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableGet(runtime)
	},
}
View Source
var BaseTableList = common.Shortcut{
	Service:     "base",
	Command:     "+table-list",
	Description: "List tables in a base",
	Risk:        "read",
	Scopes:      []string{"base:table:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Default: "50", Desc: "pagination size, range 1-100"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "limit", 50, 1, 100)
		return err
	},
	DryRun: dryRunTableList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableList(runtime)
	},
}
View Source
var BaseTableUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+table-update",
	Description: "Rename a table by ID or name",
	Risk:        "write",
	Scopes:      []string{"base:table:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "name", Desc: "new table name", Required: true},
	},
	DryRun: dryRunTableUpdate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTableUpdate(runtime)
	},
}
View Source
var BaseTemplateCategories = common.Shortcut{
	Service:     "base",
	Command:     "+template-categories",
	Description: "List Base template center categories",
	Risk:        "read",
	Scopes:      []string{templateReadScope},
	AuthTypes:   authTypes(),
	Tips: []string{
		"Use this first when the user asks to browse template categories.",
		`Example: lark-cli base +template-categories --as user`,
	},
	DryRun: dryRunTemplateCategories,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTemplateCategories(runtime)
	},
}
View Source
var BaseTemplateList = common.Shortcut{
	Service:     "base",
	Command:     "+template-list",
	Description: "List Base templates by category",
	Risk:        "read",
	Scopes:      []string{templateReadScope},
	AuthTypes:   authTypes(),
	Flags: append([]common.Flag{
		{Name: "category-key", Desc: "template category key; omit to list the recommended category"},
	}, templatePaginationFlags()...),
	Tips: []string{
		"Use --category-key with a key returned by +template-categories; omit it to read the recommended category.",
		"Returned template.token is the Base template token. To create from it, run +base-copy --base-token <token>.",
		`Example: lark-cli base +template-list --category-key office --limit 10 --as user`,
	},
	Validate: validateTemplatePagination,
	DryRun:   dryRunTemplateList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTemplateList(runtime)
	},
}
View Source
var BaseTemplateSearch = common.Shortcut{
	Service:     "base",
	Command:     "+template-search",
	Description: "Search Base templates by keyword",
	Risk:        "read",
	Scopes:      []string{templateReadScope},
	AuthTypes:   authTypes(),
	Flags: append([]common.Flag{
		{Name: "keyword", Required: true, Desc: "template keyword; empty search is not supported"},
	}, templatePaginationFlags()...),
	Tips: []string{
		"Use this when the user wants to create a new Base and has no owned/recent Base anchor.",
		"Do not use drive +search for marketplace templates; drive search only finds user-accessible Drive/Wiki objects.",
		"Returned template.token is the Base template token. To create from it, run +base-copy --base-token <token>.",
		`Example: lark-cli base +template-search --keyword "project management" --limit 10 --as user`,
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("keyword")) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "template keyword must not be blank").WithParam("--keyword")
		}
		return validateTemplatePagination(ctx, runtime)
	},
	DryRun: dryRunTemplateSearch,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeTemplateSearch(runtime)
	},
}
View Source
var BaseTitleResolve = common.Shortcut{
	Service:     "base",
	Command:     "+title-resolve",
	Description: "Resolve a Base title or keyword through Drive search",
	Risk:        "read",
	Scopes:      []string{"search:docs:read"},
	AuthTypes:   []string{"user"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "title", Aliases: []string{"query", "url"}, Desc: "Base title keyword to search via Drive (30 characters or fewer)"},
	},
	Tips: []string{
		`Example: lark-cli base +title-resolve --title "Sales pipeline"`,
		"Pass a short keyword from the Base title, 30 characters or fewer. Use +url-resolve for URLs.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := readTitleResolveQuery(runtime)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		query, err := readTitleResolveQuery(runtime)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		return common.NewDryRunAPI().
			POST("/open-apis/search/v2/doc_wiki/search").
			Body(buildTitleResolveSearchBody(query))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseTitleResolve(runtime)
	},
}
View Source
var BaseURLResolve = common.Shortcut{
	Service:     "base",
	Command:     "+url-resolve",
	Description: "Resolve a Base or BaseApp URL into usable coordinates",
	Risk:        "read",
	Scopes:      []string{},
	ConditionalScopes: []string{
		"base:block:read",
		"base:field:read",
		"base:record:read",
		"wiki:node:retrieve",
	},
	AuthTypes: authTypes(),
	HasFormat: true,
	Flags: []common.Flag{
		{Name: "url", Aliases: []string{"query"}, Desc: "Base/BaseApp/Wiki/record-share URL to resolve"},
	},
	Tips: []string{
		`Example: lark-cli base +url-resolve --url "https://example.larkoffice.com/base/<base_token>?table=<table_id>&view=<view_id>"`,
		`BaseApp example: lark-cli base +url-resolve --url "https://example.larkoffice.com/app/<app_token>?pre_pathname=/base/workspace/<workspace_token>&pageId=<page_id>"`,
		"Only URLs are accepted. For Base titles or keywords, use +title-resolve --title.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := readURLResolveInput(runtime)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		raw, err := readURLResolveInput(runtime)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		parsed, err := parseResolveURL(raw)
		if err != nil {
			return common.NewDryRunAPI().Set("error", err.Error())
		}
		switch classifyBaseURL(parsed) {
		case "base_url":
			baseToken := firstPathSegmentAfter(parsed.Path, "/base/")
			if selectedBlockID := strings.TrimSpace(parsed.Query().Get("table")); selectedBlockID != "" {
				return common.NewDryRunAPI().
					POST("/open-apis/base/v3/bases/:base_token/blocks/list").
					Body(map[string]interface{}{}).
					Set("base_token", baseToken).
					Set("selected_block_id", selectedBlockID)
			}
			return common.NewDryRunAPI().Set("url", raw).Set("resolution", "local")
		case "wiki_url":
			dry := common.NewDryRunAPI()
			selectedBlockID := strings.TrimSpace(parsed.Query().Get("table"))
			if selectedBlockID == "" {
				return dry.
					GET("/open-apis/wiki/v2/spaces/get_node").
					Params(map[string]interface{}{"token": firstPathSegmentAfter(parsed.Path, "/wiki/")})
			}
			dry.Desc("2-step: resolve the Wiki node to a Base, then identify the selected Base block")
			dry.GET("/open-apis/wiki/v2/spaces/get_node").
				Desc("[1] Resolve the Wiki node to its underlying Base").
				Params(map[string]interface{}{"token": firstPathSegmentAfter(parsed.Path, "/wiki/")})
			dry.POST("/open-apis/base/v3/bases/:base_token/blocks/list").
				Desc("[2] List Base blocks and match selected_block_id").
				Body(map[string]interface{}{})
			return dry.
				Set("base_token", "<obj_token from step 1>").
				Set("selected_block_id", selectedBlockID)
		case "record_share_url":
			return common.NewDryRunAPI().
				GET("/open-apis/base/v3/record_share/:record_share_token/meta").
				Set("record_share_token", firstPathSegmentAfter(parsed.Path, "/record/"))
		default:
			return common.NewDryRunAPI().Set("url", raw).Set("resolution", "local")
		}
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeBaseURLResolve(runtime)
	},
}
View Source
var BaseViewCreate = common.Shortcut{
	Service:     "base",
	Command:     "+view-create",
	Description: "Create one or more views",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "json", Desc: "view JSON object/array; type defaults to grid; type range: grid, kanban, gallery, calendar, gantt", Required: true},
	},
	Tips: []string{
		`Example: lark-cli base +view-create --base-token <base_token> --table-id <table_id> --json '{"name":"Main","type":"grid"}'`,
		`Minimal: --json '{"name":"Main"}' creates a grid view.`,
		"Do not pass form as a view type; form views are managed through form commands.",
		`Use +view-set-visible-fields after creation when the user needs a specific field order or visibility.`,
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewCreate(runtime)
	},
	DryRun: dryRunViewCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewCreate(runtime)
	},
}
View Source
var BaseViewDelete = common.Shortcut{
	Service:     "base",
	Command:     "+view-delete",
	Description: "Delete a view by ID or name",
	Risk:        "high-risk-write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	Tips: []string{
		baseHighRiskYesTip,
		`Example: lark-cli base +view-delete --base-token <base_token> --table-id <table_id> --view-id "Old View" --yes`,
	},
	DryRun: dryRunViewDelete,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewDelete(runtime)
	},
}
View Source
var BaseViewGet = common.Shortcut{
	Service:     "base",
	Command:     "+view-get",
	Description: "Get a view by ID or name",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGet,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGet(runtime)
	},
}
View Source
var BaseViewGetCard = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-card",
	Description: "Get view card configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetCard,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "card", "card")
	},
}
View Source
var BaseViewGetFilter = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-filter",
	Description: "Get view filter configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetFilter,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "filter", "filter")
	},
}
View Source
var BaseViewGetGroup = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-group",
	Description: "Get view group configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetGroup,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "group", "group")
	},
}
View Source
var BaseViewGetSort = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-sort",
	Description: "Get view sort configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetSort,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "sort", "sort")
	},
}
View Source
var BaseViewGetTimebar = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-timebar",
	Description: "Get view timebar configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetTimebar,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "timebar", "timebar")
	},
}
View Source
var BaseViewGetVisibleFields = common.Shortcut{
	Service:     "base",
	Command:     "+view-get-visible-fields",
	Description: "Get view visible fields configuration",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags:       []common.Flag{baseTokenFlag(true), tableRefFlag(true), viewRefFlag(true)},
	DryRun:      dryRunViewGetVisibleFields,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewGetProperty(runtime, "visible_fields", "visible_fields")
	},
}
View Source
var BaseViewList = common.Shortcut{
	Service:     "base",
	Command:     "+view-list",
	Description: "List views in a table",
	Risk:        "read",
	Scopes:      []string{"base:view:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		{Name: "offset", Type: "int", Default: "0", Desc: "pagination offset"},
		{Name: "limit", Aliases: []string{"page-size"}, Type: "int", Default: "100", Desc: "pagination size, range 1-200"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		_, err := common.ValidatePageSizeTyped(runtime, "limit", 100, 1, 200)
		return err
	},
	DryRun: dryRunViewList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewList(runtime)
	},
}
View Source
var BaseViewRename = common.Shortcut{
	Service:     "base",
	Command:     "+view-rename",
	Description: "Rename a view by ID or name",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "name", Desc: "new view name", Required: true},
	},
	DryRun: dryRunViewRename,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewRename(runtime)
	},
}
View Source
var BaseViewSetCard = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-card",
	Description: "Set view card configuration",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `card JSON object, e.g. {"cover_field":"Cover"} or {"cover_field":null} to clear`, Required: true},
	},
	Tips: []string{
		"Supported view types: gallery, kanban.",
		"cover_field should be an attachment field id/name, or null to clear.",
		"Use +view-get-card first when updating an existing card view configuration.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetCard,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetJSONObject(runtime, "card", "card")
	},
}
View Source
var BaseViewSetFilter = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-filter",
	Description: "Set view filter configuration",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `filter JSON object, e.g. {"logic":"and","conditions":[["Status","==","Todo"]]}`, Required: true},
	},
	Tips: []string{
		"Agent hint: use the lark-base skill's view-set-filter guide for usage and limits.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetFilter,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetJSONObject(runtime, "filter", "filter")
	},
}
View Source
var BaseViewSetGroup = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-group",
	Description: "Set view group configuration",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `group JSON object with group_config array, e.g. {"group_config":[{"field":"Status","desc":false}]}; use {"group_config":[]} to clear`, Required: true},
	},
	Tips: []string{
		"Supported view types: grid, kanban, gantt.",
		"Use a JSON object, not a bare array; grouping fields must be supported by the current view.",
		"group_config supports max 3 group items.",
		"Use +view-get-group first when modifying an existing grouping configuration.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetGroup,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetWrapped(runtime, "group", "group_config", "group")
	},
}
View Source
var BaseViewSetSort = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-sort",
	Description: "Set view sort configuration",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `sort_config JSON object, e.g. {"sort_config":[{"field":"Priority","desc":true}]}; use {"sort_config":[]} to clear; max 10 items`, Required: true},
	},
	Tips: []string{
		"Supported view types: grid, kanban, gallery, gantt.",
		"Use a JSON object, not a bare array; sorting fields must be supported by the current view.",
		"sort_config supports max 10 sort items.",
		"Use +view-get-sort first when modifying an existing sort configuration.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetSort,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetWrapped(runtime, "sort", "sort_config", "sort")
	},
}
View Source
var BaseViewSetTimebar = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-timebar",
	Description: "Set view timebar configuration",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `timebar JSON object with start_time, end_time, title, e.g. {"start_time":"Start Date","end_time":"End Date","title":"Name"}`, Required: true},
	},
	Tips: []string{
		"Supported view types: calendar, gantt.",
		"start_time, end_time, and title are required; use date/time fields for start_time and end_time.",
		"Use +view-get-timebar first when modifying an existing timebar configuration.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetTimebar,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetJSONObject(runtime, "timebar", "timebar")
	},
}
View Source
var BaseViewSetVisibleFields = common.Shortcut{
	Service:     "base",
	Command:     "+view-set-visible-fields",
	Description: "Set view visible fields",
	Risk:        "write",
	Scopes:      []string{"base:view:write_only"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		baseTokenFlag(true),
		tableRefFlag(true),
		viewRefFlag(true),
		{Name: "json", Desc: `visible fields JSON object, e.g. {"visible_fields":["Name","Status"]}`, Required: true},
	},
	Tips: []string{
		"Supported view types: grid, kanban, gallery, calendar, gantt.",
		"Use a JSON object, not a bare array; primary field may be forced to the first position by the API.",
		"visible_fields controls both visibility and order; include every field that should remain visible.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return validateViewJSONObject(runtime)
	},
	DryRun: dryRunViewSetVisibleFields,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeViewSetVisibleFields(runtime)
	},
}
View Source
var BaseWorkflowCreate = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-create",
	Description: "Create a new workflow in a base",
	Risk:        "write",
	Scopes:      []string{"base:workflow:create"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "json", Desc: "workflow body JSON; read lark-base-workflow.md and lark-base-workflow-schema.md before constructing steps", Required: true},
	},
	Tips: []string{
		"lark-cli base +workflow-create --base-token <base_token> --json @workflow.json",
		"client_token is required and should be unique per create request.",
		"New workflows are created disabled; call +workflow-enable after creation when the user wants it active.",
		"Before constructing steps, use +table-list and +field-list to confirm real table and field names.",
		"Step ids must be unique, and every next/children link must reference an existing step id.",
		"Use lark-base-workflow.md as the module entry and lark-base-workflow-schema.md as the steps JSON SSOT; do not invent steps[].type/data/next/children from natural language.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		pc := newParseCtx(runtime)
		raw, err := loadJSONInput(pc, runtime.Str("json"), "json")
		if err != nil {
			return err
		}
		if _, err := parseJSONObject(pc, raw, "json"); err != nil {
			return err
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		pc := newParseCtx(runtime)
		var body map[string]interface{}
		if raw, err := loadJSONInput(pc, runtime.Str("json"), "json"); err == nil {
			body, _ = parseJSONObject(pc, raw, "json")
		}
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/workflows").
			Body(body).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		pc := newParseCtx(runtime)
		raw, err := loadJSONInput(pc, runtime.Str("json"), "json")
		if err != nil {
			return err
		}
		body, err := parseJSONObject(pc, raw, "json")
		if err != nil {
			return err
		}
		data, err := baseV3Call(runtime, "POST",
			baseV3Path("bases", runtime.Str("base-token"), "workflows"),
			nil,
			body,
		)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseWorkflowDisable = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-disable",
	Description: "Disable a workflow in a base",
	Risk:        "write",
	Scopes:      []string{"base:workflow:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true},
	},
	Tips: []string{
		"workflow-id must start with wkf; do not pass a tbl table ID from the same URL.",
		"Disable only changes workflow state; it does not delete the workflow or its steps.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("workflow-id")) == "" {
			return baseFlagErrorf("--workflow-id must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id/disable").
			Set("base_token", runtime.Str("base-token")).
			Set("workflow_id", runtime.Str("workflow-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "PATCH",
			baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id"), "disable"),
			nil,
			map[string]interface{}{},
		)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseWorkflowEnable = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-enable",
	Description: "Enable a workflow in a base",
	Risk:        "write",
	Scopes:      []string{"base:workflow:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true},
	},
	Tips: []string{
		"workflow-id must start with wkf; do not pass a tbl table ID from the same URL.",
		"Enable only changes workflow state; it does not modify steps.",
		"New workflows are created disabled; enable after creation only when the user wants it active.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("workflow-id")) == "" {
			return baseFlagErrorf("--workflow-id must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		return common.NewDryRunAPI().
			PATCH("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id/enable").
			Set("base_token", runtime.Str("base-token")).
			Set("workflow_id", runtime.Str("workflow-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		data, err := baseV3Call(runtime, "PATCH",
			baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id"), "enable"),
			nil,
			map[string]interface{}{},
		)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseWorkflowGet = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-get",
	Description: "Get a single workflow definition (including steps) from a base",
	Risk:        "read",
	Scopes:      []string{"base:workflow:read"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true},
		{Name: "user-id-type", Desc: "user ID type for creator/updater fields, default open_id", Enum: []string{"open_id", "union_id", "user_id"}},
	},
	Tips: []string{
		"workflow-id must start with wkf; use +workflow-list if the ID is unknown.",
		"steps may be an empty array; that is valid for an unconfigured workflow.",
		"Use +workflow-get before +workflow-update, then edit the returned definition and keep fields you do not intend to change.",
		"Read lark-base-workflow-schema.md when interpreting or reusing returned steps.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("workflow-id")) == "" {
			return baseFlagErrorf("--workflow-id must not be blank")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		api := common.NewDryRunAPI().
			GET("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id").
			Set("base_token", runtime.Str("base-token")).
			Set("workflow_id", runtime.Str("workflow-id"))
		if t := runtime.Str("user-id-type"); t != "" {
			api = api.Params(map[string]interface{}{"user_id_type": t})
		}
		return api
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		var params map[string]interface{}
		if t := runtime.Str("user-id-type"); t != "" {
			params = map[string]interface{}{"user_id_type": t}
		}
		data, err := baseV3Call(runtime, "GET",
			baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id")),
			params,
			nil,
		)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseWorkflowList = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-list",
	Description: "List all workflows in a base (auto-paginated)",
	Risk:        "read",
	Scopes:      []string{"base:workflow:read"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "status", Desc: "filter by status", Enum: []string{"enabled", "disabled"}},
		{Name: "page-size", Type: "int", Default: "100", Desc: "page size per request, range 1-100"},
	},
	Tips: []string{
		"Returns workflow_id values with wkf prefix; pass those IDs to +workflow-get/enable/disable/update.",
		"This shortcut auto-paginates and returns all matched workflows.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		_, err := common.ValidatePageSizeTyped(runtime, "page-size", 100, 1, 100)
		return err
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		body := map[string]interface{}{
			"page_size": runtime.Int("page-size"),
		}
		if s := runtime.Str("status"); s != "" {
			body["status"] = s
		}
		return common.NewDryRunAPI().
			POST("/open-apis/base/v3/bases/:base_token/workflows/list").
			Body(body).
			Set("base_token", runtime.Str("base-token"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		var allItems []interface{}
		pageToken := ""
		for {
			body := map[string]interface{}{
				"page_size": runtime.Int("page-size"),
			}
			if pageToken != "" {
				body["page_token"] = pageToken
			}
			if s := runtime.Str("status"); s != "" {
				body["status"] = s
			}
			data, err := baseV3Call(runtime, "POST",
				baseV3Path("bases", runtime.Str("base-token"), "workflows", "list"),
				nil,
				body,
			)
			if err != nil {
				return err
			}
			items, _ := data["items"].([]interface{})
			allItems = append(allItems, items...)
			hasMore, _ := data["has_more"].(bool)
			if !hasMore {
				break
			}
			nextToken, _ := data["page_token"].(string)
			if nextToken == "" {
				break
			}
			pageToken = nextToken
		}
		runtime.Out(map[string]interface{}{
			"items": allItems,
			"total": len(allItems),
		}, nil)
		return nil
	},
}
View Source
var BaseWorkflowUpdate = common.Shortcut{
	Service:     "base",
	Command:     "+workflow-update",
	Description: "Replace a workflow's full definition (title and/or steps) in a base",
	Risk:        "write",
	Scopes:      []string{"base:workflow:update"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "base-token", Desc: "base token", Required: true},
		{Name: "workflow-id", Desc: "workflow ID (wkf... prefix)", Required: true},
		{Name: "json", Desc: "workflow body JSON; read lark-base-workflow.md and lark-base-workflow-schema.md before replacing steps", Required: true},
	},
	Tips: []string{
		"lark-cli base +workflow-update --base-token <base_token> --workflow-id <workflow_id> --json @workflow.json",
		"PUT uses full replacement semantics; omitting steps clears the existing workflow steps.",
		"Use +workflow-get first, then edit the returned definition and keep title/status/steps fields you do not intend to change.",
		"workflow-id must start with wkf; do not pass a tbl table ID.",
		"Step ids must be unique, and every next/children link must reference an existing step id.",
		"Updating does not enable or disable a workflow; call +workflow-enable or +workflow-disable separately.",
		"Use lark-base-workflow.md as the module entry and lark-base-workflow-schema.md as the steps JSON SSOT; do not invent steps[].type/data/next/children from natural language.",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if strings.TrimSpace(runtime.Str("base-token")) == "" {
			return baseFlagErrorf("--base-token must not be blank")
		}
		if strings.TrimSpace(runtime.Str("workflow-id")) == "" {
			return baseFlagErrorf("--workflow-id must not be blank")
		}
		pc := newParseCtx(runtime)
		if _, err := parseJSONObject(pc, runtime.Str("json"), "json"); err != nil {
			return err
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		pc := newParseCtx(runtime)
		var body map[string]interface{}
		body, _ = parseJSONObject(pc, runtime.Str("json"), "json")
		return common.NewDryRunAPI().
			PUT("/open-apis/base/v3/bases/:base_token/workflows/:workflow_id").
			Body(body).
			Set("base_token", runtime.Str("base-token")).
			Set("workflow_id", runtime.Str("workflow-id"))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		pc := newParseCtx(runtime)
		body, err := parseJSONObject(pc, runtime.Str("json"), "json")
		if err != nil {
			return err
		}
		data, err := baseV3Call(runtime, "PUT",
			baseV3Path("bases", runtime.Str("base-token"), "workflows", runtime.Str("workflow-id")),
			nil,
			body,
		)
		if err != nil {
			return err
		}
		runtime.Out(data, nil)
		return nil
	},
}
View Source
var BaseWorkspaceCreate = common.Shortcut{
	Service:     "base",
	Command:     "+workspace-create",
	Description: "Create a workspace",
	Risk:        "write",
	Scopes:      []string{"base:workspace:create"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		{Name: "name", Desc: "workspace name", Required: true},
	},
	Tips: []string{
		`lark-cli base +workspace-create --name "Growth team"`,
		"Record the returned workspace_token and url; +workspace-entity-list, +workspace-move-in, and +app-create need the token.",
	},
	DryRun: dryRunWorkspaceCreate,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeWorkspaceCreate(runtime)
	},
}
View Source
var BaseWorkspaceEntityList = common.Shortcut{
	Service:     "base",
	Command:     "+workspace-entity-list",
	Description: "List bases and BaseApps in a workspace",
	Risk:        "read",
	Scopes:      []string{"base:workspace:read"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		workspaceTokenFlag(true),
		{Name: "type", Desc: "filter by entity type: base|baseapp; omit to list both", Enum: entityTypeValues},
		{Name: "page-size", Type: "int", Default: "100", Desc: "page size, range 1-100"},
		{Name: "page-token", Desc: "pagination token"},
	},
	Tips: []string{
		"lark-cli base +workspace-entity-list --workspace-token <workspace_token> --type baseapp",
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		if _, err := common.ValidatePageSizeTyped(runtime, "page-size", 100, 1, 100); err != nil {
			return err
		}
		_, err := normalizeEntityType(runtime.Str("type"))
		return err
	},
	DryRun: dryRunWorkspaceEntityList,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeWorkspaceEntityList(runtime)
	},
}
View Source
var BaseWorkspaceMoveIn = common.Shortcut{
	Service:     "base",
	Command:     "+workspace-move-in",
	Description: "Move an existing Base or BaseApp into a workspace",
	Risk:        "write",
	Scopes:      []string{"base:workspace:update"},
	AuthTypes:   authTypes(),
	Flags: []common.Flag{
		workspaceTokenFlag(true),
		{Name: "entity-token", Desc: "base_token or app_token to move into the workspace", Required: true},
	},
	Tips: []string{
		"lark-cli base +workspace-move-in --workspace-token <workspace_token> --entity-token <base_token>",
		"This moves the entity into the workspace tree; it does not create the Base or App.",
		"The current OpenAPI does not accept entity_type or ordering fields for move-in.",
	},
	DryRun: dryRunWorkspaceMoveIn,
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		return executeWorkspaceMoveIn(runtime)
	},
}

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all base shortcuts.

Types

This section is empty.

Source Files

Directories

Path Synopsis
Package recordexport converts the Base OpenAPI record matrix into a stable, typed row model that output formats can share.
Package recordexport converts the Base OpenAPI record matrix into a stable, typed row model that output formats can share.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL