@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.
@@ -4,239 +4,157 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.handleAppMigrate = handleAppMigrate;
7
- const node_child_process_1 = require("node:child_process");
8
- const node_fs_1 = __importDefault(require("node:fs"));
9
- const node_os_1 = __importDefault(require("node:os"));
10
7
  const node_path_1 = __importDefault(require("node:path"));
11
- const index_1 = require("../../../config/migrate-configs/index");
12
- const index_2 = require("../../../services/app/init/index");
13
- const migrate_rule_1 = require("../../../utils/migrate-rule");
14
- const spark_meta_1 = require("../../../utils/spark-meta");
8
+ const index_1 = require("../../../services/app/migrate/index");
15
9
  const error_1 = require("../../../utils/error");
16
10
  const output_1 = require("../../../utils/output");
17
11
  const env_1 = require("../../../utils/env");
18
12
  const logger_1 = require("../../../utils/logger");
19
- const run_npm_1 = require("../../../utils/run-npm");
13
+ const package_json_1 = __importDefault(require("../../../../package.json"));
20
14
  /**
21
- * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
15
+ * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>] [--purpose <p>] [--operation-ref <id>]
22
16
  *
23
- * 跨 stack 原地迁移:把 user app 从源 stack(默认 .spark/meta.json 当前 stack)转到目标 stack。
24
- * 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
25
- * 自行 `git status` / `git diff` 评估并 commit。
17
+ * 工程转换原语的 CLI 入口:参数校验 + 结构化输出,转换流程在 services/app/migrate。
26
18
  *
27
- * 跟 sync 的区别:sync 是同 stack 内的版本对齐,migrate 是跨 stack 切换(布局重排、依赖
28
- * 集合切换、配置文件全套替换)。两条命令的 rule 引擎共享(applyMigrateRules 内部对
29
- * SyncRule 委派给 applySyncRules)。
19
+ * 结果语义:
20
+ * status=converted:原目录内文件适配、依赖安装、配置了的验证步骤全部完成。
21
+ * conflictPolicy=overwrite 时被模板替换的用户内容列在 overwritten;保留未适配的位置是
22
+ * code=MIGRATE_CONFLICT_KEPT 的 warning。
23
+ * 运行 readiness 不在 CLI 内判定(readiness.status=pending_caller_verification)——
24
+ * 沙箱里 restartSignalSent / devRestarted 只表示向本目录的 dev 进程发出了重启信号,不是健康结果。
25
+ * status=conflict:存在当前冲突策略下无法处理的冲突(conflicts 给出具体路径 / ID),冲突文件不覆盖;
26
+ * 预检阶段发现时工程未写入(originalIntact=true),应用阶段发现时可能已有部分改动。
27
+ * status=failed:某阶段失败(failure.stage / code),现场原样保留、不自动回滚;
28
+ * originalIntact=true 仅表示失败发生在任何工程写入之前。
29
+ * 非 converted 退出码 1,唯一例外见 isLenientInstallFailure:manual 调用只是依赖没装上时退出码 0。
30
30
  *
31
- * 执行流:
32
- * 1. 校验 user app 已 init(.spark/meta.json 存在)+ 当前 stack 匹配 from
33
- * 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
34
- * 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
35
- * merge-json → set-stack
36
- * 4. 清掉 node_modules(package-lock.json 已经在 rules 里 delete),跑 npm install
37
- * 物化新依赖集合 —— migrate 改了 dependencies / devDependencies 集合(删 lite +
38
- * 加 NestJS 全套),不重装就跑不起来。
39
- * 同一条 npm install 顺手把 config.followLatestPackages 钉 `@latest`,覆盖
40
- * template caret range 拉不到 pre-release 的盲区(详见 MigrateConfig 注释)。
41
- *
42
- * 不做事:
43
- * - 不自动 commit / stash —— 让用户自己用 git review 改动
44
- * - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
31
+ * 同目标(磁盘已是目标布局)不再返回 MIGRATE_NOOP:走 repair 模式补齐适配 / 安装 / 构建,
32
+ * 支持"平台已新但恢复了旧代码"与"目标 meta 已写、安装未完成"的样本。
45
33
  */
