@lark-apaas/miaoda-cli 0.1.46 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,6 +52,25 @@ miaoda file ls --output json
52
52
 
53
53
  完整命令通过 `miaoda --help` 或 `miaoda <domain> --help` 查看。
54
54
 
55
+ ## 工程初始化、同步与转换
56
+
57
+ `miaoda app init / sync / migrate` 管理本地应用工程(平台调用方与本地开发共用):
58
+
59
+ - `init`:按 `--template` 或 `--app-type` 渲染模板(`full_stack → nestjs-react-fullstack`,`frontend / jspage → vite-react`)。工程已存在时不重新渲染、不覆盖业务代码,只输出就绪核验结果 `ready / problems`;`--strict` 下未就绪(meta 损坏、stack 不符、必要文件缺失、依赖未安装、上次操作失败)退出码 1。同步安装失败输出 `status=failed`、`failedStage=install`;退出码只在 `--strict` 时为 1,不带 `--strict` 时为 0(依赖由后续 `sync` 补装)。
60
+ - `sync`:按 stack 的同步规则更新平台托管内容。全栈工程的托管文件是明确清单(`scripts/dev.sh`、`dev.js`、`dev-local.js`、`build.sh`、`run.sh`、`lint.js`、`prune-smart.js`、`hooks/run-precommit.js` 与 `.githooks/pre-commit`),`scripts/` 下其他用户文件保留;连续执行无漂移。依赖安装失败输出 `status=failed`、`failedStage=install`;退出码只在 `--strict`(平台严格调用)时为 1,不带 `--strict` 时为 0,`dev.sh` 与 lark-cli `+init` 不带它。
61
+ - `migrate`(不在 help 列表中展示):工程转换原语,在原工程目录内执行。目标模板先渲染到临时模板目录(不复制用户工程),再只读预检冲突(同路径不同内容、重复插件 ID、自定义同名 script、无法确定的 package.json / tsconfig / vite.config / index.html 形态等),随后在原目录定向转换(`src/ public/ → client/`、插件迁 `server/capabilities`、配置定向适配)并安装依赖;迁移配置声明了验证步骤时再执行验证(vite-react → 全栈不做完整构建)。冲突按 `--purpose` 决定的策略处理,结果字段 `conflictPolicy` 给出本次策略:
62
+ - `stop`(`--purpose upgrade` / `history_restore`,平台调用):预检发现冲突返回 `status=conflict` 与具体路径,工程不写入;执行中才出现的冲突即停、不覆盖用户不同内容。
63
+ - `overwrite`(`--purpose manual`,默认;feida-ai 升级全栈与本机手动执行)且跨 stack 转换:冲突处按模板覆盖并继续 —— 自定义同名 script 改成平台命令,tsconfig、`server/main.ts` / `app.module.ts`、无法适配的 `index.html` 换成模板,定制的 `eslint.config.mjs` 删除,`src/ public/` 与 `client/` 下同名文件由前者覆盖。被替换的用户内容逐条列在 `overwritten`(`path` / `reason` / `with`),不备份、不自动提交,需要时从 git 历史找回。无可替换模板内容的保留用户内容并给出 `code=MIGRATE_CONFLICT_KEPT` 的 warning:自定义 `prepare` script、不同文件的重复插件 ID(两份都搬入)、未用平台 preset 的 `vite.config.ts`(只改 alias)、非 `.ts` 的 vite 配置。文件 / 目录类型不一致与 `package.json` 不可解析仍按冲突停止。
64
+ - `keep`(`--purpose manual` 且磁盘已是目标布局):冲突处保留用户在已转换工程上的改动(同样给出 `MIGRATE_CONFLICT_KEPT` warning),其余照常修复。
65
+
66
+ 不安全的符号链接在任何策略下都按冲突停止(见下)。任一阶段失败退出码 1,`failure.stage` 给出 `install` / `verify` 等阶段,现场保留、不自动回滚;例外是 `--purpose manual` 只有依赖安装失败时:结果仍是 `status=failed`、`install.status=failed`,退出码 0,由调用方补装(平台调用 `upgrade` / `history_restore` 仍为 1)。`--app-id` / `MIAODA_APP_ID` 与 meta 的 `app_id` 不一致时,沙箱内按当前应用改写 meta(复制来的工程,warning `MIGRATE_APP_ID_REWRITTEN`),本机拒绝(`MIGRATE_APP_ID_MISMATCH`)。`originalIntact=true` 仅表示失败发生在任何工程写入之前,写入后一律为 false(已写入文件见 `changes`)。磁盘已是目标布局时走 repair(补齐适配 / 安装,保留已有 `node_modules` 接续安装),不返回 NOOP;meta 已是目标 stack 不代表上次成功,以 `.spark/local/engineering-state.json` 的终态为准。放弃转换由调用方按代码版本恢复。输出包含源 / 目标 stack、模板与 CLI 版本、真实阶段结果与冲突路径;运行 readiness 由调用方核验(`readiness.status=pending_caller_verification`)。沙箱内只向工作目录就是被转换目录的 dev 进程发重启信号(在工程副本等其他目录执行不影响正在运行的应用),`restartSignalSent` 只表示发出了重启信号。执行期间的并发编辑由调用方现有互斥与调度保证。
67
+
68
+ `migrate` 不自动解引用工程外文件。迁移涉及的文件或父目录存在符号链接时,在规则执行前返回 `MIGRATE_UNSAFE_SYMLINK` 和具体 `conflicts[].path`;请改为工程拥有的普通文件或目录后重试。`html → design-html` 的 `src → .` 涉及整个工程根目录。原目录 install/build 前拒绝绝对链接、工程外链接、无法解析的链接及指向排除目录的链接;未触及且在工程内解析的相对业务链接保留,安装生成的普通 `node_modules/.bin` 相对链接允许。规则执行后、安装后及构建前再次检查链接边界。扫描失败返回 `MIGRATE_SYMLINK_CHECK_FAILED`;失败或链接冲突在安全的 `.spark/local` 写入终态,状态写路径不安全时不写入并给出 warning。该检查不是 npm 脚本的安全沙箱,不限制脚本主动访问其他绝对路径。
69
+
70
+ 临时模板目录清理失败只加入 `warnings`(`path`、系统错误 `code`、`message`),不影响 `status`。
71
+
72
+ 全栈 / design-stack 的 `postinstall` 只升级完整历史平台值(`fullstack-cli action-plugin init` 或 `npx -y @lark-apaas/fullstack-cli action-plugin init`),目标为 `npx -y @lark-apaas/fullstack-cli@latest action-plugin init`;`latest` 是渠道,不是固定版本。带追加命令、参数或用户指定版本的值保留。`migrate` 在原目录执行正常 npm 安装生命周期,包括用户脚本及依赖补丁;生命周期失败即 `install` 阶段失败,保留现场。
73
+
55
74
  ## 全栈应用的 MCP UI 检查
