@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/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/cssm/v1/.
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 wantedNames = Array.isArray(req.body && req.body.repos)
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
- if (wantedRepos.length === 0) {
322
- return fail('No repos selected and no defaults available');
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 (_req, res) => {
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
- const { server, port } = await listenWithFallback(cfg.port);
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/cssm/v1/';
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}`);