@stim-cli/core 1.20.2 → 1.21.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.
@@ -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
4
  import path, { basename, dirname, join } from "node:path";
5
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
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;
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);
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;
112
+ }
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);
98
121
  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;
122
+ return fn();
123
+ } finally {
124
+ lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 1) - 1);
127
125
  }
128
126
  }
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
- }
138
- }
139
- const { claim, marker } = acquireDirLock(lockPath, {
140
- waitMs,
141
- pollMs,
142
- ensureParent
143
- });
127
+ function holdDirLock(lockPath, { claim, marker }, fn) {
144
128
  lockDepths.set(lockPath, 1);
145
129
  try {
146
130
  return fn();
@@ -152,6 +136,52 @@ 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, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent, signal } = {}) {
163
+ signal?.throwIfAborted();
164
+ if ((lockDepths.get(lockPath) || 0) > 0) return nestedDirLock(lockPath, fn);
165
+ prepareDirLock(lockPath, ensureParent);
166
+ const deadline = Date.now() + waitMs;
167
+ const pending = {};
168
+ let held;
169
+ try {
170
+ while (!(held = attemptDirLock(lockPath, pending))) {
171
+ if (Date.now() >= deadline) throw dirLockTimeout(lockPath, pending.claim);
172
+ releaseClaim(pending.claim);
173
+ pending.claim = void 0;
174
+ await setTimeout$1(pollMs, void 0, { signal });
175
+ }
176
+ } catch (error) {
177
+ releaseClaim(pending.claim);
178
+ throw error;
179
+ }
180
+ return holdDirLock(lockPath, held, () => {
181
+ signal?.throwIfAborted();
182
+ return fn();
183
+ });
184
+ }
155
185
  //#endregion
156
186
  //#region state/json-file.ts
157
187
  const readScope = new AsyncLocalStorage();
@@ -195,12 +225,94 @@ function readJsonObject(path) {
195
225
  }
196
226
  }
197
227
  //#endregion
228
+ //#region context.ts
229
+ const ambient = new AsyncLocalStorage();
230
+ /**
231
+ * Runs `fn` with `context` reachable through `ambientContext()`, including after an `await` inside it. A temporary
232
+ * bridge for call sites that are not passed a context yet: `configDir()`, `runId()` and the exec wrapper's base
233
+ * environment.
234
+ */
235
+ function withAmbientContext(context, fn) {
236
+ return ambient.run(context, fn);
237
+ }
238
+ /** The context set by `withAmbientContext`, for call sites that are not passed one yet. */
239
+ function ambientContext() {
240
+ return ambient.getStore();
241
+ }
242
+ /** Writes `text` to the ambient context's output, or to the process stream outside one. */
243
+ function ambientWrite(stream, text) {
244
+ const context = ambient.getStore();
245
+ if (context) context.output(stream, text);
246
+ else process[stream].write(text);
247
+ }
248
+ /** The environment of the ambient context, or `process.env` outside one. */
249
+ function ambientEnv() {
250
+ return ambient.getStore()?.env ?? process.env;
251
+ }
252
+ //#endregion
198
253
  //#region macos-resources.ts
199
254
  /** Whether a resource destination is a contained, non-empty path under Contents/Resources. */
200
255
  function validMacosResourceDestination(value) {
201
256
  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
257
  }
203
258
  //#endregion
