stim 1.3.0 → 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 (65) hide show
  1. package/README.md +17 -3
  2. package/dist/{android-CqcW1hrP.mjs → android-BTR1KTMP.mjs} +206 -120
  3. package/dist/{android-BqVIJoG7.mjs → android-BkM4qTaN.mjs} +76 -77
  4. package/dist/{android-cas-BR5YJBYv.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-C3-R4XQr.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-Spz8ozgo.mjs → deps-Ck4wuTwa.mjs} +1 -1
  15. package/dist/{dev-client-BjisHDB5.mjs → dev-client-Cd0v7iZk.mjs} +12 -12
  16. package/dist/{device-BkiW-gkk.mjs → device-CKXLnRRb.mjs} +7 -7
  17. package/dist/device-CMDFzx7C.mjs +1136 -0
  18. package/dist/{device-lease-BJYybF-2.mjs → device-lease-DDJr85OK.mjs} +3 -3
  19. package/dist/{device-pool-CPbLGfrB.mjs → device-pool-DJvbIBGW.mjs} +4 -4
  20. package/dist/{device-remote-AkYCKS8U.mjs → device-remote-CMfHhoc8.mjs} +7 -6
  21. package/dist/{doctor-Bmv86UUW.mjs → doctor-B_mfURdt.mjs} +536 -114
  22. package/dist/{error-diagnostics-DHC31rXA.mjs → error-diagnostics-_VmkqRZo.mjs} +8 -7
  23. package/dist/{gc-GGu-8ULN.mjs → gc-0AyxmECv.mjs} +61 -27
  24. package/dist/{guide-92CPA21s.mjs → guide-CGxdmQ0K.mjs} +236 -67
  25. package/dist/{ios-BSXFeO5H.mjs → ios-CtmGLx3X.mjs} +2667 -2607
  26. package/dist/{ios-device-CWOM_S44.mjs → ios-device-DJOi9FR5.mjs} +1 -1
  27. package/dist/{logs-DPNF4C9s.mjs → logs-BF3qPA7w.mjs} +32 -7
  28. package/dist/{logs-query-R-Cqc6Qv.mjs → logs-query-Cdp5aaCH.mjs} +1 -1
  29. package/dist/metro-DFLmaVz8.mjs +99 -0
  30. package/dist/named-ports-BO7Zpjp0.mjs +137 -0
  31. package/dist/{ownership-8O073GIC.mjs → ownership-BvSpZVBk.mjs} +5 -5
  32. package/dist/{ownership-BpCKiRlQ.mjs → ownership-C08Yzrp7.mjs} +6 -6
  33. package/dist/ports-BiKqP92J.mjs +39 -0
  34. package/dist/ports-ClqG8GPt.mjs +90 -0
  35. package/dist/{project-fKmuIaMI.mjs → project-CBFCwnN5.mjs} +2 -2
  36. package/dist/{reclaim-iW9Cad7k.mjs → reclaim-DpDVo-AP.mjs} +20 -13
  37. package/dist/{reload-DazHDSFA.mjs → reload-ByTqg6ZI.mjs} +8 -8
  38. package/dist/rolldown-runtime-BsOwBuO_.mjs +30 -0
  39. package/dist/{server-bare-DEzPZLvf.mjs → server-bare-Buk-k8tI.mjs} +3 -6
  40. package/dist/{server-expo-DizT4OMk.mjs → server-expo-D4P7iTvf.mjs} +3 -3
  41. package/dist/server-expo-Dho5Raiw.mjs +2 -0
  42. package/dist/{settings-DEJf6_kJ.mjs → settings-DwfltIXQ.mjs} +2 -2
  43. package/dist/{slot-launch-cci0xvmC.mjs → slot-launch-DeKen9e5.mjs} +4 -4
  44. package/dist/{start-mObjQH2W.mjs → start-BpKUHcU_.mjs} +16 -14
  45. package/dist/state-CDbvmZdt.mjs +131 -0
  46. package/dist/{stats-BR-kwj0B.mjs → stats-C59QLKm2.mjs} +1 -1
  47. package/dist/{stats-CaaQr4yN.mjs → stats-k5qMjyxo.mjs} +4 -4
  48. package/dist/{status-BxQNqu9t.mjs → status-Y2ij3dm6.mjs} +13 -12
  49. package/dist/{stop-DimGXEKY.mjs → stop-_P2OQwmZ.mjs} +15 -14
  50. package/dist/{stop-CrHGcjrV.mjs → stop-vKmmxhea.mjs} +2 -2
  51. package/dist/supervisor-run.d.mts +3 -27
  52. package/dist/supervisor-run.mjs +7 -6
  53. package/dist/{teardown-ClFp1Aax.mjs → teardown-KsPpvldq.mjs} +248 -110
  54. package/dist/{workspace-process-lock-3NcQnZ0X.mjs → workspace-process-lock-UEIrvZMJ.mjs} +4 -4
  55. package/dist/workspace-state-Do1e2mOH.mjs +146 -0
  56. package/dist/{worktree-DxTgN1CJ.mjs → worktree-BvcAIMdJ.mjs} +132 -123
  57. package/dist/{worktree-Eq6TLH8N.mjs → worktree-DWNx4tUg.mjs} +32 -2
  58. package/dist/xcode-D9tVItl-.mjs +860 -0
  59. package/package.json +7 -7
  60. package/dist/doctor-fAMXC4vl.mjs +0 -530
  61. package/dist/metro-CORqXyTL.mjs +0 -184
  62. package/dist/ownership-claim-Bf2CAfw3.mjs +0 -570
  63. package/dist/server-expo-44em5Pmg.mjs +0 -2
  64. package/dist/state-B3XA2wp2.mjs +0 -268
  65. package/dist/xcode-D7sAp-Zy.mjs +0 -1858
