@bakapiano/ccsm 0.22.5 → 0.22.7
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 +279 -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 +225 -225
- 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 +177 -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 +547 -553
- 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 +28 -9
- package/public/js/components/XtermTerminal.js +62 -2
- 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 +728 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +73 -80
- package/public/js/state.js +335 -335
- 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 +1820 -1807
- package/public/manifest.webmanifest +0 -25
- package/public/setup/index.html +0 -567
package/lib/webTerminal.js
CHANGED
|
@@ -1,126 +1,126 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// ccsm in-process PTY pool. Used by the "web terminal" launch path:
|
|
4
|
-
// claude (or any cmd) runs as a child of ccsm and its stdio is bridged
|
|
5
|
-
// to one or more WebSocket clients via xterm.js in the browser.
|
|
6
|
-
//
|
|
7
|
-
// Lifecycle: a PTY entry is created by spawn(), broadcasts every output
|
|
8
|
-
// chunk to all attached sockets, keeps a rolling history ring so a fresh
|
|
9
|
-
// connection can replay recent output. attach() wires a websocket to an
|
|
10
|
-
// entry, kill() ends a PTY explicitly, list() returns metadata for UI.
|
|
11
|
-
//
|
|
12
|
-
// node-pty is optional (Windows native binary). If it failed to load,
|
|
13
|
-
// `available` is false and spawn() throws — server.js gates the
|
|
14
|
-
// /api/sessions/web route on this flag so install failures degrade
|
|
15
|
-
// gracefully to wt-only mode.
|
|
16
|
-
|
|
17
|
-
const path = require('node:path');
|
|
18
|
-
|
|
19
|
-
let pty = null;
|
|
20
|
-
let loadError = null;
|
|
21
|
-
try {
|
|
22
|
-
pty = require('node-pty');
|
|
23
|
-
} catch (e) {
|
|
24
|
-
loadError = e;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const HISTORY_BYTES = 256 * 1024;
|
|
28
|
-
|
|
29
|
-
// Map<id, { id, pty, history, sockets:Set<ws>, meta }>
|
|
30
|
-
const sessions = new Map();
|
|
31
|
-
|
|
32
|
-
function genId() {
|
|
33
|
-
return 'web-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// Spawn a new PTY. `command` and `args` are passed straight to node-pty.
|
|
39
|
-
// `meta` is whatever the caller wants surfaced to the UI (title, cwd, etc).
|
|
40
|
-
// `id` lets the caller dictate the session id (so persistedSessions can
|
|
41
|
-
// keep PTY id == record id); defaults to an auto-generated one.
|
|
42
|
-
// `onData` / `onExit` are optional callbacks fired alongside the built-in
|
|
43
|
-
// history-recording + socket-broadcast, so persistedSessions can mark
|
|
44
|
-
// status/lastActiveAt without us having to drill the dependency in here.
|
|
45
|
-
// Throws if node-pty isn't available.
|
|
46
|
-
function spawn({ command, args = [], cwd, env, cols = 120, rows = 30, meta = {}, id, onData, onExit }) {
|
|
47
|
-
if (!pty) {
|
|
48
|
-
const err = new Error('node-pty is not available · ' + (loadError && loadError.message || 'unknown'));
|
|
49
|
-
err.code = 'PTY_UNAVAILABLE';
|
|
50
|
-
throw err;
|
|
51
|
-
}
|
|
52
|
-
const entryId = id || genId();
|
|
53
|
-
const ptyOpts = {
|
|
54
|
-
name: 'xterm-256color',
|
|
55
|
-
cols, rows,
|
|
56
|
-
cwd: cwd ? path.resolve(cwd) : process.cwd(),
|
|
57
|
-
// Advertise 24-bit colour. xterm.js renders truecolor, and CLIs like
|
|
58
|
-
// claude paint their diffs with 24-bit DARK red/green backgrounds +
|
|
59
|
-
// white text when COLORTERM says so. Without it they downgrade to the
|
|
60
|
-
// bright ANSI-16 red/green backgrounds, which wash the white foreground
|
|
61
|
-
// out (the "white text gets buried" report). VSCode sets the same on its
|
|
62
|
-
// terminal PTYs; our ANSI-16 palette already matches VSCode's verbatim.
|
|
63
|
-
env: { ...process.env, ...(env || {}), COLORTERM: 'truecolor' },
|
|
64
|
-
};
|
|
65
|
-
if (process.platform === 'win32') {
|
|
66
|
-
ptyOpts.useConpty = true;
|
|
67
|
-
ptyOpts.useConptyDll = true;
|
|
68
|
-
}
|
|
69
|
-
const proc = pty.spawn(command, args, ptyOpts);
|
|
70
|
-
const entry = {
|
|
71
|
-
id: entryId,
|
|
72
|
-
pty: proc,
|
|
73
|
-
history: '',
|
|
74
|
-
sockets: new Set(),
|
|
75
|
-
meta: { ...meta, startedAt: Date.now(), command, args, cwd: cwd || process.cwd(), pid: proc.pid },
|
|
76
|
-
exitCode: null,
|
|
77
|
-
exitedAt: null,
|
|
78
|
-
onDataExtra: onData,
|
|
79
|
-
onExitExtra: onExit,
|
|
80
|
-
};
|
|
81
|
-
proc.onData((data) => {
|
|
82
|
-
entry.history = (entry.history + data);
|
|
83
|
-
if (entry.history.length > HISTORY_BYTES) {
|
|
84
|
-
entry.history = entry.history.slice(-HISTORY_BYTES);
|
|
85
|
-
}
|
|
86
|
-
const frame = JSON.stringify({ type: 'output', data });
|
|
87
|
-
for (const ws of entry.sockets) {
|
|
88
|
-
try { ws.send(frame); } catch {}
|
|
89
|
-
}
|
|
90
|
-
if (entry.onDataExtra) { try { entry.onDataExtra(data); } catch {} }
|
|
91
|
-
});
|
|
92
|
-
proc.onExit(({ exitCode, signal }) => {
|
|
93
|
-
// If a respawn replaced us in the pool (same entryId, new entry
|
|
94
|
-
// object), do not touch persistedSessions or schedule a delete —
|
|
95
|
-
// those belong to the new entry now. Without this guard, a slow-
|
|
96
|
-
// dying old PTY would fire markExited on the same sessionId and
|
|
97
|
-
// clobber the new spawn's markRunning state.
|
|
98
|
-
if (sessions.get(entryId) !== entry) return;
|
|
99
|
-
entry.exitCode = exitCode;
|
|
100
|
-
entry.exitedAt = Date.now();
|
|
101
|
-
const frame = JSON.stringify({ type: 'exit', code: exitCode, signal });
|
|
102
|
-
for (const ws of entry.sockets) {
|
|
103
|
-
try { ws.send(frame); } catch {}
|
|
104
|
-
}
|
|
105
|
-
if (entry.onExitExtra) { try { entry.onExitExtra({ exitCode, signal }); } catch {} }
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
if (sessions.get(entryId) === entry) sessions.delete(entryId);
|
|
108
|
-
}, 30_000);
|
|
109
|
-
});
|
|
110
|
-
// If a previous entry exists under the same id (respawn), kill its
|
|
111
|
-
// pty so we don't have zombie claude.exe processes hanging on. The
|
|
112
|
-
// onExit guard above ensures its callback no-ops once we've taken
|
|
113
|
-
// over the slot.
|
|
114
|
-
const prev = sessions.get(entryId);
|
|
115
|
-
if (prev && !prev.exitedAt) {
|
|
116
|
-
try { prev.pty.kill(); } catch {}
|
|
117
|
-
}
|
|
118
|
-
sessions.set(entryId, entry);
|
|
119
|
-
return entry;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Strip ANSI sequences from history that would cause spurious
|
|
123
|
-
// terminal-to-host responses if a fresh xterm.js re-parses the replay.
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ccsm in-process PTY pool. Used by the "web terminal" launch path:
|
|
4
|
+
// claude (or any cmd) runs as a child of ccsm and its stdio is bridged
|
|
5
|
+
// to one or more WebSocket clients via xterm.js in the browser.
|
|
6
|
+
//
|
|
7
|
+
// Lifecycle: a PTY entry is created by spawn(), broadcasts every output
|
|
8
|
+
// chunk to all attached sockets, keeps a rolling history ring so a fresh
|
|
9
|
+
// connection can replay recent output. attach() wires a websocket to an
|
|
10
|
+
// entry, kill() ends a PTY explicitly, list() returns metadata for UI.
|
|
11
|
+
//
|
|
12
|
+
// node-pty is optional (Windows native binary). If it failed to load,
|
|
13
|
+
// `available` is false and spawn() throws — server.js gates the
|
|
14
|
+
// /api/sessions/web route on this flag so install failures degrade
|
|
15
|
+
// gracefully to wt-only mode.
|
|
16
|
+
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
let pty = null;
|
|
20
|
+
let loadError = null;
|
|
21
|
+
try {
|
|
22
|
+
pty = require('node-pty');
|
|
23
|
+
} catch (e) {
|
|
24
|
+
loadError = e;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HISTORY_BYTES = 256 * 1024;
|
|
28
|
+
|
|
29
|
+
// Map<id, { id, pty, history, sockets:Set<ws>, meta }>
|
|
30
|
+
const sessions = new Map();
|
|
31
|
+
|
|
32
|
+
function genId() {
|
|
33
|
+
return 'web-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Spawn a new PTY. `command` and `args` are passed straight to node-pty.
|
|
39
|
+
// `meta` is whatever the caller wants surfaced to the UI (title, cwd, etc).
|
|
40
|
+
// `id` lets the caller dictate the session id (so persistedSessions can
|
|
41
|
+
// keep PTY id == record id); defaults to an auto-generated one.
|
|
42
|
+
// `onData` / `onExit` are optional callbacks fired alongside the built-in
|
|
43
|
+
// history-recording + socket-broadcast, so persistedSessions can mark
|
|
44
|
+
// status/lastActiveAt without us having to drill the dependency in here.
|
|
45
|
+
// Throws if node-pty isn't available.
|
|
46
|
+
function spawn({ command, args = [], cwd, env, cols = 120, rows = 30, meta = {}, id, onData, onExit }) {
|
|
47
|
+
if (!pty) {
|
|
48
|
+
const err = new Error('node-pty is not available · ' + (loadError && loadError.message || 'unknown'));
|
|
49
|
+
err.code = 'PTY_UNAVAILABLE';
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
const entryId = id || genId();
|
|
53
|
+
const ptyOpts = {
|
|
54
|
+
name: 'xterm-256color',
|
|
55
|
+
cols, rows,
|
|
56
|
+
cwd: cwd ? path.resolve(cwd) : process.cwd(),
|
|
57
|
+
// Advertise 24-bit colour. xterm.js renders truecolor, and CLIs like
|
|
58
|
+
// claude paint their diffs with 24-bit DARK red/green backgrounds +
|
|
59
|
+
// white text when COLORTERM says so. Without it they downgrade to the
|
|
60
|
+
// bright ANSI-16 red/green backgrounds, which wash the white foreground
|
|
61
|
+
// out (the "white text gets buried" report). VSCode sets the same on its
|
|
62
|
+
// terminal PTYs; our ANSI-16 palette already matches VSCode's verbatim.
|
|
63
|
+
env: { ...process.env, ...(env || {}), COLORTERM: 'truecolor' },
|
|
64
|
+
};
|
|
65
|
+
if (process.platform === 'win32') {
|
|
66
|
+
ptyOpts.useConpty = true;
|
|
67
|
+
ptyOpts.useConptyDll = true;
|
|
68
|
+
}
|
|
69
|
+
const proc = pty.spawn(command, args, ptyOpts);
|
|
70
|
+
const entry = {
|
|
71
|
+
id: entryId,
|
|
72
|
+
pty: proc,
|
|
73
|
+
history: '',
|
|
74
|
+
sockets: new Set(),
|
|
75
|
+
meta: { ...meta, startedAt: Date.now(), command, args, cwd: cwd || process.cwd(), pid: proc.pid },
|
|
76
|
+
exitCode: null,
|
|
77
|
+
exitedAt: null,
|
|
78
|
+
onDataExtra: onData,
|
|
79
|
+
onExitExtra: onExit,
|
|
80
|
+
};
|
|
81
|
+
proc.onData((data) => {
|
|
82
|
+
entry.history = (entry.history + data);
|
|
83
|
+
if (entry.history.length > HISTORY_BYTES) {
|
|
84
|
+
entry.history = entry.history.slice(-HISTORY_BYTES);
|
|
85
|
+
}
|
|
86
|
+
const frame = JSON.stringify({ type: 'output', data });
|
|
87
|
+
for (const ws of entry.sockets) {
|
|
88
|
+
try { ws.send(frame); } catch {}
|
|
89
|
+
}
|
|
90
|
+
if (entry.onDataExtra) { try { entry.onDataExtra(data); } catch {} }
|
|
91
|
+
});
|
|
92
|
+
proc.onExit(({ exitCode, signal }) => {
|
|
93
|
+
// If a respawn replaced us in the pool (same entryId, new entry
|
|
94
|
+
// object), do not touch persistedSessions or schedule a delete —
|
|
95
|
+
// those belong to the new entry now. Without this guard, a slow-
|
|
96
|
+
// dying old PTY would fire markExited on the same sessionId and
|
|
97
|
+
// clobber the new spawn's markRunning state.
|
|
98
|
+
if (sessions.get(entryId) !== entry) return;
|
|
99
|
+
entry.exitCode = exitCode;
|
|
100
|
+
entry.exitedAt = Date.now();
|
|
101
|
+
const frame = JSON.stringify({ type: 'exit', code: exitCode, signal });
|
|
102
|
+
for (const ws of entry.sockets) {
|
|
103
|
+
try { ws.send(frame); } catch {}
|
|
104
|
+
}
|
|
105
|
+
if (entry.onExitExtra) { try { entry.onExitExtra({ exitCode, signal }); } catch {} }
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
if (sessions.get(entryId) === entry) sessions.delete(entryId);
|
|
108
|
+
}, 30_000);
|
|
109
|
+
});
|
|
110
|
+
// If a previous entry exists under the same id (respawn), kill its
|
|
111
|
+
// pty so we don't have zombie claude.exe processes hanging on. The
|
|
112
|
+
// onExit guard above ensures its callback no-ops once we've taken
|
|
113
|
+
// over the slot.
|
|
114
|
+
const prev = sessions.get(entryId);
|
|
115
|
+
if (prev && !prev.exitedAt) {
|
|
116
|
+
try { prev.pty.kill(); } catch {}
|
|
117
|
+
}
|
|
118
|
+
sessions.set(entryId, entry);
|
|
119
|
+
return entry;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Strip ANSI sequences from history that would cause spurious
|
|
123
|
+
// terminal-to-host responses if a fresh xterm.js re-parses the replay.
|
|
124
124
|
// Specifically: device-attribute / device-status queries (CSI c, CSI 0c,
|
|
125
125
|
// CSI >0c, CSI 5n, CSI 6n, …) and OSC 10/11 default color queries. The
|
|
126
126
|
// original xterm already answered them, but on attach we replay everything;
|
|
@@ -134,110 +134,110 @@ function scrubReplayResponses(history) {
|
|
|
134
134
|
// CSI [ ? Ps c (primary DA query)
|
|
135
135
|
.replace(/\x1b\[[?>0-9]*c/g, '')
|
|
136
136
|
// CSI [ Ps n (device status / cursor position queries)
|
|
137
|
-
.replace(/\x1b\[[?>0-9;]*n/g, '');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Wire a websocket to a session. Replays history immediately so the
|
|
141
|
-
// client sees recent context; then forwards input/resize messages from
|
|
142
|
-
// the client to the PTY and broadcast outputs back via onData above.
|
|
143
|
-
function attach(id, ws) {
|
|
144
|
-
const entry = sessions.get(id);
|
|
145
|
-
if (!entry) {
|
|
146
|
-
try { ws.close(4404, 'no such terminal'); } catch {}
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
// Latest-wins: a session has at most one live WebSocket at any
|
|
150
|
-
// moment. The new attach displaces any existing ones — keeps PTY
|
|
151
|
-
// resize semantics unambiguous (each client sends its own dimensions
|
|
152
|
-
// for its own viewport; with two clients fighting, claude's TUI
|
|
153
|
-
// re-renders for whichever sent last and the other side sees a
|
|
154
|
-
// mis-sized layout). Close code 4001 + reason lets the displaced
|
|
155
|
-
// client show a clearer message than the generic "[disconnected]".
|
|
156
|
-
for (const other of entry.sockets) {
|
|
157
|
-
try { other.close(4001, 'displaced by another client'); } catch {}
|
|
158
|
-
}
|
|
137
|
+
.replace(/\x1b\[[?>0-9;]*n/g, '');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Wire a websocket to a session. Replays history immediately so the
|
|
141
|
+
// client sees recent context; then forwards input/resize messages from
|
|
142
|
+
// the client to the PTY and broadcast outputs back via onData above.
|
|
143
|
+
function attach(id, ws) {
|
|
144
|
+
const entry = sessions.get(id);
|
|
145
|
+
if (!entry) {
|
|
146
|
+
try { ws.close(4404, 'no such terminal'); } catch {}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Latest-wins: a session has at most one live WebSocket at any
|
|
150
|
+
// moment. The new attach displaces any existing ones — keeps PTY
|
|
151
|
+
// resize semantics unambiguous (each client sends its own dimensions
|
|
152
|
+
// for its own viewport; with two clients fighting, claude's TUI
|
|
153
|
+
// re-renders for whichever sent last and the other side sees a
|
|
154
|
+
// mis-sized layout). Close code 4001 + reason lets the displaced
|
|
155
|
+
// client show a clearer message than the generic "[disconnected]".
|
|
156
|
+
for (const other of entry.sockets) {
|
|
157
|
+
try { other.close(4001, 'displaced by another client'); } catch {}
|
|
158
|
+
}
|
|
159
159
|
entry.sockets.clear();
|
|
160
160
|
entry.sockets.add(ws);
|
|
161
161
|
if (entry.history) {
|
|
162
162
|
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history), replay: true })); } catch {}
|
|
163
163
|
}
|
|
164
|
-
if (entry.exitedAt) {
|
|
165
|
-
try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
|
|
166
|
-
} else {
|
|
167
|
-
try { ws.send(JSON.stringify({ type: 'attached', meta: entry.meta })); } catch {}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
ws.on('message', (msg) => {
|
|
171
|
-
let event;
|
|
172
|
-
try { event = JSON.parse(msg.toString()); } catch { return; }
|
|
173
|
-
if (entry.exitedAt) return; // PTY is dead, ignore further input
|
|
174
|
-
switch (event.type) {
|
|
175
|
-
case 'input':
|
|
176
|
-
if (typeof event.data === 'string') {
|
|
177
|
-
if (process.env.CCSM_DEBUG_PASTE === '1') {
|
|
178
|
-
const d = event.data;
|
|
179
|
-
const hex = Buffer.from(d, 'utf8').toString('hex').match(/.{1,2}/g).join(' ');
|
|
180
|
-
console.log(`[pty.write id=${id}] len=${d.length} hex=${hex.slice(0, 400)}${hex.length > 400 ? '...' : ''}`);
|
|
181
|
-
}
|
|
182
|
-
entry.pty.write(event.data);
|
|
183
|
-
}
|
|
184
|
-
break;
|
|
185
|
-
case 'resize':
|
|
186
|
-
if (Number(event.cols) > 0 && Number(event.rows) > 0) {
|
|
187
|
-
try { entry.pty.resize(Number(event.cols), Number(event.rows)); } catch {}
|
|
188
|
-
}
|
|
189
|
-
break;
|
|
190
|
-
case 'kill':
|
|
191
|
-
kill(id);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
ws.on('close', () => {
|
|
197
|
-
entry.sockets.delete(ws);
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function kill(id) {
|
|
202
|
-
const entry = sessions.get(id);
|
|
203
|
-
if (!entry || entry.exitedAt) return false;
|
|
204
|
-
try { entry.pty.kill(); } catch {}
|
|
205
|
-
return true;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Public summary for the frontend. Don't leak the pty / sockets objects.
|
|
209
|
-
function describe(entry) {
|
|
210
|
-
return {
|
|
211
|
-
id: entry.id,
|
|
212
|
-
meta: entry.meta,
|
|
213
|
-
attached: entry.sockets.size,
|
|
214
|
-
exitedAt: entry.exitedAt,
|
|
215
|
-
exitCode: entry.exitCode,
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function list() {
|
|
220
|
-
return Array.from(sessions.values()).map(describe);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function get(id) {
|
|
224
|
-
const e = sessions.get(id);
|
|
225
|
-
return e ? describe(e) : null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function killAll() {
|
|
229
|
-
for (const e of sessions.values()) {
|
|
230
|
-
try { e.pty.kill(); } catch {}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
module.exports = {
|
|
235
|
-
available: !!pty,
|
|
236
|
-
loadError,
|
|
237
|
-
spawn,
|
|
238
|
-
attach,
|
|
239
|
-
kill,
|
|
240
|
-
list,
|
|
241
|
-
get,
|
|
242
|
-
killAll,
|
|
243
|
-
};
|
|
164
|
+
if (entry.exitedAt) {
|
|
165
|
+
try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
|
|
166
|
+
} else {
|
|
167
|
+
try { ws.send(JSON.stringify({ type: 'attached', meta: entry.meta })); } catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ws.on('message', (msg) => {
|
|
171
|
+
let event;
|
|
172
|
+
try { event = JSON.parse(msg.toString()); } catch { return; }
|
|
173
|
+
if (entry.exitedAt) return; // PTY is dead, ignore further input
|
|
174
|
+
switch (event.type) {
|
|
175
|
+
case 'input':
|
|
176
|
+
if (typeof event.data === 'string') {
|
|
177
|
+
if (process.env.CCSM_DEBUG_PASTE === '1') {
|
|
178
|
+
const d = event.data;
|
|
179
|
+
const hex = Buffer.from(d, 'utf8').toString('hex').match(/.{1,2}/g).join(' ');
|
|
180
|
+
console.log(`[pty.write id=${id}] len=${d.length} hex=${hex.slice(0, 400)}${hex.length > 400 ? '...' : ''}`);
|
|
181
|
+
}
|
|
182
|
+
entry.pty.write(event.data);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case 'resize':
|
|
186
|
+
if (Number(event.cols) > 0 && Number(event.rows) > 0) {
|
|
187
|
+
try { entry.pty.resize(Number(event.cols), Number(event.rows)); } catch {}
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case 'kill':
|
|
191
|
+
kill(id);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
ws.on('close', () => {
|
|
197
|
+
entry.sockets.delete(ws);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function kill(id) {
|
|
202
|
+
const entry = sessions.get(id);
|
|
203
|
+
if (!entry || entry.exitedAt) return false;
|
|
204
|
+
try { entry.pty.kill(); } catch {}
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Public summary for the frontend. Don't leak the pty / sockets objects.
|
|
209
|
+
function describe(entry) {
|
|
210
|
+
return {
|
|
211
|
+
id: entry.id,
|
|
212
|
+
meta: entry.meta,
|
|
213
|
+
attached: entry.sockets.size,
|
|
214
|
+
exitedAt: entry.exitedAt,
|
|
215
|
+
exitCode: entry.exitCode,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function list() {
|
|
220
|
+
return Array.from(sessions.values()).map(describe);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function get(id) {
|
|
224
|
+
const e = sessions.get(id);
|
|
225
|
+
return e ? describe(e) : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function killAll() {
|
|
229
|
+
for (const e of sessions.values()) {
|
|
230
|
+
try { e.pty.kill(); } catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
available: !!pty,
|
|
236
|
+
loadError,
|
|
237
|
+
spawn,
|
|
238
|
+
attach,
|
|
239
|
+
kill,
|
|
240
|
+
list,
|
|
241
|
+
get,
|
|
242
|
+
killAll,
|
|
243
|
+
};
|