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,19 +1,19 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { B as removeSlotDevice, C as withConfigLock, F as assignSlotDevice, L as deviceSlotPlatforms, g as loadConfig, s as ensureConfig, y as saveConfig, z as projectDeviceSlots } from "./config-CcefMY2Q.mjs";
2
+ import { C as withConfigLock, E as ownership_claim_exports, F as deviceSlotPlatforms, L as projectDeviceSlots, N as assignSlotDevice, R as removeSlotDevice, T as acquireAvdClaim, d as getConfigPath, g as loadConfig, s as ensureConfig, u as getConfigDir, y as saveConfig } from "./config-6vdGngKa.mjs";
3
3
  import { r as createNdjsonWriter } from "./ndjson-BZwI1NBv.mjs";
4
- import { D as workspaceLogsDir } from "./state-B3XA2wp2.mjs";
5
- import { l as resolvePackageJson } from "./project-fKmuIaMI.mjs";
4
+ import { _ as workspaceLogsDir } from "./workspace-state-Do1e2mOH.mjs";
5
+ import { l as resolvePackageJson } from "./project-CBFCwnN5.mjs";
6
6
  import { n as stripAnsi } from "./process-output-C7IlIzd7.mjs";
7
- import { l as phaseLine } from "./command-output-8wHMEde0.mjs";
8
- import { E as ownedAvdMatchesConfiguration, F as shutdownAndroidEmulator, I as waitForAndroidEmulatorShutdown, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, l as avdStorageRoots, o as assertOwnedAvdStopped, p as deleteAvd, s as avdPathExists } from "./android-hMj83VpN.mjs";
9
- import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-DizT4OMk.mjs";
7
+ import { l as phaseLine } from "./command-output-BuIb_ZOI.mjs";
8
+ import { E as ownedAvdMatchesConfiguration, F as shutdownAndroidEmulator, I as waitForAndroidEmulatorShutdown, N as resolveOwnedAvdSerial, O as ownedAvdSystemImage, S as listOrphanedAvdDirectories, T as ownedAvdDirectory, l as avdStorageRoots, o as assertOwnedAvdStopped, p as deleteAvd, s as avdPathExists } from "./android-BkM4qTaN.mjs";
9
+ import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-D4P7iTvf.mjs";
10
10
  import { C as shutdownIosSim, _ as renameIosSim, a as deleteIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, o as deleteParkedIosSim, r as clearAppDataContainer, s as findAppDataContainer, x as resolveOwnedIosSim } from "./ios-Deh5js5V.mjs";
11
11
  import { createRequire } from "node:module";
12
12
  import { existsSync, lstatSync, readFileSync, renameSync, rmSync } from "node:fs";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { dirname, join } from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
- import { format } from "node:util";
16
+ import { format, isDeepStrictEqual } from "node:util";
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
18
18
  //#region src/engine/remote-cache.ts
19
19
  const EAS_PROVIDER_PACKAGE = "eas-build-cache-provider";
