@lark-apaas/miaoda-cli 0.1.37 → 0.1.39-alpha.0b274c0
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 +25 -0
- package/dist/api/deploy/api.js +3 -1
- package/dist/api/deploy/approval.js +77 -0
- package/dist/api/deploy/index.js +7 -1
- package/dist/api/deploy/schemas.js +3 -0
- package/dist/api/deploy/types.js +19 -1
- package/dist/cli/commands/app/index.js +59 -14
- package/dist/cli/commands/deploy/index.js +30 -6
- package/dist/cli/commands/deploy/modern.js +34 -11
- package/dist/cli/commands/skills/index.js +1 -1
- package/dist/cli/handlers/app/export.js +3 -3
- package/dist/cli/handlers/app/init.js +67 -2
- package/dist/cli/handlers/app/migrate.js +105 -213
- package/dist/cli/handlers/app/sync.js +39 -5
- package/dist/cli/handlers/db/sql.js +8 -6
- package/dist/cli/handlers/deploy/deploy.js +57 -4
- package/dist/cli/handlers/deploy/format.js +13 -0
- package/dist/cli/handlers/deploy/get.js +41 -1
- package/dist/cli/handlers/deploy/helpers.js +29 -2
- package/dist/cli/handlers/deploy/modern.js +39 -3
- package/dist/cli/handlers/deploy/polling.js +13 -9
- package/dist/cli/handlers/plugin/plugin-local.js +5 -4
- package/dist/config/fullstack-managed-files.js +67 -0
- package/dist/config/migrate-configs/index.js +9 -0
- package/dist/config/migrate-configs/vite-react-to-nestjs-react-fullstack.js +183 -193
- package/dist/config/sync-configs/design-stack.js +3 -4
- package/dist/config/sync-configs/index.js +3 -1
- package/dist/config/sync-configs/nestjs-react-fullstack.js +17 -23
- package/dist/config/sync-configs/vite-react.js +20 -0
- package/dist/services/app/init/async-install.js +34 -2
- package/dist/services/app/init/index.js +3 -1
- package/dist/services/app/init/install.js +4 -3
- 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/run.js +502 -0
- package/dist/services/app/migrate/symlinks.js +146 -0
- package/dist/services/deploy/modern/atoms/build.js +3 -3
- package/dist/services/deploy/modern/atoms/local-release.js +3 -0
- package/dist/services/deploy/modern/atoms/tosutil.js +6 -8
- package/dist/services/deploy/modern/patch/actions.js +7 -1
- package/dist/services/deploy/modern/patch/index.js +2 -1
- package/dist/services/deploy/modern/pipelines/design-local.js +7 -1
- package/dist/services/deploy/modern/pipelines/local.js +7 -1
- package/dist/utils/archive.js +103 -0
- package/dist/utils/coding-steering.js +30 -2
- package/dist/utils/engineering-state.js +52 -0
- package/dist/utils/file-ops.js +28 -8
- package/dist/utils/http.js +6 -6
- package/dist/utils/migrate-adapters.js +513 -0
- package/dist/utils/migrate-rule.js +508 -26
- package/dist/utils/npm-pack.js +6 -7
- package/dist/utils/project-snapshot.js +99 -0
- package/dist/utils/run-npm.js +102 -0
- package/dist/utils/spark-meta.js +31 -10
- package/dist/utils/sync-rule.js +86 -11
- package/dist/utils/typescript-exclude.js +36 -0
- package/dist/utils/which.js +27 -0
- package/package.json +6 -2
- package/upgrade/templates/nestjs-react-fullstack/templates/nest-cli.json +1 -10
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/build.sh +5 -0
- package/upgrade/templates/nestjs-react-fullstack/templates/scripts/prune-smart.js +41 -1
- package/upgrade/templates/vite-react/templates/scripts/build.sh +61 -0
|
@@ -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,54 @@ 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 并退出码 1。
|
|
37
42
|
*/
|
|
38
43
|
async function handleAppInit(opts) {
|
|
39
44
|
await Promise.resolve();
|
|
40
45
|
const targetDir = opts.targetDir ?? process.cwd();
|
|
41
46
|
const metaPath = node_path_1.default.join(targetDir, '.spark', 'meta.json');
|
|
42
47
|
if (node_fs_1.default.existsSync(metaPath)) {
|
|
48
|
+
let expectedStack;
|
|
49
|
+
if (opts.template || opts.appType) {
|
|
50
|
+
try {
|
|
51
|
+
expectedStack = (0, index_1.resolveStack)({ template: opts.template, appType: opts.appType });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// 非严格调用保持历史行为:已初始化时不因参数校验失败而报错
|
|
55
|
+
if (opts.strict)
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const check = (0, index_1.checkWorkspaceReady)({ targetDir, expectedStack });
|
|
43
60
|
if (!(0, output_1.isJsonMode)()) {
|
|
44
61
|
process.stdout.write(`Already initialized: ${metaPath}\n`);
|
|
62
|
+
for (const p of check.problems) {
|
|
63
|
+
process.stdout.write(` ✗ ${p.code}: ${p.message}\n`);
|
|
64
|
+
}
|
|
45
65
|
}
|
|
46
66
|
(0, output_1.emit)({
|
|
47
67
|
data: {
|
|
48
68
|
initialized: false,
|
|
49
69
|
reason: 'already_initialized',
|
|
50
70
|
targetDir,
|
|
71
|
+
stack: check.stack,
|
|
72
|
+
ready: check.ready,
|
|
73
|
+
problems: check.problems,
|
|
51
74
|
},
|
|
52
75
|
});
|
|
76
|
+
if (opts.strict && !check.ready) {
|
|
77
|
+
(0, output_1.emitError)(new error_1.AppError('WORKSPACE_NOT_READY', check.problems.map((p) => `${p.code}: ${p.message}`).join('; '), {
|
|
78
|
+
next_actions: [
|
|
79
|
+
'按 problems 修复:依赖 / 文件问题执行 miaoda app sync,布局不符执行 miaoda app migrate',
|
|
80
|
+
],
|
|
81
|
+
}));
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
}
|
|
53
84
|
return;
|
|
54
85
|
}
|
|
55
86
|
const stack = (0, index_1.resolveStack)({ template: opts.template, appType: opts.appType });
|
|
@@ -130,21 +161,29 @@ async function handleAppInit(opts) {
|
|
|
130
161
|
app_id: opts.appId,
|
|
131
162
|
});
|
|
132
163
|
// async 模式:meta 已落盘(= 脚手架就绪),再派发后台安装并立即返回(不等装完)。
|
|
164
|
+
// 执行记录先标 running,后台 worker 结束时改写为 succeeded / failed;中断则停留在 running。
|
|
133
165
|
let asyncDispatch;
|
|
134
166
|
if (opts.asyncInstall) {
|
|
167
|
+
recordInitState(targetDir, 'running', 'install');
|
|
135
168
|
asyncDispatch = (0, index_1.dispatchAsyncInstall)({ targetDir });
|
|
136
169
|
}
|
|
170
|
+
const installFailed = installResult?.source === 'failed';
|
|
171
|
+
if (installResult !== undefined) {
|
|
172
|
+
recordInitState(targetDir, installFailed ? 'failed' : 'succeeded', installResult.source === 'skipped' ? 'install-skipped' : 'install', installResult.error);
|
|
173
|
+
}
|
|
137
174
|
if (!(0, output_1.isJsonMode)()) {
|
|
138
175
|
if (opts.asyncInstall && asyncDispatch) {
|
|
139
176
|
process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install dispatched in background pid ${String(asyncDispatch.pid)}) in ${targetDir}\n`);
|
|
140
177
|
}
|
|
141
178
|
else if (installResult) {
|
|
142
|
-
process.stdout.write(
|
|
179
|
+
process.stdout.write(`${installFailed ? '✗' : '✓'} Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
|
|
143
180
|
}
|
|
144
181
|
}
|
|
145
182
|
(0, output_1.emit)({
|
|
146
183
|
data: {
|
|
147
184
|
initialized: true,
|
|
185
|
+
status: installFailed ? 'failed' : 'succeeded',
|
|
186
|
+
failedStage: installFailed ? 'install' : undefined,
|
|
148
187
|
template: stack,
|
|
149
188
|
templatePackage: tplResult.packageName,
|
|
150
189
|
templateVersion: tplResult.version,
|
|
@@ -177,8 +216,34 @@ async function handleAppInit(opts) {
|
|
|
177
216
|
cacheZip: installResult?.cacheZip,
|
|
178
217
|
installError: installResult?.error,
|
|
179
218
|
}),
|
|
219
|
+
nextActions: installFailed
|
|
220
|
+
? [
|
|
221
|
+
'检查 npm 输出定位安装错误(网络 / registry / 依赖冲突),修复后执行 miaoda app sync 补装依赖',
|
|
222
|
+
]
|
|
223
|
+
: undefined,
|
|
180
224
|
},
|
|
181
225
|
});
|
|
226
|
+
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;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function recordInitState(targetDir, status, stage, error) {
|
|
235
|
+
try {
|
|
236
|
+
(0, engineering_state_1.writeEngineeringState)(targetDir, {
|
|
237
|
+
operation: 'init',
|
|
238
|
+
status,
|
|
239
|
+
stage,
|
|
240
|
+
code: status === 'failed' ? 'INIT_INSTALL_FAILED' : undefined,
|
|
241
|
+
message: error?.slice(0, 500),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
(0, logger_1.log)('init', `⚠ failed to record engineering state: ${err instanceof Error ? err.message : String(err)}`);
|
|
246
|
+
}
|
|
182
247
|
}
|
|
183
248
|
/**
|
|
184
249
|
* 处理 --source-path 导入:当前仅 app_type=modern_html 生效。
|
|
@@ -4,236 +4,127 @@ 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
|
-
const
|
|
12
|
+
const package_json_1 = __importDefault(require("../../../../package.json"));
|
|
19
13
|
/**
|
|
20
|
-
* miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
|
|
14
|
+
* miaoda app migrate --to <stack> [--from <stack>] [--dir <path>] [--purpose <p>] [--operation-ref <id>]
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
* 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
|
|
24
|
-
* 自行 `git status` / `git diff` 评估并 commit。
|
|
16
|
+
* 工程转换原语的 CLI 入口:参数校验 + 结构化输出,转换流程在 services/app/migrate。
|
|
25
17
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
18
|
+
* 结果语义:
|
|
19
|
+
* status=converted:原目录内文件适配、依赖安装、严格验证(全栈为完整构建)全部完成。
|
|
20
|
+
* 运行 readiness 不在 CLI 内判定(readiness.status=pending_caller_verification)——
|
|
21
|
+
* 沙箱里 restartSignalSent / devRestarted 只表示发出了 pkill 重启信号,不是健康结果。
|
|
22
|
+
* status=conflict:存在无法确定性处理的冲突(conflicts 给出具体路径 / ID),冲突文件不覆盖;
|
|
23
|
+
* 预检阶段发现时工程未写入(originalIntact=true),应用阶段发现时可能已有部分改动。
|
|
24
|
+
* status=failed:某阶段失败(failure.stage / code),现场原样保留、不自动回滚;
|
|
25
|
+
* originalIntact=true 仅表示失败发生在任何工程写入之前。
|
|
26
|
+
* 非 converted 一律退出码 1。
|
|
29
27
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
|
|
33
|
-
* 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
|
|
34
|
-
* merge-json → set-stack
|
|
35
|
-
* 4. 清掉 node_modules(package-lock.json 已经在 rules 里 delete),跑 npm install
|
|
36
|
-
* 物化新依赖集合 —— migrate 改了 dependencies / devDependencies 集合(删 lite +
|
|
37
|
-
* 加 NestJS 全套),不重装就跑不起来。
|
|
38
|
-
* 同一条 npm install 顺手把 config.followLatestPackages 钉 `@latest`,覆盖
|
|
39
|
-
* template caret range 拉不到 pre-release 的盲区(详见 MigrateConfig 注释)。
|
|
40
|
-
*
|
|
41
|
-
* 不做事:
|
|
42
|
-
* - 不自动 commit / stash —— 让用户自己用 git review 改动
|
|
43
|
-
* - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
|
|
28
|
+
* 同目标(磁盘已是目标布局)不再返回 MIGRATE_NOOP:走 repair 模式补齐适配 / 安装 / 构建,
|
|
29
|
+
* 支持"平台已新但恢复了旧代码"与"目标 meta 已写、安装未完成"的样本。
|
|
44
30
|
*/
|
|
45
31
|
async function handleAppMigrate(opts) {
|
|
46
32
|
await Promise.resolve();
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
const from = opts.from ?? meta.stack;
|
|
53
|
-
if (opts.from !== undefined && opts.from !== meta.stack) {
|
|
54
|
-
throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${meta.stack}'`, {
|
|
55
|
-
next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'],
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
if (from === opts.to) {
|
|
59
|
-
throw new error_1.AppError('MIGRATE_NOOP', `from and to are both '${from}', nothing to do`);
|
|
60
|
-
}
|
|
61
|
-
const config = (0, index_1.getMigrateConfig)(from, opts.to);
|
|
62
|
-
if (config === null) {
|
|
63
|
-
const supported = (0, index_1.listSupportedMigrations)()
|
|
64
|
-
.map((m) => `${m.from} → ${m.to}`)
|
|
65
|
-
.join(', ');
|
|
66
|
-
throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, {
|
|
67
|
-
next_actions: [`已支持:${supported || '(none)'}`],
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
// migrate 用 init 同款的 renderTemplate 拉目标 stack 的 npm template tarball 到 tmp,
|
|
71
|
-
// 再把 tmp 当作 sourceRoot 喂给 applyMigrateRules。这样 migrate config 里 file/directory
|
|
72
|
-
// rule 的 `from` 引用的是完整的 npm template 渲染产物,不需要在 upgrade/templates/ 重复
|
|
73
|
-
// 维护一份 server/ + client/ + 全套配置 —— 复杂度由 init 那条链路 amortize。
|
|
74
|
-
const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-migrate-'));
|
|
75
|
-
const projectName = node_path_1.default.basename(targetDir);
|
|
76
|
-
let templateVersion;
|
|
77
|
-
let templateArchType;
|
|
78
|
-
let results;
|
|
79
|
-
try {
|
|
80
|
-
const tpl = (0, index_2.renderTemplate)({ stack: opts.to, targetDir: tmpRoot, projectName });
|
|
81
|
-
templateVersion = tpl.version;
|
|
82
|
-
templateArchType = tpl.archType;
|
|
83
|
-
// template package.json 的 user 私有字段(name / version / private 等)不参与 merge —— 否
|
|
84
|
-
// 则会被 deepMergeJson 当成 template 端"权威值"覆盖 user 项目原本的字段。这里在 tmp 端
|
|
85
|
-
// 直接 strip 这些字段,让 merge-json rule 只对 dependencies / devDependencies / scripts
|
|
86
|
-
// 等 schema-level 字段生效。
|
|
87
|
-
stripUserOwnedFields(node_path_1.default.join(tmpRoot, 'package.json'));
|
|
88
|
-
(0, logger_1.log)('migrate', `Rendered ${opts.to}@${tpl.version} to tmp, applying ${config.rules.length.toString()} rule(s)`);
|
|
89
|
-
results = (0, migrate_rule_1.applyMigrateRules)(config.rules, {
|
|
90
|
-
sourceRoot: tmpRoot,
|
|
91
|
-
targetDir,
|
|
92
|
-
logPrefix: 'migrate',
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
|
|
97
|
-
}
|
|
98
|
-
// archType / version 字段同步:set-stack rule 只切 stack,archType 是从 npm template 包
|
|
99
|
-
// 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
|
|
100
|
-
// renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
|
|
101
|
-
(0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
|
|
102
|
-
// 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
|
|
103
|
-
// 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
|
|
104
|
-
// 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
|
|
105
|
-
// (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
|
|
106
|
-
// 软失败:node_modules 不存在不报错。
|
|
107
|
-
const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
|
|
108
|
-
if (node_fs_1.default.existsSync(nodeModulesPath)) {
|
|
109
|
-
(0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
|
|
110
|
-
node_fs_1.default.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
// 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
|
|
113
|
-
// --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
|
|
114
|
-
// --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
|
|
115
|
-
// 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
|
|
116
|
-
const followLatest = config.followLatestPackages ?? [];
|
|
117
|
-
const installArgs = [
|
|
118
|
-
'install',
|
|
119
|
-
'--no-audit',
|
|
120
|
-
'--no-fund',
|
|
121
|
-
'--ignore-scripts',
|
|
122
|
-
'--registry',
|
|
123
|
-
(0, index_2.resolveNpmInstallRegistry)(),
|
|
124
|
-
...followLatest.map((pkg) => `${pkg}@latest`),
|
|
125
|
-
];
|
|
126
|
-
(0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
|
|
127
|
-
let installError;
|
|
128
|
-
try {
|
|
129
|
-
(0, node_child_process_1.execFileSync)('npm', installArgs, {
|
|
130
|
-
cwd: targetDir,
|
|
131
|
-
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
33
|
+
const purpose = opts.purpose ?? 'manual';
|
|
34
|
+
if (!index_1.MIGRATION_PURPOSES.includes(purpose)) {
|
|
35
|
+
throw new error_1.AppError('ARGS_INVALID', `不支持的 --purpose: ${purpose}`, {
|
|
36
|
+
next_actions: [`可用值:${index_1.MIGRATION_PURPOSES.join(', ')}`],
|
|
132
37
|
});
|
|
133
38
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
|
|
146
|
-
//
|
|
147
|
-
// - 只在沙箱内做(isSandboxEnv;本地一般不会撞这条链路)
|
|
148
|
-
// - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
|
|
149
|
-
// - 软失败:.vite 不存在 / 删失败都不阻断
|
|
150
|
-
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
151
|
-
const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
|
|
152
|
-
if (node_fs_1.default.existsSync(viteCacheDir)) {
|
|
153
|
-
(0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
|
|
154
|
-
try {
|
|
155
|
-
node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
(0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
|
|
163
|
-
// 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
|
|
164
|
-
// 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
|
|
165
|
-
// 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
|
|
166
|
-
// - 只在沙箱内做(isSandboxEnv;本地环境用户进程混杂,主动 pkill 风险大)
|
|
167
|
-
// - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
|
|
168
|
-
// 留着旧进程让用户先处理 installError
|
|
169
|
-
// - 软失败:pkill 无匹配进程退出 1,catch 吞掉
|
|
170
|
-
let devRestarted = false;
|
|
171
|
-
if (installError === undefined && (0, env_1.isSandboxEnv)()) {
|
|
172
|
-
(0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
|
|
173
|
-
try {
|
|
174
|
-
(0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
|
|
175
|
-
devRestarted = true;
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// pkill 无匹配进程时退出 1,正常情况(可能 dev 没在跑)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
(0, output_1.emit)({
|
|
182
|
-
data: {
|
|
183
|
-
from,
|
|
184
|
-
to: opts.to,
|
|
185
|
-
templateVersion,
|
|
186
|
-
appliedRules: results.map((r) => ({
|
|
187
|
-
type: r.rule.type,
|
|
188
|
-
action: r.action,
|
|
189
|
-
path: r.path,
|
|
190
|
-
detail: r.detail,
|
|
191
|
-
})),
|
|
192
|
-
...summarizeResults(results),
|
|
193
|
-
followLatestPackages: followLatest,
|
|
194
|
-
installError,
|
|
195
|
-
devRestarted,
|
|
196
|
-
nextActions: (0, env_1.isSandboxEnv)()
|
|
197
|
-
? [
|
|
198
|
-
'git status / git diff 评估改动并 commit',
|
|
199
|
-
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
200
|
-
]
|
|
201
|
-
: [
|
|
202
|
-
'git status / git diff 评估改动并 commit',
|
|
203
|
-
'重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev(killOrphansByPort 会自动踢掉旧进程)',
|
|
204
|
-
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
205
|
-
],
|
|
206
|
-
},
|
|
39
|
+
const sandbox = (0, env_1.isSandboxEnv)();
|
|
40
|
+
const outcome = (0, index_1.runMigration)({
|
|
41
|
+
targetDir: node_path_1.default.resolve(opts.dir ?? process.cwd()),
|
|
42
|
+
to: opts.to,
|
|
43
|
+
from: opts.from,
|
|
44
|
+
appId: opts.appId,
|
|
45
|
+
purpose: purpose,
|
|
46
|
+
operationRef: opts.operationRef,
|
|
47
|
+
sandbox,
|
|
48
|
+
cliVersion: package_json_1.default.version,
|
|
49
|
+
quietStdout: (0, output_1.isJsonMode)(),
|
|
207
50
|
});
|
|
51
|
+
const data = toOutput(outcome, sandbox);
|
|
52
|
+
(0, output_1.emit)({ data });
|
|
53
|
+
if (outcome.status !== 'converted') {
|
|
54
|
+
// 结构化结果在 stdout;stderr 同时给出一条标准错误,便于只看退出码 / stderr 的调用方
|
|
55
|
+
(0, output_1.emitError)(new error_1.AppError(outcome.failure?.code ?? 'MIGRATE_FAILED', outcome.failure?.message ?? outcome.status, {
|
|
56
|
+
retryable: outcome.failure?.retryable ?? false,
|
|
57
|
+
next_actions: data.nextActions,
|
|
58
|
+
}));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
208
61
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
62
|
+
function toOutput(outcome, sandbox) {
|
|
63
|
+
return {
|
|
64
|
+
status: outcome.status,
|
|
65
|
+
mode: outcome.mode,
|
|
66
|
+
runId: outcome.runId,
|
|
67
|
+
purpose: outcome.purpose,
|
|
68
|
+
operationRef: outcome.operationRef,
|
|
69
|
+
from: outcome.source.stack,
|
|
70
|
+
to: outcome.target.stack,
|
|
71
|
+
source: outcome.source,
|
|
72
|
+
target: outcome.target,
|
|
73
|
+
templateVersion: outcome.target.templateVersion,
|
|
74
|
+
stages: outcome.stages,
|
|
75
|
+
failure: outcome.failure,
|
|
76
|
+
warnings: outcome.warnings,
|
|
77
|
+
conflicts: outcome.conflicts,
|
|
78
|
+
changes: outcome.changes,
|
|
79
|
+
appliedRules: outcome.results.map((r) => ({
|
|
80
|
+
type: r.rule.type,
|
|
81
|
+
action: r.action,
|
|
82
|
+
path: r.path,
|
|
83
|
+
detail: r.detail,
|
|
84
|
+
})),
|
|
85
|
+
...summarizeResults(outcome.results),
|
|
86
|
+
followLatestPackages: outcome.followLatestPackages,
|
|
87
|
+
install: outcome.install,
|
|
88
|
+
installError: outcome.install.error,
|
|
89
|
+
verify: outcome.verify,
|
|
90
|
+
originalIntact: outcome.originalIntact,
|
|
91
|
+
restartSignalSent: outcome.restartSignalSent,
|
|
92
|
+
// 兼容旧字段:只表示发出了重启信号,不是健康结果
|
|
93
|
+
devRestarted: outcome.restartSignalSent,
|
|
94
|
+
readiness: outcome.readiness,
|
|
95
|
+
nextActions: nextActions(outcome, sandbox),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function nextActions(outcome, sandbox) {
|
|
99
|
+
if (outcome.status === 'conflict') {
|
|
100
|
+
return [
|
|
101
|
+
outcome.originalIntact
|
|
102
|
+
? '按 conflicts 列出的路径 / ID 手动处理冲突(预检发现,工程未写入)'
|
|
103
|
+
: '按 conflicts 列出的路径 / ID 手动处理冲突;工程已有部分改动(见 changes),冲突文件未覆盖',
|
|
104
|
+
'处理后对同一工程重新执行 migrate --to 同一目标,按真实布局继续 / 修复',
|
|
105
|
+
];
|
|
233
106
|
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
107
|
+
if (outcome.status === 'failed') {
|
|
108
|
+
if (outcome.originalIntact) {
|
|
109
|
+
return [`工程未写入;修复 ${outcome.failure?.stage ?? 'unknown'} 阶段问题后重试`];
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
`${outcome.failure?.stage ?? 'unknown'} 阶段失败,现场保留在原目录(部分改动见 changes),不会自动回滚`,
|
|
113
|
+
'修复问题后对同一工程重新执行 migrate --to 同一目标,按真实布局修复并接续安装 / 构建',
|
|
114
|
+
'如需放弃本次转换,由调用方按代码版本(如 git)恢复原工程',
|
|
115
|
+
];
|
|
236
116
|
}
|
|
117
|
+
return sandbox
|
|
118
|
+
? [
|
|
119
|
+
'调用方核验沙箱 dev 进程与预览 readiness(restartSignalSent 只表示发出了重启信号)',
|
|
120
|
+
'git status / git diff 评估改动并 commit',
|
|
121
|
+
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
122
|
+
]
|
|
123
|
+
: [
|
|
124
|
+
'重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev,并验证预览',
|
|
125
|
+
'git status / git diff 评估改动并 commit',
|
|
126
|
+
'miaoda skills sync 同步到新 stack 的 agent skills',
|
|
127
|
+
];
|
|
237
128
|
}
|
|
238
129
|
function summarizeResults(results) {
|
|
239
130
|
const moved = [];
|
|
@@ -267,6 +158,7 @@ function summarizeResults(results) {
|
|
|
267
158
|
break;
|
|
268
159
|
case 'appended':
|
|
269
160
|
case 'noop':
|
|
161
|
+
case 'conflict':
|
|
270
162
|
break;
|
|
271
163
|
}
|
|
272
164
|
}
|
|
@@ -9,7 +9,6 @@ exports.runStackSync = runStackSync;
|
|
|
9
9
|
exports.summarizeSyncResults = summarizeSyncResults;
|
|
10
10
|
exports.snapshotManagedDepSpecs = snapshotManagedDepSpecs;
|
|
11
11
|
exports.diffManagedSpecs = diffManagedSpecs;
|
|
12
|
-
const node_child_process_1 = require("node:child_process");
|
|
13
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
14
|
const sync_configs_1 = require("../../../config/sync-configs");
|
|
@@ -19,6 +18,8 @@ const githooks_1 = require("../../../utils/githooks");
|
|
|
19
18
|
const logs_dir_1 = require("../../../utils/logs-dir");
|
|
20
19
|
const install_1 = require("../../../services/app/init/install");
|
|
21
20
|
const spark_meta_1 = require("../../../utils/spark-meta");
|
|
21
|
+
const engineering_state_1 = require("../../../utils/engineering-state");
|
|
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");
|
|
24
25
|
const logger_1 = require("../../../utils/logger");
|
|
@@ -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,8 @@ 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 + 退出码 1,
|
|
112
|
+
// 不把"文件同步了、依赖没装上"报成成功。已应用的规则保留(规则幂等,重跑 sync 补齐)。
|
|
110
113
|
// 把 user app 装着的**所有**管控包显式作为 `<pkg>@latest` 位置参数传给 npm install:
|
|
111
114
|
// - npm 自然会把 spec 改写成 ^具体最新版,lockfile 同步更新 → 不需要 sync 主动改 spec
|
|
112
115
|
// - 没装的管控包不动 (sync 的职责是对齐,不是塞依赖)
|
|
@@ -127,27 +130,58 @@ async function handleAppSync(opts) {
|
|
|
127
130
|
(0, logger_1.log)('sync', `Running npm ${installArgs.join(' ')}...`);
|
|
128
131
|
let installError;
|
|
129
132
|
try {
|
|
130
|
-
(0,
|
|
133
|
+
(0, run_npm_1.execNpmSync)(installArgs, {
|
|
131
134
|
cwd: targetDir,
|
|
132
135
|
stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
catch (err) {
|
|
136
139
|
installError = err instanceof Error ? err.message : String(err);
|
|
137
|
-
(0, logger_1.log)('sync',
|
|
140
|
+
(0, logger_1.log)('sync', `✗ npm install failed: ${installError}`);
|
|
138
141
|
}
|
|
139
142
|
// 5. install 之后再读一次 package.json,diff 出 npm 实际改写的 spec
|
|
140
143
|
const afterSpecs = snapshotManagedDepSpecs(targetDir);
|
|
141
144
|
const upgradedPackages = diffManagedSpecs(beforeSpecs, afterSpecs, 'sync');
|
|
145
|
+
const failed = installError !== undefined;
|
|
146
|
+
recordSyncState(targetDir, meta.stack, installError);
|
|
142
147
|
(0, output_1.emit)({
|
|
143
148
|
data: {
|
|
144
149
|
stack: meta.stack,
|
|
150
|
+
status: failed ? 'failed' : 'succeeded',
|
|
151
|
+
failedStage: failed ? 'install' : undefined,
|
|
145
152
|
...summarizeSyncResults(syncResults),
|
|
146
153
|
upgradedPackages,
|
|
147
154
|
gitHooks: hookActivation.action,
|
|
148
155
|
installError,
|
|
156
|
+
nextActions: failed
|
|
157
|
+
? [
|
|
158
|
+
'检查上方 npm 输出定位安装错误(网络 / registry / 依赖冲突),修复后重跑 miaoda app sync',
|
|
159
|
+
]
|
|
160
|
+
: undefined,
|
|
149
161
|
},
|
|
150
162
|
});
|
|
163
|
+
if (failed) {
|
|
164
|
+
(0, output_1.emitError)(new error_1.AppError('SYNC_INSTALL_FAILED', `npm install failed: ${installError ?? ''}`, {
|
|
165
|
+
retryable: true,
|
|
166
|
+
next_actions: ['修复安装错误(网络 / registry / 依赖冲突)后重跑 miaoda app sync'],
|
|
167
|
+
}));
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function recordSyncState(targetDir, stack, installError) {
|
|
172
|
+
try {
|
|
173
|
+
(0, engineering_state_1.writeEngineeringState)(targetDir, {
|
|
174
|
+
operation: 'sync',
|
|
175
|
+
status: installError === undefined ? 'succeeded' : 'failed',
|
|
176
|
+
stage: 'install',
|
|
177
|
+
code: installError === undefined ? undefined : 'SYNC_INSTALL_FAILED',
|
|
178
|
+
message: installError?.slice(0, 500),
|
|
179
|
+
toStack: stack,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
(0, logger_1.log)('sync', `⚠ failed to record engineering state: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
}
|
|
151
185
|
}
|
|
152
186
|
/** 老命令 `miaoda app upgrade` 的 handler 名,保留为 sync handler 的 alias。 */
|
|
153
187
|
exports.handleAppUpgrade = handleAppSync;
|
|
@@ -47,7 +47,7 @@ const colors_1 = require("../../../utils/colors");
|
|
|
47
47
|
const fuzzy_match_1 = require("../../../utils/fuzzy-match");
|
|
48
48
|
const index_1 = require("../../../api/db/index");
|
|
49
49
|
const sql_keywords_1 = require("../../../api/db/sql-keywords");
|
|
50
|
-
const
|
|
50
|
+
const run_npm_1 = require("../../../utils/run-npm");
|
|
51
51
|
const node_fs_1 = require("node:fs");
|
|
52
52
|
const node_path_1 = __importDefault(require("node:path"));
|
|
53
53
|
/**
|
|
@@ -140,20 +140,22 @@ async function maybeSyncAgentSchema(results) {
|
|
|
140
140
|
(0, logger_1.debug)(`[db sql] DDL detected, running \`npm run gen:db-schema\` in ${projectRoot}`);
|
|
141
141
|
try {
|
|
142
142
|
await new Promise((resolve) => {
|
|
143
|
-
const proc = (0,
|
|
143
|
+
const proc = (0, run_npm_1.spawnNpm)(['run', 'gen:db-schema'], {
|
|
144
144
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
145
145
|
cwd: projectRoot,
|
|
146
146
|
});
|
|
147
147
|
// --verbose:实时透传到 stderr 看进度;默认完全静默丢弃。
|
|
148
148
|
// 任何路径下子进程输出都不会进入当前 CLI 的 stdout,--json 模式安全。
|
|
149
149
|
// 注:默认路径必须 attach 监听器,否则 pipe 缓冲区写满会反压子进程。
|
|
150
|
+
// `?.`:stdio 走数组形式时 node 类型上 stdout/stderr 可为 null。这里 fd1/fd2
|
|
151
|
+
// 显式给了 'pipe',实际恒非 null;真为 null 也意味着不是 pipe、不存在缓冲反压。
|
|
150
152
|
if (verbose) {
|
|
151
|
-
proc.stdout
|
|
152
|
-
proc.stderr
|
|
153
|
+
proc.stdout?.pipe(process.stderr);
|
|
154
|
+
proc.stderr?.pipe(process.stderr);
|
|
153
155
|
}
|
|
154
156
|
else {
|
|
155
|
-
proc.stdout
|
|
156
|
-
proc.stderr
|
|
157
|
+
proc.stdout?.on('data', drainChunk);
|
|
158
|
+
proc.stderr?.on('data', drainChunk);
|
|
157
159
|
}
|
|
158
160
|
let timedOut = false;
|
|
159
161
|
const timer = setTimeout(() => {
|