skydive-cli 0.2.0-beta.618 → 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 +636 -104
- package/dist/js/{boot-CaWHIM4R.mjs → boot-CI6nXtmu.mjs} +2 -2
- package/dist/js/{install-BhPuzBxW.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/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,53 @@ The Skydive CLI — manage agents and chat with them from the terminal.
|
|
|
6
6
|
npx skydive-cli --help
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
## Installing
|
|
10
|
+
|
|
11
|
+
Two ways to install, depending on whether you have a Node toolchain:
|
|
12
|
+
|
|
13
|
+
**npm (needs Node ≥ 20):**
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g skydive-cli # then: skydive --help
|
|
17
|
+
# or run without installing:
|
|
18
|
+
npx skydive-cli --help
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Standalone binary (no Node required):**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
curl -fsSL https://skydive.com/api/v1/cli/install.sh | sh
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Installs a self-contained `skydive` to `~/.local/bin`. The installer detects
|
|
28
|
+
your OS/arch, downloads the matching binary from the release CDN, verifies its
|
|
29
|
+
sha256, and installs it. Options via env vars: `SKYDIVE_CHANNEL=canary` for the
|
|
30
|
+
canary line, `SKYDIVE_VERSION=X.Y.Z` to pin a version, `SKYDIVE_INSTALL_DIR` to
|
|
31
|
+
choose the directory.
|
|
32
|
+
|
|
33
|
+
## Updating
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
skydive update # update to the latest release
|
|
37
|
+
skydive update --check # just report whether an update is available
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A **binary** install self-updates in place (download → verify sha256 → atomic
|
|
41
|
+
replace). An **npm/yarn/pnpm/bun** install is owned by your package manager, so
|
|
42
|
+
`skydive update` offers to run the right upgrade command for it and relaunches
|
|
43
|
+
the CLI on the new version.
|
|
44
|
+
|
|
45
|
+
The CLI also checks for updates in the background (at most once a day, never
|
|
46
|
+
blocking a command) and prints a one-line notice when a newer version exists.
|
|
47
|
+
Control this in `config.json` or via env vars:
|
|
48
|
+
|
|
49
|
+
- `autoupdate`: `"patch"` (default — silently self-apply a stable **patch**
|
|
50
|
+
release of a binary install; notify for minor/major, canary, and
|
|
51
|
+
package-manager installs), `"notify"` (only ever print the notice), or
|
|
52
|
+
`false` (no auto-update and no notice). Env override: `SKYDIVE_AUTOUPDATE`.
|
|
53
|
+
- `updateCheck: false` (or `SKYDIVE_NO_UPDATE_CHECK` / `NO_UPDATE_NOTIFIER`)
|
|
54
|
+
turns the background check off entirely.
|
|
55
|
+
|
|
9
56
|
## Authentication
|
|
10
57
|
|
|
11
58
|
Sign in once via the browser — the same shape as `doppler login`: the CLI
|
package/dist/js/bin.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
import { a as registerPortalDevice, i as grantPortalAccess, n as fetchPortalDevices, o as revokePortalAccess, r as findThisDevice, s as machineIdentity } from "./client-DT9bWi1D.mjs";
|
|
@@ -17,7 +17,9 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import zlib from "node:zlib";
|
|
20
|
+
import fsp from "node:fs/promises";
|
|
20
21
|
import semver from "semver";
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
21
23
|
|
|
22
24
|
//#region src/types.ts
|
|
23
25
|
const NON_INTERACTIVE_ENV_VARS = [
|
|
@@ -1196,7 +1198,7 @@ const importCommand = {
|
|
|
1196
1198
|
process.exit(1);
|
|
1197
1199
|
}
|
|
1198
1200
|
}
|
|
1199
|
-
const { runChat } = await import("./boot-
|
|
1201
|
+
const { runChat } = await import("./boot-CI6nXtmu.mjs");
|
|
1200
1202
|
await runChat({
|
|
1201
1203
|
appUrl,
|
|
1202
1204
|
sessionToken: session.value.sessionToken,
|
|
@@ -1231,7 +1233,7 @@ async function runImportPrintMode({ argv, appUrl }) {
|
|
|
1231
1233
|
});
|
|
1232
1234
|
const extra = (argv.print ?? "").trim();
|
|
1233
1235
|
const prompt = buildImportSeedPrompt(process.cwd()) + (extra ? `\n\nAdditional instructions: ${extra}` : "");
|
|
1234
|
-
const { runPrint } = await import("./print-
|
|
1236
|
+
const { runPrint } = await import("./print-CB6L_1YB.mjs");
|
|
1235
1237
|
try {
|
|
1236
1238
|
const result = await runPrint({
|
|
1237
1239
|
appUrl,
|
|
@@ -1512,7 +1514,7 @@ const chatCommand = {
|
|
|
1512
1514
|
printError(`Unknown theme "${themeId}". Valid themes: ${themes.map((t) => t.id).join(", ")}`);
|
|
1513
1515
|
process.exit(1);
|
|
1514
1516
|
}
|
|
1515
|
-
const { runChat } = await import("./boot-
|
|
1517
|
+
const { runChat } = await import("./boot-CI6nXtmu.mjs");
|
|
1516
1518
|
await runChat({
|
|
1517
1519
|
appUrl,
|
|
1518
1520
|
sessionToken: session.value.sessionToken,
|
|
@@ -1549,7 +1551,7 @@ async function runPrintMode({ argv, appUrl }) {
|
|
|
1549
1551
|
printError(`${session.error.message} For non-interactive use, run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1550
1552
|
process.exit(1);
|
|
1551
1553
|
}
|
|
1552
|
-
const { runPrint, readStdin } = await import("./print-
|
|
1554
|
+
const { runPrint, readStdin } = await import("./print-CB6L_1YB.mjs");
|
|
1553
1555
|
let prompt = (argv.print ?? "").trim();
|
|
1554
1556
|
if (!prompt) {
|
|
1555
1557
|
if (process.stdin.isTTY) {
|
|
@@ -1623,7 +1625,7 @@ const getCommand = {
|
|
|
1623
1625
|
printError(`${session.error.message} Run \`skydive auth login\` first, or set SKYDIVE_SESSION_TOKEN.`);
|
|
1624
1626
|
process.exit(1);
|
|
1625
1627
|
}
|
|
1626
|
-
const { messageGet } = await import("./print-
|
|
1628
|
+
const { messageGet } = await import("./print-CB6L_1YB.mjs");
|
|
1627
1629
|
try {
|
|
1628
1630
|
const result = await messageGet({
|
|
1629
1631
|
appUrl,
|
|
@@ -1820,7 +1822,7 @@ const switchCommand = {
|
|
|
1820
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.");
|
|
1821
1823
|
process.exit(1);
|
|
1822
1824
|
}
|
|
1823
|
-
const { runWorkspacePicker } = await import("./boot-
|
|
1825
|
+
const { runWorkspacePicker } = await import("./boot-CI6nXtmu.mjs");
|
|
1824
1826
|
await runWorkspacePicker(session);
|
|
1825
1827
|
return;
|
|
1826
1828
|
}
|
|
@@ -2172,7 +2174,7 @@ const sandboxCommand = {
|
|
|
2172
2174
|
handler: async (argv) => {
|
|
2173
2175
|
const session = requireSession(argv);
|
|
2174
2176
|
const { createRestClient } = await import("./rest-CBROy0L-.mjs");
|
|
2175
|
-
const { resolveAgent } = await import("./print-
|
|
2177
|
+
const { resolveAgent } = await import("./print-CB6L_1YB.mjs");
|
|
2176
2178
|
const client = createRestClient({
|
|
2177
2179
|
appUrl: session.appUrl,
|
|
2178
2180
|
sessionToken: session.sessionToken
|
|
@@ -2253,6 +2255,630 @@ async function runPty({ session, agentId, agentName }) {
|
|
|
2253
2255
|
return result.code;
|
|
2254
2256
|
}
|
|
2255
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
|
+
|
|
2256
2882
|
//#endregion
|
|
2257
2883
|
//#region src/cli.ts
|
|
2258
2884
|
function createCli(argv) {
|
|
@@ -2270,7 +2896,7 @@ function createCli(argv) {
|
|
|
2270
2896
|
type: "string",
|
|
2271
2897
|
global: true,
|
|
2272
2898
|
describe: "Override API base URL"
|
|
2273
|
-
}).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) => {
|
|
2274
2900
|
printError(err ? err instanceof Error ? err.message : String(err) : msg ?? "Unknown error");
|
|
2275
2901
|
process.exit(1);
|
|
2276
2902
|
});
|
|
@@ -2382,100 +3008,6 @@ function isCheckDue(lastCheckedAt, now = Date.now()) {
|
|
|
2382
3008
|
return now - then >= CHECK_INTERVAL_MS;
|
|
2383
3009
|
}
|
|
2384
3010
|
|
|
2385
|
-
//#endregion
|
|
2386
|
-
//#region src/update-check/versions.ts
|
|
2387
|
-
/** Canary builds carry a prerelease suffix (`X.Y.Z-beta.N`, synthesized in
|
|
2388
|
-
* CI); a plain semver is a stable release. An unparseable version defaults
|
|
2389
|
-
* to stable. */
|
|
2390
|
-
function resolveChannel(version) {
|
|
2391
|
-
return (semver.prerelease(version, { loose: true })?.length ?? 0) > 0 ? "canary" : "stable";
|
|
2392
|
-
}
|
|
2393
|
-
/** npm dist-tag for a channel (see release-skydive-cli.yml). */
|
|
2394
|
-
function distTagForChannel(channel) {
|
|
2395
|
-
return channel === "canary" ? "beta" : "latest";
|
|
2396
|
-
}
|
|
2397
|
-
/**
|
|
2398
|
-
* True when `candidate` is a strictly newer release than `current`, per
|
|
2399
|
-
* semver precedence (the `semver` package, including prerelease ordering).
|
|
2400
|
-
* Unparseable input is never newer, so garbage from the registry can't
|
|
2401
|
-
* produce a notice.
|
|
2402
|
-
*/
|
|
2403
|
-
function isNewerVersion(candidate, current) {
|
|
2404
|
-
if (!semver.valid(candidate, { loose: true })) return false;
|
|
2405
|
-
if (!semver.valid(current, { loose: true })) return false;
|
|
2406
|
-
return semver.gt(candidate, current, { loose: true });
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
//#endregion
|
|
2410
|
-
//#region src/update-check/notice.ts
|
|
2411
|
-
/** The install-mode-specific action line. A package-manager install is never
|
|
2412
|
-
* self-mutated — we only tell the user what to run. */
|
|
2413
|
-
function renderUpdateCommand(channel, source) {
|
|
2414
|
-
if (source === "binary") return `curl -fsSL ${DEFAULT_WEB_URL}/api/v1/cli/install.sh | ${channel === "canary" ? "SKYDIVE_CHANNEL=canary " : ""}sh`;
|
|
2415
|
-
return `npm install -g ${name}@${distTagForChannel(channel)}`;
|
|
2416
|
-
}
|
|
2417
|
-
function renderUpdateNotice(opts) {
|
|
2418
|
-
return `\nUpdate available: ${opts.currentVersion} \u2192 ${opts.latestVersion}\nRun ${renderUpdateCommand(opts.channel, opts.source)}\n`;
|
|
2419
|
-
}
|
|
2420
|
-
/**
|
|
2421
|
-
* Whether this invocation may print the notice. Pure so it's testable; the
|
|
2422
|
-
* inputs are raw pre-yargs argv (parsing hasn't happened when this runs) and
|
|
2423
|
-
* stderr's TTY-ness. `--json`/`--quiet` go to stdout, and the notice goes to
|
|
2424
|
-
* stderr — but scripts commonly capture 2>&1, so machine-readable modes
|
|
2425
|
-
* suppress it entirely rather than risk corrupting piped output.
|
|
2426
|
-
*/
|
|
2427
|
-
function shouldNotify(opts) {
|
|
2428
|
-
if (!opts.stderrIsTTY) return false;
|
|
2429
|
-
if (opts.argv.includes("--json") || opts.argv.includes("--quiet")) return false;
|
|
2430
|
-
return true;
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
//#endregion
|
|
2434
|
-
//#region src/update-check/sources.ts
|
|
2435
|
-
function resolveInstallSource() {
|
|
2436
|
-
return typeof SKYDIVE_CLI_INSTALL_SOURCE === "string" && SKYDIVE_CLI_INSTALL_SOURCE === "binary" ? "binary" : "package-manager";
|
|
2437
|
-
}
|
|
2438
|
-
const NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2439
|
-
const npmDistTagsSchema = z.record(z.string(), z.unknown());
|
|
2440
|
-
const binaryManifestSchema = z.object({ version: z.unknown().optional() });
|
|
2441
|
-
/** npm dist-tags for the published package (`skydive-cli`). */
|
|
2442
|
-
const npmReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2443
|
-
const response = await fetch(`${NPM_REGISTRY_URL}/-/package/${name}/dist-tags`, {
|
|
2444
|
-
signal,
|
|
2445
|
-
headers: { accept: "application/json" }
|
|
2446
|
-
});
|
|
2447
|
-
if (!response.ok) return null;
|
|
2448
|
-
const tags = npmDistTagsSchema.safeParse(await response.json());
|
|
2449
|
-
if (!tags.success) return null;
|
|
2450
|
-
const version = tags.data[distTagForChannel(channel)];
|
|
2451
|
-
return typeof version === "string" ? version : null;
|
|
2452
|
-
} };
|
|
2453
|
-
/**
|
|
2454
|
-
* Release CDN (CloudFront over the releases bucket, infra: CliReleasesCdn).
|
|
2455
|
-
* Serves the channel pointers the release workflow uploads
|
|
2456
|
-
* (`channels/{stable,canary}.json`, cached max-age=60) alongside the
|
|
2457
|
-
* binaries. The daily poll goes here, not to the api, so a fleet of
|
|
2458
|
-
* installed binaries puts no load on — and takes no dependency on — the api.
|
|
2459
|
-
*/
|
|
2460
|
-
const RELEASE_CDN_URL = "https://dl.skydive.com";
|
|
2461
|
-
/** Channel manifest on the release CDN, for compiled binaries. The same
|
|
2462
|
-
* document the api's channel route serves (see
|
|
2463
|
-
* apps/anyone/api/src/routes/cli-releases.ts, which reads it from the
|
|
2464
|
-
* bucket this CDN fronts). */
|
|
2465
|
-
const binaryReleaseSource = { async fetchLatestVersion(channel, { signal }) {
|
|
2466
|
-
const response = await fetch(`${RELEASE_CDN_URL}/channels/${channel}.json`, {
|
|
2467
|
-
signal,
|
|
2468
|
-
headers: { accept: "application/json" }
|
|
2469
|
-
});
|
|
2470
|
-
if (!response.ok) return null;
|
|
2471
|
-
const manifest = binaryManifestSchema.safeParse(await response.json());
|
|
2472
|
-
if (!manifest.success) return null;
|
|
2473
|
-
return typeof manifest.data.version === "string" ? manifest.data.version : null;
|
|
2474
|
-
} };
|
|
2475
|
-
function releaseSourceForInstall(source) {
|
|
2476
|
-
return source === "binary" ? binaryReleaseSource : npmReleaseSource;
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
3011
|
//#endregion
|
|
2480
3012
|
//#region src/update-check/index.ts
|
|
2481
3013
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-
|
|
3
|
-
import {
|
|
2
|
+
import { C as setActiveWorkspace, S as listWorkspaces, a as WORDMARK, b as getActiveWorkspaceId, c as applyTheme, d as noColorRequested, f as theme, g as themeVersion, h as themeModeFromColorFgBg, i as MARK_CELLS, l as findTheme, m as themeMode, n as buildCrashReport, p as themeForMode, r as writeCrashReport, s as DEFAULT_THEME_ID, t as installCrashHandler, u as monoTheme, v as themesForMode } from "./install-DKTnh1gy.mjs";
|
|
3
|
+
import { L as saveTheme, P as resolveWebUrl, T as getSavedTheme, c as cardActionErrorMessage, d as reconcileMaskedInput, f as resolveConnectUrl, i as resolveAgent, l as parseExternalOauthConnectParams, m as specKeyFor, p as parseConnectCard, s as MASK_CHAR, u as parseOauthConnectParams, v as DEFAULT_API_URL, w as getReviewStateDir, x as getConfigPath, y as DEFAULT_APP_URL } from "./print-DaPpTqxu.mjs";
|
|
4
4
|
import { a as errorMessage, i as sendErrorMessage, n as createRestClient, o as isRecord, r as errorDetail, t as HttpError } from "./rest-Q5yl_b2-.mjs";
|
|
5
5
|
import { t as PortalClient } from "./client-DT9bWi1D.mjs";
|
|
6
6
|
import { d as makeLineParser, f as parseDaemonMessage, l as daemonPaths, n as ensureDaemonRunning, s as LOCAL_PROTOCOL_VERSION, u as encodeLine } from "./daemon-DfMgVlYE.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { x as getConfigPath } from "./print-DaPpTqxu.mjs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { err, ok } from "neverthrow";
|
|
@@ -8,7 +8,7 @@ import fs from "node:fs";
|
|
|
8
8
|
|
|
9
9
|
//#region package.json
|
|
10
10
|
var name = "skydive-cli";
|
|
11
|
-
var version$1 = "0.2.0-beta.
|
|
11
|
+
var version$1 = "0.2.0-beta.624";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/auth/organization.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-
|
|
2
|
+
import { a as runPrint, i as resolveAgent, n as messageGet, o as toPrintError, r as readStdin, t as collectRunText } from "./print-DaPpTqxu.mjs";
|
|
3
3
|
import "./rest-Q5yl_b2-.mjs";
|
|
4
4
|
|
|
5
5
|
export { messageGet, readStdin, resolveAgent, runPrint };
|
|
@@ -677,4 +677,4 @@ async function readStdin() {
|
|
|
677
677
|
}
|
|
678
678
|
|
|
679
679
|
//#endregion
|
|
680
|
-
export {
|
|
680
|
+
export { resolveAppUrl as A, getPromptHistoryPath as C, getStoredApiKeyId as D, getShareMachineDefault as E, saveConfig as F, saveSession as I, saveTheme as L, resolveManagementAuth as M, resolveSession as N, getStoredApiKeyWorkspaceName as O, resolveWebUrl as P, setLastSeenVersion as R, getLastSeenVersion as S, getSavedTheme as T, API_KEY_PREFIX as _, runPrint as a, deleteConfig as b, cardActionErrorMessage as c, reconcileMaskedInput as d, resolveConnectUrl as f, API_KEY_FAMILY_PREFIX as g, API_KEYS_URL as h, resolveAgent as i, resolveConfig as j, getUpdateCheckDisabled as k, parseExternalOauthConnectParams as l, specKeyFor as m, messageGet as n, toPrintError as o, parseConnectCard as p, readStdin as r, MASK_CHAR as s, collectRunText as t, parseOauthConnectParams as u, DEFAULT_API_URL as v, getReviewStateDir as w, getConfigPath as x, DEFAULT_APP_URL as y };
|