stim 1.1.0 → 1.3.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 (64) hide show
  1. package/README.md +37 -0
  2. package/dist/{android-CC5bUer7.mjs → android-BW8i_YxU.mjs} +1 -1
  3. package/dist/android-BqVIJoG7.mjs +1368 -0
  4. package/dist/{android-DnZlT3aU.mjs → android-CqcW1hrP.mjs} +205 -58
  5. package/dist/{android-cas-DGlTStWu.mjs → android-cas-BR5YJBYv.mjs} +6 -6
  6. package/dist/android-cas-compiler.mjs +1 -1
  7. package/dist/{app-install-oZIQRbdD.mjs → app-install-DIp9riNx.mjs} +29 -21
  8. package/dist/{build-slots-CgacA9zQ.mjs → build-slots-C3-R4XQr.mjs} +3 -3
  9. package/dist/{cache-manifest-oeH18ERr.mjs → cache-manifest-CXR-Y8pH.mjs} +1 -1
  10. package/dist/cache-manifest.mjs +1 -1
  11. package/dist/cli.mjs +13 -13
  12. package/dist/collector-run.d.mts +4 -1
  13. package/dist/collector-run.mjs +36 -11
  14. package/dist/{config-D7uu8Gbq.mjs → config-CcefMY2Q.mjs} +94 -13
  15. package/dist/{deps-64wVnpZc.mjs → deps-Spz8ozgo.mjs} +2 -2
  16. package/dist/{dev-client-Bvky1hya.mjs → dev-client-BjisHDB5.mjs} +250 -10
  17. package/dist/{device-DJKv5Qej.mjs → device-BkiW-gkk.mjs} +19 -11
  18. package/dist/{device-lease-D-G3SgVe.mjs → device-lease-BJYybF-2.mjs} +107 -83
  19. package/dist/{device-pool-BlHXnYYs.mjs → device-pool-CPbLGfrB.mjs} +19 -13
  20. package/dist/{device-remote-CCgkG82J.mjs → device-remote-AkYCKS8U.mjs} +8 -8
  21. package/dist/{doctor-DlVxTtMq.mjs → doctor-Bmv86UUW.mjs} +18 -26
  22. package/dist/{doctor-BRF9aAJ3.mjs → doctor-fAMXC4vl.mjs} +10 -10
  23. package/dist/{error-diagnostics-B5IJwv3-.mjs → error-diagnostics-DHC31rXA.mjs} +65 -55
  24. package/dist/{exec-bsN9MJXb.mjs → exec-CyylIdq9.mjs} +4 -2
  25. package/dist/{gc-DhXdgSdt.mjs → gc-GGu-8ULN.mjs} +90 -67
  26. package/dist/{guide-DrhrDBo5.mjs → guide-92CPA21s.mjs} +414 -47
  27. package/dist/{ios-CK1Vrk6Q.mjs → ios-BSXFeO5H.mjs} +200 -59
  28. package/dist/{ios-BZxKPuqP.mjs → ios-Deh5js5V.mjs} +147 -40
  29. package/dist/{ios-device-CdSm0GDw.mjs → ios-device-CWOM_S44.mjs} +2 -2
  30. package/dist/{ios-device-CYbuRNXR.mjs → ios-device-D3pzTKQE.mjs} +1 -1
  31. package/dist/{logs-Dxoy16Qv.mjs → logs-DPNF4C9s.mjs} +9 -6
  32. package/dist/{logs-query-oqn47Ffx.mjs → logs-query-R-Cqc6Qv.mjs} +22 -5
  33. package/dist/{metro-B01H3EuD.mjs → metro-CORqXyTL.mjs} +3 -3
  34. package/dist/{ndjson-DcAtEx_K.mjs → ndjson-BZwI1NBv.mjs} +5 -2
  35. package/dist/{ownership-D8HmSv2E.mjs → ownership-8O073GIC.mjs} +3 -3
  36. package/dist/{ownership-B9QIdQOw.mjs → ownership-BpCKiRlQ.mjs} +4 -4
  37. package/dist/{ownership-claim-CeJLEpjD.mjs → ownership-claim-Bf2CAfw3.mjs} +4 -3
  38. package/dist/{project-COj6nzzh.mjs → project-fKmuIaMI.mjs} +2 -2
  39. package/dist/{reclaim-BPvN2HlP.mjs → reclaim-iW9Cad7k.mjs} +80 -73
  40. package/dist/{reload-DYZEBbZy.mjs → reload-DazHDSFA.mjs} +32 -24
  41. package/dist/{server-bare-BYHqEeyw.mjs → server-bare-DEzPZLvf.mjs} +7 -2
  42. package/dist/server-expo-44em5Pmg.mjs +2 -0
  43. package/dist/{server-expo-BEMr_m6p.mjs → server-expo-DizT4OMk.mjs} +5 -5
  44. package/dist/{settings-BSwrY4FZ.mjs → settings-DEJf6_kJ.mjs} +26 -5
  45. package/dist/slot-launch-cci0xvmC.mjs +19 -0
  46. package/dist/{start-ByCylYtB.mjs → start-mObjQH2W.mjs} +12 -12
  47. package/dist/{state-IYEY-zi8.mjs → state-B3XA2wp2.mjs} +9 -9
  48. package/dist/{stats-wqxPm7Tv.mjs → stats-BR-kwj0B.mjs} +1 -1
  49. package/dist/{stats-DZ4Q5J-7.mjs → stats-CaaQr4yN.mjs} +3 -3
  50. package/dist/{status-Dia0XTqG.mjs → status-BxQNqu9t.mjs} +86 -25
  51. package/dist/{stop-D6o5zAdm.mjs → stop-CrHGcjrV.mjs} +2 -2
  52. package/dist/{stop-BApyaF21.mjs → stop-DimGXEKY.mjs} +93 -42
  53. package/dist/supervisor-run.d.mts +1 -2
  54. package/dist/supervisor-run.mjs +5 -5
  55. package/dist/{remote-cache-DOi5-zLj.mjs → teardown-ClFp1Aax.mjs} +889 -830
  56. package/dist/{workspace-process-lock-CxOO9-pf.mjs → workspace-process-lock-3NcQnZ0X.mjs} +1 -1
  57. package/dist/{worktree-DxEmUI_n.mjs → worktree-DxTgN1CJ.mjs} +16 -16
  58. package/dist/{worktree-Dx3ZC5lS.mjs → worktree-Eq6TLH8N.mjs} +7 -3
  59. package/dist/{xcode-CDB6fytw.mjs → xcode-D7sAp-Zy.mjs} +256 -103
  60. package/package.json +5 -5
  61. package/shim/bundle-response.cjs +5 -0
  62. package/dist/android-xmyGBNy5.mjs +0 -665
  63. package/dist/build-lock-vE45ZzJZ.mjs +0 -645
  64. package/dist/server-expo-rb0k-0w7.mjs +0 -2
