@stim-cli/core 1.20.2 → 1.22.0

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 (34) hide show
  1. package/README.md +2 -0
  2. package/dist/{core-BkgfKqJ8.mjs → core-BTsCO1nW.mjs} +444 -60
  3. package/dist/{index-VDWBflH3.d.mts → index-DKq1j8dw.d.mts} +340 -91
  4. package/dist/index.d.mts +101 -5
  5. package/dist/index.mjs +3 -3
  6. package/dist/keyed-delta-BBoOeR2J.mjs +289 -0
  7. package/dist/noise-DYIbymAJ.d.mts +100 -0
  8. package/dist/noise-session.d.mts +164 -0
  9. package/dist/noise-session.mjs +402 -0
  10. package/dist/noise.d.mts +2 -0
  11. package/dist/noise.mjs +393 -0
  12. package/dist/oversight.mjs +16 -6
  13. package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
  14. package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-RY3fbm1D.d.mts} +3 -2
  15. package/dist/ownership-claim.d.mts +2 -2
  16. package/dist/ownership-claim.mjs +2 -2
  17. package/dist/phone-protocol.d.mts +85 -16
  18. package/dist/phone-protocol.mjs +2 -2
  19. package/dist/{process-identity-BTyww2Xj.d.mts → process-identity-DVNGpF5c.d.mts} +1 -1
  20. package/dist/process-identity.d.mts +1 -1
  21. package/dist/process-identity.mjs +3 -1
  22. package/dist/{protocol-pH632kA3.d.mts → protocol-BjJt4ghB.d.mts} +632 -65
  23. package/dist/protocol.d.mts +2 -2
  24. package/dist/protocol.mjs +81 -10
  25. package/dist/receive-protocol.d.mts +1 -1
  26. package/dist/receive-protocol.mjs +20163 -14469
  27. package/dist/state.d.mts +2 -2
  28. package/dist/state.mjs +1261 -208
  29. package/dist/toolchain.d.mts +64 -0
  30. package/dist/toolchain.mjs +326 -0
  31. package/dist/workspace-link.d.mts +16 -0
  32. package/dist/workspace-link.mjs +50 -0
  33. package/package.json +21 -1
  34. package/dist/status-derived-C13g6eQt.mjs +0 -151
package/README.md CHANGED
@@ -42,6 +42,8 @@ strict parser and reader.
42
42
  shared by stim-server and the phone. It imports no Node APIs; mobile consumers
43
43
  import this subpath without loading the Node-only root or state entry points.
44
44
  Build core before typechecking, testing or bundling the phone app.
45
+ Its `wakeAt` includes physical-device lease expiry and unattended EAS-session age;
46
+ callers retain their clock and quiet-hours reevaluation.
45
47
 
46
48
  The npm scope remains `@stim-cli` until the `@stim` scope is available.
47
49
 
