@tom2012/cc-web 1.5.10

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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/backend/dist/auth.d.ts +15 -0
  4. package/backend/dist/auth.d.ts.map +1 -0
  5. package/backend/dist/auth.js +92 -0
  6. package/backend/dist/auth.js.map +1 -0
  7. package/backend/dist/config.d.ts +33 -0
  8. package/backend/dist/config.d.ts.map +1 -0
  9. package/backend/dist/config.js +155 -0
  10. package/backend/dist/config.js.map +1 -0
  11. package/backend/dist/index.d.ts +3 -0
  12. package/backend/dist/index.d.ts.map +1 -0
  13. package/backend/dist/index.js +499 -0
  14. package/backend/dist/index.js.map +1 -0
  15. package/backend/dist/routes/auth.d.ts +3 -0
  16. package/backend/dist/routes/auth.d.ts.map +1 -0
  17. package/backend/dist/routes/auth.js +108 -0
  18. package/backend/dist/routes/auth.js.map +1 -0
  19. package/backend/dist/routes/filesystem.d.ts +3 -0
  20. package/backend/dist/routes/filesystem.d.ts.map +1 -0
  21. package/backend/dist/routes/filesystem.js +243 -0
  22. package/backend/dist/routes/filesystem.js.map +1 -0
  23. package/backend/dist/routes/projects.d.ts +3 -0
  24. package/backend/dist/routes/projects.d.ts.map +1 -0
  25. package/backend/dist/routes/projects.js +235 -0
  26. package/backend/dist/routes/projects.js.map +1 -0
  27. package/backend/dist/routes/shortcuts.d.ts +3 -0
  28. package/backend/dist/routes/shortcuts.d.ts.map +1 -0
  29. package/backend/dist/routes/shortcuts.js +88 -0
  30. package/backend/dist/routes/shortcuts.js.map +1 -0
  31. package/backend/dist/routes/update.d.ts +3 -0
  32. package/backend/dist/routes/update.d.ts.map +1 -0
  33. package/backend/dist/routes/update.js +104 -0
  34. package/backend/dist/routes/update.js.map +1 -0
  35. package/backend/dist/session-manager.d.ts +47 -0
  36. package/backend/dist/session-manager.d.ts.map +1 -0
  37. package/backend/dist/session-manager.js +345 -0
  38. package/backend/dist/session-manager.js.map +1 -0
  39. package/backend/dist/terminal-manager.d.ts +27 -0
  40. package/backend/dist/terminal-manager.d.ts.map +1 -0
  41. package/backend/dist/terminal-manager.js +211 -0
  42. package/backend/dist/terminal-manager.js.map +1 -0
  43. package/backend/dist/types.d.ts +17 -0
  44. package/backend/dist/types.d.ts.map +1 -0
  45. package/backend/dist/types.js +3 -0
  46. package/backend/dist/types.js.map +1 -0
  47. package/backend/dist/usage-terminal.d.ts +18 -0
  48. package/backend/dist/usage-terminal.d.ts.map +1 -0
  49. package/backend/dist/usage-terminal.js +189 -0
  50. package/backend/dist/usage-terminal.js.map +1 -0
  51. package/backend/package-lock.json +1965 -0
  52. package/backend/package.json +31 -0
  53. package/bin/ccweb.js +478 -0
  54. package/electron/dist/main.js +455 -0
  55. package/frontend/dist/assets/index-CQjbS4zv.css +32 -0
  56. package/frontend/dist/assets/index-CtyR65A4.js +434 -0
  57. package/frontend/dist/index.html +14 -0
  58. package/frontend/dist/terminal.svg +4 -0
  59. package/package.json +88 -0
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "cc-web-backend",
3
+ "version": "1.0.0",
4
+ "scripts": {
5
+ "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
6
+ "build": "tsc",
7
+ "start": "node dist/index.js",
8
+ "postinstall": "chmod +x node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper 2>/dev/null || true"
9
+ },
10
+ "dependencies": {
11
+ "bcryptjs": "^2.4.3",
12
+ "cors": "^2.8.5",
13
+ "express": "^4.18.2",
14
+ "jsonwebtoken": "^9.0.2",
15
+ "node-pty": "^1.0.0",
16
+ "uuid": "^9.0.1",
17
+ "ws": "^8.16.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bcryptjs": "^2.4.6",
21
+ "@types/cors": "^2.8.17",
22
+ "@types/express": "^4.17.21",
23
+ "@types/jsonwebtoken": "^9.0.5",
24
+ "@types/node": "^20.11.5",
25
+ "@types/uuid": "^9.0.7",
26
+ "@types/ws": "^8.5.10",
27
+ "ts-node": "^10.9.2",
28
+ "ts-node-dev": "^2.0.0",
29
+ "typescript": "^5.3.3"
30
+ }
31
+ }
package/bin/ccweb.js ADDED
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { fork, execSync, execFileSync } = require('child_process');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const os = require('os');
8
+ const readline = require('readline');
9
+ const crypto = require('crypto');
10
+
11
+ // ── Paths ─────────────────────────────────────────────────────────────────────
12
+
13
+ const PKG_ROOT = path.join(__dirname, '..');
14
+ const BACKEND_ENTRY = path.join(PKG_ROOT, 'backend', 'dist', 'index.js');
15
+ const BACKEND_DIR = path.join(PKG_ROOT, 'backend');
16
+
17
+ /** All user data lives in ~/.ccweb — survives package updates */
18
+ const DATA_DIR = path.join(os.homedir(), '.ccweb');
19
+ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
20
+ const PID_FILE = path.join(DATA_DIR, 'ccweb.pid');
21
+ const PORT_FILE = path.join(DATA_DIR, 'ccweb.port');
22
+ const LOG_FILE = path.join(DATA_DIR, 'ccweb.log');
23
+ const PREFS_FILE = path.join(DATA_DIR, 'prefs.json');
24
+
25
+ const LAUNCHD_LABEL = 'com.ccweb.server';
26
+ const LAUNCHD_PLIST = path.join(
27
+ os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`
28
+ );
29
+ const SYSTEMD_SERVICE = path.join(
30
+ os.homedir(), '.config', 'systemd', 'user', 'ccweb.service'
31
+ );
32
+
33
+ // ── Helpers ───────────────────────────────────────────────────────────────────
34
+
35
+ function ensureDataDir() {
36
+ fs.mkdirSync(DATA_DIR, { recursive: true });
37
+ }
38
+
39
+ function readPrefs() {
40
+ try { return JSON.parse(fs.readFileSync(PREFS_FILE, 'utf-8')); } catch { return {}; }
41
+ }
42
+
43
+ function savePrefs(updates) {
44
+ ensureDataDir();
45
+ const prefs = { ...readPrefs(), ...updates };
46
+ fs.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2), 'utf-8');
47
+ }
48
+
49
+ function readPid() {
50
+ try {
51
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim(), 10);
52
+ return isNaN(pid) ? null : pid;
53
+ } catch { return null; }
54
+ }
55
+
56
+ function readPort() {
57
+ try {
58
+ const port = parseInt(fs.readFileSync(PORT_FILE, 'utf-8').trim(), 10);
59
+ return isNaN(port) ? null : port;
60
+ } catch { return null; }
61
+ }
62
+
63
+ function isProcessRunning(pid) {
64
+ try { process.kill(pid, 0); return true; } catch { return false; }
65
+ }
66
+
67
+ function getStatus() {
68
+ const pid = readPid();
69
+ if (!pid) return { running: false };
70
+ if (!isProcessRunning(pid)) {
71
+ try { fs.unlinkSync(PID_FILE); } catch {}
72
+ try { fs.unlinkSync(PORT_FILE); } catch {}
73
+ return { running: false };
74
+ }
75
+ return { running: true, pid, port: readPort() };
76
+ }
77
+
78
+ function ask(question, defaultVal) {
79
+ return new Promise((resolve) => {
80
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
81
+ const hint = defaultVal !== undefined ? ` (default: ${defaultVal})` : '';
82
+ rl.question(question + hint + ': ', (answer) => {
83
+ rl.close();
84
+ resolve(answer.trim() || defaultVal || '');
85
+ });
86
+ });
87
+ }
88
+
89
+ function askYN(question, defaultYes = true) {
90
+ const hint = defaultYes ? '(Y/n)' : '(y/N)';
91
+ return new Promise((resolve) => {
92
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
93
+ rl.question(`${question} ${hint} `, (answer) => {
94
+ rl.close();
95
+ const a = answer.trim().toLowerCase();
96
+ if (!a) resolve(defaultYes);
97
+ else resolve(a === 'y' || a === 'yes');
98
+ });
99
+ });
100
+ }
101
+
102
+ function openBrowser(url) {
103
+ try {
104
+ if (process.platform === 'darwin') execSync(`open ${url}`, { stdio: 'ignore' });
105
+ else if (process.platform === 'win32') execSync(`start "" "${url}"`, { shell: true, stdio: 'ignore' });
106
+ else execSync(`xdg-open ${url}`, { stdio: 'ignore' });
107
+ } catch { /* ignore — browser open is best-effort */ }
108
+ }
109
+
110
+ // ── Dependency check ─────────────────────────────────────────────────────────
111
+
112
+ function checkDependencies() {
113
+ // If backend dist doesn't exist, offer to build
114
+ if (!fs.existsSync(BACKEND_ENTRY)) {
115
+ console.error('Backend not built. Run: npm run build\n');
116
+ console.error(`(from: ${PKG_ROOT})`);
117
+ process.exit(1);
118
+ }
119
+
120
+ // If backend node_modules doesn't exist, install them
121
+ if (!fs.existsSync(path.join(BACKEND_DIR, 'node_modules'))) {
122
+ console.log('Installing backend dependencies (first run)...');
123
+ try {
124
+ execSync('npm install --production', { cwd: BACKEND_DIR, stdio: 'inherit' });
125
+ } catch (err) {
126
+ console.error('Failed to install dependencies:', err.message);
127
+ process.exit(1);
128
+ }
129
+ }
130
+ }
131
+
132
+ function requireBcrypt() {
133
+ // Try backend's bcryptjs first, then global
134
+ for (const p of [
135
+ path.join(BACKEND_DIR, 'node_modules', 'bcryptjs'),
136
+ 'bcryptjs',
137
+ ]) {
138
+ try { return require(p); } catch {}
139
+ }
140
+ console.error('bcryptjs not found. Please run: npm install (in the backend directory)');
141
+ process.exit(1);
142
+ }
143
+
144
+ // ── Setup wizard ──────────────────────────────────────────────────────────────
145
+
146
+ async function runSetup() {
147
+ console.log('\n=== CCWeb Setup ===\n');
148
+
149
+ let username = await ask('Username', 'admin');
150
+ if (!username) username = 'admin';
151
+
152
+ let password;
153
+ while (true) {
154
+ password = await ask('Password (min 6 chars)');
155
+ if (password.length >= 6) break;
156
+ console.log(' Password must be at least 6 characters.');
157
+ }
158
+
159
+ const bcrypt = requireBcrypt();
160
+ const passwordHash = bcrypt.hashSync(password, 12);
161
+ const jwtSecret = crypto.randomBytes(64).toString('hex');
162
+
163
+ ensureDataDir();
164
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ username, passwordHash, jwtSecret }, null, 2), { mode: 0o600 });
165
+
166
+ console.log(`\nCredentials saved (data dir: ${DATA_DIR})`);
167
+ console.log(`Username: ${username}\n`);
168
+ }
169
+
170
+ // ── Auto-start ────────────────────────────────────────────────────────────────
171
+
172
+ async function enableAutoStart() {
173
+ const nodePath = process.execPath;
174
+ const scriptPath = fs.realpathSync(process.argv[1]);
175
+
176
+ if (process.platform === 'darwin') {
177
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
178
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
179
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
180
+ <plist version="1.0">
181
+ <dict>
182
+ <key>Label</key> <string>${LAUNCHD_LABEL}</string>
183
+ <key>ProgramArguments</key>
184
+ <array>
185
+ <string>${nodePath}</string>
186
+ <string>${scriptPath}</string>
187
+ <string>start</string>
188
+ <string>--daemon</string>
189
+ </array>
190
+ <key>RunAtLoad</key> <true/>
191
+ <key>KeepAlive</key> <false/>
192
+ <key>StandardOutPath</key> <string>${LOG_FILE}</string>
193
+ <key>StandardErrorPath</key><string>${LOG_FILE}</string>
194
+ </dict>
195
+ </plist>`;
196
+
197
+ fs.mkdirSync(path.dirname(LAUNCHD_PLIST), { recursive: true });
198
+ fs.writeFileSync(LAUNCHD_PLIST, plist, 'utf-8');
199
+ try {
200
+ execSync(`launchctl load "${LAUNCHD_PLIST}"`, { stdio: 'ignore' });
201
+ console.log('Auto-start enabled (macOS launchd).');
202
+ } catch {
203
+ console.log(`Plist saved. Enable with:\n launchctl load "${LAUNCHD_PLIST}"`);
204
+ }
205
+
206
+ } else if (process.platform === 'linux') {
207
+ const service = [
208
+ '[Unit]',
209
+ 'Description=CCWeb Server',
210
+ '',
211
+ '[Service]',
212
+ `ExecStart=${nodePath} ${scriptPath} start --daemon`,
213
+ `Environment=HOME=${os.homedir()}`,
214
+ 'Restart=no',
215
+ '',
216
+ '[Install]',
217
+ 'WantedBy=default.target',
218
+ ].join('\n');
219
+
220
+ fs.mkdirSync(path.dirname(SYSTEMD_SERVICE), { recursive: true });
221
+ fs.writeFileSync(SYSTEMD_SERVICE, service, 'utf-8');
222
+ try {
223
+ execSync('systemctl --user daemon-reload && systemctl --user enable ccweb', { stdio: 'ignore' });
224
+ console.log('Auto-start enabled (systemd user service).');
225
+ } catch {
226
+ console.log(`Service saved. Enable with:\n systemctl --user enable ccweb`);
227
+ }
228
+
229
+ } else {
230
+ console.log('Auto-start is not yet supported on Windows.');
231
+ }
232
+
233
+ savePrefs({ autoStartConfigured: true });
234
+ }
235
+
236
+ async function disableAutoStart() {
237
+ if (process.platform === 'darwin') {
238
+ try { execSync(`launchctl unload "${LAUNCHD_PLIST}"`, { stdio: 'ignore' }); } catch {}
239
+ try { fs.unlinkSync(LAUNCHD_PLIST); } catch {}
240
+ console.log('Auto-start disabled.');
241
+ } else if (process.platform === 'linux') {
242
+ try { execSync('systemctl --user disable ccweb', { stdio: 'ignore' }); } catch {}
243
+ try { fs.unlinkSync(SYSTEMD_SERVICE); } catch {}
244
+ console.log('Auto-start disabled.');
245
+ }
246
+ savePrefs({ autoStartConfigured: true });
247
+ }
248
+
249
+ // ── Server lifecycle ──────────────────────────────────────────────────────────
250
+
251
+ async function startServer(opts = {}) {
252
+ checkDependencies();
253
+ ensureDataDir();
254
+
255
+ // Already running?
256
+ const status = getStatus();
257
+ if (status.running) {
258
+ console.log(`CCWeb is already running — http://localhost:${status.port} (PID ${status.pid})`);
259
+ if (opts.open !== false) openBrowser(`http://localhost:${status.port}`);
260
+ return;
261
+ }
262
+
263
+ // First-time setup
264
+ if (!fs.existsSync(CONFIG_FILE)) {
265
+ await runSetup();
266
+
267
+ // Ask about auto-start (only on first run)
268
+ const prefs = readPrefs();
269
+ if (!prefs.autoStartConfigured) {
270
+ const doAutoStart = await askYN('Enable auto-start on login?', false);
271
+ if (doAutoStart) await enableAutoStart();
272
+ else savePrefs({ autoStartConfigured: true });
273
+ }
274
+ }
275
+
276
+ // Daemon mode: explicit flag > interactive prompt
277
+ let daemon = opts.daemon;
278
+ if (daemon === undefined) {
279
+ daemon = await askYN('Run in background?', true);
280
+ }
281
+
282
+ console.log('\nStarting CCWeb...');
283
+
284
+ // Open log file for daemon mode
285
+ let outFd, errFd;
286
+ if (daemon) {
287
+ outFd = fs.openSync(LOG_FILE, 'a');
288
+ errFd = fs.openSync(LOG_FILE, 'a');
289
+ }
290
+
291
+ const child = fork(BACKEND_ENTRY, [], {
292
+ env: {
293
+ ...process.env,
294
+ CCWEB_DATA_DIR: DATA_DIR,
295
+ NODE_ENV: 'production',
296
+ },
297
+ cwd: PKG_ROOT,
298
+ stdio: daemon
299
+ ? ['ignore', outFd, errFd, 'ipc']
300
+ : ['inherit', 'inherit', 'inherit', 'ipc'],
301
+ });
302
+
303
+ // Wait for the actual port from the backend
304
+ const port = await new Promise((resolve, reject) => {
305
+ const timeout = setTimeout(
306
+ () => reject(new Error('Server did not start within 20 s')),
307
+ 20000
308
+ );
309
+ child.on('message', (msg) => {
310
+ if (msg && msg.type === 'server-port' && msg.port) {
311
+ clearTimeout(timeout);
312
+ resolve(msg.port);
313
+ }
314
+ });
315
+ child.on('error', (err) => { clearTimeout(timeout); reject(err); });
316
+ child.on('exit', (code) => {
317
+ clearTimeout(timeout);
318
+ reject(new Error(`Server exited unexpectedly (code ${code})`));
319
+ });
320
+ });
321
+
322
+ if (daemon && outFd !== undefined) { try { fs.closeSync(outFd); } catch {} }
323
+ if (daemon && errFd !== undefined) { try { fs.closeSync(errFd); } catch {} }
324
+
325
+ // Persist state
326
+ fs.writeFileSync(PID_FILE, String(child.pid), 'utf-8');
327
+ fs.writeFileSync(PORT_FILE, String(port), 'utf-8');
328
+
329
+ console.log(`\nCCWeb running at http://localhost:${port}`);
330
+ openBrowser(`http://localhost:${port}`);
331
+
332
+ if (daemon) {
333
+ child.disconnect(); // close IPC; child keeps running
334
+ child.unref(); // let parent exit without waiting
335
+ console.log(`Running in background PID ${child.pid}`);
336
+ console.log(`Logs : ${LOG_FILE}`);
337
+ console.log(`Stop : ccweb stop\n`);
338
+ process.exit(0);
339
+ } else {
340
+ console.log('Press Ctrl+C to stop.\n');
341
+ const cleanup = () => {
342
+ try { fs.unlinkSync(PID_FILE); } catch {}
343
+ try { fs.unlinkSync(PORT_FILE); } catch {}
344
+ try { child.kill(); } catch {}
345
+ process.exit(0);
346
+ };
347
+ process.on('SIGINT', cleanup);
348
+ process.on('SIGTERM', cleanup);
349
+ child.on('exit', () => {
350
+ try { fs.unlinkSync(PID_FILE); } catch {}
351
+ try { fs.unlinkSync(PORT_FILE); } catch {}
352
+ });
353
+ }
354
+ }
355
+
356
+ function stopServer() {
357
+ const status = getStatus();
358
+ if (!status.running) { console.log('CCWeb is not running.'); return; }
359
+ try {
360
+ process.kill(status.pid, 'SIGTERM');
361
+ try { fs.unlinkSync(PID_FILE); } catch {}
362
+ try { fs.unlinkSync(PORT_FILE); } catch {}
363
+ console.log(`Stopped (PID ${status.pid}).`);
364
+ } catch (err) {
365
+ console.error('Failed to stop:', err.message);
366
+ }
367
+ }
368
+
369
+ function showStatus() {
370
+ const status = getStatus();
371
+ if (status.running) {
372
+ console.log(`Status : running`);
373
+ console.log(`PID : ${status.pid}`);
374
+ console.log(`URL : http://localhost:${status.port}`);
375
+ console.log(`Data : ${DATA_DIR}`);
376
+ console.log(`Logs : ${LOG_FILE}`);
377
+ } else {
378
+ console.log('Status : stopped');
379
+ console.log(`Data : ${DATA_DIR}`);
380
+ }
381
+ }
382
+
383
+ function showHelp() {
384
+ console.log(`
385
+ CCWeb — Self-hosted Claude Code web interface
386
+
387
+ Usage:
388
+ ccweb Start server (interactive)
389
+ ccweb start Start server (interactive)
390
+ ccweb start --daemon Start in background (no prompt)
391
+ ccweb start --foreground Start in foreground (no prompt)
392
+ ccweb stop Stop background server
393
+ ccweb status Show running status
394
+ ccweb open Open browser to running server
395
+ ccweb setup Reconfigure username / password
396
+ ccweb enable-autostart Enable auto-start on login
397
+ ccweb disable-autostart Disable auto-start on login
398
+ ccweb logs Tail log file (background mode)
399
+
400
+ Data directory : ${DATA_DIR}
401
+ Config file : ${CONFIG_FILE}
402
+ Log file : ${LOG_FILE}
403
+ `);
404
+ }
405
+
406
+ // ── Entry point ───────────────────────────────────────────────────────────────
407
+
408
+ const args = process.argv.slice(2);
409
+ const command = args.find((a) => !a.startsWith('--')) || 'start';
410
+ const isDaemon = args.includes('--daemon');
411
+ const isForeground = args.includes('--foreground');
412
+
413
+ (async () => {
414
+ try {
415
+ switch (command) {
416
+ case 'start':
417
+ await startServer({
418
+ daemon: isDaemon ? true : isForeground ? false : undefined,
419
+ });
420
+ break;
421
+
422
+ case 'stop':
423
+ stopServer();
424
+ break;
425
+
426
+ case 'status':
427
+ showStatus();
428
+ break;
429
+
430
+ case 'open': {
431
+ const s = getStatus();
432
+ if (!s.running) { console.log('CCWeb is not running. Start it with: ccweb start'); break; }
433
+ openBrowser(`http://localhost:${s.port}`);
434
+ console.log(`Opening http://localhost:${s.port}`);
435
+ break;
436
+ }
437
+
438
+ case 'setup':
439
+ await runSetup();
440
+ break;
441
+
442
+ case 'enable-autostart':
443
+ await enableAutoStart();
444
+ break;
445
+
446
+ case 'disable-autostart':
447
+ await disableAutoStart();
448
+ break;
449
+
450
+ case 'logs': {
451
+ if (!fs.existsSync(LOG_FILE)) { console.log('No log file found.'); break; }
452
+ // Tail the log file (cross-platform)
453
+ try {
454
+ if (process.platform === 'win32') {
455
+ execSync(`Get-Content -Wait "${LOG_FILE}"`, { shell: 'powershell', stdio: 'inherit' });
456
+ } else {
457
+ execFileSync('tail', ['-f', LOG_FILE], { stdio: 'inherit' });
458
+ }
459
+ } catch {}
460
+ break;
461
+ }
462
+
463
+ case 'help':
464
+ case '--help':
465
+ case '-h':
466
+ showHelp();
467
+ break;
468
+
469
+ default:
470
+ console.error(`Unknown command: ${command}`);
471
+ showHelp();
472
+ process.exit(1);
473
+ }
474
+ } catch (err) {
475
+ console.error('\nError:', err.message || err);
476
+ process.exit(1);
477
+ }
478
+ })();