@bakapiano/ccsm 0.22.2 → 0.22.4
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 +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +233 -231
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +592 -592
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +187 -15
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +148 -14
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +100 -100
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- package/server.js +1807 -1807
package/scripts/dev.js
CHANGED
|
@@ -1,149 +1,149 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Dev launcher · fully isolates from the user's prod ccsm install.
|
|
5
|
-
//
|
|
6
|
-
// Why: many contributors run the published `@bakapiano/ccsm` package
|
|
7
|
-
// for their day-to-day work (port 7777, ~/.ccsm). If `npm run dev`
|
|
8
|
-
// reused the same data dir + port, every hot-reload would clobber the
|
|
9
|
-
// live sessions.json. So dev gets its own:
|
|
10
|
-
//
|
|
11
|
-
// - CCSM_HOME → ~/.ccsm-dev/ (separate config.json, sessions.json, folders.json)
|
|
12
|
-
// - port → 7788 (no contention with prod 7777)
|
|
13
|
-
// - workDir → ~/ccsm-workspaces-dev (separate workspace tree)
|
|
14
|
-
// - no browser auto-open (we're iterating in an already-open tab)
|
|
15
|
-
//
|
|
16
|
-
// Run via `npm run dev`. The first launch seeds a starter config; later
|
|
17
|
-
// launches leave it alone so dev's own customisations stick.
|
|
18
|
-
|
|
19
|
-
const path = require('node:path');
|
|
20
|
-
const os = require('node:os');
|
|
21
|
-
const fs = require('node:fs');
|
|
22
|
-
const { spawn } = require('node:child_process');
|
|
23
|
-
|
|
24
|
-
const DEV_HOME = path.join(os.homedir(), '.ccsm-dev');
|
|
25
|
-
const DEV_PORT = '7788';
|
|
26
|
-
const DEV_WORKDIR = path.join(os.homedir(), 'ccsm-workspaces-dev');
|
|
27
|
-
|
|
28
|
-
fs.mkdirSync(DEV_HOME, { recursive: true });
|
|
29
|
-
|
|
30
|
-
// Seed a fresh dev config the first time. Subsequent runs leave the
|
|
31
|
-
// existing file alone — the dev's own UI edits persist across restarts.
|
|
32
|
-
const configPath = path.join(DEV_HOME, 'config.json');
|
|
33
|
-
if (!fs.existsSync(configPath)) {
|
|
34
|
-
fs.writeFileSync(configPath, JSON.stringify({
|
|
35
|
-
port: Number(DEV_PORT),
|
|
36
|
-
workDir: DEV_WORKDIR,
|
|
37
|
-
repos: [],
|
|
38
|
-
}, null, 2));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Mirror pages-root assets into public/ so the dev server can serve
|
|
42
|
-
// them at the URL paths the deployed site uses (manifest at
|
|
43
|
-
// /manifest.webmanifest, setup page at /setup/). Both mirror files
|
|
44
|
-
// are .gitignored — they exist only for local preview.
|
|
45
|
-
//
|
|
46
|
-
// The manifest is rewritten with a "ccsm-dev" identity so the PWA
|
|
47
|
-
// installed from dev shows up separately in Chrome's installed-apps
|
|
48
|
-
// list and Start Menu, instead of conflicting with the prod CCSM
|
|
49
|
-
// install.
|
|
50
|
-
const REPO_ROOT = path.join(__dirname, '..');
|
|
51
|
-
const PAGES_ROOT = path.join(REPO_ROOT, 'pages-root');
|
|
52
|
-
const PUBLIC_DIR = path.join(REPO_ROOT, 'public');
|
|
53
|
-
|
|
54
|
-
function mirrorSetup() {
|
|
55
|
-
try {
|
|
56
|
-
const src = path.join(PAGES_ROOT, 'setup');
|
|
57
|
-
const dst = path.join(PUBLIC_DIR, 'setup');
|
|
58
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
59
|
-
for (const f of fs.readdirSync(src)) {
|
|
60
|
-
fs.copyFileSync(path.join(src, f), path.join(dst, f));
|
|
61
|
-
}
|
|
62
|
-
} catch (e) { console.warn('[dev] setup mirror failed:', e.message); }
|
|
63
|
-
}
|
|
64
|
-
function writeDevManifest() {
|
|
65
|
-
try {
|
|
66
|
-
const src = path.join(PAGES_ROOT, 'manifest.webmanifest');
|
|
67
|
-
const m = JSON.parse(fs.readFileSync(src, 'utf8'));
|
|
68
|
-
m.id = '/?ccsm-dev';
|
|
69
|
-
m.name = 'CCSM dev';
|
|
70
|
-
m.short_name = 'CCSM dev';
|
|
71
|
-
// Dev runs at host root (localhost:7788/), so scope + start_url
|
|
72
|
-
// anchor at `/` not `./` (which would resolve relative to the
|
|
73
|
-
// manifest URL — same result here, but explicit is clearer).
|
|
74
|
-
m.scope = '/';
|
|
75
|
-
m.start_url = '/';
|
|
76
|
-
// Drop related_applications self-reference — its URL points at
|
|
77
|
-
// the prod GH Pages manifest, not this dev one. Leaving it in
|
|
78
|
-
// would let prod's getInstalledRelatedApps() detect dev installs
|
|
79
|
-
// as if they were prod, which is the opposite of what we want.
|
|
80
|
-
delete m.related_applications;
|
|
81
|
-
fs.writeFileSync(path.join(PUBLIC_DIR, 'manifest.webmanifest'), JSON.stringify(m, null, 2));
|
|
82
|
-
} catch (e) { console.warn('[dev] manifest mirror failed:', e.message); }
|
|
83
|
-
}
|
|
84
|
-
mirrorSetup();
|
|
85
|
-
writeDevManifest();
|
|
86
|
-
|
|
87
|
-
const env = {
|
|
88
|
-
...process.env,
|
|
89
|
-
CCSM_HOME: DEV_HOME,
|
|
90
|
-
CCSM_PORT: DEV_PORT,
|
|
91
|
-
CCSM_NO_BROWSER: '1',
|
|
92
|
-
// Marks the running server as "launched by dev.js" so /api/restart can
|
|
93
|
-
// skip the production restart-helper path (which respawns the global
|
|
94
|
-
// `ccsm.cmd` and would replace our --watch checkout server). In dev
|
|
95
|
-
// mode the server just process.exit(0)s and this script respawns it.
|
|
96
|
-
CCSM_DEV: '1',
|
|
97
|
-
// Always opt out of the 90s heartbeat watchdog in dev. The watchdog
|
|
98
|
-
// only matters when ccsm is tied to its own spawned browser window —
|
|
99
|
-
// closing that window means ccsm should stop. In dev there's no such
|
|
100
|
-
// window (CCSM_NO_BROWSER above) and the contributor's browser tab
|
|
101
|
-
// may be closed for minutes during a long file edit. Without this,
|
|
102
|
-
// any ambient CCSM_LAUNCHER=1 in the parent shell would silently make
|
|
103
|
-
// the dev server self-terminate every 90s.
|
|
104
|
-
CCSM_KEEP_ALIVE: '1',
|
|
105
|
-
// Explicitly clear CCSM_LAUNCHER so the watchdog activation condition
|
|
106
|
-
// can never be true here regardless of the parent env.
|
|
107
|
-
CCSM_LAUNCHER: '',
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
111
|
-
|
|
112
|
-
let current = null;
|
|
113
|
-
let stopping = false;
|
|
114
|
-
|
|
115
|
-
function spawnServer() {
|
|
116
|
-
// Don't use `node --watch` here — its restart-on-exit semantics are
|
|
117
|
-
// "wait for a file change after a clean exit", so calling
|
|
118
|
-
// process.exit(0) from /api/restart leaves --watch idling forever
|
|
119
|
-
// until the user touches a file. We do our own respawn-on-exit
|
|
120
|
-
// (below) which handles both the restart-by-exit path AND crashes,
|
|
121
|
-
// and the dev/api SSE endpoint still gives us frontend hot-reload
|
|
122
|
-
// without needing --watch for backend code (each restart pulls fresh
|
|
123
|
-
// require() cache anyway since this is a new process).
|
|
124
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
125
|
-
env,
|
|
126
|
-
stdio: 'inherit',
|
|
127
|
-
});
|
|
128
|
-
child.on('exit', (code, signal) => {
|
|
129
|
-
if (stopping) {
|
|
130
|
-
process.exit(signal ? 1 : (code ?? 0));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
// Server asked to restart (POST /api/restart → gracefulShutdown +
|
|
134
|
-
// exit 0). Respawn — node --watch picks up any code changes that
|
|
135
|
-
// landed in the meantime. A small delay lets the port fully release.
|
|
136
|
-
console.log(`[dev] server exited (code=${code} signal=${signal || ''}) · respawning`);
|
|
137
|
-
setTimeout(() => { current = spawnServer(); }, 500);
|
|
138
|
-
});
|
|
139
|
-
return child;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const stop = (sig) => () => {
|
|
143
|
-
stopping = true;
|
|
144
|
-
if (current) current.kill(sig);
|
|
145
|
-
};
|
|
146
|
-
process.on('SIGINT', stop('SIGINT'));
|
|
147
|
-
process.on('SIGTERM', stop('SIGTERM'));
|
|
148
|
-
|
|
149
|
-
current = spawnServer();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Dev launcher · fully isolates from the user's prod ccsm install.
|
|
5
|
+
//
|
|
6
|
+
// Why: many contributors run the published `@bakapiano/ccsm` package
|
|
7
|
+
// for their day-to-day work (port 7777, ~/.ccsm). If `npm run dev`
|
|
8
|
+
// reused the same data dir + port, every hot-reload would clobber the
|
|
9
|
+
// live sessions.json. So dev gets its own:
|
|
10
|
+
//
|
|
11
|
+
// - CCSM_HOME → ~/.ccsm-dev/ (separate config.json, sessions.json, folders.json)
|
|
12
|
+
// - port → 7788 (no contention with prod 7777)
|
|
13
|
+
// - workDir → ~/ccsm-workspaces-dev (separate workspace tree)
|
|
14
|
+
// - no browser auto-open (we're iterating in an already-open tab)
|
|
15
|
+
//
|
|
16
|
+
// Run via `npm run dev`. The first launch seeds a starter config; later
|
|
17
|
+
// launches leave it alone so dev's own customisations stick.
|
|
18
|
+
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const { spawn } = require('node:child_process');
|
|
23
|
+
|
|
24
|
+
const DEV_HOME = path.join(os.homedir(), '.ccsm-dev');
|
|
25
|
+
const DEV_PORT = '7788';
|
|
26
|
+
const DEV_WORKDIR = path.join(os.homedir(), 'ccsm-workspaces-dev');
|
|
27
|
+
|
|
28
|
+
fs.mkdirSync(DEV_HOME, { recursive: true });
|
|
29
|
+
|
|
30
|
+
// Seed a fresh dev config the first time. Subsequent runs leave the
|
|
31
|
+
// existing file alone — the dev's own UI edits persist across restarts.
|
|
32
|
+
const configPath = path.join(DEV_HOME, 'config.json');
|
|
33
|
+
if (!fs.existsSync(configPath)) {
|
|
34
|
+
fs.writeFileSync(configPath, JSON.stringify({
|
|
35
|
+
port: Number(DEV_PORT),
|
|
36
|
+
workDir: DEV_WORKDIR,
|
|
37
|
+
repos: [],
|
|
38
|
+
}, null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Mirror pages-root assets into public/ so the dev server can serve
|
|
42
|
+
// them at the URL paths the deployed site uses (manifest at
|
|
43
|
+
// /manifest.webmanifest, setup page at /setup/). Both mirror files
|
|
44
|
+
// are .gitignored — they exist only for local preview.
|
|
45
|
+
//
|
|
46
|
+
// The manifest is rewritten with a "ccsm-dev" identity so the PWA
|
|
47
|
+
// installed from dev shows up separately in Chrome's installed-apps
|
|
48
|
+
// list and Start Menu, instead of conflicting with the prod CCSM
|
|
49
|
+
// install.
|
|
50
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
51
|
+
const PAGES_ROOT = path.join(REPO_ROOT, 'pages-root');
|
|
52
|
+
const PUBLIC_DIR = path.join(REPO_ROOT, 'public');
|
|
53
|
+
|
|
54
|
+
function mirrorSetup() {
|
|
55
|
+
try {
|
|
56
|
+
const src = path.join(PAGES_ROOT, 'setup');
|
|
57
|
+
const dst = path.join(PUBLIC_DIR, 'setup');
|
|
58
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
59
|
+
for (const f of fs.readdirSync(src)) {
|
|
60
|
+
fs.copyFileSync(path.join(src, f), path.join(dst, f));
|
|
61
|
+
}
|
|
62
|
+
} catch (e) { console.warn('[dev] setup mirror failed:', e.message); }
|
|
63
|
+
}
|
|
64
|
+
function writeDevManifest() {
|
|
65
|
+
try {
|
|
66
|
+
const src = path.join(PAGES_ROOT, 'manifest.webmanifest');
|
|
67
|
+
const m = JSON.parse(fs.readFileSync(src, 'utf8'));
|
|
68
|
+
m.id = '/?ccsm-dev';
|
|
69
|
+
m.name = 'CCSM dev';
|
|
70
|
+
m.short_name = 'CCSM dev';
|
|
71
|
+
// Dev runs at host root (localhost:7788/), so scope + start_url
|
|
72
|
+
// anchor at `/` not `./` (which would resolve relative to the
|
|
73
|
+
// manifest URL — same result here, but explicit is clearer).
|
|
74
|
+
m.scope = '/';
|
|
75
|
+
m.start_url = '/';
|
|
76
|
+
// Drop related_applications self-reference — its URL points at
|
|
77
|
+
// the prod GH Pages manifest, not this dev one. Leaving it in
|
|
78
|
+
// would let prod's getInstalledRelatedApps() detect dev installs
|
|
79
|
+
// as if they were prod, which is the opposite of what we want.
|
|
80
|
+
delete m.related_applications;
|
|
81
|
+
fs.writeFileSync(path.join(PUBLIC_DIR, 'manifest.webmanifest'), JSON.stringify(m, null, 2));
|
|
82
|
+
} catch (e) { console.warn('[dev] manifest mirror failed:', e.message); }
|
|
83
|
+
}
|
|
84
|
+
mirrorSetup();
|
|
85
|
+
writeDevManifest();
|
|
86
|
+
|
|
87
|
+
const env = {
|
|
88
|
+
...process.env,
|
|
89
|
+
CCSM_HOME: DEV_HOME,
|
|
90
|
+
CCSM_PORT: DEV_PORT,
|
|
91
|
+
CCSM_NO_BROWSER: '1',
|
|
92
|
+
// Marks the running server as "launched by dev.js" so /api/restart can
|
|
93
|
+
// skip the production restart-helper path (which respawns the global
|
|
94
|
+
// `ccsm.cmd` and would replace our --watch checkout server). In dev
|
|
95
|
+
// mode the server just process.exit(0)s and this script respawns it.
|
|
96
|
+
CCSM_DEV: '1',
|
|
97
|
+
// Always opt out of the 90s heartbeat watchdog in dev. The watchdog
|
|
98
|
+
// only matters when ccsm is tied to its own spawned browser window —
|
|
99
|
+
// closing that window means ccsm should stop. In dev there's no such
|
|
100
|
+
// window (CCSM_NO_BROWSER above) and the contributor's browser tab
|
|
101
|
+
// may be closed for minutes during a long file edit. Without this,
|
|
102
|
+
// any ambient CCSM_LAUNCHER=1 in the parent shell would silently make
|
|
103
|
+
// the dev server self-terminate every 90s.
|
|
104
|
+
CCSM_KEEP_ALIVE: '1',
|
|
105
|
+
// Explicitly clear CCSM_LAUNCHER so the watchdog activation condition
|
|
106
|
+
// can never be true here regardless of the parent env.
|
|
107
|
+
CCSM_LAUNCHER: '',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
111
|
+
|
|
112
|
+
let current = null;
|
|
113
|
+
let stopping = false;
|
|
114
|
+
|
|
115
|
+
function spawnServer() {
|
|
116
|
+
// Don't use `node --watch` here — its restart-on-exit semantics are
|
|
117
|
+
// "wait for a file change after a clean exit", so calling
|
|
118
|
+
// process.exit(0) from /api/restart leaves --watch idling forever
|
|
119
|
+
// until the user touches a file. We do our own respawn-on-exit
|
|
120
|
+
// (below) which handles both the restart-by-exit path AND crashes,
|
|
121
|
+
// and the dev/api SSE endpoint still gives us frontend hot-reload
|
|
122
|
+
// without needing --watch for backend code (each restart pulls fresh
|
|
123
|
+
// require() cache anyway since this is a new process).
|
|
124
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
125
|
+
env,
|
|
126
|
+
stdio: 'inherit',
|
|
127
|
+
});
|
|
128
|
+
child.on('exit', (code, signal) => {
|
|
129
|
+
if (stopping) {
|
|
130
|
+
process.exit(signal ? 1 : (code ?? 0));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Server asked to restart (POST /api/restart → gracefulShutdown +
|
|
134
|
+
// exit 0). Respawn — node --watch picks up any code changes that
|
|
135
|
+
// landed in the meantime. A small delay lets the port fully release.
|
|
136
|
+
console.log(`[dev] server exited (code=${code} signal=${signal || ''}) · respawning`);
|
|
137
|
+
setTimeout(() => { current = spawnServer(); }, 500);
|
|
138
|
+
});
|
|
139
|
+
return child;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const stop = (sig) => () => {
|
|
143
|
+
stopping = true;
|
|
144
|
+
if (current) current.kill(sig);
|
|
145
|
+
};
|
|
146
|
+
process.on('SIGINT', stop('SIGINT'));
|
|
147
|
+
process.on('SIGTERM', stop('SIGTERM'));
|
|
148
|
+
|
|
149
|
+
current = spawnServer();
|
package/scripts/install.js
CHANGED
|
@@ -1,153 +1,153 @@
|
|
|
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
|
-
// Open the hosted setup guide. The page walks the user through the
|
|
134
|
-
// remaining one-time setup (allow ccsm:// protocol, firewall, install
|
|
135
|
-
// as PWA) and Step 1's "Try ccsm://start" button doubles as ccsm
|
|
136
|
-
// auto-launch — so we don't need a separate spawn here. Set
|
|
137
|
-
// CCSM_NO_AUTOLAUNCH=1 to skip (CI, headless setups).
|
|
138
|
-
if (process.env.CCSM_NO_AUTOLAUNCH !== '1') {
|
|
139
|
-
try {
|
|
140
|
-
// `start` on Windows opens the default browser without attaching a
|
|
141
|
-
// console. Run via cmd.exe /c since `start` is a cmd builtin.
|
|
142
|
-
require('node:child_process').spawn(
|
|
143
|
-
'cmd.exe',
|
|
144
|
-
['/d', '/s', '/c', 'start', '', 'https://bakapiano.github.io/ccsm/setup/'],
|
|
145
|
-
{ detached: true, stdio: 'ignore', windowsHide: true }
|
|
146
|
-
).unref();
|
|
147
|
-
log('opened setup guide · https://bakapiano.github.io/ccsm/setup/');
|
|
148
|
-
log('(set CCSM_NO_AUTOLAUNCH=1 to skip this on future installs)');
|
|
149
|
-
} catch (e) {
|
|
150
|
-
warn(`setup guide open failed · ${e.message}`);
|
|
151
|
-
warn('run `ccsm` manually to start.');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
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
|
+
// Open the hosted setup guide. The page walks the user through the
|
|
134
|
+
// remaining one-time setup (allow ccsm:// protocol, firewall, install
|
|
135
|
+
// as PWA) and Step 1's "Try ccsm://start" button doubles as ccsm
|
|
136
|
+
// auto-launch — so we don't need a separate spawn here. Set
|
|
137
|
+
// CCSM_NO_AUTOLAUNCH=1 to skip (CI, headless setups).
|
|
138
|
+
if (process.env.CCSM_NO_AUTOLAUNCH !== '1') {
|
|
139
|
+
try {
|
|
140
|
+
// `start` on Windows opens the default browser without attaching a
|
|
141
|
+
// console. Run via cmd.exe /c since `start` is a cmd builtin.
|
|
142
|
+
require('node:child_process').spawn(
|
|
143
|
+
'cmd.exe',
|
|
144
|
+
['/d', '/s', '/c', 'start', '', 'https://bakapiano.github.io/ccsm/setup/'],
|
|
145
|
+
{ detached: true, stdio: 'ignore', windowsHide: true }
|
|
146
|
+
).unref();
|
|
147
|
+
log('opened setup guide · https://bakapiano.github.io/ccsm/setup/');
|
|
148
|
+
log('(set CCSM_NO_AUTOLAUNCH=1 to skip this on future installs)');
|
|
149
|
+
} catch (e) {
|
|
150
|
+
warn(`setup guide open failed · ${e.message}`);
|
|
151
|
+
warn('run `ccsm` manually to start.');
|
|
152
|
+
}
|
|
153
|
+
}
|