@lark-apaas/miaoda-cli 0.1.16 → 0.1.17-alpha.ddef3e9

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.
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerAppCommands = registerAppCommands;
4
+ const commander_1 = require("commander");
4
5
  const shared_1 = require("../../../cli/commands/shared");
5
6
  const index_1 = require("../../../cli/handlers/app/index");
7
+ const index_2 = require("../../../config/migrate-configs/index");
6
8
  const error_1 = require("../../../utils/error");
7
9
  function registerAppCommands(program, opts = {}) {
8
10
  const description = opts.includeInit
@@ -22,8 +24,49 @@ function registerAppCommands(program, opts = {}) {
22
24
  if (opts.includeInit) {
23
25
  registerAppInit(appCmd);
24
26
  registerAppSync(appCmd);
27
+ registerAppMigrate(appCmd);
25
28
  }
26
29
  }
30
+ function registerAppMigrate(parent) {
31
+ const supportedDesc = (0, index_2.listSupportedMigrations)()
32
+ .map((m) => `${m.from} → ${m.to}`)
33
+ .join(', ');
34
+ // 渐进式披露:migrate 是给妙搭平台 / 用户主动触发的命令,**不暴露给 Agent**。
35
+ // - `miaoda app -h` / `miaoda app help` 都不列出此命令
36
+ // - 但 `miaoda app migrate --to xxx` 仍能直接调用(仅命令注册时打 hidden 标记,行为不变)
37
+ // - `--help` 也不显示 ——commander hidden 命令仅显式 invoke 时才暴露
38
+ // 用 addCommand(cmd, { hidden: true }) 而非 parent.command(...),因为 commander 13
39
+ // 的 fluent `.command()` 不支持设置 hidden。
40
+ const cmd = new commander_1.Command('migrate')
41
+ .description('跨 stack 原地迁移:按 MigrateConfig 全套 apply(move + delete + 模板覆盖 + 字段 merge + set-stack)')
42
+ .requiredOption('--to <stack>', '目标 stack 短名')
43
+ .option('--from <stack>', '源 stack;默认读 .spark/meta.json 当前 stack')
44
+ .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
45
+ .addHelpText('after', `
46
+ 已支持的迁移路径
47
+ ${supportedDesc || '(none)'}
48
+
49
+ JSON 输出
50
+ {"data": {"from": "...", "to": "...",
51
+ "appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
52
+ "moved": [...], "deleted": [...], "synced": [...], "merged": [...], "patched": [...],
53
+ "skipped": N,
54
+ "nextActions": [...]}}
55
+
56
+ 示例
57
+ $ miaoda app migrate --to nestjs-react-fullstack
58
+ $ miaoda app migrate --from vite-react --to nestjs-react-fullstack
59
+ $ miaoda app migrate --to nestjs-react-fullstack --dir /path/to/app
60
+ `);
61
+ cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
62
+ await (0, index_1.handleAppMigrate)({
63
+ dir: rawOpts.dir,
64
+ from: rawOpts.from,
65
+ to: rawOpts.to,
66
+ });
67
+ }));
68
+ parent.addCommand(cmd, { hidden: true });
69
+ }
27
70
  function registerAppSync(parent) {
28
71
  const cmd = parent
29
72
  .command('sync')
@@ -123,6 +166,7 @@ function registerAppInit(parent) {
123
166
  .option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
124
167
  .option('--conf <json>', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
125
168
  .option('--skip-install', '跳过依赖安装', false)
169
+ .option('--async-install', '派发后台进程装依赖并立即返回(与 --skip-install 互斥)', false)
126
170
  .addOption((0, shared_1.appIdOption)())
127
171
  .addHelpText('after', `
128
172
  幂等
@@ -151,17 +195,31 @@ function registerAppInit(parent) {
151
195
  为空都会 fallback npm install。
152
196
  JSON 模式下子进程 stdout 重定向到 stderr,避免污染最终 emit 的 JSON。
153
197
 
198
+ 异步安装(--async-install)
199
+ 跳过同步 npm install,派发一个 detached 后台进程装依赖后立即返回。
200
+ .spark/meta.json 在返回前写出(= 脚手架就绪),install 真实结果由 event marker 表达。
201
+ 仅沙箱(SANDBOX_ID 非空)写 marker(presence 定成败,content 给排障):
202
+ 成功 → /tmp/event/WORKSPACE_READY
203
+ 失败 → /tmp/event/WORKSPACE_FAILED
204
+ 安装日志:/tmp/async_install_dep.std.log
205
+ 失败恢复:靠后续 'miaoda app sync'(dev.sh 入口 / 沙箱 pod 启动会跑)兜底,init 不重试。
206
+ 与 --skip-install 互斥。
207
+
154
208
  JSON 输出
155
209
  已初始化:{"data": {"initialized": false, "reason": "already_initialized", "targetDir": "..."}}
156
210
  新初始化:{"data": {"initialized": true, "template": "...", "templateVersion": "...", "steeringVersion": "...",
157
211
  "appId": "...", "platformStackFound": true, "platformSyncedFiles": [...],
158
212
  "installed": true, "installSource": "cache|npm|skipped", "installHash": "...", ...}}
213
+ async 模式:{"data": {"initialized": true, "asyncInstall": true, "installed": false,
214
+ "installSource": "async", "installPid": 123, "installLogPath": "...",
215
+ "eventReadyPath": "...", "eventFailedPath": "..."}}
159
216
 
160
217
  示例
161
218
  $ miaoda app init --template vite-react
162
219
  $ miaoda app init --template nestjs-react-fullstack --app-id app_xxx
163
220
  $ miaoda app init --template vite-react --conf '{"version": "0.1.0"}'
164
221
  $ miaoda app init --template vite-react --skip-install
222
+ $ miaoda app init --template vite-react --async-install
165
223
  $ MIAODA_DEP_CACHE_DIR=/tmp/dep-cache miaoda app init --template vite-react
166
224
  `);
167
225
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
@@ -171,6 +229,7 @@ JSON 输出
171
229
  conf,
172
230
  skipInstall: rawOpts.skipInstall,
173
231
  appId: rawOpts.appId,
232
+ asyncInstall: rawOpts.asyncInstall,
174
233
  });
175
234
  }));
