stim 1.8.0 → 1.9.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.
Files changed (80) hide show
  1. package/README.md +23 -155
  2. package/dist/{android-BRgBYzU0.mjs → android-BPGizezh.mjs} +112 -135
  3. package/dist/{android-DeYdjjBP.mjs → android-DEgo4oCo.mjs} +2 -2
  4. package/dist/{android-DWssqHQ8.mjs → android-_y4Ohi5z.mjs} +54 -15
  5. package/dist/android-cas-compiler.mjs +2 -2
  6. package/dist/{android-cas-SWIjzeuL.mjs → android-cas-eiExNLf7.mjs} +21 -14
  7. package/dist/app-install-Uf3a83Tv.mjs +819 -0
  8. package/dist/{build-lock-CgtsB6TV.mjs → build-lock-DPXPLgTc.mjs} +120 -24
  9. package/dist/build-progress-CVqkfPC6.mjs +188 -0
  10. package/dist/{cache-manifest-CYJi8-Hx.mjs → cache-manifest-Dg7OnMrp.mjs} +4 -4
  11. package/dist/cache-manifest.mjs +1 -1
  12. package/dist/cli.mjs +22 -19
  13. package/dist/collector-run.d.mts +2 -0
  14. package/dist/collector-run.mjs +10 -9
  15. package/dist/{command-output-B3ycpsYv.mjs → command-output-gByVmIjy.mjs} +11 -2
  16. package/dist/{config-B7iyKWkL.mjs → config-Cp9lM3wX.mjs} +187 -57
  17. package/dist/{dependency-state-D0M7oKA7.mjs → dependency-state-Bq5DcLGn.mjs} +2 -2
  18. package/dist/{deps-oCXVNEqQ.mjs → deps-Cip5vUm1.mjs} +8 -5
  19. package/dist/{detached-entry-Bpo6ZY_I.mjs → detached-entry-KEu0_AXL.mjs} +1 -1
  20. package/dist/{device-BUoZyMoR.mjs → dev-client-HyqISM0h.mjs} +1178 -635
  21. package/dist/{device-RdujjHLB.mjs → device-D4U-xLty.mjs} +13 -20
  22. package/dist/{device-lease-Ffz_M5t0.mjs → device-lease-Crs-HkJY.mjs} +16 -9
  23. package/dist/{device-pool-BxRwdufO.mjs → device-pool-CnTAz8X1.mjs} +7 -7
  24. package/dist/{device-remote-ByN8P6CB.mjs → device-remote-AQ5rQPqr.mjs} +482 -35
  25. package/dist/{doctor-BMt156qe.mjs → doctor-B_Ei3bZu.mjs} +95 -107
  26. package/dist/{error-diagnostics-D1Kaf-eT.mjs → error-diagnostics-Vfz83GCF.mjs} +22 -16
  27. package/dist/exec-jIAlgilk.mjs +132 -0
  28. package/dist/{gc-v2YcoJvm.mjs → gc-Ckw8RQHi.mjs} +763 -126
  29. package/dist/{guide-BTAlD1_y.mjs → guide-BiAbhs1J.mjs} +891 -501
  30. package/dist/{guide-status-Craq9z0t.mjs → guide-status-BAMVU0ZA.mjs} +5 -3
  31. package/dist/{ios-BcCiI4TS.mjs → ios-BL2NFckr.mjs} +43 -20
  32. package/dist/{ios-BP04pY5f.mjs → ios-BzUJWk1K.mjs} +141 -175
  33. package/dist/{ios-device-CauFJiYx.mjs → ios-device-D972i9wf.mjs} +149 -52
  34. package/dist/{ios-device-CFXA46ez.mjs → ios-device-DPsSm0v_.mjs} +3 -2
  35. package/dist/launch-verify-Q_OtW1Rf.mjs +365 -0
  36. package/dist/{logs-CakdBTtm.mjs → logs-CI9NQSK7.mjs} +18 -13
  37. package/dist/{logs-query-BGL-Pl12.mjs → logs-query-Cqma3WIu.mjs} +36 -28
  38. package/dist/{metro-C7bcJ0uo.mjs → metro-DrLHaMn2.mjs} +78 -8
  39. package/dist/{named-ports-DzMOlmaX.mjs → named-ports-CmLQSIRj.mjs} +4 -4
  40. package/dist/{ndjson-BZwI1NBv.mjs → ndjson-9uFNRwfe.mjs} +36 -3
  41. package/dist/{ownership-Cp52QAK1.mjs → ownership-D15Op8vN.mjs} +3 -3
  42. package/dist/{ownership-wolE-0Do.mjs → ownership-DCQPuWEc.mjs} +26 -7
  43. package/dist/{ports-BIbJaKNW.mjs → ports-BNKoWVWZ.mjs} +3 -3
  44. package/dist/{process-output-Dl5uKE03.mjs → process-output-Fpl8PYi8.mjs} +1 -1
  45. package/dist/{project-DDZWf_16.mjs → project-DS112DYB.mjs} +8 -3
  46. package/dist/{reload-BdypPbjM.mjs → reload-ZNJUo1pl.mjs} +12 -13
  47. package/dist/{server-bare-BQkyxa_b.mjs → server-bare-DWkkcIxV.mjs} +2 -2
  48. package/dist/{server-expo-BVAA1OqJ.mjs → server-expo-B_lctBR3.mjs} +8 -7
  49. package/dist/server-expo-CeILEu86.mjs +2 -0
  50. package/dist/settings-BKNh5rl4.mjs +376 -0
  51. package/dist/{settings-BXXorVM2.mjs → settings-D030k0t4.mjs} +464 -152
  52. package/dist/settings.schema.json +989 -0
  53. package/dist/{teardown-BJqEzPo_.mjs → sim-pool-KaIsp2Ft.mjs} +14 -419
  54. package/dist/simslim-B_LsGLuJ.mjs +259 -0
  55. package/dist/{slot-launch-ZUUe0TQP.mjs → slot-launch-DU9A-gYw.mjs} +4 -4
  56. package/dist/spawn-claims-Dn3C_N6w.mjs +49 -0
  57. package/dist/{start-CmxtOKjU.mjs → start-DYkkKF0s.mjs} +27 -31
  58. package/dist/{state-DZobzxLW.mjs → state-NcJzMEBF.mjs} +5 -3
  59. package/dist/{stats-CLg5RuDS.mjs → stats-B63-JVAY.mjs} +4 -4
  60. package/dist/{stats-BYcZ6n1h.mjs → stats-DG9e66Rz.mjs} +66 -5
  61. package/dist/status-S6ehL4Ui.mjs +932 -0
  62. package/dist/{stop-C8JW6kKp.mjs → stop-BO28HGqF.mjs} +101 -25
  63. package/dist/{stop-B1kOpYei.mjs → stop-T1R-cZRz.mjs} +2 -2
  64. package/dist/supervisor-run.mjs +52 -34
  65. package/dist/{workspace-process-lock-BYfZz_S-.mjs → workspace-process-lock-DWaiRD0e.mjs} +14 -9
  66. package/dist/{workspace-state-BT5E3SA2.mjs → workspace-state-DF2gCFhx.mjs} +52 -29
  67. package/dist/worktree-DKhSF0UX.mjs +2 -0
  68. package/dist/{worktree-CFLA-6MX.mjs → worktree-LGB-UwgG.mjs} +603 -59
  69. package/dist/{worktree-FG_ZKKdb.mjs → worktree-R_cUlE7s.mjs} +62 -41
  70. package/dist/{xcode-B6YaWeg0.mjs → xcode-BZqyxh3e.mjs} +216 -58
  71. package/package.json +5 -6
  72. package/skill/SKILL.md +2 -0
  73. package/dist/app-install-BDbsSgCl.mjs +0 -1175
  74. package/dist/build-slots-C-oAHRT1.mjs +0 -151
  75. package/dist/dev-client-BmT-ioFI.mjs +0 -797
  76. package/dist/exec-B29XNuhP.mjs +0 -77
  77. package/dist/reclaim-KJ-0uWEF.mjs +0 -361
  78. package/dist/server-expo-YDDG3eP7.mjs +0 -2
  79. package/dist/status-DdkLki47.mjs +0 -426
  80. package/dist/{metro-store-CDDZHtPl.mjs → metro-store-DzeWkfqW.mjs} +1 -1
