Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
var AppsAccessScopeGet = common.Shortcut{ Service: appsService, Command: "+access-scope-get", Description: "Get Miaoda app access scope configuration", Risk: "read", Tips: []string{ "Example: lark-cli apps +access-scope-get --app-id <app_id>", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) return common.NewDryRunAPI(). GET(fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))). Desc("Get Miaoda app access scope") }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) path := fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("GET", path, nil, nil) if err != nil { return withAppsHint(err, "verify --app-id is correct and you have access to the app; list your apps with `lark-cli apps +list`") } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "scope: %v\n", data["scope"]) }) return nil }, }
AppsAccessScopeGet reads the current access scope configuration of a Miaoda app. 响应原样透传服务端契约(字符串 scope 枚举 All/Tenant/Range + 拆分的 users/departments/chats 数组)。
var AppsAccessScopeSet = common.Shortcut{ Service: appsService, Command: "+access-scope-set", Description: "Set Miaoda app access scope (specific / public / tenant)", Risk: "write", Tips: []string{ `Example: lark-cli apps +access-scope-set --app-id <app_id> --scope tenant`, `Example: lark-cli apps +access-scope-set --app-id <app_id> --scope public --require-login`, `Example: lark-cli apps +access-scope-set --app-id <app_id> --scope specific --targets '[{"type":"user","id":"<open_id>"}]'`, }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "scope", Desc: "scope: specific | public | tenant", Required: true, Enum: []string{"specific", "public", "tenant"}}, {Name: "targets", Desc: `targets JSON array: [{"type":"user|department|chat","id":"..."}, ...]`}, {Name: "apply-enabled", Type: "bool", Desc: "allow apply for access (scope=specific)"}, {Name: "approver", Desc: "approver open_id (when --apply-enabled; server allows exactly one)"}, {Name: "require-login", Type: "bool", Desc: "require login (scope=public)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return validateAccessScopeFlags(rctx) }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) dry := common.NewDryRunAPI(). PUT(fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID))). Desc("Set Miaoda app access scope") body, bodyErr := buildAccessScopeBody(rctx) if bodyErr != nil { dry.Set("body_error", bodyErr.Error()) } else { dry.Body(body) } return dry }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { body, err := buildAccessScopeBody(rctx) if err != nil { return err } appID := strings.TrimSpace(rctx.Str("app-id")) path := fmt.Sprintf("%s/apps/%s/access-scope", apiBasePath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("PUT", path, nil, body) if err != nil { return withAppsHint(err, "verify --app-id is correct; for scope=specific, each --targets id must be a valid open_id/department_id/chat_id and --approver a valid open_id; review the current scope with `lark-cli apps +access-scope-get --app-id <app_id>`") } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "access-scope set: %s\n", rctx.Str("scope")) }) return nil }, }
AppsAccessScopeSet sets the app's access scope (specific / public / tenant).
var AppsChat = common.Shortcut{ Service: appsService, Command: "+chat", Description: "Send a message to a session to start/continue a conversation", Risk: "write", Tips: []string{ `Example: lark-cli apps +chat --app-id <app_id> --session-id <session_id> --message "做一个待办清单页面"`, `Example: lark-cli apps +chat --app-id <app_id> --session-id <session_id> --message "把首页标题改为 我的待办"`, }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "session-id", Desc: "session ID", Required: true}, {Name: "message", Desc: "user message text", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if strings.TrimSpace(rctx.Str("session-id")) == "" { return appsValidationParamError("--session-id", "--session-id is required") } if strings.TrimSpace(rctx.Str("message")) == "" { return appsValidationParamError("--message", "--message is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST(chatPath(rctx.Str("app-id"), rctx.Str("session-id"))). Desc("Send a message to a session"). Body(buildChatBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("POST", chatPath(rctx.Str("app-id"), rctx.Str("session-id")), nil, buildChatBody(rctx)) if err != nil { return withAppsHint(err, "if the session_id is unknown or invalid, list this app's sessions with `lark-cli apps +session-list --app-id "+strings.TrimSpace(rctx.Str("app-id"))+"`") } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "message sent; poll +session-get for turn status\n") }) return nil }, }
Turn cost varies sharply by init state: the first +chat on a not-initialized app runs a one-time design + first-generation pass server-side (~20-50 min); chat on an already-initialized app is incremental and finishes in minutes. The init-state check and matching polling cadence live in the lark-apps skill reference (references/lark-apps-cloud-dev.md) — the canonical source.
var AppsCreate = common.Shortcut{ Service: appsService, Command: "+create", Description: "Create a new Miaoda app", Risk: "write", Tips: []string{ `Example: lark-cli apps +create --name "审批系统" --app-type full_stack`, `Example: lark-cli apps +create --name "活动页" --app-type html --description "活动报名"`, }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "name", Desc: "app display name", Required: true}, {Name: "app-type", Desc: "app type", Required: true, Enum: []string{"html", "full_stack"}}, {Name: "description", Desc: "app description"}, {Name: "icon-url", Desc: "app icon URL (server uses default if omitted)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("name")) == "" { return appsValidationParamError("--name", "--name is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST(apiBasePath + "/apps"). Desc("Create a Miaoda app"). Body(buildAppsCreateBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("POST", apiBasePath+"/apps", nil, buildAppsCreateBody(rctx)) if err != nil { return withAppsHint(err, createHint) } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "created: %s\n", common.GetString(data, "app", "app_id")) }) return nil }, }
AppsCreate creates a new Miaoda app.
var AppsDBEnvCreate = common.Shortcut{ Service: appsService, Command: "+db-env-create", Description: "Create a DB environment (split single-env DB into dev/online, irreversible)", Risk: "high-risk-write", Tips: []string{ "Example: lark-cli apps +db-env-create --env dev --sync-data --app-id <app_id> --yes", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app id", Required: true}, {Name: "env", Default: "dev", Enum: []string{"dev"}, Desc: "environment to create (only dev supported for now)"}, {Name: "sync-data", Type: "bool", Desc: "copy existing online data into the new environment (default off)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { _, err := requireAppID(rctx.Str("app-id")) return err }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID, _ := requireAppID(rctx.Str("app-id")) return common.NewDryRunAPI(). POST(appDbEnvCreatePath(appID)). Desc("Create Miaoda app DB environment"). Body(buildDBEnvCreateBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) if err != nil { return err } data, err := rctx.CallAPITyped("POST", appDbEnvCreatePath(appID), nil, buildDBEnvCreateBody(rctx)) if err != nil { return withAppsHint(err, dbEnvCreateHint) } rctx.OutFormat(data, nil, func(w io.Writer) { renderEnvCreatePretty(w, data) }) return nil }, }
AppsDBEnvCreate creates a DB environment for a Miaoda app(拆分单库为 dev/online 多环境)。
调 POST /apps/{app_id}/db_dev_init。--env 指定要创建的环境,由调用方传入,目前只支持 dev。 不可逆:单库一旦拆成 dev/online 双库无法回退。Risk: high-risk-write 触发框架自动注入 --yes 确认关卡。
var AppsDBExecute = common.Shortcut{ Service: appsService, Command: "+db-execute", Description: "Execute SQL (SELECT / DML / DDL) against a Miaoda app database", Risk: "high-risk-write", Tips: []string{ `Example: lark-cli apps +db-execute --app-id <app_id> --sql "SELECT * FROM orders LIMIT 10" --yes`, `Example: lark-cli apps +db-execute --app-id <app_id> --env dev --file ./migration.sql --yes`, "Tip: filter fields with --jq, e.g. -q '.data.results[].sql_type'", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app id", Required: true}, {Name: "sql", Desc: "SQL text; use - to read stdin. Mutually exclusive with --file", Input: []string{common.Stdin}}, {Name: "file", Desc: "path to a .sql file (relative to cwd). Mutually exclusive with --sql"}, {Name: "env", Default: "dev", Enum: []string{"dev", "online"}, Desc: "target db environment (default dev; use --env online for the online environment)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if _, err := requireAppID(rctx.Str("app-id")); err != nil { return err } sql := strings.TrimSpace(rctx.Str("sql")) file := strings.TrimSpace(rctx.Str("file")) if sql != "" && file != "" { return appsValidationError("--sql and --file are mutually exclusive"). WithParams( appsInvalidParam("--sql", "mutually exclusive with --file"), appsInvalidParam("--file", "mutually exclusive with --sql"), ) } if file != "" { data, err := cmdutil.ReadInputFile(rctx.FileIO(), file) if err != nil { return appsValidationParamError("--file", "--file: %v", err).WithCause(err) } rctx.Cmd.Flags().Set("sql", string(data)) sql = strings.TrimSpace(string(data)) } if sql == "" { return appsValidationError("one of --sql or --file is required (use --sql - to read stdin)"). WithParams( appsInvalidParam("--sql", "one of --sql or --file is required"), appsInvalidParam("--file", "one of --sql or --file is required"), ) } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID, _ := requireAppID(rctx.Str("app-id")) return common.NewDryRunAPI(). POST(appSQLPath(appID)). Desc("Execute SQL on Miaoda app database"). Params(buildDBSQLParams(rctx)). Body(buildDBSQLBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) if err != nil { return err } raw, err := rctx.CallAPITyped("POST", appSQLPath(appID), buildDBSQLParams(rctx), buildDBSQLBody(rctx)) if err != nil { return withAppsHint(err, "verify table/column names with `lark-cli apps +db-table-get --app-id "+appID+" --table <table>`; for day-to-day debugging target the dev database with `--env dev`") } stmts := parseSQLResult(common.GetString(raw, "result")) data := map[string]interface{}{"results": stmts} if errIdx, errStmt, failed := findErrorSentinel(stmts); failed { if rctx.Format == "pretty" { renderSQLPretty(rctx.IO().Out, stmts) return output.PartialFailure(output.ExitAPI) } return rctx.OutPartialFailure(sqlStatementFailurePayload(stmts, errIdx, errStmt), nil) } rctx.OutFormat(data, nil, func(w io.Writer) { renderSQLPretty(w, stmts) }) return nil }, }
AppsDBExecute executes SQL against a Miaoda app database.
POST /apps/{app_id}/sql_commands,CLI 永远带 ?transactional=false 进入 DBA 模式 (不默认包事务、支持 DDL、result 字符串内嵌结构化 JSON)。
pretty 渲染 6 种形态:
- 单 SELECT:表格(列间两空格、列对齐填充)
- 空 SELECT:`(0 rows)`
- 单 DML:`✓ N row(s) <verb>`(verb 跟 sql_type:INSERT→inserted/UPDATE→updated/DELETE→deleted)
- 单 DDL:`✓ DDL executed`
- 多语句全部成功:逐条 `Statement K: ✓ <summary>` + 末尾 `✓ N statements executed`
- 多语句部分失败:`Statement K: ✗ <message> [<code>]` + 末尾「前序语句已落地」提示
失败语义:server 多语句失败仍返 code:0,把失败语句标成 ERROR 哨兵塞进 result。Execute 检测到哨兵 后按 partial failure 上报(exit 非 0):stdout 输出 ok:false 数据,带 results / statement_index / error_code / error_message / rolled_back / note,避免 agent 误判 ok:true 假成功。CLI 永远 DBA 模式(transactional=false),失败前的语句已 auto-commit 落地,故 rolled_back=false(真机 boe 实证)。
JSON envelope(成功路径):CLI 把 server 返的 result 字符串解出来放进 `data.results` 数组。
Risk: high-risk-write —— SQL 可含 DML/DDL,框架对所有执行强制 --yes 确认关卡(--dry-run 预览豁免)。
SQL 来源二选一:--sql(内联文本,或 - 读 stdin)/ --file(.sql 文件路径,受 CLI 相对路径约束)。 --file 在 Validate 阶段读出内容、归一化到 --sql,下游统一从 rctx.Str("sql") 取。
var AppsDBTableGet = common.Shortcut{ Service: appsService, Command: "+db-table-get", Description: "Get a table's structure: columns, indexes and constraints", Risk: "read", Tips: []string{ "Example: lark-cli apps +db-table-get --app-id <app_id> --table <table>", "Tip: filter fields with --jq (json format), e.g. -q '.data.columns[].name'", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app id", Required: true}, {Name: "table", Desc: "table name", Required: true}, {Name: "env", Default: "online", Enum: []string{"dev", "online"}, Desc: "target db environment"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if _, err := requireAppID(rctx.Str("app-id")); err != nil { return err } if strings.TrimSpace(rctx.Str("table")) == "" { return appsValidationParamError("--table", "--table is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID, _ := requireAppID(rctx.Str("app-id")) return common.NewDryRunAPI(). GET(appTablePath(appID, strings.TrimSpace(rctx.Str("table")))). Desc("Get Miaoda app db table schema"). Params(buildDBTableGetParams(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) if err != nil { return err } path := appTablePath(appID, strings.TrimSpace(rctx.Str("table"))) data, err := rctx.CallAPITyped("GET", path, buildDBTableGetParams(rctx), nil) if err != nil { return withAppsHint(err, dbTableGetHint) } rctx.OutFormat(data, nil, func(w io.Writer) { io.WriteString(w, common.GetString(data, "ddl")) }) return nil }, }
AppsDBTableGet gets one table's structure (动词对齐 +db-table-list)。
GET /apps/{app_id}/tables/{table_name}。
`--format` 同时驱动 CLI 渲染和 server 请求形态:
- `--format json`(默认)/ table / ndjson / csv:CLI 不传 format query,response 含结构化 columns / indexes / constraints / stats,envelope 化输出。
- `--format pretty`:CLI 给 server 带 ?format=ddl,response 含 ddl 字符串,stdout 直接打 ddl 内容(无 envelope / 无表格包装)。
var AppsDBTableList = common.Shortcut{ Service: appsService, Command: "+db-table-list", Description: "List tables in a Miaoda app database (cursor pagination)", Risk: "read", Tips: []string{ "Example: lark-cli apps +db-table-list --app-id <app_id>", "Tip: filter fields with --jq, e.g. -q '.data.items[].name'", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app id", Required: true}, {Name: "env", Default: "online", Enum: []string{"dev", "online"}, Desc: "target db environment"}, {Name: "page-size", Type: "int", Default: "20", Desc: "page size"}, {Name: "page-token", Desc: "pagination cursor from previous response"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { _, err := requireAppID(rctx.Str("app-id")) return err }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID, _ := requireAppID(rctx.Str("app-id")) return common.NewDryRunAPI(). GET(appTablesPath(appID)). Desc("List Miaoda app db tables"). Params(buildDBTableListParams(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID, err := requireAppID(rctx.Str("app-id")) if err != nil { return err } data, err := rctx.CallAPITyped("GET", appTablesPath(appID), buildDBTableListParams(rctx), nil) if err != nil { return withAppsHint(err, dbTableListHint) } items := projectTableListItems(data["items"]) data["items"] = items rctx.OutFormat(data, nil, func(w io.Writer) { renderTableListPretty(w, items) }) return nil }, }
AppsDBTableList lists tables in a Miaoda app's database.
GET /apps/{app_id}/tables(cursor 分页),response items[] 含 estimated_row_count / size_bytes optional 字段,默认返回,不必额外传 query。
输出裁剪:server 给每张表回完整 columns[](与 +db-table-get 同源、内容一致)。CLI 用白名单 投影(dbTableListItem)只组装产品要求字段、把 columns[] 折算成 column_count,避免逐表重复列定义 放大 token、并与 +db-table-get 职责区分。完整列定义 / 索引 / 约束 / DDL 用 +db-table-get。
pretty 渲染 5 列:name / description / estimated_row_count / size / columns(即 column_count); 列间两空格、列对齐填充、空 description 用 "—" 占位、size 按 KB/MB/GB 友好格式化。
var AppsEnvPull = common.Shortcut{ Service: appsService, Command: "+env-pull", Description: "Pull app startup env vars into the local project .env.local", Risk: "write", Tips: []string{ "Example: lark-cli apps +env-pull --app-id <app_id>", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID"}, {Name: "project-path", Desc: "local project root path (defaults to current directory)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } _, envFile, err := resolveEnvPullTarget(strings.TrimSpace(rctx.Str("project-path"))) if err != nil { return appsValidationParamError("--project-path", "--project-path: %v", err).WithCause(err) } if err := checkEnvPullTarget(envFile); err != nil { return err } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { projectPath, envFile, _ := resolveEnvPullTarget(strings.TrimSpace(rctx.Str("project-path"))) appID := strings.TrimSpace(rctx.Str("app-id")) return common.NewDryRunAPI(). POST(fmt.Sprintf("%s/apps/%s/env_vars", apiBasePath, validate.EncodePathSegment(appID))). Desc("Pull app startup env vars into the local .env.local file"). Set("project_path", projectPath). Set("env_file", envFile) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) _, envFile, err := resolveEnvPullTarget(strings.TrimSpace(rctx.Str("project-path"))) if err != nil { return appsValidationParamError("--project-path", "--project-path: %v", err).WithCause(err) } if err := checkEnvPullTarget(envFile); err != nil { return err } if err := rctx.EnsureScopes([]string{"spark:app:read"}); err != nil { return err } path := fmt.Sprintf("%s/apps/%s/env_vars", apiBasePath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("POST", path, nil, nil) if err != nil { return withAppsHint(err, "verify --app-id is correct and you have access to the app; list your apps with `lark-cli apps +list`") } envVars, databaseInfo, skippedKeys, err := extractEnvPullVars(data) if err != nil { return err } if envVars == nil { envVars = map[string]string{} } envVars["FORCE_DB_BRANCH"] = "dev" original, err := readEnvPullFile(envFile) if err != nil { return err } merged, updated, created := mergeEnvPullFileContent(original, envVars) if err := ensureEnvPullParentDir(envFile); err != nil { return err } if err := validate.AtomicWrite(envFile, []byte(merged), 0o600); err != nil { return &errs.InternalError{Problem: errs.Problem{Category: errs.CategoryInternal, Subtype: errs.SubtypeUnknown, Message: fmt.Sprintf("cannot write %s: %v", envFile, err)}, Cause: err} } result := buildEnvPullSuccessData(appID, envFile, databaseInfo) rctx.OutFormat(result, nil, func(w io.Writer) { writeEnvPullPretty(w, appID, envFile, databaseInfo, skippedKeys) }) _ = updated _ = created return nil }, }
AppsEnvPull pulls startup env vars for an app into the local .env.local file.
var AppsGitCredentialInit = common.Shortcut{ Service: appsService, Command: "+git-credential-init", Description: "Initialize Git credentials and a URL-scoped Git helper for a Miaoda app repository", Risk: "write", Tips: []string{ "Example: lark-cli apps +git-credential-init --app-id <app_id>", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if err := validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id"); err != nil { return appsValidationParamError("--app-id", "%v", err).WithCause(err) } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) return common.NewDryRunAPI(). GET(gitCredentialIssuePath). Desc("Issue a Miaoda Git repository PAT"). Set("mode", "api-plus-local-setup"). Set("action", "initialize_local_git_credential"). Set("app_id", appID). Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)). Set("local_effects", []string{ "save the issued PAT in the local system credential store", "write app-scoped git credential metadata", "configure a URL-scoped Git credential helper in global git config when possible", }). Params(gitCredentialIssueParams(appID)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) manager := newGitCredentialManager(appID, rctx.Factory.Keychain, runtimeIssuer{rctx: rctx}) result, err := manager.Init(ctx, profileFromConfig(rctx.Config), appID) if err != nil { return gitCredentialLocalError("Initialize local Miaoda Git credential", err) } payload := map[string]interface{}{ "app_id": result.AppID, "repository_url": result.GitHTTPURL, "status": initStatus(result), } if result.ConfigWarning != "" { payload["git_config_warning"] = result.ConfigWarning } rctx.OutFormat(payload, nil, func(w io.Writer) { title := "Git credential initialized" if result.Refreshed { title = "Git credential refreshed" } fmt.Fprintln(w, title) fmt.Fprintln(w) fmt.Fprintf(w, "App ID: %s\n", result.AppID) fmt.Fprintf(w, "Status: %s\n", initStatus(result)) fmt.Fprintf(w, "Repository URL: %s\n", result.GitHTTPURL) if result.ConfigWarning != "" { fmt.Fprintln(w) fmt.Fprintln(w, "Git credential saved, but Git helper was not configured") fmt.Fprintf(w, "Reason: %s\n", result.ConfigWarning) fmt.Fprintf(w, "Next step: lark-cli apps +git-credential-init --app-id %s\n", result.AppID) return } fmt.Fprintln(w) fmt.Fprintln(w, "Next step:") fmt.Fprintf(w, " git clone %s\n", result.GitHTTPURL) }) return nil }, }
var AppsGitCredentialList = common.Shortcut{ Service: appsService, Command: "+git-credential-list", Description: "List local Git credentials for Miaoda app repositories", Risk: "read", Tips: []string{ "Example: lark-cli apps +git-credential-list", }, Scopes: []string{}, AuthTypes: []string{"user"}, HasFormat: true, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). Desc("Preview local Git credential listing (no API call, read-only local state)."). Set("mode", "local-read-only"). Set("action", "list_local_git_credentials"). Set("storage_root", filepath.Join(core.GetConfigDir(), storageRoot)). Set("reads", []string{ "scan app-scoped git credential metadata under the CLI config directory", "derive per-app repository URLs and local credential status from local metadata", }) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { records, err := listGitCredentialRecords(rctx.Factory.Keychain, time.Now) if err != nil { return gitCredentialLocalError("List local Miaoda Git credentials", err) } payload := map[string]interface{}{ "count": len(records), "credentials": gitCredentialListPayload(records), } rctx.OutFormat(payload, nil, func(w io.Writer) { if len(records) == 0 { fmt.Fprintln(w, "No Git credentials initialized") fmt.Fprintln(w) fmt.Fprintln(w, "Next step: lark-cli apps +git-credential-init --app-id <app_id>") return } tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) fmt.Fprintln(tw, "App ID\tRepository URL\tStatus") for _, record := range records { fmt.Fprintf(tw, "%s\t%s\t%s\n", record.AppID, record.GitHTTPURL, gitCredentialDisplayStatus(record.Status)) } _ = tw.Flush() fmt.Fprintln(w) fmt.Fprintln(w, "Profile switches do not remove old URL-scoped Git helpers automatically.") fmt.Fprintln(w, "Cleanup: lark-cli apps +git-credential-remove --app-id <app_id>") }) return nil }, }
var AppsGitCredentialRemove = common.Shortcut{ Service: appsService, Command: "+git-credential-remove", Description: "Remove local Git credentials and the URL-scoped Git helper for a Miaoda app repository", Risk: "write", Tips: []string{ "Example: lark-cli apps +git-credential-remove --app-id <app_id>", }, Scopes: []string{}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if err := validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id"); err != nil { return appsValidationParamError("--app-id", "%v", err).WithCause(err) } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) return common.NewDryRunAPI(). Desc("Preview local Git credential cleanup (no API call; would clean up local-only state)."). Set("mode", "local-cleanup-only"). Set("action", "remove_local_git_credential"). Set("app_id", appID). Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)). Set("effects", []string{ "read app-scoped git credential metadata", "remove the saved PAT from the local system credential store", "remove the app-scoped Git helper from global git config when present", "delete the local metadata record after cleanup succeeds", }) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) manager := newGitCredentialManager(appID, rctx.Factory.Keychain, nil) result, err := manager.Remove(ctx, profileFromConfig(rctx.Config), appID) if err != nil { return gitCredentialLocalError("Remove local Miaoda Git credential", err) } payload := map[string]interface{}{ "app_id": result.AppID, "removed": result.Removed, } if result.ConfigWarning != "" { payload["git_config_warning"] = result.ConfigWarning } rctx.OutFormat(payload, nil, func(w io.Writer) { if !result.Removed { fmt.Fprintln(w, "No local Git credential found") return } fmt.Fprintln(w, "Git credential removed") fmt.Fprintln(w) fmt.Fprintf(w, "App ID: %s\n", result.AppID) if len(result.Records) > 0 { fmt.Fprintf(w, "Repository URL: %s\n", result.Records[0].GitHTTPURL) } fmt.Fprintln(w, "Status: removed") if result.ConfigWarning != "" { fmt.Fprintln(w) fmt.Fprintln(w, "Git config cleanup warning") fmt.Fprintf(w, "Reason: %s\n", result.ConfigWarning) } }) return nil }, }
var AppsHTMLPublish = common.Shortcut{ Service: appsService, Command: "+html-publish", Description: "Publish HTML to a Miaoda app (single multipart POST returns the access URL)", Risk: "write", Tips: []string{ "Example: lark-cli apps +html-publish --app-id <app_id> --path ./dist", "Example: lark-cli apps +html-publish --app-id <app_id> --path ./site --dry-run", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, {Name: "path", Desc: "path to HTML file or directory", Required: true}, {Name: "allow-sensitive", Type: "bool", Desc: "skip the credential-file scan (allow .env / .npmrc / .aws/credentials / etc. in the publish payload)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } path := strings.TrimSpace(rctx.Str("path")) if path == "" { return appsValidationParamError("--path", "--path is required") } if rctx.Bool("allow-sensitive") { return nil } candidates, err := walkHTMLPublishCandidates(rctx.FileIO(), path) if err != nil { return nil } var hits []string for _, c := range candidates { if isSensitiveCandidate(path, c) { hits = append(hits, c.RelPath) } } if len(hits) > 0 { return sensitiveCandidatesError(hits) } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) path := strings.TrimSpace(rctx.Str("path")) dry := common.NewDryRunAPI() dry.Desc("Upload tar.gz + publish HTML (multipart, returns url)") dry.POST(fmt.Sprintf("%s/apps/%s/upload_and_release_html_code", apiBasePath, validate.EncodePathSegment(appID))). Set("content_type", "multipart/form-data") candidates, err := walkHTMLPublishCandidates(rctx.FileIO(), path) if err != nil { dry.Set("path_error", err.Error()) return dry } if err := ensureIndexHTML(candidates); err != nil { dry.Set("validation_error", err.Error()) } dry.Set("file_count", len(candidates)) var totalSize int64 names := make([]string, 0, len(candidates)) for _, c := range candidates { totalSize += c.Size names = append(names, c.RelPath) } dry.Set("total_size_bytes", totalSize) dry.Set("files", names) if rctx.Bool("allow-sensitive") { var waived []string for _, c := range candidates { if isSensitiveCandidate(path, c) { waived = append(waived, c.RelPath) } } if len(waived) > 0 { dry.Set("sensitive_waived", waived) dry.Set("sensitive_waived_summary", fmt.Sprintf("%d credential file(s) included because --allow-sensitive is set", len(waived))) } } return dry }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { spec := appsHTMLPublishSpec{ AppID: strings.TrimSpace(rctx.Str("app-id")), Path: strings.TrimSpace(rctx.Str("path")), } client := appsHTMLPublishAPI{runtime: rctx} out, err := runHTMLPublish(ctx, rctx.FileIO(), client, spec) if err != nil { return err } rctx.OutFormat(out, nil, func(w io.Writer) { if url, ok := out["url"].(string); ok && url != "" { fmt.Fprintf(w, "url: %s\n", url) } }) return nil }, }
AppsHTMLPublish packs --path as tar.gz and uploads + publishes via one multipart POST.
var AppsInit = common.Shortcut{ Service: appsService, Command: "+init", Description: "Initialize a Miaoda app's code and local development environment", Risk: "write", Tips: []string{ "Example: lark-cli apps +init --app-id <app_id> --dir <dir>", "Example: lark-cli apps +init --app-id <app_id> --dir <dir> --dry-run", }, Scopes: []string{}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID"}, {Name: "dir", Desc: "clone target directory; absolute or relative path (default ./<app-id>)"}, {Name: "template", Desc: "code-init template for an empty repo; optional — if omitted, derived from the app's tech stack"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) template := resolveTemplate(rctx, appID) dry := common.NewDryRunAPI(). Desc("Initialize Miaoda app code (credential-init, clone, checkout, npx code-init, optional commit/push)"). Set("credential_init", fmt.Sprintf("apps +git-credential-init --app-id %s --format json", appID)). Set("checkout", "git checkout "+defaultInitBranch). Set("scaffold", fmt.Sprintf("empty repo: npx -y --prefer-online %s app init --template %s --app-id %s; non-empty: npx -y --prefer-online %s app sync + .spark/meta.json app_id patch + conditional skills sync --local", miaodaCLIPkg, template, appID, miaodaCLIPkg)). Set("commit_push", "conditional: git add -A + commit + push origin "+defaultInitBranch+" when the working tree has changes"). Set("template", template). Set("env_pull", fmt.Sprintf("apps +env-pull --app-id %s --project-path <clone_path> --format json (after successful init)", appID)) dir, err := resolveTargetPath(rctx, appID) if err != nil { dry.Set("dir_error", err.Error()) dir = defaultCloneDir(appID) } else if isAlreadyInitialized(dir) { dry.Set("already_initialized", true) } else if e := ensureEmptyDir(dir); e != nil { dry.Set("dir_error", e.Error()) } dry.Set("clone", fmt.Sprintf("git clone -- <repository_url-from-credential-init> %s", dir)) dry.Set("clone_path", dir) return dry }, Execute: appsInitExecute, }
AppsInit initializes a Miaoda app's code and local development environment.
var AppsList = common.Shortcut{ Service: appsService, Command: "+list", Description: "List Miaoda apps visible to the calling user (cursor pagination)", Risk: "read", Tips: []string{ "Example: lark-cli apps +list", "Example: lark-cli apps +list --keyword <keyword>", "Tip: filter fields with --jq, e.g. -q '.data.items[].app_id'", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "keyword", Desc: "fuzzy match on app name"}, {Name: "ownership", Desc: "ownership filter: all (created by me + shared with me) | mine | shared", Enum: []string{"all", "mine", "shared"}}, {Name: "app-type", Desc: "app type filter (html or full_stack)", Enum: []string{"html", "full_stack"}}, {Name: "page-size", Type: "int", Default: "20", Desc: "page size"}, {Name: "page-token", Desc: "pagination cursor from previous response"}, }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET(apiBasePath + "/apps"). Desc("List Miaoda apps"). Params(buildAppsListParams(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("GET", apiBasePath+"/apps", buildAppsListParams(rctx), nil) if err != nil { return err } rawItems, _ := data["items"].([]interface{}) items := make([]interface{}, 0, len(rawItems)) for _, item := range rawItems { m, ok := item.(map[string]interface{}) if !ok { items = append(items, item) continue } out := make(map[string]interface{}, len(m)) for k, v := range m { if k == "icon_url" || k == "created_at" { continue } out[k] = v } items = append(items, out) } data["items"] = items rctx.OutFormat(data, nil, func(w io.Writer) { rows := make([]map[string]interface{}, 0, len(items)) for _, item := range items { m, ok := item.(map[string]interface{}) if !ok { continue } rows = append(rows, map[string]interface{}{ "app_id": m["app_id"], "name": m["name"], "is_published": m["is_published"], "online_url": m["online_url"], "updated_at": m["updated_at"], }) } output.PrintTable(w, rows) }) return nil }, }
AppsList lists Miaoda apps visible to the calling user (cursor pagination).
Supports name fuzzy match (--keyword), ownership-dimension filter (--ownership: all / mine / shared), and app-type filter (--app-type). See lark-apps SKILL.md for when an agent should use this to resolve an app_id from a user-supplied name (only when the user named an app and a downstream op needs its app_id — never unconditional enumeration).
var AppsReleaseCreate = common.Shortcut{ Service: appsService, Command: "+release-create", Description: "Create a release for a Miaoda app (returns release_id for status polling)", Risk: "write", Tips: []string{ "Example: lark-cli apps +release-create --app-id <app_id>", "Example: lark-cli apps +release-create --app-id <app_id> --branch sprint/default --dry-run", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, {Name: "branch", Desc: "release branch (server uses default if omitted)"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) branch := strings.TrimSpace(rctx.Str("branch")) dry := common.NewDryRunAPI() dry.POST(fmt.Sprintf(releaseCreatePath, validate.EncodePathSegment(appID))). Desc("Create a release"). Body(buildPublishBody(branch)) return dry }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) branch := strings.TrimSpace(rctx.Str("branch")) path := fmt.Sprintf(releaseCreatePath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("POST", path, nil, buildPublishBody(branch)) if err != nil { return withAppsHint(err, "if the push was rejected (non-fast-forward), sync first with `git pull --rebase origin sprint/default` then retry; inspect the failure via `lark-cli apps +release-get --app-id "+appID+" --release-id <release_id>`") } out := map[string]interface{}{ "release_id": common.GetString(data, "release_id"), "status": common.GetString(data, "status"), } rctx.OutFormat(out, nil, func(w io.Writer) { fmt.Fprintf(w, "release_id: %s\nstatus: %s\n", out["release_id"], out["status"]) }) return nil }, }
AppsReleaseCreate creates a release for a Miaoda app.
var AppsReleaseGet = common.Shortcut{ Service: appsService, Command: "+release-get", Description: "Get a single release's status/detail by release ID", Risk: "read", Tips: []string{ "Example: lark-cli apps +release-get --app-id <app_id> --release-id <release_id>", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, {Name: "release-id", Desc: "release ID (the release_id returned by +release-create)", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if strings.TrimSpace(rctx.Str("release-id")) == "" { return appsValidationParamError("--release-id", "--release-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) releaseID := strings.TrimSpace(rctx.Str("release-id")) dry := common.NewDryRunAPI() dry.GET(fmt.Sprintf(releaseGetPath, validate.EncodePathSegment(appID), validate.EncodePathSegment(releaseID))). Desc("Get release detail") return dry }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) releaseID := strings.TrimSpace(rctx.Str("release-id")) path := fmt.Sprintf(releaseGetPath, validate.EncodePathSegment(appID), validate.EncodePathSegment(releaseID)) data, err := rctx.CallAPITyped("GET", path, nil, nil) if err != nil { return withAppsHint(err, "if the release_id is unknown or invalid, list this app's releases with `lark-cli apps +release-list --app-id "+appID+"`") } out := data if release, ok := data["release"].(map[string]interface{}); ok { out = release } rctx.OutFormat(out, nil, func(w io.Writer) { fmt.Fprintf(w, "release_id: %v\nstatus: %v\ncreated_at: %v\nupdated_at: %v\n", out["release_id"], out["status"], out["created_at"], out["updated_at"]) if commitID, ok := out["commit_id"].(string); ok && commitID != "" { fmt.Fprintf(w, "commit_id: %s\n", commitID) } status, _ := out["status"].(string) switch status { case "finished": if url, ok := out["online_url"].(string); ok && url != "" { fmt.Fprintf(w, "online_url: %s\n", url) } case "failed": writeReleaseErrorLogTable(w, out["error_logs"]) } }) return nil }, }
AppsReleaseGet fetches a single release's detail by release ID.
var AppsReleaseList = common.Shortcut{ Service: appsService, Command: "+release-list", Description: "List a Miaoda app's release history (most recent first)", Risk: "read", Tips: []string{ "Example: lark-cli apps +release-list --app-id <app_id>", "Tip: filter fields with --jq, e.g. -q '.data.releases[].release_id'", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "Miaoda app ID", Required: true}, {Name: "status", Enum: []string{"publishing", "finished", "failed"}, Desc: "filter by release status: publishing | finished | failed"}, {Name: "page-size", Type: "int", Default: "20", Desc: "page size (max 500)"}, {Name: "page-token", Desc: "pagination cursor from a previous response"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) status := strings.TrimSpace(rctx.Str("status")) pageSize := rctx.Int("page-size") pageToken := strings.TrimSpace(rctx.Str("page-token")) dry := common.NewDryRunAPI() dry.GET(fmt.Sprintf(releaseListPath, validate.EncodePathSegment(appID))). Desc("List release history"). Params(buildReleaseListQuery(status, pageSize, pageToken)) return dry }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) status := strings.TrimSpace(rctx.Str("status")) pageSize := rctx.Int("page-size") pageToken := strings.TrimSpace(rctx.Str("page-token")) path := fmt.Sprintf(releaseListPath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("GET", path, buildReleaseListQuery(status, pageSize, pageToken), nil) if err != nil { return withAppsHint(err, appIDListHint) } releases, _ := data["releases"].([]interface{}) rctx.OutFormat(data, nil, func(w io.Writer) { rows := make([]map[string]interface{}, 0, len(releases)) for _, it := range releases { m, ok := it.(map[string]interface{}) if !ok { continue } rows = append(rows, map[string]interface{}{ "release_id": m["release_id"], "status": m["status"], "created_at": m["created_at"], "updated_at": m["updated_at"], }) } output.PrintTable(w, rows) }) return nil }, }
AppsReleaseList lists a Miaoda app's release history (most recent first).
var AppsSessionCreate = common.Shortcut{ Service: appsService, Command: "+session-create", Description: "Create a session under a Miaoda app", Risk: "write", Tips: []string{ "Example: lark-cli apps +session-create --app-id <app_id>", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST(sessionsPath(rctx.Str("app-id"))). Desc("Create a session under a Miaoda app") }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("POST", sessionsPath(rctx.Str("app-id")), nil, nil) if err != nil { return withAppsHint(err, appIDListHint) } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "session created: %s\n", common.GetString(data, "session_id")) }) return nil }, }
AppsSessionCreate creates a new session under an existing Miaoda app.
var AppsSessionGet = common.Shortcut{ Service: appsService, Command: "+session-get", Description: "Read a session's current status, queued turns, and latest turn", Risk: "read", Tips: []string{ "Example: lark-cli apps +session-get --app-id <app_id> --session-id <session_id>", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "session-id", Desc: "session ID", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if strings.TrimSpace(rctx.Str("session-id")) == "" { return appsValidationParamError("--session-id", "--session-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET(sessionPath(rctx.Str("app-id"), rctx.Str("session-id"))). Desc("Read a session's status") }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("GET", sessionPath(rctx.Str("app-id"), rctx.Str("session-id")), nil, nil) if err != nil { return withAppsHint(err, "if the session_id is unknown or invalid, list this app's sessions with `lark-cli apps +session-list --app-id "+strings.TrimSpace(rctx.Str("app-id"))+"`") } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "session: %s\n", common.GetString(data, "session_id")) fmt.Fprintf(w, "active: %v streaming: %v\n", data["is_active"], data["is_streaming"]) if lt, ok := data["latest_turn"].(map[string]interface{}); ok { fmt.Fprintf(w, "latest turn: %v (%v)\n", lt["turn_id"], lt["status"]) } fmt.Fprintf(w, "queued: %v\n", data["queued_count"]) fmt.Fprintf(w, "next poll after: %vms\n", data["next_poll_after_ms"]) }) return nil }, }
AppsSessionGet reads a session's current status, queued turns, and latest turn. Single-shot: the caller drives polling using next_poll_after_ms.
var AppsSessionList = common.Shortcut{ Service: appsService, Command: "+session-list", Description: "List sessions under a Miaoda app (cursor pagination)", Risk: "read", Tips: []string{ "Example: lark-cli apps +session-list --app-id <app_id>", "Tip: filter fields with --jq, e.g. -q '.data.sessions[].session_id'", }, Scopes: []string{"spark:app:read"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "page-size", Type: "int", Default: "20", Desc: "page size (max 50)"}, {Name: "page-token", Desc: "pagination cursor from previous response"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). GET(sessionsPath(rctx.Str("app-id"))). Desc("List sessions under a Miaoda app"). Params(buildSessionListParams(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("GET", sessionsPath(rctx.Str("app-id")), buildSessionListParams(rctx), nil) if err != nil { return withAppsHint(err, appIDListHint) } sessions, _ := data["sessions"].([]interface{}) rctx.OutFormat(data, nil, func(w io.Writer) { rows := make([]map[string]interface{}, 0, len(sessions)) for _, item := range sessions { m, ok := item.(map[string]interface{}) if !ok { continue } rows = append(rows, map[string]interface{}{ "session_id": m["session_id"], "name": m["name"], "is_active": m["is_active"], "updated_at": m["updated_at"], }) } output.PrintTable(w, rows) }) return nil }, }
AppsSessionList lists sessions under a Miaoda app (cursor pagination, single page).
var AppsSessionStop = common.Shortcut{ Service: appsService, Command: "+session-stop", Description: "Stop (interrupt) the running turn of a session", Risk: "write", Tips: []string{ "Example: lark-cli apps +session-stop --app-id <app_id> --session-id <session_id> --turn-id <turn_id>", }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "session-id", Desc: "session ID", Required: true}, {Name: "turn-id", Desc: "turn ID to stop (from +session-get latest_turn.turn_id)", Required: true}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } if strings.TrimSpace(rctx.Str("session-id")) == "" { return appsValidationParamError("--session-id", "--session-id is required") } if strings.TrimSpace(rctx.Str("turn-id")) == "" { return appsValidationParamError("--turn-id", "--turn-id is required") } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { return common.NewDryRunAPI(). POST(stopPath(rctx.Str("app-id"), rctx.Str("session-id"))). Desc("Stop the running turn of a session"). Body(buildStopBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { data, err := rctx.CallAPITyped("POST", stopPath(rctx.Str("app-id"), rctx.Str("session-id")), nil, buildStopBody(rctx)) if err != nil { return withAppsHint(err, sessionStopHint) } turnID := strings.TrimSpace(rctx.Str("turn-id")) rctx.OutFormat(data, nil, func(w io.Writer) { stopped, _ := data["stopped"].(bool) if stopped { fmt.Fprintf(w, "stopped turn %s. %v\n", turnID, data["message"]) } else { fmt.Fprintf(w, "no-op: turn %s not stopped. %v\n", turnID, data["message"]) } }) return nil }, }
AppsSessionStop interrupts the RUNNING turn of a session. No-op if the turn is queued or already finished. Does not close the session.
var AppsUpdate = common.Shortcut{ Service: appsService, Command: "+update", Description: "Partially update a Miaoda app (only provided fields are sent)", Risk: "write", Tips: []string{ `Example: lark-cli apps +update --app-id <app_id> --name "新名称"`, `Example: lark-cli apps +update --app-id <app_id> --description "..."`, }, Scopes: []string{"spark:app:write"}, AuthTypes: []string{"user"}, HasFormat: true, Flags: []common.Flag{ {Name: "app-id", Desc: "app ID", Required: true}, {Name: "name", Desc: "new app display name"}, {Name: "description", Desc: "new app description"}, }, Validate: func(ctx context.Context, rctx *common.RuntimeContext) error { if strings.TrimSpace(rctx.Str("app-id")) == "" { return appsValidationParamError("--app-id", "--app-id is required") } body := buildAppsUpdateBody(rctx) if len(body) == 0 { return appsValidationError("provide at least one of --name or --description"). WithParams( appsInvalidParam("--name", "provide at least one of --name or --description"), appsInvalidParam("--description", "provide at least one of --name or --description"), ) } return nil }, DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI { appID := strings.TrimSpace(rctx.Str("app-id")) return common.NewDryRunAPI(). PATCH(fmt.Sprintf("%s/apps/%s", apiBasePath, validate.EncodePathSegment(appID))). Desc("Update a Miaoda app"). Body(buildAppsUpdateBody(rctx)) }, Execute: func(ctx context.Context, rctx *common.RuntimeContext) error { appID := strings.TrimSpace(rctx.Str("app-id")) path := fmt.Sprintf("%s/apps/%s", apiBasePath, validate.EncodePathSegment(appID)) data, err := rctx.CallAPITyped("PATCH", path, nil, buildAppsUpdateBody(rctx)) if err != nil { return withAppsHint(err, appIDListHint) } rctx.OutFormat(data, nil, func(w io.Writer) { fmt.Fprintf(w, "updated: %s\n", common.GetString(data, "app", "app_id")) }) return nil }, }
AppsUpdate partially updates a Miaoda app's name / description.
Functions ¶
func Delete ¶ added in v1.0.51
Delete removes the file under (appID, key). A missing file is not an error.
func InstallOnApps ¶ added in v1.0.51
InstallOnApps attaches hidden, apps-domain commands that are not regular shortcuts. git-credential-helper must speak Git's stdin/stdout protocol directly, so it intentionally does not use the shortcut JSON envelope.
func List ¶ added in v1.0.51
List returns the keys stored under appID, skipping subdirectories and names that fail to unescape or validate after decoding. A missing app directory yields an empty list.
func Read ¶ added in v1.0.51
Read returns the bytes stored under (appID, key). A missing file returns (nil, nil). Content is opaque — callers own the format. Note: an empty stored value is indistinguishable from a missing key (both yield nil), so this store is unsuitable as an existence flag.
func Write ¶ added in v1.0.51
Write atomically stores data under (appID, key): file 0600, dir 0700. It is a create-or-replace upsert for that key; content is written verbatim in plaintext. 0600 only guards against other local OS users — it does not protect against this user's processes, backups, or synced folders. appID and key are opaque strings: any "/" is escaped into a single path segment, never treated as a directory separator.
Types ¶
This section is empty.
Source Files
¶
- apps_access_scope_get.go
- apps_access_scope_set.go
- apps_chat.go
- apps_create.go
- apps_db_env_create.go
- apps_db_execute.go
- apps_db_table_get.go
- apps_db_table_list.go
- apps_env_pull.go
- apps_errors.go
- apps_html_publish.go
- apps_init.go
- apps_list.go
- apps_release_common.go
- apps_release_create.go
- apps_release_get.go
- apps_release_list.go
- apps_session_create.go
- apps_session_get.go
- apps_session_list.go
- apps_session_stop.go
- apps_update.go
- command_runner.go
- common.go
- db_common.go
- git_credential.go
- git_credential_storage.go
- html_publish_client.go
- html_publish_tarball.go
- sensitive_paths.go
- shortcuts.go
- storage.go
- walk_html_publish_candidates.go