stim 1.11.0 → 1.13.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 (82) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-BtG4Va9l.mjs → activity-DfVKZTEB.mjs} +44 -26
  3. package/dist/{android-BA2i_KAV.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-C1q4l523.mjs → android-CD8pr8Q2.mjs} +112 -54
  5. package/dist/{android-cas-DfJkwCXW.mjs → android-cas-B9Kv9aIy.mjs} +12 -10
  6. package/dist/{android-C4zG2Tuc.mjs → android-qx_31wJo.mjs} +414 -118
  7. package/dist/{app-install-DHut6JnV.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/budget-E5o91NR1.mjs +1739 -0
  9. package/dist/{build-plan-CeAYgKAx.mjs → build-plan-BTa93o_r.mjs} +125 -107
  10. package/dist/{build-progress-CDwogz6R.mjs → build-progress-l7d-X-ZP.mjs} +76 -8
  11. package/dist/{cache-manifest-Pubfp8wV.mjs → cache-manifest-DWPF4RKJ.mjs} +1 -1
  12. package/dist/cache-manifest.mjs +1 -1
  13. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  14. package/dist/chrome-B5hWgQJS.mjs +61 -0
  15. package/dist/cli.mjs +17 -16
  16. package/dist/collector-run.mjs +15 -9
  17. package/dist/{command-output-D5z2_bcF.mjs → command-output-BIL7rlX7.mjs} +1 -1
  18. package/dist/{config-BEq0Qw_a.mjs → config-0jCcMM0E.mjs} +10 -1
  19. package/dist/created-devices-Cd558h31.mjs +37 -0
  20. package/dist/{dependency-state-r9UesA9a.mjs → dependency-state-DNN0yfx6.mjs} +1 -1
  21. package/dist/{deps-OF7CD5Dx.mjs → deps-2Wo81np9.mjs} +2 -2
  22. package/dist/{device-BwH2iI3O.mjs → device-Cf7vl-so.mjs} +7 -7
  23. package/dist/{device-lease-D83nqeMX.mjs → device-lease-CL9dZf73.mjs} +55 -6
  24. package/dist/{device-pool-DHj7cdPy.mjs → device-pool-MuoiyRcu.mjs} +4 -4
  25. package/dist/{device-remote-BQZSnnmE.mjs → device-remote-DT9ngZiD.mjs} +254 -579
  26. package/dist/{doctor-D8QdMRMf.mjs → doctor-D2iysNNH.mjs} +50 -20
  27. package/dist/{error-diagnostics-UYFSQwRy.mjs → error-diagnostics-BoiOhck8.mjs} +7 -7
  28. package/dist/{gc-COZteyiq.mjs → gc-eA-PRBX8.mjs} +1022 -131
  29. package/dist/{guide-NDvTmMi-.mjs → guide-BfZ0OmiP.mjs} +909 -132
  30. package/dist/{guide-status-CWvti2Lg.mjs → guide-status-BaMYu55M.mjs} +2 -2
  31. package/dist/{idle-NETbeNiQ.mjs → idle-DxLi5OkN.mjs} +43 -10
  32. package/dist/{in-use-DVhlSoyn.mjs → in-use-wYEaKQM9.mjs} +11 -7
  33. package/dist/{ios-CQLKpNfr.mjs → ios-CcfCeZ19.mjs} +103 -58
  34. package/dist/{ios-DArbmwer.mjs → ios-CmEzZiOj.mjs} +31 -22
  35. package/dist/{ios-device-CwDZo0X4.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  36. package/dist/{launch-verify-CuPDGXdr.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  37. package/dist/{logs-B44a1UeU.mjs → logs-DcSRbC2t.mjs} +12 -11
  38. package/dist/{merge-state-DDxS-XIZ.mjs → merge-state-ytFQCeh4.mjs} +38 -8
  39. package/dist/{metro-BWLOJ_Wr.mjs → metro-BPeY1YMh.mjs} +5 -5
  40. package/dist/{named-ports-Dc7neAlc.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  41. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  42. package/dist/{ownership-D2xbLVoz.mjs → ownership-Bz2kETAS.mjs} +2 -2
  43. package/dist/{ownership-BiojdtAp.mjs → ownership-PUP4Dwai.mjs} +175 -217
  44. package/dist/page-BTu7I60K.mjs +29 -0
  45. package/dist/{ports-DwBaRFJs.mjs → ports-C5UutqpJ.mjs} +5 -5
  46. package/dist/{project-BXZzOABt.mjs → project-DGeclhvO.mjs} +55 -87
  47. package/dist/{reload-By9Dwsrt.mjs → reload-C-cV5yy_.mjs} +80 -21
  48. package/dist/remote-cache--aehr4Pr.mjs +526 -0
  49. package/dist/{server-bare-D_RK1RCj.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  50. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  51. package/dist/{server-expo-o6H5gcBc.mjs → server-expo-DUCg52-K.mjs} +2 -2
  52. package/dist/{settings-C0ugALSC.mjs → settings-ssVBToDR.mjs} +29 -17
  53. package/dist/{settings-B5oFZEri.mjs → settings-xB0Frkg6.mjs} +13 -4
  54. package/dist/settings.schema.json +74 -2
  55. package/dist/{simslim-CIR3IYJO.mjs → simslim-Bbg9UWrt.mjs} +13 -6
  56. package/dist/{slot-launch-rggO1Esw.mjs → slot-launch-BIWEW-wI.mjs} +4 -4
  57. package/dist/{spawn-claims-BJwXTMPt.mjs → spawn-claims-PT8aQpCa.mjs} +1 -1
  58. package/dist/{start-BSsf2tUs.mjs → start-BZhItkiL.mjs} +110 -18
  59. package/dist/{start-i8zc9VmS.mjs → start-Rw2GD3nV.mjs} +1 -1
  60. package/dist/{state-WGdIbi_d.mjs → state-DwAU2_-D.mjs} +15 -3
  61. package/dist/state-l1pYwOre.mjs +148 -0
  62. package/dist/{stats-DBYJ5bxL.mjs → stats-CaSwmsSZ.mjs} +4 -4
  63. package/dist/{stats-BAgJaLA4.mjs → stats-D8rUTPHd.mjs} +1 -1
  64. package/dist/{status-CitcBRsC.mjs → status-TJlL0HWo.mjs} +649 -116
  65. package/dist/{status-Q-WDEGCB.mjs → status-sszP4XKU.mjs} +130 -28
  66. package/dist/stim-desktop-CXt8-Z3J.mjs +40 -0
  67. package/dist/{stop-DIhrvu0B.mjs → stop-BZ2qMiBP.mjs} +50 -18
  68. package/dist/{stop-rst5mL14.mjs → stop-DskDKNTU.mjs} +2 -2
  69. package/dist/supervisor-run.mjs +11 -11
  70. package/dist/web-run.d.mts +22 -0
  71. package/dist/web-run.mjs +481 -0
  72. package/dist/web-sWXd5iek.mjs +394 -0
  73. package/dist/{worktree-DhxDTK44.mjs → worktree-3cBa-bQt.mjs} +1 -1
  74. package/dist/{worktree-KOjMX3zA.mjs → worktree-D4Do_XiQ.mjs} +229 -21
  75. package/dist/{worktree-BlizKDpZ.mjs → worktree-DeBQSVtR.mjs} +3 -1
  76. package/dist/{xcode-B5SPD--o.mjs → xcode-LM6A8cNZ.mjs} +7 -7
  77. package/package.json +5 -5
  78. package/dist/budget-De3P2rq_.mjs +0 -620
  79. package/dist/native-run-D7yB_AsB.mjs +0 -143
  80. package/dist/server-expo-B-CCUJZj.mjs +0 -2
  81. package/dist/workspace-process-lock-Cx74RSDZ.mjs +0 -57
  82. package/dist/workspaces-COD2xCz-.mjs +0 -1013
@@ -1,14 +1,18 @@
1
- import { D as process_identity_exports, H as projectDeviceSlots, I as assignSlotDevice, O as acquireAvdClaim, T as withConfigLock, U as removeSlotDevice, d as getConfigPath, g as loadConfig, k as ownership_claim_exports, s as ensureConfig, u as getConfigDir, x as saveConfig, z as deviceSlotPlatforms } from "./config-BEq0Qw_a.mjs";
1
+ import { A as ownership_claim_exports, B as deviceSlotPlatforms, E as withConfigLock, O as process_identity_exports, U as projectDeviceSlots, _ as loadConfig, d as getConfigDir, k as acquireAvdClaim, p as getProject } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { c as signalProcessTree, i as pidExists } from "./metro-BWLOJ_Wr.mjs";
4
- import { r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-Cx74RSDZ.mjs";
3
+ import { a as pidExists, k as workspaceDir, l as signalProcessTree } from "./metro-BPeY1YMh.mjs";
4
+ import { a as withWorkspaceProcessLock, r as releaseBrowserPort, s as workspaceProcessLockPath } from "./named-ports-n7tnJsI8.mjs";
5
5
  import { t as createLineReader } from "./process-output-Fpl8PYi8.mjs";
6
- import { l as phaseLine } from "./command-output-D5z2_bcF.mjs";
7
- import { G as sleepSync, H as resolveOwnedAvdSerial, J as wipeAvdUserData, K as waitForAndroidEmulatorShutdown, L as ownedAvdSystemImage, N as ownedAvdDirectory, P as ownedAvdMatchesConfiguration, W as shutdownAndroidEmulator, _ as deleteAvd, d as avdPathExists, k as listOrphanedAvdDirectories, p as avdStorageRoots, u as assertOwnedAvdStopped, w as isStimOwnedAvdName } from "./android-C4zG2Tuc.mjs";
8
- import { S as resolveOwnedIosSim, a as deleteParkedIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, i as deleteIosSim, o as eraseIosSim, v as renameIosSim, w as shutdownIosSim } from "./ios-DArbmwer.mjs";
9
- import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync } from "node:fs";
10
- import { basename, dirname, join, resolve, sep } from "node:path";
6
+ import { l as phaseLine } from "./command-output-BIL7rlX7.mjs";
7
+ import { a as readAgentDeviceRecords } from "./activity-DfVKZTEB.mjs";
8
+ import { F as ownedAvdDirectory, I as ownedAvdMatchesConfiguration, Q as waitForAndroidEmulatorShutdown, X as sleepSync, Y as shutdownAndroidEmulator, b as deleteAvd, d as assertOwnedAvdStopped, et as wipeAvdUserData, g as avdStorageRoots, j as listOrphanedAvdDirectories, lt as readParked, m as avdPathExists, q as resolveOwnedAvdSerial, st as parkSim, tt as isStimOwnedAvd, ut as removeParkedAfter, z as ownedAvdSystemImage } from "./android-qx_31wJo.mjs";
9
+ import { n as readCreatedDevices, r as recordCreatedDevice, t as forgetCreatedDevice } from "./created-devices-Cd558h31.mjs";
10
+ import { a as readWebRecord, d as webProfileDir, g as removeSingletonFiles, h as liveProfileHolder, i as clearWebRecord, m as chromeProcessState, s as webClaimRoot, t as CDP_PORT_LABEL } from "./state-l1pYwOre.mjs";
11
+ import { S as resolveOwnedIosSim, a as deleteParkedIosSim, d as listIosDeviceTypes, g as parseRuntimeVersion, h as parkedSimName, i as deleteIosSim, o as eraseIosSim, v as renameIosSim, w as shutdownIosSim } from "./ios-CmEzZiOj.mjs";
12
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync } from "node:fs";
13
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
14
  import { createHash, randomUUID } from "node:crypto";
15
+ import { homedir } from "node:os";
12
16
  import { isDeepStrictEqual } from "node:util";
13
17
  import { resolve4 } from "node:dns/promises";
14
18
  import { request } from "node:https";
@@ -22,17 +26,41 @@ function parseAgentDeviceSessions(output) {
22
26
  const id = row.platform === "ios" ? row.device_udid : row.id;
23
27
  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 [];
24
28
  return [{
25
- name: row.name,
29
+ name: typeof row.address === "string" && row.address.trim() ? row.address : row.name,
26
30
  platform: row.platform,
27
31
  id,
32
+ device: typeof row.device === "string" ? row.device : null,
28
33
  createdAt: row.createdAt
29
34
  }];
30
35
  });
31
36
  }
37
+ function isOwnDeviceSession(session, device, owner) {
38
+ if (session.platform !== device.platform || session.id !== device.id) return false;
39
+ if (device.platform === "android" && session.device !== device.avdName.replaceAll("_", " ")) return false;
40
+ if (!owner) return true;
41
+ return owner.claims.some((claim) => claim.session === session.name && claim.workspace !== null && within(owner.workspace, claim.workspace));
42
+ }
43
+ function within(root, path) {
44
+ const rest = relative(root, path);
45
+ return rest === "" || !isAbsolute(rest) && rest.split(sep)[0] !== "..";
46
+ }
47
+ function canonical(path) {
48
+ try {
49
+ return realpathSync(path);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function deviceClaims(device) {
55
+ return readAgentDeviceRecords(homedir()).filter((record) => record.kind === "claim" && record.deviceId === device.id).map((record) => ({
56
+ session: record.session,
57
+ workspace: record.workspace && canonical(record.workspace)
58
+ }));
59
+ }
32
60
  function report(message) {
33
61
  process.stderr.write(`${phaseLine("device", message)}\n`);
34
62
  }
35
- function closeOwnedDeviceSessions(device, stillOwned) {
63
+ function closeOwnedDeviceSessions(device, stillOwned, workspace) {
36
64
  const exec = getExecutor();
37
65
  const deadline = Date.now() + 15e3;
38
66
  const run = (args) => {
@@ -56,7 +84,13 @@ function closeOwnedDeviceSessions(device, stillOwned) {
56
84
  ]));
57
85
  try {
58
86
  if (!exec.findExecutable("agent-device")) return;
59
- const sessions = list().filter((session) => session.platform === device.platform && session.id === device.id);
87
+ const root = workspace === void 0 ? void 0 : canonical(workspace);
88
+ if (root === null) return;
89
+ const owner = root === void 0 ? void 0 : {
90
+ workspace: root,
91
+ claims: deviceClaims(device)
92
+ };
93
+ const sessions = list().filter((session) => isOwnDeviceSession(session, device, owner));
60
94
  for (const session of sessions) try {
61
95
  if (!list().some((current) => current.name === session.name && current.platform === session.platform && current.id === session.id && current.createdAt === session.createdAt)) continue;
62
96
  if (!stillOwned()) return;
@@ -79,202 +113,6 @@ function closeOwnedDeviceSessions(device, stillOwned) {
79
113
  report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
80
114
  }
81
115
  }
82
- const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
83
- const MAX_SETTING = {
84
- ios: {
85
- key: "iosParkedMax",
86
- env: "STIM_POOL_IOS_PARKED_MAX"
87
- },
88
- android: {
89
- key: "androidParkedMax",
90
- env: "STIM_POOL_ANDROID_PARKED_MAX"
91
- }
92
- };
93
- function parseMax(raw, strings) {
94
- const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
95
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
96
- return value;
97
- }
98
- function parkedMaxSetting(platform, { config, env = process.env } = {}) {
99
- const { key, env: envKey } = MAX_SETTING[platform];
100
- const fromEnv = env[envKey];
101
- const explicit = fromEnv !== void 0 && fromEnv !== "";
102
- const pool = (config === void 0 ? loadConfig() : config)?.pool;
103
- if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
104
- max: 0,
105
- error: "Invalid pool value. Expected an object with simulator and emulator bounds."
106
- };
107
- const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
108
- const raw = explicit ? fromEnv : fromConfig;
109
- if (raw === void 0) return {
110
- max: env.STIM_HOME ? 0 : 3,
111
- error: null
112
- };
113
- const parsed = parseMax(raw, explicit);
114
- if (parsed === null) return {
115
- max: 0,
116
- error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
117
- };
118
- return {
119
- max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
120
- error: null
121
- };
122
- }
123
- function isParkedRecord(value, platform) {
124
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
125
- const record = value;
126
- 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"));
127
- }
128
- function poolBlock(config) {
129
- const raw = config?.parked;
130
- const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
131
- const read = (platform) => {
132
- const list = block[platform];
133
- return Array.isArray(list) ? list.filter((record) => isParkedRecord(record, platform)) : [];
134
- };
135
- return {
136
- ios: read("ios"),
137
- android: read("android")
138
- };
139
- }
140
- function readParked(platform, { config } = {}) {
141
- return poolBlock(config === void 0 ? loadConfig() : config)[platform];
142
- }
143
- function writeParked(config, platform, records) {
144
- config.parked = {
145
- ...poolBlock(config),
146
- [platform]: records
147
- };
148
- }
149
- function oldestFirst(records) {
150
- return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
151
- }
152
- function isLegacyDeletionClaim(value) {
153
- if (value === void 0) return false;
154
- if (value === null || typeof value !== "object" || Array.isArray(value)) return true;
155
- const marker = value;
156
- return marker.kind !== "ownership-claim" || typeof marker.claimId !== "string" || marker.claimId.length === 0;
157
- }
158
- function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
159
- return oldestFirst(records.filter((r) => !isLegacyDeletionClaim(r.deletionClaim) && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
160
- }
161
- function evictOverflow(records, max) {
162
- if (records.length <= max) return {
163
- keep: [...records],
164
- evicted: []
165
- };
166
- const ordered = oldestFirst(records);
167
- const evicted = ordered.slice(0, ordered.length - max);
168
- const dropped = new Set(evicted.map((r) => r.udid));
169
- return {
170
- keep: records.filter((r) => !dropped.has(r.udid)),
171
- evicted
172
- };
173
- }
174
- function parkSim({ platform, projectPath, slot = "default", record, max }) {
175
- return withConfigLock(() => {
176
- const cfg = ensureConfig();
177
- const current = deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform];
178
- const currentId = platform === "ios" ? current?.deviceUdid : current?.avdName;
179
- if (!current?.owned || currentId !== record.udid) throw new Error(`The ${platform === "ios" ? "simulator" : "emulator"} assignment changed before parking.`);
180
- const { keep, evicted } = evictOverflow([...readParked(platform, { config: cfg }).filter((r) => r.udid !== record.udid), record], max);
181
- writeParked(cfg, platform, [...keep, ...evicted]);
182
- const project = cfg.projects[projectPath];
183
- if (project) removeSlotDevice(project, platform, slot);
184
- saveConfig(cfg);
185
- return evicted;
186
- });
187
- }
188
- function adoptParked({ platform, projectPath, slot = "default", udid, device }) {
189
- const claim = claimParkedOperation(platform, udid);
190
- if (!claim) return null;
191
- try {
192
- return withConfigLock(() => {
193
- const cfg = ensureConfig();
194
- const records = readParked(platform, { config: cfg });
195
- const taken = records.find((r) => r.udid === udid);
196
- if (!taken || isLegacyDeletionClaim(taken.deletionClaim)) return null;
197
- if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
198
- writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
199
- const project = cfg.projects[projectPath];
200
- if (!project) throw new Error(`Project not registered: ${projectPath}`);
201
- assignSlotDevice(project, platform, device, slot);
202
- saveConfig(cfg);
203
- const adopted = { ...taken };
204
- delete adopted.deletionClaim;
205
- return adopted;
206
- });
207
- } finally {
208
- (0, ownership_claim_exports.releaseClaim)(claim);
209
- }
210
- }
211
- function claimParkedOperation(platform, udid) {
212
- const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
213
- root: join(getConfigDir(), "pool-locks", platform, `${encodeURIComponent(udid.toLowerCase())}.lock`),
214
- mode: "exclusive",
215
- label: `parked ${platform} device ${udid}`
216
- });
217
- if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
218
- return attempt.acquired ?? null;
219
- }
220
- function removeParkedAfter(platform, udid, beforeRemove) {
221
- const claim = claimParkedOperation(platform, udid);
222
- if (!claim) return null;
223
- try {
224
- const claimed = withConfigLock(() => {
225
- const cfg = loadConfig();
226
- if (!cfg) return null;
227
- const records = readParked(platform, { config: cfg });
228
- const current = records.find((candidate) => candidate.udid === udid);
229
- if (!current) return null;
230
- 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.`);
231
- const record = { ...current };
232
- delete record.deletionClaim;
233
- const marked = {
234
- ...record,
235
- deletionClaim: {
236
- kind: "ownership-claim",
237
- claimId: claim.claimId
238
- }
239
- };
240
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? marked : candidate));
241
- saveConfig(cfg);
242
- return {
243
- record,
244
- marked
245
- };
246
- });
247
- if (!claimed) return null;
248
- let completed = false;
249
- let removed = null;
250
- try {
251
- (0, ownership_claim_exports.markClaimChildPending)(claim);
252
- try {
253
- beforeRemove(claimed.record);
254
- } finally {
255
- (0, ownership_claim_exports.clearClaimChild)(claim);
256
- }
257
- completed = true;
258
- } finally {
259
- withConfigLock(() => {
260
- const cfg = loadConfig();
261
- if (!cfg) return;
262
- const records = readParked(platform, { config: cfg });
263
- const current = records.find((candidate) => candidate.udid === udid);
264
- if (!isDeepStrictEqual(current, claimed.marked)) return;
265
- writeParked(cfg, platform, completed ? records.filter((candidate) => candidate.udid !== udid) : records.map((candidate) => candidate.udid === udid ? claimed.record : candidate));
266
- saveConfig(cfg);
267
- if (completed) removed = claimed.record;
268
- });
269
- }
270
- return removed;
271
- } finally {
272
- (0, ownership_claim_exports.releaseClaim)(claim);
273
- }
274
- }
275
- function dropParked(platform, udid) {
276
- return removeParkedAfter(platform, udid, () => {}) !== null;
277
- }
278
116
  //#endregion
279
117
  //#region src/devices/teardown.ts
280
118
  function teardownParkedAvd(avdName) {
@@ -376,7 +214,7 @@ function parkOwnedIosSim(settled, udid, park) {
376
214
  })
377
215
  };
378
216
  }
379
- function teardownOwnedIosSim(udid, { del = false, label, park, shutdownClock = {} } = {}) {
217
+ function teardownOwnedIosSim(udid, { del = false, label, park, workspace, shutdownClock = {} } = {}) {
380
218
  let parkFallback;
381
219
  try {
382
220
  const resolved = resolveOwnedIosSim(udid);
@@ -389,7 +227,7 @@ function teardownOwnedIosSim(udid, { del = false, label, park, shutdownClock = {
389
227
  closeOwnedDeviceSessions({
390
228
  platform: "ios",
391
229
  id: udid
392
- }, () => Boolean(resolveOwnedIosSim(udid).sim));
230
+ }, () => Boolean(resolveOwnedIosSim(udid).sim), workspace);
393
231
  const current = resolveOwnedIosSim(udid);
394
232
  if (current.missing) return { status: "missing" };
395
233
  if (current.notOwned) return {
@@ -468,8 +306,16 @@ function removeOrphanedAvdDirectory(candidate, owner) {
468
306
  const current = lstatSync(candidate.directory);
469
307
  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.`);
470
308
  assertAvdReferences(candidate.name, owner);
471
- const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
472
- renameSync(candidate.directory, destination);
309
+ const name = `stim-gc-${randomUUID()}`;
310
+ recordCreatedDevice("android", name);
311
+ const destination = join(dirname(candidate.directory), `${name}.avd`);
312
+ try {
313
+ renameSync(candidate.directory, destination);
314
+ } catch (error) {
315
+ forgetCreatedDevice("android", name);
316
+ throw error;
317
+ }
318
+ forgetCreatedDevice("android", candidate.name);
473
319
  return destination;
474
320
  });
475
321
  const current = lstatSync(detached);
@@ -479,6 +325,7 @@ function removeOrphanedAvdDirectory(candidate, owner) {
479
325
  } catch (error) {
480
326
  throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
481
327
  }
328
+ forgetCreatedDevice("android", basename(detached, ".avd"));
482
329
  }
483
330
  function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
484
331
  const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
@@ -502,7 +349,7 @@ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
502
349
  function teardownOwnedAvd(avdName, options = {}) {
503
350
  let claim;
504
351
  try {
505
- if (!isStimOwnedAvdName(avdName, options.orphanedDirectory ? options.orphanedDirectory.directory : void 0)) return {
352
+ if (!isStimOwnedAvd(avdName)) return {
506
353
  status: "skipped",
507
354
  kind: "not-owned",
508
355
  reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
@@ -520,7 +367,7 @@ function teardownOwnedAvd(avdName, options = {}) {
520
367
  (0, ownership_claim_exports.releaseClaim)(claim);
521
368
  }
522
369
  }
523
- function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphanedDirectory, onlyIfMissing = false, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
370
+ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, workspace, orphanedDirectory, onlyIfMissing = false, waitForShutdown = waitForAndroidEmulatorShutdown, assertStopped = assertOwnedAvdStopped, resolveAvd = resolveOwnedAvdSerial }) {
524
371
  let parkFallback;
525
372
  try {
526
373
  if (del) withConfigLock(() => assertAvdReferences(avdName, owner));
@@ -547,8 +394,9 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
547
394
  };
548
395
  closeOwnedDeviceSessions({
549
396
  platform: "android",
550
- id: serial
551
- }, stillOwned);
397
+ id: serial,
398
+ avdName
399
+ }, stillOwned, workspace);
552
400
  if (!stillOwned()) throw new Error(`Owned AVD ${avdName} changed before shutdown; it was kept.`);
553
401
  waitForShutdown(avdName, (timeoutMs) => {
554
402
  (0, ownership_claim_exports.markClaimChildPending)(claim);
@@ -558,7 +406,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
558
406
  (0, ownership_claim_exports.clearClaimChild)(claim);
559
407
  }
560
408
  });
561
- } else assertStopped(avdName);
409
+ } else waitForShutdown(avdName, null);
562
410
  if (del) {
563
411
  const current = resolveAvd(avdName);
564
412
  if (current.notOwned) return {
@@ -639,6 +487,116 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, orphaned
639
487
  };
640
488
  }
