linkshell-cli 0.5.42 → 0.6.1
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/commands/host.d.ts +30 -0
- package/dist/cli/src/commands/host.js +278 -0
- package/dist/cli/src/commands/host.js.map +1 -0
- package/dist/cli/src/commands/pair.d.ts +1 -0
- package/dist/cli/src/commands/pair.js +51 -0
- package/dist/cli/src/commands/pair.js.map +1 -0
- package/dist/cli/src/index.js +69 -0
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +1 -1
- package/dist/cli/src/utils/daemon.js +3 -2
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/host/src/drivers/acp/connection.d.ts +61 -0
- package/dist/host/src/drivers/acp/connection.js +121 -0
- package/dist/host/src/drivers/acp/connection.js.map +1 -0
- package/dist/host/src/drivers/acp/driver.d.ts +110 -0
- package/dist/host/src/drivers/acp/driver.js +438 -0
- package/dist/host/src/drivers/acp/driver.js.map +1 -0
- package/dist/host/src/drivers/acp/mapper.d.ts +49 -0
- package/dist/host/src/drivers/acp/mapper.js +411 -0
- package/dist/host/src/drivers/acp/mapper.js.map +1 -0
- package/dist/host/src/drivers/auth.d.ts +7 -0
- package/dist/host/src/drivers/auth.js +57 -0
- package/dist/host/src/drivers/auth.js.map +1 -0
- package/dist/host/src/drivers/claude/driver.d.ts +76 -0
- package/dist/host/src/drivers/claude/driver.js +331 -0
- package/dist/host/src/drivers/claude/driver.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +335 -0
- package/dist/host/src/drivers/claude/transcript.js +516 -0
- package/dist/host/src/drivers/claude/transcript.js.map +1 -0
- package/dist/host/src/drivers/codex/app-server.d.ts +36 -0
- package/dist/host/src/drivers/codex/app-server.js +144 -0
- package/dist/host/src/drivers/codex/app-server.js.map +1 -0
- package/dist/host/src/drivers/codex/driver.d.ts +89 -0
- package/dist/host/src/drivers/codex/driver.js +452 -0
- package/dist/host/src/drivers/codex/driver.js.map +1 -0
- package/dist/host/src/drivers/codex/mapper.d.ts +61 -0
- package/dist/host/src/drivers/codex/mapper.js +623 -0
- package/dist/host/src/drivers/codex/mapper.js.map +1 -0
- package/dist/host/src/drivers/codex/settings.d.ts +51 -0
- package/dist/host/src/drivers/codex/settings.js +107 -0
- package/dist/host/src/drivers/codex/settings.js.map +1 -0
- package/dist/host/src/drivers/images.d.ts +11 -0
- package/dist/host/src/drivers/images.js +34 -0
- package/dist/host/src/drivers/images.js.map +1 -0
- package/dist/host/src/drivers/nesting.d.ts +6 -0
- package/dist/host/src/drivers/nesting.js +25 -0
- package/dist/host/src/drivers/nesting.js.map +1 -0
- package/dist/host/src/drivers/registry.d.ts +21 -0
- package/dist/host/src/drivers/registry.js +30 -0
- package/dist/host/src/drivers/registry.js.map +1 -0
- package/dist/host/src/drivers/types.d.ts +105 -0
- package/dist/host/src/drivers/types.js +2 -0
- package/dist/host/src/drivers/types.js.map +1 -0
- package/dist/host/src/fs.d.ts +69 -0
- package/dist/host/src/fs.js +198 -0
- package/dist/host/src/fs.js.map +1 -0
- package/dist/host/src/gateway.d.ts +55 -0
- package/dist/host/src/gateway.js +263 -0
- package/dist/host/src/gateway.js.map +1 -0
- package/dist/host/src/host.d.ts +51 -0
- package/dist/host/src/host.js +133 -0
- package/dist/host/src/host.js.map +1 -0
- package/dist/host/src/hub.d.ts +89 -0
- package/dist/host/src/hub.js +592 -0
- package/dist/host/src/hub.js.map +1 -0
- package/dist/host/src/index.d.ts +10 -0
- package/dist/host/src/index.js +10 -0
- package/dist/host/src/index.js.map +1 -0
- package/dist/host/src/ports.d.ts +24 -0
- package/dist/host/src/ports.js +201 -0
- package/dist/host/src/ports.js.map +1 -0
- package/dist/host/src/rpc/client.d.ts +9 -0
- package/dist/host/src/rpc/client.js +42 -0
- package/dist/host/src/rpc/client.js.map +1 -0
- package/dist/host/src/rpc/server.d.ts +52 -0
- package/dist/host/src/rpc/server.js +293 -0
- package/dist/host/src/rpc/server.js.map +1 -0
- package/dist/host/src/screen.d.ts +30 -0
- package/dist/host/src/screen.js +230 -0
- package/dist/host/src/screen.js.map +1 -0
- package/dist/host/src/shell-env.d.ts +8 -0
- package/dist/host/src/shell-env.js +39 -0
- package/dist/host/src/shell-env.js.map +1 -0
- package/dist/host/src/store.d.ts +75 -0
- package/dist/host/src/store.js +265 -0
- package/dist/host/src/store.js.map +1 -0
- package/dist/host/src/terminals.d.ts +55 -0
- package/dist/host/src/terminals.js +367 -0
- package/dist/host/src/terminals.js.map +1 -0
- package/dist/shared-protocol/src/index.d.ts +3345 -3345
- package/dist/wire/src/crypto.d.ts +65 -0
- package/dist/wire/src/crypto.js +253 -0
- package/dist/wire/src/crypto.js.map +1 -0
- package/dist/wire/src/index.d.ts +8 -0
- package/dist/wire/src/index.js +9 -0
- package/dist/wire/src/index.js.map +1 -0
- package/dist/wire/src/model.d.ts +704 -0
- package/dist/wire/src/model.js +163 -0
- package/dist/wire/src/model.js.map +1 -0
- package/dist/wire/src/peer.d.ts +42 -0
- package/dist/wire/src/peer.js +143 -0
- package/dist/wire/src/peer.js.map +1 -0
- package/dist/wire/src/relay-client.d.ts +82 -0
- package/dist/wire/src/relay-client.js +259 -0
- package/dist/wire/src/relay-client.js.map +1 -0
- package/dist/wire/src/relay.d.ts +481 -0
- package/dist/wire/src/relay.js +129 -0
- package/dist/wire/src/relay.js.map +1 -0
- package/dist/wire/src/rpc.d.ts +4170 -0
- package/dist/wire/src/rpc.js +332 -0
- package/dist/wire/src/rpc.js.map +1 -0
- package/dist/wire/src/shell.d.ts +2 -0
- package/dist/wire/src/shell.js +62 -0
- package/dist/wire/src/shell.js.map +1 -0
- package/dist/wire/src/updates.d.ts +3325 -0
- package/dist/wire/src/updates.js +210 -0
- package/dist/wire/src/updates.js.map +1 -0
- package/package.json +8 -3
- package/src/commands/host.ts +297 -0
- package/src/commands/pair.ts +52 -0
- package/src/index.ts +74 -0
- package/src/utils/daemon.ts +4 -3
- package/dist/cli/src/commands/hook.d.ts +0 -6
- package/dist/cli/src/commands/hook.js +0 -120
- package/dist/cli/src/commands/hook.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace-git.d.ts +0 -63
- package/dist/cli/src/runtime/acp/agent-workspace-git.js +0 -297
- package/dist/cli/src/runtime/acp/agent-workspace-git.js.map +0 -1
- package/dist/cli/src/runtime/acp-relay.d.ts +0 -23
- package/dist/cli/src/runtime/acp-relay.js +0 -73
- package/dist/cli/src/runtime/acp-relay.js.map +0 -1
- package/dist/cli/src/runtime/hook-permission-server.d.ts +0 -36
- package/dist/cli/src/runtime/hook-permission-server.js +0 -147
- package/dist/cli/src/runtime/hook-permission-server.js.map +0 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type PortInfo } from "@linkshell/wire";
|
|
2
|
+
/**
|
|
3
|
+
* The host's web servers worth previewing: ones that answer HTTP and were
|
|
4
|
+
* started from a directory (system services run from `/`), minus this host's
|
|
5
|
+
* own ports. Databases, app helpers and build daemons listen too, but aren't
|
|
6
|
+
* pages.
|
|
7
|
+
*/
|
|
8
|
+
export declare function listPorts(options?: {
|
|
9
|
+
exclude?: number[];
|
|
10
|
+
}): Promise<PortInfo[]>;
|
|
11
|
+
/** One device connection's open streams; all end when the connection does. */
|
|
12
|
+
export declare class ProxyStreams {
|
|
13
|
+
private readonly send;
|
|
14
|
+
private readonly streams;
|
|
15
|
+
constructor(send: {
|
|
16
|
+
data: (streamId: string, data: string) => void;
|
|
17
|
+
closed: (streamId: string, error?: string) => void;
|
|
18
|
+
});
|
|
19
|
+
open(port: number): Promise<string>;
|
|
20
|
+
write(streamId: string, data: string): void;
|
|
21
|
+
/** The device hung up: finish sending, then close. */
|
|
22
|
+
close(streamId: string): void;
|
|
23
|
+
closeAll(): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readlink } from "node:fs/promises";
|
|
3
|
+
import { connect } from "node:net";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { RpcError } from "@linkshell/wire";
|
|
7
|
+
// Previews of the host's local servers. `listPorts` finds what's listening
|
|
8
|
+
// (and what looks like a dev server); `ProxyStreams` carries TCP streams to
|
|
9
|
+
// those ports over a device's encrypted channel, like `ssh -L`.
|
|
10
|
+
const run = promisify(execFile);
|
|
11
|
+
/** `lsof -F` output: `p<pid>`, `c<command>`, then an `n<address>` per socket. */
|
|
12
|
+
function parseLsof(output) {
|
|
13
|
+
const found = [];
|
|
14
|
+
let pid;
|
|
15
|
+
let command = "";
|
|
16
|
+
for (const line of output.split("\n")) {
|
|
17
|
+
const value = line.slice(1);
|
|
18
|
+
if (line.startsWith("p"))
|
|
19
|
+
pid = Number(value);
|
|
20
|
+
else if (line.startsWith("c"))
|
|
21
|
+
command = value;
|
|
22
|
+
else if (line.startsWith("n")) {
|
|
23
|
+
const port = Number(/:(\d+)$/.exec(value)?.[1]);
|
|
24
|
+
if (port)
|
|
25
|
+
found.push({ port, pid, process: command });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
/** `ss -ltnpH`: `LISTEN 0 511 127.0.0.1:5173 0.0.0.0:* users:(("node",pid=1234,fd=20))`. */
|
|
31
|
+
function parseSs(output) {
|
|
32
|
+
const found = [];
|
|
33
|
+
for (const line of output.split("\n")) {
|
|
34
|
+
const columns = line.trim().split(/\s+/);
|
|
35
|
+
const port = Number(/:(\d+)$/.exec(columns[3] ?? "")?.[1]);
|
|
36
|
+
if (!port)
|
|
37
|
+
continue;
|
|
38
|
+
const owner = /\("([^"]+)",pid=(\d+)/.exec(line);
|
|
39
|
+
found.push({ port, process: owner?.[1] ?? "", pid: owner ? Number(owner[2]) : undefined });
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|
|
43
|
+
async function listeners() {
|
|
44
|
+
if (process.platform === "darwin") {
|
|
45
|
+
const { stdout } = await run("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fpcn"], { timeout: 5000, maxBuffer: 4_000_000 }).catch((error) => ({ stdout: error.stdout ?? "" }));
|
|
46
|
+
return parseLsof(stdout);
|
|
47
|
+
}
|
|
48
|
+
if (process.platform === "linux") {
|
|
49
|
+
const { stdout } = await run("ss", ["-ltnpH"], { timeout: 5000 }).catch(() => ({ stdout: "" }));
|
|
50
|
+
return parseSs(stdout);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
async function workingDirectories(pids) {
|
|
55
|
+
const cwd = new Map();
|
|
56
|
+
if (!pids.length)
|
|
57
|
+
return cwd;
|
|
58
|
+
if (process.platform === "darwin") {
|
|
59
|
+
const { stdout } = await run("lsof", ["-a", "-d", "cwd", "-Fpn", "-p", pids.join(",")], { timeout: 5000 }).catch((error) => ({ stdout: error.stdout ?? "" }));
|
|
60
|
+
let pid;
|
|
61
|
+
for (const line of stdout.split("\n")) {
|
|
62
|
+
if (line.startsWith("p"))
|
|
63
|
+
pid = Number(line.slice(1));
|
|
64
|
+
else if (line.startsWith("n") && pid !== undefined)
|
|
65
|
+
cwd.set(pid, line.slice(1));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (process.platform === "linux") {
|
|
69
|
+
await Promise.all(pids.map(async (pid) => cwd.set(pid, await readlink(`/proc/${pid}/cwd`).catch(() => ""))));
|
|
70
|
+
}
|
|
71
|
+
return cwd;
|
|
72
|
+
}
|
|
73
|
+
/** Connects to a port on this machine's loopback, IPv4 first. */
|
|
74
|
+
function connectLoopback(port) {
|
|
75
|
+
const attempt = (host) => new Promise((resolve, reject) => {
|
|
76
|
+
const socket = connect({ host, port });
|
|
77
|
+
socket.once("connect", () => {
|
|
78
|
+
socket.removeAllListeners("error");
|
|
79
|
+
resolve(socket);
|
|
80
|
+
});
|
|
81
|
+
socket.once("error", reject);
|
|
82
|
+
});
|
|
83
|
+
return attempt("127.0.0.1").catch(() => attempt("::1"));
|
|
84
|
+
}
|
|
85
|
+
/** Whether the port answers HTTP, and the page's <title> if it serves HTML. */
|
|
86
|
+
async function probeHttp(port, timeoutMs = 1200) {
|
|
87
|
+
let socket;
|
|
88
|
+
try {
|
|
89
|
+
socket = await connectLoopback(port);
|
|
90
|
+
const open = socket;
|
|
91
|
+
return await new Promise((resolve) => {
|
|
92
|
+
let head = "";
|
|
93
|
+
const done = () => {
|
|
94
|
+
open.destroy();
|
|
95
|
+
const http = head.startsWith("HTTP/");
|
|
96
|
+
const html = /content-type:\s*text\/html/i.test(head);
|
|
97
|
+
const title = html ? /<title[^>]*>([^<]{1,200})<\/title>/i.exec(head)?.[1]?.trim() : undefined;
|
|
98
|
+
resolve({ http, title: title || undefined });
|
|
99
|
+
};
|
|
100
|
+
const timer = setTimeout(done, timeoutMs);
|
|
101
|
+
open.on("data", (chunk) => {
|
|
102
|
+
head += chunk.toString("utf8");
|
|
103
|
+
if (head.length > 64_000 || /<\/title>/i.test(head)) {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
done();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
open.on("end", () => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
done();
|
|
111
|
+
});
|
|
112
|
+
open.on("error", () => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
done();
|
|
115
|
+
});
|
|
116
|
+
open.write(`GET / HTTP/1.1\r\nHost: localhost:${port}\r\nAccept: text/html\r\nConnection: close\r\n\r\n`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
socket?.destroy();
|
|
121
|
+
return { http: false };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The host's web servers worth previewing: ones that answer HTTP and were
|
|
126
|
+
* started from a directory (system services run from `/`), minus this host's
|
|
127
|
+
* own ports. Databases, app helpers and build daemons listen too, but aren't
|
|
128
|
+
* pages.
|
|
129
|
+
*/
|
|
130
|
+
export async function listPorts(options = {}) {
|
|
131
|
+
const exclude = new Set(options.exclude ?? []);
|
|
132
|
+
const seen = new Map();
|
|
133
|
+
for (const listener of await listeners()) {
|
|
134
|
+
if (exclude.has(listener.port) || listener.pid === process.pid || seen.has(listener.port))
|
|
135
|
+
continue;
|
|
136
|
+
seen.set(listener.port, listener);
|
|
137
|
+
}
|
|
138
|
+
const all = [...seen.values()];
|
|
139
|
+
const cwd = await workingDirectories([...new Set(all.flatMap((listener) => (listener.pid ? [listener.pid] : [])))]);
|
|
140
|
+
const candidates = all
|
|
141
|
+
.map((listener) => ({ ...listener, cwd: listener.pid ? cwd.get(listener.pid) : undefined }))
|
|
142
|
+
.filter((listener) => listener.cwd && listener.cwd !== "/")
|
|
143
|
+
.slice(0, 40);
|
|
144
|
+
const probed = await Promise.all(candidates.map(async (listener) => ({ ...listener, ...(await probeHttp(listener.port)) })));
|
|
145
|
+
return probed
|
|
146
|
+
.filter((entry) => entry.http)
|
|
147
|
+
.map(({ port, pid, process: name, cwd: dir, http, title }) => ({ port, pid, process: name, cwd: dir, http, title }))
|
|
148
|
+
.sort((a, b) => a.port - b.port);
|
|
149
|
+
}
|
|
150
|
+
const MAX_STREAMS = 96;
|
|
151
|
+
/** One device connection's open streams; all end when the connection does. */
|
|
152
|
+
export class ProxyStreams {
|
|
153
|
+
send;
|
|
154
|
+
streams = new Map();
|
|
155
|
+
constructor(send) {
|
|
156
|
+
this.send = send;
|
|
157
|
+
}
|
|
158
|
+
async open(port) {
|
|
159
|
+
if (this.streams.size >= MAX_STREAMS)
|
|
160
|
+
throw RpcError.app("busy", "too many open preview connections");
|
|
161
|
+
let socket;
|
|
162
|
+
try {
|
|
163
|
+
socket = await connectLoopback(port);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw RpcError.app("not_found", `nothing is listening on port ${port}`, { cause: String(error) });
|
|
167
|
+
}
|
|
168
|
+
const streamId = randomUUID();
|
|
169
|
+
this.streams.set(streamId, socket);
|
|
170
|
+
socket.setNoDelay(true);
|
|
171
|
+
socket.on("data", (chunk) => this.send.data(streamId, chunk.toString("base64")));
|
|
172
|
+
let failure;
|
|
173
|
+
socket.on("error", (error) => (failure = error.message));
|
|
174
|
+
socket.on("close", () => {
|
|
175
|
+
if (this.streams.delete(streamId))
|
|
176
|
+
this.send.closed(streamId, failure);
|
|
177
|
+
});
|
|
178
|
+
return streamId;
|
|
179
|
+
}
|
|
180
|
+
write(streamId, data) {
|
|
181
|
+
const socket = this.streams.get(streamId);
|
|
182
|
+
if (!socket)
|
|
183
|
+
throw RpcError.app("not_found", "this preview connection has closed");
|
|
184
|
+
socket.write(Buffer.from(data, "base64"));
|
|
185
|
+
}
|
|
186
|
+
/** The device hung up: finish sending, then close. */
|
|
187
|
+
close(streamId) {
|
|
188
|
+
const socket = this.streams.get(streamId);
|
|
189
|
+
if (!socket)
|
|
190
|
+
return;
|
|
191
|
+
this.streams.delete(streamId);
|
|
192
|
+
socket.end();
|
|
193
|
+
setTimeout(() => socket.destroy(), 5000).unref();
|
|
194
|
+
}
|
|
195
|
+
closeAll() {
|
|
196
|
+
for (const socket of this.streams.values())
|
|
197
|
+
socket.destroy();
|
|
198
|
+
this.streams.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../../../../host/src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAe,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAiB,MAAM,iBAAiB,CAAC;AAE1D,2EAA2E;AAC3E,4EAA4E;AAC5E,gEAAgE;AAEhE,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAQhC,iFAAiF;AACjF,SAAS,SAAS,CAAC,MAAc;IAC/B,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,IAAI,GAAuB,CAAC;IAC5B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;aACzC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,KAAK,CAAC;aAC1C,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,IAAI,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,4FAA4F;AAC5F,SAAS,OAAO,CAAC,MAAc;IAC7B,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,KAAK,GAAG,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,KAAK,UAAU,SAAS;IACtB,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,CAC5H,CAAC,KAA0B,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CACjE,CAAC;QACF,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;QAChG,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,IAAc;IAC9C,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,CAAC;IAC7B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAC9G,CAAC,KAA0B,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CACjE,CAAC;QACF,IAAI,GAAuB,CAAC;QAC5B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;iBACjD,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,SAAS;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;SAAM,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACxC,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,QAAQ,CAAC,SAAS,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/G,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,iEAAiE;AACjE,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE,CAC/B,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;YAC1B,MAAM,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;YACnC,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IACL,OAAO,OAAO,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,+EAA+E;AAC/E,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,SAAS,GAAG,IAAI;IACrD,IAAI,MAA0B,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC;QACpB,OAAO,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YACnC,IAAI,IAAI,GAAG,EAAE,CAAC;YACd,MAAM,IAAI,GAAG,GAAG,EAAE;gBAChB,IAAI,CAAC,OAAO,EAAE,CAAC;gBACf,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBACtC,MAAM,IAAI,GAAG,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACtD,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,qCAAqC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC/F,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,SAAS,EAAE,CAAC,CAAC;YAC/C,CAAC,CAAC;YACF,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YAC1C,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;gBACxB,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBAC/B,IAAI,IAAI,CAAC,MAAM,GAAG,MAAM,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACpD,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,IAAI,EAAE,CAAC;gBACT,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBAClB,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,IAAI,EAAE,CAAC;YACT,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACpB,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,IAAI,EAAE,CAAC;YACT,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,qCAAqC,IAAI,oDAAoD,CAAC,CAAC;QAC5G,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,EAAE,OAAO,EAAE,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,UAAkC,EAAE;IAClE,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;IAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACzC,KAAK,MAAM,QAAQ,IAAI,MAAM,SAAS,EAAE,EAAE,CAAC;QACzC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,SAAS;QACpG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/B,MAAM,GAAG,GAAG,MAAM,kBAAkB,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpH,MAAM,UAAU,GAAG,GAAG;SACnB,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;SAC3F,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,IAAI,QAAQ,CAAC,GAAG,KAAK,GAAG,CAAC;SAC1D,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAChB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,CAAC,MAAM,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC7H,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;SAC7B,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;SACnH,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,8EAA8E;AAC9E,MAAM,OAAO,YAAY;IAIJ;IAHF,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAErD,YACmB,IAGhB;QAHgB,SAAI,GAAJ,IAAI,CAGpB;IACA,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAAC;QACtG,IAAI,MAAc,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;QACvC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,gCAAgC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpG,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACxB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACjF,IAAI,OAA2B,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QACzD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QACzE,CAAC,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,QAAgB,EAAE,IAAY;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,oCAAoC,CAAC,CAAC;QACnF,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,QAAgB;QACpB,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,EAAE,CAAC;QACb,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;IACnD,CAAC;IAED,QAAQ;QACN,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE;YAAE,MAAM,CAAC,OAAO,EAAE,CAAC;QAC7D,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MethodName, type MethodParams, type MethodResult, type NotificationName, type NotificationParams } from "@linkshell/wire";
|
|
2
|
+
export interface HostClient {
|
|
3
|
+
call<M extends MethodName>(method: M, params: MethodParams<M>, timeoutMs?: number): Promise<MethodResult<M>>;
|
|
4
|
+
on<N extends NotificationName>(name: N, listener: (params: NotificationParams<N>) => void): () => void;
|
|
5
|
+
close(): void;
|
|
6
|
+
readonly closed: Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/** Connects to a host over its unix socket (`path`) or a `ws://` URL. */
|
|
9
|
+
export declare function connectHost(target: string): Promise<HostClient>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
import { RpcPeer, } from "@linkshell/wire";
|
|
3
|
+
/** Connects to a host over its unix socket (`path`) or a `ws://` URL. */
|
|
4
|
+
export async function connectHost(target) {
|
|
5
|
+
const url = target.startsWith("ws://") || target.startsWith("wss://") ? target : `ws+unix://${target}:/`;
|
|
6
|
+
const socket = new WebSocket(url, { perMessageDeflate: false });
|
|
7
|
+
await new Promise((resolve, reject) => {
|
|
8
|
+
socket.once("open", () => resolve());
|
|
9
|
+
socket.once("error", reject);
|
|
10
|
+
});
|
|
11
|
+
const listeners = new Map();
|
|
12
|
+
const peer = new RpcPeer({
|
|
13
|
+
send: (text) => socket.send(text),
|
|
14
|
+
onNotification: (method, params) => {
|
|
15
|
+
for (const listener of listeners.get(method) ?? [])
|
|
16
|
+
listener(params);
|
|
17
|
+
},
|
|
18
|
+
requestTimeoutMs: 120_000,
|
|
19
|
+
});
|
|
20
|
+
socket.on("message", (data) => peer.receive(data.toString()));
|
|
21
|
+
const closed = new Promise((resolve) => {
|
|
22
|
+
socket.once("close", () => {
|
|
23
|
+
peer.close();
|
|
24
|
+
resolve();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
call: (method, params, timeoutMs) => peer.request(method, params, timeoutMs),
|
|
29
|
+
on(name, listener) {
|
|
30
|
+
let set = listeners.get(name);
|
|
31
|
+
if (!set) {
|
|
32
|
+
set = new Set();
|
|
33
|
+
listeners.set(name, set);
|
|
34
|
+
}
|
|
35
|
+
set.add(listener);
|
|
36
|
+
return () => set.delete(listener);
|
|
37
|
+
},
|
|
38
|
+
close: () => socket.close(),
|
|
39
|
+
closed,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../../../host/src/rpc/client.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,IAAI,CAAC;AAC3B,OAAO,EACL,OAAO,GAMR,MAAM,iBAAiB,CAAC;AASzB,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,MAAM,IAAI,CAAC;IACzG,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,IAAI,GAAG,EAA0C,CAAC;IACpE,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC;QACvB,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;QACjC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;YACjC,KAAK,MAAM,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;gBAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;QACvE,CAAC;QACD,gBAAgB,EAAE,OAAO;KAC1B,CAAC,CAAC;IACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC3C,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;YACxB,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,OAAO;QACL,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,CAAC;QAC5E,EAAE,CAAC,IAAI,EAAE,QAAQ;YACf,IAAI,GAAG,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC;gBAChB,SAAS,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAC3B,CAAC;YACD,GAAG,CAAC,GAAG,CAAC,QAAqC,CAAC,CAAC;YAC/C,OAAO,GAAG,EAAE,CAAC,GAAI,CAAC,MAAM,CAAC,QAAqC,CAAC,CAAC;QAClE,CAAC;QACD,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;QAC3B,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type MachineInfo, type NotificationName, type NotificationParams } from "@linkshell/wire";
|
|
2
|
+
import type { GatewayLink } from "../gateway.js";
|
|
3
|
+
import type { SessionHub } from "../hub.js";
|
|
4
|
+
import type { TerminalManager } from "../terminals.js";
|
|
5
|
+
export interface HostRpcServerOptions {
|
|
6
|
+
hub: SessionHub;
|
|
7
|
+
terminals: TerminalManager;
|
|
8
|
+
machineInfo: () => MachineInfo;
|
|
9
|
+
/** Unix socket for local clients (terminal shims, CLI). */
|
|
10
|
+
socketPath: string;
|
|
11
|
+
/** Optional loopback TCP port, for local development clients. */
|
|
12
|
+
tcpPort?: number;
|
|
13
|
+
log: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/** Anything that carries JSON-RPC text both ways: a local WebSocket, or an encrypted tunnel through the gateway. */
|
|
16
|
+
export interface RpcTransport {
|
|
17
|
+
send(text: string): void;
|
|
18
|
+
close(): void;
|
|
19
|
+
onMessage(listener: (text: string) => void): void;
|
|
20
|
+
onClose(listener: () => void): void;
|
|
21
|
+
/** Local transports (this machine's own sockets) may use desktop.* methods. */
|
|
22
|
+
local: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* JSON-RPC over WebSocket for host clients. Each connection gets live
|
|
26
|
+
* `session.summary` notifications, plus `session.event` for the sessions it
|
|
27
|
+
* subscribed to.
|
|
28
|
+
*/
|
|
29
|
+
export declare class HostRpcServer {
|
|
30
|
+
private readonly options;
|
|
31
|
+
private readonly servers;
|
|
32
|
+
private readonly sockets;
|
|
33
|
+
private readonly connections;
|
|
34
|
+
private gateway?;
|
|
35
|
+
private readonly screen;
|
|
36
|
+
private readonly handlers;
|
|
37
|
+
constructor(options: HostRpcServerOptions);
|
|
38
|
+
start(): Promise<void>;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
/** The TCP port actually bound (useful when started with port 0). */
|
|
41
|
+
tcpAddress(): number | undefined;
|
|
42
|
+
private listen;
|
|
43
|
+
/** Makes this connection the terminal driving a handoff session. */
|
|
44
|
+
private adoptDesktop;
|
|
45
|
+
private accept;
|
|
46
|
+
setGateway(gateway: GatewayLink): void;
|
|
47
|
+
/** A notification to every connected client. */
|
|
48
|
+
broadcast<N extends NotificationName>(name: N, params: NotificationParams<N>): void;
|
|
49
|
+
/** Serves one client connection over `transport` until it closes. */
|
|
50
|
+
connect(transport: RpcTransport): void;
|
|
51
|
+
private dispatch;
|
|
52
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
4
|
+
import { RPC_INVALID_PARAMS, RPC_METHOD_NOT_FOUND, RpcError, RpcPeer, isMethodName, methods, } from "@linkshell/wire";
|
|
5
|
+
import { listDirectory, makeDirectory, readFile, searchDirectories, uploadFile } from "../fs.js";
|
|
6
|
+
import { listPorts, ProxyStreams } from "../ports.js";
|
|
7
|
+
import { ScreenShare } from "../screen.js";
|
|
8
|
+
/**
|
|
9
|
+
* JSON-RPC over WebSocket for host clients. Each connection gets live
|
|
10
|
+
* `session.summary` notifications, plus `session.event` for the sessions it
|
|
11
|
+
* subscribed to.
|
|
12
|
+
*/
|
|
13
|
+
export class HostRpcServer {
|
|
14
|
+
options;
|
|
15
|
+
servers = [];
|
|
16
|
+
sockets = new Set();
|
|
17
|
+
connections = new Set();
|
|
18
|
+
gateway;
|
|
19
|
+
screen;
|
|
20
|
+
handlers;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
this.screen = new ScreenShare(options.log);
|
|
24
|
+
const hub = options.hub;
|
|
25
|
+
const terminals = options.terminals;
|
|
26
|
+
this.handlers = {
|
|
27
|
+
"machine.info": () => {
|
|
28
|
+
void hub.refreshAuthIfStale();
|
|
29
|
+
return options.machineInfo();
|
|
30
|
+
},
|
|
31
|
+
"projects.list": (params) => ({ projects: hub.listProjects(params.limit) }),
|
|
32
|
+
"sessions.list": (params) => hub.listSessions(params),
|
|
33
|
+
"sessions.create": async (params) => ({ session: await hub.createSession(params) }),
|
|
34
|
+
"sessions.subscribe": async (params, context) => {
|
|
35
|
+
const previous = context.subscriptions.get(params.sessionId);
|
|
36
|
+
if (previous)
|
|
37
|
+
hub.unsubscribe(params.sessionId, previous);
|
|
38
|
+
const subscriber = { event: (event) => context.peer.notify("session.event", event) };
|
|
39
|
+
context.subscriptions.set(params.sessionId, subscriber);
|
|
40
|
+
return { session: await hub.subscribe(params.sessionId, params.fromSeq, subscriber) };
|
|
41
|
+
},
|
|
42
|
+
"sessions.unsubscribe": (params, context) => {
|
|
43
|
+
const subscriber = context.subscriptions.get(params.sessionId);
|
|
44
|
+
if (subscriber)
|
|
45
|
+
hub.unsubscribe(params.sessionId, subscriber);
|
|
46
|
+
context.subscriptions.delete(params.sessionId);
|
|
47
|
+
return {};
|
|
48
|
+
},
|
|
49
|
+
"sessions.prompt": async (params) => ({
|
|
50
|
+
delivery: await hub.prompt(params.sessionId, params.clientMessageId, params.content),
|
|
51
|
+
}),
|
|
52
|
+
"sessions.cancel": async (params) => {
|
|
53
|
+
await hub.cancel(params.sessionId);
|
|
54
|
+
return {};
|
|
55
|
+
},
|
|
56
|
+
"sessions.permission": async (params) => {
|
|
57
|
+
await hub.respondPermission(params.sessionId, params.requestId, params.optionId);
|
|
58
|
+
return {};
|
|
59
|
+
},
|
|
60
|
+
"sessions.setConfig": async (params) => {
|
|
61
|
+
await hub.setConfig(params.sessionId, params.optionId, params.value);
|
|
62
|
+
return {};
|
|
63
|
+
},
|
|
64
|
+
"sessions.takeover": async (params) => ({ driver: (await hub.takeover(params.sessionId)).driver }),
|
|
65
|
+
"sessions.release": (params) => ({ driver: hub.getSession(params.sessionId).driver }),
|
|
66
|
+
"desktop.launch": async (params, context) => {
|
|
67
|
+
const launch = await hub.desktopLaunch(params.agent, params.args, params.sessionId, { cwd: params.cwd, env: params.env });
|
|
68
|
+
if (launch.sessionId)
|
|
69
|
+
this.adoptDesktop(launch.sessionId, context);
|
|
70
|
+
return launch;
|
|
71
|
+
},
|
|
72
|
+
"desktop.yielded": (params, context) => {
|
|
73
|
+
const entry = context.desktops.get(params.sessionId);
|
|
74
|
+
entry?.pendingYield?.();
|
|
75
|
+
if (entry)
|
|
76
|
+
entry.pendingYield = undefined;
|
|
77
|
+
return {};
|
|
78
|
+
},
|
|
79
|
+
"gateway.status": () => this.gateway?.status() ?? { status: "off", devices: [] },
|
|
80
|
+
"pairing.start": (_params, context) => {
|
|
81
|
+
if (!context.local)
|
|
82
|
+
throw RpcError.app("forbidden", "pairing starts on the computer itself");
|
|
83
|
+
if (!this.gateway)
|
|
84
|
+
throw RpcError.app("not_supported", "no gateway configured: start the host with --gateway <url>");
|
|
85
|
+
return this.gateway.startPairing();
|
|
86
|
+
},
|
|
87
|
+
"devices.revoke": async (params) => {
|
|
88
|
+
await this.gateway?.revoke(params.deviceId);
|
|
89
|
+
return {};
|
|
90
|
+
},
|
|
91
|
+
"fs.list": (params) => listDirectory(params),
|
|
92
|
+
"fs.search": (params) => searchDirectories(params),
|
|
93
|
+
"fs.read": (params) => readFile(params),
|
|
94
|
+
"fs.upload": (params) => uploadFile(params),
|
|
95
|
+
"fs.mkdir": (params) => makeDirectory(params),
|
|
96
|
+
"terminals.list": () => ({ terminals: terminals.list() }),
|
|
97
|
+
"terminals.create": (params) => ({ terminal: terminals.create(params) }),
|
|
98
|
+
"terminals.attach": (params, context) => {
|
|
99
|
+
const previous = context.terminals.get(params.terminalId);
|
|
100
|
+
if (previous)
|
|
101
|
+
terminals.detach(params.terminalId, previous);
|
|
102
|
+
const listener = (seq, data) => context.peer.notify("terminal.output", { terminalId: params.terminalId, seq, data });
|
|
103
|
+
const attached = terminals.attach(params.terminalId, listener, params.fromSeq);
|
|
104
|
+
context.terminals.set(params.terminalId, listener);
|
|
105
|
+
return attached;
|
|
106
|
+
},
|
|
107
|
+
"terminals.detach": (params, context) => {
|
|
108
|
+
const listener = context.terminals.get(params.terminalId);
|
|
109
|
+
if (listener)
|
|
110
|
+
terminals.detach(params.terminalId, listener);
|
|
111
|
+
context.terminals.delete(params.terminalId);
|
|
112
|
+
return {};
|
|
113
|
+
},
|
|
114
|
+
"terminals.input": (params) => {
|
|
115
|
+
terminals.input(params.terminalId, params.data);
|
|
116
|
+
return {};
|
|
117
|
+
},
|
|
118
|
+
"terminals.resize": (params) => {
|
|
119
|
+
terminals.resize(params.terminalId, params.cols, params.rows);
|
|
120
|
+
return {};
|
|
121
|
+
},
|
|
122
|
+
"ports.list": async () => ({ ports: await listPorts() }),
|
|
123
|
+
"screen.start": () => this.screen.start(),
|
|
124
|
+
"proxy.open": async (params, context) => ({ streamId: await context.proxy.open(params.port) }),
|
|
125
|
+
"proxy.write": (params, context) => {
|
|
126
|
+
context.proxy.write(params.streamId, params.data);
|
|
127
|
+
return {};
|
|
128
|
+
},
|
|
129
|
+
"proxy.close": (params, context) => {
|
|
130
|
+
context.proxy.close(params.streamId);
|
|
131
|
+
return {};
|
|
132
|
+
},
|
|
133
|
+
"terminals.close": (params) => {
|
|
134
|
+
terminals.close(params.terminalId);
|
|
135
|
+
return {};
|
|
136
|
+
},
|
|
137
|
+
"desktop.reclaim": async (params, context) => {
|
|
138
|
+
const launch = await hub.reclaim(params.sessionId, {});
|
|
139
|
+
this.adoptDesktop(params.sessionId, context);
|
|
140
|
+
return launch;
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async start() {
|
|
145
|
+
if (existsSync(this.options.socketPath))
|
|
146
|
+
rmSync(this.options.socketPath, { force: true });
|
|
147
|
+
await this.listen((server) => server.listen(this.options.socketPath));
|
|
148
|
+
if (this.options.tcpPort !== undefined) {
|
|
149
|
+
await this.listen((server) => server.listen(this.options.tcpPort, "127.0.0.1"));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async stop() {
|
|
153
|
+
this.screen.stop();
|
|
154
|
+
for (const socket of this.sockets)
|
|
155
|
+
socket.terminate();
|
|
156
|
+
await Promise.all(this.servers.map((server) => new Promise((resolve) => server.close(() => resolve()))));
|
|
157
|
+
rmSync(this.options.socketPath, { force: true });
|
|
158
|
+
}
|
|
159
|
+
/** The TCP port actually bound (useful when started with port 0). */
|
|
160
|
+
tcpAddress() {
|
|
161
|
+
const address = this.servers[1]?.address();
|
|
162
|
+
return address && typeof address === "object" ? address.port : undefined;
|
|
163
|
+
}
|
|
164
|
+
listen(bind) {
|
|
165
|
+
const server = createServer((_, response) => {
|
|
166
|
+
response.writeHead(426, { "content-type": "text/plain" });
|
|
167
|
+
response.end("LinkShell host: WebSocket required\n");
|
|
168
|
+
});
|
|
169
|
+
const wss = new WebSocketServer({ server, perMessageDeflate: false, maxPayload: 32 * 1024 * 1024 });
|
|
170
|
+
wss.on("connection", (socket) => this.accept(socket));
|
|
171
|
+
this.servers.push(server);
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
server.once("error", reject);
|
|
174
|
+
server.once("listening", () => {
|
|
175
|
+
server.off("error", reject);
|
|
176
|
+
resolve();
|
|
177
|
+
});
|
|
178
|
+
bind(server);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Makes this connection the terminal driving a handoff session. */
|
|
182
|
+
adoptDesktop(sessionId, context) {
|
|
183
|
+
// Already this terminal's session (e.g. after reclaim): keep the registration.
|
|
184
|
+
if (context.desktops.has(sessionId))
|
|
185
|
+
return;
|
|
186
|
+
const entry = { unregister: () => { } };
|
|
187
|
+
entry.unregister = this.options.hub.registerDesktop(sessionId, {
|
|
188
|
+
yield: () => new Promise((resolve) => {
|
|
189
|
+
const timer = setTimeout(done, 15_000);
|
|
190
|
+
function done() {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
resolve();
|
|
193
|
+
}
|
|
194
|
+
entry.pendingYield = done;
|
|
195
|
+
context.peer.notify("desktop.yield", { sessionId });
|
|
196
|
+
}),
|
|
197
|
+
activity: (line) => context.peer.notify("desktop.remoteActivity", { sessionId, line }),
|
|
198
|
+
});
|
|
199
|
+
context.desktops.set(sessionId, entry);
|
|
200
|
+
}
|
|
201
|
+
accept(socket) {
|
|
202
|
+
this.sockets.add(socket);
|
|
203
|
+
const listeners = {};
|
|
204
|
+
socket.on("message", (data) => listeners.message?.(data.toString()));
|
|
205
|
+
socket.on("close", () => {
|
|
206
|
+
this.sockets.delete(socket);
|
|
207
|
+
listeners.close?.();
|
|
208
|
+
});
|
|
209
|
+
this.connect({
|
|
210
|
+
local: true,
|
|
211
|
+
send: (text) => {
|
|
212
|
+
if (socket.readyState === WebSocket.OPEN)
|
|
213
|
+
socket.send(text);
|
|
214
|
+
},
|
|
215
|
+
close: () => socket.close(),
|
|
216
|
+
onMessage: (listener) => (listeners.message = listener),
|
|
217
|
+
onClose: (listener) => (listeners.close = listener),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
setGateway(gateway) {
|
|
221
|
+
this.gateway = gateway;
|
|
222
|
+
}
|
|
223
|
+
/** A notification to every connected client. */
|
|
224
|
+
broadcast(name, params) {
|
|
225
|
+
for (const peer of this.connections)
|
|
226
|
+
peer.notify(name, params);
|
|
227
|
+
}
|
|
228
|
+
/** Serves one client connection over `transport` until it closes. */
|
|
229
|
+
connect(transport) {
|
|
230
|
+
const peer = new RpcPeer({
|
|
231
|
+
send: (text) => transport.send(text),
|
|
232
|
+
onRequest: (method, params) => this.dispatch(method, params, context),
|
|
233
|
+
});
|
|
234
|
+
const context = {
|
|
235
|
+
local: transport.local,
|
|
236
|
+
desktops: new Map(),
|
|
237
|
+
subscriptions: new Map(),
|
|
238
|
+
terminals: new Map(),
|
|
239
|
+
proxy: new ProxyStreams({
|
|
240
|
+
data: (streamId, data) => peer.notify("proxy.data", { streamId, data }),
|
|
241
|
+
closed: (streamId, error) => peer.notify("proxy.closed", error ? { streamId, error } : { streamId }),
|
|
242
|
+
}),
|
|
243
|
+
peer,
|
|
244
|
+
};
|
|
245
|
+
this.connections.add(context.peer);
|
|
246
|
+
const stopSummaries = this.options.hub.onSummary((session) => context.peer.notify("session.summary", { session }));
|
|
247
|
+
const stopTerminals = this.options.terminals.onChange((terminal, closed) => context.peer.notify("terminal.changed", closed ? { terminal, closed } : { terminal }));
|
|
248
|
+
transport.onMessage((text) => context.peer.receive(text));
|
|
249
|
+
transport.onClose(() => {
|
|
250
|
+
this.connections.delete(context.peer);
|
|
251
|
+
stopSummaries();
|
|
252
|
+
stopTerminals();
|
|
253
|
+
for (const [terminalId, listener] of context.terminals)
|
|
254
|
+
this.options.terminals.detach(terminalId, listener);
|
|
255
|
+
context.terminals.clear();
|
|
256
|
+
for (const [sessionId, subscriber] of context.subscriptions)
|
|
257
|
+
this.options.hub.unsubscribe(sessionId, subscriber);
|
|
258
|
+
context.subscriptions.clear();
|
|
259
|
+
// A terminal that went away can't be driving anything; release waiters.
|
|
260
|
+
for (const entry of context.desktops.values()) {
|
|
261
|
+
entry.pendingYield?.();
|
|
262
|
+
entry.unregister();
|
|
263
|
+
}
|
|
264
|
+
context.desktops.clear();
|
|
265
|
+
context.proxy.closeAll();
|
|
266
|
+
context.peer.close();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async dispatch(method, params, context) {
|
|
270
|
+
if (!isMethodName(method))
|
|
271
|
+
throw new RpcError(RPC_METHOD_NOT_FOUND, `unknown method ${method}`);
|
|
272
|
+
// The terminal shim's methods launch local processes with a given environment: never over the network.
|
|
273
|
+
if (method.startsWith("desktop.") && !context.local)
|
|
274
|
+
throw RpcError.app("forbidden", `${method} is only available locally`);
|
|
275
|
+
const parsed = methods[method].params.safeParse(params ?? {});
|
|
276
|
+
if (!parsed.success) {
|
|
277
|
+
throw new RpcError(RPC_INVALID_PARAMS, `invalid params for ${method}`, {
|
|
278
|
+
code: "invalid_params",
|
|
279
|
+
issues: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const handler = this.handlers[method];
|
|
283
|
+
try {
|
|
284
|
+
return await handler(parsed.data, context);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (!(error instanceof RpcError))
|
|
288
|
+
this.options.log(`[rpc] ${method} failed: ${error instanceof Error ? error.stack : String(error)}`);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//# sourceMappingURL=server.js.map
|