@@ -1,14 +1,15 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { E as ownership_claim_exports, u as getConfigDir } from "./config-B7iyKWkL.mjs";
3
- import { r as register } from "./cache-manifest-CYJi8-Hx.mjs";
4
- import { l as sharedBuildCache } from "./workspace-state-BT5E3SA2.mjs";
5
- import { a as formatElapsed } from "./command-output-B3ycpsYv.mjs";
1
+ import { k as ownership_claim_exports, u as getConfigDir } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { r as register } from "./cache-manifest-Dg7OnMrp.mjs";
4
+ import { d as sharedBuildCache } from "./workspace-state-DF2gCFhx.mjs";
5
+ import { a as formatElapsed, i as formatDuration, m as shortHash } from "./command-output-gByVmIjy.mjs";
6
+ import { r as stopDeclaringSpawnsOn, t as declareSpawnsOn } from "./spawn-claims-Dn3C_N6w.mjs";
6
7
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
7
- import { createHash } from "node:crypto";
8
- import { join } from "node:path";
9
8
  import { existsSync as existsSync$1, mkdirSync as mkdirSync$1, readFileSync as readFileSync$1, rmSync as rmSync$1, writeFileSync as writeFileSync$1 } from "fs";
10
- import { join as join$1 } from "path";
9
+ import { join } from "path";
11
10
  import { buildCacheKey, resolveArtifact, storeArtifact } from "@stim-cli/core";
11
+ import { join as join$1 } from "node:path";
12
+ import { createHash } from "node:crypto";
12
13
  import * as expoFingerprint from "@expo/fingerprint";
13
14
  import { buildUploadTimeoutMs } from "@stim-cli/cache";
14
15
  const ASSET_MANIFEST_FILE = "assets-manifest.json";
@@ -34,7 +35,7 @@ function readAssetManifest(root) {
34
35
  return;
35
36
  }
