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
@@ -1,19 +1,18 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { A as directorySize, M as isOnMountedVolume, N as listMountedVolumes, P as volumeRootFor, g as loadConfig, i as clearDevice, j as formatBytes, u as getConfigDir, v as removeProject, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
3
- import { i as registeredCaches } from "./cache-manifest-CXR-Y8pH.mjs";
4
- import { T as workspaceDir, k as workspaceStateFile } from "./state-B3XA2wp2.mjs";
5
- import { g as repoRoot, s as gitCommonDir } from "./worktree-Eq6TLH8N.mjs";
6
- import { a as findProjectRoot, i as detectIsExpo } from "./project-fKmuIaMI.mjs";
7
- import { m as shortUdid, o as formatLongDuration, u as plural } from "./command-output-8wHMEde0.mjs";
8
- import { o as clearFreeClaimSet, u as readClaimSet } from "./ownership-claim-BtPtJVmq.mjs";
9
- import { B as listBuildLocks, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, b as listAvds } from "./android-hMj83VpN.mjs";
10
- import { A as resolveSettings, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-DEJf6_kJ.mjs";
11
- import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting, i as teardownParkedIosSim, l as readParked, n as teardownOwnedIosSim, r as teardownParkedAvd, s as dropParked, t as teardownOwnedAvd, x as resolveEasCliBin } from "./teardown--gESs9ck.mjs";
2
+ import { A as isOnMountedVolume, C as withConfigLock, E as ownership_claim_exports, F as deviceSlotPlatforms, L as projectDeviceSlots, M as volumeRootFor, O as directorySize, f as getProject, g as loadConfig, i as clearDevice, j as listMountedVolumes, k as formatBytes, u as getConfigDir, v as removeProject } from "./config-DMP7L7H6.mjs";
3
+ import { i as registeredCaches } from "./cache-manifest-uoOKhgUD.mjs";
4
+ import { h as workspaceDir, y as workspaceStateFile } from "./workspace-state-WVwWw_hB.mjs";
5
+ import { g as repoRoot, s as gitCommonDir } from "./worktree-DJnBbQZ9.mjs";
6
+ import { a as findProjectRoot, i as detectIsExpo } from "./project-B_SDt4ip.mjs";
7
+ import { m as shortUdid, o as formatLongDuration, u as plural } from "./command-output-BuIb_ZOI.mjs";
8
+ import { B as listBuildLocks, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, b as listAvds } from "./android-DJWiTBkz.mjs";
9
+ import { A as resolveSettings, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-kO2ZqIKy.mjs";
10
+ import { S as resolveEasCliBin, a as POOL_SETTING_REMEDY, i as teardownParkedIosSim, l as parkedMaxSetting, n as teardownOwnedIosSim, r as teardownParkedAvd, s as dropParked, t as teardownOwnedAvd, u as readParked } from "./teardown-CAVcWeRD.mjs";
12
11
  import { d as listIosDeviceTypes, g as parseRuntimeVersion, u as listAllIosSims } from "./ios-Deh5js5V.mjs";
13
- import { i as listLeaseFiles, r as leaseIsExpired, u as removeExpiredLease } from "./device-lease-CJwK7OZr.mjs";
14
- import { n as listBuildSlots } from "./build-slots-C4AouyNz.mjs";
15
- import { C as verifyStoppedSession, S as stopSessionArgs, _ as inspectSessionForTeardown, d as withEasProjectLock, f as easMachineStateRoot, g as getSessionArgs, h as findOrphanedOwnedSessions, m as removeEasSessionClaim, p as readEasSessionLedger, u as withRemoteSessionLock, v as isDefinitiveMissingSessionError, x as parseSessionListPage, y as listOwnedSessionsArgs } from "./device-remote-DAA0Zm6Z.mjs";
16
- import { n as reclaimProject, t as describeDereferenced } from "./reclaim-CUmwucWG.mjs";
12
+ import { i as listLeaseFiles, r as leaseIsExpired, u as removeExpiredLease } from "./device-lease-r7yEQ1R8.mjs";
13
+ import { n as listBuildSlots } from "./build-slots-CuPszJDu.mjs";
14
+ import { C as verifyStoppedSession, S as stopSessionArgs, _ as inspectSessionForTeardown, d as withEasProjectLock, f as easMachineStateRoot, g as getSessionArgs, h as findOrphanedOwnedSessions, m as removeEasSessionClaim, p as readEasSessionLedger, u as withRemoteSessionLock, v as isDefinitiveMissingSessionError, x as parseSessionListPage, y as listOwnedSessionsArgs } from "./device-remote-BTUqbl0i.mjs";
15
+ import { n as reclaimProject, t as describeDereferenced } from "./reclaim-C_rB25f4.mjs";
17
16
  import { InvalidArgumentError } from "commander";
18
17
  import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from "fs";
19
18
  import { homedir, tmpdir } from "os";
@@ -690,13 +689,20 @@ function collectDeviceLeases(now) {
690
689
  }
691
690
  function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords) {
692
691
  let deleteFailures = 0;
693
- function reap(d) {
692
+ function reap(d, onlyIfMissing = false) {
694
693
  const r = d.kind === "ios" ? teardownOwnedIosSim(d.id, {
695
694
  del: true,
696
695
  label: d.name
697
696
  }) : teardownOwnedAvd(d.name, {
698
697
  del: true,
699
- ..."project" in d ? { owner: { projectPath: d.project } } : {},
698
+ onlyIfMissing,
699
+ ..."project" in d ? { owner: {
700
+ projectPath: d.project,
701
+ slot: d.slot
702
+ } } : {},
703
+ ..."project" in d ? { onRemoved: () => {
704
+ if (clearDevice(d.project, d.kind, d.slot, d.id)) console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
705
+ } } : {},
700
706
  ..."orphanedDirectory" in d ? { orphanedDirectory: d.orphanedDirectory } : {}
701
707
  });
702
708
  const what = d.kind === "ios" ? `ios sim ${d.name} (${d.id})` : `android avd ${d.name}`;
@@ -712,15 +718,20 @@ function deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords)
712
718
  for (const d of orphanedDevices) reap(d);
713
719
  for (const d of staleDevices) {
714
720
  const status = reap(d);
715
- if (status === "torn-down" || status === "missing") {
716
- clearDevice(d.project, d.kind, d.slot);
721
+ if (d.kind === "ios" && (status === "torn-down" || status === "missing")) {
722
+ clearDevice(d.project, d.kind, d.slot, d.id);
717
723
  console.log(chalk.dim(` cleared the ${d.kind} record for ${d.project}`));
718
724
  }
719
725
  }
720
- for (const r of staleDeviceRecords) {
721
- clearDevice(r.project, r.kind, r.slot);
722
- console.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
723
- }
726
+ for (const r of staleDeviceRecords) if (r.kind === "android" && r.owned) reap({
727
+ ...r,
728
+ name: r.id,
729
+ idleDays: 0
730
+ }, true);
731
+ else if (withConfigLock(() => {
732
+ if (r.kind === "android" && deviceSlotPlatforms(getProject(r.project), r.slot)?.android?.owned) return false;
733
+ return clearDevice(r.project, r.kind, r.slot, r.id);
734
+ })) console.log(chalk.green(`Cleared the ${r.kind} record for ${r.project} (${r.id} is not on this machine)`));
724
735
  return deleteFailures;
725
736
  }
726
737
  //#endregion
@@ -932,7 +943,7 @@ function malformedRemoteSessionLockNotice(roots) {
932
943
  for (const root of roots) {
933
944
  const lockPath = join(workspaceDir(root), "remote-session.lock");
934
945
  if (!existsSync(lockPath)) continue;
935
- if (readClaimSet(lockPath).unresolved.length === 0) continue;
946
+ if ((0, ownership_claim_exports.readClaimSet)(lockPath).unresolved.length === 0) continue;
936
947
  return `EAS session sweep skipped: the remote-session lock at ${lockPath} has a malformed owner record.`;
937
948
  }
938
949
  return null;
@@ -1154,7 +1165,7 @@ function unresolvedLockLines(locks, slots) {
1154
1165
  " remove one yourself once you know nothing is building with it"
1155
1166
  ];
1156
1167
  }
1157
- function formatGcReport({ skipped = [], deadProjects = [], invalidProjects = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], staleDevices = [], staleDeviceRecords = [], buildLocks = {
1168
+ function formatGcReport({ skipped = [], deadProjects = [], orphanedPorts, invalidProjects = [], parkedSims = [], parkedAvds = [], orphanedDevices = [], staleDevices = [], staleDeviceRecords = [], buildLocks = {
1158
1169
  stale: [],
1159
1170
  live: [],
1160
1171
  unresolved: []
@@ -1187,6 +1198,7 @@ function formatGcReport({ skipped = [], deadProjects = [], invalidProjects = [],
1187
1198
  }
1188
1199
  lines.push(...projectEntryLines(`Dead project entries (${deadProjects.length}):`, deadProjects));
1189
1200
  lines.push(...projectEntryLines(`Invalid project entries (${invalidProjects.length}) - the key is not an absolute path:`, invalidProjects));
1201
+ lines.push(...namedPortLines(orphanedPorts));
1190
1202
  lines.push(...formatParkedSimReport(parkedSims, now));
1191
1203
  lines.push(...formatParkedAvdReport(parkedAvds, now));
1192
1204
  if (orphanedDevices.length) {
@@ -1292,6 +1304,14 @@ function leaseGarbageLines({ expired, kept }) {
1292
1304
  function deviceSizeSuffix(device) {
1293
1305
  return device.kind === "android" && device.bytes !== void 0 ? ` - ${formatBytes(device.bytes)} on disk` : "";
1294
1306
  }
1307
+ function namedPortLines(ports = []) {
1308
+ if (!ports.length) return [];
1309
+ return [
1310
+ `Orphaned named ports (${ports.length}):`,
1311
+ ...ports.map(({ project, label, port }) => ` ${project}: ${label} (${port})`),
1312
+ " --delete stops TCP listeners and releases these allocations."
1313
+ ];
1314
+ }
1295
1315
  //#endregion
1296
1316
  //#region src/commands/gc.ts
1297
1317
  function removeInvalidProjectEntries(invalidProjects) {
@@ -1464,6 +1484,11 @@ async function collectGcReport({ olderThan = null, cache = null, now = Date.now(
1464
1484
  return {
1465
1485
  skipped,
1466
1486
  deadProjects,
1487
+ orphanedPorts: deadProjects.flatMap((project) => Object.entries(cfg?.projects[project]?.ports ?? {}).map(([label, port]) => ({
1488
+ project,
1489
+ label,
1490
+ port
1491
+ }))),
1467
1492
  invalidProjects,
1468
1493
  parkedSims,
1469
1494
  parkedAvds,
@@ -1591,7 +1616,16 @@ async function runGcCore(opts, deps) {
1591
1616
  let deleteFailures = deleteParkedSims(report.parkedSims, deps) + deleteParkedAvds(report.parkedAvds);
1592
1617
  removeInvalidProjectEntries(invalidProjects);
1593
1618
  for (const path of deadProjects) {
1594
- const result = await reclaimProject(path);
1619
+ if (existsSync(path) || !isOnMountedVolume(path)) {
1620
+ console.log(chalk.yellow(`Kept ${path}: its absence can no longer be confirmed.`));
1621
+ continue;
1622
+ }
1623
+ const result = await reclaimProject(path).catch((error) => {
1624
+ deleteFailures++;
1625
+ console.log(chalk.red(`Could not prune ${path}; its registry entry was kept: ${error.message}`));
1626
+ return null;
1627
+ });
1628
+ if (!result) continue;
1595
1629
  if (result.keptEntry) console.log(chalk.yellow(`Could not fully prune ${path}; its registry entry was kept.`));
1596
1630
  else console.log(chalk.green(`Pruned ${path}`));
1597
1631
  for (const dir of result.removedWorkspaceDirs) console.log(chalk.dim(` removed workspace output ${dir}`));
@@ -1606,7 +1640,7 @@ async function runGcCore(opts, deps) {
1606
1640
  }
1607
1641
  deleteFailures += deleteProjectDevices(orphanedDevices, staleDevices, staleDeviceRecords);
1608
1642
  for (const lock of buildLocks.stale) {
1609
- const cleared = clearFreeClaimSet({
1643
+ const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
1610
1644
  root: lock.path,
1611
1645
  label: `${lock.platform} build`
1612
1646
  });
@@ -1623,7 +1657,7 @@ async function runGcCore(opts, deps) {
1623
1657
  console.log(chalk.green(`Cleared the ${lock.platform} build lock left by pid ${lock.pid ?? "?"} (${lock.projectRoot || "unrecorded workspace"})`));
1624
1658
  }
1625
1659
  for (const slot of buildSlots.stale) {
1626
- const cleared = clearFreeClaimSet({
1660
+ const cleared = (0, ownership_claim_exports.clearFreeClaimSet)({
1627
1661
  root: slot.path,
1628
1662
  label: "build slot"
1629
1663
  });
@@ -1,4 +1,6 @@
1
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-DEJf6_kJ.mjs";
1
+ import { a as findProjectRoot } from "./project-B_SDt4ip.mjs";
2
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-kO2ZqIKy.mjs";
3
+ import { t as guideStatus } from "./guide-status-BF6RsJJp.mjs";
2
4
  import chalk from "chalk";
3
5
  //#endregion
4
6
  //#region src/guide/index.ts
@@ -65,8 +67,12 @@ If warm fails or reports incomplete, resolve the reported failure first.
65
67
  A warm that cannot record its ownership claim refuses before copying. Follow
66
68
  the printed remedy and keep concurrent runs on the same STIM_HOME.
67
69
 
68
- Before native worktree work, run doctor for the platform in scope. It checks
69
- the source checkout from a linked worktree. Fix relevant findings and inspect the
70
+ Before native worktree work, run doctor for the platform in scope. The STATUS
71
+ block at the top of this topic says when it is due here: never run in this
72
+ checkout, older than seven days, or run under another Stim version. No block
73
+ means doctor is current and Stim is up to date; the same block names a newer
74
+ Stim release when one exists. Doctor checks the source
75
+ checkout from a linked worktree. Fix relevant findings and inspect the
70
76
  upstream gap; in the single-checkout workflow those seed findings do not
71
77
  appear. It also prints the running CLI version and the stim installation
72
78
  resolved from PATH. If that resolved installation is older than another one,
@@ -153,7 +159,7 @@ RULES DURING THE LOOP
153
159
  change does not need one.
154
160
  - For stale Metro transforms or file-map state, use stim start --reset-cache.
155
161
  It restarts only this app's verified owned Metro, preserving devices and other
156
- apps' caches. See guide lifecycle for reset scope and Expo requirements.
162
+ apps' caches. See guide lifecycle for reset scope.
157
163
  - Reload is not part of the normal workflow. Use stim reload on an owned local
158
164
  simulator or emulator when an error screen remains after the fix, and on
159
165
  Android after a failed first bundle load. It reloads JavaScript and never
@@ -226,8 +232,10 @@ shut down the phone or uninstall the app.
226
232
 
227
233
  Treat a refusal as an ownership or state mismatch: read its code and remedy.
228
234
  Never reach for --force first.
229
- Stim leaves externally started servers alone. Stop them with their original
230
- tool; neither a matching port nor --force grants process ownership.
235
+ Stim leaves externally started Metro servers alone. Stop them with their original
236
+ tool; neither a matching Metro port nor --force grants process ownership.
237
+ Named ports are separate: ports stop kills TCP listeners on the workspace's
238
+ reserved named ports, even outside the workspace. Read guide ports before use.
231
239
 
232
240
  Ask the user before these actions:
233
241
 
@@ -266,6 +274,7 @@ Read the matching guide before acting in these situations:
266
274
  | Refusal with a CODE | stim guide errors <CODE> |
267
275
  | Running under a sandbox | stim guide errors sandbox |
268
276
  | Release configuration or ...Release variant | stim guide lifecycle release |
277
+ | Web or API server ports | stim guide ports |
269
278
  | Remote device, custom Metro, or tunnel | stim guide metro |
270
279
  | Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
271
280
  | Capacity limits | stim guide lifecycle concurrency |
@@ -301,6 +310,7 @@ FULL TOPIC LIST
301
310
  stim guide lifecycle release # Release configurations and ...Release variants
302
311
  stim guide facts # the --json payloads
303
312
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
313
+ stim guide ports # named ports for web and API servers
304
314
  stim guide metro # supervisor, custom Metro, tunnels, and remote devices
305
315
  stim guide logs # filters, record shape, and capture limits
306
316
  stim guide cleanup # what reclaims a device, and what deletes
@@ -896,6 +906,58 @@ STARTING YOUR OWN BUNDLER STILL WORKS
896
906
 
897
907
  \`stop\` leaves an externally started server running. Stop it with the tool
898
908
  that started it.`
909
+ },
910
+ ports: {
911
+ summary: "Workspace ports for web and API servers that Stim does not manage",
912
+ body: () => `NAMED SERVER PORTS
913
+
914
+ If Stim is not installed globally, replace stim with npx stim.
915
+
916
+ stim ports get web
917
+ pnpm exec vite --port "$(stim ports get web)" --strictPort
918
+ stim ports
919
+ stim ports stop web --dry-run
920
+ stim ports stop web
921
+ stim ports release api
922
+
923
+ get <label> allocates on first use and prints only the number to stdout.
924
+ Repeated calls reuse it, even while a server is listening. Labels start with
925
+ a letter and contain up to 64 letters, digits, underscores, or hyphens.
926
+ metro is reserved; use stim start and stim stop for managed Metro.
927
+
928
+ New allocations scan TCP ports 8900-8999. They skip registry reservations
929
+ and existing listeners, announcing occupied ports and upward retries on
930
+ stderr. Allocation requires lsof. All 100 ports occupied or reserved is a
931
+ refusal; stop or release unused allocations in their owning workspaces.
932
+
933
+ The machine registry, under STIM_HOME, serializes allocation and cleanup.
934
+ The workspace is the nearest package.json directory, resolved through
935
+ symlinks. Use the same package directory for every command in a monorepo.
936
+ The allocation reserves a number, not a listening socket. Another process
937
+ can bind it before your server does. Use strict-port behavior when supported
938
+ and verify the server bound the number supplied. Stim does not start,
939
+ supervise, or capture logs for these servers.
940
+
941
+ ports lists named labels and ports, plus Metro marked managed.
942
+ ports stop [label] kills TCP listeners on those named ports and releases
943
+ the allocations. It sends SIGTERM, waits two seconds, then SIGKILL if needed.
944
+ It prints the PID and command for each stopped process. The listener's cwd
945
+ can be anywhere: the named reservation is permission to stop that listener.
946
+ Reserve only services this workspace may stop. --dry-run prints what would
947
+ be stopped and released, without doing either. A failed inspection or stop
948
+ keeps that allocation for retry; other labels are still processed.
949
+ ports release [label] releases without signalling a process.
950
+ Omitting the label selects every named port, never Metro. stim stop leaves
951
+ named ports alone.
952
+
953
+ worktree remove stops named listeners and releases their allocations.
954
+ gc reports allocations whose workspace no longer exists; gc --delete stops
955
+ and releases them. Unmounted or unresolved workspace paths are retained.
956
+ Failed stops keep the registry entry. Use the same current Stim version for
957
+ cleanup: versions without ports do not know about named allocations.
958
+
959
+ A shared API does not get a shared reservation. Pass its port by environment
960
+ instead of allocating a separate label in every worktree.`
899
961
  },
900
962
  logs: {
901
963
  summary: "Querying the merged NDJSON timeline, and what --errors means",
@@ -1401,7 +1463,7 @@ code, never on the message.`,
1401
1463
  run the command again.`
1402
1464
  },
1403
1465
  STIM_CLAIM_REFUSED: {
1404
- summary: "a build lock exists whose holder cannot be identified; Stim neither removes it nor waits on it",
1466
+ summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
1405
1467
  body: () => `STIM_CLAIM_REFUSED
1406
1468
  A build lock records the holder's process IDENTITY, not just its pid, so a
1407
1469
  recycled pid reads as a gone builder rather than a live one, and a builder
@@ -1420,7 +1482,26 @@ code, never on the message.`,
1420
1482
  and nothing was copied. A STIM_HOME or warm-locks ancestor that is a file
1421
1483
  also refuses. The message names that blocking path and prints a shell-quoted
1422
1484
  move-aside command. Inspect it first: the file may contain unrelated data.
1423
- An existing backup prompts before overwriting; preserve both files.`
1485
+ An existing backup prompts before overwriting; preserve both files.
1486
+ Android creation, recovery and teardown use per-AVD claims under
1487
+ ~/.stim/avd-locks. Creation records the avdmanager process group, so a claim
1488
+ left by a killed creator becomes recoverable after that group exits. Retry
1489
+ after a live creator or cleanup finishes. An interruption before the child
1490
+ is recorded, or during synchronous teardown, can leave no verifiable
1491
+ identity: inspect the named claim and confirm that neither Stim nor its
1492
+ avdmanager or adb child is still using the AVD before removing only that
1493
+ claim. Keep the AVD and its incomplete workspace record. Once the claim is
1494
+ safe to clear, retry \`stim android\` to reconcile incomplete setup through
1495
+ owned-device teardown. GC and project removal refuse while the claim is
1496
+ live or unresolved.
1497
+ Parked-device adoption and deletion use claims under $STIM_HOME/pool-locks.
1498
+ If a holder dies during a synchronous native callback, its child cannot be
1499
+ identified and the device stays protected, including from older CLIs through
1500
+ an opaque inline marker. Verify that the old Stim process
1501
+ and its native children have finished before removing the named claim.
1502
+ Legacy inline \`deletionClaim\` fields in config.json have no process
1503
+ identity and require separate manual inspection; \`stim guide lifecycle pool\`
1504
+ describes their field-only recovery.`
1424
1505
  },
1425
1506
  STIM_CLAIM_UNAVAILABLE: {
1426
1507
  summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
@@ -2062,8 +2143,21 @@ captured" (in metro.ndjson, bare RN)
2062
2143
  "Timed out waiting for the lock at <path>."
2063
2144
  Short directory locks serialize writes to config, workspace state, device
2064
2145
  leases, ownership records, metadata, and cache manifests. The path identifies
2065
- the lock. A lock older than 10s is taken over automatically. Wait for the
2066
- command holding it; if none is running, remove the named directory.`
2146
+ the lock. These locks wait up to 12s and never expire based on age. Wait for
2147
+ the command holding it; if none is running, remove the named directory.
2148
+ Short locks use the same process-identity claims as long operations, stored
2149
+ beside the visible directory at <path>.claims. An opaque marker in the visible
2150
+ directory also excludes older Stim versions. Only the exclusive claim holder
2151
+ can recover a recognized marker. A
2152
+ complete claim left by a proven-dead owner is recovered automatically on
2153
+ the next attempt. An unreadable or undecodable record instead reports
2154
+ STIM_CLAIM_REFUSED and names the claim to inspect; an unavailable native
2155
+ identity reports STIM_CLAIM_UNAVAILABLE without running the protected work.
2156
+ Older lock directories have no process identity. A visible directory left
2157
+ empty before marker publication or during final removal also cannot prove it is free.
2158
+ These paths still time out; verify that no holder is running before removing
2159
+ only the named directory. Standalone Metro and Expo cache packages use the
2160
+ same core protocol and do not require the Stim CLI.`
2067
2161
  },
2068
2162
  teardown: {
2069
2163
  summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
@@ -2337,12 +2431,11 @@ a seed belongs to this workflow.
2337
2431
 
2338
2432
  # If stale Metro transforms or file-map state require recovery:
2339
2433
  stim start --reset-cache
2340
- # Restarts only this app's verified owned Metro, retaining its port/devices.
2341
- # Uses a fresh persistent cache namespace, not deletion of shared cache files.
2342
- # Other apps and worktrees keep their caches. Subsequent starts reuse the new
2343
- # namespace; another reset changes it again. Native build caches are unchanged.
2344
- # Expo requires SDK 54+ and Stim's config adapter. Custom file-map cache
2345
- # managers must honor Metro's fileMapCacheDirectory for file-map invalidation.
2434
+ # Restarts only this app's verified owned Metro, retaining its port/devices,
2435
+ # with Metro's own reset (resetCache on a bare server, expo start --clear).
2436
+ # That clears every store in the app's Metro config, including this app's
2437
+ # shared transform store, so other worktrees of the SAME app rebuild their
2438
+ # transforms too. Other apps and native build caches are unchanged.
2346
2439
 
2347
2440
  # 3. Owned device booted, native inputs fingerprinted, cached build
2348
2441
  # installed (or built), app launched wired to port 8082, device-log
@@ -2820,6 +2913,22 @@ result as proof instead of requiring an unrelated screenshot.`
2820
2913
  retry it. The reported pool can temporarily exceed the bound rather than
2821
2914
  orphaning a simulator.
2822
2915
 
2916
+ Adoption and deletion share a process-identity claim for each parked iOS
2917
+ simulator or Android emulator under $STIM_HOME/pool-locks. A live owner
2918
+ excludes both operations. A proven dead or different owner is recovered on
2919
+ the next attempt, unless it may have left native work running. Deletion also
2920
+ puts an opaque marker on the pool record so older Stim versions keep it
2921
+ protected. New versions resolve that marker through the same identity claim.
2922
+ A crash during a synchronous device-tool call leaves that work unverifiable; inspect
2923
+ the old process and its native children before following the claim's removal
2924
+ remedy. See \`stim guide errors STIM_CLAIM_REFUSED\`.
2925
+
2926
+ Older inline \`deletionClaim\` markers contain only a pid and stay protected,
2927
+ even when that pid is absent. After verifying that neither the old Stim
2928
+ process nor its native child is using the device, remove only that parked
2929
+ record's \`deletionClaim\` field from config.json, then retry. Keep the pool
2930
+ record and device; do not remove the whole config.
2931
+
2823
2932
  and an adopting run says so where a plain boot would say \`booted\`:
2824
2933
 
2825
2934
  device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (11s)
@@ -2863,6 +2972,14 @@ result as proof instead of requiring an unrelated screenshot.`
2863
2972
  When a fresh AVD's preferred name belongs to another workspace or a parked
2864
2973
  entry, Stim adds a short suffix instead of taking over that device. Always
2865
2974
  use the actual device name and serial reported by the platform command.
2975
+ Creation reserves the owned AVD before running native tools, so other
2976
+ workspaces can update config while GC and teardown retain that reservation.
2977
+ Interrupted creation keeps an incomplete record and protects its recorded
2978
+ native process group until it exits. Retry \`stim android\` for owned-device
2979
+ cleanup and recreation; a live or unresolved per-AVD claim
2980
+ refuses cleanup. Follow \`stim guide errors STIM_CLAIM_REFUSED\` before
2981
+ manually clearing a claim, and keep the AVD data while its process state
2982
+ cannot be verified.
2866
2983
 
2867
2984
  Android cleanup happens AFTER boot, before install or launch: \`adb shell
2868
2985
  pm clear\` clears the adopting app's data while retaining its APK, and
@@ -3881,6 +3998,13 @@ to stats.json.corrupt-<unix ms> and starts a new one.`,
3881
3998
  branches stay. A branch with an existing Stim ownership record is deleted
3882
3999
  only when it has no unique commits.
3883
4000
 
4001
+ NAMED SERVER PORTS
4002
+ worktree remove stops TCP listeners on each named allocation and releases
4003
+ the ports. gc reports named allocations for missing workspaces; gc --delete
4004
+ stops their listeners and releases them. Unmounted or unresolved paths stay
4005
+ registered. Failed stops retain their allocations for a later retry.
4006
+ stim stop does not touch named ports. See guide ports.
4007
+
3884
4008
  ON THE SOURCE CHECKOUT
3885
4009
  git cannot remove a repository's main working tree, and deleting the source
3886
4010
  checkout is not what anyone meant -- so there, and only there,
@@ -4700,10 +4824,17 @@ function renderIndex(version) {
4700
4824
  lines.push("", "Read one with: stim guide <topic> (sectioned topics: stim guide <topic> <section>)");
4701
4825
  return lines.join("\n");
4702
4826
  }
4703
- function guideCommand(program, version) {
4704
- program.command("guide [topic] [section]").description("Print reference documentation for THIS version of Stim (topics: " + topicNames().join(", ") + "). A topic with sections prints its section index; name a section to print only it, e.g. `stim guide errors STIM_NO_METRO` or `stim guide lifecycle builds`. Generated by the binary, so it cannot drift from the installed CLI.").action((topic, section) => {
4827
+ function guideCommand(program, version, status = (running) => guideStatus({
4828
+ projectRoot: findProjectRoot(process.cwd()),
4829
+ running
4830
+ })) {
4831
+ program.command("guide [topic] [section]").description("Print reference documentation for THIS version of Stim (topics: " + topicNames().join(", ") + "). A topic with sections prints its section index; name a section to print only it, e.g. `stim guide errors STIM_NO_METRO` or `stim guide lifecycle builds`. Generated by the binary, so it cannot drift from the installed CLI.").action(async (topic, section) => {
4832
+ const withStatus = async (body) => {
4833
+ const block = await status(version);
4834
+ return block ? `${block}\n\n${body}` : body;
4835
+ };
4705
4836
  if (!topic) {
4706
- console.log(renderIndex(version));
4837
+ console.log(await withStatus(renderIndex(version)));
4707
4838
  return;
4708
4839
  }
4709
4840
  const body = renderTopic(topic);
@@ -4715,7 +4846,7 @@ function guideCommand(program, version) {
4715
4846
  process.exit(1);
4716
4847
  }
4717
4848
  if (!section) {
4718
- console.log(body);
4849
+ console.log(topic === "agent" ? await withStatus(body) : body);
4719
4850
  return;
4720
4851
  }
4721
4852
  const index = renderSectionIndex(topic);