259
+ //#region systemd-unit.ts
260
+ const ESCAPES = {
261
+ "\\": "\\\\",
262
+ "\"": "\\\"",
263
+ "\n": "\\n",
264
+ " ": "\\t",
265
+ "\r": "\\r"
266
+ };
267
+ const UNESCAPES = {
268
+ n: "\n",
269
+ t: " ",
270
+ r: "\r"
271
+ };
272
+ /**
273
+ * One double-quoted word as systemd.syntax(7) reads it: C escapes inside the quotes, `%` doubled because unit files
274
+ * expand specifiers, and in `ExecStart=` `$` doubled because systemd expands variables in command lines.
275
+ */
276
+ function quoteUnitWord(value, exec) {
277
+ return `"${value.replace(/[\\"\n\t\r%$]/g, (char) => char === "%" ? "%%" : char === "$" ? exec ? "$$" : "$" : ESCAPES[char] ?? char)}"`;
278
+ }
279
+ /** The words of a unit value `quoteUnitWord` wrote, or null when the value is not in that form. */
280
+ function unitWords(line, exec) {
281
+ const out = [];
282
+ const pattern = /\s*(?:"((?:[^"\\]|\\.)*)"|([^\s"]+))/y;
283
+ let index = 0;
284
+ while (index < line.length && line.slice(index).trim()) {
285
+ pattern.lastIndex = index;
286
+ const match = pattern.exec(line);
287
+ if (!match) return null;
288
+ index = pattern.lastIndex;
289
+ const raw = match[1] ?? match[2] ?? "";
290
+ out.push(raw.replace(/\\(.)|%%|\$\$/g, (whole, escaped) => escaped !== void 0 ? UNESCAPES[escaped] ?? escaped : whole === "%%" ? "%" : exec ? "$" : whole));
291
+ }
292
+ return out;
293
+ }
294
+ /** The `key=value` lines of a unit file with the section each is in, skipping comments and blank lines. */
295
+ function unitLines(text) {
296
+ let section = "";
297
+ const out = [];
298
+ for (const line of text.split("\n")) {
299
+ const trimmed = line.trim();
300
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
301
+ const header = /^\[(.+)\]$/.exec(trimmed)?.[1];
302
+ if (header) {
303
+ section = header;
304
+ continue;
305
+ }
306
+ const equals = trimmed.indexOf("=");
307
+ if (equals > 0) out.push({
308
+ section,
309
+ key: trimmed.slice(0, equals).trim(),
310
+ value: trimmed.slice(equals + 1).trim()
311
+ });
312
+ }
313
+ return out;
314
+ }
315
+ //#endregion
204
316
  //#region artifact-store.ts
205
317
  function artifactIn(dir) {
206
318
  if (!existsSync(dir)) return null;
@@ -298,6 +410,68 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
298
410
  return artifactIn(dest);
299
411
  }
300
412
  //#endregion
413
+ //#region artifact-store-async.ts
414
+ /** Awaits copying before atomic publication; cancellation before publication preserves the previous entry. */
415
+ async function storeArtifactAsync(dest, buildPath, { runFile, overwrite = false, writeMetadata, onRenameError, signal }) {
416
+ signal?.throwIfAborted();
417
+ const existing = artifactIn(dest);
418
+ if (existing && !overwrite) return existing;
419
+ const token = randomUUID();
420
+ const staging = `${dest}.staging-${token}`;
421
+ const replaced = `${dest}.replaced-${token}`;
422
+ const target = join(staging, basename(buildPath));
423
+ await mkdir(staging, { recursive: true });
424
+ try {
425
+ signal?.throwIfAborted();
426
+ try {
427
+ await runFile("cp", [
428
+ "-c",
429
+ "-R",
430
+ buildPath,
431
+ target
432
+ ]);
433
+ } catch {
434
+ signal?.throwIfAborted();
435
+ await rm(target, {
436
+ recursive: true,
437
+ force: true
438
+ });
439
+ signal?.throwIfAborted();
440
+ await runFile("cp", [
441
+ "-R",
442
+ buildPath,
443
+ target
444
+ ]);
445
+ }
446
+ signal?.throwIfAborted();
447
+ await writeMetadata?.(staging);
448
+ signal?.throwIfAborted();
449
+ const published = artifactIn(dest);
450
+ if (published && !overwrite) return published;
451
+ mkdirSync(dirname(dest), { recursive: true });
452
+ if (existsSync(dest)) renameSync(dest, replaced);
453
+ try {
454
+ renameSync(staging, dest);
455
+ } catch (error) {
456
+ try {
457
+ renameSync(replaced, dest);
458
+ } catch {}
459
+ if (!onRenameError) throw error;
460
+ await onRenameError(staging);
461
+ }
462
+ return artifactIn(dest);
463
+ } finally {
464
+ await rm(staging, {
465
+ recursive: true,
466
+ force: true
467
+ });
468
+ await rm(replaced, {
469
+ recursive: true,
470
+ force: true
471
+ });
472
+ }
473
+ }
474
+ //#endregion
301
475
  //#region compiled-helper.ts
