@rightkit/release 0.2.164 → 0.2.172

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.
Files changed (77) hide show
  1. package/README.md +121 -4
  2. package/addon-command.mjs +6 -13
  3. package/android-aab-scan.mjs +265 -0
  4. package/android-dev.mjs +111 -0
  5. package/android-ndk.mjs +77 -0
  6. package/android-play-upload.mjs +262 -0
  7. package/android-release.mjs +344 -0
  8. package/android-test-fixtures.mjs +79 -0
  9. package/apple-profile-create.mjs +363 -0
  10. package/bin/darwin/rightkit-swap +0 -0
  11. package/bin/darwin/rightkit-swap.sign.json +6 -0
  12. package/build-release.mjs +73 -13
  13. package/cargo-guard.mjs +2 -0
  14. package/cargo-profile-contract.mjs +183 -0
  15. package/cargo-target.mjs +8 -0
  16. package/cli/right-release.mjs +14 -4
  17. package/committed-source.mjs +1 -1
  18. package/dev-artifact-refresh.mjs +176 -0
  19. package/dev-artifact-support.mjs +61 -0
  20. package/development-signing.mjs +71 -4
  21. package/development.mjs +78 -46
  22. package/direct-bootstrap.mjs +26 -41
  23. package/disk-command.mjs +315 -13
  24. package/disk-policy.json +7 -5
  25. package/disk-policy.mjs +141 -22
  26. package/disk-stable-root.mjs +19 -16
  27. package/engine-locator.mjs +116 -0
  28. package/external-bin.mjs +133 -0
  29. package/github-release.mjs +2 -2
  30. package/github-updater-release.mjs +154 -0
  31. package/hardeningscan.mjs +1 -1
  32. package/in-place-install.mjs +56 -0
  33. package/install-app-windows.mjs +63 -0
  34. package/install-app.mjs +53 -23
  35. package/local-publish.mjs +11 -1
  36. package/mac-signing-matrix.mjs +23 -11
  37. package/managed-cargo.mjs +6 -0
  38. package/media-sidecar-catalog.mjs +45 -0
  39. package/media-sidecar.mjs +306 -0
  40. package/ownership-approval-command.mjs +10 -44
  41. package/ownership-approval.mjs +3 -42
  42. package/ownership-catalog-config.json +2 -1
  43. package/ownership-catalog.mjs +4 -2
  44. package/ownership-command.mjs +7 -9
  45. package/ownership-dependency-allowlist.json +25 -1
  46. package/ownership-evidence.mjs +8 -5
  47. package/ownership-git-inventory.mjs +41 -7
  48. package/ownership-lockfiles.mjs +91 -8
  49. package/ownership-mandatory-set.json +36 -16
  50. package/ownership-report.mjs +5 -6
  51. package/ownership-scan-worker.mjs +2 -2
  52. package/ownership-verdict.mjs +10 -7
  53. package/ownership.mjs +47 -29
  54. package/package.json +7 -4
  55. package/portable-development.mjs +32 -115
  56. package/preflight.mjs +4 -6
  57. package/publication-artifacts.mjs +34 -0
  58. package/publish-update.mjs +72 -11
  59. package/qa-adoption-exemptions.json +5 -0
  60. package/qa-build-features.mjs +7 -0
  61. package/qa-contract.mjs +182 -0
  62. package/qa-control-scan.mjs +3 -3
  63. package/release-qa-command.mjs +38 -12
  64. package/release-qa-installed.mjs +136 -0
  65. package/release-qa-receipt.mjs +23 -2
  66. package/release.mjs +283 -14
  67. package/rightkit-ownership.json +537 -5
  68. package/rightkit-sync.mjs +26 -0
  69. package/rightkit-versions.json +110 -62
  70. package/runtime-budget-classes.json +28 -0
  71. package/runtime-budgets.mjs +4 -1
  72. package/sign-candidate.mjs +161 -0
  73. package/sign-dev-artifact.mjs +198 -0
  74. package/stage-binary.mjs +60 -0
  75. package/swap-helper-bundle.mjs +105 -0
  76. package/templates/cargo-profiles.toml +28 -0
  77. package/upload-release.mjs +11 -1
package/disk-command.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawnSync } from "./background-process.mjs";
7
7
  import { compareVersions } from "./rightkit-sync.mjs";
8
8
  import {
9
- DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy,
9
+ DISK_EXIT_WARNING, applySweep, checkDisk, ensureScratchRoots, formatBytes, listOpenPaths, logEvent, planSweep, platformKey, purgeHoldings, readDiskPolicy, setWorkspaceSweepAllowed,
10
10
  } from "./disk-policy.mjs";