@@ -1,18 +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 { 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-BqVIJoG7.mjs";
8
- 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";
9
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";
10
11
  import { createRequire } from "node:module";
11
12
  import { existsSync, lstatSync, readFileSync, renameSync, rmSync } from "node:fs";
12
13
  import { randomUUID } from "node:crypto";
13
14
  import { dirname, join } from "node:path";
14
15
  import { pathToFileURL } from "node:url";
15
- import { format } from "node:util";
16
+ import { format, isDeepStrictEqual } from "node:util";
16
17
  import { AsyncLocalStorage } from "node:async_hooks";
17
18
  //#region src/engine/remote-cache.ts
18
19
  const EAS_PROVIDER_PACKAGE = "eas-build-cache-provider";
@@ -592,8 +593,14 @@ function writeParked(config, platform, records) {
592
593
  function oldestFirst(records) {
593
594
  return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
594
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
+ }
595
602
  function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
596
- 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));
597
604
  }
598
605
  function evictOverflow(records, max) {
599
606
  if (records.length <= max) return {
@@ -623,103 +630,167 @@ function parkSim({ platform, projectPath, slot = "default", record, max }) {
623
630
  });
624
631
  }
625
632
  function adoptParked({ platform, projectPath, slot = "default", udid, device }) {
626
- return withConfigLock(() => {
627
- const cfg = ensureConfig();
628
- const records = readParked(platform, { config: cfg });
629
- const taken = records.find((r) => r.udid === udid);
630
- if (!taken || taken.deletionClaim !== void 0) return null;
631
- if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
632
- writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
633
- const project = cfg.projects[projectPath];
634
- if (!project) throw new Error(`Project not registered: ${projectPath}`);
635
- assignSlotDevice(project, platform, device, slot);
636
- saveConfig(cfg);
637
- return taken;
638
- });
639
- }
640
- function isProcessAlive(pid) {
633
+ const claim = claimParkedOperation(platform, udid);
634
+ if (!claim) return null;
641
635
  try {
642
- process.kill(pid, 0);
643
- return true;
644
- } catch (error) {
645
- 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);
646
653
  }
647
654
  }
