claude-code-hub 1.34.0 → 1.36.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.
- package/features/voice/feature.json +1 -1
- package/lib/keymap.js +1 -1
- package/lib/onboarding.js +59 -0
- package/lib/settings.js +8 -1
- package/lib/tray.js +37 -17
- package/package.json +5 -5
- package/packages/claude-hub-sdk/src/client.js +19 -2
- package/packages/claude-hub-sdk/src/keys.js +52 -9
- package/packages/claude-hub-sdk/src/stub.js +1 -0
- package/plugin/plugins/claude-code-hub/skills/hub-builder/references/keys.md +4 -2
- package/public/app.js +689 -22
- package/public/index.html +156 -2
- package/server.js +135 -4
package/public/index.html
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
--text: #e8e8ef;
|
|
26
26
|
--text-dim: #8a8fa3;
|
|
27
27
|
--warn: #f5a524;
|
|
28
|
+
--ok: #3fb950;
|
|
28
29
|
--shadow: rgba(0, 0, 0, 0.55);
|
|
29
30
|
--ghost-mix: 22%;
|
|
30
31
|
}
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
--text: #18181b;
|
|
42
43
|
--text-dim: #71717a;
|
|
43
44
|
--warn: #8a4b00;
|
|
45
|
+
--ok: #1a7f37;
|
|
44
46
|
--shadow: rgba(0, 0, 0, 0.18);
|
|
45
47
|
--ghost-mix: 10%;
|
|
46
48
|
}
|
|
@@ -413,6 +415,28 @@
|
|
|
413
415
|
.settings-bar h3 { font-size: 14px; font-weight: 600; }
|
|
414
416
|
.settings-bar kbd { margin-left: auto; }
|
|
415
417
|
|
|
418
|
+
#settings .palette-box { width: min(820px, 94vw); }
|
|
419
|
+
#settings-body:has(> .set-nav) { display: grid; grid-template-columns: 150px 1fr; min-height: 360px; }
|
|
420
|
+
|
|
421
|
+
.set-nav {
|
|
422
|
+
display: flex;
|
|
423
|
+
flex-direction: column;
|
|
424
|
+
gap: 2px;
|
|
425
|
+
padding: 8px 6px;
|
|
426
|
+
border-right: 1px solid var(--border);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.settings-box .set-nav button {
|
|
430
|
+
border: none;
|
|
431
|
+
border-radius: 6px;
|
|
432
|
+
background: none;
|
|
433
|
+
padding: 6px 10px;
|
|
434
|
+
text-align: left;
|
|
435
|
+
color: var(--text-dim);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.set-pane { min-width: 0; }
|
|
439
|
+
|
|
416
440
|
.set-sec { padding: 6px 0; border-bottom: 1px solid var(--border); }
|
|
417
441
|
.set-sec:last-child { border-bottom: none; }
|
|
418
442
|
|
|
@@ -450,6 +474,103 @@
|
|
|
450
474
|
cursor: pointer;
|
|
451
475
|
}
|
|
452
476
|
|
|
477
|
+
#setup { padding-top: 6vh; }
|
|
478
|
+
|
|
479
|
+
.setup-box {
|
|
480
|
+
width: min(860px, 94vw);
|
|
481
|
+
max-height: 88vh;
|
|
482
|
+
border-radius: 14px;
|
|
483
|
+
box-shadow: 0 28px 80px var(--shadow);
|
|
484
|
+
font-size: 14px;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.setup-hero {
|
|
488
|
+
display: flex;
|
|
489
|
+
align-items: center;
|
|
490
|
+
gap: 18px;
|
|
491
|
+
padding: 28px 32px 22px;
|
|
492
|
+
border-bottom: 1px solid var(--border);
|
|
493
|
+
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 14%, transparent), transparent 70%);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.setup-hero img { width: 56px; height: 56px; flex: none; }
|
|
497
|
+
.setup-hero h3 { font-size: 22px; font-weight: 700; letter-spacing: -0.01em; }
|
|
498
|
+
.setup-hero p { margin-top: 4px; color: var(--text-dim); font-size: 13px; }
|
|
499
|
+
.setup-hero kbd { margin-left: auto; align-self: flex-start; }
|
|
500
|
+
|
|
501
|
+
.setup-meter { display: flex; align-items: center; gap: 14px; padding: 18px 32px 6px; }
|
|
502
|
+
.setup-lead { flex: none; color: var(--text-dim); font-size: 13px; }
|
|
503
|
+
.setup-lead b { color: var(--text); }
|
|
504
|
+
.setup-progress { flex: 1; height: 6px; border-radius: 3px; background: var(--border); overflow: hidden; }
|
|
505
|
+
.setup-progress > i { display: block; height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s; }
|
|
506
|
+
|
|
507
|
+
.setup-steps { display: grid; gap: 10px; list-style: none; padding: 12px 24px 24px; }
|
|
508
|
+
|
|
509
|
+
.setup-step {
|
|
510
|
+
display: grid;
|
|
511
|
+
grid-template-columns: 36px 1fr auto;
|
|
512
|
+
gap: 4px 16px;
|
|
513
|
+
align-items: center;
|
|
514
|
+
padding: 16px 18px;
|
|
515
|
+
border: 1px solid var(--border);
|
|
516
|
+
border-radius: 10px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.setup-step.current {
|
|
520
|
+
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
|
521
|
+
background: color-mix(in srgb, var(--accent) 7%, transparent);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.setup-step.done { opacity: 0.75; }
|
|
525
|
+
|
|
526
|
+
.setup-mark {
|
|
527
|
+
grid-row: span 2;
|
|
528
|
+
align-self: start;
|
|
529
|
+
display: grid;
|
|
530
|
+
place-items: center;
|
|
531
|
+
width: 36px;
|
|
532
|
+
height: 36px;
|
|
533
|
+
border: 2px solid var(--border);
|
|
534
|
+
border-radius: 50%;
|
|
535
|
+
font-size: 15px;
|
|
536
|
+
font-weight: 700;
|
|
537
|
+
color: var(--text-dim);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.setup-step.done .setup-mark { border-color: var(--accent); background: var(--accent); color: var(--bg); }
|
|
541
|
+
.setup-step.current .setup-mark { border-color: var(--accent); color: var(--accent); }
|
|
542
|
+
.setup-title { font-size: 16px; font-weight: 700; }
|
|
543
|
+
.setup-step.done .setup-title { font-weight: 600; }
|
|
544
|
+
.setup-desc { grid-column: 2 / -1; color: var(--text-dim); font-size: 13px; line-height: 1.5; }
|
|
545
|
+
.setup-tag { margin-left: 8px; padding: 1px 6px; border: 1px solid var(--border); border-radius: 4px; font-size: 11px; font-weight: 500; color: var(--text-dim); vertical-align: 2px; }
|
|
546
|
+
|
|
547
|
+
.setup-list { grid-column: 2 / -1; display: grid; gap: 6px; margin-top: 10px; }
|
|
548
|
+
|
|
549
|
+
.setup-item {
|
|
550
|
+
display: grid;
|
|
551
|
+
grid-template-columns: 1fr auto;
|
|
552
|
+
gap: 4px 12px;
|
|
553
|
+
align-items: center;
|
|
554
|
+
padding: 8px 12px;
|
|
555
|
+
border-radius: 6px;
|
|
556
|
+
background: var(--bg);
|
|
557
|
+
font-size: 13px;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.setup-item code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
561
|
+
.setup-item .set-help { grid-column: 1 / -1; }
|
|
562
|
+
.setup-item a { color: var(--accent); font-size: 12px; }
|
|
563
|
+
.setup-notice { padding: 8px 12px; border-left: 3px solid var(--warn); background: var(--bg); border-radius: 6px; font-size: 13px; }
|
|
564
|
+
.setup-ok { color: var(--text-dim); font-size: 12px; }
|
|
565
|
+
.setup-ok.good { color: var(--ok); font-weight: 600; }
|
|
566
|
+
.setup-out { grid-column: 2 / -1; max-height: 160px; overflow: auto; margin-top: 8px; padding: 8px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; white-space: pre-wrap; }
|
|
567
|
+
|
|
568
|
+
.setup-box .settings-foot { padding: 14px 32px; align-items: center; font-size: 13px; }
|
|
569
|
+
.setup-box button { padding: 7px 16px; border-radius: 6px; font-size: 13px; font-weight: 600; }
|
|
570
|
+
.setup-box .setup-item button { padding: 3px 10px; font-size: 12px; }
|
|
571
|
+
.settings-box.setup-box button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
572
|
+
.settings-box.setup-box button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
|
|
573
|
+
|
|
453
574
|
.settings-box button:hover:not(:disabled) { color: var(--text); }
|
|
454
575
|
.settings-box button:disabled { opacity: 0.4; cursor: default; }
|
|
455
576
|
|
|
@@ -469,7 +590,8 @@
|
|
|
469
590
|
|
|
470
591
|
.seg button { border-color: transparent; }
|
|
471
592
|
|
|
472
|
-
.seg button.on
|
|
593
|
+
.seg button.on,
|
|
594
|
+
.settings-box .set-nav button[aria-selected="true"] {
|
|
473
595
|
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
|
474
596
|
color: var(--text);
|
|
475
597
|
font-weight: 600;
|
|
@@ -486,6 +608,14 @@
|
|
|
486
608
|
.set-apps th { color: var(--text-dim); font-size: 11px; font-weight: 400; }
|
|
487
609
|
.set-apps td:first-child { font-weight: 600; }
|
|
488
610
|
|
|
611
|
+
.keys-table tr.recording td { background: color-mix(in srgb, var(--accent) 7%, transparent); }
|
|
612
|
+
.key-btns { display: flex; gap: 4px; }
|
|
613
|
+
.key-tag { margin-left: 6px; padding: 0 5px; border: 1px solid var(--border); border-radius: 4px; font-size: 10px; font-weight: 500; color: var(--text-dim); }
|
|
614
|
+
.key-combo { display: inline-flex; align-items: center; gap: 6px; }
|
|
615
|
+
.key-local { color: var(--text-dim); font-size: 11px; }
|
|
616
|
+
.key-prompt { color: var(--accent); font-weight: 600; }
|
|
617
|
+
.key-keeps { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 14px 10px; }
|
|
618
|
+
|
|
489
619
|
.settings-foot { display: flex; gap: 12px; }
|
|
490
620
|
|
|
491
621
|
.settings-foot .set-keys { margin-left: auto; }
|
|
@@ -707,12 +837,36 @@
|
|
|
707
837
|
</div>
|
|
708
838
|
</div>
|
|
709
839
|
|
|
840
|
+
<div class="palette" id="shortcuts" hidden>
|
|
841
|
+
<div class="palette-box settings-box" role="dialog" aria-modal="true" aria-labelledby="shortcuts-title" tabindex="-1">
|
|
842
|
+
<div class="settings-bar"><h3 id="shortcuts-title">Keyboard shortcuts</h3><kbd>Esc</kbd></div>
|
|
843
|
+
<div id="shortcuts-body"></div>
|
|
844
|
+
<div class="palette-hint settings-foot" id="shortcuts-foot"></div>
|
|
845
|
+
</div>
|
|
846
|
+
</div>
|
|
847
|
+
|
|
848
|
+
<div class="palette" id="setup" hidden>
|
|
849
|
+
<div class="palette-box settings-box setup-box" role="dialog" aria-modal="true" aria-labelledby="setup-title" tabindex="-1">
|
|
850
|
+
<div class="setup-hero">
|
|
851
|
+
<img src="/icons/icon-192.svg" alt="">
|
|
852
|
+
<div>
|
|
853
|
+
<h3 id="setup-title">Welcome to Claude Code Hub</h3>
|
|
854
|
+
<p id="setup-sub">A few quick steps make the hub feel like a desktop app.</p>
|
|
855
|
+
</div>
|
|
856
|
+
<kbd>Esc</kbd>
|
|
857
|
+
</div>
|
|
858
|
+
<div id="setup-body"></div>
|
|
859
|
+
<div class="palette-hint settings-foot" id="setup-foot"></div>
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
|
|
710
863
|
<div class="update-toast" id="update-toast" role="status" hidden>
|
|
711
864
|
<div class="update-toast-body">
|
|
712
865
|
<div class="update-toast-title">Claude Code Hub <span id="update-latest"></span> is available</div>
|
|
713
866
|
<div class="update-toast-meta">You have <span id="update-current"></span> · <a id="update-notes" target="_blank" rel="noopener">Release notes</a></div>
|
|
714
|
-
<code
|
|
867
|
+
<code id="update-command"></code>
|
|
715
868
|
<div class="update-toast-actions">
|
|
869
|
+
<button class="update-toast-skip" id="update-review" type="button">Review update…</button>
|
|
716
870
|
<button class="update-toast-skip" id="update-skip" type="button">Skip this version</button>
|
|
717
871
|
</div>
|
|
718
872
|
</div>
|
package/server.js
CHANGED
|
@@ -33,6 +33,7 @@ const { liveHub, removeRecord, writeRecord } = require('./lib/hub-record');
|
|
|
33
33
|
const { HUB_PLUGIN } = require('./lib/install');
|
|
34
34
|
const { pluginChecker } = require('./lib/plugin-status');
|
|
35
35
|
const { readConfig, serializeConfig, writeConfigFile } = require('./lib/hub-config');
|
|
36
|
+
const { ONBOARDING, findConfigDirs, pluginsToInstall, setupStatus } = require('./lib/onboarding');
|
|
36
37
|
const {
|
|
37
38
|
appSettings,
|
|
38
39
|
appsShape,
|
|
@@ -174,6 +175,7 @@ function resolveHubConfig() {
|
|
|
174
175
|
if (typeof saved.modsEnabled === 'boolean') config.modsEnabled = saved.modsEnabled;
|
|
175
176
|
if (saved.keys !== undefined) config.keys = saved.keys;
|
|
176
177
|
if (saved.voice && typeof saved.voice === 'object') config.voice = saved.voice;
|
|
178
|
+
if (ONBOARDING.includes(saved.onboarding)) config.onboarding = saved.onboarding;
|
|
177
179
|
copyLive(saved, config);
|
|
178
180
|
const savedPrevious = typeof saved.previousConfigDir === 'string' ? canonical(saved.previousConfigDir) : null;
|
|
179
181
|
if (dirs.includes(savedPrevious) && savedPrevious !== active) config.previousConfigDir = savedPrevious;
|
|
@@ -232,7 +234,7 @@ const MODS = resolveMods(hubConfig, path.dirname(HUB_CONFIG_FILE), {
|
|
|
232
234
|
});
|
|
233
235
|
for (const m of MODS) console.log(`mod ${m.id} changes ${Object.keys(m.targets).join(', ')}`);
|
|
234
236
|
const ACTIONS = actionTable(ENABLED_APPS);
|
|
235
|
-
|
|
237
|
+
let { keys: userKeyMap, lines: keyLines } = userKeys(hubConfig.keys, ENABLED_APPS);
|
|
236
238
|
for (const line of keyLines) console.log(`${HUB_CONFIG_FILE}: ${line}`);
|
|
237
239
|
// The first enabled app in tab order that declares the capability in its manifest.
|
|
238
240
|
const provider = (cap) => ENABLED_APPS.find((a) => a.provides[cap]);
|
|
@@ -395,6 +397,7 @@ async function restartApp(pool, app) {
|
|
|
395
397
|
const { id } = app;
|
|
396
398
|
pool.restarts[id] = [];
|
|
397
399
|
pool.stopped.delete(id);
|
|
400
|
+
pool.deferred.delete(id);
|
|
398
401
|
await killApp(pool, id);
|
|
399
402
|
if (pool.retired) return false;
|
|
400
403
|
const child = spawnOne(pool, app);
|
|
@@ -404,9 +407,21 @@ async function restartApp(pool, app) {
|
|
|
404
407
|
|
|
405
408
|
async function stopApp(pool, { id }) {
|
|
406
409
|
pool.stopped.add(id);
|
|
410
|
+
pool.deferred.delete(id);
|
|
407
411
|
await killApp(pool, id);
|
|
408
412
|
}
|
|
409
413
|
|
|
414
|
+
// A deferredStart app that nothing has started yet starts on its first page load: its tab, a pane,
|
|
415
|
+
// an action. One the user stopped stays stopped.
|
|
416
|
+
function startOnDemand(name) {
|
|
417
|
+
const pool = activePool();
|
|
418
|
+
if (!pool?.deferred.delete(name)) return;
|
|
419
|
+
console.log(`[${name}] starting on first open`);
|
|
420
|
+
pool.stopped.delete(name);
|
|
421
|
+
const target = ENABLED_APPS.find((a) => a.id === name);
|
|
422
|
+
spawnOne(pool, target);
|
|
423
|
+
}
|
|
424
|
+
|
|
410
425
|
// Out of pool.children before the kill, so the exit handler does not restart it.
|
|
411
426
|
async function killApp(pool, id) {
|
|
412
427
|
const old = pool.children.get(id);
|
|
@@ -446,7 +461,7 @@ async function ensurePool(dir) {
|
|
|
446
461
|
pool = pools.get(dir);
|
|
447
462
|
if (pool) return pool.ready;
|
|
448
463
|
const stopped = new Set(ENABLED_APPS.filter((a) => a.deferredStart).map((a) => a.id));
|
|
449
|
-
pool = { dir, children: new Map(), ports: {}, restarts: {}, stopped, retired: false };
|
|
464
|
+
pool = { dir, children: new Map(), ports: {}, restarts: {}, stopped, deferred: new Set(stopped), retired: false };
|
|
450
465
|
pools.set(dir, pool);
|
|
451
466
|
spawnChildren(pool);
|
|
452
467
|
const ready = [...pool.children.values()].map((c) => c.ready);
|
|
@@ -817,6 +832,7 @@ function proxyHandler(name) {
|
|
|
817
832
|
if (body === undefined) return;
|
|
818
833
|
// One deadline for the whole request, not one per attempt: a retry must not re-spend the wait.
|
|
819
834
|
const budget = { deadline: Date.now() + (isNavigation(req) ? NAV_PORT_WAIT_MS : API_PORT_WAIT_MS) };
|
|
835
|
+
if (isNavigation(req)) startOnDemand(name);
|
|
820
836
|
// One listener for the request, however many attempts it takes.
|
|
821
837
|
let current = null;
|
|
822
838
|
res.once('close', () => current?.destroy());
|
|
@@ -961,6 +977,7 @@ function appsConfig() {
|
|
|
961
977
|
publishes: a.publishes,
|
|
962
978
|
};
|
|
963
979
|
if (a.hidden) entry.hidden = true;
|
|
980
|
+
if (a.deferredStart) entry.deferredStart = true;
|
|
964
981
|
if (a.settings.length) entry.settings = appValues.get(a.id);
|
|
965
982
|
if (TERMINAL_TOKEN && a.id === TERMINAL_APP.id) entry.terminalToken = TERMINAL_TOKEN;
|
|
966
983
|
return [a.id, entry];
|
|
@@ -991,12 +1008,13 @@ app.get('/api/config', (_req, res) => {
|
|
|
991
1008
|
...themeConfig(THEME_REGISTRY, { userFile: USER_THEMES_FILE, extra: MOD_THEMES }),
|
|
992
1009
|
apps: appsConfig(),
|
|
993
1010
|
actions: ACTIONS,
|
|
994
|
-
keys:
|
|
1011
|
+
keys: userKeyMap,
|
|
995
1012
|
activeConfigDir: hubConfig.activeConfigDir,
|
|
996
1013
|
defaultConfigDir,
|
|
997
1014
|
configDirSwitch: configDirSwitch(hubConfig),
|
|
998
1015
|
voice: voice && { provider: voice.provider, auto: voice.auto, commands: voice.commands },
|
|
999
1016
|
version: HUB_VERSION,
|
|
1017
|
+
onboarding: process.env.CLAUDE_HUB_NO_ONBOARDING ? 'skipped' : (hubConfig.onboarding ?? null),
|
|
1000
1018
|
});
|
|
1001
1019
|
});
|
|
1002
1020
|
|
|
@@ -1129,6 +1147,9 @@ function settingsView(saved) {
|
|
|
1129
1147
|
...(SETTING_SCHEMAS[a.id] && { settings: settingsOf(a.id) }),
|
|
1130
1148
|
})),
|
|
1131
1149
|
restartNeeded: appsShape(apps) !== RUNNING_APPS,
|
|
1150
|
+
keys: userKeyMap,
|
|
1151
|
+
keyWarnings: keyLines,
|
|
1152
|
+
keeps: ENABLED_APPS.filter((a) => a.keeps.length).map((a) => ({ id: a.id, name: appName(a), combos: a.keeps })),
|
|
1132
1153
|
};
|
|
1133
1154
|
}
|
|
1134
1155
|
|
|
@@ -1156,17 +1177,127 @@ app.post('/api/settings', (req, res) => {
|
|
|
1156
1177
|
}
|
|
1157
1178
|
const saved = readSettingsConfig(res);
|
|
1158
1179
|
if (!saved) return;
|
|
1159
|
-
const r = applySettings(saved, req.body, CONFIG_BASE, SETTING_SCHEMAS);
|
|
1180
|
+
const r = applySettings(saved, req.body, CONFIG_BASE, SETTING_SCHEMAS, ENABLED_APPS);
|
|
1160
1181
|
if (r.error) {
|
|
1161
1182
|
res.status(400).json({ error: r.error });
|
|
1162
1183
|
return;
|
|
1163
1184
|
}
|
|
1164
1185
|
writeHubConfig(r.config);
|
|
1165
1186
|
copyLive(r.config, hubConfig);
|
|
1187
|
+
if (req.body.keys !== undefined) ({ keys: userKeyMap, lines: keyLines } = userKeys(r.config.keys, ENABLED_APPS));
|
|
1166
1188
|
loadAppValues(r.config);
|
|
1167
1189
|
res.json(settingsView(r.config));
|
|
1168
1190
|
});
|
|
1169
1191
|
|
|
1192
|
+
// The setup checklist runs the same commands a user would type, in a child hub process: --install and
|
|
1193
|
+
// ext install call the claude CLI with execSync, and the tray flags start the tray and exit.
|
|
1194
|
+
const SETUP_PLUGINS = [
|
|
1195
|
+
{ ...HUB_PLUGIN, name: 'Hub skill', args: ['--install'] },
|
|
1196
|
+
...ENABLED_APPS.filter((a) => a.plugin).map((a) => ({ ...a.plugin, name: a.name, args: ['ext', 'install', a.id] })),
|
|
1197
|
+
];
|
|
1198
|
+
const SETUP_TIMEOUT_MS = 120_000;
|
|
1199
|
+
let setupBusy = false;
|
|
1200
|
+
|
|
1201
|
+
function runHub(args) {
|
|
1202
|
+
return new Promise((resolve) => {
|
|
1203
|
+
const child = spawn(process.execPath, [__filename, ...args, '--hub-dir', HUB_DIR], {
|
|
1204
|
+
env: process.env,
|
|
1205
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1206
|
+
windowsHide: true,
|
|
1207
|
+
timeout: SETUP_TIMEOUT_MS,
|
|
1208
|
+
});
|
|
1209
|
+
let output = '';
|
|
1210
|
+
const add = (d) => (output += d);
|
|
1211
|
+
child.stdout.on('data', add);
|
|
1212
|
+
child.stderr.on('data', add);
|
|
1213
|
+
child.on('error', (e) => resolve({ ok: false, output: e.message }));
|
|
1214
|
+
child.on('close', (code) => resolve({ ok: code === 0, output: output.trim() }));
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function currentSetup() {
|
|
1219
|
+
const { trayState } = require('./lib/tray');
|
|
1220
|
+
return setupStatus({
|
|
1221
|
+
tray: await trayState({ hubDir: HUB_DIR }),
|
|
1222
|
+
configDirs: hubConfig.configDirs,
|
|
1223
|
+
activeConfigDir: hubConfig.activeConfigDir,
|
|
1224
|
+
candidates: findConfigDirs({ home: os.homedir(), known: hubConfig.configDirs, hubDir: HUB_DIR }),
|
|
1225
|
+
plugins: SETUP_PLUGINS,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async function setupJob(res, job) {
|
|
1230
|
+
if (setupBusy) {
|
|
1231
|
+
res.status(409).json({ error: 'Another setup step is running' });
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
setupBusy = true;
|
|
1235
|
+
try {
|
|
1236
|
+
const r = await job();
|
|
1237
|
+
res.status(r.ok ? 200 : 500).json({ ...r, setup: await currentSetup() });
|
|
1238
|
+
} finally {
|
|
1239
|
+
setupBusy = false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
app.get('/api/setup', async (_req, res) => {
|
|
1244
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1245
|
+
res.json(await currentSetup());
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
app.post('/api/setup', (req, res) => {
|
|
1249
|
+
const value = req.body?.onboarding;
|
|
1250
|
+
if (!ONBOARDING.includes(value)) {
|
|
1251
|
+
res.status(400).json({ error: `onboarding must be one of ${ONBOARDING.join(', ')}` });
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const saved = readSettingsConfig(res);
|
|
1255
|
+
if (!saved) return;
|
|
1256
|
+
hubConfig.onboarding = value;
|
|
1257
|
+
writeHubConfig({ ...saved, onboarding: value });
|
|
1258
|
+
res.json({ onboarding: value });
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
app.post('/api/setup/tray', (_req, res) => {
|
|
1262
|
+
const appId = getArg('app-id');
|
|
1263
|
+
setupJob(res, () => runHub(['--autostart', '--port', String(hubPort), ...(appId ? ['--app-id', appId] : [])]));
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
async function installPlugins(dir) {
|
|
1267
|
+
const outputs = [];
|
|
1268
|
+
let pending = pluginsToInstall(dir, SETUP_PLUGINS);
|
|
1269
|
+
while (pending.length) {
|
|
1270
|
+
const next = pending[0];
|
|
1271
|
+
const r = await runHub([...next.args, '--dir', dir]);
|
|
1272
|
+
outputs.push(r.output);
|
|
1273
|
+
pending = pluginsToInstall(dir, SETUP_PLUGINS);
|
|
1274
|
+
if (!r.ok || pending[0]?.id === next.id) return { dir, ok: false, output: outputs.join('\n') };
|
|
1275
|
+
}
|
|
1276
|
+
return { dir, ok: true, output: outputs.join('\n') };
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
app.post('/api/setup/plugins', (req, res) => {
|
|
1280
|
+
const all = req.body?.all === true;
|
|
1281
|
+
const dir = req.body?.dir;
|
|
1282
|
+
if (!all && !hubConfig.configDirs.includes(dir)) {
|
|
1283
|
+
res.status(404).json({ error: 'Unknown config dir; add it first' });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
setupJob(res, async () => {
|
|
1287
|
+
const dirs = all ? hubConfig.configDirs.filter((d) => pluginsToInstall(d, SETUP_PLUGINS).length) : [dir];
|
|
1288
|
+
const results = [];
|
|
1289
|
+
for (const d of dirs) results.push(await installPlugins(d));
|
|
1290
|
+
return {
|
|
1291
|
+
ok: results.every((r) => r.ok),
|
|
1292
|
+
output: results
|
|
1293
|
+
.filter((r) => r.output)
|
|
1294
|
+
.map((r) => `${r.dir}\n${r.output}`)
|
|
1295
|
+
.join('\n\n'),
|
|
1296
|
+
results: results.map(({ dir, ok }) => ({ dir, ok })),
|
|
1297
|
+
};
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1170
1301
|
// The stalls hit a few percent of requests, so each round sends several probes per app.
|
|
1171
1302
|
const HEALTH_INTERVAL_MS = 30_000;
|
|
1172
1303
|
const HEALTH_PROBES = 5;
|