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.
- package/CHANGELOG.md +2 -0
- package/README.md +25 -8
- package/dist/js/bin.mjs +20 -20
- package/dist/js/{boot-BoqVFa4M.mjs → boot-D_VFIRJj.mjs} +301 -252
- package/dist/js/client-CkPQG8M1.mjs +5 -0
- package/dist/js/{client-C2lPer4b.mjs → client-aFwHzCPG.mjs} +99 -8
- package/dist/js/{daemon-B57zYVAN.mjs → daemon-CORAITjj.mjs} +2 -2
- package/dist/js/{daemon-client-Btr9rmKl.mjs → daemon-client-Cfi66Xy9.mjs} +1 -1
- package/dist/js/daemon-client-DJPW9cRp.mjs +8 -0
- package/dist/js/{daemon-B7Wv4P6W.mjs → daemon-uzpOdRPL.mjs} +2 -2
- package/dist/js/{install-BOtarTEl.mjs → install-BBplm-Zp.mjs} +2 -2
- package/dist/js/{print-ejS3eAZz.mjs → print-Cuao6LN6.mjs} +2 -2
- package/dist/js/{print-SD_y9CTF.mjs → print-DR6Gas-M.mjs} +5 -5
- package/dist/js/{print-share-D20YvVbb.mjs → print-share-BypBUrNh.mjs} +2 -2
- package/dist/js/{rest-Cz-aN-ME.mjs → rest-CJkBP2Jz.mjs} +1 -1
- package/dist/js/{rest-B-dizrBw.mjs → rest-DkuT5_oX.mjs} +1 -1
- package/package.json +5 -5
- package/dist/js/client-CSKr4Swa.mjs +0 -5
- package/dist/js/daemon-client-DL3eNr6N.mjs +0 -8
|
@@ -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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as isRecord, t as PortalClient } from "./client-
|
|
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 "
|
|
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-
|
|
3
|
-
import { w as getConfigPath } from "./print-
|
|
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-
|
|
3
|
-
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-
|
|
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-
|
|
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/
|
|
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}
|
|
428
|
+
title: `Let ${agentName} run commands on your machine?`,
|
|
429
429
|
subtitle: null,
|
|
430
|
-
description: `Approving
|
|
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
|
|
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-
|
|
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.
|
|
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-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skydive-cli",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
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.
|
|
73
|
-
"skydive-cli-darwin-x64": "0.5.0-beta.
|
|
74
|
-
"skydive-cli-linux-x64": "0.5.0-beta.
|
|
75
|
-
"skydive-cli-linux-arm64": "0.5.0-beta.
|
|
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
|
}
|