aicodeswitch 6.0.0 → 6.0.2

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/scripts/dev.js DELETED
@@ -1,283 +0,0 @@
1
- /**
2
- * 开发模式启动器(替代 concurrently)
3
- *
4
- * 目标:
5
- * 1. 先启动 server,轮询 /health 确认服务可用后,再启动 UI(避免 UI 起来后代理目标尚未就绪)。
6
- * 2. Ctrl+C 后「同步阻塞」到服务子进程真正退出(即服务端打印 Server stopped. 并
7
- * process.exit 之后)再退出父进程,从而避免「终端提示符已回但端口 4567 尚未释放」
8
- * 导致的快速重启 EADDRINUSE 冲突。
9
- *
10
- * 原理:Node 无法真正同步阻塞事件循环,但只要本进程(前台进程组长)不调用
11
- * process.exit,终端就不会回到提示符。因此我们在信号处理器里 await 子进程的
12
- * 'exit' 事件,等价于「等到 Server stopped. 出现」。
13
- *
14
- * 直接运行:node scripts/dev.js(package.json 的 dev 脚本)
15
- */
16
-
17
- const { spawn } = require('child_process');
18
- const path = require('path');
19
- const readline = require('readline');
20
- const http = require('http');
21
-
22
- // 可选彩色前缀;非 TTY 时 chalk 自动失活,无副作用
23
- let chalk;
24
- try {
25
- // eslint-disable-next-line global-require
26
- chalk = require('chalk');
27
- } catch {
28
- chalk = {
29
- cyan: (s) => s,
30
- magenta: (s) => s,
31
- gray: (s) => s,
32
- yellow: (s) => s,
33
- };
34
- }
35
-
36
- const ROOT = path.resolve(__dirname, '..');
37
-
38
- // 等待子进程优雅退出的硬超时(秒)。需大于服务端 server.close 的 5s 上限 + 配置恢复余量。
39
- const SHUTDOWN_TIMEOUT_MS = 15000;
40
-
41
- // 服务端健康检查配置:轮询 /health 直到服务可用再启动 UI。
42
- const SERVER_PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 4567;
43
- const SERVER_HOST = '127.0.0.1';
44
- const HEALTH_PATH = '/health';
45
- const HEALTH_POLL_INTERVAL_MS = 300;
46
- const HEALTH_WAIT_TIMEOUT_MS = 30000;
47
-
48
- const SERVER_LABEL = chalk.cyan('[server]');
49
- const UI_LABEL = chalk.magenta('[ui]');
50
- const DEV_LABEL = chalk.gray('[dev]');
51
-
52
- /**
53
- * 探测服务端 /health 是否可用(返回 2xx 即视为就绪)。
54
- * @returns {Promise<boolean>}
55
- */
56
- function checkServerHealth() {
57
- return new Promise((resolve) => {
58
- const req = http.get(
59
- {
60
- host: SERVER_HOST,
61
- port: SERVER_PORT,
62
- path: HEALTH_PATH,
63
- timeout: 1500,
64
- },
65
- (res) => {
66
- res.resume();
67
- resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300);
68
- },
69
- );
70
- req.on('error', () => resolve(false));
71
- req.on('timeout', () => { req.destroy(); resolve(false); });
72
- });
73
- }
74
-
75
- /**
76
- * 轮询等待服务端可用。期间若 serverChild 已退出,立即返回 false。
77
- * @param {import('child_process').ChildProcess} serverChild
78
- * @returns {Promise<boolean>} 服务是否在超时内就绪
79
- */
80
- async function waitForServerReady(serverChild) {
81
- const deadline = Date.now() + HEALTH_WAIT_TIMEOUT_MS;
82
- while (Date.now() < deadline) {
83
- // 服务进程已退出则无需再等
84
- if (serverChild.exitCode !== null || serverChild.signalCode) return false;
85
- if (await checkServerHealth()) return true; // eslint-disable-line no-await-in-loop
86
- await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL_MS)); // eslint-disable-line no-await-in-loop
87
- }
88
- return false;
89
- }
90
-
91
- /**
92
- * 给子进程的 stdout/stderr 加行级前缀后写出。
93
- * @param {import('child_process').ChildProcess} child
94
- * @param {string} label
95
- */
96
- function prefixStream(child, label) {
97
- for (const streamName of ['stdout', 'stderr']) {
98
- const stream = child[streamName];
99
- if (!stream) continue;
100
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
101
- rl.on('line', (line) => {
102
- process.stdout.write(`${label} ${line}\n`);
103
- });
104
- }
105
- }
106
-
107
- const IS_WIN = process.platform === 'win32';
108
-
109
- /**
110
- * 进程组是否仍存活(组内还有任意进程)。
111
- * 子进程以 detached:true 启动后,其 pid == pgid,故 -pid 可探测整组。
112
- * 关键点:tsx 会 fork——serverChild 只是 tsx 启动器(launcher),真正运行
113
- * main.ts 的 node 进程、以及 esbuild 服务进程都是它的子孙,与启动器同组。
114
- * 启动器的 'exit'/'close' 在它自身一退出就触发(Ctrl+C 时几乎立刻),
115
- * 而真正的服务进程仍在做数秒优雅关闭并随后打印 "Server stopped."。
116
- * 因此「等启动器退出」≠「等服务停止」。改用「轮询整组存活」:只要组内
117
- * 还有任何进程(真正的 node 服务),就算未停止;等整组清空才等价于
118
- * 「Server stopped. 已出现并 process.exit」。
119
- */
120
- function groupAlive(pid) {
121
- if (pid == null) return false;
122
- try {
123
- process.kill(-pid, 0); // 信号 0 = 探测存在性;-pid = 整个进程组
124
- return true;
125
- } catch {
126
- return false;
127
- }
128
- }
129
-
130
- /**
131
- * 向子进程所在进程组发信号(覆盖 launcher + 真正服务 + esbuild)。
132
- * Windows 无进程组语义,退化为直接 kill 直接子进程。
133
- */
134
- function signalGroup(child, signal) {
135
- if (!child || child.exitCode !== null || child.signalCode) return;
136
- const pid = child.pid;
137
- if (IS_WIN || pid == null) {
138
- try { child.kill(signal); } catch { /* ignore */ }
139
- return;
140
- }
141
- try {
142
- process.kill(-pid, signal);
143
- } catch {
144
- try { child.kill(signal); } catch { /* ignore */ }
145
- }
146
- }
147
-
148
- /**
149
- * 等待整个进程组清空(POSIX),Windows 退化为等待直接子进程 'close'。
150
- * 不带内部超时——超时由调用方用 SIGKILL 清组后,本函数下一拍(≤100ms)即感知并 resolve。
151
- * @returns {Promise<number>} 直接子进程的 exit code(仅用于异常码上报)
152
- */
153
- function waitForGroupExit(child) {
154
- return new Promise((resolve) => {
155
- const pid = child.pid;
156
- if (IS_WIN || pid == null) {
157
- if (child.exitCode !== null || child.signalCode) { resolve(child.exitCode ?? 0); return; }
158
- child.once('close', (code, signal) => resolve(code ?? (signal ? 128 + 1 : 0)));
159
- return;
160
- }
161
- if (!groupAlive(pid)) { resolve(child.exitCode ?? 0); return; }
162
- const iv = setInterval(() => {
163
- if (!groupAlive(pid)) {
164
- clearInterval(iv);
165
- resolve(child.exitCode ?? 0);
166
- }
167
- }, 100);
168
- });
169
- }
170
-
171
- async function main() {
172
- let exitCode = 0;
173
- let shuttingDown = false;
174
- let uiChild = null;
175
-
176
- // 直接调本地 bin,去掉 `npm run` 额外进程层。
177
- // POSIX 不开 shell(避免中间 /bin/sh 抢先于 SIGINT 退出);Windows 的 .cmd 仍需 shell。
178
- // detached:true:把子进程放进它自己的进程组(POSIX setsid),这样我们可以用
179
- // process.kill(-pid, sig) 给整组发信号、用 groupAlive(-pid) 探测整组存活。
180
- // 这一点对本脚本的核心目标至关重要——见 waitForGroupExit 注释:tsx 会 fork,
181
- // serverChild 只是启动器,真正的 node 服务是它的孙进程,只有「整组存活探测」
182
- // 才能把退出阻塞到真正的 "Server stopped."。
183
- const USE_SHELL = IS_WIN;
184
- const serverChild = spawn(
185
- 'tsx',
186
- ['--tsconfig=tsconfig.server.json', 'src/server/main.ts'],
187
- { cwd: ROOT, shell: USE_SHELL, detached: !IS_WIN, stdio: ['inherit', 'pipe', 'pipe'] },
188
- );
189
- prefixStream(serverChild, SERVER_LABEL);
190
-
191
- // 在启动 UI 之前注册信号处理与 server 退出级联,确保等待健康期间 Ctrl+C 也能正确清理
192
- process.on('SIGINT', () => { void stop('Ctrl+C'); });
193
- process.on('SIGTERM', () => { void stop('SIGTERM'); });
194
-
195
- serverChild.once('exit', (code) => {
196
- if (!shuttingDown) {
197
- process.stderr.write(
198
- `${DEV_LABEL} 服务进程已退出${code ? `(code=${code})` : ''},停止 UI 进程。\n`,
199
- );
200
- if (code && code !== 0) exitCode = code;
201
- void stop();
202
- }
203
- });
204
-
205
- // 1) 先等待服务端 /health 可用,再启动 UI,避免 UI 启动时代理目标尚未就绪
206
- process.stdout.write(
207
- `${DEV_LABEL} 等待服务端就绪(http://${SERVER_HOST}:${SERVER_PORT}${HEALTH_PATH})...\n`,
208
- );
209
- const ready = await waitForServerReady(serverChild);
210
- if (!ready) {
211
- process.stderr.write(
212
- `${DEV_LABEL} 服务端在 ${HEALTH_WAIT_TIMEOUT_MS / 1000}s 内未就绪,放弃启动 UI。\n`,
213
- );
214
- // 走正常停止流程:清理可能残留的服务子进程后退出
215
- await stop();
216
- return;
217
- }
218
- process.stdout.write(`${DEV_LABEL} 服务端已就绪,启动 UI...\n`);
219
-
220
- // 2) 启动 UI(同样放进独立进程组,便于整组信号/清理,避免 vite 的 esbuild 子进程残留)
221
- uiChild = spawn('vite', [], {
222
- cwd: ROOT,
223
- shell: USE_SHELL,
224
- detached: !IS_WIN,
225
- stdio: ['inherit', 'pipe', 'pipe'],
226
- });
227
- prefixStream(uiChild, UI_LABEL);
228
-
229
- uiChild.once('exit', (code) => {
230
- if (!shuttingDown) {
231
- process.stderr.write(
232
- `${DEV_LABEL} UI 进程已退出${code ? `(code=${code})` : ''},停止服务进程。\n`,
233
- );
234
- if (code && code !== 0) exitCode = code;
235
- void stop();
236
- }
237
- });
238
-
239
- /**
240
- * 停止流程:向子进程转发信号,等待服务子进程(长时序)与 UI 子进程(若已启动)都退出。
241
- * 关键:不在此处立即 process.exit——父进程存活 = 终端提示符不回。
242
- */
243
- async function stop(reason) {
244
- if (shuttingDown) return;
245
- shuttingDown = true;
246
-
247
- if (reason) {
248
- process.stdout.write(
249
- `${DEV_LABEL} ${chalk.yellow(reason)},等待服务完全停止后再退出...\n`,
250
- );
251
- }
252
-
253
- // 子进程各自在独立进程组里,Ctrl+C 不会直达它们,必须由父进程显式给整组发 SIGINT,
254
- // 真正的服务进程才会进入优雅关闭(恢复配置 + server.close + 打印 "Server stopped.")。
255
- signalGroup(serverChild, 'SIGINT');
256
- signalGroup(uiChild, 'SIGINT');
257
-
258
- let timedOut = false;
259
- const timer = setTimeout(() => {
260
- timedOut = true;
261
- process.stderr.write(
262
- `${DEV_LABEL} 等待超时(${SHUTDOWN_TIMEOUT_MS / 1000}s),强制终止残留子进程。\n`,
263
- );
264
- signalGroup(serverChild, 'SIGKILL');
265
- signalGroup(uiChild, 'SIGKILL');
266
- }, SHUTDOWN_TIMEOUT_MS);
267
-
268
- // 阻塞到「整组清空」:等真正的 node 服务进程也退出(而不仅是 tsx 启动器)。
269
- const exits = [waitForGroupExit(serverChild)];
270
- if (uiChild) exits.push(waitForGroupExit(uiChild));
271
- const [serverCode, uiCode] = await Promise.all(exits);
272
-
273
- clearTimeout(timer);
274
-
275
- if (timedOut) exitCode = 1;
276
- else if (serverCode && serverCode !== 0) exitCode = serverCode;
277
- else if (uiCode && uiCode !== 0) exitCode = uiCode;
278
-
279
- process.exit(exitCode);
280
- }
281
- }
282
-
283
- main();