@@ -1,9 +1,11 @@
1
- import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
1
+ import { C as quotedPath, S as tryAcquireClaim, _ as releaseClaim, r as ClaimRefusedError, x as sleepSync } from "./ownership-claim-DHUs_Dir.mjs";
2
2
  import fs, { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
3
3
  import os from "node:os";
4
- import path, { basename, dirname, join } from "node:path";
5
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
4
+ import path, { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
5
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
6
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
6
7
  import { AsyncLocalStorage } from "node:async_hooks";
8
+ import { mkdir, rm } from "node:fs/promises";
7
9
  import { spawnSync } from "node:child_process";
8
10
  import { connect, createServer, isIP } from "node:net";
9
11
  //#region dir-lock.ts
@@ -11,9 +13,6 @@ const DEFAULT_LOCK_WAIT_MS = 12e3;
11
13
  const DEFAULT_LOCK_POLL_MS = 25;
12
14
  const lockDepths = /* @__PURE__ */ new Map();
13
15
  const OWNER_MARKER = /^\.stim-claim-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/;
14
- function sleepSync(ms) {
15
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
16
- }
17
16
  /**
18
17
  * Whether Windows refused a call on the lock directory because another process is removing it at that
19
18
  * moment. NTFS answers ERROR_ACCESS_DENIED, which Node reports as EPERM, until that removal completes,
@@ -90,57 +89,42 @@ function takeLockDirectory(lockPath, claim) {
90
89
  }
91
90
  return marker;
92
91
  }
93
- function acquireDirLock(lockPath, { waitMs, pollMs, ensureParent }) {
92
+ function prepareDirLock(lockPath, ensureParent) {
94
93
  ensureParent?.();
95
94
  statSync(dirname(lockPath));
96
- const deadline = Date.now() + waitMs;
97
- let claim;
98
- try {
99
- for (;;) {
100
- if (!claim) {
101
- const attempt = tryAcquireClaim({
102
- root: `${lockPath}.claims`,
103
- mode: "exclusive",
104
- label: "directory lock"
105
- });
106
- claim = attempt.acquired;
107
- if (attempt.pending) releaseClaim(attempt.pending);
108
- }
109
- if (claim) {
110
- const marker = takeLockDirectory(lockPath, claim);
111
- if (marker) return {
112
- claim,
113
- marker
114
- };
115
- }
116
- if (Date.now() >= deadline) {
117
- const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. ` + (claim ? `No current Stim holds it: the directory is empty, or an older Stim version holds or left it. If no older Stim is running, remove it and run the command again:\n rm -rf ${quotedPath(lockPath)}` : "Another Stim process is holding it; wait for that command to finish and run the command again."));
118
- error.code = "STIM_LOCK_TIMEOUT";
119
- error.lockPath = lockPath;
120
- throw error;
121
- }
122
- sleepSync(pollMs);
123
- }
124
- } catch (error) {
125
- releaseClaim(claim);
126
- throw error;
95
+ }
96
+ function attemptDirLock(lockPath, pending) {
97
+ if (!pending.claim) {
98
+ const attempt = tryAcquireClaim({
99
+ root: `${lockPath}.claims`,
100
+ mode: "exclusive",
101
+ label: "directory lock"
102
+ });
103
+ pending.claim = attempt.acquired;
104
+ if (attempt.pending) releaseClaim(attempt.pending);
127
105
  }
106
+ if (!pending.claim) return null;
107
+ const marker = takeLockDirectory(lockPath, pending.claim);
108
+ return marker ? {
109
+ claim: pending.claim,
110
+ marker
111
+ } : null;
128
112
  }
129
- function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent } = {}) {
130
- const depth = lockDepths.get(lockPath) || 0;
131
- if (depth > 0) {
132
- lockDepths.set(lockPath, depth + 1);
133
- try {
134
- return fn();
135
- } finally {
136
- lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 1) - 1);
137
- }
113
+ function dirLockTimeout(lockPath, claim) {
114
+ const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. ` + (claim ? `No current Stim holds it: the directory is empty, or an older Stim version holds or left it. If no older Stim is running, remove it and run the command again:\n rm -rf ${quotedPath(lockPath)}` : "Another Stim process is holding it; wait for that command to finish and run the command again."));
115
+ error.code = "STIM_LOCK_TIMEOUT";
116
+ error.lockPath = lockPath;
117
+ return error;
118
+ }
119
+ function nestedDirLock(lockPath, fn) {
120
+ lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 0) + 1);
121
+ try {
122
+ return fn();
123
+ } finally {
124
+ lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 1) - 1);
138
125
  }
139
- const { claim, marker } = acquireDirLock(lockPath, {
140
- waitMs,
141
- pollMs,
142
- ensureParent
143
- });
126
+ }
127
+ function holdDirLock(lockPath, { claim, marker }, fn) {
144
128
  lockDepths.set(lockPath, 1);
145
129
  try {
146
130
  return fn();
@@ -152,6 +136,73 @@ function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEF
152
136
  releaseClaim(claim);
153
137
  }
154
138
  }
139
+ function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent } = {}) {
140
+ if ((lockDepths.get(lockPath) || 0) > 0) return nestedDirLock(lockPath, fn);
141
+ prepareDirLock(lockPath, ensureParent);
142
+ const deadline = Date.now() + waitMs;
143
+ const pending = {};
144
+ let held;
145
+ try {
146
+ while (!(held = attemptDirLock(lockPath, pending))) {
147
+ if (Date.now() >= deadline) throw dirLockTimeout(lockPath, pending.claim);
148
+ sleepSync(pollMs);
149
+ }
150
+ } catch (error) {
151
+ releaseClaim(pending.claim);
152
+ throw error;
153
+ }
154
+ return holdDirLock(lockPath, held, fn);
155
+ }
156
+ /**
157
+ * `withDirLock` that waits for a contended lock on a timer instead of blocking the thread. `fn` still runs
158
+ * synchronously: nothing awaits between taking the lock, running `fn` and releasing the lock, so the held
159
+ * transaction and reentrant nesting behave exactly as with `withDirLock`.
160
+ * An optional signal cancels acquisition; it cannot interrupt the synchronous transaction.
161
+ */
162
+ async function withDirLockAsync(lockPath, fn, options = {}) {
163
+ options.signal?.throwIfAborted();
164
+ if ((lockDepths.get(lockPath) || 0) > 0) return nestedDirLock(lockPath, fn);
165
+ return acquireDirLockAsync(lockPath, options, (held) => holdDirLock(lockPath, held, () => {
166
+ options.signal?.throwIfAborted();
167
+ return fn();
168
+ }));
169
+ }
170
+ async function acquireDirLockAsync(lockPath, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent, signal }, fn) {
171
+ signal?.throwIfAborted();
172
+ prepareDirLock(lockPath, ensureParent);
173
+ const deadline = Date.now() + waitMs;
174
+ const pending = {};
175
+ let held;
176
+ try {
177
+ while (!(held = attemptDirLock(lockPath, pending))) {
178
+ if (Date.now() >= deadline) throw dirLockTimeout(lockPath, pending.claim);
179
+ releaseClaim(pending.claim);
180
+ pending.claim = void 0;
181
+ await setTimeout$1(pollMs, void 0, { signal });
182
+ }
183
+ } catch (error) {
184
+ releaseClaim(pending.claim);
185
+ throw error;
186
+ }
187
+ return fn(held);
188
+ }
189
+ /**
190
+ * Holds the directory lock and its ownership claim until the body settles. Transactions are exclusive even
191
+ * within one process and are not reentrant. Cancellation stops acquisition; the body owns cancellation after admission.
192
+ */
193
+ async function withDirLockTransaction(lockPath, fn, options = {}) {
194
+ return acquireDirLockAsync(lockPath, options, async ({ claim, marker }) => {
195
+ try {
196
+ options.signal?.throwIfAborted();
197
+ return await fn();
198
+ } finally {
199
+ try {
200
+ removeLockDirectory(lockPath, marker);
201
+ } catch {}
202
+ releaseClaim(claim);
203
+ }
204
+ });
205
+ }
155
206
  //#endregion
156
207
  //#region state/json-file.ts
157
208
  const readScope = new AsyncLocalStorage();
@@ -195,12 +246,94 @@ function readJsonObject(path) {
195
246
  }
196
247
  }
