stim 1.12.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 (71) hide show
  1. package/README.md +3 -1
  2. package/dist/{activity-CCZJs__P.mjs → activity-DfVKZTEB.mjs} +42 -25
  3. package/dist/{android-BQiKJYK0.mjs → android-BHfAu2mf.mjs} +1 -1
  4. package/dist/{android-B7X-8H7h.mjs → android-CD8pr8Q2.mjs} +62 -47
  5. package/dist/{android-cas-DzLPXiEn.mjs → android-cas-B9Kv9aIy.mjs} +9 -7
  6. package/dist/{android-75Sk2rIR.mjs → android-qx_31wJo.mjs} +395 -116
  7. package/dist/{app-install-CsZi0SAT.mjs → app-install-CDFY2nUL.mjs} +1 -1
  8. package/dist/{budget-DbzW5VTD.mjs → budget-E5o91NR1.mjs} +154 -37
  9. package/dist/{build-plan-CqntbR7b.mjs → build-plan-BTa93o_r.mjs} +21 -88
  10. package/dist/{build-progress-h60GPV27.mjs → build-progress-l7d-X-ZP.mjs} +75 -7
  11. package/dist/cdp-Dq_ypAlJ.mjs +115 -0
  12. package/dist/chrome-B5hWgQJS.mjs +61 -0
  13. package/dist/cli.mjs +16 -15
  14. package/dist/collector-run.mjs +14 -8
  15. package/dist/{command-output-BWCj5Vh-.mjs → command-output-BIL7rlX7.mjs} +1 -1
  16. package/dist/created-devices-Cd558h31.mjs +37 -0
  17. package/dist/{deps-CO-_7UOm.mjs → deps-2Wo81np9.mjs} +1 -1
  18. package/dist/{device-9r9xRYDa.mjs → device-Cf7vl-so.mjs} +6 -6
  19. package/dist/{device-lease-B9LAXIUH.mjs → device-lease-CL9dZf73.mjs} +52 -3
  20. package/dist/{device-pool-BoezJd-c.mjs → device-pool-MuoiyRcu.mjs} +3 -3
  21. package/dist/{device-remote-C0_o-tJH.mjs → device-remote-DT9ngZiD.mjs} +10 -10
  22. package/dist/{doctor-LfDlbR-N.mjs → doctor-D2iysNNH.mjs} +47 -18
  23. package/dist/{error-diagnostics-CCxjn31V.mjs → error-diagnostics-BoiOhck8.mjs} +6 -6
  24. package/dist/{gc-C4nQFcTX.mjs → gc-eA-PRBX8.mjs} +853 -126
  25. package/dist/{guide-BPBlh9ML.mjs → guide-BfZ0OmiP.mjs} +736 -110
  26. package/dist/{guide-status-i9ogHt4v.mjs → guide-status-BaMYu55M.mjs} +1 -1
  27. package/dist/{idle-S3tFZ53_.mjs → idle-DxLi5OkN.mjs} +42 -9
  28. package/dist/{in-use-D9XYEgFD.mjs → in-use-wYEaKQM9.mjs} +9 -5
  29. package/dist/{ios-CDzWL1mj.mjs → ios-CcfCeZ19.mjs} +98 -54
  30. package/dist/{ios-CZWOHTTc.mjs → ios-CmEzZiOj.mjs} +10 -5
  31. package/dist/{ios-device-D9cLxVr5.mjs → ios-device-BW-t_Lkv.mjs} +1 -1
  32. package/dist/{launch-verify-B2oR_kFN.mjs → launch-verify-CuJaBpEa.mjs} +33 -20
  33. package/dist/{logs-SzuYudOj.mjs → logs-DcSRbC2t.mjs} +11 -10
  34. package/dist/{metro-Dzo30hvJ.mjs → metro-BPeY1YMh.mjs} +4 -4
  35. package/dist/{named-ports-Dn1o61D9.mjs → named-ports-n7tnJsI8.mjs} +78 -6
  36. package/dist/native-runtime-CiiFHt0e.mjs +74 -0
  37. package/dist/{ownership-Dqnrjrio.mjs → ownership-Bz2kETAS.mjs} +1 -1
  38. package/dist/{ownership-B8d4kXBx.mjs → ownership-PUP4Dwai.mjs} +134 -209
  39. package/dist/page-BTu7I60K.mjs +29 -0
  40. package/dist/{ports-Dp_QTwTC.mjs → ports-C5UutqpJ.mjs} +4 -4
  41. package/dist/{project-B8rbutVC.mjs → project-DGeclhvO.mjs} +53 -17
  42. package/dist/{reload-nocmx8KB.mjs → reload-C-cV5yy_.mjs} +79 -20
  43. package/dist/{remote-cache-BRsRZru5.mjs → remote-cache--aehr4Pr.mjs} +3 -3
  44. package/dist/{server-bare-Gn3ssSdK.mjs → server-bare-Bq8BpSIo.mjs} +1 -1
  45. package/dist/server-expo-BeYwP_Xy.mjs +2 -0
  46. package/dist/{server-expo-CeT1nIIk.mjs → server-expo-DUCg52-K.mjs} +2 -2
  47. package/dist/{settings-bE-Gu3Ti.mjs → settings-ssVBToDR.mjs} +10 -10
  48. package/dist/{settings-BlyDn2Oz.mjs → settings-xB0Frkg6.mjs} +11 -2
  49. package/dist/settings.schema.json +53 -1
  50. package/dist/{simslim-COtEEP7x.mjs → simslim-Bbg9UWrt.mjs} +12 -6
  51. package/dist/{slot-launch-DIACGEfH.mjs → slot-launch-BIWEW-wI.mjs} +3 -3
  52. package/dist/{start-BboE1HIS.mjs → start-BZhItkiL.mjs} +11 -10
  53. package/dist/{start-BFstVBlx.mjs → start-Rw2GD3nV.mjs} +1 -1
  54. package/dist/{state-DcDgzfu3.mjs → state-DwAU2_-D.mjs} +1 -1
  55. package/dist/state-l1pYwOre.mjs +148 -0
  56. package/dist/{stats-DiCtmluY.mjs → stats-CaSwmsSZ.mjs} +2 -2
  57. package/dist/{status-DI9Tjo7T.mjs → status-TJlL0HWo.mjs} +624 -103
  58. package/dist/{status-BUASYyy_.mjs → status-sszP4XKU.mjs} +59 -2
  59. package/dist/{stim-desktop-D0xhpZt8.mjs → stim-desktop-CXt8-Z3J.mjs} +13 -2
  60. package/dist/{stop-Bn6v-GvS.mjs → stop-BZ2qMiBP.mjs} +37 -12
  61. package/dist/{stop-BI7QY76M.mjs → stop-DskDKNTU.mjs} +2 -2
  62. package/dist/supervisor-run.mjs +10 -10
  63. package/dist/web-run.d.mts +22 -0
  64. package/dist/web-run.mjs +481 -0
  65. package/dist/web-sWXd5iek.mjs +394 -0
  66. package/dist/{worktree-B5sTRskL.mjs → worktree-3cBa-bQt.mjs} +1 -1
  67. package/dist/{worktree-ChDL5fO1.mjs → worktree-D4Do_XiQ.mjs} +226 -19
  68. package/dist/{xcode-BzplpQzc.mjs → xcode-LM6A8cNZ.mjs} +5 -5
  69. package/package.json +5 -5
  70. package/dist/server-expo-eWi9Coof.mjs +0 -2
  71. package/dist/workspace-process-lock-BvvJTfo_.mjs +0 -57
