@bakapiano/ccsm 0.3.0 → 0.5.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
@@ -4,7 +4,8 @@
4
4
  const path = require('node:path');
5
5
  const express = require('express');
6
6
 
7
- const { listSessions } = require('./lib/sessions');
7
+ const { listSessions, listRecentSessions, findSessionMetadata } = require('./lib/sessions');
8
+ const { listFavorites, addFavorite, removeFavorite, loadFavorites } = require('./lib/favorites');
8
9
  const { loadConfig, saveConfig, DATA_DIR } = require('./lib/config');
9
10
  const {
10
11
  saveSnapshot,
@@ -26,6 +27,7 @@ const {
26
27
  } = require('./lib/launcher');
27
28
  const {
28
29
  focusByPid,
30
+ focusBySession,
29
31
  snapshotWindowsOf,
30
32
  focusNewlyOpenedHwnd,
31
33
  } = require('./lib/focus');
@@ -61,6 +63,48 @@ app.get('/api/sessions', asyncH(async (_req, res) => {
61
63
  res.json({ sessions, takenAt: Date.now() });
62
64
  }));
63
65
 
66
+ app.get('/api/sessions/recent', asyncH(async (req, res) => {
67
+ const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 15));
68
+ const offset = Math.max(0, Number(req.query.offset) || 0);
69
+ const live = await listSessions();
70
+ const excludeIds = new Set(live.map((s) => s.sessionId));
71
+ const { recent, total } = await listRecentSessions({ limit, offset, excludeIds });
72
+ res.json({ recent, total, limit, offset, takenAt: Date.now() });
73
+ }));
74
+
75
+ // ---- favorites ----
76
+ // Sessions the user has starred. Stored at $DATA_DIR/favorites.json.
77
+ // Frontend usually GETs once at boot and updates optimistically.
78
+ app.get('/api/favorites', asyncH(async (_req, res) => {
79
+ const favorites = await listFavorites();
80
+ res.json({ favorites });
81
+ }));
82
+
83
+ app.post('/api/favorites/:sessionId', asyncH(async (req, res) => {
84
+ const sessionId = req.params.sessionId;
85
+ let info = req.body && typeof req.body === 'object' ? req.body : {};
86
+ // If client didn't supply title/cwd, try to look them up from the live
87
+ // session list or from the jsonl files on disk. This way star-from-empty
88
+ // (e.g. via API) still produces a usable favorite.
89
+ if (!info.cwd || !info.title) {
90
+ const live = await listSessions();
91
+ const livehit = live.find((s) => s.sessionId === sessionId);
92
+ if (livehit) {
93
+ info = { cwd: livehit.cwd, title: livehit.title, ...info };
94
+ } else {
95
+ const meta = await findSessionMetadata(sessionId);
96
+ if (meta) info = { cwd: meta.cwd, title: meta.title, gitBranch: meta.gitBranch, ...info };
97
+ }
98
+ }
99
+ const fav = await addFavorite(sessionId, info);
100
+ res.json({ favorite: fav });
101
+ }));
102
+
103
+ app.delete('/api/favorites/:sessionId', asyncH(async (req, res) => {
104
+ const removed = await removeFavorite(req.params.sessionId);
105
+ res.json({ removed });
106
+ }));
107
+
64
108
  // ---- config ----
65
109
 
66
110
  app.get('/api/config', asyncH(async (_req, res) => {
@@ -296,8 +340,13 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
296
340
  const sessions = await listSessions();
297
341
  const s = sessions.find((x) => x.sessionId === sessionId);
298
342
  if (!s) return res.status(404).json({ error: `session ${sessionId} not live` });
299
- const result = await focusByPid(s.pid);
300
- res.json({ session: { pid: s.pid, sessionId: s.sessionId, cwd: s.cwd }, ...result });
343
+ const result = await focusBySession({
344
+ pid: s.pid,
345
+ sessionId: s.sessionId,
346
+ title: s.title,
347
+ cwd: s.cwd,
348
+ });
349
+ res.json({ session: { pid: s.pid, sessionId: s.sessionId, cwd: s.cwd, title: s.title }, ...result });
301
350
  }));
302
351
 
303
352
  // ---- terminal kinds ----
@@ -359,7 +408,7 @@ function findAppModeBrowser() {
359
408
  }
360
409
 
361
410
  function openInBrowser(url, mode) {
362
- if (process.platform !== 'win32' || mode === 'none') return;
411
+ if (mode === 'none' || process.platform !== 'win32') return { kind: 'none', child: null };
363
412
  const { spawn } = require('node:child_process');
364
413
  const fs = require('node:fs');
365
414
 
@@ -376,25 +425,28 @@ function openInBrowser(url, mode) {
376
425
  [
377
426
  `--app=${url}`,
378
427
  `--user-data-dir=${profileDir}`,
379
- '--window-size=1400,1000',
428
+ '--window-size=1500,1100',
380
429
  '--no-first-run',
381
430
  '--no-default-browser-check',
382
431
  ],
383
432
  { detached: true, stdio: 'ignore' }
384
433
  );
385
434
  child.unref();
386
- return;
435
+ return { kind: 'app', child };
387
436
  }
388
437
  console.log('[ccsm] no Edge/Chrome found for app mode, falling back to default browser');
389
438
  }
390
439
 
391
- // mode === 'tab' (or app-mode fallback)
440
+ // mode === 'tab' (or app-mode fallback). cmd's `start` builtin exits
441
+ // immediately after launching the default browser — the child handle
442
+ // isn't usable for lifecycle tracking.
392
443
  const child = spawn('cmd.exe', ['/c', 'start', '', url], {
393
444
  detached: true,
394
445
  stdio: 'ignore',
395
446
  windowsHide: true,
396
447
  });
397
448
  child.unref();
449
+ return { kind: 'tab', child: null };
398
450
  }
399
451
 
400
452
  (async () => {
@@ -406,7 +458,19 @@ function openInBrowser(url, mode) {
406
458
  console.log(`work dir: ${cfg.workDir}`);
407
459
  console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
408
460
  const mode = cfg.browserMode || (cfg.autoOpenBrowser === false ? 'none' : 'app');
409
- openInBrowser(url, mode);
461
+ const opened = openInBrowser(url, mode);
462
+
463
+ // Interactive npx-style launch: tie server lifetime to the chromeless
464
+ // browser window. When the user closes the window, msedge.exe (with its
465
+ // own --user-data-dir process group) exits — we hear that and shut down
466
+ // so the terminal returns. Headless / nohup launches stay running.
467
+ if (opened.kind === 'app' && opened.child && process.stdout.isTTY) {
468
+ opened.child.on('exit', () => {
469
+ console.log('[ccsm] browser window closed · shutting down');
470
+ process.exit(0);
471
+ });
472
+ console.log('[ccsm] tied to browser window — close it to stop ccsm');
473
+ }
410
474
  startSnapshotLoop();
411
475
  })().catch((err) => {
412
476
  console.error('startup failed:', err);