@bakapiano/ccsm 0.1.0 → 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/CLAUDE.md CHANGED
@@ -19,6 +19,8 @@ Then open http://localhost:7777.
19
19
 
20
20
  Default port `7777`, default workDir `~/ccsm-workspaces`. Config + snapshots live at `~/.ccsm/` (override with `CCSM_HOME=<path>`). All settings editable through the Config panel (`~/.ccsm/config.json` on disk). Notable knobs:
21
21
 
22
+ - `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port. The startup log prints the actual URL so you always see where it ended up.
23
+ - `autoOpenBrowser` (default true) — after `listen()` succeeds, ccsm spawns `cmd /c start "" <url>` to open the UI in the default browser. Disable for headless / nohup setups.
22
24
  - `claudeCommand` (default `"claude"`) — what gets `--resume`'d or freshly invoked inside the new terminal. Can be an exe (`claude`, `claude.exe`), a PowerShell alias or function (`ccp`), or any wrapper script — see `commandShell` below.
23
25
  - `terminal` — `wt` | `powershell` | `pwsh` | `cmd`. wt opens a fresh window per launch (`wt -w new` is set to defeat the "fold into existing window" setting some users have). The other three each spawn via `cmd /c start ... <shell>`.
24
26
  - `commandShell` (default `pwsh`) — only consulted when `terminal=wt`. Values `pwsh` / `powershell` wrap `claudeCommand` inside `<shell> -NoExit -NoLogo -Command "Set-Location ...; & '<cmd>' '<args>'..."` so PowerShell aliases / functions / profile-defined names (like `ccp` from `$PROFILE`) resolve. `none` runs the command directly via wt (raw `CreateProcess`) — fine if `claudeCommand` is an actual exe on PATH, broken for aliases. `pwsh` / `powershell` kinds already wrap natively so this knob doesn't affect them; `cmd` kind has no shell concept for aliases.
package/lib/config.js CHANGED
@@ -22,6 +22,7 @@ const DEFAULTS = {
22
22
  terminal: 'wt',
23
23
  commandShell: 'pwsh',
24
24
  autoFocusOnLaunch: true,
25
+ autoOpenBrowser: true,
25
26
  // Add the repos you most often need on hand. The "new session" button
26
27
  // clones any selected entries into the workspace before launching claude.
27
28
  // Example shape:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
package/public/app.js CHANGED
@@ -165,6 +165,7 @@ function renderConfig() {
165
165
  $('#cfgClaudeCommand').value = state.config.claudeCommand || 'claude';
166
166
  $('#cfgCommandShell').value = state.config.commandShell || 'pwsh';
167
167
  $('#cfgAutoFocus').checked = state.config.autoFocusOnLaunch !== false;
168
+ $('#cfgAutoOpenBrowser').checked = state.config.autoOpenBrowser !== false;
168
169
  const termSel = $('#cfgTerminal');
169
170
  termSel.innerHTML = (state.terminals || []).map((t) =>
170
171
  `<option value="${escapeHtml(t.name)}" ${t.name === state.config.terminal ? 'selected' : ''}>${escapeHtml(t.name)} (${escapeHtml(t.processName)})</option>`
@@ -204,6 +205,7 @@ function readConfigFromForm() {
204
205
  terminal: $('#cfgTerminal').value || 'wt',
205
206
  commandShell: $('#cfgCommandShell').value || 'pwsh',
206
207
  autoFocusOnLaunch: $('#cfgAutoFocus').checked,
208
+ autoOpenBrowser: $('#cfgAutoOpenBrowser').checked,
207
209
  finderPrompt: $('#cfgFinderPrompt').value,
208
210
  repos,
209
211
  };
package/public/index.html CHANGED
@@ -123,6 +123,10 @@
123
123
  <input id="cfgAutoFocus" type="checkbox" />
124
124
  <span style="color: var(--text);">auto-focus newly launched window</span>
125
125
  </label>
126
+ <label class="full" style="flex-direction: row; align-items: center; gap: 8px;">
127
+ <input id="cfgAutoOpenBrowser" type="checkbox" />
128
+ <span style="color: var(--text);">auto-open this UI in browser on server start</span>
129
+ </label>
126
130
  <label class="full">finder prompt
127
131
  <textarea id="cfgFinderPrompt" rows="3"></textarea>
128
132
  </label>
package/server.js CHANGED
@@ -324,14 +324,47 @@ async function startSnapshotLoop() {
324
324
  console.log(`[snapshot] auto-saving every ${Math.round(interval / 1000)}s`);
325
325
  }
326
326
 
327
+ // Try the preferred port, then preferred+1..+9, then let the OS pick a free
328
+ // one. Resolves with the port the server actually bound to.
329
+ function listenWithFallback(preferred) {
330
+ return new Promise((resolve, reject) => {
331
+ const attempt = (port, tries) => {
332
+ const server = app.listen(port);
333
+ server.once('listening', () => resolve({ server, port: server.address().port }));
334
+ server.once('error', (err) => {
335
+ if (err.code !== 'EADDRINUSE') return reject(err);
336
+ if (tries < 9) attempt(port + 1, tries + 1);
337
+ else if (tries === 9) attempt(0, tries + 1); // OS-assigned free port
338
+ else reject(err);
339
+ });
340
+ };
341
+ attempt(preferred, 0);
342
+ });
343
+ }
344
+
345
+ function openInBrowser(url) {
346
+ if (process.platform !== 'win32') return;
347
+ const { spawn } = require('node:child_process');
348
+ // cmd's `start` builtin opens URL in the default browser. The empty "" is
349
+ // the window-title slot for `start`, otherwise `<url>` would be eaten as
350
+ // the title when it has spaces.
351
+ const child = spawn('cmd.exe', ['/c', 'start', '', url], {
352
+ detached: true,
353
+ stdio: 'ignore',
354
+ windowsHide: true,
355
+ });
356
+ child.unref();
357
+ }
358
+
327
359
  (async () => {
328
360
  const cfg = await loadConfig();
329
- app.listen(cfg.port, () => {
330
- console.log(`ccsm listening on http://localhost:${cfg.port}`);
331
- console.log(`data dir: ${DATA_DIR}`);
332
- console.log(`work dir: ${cfg.workDir}`);
333
- console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
334
- });
361
+ const { port } = await listenWithFallback(cfg.port);
362
+ const url = `http://localhost:${port}`;
363
+ console.log(`ccsm listening on ${url}${port !== cfg.port ? ` (requested ${cfg.port}, was taken)` : ''}`);
364
+ console.log(`data dir: ${DATA_DIR}`);
365
+ console.log(`work dir: ${cfg.workDir}`);
366
+ console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
367
+ if (cfg.autoOpenBrowser !== false) openInBrowser(url);
335
368
  startSnapshotLoop();
336
369
  })().catch((err) => {
337
370
  console.error('startup failed:', err);