@stim-cli/core 1.14.0 → 1.16.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 +18 -0
- package/dist/{core-CmZJEFj_.mjs → core-DWOUn9xp.mjs} +179 -5
- package/dist/index-DPo8Bexj.d.mts +2228 -0
- package/dist/index.d.mts +42 -1
- package/dist/index.mjs +2 -2
- package/dist/oversight.d.mts +308 -0
- package/dist/oversight.mjs +666 -0
- package/dist/ownership-claim-Xxxr9MPp.d.mts +152 -0
- package/dist/ownership-claim.d.mts +1 -151
- package/dist/phone-protocol.d.mts +458 -0
- package/dist/phone-protocol.mjs +2 -0
- package/dist/process-identity-BTyww2Xj.d.mts +37 -0
- package/dist/process-identity.d.mts +2 -30
- package/dist/process-identity.mjs +10 -2
- package/dist/protocol-B0KcaUee.d.mts +1465 -0
- package/dist/protocol.d.mts +2 -0
- package/dist/protocol.mjs +379 -0
- package/dist/receive-protocol.d.mts +12 -0
- package/dist/receive-protocol.mjs +19751 -0
- package/dist/state.d.mts +2 -777
- package/dist/state.mjs +4126 -1133
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -34,5 +34,23 @@ path layout, the config and workspace state types and readers, the settings
|
|
|
34
34
|
registry and its JSON Schema, the created-device and EAS session ledger readers,
|
|
35
35
|
NDJSON log records and queries, and the status and settings payload types. The
|
|
36
36
|
CLI owns every write to that state. The cache packages do not import it.
|
|
37
|
+
Setup journals are an exception: stim-server writes and prunes
|
|
38
|
+
`$STIM_HOME/server/setup/<sha256(ticket) hex>.json`; core owns their paths,
|
|
39
|
+
strict parser and reader.
|
|
40
|
+
|
|
41
|
+
`@stim-cli/core/oversight` holds the pure notification and needs-attention rules
|
|
42
|
+
shared by stim-server and the phone. It imports no Node APIs; mobile consumers
|
|
43
|
+
import this subpath without loading the Node-only root or state entry points.
|
|
44
|
+
Build core before typechecking, testing or bundling the phone app.
|
|
37
45
|
|
|
38
46
|
The npm scope remains `@stim-cli` until the `@stim` scope is available.
|
|
47
|
+
|
|
48
|
+
`@stim-cli/core/protocol` defines the server's wire contracts. The
|
|
49
|
+
`phone-protocol` entry derives the phone's receiving types, including optional
|
|
50
|
+
fields from older servers. `receive-protocol` contains precompiled validators
|
|
51
|
+
for method results, known events and errors, with no Node or schema compiler
|
|
52
|
+
dependency in the phone runtime. Unknown compatible fields remain accepted.
|
|
53
|
+
Display enums accept open strings, and unknown values show neutral information or are skipped when unsupported.
|
|
54
|
+
|
|
55
|
+
After changing these contracts, run `pnpm run rpc:generate` at the repository
|
|
56
|
+
root; `pnpm run rpc:check` verifies the committed validators in CI.
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { S as quotedPath, _ as releaseClaim, r as ClaimRefusedError, x as tryAcquireClaim } from "./ownership-claim-O31Y5iIb.mjs";
|
|
2
|
-
import fs, { existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
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 } from "node:crypto";
|
|
5
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { connect, createServer, isIP } from "node:net";
|
|
6
8
|
//#region dir-lock.ts
|
|
7
9
|
const DEFAULT_LOCK_WAIT_MS = 12e3;
|
|
8
10
|
const DEFAULT_LOCK_POLL_MS = 25;
|
|
@@ -150,6 +152,12 @@ function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEF
|
|
|
150
152
|
}
|
|
151
153
|
}
|
|
152
154
|
//#endregion
|
|
155
|
+
//#region macos-resources.ts
|
|
156
|
+
/** Whether a resource destination is a contained, non-empty path under Contents/Resources. */
|
|
157
|
+
function validMacosResourceDestination(value) {
|
|
158
|
+
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 !== "..");
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
153
161
|
//#region artifact-store.ts
|
|
154
162
|
function artifactIn(dir) {
|
|
155
163
|
if (!existsSync(dir)) return null;
|
|
@@ -247,6 +255,167 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
|
|
|
247
255
|
return artifactIn(dest);
|
|
248
256
|
}
|
|
249
257
|
//#endregion
|
|
258
|
+
//#region compiled-helper.ts
|
|
259
|
+
const RECENT_MS = 6e5;
|
|
260
|
+
const pruned = /* @__PURE__ */ new Set();
|
|
261
|
+
function isHelperName(entry, name) {
|
|
262
|
+
return entry.length === name.length + 17 && entry.startsWith(`${name}-`) && /^[0-9a-f]{16}$/.test(entry.slice(name.length + 1));
|
|
263
|
+
}
|
|
264
|
+
function selectHelpersToPrune(entries, currentName, name, running) {
|
|
265
|
+
return entries.filter((entry) => isHelperName(entry.name, name) && entry.name !== currentName).toSorted((a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name)).slice(1).filter((entry) => !running.has(entry.name)).map((entry) => entry.name);
|
|
266
|
+
}
|
|
267
|
+
function runningExecutablePaths() {
|
|
268
|
+
const result = spawnSync("ps", ["-axo", "comm="], {
|
|
269
|
+
encoding: "utf8",
|
|
270
|
+
timeout: 500
|
|
271
|
+
});
|
|
272
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== "string" || !result.stdout.trim()) return null;
|
|
273
|
+
return new Set(result.stdout.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
274
|
+
}
|
|
275
|
+
function pruneCompiledHelpers(dir, currentName, name, runningPaths = runningExecutablePaths) {
|
|
276
|
+
try {
|
|
277
|
+
const entries = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && isHelperName(entry.name, name)).map((entry) => ({
|
|
278
|
+
name: entry.name,
|
|
279
|
+
mtimeMs: statSync(join(dir, entry.name)).mtimeMs
|
|
280
|
+
}));
|
|
281
|
+
if (entries.length <= 2) return;
|
|
282
|
+
const paths = runningPaths();
|
|
283
|
+
if (paths === null) return;
|
|
284
|
+
const canonicalPaths = /* @__PURE__ */ new Set();
|
|
285
|
+
for (const path of paths) try {
|
|
286
|
+
canonicalPaths.add(realpathSync(path));
|
|
287
|
+
} catch {}
|
|
288
|
+
const running = /* @__PURE__ */ new Set();
|
|
289
|
+
for (const entry of entries) try {
|
|
290
|
+
if (canonicalPaths.has(realpathSync(join(dir, entry.name)))) running.add(entry.name);
|
|
291
|
+
} catch {
|
|
292
|
+
running.add(entry.name);
|
|
293
|
+
}
|
|
294
|
+
const recent = Date.now() - RECENT_MS;
|
|
295
|
+
for (const entry of entries) if (entry.mtimeMs > recent) running.add(entry.name);
|
|
296
|
+
for (const entry of selectHelpersToPrune(entries, currentName, name, running)) rmSync(join(dir, entry), { force: true });
|
|
297
|
+
} catch {}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Returns `<dir>/<name>-<hash>`, compiling it with `compile` first when it is missing. The hash covers `version` and
|
|
301
|
+
* each input's name and bytes, so a changed source or compiler builds a new helper beside the old one. `compile`
|
|
302
|
+
* writes to a private temporary path that is renamed into place, so concurrent builders never expose a partial file.
|
|
303
|
+
* 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.
|
|
304
|
+
*/
|
|
305
|
+
async function compiledHelper({ dir, name, inputs, version, compile }) {
|
|
306
|
+
const hash = createHash("sha256").update(version);
|
|
307
|
+
for (const input of inputs) hash.update(basename(input)).update(readFileSync(input));
|
|
308
|
+
const helper = join(dir, `${name}-${hash.digest("hex").slice(0, 16)}`);
|
|
309
|
+
const built = !existsSync(helper);
|
|
310
|
+
if (built) {
|
|
311
|
+
mkdirSync(dir, {
|
|
312
|
+
recursive: true,
|
|
313
|
+
mode: 448
|
|
314
|
+
});
|
|
315
|
+
const output = `${helper}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
316
|
+
try {
|
|
317
|
+
await compile(output);
|
|
318
|
+
renameSync(output, helper);
|
|
319
|
+
} finally {
|
|
320
|
+
rmSync(output, { force: true });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
|
|
324
|
+
pruned.add(`${dir}/${name}`);
|
|
325
|
+
return helper;
|
|
326
|
+
}
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region metro-bridge.ts
|
|
329
|
+
function port(value) {
|
|
330
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) throw new Error("Invalid Metro bridge port.");
|
|
331
|
+
}
|
|
332
|
+
function credential(secret) {
|
|
333
|
+
if (!/^[a-f0-9]{64}$/.test(secret)) throw new Error("A Metro bridge needs a 256-bit secret.");
|
|
334
|
+
return Buffer.from(`${secret}\n`, "ascii");
|
|
335
|
+
}
|
|
336
|
+
function trackedServer(accept) {
|
|
337
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
338
|
+
let closing;
|
|
339
|
+
const track = (socket) => {
|
|
340
|
+
sockets.add(socket);
|
|
341
|
+
socket.once("close", () => sockets.delete(socket));
|
|
342
|
+
socket.on("error", () => socket.destroy());
|
|
343
|
+
};
|
|
344
|
+
const server = createServer((socket) => {
|
|
345
|
+
if (closing || sockets.size >= 128) return void socket.destroy();
|
|
346
|
+
track(socket);
|
|
347
|
+
accept(socket, track);
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
server,
|
|
351
|
+
close: () => {
|
|
352
|
+
closing ??= new Promise((resolve, reject) => {
|
|
353
|
+
for (const socket of sockets) socket.destroy();
|
|
354
|
+
if (!server.listening) return resolve();
|
|
355
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
356
|
+
});
|
|
357
|
+
return closing;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function forward(source, target) {
|
|
362
|
+
source.once("close", () => target.destroy());
|
|
363
|
+
target.once("close", () => source.destroy());
|
|
364
|
+
source.pipe(target).pipe(source);
|
|
365
|
+
}
|
|
366
|
+
/** Bind this gateway only to the client's own Tailscale address; `peer` is the pinned worker's literal address. */
|
|
367
|
+
function createMetroGateway({ metroPort, peer, secret }) {
|
|
368
|
+
port(metroPort);
|
|
369
|
+
if (!isIP(peer)) throw new Error("The Metro gateway requires a pinned peer address.");
|
|
370
|
+
const expected = credential(secret);
|
|
371
|
+
return trackedServer((socket, track) => {
|
|
372
|
+
if (socket.remoteAddress?.replace(/^::ffff:/, "") !== peer) return void socket.destroy();
|
|
373
|
+
const deadline = setTimeout(() => socket.destroy(), 5e3);
|
|
374
|
+
socket.once("close", () => clearTimeout(deadline));
|
|
375
|
+
let prefix = Buffer.alloc(0);
|
|
376
|
+
const authenticate = (chunk) => {
|
|
377
|
+
const needed = expected.length - prefix.length;
|
|
378
|
+
prefix = Buffer.concat([prefix, chunk.subarray(0, needed)]);
|
|
379
|
+
if (prefix.length < expected.length) return;
|
|
380
|
+
socket.removeListener("data", authenticate);
|
|
381
|
+
socket.pause();
|
|
382
|
+
if (!timingSafeEqual(prefix, expected)) return void socket.destroy();
|
|
383
|
+
clearTimeout(deadline);
|
|
384
|
+
const upstream = connect({
|
|
385
|
+
host: "127.0.0.1",
|
|
386
|
+
port: metroPort
|
|
387
|
+
});
|
|
388
|
+
track(upstream);
|
|
389
|
+
if (chunk.length > needed) socket.unshift(chunk.subarray(needed));
|
|
390
|
+
forward(socket, upstream);
|
|
391
|
+
};
|
|
392
|
+
socket.on("data", authenticate);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/** Bind this endpoint only to worker loopback; `peer` comes from the approved client's authenticated connection. */
|
|
396
|
+
function createMetroBridge({ gatewayPort, peer, secret }) {
|
|
397
|
+
port(gatewayPort);
|
|
398
|
+
if (!isIP(peer)) throw new Error("The Metro bridge requires an authenticated peer address.");
|
|
399
|
+
const prefix = credential(secret);
|
|
400
|
+
return trackedServer((socket, track) => {
|
|
401
|
+
socket.pause();
|
|
402
|
+
const upstream = connect({
|
|
403
|
+
host: peer,
|
|
404
|
+
port: gatewayPort,
|
|
405
|
+
timeout: 5e3
|
|
406
|
+
});
|
|
407
|
+
track(upstream);
|
|
408
|
+
upstream.once("timeout", () => upstream.destroy());
|
|
409
|
+
upstream.once("close", () => socket.destroy());
|
|
410
|
+
upstream.once("connect", () => {
|
|
411
|
+
upstream.setTimeout(0);
|
|
412
|
+
upstream.write(prefix);
|
|
413
|
+
forward(socket, upstream);
|
|
414
|
+
});
|
|
415
|
+
socket.once("close", () => upstream.destroy());
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
250
419
|
//#region index.ts
|
|
251
420
|
const warnedRelative = /* @__PURE__ */ new Set();
|
|
252
421
|
function absoluteEnvPath(name) {
|
|
@@ -353,14 +522,19 @@ function buildTarget(options) {
|
|
|
353
522
|
if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return "sim";
|
|
354
523
|
return `on-${slug(name)}`;
|
|
355
524
|
}
|
|
525
|
+
/** The simulator slice a local build compiles for this machine: `arm64` or `x86_64`. */
|
|
526
|
+
function hostSimulatorArch(arch = process.arch) {
|
|
527
|
+
return arch === "x64" ? "x86_64" : "arm64";
|
|
528
|
+
}
|
|
356
529
|
function buildCacheKey(platform, fingerprintHash, options = {}) {
|
|
357
530
|
const opts = options && typeof options === "object" ? options : {};
|
|
358
|
-
const
|
|
531
|
+
const target = buildTarget(opts);
|
|
532
|
+
const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : platform === "ios" && target === "sim" && typeof opts.arch === "string" ? slug(opts.arch) : "";
|
|
359
533
|
const allArch = platform === "android" && opts.allArch === true ? "-all-arch" : "";
|
|
360
534
|
const compiler = typeof opts.compiler === "string" ? slug(opts.compiler) : "";
|
|
361
535
|
const profile = typeof opts.buildProfile === "string" ? slug(opts.buildProfile) : "";
|
|
362
536
|
const scheme = platform === "ios" && typeof opts.scheme === "string" && opts.scheme ? createHash("sha256").update(opts.scheme).digest("hex") : "";
|
|
363
|
-
return `${fingerprintHash}-${buildVariant(platform, opts)}-${
|
|
537
|
+
return `${fingerprintHash}-${buildVariant(platform, opts)}-${target}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
|
|
364
538
|
}
|
|
365
539
|
const METRO_NAMED_CACHE_LAYOUT = "metro-named-v1";
|
|
366
540
|
function cacheManifestLockPath(file) {
|
|
@@ -426,4 +600,4 @@ function matchesCache(cache, match) {
|
|
|
426
600
|
return match.layout === void 0 || cache.layout === match.layout;
|
|
427
601
|
}
|
|
428
602
|
//#endregion
|
|
429
|
-
export {
|
|
603
|
+
export { validMacosResourceDestination as A, workspaceStateDir as C, artifactIn as D, compiledHelper as E, resolveArtifact as O, workspaceSlug as S, createMetroGateway as T, tagSharedStore as _, buildCacheRoot as a, workspaceLogDir as b, cachePathSetting as c, metroCacheRoot as d, readCacheManifest as f, sharedStoreRoot as g, rotatedLogPath as h, buildCacheKey as i, withDirLock as j, storeArtifact as k, configDir as l, rotateLog as m, METRO_NAMED_CACHE_LAYOUT as n, cacheManifestLockPath as o, registerCache as p, STORE_ROOT_TAG as r, cacheNameSegment as s, LOG_ROTATE_BYTES as t, hostSimulatorArch as u, updateCacheManifest as v, createMetroBridge as w, workspaceName as x, workspaceId as y };
|