@rightkit/release 0.2.65 → 0.2.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.
@@ -13,8 +13,8 @@ function fixture() {
13
13
  mkdirSync(path.join(root, "legal")); mkdirSync(path.join(root, "assets"));
14
14
  for (const name of ["LICENSE", "EULA.txt", "PRIVACY.md", "THIRD-PARTY-NOTICES.txt"]) writeFileSync(path.join(root, "legal", name), name);
15
15
  writeFileSync(path.join(root, "assets", "tab.png"), "icon");
16
- const files = [["command", "membrane", "out/membrane", true], ["service", "crypt-service", "out/crypt-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false], ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false]].map(([role, name, source, executable]) => ({ role, name, source, executable }));
17
- writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "orthic-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "false", args: [] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } }, null, 2)};\n`);
16
+ const files = [["command", "membrane", "out/membrane", true], ["service", "membrane-service", "out/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false], ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false]].map(([role, name, source, executable]) => ({ role, name, source, executable }));
17
+ writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "false", args: [] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } }, null, 2)};\n`);
18
18
  execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root }); execFileSync("git", ["config", "user.name", "Test"], { cwd: root }); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "init"], { cwd: root });
19
19
  return root;
20
20
  }
@@ -25,7 +25,7 @@ test("add-on dry-run creates no release state and never executes build", () => {
25
25
 
26
26
  test("local adoption verifies lock and stages portable plus Tauri-qualified binaries", async () => {
27
27
  const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?test=${Date.now()}`)).default;
28
- mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "crypt-service"), "service");
28
+ mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
29
29
  const manifest = createAddonManifest({ config, root, platform: "mac", commit: "d".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
30
30
  const sealed = path.join(root, "sealed"); mkdirSync(sealed);
31
31
  for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source)));
