linkshell-cli 0.2.34 → 0.2.35
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.js +10 -77
- 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 +46 -149
- package/dist/shared-protocol/src/index.js +0 -16
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/bridge-session.ts +12 -82
- 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;
|
|
@@ -284,29 +281,21 @@ export class BridgeSession {
|
|
|
284
281
|
case "terminal.input": {
|
|
285
282
|
const p = parseTypedPayload("terminal.input", envelope.payload);
|
|
286
283
|
const term = this.terminals.get(tid);
|
|
287
|
-
if (term && term.status === "running"
|
|
284
|
+
if (term && term.status === "running") term.pty.write(p.data);
|
|
288
285
|
break;
|
|
289
286
|
}
|
|
290
287
|
case "terminal.resize": {
|
|
291
288
|
const p = parseTypedPayload("terminal.resize", envelope.payload);
|
|
292
289
|
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
|
-
}
|
|
290
|
+
if (term && term.status === "running") term.pty.resize(p.cols, p.rows);
|
|
301
291
|
break;
|
|
302
292
|
}
|
|
303
293
|
case "terminal.spawn": {
|
|
304
294
|
const p = parseTypedPayload("terminal.spawn", envelope.payload);
|
|
305
295
|
const normalizedCwd = resolve(p.cwd);
|
|
306
|
-
|
|
307
|
-
// Dedup: if a running terminal already exists for this cwd and mode, return it
|
|
296
|
+
// Dedup: if a running terminal already exists for this cwd, return it
|
|
308
297
|
const existing = [...this.terminals.values()].find(
|
|
309
|
-
(t) => t.status === "running" && resolve(t.cwd) === normalizedCwd
|
|
298
|
+
(t) => t.status === "running" && resolve(t.cwd) === normalizedCwd,
|
|
310
299
|
);
|
|
311
300
|
if (existing) {
|
|
312
301
|
this.send(createEnvelope({
|
|
@@ -318,7 +307,7 @@ export class BridgeSession {
|
|
|
318
307
|
} else {
|
|
319
308
|
const newId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
320
309
|
try {
|
|
321
|
-
await this.spawnTerminal(newId, normalizedCwd, p.provider
|
|
310
|
+
await this.spawnTerminal(newId, normalizedCwd, p.provider);
|
|
322
311
|
this.send(createEnvelope({
|
|
323
312
|
type: "terminal.spawned",
|
|
324
313
|
sessionId: this.sessionId,
|
|
@@ -342,8 +331,7 @@ export class BridgeSession {
|
|
|
342
331
|
const p = parseTypedPayload("terminal.kill", envelope.payload);
|
|
343
332
|
const term = this.terminals.get(p.terminalId);
|
|
344
333
|
if (term && term.status === "running") {
|
|
345
|
-
|
|
346
|
-
if (term.acpRelay) term.acpRelay.kill();
|
|
334
|
+
term.pty.kill();
|
|
347
335
|
}
|
|
348
336
|
break;
|
|
349
337
|
}
|
|
@@ -425,7 +413,7 @@ export class BridgeSession {
|
|
|
425
413
|
writeFileSync(tempPath, Buffer.from(p.data, "base64"));
|
|
426
414
|
this.log(`image saved to ${tempPath}`);
|
|
427
415
|
const term = this.terminals.get(tid);
|
|
428
|
-
if (term && term.status === "running"
|
|
416
|
+
if (term && term.status === "running") {
|
|
429
417
|
term.pty.write(`\x1b[200~${tempPath}\x1b[201~`);
|
|
430
418
|
}
|
|
431
419
|
break;
|
|
@@ -442,7 +430,6 @@ export class BridgeSession {
|
|
|
442
430
|
projectName: t.projectName,
|
|
443
431
|
provider: t.provider,
|
|
444
432
|
status: t.status,
|
|
445
|
-
mode: t.mode,
|
|
446
433
|
}));
|
|
447
434
|
this.send(createEnvelope({
|
|
448
435
|
type: "terminal.list",
|
|
@@ -473,7 +460,7 @@ export class BridgeSession {
|
|
|
473
460
|
}
|
|
474
461
|
}
|
|
475
462
|
|
|
476
|
-
private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string
|
|
463
|
+
private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string): Promise<void> {
|
|
477
464
|
const cleanEnv: Record<string, string> = {};
|
|
478
465
|
for (const [k, v] of Object.entries(this.options.providerConfig.env)) {
|
|
479
466
|
if (v !== undefined) cleanEnv[k] = v;
|
|
@@ -482,58 +469,6 @@ export class BridgeSession {
|
|
|
482
469
|
const provider = providerOverride ?? this.options.providerConfig.provider;
|
|
483
470
|
const args = [...this.options.providerConfig.args];
|
|
484
471
|
|
|
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
472
|
// Set up hook server for structured status (all supported providers)
|
|
538
473
|
// For "custom" shell, set up hooks for all providers since user may launch any of them
|
|
539
474
|
let hookServer: http.Server | undefined;
|
|
@@ -570,8 +505,6 @@ export class BridgeSession {
|
|
|
570
505
|
env: cleanEnv,
|
|
571
506
|
},
|
|
572
507
|
),
|
|
573
|
-
acpRelay: null,
|
|
574
|
-
mode: "pty",
|
|
575
508
|
cwd,
|
|
576
509
|
projectName: basename(cwd),
|
|
577
510
|
provider,
|
|
@@ -584,7 +517,7 @@ export class BridgeSession {
|
|
|
584
517
|
hookConfigPath,
|
|
585
518
|
};
|
|
586
519
|
|
|
587
|
-
term.pty
|
|
520
|
+
term.pty.onData((data) => {
|
|
588
521
|
const seq = term.outputSeq++;
|
|
589
522
|
const envelope = createEnvelope({
|
|
590
523
|
type: "terminal.output",
|
|
@@ -603,7 +536,7 @@ export class BridgeSession {
|
|
|
603
536
|
this.send(envelope);
|
|
604
537
|
});
|
|
605
538
|
|
|
606
|
-
term.pty
|
|
539
|
+
term.pty.onExit(({ exitCode, signal }) => {
|
|
607
540
|
term.status = "exited";
|
|
608
541
|
this.cleanupHookServer(term);
|
|
609
542
|
this.send(createEnvelope({
|
|
@@ -1191,10 +1124,7 @@ export class BridgeSession {
|
|
|
1191
1124
|
this.socket = undefined;
|
|
1192
1125
|
for (const term of this.terminals.values()) {
|
|
1193
1126
|
this.cleanupHookServer(term);
|
|
1194
|
-
if (term.status === "running")
|
|
1195
|
-
if (term.pty) term.pty.kill();
|
|
1196
|
-
if (term.acpRelay) term.acpRelay.kill();
|
|
1197
|
-
}
|
|
1127
|
+
if (term.status === "running") term.pty.kill();
|
|
1198
1128
|
}
|
|
1199
1129
|
this.terminals.clear();
|
|
1200
1130
|
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
|
-
}
|