@bakapiano/ccsm 0.22.6 → 0.22.8
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 +521 -540
- package/README.md +186 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +36 -139
- package/lib/codexSeed.js +126 -183
- package/lib/config.js +277 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/persistedSessions.js +179 -139
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +225 -225
- package/lib/winPath.js +1 -1
- 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 +154 -154
- 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 +546 -546
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2347 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +349 -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 +28 -0
- 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 +730 -713
- package/public/js/pages/LaunchPage.js +403 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +54 -54
- package/public/js/state.js +335 -335
- package/public/js/util.js +1 -1
- 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 +1748 -1817
- package/lib/localCliSessions.js +0 -519
- package/public/js/components/AdoptModal.js +0 -261
- package/public/manifest.webmanifest +0 -25
- package/public/setup/index.html +0 -567
package/CLAUDE.md
CHANGED
|
@@ -1,543 +1,524 @@
|
|
|
1
|
-
# ccsm — Claude Code Session Manager
|
|
2
|
-
|
|
3
|
-
A small Node/Express + Preact web tool that runs every Claude/Codex/
|
|
4
|
-
Copilot CLI session inside a single web app. PTYs live in-process
|
|
5
|
-
(node-pty), sessions persist across restarts, and `--resume <uuid>`
|
|
6
|
-
reattaches to the exact upstream conversation.
|
|
7
|
-
|
|
8
|
-
## Why this exists
|
|
9
|
-
|
|
10
|
-
When you're running 8–10 concurrent `claude` sessions across ad-hoc
|
|
11
|
-
clones (`D:\proj`, `D:\proj2`, `…`, plus GUID worktree dirs), it's easy
|
|
12
|
-
to lose track of which terminal is which session. ccsm gives an
|
|
13
|
-
at-a-glance sidebar, organises sessions into folders, and `--resume`s
|
|
14
|
-
each one in the same xterm.js panel.
|
|
15
|
-
|
|
16
|
-
## Architecture: hosted frontend + local backend
|
|
17
|
-
|
|
18
|
-
The frontend is **not** bundled in the npm package — it's hosted on
|
|
19
|
-
GitHub Pages and matched to your installed backend version through a
|
|
20
|
-
version router.
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
┌── browser ────────────────────────────┐
|
|
24
|
-
│ https://bakapiano.github.io/ccsm/ ← version router (tiny)
|
|
25
|
-
│ ↓
|
|
26
|
-
│ https://bakapiano.github.io/ccsm/X.Y.Z/ ← per-version frontend
|
|
27
|
-
└────────────┬──────────────────────────┘
|
|
28
|
-
│ fetch /api/* (CORS, allow-list)
|
|
29
|
-
│ ws://localhost:7777/ws/*
|
|
30
|
-
▼
|
|
31
|
-
┌── local backend ──────────────────────┐
|
|
32
|
-
│ npm i -g @bakapiano/ccsm │
|
|
33
|
-
│ ccsm │
|
|
34
|
-
│ ├── /api/sessions /api/sessions/new │
|
|
35
|
-
│ ├── /api/sessions/:id/resume │
|
|
36
|
-
│ ├── /
|
|
37
|
-
│ ├── /
|
|
38
|
-
│ ├── /api/
|
|
39
|
-
│
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- `
|
|
91
|
-
- `
|
|
92
|
-
- `
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
|
143
|
-
| `POST /api/
|
|
144
|
-
|
|
|
145
|
-
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
- `
|
|
159
|
-
- `
|
|
160
|
-
- `
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
the
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
├──
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
├──
|
|
218
|
-
│ ├──
|
|
219
|
-
│ └──
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
│
|
|
226
|
-
│ ├──
|
|
227
|
-
│ ├──
|
|
228
|
-
│ ├──
|
|
229
|
-
│
|
|
230
|
-
├──
|
|
231
|
-
│ ├──
|
|
232
|
-
│ ├──
|
|
233
|
-
│
|
|
234
|
-
|
|
235
|
-
├──
|
|
236
|
-
├──
|
|
237
|
-
├──
|
|
238
|
-
|
|
239
|
-
│
|
|
240
|
-
│
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
`
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
**One source of truth for cross-origin.** `public/js/backend.js`
|
|
302
|
-
exports `httpBase()` and `wsBase()`. Localhost → same-origin (empty
|
|
303
|
-
base). Anything else → `http://localhost:7777`. CORS on the backend
|
|
304
|
-
allows `https://bakapiano.github.io` only — never `*`.
|
|
305
|
-
|
|
306
|
-
## API surface
|
|
307
|
-
|
|
308
|
-
| Method | Path | Purpose |
|
|
309
|
-
|---|---|---|
|
|
310
|
-
| GET / PUT | `/api/config` | read / replace config |
|
|
311
|
-
| GET | `/api/sessions` | list persisted sessions |
|
|
312
|
-
| PUT | `/api/sessions/:id` | rename / move to folder |
|
|
1
|
+
# ccsm — Claude Code Session Manager
|
|
2
|
+
|
|
3
|
+
A small Node/Express + Preact web tool that runs every Claude/Codex/
|
|
4
|
+
Copilot CLI session inside a single web app. PTYs live in-process
|
|
5
|
+
(node-pty), sessions persist across restarts, and `--resume <uuid>`
|
|
6
|
+
reattaches to the exact upstream conversation.
|
|
7
|
+
|
|
8
|
+
## Why this exists
|
|
9
|
+
|
|
10
|
+
When you're running 8–10 concurrent `claude` sessions across ad-hoc
|
|
11
|
+
clones (`D:\proj`, `D:\proj2`, `…`, plus GUID worktree dirs), it's easy
|
|
12
|
+
to lose track of which terminal is which session. ccsm gives an
|
|
13
|
+
at-a-glance sidebar, organises sessions into folders, and `--resume`s
|
|
14
|
+
each one in the same xterm.js panel.
|
|
15
|
+
|
|
16
|
+
## Architecture: hosted frontend + local backend
|
|
17
|
+
|
|
18
|
+
The frontend is **not** bundled in the npm package — it's hosted on
|
|
19
|
+
GitHub Pages and matched to your installed backend version through a
|
|
20
|
+
version router.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌── browser ────────────────────────────┐
|
|
24
|
+
│ https://bakapiano.github.io/ccsm/ ← version router (tiny)
|
|
25
|
+
│ ↓
|
|
26
|
+
│ https://bakapiano.github.io/ccsm/X.Y.Z/ ← per-version frontend
|
|
27
|
+
└────────────┬──────────────────────────┘
|
|
28
|
+
│ fetch /api/* (CORS, allow-list)
|
|
29
|
+
│ ws://localhost:7777/ws/*
|
|
30
|
+
▼
|
|
31
|
+
┌── local backend ──────────────────────┐
|
|
32
|
+
│ npm i -g @bakapiano/ccsm │
|
|
33
|
+
│ ccsm │
|
|
34
|
+
│ ├── /api/sessions /api/sessions/new │
|
|
35
|
+
│ ├── /api/sessions/:id/resume │
|
|
36
|
+
│ ├── /ws/terminal/:id (PTY) │
|
|
37
|
+
│ ├── /api/version /api/upgrade │
|
|
38
|
+
│ ├── /api/heartbeat /api/health │
|
|
39
|
+
│ └── /api/shutdown │
|
|
40
|
+
└───────────────────────────────────────┘
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Version routing.** GH Pages root (`/ccsm/`) hosts a tiny static
|
|
44
|
+
router (`pages-root/index.html`) that probes `localhost:7777/api/health`
|
|
45
|
+
and redirects to `./<backend.version>/`. Each release publishes a fresh
|
|
46
|
+
`/ccsm/<X.Y.Z>/` subdir; old ones stay forever via the workflow's
|
|
47
|
+
`keep_files: true`. Result: a 1:1 frontend↔backend version pin, no
|
|
48
|
+
semver-compat logic, and old backends keep working indefinitely.
|
|
49
|
+
|
|
50
|
+
Each per-version frontend has its version baked into a `<meta
|
|
51
|
+
name="ccsm-frontend-version">` at deploy time (injected by the GH Pages
|
|
52
|
+
workflow). On boot it re-fetches `/api/health` and bounces back through
|
|
53
|
+
the router via `location.replace('../')` if the backend has since been
|
|
54
|
+
upgraded.
|
|
55
|
+
|
|
56
|
+
When the backend is offline the router itself shows a "Start ccsm" UI
|
|
57
|
+
with a `ccsm://start` link (same protocol-handler trick we already
|
|
58
|
+
register at install time). No need to redirect to a stale version.
|
|
59
|
+
|
|
60
|
+
**Dev mode.** When running from a checkout (`__dirname` not under
|
|
61
|
+
`node_modules`), the backend ALSO serves `public/` so contributors can
|
|
62
|
+
iterate at `localhost:7777/` without pushing. In dev there's no
|
|
63
|
+
`<meta>` tag → the version guard no-ops.
|
|
64
|
+
|
|
65
|
+
## Run
|
|
66
|
+
|
|
67
|
+
```powershell
|
|
68
|
+
# install once
|
|
69
|
+
npm install -g @bakapiano/ccsm
|
|
70
|
+
|
|
71
|
+
# then anywhere
|
|
72
|
+
ccsm
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`ccsm` opens the version router in a chromeless Edge `--app=` window.
|
|
76
|
+
Terminal returns immediately (the server is spawned detached). Close
|
|
77
|
+
the window → server saves a final snapshot of state and exits within
|
|
78
|
+
~12s.
|
|
79
|
+
|
|
80
|
+
If you don't want the auto-opened window (e.g. you live in the PWA),
|
|
81
|
+
just visit `https://bakapiano.github.io/ccsm/` — when backend is
|
|
82
|
+
down you see the inline OfflineBanner with a **Start ccsm** button.
|
|
83
|
+
|
|
84
|
+
Default port `7777`, default workDir `~/ccsm-workspaces`. Config +
|
|
85
|
+
state live at `~/.ccsm/` (override with `CCSM_HOME=<path>`). All
|
|
86
|
+
settings editable through the Configure page
|
|
87
|
+
(`~/.ccsm/config.json` on disk). Notable knobs:
|
|
88
|
+
|
|
89
|
+
- `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port.
|
|
90
|
+
- `resumeMode` (default `latest`) — `latest` uses each CLI's `resumeLatestArgs`; `picker` uses `resumePickerArgs`.
|
|
91
|
+
- `clis` — array of CLI definitions. Built-ins for `claude`, `codex`, `copilot`; users can add `other` CLIs with custom `command`, `args`, `resumeLatestArgs`, `resumePickerArgs`, `shell` (direct/pwsh/cmd).
|
|
92
|
+
- `defaultCliId` — which CLI the Launch page pre-selects.
|
|
93
|
+
|
|
94
|
+
## ccsm:// protocol · "wake on click"
|
|
95
|
+
|
|
96
|
+
The hosted frontend can't spawn processes (sandboxed). For "click to
|
|
97
|
+
wake backend" we register a per-user URL protocol handler on Windows:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
HKCU\Software\Classes\ccsm\shell\open\command
|
|
101
|
+
→ wscript.exe "<LOCALAPPDATA>\ccsm\launcher.vbs" "%1"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`launcher.vbs` uses `Shell.Run(..., 0, False)` — windowstyle 0 means the
|
|
105
|
+
spawned `ccsm.cmd` runs **completely hidden**. No console flash. The
|
|
106
|
+
`.cmd` goes through `bin/ccsm.js`, which detects `ccsm://start` in argv,
|
|
107
|
+
spawns `server.js` detached with `CCSM_NO_BROWSER=1`, and exits.
|
|
108
|
+
|
|
109
|
+
The router's "Start ccsm" button (and OfflineBanner inside each
|
|
110
|
+
per-version frontend) is just `<a href="ccsm://start">`. First click
|
|
111
|
+
triggers a one-time Windows confirmation dialog ("Open ccsm.cmd?");
|
|
112
|
+
ticking "Always allow" makes it silent thereafter.
|
|
113
|
+
|
|
114
|
+
postinstall (`scripts/install.js`) registers the protocol
|
|
115
|
+
unconditionally on Windows — including npx-cache installs. The path
|
|
116
|
+
stored in the registry points at whatever `ccsm.cmd` location npm gave
|
|
117
|
+
us (`<prefix>/ccsm.cmd` from `npm config get prefix`).
|
|
118
|
+
|
|
119
|
+
## In-app upgrade
|
|
120
|
+
|
|
121
|
+
About page surfaces the installed version, polls
|
|
122
|
+
`registry.npmjs.org/@bakapiano%2Fccsm/latest` (cached 30 min) for the
|
|
123
|
+
latest published version, and offers an **Upgrade** button when newer.
|
|
124
|
+
`POST /api/upgrade` spawns `npm i -g @bakapiano/ccsm@latest` detached,
|
|
125
|
+
then on success spawns a fresh `ccsm` (also detached) and
|
|
126
|
+
gracefulShutdowns. The OfflineBanner appears briefly; the router then
|
|
127
|
+
picks up the new version on its next probe.
|
|
128
|
+
|
|
129
|
+
The `target` field is regex-validated (`/^[a-z0-9.+\-^~]+$/i`) before
|
|
130
|
+
the spawn — npm install doesn't shell out, but defends against argv
|
|
131
|
+
weirdness regardless. Concurrent calls return `409`.
|
|
132
|
+
|
|
133
|
+
## Lifecycle
|
|
134
|
+
|
|
135
|
+
Single `gracefulShutdown(reason)` function in `server.js` is the only
|
|
136
|
+
exit path. It kills any PTY children, then `process.exit(0)`. Every
|
|
137
|
+
trigger funnels here:
|
|
138
|
+
|
|
139
|
+
| trigger | path |
|
|
140
|
+
|---|---|
|
|
141
|
+
| auto-spawned browser window closes | `child.on('exit')` — see smart-kill below |
|
|
142
|
+
| `POST /api/shutdown` | from npm uninstall, from launcher's auto-upgrade |
|
|
143
|
+
| `POST /api/upgrade` after install completes | self-restart |
|
|
144
|
+
| SIGINT / SIGTERM | OS signals |
|
|
145
|
+
| heartbeat watchdog timeout | 90s with no heartbeat, only when launched via `bin/ccsm.js` |
|
|
146
|
+
|
|
147
|
+
**Smart browser-exit**: when the spawned browser child dies, we don't
|
|
148
|
+
kill immediately. Two filters:
|
|
149
|
+
|
|
150
|
+
1. **Fast-exit (<5s)** — Edge `--app=` often hands the URL off to an existing Edge profile process group and the spawned child dies milliseconds after creation. We ignore any exit inside the first 5s.
|
|
151
|
+
2. **Deferred multi-client check (12s)** — after a real close, wait 12s and check if any heartbeat arrived AFTER the close timestamp. If yes, a hosted-frontend tab (or another window) is keeping us busy, stay alive. If no, gracefulShutdown.
|
|
152
|
+
|
|
153
|
+
Frontend heartbeat cadence is 10s (in `main.js`), so one full cycle
|
|
154
|
+
fits inside the 12s decision window.
|
|
155
|
+
|
|
156
|
+
Environment overrides:
|
|
157
|
+
- `CCSM_KEEP_ALIVE=1` → disable both browser-exit hook and heartbeat watchdog. For automation hosts.
|
|
158
|
+
- `CCSM_LAUNCHER=1` → set by `bin/ccsm.js` when it spawns the server; enables the heartbeat watchdog.
|
|
159
|
+
- `CCSM_NO_BROWSER=1` → set by the launcher when handling a `ccsm://` click or by `/api/upgrade` self-respawn; suppresses the server's auto-open browser.
|
|
160
|
+
- `CCSM_NO_DEV=1` → suppress dev-mode features (static serving, hot-reload SSE) even when running from a checkout.
|
|
161
|
+
|
|
162
|
+
## Sessions: persisted and resumed
|
|
163
|
+
|
|
164
|
+
There's **one source of truth**: `~/.ccsm/sessions.json`, managed by
|
|
165
|
+
`lib/persistedSessions.js`. Every session ccsm starts goes in there
|
|
166
|
+
with `{ id, cliId, cwd, workspace, title, folderId, repos,
|
|
167
|
+
status, … }`. The persisted `id` is ccsm-owned and matches the PTY id;
|
|
168
|
+
it is not an upstream CLI session id.
|
|
169
|
+
|
|
170
|
+
**Folder-level resume.** ccsm does not persist upstream session ids and
|
|
171
|
+
does not seed CLI transcript files. Resume launches the configured CLI
|
|
172
|
+
at the record's `cwd` and appends one of two folder-level templates:
|
|
173
|
+
|
|
174
|
+
- `resumeMode: 'latest'` -> `cli.resumeLatestArgs`
|
|
175
|
+
- `resumeMode: 'picker'` -> `cli.resumePickerArgs`
|
|
176
|
+
|
|
177
|
+
Built-ins default to:
|
|
178
|
+
|
|
179
|
+
- Claude: latest `--continue`, picker `--resume`
|
|
180
|
+
- Codex: latest `resume --last`, picker `resume`
|
|
181
|
+
- Copilot: latest `--continue`, picker `--resume`
|
|
182
|
+
|
|
183
|
+
User-added `other` CLIs can leave those arrays empty if they do not
|
|
184
|
+
support resume, or set either template explicitly.
|
|
185
|
+
|
|
186
|
+
**Duplicate records.** New sessions are keyed by normalized
|
|
187
|
+
`cliId + cwd`. If the user launches the same CLI in the same folder
|
|
188
|
+
again, ccsm reuses the existing record instead of creating a second
|
|
189
|
+
sidebar entry. Workspace allocation treats every persisted session with
|
|
190
|
+
a `cwd` as occupying that workspace until the session record is deleted.
|
|
191
|
+
|
|
192
|
+
**Auto-resume.** SessionsPage doesn't show a "Resume" button. On
|
|
193
|
+
mount, if the active session's status isn't `running`, it calls
|
|
194
|
+
`resumeSession()`. `resumeSession()` in `api.js` keeps a per-id
|
|
195
|
+
in-flight Map so the same call from Sidebar.onClick and the
|
|
196
|
+
SessionsPage effect collapse into a single backend hit.
|
|
197
|
+
|
|
198
|
+
## Layout
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
ccsm/
|
|
202
|
+
├── server.js # Express + WebSocket; API-only in prod
|
|
203
|
+
├── bin/ccsm.js # launcher · detach, wake-on-protocol,
|
|
204
|
+
│ # auto-upgrade-restart, first-run hint
|
|
205
|
+
├── scripts/
|
|
206
|
+
│ ├── install.js # postinstall · ccsm:// + launcher.vbs
|
|
207
|
+
│ └── uninstall.js # preuninstall · cleanup + /api/shutdown
|
|
208
|
+
├── lib/
|
|
209
|
+
│ ├── persistedSessions.js # ~/.ccsm/sessions.json — source of truth
|
|
210
|
+
│ ├── folders.js # ~/.ccsm/folders.json — sidebar tree
|
|
211
|
+
│ ├── codexSeed.js # Codex CODEX_HOME probe + bundled light theme install
|
|
212
|
+
│ ├── workspace.js # ws-N allocation under workDir, repo clones
|
|
213
|
+
│ ├── webTerminal.js # in-process PTY pool · node-pty + WebSocket
|
|
214
|
+
│ ├── jsonStore.js # shared keyed-JSON store factory
|
|
215
|
+
│ └── config.js # loadConfig / saveConfig + DATA_DIR
|
|
216
|
+
├── pages-root/ # pushed to GH Pages /
|
|
217
|
+
│ ├── index.html # version router · probe localhost, redirect
|
|
218
|
+
│ ├── manifest.webmanifest # PWA · stable id, start_url: ./
|
|
219
|
+
│ └── favicon.svg
|
|
220
|
+
└── public/ # pushed to GH Pages /<version>/
|
|
221
|
+
├── index.html # workflow injects <meta ccsm-frontend-version>
|
|
222
|
+
├── manifest.webmanifest # per-version (links back to root scope)
|
|
223
|
+
├── favicon.svg
|
|
224
|
+
├── js/
|
|
225
|
+
│ ├── backend.js # httpBase() / wsBase() — same-origin local, cross-origin hosted
|
|
226
|
+
│ ├── main.js # boot · version guard · clock · heartbeat
|
|
227
|
+
│ ├── state.js # signals
|
|
228
|
+
│ ├── api.js # fetch wrapper + loaders + dedup-aware resumeSession
|
|
229
|
+
│ ├── streaming.js # NDJSON clone-progress stream
|
|
230
|
+
│ ├── dialog.js · toast.js # ccsmConfirm / ccsmPrompt / setToast
|
|
231
|
+
│ ├── html.js · icons.js · util.js
|
|
232
|
+
│ ├── components/
|
|
233
|
+
│ │ ├── App.js · Sidebar.js · PageTitleBar.js
|
|
234
|
+
│ │ ├── ServerStatus.js · Toast.js · OfflineBanner.js · DialogHost.js
|
|
235
|
+
│ │ ├── Card.js · Modal.js · Popover.js · Picker.js · EntityFormModal.js
|
|
236
|
+
│ │ ├── DirectoryPicker.js
|
|
237
|
+
│ │ ├── ProgressList.js · TerminalView.js · useDragSort.js
|
|
238
|
+
│ └── pages/
|
|
239
|
+
│ ├── SessionsPage.js · LaunchPage.js
|
|
240
|
+
│ ├── ConfigurePage.js · AboutPage.js
|
|
241
|
+
└── css/ # 12 focused stylesheets
|
|
242
|
+
├── tokens.css · base.css · layout.css
|
|
243
|
+
├── sidebar.css · cards.css · forms.css
|
|
244
|
+
├── widgets.css · feedback.css · modal.css
|
|
245
|
+
├── terminals.css · wco.css · responsive.css
|
|
246
|
+
|
|
247
|
+
~/.ccsm/ # or $CCSM_HOME
|
|
248
|
+
├── config.json # source of truth
|
|
249
|
+
├── sessions.json # persisted sessions (ccsm id, cliId, cwd, …)
|
|
250
|
+
├── folders.json # folder tree
|
|
251
|
+
├── server.log # detached-server stdout/stderr
|
|
252
|
+
├── .first-run-shown # marker so launcher only prints PWA hint once
|
|
253
|
+
└── browser-profile/ # Edge/Chrome --user-data-dir when browserMode=app
|
|
254
|
+
|
|
255
|
+
%LOCALAPPDATA%/ccsm/
|
|
256
|
+
└── launcher.vbs # silent ccsm:// dispatcher (written by postinstall)
|
|
257
|
+
|
|
258
|
+
HKCU\Software\Classes\ccsm # URL protocol registration
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
On first run, if a legacy `<repo>/data/` directory exists and `~/.ccsm/`
|
|
262
|
+
is empty, `lib/config.js` copies the old data over (one-time,
|
|
263
|
+
idempotent).
|
|
264
|
+
|
|
265
|
+
## Locked-in design decisions
|
|
266
|
+
|
|
267
|
+
**Single in-app terminal, no `wt`.** PTYs run in-process via node-pty
|
|
268
|
+
and stream to xterm.js over `/ws/terminal/:id`. We dropped the
|
|
269
|
+
`wt`-per-session, focus-by-HWND, snapshot-of-live-claudes layer
|
|
270
|
+
entirely — too platform-specific and the web terminal handles
|
|
271
|
+
everything the old path did.
|
|
272
|
+
|
|
273
|
+
**Workspace = folder holding multiple repo clones.** Each `ws-N` under
|
|
274
|
+
`workDir` contains a subdirectory per cloned repo. CLIs launch at the
|
|
275
|
+
single selected repo's directory; with zero or multiple repos selected,
|
|
276
|
+
they launch at the workspace root so selected repos are sibling folders.
|
|
277
|
+
|
|
278
|
+
**Workspace naming.** Auto-allocated names are `ws-1`, `ws-2`, …
|
|
279
|
+
(lowest free integer). Hand-named folders under `workDir` are still
|
|
280
|
+
picked up.
|
|
281
|
+
|
|
282
|
+
**Frontend trusts the backend's capability advertisement.**
|
|
283
|
+
`/api/capabilities` returns `{ webTerminal: true|false, ... }`. The
|
|
284
|
+
frontend uses ONLY features the backend says it has. Breaking changes
|
|
285
|
+
ship a new `/ccsm/<X.Y.Z>/` frontend; the router pins users to the
|
|
286
|
+
matching version.
|
|
287
|
+
|
|
288
|
+
**One source of truth for cross-origin.** `public/js/backend.js`
|
|
289
|
+
exports `httpBase()` and `wsBase()`. Localhost → same-origin (empty
|
|
290
|
+
base). Anything else → `http://localhost:7777`. CORS on the backend
|
|
291
|
+
allows `https://bakapiano.github.io` only — never `*`.
|
|
292
|
+
|
|
293
|
+
## API surface
|
|
294
|
+
|
|
295
|
+
| Method | Path | Purpose |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| GET / PUT | `/api/config` | read / replace config |
|
|
298
|
+
| GET | `/api/sessions` | list persisted sessions |
|
|
299
|
+
| PUT | `/api/sessions/:id` | rename / move to folder |
|
|
313
300
|
| DELETE | `/api/sessions/:id` | kill PTY + drop record |
|
|
314
|
-
| POST | `/api/sessions/:id/switch-cli` | change the persisted `cliId` for future resumes
|
|
301
|
+
| POST | `/api/sessions/:id/switch-cli` | change the persisted `cliId` for future resumes |
|
|
315
302
|
| POST | `/api/sessions/:id/stop` | kill the live PTY but keep the record; sets `manualStopped:true` so UI won't auto-resume |
|
|
316
303
|
| POST | `/api/sessions/new` | body `{cliId, cwd?, repos?, folderId?, title?}` — NDJSON stream (workspace · clone-progress · launched) |
|
|
317
|
-
| POST | `/api/sessions/:id/resume` | re-spawn at `cwd` with
|
|
318
|
-
| GET | `/api/
|
|
319
|
-
|
|
|
320
|
-
| GET | `/api/
|
|
321
|
-
| GET | `/api/
|
|
322
|
-
|
|
|
323
|
-
| GET | `/api/
|
|
324
|
-
|
|
|
325
|
-
|
|
|
326
|
-
|
|
|
327
|
-
| POST | `/api/
|
|
328
|
-
|
|
|
329
|
-
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
progress
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
**
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
- `--
|
|
395
|
-
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
406
|
-
-
|
|
407
|
-
|
|
408
|
-
**
|
|
409
|
-
-
|
|
410
|
-
-
|
|
411
|
-
-
|
|
412
|
-
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
git
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
gh
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
-
|
|
522
|
-
- `
|
|
523
|
-
- `
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
- **New REST routes**: `server.js` (keep under `/api/*`, use the `asyncH` wrapper, decide if it needs CORS by being in the allow-list).
|
|
539
|
-
- **Frontend page**: `public/js/pages/<Name>Page.js`, route in `App.js`, sidebar nav item in `Sidebar.js`, heading in `state.js`'s `TAB_HEADINGS`.
|
|
540
|
-
- **Persistent user data**: drop a JSON file under `~/.ccsm/` and use `lib/jsonStore.js`'s factory.
|
|
541
|
-
- **Different CLIs**: add a built-in to `DEFAULT_CLIS` in `lib/config.js` (set `newSessionIdArgs` if the CLI accepts a pre-assigned UUID, `resumeIdArgs` for precise resume), an icon to `public/js/icons.js`, and (if the CLI persists transcripts on disk and you want adopt support) a list helper to `lib/localCliSessions.js`.
|
|
542
|
-
- **A capability**: advertise via `/api/capabilities`. Frontend gates UI on `caps.<feature>`.
|
|
543
|
-
- **Bumping the frontend**: just `npm version <patch|minor|major>` + push. The GH Pages workflow publishes to `/<new-version>/` and the router redirects users to it.
|
|
304
|
+
| POST | `/api/sessions/:id/resume` | re-spawn at record `cwd` with the configured latest/picker resume args |
|
|
305
|
+
| GET | `/api/folders` · POST `/api/folders` · PUT/DELETE `/api/folders/:id` · POST `/api/folders/reorder` | folder CRUD |
|
|
306
|
+
| GET | `/api/workspaces` | workspaces under workDir with repo clone status + in-use flag |
|
|
307
|
+
| GET | `/api/browse` | directory browser for the Launch page workdir picker |
|
|
308
|
+
| GET | `/api/version` | `{ current, latest, updateAvailable, fetchedAt, cached, error? }` (npm registry cached 30 min, `?refresh=1` to bust) |
|
|
309
|
+
| POST | `/api/upgrade` | body `{target?}` — `npm i -g @bakapiano/ccsm@<target>` then self-restart |
|
|
310
|
+
| GET | `/api/capabilities` | `{ webTerminal: bool, ... }` for frontend feature gating |
|
|
311
|
+
| GET | `/api/health` | `{ ok, pid, version, name }` — used by router probe + heartbeat |
|
|
312
|
+
| POST | `/api/heartbeat` | called every 10s by the frontend; feeds lifecycle decisions |
|
|
313
|
+
| POST | `/api/spawn-browser` | open another browser window into the running server (used by `bin/ccsm.js` for auto-upgrade-restart) |
|
|
314
|
+
| POST | `/api/shutdown` | gracefulShutdown — used by uninstall + auto-upgrade |
|
|
315
|
+
| WS | `/ws/terminal/:id` | xterm.js bridge to a PTY in the webTerminal pool |
|
|
316
|
+
| GET (dev) | `/api/dev/ping` · `/api/dev/reload` | hot-reload SSE (only when running from a checkout) |
|
|
317
|
+
|
|
318
|
+
`/api/sessions/new` streams **NDJSON** (one JSON object per line). Event
|
|
319
|
+
types: `workspace`, `clone-start`, `clone-progress` (phase/percent/
|
|
320
|
+
current/total/detail), `clone-line` (raw git stderr line when not a
|
|
321
|
+
progress line), `clone-end`, `launched`, `done`. The frontend reads it
|
|
322
|
+
with `fetch().body.getReader()` + `TextDecoder` and updates per-repo
|
|
323
|
+
progress bars live.
|
|
324
|
+
|
|
325
|
+
**WebSocket Origin check**: same allow-list as CORS. The upgrade handler
|
|
326
|
+
rejects any Origin not in `ALLOWED_ORIGINS` (plus localhost/127.0.0.1).
|
|
327
|
+
Browsers always send Origin on WS upgrades.
|
|
328
|
+
|
|
329
|
+
## Non-obvious gotchas
|
|
330
|
+
|
|
331
|
+
**Resume is cwd-scoped.** ccsm assumes the upstream CLI can find the
|
|
332
|
+
right conversation from the current working directory when handed its
|
|
333
|
+
latest/picker resume command. We deliberately do not persist or replay
|
|
334
|
+
upstream session UUIDs.
|
|
335
|
+
|
|
336
|
+
**Workspace reservation is record-scoped.** A stopped session still owns
|
|
337
|
+
its workspace until the session record is deleted. This keeps auto
|
|
338
|
+
allocation from reusing `ws-N` and later resuming an older session in a
|
|
339
|
+
folder that has been repurposed.
|
|
340
|
+
|
|
341
|
+
**Auto-resume dedup is module-level in api.js.** Sidebar.onClick and
|
|
342
|
+
SessionsPage's effect can both fire for the same exited session in the
|
|
343
|
+
same tick. `resumeSession()` keeps a per-id in-flight `Map` so the
|
|
344
|
+
second caller awaits the first one's promise instead of issuing a
|
|
345
|
+
second `/resume`.
|
|
346
|
+
|
|
347
|
+
**Heartbeat watchdog only when launched.** Set via `CCSM_LAUNCHER=1` by
|
|
348
|
+
`bin/ccsm.js`. If you start `server.js` directly (e.g. dev), the
|
|
349
|
+
90-second timeout doesn't apply — convenient when stepping through
|
|
350
|
+
code, but you have to ctrl-c yourself when done.
|
|
351
|
+
|
|
352
|
+
**ccsm:// silent dispatch.** Direct registration of `ccsm.cmd` as the
|
|
353
|
+
protocol handler causes a brief console window flash (cmd hosts the
|
|
354
|
+
.cmd file). The wscript.exe + .vbs wrapper avoids it entirely — wscript
|
|
355
|
+
is a Windows-subsystem host (no console) and `Shell.Run(..., 0, False)`
|
|
356
|
+
launches the target hidden. The `.vbs` is generated at install time
|
|
357
|
+
with the correct ccsm.cmd path baked in.
|
|
358
|
+
|
|
359
|
+
**Edge --app handoff race.** When the user has an existing Edge profile
|
|
360
|
+
process running, `--app=URL --user-data-dir=DIR` against the same DIR
|
|
361
|
+
may cause the new msedge.exe to immediately exit after handing the URL
|
|
362
|
+
off to the existing process. Our child handle dies milliseconds after
|
|
363
|
+
spawn. The lifecycle hook ignores any browser-child exit inside the
|
|
364
|
+
first 5s for exactly this reason.
|
|
365
|
+
|
|
366
|
+
## Frontend design language
|
|
367
|
+
|
|
368
|
+
The UI deliberately copies **claude.ai's** calm light aesthetic — warm
|
|
369
|
+
cream surfaces, generous spacing, soft borders, **no orange highlights**.
|
|
370
|
+
The brand orange `#b3614a` survives only in the brand mark / wordmark
|
|
371
|
+
dot. Every other "highlight" use (selection, focus rings, dirty
|
|
372
|
+
indicators, progress bars, page-actions banner) is ink/gray.
|
|
373
|
+
|
|
374
|
+
**Palette** (CSS vars in `public/css/tokens.css`):
|
|
375
|
+
- `--bg` `#faf9f5` warm cream page background
|
|
376
|
+
- `--bg-elev` `#ffffff` card surfaces
|
|
377
|
+
- `--sidebar-bg` `#faf9f5` (same as `--bg`, single continuous surface)
|
|
378
|
+
- `--border` `#e8e3d5`
|
|
379
|
+
- `--ink` `#1a1815` body text (warm near-black, also used for terminal background)
|
|
380
|
+
- `--ink-mid` / `--ink-muted` / `--ink-faint`
|
|
381
|
+
- `--accent` `#b3614a` desaturated terracotta — brand only
|
|
382
|
+
- Status: green `#4a8a4a` idle · blue `#4a73a5` busy (pulsing) · red `#b73f3f` danger
|
|
383
|
+
|
|
384
|
+
**Type**:
|
|
385
|
+
- Body / headings: **Geist** (Google Fonts, 300–700).
|
|
386
|
+
- Mono: **JetBrains Mono** for paths, PIDs, sessionIds, branch tags.
|
|
387
|
+
- Always `font-variant-numeric: tabular-nums` on numeric cells.
|
|
388
|
+
|
|
389
|
+
**Buttons**:
|
|
390
|
+
- `.action` (default) — white bg, ink-mid border, ink text.
|
|
391
|
+
- `.action.primary` — black ink bg, white text. The "do this" CTA.
|
|
392
|
+
- `.action.subtle` — transparent bg, light border.
|
|
393
|
+
- `.action.danger` — filled red bg + white text.
|
|
394
|
+
|
|
395
|
+
**Layout**:
|
|
396
|
+
- Sidebar (collapsible, ~232px ↔ ~60px, state in `localStorage["ccsm.sidebar-collapsed"]`)
|
|
397
|
+
- brand mark + `CCSM.` wordmark
|
|
398
|
+
- tabs: Sessions / Launch / Configure / About
|
|
399
|
+
- folder tree of persisted sessions (drag-sortable)
|
|
400
|
+
- Page-title bar: title on the left, server-status pill + Refresh button on the right
|
|
401
|
+
- Top-right control group uses fixed `min-height: 28px` and `border-radius: 999px` so server-status + Refresh align as a coherent control row
|
|
402
|
+
|
|
403
|
+
**No emoji in the UI** unless the user typed it. Use inline SVG icons
|
|
404
|
+
everywhere (line stroke, 1.5–2px) so they take `currentColor`.
|
|
405
|
+
|
|
406
|
+
**PWA + WCO**:
|
|
407
|
+
- `display_override: ["window-controls-overlay", "standalone"]` in the manifest. When installed and launched as PWA, the title bar's middle is reclaimed; only OS controls float top-right.
|
|
408
|
+
- `public/css/wco.css` provides drag regions (`-webkit-app-region: drag`) on `.sidebar-brand`, `.page-head`, etc., unconditional. Interactive elements opt out via the no-drag block.
|
|
409
|
+
- The root PWA manifest at `pages-root/manifest.webmanifest` has stable `id: /ccsm/` so installs survive across version-router redirects to new `/ccsm/<X.Y.Z>/` subdirs.
|
|
410
|
+
|
|
411
|
+
## Versioning
|
|
412
|
+
|
|
413
|
+
The hosted frontend lives at `https://bakapiano.github.io/ccsm/`. The
|
|
414
|
+
deploy workflow publishes two things to gh-pages on every push to main:
|
|
415
|
+
|
|
416
|
+
1. `pages-root/` → `/` (the router, plus root PWA manifest)
|
|
417
|
+
2. `public/` → `/<pkg.version>/` (the per-version frontend; workflow injects `<meta name="ccsm-frontend-version">` at build time)
|
|
418
|
+
|
|
419
|
+
Old version dirs stay forever (`keep_files: true`), so a user on an
|
|
420
|
+
older backend keeps loading the matching frontend until they upgrade.
|
|
421
|
+
|
|
422
|
+
`bin/ccsm.js` does auto-upgrade-restart: when the user runs `ccsm` and
|
|
423
|
+
the installed package version differs from a running backend, it POSTs
|
|
424
|
+
`/api/shutdown` to the old, waits for the port to free, then spawns a
|
|
425
|
+
fresh server. So `npm i -g @bakapiano/ccsm@latest && ccsm` is one
|
|
426
|
+
seamless step. From the frontend, the About page's Upgrade button
|
|
427
|
+
achieves the same thing without leaving the browser.
|
|
428
|
+
|
|
429
|
+
### Release process
|
|
430
|
+
|
|
431
|
+
**Do not release or push without explicit permission from the user.**
|
|
432
|
+
This means: don't run `git push`, don't run `npm version`, don't run
|
|
433
|
+
`gh release edit --draft=false`, don't commit + tag in one breath
|
|
434
|
+
because "the fix is ready". Wait for the user to say so. The instinct
|
|
435
|
+
to ship is wrong here — a half-baked release on the public npm registry
|
|
436
|
+
is much worse than a few minutes of waiting.
|
|
437
|
+
|
|
438
|
+
Three artifacts ship per release: a git tag, a GitHub Release, and an
|
|
439
|
+
npm publish. The whole thing is CI-driven — you never `npm publish`
|
|
440
|
+
locally — but it requires you to drive three steps in order:
|
|
441
|
+
|
|
442
|
+
1. **Commit + bump + push (local).** Stage everything, write a release
|
|
443
|
+
commit, then bump + tag + push:
|
|
444
|
+
|
|
445
|
+
```powershell
|
|
446
|
+
git add -A
|
|
447
|
+
git commit -m "vX.Y.Z: <one-line summary>
|
|
448
|
+
|
|
449
|
+
<body>
|
|
450
|
+
|
|
451
|
+
Co-Authored-By: Claude ..."
|
|
452
|
+
npm --prefix . version <patch|minor|major> -m "v%s"
|
|
453
|
+
git push origin main
|
|
454
|
+
git push origin vX.Y.Z
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`npm version` writes the new version into `package.json` +
|
|
458
|
+
`package-lock.json`, creates its OWN commit, and tags it. The
|
|
459
|
+
`--prefix .` is needed on Windows where bare `npm version` errors on
|
|
460
|
+
the global `%APPDATA%\npm\package.json`. Push BOTH `main` and the
|
|
461
|
+
tag — pushing only main skips the tag-triggered draft-release
|
|
462
|
+
workflow.
|
|
463
|
+
|
|
464
|
+
2. **Tag-push fires two workflows automatically:**
|
|
465
|
+
- `Deploy frontend to GitHub Pages` → publishes `pages-root/` → `/`
|
|
466
|
+
and `public/` → `/<X.Y.Z>/` on `gh-pages`. Old `/<X.Y.Z>/`
|
|
467
|
+
subdirs stay forever (`keep_files: true`).
|
|
468
|
+
- `Draft GitHub Release on tag push` → creates a **draft** release
|
|
469
|
+
for `vX.Y.Z`.
|
|
470
|
+
|
|
471
|
+
3. **Publish the draft (manual one-liner):**
|
|
472
|
+
|
|
473
|
+
```powershell
|
|
474
|
+
gh release edit vX.Y.Z --draft=false
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
This flips the draft to "published", which fires the third workflow
|
|
478
|
+
— `Publish to npm` — using the `NPM_TOKEN` repo secret with
|
|
479
|
+
provenance. The runner needs ~30s; verify with
|
|
480
|
+
`gh run watch <run-id> --exit-status` or just refresh npmjs.com.
|
|
481
|
+
|
|
482
|
+
The reason for the draft step instead of auto-publishing on tag push:
|
|
483
|
+
gives you a chance to abort a half-baked tag (delete the draft +
|
|
484
|
+
`git push --delete origin vX.Y.Z`) before it lands on the public
|
|
485
|
+
registry.
|
|
486
|
+
|
|
487
|
+
### Why we don't publish from the local box
|
|
488
|
+
|
|
489
|
+
`npm publish` from a dev machine works in principle but skips
|
|
490
|
+
provenance attestation (the sigstore + GitHub OIDC binding that npm
|
|
491
|
+
displays as a "Provenance" badge on the package page). CI has the OIDC
|
|
492
|
+
token; you don't. Local publish also wouldn't have the consistent
|
|
493
|
+
runner state, so reproducible-build claims fall apart. The pipeline
|
|
494
|
+
exists; use it.
|
|
495
|
+
|
|
496
|
+
## Cross-platform
|
|
497
|
+
|
|
498
|
+
Today: Windows-first.
|
|
499
|
+
|
|
500
|
+
Cross-platform-clean already:
|
|
501
|
+
- Frontend (pure web)
|
|
502
|
+
- Router page (pure HTML/JS)
|
|
503
|
+
- `bin/ccsm.js` (pure node)
|
|
504
|
+
- `lib/webTerminal.js` (node-pty handles platform)
|
|
505
|
+
- `lib/persistedSessions.js`, `lib/folders.js`, `lib/config.js`, `lib/jsonStore.js`, `lib/workspace.js` (fs only)
|
|
506
|
+
- `server.js` Express + ws
|
|
507
|
+
|
|
508
|
+
Windows-specific (need ports for Mac/Linux):
|
|
509
|
+
- `scripts/install.js` — uses `reg.exe` and `wscript.exe`. Mac: write `Info.plist` with `CFBundleURLTypes`. Linux: write `~/.local/share/applications/ccsm.desktop` with `MimeType=x-scheme-handler/ccsm`.
|
|
510
|
+
- The `--app=` browser detection and PATH-merge in `server.js` are Windows-shaped (Edge first, registry HKCU\Environment for PATH).
|
|
511
|
+
|
|
512
|
+
Pattern for adding a platform: `switch (process.platform)` at each
|
|
513
|
+
entry point in those files. Each platform branch is roughly 50-100
|
|
514
|
+
lines.
|
|
515
|
+
|
|
516
|
+
## Extending
|
|
517
|
+
|
|
518
|
+
When adding features, the natural extension points:
|
|
519
|
+
- **New REST routes**: `server.js` (keep under `/api/*`, use the `asyncH` wrapper, decide if it needs CORS by being in the allow-list).
|
|
520
|
+
- **Frontend page**: `public/js/pages/<Name>Page.js`, route in `App.js`, sidebar nav item in `Sidebar.js`, heading in `state.js`'s `TAB_HEADINGS`.
|
|
521
|
+
- **Persistent user data**: drop a JSON file under `~/.ccsm/` and use `lib/jsonStore.js`'s factory.
|
|
522
|
+
- **Different CLIs**: add a built-in to `DEFAULT_CLIS` in `lib/config.js` with `resumeLatestArgs` / `resumePickerArgs`, and add an icon to `public/js/icons.js`.
|
|
523
|
+
- **A capability**: advertise via `/api/capabilities`. Frontend gates UI on `caps.<feature>`.
|
|
524
|
+
- **Bumping the frontend**: just `npm version <patch|minor|major>` + push. The GH Pages workflow publishes to `/<new-version>/` and the router redirects users to it.
|