adhdev 0.7.16 → 0.7.19
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/cli/index.js +470 -343
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +886 -305
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/vendor/session-host-daemon/index.d.mts +33 -0
- package/vendor/session-host-daemon/index.d.ts +33 -0
- package/vendor/session-host-daemon/index.js +812 -0
- package/vendor/session-host-daemon/index.js.map +1 -0
- package/vendor/session-host-daemon/index.mjs +795 -0
- package/vendor/session-host-daemon/index.mjs.map +1 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { randomUUID } from "crypto";
|
|
11
|
+
import {
|
|
12
|
+
SessionHostClient,
|
|
13
|
+
formatRuntimeOwner,
|
|
14
|
+
getDefaultSessionHostEndpoint as getDefaultSessionHostEndpoint2,
|
|
15
|
+
resolveRuntimeRecord
|
|
16
|
+
} from "@adhdev/session-host-core";
|
|
17
|
+
|
|
18
|
+
// src/server.ts
|
|
19
|
+
import { EventEmitter } from "events";
|
|
20
|
+
import * as fs2 from "fs";
|
|
21
|
+
import * as net from "net";
|
|
22
|
+
import {
|
|
23
|
+
SessionHostRegistry,
|
|
24
|
+
createLineParser,
|
|
25
|
+
createResponseEnvelope,
|
|
26
|
+
getDefaultSessionHostEndpoint,
|
|
27
|
+
writeEnvelope
|
|
28
|
+
} from "@adhdev/session-host-core";
|
|
29
|
+
|
|
30
|
+
// src/runtime.ts
|
|
31
|
+
import * as os from "os";
|
|
32
|
+
import * as path from "path";
|
|
33
|
+
import * as pty from "node-pty";
|
|
34
|
+
if (os.platform() !== "win32") {
|
|
35
|
+
try {
|
|
36
|
+
const fs3 = __require("fs");
|
|
37
|
+
const ptyDir = path.resolve(path.dirname(__require.resolve("node-pty")), "..");
|
|
38
|
+
const platformArch = `${os.platform()}-${os.arch()}`;
|
|
39
|
+
const helper = path.join(ptyDir, "prebuilds", platformArch, "spawn-helper");
|
|
40
|
+
if (fs3.existsSync(helper)) {
|
|
41
|
+
const stat = fs3.statSync(helper);
|
|
42
|
+
if (!(stat.mode & 73)) {
|
|
43
|
+
fs3.chmodSync(helper, stat.mode | 493);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
var PtySessionRuntime = class {
|
|
50
|
+
sessionId;
|
|
51
|
+
payload;
|
|
52
|
+
cols;
|
|
53
|
+
rows;
|
|
54
|
+
ptyProcess = null;
|
|
55
|
+
onDataCallback;
|
|
56
|
+
onExitCallback;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.sessionId = options.sessionId;
|
|
59
|
+
this.payload = options.payload;
|
|
60
|
+
this.cols = options.payload.cols || 120;
|
|
61
|
+
this.rows = options.payload.rows || 40;
|
|
62
|
+
this.onDataCallback = options.onData;
|
|
63
|
+
this.onExitCallback = options.onExit;
|
|
64
|
+
}
|
|
65
|
+
start() {
|
|
66
|
+
if (this.ptyProcess) return this.ptyProcess.pid;
|
|
67
|
+
const command = this.payload.launchCommand.command;
|
|
68
|
+
const args = this.payload.launchCommand.args || [];
|
|
69
|
+
const cwd = this.payload.workspace || process.cwd();
|
|
70
|
+
const env = {
|
|
71
|
+
...process.env,
|
|
72
|
+
...this.payload.launchCommand.env || {}
|
|
73
|
+
};
|
|
74
|
+
this.ptyProcess = pty.spawn(command, args, {
|
|
75
|
+
name: os.platform() === "win32" ? "xterm-color" : "xterm-256color",
|
|
76
|
+
cols: this.cols,
|
|
77
|
+
rows: this.rows,
|
|
78
|
+
cwd,
|
|
79
|
+
env
|
|
80
|
+
});
|
|
81
|
+
this.ptyProcess.onData((data) => {
|
|
82
|
+
this.onDataCallback(data);
|
|
83
|
+
});
|
|
84
|
+
this.ptyProcess.onExit(({ exitCode }) => {
|
|
85
|
+
this.ptyProcess = null;
|
|
86
|
+
this.onExitCallback(exitCode ?? null);
|
|
87
|
+
});
|
|
88
|
+
return this.ptyProcess.pid;
|
|
89
|
+
}
|
|
90
|
+
write(data) {
|
|
91
|
+
if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
|
|
92
|
+
this.ptyProcess.write(data);
|
|
93
|
+
}
|
|
94
|
+
resize(cols, rows) {
|
|
95
|
+
if (!this.ptyProcess) throw new Error(`Session not running: ${this.sessionId}`);
|
|
96
|
+
this.ptyProcess.resize(cols, rows);
|
|
97
|
+
}
|
|
98
|
+
stop() {
|
|
99
|
+
if (!this.ptyProcess) return;
|
|
100
|
+
this.ptyProcess.kill();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/storage.ts
|
|
105
|
+
import * as fs from "fs";
|
|
106
|
+
import * as os2 from "os";
|
|
107
|
+
import * as path2 from "path";
|
|
108
|
+
var SessionHostStorage = class {
|
|
109
|
+
rootDir;
|
|
110
|
+
runtimesDir;
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
const appName = options.appName || "adhdev";
|
|
113
|
+
this.rootDir = path2.join(os2.homedir(), ".adhdev", "session-host", appName);
|
|
114
|
+
this.runtimesDir = path2.join(this.rootDir, "runtimes");
|
|
115
|
+
}
|
|
116
|
+
loadAll() {
|
|
117
|
+
if (!fs.existsSync(this.runtimesDir)) return [];
|
|
118
|
+
const entries = fs.readdirSync(this.runtimesDir, { withFileTypes: true });
|
|
119
|
+
const states = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
122
|
+
const fullPath = path2.join(this.runtimesDir, entry.name);
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
125
|
+
if (parsed?.record?.sessionId) {
|
|
126
|
+
states.push(parsed);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return states.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
132
|
+
}
|
|
133
|
+
save(record, snapshot) {
|
|
134
|
+
fs.mkdirSync(this.runtimesDir, { recursive: true });
|
|
135
|
+
const filePath = path2.join(this.runtimesDir, `${record.sessionId}.json`);
|
|
136
|
+
const payload = {
|
|
137
|
+
record,
|
|
138
|
+
snapshot,
|
|
139
|
+
updatedAt: Date.now()
|
|
140
|
+
};
|
|
141
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/server.ts
|
|
146
|
+
var SessionHostServer = class extends EventEmitter {
|
|
147
|
+
endpoint;
|
|
148
|
+
registry = new SessionHostRegistry();
|
|
149
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
150
|
+
storage;
|
|
151
|
+
ipcServer = null;
|
|
152
|
+
sockets = /* @__PURE__ */ new Set();
|
|
153
|
+
persistTimers = /* @__PURE__ */ new Map();
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
super();
|
|
156
|
+
this.endpoint = options.endpoint || getDefaultSessionHostEndpoint(options.appName || "adhdev");
|
|
157
|
+
this.storage = new SessionHostStorage({ appName: options.appName || "adhdev" });
|
|
158
|
+
}
|
|
159
|
+
async start() {
|
|
160
|
+
this.restorePersistedRuntimes();
|
|
161
|
+
if (this.endpoint.kind === "unix") {
|
|
162
|
+
try {
|
|
163
|
+
fs2.unlinkSync(this.endpoint.path);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.ipcServer = net.createServer((socket) => {
|
|
168
|
+
this.sockets.add(socket);
|
|
169
|
+
socket.on("close", () => {
|
|
170
|
+
this.sockets.delete(socket);
|
|
171
|
+
});
|
|
172
|
+
socket.on("data", createLineParser((envelope) => {
|
|
173
|
+
if (envelope.kind !== "request") return;
|
|
174
|
+
void this.handleIncomingRequest(socket, envelope);
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
await new Promise((resolve2, reject) => {
|
|
178
|
+
this.ipcServer?.once("listening", () => resolve2());
|
|
179
|
+
this.ipcServer?.once("error", reject);
|
|
180
|
+
this.ipcServer?.listen(this.endpoint.path);
|
|
181
|
+
});
|
|
182
|
+
this.emit("log", `session host endpoint ready: ${this.endpoint.path}`);
|
|
183
|
+
}
|
|
184
|
+
async stop() {
|
|
185
|
+
this.flushAllPersistence();
|
|
186
|
+
for (const runtime of this.runtimes.values()) {
|
|
187
|
+
try {
|
|
188
|
+
runtime.stop();
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
this.runtimes.clear();
|
|
193
|
+
for (const timer of this.persistTimers.values()) {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
this.persistTimers.clear();
|
|
197
|
+
for (const socket of this.sockets) {
|
|
198
|
+
socket.destroy();
|
|
199
|
+
}
|
|
200
|
+
this.sockets.clear();
|
|
201
|
+
if (this.ipcServer) {
|
|
202
|
+
const server = this.ipcServer;
|
|
203
|
+
this.ipcServer = null;
|
|
204
|
+
await new Promise((resolve2) => server.close(() => resolve2()));
|
|
205
|
+
}
|
|
206
|
+
if (this.endpoint.kind === "unix") {
|
|
207
|
+
try {
|
|
208
|
+
fs2.unlinkSync(this.endpoint.path);
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.removeAllListeners();
|
|
213
|
+
}
|
|
214
|
+
async handleRequest(request) {
|
|
215
|
+
try {
|
|
216
|
+
switch (request.type) {
|
|
217
|
+
case "create_session": {
|
|
218
|
+
const record = this.registry.createSession(request.payload);
|
|
219
|
+
this.schedulePersist(record.sessionId);
|
|
220
|
+
this.emitEvent({ type: "session_created", sessionId: record.sessionId, record });
|
|
221
|
+
try {
|
|
222
|
+
const startedRecord = this.startRuntime(record, request.payload, "session_started");
|
|
223
|
+
return { success: true, result: startedRecord };
|
|
224
|
+
} catch (error) {
|
|
225
|
+
this.registry.markStopped(record.sessionId, "failed");
|
|
226
|
+
this.persistNow(record.sessionId);
|
|
227
|
+
return { success: false, error: error?.message || String(error) };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
case "list_sessions":
|
|
231
|
+
return { success: true, result: this.registry.listSessions() };
|
|
232
|
+
case "attach_session": {
|
|
233
|
+
const record = this.registry.attachClient(request.payload);
|
|
234
|
+
this.schedulePersist(record.sessionId);
|
|
235
|
+
const client = record.attachedClients.find((item) => item.clientId === request.payload.clientId);
|
|
236
|
+
if (client) {
|
|
237
|
+
this.emitEvent({ type: "client_attached", sessionId: record.sessionId, client });
|
|
238
|
+
}
|
|
239
|
+
return { success: true, result: record };
|
|
240
|
+
}
|
|
241
|
+
case "detach_session": {
|
|
242
|
+
const record = this.registry.detachClient(request.payload);
|
|
243
|
+
this.schedulePersist(record.sessionId);
|
|
244
|
+
this.emitEvent({ type: "client_detached", sessionId: record.sessionId, clientId: request.payload.clientId });
|
|
245
|
+
return { success: true, result: record };
|
|
246
|
+
}
|
|
247
|
+
case "acquire_write": {
|
|
248
|
+
const record = this.registry.acquireWrite(request.payload);
|
|
249
|
+
this.persistNow(record.sessionId);
|
|
250
|
+
this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
|
|
251
|
+
return { success: true, result: record };
|
|
252
|
+
}
|
|
253
|
+
case "release_write": {
|
|
254
|
+
const record = this.registry.releaseWrite(request.payload);
|
|
255
|
+
this.persistNow(record.sessionId);
|
|
256
|
+
this.emitEvent({ type: "write_owner_changed", sessionId: record.sessionId, owner: record.writeOwner });
|
|
257
|
+
return { success: true, result: record };
|
|
258
|
+
}
|
|
259
|
+
case "get_snapshot":
|
|
260
|
+
return { success: true, result: this.registry.getSnapshot(request.payload.sessionId, request.payload.sinceSeq) };
|
|
261
|
+
case "clear_session_buffer": {
|
|
262
|
+
const record = this.registry.clearBuffer(request.payload.sessionId);
|
|
263
|
+
this.persistNow(record.sessionId);
|
|
264
|
+
this.emitEvent({ type: "session_cleared", sessionId: record.sessionId });
|
|
265
|
+
return { success: true, result: record };
|
|
266
|
+
}
|
|
267
|
+
case "send_input": {
|
|
268
|
+
const client = this.getAttachedClient(request.payload.sessionId, request.payload.clientId);
|
|
269
|
+
if (client?.readOnly) {
|
|
270
|
+
return { success: false, error: `Client ${request.payload.clientId} is read-only` };
|
|
271
|
+
}
|
|
272
|
+
const session = this.registry.getSession(request.payload.sessionId);
|
|
273
|
+
if (session?.writeOwner && session.writeOwner.clientId !== request.payload.clientId) {
|
|
274
|
+
return { success: false, error: `Write owned by ${session.writeOwner.clientId}` };
|
|
275
|
+
}
|
|
276
|
+
this.requireRuntime(request.payload.sessionId).write(request.payload.data);
|
|
277
|
+
return { success: true, result: this.registry.getSession(request.payload.sessionId) };
|
|
278
|
+
}
|
|
279
|
+
case "resize_session": {
|
|
280
|
+
this.requireRuntime(request.payload.sessionId).resize(request.payload.cols, request.payload.rows);
|
|
281
|
+
const record = this.registry.getSession(request.payload.sessionId);
|
|
282
|
+
if (record) {
|
|
283
|
+
this.registry.restoreSession(
|
|
284
|
+
{
|
|
285
|
+
...record,
|
|
286
|
+
meta: {
|
|
287
|
+
...record.meta || {},
|
|
288
|
+
sessionHostCols: request.payload.cols,
|
|
289
|
+
sessionHostRows: request.payload.rows
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
this.registry.getSnapshot(request.payload.sessionId)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
this.schedulePersist(request.payload.sessionId);
|
|
296
|
+
this.emitEvent({
|
|
297
|
+
type: "session_resized",
|
|
298
|
+
sessionId: request.payload.sessionId,
|
|
299
|
+
cols: request.payload.cols,
|
|
300
|
+
rows: request.payload.rows
|
|
301
|
+
});
|
|
302
|
+
return { success: true, result: this.registry.getSession(request.payload.sessionId) };
|
|
303
|
+
}
|
|
304
|
+
case "stop_session": {
|
|
305
|
+
this.registry.setLifecycle(request.payload.sessionId, "stopping");
|
|
306
|
+
this.persistNow(request.payload.sessionId);
|
|
307
|
+
this.requireRuntime(request.payload.sessionId).stop();
|
|
308
|
+
this.emitEvent({ type: "session_stopped", sessionId: request.payload.sessionId });
|
|
309
|
+
return { success: true, result: this.registry.getSession(request.payload.sessionId) };
|
|
310
|
+
}
|
|
311
|
+
case "resume_session": {
|
|
312
|
+
const existing = this.registry.getSession(request.payload.sessionId);
|
|
313
|
+
if (!existing) {
|
|
314
|
+
return { success: false, error: `Unknown session: ${request.payload.sessionId}` };
|
|
315
|
+
}
|
|
316
|
+
if (this.runtimes.has(request.payload.sessionId)) {
|
|
317
|
+
return { success: true, result: existing };
|
|
318
|
+
}
|
|
319
|
+
const resumed = this.startRuntime(existing, this.buildPayloadFromRecord(existing), "session_resumed");
|
|
320
|
+
return { success: true, result: resumed };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return { success: false, error: error?.message || String(error) };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
requireRuntime(sessionId) {
|
|
328
|
+
const runtime = this.runtimes.get(sessionId);
|
|
329
|
+
if (!runtime) throw new Error(`Runtime not found for session: ${sessionId}`);
|
|
330
|
+
return runtime;
|
|
331
|
+
}
|
|
332
|
+
getAttachedClient(sessionId, clientId) {
|
|
333
|
+
const session = this.registry.getSession(sessionId);
|
|
334
|
+
return session?.attachedClients.find((client) => client.clientId === clientId) || null;
|
|
335
|
+
}
|
|
336
|
+
emitEvent(event) {
|
|
337
|
+
for (const socket of this.sockets) {
|
|
338
|
+
writeEnvelope(socket, {
|
|
339
|
+
kind: "event",
|
|
340
|
+
event
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
this.emit("event", event);
|
|
344
|
+
}
|
|
345
|
+
async handleIncomingRequest(socket, envelope) {
|
|
346
|
+
const response = await this.handleRequest(envelope.request);
|
|
347
|
+
writeEnvelope(socket, createResponseEnvelope(envelope.requestId, response));
|
|
348
|
+
}
|
|
349
|
+
schedulePersist(sessionId) {
|
|
350
|
+
const existing = this.persistTimers.get(sessionId);
|
|
351
|
+
if (existing) clearTimeout(existing);
|
|
352
|
+
this.persistTimers.set(sessionId, setTimeout(() => {
|
|
353
|
+
this.persistTimers.delete(sessionId);
|
|
354
|
+
this.persistNow(sessionId);
|
|
355
|
+
}, 200));
|
|
356
|
+
}
|
|
357
|
+
persistNow(sessionId) {
|
|
358
|
+
const record = this.registry.getSession(sessionId);
|
|
359
|
+
if (!record) return;
|
|
360
|
+
const snapshot = this.registry.getSnapshot(sessionId);
|
|
361
|
+
this.storage.save(record, snapshot);
|
|
362
|
+
}
|
|
363
|
+
flushAllPersistence() {
|
|
364
|
+
for (const sessionId of this.runtimes.keys()) {
|
|
365
|
+
this.persistNow(sessionId);
|
|
366
|
+
}
|
|
367
|
+
for (const record of this.registry.listSessions()) {
|
|
368
|
+
this.persistNow(record.sessionId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
restorePersistedRuntimes() {
|
|
372
|
+
const states = this.storage.loadAll();
|
|
373
|
+
for (const persisted of states) {
|
|
374
|
+
const wasLiveRuntime = !["stopped", "failed"].includes(persisted.record.lifecycle);
|
|
375
|
+
const recoveredRecord = {
|
|
376
|
+
...persisted.record,
|
|
377
|
+
attachedClients: [],
|
|
378
|
+
writeOwner: null,
|
|
379
|
+
lifecycle: wasLiveRuntime ? "interrupted" : persisted.record.lifecycle,
|
|
380
|
+
lastActivityAt: Date.now(),
|
|
381
|
+
meta: {
|
|
382
|
+
...persisted.record.meta || {},
|
|
383
|
+
restoredFromStorage: true,
|
|
384
|
+
runtimeRecoveryState: wasLiveRuntime ? "host_restart_interrupted" : "snapshot"
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
this.registry.restoreSession(recoveredRecord, persisted.snapshot);
|
|
388
|
+
this.storage.save(recoveredRecord, persisted.snapshot);
|
|
389
|
+
if (wasLiveRuntime) {
|
|
390
|
+
try {
|
|
391
|
+
const resumed = this.startRuntime(
|
|
392
|
+
recoveredRecord,
|
|
393
|
+
this.buildPayloadFromRecord(recoveredRecord),
|
|
394
|
+
"session_resumed"
|
|
395
|
+
);
|
|
396
|
+
const resumedMeta = {
|
|
397
|
+
...resumed.meta || {},
|
|
398
|
+
restoredFromStorage: true,
|
|
399
|
+
runtimeRecoveryState: "auto_resumed"
|
|
400
|
+
};
|
|
401
|
+
this.registry.restoreSession(
|
|
402
|
+
{ ...resumed, meta: resumedMeta },
|
|
403
|
+
this.registry.getSnapshot(resumed.sessionId)
|
|
404
|
+
);
|
|
405
|
+
this.persistNow(resumed.sessionId);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const interrupted = this.registry.setLifecycle(recoveredRecord.sessionId, "interrupted");
|
|
408
|
+
this.registry.restoreSession({
|
|
409
|
+
...interrupted,
|
|
410
|
+
meta: {
|
|
411
|
+
...interrupted.meta || {},
|
|
412
|
+
restoredFromStorage: true,
|
|
413
|
+
runtimeRecoveryState: "resume_failed",
|
|
414
|
+
runtimeRecoveryError: error?.message || String(error)
|
|
415
|
+
}
|
|
416
|
+
}, persisted.snapshot);
|
|
417
|
+
this.persistNow(recoveredRecord.sessionId);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
buildPayloadFromRecord(record) {
|
|
423
|
+
return {
|
|
424
|
+
sessionId: record.sessionId,
|
|
425
|
+
runtimeKey: record.runtimeKey,
|
|
426
|
+
displayName: record.displayName,
|
|
427
|
+
providerType: record.providerType,
|
|
428
|
+
category: record.category,
|
|
429
|
+
workspace: record.workspace,
|
|
430
|
+
launchCommand: record.launchCommand,
|
|
431
|
+
cols: typeof record.meta?.sessionHostCols === "number" ? record.meta.sessionHostCols : 120,
|
|
432
|
+
rows: typeof record.meta?.sessionHostRows === "number" ? record.meta.sessionHostRows : 40,
|
|
433
|
+
meta: record.meta
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
startRuntime(record, payload, startEventType) {
|
|
437
|
+
const runtime = new PtySessionRuntime({
|
|
438
|
+
sessionId: record.sessionId,
|
|
439
|
+
payload,
|
|
440
|
+
onData: (data) => {
|
|
441
|
+
const { seq } = this.registry.appendOutput(record.sessionId, data);
|
|
442
|
+
this.schedulePersist(record.sessionId);
|
|
443
|
+
this.emitEvent({ type: "session_output", sessionId: record.sessionId, seq, data });
|
|
444
|
+
},
|
|
445
|
+
onExit: (exitCode) => {
|
|
446
|
+
this.registry.markStopped(record.sessionId, exitCode === 0 ? "stopped" : "failed");
|
|
447
|
+
this.runtimes.delete(record.sessionId);
|
|
448
|
+
this.persistNow(record.sessionId);
|
|
449
|
+
this.emitEvent({ type: "session_exit", sessionId: record.sessionId, exitCode });
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
this.registry.setLifecycle(record.sessionId, "starting");
|
|
453
|
+
const pid = runtime.start();
|
|
454
|
+
this.runtimes.set(record.sessionId, runtime);
|
|
455
|
+
const startedRecord = this.registry.markStarted(record.sessionId, pid);
|
|
456
|
+
this.persistNow(record.sessionId);
|
|
457
|
+
this.emitEvent({ type: startEventType, sessionId: record.sessionId, pid });
|
|
458
|
+
return startedRecord;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// src/index.ts
|
|
463
|
+
var SESSION_HOST_APP_NAME = process.env.ADHDEV_SESSION_HOST_NAME || "adhdev";
|
|
464
|
+
function parseArgs(argv) {
|
|
465
|
+
const [command, ...rest] = argv;
|
|
466
|
+
const readOnly = rest.includes("--read-only");
|
|
467
|
+
const takeover = rest.includes("--takeover");
|
|
468
|
+
const showAll = rest.includes("--all");
|
|
469
|
+
const positional = rest.filter((arg) => arg !== "--read-only" && arg !== "--takeover" && arg !== "--all");
|
|
470
|
+
return {
|
|
471
|
+
command: command || "serve",
|
|
472
|
+
positional,
|
|
473
|
+
readOnly,
|
|
474
|
+
takeover,
|
|
475
|
+
showAll
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async function runServer() {
|
|
479
|
+
const server = new SessionHostServer({ appName: SESSION_HOST_APP_NAME });
|
|
480
|
+
await server.start();
|
|
481
|
+
process.on("SIGINT", async () => {
|
|
482
|
+
await server.stop();
|
|
483
|
+
process.exit(0);
|
|
484
|
+
});
|
|
485
|
+
process.on("SIGTERM", async () => {
|
|
486
|
+
await server.stop();
|
|
487
|
+
process.exit(0);
|
|
488
|
+
});
|
|
489
|
+
await new Promise(() => {
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async function listRuntimes(showAll = false) {
|
|
493
|
+
const client = new SessionHostClient({ endpoint: getDefaultSessionHostEndpoint2(SESSION_HOST_APP_NAME) });
|
|
494
|
+
try {
|
|
495
|
+
const response = await client.request({
|
|
496
|
+
type: "list_sessions",
|
|
497
|
+
payload: {}
|
|
498
|
+
});
|
|
499
|
+
if (!response.success) {
|
|
500
|
+
throw new Error(response.error || "Failed to list runtimes");
|
|
501
|
+
}
|
|
502
|
+
const runtimes = (response.result || []).filter((runtime) => showAll || runtime.lifecycle !== "stopped");
|
|
503
|
+
if (runtimes.length === 0) {
|
|
504
|
+
console.log("No runtimes.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log("runtimeKey lifecycle owner workspace id displayName");
|
|
508
|
+
for (const runtime of runtimes) {
|
|
509
|
+
console.log([
|
|
510
|
+
runtime.runtimeKey,
|
|
511
|
+
runtime.lifecycle,
|
|
512
|
+
formatRuntimeOwner(runtime),
|
|
513
|
+
runtime.workspaceLabel,
|
|
514
|
+
runtime.sessionId,
|
|
515
|
+
runtime.displayName
|
|
516
|
+
].join(" "));
|
|
517
|
+
}
|
|
518
|
+
} finally {
|
|
519
|
+
await client.close().catch(() => {
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
async function attachRuntime(target, readOnly = false, takeover = false) {
|
|
524
|
+
const client = new SessionHostClient({ endpoint: getDefaultSessionHostEndpoint2(SESSION_HOST_APP_NAME) });
|
|
525
|
+
const clientId = `local-terminal-${process.pid}-${randomUUID().slice(0, 8)}`;
|
|
526
|
+
let lastSeq = 0;
|
|
527
|
+
let restoredRawMode = false;
|
|
528
|
+
let runtimeId = "";
|
|
529
|
+
let localReadOnly = readOnly;
|
|
530
|
+
const cleanup = async () => {
|
|
531
|
+
process.stdout.off("resize", handleResize);
|
|
532
|
+
process.stdin.off("data", handleInput);
|
|
533
|
+
process.stdin.pause();
|
|
534
|
+
if (process.stdin.isTTY && restoredRawMode) {
|
|
535
|
+
process.stdin.setRawMode(false);
|
|
536
|
+
}
|
|
537
|
+
await client.request({
|
|
538
|
+
type: "release_write",
|
|
539
|
+
payload: {
|
|
540
|
+
sessionId: runtimeId,
|
|
541
|
+
clientId
|
|
542
|
+
}
|
|
543
|
+
}).catch(() => ({ success: false }));
|
|
544
|
+
await client.request({
|
|
545
|
+
type: "detach_session",
|
|
546
|
+
payload: {
|
|
547
|
+
sessionId: runtimeId,
|
|
548
|
+
clientId
|
|
549
|
+
}
|
|
550
|
+
}).catch(() => ({ success: false }));
|
|
551
|
+
await client.close().catch(() => {
|
|
552
|
+
});
|
|
553
|
+
};
|
|
554
|
+
const handleResize = () => {
|
|
555
|
+
void client.request({
|
|
556
|
+
type: "resize_session",
|
|
557
|
+
payload: {
|
|
558
|
+
sessionId: runtimeId,
|
|
559
|
+
cols: process.stdout.columns || 120,
|
|
560
|
+
rows: process.stdout.rows || 40
|
|
561
|
+
}
|
|
562
|
+
}).catch(() => ({ success: false }));
|
|
563
|
+
};
|
|
564
|
+
const sendInputWithTakeover = async (data) => {
|
|
565
|
+
let response = await client.request({
|
|
566
|
+
type: "send_input",
|
|
567
|
+
payload: {
|
|
568
|
+
sessionId: runtimeId,
|
|
569
|
+
clientId,
|
|
570
|
+
data
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
if (!response.success && response.error?.startsWith("Write owned by ")) {
|
|
574
|
+
const ownerResponse = await client.request({
|
|
575
|
+
type: "acquire_write",
|
|
576
|
+
payload: {
|
|
577
|
+
sessionId: runtimeId,
|
|
578
|
+
clientId,
|
|
579
|
+
ownerType: "user",
|
|
580
|
+
force: true
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
if (ownerResponse.success && ownerResponse.result) {
|
|
584
|
+
response = await client.request({
|
|
585
|
+
type: "send_input",
|
|
586
|
+
payload: {
|
|
587
|
+
sessionId: runtimeId,
|
|
588
|
+
clientId,
|
|
589
|
+
data
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
if (response.success) {
|
|
593
|
+
process.stderr.write(`Took control of ${ownerResponse.result.runtimeKey}.
|
|
594
|
+
`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return response;
|
|
599
|
+
};
|
|
600
|
+
const handleInput = (chunk) => {
|
|
601
|
+
if (!localReadOnly && chunk.length === 1 && chunk[0] === 29) {
|
|
602
|
+
void cleanup().finally(() => process.exit(0));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (localReadOnly) return;
|
|
606
|
+
void sendInputWithTakeover(chunk.toString("utf8")).catch(() => ({ success: false }));
|
|
607
|
+
};
|
|
608
|
+
try {
|
|
609
|
+
if (readOnly && takeover) {
|
|
610
|
+
throw new Error("Use either --read-only or --takeover, not both");
|
|
611
|
+
}
|
|
612
|
+
const listResponse = await client.request({
|
|
613
|
+
type: "list_sessions",
|
|
614
|
+
payload: {}
|
|
615
|
+
});
|
|
616
|
+
if (!listResponse.success || !listResponse.result) {
|
|
617
|
+
throw new Error(listResponse.error || "Failed to list runtimes");
|
|
618
|
+
}
|
|
619
|
+
let runtimeRecord = resolveRuntimeRecord(listResponse.result, target);
|
|
620
|
+
runtimeId = runtimeRecord.sessionId;
|
|
621
|
+
if (runtimeRecord.lifecycle === "interrupted" && !readOnly) {
|
|
622
|
+
const resumeResponse = await client.request({
|
|
623
|
+
type: "resume_session",
|
|
624
|
+
payload: {
|
|
625
|
+
sessionId: runtimeId
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
if (resumeResponse.success && resumeResponse.result) {
|
|
629
|
+
runtimeRecord = resumeResponse.result;
|
|
630
|
+
} else {
|
|
631
|
+
process.stderr.write(
|
|
632
|
+
`Runtime ${runtimeRecord.runtimeKey} could not be resumed automatically: ${resumeResponse.error || "unknown error"}
|
|
633
|
+
`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
let effectiveReadOnly = readOnly;
|
|
638
|
+
if (!effectiveReadOnly && runtimeRecord.writeOwner && runtimeRecord.writeOwner.clientId !== clientId && !takeover) {
|
|
639
|
+
process.stderr.write(
|
|
640
|
+
`Runtime ${runtimeRecord.runtimeKey} is currently owned by ${runtimeRecord.writeOwner.clientId}; first input will take control here.
|
|
641
|
+
`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
localReadOnly = effectiveReadOnly;
|
|
645
|
+
const attachResponse = await client.request({
|
|
646
|
+
type: "attach_session",
|
|
647
|
+
payload: {
|
|
648
|
+
sessionId: runtimeId,
|
|
649
|
+
clientId,
|
|
650
|
+
clientType: "local-terminal",
|
|
651
|
+
readOnly: effectiveReadOnly
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
if (!attachResponse.success) {
|
|
655
|
+
throw new Error(attachResponse.error || `Failed to attach runtime ${runtimeId}`);
|
|
656
|
+
}
|
|
657
|
+
const attachedRecord = attachResponse.result || null;
|
|
658
|
+
if (!effectiveReadOnly && takeover) {
|
|
659
|
+
const ownerResponse = await client.request({
|
|
660
|
+
type: "acquire_write",
|
|
661
|
+
payload: {
|
|
662
|
+
sessionId: runtimeId,
|
|
663
|
+
clientId,
|
|
664
|
+
ownerType: "user",
|
|
665
|
+
force: takeover
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
if (!ownerResponse.success) {
|
|
669
|
+
throw new Error(ownerResponse.error || `Failed to acquire write owner for runtime ${runtimeId}`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const snapshotResponse = await client.request({
|
|
673
|
+
type: "get_snapshot",
|
|
674
|
+
payload: { sessionId: runtimeId }
|
|
675
|
+
});
|
|
676
|
+
if (!snapshotResponse.success) {
|
|
677
|
+
throw new Error(snapshotResponse.error || `Failed to read runtime snapshot ${runtimeId}`);
|
|
678
|
+
}
|
|
679
|
+
lastSeq = snapshotResponse.result?.seq || 0;
|
|
680
|
+
if (snapshotResponse.result?.text) {
|
|
681
|
+
process.stdout.write(snapshotResponse.result.text);
|
|
682
|
+
}
|
|
683
|
+
if (attachedRecord?.lifecycle === "stopped" || attachedRecord?.lifecycle === "failed" || attachedRecord?.lifecycle === "interrupted") {
|
|
684
|
+
process.stderr.write(`Runtime ${attachedRecord.runtimeKey} is already ${attachedRecord.lifecycle}. Detached after snapshot.
|
|
685
|
+
`);
|
|
686
|
+
await cleanup();
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const stopSignals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
690
|
+
const signalHandlers = stopSignals.map((signal) => {
|
|
691
|
+
const handler = () => {
|
|
692
|
+
void cleanup().finally(() => process.exit(0));
|
|
693
|
+
};
|
|
694
|
+
process.on(signal, handler);
|
|
695
|
+
return { signal, handler };
|
|
696
|
+
});
|
|
697
|
+
const unsubscribe = client.onEvent((event) => {
|
|
698
|
+
if (event.sessionId !== runtimeId) return;
|
|
699
|
+
if (event.type === "session_output") {
|
|
700
|
+
if (event.seq <= lastSeq) return;
|
|
701
|
+
lastSeq = event.seq;
|
|
702
|
+
process.stdout.write(event.data);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (event.type === "session_exit") {
|
|
706
|
+
void cleanup().finally(() => {
|
|
707
|
+
for (const { signal, handler } of signalHandlers) {
|
|
708
|
+
process.off(signal, handler);
|
|
709
|
+
}
|
|
710
|
+
unsubscribe();
|
|
711
|
+
process.exit(event.exitCode ?? 0);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
process.stdout.on("resize", handleResize);
|
|
716
|
+
process.stdin.on("data", handleInput);
|
|
717
|
+
process.stdin.resume();
|
|
718
|
+
if (process.stdin.isTTY) {
|
|
719
|
+
process.stdin.setRawMode(true);
|
|
720
|
+
restoredRawMode = true;
|
|
721
|
+
}
|
|
722
|
+
handleResize();
|
|
723
|
+
if (!effectiveReadOnly) {
|
|
724
|
+
process.stderr.write(`Attached to runtime ${attachedRecord?.runtimeKey || runtimeId}. Press Ctrl+] to detach.
|
|
725
|
+
`);
|
|
726
|
+
} else {
|
|
727
|
+
process.stderr.write(`Attached to runtime ${attachedRecord?.runtimeKey || runtimeId} (read-only).
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
await new Promise(() => {
|
|
731
|
+
});
|
|
732
|
+
} catch (error) {
|
|
733
|
+
await cleanup().catch(() => {
|
|
734
|
+
});
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async function main() {
|
|
739
|
+
const { command, positional, readOnly, takeover, showAll } = parseArgs(process.argv.slice(2));
|
|
740
|
+
if (command === "serve") {
|
|
741
|
+
await runServer();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (command === "list") {
|
|
745
|
+
await listRuntimes(showAll);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (command === "attach") {
|
|
749
|
+
const target = positional[0];
|
|
750
|
+
if (!target) {
|
|
751
|
+
throw new Error("runtime target is required: adhdev-sessiond attach <runtimeId|runtimeKey>");
|
|
752
|
+
}
|
|
753
|
+
await attachRuntime(target, readOnly, takeover);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (command === "resume") {
|
|
757
|
+
const target = positional[0];
|
|
758
|
+
if (!target) {
|
|
759
|
+
throw new Error("runtime target is required: adhdev-sessiond resume <runtimeId|runtimeKey>");
|
|
760
|
+
}
|
|
761
|
+
const client = new SessionHostClient({ endpoint: getDefaultSessionHostEndpoint2(SESSION_HOST_APP_NAME) });
|
|
762
|
+
try {
|
|
763
|
+
const listResponse = await client.request({ type: "list_sessions", payload: {} });
|
|
764
|
+
if (!listResponse.success || !listResponse.result) {
|
|
765
|
+
throw new Error(listResponse.error || "Failed to list runtimes");
|
|
766
|
+
}
|
|
767
|
+
const runtimeRecord = resolveRuntimeRecord(listResponse.result, target);
|
|
768
|
+
const resumeResponse = await client.request({
|
|
769
|
+
type: "resume_session",
|
|
770
|
+
payload: {
|
|
771
|
+
sessionId: runtimeRecord.sessionId
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
if (!resumeResponse.success || !resumeResponse.result) {
|
|
775
|
+
throw new Error(resumeResponse.error || `Failed to resume runtime ${runtimeRecord.runtimeKey}`);
|
|
776
|
+
}
|
|
777
|
+
console.log(`Resumed ${resumeResponse.result.runtimeKey} (${resumeResponse.result.sessionId})`);
|
|
778
|
+
} finally {
|
|
779
|
+
await client.close().catch(() => {
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
throw new Error(`Unknown command: ${command}`);
|
|
785
|
+
}
|
|
786
|
+
if (__require.main === module) {
|
|
787
|
+
void main().catch((error) => {
|
|
788
|
+
console.error(error instanceof Error ? error.message : error);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
export {
|
|
793
|
+
SessionHostServer
|
|
794
|
+
};
|
|
795
|
+
//# sourceMappingURL=index.mjs.map
|