skydive-cli 0.5.0-beta.48 → 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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { t as PortalClient } from "./client-aFwHzCPG.mjs";
3
+ import "./api-TwLD7ibI.mjs";
4
+
5
+ export { PortalClient };
@@ -541,10 +541,34 @@ const DEFAULT_HEARTBEAT = {
541
541
  maxMissed: 2,
542
542
  wakeTickMs: 2e3,
543
543
  wakeGapThresholdMs: 1e4,
544
+ netWatchTickMs: 2e3,
544
545
  initialBackoffMs: 500,
545
546
  maxBackoffMs: 1e4
546
547
  };
547
548
  /**
549
+ * A cheap fingerprint of the machine's network identity: every non-internal
550
+ * interface name plus its IPv4 addresses. When it changes, the active network
551
+ * changed (interface came/went, DHCP handed out a new address, VPN tunnel
552
+ * up/down) and any live socket is riding a route that no longer exists.
553
+ * IPv6 addresses are deliberately excluded: macOS rotates temporary IPv6
554
+ * addresses on a schedule, which would fingerprint as a "change" and cause
555
+ * spurious reconnects — the interface NAME still captures a v6-only tunnel
556
+ * appearing or vanishing.
557
+ */
558
+ function networkFingerprint() {
559
+ const parts = [];
560
+ for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
561
+ let external = false;
562
+ for (const addr of addrs ?? []) {
563
+ if (addr.internal) continue;
564
+ external = true;
565
+ if (addr.family === "IPv4") parts.push(`${name}/${addr.address}`);
566
+ }
567
+ if (external) parts.push(name);
568
+ }
569
+ return parts.sort().join("|");
570
+ }
571
+ /**
548
572
  * Shares the local machine with agents over the portal: dials OUT to the api's
549
573
  * desktop-portal WebSocket (authenticating with a short-lived device token
550
574
  * minted from the CLI session), then runs inbound `exec` directives via a
@@ -568,6 +592,9 @@ var PortalClient = class {
568
592
  legacyGrantsChecked = false;
569
593
  wakeWatch = null;
570
594
  lastTick = Date.now();
595
+ netWatch = null;
596
+ netFingerprint = networkFingerprint();
597
+ backoffKick = null;
571
598
  handingOff = false;
572
599
  handoffResolve = null;
573
600
  heartbeatConfig;
@@ -578,6 +605,7 @@ var PortalClient = class {
578
605
  ...opts.heartbeat
579
606
  };
580
607
  this.startWakeWatch();
608
+ this.startNetWatch();
581
609
  }
582
610
  /**
583
611
  * Detect wake-from-sleep without an Electron/`powerMonitor` dependency (the
@@ -594,13 +622,69 @@ var PortalClient = class {
594
622
  const now = Date.now();
595
623
  const gap = now - this.lastTick;
596
624
  this.lastTick = now;
597
- if (gap > this.heartbeatConfig.wakeGapThresholdMs && this.ws) {
598
- this.error = "woke from sleep; reconnecting portal";
599
- this.ws.terminate();
600
- }
625
+ if (gap > this.heartbeatConfig.wakeGapThresholdMs) this.forceReconnect("woke from sleep; reconnecting portal");
601
626
  }, this.heartbeatConfig.wakeTickMs);
602
627
  this.wakeWatch.unref?.();
603
628
  }
629
+ /**
630
+ * Detect a network switch — the awake sibling of the wake detector. Changing
631
+ * Wi-Fi↔ethernet, switching SSID, or toggling a VPN kills the socket's route
632
+ * with no FIN/RST, so `ws` may never fire 'close' and the heartbeat's
633
+ * missed-pong deadline is the best case (~2 intervals), not a guarantee.
634
+ * Poll the network-identity fingerprint and, the moment it changes, terminate
635
+ * the socket so `connectLoop` redials over the new network immediately.
636
+ * Cheap: `os.networkInterfaces()` is one syscall-backed read per tick.
637
+ */
638
+ startNetWatch() {
639
+ this.netWatch = setInterval(() => {
640
+ const fingerprint = networkFingerprint();
641
+ if (fingerprint === this.netFingerprint) return;
642
+ this.netFingerprint = fingerprint;
643
+ this.forceReconnect("network changed; reconnecting portal");
644
+ }, this.heartbeatConfig.netWatchTickMs);
645
+ this.netWatch.unref?.();
646
+ }
647
+ /**
648
+ * The one reaction every detector (wake, network change) converges on: make
649
+ * the connect loop re-evaluate NOW, whatever state it is in. With a live
650
+ * socket, terminate it — 'close' resolves `runConnection` and the loop
651
+ * redials. Between dials (backing off after a failure, e.g. while the
652
+ * network was down), cut the sleep short so the retry fires immediately — a
653
+ * dial that was pointless a moment ago is exactly right the moment the
654
+ * network comes back. These are per-machine local events, uncorrelated
655
+ * across the fleet, so the immediate retry can't herd; the backoff VALUE is
656
+ * untouched and still governs the next failure.
657
+ */
658
+ forceReconnect(reason) {
659
+ if (this.ws) {
660
+ this.error = reason;
661
+ this.ws.terminate();
662
+ return;
663
+ }
664
+ this.kickBackoff();
665
+ }
666
+ kickBackoff() {
667
+ const kick = this.backoffKick;
668
+ this.backoffKick = null;
669
+ kick?.();
670
+ }
671
+ /** The between-redials sleep, interruptible by a detector (forceReconnect). */
672
+ backoffSleep(ms) {
673
+ return new Promise((resolve) => {
674
+ let done = false;
675
+ const finish = () => {
676
+ if (done) return;
677
+ done = true;
678
+ this.backoffKick = null;
679
+ resolve();
680
+ };
681
+ const timer = setTimeout(finish, ms);
682
+ this.backoffKick = () => {
683
+ clearTimeout(timer);
684
+ finish();
685
+ };
686
+ });
687
+ }
604
688
  isEnabled() {
605
689
  return this.enabled;
606
690
  }