197
248
  //#endregion
249
+ //#region context.ts
250
+ const ambient = new AsyncLocalStorage();
251
+ /**
252
+ * Runs `fn` with `context` reachable through `ambientContext()`, including after an `await` inside it. A temporary
253
+ * bridge for call sites that are not passed a context yet: `configDir()`, `runId()` and the exec wrapper's base
254
+ * environment.
255
+ */
256
+ function withAmbientContext(context, fn) {
257
+ return ambient.run(context, fn);
258
+ }
259
+ /** The context set by `withAmbientContext`, for call sites that are not passed one yet. */
260
+ function ambientContext() {
261
+ return ambient.getStore();
262
+ }
263
+ /** Writes `text` to the ambient context's output, or to the process stream outside one. */
264
+ function ambientWrite(stream, text) {
265
+ const context = ambient.getStore();
266
+ if (context) context.output(stream, text);
267
+ else process[stream].write(text);
268
+ }
269
+ /** The environment of the ambient context, or `process.env` outside one. */
270
+ function ambientEnv() {
271
+ return ambient.getStore()?.env ?? process.env;
272
+ }
273
+ //#endregion
198
274
  //#region macos-resources.ts
199
275
  /** Whether a resource destination is a contained, non-empty path under Contents/Resources. */
200
276
  function validMacosResourceDestination(value) {
201
277
  return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0") && !value.includes("\\") && !/^[A-Za-z]:/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
202
278
  }
203
279
  //#endregion
