claude-code-hub 1.34.0 → 1.35.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/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 +13 -0
- 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 +674 -21
- package/public/index.html +156 -2
- package/server.js +119 -3
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]);
|
|
@@ -991,12 +993,13 @@ app.get('/api/config', (_req, res) => {
|
|
|
991
993
|
...themeConfig(THEME_REGISTRY, { userFile: USER_THEMES_FILE, extra: MOD_THEMES }),
|
|
992
994
|
apps: appsConfig(),
|
|
993
995
|
actions: ACTIONS,
|
|
994
|
-
keys:
|
|
996
|
+
keys: userKeyMap,
|
|
995
997
|
activeConfigDir: hubConfig.activeConfigDir,
|
|
996
998
|
defaultConfigDir,
|
|
997
999
|
configDirSwitch: configDirSwitch(hubConfig),
|
|
998
1000
|
voice: voice && { provider: voice.provider, auto: voice.auto, commands: voice.commands },
|
|
999
1001
|
version: HUB_VERSION,
|
|
1002
|
+
onboarding: hubConfig.onboarding ?? null,
|
|
1000
1003
|
});
|
|
1001
1004
|
});
|
|
1002
1005
|
|
|
@@ -1129,6 +1132,9 @@ function settingsView(saved) {
|
|
|
1129
1132
|
...(SETTING_SCHEMAS[a.id] && { settings: settingsOf(a.id) }),
|
|
1130
1133
|
})),
|
|
1131
1134
|
restartNeeded: appsShape(apps) !== RUNNING_APPS,
|
|
1135
|
+
keys: userKeyMap,
|
|
1136
|
+
keyWarnings: keyLines,
|
|
1137
|
+
keeps: ENABLED_APPS.filter((a) => a.keeps.length).map((a) => ({ id: a.id, name: appName(a), combos: a.keeps })),
|
|
1132
1138
|
};
|
|
1133
1139
|
}
|
|
1134
1140
|
|
|
@@ -1156,17 +1162,127 @@ app.post('/api/settings', (req, res) => {
|
|
|
1156
1162
|
}
|
|
1157
1163
|
const saved = readSettingsConfig(res);
|
|
1158
1164
|
if (!saved) return;
|
|
1159
|
-
const r = applySettings(saved, req.body, CONFIG_BASE, SETTING_SCHEMAS);
|
|
1165
|
+
const r = applySettings(saved, req.body, CONFIG_BASE, SETTING_SCHEMAS, ENABLED_APPS);
|
|
1160
1166
|
if (r.error) {
|
|
1161
1167
|
res.status(400).json({ error: r.error });
|
|
1162
1168
|
return;
|
|
1163
1169
|
}
|
|
1164
1170
|
writeHubConfig(r.config);
|
|
1165
1171
|
copyLive(r.config, hubConfig);
|
|
1172
|
+
if (req.body.keys !== undefined) ({ keys: userKeyMap, lines: keyLines } = userKeys(r.config.keys, ENABLED_APPS));
|
|
1166
1173
|
loadAppValues(r.config);
|
|
1167
1174
|
res.json(settingsView(r.config));
|
|
1168
1175
|
});
|
|
1169
1176
|
|
|
1177
|
+
// The setup checklist runs the same commands a user would type, in a child hub process: --install and
|
|
1178
|
+
// ext install call the claude CLI with execSync, and the tray flags start the tray and exit.
|
|
1179
|
+
const SETUP_PLUGINS = [
|
|
1180
|
+
{ ...HUB_PLUGIN, name: 'Hub skill', args: ['--install'] },
|
|
1181
|
+
...ENABLED_APPS.filter((a) => a.plugin).map((a) => ({ ...a.plugin, name: a.name, args: ['ext', 'install', a.id] })),
|
|
1182
|
+
];
|
|
1183
|
+
const SETUP_TIMEOUT_MS = 120_000;
|
|
1184
|
+
let setupBusy = false;
|
|
1185
|
+
|
|
1186
|
+
function runHub(args) {
|
|
1187
|
+
return new Promise((resolve) => {
|
|
1188
|
+
const child = spawn(process.execPath, [__filename, ...args, '--hub-dir', HUB_DIR], {
|
|
1189
|
+
env: process.env,
|
|
1190
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1191
|
+
windowsHide: true,
|
|
1192
|
+
timeout: SETUP_TIMEOUT_MS,
|
|
1193
|
+
});
|
|
1194
|
+
let output = '';
|
|
1195
|
+
const add = (d) => (output += d);
|
|
1196
|
+
child.stdout.on('data', add);
|
|
1197
|
+
child.stderr.on('data', add);
|
|
1198
|
+
child.on('error', (e) => resolve({ ok: false, output: e.message }));
|
|
1199
|
+
child.on('close', (code) => resolve({ ok: code === 0, output: output.trim() }));
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
async function currentSetup() {
|
|
1204
|
+
const { trayState } = require('./lib/tray');
|
|
1205
|
+
return setupStatus({
|
|
1206
|
+
tray: await trayState({ hubDir: HUB_DIR }),
|
|
1207
|
+
configDirs: hubConfig.configDirs,
|
|
1208
|
+
activeConfigDir: hubConfig.activeConfigDir,
|
|
1209
|
+
candidates: findConfigDirs({ home: os.homedir(), known: hubConfig.configDirs, hubDir: HUB_DIR }),
|
|
1210
|
+
plugins: SETUP_PLUGINS,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function setupJob(res, job) {
|
|
1215
|
+
if (setupBusy) {
|
|
1216
|
+
res.status(409).json({ error: 'Another setup step is running' });
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
setupBusy = true;
|
|
1220
|
+
try {
|
|
1221
|
+
const r = await job();
|
|
1222
|
+
res.status(r.ok ? 200 : 500).json({ ...r, setup: await currentSetup() });
|
|
1223
|
+
} finally {
|
|
1224
|
+
setupBusy = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
app.get('/api/setup', async (_req, res) => {
|
|
1229
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
1230
|
+
res.json(await currentSetup());
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
app.post('/api/setup', (req, res) => {
|
|
1234
|
+
const value = req.body?.onboarding;
|
|
1235
|
+
if (!ONBOARDING.includes(value)) {
|
|
1236
|
+
res.status(400).json({ error: `onboarding must be one of ${ONBOARDING.join(', ')}` });
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const saved = readSettingsConfig(res);
|
|
1240
|
+
if (!saved) return;
|
|
1241
|
+
hubConfig.onboarding = value;
|
|
1242
|
+
writeHubConfig({ ...saved, onboarding: value });
|
|
1243
|
+
res.json({ onboarding: value });
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
app.post('/api/setup/tray', (_req, res) => {
|
|
1247
|
+
const appId = getArg('app-id');
|
|
1248
|
+
setupJob(res, () => runHub(['--autostart', '--port', String(hubPort), ...(appId ? ['--app-id', appId] : [])]));
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
async function installPlugins(dir) {
|
|
1252
|
+
const outputs = [];
|
|
1253
|
+
let pending = pluginsToInstall(dir, SETUP_PLUGINS);
|
|
1254
|
+
while (pending.length) {
|
|
1255
|
+
const next = pending[0];
|
|
1256
|
+
const r = await runHub([...next.args, '--dir', dir]);
|
|
1257
|
+
outputs.push(r.output);
|
|
1258
|
+
pending = pluginsToInstall(dir, SETUP_PLUGINS);
|
|
1259
|
+
if (!r.ok || pending[0]?.id === next.id) return { dir, ok: false, output: outputs.join('\n') };
|
|
1260
|
+
}
|
|
1261
|
+
return { dir, ok: true, output: outputs.join('\n') };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
app.post('/api/setup/plugins', (req, res) => {
|
|
1265
|
+
const all = req.body?.all === true;
|
|
1266
|
+
const dir = req.body?.dir;
|
|
1267
|
+
if (!all && !hubConfig.configDirs.includes(dir)) {
|
|
1268
|
+
res.status(404).json({ error: 'Unknown config dir; add it first' });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
setupJob(res, async () => {
|
|
1272
|
+
const dirs = all ? hubConfig.configDirs.filter((d) => pluginsToInstall(d, SETUP_PLUGINS).length) : [dir];
|
|
1273
|
+
const results = [];
|
|
1274
|
+
for (const d of dirs) results.push(await installPlugins(d));
|
|
1275
|
+
return {
|
|
1276
|
+
ok: results.every((r) => r.ok),
|
|
1277
|
+
output: results
|
|
1278
|
+
.filter((r) => r.output)
|
|
1279
|
+
.map((r) => `${r.dir}\n${r.output}`)
|
|
1280
|
+
.join('\n\n'),
|
|
1281
|
+
results: results.map(({ dir, ok }) => ({ dir, ok })),
|
|
1282
|
+
};
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1170
1286
|
// The stalls hit a few percent of requests, so each round sends several probes per app.
|
|
1171
1287
|
const HEALTH_INTERVAL_MS = 30_000;
|
|
1172
1288
|
const HEALTH_PROBES = 5;
|