@co0ontty/wand 4.38.0 → 4.39.0
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/dist/build-info.json +3 -3
- package/dist/cli.js +6 -0
- package/dist/process-manager.d.ts +12 -4
- package/dist/process-manager.js +314 -258
- package/dist/server-session-routes.js +9 -9
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -6
- package/dist/session-transport.d.ts +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.js +31 -6
- package/dist/terminal-daemon-client.d.ts +29 -0
- package/dist/terminal-daemon-client.js +406 -0
- package/dist/terminal-daemon-protocol.d.ts +45 -0
- package/dist/terminal-daemon-protocol.js +31 -0
- package/dist/terminal-daemon-server.d.ts +2 -0
- package/dist/terminal-daemon-server.js +349 -0
- package/dist/terminal-host.d.ts +79 -0
- package/dist/terminal-host.js +123 -0
- package/dist/tui/commands.js +220 -33
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +65 -65
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import net from "node:net";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { TERMINAL_DAEMON_PROTOCOL_VERSION, terminalDaemonPaths, } from "./terminal-daemon-protocol.js";
|
|
6
|
+
import { appendTerminalChunkWindow, InProcessTerminalHost, } from "./terminal-host.js";
|
|
7
|
+
import { appendWindow, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
|
|
8
|
+
class RemoteTerminalProcess {
|
|
9
|
+
sessionId;
|
|
10
|
+
incarnationId;
|
|
11
|
+
pid;
|
|
12
|
+
client;
|
|
13
|
+
dataListeners = new Set();
|
|
14
|
+
exitListeners = new Set();
|
|
15
|
+
paused = false;
|
|
16
|
+
pausedEvents = [];
|
|
17
|
+
undeliveredEvents = [];
|
|
18
|
+
pendingExit = null;
|
|
19
|
+
constructor(sessionId, incarnationId, pid, client) {
|
|
20
|
+
this.sessionId = sessionId;
|
|
21
|
+
this.incarnationId = incarnationId;
|
|
22
|
+
this.pid = pid;
|
|
23
|
+
this.client = client;
|
|
24
|
+
}
|
|
25
|
+
write(data) {
|
|
26
|
+
void this.client.request("write", { sessionId: this.sessionId, data }).catch((error) => {
|
|
27
|
+
this.client.reportOperationError(this.sessionId, error);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
resize(cols, rows) {
|
|
31
|
+
void this.client.request("resize", { sessionId: this.sessionId, cols, rows }).catch((error) => {
|
|
32
|
+
this.client.reportOperationError(this.sessionId, error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
kill(signal) {
|
|
36
|
+
void this.client.request("kill", { sessionId: this.sessionId, signal }).catch((error) => {
|
|
37
|
+
this.client.reportOperationError(this.sessionId, error);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
pause() {
|
|
41
|
+
this.paused = true;
|
|
42
|
+
}
|
|
43
|
+
resume() {
|
|
44
|
+
if (!this.paused)
|
|
45
|
+
return;
|
|
46
|
+
this.paused = false;
|
|
47
|
+
const pending = this.pausedEvents;
|
|
48
|
+
this.pausedEvents = [];
|
|
49
|
+
for (const event of pending)
|
|
50
|
+
this.emitData(event);
|
|
51
|
+
}
|
|
52
|
+
onData(listener) {
|
|
53
|
+
this.dataListeners.add(listener);
|
|
54
|
+
if (this.undeliveredEvents.length > 0) {
|
|
55
|
+
const pending = this.undeliveredEvents;
|
|
56
|
+
this.undeliveredEvents = [];
|
|
57
|
+
queueMicrotask(() => {
|
|
58
|
+
if (!this.dataListeners.has(listener))
|
|
59
|
+
return;
|
|
60
|
+
for (const event of pending)
|
|
61
|
+
listener(event);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return { dispose: () => this.dataListeners.delete(listener) };
|
|
65
|
+
}
|
|
66
|
+
onExit(listener) {
|
|
67
|
+
this.exitListeners.add(listener);
|
|
68
|
+
const pending = this.pendingExit;
|
|
69
|
+
if (pending) {
|
|
70
|
+
queueMicrotask(() => {
|
|
71
|
+
if (this.exitListeners.has(listener))
|
|
72
|
+
listener(pending);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { dispose: () => this.exitListeners.delete(listener) };
|
|
76
|
+
}
|
|
77
|
+
acceptData(event) {
|
|
78
|
+
if (this.paused) {
|
|
79
|
+
this.pausedEvents = appendTerminalChunkWindow(this.pausedEvents, event);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.dataListeners.size === 0) {
|
|
83
|
+
this.undeliveredEvents = appendTerminalChunkWindow(this.undeliveredEvents, event);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.emitData(event);
|
|
87
|
+
}
|
|
88
|
+
acceptExit(event) {
|
|
89
|
+
this.pendingExit = event;
|
|
90
|
+
for (const listener of Array.from(this.exitListeners))
|
|
91
|
+
listener(event);
|
|
92
|
+
}
|
|
93
|
+
emitData(event) {
|
|
94
|
+
for (const listener of Array.from(this.dataListeners))
|
|
95
|
+
listener(event);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class TerminalDaemonClient {
|
|
99
|
+
socketPath;
|
|
100
|
+
token;
|
|
101
|
+
persistent = true;
|
|
102
|
+
socket = null;
|
|
103
|
+
buffer = "";
|
|
104
|
+
nextRequestId = 1;
|
|
105
|
+
pending = new Map();
|
|
106
|
+
inventory = new Map();
|
|
107
|
+
handles = new Map();
|
|
108
|
+
pendingEvents = new Map();
|
|
109
|
+
constructor(socketPath, token) {
|
|
110
|
+
this.socketPath = socketPath;
|
|
111
|
+
this.token = token;
|
|
112
|
+
}
|
|
113
|
+
async connect() {
|
|
114
|
+
if (this.socket && !this.socket.destroyed)
|
|
115
|
+
return;
|
|
116
|
+
const socket = net.createConnection(this.socketPath);
|
|
117
|
+
socket.setNoDelay(true);
|
|
118
|
+
this.socket = socket;
|
|
119
|
+
await new Promise((resolve, reject) => {
|
|
120
|
+
const onConnect = () => { cleanup(); resolve(); };
|
|
121
|
+
const onError = (error) => { cleanup(); reject(error); };
|
|
122
|
+
const cleanup = () => {
|
|
123
|
+
socket.off("connect", onConnect);
|
|
124
|
+
socket.off("error", onError);
|
|
125
|
+
};
|
|
126
|
+
socket.once("connect", onConnect);
|
|
127
|
+
socket.once("error", onError);
|
|
128
|
+
});
|
|
129
|
+
socket.on("data", (data) => this.consume(data.toString("utf8")));
|
|
130
|
+
socket.on("close", () => this.rejectPending(new Error("Terminal daemon disconnected")));
|
|
131
|
+
socket.on("error", (error) => this.rejectPending(error));
|
|
132
|
+
await this.request("hello");
|
|
133
|
+
const sessions = await this.request("list");
|
|
134
|
+
this.inventory.clear();
|
|
135
|
+
for (const session of sessions)
|
|
136
|
+
this.inventory.set(session.sessionId, session);
|
|
137
|
+
}
|
|
138
|
+
attach(sessionId, afterSeq = 0) {
|
|
139
|
+
const state = this.inventory.get(sessionId);
|
|
140
|
+
if (!state)
|
|
141
|
+
return null;
|
|
142
|
+
return this.resultFromState(state, false, afterSeq);
|
|
143
|
+
}
|
|
144
|
+
async createOrAttach(request, afterSeq = 0) {
|
|
145
|
+
const existing = this.inventory.get(request.sessionId);
|
|
146
|
+
if (existing)
|
|
147
|
+
return this.resultFromState(existing, false, afterSeq);
|
|
148
|
+
const payload = await this.request("createOrAttach", { ...request, afterSeq });
|
|
149
|
+
this.inventory.set(payload.state.sessionId, payload.state);
|
|
150
|
+
return this.resultFromState(payload.state, payload.isNew, afterSeq);
|
|
151
|
+
}
|
|
152
|
+
forget(sessionId) {
|
|
153
|
+
this.inventory.delete(sessionId);
|
|
154
|
+
this.handles.delete(sessionId);
|
|
155
|
+
this.pendingEvents.delete(sessionId);
|
|
156
|
+
void this.request("forget", { sessionId }).catch((error) => this.reportOperationError(sessionId, error));
|
|
157
|
+
}
|
|
158
|
+
disconnect() {
|
|
159
|
+
const socket = this.socket;
|
|
160
|
+
this.socket = null;
|
|
161
|
+
if (socket && !socket.destroyed)
|
|
162
|
+
socket.destroy();
|
|
163
|
+
this.rejectPending(new Error("Terminal daemon client disposed"));
|
|
164
|
+
this.handles.clear();
|
|
165
|
+
this.pendingEvents.clear();
|
|
166
|
+
}
|
|
167
|
+
async request(method, params) {
|
|
168
|
+
const socket = this.socket;
|
|
169
|
+
if (!socket || socket.destroyed || !socket.writable)
|
|
170
|
+
throw new Error("Terminal daemon is unavailable");
|
|
171
|
+
const id = this.nextRequestId++;
|
|
172
|
+
const request = {
|
|
173
|
+
kind: "request",
|
|
174
|
+
id,
|
|
175
|
+
token: this.token,
|
|
176
|
+
protocolVersion: TERMINAL_DAEMON_PROTOCOL_VERSION,
|
|
177
|
+
method,
|
|
178
|
+
params,
|
|
179
|
+
};
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
this.pending.delete(id);
|
|
183
|
+
reject(new Error(`Terminal daemon ${method} timed out`));
|
|
184
|
+
}, 10_000);
|
|
185
|
+
timer.unref?.();
|
|
186
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
187
|
+
socket.write(`${JSON.stringify(request)}\n`);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
reportOperationError(sessionId, error) {
|
|
191
|
+
if (!this.socket || this.socket.destroyed)
|
|
192
|
+
return;
|
|
193
|
+
process.stderr.write(`[wand] terminal daemon operation failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
194
|
+
}
|
|
195
|
+
resultFromState(state, isNew, afterSeq) {
|
|
196
|
+
let process = this.handles.get(state.sessionId) ?? null;
|
|
197
|
+
if (state.status === "running" && (!process || process.incarnationId !== state.incarnationId)) {
|
|
198
|
+
process = new RemoteTerminalProcess(state.sessionId, state.incarnationId, state.pid, this);
|
|
199
|
+
this.handles.set(state.sessionId, process);
|
|
200
|
+
const pending = this.pendingEvents.get(state.sessionId) ?? [];
|
|
201
|
+
this.pendingEvents.delete(state.sessionId);
|
|
202
|
+
for (const event of pending) {
|
|
203
|
+
const alreadyInSnapshot = event.event === "data"
|
|
204
|
+
? typeof event.seq === "number" && event.seq <= state.seq
|
|
205
|
+
: false;
|
|
206
|
+
if (!alreadyInSnapshot)
|
|
207
|
+
this.routeEvent(event);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (state.status !== "running")
|
|
211
|
+
process = null;
|
|
212
|
+
return {
|
|
213
|
+
process,
|
|
214
|
+
state,
|
|
215
|
+
replay: state.chunks.filter((chunk) => chunk.seq > afterSeq),
|
|
216
|
+
isNew,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
consume(chunk) {
|
|
220
|
+
this.buffer += chunk;
|
|
221
|
+
while (true) {
|
|
222
|
+
const newline = this.buffer.indexOf("\n");
|
|
223
|
+
if (newline < 0)
|
|
224
|
+
return;
|
|
225
|
+
const line = this.buffer.slice(0, newline);
|
|
226
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
227
|
+
if (!line)
|
|
228
|
+
continue;
|
|
229
|
+
let message;
|
|
230
|
+
try {
|
|
231
|
+
message = JSON.parse(line);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (message.kind === "response") {
|
|
237
|
+
const pending = this.pending.get(message.id);
|
|
238
|
+
if (!pending)
|
|
239
|
+
continue;
|
|
240
|
+
this.pending.delete(message.id);
|
|
241
|
+
clearTimeout(pending.timer);
|
|
242
|
+
if (message.ok)
|
|
243
|
+
pending.resolve(message.result);
|
|
244
|
+
else
|
|
245
|
+
pending.reject(new Error(message.error));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
this.routeEvent(message);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
routeEvent(event) {
|
|
253
|
+
const state = this.inventory.get(event.sessionId);
|
|
254
|
+
if (state && state.incarnationId === event.incarnationId) {
|
|
255
|
+
if (event.event === "data" && typeof event.data === "string" && typeof event.seq === "number") {
|
|
256
|
+
state.seq = Math.max(state.seq, event.seq);
|
|
257
|
+
state.output = appendWindow(state.output, event.data, PTY_OUTPUT_MAX_SIZE);
|
|
258
|
+
state.chunks = appendTerminalChunkWindow(state.chunks, { data: event.data, seq: event.seq });
|
|
259
|
+
}
|
|
260
|
+
else if (event.event === "exit") {
|
|
261
|
+
state.status = "exited";
|
|
262
|
+
state.exitCode = event.exitCode ?? -1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const handle = this.handles.get(event.sessionId);
|
|
266
|
+
if (!handle || handle.incarnationId !== event.incarnationId) {
|
|
267
|
+
const pending = this.pendingEvents.get(event.sessionId) ?? [];
|
|
268
|
+
pending.push(event);
|
|
269
|
+
this.pendingEvents.set(event.sessionId, pending.slice(-512));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (event.event === "data" && typeof event.data === "string" && typeof event.seq === "number") {
|
|
273
|
+
handle.acceptData({ data: event.data, seq: event.seq });
|
|
274
|
+
}
|
|
275
|
+
else if (event.event === "exit") {
|
|
276
|
+
handle.acceptExit({ exitCode: event.exitCode ?? -1, signal: event.signal });
|
|
277
|
+
this.handles.delete(event.sessionId);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
rejectPending(error) {
|
|
281
|
+
for (const pending of this.pending.values()) {
|
|
282
|
+
clearTimeout(pending.timer);
|
|
283
|
+
pending.reject(error);
|
|
284
|
+
}
|
|
285
|
+
this.pending.clear();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
export async function createTerminalHost(configPath) {
|
|
289
|
+
if (process.env.WAND_TEST_MODE === "1" || process.env.NODE_TEST_CONTEXT)
|
|
290
|
+
return new InProcessTerminalHost();
|
|
291
|
+
const paths = terminalDaemonPaths(configPath);
|
|
292
|
+
const endpointExists = process.platform === "win32" || existsSync(paths.socketPath);
|
|
293
|
+
if (endpointExists) {
|
|
294
|
+
const adopted = await tryConnect(paths.socketPath, paths.tokenPath);
|
|
295
|
+
if (adopted)
|
|
296
|
+
return adopted;
|
|
297
|
+
if (await probeSocket(paths.socketPath)) {
|
|
298
|
+
const starting = await waitForTerminalDaemon(paths.socketPath, paths.tokenPath, 5_000);
|
|
299
|
+
if (starting)
|
|
300
|
+
return starting;
|
|
301
|
+
process.stderr.write("[wand] Existing terminal daemon rejected adoption; using non-persistent PTYs without replacing it.\n");
|
|
302
|
+
return new InProcessTerminalHost();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Service managers can start terminald and the web process at nearly the
|
|
306
|
+
// same time. The daemon writes its pid before opening the socket, so give
|
|
307
|
+
// that owner time to become ready instead of spawning a competing daemon.
|
|
308
|
+
if (hasLiveDaemonPid(paths.pidPath)) {
|
|
309
|
+
const adopted = await waitForTerminalDaemon(paths.socketPath, paths.tokenPath, 5_000);
|
|
310
|
+
if (adopted)
|
|
311
|
+
return adopted;
|
|
312
|
+
process.stderr.write("[wand] Terminal daemon process is alive but its socket is unavailable; using non-persistent PTYs without replacing it.\n");
|
|
313
|
+
return new InProcessTerminalHost();
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
startDetachedDaemon(configPath);
|
|
317
|
+
const client = await waitForTerminalDaemon(paths.socketPath, paths.tokenPath, 5_000);
|
|
318
|
+
if (client)
|
|
319
|
+
return client;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
process.stderr.write(`[wand] Failed to start terminal daemon: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
323
|
+
}
|
|
324
|
+
process.stderr.write("[wand] Terminal daemon unavailable; falling back to in-process PTYs.\n");
|
|
325
|
+
return new InProcessTerminalHost();
|
|
326
|
+
}
|
|
327
|
+
async function waitForTerminalDaemon(socketPath, tokenPath, timeoutMs) {
|
|
328
|
+
const deadline = Date.now() + timeoutMs;
|
|
329
|
+
while (Date.now() < deadline) {
|
|
330
|
+
const client = await tryConnect(socketPath, tokenPath);
|
|
331
|
+
if (client)
|
|
332
|
+
return client;
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
function hasLiveDaemonPid(pidPath) {
|
|
338
|
+
let pid;
|
|
339
|
+
try {
|
|
340
|
+
pid = Number(readFileSync(pidPath, "utf8").trim());
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
346
|
+
return false;
|
|
347
|
+
try {
|
|
348
|
+
process.kill(pid, 0);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function tryConnect(socketPath, tokenPath) {
|
|
356
|
+
let token;
|
|
357
|
+
try {
|
|
358
|
+
token = readFileSync(tokenPath, "utf8").trim();
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
if (!token)
|
|
364
|
+
return null;
|
|
365
|
+
const client = new TerminalDaemonClient(socketPath, token);
|
|
366
|
+
try {
|
|
367
|
+
await client.connect();
|
|
368
|
+
return client;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
client.disconnect();
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Connect to an already-running terminal daemon without starting a replacement. */
|
|
376
|
+
export async function connectExistingTerminalHost(configPath) {
|
|
377
|
+
const paths = terminalDaemonPaths(configPath);
|
|
378
|
+
return tryConnect(paths.socketPath, paths.tokenPath);
|
|
379
|
+
}
|
|
380
|
+
function probeSocket(socketPath) {
|
|
381
|
+
return new Promise((resolve) => {
|
|
382
|
+
const socket = net.createConnection(socketPath);
|
|
383
|
+
const finish = (alive) => {
|
|
384
|
+
socket.removeAllListeners();
|
|
385
|
+
socket.destroy();
|
|
386
|
+
resolve(alive);
|
|
387
|
+
};
|
|
388
|
+
socket.once("connect", () => finish(true));
|
|
389
|
+
socket.once("error", () => finish(false));
|
|
390
|
+
const timer = setTimeout(() => finish(false), 300);
|
|
391
|
+
timer.unref?.();
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function startDetachedDaemon(configPath) {
|
|
395
|
+
const entry = process.argv[1];
|
|
396
|
+
if (!entry || !existsSync(entry))
|
|
397
|
+
throw new Error("Cannot resolve Wand CLI entrypoint");
|
|
398
|
+
const runtimeArgs = entry.endsWith(".ts") ? process.execArgv : [];
|
|
399
|
+
const child = spawn(process.execPath, [...runtimeArgs, entry, "terminald", "-c", configPath], {
|
|
400
|
+
detached: true,
|
|
401
|
+
stdio: "ignore",
|
|
402
|
+
cwd: process.cwd(),
|
|
403
|
+
env: process.env,
|
|
404
|
+
});
|
|
405
|
+
child.unref();
|
|
406
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TerminalSessionState, TerminalSpawnRequest } from "./terminal-host.js";
|
|
2
|
+
export declare const TERMINAL_DAEMON_PROTOCOL_VERSION = 1;
|
|
3
|
+
export interface TerminalDaemonPaths {
|
|
4
|
+
socketPath: string;
|
|
5
|
+
tokenPath: string;
|
|
6
|
+
pidPath: string;
|
|
7
|
+
}
|
|
8
|
+
export type TerminalDaemonRequest = {
|
|
9
|
+
kind: "request";
|
|
10
|
+
id: number;
|
|
11
|
+
token: string;
|
|
12
|
+
protocolVersion: number;
|
|
13
|
+
method: "hello" | "list" | "createOrAttach" | "write" | "resize" | "kill" | "forget";
|
|
14
|
+
params?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
export type TerminalDaemonResponse = {
|
|
17
|
+
kind: "response";
|
|
18
|
+
id: number;
|
|
19
|
+
ok: true;
|
|
20
|
+
result?: unknown;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "response";
|
|
23
|
+
id: number;
|
|
24
|
+
ok: false;
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
27
|
+
export type TerminalDaemonEvent = {
|
|
28
|
+
kind: "event";
|
|
29
|
+
event: "data" | "exit";
|
|
30
|
+
sessionId: string;
|
|
31
|
+
incarnationId: string;
|
|
32
|
+
data?: string;
|
|
33
|
+
seq?: number;
|
|
34
|
+
exitCode?: number;
|
|
35
|
+
signal?: number;
|
|
36
|
+
};
|
|
37
|
+
export interface TerminalDaemonCreateParams extends TerminalSpawnRequest {
|
|
38
|
+
afterSeq?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TerminalDaemonAttachPayload {
|
|
41
|
+
state: TerminalSessionState;
|
|
42
|
+
isNew: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function terminalDaemonPaths(configPath: string): TerminalDaemonPaths;
|
|
45
|
+
export declare function terminalDaemonRuntimeLabel(configPath: string): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const TERMINAL_DAEMON_PROTOCOL_VERSION = 1;
|
|
5
|
+
export function terminalDaemonPaths(configPath) {
|
|
6
|
+
const resolved = path.resolve(configPath);
|
|
7
|
+
const suffix = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
8
|
+
const dir = path.dirname(resolved);
|
|
9
|
+
const uid = (() => {
|
|
10
|
+
try {
|
|
11
|
+
return os.userInfo().uid;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
return {
|
|
18
|
+
socketPath: process.platform === "win32"
|
|
19
|
+
? `\\\\.\\pipe\\wand-terminald-${suffix}`
|
|
20
|
+
// Unix-domain socket paths are limited to roughly 100 bytes on macOS.
|
|
21
|
+
// Keep the endpoint short even when the config lives in a deep directory;
|
|
22
|
+
// the per-config secret remains alongside the config with mode 0600.
|
|
23
|
+
: path.join("/tmp", `wand-terminald-${uid}-${suffix}.sock`),
|
|
24
|
+
tokenPath: path.join(dir, `.terminald-${suffix}.token`),
|
|
25
|
+
pidPath: path.join(dir, `.terminald-${suffix}.pid`),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function terminalDaemonRuntimeLabel(configPath) {
|
|
29
|
+
const suffix = createHash("sha256").update(path.resolve(configPath)).digest("hex").slice(0, 8);
|
|
30
|
+
return `wand-terminald-${os.userInfo().uid}-${suffix}`;
|
|
31
|
+
}
|