@lark-apaas/miaoda-cli 0.1.16-alpha.c0b0ae2 → 0.1.16-alpha.cfec413

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.
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerAppCommands = registerAppCommands;
4
4
  const shared_1 = require("../../../cli/commands/shared");
5
5
  const index_1 = require("../../../cli/handlers/app/index");
6
- const index_2 = require("../../../config/migrate-configs/index");
7
6
  const error_1 = require("../../../utils/error");
8
7
  function registerAppCommands(program, opts = {}) {
9
8
  const description = opts.includeInit
@@ -23,56 +22,8 @@ function registerAppCommands(program, opts = {}) {
23
22
  if (opts.includeInit) {
24
23
  registerAppInit(appCmd);
25
24
  registerAppSync(appCmd);
26
- registerAppMigrate(appCmd);
27
25
  }
28
26
  }
29
- function registerAppMigrate(parent) {
30
- const supportedDesc = (0, index_2.listSupportedMigrations)()
31
- .map((m) => `${m.from} → ${m.to}`)
32
- .join(', ');
33
- const cmd = parent
34
- .command('migrate')
35
- .description('跨 stack 原地迁移:按 MigrateConfig 全套 apply(move + delete + 模板覆盖 + 字段 merge + set-stack)')
36
- .requiredOption('--to <stack>', '目标 stack 短名')
37
- .option('--from <stack>', '源 stack;默认读 .spark/meta.json 当前 stack')
38
- .option('--dir <path>', '项目目录,默认 cwd(需含 .spark/meta.json)')
39
- .addHelpText('after', `
40
- 已支持的迁移路径
41
- ${supportedDesc || '(none)'}
42
-
43
- 执行流
44
- 1. 校验 .spark/meta.json 存在且当前 stack 匹配 --from(或自动读 meta)
45
- 2. 按 (from, to) 在 src/config/migrate-configs/ 取 MigrateConfig
46
- 3. 顺序执行 rules:
47
- move-directory / move-file → 用户领地搬到新布局(src → client/src 等)
48
- delete-file / delete-directory → 清理老 stack 的残留文件
49
- delete-json-keys → 从 user package.json 删 vite-react 专属字段
50
- file / directory → 拷新 stack 的模板资产覆盖
51
- merge-json → package.json 等字段级深合并
52
- add-script / patch-script / add-line / remove-line → 局部修补
53
- set-stack → 收尾切 .spark/meta.json 的 stack 字段
54
- 4. 不做 npm install —— 让用户自行 review 改动后再装依赖(package-lock.json 已删)
55
-
56
- JSON 输出
57
- {"data": {"from": "...", "to": "...",
58
- "appliedRules": [{"type": "...", "action": "...", "path": "...", "detail": "..."}],
59
- "moved": [...], "deleted": [...], "synced": [...], "merged": [...], "patched": [...],
60
- "skipped": N,
61
- "nextActions": [...]}}
62
-
63
- 示例
64
- $ miaoda app migrate --to nestjs-react-fullstack
65
- $ miaoda app migrate --from vite-react --to nestjs-react-fullstack
66
- $ miaoda app migrate --to nestjs-react-fullstack --dir /path/to/app
67
- `);
68
- cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
69
- await (0, index_1.handleAppMigrate)({
70
- dir: rawOpts.dir,
71
- from: rawOpts.from,
72
- to: rawOpts.to,
73
- });
74
- }));
75
- }
76
27
  function registerAppSync(parent) {
77
28
  const cmd = parent
78
29
  .command('sync')
@@ -172,6 +123,7 @@ function registerAppInit(parent) {
172
123
  .option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
173
124
  .option('--conf <json>', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
174
125
  .option('--skip-install', '跳过依赖安装', false)
126
+ .option('--async-install', '派发后台进程装依赖并立即返回(与 --skip-install 互斥)', false)
175
127
  .addOption((0, shared_1.appIdOption)())
176
128
  .addHelpText('after', `
177
129
  幂等
@@ -200,17 +152,31 @@ function registerAppInit(parent) {
200
152
  为空都会 fallback npm install。
201
153
  JSON 模式下子进程 stdout 重定向到 stderr,避免污染最终 emit 的 JSON。
202
154
 
155
+ 异步安装(--async-install)
156
+ 跳过同步 npm install,派发一个 detached 后台进程装依赖后立即返回。
157
+ .spark/meta.json 在返回前写出(= 脚手架就绪),install 真实结果由 event marker 表达。
158
+ 仅沙箱(SANDBOX_ID 非空)写 marker(presence 定成败,content 给排障):
159
+ 成功 → /tmp/event/MIAODA_DEPS_READY
160
+ 失败 → /tmp/event/MIAODA_DEPS_FAILED
161
+ 安装日志:/tmp/async_install_dep.std.log
162
+ 失败恢复:靠后续 'miaoda app sync'(dev.sh 入口 / 沙箱 pod 启动会跑)兜底,init 不重试。
163
+ 与 --skip-install 互斥。
164
+
203
165
  JSON 输出
204
166
  已初始化:{"data": {"initialized": false, "reason": "already_initialized", "targetDir": "..."}}
205
167
  新初始化:{"data": {"initialized": true, "template": "...", "templateVersion": "...", "steeringVersion": "...",
206
168
  "appId": "...", "platformStackFound": true, "platformSyncedFiles": [...],
207
169
  "installed": true, "installSource": "cache|npm|skipped", "installHash": "...", ...}}
170
+ async 模式:{"data": {"initialized": true, "asyncInstall": true, "installed": false,
171
+ "installSource": "async", "installPid": 123, "installLogPath": "...",
172
+ "eventReadyPath": "...", "eventFailedPath": "..."}}
208
173
 
209
174
  示例
210
175
  $ miaoda app init --template vite-react
211
176
  $ miaoda app init --template nestjs-react-fullstack --app-id app_xxx
212
177
  $ miaoda app init --template vite-react --conf '{"version": "0.1.0"}'
213
178
  $ miaoda app init --template vite-react --skip-install
179
+ $ miaoda app init --template vite-react --async-install
214
180
  $ MIAODA_DEP_CACHE_DIR=/tmp/dep-cache miaoda app init --template vite-react
215
181
  `);
216
182
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
@@ -220,6 +186,7 @@ JSON 输出
220
186
  conf,
221
187
  skipInstall: rawOpts.skipInstall,
222
188
  appId: rawOpts.appId,
189
+ asyncInstall: rawOpts.asyncInstall,
223
190
  });
224
191
  }));
