stim 1.3.1 → 1.4.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 (63) hide show
  1. package/README.md +13 -0
  2. package/dist/{android-CT-PkXUh.mjs → android-BTR1KTMP.mjs} +197 -114
  3. package/dist/{android-hMj83VpN.mjs → android-BkM4qTaN.mjs} +65 -72
  4. package/dist/{android-cas-DXorPCi_.mjs → android-cas-BjLRZyks.mjs} +15 -12
  5. package/dist/{app-install-DIp9riNx.mjs → app-install--PAocWvq.mjs} +2 -2
  6. package/dist/{build-slots-C4AouyNz.mjs → build-slots-D6sDzOe-.mjs} +7 -8
  7. package/dist/{cache-manifest-CXR-Y8pH.mjs → cache-manifest-D02TpYjA.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-6vdGngKa.mjs} +51 -51
  13. package/dist/dependency-state-BMirHEPD.mjs +76 -0
  14. package/dist/{deps-0Yt0Inly.mjs → deps-Ck4wuTwa.mjs} +1 -1
  15. package/dist/{dev-client-VfSZNLT4.mjs → dev-client-Cd0v7iZk.mjs} +12 -12
  16. package/dist/{device-Bbv45Z9Z.mjs → device-CKXLnRRb.mjs} +7 -7
  17. package/dist/{xcode-xnxyvJkD.mjs → device-CMDFzx7C.mjs} +208 -967
  18. package/dist/{device-lease-CJwK7OZr.mjs → device-lease-DDJr85OK.mjs} +3 -3
  19. package/dist/{device-pool-CcBNTvM7.mjs → device-pool-DJvbIBGW.mjs} +4 -4
  20. package/dist/{device-remote-DAA0Zm6Z.mjs → device-remote-CMfHhoc8.mjs} +7 -6
  21. package/dist/{doctor-B4EITgQB.mjs → doctor-B_mfURdt.mjs} +536 -114
  22. package/dist/{error-diagnostics-DwhNBFDU.mjs → error-diagnostics-_VmkqRZo.mjs} +7 -6
  23. package/dist/{gc-BRu62v4o.mjs → gc-0AyxmECv.mjs} +61 -27
  24. package/dist/{guide-D95E0u3i.mjs → guide-CGxdmQ0K.mjs} +126 -7
  25. package/dist/{ios-DAju3TVy.mjs → ios-CtmGLx3X.mjs} +2667 -2607
  26. package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
  27. package/dist/{logs-CAdwW6c2.mjs → logs-BF3qPA7w.mjs} +5 -5
  28. package/dist/metro-DFLmaVz8.mjs +99 -0
  29. package/dist/named-ports-BO7Zpjp0.mjs +137 -0
  30. package/dist/{ownership-8O073GIC.mjs → ownership-BvSpZVBk.mjs} +5 -5
  31. package/dist/{ownership-DXcol4oU.mjs → ownership-C08Yzrp7.mjs} +6 -6
  32. package/dist/ports-BiKqP92J.mjs +39 -0
  33. package/dist/ports-ClqG8GPt.mjs +90 -0
  34. package/dist/{project-fKmuIaMI.mjs → project-CBFCwnN5.mjs} +2 -2
  35. package/dist/{reclaim-CUmwucWG.mjs → reclaim-DpDVo-AP.mjs} +20 -13
  36. package/dist/{reload-CWpXoOgN.mjs → reload-ByTqg6ZI.mjs} +8 -8
  37. package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
  38. package/dist/{server-bare-DEzPZLvf.mjs → server-bare-Buk-k8tI.mjs} +3 -6
  39. package/dist/{server-expo-DizT4OMk.mjs → server-expo-D4P7iTvf.mjs} +3 -3
  40. package/dist/server-expo-Dho5Raiw.mjs +2 -0
  41. package/dist/{settings-DEJf6_kJ.mjs → settings-DwfltIXQ.mjs} +2 -2
  42. package/dist/{slot-launch-CZklomj6.mjs → slot-launch-DeKen9e5.mjs} +4 -4
  43. package/dist/{start-B5G4NjiA.mjs → start-BpKUHcU_.mjs} +15 -13
  44. package/dist/state-CDbvmZdt.mjs +131 -0
  45. package/dist/{stats-BR-kwj0B.mjs → stats-C59QLKm2.mjs} +1 -1
  46. package/dist/{stats-CaaQr4yN.mjs → stats-k5qMjyxo.mjs} +4 -4
  47. package/dist/{status-CI9clRSb.mjs → status-Y2ij3dm6.mjs} +12 -11
  48. package/dist/{stop-CjlgDk0a.mjs → stop-_P2OQwmZ.mjs} +15 -14
  49. package/dist/{stop-CQhffpmX.mjs → stop-vKmmxhea.mjs} +2 -2
  50. package/dist/supervisor-run.d.mts +3 -27
  51. package/dist/supervisor-run.mjs +7 -6
  52. package/dist/{teardown--gESs9ck.mjs → teardown-KsPpvldq.mjs} +148 -109
  53. package/dist/{workspace-process-lock-IBj1Towr.mjs → workspace-process-lock-UEIrvZMJ.mjs} +4 -4
  54. package/dist/workspace-state-Do1e2mOH.mjs +146 -0
  55. package/dist/{worktree-DtQGO9-s.mjs → worktree-BvcAIMdJ.mjs} +24 -25
  56. package/dist/{worktree-Eq6TLH8N.mjs → worktree-DWNx4tUg.mjs} +32 -2
  57. package/dist/xcode-D9tVItl-.mjs +860 -0
  58. package/package.json +7 -7
  59. package/dist/doctor-BueDfLYG.mjs +0 -530
  60. package/dist/metro-CORqXyTL.mjs +0 -184
  61. package/dist/ownership-claim-BtPtJVmq.mjs +0 -584
  62. package/dist/server-expo-44em5Pmg.mjs +0 -2
  63. package/dist/state-B3XA2wp2.mjs +0 -268