@@ -593,8 +593,14 @@ function writeParked(config, platform, records) {
593
593
  function oldestFirst(records) {
594
594
  return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
595
595
  }
596
+ function isLegacyDeletionClaim(value) {
597
+ if (value === void 0) return false;
598
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return true;
599
+ const marker = value;
600
+ return marker.kind !== "ownership-claim" || typeof marker.claimId !== "string" || marker.claimId.length === 0;
601
+ }
596
602
  function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
597
- return oldestFirst(records.filter((r) => r.deletionClaim === void 0 && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
603
+ return oldestFirst(records.filter((r) => !isLegacyDeletionClaim(r.deletionClaim) && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
598
604
  }
599
605
  function evictOverflow(records, max) {
600
606
  if (records.length <= max) return {
@@ -624,98 +630,91 @@ function parkSim({ platform, projectPath, slot = "default", record, max }) {
624
630
  });
625
631
  }
626
632
  function adoptParked({ platform, projectPath, slot = "default", udid, device }) {
627
- return withConfigLock(() => {
628
- const cfg = ensureConfig();
629
- const records = readParked(platform, { config: cfg });
630
- const taken = records.find((r) => r.udid === udid);
631
- if (!taken || taken.deletionClaim !== void 0) return null;
632
- if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
633
- writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
634
- const project = cfg.projects[projectPath];
635
- if (!project) throw new Error(`Project not registered: ${projectPath}`);
636
- assignSlotDevice(project, platform, device, slot);
637
- saveConfig(cfg);
638
- return taken;
639
- });
640
- }
641
- function isProcessAlive(pid) {
633
+ const claim = claimParkedOperation(platform, udid);
634
+ if (!claim) return null;
642
635
  try {
643
- process.kill(pid, 0);
644
- return true;
645
- } catch (error) {
646
- return error.code === "EPERM";
636
+ return withConfigLock(() => {
637
+ const cfg = ensureConfig();
638
+ const records = readParked(platform, { config: cfg });
639
+ const taken = records.find((r) => r.udid === udid);
640
+ if (!taken || isLegacyDeletionClaim(taken.deletionClaim)) return null;
641
+ if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
642
+ writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
643
+ const project = cfg.projects[projectPath];
644
+ if (!project) throw new Error(`Project not registered: ${projectPath}`);
645
+ assignSlotDevice(project, platform, device, slot);
646
+ saveConfig(cfg);
647
+ const adopted = { ...taken };
648
+ delete adopted.deletionClaim;
649
+ return adopted;
650
+ });
651
+ } finally {
652
+ (0, ownership_claim_exports.releaseClaim)(claim);
647
653
  }
648
654
  }
649
- function parseDeletionClaim(value) {
650
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
651
- const claim = value;
652
- if (!Number.isSafeInteger(claim.pid) || claim.pid <= 0) return null;
653
- if (typeof claim.token !== "string" || claim.token.length === 0) return null;
654
- return {
655
- pid: claim.pid,
656
- token: claim.token
657
- };
658
- }
659
- function claimParkedRemoval(platform, udid) {
660
- return withConfigLock(() => {
661
- const cfg = loadConfig();
662
- if (!cfg) return null;
663
- const records = readParked(platform, { config: cfg });
664
- const record = records.find((candidate) => candidate.udid === udid);
665
- if (!record) return null;
666
- const existingClaim = parseDeletionClaim(record.deletionClaim);
667
- if (record.deletionClaim !== void 0 && (!existingClaim || isProcessAlive(existingClaim.pid))) return null;
668
- const token = randomUUID();
669
- const claimedRecord = {
670
- ...record,
671
- deletionClaim: {
672
- pid: process.pid,
673
- token
674
- }
675
- };
676
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? claimedRecord : candidate));
677
- saveConfig(cfg);
678
- const claimed = { ...record };
679
- delete claimed.deletionClaim;
680
- return {
681
- record: claimed,
682
- token
683
- };
684
- });
685
- }
686
- function clearParkedRemovalClaim(platform, udid, token) {
687
- withConfigLock(() => {
688
- const cfg = loadConfig();
689
- if (!cfg) return;
690
- const records = readParked(platform, { config: cfg });
691
- const record = records.find((candidate) => candidate.udid === udid);
692
- if (!record || parseDeletionClaim(record.deletionClaim)?.token !== token) return;
693
- const restored = { ...record };
694
- delete restored.deletionClaim;
695
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? restored : candidate));
696
- saveConfig(cfg);
655
+ function claimParkedOperation(platform, udid) {
656
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
657
+ root: join(getConfigDir(), "pool-locks", platform, `${encodeURIComponent(udid.toLowerCase())}.lock`),
658
+ mode: "exclusive",
659
+ label: `parked ${platform} device ${udid}`
697
660
  });
661
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
662
+ return attempt.acquired ?? null;
698
663
  }
699
664
  function removeParkedAfter(platform, udid, beforeRemove) {
700
- const claim = claimParkedRemoval(platform, udid);
665
+ const claim = claimParkedOperation(platform, udid);
701
666
  if (!claim) return null;
702
667
  try {
703
- beforeRemove(claim.record);
704
- } catch (error) {
668
+ const claimed = withConfigLock(() => {
669
+ const cfg = loadConfig();
670
+ if (!cfg) return null;
671
+ const records = readParked(platform, { config: cfg });
672
+ const current = records.find((candidate) => candidate.udid === udid);
673
+ if (!current) return null;
674
+ if (isLegacyDeletionClaim(current.deletionClaim)) throw new Error(`Parked ${platform} device ${udid} has a legacy deletionClaim in ${getConfigPath()}. Its PID does not establish whether the original operation finished. Keep the device and pool record; after verifying that neither the old Stim process nor its native child is using it, remove only that record's deletionClaim field and retry.`);
675
+ const record = { ...current };
676
+ delete record.deletionClaim;
677
+ const marked = {
678
+ ...record,
679
+ deletionClaim: {
680
+ kind: "ownership-claim",
681
+ claimId: claim.claimId
682
+ }
683
+ };
684
+ writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? marked : candidate));
685
+ saveConfig(cfg);
686
+ return {
687
+ record,
688
+ marked
689
+ };
690
+ });
691
+ if (!claimed) return null;
692
+ let completed = false;
693
+ let removed = null;
705
694
  try {
706
- clearParkedRemovalClaim(platform, udid, claim.token);
707
- } catch {}
708
- throw error;
695
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
696
+ try {
697
+ beforeRemove(claimed.record);
698
+ } finally {
699
+ (0, ownership_claim_exports.clearClaimChild)(claim);
700
+ }
701
+ completed = true;
702
+ } finally {
703
+ withConfigLock(() => {
704
+ const cfg = loadConfig();
705
+ if (!cfg) return;
706
+ const records = readParked(platform, { config: cfg });
707
+ const current = records.find((candidate) => candidate.udid === udid);
708
+ if (!isDeepStrictEqual(current, claimed.marked)) return;
709
+ writeParked(cfg, platform, completed ? records.filter((candidate) => candidate.udid !== udid) : records.map((candidate) => candidate.udid === udid ? claimed.record : candidate));
710
+ saveConfig(cfg);
711
+ if (completed) removed = claimed.record;
712
+ });
713
+ }
714
+ return removed;
715
+ } finally {
716
+ (0, ownership_claim_exports.releaseClaim)(claim);
709
717
  }
