stim 1.2.0 → 1.3.1

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 (59) hide show
  1. package/README.md +34 -3
  2. package/dist/{android-Bcmev2s2.mjs → android-CT-PkXUh.mjs} +157 -62
  3. package/dist/{android-cas-D6HPZ6UQ.mjs → android-cas-DXorPCi_.mjs} +5 -5
  4. package/dist/android-hMj83VpN.mjs +1374 -0
  5. package/dist/{app-install-NFxUahnH.mjs → app-install-DIp9riNx.mjs} +6 -6
  6. package/dist/{build-slots-D0b6sJyo.mjs → build-slots-C4AouyNz.mjs} +2 -2
  7. package/dist/{cache-manifest-Cu4_Znw9.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  8. package/dist/cache-manifest.mjs +1 -1
  9. package/dist/cli.mjs +13 -13
  10. package/dist/collector-run.d.mts +3 -1
  11. package/dist/collector-run.mjs +33 -8
  12. package/dist/{config-CMtIaTk9.mjs → config-CcefMY2Q.mjs} +93 -12
  13. package/dist/{deps-BLBsTNDS.mjs → deps-0Yt0Inly.mjs} +1 -1
  14. package/dist/{dev-client-RpvXxCXV.mjs → dev-client-VfSZNLT4.mjs} +186 -9
  15. package/dist/{device-CoCPmazS.mjs → device-Bbv45Z9Z.mjs} +19 -11
  16. package/dist/{device-lease-Dpik41RY.mjs → device-lease-CJwK7OZr.mjs} +107 -83
  17. package/dist/{device-pool-B5pEVa3t.mjs → device-pool-CcBNTvM7.mjs} +18 -12
  18. package/dist/{device-remote-E81KAdXf.mjs → device-remote-DAA0Zm6Z.mjs} +7 -7
  19. package/dist/{doctor-CqPeMsSm.mjs → doctor-B4EITgQB.mjs} +11 -12
  20. package/dist/{doctor-D_CX7ib5.mjs → doctor-BueDfLYG.mjs} +4 -4
  21. package/dist/{error-diagnostics-Dw8Gn3yw.mjs → error-diagnostics-DwhNBFDU.mjs} +62 -52
  22. package/dist/{gc-B7KkDinF.mjs → gc-BRu62v4o.mjs} +66 -57
  23. package/dist/{guide-BS2Xc24o.mjs → guide-D95E0u3i.mjs} +307 -74
  24. package/dist/{ios-B7KLgIp5.mjs → ios-DAju3TVy.mjs} +183 -54
  25. package/dist/{ios-D1h1Di39.mjs → ios-Deh5js5V.mjs} +78 -40
  26. package/dist/{ios-device-xNt0Lg6v.mjs → ios-device-CWOM_S44.mjs} +1 -1
  27. package/dist/{logs-DcX9AvRF.mjs → logs-CAdwW6c2.mjs} +34 -6
  28. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-Cdp5aaCH.mjs} +23 -6
  29. package/dist/{metro-COslRK_F.mjs → metro-CORqXyTL.mjs} +2 -2
  30. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  31. package/dist/{ownership-CGPart6T.mjs → ownership-8O073GIC.mjs} +3 -3
  32. package/dist/{ownership-CYOyqxUo.mjs → ownership-DXcol4oU.mjs} +3 -3
  33. package/dist/{ownership-claim-7zRxxTfZ.mjs → ownership-claim-BtPtJVmq.mjs} +38 -23
  34. package/dist/{project-BBTVABUz.mjs → project-fKmuIaMI.mjs} +2 -2
  35. package/dist/{reclaim-C5Jmuahx.mjs → reclaim-CUmwucWG.mjs} +80 -74
  36. package/dist/{reload-Z8nKifAk.mjs → reload-CWpXoOgN.mjs} +32 -24
  37. package/dist/{server-bare-DEk37Mz2.mjs → server-bare-DEzPZLvf.mjs} +2 -2
  38. package/dist/server-expo-44em5Pmg.mjs +2 -0
  39. package/dist/{server-expo-C3ktAwUG.mjs → server-expo-DizT4OMk.mjs} +4 -4
  40. package/dist/{settings-DwU1_U_q.mjs → settings-DEJf6_kJ.mjs} +2 -2
  41. package/dist/slot-launch-CZklomj6.mjs +19 -0
  42. package/dist/{start-BrY4EuIg.mjs → start-B5G4NjiA.mjs} +11 -11
  43. package/dist/{state-CZ-ER-8d.mjs → state-B3XA2wp2.mjs} +9 -9
  44. package/dist/{stats-BCWEVLrM.mjs → stats-BR-kwj0B.mjs} +1 -1
  45. package/dist/{stats-BsF15Bti.mjs → stats-CaaQr4yN.mjs} +3 -3
  46. package/dist/{status-DFI1Hfpg.mjs → status-CI9clRSb.mjs} +55 -24
  47. package/dist/{stop-B7DR4awK.mjs → stop-CQhffpmX.mjs} +2 -2
  48. package/dist/{stop-Btg770vV.mjs → stop-CjlgDk0a.mjs} +93 -42
  49. package/dist/supervisor-run.d.mts +1 -2
  50. package/dist/supervisor-run.mjs +5 -5
  51. package/dist/{remote-cache-DDmJ1Roq.mjs → teardown--gESs9ck.mjs} +985 -887
  52. package/dist/{workspace-process-lock-BKfdys1q.mjs → workspace-process-lock-IBj1Towr.mjs} +1 -1
  53. package/dist/{worktree-BPTbI-LN.mjs → worktree-DtQGO9-s.mjs} +121 -111
  54. package/dist/{worktree-C79xx_kM.mjs → worktree-Eq6TLH8N.mjs} +1 -1
  55. package/dist/{xcode-DeKCribl.mjs → xcode-xnxyvJkD.mjs} +159 -96
  56. package/package.json +5 -5
  57. package/dist/android-K63_0UfF.mjs +0 -727
  58. package/dist/build-lock-BsVoLDaq.mjs +0 -645
  59. package/dist/server-expo-Drd7ksHu.mjs +0 -2
@@ -1,12 +1,13 @@
1
1
  import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
- import { C as withConfigLock, g as loadConfig, s as ensureConfig, y as saveConfig } from "./config-CMtIaTk9.mjs";
3
- import { r as createNdjsonWriter } from "./ndjson-DcAtEx_K.mjs";
4
- import { D as workspaceLogsDir } from "./state-CZ-ER-8d.mjs";
5
- import { l as resolvePackageJson } from "./project-BBTVABUz.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";
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";
6
6
  import { n as stripAnsi } from "./process-output-C7IlIzd7.mjs";
7
- 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-D1h1Di39.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-K63_0UfF.mjs";
9
- import { n as expoBinFromPackage, r as expoBinPath, s as findBinUpward } from "./server-expo-C3ktAwUG.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";
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";
@@ -14,981 +15,1078 @@ import { dirname, join } from "node:path";
14
15
  import { pathToFileURL } from "node:url";
15
16
  import { format } from "node:util";
16
17
  import { AsyncLocalStorage } from "node:async_hooks";
