skydive-cli 0.1.0-beta.260 → 0.1.0-beta.276

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.
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { A as getSavedTheme, C as themesForMode, D as DEFAULT_API_URL, E as setActiveWorkspace, M as saveTheme, O as DEFAULT_APP_URL, S as themeVersion, T as listWorkspaces, _ as noColorRequested, a as parseOauthConnectParams, b as themeMode, c as parseConnectCard, d as HttpError, f as createRestClient, g as monoTheme, h as findTheme, i as parseExternalOauthConnectParams, j as resolveWebUrl, k as getConfigPath, l as errorMessage, m as applyTheme, n as MASK_CHAR, o as reconcileMaskedInput, p as DEFAULT_THEME_ID, r as cardActionErrorMessage, s as resolveConnectUrl, t as resolveAgent, u as isRecord, v as theme, w as getActiveWorkspaceId, x as themeModeFromColorFgBg, y as themeForMode } from "./bin.mjs";
2
+ import { A as themesForMode, C as monoTheme, D as themeMode, E as themeForMode, F as DEFAULT_APP_URL, I as getConfigPath, L as getSavedTheme, M as listWorkspaces, N as setActiveWorkspace, O as themeModeFromColorFgBg, P as DEFAULT_API_URL, R as resolveWebUrl, S as findTheme, T as theme, _ as isRecord, b as DEFAULT_THEME_ID, c as resolveAgent, d as parseExternalOauthConnectParams, f as parseOauthConnectParams, g as errorMessage, h as parseConnectCard, j as getActiveWorkspaceId, k as themeVersion, l as MASK_CHAR, m as resolveConnectUrl, p as reconcileMaskedInput, u as cardActionErrorMessage, v as HttpError, w as noColorRequested, x as applyTheme, y as createRestClient, z as saveTheme } from "./bin.mjs";
3
+ import { t as PortalClient } from "./client-_OL8-XGH.mjs";
3
4
  import path, { basename, isAbsolute, join, win32 } from "node:path";
4
5
  import { z } from "zod";
5
6
  import open from "open";
6
7
  import { execFile, spawn } from "node:child_process";
7
8
  import { createHash } from "node:crypto";
8
9
  import { constants } from "node:fs";
10
+ import * as os$1 from "node:os";
11
+ import { homedir, platform, release, tmpdir } from "node:os";
9
12
  import { MarkdownRenderable, RenderableEvents, SyntaxStyle, createCliRenderer, decodePasteBytes, detectLinks } from "@opentui/core";
10
13
  import { createRoot, extend, useKeyboard, usePaste, useRenderer, useTerminalDimensions } from "@opentui/react";
11
14
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
12
15
  import { create } from "zustand";
13
16
  import { WebSocket } from "ws";
14
- import * as os$1 from "node:os";
15
- import os, { homedir, platform, release, tmpdir } from "node:os";
16
17
  import { createConnection } from "node:net";
17
18
  import { access, appendFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
18
19
  import { Fragment, jsx, jsxs } from "@opentui/react/jsx-runtime";
@@ -158,448 +159,6 @@ async function resolveInitialScreen({ rest, agentSelector }) {
158
159
  };
159
160
  }
160
161
 