46
34
  async function handleAppMigrate(opts) {
47
35
  await Promise.resolve();
48
- const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
49
- const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
50
- if (meta.stack === undefined || meta.stack === '') {
51
- throw new error_1.AppError('MIGRATE_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
52
- }
53
- const from = opts.from ?? meta.stack;
54
- if (opts.from !== undefined && opts.from !== meta.stack) {
55
- throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${meta.stack}'`, {
56
- next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'],
57
- });
58
- }
59
- if (from === opts.to) {
60
- throw new error_1.AppError('MIGRATE_NOOP', `from and to are both '${from}', nothing to do`);
61
- }
62
- const config = (0, index_1.getMigrateConfig)(from, opts.to);
63
- if (config === null) {
64
- const supported = (0, index_1.listSupportedMigrations)()
65
- .map((m) => `${m.from} → ${m.to}`)
66
- .join(', ');
67
- throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, {
68
- next_actions: [`已支持:${supported || '(none)'}`],
69
- });
70
- }
71
- // migrate 用 init 同款的 renderTemplate 拉目标 stack 的 npm template tarball 到 tmp,
72
- // 再把 tmp 当作 sourceRoot 喂给 applyMigrateRules。这样 migrate config 里 file/directory
73
- // rule 的 `from` 引用的是完整的 npm template 渲染产物,不需要在 upgrade/templates/ 重复
74
- // 维护一份 server/ + client/ + 全套配置 —— 复杂度由 init 那条链路 amortize。
75
- const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-migrate-'));
76
- const projectName = node_path_1.default.basename(targetDir);
77
- let templateVersion;
78
- let templateArchType;
79
- let results;
80
- try {
81
- const tpl = (0, index_2.renderTemplate)({ stack: opts.to, targetDir: tmpRoot, projectName });
82
- templateVersion = tpl.version;
83
- templateArchType = tpl.archType;
84
- // template package.json 的 user 私有字段(name / version / private 等)不参与 merge —— 否
85
- // 则会被 deepMergeJson 当成 template 端"权威值"覆盖 user 项目原本的字段。这里在 tmp 端
86
- // 直接 strip 这些字段,让 merge-json rule 只对 dependencies / devDependencies / scripts
87
- // 等 schema-level 字段生效。
88
- stripUserOwnedFields(node_path_1.default.join(tmpRoot, 'package.json'));
89
- (0, logger_1.log)('migrate', `Rendered ${opts.to}@${tpl.version} to tmp, applying ${config.rules.length.toString()} rule(s)`);
90
- results = (0, migrate_rule_1.applyMigrateRules)(config.rules, {
91
- sourceRoot: tmpRoot,
92
- targetDir,
93
- logPrefix: 'migrate',
94
- });
95
- }
96
- finally {
97
- node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
98
- }
99
- // archType / version 字段同步:set-stack rule 只切 stack,archType 是从 npm template 包
100
- // 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
101
- // renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
102
- (0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
103
- // 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
104
- // 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
105
- // 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
106
- // (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
107
- // 软失败:node_modules 不存在不报错。
108
- const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
109
- if (node_fs_1.default.existsSync(nodeModulesPath)) {
110
- (0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
111
- node_fs_1.default.rmSync(nodeModulesPath, { recursive: true, force: true });
112
- }
113
- // 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
114
- // --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
115
- // --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
116
- // 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
117
- const followLatest = config.followLatestPackages ?? [];
118
- const installArgs = [
119
- 'install',
120
- '--no-audit',
121
- '--no-fund',
122
- '--ignore-scripts',
123
- '--registry',
124
- (0, index_2.resolveNpmInstallRegistry)(),
125
- ...followLatest.map((pkg) => `${pkg}@latest`),
126
- ];
127
- (0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
128
- let installError;
129
- try {
130
- (0, run_npm_1.execNpmSync)(installArgs, {
131
- cwd: targetDir,
132
- stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
36
+ const purpose = opts.purpose ?? 'manual';
37
+ if (!index_1.MIGRATION_PURPOSES.includes(purpose)) {
38
+ throw new error_1.AppError('ARGS_INVALID', `不支持的 --purpose: ${purpose}`, {
39
+ next_actions: [`可用值:${index_1.MIGRATION_PURPOSES.join(', ')}`],
133
40
  });
134
41
  }
135
- catch (err) {
136
- installError = err instanceof Error ? err.message : String(err);
137
- (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
138
- }
139
- // 重启 dev process 前清 vite cache —— migrate 改了 package.json + lockfile,
140
- // 老 vite 进程的 .vite/deps cache 的 lockfileHash 跟新 lockfile 不一致。
141
- // 不清的话 supervisor 拉起的新 vite cold start 会立即 "Re-optimizing because
142
- // lockfile has changed", re-optimize 期间 ModuleGraph transform cache 跟
143
- // deps middleware metadata 短暂不同步, 浏览器请求老 hash 触发
144
- // ERR_OUTDATED_OPTIMIZED_DEP → 504 死锁(详见 fullstack-plugin#1143 的 504
145
- // ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
146
- // optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
147
- //
148
- // - 只在沙箱内做(isSandboxEnv;本地一般不会撞这条链路)
149
- // - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
150
- // - 软失败:.vite 不存在 / 删失败都不阻断
151
- if (installError === undefined && (0, env_1.isSandboxEnv)()) {
152
- const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
153
- if (node_fs_1.default.existsSync(viteCacheDir)) {
154
- (0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
155
- try {
156
- node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
157
- }
158
- catch (err) {
159
- (0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
160
- }
161
- }
42
+ const sandbox = (0, env_1.isSandboxEnv)();
43
+ const outcome = (0, index_1.runMigration)({
44
+ targetDir: node_path_1.default.resolve(opts.dir ?? process.cwd()),
45
+ to: opts.to,
46
+ from: opts.from,
47
+ appId: opts.appId,
48
+ purpose: purpose,
49
+ operationRef: opts.operationRef,
50
+ sandbox,
51
+ cliVersion: package_json_1.default.version,
52
+ quietStdout: (0, output_1.isJsonMode)(),
53
+ });
54
+ const data = toOutput(outcome, sandbox);
55
+ (0, output_1.emit)({ data });
56
+ if (isLenientInstallFailure(outcome)) {
57
+ (0, logger_1.log)('migrate', '⚠ npm install failed (continuing): 文件已转换,依赖需调用方补装');
58
+ return;
162
59
  }
163
- // 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
164
- // 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
165
- // 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
166
- // 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
167
- // - 只在沙箱内做(isSandboxEnv;本地环境用户进程混杂,主动 pkill 风险大)
168
- // - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
169
- // 留着旧进程让用户先处理 installError
170
- // - 软失败:pkill 无匹配进程退出 1,catch 吞掉
171
- // - pkill 是 POSIX-only,但整段被 isSandboxEnv() 包着(沙箱恒为 Linux),
172
- // Windows 本地永远走不到这里,故不需要跨平台封装
173
- let devRestarted = false;
174
- if (installError === undefined && (0, env_1.isSandboxEnv)()) {
175
- (0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
176
- try {
177
- (0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
178
- devRestarted = true;
179
- }
180
- catch {
181
- // pkill 无匹配进程时退出 1,正常情况(可能 dev 没在跑)
182
- }
60
+ if (outcome.status !== 'converted') {
61
+ // 结构化结果在 stdout;stderr 同时给出一条标准错误,便于只看退出码 / stderr 的调用方
62
+ (0, output_1.emitError)(new error_1.AppError(outcome.failure?.code ?? 'MIGRATE_FAILED', outcome.failure?.message ?? outcome.status, {
63
+ retryable: outcome.failure?.retryable ?? false,
64
+ next_actions: data.nextActions,
65
+ }));
66
+ process.exitCode = 1;
183
67
  }
184
- (0, output_1.emit)({
185
- data: {
186
- from,
187
- to: opts.to,
188
- templateVersion,
189
- appliedRules: results.map((r) => ({
190
- type: r.rule.type,
191
- action: r.action,
192
- path: r.path,
193
- detail: r.detail,
194
- })),
195
- ...summarizeResults(results),
196
- followLatestPackages: followLatest,
197
- installError,
198
- devRestarted,
199
- nextActions: (0, env_1.isSandboxEnv)()
200
- ? [
201
- 'git status / git diff 评估改动并 commit',
202
- 'miaoda skills sync 同步到新 stack 的 agent skills',
203
- ]
204
- : [
205
- 'git status / git diff 评估改动并 commit',
206
- '重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev(killOrphansByPort 会自动踢掉旧进程)',
207
- 'miaoda skills sync 同步到新 stack 的 agent skills',
208
- ],
209
- },
210
- });
211
68
  }
212
69
  /**
213
- * 从 tmp 端 template package.json 中删除 user 项目通常拥有的字段,避免 merge-json 时这些
214
- * 字段被 template 值覆盖。需要 strip 的字段以 npm 模板里"通常专属于具体应用"的为准。
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)按结构化结果推进,保持严格。
215
75
  */
216
- function stripUserOwnedFields(pkgPath) {
217
- if (!node_fs_1.default.existsSync(pkgPath))
218
- return;
219
- const json = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
220
- const userOwned = [
221
- 'name',
222
- 'version',
223
- 'private',
224
- 'description',
225
- 'author',
226
- 'keywords',
227
- 'license',
228
- 'repository',
229
- 'homepage',
230
- 'bugs',
231
- ];
232
- let changed = false;
233
- for (const key of userOwned) {
234
- if (Reflect.deleteProperty(json, key))
235
- changed = true;
76
+ function isLenientInstallFailure(outcome) {
77
+ return (outcome.purpose === 'manual' &&
78
+ outcome.status === 'failed' &&
79
+ outcome.failure?.stage === 'install');
80
+ }
81
+ function toOutput(outcome, sandbox) {
82
+ return {
83
+ status: outcome.status,
84
+ mode: outcome.mode,
85
+ runId: outcome.runId,
86
+ purpose: outcome.purpose,
87
+ conflictPolicy: outcome.conflictPolicy,
88
+ operationRef: outcome.operationRef,
89
+ from: outcome.source.stack,
90
+ to: outcome.target.stack,
91
+ source: outcome.source,
92
+ target: outcome.target,
93
+ templateVersion: outcome.target.templateVersion,
94
+ stages: outcome.stages,
95
+ failure: outcome.failure,
96
+ warnings: outcome.warnings,
97
+ conflicts: outcome.conflicts,
98
+ overwritten: outcome.overwritten,
99
+ changes: outcome.changes,
100
+ appliedRules: outcome.results.map((r) => ({
101
+ type: r.rule.type,
102
+ action: r.action,
103
+ path: r.path,
104
+ detail: r.detail,
105
+ })),
106
+ ...summarizeResults(outcome.results),
107
+ followLatestPackages: outcome.followLatestPackages,
108
+ install: outcome.install,
109
+ installError: outcome.install.error,
110
+ verify: outcome.verify,
111
+ originalIntact: outcome.originalIntact,
112
+ restartSignalSent: outcome.restartSignalSent,
113
+ // 兼容旧字段:只表示发出了重启信号,不是健康结果
114
+ devRestarted: outcome.restartSignalSent,
115
+ readiness: outcome.readiness,
116
+ nextActions: nextActions(outcome, sandbox),
117
+ };
118
+ }
119
+ function nextActions(outcome, sandbox) {
120
+ if (outcome.status === 'conflict') {
121
+ return [
122
+ outcome.originalIntact
123
+ ? '按 conflicts 列出的路径 / ID 手动处理冲突(预检发现,工程未写入)'
124
+ : '按 conflicts 列出的路径 / ID 手动处理冲突;工程已有部分改动(见 changes),冲突文件未覆盖',
125
+ '处理后对同一工程重新执行 migrate --to 同一目标,按真实布局继续 / 修复',
126
+ ];
127
+ }
128
+ if (outcome.status === 'failed') {
129
+ if (outcome.originalIntact) {
130
+ return [`工程未写入;修复 ${outcome.failure?.stage ?? 'unknown'} 阶段问题后重试`];
131
+ }
132
+ return [
133
+ `${outcome.failure?.stage ?? 'unknown'} 阶段失败,现场保留在原目录(部分改动见 changes),不会自动回滚`,
134
+ '修复问题后对同一工程重新执行 migrate --to 同一目标,按真实布局修复并接续安装 / 构建',
135
+ '如需放弃本次转换,由调用方按代码版本(如 git)恢复原工程',
136
+ ];
137
+ }
138
+ const policyActions = [];
139
+ if (outcome.overwritten.length > 0) {
140
+ policyActions.push('overwritten 列出的用户改动已被模板替换:用 git diff 核对,需要保留的业务改动从 git 历史手动合回');
236
141
  }
237
- if (changed) {
238
- node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + '\n');
142
+ if (outcome.warnings.some((w) => w.code === 'MIGRATE_CONFLICT_KEPT')) {
143
+ policyActions.push('warnings 中 MIGRATE_CONFLICT_KEPT 的位置保留了用户内容、未按目标 stack 适配,按需手动调整');
239
144
  }
145
+ return sandbox
146
+ ? [
147
+ ...policyActions,
148
+ '调用方核验沙箱 dev 进程与预览 readiness(restartSignalSent 只表示发出了重启信号)',
149
+ 'git status / git diff 评估改动并 commit',
150
+ 'miaoda skills sync 同步到新 stack 的 agent skills',
151
+ ]
152
+ : [
153
+ ...policyActions,
154
+ '重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev,并验证预览',
155
+ 'git status / git diff 评估改动并 commit',
156
+ 'miaoda skills sync 同步到新 stack 的 agent skills',
157
+ ];
240
158
  }
241
159
  function summarizeResults(results) {
242
160
  const moved = [];
@@ -270,6 +188,7 @@ function summarizeResults(results) {
270
188
  break;
271
189
  case 'appended':
272
190
  case 'noop':
191
+ case 'conflict':
273
192
  break;
274
193
  }
275
194
  }
@@ -18,6 +18,7 @@ const githooks_1 = require("../../../utils/githooks");
18
18
  const logs_dir_1 = require("../../../utils/logs-dir");
19
19
  const install_1 = require("../../../services/app/init/install");
20
20
  const spark_meta_1 = require("../../../utils/spark-meta");
21
+ const engineering_state_1 = require("../../../utils/engineering-state");
21
22
  const run_npm_1 = require("../../../utils/run-npm");
22
23
  const error_1 = require("../../../utils/error");
23
24
  const output_1 = require("../../../utils/output");
@@ -46,7 +47,7 @@ const MANAGED_PLATFORM_PACKAGES = {
46
47
  '@lark-apaas/http-client': 'latest',
47
48
  };
48
49
  /**
49
- * miaoda app sync [--dir <path>]
50
+ * miaoda app sync [--dir <path>] [--strict]
50
51
  *
51
52
  * 一站式同步:把当前 stack 的 sync 规则全套 apply 到 user app,对齐沙箱里
52
53
  * `npm run upgrade → npx fullstack-cli sync` 的行为,并补 miaoda-cli 本地开发独有规则。
@@ -68,7 +69,8 @@ const MANAGED_PLATFORM_PACKAGES = {
68
69
  async function handleAppSync(opts) {
69
70
  await Promise.resolve();
70
71
  const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
71
- const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
72
+ // 严格读:meta 损坏时报 SPARK_META_CORRUPT,不当成空对象继续
73
+ const meta = (0, spark_meta_1.readSparkMetaStrict)(targetDir) ?? {};
72
74
  if (meta.stack === undefined || meta.stack === '') {
73
75
  throw new error_1.AppError('SYNC_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
74
76
  }
@@ -106,7 +108,9 @@ async function handleAppSync(opts) {
106
108
  (0, logs_dir_1.ensureLogsDir)(targetDir);
107
109
  // 3. snapshot 当前 user app 装的管控包版本,供 install 后 diff 用
108
110
  const beforeSpecs = snapshotManagedDepSpecs(targetDir);
109
- // 4. npm install —— 跟 init 一样软失败,install 挂了不该阻断 emit / sync 总结输出。
111
+ // 4. npm install —— 失败时仍输出完整的规则应用结果,status=failed,不把"文件同步了、依赖没装上"
112
+ // 报成成功;已应用的规则保留(规则幂等,重跑 sync 补齐)。只有 --strict 以退出码 1 结束:
113
+ // dev.sh 与 lark-cli `+init` 不带 --strict,后者按退出码判成败,安装抖动不能让它整条失败。
110
114
  // 把 user app 装着的**所有**管控包显式作为 `<pkg>@latest` 位置参数传给 npm install:
111
115
  // - npm 自然会把 spec 改写成 ^具体最新版,lockfile 同步更新 → 不需要 sync 主动改 spec
112
116
  // - 没装的管控包不动 (sync 的职责是对齐,不是塞依赖)
@@ -134,20 +138,53 @@ async function handleAppSync(opts) {
134
138
  }
135
139
  catch (err) {
136
140
  installError = err instanceof Error ? err.message : String(err);
137
- (0, logger_1.log)('sync', `⚠ npm install failed (continuing): ${installError}`);
141
+ (0, logger_1.log)('sync', opts.strict
142
+ ? `✗ npm install failed: ${installError}`
143
+ : `⚠ npm install failed (continuing): ${installError}`);
138
144
  }
139
145
  // 5. install 之后再读一次 package.json,diff 出 npm 实际改写的 spec
140
146
  const afterSpecs = snapshotManagedDepSpecs(targetDir);
141
147
  const upgradedPackages = diffManagedSpecs(beforeSpecs, afterSpecs, 'sync');
148
+ const failed = installError !== undefined;
149
+ recordSyncState(targetDir, meta.stack, installError);
142
150
  (0, output_1.emit)({
143
151
  data: {
144
152
  stack: meta.stack,
153
+ status: failed ? 'failed' : 'succeeded',
154
+ failedStage: failed ? 'install' : undefined,
145
155
  ...summarizeSyncResults(syncResults),
146
156
  upgradedPackages,
147
157
  gitHooks: hookActivation.action,
148
158
  installError,
159
+ nextActions: failed
160
+ ? [
161
+ '检查上方 npm 输出定位安装错误(网络 / registry / 依赖冲突),修复后重跑 miaoda app sync',
162
+ ]
163
+ : undefined,
149
164
  },
150
165
  });
166
+ if (failed && opts.strict) {
167
+ (0, output_1.emitError)(new error_1.AppError('SYNC_INSTALL_FAILED', `npm install failed: ${installError ?? ''}`, {
168
+ retryable: true,
169
+ next_actions: ['修复安装错误(网络 / registry / 依赖冲突)后重跑 miaoda app sync'],
170
+ }));
171
+ process.exitCode = 1;
172
+ }
173
+ }
174
+ function recordSyncState(targetDir, stack, installError) {
175
+ try {
176
+ (0, engineering_state_1.writeEngineeringState)(targetDir, {
177
+ operation: 'sync',
178
+ status: installError === undefined ? 'succeeded' : 'failed',
179
+ stage: 'install',
180
+ code: installError === undefined ? undefined : 'SYNC_INSTALL_FAILED',
181
+ message: installError?.slice(0, 500),
182
+ toStack: stack,
183
+ });
184
+ }
185
+ catch (err) {
186
+ (0, logger_1.log)('sync', `⚠ failed to record engineering state: ${err instanceof Error ? err.message : String(err)}`);
187
+ }
151
188
  }
152
189
  /** 老命令 `miaoda app upgrade` 的 handler 名,保留为 sync handler 的 alias。 */
153
190
  exports.handleAppUpgrade = handleAppSync;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ /**
3
+ * nestjs-react-fullstack 工程里**明确由平台托管**的文件清单 —— sync 与 migrate 共用的 SSOT。
4
+ *
5
+ * 所有权用"本次会触及的明确清单"表达(不做通用三方合并、不补全量历史模板基线):
6
+ * - 清单内文件:按兼容模板版本整文件更新(同名覆盖)。
7
+ * - 清单外文件:一律视为用户所有。scripts/ 下用户自建的 report.js 等额外文件不删除、不覆盖。
8
+ *
9
+ * 清单取自本仓 upgrade/templates/nestjs-react-fullstack/templates 下的 scripts/ 与 .githooks/。
10
+ * test/config/fullstack-managed-files 会校验 upgrade 模板目录与本清单一一对应 —— 模板新增托管脚本
11
+ * 必须同步加到这里,否则 CI 失败。
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.FULLSTACK_MANAGED_BUILD_DEV_DEPENDENCIES = exports.FULLSTACK_MANAGED_FILES = exports.FULLSTACK_MANAGED_HOOK_FILES = exports.FULLSTACK_MANAGED_SCRIPT_FILES = void 0;
15
+ exports.managedFileRules = managedFileRules;
16
+ exports.managedBuildDependencyRules = managedBuildDependencyRules;
17
+ /** 平台托管的脚本(相对项目根) */
18
+ exports.FULLSTACK_MANAGED_SCRIPT_FILES = [
19
+ 'scripts/dev.sh',
20
+ 'scripts/dev.js',
21
+ 'scripts/dev-local.js',
22
+ 'scripts/build.sh',
23
+ 'scripts/run.sh',
24
+ 'scripts/lint.js',
25
+ 'scripts/prune-smart.js',
26
+ 'scripts/hooks/run-precommit.js',
27
+ ];
28
+ /** 平台托管的 git hook 入口(相对项目根) */
29
+ exports.FULLSTACK_MANAGED_HOOK_FILES = ['.githooks/pre-commit'];
30
+ /** 全部托管文件 */
31
+ exports.FULLSTACK_MANAGED_FILES = [
32
+ ...exports.FULLSTACK_MANAGED_SCRIPT_FILES,
33
+ ...exports.FULLSTACK_MANAGED_HOOK_FILES,
34
+ ];
35
+ /**
36
+ * 把托管清单展开成逐文件的 `file` 规则(overwrite=true)。
37
+ *
38
+ * 不用 `directory` 规则:directory 会把模板目录里**任何**文件都当托管文件同步过去,清单边界
39
+ * 由模板目录内容隐式决定;逐文件展开让"哪些文件归平台"显式可审。源文件缺失时规则返回
40
+ * skipped(模板与清单漂移由测试兜底)。
41
+ */
42
+ function managedFileRules() {
43
+ return exports.FULLSTACK_MANAGED_FILES.map((file) => ({
44
+ type: 'file',
45
+ from: file,
46
+ to: file,
47
+ overwrite: true,
48
+ }));
49
+ }
50
+ /**
51
+ * 托管构建脚本的配套依赖:build.sh 的智能裁剪(prune-smart.js)require('@vercel/nft'),模板与
52
+ * 沙箱镜像都不提供它。迁移后的工程第一次完整构建(发布或本地 npm run build)会用到,所以
53
+ * migrate / repair 对未声明的工程补 devDependencies(版本为实测的 0.30.4);已声明的用户版本
54
+ * 不降级、不覆盖。
55
+ */
56
+ exports.FULLSTACK_MANAGED_BUILD_DEV_DEPENDENCIES = {
57
+ '@vercel/nft': '0.30.4',
58
+ };
59
+ /** 把托管构建依赖展开成 `ensure-dependency` 规则(缺失才补,已声明保持用户版本)。 */
60
+ function managedBuildDependencyRules() {
61
+ return Object.entries(exports.FULLSTACK_MANAGED_BUILD_DEV_DEPENDENCIES).map(([name, version]) => ({
62
+ type: 'ensure-dependency',
63
+ name,
64
+ version,
65
+ section: 'devDependencies',
66
+ }));
67
+ }
@@ -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,
@@ -16,6 +16,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
16
16
  };
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.getMigrateConfig = getMigrateConfig;
19
+ exports.getRepairConfig = getRepairConfig;
19
20
  exports.listSupportedMigrations = listSupportedMigrations;
20
21
  const vite_react_to_nestjs_react_fullstack_1 = __importDefault(require("./vite-react-to-nestjs-react-fullstack"));
21
22
  const html_to_design_html_1 = __importDefault(require("./html-to-design-html"));
@@ -28,6 +29,14 @@ const MIGRATE_REGISTRY = {
28
29
  function getMigrateConfig(from, to) {
29
30
  return MIGRATE_REGISTRY[`${from}:${to}`] ?? null;
30
31
  }
32
+ /**
33
+ * 磁盘已是目标 stack 时(同目标重试、历史恢复后平台已新、旧版 CLI 半完成样本)用于 repair 的
34
+ * 配置:取第一条以 stack 为目标的迁移配置。规则本身对已转换工程幂等(源目录不存在即跳过、
35
+ * 定向适配已对齐即 noop),配合 repair 模式跳过 migrateOnly 规则。
36
+ */
37
+ function getRepairConfig(stack) {
38
+ return Object.values(MIGRATE_REGISTRY).find((c) => c.to === stack) ?? null;
39
+ }
31
40
  /** 列出已支持的迁移路径,给 handler / 命令 help 用 */
32
41
  function listSupportedMigrations() {
33
42
  return Object.keys(MIGRATE_REGISTRY).map((key) => {