@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 +2 -0
- package/lib/config.js +1 -0
- package/package.json +1 -1
- package/public/app.js +2 -0
- package/public/index.html +4 -0
- package/server.js +39 -6
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.
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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);
|