@bakapiano/ccsm 0.1.0 → 0.3.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 +4 -0
- package/package.json +1 -1
- package/public/app.js +4 -0
- package/public/index.html +7 -0
- package/server.js +81 -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
|
+
- `browserMode` (default `app`) — how to open the UI on server start. `app` finds Edge or Chrome and spawns it with `--app=<url> --user-data-dir=<DATA_DIR>/browser-profile` for a chromeless webview-style window (no tabs, no address bar). `tab` opens the default browser as a regular tab. `none` skips opening. Legacy `autoOpenBrowser: false` still maps to `none` for back-compat.
|
|
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,10 @@ const DEFAULTS = {
|
|
|
22
22
|
terminal: 'wt',
|
|
23
23
|
commandShell: 'pwsh',
|
|
24
24
|
autoFocusOnLaunch: true,
|
|
25
|
+
// 'app' — Edge/Chrome --app=<url> chromeless window (looks like a desktop app)
|
|
26
|
+
// 'tab' — open in default browser as a normal tab
|
|
27
|
+
// 'none' — don't open anything
|
|
28
|
+
browserMode: 'app',
|
|
25
29
|
// Add the repos you most often need on hand. The "new session" button
|
|
26
30
|
// clones any selected entries into the workspace before launching claude.
|
|
27
31
|
// Example shape:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,9 @@ 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
|
+
$('#cfgBrowserMode').value =
|
|
169
|
+
state.config.browserMode ||
|
|
170
|
+
(state.config.autoOpenBrowser === false ? 'none' : 'app');
|
|
168
171
|
const termSel = $('#cfgTerminal');
|
|
169
172
|
termSel.innerHTML = (state.terminals || []).map((t) =>
|
|
170
173
|
`<option value="${escapeHtml(t.name)}" ${t.name === state.config.terminal ? 'selected' : ''}>${escapeHtml(t.name)} (${escapeHtml(t.processName)})</option>`
|
|
@@ -204,6 +207,7 @@ function readConfigFromForm() {
|
|
|
204
207
|
terminal: $('#cfgTerminal').value || 'wt',
|
|
205
208
|
commandShell: $('#cfgCommandShell').value || 'pwsh',
|
|
206
209
|
autoFocusOnLaunch: $('#cfgAutoFocus').checked,
|
|
210
|
+
browserMode: $('#cfgBrowserMode').value || 'app',
|
|
207
211
|
finderPrompt: $('#cfgFinderPrompt').value,
|
|
208
212
|
repos,
|
|
209
213
|
};
|
package/public/index.html
CHANGED
|
@@ -123,6 +123,13 @@
|
|
|
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>browser open mode (on server start)
|
|
127
|
+
<select id="cfgBrowserMode" class="select">
|
|
128
|
+
<option value="app">app — Edge/Chrome chromeless window</option>
|
|
129
|
+
<option value="tab">tab — default browser, normal tab</option>
|
|
130
|
+
<option value="none">off — don't open anything</option>
|
|
131
|
+
</select>
|
|
132
|
+
</label>
|
|
126
133
|
<label class="full">finder prompt
|
|
127
134
|
<textarea id="cfgFinderPrompt" rows="3"></textarea>
|
|
128
135
|
</label>
|
package/server.js
CHANGED
|
@@ -324,14 +324,89 @@ 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 findAppModeBrowser() {
|
|
346
|
+
const candidates = [
|
|
347
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
348
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
349
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
350
|
+
process.env.LOCALAPPDATA &&
|
|
351
|
+
path.join(process.env.LOCALAPPDATA, 'Google\\Chrome\\Application\\chrome.exe'),
|
|
352
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
353
|
+
].filter(Boolean);
|
|
354
|
+
const fs = require('node:fs');
|
|
355
|
+
for (const p of candidates) {
|
|
356
|
+
if (fs.existsSync(p)) return p;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function openInBrowser(url, mode) {
|
|
362
|
+
if (process.platform !== 'win32' || mode === 'none') return;
|
|
363
|
+
const { spawn } = require('node:child_process');
|
|
364
|
+
const fs = require('node:fs');
|
|
365
|
+
|
|
366
|
+
if (mode === 'app') {
|
|
367
|
+
const exe = findAppModeBrowser();
|
|
368
|
+
if (exe) {
|
|
369
|
+
// Per-ccsm profile dir so we don't get the "already running, --app
|
|
370
|
+
// ignored" merge behavior of Edge/Chrome when the user has a normal
|
|
371
|
+
// window open. Lives under DATA_DIR so it's tidied with the rest.
|
|
372
|
+
const profileDir = path.join(DATA_DIR, 'browser-profile');
|
|
373
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
374
|
+
const child = spawn(
|
|
375
|
+
exe,
|
|
376
|
+
[
|
|
377
|
+
`--app=${url}`,
|
|
378
|
+
`--user-data-dir=${profileDir}`,
|
|
379
|
+
'--window-size=1400,1000',
|
|
380
|
+
'--no-first-run',
|
|
381
|
+
'--no-default-browser-check',
|
|
382
|
+
],
|
|
383
|
+
{ detached: true, stdio: 'ignore' }
|
|
384
|
+
);
|
|
385
|
+
child.unref();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
console.log('[ccsm] no Edge/Chrome found for app mode, falling back to default browser');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// mode === 'tab' (or app-mode fallback)
|
|
392
|
+
const child = spawn('cmd.exe', ['/c', 'start', '', url], {
|
|
393
|
+
detached: true,
|
|
394
|
+
stdio: 'ignore',
|
|
395
|
+
windowsHide: true,
|
|
396
|
+
});
|
|
397
|
+
child.unref();
|
|
398
|
+
}
|
|
399
|
+
|
|
327
400
|
(async () => {
|
|
328
401
|
const cfg = await loadConfig();
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
});
|
|
402
|
+
const { port } = await listenWithFallback(cfg.port);
|
|
403
|
+
const url = `http://localhost:${port}`;
|
|
404
|
+
console.log(`ccsm listening on ${url}${port !== cfg.port ? ` (requested ${cfg.port}, was taken)` : ''}`);
|
|
405
|
+
console.log(`data dir: ${DATA_DIR}`);
|
|
406
|
+
console.log(`work dir: ${cfg.workDir}`);
|
|
407
|
+
console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
|
|
408
|
+
const mode = cfg.browserMode || (cfg.autoOpenBrowser === false ? 'none' : 'app');
|
|
409
|
+
openInBrowser(url, mode);
|
|
335
410
|
startSnapshotLoop();
|
|
336
411
|
})().catch((err) => {
|
|
337
412
|
console.error('startup failed:', err);
|