claude-code-hub 0.2.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/README.md +54 -0
- package/package.json +54 -0
- package/public/app.js +116 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-192.svg +9 -0
- package/public/icons/icon-512.png +0 -0
- package/public/index.html +89 -0
- package/public/manifest.json +13 -0
- package/public/sw.js +28 -0
- package/server.js +117 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Claude Code Hub
|
|
2
|
+
|
|
3
|
+
Unified launcher for Claude Code tools — browse plugins in **Marketplace** and track tasks in **Kanban**, all from a single chromeless PWA.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone --recurse-submodules https://github.com/NikiforovAll/claude-code-hub.git
|
|
9
|
+
cd claude-code-hub
|
|
10
|
+
npm install && npm install --prefix marketplace && npm install --prefix cck
|
|
11
|
+
npm start # http://localhost:3455
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Keyboard Shortcuts
|
|
15
|
+
|
|
16
|
+
| Shortcut | Action |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `Alt+1` | Switch to Marketplace |
|
|
19
|
+
| `Alt+2` | Switch to Kanban |
|
|
20
|
+
| `Ctrl+Alt+Right` | Switch to next tool |
|
|
21
|
+
| `Ctrl+Alt+Left` | Switch to previous tool |
|
|
22
|
+
|
|
23
|
+
## How It Works
|
|
24
|
+
|
|
25
|
+
The hub server spawns both sub-apps as child processes, each on its own port. A minimal shell page embeds them in iframes and switches visibility on tab change — zero UI chrome, just keyboard shortcuts.
|
|
26
|
+
|
|
27
|
+
Sub-apps communicate with the hub via `postMessage`:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
// From inside a sub-app, trigger cross-app navigation:
|
|
31
|
+
hubNavigate('marketplace', '?project=/path/to/project');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The Kanban sidebar shows a marketplace button on session cards — click it to jump to the Marketplace pre-filtered to that project.
|
|
35
|
+
|
|
36
|
+
## Included Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Submodule | Default Port |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| [Marketplace](https://github.com/NikiforovAll/claude-code-marketplace) | `marketplace/` | 3457 |
|
|
41
|
+
| [Kanban](https://github.com/NikiforovAll/claude-task-viewer) | `cck/` | 3456 |
|
|
42
|
+
|
|
43
|
+
## CLI Flags
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
--port <n> Hub port (default: 3455)
|
|
47
|
+
--marketplace-port <n> Marketplace port (default: 3457)
|
|
48
|
+
--kanban-port <n> Kanban port (default: 3456)
|
|
49
|
+
--open Auto-open browser
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-hub",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Unified hub for Claude Code tools — Marketplace + Kanban in one PWA",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-code-hub": "./server.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node server.js",
|
|
11
|
+
"dev": "node server.js --open",
|
|
12
|
+
"lint": "npx @biomejs/biome check public/app.js server.js",
|
|
13
|
+
"lint:fix": "npx @biomejs/biome check --fix public/app.js server.js",
|
|
14
|
+
"prepare": "husky"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/NikiforovAll/claude-code-hub.git"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"hub",
|
|
25
|
+
"dashboard",
|
|
26
|
+
"marketplace",
|
|
27
|
+
"kanban",
|
|
28
|
+
"pwa",
|
|
29
|
+
"developer-tools"
|
|
30
|
+
],
|
|
31
|
+
"author": "NikiforovAll",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/NikiforovAll/claude-code-hub/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/NikiforovAll/claude-code-hub#readme",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"claude-code-kanban": "^2.2.0",
|
|
39
|
+
"claude-code-marketplace": "^0.5.0",
|
|
40
|
+
"express": "^4.21.0",
|
|
41
|
+
"open": "^10.1.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"files": [
|
|
47
|
+
"server.js",
|
|
48
|
+
"public/**/*"
|
|
49
|
+
],
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@biomejs/biome": "^2.4.10",
|
|
52
|
+
"husky": "^9.1.7"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
let apps = {};
|
|
2
|
+
let activeApp = null;
|
|
3
|
+
const iframes = {};
|
|
4
|
+
const loadedApps = new Set();
|
|
5
|
+
let allowedOrigins = new Set();
|
|
6
|
+
|
|
7
|
+
async function init() {
|
|
8
|
+
const res = await fetch('/api/config');
|
|
9
|
+
apps = (await res.json()).apps;
|
|
10
|
+
allowedOrigins = new Set(Object.values(apps).map((a) => new URL(a.url).origin));
|
|
11
|
+
buildIframes();
|
|
12
|
+
switchTab(Object.keys(apps)[0]);
|
|
13
|
+
listenMessages();
|
|
14
|
+
listenKeys();
|
|
15
|
+
registerSW();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildIframes() {
|
|
19
|
+
const container = document.getElementById('iframe-container');
|
|
20
|
+
for (const [id, cfg] of Object.entries(apps)) {
|
|
21
|
+
const iframe = document.createElement('iframe');
|
|
22
|
+
iframe.id = `iframe-${id}`;
|
|
23
|
+
iframe.src = cfg.url;
|
|
24
|
+
iframe.className = 'hidden';
|
|
25
|
+
iframe.addEventListener('load', () => onIframeLoad(id));
|
|
26
|
+
container.appendChild(iframe);
|
|
27
|
+
iframes[id] = iframe;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function switchTab(appId) {
|
|
32
|
+
if (!apps[appId]) return;
|
|
33
|
+
activeApp = appId;
|
|
34
|
+
|
|
35
|
+
for (const [id, iframe] of Object.entries(iframes)) {
|
|
36
|
+
iframe.classList.toggle('hidden', id !== appId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const overlay = document.getElementById('loading-overlay');
|
|
40
|
+
if (!loadedApps.has(appId)) {
|
|
41
|
+
overlay.classList.remove('fade-out');
|
|
42
|
+
} else {
|
|
43
|
+
overlay.classList.add('fade-out');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function onIframeLoad(appId) {
|
|
48
|
+
loadedApps.add(appId);
|
|
49
|
+
if (appId === activeApp) {
|
|
50
|
+
document.getElementById('loading-overlay').classList.add('fade-out');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function listenMessages() {
|
|
55
|
+
window.addEventListener('message', (e) => {
|
|
56
|
+
if (!allowedOrigins.has(e.origin)) return;
|
|
57
|
+
const data = e.data ?? {};
|
|
58
|
+
if (data.type === 'hub:navigate') {
|
|
59
|
+
if (!apps[data.app]) return;
|
|
60
|
+
switchTab(data.app);
|
|
61
|
+
if (data.url) iframes[data.app].src = apps[data.app].url + data.url;
|
|
62
|
+
} else if (data.type === 'hub:keydown') {
|
|
63
|
+
if (data.key === 'ArrowLeft') cycleTab(-1);
|
|
64
|
+
else if (data.key === 'ArrowRight') cycleTab(1);
|
|
65
|
+
else {
|
|
66
|
+
const digit = parseInt(data.key, 10);
|
|
67
|
+
if (digit >= 1 && digit <= 9) switchByIndex(digit - 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cycleTab(delta) {
|
|
74
|
+
const ids = Object.keys(apps);
|
|
75
|
+
const idx = ids.indexOf(activeApp);
|
|
76
|
+
const next = ids[(idx + delta + ids.length) % ids.length];
|
|
77
|
+
switchTab(next);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function switchByIndex(idx) {
|
|
81
|
+
const ids = Object.keys(apps);
|
|
82
|
+
if (idx < ids.length) switchTab(ids[idx]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function listenKeys() {
|
|
86
|
+
document.addEventListener('keydown', (e) => {
|
|
87
|
+
if (e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
|
|
88
|
+
const digit = parseInt(e.key, 10);
|
|
89
|
+
if (digit >= 1 && digit <= 9) {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
switchByIndex(digit - 1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey) {
|
|
96
|
+
if (e.key === 'ArrowLeft') {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
cycleTab(-1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (e.key === 'ArrowRight') {
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
cycleTab(1);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function registerSW() {
|
|
111
|
+
if ('serviceWorker' in navigator) {
|
|
112
|
+
navigator.serviceWorker.register('/sw.js');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
init();
|
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
|
2
|
+
<rect width="192" height="192" rx="32" fill="#1a1a2e"/>
|
|
3
|
+
<g transform="translate(96,96)" stroke="#e94560" fill="none" stroke-width="6" stroke-linecap="round" stroke-linejoin="round">
|
|
4
|
+
<rect x="-52" y="-52" width="40" height="40" rx="6"/>
|
|
5
|
+
<rect x="12" y="-52" width="40" height="40" rx="6"/>
|
|
6
|
+
<rect x="-52" y="12" width="40" height="40" rx="6"/>
|
|
7
|
+
<rect x="12" y="12" width="40" height="40" rx="6"/>
|
|
8
|
+
</g>
|
|
9
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<meta name="theme-color" content="#1a1a2e">
|
|
7
|
+
<link rel="manifest" href="/manifest.json">
|
|
8
|
+
<title>Claude Code Hub</title>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #1a1a2e;
|
|
12
|
+
--border: #0f3460;
|
|
13
|
+
--accent: #e94560;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media (prefers-color-scheme: light) {
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #f4f4f5;
|
|
19
|
+
--border: #e4e4e7;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
background: var(--bg);
|
|
27
|
+
height: 100vh;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.iframe-container {
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
position: relative;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.iframe-container iframe {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
left: 0;
|
|
41
|
+
width: 100%;
|
|
42
|
+
height: 100%;
|
|
43
|
+
border: none;
|
|
44
|
+
background: var(--bg);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.iframe-container iframe.hidden {
|
|
48
|
+
visibility: hidden;
|
|
49
|
+
pointer-events: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.loading-overlay {
|
|
53
|
+
position: absolute;
|
|
54
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
background: var(--bg);
|
|
59
|
+
z-index: 10;
|
|
60
|
+
transition: opacity 0.3s ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.loading-overlay.fade-out {
|
|
64
|
+
opacity: 0;
|
|
65
|
+
pointer-events: none;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.spinner {
|
|
69
|
+
width: 28px;
|
|
70
|
+
height: 28px;
|
|
71
|
+
border: 3px solid var(--border);
|
|
72
|
+
border-top-color: var(--accent);
|
|
73
|
+
border-radius: 50%;
|
|
74
|
+
animation: spin 0.8s linear infinite;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<div class="iframe-container" id="iframe-container">
|
|
82
|
+
<div class="loading-overlay" id="loading-overlay">
|
|
83
|
+
<div class="spinner"></div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<script src="/app.js"></script>
|
|
88
|
+
</body>
|
|
89
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Claude Code Hub",
|
|
3
|
+
"short_name": "CC Hub",
|
|
4
|
+
"description": "Unified hub for Claude Code tools",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#1a1a2e",
|
|
8
|
+
"theme_color": "#1a1a2e",
|
|
9
|
+
"icons": [
|
|
10
|
+
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
11
|
+
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
|
12
|
+
]
|
|
13
|
+
}
|
package/public/sw.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const CACHE_NAME = 'hub-shell-v1';
|
|
2
|
+
const SHELL_ASSETS = ['/', '/index.html', '/app.js', '/manifest.json'];
|
|
3
|
+
|
|
4
|
+
self.addEventListener('install', (e) => {
|
|
5
|
+
e.waitUntil(
|
|
6
|
+
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_ASSETS))
|
|
7
|
+
);
|
|
8
|
+
self.skipWaiting();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
self.addEventListener('activate', (e) => {
|
|
12
|
+
e.waitUntil(
|
|
13
|
+
caches.keys().then((keys) =>
|
|
14
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
self.clients.claim();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener('fetch', (e) => {
|
|
21
|
+
if (e.request.url.includes('/api/')) {
|
|
22
|
+
e.respondWith(fetch(e.request));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
e.respondWith(
|
|
26
|
+
fetch(e.request).catch(() => caches.match(e.request))
|
|
27
|
+
);
|
|
28
|
+
});
|
package/server.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function getArg(name) {
|
|
8
|
+
const idx = process.argv.findIndex((a) => a.startsWith(`--${name}`));
|
|
9
|
+
if (idx === -1) return null;
|
|
10
|
+
const arg = process.argv[idx];
|
|
11
|
+
if (arg.includes('=')) return arg.split('=').slice(1).join('=');
|
|
12
|
+
return process.argv[idx + 1] || null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const HUB_PORT = parseInt(getArg('port') || process.env.PORT || '3455', 10);
|
|
16
|
+
const MARKETPLACE_PORT = parseInt(getArg('marketplace-port') || '3457', 10);
|
|
17
|
+
const KANBAN_PORT = parseInt(getArg('kanban-port') || '3456', 10);
|
|
18
|
+
|
|
19
|
+
const children = [];
|
|
20
|
+
const actualPorts = { marketplace: MARKETPLACE_PORT, kanban: KANBAN_PORT };
|
|
21
|
+
|
|
22
|
+
function spawnApp(name, cmd, args, envPort) {
|
|
23
|
+
const child = spawn(cmd, args, {
|
|
24
|
+
cwd: __dirname,
|
|
25
|
+
env: {
|
|
26
|
+
...process.env,
|
|
27
|
+
PORT: String(envPort),
|
|
28
|
+
CLAUDE_HUB: '1',
|
|
29
|
+
HUB_URL: `http://localhost:${HUB_PORT}`,
|
|
30
|
+
},
|
|
31
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let stdoutBuf = '';
|
|
35
|
+
child.stdout.on('data', (d) => {
|
|
36
|
+
stdoutBuf += d.toString();
|
|
37
|
+
let nl;
|
|
38
|
+
while ((nl = stdoutBuf.indexOf('\n')) !== -1) {
|
|
39
|
+
const line = stdoutBuf.slice(0, nl + 1);
|
|
40
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
41
|
+
process.stdout.write(`[${name}] ${line}`);
|
|
42
|
+
const match = line.match(/running at http:\/\/localhost:(\d+)/i);
|
|
43
|
+
if (match) actualPorts[name] = parseInt(match[1], 10);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on('data', (d) => process.stderr.write(`[${name}] ${d}`));
|
|
47
|
+
child.on('exit', (code) => console.log(`[${name}] exited (code ${code})`));
|
|
48
|
+
|
|
49
|
+
children.push(child);
|
|
50
|
+
return child;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function killAll() {
|
|
54
|
+
for (const child of children) {
|
|
55
|
+
if (!child.killed) child.kill();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
process.on('SIGINT', () => {
|
|
60
|
+
killAll();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
});
|
|
63
|
+
process.on('SIGTERM', () => {
|
|
64
|
+
killAll();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function resolveApp(submoduleDir, npmPackage) {
|
|
69
|
+
const local = path.join(__dirname, submoduleDir, 'server.js');
|
|
70
|
+
try {
|
|
71
|
+
require.resolve(local);
|
|
72
|
+
return local;
|
|
73
|
+
} catch {}
|
|
74
|
+
return require.resolve(`${npmPackage}/server.js`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const marketplacePath = resolveApp('marketplace', 'claude-code-marketplace');
|
|
78
|
+
const kanbanPath = resolveApp('cck', 'claude-code-kanban');
|
|
79
|
+
|
|
80
|
+
spawnApp('marketplace', process.execPath, [marketplacePath, `--port=${MARKETPLACE_PORT}`], MARKETPLACE_PORT);
|
|
81
|
+
spawnApp('kanban', process.execPath, [kanbanPath], KANBAN_PORT);
|
|
82
|
+
|
|
83
|
+
const app = express();
|
|
84
|
+
|
|
85
|
+
app.get('/api/config', (_req, res) => {
|
|
86
|
+
res.json({
|
|
87
|
+
apps: {
|
|
88
|
+
marketplace: { name: 'Marketplace', url: `http://localhost:${actualPorts.marketplace}`, icon: 'store' },
|
|
89
|
+
kanban: { name: 'Kanban', url: `http://localhost:${actualPorts.kanban}`, icon: 'columns' },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
95
|
+
|
|
96
|
+
const server = app.listen(HUB_PORT, () => {
|
|
97
|
+
const actual = server.address().port;
|
|
98
|
+
console.log(`Claude Code Hub running at http://localhost:${actual}`);
|
|
99
|
+
if (process.argv.includes('--open')) {
|
|
100
|
+
import('open').then((m) => m.default(`http://localhost:${actual}`));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
server.on('error', (err) => {
|
|
105
|
+
if (err.code === 'EADDRINUSE') {
|
|
106
|
+
console.log(`Port ${HUB_PORT} in use, trying random port...`);
|
|
107
|
+
const fallback = app.listen(0, () => {
|
|
108
|
+
const actual = fallback.address().port;
|
|
109
|
+
console.log(`Claude Code Hub running at http://localhost:${actual}`);
|
|
110
|
+
if (process.argv.includes('--open')) {
|
|
111
|
+
import('open').then((m) => m.default(`http://localhost:${actual}`));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
});
|