17
- const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
18
- const MAX_SETTING = {
19
- ios: {
20
- key: "iosParkedMax",
21
- env: "STIM_POOL_IOS_PARKED_MAX"
22
- },
23
- android: {
24
- key: "androidParkedMax",
25
- env: "STIM_POOL_ANDROID_PARKED_MAX"
26
- }
27
- };
28
- function parseMax(raw, strings) {
29
- const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
30
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
31
- return value;
18
+ //#region src/engine/remote-cache.ts
19
+ const EAS_PROVIDER_PACKAGE = "eas-build-cache-provider";
20
+ const RESOLVE_TIMEOUT_MS = 3e4;
21
+ const UPLOAD_TIMEOUT_MS = 6e4;
22
+ const CONFIG_TIMEOUT_MS = 3e4;
23
+ const DYNAMIC_CONFIG_FILES = [
24
+ "app.config.ts",
25
+ "app.config.js",
26
+ "app.config.mjs",
27
+ "app.config.cjs"
28
+ ];
29
+ const TIMED_OUT = Symbol("timed-out");
30
+ function dynamicConfigFile(root) {
31
+ for (const name of DYNAMIC_CONFIG_FILES) if (existsSync(join(root, name))) return name;
32
+ return null;
32
33
  }
33
- function parkedMaxSetting(platform, { config, env = process.env } = {}) {
34
- const { key, env: envKey } = MAX_SETTING[platform];
35
- const fromEnv = env[envKey];
36
- const explicit = fromEnv !== void 0 && fromEnv !== "";
37
- const pool = (config === void 0 ? loadConfig() : config)?.pool;
38
- if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
39
- max: 0,
40
- error: "Invalid pool value. Expected an object with simulator and emulator bounds."
41
- };
42
- const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
43
- const raw = explicit ? fromEnv : fromConfig;
44
- if (raw === void 0) return {
45
- max: env.STIM_HOME ? 0 : 3,
46
- error: null
47
- };
48
- const parsed = parseMax(raw, explicit);
49
- if (parsed === null) return {
50
- max: 0,
51
- error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
52
- };
53
- return {
54
- max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
55
- error: null
56
- };
34
+ function providerFromConfig(config) {
35
+ if (!config || typeof config !== "object") return null;
36
+ const expoField = config.expo;
37
+ const exp = expoField && typeof expoField === "object" ? expoField : config;
38
+ return exp.buildCacheProvider ?? exp.experiments?.buildCacheProvider ?? null;
57
39
  }
58
- function isParkedRecord(value, platform) {
59
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
60
- const record = value;
61
- return typeof record.udid === "string" && typeof record.name === "string" && record.name.startsWith("stim-") && typeof record.parkedAt === "string" && (platform === "android" ? record.udid === record.name && /^stim-[A-Za-z0-9._-]+$/.test(record.name) && typeof record.systemImage === "string" && typeof record.configuration === "string" : typeof record.deviceTypeIdentifier === "string" && typeof record.runtimeIdentifier === "string" && typeof record.simslimManaged === "boolean" && (record.bundleId === void 0 || typeof record.bundleId === "string") && (record.cacheKey === void 0 || typeof record.cacheKey === "string"));
40
+ function ownerFromConfig(config) {
41
+ if (!config || typeof config !== "object") return null;
42
+ const expoField = config.expo;
43
+ const owner = (expoField && typeof expoField === "object" ? expoField : config).owner;
44
+ return typeof owner === "string" && owner.trim() !== "" ? owner.trim() : null;
62
45
  }
63
- function poolBlock(config) {
64
- const raw = config?.parked;
65
- const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
66
- const read = (platform) => {
67
- const list = block[platform];
68
- return Array.isArray(list) ? list.filter((record) => isParkedRecord(record, platform)) : [];
69
- };
70
- return {
71
- ios: read("ios"),
72
- android: read("android")
46
+ function normalizeProvider(raw) {
47
+ if (raw === null || raw === void 0 || raw === "") return null;
48
+ if (raw === "eas") return {
49
+ name: "eas",
50
+ reference: EAS_PROVIDER_PACKAGE,
51
+ options: {}
73
52
  };
53
+ if (typeof raw === "object") {
54
+ const obj = raw;
55
+ if (typeof obj.plugin === "string" && obj.plugin.trim() !== "") {
56
+ const reference = obj.plugin.trim();
57
+ return {
58
+ name: reference,
59
+ reference,
60
+ options: obj.options && typeof obj.options === "object" ? obj.options : {}
61
+ };
62
+ }
63
+ }
64
+ return { invalid: typeof raw === "string" ? `"${raw}"` : JSON.stringify(raw) };
74
65
  }
75
- function readParked(platform, { config } = {}) {
76
- return poolBlock(config === void 0 ? loadConfig() : config)[platform];
77
- }
78
- function writeParked(config, platform, records) {
79
- config.parked = {
80
- ...poolBlock(config),
81
- [platform]: records
82
- };
66
+ function runOptionsFor(platform) {
67
+ return platform === "android" ? { variant: "debug" } : { configuration: "Debug" };
83
68
  }
84
- function oldestFirst(records) {
85
- return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
69
+ function isProviderPlugin(plugin) {
70
+ if (!plugin || typeof plugin !== "object") return false;
71
+ const p = plugin;
72
+ const resolves = typeof p.resolveBuildCache === "function" || typeof p.resolveRemoteBuildCache === "function";
73
+ const uploads = typeof p.uploadBuildCache === "function" || typeof p.uploadRemoteBuildCache === "function";
74
+ return resolves && uploads;
86
75
  }
87
- function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
88
- return oldestFirst(records.filter((r) => r.deletionClaim === void 0 && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
76
+ function cacheLevel(value) {
77
+ return value === "local" || value === "remote" ? value : false;
89
78
  }
90
- function evictOverflow(records, max) {
91
- if (records.length <= max) return {
92
- keep: [...records],
93
- evicted: []
94
- };
95
- const ordered = oldestFirst(records);
96
- const evicted = ordered.slice(0, ordered.length - max);
97
- const dropped = new Set(evicted.map((r) => r.udid));
98
- return {
99
- keep: records.filter((r) => !dropped.has(r.udid)),
100
- evicted
101
- };
79
+ function exitAfterFlush(code = 0, { exit = (c) => process.exit(c), stream = process.stdout } = {}) {
80
+ try {
81
+ stream.write("", () => exit(code));
82
+ } catch {
83
+ exit(code);
84
+ }
102
85
  }
103
- function parkSim({ platform, projectPath, record, max }) {
104
- return withConfigLock(() => {
105
- const cfg = ensureConfig();
106
- if (platform === "android") {
107
- const current = cfg.projects[projectPath]?.platforms?.android;
108
- if (!current?.owned || current.avdName !== record.udid) throw new Error("The emulator assignment changed before parking.");
86
+ function readProjectConfig(root, { run = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
87
+ const dynamic = dynamicConfigFile(root);
88
+ if (!dynamic) {
89
+ const file = join(root, "app.json");
90
+ if (!existsSync(file)) return {
91
+ config: null,
92
+ source: null
93
+ };
94
+ try {
95
+ return {
96
+ config: JSON.parse(readFileSync(file, "utf-8")),
97
+ source: "app.json"
98
+ };
99
+ } catch (err) {
100
+ return { unavailable: `app.json could not be parsed: ${firstLine(err)}` };
109
101
  }
110
- const { keep, evicted } = evictOverflow([...readParked(platform, { config: cfg }).filter((r) => r.udid !== record.udid), record], max);
111
- writeParked(cfg, platform, [...keep, ...evicted]);
112
- const platforms = cfg.projects?.[projectPath]?.platforms;
113
- if (platforms) delete platforms[platform];
114
- saveConfig(cfg);
115
- return evicted;
116
- });
117
- }
118
- function adoptParked({ platform, projectPath, udid, device }) {
119
- return withConfigLock(() => {
120
- const cfg = ensureConfig();
121
- const records = readParked(platform, { config: cfg });
122
- const taken = records.find((r) => r.udid === udid);
123
- if (!taken || taken.deletionClaim !== void 0) return null;
124
- if (platform === "android" && cfg.projects[projectPath]?.platforms?.android) return null;
125
- writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
126
- const project = cfg.projects[projectPath];
127
- if (!project) throw new Error(`Project not registered: ${projectPath}`);
128
- project.platforms = project.platforms || {};
129
- project.platforms[platform] = device;
130
- saveConfig(cfg);
131
- return taken;
132
- });
102
+ }
103
+ const bin = expoBinPath(root);
104
+ if (!bin) return { unavailable: `${dynamic} is code, so it has to be evaluated to read its buildCacheProvider, and the \`expo\` package is not resolvable from this project (no expo/package.json to read a bin from, and no node_modules/.bin/expo here or in any parent)` };
105
+ const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
106
+ let stdout;
107
+ try {
108
+ stdout = exec(bin, [
109
+ "config",
110
+ "--json",
111
+ root
112
+ ], { timeoutMs });
113
+ } catch (err) {
114
+ return { unavailable: `\`expo config --json\` failed: ${firstLine(err)}` };
115
+ }
116
+ try {
117
+ return {
118
+ config: JSON.parse(String(stdout)),
119
+ source: dynamic
120
+ };
121
+ } catch (err) {
122
+ return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
123
+ }
133
124
  }
134
- function isProcessAlive(pid) {
125
+ async function loadPlugin(projectRoot, reference, { requireFrom = null } = {}) {
126
+ const localRequire = requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"));
127
+ const file = localRequire.resolve(reference);
128
+ let mod;
135
129
  try {
136
- process.kill(pid, 0);
137
- return true;
138
- } catch (error) {
139
- return error.code === "EPERM";
130
+ mod = localRequire(file);
131
+ } catch (err) {
132
+ if (err?.code !== "ERR_REQUIRE_ESM") throw err;
133
+ mod = await import(pathToFileURL(file).href);
140
134
  }
135
+ const plugin = mod?.default ?? mod;
136
+ if (!isProviderPlugin(plugin)) throw new Error(`"${reference}" does not export resolveBuildCache and uploadBuildCache functions`);
137
+ return plugin;
141
138
  }
142
- function parseDeletionClaim(value) {
143
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
144
- const claim = value;
145
- if (!Number.isSafeInteger(claim.pid) || claim.pid <= 0) return null;
146
- if (typeof claim.token !== "string" || claim.token.length === 0) return null;
139
+ async function loadProjectProvider(projectRoot, { isExpo = true, run = null, requireFrom = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
140
+ if (!isExpo) return { none: true };
141
+ const read = readProjectConfig(projectRoot, {
142
+ run,
143
+ timeoutMs
144
+ });
145
+ if (read.unavailable) return { unavailable: read.unavailable };
146
+ const normalized = normalizeProvider(providerFromConfig(read.config));
147
+ if (!normalized) return { none: true };
148
+ if (normalized.invalid) return { unavailable: `buildCacheProvider is ${normalized.invalid}, which is not "eas" or { plugin: <module> }` };
149
+ if (normalized.reference === "@stim-cli/expo-build-cache") return { none: true };
150
+ let plugin;
151
+ try {
152
+ plugin = await loadPlugin(projectRoot, normalized.reference, { requireFrom });
153
+ } catch (err) {
154
+ const missing = err?.code === "MODULE_NOT_FOUND";
155
+ return { unavailable: normalized.name === "eas" && missing ? `the EAS build cache needs the \`${EAS_PROVIDER_PACKAGE}\` package, which is not installed in this project` : `${normalized.name} could not be loaded: ${firstLine(err)}` };
156
+ }
147
157
  return {
148
- pid: claim.pid,
149
- token: claim.token
158
+ provider: {
159
+ plugin,
160
+ options: normalized.options ?? {}
161
+ },
162
+ name: normalized.name,
163
+ owner: ownerFromConfig(read.config)
150
164
  };
151
165
  }
152
- function claimParkedRemoval(platform, udid) {
153
- return withConfigLock(() => {
154
- const cfg = loadConfig();
155
- if (!cfg) return null;
156
- const records = readParked(platform, { config: cfg });
157
- const record = records.find((candidate) => candidate.udid === udid);
158
- if (!record) return null;
159
- const existingClaim = parseDeletionClaim(record.deletionClaim);
160
- if (record.deletionClaim !== void 0 && (!existingClaim || isProcessAlive(existingClaim.pid))) return null;
161
- const token = randomUUID();
162
- const claimedRecord = {
163
- ...record,
164
- deletionClaim: {
165
- pid: process.pid,
166
- token
167
- }
168
- };
169
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? claimedRecord : candidate));
170
- saveConfig(cfg);
171
- const claimed = { ...record };
172
- delete claimed.deletionClaim;
173
- return {
174
- record: claimed,
175
- token
176
- };
177
- });
166
+ const WHOAMI_TIMEOUT_MS = 15e3;
167
+ const EAS_CLI_PACKAGE = "eas-cli";
168
+ const EAS_CLI_BIN = "eas";
169
+ function parseWhoami({ stdout = "", stderr = "", exitCode = 0 } = {}) {
170
+ const out = stripAnsi(stdout);
171
+ const combined = `${out}\n${stripAnsi(stderr)}`;
172
+ if (/^\s*Not logged in\s*$/m.test(combined)) return { loggedOut: true };
173
+ if (exitCode !== 0) return { unknown: firstLine(combined.trim() || `eas whoami exited ${exitCode}`) };
174
+ const lines = out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
175
+ if (lines.length === 0) return { unknown: "eas whoami printed nothing" };
176
+ const raw = lines[0];
177
+ if (raw === void 0) return { unknown: "eas whoami printed nothing" };
178
+ const viaToken = /\(authenticated using EXPO_TOKEN\)\s*$/.test(raw);
179
+ const display = raw.replace(/\s*\(authenticated using EXPO_TOKEN\)\s*$/, "").trim();
180
+ const accounts = [];
181
+ for (const line of lines) {
182
+ const name = /^\u2022\s*(\S+)\s*\(Role:/.exec(line)?.[1];
183
+ if (name !== void 0) accounts.push(name);
184
+ }
185
+ if (accounts.length === 0 && !display.endsWith("(robot)") && display !== "robot") accounts.push(display);
186
+ return {
187
+ loggedIn: true,
188
+ account: display,
189
+ accounts: accounts.length ? accounts : null,
190
+ viaToken
191
+ };
178
192
  }
179
- function clearParkedRemovalClaim(platform, udid, token) {
180
- withConfigLock(() => {
181
- const cfg = loadConfig();
182
- if (!cfg) return;
183
- const records = readParked(platform, { config: cfg });
184
- const record = records.find((candidate) => candidate.udid === udid);
185
- if (!record || parseDeletionClaim(record.deletionClaim)?.token !== token) return;
186
- const restored = { ...record };
187
- delete restored.deletionClaim;
188
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? restored : candidate));
189
- saveConfig(cfg);
193
+ function isEasAuthFailureText(text) {
194
+ const t = stripAnsi(text || "");
195
+ if (t.trim() === "") return false;
196
+ return /not logged in/i.test(t) || /\beas login\b/i.test(t) || /EXPO_TOKEN/.test(t) || /unauthorized/i.test(t) || /not authorized/i.test(t) || /authentication (failed|required)/i.test(t);
197
+ }
198
+ function easAuthNote(status) {
199
+ const { code, account, owner, reason, phase = "resolve" } = status || {};
200
+ const because = reason ? ` (${reason})` : "";
201
+ if (code === "logged-out") return `eas is not authenticated${because} -- run \`eas login\` (or set EXPO_TOKEN); ` + (phase === "upload" ? "this build stayed in the local cache" : "building with the local cache only");
202
+ if (code === "wrong-account") return `eas is authenticated as ${account}, and this project's owner is ${owner} -- run \`eas login\` as a member of ${owner} (or set EXPO_TOKEN); consulting the EAS cache anyway`;
203
+ return null;
204
+ }
205
+ function resolveEasCliBin(projectRoot, { lookupPath = null, timeoutMs = 5e3 } = {}) {
206
+ const fromPackage = expoBinFromPackage(resolvePackageJson(projectRoot, EAS_CLI_PACKAGE), EAS_CLI_BIN);
207
+ if (fromPackage) return {
208
+ file: fromPackage,
209
+ source: "project"
210
+ };
211
+ const shim = findBinUpward(projectRoot, EAS_CLI_BIN);
212
+ if (shim) return {
213
+ file: shim,
214
+ source: "project"
215
+ };
216
+ const onPath = lookupPath ? lookupPath() : getExecutor().runQuiet(`command -v ${EAS_CLI_BIN}`, { timeoutMs });
217
+ const file = (String(onPath || "").split("\n")[0] ?? "").trim();
218
+ return file ? {
219
+ file,
220
+ source: "path"
221
+ } : null;
222
+ }
223
+ const easAuthCache = /* @__PURE__ */ new Map();
224
+ function checkEasAuth({ projectRoot, owner = null, run = null, resolveBin = null, timeoutMs = WHOAMI_TIMEOUT_MS, cache = easAuthCache }) {
225
+ const key = `${projectRoot}::${owner || ""}`;
226
+ if (cache?.has(key)) return cache.get(key);
227
+ const answer = probeEasAuth({
228
+ projectRoot,
229
+ owner,
230
+ run,
231
+ resolveBin,
232
+ timeoutMs
190
233
  });
234
+ cache?.set(key, answer);
235
+ return answer;
191
236
  }
192
- function removeParkedAfter(platform, udid, beforeRemove) {
193
- const claim = claimParkedRemoval(platform, udid);
194
- if (!claim) return null;
237
+ function probeEasAuth({ projectRoot, owner, run, resolveBin, timeoutMs }) {
238
+ const bin = (resolveBin || resolveEasCliBin)(projectRoot);
239
+ if (!bin) return {
240
+ failed: true,
241
+ code: "no-cli",
242
+ reason: "no `eas` executable is resolvable from this project or on PATH",
243
+ remedy: "Install eas-cli (`npm i -g eas-cli`, or as a project devDependency), then run `eas login`."
244
+ };
245
+ const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
246
+ let result;
195
247
  try {
196
- beforeRemove(claim.record);
197
- } catch (error) {
198
- try {
199
- clearParkedRemovalClaim(platform, udid, claim.token);
200
- } catch {}
201
- throw error;
248
+ result = {
249
+ stdout: String(exec(bin.file, ["whoami"], { timeoutMs }) ?? ""),
250
+ exitCode: 0
251
+ };
252
+ } catch (e) {
253
+ const err = e;
254
+ if (err?.code === "ETIMEDOUT" || err?.signal === "SIGTERM" || err?.killed) return { unknown: `eas whoami timed out after ${timeoutMs}ms` };
255
+ result = {
256
+ stdout: String(err?.stdout ?? ""),
257
+ stderr: String(err?.stderr ?? err?.message ?? ""),
258
+ exitCode: typeof err?.status === "number" ? err.status : 1
259
+ };
202
260
  }
203
- return withConfigLock(() => {
204
- const cfg = loadConfig();
205
- if (!cfg) return null;
206
- const records = readParked(platform, { config: cfg });
207
- if (parseDeletionClaim(records.find((candidate) => candidate.udid === udid)?.deletionClaim)?.token !== claim.token) return null;
208
- writeParked(cfg, platform, records.filter((candidate) => candidate.udid !== udid));
209
- saveConfig(cfg);
210
- return claim.record;
261
+ const parsed = parseWhoami(result);
262
+ if (parsed.loggedOut) return {
263
+ failed: true,
264
+ code: "logged-out",
265
+ reason: "eas whoami says \"Not logged in\"",
266
+ remedy: "Run `eas login` (or set EXPO_TOKEN)."
267
+ };
268
+ if (parsed.unknown) return { unknown: parsed.unknown };
269
+ if (owner && Array.isArray(parsed.accounts) && !parsed.accounts.includes(owner)) return {
270
+ failed: true,
271
+ code: "wrong-account",
272
+ account: parsed.account,
273
+ accounts: parsed.accounts,
274
+ owner,
275
+ reason: `eas is authenticated as ${parsed.account} (accounts: ${parsed.accounts.join(", ")}), which does not include this project's owner ${owner}`,
276
+ remedy: `Run \`eas login\` as a member of ${owner}, or set EXPO_TOKEN to a token for it.`
277
+ };
278
+ return {
279
+ ok: true,
280
+ account: parsed.account,
281
+ accounts: parsed.accounts,
282
+ viaToken: parsed.viaToken,
283
+ source: bin.source
284
+ };
285
+ }
286
+ const capturing = [];
287
+ const abandoned = /* @__PURE__ */ new Set();
288
+ let patched = null;
289
+ const PROVIDER_CONTEXT = new AsyncLocalStorage();
290
+ function uploadDestination(lines) {
291
+ const text = (lines || []).map(stripAnsi);
292
+ for (const line of text) {
293
+ const url = /(https?:\/\/[^\s'"]+)/.exec(line)?.[1];
294
+ if (url !== void 0) return url.replace(/[.,)\]]+$/, "");
295
+ }
296
+ for (const line of text) {
297
+ const slug = /\bto\s+(@?[\w.-]+\/[\w.-]+)/i.exec(line)?.[1];
298
+ if (slug !== void 0) return slug.replace(/[.,)\]]+$/, "");
299
+ }
300
+ return null;
301
+ }
302
+ function providerNote(text) {
303
+ return `${"cache".padEnd(11)} ${text}`;
304
+ }
305
+ function currentFrame() {
306
+ const store = PROVIDER_CONTEXT.getStore();
307
+ return store && (capturing.includes(store) || abandoned.has(store)) ? store : null;
308
+ }
309
+ function install() {
310
+ if (patched && process.stdout.write === patched.patchedWrite) return;
311
+ const stdout = process.stdout;
312
+ const write = stdout.write.bind(stdout);
313
+ const log = console.log;
314
+ const passthrough = write;
315
+ const patchedWrite = ((chunk, encoding, callback) => {
316
+ const frame = currentFrame();
317
+ if (!frame) return passthrough(chunk, encoding, callback);
318
+ const cb = typeof encoding === "function" ? encoding : callback;
319
+ absorb(frame, typeof chunk === "string" ? chunk : String(chunk));
320
+ if (typeof cb === "function") cb();
321
+ return true;
211
322
  });
323
+ const patchedLog = (...args) => {
324
+ const frame = currentFrame();
325
+ if (!frame) return log(...args);
326
+ absorb(frame, `${format(...args)}\n`);
327
+ };
328
+ stdout.write = patchedWrite;
329
+ console.log = patchedLog;
330
+ patched = {
331
+ stdout,
332
+ write,
333
+ log,
334
+ patchedWrite,
335
+ patchedLog
336
+ };
212
337
  }
213
- function dropParked(platform, udid) {
214
- return removeParkedAfter(platform, udid, () => {}) !== null;
338
+ function uninstall() {
339
+ if (!patched || capturing.length || abandoned.size) return;
340
+ if (patched.stdout.write === patched.patchedWrite) patched.stdout.write = patched.write;
341
+ if (console.log === patched.patchedLog) console.log = patched.log;
342
+ patched = null;
215
343
  }
216
- //#endregion
217
- //#region src/teardown.ts
218
- function teardownParkedAvd(avdName) {
344
+ function absorb(frame, chunk) {
219
345
  try {
220
- return removeParkedAfter("android", avdName, () => {
221
- const result = teardownOwnedAvd(avdName, {
222
- del: true,
223
- owner: { pool: true }
224
- });
225
- if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
226
- }) ? {
227
- status: "torn-down",
228
- label: avdName
229
- } : {
230
- status: "skipped",
231
- kind: "not-parked",
232
- reason: "emulator is no longer parked"
233
- };
234
- } catch (error) {
235
- return {
236
- status: "failed",
237
- reason: String(error?.message || error)
238
- };
346
+ process.stderr.write(chunk);
347
+ } catch {}
348
+ frame.pending += chunk;
349
+ let index = frame.pending.indexOf("\n");
350
+ while (index !== -1) {
351
+ emitLine(frame, frame.pending.slice(0, index));
352
+ frame.pending = frame.pending.slice(index + 1);
353
+ index = frame.pending.indexOf("\n");
239
354
  }
240
355
  }
241
- function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
356
+ function emitLine(frame, raw) {
357
+ const line = stripAnsi(raw).trim();
358
+ if (line === "") return;
359
+ frame.lines.push(line);
360
+ const writer = frame.writer();
242
361
  try {
243
- const removed = removeParkedAfter("ios", udid, () => deleteSim(udid));
244
- if (!removed) return {
245
- status: "skipped",
246
- kind: "not-parked",
247
- reason: "simulator is no longer parked"
248
- };
249
- return {
250
- status: "torn-down",
251
- label: label ?? removed.name
252
- };
253
- } catch (error) {
254
- return {
255
- status: "failed",
256
- reason: String(error?.message || error)
257
- };
258
- }
362
+ writer?.write({
363
+ src: "build",
364
+ level: "debug",
365
+ event: "provider",
366
+ msg: line
367
+ });
368
+ } catch {}
369
+ frame.onLine?.(line);
259
370
  }
260
- function parkOwnedIosSim(udid, park) {
261
- const resolved = resolveOwnedIosSim(udid);
262
- if (resolved.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
263
- if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
264
- const sim = resolved.sim;
265
- if (sim.state !== "Shutdown") throw new Error(`simulator ${udid} is still ${sim.state} after shutdown`);
266
- const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
267
- const runtime = parseRuntimeVersion(sim.runtime);
268
- if (park.bundleId) {
269
- if (!sim.dataPath) throw new Error(`simulator ${udid} did not report a data path for app cleanup`);
270
- const container = findAppDataContainer(sim.dataPath, park.bundleId);
271
- if (container) clearAppDataContainer(container);
371
+ function beginCapture({ logWriter = null, projectRoot = null, platform = null, onLine = null } = {}) {
372
+ let own = null;
373
+ const frame = {
374
+ lines: [],
375
+ pending: "",
376
+ onLine,
377
+ writer: () => {
378
+ if (logWriter) return logWriter;
379
+ if (own) return own;
380
+ if (!projectRoot || !platform) return null;
381
+ own = createNdjsonWriter(join(workspaceLogsDir(projectRoot), `build-${platform}.ndjson`));
382
+ return own;
383
+ },
384
+ close: () => {
385
+ own?.close?.();
386
+ own = null;
387
+ }
388
+ };
389
+ capturing.push(frame);
390
+ install();
391
+ return frame;
392
+ }
393
+ function endCapture(frame) {
394
+ if (!frame) return;
395
+ const at = capturing.lastIndexOf(frame);
396
+ if (at !== -1) capturing.splice(at, 1);
397
+ abandoned.delete(frame);
398
+ if (frame.pending !== "") {
399
+ const rest = frame.pending;
400
+ frame.pending = "";
401
+ emitLine(frame, rest);
272
402
  }
273
- const name = parkedSimName(sim.udid, {
274
- model,
275
- runtime
403
+ frame.close();
404
+ uninstall();
405
+ }
406
+ function abandonCapture(frame, work) {
407
+ if (!frame) return;
408
+ const at = capturing.lastIndexOf(frame);
409
+ if (at !== -1) capturing.splice(at, 1);
410
+ abandoned.add(frame);
411
+ Promise.resolve(work).then(() => endCapture(frame), () => endCapture(frame));
412
+ }
413
+ async function resolveRemote({ provider, platform, projectRoot, fingerprintHash, runOptions = null, timeoutMs = RESOLVE_TIMEOUT_MS, logWriter = null } = {}) {
414
+ if (!provider?.plugin || !fingerprintHash) return null;
415
+ const opts = runOptions || runOptionsFor(platform);
416
+ const outcome = await withBudget(async () => {
417
+ const props = {
418
+ fingerprintHash: await providerFingerprint({
419
+ provider,
420
+ platform,
421
+ projectRoot,
422
+ runOptions: opts
423
+ }) ?? fingerprintHash,
424
+ platform,
425
+ runOptions: opts,
426
+ projectRoot
427
+ };
428
+ return typeof provider.plugin.resolveBuildCache === "function" ? provider.plugin.resolveBuildCache(props, provider.options) : provider.plugin.resolveRemoteBuildCache(props, provider.options);
429
+ }, timeoutMs, {
430
+ logWriter,
431
+ projectRoot,
432
+ platform
276
433
  });
277
- renameIosSim(sim.udid, name);
278
- const record = {
279
- udid: sim.udid,
280
- name,
281
- deviceTypeIdentifier: sim.deviceTypeIdentifier,
282
- runtimeIdentifier: sim.runtime,
283
- parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
284
- simslimManaged: Boolean(park.simslimManaged),
285
- ...park.bundleId ? { bundleId: park.bundleId } : {},
286
- ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
287
- };
288
- return {
289
- record,
290
- evicted: parkSim({
291
- platform: "ios",
292
- projectPath: park.projectPath,
293
- record,
294
- max: park.max
295
- })
434
+ if (outcome.timedOut) return { timedOut: true };
435
+ if (outcome.error) return { failed: firstLine(outcome.error) };
436
+ const appPath = typeof outcome.value === "string" ? outcome.value.trim() : "";
437
+ if (!appPath) return null;
438
+ if (!existsSync(appPath)) return { failed: `returned ${appPath}, which does not exist` };
439
+ return { appPath };
440
+ }
441
+ async function uploadRemote({ provider, platform, projectRoot, fingerprintHash, buildPath, runOptions = null, timeoutMs = UPLOAD_TIMEOUT_MS, logWriter = null, note = (line) => console.error(line) } = {}) {
442
+ if (!provider?.plugin || !fingerprintHash || !buildPath) return { skipped: true };
443
+ const opts = runOptions || runOptionsFor(platform);
444
+ let announced = false;
445
+ const onLine = (line) => {
446
+ if (announced) return;
447
+ const dest = uploadDestination([line]);
448
+ if (!dest) return;
449
+ announced = true;
450
+ note(providerNote(`uploading to ${dest}`));
296
451
  };
452
+ const outcome = await withBudget(async () => {
453
+ const props = {
454
+ projectRoot,
455
+ platform,
456
+ fingerprintHash: await providerFingerprint({
457
+ provider,
458
+ platform,
459
+ projectRoot,
460
+ runOptions: opts
461
+ }) ?? fingerprintHash,
462
+ buildPath,
463
+ runOptions: opts
464
+ };
465
+ return typeof provider.plugin.uploadBuildCache === "function" ? provider.plugin.uploadBuildCache(props, provider.options) : provider.plugin.uploadRemoteBuildCache(props, provider.options);
466
+ }, timeoutMs, {
467
+ logWriter,
468
+ projectRoot,
469
+ platform,
470
+ onLine
471
+ });
472
+ if (outcome.timedOut) return { timedOut: true };
473
+ if (outcome.error) return { failed: firstLine(outcome.error) };
474
+ const destination = uploadDestination(outcome.lines);
475
+ return destination ? {
476
+ uploaded: true,
477
+ destination
478
+ } : { uploaded: true };
297
479
  }
298
- function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
299
- let parkFallback;
480
+ async function providerFingerprint({ provider, platform, projectRoot, runOptions }) {
481
+ if (typeof provider.plugin.calculateFingerprintHash !== "function") return null;
300
482
  try {
301
- const resolved = resolveOwnedIosSim(udid);
302
- if (resolved.notOwned) return {
303
- status: "skipped",
304
- kind: "not-owned",
305
- reason: `sim is now named "${resolved.notOwned}", not Stim-owned by name`
306
- };
307
- if (resolved.missing) return { status: "missing" };
308
- shutdownIosSim(udid);
309
- const sim = resolved.sim;
310
- if (del && park && park.max > 0) try {
311
- const { record, evicted } = parkOwnedIosSim(udid, park);
312
- const removed = [];
313
- const failures = [];
314
- for (const entry of evicted) {
315
- const result = teardownParkedIosSim(entry.udid, { label: entry.name });
316
- if (result.status === "torn-down") removed.push({
317
- udid: entry.udid,
318
- name: entry.name
319
- });
320
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
321
- }
322
- return {
323
- status: "torn-down",
324
- label: label ?? sim.name ?? udid,
325
- parked: {
326
- udid: record.udid,
327
- name: record.name
328
- },
329
- evicted: removed,
330
- ...failures.length ? { evictionFailures: failures } : {}
483
+ const hash = await provider.plugin.calculateFingerprintHash({
484
+ projectRoot,
485
+ platform,
486
+ runOptions
487
+ }, provider.options);
488
+ return typeof hash === "string" && hash.trim() !== "" ? hash.trim() : null;
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+ async function withBudget(factory, ms, capture) {
494
+ let timer = null;
495
+ const timeout = new Promise((resolve) => {
496
+ timer = setTimeout(() => resolve(TIMED_OUT), ms);
497
+ timer?.unref?.();
498
+ });
499
+ const frame = beginCapture(capture || {});
500
+ const work = Promise.resolve().then(() => PROVIDER_CONTEXT.run(frame, factory)).then((value) => ({ value }), (error) => ({ error }));
501
+ try {
502
+ const settled = await Promise.race([work, timeout]);
503
+ if (settled === TIMED_OUT) {
504
+ abandonCapture(frame, work);
505
+ return {
506
+ timedOut: true,
507
+ lines: frame.lines
331
508
  };
332
- } catch (e) {
333
- parkFallback = String(e?.message || e);
334
509
  }
335
- if (del) deleteIosSim(udid);
336
- return {
337
- status: "torn-down",
338
- label: label ?? sim.name ?? udid,
339
- ...parkFallback ? { parkFallback } : {}
340
- };
341
- } catch (e) {
510
+ endCapture(frame);
342
511
  return {
343
- status: "failed",
344
- reason: String(e?.message || e),
345
- ...parkFallback ? { parkFallback } : {}
512
+ ...settled,
513
+ lines: frame.lines
346
514
  };
515
+ } catch (err) {
516
+ endCapture(frame);
517
+ throw err;
518
+ } finally {
519
+ if (timer) clearTimeout(timer);
347
520
  }
348
521
  }
349
- function removeOrphanedAvdDirectory(candidate, owner) {
350
- const detached = withConfigLock(() => {
351
- const current = listOrphanedAvdDirectories(candidate.name).find((entry) => entry.directory === candidate.directory);
352
- 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.`);
353
- const config = loadConfig();
354
- if (!config) throw new Error("Cannot verify AVD references without a Stim config; the data was kept.");
355
- for (const [projectPath, project] of Object.entries(config.projects)) {
356
- if (project.platforms?.android?.avdName !== candidate.name) continue;
357
- if (!owner || !("projectPath" in owner) || owner.projectPath !== projectPath || !project.platforms.android.owned) throw new Error(`AVD ${candidate.name} is referenced by ${projectPath}; its data was kept.`);
358
- }
359
- 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.`);
360
- assertOwnedAvdStopped(candidate.name, { resolveDirectory: () => candidate.directory });
361
- const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
362
- renameSync(candidate.directory, destination);
363
- return destination;
364
- });
365
- const current = lstatSync(detached);
366
- if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
367
- try {
368
- rmSync(detached, { recursive: true });
369
- } catch (error) {
370
- throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
371
- }
522
+ function firstLine(err) {
523
+ const line = (String(err?.message || err || "unknown error").trim().split("\n")[0] ?? "").trim();
524
+ return line.length > 200 ? `${line.slice(0, 197)}...` : line;
372
525
  }
373
- function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
374
- const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
375
- if (candidates.length === 0) {
376
- if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
377
- return { status: "missing" };
526
+ const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
527
+ const MAX_SETTING = {
528
+ ios: {
529
+ key: "iosParkedMax",
530
+ env: "STIM_POOL_IOS_PARKED_MAX"
531
+ },
532
+ android: {
533
+ key: "androidParkedMax",
534
+ env: "STIM_POOL_ANDROID_PARKED_MAX"
378
535
  }
379
- if (!del) return {
380
- status: "skipped",
381
- reason: "AVD registration is missing; its data was kept."
536
+ };
537
+ function parseMax(raw, strings) {
538
+ const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
539
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
540
+ return value;
541
+ }
542
+ function parkedMaxSetting(platform, { config, env = process.env } = {}) {
543
+ const { key, env: envKey } = MAX_SETTING[platform];
544
+ const fromEnv = env[envKey];
545
+ const explicit = fromEnv !== void 0 && fromEnv !== "";
546
+ const pool = (config === void 0 ? loadConfig() : config)?.pool;
547
+ if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
548
+ max: 0,
549
+ error: "Invalid pool value. Expected an object with simulator and emulator bounds."
550
+ };
551
+ const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
552
+ const raw = explicit ? fromEnv : fromConfig;
553
+ if (raw === void 0) return {
554
+ max: env.STIM_HOME ? 0 : 3,
555
+ error: null
556
+ };
557
+ const parsed = parseMax(raw, explicit);
558
+ if (parsed === null) return {
559
+ max: 0,
560
+ error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
382
561
  };
383
- for (const candidate of candidates) {
384
- if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
385
- removeOrphanedAvdDirectory(candidate, owner);
386
- }
387
562
  return {
388
- status: "torn-down",
389
- label: avdName
563
+ max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
564
+ error: null
390
565
  };
391
566
  }
392
- function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial } = {}) {
393
- let parkFallback;
394
- try {
395
- if (!/^stim-[A-Za-z0-9._-]+$/.test(avdName)) return {
396
- status: "skipped",
397
- kind: "not-owned",
398
- reason: `AVD ${avdName} is not Stim-owned by name`
399
- };
400
- const resolved = resolveAvd(avdName);
401
- if (resolved.notOwned) return {
402
- status: "skipped",
403
- kind: "not-owned",
404
- reason: `AVD ${avdName} is not Stim-owned by name`
405
- };
406
- if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
407
- if (orphanedDirectory) return {
408
- status: "skipped",
409
- reason: "AVD registration appeared; its data was kept."
410
- };
411
- const serial = resolved.serial;
412
- if (serial) waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
413
- else assertStopped(avdName);
414
- if (del) {
415
- const current = resolveAvd(avdName);
416
- if (current.notOwned) return {
417
- status: "skipped",
418
- kind: "not-owned",
419
- reason: `AVD ${avdName} is not Stim-owned by name`
420
- };
421
- if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
422
- if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
423
- assertStopped(avdName);
424
- if (park && park.max > 0) try {
425
- const systemImage = ownedAvdSystemImage(avdName);
426
- if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
427
- const evicted = parkSim({
428
- platform: "android",
429
- projectPath: park.projectPath,
430
- max: park.max,
431
- record: {
432
- udid: avdName,
433
- name: avdName,
434
- systemImage,
435
- configuration: park.configuration,
436
- parkedAt: (/* @__PURE__ */ new Date()).toISOString()
437
- }
438
- });
439
- const removed = [];
440
- const failures = [];
441
- for (const entry of evicted) {
442
- const result = teardownParkedAvd(entry.name);
443
- if (result.status === "torn-down") removed.push({
444
- udid: entry.name,
445
- name: entry.name,
446
- platform: "android"
447
- });
448
- else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
449
- }
450
- return {
451
- status: "torn-down",
452
- label: avdName,
453
- parked: {
454
- udid: avdName,
455
- name: avdName,
456
- platform: "android"
457
- },
458
- evicted: removed,
459
- ...failures.length ? { evictionFailures: failures } : {}
460
- };
461
- } catch (error) {
462
- parkFallback = String(error?.message || error);
463
- }
464
- const directory = ownedAvdDirectory(avdName);
465
- const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
466
- if (directory) paths.push(directory);
467
- deleteAvd(avdName);
468
- const remaining = paths.filter(avdPathExists);
469
- if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
470
- }
471
- return {
472
- status: "torn-down",
473
- label: avdName,
474
- serial: resolved.serial ?? null,
475
- ...parkFallback ? { parkFallback } : {}
476
- };
477
- } catch (e) {
478
- return {
479
- status: "failed",
480
- reason: String(e?.message || e)
481
- };
482
- }
483
- }
484
- //#endregion
485
- //#region src/engine/remote-cache.ts
486
- const EAS_PROVIDER_PACKAGE = "eas-build-cache-provider";
487
- const RESOLVE_TIMEOUT_MS = 3e4;
488
- const UPLOAD_TIMEOUT_MS = 6e4;
489
- const CONFIG_TIMEOUT_MS = 3e4;
490
- const DYNAMIC_CONFIG_FILES = [
491
- "app.config.ts",
492
- "app.config.js",
493
- "app.config.mjs",
494
- "app.config.cjs"
495
- ];
496
- const TIMED_OUT = Symbol("timed-out");
497
- function dynamicConfigFile(root) {
498
- for (const name of DYNAMIC_CONFIG_FILES) if (existsSync(join(root, name))) return name;
499
- return null;
567
+ function isParkedRecord(value, platform) {
568
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
569
+ const record = value;
570
+ return typeof record.udid === "string" && typeof record.name === "string" && record.name.startsWith("stim-") && typeof record.parkedAt === "string" && (platform === "android" ? record.udid === record.name && /^stim-[A-Za-z0-9._-]+$/.test(record.name) && typeof record.systemImage === "string" && typeof record.configuration === "string" : typeof record.deviceTypeIdentifier === "string" && typeof record.runtimeIdentifier === "string" && typeof record.simslimManaged === "boolean" && (record.bundleId === void 0 || typeof record.bundleId === "string") && (record.cacheKey === void 0 || typeof record.cacheKey === "string"));
500
571
  }
501
- function providerFromConfig(config) {
502
- if (!config || typeof config !== "object") return null;
503
- const expoField = config.expo;
504
- const exp = expoField && typeof expoField === "object" ? expoField : config;
505
- return exp.buildCacheProvider ?? exp.experiments?.buildCacheProvider ?? null;
572
+ function poolBlock(config) {
573
+ const raw = config?.parked;
574
+ const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
575
+ const read = (platform) => {
576
+ const list = block[platform];
577
+ return Array.isArray(list) ? list.filter((record) => isParkedRecord(record, platform)) : [];
578
+ };
579
+ return {
580
+ ios: read("ios"),
581
+ android: read("android")
582
+ };
506
583
  }
507
- function ownerFromConfig(config) {
508
- if (!config || typeof config !== "object") return null;
509
- const expoField = config.expo;
510
- const owner = (expoField && typeof expoField === "object" ? expoField : config).owner;
511
- return typeof owner === "string" && owner.trim() !== "" ? owner.trim() : null;
584
+ function readParked(platform, { config } = {}) {
585
+ return poolBlock(config === void 0 ? loadConfig() : config)[platform];
512
586
  }
513
- function normalizeProvider(raw) {
514
- if (raw === null || raw === void 0 || raw === "") return null;
515
- if (raw === "eas") return {
516
- name: "eas",
517
- reference: EAS_PROVIDER_PACKAGE,
518
- options: {}
587
+ function writeParked(config, platform, records) {
588
+ config.parked = {
589
+ ...poolBlock(config),
590
+ [platform]: records
519
591
  };
520
- if (typeof raw === "object") {
521
- const obj = raw;
522
- if (typeof obj.plugin === "string" && obj.plugin.trim() !== "") {
523
- const reference = obj.plugin.trim();
524
- return {
525
- name: reference,
526
- reference,
527
- options: obj.options && typeof obj.options === "object" ? obj.options : {}
528
- };
529
- }
530
- }
531
- return { invalid: typeof raw === "string" ? `"${raw}"` : JSON.stringify(raw) };
532
592
  }
533
- function runOptionsFor(platform) {
534
- return platform === "android" ? { variant: "debug" } : { configuration: "Debug" };
593
+ function oldestFirst(records) {
594
+ return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
595
+ }
596
+ function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
597
+ return oldestFirst(records.filter((r) => r.deletionClaim === void 0 && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
598
+ }
599
+ function evictOverflow(records, max) {
600
+ if (records.length <= max) return {
601
+ keep: [...records],
602
+ evicted: []
603
+ };
604
+ const ordered = oldestFirst(records);
605
+ const evicted = ordered.slice(0, ordered.length - max);
606
+ const dropped = new Set(evicted.map((r) => r.udid));
607
+ return {
608
+ keep: records.filter((r) => !dropped.has(r.udid)),
609
+ evicted
610
+ };
535
611
  }
536
- function isProviderPlugin(plugin) {
537
- if (!plugin || typeof plugin !== "object") return false;
538
- const p = plugin;
539
- const resolves = typeof p.resolveBuildCache === "function" || typeof p.resolveRemoteBuildCache === "function";
540
- const uploads = typeof p.uploadBuildCache === "function" || typeof p.uploadRemoteBuildCache === "function";
541
- return resolves && uploads;
612
+ function parkSim({ platform, projectPath, slot = "default", record, max }) {
613
+ return withConfigLock(() => {
614
+ const cfg = ensureConfig();
615
+ const current = deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform];
616
+ const currentId = platform === "ios" ? current?.deviceUdid : current?.avdName;
617
+ if (!current?.owned || currentId !== record.udid) throw new Error(`The ${platform === "ios" ? "simulator" : "emulator"} assignment changed before parking.`);
618
+ const { keep, evicted } = evictOverflow([...readParked(platform, { config: cfg }).filter((r) => r.udid !== record.udid), record], max);
619
+ writeParked(cfg, platform, [...keep, ...evicted]);
620
+ const project = cfg.projects[projectPath];
621
+ if (project) removeSlotDevice(project, platform, slot);
622
+ saveConfig(cfg);
623
+ return evicted;
624
+ });
542
625
  }
543
- function cacheLevel(value) {
544
- return value === "local" || value === "remote" ? value : false;
626
+ 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
+ });
545
640
  }
546
- function exitAfterFlush(code = 0, { exit = (c) => process.exit(c), stream = process.stdout } = {}) {
641
+ function isProcessAlive(pid) {
547
642
  try {
548
- stream.write("", () => exit(code));
549
- } catch {
550
- exit(code);
643
+ process.kill(pid, 0);
644
+ return true;
645
+ } catch (error) {
646
+ return error.code === "EPERM";
551
647
  }
552
648
  }
553
- function readProjectConfig(root, { run = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
554
- const dynamic = dynamicConfigFile(root);
555
- if (!dynamic) {
556
- const file = join(root, "app.json");
557
- if (!existsSync(file)) return {
558
- config: null,
559
- source: null
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
+ }
560
675
  };
561
- try {
562
- return {
563
- config: JSON.parse(readFileSync(file, "utf-8")),
564
- source: "app.json"
565
- };
566
- } catch (err) {
567
- return { unavailable: `app.json could not be parsed: ${firstLine(err)}` };
568
- }
569
- }
570
- const bin = expoBinPath(root);
571
- if (!bin) return { unavailable: `${dynamic} is code, so it has to be evaluated to read its buildCacheProvider, and the \`expo\` package is not resolvable from this project (no expo/package.json to read a bin from, and no node_modules/.bin/expo here or in any parent)` };
572
- const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
573
- let stdout;
574
- try {
575
- stdout = exec(bin, [
576
- "config",
577
- "--json",
578
- root
579
- ], { timeoutMs });
580
- } catch (err) {
581
- return { unavailable: `\`expo config --json\` failed: ${firstLine(err)}` };
582
- }
583
- try {
676
+ writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? claimedRecord : candidate));
677
+ saveConfig(cfg);
678
+ const claimed = { ...record };
679
+ delete claimed.deletionClaim;
584
680
  return {
585
- config: JSON.parse(String(stdout)),
586
- source: dynamic
681
+ record: claimed,
682
+ token
587
683
  };
588
- } catch (err) {
589
- return { unavailable: `\`expo config --json\` did not print JSON: ${firstLine(err)}` };
590
- }
591
- }
592
- async function loadPlugin(projectRoot, reference, { requireFrom = null } = {}) {
593
- const localRequire = requireFrom ? requireFrom(projectRoot) : createRequire(join(projectRoot, "package.json"));
594
- const file = localRequire.resolve(reference);
595
- let mod;
596
- try {
597
- mod = localRequire(file);
598
- } catch (err) {
599
- if (err?.code !== "ERR_REQUIRE_ESM") throw err;
600
- mod = await import(pathToFileURL(file).href);
601
- }
602
- const plugin = mod?.default ?? mod;
603
- if (!isProviderPlugin(plugin)) throw new Error(`"${reference}" does not export resolveBuildCache and uploadBuildCache functions`);
604
- return plugin;
684
+ });
605
685
  }
606
- async function loadProjectProvider(projectRoot, { isExpo = true, run = null, requireFrom = null, timeoutMs = CONFIG_TIMEOUT_MS } = {}) {
607
- if (!isExpo) return { none: true };
608
- const read = readProjectConfig(projectRoot, {
609
- run,
610
- timeoutMs
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);
611
697
  });
612
- if (read.unavailable) return { unavailable: read.unavailable };
613
- const normalized = normalizeProvider(providerFromConfig(read.config));
614
- if (!normalized) return { none: true };
615
- if (normalized.invalid) return { unavailable: `buildCacheProvider is ${normalized.invalid}, which is not "eas" or { plugin: <module> }` };
616
- if (normalized.reference === "@stim-cli/expo-build-cache") return { none: true };
617
- let plugin;
618
- try {
619
- plugin = await loadPlugin(projectRoot, normalized.reference, { requireFrom });
620
- } catch (err) {
621
- const missing = err?.code === "MODULE_NOT_FOUND";
622
- return { unavailable: normalized.name === "eas" && missing ? `the EAS build cache needs the \`${EAS_PROVIDER_PACKAGE}\` package, which is not installed in this project` : `${normalized.name} could not be loaded: ${firstLine(err)}` };
623
- }
624
- return {
625
- provider: {
626
- plugin,
627
- options: normalized.options ?? {}
628
- },
629
- name: normalized.name,
630
- owner: ownerFromConfig(read.config)
631
- };
632
698
  }
633
- const WHOAMI_TIMEOUT_MS = 15e3;
634
- const EAS_CLI_PACKAGE = "eas-cli";
635
- const EAS_CLI_BIN = "eas";
636
- function parseWhoami({ stdout = "", stderr = "", exitCode = 0 } = {}) {
637
- const out = stripAnsi(stdout);
638
- const combined = `${out}\n${stripAnsi(stderr)}`;
639
- if (/^\s*Not logged in\s*$/m.test(combined)) return { loggedOut: true };
640
- if (exitCode !== 0) return { unknown: firstLine(combined.trim() || `eas whoami exited ${exitCode}`) };
641
- const lines = out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
642
- if (lines.length === 0) return { unknown: "eas whoami printed nothing" };
643
- const raw = lines[0];
644
- if (raw === void 0) return { unknown: "eas whoami printed nothing" };
645
- const viaToken = /\(authenticated using EXPO_TOKEN\)\s*$/.test(raw);
646
- const display = raw.replace(/\s*\(authenticated using EXPO_TOKEN\)\s*$/, "").trim();
647
- const accounts = [];
648
- for (const line of lines) {
649
- const name = /^\u2022\s*(\S+)\s*\(Role:/.exec(line)?.[1];
650
- if (name !== void 0) accounts.push(name);
699
+ function removeParkedAfter(platform, udid, beforeRemove) {
700
+ const claim = claimParkedRemoval(platform, udid);
701
+ if (!claim) return null;
702
+ try {
703
+ beforeRemove(claim.record);
704
+ } catch (error) {
705
+ try {
706
+ clearParkedRemovalClaim(platform, udid, claim.token);
707
+ } catch {}
708
+ throw error;
651
709
  }
652
- if (accounts.length === 0 && !display.endsWith("(robot)") && display !== "robot") accounts.push(display);
653
- return {
654
- loggedIn: true,
655
- account: display,
656
- accounts: accounts.length ? accounts : null,
657
- viaToken
658
- };
659
- }
660
- function isEasAuthFailureText(text) {
661
- const t = stripAnsi(text || "");
662
- if (t.trim() === "") return false;
663
- return /not logged in/i.test(t) || /\beas login\b/i.test(t) || /EXPO_TOKEN/.test(t) || /unauthorized/i.test(t) || /not authorized/i.test(t) || /authentication (failed|required)/i.test(t);
664
- }
665
- function easAuthNote(status) {
666
- const { code, account, owner, reason, phase = "resolve" } = status || {};
667
- const because = reason ? ` (${reason})` : "";
668
- if (code === "logged-out") return `eas is not authenticated${because} -- run \`eas login\` (or set EXPO_TOKEN); ` + (phase === "upload" ? "this build stayed in the local cache" : "building with the local cache only");
669
- if (code === "wrong-account") return `eas is authenticated as ${account}, and this project's owner is ${owner} -- run \`eas login\` as a member of ${owner} (or set EXPO_TOKEN); consulting the EAS cache anyway`;
670
- return null;
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
+ });
671
719
  }
672
- function resolveEasCliBin(projectRoot, { lookupPath = null, timeoutMs = 5e3 } = {}) {
673
- const fromPackage = expoBinFromPackage(resolvePackageJson(projectRoot, EAS_CLI_PACKAGE), EAS_CLI_BIN);
674
- if (fromPackage) return {
675
- file: fromPackage,
676
- source: "project"
677
- };
678
- const shim = findBinUpward(projectRoot, EAS_CLI_BIN);
679
- if (shim) return {
680
- file: shim,
681
- source: "project"
682
- };
683
- const onPath = lookupPath ? lookupPath() : getExecutor().runQuiet(`command -v ${EAS_CLI_BIN}`, { timeoutMs });
684
- const file = (String(onPath || "").split("\n")[0] ?? "").trim();
685
- return file ? {
686
- file,
687
- source: "path"
688
- } : null;
720
+ function dropParked(platform, udid) {
721
+ return removeParkedAfter(platform, udid, () => {}) !== null;
689
722
  }
690
- const easAuthCache = /* @__PURE__ */ new Map();
691
- function checkEasAuth({ projectRoot, owner = null, run = null, resolveBin = null, timeoutMs = WHOAMI_TIMEOUT_MS, cache = easAuthCache }) {
692
- const key = `${projectRoot}::${owner || ""}`;
693
- if (cache?.has(key)) return cache.get(key);
694
- const answer = probeEasAuth({
695
- projectRoot,
696
- owner,
697
- run,
698
- resolveBin,
699
- timeoutMs
723
+ //#endregion
724
+ //#region src/agent-device-cleanup.ts
725
+ function parseAgentDeviceSessions(output) {
726
+ const result = JSON.parse(output);
727
+ if (result?.success !== true || !Array.isArray(result.data?.sessions)) throw new Error("agent-device did not return a successful session list");
728
+ return result.data.sessions.flatMap((entry) => {
729
+ if (!entry || typeof entry !== "object") return [];
730
+ const row = entry;
731
+ const id = row.platform === "ios" ? row.device_udid : row.id;
732
+ 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 [];
733
+ return [{
734
+ name: row.name,
735
+ platform: row.platform,
736
+ id,
737
+ createdAt: row.createdAt
738
+ }];
700
739
  });
701
- cache?.set(key, answer);
702
- return answer;
703
740
  }
704
- function probeEasAuth({ projectRoot, owner, run, resolveBin, timeoutMs }) {
705
- const bin = (resolveBin || resolveEasCliBin)(projectRoot);
706
- if (!bin) return {
707
- failed: true,
708
- code: "no-cli",
709
- reason: "no `eas` executable is resolvable from this project or on PATH",
710
- remedy: "Install eas-cli (`npm i -g eas-cli`, or as a project devDependency), then run `eas login`."
741
+ function report(message) {
742
+ process.stderr.write(`${phaseLine("device", message)}\n`);
743
+ }
744
+ function closeOwnedDeviceSessions(device, stillOwned) {
745
+ const exec = getExecutor();
746
+ const deadline = Date.now() + 15e3;
747
+ const run = (args) => {
748
+ const remaining = deadline - Date.now();
749
+ if (remaining <= 0) throw new Error("agent-device cleanup timed out");
750
+ return exec.runFile("agent-device", [
751
+ ...args,
752
+ "--json",
753
+ "--daemon-transport",
754
+ "socket"
755
+ ], {
756
+ timeoutMs: Math.min(5e3, remaining),
757
+ killSignal: "SIGKILL"
758
+ });
711
759
  };
712
- const exec = run || ((file, args, opts) => getExecutor().runFile(file, args, opts));
713
- let result;
760
+ const list = () => parseAgentDeviceSessions(run([
761
+ "session",
762
+ "list",
763
+ "--session",
764
+ "stim-teardown-inventory"
765
+ ]));
714
766
  try {
715
- result = {
716
- stdout: String(exec(bin.file, ["whoami"], { timeoutMs }) ?? ""),
717
- exitCode: 0
767
+ if (!exec.runQuiet("command -v agent-device", {
768
+ timeoutMs: 2e3,
769
+ killSignal: "SIGKILL"
770
+ })) return;
771
+ const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
772
+ for (const session of sessions) try {
773
+ if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
774
+ if (!stillOwned()) return;
775
+ if (JSON.parse(run([
776
+ "close",
777
+ "--session",
778
+ session.name,
779
+ "--session-lock",
780
+ "reject",
781
+ "--platform",
782
+ device.platform,
783
+ device.platform === "ios" ? "--udid" : "--serial",
784
+ device.id
785
+ ]))?.success !== true) throw new Error("agent-device did not confirm session close");
786
+ report(`closed agent-device session ${session.name} on ${device.id}`);
787
+ } catch (error) {
788
+ report(`could not close agent-device session ${session.name} on ${device.id}: ${error.message}`);
789
+ }
790
+ } catch (error) {
791
+ report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
792
+ }
793
+ }
794
+ //#endregion
795
+ //#region src/teardown.ts
796
+ function teardownParkedAvd(avdName) {
797
+ try {
798
+ return removeParkedAfter("android", avdName, () => {
799
+ const result = teardownOwnedAvd(avdName, {
800
+ del: true,
801
+ owner: { pool: true }
802
+ });
803
+ if (result.status !== "torn-down" && result.status !== "missing") throw new Error(result.reason);
804
+ }) ? {
805
+ status: "torn-down",
806
+ label: avdName
807
+ } : {
808
+ status: "skipped",
809
+ kind: "not-parked",
810
+ reason: "emulator is no longer parked"
718
811
  };
719
- } catch (e) {
720
- const err = e;
721
- if (err?.code === "ETIMEDOUT" || err?.signal === "SIGTERM" || err?.killed) return { unknown: `eas whoami timed out after ${timeoutMs}ms` };
722
- result = {
723
- stdout: String(err?.stdout ?? ""),
724
- stderr: String(err?.stderr ?? err?.message ?? ""),
725
- exitCode: typeof err?.status === "number" ? err.status : 1
812
+ } catch (error) {
813
+ return {
814
+ status: "failed",
815
+ reason: String(error?.message || error)
726
816
  };
727
817
  }
728
- const parsed = parseWhoami(result);
729
- if (parsed.loggedOut) return {
730
- failed: true,
731
- code: "logged-out",
732
- reason: "eas whoami says \"Not logged in\"",
733
- remedy: "Run `eas login` (or set EXPO_TOKEN)."
734
- };
735
- if (parsed.unknown) return { unknown: parsed.unknown };
736
- if (owner && Array.isArray(parsed.accounts) && !parsed.accounts.includes(owner)) return {
737
- failed: true,
738
- code: "wrong-account",
739
- account: parsed.account,
740
- accounts: parsed.accounts,
741
- owner,
742
- reason: `eas is authenticated as ${parsed.account} (accounts: ${parsed.accounts.join(", ")}), which does not include this project's owner ${owner}`,
743
- remedy: `Run \`eas login\` as a member of ${owner}, or set EXPO_TOKEN to a token for it.`
744
- };
745
- return {
746
- ok: true,
747
- account: parsed.account,
748
- accounts: parsed.accounts,
749
- viaToken: parsed.viaToken,
750
- source: bin.source
751
- };
752
818
  }
753
- const capturing = [];
754
- const abandoned = /* @__PURE__ */ new Set();
755
- let patched = null;
756
- const PROVIDER_CONTEXT = new AsyncLocalStorage();
757
- function uploadDestination(lines) {
758
- const text = (lines || []).map(stripAnsi);
759
- for (const line of text) {
760
- const url = /(https?:\/\/[^\s'"]+)/.exec(line)?.[1];
761
- if (url !== void 0) return url.replace(/[.,)\]]+$/, "");
762
- }
763
- for (const line of text) {
764
- const slug = /\bto\s+(@?[\w.-]+\/[\w.-]+)/i.exec(line)?.[1];
765
- if (slug !== void 0) return slug.replace(/[.,)\]]+$/, "");
819
+ function teardownParkedIosSim(udid, { label, deleteSim = deleteParkedIosSim } = {}) {
820
+ try {
821
+ const removed = removeParkedAfter("ios", udid, () => {
822
+ closeOwnedDeviceSessions({
823
+ platform: "ios",
824
+ id: udid
825
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
826
+ deleteSim(udid);
827
+ });
828
+ if (!removed) return {
829
+ status: "skipped",
830
+ kind: "not-parked",
831
+ reason: "simulator is no longer parked"
832
+ };
833
+ return {
834
+ status: "torn-down",
835
+ label: label ?? removed.name
836
+ };
837
+ } catch (error) {
838
+ return {
839
+ status: "failed",
840
+ reason: String(error?.message || error)
841
+ };
766
842
  }
767
- return null;
768
843
  }
769
- function providerNote(text) {
770
- return `${"cache".padEnd(11)} ${text}`;
771
- }
772
- function currentFrame() {
773
- const store = PROVIDER_CONTEXT.getStore();
774
- return store && (capturing.includes(store) || abandoned.has(store)) ? store : null;
775
- }
776
- function install() {
777
- if (patched && process.stdout.write === patched.patchedWrite) return;
778
- const stdout = process.stdout;
779
- const write = stdout.write.bind(stdout);
780
- const log = console.log;
781
- const passthrough = write;
782
- const patchedWrite = ((chunk, encoding, callback) => {
783
- const frame = currentFrame();
784
- if (!frame) return passthrough(chunk, encoding, callback);
785
- const cb = typeof encoding === "function" ? encoding : callback;
786
- absorb(frame, typeof chunk === "string" ? chunk : String(chunk));
787
- if (typeof cb === "function") cb();
788
- return true;
844
+ function parkOwnedIosSim(udid, park) {
845
+ const resolved = resolveOwnedIosSim(udid);
846
+ if (resolved.missing) throw new Error(`simulator ${udid} disappeared after shutdown`);
847
+ if (resolved.notOwned) throw new Error(`simulator ${udid} is now named ${JSON.stringify(resolved.notOwned)}`);
848
+ const sim = resolved.sim;
849
+ if (sim.state !== "Shutdown") throw new Error(`simulator ${udid} is still ${sim.state} after shutdown`);
850
+ const model = listIosDeviceTypes().find((d) => d.identifier === sim.deviceTypeIdentifier)?.name ?? null;
851
+ const runtime = parseRuntimeVersion(sim.runtime);
852
+ if (park.bundleId) {
853
+ if (!sim.dataPath) throw new Error(`simulator ${udid} did not report a data path for app cleanup`);
854
+ const container = findAppDataContainer(sim.dataPath, park.bundleId);
855
+ if (container) clearAppDataContainer(container);
856
+ }
857
+ const name = parkedSimName(sim.udid, {
858
+ model,
859
+ runtime
789
860
  });
790
- const patchedLog = (...args) => {
791
- const frame = currentFrame();
792
- if (!frame) return log(...args);
793
- absorb(frame, `${format(...args)}\n`);
861
+ renameIosSim(sim.udid, name);
862
+ const record = {
863
+ udid: sim.udid,
864
+ name,
865
+ deviceTypeIdentifier: sim.deviceTypeIdentifier,
866
+ runtimeIdentifier: sim.runtime,
867
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString(),
868
+ simslimManaged: Boolean(park.simslimManaged),
869
+ ...park.bundleId ? { bundleId: park.bundleId } : {},
870
+ ...park.cacheKey ? { cacheKey: park.cacheKey } : {}
794
871
  };
795
- stdout.write = patchedWrite;
796
- console.log = patchedLog;
797
- patched = {
798
- stdout,
799
- write,
800
- log,
801
- patchedWrite,
802
- patchedLog
872
+ return {
873
+ record,
874
+ evicted: parkSim({
875
+ platform: "ios",
876
+ projectPath: park.projectPath,
877
+ slot: park.slot,
878
+ record,
879
+ max: park.max
880
+ })
803
881
  };
804
882
  }
805
- function uninstall() {
806
- if (!patched || capturing.length || abandoned.size) return;
807
- if (patched.stdout.write === patched.patchedWrite) patched.stdout.write = patched.write;
808
- if (console.log === patched.patchedLog) console.log = patched.log;
809
- patched = null;
810
- }
811
- function absorb(frame, chunk) {
812
- try {
813
- process.stderr.write(chunk);
814
- } catch {}
815
- frame.pending += chunk;
816
- let index = frame.pending.indexOf("\n");
817
- while (index !== -1) {
818
- emitLine(frame, frame.pending.slice(0, index));
819
- frame.pending = frame.pending.slice(index + 1);
820
- index = frame.pending.indexOf("\n");
821
- }
822
- }
823
- function emitLine(frame, raw) {
824
- const line = stripAnsi(raw).trim();
825
- if (line === "") return;
826
- frame.lines.push(line);
827
- const writer = frame.writer();
883
+ function teardownOwnedIosSim(udid, { del = false, label, park } = {}) {
884
+ let parkFallback;
828
885
  try {
829
- writer?.write({
830
- src: "build",
831
- level: "debug",
832
- event: "provider",
833
- msg: line
834
- });
835
- } catch {}
836
- frame.onLine?.(line);
837
- }
838
- function beginCapture({ logWriter = null, projectRoot = null, platform = null, onLine = null } = {}) {
839
- let own = null;
840
- const frame = {
841
- lines: [],
842
- pending: "",
843
- onLine,
844
- writer: () => {
845
- if (logWriter) return logWriter;
846
- if (own) return own;
847
- if (!projectRoot || !platform) return null;
848
- own = createNdjsonWriter(join(workspaceLogsDir(projectRoot), `build-${platform}.ndjson`));
849
- return own;
850
- },
851
- close: () => {
852
- own?.close?.();
853
- own = null;
886
+ const resolved = resolveOwnedIosSim(udid);
887
+ if (resolved.notOwned) return {
888
+ status: "skipped",
889
+ kind: "not-owned",
890
+ reason: `sim is now named "${resolved.notOwned}", not Stim-owned by name`
891
+ };
892
+ if (resolved.missing) return { status: "missing" };
893
+ closeOwnedDeviceSessions({
894
+ platform: "ios",
895
+ id: udid
896
+ }, () => Boolean(resolveOwnedIosSim(udid).sim));
897
+ const current = resolveOwnedIosSim(udid);
898
+ if (current.missing) return { status: "missing" };
899
+ if (current.notOwned) return {
900
+ status: "skipped",
901
+ kind: "not-owned",
902
+ reason: "simulator ownership changed before shutdown"
903
+ };
904
+ shutdownIosSim(udid);
905
+ const sim = resolved.sim;
906
+ if (del && park && park.max > 0) try {
907
+ const { record, evicted } = parkOwnedIosSim(udid, park);
908
+ const removed = [];
909
+ const failures = [];
910
+ for (const entry of evicted) {
911
+ const result = teardownParkedIosSim(entry.udid, { label: entry.name });
912
+ if (result.status === "torn-down") removed.push({
913
+ udid: entry.udid,
914
+ name: entry.name
915
+ });
916
+ else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name} (${entry.udid}): ${result.reason}`);
917
+ }
918
+ return {
919
+ status: "torn-down",
920
+ label: label ?? sim.name ?? udid,
921
+ parked: {
922
+ udid: record.udid,
923
+ name: record.name
924
+ },
925
+ evicted: removed,
926
+ ...failures.length ? { evictionFailures: failures } : {}
927
+ };
928
+ } catch (e) {
929
+ parkFallback = String(e?.message || e);
854
930
  }
855
- };
856
- capturing.push(frame);
857
- install();
858
- return frame;
859
- }
860
- function endCapture(frame) {
861
- if (!frame) return;
862
- const at = capturing.lastIndexOf(frame);
863
- if (at !== -1) capturing.splice(at, 1);
864
- abandoned.delete(frame);
865
- if (frame.pending !== "") {
866
- const rest = frame.pending;
867
- frame.pending = "";
868
- emitLine(frame, rest);
931
+ if (del) deleteIosSim(udid);
932
+ return {
933
+ status: "torn-down",
934
+ label: label ?? sim.name ?? udid,
935
+ ...parkFallback ? { parkFallback } : {}
936
+ };
937
+ } catch (e) {
938
+ return {
939
+ status: "failed",
940
+ reason: String(e?.message || e),
941
+ ...parkFallback ? { parkFallback } : {}
942
+ };
869
943
  }
870
- frame.close();
871
- uninstall();
872
- }
873
- function abandonCapture(frame, work) {
874
- if (!frame) return;
875
- const at = capturing.lastIndexOf(frame);
876
- if (at !== -1) capturing.splice(at, 1);
877
- abandoned.add(frame);
878
- Promise.resolve(work).then(() => endCapture(frame), () => endCapture(frame));
879
944
  }
880
- async function resolveRemote({ provider, platform, projectRoot, fingerprintHash, runOptions = null, timeoutMs = RESOLVE_TIMEOUT_MS, logWriter = null } = {}) {
881
- if (!provider?.plugin || !fingerprintHash) return null;
882
- const opts = runOptions || runOptionsFor(platform);
883
- const outcome = await withBudget(async () => {
884
- const props = {
885
- fingerprintHash: await providerFingerprint({
886
- provider,
887
- platform,
888
- projectRoot,
889
- runOptions: opts
890
- }) ?? fingerprintHash,
891
- platform,
892
- runOptions: opts,
893
- projectRoot
894
- };
895
- return typeof provider.plugin.resolveBuildCache === "function" ? provider.plugin.resolveBuildCache(props, provider.options) : provider.plugin.resolveRemoteBuildCache(props, provider.options);
896
- }, timeoutMs, {
897
- logWriter,
898
- projectRoot,
899
- platform
945
+ function removeOrphanedAvdDirectory(candidate, owner) {
946
+ 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 });
957
+ const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
958
+ renameSync(candidate.directory, destination);
959
+ return destination;
900
960
  });
901
- if (outcome.timedOut) return { timedOut: true };
902
- if (outcome.error) return { failed: firstLine(outcome.error) };
903
- const appPath = typeof outcome.value === "string" ? outcome.value.trim() : "";
904
- if (!appPath) return null;
905
- if (!existsSync(appPath)) return { failed: `returned ${appPath}, which does not exist` };
906
- return { appPath };
961
+ const current = lstatSync(detached);
962
+ if (!current.isDirectory() || current.dev !== candidate.dev || current.ino !== candidate.ino) throw new Error(`Detached AVD data changed at ${detached}; it was kept.`);
963
+ try {
964
+ rmSync(detached, { recursive: true });
965
+ } catch (error) {
966
+ throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
967
+ }
907
968
  }
908
- async function uploadRemote({ provider, platform, projectRoot, fingerprintHash, buildPath, runOptions = null, timeoutMs = UPLOAD_TIMEOUT_MS, logWriter = null, note = (line) => console.error(line) } = {}) {
909
- if (!provider?.plugin || !fingerprintHash || !buildPath) return { skipped: true };
910
- const opts = runOptions || runOptionsFor(platform);
911
- let announced = false;
912
- const onLine = (line) => {
913
- if (announced) return;
914
- const dest = uploadDestination([line]);
915
- if (!dest) return;
916
- announced = true;
917
- note(providerNote(`uploading to ${dest}`));
969
+ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
970
+ const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
971
+ if (candidates.length === 0) {
972
+ if (avdStorageRoots().some((root) => avdPathExists(join(root, `${avdName}.ini`)) || avdPathExists(join(root, `${avdName}.avd`)))) throw new Error(`AVD ${avdName} is not listed but its storage remains; it was kept.`);
973
+ return { status: "missing" };
974
+ }
975
+ if (!del) return {
976
+ status: "skipped",
977
+ reason: "AVD registration is missing; its data was kept."
918
978
  };
919
- const outcome = await withBudget(async () => {
920
- const props = {
921
- projectRoot,
922
- platform,
923
- fingerprintHash: await providerFingerprint({
924
- provider,
925
- platform,
926
- projectRoot,
927
- runOptions: opts
928
- }) ?? fingerprintHash,
929
- buildPath,
930
- runOptions: opts
931
- };
932
- return typeof provider.plugin.uploadBuildCache === "function" ? provider.plugin.uploadBuildCache(props, provider.options) : provider.plugin.uploadRemoteBuildCache(props, provider.options);
933
- }, timeoutMs, {
934
- logWriter,
935
- projectRoot,
936
- platform,
937
- onLine
938
- });
939
- if (outcome.timedOut) return { timedOut: true };
940
- if (outcome.error) return { failed: firstLine(outcome.error) };
941
- const destination = uploadDestination(outcome.lines);
942
- return destination ? {
943
- uploaded: true,
944
- destination
945
- } : { uploaded: true };
946
- }
947
- async function providerFingerprint({ provider, platform, projectRoot, runOptions }) {
948
- if (typeof provider.plugin.calculateFingerprintHash !== "function") return null;
949
- try {
950
- const hash = await provider.plugin.calculateFingerprintHash({
951
- projectRoot,
952
- platform,
953
- runOptions
954
- }, provider.options);
955
- return typeof hash === "string" && hash.trim() !== "" ? hash.trim() : null;
956
- } catch {
957
- return null;
979
+ for (const candidate of candidates) {
980
+ if (candidate.name !== avdName) throw new Error(`AVD data does not match ${avdName}; it was kept.`);
981
+ removeOrphanedAvdDirectory(candidate, owner);
958
982
  }
983
+ return {
984
+ status: "torn-down",
985
+ label: avdName
986
+ };
959
987
  }
960
- async function withBudget(factory, ms, capture) {
961
- let timer = null;
962
- const timeout = new Promise((resolve) => {
963
- timer = setTimeout(() => resolve(TIMED_OUT), ms);
964
- timer?.unref?.();
965
- });
966
- const frame = beginCapture(capture || {});
967
- const work = Promise.resolve().then(() => PROVIDER_CONTEXT.run(frame, factory)).then((value) => ({ value }), (error) => ({ error }));
988
+ function teardownOwnedAvd(avdName, { del = false, park, owner, orphanedDirectory, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial } = {}) {
989
+ let parkFallback;
968
990
  try {
969
- const settled = await Promise.race([work, timeout]);
970
- if (settled === TIMED_OUT) {
971
- abandonCapture(frame, work);
972
- return {
973
- timedOut: true,
974
- lines: frame.lines
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`
995
+ };
996
+ const resolved = resolveAvd(avdName);
997
+ if (resolved.notOwned) return {
998
+ status: "skipped",
999
+ kind: "not-owned",
1000
+ reason: `AVD ${avdName} is not Stim-owned by name`
1001
+ };
1002
+ if (resolved.missing) return teardownUnregisteredAvd(avdName, del, owner, orphanedDirectory);
1003
+ if (orphanedDirectory) return {
1004
+ status: "skipped",
1005
+ reason: "AVD registration appeared; its data was kept."
1006
+ };
1007
+ const serial = resolved.serial;
1008
+ if (serial) {
1009
+ const stillOwned = () => {
1010
+ const current = resolveAvd(avdName);
1011
+ return !current.notOwned && !current.missing && current.serial === serial;
1012
+ };
1013
+ closeOwnedDeviceSessions({
1014
+ platform: "android",
1015
+ id: serial
1016
+ }, stillOwned);
1017
+ if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
1018
+ waitForShutdown(avdName, (timeoutMs) => shutdownAndroidEmulator(serial, timeoutMs));
1019
+ } else assertStopped(avdName);
1020
+ if (del) {
1021
+ const current = resolveAvd(avdName);
1022
+ if (current.notOwned) return {
1023
+ status: "skipped",
1024
+ kind: "not-owned",
1025
+ reason: `AVD ${avdName} is not Stim-owned by name`
975
1026
  };
1027
+ if (current.missing) return teardownUnregisteredAvd(avdName, del, owner);
1028
+ if (current.serial) throw new Error(`Owned AVD ${avdName} started again before deletion.`);
1029
+ assertStopped(avdName);
1030
+ if (park && park.max > 0) try {
1031
+ const systemImage = ownedAvdSystemImage(avdName);
1032
+ if (!systemImage || !park.configuration || !ownedAvdMatchesConfiguration(avdName, park.configuration)) throw new Error("the AVD has no verified creation configuration");
1033
+ const evicted = parkSim({
1034
+ platform: "android",
1035
+ projectPath: park.projectPath,
1036
+ slot: park.slot,
1037
+ max: park.max,
1038
+ record: {
1039
+ udid: avdName,
1040
+ name: avdName,
1041
+ systemImage,
1042
+ configuration: park.configuration,
1043
+ parkedAt: (/* @__PURE__ */ new Date()).toISOString()
1044
+ }
1045
+ });
1046
+ const removed = [];
1047
+ const failures = [];
1048
+ for (const entry of evicted) {
1049
+ const result = teardownParkedAvd(entry.name);
1050
+ if (result.status === "torn-down") removed.push({
1051
+ udid: entry.name,
1052
+ name: entry.name,
1053
+ platform: "android"
1054
+ });
1055
+ else if (result.status === "failed") failures.push(`could not delete evicted ${entry.name}: ${result.reason}`);
1056
+ }
1057
+ return {
1058
+ status: "torn-down",
1059
+ label: avdName,
1060
+ parked: {
1061
+ udid: avdName,
1062
+ name: avdName,
1063
+ platform: "android"
1064
+ },
1065
+ evicted: removed,
1066
+ ...failures.length ? { evictionFailures: failures } : {}
1067
+ };
1068
+ } catch (error) {
1069
+ parkFallback = String(error?.message || error);
1070
+ }
1071
+ const directory = ownedAvdDirectory(avdName);
1072
+ const paths = avdStorageRoots().map((root) => join(root, `${avdName}.ini`));
1073
+ if (directory) paths.push(directory);
1074
+ deleteAvd(avdName);
1075
+ const remaining = paths.filter(avdPathExists);
1076
+ if (remaining.length) throw new Error(`AVD ${avdName} was only partially deleted; data remains at ${remaining.join(", ")}.`);
976
1077
  }
977
- endCapture(frame);
978
1078
  return {
979
- ...settled,
980
- lines: frame.lines
1079
+ status: "torn-down",
1080
+ label: avdName,
1081
+ serial: resolved.serial ?? null,
1082
+ ...parkFallback ? { parkFallback } : {}
1083
+ };
1084
+ } catch (e) {
1085
+ return {
1086
+ status: "failed",
1087
+ reason: String(e?.message || e)
981
1088
  };
982
- } catch (err) {
983
- endCapture(frame);
984
- throw err;
985
- } finally {
986
- if (timer) clearTimeout(timer);
987
1089
  }
988
1090
  }
989
- function firstLine(err) {
990
- const line = (String(err?.message || err || "unknown error").trim().split("\n")[0] ?? "").trim();
991
- return line.length > 200 ? `${line.slice(0, 197)}...` : line;
992
- }
993
1091
  //#endregion
994
- export { selectParked as C, readParked as S, teardownParkedIosSim as _, easAuthNote as a, dropParked as b, loadProjectProvider as c, resolveEasCliBin as d, resolveRemote as f, teardownParkedAvd as g, teardownOwnedIosSim as h, checkEasAuth as i, ownerFromConfig as l, teardownOwnedAvd as m, UPLOAD_TIMEOUT_MS as n, exitAfterFlush as o, uploadRemote as p, cacheLevel as r, isEasAuthFailureText as s, RESOLVE_TIMEOUT_MS as t, providerFromConfig as u, POOL_SETTING_REMEDY as v, parkedMaxSetting as x, adoptParked as y };
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 };