36
37
  for (const name of names) {
37
- const child = join(abs, name);
38
+ const child = join$1(abs, name);
38
39
  let stat;
39
40
  try {
40
41
  stat = statSync(child);
@@ -106,16 +107,16 @@ function pickGeneratedResDir(names, variant, reactChildren = []) {
106
107
  return null;
107
108
  }
108
109
  function androidModuleDirs(root, { list = safeList } = {}) {
109
- const android = join(root, "android");
110
- return ["app", ...list(android).filter((name) => name !== "app" && existsSync(join(android, name, "build", "generated", "res"))).toSorted()];
110
+ const android = join$1(root, "android");
111
+ return ["app", ...list(android).filter((name) => name !== "app" && existsSync(join$1(android, name, "build", "generated", "res"))).toSorted()];
111
112
  }
112
113
  function findGeneratedResDir(root, variant, { list = safeList } = {}) {
113
114
  for (const module of androidModuleDirs(root, { list })) {
114
- const base = join(root, "android", module, "build", "generated", "res");
115
+ const base = join$1(root, "android", module, "build", "generated", "res");
115
116
  const names = list(base);
116
117
  if (names.length === 0) continue;
117
- const picked = pickGeneratedResDir(names, variant, names.includes("react") ? list(join(base, "react")) : []);
118
- if (picked) return join(base, ...picked.split("/"));
118
+ const picked = pickGeneratedResDir(names, variant, names.includes("react") ? list(join$1(base, "react")) : []);
119
+ if (picked) return join$1(base, ...picked.split("/"));
119
120
  }
120
121
  return null;
121
122
  }
@@ -157,7 +158,7 @@ function safeList(dir) {
157
158
  //#endregion
158
159
  //#region src/cache/build-cache.ts
159
160
  function entryDir(platform, key, root = sharedBuildCache()) {
160
- return join$1(root, platform, key);
161
+ return join(root, platform, key);
161
162
  }
162
163
  const FINGERPRINT_PLATFORMS = /* @__PURE__ */ new Set(["ios", "android"]);
163
164
  /**
@@ -209,17 +210,17 @@ function storeBuild(platform, key, buildPath, rootOrOptions = {}) {
209
210
  overwrite: Boolean(options.overwrite),
210
211
  writeMetadata: (staging) => {
211
212
  if (Array.isArray(options.sources)) try {
212
- writeFileSync$1(join$1(staging, SOURCES_FILE), JSON.stringify(options.sources));
213
+ writeFileSync$1(join(staging, SOURCES_FILE), JSON.stringify(options.sources));
213
214
  } catch {}
214
215
  if (options.assetManifest) try {
215
- writeFileSync$1(join$1(staging, ASSET_MANIFEST_FILE), JSON.stringify(options.assetManifest));
216
+ writeFileSync$1(join(staging, ASSET_MANIFEST_FILE), JSON.stringify(options.assetManifest));
216
217
  } catch {}
217
218
  }
218
219
  });
219
220
  }
220
221
  const PROVIDER_DOWNLOAD_DIR = "cache-provider";
221
222
  function providerDownloadPath(workspacePath) {
222
- return join$1(workspacePath, PROVIDER_DOWNLOAD_DIR);
223
+ return join(workspacePath, PROVIDER_DOWNLOAD_DIR);
223
224
  }
224
225
  /**
225
226
  * Empties and registers the scratch directory a provider downloads into. It is
@@ -275,7 +276,7 @@ function providerUploadOutcome(result, name) {
275
276
  const SOURCES_FILE = "fingerprint-sources.json";
276
277
  function storedSources(platform, key, root = sharedBuildCache()) {
277
278
  try {
278
- const parsed = JSON.parse(readFileSync$1(join$1(entryDir(platform, key, root), SOURCES_FILE), "utf-8"));
279
+ const parsed = JSON.parse(readFileSync$1(join(entryDir(platform, key, root), SOURCES_FILE), "utf-8"));
279
280
  return Array.isArray(parsed) ? parsed : null;
280
281
  } catch {
281
282
  return null;
@@ -283,7 +284,7 @@ function storedSources(platform, key, root = sharedBuildCache()) {
283
284
  }
284
285
  function storedAssetManifest(platform, key, root = sharedBuildCache()) {
285
286
  try {
286
- return parseAssetManifest(readFileSync$1(join$1(entryDir(platform, key, root), ASSET_MANIFEST_FILE), "utf-8"));
287
+ return parseAssetManifest(readFileSync$1(join(entryDir(platform, key, root), ASSET_MANIFEST_FILE), "utf-8"));
287
288
  } catch {
288
289
  return null;
289
290
  }
@@ -435,10 +436,10 @@ function segment(value) {
435
436
  return String(value).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "unknown";
436
437
  }
437
438
  function buildLocksDir() {
438
- return join(getConfigDir(), "build-locks");
439
+ return join$1(getConfigDir(), "build-locks");
439
440
  }
440
441
  function buildLockPath(platform, key) {
441
- return join(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX}`);
442
+ return join$1(buildLocksDir(), `${segment(platform)}-${segment(key)}${LOCK_SUFFIX}`);
442
443
  }
443
444
  function label(platform) {
444
445
  return `${platform} build`;
@@ -482,6 +483,7 @@ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
482
483
  },
483
484
  claim: attempt.acquired
484
485
  };
486
+ declareSpawnsOn(attempt.acquired);
485
487
  return reaped ? {
486
488
  ...handle,
487
489
  tookOver: toRecord(reaped)
@@ -495,6 +497,7 @@ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
495
497
  } : { path };
496
498
  }
497
499
  function releaseBuildLock(handle) {
500
+ stopDeclaringSpawnsOn(handle?.claim);
498
501
  return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
499
502
  }
500
503
  function listBuildLocks() {
@@ -508,7 +511,7 @@ function listBuildLocks() {
508
511
  const locks = [];
509
512
  for (const name of names) {
510
513
  if (!name.endsWith(LOCK_SUFFIX)) continue;
511
- const path = join(dir, name);
514
+ const path = join$1(dir, name);
512
515
  try {
513
516
  if (!statSync(path).isDirectory()) continue;
514
517
  } catch {
@@ -600,5 +603,98 @@ async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs
600
603
  await sleep(intervalMs);
601
604
  }
602
605
  }
606
+ async function waitForSharedBuild({ platform, key, fingerprint, root, logFile, command, acquire, wait, now, phase, warn, out }) {
607
+ const waitStarted = now();
608
+ let failedHolder;
609
+ let released = null;
610
+ for (;;) {
611
+ let attempt = null;
612
+ try {
613
+ attempt = acquire({
614
+ platform,
615
+ key,
616
+ root,
617
+ logFile
618
+ });
619
+ } catch (err) {
620
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
621
+ if (refusal) return { refusal };
622
+ warn(`could not take the build lock: ${err?.message || err}; building anyway`);
623
+ }
624
+ if (attempt?.acquired) {
625
+ const previous = attempt.tookOver ?? failedHolder;
626
+ if (previous) warn(takeoverLine(previous));
627
+ return {
628
+ refusal: null,
629
+ lock: attempt,
630
+ hit: null,
631
+ released
632
+ };
633
+ }
634
+ if (!attempt?.held) return {
635
+ refusal: null,
636
+ lock: null,
637
+ hit: null,
638
+ released
639
+ };
640
+ released = null;
641
+ const holder = attempt.held;
642
+ const who = holder.projectRoot || "another workspace";
643
+ phase(`${who} is already building ${shortHash(fingerprint)} (pid ${holder.pid})${holder.logFile ? ` -- tail ${holder.logFile}` : ""} -- stim guide lifecycle concurrency`);
644
+ let waited;
645
+ try {
646
+ const ceilingMs = WAIT_CEILING_MS - (now() - waitStarted);
647
+ if (ceilingMs <= 0) throw Object.assign(/* @__PURE__ */ new Error(`Waited ${formatDuration(now() - waitStarted)} for shared builds without an artifact; ${who} (pid ${holder.pid}) holds ${attempt.path}.`), {
648
+ code: "STIM_BUILD_WAIT_TIMEOUT",
649
+ lockPath: attempt.path
650
+ });
651
+ waited = await wait({
652
+ platform,
653
+ key,
654
+ out,
655
+ ceilingMs
656
+ });
657
+ } catch (err) {
658
+ const refusal = (0, ownership_claim_exports.claimFailure)(err, command);
659
+ if (refusal) return { refusal };
660
+ const timeout = err;
661
+ if (timeout?.code !== "STIM_BUILD_WAIT_TIMEOUT") throw err;
662
+ return { refusal: {
663
+ code: "STIM_BUILD_WAIT_TIMEOUT",
664
+ message: timeout.message,
665
+ remedy: `Check pid ${holder.pid}; if it is not really building, remove ${timeout.lockPath} and run \`${command}\` again.`
666
+ } };
667
+ }
668
+ if (waited.hit) {
669
+ phase(`waited ${formatDuration(waited.waitedMs)} for ${who}'s build -> installed from cache -- stim guide lifecycle concurrency`);
670
+ return {
671
+ refusal: null,
672
+ lock: null,
673
+ hit: {
674
+ path: waited.hit,
675
+ waited: {
676
+ pid: holder.pid,
677
+ ms: waited.waitedMs
678
+ }
679
+ },
680
+ released
681
+ };
682
+ }
683
+ if (waited.lockReleased) {
684
+ failedHolder = void 0;
685
+ released = {
686
+ facts: {
687
+ pid: holder.pid,
688
+ ms: waited.waitedMs
689
+ },
690
+ who
691
+ };
692
+ phase(`${who}'s build lock was released; rechecking this workspace before building`);
693
+ } else {
694
+ warn(`${who}'s build ended without an artifact (${waited.builderFailed}); retrying the build lock`);
695
+ failedHolder = holder;
696
+ }
697
+ }
698
+ }
603
699
  //#endregion
