@sma1lboy/rove 0.8.89
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/README.md +60 -0
- package/dist/cli/index.js +959 -0
- package/dist/cli/kobe.js +959 -0
- package/dist/cli/pty-host-node.mjs +1015 -0
- package/dist/cli/rove.js +959 -0
- package/dist/pulse-n3cq1btw.wav +0 -0
- package/dist/skills/kobe/SKILL.md +317 -0
- package/dist/web-ui/assets/AppShell-BTjvuY-t.js +5 -0
- package/dist/web-ui/assets/AppShell-HiyATJse.css +1 -0
- package/dist/web-ui/assets/Board-DPB4h6mJ.js +1 -0
- package/dist/web-ui/assets/BoardPeek-69VJptGV.js +2 -0
- package/dist/web-ui/assets/ChatTerminal-BrP-ENHg.css +1 -0
- package/dist/web-ui/assets/ChatTerminal-MCI1aBbT.js +144 -0
- package/dist/web-ui/assets/ChatTranscript-DLthbnmB.js +1 -0
- package/dist/web-ui/assets/ConfirmDialog-CDPOtXww.js +1 -0
- package/dist/web-ui/assets/IssueIntakePanel-XYsIcIhp.js +1 -0
- package/dist/web-ui/assets/IssuePeek-CP_rAkqO.js +1 -0
- package/dist/web-ui/assets/IssuesPage-BR5mpuo-.js +1 -0
- package/dist/web-ui/assets/RichEditor-B4sEOPyN.css +1 -0
- package/dist/web-ui/assets/RichEditor-DJFl5TvH.js +173 -0
- package/dist/web-ui/assets/SettingsPage-CLRhr-sg.js +2 -0
- package/dist/web-ui/assets/SlideOver-Dxtk6Gn2.js +1 -0
- package/dist/web-ui/assets/ViewToggle-BdPCMBWa.js +1 -0
- package/dist/web-ui/assets/WorkspaceTabs-CMKlvN_Y.js +3 -0
- package/dist/web-ui/assets/WorkspaceTabs-DLzL-Tad.css +1 -0
- package/dist/web-ui/assets/active-task-GeQZqbJK.js +1 -0
- package/dist/web-ui/assets/activity-BjTd1D8q.js +1 -0
- package/dist/web-ui/assets/arrow-left-DNM2CHhS.js +1 -0
- package/dist/web-ui/assets/board-CGiK0u0M.js +1 -0
- package/dist/web-ui/assets/chips-Da37C4KZ.js +1 -0
- package/dist/web-ui/assets/columns-3-CQGAnX-4.js +1 -0
- package/dist/web-ui/assets/createLucideIcon-CM5xLVj1.js +1 -0
- package/dist/web-ui/assets/desktop-BThq9eSR.js +2 -0
- package/dist/web-ui/assets/engines-Cnxy1hW9.js +1 -0
- package/dist/web-ui/assets/external-link-BwT2u1hD.js +1 -0
- package/dist/web-ui/assets/external-store-DJlWpyYC.js +1 -0
- package/dist/web-ui/assets/harness-q9gT7Jic.js +1 -0
- package/dist/web-ui/assets/index-CgL6oDIY.js +10 -0
- package/dist/web-ui/assets/index-MuA3NLRu.css +1 -0
- package/dist/web-ui/assets/issues-1SCRkcB0.js +4 -0
- package/dist/web-ui/assets/issues-BKkua1D9.js +1 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/web-ui/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist/web-ui/assets/lazy-issue-panels-0Rhl2uhh.js +6 -0
- package/dist/web-ui/assets/plus-BbX5qXba.js +1 -0
- package/dist/web-ui/assets/react-dom-DCnDkJvp.js +1 -0
- package/dist/web-ui/assets/rotate-cw-D-UUtiD0.js +1 -0
- package/dist/web-ui/assets/routes-SBb-uSy8.js +1 -0
- package/dist/web-ui/assets/search-BxbFZoup.js +1 -0
- package/dist/web-ui/assets/settings-tPcmEHCP.js +1 -0
- package/dist/web-ui/assets/store-uT65Mask.js +2 -0
- package/dist/web-ui/assets/tabs-Dx-TnbvG.js +1 -0
- package/dist/web-ui/assets/task._taskId-BlZ0vG_1.js +2 -0
- package/dist/web-ui/assets/task._taskId-BmObFoVZ.js +1 -0
- package/dist/web-ui/assets/terminal-qBEkzc-m.js +1 -0
- package/dist/web-ui/assets/text-match-DW3sD-Rt.js +1 -0
- package/dist/web-ui/assets/time-kviT26Tm.js +1 -0
- package/dist/web-ui/assets/toast-BXjAT0ix.js +1 -0
- package/dist/web-ui/assets/triage-7QLsCY8d.js +1 -0
- package/dist/web-ui/assets/use-focus-trap-DNMrlNR0.js +1 -0
- package/dist/web-ui/assets/vendor-CdtpTJrf.js +1 -0
- package/dist/web-ui/assets/web-transport-D0bugxxv.js +1 -0
- package/dist/web-ui/assets/with-selector-B5R3h-Ow.js +1 -0
- package/dist/web-ui/assets/worktrees-C_XKA7jM.js +1 -0
- package/dist/web-ui/assets/x-7cB9X7Hi.js +1 -0
- package/dist/web-ui/favicon.ico +0 -0
- package/dist/web-ui/index.html +35 -0
- package/dist/web-ui/logo192.png +0 -0
- package/dist/web-ui/logo512.png +0 -0
- package/dist/web-ui/manifest.json +25 -0
- package/dist/web-ui/origin-policy.mjs +41 -0
- package/dist/web-ui/pty-scrollback.mjs +51 -0
- package/dist/web-ui/pty-server.mjs +212 -0
- package/dist/web-ui/pty-session-lifecycle.mjs +282 -0
- package/dist/web-ui/robots.txt +3 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// ../kobe-daemon/src/daemon/pty-driver.ts
|
|
5
|
+
var TERMINAL_NAME = "xterm-256color";
|
|
6
|
+
function bunTerminalDriver(spawn) {
|
|
7
|
+
const spawnTerminal = spawn ?? ((argv, options) => Bun.spawn(argv, options));
|
|
8
|
+
return (request) => {
|
|
9
|
+
const proc = spawnTerminal([...request.argv], {
|
|
10
|
+
cwd: request.cwd,
|
|
11
|
+
env: request.env,
|
|
12
|
+
terminal: {
|
|
13
|
+
cols: request.cols,
|
|
14
|
+
rows: request.rows,
|
|
15
|
+
name: TERMINAL_NAME,
|
|
16
|
+
data: (_terminal, data) => request.onData(data)
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
pid: proc.pid,
|
|
21
|
+
exited: proc.exited.then(() => ({ code: proc.exitCode ?? null, signal: proc.signalCode ?? null }), () => ({ code: null, signal: null })),
|
|
22
|
+
write: (data) => proc.terminal?.write(data),
|
|
23
|
+
resize: (cols, rows) => proc.terminal?.resize(cols, rows),
|
|
24
|
+
close: () => proc.terminal?.close(),
|
|
25
|
+
kill: (signal) => proc.kill(signal)
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function nodePtyDriver(spawn) {
|
|
30
|
+
const spawnPty = spawn ?? (await import("node-pty")).spawn;
|
|
31
|
+
return (request) => {
|
|
32
|
+
const [file, ...args] = request.argv;
|
|
33
|
+
const child = spawnPty(file ?? "", args, {
|
|
34
|
+
name: TERMINAL_NAME,
|
|
35
|
+
cols: request.cols,
|
|
36
|
+
rows: request.rows,
|
|
37
|
+
cwd: request.cwd,
|
|
38
|
+
env: Object.fromEntries(Object.entries(request.env).filter((entry) => entry[1] !== undefined))
|
|
39
|
+
});
|
|
40
|
+
let settle = () => {};
|
|
41
|
+
const exited = new Promise((resolve) => {
|
|
42
|
+
settle = resolve;
|
|
43
|
+
});
|
|
44
|
+
child.onData((data) => request.onData(data));
|
|
45
|
+
child.onExit(({ exitCode }) => settle({ code: exitCode, signal: null }));
|
|
46
|
+
return {
|
|
47
|
+
pid: child.pid,
|
|
48
|
+
exited,
|
|
49
|
+
write: (data) => child.write(data),
|
|
50
|
+
resize: (cols, rows) => child.resize(cols, rows),
|
|
51
|
+
close: () => {},
|
|
52
|
+
kill: () => child.kill()
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../kobe-daemon/src/daemon/pty-server.ts
|
|
58
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
59
|
+
import { createServer } from "node:net";
|
|
60
|
+
import { dirname as dirname2 } from "node:path";
|
|
61
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
62
|
+
|
|
63
|
+
// ../kobe-daemon/src/daemon/client-writer.ts
|
|
64
|
+
var DEFAULT_WRITE_HIGH_WATER_MARK = 8 * 1024 * 1024;
|
|
65
|
+
|
|
66
|
+
class ClientWriter {
|
|
67
|
+
socket;
|
|
68
|
+
highWaterMark;
|
|
69
|
+
onOverflow;
|
|
70
|
+
queue = [];
|
|
71
|
+
queuedBytes = 0;
|
|
72
|
+
paused = false;
|
|
73
|
+
draining = false;
|
|
74
|
+
droppedFrames = 0;
|
|
75
|
+
overflowed = false;
|
|
76
|
+
constructor(socket, options = {}) {
|
|
77
|
+
this.socket = socket;
|
|
78
|
+
this.highWaterMark = options.highWaterMark ?? DEFAULT_WRITE_HIGH_WATER_MARK;
|
|
79
|
+
this.onOverflow = options.onOverflow;
|
|
80
|
+
}
|
|
81
|
+
get isPaused() {
|
|
82
|
+
return this.paused;
|
|
83
|
+
}
|
|
84
|
+
get pendingBytes() {
|
|
85
|
+
return this.queuedBytes;
|
|
86
|
+
}
|
|
87
|
+
get pendingCount() {
|
|
88
|
+
return this.queue.length;
|
|
89
|
+
}
|
|
90
|
+
get dropped() {
|
|
91
|
+
return this.droppedFrames;
|
|
92
|
+
}
|
|
93
|
+
write(line, critical) {
|
|
94
|
+
if (this.overflowed)
|
|
95
|
+
return;
|
|
96
|
+
if (!this.paused) {
|
|
97
|
+
if (this.safeWrite(line))
|
|
98
|
+
return;
|
|
99
|
+
this.pause();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.enqueue({ line, bytes: Buffer.byteLength(line), critical });
|
|
103
|
+
}
|
|
104
|
+
enqueue(frame) {
|
|
105
|
+
this.queue.push(frame);
|
|
106
|
+
this.queuedBytes += frame.bytes;
|
|
107
|
+
if (this.queuedBytes <= this.highWaterMark)
|
|
108
|
+
return;
|
|
109
|
+
const survivors = [];
|
|
110
|
+
for (const queued of this.queue) {
|
|
111
|
+
if (this.queuedBytes > this.highWaterMark && !queued.critical) {
|
|
112
|
+
this.queuedBytes -= queued.bytes;
|
|
113
|
+
this.droppedFrames++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
survivors.push(queued);
|
|
117
|
+
}
|
|
118
|
+
this.queue = survivors;
|
|
119
|
+
if (this.queuedBytes <= this.highWaterMark || !this.onOverflow)
|
|
120
|
+
return;
|
|
121
|
+
this.overflowed = true;
|
|
122
|
+
this.queue = [];
|
|
123
|
+
this.queuedBytes = 0;
|
|
124
|
+
this.onOverflow();
|
|
125
|
+
}
|
|
126
|
+
pause() {
|
|
127
|
+
if (this.draining)
|
|
128
|
+
return;
|
|
129
|
+
this.paused = true;
|
|
130
|
+
this.draining = true;
|
|
131
|
+
this.socket.once("drain", () => {
|
|
132
|
+
this.draining = false;
|
|
133
|
+
this.paused = false;
|
|
134
|
+
this.flush();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
flush() {
|
|
138
|
+
while (this.queue.length > 0) {
|
|
139
|
+
const frame = this.queue[0];
|
|
140
|
+
const ok = this.safeWrite(frame.line);
|
|
141
|
+
this.queue.shift();
|
|
142
|
+
this.queuedBytes -= frame.bytes;
|
|
143
|
+
if (!ok) {
|
|
144
|
+
this.pause();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
safeWrite(line) {
|
|
150
|
+
try {
|
|
151
|
+
return this.socket.write(line);
|
|
152
|
+
} catch {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ../kobe-daemon/src/daemon/crash-log.ts
|
|
159
|
+
function toError(err) {
|
|
160
|
+
if (err instanceof Error)
|
|
161
|
+
return err;
|
|
162
|
+
let text;
|
|
163
|
+
try {
|
|
164
|
+
text = typeof err === "string" ? err : JSON.stringify(err);
|
|
165
|
+
} catch {
|
|
166
|
+
text = String(err);
|
|
167
|
+
}
|
|
168
|
+
return new Error(text);
|
|
169
|
+
}
|
|
170
|
+
function formatDaemonError(subsystem, err, now = new Date) {
|
|
171
|
+
const e = toError(err);
|
|
172
|
+
return `[${now.toISOString()}] daemon error [${subsystem}]: ${e.stack ?? `${e.name}: ${e.message}`}
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
function logDaemonError(subsystem, err) {
|
|
176
|
+
process.stderr.write(formatDaemonError(subsystem, err));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ../kobe-daemon/src/daemon/handler-validators.ts
|
|
180
|
+
function objectPayload(payload) {
|
|
181
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
182
|
+
return {};
|
|
183
|
+
return payload;
|
|
184
|
+
}
|
|
185
|
+
function requireString(payload, key) {
|
|
186
|
+
const value = payload[key];
|
|
187
|
+
if (typeof value !== "string" || value.length === 0)
|
|
188
|
+
throw new Error(`${key} is required`);
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ../kobe-daemon/src/daemon/paths.ts
|
|
193
|
+
import { createHash } from "node:crypto";
|
|
194
|
+
import { homedir, tmpdir } from "node:os";
|
|
195
|
+
import { join } from "node:path";
|
|
196
|
+
|
|
197
|
+
// ../kobe-daemon/src/compat-env.ts
|
|
198
|
+
var ROVE_ENV_PREFIX = "ROVE_";
|
|
199
|
+
var LEGACY_KOBE_ENV_PREFIX = "KOBE_";
|
|
200
|
+
var COMPAT_STATE_DIR_BASENAME = ".kobe";
|
|
201
|
+
function readRoveEnv(suffix, env = process.env) {
|
|
202
|
+
return env[`${ROVE_ENV_PREFIX}${suffix}`] ?? env[`${LEGACY_KOBE_ENV_PREFIX}${suffix}`];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ../kobe-daemon/src/daemon/paths.ts
|
|
206
|
+
var SOCKET_PATH_SAFETY_LIMIT = 100;
|
|
207
|
+
function shortHomeTag(homeDir) {
|
|
208
|
+
return createHash("sha1").update(homeDir).digest("hex").slice(0, 8);
|
|
209
|
+
}
|
|
210
|
+
function fitSocketPath(naturalPath, homeDir, role, pidTag) {
|
|
211
|
+
if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
|
|
212
|
+
return naturalPath;
|
|
213
|
+
const tag = shortHomeTag(homeDir);
|
|
214
|
+
const suffix = pidTag === undefined ? "" : `-${pidTag}`;
|
|
215
|
+
const fallback = join(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
|
|
216
|
+
if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
|
|
217
|
+
return fallback;
|
|
218
|
+
throw new Error(`daemon socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
|
|
219
|
+
}
|
|
220
|
+
function isWindowsPipePath(path) {
|
|
221
|
+
return path.startsWith("\\\\.\\pipe\\") || path.startsWith("//./pipe/");
|
|
222
|
+
}
|
|
223
|
+
function windowsPipePath(homeDir, role) {
|
|
224
|
+
return `\\\\.\\pipe\\kobe-${shortHomeTag(homeDir)}-${role}`;
|
|
225
|
+
}
|
|
226
|
+
function defaultPtyHostSocketPath(homeDir, platform = process.platform) {
|
|
227
|
+
const override = readRoveEnv("PTY_SOCKET_PATH");
|
|
228
|
+
if (override && override.length > 0)
|
|
229
|
+
return override;
|
|
230
|
+
const explicit = homeDir ?? readRoveEnv("HOME_DIR");
|
|
231
|
+
if (platform === "win32")
|
|
232
|
+
return windowsPipePath(explicit || homedir(), "pty");
|
|
233
|
+
if (explicit && explicit.length > 0) {
|
|
234
|
+
return fitSocketPath(join(explicit, COMPAT_STATE_DIR_BASENAME, "pty.sock"), explicit, "pty");
|
|
235
|
+
}
|
|
236
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
237
|
+
if (runtimeDir && runtimeDir.length > 0) {
|
|
238
|
+
return fitSocketPath(join(runtimeDir, "kobe-pty.sock"), runtimeDir, "pty");
|
|
239
|
+
}
|
|
240
|
+
const home = homedir();
|
|
241
|
+
return fitSocketPath(join(home, COMPAT_STATE_DIR_BASENAME, "pty.sock"), home, "pty");
|
|
242
|
+
}
|
|
243
|
+
function defaultPtyHostPidPath(homeDir = readRoveEnv("HOME_DIR") ?? homedir()) {
|
|
244
|
+
const override = readRoveEnv("PTY_PID_PATH");
|
|
245
|
+
if (override && override.length > 0)
|
|
246
|
+
return override;
|
|
247
|
+
return join(homeDir, COMPAT_STATE_DIR_BASENAME, "pty.pid");
|
|
248
|
+
}
|
|
249
|
+
function defaultPtyExitsPath(homeDir = readRoveEnv("HOME_DIR") ?? homedir()) {
|
|
250
|
+
return join(homeDir, COMPAT_STATE_DIR_BASENAME, "pty-exits.json");
|
|
251
|
+
}
|
|
252
|
+
// ../kobe-plugin-sdk/src/contract.ts
|
|
253
|
+
var DAEMON_CHANNELS = [
|
|
254
|
+
"task.snapshot",
|
|
255
|
+
"issue.snapshot",
|
|
256
|
+
"active-task",
|
|
257
|
+
"update",
|
|
258
|
+
"engine-state",
|
|
259
|
+
"attention.inbox",
|
|
260
|
+
"ui-prefs",
|
|
261
|
+
"keybindings",
|
|
262
|
+
"task.jobs",
|
|
263
|
+
"worktree.changes",
|
|
264
|
+
"transcript.activity",
|
|
265
|
+
"session.deliver",
|
|
266
|
+
"tab.open",
|
|
267
|
+
"tab.close",
|
|
268
|
+
"engine.lifecycle",
|
|
269
|
+
"notice.event",
|
|
270
|
+
"usage.snapshot",
|
|
271
|
+
"ui.prompt"
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// ../kobe-daemon/src/daemon/channels.ts
|
|
275
|
+
var CHANNEL_NAMES = DAEMON_CHANNELS;
|
|
276
|
+
var CHANNEL_NAME_SET = new Set(CHANNEL_NAMES);
|
|
277
|
+
|
|
278
|
+
// ../kobe-daemon/src/daemon/protocol.ts
|
|
279
|
+
var DAEMON_PROTOCOL_VERSION = 4;
|
|
280
|
+
function frameToLine(frame) {
|
|
281
|
+
return `${JSON.stringify(frame)}
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ../kobe-daemon/src/daemon/pty-exit-store.ts
|
|
286
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
287
|
+
import { dirname } from "node:path";
|
|
288
|
+
var MAX_RECORDS = 50;
|
|
289
|
+
var TAIL_LINES = 40;
|
|
290
|
+
var TAIL_LINE_CHARS = 500;
|
|
291
|
+
var ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]/g;
|
|
292
|
+
function plainTail(raw) {
|
|
293
|
+
const plain = raw.replace(ANSI_RE, "").replace(/\r\n/g, `
|
|
294
|
+
`);
|
|
295
|
+
const lines = plain.split(`
|
|
296
|
+
`).map((line) => (line.split("\r").pop() ?? "").slice(0, TAIL_LINE_CHARS));
|
|
297
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "")
|
|
298
|
+
lines.pop();
|
|
299
|
+
return lines.slice(-TAIL_LINES);
|
|
300
|
+
}
|
|
301
|
+
function readPtyExitRecords(path = defaultPtyExitsPath()) {
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
304
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
305
|
+
return {};
|
|
306
|
+
return parsed;
|
|
307
|
+
} catch {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function recordPtyExit(info, path = defaultPtyExitsPath()) {
|
|
312
|
+
if (info.key.startsWith("::"))
|
|
313
|
+
return;
|
|
314
|
+
if (info.exit.code === 0 && info.exit.signal === null)
|
|
315
|
+
return;
|
|
316
|
+
const records = readPtyExitRecords(path);
|
|
317
|
+
records[info.key] = {
|
|
318
|
+
key: info.key,
|
|
319
|
+
pid: info.pid,
|
|
320
|
+
code: info.exit.code,
|
|
321
|
+
signal: info.exit.signal,
|
|
322
|
+
at: info.exit.at,
|
|
323
|
+
tail: plainTail(info.tail)
|
|
324
|
+
};
|
|
325
|
+
const newest = Object.values(records).sort((a, b) => a.at < b.at ? 1 : -1).slice(0, MAX_RECORDS);
|
|
326
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
327
|
+
writeFileSync(path, JSON.stringify(Object.fromEntries(newest.map((r) => [r.key, r])), null, 2), "utf8");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ../kobe-daemon/src/daemon/pty-host.ts
|
|
331
|
+
import { StringDecoder } from "node:string_decoder";
|
|
332
|
+
|
|
333
|
+
// ../kobe-daemon/src/daemon/platform-shell.js
|
|
334
|
+
import { existsSync } from "node:fs";
|
|
335
|
+
function windowsBashCandidates(env) {
|
|
336
|
+
const roots = [env.ProgramFiles, env["ProgramFiles(x86)"], env.LOCALAPPDATA && `${env.LOCALAPPDATA}\\Programs`];
|
|
337
|
+
return roots.filter(Boolean).map((root) => `${root}\\Git\\bin\\bash.exe`);
|
|
338
|
+
}
|
|
339
|
+
var WINDOWS_BASH_FALLBACK = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
340
|
+
function isWindowsSpawnable(shellPath) {
|
|
341
|
+
return /^[A-Za-z]:[\\/]/.test(shellPath) || shellPath.startsWith("\\\\");
|
|
342
|
+
}
|
|
343
|
+
var cache = new Map;
|
|
344
|
+
function resolveLoginShell(options = {}) {
|
|
345
|
+
const fallback = options.fallback ?? "/bin/bash";
|
|
346
|
+
const platform = options.platform ?? process.platform;
|
|
347
|
+
const env = options.env ?? process.env;
|
|
348
|
+
const exists = options.exists ?? existsSync;
|
|
349
|
+
const cacheable = platform === process.platform && env === process.env && options.exists === undefined;
|
|
350
|
+
if (cacheable) {
|
|
351
|
+
const hit = cache.get(fallback);
|
|
352
|
+
if (hit)
|
|
353
|
+
return hit;
|
|
354
|
+
}
|
|
355
|
+
const resolved = resolveUncached(fallback, platform, env, exists);
|
|
356
|
+
if (cacheable)
|
|
357
|
+
cache.set(fallback, resolved);
|
|
358
|
+
return resolved;
|
|
359
|
+
}
|
|
360
|
+
function resolveUncached(fallback, platform, env, exists) {
|
|
361
|
+
const explicit = env.SHELL?.trim();
|
|
362
|
+
if (platform !== "win32")
|
|
363
|
+
return explicit || fallback;
|
|
364
|
+
if (explicit && isWindowsSpawnable(explicit) && exists(explicit))
|
|
365
|
+
return explicit;
|
|
366
|
+
for (const candidate of windowsBashCandidates(env)) {
|
|
367
|
+
if (exists(candidate))
|
|
368
|
+
return candidate;
|
|
369
|
+
}
|
|
370
|
+
return WINDOWS_BASH_FALLBACK;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ../kobe-daemon/src/daemon/pty-env.js
|
|
374
|
+
var IDENTITY_VARS = new Set([
|
|
375
|
+
"TERM_PROGRAM",
|
|
376
|
+
"TERM_PROGRAM_VERSION",
|
|
377
|
+
"TERM_SESSION_ID",
|
|
378
|
+
"TERM_FEATURES",
|
|
379
|
+
"LC_TERMINAL",
|
|
380
|
+
"LC_TERMINAL_VERSION",
|
|
381
|
+
"__CFBundleIdentifier",
|
|
382
|
+
"VTE_VERSION",
|
|
383
|
+
"WT_SESSION",
|
|
384
|
+
"WT_PROFILE_ID",
|
|
385
|
+
"TMUX",
|
|
386
|
+
"TMUX_PANE",
|
|
387
|
+
"ZELLIJ",
|
|
388
|
+
"STY",
|
|
389
|
+
"WINDOW"
|
|
390
|
+
]);
|
|
391
|
+
var IDENTITY_PREFIXES = ["ITERM_", "KITTY_", "GHOSTTY_", "WEZTERM_", "ALACRITTY_", "KONSOLE_", "ZELLIJ_"];
|
|
392
|
+
function embeddedTerminalEnv(base, overrides = {}) {
|
|
393
|
+
const env = {};
|
|
394
|
+
for (const [key, value] of Object.entries(base)) {
|
|
395
|
+
if (IDENTITY_VARS.has(key))
|
|
396
|
+
continue;
|
|
397
|
+
if (IDENTITY_PREFIXES.some((prefix) => key.startsWith(prefix)))
|
|
398
|
+
continue;
|
|
399
|
+
env[key] = value;
|
|
400
|
+
}
|
|
401
|
+
return { ...env, ...overrides };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ../kobe-daemon/src/daemon/pty-observability.ts
|
|
405
|
+
function describeExit(exit) {
|
|
406
|
+
if (exit?.signal)
|
|
407
|
+
return ` (signal ${exit.signal})`;
|
|
408
|
+
if (exit && exit.code !== null)
|
|
409
|
+
return ` (code ${exit.code})`;
|
|
410
|
+
return " (cause unknown)";
|
|
411
|
+
}
|
|
412
|
+
function ringTail(chunks, bytes, maxBytes) {
|
|
413
|
+
const skip = Math.max(0, bytes - maxBytes);
|
|
414
|
+
let seen = 0;
|
|
415
|
+
const parts = [];
|
|
416
|
+
for (const chunk of chunks) {
|
|
417
|
+
if (seen + chunk.byteLength > skip)
|
|
418
|
+
parts.push(seen >= skip ? chunk : chunk.subarray(skip - seen));
|
|
419
|
+
seen += chunk.byteLength;
|
|
420
|
+
}
|
|
421
|
+
return Buffer.concat(parts).toString("utf8");
|
|
422
|
+
}
|
|
423
|
+
var OSC_TITLE_RE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
|
|
424
|
+
var TITLE_CARRY_CAP = 1024;
|
|
425
|
+
function titleCarryFrom(rest) {
|
|
426
|
+
const osc = rest.lastIndexOf("\x1B]");
|
|
427
|
+
if (osc !== -1)
|
|
428
|
+
return rest.slice(osc, osc + TITLE_CARRY_CAP);
|
|
429
|
+
return rest.endsWith("\x1B") ? "\x1B" : "";
|
|
430
|
+
}
|
|
431
|
+
function foldOscTitle(prevCarry, chunkText) {
|
|
432
|
+
const text = prevCarry + chunkText;
|
|
433
|
+
let title = null;
|
|
434
|
+
let end = 0;
|
|
435
|
+
OSC_TITLE_RE.lastIndex = 0;
|
|
436
|
+
for (let match = OSC_TITLE_RE.exec(text);match; match = OSC_TITLE_RE.exec(text)) {
|
|
437
|
+
title = match[1] ?? "";
|
|
438
|
+
end = match.index + match[0].length;
|
|
439
|
+
}
|
|
440
|
+
return { title, carry: titleCarryFrom(text.slice(end)) };
|
|
441
|
+
}
|
|
442
|
+
function scanOscTitle(session, buf) {
|
|
443
|
+
const { title, carry } = foldOscTitle(session.titleCarry, session.titleDecoder.write(buf));
|
|
444
|
+
if (title !== null)
|
|
445
|
+
session.title = title;
|
|
446
|
+
session.titleCarry = carry;
|
|
447
|
+
}
|
|
448
|
+
function peekRing(session, sinceOffset) {
|
|
449
|
+
if (!session) {
|
|
450
|
+
return { exists: false, alive: false, pid: null, offset: 0, data: "", sinceValid: false, exit: null };
|
|
451
|
+
}
|
|
452
|
+
const windowStart = session.totalBytes - session.bytes;
|
|
453
|
+
let buf = Buffer.concat(session.chunks);
|
|
454
|
+
let sinceValid = false;
|
|
455
|
+
if (sinceOffset !== undefined && sinceOffset >= windowStart && sinceOffset <= session.totalBytes) {
|
|
456
|
+
buf = buf.subarray(sinceOffset - windowStart);
|
|
457
|
+
sinceValid = true;
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
exists: true,
|
|
461
|
+
alive: session.alive,
|
|
462
|
+
pid: session.proc?.pid ?? null,
|
|
463
|
+
offset: session.totalBytes,
|
|
464
|
+
data: buf.toString("base64"),
|
|
465
|
+
sinceValid,
|
|
466
|
+
exit: session.exit ?? null
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ../kobe-daemon/src/daemon/pty-termination.ts
|
|
471
|
+
async function settledWithin(exited, ms) {
|
|
472
|
+
let timer;
|
|
473
|
+
const settled = await Promise.race([
|
|
474
|
+
exited.then(() => true, () => true),
|
|
475
|
+
new Promise((resolve) => {
|
|
476
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
477
|
+
})
|
|
478
|
+
]);
|
|
479
|
+
if (timer)
|
|
480
|
+
clearTimeout(timer);
|
|
481
|
+
return settled;
|
|
482
|
+
}
|
|
483
|
+
function signalProcessGroup(pid, signal, fallback, platform = process.platform) {
|
|
484
|
+
if (platform !== "win32" && pid > 1) {
|
|
485
|
+
try {
|
|
486
|
+
process.kill(-pid, signal);
|
|
487
|
+
return;
|
|
488
|
+
} catch {}
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
fallback();
|
|
492
|
+
} catch {}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ../kobe-daemon/src/daemon/pty-host.ts
|
|
496
|
+
var DEFAULT_SCROLLBACK_CAP = 512 * 1024;
|
|
497
|
+
var TERMINATION_GRACE_MS = 500;
|
|
498
|
+
var EXIT_TAIL_BYTES = 16 * 1024;
|
|
499
|
+
|
|
500
|
+
class PtyHost {
|
|
501
|
+
sessions = new Map;
|
|
502
|
+
opts;
|
|
503
|
+
parkRestoreDeltas = 0;
|
|
504
|
+
parkRestoreFallbacks = 0;
|
|
505
|
+
spare = null;
|
|
506
|
+
constructor(opts = {}) {
|
|
507
|
+
this.opts = opts;
|
|
508
|
+
}
|
|
509
|
+
open(key, spec, token, sink, sinceOffset, sincePid) {
|
|
510
|
+
let session = this.sessions.get(key);
|
|
511
|
+
let created = false;
|
|
512
|
+
if (!session) {
|
|
513
|
+
created = true;
|
|
514
|
+
session = this.adoptSpare(key, spec) ?? this.spawn(key, spec);
|
|
515
|
+
this.sessions.set(key, session);
|
|
516
|
+
} else if (session.alive && spec.cols !== undefined && spec.rows !== undefined && (session.cols !== spec.cols || session.rows !== spec.rows)) {
|
|
517
|
+
this.resize(key, spec.cols, spec.rows);
|
|
518
|
+
}
|
|
519
|
+
session.sinks.set(token, sink);
|
|
520
|
+
session.parked = false;
|
|
521
|
+
session.parkedScreenBytes = 0;
|
|
522
|
+
const windowStart = session.totalBytes - session.bytes;
|
|
523
|
+
let replay = Buffer.concat(session.chunks);
|
|
524
|
+
let sinceValid = false;
|
|
525
|
+
if (!created && sinceOffset !== undefined && sinceOffset >= windowStart && sinceOffset <= session.totalBytes && sincePid !== undefined && sincePid === session.proc?.pid) {
|
|
526
|
+
replay = replay.subarray(sinceOffset - windowStart);
|
|
527
|
+
sinceValid = true;
|
|
528
|
+
}
|
|
529
|
+
if (sinceOffset !== undefined && sincePid !== undefined) {
|
|
530
|
+
if (sinceValid)
|
|
531
|
+
this.parkRestoreDeltas++;
|
|
532
|
+
else
|
|
533
|
+
this.parkRestoreFallbacks++;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
replay: replay.toString("base64"),
|
|
537
|
+
alive: session.alive,
|
|
538
|
+
pid: session.proc?.pid ?? null,
|
|
539
|
+
created,
|
|
540
|
+
offset: session.totalBytes,
|
|
541
|
+
sinceValid
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
warm(cwd, shell, cols = 80, rows = 24) {
|
|
545
|
+
const argv0 = shell ?? resolveLoginShell();
|
|
546
|
+
if (this.spare?.alive && this.spare.cwd === cwd && this.spare.command[0] === argv0)
|
|
547
|
+
return;
|
|
548
|
+
const old = this.spare;
|
|
549
|
+
this.spare = null;
|
|
550
|
+
if (old)
|
|
551
|
+
this.endChild(old);
|
|
552
|
+
const session = this.spawn("::spare", { cwd, command: [argv0], cols, rows }, true);
|
|
553
|
+
this.spare = session.alive ? session : null;
|
|
554
|
+
}
|
|
555
|
+
adoptSpare(key, spec) {
|
|
556
|
+
const spare = this.spare;
|
|
557
|
+
if (!spare?.alive || spare.cwd !== spec.cwd)
|
|
558
|
+
return null;
|
|
559
|
+
const want = spec.command && spec.command.length > 0 ? spec.command : [spec.shell ?? resolveLoginShell()];
|
|
560
|
+
if (want.length !== 1 || want[0] !== spare.command[0])
|
|
561
|
+
return null;
|
|
562
|
+
this.spare = null;
|
|
563
|
+
spare.key = key;
|
|
564
|
+
const cols = spec.cols ?? spare.cols;
|
|
565
|
+
const rows = spec.rows ?? spare.rows;
|
|
566
|
+
if (spare.cols !== cols || spare.rows !== rows) {
|
|
567
|
+
spare.cols = cols;
|
|
568
|
+
spare.rows = rows;
|
|
569
|
+
try {
|
|
570
|
+
spare.proc?.resize(cols, rows);
|
|
571
|
+
} catch {
|
|
572
|
+
this.markExited(spare);
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
this.opts.log?.("pty", `adopted warm shell for ${key} (pid ${spare.proc?.pid})`);
|
|
577
|
+
this.opts.onSessionStart?.();
|
|
578
|
+
this.warm(spec.cwd, spare.command[0], cols, rows);
|
|
579
|
+
return spare;
|
|
580
|
+
}
|
|
581
|
+
write(key, data) {
|
|
582
|
+
const session = this.sessions.get(key);
|
|
583
|
+
if (!session?.alive || data.length === 0)
|
|
584
|
+
return;
|
|
585
|
+
try {
|
|
586
|
+
session.proc?.write(data);
|
|
587
|
+
} catch {}
|
|
588
|
+
}
|
|
589
|
+
resize(key, cols, rows) {
|
|
590
|
+
const session = this.sessions.get(key);
|
|
591
|
+
if (!session?.alive)
|
|
592
|
+
return;
|
|
593
|
+
session.cols = cols;
|
|
594
|
+
session.rows = rows;
|
|
595
|
+
try {
|
|
596
|
+
session.proc?.resize(cols, rows);
|
|
597
|
+
} catch {}
|
|
598
|
+
}
|
|
599
|
+
kill(key) {
|
|
600
|
+
const session = this.sessions.get(key);
|
|
601
|
+
if (!session)
|
|
602
|
+
return Promise.resolve();
|
|
603
|
+
this.sessions.delete(key);
|
|
604
|
+
return this.endChild(session);
|
|
605
|
+
}
|
|
606
|
+
detach(key, token, parked = false, parkedScreenBytes = 0) {
|
|
607
|
+
const session = this.sessions.get(key);
|
|
608
|
+
if (!session)
|
|
609
|
+
return;
|
|
610
|
+
session.sinks.delete(token);
|
|
611
|
+
if (session.sinks.size === 0) {
|
|
612
|
+
session.parked = parked;
|
|
613
|
+
session.parkedScreenBytes = parked ? Math.max(0, parkedScreenBytes) : 0;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
detachClient(token) {
|
|
617
|
+
for (const session of this.sessions.values()) {
|
|
618
|
+
session.sinks.delete(token);
|
|
619
|
+
if (session.sinks.size === 0) {
|
|
620
|
+
session.parked = false;
|
|
621
|
+
session.parkedScreenBytes = 0;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
list() {
|
|
626
|
+
return Array.from(this.sessions.values(), (s) => ({
|
|
627
|
+
key: s.key,
|
|
628
|
+
alive: s.alive,
|
|
629
|
+
pid: s.proc?.pid ?? null,
|
|
630
|
+
command: s.command,
|
|
631
|
+
title: s.title,
|
|
632
|
+
totalBytes: s.totalBytes,
|
|
633
|
+
parked: s.parked,
|
|
634
|
+
parkedScreenBytes: s.parkedScreenBytes,
|
|
635
|
+
exit: s.exit
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
peek(key, sinceOffset) {
|
|
639
|
+
return peekRing(this.sessions.get(key), sinceOffset);
|
|
640
|
+
}
|
|
641
|
+
stats() {
|
|
642
|
+
let ringBytes = 0;
|
|
643
|
+
let parkedSessions = 0;
|
|
644
|
+
let parkedScreenBytes = 0;
|
|
645
|
+
for (const session of this.sessions.values()) {
|
|
646
|
+
ringBytes += session.bytes;
|
|
647
|
+
if (session.parked) {
|
|
648
|
+
parkedSessions++;
|
|
649
|
+
parkedScreenBytes += session.parkedScreenBytes;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
ringBytes,
|
|
654
|
+
ringCapacityBytes: this.sessions.size * (this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP),
|
|
655
|
+
parkedSessions,
|
|
656
|
+
parkedScreenBytes,
|
|
657
|
+
parkRestoreDeltas: this.parkRestoreDeltas,
|
|
658
|
+
parkRestoreFallbacks: this.parkRestoreFallbacks
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
sweepTasks(liveTaskIds) {
|
|
662
|
+
for (const key of Array.from(this.sessions.keys())) {
|
|
663
|
+
const taskId = key.split("::")[0] ?? key;
|
|
664
|
+
if (!liveTaskIds.has(taskId))
|
|
665
|
+
this.kill(key);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async killAll() {
|
|
669
|
+
const sessions = Array.from(this.sessions.keys(), (key) => this.kill(key));
|
|
670
|
+
const spare = this.spare;
|
|
671
|
+
this.spare = null;
|
|
672
|
+
if (spare)
|
|
673
|
+
sessions.push(this.endChild(spare));
|
|
674
|
+
await Promise.all(sessions);
|
|
675
|
+
}
|
|
676
|
+
liveCount() {
|
|
677
|
+
let n = 0;
|
|
678
|
+
for (const session of this.sessions.values())
|
|
679
|
+
if (session.alive)
|
|
680
|
+
n++;
|
|
681
|
+
return n;
|
|
682
|
+
}
|
|
683
|
+
spawn(key, spec, spare = false) {
|
|
684
|
+
const argv = spec.command && spec.command.length > 0 ? [...spec.command] : [spec.shell ?? resolveLoginShell()];
|
|
685
|
+
const cols = spec.cols ?? 80;
|
|
686
|
+
const rows = spec.rows ?? 24;
|
|
687
|
+
const session = {
|
|
688
|
+
key,
|
|
689
|
+
cwd: spec.cwd,
|
|
690
|
+
proc: null,
|
|
691
|
+
alive: true,
|
|
692
|
+
chunks: [],
|
|
693
|
+
bytes: 0,
|
|
694
|
+
totalBytes: 0,
|
|
695
|
+
cols,
|
|
696
|
+
rows,
|
|
697
|
+
command: argv,
|
|
698
|
+
title: "",
|
|
699
|
+
titleCarry: "",
|
|
700
|
+
titleDecoder: new StringDecoder("utf8"),
|
|
701
|
+
sinks: new Map,
|
|
702
|
+
parked: false,
|
|
703
|
+
parkedScreenBytes: 0,
|
|
704
|
+
exit: null
|
|
705
|
+
};
|
|
706
|
+
try {
|
|
707
|
+
session.proc = (this.opts.driver ?? bunTerminalDriver())({
|
|
708
|
+
argv,
|
|
709
|
+
cwd: spec.cwd,
|
|
710
|
+
env: embeddedTerminalEnv(process.env, {
|
|
711
|
+
TERM: "xterm-256color",
|
|
712
|
+
COLUMNS: String(cols),
|
|
713
|
+
LINES: String(rows),
|
|
714
|
+
BASH_SILENCE_DEPRECATION_WARNING: "1",
|
|
715
|
+
KOBE_TERMINAL_PTY: "1"
|
|
716
|
+
}),
|
|
717
|
+
cols,
|
|
718
|
+
rows,
|
|
719
|
+
onData: (data) => this.onData(session, data)
|
|
720
|
+
});
|
|
721
|
+
session.proc.exited.then((exit) => this.markExited(session, exit), () => this.markExited(session));
|
|
722
|
+
this.opts.log?.("pty", `spawned ${argv[0]} for ${key} (pid ${session.proc.pid})`);
|
|
723
|
+
if (!spare)
|
|
724
|
+
this.opts.onSessionStart?.();
|
|
725
|
+
} catch (err) {
|
|
726
|
+
session.alive = false;
|
|
727
|
+
this.opts.log?.("pty", `spawn failed for ${key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
728
|
+
}
|
|
729
|
+
return session;
|
|
730
|
+
}
|
|
731
|
+
onData(session, data) {
|
|
732
|
+
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
733
|
+
scanOscTitle(session, buf);
|
|
734
|
+
session.chunks.push(buf);
|
|
735
|
+
session.bytes += buf.byteLength;
|
|
736
|
+
session.totalBytes += buf.byteLength;
|
|
737
|
+
while (session.bytes > (this.opts.scrollbackCap ?? DEFAULT_SCROLLBACK_CAP) && session.chunks.length > 1) {
|
|
738
|
+
const dropped = session.chunks.shift();
|
|
739
|
+
if (dropped)
|
|
740
|
+
session.bytes -= dropped.byteLength;
|
|
741
|
+
}
|
|
742
|
+
if (session.sinks.size === 0)
|
|
743
|
+
return;
|
|
744
|
+
const frame = {
|
|
745
|
+
type: "event",
|
|
746
|
+
name: "pty.data",
|
|
747
|
+
payload: { key: session.key, data: buf.toString("base64") }
|
|
748
|
+
};
|
|
749
|
+
for (const sink of session.sinks.values())
|
|
750
|
+
sink(frame);
|
|
751
|
+
}
|
|
752
|
+
markExited(session, exit) {
|
|
753
|
+
if (!session.alive)
|
|
754
|
+
return;
|
|
755
|
+
session.alive = false;
|
|
756
|
+
session.exit = { code: exit?.code ?? null, signal: exit?.signal ?? null, at: new Date().toISOString() };
|
|
757
|
+
try {
|
|
758
|
+
session.proc?.close();
|
|
759
|
+
} catch {}
|
|
760
|
+
const frame = {
|
|
761
|
+
type: "event",
|
|
762
|
+
name: "pty.exit",
|
|
763
|
+
payload: { key: session.key, pid: session.proc?.pid ?? null, ...session.exit }
|
|
764
|
+
};
|
|
765
|
+
for (const sink of session.sinks.values())
|
|
766
|
+
sink(frame);
|
|
767
|
+
this.opts.log?.("pty", `session ${session.key} exited${describeExit(session.exit)}`);
|
|
768
|
+
try {
|
|
769
|
+
this.opts.onSessionExit?.({
|
|
770
|
+
key: session.key,
|
|
771
|
+
pid: session.proc?.pid ?? null,
|
|
772
|
+
exit: session.exit,
|
|
773
|
+
tail: ringTail(session.chunks, session.bytes, EXIT_TAIL_BYTES)
|
|
774
|
+
});
|
|
775
|
+
} catch {}
|
|
776
|
+
this.opts.onSessionEnd?.();
|
|
777
|
+
}
|
|
778
|
+
async endChild(session) {
|
|
779
|
+
if (!session.alive)
|
|
780
|
+
return;
|
|
781
|
+
const proc = session.proc;
|
|
782
|
+
if (!proc) {
|
|
783
|
+
this.markExited(session);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
signalProcessGroup(proc.pid, "SIGTERM", () => proc.kill("SIGTERM"));
|
|
787
|
+
if (!await settledWithin(proc.exited, TERMINATION_GRACE_MS)) {
|
|
788
|
+
signalProcessGroup(proc.pid, "SIGKILL", () => proc.kill("SIGKILL"));
|
|
789
|
+
await settledWithin(proc.exited, TERMINATION_GRACE_MS);
|
|
790
|
+
}
|
|
791
|
+
this.markExited(session);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ../kobe-daemon/src/daemon/pty-server.ts
|
|
796
|
+
var DEFAULT_IDLE_EXIT_MS = 60000;
|
|
797
|
+
function resolveIdleExitMs() {
|
|
798
|
+
const raw = process.env.KOBE_PTY_IDLE_EXIT_MS;
|
|
799
|
+
if (raw === undefined)
|
|
800
|
+
return DEFAULT_IDLE_EXIT_MS;
|
|
801
|
+
const n = Number(raw);
|
|
802
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_IDLE_EXIT_MS;
|
|
803
|
+
}
|
|
804
|
+
async function startPtyHostServer(options = {}) {
|
|
805
|
+
const socketPath = options.socketPath ?? defaultPtyHostSocketPath();
|
|
806
|
+
const pidPath = options.pidPath ?? defaultPtyHostPidPath();
|
|
807
|
+
const idleExitMs = options.idleExitMs || resolveIdleExitMs();
|
|
808
|
+
const log = options.log ?? (() => {});
|
|
809
|
+
const clients = new Set;
|
|
810
|
+
let stopping = false;
|
|
811
|
+
let idleTimer = null;
|
|
812
|
+
const cancelIdle = () => {
|
|
813
|
+
if (idleTimer)
|
|
814
|
+
clearTimeout(idleTimer);
|
|
815
|
+
idleTimer = null;
|
|
816
|
+
};
|
|
817
|
+
const armIdle = () => {
|
|
818
|
+
if (stopping)
|
|
819
|
+
return;
|
|
820
|
+
cancelIdle();
|
|
821
|
+
idleTimer = setTimeout(() => {
|
|
822
|
+
if (stopping || ptys.liveCount() > 0)
|
|
823
|
+
return;
|
|
824
|
+
log("idle", `no live sessions for ${idleExitMs}ms — exiting`);
|
|
825
|
+
stop();
|
|
826
|
+
}, idleExitMs);
|
|
827
|
+
};
|
|
828
|
+
const ptys = new PtyHost({
|
|
829
|
+
onSessionStart: cancelIdle,
|
|
830
|
+
onSessionEnd: () => {
|
|
831
|
+
if (ptys.liveCount() === 0)
|
|
832
|
+
armIdle();
|
|
833
|
+
},
|
|
834
|
+
onSessionExit: (info) => {
|
|
835
|
+
try {
|
|
836
|
+
recordPtyExit(info);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
log("pty", `exit record write failed for ${info.key}: ${err instanceof Error ? err.message : String(err)}`);
|
|
839
|
+
}
|
|
840
|
+
},
|
|
841
|
+
driver: options.driver,
|
|
842
|
+
log
|
|
843
|
+
});
|
|
844
|
+
const pipeSocket = isWindowsPipePath(socketPath);
|
|
845
|
+
if (!pipeSocket)
|
|
846
|
+
await mkdir(dirname2(socketPath), { recursive: true });
|
|
847
|
+
await mkdir(dirname2(pidPath), { recursive: true });
|
|
848
|
+
const server = createServer((socket) => {
|
|
849
|
+
const client = {
|
|
850
|
+
socket,
|
|
851
|
+
writer: new ClientWriter(socket, {
|
|
852
|
+
onOverflow: () => {
|
|
853
|
+
log("backpressure", "disconnecting PTY client whose critical queue exceeded 8MiB");
|
|
854
|
+
socket.destroy();
|
|
855
|
+
}
|
|
856
|
+
}),
|
|
857
|
+
buffer: ""
|
|
858
|
+
};
|
|
859
|
+
clients.add(client);
|
|
860
|
+
const decoder = new StringDecoder2("utf8");
|
|
861
|
+
socket.on("data", (chunk) => {
|
|
862
|
+
client.buffer += decoder.write(chunk);
|
|
863
|
+
drain(client);
|
|
864
|
+
});
|
|
865
|
+
socket.on("error", () => {});
|
|
866
|
+
socket.on("close", () => {
|
|
867
|
+
clients.delete(client);
|
|
868
|
+
ptys.detachClient(client);
|
|
869
|
+
});
|
|
870
|
+
});
|
|
871
|
+
const api = {
|
|
872
|
+
socketPath,
|
|
873
|
+
pidPath,
|
|
874
|
+
async close() {
|
|
875
|
+
if (stopping)
|
|
876
|
+
return;
|
|
877
|
+
stopping = true;
|
|
878
|
+
cancelIdle();
|
|
879
|
+
await ptys.killAll();
|
|
880
|
+
for (const client of Array.from(clients))
|
|
881
|
+
client.socket.destroy();
|
|
882
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
883
|
+
if (!pipeSocket)
|
|
884
|
+
await unlink(socketPath).catch(() => {});
|
|
885
|
+
await unlink(pidPath).catch(() => {});
|
|
886
|
+
}
|
|
887
|
+
};
|
|
888
|
+
async function stop() {
|
|
889
|
+
await api.close().catch((err) => logDaemonError("pty-host-shutdown", err));
|
|
890
|
+
options.onStop?.();
|
|
891
|
+
}
|
|
892
|
+
function dispatch(req, client) {
|
|
893
|
+
switch (req.name) {
|
|
894
|
+
case "hello":
|
|
895
|
+
return { protocolVersion: DAEMON_PROTOCOL_VERSION, ptyHost: true, pid: process.pid };
|
|
896
|
+
case "pty.open": {
|
|
897
|
+
const payload = objectPayload(req.payload);
|
|
898
|
+
return ptys.open(requireString(payload, "key"), {
|
|
899
|
+
cwd: requireString(payload, "cwd"),
|
|
900
|
+
command: Array.isArray(payload.command) ? payload.command.filter((c) => typeof c === "string") : undefined,
|
|
901
|
+
shell: typeof payload.shell === "string" ? payload.shell : undefined,
|
|
902
|
+
cols: typeof payload.cols === "number" ? payload.cols : undefined,
|
|
903
|
+
rows: typeof payload.rows === "number" ? payload.rows : undefined
|
|
904
|
+
}, client, (frame) => writeFrame(client, frame), typeof payload.sinceOffset === "number" ? payload.sinceOffset : undefined, typeof payload.sincePid === "number" ? payload.sincePid : undefined);
|
|
905
|
+
}
|
|
906
|
+
case "pty.write": {
|
|
907
|
+
const payload = objectPayload(req.payload);
|
|
908
|
+
ptys.write(requireString(payload, "key"), typeof payload.data === "string" ? payload.data : "");
|
|
909
|
+
return {};
|
|
910
|
+
}
|
|
911
|
+
case "pty.resize": {
|
|
912
|
+
const payload = objectPayload(req.payload);
|
|
913
|
+
ptys.resize(requireString(payload, "key"), typeof payload.cols === "number" ? payload.cols : 80, typeof payload.rows === "number" ? payload.rows : 24);
|
|
914
|
+
return {};
|
|
915
|
+
}
|
|
916
|
+
case "pty.kill":
|
|
917
|
+
ptys.kill(requireString(objectPayload(req.payload), "key"));
|
|
918
|
+
return {};
|
|
919
|
+
case "pty.detach":
|
|
920
|
+
{
|
|
921
|
+
const payload = objectPayload(req.payload);
|
|
922
|
+
ptys.detach(requireString(payload, "key"), client, payload.parked === true, typeof payload.parkedScreenBytes === "number" ? payload.parkedScreenBytes : 0);
|
|
923
|
+
}
|
|
924
|
+
return {};
|
|
925
|
+
case "pty.list":
|
|
926
|
+
return { pid: process.pid, rssBytes: process.memoryUsage().rss, sessions: ptys.list(), stats: ptys.stats() };
|
|
927
|
+
case "pty.peek": {
|
|
928
|
+
const payload = objectPayload(req.payload);
|
|
929
|
+
return ptys.peek(requireString(payload, "key"), typeof payload.sinceOffset === "number" ? payload.sinceOffset : undefined);
|
|
930
|
+
}
|
|
931
|
+
case "pty.warm": {
|
|
932
|
+
const payload = objectPayload(req.payload);
|
|
933
|
+
ptys.warm(requireString(payload, "cwd"), typeof payload.shell === "string" ? payload.shell : undefined, typeof payload.cols === "number" ? payload.cols : undefined, typeof payload.rows === "number" ? payload.rows : undefined);
|
|
934
|
+
return {};
|
|
935
|
+
}
|
|
936
|
+
case "pty.sweep": {
|
|
937
|
+
const payload = objectPayload(req.payload);
|
|
938
|
+
const ids = Array.isArray(payload.liveTaskIds) ? payload.liveTaskIds.filter((id) => typeof id === "string") : [];
|
|
939
|
+
ptys.sweepTasks(new Set(ids));
|
|
940
|
+
return {};
|
|
941
|
+
}
|
|
942
|
+
case "daemon.stop":
|
|
943
|
+
setTimeout(() => void stop(), 0).unref();
|
|
944
|
+
return {};
|
|
945
|
+
default:
|
|
946
|
+
throw new Error(`unknown pty-host request: ${req.name}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function drain(client) {
|
|
950
|
+
let nl = client.buffer.indexOf(`
|
|
951
|
+
`);
|
|
952
|
+
while (nl !== -1) {
|
|
953
|
+
const line = client.buffer.slice(0, nl);
|
|
954
|
+
client.buffer = client.buffer.slice(nl + 1);
|
|
955
|
+
if (line.trim().length > 0) {
|
|
956
|
+
let frame = null;
|
|
957
|
+
try {
|
|
958
|
+
frame = JSON.parse(line);
|
|
959
|
+
} catch {
|
|
960
|
+
writeFrame(client, { type: "response", id: "parse-error", error: { message: "malformed frame" } });
|
|
961
|
+
}
|
|
962
|
+
if (frame) {
|
|
963
|
+
if (frame.type !== "request") {
|
|
964
|
+
writeFrame(client, { type: "response", id: "parse-error", error: { message: "requests only" } });
|
|
965
|
+
} else {
|
|
966
|
+
try {
|
|
967
|
+
writeFrame(client, { type: "response", id: frame.id, name: frame.name, payload: dispatch(frame, client) });
|
|
968
|
+
} catch (err) {
|
|
969
|
+
writeFrame(client, {
|
|
970
|
+
type: "response",
|
|
971
|
+
id: frame.id,
|
|
972
|
+
name: frame.name,
|
|
973
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
nl = client.buffer.indexOf(`
|
|
980
|
+
`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
await new Promise((resolve, reject) => {
|
|
984
|
+
server.once("error", reject);
|
|
985
|
+
server.listen(socketPath, () => resolve());
|
|
986
|
+
});
|
|
987
|
+
await writeFile(pidPath, `${process.pid}
|
|
988
|
+
`, "utf8");
|
|
989
|
+
armIdle();
|
|
990
|
+
log("boot", `pty host listening on ${socketPath}`);
|
|
991
|
+
return api;
|
|
992
|
+
}
|
|
993
|
+
function writeFrame(client, frame) {
|
|
994
|
+
client.writer.write(frameToLine(frame), true);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ../kobe-daemon/src/daemon/pty-host-node-entry.ts
|
|
998
|
+
async function main() {
|
|
999
|
+
process.on("uncaughtException", (err) => console.error(`[pty-host crash] ${err?.stack ?? String(err)}`));
|
|
1000
|
+
process.on("unhandledRejection", (err) => console.error(`[pty-host reject] ${String(err)}`));
|
|
1001
|
+
const driver = await nodePtyDriver();
|
|
1002
|
+
const server = await startPtyHostServer({
|
|
1003
|
+
driver,
|
|
1004
|
+
log: (event, message) => console.log(`[pty-host ${event}] ${message}`),
|
|
1005
|
+
onStop: () => process.exit(0)
|
|
1006
|
+
});
|
|
1007
|
+
console.log(`rove pty-host (node): listening on ${server.socketPath}`);
|
|
1008
|
+
const shutdown = async () => {
|
|
1009
|
+
await server.close();
|
|
1010
|
+
process.exit(0);
|
|
1011
|
+
};
|
|
1012
|
+
process.once("SIGINT", () => void shutdown());
|
|
1013
|
+
process.once("SIGTERM", () => void shutdown());
|
|
1014
|
+
}
|
|
1015
|
+
await main();
|