@lark-apaas/miaoda-cli 0.1.39-alpha.8b4b278 → 0.1.39-alpha.8eeaf6e

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
@@ -56,9 +56,14 @@ miaoda file ls --output json
56
56
 
57
57
  `miaoda app init / sync / migrate` 管理本地应用工程(平台调用方与本地开发共用):
58
58
 
59
- - `init`:按 `--template` 或 `--app-type` 渲染模板(`full_stack → nestjs-react-fullstack`,`frontend / jspage → vite-react`)。工程已存在时不重新渲染、不覆盖业务代码,只输出就绪核验结果 `ready / problems`;`--strict` 下未就绪(meta 损坏、stack 不符、必要文件缺失、依赖未安装、上次操作失败)退出码 1。同步安装失败输出 `status=failed` 并退出码 1。
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` 并退出码 1。
61
- - `migrate`(不在 help 列表中展示):工程转换原语,在原工程目录内执行。目标模板先渲染到临时模板目录(不复制用户工程),再只读预检冲突:同路径不同内容、重复插件 ID、无法确定的 package.json / tsconfig / vite.config / index.html 形态等返回 `status=conflict` 与具体路径,工程不写入。预检通过后在原目录定向转换(`src/ public/ → client/`、插件迁 `server/capabilities`、配置定向适配),执行中出现的冲突即停、不覆盖用户不同内容;随后在原目录安装依赖并完整构建(`npm run build`)。任一阶段失败退出码 1,`failure.stage` 给出 `install` / `verify` 等阶段,现场保留、不自动回滚;`originalIntact=true` 仅表示失败发生在任何工程写入之前,写入后一律为 false(已写入文件见 `changes`)。磁盘已是目标布局时走 repair(补齐适配 / 安装 / 构建,保留已有 `node_modules` 接续安装),不返回 NOOP;meta 已是目标 stack 不代表上次成功,以 `.spark/local/engineering-state.json` 的终态为准。放弃转换由调用方按代码版本恢复。输出包含源 / 目标 stack、模板与 CLI 版本、真实阶段结果与冲突路径;运行 readiness 由调用方核验(`readiness.status=pending_caller_verification`),`restartSignalSent` 只表示发出了重启信号。执行期间的并发编辑由调用方现有互斥与调度保证。
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` 只表示发出了重启信号。执行期间的并发编辑由调用方现有互斥与调度保证。
62
67
 
63
68
  `migrate` 不自动解引用工程外文件。迁移涉及的文件或父目录存在符号链接时,在规则执行前返回 `MIGRATE_UNSAFE_SYMLINK` 和具体 `conflicts[].path`;请改为工程拥有的普通文件或目录后重试。`html → design-html` 的 `src → .` 涉及整个工程根目录。原目录 install/build 前拒绝绝对链接、工程外链接、无法解析的链接及指向排除目录的链接;未触及且在工程内解析的相对业务链接保留,安装生成的普通 `node_modules/.bin` 相对链接允许。规则执行后、安装后及构建前再次检查链接边界。扫描失败返回 `MIGRATE_SYMLINK_CHECK_FAILED`;失败或链接冲突在安全的 `.spark/local` 写入终态,状态写路径不安全时不写入并给出 warning。该检查不是 npm 脚本的安全沙箱,不限制脚本主动访问其他绝对路径。
64
69
 
@@ -40,11 +40,11 @@ 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('工程转换原语:原目录内按 MigrateConfig 预检冲突 → 定向转换 → 安装 → 严格验证;冲突不覆盖用户内容,失败保留现场')
43
+ .description('工程转换原语:原目录内按 MigrateConfig 预检冲突 → 定向转换 → 安装;冲突按调用目的即停或按模板覆盖,失败保留现场')
44
44
  .requiredOption('--to <stack>', '目标 stack 短名')
45
45
  .option('--from <stack>', '源 stack;默认按磁盘实际布局识别(识别不了再读 .spark/meta.json)')
46
46
  .option('--dir <path>', '项目目录,默认 cwd')
47
- .option('--purpose <purpose>', '调用目的:manual / upgrade / copy_adapt / history_restore(只用于记录与关联)', 'manual')
47
+ .option('--purpose <purpose>', '调用目的:manual / upgrade / history_restore;决定冲突策略(见下文),同时用于记录与关联', 'manual')
48
48
  .option('--operation-ref <id>', '调用方已有的操作 / 任务关联标识,原样回显')
49
49
  .addOption((0, shared_1.appIdOption)())