641
489
  }
490
+ const BROWSER_EXIT_WAIT_MS = 15e3;
491
+ const CHROME_KILL_WAIT_MS = 5e3;
492
+ async function stopOwnedChrome(record) {
493
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
494
+ const state = chromeProcessState(record);
495
+ if (state === "gone") return true;
496
+ if (state === "unknown") return false;
497
+ try {
498
+ signalProcessTree(record.pid, signal, { group: true });
499
+ } catch {}
500
+ const deadline = Date.now() + CHROME_KILL_WAIT_MS;
501
+ while (chromeProcessState(record) !== "gone" && Date.now() < deadline) await sleepAsync(50);
502
+ }
503
+ return chromeProcessState(record) === "gone";
504
+ }
505
+ const sleepAsync = (ms) => new Promise((done) => setTimeout(done, ms));
506
+ /**
507
+ * Closes the workspace's Stim-owned Chrome and its supervisor, after verifying both process identities. With
508
+ * `deleteProfile`, it also deletes the profile directory, only when the created-devices ledger lists it.
509
+ */
510
+ async function teardownOwnedBrowser(root, options = {}) {
511
+ try {
512
+ return await withWorkspaceProcessLock(workspaceDir(root), "web", () => teardownBrowserHeld(root, options), {
513
+ external: true,
514
+ ownerPurpose: "browser teardown"
515
+ });
516
+ } catch (error) {
517
+ return {
518
+ status: "failed",
519
+ reason: String(error?.message || error)
520
+ };
521
+ }
522
+ }
523
+ /** {@link teardownOwnedBrowser} for a caller that already holds the {@link BROWSER_LOCK} lock. */
524
+ async function teardownBrowserHeld(root, { deleteProfile = false } = {}) {
525
+ try {
526
+ const record = readWebRecord(root);
527
+ const profile = record?.profile ?? webProfileDir(root);
528
+ if (record) {
529
+ const supervisor = (0, process_identity_exports.inspectProcessIdentity)(record);
530
+ const chrome = record.chromeProcess ? (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) : "gone";
531
+ if (supervisor === "unknown" || chrome === "unknown") return {
532
+ status: "skipped",
533
+ kind: "not-verified",
534
+ reason: `the browser supervisor (pid ${record.pid}) or its Chrome could not be verified; its record was kept`
535
+ };
536
+ if (supervisor === "same") {
537
+ try {
538
+ process.kill(record.pid, "SIGTERM");
539
+ } catch {}
540
+ if (!await (0, process_identity_exports.waitForProcessExit)(record, BROWSER_EXIT_WAIT_MS)) return {
541
+ status: "failed",
542
+ reason: `browser supervisor pid ${record.pid} did not exit`
543
+ };
544
+ }
545
+ if (record.chromeProcess && !await stopOwnedChrome(record.chromeProcess)) return {
546
+ status: "failed",
547
+ reason: `Chrome pid ${record.chromeProcess.pid} did not exit`
548
+ };
549
+ const holder = liveProfileHolder(profile);
550
+ if (holder !== null) return {
551
+ status: "skipped",
552
+ kind: "not-verified",
553
+ reason: `pid ${holder} still holds ${profile}, and the browser record does not name it; it was left running`
554
+ };
555
+ const claims = (0, ownership_claim_exports.clearFreeClaimSet)({
556
+ root: webClaimRoot(root),
557
+ label: "browser supervisor"
558
+ });
559
+ if (claims.status !== "cleared") return {
560
+ status: "skipped",
561
+ kind: "not-verified",
562
+ reason: claims.status === "held" ? `the browser supervisor claim is still held${claims.holder ? ` by pid ${claims.holder.owner.pid}` : ""}` : `the browser supervisor claim could not be cleared (${claims.reason}); remove it with ${(0, ownership_claim_exports.claimRemoveCommand)(webClaimRoot(root))}`
563
+ };
564
+ clearWebRecord(root, record);
565
+ await releaseBrowserPort(root, record.cdpPort);
566
+ } else {
567
+ const holder = liveProfileHolder(profile);
568
+ if (holder !== null) return {
569
+ status: "skipped",
570
+ kind: "not-verified",
571
+ reason: `pid ${holder} holds ${profile} but no browser supervisor is recorded; it was left running`
572
+ };
573
+ const port = getProject(root)?.ports?.[CDP_PORT_LABEL];
574
+ if (typeof port === "number") await releaseBrowserPort(root, port);
575
+ }
576
+ if (existsSync(profile)) removeSingletonFiles(profile);
577
+ if (deleteProfile && existsSync(profile)) {
578
+ if (!readCreatedDevices().web.has(profile)) return {
579
+ status: "skipped",
580
+ kind: "not-owned",
581
+ reason: `${profile} is not in Stim's ledger; it was kept`
582
+ };
583
+ rmSync(profile, {
584
+ recursive: true,
585
+ force: true
586
+ });
587
+ forgetCreatedDevice("web", profile);
588
+ }
589
+ return record ? {
590
+ status: "torn-down",
591
+ label: `Chrome (${profile})`
592
+ } : { status: "missing" };
593
+ } catch (error) {
594
+ return {
595
+ status: "failed",
596
+ reason: String(error?.message || error)
597
+ };
598
+ }
599
+ }
642
600
  //#endregion