176
235
  }
@@ -16,6 +16,7 @@ function registerDeployCommandsModern(program) {
16
16
  .description('触发 modern 应用发布:本地构建 + 本地部署(preRelease + localPublish)')
17
17
  .option('--dir <path>', '项目目录', '.')
18
18
  .option('--skip-build', '跳过 build 步骤(已构建好时使用)', false)
19
+ .option('--conf <json>', '关联元信息 JSON,仅识别 checkPointVersion / commitID 两字段')
19
20
  .addHelpText('after', `
20
21
  不要用异步模式或后台模式调用 deploy,否则调用可能提前结束,Agent 会误判发布已完成。
21
22
 
@@ -32,6 +33,12 @@ function registerDeployCommandsModern(program) {
32
33
  6. savePluginInstances(扫描 dist/output_capabilities/*.json 批量注册)
33
34
  7. finalizeLocalRelease(Finished|Failed)
34
35
 
36
+ --conf(关联元信息透传)
37
+ 值为 JSON string,只识别两个字段,其余 key 忽略:
38
+ checkPointVersion 关联的 checkpoint 版本
39
+ commitID 关联的代码 commit ID
40
+ 两字段均可选;非法 JSON / 非对象 / 字段值非字符串会报错。
41
+
35
42
  JSON 输出(stdout)
36
43
  {"data": {"appId": "...", "version": <n>, "url": "...", "releaseID": "...", "preReleaseID": "..."}}
37
44
 
@@ -39,12 +46,14 @@ JSON 输出(stdout)
39
46
  $ miaoda deploy
40
47
  $ miaoda deploy --dir ./my-app
41
48
  $ miaoda deploy --skip-build
49
+ $ miaoda deploy --conf '{"checkPointVersion":"v3","commitID":"a1b2c3d"}'
42
50
  `);
43
51
  deployCmd.action((0, shared_1.withHelp)(deployCmd, async (rawOpts) => {
44
52
  await (0, modern_1.handleDeployModern)({
45
53
  dir: rawOpts.dir ?? '.',
46
54
  appId: (0, shared_1.resolveAppId)({}),
47
55
  skipBuild: rawOpts.skipBuild,
56
+ conf: rawOpts.conf,
48
57
  });
49
58
  }));
50
59
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SUPPORTED_STACKS = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
3
+ exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
4
4
  var get_1 = require("./get");
5
5
  Object.defineProperty(exports, "handleAppGet", { enumerable: true, get: function () { return get_1.handleAppGet; } });
6
6
  var update_1 = require("./update");
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "handleAppInit", { enumerable: true, get: functio
10
10
  var sync_1 = require("./sync");
11
11
  Object.defineProperty(exports, "handleAppSync", { enumerable: true, get: function () { return sync_1.handleAppSync; } });
12
12
  Object.defineProperty(exports, "handleAppUpgrade", { enumerable: true, get: function () { return sync_1.handleAppUpgrade; } });
13
+ var migrate_1 = require("./migrate");
14
+ Object.defineProperty(exports, "handleAppMigrate", { enumerable: true, get: function () { return migrate_1.handleAppMigrate; } });
13
15
  // commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
14
16
  var index_1 = require("../../../services/app/init/index");
15
17
  Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return index_1.SUPPORTED_STACKS; } });