@@ -1,13 +1,69 @@
1
- import { E as withConfigLock, O as process_identity_exports, S as saveConfig, _ as loadConfig, c as ensureConfig, d as getConfigDir, p as getProject } from "./config-0jCcMM0E.mjs";
1
+ import { A as ownership_claim_exports, E as withConfigLock, O as process_identity_exports, S as saveConfig, _ as loadConfig, c as ensureConfig, d as getConfigDir, p as getProject } from "./config-0jCcMM0E.mjs";
2
2
  import { t as getExecutor } from "./exec-DK8KaVHK.mjs";
3
- import { c as signalProcessTree, n as listeningPids, u as isPortFree } from "./metro-Dzo30hvJ.mjs";
4
- import { t as withWorkspaceProcessLock } from "./workspace-process-lock-BvvJTfo_.mjs";
3
+ import { d as isPortFree, l as signalProcessTree, r as listeningPids } from "./metro-BPeY1YMh.mjs";
4
+ import { n as declareSpawnsOn, o as stopDeclaringSpawnsOn } from "./spawn-claims-PT8aQpCa.mjs";
5
5
  import { realpathSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ //#region src/engine/workspace-process-lock.ts
8
+ const DEFAULT_WAIT_MS = 6e4;
9
+ const POLL_MS = 25;
10
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ function workspaceProcessLockError(err) {
12
+ const code = err?.code;
13
+ if (code === "STIM_LOCK_REFUSED") return "refused";
14
+ if (code === "STIM_LOCK_TIMEOUT") return "timeout";
15
+ return null;
16
+ }
17
+ function workspaceProcessLockPath(root, name, external) {
18
+ return external ? join(root, `${name}.lock`) : join(root, ".stim", `${name}.lock`);
19
+ }
20
+ async function withWorkspaceProcessLock(root, name, fn, { now = Date.now, sleep = defaultSleep, waitMs = DEFAULT_WAIT_MS, ownerPurpose, details = {}, onHeld, rejectOwnerPurposes = [], external = false, declareSpawns = false } = {}) {
21
+ const path = workspaceProcessLockPath(root, name, external);
22
+ const deadline = now() + waitMs;
23
+ for (;;) {
24
+ const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
25
+ root: path,
26
+ mode: "exclusive",
27
+ label: `${name} lock`,
28
+ details: ownerPurpose ? {
29
+ ...details,
30
+ purpose: ownerPurpose
31
+ } : details
32
+ });
33
+ if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
34
+ if (attempt.acquired) {
35
+ if (declareSpawns) declareSpawnsOn(attempt.acquired);
36
+ try {
37
+ return await fn(attempt.acquired);
38
+ } finally {
39
+ stopDeclaringSpawnsOn(attempt.acquired);
40
+ (0, ownership_claim_exports.releaseClaim)(attempt.acquired);
41
+ }
42
+ }
43
+ const holder = attempt.held ?? attempt.waitingFor?.[0];
44
+ const purpose = holder?.details.purpose;
45
+ if (typeof purpose === "string" && rejectOwnerPurposes.includes(purpose)) {
46
+ const error = /* @__PURE__ */ new Error(`The ${name} lock at ${path} is held for ${purpose}.`);
47
+ error.code = "STIM_LOCK_REFUSED";
48
+ throw error;
49
+ }
50
+ if (holder && onHeld) await onHeld(holder);
51
+ if (now() >= deadline) {
52
+ const error = /* @__PURE__ */ new Error(`Timed out waiting for the ${name} lock at ${path}.`);
53
+ error.code = "STIM_LOCK_TIMEOUT";
54
+ throw error;
55
+ }
56
+ await sleep(POLL_MS);
57
+ }
58
+ }
59
+ //#endregion
6
60
  //#region src/named-ports.ts