56
75
 
57
76
  创建 MCP UI 时,Agent 在 `server/mcp/ui` 中同时生成 `tsconfig.json` 和 `eslint.config.cjs`,修改 UI 或其 shared 依赖后主动运行独立检查。模板和 CLI 不生成 UI 配置,也不将 UI 检查接入主工程 lint/type:check。
@@ -122,6 +141,7 @@ miaoda registry add deck-slide --overwrite # 目标已存在时覆盖(
122
141
  ## 环境变量
123
142
 
124
143
  - `MIAODA_APP_ID`:默认应用 ID,等价于 `--app-id`。
144
+ - `MIAODA_MIGRATE_STEP_TIMEOUT_MS`:`app migrate` 单个依赖安装 / 构建步骤的超时(毫秒),默认 20 分钟。
125
145
  - `MIAODA_CANARY_HEADER`:HTTP 请求的 `x-tt-env` 小流量头取值。未设置时默认 `boe_miaoda_ccm`;设为空字符串则不带该头(例:`export MIAODA_CANARY_HEADER=boe_xxx`)。值形如 `ppe_xxx` 时额外带上 `x-use-ppe: 1`。
126
146
 
127
147
  ## 输出契约
@@ -40,24 +40,74 @@ function registerAppMigrate(parent) {
40
40
  // 用 addCommand(cmd, { hidden: true }) 而非 parent.command(...),因为 commander 13
41
41
  // 的 fluent `.command()` 不支持设置 hidden。
42
42
  const cmd = new commander_1.Command('migrate')
43
- .description('跨 stack 原地迁移:按 MigrateConfig 全套 apply(move + delete + 模板覆盖 + 字段 merge + set-stack)')
43
+ .description('工程转换原语:原目录内按 MigrateConfig 预检冲突 → 定向转换 → 安装;冲突按调用目的即停或按模板覆盖,失败保留现场')
44
44
  .requiredOption('--to <stack>', '目标 stack 短名')
45
- .option('--from <stack>', '源 stack;默认读 .spark/meta.json 当前 stack')
46
- .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
45
+ .option('--from <stack>', '源 stack;默认按磁盘实际布局识别(识别不了再读 .spark/meta.json)')
46
+ .option('--dir <path>', '项目目录,默认 cwd')
47
+ .option('--purpose <purpose>', '调用目的:manual / upgrade / history_restore;决定冲突策略(见下文),同时用于记录与关联', 'manual')
48
+ .option('--operation-ref <id>', '调用方已有的操作 / 任务关联标识,原样回显')
49
+ .addOption((0, shared_1.appIdOption)())
47
50
  .addHelpText('after', `
48
51
  已支持的迁移路径
49
52
  ${supportedDesc || '(none)'}
53
+ 磁盘已是目标布局(同目标)时走 repair:补齐适配 / 安装 / 构建,不返回 NOOP。
54
+
55
+ 文件规则
56
+ 明确托管的平台脚本(scripts 清单 + .githooks/pre-commit)按模板更新,scripts/ 下用户文件保留;
57
+ src/ public/ 搬入 client/、插件迁 server/capabilities;server / shared 只补平台缺失文件,
58
+ app.module.ts 定向注册平台模块;package.json scripts、tsconfig、vite.config、index.html 定向适配。
59
+ 同路径不同内容、重复插件 ID、自定义同名 script、无法确定的配置形态即为冲突,按冲突策略处理。
60
+
61
+ 冲突策略(conflictPolicy,由 --purpose 与磁盘布局决定)
62
+ stop --purpose upgrade / history_restore(平台调用):预检发现冲突即停、工程不写入;
63
+ 执行中才出现的冲突即停,不覆盖用户内容
64
+ overwrite --purpose manual(默认)且跨 stack 转换:冲突处按模板覆盖 —— 自定义同名 script 改成平台命令,
65
+ tsconfig / server 入口 / 无法适配的 index.html 换成模板,定制的 eslint.config.mjs 删除,
66
+ src/ public/ 与 client/ 同名文件由前者覆盖,被替换的列在 overwritten;
67
+ 自定义 prepare script、不同文件的重复插件 ID(两份都搬入)、未用平台 preset 的 vite.config(只改 alias)、
68
+ 非 .ts 的 vite 配置保留用户内容,记为 code=MIGRATE_CONFLICT_KEPT 的 warning;
69
+ 文件 / 目录类型不一致、package.json 不可解析仍按冲突停止
70
+ keep --purpose manual 且磁盘已是目标布局(repair):冲突处保留用户内容(warning 同上),其余照常修复
71
+ 不安全的符号链接在任何策略下都按冲突停止(MIGRATE_UNSAFE_SYMLINK)。
72
+
73
+ 结果语义
74
+ status=converted 原目录内文件适配、依赖安装、配置了的验证步骤全部完成;
75
+ 运行 readiness 由调用方核验(readiness.status=pending_caller_verification);
76
+ 沙箱里只向工作目录为本工程的 dev 进程发重启信号,
77
+ restartSignalSent / devRestarted 只表示发出了重启信号
78
+ status=conflict conflicts 给出具体路径 / ID,冲突文件不覆盖
79
+ status=failed failure.stage / failure.code 给出失败阶段与原因,现场保留、不自动回滚
80
+ originalIntact=true 仅表示失败发生在任何工程写入之前;写入后为 false(部分改动见 changes)。
81
+ 失败后对同一工程重跑同目标 migrate 即按真实布局 repair(可接续安装 / 构建)。
82
+ 非 converted 退出码为 1。例外:--purpose manual 时只有依赖安装失败(failure.stage=install),
83
+ 结果仍是 status=failed、install.status=failed,但退出码 0 —— 文件已转换,依赖由调用方补装;
84
+ 平台调用(upgrade / history_restore)安装失败仍为退出码 1。
85
+
86
+ 应用身份(app_id)
87
+ --app-id / MIAODA_APP_ID 与 .spark/meta.json 的 app_id 不一致时:
88
+ 沙箱内 → 视为复制来的工程,按当前应用改写 meta.app_id(随转换写入),warnings 给出
89
+ code=MIGRATE_APP_ID_REWRITTEN
90
+ 本机 → 拒绝(MIGRATE_APP_ID_MISMATCH),不渲染、不写入
50
91
 
51
92
  JSON 输出
52
- {"data": {"from": "...", "to": "...",
53
- "appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
54
- "moved": [...], "deleted": [...], "synced": [...], "merged": [...], "patched": [...],
55
- "skipped": N,
56
- "nextActions": [...]}}
93
+ {"data": {"status": "converted|conflict|failed", "mode": "migrate|repair", "runId": "...",
94
+ "purpose": "...", "conflictPolicy": "stop|overwrite|keep", "operationRef": "...",
95
+ "source": {"metaStack": "...", "detectedStack": "...", "stack": "..."},
96
+ "target": {"stack": "...", "templatePackage": "...", "templateVersion": "...", "cliVersion": "..."},
97
+ "stages": [{"name": "preflight|render|precheck|apply|install|verify|restart", "status": "...", "durationMs": 0}],
98
+ "failure": {"stage": "...", "code": "...", "message": "...", "retryable": true},
99
+ "conflicts": [{"path": "...", "reason": "...", "with": "...", "id": "..."}],
100
+ "overwritten": [{"path": "...", "reason": "...", "with": "..."}],
101
+ "warnings": [{"path": "...", "code": "MIGRATE_CONFLICT_KEPT", "message": "..."}],
102
+ "changes": {"added": [...], "modified": [...], "deleted": [...]},
103
+ "install": {"status": "ok|failed|not_run"}, "verify": {"status": "...", "steps": [...]},
104
+ "originalIntact": true,
105
+ "restartSignalSent": false, "readiness": {"status": "...", "detail": "..."},
106
+ "appliedRules": [...], "nextActions": [...]}}
57
107
 
58
108
  示例
59
109
  $ miaoda app migrate --to nestjs-react-fullstack
60
- $ miaoda app migrate --from vite-react --to nestjs-react-fullstack
110
+ $ miaoda app migrate --to nestjs-react-fullstack --purpose upgrade --operation-ref op_demo_xxx
61
111
  $ miaoda app migrate --to nestjs-react-fullstack --dir /path/to/app
62
112
  `);
63
113
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
@@ -65,6 +115,9 @@ JSON 输出
65
115
  dir: rawOpts.dir,
66
116
  from: rawOpts.from,
67
117
  to: rawOpts.to,
118
+ appId: rawOpts.appId,
119
+ purpose: rawOpts.purpose,
120
+ operationRef: rawOpts.operationRef,
68
121
  });
