skydive-cli 0.6.0-beta.14 → 0.6.0-beta.141
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 +15 -0
- package/dist/js/bin.mjs +867 -89
- package/dist/js/{boot-BfQYdBWa.mjs → boot-DqZtJefD.mjs} +9013 -6755
- package/dist/js/client-BT0d1zd8.mjs +5 -0
- package/dist/js/{client-2oV6FnzY.mjs → client-CBU_fH_Y.mjs} +67 -8
- package/dist/js/{client-l6orYfqq.mjs → client-Cxk3khQu.mjs} +18 -4
- package/dist/js/{daemon-C6ofeKx-.mjs → daemon-DrHUy6Fd.mjs} +2 -2
- package/dist/js/{daemon-DTIvt7C_.mjs → daemon-DyAyWGH-.mjs} +117 -27
- package/dist/js/daemon-client-BCegFMYY.mjs +8 -0
- package/dist/js/{daemon-client-d2hpSMj2.mjs → daemon-client-BbVd5yb4.mjs} +29 -4
- package/dist/js/{install-BR_KFDLp.mjs → install-CMxeh2Qo.mjs} +49 -3
- package/dist/js/{print-DhBgLSFj.mjs → print-CeE1p-iY.mjs} +2 -2
- package/dist/js/{print-CYVsZkwl.mjs → print-MMOJZcKb.mjs} +13 -12
- package/dist/js/{print-share-BJr-PqPA.mjs → print-share-D49HTR-b.mjs} +1 -1
- package/dist/js/{raw-pty-dndn1Kxc.mjs → raw-pty-B-WKaaE3.mjs} +1 -1
- package/dist/js/raw-pty-BJIs_xjQ.mjs +5 -0
- package/dist/js/{rest-DGN5sn3A.mjs → rest-BZlWCfSy.mjs} +1 -1
- package/dist/js/{rest-BPTJhDk3.mjs → rest-CU32bzPu.mjs} +148 -15
- package/package.json +10 -9
- package/dist/js/client-rIM5gVCQ.mjs +0 -5
- package/dist/js/daemon-client-QBi80LnY.mjs +0 -8
- package/dist/js/raw-pty-DRZuf1oW.mjs +0 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { t as HttpError } from "./http-error-BF2NZZE3.mjs";
|
|
2
3
|
import { a as mintPortalDeviceToken, c as unifyLegacyCliGrants, i as grantPortalAccess, l as startTlsForward, n as fetchPortalDevices, r as findThisDevice } from "./api-BFQ4PQDA.mjs";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -160,7 +161,7 @@ function errorMessage(err) {
|
|
|
160
161
|
* the commit time of the built tree is one monotonic clock they share.
|
|
161
162
|
*/
|
|
162
163
|
function portalDaemonBuild() {
|
|
163
|
-
return "
|
|
164
|
+
return "1792701670";
|
|
164
165
|
}
|
|
165
166
|
/**
|
|
166
167
|
* Whether a client carrying `mine` should replace a running daemon carrying
|
|
@@ -579,18 +580,41 @@ var JobManager = class {
|
|
|
579
580
|
|
|
580
581
|
//#endregion
|
|
581
582
|
//#region ../portal-daemon/src/client.ts
|
|
583
|
+
const INITIAL_BACKOFF_MS = 500;
|
|
584
|
+
const MAX_BACKOFF_MS = 1e4;
|
|
585
|
+
const AUTH_STAND_DOWN_MS = 5 * 6e4;
|
|
586
|
+
const AUTH_STAND_DOWN_MESSAGE = "portal sign-in expired — reopen Skydive Desktop (or run `skydive portal open`) to reconnect";
|
|
587
|
+
/**
|
|
588
|
+
* True when the error means the server could not authenticate us: a token-mint
|
|
589
|
+
* failure (HttpError 401) or a WS upgrade rejected at the auth layer. `ws`
|
|
590
|
+
* reports a rejected upgrade as `Unexpected server response: <status>` with no
|
|
591
|
+
* structured status property, so the status is read from the message. A 429 is
|
|
592
|
+
* deliberately NOT an auth failure — see isRateLimited.
|
|
593
|
+
*/
|
|
594
|
+
function isAuthFailure(err) {
|
|
595
|
+
if (err instanceof HttpError) return err.status === 401;
|
|
596
|
+
return /Unexpected server response: 401\b/.test(errorMessage(err));
|
|
597
|
+
}
|
|
598
|
+
const RATE_LIMIT_STAND_DOWN_MS = 12e4;
|
|
599
|
+
const RATE_LIMIT_STAND_DOWN_MESSAGE = "portal connection rate limited — retrying in 2 minutes";
|
|
600
|
+
function isRateLimited(err) {
|
|
601
|
+
if (err instanceof HttpError) return err.status === 429;
|
|
602
|
+
return /Unexpected server response: 429\b/.test(errorMessage(err));
|
|
603
|
+
}
|
|
582
604
|
const DEFAULT_HEARTBEAT = {
|
|
583
605
|
intervalMs: 15e3,
|
|
584
606
|
maxMissed: 2,
|
|
585
607
|
wakeTickMs: 2e3,
|
|
586
608
|
wakeGapThresholdMs: 1e4,
|
|
587
609
|
netWatchTickMs: 2e3,
|
|
588
|
-
initialBackoffMs:
|
|
589
|
-
maxBackoffMs:
|
|
610
|
+
initialBackoffMs: INITIAL_BACKOFF_MS,
|
|
611
|
+
maxBackoffMs: MAX_BACKOFF_MS,
|
|
590
612
|
supersededBackoffFloorMs: 5e3,
|
|
591
613
|
flapWindowMs: 6e4,
|
|
592
614
|
flapMaxStandDowns: 3,
|
|
593
|
-
flapBackoffMs: 6e4
|
|
615
|
+
flapBackoffMs: 6e4,
|
|
616
|
+
authStandDownMs: AUTH_STAND_DOWN_MS,
|
|
617
|
+
rateLimitStandDownMs: RATE_LIMIT_STAND_DOWN_MS
|
|
594
618
|
};
|
|
595
619
|
/**
|
|
596
620
|
* A cheap fingerprint of the machine's network identity: every non-internal
|
|
@@ -670,6 +694,8 @@ var PortalClient = class {
|
|
|
670
694
|
machineName = "";
|
|
671
695
|
friendlyName = "";
|
|
672
696
|
legacyGrantsChecked = false;
|
|
697
|
+
authRejected = false;
|
|
698
|
+
rateLimited = false;
|
|
673
699
|
wakeWatch = null;
|
|
674
700
|
lastTick = Date.now();
|
|
675
701
|
netWatch = null;
|
|
@@ -785,6 +811,8 @@ var PortalClient = class {
|
|
|
785
811
|
if (this.enabled || this.disposed) return;
|
|
786
812
|
this.enabled = true;
|
|
787
813
|
this.error = null;
|
|
814
|
+
this.authRejected = false;
|
|
815
|
+
this.rateLimited = false;
|
|
788
816
|
this.connectLoop();
|
|
789
817
|
}
|
|
790
818
|
disable() {
|
|
@@ -930,6 +958,7 @@ var PortalClient = class {
|
|
|
930
958
|
const token = await this.wsToken();
|
|
931
959
|
const { healthy, standDown } = await this.runConnection(token);
|
|
932
960
|
if (standDown) {
|
|
961
|
+
this.setStatus("stand-down");
|
|
933
962
|
const now = Date.now();
|
|
934
963
|
standDowns.push(now);
|
|
935
964
|
standDowns = standDowns.filter((at) => now - at <= this.heartbeatConfig.flapWindowMs);
|
|
@@ -939,11 +968,31 @@ var PortalClient = class {
|
|
|
939
968
|
} else backoff = Math.max(Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs), this.heartbeatConfig.supersededBackoffFloorMs);
|
|
940
969
|
sleepFloor = backoff / 2;
|
|
941
970
|
} else if (healthy) backoff = this.heartbeatConfig.initialBackoffMs;
|
|
942
|
-
else
|
|
971
|
+
else if (this.authRejected) {
|
|
972
|
+
this.setStatus("error", AUTH_STAND_DOWN_MESSAGE);
|
|
973
|
+
this.authRejected = false;
|
|
974
|
+
backoff = this.heartbeatConfig.authStandDownMs;
|
|
975
|
+
sleepFloor = backoff;
|
|
976
|
+
} else if (this.rateLimited) {
|
|
977
|
+
this.setStatus("error", RATE_LIMIT_STAND_DOWN_MESSAGE);
|
|
978
|
+
this.rateLimited = false;
|
|
979
|
+
backoff = this.heartbeatConfig.rateLimitStandDownMs;
|
|
980
|
+
sleepFloor = backoff;
|
|
981
|
+
} else backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
|
|
943
982
|
} catch (err) {
|
|
944
983
|
if (!this.enabled || this.disposed || this.handingOff) break;
|
|
945
|
-
|
|
946
|
-
|
|
984
|
+
if (isAuthFailure(err)) {
|
|
985
|
+
this.setStatus("error", AUTH_STAND_DOWN_MESSAGE);
|
|
986
|
+
backoff = this.heartbeatConfig.authStandDownMs;
|
|
987
|
+
sleepFloor = backoff;
|
|
988
|
+
} else if (isRateLimited(err)) {
|
|
989
|
+
this.setStatus("error", RATE_LIMIT_STAND_DOWN_MESSAGE);
|
|
990
|
+
backoff = this.heartbeatConfig.rateLimitStandDownMs;
|
|
991
|
+
sleepFloor = backoff;
|
|
992
|
+
} else {
|
|
993
|
+
this.setStatus("error", errorMessage(err));
|
|
994
|
+
backoff = Math.min(backoff * 2, this.heartbeatConfig.maxBackoffMs);
|
|
995
|
+
}
|
|
947
996
|
}
|
|
948
997
|
if (!this.enabled || this.disposed || this.handingOff) break;
|
|
949
998
|
this.inStandDownSleep = sleepFloor > 0;
|
|
@@ -1000,6 +1049,8 @@ var PortalClient = class {
|
|
|
1000
1049
|
};
|
|
1001
1050
|
ws.on("open", () => {
|
|
1002
1051
|
missed = 0;
|
|
1052
|
+
this.authRejected = false;
|
|
1053
|
+
this.rateLimited = false;
|
|
1003
1054
|
this.dialReason = null;
|
|
1004
1055
|
this.disconnectedAt = null;
|
|
1005
1056
|
this.setStatus("connected");
|
|
@@ -1013,6 +1064,14 @@ var PortalClient = class {
|
|
|
1013
1064
|
});
|
|
1014
1065
|
ws.on("error", (err) => {
|
|
1015
1066
|
this.error = errorMessage(err);
|
|
1067
|
+
if (isAuthFailure(err)) {
|
|
1068
|
+
this.authRejected = true;
|
|
1069
|
+
this.dialReason ??= "portal credentials rejected; standing down";
|
|
1070
|
+
}
|
|
1071
|
+
if (isRateLimited(err)) {
|
|
1072
|
+
this.rateLimited = true;
|
|
1073
|
+
this.dialReason ??= "portal connection rate limited; standing down";
|
|
1074
|
+
}
|
|
1016
1075
|
});
|
|
1017
1076
|
ws.on("close", (code, reason) => {
|
|
1018
1077
|
standDown = code === CLOSE_SUPERSEDED || code === CLOSE_FLAP_COOLDOWN || code === 1e3 && reason.toString() === "superseded";
|
|
@@ -1076,4 +1135,4 @@ function sleep(ms) {
|
|
|
1076
1135
|
}
|
|
1077
1136
|
|
|
1078
1137
|
//#endregion
|
|
1079
|
-
export {
|
|
1138
|
+
export { portalDaemonBuild as a, isNewerBuild as i, PortalClient as n, isRecord as o, RATE_LIMIT_STAND_DOWN_MS as r, resolveMachineIdentity as s, AUTH_STAND_DOWN_MS as t };
|
|
@@ -91,7 +91,7 @@ function parseFrame(expectedType, frameBytes) {
|
|
|
91
91
|
const parsed = jsonLen ? JSON.parse(textDecoder.decode(frameBytes.subarray(jsonStart, jsonEnd))) : {};
|
|
92
92
|
if (typeof parsed !== "object" || parsed === null) return null;
|
|
93
93
|
json = parsed;
|
|
94
|
-
} catch {
|
|
94
|
+
} catch (_error) {
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
return {
|
|
@@ -113,6 +113,18 @@ const FRAME = {
|
|
|
113
113
|
RESIZE: 17
|
|
114
114
|
};
|
|
115
115
|
const MAX_INPUT_BYTES = 1 * 1024 * 1024;
|
|
116
|
+
/** Upper bound for an exec's `timeoutMs`; larger asks are clamped to this. */
|
|
117
|
+
const MAX_EXEC_TIMEOUT_MS = 1440 * 60 * 1e3;
|
|
118
|
+
/**
|
|
119
|
+
* The exec budget applied when the client sends no `timeoutMs` of its own.
|
|
120
|
+
* Liberal by design — one-shot execs legitimately run installs, builds, and
|
|
121
|
+
* long data pulls — but not absent: without it, the e2b SDK's own default
|
|
122
|
+
* (60s) silently kills anything longer mid-run with an opaque
|
|
123
|
+
* `deadline_exceeded`, which is far below what the CLI's `--timeout` help
|
|
124
|
+
* promises ("the server default (~10 min) applies"). Callers with a bigger
|
|
125
|
+
* budget send an explicit `timeoutMs` (clamped to {@link MAX_EXEC_TIMEOUT_MS}).
|
|
126
|
+
*/
|
|
127
|
+
const DEFAULT_EXEC_TIMEOUT_MS = 600 * 1e3;
|
|
116
128
|
/** Query params for the upgrade URL, from a spec. Inverse of {@link parseStreamSpec}. */
|
|
117
129
|
function streamSpecToQuery(spec) {
|
|
118
130
|
if (spec.mode === "pty") return {
|
|
@@ -124,7 +136,8 @@ function streamSpecToQuery(spec) {
|
|
|
124
136
|
if (spec.mode === "exec") return {
|
|
125
137
|
agentId: spec.agentId,
|
|
126
138
|
mode: "exec",
|
|
127
|
-
command: spec.command
|
|
139
|
+
command: spec.command,
|
|
140
|
+
...spec.timeoutMs !== void 0 ? { timeoutMs: String(spec.timeoutMs) } : {}
|
|
128
141
|
};
|
|
129
142
|
return {
|
|
130
143
|
agentId: spec.agentId,
|
|
@@ -273,7 +286,8 @@ var SandboxStream = class SandboxStream {
|
|
|
273
286
|
} : {
|
|
274
287
|
mode: "exec",
|
|
275
288
|
agentId: opts.agentId,
|
|
276
|
-
command: opts.command
|
|
289
|
+
command: opts.command,
|
|
290
|
+
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
|
|
277
291
|
};
|
|
278
292
|
const url = new URL(`${wsBase(opts.appUrl)}${SANDBOX_STREAM_PATH}`);
|
|
279
293
|
for (const [key, value] of Object.entries(streamSpecToQuery(spec))) url.searchParams.set(key, value);
|
|
@@ -293,4 +307,4 @@ function toBuffer(data) {
|
|
|
293
307
|
}
|
|
294
308
|
|
|
295
309
|
//#endregion
|
|
296
|
-
export {
|
|
310
|
+
export { FS_MAX_BLOB_BYTES as a, streamSpecToQuery as i, DEFAULT_EXEC_TIMEOUT_MS as n, decodeFsResponse as o, SANDBOX_STREAM_PATH as r, encodeFsRequest as s, SandboxStream as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./client-
|
|
2
|
+
import "./client-CBU_fH_Y.mjs";
|
|
3
3
|
import "./tls-cert-Rua2oV7n.mjs";
|
|
4
4
|
import "./api-BFQ4PQDA.mjs";
|
|
5
|
-
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-
|
|
5
|
+
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-DyAyWGH-.mjs";
|
|
6
6
|
|
|
7
7
|
export { runPortalDaemon };
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as portalDaemonBuild, i as isNewerBuild, n as PortalClient, o as isRecord } from "./client-CBU_fH_Y.mjs";
|
|
3
3
|
import { t as defaultTlsCertSource } from "./tls-cert-Rua2oV7n.mjs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { access, appendFile, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { spawn } from "node:child_process";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
9
10
|
import { connect, createServer } from "node:net";
|
|
10
|
-
import { access, appendFile, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
|
|
12
12
|
//#region ../portal-daemon/src/local-protocol.ts
|
|
13
13
|
/**
|
|
@@ -66,6 +66,18 @@ function canonicalPortalAppUrl(appUrl) {
|
|
|
66
66
|
if (host === "skydive.com" || host === "www.skydive.com" || host === "api.skydive.com") return "https://api.skydive.com";
|
|
67
67
|
return trimmed;
|
|
68
68
|
}
|
|
69
|
+
/** Prefix every Windows named pipe lives under (the pipe filesystem root). */
|
|
70
|
+
const WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
71
|
+
/**
|
|
72
|
+
* Whether a daemon control-socket path is a Windows named pipe rather than a
|
|
73
|
+
* Unix-domain-socket FILE. Pipes have no on-disk file: they cannot be
|
|
74
|
+
* `stat`ed, `unlink`ed, or purged by a tmp cleaner, and the OS frees the name
|
|
75
|
+
* the moment the owning server closes — so every file-based lifecycle step
|
|
76
|
+
* (stale-file cleanup, the socket-ownership watch) must be skipped for them.
|
|
77
|
+
*/
|
|
78
|
+
function isNamedPipePath(socketPath) {
|
|
79
|
+
return socketPath.startsWith(WINDOWS_PIPE_PREFIX);
|
|
80
|
+
}
|
|
69
81
|
/**
|
|
70
82
|
* Per-user, per-app daemon socket + state paths. Scoped by OS user and a hash
|
|
71
83
|
* of the CANONICAL appUrl so two logins or two deployments (prod vs a PR
|
|
@@ -73,17 +85,42 @@ function canonicalPortalAppUrl(appUrl) {
|
|
|
73
85
|
* every alias of one deployment (see {@link canonicalPortalAppUrl}) shares
|
|
74
86
|
* one. Kept under the OS temp dir because Unix socket paths are
|
|
75
87
|
* length-limited (~104 bytes on macOS).
|
|
88
|
+
*
|
|
89
|
+
* On Windows a `node:net` server cannot listen on a filesystem path at all —
|
|
90
|
+
* binding the tmp-dir path fails with EACCES and the daemon dies silently
|
|
91
|
+
* (ANY-7285's Windows facet) — so the socket is a named pipe under
|
|
92
|
+
* `\\.\pipe\`, keyed by the same (user, appUrl-hash) pair. State and log
|
|
93
|
+
* files stay under the temp dir on every platform.
|
|
94
|
+
*
|
|
95
|
+
* Darwin `sockaddr_un.sun_path` is 104 bytes including NUL (103 usable).
|
|
96
|
+
* `os.tmpdir()` on macOS is `$TMPDIR` (`/var/folders/<id>/T`, often ~50
|
|
97
|
+
* chars). Adding `skydive-portal-<user>-<hash>/daemon.sock` overruns that
|
|
98
|
+
* cap: bind() fails, the sidecar handshake still reports the long path, and
|
|
99
|
+
* connect() returns ENOENT (or Go's "path must be shorter than SUN_LEN").
|
|
100
|
+
* Stay in `os.tmpdir()` (do not relocate to `/tmp`) and shorten the leaf
|
|
101
|
+
* when the composed path would blow the cap. Windows named pipes have no
|
|
102
|
+
* sun_path, so they keep the long leaf.
|
|
76
103
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
104
|
+
const SUN_PATH_MAX = 103;
|
|
105
|
+
function daemonPaths(appUrl, platform = process.platform) {
|
|
106
|
+
const parent = os.tmpdir();
|
|
107
|
+
const key = `skydive-portal-${safeSegment(os.userInfo().username || "user")}-${createHash("sha256").update(canonicalPortalAppUrl(appUrl)).digest("hex").slice(0, 12)}`;
|
|
108
|
+
const dir = path.join(parent, key);
|
|
109
|
+
const paths = {
|
|
82
110
|
dir,
|
|
83
|
-
socketPath: path.join(dir, "daemon.sock"),
|
|
111
|
+
socketPath: platform === "win32" ? `${WINDOWS_PIPE_PREFIX}${key}` : path.join(dir, "daemon.sock"),
|
|
84
112
|
statePath: path.join(dir, "state.json"),
|
|
85
113
|
logPath: path.join(dir, "daemon.log")
|
|
86
114
|
};
|
|
115
|
+
if (platform === "win32" || Buffer.byteLength(paths.socketPath) <= SUN_PATH_MAX) return paths;
|
|
116
|
+
const short = createHash("sha256").update(`${os.userInfo().username || "user"}\0${canonicalPortalAppUrl(appUrl)}`).digest("hex").slice(0, 12);
|
|
117
|
+
const shortDir = path.join(parent, `skydive-p-${short}`);
|
|
118
|
+
return {
|
|
119
|
+
dir: shortDir,
|
|
120
|
+
socketPath: path.join(shortDir, "d.sock"),
|
|
121
|
+
statePath: path.join(shortDir, "state.json"),
|
|
122
|
+
logPath: path.join(shortDir, "daemon.log")
|
|
123
|
+
};
|
|
87
124
|
}
|
|
88
125
|
function safeSegment(value) {
|
|
89
126
|
return value.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 24) || "user";
|
|
@@ -344,6 +381,16 @@ const IDLE_SHUTDOWN_MS = 3e4;
|
|
|
344
381
|
*/
|
|
345
382
|
const SOCKET_WATCH_TICK_MS = 3e4;
|
|
346
383
|
/**
|
|
384
|
+
* Grace window after a server stand-down: the check fires the instant the
|
|
385
|
+
* close lands, but the WINNER's socket bind may still be in flight — a check
|
|
386
|
+
* run at t=0 can see our own still-good socket, conclude we are the owner, and
|
|
387
|
+
* redial into the supersede war the stand-down exists to end. On a
|
|
388
|
+
* stand-down-triggered check, wait this out once and re-stat before
|
|
389
|
+
* concluding ownership. The periodic watch tick runs grace-free (nothing just
|
|
390
|
+
* displaced us there).
|
|
391
|
+
*/
|
|
392
|
+
const STAND_DOWN_GRACE_MS = 1500;
|
|
393
|
+
/**
|
|
347
394
|
* Upper bound an incumbent daemon waits, during a graceful handoff, for the
|
|
348
395
|
* successor to dial out and claim presence before giving up and releasing the
|
|
349
396
|
* slot itself. Comfortably above a normal successor boot + WS dial (a few
|
|
@@ -370,12 +417,14 @@ var PortalDaemon = class {
|
|
|
370
417
|
socketId = null;
|
|
371
418
|
socketCheckRunning = false;
|
|
372
419
|
socketWatchTickMs;
|
|
420
|
+
standDownGraceMs;
|
|
373
421
|
exit;
|
|
374
422
|
constructor(appUrl, options) {
|
|
375
423
|
this.appUrl = canonicalPortalAppUrl(appUrl);
|
|
376
424
|
this.paths = daemonPaths(this.appUrl);
|
|
377
425
|
this.fallbackCwd = process.env.HOME ?? process.cwd();
|
|
378
426
|
this.socketWatchTickMs = options?.socketWatchTickMs ?? SOCKET_WATCH_TICK_MS;
|
|
427
|
+
this.standDownGraceMs = options?.standDownGraceMs ?? STAND_DOWN_GRACE_MS;
|
|
379
428
|
this.exit = options?.exit ?? (() => process.exit(0));
|
|
380
429
|
}
|
|
381
430
|
/** Start listening. Rejects if the socket is already held by another daemon. */
|
|
@@ -392,8 +441,10 @@ var PortalDaemon = class {
|
|
|
392
441
|
resolve();
|
|
393
442
|
});
|
|
394
443
|
});
|
|
395
|
-
|
|
396
|
-
|
|
444
|
+
if (!isNamedPipePath(this.paths.socketPath)) {
|
|
445
|
+
await this.recordSocketIdentity();
|
|
446
|
+
this.startSocketWatch();
|
|
447
|
+
}
|
|
397
448
|
}
|
|
398
449
|
/**
|
|
399
450
|
* Remember which socket FILE we bound (dev+ino), so the watch can tell "our
|
|
@@ -421,24 +472,42 @@ var PortalDaemon = class {
|
|
|
421
472
|
*/
|
|
422
473
|
startSocketWatch() {
|
|
423
474
|
this.socketWatch = setInterval(() => {
|
|
424
|
-
|
|
425
|
-
this.socketCheckRunning = true;
|
|
426
|
-
this.checkSocketOwnership().catch((error) => this.logError("socket ownership check failed", error)).finally(() => {
|
|
427
|
-
this.socketCheckRunning = false;
|
|
428
|
-
});
|
|
475
|
+
this.runOwnershipCheck();
|
|
429
476
|
}, this.socketWatchTickMs);
|
|
430
477
|
this.socketWatch.unref();
|
|
431
478
|
}
|
|
479
|
+
/** One socket-ownership pass, shared by the watch timer (grace-free) and the
|
|
480
|
+
* client's 'stand-down' state (with grace — see STAND_DOWN_GRACE_MS).
|
|
481
|
+
*/
|
|
482
|
+
runOwnershipCheck(graceMs = 0) {
|
|
483
|
+
if (this.socketCheckRunning) return;
|
|
484
|
+
this.socketCheckRunning = true;
|
|
485
|
+
this.checkSocketOwnership(graceMs).catch((error) => this.logError("socket ownership check failed", error)).finally(() => {
|
|
486
|
+
this.socketCheckRunning = false;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
432
489
|
stopSocketWatch() {
|
|
433
490
|
if (this.socketWatch) {
|
|
434
491
|
clearInterval(this.socketWatch);
|
|
435
492
|
this.socketWatch = null;
|
|
436
493
|
}
|
|
437
494
|
}
|
|
438
|
-
async checkSocketOwnership() {
|
|
495
|
+
async checkSocketOwnership(graceMs = 0) {
|
|
439
496
|
if (!this.server || this.handingOff) return;
|
|
440
497
|
const current = await stat(this.paths.socketPath).catch(() => null);
|
|
441
|
-
if (current && this.socketId && current.dev === this.socketId.dev && current.ino === this.socketId.ino)
|
|
498
|
+
if (current && this.socketId && current.dev === this.socketId.dev && current.ino === this.socketId.ino) {
|
|
499
|
+
if (graceMs > 0) {
|
|
500
|
+
await new Promise((r) => setTimeout(r, graceMs));
|
|
501
|
+
const recheck = await stat(this.paths.socketPath).catch(() => null);
|
|
502
|
+
const socketId = this.socketId;
|
|
503
|
+
if (recheck && socketId && !(recheck.dev === socketId.dev && recheck.ino === socketId.ino)) {
|
|
504
|
+
this.logInfo("control socket taken over during stand-down grace; standing down");
|
|
505
|
+
this.standDown();
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
442
511
|
if (current) {
|
|
443
512
|
this.logInfo("control socket path is owned by another daemon; standing down");
|
|
444
513
|
this.standDown();
|
|
@@ -490,6 +559,10 @@ var PortalDaemon = class {
|
|
|
490
559
|
this.exit();
|
|
491
560
|
}
|
|
492
561
|
async clearStaleSocket() {
|
|
562
|
+
if (isNamedPipePath(this.paths.socketPath)) {
|
|
563
|
+
if (await isDaemonListening(this.paths.socketPath)) throw new Error("daemon already running");
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
493
566
|
if (!await pathExists(this.paths.socketPath)) return;
|
|
494
567
|
if (await new Promise((resolve) => {
|
|
495
568
|
const probe = connect(this.paths.socketPath);
|
|
@@ -605,13 +678,23 @@ var PortalDaemon = class {
|
|
|
605
678
|
tlsCertSource: defaultTlsCertSource(process.env, (msg) => this.logInfo(msg)),
|
|
606
679
|
persistedMachineName: this.persistedMachineName,
|
|
607
680
|
onMachineName: (name, source) => this.onMachineName(name, source),
|
|
608
|
-
onState: (state) =>
|
|
609
|
-
this.lastState = state;
|
|
610
|
-
this.broadcastState();
|
|
611
|
-
}
|
|
681
|
+
onState: (state) => this.handleClientState(state)
|
|
612
682
|
});
|
|
613
683
|
}
|
|
614
684
|
/**
|
|
685
|
+
* Client state transition. A server stand-down ('stand-down' status — the
|
|
686
|
+
* superseded/flap-cooldown close, distinguishable from an ordinary close
|
|
687
|
+
* through the status) runs the ownership check NOW with grace, so a
|
|
688
|
+
* duplicate that already lost the control socket yields within one server
|
|
689
|
+
* round trip instead of waiting for the next watch tick and fighting
|
|
690
|
+
* (supersede war).
|
|
691
|
+
*/
|
|
692
|
+
handleClientState(state) {
|
|
693
|
+
if (state.status === "stand-down") this.runOwnershipCheck(this.standDownGraceMs);
|
|
694
|
+
this.lastState = state;
|
|
695
|
+
this.broadcastState();
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
615
698
|
* The identity resolved on connect. Persist the name so a later scutil
|
|
616
699
|
* failure reuses it instead of adopting the volatile hostname, and log the
|
|
617
700
|
* source — a `hostname-fallback` after we'd previously registered under
|
|
@@ -693,7 +776,7 @@ var PortalDaemon = class {
|
|
|
693
776
|
appUrl: this.appUrl,
|
|
694
777
|
build: portalDaemonBuild(),
|
|
695
778
|
portal: {
|
|
696
|
-
status: portal?.status ?? "off",
|
|
779
|
+
status: portal?.status === "stand-down" ? "connecting" : portal?.status ?? "off",
|
|
697
780
|
machineName: portal?.machineName ?? "",
|
|
698
781
|
friendlyName: portal?.friendlyName ?? "",
|
|
699
782
|
error: portal?.error ?? null,
|
|
@@ -786,13 +869,19 @@ var PortalDaemon = class {
|
|
|
786
869
|
* drains attached clients, who reconnect within ~500ms) and spawn from this
|
|
787
870
|
* binary. Both spawners apply the same rule, so a host converges on the
|
|
788
871
|
* newest installed build with at most one bounce.
|
|
872
|
+
*
|
|
873
|
+
* Returns whether a daemon is actually listening when it's done. A spawned
|
|
874
|
+
* daemon that never binds (its process died on startup — e.g. the Windows
|
|
875
|
+
* EACCES bind failure this used to swallow) resolves `false` so callers can
|
|
876
|
+
* SAY so, instead of silently handing the caller a socket nobody serves and
|
|
877
|
+
* leaving the reconnect loop to spin forever with no visible error.
|
|
789
878
|
*/
|
|
790
879
|
async function ensureDaemonRunning(appUrl) {
|
|
791
880
|
const { socketPath } = daemonPaths(appUrl);
|
|
792
881
|
if (await isDaemonListening(socketPath)) {
|
|
793
882
|
const status = await queryDaemonStatus(appUrl);
|
|
794
|
-
if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return;
|
|
795
|
-
if (await stopDaemonForHandoff(appUrl) === "failed") return;
|
|
883
|
+
if (!status || !isNewerBuild(portalDaemonBuild(), status.build)) return true;
|
|
884
|
+
if (await stopDaemonForHandoff(appUrl) === "failed") return true;
|
|
796
885
|
}
|
|
797
886
|
const entry = process.argv[1];
|
|
798
887
|
const args = entry ? [
|
|
@@ -806,9 +895,10 @@ async function ensureDaemonRunning(appUrl) {
|
|
|
806
895
|
env: process.env
|
|
807
896
|
}).unref();
|
|
808
897
|
for (let i = 0; i < 50; i += 1) {
|
|
809
|
-
if (await isDaemonListening(socketPath)) return;
|
|
898
|
+
if (await isDaemonListening(socketPath)) return true;
|
|
810
899
|
await sleep(100);
|
|
811
900
|
}
|
|
901
|
+
return false;
|
|
812
902
|
}
|
|
813
903
|
/**
|
|
814
904
|
* Entry the CLI calls when launched with PORTAL_DAEMON_FLAG: become the daemon.
|
|
@@ -829,7 +919,7 @@ function startPortalDaemon(appUrl) {
|
|
|
829
919
|
return new PortalDaemon(appUrl, null).listen();
|
|
830
920
|
}
|
|
831
921
|
async function isDaemonListening(socketPath) {
|
|
832
|
-
if (!await pathExists(socketPath)) return false;
|
|
922
|
+
if (!isNamedPipePath(socketPath) && !await pathExists(socketPath)) return false;
|
|
833
923
|
return new Promise((resolve) => {
|
|
834
924
|
const probe = connect(socketPath);
|
|
835
925
|
probe.on("connect", () => {
|
|
@@ -956,7 +1046,7 @@ async function stopDaemon(appUrl) {
|
|
|
956
1046
|
function stateMessage(state, declined) {
|
|
957
1047
|
return {
|
|
958
1048
|
t: "state",
|
|
959
|
-
status: state.status,
|
|
1049
|
+
status: state.status === "stand-down" ? "connecting" : state.status,
|
|
960
1050
|
machineName: state.machineName,
|
|
961
1051
|
friendlyName: state.friendlyName,
|
|
962
1052
|
error: state.error,
|
|
@@ -1,14 +1,21 @@
|
|
|
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-DyAyWGH-.mjs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { connect } from "node:net";
|
|
5
5
|
|
|
6
6
|
//#region ../portal-daemon/src/daemon-client.ts
|
|
7
|
+
/** Reconnect delay when an established daemon connection drops. */
|
|
8
|
+
const RECONNECT_DELAY_MS = 500;
|
|
9
|
+
/** Base delay between daemon SPAWN retries (doubles per consecutive failure). */
|
|
10
|
+
const START_RETRY_BASE_MS = 2e3;
|
|
11
|
+
/** Consecutive failed spawns before start() stops respawning. */
|
|
12
|
+
const MAX_START_ATTEMPTS = 3;
|
|
7
13
|
var PortalDaemonClient = class {
|
|
8
14
|
sessionId = randomUUID();
|
|
9
15
|
socket = null;
|
|
10
16
|
disposed = false;
|
|
11
17
|
reconnectTimer = null;
|
|
18
|
+
startFailures = 0;
|
|
12
19
|
lastBind = null;
|
|
13
20
|
fallbackCwd = null;
|
|
14
21
|
wantEnabled = false;
|
|
@@ -21,7 +28,21 @@ var PortalDaemonClient = class {
|
|
|
21
28
|
/** Spawn the daemon if needed and attach. Idempotent. */
|
|
22
29
|
async start() {
|
|
23
30
|
if (this.disposed) return;
|
|
24
|
-
await ensureDaemonRunning(this.opts.appUrl)
|
|
31
|
+
if (!await ensureDaemonRunning(this.opts.appUrl)) {
|
|
32
|
+
this.startFailures += 1;
|
|
33
|
+
const exhausted = this.startFailures >= MAX_START_ATTEMPTS;
|
|
34
|
+
this.opts.onState({
|
|
35
|
+
status: "error",
|
|
36
|
+
machineName: "",
|
|
37
|
+
friendlyName: "",
|
|
38
|
+
error: exhausted ? "portal daemon failed to start repeatedly; toggle sharing to retry" : "portal daemon failed to start (control socket never came up)",
|
|
39
|
+
grantedAgentIds: [],
|
|
40
|
+
declinedAgentIds: []
|
|
41
|
+
});
|
|
42
|
+
if (!exhausted) this.scheduleReconnect(START_RETRY_BASE_MS * 2 ** (this.startFailures - 1));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
this.startFailures = 0;
|
|
25
46
|
this.connect();
|
|
26
47
|
}
|
|
27
48
|
connect() {
|
|
@@ -74,12 +95,12 @@ var PortalDaemonClient = class {
|
|
|
74
95
|
socket.on("close", onClose);
|
|
75
96
|
socket.on("error", onClose);
|
|
76
97
|
}
|
|
77
|
-
scheduleReconnect() {
|
|
98
|
+
scheduleReconnect(delayMs = RECONNECT_DELAY_MS) {
|
|
78
99
|
if (this.disposed || this.reconnectTimer) return;
|
|
79
100
|
this.reconnectTimer = setTimeout(() => {
|
|
80
101
|
this.reconnectTimer = null;
|
|
81
102
|
this.start();
|
|
82
|
-
},
|
|
103
|
+
}, delayMs);
|
|
83
104
|
this.reconnectTimer.unref();
|
|
84
105
|
}
|
|
85
106
|
onMessage(msg) {
|
|
@@ -109,6 +130,10 @@ var PortalDaemonClient = class {
|
|
|
109
130
|
/** Turn machine sharing on (the daemon connects the portal). */
|
|
110
131
|
enable() {
|
|
111
132
|
this.wantEnabled = true;
|
|
133
|
+
if (this.startFailures >= MAX_START_ATTEMPTS) {
|
|
134
|
+
this.startFailures = 0;
|
|
135
|
+
this.start();
|
|
136
|
+
}
|
|
112
137
|
this.send({ t: "enable" });
|
|
113
138
|
}
|
|
114
139
|
/** Turn machine sharing off. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as version$1 } from "./rest-
|
|
3
|
-
import {
|
|
2
|
+
import { c as version$1 } from "./rest-CU32bzPu.mjs";
|
|
3
|
+
import { D as getConfigPath } from "./print-MMOJZcKb.mjs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { err, ok } from "neverthrow";
|
|
@@ -1547,6 +1547,51 @@ function formatMb(bytes) {
|
|
|
1547
1547
|
return `${(bytes / MB).toFixed(1)} MB`;
|
|
1548
1548
|
}
|
|
1549
1549
|
|
|
1550
|
+
//#endregion
|
|
1551
|
+
//#region src/chat/tui/autowrap.ts
|
|
1552
|
+
/**
|
|
1553
|
+
* Why the TUI turns the terminal's autowrap off for its lifetime.
|
|
1554
|
+
*
|
|
1555
|
+
* OpenTUI's incremental cell-diff emitter positions every run with an
|
|
1556
|
+
* absolute cursor move — it never relies on the terminal to advance the
|
|
1557
|
+
* cursor — but it leaves the terminal's autowrap (DECAWM) enabled. When a
|
|
1558
|
+
* run reaches the last column, the host terminal wraps its cursor to column
|
|
1559
|
+
* 1 of the next row and paints the run's tail there, into cells owned by a
|
|
1560
|
+
* different element. The diff buffer then records the *intended* cells, so
|
|
1561
|
+
* those stray bytes are never repainted and unrelated text smears together
|
|
1562
|
+
* until a full repaint (a resize). Filed upstream with a repro and root
|
|
1563
|
+
* cause: https://github.com/anomalyco/opentui/issues/1187 — unfixed there,
|
|
1564
|
+
* the consumer-side fix (verified in numux#115) is to disable DECAWM while
|
|
1565
|
+
* the renderer runs. Pane content still wraps, inside OpenTUI's own grid.
|
|
1566
|
+
*/
|
|
1567
|
+
const AUTOWRAP_OFF = "\x1B[?7l";
|
|
1568
|
+
const AUTOWRAP_ON = "\x1B[?7h";
|
|
1569
|
+
/** Escape hatch: if any surface turns out to depend on host autowrap. */
|
|
1570
|
+
const keepAutowrap = () => Boolean(process.env["SKYDIVE_TUI_AUTOWRAP"]);
|
|
1571
|
+
let disabledBy = false;
|
|
1572
|
+
/**
|
|
1573
|
+
* Disable host autowrap so the diff emitter's runs can never wrap onto the
|
|
1574
|
+
* next row. No-op when stdout is not a terminal (piped output, tests) and
|
|
1575
|
+
* under `SKYDIVE_TUI_AUTOWRAP`.
|
|
1576
|
+
*/
|
|
1577
|
+
function disableTerminalAutowrap(stdout = process.stdout) {
|
|
1578
|
+
if (!stdout.isTTY || keepAutowrap() || disabledBy) return;
|
|
1579
|
+
disabledBy = true;
|
|
1580
|
+
stdout.write(AUTOWRAP_OFF);
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Restore host autowrap. The terminal mode is a property of the host, so
|
|
1584
|
+
* every teardown path must run this even when the renderer cannot finish
|
|
1585
|
+
* its own teardown: the renderer's `onDestroy` hook covers renderer-driven
|
|
1586
|
+
* exits, and the forced-fallback reset in `hard-exit.ts` carries the
|
|
1587
|
+
* restore for a teardown that never got there. Idempotent.
|
|
1588
|
+
*/
|
|
1589
|
+
function restoreTerminalAutowrap(stdout = process.stdout) {
|
|
1590
|
+
if (!disabledBy) return;
|
|
1591
|
+
disabledBy = false;
|
|
1592
|
+
stdout.write(AUTOWRAP_ON);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1550
1595
|
//#endregion
|
|
1551
1596
|
//#region src/crash/install.ts
|
|
1552
1597
|
let installed = false;
|
|
@@ -1566,6 +1611,7 @@ function installCrashHandler(opts = {}) {
|
|
|
1566
1611
|
if (installed) return;
|
|
1567
1612
|
installed = true;
|
|
1568
1613
|
process.on("uncaughtException", (error) => {
|
|
1614
|
+
restoreTerminalAutowrap();
|
|
1569
1615
|
const memory = safeMemory();
|
|
1570
1616
|
printFatalNotice(writeCrashReport(buildCrashReport({
|
|
1571
1617
|
reason: "uncaughtException",
|
|
@@ -1594,4 +1640,4 @@ function printFatalNotice(file, memory) {
|
|
|
1594
1640
|
}
|
|
1595
1641
|
|
|
1596
1642
|
//#endregion
|
|
1597
|
-
export {
|
|
1643
|
+
export { aliasActivationHint as A, getSessionIdentity as B, themeMode as C, themesForMode as D, themes as E, SUPPORTED_SHELLS as F, resolveWorkspaceId as H, defaultInstallEnv as I, detectShell as L, installAgentAlias as M, slugifyAliasName as N, buildImportSeedPrompt as O, takenAliasNames as P, ensureActiveOrganization as R, themeForMode as S, themeVersion as T, setActiveWorkspace as U, listWorkspaces as V, applyTheme as _, writeCrashReport as a, noColorRequested as b, brandHelpArt as c, maybeStartProfiling as d, profileDir as f, DEFAULT_THEME_ID as g, writeArtifact as h, buildCrashReport as i, dedupeAliasName as j, machineOsFromPlatform as k, splashFitsWidth as l, record as m, disableTerminalAutowrap as n, MARK_CELLS as o, profilingEnabled as p, restoreTerminalAutowrap as r, WORDMARK as s, installCrashHandler as t, getAutomaticLogsDir as u, findTheme as v, themeModeFromColorFgBg as w, theme as x, monoTheme as y, getActiveWorkspaceId as z };
|
|
@@ -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-CU32bzPu.mjs";
|
|
3
|
+
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-MMOJZcKb.mjs";
|
|
4
4
|
import "./billing-blocked-D3l5kJlX.mjs";
|
|
5
5
|
|
|
6
6
|
export { messageGet, readStdin, resolveAgent, runPrint };
|