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.
@@ -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,6 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-CamHVOce.mjs";
3
+ import "./api-CDTKq_5Q.mjs";
4
+ import { t as PortalClient } from "./client-B3ZhhF7e.mjs";
5
+
6
+ export { PortalClient };
@@ -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 };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-CamHVOce.mjs";
3
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DausK_KZ.mjs";
4
+
5
+ export { messageGet, readStdin, resolveAgent, runPrint };