710
- return withConfigLock(() => {
711
- const cfg = loadConfig();
712
- if (!cfg) return null;
713
- const records = readParked(platform, { config: cfg });
714
- if (parseDeletionClaim(records.find((candidate) => candidate.udid === udid)?.deletionClaim)?.token !== claim.token) return null;
715
- writeParked(cfg, platform, records.filter((candidate) => candidate.udid !== udid));
716
- saveConfig(cfg);
717
- return claim.record;
718
- });
719
718
  }
720
719
  function dropParked(platform, udid) {
721
720
  return removeParkedAfter(platform, udid, () => {}) !== null;
@@ -942,18 +941,24 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
942
941
  };
943
942
  }
944
943
  }
944
+ function assertAvdReferences(avdName, owner) {
945
+ const config = loadConfig();
946
+ if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
947
+ if (owner && "projectPath" in owner && owner.expectedRecord && !isDeepStrictEqual(deviceSlotPlatforms(config.projects[owner.projectPath], owner.slot)?.android, owner.expectedRecord)) throw new Error(`AVD ${avdName} has a changed workspace record; its data was kept.`);
948
+ for (const [projectPath, project] of Object.entries(config.projects)) for (const { slot, platforms } of projectDeviceSlots(project)) {
949
+ if (platforms.android?.avdName?.toLowerCase() !== avdName.toLowerCase()) continue;
950
+ if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || (owner.slot ?? "default") !== slot || !platforms.android.owned) throw new Error(`AVD ${avdName} is referenced by ${projectPath} (${slot}); its data was kept.`);
951
+ }
952
+ if (readParked("android", { config }).some((entry) => entry.name.toLowerCase() === avdName.toLowerCase()) && !(owner && "pool" in owner)) throw new Error(`AVD ${avdName} is in the emulator pool; its data was kept.`);
953
+ }
945
954
  function removeOrphanedAvdDirectory(candidate, owner) {
955
+ const surveyed = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
956
+ if (!surveyed || surveyed.name !== candidate.name || surveyed.dev !== candidate.dev || surveyed.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
957
+ assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
946
958
  const detached = withConfigLock(() => {
947
- const current = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
948
- if (!current || current.name !== candidate.name || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
949
- const config = loadConfig();
950
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
951
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { platforms } of projectDeviceSlots(project)) {
952
- if (platforms.android?.avdName !== candidate.name) continue;
953
- if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || !platforms.android.owned) throw new Error(`AVD ${candidate.name} is referenced by ${projectPath}; its data was kept.`);
954
- }
955
- if (readParked("android", { config }).some((entry) => entry.name === candidate.name) && !(owner && "pool" in owner)) throw new Error(`AVD ${candidate.name} is in the emulator pool; its data was kept.`);
956
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
959
+ const current = lstatSync(candidate.directory);
960
+ if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`AVD data at ${candidate.directory} changed or is registered; it was kept.`);
961
+ assertAvdReferences(candidate.name, owner);
957
962
  const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
958
963
  renameSync(candidate.directory, destination);
959
964
  return destination;
@@ -985,14 +990,31 @@ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
985
990
  label: avdName
986
991
  };
987
992
  }