50
50
  .addHelpText('after', `
@@ -54,28 +54,51 @@ function registerAppMigrate(parent) {
54
54
 
55
55
  文件规则
56
56
  明确托管的平台脚本(scripts 清单 + .githooks/pre-commit)按模板更新,scripts/ 下用户文件保留;
57
- src/ public/ 搬入 client/、插件迁 server/capabilities,同路径不同内容或重复插件 ID → 冲突;
58
- server / shared 只补平台缺失文件,app.module.ts 定向注册平台模块;package.json scripts、
59
- tsconfig、vite.config、index.html 定向适配,无法确定的形态 → 冲突,不整文件覆盖。
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)。
60
72
 
61
73
  结果语义
62
- status=converted 原目录内文件适配、依赖安装、严格验证(全栈为完整构建)全部完成;
63
- 运行 readiness 由调用方核验(readiness.status=pending_caller_verification),
74
+ status=converted 原目录内文件适配、依赖安装、配置了的验证步骤全部完成;
75
+ 运行 readiness 由调用方核验(readiness.status=pending_caller_verification);
76
+ 沙箱里只向工作目录为本工程的 dev 进程发重启信号,
64
77
  restartSignalSent / devRestarted 只表示发出了重启信号
65
78
  status=conflict conflicts 给出具体路径 / ID,冲突文件不覆盖
66
79
  status=failed failure.stage / failure.code 给出失败阶段与原因,现场保留、不自动回滚
67
80
  originalIntact=true 仅表示失败发生在任何工程写入之前;写入后为 false(部分改动见 changes)。
68
81
  失败后对同一工程重跑同目标 migrate 即按真实布局 repair(可接续安装 / 构建)。
69
- 非 converted 退出码为 1。
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),不渲染、不写入
70
91
 
71
92
  JSON 输出
72
93
  {"data": {"status": "converted|conflict|failed", "mode": "migrate|repair", "runId": "...",
73
- "purpose": "...", "operationRef": "...",
94
+ "purpose": "...", "conflictPolicy": "stop|overwrite|keep", "operationRef": "...",
74
95
  "source": {"metaStack": "...", "detectedStack": "...", "stack": "..."},
75
96
  "target": {"stack": "...", "templatePackage": "...", "templateVersion": "...", "cliVersion": "..."},
76
97
  "stages": [{"name": "preflight|render|precheck|apply|install|verify|restart", "status": "...", "durationMs": 0}],
77
98
  "failure": {"stage": "...", "code": "...", "message": "...", "retryable": true},
78
99
  "conflicts": [{"path": "...", "reason": "...", "with": "...", "id": "..."}],
100
+ "overwritten": [{"path": "...", "reason": "...", "with": "..."}],
101
+ "warnings": [{"path": "...", "code": "MIGRATE_CONFLICT_KEPT", "message": "..."}],
79
102
  "changes": {"added": [...], "modified": [...], "deleted": [...]},
80
103
  "install": {"status": "ok|failed|not_run"}, "verify": {"status": "...", "steps": [...]},
81
104
  "originalIntact": true,
@@ -105,6 +128,7 @@ function registerAppSync(parent) {
105
128
  .alias('upgrade')
106
129
  .description('一站式同步:按 stack 的 SyncRule 全套 apply + 按钉死版本表升 @lark-apaas/* deps + npm install')
107
130
  .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
131
+ .option('--strict', '平台严格调用:依赖安装失败时以退出码 1 结束', false)
108
132
  .addHelpText('after', `
109
133
  别名
110
134
  miaoda app sync 与 miaoda app upgrade 等价(upgrade 是历史别名,新调用方请用 sync)。
@@ -115,7 +139,7 @@ function registerAppSync(parent) {
115
139
  file → 单文件覆盖(可选 onlyIfExists / overwrite)
116
140
  merge-json → JSON deep merge(可选 arrayMerge by key 去重)
117
141
  add-script → npm script 不存在时添加(overwrite=false 默认不覆盖)
118
- patch-script → npm script 以指定前缀开头时改写(守护用户自定义)
142
+ patch-script → npm script 以指定前缀开头、或整值匹配平台旧值时改写(守护用户自定义)
119
143
  add-line → 行不存在时追加到文件
120
144
  remove-line → 行存在时移除
121
145
  delete-file/delete-directory → 删除
@@ -127,8 +151,8 @@ function registerAppSync(parent) {
127
151
  4. npm install --no-audit --no-fund --ignore-scripts。--ignore-scripts 是为绕开
128
152
  action-plugin postinstall 在 npm install 时缺平台 env 的卡点(需要时再
129
153
  FORCE_AUTHN_INNERAPI_DOMAIN=... npm install 手动跑一次)。
130
- 安装是必要步骤:失败时输出 status=failed、failedStage=install 与 installError,退出码 1
131
- (已应用的文件规则保留,重跑 sync 幂等补齐)。
154
+ 安装失败时输出 status=failed、failedStage=install 与 installError(已应用的文件规则保留,
155
+ 重跑 sync 幂等补齐)。退出码:--strict 时为 1;不带 --strict 时为 0,调用方按 status 判断。
132
156
 
133
157
  nestjs-react-fullstack 的平台托管文件是明确清单(scripts/dev.sh、dev.js、dev-local.js、
134
158
  build.sh、run.sh、lint.js、prune-smart.js、hooks/run-precommit.js 与 .githooks/pre-commit),
@@ -149,11 +173,13 @@ JSON 输出
149
173
  示例
150
174
  $ miaoda app sync
151
175
  $ miaoda app sync --dir /path/to/app
176
+ $ miaoda app sync --strict # 平台调用:安装失败退出码 1
152
177
  $ miaoda app upgrade # 等价于 miaoda app sync
153
178
  `);
154
179
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
155
180
  await (0, index_1.handleAppSync)({
156
181
  dir: rawOpts.dir,
182
+ strict: rawOpts.strict,
157
183
  });
158
184
  }));
159
185
  }
@@ -344,7 +370,7 @@ function registerAppInit(parent) {
344
370
  .option('--conf [json]', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
345
371
  .option('--skip-install', '跳过依赖安装', false)
346
372
  .option('--async-install', '派发后台进程装依赖并立即返回(与 --skip-install 互斥)', false)
347
- .option('--strict', '平台严格调用:工程已存在时核验是否真正就绪(meta 可解析、stack 匹配、必要文件、依赖安装、上次操作结果),未就绪退出码 1', false)
373
+ .option('--strict', '平台严格调用:工程已存在时核验是否真正就绪(meta 可解析、stack 匹配、必要文件、依赖安装、上次操作结果),未就绪退出码 1;新初始化时同步安装失败也以退出码 1 结束', false)
348
374
  .addOption((0, shared_1.appIdOption)())
349
375
  .addHelpText('after', `
350
376
  幂等
@@ -355,9 +381,10 @@ function registerAppInit(parent) {
355
381
  上一次失败留下半渲染状态时(package.json 在但 meta.json 没在)允许重跑。
356
382
 
357
383
  依赖安装失败
358
- 同步安装失败时 init 输出 status=failed、failedStage=install 并以退出码 1 结束;
359
- 已渲染的脚手架与 meta 保留(避免重跑时覆盖已开始编辑的文件),本地执行记录标为 install_failed,
384
+ 同步安装失败时 init 输出 status=failed、failedStage=install 与 installError;
385
+ 已渲染的脚手架与 meta 保留(避免重跑时覆盖已开始编辑的文件),本地执行记录标为失败,
360
386
  后续 'miaoda app sync' 会补装依赖,--strict 在补装前报告未就绪。
387
+ 退出码:--strict 时为 1;不带 --strict 时为 0,调用方按 status 判断。
361
388
 
362
389
  应用身份(app_id)
363
390
  --app-id <id>(或 MIAODA_APP_ID env)提供时写入 .spark/meta.json 的 app_id 字段;
@@ -37,8 +37,10 @@ const engineering_state_1 = require("../../../utils/engineering-state");
37
37
  * ready / problems —— meta 存在不等于就绪;--strict 时未就绪退出码 1。
38
38
  * 半渲染状态(package.json 在但 meta.json 不在)会重新跑完整流程。
39
39
  *
40
- * 同步安装失败是失败:meta 仍写出(脚手架已渲染,避免重跑覆盖已开始编辑的文件),本地执行
41
- * 记录标为失败,输出 status=failed / failedStage=install 并退出码 1。
40
+ * 同步安装失败:meta 仍写出(脚手架已渲染,避免重跑覆盖已开始编辑的文件),本地执行记录标为
41
+ * 失败,输出 status=failed / failedStage=install。只有 --strict(平台严格调用)以退出码 1 结束;
42
+ * 不带 --strict 时退出码 0 —— lark-cli `+init` 等调用方按退出码判成败,且依赖由后续
43
+ * `miaoda app sync`(dev.sh 入口)补装,安装抖动不能让整条初始化失败。
42
44
  */
43
45
  async function handleAppInit(opts) {
44
46
  await Promise.resolve();
@@ -224,11 +226,16 @@ async function handleAppInit(opts) {
224
226
  },
225
227
  });
226
228
  if (installFailed) {
227
- (0, output_1.emitError)(new error_1.AppError('INIT_INSTALL_FAILED', `npm install failed: ${installResult?.error ?? ''}`, {
228
- retryable: true,
229
- next_actions: ['修复安装错误后执行 miaoda app sync 补装依赖'],
230
- }));
231
- process.exitCode = 1;
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
+ }
232
239
  }
233
240
  }
234
241
  function recordInitState(targetDir, status, stage, error) {
@@ -9,6 +9,7 @@ const index_1 = require("../../../services/app/migrate/index");
9
9
  const error_1 = require("../../../utils/error");
10
10
  const output_1 = require("../../../utils/output");
11
11
  const env_1 = require("../../../utils/env");
12
+ const logger_1 = require("../../../utils/logger");
12
13
  const package_json_1 = __importDefault(require("../../../../package.json"));
13
14
  /**
14
15
  * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>] [--purpose <p>] [--operation-ref <id>]
@@ -16,14 +17,16 @@ const package_json_1 = __importDefault(require("../../../../package.json"));
16
17
  * 工程转换原语的 CLI 入口:参数校验 + 结构化输出,转换流程在 services/app/migrate。
17
18
  *
18
19
  * 结果语义:
19
- * status=converted:原目录内文件适配、依赖安装、严格验证(全栈为完整构建)全部完成。
20
+ * status=converted:原目录内文件适配、依赖安装、配置了的验证步骤全部完成。
21
+ * conflictPolicy=overwrite 时被模板替换的用户内容列在 overwritten;保留未适配的位置是
22
+ * code=MIGRATE_CONFLICT_KEPT 的 warning。
20
23
  * 运行 readiness 不在 CLI 内判定(readiness.status=pending_caller_verification)——
21
- * 沙箱里 restartSignalSent / devRestarted 只表示发出了 pkill 重启信号,不是健康结果。
22
- * status=conflict:存在无法确定性处理的冲突(conflicts 给出具体路径 / ID),冲突文件不覆盖;
24
+ * 沙箱里 restartSignalSent / devRestarted 只表示向本目录的 dev 进程发出了重启信号,不是健康结果。
25
+ * status=conflict:存在当前冲突策略下无法处理的冲突(conflicts 给出具体路径 / ID),冲突文件不覆盖;
23
26
  * 预检阶段发现时工程未写入(originalIntact=true),应用阶段发现时可能已有部分改动。
24
27
  * status=failed:某阶段失败(failure.stage / code),现场原样保留、不自动回滚;
25
28
  * originalIntact=true 仅表示失败发生在任何工程写入之前。
26
- * 非 converted 一律退出码 1。
29
+ * 非 converted 退出码 1,唯一例外见 isLenientInstallFailure:manual 调用只是依赖没装上时退出码 0。
27
30
  *
28
31
  * 同目标(磁盘已是目标布局)不再返回 MIGRATE_NOOP:走 repair 模式补齐适配 / 安装 / 构建,
29
32
  * 支持"平台已新但恢复了旧代码"与"目标 meta 已写、安装未完成"的样本。
@@ -50,6 +53,10 @@ async function handleAppMigrate(opts) {
50
53
  });
51
54
  const data = toOutput(outcome, sandbox);
52
55
  (0, output_1.emit)({ data });
56
+ if (isLenientInstallFailure(outcome)) {
57
+ (0, logger_1.log)('migrate', '⚠ npm install failed (continuing): 文件已转换,依赖需调用方补装');
58
+ return;
59
+ }
53
60
  if (outcome.status !== 'converted') {
54
61
  // 结构化结果在 stdout;stderr 同时给出一条标准错误,便于只看退出码 / stderr 的调用方
55
62
  (0, output_1.emitError)(new error_1.AppError(outcome.failure?.code ?? 'MIGRATE_FAILED', outcome.failure?.message ?? outcome.status, {
@@ -59,12 +66,25 @@ async function handleAppMigrate(opts) {
59
66
  process.exitCode = 1;
60
67
  }
61
68
  }
69
+ /**
70
+ * manual 调用(feida-ai 升级全栈 / html → design-html、本机手动执行)只是依赖没装上:结果仍是
71
+ * status=failed、install.status=failed,但退出码 0。这些调用方只看退出码:退出码非 0 时,升级全栈
72
+ * 照样切应用类型、却跳过它自己的 `npm install --force` 补装;design-html 不回写库里的 stack,
73
+ * 而下一轮读到已转换的 meta 又跳过迁移,库与沙箱永久不一致。文件已转换、依赖可补装,
74
+ * 按 0.1.46 的软失败交还调用方。平台调用(upgrade / history_restore)按结构化结果推进,保持严格。
75
+ */
76
+ function isLenientInstallFailure(outcome) {
77
+ return (outcome.purpose === 'manual' &&
78
+ outcome.status === 'failed' &&
79
+ outcome.failure?.stage === 'install');
80
+ }
62
81
  function toOutput(outcome, sandbox) {
63
82
  return {
64
83
  status: outcome.status,
65
84
  mode: outcome.mode,
66
85
  runId: outcome.runId,
67
86
  purpose: outcome.purpose,
87
+ conflictPolicy: outcome.conflictPolicy,
68
88
  operationRef: outcome.operationRef,
69
89
  from: outcome.source.stack,
70
90
  to: outcome.target.stack,
@@ -75,6 +95,7 @@ function toOutput(outcome, sandbox) {
75
95
  failure: outcome.failure,
76
96
  warnings: outcome.warnings,
77
97
  conflicts: outcome.conflicts,
98
+ overwritten: outcome.overwritten,
78
99
  changes: outcome.changes,
79
100
  appliedRules: outcome.results.map((r) => ({
80
101
  type: r.rule.type,
@@ -114,13 +135,22 @@ function nextActions(outcome, sandbox) {
114
135
  '如需放弃本次转换,由调用方按代码版本(如 git)恢复原工程',
115
136
  ];
116
137
  }
138
+ const policyActions = [];
139
+ if (outcome.overwritten.length > 0) {
140
+ policyActions.push('overwritten 列出的用户改动已被模板替换:用 git diff 核对,需要保留的业务改动从 git 历史手动合回');
141
+ }
142
+ if (outcome.warnings.some((w) => w.code === 'MIGRATE_CONFLICT_KEPT')) {
143
+ policyActions.push('warnings 中 MIGRATE_CONFLICT_KEPT 的位置保留了用户内容、未按目标 stack 适配,按需手动调整');
144
+ }
117
145
  return sandbox
118
146
  ? [
147
+ ...policyActions,
119
148
  '调用方核验沙箱 dev 进程与预览 readiness(restartSignalSent 只表示发出了重启信号)',
120
149
  'git status / git diff 评估改动并 commit',
121
150
  'miaoda skills sync 同步到新 stack 的 agent skills',
122
151
  ]
123
152
  : [
153
+ ...policyActions,
124
154
  '重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev,并验证预览',
125
155
  'git status / git diff 评估改动并 commit',
126
156
  'miaoda skills sync 同步到新 stack 的 agent skills',
@@ -47,7 +47,7 @@ const MANAGED_PLATFORM_PACKAGES = {
47
47
  '@lark-apaas/http-client': 'latest',
48
48
  };
49
49
  /**
50
- * miaoda app sync [--dir <path>]
50
+ * miaoda app sync [--dir <path>] [--strict]
51
51
  *
52
52
  * 一站式同步:把当前 stack 的 sync 规则全套 apply 到 user app,对齐沙箱里
53
53
  * `npm run upgrade → npx fullstack-cli sync` 的行为,并补 miaoda-cli 本地开发独有规则。
@@ -108,8 +108,9 @@ async function handleAppSync(opts) {
108
108
  (0, logs_dir_1.ensureLogsDir)(targetDir);
109
109
  // 3. snapshot 当前 user app 装的管控包版本,供 install 后 diff 用
110
110
  const beforeSpecs = snapshotManagedDepSpecs(targetDir);
111
- // 4. npm install —— 必要步骤:失败时仍输出完整的规则应用结果,但 status=failed + 退出码 1,
112
- // 不把"文件同步了、依赖没装上"报成成功。已应用的规则保留(规则幂等,重跑 sync 补齐)。
111
+ // 4. npm install —— 失败时仍输出完整的规则应用结果,status=failed,不把"文件同步了、依赖没装上"
112
+ // 报成成功;已应用的规则保留(规则幂等,重跑 sync 补齐)。只有 --strict 以退出码 1 结束:
113
+ // dev.sh 与 lark-cli `+init` 不带 --strict,后者按退出码判成败,安装抖动不能让它整条失败。
113
114
  // 把 user app 装着的**所有**管控包显式作为 `<pkg>@latest` 位置参数传给 npm install:
114
115
  // - npm 自然会把 spec 改写成 ^具体最新版,lockfile 同步更新 → 不需要 sync 主动改 spec
115
116
  // - 没装的管控包不动 (sync 的职责是对齐,不是塞依赖)
@@ -137,7 +138,9 @@ async function handleAppSync(opts) {
137
138
  }
138
139
  catch (err) {
139
140
  installError = err instanceof Error ? err.message : String(err);
140
- (0, logger_1.log)('sync', `✗ npm install failed: ${installError}`);
141
+ (0, logger_1.log)('sync', opts.strict
142
+ ? `✗ npm install failed: ${installError}`
143
+ : `⚠ npm install failed (continuing): ${installError}`);
141
144
  }
142
145
  // 5. install 之后再读一次 package.json,diff 出 npm 实际改写的 spec
143
146
  const afterSpecs = snapshotManagedDepSpecs(targetDir);
@@ -160,7 +163,7 @@ async function handleAppSync(opts) {
160
163
  : undefined,
161
164
  },
162
165
  });
163
- if (failed) {
166
+ if (failed && opts.strict) {
164
167
  (0, output_1.emitError)(new error_1.AppError('SYNC_INSTALL_FAILED', `npm install failed: ${installError ?? ''}`, {
165
168
  retryable: true,
166
169
  next_actions: ['修复安装错误(网络 / registry / 依赖冲突)后重跑 miaoda app sync'],
@@ -49,8 +49,9 @@ function managedFileRules() {
49
49
  }
50
50
  /**
51
51
  * 托管构建脚本的配套依赖:build.sh 的智能裁剪(prune-smart.js)require('@vercel/nft'),模板与
52
- * 沙箱镜像都不提供它。migrate / repair 后的验证要跑完整 npm run build,所以对未声明的工程补
53
- * devDependencies(版本为实测的 0.30.4);已声明的用户版本不降级、不覆盖。
52
+ * 沙箱镜像都不提供它。迁移后的工程第一次完整构建(发布或本地 npm run build)会用到,所以
53
+ * migrate / repair 对未声明的工程补 devDependencies(版本为实测的 0.30.4);已声明的用户版本
54
+ * 不降级、不覆盖。
54
55
  */
55
56
  exports.FULLSTACK_MANAGED_BUILD_DEV_DEPENDENCIES = {
56
57
  '@vercel/nft': '0.30.4',
@@ -19,7 +19,7 @@
19
19
  * - 不从 design-html template 拷任何文件(模板本身只有 _gitignore + package.json;
20
20
  * 用户已有的 .gitignore 是 html 版超集,保留无害)
21
21
  * - 不动 README.md / .npmrc / .env —— 部署时均被 EXCLUDES 过滤
22
- * - 不管 dev 进程 / MIAODA_APP_TYPE 切换(平台职责;沙箱 pkill 匹配的
22
+ * - 不管 dev 进程 / MIAODA_APP_TYPE 切换(平台职责;沙箱重启信号匹配的
23
23
  * scripts/dev.js 在 html 项目里不存在,自然跳过)
24
24
  *
25
25
  * handler 收尾的 npm install 照常跑:devserver 依赖删掉后重新生成精简 lockfile,
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * vite-react → nestjs-react-fullstack 的跨 stack 迁移规则。
4
4
  *
5
- * 设计原则:**明确托管清单 + 定向适配 + 冲突停止**。
5
+ * 设计原则:**明确托管清单 + 定向适配 + 冲突按调用方策略处理**。
6
6
  * - 明确托管的平台文件(scripts 清单、.githooks/pre-commit)按兼容模板整文件更新;清单外的
7
7
  * scripts/ 用户文件保留。
8
8
  * - 业务代码、server/shared、插件不整目录覆盖:只补平台启动所需的缺失文件,已有 app.module.ts
@@ -10,20 +10,22 @@
10
10
  * - package.json / tsconfig / vite.config / index.html 等混合配置只改本次运行要求的部分,
11
11
  * 保留 alias、paths、用户编译选项、HTML 自定义 head、额外 scripts 等业务部分。
12
12
  * - 无法确定性处理(同路径不同内容、同 ID 插件、自定义同名 script、不支持的配置形态)一律
13
- * action=conflict 不写入;执行前对原目录预检可识别冲突(不写盘),执行中出现的冲突即停止,
14
- * 不覆盖用户内容。
13
+ * action=conflict。冲突策略 stop(平台调用)下:执行前对原目录预检可识别冲突(不写盘),执行中
14
+ * 出现的冲突即停止,不覆盖用户内容;overwrite(手动 / Agent 调用)下按各规则说明用模板替换,
15
+ * 替换不了的保留用户内容(见 config/migrate.ts 各规则类型)。
15
16
  *
16
17
  * 步骤:
17
18
  * 1. src/ public/ 搬到 client/ 下(同名不同内容 → 冲突)
18
- * 2. 旧 stack 平台残留:eslint.config.mjs 仅在是平台默认内容时删除;lockfile 删除(依赖集合变化)
19
+ * 2. 旧 stack 平台残留:补 eslint.config.js 并带上旧配置的 globalIgnores,eslint.config.mjs 仅在是
20
+ * 平台默认内容时删除;lockfile 删除(依赖集合变化)
19
21
  * 3. 删 user package.json 里 lite SDK / type=module
20
22
  * 4. server 平台启动文件按缺失补齐 + app.module.ts 定向注册 PlatformModule / ViewModule
21
- * 5. nest-cli.json 合并、tsconfig 三件套定向适配、eslint/stylelint 缺失补齐
23
+ * 5. nest-cli.json 合并、tsconfig 三件套定向适配(exclude 随布局改写)、stylelint 缺失补齐
22
24
  * 6. 根 index.html → client/index.html(HBS 骨架 + 用户 head/body 自定义部分)
23
25
  * 7. 托管脚本清单更新;shared / .env 缺失补齐
24
26
  * 8. 插件 shared/capabilities → server/capabilities(重复 ID 检查)
25
27
  * 9. scripts 定向适配(旧平台写法改写、自定义同名冲突)
26
- * 10. 从 fullstack template/package.json 取版本号,精确加 NestJS 强依赖白名单
28
+ * 10. 从 fullstack template/package.json 取版本号,精确加 NestJS 强依赖白名单(SWC 钉 lockfile 版本)
27
29
  * 11. codemod:lite → client-toolkit;vite.config alias src → client/src
28
30
  * 12. set-stack 切 .spark/meta.json
29
31
  *
@@ -43,6 +45,23 @@ const fullstack_managed_files_1 = require("../../config/fullstack-managed-files"
43
45
  const VITE_REACT_DEFAULT_ESLINT = 'import\\{globalIgnores\\}from"eslint/config"' +
44
46
  'import\\{eslintPresets\\}from"@lark-apaas/coding-presets-react"' +
45
47
  'exportdefault\\[(globalIgnores\\(\\[("[^"]*",?)*\\]\\),?)?\\.\\.\\.eslintPresets\\.client,?\\]';
48
+ /**
49
+ * 早期 vite-react 模板(coding-template-vite-react 0.1.8~0.1.17,2026-05~06 初)默认的
50
+ * eslint.config.mjs(规范化后):自包含的 flat config,直接引 @eslint/js / typescript-eslint /
51
+ * react-hooks / react-refresh,没有平台 preset。忽略列表允许不同。
52
+ */
53
+ const VITE_REACT_EARLY_DEFAULT_ESLINT = 'importjsfrom"@eslint\\/js"' +
54
+ 'importglobalsfrom"globals"' +
55
+ 'importreactHooksfrom"eslint-plugin-react-hooks"' +
56
+ 'importreactRefreshfrom"eslint-plugin-react-refresh"' +
57
+ 'importtseslintfrom"typescript-eslint"' +
58
+ 'import\\{defineConfig,globalIgnores\\}from"eslint\\/config"' +
59
+ 'exportdefaultdefineConfig\\(\\[' +
60
+ 'globalIgnores\\(\\[("[^"]*",?)*\\]\\),' +
61
+ '\\{files:\\["\\*\\*\\/\\*\\.\\{ts,tsx\\}"\\],' +
62
+ 'extends:\\[js\\.configs\\.recommended,tseslint\\.configs\\.recommended,reactHooks\\.configs\\.flat\\.recommended,reactRefresh\\.configs\\.vite,\\],' +
63
+ 'languageOptions:\\{ecmaVersion:2020,globals:globals\\.browser,\\},' +
64
+ 'rules:\\{"react-refresh\\/only-export-components":"off","@typescript-eslint\\/no-unused-vars":"off","@typescript-eslint\\/no-explicit-any":"off","@typescript-eslint\\/no-empty-object-type":"off","@typescript-eslint\\/no-require-imports":"off","no-empty":"off","prefer-const":"off","no-unused-labels":"off",\\},\\},\\]\\)';
46
65
  exports.MIGRATE_CONFIG = {
47
66
  from: 'vite-react',
48
67
  to: 'nestjs-react-fullstack',
@@ -52,11 +71,16 @@ exports.MIGRATE_CONFIG = {
52
71
  { type: 'move-directory', from: 'public', to: 'client/public', onConflict: 'fail' },
53
72
  // shared/ 不动(两边布局相同);.env / .spark_project 同理。
54
73
  // ===== 2. 清理 vite-react 平台残留 =====
55
- // ESLint 9 flat config 文件名变了(eslint.config.mjs → eslint.config.js);用户改过则冲突
74
+ // ESLint 9 flat config 文件名变了(eslint.config.mjs → eslint.config.js):先补新配置(用户已有则
75
+ // 保留),把旧配置 globalIgnores 的条目带过去 —— vite-react 模板排除了 **/components/ui/**,
76
+ // 不带过去 shadcn 组件会被 lint,组件里引用新版 react-hooks 规则的 disable 注释报"规则不存在"。
77
+ { type: 'file', from: 'eslint.config.js', to: 'eslint.config.js', overwrite: false },
78
+ { type: 'carry-eslint-ignores', from: 'eslint.config.mjs', to: 'eslint.config.js' },
79
+ // 旧配置用户改过则冲突
56
80
  {
57
81
  type: 'remove-known-default',
58
82
  to: 'eslint.config.mjs',
59
- knownDefaults: [VITE_REACT_DEFAULT_ESLINT],
83
+ knownDefaults: [VITE_REACT_DEFAULT_ESLINT, VITE_REACT_EARLY_DEFAULT_ESLINT],
60
84
  reason: 'customized ESLint config (eslint.config.mjs) cannot be converted automatically; move custom rules into eslint.config.js extending @lark-apaas/fullstack-presets, then retry',
61
85
  },
62
86
  // 依赖集合变了,lockfile 由原目录里的 npm install 重新生成。
@@ -101,11 +125,14 @@ exports.MIGRATE_CONFIG = {
101
125
  role: 'app',
102
126
  from: 'tsconfig.app.json',
103
127
  to: 'tsconfig.app.json',
104
- legacyExtends: ['@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json'],
128
+ legacyExtends: [
129
+ '@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json',
130
+ // 早期模板(0.1.8~0.1.17)的 preset 包名
131
+ '@lark-apaas/coding-presets/lib/tsconfig/tsconfig.app.json',
132
+ ],
105
133
  },
106
134
  { type: 'adapt-tsconfig', role: 'node', from: 'tsconfig.node.json', to: 'tsconfig.node.json' },
107
- // ESLint / stylelint:缺失时补模板(用户已有则保留)
108
- { type: 'file', from: 'eslint.config.js', to: 'eslint.config.js', overwrite: false },
135
+ // stylelint:缺失时补模板(用户已有则保留);eslint.config.js 已在第 2 步补齐
109
136
  // package.json 的 `stylelint` script + precommit 会调它,缺配置时报 `No configuration provided`
110
137
  { type: 'file', from: '.stylelintrc.js', to: '.stylelintrc.js', overwrite: false },
111
138
  // ===== 6. HTML 入口:根 index.html → client/index.html(HBS) =====
@@ -183,6 +210,7 @@ exports.MIGRATE_CONFIG = {
183
210
  name: 'prepare',
184
211
  command: 'chmod +x .githooks/pre-commit 2>/dev/null; git config core.hooksPath .githooks 2>/dev/null || true',
185
212
  legacy: ['node scripts/setup-git-hooks.mjs'],
213
+ keepCustom: true,
186
214
  },
187
215
  {
188
216
  name: 'upgrade',
@@ -269,6 +297,10 @@ exports.MIGRATE_CONFIG = {
269
297
  // 配套的 .stylelintrc.js 由上面 file rule 从 template 搬过来。
270
298
  'stylelint',
271
299
  ],
300
+ // SWC 钉模板 lockfile 里的精确版本:迁移删了用户 lockfile,^range 会解析到模板发布之后的
301
+ // 新版本(@swc/core 1.16.18 在沙箱里加载 native binding 失败,nest 起不来)。钉 lockfile
302
+ // 版本后与同版本模板新建的应用一致,模板升级 SWC 时自动跟随。
303
+ exactFromLock: ['@swc/cli', '@swc/core'],
272
304
  },
273
305
  // 10b. 托管构建脚本的配套依赖(@vercel/nft,build.sh → prune-smart.js):缺失才补固定版本,
274
306
  // 已声明保持用户版本。迁移 / repair 的验证要跑完整 npm run build。
@@ -286,6 +318,9 @@ exports.MIGRATE_CONFIG = {
286
318
  to: 'vite.config.ts',
287
319
  template: 'vite.config.ts',
288
320
  requirePreset: '@lark-apaas/coding-preset-vite-react',
321
+ // 早期模板(0.1.8~0.1.10)的 preset 包名:配置里只有 defineConfig({ resolve.alias }),换包等价;
322
+ // 新包在 followLatestPackages 里,安装时补上
323
+ legacyPresets: ['@lark-apaas/coding-vite-preset'],
289
324
  },
290
325
  // ===== 12. 切 stack =====
291
326
  { type: 'set-stack', stack: 'nestjs-react-fullstack' },
@@ -295,7 +330,7 @@ exports.MIGRATE_CONFIG = {
295
330
  // 这些 API 当前只在 alpha 版本里。template caret range 不匹配 pre-release,必须显式 latest。
296
331
  // - coding-preset-vite-react:本次切到 MIAODA_APP_TYPE='3' env 检测,老版本 preset 不认 env。
297
332
  followLatestPackages: ['@lark-apaas/client-toolkit', '@lark-apaas/coding-preset-vite-react'],
298
- // 成功必须包含完整构建:首次 FaaS 发布需要可启动 server、页面资源与 run.sh
299
- verify: [['run', 'build']],
333
+ // 不配置 verify(不做完整构建):发布时 DevOps 构建机会完整构建一次,转换问题在预览 / 发布时暴露;
334
+ // 沙箱里跑模板 build.sh 还会经裸名 `npx fullstack-cli` 解析到公网同名第三方包。
300
335
  };
301
336
  exports.default = exports.MIGRATE_CONFIG;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * 沙箱内重启被转换工程的 dev 进程:向 `node …scripts/dev.js` 主进程发 SIGTERM,由平台
4
+ * supervisor 重新 exec dev.sh → dev.js 加载新脚本与依赖。这只是重启信号,新进程是否健康由
5
+ * 调用方核验。
6
+ *
7
+ * 只认工作目录等于被转换目录的进程:dev.js 启动即 chdir 到自己所在的工程根,cwd 唯一对应它
8
+ * 服务的工程。按命令行全局匹配会连带杀掉同一沙箱里其他目录的预览进程(例如在工程副本里执行
9
+ * migrate 时,真实应用的预览也被重启)。.spark/meta.json 的 app_id 区分不了副本与原工程,沙箱
10
+ * 也没有所有调用方都会设置的工作区路径环境变量,所以都不用来判定。
11
+ *
12
+ * 读 /proc 是 Linux-only:只在沙箱(恒为 Linux)调用;/proc 不可读时视为没有可重启的进程。
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.signalDevRestart = signalDevRestart;
19
+ const node_fs_1 = __importDefault(require("node:fs"));
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ const DEV_PROCESS_RE = /node.*scripts\/dev\.js/;
22
+ /** 返回已发出重启信号的进程号;没有命中返回空数组 */
23
+ function signalDevRestart(projectDir, deps = {}) {
24
+ const procRoot = deps.procRoot ?? '/proc';
25
+ const kill = deps.kill ??
26
+ ((pid) => {
27
+ process.kill(pid, 'SIGTERM');
28
+ });
29
+ let project;
30
+ let entries;
31
+ try {
32
+ project = node_fs_1.default.realpathSync(projectDir);
33
+ entries = node_fs_1.default.readdirSync(procRoot);
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ const signaled = [];
39
+ for (const entry of entries) {
40
+ if (!/^\d+$/.test(entry) || Number(entry) === process.pid)
41
+ continue;
42
+ try {
43
+ const cmdline = node_fs_1.default
44
+ .readFileSync(node_path_1.default.join(procRoot, entry, 'cmdline'), 'utf-8')
45
+ .replaceAll('\0', ' ');
46
+ if (!DEV_PROCESS_RE.test(cmdline))
47
+ continue;
48
+ // 内核给出的 cwd 已是解析过符号链接的绝对路径
49
+ if (node_fs_1.default.readlinkSync(node_path_1.default.join(procRoot, entry, 'cwd')) !== project)
50
+ continue;
51
+ kill(Number(entry));
52
+ signaled.push(Number(entry));
53
+ }
54
+ catch {
55
+ // 进程已退出或无权读取:不是本工程可重启的进程
56
+ }
57
+ }
58
+ return signaled;
59
+ }