@stim-cli/core 1.15.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,6 +34,9 @@ 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.
37
40
 
38
41
  `@stim-cli/core/oversight` holds the pure notification and needs-attention rules
39
42
  shared by stim-server and the phone. It imports no Node APIs; mobile consumers
@@ -1,8 +1,9 @@
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, readFileSync, 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
5
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
6
+ import { spawnSync } from "node:child_process";
6
7
  import { connect, createServer, isIP } from "node:net";
7
8
  //#region dir-lock.ts
8
9
  const DEFAULT_LOCK_WAIT_MS = 12e3;
@@ -255,27 +256,72 @@ function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetad
255
256
  }
256
257
  //#endregion
257
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
+ }
258
299
  /**
259
300
  * Returns `<dir>/<name>-<hash>`, compiling it with `compile` first when it is missing. The hash covers `version` and
260
301
  * each input's name and bytes, so a changed source or compiler builds a new helper beside the old one. `compile`
261
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.
262
304
  */
263
305
  async function compiledHelper({ dir, name, inputs, version, compile }) {
264
306
  const hash = createHash("sha256").update(version);
265
307
  for (const input of inputs) hash.update(basename(input)).update(readFileSync(input));
266
308
  const helper = join(dir, `${name}-${hash.digest("hex").slice(0, 16)}`);
267
- if (existsSync(helper)) return helper;
268
- mkdirSync(dir, {
269
- recursive: true,
270
- mode: 448
271
- });
272
- const output = `${helper}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
273
- try {
274
- await compile(output);
275
- renameSync(output, helper);
276
- } finally {
277
- rmSync(output, { force: true });
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
+ }
278
322
  }
323
+ if (built || !pruned.has(`${dir}/${name}`)) pruneCompiledHelpers(dir, basename(helper), name);
324
+ pruned.add(`${dir}/${name}`);
279
325
  return helper;
280
326
  }
281
327
  //#endregion