@@ -44,11 +44,11 @@ test("local adoption verifies lock and stages portable plus Tauri-qualified bina
44
44
 
45
45
  test("remote downloader accepts only immutable manifest plus file routes", async () => {
46
46
  const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?remote=${Date.now()}`)).default;
47
- mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "crypt-service"), "service");
47
+ mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "membrane-service"), "service");
48
48
  const manifest = createAddonManifest({ config, root, platform: "mac", commit: "e".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
49
49
  const digest = addonManifestSha256(manifest); const base = `https://example.test/membrane/addons/mac/sha256/${digest}/`; const bytes = new Map([[`${base}addon-manifest.json`, Buffer.from(canonicalAddonManifest(manifest))], ...manifest.files.map((file) => [base + encodeURIComponent(file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source))])]);
50
50
  const destination = path.join(root, "remote"); mkdirSync(destination);
51
51
  await downloadImmutableAddOn(`${base}addon-manifest.json`, destination, { fetchImpl: async (url) => ({ ok: bytes.has(url), status: bytes.has(url) ? 200 : 404, arrayBuffer: async () => bytes.get(url) }) });
52
- assert.equal(readFileSync(path.join(destination, "crypt-service"), "utf8"), "service");
52
+ assert.equal(readFileSync(path.join(destination, "membrane-service"), "utf8"), "service");
53
53
  assert.equal(immutableAddonManifestUrl(manifest, `https://github.com/Orthic-Labs/Membrane/releases/download/addon-membrane-v0.1.0-mac-sha256-${digest}/addon-manifest.json`, digest), true);
54
54
  });
@@ -8,12 +8,12 @@ import { addonManifestSha256, addonRoutes, createAddonManifest, validateAddonCon
8
8
  function fixture() {
9
9
  const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-contract-"));
10
10
  mkdirSync(path.join(root, "bin")); mkdirSync(path.join(root, "assets")); mkdirSync(path.join(root, "legal"));
11
- for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/crypt-service", "service"], ["assets/tab.png", "icon"], ["legal/LICENSE", "license"], ["legal/EULA.txt", "eula"], ["legal/PRIVACY.md", "privacy"], ["legal/THIRD-PARTY-NOTICES.txt", "notice"]]) writeFileSync(path.join(root, file), bytes);
11
+ for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/membrane-service", "service"], ["assets/tab.png", "icon"], ["legal/LICENSE", "license"], ["legal/EULA.txt", "eula"], ["legal/PRIVACY.md", "privacy"], ["legal/THIRD-PARTY-NOTICES.txt", "notice"]]) writeFileSync(path.join(root, file), bytes);
12
12
  const files = [
13
- ["command", "membrane", "bin/membrane", true], ["service", "crypt-service", "bin/crypt-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false],
13
+ ["command", "membrane", "bin/membrane", true], ["service", "membrane-service", "bin/membrane-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false],
14
14
  ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false],
15
15
  ].map(([role, name, source, executable]) => ({ role, name, source, executable }));
16
- return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "orthic-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "azure-artifact-signing-v1" }, files } } } };
16
+ return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "membrane-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "azure-artifact-signing-v1" }, files } } } };
17
17
  }
18
18
 
19
19
  test("add-on manifest has portable immutable identity and routes", () => {
package/build-release.mjs CHANGED
@@ -26,12 +26,14 @@ import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInpu
26
26
  import { commandOutputPortable, releaseEnvironment, resolveReleaseLayout, runBuildStateMachine, verifySealedRelease, watchProgress } from "./release-state.mjs";
27
27
  import { acquireCacheLease, acquireSuiteBuildSlot, applyCachePrune, assertWriteVolumeFloors, inspectCache, inspectWriteVolumes, markCacheEntrySuccessful, planCachePrune, readCachePolicy, resolveCacheLayout, resolveSharedCacheIdentity, resolveSharedCacheRoot } from "./cache-policy.mjs";
28
28
  import { createTargetBridge } from "./target-bridge.mjs";
29
- import { managedCargoShimOnPath } from "./cargo-contract.mjs";
29
+ import { brokerManagedHost } from "./cargo-contract.mjs";
30
+ import { resolveTargetRoot } from "./cargo-target.mjs";
30
31
  import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
31
32
  import { buildPathPrefix, collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
32
33
  import { assertCleanSource } from "./source-gate.mjs";
33
34
  import { acquireHeavyWorkSlot, heavyCommandEnvironment, terminateProcessTree } from "./heavy-command.mjs";
34
35
  import { processGroupCpuSeconds } from "./process-liveness.mjs";
36
+ import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
35
37
 
36
38
  // Fingerprint of the pipeline code that can change the bytes we ship or the way
37
39
  // they are signed — deliberately NOT the package version, which moves for docs
@@ -103,6 +105,7 @@ let suiteSlot;
103
105
  let cacheLease;
104
106
  let heavySlot;
105
107
  let interrupted;
108
+ let requestActiveRunStop = null;
106
109
 
107
110
  // A dry-run is an inspection contract, not a partial build. It must finish
108
111
  // before locks, cache directories, receipts, dependency installation, or any
@@ -123,9 +126,10 @@ if (dryRun) {
123
126
 
124
127
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
125
128
  process.once(signal, () => {
126
- if (child?.pid) killTree(child.pid);
127
129
  interrupted = new Error(`release interrupted by ${signal}`);
128
130
  process.exitCode = signal === "SIGINT" ? 130 : 143;
131
+ if (requestActiveRunStop) void requestActiveRunStop(interrupted.message);
132
+ else if (child?.pid) killTree(child.pid);
129
133
  });
130
134
  }
131
135
 
@@ -174,13 +178,23 @@ try {
174
178
  // shells take the Cache V2 branch, bridge src-tauri/target into the release cache,
175
179
  // and then lose CARGO_TARGET_DIR to the broker's unconditional override — so the
176
180
  // app bundle landed in the broker workspace and packaging failed on an empty cache.
177
- const managedCargoTarget = (process.env.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(process.env))
178
- ? resolveManagedCargoTarget(cargoToml)
179
- : null;
181
+ const managedCargoTarget = brokerManagedHost(process.env) ? resolveTargetRoot(cargoToml) : null;
180
182
  const cacheIdentity = resolveSharedCacheIdentity({ cargoLockPath: cargoLock, cargoTomlPath: cargoToml, rustcVerbose: toolVersions.rustc, targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget ?? toolVersions.rustHost, profile: target.profile ?? "release" });
181
183
  const cacheKey = hashFileText(JSON.stringify({ cache: cacheIdentity.fingerprint, signingIdentity })).slice(0, 16);
182
- const cacheMode = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
183
- if (cacheMode !== "legacy" && cacheMode !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
184
+ const cacheModeRequested = process.env.RIGHT_RELEASE_CACHE_MODE ?? (platform === "mac" || platform === "win" ? "shared" : "legacy");
185
+ if (cacheModeRequested !== "legacy" && cacheModeRequested !== "shared") fail("RIGHT_RELEASE_CACHE_MODE must be legacy or shared");
186
+ // On a broker-managed host, Cache V2's lease/slot/prune/floor/mark chain is not
187
+ // running against the directory that will actually hold build output — the
188
+ // broker owns CARGO_TARGET_DIR unconditionally (see managedCargoTarget above).
189
+ // Leaving cacheMode at "shared" there ran that whole chain against an
190
+ // essentially unrelated cache root: acquireSuiteBuildSlot/acquireCacheLease
191
+ // guarded a lock nothing else respects, and assertWriteVolumeFloors could
192
+ // fail() the build outright on a volume that isn't where the real ~18 GB of
193
+ // output lives. Force broker so none of that machinery runs.
194
+ const cacheMode = managedCargoTarget ? "broker" : cacheModeRequested;
195
+ if (managedCargoTarget && cacheModeRequested === "shared") {
196
+ console.error("[right-release] broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 shared-cache lease/prune/floor checks skipped");
197
+ }
184
198
  const sharedCacheRoot = cacheMode === "shared" ? resolveSharedCacheRoot({ platform, env: process.env }) : undefined;
185
199
  const sharedLayout = cacheMode === "shared"
186
200
  ? resolveCacheLayout({ cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, fingerprint: cacheKey, kind: "release" })
@@ -216,9 +230,9 @@ try {
216
230
  delete env[name];
217
231
  }
218
232
  }
219
- if (cacheMode === "legacy") delete env.RIGHT_RELEASE_CACHE_OWNER;
233
+ if (cacheMode !== "shared") delete env.RIGHT_RELEASE_CACHE_OWNER;
220
234
  if (cacheMode === "shared" && env.RIGHT_RELEASE_CACHE_OWNER !== "rightkit-v2") fail("shared cache ownership token was not configured");
221
- if (cacheMode === "legacy" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
235
+ if (cacheMode !== "shared" && !commandExists("sccache")) delete env.RUSTC_WRAPPER;
222
236
  if (process.env.RIGHTSUITE_HEAVY_WORK_OWNER !== "1") heavySlot = acquireHeavyWorkSlot();
223
237
  if (cacheMode === "shared") {
224
238
  suiteSlot = acquireSuiteBuildSlot({ layout: sharedLayout, pid: process.pid, argv: process.argv, waitMs: Number(process.env.BUILD_GUARD_WAIT_SECS ?? 900) * 1000 });
@@ -250,6 +264,7 @@ try {
250
264
  link: targetLink,
251
265
  target: managedCargoTarget ?? env.CARGO_TARGET_DIR,
252
266
  ownedRoot: path.dirname(managedCargoTarget ?? env.CARGO_TARGET_DIR),
267
+ brokerManaged: Boolean(managedCargoTarget),
253
268
  });
254
269
 
255
270
  const result = await targetBridge.run(async () => runBuildStateMachine({
@@ -285,7 +300,13 @@ try {
285
300
  throwIfInterrupted();
286
301
  const cacheTarget = managedCargoTarget ?? env.CARGO_TARGET_DIR;
287
302
  mkdirSync(cacheTarget, { recursive: true });
288
- if (cacheMode === "shared") targetBridge.ensure();
303
+ // Broker-managed hosts route through the same target bridge as shared
304
+ // mode even though cacheMode is forced to "broker" above: the naive
305
+ // else-branch below fail()s outright when targetLink is a real
306
+ // directory, which is exactly the "corruption" misreading that nearly
307
+ // cost ~5.9 GB of warm broker intermediates. The bridge's brokerManaged
308
+ // flag already knows to leave a real directory untouched instead.
309
+ if (cacheMode === "shared" || managedCargoTarget) targetBridge.ensure();
289
310
  else if (!existsSync(targetLink)) symlinkSync(cacheTarget, targetLink, process.platform === "win32" ? "junction" : "dir");
290
311
  else if (realpathSync(targetLink) !== realpathSync(cacheTarget)) {
291
312
  fail(`primary checkout target exists and is not the release cache link: ${targetLink}`);
@@ -307,7 +328,7 @@ try {
307
328
  checkpoint(stateRoot, "hardened");
308
329
  },
309
330
  seal: async ({ sealedDir }) => {
310
- sealRelease({ configRoot: appRoot, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
331
+ sealRelease({ configRoot: appRoot, managedCargoTarget, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
311
332
  verifySealedRelease(sealedDir);
312
333
  if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
313
334
  checkpoint(stateRoot, "sealed");
@@ -347,17 +368,32 @@ function sqlCipherFeatures(text) {
347
368
  .filter((value) => /sqlcipher|openssl/i.test(value));
348
369
  }
349
370
 
350
- function sealRelease({ configRoot, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity }) {
371
+ function sealRelease({ configRoot, managedCargoTarget, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity }) {
372
+ // Every app's right-release.config.mjs names installer/updater artifacts
373
+ // relative to the local `src-tauri/target`. On a broker-managed host that
374
+ // directory is never populated (the broker owns CARGO_TARGET_DIR and
375
+ // targetBridge.ensure() deliberately leaves a real local directory alone,
376
+ // see target-bridge.mjs) so those paths must resolve against the broker's
377
+ // own target root instead, or every managed build fails at seal after a
378
+ // full compile, sign, and notarize.
379
+ const targetPrefix = "src-tauri/target/";
380
+ const resolveArtifactPath = (file) => {
381
+ const normalized = file.replaceAll(path.win32.sep, "/");
382
+ if (managedCargoTarget && normalized.startsWith(targetPrefix)) {
383
+ return path.join(managedCargoTarget, normalized.slice(targetPrefix.length));
384
+ }
385
+ return path.resolve(configRoot, file);
386
+ };
351
387
  const sources = new Map();
352
388
  const addSource = (source, role) => {
353
389
  const current = sources.get(source) ?? new Set();
354
390
  current.add(role);
355
391
  sources.set(source, current);
356
392
  };
357
- for (const artifact of target.installer?.artifacts ?? []) addSource(path.resolve(configRoot, artifact.file), "installer");
393
+ for (const artifact of target.installer?.artifacts ?? []) addSource(resolveArtifactPath(artifact.file), "installer");
358
394
  for (const artifact of target.updater?.artifacts ?? []) {
359
- addSource(path.resolve(configRoot, artifact.file), "updater");
360
- addSource(path.resolve(configRoot, artifact.signature), "updater-signature");
395
+ addSource(resolveArtifactPath(artifact.file), "updater");
396
+ addSource(resolveArtifactPath(artifact.signature), "updater-signature");
361
397
  }
362
398
  for (const file of sources.keys()) if (!existsSync(file)) fail(`cannot seal missing artifact: ${file}`);
363
399
  const temp = `${sealedDir}.tmp-${process.pid}`;
@@ -374,14 +410,15 @@ function sealRelease({ configRoot, sealedDir, releaseId, config, target, platfor
374
410
  }
375
411
  const routes = { patch: [], update: [] };
376
412
  for (const artifact of target.installer?.artifacts ?? []) {
377
- const source = path.resolve(configRoot, artifact.file);
413
+ const source = resolveArtifactPath(artifact.file);
378
414
  routes.patch.push({ role: "installer", name: sourceNames.get(source), bucket: "public", key: artifact.key });
379
415
  }
380
416
  for (const artifact of target.updater?.artifacts ?? []) {
381
- const source = path.resolve(configRoot, artifact.file);
417
+ const source = resolveArtifactPath(artifact.file);
418
+ const signatureSource = resolveArtifactPath(artifact.signature);
382
419
  const patchKey = artifact.patchKey ?? artifact.key.replace("/updates/", "/installers/");
383
- routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(path.resolve(configRoot, artifact.signature)), bucket: "public", key: patchKey, platform: artifact.platform });
384
- routes.update.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(path.resolve(configRoot, artifact.signature)), bucket: "private", key: artifact.key, platform: artifact.platform });
420
+ routes.patch.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "public", key: patchKey, platform: artifact.platform });
421
+ routes.update.push({ role: "updater", name: sourceNames.get(source), signature: sourceNames.get(signatureSource), bucket: "private", key: artifact.key, platform: artifact.platform });
385
422
  }
386
423
  writeJson(path.join(temp, "hardening.json"), { schema: 1, passed: true, checkedAt: new Date().toISOString(), files: files.map((file) => file.sha256) });
387
424
  const manifest = {
@@ -427,26 +464,18 @@ function collectToolVersions(packageManager) {
427
464
  }
428
465
  }
429
466
 
430
- function resolveManagedCargoTarget(manifestPath) {
431
- if (!manifestPath) fail("managed release requires a Cargo.toml build input");
432
- const output = commandOutputAt(
433
- "cargo",
434
- ["metadata", "--no-deps", "--offline", "--format-version", "1", "--manifest-path", manifestPath],
435
- path.dirname(manifestPath),
436
- );
437
- const target = JSON.parse(output).target_directory;
438
- if (!target || !path.isAbsolute(target)) fail("managed Cargo metadata returned no absolute target directory");
439
- return target;
440
- }
441
-
442
467
  async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = null) {
443
- const inactivityMs = Number(process.env.RIGHT_RELEASE_STALL_MS ?? 10 * 60 * 1000);
444
- const absoluteMs = Number(process.env.RIGHT_RELEASE_ABSOLUTE_MS ?? 90 * 60 * 1000);
468
+ const { inactivityMs, absoluteMs } = releaseProgressLimits(process.env);
445
469
  let lastProgress = Date.now();
446
470
  const watchDirs = Array.isArray(watchDir) ? watchDir : [watchDir];
447
471
  let lastMtime = Math.max(...watchDirs.map(newestMtime));
448
472
  const started = Date.now();
449
473
  await new Promise((resolve, reject) => {
474
+ let settled = false;
475
+ let stopping = false;
476
+ let managedRequestId = null;
477
+ const settleResolve = () => { if (!settled) { settled = true; resolve(); } };
478
+ const settleReject = (error) => { if (!settled) { settled = true; reject(error); } };
450
479
  child = spawn(cmd, runArgs, { cwd, env, stdio: ["inherit", "pipe", "pipe"], windowsHide: true, shell: process.platform === "win32", detached: process.platform !== "win32" });
451
480
  try {
452
481
  if (child.pid && heavySlot) heavySlot.trackChild(child.pid, cmd);
@@ -465,6 +494,7 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
465
494
  stream.on("data", (chunk) => {
466
495
  lastProgress = Date.now();
467
496
  tail = `${tail}${chunk}`.slice(-64 * 1024);
497
+ if (!managedRequestId) managedRequestId = firstManagedRequestId(tail);
468
498
  output.write(chunk);
469
499
  });
470
500
  }
@@ -475,12 +505,24 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
475
505
  const cpu = processGroupCpuSeconds(child.pid);
476
506
  if (cpu !== null && lastCpu !== null && cpu > lastCpu) lastProgress = Date.now();
477
507
  if (cpu !== null) lastCpu = cpu;
478
- if (Date.now() - started > absoluteMs) stop(`absolute limit exceeded (${Math.round(absoluteMs / 60000)}m)`, cpu);
479
- else if (Date.now() - lastProgress > inactivityMs) stop(`no output, file, or CPU progress for ${Math.round(inactivityMs / 60000)}m`, cpu);
508
+ if (Date.now() - started > absoluteMs) void stop(`absolute limit exceeded (${Math.round(absoluteMs / 60000)}m)`, cpu);
509
+ else if (Date.now() - lastProgress > inactivityMs) void stop(`no output, file, or CPU progress for ${Math.round(inactivityMs / 60000)}m`, cpu);
480
510
  }, Math.min(30_000, Math.max(250, Math.floor(inactivityMs / 4))));
481
- const cleanup = () => { clearInterval(monitor); closeWatchers(); };
482
- const stop = (reason, cpu = null) => {
511
+ const cleanup = () => {
512
+ clearInterval(monitor);
513
+ closeWatchers();
514
+ if (requestActiveRunStop === stop) requestActiveRunStop = null;
515
+ };
516
+ const stop = async (reason, cpu = null) => {
517
+ if (stopping || settled) return;
518
+ stopping = true;
483
519
  cleanup();
520
+ let cancellation = { status: "NO_MANAGED_REQUEST" };
521
+ try {
522
+ cancellation = await cancelAndReapManagedRequest(managedRequestId, { cwd, env });
523
+ } catch (error) {
524
+ cancellation = { status: "CANCEL_REAP_FAILED", error: error.message };
525
+ }
484
526
  if (diagnosticPath) {
485
527
  try {
486
528
  mkdirSync(path.dirname(diagnosticPath), { recursive: true });
@@ -491,6 +533,8 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
491
533
  `watched: ${watchDirs.join(path.delimiter)}`,
492
534
  `elapsedMs: ${Date.now() - started}`,
493
535
  `cpuSeconds: ${cpu ?? "unavailable"}`,
536
+ `managedRequestId: ${managedRequestId ?? "none"}`,
537
+ `managedCancellation: ${JSON.stringify(cancellation)}`,
494
538
  `killedAt: ${new Date().toISOString()}`,
495
539
  "",
496
540
  "--- last output ---",
@@ -500,11 +544,13 @@ async function runProgress(cmd, runArgs, cwd, env, watchDir, diagnosticPath = nu
500
544
  console.error(`right-release: stall diagnostic written to ${diagnosticPath}`);
501
545
  } catch { /* diagnostics are best effort; never mask the stall itself */ }
502
546
  }
503
- killTree(child.pid);
504
- reject(new Error(`release step stalled: ${reason}`));
547
+ if (child?.pid) killTree(child.pid);
548
+ child = null;
549
+ settleReject(new Error(`release step stalled: ${reason}`));
505
550
  };
506
- child.once("error", (error) => { cleanup(); reject(error); });
507
- child.once("exit", (code) => { cleanup(); child = null; code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)); });
551
+ requestActiveRunStop = stop;
552
+ child.once("error", (error) => { cleanup(); child = null; if (!stopping) settleReject(error); });
553
+ child.once("exit", (code) => { cleanup(); child = null; if (!stopping) code === 0 ? settleResolve() : settleReject(new Error(`${cmd} exited ${code}`)); });
508
554
  });
509
555
  }
510
556
 
@@ -6,7 +6,7 @@ import { execFileSync, spawnSync } from "node:child_process";
6
6
  import test from "node:test";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { parseCpuSeconds, processGroupCpuSeconds } from "./process-liveness.mjs";
9
- import { managedCargoShimOnPath } from "./cargo-contract.mjs";
9
+ import { brokerManagedHost } from "./cargo-contract.mjs";
10
10
 
11
11
  const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
12
12
  const build = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs");
@@ -27,7 +27,7 @@ test("macOS and Windows builds default to Cache V2 while explicit legacy mode re
27
27
  assert.match(source, /right-release build/);
28
28
  assert.match(source, /RIGHT_RELEASE_CACHE_MODE/);
29
29
  assert.match(source, /platform === "mac" \|\| platform === "win" \? "shared" : "legacy"/);
30
- assert.match(source, /cacheMode !== "legacy" && cacheMode !== "shared"/);
30
+ assert.match(source, /cacheModeRequested !== "legacy" && cacheModeRequested !== "shared"/);
31
31
  assert.match(source, /resolveSharedCacheRoot/);
32
32
  assert.match(source, /acquireSuiteBuildSlot/);
33
33
  assert.match(source, /acquireHeavyWorkSlot/);
@@ -54,6 +54,24 @@ test("production builds package from the real primary app checkout", () => {
54
54
  assert.match(source, /sealRelease\(\{ configRoot: appRoot,/);
55
55
  });
56
56
 
57
+ test("sealing resolves installer and updater artifacts against the broker target on a managed host", () => {
58
+ assert.match(source, /sealRelease\(\{ configRoot: appRoot, managedCargoTarget,/);
59
+ assert.match(source, /function sealRelease\(\{ configRoot, managedCargoTarget,/);
60
+ assert.match(source, /const targetPrefix = "src-tauri\/target\/";/);
61
+ assert.match(source, /if \(managedCargoTarget && normalized\.startsWith\(targetPrefix\)\) \{/);
62
+ // Every configRoot-relative artifact lookup inside sealRelease must route
63
+ // through the resolver, or a managed build fails at seal after a full
64
+ // compile, sign, and notarize because src-tauri/target is never populated
65
+ // locally on a broker-managed host (target-bridge.mjs leaves it alone).
66
+ assert.doesNotMatch(source, /addSource\(path\.resolve\(configRoot, artifact\.file\)/);
67
+ assert.doesNotMatch(source, /addSource\(path\.resolve\(configRoot, artifact\.signature\)/);
68
+ assert.match(source, /addSource\(resolveArtifactPath\(artifact\.file\), "installer"\)/);
69
+ assert.match(source, /addSource\(resolveArtifactPath\(artifact\.file\), "updater"\)/);
70
+ assert.match(source, /addSource\(resolveArtifactPath\(artifact\.signature\), "updater-signature"\)/);
71
+ assert.match(source, /const source = resolveArtifactPath\(artifact\.file\);/);
72
+ assert.match(source, /const signatureSource = resolveArtifactPath\(artifact\.signature\);/);
73
+ });
74
+
57
75
  test("production builds never create or use an internal Git worktree", () => {
58
76
  assert.doesNotMatch(source, /git", \["worktree", "add"/);
59
77
  assert.doesNotMatch(source, /path\.join\(vaultRoot, "worktrees"/);
@@ -74,7 +92,30 @@ test("shared Cache V2 target bridge is owned by a finally-cleaned lifecycle", ()
74
92
  // next build until someone deletes the link by hand.
75
93
  assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\)/);
76
94
  assert.match(source, /targetBridge\.run\(.*runBuildStateMachine/s);
77
- assert.match(source, /if \(cacheMode === "shared"\) targetBridge\.ensure\(\)/);
95
+ // A broker-managed host also routes through the bridge even though cacheMode
96
+ // is forced to "broker" there — its brokerManaged flag is what keeps a real
97
+ // (non-symlink) target directory from being treated as corruption.
98
+ assert.match(source, /if \(cacheMode === "shared" \|\| managedCargoTarget\) targetBridge\.ensure\(\)/);
99
+ });
100
+
101
+ test("a broker-managed host forces broker cacheMode so Cache V2's lease/prune/floor chain never runs against an unrelated volume", () => {
102
+ assert.match(
103
+ source,
104
+ /const cacheMode = managedCargoTarget \? "broker" : cacheModeRequested;/,
105
+ );
106
+ // Every Cache V2 lease/slot/prune/floor/mark call stays gated on cacheMode,
107
+ // never on cacheModeRequested directly, so forcing cacheMode to "broker" on a
108
+ // broker host is sufficient to skip all of them without a second gate.
109
+ for (const guarded of [
110
+ /acquireSuiteBuildSlot\(\{ layout: sharedLayout/,
111
+ /cacheLease = acquireCacheLease\(\{ layout: sharedLayout/,
112
+ /planCachePrune\(\{ snapshot, policy, protectedEntryIds: new Set\(\[sharedLayout\.id\]\) \}\)/,
113
+ /assertWriteVolumeFloors\(\{ volumes, policy,/,
114
+ /markCacheEntrySuccessful\(\{ layout: sharedLayout \}\)/,
115
+ ]) {
116
+ assert.match(source, guarded);
117
+ }
118
+ assert.doesNotMatch(source, /if \(cacheModeRequested === "shared"\) \{\s*\n\s*suiteSlot/);
78
119
  });
79
120
 
80
121
  test("Windows seal and cache identity bind the signing contract, config, and receipts", () => {
@@ -122,16 +163,17 @@ test("CPU sampling degrades to the old behaviour rather than inventing a verdict
122
163
  // bridged src-tauri/target into the release cache, and then lost CARGO_TARGET_DIR
123
164
  // to the broker's unconditional override. The app built into the broker workspace
124
165
  // and packaging failed on an empty cache before notarize/staple.
125
- const managedHostDetected = (env, platform, exists) =>
126
- Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET || managedCargoShimOnPath(env, platform, exists));
166
+ const managedHostDetected = (env, platform, exists) => brokerManagedHost(env, platform, exists);
127
167
 
128
168
  test("managed-host gate fires from the managed shim on PATH when no broker socket is exported", () => {
129
- // The build gate is exactly the dual detection cargo-contract.mjs already uses.
169
+ // The build gate is exactly the shared brokerManagedHost predicate cargo-contract.mjs exports,
170
+ // so there is exactly one definition of "is this a broker-managed host" in the codebase.
130
171
  assert.match(
131
172
  source,
132
- /const managedCargoTarget = \(process\.env\.RIGHTKIT_BUILD_BROKER_SOCKET \|\| managedCargoShimOnPath\(process\.env\)\)\n\s*\? resolveManagedCargoTarget\(cargoToml\)\n\s*: null;/,
173
+ /const managedCargoTarget = brokerManagedHost\(process\.env\) \? resolveTargetRoot\(cargoToml\) : null;/,
133
174
  );
134
- assert.match(source, /import \{ managedCargoShimOnPath \} from "\.\/cargo-contract\.mjs";/);
175
+ assert.match(source, /import \{ brokerManagedHost \} from "\.\/cargo-contract\.mjs";/);
176
+ assert.match(source, /import \{ resolveTargetRoot \} from "\.\/cargo-target\.mjs";/);
135
177
 
136
178
  const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
137
179
  const exists = (candidate) => candidate === shim;
@@ -151,6 +193,23 @@ test("managed-host gate fires from the managed shim on PATH when no broker socke
151
193
  assert.equal(managedHostDetected({ PATH: `/usr/local/bin:${path.dirname(shim)}` }, "darwin", (c) => both.has(c)), false);
152
194
  });
153
195
 
196
+ test("brokerManagedHost is the single shared predicate: env var alone, shim alone, both, or neither", () => {
197
+ const shim = "/Volumes/D/.rightkit-managed/agent-bin/cargo";
198
+ const exists = (candidate) => candidate === shim;
199
+
200
+ // Broker socket alone (login shell), no shim resolvable.
201
+ assert.equal(brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: "/usr/bin" }, "darwin", () => false), true);
202
+ // Shim on PATH alone (agent/non-login shell), no socket exported.
203
+ assert.equal(brokerManagedHost({ PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists), true);
204
+ // Both signals present.
205
+ assert.equal(
206
+ brokerManagedHost({ RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock", PATH: `${path.dirname(shim)}:/usr/bin` }, "darwin", exists),
207
+ true,
208
+ );
209
+ // Neither signal: unmanaged host.
210
+ assert.equal(brokerManagedHost({ PATH: "/usr/bin:/usr/local/bin" }, "darwin", () => false), false);
211
+ });
212
+
154
213
  test("the managed branch hands the Rust target directory to the broker while the unmanaged branch keeps Cache V2", () => {
155
214
  // Managed: CARGO_TARGET_DIR (and the rest of the Cache V2 environment) is deleted
156
215
  // from the child environment, so the broker's own override is the only one.
@@ -164,7 +223,9 @@ test("the managed branch hands the Rust target directory to the broker while the
164
223
  assert.match(source, /ownedRoot: path\.dirname\(managedCargoTarget \?\? env\.CARGO_TARGET_DIR\),/);
165
224
  // The managed target is read from crate metadata through the shim, which is what
166
225
  // makes the bridge target equal to the directory the broker actually builds into.
167
- assert.match(source, /function resolveManagedCargoTarget\(manifestPath\)[\s\S]*?JSON\.parse\(output\)\.target_directory/);
226
+ // resolveTargetRoot (cargo-target.mjs) is the single shared implementation —
227
+ // build-release.mjs no longer defines its own copy.
228
+ assert.doesNotMatch(source, /function resolveManagedCargoTarget/);
168
229
  // Unmanaged (managedCargoTarget === null): Cache V2 still supplies CARGO_TARGET_DIR.
169
230
  assert.match(source, /\.\.\.releaseEnvironment\(\{ root: vaultRoot, cacheRoot: sharedCacheRoot,/);
170
231
  });
package/cache-command.mjs CHANGED
@@ -13,12 +13,28 @@ import {
13
13
  resolveSharedCacheRoot,
14
14
  } from "./cache-policy.mjs";
15
15
  import { resolveReleaseLayout } from "./release-state.mjs";
16
+ import { brokerManagedHost } from "./cargo-contract.mjs";
17
+
18
+ // On a broker-managed host, CARGO_TARGET_DIR is not Cache V2's to own: the build
19
+ // broker unconditionally overrides it to its own per-namespace workspace, so the
20
+ // Cache V2 target layout under cacheRoot is legitimately empty (marker files only)
21
+ // while the real ~18 GB of build output lives in the broker's workspace. Inspecting,
22
+ // migrating, or pruning against that layout on a broker host reads real output as
23
+ // missing/corrupt and can recommend destroying it — never phrase this as a problem.
24
+ const BROKER_MANAGED_MESSAGE = "broker-managed host: the build broker owns CARGO_TARGET_DIR; Cache V2 target checks skipped";
25
+ const BROKER_GATED_COMMANDS = new Set(["status", "prune", "migrate"]);
16
26
 
17
27
  export async function runCacheCommand(args = process.argv.slice(2), { env = process.env, stdout = console.log, stderr = console.error } = {}) {
18
28
  const [command, ...rest] = args;
19
29
  const json = rest.includes("--json");
20
30
  const platform = platformFrom(env);
21
31
  const cacheRoot = resolveSharedCacheRoot({ platform, env });
32
+ if (BROKER_GATED_COMMANDS.has(command) && brokerManagedHost(env)) {
33
+ const result = { schema: 1, command, cacheRoot, brokerManaged: true, skipped: true, message: BROKER_MANAGED_MESSAGE };
34
+ if (json) stdout(JSON.stringify(result));
35
+ else stdout(BROKER_MANAGED_MESSAGE);
36
+ return result;
37
+ }
22
38
  if (command === "status") {
23
39
  rejectUnknown(rest, new Set(["--json"]));
24
40
  const snapshot = inspectCache({ cacheRoot });
@@ -9,8 +9,24 @@ import { fileURLToPath } from "node:url";
9
9
  const root = path.dirname(fileURLToPath(import.meta.url));
10
10
  const cli = path.join(root, "cli", "right-release.mjs");
11
11
 
12
+ // This suite must exercise the non-broker cache-command path deterministically,
13
+ // regardless of whether the machine running the tests is itself broker-managed
14
+ // (this workspace's own agent shells are: the managed cargo shim sits on PATH).
15
+ // Strip both broker signals from the inherited environment so "unmanaged host"
16
+ // tests stay unmanaged; individual tests opt back into broker signals explicitly.
17
+ function unmanagedEnv(base = process.env) {
18
+ const sanitized = { ...base };
19
+ delete sanitized.RIGHTKIT_BUILD_BROKER_SOCKET;
20
+ const delimiter = process.platform === "win32" ? ";" : ":";
21
+ sanitized.PATH = String(base.PATH ?? "")
22
+ .split(delimiter)
23
+ .filter((entry) => !/(?:^|[/\\])(?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin$/i.test(entry))
24
+ .join(delimiter);
25
+ return sanitized;
26
+ }
27
+
12
28
  function run(args, cacheRoot, env = {}) {
13
- return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_CACHE_ROOT: cacheRoot, ...env } });
29
+ return spawnSync(process.execPath, [cli, "cache", ...args], { encoding: "utf8", env: { ...unmanagedEnv(), RIGHT_RELEASE_CACHE_ROOT: cacheRoot, ...env } });
14
30
  }
15
31
 
16
32
  test("cache status has human and schema-stable JSON output", () => {
@@ -73,3 +89,30 @@ test("cache migrate uses the repository vault Cargo home, not an app-local looka
73
89
  assert.equal(readFileSync(path.join(cacheRoot, "cargo-home", "registry"), "utf8"), "vault");
74
90
  assert.equal(readFileSync(path.join(appCargoHome, "registry"), "utf8"), "app-local");
75
91
  });
92
+
93
+ test("status, prune, and migrate short-circuit cleanly on a broker-managed host without proposing a repair", () => {
94
+ const cacheRoot = mkdtempSync(path.join(os.tmpdir(), "rightkit-cache-command-"));
95
+ const brokerEnv = { RIGHTKIT_BUILD_BROKER_SOCKET: "/managed/broker.sock" };
96
+
97
+ const status = run(["status", "--json"], cacheRoot, brokerEnv);
98
+ assert.equal(status.status, 0, status.stderr);
99
+ const statusBody = JSON.parse(status.stdout);
100
+ assert.equal(statusBody.brokerManaged, true);
101
+ assert.match(statusBody.message, /broker-managed host/);
102
+ assert.doesNotMatch(status.stdout + status.stderr, /warn|repair|corrupt/i);
103
+
104
+ const prune = run(["prune", "--dry-run", "--json"], cacheRoot, brokerEnv);
105
+ assert.equal(prune.status, 0, prune.stderr);
106
+ assert.equal(JSON.parse(prune.stdout).brokerManaged, true);
107
+
108
+ // migrate short-circuits before even requiring --config, since a broker host has
109
+ // nothing under Cache V2's target layout for it to migrate into.
110
+ const migrate = run(["migrate", "--json"], cacheRoot, brokerEnv);
111
+ assert.equal(migrate.status, 0, migrate.stderr);
112
+ assert.equal(JSON.parse(migrate.stdout).brokerManaged, true);
113
+
114
+ // Non-broker host: unchanged behaviour.
115
+ const unmanaged = run(["status", "--json"], cacheRoot);
116
+ assert.equal(unmanaged.status, 0, unmanaged.stderr);
117
+ assert.equal(JSON.parse(unmanaged.stdout).brokerManaged, undefined);
118
+ });
package/cache-policy.mjs CHANGED
@@ -30,21 +30,59 @@ export const CACHE_LOCK_ORDER = ["release", "suite-build-slot", "gc", "entry-lea
30
30
 
31
31
  const MARKER = ".rightkit-cache-entry.json";
32
32
 
33
- export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir(), xdgCacheHome } = {}) {
34
- const override = env.RIGHT_RELEASE_CACHE_ROOT;
35
- if (override) {
36
- if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
37
- return resolveForPlatform(override, platform);
38
- }
33
+ export function resolveSharedCacheRoot({ platform = process.platform, env = process.env, home = os.homedir() } = {}) {
39
34
  if (platform === "darwin" || platform === "mac") {
35
+ const override = env.RIGHT_RELEASE_CACHE_ROOT;
36
+ if (override) {
37
+ if (!isAbsoluteForPlatform(override, platform)) throw new Error("RIGHT_RELEASE_CACHE_ROOT must be an absolute path");
38
+ return resolveForPlatform(override, platform);
39
+ }
40
40
  throw new Error("RIGHT_RELEASE_CACHE_ROOT is required on macOS; set it to external storage");
41
41
  }
42
+ return resolveStableRoot({
43
+ platform,
44
+ env,
45
+ home,
46
+ explicitOverride: env.RIGHT_RELEASE_CACHE_ROOT,
47
+ overrideName: "RIGHT_RELEASE_CACHE_ROOT",
48
+ win32EnvVar: "LOCALAPPDATA",
49
+ win32Segments: ["RightSuite", "Cache", "release"],
50
+ otherSegments: ["rightsuite", "release"],
51
+ });
52
+ }
53
+
54
+ // resolveStableRoot centralizes platform-native root derivation for both cache roots
55
+ // (waste-tolerant, may split) and lock roots (must never split, or mutual exclusion
56
+ // silently breaks). Route every such root through this one primitive.
57
+ export function resolveStableRoot({
58
+ platform = process.platform,
59
+ env = process.env,
60
+ home = os.homedir(),
61
+ explicitOverride,
62
+ overrideName,
63
+ darwinSegments,
64
+ win32EnvVar = "LOCALAPPDATA",
65
+ win32Segments,
66
+ otherSegments,
67
+ otherBaseSegments = [".cache"],
68
+ } = {}) {
69
+ if (explicitOverride) {
70
+ if (!isAbsoluteForPlatform(explicitOverride, platform)) throw new Error(`${overrideName} must be an absolute path`);
71
+ return resolveForPlatform(explicitOverride, platform);
72
+ }
42
73
  if (platform === "win32" || platform === "win") {
43
- const local = env.LOCALAPPDATA;
44
- if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error("LOCALAPPDATA must be an absolute path for the RightSuite cache");
45
- return path.win32.resolve(local, "RightSuite", "Cache", "release");
74
+ const local = env[win32EnvVar];
75
+ if (!local || !isAbsoluteForPlatform(local, platform)) throw new Error(`${win32EnvVar} must be an absolute path for RightSuite`);
76
+ return path.win32.resolve(local, ...win32Segments);
77
+ }
78
+ if (platform === "darwin" || platform === "mac") {
79
+ return path.posix.join(home, ...darwinSegments);
46
80
  }
47
- return path.posix.resolve(xdgCacheHome || env.XDG_CACHE_HOME || path.posix.join(home, ".cache"), "rightsuite", "release");
81
+ // Never read a shell-profile-scoped variable (XDG_CACHE_HOME, XDG_CONFIG_HOME,
82
+ // npm_config_cache, etc.) as an implicit default here: those differ between login
83
+ // and non-login shells, so the same process can resolve to two different roots
84
+ // depending on how it was launched. Always derive from the OS home directory instead.
85
+ return path.posix.resolve(path.posix.join(home, ...otherBaseSegments), ...otherSegments);
48
86
  }
49
87
 
50
88
  export function resolveCacheLayout({ cacheRoot, platform, architecture, app, fingerprint, kind = "release" } = {}) {