@stim-cli/core 1.9.0 → 1.10.1

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/dist/index.mjs CHANGED
@@ -1,421 +1,3 @@
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";
3
- import os from "node:os";
4
- import path, { basename, dirname, join } from "node:path";
5
- import { createHash } from "node:crypto";
6
- //#region dir-lock.ts
7
- const DEFAULT_LOCK_WAIT_MS = 12e3;
8
- const DEFAULT_LOCK_POLL_MS = 25;
9
- const lockDepths = /* @__PURE__ */ new Map();
10
- const OWNER_MARKER = /^\.stim-claim-[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/;
11
- function sleepSync(ms) {
12
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
13
- }
14
- /**
15
- * Whether Windows refused a call on the lock directory because another process is removing it at that
16
- * moment. NTFS answers ERROR_ACCESS_DENIED, which Node reports as EPERM, until that removal completes,
17
- * where POSIX answers EEXIST or ENOENT: https://github.com/appandflow/stim/issues/883.
18
- */
19
- function removalInFlight(error) {
20
- return process.platform === "win32" && error?.code === "EPERM";
21
- }
22
- function createLockDirectory(lockPath) {
23
- try {
24
- mkdirSync(lockPath);
25
- return true;
26
- } catch (error) {
27
- if (error?.code === "EEXIST" || removalInFlight(error)) return false;
28
- throw error;
29
- }
30
- }
31
- function removeLockDirectory(lockPath, marker) {
32
- try {
33
- unlinkSync(marker);
34
- } catch (error) {
35
- if (error?.code === "ENOENT" || removalInFlight(error)) return false;
36
- throw error;
37
- }
38
- try {
39
- rmdirSync(lockPath);
40
- return true;
41
- } catch (error) {
42
- if ([
43
- "ENOENT",
44
- "ENOTEMPTY",
45
- "EEXIST"
46
- ].includes(error?.code ?? "")) return false;
47
- if (removalInFlight(error)) return false;
48
- throw error;
49
- }
50
- }
51
- function takeLockDirectory(lockPath, claim) {
52
- if (!createLockDirectory(lockPath)) {
53
- let entries;
54
- try {
55
- entries = readdirSync(lockPath);
56
- } catch (error) {
57
- if (error?.code === "ENOENT" || removalInFlight(error)) return null;
58
- throw error;
59
- }
60
- const name = entries.length === 1 ? entries[0] : void 0;
61
- if (!name || !OWNER_MARKER.test(name)) return null;
62
- const marker = join(lockPath, name);
63
- let entry;
64
- try {
65
- entry = lstatSync(marker);
66
- } catch (error) {
67
- if (error?.code === "ENOENT" || removalInFlight(error)) return null;
68
- throw error;
69
- }
70
- if (!entry.isFile() || entry.size !== 0) throw new ClaimRefusedError({
71
- root: lockPath,
72
- claimPath: lockPath,
73
- label: "directory lock",
74
- reason: `its compatibility marker ${name} is not an empty file`
75
- });
76
- if (!removeLockDirectory(lockPath, marker) || !createLockDirectory(lockPath)) return null;
77
- }
78
- const marker = join(lockPath, `.stim-claim-${claim.claimId}`);
79
- writeFileSync(marker, "", { flag: "wx" });
80
- return marker;
81
- }
82
- function acquireDirLock(lockPath, { waitMs, pollMs, ensureParent }) {
83
- ensureParent?.();
84
- statSync(dirname(lockPath));
85
- const deadline = Date.now() + waitMs;
86
- let claim;
87
- try {
88
- for (;;) {
89
- if (!claim) {
90
- const attempt = tryAcquireClaim({
91
- root: `${lockPath}.claims`,
92
- mode: "exclusive",
93
- label: "directory lock"
94
- });
95
- claim = attempt.acquired;
96
- if (attempt.pending) releaseClaim(attempt.pending);
97
- }
98
- if (claim) {
99
- const marker = takeLockDirectory(lockPath, claim);
100
- if (marker) return {
101
- claim,
102
- marker
103
- };
104
- }
105
- if (Date.now() >= deadline) {
106
- const error = /* @__PURE__ */ new Error(`Timed out waiting for the lock at ${lockPath}. Another Stim process may be holding it; if none is running, remove that directory.`);
107
- error.code = "STIM_LOCK_TIMEOUT";
108
- error.lockPath = lockPath;
109
- throw error;
110
- }
111
- sleepSync(pollMs);
112
- }
113
- } catch (error) {
114
- releaseClaim(claim);
115
- throw error;
116
- }
117
- }
118
- function withDirLock(lockPath, fn, { waitMs = DEFAULT_LOCK_WAIT_MS, pollMs = DEFAULT_LOCK_POLL_MS, ensureParent } = {}) {
119
- const depth = lockDepths.get(lockPath) || 0;
120
- if (depth > 0) {
121
- lockDepths.set(lockPath, depth + 1);
122
- try {
123
- return fn();
124
- } finally {
125
- lockDepths.set(lockPath, (lockDepths.get(lockPath) ?? 1) - 1);
126
- }
127
- }
128
- const { claim, marker } = acquireDirLock(lockPath, {
129
- waitMs,
130
- pollMs,
131
- ensureParent
132
- });
133
- lockDepths.set(lockPath, 1);
134
- try {
135
- return fn();
136
- } finally {
137
- lockDepths.set(lockPath, 0);
138
- try {
139
- removeLockDirectory(lockPath, marker);
140
- } catch {}
141
- releaseClaim(claim);
142
- }
143
- }
144
- //#endregion
145
- //#region artifact-store.ts
146
- function artifactIn(dir) {
147
- if (!existsSync(dir)) return null;
148
- let found;
149
- try {
150
- found = readdirSync(dir).find((f) => f.endsWith(".app") || f.endsWith(".apk"));
151
- } catch {
152
- return null;
153
- }
154
- return found ? join(dir, found) : null;
155
- }
156
- function resolveArtifact(dir) {
157
- const hit = artifactIn(dir);
158
- if (!hit) return null;
159
- try {
160
- utimesSync(dir, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
161
- } catch {}
162
- return hit;
163
- }
164
- function storeArtifact(dest, buildPath, { runFile, overwrite = false, writeMetadata, onRenameError }) {
165
- const existing = artifactIn(dest);
166
- if (existing && !overwrite) return existing;
167
- const staging = `${dest}.staging-${process.pid}`;
168
- const replaced = `${dest}.replaced-${process.pid}`;
169
- const target = join(staging, basename(buildPath));
170
- rmSync(staging, {
171
- recursive: true,
172
- force: true
173
- });
174
- mkdirSync(staging, { recursive: true });
175
- try {
176
- try {
177
- runFile("cp", [
178
- "-c",
179
- "-R",
180
- buildPath,
181
- target
182
- ]);
183
- } catch {
184
- rmSync(target, {
185
- recursive: true,
186
- force: true
187
- });
188
- runFile("cp", [
189
- "-R",
190
- buildPath,
191
- target
192
- ]);
193
- }
194
- writeMetadata?.(staging);
195
- const published = artifactIn(dest);
196
- if (published && !overwrite) {
197
- rmSync(staging, {
198
- recursive: true,
199
- force: true
200
- });
201
- return published;
202
- }
203
- mkdirSync(dirname(dest), { recursive: true });
204
- rmSync(replaced, {
205
- recursive: true,
206
- force: true
207
- });
208
- if (existsSync(dest)) renameSync(dest, replaced);
209
- } catch (error) {
210
- rmSync(staging, {
211
- recursive: true,
212
- force: true
213
- });
214
- throw error;
215
- }
216
- try {
217
- renameSync(staging, dest);
218
- } catch (error) {
219
- try {
220
- renameSync(replaced, dest);
221
- } catch {}
222
- if (!onRenameError) {
223
- rmSync(staging, {
224
- recursive: true,
225
- force: true
226
- });
227
- rmSync(replaced, {
228
- recursive: true,
229
- force: true
230
- });
231
- throw error;
232
- }
233
- onRenameError(staging);
234
- }
235
- rmSync(replaced, {
236
- recursive: true,
237
- force: true
238
- });
239
- return artifactIn(dest);
240
- }
241
- //#endregion
242
- //#region index.ts
243
- const warnedRelative = /* @__PURE__ */ new Set();
244
- function absoluteEnvPath(name) {
245
- const value = process.env[name];
246
- if (!value) return null;
247
- if (path.isAbsolute(value)) return value;
248
- if (!warnedRelative.has(name)) {
249
- warnedRelative.add(name);
250
- process.emitWarning(`${name}=${value} is not an absolute path and is ignored.`, { code: "STIM_RELATIVE_PATH" });
251
- }
252
- return null;
253
- }
254
- function configDir() {
255
- return absoluteEnvPath("STIM_HOME") || path.join(os.homedir(), ".stim");
256
- }
257
- function workspaceSlug(projectRoot) {
258
- return path.basename(path.resolve(projectRoot)).normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 48) || "workspace";
259
- }
260
- function workspaceId(projectRoot) {
261
- return createHash("sha256").update(path.resolve(projectRoot)).digest("hex").slice(0, 16);
262
- }
263
- function workspaceName(projectRoot) {
264
- return `${workspaceSlug(projectRoot)}--${workspaceId(projectRoot)}`;
265
- }
266
- function workspaceStateDir(projectRoot) {
267
- return path.join(configDir(), "workspaces", workspaceName(projectRoot));
268
- }
269
- function workspaceLogDir(projectRoot) {
270
- return path.join(workspaceStateDir(projectRoot), "logs");
271
- }
272
- const LOG_ROTATE_BYTES = 8388608;
273
- function rotatedLogPath(file) {
274
- return `${file}.1`;
275
- }
276
- /** Moves `file` over its previous generation once it reaches `maxBytes`; readers read both generations. */
277
- function rotateLog(file, maxBytes) {
278
- if (logSize(file) < maxBytes) return;
279
- withDirLock(`${file}.lock`, () => {
280
- if (logSize(file) >= maxBytes) fs.renameSync(file, rotatedLogPath(file));
281
- }, {
282
- waitMs: 100,
283
- pollMs: 10
284
- });
285
- }
286
- function logSize(file) {
287
- try {
288
- return fs.statSync(file).size;
289
- } catch {
290
- return 0;
291
- }
292
- }
293
- function cachePathSetting(key) {
294
- try {
295
- const value = JSON.parse(fs.readFileSync(path.join(configDir(), "config.json"), "utf-8"))?.caches?.[key];
296
- return typeof value === "string" && path.isAbsolute(value) ? value : null;
297
- } catch {
298
- return null;
299
- }
300
- }
301
- function cacheNameSegment(name) {
302
- return String(name).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "app";
303
- }
304
- function buildCacheRoot() {
305
- return absoluteEnvPath("STIM_BUILD_CACHE") || cachePathSetting("buildCache") || path.join(configDir(), "build-cache");
306
- }
307
- function metroCacheRoot(name) {
308
- const root = absoluteEnvPath("STIM_METRO_CACHE") || cachePathSetting("metroCache") || path.join(configDir(), "metro-cache");
309
- return name === void 0 || name === null || name === "" ? root : path.join(root, cacheNameSegment(name));
310
- }
311
- const STORE_ROOT_TAG = "stimStoreRoot";
312
- function tagSharedStore(store, root) {
313
- try {
314
- Object.defineProperty(store, STORE_ROOT_TAG, {
315
- value: root,
316
- enumerable: false,
317
- configurable: true
318
- });
319
- } catch {}
320
- return store;
321
- }
322
- function sharedStoreRoot(store) {
323
- if (store === null || typeof store !== "object") return null;
324
- const tagged = store[STORE_ROOT_TAG];
325
- if (typeof tagged === "string") return tagged;
326
- const legacy = store._root;
327
- return typeof legacy === "string" ? legacy : null;
328
- }
329
- const SIMULATOR_UDID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
330
- const EMULATOR_SERIAL = /^emulator-\d+$/;
331
- function slug(value) {
332
- return String(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
333
- }
334
- function buildVariant(platform, options) {
335
- const raw = platform === "android" ? options.variant : options.configuration ?? options.buildConfiguration;
336
- return (typeof raw === "string" ? slug(raw) : "") || "debug";
337
- }
338
- function buildTarget(options) {
339
- if (typeof options.isSimulator === "boolean") return options.isSimulator ? "sim" : "device";
340
- const device = options.device;
341
- if (device === void 0 || device === null || device === false) return "sim";
342
- if (typeof device !== "string") return "prompted";
343
- const name = device.trim();
344
- if (name === "" || name === "generic") return "sim";
345
- if (SIMULATOR_UDID.test(name) || EMULATOR_SERIAL.test(name)) return "sim";
346
- return `on-${slug(name)}`;
347
- }
348
- function buildCacheKey(platform, fingerprintHash, options = {}) {
349
- const opts = options && typeof options === "object" ? options : {};
350
- const abi = platform === "android" && typeof opts.abi === "string" ? slug(opts.abi) : "";
351
- const allArch = platform === "android" && opts.allArch === true ? "-all-arch" : "";
352
- const compiler = typeof opts.compiler === "string" ? slug(opts.compiler) : "";
353
- const profile = typeof opts.buildProfile === "string" ? slug(opts.buildProfile) : "";
354
- const scheme = platform === "ios" && typeof opts.scheme === "string" && opts.scheme ? createHash("sha256").update(opts.scheme).digest("hex") : "";
355
- return `${fingerprintHash}-${buildVariant(platform, opts)}-${buildTarget(opts)}${abi ? `-${abi}` : ""}${allArch}${compiler ? `-${compiler}` : ""}${profile ? `-${profile}` : ""}${scheme ? `-scheme-${scheme}` : ""}`;
356
- }
357
- const METRO_NAMED_CACHE_LAYOUT = "metro-named-v1";
358
- function cacheManifestLockPath(file) {
359
- return `${file}.lock`;
360
- }
361
- function readCacheManifest(file) {
362
- try {
363
- const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
364
- return {
365
- version: 1,
366
- caches: Array.isArray(parsed?.caches) ? parsed.caches : []
367
- };
368
- } catch {
369
- return {
370
- version: 1,
371
- caches: []
372
- };
373
- }
374
- }
375
- function updateCacheManifest(file, mutate) {
376
- const parent = path.dirname(file);
377
- return withDirLock(cacheManifestLockPath(file), () => {
378
- const next = {
379
- version: 1,
380
- caches: mutate(readCacheManifest(file).caches)
381
- };
382
- const temporary = path.join(parent, `.${path.basename(file)}.${process.pid}.tmp`);
383
- try {
384
- fs.writeFileSync(temporary, JSON.stringify(next, null, 2));
385
- fs.renameSync(temporary, file);
386
- } catch (error) {
387
- fs.rmSync(temporary, { force: true });
388
- throw error;
389
- }
390
- return next;
391
- }, { ensureParent: () => fs.mkdirSync(parent, { recursive: true }) });
392
- }
393
- function registerCache({ dir, name, prune, note, entriesDepth, layout, replaces = [] }) {
394
- if (!path.isAbsolute(dir)) return;
395
- try {
396
- updateCacheManifest(path.join(configDir(), "caches.json"), (caches) => {
397
- const others = caches.filter((cache) => cache.dir !== dir && !replaces.some((match) => matchesCache(cache, match)));
398
- const record = {
399
- dir,
400
- name,
401
- prune,
402
- note,
403
- registeredBy: process.cwd()
404
- };
405
- if (entriesDepth) record.entriesDepth = entriesDepth;
406
- if (layout) record.layout = layout;
407
- others.push(record);
408
- return others;
409
- });
410
- } catch {}
411
- }
412
- function matchesCache(cache, match) {
413
- if (cache.dir !== match.dir) return false;
414
- if (match.name !== void 0 && cache.name !== match.name) return false;
415
- if (match.prune !== void 0 && cache.prune !== match.prune) return false;
416
- if (match.entriesDepth !== void 0 && cache.entriesDepth !== match.entriesDepth) return false;
417
- if (match.layout === null) return !Object.hasOwn(cache, "layout");
418
- return match.layout === void 0 || cache.layout === match.layout;
419
- }
420
- //#endregion
1
+ import { S as quotedPath } from "./ownership-claim-O31Y5iIb.mjs";
2
+ import { C as artifactIn, E as withDirLock, S as workspaceStateDir, T as storeArtifact, _ as updateCacheManifest, a as buildCacheRoot, b as workspaceName, c as cachePathSetting, d as readCacheManifest, f as registerCache, g as tagSharedStore, h as sharedStoreRoot, i as buildCacheKey, l as configDir, m as rotatedLogPath, n as METRO_NAMED_CACHE_LAYOUT, o as cacheManifestLockPath, p as rotateLog, r as STORE_ROOT_TAG, s as cacheNameSegment, t as LOG_ROTATE_BYTES, u as metroCacheRoot, v as workspaceId, w as resolveArtifact, x as workspaceSlug, y as workspaceLogDir } from "./core-CJWLJIr6.mjs";
421
3
  export { LOG_ROTATE_BYTES, METRO_NAMED_CACHE_LAYOUT, STORE_ROOT_TAG, artifactIn, buildCacheKey, buildCacheRoot, cacheManifestLockPath, cacheNameSegment, cachePathSetting, configDir, metroCacheRoot, quotedPath, readCacheManifest, registerCache, resolveArtifact, rotateLog, rotatedLogPath, sharedStoreRoot, storeArtifact, tagSharedStore, updateCacheManifest, withDirLock, workspaceId, workspaceLogDir, workspaceName, workspaceSlug, workspaceStateDir };
@@ -15,6 +15,16 @@ declare function captureProcessIdentity(pid: number): CapturedIdentity;
15
15
  declare function captureProcessToken(pid: number): string | null;
16
16
  type ProcessIdentityStatus = "same" | "different" | "gone" | "unknown";
17
17
  declare function inspectProcessIdentity(record: ProcessRecord | null | undefined): ProcessIdentityStatus;
18
+ type ProcessStart = {
19
+ status: "running";
20
+ startedAtMs: number;
21
+ } | {
22
+ status: "gone";
23
+ } | {
24
+ status: "unknown";
25
+ };
26
+ /** Start time of a live process, read from the same OS identity that `captureProcessIdentity` records. */
27
+ declare function inspectProcessStart(pid: number): ProcessStart;
18
28
  declare function waitForProcessExit(record: ProcessRecord, timeoutMs: number): Promise<boolean>;
19
29
  //#endregion
20
- export { CapturedIdentity, ProcessIdentityStatus, ProcessRecord, captureProcessIdentity, captureProcessToken, inspectProcessIdentity, sameProcessRecord, waitForProcessExit };
30
+ export { CapturedIdentity, ProcessIdentityStatus, ProcessRecord, ProcessStart, captureProcessIdentity, captureProcessToken, inspectProcessIdentity, inspectProcessStart, sameProcessRecord, waitForProcessExit };
@@ -26,6 +26,19 @@ function inspectProcessIdentity(record) {
26
26
  const result = check(record.processToken);
27
27
  return result.ok ? result.value : "unknown";
28
28
  }
29
+ /** Start time of a live process, read from the same OS identity that `captureProcessIdentity` records. */
30
+ function inspectProcessStart(pid) {
31
+ const captured = capture(pid);
32
+ if (!captured.ok) return captured.error.code === "NOT_FOUND" ? { status: "gone" } : { status: "unknown" };
33
+ const decoded = decode(captured.value);
34
+ if (!decoded.ok || decoded.value.platform !== "darwin") return { status: "unknown" };
35
+ const match = /^(\d+):(\d+)$/.exec(decoded.value.startTime);
36
+ if (!match) return { status: "unknown" };
37
+ return {
38
+ status: "running",
39
+ startedAtMs: Number(match[1]) * 1e3 + Math.floor(Number(match[2]) / 1e3)
40
+ };
41
+ }
29
42
  async function waitForProcessExit(record, timeoutMs) {
30
43
  const deadline = Date.now() + timeoutMs;
31
44
  while (Date.now() <= deadline) {
@@ -37,4 +50,4 @@ async function waitForProcessExit(record, timeoutMs) {
37
50
  return false;
38
51
  }
39
52
  //#endregion
40
- export { captureProcessIdentity, captureProcessToken, inspectProcessIdentity, sameProcessRecord, waitForProcessExit };
53
+ export { captureProcessIdentity, captureProcessToken, inspectProcessIdentity, inspectProcessStart, sameProcessRecord, waitForProcessExit };