skydive-cli 0.5.0-beta.5 → 0.5.0-beta.52

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +60 -12
  3. package/dist/js/api-TwLD7ibI.mjs +315 -0
  4. package/dist/js/{billing-blocked-Dgu5-oDy.mjs → billing-blocked-SE6tySLd.mjs} +1 -1
  5. package/dist/js/bin.mjs +658 -279
  6. package/dist/js/{boot-Brwh0it_.mjs → boot-D_VFIRJj.mjs} +3556 -995
  7. package/dist/js/chunk-BbwQpWto.mjs +33 -0
  8. package/dist/js/{client-Cn2af31H.mjs → client-Btq6bMzX.mjs} +108 -2
  9. package/dist/js/client-CkPQG8M1.mjs +5 -0
  10. package/dist/js/client-aFwHzCPG.mjs +963 -0
  11. package/dist/js/daemon-CORAITjj.mjs +7 -0
  12. package/dist/js/{daemon-client-DcuD4v12.mjs → daemon-client-Cfi66Xy9.mjs} +12 -8
  13. package/dist/js/daemon-client-DJPW9cRp.mjs +8 -0
  14. package/dist/js/{daemon-BhArnzeW.mjs → daemon-uzpOdRPL.mjs} +135 -10
  15. package/dist/js/dist-CRtjM7ba.mjs +1750 -0
  16. package/dist/js/forward-Ct8Vd2WW.mjs +208 -0
  17. package/dist/js/{profiler-LLaIFZgn.mjs → install-BBplm-Zp.mjs} +529 -215
  18. package/dist/js/launcher.mjs +49 -0
  19. package/dist/js/localhost-cert-Bn-UBUmj.mjs +67 -0
  20. package/dist/js/{print-ba_0hiV9.mjs → print-Cuao6LN6.mjs} +3 -3
  21. package/dist/js/{print-CbayCa87.mjs → print-DR6Gas-M.mjs} +291 -39
  22. package/dist/js/{print-share-uwUG16Ov.mjs → print-share-BypBUrNh.mjs} +9 -3
  23. package/dist/js/raw-pty-Ci2qFR9F.mjs +5 -0
  24. package/dist/js/{raw-pty-B6mAroiI.mjs → raw-pty-D5PhKZSl.mjs} +1 -1
  25. package/dist/js/rest-CJkBP2Jz.mjs +6 -0
  26. package/dist/js/{rest-BY2nADw5.mjs → rest-DkuT5_oX.mjs} +117 -28
  27. package/dist/js/tls-cert-CLgSQALB.mjs +4 -0
  28. package/dist/js/tls-cert-CV-pwxVN.mjs +67 -0
  29. package/package.json +11 -4
  30. package/dist/js/client-DZstLQ_1.mjs +0 -4
  31. package/dist/js/client-Dd5sMXPv.mjs +0 -620
  32. package/dist/js/daemon-CjzrUvXx.mjs +0 -5
  33. package/dist/js/daemon-client-D8bNXTxu.mjs +0 -6
  34. package/dist/js/raw-pty-3EkG-jjH.mjs +0 -5
  35. package/dist/js/rest-DADJh0bi.mjs +0 -6
  36. /package/dist/js/{billing-blocked-2wju4gC_.mjs → billing-blocked-D3l5kJlX.mjs} +0 -0
  37. /package/dist/js/{http-error-DzyrsLAZ.mjs → http-error-BF2NZZE3.mjs} +0 -0
  38. /package/dist/js/{output-DYzzdXYV.mjs → output-C9mb3sUB.mjs} +0 -0
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import "./client-aFwHzCPG.mjs";
3
+ import { a as runPortalDaemon, c as stopDaemonForHandoff, i as queryDaemonStatus, n as ensureDaemonRunning, o as startPortalDaemon, r as isDaemonListening, s as stopDaemon, t as PortalDaemon } from "./daemon-uzpOdRPL.mjs";
4
+ import "./tls-cert-CV-pwxVN.mjs";
5
+ import "./api-TwLD7ibI.mjs";
6
+
7
+ export { runPortalDaemon };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as LOCAL_PROTOCOL_VERSION, d as encodeLine, f as makeLineParser, n as ensureDaemonRunning, p as parseDaemonMessage, u as daemonPaths } from "./daemon-BhArnzeW.mjs";
2
+ import { d as daemonPaths, f as encodeLine, l as LOCAL_PROTOCOL_VERSION, m as parseDaemonMessage, n as ensureDaemonRunning, p as makeLineParser } from "./daemon-uzpOdRPL.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { connect } from "node:net";
5
5
 
