stim 1.3.1 → 1.5.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 (67) hide show
  1. package/README.md +16 -3
  2. package/dist/{android-hMj83VpN.mjs → android-DJWiTBkz.mjs} +65 -72
  3. package/dist/{android-cas-DXorPCi_.mjs → android-cas-CSS8gjR7.mjs} +15 -12
  4. package/dist/{android-CT-PkXUh.mjs → android-sobvO9ia.mjs} +197 -114
  5. package/dist/{app-install-DIp9riNx.mjs → app-install--PAocWvq.mjs} +2 -2
  6. package/dist/{build-slots-C4AouyNz.mjs → build-slots-CuPszJDu.mjs} +7 -8
  7. package/dist/{cache-manifest-CXR-Y8pH.mjs → cache-manifest-uoOKhgUD.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +14 -13
  10. package/dist/collector-run.mjs +4 -4
  11. package/dist/{command-output-8wHMEde0.mjs → command-output-BuIb_ZOI.mjs} +1 -4
  12. package/dist/{config-CcefMY2Q.mjs → config-DMP7L7H6.mjs} +52 -52
  13. package/dist/dependency-state-CTL8A9p3.mjs +76 -0
  14. package/dist/{deps-0Yt0Inly.mjs → deps-BAGOnrc7.mjs} +1 -1
  15. package/dist/{dev-client-VfSZNLT4.mjs → dev-client-BoifdqkU.mjs} +12 -12
  16. package/dist/{device-Bbv45Z9Z.mjs → device-BUw4JBuH.mjs} +7 -7
  17. package/dist/{xcode-xnxyvJkD.mjs → device-DvKJ454A.mjs} +208 -967
  18. package/dist/{device-lease-CJwK7OZr.mjs → device-lease-r7yEQ1R8.mjs} +3 -3
  19. package/dist/{device-pool-CcBNTvM7.mjs → device-pool-DU4FQKp5.mjs} +4 -4
  20. package/dist/{device-remote-DAA0Zm6Z.mjs → device-remote-BTUqbl0i.mjs} +7 -6
  21. package/dist/{doctor-B4EITgQB.mjs → doctor-Di_B26uh.mjs} +396 -117
  22. package/dist/{error-diagnostics-DwhNBFDU.mjs → error-diagnostics-CTePWAzW.mjs} +7 -6
  23. package/dist/{gc-BRu62v4o.mjs → gc-C20dWw_8.mjs} +61 -27
  24. package/dist/{guide-D95E0u3i.mjs → guide-sAnoty2y.mjs} +151 -20
  25. package/dist/guide-status-BF6RsJJp.mjs +271 -0
  26. package/dist/{ios-DAju3TVy.mjs → ios-BdpkncL6.mjs} +2667 -2607
  27. package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
  28. package/dist/{logs-CAdwW6c2.mjs → logs-BfQdIhiF.mjs} +5 -5
  29. package/dist/metro-ato6ef3J.mjs +99 -0
  30. package/dist/named-ports-DH9SXd0f.mjs +137 -0
  31. package/dist/{ownership-8O073GIC.mjs → ownership-CD_XnpDu.mjs} +5 -5
  32. package/dist/{ownership-DXcol4oU.mjs → ownership-CVY9cqCr.mjs} +6 -6
  33. package/dist/ports-A6uTJSPj.mjs +39 -0
  34. package/dist/ports-BZprTJRa.mjs +90 -0
  35. package/dist/{project-fKmuIaMI.mjs → project-B_SDt4ip.mjs} +2 -2
  36. package/dist/{reclaim-CUmwucWG.mjs → reclaim-C_rB25f4.mjs} +20 -13
  37. package/dist/{reload-CWpXoOgN.mjs → reload-CYBJbJfI.mjs} +8 -8
  38. package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
  39. package/dist/{server-bare-DEzPZLvf.mjs → server-bare-H3pAhbMs.mjs} +11 -35
  40. package/dist/server-expo-CSwUVYOa.mjs +2 -0
  41. package/dist/{server-expo-DizT4OMk.mjs → server-expo-Ds8_mfng.mjs} +6 -14
  42. package/dist/{settings-DEJf6_kJ.mjs → settings-kO2ZqIKy.mjs} +2 -2
  43. package/dist/{slot-launch-CZklomj6.mjs → slot-launch-BpHiHH6A.mjs} +4 -4
  44. package/dist/{start-B5G4NjiA.mjs → start-DiEfqEWk.mjs} +20 -22
  45. package/dist/state-CnWzvCWP.mjs +131 -0
  46. package/dist/{stats-CaaQr4yN.mjs → stats-DHrO0frH.mjs} +4 -4
  47. package/dist/{stats-BR-kwj0B.mjs → stats-Fo9ACIv7.mjs} +1 -1
  48. package/dist/{status-CI9clRSb.mjs → status-J5LAXpag.mjs} +15 -12
  49. package/dist/{stop-CjlgDk0a.mjs → stop-0jJNahwN.mjs} +15 -14
  50. package/dist/{stop-CQhffpmX.mjs → stop-CM1fejKW.mjs} +2 -2
  51. package/dist/supervisor-run.d.mts +7 -28
  52. package/dist/supervisor-run.mjs +20 -11
  53. package/dist/{teardown--gESs9ck.mjs → teardown-CAVcWeRD.mjs} +148 -109
  54. package/dist/{workspace-process-lock-IBj1Towr.mjs → workspace-process-lock-Bz19_R9R.mjs} +4 -4
  55. package/dist/workspace-state-WVwWw_hB.mjs +146 -0
  56. package/dist/{worktree-DtQGO9-s.mjs → worktree-CeUKDAQw.mjs} +38 -34
  57. package/dist/{worktree-Eq6TLH8N.mjs → worktree-DJnBbQZ9.mjs} +79 -22
  58. package/dist/xcode-BqDeRkpT.mjs +860 -0
  59. package/package.json +7 -7
  60. package/shim/expo-metro-config.cjs +11 -17
  61. package/dist/doctor-BueDfLYG.mjs +0 -530
  62. package/dist/metro-CORqXyTL.mjs +0 -184
  63. package/dist/ownership-claim-BtPtJVmq.mjs +0 -584
  64. package/dist/server-expo-44em5Pmg.mjs +0 -2
  65. package/dist/state-B3XA2wp2.mjs +0 -268
  66. package/shim/metro-cache-generation.cjs +0 -20
  67. package/shim/metro-cache-generation.d.cts +0 -5
package/README.md CHANGED
@@ -50,9 +50,9 @@ registered descendant app with logs when one exists.
50
50
 
51
51
  Use `stim start --reset-cache` to recover from stale Metro transforms or file-map
52
52
  state. It restarts only this app's verified owned Metro, keeping its port and
53
- devices. A fresh persistent cache namespace bypasses old entries without deleting
54
- shared stores or changing other apps or native build caches. Expo requires SDK
55
- 54+ and Stim's config adapter. See `stim guide lifecycle` for scope and limitations.
53
+ devices, with Metro's own reset: every store in the app's Metro config is cleared,
54
+ including this app's shared transform store, so other worktrees of the same app
55
+ rebuild their transforms too. Other apps and native build caches are unchanged.
56
56
 
57
57
  Use `stim doctor --platform ios` or `stim doctor --platform android` when only
58
58
  one native platform is in scope; shared project checks still run. Doctor also
@@ -198,3 +198,16 @@ stim stop --slot tablet
198
198
  `--slot` preserves the shared server and other devices. `stim reload ios` or
199
199
  `stim reload android` can reload multiple connected devices. Read
200
200
  `stim guide lifecycle options` for launch verification and recycling behavior.
201
+
202
+ ### Named server ports
203
+
204
+ Use `stim ports get web` to reserve a workspace port for a server Stim does not
205
+ manage, then pass that number to the server, for example
206
+ `pnpm exec vite --port "$(stim ports get web)" --strictPort`.
207
+ `stim ports` lists named allocations and managed Metro. `stim ports stop web`
208
+ stops the TCP listener and releases its port; `--dry-run` previews the action.
209
+ `stim ports release web` releases without stopping. Omitting the label selects
210
+ all named ports. These commands leave Metro alone, and `stim stop` leaves
211
+ named ports alone. Worktree removal and `gc --delete` stop and release named
212
+ ports for removed workspaces. Read `stim guide ports` for the reserved band,
213
+ listener ownership, and allocation limits.
@@ -1,18 +1,17 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
- import { r as register } from "./cache-manifest-CXR-Y8pH.mjs";
4
- import { b as sharedBuildCache } from "./state-B3XA2wp2.mjs";
5
- import { r as pidExists } from "./metro-CORqXyTL.mjs";
6
- import { a as formatElapsed } from "./command-output-8wHMEde0.mjs";
7
- import { d as releaseClaim, i as claimRemoveCommand, m as tryAcquireClaim, t as ClaimRefusedError, u as readClaimSet } from "./ownership-claim-BtPtJVmq.mjs";
8
- import { c as androidDataPartitionSizeBytes } from "./settings-DEJf6_kJ.mjs";
2
+ import { E as ownership_claim_exports, u as getConfigDir } from "./config-DMP7L7H6.mjs";
3
+ import { r as register } from "./cache-manifest-uoOKhgUD.mjs";
4
+ import { l as sharedBuildCache } from "./workspace-state-WVwWw_hB.mjs";
5
+ import { r as pidExists } from "./metro-ato6ef3J.mjs";
6
+ import { a as formatElapsed } from "./command-output-BuIb_ZOI.mjs";
7
+ import { c as androidDataPartitionSizeBytes } from "./settings-kO2ZqIKy.mjs";
9
8
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
10
9
  import { createHash } from "node:crypto";
11
10
  import { join } from "node:path";
12
- import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, utimesSync, writeFileSync as writeFileSync$1 } from "fs";
11
+ import { existsSync as existsSync$1, lstatSync as lstatSync$1, mkdirSync as mkdirSync$1, openSync as openSync$1, readFileSync as readFileSync$1, readdirSync as readdirSync$1, realpathSync as realpathSync$1, renameSync as renameSync$1, rmSync as rmSync$1, statSync as statSync$1, writeFileSync as writeFileSync$1 } from "fs";
13
12
  import { homedir } from "os";
