@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.
- package/README.md +2 -0
- package/dist/{core-BkgfKqJ8.mjs → core-BTsCO1nW.mjs} +444 -60
- package/dist/{index-VDWBflH3.d.mts → index-DKq1j8dw.d.mts} +340 -91
- package/dist/index.d.mts +101 -5
- package/dist/index.mjs +3 -3
- package/dist/keyed-delta-BBoOeR2J.mjs +289 -0
- package/dist/noise-DYIbymAJ.d.mts +100 -0
- package/dist/noise-session.d.mts +164 -0
- package/dist/noise-session.mjs +402 -0
- package/dist/noise.d.mts +2 -0
- package/dist/noise.mjs +393 -0
- package/dist/oversight.mjs +16 -6
- package/dist/{ownership-claim-O31Y5iIb.mjs → ownership-claim-DHUs_Dir.mjs} +1 -1
- package/dist/{ownership-claim-Xxxr9MPp.d.mts → ownership-claim-RY3fbm1D.d.mts} +3 -2
- package/dist/ownership-claim.d.mts +2 -2
- package/dist/ownership-claim.mjs +2 -2
- package/dist/phone-protocol.d.mts +85 -16
- package/dist/phone-protocol.mjs +2 -2
- package/dist/{process-identity-BTyww2Xj.d.mts → process-identity-DVNGpF5c.d.mts} +1 -1
- package/dist/process-identity.d.mts +1 -1
- package/dist/process-identity.mjs +3 -1
- package/dist/{protocol-pH632kA3.d.mts → protocol-BjJt4ghB.d.mts} +632 -65
- package/dist/protocol.d.mts +2 -2
- package/dist/protocol.mjs +81 -10
- package/dist/receive-protocol.d.mts +1 -1
- package/dist/receive-protocol.mjs +20163 -14469
- package/dist/state.d.mts +2 -2
- package/dist/state.mjs +1261 -208
- package/dist/toolchain.d.mts +64 -0
- package/dist/toolchain.mjs +326 -0
- package/dist/workspace-link.d.mts +16 -0
- package/dist/workspace-link.mjs +50 -0
- package/package.json +21 -1
- 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 {
|
|
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
|
|
92
|
+
function prepareDirLock(lockPath, ensureParent) {
|
|
94
93
|
ensureParent?.();
|
|
95
94
|
statSync(dirname(lockPath));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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 (
|
|
367
|
-
|
|
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 =
|
|
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 {
|
|
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 };
|