specrails-hub 0.1.4 → 0.1.6

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.
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * srm — specrails CLI bridge
4
+ * specrails-hub — specrails CLI bridge
5
5
  *
6
6
  * Routes commands to the manager when running, or falls back to invoking
7
7
  * claude directly when the manager is not reachable.
8
8
  *
9
9
  * Usage:
10
- * srm implement #42 → /sr:implement #42 (via manager or direct)
11
- * srm "any raw prompt" → raw prompt (no /sr: prefix)
12
- * srm --status → print manager state
13
- * srm --jobs → print job history table
14
- * srm --port 5000 <command> → use port 5000 instead of 4200
15
- * srm --help → print usage and exit 0
10
+ * specrails-hub implement #42 → /sr:implement #42 (via manager or direct)
11
+ * specrails-hub "any raw prompt" → raw prompt (no /sr: prefix)
12
+ * specrails-hub --status → print manager state
13
+ * specrails-hub --jobs → print job history table
14
+ * specrails-hub --port 5000 <command> → use port 5000 instead of 4200
15
+ * specrails-hub --help → print usage and exit 0
16
16
  */
17
17
  var __importDefault = (this && this.__importDefault) || function (mod) {
18
18
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -60,17 +60,17 @@ const dim = (t) => ansi('2', t);
60
60
  const red = (t) => ansi('31', t);
61
61
  const bold = (t) => ansi('1', t);
62
62
  const dimCyan = (t) => ansi('2;36', t);
63
- function srmPrefix() {
63
+ function hubPrefix() {
64
64
  return dim('[specrails-hub]');
65
65
  }
66
- function srmLog(msg) {
67
- process.stdout.write(`${srmPrefix()} ${msg}\n`);
66
+ function hubLog(msg) {
67
+ process.stdout.write(`${hubPrefix()} ${msg}\n`);
68
68
  }
69
- function srmError(msg) {
70
- process.stderr.write(`${srmPrefix()} ${red(`error: ${msg}`)}\n`);
69
+ function hubError(msg) {
70
+ process.stderr.write(`${hubPrefix()} ${red(`error: ${msg}`)}\n`);
71
71
  }
72
- function srmWarn(msg) {
73
- process.stderr.write(`${srmPrefix()} ${dim(`warning: ${msg}`)}\n`);
72
+ function hubWarn(msg) {
73
+ process.stderr.write(`${hubPrefix()} ${dim(`warning: ${msg}`)}\n`);
74
74
  }
75
75
  const HUB_SUBCOMMANDS = new Set(['start', 'stop', 'status', 'add', 'remove', 'list']);
76
76
  function parseArgs(argv) {
@@ -260,13 +260,13 @@ async function runViaWebManager(command, baseUrl) {
260
260
  // Hub mode: resolve project from CWD
261
261
  const project = await resolveProjectFromCwd(baseUrl);
262
262
  if (!project) {
263
- srmError('hub is running but no project registered for the current directory.\n' +
263
+ hubError('hub is running but no project registered for the current directory.\n' +
264
264
  ` Run: specrails-hub hub add ${process.cwd()}`);
265
265
  return 1;
266
266
  }
267
267
  spawnUrl = `${baseUrl}/api/projects/${project.id}/spawn`;
268
268
  jobApiBase = `${baseUrl}/api/projects/${project.id}`;
269
- srmLog(`project: ${project.name}`);
269
+ hubLog(`project: ${project.name}`);
270
270
  }
271
271
  }
272
272
  catch {
@@ -278,11 +278,11 @@ async function runViaWebManager(command, baseUrl) {
278
278
  spawnRes = await httpPost(spawnUrl, { command });
279
279
  }
280
280
  catch (err) {
281
- srmError('failed to connect to manager');
281
+ hubError('failed to connect to manager');
282
282
  return 1;
283
283
  }
284
284
  if (spawnRes.status === 409) {
285
- srmError('manager is busy (another job is running)');
285
+ hubError('manager is busy (another job is running)');
286
286
  return 1;
287
287
  }
288
288
  if (spawnRes.status >= 400) {
@@ -293,7 +293,7 @@ async function runViaWebManager(command, baseUrl) {
293
293
  errMsg = parsed.error;
294
294
  }
295
295
  catch { /* use default */ }
296
- srmError(errMsg);
296
+ hubError(errMsg);
297
297
  return 1;
298
298
  }
299
299
  let processId;
@@ -305,7 +305,7 @@ async function runViaWebManager(command, baseUrl) {
305
305
  throw new Error('missing jobId');
306
306
  }
307
307
  catch {
308
- srmError('invalid response from /api/spawn');
308
+ hubError('invalid response from /api/spawn');
309
309
  return 1;
310
310
  }
311
311
  const startTime = Date.now();
@@ -368,14 +368,14 @@ async function runViaWebManager(command, baseUrl) {
368
368
  }
369
369
  ws.on('close', () => {
370
370
  if (!resolved) {
371
- srmWarn('lost connection to manager');
371
+ hubWarn('lost connection to manager');
372
372
  resolved = true;
373
373
  resolve();
374
374
  }
375
375
  });
376
376
  ws.on('error', (err) => {
377
377
  if (!resolved) {
378
- srmWarn(`WebSocket error: ${err.message}`);
378
+ hubWarn(`WebSocket error: ${err.message}`);
379
379
  resolved = true;
380
380
  resolve();
381
381
  }
@@ -428,10 +428,10 @@ async function runDirect(command) {
428
428
  catch (err) {
429
429
  const code = err.code;
430
430
  if (code === 'ENOENT') {
431
- srmError('claude binary not found');
431
+ hubError('claude binary not found');
432
432
  }
433
433
  else {
434
- srmError(`failed to spawn claude: ${err.message}`);
434
+ hubError(`failed to spawn claude: ${err.message}`);
435
435
  }
436
436
  return 1;
437
437
  }
@@ -468,10 +468,10 @@ async function runDirect(command) {
468
468
  });
469
469
  child.on('error', (err) => {
470
470
  if (err.code === 'ENOENT') {
471
- srmError('claude binary not found');
471
+ hubError('claude binary not found');
472
472
  }
473
473
  else {
474
- srmError(`claude process error: ${err.message}`);
474
+ hubError(`claude process error: ${err.message}`);
475
475
  }
476
476
  resolve(1);
477
477
  });
@@ -548,7 +548,7 @@ async function handleJobs(port) {
548
548
  const baseUrl = `http://127.0.0.1:${port}`;
549
549
  const detection = await detectWebManager(port);
550
550
  if (!detection.running) {
551
- srmError(`manager is not running (${baseUrl})`);
551
+ hubError(`manager is not running (${baseUrl})`);
552
552
  return 1;
553
553
  }
554
554
  let res;
@@ -556,15 +556,15 @@ async function handleJobs(port) {
556
556
  res = await httpGet(`${baseUrl}/api/jobs`);
557
557
  }
558
558
  catch {
559
- srmError('failed to fetch job list');
559
+ hubError('failed to fetch job list');
560
560
  return 1;
561
561
  }
562
562
  if (res.status === 501 || res.status === 404) {
563
- srmLog('jobs history requires manager with SQLite persistence (#57)');
563
+ hubLog('jobs history requires manager with SQLite persistence (#57)');
564
564
  return 1;
565
565
  }
566
566
  if (res.status !== 200) {
567
- srmError(`unexpected response from /api/jobs: HTTP ${res.status}`);
567
+ hubError(`unexpected response from /api/jobs: HTTP ${res.status}`);
568
568
  return 1;
569
569
  }
570
570
  let data;
@@ -572,11 +572,11 @@ async function handleJobs(port) {
572
572
  data = JSON.parse(res.body);
573
573
  }
574
574
  catch {
575
- srmError('invalid response from /api/jobs');
575
+ hubError('invalid response from /api/jobs');
576
576
  return 1;
577
577
  }
578
578
  if (!data.jobs || data.jobs.length === 0) {
579
- srmLog('no jobs recorded yet');
579
+ hubLog('no jobs recorded yet');
580
580
  return 0;
581
581
  }
582
582
  // Column widths
@@ -627,7 +627,7 @@ function isProcessRunning(pid) {
627
627
  }
628
628
  }
629
629
  function hubServerPath() {
630
- // cli/dist/srm.js → __dirname is cli/dist/ → go up two levels to package root
630
+ // cli/dist/hub.js → __dirname is cli/dist/ → go up two levels to package root
631
631
  const base = path_1.default.resolve(__dirname, '..', '..');
632
632
  // npm install: compiled server at dist/server/index.js
633
633
  const compiled = path_1.default.join(base, 'dist', 'server', 'index.js');
@@ -642,7 +642,7 @@ function hubServerPath() {
642
642
  async function hubStart(port) {
643
643
  const pid = readPid();
644
644
  if (pid !== null && isProcessRunning(pid)) {
645
- srmLog(`hub already running (pid ${pid}) on port ${port}`);
645
+ hubLog(`hub already running (pid ${pid}) on port ${port}`);
646
646
  return 0;
647
647
  }
648
648
  const serverPath = hubServerPath();
@@ -656,24 +656,29 @@ async function hubStart(port) {
656
656
  env: { ...process.env },
657
657
  });
658
658
  child.unref();
659
- // Wait briefly and confirm it started
660
- await new Promise((resolve) => setTimeout(resolve, 800));
661
- const detection = await detectWebManager(port);
662
- if (detection.running) {
663
- srmLog(`hub started on http://127.0.0.1:${port}`);
664
- return 0;
659
+ // Poll until the hub is ready (up to 15 seconds)
660
+ const deadline = Date.now() + 15_000;
661
+ const pollInterval = 500;
662
+ await new Promise((resolve) => setTimeout(resolve, 500));
663
+ while (Date.now() < deadline) {
664
+ const detection = await detectWebManager(port);
665
+ if (detection.running) {
666
+ hubLog(`hub started on http://127.0.0.1:${port}`);
667
+ return 0;
668
+ }
669
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
665
670
  }
666
- srmError('hub failed to start (check logs)');
671
+ hubError('hub failed to start (check logs)');
667
672
  return 1;
668
673
  }
669
674
  async function hubStop() {
670
675
  const pid = readPid();
671
676
  if (pid === null) {
672
- srmLog('hub is not running (no pid file)');
677
+ hubLog('hub is not running (no pid file)');
673
678
  return 0;
674
679
  }
675
680
  if (!isProcessRunning(pid)) {
676
- srmLog('hub is not running (stale pid file)');
681
+ hubLog('hub is not running (stale pid file)');
677
682
  try {
678
683
  fs_1.default.unlinkSync(HUB_PID_FILE);
679
684
  }
@@ -682,11 +687,11 @@ async function hubStop() {
682
687
  }
683
688
  try {
684
689
  process.kill(pid, 'SIGTERM');
685
- srmLog(`hub stopped (pid ${pid})`);
690
+ hubLog(`hub stopped (pid ${pid})`);
686
691
  return 0;
687
692
  }
688
693
  catch (err) {
689
- srmError(`failed to stop hub: ${err.message}`);
694
+ hubError(`failed to stop hub: ${err.message}`);
690
695
  return 1;
691
696
  }
692
697
  }
@@ -717,7 +722,7 @@ async function hubStatus(port) {
717
722
  async function hubAdd(projectPath, port) {
718
723
  const detection = await detectWebManager(port);
719
724
  if (!detection.running) {
720
- srmError('hub is not running. Start it first with: specrails-hub start');
725
+ hubError('hub is not running. Start it first with: specrails-hub start');
721
726
  return 1;
722
727
  }
723
728
  try {
@@ -726,11 +731,11 @@ async function hubAdd(projectPath, port) {
726
731
  });
727
732
  if (res.status === 201) {
728
733
  const data = JSON.parse(res.body);
729
- srmLog(`added project: ${data.project?.name ?? projectPath}`);
734
+ hubLog(`added project: ${data.project?.name ?? projectPath}`);
730
735
  return 0;
731
736
  }
732
737
  else if (res.status === 409) {
733
- srmLog('project already registered');
738
+ hubLog('project already registered');
734
739
  return 0;
735
740
  }
736
741
  else {
@@ -739,19 +744,19 @@ async function hubAdd(projectPath, port) {
739
744
  errMsg = JSON.parse(res.body).error ?? errMsg;
740
745
  }
741
746
  catch { /* use default */ }
742
- srmError(`failed to add project: ${errMsg}`);
747
+ hubError(`failed to add project: ${errMsg}`);
743
748
  return 1;
744
749
  }
745
750
  }
746
751
  catch (err) {
747
- srmError(`failed to connect to hub: ${err.message}`);
752
+ hubError(`failed to connect to hub: ${err.message}`);
748
753
  return 1;
749
754
  }
750
755
  }
751
756
  async function hubRemove(projectId, port) {
752
757
  const detection = await detectWebManager(port);
753
758
  if (!detection.running) {
754
- srmError('hub is not running');
759
+ hubError('hub is not running');
755
760
  return 1;
756
761
  }
757
762
  try {
@@ -772,30 +777,30 @@ async function hubRemove(projectId, port) {
772
777
  req.end();
773
778
  });
774
779
  if (deleteRes.status === 200) {
775
- srmLog(`project removed`);
780
+ hubLog(`project removed`);
776
781
  return 0;
777
782
  }
778
783
  else {
779
- srmError(`failed to remove project: HTTP ${deleteRes.status}`);
784
+ hubError(`failed to remove project: HTTP ${deleteRes.status}`);
780
785
  return 1;
781
786
  }
782
787
  }
783
788
  catch (err) {
784
- srmError(`failed to connect to hub: ${err.message}`);
789
+ hubError(`failed to connect to hub: ${err.message}`);
785
790
  return 1;
786
791
  }
787
792
  }
788
793
  async function hubList(port) {
789
794
  const detection = await detectWebManager(port);
790
795
  if (!detection.running) {
791
- srmError('hub is not running');
796
+ hubError('hub is not running');
792
797
  return 1;
793
798
  }
794
799
  try {
795
800
  const res = await httpGet(`${detection.baseUrl}/api/hub/projects`);
796
801
  const data = JSON.parse(res.body);
797
802
  if (!data.projects || data.projects.length === 0) {
798
- srmLog('no projects registered');
803
+ hubLog('no projects registered');
799
804
  return 0;
800
805
  }
801
806
  const idW = 36;
@@ -807,7 +812,7 @@ async function hubList(port) {
807
812
  return 0;
808
813
  }
809
814
  catch (err) {
810
- srmError(`failed to fetch projects: ${err.message}`);
815
+ hubError(`failed to fetch projects: ${err.message}`);
811
816
  return 1;
812
817
  }
813
818
  }
@@ -839,7 +844,7 @@ ${bold('Usage:')}
839
844
  if (sub === 'add') {
840
845
  const projectPath = subArgs[1];
841
846
  if (!projectPath) {
842
- srmError('usage: specrails-hub add <path>');
847
+ hubError('usage: specrails-hub add <path>');
843
848
  return 1;
844
849
  }
845
850
  return hubAdd(projectPath, port);
@@ -847,7 +852,7 @@ ${bold('Usage:')}
847
852
  if (sub === 'remove') {
848
853
  const projectId = subArgs[1];
849
854
  if (!projectId) {
850
- srmError('usage: specrails-hub remove <id>');
855
+ hubError('usage: specrails-hub remove <id>');
851
856
  return 1;
852
857
  }
853
858
  return hubRemove(projectId, port);
@@ -855,7 +860,7 @@ ${bold('Usage:')}
855
860
  if (sub === 'list') {
856
861
  return hubList(port);
857
862
  }
858
- srmError(`unknown hub subcommand: ${sub}`);
863
+ hubError(`unknown hub subcommand: ${sub}`);
859
864
  return 1;
860
865
  }
861
866
  // ---------------------------------------------------------------------------
@@ -883,15 +888,15 @@ async function main() {
883
888
  // Command or raw: resolve command string
884
889
  const command = parsed.resolved;
885
890
  const port = parsed.port;
886
- srmLog(`running: ${command}`);
891
+ hubLog(`running: ${command}`);
887
892
  const detection = await detectWebManager(port);
888
893
  let exitCode;
889
894
  if (detection.running) {
890
- srmLog(`routing via manager at ${detection.baseUrl}`);
895
+ hubLog(`routing via manager at ${detection.baseUrl}`);
891
896
  exitCode = await runViaWebManager(command, detection.baseUrl);
892
897
  }
893
898
  else {
894
- srmLog('manager not running — invoking claude directly');
899
+ hubLog('manager not running — invoking claude directly');
895
900
  exitCode = await runDirect(command);
896
901
  }
897
902
  process.exit(exitCode);
@@ -899,7 +904,7 @@ async function main() {
899
904
  // Only run main() when this file is executed directly (not when imported in tests)
900
905
  if (require.main === module) {
901
906
  main().catch((err) => {
902
- srmError(err.message ?? String(err));
907
+ hubError(err.message ?? String(err));
903
908
  process.exit(1);
904
909
  });
905
910
  }