skydive-cli 0.2.0-beta.615 → 0.2.0-beta.624
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/README.md +47 -0
- package/dist/js/bin.mjs +758 -118
- package/dist/js/{boot-BJZrdTk0.mjs → boot-CI6nXtmu.mjs} +232 -18
- package/dist/js/client-CKlIpyAb.mjs +5 -0
- package/dist/js/{client-BigvBsPk.mjs → client-DT9bWi1D.mjs} +148 -8
- package/dist/js/daemon-CqhMYzpN.mjs +6 -0
- package/dist/js/daemon-DfMgVlYE.mjs +607 -0
- package/dist/js/{install-B5sKLfRc.mjs → install-DKTnh1gy.mjs} +2 -2
- package/dist/js/{print-nefugWMx.mjs → print-CB6L_1YB.mjs} +1 -1
- package/dist/js/{print-CNIvmpqW.mjs → print-DaPpTqxu.mjs} +1 -1
- package/dist/js/{print-share-rM8kNeEw.mjs → print-share-D8Nn1qFq.mjs} +2 -2
- package/dist/js/{raw-pty-Dbi2kb9v.mjs → raw-pty-BcjbTjHJ.mjs} +1 -1
- package/dist/js/raw-pty-ChUHav4d.mjs +5 -0
- package/package.json +1 -1
- package/dist/js/api-CLTp-mKM.mjs +0 -145
- package/dist/js/client-D6UUwESi.mjs +0 -6
- package/dist/js/raw-pty-pspO57gT.mjs +0 -5
- /package/dist/js/{client-BVOAwU8M.mjs → client-DfcJFEbh.mjs} +0 -0
package/dist/js/bin.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-
|
|
3
|
-
import { A as
|
|
2
|
+
import { C as setActiveWorkspace, S as listWorkspaces, T as version, _ as themes, b as getActiveWorkspaceId, o as brandHelpArt, t as installCrashHandler, w as name, x as getSessionIdentity, y as ensureActiveOrganization } from "./install-DKTnh1gy.mjs";
|
|
3
|
+
import { A as resolveAppUrl, C as getPromptHistoryPath, D as getStoredApiKeyId, E as getShareMachineDefault, F as saveConfig, I as saveSession, M as resolveManagementAuth, N as resolveSession, O as getStoredApiKeyWorkspaceName, R as setLastSeenVersion, S as getLastSeenVersion, _ as API_KEY_PREFIX, b as deleteConfig, g as API_KEY_FAMILY_PREFIX, h as API_KEYS_URL, i as resolveAgent, j as resolveConfig, k as getUpdateCheckDisabled, v as DEFAULT_API_URL, x as getConfigPath } from "./print-DaPpTqxu.mjs";
|
|
4
4
|
import { n as printError, r as printTable, t as output } from "./output-DYzzdXYV.mjs";
|
|
5
5
|
import { n as createRestClient } from "./rest-Q5yl_b2-.mjs";
|
|
6
|
-
import { a as registerPortalDevice,
|
|
7
|
-
import {
|
|
6
|
+
import { a as registerPortalDevice, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice, s as machineIdentity } from "./client-DT9bWi1D.mjs";
|
|
7
|
+
import { c as PORTAL_DAEMON_FLAG, i as queryDaemonStatus, l as daemonPaths, n as ensureDaemonRunning, o as stopDaemon } from "./daemon-DfMgVlYE.mjs";
|
|
8
|
+
import { t as SandboxStream } from "./client-DfcJFEbh.mjs";
|
|
8
9
|
import { hideBin } from "yargs/helpers";
|
|
9
10
|
import yargs from "yargs";
|
|
10
11
|
import { hostname } from "node:os";
|
|
@@ -16,7 +17,9 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
16
17
|
import { createHash } from "node:crypto";
|
|
17
18
|
import fs from "node:fs";
|
|
18
19
|
import zlib from "node:zlib";
|
|
20
|
+
import fsp from "node:fs/promises";
|
|
19
21
|
import semver from "semver";
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
20
23
|
|
|
21
24
|
//#region src/types.ts
|
|
22
25
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
@@ -1195,7 +1198,7 @@ const importCommand = {
|
|
|
1195
1198
|
process.exit(1);
|
|
1196
1199
|
}
|
|
1197
1200
|
}
|
|
1198
|
-
const { runChat } = await import("./boot-
|
|
1201
|
+
const { runChat } = await import("./boot-CI6nXtmu.mjs");
|
|
1199
1202
|
await runChat({
|
|
1200
1203
|
appUrl,
|
|
1201
1204
|
sessionToken: session.value.sessionToken,
|
|
@@ -1222,7 +1225,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1222
1225
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login --web\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1223
1226
|
process.exit(1);
|
|
1224
1227
|
}
|
|
1225
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1228
|
+
const { connectMachineShare } = await import("./print-share-D8Nn1qFq.mjs");
|
|
1226
1229
|
const machineShare = await connectMachineShare({
|
|
1227
1230
|
appUrl,
|
|
1228
1231
|
sessionToken: session.value.sessionToken,
|
|
@@ -1230,7 +1233,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1230
1233
|
});
|
|
1231
1234
|
const extra = (argv.print ?? "").trim();
|
|
1232
1235
|
const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1233
|
-
const { runPrint } = await import("./print-
|
|
1236
|
+
const { runPrint } = await import("./print-CB6L_1YB.mjs");
|
|
1234
1237
|
try {
|
|
1235
1238
|
const result = await runPrint({
|
|
1236
1239
|
appUrl,
|
|
@@ -1511,7 +1514,7 @@ const chatCommand = {
|
|
|
1511
1514
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1512
1515
|
process.exit(1);
|
|
1513
1516
|
}
|
|
1514
|
-
const { runChat } = await import("./boot-
|
|
1517
|
+
const { runChat } = await import("./boot-CI6nXtmu.mjs");
|
|
1515
1518
|
await runChat({
|
|
1516
1519
|
appUrl,
|
|
1517
1520
|
sessionToken: session.value.sessionToken,
|
|
@@ -1548,7 +1551,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1548
1551
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1549
1552
|
process.exit(1);
|
|
1550
1553
|
}
|
|
1551
|
-
const { runPrint, readStdin } = await import("./print-
|
|
1554
|
+
const { runPrint, readStdin } = await import("./print-CB6L_1YB.mjs");
|
|
1552
1555
|
let prompt = (argv.print ?? "").trim();
|
|
1553
1556
|
if (!prompt) {
|
|
1554
1557
|
if (process.stdin.isTTY) {
|
|
@@ -1563,7 +1566,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1563
1566
|
}
|
|
1564
1567
|
let machineShare = null;
|
|
1565
1568
|
if (resolveShareMachine(argv)) {
|
|
1566
|
-
const { connectMachineShare } = await import("./print-share-
|
|
1569
|
+
const { connectMachineShare } = await import("./print-share-D8Nn1qFq.mjs");
|
|
1567
1570
|
machineShare = await connectMachineShare({
|
|
1568
1571
|
appUrl,
|
|
1569
1572
|
sessionToken: session.value.sessionToken,
|
|
@@ -1622,7 +1625,7 @@ const getCommand = {
|
|
|
1622
1625
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1623
1626
|
process.exit(1);
|
|
1624
1627
|
}
|
|
1625
|
-
const { messageGet } = await import("./print-
|
|
1628
|
+
const { messageGet } = await import("./print-CB6L_1YB.mjs");
|
|
1626
1629
|
try {
|
|
1627
1630
|
const result = await messageGet({
|
|
1628
1631
|
appUrl,
|
|
@@ -1819,7 +1822,7 @@ const switchCommand = {
|
|
|
1819
1822
|
printError("The workspace picker needs the Bun runtime and it could not be set up automatically. Pass a workspace slug instead, or install Bun and retry.");
|
|
1820
1823
|
process.exit(1);
|
|
1821
1824
|
}
|
|
1822
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
1825
|
+
const { runWorkspacePicker } = await import("./boot-CI6nXtmu.mjs");
|
|
1823
1826
|
await runWorkspacePicker(session);
|
|
1824
1827
|
return;
|
|
1825
1828
|
}
|
|
@@ -1897,7 +1900,7 @@ const openCommand = {
|
|
|
1897
1900
|
const agent = argv.agent ? resolveAgent((await fetchPortalDevices(session)).agents, argv.agent) : null;
|
|
1898
1901
|
const cwd = argv.cwd ? path.resolve(argv.cwd) : process.cwd();
|
|
1899
1902
|
const { machineName } = machineIdentity();
|
|
1900
|
-
const { PortalClient } = await import("./client-
|
|
1903
|
+
const { PortalClient } = await import("./client-CKlIpyAb.mjs");
|
|
1901
1904
|
let lastLine = "";
|
|
1902
1905
|
let signalConnected;
|
|
1903
1906
|
const connected = new Promise((resolve) => {
|
|
@@ -1906,7 +1909,7 @@ const openCommand = {
|
|
|
1906
1909
|
const client = new PortalClient({
|
|
1907
1910
|
appUrl: session.appUrl,
|
|
1908
1911
|
sessionToken: session.sessionToken,
|
|
1909
|
-
cwd,
|
|
1912
|
+
resolveCwd: () => cwd,
|
|
1910
1913
|
onState: (state) => {
|
|
1911
1914
|
if (state.status === "connected") signalConnected();
|
|
1912
1915
|
const line = state.status === "error" ? `portal: connection error: ${state.error ?? "unknown"} — retrying` : `portal: ${state.status}`;
|
|
@@ -2021,10 +2024,111 @@ function buildDeviceTable(devices, agents, thisMachineName) {
|
|
|
2021
2024
|
])
|
|
2022
2025
|
};
|
|
2023
2026
|
}
|
|
2027
|
+
/**
|
|
2028
|
+
* `skydive portal daemon <cmd>` — operator control of the per-host portal daemon
|
|
2029
|
+
* (the process that owns the single portal connection and routes exec per
|
|
2030
|
+
* conversation). Distinct from the agent-facing `platform portal`: this manages
|
|
2031
|
+
* the local daemon a human runs, so it lives on the skydive CLI only.
|
|
2032
|
+
*/
|
|
2033
|
+
const daemonStatusCommand = {
|
|
2034
|
+
command: "status",
|
|
2035
|
+
describe: "Show the portal daemon: running state, connection, and routing map",
|
|
2036
|
+
handler: async (argv) => {
|
|
2037
|
+
const status = await queryDaemonStatus(requireSession(argv).appUrl);
|
|
2038
|
+
if (argv.json) {
|
|
2039
|
+
output(argv, status ?? { running: false });
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (!status) {
|
|
2043
|
+
console.log("portal daemon: not running");
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const cwdCount = Object.keys(status.cwds).length;
|
|
2047
|
+
console.log(`portal daemon: running (pid ${status.pid})`);
|
|
2048
|
+
console.log(` app: ${status.appUrl}`);
|
|
2049
|
+
console.log(` connection: ${status.portal.status}${status.portal.error ? ` (${status.portal.error})` : ""}`);
|
|
2050
|
+
if (status.portal.machineName) console.log(` machine: ${status.portal.friendlyName}`);
|
|
2051
|
+
console.log(` attached: ${status.clientCount} CLI session(s)`);
|
|
2052
|
+
console.log(` granted: ${status.portal.grantedAgentIds.length} agent(s)`);
|
|
2053
|
+
console.log(` routing: ${cwdCount} conversation(s)`);
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
const daemonStopCommand = {
|
|
2057
|
+
command: "stop",
|
|
2058
|
+
describe: "Stop the portal daemon, cutting every attached session and live exec/tunnel",
|
|
2059
|
+
handler: async (argv) => {
|
|
2060
|
+
const result = await stopDaemon(requireSession(argv).appUrl);
|
|
2061
|
+
if (argv.json) {
|
|
2062
|
+
output(argv, { result });
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
console.log(result === "not-running" ? "portal daemon: not running" : result === "failed" ? "portal daemon: could not be stopped" : `portal daemon: stopped${result === "killed" ? " (force-killed)" : ""}`);
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
const daemonStartCommand = {
|
|
2069
|
+
command: "start",
|
|
2070
|
+
describe: "Start the portal daemon if it is not already running",
|
|
2071
|
+
handler: async (argv) => {
|
|
2072
|
+
const session = requireSession(argv);
|
|
2073
|
+
await ensureDaemonRunning(session.appUrl);
|
|
2074
|
+
const status = await queryDaemonStatus(session.appUrl);
|
|
2075
|
+
if (argv.json) {
|
|
2076
|
+
output(argv, {
|
|
2077
|
+
running: !!status,
|
|
2078
|
+
pid: status?.pid ?? null
|
|
2079
|
+
});
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
console.log(status ? `portal daemon: running (pid ${status.pid})` : "portal daemon: failed to start");
|
|
2083
|
+
}
|
|
2084
|
+
};
|
|
2085
|
+
const daemonRestartCommand = {
|
|
2086
|
+
command: "restart",
|
|
2087
|
+
describe: "Stop and restart the portal daemon (e.g. to pick up a new build)",
|
|
2088
|
+
handler: async (argv) => {
|
|
2089
|
+
const session = requireSession(argv);
|
|
2090
|
+
await stopDaemon(session.appUrl);
|
|
2091
|
+
await ensureDaemonRunning(session.appUrl);
|
|
2092
|
+
const status = await queryDaemonStatus(session.appUrl);
|
|
2093
|
+
if (argv.json) {
|
|
2094
|
+
output(argv, {
|
|
2095
|
+
running: !!status,
|
|
2096
|
+
pid: status?.pid ?? null
|
|
2097
|
+
});
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
console.log(status ? `portal daemon: restarted (pid ${status.pid})` : "portal daemon: failed to restart");
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const daemonLogsCommand = {
|
|
2104
|
+
command: "logs",
|
|
2105
|
+
describe: "Print the portal daemon's log",
|
|
2106
|
+
builder: (y) => y.option("follow", {
|
|
2107
|
+
alias: "f",
|
|
2108
|
+
type: "boolean",
|
|
2109
|
+
default: false,
|
|
2110
|
+
describe: "Follow the log (like tail -f)"
|
|
2111
|
+
}),
|
|
2112
|
+
handler: async (argv) => {
|
|
2113
|
+
const { logPath } = daemonPaths(requireSession(argv).appUrl);
|
|
2114
|
+
const child = spawn("tail", argv.follow === true ? ["-f", logPath] : [
|
|
2115
|
+
"-n",
|
|
2116
|
+
"200",
|
|
2117
|
+
logPath
|
|
2118
|
+
], { stdio: "inherit" });
|
|
2119
|
+
await new Promise((resolve) => child.on("close", () => resolve()));
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
const daemonCommand = {
|
|
2123
|
+
command: "daemon",
|
|
2124
|
+
describe: "Control the local portal daemon (status, stop, start, restart, logs)",
|
|
2125
|
+
builder: (y) => y.command(daemonStatusCommand).command(daemonStopCommand).command(daemonStartCommand).command(daemonRestartCommand).command(daemonLogsCommand).demandCommand(1, "Specify a subcommand: status, stop, start, restart, logs"),
|
|
2126
|
+
handler: () => {}
|
|
2127
|
+
};
|
|
2024
2128
|
const portalCommand = {
|
|
2025
2129
|
command: "portal",
|
|
2026
2130
|
describe: "Open this machine's portal to agents and manage their access",
|
|
2027
|
-
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status"),
|
|
2131
|
+
builder: (y) => y.command(openCommand).command(grantCommand).command(revokeCommand).command(statusCommand).command(daemonCommand).demandCommand(1, "Specify a subcommand: open, grant, revoke, status, daemon"),
|
|
2028
2132
|
handler: () => {}
|
|
2029
2133
|
};
|
|
2030
2134
|
|
|
@@ -2070,7 +2174,7 @@ const sandboxCommand = {
|
|
|
2070
2174
|
handler: async (argv) => {
|
|
2071
2175
|
const session = requireSession(argv);
|
|
2072
2176
|
const { createRestClient } = await import("./rest-CBROy0L-.mjs");
|
|
2073
|
-
const { resolveAgent } = await import("./print-
|
|
2177
|
+
const { resolveAgent } = await import("./print-CB6L_1YB.mjs");
|
|
2074
2178
|
const client = createRestClient({
|
|
2075
2179
|
appUrl: session.appUrl,
|
|
2076
2180
|
sessionToken: session.sessionToken
|
|
@@ -2139,7 +2243,7 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2139
2243
|
return 1;
|
|
2140
2244
|
}
|
|
2141
2245
|
console.error(`Connecting to ${agentName}'s sandbox… (Ctrl-] detaches)`);
|
|
2142
|
-
const { runRawPtyPassthrough } = await import("./raw-pty-
|
|
2246
|
+
const { runRawPtyPassthrough } = await import("./raw-pty-ChUHav4d.mjs");
|
|
2143
2247
|
const result = await runRawPtyPassthrough({
|
|
2144
2248
|
stdin: process.stdin,
|
|
2145
2249
|
stdout: process.stdout,
|
|
@@ -2151,6 +2255,630 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2151
2255
|
return result.code;
|
|
2152
2256
|
}
|
|
2153
2257
|
|
|
2258
|
+
//#endregion
|
|
2259
|
+
//#region src/update-check/manifest.ts
|
|
2260
|
+
/**
|
|
2261
|
+
* The release manifest the binary pipeline publishes
|
|
2262
|
+
* (`.github/workflows/release-skydive-cli-binaries.yml`) and the api serves
|
|
2263
|
+
* (`apps/anyone/api/src/routes/cli-releases.ts`). The update *check* only
|
|
2264
|
+
* needs `version` (see ./sources.ts), but the self-updater
|
|
2265
|
+
* (`../commands/update.ts`) needs the per-target byte pointer + integrity
|
|
2266
|
+
* digest too, so the full shape lives here and both consume it.
|
|
2267
|
+
*
|
|
2268
|
+
* Kept deliberately in sync with the api's `manifestSchema`: a field the api
|
|
2269
|
+
* validates but we drop here is fine, but a field we *require* that the api
|
|
2270
|
+
* doesn't emit would make every install fail — so required fields mirror the
|
|
2271
|
+
* api exactly (name, version, channel, per-target key/sha256/size).
|
|
2272
|
+
*/
|
|
2273
|
+
const releaseManifestSchema = z.object({
|
|
2274
|
+
name: z.string().min(1),
|
|
2275
|
+
version: z.string().min(1),
|
|
2276
|
+
channel: z.string().min(1),
|
|
2277
|
+
pubDate: z.string().optional(),
|
|
2278
|
+
targets: z.record(z.object({
|
|
2279
|
+
key: z.string().min(1),
|
|
2280
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
2281
|
+
size: z.number().int().positive()
|
|
2282
|
+
}))
|
|
2283
|
+
});
|
|
2284
|
+
/**
|
|
2285
|
+
* The four release targets the pipeline builds. Named `<os>-<arch>` to match
|
|
2286
|
+
* the manifest's `targets` keys and the install script's `$OS-$ARCH`.
|
|
2287
|
+
*/
|
|
2288
|
+
const RELEASE_TARGETS = [
|
|
2289
|
+
"darwin-arm64",
|
|
2290
|
+
"darwin-x64",
|
|
2291
|
+
"linux-arm64",
|
|
2292
|
+
"linux-x64"
|
|
2293
|
+
];
|
|
2294
|
+
function isReleaseTarget(value) {
|
|
2295
|
+
return RELEASE_TARGETS.some((target) => target === value);
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Map this host's `process.platform` / `process.arch` to a release target,
|
|
2299
|
+
* or null when we don't publish a binary for it. Mirrors the install
|
|
2300
|
+
* script's `uname -s` / `uname -m` normalization (arm64/aarch64 -> arm64,
|
|
2301
|
+
* x86_64/amd64 -> x64) so a CLI self-update resolves the same object a fresh
|
|
2302
|
+
* `curl | sh` install would.
|
|
2303
|
+
*/
|
|
2304
|
+
function hostReleaseTarget(platform, arch) {
|
|
2305
|
+
const os = platform === "darwin" ? "darwin" : platform === "linux" ? "linux" : null;
|
|
2306
|
+
if (!os) return null;
|
|
2307
|
+
const cpu = arch === "arm64" || arch === "aarch64" ? "arm64" : arch === "x64" || arch === "x86_64" || arch === "amd64" ? "x64" : null;
|
|
2308
|
+
if (!cpu) return null;
|
|
2309
|
+
const target = `${os}-${cpu}`;
|
|
2310
|
+
return isReleaseTarget(target) ? target : null;
|
|
2311
|
+
}
|
|
2312
|
+
function lookupTarget(manifest, platform, arch) {
|
|
2313
|
+
const target = hostReleaseTarget(platform, arch);
|
|
2314
|
+
if (!target) return {
|
|
2315
|
+
ok: false,
|
|
2316
|
+
reason: "unsupported-host",
|
|
2317
|
+
target: null
|
|
2318
|
+
};
|
|
2319
|
+
const entry = manifest.targets[target];
|
|
2320
|
+
if (!entry) return {
|
|
2321
|
+
ok: false,
|
|
2322
|
+
reason: "missing-from-manifest",
|
|
2323
|
+
target
|
|
2324
|
+
};
|
|
2325
|
+
return {
|
|
2326
|
+
ok: true,
|
|
2327
|
+
target,
|
|
2328
|
+
entry
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
//#endregion
|
|
2333
|
+
//#region src/update-check/versions.ts
|
|
2334
|
+
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2335
|
+
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2336
|
+
* to stable. */
|
|
2337
|
+
function resolveChannel(version) {
|
|
2338
|
+
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2339
|
+
}
|
|
2340
|
+
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2341
|
+
function distTagForChannel(channel) {
|
|
2342
|
+
return channel === "canary" ? "beta" : "latest";
|
|
2343
|
+
}
|
|
2344
|
+
/**
|
|
2345
|
+
* True when `candidate` is a strictly newer release than `current`, per
|
|
2346
|
+
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2347
|
+
* Unparseable input is never newer, so garbage from the registry can't
|
|
2348
|
+
* produce a notice.
|
|
2349
|
+
*/
|
|
2350
|
+
function isNewerVersion(candidate, current) {
|
|
2351
|
+
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2352
|
+
if (!semver.valid(current, { loose: true })) return false;
|
|
2353
|
+
return semver.gt(candidate, current, { loose: true });
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/update-check/install.ts
|
|
2358
|
+
/**
|
|
2359
|
+
* The self-updater for compiled-binary installs. The update *check*
|
|
2360
|
+
* (./index.ts) is notify-only; this is what a user-invoked `skydive update`
|
|
2361
|
+
* (../commands/update.ts) runs to actually replace the binary.
|
|
2362
|
+
*
|
|
2363
|
+
* package-manager installs are never self-mutated here — that's the install
|
|
2364
|
+
* mode's own tool's job, and silently reaching into a global npm/yarn/pnpm
|
|
2365
|
+
* prefix is exactly the footgun the existing notice policy avoids. The
|
|
2366
|
+
* command handles that mode by printing the upgrade command; this module is
|
|
2367
|
+
* binary-only.
|
|
2368
|
+
*
|
|
2369
|
+
* The dangerous part is replacing a running executable in place, so the flow
|
|
2370
|
+
* is split: a pure {@link planUpdate} decides *whether* and *what* (testable
|
|
2371
|
+
* with no IO), and {@link downloadAndVerify} / {@link applyBinary} do the IO
|
|
2372
|
+
* with integrity checks and an atomic rename.
|
|
2373
|
+
*/
|
|
2374
|
+
/** Cap so a wrong URL or a hung CDN can't wedge `skydive update` forever. */
|
|
2375
|
+
const DOWNLOAD_TIMEOUT_MS = 6e4;
|
|
2376
|
+
/**
|
|
2377
|
+
* Decide what `skydive update` should do, given the running version, the
|
|
2378
|
+
* install source, the fetched manifest (or null), and this host. Pure — no
|
|
2379
|
+
* network, no filesystem — so every branch is unit-testable.
|
|
2380
|
+
*
|
|
2381
|
+
* `allowSameVersion` forces a reinstall of the current version (the `--force`
|
|
2382
|
+
* escape hatch for a corrupt binary); `allowDowngrade` lets an explicit
|
|
2383
|
+
* pinned/older manifest install over a newer running binary (rollback).
|
|
2384
|
+
*/
|
|
2385
|
+
function planUpdate(opts) {
|
|
2386
|
+
const { currentVersion, source, manifest, platform, arch } = opts;
|
|
2387
|
+
if (source !== "binary") return {
|
|
2388
|
+
kind: "unsupported-source",
|
|
2389
|
+
source,
|
|
2390
|
+
latestVersion: manifest?.version ?? currentVersion
|
|
2391
|
+
};
|
|
2392
|
+
if (!manifest) return {
|
|
2393
|
+
kind: "up-to-date",
|
|
2394
|
+
currentVersion
|
|
2395
|
+
};
|
|
2396
|
+
const newer = isNewerVersion(manifest.version, currentVersion);
|
|
2397
|
+
const same = manifest.version === currentVersion;
|
|
2398
|
+
if (!(newer || same && opts.allowSameVersion === true || !newer && !same && opts.allowDowngrade === true)) return {
|
|
2399
|
+
kind: "up-to-date",
|
|
2400
|
+
currentVersion
|
|
2401
|
+
};
|
|
2402
|
+
const lookup = lookupTarget(manifest, platform, arch);
|
|
2403
|
+
if (!lookup.ok) return lookup.reason === "unsupported-host" ? {
|
|
2404
|
+
kind: "unsupported-host",
|
|
2405
|
+
platform,
|
|
2406
|
+
arch
|
|
2407
|
+
} : {
|
|
2408
|
+
kind: "missing-from-manifest",
|
|
2409
|
+
target: lookup.target,
|
|
2410
|
+
version: manifest.version
|
|
2411
|
+
};
|
|
2412
|
+
return {
|
|
2413
|
+
kind: "update",
|
|
2414
|
+
target: lookup.target,
|
|
2415
|
+
version: manifest.version,
|
|
2416
|
+
key: lookup.entry.key,
|
|
2417
|
+
sha256: lookup.entry.sha256,
|
|
2418
|
+
size: lookup.entry.size
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
var UpdateError = class extends Error {};
|
|
2422
|
+
/** Read a Node errno string (`EACCES`, `EROFS`, ...) off an unknown thrown
|
|
2423
|
+
* value without a type assertion — the property is validated before it's read. */
|
|
2424
|
+
function errnoCode(error) {
|
|
2425
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
2426
|
+
const { code } = error;
|
|
2427
|
+
if (typeof code === "string") return code;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
/** Lowercase hex sha256 of a buffer, for comparing against the manifest. */
|
|
2431
|
+
function sha256Hex(bytes) {
|
|
2432
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Download the release object and verify its digest before it ever touches
|
|
2436
|
+
* the install path. Returns the raw bytes; the caller writes them atomically.
|
|
2437
|
+
* Throws {@link UpdateError} (with an actionable message) on any HTTP,
|
|
2438
|
+
* timeout, or integrity failure — never a bare fetch error.
|
|
2439
|
+
*/
|
|
2440
|
+
async function downloadAndVerify(opts) {
|
|
2441
|
+
const url = `${opts.downloadBaseUrl}/${opts.key}`;
|
|
2442
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
2443
|
+
const signal = opts.signal ?? AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS);
|
|
2444
|
+
let response;
|
|
2445
|
+
try {
|
|
2446
|
+
response = await doFetch(url, {
|
|
2447
|
+
signal,
|
|
2448
|
+
redirect: "follow"
|
|
2449
|
+
});
|
|
2450
|
+
} catch (error) {
|
|
2451
|
+
throw new UpdateError(`download failed from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2452
|
+
}
|
|
2453
|
+
if (!response.ok) throw new UpdateError(`download failed from ${url}: HTTP ${response.status}`);
|
|
2454
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
2455
|
+
if (bytes.byteLength === 0) throw new UpdateError(`download from ${url} was empty`);
|
|
2456
|
+
const actual = sha256Hex(bytes);
|
|
2457
|
+
if (actual !== opts.expectedSha256) throw new UpdateError(`sha256 mismatch (expected ${opts.expectedSha256}, got ${actual}) — the download may be corrupt; re-run \`skydive update\``);
|
|
2458
|
+
return bytes;
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Atomically replace the executable at `destPath` with `bytes`. The temp file
|
|
2462
|
+
* is written in the *same directory* as the target so the final `rename` is
|
|
2463
|
+
* atomic (rename across filesystems is not) — a crash mid-update leaves either
|
|
2464
|
+
* the old binary or the new one, never a truncated file.
|
|
2465
|
+
*
|
|
2466
|
+
* On a locked destination (Windows, or a read-only install dir) the rename
|
|
2467
|
+
* fails; we surface an actionable UpdateError instead of a raw errno. macOS
|
|
2468
|
+
* and Linux happily rename over a running executable (the old inode stays
|
|
2469
|
+
* live for the current process), which is what makes in-place self-update
|
|
2470
|
+
* safe here.
|
|
2471
|
+
*/
|
|
2472
|
+
async function applyBinary(destPath, bytes) {
|
|
2473
|
+
const dir = path.dirname(destPath);
|
|
2474
|
+
const tmp = path.join(dir, `.skydive-update-${process.pid}-${Date.now()}.tmp`);
|
|
2475
|
+
try {
|
|
2476
|
+
await fsp.writeFile(tmp, bytes, { mode: 493 });
|
|
2477
|
+
await fsp.chmod(tmp, 493);
|
|
2478
|
+
await fsp.rename(tmp, destPath);
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
await fsp.rm(tmp, { force: true }).catch(() => {});
|
|
2481
|
+
const code = errnoCode(error);
|
|
2482
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new UpdateError(`cannot write ${destPath} (permission denied). Re-run with the right permissions, or reinstall with the install script.`);
|
|
2483
|
+
throw new UpdateError(`failed to install the new binary at ${destPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Resolve the path of the currently running executable to overwrite. For a
|
|
2488
|
+
* `bun --compile` binary that's `process.execPath`; we resolve symlinks so a
|
|
2489
|
+
* `~/.local/bin/skydive` symlink into a versioned store updates the real
|
|
2490
|
+
* file, not the link. Returns null when the path can't be determined (e.g. a
|
|
2491
|
+
* non-binary context), which the command treats as "can't self-update".
|
|
2492
|
+
*/
|
|
2493
|
+
function resolveSelfPath(execPath = process.execPath) {
|
|
2494
|
+
if (!execPath) return null;
|
|
2495
|
+
try {
|
|
2496
|
+
return fs.realpathSync(execPath);
|
|
2497
|
+
} catch (_error) {
|
|
2498
|
+
return execPath;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
//#endregion
|
|
2503
|
+
//#region src/update-check/sources.ts
|
|
2504
|
+
function resolveInstallSource() {
|
|
2505
|
+
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2506
|
+
}
|
|
2507
|
+
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2508
|
+
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2509
|
+
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2510
|
+
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2511
|
+
const npmReleaseSource = {
|
|
2512
|
+
async fetchLatestVersion(channel, { signal }) {
|
|
2513
|
+
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2514
|
+
signal,
|
|
2515
|
+
headers: { accept: "application/json" }
|
|
2516
|
+
});
|
|
2517
|
+
if (!response.ok) return null;
|
|
2518
|
+
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2519
|
+
if (!tags.success) return null;
|
|
2520
|
+
const version = tags.data[distTagForChannel(channel)];
|
|
2521
|
+
return typeof version === "string" ? version : null;
|
|
2522
|
+
},
|
|
2523
|
+
async fetchManifest() {
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
/**
|
|
2528
|
+
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2529
|
+
* Serves the channel pointers the release workflow uploads
|
|
2530
|
+
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2531
|
+
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2532
|
+
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2533
|
+
*/
|
|
2534
|
+
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2535
|
+
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2536
|
+
* document the api's channel route serves (see
|
|
2537
|
+
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2538
|
+
* bucket this CDN fronts). */
|
|
2539
|
+
const binaryReleaseSource = {
|
|
2540
|
+
async fetchLatestVersion(channel, { signal }) {
|
|
2541
|
+
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2542
|
+
signal,
|
|
2543
|
+
headers: { accept: "application/json" }
|
|
2544
|
+
});
|
|
2545
|
+
if (!response.ok) return null;
|
|
2546
|
+
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
2547
|
+
if (!manifest.success) return null;
|
|
2548
|
+
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
2549
|
+
},
|
|
2550
|
+
async fetchManifest(channel, { signal }) {
|
|
2551
|
+
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2552
|
+
signal,
|
|
2553
|
+
headers: { accept: "application/json" }
|
|
2554
|
+
});
|
|
2555
|
+
if (!response.ok) return null;
|
|
2556
|
+
const manifest = releaseManifestSchema.safeParse(await response.json());
|
|
2557
|
+
return manifest.success ? manifest.data : null;
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
function releaseSourceForInstall(source) {
|
|
2561
|
+
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
//#endregion
|
|
2565
|
+
//#region src/update-check/notice.ts
|
|
2566
|
+
/** The install-mode-specific action line. A package-manager install is never
|
|
2567
|
+
* self-mutated — we only tell the user what to run. A binary install has a
|
|
2568
|
+
* built-in self-updater (`skydive update`, ../commands/update.ts), so that's
|
|
2569
|
+
* the action; the curl re-install stays the documented fallback. */
|
|
2570
|
+
function renderUpdateCommand(channel, source) {
|
|
2571
|
+
if (source === "binary") return channel === "canary" ? "skydive update --channel canary" : "skydive update";
|
|
2572
|
+
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
2573
|
+
}
|
|
2574
|
+
function renderUpdateNotice(opts) {
|
|
2575
|
+
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
2576
|
+
}
|
|
2577
|
+
/**
|
|
2578
|
+
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
2579
|
+
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
2580
|
+
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
2581
|
+
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
2582
|
+
* suppress it entirely rather than risk corrupting piped output.
|
|
2583
|
+
*/
|
|
2584
|
+
function shouldNotify(opts) {
|
|
2585
|
+
if (!opts.stderrIsTTY) return false;
|
|
2586
|
+
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
2587
|
+
return true;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
//#endregion
|
|
2591
|
+
//#region src/update-check/package-manager.ts
|
|
2592
|
+
const KNOWN = [
|
|
2593
|
+
"npm",
|
|
2594
|
+
"yarn",
|
|
2595
|
+
"pnpm",
|
|
2596
|
+
"bun"
|
|
2597
|
+
];
|
|
2598
|
+
function isKnown(value) {
|
|
2599
|
+
return KNOWN.some((m) => m === value);
|
|
2600
|
+
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Parse the manager name out of an `npm_config_user_agent` string. npm, yarn,
|
|
2603
|
+
* pnpm and bun all set it to `"<manager>/<version> ..."` when they spawn a
|
|
2604
|
+
* script, which is the most reliable signal for how *this* process was
|
|
2605
|
+
* launched. Returns null when the var is absent or unrecognized.
|
|
2606
|
+
*/
|
|
2607
|
+
function parseUserAgent(userAgent) {
|
|
2608
|
+
if (!userAgent) return null;
|
|
2609
|
+
const name = userAgent.trim().split("/")[0]?.toLowerCase();
|
|
2610
|
+
return name && isKnown(name) ? name : null;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Best-effort detection of the manager that owns this install. Order of
|
|
2614
|
+
* signals, most to least reliable:
|
|
2615
|
+
* 1. `npm_config_user_agent` — set by the manager that spawned us.
|
|
2616
|
+
* 2. the executable path — a global bin under a `pnpm`/`yarn`/`bun` store
|
|
2617
|
+
* names the manager even when we weren't spawned by it (e.g. the user
|
|
2618
|
+
* typed `skydive` directly).
|
|
2619
|
+
* Falls back to `npm`, the documented default install path, so the command we
|
|
2620
|
+
* show is always runnable — the prompt still lets the user decline and copy a
|
|
2621
|
+
* different one.
|
|
2622
|
+
*/
|
|
2623
|
+
function detectPackageManager(opts) {
|
|
2624
|
+
const fromUa = parseUserAgent(opts.env["npm_config_user_agent"]);
|
|
2625
|
+
if (fromUa) return fromUa;
|
|
2626
|
+
const exec = opts.execPath.toLowerCase();
|
|
2627
|
+
if (exec.includes("pnpm")) return "pnpm";
|
|
2628
|
+
if (exec.includes("yarn")) return "yarn";
|
|
2629
|
+
if (exec.includes("bun")) return "bun";
|
|
2630
|
+
return "npm";
|
|
2631
|
+
}
|
|
2632
|
+
/**
|
|
2633
|
+
* The global-install command (as argv) for a manager + channel. Each manager's
|
|
2634
|
+
* global-add spelling differs; the version target is the channel's dist-tag
|
|
2635
|
+
* (`latest`/`beta`), matching what the release workflow publishes.
|
|
2636
|
+
*/
|
|
2637
|
+
function installCommand(manager, channel) {
|
|
2638
|
+
const spec = `${name}@${distTagForChannel(channel)}`;
|
|
2639
|
+
switch (manager) {
|
|
2640
|
+
case "npm": return [
|
|
2641
|
+
"npm",
|
|
2642
|
+
"install",
|
|
2643
|
+
"-g",
|
|
2644
|
+
spec
|
|
2645
|
+
];
|
|
2646
|
+
case "pnpm": return [
|
|
2647
|
+
"pnpm",
|
|
2648
|
+
"add",
|
|
2649
|
+
"-g",
|
|
2650
|
+
spec
|
|
2651
|
+
];
|
|
2652
|
+
case "bun": return [
|
|
2653
|
+
"bun",
|
|
2654
|
+
"add",
|
|
2655
|
+
"-g",
|
|
2656
|
+
spec
|
|
2657
|
+
];
|
|
2658
|
+
case "yarn": return [
|
|
2659
|
+
"yarn",
|
|
2660
|
+
"global",
|
|
2661
|
+
"add",
|
|
2662
|
+
spec
|
|
2663
|
+
];
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
/** The command as a copy-pasteable string for prompts and notices. */
|
|
2667
|
+
function installCommandString(manager, channel) {
|
|
2668
|
+
return installCommand(manager, channel).join(" ");
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
//#endregion
|
|
2672
|
+
//#region src/update-check/pm-update.ts
|
|
2673
|
+
/**
|
|
2674
|
+
* The interactive upgrade path for a package-manager install: instead of only
|
|
2675
|
+
* printing "run npm install -g …", offer to run it now, and on confirm run
|
|
2676
|
+
* that exact command and relaunch the CLI on the new version.
|
|
2677
|
+
*
|
|
2678
|
+
* We never silently mutate a package-manager install (the manager owns it),
|
|
2679
|
+
* but an explicit prompt the user answers with a keypress is honest consent —
|
|
2680
|
+
* and it runs the *same* command we detected + display, so what you approve is
|
|
2681
|
+
* what executes. Non-interactive contexts (no TTY, --json/--quiet, CI) skip
|
|
2682
|
+
* the prompt entirely and fall back to the printed notice.
|
|
2683
|
+
*/
|
|
2684
|
+
/** Whether it's safe to show an interactive prompt at all. */
|
|
2685
|
+
function canPromptInteractively(opts) {
|
|
2686
|
+
if (opts.json || opts.quiet) return false;
|
|
2687
|
+
if (!opts.stdinIsTTY || !opts.stdoutIsTTY) return false;
|
|
2688
|
+
if (opts.env["CI"] || opts.env["SKYDIVE_NO_UPDATE_CHECK"]) return false;
|
|
2689
|
+
return true;
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Interpret a raw prompt answer. Default (empty / Enter) is **yes** — the
|
|
2693
|
+
* prompt renders `[Y/n]`, so a bare Enter accepts. Anything starting with `n`
|
|
2694
|
+
* (case-insensitive) is no; any other non-empty input is treated as no too,
|
|
2695
|
+
* so a fat-fingered answer never runs an install the user didn't mean.
|
|
2696
|
+
*/
|
|
2697
|
+
function interpretYesNo(answer) {
|
|
2698
|
+
const a = answer.trim().toLowerCase();
|
|
2699
|
+
if (a === "") return true;
|
|
2700
|
+
return a === "y" || a === "yes";
|
|
2701
|
+
}
|
|
2702
|
+
/** Ask a `[Y/n]` question on the TTY and resolve to the boolean answer. */
|
|
2703
|
+
async function promptYesNo(question) {
|
|
2704
|
+
const rl = createInterface({
|
|
2705
|
+
input: process.stdin,
|
|
2706
|
+
output: process.stdout
|
|
2707
|
+
});
|
|
2708
|
+
try {
|
|
2709
|
+
return interpretYesNo(await new Promise((resolve) => {
|
|
2710
|
+
rl.question(question, resolve);
|
|
2711
|
+
}));
|
|
2712
|
+
} finally {
|
|
2713
|
+
rl.close();
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Offer + run the package-manager upgrade. Returns a result the caller turns
|
|
2718
|
+
* into output and an exit/relaunch decision — kept separate from the actual
|
|
2719
|
+
* relaunch so it's testable without re-execing the test runner.
|
|
2720
|
+
*/
|
|
2721
|
+
async function offerPackageManagerUpdate(opts) {
|
|
2722
|
+
const manager = detectPackageManager({
|
|
2723
|
+
env: opts.env,
|
|
2724
|
+
execPath: opts.execPath
|
|
2725
|
+
});
|
|
2726
|
+
const command = installCommand(manager, opts.channel);
|
|
2727
|
+
const commandStr = installCommandString(manager, opts.channel);
|
|
2728
|
+
if (!opts.interactive) return { status: "skipped-noninteractive" };
|
|
2729
|
+
if (!await (opts.ask ?? promptYesNo)(`Update available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun \`${commandStr}\` now? [Y/n] `)) return { status: "declined" };
|
|
2730
|
+
const { status } = (opts.run ?? ((cmd) => {
|
|
2731
|
+
const [bin, ...args] = cmd;
|
|
2732
|
+
return { status: spawnSync(bin ?? "", args, { stdio: "inherit" }).status };
|
|
2733
|
+
}))(command);
|
|
2734
|
+
if (status !== 0) return {
|
|
2735
|
+
status: "failed",
|
|
2736
|
+
code: status,
|
|
2737
|
+
command: commandStr
|
|
2738
|
+
};
|
|
2739
|
+
return {
|
|
2740
|
+
status: "installed",
|
|
2741
|
+
command: commandStr
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
/**
|
|
2745
|
+
* Relaunch the CLI after a successful in-place upgrade so the user lands back
|
|
2746
|
+
* on their command running the new version. Uses the same interpreter + argv
|
|
2747
|
+
* the current process was launched with; detaches so the parent can exit
|
|
2748
|
+
* cleanly. Never throws — a failed relaunch just means the user re-runs
|
|
2749
|
+
* manually, which the caller's message covers.
|
|
2750
|
+
*/
|
|
2751
|
+
function relaunchCli(argv = process.argv) {
|
|
2752
|
+
try {
|
|
2753
|
+
const [, script, ...rest] = argv;
|
|
2754
|
+
spawn(process.execPath, script ? [script, ...rest] : rest, { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
|
|
2755
|
+
} catch (_error) {
|
|
2756
|
+
process.exit(0);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
//#endregion
|
|
2761
|
+
//#region src/commands/update.ts
|
|
2762
|
+
/**
|
|
2763
|
+
* `skydive update` — the user-invoked self-updater the update *check*
|
|
2764
|
+
* (../update-check/index.ts) has always pointed at. Notify-only remains the
|
|
2765
|
+
* automatic behavior; this is the explicit "do it now" action.
|
|
2766
|
+
*
|
|
2767
|
+
* Binary installs self-update in place (download -> verify sha256 -> atomic
|
|
2768
|
+
* replace); package-manager installs are never self-mutated (the notice
|
|
2769
|
+
* policy) — we print the exact upgrade command instead. `--check` is a dry
|
|
2770
|
+
* run; `--force` reinstalls the current version (repair a corrupt binary).
|
|
2771
|
+
*/
|
|
2772
|
+
const FETCH_TIMEOUT_MS$1 = 15e3;
|
|
2773
|
+
function normalizeChannel(raw, currentVersion) {
|
|
2774
|
+
if (raw === "stable" || raw === "canary") return raw;
|
|
2775
|
+
return resolveChannel(currentVersion);
|
|
2776
|
+
}
|
|
2777
|
+
/** Human-readable line for a plan outcome. Pure so the handler stays thin. */
|
|
2778
|
+
function describePlan(plan, channel, isCheck) {
|
|
2779
|
+
switch (plan.kind) {
|
|
2780
|
+
case "up-to-date": return `Already on the latest ${channel} release (${plan.currentVersion}).`;
|
|
2781
|
+
case "unsupported-source": return `This is a ${plan.source} install, so update through your package manager:\n ${renderUpdateCommand(channel, plan.source)}`;
|
|
2782
|
+
case "unsupported-host": return `No Skydive binary is published for ${plan.platform}/${plan.arch}.`;
|
|
2783
|
+
case "missing-from-manifest": return `The ${channel} ${plan.version} release has no binary for your platform (${plan.target}).`;
|
|
2784
|
+
case "update": return isCheck ? `Update available: ${version} \u2192 ${plan.version} (${channel}, ${plan.target}).\nRun \`skydive update\` to install it.` : `Updating ${version} \u2192 ${plan.version} (${channel}, ${plan.target})...`;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
const updateCommand = {
|
|
2788
|
+
command: "update",
|
|
2789
|
+
describe: "Update the Skydive CLI to the latest release",
|
|
2790
|
+
builder: (y) => y.option("channel", {
|
|
2791
|
+
type: "string",
|
|
2792
|
+
choices: ["stable", "canary"],
|
|
2793
|
+
describe: "Release channel to update to (default: this build's channel)"
|
|
2794
|
+
}).option("check", {
|
|
2795
|
+
type: "boolean",
|
|
2796
|
+
default: false,
|
|
2797
|
+
describe: "Only report whether an update is available; don't install"
|
|
2798
|
+
}).option("force", {
|
|
2799
|
+
type: "boolean",
|
|
2800
|
+
default: false,
|
|
2801
|
+
describe: "Reinstall the current version even if already up to date (repairs a corrupt binary)"
|
|
2802
|
+
}).example("skydive update", "Install the latest release").example("skydive update --check", "See if an update is available").example("skydive update --channel canary", "Switch to the canary line"),
|
|
2803
|
+
handler: async (argv) => {
|
|
2804
|
+
const currentVersion = version;
|
|
2805
|
+
const source = resolveInstallSource();
|
|
2806
|
+
const channel = normalizeChannel(argv.channel, currentVersion);
|
|
2807
|
+
const manifest = await releaseSourceForInstall(source).fetchManifest(channel, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS$1) }).catch(() => null);
|
|
2808
|
+
if (source === "binary" && manifest === null) {
|
|
2809
|
+
if (argv.json) {
|
|
2810
|
+
output(argv, {
|
|
2811
|
+
status: "unavailable",
|
|
2812
|
+
channel,
|
|
2813
|
+
currentVersion
|
|
2814
|
+
});
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
throw new UpdateError(`couldn't reach the ${channel} release channel — check your connection and try again.`);
|
|
2818
|
+
}
|
|
2819
|
+
const plan = planUpdate({
|
|
2820
|
+
currentVersion,
|
|
2821
|
+
source,
|
|
2822
|
+
manifest,
|
|
2823
|
+
platform: process.platform,
|
|
2824
|
+
arch: process.arch,
|
|
2825
|
+
allowSameVersion: argv.force
|
|
2826
|
+
});
|
|
2827
|
+
if (argv.json) {
|
|
2828
|
+
output(argv, {
|
|
2829
|
+
plan,
|
|
2830
|
+
channel,
|
|
2831
|
+
currentVersion,
|
|
2832
|
+
dryRun: argv.check
|
|
2833
|
+
});
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (plan.kind === "unsupported-source" && !argv.check) {
|
|
2837
|
+
const interactive = canPromptInteractively({
|
|
2838
|
+
stdinIsTTY: Boolean(process.stdin.isTTY),
|
|
2839
|
+
stdoutIsTTY: Boolean(process.stdout.isTTY),
|
|
2840
|
+
json: argv.json,
|
|
2841
|
+
quiet: argv.quiet,
|
|
2842
|
+
env: process.env
|
|
2843
|
+
});
|
|
2844
|
+
const result = await offerPackageManagerUpdate({
|
|
2845
|
+
channel,
|
|
2846
|
+
currentVersion,
|
|
2847
|
+
latestVersion: plan.latestVersion,
|
|
2848
|
+
env: process.env,
|
|
2849
|
+
execPath: process.execPath,
|
|
2850
|
+
interactive
|
|
2851
|
+
});
|
|
2852
|
+
switch (result.status) {
|
|
2853
|
+
case "installed":
|
|
2854
|
+
console.log(`Updated via \`${result.command}\`. Relaunching \`skydive\`...`);
|
|
2855
|
+
relaunchCli();
|
|
2856
|
+
return;
|
|
2857
|
+
case "failed": throw new UpdateError(`\`${result.command}\` exited with code ${result.code ?? "unknown"}. Try running it yourself.`);
|
|
2858
|
+
case "declined":
|
|
2859
|
+
console.log(describePlan(plan, channel, false));
|
|
2860
|
+
return;
|
|
2861
|
+
case "skipped-noninteractive":
|
|
2862
|
+
console.log(describePlan(plan, channel, false));
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
if (plan.kind !== "update" || argv.check) {
|
|
2867
|
+
console.log(describePlan(plan, channel, argv.check));
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
const selfPath = resolveSelfPath();
|
|
2871
|
+
if (!selfPath) throw new UpdateError("couldn't determine this binary's path to replace it — reinstall with the install script instead.");
|
|
2872
|
+
console.log(describePlan(plan, channel, false));
|
|
2873
|
+
await applyBinary(selfPath, await downloadAndVerify({
|
|
2874
|
+
downloadBaseUrl: RELEASE_CDN_URL,
|
|
2875
|
+
key: plan.key,
|
|
2876
|
+
expectedSha256: plan.sha256
|
|
2877
|
+
}));
|
|
2878
|
+
console.log(`Updated to ${plan.version}. Run \`skydive --version\` to confirm.`);
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
|
|
2154
2882
|
//#endregion
|
|
2155
2883
|
//#region src/cli.ts
|
|
2156
2884
|
function createCli(argv) {
|
|
@@ -2168,7 +2896,7 @@ function createCli(argv) {
|
|
|
2168
2896
|
type: "string",
|
|
2169
2897
|
global: true,
|
|
2170
2898
|
describe: "Override API base URL"
|
|
2171
|
-
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
2899
|
+
}).command(authCommand).command(chatCommand).command(messagesCommand).command(conversationsCommand).command(agentsCommand).command(importCommand).command(keysCommand).command(secretsCommand).command(workspaceCommand).command(portalCommand).command(sandboxCommand).command(updateCommand).demandCommand(1, "Specify a command. Run --help for usage.").strict().wrap(null).version(version).alias("v", "version").alias("h", "help").help().fail((msg, err) => {
|
|
2172
2900
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
2173
2901
|
process.exit(1);
|
|
2174
2902
|
});
|
|
@@ -2280,100 +3008,6 @@ function isCheckDue(lastCheckedAt, now = Date.now()) {
|
|
|
2280
3008
|
return now - then >= CHECK_INTERVAL_MS;
|
|
2281
3009
|
}
|
|
2282
3010
|
|
|
2283
|
-
//#endregion
|
|
2284
|
-
//#region src/update-check/versions.ts
|
|
2285
|
-
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2286
|
-
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2287
|
-
* to stable. */
|
|
2288
|
-
function resolveChannel(version) {
|
|
2289
|
-
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2290
|
-
}
|
|
2291
|
-
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2292
|
-
function distTagForChannel(channel) {
|
|
2293
|
-
return channel === "canary" ? "beta" : "latest";
|
|
2294
|
-
}
|
|
2295
|
-
/**
|
|
2296
|
-
* True when `candidate` is a strictly newer release than `current`, per
|
|
2297
|
-
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2298
|
-
* Unparseable input is never newer, so garbage from the registry can't
|
|
2299
|
-
* produce a notice.
|
|
2300
|
-
*/
|
|
2301
|
-
function isNewerVersion(candidate, current) {
|
|
2302
|
-
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2303
|
-
if (!semver.valid(current, { loose: true })) return false;
|
|
2304
|
-
return semver.gt(candidate, current, { loose: true });
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
//#endregion
|
|
2308
|
-
//#region src/update-check/notice.ts
|
|
2309
|
-
/** The install-mode-specific action line. A package-manager install is never
|
|
2310
|
-
* self-mutated — we only tell the user what to run. */
|
|
2311
|
-
function renderUpdateCommand(channel, source) {
|
|
2312
|
-
if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
|
|
2313
|
-
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
2314
|
-
}
|
|
2315
|
-
function renderUpdateNotice(opts) {
|
|
2316
|
-
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
2317
|
-
}
|
|
2318
|
-
/**
|
|
2319
|
-
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
2320
|
-
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
2321
|
-
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
2322
|
-
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
2323
|
-
* suppress it entirely rather than risk corrupting piped output.
|
|
2324
|
-
*/
|
|
2325
|
-
function shouldNotify(opts) {
|
|
2326
|
-
if (!opts.stderrIsTTY) return false;
|
|
2327
|
-
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
2328
|
-
return true;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
//#endregion
|
|
2332
|
-
//#region src/update-check/sources.ts
|
|
2333
|
-
function resolveInstallSource() {
|
|
2334
|
-
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2335
|
-
}
|
|
2336
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2337
|
-
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2338
|
-
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2339
|
-
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2340
|
-
const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2341
|
-
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2342
|
-
signal,
|
|
2343
|
-
headers: { accept: "application/json" }
|
|
2344
|
-
});
|
|
2345
|
-
if (!response.ok) return null;
|
|
2346
|
-
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2347
|
-
if (!tags.success) return null;
|
|
2348
|
-
const version = tags.data[distTagForChannel(channel)];
|
|
2349
|
-
return typeof version === "string" ? version : null;
|
|
2350
|
-
} };
|
|
2351
|
-
/**
|
|
2352
|
-
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2353
|
-
* Serves the channel pointers the release workflow uploads
|
|
2354
|
-
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2355
|
-
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2356
|
-
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2357
|
-
*/
|
|
2358
|
-
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2359
|
-
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2360
|
-
* document the api's channel route serves (see
|
|
2361
|
-
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2362
|
-
* bucket this CDN fronts). */
|
|
2363
|
-
const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2364
|
-
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2365
|
-
signal,
|
|
2366
|
-
headers: { accept: "application/json" }
|
|
2367
|
-
});
|
|
2368
|
-
if (!response.ok) return null;
|
|
2369
|
-
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
2370
|
-
if (!manifest.success) return null;
|
|
2371
|
-
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
2372
|
-
} };
|
|
2373
|
-
function releaseSourceForInstall(source) {
|
|
2374
|
-
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
3011
|
//#endregion
|
|
2378
3012
|
//#region src/update-check/index.ts
|
|
2379
3013
|
/**
|
|
@@ -2616,12 +3250,18 @@ if (process.argv.includes(UPDATE_WORKER_FLAG)) {
|
|
|
2616
3250
|
await runUpdateCheckWorker();
|
|
2617
3251
|
process.exit(0);
|
|
2618
3252
|
}
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
3253
|
+
if (process.argv.includes(PORTAL_DAEMON_FLAG)) {
|
|
3254
|
+
const { runPortalDaemon } = await import("./daemon-CqhMYzpN.mjs");
|
|
3255
|
+
runPortalDaemon(process.argv);
|
|
3256
|
+
} else runCli();
|
|
3257
|
+
function runCli() {
|
|
3258
|
+
maybePrintWhatsNew();
|
|
3259
|
+
setupUpdateCheck(hideBin(process.argv));
|
|
3260
|
+
const resolved = resolveArgv(hideBin(process.argv));
|
|
3261
|
+
const raw = hideBin(process.argv);
|
|
3262
|
+
if ((raw.includes("--help") || raw.includes("-h")) && !raw.some((a) => !a.startsWith("-")) && !raw.includes("--json") && !raw.includes("--quiet")) process.stdout.write(brandHelpArt());
|
|
3263
|
+
createCli(resolved).parse();
|
|
3264
|
+
}
|
|
2625
3265
|
|
|
2626
3266
|
//#endregion
|
|
2627
3267
|
export { };
|