claude-code-hub 1.0.2 → 1.0.3

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.
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const REGISTRY_URL = 'https://registry.npmjs.org/claude-code-hub/latest';
4
+ const RELEASE_URL = 'https://github.com/NikiforovAll/claude-code-hub/releases/tag/v';
5
+ const TIMEOUT_MS = 3000;
6
+ const TTL_MS = 12 * 60 * 60 * 1000;
7
+
8
+ function parse(v) {
9
+ const m = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(String(v));
10
+ return m && { nums: m.slice(1, 4).map(Number), pre: Boolean(m[4]) };
11
+ }
12
+
13
+ // A stable release is newer than its own prereleases. A prerelease is never offered.
14
+ function isNewer(latest, current) {
15
+ const a = parse(latest);
16
+ const b = parse(current);
17
+ if (!a || !b || a.pre) return false;
18
+ for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] > b.nums[i];
19
+ return b.pre;
20
+ }
21
+
22
+ async function fetchLatest() {
23
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(TIMEOUT_MS) });
24
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
25
+ return (await res.json()).version;
26
+ }
27
+
28
+ function createUpdateCheck({ current, enabled = true, fetch = fetchLatest, now = Date.now, ttlMs = TTL_MS }) {
29
+ let latest = null;
30
+ let checkedAt = -Infinity;
31
+ let inflight = null;
32
+
33
+ function refresh() {
34
+ if (!enabled) return Promise.resolve();
35
+ if (inflight) return inflight;
36
+ checkedAt = now();
37
+ inflight = (async () => {
38
+ latest = await fetch();
39
+ })()
40
+ .catch(() => {})
41
+ .finally(() => {
42
+ inflight = null;
43
+ });
44
+ return inflight;
45
+ }
46
+
47
+ async function update() {
48
+ if (now() - checkedAt >= ttlMs) refresh();
49
+ await inflight;
50
+ return isNewer(latest, current) ? { latest, url: RELEASE_URL + latest } : null;
51
+ }
52
+
53
+ return { refresh, update };
54
+ }
55
+
56
+ module.exports = { createUpdateCheck, isNewer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hub",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
5
5
  "main": "server.js",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -229,6 +229,48 @@ async function init() {
229
229
  listenKeys();
230
230
  bindPalette();
231
231
  registerSW();
232
+ showUpdateToast();
233
+ }
234
+
235
+ // One key, overwritten by each dismissal: a dismissed version stays hidden until a newer one ships.
236
+ const UPDATE_DISMISSED_KEY = 'hub-update-dismissed';
237
+ const UPDATE_TOAST_MS = 10_000;
238
+
239
+ async function showUpdateToast() {
240
+ let info;
241
+ try {
242
+ info = await sendJson('GET', '/api/update');
243
+ } catch {
244
+ return;
245
+ }
246
+ const latest = info.update?.latest;
247
+ let dismissed = null;
248
+ try {
249
+ dismissed = localStorage.getItem(UPDATE_DISMISSED_KEY);
250
+ } catch {}
251
+ if (!latest || latest === dismissed) return;
252
+ const toast = document.getElementById('update-toast');
253
+ document.getElementById('update-latest').textContent = latest;
254
+ document.getElementById('update-current').textContent = info.version;
255
+ document.getElementById('update-notes').href = info.update.url;
256
+ let timer = 0;
257
+ const hide = () => {
258
+ clearTimeout(timer);
259
+ toast.classList.add('leaving');
260
+ setTimeout(() => (toast.hidden = true), 200);
261
+ };
262
+ const arm = () => (timer = setTimeout(hide, UPDATE_TOAST_MS));
263
+ toast.addEventListener('mouseenter', () => clearTimeout(timer));
264
+ toast.addEventListener('mouseleave', arm);
265
+ document.getElementById('update-close').addEventListener('click', hide);
266
+ document.getElementById('update-skip').addEventListener('click', () => {
267
+ try {
268
+ localStorage.setItem(UPDATE_DISMISSED_KEY, latest);
269
+ } catch {}
270
+ hide();
271
+ });
272
+ toast.hidden = false;
273
+ arm();
232
274
  }
233
275
 
234
276
  // The hub has no visible chrome, so the window/tab title is the only place the active config dir
package/public/index.html CHANGED
@@ -322,6 +322,60 @@
322
322
  }
323
323
 
324
324
  .palette-hint:empty { display: none; }
