skydive-cli 0.2.0 → 0.3.0

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,10 +1,66 @@
1
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";
2
+ import { a as errorMessage, t as HttpError } from "./rest-BlN_uWmL.mjs";
3
+ import os from "node:os";
4
4
  import { z } from "zod";
5
5
  import { spawn } from "node:child_process";
6
6
  import { WebSocket } from "ws";
7
7
 
8
+ //#region src/chat/portal/machine.ts
9
+ /**
10
+ * Identity this machine registers under when the CLI shares it via the portal.
11
+ *
12
+ * The `-cli` suffix / `(CLI)` label keep a CLI-shared machine a DISTINCT portal
13
+ * device from the same host's Skydive Desktop app. `portal_device` is unique on
14
+ * (org, user, machineName), and directives route to whichever socket holds the
15
+ * device — if the CLI and desktop registered the same name they'd share a
16
+ * device row and both execute every directive. Distinct names also make the
17
+ * grant UI unambiguous about which surface is being authorized.
18
+ */
19
+ function machineIdentity() {
20
+ const host = (os.hostname() || "machine").trim().replace(/\.local$/i, "") || "machine";
21
+ return {
22
+ machineName: `${host}-cli`,
23
+ friendlyName: `${host} (CLI)`
24
+ };
25
+ }
26
+ const INHERITED_ENV = [
27
+ "HOME",
28
+ "USER",
29
+ "LOGNAME",
30
+ "SHELL",
31
+ "LANG",
32
+ "LC_ALL",
33
+ "TMPDIR",
34
+ "TERM",
35
+ "PATH"
36
+ ];
37
+ function buildEnv(extra) {
38
+ const env = {};
39
+ for (const key of INHERITED_ENV) {
40
+ const value = process.env[key];
41
+ if (value !== void 0) env[key] = value;
42
+ }
43
+ if (extra) for (const [key, value] of Object.entries(extra)) env[key] = value;
44
+ return env;
45
+ }
46
+ /**
47
+ * Build the desktop-portal WebSocket URL from the chat origin. Mirrors the Rust
48
+ * desktop client: http→ws, https→wss, scheme-less defaults to wss, and the
49
+ * machine/label ride as query pairs (percent-encoded by URL).
50
+ */
51
+ function portalWsUrl(appUrl, machine, label) {
52
+ const base = appUrl.replace(/\/+$/, "");
53
+ let wsBase;
54
+ if (base.startsWith("https://")) wsBase = `wss://${base.slice(8)}`;
55
+ else if (base.startsWith("http://")) wsBase = `ws://${base.slice(7)}`;
56
+ else wsBase = `wss://${base}`;
57
+ const url = new URL(`${wsBase}/api/v1/portal/desktop`);
58
+ url.searchParams.set("machine", machine);
59
+ url.searchParams.set("label", label);
60
+ return url.toString();
61
+ }
62
+
63
+ //#endregion
8
64
  //#region ../portal-protocol/src/index.ts
9
65
  const MAX_WS_FRAME_BYTES = 16 * 1024 * 1024;
10
66
  const T_DATA = 1;
