great-cto 2.28.0 → 2.30.0

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 (2) hide show
  1. package/dist/main.js +125 -14
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -20,9 +20,10 @@ import { installAllCompanions } from "./companion.js";
20
20
  import { bootstrap } from "./bootstrap.js";
21
21
  import { compileFlow } from "./flow.js";
22
22
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
23
- import { readFileSync, copyFileSync, chmodSync, existsSync as fsExistsSync } from "node:fs";
23
+ import { readFileSync, writeFileSync, copyFileSync, chmodSync, mkdirSync, readdirSync, unlinkSync, existsSync as fsExistsSync } from "node:fs";
24
24
  import { dirname, join } from "node:path";
25
25
  import { fileURLToPath } from "node:url";
26
+ import { homedir } from "node:os";
26
27
  function getCliVersion() {
27
28
  try {
28
29
  const here = dirname(fileURLToPath(import.meta.url));
@@ -309,23 +310,24 @@ async function runRegister(args) {
309
310
  log(` → will appear in great-cto board project switcher`);
310
311
  return 0;
311
312
  }
312
- async function runBoard(args) {
313
- const { join, dirname } = await import("node:path");
314
- const { fileURLToPath } = await import("node:url");
315
- const { existsSync } = await import("node:fs");
316
- const { spawn } = await import("node:child_process");
317
- // Find board server: relative to this file (dist/) → packages/board/server.mjs
318
- const { homedir } = await import("node:os");
319
- const { readdirSync } = await import("node:fs");
313
+ // ── Board server lifecycle helpers ───────────────────────────────────────────
314
+ /** Absolute path to the board PID file (persists across CLI invocations). */
315
+ function boardPidFilePath() {
316
+ return join(homedir(), ".great_cto", "board.pid");
317
+ }
318
+ /**
319
+ * Locate the board server.mjs — checks dev layouts then plugin cache versions
320
+ * in descending order, returning the first path that exists.
321
+ */
322
+ function findBoardServerPath() {
320
323
  const here = dirname(fileURLToPath(import.meta.url));
321
324
  const candidates = [
322
- join(here, "..", "..", "board", "server.mjs"), // from packages/cli/dist (dev)
325
+ join(here, "..", "..", "board", "server.mjs"), // packages/cli/dist (dev)
323
326
  join(here, "..", "board", "server.mjs"), // alt dev layout
324
327
  join(here, "board", "server.mjs"), // flat layout
325
328
  ];
326
- // Also search plugin cache (installed via npx great-cto)
327
329
  const pluginBase = join(homedir(), ".claude", "plugins", "cache", "local", "great_cto");
328
- if (existsSync(pluginBase)) {
330
+ if (fsExistsSync(pluginBase)) {
329
331
  try {
330
332
  const versions = readdirSync(pluginBase).filter(v => /^\d/.test(v)).sort().reverse();
331
333
  for (const v of versions.slice(0, 5)) {
@@ -334,7 +336,99 @@ async function runBoard(args) {
334
336
  }
335
337
  catch { /* ignore */ }
336
338
  }
337
- const serverPath = candidates.find(existsSync);
339
+ return candidates.find(fsExistsSync);
340
+ }
341
+ /**
342
+ * Kill the board server recorded in the PID file.
343
+ * Returns true if a live process was terminated.
344
+ */
345
+ async function killExistingBoard() {
346
+ const pidFile = boardPidFilePath();
347
+ if (!fsExistsSync(pidFile))
348
+ return false;
349
+ const raw = readFileSync(pidFile, "utf8").trim();
350
+ const pid = parseInt(raw, 10);
351
+ if (!pid || isNaN(pid)) {
352
+ try {
353
+ unlinkSync(pidFile);
354
+ }
355
+ catch { /* ignore */ }
356
+ return false;
357
+ }
358
+ try {
359
+ process.kill(pid, "SIGTERM");
360
+ await new Promise(r => setTimeout(r, 400));
361
+ try {
362
+ process.kill(pid, "SIGKILL");
363
+ }
364
+ catch { /* already dead — fine */ }
365
+ try {
366
+ unlinkSync(pidFile);
367
+ }
368
+ catch { /* ignore */ }
369
+ return true;
370
+ }
371
+ catch {
372
+ // process was already gone
373
+ try {
374
+ unlinkSync(pidFile);
375
+ }
376
+ catch { /* ignore */ }
377
+ return false;
378
+ }
379
+ }
380
+ /**
381
+ * If the board server is running (PID file present + process alive), kill it
382
+ * and relaunch with the latest installed version in the background.
383
+ * Called by runInit() after a new plugin version is installed.
384
+ */
385
+ async function restartBoardAfterUpgrade(port) {
386
+ const { spawn } = await import("node:child_process");
387
+ const pidFile = boardPidFilePath();
388
+ if (!fsExistsSync(pidFile))
389
+ return; // board wasn't running — nothing to do
390
+ const raw = readFileSync(pidFile, "utf8").trim();
391
+ const oldPid = parseInt(raw, 10);
392
+ if (!oldPid || isNaN(oldPid))
393
+ return;
394
+ // Check if the process is actually alive (signal 0 = existence check)
395
+ try {
396
+ process.kill(oldPid, 0);
397
+ }
398
+ catch {
399
+ return;
400
+ }
401
+ log(` ${dim(`↺ board server (pid ${oldPid}) running with old version — restarting…`)}`);
402
+ await killExistingBoard();
403
+ const serverPath = findBoardServerPath();
404
+ if (!serverPath) {
405
+ warn("board server not found after upgrade — start it manually with: great-cto board");
406
+ return;
407
+ }
408
+ // Launch detached so it outlives this init process; --no-open because the
409
+ // browser tab is already open pointing at the same port.
410
+ const child = spawn(process.execPath, [serverPath, "--no-open"], {
411
+ env: { ...process.env, BOARD_PORT: String(port) },
412
+ stdio: "ignore",
413
+ detached: true,
414
+ });
415
+ child.unref();
416
+ try {
417
+ mkdirSync(join(homedir(), ".great_cto"), { recursive: true });
418
+ writeFileSync(pidFile, String(child.pid));
419
+ }
420
+ catch { /* ignore */ }
421
+ log(` ${green("✓")} board restarted → http://localhost:${port}`);
422
+ }
423
+ // ─────────────────────────────────────────────────────────────────────────────
424
+ async function runBoard(args) {
425
+ const { spawn } = await import("node:child_process");
426
+ // Stop any existing board server (e.g. old version left over after upgrade)
427
+ const killed = await killExistingBoard();
428
+ if (killed) {
429
+ log(` ${dim("stopped previous board server")}`);
430
+ }
431
+ const serverPath = findBoardServerPath();
338
432
  if (!serverPath) {
339
433
  error("Board server not found. Try reinstalling: npx great-cto@latest");
340
434
  return 1;
@@ -347,7 +441,19 @@ async function runBoard(args) {
347
441
  stdio: "inherit",
348
442
  detached: false,
349
443
  });
350
- child.on("exit", code => process.exit(code ?? 0));
444
+ // Write PID so future invocations (including init upgrades) can find us
445
+ try {
446
+ mkdirSync(join(homedir(), ".great_cto"), { recursive: true });
447
+ writeFileSync(boardPidFilePath(), String(child.pid));
448
+ }
449
+ catch { /* best-effort */ }
450
+ child.on("exit", code => {
451
+ try {
452
+ unlinkSync(boardPidFilePath());
453
+ }
454
+ catch { /* ignore */ }
455
+ process.exit(code ?? 0);
456
+ });
351
457
  return 0;
352
458
  }
353
459
  function printHelp() {
@@ -624,6 +730,11 @@ async function runInit(args) {
624
730
  log(` ${dim("version")} ${installResult.version} ${dim("already installed at")} ${installResult.pluginDir}`);
625
731
  log(` ${dim("(use --force to reinstall)")}`);
626
732
  }
733
+ else {
734
+ // New version installed — restart board server if it was running so it
735
+ // picks up the updated server.mjs immediately (no manual restart needed).
736
+ await restartBoardAfterUpgrade(args.boardPort);
737
+ }
627
738
  // ── 4. enable in settings ────────────────────────────────
628
739
  step(4, 5, "enabling plugin in ~/.claude/settings.json");
629
740
  const enableResult = enableGreatCto();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.28.0",
3
+ "version": "2.30.0",
4
4
  "description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
5
5
  "keywords": [
6
6
  "claude-code",