225
192
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SUPPORTED_STACKS = exports.handleAppMigrate = exports.handleAppUpgrade = exports.handleAppSync = exports.handleAppInit = exports.handleAppUpdate = exports.handleAppGet = void 0;
3
+ exports.SUPPORTED_STACKS = 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,8 +10,6 @@ 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; } });
15
13
  // commands 层渲染 help 时需要这份枚举;从 handler barrel 转发,避免 commands → services 越界
16
14
  var index_1 = require("../../../services/app/init/index");
17
15
  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,116 @@
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.ASYNC_INSTALL_LOG = void 0;
7
+ exports.isSandboxEnv = isSandboxEnv;
8
+ exports.runAsyncInstallWorker = runAsyncInstallWorker;
9
+ exports.dispatchAsyncInstall = dispatchAsyncInstall;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const install_1 = require("./install");
14
+ /** event marker 目录,写死;写前 mkdir -p。沙箱里 code server 轮询此目录。 */
15
+ const EVENT_DIR = '/tmp/event';
16
+ /** 安装成功 marker 名 */
17
+ const MARKER_READY = 'MIAODA_DEPS_READY';
18
+ /** 安装失败 marker 名 */
19
+ const MARKER_FAILED = 'MIAODA_DEPS_FAILED';
20
+ /** 后台 worker stdout/stderr 落盘路径 */
21
+ exports.ASYNC_INSTALL_LOG = '/tmp/async_install_dep.std.log';
22
+ /** 沙箱判定:SANDBOX_ID 非空。与 init handler 的 outputLayout 分流口径一致。 */
23
+ function isSandboxEnv() {
24
+ return process.env.SANDBOX_ID !== undefined && process.env.SANDBOX_ID !== '';
25
+ }
26
+ function nowIso() {
27
+ return new Date().toISOString();
28
+ }
29
+ function clearStaleMarkers(eventDir) {
30
+ for (const name of [MARKER_READY, MARKER_FAILED]) {
31
+ const p = node_path_1.default.join(eventDir, name);
32
+ if (node_fs_1.default.existsSync(p))
33
+ node_fs_1.default.rmSync(p, { force: true });
34
+ }
35
+ }
36
+ function writeMarker(eventDir, name, content) {
37
+ node_fs_1.default.mkdirSync(eventDir, { recursive: true });
38
+ node_fs_1.default.writeFileSync(node_path_1.default.join(eventDir, name), JSON.stringify(content) + '\n', 'utf-8');
39
+ }
40
+ /**
41
+ * 后台 install worker 体:跑 installDependencies,按结果写 marker(仅沙箱)。
42
+ * 由 dispatchAsyncInstall 通过 `node -e` 在 detached 子进程里调用(见下)。
43
+ * presence 定成败:成功写 MIAODA_DEPS_READY,失败写 MIAODA_DEPS_FAILED;content 给排障。
44
+ */
45
+ function runAsyncInstallWorker(opts) {
46
+ const eventDir = opts.eventDir ?? EVENT_DIR;
47
+ const sandbox = isSandboxEnv();
48
+ if (sandbox)
49
+ clearStaleMarkers(eventDir);
50
+ const start = Date.now();
51
+ let result;
52
+ try {
53
+ result = (0, install_1.installDependencies)({ targetDir: opts.targetDir, quietStdout: false });
54
+ }
55
+ catch (err) {
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ if (sandbox) {
58
+ writeMarker(eventDir, MARKER_FAILED, {
59
+ ts: nowIso(),
60
+ error: msg,
61
+ logPath: exports.ASYNC_INSTALL_LOG,
62
+ });
63
+ }
64
+ return;
65
+ }
66
+ if (!sandbox)
67
+ return;
68
+ if (result.source === 'failed') {
69
+ writeMarker(eventDir, MARKER_FAILED, {
70
+ ts: nowIso(),
71
+ error: result.error ?? 'npm install failed',
72
+ logPath: exports.ASYNC_INSTALL_LOG,
73
+ });
74
+ }
75
+ else {
76
+ writeMarker(eventDir, MARKER_READY, {
77
+ ts: nowIso(),
78
+ durationMs: Date.now() - start,
79
+ source: result.source,
80
+ });
81
+ }
82
+ }
83
+ /**
84
+ * 派发 detached 后台进程跑依赖安装并立即返回。
85
+ * 用 `node -e` 直接 require 本模块(编译产物)调 runAsyncInstallWorker,
86
+ * 不挂任何 CLI 子命令、对外不暴露;worker 内复用 installDependencies(cache / registry / fallback)。
87
+ * 沙箱下派发前清旧 marker;stdout/stderr 重定向到 logPath。
88
+ */
89
+ function dispatchAsyncInstall(opts) {
90
+ const eventDir = opts.eventDir ?? EVENT_DIR;
91
+ const logPath = opts.logPath ?? exports.ASYNC_INSTALL_LOG;
92
+ const sandbox = isSandboxEnv();
93
+ if (sandbox)
94
+ clearStaleMarkers(eventDir);
95
+ // targetDir 走 argv 传,不拼进代码串(免转义);只把本模块路径 JSON 内联进去。
96
+ const workerCode = `require(${JSON.stringify(__filename)}).runAsyncInstallWorker({ targetDir: process.argv[1] })`;
97
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(logPath), { recursive: true });
98
+ const fd = node_fs_1.default.openSync(logPath, 'w');
99
+ try {
100
+ const child = (0, node_child_process_1.spawn)(process.execPath, ['-e', workerCode, opts.targetDir], {
101
+ detached: true,
102
+ stdio: ['ignore', fd, fd],
103
+ });
104
+ child.unref();
105
+ return {
106
+ pid: child.pid,
107
+ logPath,
108
+ eventReadyPath: sandbox ? node_path_1.default.join(eventDir, MARKER_READY) : undefined,
109
+ eventFailedPath: sandbox ? node_path_1.default.join(eventDir, MARKER_FAILED) : undefined,
110
+ };
111
+ }
112
+ finally {
113
+ // 父进程关掉自己那份 fd;子进程已通过 stdio dup 持有独立副本。
114
+ node_fs_1.default.closeSync(fd);
115
+ }
116
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.TEMPLATE_PACKAGE_BY_STACK = exports.SUPPORTED_STACKS = exports.renderTemplate = void 0;
3
+ exports.ASYNC_INSTALL_LOG = exports.isSandboxEnv = exports.runAsyncInstallWorker = exports.dispatchAsyncInstall = exports.installDependencies = exports.writeSparkMeta = exports.readSparkMeta = exports.TEMPLATE_PACKAGE_BY_STACK = exports.SUPPORTED_STACKS = exports.renderTemplate = void 0;
4
4
  var template_1 = require("./template");