604
- export { untrackedNativeFiles as C, readAssetManifest as D, compareAssetManifests as E, untrackedMissLine as S, captureAssetManifest as T, providerUploadOutcome as _, segment as a, storeBuild as b, buildCacheKey as c, filesystemBuildCapability as d, fingerprintDiffRecord as f, providerDownloadPath as g, prepareProviderDownloadDir as h, releaseBuildLock as i, describeFingerprintMiss as l, fingerprintProject as m, acquireBuildLock as n, takeoverLine as o, fingerprintDiffSuffix as p, listBuildLocks as r, waitForBuild as s, WAIT_CEILING_MS as t, diffFingerprintSources as u, refingerprintAfterMutation as v, assetDiffReason as w, storedAssetManifest as x, resolveBuild as y };
700
+ export { assetDiffReason as C, readAssetManifest as E, untrackedNativeFiles as S, compareAssetManifests as T, refingerprintAfterMutation as _, waitForBuild as a, storedAssetManifest as b, describeFingerprintMiss as c, fingerprintDiffRecord as d, fingerprintDiffSuffix as f, providerUploadOutcome as g, providerDownloadPath as h, segment as i, diffFingerprintSources as l, prepareProviderDownloadDir as m, listBuildLocks as n, waitForSharedBuild as o, fingerprintProject as p, releaseBuildLock as r, buildCacheKey as s, acquireBuildLock as t, filesystemBuildCapability as u, resolveBuild as v, captureAssetManifest as w, untrackedMissLine as x, storeBuild as y };
@@ -0,0 +1,188 @@
1
+ import { k as ownership_claim_exports } from "./config-Cp9lM3wX.mjs";
2
+ import { a as updateWorkspaceState, t as clearWorkspaceStateKey } from "./workspace-state-DF2gCFhx.mjs";
3
+ import { a as formatElapsed, u as plural } from "./command-output-gByVmIjy.mjs";
4
+ //#region src/engine/build-progress.ts
5
+ const BUILD_PHASES = [
6
+ "prepare",
7
+ "cache-lookup",
8
+ "wait",
9
+ "prebuild",
10
+ "pods",
11
+ "compile",
12
+ "install",
13
+ "launch"
14
+ ];
15
+ const ACTIVE_BUILD_KEY = "activeBuild";
16
+ const NO_BUILD_PROGRESS = {
17
+ step: () => {},
18
+ durations: () => ({}),
19
+ clear: () => {}
20
+ };
21
+ const COLD_PHASES = [
22
+ "prebuild",
23
+ "pods",
24
+ "compile"
25
+ ];
26
+ const DEVICE_PHASES = ["install", "launch"];
27
+ function startBuildProgress({ root, platform, slot, claim, now = Date.now, note = () => {} }) {
28
+ const startedAt = new Date(now()).toISOString();
29
+ const record = {
30
+ platform,
31
+ slot,
32
+ startedAt,
33
+ phase: "prepare",
34
+ phaseStartedAt: startedAt,
35
+ phases: [{
36
+ phase: "prepare",
37
+ startedAt
38
+ }],
39
+ claim: {
40
+ root: claim.root,
41
+ path: claim.path,
42
+ claimId: claim.claimId,
43
+ pid: claim.owner.pid
44
+ }
45
+ };
46
+ let warned = false;
47
+ const guard = (fn) => {
48
+ try {
49
+ fn();
50
+ } catch (error) {
51
+ if (warned) return;
52
+ warned = true;
53
+ note(`Build progress could not be recorded in the workspace state: ${error?.message || error}`);
54
+ }
55
+ };
56
+ const write = () => guard(() => updateWorkspaceState(root, (state) => ({
57
+ ...state,
58
+ [ACTIVE_BUILD_KEY]: record
59
+ })));
60
+ write();
61
+ return {
62
+ step(phase) {
63
+ if (phase === record.phase) return;
64
+ const at = new Date(now()).toISOString();
65
+ record.phase = phase;
66
+ record.phaseStartedAt = at;
67
+ record.phases.push({
68
+ phase,
69
+ startedAt: at
70
+ });
71
+ write();
72
+ },
73
+ durations() {
74
+ return phaseDurations(record.phases, now());
75
+ },
76
+ clear() {
77
+ guard(() => {
78
+ clearWorkspaceStateKey(root, ACTIVE_BUILD_KEY, (value) => parseActiveBuild(value)?.claim.claimId === record.claim.claimId);
79
+ });
80
+ }
81
+ };
82
+ }
83
+ function phaseDurations(phases, now) {
84
+ const out = {};
85
+ phases.forEach(({ phase, startedAt }, index) => {
86
+ const start = Date.parse(startedAt);
87
+ const next = phases[index + 1];
88
+ const end = next ? Date.parse(next.startedAt) : now;
89
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
90
+ out[phase] = (out[phase] ?? 0) + Math.max(0, end - start);
91
+ });
92
+ return out;
93
+ }
94
+ function parseActiveBuild(value) {
95
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
96
+ const record = value;
97
+ const claim = record.claim;
98
+ if (record.platform !== "ios" && record.platform !== "android") return null;
99
+ if (!isPhase(record.phase)) return null;
100
+ if (typeof record.startedAt !== "string" || typeof record.phaseStartedAt !== "string") return null;
101
+ if (!claim || typeof claim.root !== "string" || typeof claim.claimId !== "string") return null;
102
+ const phases = Array.isArray(record.phases) ? record.phases.filter((entry) => isPhase(entry?.phase) && typeof entry?.startedAt === "string") : [];
103
+ return {
104
+ platform: record.platform,
105
+ slot: typeof record.slot === "string" ? record.slot : "default",
106
+ startedAt: record.startedAt,
107
+ phase: record.phase,
108
+ phaseStartedAt: record.phaseStartedAt,
109
+ phases,
110
+ claim: {
111
+ root: claim.root,
112
+ path: typeof claim.path === "string" ? claim.path : "",
113
+ claimId: claim.claimId,
114
+ pid: typeof claim.pid === "number" ? claim.pid : 0
115
+ }
116
+ };
117
+ }
118
+ function isPhase(value) {
119
+ return BUILD_PHASES.includes(value);
120
+ }
121
+ function activeBuildState(claim, survey = (0, ownership_claim_exports.readClaimSet)(claim.root)) {
122
+ if (survey.live.some((holder) => holder.claimId === claim.claimId)) return "running";
123
+ if (survey.dead.some((holder) => holder.claimId === claim.claimId)) return "stale";
124
+ return survey.unresolved.length ? "unknown" : "stale";
125
+ }
126
+ function liveOutcome(record) {
127
+ const seen = new Set(record.phases.map((entry) => entry.phase));
128
+ if (COLD_PHASES.some((phase) => seen.has(phase))) return "cold";
129
+ if (DEVICE_PHASES.some((phase) => seen.has(phase))) return "hit";
130
+ return null;
131
+ }
132
+ function median(values) {
133
+ if (!values.length) return null;
134
+ const sorted = values.toSorted((a, b) => a - b);
135
+ const mid = Math.floor(sorted.length / 2);
136
+ return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
137
+ }
138
+ function estimateBuild(history, platform, known, phase) {
139
+ const lists = history?.[platform];
140
+ const outcome = known ?? latestOutcome(lists);
141
+ const samples = outcome && lists?.[outcome] || [];
142
+ const phaseSamples = samples.flatMap((sample) => phase in sample.phases ? [sample.phases[phase]] : []);
143
+ return {
144
+ outcome,
145
+ expectedMs: median(samples.map((sample) => sample.durationMs)),
146
+ expectedPhaseMs: median(phaseSamples),
147
+ basis: samples.length
148
+ };
149
+ }
150
+ function latestOutcome(lists) {
151
+ let latest = null;
152
+ for (const outcome of ["hit", "cold"]) {
153
+ const last = lists?.[outcome]?.at(-1);
154
+ const at = last ? Date.parse(last.at) : NaN;
155
+ if (Number.isFinite(at) && (!latest || at > latest.at)) latest = {
156
+ outcome,
157
+ at
158
+ };
159
+ }
160
+ return latest?.outcome ?? null;
161
+ }
162
+ function buildReport(record, { state, history }) {
163
+ return {
164
+ platform: record.platform,
165
+ slot: record.slot,
166
+ state,
167
+ phase: record.phase,
168
+ startedAt: record.startedAt,
169
+ phaseStartedAt: record.phaseStartedAt,
170
+ ...estimateBuild(history, record.platform, liveOutcome(record), record.phase)
171
+ };
172
+ }
173
+ function remainingText(remainingMs) {
174
+ return remainingMs < 6e4 ? "under a minute left" : `about ${Math.ceil(remainingMs / 6e4)} min left`;
175
+ }
176
+ function buildStatusLine(report, now) {
177
+ const slot = report.slot === "default" ? "" : ` [${report.slot}]`;
178
+ if (report.state === "stale") return `build: stale ${report.platform}${slot} record from ${report.startedAt} (its run is gone; the next run replaces it)`;
179
+ const elapsedMs = Math.max(0, now - Date.parse(report.startedAt));
180
+ const head = `build: ${report.platform}${slot} ${report.phase}, ${formatElapsed(elapsedMs)} elapsed`;
181
+ if (report.state === "unknown") return `${head} (its native-run claim cannot be resolved, so it may not be running)`;
182
+ if (report.expectedMs === null) return head;
183
+ const basis = `median of ${plural(report.basis, `${report.outcome} run`)}`;
184
+ const remainingMs = report.expectedMs - elapsedMs;
185
+ return remainingMs > 0 ? `${head} -- ${remainingText(remainingMs)} (${basis})` : `${head} (usually ~${formatElapsed(report.expectedMs)}, ${basis})`;
186
+ }
187
+ //#endregion
188
+ export { buildStatusLine as a, buildReport as i, NO_BUILD_PROGRESS as n, parseActiveBuild as o, activeBuildState as r, startBuildProgress as s, ACTIVE_BUILD_KEY as t };
@@ -1,8 +1,8 @@
1
- import { u as getConfigDir } from "./config-B7iyKWkL.mjs";
1
+ import { u as getConfigDir } from "./config-Cp9lM3wX.mjs";
2
2
  import { existsSync } from "fs";
