ccgauge 1.1.0 → 1.1.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.
Files changed (127) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-build-manifest.json +42 -42
  3. package/.next/standalone/.next/app-path-routes-manifest.json +6 -6
  4. package/.next/standalone/.next/build-manifest.json +2 -2
  5. package/.next/standalone/.next/prerender-manifest.json +3 -3
  6. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  7. package/.next/standalone/.next/server/app/api/blocks/route_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/api/export/usage/route.js +1 -1
  9. package/.next/standalone/.next/server/app/api/export/usage/route_client-reference-manifest.js +1 -1
  10. package/.next/standalone/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
  11. package/.next/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/api/scan/route_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  14. package/.next/standalone/.next/server/app/api/usage/route_client-reference-manifest.js +1 -1
  15. package/.next/standalone/.next/server/app/models/page.js +1 -1
  16. package/.next/standalone/.next/server/app/models/page_client-reference-manifest.js +1 -1
  17. package/.next/standalone/.next/server/app/page.js +2 -2
  18. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/projects/[id]/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/projects/page.js +1 -1
  21. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  22. package/.next/standalone/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -1
  23. package/.next/standalone/.next/server/app/sessions/page.js +1 -1
  24. package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  26. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  27. package/.next/standalone/.next/server/app/usage/page.js +1 -1
  28. package/.next/standalone/.next/server/app/usage/page_client-reference-manifest.js +1 -1
  29. package/.next/standalone/.next/server/app-paths-manifest.json +6 -6
  30. package/.next/standalone/.next/server/chunks/125.js +1 -1
  31. package/.next/standalone/.next/server/chunks/567.js +3 -3
  32. package/.next/standalone/.next/server/chunks/716.js +1 -1
  33. package/.next/standalone/.next/server/chunks/98.js +1 -1
  34. package/.next/standalone/.next/server/functions-config-manifest.json +1 -1
  35. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  36. package/.next/standalone/.next/server/pages/500.html +1 -1
  37. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/static/chunks/148-edf90b0918345dc2.js +1 -0
  39. package/.next/standalone/.next/static/chunks/app/layout-af71188a257674b5.js +1 -0
  40. package/.next/standalone/.next/static/chunks/app/page-1a147d12fca0b184.js +1 -0
  41. package/.next/standalone/.next/static/chunks/app/usage/{page-7fcc2a2d931307d5.js → page-051223f62647aadc.js} +1 -1
  42. package/.next/standalone/.next/static/css/fabb40b2545c70dd.css +5 -0
  43. package/.next/standalone/package.json +8 -2
  44. package/.next/standalone/public/codex-logo.webp +0 -0
  45. package/CHANGELOG.md +160 -0
  46. package/README.md +136 -384
  47. package/README.zh-CN.md +146 -398
  48. package/bin/cli.mjs +30 -190
  49. package/dist/mcp/server.mjs +11 -11
  50. package/dist/report/index.mjs +36 -3127
  51. package/package.json +8 -2
  52. package/.next/standalone/.next/static/chunks/148-f2cba0b76260b8d3.js +0 -1
  53. package/.next/standalone/.next/static/chunks/app/layout-0adb4fc0305adf29.js +0 -1
  54. package/.next/standalone/.next/static/chunks/app/page-3cda7f70ecf5017a.js +0 -1
  55. package/.next/standalone/.next/static/css/bde47638beb0c717.css +0 -3
  56. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/constants.js +0 -10
  57. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/fontkit/index.js +0 -1
  58. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/format-available-values.js +0 -9
  59. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/fetch-css-from-google-fonts.js +0 -28
  60. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/fetch-font-file.js +0 -24
  61. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/fetch-resource.js +0 -46
  62. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/find-font-files-in-css.js +0 -34
  63. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/font-data.json +0 -17669
  64. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/get-fallback-font-override-metrics.js +0 -62
  65. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/get-font-axes.js +0 -66
  66. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/get-google-fonts-url.js +0 -55
  67. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/get-proxy-agent.js +0 -23
  68. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/google-fonts-metadata.js +0 -8
  69. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/loader.js +0 -175
  70. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/retry.js +0 -18
  71. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/sort-fonts-variant-values.js +0 -26
  72. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/google/validate-google-font-function-call.js +0 -101
  73. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/local/get-fallback-metrics-from-font-file.js +0 -85
  74. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/local/loader.js +0 -78
  75. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/local/pick-font-file-for-fallback-generation.js +0 -85
  76. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/local/validate-local-font-function-call.js +0 -66
  77. package/.next/standalone/node_modules/next/dist/compiled/@next/font/dist/next-font-error.js +0 -11
  78. package/.next/standalone/node_modules/next/dist/compiled/@next/font/google/loader.js +0 -1
  79. package/.next/standalone/node_modules/next/dist/compiled/@next/font/local/loader.js +0 -1
  80. package/.next/standalone/node_modules/next/dist/compiled/@next/font/package.json +0 -1
  81. package/.next/standalone/node_modules/next/dist/compiled/amphtml-validator/index.js +0 -1
  82. package/.next/standalone/node_modules/next/dist/compiled/amphtml-validator/package.json +0 -1
  83. package/.next/standalone/node_modules/next/dist/compiled/amphtml-validator/validator_wasm.js +0 -2672
  84. package/.next/standalone/node_modules/next/dist/compiled/babel/bundle.js +0 -227
  85. package/.next/standalone/node_modules/next/dist/compiled/babel/code-frame.js +0 -1
  86. package/.next/standalone/node_modules/next/dist/compiled/babel/core-lib-block-hoist-plugin.js +0 -1
  87. package/.next/standalone/node_modules/next/dist/compiled/babel/core-lib-config.js +0 -1
  88. package/.next/standalone/node_modules/next/dist/compiled/babel/core-lib-normalize-file.js +0 -1
  89. package/.next/standalone/node_modules/next/dist/compiled/babel/core-lib-normalize-opts.js +0 -1
  90. package/.next/standalone/node_modules/next/dist/compiled/babel/core-lib-plugin-pass.js +0 -1
  91. package/.next/standalone/node_modules/next/dist/compiled/babel/core.js +0 -1
  92. package/.next/standalone/node_modules/next/dist/compiled/babel/generator.js +0 -1
  93. package/.next/standalone/node_modules/next/dist/compiled/babel/package.json +0 -1
  94. package/.next/standalone/node_modules/next/dist/compiled/babel/parser.js +0 -1
  95. package/.next/standalone/node_modules/next/dist/compiled/babel/plugin-syntax-jsx.js +0 -1
  96. package/.next/standalone/node_modules/next/dist/compiled/babel/plugin-transform-define.js +0 -1
  97. package/.next/standalone/node_modules/next/dist/compiled/babel/plugin-transform-modules-commonjs.js +0 -1
  98. package/.next/standalone/node_modules/next/dist/compiled/babel/preset-typescript.js +0 -1
  99. package/.next/standalone/node_modules/next/dist/compiled/babel/traverse.js +0 -1
  100. package/.next/standalone/node_modules/next/dist/compiled/babel/types.js +0 -1
  101. package/.next/standalone/node_modules/next/dist/compiled/babel-packages/package.json +0 -1
  102. package/.next/standalone/node_modules/next/dist/compiled/babel-packages/packages-bundle.js +0 -335
  103. package/.next/standalone/node_modules/next/dist/server/capsize-font-metrics.json +0 -181516
  104. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/LICENSE +0 -191
  105. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/lib/sharp-darwin-arm64.node +0 -0
  106. package/.next/standalone/node_modules/next/node_modules/@img/sharp-darwin-arm64/package.json +0 -40
  107. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/lib/index.js +0 -1
  108. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/lib/libvips-cpp.8.17.3.dylib +0 -0
  109. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/package.json +0 -36
  110. package/.next/standalone/node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64/versions.json +0 -30
  111. package/.next/standalone/node_modules/next/node_modules/sharp/lib/channel.js +0 -177
  112. package/.next/standalone/node_modules/next/node_modules/sharp/lib/colour.js +0 -195
  113. package/.next/standalone/node_modules/next/node_modules/sharp/lib/composite.js +0 -212
  114. package/.next/standalone/node_modules/next/node_modules/sharp/lib/constructor.js +0 -499
  115. package/.next/standalone/node_modules/next/node_modules/sharp/lib/index.js +0 -16
  116. package/.next/standalone/node_modules/next/node_modules/sharp/lib/input.js +0 -809
  117. package/.next/standalone/node_modules/next/node_modules/sharp/lib/is.js +0 -143
  118. package/.next/standalone/node_modules/next/node_modules/sharp/lib/libvips.js +0 -207
  119. package/.next/standalone/node_modules/next/node_modules/sharp/lib/operation.js +0 -1016
  120. package/.next/standalone/node_modules/next/node_modules/sharp/lib/output.js +0 -1666
  121. package/.next/standalone/node_modules/next/node_modules/sharp/lib/resize.js +0 -595
  122. package/.next/standalone/node_modules/next/node_modules/sharp/lib/sharp.js +0 -121
  123. package/.next/standalone/node_modules/next/node_modules/sharp/lib/utility.js +0 -291
  124. package/.next/standalone/node_modules/next/node_modules/sharp/package.json +0 -202
  125. package/.next/standalone/public/codex-logo.png +0 -0
  126. /package/.next/standalone/.next/static/{jncTEohJB76Iq9TUm3G21 → EQqRlXV5HyaCYSZWOVllH}/_buildManifest.js +0 -0
  127. /package/.next/standalone/.next/static/{jncTEohJB76Iq9TUm3G21 → EQqRlXV5HyaCYSZWOVllH}/_ssgManifest.js +0 -0
