@bakapiano/ccsm 0.14.0 → 0.15.1

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.
Files changed (53) hide show
  1. package/CLAUDE.md +474 -475
  2. package/README.md +189 -190
  3. package/bin/ccsm.js +194 -194
  4. package/lib/cliActivity.js +118 -0
  5. package/lib/codexSeed.js +147 -0
  6. package/lib/config.js +205 -188
  7. package/lib/folders.js +105 -105
  8. package/lib/localCliSessions.js +489 -489
  9. package/lib/persistedSessions.js +144 -142
  10. package/lib/webTerminal.js +224 -224
  11. package/lib/workspace.js +230 -230
  12. package/package.json +57 -57
  13. package/public/css/base.css +99 -99
  14. package/public/css/cards.css +183 -183
  15. package/public/css/feedback.css +303 -303
  16. package/public/css/forms.css +405 -405
  17. package/public/css/layout.css +160 -160
  18. package/public/css/modal.css +190 -190
  19. package/public/css/responsive.css +10 -10
  20. package/public/css/sidebar.css +613 -608
  21. package/public/css/terminals.css +294 -294
  22. package/public/css/tokens.css +81 -81
  23. package/public/css/wco.css +98 -98
  24. package/public/css/widgets.css +1628 -1628
  25. package/public/index.html +111 -105
  26. package/public/js/api.js +296 -280
  27. package/public/js/components/AdoptModal.js +343 -343
  28. package/public/js/components/App.js +35 -35
  29. package/public/js/components/DirectoryPicker.js +203 -203
  30. package/public/js/components/EntityFormModal.js +141 -141
  31. package/public/js/components/Modal.js +51 -51
  32. package/public/js/components/OfflineBanner.js +93 -93
  33. package/public/js/components/PageTitleBar.js +13 -13
  34. package/public/js/components/Picker.js +179 -179
  35. package/public/js/components/Popover.js +55 -55
  36. package/public/js/components/Sidebar.js +299 -299
  37. package/public/js/components/TerminalView.js +314 -314
  38. package/public/js/components/useDragSort.js +67 -67
  39. package/public/js/dialog.js +67 -67
  40. package/public/js/icons.js +177 -177
  41. package/public/js/main.js +132 -132
  42. package/public/js/pages/AboutPage.js +173 -165
  43. package/public/js/pages/ConfigurePage.js +513 -475
  44. package/public/js/pages/LaunchPage.js +369 -369
  45. package/public/js/pages/SessionsPage.js +101 -97
  46. package/public/js/state.js +231 -231
  47. package/scripts/dev.js +44 -11
  48. package/scripts/install.js +158 -158
  49. package/scripts/restart-helper.js +96 -0
  50. package/scripts/upgrade-helper.js +6 -1
  51. package/server.js +1282 -1254
  52. package/lib/cliSessionWatcher.js +0 -275
  53. package/public/manifest.webmanifest +0 -15