14
- import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1 } from "path";
15
- import { buildCacheKey } from "@stim-cli/core";
13
+ import { dirname as dirname$1, isAbsolute as isAbsolute$1, join as join$1, resolve as resolve$1 } from "path";
14
+ import { buildCacheKey, resolveArtifact, storeArtifact } from "@stim-cli/core";
16
15
  import * as expoFingerprint from "@expo/fingerprint";
17
16
  import { buildUploadTimeoutMs } from "@stim-cli/cache";
18
17
  const ASSET_MANIFEST_FILE = "assets-manifest.json";
@@ -163,16 +162,6 @@ function safeList$1(dir) {
163
162
  function entryDir(platform, key, root = sharedBuildCache()) {
164
163
  return join$1(root, platform, key);
165
164
  }
166
- function artifactIn(dir) {
167
- if (!existsSync$1(dir)) return null;
168
- let found;
169
- try {
170
- found = readdirSync$1(dir).find((f) => f.endsWith(".app") || f.endsWith(".apk"));
171
- } catch {
172
- return null;
173
- }
174
- return found ? join$1(dir, found) : null;
175
- }
176
165
  const FINGERPRINT_PLATFORMS = /* @__PURE__ */ new Set(["ios", "android"]);
177
166
  /**
178
167
  * Paths that exist in a working checkout and not in a fresh one, which
@@ -211,55 +200,25 @@ function registerOnce(root) {
211
200
  } catch {}
212
201
  }
213
202
  function resolveBuild(platform, key, root = sharedBuildCache()) {
214
- const hit = artifactIn(entryDir(platform, key, root));
215
- if (!hit) return null;
216
- try {
217
- utimesSync(dirname$1(hit), /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
218
- } catch {}
219
- return hit;
203
+ return resolveArtifact(entryDir(platform, key, root));
220
204
  }
221
205
  function storeBuild(platform, key, buildPath, rootOrOptions = {}) {
222
206
  const options = typeof rootOrOptions === "string" ? { root: rootOrOptions } : rootOrOptions || {};
223
207
  const root = options.root || sharedBuildCache();
224
- const overwrite = Boolean(options.overwrite);
225
208
  if (!buildPath || !existsSync$1(buildPath)) throw new Error(`No build to store at ${buildPath}`);
226
209
  registerOnce(root);
227
- const dest = entryDir(platform, key, root);
228
- const existing = artifactIn(dest);
229
- if (existing && !overwrite) return existing;
230
- const staging = `${dest}.staging-${process.pid}`;
231
- rmSync$1(staging, {
232
- recursive: true,
233
- force: true
234
- });
235
- mkdirSync$1(staging, { recursive: true });
236
- try {
237
- getExecutor().runFile("cp", [
238
- "-c",
239
- "-R",
240
- buildPath,
241
- join$1(staging, basename$1(buildPath))
242
- ]);
243
- } catch {
244
- getExecutor().runFile("cp", [
245
- "-R",
246
- buildPath,
247
- join$1(staging, basename$1(buildPath))
248
- ]);
249
- }
250
- if (Array.isArray(options.sources)) try {
251
- writeFileSync$1(join$1(staging, SOURCES_FILE), JSON.stringify(options.sources));
252
- } catch {}
253
- if (options.assetManifest) try {
254
- writeFileSync$1(join$1(staging, ASSET_MANIFEST_FILE), JSON.stringify(options.assetManifest));
255
- } catch {}
256
- mkdirSync$1(dirname$1(dest), { recursive: true });
257
- rmSync$1(dest, {
258
- recursive: true,
259
- force: true
210
+ return storeArtifact(entryDir(platform, key, root), buildPath, {
211
+ runFile: getExecutor().runFile,
212
+ overwrite: Boolean(options.overwrite),
213
+ writeMetadata: (staging) => {
214
+ if (Array.isArray(options.sources)) try {
215
+ writeFileSync$1(join$1(staging, SOURCES_FILE), JSON.stringify(options.sources));
216
+ } catch {}
217
+ if (options.assetManifest) try {
218
+ writeFileSync$1(join$1(staging, ASSET_MANIFEST_FILE), JSON.stringify(options.assetManifest));
219
+ } catch {}
220
+ }
260
221
  });
261
- renameSync$1(staging, dest);
262
- return artifactIn(dest);
263
222
  }
264
223
  const PROVIDER_DOWNLOAD_DIR = "cache-provider";
265
224
  function providerDownloadPath(workspacePath) {
@@ -498,13 +457,13 @@ function toRecord(holder) {
498
457
  }
499
458
  function readBuildLock(pathOrSpec) {
500
459
  const path = typeof pathOrSpec === "string" ? pathOrSpec : buildLockPath(pathOrSpec.platform, pathOrSpec.key);
501
- const survey = readClaimSet(path);
460
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
502
461
  const holder = survey.live[0] ?? survey.dead[0];
503
462
  return holder ? toRecord(holder) : null;
504
463
  }
505
464
  function acquireBuildLock({ platform, key, root = null, logFile = null }) {
506
465
  const path = buildLockPath(platform, key);
507
- const attempt = tryAcquireClaim({
466
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
508
467
  root: path,
509
468
  mode: "exclusive",
510
469
  label: label(platform),
@@ -531,7 +490,7 @@ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
531
490
  tookOver: toRecord(reaped)
532
491
  } : handle;
533
492
  }
534
- if (attempt.pending) releaseClaim(attempt.pending);
493
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
535
494
  const holder = attempt.held ?? attempt.waitingFor?.[0];
536
495
  return holder ? {
537
496
  held: toRecord(holder),
@@ -539,7 +498,7 @@ function acquireBuildLock({ platform, key, root = null, logFile = null }) {
539
498
  } : { path };
540
499
  }
541
500
  function releaseBuildLock(handle) {
542
- return releaseClaim(handle?.claim);
501
+ return (0, ownership_claim_exports.releaseClaim)(handle?.claim);
543
502
  }
544
503
  function listBuildLocks() {
545
504
  const dir = buildLocksDir();
@@ -560,7 +519,7 @@ function listBuildLocks() {
560
519
  }
561
520
  const stem = name.slice(0, -5);
562
521
  const cut = stem.indexOf("-");
563
- const survey = readClaimSet(path);
522
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
564
523
  const holder = survey.live[0] ?? survey.dead[0];
565
524
  const info = holder ? toRecord(holder) : null;
566
525
  locks.push({
@@ -601,9 +560,9 @@ async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs
601
560
  hit,
602
561
  waitedMs: now() - started
603
562
  };
604
- const survey = readClaimSet(path);
563
+ const survey = (0, ownership_claim_exports.readClaimSet)(path);
605
564
  const unresolved = survey.unresolved[0];
606
- if (unresolved) throw new ClaimRefusedError({
565
+ if (unresolved) throw new ownership_claim_exports.ClaimRefusedError({
607
566
  claimPath: unresolved.path,
608
567
  root: path,
609
568
  label: label(platform),
@@ -626,7 +585,7 @@ async function waitForBuild({ platform, key, resolve = resolveBuild, intervalMs
626
585
  }
627
586
  const elapsed = now() - started;
628
587
  if (elapsed >= ceilingMs) {
629
- const err = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsed)} for ${holder?.projectRoot || "another workspace"}'s ${platform} build of ${key} without an artifact, and pid ${holder?.pid} is still the live holder. The lock is ${path}; remove it if that process is not really building:\n ${claimRemoveCommand(path)}`);
588
+ const err = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsed)} for ${holder?.projectRoot || "another workspace"}'s ${platform} build of ${key} without an artifact, and pid ${holder?.pid} is still the live holder. The lock is ${path}; remove it if that process is not really building:\n ${(0, ownership_claim_exports.claimRemoveCommand)(path)}`);
630
589
  err.code = "STIM_BUILD_WAIT_TIMEOUT";
631
590
  err.lockPath = path;
632
591
  err.holder = holder ?? void 0;
@@ -750,14 +709,48 @@ function pickDefaultSystemImage(images, { systemImage, hostArch } = {}) {
750
709
  if (matching.length === 0) return null;
751
710
  return matching.toSorted((a, b) => pageSizeRank(a) - pageSizeRank(b) || b.api - a.api || (b.tag === "google_apis" ? 1 : 0) - (a.tag === "google_apis" ? 1 : 0))[0] ?? null;
752
711
  }
753
- function createOwnedAvd(label, { systemImage } = {}) {
712
+ async function createOwnedAvd(label, { systemImage, spawn = (...args) => getExecutor().spawn(...args) } = {}) {
754
713
  const pick = pickDefaultSystemImage(listInstalledSystemImages(), { systemImage });
755
714
  if (!pick) {
756
715
  const arch = hostSystemImageArch();
757
716
  throw new Error(`No ${arch} Android system image is installed. Install one, e.g.: sdkmanager "system-images;android-36;google_apis;${arch}"`);
758
717
  }
759
718
  const avdName = ownedAvdName(label);
760
- getExecutor().run(`echo no | ${androidTool("avdmanager")} create avd -n "${avdName}" -k "${pick.pkg}" --device "${DEFAULT_AVD_DEVICE}"`);
719
+ const tool = androidToolPath("avdmanager");
720
+ const args = [
721
+ "create",
722
+ "avd",
723
+ "-n",
724
+ avdName,
725
+ "-k",
726
+ pick.pkg,
727
+ "--device",
728
+ DEFAULT_AVD_DEVICE
729
+ ];
730
+ const child = spawn(tool, args, {
731
+ detached: true,
732
+ stdio: [
733
+ "pipe",
734
+ "pipe",
735
+ "pipe"
736
+ ]
737
+ });
738
+ let stderr = "";
739
+ child.stdout?.on("data", () => {});
740
+ child.stderr?.on("data", (chunk) => {
741
+ stderr = (stderr + String(chunk)).slice(-67108864);
742
+ });
743
+ const completed = new Promise((finish, reject) => {
744
+ child.once("error", reject);
745
+ child.once("close", (code, signal) => finish({
746
+ code,
747
+ signal
748
+ }));
749
+ });
750
+ child.stdin?.on("error", () => {});
751
+ child.stdin?.end("no\n");
752
+ const result = await completed;
753
+ if (result.code !== 0) throw new Error(`Command failed: ${tool} ${args.join(" ")} (${result.signal ?? result.code})\n${stderr.trim()}`);
761
754
  return {
762
755
  avdName,
763
756
  systemImage: pick.pkg
@@ -1,11 +1,11 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
3
- import { r as register } from "./cache-manifest-CXR-Y8pH.mjs";
4
- import { D as workspaceLogsDir, x as sharedCcache, y as ensureWorkspaceStorage } from "./state-B3XA2wp2.mjs";
2
+ import { D as withDirLock, u as getConfigDir } from "./config-DMP7L7H6.mjs";
3
+ import { r as register } from "./cache-manifest-uoOKhgUD.mjs";
4
+ import { _ as workspaceLogsDir, c as ensureWorkspaceStorage, u as sharedCcache } from "./workspace-state-WVwWw_hB.mjs";
5
5
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
- import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
7
- import { n as androidHome } from "./android-hMj83VpN.mjs";
8
- import { f as startBuildHeartbeat, r as HEARTBEAT_INTERVAL_MS } from "./xcode-xnxyvJkD.mjs";
6
+ import { l as phaseLine } from "./command-output-BuIb_ZOI.mjs";
7
+ import { n as androidHome } from "./android-DJWiTBkz.mjs";
8
+ import { d as startBuildHeartbeat, n as HEARTBEAT_INTERVAL_MS } from "./xcode-BqDeRkpT.mjs";
9
9
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
10
10
  import { createHash, randomUUID } from "node:crypto";
11
11
  import { dirname, isAbsolute, join, relative } from "node:path";
@@ -717,7 +717,10 @@ function gradleArgs(task, { buildCache = true, abi = null } = {}) {
717
717
  async function buildAndroid({ root, logWriter, variant = null, abi = null }, { spawnFn = null, now = Date.now, env = process.env, buildCache = true, ccache = null, cas = null, pch = "auto", compilerCacheDisabled = false, heartbeatMs = HEARTBEAT_INTERVAL_MS, estimateMs = null, onHeartbeat = (line) => console.error(line), onNote = (line) => console.error(line) } = {}) {
718
718
  const project = discoverAndroidProject(root);
719
719
  if (project.failed) return {
720
- ...project,
720
+ ok: false,
721
+ code: project.code,
722
+ reason: project.reason,
723
+ remedy: project.remedy,
721
724
  diagnostics: [],
722
725
  truncated: 0,
723
726
  lastLines: [],
@@ -730,7 +733,7 @@ async function buildAndroid({ root, logWriter, variant = null, abi = null }, { s
730
733
  hasLocalProperties: existsSync(join(project.androidDir, "local.properties"))
731
734
  });
732
735
  if (refusal) return {
733
- failed: true,
736
+ ok: false,
734
737
  ...refusal,
735
738
  diagnostics: [],
736
739
  truncated: 0,
@@ -841,7 +844,7 @@ async function buildAndroid({ root, logWriter, variant = null, abi = null }, { s
841
844
  const how = result.signal ? `signal ${result.signal}` : `exit code ${result.code}`;
842
845
  const { shown, truncated } = capDiagnostics(extractGradleDiagnostics(transcript));
843
846
  return {
844
- failed: true,
847
+ ok: false,
845
848
  code: BUILD_ERROR,
846
849
  reason: `\`./gradlew ${task}\` failed (${how}).`,
847
850
  diagnostics: shown,
@@ -853,7 +856,7 @@ async function buildAndroid({ root, logWriter, variant = null, abi = null }, { s
853
856
  }
854
857
  const located = locateApk(root, transcript, variant);
855
858
  if (!located.apkPath && located.candidates?.length) return {
856
- failed: true,
859
+ ok: false,
857
860
  code: BUILD_ERROR,
858
861
  reason: `\`./gradlew ${task}\` left ${located.candidates.length} debug APKs under ${apkOutputsDir(root)}, and nothing says which flavor to install.`,
859
862
  remedy: `Set the android.variant setting to the variant to install -- e.g. {"android": {"variant": "${variantNameOf(relative(apkOutputsDir(root), located.candidates[0]).split("/").slice(0, -1))}"}} in .stim.json.`,
@@ -864,7 +867,7 @@ async function buildAndroid({ root, logWriter, variant = null, abi = null }, { s
864
867
  ccache: ccacheActivity
865
868
  };
866
869
  if (!located.apkPath) return {
867
- failed: true,
870
+ ok: false,
868
871
  code: BUILD_ERROR,
869
872
  reason: variant ? `\`./gradlew ${task}\` succeeded but produced no APK under ${apkOutputsDir(root)} for variant "${variant}".` : `\`./gradlew ${task}\` succeeded but produced no APK in ${debugApkDir(root)}.`,
870
873
  remedy: variant ? `Check that the android.variant setting ("${variant}") names a real variant (\`./gradlew :app:tasks\` lists the assemble tasks).` : `Check that ${task} builds the app module (\`./gradlew :app:${task}\` in android/) and that no flavour redirects the output.`,
@@ -894,7 +897,7 @@ function spawnFailure(err, project, durationMs) {
894
897
  const message = String(nodeErr?.message || err || "");
895
898
  const permissionDenied = nodeErr?.code === "EACCES" || /EACCES|permission denied/i.test(message);
896
899
  return {
897
- failed: true,
900
+ ok: false,
898
901
  code: BUILD_ERROR,
899
902
  reason: `Could not run ${project.gradlew}: ${message}`,
900
903
  remedy: permissionDenied ? `Make the wrapper executable: \`chmod +x ${project.gradlew}\`.` : "Check that the gradle wrapper is intact (android/gradlew and android/gradle/wrapper/) and that JAVA_HOME points at a JDK 17 install.",