69
122
  }));
70
123
  parent.addCommand(cmd, { hidden: true });
@@ -75,6 +128,7 @@ function registerAppSync(parent) {
75
128
  .alias('upgrade')
76
129
  .description('一站式同步:按 stack 的 SyncRule 全套 apply + 按钉死版本表升 @lark-apaas/* deps + npm install')
77
130
  .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
131
+ .option('--strict', '平台严格调用:依赖安装失败时以退出码 1 结束', false)
78
132
  .addHelpText('after', `
79
133
  别名
80
134
  miaoda app sync 与 miaoda app upgrade 等价(upgrade 是历史别名,新调用方请用 sync)。
@@ -85,7 +139,7 @@ function registerAppSync(parent) {
85
139
  file → 单文件覆盖(可选 onlyIfExists / overwrite)
86
140
  merge-json → JSON deep merge(可选 arrayMerge by key 去重)
87
141
  add-script → npm script 不存在时添加(overwrite=false 默认不覆盖)
88
- patch-script → npm script 以指定前缀开头时改写(守护用户自定义)
142
+ patch-script → npm script 以指定前缀开头、或整值匹配平台旧值时改写(守护用户自定义)
89
143
  add-line → 行不存在时追加到文件
90
144
  remove-line → 行存在时移除
91
145
  delete-file/delete-directory → 删除
@@ -97,12 +151,18 @@ function registerAppSync(parent) {
97
151
  4. npm install --no-audit --no-fund --ignore-scripts。--ignore-scripts 是为绕开
98
152
  action-plugin postinstall 在 npm install 时缺平台 env 的卡点(需要时再
99
153
  FORCE_AUTHN_INNERAPI_DOMAIN=... npm install 手动跑一次)。
154
+ 安装失败时输出 status=failed、failedStage=install 与 installError(已应用的文件规则保留,
155
+ 重跑 sync 幂等补齐)。退出码:--strict 时为 1;不带 --strict 时为 0,调用方按 status 判断。
156
+
157
+ nestjs-react-fullstack 的平台托管文件是明确清单(scripts/dev.sh、dev.js、dev-local.js、
158
+ build.sh、run.sh、lint.js、prune-smart.js、hooks/run-precommit.js 与 .githooks/pre-commit),
159
+ 同名更新;scripts/ 下其他用户文件保留。内容未变化的规则报 noop,连续 sync 不产生漂移。
100
160
 
101
161
  stack 未纳入 sync 范围(无 SyncConfig 且无 upgrade/templates/<stack>/,如 design-html
102
162
  buildless 静态 HTML)→ 跳过同步(no-op, skipped=true),退出码 0,不阻断 dev/启动链路。
103
163
 
104
164
  JSON 输出
105
- {"data": {"stack": "...",
165
+ {"data": {"stack": "...", "status": "succeeded|failed", "failedStage": "install",
106
166
  "appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
107
167
  "syncedFiles": [...],
108
168
  "mergedJsonFiles": [{"path": "...", "keys": [...]}],
@@ -113,11 +173,13 @@ JSON 输出
113
173
  示例
114
174
  $ miaoda app sync
115
175
  $ miaoda app sync --dir /path/to/app
176
+ $ miaoda app sync --strict # 平台调用:安装失败退出码 1
116
177
  $ miaoda app upgrade # 等价于 miaoda app sync
117
178
  `);
118
179
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
119
180
  await (0, index_1.handleAppSync)({
120
181
  dir: rawOpts.dir,
182
+ strict: rawOpts.strict,
121
183
  });
122
184
  }));