@@ -1,158 +1,158 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // ccsm postinstall · Windows-only · runs after `npm install -g @bakapiano/ccsm`.
5
- // Registers the `ccsm://` URL protocol in HKCU so the hosted frontend
6
- // (https://bakapiano.github.io/ccsm/v1/) can fire `<a href="ccsm://start">`
7
- // from its OfflineBanner and have Windows spawn the backend on demand.
8
- //
9
- // Best-effort: any failure MUST NOT break npm install. Each step is in
10
- // its own try/catch; we just log and move on.
11
- //
12
- // No .lnk file, no Start Menu shortcut — just the protocol handler.
13
-
14
- const path = require('node:path');
15
- const fs = require('node:fs');
16
- const { spawnSync } = require('node:child_process');
17
-
18
- function log(msg) { process.stdout.write(`[ccsm install] ${msg}\n`); }
19
- function warn(msg) { process.stderr.write(`[ccsm install] ${msg}\n`); }
20
-
21
- if (process.platform !== 'win32') {
22
- log('non-Windows · skipping ccsm:// registration');
23
- process.exit(0);
24
- }
25
- // Note: we DO register on npx-cache installs too (not just global). The
26
- // npx cache path is stable across re-runs of the same package, and even
27
- // if the user later cleans the cache, the only consequence is the
28
- // OfflineBanner button no-ops — nothing actively broken. Registering
29
- // always means a first-time `npx @bakapiano/ccsm` gets the full "click
30
- // to wake" UX without needing a separate `npm i -g`.
31
-
32
- // Returns { ccsmCmd, isSandbox } where isSandbox=true means this install
33
- // went into a non-default prefix (e.g. `npm i -g --prefix=<tmp>` from
34
- // the in-app upgrade's test mode). For sandboxed installs we DO NOT
35
- // touch the global launcher.vbs / ccsm:// protocol registration —
36
- // otherwise we'd repoint them at a directory that gets deleted later.
37
- function findCcsmCmd() {
38
- const givenPrefix = process.env.npm_config_prefix || null;
39
- let defaultPrefix = null;
40
- try {
41
- // npm config get prefix when run WITHOUT --prefix returns the
42
- // user's default global prefix; with --prefix it echoes the flag.
43
- // We want the env-independent default so we can compare. INIT_CWD
44
- // and a clean spawn give us the user-default value.
45
- const r = spawnSync('npm', ['config', 'get', 'prefix'], {
46
- encoding: 'utf8', shell: true,
47
- env: { ...process.env, npm_config_prefix: '' },
48
- });
49
- defaultPrefix = r.stdout?.trim() || null;
50
- } catch {}
51
- const prefix = givenPrefix || defaultPrefix;
52
- if (!prefix) return { ccsmCmd: null, isSandbox: false };
53
- const candidate = path.join(prefix, 'ccsm.cmd');
54
- const isSandbox = !!(givenPrefix && defaultPrefix
55
- && path.resolve(givenPrefix).toLowerCase() !== path.resolve(defaultPrefix).toLowerCase());
56
- return {
57
- ccsmCmd: fs.existsSync(candidate) ? candidate : null,
58
- isSandbox,
59
- };
60
- }
61
-
62
- // Write a tiny VBScript wrapper that ccsm:// dispatches into. Why VBS:
63
- // wscript.exe is a Windows-subsystem host (no console window), and
64
- // `Shell.Run(..., 0, False)` launches the target completely hidden — so
65
- // when the user clicks ccsm://start, NOTHING flashes on screen, the
66
- // backend just appears in the next health probe.
67
- function writeLauncherVbs(ccsmCmd) {
68
- const home = process.env.LOCALAPPDATA || process.env.APPDATA;
69
- if (!home) throw new Error('no LOCALAPPDATA/APPDATA env var');
70
- const dir = path.join(home, 'ccsm');
71
- fs.mkdirSync(dir, { recursive: true });
72
- const vbsPath = path.join(dir, 'launcher.vbs');
73
- // Escape any double-quotes in the cmd path (rare but possible).
74
- const cmdEsc = ccsmCmd.replace(/"/g, '""');
75
- const vbs = [
76
- "' ccsm protocol launcher · invoked by wscript.exe via the registered",
77
- "' ccsm:// URL handler. Spawns ccsm.cmd with WindowStyle 0 (hidden) +",
78
- "' bWaitOnReturn=False (async), so the click leaves zero visible trace.",
79
- 'If WScript.Arguments.Count >= 1 Then',
80
- ' arg = WScript.Arguments(0)',
81
- 'Else',
82
- ' arg = ""',
83
- 'End If',
84
- 'Set sh = CreateObject("WScript.Shell")',
85
- `sh.Run """${cmdEsc}"" """ & arg & """", 0, False`,
86
- '',
87
- ].join('\r\n');
88
- fs.writeFileSync(vbsPath, vbs, { encoding: 'utf8' });
89
- return vbsPath;
90
- }
91
-
92
- function registerProtocol(vbsPath) {
93
- // wscript.exe is a no-console host. The protocol-registered command
94
- // hands the entire ccsm:// URL to launcher.vbs as argv[0]; the VBS
95
- // forwards it to ccsm.cmd "%1" with a hidden window.
96
- const command = `wscript.exe "${vbsPath}" "%1"`;
97
- const root = 'HKCU\\Software\\Classes\\ccsm';
98
- const calls = [
99
- ['add', root, '/ve', '/d', 'URL:ccsm protocol', '/f'],
100
- ['add', root, '/v', 'URL Protocol', '/d', '', '/f'],
101
- ['add', `${root}\\shell\\open\\command`, '/ve', '/d', command, '/f'],
102
- ];
103
- for (const args of calls) {
104
- const r = spawnSync('reg.exe', args, { windowsHide: true });
105
- if (r.status !== 0) {
106
- throw new Error(`reg ${args.join(' ')} → exit ${r.status}: ${r.stderr?.toString() || ''}`);
107
- }
108
- }
109
- }
110
-
111
- const { ccsmCmd, isSandbox } = (() => {
112
- try { return findCcsmCmd(); } catch { return { ccsmCmd: null, isSandbox: false }; }
113
- })();
114
- if (!ccsmCmd) {
115
- warn('could not locate ccsm.cmd · skipping protocol registration');
116
- process.exit(0);
117
- }
118
- if (isSandbox) {
119
- log(`sandbox install detected (prefix=${process.env.npm_config_prefix}) · skipping global launcher.vbs + protocol registration + auto-launch`);
120
- process.exit(0);
121
- }
122
-
123
- try {
124
- const vbsPath = writeLauncherVbs(ccsmCmd);
125
- registerProtocol(vbsPath);
126
- log(`launcher · ${vbsPath}`);
127
- log(`ccsm:// protocol registered (silent · via wscript.exe)`);
128
- } catch (e) {
129
- warn(`failed · ${e.message}`);
130
- warn('the hosted frontend\'s "Start ccsm" button will not be able to launch the backend. You can still run `ccsm` manually in a terminal.');
131
- }
132
-
133
- // Auto-launch ccsm after install so the user lands directly in the app
134
- // without needing a second command. Detached + windowsHide so the npm
135
- // install command returns immediately. Skip if CCSM_NO_AUTOLAUNCH=1 is
136
- // set (CI, headless setups).
137
- if (process.env.CCSM_NO_AUTOLAUNCH !== '1') {
138
- try {
139
- const { spawn } = require('node:child_process');
140
- // Spawn `node bin/ccsm.js` directly — NOT ccsm.cmd. On Windows,
141
- // child_process.spawn() with shell:false refuses .cmd files (throws
142
- // EINVAL); using shell:true would flash a console window. Going
143
- // through node + the JS entrypoint sidesteps both problems and
144
- // matches exactly what the .cmd shim would have invoked.
145
- const launcherJs = path.join(__dirname, '..', 'bin', 'ccsm.js');
146
- const child = spawn(process.execPath, [launcherJs], {
147
- detached: true,
148
- stdio: 'ignore',
149
- windowsHide: true,
150
- });
151
- child.unref();
152
- log('launching ccsm now · check for the chromeless window');
153
- log('(set CCSM_NO_AUTOLAUNCH=1 to skip this on future installs)');
154
- } catch (e) {
155
- warn(`auto-launch failed · ${e.message}`);
156
- warn('run `ccsm` manually to start.');
157
- }
158
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ccsm postinstall · Windows-only · runs after `npm install -g @bakapiano/ccsm`.
5
+ // Registers the `ccsm://` URL protocol in HKCU so the hosted frontend
6
+ // (https://bakapiano.github.io/ccsm/v1/) can fire `<a href="ccsm://start">`
7
+ // from its OfflineBanner and have Windows spawn the backend on demand.
8
+ //
9
+ // Best-effort: any failure MUST NOT break npm install. Each step is in
10
+ // its own try/catch; we just log and move on.
11
+ //
12
+ // No .lnk file, no Start Menu shortcut — just the protocol handler.
13
+
14
+ const path = require('node:path');
15
+ const fs = require('node:fs');
16
+ const { spawnSync } = require('node:child_process');
17
+
18
+ function log(msg) { process.stdout.write(`[ccsm install] ${msg}\n`); }
19
+ function warn(msg) { process.stderr.write(`[ccsm install] ${msg}\n`); }
20
+
21
+ if (process.platform !== 'win32') {
22
+ log('non-Windows · skipping ccsm:// registration');
23
+ process.exit(0);
24
+ }
25
+ // Note: we DO register on npx-cache installs too (not just global). The
26
+ // npx cache path is stable across re-runs of the same package, and even
27
+ // if the user later cleans the cache, the only consequence is the
28
+ // OfflineBanner button no-ops — nothing actively broken. Registering
29
+ // always means a first-time `npx @bakapiano/ccsm` gets the full "click
30
+ // to wake" UX without needing a separate `npm i -g`.
31
+
32
+ // Returns { ccsmCmd, isSandbox } where isSandbox=true means this install
33
+ // went into a non-default prefix (e.g. `npm i -g --prefix=<tmp>` from
34
+ // the in-app upgrade's test mode). For sandboxed installs we DO NOT
35
+ // touch the global launcher.vbs / ccsm:// protocol registration —
36
+ // otherwise we'd repoint them at a directory that gets deleted later.
37
+ function findCcsmCmd() {
38
+ const givenPrefix = process.env.npm_config_prefix || null;
39
+ let defaultPrefix = null;
40
+ try {
41
+ // npm config get prefix when run WITHOUT --prefix returns the
42
+ // user's default global prefix; with --prefix it echoes the flag.
43
+ // We want the env-independent default so we can compare. INIT_CWD
44
+ // and a clean spawn give us the user-default value.
45
+ const r = spawnSync('npm', ['config', 'get', 'prefix'], {
46
+ encoding: 'utf8', shell: true,
47
+ env: { ...process.env, npm_config_prefix: '' },
48
+ });
49
+ defaultPrefix = r.stdout?.trim() || null;
50
+ } catch {}
51
+ const prefix = givenPrefix || defaultPrefix;
52
+ if (!prefix) return { ccsmCmd: null, isSandbox: false };
53
+ const candidate = path.join(prefix, 'ccsm.cmd');
54
+ const isSandbox = !!(givenPrefix && defaultPrefix
55
+ && path.resolve(givenPrefix).toLowerCase() !== path.resolve(defaultPrefix).toLowerCase());
56
+ return {
57
+ ccsmCmd: fs.existsSync(candidate) ? candidate : null,
58
+ isSandbox,
59
+ };
60
+ }
61
+
62
+ // Write a tiny VBScript wrapper that ccsm:// dispatches into. Why VBS:
63
+ // wscript.exe is a Windows-subsystem host (no console window), and
64
+ // `Shell.Run(..., 0, False)` launches the target completely hidden — so
65
+ // when the user clicks ccsm://start, NOTHING flashes on screen, the
66
+ // backend just appears in the next health probe.
67
+ function writeLauncherVbs(ccsmCmd) {
68
+ const home = process.env.LOCALAPPDATA || process.env.APPDATA;
69
+ if (!home) throw new Error('no LOCALAPPDATA/APPDATA env var');
70
+ const dir = path.join(home, 'ccsm');
71
+ fs.mkdirSync(dir, { recursive: true });
72
+ const vbsPath = path.join(dir, 'launcher.vbs');
73
+ // Escape any double-quotes in the cmd path (rare but possible).
74
+ const cmdEsc = ccsmCmd.replace(/"/g, '""');
75
+ const vbs = [
76
+ "' ccsm protocol launcher · invoked by wscript.exe via the registered",
77
+ "' ccsm:// URL handler. Spawns ccsm.cmd with WindowStyle 0 (hidden) +",
78
+ "' bWaitOnReturn=False (async), so the click leaves zero visible trace.",
79
+ 'If WScript.Arguments.Count >= 1 Then',
80
+ ' arg = WScript.Arguments(0)',
81
+ 'Else',
82
+ ' arg = ""',
83
+ 'End If',
84
+ 'Set sh = CreateObject("WScript.Shell")',
85
+ `sh.Run """${cmdEsc}"" """ & arg & """", 0, False`,
86
+ '',
87
+ ].join('\r\n');
88
+ fs.writeFileSync(vbsPath, vbs, { encoding: 'utf8' });
89
+ return vbsPath;
90
+ }
91
+
92
+ function registerProtocol(vbsPath) {
93
+ // wscript.exe is a no-console host. The protocol-registered command
94
+ // hands the entire ccsm:// URL to launcher.vbs as argv[0]; the VBS
95
+ // forwards it to ccsm.cmd "%1" with a hidden window.
96
+ const command = `wscript.exe "${vbsPath}" "%1"`;
97
+ const root = 'HKCU\\Software\\Classes\\ccsm';
98
+ const calls = [
99
+ ['add', root, '/ve', '/d', 'URL:ccsm protocol', '/f'],
100
+ ['add', root, '/v', 'URL Protocol', '/d', '', '/f'],
101
+ ['add', `${root}\\shell\\open\\command`, '/ve', '/d', command, '/f'],
102
+ ];
103
+ for (const args of calls) {
104
+ const r = spawnSync('reg.exe', args, { windowsHide: true });
105
+ if (r.status !== 0) {
106
+ throw new Error(`reg ${args.join(' ')} → exit ${r.status}: ${r.stderr?.toString() || ''}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ const { ccsmCmd, isSandbox } = (() => {
112
+ try { return findCcsmCmd(); } catch { return { ccsmCmd: null, isSandbox: false }; }
113
+ })();
114
+ if (!ccsmCmd) {
115
+ warn('could not locate ccsm.cmd · skipping protocol registration');
116
+ process.exit(0);
117
+ }
118
+ if (isSandbox) {
119
+ log(`sandbox install detected (prefix=${process.env.npm_config_prefix}) · skipping global launcher.vbs + protocol registration + auto-launch`);
120
+ process.exit(0);
121
+ }
122
+
123
+ try {
124
+ const vbsPath = writeLauncherVbs(ccsmCmd);
125
+ registerProtocol(vbsPath);
126
+ log(`launcher · ${vbsPath}`);
127
+ log(`ccsm:// protocol registered (silent · via wscript.exe)`);
128
+ } catch (e) {
129
+ warn(`failed · ${e.message}`);
130
+ warn('the hosted frontend\'s "Start ccsm" button will not be able to launch the backend. You can still run `ccsm` manually in a terminal.');
131
+ }
132
+
133
+ // Auto-launch ccsm after install so the user lands directly in the app
134
+ // without needing a second command. Detached + windowsHide so the npm
135
+ // install command returns immediately. Skip if CCSM_NO_AUTOLAUNCH=1 is
136
+ // set (CI, headless setups).
137
+ if (process.env.CCSM_NO_AUTOLAUNCH !== '1') {
138
+ try {
139
+ const { spawn } = require('node:child_process');
140
+ // Spawn `node bin/ccsm.js` directly — NOT ccsm.cmd. On Windows,
141
+ // child_process.spawn() with shell:false refuses .cmd files (throws
142
+ // EINVAL); using shell:true would flash a console window. Going
143
+ // through node + the JS entrypoint sidesteps both problems and
144
+ // matches exactly what the .cmd shim would have invoked.
145
+ const launcherJs = path.join(__dirname, '..', 'bin', 'ccsm.js');
146
+ const child = spawn(process.execPath, [launcherJs], {
147
+ detached: true,
148
+ stdio: 'ignore',
149
+ windowsHide: true,
150
+ });
151
+ child.unref();
152
+ log('launching ccsm now · check for the chromeless window');
153
+ log('(set CCSM_NO_AUTOLAUNCH=1 to skip this on future installs)');
154
+ } catch (e) {
155
+ warn(`auto-launch failed · ${e.message}`);
156
+ warn('run `ccsm` manually to start.');
157
+ }
158
+ }
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Restart helper · spawned detached by /api/restart.
5
+ //
6
+ // Just like upgrade-helper but skips the `npm i` step. Server kicks
7
+ // this off + gracefulShutdowns; helper waits for the port to free, then
8
+ // respawns ccsm (which finds no live backend and starts a fresh one).
9
+ //
10
+ // Argv: node restart-helper.js <port> <pid>
11
+
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+ const os = require('node:os');
15
+ const net = require('node:net');
16
+ const { spawn } = require('node:child_process');
17
+
18
+ const oldPort = Number(process.argv[2] || 7777);
19
+ const oldPid = Number(process.argv[3] || 0);
20
+
21
+ const HOME = process.env.CCSM_HOME || path.join(os.homedir(), '.ccsm');
22
+ const LOG = path.join(HOME, 'restart.log');
23
+ try { fs.mkdirSync(HOME, { recursive: true }); } catch {}
24
+
25
+ function log(msg) {
26
+ const line = `[${new Date().toISOString()}] ${msg}\n`;
27
+ try { fs.appendFileSync(LOG, line); } catch {}
28
+ }
29
+
30
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
31
+
32
+ function portFree(port, timeoutMs = 800) {
33
+ return new Promise((resolve) => {
34
+ const s = new net.Socket();
35
+ let settled = false;
36
+ const finish = (free) => { if (settled) return; settled = true; try { s.destroy(); } catch {} resolve(free); };
37
+ s.setTimeout(timeoutMs);
38
+ s.once('connect', () => finish(false));
39
+ s.once('timeout', () => finish(true));
40
+ s.once('error', () => finish(true));
41
+ s.connect(port, '127.0.0.1');
42
+ });
43
+ }
44
+
45
+ function pidAlive(pid) {
46
+ if (!pid) return false;
47
+ try { process.kill(pid, 0); return true; }
48
+ catch (e) { return e.code === 'EPERM'; }
49
+ }
50
+
51
+ (async () => {
52
+ log(`start · oldPort=${oldPort} oldPid=${oldPid}`);
53
+
54
+ const deadline = Date.now() + 30_000;
55
+ while (Date.now() < deadline) {
56
+ const free = await portFree(oldPort);
57
+ const dead = !pidAlive(oldPid);
58
+ if (free && dead) break;
59
+ await sleep(250);
60
+ }
61
+ log(`old server gone (or 30s elapsed) · respawning`);
62
+
63
+ const isWin = process.platform === 'win32';
64
+ const ccsmCmd = isWin ? 'ccsm.cmd' : 'ccsm';
65
+ // Inherit env but DROP CCSM_NO_BROWSER so the respawned server pops a
66
+ // fresh browser window — the frontend that triggered the restart
67
+ // called window.close() in parallel, and the new window takes its
68
+ // place without the OfflineBanner gap.
69
+ const childEnv = { ...process.env };
70
+ delete childEnv.CCSM_NO_BROWSER;
71
+ let exe, exeArgs;
72
+ if (isWin) {
73
+ exe = process.env.ComSpec || 'cmd.exe';
74
+ exeArgs = ['/d', '/s', '/c', ccsmCmd];
75
+ } else {
76
+ exe = ccsmCmd;
77
+ exeArgs = [];
78
+ }
79
+ try {
80
+ const child = spawn(exe, exeArgs, {
81
+ detached: true,
82
+ stdio: 'ignore',
83
+ windowsHide: true,
84
+ shell: false,
85
+ env: childEnv,
86
+ });
87
+ child.unref();
88
+ log(`respawned ${ccsmCmd} (via ${path.basename(exe)})`);
89
+ } catch (e) {
90
+ log(`respawn failed: ${e.message}`);
91
+ process.exit(1);
92
+ }
93
+ })().catch((e) => {
94
+ log(`fatal: ${e.message}`);
95
+ process.exit(1);
96
+ });
@@ -126,7 +126,12 @@ function pidAlive(pid) {
126
126
  const ccsmCmd = installPrefix
127
127
  ? (isWin ? path.join(installPrefix, 'ccsm.cmd') : path.join(installPrefix, 'bin', 'ccsm'))
128
128
  : (isWin ? 'ccsm.cmd' : 'ccsm');
129
- const childEnv = { ...process.env, CCSM_NO_BROWSER: '1' };
129
+ // Drop CCSM_NO_BROWSER so the post-upgrade server pops a fresh window
130
+ // — the frontend that triggered the upgrade window.close()'d in
131
+ // parallel, and the new window takes its place. Skips the
132
+ // OfflineBanner gap that used to bridge the upgrade.
133
+ const childEnv = { ...process.env };
134
+ delete childEnv.CCSM_NO_BROWSER;
130
135
  let exe, exeArgs;
131
136
  if (isWin) {
132
137
  exe = process.env.ComSpec || 'cmd.exe';