@@ -31,7 +87,8 @@ const ctrlMessageSchema = z.discriminatedUnion("t", [
31
87
  z.object({
32
88
  t: z.literal("open"),
33
89
  argv: z.array(z.string()),
34
- env: z.record(z.string()).nullable()
90
+ env: z.record(z.string()).nullable(),
91
+ conversationId: z.string().nullable().optional()
35
92
  }),
36
93
  z.object({ t: z.literal("stdin_eof") }),
37
94
  z.object({ t: z.literal("pause") }),
@@ -103,7 +160,7 @@ var JobManager = class {
103
160
  handleCtrl(id, msg) {
104
161
  switch (msg.t) {
105
162
  case "open":
106
- this.startJob(id, msg.argv, msg.env);
163
+ this.startJob(id, msg.argv, msg.env, msg.conversationId ?? null);
107
164
  return;
108
165
  case "stdin_eof":
109
166
  this.jobs.get(id)?.child.stdin.end();
@@ -133,7 +190,7 @@ var JobManager = class {
133
190
  job.child.stderr.resume();
134
191
  }
135
192
  }
136
- startJob(id, argv, env) {
193
+ startJob(id, argv, env, conversationId) {
137
194
  const [program, ...args] = argv;
138
195
  if (!program) {
139
196
  this.opts.send(encodeCtrl(id, {
@@ -145,7 +202,7 @@ var JobManager = class {
145
202
  let child;
146
203
  try {
147
204
  child = spawn(program, args, {
148
- cwd: this.opts.cwd,
205
+ cwd: this.opts.resolveCwd(conversationId),
149
206
  env: buildEnv(env),
150
207
  stdio: [
151
208
  "pipe",
@@ -194,6 +251,89 @@ var JobManager = class {
194
251
  }
195
252
  };
196
253
 
254
+ //#endregion
255
+ //#region src/chat/portal/api.ts
256
+ /**
257
+ * The portal's session-authed REST surface, shared by `PortalClient` (the
258
+ * TUI/`portal open` connection) and the `skydive portal` management
259
+ * commands, so the endpoint contracts and response schemas live in exactly
260
+ * one place.
261
+ */
262
+ const deviceSchema = z.object({
263
+ id: z.string(),
264
+ machineName: z.string(),
265
+ friendlyName: z.string(),
266
+ connected: z.boolean(),
267
+ lastSeen: z.string().nullable(),
268
+ grantedAgentIds: z.array(z.string())
269
+ });
270
+ const devicesResponseSchema = z.object({
271
+ devices: z.array(deviceSchema),
272
+ agents: z.array(z.object({
273
+ id: z.string(),
274
+ name: z.string()
275
+ }))
276
+ });
277
+ const deviceTokenSchema = z.object({ token: z.string().min(1) });
278
+ async function portalFetch(auth, path, init) {
279
+ const res = await fetch(`${auth.appUrl}${path}`, {
280
+ method: init.method,
281
+ headers: {
282
+ authorization: `Bearer ${auth.sessionToken}`,
283
+ accept: "application/json",
284
+ ...init.body ? { "content-type": "application/json" } : {}
285
+ },
286
+ ...init.body ? { body: init.body } : {}
287
+ });
288
+ if (!res.ok) {
289
+ const body = await res.text().catch(() => "");
290
+ throw new HttpError(res.status, body);
291
+ }
292
+ return res.json();
293
+ }
294
+ async function fetchPortalDevices(auth) {
295
+ const json = await portalFetch(auth, "/api/v1/portal/devices", { method: "GET" });
296
+ return devicesResponseSchema.parse(json);
297
+ }
298
+ const registerResponseSchema = z.object({ device: z.object({ id: z.string() }) });
299
+ /**
300
+ * Register this machine's device row without connecting. Connecting registers
301
+ * as a side effect; this covers granting an agent on a machine that has never
302
+ * shared yet (the grant references the device row).
303
+ */
304
+ async function registerPortalDevice(auth, { machineName, friendlyName }) {
305
+ const json = await portalFetch(auth, "/api/v1/portal/devices", {
306
+ method: "POST",
307
+ body: JSON.stringify({
308
+ machineName,
309
+ friendlyName
310
+ })
311
+ });
312
+ return registerResponseSchema.parse(json).device;
313
+ }
314
+ /** Short-lived token the machine presents when dialing the portal WebSocket. */
315
+ async function mintPortalDeviceToken(auth) {
316
+ const json = await portalFetch(auth, "/api/v1/portal/device-token", { method: "POST" });
317
+ return deviceTokenSchema.parse(json).token;
318
+ }
319
+ async function grantPortalAccess(auth, { deviceId, agentId }) {
320
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants`, {
321
+ method: "POST",
322
+ body: JSON.stringify({ agentId })
323
+ });
324
+ }
325
+ async function revokePortalAccess(auth, { deviceId, agentId }) {
326
+ await portalFetch(auth, `/api/v1/portal/devices/${encodeURIComponent(deviceId)}/grants/${encodeURIComponent(agentId)}`, { method: "DELETE" });
327
+ }
328
+ /**
329
+ * The device row for a given machine identity. Matching is by `machineName`
330
+ * equality — the stable per-surface handle (`<host>-cli` vs the desktop's
331
+ * `<host>`), not the display label.
332
+ */
333
+ function findThisDevice(devices, machineName) {
334
+ return devices.find((device) => device.machineName === machineName) ?? null;
335
+ }
336
+
197
337
  //#endregion
198
338
  //#region src/chat/portal/client.ts
199
339
  const INITIAL_BACKOFF_MS = 500;
@@ -309,7 +449,7 @@ var PortalClient = class {
309
449
  });
310
450
  this.ws = ws;
311
451
  const jobs = new JobManager({
312
- cwd: this.opts.cwd,
452
+ resolveCwd: this.opts.resolveCwd,
313
453
  send: (frame) => {
314
454
  if (ws.readyState === WebSocket.OPEN) ws.send(frame);
315
455
  }
@@ -363,4 +503,4 @@ function sleep(ms) {
363
503
  }
364
504
 
365
505
  //#endregion
366
- export { PortalClient as t };
506
+ export { registerPortalDevice as a, grantPortalAccess as i, fetchPortalDevices as n, revokePortalAccess as o, findThisDevice as r, machineIdentity as s, PortalClient as t };
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-BlN_uWmL.mjs";
3
+ import { t as PortalClient } from "./client-Dc7GZ3PG.mjs";
4
+
5
+ export { PortalClient };
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import "./rest-BlN_uWmL.mjs";
3
+ import "./client-Dc7GZ3PG.mjs";
4
+ import { a as runPortalDaemon, i as queryDaemonStatus, n as ensureDaemonRunning, o as stopDaemon, r as isDaemonListening, t as PortalDaemon } from "./daemon-Co4CtpXZ.mjs";
5
+
6
+ export { runPortalDaemon };