@lark-apaas/miaoda-cli 0.1.31 → 0.1.33-alpha.59a1bf2

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 CHANGED
@@ -55,7 +55,7 @@ miaoda file ls --output json
55
55
 
56
56
  `miaoda registry` 提供 shadcn 式的「组件 registry」:把 SDK 文件**拷进**当前项目(copy-in),而非作为依赖安装。registry 是独立 npm 包,按运行时分发(design-html / html → buildless runtime → `@lark-apaas/coding-registry-buildless`)。
57
57
 
58
- 前置:当前目录(或 `--dir`)已走过 `miaoda app init`,且 `.spark/meta.json` 的 `stack` 有对应 registry 包(当前支持 `design-html` / `html`);其余 stack 执行时报 `REGISTRY_STACK_UNSUPPORTED` 并提示支持列表。
58
+ 前置:当前工作目录已走过 `miaoda app init`,且 `.spark/meta.json` 的 `stack` 有对应 registry 包(当前支持 `design-html` / `html`);其余 stack 执行时报 `REGISTRY_STACK_UNSUPPORTED` 并提示支持列表。meta 一律从 cwd 读取,不随 `--dir` 走。
59
59
 
60
60
  ```bash
61
61
  # 列出可用 SDK 条目(输出各文件 USAGE 注释块原文,便于 Agent 直接读用法)
@@ -76,7 +76,7 @@ miaoda registry add deck-slide --overwrite # 目标已存在时覆盖(
76
76
  - **拍平落地**:所有文件**拍平**到项目根(`scripts/deck-slide.js` → 项目 `deck-slide.js`,不保留子目录);闭包内同名 basename 撞车时后者覆盖前者。
77
77
  - **skip-by-disk**:项目已有同名(拍平后的 basename)文件默认跳过,`--overwrite` 才覆盖;不写任何安装记账文件,项目文件树即状态。
78
78
  - **add 回执带 usage**:`add` 结果的 `entries` 只列**你请求的条目**(不含被 `Depends` 传递拉进来的依赖——依赖照常落地在 `added`/`skipped`,但那是"管道"),逐条给出 `usage`(USAGE 块原文)与 `dependsOn`,Agent `add` 完即可直接读用法,无需再 `registry list`。请求的条目即使已存在被跳过也回带 usage(`status` 标 `added`/`skipped`,据此判断磁盘上是 registry 版还是你改过的旧版)。`--json` 结构为 `{registryVersion, added, skipped, dryRun, entries}`;pretty(默认)输出 `registry@<ver>` + `added:`/`skipped:` 逗号列表 + 各请求条目的 USAGE 块,稳定 label、无 JSON 数组、无截断,对 Agent 与人同样可读。
79
- - `--version <ver>` 指定 registry 包版本或 dist-tag(缺省 `latest`),`--dir <path>` 指定项目目录(缺省当前目录)。
79
+ - `--version <ver>` 指定 registry 包版本或 dist-tag(缺省 `latest`);`add` 的 `--dir <path>` 指定 copy-in 落地目录(缺省当前目录),不影响 `.spark/meta.json` 读取。
80
80
 
81
81
  ## 全局参数
82
82
 
@@ -33,8 +33,9 @@ function registerRegistryCommands(program) {
33
33
  - add 落地:拍平到项目根(包根 scripts/x.js → 项目 x.js);同名 basename 撞车时后者覆盖前者
34
34
 
35
35
  前置
36
- 当前目录(或 --dir)已走过 'miaoda app init',且 .spark/meta.json 的 stack 有对应
36
+ 当前工作目录已走过 'miaoda app init',且 .spark/meta.json 的 stack 有对应
37
37
  registry 包(当前支持 design-html / html);其余 stack 执行时报 REGISTRY_STACK_UNSUPPORTED
38
+ meta 一律从 cwd 读取,add 的 --dir 只影响 copy-in 落地目录
38
39
  `);
39
40
  registerRegistryList(registryCmd);
40
41
  registerRegistryAdd(registryCmd);
