@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/lib/webTerminal.js
CHANGED
|
@@ -1,241 +1,243 @@
|
|
|
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
|
-
// CSI >0c, CSI 5n, CSI 6n, …)
|
|
126
|
-
// them, but on attach we replay everything;
|
|
127
|
-
// xterm answers them too, the reply goes through
|
|
128
|
-
// the CLI sees garbage bytes in its stdin, and echoes
|
|
129
|
-
// visible junk like `[?12;2c
|
|
125
|
+
// CSI >0c, CSI 5n, CSI 6n, …) and OSC 10/11 default color queries. The
|
|
126
|
+
// original xterm already answered them, but on attach we replay everything;
|
|
127
|
+
// without scrubbing, the new xterm answers them too, the reply goes through
|
|
128
|
+
// our onData→PTY pipe, the CLI sees garbage bytes in its stdin, and echoes
|
|
129
|
+
// them back as visible junk like `[?12;2c` or `]11;rgb:…`.
|
|
130
130
|
function scrubReplayResponses(history) {
|
|
131
131
|
return history
|
|
132
|
+
// OSC 10/11 ; ? ST/BEL (default foreground/background color queries)
|
|
133
|
+
.replace(/\x1b\](?:10|11);\?(?:\x07|\x1b\\)/g, '')
|
|
132
134
|
// CSI [ ? Ps c (primary DA query)
|
|
133
135
|
.replace(/\x1b\[[?>0-9]*c/g, '')
|
|
134
136
|
// CSI [ Ps n (device status / cursor position queries)
|
|
135
|
-
.replace(/\x1b\[[?>0-9;]*n/g, '');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Wire a websocket to a session. Replays history immediately so the
|
|
139
|
-
// client sees recent context; then forwards input/resize messages from
|
|
140
|
-
// the client to the PTY and broadcast outputs back via onData above.
|
|
141
|
-
function attach(id, ws) {
|
|
142
|
-
const entry = sessions.get(id);
|
|
143
|
-
if (!entry) {
|
|
144
|
-
try { ws.close(4404, 'no such terminal'); } catch {}
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
// Latest-wins: a session has at most one live WebSocket at any
|
|
148
|
-
// moment. The new attach displaces any existing ones — keeps PTY
|
|
149
|
-
// resize semantics unambiguous (each client sends its own dimensions
|
|
150
|
-
// for its own viewport; with two clients fighting, claude's TUI
|
|
151
|
-
// re-renders for whichever sent last and the other side sees a
|
|
152
|
-
// mis-sized layout). Close code 4001 + reason lets the displaced
|
|
153
|
-
// client show a clearer message than the generic "[disconnected]".
|
|
154
|
-
for (const other of entry.sockets) {
|
|
155
|
-
try { other.close(4001, 'displaced by another client'); } catch {}
|
|
156
|
-
}
|
|
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
|
+
}
|
|
157
159
|
entry.sockets.clear();
|
|
158
160
|
entry.sockets.add(ws);
|
|
159
161
|
if (entry.history) {
|
|
160
|
-
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history) })); } catch {}
|
|
161
|
-
}
|
|
162
|
-
if (entry.exitedAt) {
|
|
163
|
-
try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
|
|
164
|
-
} else {
|
|
165
|
-
try { ws.send(JSON.stringify({ type: 'attached', meta: entry.meta })); } catch {}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
ws.on('message', (msg) => {
|
|
169
|
-
let event;
|
|
170
|
-
try { event = JSON.parse(msg.toString()); } catch { return; }
|
|
171
|
-
if (entry.exitedAt) return; // PTY is dead, ignore further input
|
|
172
|
-
switch (event.type) {
|
|
173
|
-
case 'input':
|
|
174
|
-
if (typeof event.data === 'string') {
|
|
175
|
-
if (process.env.CCSM_DEBUG_PASTE === '1') {
|
|
176
|
-
const d = event.data;
|
|
177
|
-
const hex = Buffer.from(d, 'utf8').toString('hex').match(/.{1,2}/g).join(' ');
|
|
178
|
-
console.log(`[pty.write id=${id}] len=${d.length} hex=${hex.slice(0, 400)}${hex.length > 400 ? '...' : ''}`);
|
|
179
|
-
}
|
|
180
|
-
entry.pty.write(event.data);
|
|
181
|
-
}
|
|
182
|
-
break;
|
|
183
|
-
case 'resize':
|
|
184
|
-
if (Number(event.cols) > 0 && Number(event.rows) > 0) {
|
|
185
|
-
try { entry.pty.resize(Number(event.cols), Number(event.rows)); } catch {}
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
188
|
-
case 'kill':
|
|
189
|
-
kill(id);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
ws.on('close', () => {
|
|
195
|
-
entry.sockets.delete(ws);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function kill(id) {
|
|
200
|
-
const entry = sessions.get(id);
|
|
201
|
-
if (!entry || entry.exitedAt) return false;
|
|
202
|
-
try { entry.pty.kill(); } catch {}
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Public summary for the frontend. Don't leak the pty / sockets objects.
|
|
207
|
-
function describe(entry) {
|
|
208
|
-
return {
|
|
209
|
-
id: entry.id,
|
|
210
|
-
meta: entry.meta,
|
|
211
|
-
attached: entry.sockets.size,
|
|
212
|
-
exitedAt: entry.exitedAt,
|
|
213
|
-
exitCode: entry.exitCode,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function list() {
|
|
218
|
-
return Array.from(sessions.values()).map(describe);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function get(id) {
|
|
222
|
-
const e = sessions.get(id);
|
|
223
|
-
return e ? describe(e) : null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function killAll() {
|
|
227
|
-
for (const e of sessions.values()) {
|
|
228
|
-
try { e.pty.kill(); } catch {}
|
|
162
|
+
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history), replay: true })); } catch {}
|
|
229
163
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
};
|