302
476
  const RECENT_MS = 6e5;
303
477
  const pruned = /* @__PURE__ */ new Set();
@@ -343,9 +517,9 @@ function pruneCompiledHelpers(dir, currentName, name, runningPaths = runningExec
343
517
  * Returns `<dir>/<name>-<hash>`, compiling it with `compile` first when it is missing. The hash covers `version` and
344
518
  * each input's name and bytes, so a changed source or compiler builds a new helper beside the old one. `compile`
345
519
  * 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.
520
+ * 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
521
  */
348
- async function compiledHelper({ dir, name, inputs, version, compile }) {
522
+ async function compiledHelper({ dir, name, inputs, version, compile, prune = true }) {
349
523
  const hash = createHash("sha256").update(version);
350
524
  for (const input of inputs) hash.update(basename(input)).update(readFileSync(input));
351
525
  const helper = join(dir, `${name}-${hash.digest("hex").slice(0, 16)}`);
@@ -363,8 +537,10 @@ async function compiledHelper({ dir, name, inputs, version, compile }) {
363
537
  rmSync(output, { force: true });
364
538
  }
365
539
  }
366
- if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
367
- pruned.add(`${dir}/${name}`);
540
+ if (prune) {
541
+ if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
542
+ pruned.add(`${dir}/${name}`);
543
+ }
368
544
  return helper;
369
545
  }
370
546
  //#endregion
@@ -462,7 +638,7 @@ function createMetroBridge({ gatewayPort, peer, secret }) {
462
638
  //#region index.ts
463
639
  const warnedRelative = /* @__PURE__ */ new Set();
464
640
  function absoluteEnvPath(name) {
465
- const value = process.env[name];
641
+ const value = ambientEnv()[name];
466
642
  if (!value) return null;
467
643
  if (path.isAbsolute(value)) return value;
468
644
  if (!warnedRelative.has(name)) {
@@ -472,7 +648,7 @@ function absoluteEnvPath(name) {
472
648
  return null;
473
649
  }
474
650
  function configDir() {
475
- return absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim");
651
+ return ambientContext()?.home ?? (absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim"));
476
652
  }
477
653
  /** 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
654
  function ensurePrivateDir(dir) {
@@ -660,7 +836,7 @@ function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces
660
836
  name,
661
837
  prune,
662
838
  note,
663
- registeredBy: process.cwd()
839
+ registeredBy: ambientContext()?.cwd ?? process.cwd()
664
840
  };
665
841
  if (entriesDepth) record.entriesDepth = entriesDepth;
666
842
  if (layout) record.layout = layout;
@@ -678,4 +854,4 @@ function matchesCache(cache, match) {
678
854
  return match.layout === void 0 || cache.layout === match.layout;
679
855
  }
680
856
  //#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 };
857
+ export { artifactIn as A, withAmbientContext as B, workspaceName as C, createMetroGateway as D, createMetroBridge as E, unitWords as F, withStateReadCache as G, readJsonFile as H, validMacosResourceDestination as I, withDirLock as K, ambientContext as L, storeArtifact as M, quoteUnitWord as N, compiledHelper as O, unitLines as P, ambientEnv as R, workspaceLogDir as S, workspaceStateDir as T, readJsonObject as U, isJsonObject as V, readStateFile as W, 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, resolveArtifact as j, storeArtifactAsync as k, cachePathSetting as l, readCacheManifest as m, METRO_NAMED_CACHE_LAYOUT as n, cacheDirRefusal as o, metroCacheRoot as p, withDirLockAsync 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, ambientWrite as z };