@@ -60,6 +60,9 @@ async function handleAppInit(opts) {
60
60
  next_actions: [`可用 stack:${index_1.SUPPORTED_STACKS.join(', ')}`],
61
61
  });
62
62
  }
63
+ if (opts.asyncInstall && opts.skipInstall) {
64
+ throw new error_1.AppError('ARGS_INVALID', '--async-install 与 --skip-install 互斥');
65
+ }
63
66
  const version = opts.conf?.version;
64
67
  const projectName = node_path_1.default.basename(targetDir);
65
68
  const tplResult = (0, index_1.renderTemplate)({ stack, version, targetDir, projectName });
@@ -86,13 +89,17 @@ async function handleAppInit(opts) {
86
89
  (0, logger_1.log)('init', `⚠ skills sync failed (continuing init): ${steeringError}`);
87
90
  steeringResult = { version: 'unknown', syncedSkills: [], techSynced: false };
88
91
  }
89
- // 装模板钉死的依赖。不带 <pkg>@latest 位置参数,因此 npm 不会改写 package.json/lockfile
92
+ // 依赖安装:async 模式留到 meta 落盘后派发后台进程(不等装完),同步模式当场装。
93
+ // 同步:装模板钉死的依赖。不带 <pkg>@latest 位置参数,因此 npm 不会改写 package.json/lockfile
90
94
  // —— "升管控包到 latest" 是 sync 的职责,在 dev.sh 入口跑 `miaoda app sync` 时触发。
91
- const installResult = (0, index_1.installDependencies)({
92
- targetDir,
93
- skip: opts.skipInstall,
94
- quietStdout: (0, output_1.isJsonMode)(),
95
- });
95
+ let installResult;
96
+ if (!opts.asyncInstall) {
97
+ installResult = (0, index_1.installDependencies)({
98
+ targetDir,
99
+ skip: opts.skipInstall,
100
+ quietStdout: (0, output_1.isJsonMode)(),
101
+ });
102
+ }
96
103
  // template 自带 .githooks/pre-commit;如果用户项目已 git init,顺便设上 core.hooksPath。
97
104
  const hookActivation = (0, githooks_1.activateGitHooks)(targetDir);