280
+ //#region systemd-unit.ts
281
+ const ESCAPES = {
282
+ "\\": "\\\\",
283
+ "\"": "\\\"",
284
+ "\n": "\\n",
285
+ " ": "\\t",
286
+ "\r": "\\r"
287
+ };
288
+ const UNESCAPES = {
289
+ n: "\n",
290
+ t: " ",
291
+ r: "\r"
292
+ };
293
+ /**
294
+ * One double-quoted word as systemd.syntax(7) reads it: C escapes inside the quotes, `%` doubled because unit files
295
+ * expand specifiers, and in `ExecStart=` `$` doubled because systemd expands variables in command lines.
296
+ */
297
+ function quoteUnitWord(value, exec) {
298
+ return `"${value.replace(/[\\"\n\t\r%$]/g, (char) => char === "%" ? "%%" : char === "$" ? exec ? "$$" : "$" : ESCAPES[char] ?? char)}"`;
299
+ }
300
+ /** The words of a unit value `quoteUnitWord` wrote, or null when the value is not in that form. */
301
+ function unitWords(line, exec) {
302
+ const out = [];
303
+ const pattern = /\s*(?:"((?:[^"\\]|\\.)*)"|([^\s"]+))/y;
304
+ let index = 0;
305
+ while (index < line.length && line.slice(index).trim()) {
306
+ pattern.lastIndex = index;
307
+ const match = pattern.exec(line);
308
+ if (!match) return null;
309
+ index = pattern.lastIndex;
310
+ const raw = match[1] ?? match[2] ?? "";
311
+ out.push(raw.replace(/\\(.)|%%|\$\$/g, (whole, escaped) => escaped !== void 0 ? UNESCAPES[escaped] ?? escaped : whole === "%%" ? "%" : exec ? "$" : whole));
312
+ }
313
+ return out;
314
+ }
315
+ /** The `key=value` lines of a unit file with the section each is in, skipping comments and blank lines. */
316
+ function unitLines(text) {
317
+ let section = "";
318
+ const out = [];
319
+ for (const line of text.split("\n")) {
320
+ const trimmed = line.trim();
321
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
322
+ const header = /^\[(.+)\]$/.exec(trimmed)?.[1];
323
+ if (header) {
324
+ section = header;
325
+ continue;
326
+ }
327
+ const equals = trimmed.indexOf("=");
328
+ if (equals > 0) out.push({
329
+ section,
330
+ key: trimmed.slice(0, equals).trim(),
331
+ value: trimmed.slice(equals + 1).trim()
332
+ });
333
+ }
334
+ return out;
335
+ }
336
+ //#endregion
204
337
  //#region artifact-store.ts
205
338
  function artifactIn(dir) {
206
339
  if (!existsSync(dir)) return null;
@@ -298,6 +431,68 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
298
431
  return artifactIn(dest);
299
432
  }
300
433
  //#endregion
434
+ //#region artifact-store-async.ts
435
+ /** Awaits copying before atomic publication; cancellation before publication preserves the previous entry. */
436
+ async function storeArtifactAsync(dest, buildPath, { runFile, overwrite = false, writeMetadata, onRenameError, signal }) {
437
+ signal?.throwIfAborted();
438
+ const existing = artifactIn(dest);
439
+ if (existing && !overwrite) return existing;
440
+ const token = randomUUID();
441
+ const staging = `${dest}.staging-${token}`;
442
+ const replaced = `${dest}.replaced-${token}`;
443
+ const target = join(staging, basename(buildPath));
444
+ await mkdir(staging, { recursive: true });
445
+ try {
446
+ signal?.throwIfAborted();
447
+ try {
448
+ await runFile("cp", [
449
+ "-c",
450
+ "-R",
451
+ buildPath,
452
+ target
453
+ ]);
454
+ } catch {
455
+ signal?.throwIfAborted();
456
+ await rm(target, {
457
+ recursive: true,
458
+ force: true
459
+ });
460
+ signal?.throwIfAborted();
461
+ await runFile("cp", [
462
+ "-R",
463
+ buildPath,
464
+ target
465
+ ]);
466
+ }
467
+ signal?.throwIfAborted();
468
+ await writeMetadata?.(staging);
469
+ signal?.throwIfAborted();
470
+ const published = artifactIn(dest);
471
+ if (published && !overwrite) return published;
472
+ mkdirSync(dirname(dest), { recursive: true });
473
+ if (existsSync(dest)) renameSync(dest, replaced);
474
+ try {
475
+ renameSync(staging, dest);
476
+ } catch (error) {
477
+ try {
478
+ renameSync(replaced, dest);
479
+ } catch {}
480
+ if (!onRenameError) throw error;
481
+ await onRenameError(staging);
482
+ }
483
+ return artifactIn(dest);
484
+ } finally {
485
+ await rm(staging, {
486
+ recursive: true,
487
+ force: true
488
+ });
489
+ await rm(replaced, {
490
+ recursive: true,
491
+ force: true
492
+ });
493
+ }
494
+ }
495
+ //#endregion
301
496
  //#region compiled-helper.ts
