@lark-apaas/miaoda-cli 0.1.17 → 0.1.18-alpha.b094028

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.
@@ -166,6 +166,7 @@ function registerAppInit(parent) {
166
166
  .option('--template <stack>', `技术栈短名(${index_1.SUPPORTED_STACKS.join(' / ')})`)
167
167
  .option('--conf <json>', 'init 配置 JSON。支持 {"version": "<template 版本>"},默认 latest')
168
168
  .option('--skip-install', '跳过依赖安装', false)
169
+ .option('--async-install', '派发后台进程装依赖并立即返回(与 --skip-install 互斥)', false)
169
170
  .addOption((0, shared_1.appIdOption)())
170
171
  .addHelpText('after', `
171
172
  幂等
@@ -194,17 +195,31 @@ function registerAppInit(parent) {
194
195
  为空都会 fallback npm install。
195
196
  JSON 模式下子进程 stdout 重定向到 stderr,避免污染最终 emit 的 JSON。
196
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
+
197
208
  JSON 输出
198
209
  已初始化:{"data": {"initialized": false, "reason": "already_initialized", "targetDir": "..."}}
199
210
  新初始化:{"data": {"initialized": true, "template": "...", "templateVersion": "...", "steeringVersion": "...",
200
211
  "appId": "...", "platformStackFound": true, "platformSyncedFiles": [...],
201
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": "..."}}
202
216
 
203
217
  示例
204
218
  $ miaoda app init --template vite-react
205
219
  $ miaoda app init --template nestjs-react-fullstack --app-id app_xxx
206
220
  $ miaoda app init --template vite-react --conf '{"version": "0.1.0"}'
207
221
  $ miaoda app init --template vite-react --skip-install
222
+ $ miaoda app init --template vite-react --async-install
208
223
  $ MIAODA_DEP_CACHE_DIR=/tmp/dep-cache miaoda app init --template vite-react
209
224
  `);
210
225
  cmd.action((0, shared_1.withHelp)(cmd, async (rawOpts) => {
@@ -214,6 +229,7 @@ JSON 输出
214
229
  conf,
215
230
  skipInstall: rawOpts.skipInstall,
216
231
  appId: rawOpts.appId,
232
+ asyncInstall: rawOpts.asyncInstall,
217
233
  });
218
234
  }));
219
235
  }
@@ -10,6 +10,7 @@ const index_1 = require("../../../services/app/init/index");
10
10
  const coding_steering_1 = require("../../../utils/coding-steering");
11
11
  const logger_1 = require("../../../utils/logger");
12
12
  const githooks_1 = require("../../../utils/githooks");
13
+ const logs_dir_1 = require("../../../utils/logs-dir");
13
14
  const error_1 = require("../../../utils/error");
14
15
  const output_1 = require("../../../utils/output");
