comp-hub 0.42.2 → 0.42.3

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/bin/cli.js CHANGED
@@ -52,4 +52,8 @@ program.action(async () => {
52
52
  }
53
53
  });
54
54
 
55
- program.parse();
55
+ // parseAsync 确保异步 action(如 main 中 await new Promise)完成后进程才退出
56
+ program.parseAsync().catch(err => {
57
+ console.error(`✗ CLI error: ${err.message}`);
58
+ process.exit(1);
59
+ });
package/bin/lib/http.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const http = require("http");
2
2
  const { HTTP_TIMEOUT_GET, HTTP_TIMEOUT_POST } = require("./constants");
3
+ const { apiUrl } = require("./utils");
3
4
 
4
5
  function parseBody(raw) {
5
6
  try {
@@ -59,4 +60,71 @@ function httpPost(url, data) {
59
60
  });
60
61
  }
61
62
 
62
- module.exports = { httpGet, httpPost };
63
+ /**
64
+ * SSE 日志流 — 连接 master 的 /__master__/projects/:hash/logs 端点
65
+ * 持续输出 MidwayJS 子进程的实时日志到终端
66
+ * 返回一个 Promise,在连接关闭时 resolve
67
+ */
68
+ function streamLogs(url, _hash) {
69
+ return new Promise((resolve, reject) => {
70
+ const urlObj = new URL(url);
71
+ const options = {
72
+ hostname: urlObj.hostname,
73
+ port: urlObj.port,
74
+ path: urlObj.pathname,
75
+ method: "GET",
76
+ headers: { Accept: "text/event-stream" }
77
+ };
78
+
79
+ const req = http.request(options, res => {
80
+ if (res.statusCode !== 200) {
81
+ reject(new Error(`SSE connection failed (HTTP ${res.statusCode})`));
82
+ return;
83
+ }
84
+
85
+ let buffer = "";
86
+
87
+ res.on("data", chunk => {
88
+ buffer += chunk.toString();
89
+ // SSE 格式: "data: <json>\n\n"
90
+ const events = buffer.split("\n\n");
91
+ buffer = events.pop() || ""; // 保留不完整的最后一个事件
92
+
93
+ for (const event of events) {
94
+ const dataLine = event.startsWith("data: ") ? event.slice(6) : event;
95
+ try {
96
+ const line = JSON.parse(dataLine);
97
+ // 直接输出日志内容(已有换行符)
98
+ process.stdout.write(line);
99
+ } catch {
100
+ // 非 JSON 数据,忽略
101
+ }
102
+ }
103
+ });
104
+
105
+ res.on("end", () => {
106
+ resolve();
107
+ });
108
+
109
+ res.on("error", err => {
110
+ reject(err);
111
+ });
112
+ });
113
+
114
+ req.on("error", err => {
115
+ reject(err);
116
+ });
117
+
118
+ req.setTimeout(0); // 无超时限制
119
+ req.end();
120
+ });
121
+ }
122
+
123
+ /**
124
+ * 从 master 注销项目(Ctrl+C 时通知 master 停止对应 MidwayJS 子进程)
125
+ */
126
+ function deregisterProject(masterPort, hash) {
127
+ return httpPost(apiUrl(masterPort, "/__master__/deregister"), { hash });
128
+ }
129
+
130
+ module.exports = { httpGet, httpPost, streamLogs, deregisterProject };
@@ -1,7 +1,7 @@
1
1
  const path = require("path");
2
2
  const fs = require("fs");
3
3
  const { spawn } = require("child_process");
4
- const { httpPost } = require("./http");
4
+ const { httpPost, streamLogs, deregisterProject } = require("./http");
5
5
  const { computeHash, projectRoot, readMasterRuntime, loadComphubConfig, colors, apiUrl } = require("./utils");
6
6
  const { cyan, green, white, yellow, red } = colors;
7
7
  const { ADMIN_API, POLL_INTERVAL, MAX_RETRIES, DEFAULT_PORT } = require("./constants");
@@ -34,7 +34,7 @@ async function waitForMasterRuntime() {
34
34
  return null;
35
35
  }
36
36
 