11
11
  import { evaluateBinding, installStableRoot, readScheduleBinding, selfCheck } from "./disk-stable-root.mjs";
12
12
 
@@ -27,7 +27,7 @@ export function renderCheck(result) {
27
27
  return lines.join("\n");
28
28
  }
29
29
 
30
- export function renderSweep(plan, applied) {
30
+ export function renderSweep(plan, applied, { all = false } = {}) {
31
31
  const lines = [`${applied ? "Swept" : "Dry run: would move"} ${plan.candidates.length} item(s), ${formatBytes(plan.totalBytes)} (stale > ${plan.staleDays} days)`];
32
32
  const rows = applied ? applied.results : plan.candidates;
33
33
  const byWriter = new Map();
@@ -35,24 +35,27 @@ export function renderSweep(plan, applied) {
35
35
  for (const [writer, items] of byWriter) {
36
36
  const total = items.reduce((s, r) => s + r.bytes, 0);
37
37
  lines.push(` ${writer.padEnd(26)} ${String(items.length).padStart(4)} item(s) ${formatBytes(total).padStart(9)}`);
38
- for (const r of [...items].sort((a, b) => b.bytes - a.bytes).slice(0, 5)) lines.push(` ${formatBytes(r.bytes).padStart(9)} ${r.path}${r.destination ? ` -> ${r.destination}` : ""}${r.ok === false ? ` FAILED: ${r.error}` : ""}`);
39
- if (items.length > 5) lines.push(` ... ${items.length - 5} more`);
38
+ for (const r of [...items].sort((a, b) => b.bytes - a.bytes).slice(0, all ? Infinity : 5)) lines.push(` ${formatBytes(r.bytes).padStart(9)} ${r.path}${r.destination ? ` -> ${r.destination}` : ""}${r.ok === false ? ` FAILED: ${r.error}` : ""}`);
39
+ if (!all && items.length > 5) lines.push(` ... ${items.length - 5} more`);
40
40
  }
41
41
  const counts = {};
42
42
  for (const s of plan.skipped) counts[s.reason] = (counts[s.reason] ?? 0) + 1;
43
+ const reportOnly = plan.skipped.filter((s) => s.reason === "report-only");
44
+ if (reportOnly.length) lines.push(` workspace-unknown is report-only on this host: ${reportOnly.length} unknown entr${reportOnly.length === 1 ? "y" : "ies"} will not be moved (opt in with: right-release disk allow-workspace-sweep)`);
43
45
  if (plan.skipped.length) lines.push(` skipped: ${Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ")}`);
44
46
  return lines.join("\n");
45
47
  }
46
48
 
47
49
  function parse(rest) {
48
- const known = new Set(["--json", "--dry-run", "--delete", "--days", "--policy"]);
49
- const opts = { json: false, dryRun: false, deleteMode: false, days: undefined, policy: undefined };
50
+ const known = new Set(["--json", "--dry-run", "--delete", "--all", "--days", "--policy"]);
51
+ const opts = { json: false, dryRun: false, deleteMode: false, all: false, days: undefined, policy: undefined };
50
52
  for (let i = 0; i < rest.length; i += 1) {
51
53
  const a = rest[i];
52
54
  if (!known.has(a)) throw new Error(`disk: unknown option ${a}`);
53
55
  if (a === "--json") opts.json = true;
54
56
  else if (a === "--dry-run") opts.dryRun = true;
55
57
  else if (a === "--delete") opts.deleteMode = true;
58
+ else if (a === "--all") opts.all = true;
56
59
  else { const v = rest[++i]; if (!v) throw new Error(`disk: ${a} needs a value`); if (a === "--days") opts.days = Number(v); else opts.policy = v; }
57
60
  }
58
61
  if (opts.days !== undefined && !(opts.days >= 0)) throw new Error("disk: --days must be a non-negative number");
@@ -147,7 +150,8 @@ function defaultGlobalRoots() {
147
150
 
148
151
  export function scheduleSpec({ platform = process.platform, nodePath = process.execPath, cli = path.join(path.dirname(fileURLToPath(import.meta.url)), "cli", "right-release.mjs"), home = os.homedir(), hour = 3, minute = 30 } = {}) {
149
152
  if (platform === "darwin") {
150
- const logDir = "/Volumes/D/claude/tmp/swept";
153
+ // launchd cannot open log files on an external volume for a background job (spawn fails with EX_CONFIG);
154
+ // `disk run` writes its own structured log to disk-hygiene.jsonl on the heavy volume.
151
155
  const file = path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
152
156
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
153
157
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -155,8 +159,8 @@ export function scheduleSpec({ platform = process.platform, nodePath = process.e
155
159
  <key>Label</key><string>${LAUNCHD_LABEL}</string>
156
160
  <key>ProgramArguments</key><array><string>${nodePath}</string><string>${cli}</string><string>disk</string><string>run</string></array>
157
161
  <key>StartCalendarInterval</key><dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>${minute}</integer></dict>
158
- <key>StandardOutPath</key><string>${logDir}/launchd.out.log</string>
159
- <key>StandardErrorPath</key><string>${logDir}/launchd.err.log</string>
162
+ <key>StandardOutPath</key><string>/dev/null</string>
163
+ <key>StandardErrorPath</key><string>/dev/null</string>
160
164
  <key>ProcessType</key><string>Background</string><key>LowPriorityIO</key><true/><key>Nice</key><integer>10</integer>
161
165
  </dict></plist>
162
166
  `;
@@ -239,13 +243,311 @@ function schedule(sub, { stdout, cli, platform = process.platform, env = process
239
243
  return 0;
240
244
  }
241
245
 
246
+ // ---- agent-env and probe: keep every agent and child-tool write on the external root (Adrian, 2026-10-17) -------------------
247
+
248
+ const defaultAgentFs = { existsSync, statSync, mkdirSync, writeFileSync, unlinkSync, realpathSync };
249
+
250
+ const pathFor = (platform) => (platform === "win32" ? path.win32 : path.posix);
251
+
252
+ /** External root used when --root is absent: D:\ on Windows, /Volumes/D on macOS. No other platform is supported. */
253
+ export function defaultExternalRoot(platform = process.platform) {
254
+ if (platform === "win32") return "D:\\";
255
+ if (platform === "darwin") return "/Volumes/D";
256
+ throw new Error(`disk agent-env: unsupported platform ${platform}; pass --root`);
257
+ }
258
+
259
+ function envValue(env, name, platform) {
260
+ if (platform !== "win32") return env[name];
261
+ const key = Object.keys(env).find((k) => k.toLowerCase() === name.toLowerCase());
262
+ return key === undefined ? undefined : env[key];
263
+ }
264
+
265
+ /** The one table of agent-environment paths, relative to the external root, identical on Windows and macOS (these are the D:
266
+ * paths RustRight already applied to the user environment). RustRight's broker imports this. `agentHome` rows are emitted only
267
+ * with --include-agent-homes. CARGO_TARGET_DIR is deliberately absent: RustRight owns cargo targets. */
268
+ export const AGENT_ENV_TABLE = Object.freeze([
269
+ { name: "TEMP", segments: ["agent-cache", "tmp"] },
270
+ { name: "TMP", segments: ["agent-cache", "tmp"] },
271
+ { name: "TMPDIR", segments: ["agent-cache", "tmp"] },
272
+ { name: "CLAUDE_CODE_TMPDIR", segments: ["agent-cache", "tmp"] },
273
+ { name: "XDG_CACHE_HOME", segments: ["agent-cache", "xdg"] },
274
+ { name: "XDG_DATA_HOME", segments: ["agent-cache", "xdg-data"] },
275
+ { name: "XDG_STATE_HOME", segments: ["agent-cache", "xdg-state"] },
276
+ { name: "XDG_CONFIG_HOME", segments: ["agent-cache", "xdg-config"] },
277
+ { name: "npm_config_cache", segments: [".dev-cache", "npm-cache"] },
278
+ { name: "PNPM_STORE_DIR", segments: ["agent-cache", "pnpm-store"] },
279
+ { name: "YARN_CACHE_FOLDER", segments: ["agent-cache", "yarn"] },
280
+ { name: "PIP_CACHE_DIR", segments: [".dev-cache", "pip"] },
281
+ { name: "UV_CACHE_DIR", segments: [".dev-cache", "uv"] },
282
+ { name: "HF_HOME", segments: [".dev-cache", "huggingface"] },
283
+ { name: "TORCH_HOME", segments: ["agent-cache", "torch"] },
284
+ { name: "TRANSFORMERS_CACHE", segments: ["agent-cache", "transformers"] },
285
+ { name: "PLAYWRIGHT_BROWSERS_PATH", segments: ["agent-cache", "playwright"] },
286
+ { name: "PUPPETEER_CACHE_DIR", segments: ["agent-cache", "puppeteer"] },
287
+ { name: "NUGET_PACKAGES", segments: ["agent-cache", "nuget"] },
288
+ { name: "GOCACHE", segments: ["agent-cache", "go-build"] },
289
+ { name: "GOMODCACHE", segments: ["agent-cache", "go-mod"] },
290
+ { name: "DOTNET_CLI_HOME", segments: ["agent-cache", "dotnet"] },
291
+ { name: "ELECTRON_CACHE", segments: ["agent-cache", "electron"] },
292
+ { name: "DENO_DIR", segments: ["agent-cache", "deno"] },
293
+ { name: "CODEX_HOME", segments: ["agent-home", "codex"], agentHome: true },
294
+ { name: "CLAUDE_CONFIG_DIR", segments: ["agent-home", "claude"], agentHome: true },
295
+ ]);
296
+
297
+ /** Ordered variable map for one external root from AGENT_ENV_TABLE. Pure. */
298
+ export function agentEnvMap({ root, platform = process.platform, includeAgentHomes = false } = {}) {
299
+ if (!root) throw new Error("agentEnvMap needs a root");
300
+ const p = pathFor(platform);
301
+ return AGENT_ENV_TABLE.filter((e) => includeAgentHomes || !e.agentHome).map((e) => ({ name: e.name, value: p.join(root, ...e.segments) }));
302
+ }
303
+
304
+ export const AGENT_ENV_NOTES = [
305
+ "CARGO_TARGET_DIR is intentionally NOT set: RustRight owns cargo targets.",
306
+ "CODEX_HOME and CLAUDE_CONFIG_DIR are only emitted with --include-agent-homes (moving them requires restarting every chat).",
307
+ ];
308
+
309
+ const shQuote = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
310
+ const psQuote = (v) => `'${String(v).replace(/'/g, "''")}'`;
311
+
312
+ /** Render the map as a shell script, a PowerShell script, a cmd batch or JSON. Pure. */
313
+ export function renderAgentEnv(map, { format, root, notes = AGENT_ENV_NOTES } = {}) {
314
+ if (format === "json") return JSON.stringify({ root, env: Object.fromEntries(map.map((m) => [m.name, m.value])), notes }, null, 2);
315
+ if (format === "sh") return [...notes.map((n) => `# ${n}`), ...map.map((m) => `export ${m.name}=${shQuote(m.value)}`)].join("\n");
316
+ if (format === "ps1") return [...notes.map((n) => `# ${n}`), ...map.map((m) => `$env:${m.name} = ${psQuote(m.value)}`)].join("\n");
317
+ if (format === "cmd") return [...notes.map((n) => `rem ${n}`), ...map.map((m) => `set "${m.name}=${m.value}"`)].join("\r\n");
318
+ throw new Error(`disk agent-env: unknown --format ${format} (sh|ps1|cmd|json)`);
319
+ }
320
+
321
+ function parseAgentEnv(rest, platform) {
322
+ const opts = { root: undefined, format: platform === "win32" ? "ps1" : "sh", check: false, includeAgentHomes: false };
323
+ for (let i = 0; i < rest.length; i += 1) {
324
+ const a = rest[i];
325
+ if (a === "--check") opts.check = true;
326
+ else if (a === "--include-agent-homes") opts.includeAgentHomes = true;
327
+ else if (a === "--root" || a === "--format") {
328
+ const v = rest[++i];
329
+ if (!v) throw new Error(`disk agent-env: ${a} needs a value`);
330
+ if (a === "--root") opts.root = v; else opts.format = v;
331
+ } else throw new Error(`disk agent-env: unknown option ${a}`);
332
+ }
333
+ if (!["sh", "ps1", "cmd", "json"].includes(opts.format)) throw new Error(`disk agent-env: unknown --format ${opts.format} (sh|ps1|cmd|json)`);
334
+ return opts;
335
+ }
336
+
337
+ const LOCAL_UNC_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "."]);
338
+
339
+ /** Realpath of `value`, resolving its nearest existing ancestor when the path itself does not exist yet. Null when nothing resolves. */
340
+ function realpathOfExisting(value, realpath) {
341
+ let current = value;
342
+ let rest = "";
343
+ for (;;) {
344
+ try { return rest ? path.posix.join(realpath(current), rest) : realpath(current); } catch { /* try the parent */ }
345
+ const up = path.posix.dirname(current);
346
+ if (up === current) return null;
347
+ rest = rest ? `${path.posix.basename(current)}/${rest}` : path.posix.basename(current);
348
+ current = up;
349
+ }
350
+ }
351
+
352
+ /** True when `value` lies on the internal drive, or cannot be proven external. Windows: a path on the system drive, including the
353
+ * `\\?\C:\`, `\\.\C:\` and `\\?\UNC\localhost\C$\` / `\\localhost\C$\` spellings; any other path on this machine's own host name or
354
+ * loopback, a relative path and any device or volume-GUID path are unprovable and count as internal. macOS: any path outside
355
+ * /Volumes, and a /Volumes entry that resolves outside it (`/Volumes/Macintosh HD` is a symlink to /). Pure apart from the
356
+ * injectable `realpath` used on macOS. */
357
+ export function pointsAtInternalDrive(value, { platform = process.platform, env = {}, realpath = realpathSync } = {}) {
358
+ const v = String(value ?? "");
359
+ if (platform === "win32") {
360
+ const system = String(envValue(env, "SystemDrive", platform) ?? "C:").slice(0, 1).toUpperCase();
361
+ let p = v.replace(/\//g, "\\");
362
+ let unc = null;
363
+ const extended = /^\\\\[?.]\\(.*)$/.exec(p);
364
+ if (extended) {
365
+ const tail = extended[1];
366
+ if (/^UNC\\/i.test(tail)) unc = tail.slice(4);
367
+ else if (/^[A-Za-z]:/.test(tail)) p = tail;
368
+ else return true;
369
+ } else if (/^\\\\[^\\]/.test(p)) unc = p.slice(2);
370
+ if (unc !== null) {
371
+ const [host = "", share = ""] = unc.split("\\");
372
+ const hostName = host.toLowerCase();
373
+ const own = String(envValue(env, "COMPUTERNAME", platform) ?? "").toLowerCase();
374
+ if (!host) return true;
375
+ if (!LOCAL_UNC_HOSTS.has(hostName) && !(own && hostName === own)) return false;
376
+ const admin = /^([A-Za-z])\$$/.exec(share);
377
+ return admin ? admin[1].toUpperCase() === system : true;
378
+ }
379
+ const m = /^([A-Za-z]):/.exec(p);
380
+ if (!m) return true;
381
+ return m[1].toUpperCase() === system;
382
+ }
383
+ if (!v.startsWith("/Volumes/")) return true;
384
+ if (platform === "darwin") {
385
+ const real = realpathOfExisting(v, realpath);
386
+ if (real !== null && !real.startsWith("/Volumes/")) return true;
387
+ }
388
+ return false;
389
+ }
390
+
391
+ function isUnder(value, root, platform) {
392
+ const p = pathFor(platform);
393
+ const norm = (s) => { const n = p.resolve(s); return platform === "win32" ? n.toLowerCase() : n; };
394
+ const r = norm(root);
395
+ const v = norm(value);
396
+ return v === r || v.startsWith(r.endsWith(p.sep) ? r : r + p.sep);
397
+ }
398
+
399
+ function uniqueProbeName(nonce) {
400
+ return `.rk-disk-probe-${process.pid}-${nonce ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`}`;
401
+ }
402
+
403
+ /** Create then immediately remove one 0-byte uniquely named file in `dir`. Writes nothing else. Pure with injected fs. */
404
+ const BLOCKED_CODES = new Set(["EACCES", "EPERM", "EROFS"]);
405
+
406
+ /** Outcome: `allowed` (created and removed), `blocked` (EACCES/EPERM/EROFS), or `error` (missing directory or any other
407
+ * failure, with its code). A failed removal is `allowed` with code `UNLINK_<code>` and `leftover` set. */
408
+ export function probeDirectory(dir, { fs = defaultAgentFs, nonce, platform = process.platform } = {}) {
409
+ let file;
410
+ try {
411
+ if (!dir) return { outcome: "error", code: "ENODIR" };
412
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return { outcome: "error", code: "ENOENT" };
413
+ file = pathFor(platform).join(dir, uniqueProbeName(nonce));
414
+ fs.writeFileSync(file, "", { flag: "wx" });
415
+ } catch (error) {
416
+ const code = error.code ?? "EUNKNOWN";
417
+ return { outcome: BLOCKED_CODES.has(code) ? "blocked" : "error", code };
418
+ }
419
+ try {
420
+ fs.unlinkSync(file);
421
+ } catch (error) {
422
+ return { outcome: "allowed", code: `UNLINK_${error.code ?? "EUNKNOWN"}`, leftover: true, file };
423
+ }
424
+ return { outcome: "allowed", code: null };
425
+ }
426
+
427
+ /** Locations that must be blocked for agents: user profile root, local temp (AppData\Local\Temp / default OS temp), OS temp, and
428
+ * the external root. */
429
+ export function probeLocations({ platform = process.platform, env = process.env, home = os.homedir(), tmpdir = os.tmpdir(), root } = {}) {
430
+ const p = pathFor(platform);
431
+ const localTemp = platform === "win32"
432
+ ? p.join(envValue(env, "LOCALAPPDATA", platform) ?? p.join(home, "AppData", "Local"), "Temp")
433
+ : (envValue(env, "TMPDIR", platform) ?? tmpdir);
434
+ return [
435
+ { location: "profile", dir: home },
436
+ { location: "local-temp", dir: localTemp },
437
+ { location: "os-temp", dir: tmpdir },
438
+ { location: "external-root", dir: root ?? defaultExternalRoot(platform) },
439
+ ];
440
+ }
441
+
442
+ export const PROBE_SCHEMA = "rightkit.disk.probe.v1";
443
+
444
+ /** Probe all four locations; the report is the exact `rightkit.disk.probe.v1` shape. `internalWritable` is true when any
445
+ * internal location (profile, local-temp, os-temp) accepted the probe file. */
446
+ export function runProbe({ platform = process.platform, env = process.env, home, tmpdir, root, fs = defaultAgentFs, nonce, host = os.hostname() } = {}) {
447
+ const locations = probeLocations({ platform, env, ...(home ? { home } : {}), ...(tmpdir ? { tmpdir } : {}), root });
448
+ const probed = locations.map((l) => ({ l, r: probeDirectory(l.dir, { fs, nonce, platform }) }));
449
+ const external = locations.find((l) => l.location === "external-root").dir;
450
+ // Fail closed. An internal location is sealed only when the write was refused (EACCES, EPERM, EROFS) or the directory does not
451
+ // exist (ENOENT); ENOSPC, EBUSY and every other error prove nothing, so they count against the probe. The external root must
452
+ // accept the probe file, or agent writes have nowhere to go.
453
+ const problems = [];
454
+ for (const { l, r } of probed) {
455
+ if (l.location === "external-root") {
456
+ if (r.outcome !== "allowed") problems.push(`${l.location} ${l.dir} is not writable (${r.code})`);
457
+ } else if (r.outcome === "allowed") problems.push(`${l.location} ${l.dir} is writable`);
458
+ else if (r.outcome === "error" && r.code !== "ENOENT") problems.push(`${l.location} ${l.dir} could not be proven blocked (${r.code})`);
459
+ }
460
+ return {
461
+ problems,
462
+ report: {
463
+ schema: PROBE_SCHEMA, host, root: external,
464
+ results: probed.map(({ l, r }) => ({ location: l.location, path: l.dir, outcome: r.outcome, code: r.code })),
465
+ internalWritable: probed.some(({ l, r }) => l.location !== "external-root" && r.outcome === "allowed"),
466
+ },
467
+ leftovers: probed.filter(({ r }) => r.leftover).map(({ r }) => r.file),
468
+ };
469
+ }
470
+
471
+ export function renderProbe(report) {
472
+ return [...report.results.map((r) => `${r.outcome.toUpperCase().padEnd(8)} ${r.location.padEnd(14)} ${r.path}${r.code ? ` (${r.code})` : ""}`), report.internalWritable ? "INTERNAL DRIVE IS WRITABLE" : "internal drive is not writable"].join("\n");
473
+ }
474
+
475
+ /** Compare the live environment with the map. Each row: ok (under root), elsewhere (external, not under root), internal (set on
476
+ * the internal drive), unset (tool default lands on the internal drive). */
477
+ export function checkAgentEnv(map, { env = process.env, platform = process.platform, root } = {}) {
478
+ return map.map(({ name, value }) => {
479
+ const current = envValue(env, name, platform);
480
+ let status;
481
+ if (current === undefined || current === "") status = "unset";
482
+ else if (pointsAtInternalDrive(current, { platform, env })) status = "internal";
483
+ else if (isUnder(current, root, platform)) status = "ok";
484
+ else status = "elsewhere";
485
+ return { name, expected: value, current: current ?? null, status };
486
+ });
487
+ }
488
+
489
+ function agentEnvCommand(rest, { env, stdout, platform, deps }) {
490
+ const stderr = deps.stderr ?? console.error;
491
+ const fs = deps.fs ?? defaultAgentFs;
492
+ const opts = parseAgentEnv(rest, platform);
493
+ const root = opts.root ?? defaultExternalRoot(platform);
494
+ const fail = (message) => { stderr(`disk agent-env: ${message}; refusing (no fallback to the internal drive)`); return { exitCode: 1 }; };
495
+ if (pointsAtInternalDrive(root, { platform, env, realpath: (p) => fs.realpathSync(p) })) return fail(`root ${root} is on the internal drive`);
496
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return fail(`root ${root} is missing`);
497
+ if (platform === "darwin") {
498
+ let real = root;
499
+ try { real = fs.realpathSync(root); } catch { /* keep lexical */ }
500
+ if (!real.startsWith("/Volumes/")) return fail(`root ${root} resolves to ${real}, not an external volume`);
501
+ }
502
+ const writable = probeDirectory(root, { fs, nonce: deps.nonce, platform });
503
+ if (writable.outcome !== "allowed" || writable.leftover) return fail(`root ${root} is not writable (${writable.code})`);
504
+ const map = agentEnvMap({ root, platform, includeAgentHomes: opts.includeAgentHomes });
505
+ if (opts.check) {
506
+ const rows = checkAgentEnv(map, { env, platform, root });
507
+ const bad = rows.filter((r) => r.status === "internal" || r.status === "unset");
508
+ if (opts.format === "json") stdout(JSON.stringify({ root, ok: bad.length === 0, variables: rows }, null, 2));
509
+ else {
510
+ for (const r of rows) stdout(`${r.status.toUpperCase().padEnd(9)} ${r.name.padEnd(26)} ${r.current ?? "(unset)"}${r.status === "ok" ? "" : ` -> ${r.expected}`}`);
511
+ stdout(bad.length ? `${bad.length} variable(s) point at the internal drive or are unset` : "all variables point at the external root");
512
+ }
513
+ return { exitCode: bad.length ? 1 : 0, rows };
514
+ }
515
+ for (const m of map) fs.mkdirSync(m.value, { recursive: true });
516
+ stdout(renderAgentEnv(map, { format: opts.format, root }));
517
+ return { exitCode: 0, map };
518
+ }
519
+
520
+ function probeCommand(rest, { env, stdout, platform, deps }) {
521
+ let json = false;
522
+ let root;
523
+ for (let i = 0; i < rest.length; i += 1) {
524
+ if (rest[i] === "--json") json = true;
525
+ else if (rest[i] === "--root") { root = rest[++i]; if (!root) throw new Error("disk probe: --root needs a value"); }
526
+ else throw new Error(`disk probe: unknown option ${rest[i]}`);
527
+ }
528
+ const { report, leftovers, problems } = runProbe({ platform, env, root, ...(deps.probe ?? {}), ...(deps.fs ? { fs: deps.fs } : {}), nonce: deps.nonce });
529
+ stdout(json ? JSON.stringify(report) : renderProbe(report));
530
+ const stderr = deps.stderr ?? console.error;
531
+ for (const problem of problems) stderr(`disk probe: ${problem}`);
532
+ for (const file of leftovers) stderr(`disk probe: could not remove ${file}`);
533
+ return { exitCode: report.internalWritable || leftovers.length || problems.length ? 1 : 0, report, problems };
534
+ }
535
+
242
536
  export async function runDiskCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, platform = process.platform, deps = {} } = {}) {
243
537
  const [command, ...rest] = args;
538
+ if (command === "agent-env") return agentEnvCommand(rest, { env, stdout, platform, deps });
539
+ if (command === "probe") return probeCommand(rest, { env, stdout, platform, deps });
244
540
  if (command === "schedule") {
245
541
  const at = rest.indexOf("--cli");
246
542
  const exitCode = schedule(rest[0], { stdout, cli: at === -1 ? undefined : rest[at + 1], platform, env });
247
543
  return { exitCode };
248
544
  }
545
+ if (command === "allow-workspace-sweep") {
546
+ const off = rest.includes("--off");
547
+ const file = setWorkspaceSweepAllowed(!off, { platform, env, ...(deps.flagHome ? { home: deps.flagHome } : {}) });
548
+ stdout(off ? `workspace-unknown sweep revoked on this host (${file}.revoked)` : `workspace-unknown sweep allowed on this host (${file}); review \`disk sweep --dry-run --all\` first, revoke with --off`);
549
+ return { exitCode: 0 };
550
+ }
249
551
  const opts = parse(rest);
250
552
  const policy = readDiskPolicy(opts.policy);
251
553
  const key = platformKey(platform);
@@ -262,9 +564,9 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
262
564
  if (command === "sweep") {
263
565
  if (!opts.dryRun) ensureScratchRoots({ policy, platform, env });
264
566
  const p = plan();
265
- if (opts.dryRun) { out(p, renderSweep(p)); return { exitCode: 0, plan: p }; }
567
+ if (opts.dryRun) { out(p, renderSweep(p, undefined, { all: opts.all })); return { exitCode: 0, plan: p }; }
266
568
  const applied = applySweep({ plan: p, policy, platform, deleteMode: opts.deleteMode, ...deps.apply });
267
- out({ ...p, applied }, renderSweep(p, applied));
569
+ out({ ...p, applied }, renderSweep(p, applied, { all: opts.all }));
268
570
  return { exitCode: applied.results.some((r) => !r.ok) ? 1 : 0, plan: p, applied };
269
571
  }
270
572
  if (command === "purge") {
@@ -294,7 +596,7 @@ export async function runDiskCommand(args = process.argv.slice(2), { env = proce
294
596
  if (warn) stdout(`WARNING: ${after.warnings.join("; ")}`);
295
597
  return { exitCode: !self.ok ? 2 : warn ? DISK_EXIT_WARNING : 0, before, after, applied, purged, selfCheck: self };
296
598
  }
297
- throw new Error("disk usage: check|sweep [--dry-run|--delete] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status [--json]");
599
+ throw new Error("disk usage: check|sweep [--dry-run|--delete] [--all]|allow-workspace-sweep [--off] [--days N]|purge [--dry-run] [--days N]|run|schedule install|uninstall|status|agent-env [--root dir] [--format sh|ps1|cmd|json] [--check] [--include-agent-homes]|probe [--json]");
298
600
  }
299
601
 
300
602
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
package/disk-policy.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "holdingDays": 3,
9
9
  "holding": {
10
- "mac": [{"prefix": "/", "path": "~/.rightkit-swept"}],
10
+ "mac": [{"prefix": "/", "path": "/Volumes/D/.rightkit-swept", "mount": "/Volumes/D"}, {"prefix": "/", "path": "~/.rightkit-swept"}],
11
11
  "win": [{"prefix": "C:\\", "path": "${LOCALAPPDATA}\\Orthic Labs\\.rightkit-swept"}]
12
12
  },
13
13
  "heavyVolume": {
@@ -27,13 +27,15 @@
27
27
  "writers": [
28
28
  {"id": "claude-session-dirs", "platform": "mac", "path": "/private/tmp/claude-501/*/*", "capBytes": 2147483648, "action": "sweep", "unit": "self"},
29
29
  {"id": "rightkit-qa-tmp", "platform": "mac", "path": "/private/tmp/rightkit-qa", "capBytes": 2147483648, "action": "sweep", "unit": "children"},
30
+ {"id": "cache-coreml-e5rt-tmp", "platform": "mac", "path": "~/Library/Caches/*/com.apple.e5rt.e5bundlecache/*/*/*.tmp.*", "capBytes": 4294967296, "action": "sweep", "unit": "self", "staleDays": 1},
31
+ {"id": "cache-coreml-e5rt", "platform": "mac", "path": "~/Library/Caches/*/com.apple.e5rt.e5bundlecache/*/*", "capBytes": 4294967296, "action": "sweep", "unit": "children", "staleDays": 2},
30
32
  {"id": "cache-e2e", "platform": "mac", "path": "~/Library/Caches/e2e_*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
31
33
  {"id": "cache-parakeet", "platform": "mac", "path": "~/Library/Caches/parakeet_*", "capBytes": 1073741824, "action": "sweep", "unit": "self"},
32
34
  {"id": "cache-swiftright-factory", "platform": "mac", "path": "~/Library/Caches/SwiftRightFactory", "capBytes": 10737418240, "action": "sweep", "unit": "children"},
33
- {"id": "cache-cutright-engine", "platform": "mac", "path": "~/Library/Caches/cutright-engine", "capBytes": 2147483648, "action": "report"},
34
- {"id": "cache-heardright-engine", "platform": "mac", "path": "~/Library/Caches/heardright-engine", "capBytes": 2147483648, "action": "report"},
35
- {"id": "cache-pnpm", "platform": "mac", "path": "~/Library/Caches/pnpm", "capBytes": 3221225472, "action": "report"},
36
- {"id": "cache-python", "platform": "mac", "path": "~/Library/Caches/python", "capBytes": 3221225472, "action": "report"},
35
+ {"id": "cache-cutright-engine", "platform": "mac", "path": "~/Library/Caches/cutright-engine", "capBytes": 2147483648, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
36
+ {"id": "cache-heardright-engine", "platform": "mac", "path": "~/Library/Caches/heardright-engine", "capBytes": 2147483648, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
37
+ {"id": "cache-pnpm", "platform": "mac", "path": "~/Library/Caches/pnpm", "capBytes": 3221225472, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
38
+ {"id": "cache-python", "platform": "mac", "path": "~/Library/Caches/python", "capBytes": 3221225472, "action": "report", "unit": "children", "escalate": {"factor": 2, "staleDays": 7}},
37
39
  {"id": "xcode-deriveddata", "platform": "mac", "path": "~/Library/Developer/Xcode/DerivedData", "capBytes": 10737418240, "action": "sweep", "unit": "children"},
38
40
  {"id": "coresimulator", "platform": "mac", "path": "~/Library/Developer/CoreSimulator", "capBytes": 26843545600, "action": "report"},
39
41
  {"id": "documents-scratch", "platform": "mac", "path": "~/Documents", "capBytes": 21474836480, "action": "report"},