643
601
  //#region src/engine/public-http-probe.ts
644
602
  const DNS_TIMEOUT_MS = 5e3;
@@ -1177,4 +1135,4 @@ function numberOrNull(v) {
1177
1135
  return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
1178
1136
  }
1179
1137
  //#endregion
1180
- export { dropParked as _, terminateChild as a, readParked as b, withManagedTunnelLock as c, teardownOwnedAvd as d, teardownOwnedIosSim as f, adoptParked as g, POOL_SETTING_REMEDY as h, stopTunnel as i, withManagedTunnelRemovalLock as l, teardownParkedIosSim as m, managedLockHolders as n, withManagedRemoteWorktreeLock as o, teardownParkedAvd as p, startTunnelSequence as r, withManagedRemoteWorktreeRemovalLock as s, resolveSupervisorTarget as t, probePublicHttp as u, isLegacyDeletionClaim as v, selectParked as x, parkedMaxSetting as y };
1138
+ export { terminateChild as a, withManagedTunnelLock as c, teardownBrowserHeld as d, teardownOwnedAvd as f, teardownParkedIosSim as g, teardownParkedAvd as h, stopTunnel as i, withManagedTunnelRemovalLock as l, teardownOwnedIosSim as m, managedLockHolders as n, withManagedRemoteWorktreeLock as o, teardownOwnedBrowser as p, startTunnelSequence as r, withManagedRemoteWorktreeRemovalLock as s, resolveSupervisorTarget as t, probePublicHttp as u };
@@ -0,0 +1,29 @@
1
+ import { O as process_identity_exports } from "./config-0jCcMM0E.mjs";
2
+ import { t as connectOwnedBrowser } from "./cdp-Dq_ypAlJ.mjs";
3
+ //#region src/web/page.ts
4
+ /** The record of a verified live browser: its supervisor and Chrome are the processes the record names. */
5
+ function liveWebRecord(record) {
6
+ if (!record?.targetId || !record.chromeProcess) return null;
7
+ if ((0, process_identity_exports.inspectProcessIdentity)(record) !== "same" || (0, process_identity_exports.inspectProcessIdentity)(record.chromeProcess) !== "same") return null;
8
+ return record;
9
+ }
10
+ const NAVIGATE_ACK_MS = 1e3;
11
+ /** Sends one page-level DevTools command to the owned page, through a connection verified to reach its Chrome. */
12
+ async function sendToOwnedPage(record, method, params = {}) {
13
+ const cdp = await connectOwnedBrowser(record.cdpPort, record.chromeProcess.pid);
14
+ try {
15
+ const { sessionId } = await cdp.send("Target.attachToTarget", {
16
+ targetId: record.targetId,
17
+ flatten: true
18
+ });
19
+ const reply = cdp.send(method, params, sessionId);
20
+ if (method === "Page.navigate") {
21
+ reply.catch(() => {});
22
+ await Promise.race([reply, new Promise((resolve) => setTimeout(resolve, NAVIGATE_ACK_MS))]);
23
+ } else await reply;
24
+ } finally {
25
+ cdp.close();
26
+ }
27
+ }
28
+ //#endregion
29
+ export { sendToOwnedPage as n, liveWebRecord as t };
@@ -1,10 +1,10 @@
1
- import { f as getProject } from "./config-BEq0Qw_a.mjs";
2
- import { o as findProjectRoot } from "./project-BXZzOABt.mjs";
3
- import { n as getNamedPort, t as clearNamedPorts } from "./named-ports-Dc7neAlc.mjs";
1
+ import { p as getProject } from "./config-0jCcMM0E.mjs";
2
+ import { i as findCommandWorkspace } from "./project-DGeclhvO.mjs";
3
+ import { n as getNamedPort, t as clearNamedPorts } from "./named-ports-n7tnJsI8.mjs";
4
4
  //#region src/commands/ports.ts
5
5
  async function inProject(action) {
6
6
  try {
7
- const root = findProjectRoot(process.cwd());
7
+ const root = findCommandWorkspace(process.cwd());
8
8
  if (!root) throw new Error("No package.json found. Run stim ports from the workspace that owns the server.");
9
9
  await action(root);
10
10
  } catch (error) {
@@ -16,7 +16,7 @@ function portsCommand(program) {
16
16
  const ports = program.command("ports").description("Reserve workspace ports for servers Stim does not manage");
17
17
  ports.action(() => inProject(async (root) => {
18
18
  const project = getProject(root);
19
- const rows = Object.entries(project?.ports ?? {});
19
+ const rows = Object.entries(project?.ports ?? {}).map(([name, port]) => [name === "web-cdp" ? "web-cdp (managed)" : name, port]);
20
20
  if (typeof project?.metroPort === "number") rows.push(["metro (managed)", project.metroPort]);
21
21
  if (!rows.length) {
22
22
  console.log("No ports allocated. Use stim ports get <label>.");