988
- function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial } = {}) {
989
- let parkFallback;
993
+ function teardownOwnedAvd(avdName, options = {}) {
994
+ if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
995
+ status: "skipped",
996
+ kind: "not-owned",
997
+ reason: `AVD ${avdName} is not Stim-owned by name`
998
+ };
999
+ let claim;
990
1000
  try {
991
- if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
992
- status: "skipped",
993
- kind: "not-owned",
994
- reason: `AVD ${avdName} is not Stim-owned by name`
1001
+ claim = acquireAvdClaim(avdName);
1002
+ const result = teardownClaimedAvd(avdName, claim, options);
1003
+ if (result.status === "torn-down" || result.status === "missing") options.onRemoved?.();
1004
+ return result;
1005
+ } catch (error) {
1006
+ return {
1007
+ status: "failed",
1008
+ reason: String(error?.message || error)
995
1009
  };
1010
+ } finally {
1011
+ (0, ownership_claim_exports.releaseClaim)(claim);
1012
+ }
1013
+ }
1014
+ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphanedDirectory, onlyIfMissing = false, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
1015
+ let parkFallback;
1016
+ try {
1017
+ if (del) withConfigLock(() => assertAvdReferences(avdName, owner));
996
1018
  const resolved = resolveAvd(avdName);
997
1019
  if (resolved.notOwned) return {
998
1020
  status: "skipped",
@@ -1000,6 +1022,10 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
1000
1022
  reason: `AVD ${avdName} is not Stim-owned by name`
1001
1023
  };
1002
1024
  if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
1025
+ if (onlyIfMissing) return {
1026
+ status: "skipped",
1027
+ reason: "AVD registration appeared; its record was kept."
1028
+ };
1003
1029
  if (orphanedDirectory) return {
1004
1030
  status: "skipped",
1005
1031
  reason: "AVD registration appeared; its data was kept."
@@ -1015,7 +1041,14 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
1015
1041
  id: serial
1016
1042
  }, stillOwned);
1017
1043
  if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
1018
- waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
1044
+ waitForShutdown(avdName, (timeoutMs) => {
1045
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
1046
+ try {
1047
+ shutdownAndroidEmulator(serial, timeoutMs);
1048
+ } finally {
1049
+ (0, ownership_claim_exports.clearClaimChild)(claim);
1050
+ }
1051
+ });
1019
1052
  } else assertStopped(avdName);
1020
1053
  if (del) {
1021
1054
  const current = resolveAvd(avdName);
@@ -1027,6 +1060,7 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
1027
1060
  if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
1028
1061
  if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
1029
1062
  assertStopped(avdName);
1063
+ withConfigLock(() => assertAvdReferences(avdName, owner));
1030
1064
  if (park && park.max > 0) try {
1031
1065
  const systemImage = ownedAvdSystemImage(avdName);
1032
1066
  if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
@@ -1071,7 +1105,12 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
1071
1105
  const directory = ownedAvdDirectory(avdName);
1072
1106
  const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
1073
1107
  if (directory) paths.push(directory);
1074
- deleteAvd(avdName);
1108
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
1109
+ try {
1110
+ deleteAvd(avdName);
1111
+ } finally {
1112
+ (0, ownership_claim_exports.clearClaimChild)(claim);
1113
+ }
1075
1114
  const remaining = paths.filter(avdPathExists);
1076
1115
  if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
1077
1116
  }
@@ -1089,4 +1128,4 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
1089
1128
  }
1090
1129
  }
1091
1130
  //#endregion
1092
- export { uploadRemote as C, resolveRemote as S, isEasAuthFailureText as _, POOL_SETTING_REMEDY as a, providerFromConfig as b, parkedMaxSetting as c, RESOLVE_TIMEOUT_MS as d, UPLOAD_TIMEOUT_MS as f, exitAfterFlush as g, easAuthNote as h, teardownParkedIosSim as i, readParked as l, checkEasAuth as m, teardownOwnedIosSim as n, adoptParked as o, cacheLevel as p, teardownParkedAvd as r, dropParked as s, teardownOwnedAvd as t, selectParked as u, loadProjectProvider as v, resolveEasCliBin as x, ownerFromConfig as y };
1131
+ export { resolveRemote as C, resolveEasCliBin as S, exitAfterFlush as _, POOL_SETTING_REMEDY as a, ownerFromConfig as b, isLegacyDeletionClaim as c, selectParked as d, RESOLVE_TIMEOUT_MS as f, easAuthNote as g, checkEasAuth as h, teardownParkedIosSim as i, parkedMaxSetting as l, cacheLevel as m, teardownOwnedIosSim as n, adoptParked as o, UPLOAD_TIMEOUT_MS as p, teardownParkedAvd as r, dropParked as s, teardownOwnedAvd as t, readParked as u, isEasAuthFailureText as v, uploadRemote as w, providerFromConfig as x, loadProjectProvider as y };
@@ -1,4 +1,4 @@
1
- import { d as releaseClaim, m as tryAcquireClaim } from "./ownership-claim-BtPtJVmq.mjs";
1
+ import { E as ownership_claim_exports } from "./config-6vdGngKa.mjs";
2
2
  import { join } from "node:path";