3
- import { homedir } from "os";
4
- import { join, resolve } from "path";
3
+ import { isAbsolute, join, resolve } from "path";
5
4
  import { readCacheManifest, updateCacheManifest } from "@stim-cli/core";
5
+ import { homedir } from "os";
6
6
  //#region src/cache/cache-manifest.ts
7
7
  function manifestPath() {
8
8
  return join(getConfigDir(), "caches.json");
@@ -53,7 +53,7 @@ function unregister(dir, path = manifestPath()) {
53
53
  return removed;
54
54
  }
55
55
  function registeredCaches(path = manifestPath()) {
56
- return readManifest(path).caches.filter((c) => c.dir && existsSync(c.dir)).map((c) => ({
56
+ return readManifest(path).caches.filter((c) => isAbsolute(c.dir) && existsSync(c.dir)).map((c) => ({
57
57
  name: c.name,
58
58
  dir: c.dir,
59
59
  prune: c.prune === "atomic" ? "atomic" : "entries",
@@ -1,2 +1,2 @@
1
- import { a as unregister, i as registeredCaches, n as readManifest, r as register, t as manifestPath } from "./cache-manifest-CYJi8-Hx.mjs";
1
+ import { a as unregister, i as registeredCaches, n as readManifest, r as register, t as manifestPath } from "./cache-manifest-Dg7OnMrp.mjs";
2
2
  export { manifestPath, readManifest, register, registeredCaches, unregister };
package/dist/cli.mjs CHANGED
@@ -1,27 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import { _ as refuseRelativeStimPaths } from "./config-Cp9lM3wX.mjs";
2
3
  import { readFileSync } from "node:fs";
3
4
  import { Command } from "commander";
4
5
  //#region bin/cli.ts
5
6
  const commands = /* @__PURE__ */ new Map([
6
- ["doctor", () => import("./doctor-BMt156qe.mjs")],
7
- ["worktree", () => import("./worktree-CFLA-6MX.mjs")],
8
- ["start", () => import("./start-CmxtOKjU.mjs")],
9
- ["ports", () => import("./ports-BIbJaKNW.mjs")],
10
- ["stop", () => import("./stop-B1kOpYei.mjs")],
11
- ["ios", () => import("./ios-BP04pY5f.mjs")],
12
- ["android", () => import("./android-BRgBYzU0.mjs")],
13
- ["reload", () => import("./reload-BdypPbjM.mjs")],
14
- ["device", () => import("./device-RdujjHLB.mjs")],
15
- ["logs", () => import("./logs-CakdBTtm.mjs")],
16
- ["status", () => import("./status-DdkLki47.mjs")],
17
- ["stats", () => import("./stats-CLg5RuDS.mjs")],
18
- ["gc", () => import("./gc-v2YcoJvm.mjs")],
19
- ["guide", () => import("./guide-BTAlD1_y.mjs")]
7
+ ["doctor", () => import("./doctor-B_Ei3bZu.mjs")],
8
+ ["worktree", () => import("./worktree-DKhSF0UX.mjs")],
9
+ ["start", () => import("./start-DYkkKF0s.mjs")],
10
+ ["ports", () => import("./ports-BNKoWVWZ.mjs")],
11
+ ["stop", () => import("./stop-T1R-cZRz.mjs")],
12
+ ["ios", () => import("./ios-BzUJWk1K.mjs")],
13
+ ["android", () => import("./android-BPGizezh.mjs")],
14
+ ["reload", () => import("./reload-ZNJUo1pl.mjs")],
15
+ ["device", () => import("./device-D4U-xLty.mjs")],
16
+ ["logs", () => import("./logs-CI9NQSK7.mjs")],
17
+ ["settings", () => import("./settings-BKNh5rl4.mjs")],
18
+ ["status", () => import("./status-S6ehL4Ui.mjs")],
19
+ ["stats", () => import("./stats-B63-JVAY.mjs")],
20
+ ["gc", () => import("./gc-Ckw8RQHi.mjs")],
21
+ ["guide", () => import("./guide-BiAbhs1J.mjs")]
20
22
  ]);
21
23
  const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
22
24
  const program = new Command();
23
25
  program.name("stim").description("Isolated React Native dev environments per project/worktree").version(pkg.version);
24
26
  try {
27
+ refuseRelativeStimPaths();
25
28
  const first = process.argv[2];
26
29
  const loadCommand = commands.get((first === "help" ? process.argv[3] : first) ?? "");
27
30
  if (loadCommand) (await loadCommand()).default(program, pkg.version);
@@ -31,11 +34,11 @@ try {
31
34
  }
32
35
  await program.parseAsync();
33
36
  } catch (err) {
34
- if (err?.code === "STIM_CONFIG_CORRUPT") {
35
- console.error(err.message);
36
- process.exit(1);
37
- }
38
- throw err;
37
+ const code = err?.code;
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ if (typeof code === "string" && code.startsWith("STIM_")) console.error(`${code}: ${message}`);
40
+ else console.error(`Unexpected error: ${message.replace(/\.$/, "")}. Report it at ${pkg.bugs.url}`);
41
+ process.exit(1);
39
42
  }
40
43
  //#endregion
41
44
  export {};
@@ -12,6 +12,8 @@ interface ExecOptions {
12
12
  interface Executor {
13
13
  run(cmd: string, opts?: ExecOptions): string;
14
14
  runFile(file: string, args?: string[], opts?: ExecOptions): string;
15
+ /** `runFile` without blocking the event loop; rejects where `runFile` throws. */
16
+ runFileAsync(file: string, args?: string[], opts?: ExecOptions): Promise<string>;
15
17
  runQuiet(cmd: string, opts?: ExecOptions): string | null;
16
18
  runFileQuiet(file: string, args?: string[], opts?: ExecOptions): string | null;
17
19
  spawn(cmd: string, args?: readonly string[], opts?: SpawnOptions): ChildProcess;
@@ -1,12 +1,13 @@
1
- import { t as getExecutor } from "./exec-B29XNuhP.mjs";
2
- import { B as validateDeviceSlot, F as deviceSlotKey, w as process_identity_exports } from "./config-B7iyKWkL.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
4
- import { _ as workspaceLogsDir } from "./workspace-state-BT5E3SA2.mjs";
5
- import { t as createLineReader } from "./process-output-Dl5uKE03.mjs";
6
- import { i as startIosDeviceConsole, r as parseDeviceConsoleLine } from "./ios-device-CFXA46ez.mjs";
7
- import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-Cp52QAK1.mjs";
8
- import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-DeYdjjBP.mjs";
1
+ import { D as process_identity_exports, H as deviceSlotKey, J as validateDeviceSlot } from "./config-Cp9lM3wX.mjs";
2
+ import { t as getExecutor } from "./exec-jIAlgilk.mjs";
3
+ import { r as createNdjsonWriter } from "./ndjson-9uFNRwfe.mjs";
4
+ import { b as workspaceLogsDir } from "./workspace-state-DF2gCFhx.mjs";
5
+ import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
6
+ import { a as startIosDeviceConsole, i as parseDeviceConsoleLine } from "./ios-device-DPsSm0v_.mjs";
7
+ import { a as unregisterCollector, i as registerCollector, r as readCollectors, t as collectorProcessTitle } from "./ownership-D15Op8vN.mjs";
8
+ import { a as watchAppPid, i as waitForAppPid, n as parseLogcatLine, r as startAndroidLogcat, t as androidClockOffset } from "./android-DEgo4oCo.mjs";
9
9
  import { existsSync } from "node:fs";
10
+ import { LOG_ROTATE_BYTES } from "@stim-cli/core";
10
11
  import { isAbsolute, join, resolve } from "node:path";
11
12
  import { fileURLToPath } from "node:url";
12
13
  //#region src/collector/ios.ts
@@ -299,7 +300,7 @@ async function runCollector({ slot = "default", platform, root, udid = null, app
299
300
  slot,
300
301
  deviceId: udid ?? serial
301
302
  };
302
- const writer = createNdjsonWriter(join(workspaceLogsDir(root), "device.ndjson"));
303
+ const writer = createNdjsonWriter(join(workspaceLogsDir(root), "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
303
304
  const startedAt = new Date(now()).toISOString();
304
305
  const processToken = (0, process_identity_exports.captureProcessToken)(process.pid);
305
306
  if (!processToken) {
@@ -1,5 +1,7 @@
1
- import { n as stripAnsi } from "./process-output-Dl5uKE03.mjs";
1
+ import { t as NO_PROJECT_REFUSAL } from "./project-DS112DYB.mjs";
2
+ import { n as stripAnsi } from "./process-output-Fpl8PYi8.mjs";
2
3
  import { quotedPath } from "@stim-cli/core";
4
+ import chalk from "chalk";
3
5
  //#region src/diagnostics/launch-error-preview.ts
4
6
  function framePriority(frame, root) {
5
7
  const normalized = frame.replaceAll("\\", "/");
@@ -245,6 +247,13 @@ function releasedLeaseFact(lease) {
245
247
  function phaseLine(label, text) {
246
248
  return ` ${String(label).padEnd(LABEL_WIDTH)} ${text}`;
247
249
  }
250
+ function refuseNoProject({ json }) {
251
+ const { code, message, remedy } = NO_PROJECT_REFUSAL;
252
+ console.error(chalk.red(phaseLine("error", `${code}: ${message}`)));
253
+ console.error(phaseLine("remedy", remedy));
254
+ if (json) console.log(JSON.stringify(NO_PROJECT_REFUSAL));
255
+ process.exitCode = 1;
256
+ }
248
257
  function shortUdid(udid) {
249
258
  const text = String(udid ?? "");
250
259
  return text.length > 4 ? `${text.slice(0, 4)}..` : text;
@@ -283,4 +292,4 @@ function stepTimer(now = Date.now) {
283
292
  return () => `(${formatDuration(elapsed())})`;
284
293
  }
285
294
  //#endregion
286
- export { launchErrorPreview as _, formatElapsed as a, launchErrorReport as c, quotedPath as d, releasedLeaseFact as f, stepTimer as g, stepClock as h, formatDuration as i, phaseLine as l, shortUdid as m, appReadinessMessage as n, formatLongDuration as o, shortHash as p, clockTime as r, isAppLaunchError as s, SLOW_STEP_MS as t, plural as u };
295
+ export { stepTimer as _, formatElapsed as a, launchErrorReport as c, quotedPath as d, refuseNoProject as f, stepClock as g, shortUdid as h, formatDuration as i, phaseLine as l, shortHash as m, appReadinessMessage as n, formatLongDuration as o, releasedLeaseFact as p, clockTime as r, isAppLaunchError as s, SLOW_STEP_MS as t, plural as u, launchErrorPreview as v };