linkshell-cli 0.2.34 → 0.2.36
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/src/runtime/bridge-session.d.ts +5 -0
- package/dist/cli/src/runtime/bridge-session.js +99 -87
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +63 -146
- package/dist/shared-protocol/src/index.js +17 -19
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +3 -3
- package/src/runtime/bridge-session.ts +97 -92
- package/src/runtime/acp-relay.ts +0 -92
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import type { Envelope } from "@linkshell/protocol";
|
|
16
16
|
import type { ProviderConfig } from "../providers.js";
|
|
17
17
|
import { ScrollbackBuffer } from "./scrollback.js";
|
|
18
|
-
import { AcpRelay } from "./acp-relay.js";
|
|
19
18
|
import { ScreenFallback } from "./screen-fallback.js";
|
|
20
19
|
import { ScreenShare } from "./screen-share.js";
|
|
21
20
|
import { getLanIp } from "../utils/lan-ip.js";
|
|
@@ -42,9 +41,7 @@ const DEFAULT_TERMINAL_ID = "default";
|
|
|
42
41
|
|
|
43
42
|
interface TerminalInstance {
|
|
44
43
|
id: string;
|
|
45
|
-
pty: pty.IPty
|
|
46
|
-
acpRelay: AcpRelay | null;
|
|
47
|
-
mode: "pty" | "acp";
|
|
44
|
+
pty: pty.IPty;
|
|
48
45
|
cwd: string;
|
|
49
46
|
projectName: string;
|
|
50
47
|
provider: string;
|
|
@@ -131,6 +128,8 @@ export class BridgeSession {
|
|
|
131
128
|
permissionRequest: string;
|
|
132
129
|
timestamp: number;
|
|
133
130
|
}>>();
|
|
131
|
+
// Pending permission responses: requestId → HTTP response callback
|
|
132
|
+
private pendingPermissions = new Map<string, (decision: "allow" | "deny") => void>();
|
|
134
133
|
private screenCapture: ScreenFallback | undefined;
|
|
135
134
|
private screenShare: ScreenShare | undefined;
|
|
136
135
|
|
|
@@ -284,29 +283,21 @@ export class BridgeSession {
|
|
|
284
283
|
case "terminal.input": {
|
|
285
284
|
const p = parseTypedPayload("terminal.input", envelope.payload);
|
|
286
285
|
const term = this.terminals.get(tid);
|
|
287
|
-
if (term && term.status === "running"
|
|
286
|
+
if (term && term.status === "running") term.pty.write(p.data);
|
|
288
287
|
break;
|
|
289
288
|
}
|
|
290
289
|
case "terminal.resize": {
|
|
291
290
|
const p = parseTypedPayload("terminal.resize", envelope.payload);
|
|
292
291
|
const term = this.terminals.get(tid);
|
|
293
|
-
if (term && term.status === "running"
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
case "acp.rpc": {
|
|
297
|
-
const term = this.terminals.get(tid);
|
|
298
|
-
if (term && term.status === "running" && term.acpRelay) {
|
|
299
|
-
term.acpRelay.write(envelope.payload);
|
|
300
|
-
}
|
|
292
|
+
if (term && term.status === "running") term.pty.resize(p.cols, p.rows);
|
|
301
293
|
break;
|
|
302
294
|
}
|
|
303
295
|
case "terminal.spawn": {
|
|
304
296
|
const p = parseTypedPayload("terminal.spawn", envelope.payload);
|
|
305
297
|
const normalizedCwd = resolve(p.cwd);
|
|
306
|
-
|
|
307
|
-
// Dedup: if a running terminal already exists for this cwd and mode, return it
|
|
298
|
+
// Dedup: if a running terminal already exists for this cwd, return it
|
|
308
299
|
const existing = [...this.terminals.values()].find(
|
|
309
|
-
(t) => t.status === "running" && resolve(t.cwd) === normalizedCwd
|
|
300
|
+
(t) => t.status === "running" && resolve(t.cwd) === normalizedCwd,
|
|
310
301
|
);
|
|
311
302
|
if (existing) {
|
|
312
303
|
this.send(createEnvelope({
|
|
@@ -318,7 +309,7 @@ export class BridgeSession {
|
|
|
318
309
|
} else {
|
|
319
310
|
const newId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
320
311
|
try {
|
|
321
|
-
await this.spawnTerminal(newId, normalizedCwd, p.provider
|
|
312
|
+
await this.spawnTerminal(newId, normalizedCwd, p.provider);
|
|
322
313
|
this.send(createEnvelope({
|
|
323
314
|
type: "terminal.spawned",
|
|
324
315
|
sessionId: this.sessionId,
|
|
@@ -342,8 +333,7 @@ export class BridgeSession {
|
|
|
342
333
|
const p = parseTypedPayload("terminal.kill", envelope.payload);
|
|
343
334
|
const term = this.terminals.get(p.terminalId);
|
|
344
335
|
if (term && term.status === "running") {
|
|
345
|
-
|
|
346
|
-
if (term.acpRelay) term.acpRelay.kill();
|
|
336
|
+
term.pty.kill();
|
|
347
337
|
}
|
|
348
338
|
break;
|
|
349
339
|
}
|
|
@@ -425,11 +415,32 @@ export class BridgeSession {
|
|
|
425
415
|
writeFileSync(tempPath, Buffer.from(p.data, "base64"));
|
|
426
416
|
this.log(`image saved to ${tempPath}`);
|
|
427
417
|
const term = this.terminals.get(tid);
|
|
428
|
-
if (term && term.status === "running"
|
|
418
|
+
if (term && term.status === "running") {
|
|
429
419
|
term.pty.write(`\x1b[200~${tempPath}\x1b[201~`);
|
|
430
420
|
}
|
|
431
421
|
break;
|
|
432
422
|
}
|
|
423
|
+
case "permission.decision": {
|
|
424
|
+
const p = envelope.payload as { requestId: string; decision: "allow" | "deny" };
|
|
425
|
+
const resolve = this.pendingPermissions.get(p.requestId);
|
|
426
|
+
if (resolve) {
|
|
427
|
+
this.pendingPermissions.delete(p.requestId);
|
|
428
|
+
resolve(p.decision);
|
|
429
|
+
this.log(`permission decision for ${p.requestId}: ${p.decision}`);
|
|
430
|
+
// Pop from permission stack
|
|
431
|
+
if (p.decision === "allow" || p.decision === "deny") {
|
|
432
|
+
const stack = this.permissionStacks.get(tid);
|
|
433
|
+
if (stack) {
|
|
434
|
+
const idx = stack.findIndex((s) => s.requestId === p.requestId);
|
|
435
|
+
if (idx >= 0) stack.splice(idx, 1);
|
|
436
|
+
if (stack.length === 0) this.permissionStacks.delete(tid);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
this.log(`no pending permission for ${p.requestId}`);
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
433
444
|
default:
|
|
434
445
|
break;
|
|
435
446
|
}
|
|
@@ -442,7 +453,6 @@ export class BridgeSession {
|
|
|
442
453
|
projectName: t.projectName,
|
|
443
454
|
provider: t.provider,
|
|
444
455
|
status: t.status,
|
|
445
|
-
mode: t.mode,
|
|
446
456
|
}));
|
|
447
457
|
this.send(createEnvelope({
|
|
448
458
|
type: "terminal.list",
|
|
@@ -473,7 +483,7 @@ export class BridgeSession {
|
|
|
473
483
|
}
|
|
474
484
|
}
|
|
475
485
|
|
|
476
|
-
private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string
|
|
486
|
+
private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string): Promise<void> {
|
|
477
487
|
const cleanEnv: Record<string, string> = {};
|
|
478
488
|
for (const [k, v] of Object.entries(this.options.providerConfig.env)) {
|
|
479
489
|
if (v !== undefined) cleanEnv[k] = v;
|
|
@@ -482,58 +492,6 @@ export class BridgeSession {
|
|
|
482
492
|
const provider = providerOverride ?? this.options.providerConfig.provider;
|
|
483
493
|
const args = [...this.options.providerConfig.args];
|
|
484
494
|
|
|
485
|
-
// ACP mode: spawn as child process with JSON-RPC over stdio
|
|
486
|
-
if (mode === "acp") {
|
|
487
|
-
const term: TerminalInstance = {
|
|
488
|
-
id: terminalId,
|
|
489
|
-
pty: null,
|
|
490
|
-
acpRelay: new AcpRelay({
|
|
491
|
-
command: this.options.providerConfig.command,
|
|
492
|
-
args,
|
|
493
|
-
cwd,
|
|
494
|
-
env: cleanEnv,
|
|
495
|
-
sessionId: this.sessionId,
|
|
496
|
-
terminalId,
|
|
497
|
-
send: (envelope) => this.send(envelope),
|
|
498
|
-
log: (msg) => this.log(msg),
|
|
499
|
-
}),
|
|
500
|
-
mode: "acp",
|
|
501
|
-
cwd,
|
|
502
|
-
projectName: basename(cwd),
|
|
503
|
-
provider,
|
|
504
|
-
scrollback: new ScrollbackBuffer(1000),
|
|
505
|
-
outputSeq: 0,
|
|
506
|
-
statusSeq: 0,
|
|
507
|
-
status: "running",
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
term.acpRelay!.onExit((exitCode, signal) => {
|
|
511
|
-
term.status = "exited";
|
|
512
|
-
this.send(createEnvelope({
|
|
513
|
-
type: "terminal.exit",
|
|
514
|
-
sessionId: this.sessionId,
|
|
515
|
-
terminalId,
|
|
516
|
-
payload: { exitCode, signal: signal ? parseInt(signal, 10) || 0 : 0 },
|
|
517
|
-
}));
|
|
518
|
-
this.sendTerminalList();
|
|
519
|
-
|
|
520
|
-
const allExited = [...this.terminals.values()].every((t) => t.status === "exited");
|
|
521
|
-
if (allExited) {
|
|
522
|
-
this.exited = true;
|
|
523
|
-
setTimeout(() => {
|
|
524
|
-
this.stopHeartbeat();
|
|
525
|
-
this.socket?.close();
|
|
526
|
-
}, 500);
|
|
527
|
-
process.exitCode = exitCode ?? 0;
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
this.terminals.set(terminalId, term);
|
|
532
|
-
this.log(`spawned ACP terminal ${terminalId} in ${cwd}`);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// PTY mode: existing behavior
|
|
537
495
|
// Set up hook server for structured status (all supported providers)
|
|
538
496
|
// For "custom" shell, set up hooks for all providers since user may launch any of them
|
|
539
497
|
let hookServer: http.Server | undefined;
|
|
@@ -570,8 +528,6 @@ export class BridgeSession {
|
|
|
570
528
|
env: cleanEnv,
|
|
571
529
|
},
|
|
572
530
|
),
|
|
573
|
-
acpRelay: null,
|
|
574
|
-
mode: "pty",
|
|
575
531
|
cwd,
|
|
576
532
|
projectName: basename(cwd),
|
|
577
533
|
provider,
|
|
@@ -584,7 +540,7 @@ export class BridgeSession {
|
|
|
584
540
|
hookConfigPath,
|
|
585
541
|
};
|
|
586
542
|
|
|
587
|
-
term.pty
|
|
543
|
+
term.pty.onData((data) => {
|
|
588
544
|
const seq = term.outputSeq++;
|
|
589
545
|
const envelope = createEnvelope({
|
|
590
546
|
type: "terminal.output",
|
|
@@ -603,7 +559,7 @@ export class BridgeSession {
|
|
|
603
559
|
this.send(envelope);
|
|
604
560
|
});
|
|
605
561
|
|
|
606
|
-
term.pty
|
|
562
|
+
term.pty.onExit(({ exitCode, signal }) => {
|
|
607
563
|
term.status = "exited";
|
|
608
564
|
this.cleanupHookServer(term);
|
|
609
565
|
this.send(createEnvelope({
|
|
@@ -645,13 +601,35 @@ export class BridgeSession {
|
|
|
645
601
|
let body = "";
|
|
646
602
|
req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
|
|
647
603
|
req.on("end", () => {
|
|
648
|
-
res.writeHead(200);
|
|
649
|
-
res.end("ok");
|
|
650
604
|
this.log(`hook body (${body.length} bytes): ${body.slice(0, 200)}`);
|
|
651
605
|
try {
|
|
652
606
|
const event = JSON.parse(body);
|
|
653
|
-
|
|
607
|
+
const hookName = (event.hook_event_name ?? event.event_name) as string | undefined;
|
|
608
|
+
|
|
609
|
+
// PermissionRequest: hold connection, wait for user decision from mobile app
|
|
610
|
+
if (hookName === "PermissionRequest") {
|
|
611
|
+
const requestId = `pr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
612
|
+
this.pendingPermissions.set(requestId, (decision) => {
|
|
613
|
+
const responseJson = JSON.stringify({
|
|
614
|
+
hookSpecificOutput: {
|
|
615
|
+
hookEventName: "PermissionRequest",
|
|
616
|
+
decision: { behavior: decision },
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
620
|
+
res.end(responseJson);
|
|
621
|
+
});
|
|
622
|
+
// Send status with requestId so app can route decision back
|
|
623
|
+
this.handleHookEvent(terminalId, event, provider, requestId);
|
|
624
|
+
} else {
|
|
625
|
+
// All other hooks: respond immediately
|
|
626
|
+
res.writeHead(200);
|
|
627
|
+
res.end("ok");
|
|
628
|
+
this.handleHookEvent(terminalId, event, provider);
|
|
629
|
+
}
|
|
654
630
|
} catch (e) {
|
|
631
|
+
res.writeHead(200);
|
|
632
|
+
res.end("ok");
|
|
655
633
|
this.log(`hook parse error: ${e}`);
|
|
656
634
|
}
|
|
657
635
|
});
|
|
@@ -822,7 +800,7 @@ export class BridgeSession {
|
|
|
822
800
|
return hooksPath;
|
|
823
801
|
}
|
|
824
802
|
|
|
825
|
-
private handleHookEvent(terminalId: string, event: Record<string, unknown>, provider: string): void {
|
|
803
|
+
private handleHookEvent(terminalId: string, event: Record<string, unknown>, provider: string, permissionRequestId?: string): void {
|
|
826
804
|
const rawHookName = (event.hook_event_name ?? event.event_name) as string | undefined;
|
|
827
805
|
if (!rawHookName) return;
|
|
828
806
|
|
|
@@ -878,11 +856,12 @@ export class BridgeSession {
|
|
|
878
856
|
case "PostToolUse":
|
|
879
857
|
phase = "thinking";
|
|
880
858
|
toolName = (event.tool_name ?? event.toolName) as string | undefined;
|
|
881
|
-
// Pop permission stack
|
|
859
|
+
// Pop permission stack + auto-resolve pending HTTP connection
|
|
882
860
|
{
|
|
883
861
|
const stack = this.permissionStacks.get(terminalId);
|
|
884
862
|
if (stack && stack.length > 0) {
|
|
885
|
-
stack.pop();
|
|
863
|
+
const popped = stack.pop();
|
|
864
|
+
if (popped) this.autoResolvePending(popped.requestId);
|
|
886
865
|
if (stack.length === 0) this.permissionStacks.delete(terminalId);
|
|
887
866
|
}
|
|
888
867
|
}
|
|
@@ -893,7 +872,8 @@ export class BridgeSession {
|
|
|
893
872
|
{
|
|
894
873
|
const stack = this.permissionStacks.get(terminalId);
|
|
895
874
|
if (stack && stack.length > 0) {
|
|
896
|
-
stack.pop();
|
|
875
|
+
const popped = stack.pop();
|
|
876
|
+
if (popped) this.autoResolvePending(popped.requestId);
|
|
897
877
|
if (stack.length === 0) this.permissionStacks.delete(terminalId);
|
|
898
878
|
}
|
|
899
879
|
}
|
|
@@ -901,6 +881,7 @@ export class BridgeSession {
|
|
|
901
881
|
case "Stop":
|
|
902
882
|
phase = "idle";
|
|
903
883
|
if (event.stop_reason) summary = String(event.stop_reason);
|
|
884
|
+
this.drainPendingPermissions(terminalId);
|
|
904
885
|
this.permissionStacks.delete(terminalId);
|
|
905
886
|
break;
|
|
906
887
|
case "PermissionRequest":
|
|
@@ -912,14 +893,14 @@ export class BridgeSession {
|
|
|
912
893
|
} else if (event.toolInput && typeof event.toolInput === "object") {
|
|
913
894
|
permissionRequest = JSON.stringify(event.toolInput).slice(0, 300);
|
|
914
895
|
}
|
|
915
|
-
// Push to permission stack
|
|
896
|
+
// Push to permission stack (use requestId from hook server if available)
|
|
916
897
|
{
|
|
917
|
-
const
|
|
898
|
+
const reqId = permissionRequestId ?? `pr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
918
899
|
if (!this.permissionStacks.has(terminalId)) {
|
|
919
900
|
this.permissionStacks.set(terminalId, []);
|
|
920
901
|
}
|
|
921
902
|
this.permissionStacks.get(terminalId)!.push({
|
|
922
|
-
requestId,
|
|
903
|
+
requestId: reqId,
|
|
923
904
|
toolName: toolName ?? "unknown",
|
|
924
905
|
toolInput: toolInput ?? (permissionRequest ?? ""),
|
|
925
906
|
permissionRequest: permissionRequest ?? "",
|
|
@@ -933,6 +914,7 @@ export class BridgeSession {
|
|
|
933
914
|
break;
|
|
934
915
|
case "UserPromptSubmit":
|
|
935
916
|
phase = "thinking";
|
|
917
|
+
this.drainPendingPermissions(terminalId);
|
|
936
918
|
this.permissionStacks.delete(terminalId);
|
|
937
919
|
break;
|
|
938
920
|
default:
|
|
@@ -1023,7 +1005,33 @@ export class BridgeSession {
|
|
|
1023
1005
|
return undefined;
|
|
1024
1006
|
}
|
|
1025
1007
|
|
|
1008
|
+
/** Auto-resolve a single pending permission (user acted in terminal) */
|
|
1009
|
+
private autoResolvePending(requestId: string): void {
|
|
1010
|
+
const resolve = this.pendingPermissions.get(requestId);
|
|
1011
|
+
if (resolve) {
|
|
1012
|
+
this.pendingPermissions.delete(requestId);
|
|
1013
|
+
resolve("allow");
|
|
1014
|
+
this.log(`auto-resolved pending permission ${requestId} (user acted in terminal)`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Drain all pending permissions for a terminal (session ended, stop, etc.) */
|
|
1019
|
+
private drainPendingPermissions(terminalId: string): void {
|
|
1020
|
+
const stack = this.permissionStacks.get(terminalId);
|
|
1021
|
+
if (!stack) return;
|
|
1022
|
+
for (const entry of stack) {
|
|
1023
|
+
const resolve = this.pendingPermissions.get(entry.requestId);
|
|
1024
|
+
if (resolve) {
|
|
1025
|
+
this.pendingPermissions.delete(entry.requestId);
|
|
1026
|
+
resolve("deny");
|
|
1027
|
+
this.log(`drained pending permission ${entry.requestId}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1026
1032
|
private cleanupHookServer(term: TerminalInstance): void {
|
|
1033
|
+
// Drain any pending permission requests for this terminal
|
|
1034
|
+
this.drainPendingPermissions(term.id);
|
|
1027
1035
|
if (term.hookServer) {
|
|
1028
1036
|
term.hookServer.close();
|
|
1029
1037
|
term.hookServer = undefined;
|
|
@@ -1191,10 +1199,7 @@ export class BridgeSession {
|
|
|
1191
1199
|
this.socket = undefined;
|
|
1192
1200
|
for (const term of this.terminals.values()) {
|
|
1193
1201
|
this.cleanupHookServer(term);
|
|
1194
|
-
if (term.status === "running")
|
|
1195
|
-
if (term.pty) term.pty.kill();
|
|
1196
|
-
if (term.acpRelay) term.acpRelay.kill();
|
|
1197
|
-
}
|
|
1202
|
+
if (term.status === "running") term.pty.kill();
|
|
1198
1203
|
}
|
|
1199
1204
|
this.terminals.clear();
|
|
1200
1205
|
process.exitCode = exitCode;
|
package/src/runtime/acp-relay.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
|
-
import { createEnvelope, serializeEnvelope } from "@linkshell/protocol";
|
|
4
|
-
import type { Envelope } from "@linkshell/protocol";
|
|
5
|
-
|
|
6
|
-
export interface AcpRelayOptions {
|
|
7
|
-
command: string;
|
|
8
|
-
args: string[];
|
|
9
|
-
cwd: string;
|
|
10
|
-
env: Record<string, string>;
|
|
11
|
-
sessionId: string;
|
|
12
|
-
terminalId: string;
|
|
13
|
-
send: (envelope: Envelope) => void;
|
|
14
|
-
log: (msg: string) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class AcpRelay {
|
|
18
|
-
private child: ChildProcess;
|
|
19
|
-
private readonly options: AcpRelayOptions;
|
|
20
|
-
private exited = false;
|
|
21
|
-
|
|
22
|
-
constructor(options: AcpRelayOptions) {
|
|
23
|
-
this.options = options;
|
|
24
|
-
|
|
25
|
-
this.child = spawn(options.command, options.args, {
|
|
26
|
-
cwd: options.cwd,
|
|
27
|
-
env: options.env,
|
|
28
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// Read newline-delimited JSON from stdout
|
|
32
|
-
const rl = createInterface({ input: this.child.stdout! });
|
|
33
|
-
rl.on("line", (line) => {
|
|
34
|
-
if (!line.trim()) return;
|
|
35
|
-
try {
|
|
36
|
-
const msg = JSON.parse(line);
|
|
37
|
-
// Wrap JSON-RPC message in acp.rpc envelope and send to gateway
|
|
38
|
-
this.options.send(createEnvelope({
|
|
39
|
-
type: "acp.rpc",
|
|
40
|
-
sessionId: this.options.sessionId,
|
|
41
|
-
terminalId: this.options.terminalId,
|
|
42
|
-
payload: msg,
|
|
43
|
-
}));
|
|
44
|
-
} catch (e) {
|
|
45
|
-
this.options.log(`acp-relay: failed to parse stdout line: ${e}`);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Log stderr
|
|
50
|
-
this.child.stderr?.on("data", (chunk: Buffer) => {
|
|
51
|
-
this.options.log(`acp-relay stderr: ${chunk.toString().trimEnd()}`);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
this.child.on("exit", (code, signal) => {
|
|
55
|
-
this.exited = true;
|
|
56
|
-
this.options.log(`acp-relay: process exited (code=${code}, signal=${signal})`);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
this.child.on("error", (err) => {
|
|
60
|
-
this.options.log(`acp-relay: process error: ${err.message}`);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Forward an acp.rpc envelope payload to the child's stdin */
|
|
65
|
-
write(payload: unknown): void {
|
|
66
|
-
if (this.exited || !this.child.stdin?.writable) return;
|
|
67
|
-
try {
|
|
68
|
-
const line = JSON.stringify(payload) + "\n";
|
|
69
|
-
this.child.stdin.write(line);
|
|
70
|
-
} catch (e) {
|
|
71
|
-
this.options.log(`acp-relay: failed to write to stdin: ${e}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
kill(): void {
|
|
76
|
-
if (!this.exited) {
|
|
77
|
-
this.child.kill("SIGTERM");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
get isExited(): boolean {
|
|
82
|
-
return this.exited;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
get pid(): number | undefined {
|
|
86
|
-
return this.child.pid;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
onExit(cb: (code: number | null, signal: string | null) => void): void {
|
|
90
|
-
this.child.on("exit", cb);
|
|
91
|
-
}
|
|
92
|
-
}
|