5
5
  Object.defineProperty(exports, "renderTemplate", { enumerable: true, get: function () { return template_1.renderTemplate; } });
6
6
  Object.defineProperty(exports, "SUPPORTED_STACKS", { enumerable: true, get: function () { return template_1.SUPPORTED_STACKS; } });
@@ -10,3 +10,8 @@ Object.defineProperty(exports, "readSparkMeta", { enumerable: true, get: functio
10
10
  Object.defineProperty(exports, "writeSparkMeta", { enumerable: true, get: function () { return spark_meta_1.writeSparkMeta; } });
11
11
  var install_1 = require("./install");
12
12
  Object.defineProperty(exports, "installDependencies", { enumerable: true, get: function () { return install_1.installDependencies; } });
13
+ var async_install_1 = require("./async-install");
14
+ Object.defineProperty(exports, "dispatchAsyncInstall", { enumerable: true, get: function () { return async_install_1.dispatchAsyncInstall; } });
15
+ Object.defineProperty(exports, "runAsyncInstallWorker", { enumerable: true, get: function () { return async_install_1.runAsyncInstallWorker; } });
16
+ Object.defineProperty(exports, "isSandboxEnv", { enumerable: true, get: function () { return async_install_1.isSandboxEnv; } });
17
+ Object.defineProperty(exports, "ASYNC_INSTALL_LOG", { enumerable: true, get: function () { return async_install_1.ASYNC_INSTALL_LOG; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.16-alpha.c0b0ae2",
3
+ "version": "0.1.16-alpha.cfec413",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,182 +0,0 @@
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_fs_1 = __importDefault(require("node:fs"));
8
- const node_os_1 = __importDefault(require("node:os"));
9
- const node_path_1 = __importDefault(require("node:path"));
10
- const index_1 = require("../../../config/migrate-configs/index");
11
- const index_2 = require("../../../services/app/init/index");
12
- const migrate_rule_1 = require("../../../utils/migrate-rule");
13
- const spark_meta_1 = require("../../../utils/spark-meta");
14
- const error_1 = require("../../../utils/error");
15
- const output_1 = require("../../../utils/output");
16
- const logger_1 = require("../../../utils/logger");
17
- /**
18
- * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
19
- *
20
- * 跨 stack 原地迁移:把 user app 从源 stack(默认 .spark/meta.json 当前 stack)转到目标 stack。
21
- * 同一个 git 仓库内进行,不动 git history,所有改动作为 working tree 变更体现,用户跑完后
22
- * 自行 `git status` / `git diff` 评估并 commit。
23
- *
24
- * 跟 sync 的区别:sync 是同 stack 内的版本对齐,migrate 是跨 stack 切换(布局重排、依赖
25
- * 集合切换、配置文件全套替换)。两条命令的 rule 引擎共享(applyMigrateRules 内部对
26
- * SyncRule 委派给 applySyncRules)。
27
- *
28
- * 执行流:
29
- * 1. 校验 user app 已 init(.spark/meta.json 存在)+ 当前 stack 匹配 from
30
- * 2. 拿到 (from, to) 对应的 MigrateConfig 与模板源根目录
31
- * 3. 顺序执行 rules:move-* → delete-* → delete-json-keys → file/directory →
32
- * merge-json → set-stack
33
- * 4. emit 结果;不做 npm install(用户接下来跑 `npm install` 或 `miaoda app sync` 时自然装)
34
- *
35
- * 不做事:
36
- * - 不自动 commit / stash —— 让用户自己用 git review 改动
37
- * - 不跑 npm install —— 留给后续 sync 或用户手动
38
- * - 不动 .agent/skills/(那是 miaoda skills sync 的事,迁移后用户应该单独跑一次切到新 stack 的 skills)
39
- */
40
- async function handleAppMigrate(opts) {
41
- await Promise.resolve();
42
- const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
43
- const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
44
- if (meta.stack === undefined || meta.stack === '') {
45
- throw new error_1.AppError('MIGRATE_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
46
- }
47
- const from = opts.from ?? meta.stack;
48
- if (opts.from !== undefined && opts.from !== meta.stack) {
49
- throw new error_1.AppError('MIGRATE_FROM_MISMATCH', `--from '${opts.from}' but current stack is '${meta.stack}'`, {
50
- next_actions: ['省略 --from 让 cli 自动读 meta.json,或确认 user app 状态'],
51
- });
52
- }
53
- if (from === opts.to) {
54
- throw new error_1.AppError('MIGRATE_NOOP', `from and to are both '${from}', nothing to do`);
55
- }
56
- const config = (0, index_1.getMigrateConfig)(from, opts.to);
57
- if (config === null) {
58
- const supported = (0, index_1.listSupportedMigrations)()
59
- .map((m) => `${m.from} → ${m.to}`)
60
- .join(', ');
61
- throw new error_1.AppError('MIGRATE_PATH_NOT_SUPPORTED', `no migrate config for '${from}' → '${opts.to}'`, {
62
- next_actions: [`已支持:${supported || '(none)'}`],
63
- });
64
- }
65
- // migrate 用 init 同款的 renderTemplate 拉目标 stack 的 npm template tarball 到 tmp,
66
- // 再把 tmp 当作 sourceRoot 喂给 applyMigrateRules。这样 migrate config 里 file/directory
67
- // rule 的 `from` 引用的是完整的 npm template 渲染产物,不需要在 upgrade/templates/ 重复
68
- // 维护一份 server/ + client/ + 全套配置 —— 复杂度由 init 那条链路 amortize。
69
- const tmpRoot = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'miaoda-migrate-'));
70
- const projectName = node_path_1.default.basename(targetDir);
71
- let templateVersion;
72
- let templateArchType;
73
- let results;
74
- try {
75
- const tpl = (0, index_2.renderTemplate)({ stack: opts.to, targetDir: tmpRoot, projectName });
76
- templateVersion = tpl.version;
77
- templateArchType = tpl.archType;
78
- // template package.json 的 user 私有字段(name / version / private 等)不参与 merge —— 否
79
- // 则会被 deepMergeJson 当成 template 端"权威值"覆盖 user 项目原本的字段。这里在 tmp 端
80
- // 直接 strip 这些字段,让 merge-json rule 只对 dependencies / devDependencies / scripts
81
- // 等 schema-level 字段生效。
82
- stripUserOwnedFields(node_path_1.default.join(tmpRoot, 'package.json'));
83
- (0, logger_1.log)('migrate', `Rendered ${opts.to}@${tpl.version} to tmp, applying ${config.rules.length.toString()} rule(s)`);
84
- results = (0, migrate_rule_1.applyMigrateRules)(config.rules, {
85
- sourceRoot: tmpRoot,
86
- targetDir,
87
- logPrefix: 'migrate',
88
- });
89
- }
90
- finally {
91
- node_fs_1.default.rmSync(tmpRoot, { recursive: true, force: true });
92
- }
93
- // archType / version 字段同步:set-stack rule 只切 stack,archType 是从 npm template 包
94
- // 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
95
- // renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
96
- (0, spark_meta_1.writeSparkMeta)(targetDir, { archType: templateArchType, version: templateVersion });
97
- (0, output_1.emit)({
98
- data: {
99
- from,
100
- to: opts.to,
101
- templateVersion,
102
- appliedRules: results.map((r) => ({
103
- type: r.rule.type,
104
- action: r.action,
105
- path: r.path,
106
- detail: r.detail,
107
- })),
108
- ...summarizeResults(results),
109
- nextActions: [
110
- 'git status / git diff 评估改动并 commit',
111
- 'npm install 装新依赖(package-lock.json 已被删除,将重新生成)',
112
- 'miaoda skills sync 同步到新 stack 的 agent skills',
113
- ],
114
- },
115
- });
116
- }
117
- /**
118
- * 从 tmp 端 template package.json 中删除 user 项目通常拥有的字段,避免 merge-json 时这些
119
- * 字段被 template 值覆盖。需要 strip 的字段以 npm 模板里"通常专属于具体应用"的为准。
120
- */
121
- function stripUserOwnedFields(pkgPath) {
122
- if (!node_fs_1.default.existsSync(pkgPath))
123
- return;
124
- const json = JSON.parse(node_fs_1.default.readFileSync(pkgPath, 'utf-8'));
125
- const userOwned = [
126
- 'name',
127
- 'version',
128
- 'private',
129
- 'description',
130
- 'author',
131
- 'keywords',
132
- 'license',
133
- 'repository',
134
- 'homepage',
135
- 'bugs',
136
- ];
137
- let changed = false;
138
- for (const key of userOwned) {
139
- if (Reflect.deleteProperty(json, key))
140
- changed = true;
141
- }
142
- if (changed) {
143
- node_fs_1.default.writeFileSync(pkgPath, JSON.stringify(json, null, 2) + '\n');
144
- }
145
- }
146
- function summarizeResults(results) {
147
- const moved = [];
148
- const deleted = [];
149
- const synced = [];
150
- const merged = [];
151
- const patched = [];
152
- let skipped = 0;
153
- for (const r of results) {
154
- if (r.path === undefined)
155
- continue;
156
- switch (r.action) {
157
- case 'moved':
158
- moved.push(r.path);
159
- break;
160
- case 'deleted':
161
- deleted.push(r.path);
162
- break;
163
- case 'synced':
164
- case 'created':
165
- synced.push(r.path);
166
- break;
167
- case 'merged':
168
- merged.push(r.path);
169
- break;
170
- case 'patched':
171
- patched.push(r.path);
172
- break;
173
- case 'skipped':
174
- skipped++;
175
- break;
176
- case 'appended':
177
- case 'noop':
178
- break;
179
- }
180
- }
181
- return { moved, deleted, synced, merged, patched, skipped };
182
- }
@@ -1,35 +0,0 @@
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
- }