161
- //#endregion
162
- //#region ../portal-protocol/src/index.ts
163
- const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
164
- const T_DATA = 1;
165
- const T_CTRL = 2;
166
- const STREAM = {
167
- stdout: 0,
168
- stderr: 1,
169
- stdin: 2
170
- };
171
- const uuidToBytes = (id) => Buffer.from(id.replace(/-/g, ""), "hex");
172
- const bytesToUuid = (b) => {
173
- const h = b.toString("hex");
174
- return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
175
- };
176
- function encodeData(id, stream, seq, payload) {
177
- const head = Buffer.allocUnsafe(22);
178
- head[0] = T_DATA;
179
- uuidToBytes(id).copy(head, 1);
180
- head[17] = stream;
181
- head.writeUInt32BE(seq >>> 0, 18);
182
- return Buffer.concat([head, payload]);
183
- }
184
- const ctrlMessageSchema = z.discriminatedUnion("t", [
185
- z.object({
186
- t: z.literal("open"),
187
- argv: z.array(z.string()),
188
- env: z.record(z.string()).nullable()
189
- }),
190
- z.object({ t: z.literal("stdin_eof") }),
191
- z.object({ t: z.literal("pause") }),
192
- z.object({ t: z.literal("resume") }),
193
- z.object({ t: z.literal("cancel") }),
194
- z.object({
195
- t: z.literal("close"),
196
- exitCode: z.number()
197
- }),
198
- z.object({
199
- t: z.literal("error"),
200
- message: z.string()
201
- })
202
- ]);
203
- function encodeCtrl(id, obj) {
204
- const head = Buffer.allocUnsafe(17);
205
- head[0] = T_CTRL;
206
- uuidToBytes(id).copy(head, 1);
207
- return Buffer.concat([head, Buffer.from(JSON.stringify(obj), "utf8")]);
208
- }
209
- function decodeFrame(frame) {
210
- const id = bytesToUuid(frame.subarray(1, 17));
211
- if (frame[0] === T_DATA) return {
212
- kind: "data",
213
- id,
214
- stream: frame[17] ?? 0,
215
- seq: frame.readUInt32BE(18),
216
- payload: frame.subarray(22)
217
- };
218
- return {
219
- kind: "ctrl",
220
- id,
221
- obj: ctrlMessageSchema.parse(JSON.parse(frame.subarray(17).toString("utf8")))
222
- };
223
- }
224
-
225
- //#endregion
226
- //#region src/chat/portal/machine.ts
227
- /**
228
- * Identity this machine registers under when the CLI shares it via the portal.
229
- *
230
- * The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
231
- * device from the same host's Skydive Desktop app. `portal_device` is unique on
232
- * (org, user, machineName), and directives route to whichever socket holds the
233
- * device — if the CLI and desktop registered the same name they'd share a
234
- * device row and both execute every directive. Distinct names also make the
235
- * grant UI unambiguous about which surface is being authorized.
236
- */
237
- function machineIdentity() {
238
- const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
239
- return {
240
- machineName: `${host}-cli`,
241
- friendlyName: `${host} (CLI)`
242
- };
243
- }
244
- const INHERITED_ENV = [
245
- "HOME",
246
- "USER",
247
- "LOGNAME",
248
- "SHELL",
249
- "LANG",
250
- "LC_ALL",
251
- "TMPDIR",
252
- "TERM",
253
- "PATH"
254
- ];
255
- function buildEnv(extra) {
256
- const env = {};
257
- for (const key of INHERITED_ENV) {
258
- const value = process.env[key];
259
- if (value !== void 0) env[key] = value;
260
- }
261
- if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
262
- return env;
263
- }
264
- /**
265
- * Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
266
- * desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
267
- * machine/label ride as query pairs (percent-encoded by URL).
268
- */
269
- function portalWsUrl(appUrl, machine, label) {
270
- const base = appUrl.replace(/\/+$/, "");
271
- let wsBase;
272
- if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
273
- else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
274
- else wsBase = `wss://${base}`;
275
- const url = new URL(`${wsBase}/api/v1/portal/desktop`);
276
- url.searchParams.set("machine", machine);
277
- url.searchParams.set("label", label);
278
- return url.toString();
279
- }
280
-
281
- //#endregion
282
- //#region src/chat/portal/exec.ts
283
- /**
284
- * Runs portal `exec` directives locally. Each `open` spawns a child process
285
- * whose stdout/stderr stream back as data frames and whose stdin is fed by
286
- * inbound data frames, with pause/resume backpressure and cancel/teardown that
287
- * kill the child. This is the TypeScript counterpart of the desktop's Rust
288
- * `portal/mod.rs` job machinery, minus the connection supervision (which lives
289
- * in the client).
290
- */
291
- var JobManager = class {
292
- jobs = /* @__PURE__ */ new Map();
293
- constructor(opts) {
294
- this.opts = opts;
295
- }
296
- handleFrame(raw) {
297
- let decoded;
298
- try {
299
- decoded = decodeFrame(raw);
300
- } catch (_error) {
301
- return;
302
- }
303
- if (decoded.kind === "ctrl") this.handleCtrl(decoded.id, decoded.obj);
304
- else if (decoded.stream === STREAM.stdin) this.jobs.get(decoded.id)?.child.stdin.write(decoded.payload);
305
- }
306
- killAll() {
307
- for (const job of this.jobs.values()) {
308
- job.settled = true;
309
- job.child.kill("SIGKILL");
310
- }
311
- this.jobs.clear();
312
- }
313
- handleCtrl(id, msg) {
314
- switch (msg.t) {
315
- case "open":
316
- this.startJob(id, msg.argv, msg.env);
317
- return;
318
- case "stdin_eof":
319
- this.jobs.get(id)?.child.stdin.end();
320
- return;
321
- case "pause":
322
- this.setPaused(id, true);
323
- return;
324
- case "resume":
325
- this.setPaused(id, false);
326
- return;
327
- case "cancel":
328
- this.jobs.get(id)?.child.kill("SIGKILL");
329
- return;
330
- case "close":
331
- case "error": return;
332
- default: return msg;
333
- }
334
- }
335
- setPaused(id, paused) {
336
- const job = this.jobs.get(id);
337
- if (!job) return;
338
- if (paused) {
339
- job.child.stdout.pause();
340
- job.child.stderr.pause();
341
- } else {
342
- job.child.stdout.resume();
343
- job.child.stderr.resume();
344
- }
345
- }
346
- startJob(id, argv, env) {
347
- const [program, ...args] = argv;
348
- if (!program) {
349
- this.opts.send(encodeCtrl(id, {
350
- t: "error",
351
- message: "empty argv"
352
- }));
353
- return;
354
- }
355
- let child;
356
- try {
357
- child = spawn(program, args, {
358
- cwd: this.opts.cwd,
359
- env: buildEnv(env),
360
- stdio: [
361
- "pipe",
362
- "pipe",
363
- "pipe"
364
- ]
365
- });
366
- } catch (err) {
367
- this.opts.send(encodeCtrl(id, {
368
- t: "error",
369
- message: `spawn failed: ${errorMessage(err)}`
370
- }));
371
- return;
372
- }
373
- const job = {
374
- child,
375
- seq: 0,
376
- settled: false
377
- };
378
- this.jobs.set(id, job);
379
- child.on("error", (err) => {
380
- if (job.settled) return;
381
- job.settled = true;
382
- this.jobs.delete(id);
383
- this.opts.send(encodeCtrl(id, {
384
- t: "error",
385
- message: `spawn failed: ${errorMessage(err)}`
386
- }));
387
- });
388
- child.stdout.on("data", (chunk) => this.sendData(job, id, STREAM.stdout, chunk));
389
- child.stderr.on("data", (chunk) => this.sendData(job, id, STREAM.stderr, chunk));
390
- child.on("close", (code) => {
391
- if (job.settled) return;
392
- job.settled = true;
393
- this.jobs.delete(id);
394
- this.opts.send(encodeCtrl(id, {
395
- t: "close",
396
- exitCode: code ?? -1
397
- }));
398
- });
399
- }
400
- sendData(job, id, stream, chunk) {
401
- if (job.settled) return;
402
- this.opts.send(encodeData(id, stream, job.seq, chunk));
403
- job.seq = job.seq + 1 >>> 0;
404
- }
405
- };
406
-
407
- //#endregion
408
- //#region src/chat/portal/client.ts
409
- const deviceTokenSchema = z.object({ token: z.string().min(1) });
410
- const devicesSchema = z.object({ devices: z.array(z.object({
411
- id: z.string(),
412
- machineName: z.string(),
413
- grantedAgentIds: z.array(z.string())
414
- })) });
415
- const INITIAL_BACKOFF_MS = 500;
416
- const MAX_BACKOFF_MS = 1e4;
417
- /**
418
- * Shares the local machine with agents over the portal: dials OUT to the api's
419
- * desktop-portal WebSocket (authenticating with a short-lived device token
420
- * minted from the CLI session), then runs inbound `exec` directives via a
421
- * `JobManager`. Reconnects with backoff while enabled; disabling drops presence
422
- * and kills any in-flight children. No inbound port is ever opened.
423
- *
424
- * Access stays default-deny: connecting only makes the machine reachable — an
425
- * agent can't run anything until the user grants it (`grantAgent`).
426
- */
427
- var PortalClient = class {
428
- enabled = false;
429
- disposed = false;
430
- ws = null;
431
- jobs = null;
432
- status = "off";
433
- error = null;
434
- deviceId = null;
435
- granted = /* @__PURE__ */ new Set();
436
- machineName;
437
- friendlyName;
438
- constructor(opts) {
439
- this.opts = opts;
440
- const identity = machineIdentity();
441
- this.machineName = identity.machineName;
442
- this.friendlyName = identity.friendlyName;
443
- }
444
- isEnabled() {
445
- return this.enabled;
446
- }
447
- isGranted(agentId) {
448
- return this.granted.has(agentId);
449
- }
450
- enable() {
451
- if (this.enabled || this.disposed) return;
452
- this.enabled = true;
453
- this.error = null;
454
- this.connectLoop();
455
- }
456
- disable() {
457
- if (!this.enabled) return;
458
- this.enabled = false;
459
- this.jobs?.killAll();
460
- this.ws?.close();
461
- this.ws = null;
462
- this.deviceId = null;
463
- this.granted = /* @__PURE__ */ new Set();
464
- this.setStatus("off");
465
- }
466
- /**
467
- * Tear down for good (app quit). Kills children synchronously and closes the
468
- * socket so it stops holding the event loop open — otherwise the process
469
- * would hang after the TUI is destroyed.
470
- */
471
- dispose() {
472
- this.disposed = true;
473
- this.enabled = false;
474
- this.jobs?.killAll();
475
- this.jobs = null;
476
- this.ws?.close();
477
- this.ws = null;
478
- }
479
- /** Grant one agent access to this machine (default-deny; user-initiated). */
480
- async grantAgent(agentId) {
481
- const deviceId = await this.ensureDeviceId();
482
- const res = await this.fetchJson(`/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
483
- method: "POST",
484
- body: JSON.stringify({ agentId })
485
- });
486
- if (!res.ok) throw new Error(`grant failed (${res.status}): ${await shortBody(res)}`);
487
- this.granted.add(agentId);
488
- this.emit();
489
- }
490
- setStatus(status, error = null) {
491
- this.status = status;
492
- this.error = error;
493
- this.emit();
494
- }
495
- emit() {
496
- this.opts.onState({
497
- status: this.status,
498
- machineName: this.machineName,
499
- friendlyName: this.friendlyName,
500
- error: this.error,
501
- grantedAgentIds: [...this.granted]
502
- });
503
- }
504
- async connectLoop() {
505
- let backoff = INITIAL_BACKOFF_MS;
506
- while (this.enabled && !this.disposed) {
507
- this.setStatus("connecting");
508
- try {
509
- const token = await this.mintDeviceToken();
510
- await this.runConnection(token);
511
- backoff = INITIAL_BACKOFF_MS;
512
- } catch (err) {
513
- if (!this.enabled || this.disposed) break;
514
- this.setStatus("error", errorMessage(err));
515
- }
516
- if (!this.enabled || this.disposed) break;
517
- await sleep(backoff);
518
- backoff = Math.min(backoff * 2, MAX_BACKOFF_MS);
519
- }
520
- }
521
- runConnection(token) {
522
- return new Promise((resolve) => {
523
- const ws = new WebSocket(portalWsUrl(this.opts.appUrl, this.machineName, this.friendlyName), {
524
- headers: { authorization: `Bearer ${token}` },
525
- maxPayload: MAX_WS_FRAME_BYTES
526
- });
527
- this.ws = ws;
528
- const jobs = new JobManager({
529
- cwd: this.opts.cwd,
530
- send: (frame) => {
531
- if (ws.readyState === WebSocket.OPEN) ws.send(frame);
532
- }
533
- });
534
- this.jobs = jobs;
535
- ws.on("open", () => {
536
- this.setStatus("connected");
537
- this.refreshDevice();
538
- });
539
- ws.on("message", (data, isBinary) => {
540
- if (isBinary) jobs.handleFrame(toBuffer$1(data));
541
- });
542
- ws.on("error", (err) => {
543
- this.error = errorMessage(err);
544
- });
545
- ws.on("close", () => {
546
- jobs.killAll();
547
- if (this.jobs === jobs) this.jobs = null;
548
- if (this.ws === ws) this.ws = null;
549
- resolve();
550
- });
551
- });
552
- }
553
- async mintDeviceToken() {
554
- const res = await this.fetchJson("/api/v1/portal/device-token", { method: "POST" });
555
- if (!res.ok) throw new Error(`device-token failed (${res.status}): ${await shortBody(res)}`);
556
- return deviceTokenSchema.parse(await res.json()).token;
557
- }
558
- async ensureDeviceId() {
559
- if (this.deviceId) return this.deviceId;
560
- for (let attempt = 0; attempt < 10; attempt += 1) {
561
- await this.refreshDevice();
562
- if (this.deviceId) return this.deviceId;
563
- await sleep(300);
564
- }
565
- throw new Error("this machine is not connected yet");
566
- }
567
- async refreshDevice() {
568
- try {
569
- const res = await this.fetchJson("/api/v1/portal/devices", { method: "GET" });
570
- if (!res.ok) return;
571
- const { devices } = devicesSchema.parse(await res.json());
572
- const mine = devices.find((device) => device.machineName === this.machineName);
573
- if (!mine) return;
574
- this.deviceId = mine.id;
575
- this.granted = new Set(mine.grantedAgentIds);
576
- this.emit();
577
- } catch (_error) {}
578
- }
579
- fetchJson(path, init) {
580
- return fetch(`${this.opts.appUrl}${path}`, {
581
- method: init.method,
582
- headers: {
583
- authorization: `Bearer ${this.opts.sessionToken}`,
584
- accept: "application/json",
585
- ...init.body ? { "content-type": "application/json" } : {}
586
- },
587
- ...init.body ? { body: init.body } : {}
588
- });
589
- }
590
- };
591
- function toBuffer$1(data) {
592
- if (Buffer.isBuffer(data)) return data;
593
- if (Array.isArray(data)) return Buffer.concat(data);
594
- return Buffer.from(data);
595
- }
596
- function shortBody(res) {
597
- return res.text().then((text) => text.slice(0, 120)).catch(() => "");
598
- }
599
- function sleep(ms) {
600
- return new Promise((resolve) => setTimeout(resolve, ms));
601
- }
602
-
603
162
  //#endregion
604
163
  //#region src/chat/portal/use-portal.ts
605
164
  /**