@@ -625,6 +709,7 @@ var PortalClient = class {
625
709
  this.ws = null;
626
710
  this.deviceId = null;
627
711
  this.granted = /* @__PURE__ */ new Set();
712
+ this.kickBackoff();
628
713
  this.setStatus("off");
629
714
  }
630
715
  /**
@@ -639,10 +724,15 @@ var PortalClient = class {
639
724
  clearInterval(this.wakeWatch);
640
725
  this.wakeWatch = null;
641
726
  }
727
+ if (this.netWatch) {
728
+ clearInterval(this.netWatch);
729
+ this.netWatch = null;
730
+ }
642
731
  this.jobs?.killAll();
643
732
  this.jobs = null;
644
733
  this.ws?.close();
645
734
  this.ws = null;
735
+ this.kickBackoff();
646
736
  this.finishHandoff();
647
737
  }
648
738
  /**
@@ -758,7 +848,7 @@ var PortalClient = class {
758
848
  backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
759
849
  }
760
850
  if (!this.enabled || this.disposed || this.handingOff) break;
761
- await sleep(Math.random() * backoff);
851
+ await this.backoffSleep(Math.random() * backoff);
762
852
  }
763
853
  }
764
854
  runConnection(token) {
@@ -785,20 +875,21 @@ var PortalClient = class {
785
875
  this.jobs = jobs;
786
876
  let missed = 0;
787
877
  const heartbeat = setInterval(() => {
788
- if (ws.readyState !== WebSocket.OPEN) return;
878
+ if (ws.readyState !== WebSocket.OPEN && ws.readyState !== WebSocket.CONNECTING) return;
789
879
  if (missed >= this.heartbeatConfig.maxMissed) {
790
- this.error = "portal connection went silent (no pong); reconnecting";
880
+ this.error = ws.readyState === WebSocket.CONNECTING ? "portal handshake stalled; reconnecting" : "portal connection went silent (no pong); reconnecting";
791
881
  ws.terminate();
792
882
  return;
793
883
  }
794
884
  missed += 1;
795
- ws.ping();
885
+ if (ws.readyState === WebSocket.OPEN) ws.ping();
796
886
  }, this.heartbeatConfig.intervalMs);
797
887
  const markAlive = () => {
798
888
  missed = 0;
799
889
  healthy = true;
800
890
  };
801
891
  ws.on("open", () => {
892
+ missed = 0;
802
893
  this.setStatus("connected");
803
894
  this.syncDeviceState();
804
895
  });
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./client-C2lPer4b.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-B7Wv4P6W.mjs";
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
4
  import "./tls-cert-CV-pwxVN.mjs";
5
5
  import "./api-TwLD7ibI.mjs";
6
6
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { d as daemonPaths, f as encodeLine, l as LOCAL_PROTOCOL_VERSION, m as parseDaemonMessage, n as ensureDaemonRunning, p as makeLineParser } from "./daemon-B7Wv4P6W.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
 
@@ -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,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as isRecord, t as PortalClient } from "./client-C2lPer4b.mjs";
2
+ import { n as isRecord, t as PortalClient } from "./client-aFwHzCPG.mjs";
3
3
  import { t as defaultTlsCertSource } from "./tls-cert-CV-pwxVN.mjs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
@@ -21,7 +21,7 @@ import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/
21
21
  * the commit time of the built tree is one monotonic clock they share.
22
22
  */