123
185
  }
@@ -308,14 +370,22 @@ function registerAppInit(parent) {
308
370
  .option('--conf [json]', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
309
371
  .option('--skip-install', '跳过依赖安装', false)
310
372
  .option('--async-install', '派发后台进程装依赖并立即返回(与 --skip-install 互斥)', false)
373
+ .option('--strict', '平台严格调用:工程已存在时核验是否真正就绪(meta 可解析、stack 匹配、必要文件、依赖安装、上次操作结果),未就绪退出码 1;新初始化时同步安装失败也以退出码 1 结束', false)
311
374
  .addOption((0, shared_1.appIdOption)())
312
375
  .addHelpText('after', `
313
376
  幂等
314
- .spark/meta.json 已存在时直接退出(initialized=false, reason=already_initialized),
315
- 不再触发 template 渲染、不重新跑 upgrade/templates 同步、不重新拉 skills、不安装依赖。
316
- meta.json 在 init 流程最后才写,写了 = 已完整成功一次;
377
+ .spark/meta.json 已存在时不再渲染 / 安装(initialized=false, reason=already_initialized),
378
+ 不覆盖已有业务代码;同时输出 ready 与 problems(工程就绪核验结果)。
379
+ meta 存在不等于就绪:--strict 时 problems 非空(meta 损坏、stack 与请求不符、必要文件缺失、
380
+ 依赖未安装、上次 init/sync/migrate 失败)退出码 1,供平台严格调用判断。
317
381
  上一次失败留下半渲染状态时(package.json 在但 meta.json 没在)允许重跑。
318
382
 
383
+ 依赖安装失败
384
+ 同步安装失败时 init 输出 status=failed、failedStage=install 与 installError;
385
+ 已渲染的脚手架与 meta 保留(避免重跑时覆盖已开始编辑的文件),本地执行记录标为失败,
386
+ 后续 'miaoda app sync' 会补装依赖,--strict 在补装前报告未就绪。
387
+ 退出码:--strict 时为 1;不带 --strict 时为 0,调用方按 status 判断。
388
+
319
389
  应用身份(app_id)
320
390
  --app-id <id>(或 MIAODA_APP_ID env)提供时写入 .spark/meta.json 的 app_id 字段;
321
391
  未提供则不写。运行端命令(deploy / file / plugin 等)仍优先读 MIAODA_APP_ID env,
@@ -358,7 +428,8 @@ function registerAppInit(parent) {
358
428
  与 --skip-install 互斥。
359
429
 
360
430
  JSON 输出
361
- 已初始化:{"data": {"initialized": false, "reason": "already_initialized", "targetDir": "..."}}
431
+ 已初始化:{"data": {"initialized": false, "reason": "already_initialized", "targetDir": "...",
432
+ "ready": true|false, "problems": [{"code": "...", "message": "..."}]}}
362
433
  新初始化:{"data": {"initialized": true, "template": "...", "templateVersion": "...", "steeringVersion": "...",
363
434
  "appId": "...", "appType": "modern_html", "sourceImported": true, "importedFileCount": 7,
364
435
  "platformStackFound": true, "platformSyncedFiles": [...],
@@ -387,6 +458,7 @@ JSON 输出
387
458
  skipInstall: rawOpts.skipInstall,
388
459
  appId: rawOpts.appId,
389
460
  asyncInstall: rawOpts.asyncInstall,
461
+ strict: rawOpts.strict,
390
462
  });
391
463
  }));
392
464
  }
@@ -15,6 +15,7 @@ const logs_dir_1 = require("../../../utils/logs-dir");
15
15
  const env_1 = require("../../../utils/env");
16
16
  const error_1 = require("../../../utils/error");
17
17
  const output_1 = require("../../../utils/output");
18
+ const engineering_state_1 = require("../../../utils/engineering-state");
18
19
  /**
19
20
  * miaoda app init
20
21
  *
@@ -32,24 +33,56 @@ const output_1 = require("../../../utils/output");
32
33
  * 的版本(没经 sync 覆盖);首次 install 装的也是 template 钉的版本(没升 latest),
33
34
  * package.json 跟 template 完全一致。dev.sh 入口的 sync 会修正这些 drift。
34
35
  *
35
- * 幂等:.spark/meta.json 存在即表示已完整成功一次,直接退出,不重跑任何子步骤。
36
+ * 幂等:.spark/meta.json 存在时不重跑渲染 / 安装(不覆盖业务代码),但会做只读就绪核验并输出
37
+ * ready / problems —— meta 存在不等于就绪;--strict 时未就绪退出码 1。
36
38
  * 半渲染状态(package.json 在但 meta.json 不在)会重新跑完整流程。
39
+ *
40
+ * 同步安装失败:meta 仍写出(脚手架已渲染,避免重跑覆盖已开始编辑的文件),本地执行记录标为
41
+ * 失败,输出 status=failed / failedStage=install。只有 --strict(平台严格调用)以退出码 1 结束;
42
+ * 不带 --strict 时退出码 0 —— lark-cli `+init` 等调用方按退出码判成败,且依赖由后续
43
+ * `miaoda app sync`(dev.sh 入口)补装,安装抖动不能让整条初始化失败。
37
44
  */
38
45
  async function handleAppInit(opts) {
39
46
  await Promise.resolve();
40
47
  const targetDir = opts.targetDir ?? process.cwd();
41
48
  const metaPath = node_path_1.default.join(targetDir, '.spark', 'meta.json');
42
49
  if (node_fs_1.default.existsSync(metaPath)) {
50
+ let expectedStack;
51
+ if (opts.template || opts.appType) {
52
+ try {
53
+ expectedStack = (0, index_1.resolveStack)({ template: opts.template, appType: opts.appType });
54
+ }
55
+ catch (err) {
56
+ // 非严格调用保持历史行为:已初始化时不因参数校验失败而报错
57
+ if (opts.strict)
58
+ throw err;
59
+ }
60
+ }
61
+ const check = (0, index_1.checkWorkspaceReady)({ targetDir, expectedStack });
43
62
  if (!(0, output_1.isJsonMode)()) {
44
63
  process.stdout.write(`Already initialized: ${metaPath}\n`);
64
+ for (const p of check.problems) {
65
+ process.stdout.write(` ✗ ${p.code}: ${p.message}\n`);
66
+ }
45
67
  }
46
68
  (0, output_1.emit)({
47
69
  data: {
48
70
  initialized: false,
49
71
  reason: 'already_initialized',
50
72
  targetDir,
73
+ stack: check.stack,
74
+ ready: check.ready,
75
+ problems: check.problems,
51
76
  },
52
77
  });
78
+ if (opts.strict && !check.ready) {
79
+ (0, output_1.emitError)(new error_1.AppError('WORKSPACE_NOT_READY', check.problems.map((p) => `${p.code}: ${p.message}`).join('; '), {
80
+ next_actions: [
81
+ '按 problems 修复:依赖 / 文件问题执行 miaoda app sync,布局不符执行 miaoda app migrate',
82
+ ],
83
+ }));
84
+ process.exitCode = 1;
85
+ }
53
86
  return;
54
87
  }
55
88
  const stack = (0, index_1.resolveStack)({ template: opts.template, appType: opts.appType });
@@ -130,21 +163,29 @@ async function handleAppInit(opts) {
130
163
  app_id: opts.appId,
131
164
  });
132
165
  // async 模式:meta 已落盘(= 脚手架就绪),再派发后台安装并立即返回(不等装完)。
166
+ // 执行记录先标 running,后台 worker 结束时改写为 succeeded / failed;中断则停留在 running。
133
167
  let asyncDispatch;
134
168
  if (opts.asyncInstall) {
169
+ recordInitState(targetDir, 'running', 'install');
135
170
  asyncDispatch = (0, index_1.dispatchAsyncInstall)({ targetDir });
136
171
  }
172
+ const installFailed = installResult?.source === 'failed';
173
+ if (installResult !== undefined) {
174
+ recordInitState(targetDir, installFailed ? 'failed' : 'succeeded', installResult.source === 'skipped' ? 'install-skipped' : 'install', installResult.error);
175
+ }
137
176
  if (!(0, output_1.isJsonMode)()) {
138
177
  if (opts.asyncInstall && asyncDispatch) {
139
178
  process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install dispatched in background pid ${String(asyncDispatch.pid)}) in ${targetDir}\n`);
140
179
  }
141
180
  else if (installResult) {
142
- process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
181
+ process.stdout.write(`${installFailed ? '✗' : '✓'} Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
143
182
  }
144
183
  }
145
184
  (0, output_1.emit)({
146
185
  data: {
147
186
  initialized: true,
187
+ status: installFailed ? 'failed' : 'succeeded',
188
+ failedStage: installFailed ? 'install' : undefined,
148
189
  template: stack,
149
190
  templatePackage: tplResult.packageName,
150
191
  templateVersion: tplResult.version,
@@ -177,8 +218,39 @@ async function handleAppInit(opts) {
177
218
  cacheZip: installResult?.cacheZip,
178
219
  installError: installResult?.error,
179
220
  }),
221
+ nextActions: installFailed
222
+ ? [
223
+ '检查 npm 输出定位安装错误(网络 / registry / 依赖冲突),修复后执行 miaoda app sync 补装依赖',
224
+ ]
225
+ : undefined,
180
226
  },
181
227
  });
228
+ if (installFailed) {
229
+ if (opts.strict) {
230
+ (0, output_1.emitError)(new error_1.AppError('INIT_INSTALL_FAILED', `npm install failed: ${installResult?.error ?? ''}`, {
231
+ retryable: true,
232
+ next_actions: ['修复安装错误后执行 miaoda app sync 补装依赖'],
233
+ }));
234
+ process.exitCode = 1;
235
+ }
236
+ else {
237
+ (0, logger_1.log)('init', '⚠ npm install failed (continuing; run `miaoda app sync` to install later)');
238
+ }
239
+ }
240
+ }
241
+ function recordInitState(targetDir, status, stage, error) {
242
+ try {
243
+ (0, engineering_state_1.writeEngineeringState)(targetDir, {
244
+ operation: 'init',
245
+ status,
246
+ stage,
247
+ code: status === 'failed' ? 'INIT_INSTALL_FAILED' : undefined,
248
+ message: error?.slice(0, 500),
249
+ });
250
+ }
251
+ catch (err) {
252
+ (0, logger_1.log)('init', `⚠ failed to record engineering state: ${err instanceof Error ? err.message : String(err)}`);
253
+ }
182
254
  }
183
255
  /**
184
256
  * 处理 --source-path 导入:当前仅 app_type=modern_html 生效。