7
61
  const FIRST_PORT = 8900;
8
62
  const LAST_PORT = 8999;
63
+ const BROWSER_LABEL = "web-cdp";
9
64
  function validatePortLabel(label) {
10
65
  if (label === "metro") throw new Error("metro is managed by stim start and stim stop; choose another label.");
66
+ if (label === BROWSER_LABEL) throw new Error(`${BROWSER_LABEL} is managed by stim web and stim stop; choose another label.`);
11
67
  if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,63}$/.test(label)) throw new Error("A port label must start with a letter and contain at most 64 letters, digits, underscores or hyphens.");
12
68
  }
13
69
  function portListeners(port, platform) {
@@ -51,8 +107,24 @@ function processCommand(pid, platform) {
51
107
  function withPortsLock(fn) {
52
108
  return withWorkspaceProcessLock(getConfigDir(), "ports", fn, { external: true });
53
109
  }
54
- async function getNamedPort(projectPath, label, { isFree = async (port) => await isPortFree(port) && portListeners(port, process.platform).length === 0, log = console.error } = {}) {
110
+ async function getNamedPort(projectPath, label, options = {}) {
55
111
  validatePortLabel(label);
112
+ return allocateNamedPort(projectPath, label, options);
113
+ }
114
+ /** The loopback DevTools port of the workspace's owned Chrome, allocated like a named port under `web-cdp`. */
115
+ function reserveBrowserPort(projectPath, options = {}) {
116
+ return allocateNamedPort(projectPath, BROWSER_LABEL, options);
117
+ }
118
+ function releaseBrowserPort(projectPath, port) {
119
+ return withPortsLock(async () => withConfigLock(() => {
120
+ const cfg = loadConfig();
121
+ const ports = cfg?.projects?.[projectPath]?.ports;
122
+ if (!cfg || ports?.[BROWSER_LABEL] !== port) return;
123
+ delete ports[BROWSER_LABEL];
124
+ saveConfig(cfg);
125
+ }));
126
+ }
127
+ async function allocateNamedPort(projectPath, label, { isFree = async (port) => await isPortFree(port) && portListeners(port, process.platform).length === 0, log = console.error }) {
56
128
  const root = realpathSync(projectPath);
57
129
  return withPortsLock(async () => {
58
130
  const existing = getProject(root)?.ports;
@@ -121,7 +193,7 @@ async function clearNamedPorts(projectPath, { label, stop = false, dryRun = fals
121
193
  if (label !== void 0) validatePortLabel(label);
122
194
  await withPortsLock(async () => {
123
195
  const ports = getProject(projectPath)?.ports ?? {};
124
- const selected = Object.entries(ports).filter(([name]) => label === void 0 || name === label);
196
+ const selected = Object.entries(ports).filter(([name]) => name !== BROWSER_LABEL && (label === void 0 || name === label));
125
197
  if (stop && selected.length > 0 && platform !== "win32" && !getExecutor().findExecutable("lsof")) throw new Error("Cannot stop named ports: lsof is not installed.");
126
198
  const failures = [];
127
199
  for (const [name, port] of selected) try {
@@ -149,4 +221,4 @@ async function clearNamedPorts(projectPath, { label, stop = false, dryRun = fals
149
221
  });
150
222
  }
151
223
  //#endregion
152
- export { getNamedPort as n, clearNamedPorts as t };
224
+ export { withWorkspaceProcessLock as a, reserveBrowserPort as i, getNamedPort as n, workspaceProcessLockError as o, releaseBrowserPort as r, workspaceProcessLockPath as s, clearNamedPorts as t };
@@ -0,0 +1,74 @@
1
+ import { S as ensureWorkspaceStorage, c as resolveProjectMetro, h as readWorkspaceState$1 } from "./metro-BPeY1YMh.mjs";
2
+ import { l as phaseLine } from "./command-output-BIL7rlX7.mjs";
3
+ import { r as LAUNCH_UNVERIFIED, t as LAUNCH_BUNDLING } from "./launch-verify-CuJaBpEa.mjs";
4
+ import { s as startDevServer } from "./start-BZhItkiL.mjs";
5
+ import { readIdleStop } from "@stim-cli/core/state";
6
+ import chalk from "chalk";
7
+ //#region src/commands/native-runtime.ts
8
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+ async function ensureDevServer({ root, port, settings, remote, note, resolve = resolveProjectMetro, start = startDevServer, readState = readWorkspaceState$1 }) {
10
+ const held = port === null ? null : (await resolve(port, root)).metro;
11
+ if (port !== null && held) return {
12
+ ok: true,
13
+ port,
14
+ pid: held.pid ?? null,
15
+ devServer: null,
16
+ reclaimed: []
17
+ };
18
+ const reason = readIdleStop(readState(root)) ? "stopped (idle)" : "not running";
19
+ note(chalk.dim(phaseLine("metro", `dev server ${reason}; starting it${remote ? " for a remote device" : ""}`)));
20
+ const result = await start({
21
+ root,
22
+ settings,
23
+ remote,
24
+ out: note,
25
+ note
26
+ });
27
+ if (!result.ok) return {
28
+ ok: false,
29
+ code: result.error.code,
30
+ message: `Could not start this workspace's dev server: ${result.error.message}`,
31
+ remedy: result.error.remedy,
32
+ lines: result.lines,
33
+ reclaimed: result.reclaimed
34
+ };
35
+ const { facts } = result;
36
+ return {
37
+ ok: true,
38
+ port: facts.port,
39
+ pid: facts.supervisorPid,
40
+ devServer: facts.alreadyRunning ? null : {
41
+ started: true,
42
+ reason
43
+ },
44
+ reclaimed: result.reclaimed
45
+ };
46
+ }
47
+ async function ensureWorkspaceStorageSafely(root, { note = (_line) => {} } = {}) {
48
+ try {
49
+ return ensureWorkspaceStorage(root);
50
+ } catch (err) {
51
+ note(chalk.dim(`Could not prepare this workspace's Stim state: ${err?.message || err}`));
52
+ throw err;
53
+ }
54
+ }
55
+ function launchOutcomeRecord({ launchState, release, bundleId, configuration, metroPort }) {
56
+ const unverified = launchState === LAUNCH_UNVERIFIED;
57
+ const bundling = launchState === LAUNCH_BUNDLING;
58
+ let msg;
59
+ if (release) msg = unverified ? `${bundleId} could not be verified as running after its ${configuration} launch` : `${bundleId} is running its embedded ${configuration} bundle`;
60
+ else if (unverified) msg = `no bundle request from ${bundleId} reached this workspace's Metro on port ${metroPort}`;
61
+ else if (bundling) msg = `${bundleId} requested a bundle from this workspace's Metro on port ${metroPort}; it was still being built when the launch check ended`;
62
+ else msg = `${bundleId} fetched a bundle from this workspace's Metro on port ${metroPort}`;
63
+ return {
64
+ src: "build",
65
+ level: unverified ? "warn" : "info",
66
+ event: unverified ? "launch_unverified" : bundling ? "launch_bundling" : "launch_verified",
67
+ msg
68
+ };
69
+ }
70
+ function isPhysicalDeviceRequest(flag) {
71
+ return flag !== null && flag !== void 0 && flag !== false;
72
+ }
73
+ //#endregion
74
+ export { sleep as a, launchOutcomeRecord as i, ensureWorkspaceStorageSafely as n, isPhysicalDeviceRequest as r, ensureDevServer as t };
@@ -1,5 +1,5 @@
1
1
  import { O as process_identity_exports } from "./config-0jCcMM0E.mjs";
2
- import { _ as withWorkspaceStateLock, i as pidExists, m as readWorkspaceState, y as writeWorkspaceState } from "./metro-Dzo30hvJ.mjs";
2
+ import { a as pidExists, b as writeWorkspaceState, h as readWorkspaceState, v as withWorkspaceStateLock } from "./metro-BPeY1YMh.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  //#region src/collector/state.ts
5
5
  function collectorRoot(root) {
@@ -1,13 +1,15 @@
1
- import { A as ownership_claim_exports, B as deviceSlotPlatforms, E as withConfigLock, L as assignSlotDevice, O as process_identity_exports, S as saveConfig, U as projectDeviceSlots, W as removeSlotDevice, _ as loadConfig, c as ensureConfig, d as getConfigDir, f as getConfigPath, k as acquireAvdClaim } from "./config-0jCcMM0E.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-Dzo30hvJ.mjs";
4
- import { r as workspaceProcessLockPath, t as withWorkspaceProcessLock } from "./workspace-process-lock-BvvJTfo_.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-BWCj5Vh-.mjs";
7
- import { i as readAgentDeviceRecords } from "./activity-CCZJs__P.mjs";
8
- import { A as listOrphanedAvdDirectories, F as ownedAvdMatchesConfiguration, K as shutdownAndroidEmulator, P as ownedAvdDirectory, R as ownedAvdSystemImage, T as isStimOwnedAvdName, W as resolveOwnedAvdSerial, Y as waitForAndroidEmulatorShutdown, Z as wipeAvdUserData, d as assertOwnedAvdStopped, f as avdPathExists, m as avdStorageRoots, q as sleepSync, v as deleteAvd } from "./android-75Sk2rIR.mjs";
9
- 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-CZWOHTTc.mjs";
10
- import { closeSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync } from "node:fs";
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";
11
13
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
14
  import { createHash, randomUUID } from "node:crypto";
13
15
  import { homedir } from "node:os";
@@ -111,202 +113,6 @@ function closeOwnedDeviceSessions(device, stillOwned, workspace) {
111
113
  report(`could not list agent-device sessions for ${device.id}: ${error.message}`);
112
114
  }
113
115
  }
114
- const POOL_SETTING_REMEDY = "Run `stim guide settings` for the device pool bounds and where it can be set.";
115
- const MAX_SETTING = {
116
- ios: {
117
- key: "iosParkedMax",
118
- env: "STIM_POOL_IOS_PARKED_MAX"
119
- },
120
- android: {
121
- key: "androidParkedMax",
122
- env: "STIM_POOL_ANDROID_PARKED_MAX"
123
- }
124
- };
125
- function parseMax(raw, strings) {
126
- const value = strings && typeof raw === "string" ? /^\d+$/.test(raw.trim()) ? Number(raw.trim()) : NaN : raw;
127
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return null;
128
- return value;
129
- }
130
- function parkedMaxSetting(platform, { config, env = process.env } = {}) {
131
- const { key, env: envKey } = MAX_SETTING[platform];
132
- const fromEnv = env[envKey];
133
- const explicit = fromEnv !== void 0 && fromEnv !== "";
134
- const pool = (config === void 0 ? loadConfig() : config)?.pool;
135
- if (!explicit && pool !== void 0 && (pool === null || typeof pool !== "object" || Array.isArray(pool))) return {
136
- max: 0,
137
- error: "Invalid pool value. Expected an object with simulator and emulator bounds."
138
- };
139
- const fromConfig = pool !== null && typeof pool === "object" && !Array.isArray(pool) ? pool[key] : void 0;
140
- const raw = explicit ? fromEnv : fromConfig;
141
- if (raw === void 0) return {
142
- max: env.STIM_HOME ? 0 : 3,
143
- error: null
144
- };
145
- const parsed = parseMax(raw, explicit);
146
- if (parsed === null) return {
147
- max: 0,
148
- error: `Invalid ${explicit ? envKey : `pool.${key}`} value ${JSON.stringify(raw)}. Expected a whole number of parked ${platform === "ios" ? "simulators" : "emulators"}, 0 or more.`
149
- };
150
- return {
151
- max: explicit ? parsed : env.STIM_HOME ? 0 : parsed,
152
- error: null
153
- };
154
- }
155
- function isParkedRecord(value, platform) {
156
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
157
- const record = value;
158
- 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"));
159
- }
160
- function poolBlock(config) {
161
- const raw = config?.parked;
162
- const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
163
- const read = (platform) => {
164
- const list = block[platform];
165
- return Array.isArray(list) ? list.filter((record) => isParkedRecord(record, platform)) : [];
166
- };
167
- return {
168
- ios: read("ios"),
169
- android: read("android")
170
- };
171
- }
172
- function readParked(platform, { config } = {}) {
173
- return poolBlock(config === void 0 ? loadConfig() : config)[platform];
174
- }
175
- function writeParked(config, platform, records) {
176
- config.parked = {
177
- ...poolBlock(config),
178
- [platform]: records
179
- };
180
- }
181
- function oldestFirst(records) {
182
- return records.toSorted((a, b) => String(a.parkedAt).localeCompare(String(b.parkedAt)));
183
- }
184
- function isLegacyDeletionClaim(value) {
185
- if (value === void 0) return false;
186
- if (value === null || typeof value !== "object" || Array.isArray(value)) return true;
187
- const marker = value;
188
- return marker.kind !== "ownership-claim" || typeof marker.claimId !== "string" || marker.claimId.length === 0;
189
- }
190
- function selectParked(records, { deviceTypeIdentifier, runtimeIdentifier }) {
191
- return oldestFirst(records.filter((r) => !isLegacyDeletionClaim(r.deletionClaim) && r.deviceTypeIdentifier === deviceTypeIdentifier && r.runtimeIdentifier === runtimeIdentifier));
192
- }
193
- function evictOverflow(records, max) {
194
- if (records.length <= max) return {
195
- keep: [...records],
196
- evicted: []
197
- };
198
- const ordered = oldestFirst(records);
199
- const evicted = ordered.slice(0, ordered.length - max);
200
- const dropped = new Set(evicted.map((r) => r.udid));
201
- return {
202
- keep: records.filter((r) => !dropped.has(r.udid)),
203
- evicted
204
- };
205
- }
206
- function parkSim({ platform, projectPath, slot = "default", record, max }) {
207
- return withConfigLock(() => {
208
- const cfg = ensureConfig();
209
- const current = deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform];
210
- const currentId = platform === "ios" ? current?.deviceUdid : current?.avdName;
211
- if (!current?.owned || currentId !== record.udid) throw new Error(`The ${platform === "ios" ? "simulator" : "emulator"} assignment changed before parking.`);
212
- const { keep, evicted } = evictOverflow([...readParked(platform, { config: cfg }).filter((r) => r.udid !== record.udid), record], max);
213
- writeParked(cfg, platform, [...keep, ...evicted]);
214
- const project = cfg.projects[projectPath];
215
- if (project) removeSlotDevice(project, platform, slot);
216
- saveConfig(cfg);
217
- return evicted;
218
- });
219
- }
220
- function adoptParked({ platform, projectPath, slot = "default", udid, device }) {
221
- const claim = claimParkedOperation(platform, udid);
222
- if (!claim) return null;
223
- try {
224
- return withConfigLock(() => {
225
- const cfg = ensureConfig();
226
- const records = readParked(platform, { config: cfg });
227
- const taken = records.find((r) => r.udid === udid);
228
- if (!taken || isLegacyDeletionClaim(taken.deletionClaim)) return null;
229
- if (deviceSlotPlatforms(cfg.projects[projectPath], slot)?.[platform]) return null;
230
- writeParked(cfg, platform, records.filter((r) => r.udid !== udid));
231
- const project = cfg.projects[projectPath];
232
- if (!project) throw new Error(`Project not registered: ${projectPath}`);
233
- assignSlotDevice(project, platform, device, slot);
234
- saveConfig(cfg);
235
- const adopted = { ...taken };
236
- delete adopted.deletionClaim;
237
- return adopted;
238
- });
239
- } finally {
240
- (0, ownership_claim_exports.releaseClaim)(claim);
241
- }
242
- }
243
- function claimParkedOperation(platform, udid) {
244
- const attempt = (0, ownership_claim_exports.tryAcquireClaim)({
245
- root: join(getConfigDir(), "pool-locks", platform, `${encodeURIComponent(udid.toLowerCase())}.lock`),
246
- mode: "exclusive",
247
- label: `parked ${platform} device ${udid}`
248
- });
249
- if (attempt.pending) (0, ownership_claim_exports.releaseClaim)(attempt.pending);
250
- return attempt.acquired ?? null;
251
- }
252
- function removeParkedAfter(platform, udid, beforeRemove) {
253
- const claim = claimParkedOperation(platform, udid);
254
- if (!claim) return null;
255
- try {
256
- const claimed = withConfigLock(() => {
257
- const cfg = loadConfig();
258
- if (!cfg) return null;
259
- const records = readParked(platform, { config: cfg });
260
- const current = records.find((candidate) => candidate.udid === udid);
261
- if (!current) return null;
262
- 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.`);
263
- const record = { ...current };
264
- delete record.deletionClaim;
265
- const marked = {
266
- ...record,
267
- deletionClaim: {
268
- kind: "ownership-claim",
269
- claimId: claim.claimId
270
- }
271
- };
272
- writeParked(cfg, platform, records.map((candidate) => candidate.udid === udid ? marked : candidate));
273
- saveConfig(cfg);
274
- return {
275
- record,
276
- marked
277
- };
278
- });
279
- if (!claimed) return null;
280
- let completed = false;
281
- let removed = null;
282
- try {
283
- (0, ownership_claim_exports.markClaimChildPending)(claim);
284
- try {
285
- beforeRemove(claimed.record);
286
- } finally {
287
- (0, ownership_claim_exports.clearClaimChild)(claim);
288
- }
289
- completed = true;
290
- } finally {
291
- withConfigLock(() => {
292
- const cfg = loadConfig();
293
- if (!cfg) return;
294
- const records = readParked(platform, { config: cfg });
295
- const current = records.find((candidate) => candidate.udid === udid);
296
- if (!isDeepStrictEqual(current, claimed.marked)) return;
297
- writeParked(cfg, platform, completed ? records.filter((candidate) => candidate.udid !== udid) : records.map((candidate) => candidate.udid === udid ? claimed.record : candidate));
298
- saveConfig(cfg);
299
- if (completed) removed = claimed.record;
300
- });
301
- }
302
- return removed;
303
- } finally {
304
- (0, ownership_claim_exports.releaseClaim)(claim);
305
- }
306
- }
307
- function dropParked(platform, udid) {
308
- return removeParkedAfter(platform, udid, () => {}) !== null;
309
- }
310
116
  //#endregion
311
117
  //#region src/devices/teardown.ts
312
118
  function teardownParkedAvd(avdName) {
@@ -500,8 +306,16 @@ function removeOrphanedAvdDirectory(candidate, owner) {
500
306
  const current = lstatSync(candidate.directory);
501
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.`);
502
308
  assertAvdReferences(candidate.name, owner);
503
- const destination = join(dirname(candidate.directory), `stim-gc-${randomUUID()}.avd`);
504
- 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);
505
319
  return destination;
506
320
  });
507
321
  const current = lstatSync(detached);
@@ -511,6 +325,7 @@ function removeOrphanedAvdDirectory(candidate, owner) {
511
325
  } catch (error) {
512
326
  throw new Error(`Could not remove AVD data at ${detached}; retry gc --delete: ${error.message}`, { cause: error });
513
327
  }
328
+ forgetCreatedDevice("android", basename(detached, ".avd"));
514
329
  }
515
330
  function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
516
331
  const candidates = surveyed ? [surveyed] : listOrphanedAvdDirectories(avdName);
@@ -534,7 +349,7 @@ function teardownUnregisteredAvd(avdName, del, owner, surveyed) {
534
349
  function teardownOwnedAvd(avdName, options = {}) {
535
350
  let claim;
536
351
  try {
537
- if (!isStimOwnedAvdName(avdName, options.orphanedDirectory ? options.orphanedDirectory.directory : void 0)) return {
352
+ if (!isStimOwnedAvd(avdName)) return {
538
353
  status: "skipped",
539
354
  kind: "not-owned",
540
355
  reason: `AVD ${avdName} is not Stim-owned: Stim has no record of creating it`
@@ -591,7 +406,7 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, workspac
591
406
  (0, ownership_claim_exports.clearClaimChild)(claim);
592
407
  }
593
408
  });
594
- } else assertStopped(avdName);
409
+ } else waitForShutdown(avdName, null);
595
410
  if (del) {
596
411
  const current = resolveAvd(avdName);
597
412
  if (current.notOwned) return {
@@ -672,6 +487,116 @@ function teardownClaimedAvd(avdName, claim, { del = false, park, owner, workspac
672
487
  };
673
488
  }
674
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
+ }
675
600
  //#endregion
676
601
  //#region src/engine/public-http-probe.ts
677
602
  const DNS_TIMEOUT_MS = 5e3;
@@ -1210,4 +1135,4 @@ function numberOrNull(v) {
1210
1135
  return typeof v === "number" && Number.isSafeInteger(v) && v > 0 ? v : null;
1211
1136
  }
1212
1137
  //#endregion
1213
- 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
1
  import { p as getProject } from "./config-0jCcMM0E.mjs";
2
- import { i as findProjectRoot } from "./project-B8rbutVC.mjs";
3
- import { n as getNamedPort, t as clearNamedPorts } from "./named-ports-Dn1o61D9.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>.");