@@ -1,12 +1,13 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { I as deviceSlotKey, L as deviceSlotPlatforms, f as getProject, k as withDirLock } from "./config-CcefMY2Q.mjs";
3
- import { f as readWorkspaceLaunches, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
2
+ import { D as withDirLock, F as deviceSlotPlatforms, P as deviceSlotKey, f as getProject } from "./config-6vdGngKa.mjs";
3
+ import { r as readWorkspaceState } from "./workspace-state-Do1e2mOH.mjs";
4
+ import { d as readWorkspaceLaunches } from "./state-CDbvmZdt.mjs";
4
5
  import { n as deviceConsoleLevel } from "./ios-device-D3pzTKQE.mjs";
5
- import { i as resolveProjectMetro } from "./metro-CORqXyTL.mjs";
6
+ import { i as resolveProjectMetro } from "./metro-DFLmaVz8.mjs";
6
7
  import { n as parseLogcatLine, t as androidClockOffset } from "./android-BW8i_YxU.mjs";
7
- import { _ as launchErrorPreview } from "./command-output-8wHMEde0.mjs";
8
- import { n as androidHome } from "./android-hMj83VpN.mjs";
9
- import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-CJwK7OZr.mjs";
8
+ import { _ as launchErrorPreview } from "./command-output-BuIb_ZOI.mjs";
9
+ import { n as androidHome } from "./android-BkM4qTaN.mjs";
10
+ import { a as parseLease, n as fileLeaseIo, t as deviceLeasePath } from "./device-lease-DDJr85OK.mjs";
10
11
  import { l as readLogRecords, t as attachExpoErrorContext } from "./logs-query-Cdp5aaCH.mjs";
11
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
12
13
  import { createHash, randomUUID } from "node:crypto";
@@ -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-6vdGngKa.mjs";
3
+ import { i as registeredCaches } from "./cache-manifest-D02TpYjA.mjs";
4
+ import { h as workspaceDir, y as workspaceStateFile } from "./workspace-state-Do1e2mOH.mjs";
5
+ import { _ as repoRoot, s as gitCommonDir } from "./worktree-DWNx4tUg.mjs";
6
+ import { a as findProjectRoot, i as detectIsExpo } from "./project-CBFCwnN5.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-BkM4qTaN.mjs";
9
+ import { A as resolveSettings, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-DwfltIXQ.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-KsPpvldq.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-DDJr85OK.mjs";
13
+ import { n as listBuildSlots } from "./build-slots-D6sDzOe-.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-CMfHhoc8.mjs";
15
+ import { n as reclaimProject, t as describeDereferenced } from "./reclaim-DpDVo-AP.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,4 @@
1
- import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-DEJf6_kJ.mjs";
1
+ import { t as ANDROID_AVD_CONFIG_HELP } from "./settings-DwfltIXQ.mjs";
2
2
  import chalk from "chalk";
3
3
  //#endregion
4
4
  //#region src/guide/index.ts
@@ -226,8 +226,10 @@ shut down the phone or uninstall the app.
226
226
 
227
227
  Treat a refusal as an ownership or state mismatch: read its code and remedy.
228
228
  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.
229
+ Stim leaves externally started Metro servers alone. Stop them with their original
230
+ tool; neither a matching Metro port nor --force grants process ownership.
231
+ Named ports are separate: ports stop kills TCP listeners on the workspace's
232
+ reserved named ports, even outside the workspace. Read guide ports before use.
231
233
 
232
234
  Ask the user before these actions:
233
235
 
@@ -266,6 +268,7 @@ Read the matching guide before acting in these situations:
266
268
  | Refusal with a CODE | stim guide errors <CODE> |
267
269
  | Running under a sandbox | stim guide errors sandbox |
268
270
  | Release configuration or ...Release variant | stim guide lifecycle release |
271
+ | Web or API server ports | stim guide ports |
269
272
  | Remote device, custom Metro, or tunnel | stim guide metro |
270
273
  | Cache miss, bypass, or fingerprint exclusions | stim guide lifecycle builds |
271
274
  | Capacity limits | stim guide lifecycle concurrency |
@@ -301,6 +304,7 @@ FULL TOPIC LIST
301
304
  stim guide lifecycle release # Release configurations and ...Release variants
302
305
  stim guide facts # the --json payloads
303
306
  stim guide facts devmenu # the Expo dev menu or Tools button over the app
307
+ stim guide ports # named ports for web and API servers
304
308
  stim guide metro # supervisor, custom Metro, tunnels, and remote devices
305
309
  stim guide logs # filters, record shape, and capture limits
306
310
  stim guide cleanup # what reclaims a device, and what deletes
@@ -896,6 +900,58 @@ STARTING YOUR OWN BUNDLER STILL WORKS
896
900
 
897
901
  \`stop\` leaves an externally started server running. Stop it with the tool
898
902
  that started it.`
903
+ },
904
+ ports: {
905
+ summary: "Workspace ports for web and API servers that Stim does not manage",
906
+ body: () => `NAMED SERVER PORTS
907
+
908
+ If Stim is not installed globally, replace stim with npx stim.
909
+
910
+ stim ports get web
911
+ pnpm exec vite --port "$(stim ports get web)" --strictPort
912
+ stim ports
913
+ stim ports stop web --dry-run
914
+ stim ports stop web
915
+ stim ports release api
916
+
917
+ get <label> allocates on first use and prints only the number to stdout.
918
+ Repeated calls reuse it, even while a server is listening. Labels start with
919
+ a letter and contain up to 64 letters, digits, underscores, or hyphens.
920
+ metro is reserved; use stim start and stim stop for managed Metro.
921
+
922
+ New allocations scan TCP ports 8900-8999. They skip registry reservations
923
+ and existing listeners, announcing occupied ports and upward retries on
924
+ stderr. Allocation requires lsof. All 100 ports occupied or reserved is a
925
+ refusal; stop or release unused allocations in their owning workspaces.
926
+
927
+ The machine registry, under STIM_HOME, serializes allocation and cleanup.
928
+ The workspace is the nearest package.json directory, resolved through
929
+ symlinks. Use the same package directory for every command in a monorepo.
930
+ The allocation reserves a number, not a listening socket. Another process
931
+ can bind it before your server does. Use strict-port behavior when supported
932
+ and verify the server bound the number supplied. Stim does not start,
933
+ supervise, or capture logs for these servers.
934
+
935
+ ports lists named labels and ports, plus Metro marked managed.
936
+ ports stop [label] kills TCP listeners on those named ports and releases
937
+ the allocations. It sends SIGTERM, waits two seconds, then SIGKILL if needed.
938
+ It prints the PID and command for each stopped process. The listener's cwd
939
+ can be anywhere: the named reservation is permission to stop that listener.
940
+ Reserve only services this workspace may stop. --dry-run prints what would
941
+ be stopped and released, without doing either. A failed inspection or stop
942
+ keeps that allocation for retry; other labels are still processed.
943
+ ports release [label] releases without signalling a process.
944
+ Omitting the label selects every named port, never Metro. stim stop leaves
945
+ named ports alone.
946
+
947
+ worktree remove stops named listeners and releases their allocations.
948
+ gc reports allocations whose workspace no longer exists; gc --delete stops
949
+ and releases them. Unmounted or unresolved workspace paths are retained.
950
+ Failed stops keep the registry entry. Use the same current Stim version for
951
+ cleanup: versions without ports do not know about named allocations.
952
+
953
+ A shared API does not get a shared reservation. Pass its port by environment
954
+ instead of allocating a separate label in every worktree.`
899
955
  },
900
956
  logs: {
901
957
  summary: "Querying the merged NDJSON timeline, and what --errors means",
@@ -1401,7 +1457,7 @@ code, never on the message.`,
1401
1457
  run the command again.`
1402
1458
  },
1403
1459
  STIM_CLAIM_REFUSED: {
1404
- summary: "a build lock exists whose holder cannot be identified; Stim neither removes it nor waits on it",
1460
+ summary: "an ownership claim blocks the operation; inspect its holder before removing an unresolved claim",
1405
1461
  body: () => `STIM_CLAIM_REFUSED
1406
1462
  A build lock records the holder's process IDENTITY, not just its pid, so a
1407
1463
  recycled pid reads as a gone builder rather than a live one, and a builder
@@ -1420,7 +1476,26 @@ code, never on the message.`,
1420
1476
  and nothing was copied. A STIM_HOME or warm-locks ancestor that is a file
1421
1477
  also refuses. The message names that blocking path and prints a shell-quoted
1422
1478
  move-aside command. Inspect it first: the file may contain unrelated data.
1423
- An existing backup prompts before overwriting; preserve both files.`
1479
+ An existing backup prompts before overwriting; preserve both files.
1480
+ Android creation, recovery and teardown use per-AVD claims under
1481
+ ~/.stim/avd-locks. Creation records the avdmanager process group, so a claim
1482
+ left by a killed creator becomes recoverable after that group exits. Retry
1483
+ after a live creator or cleanup finishes. An interruption before the child
1484
+ is recorded, or during synchronous teardown, can leave no verifiable
1485
+ identity: inspect the named claim and confirm that neither Stim nor its
1486
+ avdmanager or adb child is still using the AVD before removing only that
1487
+ claim. Keep the AVD and its incomplete workspace record. Once the claim is
1488
+ safe to clear, retry \`stim android\` to reconcile incomplete setup through
1489
+ owned-device teardown. GC and project removal refuse while the claim is
1490
+ live or unresolved.
1491
+ Parked-device adoption and deletion use claims under $STIM_HOME/pool-locks.
1492
+ If a holder dies during a synchronous native callback, its child cannot be
1493
+ identified and the device stays protected, including from older CLIs through
1494
+ an opaque inline marker. Verify that the old Stim process
1495
+ and its native children have finished before removing the named claim.
1496
+ Legacy inline \`deletionClaim\` fields in config.json have no process
1497
+ identity and require separate manual inspection; \`stim guide lifecycle pool\`
1498
+ describes their field-only recovery.`
1424
1499
  },
1425
1500
  STIM_CLAIM_UNAVAILABLE: {
1426
1501
  summary: "a process identity or warm claim store is unavailable, so the protected operation refuses",
@@ -2062,8 +2137,21 @@ captured" (in metro.ndjson, bare RN)
2062
2137
  "Timed out waiting for the lock at <path>."
2063
2138
  Short directory locks serialize writes to config, workspace state, device
2064
2139
  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.`
2140
+ the lock. These locks wait up to 12s and never expire based on age. Wait for
2141
+ the command holding it; if none is running, remove the named directory.
2142
+ Short locks use the same process-identity claims as long operations, stored
2143
+ beside the visible directory at <path>.claims. An opaque marker in the visible
2144
+ directory also excludes older Stim versions. Only the exclusive claim holder
2145
+ can recover a recognized marker. A
2146
+ complete claim left by a proven-dead owner is recovered automatically on
2147
+ the next attempt. An unreadable or undecodable record instead reports
2148
+ STIM_CLAIM_REFUSED and names the claim to inspect; an unavailable native
2149
+ identity reports STIM_CLAIM_UNAVAILABLE without running the protected work.
2150
+ Older lock directories have no process identity. A visible directory left
2151
+ empty before marker publication or during final removal also cannot prove it is free.
2152
+ These paths still time out; verify that no holder is running before removing
2153
+ only the named directory. Standalone Metro and Expo cache packages use the
2154
+ same core protocol and do not require the Stim CLI.`
2067
2155
  },
2068
2156
  teardown: {
2069
2157
  summary: "an unmanaged port, an unverified supervisor, and a failed device teardown",
@@ -2820,6 +2908,22 @@ result as proof instead of requiring an unrelated screenshot.`
2820
2908
  retry it. The reported pool can temporarily exceed the bound rather than
2821
2909
  orphaning a simulator.
2822
2910
 
2911
+ Adoption and deletion share a process-identity claim for each parked iOS
2912
+ simulator or Android emulator under $STIM_HOME/pool-locks. A live owner
2913
+ excludes both operations. A proven dead or different owner is recovered on
2914
+ the next attempt, unless it may have left native work running. Deletion also
2915
+ puts an opaque marker on the pool record so older Stim versions keep it
2916
+ protected. New versions resolve that marker through the same identity claim.
2917
+ A crash during a synchronous device-tool call leaves that work unverifiable; inspect
2918
+ the old process and its native children before following the claim's removal
2919
+ remedy. See \`stim guide errors STIM_CLAIM_REFUSED\`.
2920
+
2921
+ Older inline \`deletionClaim\` markers contain only a pid and stay protected,
2922
+ even when that pid is absent. After verifying that neither the old Stim
2923
+ process nor its native child is using the device, remove only that parked
2924
+ record's \`deletionClaim\` field from config.json, then retry. Keep the pool
2925
+ record and device; do not remove the whole config.
2926
+
2823
2927
  and an adopting run says so where a plain boot would say \`booted\`:
2824
2928
 
2825
2929
  device stim-app-412 (iPhone 17 26.5) (9C1F..) adopted (11s)
@@ -2863,6 +2967,14 @@ result as proof instead of requiring an unrelated screenshot.`
2863
2967
  When a fresh AVD's preferred name belongs to another workspace or a parked
2864
2968
  entry, Stim adds a short suffix instead of taking over that device. Always
2865
2969
  use the actual device name and serial reported by the platform command.
2970
+ Creation reserves the owned AVD before running native tools, so other
2971
+ workspaces can update config while GC and teardown retain that reservation.
2972
+ Interrupted creation keeps an incomplete record and protects its recorded
2973
+ native process group until it exits. Retry \`stim android\` for owned-device
2974
+ cleanup and recreation; a live or unresolved per-AVD claim
2975
+ refuses cleanup. Follow \`stim guide errors STIM_CLAIM_REFUSED\` before
2976
+ manually clearing a claim, and keep the AVD data while its process state
2977
+ cannot be verified.
2866
2978
 
2867
2979
  Android cleanup happens AFTER boot, before install or launch: \`adb shell
2868
2980
  pm clear\` clears the adopting app's data while retaining its APK, and
@@ -3881,6 +3993,13 @@ to stats.json.corrupt-<unix ms> and starts a new one.`,
3881
3993
  branches stay. A branch with an existing Stim ownership record is deleted
3882
3994
  only when it has no unique commits.
3883
3995
 
3996
+ NAMED SERVER PORTS
3997
+ worktree remove stops TCP listeners on each named allocation and releases
3998
+ the ports. gc reports named allocations for missing workspaces; gc --delete
3999
+ stops their listeners and releases them. Unmounted or unresolved paths stay
4000
+ registered. Failed stops retain their allocations for a later retry.
4001
+ stim stop does not touch named ports. See guide ports.
4002
+
3884
4003
  ON THE SOURCE CHECKOUT
3885
4004
  git cannot remove a repository's main working tree, and deleting the source
3886
4005
  checkout is not what anyone meant -- so there, and only there,