37
- async function spawnMaster(publicPort, pkg, cwd, _opts) {
37
+ async function spawnMaster(publicPort, pkg, cwd) {
38
38
  const masterEntry = path.join(projectRoot, "dist/master.js");
39
39
 
40
40
  if (!fs.existsSync(masterEntry)) {
@@ -42,8 +42,12 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
42
42
  }
43
43
 
44
44
  const masterArgs = ["--port", String(publicPort), "--public-port", String(publicPort)];
45
+ // 不再使用 stdio:inherit,改为 pipe stdout/stderr:
46
+ // - stdin: inherit(Ctrl+C 信号需要)
47
+ // - stdout: pipe(过滤掉 MidwayJS 子进程日志,只保留 master 自身消息)
48
+ // - stderr: pipe(转发给 CLI 用于崩溃诊断)
45
49
  const child = spawn(process.execPath, ["--no-deprecation", masterEntry, ...masterArgs], {
46
- stdio: "inherit",
50
+ stdio: ["inherit", "pipe", "pipe"],
47
51
  cwd,
48
52
  env: {
49
53
  ...process.env,
@@ -51,6 +55,32 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
51
55
  }
52
56
  });
53
57
 
58
+ // stdout 行缓冲(跨 data 事件的未完成行)
59
+ let stdoutPending = "";
60
+
61
+ // 转发 stderr 给 CLI(用于崩溃诊断)
62
+ if (child.stderr) {
63
+ child.stderr.on("data", chunk => {
64
+ process.stderr.write(chunk);
65
+ });
66
+ }
67
+
68
+ // 过滤 stdout:只转发 master 自身消息,跳过带 [hash] 前缀的 MidwayJS 子进程日志
69
+ // MidwayJS 日志会通过 SSE 按项目过滤后展示
70
+ if (child.stdout) {
71
+ child.stdout.on("data", chunk => {
72
+ stdoutPending += chunk.toString();
73
+ const lines = stdoutPending.split("\n");
74
+ stdoutPending = lines.pop() || ""; // 保留不完整行
75
+ for (const line of lines) {
76
+ // 跳过 [8位hex_hash] 前缀的行(属于 MidwayJS 子进程日志)
77
+ if (!/^\[[0-9a-f]{8}\]/.test(line)) {
78
+ process.stdout.write(line + "\n");
79
+ }
80
+ }
81
+ });
82
+ }
83
+
54
84
  // 转发父进程信号,确保 Ctrl+C 时 master 也能优雅退出
55
85
  ["SIGINT", "SIGTERM"].forEach(sig => {
56
86
  process.on(sig, () => {
@@ -60,6 +90,10 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
60
90
 
61
91
  // Master 退出时 CLI 也跟着退出
62
92
  child.on("exit", code => {
93
+ // 输出剩余的 pending 数据
94
+ if (stdoutPending && !/^\[[0-9a-f]{8}\]/.test(stdoutPending)) {
95
+ process.stdout.write(stdoutPending);
96
+ }
63
97
  process.exit(code || 0);
64
98
  });
65
99
 
@@ -69,7 +103,7 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
69
103
  throw new Error("Master startup timeout");
70
104
  }
71
105
 
72
- return { runtime, child };
106
+ return { runtime };
73
107
  }
74
108
 
75
109
  /**
@@ -79,7 +113,6 @@ async function spawnMaster(publicPort, pkg, cwd, _opts) {
79
113
  async function ensureMasterRunning(opts, pkg) {
80
114
  let portMismatch = false;
81
115
  let runningPublicPort = 0;
82
- let masterChild = null;
83
116
 
84
117
  const existingRuntime = await readMasterRuntime();
85
118
 
@@ -90,17 +123,16 @@ async function ensureMasterRunning(opts, pkg) {
90
123
  portMismatch = true;
91
124
  runningPublicPort = existingRuntime.publicPort;
92
125
  }
93
- return { runtime: existingRuntime, portMismatch, runningPublicPort, masterChild };
126
+ return { runtime: existingRuntime, portMismatch, runningPublicPort };
94
127
  }
95
128
 
96
129
  // 启动新 master(前台运行,日志输出到终端)
97
130
  const portHint = opts.port != null ? opts.port : DEFAULT_PORT;
98
131
  const publicPort = await getPort({ port: getPort.makeRange(portHint, portHint + 100) });
99
132
 
100
- const { runtime, child } = await spawnMaster(publicPort, pkg, process.cwd(), opts);
101
- masterChild = child;
133
+ const { runtime } = await spawnMaster(publicPort, pkg, process.cwd());
102
134
 
103
- return { runtime, portMismatch, runningPublicPort, masterChild };
135
+ return { runtime, portMismatch, runningPublicPort };
104
136
  }
105
137
 
106
138
  // ---- Registration ----
@@ -183,7 +215,7 @@ async function main(pkg, opts) {
183
215
 
184
216
  console.log(`${green("✓")} ${pkg.name} v${pkg.version}`);
185
217
 
186
- const { runtime, portMismatch, runningPublicPort, masterChild } = await ensureMasterRunning(opts, pkg);
218
+ const { runtime, portMismatch, runningPublicPort } = await ensureMasterRunning(opts, pkg);
187
219
 
188
220
  const masterPort = runtime.managementPort;
189
221
  const displayPort = runtime.publicPort;
@@ -192,11 +224,30 @@ async function main(pkg, opts) {
192
224
 
193
225
  printBanner(pkg, cwd, hash, displayPort, opts, { portMismatch, runningPublicPort });
194
226
 
195
- // 如果是新启动的 master(非复用已有),CLI 进程保持前台运行以显示服务日志
196
- // masterChild 已通过 stdio:inherit + 移除 unref() 绑定到当前终端,事件循环不会退出
197
- // Ctrl+C 时会通过 spawnMaster 中的信号处理优雅关闭
198
- if (masterChild) {
199
- console.log(`\n Press ${cyan("Ctrl+C")} to stop\n`);
227
+ // Ctrl+C / kill 时通知 master 注销本项目,使 MidwayJS 子进程能够停止
228
+ let deregistered = false;
229
+ const cleanup = async () => {
230
+ if (deregistered) return;
231
+ deregistered = true;
232
+ try {
233
+ await deregisterProject(masterPort, hash);
234
+ } catch {
235
+ // 忽略注销失败(master 可能已关闭)
236
+ }
237
+ process.exit(0);
238
+ };
239
+ process.on("SIGINT", cleanup);
240
+ process.on("SIGTERM", cleanup);
241
+
242
+ // 统一通过 SSE 实时接收本项目 MidwayJS 子进程日志(按 hash 过滤)
243
+ // 第一个 CLI(spawn master 的)不再通过 stdio:inherit 看到其他项目的日志
244
+ console.log(`\n ${white("─")} Streaming logs from MidwayJS (${hash}) ${white("─")}\n`);
245
+
246
+ const logsUrl = `http://localhost:${masterPort}/__master__/projects/${hash}/logs`;
247
+ try {
248
+ await streamLogs(logsUrl, hash);
249
+ } catch (err) {
250
+ console.error(`\n ${red("✗")} Log stream error: ${err.message}`);
200
251
  }
201
252
  }
202
253