@@ -12,7 +12,7 @@ var PortalDaemonClient = class {
12
12
  lastBind = null;
13
13
  fallbackCwd = null;
14
14
  wantEnabled = false;
15
- pendingGrants = /* @__PURE__ */ new Set();
15
+ pendingGrants = /* @__PURE__ */ new Map();
16
16
  pendingDeclines = /* @__PURE__ */ new Map();
17
17
  grantedAgentIds = /* @__PURE__ */ new Set();
18
18
  constructor(opts) {
@@ -50,9 +50,10 @@ var PortalDaemonClient = class {
50
50
  conversationId: this.lastBind.conversationId,
51
51
  cwd: this.lastBind.cwd
52
52
  });
53
- for (const agentId of this.pendingGrants) this.send({
53
+ for (const [agentId, conversationId] of this.pendingGrants) this.send({
54
54
  t: "grant",
55
- agentId
55
+ agentId,
56
+ conversationId
56
57
  });
57
58
  for (const [agentId, declined] of this.pendingDeclines) this.send({
58
59
  t: "decline",
@@ -157,13 +158,16 @@ var PortalDaemonClient = class {
157
158
  * Authorize one agent to run commands on this machine. Resolves once the
158
159
  * request is sent to the daemon (the daemon performs the grant and pushes the
159
160
  * updated state); kept async so it's a drop-in for the old in-process client's
160
- * awaited `grantAgent`.
161
+ * awaited `grantAgent`. `conversationId` names the conversation whose run
162
+ * asked, so the grant can wake the waiting agent; null when the grant is not
163
+ * answering an in-chat request.
161
164
  */
162
- grantAgent(agentId) {
163
- this.pendingGrants.add(agentId);
165
+ grantAgent(agentId, conversationId) {
166
+ this.pendingGrants.set(agentId, conversationId);
164
167
  this.send({
165
168
  t: "grant",
166
- agentId
169
+ agentId,
170
+ conversationId
167
171
  });
168
172
  return Promise.resolve();
169
173
  }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import "./client-aFwHzCPG.mjs";
3
+ import "./daemon-uzpOdRPL.mjs";
4
+ import "./tls-cert-CV-pwxVN.mjs";
5
+ import "./api-TwLD7ibI.mjs";
6
+ import { t as PortalDaemonClient } from "./daemon-client-Cfi66Xy9.mjs";
7
+
8
+ export { PortalDaemonClient };
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { s as isRecord, t as PortalClient } from "./client-Dd5sMXPv.mjs";
2
+ import { n as isRecord, t as PortalClient } from "./client-aFwHzCPG.mjs";
3
+ import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { z } from "zod";
@@ -20,7 +21,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
20
21
  * the commit time of the built tree is one monotonic clock they share.
21
22
  */
22
23
  function portalDaemonBuild() {
23
- return "1786586981";
24
+ return "1787166259";
24
25
  }
25
26
  /**
26
27
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -175,10 +176,18 @@ const clientCwdSchema = z.object({
175
176
  t: z.literal("cwd"),
176
177
  cwd: z.string().min(1)
177
178
  });
178
- /** `grant` — authorize one agent to reach this machine (user-initiated). */
179
+ /**
180
+ * `grant` — authorize one agent to reach this machine (user-initiated).
181
+ * `conversationId` names the conversation whose run asked for access (the
182
+ * in-chat approval card); the grant carries it upstream so the server wakes
183
+ * the waiting agent. Defaults to null on parse so hellos from CLI builds that
184
+ * predate the field keep granting against a newer daemon — they just skip the
185
+ * wake, same as before the field existed.
186
+ */
179
187
  const clientGrantSchema = z.object({
180
188
  t: z.literal("grant"),
181
- agentId: z.string().min(1)
189
+ agentId: z.string().min(1),
190
+ conversationId: z.string().nullish().default(null)
182
191
  });
183
192
  /**
184
193
  * `decline` — the user answered no to an agent's request for this machine
@@ -211,6 +220,20 @@ const clientStatusSchema = z.object({ t: z.literal("status") });
211
220
  * exits — cutting live exec/tunnel connections, not just refusing new ones.
212
221
  */
213
222
  const clientShutdownSchema = z.object({ t: z.literal("shutdown") });
223
+ /**
224
+ * `shutdown_handoff` — a newer daemon taking over (self-update / newest-build-
225
+ * wins) asks the incumbent to step down WITHOUT dropping the portal connection
226
+ * first. The incumbent frees the control socket and disconnects attached CLIs
227
+ * (they reconnect to the new daemon in ~500ms, unchanged), but KEEPS its
228
+ * outbound portal WebSocket — and thus its presence claim — alive until the
229
+ * new daemon has dialled out and claimed the machine's presence slot, at which
230
+ * point the server supersedes the incumbent's socket and it exits. This is a
231
+ * make-before-break handoff: the presence key is owned by one daemon or the
232
+ * other for the entire takeover, so `exec`/`file write` never sees a
233
+ * `registered but not connected` gap mid-upgrade. A bounded fallback timeout
234
+ * ensures the incumbent still exits if the successor never comes up.
235
+ */
236
+ const clientShutdownHandoffSchema = z.object({ t: z.literal("shutdown_handoff") });
214
237
  const clientMessageSchema = z.discriminatedUnion("t", [
215
238
  clientHelloSchema,
216
239
  clientBindSchema,
@@ -221,7 +244,8 @@ const clientMessageSchema = z.discriminatedUnion("t", [
221
244
  clientDeclineSchema,
222
245
  clientByeSchema,
223
246
  clientStatusSchema,
224
- clientShutdownSchema
247
+ clientShutdownSchema,
248
+ clientShutdownHandoffSchema
225
249
  ]);
226
250
  /**
227
251
  * `state` — the shared portal status, pushed to every attached client so each
@@ -348,6 +372,14 @@ function parseDaemonMessage(line) {
348
372
  */
349
373
  /** Grace period after the last client detaches before the daemon exits. */
350
374
  const IDLE_SHUTDOWN_MS = 3e4;
375
+ /**
376
+ * Upper bound an incumbent daemon waits, during a graceful handoff, for the
377
+ * successor to dial out and claim presence before giving up and releasing the
378
+ * slot itself. Comfortably above a normal successor boot + WS dial (a few
379
+ * hundred ms to a couple of seconds), and short enough that a failed successor
380
+ * doesn't strand the machine as reachable-but-dead for long.
381
+ */
382
+ const HANDOFF_MAX_WAIT_MS = 1e4;
351
383
  var PortalDaemon = class {
352
384
  appUrl;
353
385
  paths;
@@ -357,8 +389,10 @@ var PortalDaemon = class {
357
389
  conns = /* @__PURE__ */ new Set();
358
390
  cwds = /* @__PURE__ */ new Map();
359
391
  declined = /* @__PURE__ */ new Set();
392
+ persistedMachineName = null;
360
393
  fallbackCwd;
361
394
  idleTimer = null;
395
+ handingOff = false;
362
396
  sessionToken = null;
363
397
  deviceToken = null;
364
398
  constructor(appUrl) {
@@ -455,7 +489,7 @@ var PortalDaemon = class {
455
489
  case "grant":
456
490
  this.declined.delete(msg.agentId);
457
491
  this.ensureClient();
458
- this.client?.grantAgent(msg.agentId).catch((error) => {
492
+ this.client?.grantAgent(msg.agentId, msg.conversationId).catch((error) => {
459
493
  this.logError("grantAgent failed", error);
460
494
  });
461
495
  return;
@@ -468,6 +502,9 @@ var PortalDaemon = class {
468
502
  case "shutdown":
469
503
  this.forceShutdown();
470
504
  return;
505
+ case "shutdown_handoff":
506
+ this.gracefulHandoffShutdown();
507
+ return;
471
508
  default: return msg;
472
509
  }
473
510
  }
@@ -491,12 +528,29 @@ var PortalDaemon = class {
491
528
  deviceToken: this.deviceToken
492
529
  }),
493
530
  resolveCwd: (conversationId) => this.resolveCwd(conversationId),
531
+ tlsCertSource: defaultTlsCertSource(process.env, (msg) => this.logInfo(msg)),
532
+ persistedMachineName: this.persistedMachineName,
533
+ onMachineName: (name, source) => this.onMachineName(name, source),
494
534
  onState: (state) => {
495
535
  this.lastState = state;
496
536
  this.broadcastState();
497
537
  }
498
538
  });
499
539
  }
540
+ /**
541
+ * The identity resolved on connect. Persist the name so a later scutil
542
+ * failure reuses it instead of adopting the volatile hostname, and log the
543
+ * source — a `hostname-fallback` after we'd previously registered under
544
+ * scutil is the tell that a flap just orphaned this machine's grants.
545
+ */
546
+ onMachineName(name, source) {
547
+ if (source === "hostname-fallback" && this.persistedMachineName) this.logInfo(`portal identity WARNING: fell back to hostname "${name}" but had previously registered as "${this.persistedMachineName}" — grants may be orphaned`);
548
+ else this.logInfo(`portal identity: "${name}" (source=${source})`);
549
+ if (name && name !== this.persistedMachineName) {
550
+ this.persistedMachineName = name;
551
+ this.persistState();
552
+ }
553
+ }
500
554
  /** The cwd an exec for `conversationId` runs in. */
501
555
  resolveCwd(conversationId) {
502
556
  if (conversationId) {
@@ -530,7 +584,14 @@ var PortalDaemon = class {
530
584
  /** Append a line to the daemon log file (best-effort, for post-hoc debugging). */
531
585
  logError(context, error) {
532
586
  const message = error instanceof Error ? error.message : String(error);
533
- const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${context}: ${message}\n`;
587
+ this.logLine(`${context}: ${message}`);
588
+ }
589
+ /** Append an informational line to the daemon log file (best-effort). */
590
+ logInfo(message) {
591
+ this.logLine(message);
592
+ }
593
+ logLine(message) {
594
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${message}\n`;
534
595
  appendFile(this.paths.logPath, line).catch((_error) => {});
535
596
  }
536
597
  /** Close the listener without exiting the process (tests own the process). */
@@ -581,6 +642,37 @@ var PortalDaemon = class {
581
642
  this.server?.close();
582
643
  process.exit(0);
583
644
  }
645
+ /**
646
+ * Graceful takeover step-down (a newer daemon is replacing us). Unlike
647
+ * `forceShutdown`, this does NOT drop the portal connection up front: doing so
648
+ * would let this machine's presence key expire (10s TTL) before the successor
649
+ * dials out and re-claims it, which is the `registered but not connected` gap
650
+ * that made `exec`/`file write` fail mid-self-update.
651
+ *
652
+ * Order matters. We free the control socket and disconnect attached CLIs
653
+ * first, because the successor cannot bind the singleton socket (and thus
654
+ * cannot start its own portal client) until we release it. But we KEEP our
655
+ * outbound portal WebSocket — and its presence claim — alive across that
656
+ * window. When the successor connects and `claim()`s the slot, the server
657
+ * supersedes us and closes our socket; `beginHandoff` resolves on that close
658
+ * and we exit. A bounded fallback inside `beginHandoff` still exits us if the
659
+ * successor never comes up, so we never hold a dead slot forever.
660
+ */
661
+ async gracefulHandoffShutdown() {
662
+ if (this.handingOff) return;
663
+ this.handingOff = true;
664
+ for (const conn of this.conns) try {
665
+ conn.socket.destroy();
666
+ } catch (_error) {}
667
+ this.conns.clear();
668
+ this.server?.close();
669
+ this.server = null;
670
+ if (this.client) {
671
+ await this.client.beginHandoff(HANDOFF_MAX_WAIT_MS);
672
+ this.client.dispose();
673
+ }
674
+ process.exit(0);
675
+ }
584
676
  async loadState() {
585
677
  try {
586
678
  const raw = await readFile(this.paths.statePath, "utf8");
@@ -590,6 +682,7 @@ var PortalDaemon = class {
590
682
  if (Array.isArray(parsed.declined)) {
591
683
  for (const id of parsed.declined) if (typeof id === "string") this.declined.add(id);
592
684
  }
685
+ if (typeof parsed.machineName === "string" && parsed.machineName) this.persistedMachineName = parsed.machineName;
593
686
  }
594
687
  } catch (_error) {}
595
688
  }
@@ -597,7 +690,8 @@ var PortalDaemon = class {
597
690
  const state = {
598
691
  version: LOCAL_PROTOCOL_VERSION,
599
692
  cwds: Object.fromEntries(this.cwds),
600
- declined: [...this.declined]
693
+ declined: [...this.declined],
694
+ machineName: this.persistedMachineName ?? void 0
601
695
  };
602
696
  writeFile(this.paths.statePath, JSON.stringify(state)).catch((error) => {
603
697
  this.logError("persistState failed", error);
@@ -627,7 +721,7 @@ async function ensureDaemonRunning(appUrl) {
627
721
  if (await isDaemonListening(socketPath)) {
628
722
  const status = await queryDaemonStatus(appUrl);
629
723
  if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return;
630
- if (await stopDaemon(appUrl) === "failed") return;
724
+ if (await stopDaemonForHandoff(appUrl) === "failed") return;
631
725
  }
632
726
  const entry = process.argv[1];
633
727
  const args = entry ? [
@@ -722,6 +816,37 @@ async function queryDaemonStatus(appUrl) {
722
816
  });
723
817
  }
724
818
  /**
819
+ * Ask a running incumbent daemon to step down for a make-before-break handoff
820
+ * (see the daemon's `gracefulHandoffShutdown`). Sends `shutdown_handoff` and
821
+ * waits for the incumbent to release the CONTROL SOCKET — at which point the
822
+ * successor can bind it and start its own portal client. Crucially, the
823
+ * incumbent keeps its portal connection (and presence) alive past this point,
824
+ * so the successor's subsequent `claim()` supersedes it with no presence gap.
825
+ *
826
+ * Returns `stopped` once the socket is free, `failed` if it never freed (the
827
+ * caller should then leave the incumbent alone rather than fight it — same
828
+ * fallback semantics as `stopDaemon`), or `not-running` if nothing was there.
829
+ * Unlike `stopDaemon` this never SIGKILLs: the incumbent is intentionally still
830
+ * alive (holding presence) after it frees the socket, so killing it by pid is
831
+ * exactly the gap this path exists to avoid.
832
+ */
833
+ async function stopDaemonForHandoff(appUrl) {
834
+ const { socketPath } = daemonPaths(appUrl);
835
+ if (!await isDaemonListening(socketPath)) return "not-running";
836
+ const sock = await connectControl(socketPath);
837
+ if (!sock) return "failed";
838
+ sock.write(encodeLine({ t: "shutdown_handoff" }));
839
+ for (let i = 0; i < 50; i += 1) {
840
+ await sleep(100);
841
+ if (!await isDaemonListening(socketPath)) {
842
+ sock.destroy();
843
+ return "stopped";
844
+ }
845
+ }
846
+ sock.destroy();
847
+ return "failed";
848
+ }
849
+ /**
725
850
  * Hard-stop a running daemon. Preferred path: send `shutdown` over the control
726
851
  * socket so it drains clients and exits cleanly. If the socket is unresponsive
727
852
  * (a wedged daemon), fall back to SIGTERM then SIGKILL by the pid the status
@@ -773,4 +898,4 @@ function sleep(ms) {
773
898
  }
774
899
 
775
900
  //#endregion
776
- export { runPortalDaemon as a, LOCAL_PROTOCOL_VERSION as c, encodeLine as d, makeLineParser as f, queryDaemonStatus as i, PORTAL_DAEMON_FLAG as l, ensureDaemonRunning as n, startPortalDaemon as o, parseDaemonMessage as p, isDaemonListening as r, stopDaemon as s, PortalDaemon as t, daemonPaths as u };
901
+ export { runPortalDaemon as a, stopDaemonForHandoff as c, daemonPaths as d, encodeLine as f, queryDaemonStatus as i, LOCAL_PROTOCOL_VERSION as l, parseDaemonMessage as m, ensureDaemonRunning as n, startPortalDaemon as o, makeLineParser as p, isDaemonListening as r, stopDaemon as s, PortalDaemon as t, PORTAL_DAEMON_FLAG as u };