@lark-apaas/miaoda-cli 0.1.46 → 0.1.48
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 +20 -0
- package/dist/cli/commands/app/index.js +87 -15
- package/dist/cli/handlers/app/init.js +74 -2
- package/dist/cli/handlers/app/migrate.js +131 -212
- package/dist/cli/handlers/app/sync.js +41 -4
- package/dist/config/fullstack-managed-files.js +67 -0
- package/dist/config/migrate-configs/html-to-design-html.js +1 -1
- package/dist/config/migrate-configs/index.js +9 -0
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +227 -197
- package/dist/config/sync-configs/design-stack.js +3 -4
- package/dist/config/sync-configs/nestjs-react-fullstack.js +11 -23
- package/dist/services/app/init/async-install.js +21 -0
- package/dist/services/app/init/index.js +3 -1
- package/dist/services/app/init/workspace-check.js +105 -0
- package/dist/services/app/migrate/detect.js +34 -0
- package/dist/services/app/migrate/index.js +8 -0
- package/dist/services/app/migrate/restart.js +59 -0
- package/dist/services/app/migrate/run.js +565 -0
- package/dist/services/app/migrate/symlinks.js +146 -0
- package/dist/utils/engineering-state.js +52 -0
- package/dist/utils/migrate-adapters.js +634 -0
- package/dist/utils/migrate-rule.js +801 -36
- package/dist/utils/project-snapshot.js +99 -0
- package/dist/utils/spark-meta.js +31 -10
- package/dist/utils/sync-rule.js +80 -11
- package/package.json +1 -1
|
@@ -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("../../../
|
|
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
|
|
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
|
-
*
|
|
24
|
-
* 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
|
|
25
|
-
* 自行 `git status` / `git diff` 评估并 commit。
|
|
17
|
+
* 工程转换原语的 CLI 入口:参数校验 + 结构化输出,转换流程在 services/app/migrate。
|
|
26
18
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
*
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
*
|
|
214
|
-
*
|
|
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
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 (
|
|
238
|
-
|
|
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
|
-
|
|
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 ——
|
|
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',
|
|
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
|
|
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) => {
|