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