@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 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 abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : "";
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)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
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 { artifactIn as C, withDirLock as E, workspaceStateDir as S, storeArtifact as T, updateCacheManifest as _, buildCacheRoot as a, workspaceName as b, cachePathSetting as c, readCacheManifest as d, registerCache as f, tagSharedStore as g, sharedStoreRoot as h, buildCacheKey as i, configDir as l, rotatedLogPath as m, METRO_NAMED_CACHE_LAYOUT as n, cacheManifestLockPath as o, rotateLog as p, STORE_ROOT_TAG as r, cacheNameSegment as s, LOG_ROTATE_BYTES as t, metroCacheRoot as u, workspaceId as v, resolveArtifact as w, workspaceSlug as x, workspaceLogDir as y };
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 };