@@ -1,15 +1,252 @@
1
- import { t as getExecutor } from "./exec-bsN9MJXb.mjs";
2
- import { y as ensureWorkspaceStorage } from "./state-IYEY-zi8.mjs";
3
- import { C as removeTemporaryEntry, S as makeTemporaryDirectory } from "./worktree-Dx3ZC5lS.mjs";
4
- import { i as detectIsExpo, o as isPackageResolvable } from "./project-COj6nzzh.mjs";
1
+ import { t as getExecutor } from "./exec-CyylIdq9.mjs";
2
+ import { y as ensureWorkspaceStorage } from "./state-B3XA2wp2.mjs";
3
+ import { C as removeTemporaryEntry, S as makeTemporaryDirectory } from "./worktree-Eq6TLH8N.mjs";
4
+ import { i as detectIsExpo, o as isPackageResolvable } from "./project-fKmuIaMI.mjs";
5
5
  import { n as stripAnsi, r as waitForChild, t as createLineReader } from "./process-output-C7IlIzd7.mjs";
6
- import { t as NOT_OURS_FOREIGN_CWD } from "./metro-B01H3EuD.mjs";
7
- import { i as LAUNCH_BUNDLING, s as LAUNCH_UNVERIFIED } from "./app-install-oZIQRbdD.mjs";
8
- import { f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS } from "./xcode-CDB6fytw.mjs";
9
- import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-BEMr_m6p.mjs";
10
- import { existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
6
+ import { t as NOT_OURS_FOREIGN_CWD } from "./metro-CORqXyTL.mjs";
7
+ import { r as claimFailure } from "./ownership-claim-Bf2CAfw3.mjs";
8
+ import { V as releaseBuildLock, z as acquireBuildLock } from "./android-BqVIJoG7.mjs";
9
+ import { i as expoBinRefusal, r as expoBinPath, t as cleanLine } from "./server-expo-DizT4OMk.mjs";
10
+ import { x as resolveEasCliBin } from "./teardown-ClFp1Aax.mjs";
11
+ import { i as LAUNCH_BUNDLING, s as LAUNCH_UNVERIFIED } from "./app-install-DIp9riNx.mjs";
12
+ import { f as startBuildHeartbeat, p as tailLines, r as HEARTBEAT_INTERVAL_MS } from "./xcode-D7sAp-Zy.mjs";
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
14
+ import { createHash } from "node:crypto";
11
15
  import { basename, join } from "node:path";
12
16
  import chalk from "chalk";
17
+ import { request } from "node:http";
18
+ //#region src/engine/eas-build.ts
19
+ function isEasBuildFailure(result) {
20
+ return result?.ok === false;
21
+ }
22
+ function easOptionRefusal({ profile, isExpo, buildSelector, buildCache }) {
23
+ if (profile === void 0) return null;
24
+ if (profile.trim() && isExpo && buildSelector === void 0 && buildCache !== false) return null;
25
+ return {
26
+ code: "STIM_BAD_ARG",
27
+ message: "--eas-profile requires an Expo project and a non-empty development profile.",
28
+ remedy: "Use --eas-profile <name> without --scheme, --configuration, --variant, or --no-build-cache."
29
+ };
30
+ }
31
+ function object(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
33
+ }
34
+ function shellArg(value) {
35
+ return /^[a-zA-Z0-9_.-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
36
+ }
37
+ function easDeviceBuildRemedy(profile) {
38
+ return `If the device is not registered, run npx eas-cli device:create. With approval for cloud build costs and signing changes, run npx eas-cli build --platform ios --profile ${shellArg(profile)} to produce a build with a valid profile for this device, then retry the same Stim command.`;
39
+ }
40
+ var EasFailure = class extends Error {
41
+ code;
42
+ remedy;
43
+ constructor(code, message, remedy) {
44
+ super(message);
45
+ this.code = code;
46
+ this.remedy = remedy;
47
+ }
48
+ };
49
+ function selectEasBuild(builds, { platform, profile, fingerprint, projectId, physical = false }) {
50
+ if (!Array.isArray(builds)) throw new Error("EAS build:list did not return an array.");
51
+ for (const value of builds) {
52
+ const build = object(value);
53
+ if (typeof build.id === "string" && build.id.length > 0 && build.status === "FINISHED" && build.platform === platform.toUpperCase() && build.buildProfile === profile && object(build.fingerprint).hash === fingerprint && object(build.project).id === projectId && (platform !== "ios" || build.isForIosSimulator === !physical) && build.distribution === "INTERNAL" && typeof object(build.artifacts).applicationArchiveUrl === "string") return build.id;
54
+ }
55
+ return null;
56
+ }
57
+ async function resolveEasDevelopmentBuild({ root, platform, profile, note, isExpo = true, physical = false, selectors = [], buildCache }) {
58
+ if (profile === void 0) return null;
59
+ const refusal = easOptionRefusal({
60
+ profile,
61
+ isExpo,
62
+ buildSelector: selectors.find((value) => value != null) ?? void 0,
63
+ buildCache
64
+ });
65
+ if (refusal) return {
66
+ ok: false,
67
+ ...refusal
68
+ };
69
+ const retry = "the same Stim command";
70
+ const buildCommand = `npx eas-cli build --platform ${platform} --profile ${shellArg(profile)}`;
71
+ let lock = null;
72
+ try {
73
+ const cli = resolveEasCliBin(root);
74
+ if (!cli) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS CLI is not installed.", "Install eas-cli, run eas login, then retry.");
75
+ let profileEnv = {};
76
+ const run = (args, timeoutMs = 12e4) => {
77
+ try {
78
+ return JSON.parse(getExecutor().runFile(cli.file, [
79
+ ...args,
80
+ "--json",
81
+ "--non-interactive"
82
+ ], {
83
+ cwd: root,
84
+ timeoutMs,
85
+ env: profileEnv
86
+ }));
87
+ } catch {
88
+ throw new EasFailure("STIM_EAS_UNAVAILABLE", `EAS ${args[0]} failed or returned invalid JSON.`, `Run npx eas-cli ${args.map(shellArg).join(" ")} to inspect the error, then retry ${retry}.`);
89
+ }
90
+ };
91
+ note(`eas resolving profile ${profile}`);
92
+ const config = object(run([
93
+ "config",
94
+ "--platform",
95
+ platform,
96
+ "--profile",
97
+ profile
98
+ ]));
99
+ const buildProfile = object(config.buildProfile);
100
+ profileEnv = object(buildProfile.env);
101
+ const projectId = object(object(object(config.appConfig).extra).eas).projectId;
102
+ if (buildProfile.developmentClient !== true || buildProfile.distribution !== "internal" || platform === "ios" && buildProfile.simulator === true !== !physical) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} is not an internal development build for this ${platform} target.`, `Set developmentClient: true and distribution: internal${platform === "ios" ? `, with ios.simulator: ${!physical}` : ""} in that eas.json profile.`);
103
+ if (typeof projectId !== "string" || !projectId) throw new EasFailure("STIM_BAD_ARG", "The Expo app is not linked to an EAS project.", "Link the intended EAS project with eas init, then retry.");
104
+ if (platform === "ios" ? buildProfile.buildConfiguration !== void 0 && buildProfile.buildConfiguration !== "Debug" : buildProfile.gradleCommand !== void 0 && (typeof buildProfile.gradleCommand !== "string" || !/^:app:assemble\w*Debug$/.test(buildProfile.gradleCommand))) throw new EasFailure("STIM_BAD_ARG", `EAS profile ${profile} overrides the development build configuration.`, "Use ios.buildConfiguration: Debug or an android.gradleCommand that assembles a single Debug APK, or remove the override.");
105
+ note(`eas fingerprinting ${platform} with profile ${profile}`);
106
+ const fingerprint = object(run([
107
+ "fingerprint:generate",
108
+ "--platform",
109
+ platform,
110
+ "--build-profile",
111
+ profile
112
+ ])).hash;
113
+ if (typeof fingerprint !== "string" || !/^[a-f0-9]{40,64}$/.test(fingerprint)) throw new EasFailure("STIM_EAS_UNAVAILABLE", "EAS returned no valid native fingerprint.", `Run npx eas-cli fingerprint:generate --platform ${platform} --build-profile ${shellArg(profile)} to inspect the result.`);
114
+ note(`eas looking for ${fingerprint.slice(0, 8)}`);
115
+ const buildId = selectEasBuild(run([
116
+ "build:list",
117
+ "--platform",
118
+ platform,
119
+ "--build-profile",
120
+ profile,
121
+ "--fingerprint-hash",
122
+ fingerprint,
123
+ "--status",
124
+ "finished",
125
+ ...platform === "ios" && !physical ? ["--simulator"] : ["--distribution", "internal"],
126
+ "--limit",
127
+ "1"
128
+ ]), {
129
+ platform,
130
+ profile,
131
+ fingerprint,
132
+ projectId,
133
+ physical
134
+ });
135
+ if (!buildId) throw new EasFailure("STIM_EAS_BUILD_MISSING", `No compatible EAS ${platform} build exists for profile ${profile} and fingerprint ${fingerprint}.`, `A cloud build may incur charges. With approval to run it, use ${buildCommand}, then retry ${retry}.`);
136
+ const cacheKey = `eas-${createHash("sha256").update(JSON.stringify({
137
+ projectId,
138
+ buildId
139
+ })).digest("hex")}-debug-${physical && platform === "ios" ? "device" : "sim"}`;
140
+ lock = acquireBuildLock({
141
+ platform,
142
+ key: cacheKey,
143
+ root
144
+ });
145
+ if (!lock.acquired) throw new EasFailure("STIM_EAS_UNAVAILABLE", `Another run holds the EAS artifact claim at ${lock.path}.`, `Wait for that run to finish, then retry ${retry}.`);
146
+ note(`eas resolving artifact for build ${buildId}`);
147
+ const result = object(run([
148
+ "build:download",
149
+ "--build-id",
150
+ buildId
151
+ ], 6e5));
152
+ if (typeof result.path !== "string" || !existsSync(result.path)) throw new Error("EAS returned no downloaded artifact.");
153
+ const path = realpathSync(result.path);
154
+ if (platform === "ios" ? !path.endsWith(".app") || !statSync(path).isDirectory() : !path.endsWith(".apk") || !statSync(path).isFile()) throw new Error("EAS returned an unexpected artifact path or type.");
155
+ note(`eas artifact ready ${fingerprint.slice(0, 8)}`);
156
+ return {
157
+ ok: true,
158
+ path,
159
+ fingerprint,
160
+ cacheKey,
161
+ cacheHit: "remote"
162
+ };
163
+ } catch (error) {
164
+ const claim = claimFailure(error, null);
165
+ if (claim) return {
166
+ ok: false,
167
+ code: claim.code,
168
+ message: claim.message,
169
+ remedy: claim.remedy
170
+ };
171
+ if (error instanceof EasFailure) return {
172
+ ok: false,
173
+ code: error.code,
174
+ message: error.message,
175
+ remedy: error.remedy
176
+ };
177
+ return {
178
+ ok: false,
179
+ code: "STIM_EAS_UNAVAILABLE",
180
+ message: `Could not resolve the EAS development build: ${error.message}`,
181
+ remedy: `Retry ${retry}. No cloud build was started.`
182
+ };
183
+ } finally {
184
+ releaseBuildLock(lock);
185
+ }
186
+ }
187
+ //#endregion
188
+ //#region src/engine/metro-warmup.ts
189
+ const WARMUP_TIMEOUT_MS = 6e4;
190
+ async function requestLocal(url, platform, prefetch) {
191
+ return new Promise((resolve) => {
192
+ const req = request(url, { headers: {
193
+ accept: "application/json",
194
+ "expo-platform": platform,
195
+ ...prefetch ? { "x-stim-metro-warmup": "1" } : {}
196
+ } });
197
+ const timer = setTimeout(() => req.destroy(), WARMUP_TIMEOUT_MS);
198
+ timer.unref();
199
+ req.on("socket", (socket) => socket.unref());
200
+ req.on("error", () => resolve(null));
201
+ req.on("close", () => {
202
+ clearTimeout(timer);
203
+ resolve(null);
204
+ });
205
+ req.on("response", (res) => {
206
+ let body = "";
207
+ res.setEncoding("utf8");
208
+ res.on("data", (chunk) => {
209
+ if (!prefetch) body += chunk;
210
+ });
211
+ res.on("error", () => resolve(null));
212
+ res.on("end", () => resolve(res.statusCode === 200 ? body : null));
213
+ });
214
+ req.end();
215
+ });
216
+ }
217
+ async function warmMetro({ port, platform, isExpo, appId, bundleUrl }) {
218
+ try {
219
+ const origin = `http://127.0.0.1:${port}`;
220
+ if (await requestLocal(new URL("/_stim/metro-warmup", origin), platform, false) !== "ready") return;
221
+ const url = new URL("/index.bundle", origin);
222
+ url.search = new URLSearchParams({
223
+ platform,
224
+ dev: "true",
225
+ lazy: "true",
226
+ minify: "false",
227
+ ...platform === "ios" ? { inlineSourceMap: "false" } : {},
228
+ modulesOnly: "false",
229
+ runModule: "true",
230
+ excludeSource: "true",
231
+ sourcePaths: "url-server",
232
+ ...appId ? { app: appId } : {}
233
+ }).toString();
234
+ if (!bundleUrl && isExpo) {
235
+ const body = await requestLocal(new URL(origin), platform, false);
236
+ if (body === null) return;
237
+ const manifest = JSON.parse(body);
238
+ bundleUrl = manifest.launchAsset?.url ?? manifest.bundleUrl;
239
+ if (typeof bundleUrl !== "string") return;
240
+ }
241
+ if (bundleUrl) {
242
+ const bundle = new URL(bundleUrl, origin);
243
+ url.pathname = bundle.pathname;
244
+ url.search = bundle.search;
245
+ }
246
+ await requestLocal(url, platform, true);
247
+ } catch {}
248
+ }
249
+ //#endregion
13
250
  //#region src/commands/native-runtime.ts
14
251
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
252
  const GATE_RETRY_DELAYS_MS = [
@@ -72,6 +309,9 @@ function launchOutcomeRecord({ launchState, release, bundleId, configuration, me
72
309
  msg
73
310
  };
74
311
  }
312
+ function isPhysicalDeviceRequest(flag) {
313
+ return flag !== null && flag !== void 0 && flag !== false;
314
+ }
75
315
  //#endregion
76
316
  //#region src/engine/prebuild.ts
77
317
  const PREBUILD_ERROR = "STIM_PREBUILD_FAILED";
@@ -554,4 +794,4 @@ function readJson(file) {
554
794
  }
555
795
  }
556
796
  //#endregion
557
- export { swapJsBundle as a, ensureWorkspaceStorageSafely as c, noMetroMessage as d, noMetroRemedy as f, detectEntryFile as i, gateShouldRetry as l, sleep as m, pickDevClientScheme as n, needsPrebuild as o, resolveMetroWithRetry as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, launchOutcomeRecord as u };
797
+ export { easDeviceBuildRemedy as _, swapJsBundle as a, ensureWorkspaceStorageSafely as c, launchOutcomeRecord as d, noMetroMessage as f, warmMetro as g, sleep as h, detectEntryFile as i, gateShouldRetry as l, resolveMetroWithRetry as m, pickDevClientScheme as n, needsPrebuild as o, noMetroRemedy as p, schemesFromInfoPlist as r, runPrebuild as s, devClientScheme as t, isPhysicalDeviceRequest as u, isEasBuildFailure as v, resolveEasDevelopmentBuild as y };
@@ -1,9 +1,10 @@
1
- import { a as findProjectRoot } from "./project-COj6nzzh.mjs";
1
+ import { V as validateDeviceSlot } from "./config-CcefMY2Q.mjs";
2
+ import { a as findProjectRoot } from "./project-fKmuIaMI.mjs";
2
3
  import { r as clockTime } from "./command-output-8wHMEde0.mjs";
3
- import { f as takeLease, l as releaseWorkspaceLeases, n as fileLeaseIo, o as parseLeaseDuration } from "./device-lease-D-G3SgVe.mjs";
4
- import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-BlHXnYYs.mjs";
5
- import { E as physicalDeviceModel, O as probeEmulatorSerial, _ as listAdbDevices, b as memoizeEmulatorProbe, i as androidPoolNoCandidatesRefusal, j as resolvePhysicalDevice, r as androidPoolCandidates } from "./android-xmyGBNy5.mjs";
6
- import { c as listIosDevices, o as iosPoolCandidates, s as iosPoolNoCandidatesRefusal, u as resolveIosPhysicalDevice } from "./ios-device-CdSm0GDw.mjs";
4
+ import { C as memoizeEmulatorProbe, P as resolvePhysicalDevice, i as androidPoolNoCandidatesRefusal, j as probeEmulatorSerial, k as physicalDeviceModel, r as androidPoolCandidates, y as listAdbDevices } from "./android-BqVIJoG7.mjs";
5
+ import { f as takeLease, l as releaseWorkspaceLeases, n as fileLeaseIo, o as parseLeaseDuration } from "./device-lease-BJYybF-2.mjs";
6
+ import { d as waitForDevice, s as parseDeviceWait, t as selectFromPool } from "./device-pool-CPbLGfrB.mjs";
7
+ import { c as listIosDevices, o as iosPoolCandidates, s as iosPoolNoCandidatesRefusal, u as resolveIosPhysicalDevice } from "./ios-device-CWOM_S44.mjs";
7
8
  import chalk from "chalk";
8
9
  //#region src/commands/device.ts
9
10
  const DEFAULT_FOR = "5m";
@@ -24,7 +25,7 @@ const DEFAULT_DEPS = {
24
25
  note: (line) => console.error(line)
25
26
  };
26
27
  function grantLine(facts, durationText) {
27
- return `locked ${facts.deviceName ? `${facts.deviceName} (${facts.id})` : facts.id} for ${facts.holder} until ${clockTime(facts.expiresAt)} (${durationText}). Renew: stim device lock ${facts.platform} --for ${durationText}. Release: stim device unlock.`;
28
+ return `locked ${facts.deviceName ? `${facts.deviceName} (${facts.id})` : facts.id} for ${facts.holder} until ${clockTime(facts.expiresAt)} (${durationText}). Renew: stim device lock ${facts.platform}${facts.slot ? ` --slot ${facts.slot}` : ""} --for ${durationText}. Release: stim device unlock${facts.slot ? ` --slot ${facts.slot}` : ""}.`;
28
29
  }
29
30
  function releasedLine(lease) {
30
31
  return `unlocked ${lease.deviceName ? `${lease.deviceName} (${lease.id})` : lease.id} -- the ${lease.platform} lease ran until ${clockTime(lease.expiresAt)}.`;
@@ -59,9 +60,10 @@ function resolveDevice(platform, requested, d) {
59
60
  function isFailure(value) {
60
61
  return "code" in value;
61
62
  }
62
- async function poolDevice(platform, idLabel, waitSeconds, deadline, root, d) {
63
+ async function poolDevice(platform, idLabel, waitSeconds, deadline, root, d, slot) {
63
64
  const isEmulator = memoizeEmulatorProbe(d.probeEmulatorSerial);
64
65
  const pooled = await d.selectFromPool({
66
+ ...slot ? { slot } : {},
65
67
  root,
66
68
  platform,
67
69
  idLabel,
@@ -140,10 +142,11 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
140
142
  const idLabel = platform === "ios" ? "udid" : "serial";
141
143
  const deadline = d.now() + wait.seconds * 1e3;
142
144
  const lastLine = { at: null };
143
- const device = idArg ? resolveDevice(platform, idArg, d) : await poolDevice(platform, idLabel, wait.seconds, deadline, root, d);
145
+ const device = idArg ? resolveDevice(platform, idArg, d) : await poolDevice(platform, idLabel, wait.seconds, deadline, root, d, opts.slot);
144
146
  if (isFailure(device)) return report(device);
145
147
  for (;;) {
146
148
  const outcome = await d.waitForDevice({
149
+ ...opts.slot ? { slot: opts.slot } : {},
147
150
  root,
148
151
  platform,
149
152
  id: device.id,
@@ -161,6 +164,7 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
161
164
  lease: outcome.refusal.lease
162
165
  });
163
166
  const taken = d.takeLease({
167
+ ...opts.slot ? { slot: opts.slot } : {},
164
168
  root,
165
169
  platform,
166
170
  id: device.id,
@@ -182,6 +186,7 @@ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
182
186
  });
183
187
  if (taken.status === "held") continue;
184
188
  const facts = {
189
+ ...taken.lease.slot ? { slot: taken.lease.slot } : {},
185
190
  platform,
186
191
  id: taken.lease.id,
187
192
  deviceName: taken.lease.deviceName,
@@ -224,7 +229,10 @@ async function runUnlock(platformArg, opts = {}, overrides = {}) {
224
229
  message: "Not in a React Native project (no package.json found).",
225
230
  remedy: "Run this from the app directory -- the one holding package.json."
226
231
  });
227
- const released = d.releaseLeases(root, { platform }, d.io);
232
+ const released = d.releaseLeases(root, {
233
+ platform,
234
+ ...opts.slot ? { slot: opts.slot } : {}
235
+ }, d.io);
228
236
  if (released.length === 0) d.note(chalk.dim(`No ${platform ? `${platform} ` : ""}device lease to release for ${root}. \`stim status\` lists every lease on this machine.`));
229
237
  if (json) d.out(JSON.stringify(released.map((lease) => ({
230
238
  ...lease,
@@ -235,10 +243,10 @@ async function runUnlock(platformArg, opts = {}, overrides = {}) {
235
243
  }
236
244
  function registerDevice(program, deps = {}) {
237
245
  const device = program.command("device").description("Lease a connected physical device to this workspace, and give it back");
238
- device.command("lock").argument("<platform>", "ios or android").argument("[id]", "the UDID or serial to lease; without one, the first free connected device is used").description("Lease a connected physical device to this workspace for a declared time, so another workspace's `--device` run waits instead of installing over it. Nothing but this command and a `--device` run moves the expiry.").option("--for <duration>", `How long to hold it: a whole number of seconds or minutes from 10s to 30m (default ${DEFAULT_FOR})`).option("--wait <seconds>", "How long to wait for a device another workspace holds, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once)").option("--json", "print the lease as JSON on stdout").action(async (platform, id, opts) => {
246
+ device.command("lock").argument("<platform>", "ios or android").argument("[id]", "the UDID or serial to lease; without one, the first free connected device is used").description("Lease a connected physical device to this workspace for a declared time, so another workspace's `--device` run waits instead of installing over it. Nothing but this command and a `--device` run moves the expiry.").option("--for <duration>", `How long to hold it: a whole number of seconds or minutes from 10s to 30m (default ${DEFAULT_FOR})`).option("--wait <seconds>", "How long to wait for a device another workspace holds, before refusing with STIM_DEVICE_BUSY (default 60, 0 refuses at once)").option("--slot <name>", "Use this workspace device slot", validateDeviceSlot).option("--json", "print the lease as JSON on stdout").action(async (platform, id, opts) => {
239
247
  if ("code" in await runLock(platform, id, opts, deps)) process.exit(1);
240
248
  });
241
- device.command("unlock").argument("[platform]", "ios or android; without one, every lease this workspace holds is released").description("Release the device lease or leases this workspace holds. Releasing nothing is not an error.").option("--json", "print the released leases as JSON on stdout").action(async (platform, opts) => {
249
+ device.command("unlock").argument("[platform]", "ios or android; without one, every lease this workspace holds is released").description("Release the device lease or leases this workspace holds. Releasing nothing is not an error.").option("--slot <name>", "Use this workspace device slot", validateDeviceSlot).option("--json", "print the released leases as JSON on stdout").action(async (platform, opts) => {
242
250
  const result = await runUnlock(platform, opts, deps);
243
251
  if (!Array.isArray(result)) process.exit(1);
244
252
  });
@@ -1,6 +1,6 @@
1
- import { k as withDirLock, u as getConfigDir } from "./config-D7uu8Gbq.mjs";
2
- import { _ as writeWorkspaceState, o as clearWorkspaceStateKeys, p as readWorkspaceState } from "./state-IYEY-zi8.mjs";
3
- import { a as segment } from "./build-lock-vE45ZzJZ.mjs";
1
+ import { I as deviceSlotKey, R as parseDeviceSlotKey, V as validateDeviceSlot, k as withDirLock, u as getConfigDir } from "./config-CcefMY2Q.mjs";
2
+ import { _ as writeWorkspaceState, m as withWorkspaceStateLock, o as clearWorkspaceStateKeys, p as readWorkspaceState } from "./state-B3XA2wp2.mjs";
3
+ import { H as segment } from "./android-BqVIJoG7.mjs";
4
4
  import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { join } from "node:path";
@@ -21,6 +21,7 @@ function deviceLeaseLockPath(platform, id) {
21
21
  return join(deviceLocksDir(), `${leaseStem(platform, id)}${LOCK_SUFFIX}`);
22
22
  }
23
23
  const fileLeaseIo = {
24
+ withHolderLock: withWorkspaceStateLock,
24
25
  now: () => Date.now(),
25
26
  readLease(path) {
26
27
  try {
@@ -83,8 +84,17 @@ function parseLease(raw) {
83
84
  const token = text(entry.token);
84
85
  const expiresAt = text(entry.expiresAt);
85
86
  if (!id || !holder || !token || !expiresAt || !Number.isFinite(Date.parse(expiresAt))) return null;
87
+ if (entry.slot !== void 0) {
88
+ if (typeof entry.slot !== "string") return null;
89
+ try {
90
+ validateDeviceSlot(entry.slot);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
86
95
  return {
87
96
  version: 1,
97
+ ...typeof entry.slot === "string" ? { slot: entry.slot } : {},
88
98
  platform: entry.platform,
89
99
  id,
90
100
  deviceName: text(entry.deviceName),
@@ -98,7 +108,7 @@ function parseWorkspaceLeases(value) {
98
108
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
99
109
  const leases = {};
100
110
  for (const [platform, entry] of Object.entries(value)) {
101
- if (!isPlatform(platform) || !entry || typeof entry !== "object") continue;
111
+ if (!parseDeviceSlotKey(platform) || !entry || typeof entry !== "object") continue;
102
112
  const record = entry;
103
113
  const id = text(record.id);
104
114
  const token = text(record.token);
@@ -145,70 +155,74 @@ function writeLease(lease, io) {
145
155
  io.writeLease(deviceLeasePath(lease.platform, lease.id), `${JSON.stringify(lease, null, 2)}\n`);
146
156
  return lease;
147
157
  }
148
- function takeLease({ root, platform, id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
149
- const previous = io.readHolder(root)[platform];
150
- if (previous && previous.id !== id) releaseHeld(root, platform, previous, io);
151
- for (const entry of listLeaseFiles(io)) {
152
- const other = entry.lease;
153
- if (!other || other.platform !== platform || other.holder !== root || other.id === id) continue;
154
- releaseByRoot(root, platform, other.id, io);
155
- }
156
- const declared = previous?.kind === "declared" && previous.id === id;
157
- const path = deviceLeasePath(platform, id);
158
- const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
159
- const now = io.now();
160
- const raw = io.readLease(path);
161
- const current = parseLease(raw);
162
- if (raw !== null && !current) return {
163
- status: "unreadable",
164
- path
165
- };
166
- const asked = now + durationMs;
167
- if (current && !leaseIsExpired(current, now)) {
168
- if (current.holder !== root) return {
169
- status: "held",
170
- lease: current
158
+ function takeLease({ root, platform, slot = "default", id, deviceName = null, kind, durationMs = DEFAULT_LEASE_MS }, io = fileLeaseIo) {
159
+ return withHolderLock(io, root, () => {
160
+ const key = deviceSlotKey(platform, slot);
161
+ const previous = io.readHolder(root)[key];
162
+ if (previous && previous.id !== id) releaseHeld(root, platform, previous, io, slot);
163
+ for (const entry of listLeaseFiles(io)) {
164
+ const other = entry.lease;
165
+ if (!other || other.platform !== platform || other.holder !== root || (other.slot ?? "default") !== slot || other.id === id) continue;
166
+ releaseByRoot(root, platform, other.id, io);
167
+ }
168
+ const declared = previous?.kind === "declared" && previous.id === id;
169
+ const path = deviceLeasePath(platform, id);
170
+ const result = io.withLeaseLock(deviceLeaseLockPath(platform, id), () => {
171
+ const now = io.now();
172
+ const raw = io.readLease(path);
173
+ const current = parseLease(raw);
174
+ if (raw !== null && !current) return {
175
+ status: "unreadable",
176
+ path
171
177
  };
172
- const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
178
+ const asked = now + durationMs;
179
+ if (current && !leaseIsExpired(current, now)) {
180
+ if (current.holder !== root || (current.slot ?? "default") !== slot) return {
181
+ status: "held",
182
+ lease: current
183
+ };
184
+ const floor = declared && kind === "run" ? Math.max(asked, Date.parse(current.expiresAt)) : asked;
185
+ return {
186
+ status: "set",
187
+ lease: writeLease({
188
+ ...current,
189
+ deviceName: deviceName ?? current.deviceName,
190
+ expiresAt: new Date(floor).toISOString()
191
+ }, io)
192
+ };
193
+ }
194
+ const expiresAt = new Date(asked).toISOString();
173
195
  return {
174
- status: "set",
196
+ status: "taken",
175
197
  lease: writeLease({
176
- ...current,
177
- deviceName: deviceName ?? current.deviceName,
178
- expiresAt: new Date(floor).toISOString()
198
+ version: 1,
199
+ ...slot === "default" ? {} : { slot },
200
+ platform,
201
+ id,
202
+ deviceName,
203
+ holder: root,
204
+ token: randomBytes(12).toString("hex"),
205
+ grantedAt: new Date(now).toISOString(),
206
+ expiresAt
179
207
  }, io)
180
208
  };
209
+ });
210
+ if (result.status === "taken" || result.status === "set") {
211
+ const recorded = result.status === "set" && declared ? "declared" : kind;
212
+ io.writeHolder(root, {
213
+ ...io.readHolder(root),
214
+ [key]: {
215
+ id,
216
+ token: result.lease.token,
217
+ kind: recorded
218
+ }
219
+ });
181
220
  }
182
- const expiresAt = new Date(asked).toISOString();
183
- return {
184
- status: "taken",
185
- lease: writeLease({
186
- version: 1,
187
- platform,
188
- id,
189
- deviceName,
190
- holder: root,
191
- token: randomBytes(12).toString("hex"),
192
- grantedAt: new Date(now).toISOString(),
193
- expiresAt
194
- }, io)
195
- };
221
+ return result;
196
222
  });
197
- if (result.status === "taken" || result.status === "set") {
198
- const recorded = result.status === "set" && declared ? "declared" : kind;
199
- io.writeHolder(root, {
200
- ...io.readHolder(root),
201
- [platform]: {
202
- id,
203
- token: result.lease.token,
204
- kind: recorded
205
- }
206
- });
207
- }
208
- return result;
209
223
  }
210
- function raiseLease({ root, platform, minMs }, io = fileLeaseIo) {
211
- const record = io.readHolder(root)[platform];
224
+ function raiseLease({ root, platform, slot = "default", minMs }, io = fileLeaseIo) {
225
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
212
226
  if (!record) return { status: "none" };
213
227
  const path = deviceLeasePath(platform, record.id);
214
228
  return io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
@@ -239,7 +253,8 @@ function summarize(lease) {
239
253
  expiresAt: lease.expiresAt
240
254
  };
241
255
  }
242
- function releaseHeld(root, platform, record, io) {
256
+ function releaseHeld(root, platform, record, io, slot = "default") {
257
+ const key = deviceSlotKey(platform, slot);
243
258
  const path = deviceLeasePath(platform, record.id);
244
259
  const released = io.withLeaseLock(deviceLeaseLockPath(platform, record.id), () => {
245
260
  const current = parseLease(io.readLease(path));
@@ -248,8 +263,8 @@ function releaseHeld(root, platform, record, io) {
248
263
  return current;
249
264
  });
250
265
  const held = io.readHolder(root);
251
- if (held[platform]?.token === record.token) {
252
- delete held[platform];
266
+ if (held[key]?.token === record.token) {
267
+ delete held[key];
253
268
  io.writeHolder(root, held);
254
269
  }
255
270
  return released;
@@ -263,27 +278,33 @@ function releaseByRoot(root, platform, id, io) {
263
278
  return current;
264
279
  });
265
280
  }
266
- function releaseRunLease({ root, platform }, io = fileLeaseIo) {
267
- const record = io.readHolder(root)[platform];
268
- if (!record || record.kind !== "run") return null;
269
- const released = releaseHeld(root, platform, record, io);
270
- return released ? summarize(released) : null;
281
+ function releaseRunLease({ root, platform, slot = "default" }, io = fileLeaseIo) {
282
+ return withHolderLock(io, root, () => {
283
+ const record = io.readHolder(root)[deviceSlotKey(platform, slot)];
284
+ if (!record || record.kind !== "run") return null;
285
+ const released = releaseHeld(root, platform, record, io, slot);
286
+ return released ? summarize(released) : null;
287
+ });
271
288
  }
272
- function releaseWorkspaceLeases(root, { platform = null } = {}, io = fileLeaseIo) {
273
- const released = [];
274
- for (const [name, record] of Object.entries(io.readHolder(root))) {
275
- if (!isPlatform(name) || platform && name !== platform) continue;
276
- const lease = releaseHeld(root, name, record, io);
277
- if (lease) released.push(summarize(lease));
278
- }
279
- for (const entry of listLeaseFiles(io)) {
280
- const lease = entry.lease;
281
- if (!lease || lease.holder !== root) continue;
282
- if (platform && lease.platform !== platform) continue;
283
- const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
284
- if (byRoot) released.push(summarize(byRoot));
285
- }
286
- return released;
289
+ function releaseWorkspaceLeases(root, { platform = null, slot = null } = {}, io = fileLeaseIo) {
290
+ return withHolderLock(io, root, () => {
291
+ const released = [];
292
+ for (const [name, record] of Object.entries(io.readHolder(root))) {
293
+ const parsed = parseDeviceSlotKey(name);
294
+ if (!parsed || platform && parsed.platform !== platform || slot !== null && parsed.slot !== slot) continue;
295
+ const lease = releaseHeld(root, parsed.platform, record, io, parsed.slot);
296
+ if (lease) released.push(summarize(lease));
297
+ }
298
+ for (const entry of listLeaseFiles(io)) {
299
+ const lease = entry.lease;
300
+ if (!lease || lease.holder !== root) continue;
301
+ if (platform && lease.platform !== platform) continue;
302
+ if (slot !== null && (lease.slot ?? "default") !== slot) continue;
303
+ const byRoot = releaseByRoot(root, lease.platform, lease.id, io);
304
+ if (byRoot) released.push(summarize(byRoot));
305
+ }
306
+ return released;
307
+ });
287
308
  }
288
309
  function removeExpiredLease(entry, io = fileLeaseIo) {
289
310
  const lease = entry.lease;
@@ -331,5 +352,8 @@ function selectPoolDevice({ candidates, leases, held = null, now }) {
331
352
  holders
332
353
  };
333
354
  }
355
+ function withHolderLock(io, root, fn) {
356
+ return io.withHolderLock ? io.withHolderLock(root, fn) : fn();
357
+ }
334
358
  //#endregion
335
359
  export { parseLease as a, releaseRunLease as c, selectPoolDevice as d, takeLease as f, listLeaseFiles as i, releaseWorkspaceLeases as l, fileLeaseIo as n, parseLeaseDuration as o, leaseIsExpired as r, raiseLease as s, deviceLeasePath as t, removeExpiredLease as u };