98
105
  (0, index_1.writeSparkMeta)(targetDir, {
@@ -101,8 +108,18 @@ async function handleAppInit(opts) {
101
108
  archType: tplResult.archType,
102
109
  app_id: opts.appId,
103
110
  });
111
+ // async 模式:meta 已落盘(= 脚手架就绪),再派发后台安装并立即返回(不等装完)。
112
+ let asyncDispatch;
113
+ if (opts.asyncInstall) {
114
+ asyncDispatch = (0, index_1.dispatchAsyncInstall)({ targetDir });
115
+ }
104
116
  if (!(0, output_1.isJsonMode)()) {
105
- process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
117
+ if (opts.asyncInstall && asyncDispatch) {
118
+ process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install dispatched in background pid ${String(asyncDispatch.pid)}) in ${targetDir}\n`);
119
+ }
120
+ else if (installResult) {
121
+ process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
122
+ }
106
123
  }
107
124
  (0, output_1.emit)({
108
125
  data: {
@@ -117,11 +134,23 @@ async function handleAppInit(opts) {
117
134
  techSynced: steeringResult.techSynced,
118
135
  steeringError,
119
136
  gitHooks: hookActivation.action,
120
- installed: installResult.installed,
121
- installSource: installResult.source,
122
- installHash: installResult.hash,
123
- cacheZip: installResult.cacheZip,
124
- installError: installResult.error,
137
+ ...(opts.asyncInstall && asyncDispatch
138
+ ? {
139
+ asyncInstall: true,
140
+ installed: false,
141
+ installSource: 'async',
142
+ installPid: asyncDispatch.pid,
143
+ installLogPath: asyncDispatch.logPath,
144
+ eventReadyPath: asyncDispatch.eventReadyPath,
145
+ eventFailedPath: asyncDispatch.eventFailedPath,
146
+ }
147
+ : {
148
+ installed: installResult?.installed,
149
+ installSource: installResult?.source,
150
+ installHash: installResult?.hash,
151
+ cacheZip: installResult?.cacheZip,
152
+ installError: installResult?.error,
153
+ }),
125
154
  },
126
155
  });
127
156
  }
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
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
+ 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");
15
+ const error_1 = require("../../../utils/error");
16
+ const output_1 = require("../../../utils/output");
17
+ const logger_1 = require("../../../utils/logger");
18
+ /**
19
+ * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
20
+ *
21
+ * 跨 stack 原地迁移:把 user app 从源 stack(默认 .spark/meta.json 当前 stack)转到目标 stack。
22
+ * 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
23
+ * 自行 `git status` / `git diff` 评估并 commit。
24
+ *
25
+ * 跟 sync 的区别:sync 是同 stack 内的版本对齐,migrate 是跨 stack 切换(布局重排、依赖
26
+ * 集合切换、配置文件全套替换)。两条命令的 rule 引擎共享(applyMigrateRules 内部对
27
+ * SyncRule 委派给 applySyncRules)。
28
+ *
29
+ * 执行流:
30
+ * 1. 校验 user app 已 init(.spark/meta.json 存在)+ 当前 stack 匹配 from
31
+ * 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
32
+ * 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
33
+ * merge-json → set-stack
34
+ * 4. 清掉 node_modules(package-lock.json 已经在 rules 里 delete),跑 npm install
35
+ * 物化新依赖集合 —— migrate 改了 dependencies / devDependencies 集合(删 lite +
36
+ * 加 NestJS 全套),不重装就跑不起来。
37
+ * 同一条 npm install 顺手把 config.followLatestPackages 钉 `@latest`,覆盖
38
+ * template caret range 拉不到 pre-release 的盲区(详见 MigrateConfig 注释)。
39
+ *
40
+ * 不做事:
41
+ * - 不自动 commit / stash —— 让用户自己用 git review 改动
42
+ * - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
43
+ */
44
+ async function handleAppMigrate(opts) {
45
+ await Promise.resolve();
46
+ const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
47
+ const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
48
+ if (meta.stack === undefined || meta.stack === '') {
49
+ throw new error_1.AppError('MIGRATE_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
50
+ }
51
+ const from = opts.from ?? meta.stack;
52
+ if (opts.from !== undefined && opts.from !== meta.stack) {
53
+ throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${meta.stack}'`, {
54
+ next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'],
55
+ });
56
+ }
57
+ if (from === opts.to) {
58
+ throw new error_1.AppError('MIGRATE_NOOP', `from and to are both '${from}', nothing to do`);
59
+ }
60
+ const config = (0, index_1.getMigrateConfig)(from, opts.to);
61
+ if (config === null) {
62
+ const supported = (0, index_1.listSupportedMigrations)()
63
+ .map((m) => `${m.from} → ${m.to}`)
64
+ .join(', ');
65
+ throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, {
66
+ next_actions: [`已支持:${supported || '(none)'}`],
67
+ });
68
+ }
69
+ // migrate 用 init 同款的 renderTemplate 拉目标 stack 的 npm template tarball 到 tmp,
70
+ // 再把 tmp 当作 sourceRoot 喂给 applyMigrateRules。这样 migrate config 里 file/directory
71
+ // rule 的 `from` 引用的是完整的 npm template 渲染产物,不需要在 upgrade/templates/ 重复
72
+ // 维护一份 server/ + client/ + 全套配置 —— 复杂度由 init 那条链路 amortize。
73
+ const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-migrate-'));
74
+ const projectName = node_path_1.default.basename(targetDir);
75
+ let templateVersion;
76
+ let templateArchType;
77
+ let results;
78
+ try {
79
+ const tpl = (0, index_2.renderTemplate)({ stack: opts.to, targetDir: tmpRoot, projectName });
80
+ templateVersion = tpl.version;
81
+ templateArchType = tpl.archType;
82
+ // template package.json 的 user 私有字段(name / version / private 等)不参与 merge —— 否
83
+ // 则会被 deepMergeJson 当成 template 端"权威值"覆盖 user 项目原本的字段。这里在 tmp 端
84
+ // 直接 strip 这些字段,让 merge-json rule 只对 dependencies / devDependencies / scripts
85
+ // 等 schema-level 字段生效。
86
+ stripUserOwnedFields(node_path_1.default.join(tmpRoot, 'package.json'));
87
+ (0, logger_1.log)('migrate', `Rendered ${opts.to}@${tpl.version} to tmp, applying ${config.rules.length.toString()} rule(s)`);
88
+ results = (0, migrate_rule_1.applyMigrateRules)(config.rules, {
89
+ sourceRoot: tmpRoot,
90
+ targetDir,
91
+ logPrefix: 'migrate',
92
+ });
93
+ }
94
+ finally {
95
+ node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
96
+ }
97
+ // archType / version 字段同步:set-stack rule 只切 stack,archType 是从 npm template 包
98
+ // 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
99
+ // renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
100
+ (0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
101
+ // 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
102
+ // 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
103
+ // 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
104
+ // (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
105
+ // 软失败:node_modules 不存在不报错。
106
+ const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
107
+ if (node_fs_1.default.existsSync(nodeModulesPath)) {
108
+ (0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
109
+ node_fs_1.default.rmSync(nodeModulesPath, { recursive: true, force: true });
110
+ }
111
+ // 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
112
+ // --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
113
+ // --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
114
+ // 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
115
+ const followLatest = config.followLatestPackages ?? [];
116
+ const installArgs = [
117
+ 'install',
118
+ '--no-audit',
119
+ '--no-fund',
120
+ '--ignore-scripts',
121
+ '--registry',
122
+ (0, index_2.resolveNpmInstallRegistry)(),
123
+ ...followLatest.map((pkg) => `${pkg}@latest`),
124
+ ];
125
+ (0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
126
+ let installError;
127
+ try {
128
+ (0, node_child_process_1.execFileSync)('npm', installArgs, {
129
+ cwd: targetDir,
130
+ stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
131
+ });
132
+ }
133
+ catch (err) {
134
+ installError = err instanceof Error ? err.message : String(err);
135
+ (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
136
+ }
137
+ // 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
138
+ // 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
139
+ // 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
140
+ // 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
141
+ // - 只在 SANDBOX_ID 非空时做(本地环境用户进程混杂,主动 pkill 风险大)
142
+ // - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
143
+ // 留着旧进程让用户先处理 installError
144
+ // - 软失败:pkill 无匹配进程退出 1,catch 吞掉
145
+ let devRestarted = false;
146
+ if (installError === undefined &&
147
+ process.env.SANDBOX_ID !== undefined &&
148
+ process.env.SANDBOX_ID !== '') {
149
+ (0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
150
+ try {
151
+ (0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
152
+ devRestarted = true;
153
+ }
154
+ catch {
155
+ // pkill 无匹配进程时退出 1,正常情况(可能 dev 没在跑)
156
+ }
157
+ }
158
+ (0, output_1.emit)({
159
+ data: {
160
+ from,
161
+ to: opts.to,
162
+ templateVersion,
163
+ appliedRules: results.map((r) => ({
164
+ type: r.rule.type,
165
+ action: r.action,
166
+ path: r.path,
167
+ detail: r.detail,
168
+ })),
169
+ ...summarizeResults(results),
170
+ followLatestPackages: followLatest,
171
+ installError,
172
+ devRestarted,
173
+ nextActions: process.env.SANDBOX_ID !== undefined && process.env.SANDBOX_ID !== ''
174
+ ? [
175
+ 'git status / git diff 评估改动并 commit',
176
+ 'miaoda skills sync 同步到新 stack 的 agent skills',
177
+ ]
178
+ : [
179
+ 'git status / git diff 评估改动并 commit',
180
+ '重启 dev 进程让新 scripts/dev.js + 新依赖生效:重跑 npm run dev(killOrphansByPort 会自动踢掉旧进程)',
181
+ 'miaoda skills sync 同步到新 stack 的 agent skills',
182
+ ],
183
+ },
184
+ });
185
+ }
186
+ /**
187
+ * 从 tmp 端 template package.json 中删除 user 项目通常拥有的字段,避免 merge-json 时这些
188
+ * 字段被 template 值覆盖。需要 strip 的字段以 npm 模板里"通常专属于具体应用"的为准。
189
+ */
190
+ function stripUserOwnedFields(pkgPath) {
191
+ if (!node_fs_1.default.existsSync(pkgPath))
192
+ return;
193
+ const json = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
194
+ const userOwned = [
195
+ 'name',
196
+ 'version',
197
+ 'private',
198
+ 'description',
199
+ 'author',
200
+ 'keywords',
201
+ 'license',
202
+ 'repository',
203
+ 'homepage',
204
+ 'bugs',
205
+ ];
206
+ let changed = false;
207
+ for (const key of userOwned) {
208
+ if (Reflect.deleteProperty(json, key))
209
+ changed = true;
210
+ }
211
+ if (changed) {
212
+ node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + '\n');
213
+ }
214
+ }
215
+ function summarizeResults(results) {
216
+ const moved = [];
217
+ const deleted = [];
218
+ const synced = [];
219
+ const merged = [];
220
+ const patched = [];
221
+ let skipped = 0;
222
+ for (const r of results) {
223
+ if (r.path === undefined)
224
+ continue;
225
+ switch (r.action) {
226
+ case 'moved':
227
+ moved.push(r.path);
228
+ break;
229
+ case 'deleted':
230
+ deleted.push(r.path);
231
+ break;
232
+ case 'synced':
233
+ case 'created':
234
+ synced.push(r.path);
235
+ break;
236
+ case 'merged':
237
+ merged.push(r.path);
238
+ break;
239
+ case 'patched':
240
+ patched.push(r.path);
241
+ break;
242
+ case 'skipped':
243
+ skipped++;
244
+ break;
245
+ case 'appended':
246
+ case 'noop':
247
+ break;
248
+ }
249
+ }
250
+ return { moved, deleted, synced, merged, patched, skipped };
251
+ }
@@ -3,10 +3,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseDeployConf = parseDeployConf;
6
7
  exports.handleDeployModern = handleDeployModern;
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const output_1 = require("../../../utils/output");
10
+ const error_1 = require("../../../utils/error");
9
11
  const index_1 = require("../../../services/deploy/modern/index");
12
+ const CONF_EXPECTED = '期望形如 {"checkPointVersion":"...","commitID":"..."} 的 JSON 对象';
13
+ /**
14
+ * 解析 `--conf` JSON string,只取 checkPointVersion / commitID 两个白名单字段。
15
+ * - 未传 → 返回 {}(行为不变)。
16
+ * - 非法 JSON / 非 object / 字段值非 string → 抛 DEPLOY_CONF_INVALID。
17
+ * - 空串字段视为未提供(不进 body,保持可选语义);未知 key 忽略。
18
+ */
19
+ function parseDeployConf(raw) {
20
+ if (raw === undefined || raw === '')
21
+ return {};
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(raw);
25
+ }
26
+ catch {
27
+ throw new error_1.AppError('DEPLOY_CONF_INVALID', `--conf 不是合法 JSON,${CONF_EXPECTED}`);
28
+ }
29
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
30
+ throw new error_1.AppError('DEPLOY_CONF_INVALID', `--conf 必须是 JSON 对象,${CONF_EXPECTED}`);
31
+ }
32
+ const source = parsed;
33
+ const conf = {};
34
+ for (const key of ['checkPointVersion', 'commitID']) {
35
+ const value = source[key];
36
+ if (value === undefined)
37
+ continue;
38
+ if (typeof value !== 'string') {
39
+ throw new error_1.AppError('DEPLOY_CONF_INVALID', `--conf.${key} 必须是字符串,${CONF_EXPECTED}`);
40
+ }
41
+ if (value !== '')
42
+ conf[key] = value;
43
+ }
44
+ return conf;
45
+ }
10
46
  /**
11
47
  * miaoda deploy(modern scene 专用,CLI 表面对齐 openclaw-cli)
12
48
  *
@@ -14,10 +50,13 @@ const index_1 = require("../../../services/deploy/modern/index");
14
50
  */
15
51
  async function handleDeployModern(opts) {
16
52
  const projectDir = node_path_1.default.resolve(opts.dir);
53
+ const conf = parseDeployConf(opts.conf);
17
54
  const result = await (0, index_1.runModernDeploy)({
18
55
  projectDir,
19
56
  appId: opts.appId,
20
57
  skipBuild: opts.skipBuild ?? false,
58
+ checkPointVersion: conf.checkPointVersion,
59
+ commitID: conf.commitID,
21
60
  });
22
61
  (0, output_1.emit)({
23
62
  data: {
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ /**
3
+ * 跨 stack 迁移注册表 —— 按 (from, to) 二元组查找对应的 MigrateConfig。
4
+ *
5
+ * 跟 sync 注册表 (src/config/sync-configs/index.ts) 的关系:sync 一份配置对应一个 stack,
6
+ * migrate 一份配置对应 (源 stack, 目标 stack) 二元组;模板资产 migrate 在 runtime 从 npm
7
+ * 拉目标 stack 的 template tarball(init 用同一套 renderTemplate),不再用 upgrade/templates/
8
+ * 的子集。
9
+ *
10
+ * 新增迁移路径:
11
+ * 1. src/config/migrate-configs/<from>-to-<to>.ts 里 export default MIGRATE_CONFIG
12
+ * 2. 在 MIGRATE_REGISTRY 加一条
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.getMigrateConfig = getMigrateConfig;
19
+ exports.listSupportedMigrations = listSupportedMigrations;
20
+ const vite_react_to_nestjs_react_fullstack_1 = __importDefault(require("./vite-react-to-nestjs-react-fullstack"));
21
+ /** key 格式:`<from>:<to>` */
22
+ const MIGRATE_REGISTRY = {
23
+ 'vite-react:nestjs-react-fullstack': vite_react_to_nestjs_react_fullstack_1.default,
24
+ };
25
+ /** 返回 (from, to) 对应的 MigrateConfig;未注册时返回 null。 */
26
+ function getMigrateConfig(from, to) {
27
+ return MIGRATE_REGISTRY[`${from}:${to}`] ?? null;
28
+ }
29
+ /** 列出已支持的迁移路径,给 handler / 命令 help 用 */
30
+ function listSupportedMigrations() {
31
+ return Object.keys(MIGRATE_REGISTRY).map((key) => {
32
+ const [from, to] = key.split(':');
33
+ return { from, to };
34
+ });
35
+ }