skydive-cli 0.1.0 → 0.2.0-beta.421
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/CHANGELOG.md +68 -0
- package/README.md +271 -28
- package/dist/js/api-CDTKq_5Q.mjs +145 -0
- package/dist/js/bin.mjs +1752 -1359
- package/dist/js/boot-CQLDUeZ9.mjs +9827 -0
- package/dist/js/client-B3ZhhF7e.mjs +366 -0
- package/dist/js/client-CpEvH2Pq.mjs +169 -0
- package/dist/js/client-D6NAkL9e.mjs +6 -0
- package/dist/js/output-B4cW10Ph.mjs +27 -0
- package/dist/js/print-B6AO13SA.mjs +5 -0
- package/dist/js/print-DausK_KZ.mjs +484 -0
- package/dist/js/print-share-D7OSxvE2.mjs +41 -0
- package/dist/js/raw-pty-2_VA1kw_.mjs +5 -0
- package/dist/js/raw-pty-GAvxm2ol.mjs +99 -0
- package/dist/js/rest-COkLEZOB.mjs +4 -0
- package/dist/js/{print-BHbFMxQv.mjs → rest-CamHVOce.mjs} +173 -158
- package/dist/js/theme-CuQhvqzN.mjs +990 -0
- package/dist/js/util-CeisaZVY.mjs +15 -0
- package/package.json +20 -9
- package/dist/js/boot-ChlVx-ts.mjs +0 -5525
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as errorMessage } from "./util-CeisaZVY.mjs";
|
|
3
|
+
import { c as machineIdentity, i as mintPortalDeviceToken, l as portalWsUrl, n as findThisDevice, r as grantPortalAccess, s as buildEnv, t as fetchPortalDevices } from "./api-CDTKq_5Q.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { WebSocket } from "ws";
|
|
7
|
+
|
|
8
|
+
//#region ../portal-protocol/src/index.ts
|
|
9
|
+
const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
|
|
10
|
+
const T_DATA = 1;
|
|
11
|
+
const T_CTRL = 2;
|
|
12
|
+
const STREAM = {
|
|
13
|
+
stdout: 0,
|
|
14
|
+
stderr: 1,
|
|
15
|
+
stdin: 2
|
|
16
|
+
};
|
|
17
|
+
const uuidToBytes = (id) => Buffer.from(id.replace(/-/g, ""), "hex");
|
|
18
|
+
const bytesToUuid = (b) => {
|
|
19
|
+
const h = b.toString("hex");
|
|
20
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
21
|
+
};
|
|
22
|
+
function encodeData(id, stream, seq, payload) {
|
|
23
|
+
const head = Buffer.allocUnsafe(22);
|
|
24
|
+
head[0] = T_DATA;
|
|
25
|
+
uuidToBytes(id).copy(head, 1);
|
|
26
|
+
head[17] = stream;
|
|
27
|
+
head.writeUInt32BE(seq >>> 0, 18);
|
|
28
|
+
return Buffer.concat([head, payload]);
|
|
29
|
+
}
|
|
30
|
+
const ctrlMessageSchema = z.discriminatedUnion("t", [
|
|
31
|
+
z.object({
|
|
32
|
+
t: z.literal("open"),
|
|
33
|
+
argv: z.array(z.string()),
|
|
34
|
+
env: z.record(z.string()).nullable()
|
|
35
|
+
}),
|
|
36
|
+
z.object({ t: z.literal("stdin_eof") }),
|
|
37
|
+
z.object({ t: z.literal("pause") }),
|
|
38
|
+
z.object({ t: z.literal("resume") }),
|
|
39
|
+
z.object({ t: z.literal("cancel") }),
|
|
40
|
+
z.object({
|
|
41
|
+
t: z.literal("close"),
|
|
42
|
+
exitCode: z.number()
|
|
43
|
+
}),
|
|
44
|
+
z.object({
|
|
45
|
+
t: z.literal("error"),
|
|
46
|
+
message: z.string()
|
|
47
|
+
})
|
|
48
|
+
]);
|
|
49
|
+
function encodeCtrl(id, obj) {
|
|
50
|
+
const head = Buffer.allocUnsafe(17);
|
|
51
|
+
head[0] = T_CTRL;
|
|
52
|
+
uuidToBytes(id).copy(head, 1);
|
|
53
|
+
return Buffer.concat([head, Buffer.from(JSON.stringify(obj), "utf8")]);
|
|
54
|
+
}
|
|
55
|
+
function decodeFrame(frame) {
|
|
56
|
+
const id = bytesToUuid(frame.subarray(1, 17));
|
|
57
|
+
if (frame[0] === T_DATA) return {
|
|
58
|
+
kind: "data",
|
|
59
|
+
id,
|
|
60
|
+
stream: frame[17] ?? 0,
|
|
61
|
+
seq: frame.readUInt32BE(18),
|
|
62
|
+
payload: frame.subarray(22)
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
kind: "ctrl",
|
|
66
|
+
id,
|
|
67
|
+
obj: ctrlMessageSchema.parse(JSON.parse(frame.subarray(17).toString("utf8")))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/chat/portal/exec.ts
|
|
73
|
+
/**
|
|
74
|
+
* Runs portal `exec` directives locally. Each `open` spawns a child process
|
|
75
|
+
* whose stdout/stderr stream back as data frames and whose stdin is fed by
|
|
76
|
+
* inbound data frames, with pause/resume backpressure and cancel/teardown that
|
|
77
|
+
* kill the child. This is the TypeScript counterpart of the desktop's Rust
|
|
78
|
+
* `portal/mod.rs` job machinery, minus the connection supervision (which lives
|
|
79
|
+
* in the client).
|
|
80
|
+
*/
|
|
81
|
+
var JobManager = class {
|
|
82
|
+
jobs = /* @__PURE__ */ new Map();
|
|
83
|
+
constructor(opts) {
|
|
84
|
+
this.opts = opts;
|
|
85
|
+
}
|
|
86
|
+
handleFrame(raw) {
|
|
87
|
+
let decoded;
|
|
88
|
+
try {
|
|
89
|
+
decoded = decodeFrame(raw);
|
|
90
|
+
} catch (_error) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (decoded.kind === "ctrl") this.handleCtrl(decoded.id, decoded.obj);
|
|
94
|
+
else if (decoded.stream === STREAM.stdin) this.jobs.get(decoded.id)?.child.stdin.write(decoded.payload);
|
|
95
|
+
}
|
|
96
|
+
killAll() {
|
|
97
|
+
for (const job of this.jobs.values()) {
|
|
98
|
+
job.settled = true;
|
|
99
|
+
job.child.kill("SIGKILL");
|
|
100
|
+
}
|
|
101
|
+
this.jobs.clear();
|
|
102
|
+
}
|
|
103
|
+
handleCtrl(id, msg) {
|
|
104
|
+
switch (msg.t) {
|
|
105
|
+
case "open":
|
|
106
|
+
this.startJob(id, msg.argv, msg.env);
|
|
107
|
+
return;
|
|
108
|
+
case "stdin_eof":
|
|
109
|
+
this.jobs.get(id)?.child.stdin.end();
|
|
110
|
+
return;
|
|
111
|
+
case "pause":
|
|
112
|
+
this.setPaused(id, true);
|
|
113
|
+
return;
|
|
114
|
+
case "resume":
|
|
115
|
+
this.setPaused(id, false);
|
|
116
|
+
return;
|
|
117
|
+
case "cancel":
|
|
118
|
+
this.jobs.get(id)?.child.kill("SIGKILL");
|
|
119
|
+
return;
|
|
120
|
+
case "close":
|
|
121
|
+
case "error": return;
|
|
122
|
+
default: return msg;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
setPaused(id, paused) {
|
|
126
|
+
const job = this.jobs.get(id);
|
|
127
|
+
if (!job) return;
|
|
128
|
+
if (paused) {
|
|
129
|
+
job.child.stdout.pause();
|
|
130
|
+
job.child.stderr.pause();
|
|
131
|
+
} else {
|
|
132
|
+
job.child.stdout.resume();
|
|
133
|
+
job.child.stderr.resume();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
startJob(id, argv, env) {
|
|
137
|
+
const [program, ...args] = argv;
|
|
138
|
+
if (!program) {
|
|
139
|
+
this.opts.send(encodeCtrl(id, {
|
|
140
|
+
t: "error",
|
|
141
|
+
message: "empty argv"
|
|
142
|
+
}));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let child;
|
|
146
|
+
try {
|
|
147
|
+
child = spawn(program, args, {
|
|
148
|
+
cwd: this.opts.cwd,
|
|
149
|
+
env: buildEnv(env),
|
|
150
|
+
stdio: [
|
|
151
|
+
"pipe",
|
|
152
|
+
"pipe",
|
|
153
|
+
"pipe"
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this.opts.send(encodeCtrl(id, {
|
|
158
|
+
t: "error",
|
|
159
|
+
message: `spawn failed: ${errorMessage(err)}`
|
|
160
|
+
}));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const job = {
|
|
164
|
+
child,
|
|
165
|
+
seq: 0,
|
|
166
|
+
settled: false
|
|
167
|
+
};
|
|
168
|
+
this.jobs.set(id, job);
|
|
169
|
+
child.on("error", (err) => {
|
|
170
|
+
if (job.settled) return;
|
|
171
|
+
job.settled = true;
|
|
172
|
+
this.jobs.delete(id);
|
|
173
|
+
this.opts.send(encodeCtrl(id, {
|
|
174
|
+
t: "error",
|
|
175
|
+
message: `spawn failed: ${errorMessage(err)}`
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
child.stdout.on("data", (chunk) => this.sendData(job, id, STREAM.stdout, chunk));
|
|
179
|
+
child.stderr.on("data", (chunk) => this.sendData(job, id, STREAM.stderr, chunk));
|
|
180
|
+
child.on("close", (code) => {
|
|
181
|
+
if (job.settled) return;
|
|
182
|
+
job.settled = true;
|
|
183
|
+
this.jobs.delete(id);
|
|
184
|
+
this.opts.send(encodeCtrl(id, {
|
|
185
|
+
t: "close",
|
|
186
|
+
exitCode: code ?? -1
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
sendData(job, id, stream, chunk) {
|
|
191
|
+
if (job.settled) return;
|
|
192
|
+
this.opts.send(encodeData(id, stream, job.seq, chunk));
|
|
193
|
+
job.seq = job.seq + 1 >>> 0;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/chat/portal/client.ts
|
|
199
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
200
|
+
const MAX_BACKOFF_MS = 1e4;
|
|
201
|
+
/**
|
|
202
|
+
* Shares the local machine with agents over the portal: dials OUT to the api's
|
|
203
|
+
* desktop-portal WebSocket (authenticating with a short-lived device token
|
|
204
|
+
* minted from the CLI session), then runs inbound `exec` directives via a
|
|
205
|
+
* `JobManager`. Reconnects with backoff while enabled; disabling drops presence
|
|
206
|
+
* and kills any in-flight children. No inbound port is ever opened.
|
|
207
|
+
*
|
|
208
|
+
* Access stays default-deny: connecting only makes the machine reachable — an
|
|
209
|
+
* agent can't run anything until the user grants it (`grantAgent`).
|
|
210
|
+
*/
|
|
211
|
+
var PortalClient = class {
|
|
212
|
+
enabled = false;
|
|
213
|
+
disposed = false;
|
|
214
|
+
ws = null;
|
|
215
|
+
jobs = null;
|
|
216
|
+
status = "off";
|
|
217
|
+
error = null;
|
|
218
|
+
deviceId = null;
|
|
219
|
+
granted = /* @__PURE__ */ new Set();
|
|
220
|
+
machineName;
|
|
221
|
+
friendlyName;
|
|
222
|
+
constructor(opts) {
|
|
223
|
+
this.opts = opts;
|
|
224
|
+
const identity = machineIdentity();
|
|
225
|
+
this.machineName = identity.machineName;
|
|
226
|
+
this.friendlyName = identity.friendlyName;
|
|
227
|
+
}
|
|
228
|
+
isEnabled() {
|
|
229
|
+
return this.enabled;
|
|
230
|
+
}
|
|
231
|
+
isGranted(agentId) {
|
|
232
|
+
return this.granted.has(agentId);
|
|
233
|
+
}
|
|
234
|
+
enable() {
|
|
235
|
+
if (this.enabled || this.disposed) return;
|
|
236
|
+
this.enabled = true;
|
|
237
|
+
this.error = null;
|
|
238
|
+
this.connectLoop();
|
|
239
|
+
}
|
|
240
|
+
disable() {
|
|
241
|
+
if (!this.enabled) return;
|
|
242
|
+
this.enabled = false;
|
|
243
|
+
this.jobs?.killAll();
|
|
244
|
+
this.ws?.close();
|
|
245
|
+
this.ws = null;
|
|
246
|
+
this.deviceId = null;
|
|
247
|
+
this.granted = /* @__PURE__ */ new Set();
|
|
248
|
+
this.setStatus("off");
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Tear down for good (app quit). Kills children synchronously and closes the
|
|
252
|
+
* socket so it stops holding the event loop open — otherwise the process
|
|
253
|
+
* would hang after the TUI is destroyed.
|
|
254
|
+
*/
|
|
255
|
+
dispose() {
|
|
256
|
+
this.disposed = true;
|
|
257
|
+
this.enabled = false;
|
|
258
|
+
this.jobs?.killAll();
|
|
259
|
+
this.jobs = null;
|
|
260
|
+
this.ws?.close();
|
|
261
|
+
this.ws = null;
|
|
262
|
+
}
|
|
263
|
+
/** Grant one agent access to this machine (default-deny; user-initiated). */
|
|
264
|
+
async grantAgent(agentId) {
|
|
265
|
+
const deviceId = await this.ensureDeviceId();
|
|
266
|
+
await grantPortalAccess(this.opts, {
|
|
267
|
+
deviceId,
|
|
268
|
+
agentId
|
|
269
|
+
});
|
|
270
|
+
this.granted.add(agentId);
|
|
271
|
+
this.emit();
|
|
272
|
+
}
|
|
273
|
+
setStatus(status, error = null) {
|
|
274
|
+
this.status = status;
|
|
275
|
+
this.error = error;
|
|
276
|
+
this.emit();
|
|
277
|
+
}
|
|
278
|
+
emit() {
|
|
279
|
+
this.opts.onState({
|
|
280
|
+
status: this.status,
|
|
281
|
+
machineName: this.machineName,
|
|
282
|
+
friendlyName: this.friendlyName,
|
|
283
|
+
error: this.error,
|
|
284
|
+
grantedAgentIds: [...this.granted]
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async connectLoop() {
|
|
288
|
+
let backoff = INITIAL_BACKOFF_MS;
|
|
289
|
+
while (this.enabled && !this.disposed) {
|
|
290
|
+
this.setStatus("connecting");
|
|
291
|
+
try {
|
|
292
|
+
const token = await mintPortalDeviceToken(this.opts);
|
|
293
|
+
await this.runConnection(token);
|
|
294
|
+
backoff = INITIAL_BACKOFF_MS;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (!this.enabled || this.disposed) break;
|
|
297
|
+
this.setStatus("error", errorMessage(err));
|
|
298
|
+
}
|
|
299
|
+
if (!this.enabled || this.disposed) break;
|
|
300
|
+
await sleep(backoff);
|
|
301
|
+
backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
runConnection(token) {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
const ws = new WebSocket(portalWsUrl(this.opts.appUrl, this.machineName, this.friendlyName), {
|
|
307
|
+
headers: { authorization: `Bearer ${token}` },
|
|
308
|
+
maxPayload: MAX_WS_FRAME_BYTES
|
|
309
|
+
});
|
|
310
|
+
this.ws = ws;
|
|
311
|
+
const jobs = new JobManager({
|
|
312
|
+
cwd: this.opts.cwd,
|
|
313
|
+
send: (frame) => {
|
|
314
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(frame);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
this.jobs = jobs;
|
|
318
|
+
ws.on("open", () => {
|
|
319
|
+
this.setStatus("connected");
|
|
320
|
+
this.refreshDevice();
|
|
321
|
+
});
|
|
322
|
+
ws.on("message", (data, isBinary) => {
|
|
323
|
+
if (isBinary) jobs.handleFrame(toBuffer(data));
|
|
324
|
+
});
|
|
325
|
+
ws.on("error", (err) => {
|
|
326
|
+
this.error = errorMessage(err);
|
|
327
|
+
});
|
|
328
|
+
ws.on("close", () => {
|
|
329
|
+
jobs.killAll();
|
|
330
|
+
if (this.jobs === jobs) this.jobs = null;
|
|
331
|
+
if (this.ws === ws) this.ws = null;
|
|
332
|
+
resolve();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async ensureDeviceId() {
|
|
337
|
+
if (this.deviceId) return this.deviceId;
|
|
338
|
+
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
339
|
+
await this.refreshDevice();
|
|
340
|
+
if (this.deviceId) return this.deviceId;
|
|
341
|
+
await sleep(300);
|
|
342
|
+
}
|
|
343
|
+
throw new Error("this machine is not connected yet");
|
|
344
|
+
}
|
|
345
|
+
async refreshDevice() {
|
|
346
|
+
try {
|
|
347
|
+
const { devices } = await fetchPortalDevices(this.opts);
|
|
348
|
+
const mine = findThisDevice(devices, this.machineName);
|
|
349
|
+
if (!mine) return;
|
|
350
|
+
this.deviceId = mine.id;
|
|
351
|
+
this.granted = new Set(mine.grantedAgentIds);
|
|
352
|
+
this.emit();
|
|
353
|
+
} catch (_error) {}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
function toBuffer(data) {
|
|
357
|
+
if (Buffer.isBuffer(data)) return data;
|
|
358
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
359
|
+
return Buffer.from(data);
|
|
360
|
+
}
|
|
361
|
+
function sleep(ms) {
|
|
362
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
export { PortalClient as t };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
|
|
4
|
+
//#region ../sandbox-stream-protocol/src/index.ts
|
|
5
|
+
const SANDBOX_STREAM_PATH = "/api/v1/sandbox/stream";
|
|
6
|
+
const FRAME = {
|
|
7
|
+
DATA: 1,
|
|
8
|
+
EXIT: 2,
|
|
9
|
+
ERROR: 3,
|
|
10
|
+
INPUT: 16,
|
|
11
|
+
RESIZE: 17
|
|
12
|
+
};
|
|
13
|
+
const MAX_INPUT_BYTES = 1 * 1024 * 1024;
|
|
14
|
+
/** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
|
|
15
|
+
function streamSpecToQuery(spec) {
|
|
16
|
+
if (spec.mode === "pty") return {
|
|
17
|
+
agentId: spec.agentId,
|
|
18
|
+
mode: "pty",
|
|
19
|
+
cols: String(spec.cols),
|
|
20
|
+
rows: String(spec.rows)
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
agentId: spec.agentId,
|
|
24
|
+
mode: "exec",
|
|
25
|
+
command: spec.command
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function withType(type, payload) {
|
|
29
|
+
const frame = new Uint8Array(1 + payload.length);
|
|
30
|
+
frame[0] = type;
|
|
31
|
+
frame.set(payload, 1);
|
|
32
|
+
return frame;
|
|
33
|
+
}
|
|
34
|
+
/** client → server: keystroke bytes for the pty stdin. */
|
|
35
|
+
function encodeInput(data) {
|
|
36
|
+
return withType(FRAME.INPUT, data);
|
|
37
|
+
}
|
|
38
|
+
/** client → server: the client terminal was resized. */
|
|
39
|
+
function encodeResize(cols, rows) {
|
|
40
|
+
const frame = new Uint8Array(5);
|
|
41
|
+
frame[0] = FRAME.RESIZE;
|
|
42
|
+
const view = new DataView(frame.buffer);
|
|
43
|
+
view.setUint16(1, cols & 65535);
|
|
44
|
+
view.setUint16(3, rows & 65535);
|
|
45
|
+
return frame;
|
|
46
|
+
}
|
|
47
|
+
const view = (frame) => new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
|
|
48
|
+
/**
|
|
49
|
+
* Decode a frame the server sent. Returns null for an empty, unknown, or
|
|
50
|
+
* truncated frame — a peer speaking a newer protocol must not crash us.
|
|
51
|
+
*/
|
|
52
|
+
function decodeServerFrame(frame) {
|
|
53
|
+
const payload = frame.subarray(1);
|
|
54
|
+
switch (frame[0]) {
|
|
55
|
+
case FRAME.DATA: return {
|
|
56
|
+
type: "data",
|
|
57
|
+
payload
|
|
58
|
+
};
|
|
59
|
+
case FRAME.EXIT: return {
|
|
60
|
+
type: "exit",
|
|
61
|
+
code: payload.length >= 4 ? view(frame).getInt32(1) : 0
|
|
62
|
+
};
|
|
63
|
+
case FRAME.ERROR: return {
|
|
64
|
+
type: "error",
|
|
65
|
+
message: new TextDecoder().decode(payload)
|
|
66
|
+
};
|
|
67
|
+
default: return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/chat/sandbox/client.ts
|
|
73
|
+
function wsBase(appUrl) {
|
|
74
|
+
const base = appUrl.replace(/\/+$/, "");
|
|
75
|
+
if (base.startsWith("https://")) return `wss://${base.slice(8)}`;
|
|
76
|
+
if (base.startsWith("http://")) return `ws://${base.slice(7)}`;
|
|
77
|
+
return `wss://${base}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A connected sandbox-stream session. Construct via `openSandboxStream`. Carries
|
|
81
|
+
* the write side (keystrokes / resize for pty mode) and teardown.
|
|
82
|
+
*/
|
|
83
|
+
var SandboxStream = class SandboxStream {
|
|
84
|
+
ws;
|
|
85
|
+
closed = false;
|
|
86
|
+
constructor(ws, onEvent) {
|
|
87
|
+
this.ws = ws;
|
|
88
|
+
let ended = false;
|
|
89
|
+
const emitEnd = (event) => {
|
|
90
|
+
if (ended) return;
|
|
91
|
+
ended = true;
|
|
92
|
+
onEvent(event);
|
|
93
|
+
};
|
|
94
|
+
ws.on("message", (data, isBinary) => {
|
|
95
|
+
if (!isBinary) return;
|
|
96
|
+
const frame = decodeServerFrame(toBuffer(data));
|
|
97
|
+
if (!frame) return;
|
|
98
|
+
switch (frame.type) {
|
|
99
|
+
case "data":
|
|
100
|
+
onEvent({
|
|
101
|
+
type: "data",
|
|
102
|
+
bytes: new Uint8Array(frame.payload)
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
case "exit":
|
|
106
|
+
emitEnd({
|
|
107
|
+
type: "exit",
|
|
108
|
+
code: frame.code
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
case "error":
|
|
112
|
+
emitEnd({
|
|
113
|
+
type: "error",
|
|
114
|
+
message: frame.message
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
let failure = null;
|
|
120
|
+
ws.on("error", (err) => {
|
|
121
|
+
failure = err.message;
|
|
122
|
+
});
|
|
123
|
+
ws.on("close", () => {
|
|
124
|
+
this.closed = true;
|
|
125
|
+
emitEnd({
|
|
126
|
+
type: "close",
|
|
127
|
+
failure
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Feed keystroke bytes to the pty stdin. */
|
|
132
|
+
sendInput(data) {
|
|
133
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
134
|
+
this.ws.send(encodeInput(data));
|
|
135
|
+
}
|
|
136
|
+
/** Notify the pty of a terminal resize. */
|
|
137
|
+
resize(cols, rows) {
|
|
138
|
+
if (this.closed || this.ws.readyState !== WebSocket.OPEN) return;
|
|
139
|
+
this.ws.send(encodeResize(cols, rows));
|
|
140
|
+
}
|
|
141
|
+
close() {
|
|
142
|
+
this.closed = true;
|
|
143
|
+
this.ws.close();
|
|
144
|
+
}
|
|
145
|
+
/** Open a stream. `mode` is 'pty' (interactive) or 'exec' (one-shot). */
|
|
146
|
+
static open(opts) {
|
|
147
|
+
const spec = opts.mode === "pty" ? {
|
|
148
|
+
mode: "pty",
|
|
149
|
+
agentId: opts.agentId,
|
|
150
|
+
cols: opts.cols,
|
|
151
|
+
rows: opts.rows
|
|
152
|
+
} : {
|
|
153
|
+
mode: "exec",
|
|
154
|
+
agentId: opts.agentId,
|
|
155
|
+
command: opts.command
|
|
156
|
+
};
|
|
157
|
+
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
158
|
+
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
159
|
+
return new SandboxStream(new WebSocket(url.toString(), { headers: { authorization: `Bearer ${opts.sessionToken}` } }), opts.onEvent);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function toBuffer(data) {
|
|
163
|
+
if (Buffer.isBuffer(data)) return data;
|
|
164
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
165
|
+
return Buffer.from(data);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
export { SandboxStream as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region src/output.ts
|
|
3
|
+
function output(argv, data) {
|
|
4
|
+
if (argv.json) {
|
|
5
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (typeof data === "string") {
|
|
9
|
+
console.log(data);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(JSON.stringify(data, null, 2));
|
|
13
|
+
}
|
|
14
|
+
function printTable(headers, rows) {
|
|
15
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
16
|
+
const pad = (s, w) => s.padEnd(w);
|
|
17
|
+
const line = (cells) => cells.map((c, i) => pad(c, widths[i] ?? 0)).join(" ");
|
|
18
|
+
console.log(line(headers));
|
|
19
|
+
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
20
|
+
for (const row of rows) console.log(line(row));
|
|
21
|
+
}
|
|
22
|
+
function printError(message) {
|
|
23
|
+
console.error(`Error: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { printError as n, printTable as r, output as t };
|