@@ -43,7 +44,6 @@ function registerRegistryList(parent) {
43
44
  const cmd = parent
44
45
  .command('list')
45
46
  .description('列出 registry 可用 SDK 条目,输出各文件的 USAGE 块原文')
46
- .option('--dir <path>', '项目目录,默认当前目录', '.')
47
47
  .option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
48
48
  .addHelpText('after', `
49
49
  输出
@@ -59,7 +59,6 @@ JSON 输出
59
59
  `);
60
60
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
61
61
  await (0, index_1.handleRegistryList)({
62
- dir: rawOpts.dir,
63
62
  version: rawOpts.version,
64
63
  });
65
64
  }));
@@ -69,7 +68,7 @@ function registerRegistryAdd(parent) {
69
68
  .command('add')
70
69
  .description('把一个或多个 SDK 文件 copy-in 到当前项目(拍平到项目根,含 Depends 闭包)')
71
70
  .argument('<token...>', 'name / 完整文件名 / relPath,可传多个;同名多匹配全拉,Depends 自动传递解析')
72
- .option('--dir <path>', '项目目录,默认当前目录', '.')
71
+ .option('--dir <path>', 'copy-in 落地目录,默认当前目录(stack 一律读 cwd 的 .spark/meta.json)', '.')
73
72
  .option('--version <ver>', 'registry 包版本或 dist-tag,缺省 latest')
74
73
  .option('--overwrite', '目标文件已存在时覆盖(默认跳过以保护用户改动)', false)
75
74
  .option('--dry-run', '只报告将写 / 将跳哪些文件,不落盘', false)
@@ -77,8 +76,9 @@ function registerRegistryAdd(parent) {
77
76
  行为
78
77
  - token 寻址:name(foo)/ 完整文件名(foo.css)/ relPath(stylesheets/foo.css)任一形式
79
78
  - 多匹配全拉:token 命中多个同名条目时全部 copy-in;想只要其一就写完整文件名 / relPath
80
- - 解析 USAGE 块 Depends 传递闭包,把 SDK 文件**拍平**拷到项目根
81
- (包根 scripts/deck-stage.js → 项目 deck-stage.js;同名 basename 撞车时后者覆盖前者)
79
+ - 解析 USAGE 块 Depends 传递闭包,把 SDK 文件**拍平**拷到落地目录(--dir,默认当前目录)
80
+ (包根 scripts/deck-stage.js → 落地 deck-stage.js;同名 basename 撞车时后者覆盖前者)
81
+ - stack 一律读当前工作目录的 .spark/meta.json,--dir 不影响 meta 读取
82
82
  - skip-by-disk:目标文件已存在默认跳过;--overwrite 才覆盖
83
83
  - 不写任何安装记账文件(项目文件树即状态)
84
84
 
@@ -15,6 +15,7 @@ const spark_meta_1 = require("../../../utils/spark-meta");
15
15
  const error_1 = require("../../../utils/error");
16
16
  const output_1 = require("../../../utils/output");
17
17
  const env_1 = require("../../../utils/env");
18
+ const preview_control_1 = require("../../../utils/preview-control");
18
19
  const logger_1 = require("../../../utils/logger");
19
20
  /**
20
21
  * miaoda app migrate --to <stack> [--from <stack>] [--dir <path>]
@@ -99,78 +100,130 @@ async function handleAppMigrate(opts) {
99
100
  // 的 miaodaTemplate.archType 拿到的动态值,rule 配置时不知道,由 handler 在拿到
100
101
  // renderTemplate 结果后单独写。version 也一并写回方便 sync 后续诊断。
101
102
  (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 });
103
+ // 沙箱环境下先停掉 dev server(预览)—— 保活由 code-server 管,`pkill dev.js` 会被立刻
104
+ // 重新拉起,拿不到"停住"的状态。dev server 活着时 vite 持续往 node_modules/.vite/deps
105
+ // 文件,下面 rm node_modules 走到顶层 rmdir 时目录又非空,直接 ENOTEMPTY 失败(历史故障)。
106
+ // - 只在沙箱内做(本地用户自己管 dev 进程,不该被 CLI 动)
107
+ // - 软失败:接口不存在(老版本 code-server 返 404)/ 连不上都不阻断,降级回老路径
108
+ // (带重试的 rm + 结尾 pkill 让保活拉起)
109
+ // - ⚠️ stop 成功后必须恢复:下面整段包在 try/finally 里,任何异常路径都会走 startPreview
110
+ let previewStopped = false;
111
+ let previewStopError;
112
+ if ((0, env_1.isSandboxEnv)()) {
113
+ (0, logger_1.log)('migrate', '停止 dev server(预览)以便安全重装依赖...');
114
+ const stopped = await (0, preview_control_1.stopPreview)({ logPrefix: 'migrate' });
115
+ previewStopped = stopped.ok;
116
+ if (!stopped.ok) {
117
+ previewStopError = stopped.error;
118
+ (0, logger_1.log)('migrate', `⚠ 停止预览失败 (continuing, 降级为不停 dev 直接重装): ${stopped.error ?? ''}`);
119
+ }
111
120
  }
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
121
  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
122
  let installError;
123
+ let nodeModulesCleanError;
124
+ let previewRestarted = false;
125
+ let previewRestartError;
128
126
  try {
129
- (0, node_child_process_1.execFileSync)('npm', installArgs, {
130
- cwd: targetDir,
131
- stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
132
- });
133
- }
134
- catch (err) {
135
- installError = err instanceof Error ? err.message : String(err);
136
- (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
137
- }
138
- // 重启 dev process 前清 vite cache —— migrate 改了 package.json + lockfile,
139
- // 老 vite 进程的 .vite/deps cache lockfileHash 跟新 lockfile 不一致。
140
- // 不清的话 supervisor 拉起的新 vite cold start 会立即 "Re-optimizing because
141
- // lockfile has changed", re-optimize 期间 ModuleGraph transform cache 跟
142
- // deps middleware metadata 短暂不同步, 浏览器请求老 hash 触发
143
- // ERR_OUTDATED_OPTIMIZED_DEP → 504 死锁(详见 fullstack-plugin#1143 的 504
144
- // ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
145
- // optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
146
- //
147
- // - 只在 SANDBOX_ID 非空时做(本地一般不会撞这条链路)
148
- // - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
149
- // - 软失败:.vite 不存在 / 删失败都不阻断
150
- if (installError === undefined &&
151
- process.env.SANDBOX_ID !== undefined &&
152
- process.env.SANDBOX_ID !== '') {
153
- const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
154
- if (node_fs_1.default.existsSync(viteCacheDir)) {
155
- (0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
127
+ // 清掉 user app 的 node_modules —— migrate 改了 dependencies 集合(删 lite + 加 NestJS
128
+ // 全套)+ rule 已经把 package-lock.json 删了。如果 node_modules 留着,npm install 时旧
129
+ // 包还在 node_modules 顶层 hoist 树里,可能跟新 package.json spec 撞 peer 冲突
130
+ // (比如旧 coding-preset-vite-react@1.0.8 peer vite@^8 跟新 vite@^7 撞)。
131
+ // - maxRetries:force 只吞 ENOENT,不管 ENOTEMPTY/EBUSY;这类错误 node 只在
132
+ // maxRetries>0 时才重试。停预览失败降级时靠它兜住残余写入。
133
+ // - 软失败:删不干净也继续跑 npm install npm 自己 reconcile —— 半迁移态
134
+ // (package-lock 已删 + deps 没装)比"清理不彻底"糟得多。
135
+ const nodeModulesPath = node_path_1.default.join(targetDir, 'node_modules');
136
+ if (node_fs_1.default.existsSync(nodeModulesPath)) {
137
+ (0, logger_1.log)('migrate', '清理 node_modules(dependencies 集合已变)...');
156
138
  try {
157
- node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
139
+ node_fs_1.default.rmSync(nodeModulesPath, {
140
+ recursive: true,
141
+ force: true,
142
+ maxRetries: 5,
143
+ retryDelay: 300,
144
+ });
158
145
  }
159
146
  catch (err) {
160
- (0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
147
+ nodeModulesCleanError = err instanceof Error ? err.message : String(err);
148
+ (0, logger_1.log)('migrate', `⚠ 清理 node_modules 失败 (continuing): ${nodeModulesCleanError}`);
149
+ }
150
+ }
151
+ // 跑 npm install 物化新依赖集合 + 把 followLatestPackages 钉 latest
152
+ // --ignore-scripts 绕开 action-plugin postinstall 在缺平台 env 时的 ENOENT
153
+ // --registry 钉同一份 npmmirror(跟 init / sync 行为一致)
154
+ // 软失败:install 挂了不阻断 emit;用户拿到详细 error,自行 npm install 兜底
155
+ const installArgs = [
156
+ 'install',
157
+ '--no-audit',
158
+ '--no-fund',
159
+ '--ignore-scripts',
160
+ '--registry',
161
+ (0, index_2.resolveNpmInstallRegistry)(),
162
+ ...followLatest.map((pkg) => `${pkg}@latest`),
163
+ ];
164
+ (0, logger_1.log)('migrate', `Running npm ${installArgs.join(' ')}...`);
165
+ try {
166
+ (0, node_child_process_1.execFileSync)('npm', installArgs, {
167
+ cwd: targetDir,
168
+ stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
169
+ });
170
+ }
171
+ catch (err) {
172
+ installError = err instanceof Error ? err.message : String(err);
173
+ (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
174
+ }
175
+ // 重启 dev process 前清 vite cache —— migrate 改了 package.json + lockfile,
176
+ // 老 vite 进程的 .vite/deps cache 的 lockfileHash 跟新 lockfile 不一致。
177
+ // 不清的话拉起的新 vite cold start 会立即 "Re-optimizing because
178
+ // lockfile has changed", re-optimize 期间 ModuleGraph transform cache 跟
179
+ // deps middleware metadata 短暂不同步, 浏览器请求老 hash 触发
180
+ // ERR_OUTDATED_OPTIMIZED_DEP → 504 死锁(详见 fullstack-plugin#1143 的 504
181
+ // ignoreOutdatedRequests fix 注释)。直接 rm -rf 让新 vite 从干净状态 cold
182
+ // optimize, 一次写到位 lockfile 一致的 metadata, 不再触发中途 re-optimize。
183
+ //
184
+ // - 只在 SANDBOX_ID 非空时做(本地一般不会撞这条链路)
185
+ // - 只在 install 成功时做(install 挂了 node_modules 状态不对, 清 cache 也救不回来)
186
+ // - 软失败:.vite 不存在 / 删失败都不阻断
187
+ if (installError === undefined &&
188
+ process.env.SANDBOX_ID !== undefined &&
189
+ process.env.SANDBOX_ID !== '') {
190
+ const viteCacheDir = node_path_1.default.join(targetDir, 'node_modules', '.vite');
191
+ if (node_fs_1.default.existsSync(viteCacheDir)) {
192
+ (0, logger_1.log)('migrate', '清理 node_modules/.vite (vite cache 跟新 lockfile 错位会 504)...');
193
+ try {
194
+ node_fs_1.default.rmSync(viteCacheDir, { recursive: true, force: true });
195
+ }
196
+ catch (err) {
197
+ (0, logger_1.log)('migrate', `⚠ 清 .vite 失败 (continuing): ${err instanceof Error ? err.message : String(err)}`);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ finally {
203
+ // 恢复 dev server —— 只要上面 stop 成功过就必须调,且必须在 finally 里:
204
+ // rule 应用 / install / 清 cache 任何一步抛异常都不能把保活留在关闭状态,否则用户沙箱
205
+ // 的预览再也起不来(比迁移失败严重得多)。
206
+ // 无条件恢复(不看 installError):dev 进程已经被停了,留着不起来只会更糟;新 scripts/dev.js
207
+ // + 新依赖也正是靠这次 start 生效。
208
+ if (previewStopped) {
209
+ (0, logger_1.log)('migrate', '恢复 dev server(预览)...');
210
+ const started = await (0, preview_control_1.startPreview)({ logPrefix: 'migrate' });
211
+ previewRestarted = started.ok;
212
+ if (!started.ok) {
213
+ previewRestartError = started.error;
214
+ (0, logger_1.log)('migrate', `✗ 恢复预览失败, 需人工处理: ${started.error ?? ''}`);
161
215
  }
162
216
  }
163
217
  }
164
- // 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
165
- // 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
166
- // 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
167
- // 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
218
+ // 降级路径:preview stop 没成功(老版本 code-server / 接口连不上),dev 进程还是老的,
219
+ // 按历史行为 pkill dev.js 主进程,靠平台 supervisor 自动重新 exec dev.sh → dev.js,
220
+ // 让新 deps + scripts 生效。
168
221
  // - 只在沙箱内做(isSandboxEnv;本地环境用户进程混杂,主动 pkill 风险大)
169
222
  // - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
170
223
  // 留着旧进程让用户先处理 installError
171
224
  // - 软失败:pkill 无匹配进程退出 1,catch 吞掉
172
- let devRestarted = false;
173
- if (installError === undefined && (0, env_1.isSandboxEnv)()) {
225
+ let devRestarted = previewRestarted;
226
+ if (!previewStopped && installError === undefined && (0, env_1.isSandboxEnv)()) {
174
227
  (0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
175
228
  try {
176
229
  (0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
@@ -194,11 +247,22 @@ async function handleAppMigrate(opts) {
194
247
  ...summarizeResults(results),
195
248
  followLatestPackages: followLatest,
196
249
  installError,
250
+ nodeModulesCleanError,
197
251
  devRestarted,
252
+ previewStopped,
253
+ previewStopError,
254
+ previewRestarted,
255
+ previewRestartError,
198
256
  nextActions: (0, env_1.isSandboxEnv)()
199
257
  ? [
200
258
  'git status / git diff 评估改动并 commit',
201
259
  'miaoda skills sync 同步到新 stack 的 agent skills',
260
+ // 预览恢复失败时置顶一条人工兜底动作 —— 保活已被关掉且不会自愈
261
+ ...(previewRestartError === undefined
262
+ ? []
263
+ : [
264
+ `⚠ dev server(预览)未恢复成功,需手动 curl -X POST ${(0, preview_control_1.resolveCodeServerBaseUrl)()}/api/v1/preview/start 恢复保活`,
265
+ ]),
202
266
  ]
203
267
  : [
204
268
  'git status / git diff 评估改动并 commit',
@@ -13,7 +13,8 @@ const output_1 = require("../../../utils/output");
13
13
  * miaoda registry add <name...> [--overwrite] [--dry-run] [--dir <path>] [--version <ver>]
14
14
  *
15
15
  * 解析 USAGE 块 Depends 传递闭包,把每个条目对应的 SDK 文件从 registry 包根的 <relPath>
16
- * **保留相对路径**拷到项目根(--dir 指定目录的根)。
16
+ * **保留相对路径**拷到落地目录(--dir,默认 cwd)。stack 一律从 <cwd>/.spark/meta.json 读,
17
+ * 不随 --dir 走。
17
18
  * skip-by-disk:目标已存在默认跳过,--overwrite 才覆盖。不写任何安装记账文件。
18
19
  */
19
20
  async function handleRegistryAdd(opts) {
@@ -22,7 +23,7 @@ async function handleRegistryAdd(opts) {
22
23
  throw new error_1.AppError('ARGS_INVALID', 'registry add 至少需要一个条目名');
23
24
  }
24
25
  const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
25
- const stack = (0, shared_1.resolveRegistryStack)(targetDir);
26
+ const stack = (0, shared_1.resolveRegistryStack)();
26
27
  const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
27
28
  try {
28
29
  const plan = (0, index_1.planAdd)({
@@ -1,17 +1,13 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.handleRegistryList = handleRegistryList;
7
- const node_path_1 = __importDefault(require("node:path"));
8
4
  const index_1 = require("../../../services/registry/index");
9
5
  const shared_1 = require("./shared");
10
6
  const output_1 = require("../../../utils/output");
11
7
  /**
12
- * miaoda registry list [--dir <path>] [--version <ver>]
8
+ * miaoda registry list [--version <ver>]
13
9
  *
14
- * 从 .spark/meta.json 读 stack → runtime → 包名,拉对应 registry 包,从包根递归扫含 USAGE 块的 SDK 文件。
10
+ * 从 <cwd>/.spark/meta.json 读 stack → runtime → 包名,拉对应 registry 包,从包根递归扫含 USAGE 块的 SDK 文件。
15
11
  *
16
12
  * - 默认(pretty):输出各条目 USAGE 块原文,带文件名分隔,给 Agent 直接读注释。
17
13
  * - --json:输出 [{name, file, usage(原文字符串), dependsOn[]}],usage 保持原文不解析成字段。
@@ -19,8 +15,7 @@ const output_1 = require("../../../utils/output");
19
15
  */
20
16
  async function handleRegistryList(opts) {
21
17
  await Promise.resolve();
22
- const targetDir = node_path_1.default.resolve(opts.dir ?? process.cwd());
23
- const stack = (0, shared_1.resolveRegistryStack)(targetDir);
18
+ const stack = (0, shared_1.resolveRegistryStack)();
24
19
  const reg = (0, index_1.loadRegistry)({ stack, version: opts.version });
25
20
  try {
26
21
  if ((0, output_1.isJsonMode)()) {
@@ -5,16 +5,20 @@ const spark_meta_1 = require("../../../utils/spark-meta");
5
5
  const index_1 = require("../../../services/registry/index");
6
6
  const error_1 = require("../../../utils/error");
7
7
  /**
8
- * 从 <targetDir>/.spark/meta.json 读 stack,校验该 stack 有组件 registry。
8
+ * 从 <cwd>/.spark/meta.json 读 stack,校验该 stack 有组件 registry。
9
+ * meta 只与当前工作目录相关(CLI 必须在 user app 根目录下运行),不随 --dir 走——
10
+ * --dir 只影响 add 的 copy-in 落地目录。
9
11
  * 缺 stack → 提示先 init;stack 无 registry(无对应 runtime / runtime 无 registry 包)→ 提示支持哪些 stack。
10
12
  *
11
13
  * 这里只做「stack 是否在支持列表」的早校验给用户友好提示;包名的两跳解析
12
14
  * (stack → runtime → 包)由 loadRegistry/resolveRegistryPackage 负责。
13
15
  */
14
- function resolveRegistryStack(targetDir) {
15
- const meta = (0, spark_meta_1.readSparkMeta)(targetDir);
16
+ function resolveRegistryStack() {
17
+ const meta = (0, spark_meta_1.readSparkMeta)(process.cwd());
16
18
  if (meta.stack === undefined || meta.stack === '') {
17
- throw new error_1.AppError('REGISTRY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first');
19
+ throw new error_1.AppError('REGISTRY_META_INCOMPLETE', '.spark/meta.json missing stack — run `miaoda app init` first', {
20
+ next_actions: ['meta 一律读当前工作目录(不随 --dir),请 cd 到 user app 根目录后重试'],
21
+ });
18
22
  }
19
23
  if (!index_1.REGISTRY_SUPPORTED_STACKS.includes(meta.stack)) {
20
24
  throw new error_1.AppError('REGISTRY_STACK_UNSUPPORTED', `stack "${meta.stack}" 没有组件 registry`, {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pollUntilDone = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.isSandboxEnv = exports.getInnerApi = exports.postInnerApi = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.fmt = exports.isJsonMode = exports.emitPaged = exports.emitOk = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
3
+ exports.pollUntilDone = exports.msToSec = exports.msToNs = exports.parseToSec = exports.parseToNs = exports.parseToMs = exports.parseTimeToMs = exports.resolveCodeServerBaseUrl = exports.startPreview = exports.stopPreview = exports.isSandboxEnv = exports.getInnerApi = exports.postInnerApi = exports.setRuntimeHttpClient = exports.setHttpClient = exports.resetHttpClient = exports.getRuntimeHttpClient = exports.getHttpClient = exports.log = exports.debug = exports.fmt = exports.isJsonMode = exports.emitPaged = exports.emitOk = exports.emitError = exports.emit = exports.getLogId = exports.generateLogId = exports.initConfigFromOpts = exports.resetConfig = exports.setConfig = exports.getConfig = exports.HttpError = exports.AppError = void 0;
4
4
  var error_1 = require("./error");
5
5
  Object.defineProperty(exports, "AppError", { enumerable: true, get: function () { return error_1.AppError; } });
6
6
  Object.defineProperty(exports, "HttpError", { enumerable: true, get: function () { return error_1.HttpError; } });
@@ -32,6 +32,10 @@ Object.defineProperty(exports, "postInnerApi", { enumerable: true, get: function
32
32
  Object.defineProperty(exports, "getInnerApi", { enumerable: true, get: function () { return http_1.getInnerApi; } });
33
33
  var env_1 = require("./env");
34
34
  Object.defineProperty(exports, "isSandboxEnv", { enumerable: true, get: function () { return env_1.isSandboxEnv; } });
35
+ var preview_control_1 = require("./preview-control");
36
+ Object.defineProperty(exports, "stopPreview", { enumerable: true, get: function () { return preview_control_1.stopPreview; } });
37
+ Object.defineProperty(exports, "startPreview", { enumerable: true, get: function () { return preview_control_1.startPreview; } });
38
+ Object.defineProperty(exports, "resolveCodeServerBaseUrl", { enumerable: true, get: function () { return preview_control_1.resolveCodeServerBaseUrl; } });
35
39
  var time_1 = require("./time");
36
40
  Object.defineProperty(exports, "parseTimeToMs", { enumerable: true, get: function () { return time_1.parseTimeToMs; } });
37
41
  Object.defineProperty(exports, "parseToMs", { enumerable: true, get: function () { return time_1.parseToMs; } });
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCodeServerBaseUrl = resolveCodeServerBaseUrl;
4
+ exports.stopPreview = stopPreview;
5
+ exports.startPreview = startPreview;
6
+ const logger_1 = require("../utils/logger");
7
+ /**
8
+ * 沙箱内 dev server(预览)的显式启停控制。
9
+ *
10
+ * 沙箱里 dev server 的保活由 code-server 负责:直接 `pkill dev.js` 只会被保活立刻重新拉起,
11
+ * 拿不到"停住"这个状态。code-server 为此暴露了两个本地接口:
12
+ *
13
+ * POST <base>/api/v1/preview/stop 停止预览(连带停掉保活)
14
+ * POST <base>/api/v1/preview/start 启动预览(恢复保活)
15
+ *
16
+ * base 默认 `http://localhost:8080/code-server` —— migrate / sync 这类脚本本身就在沙箱内执行,
17
+ * 直连 localhost 即可,无需鉴权;`MIAODA_CODE_SERVER_BASE_URL` 可覆盖(本地联调 / 端口变更)。
18
+ *
19
+ * 典型用途:migrate 要重装依赖(`rm -rf node_modules` + `npm install`)。dev server 活着时
20
+ * vite 会持续往 `node_modules/.vite/deps` 写文件,`fs.rmSync` 自底向上删到顶层 `rmdir` 时目录
21
+ * 又变非空,直接 `ENOTEMPTY` 失败。先 stop 再删才是干净的做法。
22
+ *
23
+ * ⚠️ 调用契约:**stop 成功后必须保证 start 被调到**(调用方放 `finally`)。保活被关掉后不会
24
+ * 自愈,漏掉 start 会让用户沙箱的预览再也起不来 —— 比迁移失败严重得多。
25
+ *
26
+ * 全部 API 都是软失败语义:不抛异常,失败信息放在返回值的 `error` 上,由调用方决定降级路径
27
+ * (老版本 code-server 没有这两个接口,会返回 404)。
28
+ */
29
+ const DEFAULT_CODE_SERVER_BASE_URL = 'http://localhost:8080/code-server';
30
+ const DEFAULT_TIMEOUT_MS = 10_000;
31
+ /** start 是恢复动作,值得多试几次;stop 失败可以直接降级,不重试 */
32
+ const START_ATTEMPTS = 3;
33
+ const START_RETRY_DELAY_MS = 500;
34
+ /** code-server 本地基址,去掉尾部斜杠。`MIAODA_CODE_SERVER_BASE_URL` > 默认 localhost:8080。 */
35
+ function resolveCodeServerBaseUrl() {
36
+ const fromEnv = process.env.MIAODA_CODE_SERVER_BASE_URL;
37
+ const base = fromEnv !== undefined && fromEnv !== '' ? fromEnv : DEFAULT_CODE_SERVER_BASE_URL;
38
+ return base.replace(/\/+$/, '');
39
+ }
40
+ /** 停止预览(含保活)。软失败:不抛异常。 */
41
+ async function stopPreview(opts = {}) {
42
+ return postPreview('stop', 1, opts);
43
+ }
44
+ /** 启动预览(恢复保活)。软失败:不抛异常;内部带重试,因为这是恢复动作。 */
45
+ async function startPreview(opts = {}) {
46
+ return postPreview('start', START_ATTEMPTS, opts);
47
+ }
48
+ async function postPreview(action, attempts, opts) {
49
+ const url = `${resolveCodeServerBaseUrl()}/api/v1/preview/${action}`;
50
+ const logPrefix = opts.logPrefix ?? 'preview';
51
+ let last = { ok: false, url, error: 'no attempt made' };
52
+ for (let i = 1; i <= attempts; i++) {
53
+ last = await postOnce(url, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
54
+ if (last.ok)
55
+ return last;
56
+ if (i < attempts) {
57
+ (0, logger_1.log)(logPrefix, `⚠ preview ${action} 失败(第 ${String(i)}/${String(attempts)} 次), 重试: ${last.error ?? ''}`);
58
+ await sleep(START_RETRY_DELAY_MS);
59
+ }
60
+ }
61
+ return last;
62
+ }
63
+ async function postOnce(url, timeoutMs) {
64
+ try {
65
+ // 带空 JSON body —— 这两个接口不需要参数,但显式给 content-type 可以避免服务端框架对
66
+ // 无 body POST 的处理差异。
67
+ const res = await fetch(url, {
68
+ method: 'POST',
69
+ headers: { 'content-type': 'application/json' },
70
+ body: '{}',
71
+ signal: AbortSignal.timeout(timeoutMs),
72
+ });
73
+ if (!res.ok) {
74
+ const detail = await readBodySafe(res);
75
+ return {
76
+ ok: false,
77
+ url,
78
+ status: res.status,
79
+ error: `POST ${url} → ${String(res.status)} ${res.statusText}${detail}`,
80
+ };
81
+ }
82
+ return { ok: true, url, status: res.status };
83
+ }
84
+ catch (err) {
85
+ return {
86
+ ok: false,
87
+ url,
88
+ error: `POST ${url} failed: ${err instanceof Error ? err.message : String(err)}`,
89
+ };
90
+ }
91
+ }
92
+ async function readBodySafe(res) {
93
+ try {
94
+ const text = await res.text();
95
+ if (!text)
96
+ return '';
97
+ return ` ${text.length > 300 ? `${text.slice(0, 300)}...` : text}`;
98
+ }
99
+ catch {
100
+ return '';
101
+ }
102
+ }
103
+ function sleep(ms) {
104
+ return new Promise((resolve) => {
105
+ setTimeout(resolve, ms);
106
+ });
107
+ }
@@ -128,7 +128,16 @@ function syncSandboxSkills(opts) {
128
128
  (0, logger_1.debug)(`sandbox-skills: no npm version matched -alpha.${lane}, fallback to @latest ` +
129
129
  `(TCC 里的 X 可能是老 lane dist-tag 命名 / 已 unpublish / npm view 挂 / 网络断)`);
130
130
  }
131
- const primaryVersion = laneVersion ?? 'latest';
131
+ // 版本优先级:lane(TCC 显式定向)> BOE 默认 @dev > @latest
132
+ //
133
+ // `dev` 通道由 miaoda-coding 的 publish-sandbox-skills-dev.sh 在 develop 分支自动发布
134
+ // (`<base>-dev.<sha>` + dist-tag `dev`),口径与 coding-steering 完全一致 ——
135
+ // 后者在本仓 coding-steering 侧也是 `FORCE_FRAMEWORK_ENVIRONMENT=boe` 时默认取 `dev`。
136
+ //
137
+ // lane 必须压过 dev:TCC 里配了 tgz-key 就说明是定向灰度(钉某个 feat 分支的快照),
138
+ // 不能被"BOE 就跟 develop"这条默认规则覆盖掉。
139
+ const isBoe = process.env.FORCE_FRAMEWORK_ENVIRONMENT === 'boe';
140
+ const primaryVersion = laneVersion ?? (isBoe ? 'dev' : 'latest');
132
141
  try {
133
142
  (0, logger_1.debug)(`sandbox-skills: fetching ${packageName}@${primaryVersion}`);
134
143
  fetched = (0, npm_pack_1.fetchNpmPackage)({
@@ -138,6 +147,9 @@ function syncSandboxSkills(opts) {
138
147
  });
139
148
  }
140
149
  catch (err) {
150
+ // @latest 是最后一档,它再挂就没得降了,直接抛。
151
+ // lane / dev 拉失败都降到 @latest:dev 通道可能因为 develop 还没发过包而不存在
152
+ // (dist-tag 缺失 → npm pack 404),这时候不该让整条 sync 挂掉。
141
153
  if (primaryVersion === 'latest') {
142
154
  throw err;
143
155
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.33-alpha.59a1bf2",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {