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
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
63
|
+
function hubPrefix() {
|
|
64
64
|
return dim('[specrails-hub]');
|
|
65
65
|
}
|
|
66
|
-
function
|
|
67
|
-
process.stdout.write(`${
|
|
66
|
+
function hubLog(msg) {
|
|
67
|
+
process.stdout.write(`${hubPrefix()} ${msg}\n`);
|
|
68
68
|
}
|
|
69
|
-
function
|
|
70
|
-
process.stderr.write(`${
|
|
69
|
+
function hubError(msg) {
|
|
70
|
+
process.stderr.write(`${hubPrefix()} ${red(`error: ${msg}`)}\n`);
|
|
71
71
|
}
|
|
72
|
-
function
|
|
73
|
-
process.stderr.write(`${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
hubError('failed to connect to manager');
|
|
282
282
|
return 1;
|
|
283
283
|
}
|
|
284
284
|
if (spawnRes.status === 409) {
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
+
hubError('claude binary not found');
|
|
432
432
|
}
|
|
433
433
|
else {
|
|
434
|
-
|
|
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
|
-
|
|
471
|
+
hubError('claude binary not found');
|
|
472
472
|
}
|
|
473
473
|
else {
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
+
hubError('failed to fetch job list');
|
|
560
560
|
return 1;
|
|
561
561
|
}
|
|
562
562
|
if (res.status === 501 || res.status === 404) {
|
|
563
|
-
|
|
563
|
+
hubLog('jobs history requires manager with SQLite persistence (#57)');
|
|
564
564
|
return 1;
|
|
565
565
|
}
|
|
566
566
|
if (res.status !== 200) {
|
|
567
|
-
|
|
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
|
-
|
|
575
|
+
hubError('invalid response from /api/jobs');
|
|
576
576
|
return 1;
|
|
577
577
|
}
|
|
578
578
|
if (!data.jobs || data.jobs.length === 0) {
|
|
579
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
//
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
677
|
+
hubLog('hub is not running (no pid file)');
|
|
673
678
|
return 0;
|
|
674
679
|
}
|
|
675
680
|
if (!isProcessRunning(pid)) {
|
|
676
|
-
|
|
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
|
-
|
|
690
|
+
hubLog(`hub stopped (pid ${pid})`);
|
|
686
691
|
return 0;
|
|
687
692
|
}
|
|
688
693
|
catch (err) {
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
734
|
+
hubLog(`added project: ${data.project?.name ?? projectPath}`);
|
|
730
735
|
return 0;
|
|
731
736
|
}
|
|
732
737
|
else if (res.status === 409) {
|
|
733
|
-
|
|
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
|
-
|
|
747
|
+
hubError(`failed to add project: ${errMsg}`);
|
|
743
748
|
return 1;
|
|
744
749
|
}
|
|
745
750
|
}
|
|
746
751
|
catch (err) {
|
|
747
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
+
hubLog(`project removed`);
|
|
776
781
|
return 0;
|
|
777
782
|
}
|
|
778
783
|
else {
|
|
779
|
-
|
|
784
|
+
hubError(`failed to remove project: HTTP ${deleteRes.status}`);
|
|
780
785
|
return 1;
|
|
781
786
|
}
|
|
782
787
|
}
|
|
783
788
|
catch (err) {
|
|
784
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
891
|
+
hubLog(`running: ${command}`);
|
|
887
892
|
const detection = await detectWebManager(port);
|
|
888
893
|
let exitCode;
|
|
889
894
|
if (detection.running) {
|
|
890
|
-
|
|
895
|
+
hubLog(`routing via manager at ${detection.baseUrl}`);
|
|
891
896
|
exitCode = await runViaWebManager(command, detection.baseUrl);
|
|
892
897
|
}
|
|
893
898
|
else {
|
|
894
|
-
|
|
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
|
-
|
|
907
|
+
hubError(err.message ?? String(err));
|
|
903
908
|
process.exit(1);
|
|
904
909
|
});
|
|
905
910
|
}
|