@bakapiano/ccsm 0.8.3 → 0.9.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/CLAUDE.md +377 -123
- package/README.md +7 -7
- package/bin/ccsm.js +1 -1
- package/lib/config.js +5 -0
- package/lib/webTerminal.js +25 -3
- package/package.json +4 -4
- package/public/css/forms.css +51 -0
- package/public/css/layout.css +5 -0
- package/public/css/sidebar.css +38 -1
- package/public/css/terminals.css +44 -0
- package/public/css/wco.css +29 -0
- package/public/index.html +4 -2
- package/public/js/actions.js +27 -7
- package/public/js/components/NewSessionModal.js +18 -7
- package/public/js/components/OfflineBanner.js +1 -1
- package/public/js/components/Sidebar.js +32 -1
- package/public/js/components/TerminalView.js +166 -0
- package/public/js/pages/AboutPage.js +1 -1
- package/public/js/pages/ConfigurePage.js +72 -1
- package/public/js/pages/LaunchPage.js +35 -23
- package/public/js/state.js +115 -0
- package/scripts/install.js +23 -2
- package/server.js +51 -9
package/server.js
CHANGED
|
@@ -101,7 +101,7 @@ app.use((req, res, next) => {
|
|
|
101
101
|
// so a contributor can iterate without pushing to GH Pages; (b) hot-reload
|
|
102
102
|
// SSE endpoint that watches public/ for changes. CCSM_NO_DEV=1 disables
|
|
103
103
|
// both explicitly. In production (npm-installed), backend is API-only —
|
|
104
|
-
// frontend lives at https://bakapiano.github.io/
|
|
104
|
+
// frontend lives at https://bakapiano.github.io/ccsm/v1/.
|
|
105
105
|
const IS_DEV = !__dirname.includes(`${path.sep}node_modules${path.sep}`) && process.env.CCSM_NO_DEV !== '1';
|
|
106
106
|
|
|
107
107
|
if (IS_DEV) {
|
|
@@ -313,13 +313,17 @@ app.post('/api/sessions/new', async (req, res) => {
|
|
|
313
313
|
|
|
314
314
|
try {
|
|
315
315
|
const cfg = await loadConfig();
|
|
316
|
-
const
|
|
316
|
+
const explicitRepos = Array.isArray(req.body && req.body.repos);
|
|
317
|
+
const wantedNames = explicitRepos
|
|
317
318
|
? req.body.repos
|
|
318
319
|
: cfg.repos.filter((r) => r.defaultSelected).map((r) => r.name);
|
|
319
320
|
|
|
320
321
|
const wantedRepos = cfg.repos.filter((r) => wantedNames.includes(r.name));
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
// Allow launching with zero repos — caller explicitly passed [] (or no
|
|
323
|
+
// defaults exist). The workspace is still created; claude just opens
|
|
324
|
+
// in an empty directory.
|
|
325
|
+
if (wantedRepos.length === 0 && !explicitRepos && wantedNames.length > 0) {
|
|
326
|
+
return fail('No matching repos found');
|
|
323
327
|
}
|
|
324
328
|
|
|
325
329
|
let workspace;
|
|
@@ -433,8 +437,26 @@ app.post('/api/sessions/new', async (req, res) => {
|
|
|
433
437
|
});
|
|
434
438
|
|
|
435
439
|
// ---- launch finder session (a claude session in the ccsm data dir pre-pointed at session data) ----
|
|
436
|
-
app.post('/api/sessions/finder', asyncH(async (
|
|
440
|
+
app.post('/api/sessions/finder', asyncH(async (req, res) => {
|
|
437
441
|
const cfg = await loadConfig();
|
|
442
|
+
const mode = (req.body && req.body.terminal)
|
|
443
|
+
|| cfg.defaultTerminalMode
|
|
444
|
+
|| 'wt';
|
|
445
|
+
if (mode === 'web') {
|
|
446
|
+
if (!webTerminal.available) {
|
|
447
|
+
return res.status(400).json({ error: 'node-pty unavailable · cannot launch finder in web terminal' });
|
|
448
|
+
}
|
|
449
|
+
const cmd = cfg.claudeCommand || 'claude';
|
|
450
|
+
const wrap = (cfg.commandShell || 'pwsh') === 'powershell' ? 'powershell.exe' : 'pwsh.exe';
|
|
451
|
+
const promptArg = cfg.finderPrompt ? ` '${cfg.finderPrompt.replace(/'/g, "''")}'` : '';
|
|
452
|
+
const entry = webTerminal.spawn({
|
|
453
|
+
command: wrap,
|
|
454
|
+
args: ['-NoExit', '-NoLogo', '-Command', `Set-Location -LiteralPath '${DATA_DIR.replace(/'/g, "''")}'; & '${cmd.replace(/'/g, "''")}'${promptArg}`],
|
|
455
|
+
cwd: DATA_DIR,
|
|
456
|
+
meta: { title: 'ccsm finder', cwd: DATA_DIR },
|
|
457
|
+
});
|
|
458
|
+
return res.json({ launched: { mode: 'web', id: entry.id, pid: entry.meta.pid, terminal: 'web' }, cwd: DATA_DIR, prompt: cfg.finderPrompt });
|
|
459
|
+
}
|
|
438
460
|
const beforeHwnds = await snapshotWindowsOf(processNameFor(cfg.terminal) || 'WindowsTerminal.exe');
|
|
439
461
|
const launched = launchNewClaude({
|
|
440
462
|
cwd: DATA_DIR,
|
|
@@ -449,7 +471,7 @@ app.post('/api/sessions/finder', asyncH(async (_req, res) => {
|
|
|
449
471
|
beforeHwnds,
|
|
450
472
|
autoFocus: cfg.autoFocusOnLaunch !== false,
|
|
451
473
|
});
|
|
452
|
-
res.json({ launched, cwd: DATA_DIR, prompt: cfg.finderPrompt });
|
|
474
|
+
res.json({ launched: { mode: 'wt', ...launched }, cwd: DATA_DIR, prompt: cfg.finderPrompt });
|
|
453
475
|
}));
|
|
454
476
|
|
|
455
477
|
// ---- resume single session ----
|
|
@@ -458,6 +480,23 @@ app.post('/api/sessions/:sessionId/resume', asyncH(async (req, res) => {
|
|
|
458
480
|
const cwd = req.body && req.body.cwd;
|
|
459
481
|
if (!cwd) return res.status(400).json({ error: 'cwd required in body' });
|
|
460
482
|
const cfg = await loadConfig();
|
|
483
|
+
const mode = (req.body && req.body.terminal)
|
|
484
|
+
|| cfg.defaultTerminalMode
|
|
485
|
+
|| 'wt';
|
|
486
|
+
if (mode === 'web') {
|
|
487
|
+
if (!webTerminal.available) {
|
|
488
|
+
return res.status(400).json({ error: 'node-pty unavailable · cannot resume in web terminal' });
|
|
489
|
+
}
|
|
490
|
+
const cmd = cfg.claudeCommand || 'claude';
|
|
491
|
+
const wrap = (cfg.commandShell || 'pwsh') === 'powershell' ? 'powershell.exe' : 'pwsh.exe';
|
|
492
|
+
const entry = webTerminal.spawn({
|
|
493
|
+
command: wrap,
|
|
494
|
+
args: ['-NoExit', '-NoLogo', '-Command', `Set-Location -LiteralPath '${cwd.replace(/'/g, "''")}'; & '${cmd.replace(/'/g, "''")}' --resume '${sessionId.replace(/'/g, "''")}'`],
|
|
495
|
+
cwd,
|
|
496
|
+
meta: { title: `resume ${sessionId.slice(0, 8)}`, cwd, sessionId },
|
|
497
|
+
});
|
|
498
|
+
return res.json({ launched: { mode: 'web', id: entry.id, pid: entry.meta.pid, terminal: 'web' } });
|
|
499
|
+
}
|
|
461
500
|
const beforeHwnds = await snapshotWindowsOf(processNameFor(cfg.terminal) || 'WindowsTerminal.exe');
|
|
462
501
|
const launched = launchResume({
|
|
463
502
|
cwd,
|
|
@@ -471,7 +510,7 @@ app.post('/api/sessions/:sessionId/resume', asyncH(async (req, res) => {
|
|
|
471
510
|
beforeHwnds,
|
|
472
511
|
autoFocus: cfg.autoFocusOnLaunch !== false,
|
|
473
512
|
});
|
|
474
|
-
res.json({ launched });
|
|
513
|
+
res.json({ launched: { mode: 'wt', ...launched } });
|
|
475
514
|
}));
|
|
476
515
|
|
|
477
516
|
// ---- focus the wt window that's already hosting this session ----
|
|
@@ -645,7 +684,10 @@ function openInBrowser(url, mode) {
|
|
|
645
684
|
|
|
646
685
|
(async () => {
|
|
647
686
|
const cfg = await loadConfig();
|
|
648
|
-
|
|
687
|
+
// CCSM_PORT env var wins over config — handy for running a dev instance
|
|
688
|
+
// on a non-default port (e.g. 7778) while a prod ccsm keeps 7777.
|
|
689
|
+
const preferredPort = process.env.CCSM_PORT ? Number(process.env.CCSM_PORT) : cfg.port;
|
|
690
|
+
const { server, port } = await listenWithFallback(preferredPort);
|
|
649
691
|
currentPort = port;
|
|
650
692
|
|
|
651
693
|
// WebSocket upgrade for /ws/terminal/:id → bridges xterm.js to a PTY
|
|
@@ -688,7 +730,7 @@ function openInBrowser(url, mode) {
|
|
|
688
730
|
// prod → hosted frontend on GH Pages (backend is API-only)
|
|
689
731
|
const FRONTEND_URL = IS_DEV
|
|
690
732
|
? apiUrl
|
|
691
|
-
: 'https://bakapiano.github.io/
|
|
733
|
+
: 'https://bakapiano.github.io/ccsm/v1/';
|
|
692
734
|
frontendUrl = FRONTEND_URL;
|
|
693
735
|
console.log(`ccsm listening on ${apiUrl}${port !== cfg.port ? ` (requested ${cfg.port}, was taken)` : ''}`);
|
|
694
736
|
console.log(`frontend at ${FRONTEND_URL}`);
|