648
- function parseDeletionClaim(value) {
649
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
650
- const claim = value;
651
- if (!Number.isSafeInteger(claim.pid) || claim.pid <= 0) return null;
652
- if (typeof claim.token !== "string" || claim.token.length === 0) return null;
653
- return {
654
- pid: claim.pid,
655
- token: claim.token
656
- };
657
- }
658
- function claimParkedRemoval(platform, udid) {
659
- return withConfigLock(() => {
660
- const cfg = loadConfig();
661
- if (!cfg) return null;
662
- const records = readParked(platform, { config: cfg });
663
- const record = records.find((candidate) => candidate.udid === udid);
664
- if (!record) return null;
665
- const existingClaim = parseDeletionClaim(record.deletionClaim);
666
- if (record.deletionClaim !== void 0 && (!existingClaim || isProcessAlive(existingClaim.pid))) return null;
667
- const token = randomUUID();
668
- const claimedRecord = {
669
- ...record,
670
- deletionClaim: {
671
- pid: process.pid,
672
- token
673
- }
674
- };
675
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? claimedRecord : candidate));
676
- saveConfig(cfg);
677
- const claimed = { ...record };
678
- delete claimed.deletionClaim;
679
- return {
680
- record: claimed,
681
- token
682
- };
683
- });
684
- }
685
- function clearParkedRemovalClaim(platform, udid, token) {
686
- withConfigLock(() => {
687
- const cfg = loadConfig();
688
- if (!cfg) return;
689
- const records = readParked(platform, { config: cfg });
690
- const record = records.find((candidate) => candidate.udid === udid);
691
- if (!record || parseDeletionClaim(record.deletionClaim)?.token !== token) return;
692
- const restored = { ...record };
693
- delete restored.deletionClaim;
694
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? restored : candidate));
695
- 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}`
696
660
  });
661
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
662
+ return attempt.acquired ?? null;
697
663
  }
698
664
  function removeParkedAfter(platform, udid, beforeRemove) {
699
- const claim = claimParkedRemoval(platform, udid);
665
+ const claim = claimParkedOperation(platform, udid);
700
666
  if (!claim) return null;
701
667
  try {
702
- beforeRemove(claim.record);
703
- } 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;
704
694
  try {
705
- clearParkedRemovalClaim(platform, udid, claim.token);
706
- } catch {}
707
- 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);
708
717
  }
709
- return withConfigLock(() => {
710
- const cfg = loadConfig();
711
- if (!cfg) return null;
712
- const records = readParked(platform, { config: cfg });
713
- if (parseDeletionClaim(records.find((candidate) => candidate.udid === udid)?.deletionClaim)?.token !== claim.token) return null;
714
- writeParked(cfg, platform, records.filter((candidate) => candidate.udid !== udid));
715
- saveConfig(cfg);
716
- return claim.record;
717
- });
718
718
  }
719
719
  function dropParked(platform, udid) {
720
720
  return removeParkedAfter(platform, udid, () => {}) !== null;
721
721
  }
722
722
  //#endregion
723
+ //#region src/agent-device-cleanup.ts
724
+ function parseAgentDeviceSessions(output) {
725
+ const result = JSON.parse(output);
726
+ if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
727
+ return result.data.sessions.flatMap((entry) => {
728
+ if (!entry || typeof entry !== "object") return [];
729
+ const row = entry;
730
+ const id = row.platform === "ios" ? row.device_udid : row.id;
731
+ if (row.platform !== "ios" && row.platform !== "android" || typeof row.name !== "string" || !row.name.trim() || typeof id !== "string" || !id || typeof row.createdAt !== "number" || !Number.isFinite(row.createdAt) || row.platform === "ios" && row.id !== void 0 && row.id !== id) return [];
732
+ return [{
733
+ name: row.name,
734
+ platform: row.platform,
735
+ id,
736
+ createdAt: row.createdAt
737
+ }];
738
+ });
739
+ }
740
+ function report(message) {
741
+ process.stderr.write(`${phaseLine("device", message)}\n`);
742
+ }
743
+ function closeOwnedDeviceSessions(device, stillOwned) {
744
+ const exec = getExecutor();
745
+ const deadline = Date.now() + 15e3;
746
+ const run = (args) => {
747
+ const remaining = deadline - Date.now();
748
+ if (remaining <= 0) throw new Error("agent-device cleanup timed out");
749
+ return exec.runFile("agent-device", [
750
+ ...args,
751
+ "--json",
752
+ "--daemon-transport",
753
+ "socket"
754
+ ], {
755
+ timeoutMs: Math.min(5e3, remaining),
756
+ killSignal: "SIGKILL"
757
+ });
758
+ };
759
+ const list = () => parseAgentDeviceSessions(run([
760
+ "session",
761
+ "list",
762
+ "--session",
763
+ "stim-teardown-inventory"
764
+ ]));
765
+ try {
766
+ if (!exec.runQuiet("command -v agent-device", {
767
+ timeoutMs: 2e3,
768
+ killSignal: "SIGKILL"
769
+ })) return;
770
+ const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
771
+ for (const session of sessions) try {
772
+ if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
773
+ if (!stillOwned()) return;
774
+ if (JSON.parse(run([
775
+ "close",
776
+ "--session",
777
+ session.name,
778
+ "--session-lock",
779
+ "reject",
780
+ "--platform",
781
+ device.platform,
782
+ device.platform === "ios" ? "--udid" : "--serial",
783
+ device.id
784
+ ]))?.success !== true) throw new Error("agent-device did not confirm session close");
785
+ report(`closed agent-device session ${session.name} on ${device.id}`);
786
+ } catch (error) {
787
+ report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
788
+ }
789
+ } catch (error) {
790
+ report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
791
+ }
792
+ }
793
+ //#endregion
723
794
  //#region src/teardown.ts
724
795
  function teardownParkedAvd(avdName) {
725
796
  try {
@@ -746,7 +817,13 @@ function teardownParkedAvd(avdName) {
746
817
  }
747
818
  function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
748
819
  try {
749
- const removed = removeParkedAfter("ios", udid, () => deleteSim(udid));
820
+ const removed = removeParkedAfter("ios", udid, () => {
821
+ closeOwnedDeviceSessions({
822
+ platform: "ios",
823
+ id: udid
824
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
825
+ deleteSim(udid);
826
+ });
750
827
  if (!removed) return {
751
828
  status: "skipped",
752
829
  kind: "not-parked",
@@ -812,6 +889,17 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
812
889
  reason: `sim is now named "${resolved.notOwned}", not Stim-owned by name`
813
890
  };
814
891
  if (resolved.missing) return { status: "missing" };
892
+ closeOwnedDeviceSessions({
893
+ platform: "ios",
894
+ id: udid
895
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
896
+ const current = resolveOwnedIosSim(udid);
897
+ if (current.missing) return { status: "missing" };
898
+ if (current.notOwned) return {
899
+ status: "skipped",
900
+ kind: "not-owned",
901
+ reason: "simulator ownership changed before shutdown"
902
+ };
815
903
  shutdownIosSim(udid);
816
904
  const sim = resolved.sim;
817
905
  if (del && park && park.max > 0) try {
@@ -853,18 +941,24 @@ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
853
941
  };
854
942
  }
855
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
+ }
856
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 });
857
958
  const detached = withConfigLock(() => {
858
- const current = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
859
- 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.`);
860
- const config = loadConfig();
861
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
862
- for (const [projectPath, project] of Object.entries(config.projects)) for (const { platforms } of projectDeviceSlots(project)) {
863
- if (platforms.android?.avdName !== candidate.name) continue;
864
- 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.`);
865
- }
866
- 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.`);
867
- 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);
868
962
  const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