302
497
  const RECENT_MS = 6e5;
303
498
  const pruned = /* @__PURE__ */ new Set();
@@ -343,9 +538,9 @@ function pruneCompiledHelpers(dir, currentName, name, runningPaths = runningExec
343
538
  * Returns `<dir>/<name>-<hash>`, compiling it with `compile` first when it is missing. The hash covers `version` and
344
539
  * each input's name and bytes, so a changed source or compiler builds a new helper beside the old one. `compile`
345
540
  * writes to a private temporary path that is renamed into place, so concurrent builders never expose a partial file.
346
- * Pruning is best effort and keeps this helper, the newest previous build, helpers modified in the last ten minutes and running helpers. It runs after a build and on the first call per process.
541
+ * Pruning is best effort and keeps this helper, the newest previous build, helpers modified in the last ten minutes and running helpers. It runs after a build and on the first call per process unless `prune` is false.
347
542
  */
348
- async function compiledHelper({ dir, name, inputs, version, compile }) {
543
+ async function compiledHelper({ dir, name, inputs, version, compile, prune = true }) {
349
544
  const hash = createHash("sha256").update(version);
350
545
  for (const input of inputs) hash.update(basename(input)).update(readFileSync(input));
351
546
  const helper = join(dir, `${name}-${hash.digest("hex").slice(0, 16)}`);
@@ -363,11 +558,50 @@ async function compiledHelper({ dir, name, inputs, version, compile }) {
363
558
  rmSync(output, { force: true });
364
559
  }
365
560
  }
366
- if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
367
- pruned.add(`${dir}/${name}`);
561
+ if (prune) {
562
+ if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
563
+ pruned.add(`${dir}/${name}`);
564
+ }
368
565
  return helper;
369
566
  }
370
567
  //#endregion
568
+ //#region dependency-install.ts
569
+ /** The existing worker install command and marker identity for a repository's selected lockfile. */
570
+ function dependencyInstall(root, packageName) {
571
+ const file = [
572
+ "pnpm-lock.yaml",
573
+ "yarn.lock",
574
+ "package-lock.json"
575
+ ].find((name) => existsSync(join(root, name)));
576
+ if (!file) return null;
577
+ const command = file === "pnpm-lock.yaml" ? "pnpm" : file === "yarn.lock" ? "yarn" : "npm";
578
+ const args = command === "pnpm" ? [
579
+ "install",
580
+ "--frozen-lockfile",
581
+ ...packageName ? ["--filter", `${packageName}...`] : []
582
+ ] : command === "yarn" ? ["install", "--frozen-lockfile"] : ["ci"];
583
+ const bytes = readFileSync(join(root, file));
584
+ return {
585
+ command,
586
+ args,
587
+ lockfile: createHash("sha256").update(bytes).digest("hex"),
588
+ digest: createHash("sha256").update(bytes).update(JSON.stringify(args)).digest("hex")
589
+ };
590
+ }
591
+ /** Whether the worker can reuse this project's dependency install without running the package manager. */
592
+ function reusableDependencies(area, project, packageName, lockfile) {
593
+ try {
594
+ const root = realpathSync(join(area, "src"));
595
+ const selected = realpathSync(join(root, project));
596
+ const path = relative(root, selected);
597
+ if (path === ".." || path.startsWith(`..${sep}`) || isAbsolute(path)) return false;
598
+ const install = dependencyInstall(root, packageName);
599
+ return install?.lockfile === lockfile && existsSync(join(selected, "node_modules")) && readFileSync(join(area, "dependencies"), "utf8") === install.digest;
600
+ } catch {
601
+ return false;
602
+ }
603
+ }
604
+ //#endregion
371
605
  //#region metro-bridge.ts
372
606
  function port(value) {
373
607
  if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error("Invalid Metro bridge port.");
@@ -459,10 +693,160 @@ function createMetroBridge({ gatewayPort, peer, secret }) {
459
693
  });
460
694
  }
