claude-code-hub 1.8.0 → 1.10.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/apps.js +25 -1
- package/lib/install.js +8 -4
- package/lib/plugin-status.js +47 -0
- package/package.json +5 -5
- package/plugin/plugins/claude-code-hub/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-hub/skills/hub-builder/references/extensibility.md +1 -1
- package/public/app.js +67 -17
- package/public/index.html +23 -2
- package/server.js +10 -2
package/lib/apps.js
CHANGED
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { isContained } = require('./contain');
|
|
6
6
|
const { COMBO_RE } = require('./keymap');
|
|
7
|
+
const { bundledVersion } = require('./plugin-status');
|
|
7
8
|
|
|
8
9
|
// Built-in apps in their default tab order. The rest of each app comes from its manifest.
|
|
9
10
|
const APPS = [
|
|
@@ -65,7 +66,19 @@ function manifestError(m, id, root) {
|
|
|
65
66
|
if (typeof m.run?.entry !== 'string') return 'run.entry is missing';
|
|
66
67
|
const entry = path.resolve(root, m.run.entry);
|
|
67
68
|
if (entry === path.resolve(root) || !isContained(entry, root)) return 'run.entry is outside the app directory';
|
|
68
|
-
return providesError(m.provides) ?? publishesError(m.publishes) ?? keysError(m.keys);
|
|
69
|
+
return providesError(m.provides) ?? publishesError(m.publishes) ?? keysError(m.keys) ?? pluginError(m.plugin, root);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PLUGIN_ID_RE = /^[\w.-]+@[\w.-]+$/;
|
|
73
|
+
|
|
74
|
+
function pluginError(plugin, root) {
|
|
75
|
+
if (plugin === undefined) return null;
|
|
76
|
+
if (typeof plugin?.id !== 'string' || !PLUGIN_ID_RE.test(plugin.id)) return 'plugin.id is not <name>@<marketplace>';
|
|
77
|
+
if (typeof plugin.path !== 'string') return 'plugin.path is missing';
|
|
78
|
+
const dir = path.resolve(root, plugin.path);
|
|
79
|
+
if (dir === path.resolve(root) || !isContained(dir, root)) return 'plugin.path is outside the app directory';
|
|
80
|
+
if (typeof plugin.install !== 'string' || !plugin.install) return 'plugin.install is missing';
|
|
81
|
+
return null;
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
function keysError(keys) {
|
|
@@ -146,8 +159,19 @@ function loadApp(app, { flagPort, hubRoot = HUB_ROOT, resolve = require.resolve,
|
|
|
146
159
|
log(`${path.join(root, 'hub-app.json')}: ${reason}. ${app.id} is skipped`);
|
|
147
160
|
return null;
|
|
148
161
|
}
|
|
162
|
+
let version = null;
|
|
163
|
+
try {
|
|
164
|
+
version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version ?? null;
|
|
165
|
+
} catch {}
|
|
166
|
+
const { plugin } = manifest;
|
|
149
167
|
return {
|
|
150
168
|
id: app.id,
|
|
169
|
+
version,
|
|
170
|
+
plugin: plugin && {
|
|
171
|
+
id: plugin.id,
|
|
172
|
+
bundled: bundledVersion(path.join(root, plugin.path)),
|
|
173
|
+
install: plugin.install,
|
|
174
|
+
},
|
|
151
175
|
entry: path.join(root, manifest.run.entry),
|
|
152
176
|
port: userPort(app, flagPort, log) ?? manifest.run.defaultPort ?? 0,
|
|
153
177
|
name: manifest.name ?? app.id,
|
package/lib/install.js
CHANGED
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
const { execSync } = require('node:child_process');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
5
|
const path = require('node:path');
|
|
6
|
+
const { bundledVersion, isDefaultConfigDir } = require('./plugin-status');
|
|
7
7
|
|
|
8
8
|
const PLUGIN_SRC = path.join(__dirname, '..', 'plugin');
|
|
9
9
|
const MARKETPLACE = 'claude-code-hub';
|
|
10
10
|
const PLUGIN_ID = `claude-code-hub@${MARKETPLACE}`;
|
|
11
|
+
const HUB_PLUGIN = {
|
|
12
|
+
id: PLUGIN_ID,
|
|
13
|
+
bundled: bundledVersion(path.join(PLUGIN_SRC, 'plugins', 'claude-code-hub')),
|
|
14
|
+
install: 'claude-code-hub --install',
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
// Claude Code keeps .claude.json beside ~/.claude when CLAUDE_CONFIG_DIR is unset but inside the
|
|
13
18
|
// dir when it is set, so the default dir must stay unset rather than be spelled out.
|
|
14
19
|
function claudeEnv(configDir) {
|
|
15
|
-
|
|
16
|
-
return isDefault ? process.env : { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
|
20
|
+
return isDefaultConfigDir(configDir) ? process.env : { ...process.env, CLAUDE_CONFIG_DIR: configDir };
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
// `idempotent` when the output names an ok pattern, on exit 0 too: some commands report "already" and succeed.
|
|
@@ -86,4 +90,4 @@ function runUninstall({ configDir }) {
|
|
|
86
90
|
return ok;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
module.exports = { runInstall, runUninstall, claudeEnv };
|
|
93
|
+
module.exports = { runInstall, runUninstall, claudeEnv, HUB_PLUGIN };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
function readJson(file) {
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isDefaultConfigDir(configDir) {
|
|
16
|
+
return path.resolve(configDir) === path.resolve(os.homedir(), '.claude');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --dir rather than CLAUDE_CONFIG_DIR=…, so the same line runs in bash, PowerShell and cmd.
|
|
20
|
+
function installCommand(base, configDir) {
|
|
21
|
+
return isDefaultConfigDir(configDir) ? base : `${base} --dir "${configDir}"`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The version in the plugin.json under pluginDir, the folder an app or the hub ships its plugin in.
|
|
25
|
+
function bundledVersion(pluginDir) {
|
|
26
|
+
return readJson(path.join(pluginDir, '.claude-plugin', 'plugin.json'))?.version ?? null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Reads the config dir once and gives a check for each plugin, {id, bundled, install}. The claude CLI
|
|
30
|
+
// records installs in <config dir>/plugins/installed_plugins.json, one entry per scope; a user-scope
|
|
31
|
+
// entry wins, because the installers install at user scope.
|
|
32
|
+
function pluginChecker(configDir) {
|
|
33
|
+
const installs = readJson(path.join(configDir, 'plugins', 'installed_plugins.json'))?.plugins ?? {};
|
|
34
|
+
const enabledPlugins = readJson(path.join(configDir, 'settings.json'))?.enabledPlugins ?? {};
|
|
35
|
+
return ({ id, bundled, install }) => {
|
|
36
|
+
const entries = Array.isArray(installs[id]) ? installs[id] : [];
|
|
37
|
+
const entry = entries.find((e) => e?.scope === 'user') ?? entries[0];
|
|
38
|
+
const installed = typeof entry?.version === 'string' ? entry.version : null;
|
|
39
|
+
let state = 'ok';
|
|
40
|
+
if (!installed) state = 'missing';
|
|
41
|
+
else if (enabledPlugins[id] === false) state = 'disabled';
|
|
42
|
+
else if (bundled && installed !== bundled) state = 'mismatch';
|
|
43
|
+
return { id, bundled, installed, state, installCommand: installCommand(install, configDir) };
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { pluginChecker, bundledVersion, isDefaultConfigDir };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hub",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"claude-code-cost": "1.3.
|
|
50
|
-
"claude-code-kanban": "7.
|
|
51
|
-
"claude-code-marketplace": "1.
|
|
52
|
-
"claude-code-memory-explorer": "1.
|
|
49
|
+
"claude-code-cost": "1.3.1",
|
|
50
|
+
"claude-code-kanban": "7.6.0",
|
|
51
|
+
"claude-code-marketplace": "1.5.1",
|
|
52
|
+
"claude-code-memory-explorer": "1.3.1",
|
|
53
53
|
"express": "^4.21.0",
|
|
54
54
|
"open": "^10.1.0"
|
|
55
55
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-hub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Claude Code Hub integration: a skill to build on and extend the hub, with links to its docs, the extension mechanism and custom themes",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NikiforovAll"
|
|
@@ -60,7 +60,7 @@ After a server change, restart the app: `Ctrl+Alt+A`, select it, `Ctrl+R`. After
|
|
|
60
60
|
|
|
61
61
|
## The manifest
|
|
62
62
|
|
|
63
|
-
`hub-app.json` declares how to run the app and what it offers: `manifest: 1`, `id`, `name`, `icon` (a Lucide name), `run.entry` (inside the folder; a path into `node_modules` is fine), `run.defaultPort`, `loading.verbs`, `publishes`, `actions.handles`, `provides
|
|
63
|
+
`hub-app.json` declares how to run the app and what it offers: `manifest: 1`, `id`, `name`, `icon` (a Lucide name), `run.entry` (inside the folder; a path into `node_modules` is fine), `run.defaultPort`, `loading.verbs`, `publishes`, `actions.handles`, `provides`, `keys.keeps`, `plugin` (`{id, path, install}` of a Claude Code plugin the app ships; the app launcher shows its state). The hub reads it before the app runs and skips the app, with a log line, when it breaks a rule. Page: `extensibility/reference/manifest/`.
|
|
64
64
|
|
|
65
65
|
The npm packages of the built-in tools do not ship `hub-app.json`. A patch or fork starts from the tool's manifest in its GitHub repo and changes `id`, `name` and `run.entry`. Page for what each built-in declares: `extensibility/reference/built-in/`.
|
|
66
66
|
|
package/public/app.js
CHANGED
|
@@ -37,6 +37,8 @@ const missed = new Map();
|
|
|
37
37
|
const MAX_EVENT_CHARS = 16 * 1024;
|
|
38
38
|
// The config dir the hub falls back to (~/.claude). Stays out of the window title.
|
|
39
39
|
let defaultConfigDir = null;
|
|
40
|
+
// The app launcher's footer: the hub version from /api/config and its plugin from /api/apps/stats.
|
|
41
|
+
const hubInfo = { version: null, plugin: null };
|
|
40
42
|
// mode: 'project' (Ctrl+Alt+P), 'configDir' (Ctrl+Alt+W) or 'app' (Ctrl+Alt+A). One widget, three row sources.
|
|
41
43
|
const palette = {
|
|
42
44
|
open: false,
|
|
@@ -47,6 +49,7 @@ const palette = {
|
|
|
47
49
|
sel: 0,
|
|
48
50
|
query: '',
|
|
49
51
|
confirmRestart: null,
|
|
52
|
+
message: null,
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
function loadThemeState() {
|
|
@@ -229,6 +232,7 @@ async function init() {
|
|
|
229
232
|
const res = await fetch('/api/config');
|
|
230
233
|
const config = await res.json();
|
|
231
234
|
userKeys = config.keys ?? {};
|
|
235
|
+
hubInfo.version = config.version ?? null;
|
|
232
236
|
setApps(config.apps);
|
|
233
237
|
defaultConfigDir = config.defaultConfigDir ?? null;
|
|
234
238
|
activeConfigDir = config.activeConfigDir ?? null;
|
|
@@ -816,7 +820,9 @@ function relAge(ts) {
|
|
|
816
820
|
}
|
|
817
821
|
|
|
818
822
|
async function loadAppStats() {
|
|
819
|
-
|
|
823
|
+
const { hub, apps: appStats } = await sendJson('GET', '/api/apps/stats');
|
|
824
|
+
palette.appStats = appStats;
|
|
825
|
+
hubInfo.plugin = hub.plugin;
|
|
820
826
|
}
|
|
821
827
|
|
|
822
828
|
async function loadConfigDirs() {
|
|
@@ -920,8 +926,52 @@ function rowButton(className, glyph, key, title, label) {
|
|
|
920
926
|
return btn;
|
|
921
927
|
}
|
|
922
928
|
|
|
923
|
-
|
|
924
|
-
|
|
929
|
+
// The footer shows palette.message, set by an action, else the mode's hint.
|
|
930
|
+
function drawPaletteHint() {
|
|
931
|
+
const content = palette.message ?? PALETTE_MODES[palette.mode].hint ?? '';
|
|
932
|
+
document.getElementById('palette-hint').replaceChildren(...[].concat(content));
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function setPaletteHint(message) {
|
|
936
|
+
palette.message = message;
|
|
937
|
+
drawPaletteHint();
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// The plugin in the active config dir: its version, or a ⚠ that copies the install command.
|
|
941
|
+
function pluginNode(p) {
|
|
942
|
+
const problem = {
|
|
943
|
+
missing: { label: 'plugin not installed', cause: `${p.id} is not installed in this config dir.` },
|
|
944
|
+
disabled: {
|
|
945
|
+
label: `plugin ${p.installed} is off`,
|
|
946
|
+
cause: `${p.id} ${p.installed} is disabled. Enable it with /plugin in Claude Code.`,
|
|
947
|
+
noCopy: true,
|
|
948
|
+
},
|
|
949
|
+
mismatch: {
|
|
950
|
+
label: `plugin ${p.installed}`,
|
|
951
|
+
cause: `${p.id} ${p.installed} is installed, this version ships ${p.bundled}.`,
|
|
952
|
+
},
|
|
953
|
+
}[p.state];
|
|
954
|
+
if (!problem) return el('span', 'ver', `plugin ${p.installed}`);
|
|
955
|
+
const badge = el('button', 'plugin-warn', `⚠ ${problem.label}`);
|
|
956
|
+
badge.type = 'button';
|
|
957
|
+
badge.title = problem.cause;
|
|
958
|
+
badge.setAttribute('aria-label', problem.cause);
|
|
959
|
+
if (problem.noCopy) return badge;
|
|
960
|
+
badge.title += `\nClick to copy: ${p.installCommand}\nThen restart open sessions.`;
|
|
961
|
+
badge.addEventListener('click', (e) => {
|
|
962
|
+
e.stopPropagation();
|
|
963
|
+
copyToHint(p.installCommand);
|
|
964
|
+
});
|
|
965
|
+
return badge;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function copyToHint(text) {
|
|
969
|
+
try {
|
|
970
|
+
await navigator.clipboard.writeText(text);
|
|
971
|
+
setPaletteHint(`Copied ${text}`);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
setPaletteHint(`Could not copy ${text}: ${err.message}`);
|
|
974
|
+
}
|
|
925
975
|
}
|
|
926
976
|
|
|
927
977
|
// The terminal provider answers 409 with a terminal count; the second Ctrl+R on the same app sends force.
|
|
@@ -944,16 +994,6 @@ async function restartFromPalette(id) {
|
|
|
944
994
|
switchTab(id);
|
|
945
995
|
}
|
|
946
996
|
|
|
947
|
-
async function copyAppUrl(id) {
|
|
948
|
-
const url = apps[id].url;
|
|
949
|
-
try {
|
|
950
|
-
await navigator.clipboard.writeText(url);
|
|
951
|
-
setPaletteHint(`Copied ${url}`);
|
|
952
|
-
} catch (err) {
|
|
953
|
-
setPaletteHint(`Could not copy ${url}: ${err.message}`);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
997
|
// `typed` is the trimmed query, `q` its lowercase form for matching.
|
|
958
998
|
const PALETTE_MODES = {
|
|
959
999
|
project: {
|
|
@@ -1019,7 +1059,12 @@ const PALETTE_MODES = {
|
|
|
1019
1059
|
app: {
|
|
1020
1060
|
placeholder: 'Search apps...',
|
|
1021
1061
|
label: 'Switch app',
|
|
1022
|
-
hint
|
|
1062
|
+
get hint() {
|
|
1063
|
+
const keys = 'Enter switch · Ctrl+C copy the URL · Ctrl+R restart the app';
|
|
1064
|
+
if (!hubInfo.version) return keys;
|
|
1065
|
+
const plugin = hubInfo.plugin ? [pluginNode(hubInfo.plugin), ' · '] : [];
|
|
1066
|
+
return [`Claude Code Hub ${hubInfo.version} · `, ...plugin, keys];
|
|
1067
|
+
},
|
|
1023
1068
|
empty: 'No matching app',
|
|
1024
1069
|
rows: (_typed, q) => appRows(q),
|
|
1025
1070
|
load: loadAppStats,
|
|
@@ -1030,6 +1075,8 @@ const PALETTE_MODES = {
|
|
|
1030
1075
|
fillRow(li, row, q) {
|
|
1031
1076
|
li.append(appIcon(row.app), markedSpan('name', row.app.name, q), el('span', 'url', new URL(row.app.url).host));
|
|
1032
1077
|
const s = palette.appStats?.[row.id];
|
|
1078
|
+
if (s?.version) li.append(el('span', 'ver', s.version));
|
|
1079
|
+
if (s?.plugin) li.append(pluginNode(s.plugin));
|
|
1033
1080
|
if (s?.slowMs) {
|
|
1034
1081
|
const badge = el('span', 'slow', 'slow port');
|
|
1035
1082
|
badge.title = `Its port delays requests by up to ${(s.slowMs / 1000).toFixed(1)} s. Restart (Ctrl+R) moves it to a new port.`;
|
|
@@ -1046,7 +1093,7 @@ const PALETTE_MODES = {
|
|
|
1046
1093
|
rowButton('palette-restart', '↻', 'r', 'Restart the app (Ctrl+R)', `Restart ${row.app.name}`),
|
|
1047
1094
|
);
|
|
1048
1095
|
},
|
|
1049
|
-
rowKeys: { c: (row) =>
|
|
1096
|
+
rowKeys: { c: (row) => copyToHint(apps[row.id].url), r: (row) => restartFromPalette(row.id) },
|
|
1050
1097
|
commit(row) {
|
|
1051
1098
|
closePalette();
|
|
1052
1099
|
switchTab(row.id);
|
|
@@ -1105,10 +1152,11 @@ function openPalette(mode) {
|
|
|
1105
1152
|
palette.query = '';
|
|
1106
1153
|
palette.sel = null;
|
|
1107
1154
|
palette.confirmRestart = null;
|
|
1155
|
+
palette.message = null;
|
|
1108
1156
|
input.value = '';
|
|
1109
1157
|
input.placeholder = spec.placeholder;
|
|
1110
1158
|
document.querySelector('#palette .palette-box').setAttribute('aria-label', spec.label);
|
|
1111
|
-
|
|
1159
|
+
drawPaletteHint();
|
|
1112
1160
|
document.getElementById('palette').hidden = false;
|
|
1113
1161
|
// Ctrl+Alt+P usually arrives forwarded from a focused iframe. Without inert the sub-app can keep
|
|
1114
1162
|
// or take focus back, and then Escape is handled inside it — the palette stays open and only the
|
|
@@ -1119,7 +1167,9 @@ function openPalette(mode) {
|
|
|
1119
1167
|
// Stale-while-revalidate: the cached list renders instantly, recency refreshes when this lands.
|
|
1120
1168
|
Promise.resolve(spec.load?.())
|
|
1121
1169
|
.then(() => {
|
|
1122
|
-
if (palette.open
|
|
1170
|
+
if (!palette.open || palette.mode !== mode) return;
|
|
1171
|
+
drawPaletteHint();
|
|
1172
|
+
renderPalette();
|
|
1123
1173
|
})
|
|
1124
1174
|
.catch((err) => {
|
|
1125
1175
|
if (err.status === 401) showErrorToast('Could not load the list', err);
|
package/public/index.html
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
--surface-hover: #1f2a4a;
|
|
25
25
|
--text: #e8e8ef;
|
|
26
26
|
--text-dim: #8a8fa3;
|
|
27
|
+
--warn: #f5a524;
|
|
27
28
|
--shadow: rgba(0, 0, 0, 0.55);
|
|
28
29
|
--ghost-mix: 22%;
|
|
29
30
|
}
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
--surface-hover: #f1f1f4;
|
|
40
41
|
--text: #18181b;
|
|
41
42
|
--text-dim: #71717a;
|
|
43
|
+
--warn: #8a4b00;
|
|
42
44
|
--shadow: rgba(0, 0, 0, 0.18);
|
|
43
45
|
--ghost-mix: 10%;
|
|
44
46
|
}
|
|
@@ -283,11 +285,30 @@
|
|
|
283
285
|
margin-left: 10px;
|
|
284
286
|
padding: 0 6px;
|
|
285
287
|
border-radius: 4px;
|
|
286
|
-
color:
|
|
287
|
-
border: 1px solid
|
|
288
|
+
color: var(--warn);
|
|
289
|
+
border: 1px solid var(--warn);
|
|
288
290
|
font-size: 11px;
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
.palette-row .ver,
|
|
294
|
+
.palette-hint .ver {
|
|
295
|
+
color: var(--text-dim);
|
|
296
|
+
font-size: 12px;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.plugin-warn {
|
|
300
|
+
padding: 0 6px;
|
|
301
|
+
border: 1px solid var(--warn);
|
|
302
|
+
border-radius: 4px;
|
|
303
|
+
background: transparent;
|
|
304
|
+
color: var(--warn);
|
|
305
|
+
font: inherit;
|
|
306
|
+
font-size: 11px;
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.palette-hint .plugin-warn { border: none; padding: 0; font-size: 12px; }
|
|
311
|
+
|
|
291
312
|
.palette-row.literal {
|
|
292
313
|
border-top: 1px solid var(--border);
|
|
293
314
|
color: var(--accent);
|
package/server.js
CHANGED
|
@@ -19,6 +19,8 @@ const { createPortHealth } = require('./lib/port-health');
|
|
|
19
19
|
const { readRegistry, themeConfig } = require('./lib/themes');
|
|
20
20
|
const { createUpdateCheck } = require('./lib/update-check');
|
|
21
21
|
const { liveHub, removeRecord, writeRecord } = require('./lib/hub-record');
|
|
22
|
+
const { HUB_PLUGIN } = require('./lib/install');
|
|
23
|
+
const { pluginChecker } = require('./lib/plugin-status');
|
|
22
24
|
const { version: HUB_VERSION } = require('./package.json');
|
|
23
25
|
|
|
24
26
|
function getArg(name) {
|
|
@@ -893,6 +895,7 @@ app.get('/api/config', (_req, res) => {
|
|
|
893
895
|
keys: USER_KEYS,
|
|
894
896
|
activeConfigDir: hubConfig.activeConfigDir,
|
|
895
897
|
defaultConfigDir,
|
|
898
|
+
version: HUB_VERSION,
|
|
896
899
|
});
|
|
897
900
|
});
|
|
898
901
|
|
|
@@ -999,14 +1002,19 @@ const STATS_TIMEOUT_MS = 1000;
|
|
|
999
1002
|
app.get('/api/apps/stats', async (_req, res) => {
|
|
1000
1003
|
res.setHeader('Cache-Control', 'no-store');
|
|
1001
1004
|
const pool = activePool();
|
|
1005
|
+
const check = pluginChecker(hubConfig.activeConfigDir);
|
|
1006
|
+
const plugin = (p) => (p ? check(p) : null);
|
|
1002
1007
|
const entries = await Promise.all(
|
|
1003
1008
|
ENABLED_APPS.map(async (a) => {
|
|
1004
1009
|
const child = pool?.children.get(a.id);
|
|
1005
1010
|
const slowMs = (child && portHealth.flagged(child)?.worstMs) ?? null;
|
|
1006
|
-
return [
|
|
1011
|
+
return [
|
|
1012
|
+
a.id,
|
|
1013
|
+
{ ...(await stats(child, STATS_TIMEOUT_MS)), slowMs, version: a.version, plugin: plugin(a.plugin) },
|
|
1014
|
+
];
|
|
1007
1015
|
}),
|
|
1008
1016
|
);
|
|
1009
|
-
res.json(Object.fromEntries(entries));
|
|
1017
|
+
res.json({ hub: { plugin: plugin(HUB_PLUGIN) }, apps: Object.fromEntries(entries) });
|
|
1010
1018
|
});
|
|
1011
1019
|
|
|
1012
1020
|
// Restarting the terminal provider ends its live terminals, so that takes force: true.
|