package/bin/cli.mjs CHANGED
@@ -13,11 +13,6 @@ const __dirname = dirname(__filename);
13
13
  const packageRoot = resolve(__dirname, '..');
14
14
  const pkg = require(join(packageRoot, 'package.json'));
15
15
 
16
- // commander is needed before we even parse argv to know which subcommand
17
- // the user asked for, so we load it eagerly. `get-port` (only used by
18
- // start / restart) and `open` (only by start / open) are deferred so
19
- // short-lived commands like `mcp`, `status`, `logs`, `report`, `--version`
20
- // don't pay their import cost (~20-30 ms cold-start each).
21
16
  const { Command } = await import('commander');
22
17
  async function loadGetPort() {
23
18
  const mod = await import('get-port');
@@ -38,13 +33,7 @@ const COMMAND_NAMES = new Set([
38
33
  'start', 'stop', 'restart', 'status', 'open', 'logs', 'mcp',
39
34
  'report', 'doctor',
40
35
  ]);
41
- // `start` subcommand's own option set (incl. short aliases). The value
42
- // indicates whether the option consumes the following positional as a
43
- // value. Used by `normalizeArgv` to decide whether `ccgauge -p 3000`
44
- // should be treated as the bare-`start` shortcut. Anything outside this
45
- // set (e.g. `-r` / `--range` from `report`) makes us bail out and let
46
- // commander surface its own "unknown option" against the root program
47
- // — which lists subcommands. Keep in sync with `addStartOptions`.
36
+
48
37
  const START_OPTIONS = new Map([
49
38
  ['-p', true], ['--port', true],
50
39
  ['-H', true], ['--host', true],
@@ -65,18 +54,6 @@ function buildUrl(host, port) {
65
54
  return `http://${browserHost(host)}:${port}`;
66
55
  }
67
56
 
68
- /** Decide whether to emit ANSI colour. Precedence (high → low):
69
- * 1. `forceOff` (commander's `--no-color`) → off.
70
- * 2. `NO_COLOR` env var (NO_COLOR.org convention) → off.
71
- * 3. `FORCE_COLOR` env var → on (covers tee / pipes / CI).
72
- * 4. stdout TTY check → on.
73
- * 5. else → off.
74
- *
75
- * We take `forceOff` as an explicit boolean rather than reading
76
- * `opts.color` directly because commander gives `.option('--no-color')`
77
- * a default of `true` — there's no in-band way to distinguish "user
78
- * didn't say anything" from "user passed --color". Callers translate
79
- * their own option to `forceOff: opts.color === false`. */
80
57
  function shouldUseColor({ forceOff = false } = {}) {
81
58
  if (forceOff) return false;
82
59
  if (process.env.NO_COLOR) return false;
@@ -107,26 +84,16 @@ function addStartOptions(cmd) {
107
84
  .option('--log <path>', 'background log file', DEFAULT_LOG_FILE);
108
85
  }
109
86
 
110
- // Browser-open policy:
111
- // - foreground: open by default; --no-open disables.
112
- // - background: never auto-open. Use `ccgauge open` after start.
113
87
  function shouldOpenBrowser(opts) {
114
88
  if (opts.background) return false;
115
89
  return opts.open !== false;
116
90
  }
117
91
 
118
- // For `restart`: when the user did not explicitly pass an option, fall back
119
- // to whatever the previous background run was using.
120
92
  async function inheritFromState(opts, cmd) {
121
93
  const prev = await readState();
122
94
  if (!prev) return { ...opts };
123
95
  const isDefault = (key) => cmd.getOptionValueSource(key) === 'default';
124
- // `--dir` has no default value (see addStartOptions), so commander
125
- // reports its source as `undefined` when unset and `'cli'` when the
126
- // user typed it — including `--dir ""` which means "clear override".
127
- // Using truthy-on-opts.dir would conflate "unset" and "explicit empty"
128
- // and re-inherit the old dataDir, defeating the whole point of
129
- // letting users switch back to the default Claude path on restart.
96
+
130
97
  const isProvided = (key) => cmd.getOptionValueSource(key) === 'cli';
131
98
  const merged = { ...opts };
132
99
  if (isDefault('port') && prev.port) merged.port = String(prev.port);
@@ -229,40 +196,27 @@ addReportOptions(program.command('report').description('print a formatted usage
229
196
 
230
197
  await program.parseAsync(normalizeArgv(process.argv));
231
198
 
232
- /** Implements `ccgauge` (no subcommand) as a shortcut for `ccgauge start`,
233
- * but only when every flag we see belongs to `start`. If the user typed
234
- * something that looks like a `report` / `mcp` flag without the
235
- * subcommand (e.g. `ccgauge -r 7d`), we leave argv alone so commander
236
- * surfaces "unknown option" against the root program — which lists the
237
- * available subcommands. Without this discrimination commander would
238
- * complain about `start: unknown option -r`, which is the wrong hint. */
239
199
  function normalizeArgv(argv) {
240
200
  const args = argv.slice(2);
241
- // `ccgauge` (no args) is documented as a shortcut for `ccgauge start` —
242
- // we deliberately do NOT early-return on `args.length === 0`. The flag
243
- // walk below is a no-op for an empty argv, so we fall through to the
244
- // final `[argv[0], argv[1], 'start']` injection.
201
+
245
202
  if (args.includes('--help') || args.includes('-h') || args.includes('-V') || args.includes('--version')) {
246
203
  return argv;
247
204
  }
248
- // First token is a known subcommand → caller knows what they're doing.
205
+
249
206
  if (args.length > 0 && COMMAND_NAMES.has(args[0])) return argv;
250
- // Walk flags; bail if we see anything `start` doesn't accept.
207
+
251
208
  for (let i = 0; i < args.length; i += 1) {
252
209
  const arg = args[i];
253
210
  if (arg === '--') break;
254
211
  if (!arg.startsWith('-')) {
255
- // Stray positional with no subcommand prefix → let commander error
256
- // out at the root rather than after silently picking `start`.
212
+
257
213
  return argv;
258
214
  }
259
- // Support `--port=3737` style: only the part before `=` is the flag.
215
+
260
216
  const eqIdx = arg.indexOf('=');
261
217
  const flag = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
262
218
  if (!START_OPTIONS.has(flag)) return argv;
263
- // If the option takes a value and the user didn't inline it with `=`,
264
- // the next token is the value — skip it so we don't re-check it as a
265
- // flag.
219
+
266
220
  if (START_OPTIONS.get(flag) === true && eqIdx < 0) i += 1;
267
221
  }
268
222
  return [argv[0], argv[1], 'start', ...args];
@@ -281,13 +235,7 @@ async function start(opts) {
281
235
  async function startForeground(standaloneEntry, opts) {
282
236
  const port = await resolvePort(opts);
283
237
  const env = makeServerEnv(opts, port);
284
- // `spawn` (not `fork`) because Next's standalone server is a plain
285
- // node script — it doesn't use the IPC channel. `fork` would open one
286
- // anyway, and since Next never calls `process.disconnect()`, the
287
- // parent could never exit cleanly without `process.exit()` racing the
288
- // child's shutdown. `spawn` matches the background path and lets us
289
- // hand the child our stdio directly. `--quiet` muffles both streams
290
- // (Next's warnings go to stderr, so muting only stdout misses them).
238
+
291
239
  const stdio = opts.quiet
292
240
  ? ['ignore', 'ignore', 'ignore']
293
241
  : 'inherit';
@@ -309,15 +257,10 @@ async function startForeground(standaloneEntry, opts) {
309
257
  process.exit(1);
310
258
  });
311
259
 
312
- // Forward our signals to the child, then wait for it to exit so its
313
- // teardown is observable rather than racing `process.exit()`. The
314
- // child propagates the exit code back via the `exit` event below.
315
260
  function forward(signal) {
316
261
  return () => {
317
262
  if (!child.killed) child.kill(signal);
318
- // Don't process.exit() here; let `child.on('exit')` decide. If the
319
- // child ignores the signal for some reason, a second Ctrl+C will
320
- // re-enter and the OS eventually escalates.
263
+
321
264
  };
322
265
  }
323
266
  process.on('SIGINT', forward('SIGINT'));
@@ -327,9 +270,7 @@ async function startForeground(standaloneEntry, opts) {
327
270
  });
328
271
 
329
272
  child.on('exit', (code, signal) => {
330
- // Convention: signal-terminated child → 128 + signal number (bash
331
- // standard). Plain numeric exit → forward as-is. `code === null`
332
- // happens when only `signal` is set.
273
+
333
274
  if (typeof code === 'number') process.exit(code);
334
275
  else process.exit(signal ? 128 : 0);
335
276
  });
@@ -355,14 +296,13 @@ async function startBackground(standaloneEntry, opts) {
355
296
  env,
356
297
  detached: true,
357
298
  stdio: ['ignore', out, err],
358
- // Suppress the fleeting console window that Windows pops up for a
359
- // detached background child. No-op on macOS/Linux.
299
+
360
300
  windowsHide: true,
361
301
  });
362
302
  child.unref();
363
- // Once spawn() has dup'd these fds into the child, the parent can release them.
364
- try { closeSync(out); } catch { /* ignore */ }
365
- try { closeSync(err); } catch { /* ignore */ }
303
+
304
+ try { closeSync(out); } catch { }
305
+ try { closeSync(err); } catch { }
366
306
 
367
307
  const url = buildUrl(opts.host, port);
368
308
  try {
@@ -373,10 +313,7 @@ async function startBackground(standaloneEntry, opts) {
373
313
  const exited = await waitForProcessExit(child.pid, 2_000);
374
314
  if (!exited) safeKill(child.pid, 'SIGKILL');
375
315
  }
376
- // The actual reason (EADDRINUSE, bad CCGAUGE_CONFIG_DIR, port taken
377
- // by a sibling that survived getPort's check, etc.) is in the log
378
- // file that the spawned child writes to. Surface the tail so users
379
- // don't have to discover `ccgauge logs` themselves.
316
+
380
317
  const tail = await tailLog(logFile, 5);
381
318
  const tailNote = tail ? `\nLast log lines (${logFile}):\n${tail}\n` : '';
382
319
  throw new Error(`failed to start background service: ${startErr.message}${tailNote}`);
@@ -390,11 +327,7 @@ async function startBackground(standaloneEntry, opts) {
390
327
  logFile,
391
328
  startedAt: new Date().toISOString(),
392
329
  bootId: bootId(),
393
- // The Next.js standalone bundle renames the process via
394
- // `process.title = 'next-server (vX.Y.Z)'` on boot, so `ps` will
395
- // show "next-server" in the command column. Pinning that as the
396
- // identity marker means a recycled PID belonging to some other
397
- // node process won't pass the identity check in isProcessRunning.
330
+
398
331
  cmdMarker: 'next-server',
399
332
  packageRoot,
400
333
  dataDir: opts.dir ? String(opts.dir) : null,
@@ -433,15 +366,7 @@ async function status(opts) {
433
366
  const payload = state
434
367
  ? { running, ...state }
435
368
  : { running: false };
436
- // Exit-code convention split between the two output modes:
437
- // - Plain text → systemd-style 3 when not running, so
438
- // `if ccgauge status; then …` works in shell.
439
- // - `--json` → always 0; the consumer is a script that should read
440
- // `payload.running` from the JSON. Non-zero here would break
441
- // pipelines like `ccgauge status --json | jq` under `set -e`.
442
- // Number inlined (no const) because this function sits below the
443
- // file's top-level `await program.parseAsync(...)` — a const there
444
- // would be in the TDZ when commander invokes the action handler.
369
+
445
370
  if (opts.json) {
446
371
  console.log(JSON.stringify(payload, null, 2));
447
372
  return;
@@ -485,16 +410,6 @@ async function logs(opts) {
485
410
  await followLog(logFile, content.length);
486
411
  }
487
412
 
488
- /** Shared "build artifact missing" template. Used by start / report /
489
- * mcp so all three give the same diagnostic shape and so future
490
- * artifacts only need a one-line registration here. Three install
491
- * sources mean three different remediation hints:
492
- *
493
- * - `npm`: the tarball should already contain this — reinstall is the
494
- * move. We also print `node -v` / `ccgauge -v` so issue reports
495
- * don't lose those.
496
- * - `source`: a `pnpm build` (or the per-artifact target) is missing.
497
- * - `dev`: there's no built artifact in dev mode — point at `pnpm dev`. */
498
413
  function missingArtifactError({ artifactName, expectedPath, buildCmd, devCmd }) {
499
414
  const lines = [
500
415
  '',
@@ -567,11 +482,7 @@ async function report(opts) {
567
482
  console.error(`[ccgauge] error: report failed: ${(err && err.message) || err}`);
568
483
  process.exit(1);
569
484
  }
570
- // The indexer keeps fs watchers alive, which would block process exit.
571
- // For a one-shot report we explicitly exit once stdout is drained.
572
- // Use the write() return value rather than chaining a `drain` listener
573
- // after the fact: if drain fires between the write and the listener
574
- // attach, we'd hang forever waiting for an event that already happened.
485
+
575
486
  const flushed = process.stdout.write(payload);
576
487
  if (flushed) {
577
488
  process.exit(0);
@@ -590,9 +501,6 @@ async function startMcp(opts = {}) {
590
501
  });
591
502
  }
592
503
 
593
- // --check: don't actually run the JSON-RPC server — load the bundle,
594
- // boot the indexer, print one line per provider, and exit. Lets users
595
- // verify their install without wiring up an MCP client.
596
504
  if (opts.check) {
597
505
  const mod = await import(pathToFileURL(bundle).href);
598
506
  if (typeof mod.printCheck !== 'function') {
@@ -603,21 +511,6 @@ async function startMcp(opts = {}) {
603
511
  process.exit(typeof code === 'number' ? code : 0);
604
512
  }
605
513
 
606
- // Run the bundled MCP server **in this process** — the bundle exposes a
607
- // top-level `runStdioServer()` so we just import + invoke it. Spawning a
608
- // second Node process here is wasted memory/latency (LLM clients already
609
- // spawn `ccgauge mcp` per conversation), and forwarding signals across
610
- // processes is brittle (e.g. SIGHUP isn't covered by the old shim).
611
- //
612
- // CRITICAL: `runStdioServer()` resolves as soon as
613
- // `server.connect(transport)` finishes the JSON-RPC handshake setup —
614
- // the long-running stdio listener is what holds the process alive
615
- // afterwards. We must NOT call `process.exit(0)` after the await
616
- // returns, or we'd kill the process before any client `initialize`
617
- // can land. The dist/mcp/server.mjs direct-entry path gets this right
618
- // (entry.ts uses `.catch()` only); the CLI in-process wrapper used to
619
- // exit here too — a regression introduced when we moved off the
620
- // spawn-a-child design. Don't reintroduce.
621
514
  try {
622
515
  const mod = await import(pathToFileURL(bundle).href);
623
516
  if (typeof mod.runStdioServer !== 'function') {
@@ -625,25 +518,13 @@ async function startMcp(opts = {}) {
625
518
  process.exit(1);
626
519
  }
627
520
  await mod.runStdioServer();
628
- // Function returned but didn't process.exit — stdin listener still
629
- // alive. Just let the event loop run; the transport calls
630
- // process.exit(0) when stdin closes (see `shutdown` in
631
- // lib/mcp/server.ts), which is the LLM client disconnecting.
521
+
632
522
  } catch (err) {
633
523
  console.error('[ccgauge-mcp] error: failed to start:', err?.message ?? err);
634
524
  process.exit(1);
635
525
  }
636
526
  }
637
527
 
638
- /** `ccgauge doctor` — print everything we'd ask the user to gather when
639
- * debugging a "why doesn't it work" report, in one place:
640
- * - version / platform
641
- * - env vars that influence ccgauge behaviour
642
- * - on-disk build artifacts (each can be rebuilt independently)
643
- * - background service state (if any)
644
- * - delegated MCP `--check` for indexer + per-provider scan stats
645
- * Output is plain text (no colour) so it's easy to paste into a GitHub
646
- * issue. */
647
528
  async function doctor() {
648
529
  const lines = [];
649
530
  lines.push(`ccgauge: v${pkg.version ?? '?'}`);
@@ -697,12 +578,8 @@ async function doctor() {
697
578
  }
698
579
  lines.push('');
699
580
 
700
- // Print everything we've accumulated so the indexer probe's output
701
- // appears below it (printCheck writes to stdout synchronously).
702
581
  for (const l of lines) console.log(l);
703
582
 
704
- // Delegate to the MCP bundle's printCheck() for indexer / providers
705
- // detail — same info `ccgauge mcp --check` shows, in the same format.
706
583
  const mcpBundle = join(packageRoot, 'dist', 'mcp', 'server.mjs');
707
584
  if (!existsSync(mcpBundle)) {
708
585
  console.log('indexer: (MCP bundle missing; rebuild to run the indexer probe)');
@@ -712,9 +589,7 @@ async function doctor() {
712
589
  const mod = await import(pathToFileURL(mcpBundle).href);
713
590
  if (typeof mod.printCheck === 'function') {
714
591
  const code = await mod.printCheck();
715
- // Doctor exit code mirrors the indexer probe: 0 on success, non-0
716
- // otherwise. Anything that printCheck couldn't decide we treat as
717
- // success — doctor is a diagnostic tool, not a gate.
592
+
718
593
  process.exit(typeof code === 'number' ? code : 0);
719
594
  }
720
595
  process.exit(0);
@@ -729,11 +604,7 @@ async function resolvePort(opts) {
729
604
  if (!Number.isInteger(preferred) || preferred <= 0 || preferred > 65535) {
730
605
  throw new Error(`invalid port: ${opts.port}`);
731
606
  }
732
- // Try the preferred port first, then up to 19 ports above it (capped at
733
- // 65535), then 0 (let the OS pick an ephemeral port). For unusually high
734
- // preferred values (e.g. 65530) the +N candidates are clamped by the
735
- // filter, leaving just the preferred + ephemeral fallback — that's still
736
- // correct, just narrower.
607
+
737
608
  const candidates = opts.strictPort
738
609
  ? preferred
739
610
  : [preferred, ...Array.from({ length: 19 }, (_, i) => preferred + i + 1).filter((p) => p <= 65535), 0];
@@ -758,10 +629,6 @@ function makeServerEnv(opts, port) {
758
629
  return env;
759
630
  }
760
631
 
761
- /** Read the last `lines` non-empty lines of a log file. Used as a hint
762
- * in error messages when the spawned background server fails to come
763
- * up — the actual root cause (port conflict, parse error, etc.) is in
764
- * the log file and we'd rather not make the user go fishing for it. */
765
632
  async function tailLog(logFile, lines = 5) {
766
633
  try {
767
634
  const content = await readFile(logFile, 'utf8');
@@ -795,13 +662,10 @@ async function tryOpen(url) {
795
662
  const openBrowser = await loadOpenBrowser();
796
663
  await openBrowser(url);
797
664
  } catch {
798
- // ignore — user may be on remote without a browser
665
+
799
666
  }
800
667
  }
801
668
 
802
- /** Build a small palette of ANSI escapes that collapse to '' when colour
803
- * is off, so the same template literal works for both modes without an
804
- * if/else per line. */
805
669
  function ansiPalette(useColor) {
806
670
  const seq = (...codes) => (useColor ? `\x1b[${codes.join(';')}m` : '');
807
671
  return {
@@ -852,12 +716,9 @@ async function readState() {
852
716
  const raw = await readFile(STATE_FILE, 'utf8');
853
717
  const parsed = JSON.parse(raw);
854
718
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
855
- // Treat unknown / future versions as stale (auto-clean on next stop/start).
719
+
856
720
  if (parsed.version !== STATE_VERSION) return null;
857
- // Type-guard the fields callers actually use — `stop`, `status`, and
858
- // `restart` all assume these have the right shape. A hand-edited
859
- // state.json with the right `version` but garbage in `pid` could
860
- // otherwise crash `safeKill()` or `isProcessRunning()`.
721
+
861
722
  if (typeof parsed.pid !== 'number' || !Number.isInteger(parsed.pid) || parsed.pid <= 0) {
862
723
  return null;
863
724
  }
@@ -879,22 +740,10 @@ async function removeState() {
879
740
  await rm(STATE_FILE, { force: true });
880
741
  }
881
742
 
882
- /** Best-effort approximation of system boot time, in ms since the epoch.
883
- * Used as a `bootId` on persisted state so we can reject stale PIDs
884
- * after a reboot (PID space gets recycled and would otherwise let us
885
- * SIGTERM an unrelated process that happens to inherit the old PID). */
886
743
  function bootId() {
887
744
  return Math.floor(Date.now() - os.uptime() * 1000);
888
745
  }
889
746
 
890
- /** Optional identity check on top of the existing `process.kill(pid, 0)`.
891
- * Pass `state` to additionally verify:
892
- * 1. We're still on the same boot (uptime hasn't reset).
893
- * 2. `ps -p <pid> -o command=` mentions `state.cmdMarker` — i.e. that
894
- * pid actually points at *our* dashboard child and not some other
895
- * process that re-inherited the PID.
896
- * Failures of (2) are tolerated (`ps` missing, sandboxed exec, etc.)
897
- * so we don't false-negative on minimal containers. */
898
747
  function isProcessRunning(pid, state) {
899
748
  if (!pid) return false;
900
749
  try {
@@ -903,8 +752,7 @@ function isProcessRunning(pid, state) {
903
752
  return false;
904
753
  }
905
754
  if (!state) return true;
906
- // Reboot detection: bootId drifts by tens of ms across reads on the
907
- // same boot (os.uptime() has float jitter), so use a generous window.
755
+
908
756
  if (typeof state.bootId === 'number') {
909
757
  if (Math.abs(bootId() - state.bootId) > 60_000) return false;
910
758
  }
@@ -917,12 +765,7 @@ function isProcessRunning(pid, state) {
917
765
  });
918
766
  if (!out.includes(state.cmdMarker)) return false;
919
767
  } catch {
920
- // `ps` not available or PID gone between kill(0) and exec — fall
921
- // back to the kill(0) result we already have. We deliberately do
922
- // NOT fail closed here: a missing `ps` is more common than a PID
923
- // collision (PID reuse requires a reboot or wraparound), so the
924
- // false-negative cost of "treat as not-running" outweighs the
925
- // false-positive of "treat as running".
768
+
926
769
  }
927
770
  }
928
771
  return true;
@@ -954,12 +797,9 @@ async function followLog(logFile, offset) {
954
797
  busy = true;
955
798
  try {
956
799
  const s = await stat(logFile);
957
- if (s.size < cursor) cursor = 0; // log was truncated / rotated
800
+ if (s.size < cursor) cursor = 0;
958
801
  if (s.size === cursor) return;
959
- // Pin the upper bound to the size we saw in stat() — otherwise a
960
- // chunk written *after* stat returns would slip into this read,
961
- // and the next tick (which starts at `s.size`) would replay it.
962
- // `end` is inclusive, hence the -1.
802
+
963
803
  const endAt = s.size - 1;
964
804
  await new Promise((res, rej) => {
965
805
  const stream = createReadStream(logFile, {
@@ -975,7 +815,7 @@ async function followLog(logFile, offset) {
975
815
  stream.on('error', rej);
976
816
  });
977
817
  } catch {
978
- // keep following unless interrupted
818
+
979
819
  } finally {
980
820
  busy = false;
981
821
  }