461
695
  //#endregion
696
+ //#region react-native-artifact.ts
697
+ const text$1 = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\0\r\n]/.test(value);
698
+ const path$2 = (value) => text$1(value) && !/[\\:]/.test(value) && value.split("/").every((part) => part !== "" && part !== ".." && part !== ".git");
699
+ /** Validates and canonicalizes the choices used by completed React Native peer artifacts. */
700
+ function reactNativeArtifactQuery(value) {
701
+ if (!isJsonObject(value) || typeof value.project !== "string" || value.project !== "" && !path$2(value.project) || !text$1(value.fingerprint) || typeof value.isExpo !== "boolean" || Object.keys(value).some((key) => ![
702
+ "project",
703
+ "fingerprint",
704
+ "isExpo",
705
+ "platform",
706
+ "toolchain",
707
+ "ios",
708
+ "android"
709
+ ].includes(key))) return null;
710
+ const tools = value.toolchain;
711
+ if (!isJsonObject(tools) || !text$1(tools.stimBuild)) return null;
712
+ const common = {
713
+ project: value.project,
714
+ fingerprint: value.fingerprint,
715
+ isExpo: value.isExpo
716
+ };
717
+ if (value.platform === "ios" && value.android === void 0 && isJsonObject(value.ios)) {
718
+ const { runtime, configuration, scheme, projectPath, optimizations } = value.ios;
719
+ if (!text$1(tools.arch) || !text$1(tools.xcode) || !text$1(tools.simulatorSdk) || !text$1(tools.cocoapods) || !text$1(runtime) || !text$1(configuration) || /release/i.test(configuration) || scheme !== null && !text$1(scheme) || !path$2(projectPath) || !isJsonObject(optimizations) || typeof optimizations.compilationCache !== "boolean" || optimizations.swiftCompilationCache !== null && typeof optimizations.swiftCompilationCache !== "boolean" || typeof optimizations.prefixMapping !== "boolean" || Object.keys(value.ios).some((key) => ![
720
+ "runtime",
721
+ "configuration",
722
+ "scheme",
723
+ "projectPath",
724
+ "optimizations"
725
+ ].includes(key)) || Object.keys(optimizations).some((key) => ![
726
+ "compilationCache",
727
+ "swiftCompilationCache",
728
+ "prefixMapping"
729
+ ].includes(key))) return null;
730
+ return {
731
+ ...common,
732
+ platform: "ios",
733
+ toolchain: {
734
+ stimBuild: tools.stimBuild,
735
+ arch: tools.arch,
736
+ xcode: tools.xcode,
737
+ simulatorSdk: tools.simulatorSdk,
738
+ cocoapods: tools.cocoapods
739
+ },
740
+ ios: {
741
+ runtime,
742
+ configuration,
743
+ scheme,
744
+ projectPath,
745
+ optimizations: {
746
+ compilationCache: optimizations.compilationCache,
747
+ swiftCompilationCache: optimizations.swiftCompilationCache ?? false,
748
+ prefixMapping: optimizations.prefixMapping
749
+ }
750
+ }
751
+ };
752
+ }
753
+ if (value.platform === "android" && value.ios === void 0 && isJsonObject(value.android)) {
754
+ const { variant, abi, gradleBuildCache, pch, compilerCache, gradleRoot = null, module = null } = value.android;
755
+ if (!text$1(tools.jdk) || variant !== null && (!text$1(variant) || !/^[A-Za-z0-9_-]+$/.test(variant) || /release/i.test(variant)) || abi !== null && (!text$1(abi) || !/^[A-Za-z0-9_-]+$/.test(abi)) || typeof gradleBuildCache !== "boolean" || compilerCache !== "ccache" && compilerCache !== "none" || gradleRoot !== null && !path$2(gradleRoot) || module !== null && (!text$1(module) || !/^(:(?!\.\.?(?::|$))[A-Za-z0-9._-]+){1,20}$/.test(module)) || Object.keys(value.android).some((key) => ![
756
+ "variant",
757
+ "abi",
758
+ "gradleBuildCache",
759
+ "pch",
760
+ "compilerCache",
761
+ "gradleRoot",
762
+ "module"
763
+ ].includes(key))) return null;
764
+ if (pch !== "auto" && pch !== "on" && pch !== "off") return null;
765
+ return {
766
+ ...common,
767
+ platform: "android",
768
+ toolchain: {
769
+ stimBuild: tools.stimBuild,
770
+ jdk: tools.jdk
771
+ },
772
+ android: {
773
+ variant: variant ?? "debug",
774
+ abi: abi === "all" ? null : abi,
775
+ gradleBuildCache,
776
+ pch,
777
+ compilerCache,
778
+ gradleRoot: gradleRoot ?? "android",
779
+ module: module ?? ":app"
780
+ }
781
+ };
782
+ }
783
+ return null;
784
+ }
785
+ /** The identity of a canonical React Native query and the toolchain verified around its build. */
786
+ function reactNativeArtifactIdentity(receipt) {
787
+ const fingerprint = createHash("sha256").update(JSON.stringify(receipt)).digest("hex");
788
+ return {
789
+ fingerprint,
790
+ cacheKey: `react-native-${receipt.query.platform}-${fingerprint}`
791
+ };
792
+ }
793
+ //#endregion
794
+ //#region swiftpm-artifact.ts
795
+ const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0");
796
+ const path$1 = (value) => text(value) && !/[\\:]/.test(value) && value.split("/").every((part) => part !== "" && part !== "." && part !== ".." && part.toLowerCase() !== ".git");
797
+ const strings = (value, valid) => Array.isArray(value) && value.length <= 1e3 && value.every(valid);
798
+ const normalized = (values) => [...new Set(values)].toSorted();
799
+ /** Validates and canonicalizes the source and build choices of a completed SwiftPM peer artifact. */
800
+ function swiftpmArtifactQuery(value) {
801
+ if (!isJsonObject(value) || Object.keys(value).some((key) => ![
802
+ "project",
803
+ "sourceDigest",
804
+ "stimBuild",
805
+ "inputs",
806
+ "build"
807
+ ].includes(key)) || typeof value.project !== "string" || value.project !== "" && !path$1(value.project) || typeof value.sourceDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.sourceDigest) || !text(value.stimBuild) || value.stimBuild.length > 256) return null;
808
+ const { inputs, build } = value;
809
+ if (!isJsonObject(inputs) || inputs.complete !== true || Object.keys(inputs).some((key) => ![
810
+ "complete",
811
+ "ignored",
812
+ "outputs",
813
+ "localFiles",
814
+ "environment"
815
+ ].includes(key)) || !strings(inputs.ignored, path$1) || !strings(inputs.outputs, path$1) || !strings(inputs.localFiles, text) || !strings(inputs.environment, (name) => typeof name === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) || !isJsonObject(build) || Object.keys(build).some((key) => ![
816
+ "product",
817
+ "infoPlist",
818
+ "resources",
819
+ "assetCatalog"
820
+ ].includes(key)) || typeof build.product !== "string" || !/^[A-Za-z0-9_.-]{1,100}$/.test(build.product) || !path$1(build.infoPlist) || !isJsonObject(build.resources) || Object.keys(build.resources).length > 256 || build.assetCatalog !== null && !path$1(build.assetCatalog)) return null;
821
+ const resourceEntries = [];
822
+ for (const [destination, source] of Object.entries(build.resources).toSorted(([a], [b]) => a.localeCompare(b))) {
823
+ if (!validMacosResourceDestination(destination) || !path$1(source)) return null;
824
+ resourceEntries.push([destination, source]);
825
+ }
826
+ return {
827
+ project: value.project,
828
+ sourceDigest: value.sourceDigest,
829
+ stimBuild: value.stimBuild,
830
+ inputs: {
831
+ complete: true,
832
+ ignored: normalized(inputs.ignored),
833
+ outputs: normalized(inputs.outputs),
834
+ localFiles: normalized(inputs.localFiles),
835
+ environment: normalized(inputs.environment)
836
+ },
837
+ build: {
838
+ product: build.product,
839
+ infoPlist: build.infoPlist,
840
+ resources: Object.fromEntries(resourceEntries),
841
+ assetCatalog: build.assetCatalog
842
+ }
843
+ };
844
+ }
845
+ //#endregion
462
846
  //#region index.ts