23
23
  function portalDaemonBuild() {
24
- return "1787070117";
24
+ return "1787166259";
25
25
  }
26
26
  /**
27
27
  * Whether a client carrying `mine` should replace a running daemon carrying
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { s as version$1 } from "./rest-B-dizrBw.mjs";
3
- import { w as getConfigPath } from "./print-SD_y9CTF.mjs";
2
+ import { s as version$1 } from "./rest-DkuT5_oX.mjs";
3
+ import { w as getConfigPath } from "./print-DR6Gas-M.mjs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { err, ok } from "neverthrow";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./rest-B-dizrBw.mjs";
3
- import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-SD_y9CTF.mjs";
2
+ import "./rest-DkuT5_oX.mjs";
3
+ import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DR6Gas-M.mjs";
4
4
  import "./billing-blocked-D3l5kJlX.mjs";
5
5
 
6
6
  export { messageGet, readStdin, resolveAgent, runPrint };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-B-dizrBw.mjs";
2
+ import { a as isRecord, i as errorMessage, r as sendErrorMessage, t as createRestClient } from "./rest-DkuT5_oX.mjs";
3
3
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
4
  import { i as billingBlockedOutcomeFromSendResponse, n as BillingBlockedError } from "./billing-blocked-D3l5kJlX.mjs";
5
5
  import path from "node:path";
@@ -40,7 +40,7 @@ const API_KEY_PREFIX = "sky_live_";
40
40
  */
41
41
  const API_KEY_FAMILY_PREFIX = "sky_";
42
42
  /** Where users mint and copy API keys. Shown in the login prompt. */
