agent-dag 3.22.1 → 3.22.4
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 +6 -477
- package/package.json +14 -48
- package/shim.js +107 -0
- package/LICENSE +0 -661
- package/LICENSING.md +0 -82
- package/THIRD_PARTY_NOTICES.md +0 -395
- package/bin/agent-dag.js +0 -626
- package/bin/deck.js +0 -1805
- package/dist/web/assets/index-CJYsv0lr.css +0 -1
- package/dist/web/assets/index-Ifm23DDC.js +0 -270
- package/dist/web/index.html +0 -49
- package/hook/hook.js +0 -542
- package/release-notes.json +0 -398
- package/src/server/activity.mjs +0 -52
- package/src/server/agent-activity.mjs +0 -522
- package/src/server/args.mjs +0 -183
- package/src/server/auto-update.mjs +0 -79
- package/src/server/block-notify.mjs +0 -173
- package/src/server/boot-deadline.mjs +0 -127
- package/src/server/brand.mjs +0 -16
- package/src/server/browser-history.mjs +0 -497
- package/src/server/browser-presence.mjs +0 -211
- package/src/server/browser-profiles.mjs +0 -279
- package/src/server/browser-react.mjs +0 -284
- package/src/server/browser-watch-store.mjs +0 -350
- package/src/server/browser-watch.mjs +0 -905
- package/src/server/ccusage.mjs +0 -1168
- package/src/server/claude-accounts.mjs +0 -951
- package/src/server/claude-dir.mjs +0 -213
- package/src/server/codex-auth.mjs +0 -388
- package/src/server/codex-dir.mjs +0 -171
- package/src/server/codex-quota.mjs +0 -449
- package/src/server/codex-usage.mjs +0 -512
- package/src/server/cswap-admin.mjs +0 -1562
- package/src/server/cswap-auto.mjs +0 -658
- package/src/server/cswap-install.mjs +0 -641
- package/src/server/deck-home.mjs +0 -243
- package/src/server/deck-prefs.mjs +0 -301
- package/src/server/deck-probe.mjs +0 -111
- package/src/server/detach.mjs +0 -244
- package/src/server/exec.mjs +0 -996
- package/src/server/global-install.mjs +0 -67
- package/src/server/hwmonitor.mjs +0 -56
- package/src/server/index.mjs +0 -6043
- package/src/server/installer.mjs +0 -912
- package/src/server/invoked-as.mjs +0 -144
- package/src/server/lan-about.mjs +0 -119
- package/src/server/lan-engine.mjs +0 -952
- package/src/server/lan-reach.mjs +0 -256
- package/src/server/lan-socket.mjs +0 -682
- package/src/server/lan-sync.mjs +0 -941
- package/src/server/lhm-parse.mjs +0 -91
- package/src/server/log-tail.mjs +0 -139
- package/src/server/log-writer.mjs +0 -322
- package/src/server/login-service.mjs +0 -473
- package/src/server/macmon.mjs +0 -310
- package/src/server/npx.mjs +0 -264
- package/src/server/open-url.mjs +0 -242
- package/src/server/presence.mjs +0 -40
- package/src/server/quota.mjs +0 -792
- package/src/server/relay-guard.mjs +0 -507
- package/src/server/reset-label.mjs +0 -78
- package/src/server/retire-sound-hook.mjs +0 -349
- package/src/server/running-deck.mjs +0 -234
- package/src/server/self-update.mjs +0 -1380
- package/src/server/stop-deck.mjs +0 -171
- package/src/server/supervisor.mjs +0 -392
- package/src/server/system-metrics.mjs +0 -1825
- package/src/server/term.mjs +0 -686
- package/src/server/uv-bootstrap.mjs +0 -337
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
// Last-resort acquisition of `uv`, so claude-swap can be installed on a machine
|
|
2
|
-
// with no Python tooling at all.
|
|
3
|
-
//
|
|
4
|
-
// The alternative everyone reaches for is `curl -LsSf https://astral.sh/uv/
|
|
5
|
-
// install.sh | sh`, which executes whatever that URL happens to serve, with the
|
|
6
|
-
// user's privileges, and drops files into their global tool path. This does the
|
|
7
|
-
// narrower thing instead: fetch one named release artifact from Astral's own
|
|
8
|
-
// GitHub releases, check it against the SHA-256 that release publishes beside
|
|
9
|
-
// it, and unpack it inside ~/.agents-deck. Nothing is executed until it has
|
|
10
|
-
// been verified, nothing lands outside a directory the deck already owns — the
|
|
11
|
-
// staging directory included, which is what #551 was about — and deleting that
|
|
12
|
-
// directory undoes all of it.
|
|
13
|
-
//
|
|
14
|
-
// It is still a network fetch of an executable, so it only happens when there
|
|
15
|
-
// is no other way: uv, pipx, and python -m pipx have all already been ruled out
|
|
16
|
-
// by the caller. AGENTS_DECK_NO_INSTALL=1 disables it along with everything else.
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
|
-
import { mkdir, mkdtemp, writeFile, rm, chmod, readdir, copyFile, open } from "node:fs/promises";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
import { homedir } from "node:os";
|
|
22
|
-
import { run } from "./exec.mjs";
|
|
23
|
-
// The same rename used to replace settings.json, for the same reason: on
|
|
24
|
-
// Windows a rename over an existing file fails outright while any other process
|
|
25
|
-
// holds the target open, and a virus scanner or the search indexer opens a
|
|
26
|
-
// freshly written executable the instant it appears. Sharing one implementation
|
|
27
|
-
// keeps the retryable-error list from drifting apart between the two callers.
|
|
28
|
-
import { renameWithRetry } from "./installer.mjs";
|
|
29
|
-
|
|
30
|
-
const TOOL_DIR = join(homedir(), ".agents-deck", "tools");
|
|
31
|
-
const UV_DIR = join(TOOL_DIR, "uv");
|
|
32
|
-
|
|
33
|
-
const RELEASE_API = "https://api.github.com/repos/astral-sh/uv/releases/latest";
|
|
34
|
-
const DOWNLOAD_BASE = "https://github.com/astral-sh/uv/releases/download";
|
|
35
|
-
|
|
36
|
-
// Used when the releases API cannot be reached — rate limiting is per-IP and
|
|
37
|
-
// unauthenticated, so this is a normal outcome rather than an error. Any
|
|
38
|
-
// reasonably recent uv can install claude-swap; it self-updates later.
|
|
39
|
-
const FALLBACK_VERSION = "0.12.3";
|
|
40
|
-
|
|
41
|
-
const DOWNLOAD_TIMEOUT_MS = 120_000;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Release artifact for this machine, or null if uv does not publish one.
|
|
45
|
-
*
|
|
46
|
-
* Rosetta is deliberately not special-cased: an x64 Node on Apple Silicon
|
|
47
|
-
* reports x64 and gets the x64 build, which runs.
|
|
48
|
-
*/
|
|
49
|
-
export function assetName(platform = process.platform, arch = process.arch, libc = detectLibc(platform)) {
|
|
50
|
-
// `arm` is 32-bit ARM — a Raspberry Pi on the armhf image, which uv publishes
|
|
51
|
-
// as armv7. Left out, that machine got `unsupported_platform` for a target
|
|
52
|
-
// that exists.
|
|
53
|
-
const a = { x64: "x86_64", arm64: "aarch64", ia32: "i686", arm: "armv7" }[arch];
|
|
54
|
-
if (!a) return null;
|
|
55
|
-
// Apple publishes no i686 build, and never will.
|
|
56
|
-
if (platform === "darwin") return a === "i686" ? null : `uv-${a}-apple-darwin.tar.gz`;
|
|
57
|
-
if (platform === "win32") return `uv-${a}-pc-windows-msvc.zip`;
|
|
58
|
-
if (platform === "linux") {
|
|
59
|
-
// MUSL IS ITS OWN TARGET, and asking for the gnu build on Alpine is not a
|
|
60
|
-
// degraded install, it is an endless one: the glibc ELF interpreter is
|
|
61
|
-
// absent, so the downloaded binary cannot start, `bootstrapUv` returns
|
|
62
|
-
// `does_not_run`, and because ensureCswap runs whenever cswapVersion() is
|
|
63
|
-
// null the 35 MB archive is fetched, hashed and extracted again on EVERY
|
|
64
|
-
// launch — with the accounts panel permanently unavailable. `node:alpine`
|
|
65
|
-
// is a common enough base image for this to be somebody's whole experience
|
|
66
|
-
// of the deck.
|
|
67
|
-
if (libc === "musl") return `uv-${a}-unknown-linux-musl.tar.gz`;
|
|
68
|
-
return `uv-${a}-unknown-linux-gnu.tar.gz`;
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* "glibc" or "musl" for this process.
|
|
75
|
-
*
|
|
76
|
-
* Node's own report names the glibc it is linked against, and a musl build has
|
|
77
|
-
* no such field — which is the check every native-addon loader uses, and needs
|
|
78
|
-
* no child process. Anything unexpected reads as glibc, because that is the
|
|
79
|
-
* overwhelmingly common case and the wrong guess there is the state this code
|
|
80
|
-
* was already in.
|
|
81
|
-
*/
|
|
82
|
-
export function detectLibc(platform = process.platform, report = () => process.report?.getReport?.()) {
|
|
83
|
-
// The question only exists on Linux. macOS and Windows have no glibc field
|
|
84
|
-
// either, so asking there and reading the absence as musl would answer "musl"
|
|
85
|
-
// for every Mac — which is why the platform comes first.
|
|
86
|
-
if (platform !== "linux") return "glibc";
|
|
87
|
-
try {
|
|
88
|
-
const header = report()?.header;
|
|
89
|
-
if (!header) return "glibc";
|
|
90
|
-
if (typeof header.glibcVersionRuntime === "string") return "glibc";
|
|
91
|
-
return "musl";
|
|
92
|
-
} catch { return "glibc"; }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Path to a uv this function installed earlier, or null. */
|
|
96
|
-
export function existingBootstrappedUv() {
|
|
97
|
-
const bin = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");
|
|
98
|
-
return existsSync(bin) ? bin : null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function latestVersion() {
|
|
102
|
-
try {
|
|
103
|
-
const res = await fetch(RELEASE_API, {
|
|
104
|
-
headers: { accept: "application/vnd.github+json", "user-agent": "agents-deck" },
|
|
105
|
-
signal: AbortSignal.timeout(8_000),
|
|
106
|
-
});
|
|
107
|
-
if (!res.ok) return FALLBACK_VERSION;
|
|
108
|
-
const tag = (await res.json())?.tag_name;
|
|
109
|
-
return typeof tag === "string" && /^\d/.test(tag) ? tag : FALLBACK_VERSION;
|
|
110
|
-
} catch {
|
|
111
|
-
return FALLBACK_VERSION;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async function download(url, { asText = false } = {}) {
|
|
116
|
-
const res = await fetch(url, {
|
|
117
|
-
redirect: "follow",
|
|
118
|
-
headers: { "user-agent": "agents-deck" },
|
|
119
|
-
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
120
|
-
});
|
|
121
|
-
if (!res.ok) return null;
|
|
122
|
-
return asText ? (await res.text()) : Buffer.from(await res.arrayBuffer());
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* A path as a PowerShell single-quoted string literal.
|
|
127
|
-
*
|
|
128
|
-
* Inside single quotes PowerShell expands nothing, so a doubled quote is both
|
|
129
|
-
* the only escape it has and the only one needed. Windows filenames cannot
|
|
130
|
-
* contain `"`, `<`, `>` or `|`, but they can contain an apostrophe — and the
|
|
131
|
-
* archive lands under the user's own profile either way, which is where an
|
|
132
|
-
* apostrophe comes from: C:\Users\O'Brien\.agents-deck\tools. Pasted in raw,
|
|
133
|
-
* that apostrophe ended the string mid-path and Expand-Archive died of a syntax
|
|
134
|
-
* error, so uv could never be bootstrapped on an account whose owner has that
|
|
135
|
-
* name.
|
|
136
|
-
*/
|
|
137
|
-
const psQuote = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* The command that unpacks the artifact, as file + argv.
|
|
141
|
-
*
|
|
142
|
-
* `tar` is present on macOS, on Linux, and on Windows 10 1803 and later (as
|
|
143
|
-
* bsdtar), but Windows ships .zip and older Windows has no tar at all — so zip
|
|
144
|
-
* goes through PowerShell's Expand-Archive, which is always there.
|
|
145
|
-
*
|
|
146
|
-
* Exported, pure, and with the platform as a parameter, because the branch that
|
|
147
|
-
* needs checking is the one that never runs on the machine running the tests.
|
|
148
|
-
*
|
|
149
|
-
* Only the tar branch gets to pass its paths as arguments. `powershell.exe
|
|
150
|
-
* -Command` has no argument channel to pass them through — everything after
|
|
151
|
-
* -Command is joined back into the script text — so the paths are part of a
|
|
152
|
-
* PowerShell program either way, and quoting them for PowerShell is the whole
|
|
153
|
-
* job. Nothing else is quoting them on the way there: powershell.exe is an .exe,
|
|
154
|
-
* so exec.mjs spawns it directly with shell:false and the -Command string
|
|
155
|
-
* arrives as one intact argv entry, never seeing cmd.exe.
|
|
156
|
-
*/
|
|
157
|
-
export function extractCommand(archivePath, destDir, platform = process.platform) {
|
|
158
|
-
if (platform === "win32" && archivePath.endsWith(".zip")) {
|
|
159
|
-
return {
|
|
160
|
-
file: "powershell.exe",
|
|
161
|
-
args: [
|
|
162
|
-
"-NoProfile", "-NonInteractive", "-Command",
|
|
163
|
-
`Expand-Archive -LiteralPath ${psQuote(archivePath)} ` +
|
|
164
|
-
`-DestinationPath ${psQuote(destDir)} -Force`,
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return { file: "tar", args: ["-xzf", archivePath, "-C", destDir] };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Unpack the artifact. */
|
|
172
|
-
async function extract(archivePath, destDir) {
|
|
173
|
-
const { file, args } = extractCommand(archivePath, destDir);
|
|
174
|
-
const r = await run(file, args, { timeout: 120_000 });
|
|
175
|
-
return r.ok;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Find the uv executable somewhere under `dir`.
|
|
180
|
-
*
|
|
181
|
-
* The archives put it in a versioned subdirectory whose name has changed
|
|
182
|
-
* between releases, so it is located rather than assumed.
|
|
183
|
-
*/
|
|
184
|
-
async function findUv(dir, depth = 0) {
|
|
185
|
-
const wanted = process.platform === "win32" ? "uv.exe" : "uv";
|
|
186
|
-
let entries;
|
|
187
|
-
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return null; }
|
|
188
|
-
for (const e of entries) {
|
|
189
|
-
if (e.isFile() && e.name === wanted) return join(dir, e.name);
|
|
190
|
-
}
|
|
191
|
-
if (depth >= 2) return null;
|
|
192
|
-
for (const e of entries) {
|
|
193
|
-
if (!e.isDirectory()) continue;
|
|
194
|
-
const hit = await findUv(join(dir, e.name), depth + 1);
|
|
195
|
-
if (hit) return hit;
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Download and verify uv into ~/.agents-deck/tools/uv.
|
|
202
|
-
*
|
|
203
|
-
* Returns { ok: true, bin, version } or { ok: false, reason }. Never throws:
|
|
204
|
-
* every caller treats a missing uv as an expected state.
|
|
205
|
-
*/
|
|
206
|
-
export async function bootstrapUv() {
|
|
207
|
-
// A uv from an earlier run is reused, but only once it has proved it still
|
|
208
|
-
// runs. Existing-and-therefore-good was a one-way door: the caller has no
|
|
209
|
-
// other installer left by the time it gets here, and nothing anywhere ever
|
|
210
|
-
// deletes or re-checks this path, so a uv that had been damaged — by an
|
|
211
|
-
// interrupted copy from a deck older than this fix, a half-finished disk
|
|
212
|
-
// restore, a truncating backup tool — made every boot from then on fail with
|
|
213
|
-
// `install_failed` until the user found and deleted the directory by hand.
|
|
214
|
-
// One `--version` costs milliseconds and turns that into a re-download.
|
|
215
|
-
const already = existingBootstrappedUv();
|
|
216
|
-
if (already && (await run(already, ["--version"], { timeout: 15_000 })).ok) {
|
|
217
|
-
return { ok: true, bin: already, version: "existing" };
|
|
218
|
-
}
|
|
219
|
-
// A broken one is deliberately left where it is rather than deleted: the
|
|
220
|
-
// download below renames its replacement over it, and if the download cannot
|
|
221
|
-
// happen the file is harmless — every caller probes `--version` before using
|
|
222
|
-
// it, so a uv that does not answer is already treated as absent.
|
|
223
|
-
|
|
224
|
-
// Downloading an executable is a bigger step than installing a package with a
|
|
225
|
-
// tool the user already chose, so it gets its own off switch as well as the
|
|
226
|
-
// blanket one — someone may want the managed installs and not this.
|
|
227
|
-
if (process.env.AGENTS_DECK_NO_DOWNLOAD === "1") return { ok: false, reason: "download_disabled" };
|
|
228
|
-
|
|
229
|
-
const asset = assetName();
|
|
230
|
-
if (!asset) return { ok: false, reason: "unsupported_platform" };
|
|
231
|
-
|
|
232
|
-
const version = await latestVersion();
|
|
233
|
-
const base = `${DOWNLOAD_BASE}/${version}/${asset}`;
|
|
234
|
-
|
|
235
|
-
// Created inside the try, because it is created rather than merely named:
|
|
236
|
-
// see the mkdtemp below for why that distinction is the whole fix.
|
|
237
|
-
let staging = null;
|
|
238
|
-
// Set while a half-finished binary exists under a name of its own, cleared the
|
|
239
|
-
// moment it is renamed into place; the finally below removes whatever is left.
|
|
240
|
-
let partial = null;
|
|
241
|
-
try {
|
|
242
|
-
const [archive, sumText] = await Promise.all([
|
|
243
|
-
download(base),
|
|
244
|
-
download(`${base}.sha256`, { asText: true }),
|
|
245
|
-
]);
|
|
246
|
-
if (!archive) return { ok: false, reason: "download_failed" };
|
|
247
|
-
|
|
248
|
-
// No published checksum means no way to know what was downloaded, and an
|
|
249
|
-
// unverified binary is not something to run on someone's machine.
|
|
250
|
-
const expected = sumText?.trim().split(/\s+/)[0]?.toLowerCase();
|
|
251
|
-
if (!expected || !/^[0-9a-f]{64}$/.test(expected)) {
|
|
252
|
-
return { ok: false, reason: "no_checksum" };
|
|
253
|
-
}
|
|
254
|
-
const actual = createHash("sha256").update(archive).digest("hex");
|
|
255
|
-
if (actual !== expected) return { ok: false, reason: "checksum_mismatch" };
|
|
256
|
-
|
|
257
|
-
// ── where the archive is unpacked, and why it is not the system temp dir ──
|
|
258
|
-
//
|
|
259
|
-
// This used to be `join(tmpdir(), `agents-deck-uv-${process.pid}`)`, created
|
|
260
|
-
// with `mkdir(..., { recursive: true })` — which does not fail on a
|
|
261
|
-
// directory that is already there.
|
|
262
|
-
//
|
|
263
|
-
// On macOS tmpdir() is a per-user `/var/folders/…/T` and on Windows it is
|
|
264
|
-
// inside the profile, so the header's promise held on both. On Linux it is
|
|
265
|
-
// `/tmp`, mode 1777, writable by every account on the box. The comment two
|
|
266
|
-
// hundred lines up reasons carefully about tmpdir() being inside the user's
|
|
267
|
-
// profile ON WINDOWS and never asks the Linux question.
|
|
268
|
-
//
|
|
269
|
-
// Three things then line up. The name is derived from a pid, so it is
|
|
270
|
-
// cheap to blanket the whole plausible range in advance. `findUv` returns a
|
|
271
|
-
// depth-0 file before it recurses, while the genuine uv always sits one
|
|
272
|
-
// level down inside the archive's versioned directory — so a planted
|
|
273
|
-
// `/tmp/agents-deck-uv-<pid>/uv` wins deterministically rather than by
|
|
274
|
-
// racing. And the SHA-256 above is computed on the downloaded buffer, never
|
|
275
|
-
// on the extracted file, so it does not cover the thing that is executed.
|
|
276
|
-
// What followed was a copy, a chmod 0755, a `--version` run, a rename to
|
|
277
|
-
// the permanent name and reuse of that name forever.
|
|
278
|
-
//
|
|
279
|
-
// `mkdtemp` under TOOL_DIR fixes all three at once: the directory is inside
|
|
280
|
-
// a tree the user already owns, its name is unpredictable, and mkdtemp
|
|
281
|
-
// CREATES — it cannot be handed a directory somebody else made. findUv's
|
|
282
|
-
// ordering is left alone deliberately; it is not wrong on its own, and it
|
|
283
|
-
// stops being reachable once nobody else can write here.
|
|
284
|
-
await mkdir(TOOL_DIR, { recursive: true });
|
|
285
|
-
staging = await mkdtemp(join(TOOL_DIR, "uv-staging-"));
|
|
286
|
-
const archivePath = join(staging, asset);
|
|
287
|
-
await writeFile(archivePath, archive);
|
|
288
|
-
if (!(await extract(archivePath, staging))) return { ok: false, reason: "extract_failed" };
|
|
289
|
-
|
|
290
|
-
const found = await findUv(staging);
|
|
291
|
-
if (!found) return { ok: false, reason: "not_in_archive" };
|
|
292
|
-
|
|
293
|
-
await mkdir(UV_DIR, { recursive: true });
|
|
294
|
-
const dest = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");
|
|
295
|
-
|
|
296
|
-
// Copy rather than rename out of staging: the staging dir is in the system
|
|
297
|
-
// temp directory, which is often a different filesystem, and rename across
|
|
298
|
-
// one fails. But the copy goes to a name of its own INSIDE UV_DIR — same
|
|
299
|
-
// filesystem as the destination, so THAT rename is the atomic kind — and
|
|
300
|
-
// only becomes `uv` once it is whole, flushed, executable and has answered
|
|
301
|
-
// `--version`. Copying 40MB straight to the final name is many writes long,
|
|
302
|
-
// and a Ctrl-C in the middle of them (the boot install runs before the
|
|
303
|
-
// signal handlers are up) left a truncated file under the name every later
|
|
304
|
-
// run trusts. Interrupt this instead and all that is left is an inert temp
|
|
305
|
-
// file: `uv` either does not exist or is a binary that ran once already.
|
|
306
|
-
// The pid keeps two decks starting at once off each other's copy, and the
|
|
307
|
-
// .exe keeps the file executable on Windows, where the version check below
|
|
308
|
-
// would otherwise have nothing to spawn.
|
|
309
|
-
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
310
|
-
partial = join(UV_DIR, `.uv-${process.pid}-${Date.now().toString(36)}${suffix}`);
|
|
311
|
-
await copyFile(found, partial);
|
|
312
|
-
// Flushed before the rename, so a crash or power cut just after a
|
|
313
|
-
// successful bootstrap cannot leave the name pointing at blocks that were
|
|
314
|
-
// never written — the same guarantee writeFileAtomic gives settings.json.
|
|
315
|
-
const handle = await open(partial, "r+");
|
|
316
|
-
try { await handle.sync(); } finally { await handle.close(); }
|
|
317
|
-
if (process.platform !== "win32") await chmod(partial, 0o755);
|
|
318
|
-
|
|
319
|
-
const check = await run(partial, ["--version"], { timeout: 15_000 });
|
|
320
|
-
if (!check.ok) return { ok: false, reason: "does_not_run" };
|
|
321
|
-
|
|
322
|
-
await renameWithRetry(partial, dest);
|
|
323
|
-
partial = null;
|
|
324
|
-
|
|
325
|
-
return { ok: true, bin: dest, version };
|
|
326
|
-
} catch (err) {
|
|
327
|
-
return { ok: false, reason: "error", detail: String(err?.message ?? err).slice(0, 200) };
|
|
328
|
-
} finally {
|
|
329
|
-
// maxRetries for the reason the ccusage repair gives: the archive was just
|
|
330
|
-
// unpacked here and `partial` was just run out of it, and on Windows a file
|
|
331
|
-
// a handle has not finished releasing cannot be deleted — the directory
|
|
332
|
-
// then refuses to go with ENOTEMPTY. Litter rather than a failure, since
|
|
333
|
-
// this is swallowed, but litter in the user's home that nothing else sweeps.
|
|
334
|
-
if (staging) await rm(staging, { recursive: true, force: true, maxRetries: 10, retryDelay: 25 }).catch(() => {});
|
|
335
|
-
if (partial) await rm(partial, { force: true }).catch(() => {});
|
|
336
|
-
}
|
|
337
|
-
}
|