869
963
  renameSync(candidate.directory, destination);
870
964
  return destination;
@@ -896,14 +990,31 @@ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
896
990
  label: avdName
897
991
  };
898
992
  }
899
- function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial } = {}) {
900
- 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;
901
1000
  try {
902
- if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
903
- status: "skipped",
904
- kind: "not-owned",
905
- 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)
906
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));
907
1018
  const resolved = resolveAvd(avdName);
908
1019
  if (resolved.notOwned) return {
909
1020
  status: "skipped",
@@ -911,13 +1022,34 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
911
1022
  reason: `AVD ${avdName} is not Stim-owned by name`
912
1023
  };
913
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
+ };
914
1029
  if (orphanedDirectory) return {
915
1030
  status: "skipped",
916
1031
  reason: "AVD registration appeared; its data was kept."
917
1032
  };
918
1033
  const serial = resolved.serial;
919
- if (serial) waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
920
- else assertStopped(avdName);
1034
+ if (serial) {
1035
+ const stillOwned = () => {
1036
+ const current = resolveAvd(avdName);
1037
+ return !current.notOwned && !current.missing && current.serial === serial;
1038
+ };
1039
+ closeOwnedDeviceSessions({
1040
+ platform: "android",
1041
+ id: serial
1042
+ }, stillOwned);
1043
+ if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
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
+ });
1052
+ } else assertStopped(avdName);
921
1053
  if (del) {
922
1054
  const current = resolveAvd(avdName);
923
1055
  if (current.notOwned) return {
@@ -928,6 +1060,7 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
928
1060
  if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
929
1061
  if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
930
1062
  assertStopped(avdName);
1063
+ withConfigLock(() => assertAvdReferences(avdName, owner));
931
1064
  if (park && park.max > 0) try {
932
1065
  const systemImage = ownedAvdSystemImage(avdName);
933
1066
  if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
@@ -972,7 +1105,12 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
972
1105
  const directory = ownedAvdDirectory(avdName);
973
1106
  const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
974
1107
  if (directory) paths.push(directory);
975
- deleteAvd(avdName);
1108
+ (0, ownership_claim_exports.markClaimChildPending)(claim);
1109
+ try {
1110
+ deleteAvd(avdName);
1111
+ } finally {
1112
+ (0, ownership_claim_exports.clearClaimChild)(claim);
1113
+ }
976
1114
  const remaining = paths.filter(avdPathExists);
977
1115
  if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
978
1116
  }
@@ -990,4 +1128,4 @@ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory
990
1128
  }
991
1129
  }
992
1130
  //#endregion
993
- 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 { f as releaseClaim, h as tryAcquireClaim } from "./ownership-claim-Bf2CAfw3.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 };