3
3
  //#region src/engine/workspace-process-lock.ts
4
4
  const DEFAULT_WAIT_MS = 6e4;
@@ -17,17 +17,17 @@ async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep
17
17
  const path = lockPath(root, name, external);
18
18
  const deadline = now() + waitMs;
19
19
  for (;;) {
20
- const attempt = tryAcquireClaim({
20
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
21
21
  root: path,
22
22
  mode: "exclusive",
23
23
  label: `${name} lock`,
24
24
  details: ownerPurpose ? { purpose: ownerPurpose } : {}
25
25
  });
26
- if (attempt.pending) releaseClaim(attempt.pending);
26
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
27
27
  if (attempt.acquired) try {
28
28
  return await fn();
29
29
  } finally {
30
- releaseClaim(attempt.acquired);
30
+ (0, ownership_claim_exports.releaseClaim)(attempt.acquired);
31
31
  }
32
32
  const purpose = (attempt.held ?? attempt.waitingFor?.[0])?.details.purpose;
33
33
  if (typeof purpose === "string" && rejectOwnerPurposes.includes(purpose)) {
@@ -0,0 +1,146 @@
1
+ import { D as withDirLock, u as getConfigDir } from "./config-6vdGngKa.mjs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { join as join$1, resolve as resolve$1 } from "path";
5
+ import { buildCacheRoot as sharedBuildCache, workspaceId, workspaceName as workspaceName$1, workspaceStateDir as workspaceDir } from "@stim-cli/core";
6
+ //#region src/paths.ts
7
+ const WORKSPACE_METADATA_FILE_NAME = "workspace.json";
8
+ function workspaceMetadataFile(projectRoot) {
9
+ return join$1(workspaceDir(projectRoot), WORKSPACE_METADATA_FILE_NAME);
10
+ }
11
+ function ensureWorkspaceStorage(projectRoot) {
12
+ const canonicalRoot = resolve$1(projectRoot);
13
+ const dir = workspaceDir(canonicalRoot);
14
+ const file = workspaceMetadataFile(canonicalRoot);
15
+ mkdirSync(dir, { recursive: true });
16
+ return withDirLock(join$1(dir, "metadata.lock"), () => {
17
+ if (!existsSync(file)) {
18
+ const metadata = {
19
+ projectRoot: canonicalRoot,
20
+ workspace: workspaceName$1(canonicalRoot),
21
+ version: 1
22
+ };
23
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
24
+ writeFileSync(tmp, `${JSON.stringify(metadata, null, 2)}\n`);
25
+ try {
26
+ renameSync(tmp, file);
27
+ } catch (error) {
28
+ rmSync(tmp, { force: true });
29
+ throw error;
30
+ }
31
+ return dir;
32
+ }
33
+ try {
34
+ if (JSON.parse(readFileSync(file, "utf-8")).projectRoot === canonicalRoot) return dir;
35
+ } catch {}
36
+ const error = /* @__PURE__ */ new Error(`Stim workspace collision at ${dir}: its workspace.json does not belong to ${canonicalRoot}.`);
37
+ error.code = "STIM_WORKSPACE_COLLISION";
38
+ throw error;
39
+ });
40
+ }
41
+ function workspaceLogsDir(projectRoot) {
42
+ return join$1(workspaceDir(projectRoot), "logs");
43
+ }
44
+ function workspaceDerivedData(projectRoot) {
45
+ return join$1(workspaceDir(projectRoot), "derived-data");
46
+ }
47
+ function supervisorPidFile(projectRoot) {
48
+ return join$1(workspaceDir(projectRoot), "supervisor.pid");
49
+ }
50
+ function workspaceStateFile(projectRoot) {
51
+ return join$1(workspaceDir(projectRoot), "state.json");
52
+ }
53
+ function workspaceStateLock(projectRoot) {
54
+ return join$1(workspaceDir(projectRoot), "state.lock");
55
+ }
56
+ function supervisorLogFile(projectRoot) {
57
+ return join$1(workspaceLogsDir(projectRoot), "supervisor.log");
58
+ }
59
+ function emulatorLogFile(projectRoot) {
60
+ return join$1(workspaceLogsDir(projectRoot), "emulator.log");
61
+ }
62
+ function sharedCompilationCache() {
63
+ return join$1(getConfigDir(), "compilation-cache");
64
+ }
65
+ function sharedCcache() {
66
+ return join$1(getConfigDir(), "ccache");
67
+ }
68
+ //#endregion
69
+ //#region src/workspace-state.ts
70
+ function readWorkspaceState(root) {
71
+ try {
72
+ const parsed = JSON.parse(readFileSync(workspaceStateFile(root), "utf-8"));
73
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
74
+ return parsed;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ function withWorkspaceStateLock(root, fn) {
80
+ const file = workspaceStateFile(root);
81
+ return withDirLock(workspaceStateLock(root), fn, { ensureParent: () => {
82
+ ensureWorkspaceStorage(root);
83
+ mkdirSync(dirname(file), { recursive: true });
84
+ } });
85
+ }
86
+ function writeWorkspaceState(root, patch) {
87
+ return updateWorkspaceState(root, (state) => ({
88
+ ...state,
89
+ ...patch
90
+ }));
91
+ }
92
+ function updateWorkspaceState(root, update) {
93
+ return withWorkspaceStateLock(root, () => replaceWorkspaceState(root, update(readWorkspaceState(root) ?? {})));
94
+ }
95
+ function replaceWorkspaceState(root, state) {
96
+ const file = workspaceStateFile(root);
97
+ mkdirSync(dirname(file), { recursive: true });
98
+ const tmp = `${file}.tmp-${process.pid}`;
99
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
100
+ renameSync(tmp, file);
101
+ return state;
102
+ }
103
+ function clearWorkspaceStateKeys(root, keys) {
104
+ if (!existsSync(workspaceStateFile(root))) return;
105
+ withWorkspaceStateLock(root, () => {
106
+ const state = readWorkspaceState(root);
107
+ const file = workspaceStateFile(root);
108
+ if (!state) {
109
+ try {
110
+ rmSync(file, { force: true });
111
+ } catch {}
112
+ return;
113
+ }
114
+ let changed = false;
115
+ for (const key of keys) {
116
+ if (!(key in state)) continue;
117
+ delete state[key];
118
+ changed = true;
119
+ }
120
+ if (!changed) return;
121
+ if (Object.keys(state).length === 0) {
122
+ rmSync(file, { force: true });
123
+ return;
124
+ }
125
+ replaceWorkspaceState(root, state);
126
+ });
127
+ }
128
+ function clearWorkspaceStateKey(root, key, shouldClear) {
129
+ return withWorkspaceStateLock(root, () => {
130
+ const state = readWorkspaceState(root);
131
+ if (!state || !(key in state)) return true;
132
+ if (!shouldClear(state[key])) return false;
133
+ delete state[key];
134
+ const file = workspaceStateFile(root);
135
+ if (Object.keys(state).length === 0) {
136
+ try {
137
+ rmSync(file, { force: true });
138
+ } catch {}
139
+ return true;
140
+ }
141
+ replaceWorkspaceState(root, state);
142
+ return true;
143
+ });
144
+ }
145
+ //#endregion
146
+ export { workspaceLogsDir as _, withWorkspaceStateLock as a, ensureWorkspaceStorage as c, sharedCompilationCache as d, supervisorLogFile as f, workspaceId as g, workspaceDir as h, updateWorkspaceState as i, sharedBuildCache as l, workspaceDerivedData as m, clearWorkspaceStateKeys as n, writeWorkspaceState as o, supervisorPidFile as p, readWorkspaceState as r, emulatorLogFile as s, clearWorkspaceStateKey as t, sharedCcache as u, workspaceName$1 as v, workspaceStateFile as y };
@@ -1,17 +1,16 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { S as upsertProject, f as getProject, g as loadConfig, h as isPathPrefix, u as getConfigDir, v as removeProject, w as captureProcessIdentity } from "./config-CcefMY2Q.mjs";
3
- import { c as readMetroTunnel, u as readRemoteSession } from "./state-B3XA2wp2.mjs";
4
- import { _ as resolveFullRef, b as warmWorktreePaths, c as hasRemote, d as isPodInstallChurn, f as listWorktrees, h as removeWorktree, i as depsOutOfSync, l as hasUncommittedWork, m as readWorktreeExclude, n as cloneIgnoredEntries, o as dirtyPaths, p as podsOutOfSync, r as deleteBranch, s as gitCommonDir, t as branchExists, u as isMainWorkingTree, v as restoreFile, y as unpushedCommits } from "./worktree-Eq6TLH8N.mjs";
5
- import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
6
- import { a as formatElapsed, f as releasedLeaseFact, i as formatDuration, l as phaseLine, m as shortUdid, u as plural } from "./command-output-8wHMEde0.mjs";
7
- import { a as clearClaimChild, c as markClaimChildPending, d as releaseClaim, f as setClaimChild, i as claimRemoveCommand, l as processGroupAlive, m as tryAcquireClaim, n as ClaimUnavailableError, p as settleClaim, r as claimFailure } from "./ownership-claim-BtPtJVmq.mjs";
8
- import { A as resolveSettings, I as unknownSettingKeys, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-DEJf6_kJ.mjs";
9
- import { a as POOL_SETTING_REMEDY, c as parkedMaxSetting } from "./teardown--gESs9ck.mjs";
10
- import { g as podInstallCommand, r as HEARTBEAT_INTERVAL_MS } from "./xcode-xnxyvJkD.mjs";
11
- import { a as installedNpmTreeIsValid, i as hasInstalledDependencies, o as locallyKnownUpstream, t as dependencyState } from "./doctor-B4EITgQB.mjs";
12
- import { c as withManagedTunnelRemovalLock, o as withManagedRemoteWorktreeRemovalLock } from "./ownership-DXcol4oU.mjs";
13
- import { n as reclaimProject } from "./reclaim-CUmwucWG.mjs";
14
- import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-0Yt0Inly.mjs";
2
+ import { E as ownership_claim_exports, S as upsertProject, f as getProject, g as loadConfig, h as isPathPrefix, u as getConfigDir, v as removeProject, w as process_identity_exports } from "./config-6vdGngKa.mjs";
3
+ import { b as unpushedCommits, c as hasRemote, d as isPodInstallChurn, f as listWorktrees, g as removeWorktree, h as readWorktreeExclude, i as depsOutOfSync, l as hasUncommittedWork, m as podsOutOfSync, n as cloneIgnoredEntries, o as dirtyPaths, p as locallyKnownUpstream, r as deleteBranch, s as gitCommonDir, t as branchExists, u as isMainWorkingTree, v as resolveFullRef, x as warmWorktreePaths, y as restoreFile } from "./worktree-DWNx4tUg.mjs";
4
+ import { a as findProjectRoot } from "./project-CBFCwnN5.mjs";
5
+ import { l as readRemoteSession, s as readMetroTunnel } from "./state-CDbvmZdt.mjs";
6
+ import { a as formatElapsed, f as releasedLeaseFact, i as formatDuration, l as phaseLine, m as shortUdid, u as plural } from "./command-output-BuIb_ZOI.mjs";
7
+ import { A as resolveSettings, I as unknownSettingKeys, N as settingShapeErrors, a as SETTING_SHAPE_REMEDY } from "./settings-DwfltIXQ.mjs";
8
+ import { a as POOL_SETTING_REMEDY, l as parkedMaxSetting } from "./teardown-KsPpvldq.mjs";
9
+ import { h as podInstallCommand, n as HEARTBEAT_INTERVAL_MS } from "./xcode-D9tVItl-.mjs";
10
+ import { n as hasInstalledDependencies, r as installedNpmTreeIsValid, t as dependencyState } from "./dependency-state-BMirHEPD.mjs";
11
+ import { c as withManagedTunnelRemovalLock, o as withManagedRemoteWorktreeRemovalLock } from "./ownership-C08Yzrp7.mjs";
12
+ import { n as reclaimProject } from "./reclaim-DpDVo-AP.mjs";
13
+ import { a as runPodInstall, i as runCaptured, n as podsAreStale, r as readPodState, t as DEPS_ERROR } from "./deps-Ck4wuTwa.mjs";
15
14
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
15
  import { createHash, randomUUID } from "node:crypto";
17
16
  import { join, relative } from "node:path";
@@ -72,22 +71,22 @@ function installerClaim(claim, { now, progressMs, out }) {
72
71
  const forget = () => {
73
72
  groups = [];
74
73
  try {
75
- clearClaimChild(claim);
74
+ (0, ownership_claim_exports.clearClaimChild)(claim);
76
75
  } catch {}
77
76
  };
78
- const alive = () => groups.find((pid) => processGroupAlive(pid)) ?? null;
77
+ const alive = () => groups.find((pid) => (0, ownership_claim_exports.processGroupAlive)(pid)) ?? null;
79
78
  return {
80
- declare: () => markClaimChildPending(claim),
79
+ declare: () => (0, ownership_claim_exports.markClaimChildPending)(claim),
81
80
  record: (pid) => {
82
81
  if (pid === null) {
83
82
  forget();
84
83
  return;
85
84
  }
86
85
  groups.push(pid);
87
- const captured = captureProcessIdentity(pid);
86
+ const captured = (0, process_identity_exports.captureProcessIdentity)(pid);
88
87
  let failure = captured.ok ? null : captured.reason;
89
88
  if (captured.ok) try {
90
- setClaimChild(claim, {
89
+ (0, ownership_claim_exports.setClaimChild)(claim, {
91
90
  pid,
92
91
  processToken: captured.token
93
92
  });
@@ -95,7 +94,7 @@ function installerClaim(claim, { now, progressMs, out }) {
95
94
  } catch (error) {
96
95
  failure = error?.message ?? String(error);
97
96
  }
98
- if (!processGroupAlive(pid)) {
97
+ if (!(0, ownership_claim_exports.processGroupAlive)(pid)) {
99
98
  forget();
100
99
  return;
101
100
  }
@@ -132,7 +131,7 @@ function installerClaim(claim, { now, progressMs, out }) {
132
131
  }
133
132
  function timeoutError(holder, elapsedMs, root) {
134
133
  const who = holder ? `${warmCommand(holder.phase)} (pid ${holder.pid})` : "another stim worktree warm";
135
- const error = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsedMs)} for ${who} to release the warm lock on this repository. The lock is ${root}; remove it if nothing is really warming:\n ${claimRemoveCommand(root)}`);
134
+ const error = /* @__PURE__ */ new Error(`Waited ${formatElapsed(elapsedMs)} for ${who} to release the warm lock on this repository. The lock is ${root}; remove it if nothing is really warming:\n ${(0, ownership_claim_exports.claimRemoveCommand)(root)}`);
136
135
  error.code = "STIM_LOCK_TIMEOUT";
137
136
  return error;
138
137
  }
@@ -144,7 +143,7 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
144
143
  let pending = null;
145
144
  try {
146
145
  for (;;) {
147
- const attempt = pending ? settleClaim(pending) : tryAcquireClaim({
146
+ const attempt = pending ? (0, ownership_claim_exports.settleClaim)(pending) : (0, ownership_claim_exports.tryAcquireClaim)({
148
147
  root,
149
148
  mode,
150
149
  label: WARM_CLAIM_LABEL
@@ -164,7 +163,7 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
164
163
  installer,
165
164
  release: () => {
166
165
  if (installer.holding()) return;
167
- releaseClaim(acquired);
166
+ (0, ownership_claim_exports.releaseClaim)(acquired);
168
167
  }
169
168
  };
170
169
  }
@@ -180,13 +179,13 @@ async function acquireWarmClaim({ repositoryRoot, phase, mode = phase === "refre
180
179
  await sleep(pollMs);
181
180
  }
182
181
  } catch (error) {
183
- releaseClaim(pending);
182
+ (0, ownership_claim_exports.releaseClaim)(pending);
184
183
  if ([
185
184
  "EACCES",
186
185
  "EPERM",
187
186
  "EROFS",
188
187
  "ENOENT"
189
- ].includes(error?.code ?? "")) throw new ClaimUnavailableError(error.message, { claimPath: root });
188
+ ].includes(error?.code ?? "")) throw new ownership_claim_exports.ClaimUnavailableError(error.message, { claimPath: root });
190
189
  throw error;
191
190
  }
192
191
  }
@@ -883,7 +882,7 @@ function registerWarm(worktree) {
883
882
  } catch (error) {
884
883
  const code = error?.code;
885
884
  console.error(chalk.red(`Could not warm this worktree: ${error.message}`));
886
- const claim = claimFailure(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
885
+ const claim = (0, ownership_claim_exports.claimFailure)(error, `stim worktree warm${opts.refresh ? " --refresh" : ""}`);
887
886
  if (claim) console.error(chalk.dim(claim.remedy));
888
887
  if (typeof code === "string" && code.startsWith("STIM_")) console.error(chalk.red(`failed: ${code}`));
889
888
  process.exitCode = 1;