agent-dag 1.33.65 → 1.33.67

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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>agents-deck</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-A5CpPEUh.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CcEqayMe.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-CF2yG9oG.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-dag",
3
- "version": "1.33.65",
3
+ "version": "1.33.67",
4
4
  "description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -324,5 +324,7 @@ export { AGENT_DAG_DIR, CLAUDE_DIR, CODEX_DIR, CLAUDE_EVENTS, CODEX_EVENTS };
324
324
  // parse is never treated as an empty one, and the replacement is a single
325
325
  // rename rather than a truncate a reader can land inside. installScript carries
326
326
  // the same guarantee to the hook scripts themselves, which are the files live
327
- // sessions are actually executing.
328
- export { readSettingsForWrite, writeFileAtomic, installScript };
327
+ // sessions are actually executing, and renameWithRetry goes out on its own for
328
+ // the files writeFileAtomic cannot write — the fetched uv binary in
329
+ // uv-bootstrap.mjs — which still need the Windows retry.
330
+ export { readSettingsForWrite, writeFileAtomic, installScript, renameWithRetry };
@@ -14,11 +14,17 @@
14
14
  // is no other way: uv, pipx, and python -m pipx have all already been ruled out
15
15
  // by the caller. AGENTS_DECK_NO_INSTALL=1 disables it along with everything else.
16
16
  import { createHash } from "node:crypto";
17
- import { mkdir, writeFile, rm, chmod, readdir } from "node:fs/promises";
17
+ import { mkdir, writeFile, rm, chmod, readdir, copyFile, open } from "node:fs/promises";
18
18
  import { existsSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { homedir, tmpdir } from "node:os";
21
21
  import { run } from "./exec.mjs";
22
+ // The same rename used to replace settings.json, for the same reason: on
23
+ // Windows a rename over an existing file fails outright while any other process
24
+ // holds the target open, and a virus scanner or the search indexer opens a
25
+ // freshly written executable the instant it appears. Sharing one implementation
26
+ // keeps the retryable-error list from drifting apart between the two callers.
27
+ import { renameWithRetry } from "./installer.mjs";
22
28
 
23
29
  const TOOL_DIR = join(homedir(), ".agents-deck", "tools");
24
30
  const UV_DIR = join(TOOL_DIR, "uv");
@@ -160,8 +166,22 @@ async function findUv(dir, depth = 0) {
160
166
  * every caller treats a missing uv as an expected state.
161
167
  */
162
168
  export async function bootstrapUv() {
169
+ // A uv from an earlier run is reused, but only once it has proved it still
170
+ // runs. Existing-and-therefore-good was a one-way door: the caller has no
171
+ // other installer left by the time it gets here, and nothing anywhere ever
172
+ // deletes or re-checks this path, so a uv that had been damaged — by an
173
+ // interrupted copy from a deck older than this fix, a half-finished disk
174
+ // restore, a truncating backup tool — made every boot from then on fail with
175
+ // `install_failed` until the user found and deleted the directory by hand.
176
+ // One `--version` costs milliseconds and turns that into a re-download.
163
177
  const already = existingBootstrappedUv();
164
- if (already) return { ok: true, bin: already, version: "existing" };
178
+ if (already && (await run(already, ["--version"], { timeout: 15_000 })).ok) {
179
+ return { ok: true, bin: already, version: "existing" };
180
+ }
181
+ // A broken one is deliberately left where it is rather than deleted: the
182
+ // download below renames its replacement over it, and if the download cannot
183
+ // happen the file is harmless — every caller probes `--version` before using
184
+ // it, so a uv that does not answer is already treated as absent.
165
185
 
166
186
  // Downloading an executable is a bigger step than installing a package with a
167
187
  // tool the user already chose, so it gets its own off switch as well as the
@@ -175,6 +195,9 @@ export async function bootstrapUv() {
175
195
  const base = `${DOWNLOAD_BASE}/${version}/${asset}`;
176
196
 
177
197
  const staging = join(tmpdir(), `agents-deck-uv-${process.pid}`);
198
+ // Set while a half-finished binary exists under a name of its own, cleared the
199
+ // moment it is renamed into place; the finally below removes whatever is left.
200
+ let partial = null;
178
201
  try {
179
202
  const [archive, sumText] = await Promise.all([
180
203
  download(base),
@@ -201,19 +224,41 @@ export async function bootstrapUv() {
201
224
 
202
225
  await mkdir(UV_DIR, { recursive: true });
203
226
  const dest = join(UV_DIR, process.platform === "win32" ? "uv.exe" : "uv");
204
- // Copy rather than rename: the staging dir is in the system temp directory,
205
- // which is often a different filesystem, and rename across one fails.
206
- const { copyFile } = await import("node:fs/promises");
207
- await copyFile(found, dest);
208
- if (process.platform !== "win32") await chmod(dest, 0o755);
209
227
 
210
- const check = await run(dest, ["--version"], { timeout: 15_000 });
228
+ // Copy rather than rename out of staging: the staging dir is in the system
229
+ // temp directory, which is often a different filesystem, and rename across
230
+ // one fails. But the copy goes to a name of its own INSIDE UV_DIR — same
231
+ // filesystem as the destination, so THAT rename is the atomic kind — and
232
+ // only becomes `uv` once it is whole, flushed, executable and has answered
233
+ // `--version`. Copying 40MB straight to the final name is many writes long,
234
+ // and a Ctrl-C in the middle of them (the boot install runs before the
235
+ // signal handlers are up) left a truncated file under the name every later
236
+ // run trusts. Interrupt this instead and all that is left is an inert temp
237
+ // file: `uv` either does not exist or is a binary that ran once already.
238
+ // The pid keeps two decks starting at once off each other's copy, and the
239
+ // .exe keeps the file executable on Windows, where the version check below
240
+ // would otherwise have nothing to spawn.
241
+ const suffix = process.platform === "win32" ? ".exe" : "";
242
+ partial = join(UV_DIR, `.uv-${process.pid}-${Date.now().toString(36)}${suffix}`);
243
+ await copyFile(found, partial);
244
+ // Flushed before the rename, so a crash or power cut just after a
245
+ // successful bootstrap cannot leave the name pointing at blocks that were
246
+ // never written — the same guarantee writeFileAtomic gives settings.json.
247
+ const handle = await open(partial, "r+");
248
+ try { await handle.sync(); } finally { await handle.close(); }
249
+ if (process.platform !== "win32") await chmod(partial, 0o755);
250
+
251
+ const check = await run(partial, ["--version"], { timeout: 15_000 });
211
252
  if (!check.ok) return { ok: false, reason: "does_not_run" };
212
253
 
254
+ await renameWithRetry(partial, dest);
255
+ partial = null;
256
+
213
257
  return { ok: true, bin: dest, version };
214
258
  } catch (err) {
215
259
  return { ok: false, reason: "error", detail: String(err?.message ?? err).slice(0, 200) };
216
260
  } finally {
217
261
  await rm(staging, { recursive: true, force: true }).catch(() => {});
262
+ if (partial) await rm(partial, { force: true }).catch(() => {});
218
263
  }
219
264
  }