15
16
  /**
@@ -60,9 +61,15 @@ async function handleAppInit(opts) {
60
61
  next_actions: [`可用 stack:${index_1.SUPPORTED_STACKS.join(', ')}`],
61
62
  });
62
63
  }
64
+ if (opts.asyncInstall && opts.skipInstall) {
65
+ throw new error_1.AppError('ARGS_INVALID', '--async-install 与 --skip-install 互斥');
66
+ }
63
67
  const version = opts.conf?.version;
64
68
  const projectName = node_path_1.default.basename(targetDir);
65
69
  const tplResult = (0, index_1.renderTemplate)({ stack, version, targetDir, projectName });
70
+ // 先建 logs/,防止 user 跑 dev 之前 AI/工具 redirect 到 logs/*.log 因为父目录不存在直接挂
71
+ // (dev.js / dev-local.js 自己也建 logs/,但只在它启动后;启动前的 shell redirect 会先于此)
72
+ (0, logs_dir_1.ensureLogsDir)(targetDir);
66
73
  // skills 同步软失败:拉不到 coding-steering 包不该阻断 writeSparkMeta /
67
74
  // activateGitHooks(之前会让 .spark/meta.json 没写,下次 init 半渲染状态又得重跑全套)。
68
75
  // 按运行环境(SANDBOX_ID)分流 outputLayout,不绑 stack:
@@ -86,13 +93,17 @@ async function handleAppInit(opts) {
86
93
  (0, logger_1.log)('init', `⚠ skills sync failed (continuing init): ${steeringError}`);
87
94
  steeringResult = { version: 'unknown', syncedSkills: [], techSynced: false };
88
95
  }
89
- // 装模板钉死的依赖。不带 <pkg>@latest 位置参数,因此 npm 不会改写 package.json/lockfile
96
+ // 依赖安装:async 模式留到 meta 落盘后派发后台进程(不等装完),同步模式当场装。
97
+ // 同步:装模板钉死的依赖。不带 <pkg>@latest 位置参数,因此 npm 不会改写 package.json/lockfile
90
98
  // —— "升管控包到 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
- });
99
+ let installResult;
100
+ if (!opts.asyncInstall) {
101
+ installResult = (0, index_1.installDependencies)({
102
+ targetDir,
103
+ skip: opts.skipInstall,
104
+ quietStdout: (0, output_1.isJsonMode)(),
105
+ });
106
+ }
96
107
  // template 自带 .githooks/pre-commit;如果用户项目已 git init,顺便设上 core.hooksPath。
97
108
  const hookActivation = (0, githooks_1.activateGitHooks)(targetDir);
98
109
  (0, index_1.writeSparkMeta)(targetDir, {
@@ -101,8 +112,18 @@ async function handleAppInit(opts) {
101
112
  archType: tplResult.archType,
102
113
  app_id: opts.appId,
103
114
  });
115
+ // async 模式:meta 已落盘(= 脚手架就绪),再派发后台安装并立即返回(不等装完)。
116
+ let asyncDispatch;
117
+ if (opts.asyncInstall) {
118
+ asyncDispatch = (0, index_1.dispatchAsyncInstall)({ targetDir });
119
+ }
104
120
  if (!(0, output_1.isJsonMode)()) {
105
- process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
121
+ if (opts.asyncInstall && asyncDispatch) {
122
+ process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install dispatched in background pid ${String(asyncDispatch.pid)}) in ${targetDir}\n`);
123
+ }
124
+ else if (installResult) {
125
+ process.stdout.write(`✓ Initialized ${stack} (template ${tplResult.version}, steering ${steeringResult.version}, install ${installResult.source}) in ${targetDir}\n`);
126
+ }
106
127
  }
107
128
  (0, output_1.emit)({
108
129
  data: {
@@ -117,11 +138,23 @@ async function handleAppInit(opts) {
117
138
  techSynced: steeringResult.techSynced,
118
139
  steeringError,
119
140
  gitHooks: hookActivation.action,
120
- installed: installResult.installed,
121
- installSource: installResult.source,
122
- installHash: installResult.hash,
123
- cacheZip: installResult.cacheZip,
124
- installError: installResult.error,
141
+ ...(opts.asyncInstall && asyncDispatch
142
+ ? {
143
+ asyncInstall: true,
144
+ installed: false,
145
+ installSource: 'async',
146
+ installPid: asyncDispatch.pid,
147
+ installLogPath: asyncDispatch.logPath,
148
+ eventReadyPath: asyncDispatch.eventReadyPath,
149
+ eventFailedPath: asyncDispatch.eventFailedPath,
150
+ }
151
+ : {
152
+ installed: installResult?.installed,
153
+ installSource: installResult?.source,
154
+ installHash: installResult?.hash,
155
+ cacheZip: installResult?.cacheZip,
156
+ installError: installResult?.error,
157
+ }),
125
158
  },
126
159
  });
127
160
  }
@@ -134,14 +134,54 @@ async function handleAppMigrate(opts) {
134
134
  installError = err instanceof Error ? err.message : String(err);
135
135
  (0, logger_1.log)('migrate', `⚠ npm install failed (continuing): ${installError}`);
136
136
  }
137
+ // 预热 vite deps cache —— 绕开 dev server 内嵌 optimizer 在沙箱里的 silent hang。
138
+ //
139
+ // 背景:migrate 跑完 npm install 后,fullstack user app 的 lockfile 是新的,但
140
+ // node_modules/.vite/deps/_metadata.json 还是旧/不存在。下一次 dev:client 启动时
141
+ // vite 看到 lockfile hash 跟 metadata 不一致,触发内嵌 optimizer 重跑 prebundle。
142
+ // 偶发(看起来跟沙箱 CPU / esbuild worker 状态有关) optimizer 静默 hang —— bundle 不
143
+ // 完成、不写产物、不报错,但 vite middleware 仍 serve 请求:transform 写一个 hash、
144
+ // load 又拿到另一个 hash, 持续 504。详见
145
+ // ~/docs/miaoda-fullstack-vite-deps-504-20260624.md
146
+ //
147
+ // 解法:这里 standalone `npx vite optimize --force` 跑一次,只用 vite optimizer 子流程
148
+ // (不加载 preset 那一堆 plugin 的 dev server lifecycle hook),沙箱里实测 1~2 秒就能
149
+ // 完成,写出 _metadata.json + 当下 lockfileHash。下一次 dev:client 启动时 vite 看
150
+ // hash 跟 cache 一致就跳过 Re-optimize, 直接复用 cache,绕开 hang 路径。
151
+ //
152
+ // - 只在 SANDBOX_ID 非空时做(本地环境 vite 自带 optimizer 不卡, 没必要拖慢 migrate)
153
+ // - 只在 install 成功时做(install 失败时 node_modules 状态不对, optimize 也会挂)
154
+ // - 软失败:optimize 挂了不阻断后续 pkill —— 即使没预热成功, dev server 自己跑
155
+ // optimizer 偶尔也能通过, 不要因为这一步丢掉重启 dev 的机会
156
+ let optimizeError;
157
+ if (installError === undefined &&
158
+ process.env.SANDBOX_ID !== undefined &&
159
+ process.env.SANDBOX_ID !== '') {
160
+ (0, logger_1.log)('migrate', 'Pre-warming vite deps cache (vite optimize --force)...');
161
+ try {
162
+ (0, node_child_process_1.execFileSync)('npx', ['-y', 'vite', 'optimize', '--force'], {
163
+ cwd: targetDir,
164
+ stdio: (0, output_1.isJsonMode)() ? ['ignore', 'ignore', 'inherit'] : 'inherit',
165
+ timeout: 120_000,
166
+ });
167
+ }
168
+ catch (err) {
169
+ optimizeError = err instanceof Error ? err.message : String(err);
170
+ (0, logger_1.log)('migrate', `⚠ vite optimize prewarm failed (continuing): ${optimizeError}`);
171
+ }
172
+ }
137
173
  // 沙箱环境下重启 dev process —— scripts/dev.js 被 fullstack 版本覆盖了,但当前正在
138
174
  // 跑的 node 进程仍按老逻辑工作(只起 vite,没起 nest),整体没切到 fullstack 模式。
139
175
  // 通过 pkill 杀掉 dev.js 主进程,依赖沙箱平台 supervisor 自动重新 exec dev.sh → dev.js,
140
176
  // 新进程用新 deps + 新 scripts,正常进入 fullstack 模式。
141
177
  // - 只在 SANDBOX_ID 非空时做(本地环境用户进程混杂,主动 pkill 风险大)
178
+ // - install 挂了不做 —— 新依赖不全, 杀旧 dev 后新 dev 起不来反而更糟,
179
+ // 留着旧进程让用户先处理 installError
142
180
  // - 软失败:pkill 无匹配进程退出 1,catch 吞掉
143
181
  let devRestarted = false;
144
- if (process.env.SANDBOX_ID !== undefined && process.env.SANDBOX_ID !== '') {
182
+ if (installError === undefined &&
183
+ process.env.SANDBOX_ID !== undefined &&
184
+ process.env.SANDBOX_ID !== '') {
145
185
  (0, logger_1.log)('migrate', '沙箱环境,重启 dev process(平台 supervisor 会自动拉起)...');
146
186
  try {
147
187
  (0, node_child_process_1.execFileSync)('pkill', ['-f', 'node.*scripts/dev\\.js'], { stdio: 'ignore' });
@@ -165,6 +205,7 @@ async function handleAppMigrate(opts) {
165
205
  ...summarizeResults(results),
166
206
  followLatestPackages: followLatest,
167
207
  installError,
208
+ optimizeError,
168
209
  devRestarted,
169
210
  nextActions: process.env.SANDBOX_ID !== undefined && process.env.SANDBOX_ID !== ''
170
211
  ? [
@@ -16,6 +16,7 @@ const sync_configs_1 = require("../../../config/sync-configs");
16
16
  const sync_rule_1 = require("../../../utils/sync-rule");
17
17
  const platform_sync_1 = require("../../../utils/platform-sync");
18
18
  const githooks_1 = require("../../../utils/githooks");
19
+ const logs_dir_1 = require("../../../utils/logs-dir");
19
20
  const install_1 = require("../../../services/app/init/install");
20
21
  const spark_meta_1 = require("../../../utils/spark-meta");
21
22
  const error_1 = require("../../../utils/error");
@@ -81,6 +82,10 @@ async function handleAppSync(opts) {
81
82
  }
82
83
  // 2. activate git hooks(template 自带 .githooks/pre-commit,core.hooksPath 一次性设置)
83
84
  const hookActivation = (0, githooks_1.activateGitHooks)(targetDir);
85
+ // logs/ 兜底:dev.js / dev-local.js 启动后才 mkdir,启动前任何想 redirect 到 logs/*.log 的
86
+ // shell 命令都会因父目录不存在直接 ENOENT 挂掉。sync 也建一次,长期使用的 user app 老仓库
87
+ // 第一次升级也能拿到这个目录。
88
+ (0, logs_dir_1.ensureLogsDir)(targetDir);
84
89
  // 3. snapshot 当前 user app 装的管控包版本,供 install 后 diff 用
85
90
  const beforeSpecs = snapshotManagedDepSpecs(targetDir);
86
91
  // 4. npm install —— 跟 init 一样软失败,install 挂了不该阻断 emit / sync 总结输出。
@@ -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 = 'WORKSPACE_READY';
18
+ /** 安装失败 marker 名 */
19
+ const MARKER_FAILED = 'WORKSPACE_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 定成败:成功写 WORKSPACE_READY,失败写 WORKSPACE_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.resolveNpmInstallRegistry = 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.resolveNpmInstallRegistry = 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; } });
@@ -11,3 +11,8 @@ Object.defineProperty(exports, "writeSparkMeta", { enumerable: true, get: functi
11
11
  var install_1 = require("./install");
12
12
  Object.defineProperty(exports, "installDependencies", { enumerable: true, get: function () { return install_1.installDependencies; } });
13
13
  Object.defineProperty(exports, "resolveNpmInstallRegistry", { enumerable: true, get: function () { return install_1.resolveNpmInstallRegistry; } });
14
+ var async_install_1 = require("./async-install");
15
+ Object.defineProperty(exports, "dispatchAsyncInstall", { enumerable: true, get: function () { return async_install_1.dispatchAsyncInstall; } });
16
+ Object.defineProperty(exports, "runAsyncInstallWorker", { enumerable: true, get: function () { return async_install_1.runAsyncInstallWorker; } });
17
+ Object.defineProperty(exports, "isSandboxEnv", { enumerable: true, get: function () { return async_install_1.isSandboxEnv; } });
18
+ Object.defineProperty(exports, "ASYNC_INSTALL_LOG", { enumerable: true, get: function () { return async_install_1.ASYNC_INSTALL_LOG; } });
@@ -0,0 +1,19 @@
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.ensureLogsDir = ensureLogsDir;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /**
10
+ * 确保 user app 根下存在 `logs/` 目录(幂等)。
11
+ *
12
+ * 模板的 dev.js / dev-local.js 自己也会 mkdir LOG_DIR,但只在进程启动后才执行 —— AI / 用户
13
+ * 在 dev 启动前跑 `npm run dev > logs/dev.log 2>&1` 这种 shell redirect 时,shell 先 open
14
+ * 目标文件,父目录不存在直接 ENOENT,子进程根本不会启动。init / sync 落地阶段先把空目录建出来,
15
+ * 避免这种「目录不存在导致 redirect 失败」的低级问题。
16
+ */
17
+ function ensureLogsDir(targetDir) {
18
+ node_fs_1.default.mkdirSync(node_path_1.default.join(targetDir, 'logs'), { recursive: true });
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/miaoda-cli",
3
- "version": "0.1.17",
3
+ "version": "0.1.18-alpha.b094028",
4
4
  "description": "Miaoda 平台命令行工具,面向 Agent 调用",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env sh
2
2
  [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+
3
4
  export PATH="node_modules/.bin:$PATH"
4
5
  npm run precommit
@@ -5,9 +5,11 @@
5
5
  //
6
6
  // 流程:
7
7
  // 1. env pull —— 拉沙箱身份/凭证到 .env.local
8
- // 2. skills sync —— 同步当前 stack agent skills
9
- // 3. dotenv 加载 .env / .env.local process.env(含 SUDA_WEBUSER 适配)
10
- // 4. 起单进程 dev server(design-stack 单进程,无 server/client 拆分)
8
+ // 2. action-plugin init —— user app package.json.actionPlugins 里声明的插件
9
+ // 3. skills sync —— 同步当前 stack agent skills
10
+ // 4. dotenv 加载 .env / .env.local 到 process.env(含 SUDA_WEBUSER 适配)
11
+ // 5. 起单进程 dev server(design-stack 单进程,无 server/client 拆分),
12
+ // stdout/stderr 整体 tee 到 logs/dev.std.log
11
13
  //
12
14
  // 设计同 nestjs-react-fullstack/dev-local.js,SDK 包不需要自己 require('dotenv'),
13
15
  // env 加载收敛在启动脚本单点。SUDA_WEBUSER 适配同 nrf 那份。
@@ -26,7 +28,11 @@ function warn(msg) {
26
28
  if (!process.env.MIAODA_APP_TYPE) process.env.MIAODA_APP_TYPE = '4';
27
29
  process.env.MIAODA_LOCAL_DEV = '1';
28
30
 
29
- console.log('[dev-local] (1/4) env pull...');
31
+ // 先建 logs/,防止任何步骤(尤其是 spawn 子进程前的 shell redirect)因父目录不存在挂掉
32
+ const LOG_DIR = process.env.LOG_DIR || 'logs';
33
+ fs.mkdirSync(LOG_DIR, { recursive: true });
34
+
35
+ console.log('[dev-local] (1/5) env pull...');
30
36
  const hasLarkCli = spawnSync('command', ['-v', 'lark-cli'], { shell: true, stdio: 'ignore' }).status === 0;
31
37
  if (hasLarkCli) {
32
38
  let appId = '';
@@ -47,15 +53,23 @@ if (hasLarkCli) {
47
53
  warn('lark-cli 未安装,跳过 env pull;请确保 .env.local 已就绪');
48
54
  }
49
55
 
56
+ // action-plugin init —— 装 user app 在 package.json.actionPlugins 里声明的插件。
57
+ console.log('[dev-local] (2/5) action-plugin init...');
58
+ try {
59
+ execSync('npx -y @lark-apaas/fullstack-cli@latest action-plugin init', { stdio: 'inherit' });
60
+ } catch {
61
+ warn('action-plugin init 失败,继续启动');
62
+ }
63
+
50
64
  // skills sync —— --local 切 flat layout;不传 --version,handler 默认 coding-steering@latest。
51
- console.log('[dev-local] (2/4) miaoda skills sync...');
65
+ console.log('[dev-local] (3/5) miaoda skills sync...');
52
66
  try {
53
67
  execSync('npx -y @lark-apaas/miaoda-cli@latest skills sync --local', { stdio: 'inherit' });
54
68
  } catch {
55
69
  console.log(' (skills sync 失败,继续启动)');
56
70
  }
57
71
 
58
- console.log('[dev-local] (3/4) loading .env / .env.local...');
72
+ console.log('[dev-local] (4/5) loading .env / .env.local...');
59
73
  const dotenv = require('dotenv');
60
74
  dotenv.config({ path: '.env.local' });
61
75
  dotenv.config({ path: '.env' });
@@ -75,10 +89,55 @@ if (process.env.SUDA_WEBUSER) {
75
89
  }
76
90
  }
77
91
 
78
- console.log('[dev-local] (4/4) npm run dev');
79
- const child = spawn('npm', ['run', 'dev'], { stdio: 'inherit', env: process.env });
80
- child.on('exit', (code) => process.exit(code ?? 0));
92
+ const devLogPath = path.join(LOG_DIR, 'dev.std.log');
93
+ console.log('[dev-local] (5/5) npm run dev');
94
+ console.log(`[dev-local] 日志: ${devLogPath}`);
95
+
96
+ const logFd = fs.openSync(devLogPath, 'a');
97
+ const child = spawn('npm', ['run', 'dev'], {
98
+ stdio: ['ignore', 'pipe', 'pipe'],
99
+ env: process.env,
100
+ });
101
+
102
+ const tee = (src) =>
103
+ src.on('data', (chunk) => {
104
+ try {
105
+ process.stdout.write(chunk);
106
+ } catch {
107
+ /* terminal gone */
108
+ }
109
+ try {
110
+ fs.writeSync(logFd, chunk);
111
+ } catch {
112
+ /* log fd closed */
113
+ }
114
+ });
115
+ tee(child.stdout);
116
+ tee(child.stderr);
117
+
118
+ // 外部 SIGTERM/SIGHUP 转发给 npm,避免本进程死了 nest 变孤儿
119
+ // (SIGINT 在 TTY 下 shell 直接发给整个前台进程组,不需要转发)
120
+ const forward = (sig) => () => {
121
+ try {
122
+ child.kill(sig);
123
+ } catch {
124
+ /* already gone */
125
+ }
126
+ };
127
+ process.on('SIGTERM', forward('SIGTERM'));
128
+ process.on('SIGHUP', forward('SIGHUP'));
129
+
130
+ // 'close' 而非 'exit':等 child 的 stdio stream drain 完才触发,
131
+ // 保证 tee 把最后一批 chunk 写进 logFd 再关闭,不丢尾。
132
+ child.on('close', (code) => {
133
+ try {
134
+ fs.closeSync(logFd);
135
+ } catch {
136
+ /* already closed */
137
+ }
138
+ process.exit(code ?? 0);
139
+ });
81
140
  child.on('error', (err) => {
82
- console.error(err);
141
+ console.error('[dev-local] 启动失败:', err.message);
83
142
  process.exit(1);
84
143
  });
@@ -6,6 +6,10 @@ const { spawnSync } = require('node:child_process');
6
6
 
7
7
  const SEP = ' ' + '─'.repeat(36);
8
8
 
9
+ // package-lock.json 锁内网镜像源 → 线上构建无法访问,改用公共镜像源。
10
+ // 后续如有其它内网域名需要拦截,在这里加 pattern 即可。
11
+ const INTERNAL_REGISTRY_PATTERNS = [/bnpm\.byted\.org/];
12
+
9
13
  function failAndExit(step, body) {
10
14
  process.stderr.write('\n✗ pre-commit failed: ' + step + '\n');
11
15
  process.stderr.write(SEP + '\n');
@@ -17,6 +21,37 @@ function failAndExit(step, body) {
17
21
  process.exit(1);
18
22
  }
19
23
 
24
+ function checkLockfileRegistry() {
25
+ const res = spawnSync(
26
+ 'git',
27
+ ['diff', '--cached', '--diff-filter=ACMR', '--', 'package-lock.json'],
28
+ { stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
29
+ );
30
+ // git 不可用 / 不在 git 仓库 → 静默放行,交给 lint 步骤报错
31
+ if (res.error || res.status !== 0) return;
32
+ const diff = res.stdout ? res.stdout.toString() : '';
33
+ // 只看本次新增行(`+` 开头但排除 `+++` 文件头)
34
+ const hit = diff
35
+ .split('\n')
36
+ .some(
37
+ (line) =>
38
+ line.startsWith('+') &&
39
+ !line.startsWith('+++') &&
40
+ INTERNAL_REGISTRY_PATTERNS.some((p) => p.test(line)),
41
+ );
42
+ if (!hit) return;
43
+ failAndExit(
44
+ 'package-lock.json 使用了内网镜像源',
45
+ [
46
+ '线上构建环境无法访问内网镜像源,将导致部署阶段 npm install 失败。',
47
+ '请使用公共镜像源重新生成 lockfile:',
48
+ '',
49
+ ' rm -rf node_modules package-lock.json',
50
+ ' npm install --registry=https://registry.npmmirror.com',
51
+ ].join('\n'),
52
+ );
53
+ }
54
+
20
55
  function runLint() {
21
56
  const cwd = process.cwd();
22
57
  const res = spawnSync('npm', ['run', 'lint'], {
@@ -34,4 +69,5 @@ function runLint() {
34
69
  }
35
70
  }
36
71
 
72
+ checkLockfileRegistry();
37
73
  runLint();
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env sh
2
2
  [ "$SKIP_GIT_HOOKS" = "1" ] && exit 0
3
+
3
4
  export PATH="node_modules/.bin:$PATH"
4
5
  npm run precommit
@@ -1,5 +1,5 @@
1
1
  run = ["npm", "run", "dev"] # 默认 spark-cli dev
2
- hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
2
+ hidden = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "tmp", ".spark_project", ".playwright-cli"]
3
3
  lint = ["npm", "run", "lint"]
4
4
  test = ["npm", "run", "test"]
5
5
  genDbSchema = ["npm", "run", "gen:db-schema"]
@@ -13,4 +13,4 @@ run = ["npm", "run", "start"]
13
13
  [files.restrict]
14
14
  pathPatterns = ["client/src/api/gen", "package.json", ".spark_project", ".gitignore"]
15
15
  [files.hidden]
16
- pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", "tmp", ".spark_project", ".playwright-cli"]
16
+ pathPatterns = [".config", ".git", "scripts", "node_modules", "dist", ".spark", ".agent", ".agents", ".claude", "tmp", ".spark_project", ".playwright-cli"]
@@ -5,9 +5,12 @@
5
5
  //
6
6
  // 流程:
7
7
  // 1. env pull —— 拉沙箱身份/凭证到 .env.local
8
- // 2. skills sync —— 同步当前 stack agent skills
9
- // 3. dotenv 加载 .env / .env.local process.env(含 SUDA_WEBUSER 适配)
10
- // 4. 并发起 dev:server + dev:client(子进程继承 process.env
8
+ // 2. action-plugin init —— user app package.json.actionPlugins 里声明的插件
9
+ // 3. skills sync —— 同步当前 stack agent skills
10
+ // 4. dotenv 加载 .env / .env.local 到 process.env(含 SUDA_WEBUSER 适配)
11
+ // 5. concurrently 并发起 dev:server + dev:client,整体 stdout/stderr tee 到
12
+ // logs/dev.std.log;server / client 输出靠 concurrently 自带 [server]/[client]
13
+ // 前缀区分,`grep '\[server\]' logs/dev.std.log` 拿单边日志
11
14
  //
12
15
  // 关键设计:本脚本在 spawn 子进程之前先把 .env / .env.local 加载到 process.env,
13
16
  // 然后 spawn 的 server / client 进程通过 env 继承直接拿到——SDK(fullstack-nestjs-core
@@ -33,8 +36,12 @@ function warn(msg) {
33
36
  if (!process.env.MIAODA_APP_TYPE) process.env.MIAODA_APP_TYPE = '3';
34
37
  process.env.MIAODA_LOCAL_DEV = '1';
35
38
 
39
+ // 先建 logs/,防止任何步骤(尤其是 spawn 子进程前的 shell redirect)因父目录不存在挂掉
40
+ const LOG_DIR = process.env.LOG_DIR || 'logs';
41
+ fs.mkdirSync(LOG_DIR, { recursive: true });
42
+
36
43
  // 1. env pull
37
- console.log('[dev-local] (1/4) env pull...');
44
+ console.log('[dev-local] (1/5) env pull...');
38
45
  const hasLarkCli = spawnSync('command', ['-v', 'lark-cli'], { shell: true, stdio: 'ignore' }).status === 0;
39
46
  if (hasLarkCli) {
40
47
  let appId = '';
@@ -55,20 +62,28 @@ if (hasLarkCli) {
55
62
  warn('lark-cli 未安装,跳过 env pull;请确保 .env.local 已就绪');
56
63
  }
57
64
 
58
- // 2. skills sync —— --local 切到 flat layout (.agents/skills + .claude/skills 软链),
65
+ // 2. action-plugin init —— user app package.json.actionPlugins 里声明的插件。
66
+ console.log('[dev-local] (2/5) action-plugin init...');
67
+ try {
68
+ execSync('npx -y @lark-apaas/fullstack-cli@latest action-plugin init', { stdio: 'inherit' });
69
+ } catch {
70
+ warn('action-plugin init 失败,继续启动');
71
+ }
72
+
73
+ // 3. skills sync —— --local 切到 flat layout (.agents/skills + .claude/skills 软链),
59
74
  // 跟沙箱 nested layout 区分。不传 --version,handler 默认拉 coding-steering@latest,
60
75
  // 保证每次本地 npm run dev 都把 skills 升到最新。
61
- console.log('[dev-local] (2/4) miaoda skills sync...');
76
+ console.log('[dev-local] (3/5) miaoda skills sync...');
62
77
  try {
63
78
  execSync('npx -y @lark-apaas/miaoda-cli@latest skills sync --local', { stdio: 'inherit' });
64
79
  } catch {
65
80
  console.log(' (skills sync 失败,继续启动)');
66
81
  }
67
82
 
68
- // 3. 加载 .env / .env.local 到 process.env
83
+ // 4. 加载 .env / .env.local 到 process.env
69
84
  // dotenv 默认 override:false,先到先得 → 先 .env.local 让它优先于 .env;
70
85
  // shell env 已在 process.env,两次 config 都不会覆盖。
71
- console.log('[dev-local] (3/4) loading .env / .env.local...');
86
+ console.log('[dev-local] (4/5) loading .env / .env.local...');
72
87
  const dotenv = require('dotenv');
73
88
  dotenv.config({ path: '.env.local' });
74
89
  dotenv.config({ path: '.env' });
@@ -89,8 +104,12 @@ if (process.env.SUDA_WEBUSER) {
89
104
  }
90
105
  }
91
106
 
92
- // 4. 并发起前后端 dev server
93
- console.log('[dev-local] (4/4) 并发起 dev:server + dev:client');
107
+ // 5. 并发起前后端 dev server,整体 tee 到 logs/dev.std.log
108
+ const devLogPath = path.join(LOG_DIR, 'dev.std.log');
109
+ console.log('[dev-local] (5/5) 并发起 dev:server + dev:client');
110
+ console.log(`[dev-local] 日志: ${devLogPath}`);
111
+
112
+ const logFd = fs.openSync(devLogPath, 'a');
94
113
  const child = spawn(
95
114
  'npx',
96
115
  [
@@ -104,10 +123,49 @@ const child = spawn(
104
123
  'npm run dev:server',
105
124
  'npm run dev:client',
106
125
  ],
107
- { stdio: 'inherit', env: process.env },
126
+ { stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
108
127
  );
109
- child.on('exit', (code) => process.exit(code ?? 0));
128
+
129
+ const tee = (src) =>
130
+ src.on('data', (chunk) => {
131
+ try {
132
+ process.stdout.write(chunk);
133
+ } catch {
134
+ /* terminal gone */
135
+ }
136
+ try {
137
+ fs.writeSync(logFd, chunk);
138
+ } catch {
139
+ /* log fd closed */
140
+ }
141
+ });
142
+ tee(child.stdout);
143
+ tee(child.stderr);
144
+
145
+ // 外部 SIGTERM/SIGHUP 转发给 concurrently,避免本进程死了 server/client 变孤儿
146
+ // (SIGINT 在 TTY 下 shell 直接发给整个前台进程组,不需要转发)
147
+ const forward = (sig) => () => {
148
+ try {
149
+ child.kill(sig);
150
+ } catch {
151
+ /* already gone */
152
+ }
153
+ };
154
+ process.on('SIGTERM', forward('SIGTERM'));
155
+ process.on('SIGHUP', forward('SIGHUP'));
156
+
157
+ // 'close' 而非 'exit':等 child 的 stdio stream drain 完才触发,
158
+ // 保证 tee 把最后一批 chunk 写进 logFd 再关闭,不丢尾。
159
+ child.on('close', (code) => {
160
+ try {
161
+ fs.closeSync(logFd);
162
+ } catch {
163
+ /* already closed */
164
+ }
165
+ process.exit(code ?? 0);
166
+ });
110
167
  child.on('error', (err) => {
111
- console.error(err);
168
+ console.error('[dev-local] 启动失败:', err.message);
169
+ console.error('[dev-local] 如缺 concurrently,运行: npm install');
112
170
  process.exit(1);
113
171
  });
@@ -6,6 +6,10 @@ const { spawnSync } = require('node:child_process');
6
6
 
7
7
  const SEP = ' ' + '─'.repeat(36);
8
8
 
9
+ // package-lock.json 锁内网镜像源 → 线上构建无法访问,改用公共镜像源。
10
+ // 后续如有其它内网域名需要拦截,在这里加 pattern 即可。
11
+ const INTERNAL_REGISTRY_PATTERNS = [/bnpm\.byted\.org/];
12
+
9
13
  function failAndExit(step, body) {
10
14
  process.stderr.write('\n✗ pre-commit failed: ' + step + '\n');
11
15
  process.stderr.write(SEP + '\n');
@@ -17,6 +21,37 @@ function failAndExit(step, body) {
17
21
  process.exit(1);
18
22
  }
19
23
 
24
+ function checkLockfileRegistry() {
25
+ const res = spawnSync(
26
+ 'git',
27
+ ['diff', '--cached', '--diff-filter=ACMR', '--', 'package-lock.json'],
28
+ { stdio: ['ignore', 'pipe', 'pipe'], env: process.env },
29
+ );
30
+ // git 不可用 / 不在 git 仓库 → 静默放行,交给 lint 步骤报错
31
+ if (res.error || res.status !== 0) return;
32
+ const diff = res.stdout ? res.stdout.toString() : '';
33
+ // 只看本次新增行(`+` 开头但排除 `+++` 文件头)
34
+ const hit = diff
35
+ .split('\n')
36
+ .some(
37
+ (line) =>
38
+ line.startsWith('+') &&
39
+ !line.startsWith('+++') &&
40
+ INTERNAL_REGISTRY_PATTERNS.some((p) => p.test(line)),
41
+ );
42
+ if (!hit) return;
43
+ failAndExit(
44
+ 'package-lock.json 使用了内网镜像源',
45
+ [
46
+ '线上构建环境无法访问内网镜像源,将导致部署阶段 npm install 失败。',
47
+ '请使用公共镜像源重新生成 lockfile:',
48
+ '',
49
+ ' rm -rf node_modules package-lock.json',
50
+ ' npm install --registry=https://registry.npmmirror.com',
51
+ ].join('\n'),
52
+ );
53
+ }
54
+
20
55
  function runLint() {
21
56
  const cwd = process.cwd();
22
57
  const res = spawnSync('npm', ['run', 'lint'], {
@@ -34,4 +69,5 @@ function runLint() {
34
69
  }
35
70
  }
36
71
 
72
+ checkLockfileRegistry();
37
73
  runLint();