325
+
326
+ .update-toast {
327
+ position: fixed;
328
+ right: 16px;
329
+ bottom: 16px;
330
+ z-index: 15;
331
+ display: flex;
332
+ align-items: flex-start;
333
+ gap: 12px;
334
+ width: min(440px, calc(100vw - 32px));
335
+ padding: 16px 18px;
336
+ background: var(--surface);
337
+ border: 1px solid var(--border);
338
+ border-left: 3px solid var(--accent);
339
+ border-radius: 8px;
340
+ box-shadow: 0 10px 32px var(--shadow);
341
+ color: var(--text);
342
+ font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
343
+ transition: opacity 0.2s, transform 0.2s;
344
+ }
345
+
346
+ .update-toast[hidden] { display: none; }
347
+ .update-toast.leaving { opacity: 0; transform: translateY(8px); }
348
+ .update-toast-body { flex: 1; min-width: 0; }
349
+ .update-toast-title { font-weight: 600; font-size: 15px; }
350
+ .update-toast-meta { color: var(--text-dim); }
351
+ .update-toast code { display: block; margin-top: 8px; color: var(--accent); user-select: all; }
352
+ .update-toast a { color: var(--accent); }
353
+ .update-toast-actions { margin-top: 12px; }
354
+
355
+ .update-toast-skip {
356
+ padding: 5px 12px;
357
+ border: 1px solid var(--border);
358
+ border-radius: 6px;
359
+ background: transparent;
360
+ color: var(--text-dim);
361
+ font: inherit;
362
+ font-size: 13px;
363
+ cursor: pointer;
364
+ }
365
+
366
+ .update-toast-skip:hover { color: var(--text); background: var(--surface-hover); }
367
+
368
+ .update-toast-close {
369
+ border: none;
370
+ background: none;
371
+ color: var(--text-dim);
372
+ font: inherit;
373
+ font-size: 20px;
374
+ line-height: 1;
375
+ cursor: pointer;
376
+ }
377
+
378
+ .update-toast-close:hover { color: var(--text); }
325
379
  </style>
326
380
  </head>
327
381
  <body>
@@ -382,6 +436,18 @@
382
436
  </div>
383
437
  </div>
384
438
 
439
+ <div class="update-toast" id="update-toast" role="status" hidden>
440
+ <div class="update-toast-body">
441
+ <div class="update-toast-title">Claude Code Hub <span id="update-latest"></span> is available</div>
442
+ <div class="update-toast-meta">You have <span id="update-current"></span> · <a id="update-notes" target="_blank" rel="noopener">Release notes</a></div>
443
+ <code>npm i -g claude-code-hub@latest</code>
444
+ <div class="update-toast-actions">
445
+ <button class="update-toast-skip" id="update-skip" type="button">Skip this version</button>
446
+ </div>
447
+ </div>
448
+ <button class="update-toast-close" id="update-close" type="button" aria-label="Close">×</button>
449
+ </div>
450
+
385
451
  <script src="/app.js"></script>
386
452
  </body>
387
453
  </html>
package/server.js CHANGED
@@ -15,6 +15,8 @@ const { createNetGuard, KEEP_ALIVE_MS } = require('./lib/net-guard');
15
15
  const { ping } = require('./lib/ping');
16
16
  const { killChild } = require('./lib/kill-child');
17
17
  const { readRegistry, themeConfig } = require('./lib/themes');
18
+ const { createUpdateCheck } = require('./lib/update-check');
19
+ const { version: HUB_VERSION } = require('./package.json');
18
20
 
19
21
  function getArg(name) {
20
22
  const idx = process.argv.findIndex((a) => a.startsWith(`--${name}`));
@@ -842,6 +844,16 @@ function appsConfig() {
842
844
  );
843
845
  }
844
846
 
847
+ // A git checkout updates through git, not npm.
848
+ const updateCheck = createUpdateCheck({
849
+ current: HUB_VERSION,
850
+ enabled: !process.env.CLAUDE_HUB_NO_UPDATE_CHECK && !fs.existsSync(path.join(__dirname, '.git')),
851
+ });
852
+
853
+ app.get('/api/update', async (_req, res) => {
854
+ res.json({ version: HUB_VERSION, update: await updateCheck.update() });
855
+ });
856
+
845
857
  app.get('/api/config', (_req, res) => {
846
858
  res.json({
847
859
  ...themeConfig(THEME_REGISTRY, { userFile: USER_THEMES_FILE }),
@@ -993,6 +1005,7 @@ app.use(express.static(path.join(__dirname, 'public')));
993
1005
  const onReady = (actual) => {
994
1006
  hubPort = actual;
995
1007
  ensurePool(hubConfig.activeConfigDir);
1008
+ updateCheck.refresh();
996
1009
  printBanner(actual);
997
1010
  if (process.argv.includes('--open')) {
998
1011
  import('open').then((m) => m.default(accessUrl(actual)));