Skip to content
7 changes: 4 additions & 3 deletions shortcuts/apps/apps_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/larksuite/cli/shortcuts/common"
)

const createHint = "verify --app-type is html or full_stack and --name is non-empty; if this is a permission error, confirm your account can create apps"
const createHint = "verify --app-type is html, frontend or full_stack and --name is non-empty; if this is a permission error, confirm your account can create apps"

// AppsCreate creates a new app.
var AppsCreate = common.Shortcut{
Expand All @@ -23,14 +23,15 @@ var AppsCreate = common.Shortcut{
Risk: "write",
Tips: []string{
`Example: lark-cli apps +create --name "审批系统" --app-type full_stack`,
`Example: lark-cli apps +create --name "工具页" --app-type frontend --description "纯前端工具"`,
`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: "app-type", Desc: "app type", Required: true, Enum: []string{"html", "frontend", "full_stack"}},
{Name: "description", Desc: "app description"},
{Name: "icon-url", Desc: "app icon URL (server uses default if omitted)"},
},
Expand Down Expand Up @@ -59,7 +60,7 @@ var AppsCreate = common.Shortcut{
}

func buildAppsCreateBody(rctx *common.RuntimeContext) map[string]interface{} {
// --app-type is constrained to the lowercase enum (html / full_stack) by the
// --app-type is constrained to the lowercase enum (html / frontend / full_stack) by the
// flag's Enum, so send it through verbatim. Legacy uppercase compatibility is
// a server concern and is intentionally not surfaced by the CLI.
agent := envvars.AgentName()
Expand Down
17 changes: 16 additions & 1 deletion shortcuts/apps/apps_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func TestAppsCreate_RequiresAppType(t *testing.T) {
}

// TestAppsCreate_RejectsInvalidAppType pins that --app-type is a strict
// lowercase enum (html / full_stack). Unknown values and legacy uppercase are
// lowercase enum (html / frontend / full_stack). Unknown values and legacy uppercase are
// both rejected by the flag's Enum — the CLI does not normalize case; legacy
// uppercase compatibility is a server-side concern, not surfaced by the client.
func TestAppsCreate_RejectsInvalidAppType(t *testing.T) {
Expand Down Expand Up @@ -363,3 +363,18 @@ func TestAppsCreate_AgentEnvVarNotSet(t *testing.T) {
t.Fatalf("source_agent should not be present when env var is unset: %v", sent)
}
}

// TestAppsCreate_AcceptsFrontend pins that --app-type frontend is a valid
// enum value and flows through to the request body as "frontend" verbatim.
func TestAppsCreate_AcceptsFrontend(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "frontend", "--dry-run", "--as", "user"},
factory, stdout); err != nil {
t.Fatalf("frontend dry-run err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"app_type": "frontend"`) {
t.Fatalf("expected app_type frontend in body, got %s", got)
}
}
9 changes: 8 additions & 1 deletion shortcuts/apps/apps_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const (
)

const (
miaodaCLIPkg = "@lark-apaas/miaoda-cli@latest"
miaodaCLIPkg = "@lark-apaas/miaoda-cli@0.1.24-alpha.800d445"
npmRegistry = "https://registry.npmmirror.com"
metaRelPath = ".spark/meta.json"
steeringRelPath = ".agent/skills/steering"
Expand Down Expand Up @@ -86,6 +86,10 @@ var appTypePolicies = map[string]appTypePolicy{
// no startup env vars to pull, no steering skills to sync, and no app sync.
"modern_html": {skipInstall: true, skipEnvPull: true, skipSkillsSync: true, skipAppSync: true},
"html": {skipInstall: true, skipEnvPull: true, skipSkillsSync: true, skipAppSync: true},
// frontend (vite-react, a buildable front-end app) is intentionally NOT
// listed here: it takes the zero-value policy (install deps, pull env, sync
// skills) like full_stack, since it needs a build step — it is not a static
// HTML site and must not skip those steps.
}

// policyForAppType returns the +init control strategy for appType. Unlisted
Expand Down Expand Up @@ -438,6 +442,9 @@ func runScaffold(ctx context.Context, dir, appID, appType, sourcePath string) (s
// --skip-install is appended per the app_type's policy (see appTypePolicy):
// types whose policy sets skipInstall (e.g. modern_html) skip the dependency
// install; others run it as usual.
// appType is forwarded verbatim (including "frontend") — the CLI does not
// translate the app type; mapping the app type to a concrete tech stack is the
// downstream tool's responsibility.
func scaffoldInitArgs(appType, appID, sourcePath string) []string {
base := []string{"-y", "--prefer-online", "--registry", npmRegistry, miaodaCLIPkg, "app", "init"}
at := appType
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/apps/apps_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var AppsList = common.Shortcut{
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: "app-type", Desc: "app type filter (html, frontend or full_stack)", Enum: []string{"html", "frontend", "full_stack"}},
{Name: "page-size", Type: "int", Default: "20", Desc: "page size"},
{Name: "page-token", Desc: "pagination cursor from previous response"},
},
Expand Down
2 changes: 1 addition & 1 deletion shortcuts/apps/apps_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
// queryAppType fetches the app's type string from the server via
// GET /open-apis/spark/v1/apps/{identifier}. The identifier can be either
// an app_id or a meta_token — the server resolves both. The server returns
// uppercase app_type values ("HTML", "FULL_STACK", "MODERN_HTML");
// uppercase app_type values ("HTML", "FRONTEND", "FULL_STACK", "MODERN_HTML");
// this function normalizes to lowercase. Returns an error when the API
// is unavailable or the response is malformed — callers must not proceed
// with a fallback type to avoid creating the wrong project scaffold.
Expand Down
14 changes: 10 additions & 4 deletions skills/lark-apps/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,28 @@ lark-cli auth login --domain apps

新建必先定 **app_type** 和**开发方式**两件正交的事;修改已有先按「app_id 获取」指认到 app,指认不到就问用户,不擅自 `+create`。开发方式(本地 vs 云端)只看用户对"谁来写代码"的偏好,与应用复杂度、要不要数据库无关。

**app_type 三类边界**(先判"要不要把数据存到服务端",再判"纯展示还是有交互"):

| 信号 | 判定 |
|---|---|
| 静态展示 / 单页 / PPT/deck / demo / 落地页 / 仪表盘 / UI mockup / 可交互原型 / 线框图 / 视觉探索 / 无后端状态 | `app_type=html`,加载 [`creative-design/creative-design.md`](creative-design/creative-design.md)(含完整开发与发布流程) |
| 登录 / 数据库 / 持久化 / 多人协作 / 增删改查 / 报名 / 投票 / 站会 / OKR / 泛称"系统·工具" | `app_type=full_stack` |
| 含数据库 / 后端持久化:登录 / 增删改查 / 报名·投票·站会存记录 / 多人协作 / 泛称"系统·工具"且明确要存数据 | `app_type=full_stack` |
| 纯静态展示(给人"看"的物料,无 JS 交互):PPT/deck / demo / 落地页 / 海报 / UI mockup / 线框图 / 静态仪表盘 / 视觉探索 | `app_type=html`,加载 [`creative-design/creative-design.md`](creative-design/creative-design.md)(含完整开发与发布流程) |
| 有 JS 交互但无数据库(给人"用"的前端应用):可交互原型 / SPA / 表单校验 / 动态计算 / 调用外部 API / 泛称"工具·系统"但未明确要存数据 | `app_type=frontend`(**默认倾向**:用户未明确提出数据库需求时默认引导 frontend,不默认 full_stack) |
| 类型模糊(尤其"要不要存数据"不清) | **追问**,话术偏向 frontend,例:"看起来是个前端应用,需要保存数据吗?";确认要存数据再转 full_stack,确认纯展示再转 html |
| 用户要自己写 / 本地 IDE·code agent / 拉源码到本地 / 交研发 | 本地开发,读 [`lark-apps-local-dev.md`](references/lark-apps-local-dev.md) |
| 让妙搭 AI 云端生成 / 对话式 / 自己不碰代码 | 云端会话,读 [`lark-apps-cloud-dev.md`](references/lark-apps-cloud-dev.md) |
| 未表达"谁来写"偏好 | **必须先问**(本地代码开发 vs 云端 AI 生成);选定前不擅自选边、不暗示默认,不得以"需求不模糊"为由跳过提问直接 `+init` / `git clone` / `+session-create` / 首轮 `+chat` |
| 修改已有 + 当前目录是 `.spark/meta.json` 项目 | 直接继续本地按意图路由,不必问也不必判云端 |
| 修改已有 + 有云端偏好 | 云端会话;未表达偏好且非本地项目 → 默认本地;判不准先问 |

**类型升级**:`frontend` 应用后续需要数据库/后端能力时,本地 CLI 不提供类型升级;引导用户到云端会话(打开 `https://miaoda.feishu.cn/app/{app_id}`),用自然语言描述后端需求(如"给这个应用加登录和数据存储")即可触发升级,无需特殊指令。

## 发布态护栏

- **发布意图判定**:用户要"可访问 / 线上 / 分享 / 新链接 / 上线" = 发布意图,先走发布链路、确认完成再给链接。
- 完成 ≠ 发布:云端会话完成 / `+list is_published=true` 都不代表最新内容已部署。
- 开发态链接 `https://miaoda.feishu.cn/app/{app_id}`(full_stack 应用):进应用编辑/开发态、管理与继续开发应用的入口。创意模式(html)应用开发态和发布态是同一个链接,无需额外提供开发态链接。
- 发布态链接来源:`+release-get` 轮询 `finished` 给 `online_url` / `failed` 给 `error_logs`(html full_stack 统一走 `+release-get`)。
- 开发态链接 `https://miaoda.feishu.cn/app/{app_id}`(full_stack / frontend 应用):进应用编辑/开发态、管理与继续开发应用的入口,也是 frontend 升级为 full_stack 的入口(云端会话)。创意模式(html)应用开发态和发布态是同一个链接,无需额外提供开发态链接。
- 发布态链接来源:`+release-get` 轮询 `finished` 给 `online_url` / `failed` 给 `error_logs`(html / frontend / full_stack 统一走 `+release-get`)。
- html 应用的主链路是创意模式开发方式:按 [`lark-apps-local-dev.md`](references/lark-apps-local-dev.md) 初始化仓库、在仓库内产出 HTML 及关联文件,并通过 git commit / git push / `+release-create` / `+release-get` 发布部署。任何 git 操作(clone / pull / push)报错时,先执行 `lark-cli apps +git-credential-init --app-id <app_id> --as user` 刷新本地 Git 凭证,再重试原 git 命令。如果刷新凭证也失败,**停止并向用户报告**:原始 git 错误、凭证刷新失败原因,以及是否可能是当前环境(操作系统、沙箱)限制导致(如 macOS Keychain 在沙箱中不可用、Linux 加密文件目录不可写等)。不要改走 `+html-publish`,也不要把 `+html-publish` 当作本地开发链路的 fallback。
- 创意模式(html)应用的链接格式为 `https://{租户域名}/page/{meta_token}`,**开发态和发布态是同一个链接**(区别于 full_stack 应用两者分开)。此链接形似飞书文档链接。`+get --app-id <meta_token>` 可获取应用信息(含 `app_id`),`+get --app-id <app_id>` 可获取 `meta_token`。看到 `/page/xxx` 链接时,它是妙搭创意模式应用,不要当成飞书文档跳过。

Expand Down
7 changes: 5 additions & 2 deletions skills/lark-apps/references/lark-apps-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
## 命令骨架

- 必填:`--name`、`--app-type`。
- app type 语义取值为 `html` / `full_stack`;CLI 会把输入归一成小写后校验
- app type 取值为小写 `html` / `frontend` / `full_stack`;框架按枚举精确校验(不做大小写归一),非法值直接报错
- 可选:`--description`、`--icon-url`。

## 示例

```bash
lark-cli apps +create --name "客户调研问卷" --app-type html

lark-cli apps +create --name "JSON 格式化工具" --app-type frontend \
--description "纯前端交互工具,无需数据库"

lark-cli apps +create --name "审批系统" --app-type full_stack \
--description "部门审批系统,支持登录、提交申请、多级审批"

Expand All @@ -35,5 +38,5 @@ lark-cli apps +create --name "Demo" --app-type html --dry-run

创建后按用户路径继续:

- 本地应用开发(含 html full_stack):读 [`lark-apps-local-dev.md`](lark-apps-local-dev.md)。
- 本地应用开发(含 html / frontend / full_stack):读 [`lark-apps-local-dev.md`](lark-apps-local-dev.md)。
- 云端 Agent 生成/迭代:读 [`lark-apps-cloud-dev.md`](lark-apps-cloud-dev.md)。
2 changes: 1 addition & 1 deletion skills/lark-apps/references/lark-apps-get.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ lark-cli apps +get --app-id app_xxx -q '.data.app.app_type'
| 字段 | 类型 | 说明 |
|------|------|------|
| `app_id` | string | 应用唯一标识 |
| `app_type` | string | 应用类型(如 HTML、FULL_STACK、MODERN_HTML) |
| `app_type` | string | 应用类型(如 HTML、FRONTEND、FULL_STACK、MODERN_HTML) |
| `name` | string | 应用显示名称 |
| `description` | string | 应用功能说明 |
| `icon_url` | string | 应用图标 URL |
Expand Down
2 changes: 1 addition & 1 deletion skills/lark-apps/references/lark-apps-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

- 支持 `--keyword` 按应用名模糊搜索。
- `--ownership` 枚举:`all` / `mine` / `shared`(默认 `all` = 我创建的 + 共享给我的;`mine` = 仅我创建;`shared` = 仅共享给我)。
- `--app-type` 枚举:`html` / `full_stack`。
- `--app-type` 枚举:`html` / `frontend` / `full_stack`。
- 分页:`--page-size` 默认 20,`--page-token` 传上一页 cursor。

## 示例
Expand Down
28 changes: 27 additions & 1 deletion skills/lark-apps/references/lark-apps-local-dev.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# lark-apps 本地开发

适用:用户要把妙搭应用(full_stack 或 html)源码拉到本地,用本地 code agent/IDE 开发、调试数据库,再发布。
适用:用户要把妙搭应用(full_stack、frontend 或 html)源码拉到本地,用本地 code agent/IDE 开发、再发布。其中调试数据库仅 full_stack 适用(frontend / html 无数据库)

## 新建 vs 已有应用

Expand Down Expand Up @@ -36,6 +36,32 @@ git push origin sprint/default
lark-cli apps +release-create --as user --app-id app_xxx --branch sprint/default
```

### frontend

纯前端应用(vite-react,无数据库)。流程与 full_stack 基本一致——`+init` 装依赖、`npm run dev`、commit/push/release——差别是无 `+db-*` 调库步骤。后续需要数据库/后端能力时不在本地升级,按 SKILL.md「类型升级」引导到云端会话。

```bash
# 新建 frontend 应用
lark-cli apps +create --as user --name "JSON 格式化工具" --app-type frontend \
--description "纯前端交互工具,无需数据库"

# 初始化本地仓库(--dir 取值见下方「领域规则」,勿照抄此处示例值)
lark-cli apps +init --as user --app-id app_xxx --dir ./json-tool

# 进入仓库后按项目脚手架启动(vite-react)
cd ./json-tool
npm install
npm run dev

# 开发完成后:提交本次改动 -> git push origin sprint/default -> +release-create
git add <本次开发的文件>
git commit -m "feat: ..."
git push origin sprint/default
lark-cli apps +release-create --as user --app-id app_xxx --branch sprint/default
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 发布是异步的:用 +release-get 轮询到 status=finished 才算部署完成、拿到 online_url
lark-cli apps +release-get --as user --app-id app_xxx --release-id <上一步返回的 release_id>
```

### html

#### 首次开发(无 app,无代码)
Expand Down
2 changes: 1 addition & 1 deletion skills/lark-apps/references/lark-apps-release-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## 何时用

用于把应用的代码分支推进到发布流程(html full_stack 统一走此入口)。
用于把应用的代码分支推进到发布流程(html / frontend / full_stack 统一走此入口)。

## 命令骨架

Expand Down
2 changes: 1 addition & 1 deletion tests/cli_e2e/apps/apps_create_dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func TestAppsCreateDryRun(t *testing.T) {
})

t.Run("RejectsLegacyUppercaseAppType", func(t *testing.T) {
// --app-type is a strict lowercase enum (html / full_stack); the CLI does
// --app-type is a strict lowercase enum (html / frontend / full_stack); the CLI does
// not normalize case. Legacy uppercase "HTML" is rejected — backend
// compatibility for legacy values is a server concern the client does not
// surface.
Expand Down
6 changes: 3 additions & 3 deletions tests/cli_e2e/apps/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- Live coverage: file and role workflows are fixture-gated and skipped by default CI. File upload covers absolute-path upload, metadata readback, and cleanup; role workflows cover role lifecycle and member mutations with cleanup.

## Summary
- `TestAppsCreateDryRun`: happy path with `--app-type html`, all-fields shape, rejection paths (missing name, missing app-type, invalid app-type, legacy uppercase `HTML`). `--app-type` is a strict lowercase enum (`html`/`full_stack`); the CLI does not normalize case — legacy uppercase compatibility is a server concern.
- `TestAppsCreateDryRun`: happy path with `--app-type html`, all-fields shape, rejection paths (missing name, missing app-type, invalid app-type, legacy uppercase `HTML`). `--app-type` is a strict lowercase enum (`html`/`frontend`/`full_stack`); the CLI does not normalize case — legacy uppercase compatibility is a server concern.
- `TestAppsUpdateDryRun`: partial-field PATCH semantics; `--app-id` and at-least-one-field validation.
- `TestAppsListDryRun`: default `page_size=20`; empty `--page-token` omitted; negative size passed through to server (no client-side bound check); `--keyword`/`--ownership`/`--app-type` pass-through + empty-omission; invalid `--ownership` and legacy uppercase `--app-type` enum rejection.
- `TestAppsAccessScopeSetDryRun`: CLI input `specific`/`public`/`tenant` -> server enum `Range`/`All`/`Tenant`; `apply_config.approvers` shape; four mutex rejection paths.
Expand All @@ -31,9 +31,9 @@ Blocked: General app create live E2E is intentionally not implemented yet. Apps

| Status | Cmd | Type | Testcase | Key parameter shapes | Notes / uncovered reason |
| --- | --- | --- | --- | --- | --- |
| ✓ | apps +create | shortcut | apps_create_dryrun_test.go::TestAppsCreateDryRun | `--name`, `--app-type` (required, case-sensitive, `html`/`full_stack`), `--description`, `--icon-url` | live blocked: no +delete to clean up |
| ✓ | apps +create | shortcut | apps_create_dryrun_test.go::TestAppsCreateDryRun | `--name`, `--app-type` (required, case-sensitive, `html`/`frontend`/`full_stack`), `--description`, `--icon-url` | live blocked: no +delete to clean up |
| ✓ | apps +update | shortcut | apps_update_dryrun_test.go::TestAppsUpdateDryRun | `--app-id`; at least one of `--name`/`--description` | live blocked: no +delete |
| ✓ | apps +list | shortcut | apps_list_dryrun_test.go::TestAppsListDryRun | `--keyword`; `--ownership` (enum all/mine/shared); `--app-type` (enum html/full_stack); `--page-size` default 20; `--page-token` cursor | live blocked: needs tenant fixtures |
| ✓ | apps +list | shortcut | apps_list_dryrun_test.go::TestAppsListDryRun | `--keyword`; `--ownership` (enum all/mine/shared); `--app-type` (enum html/frontend/full_stack); `--page-size` default 20; `--page-token` cursor | live blocked: needs tenant fixtures |
| ✓ | apps +access-scope-set | shortcut | apps_access_scope_set_dryrun_test.go::TestAppsAccessScopeSetDryRun | `--scope specific/public/tenant`; `--targets` JSON; `--apply-enabled --approver`; `--require-login` | live blocked: needs real open_ids |
| ✓ | apps +access-scope-get | shortcut | apps_access_scope_get_dryrun_test.go::TestAppsAccessScopeGetDryRun | `--app-id` | live blocked: depends on +access-scope-set state |
| ✓ | apps +html-publish | shortcut | apps_html_publish_dryrun_test.go::TestAppsHTMLPublishDryRun | `--app-id`, `--path` (file or directory containing `index.html`) | live blocked: real upload has side effects; no rollback API |
Expand Down
Loading