43
- const API_KEYS_URL = "skydive.com/settings/account";
43
+ const API_KEYS_URL = "skydive.com/settings/workspace";
44
44
  const store = new Conf({
45
45
  projectName: process.env["SKYDIVE_CONFIG_NAME"] ?? "skydive",
46
46
  projectSuffix: "",
@@ -425,9 +425,9 @@ function parseDesktopHandoffCard(rootEl) {
425
425
  if (!agentId) return null;
426
426
  const agentName = optionalString(props.agentName) ?? "This agent";
427
427
  return {
428
- title: `Let ${agentName} use your computer?`,
428
+ title: `Let ${agentName} run commands on your machine?`,
429
429
  subtitle: null,
430
- description: `Approving shares this machine with ${agentName} while you're signed in. Revoke anytime: skydive portal revoke --agent "${agentName}"`,
430
+ description: `Approving opens the portal to ${agentName} while you're signed in. Revoke anytime: skydive portal revoke --agent "${agentName}"`,
431
431
  fields: [],
432
432
  button: {
433
433
  label: "Approve",
@@ -785,7 +785,7 @@ function formatConnectCard(card) {
785
785
  lines.push(`Provide credential (${card.action.fields.join(", ") || "value"}) at: ${card.action.url}`);
786
786
  break;
787
787
  case "approve_portal":
788
- lines.push(`Approve local-machine access for agent ${card.action.agentId} in the TUI or web app.`);
788
+ lines.push(`Approve portal access to this machine for agent ${card.action.agentId} in the TUI or web app.`);
789
789
  break;
790
790
  case "decide_compute":
791
791
  lines.push("Approve or deny this compute increase in the interactive TUI or web app.");
@@ -14,7 +14,7 @@ import { n as printError } from "./output-C9mb3sUB.mjs";
14
14
  * the reply (and to --json).
15
15
  */
16
16
  async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
17
- const { PortalClient } = await import("./client-CSKr4Swa.mjs");
17
+ const { PortalClient } = await import("./client-CkPQG8M1.mjs");
18
18
  const { defaultTlsCertSource } = await import("./tls-cert-CLgSQALB.mjs");
19
19
  let signalConnected;
20
20
  const connected = new Promise((resolve) => {
@@ -40,7 +40,7 @@ async function connectMachineShare({ appUrl, sessionToken, timeoutHint }) {
40
40
  machineShare.enable();
41
41
  if (await Promise.race([connected.then(() => false), new Promise((resolve) => setTimeout(() => resolve(true), 3e4).unref())])) {
42
42
  machineShare.dispose();
43
- printError(`Could not connect the portal within 30s. Machine sharing is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
43
+ printError(`Could not connect the portal within 30s. The portal is unavailable (network, or the portal kill switch is off). ${timeoutHint ?? "Check `skydive portal status`."}`);
44
44
  process.exit(1);
45
45
  }
46
46
  return machineShare;
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-B-dizrBw.mjs";
2
+ import { n as errorDetail, r as sendErrorMessage, t as createRestClient } from "./rest-DkuT5_oX.mjs";
3
3
  import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
4
4
  import "./billing-blocked-D3l5kJlX.mjs";
5
5
 
@@ -6,7 +6,7 @@ import { createParser } from "eventsource-parser";
6
6
 
7
7
  //#region package.json
8
8
  var name = "skydive-cli";
9
- var version = "0.5.0-beta.48";
9
+ var version = "0.5.0-beta.52";
10
10
 
11
11
  //#endregion
12
12
  //#region src/chat/util.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skydive-cli",
3
- "version": "0.5.0-beta.48",
3
+ "version": "0.5.0-beta.52",
4
4
  "description": "Skydive CLI — cloud agents from the command line",
5
5
  "homepage": "https://skydive.com",
6
6
  "license": "MIT",
@@ -69,9 +69,9 @@
69
69
  },
70
70
  "//optionalDependencies": "The per-platform binary packages (skydive-cli-<os>-<arch>) are INTENTIONALLY not committed here. They are published by release-skydive-cli-binaries.yml and injected — pinned to the exact version being published — into the published launcher's optionalDependencies at release time (scripts/prepare-platform-packages.mjs --pin-version, run in release-skydive-cli.yml). Committing them would make `yarn install` try to resolve versions that only exist post-publish, breaking local dev and CI. npm installs of a published skydive-cli still get them (os/cpu-gated); the launcher (src/launcher.ts) resolves and execs the matching one. Local dev never needs them: it runs the JS bundle or `build:binary` directly.",
71
71
  "optionalDependencies": {
72
- "skydive-cli-darwin-arm64": "0.5.0-beta.48",
73
- "skydive-cli-darwin-x64": "0.5.0-beta.48",
74
- "skydive-cli-linux-x64": "0.5.0-beta.48",
75
- "skydive-cli-linux-arm64": "0.5.0-beta.48"
72
+ "skydive-cli-darwin-arm64": "0.5.0-beta.52",
73
+ "skydive-cli-darwin-x64": "0.5.0-beta.52",
74
+ "skydive-cli-linux-x64": "0.5.0-beta.52",
75
+ "skydive-cli-linux-arm64": "0.5.0-beta.52"
76
76
  }
77
77
  }
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { t as PortalClient } from "./client-C2lPer4b.mjs";
3
- import "./api-TwLD7ibI.mjs";
4
-
5
- export { PortalClient };
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./client-C2lPer4b.mjs";
3
- import "./daemon-B7Wv4P6W.mjs";
4
- import "./tls-cert-CV-pwxVN.mjs";
5
- import "./api-TwLD7ibI.mjs";
6
- import { t as PortalDaemonClient } from "./daemon-client-Btr9rmKl.mjs";
7
-
8
- export { PortalDaemonClient };