463
847
  const warnedRelative = /* @__PURE__ */ new Set();
464
848
  function absoluteEnvPath(name) {
465
- const value = process.env[name];
849
+ const value = ambientEnv()[name];
466
850
  if (!value) return null;
467
851
  if (path.isAbsolute(value)) return value;
468
852
  if (!warnedRelative.has(name)) {
@@ -472,7 +856,7 @@ function absoluteEnvPath(name) {
472
856
  return null;
473
857
  }
474
858
  function configDir() {
475
- return absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim");
859
+ return ambientContext()?.home ?? (absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim"));
476
860
  }
477
861
  /** Creates `dir` and its missing parents owner-only, and narrows an existing `dir` that grants group or other access when the filesystem and owner allow it. */
478
862
  function ensurePrivateDir(dir) {
@@ -660,7 +1044,7 @@ function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces
660
1044
  name,
661
1045
  prune,
662
1046
  note,
663
- registeredBy: process.cwd()
1047
+ registeredBy: ambientContext()?.cwd ?? process.cwd()
664
1048
  };
665
1049
  if (entriesDepth) record.entriesDepth = entriesDepth;
666
1050
  if (layout) record.layout = layout;
@@ -678,4 +1062,4 @@ function matchesCache(cache, match) {
678
1062
  return match.layout === void 0 || cache.layout === match.layout;
679
1063
  }
680
1064
  //#endregion
681
- export { resolveArtifact as A, workspaceName as C, createMetroGateway as D, createMetroBridge as E, readJsonObject as F, readStateFile as I, withStateReadCache as L, validMacosResourceDestination as M, isJsonObject as N, compiledHelper as O, readJsonFile as P, withDirLock as R, workspaceLogDir as S, workspaceStateDir as T, rotatedLogPath as _, buildCacheRoot as a, updateCacheManifest as b, cacheNameSegment as c, ensurePrivateDir as d, hostSimulatorArch as f, rotateLog as g, registerCache as h, buildCacheKey as i, storeArtifact as j, artifactIn as k, cachePathSetting as l, readCacheManifest as m, METRO_NAMED_CACHE_LAYOUT as n, cacheDirRefusal as o, metroCacheRoot as p, STORE_ROOT_TAG as r, cacheManifestLockPath as s, LOG_ROTATE_BYTES as t, configDir as u, sharedStoreRoot as v, workspaceSlug as w, workspaceId as x, tagSharedStore as y };
1065
+ export { withDirLockTransaction as $, createMetroGateway as A, unitWords as B, workspaceName as C, reactNativeArtifactIdentity as D, swiftpmArtifactQuery as E, artifactIn as F, withAmbientContext as G, ambientContext as H, resolveArtifact as I, readJsonObject as J, isJsonObject as K, storeArtifact as L, reusableDependencies as M, compiledHelper as N, reactNativeArtifactQuery as O, storeArtifactAsync as P, withDirLockAsync as Q, quoteUnitWord as R, workspaceLogDir as S, workspaceStateDir as T, ambientEnv as U, validMacosResourceDestination as V, ambientWrite as W, withStateReadCache as X, readStateFile as Y, withDirLock as Z, rotatedLogPath as _, buildCacheRoot as a, updateCacheManifest as b, cacheNameSegment as c, ensurePrivateDir as d, hostSimulatorArch as f, rotateLog as g, registerCache as h, buildCacheKey as i, dependencyInstall as j, createMetroBridge as k, cachePathSetting as l, readCacheManifest as m, METRO_NAMED_CACHE_LAYOUT as n, cacheDirRefusal as o, metroCacheRoot as p, readJsonFile as q, STORE_ROOT_TAG as r, cacheManifestLockPath as s, LOG_ROTATE_BYTES as t, configDir as u, sharedStoreRoot as v, workspaceSlug as w, workspaceId as x, tagSharedStore as y, unitLines as z };