stim 1.29.0 → 1.30.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 (124) hide show
  1. package/dist/{acquire-artifact-C2DKfenN.mjs → acquire-artifact-CrZ4rEbI.mjs} +1 -1
  2. package/dist/{act-CrTcskiE.mjs → act-BaKjDWGw.mjs} +12 -12
  3. package/dist/{activity-BR5oHSO5.mjs → activity-BTaUf0tc.mjs} +2 -2
  4. package/dist/{agent-device-usage-output-W0x4SoC7.mjs → agent-device-usage-output-DYBpO17K.mjs} +3 -3
  5. package/dist/{android-BPmCmyAy.mjs → android-BCYLwmH6.mjs} +5 -5
  6. package/dist/{android-CLU_MMnb.mjs → android-BEQUDpOg.mjs} +1 -1
  7. package/dist/{android-l616dRP1.mjs → android-BWqdDK6R.mjs} +36 -34
  8. package/dist/{android-BHKgeOuq.mjs → android-DdKG6lyD.mjs} +39 -30
  9. package/dist/api.mjs +21 -21
  10. package/dist/{apk-signed-tail-B-wKoF_8.mjs → apk-signed-tail-B8kw4a8K.mjs} +1 -1
  11. package/dist/{artifact-Dm9ScLrh.mjs → artifact-DTHXcmQ3.mjs} +11 -11
  12. package/dist/{artifact-DHuFYfhD.mjs → artifact-M6W2l-xh.mjs} +11 -11
  13. package/dist/{auto-placement-C8sJsyMN.mjs → auto-placement-CZaWDmcY.mjs} +8 -8
  14. package/dist/{browser-web-_tQTbQUs.mjs → browser-web-JQ7lFej3.mjs} +5 -5
  15. package/dist/{build-DtCU9Kim.mjs → build-C2ZWoCmg.mjs} +18 -18
  16. package/dist/{build-cache-BGwTsIEa.mjs → build-cache-DlC3owap.mjs} +1 -1
  17. package/dist/{build-plan-CWyjMPKo.mjs → build-plan-J3iRR3gI.mjs} +5 -5
  18. package/dist/{ccache-DsDkdP72.mjs → ccache-B8gY50-h.mjs} +11 -11
  19. package/dist/{cli-3fjOg-Oc.mjs → cli-xRPXJPtA.mjs} +17 -17
  20. package/dist/cli.mjs +1 -1
  21. package/dist/{client-BTUH8TF1.mjs → client-DrHykA24.mjs} +3 -3
  22. package/dist/collector-run.mjs +1 -1
  23. package/dist/{command-output-DjCsmzLZ.mjs → command-output-BBW8eWPL.mjs} +1 -1
  24. package/dist/{dependencies-CHNqX6Pr.mjs → dependencies-vUh9mp5z.mjs} +31 -30
  25. package/dist/{dev-client-CROrxj1x.mjs → dev-client-CASgAOu7.mjs} +2 -2
  26. package/dist/device-ChnO7bR9.mjs +761 -0
  27. package/dist/device-CsIdRXfm.mjs +175 -0
  28. package/dist/{device-Bmm5qjM-.mjs → device-android-CcdQqQWg.mjs} +26 -193
  29. package/dist/{device-capacity-B2ChA-bz.mjs → device-capacity-C5C_jOQd.mjs} +9 -9
  30. package/dist/device-host-worker.mjs +40 -31
  31. package/dist/{device-ios-CYzivd31.mjs → device-ios-DbmtPIiw.mjs} +24 -14
  32. package/dist/{device-lease-run-qRq_2W2V.mjs → device-lease-run-Do6zYA7y.mjs} +3 -3
  33. package/dist/{device-pool-CrmiE4_x.mjs → device-pool-CpoYiCGO.mjs} +2 -2
  34. package/dist/{device-provision-CK2XiEp1.mjs → device-provision-B5Ms94qY.mjs} +6 -6
  35. package/dist/{doctor-Dr3C1iXV.mjs → doctor-CJSB5q-s.mjs} +14 -14
  36. package/dist/{doctor-Cp4-HN1f.mjs → doctor-CLE4AZEI.mjs} +12 -12
  37. package/dist/{doctor-BO_kaFWX.mjs → doctor-CpfbYh3Q.mjs} +1 -1
  38. package/dist/doctor-fingerprint.mjs +1 -1
  39. package/dist/{doctor-watchman-CJr2aXXv.mjs → doctor-watchman-D1IyMle9.mjs} +2 -2
  40. package/dist/{eas-build-D-9HmGZS.mjs → eas-build-DIDOHw2P.mjs} +2 -2
  41. package/dist/{eas-fallback-Cemt2GVL.mjs → eas-fallback-DVS9oBSc.mjs} +2 -2
  42. package/dist/{eas-session-ledger-B0KLZx5m.mjs → eas-session-ledger-CW3l-inB.mjs} +1 -1
  43. package/dist/{error-diagnostics-CV_1RoEH.mjs → error-diagnostics-XJ9eWdtq.mjs} +5 -5
  44. package/dist/{gc-DjXwQXHK.mjs → gc-BVIcxCxE.mjs} +2 -2
  45. package/dist/{gc-CKHqL4aS.mjs → gc-Co7PE36D.mjs} +13 -13
  46. package/dist/{guide-CeRlWVsS.mjs → guide-DkepBgHT.mjs} +110 -13
  47. package/dist/{hosted-android-DI_vvv7U.mjs → hosted-android-Du7NxufJ.mjs} +2 -2
  48. package/dist/{hosted-client-CgoxdU9j.mjs → hosted-client-Cect92SZ.mjs} +2 -2
  49. package/dist/{hosted-logs-C-4INmkn.mjs → hosted-logs-Opcjv1Lr.mjs} +3 -3
  50. package/dist/{hosted-logs-sync-Bf5s02fi.mjs → hosted-logs-sync-wsI7N1Hv.mjs} +6 -10
  51. package/dist/{hosted-macos-Doh85YUM.mjs → hosted-macos-BP9Kucat.mjs} +4 -4
  52. package/dist/{hosted-native-DvZENAL-.mjs → hosted-native-DmdLSmvu.mjs} +6 -5
  53. package/dist/{idle-shutdown-BM37gMfU.mjs → idle-shutdown-BcMN_cLm.mjs} +4 -4
  54. package/dist/{ios-CNDH6fNF.mjs → ios-BFkBHuRT.mjs} +8 -8
  55. package/dist/{ios-UM_dManb.mjs → ios-CG0IsLtt.mjs} +24 -24
  56. package/dist/{ios-CoMCWfub.mjs → ios-CXnoVO_z.mjs} +2 -2
  57. package/dist/{ios-device-L1-pota_.mjs → ios-device-ClSfuu-_.mjs} +1 -1
  58. package/dist/{ios-project-DJYptOCh.mjs → ios-project-BM9TbTE7.mjs} +5 -5
  59. package/dist/{js-swap-BthE4kZC.mjs → js-swap-9Rx-5Nqz.mjs} +3 -3
  60. package/dist/{launch-D798aSm8.mjs → launch-DvJV1Mx4.mjs} +14 -14
  61. package/dist/{lightened-vSxkstg6.mjs → lightened-23pqe7LS.mjs} +2 -2
  62. package/dist/{logs-BND8MiBq.mjs → logs-DJgZlf2K.mjs} +5 -5
  63. package/dist/{logs-CilPftm3.mjs → logs-Pkeh3wZk.mjs} +5 -4
  64. package/dist/{macos-CxUi9xcE.mjs → macos-DPvPQadc.mjs} +15 -15
  65. package/dist/{macos-BOQrKYeJ.mjs → macos-Du07NsOB.mjs} +1 -1
  66. package/dist/maintenance-run.mjs +4 -4
  67. package/dist/{native-android-BkhrCjdX.mjs → native-android-CBOM1_gB.mjs} +12 -12
  68. package/dist/{native-gradle-artifact-inputs-Cw4P6Gyr.mjs → native-gradle-artifact-inputs-D_iAfPuS.mjs} +3 -3
  69. package/dist/{native-runtime-DyH2gkXK.mjs → native-runtime-D4cTwU0Z.mjs} +3 -3
  70. package/dist/{native-xcode-inputs-DV8jr3kJ.mjs → native-xcode-inputs-BF-ogSnG.mjs} +1 -1
  71. package/dist/{native-xcode-ios-DdpPG2iT.mjs → native-xcode-ios-DwJBfMP6.mjs} +12 -12
  72. package/dist/offload-worker.mjs +15 -15
  73. package/dist/{phone-reverses-DZRad8HT.mjs → phone-reverses-CvhhkGMH.mjs} +2 -2
  74. package/dist/{placement-log-CoN6xrjY.mjs → placement-log-CRR_TE1r.mjs} +1 -1
  75. package/dist/{plan-placement-7sHuqR-r.mjs → plan-placement-CdLc-pTb.mjs} +4 -4
  76. package/dist/{ports-VQSAPZhl.mjs → ports-g4N_cfyg.mjs} +1 -1
  77. package/dist/{prebuild-Bqp0LFGN.mjs → prebuild-DBJMhwSS.mjs} +6 -6
  78. package/dist/{project-D1n0K4yg.mjs → project-DPQjnpcI.mjs} +11 -11
  79. package/dist/{react-native-android-FQ-yX3Mv.mjs → react-native-android-idMWOwkX.mjs} +21 -21
  80. package/dist/{react-native-build-D58bIqV9.mjs → react-native-build-BSoh4HkH.mjs} +5 -5
  81. package/dist/{react-native-doctor-AkUH2aca.mjs → react-native-doctor-CYibY8-4.mjs} +8 -7
  82. package/dist/{react-native-ios-Dmsv8j7T.mjs → react-native-ios-13KIIOOy.mjs} +15 -14
  83. package/dist/{reload-CDKEzzaf.mjs → reload-B9DZOjcV.mjs} +1 -1
  84. package/dist/{reload-BV0nD-k9.mjs → reload-BUQI3dSu.mjs} +8 -8
  85. package/dist/{remote-C8xyslUa.mjs → remote-C-RGHhAP.mjs} +6 -6
  86. package/dist/{result-9fqcJ90W.mjs → result-W4HCum4A.mjs} +11 -11
  87. package/dist/{selection-D1ZDzHFX.mjs → selection-PCMT97XD.mjs} +1 -1
  88. package/dist/{server-command-RlS-Kl4a.mjs → server-command-oqi3TDy7.mjs} +2 -2
  89. package/dist/server-expo-BUFAq2In.mjs +2 -0
  90. package/dist/{server-expo-bB1VV2k-.mjs → server-expo-C1K07shx.mjs} +1 -1
  91. package/dist/{settings-BcVCcAT5.mjs → settings-CKlMOmhj.mjs} +2 -2
  92. package/dist/{settings-payload-_aatELz2.mjs → settings-payload-CE-NZ6Lq.mjs} +1 -1
  93. package/dist/{settings-write-CkTVGQ4O.mjs → settings-write-D7Xwa_-M.mjs} +2 -2
  94. package/dist/{slot-launch-Dbgfh8gc.mjs → slot-launch-CeIK1iUI.mjs} +4 -4
  95. package/dist/{stage-BuHuRj1y.mjs → stage-Blqe6bWb.mjs} +1 -1
  96. package/dist/{start-Dlv4XXTP.mjs → start-B7J5a9nS.mjs} +5 -5
  97. package/dist/{start-Cr-c3Xhq.mjs → start-CvP2pJjc.mjs} +1 -1
  98. package/dist/{stats-D5c-uIBp.mjs → stats-Uo7l8OAk.mjs} +3 -3
  99. package/dist/{status-DbQv--Yb.mjs → status-CqJI1TgG.mjs} +1 -1
  100. package/dist/{status-D6PBK6_-.mjs → status-_rWXFSO7.mjs} +15 -15
  101. package/dist/{status-watch-okrpea4i.mjs → status-watch-CnSyXUmj.mjs} +3 -3
  102. package/dist/status-worker.mjs +1 -1
  103. package/dist/{stop-CHtAiv79.mjs → stop-Bogo9oO2.mjs} +2 -2
  104. package/dist/{stop-uRQ-zYcl.mjs → stop-CMVfaA-6.mjs} +2 -2
  105. package/dist/{stop-p9uSKuGE.mjs → stop-CnF9ixYe.mjs} +10 -10
  106. package/dist/supervisor-run.mjs +9 -9
  107. package/dist/{support-PFyTkrnr.mjs → support-BGqpYMV8.mjs} +15 -11
  108. package/dist/{support-BMpQIug0.mjs → support-DUevTPur.mjs} +7 -7
  109. package/dist/{swiftpm-artifact-inputs-Cddq6jN2.mjs → swiftpm-artifact-inputs-BcJk6-3u.mjs} +1 -1
  110. package/dist/{swiftpm-macos-BSj9iZlI.mjs → swiftpm-macos-DBrmoocG.mjs} +4 -4
  111. package/dist/toolchain-Cw9VVHOY.mjs +2 -0
  112. package/dist/{toolchain-CLGnEUgT.mjs → toolchain-DLdoyxIt.mjs} +18 -10
  113. package/dist/{warm-progress--KkkBBaq.mjs → warm-progress-DQ2wQZKp.mjs} +1 -1
  114. package/dist/web-CI_qCRPw.mjs +2 -0
  115. package/dist/{web-xlVssjQf.mjs → web-g9U7L1sR.mjs} +3 -3
  116. package/dist/web-run.mjs +2 -2
  117. package/dist/{workspaces-CvSYD_78.mjs → workspaces-7_RWoi6c.mjs} +4 -4
  118. package/dist/{worktree-DTOIohFe.mjs → worktree-DEet9H3p.mjs} +1 -1
  119. package/dist/{worktree-CaQBf6kh.mjs → worktree-qKJ3x07S.mjs} +13 -13
  120. package/package.json +7 -7
  121. package/dist/device-BvpasThQ.mjs +0 -297
  122. package/dist/server-expo-BVV9qy31.mjs +0 -2
  123. package/dist/toolchain-B-mzR9Rh.mjs +0 -2
  124. package/dist/web-BxSo493O.mjs +0 -2
@@ -0,0 +1,761 @@
1
+ import { t as getExecutor } from "./exec-aULwtNly.mjs";
2
+ import { r as readWorkspaceState$1 } from "./workspace-state-DGwkPXwq.mjs";
3
+ import { G as projectDeviceSlots, H as deviceSlotPlatforms, V as deviceSlotKey, W as parseDeviceSlotOption, p as getProject } from "./config-AbUjIBOQ.mjs";
4
+ import { r as clockTime } from "./command-output-BBW8eWPL.mjs";
5
+ import { I as readWorkspaceLaunches, P as readRemoteSession, i as findProjectRoot$1, t as NO_PROJECT_REFUSAL } from "./project-DPQjnpcI.mjs";
6
+ import { M as listAdbDevicesAsync, T as emulatorProbeSession, Y as physicalDeviceModel, Z as probeEmulatorSerial, c as androidPoolNoCandidatesRefusal, d as androidToolPath, k as getAvdNameForSerialAsync, nt as resolveOwnedAvdSerialAsync, rt as resolvePhysicalDevice, s as androidPoolCandidates, u as androidToolCwd } from "./android-DdKG6lyD.mjs";
7
+ import { En as runProcessOperation } from "./ccache-B8gY50-h.mjs";
8
+ import { C as parseLeaseDuration, D as releaseWorkspaceLeases, j as takeLease, y as fileLeaseIo } from "./activity-BTaUf0tc.mjs";
9
+ import { D as resolveOwnedIosSim, f as listAllIosSimsAsync } from "./ios-CXnoVO_z.mjs";
10
+ import { l as parseDeviceWait, p as waitForDevice } from "./device-lease-run-Do6zYA7y.mjs";
11
+ import { n as runProvision } from "./device-provision-B5Ms94qY.mjs";
12
+ import { a as iosPoolCandidates, c as isWirelessIosDevice, f as resolveIosPhysicalDevice, l as listIosDevices, o as iosPoolNoCandidatesRefusal } from "./ios-device-ClSfuu-_.mjs";
13
+ import { t as selectFromPool } from "./device-pool-CpoYiCGO.mjs";
14
+ import { hostedNativeRecords } from "@stim-cli/core/state";
15
+ import chalk from "chalk";
16
+ //#region src/commands/device-tools.ts
17
+ const IOS_SERVICES = [
18
+ "all",
19
+ "calendar",
20
+ "contacts",
21
+ "contacts-limited",
22
+ "location",
23
+ "location-always",
24
+ "media-library",
25
+ "microphone",
26
+ "motion",
27
+ "photos",
28
+ "photos-add",
29
+ "reminders",
30
+ "siri"
31
+ ];
32
+ const ANDROID_SERVICES = {
33
+ calendar: ["android.permission.READ_CALENDAR", "android.permission.WRITE_CALENDAR"],
34
+ camera: ["android.permission.CAMERA"],
35
+ contacts: ["android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS"],
36
+ location: ["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"],
37
+ microphone: ["android.permission.RECORD_AUDIO"],
38
+ motion: ["android.permission.ACTIVITY_RECOGNITION"],
39
+ notifications: ["android.permission.POST_NOTIFICATIONS"],
40
+ photos: ["android.permission.READ_MEDIA_IMAGES", "android.permission.READ_EXTERNAL_STORAGE"]
41
+ };
42
+ const SIMCTL_OPTIONS = {
43
+ timeoutMs: 3e4,
44
+ killSignal: "SIGKILL"
45
+ };
46
+ const ADB_OPTIONS = {
47
+ timeoutMs: 15e3,
48
+ killSignal: "SIGKILL"
49
+ };
50
+ const NETWORK_READS = 5;
51
+ const NETWORK_READ_INTERVAL_MS = 400;
52
+ const TARGET = "STIM_DEVICE_TOOL_TARGET";
53
+ const UNSUPPORTED = "STIM_DEVICE_TOOL_UNSUPPORTED";
54
+ const FAILED = "STIM_DEVICE_TOOL_FAILED";
55
+ const DEFAULT_DEPS$1 = {
56
+ findProjectRoot: findProjectRoot$1,
57
+ getProject,
58
+ hostedSlots: (root, platform) => Object.keys(hostedNativeRecords(readWorkspaceState$1(root), platform)),
59
+ readRemotePlatform: (root) => readRemoteSession(root),
60
+ readLaunch: (root, platform, slot) => readWorkspaceLaunches(root)[deviceSlotKey(platform, slot)] ?? null,
61
+ resolveIos: async (udid) => {
62
+ const resolved = resolveOwnedIosSim(udid, await listAllIosSimsAsync({}));
63
+ if (resolved.sim) return {
64
+ name: resolved.sim.name,
65
+ booted: resolved.sim.state === "Booted"
66
+ };
67
+ return resolved.notOwned === void 0 ? "missing" : "notOwned";
68
+ },
69
+ resolveAndroid: async (avdName, preferredSerial) => (await resolveOwnedAvdSerialAsync(avdName, { preferredSerial })).serial ?? null,
70
+ run: (file, args, options) => getExecutor().runFileAsync(file, args, options),
71
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
72
+ out: (line) => console.log(line),
73
+ note: (line) => console.error(line)
74
+ };
75
+ function failure(code, message, remedy) {
76
+ return {
77
+ code,
78
+ message,
79
+ remedy
80
+ };
81
+ }
82
+ function badArg(message, remedy) {
83
+ return failure("STIM_BAD_ARG", message, remedy);
84
+ }
85
+ function readPlatform$1(value) {
86
+ return value === "ios" || value === "android" ? value : null;
87
+ }
88
+ const COORDINATE = /^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/;
89
+ function parseCoordinate(value) {
90
+ const match = COORDINATE.exec(value);
91
+ if (!match) return null;
92
+ const latitude = Number(match[1]);
93
+ const longitude = Number(match[2]);
94
+ return Math.abs(latitude) <= 90 && Math.abs(longitude) <= 180 ? {
95
+ latitude,
96
+ longitude
97
+ } : null;
98
+ }
99
+ function parseLocationRequest(platformArg, opts) {
100
+ const platform = readPlatform$1(platformArg);
101
+ if (!platform) return badArg(`\`stim device location\` takes ios or android, not ${JSON.stringify(platformArg)}.`, "Run `stim device location ios --set 45.5,-73.6` or `stim device location android --set 45.5,-73.6`.");
102
+ if (opts.set === void 0 === !opts.clear) return badArg("Pass exactly one of --set <lat,lng> or --clear.", "Run `stim device location <ios|android> --set 45.5,-73.6`, or `--clear` on iOS.");
103
+ if (opts.clear) return platform === "android" ? failure(UNSUPPORTED, "The Android emulator has no way to clear a simulated location: `adb emu geo fix` only sets one.", "Set another coordinate with `stim device location android --set <lat,lng>`, or restart the emulator with `stim stop` and `stim android`.") : {
104
+ tool: "location",
105
+ platform,
106
+ clear: true
107
+ };
108
+ const coordinate = parseCoordinate(opts.set ?? "");
109
+ if (!coordinate) return badArg(`${JSON.stringify(opts.set)} is not a coordinate.`, "Pass latitude and longitude in degrees as one value, such as --set 45.5,-73.6 (latitude within 90, longitude within 180).");
110
+ return {
111
+ tool: "location",
112
+ platform,
113
+ ...coordinate
114
+ };
115
+ }
116
+ function parsePermissionRequest(platformArg, actionArg, serviceArg) {
117
+ const platform = readPlatform$1(platformArg);
118
+ if (!platform) return badArg(`\`stim device permission\` takes ios or android, not ${JSON.stringify(platformArg)}.`, "Run `stim device permission ios grant photos` or `stim device permission android grant camera`.");
119
+ if (actionArg !== "grant" && actionArg !== "revoke" && actionArg !== "reset") return badArg(`${JSON.stringify(actionArg)} is not a permission action.`, "Use grant, revoke or reset: `stim device permission <ios|android> <grant|revoke|reset> <service>`.");
120
+ const services = platform === "ios" ? IOS_SERVICES : Object.keys(ANDROID_SERVICES);
121
+ if (!services.includes(serviceArg)) return badArg(`${JSON.stringify(serviceArg)} is not a ${platform} permission service.`, `The ${platform} services are ${services.join(", ")}.`);
122
+ return {
123
+ tool: "permission",
124
+ platform,
125
+ action: actionArg,
126
+ service: serviceArg
127
+ };
128
+ }
129
+ function parsePushRequest(platformArg, opts) {
130
+ const platform = readPlatform$1(platformArg);
131
+ if (!platform) return badArg(`\`stim device push\` takes ios, not ${JSON.stringify(platformArg)}.`, "Run `stim device push ios --payload '{\"aps\":{\"alert\":\"Hello\"}}'`.");
132
+ if (platform === "android") return failure(UNSUPPORTED, "Android has no simulated push: the emulator does not deliver notifications from the host.", "Send the notification through the app's own push provider, or use `adb shell cmd notification post` for a local one.");
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(opts.payload ?? "");
136
+ } catch {
137
+ parsed = null;
138
+ }
139
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || !("aps" in parsed)) return badArg("--payload must be a JSON object with an \"aps\" key.", "Pass something like --payload '{\"aps\":{\"alert\":{\"title\":\"Hello\",\"body\":\"World\"}}}'. simctl accepts at most 4096 bytes.");
140
+ return {
141
+ tool: "push",
142
+ platform,
143
+ payload: JSON.stringify(parsed)
144
+ };
145
+ }
146
+ function parseNetworkRequest(platformArg, stateArg) {
147
+ const platform = readPlatform$1(platformArg);
148
+ if (!platform) return badArg(`\`stim device network\` takes android, not ${JSON.stringify(platformArg)}.`, "Run `stim device network android off`.");
149
+ if (platform === "ios") return failure(UNSUPPORTED, "The iOS simulator shares the Mac's network; `simctl` has no switch to turn it off.", "Disconnect the Mac, or use Network Link Conditioner on the Mac.");
150
+ if (stateArg !== "on" && stateArg !== "off") return badArg(`${JSON.stringify(stateArg)} is not on or off.`, "Run `stim device network android on` or `off`.");
151
+ return {
152
+ tool: "network",
153
+ platform,
154
+ enabled: stateArg === "on"
155
+ };
156
+ }
157
+ function describeError(error) {
158
+ const stderr = error instanceof Error && "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim() : "";
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ return stderr && !message.includes(stderr) ? `${message} ${stderr}` : message;
161
+ }
162
+ async function resolveToolTarget(platform, slot, root, d) {
163
+ const slotText = slot === "default" ? "" : ` --slot ${slot}`;
164
+ const runCommand = `stim ${platform}${slotText}`;
165
+ const project = d.getProject(root);
166
+ if (!project) return failure("STIM_NO_PROJECT", `No Stim environment is registered for ${root}.`, "Run `stim start` first.");
167
+ if (d.hostedSlots(root, platform).includes(slot)) return failure(TARGET, `This workspace's ${platform} device${slotText ? ` in slot ${slot}` : ""} is hosted on another Mac, and device tools act only on the workspace's own local simulator or emulator.`, `Run \`${runCommand} --remote local\` to use a local device.`);
168
+ const remote = d.readRemotePlatform(root);
169
+ if (remote && (remote.platform === null || remote.platform === platform)) return failure(TARGET, `This workspace's remote device is EAS Simulator session ${remote.sessionId}, and device tools act only on the workspace's own local simulator or emulator.`, `Run \`${runCommand} --remote local\` to use a local device.`);
170
+ const records = deviceSlotPlatforms(project, slot);
171
+ const launch = d.readLaunch(root, platform, slot);
172
+ if (platform === "ios") {
173
+ const udid = records?.ios?.owned ? records.ios.deviceUdid : void 0;
174
+ if (!udid) return noOwnedDevice(platform, slot, runCommand);
175
+ const sim = await d.resolveIos(udid);
176
+ if (sim === "missing" || sim === "notOwned") return failure(TARGET, `The recorded iOS simulator ${udid} is ${sim === "missing" ? "gone" : "not owned by this Stim home"}.`, `Run \`${runCommand}\` to boot, install and launch this workspace's owned simulator.`);
177
+ if (!sim.booted) return failure(TARGET, `The owned iOS simulator ${sim.name} is not booted.`, `Run \`${runCommand}\` to boot it.`);
178
+ const appId = launch?.deviceId === udid ? launch.appId : project.bundleId ?? null;
179
+ return {
180
+ platform,
181
+ slot,
182
+ deviceId: udid,
183
+ deviceName: sim.name,
184
+ appId
185
+ };
186
+ }
187
+ const avdName = records?.android?.owned ? records.android.avdName : void 0;
188
+ if (!avdName) return noOwnedDevice(platform, slot, runCommand);
189
+ const serial = await d.resolveAndroid(avdName, launch?.deviceId ?? records?.android?.serial);
190
+ if (!serial) return failure(TARGET, `The owned Android emulator ${avdName} is not running.`, `Run \`${runCommand}\` to boot it.`);
191
+ return {
192
+ platform,
193
+ slot,
194
+ deviceId: serial,
195
+ deviceName: avdName,
196
+ appId: launch?.deviceId === serial ? launch.appId : project.androidPackage ?? null
197
+ };
198
+ }
199
+ function noOwnedDevice(platform, slot, runCommand) {
200
+ return failure(TARGET, `This workspace has no owned ${platform} ${platform === "ios" ? "simulator" : "emulator"}${slot === "default" ? "" : ` in slot ${slot}`}. Device tools do not act on physical devices or devices Stim does not own.`, `Run \`${runCommand}\` to create and boot one.`);
201
+ }
202
+ /** Reads each permission's grant state out of `dumpsys package <id>`; a permission the dump does not list is omitted. */
203
+ function parsePackagePermissions(dump, names) {
204
+ const states = [];
205
+ for (const name of names) {
206
+ const match = new RegExp(`^\\s*${name.replaceAll(".", "\\.")}: granted=(true|false)(?:, flags=\\[([^\\]]*)\\])?`, "m").exec(dump);
207
+ if (match) states.push({
208
+ name,
209
+ granted: match[1] === "true",
210
+ flags: (match[2] ?? "").split("|").map((flag) => flag.trim())
211
+ });
212
+ }
213
+ return states;
214
+ }
215
+ function hasUserFlag(state) {
216
+ return state.flags.some((flag) => /^USER_(SET|FIXED)$/.test(flag));
217
+ }
218
+ function parseSettingFlag(output) {
219
+ const value = output.trim();
220
+ return value === "1" ? true : value === "0" ? false : null;
221
+ }
222
+ function noApp(target) {
223
+ return failure(TARGET, `No app is recorded for this workspace's ${target.platform} device.`, `Run \`stim ${target.platform}${target.slot === "default" ? "" : ` --slot ${target.slot}`}\` to install and launch the app first.`);
224
+ }
225
+ async function runIos(request, target, d) {
226
+ const simctl = (verb, args, input) => d.run("xcrun", [
227
+ "simctl",
228
+ verb,
229
+ target.deviceId,
230
+ ...args
231
+ ], {
232
+ ...SIMCTL_OPTIONS,
233
+ ...input === void 0 ? {} : { input }
234
+ });
235
+ const base = {
236
+ platform: target.platform,
237
+ slot: target.slot,
238
+ deviceId: target.deviceId,
239
+ deviceName: target.deviceName
240
+ };
241
+ if (request.tool === "location") {
242
+ await simctl("location", "clear" in request ? ["clear"] : ["set", `${request.latitude},${request.longitude}`]);
243
+ return {
244
+ ...base,
245
+ tool: "location",
246
+ ..."clear" in request ? { action: "clear" } : {
247
+ action: "set",
248
+ latitude: request.latitude,
249
+ longitude: request.longitude
250
+ },
251
+ readBack: null
252
+ };
253
+ }
254
+ if (request.tool === "permission") {
255
+ if (!target.appId) return noApp(target);
256
+ await simctl("privacy", [
257
+ request.action,
258
+ request.service,
259
+ target.appId
260
+ ]);
261
+ return {
262
+ ...base,
263
+ tool: "permission",
264
+ action: request.action,
265
+ service: request.service,
266
+ appId: target.appId,
267
+ readBack: null
268
+ };
269
+ }
270
+ if (request.tool === "push") {
271
+ if (!target.appId) return noApp(target);
272
+ await simctl("push", [target.appId, "-"], request.payload);
273
+ return {
274
+ ...base,
275
+ tool: "push",
276
+ appId: target.appId,
277
+ readBack: null
278
+ };
279
+ }
280
+ return failure(UNSUPPORTED, `iOS has no ${request.tool} tool.`, null);
281
+ }
282
+ async function runAndroid(request, target, d) {
283
+ const adb = (args) => d.run(androidToolPath("adb"), [
284
+ "-s",
285
+ target.deviceId,
286
+ ...args
287
+ ], {
288
+ ...ADB_OPTIONS,
289
+ cwd: androidToolCwd()
290
+ });
291
+ const base = {
292
+ platform: target.platform,
293
+ slot: target.slot,
294
+ deviceId: target.deviceId,
295
+ deviceName: target.deviceName
296
+ };
297
+ if (request.tool === "location" && !("clear" in request)) {
298
+ const reply = await adb([
299
+ "emu",
300
+ "geo",
301
+ "fix",
302
+ String(request.longitude),
303
+ String(request.latitude)
304
+ ]);
305
+ if (/^KO\b/m.test(reply)) return failure(FAILED, `The emulator console refused the location: ${reply.trim()}`, null);
306
+ return {
307
+ ...base,
308
+ tool: "location",
309
+ action: "set",
310
+ latitude: request.latitude,
311
+ longitude: request.longitude,
312
+ readBack: null
313
+ };
314
+ }
315
+ if (request.tool === "permission") {
316
+ const appId = target.appId;
317
+ if (!appId) return noApp(target);
318
+ const requested = ANDROID_SERVICES[request.service] ?? [];
319
+ const pm = (args) => adb([
320
+ "shell",
321
+ "pm",
322
+ ...args
323
+ ]).catch(() => "");
324
+ for (const name of requested) if (request.action === "grant") await pm([
325
+ "grant",
326
+ appId,
327
+ name
328
+ ]);
329
+ else {
330
+ await pm([
331
+ "revoke",
332
+ appId,
333
+ name
334
+ ]);
335
+ if (request.action === "reset") await pm([
336
+ "clear-permission-flags",
337
+ appId,
338
+ name,
339
+ "user-set",
340
+ "user-fixed"
341
+ ]);
342
+ }
343
+ const states = parsePackagePermissions(await adb([
344
+ "shell",
345
+ "dumpsys",
346
+ "package",
347
+ appId
348
+ ]), requested);
349
+ const permissions = states.map(({ name, granted }) => ({
350
+ name,
351
+ granted
352
+ }));
353
+ if (permissions.length === 0) return failure(FAILED, `${appId} declares none of the ${request.service} permissions (${requested.join(", ")}).`, `Add one to the app's manifest and rebuild with \`stim android\`; Stim cannot ${request.action} a permission the app does not declare.`);
354
+ const wanted = request.action === "grant";
355
+ if (!(wanted ? states.some((state) => state.granted) : states.every((state) => !state.granted && (request.action !== "reset" || !hasUserFlag(state))))) return failure(FAILED, `${target.deviceName} did not report ${request.service} as ${wanted ? "granted" : request.action === "reset" ? "reset" : "revoked"} for ${appId} after the change: ${JSON.stringify(states)}`, `Run \`adb shell dumpsys package ${appId}\` to read the device state.`);
356
+ const skipped = requested.filter((name) => !permissions.some((permission) => permission.name === name));
357
+ return {
358
+ ...base,
359
+ tool: "permission",
360
+ action: request.action,
361
+ service: request.service,
362
+ appId,
363
+ readBack: { permissions },
364
+ ...skipped.length > 0 ? { skipped } : {}
365
+ };
366
+ }
367
+ if (request.tool === "network") {
368
+ const verb = request.enabled ? "enable" : "disable";
369
+ await adb([
370
+ "shell",
371
+ "svc",
372
+ "wifi",
373
+ verb
374
+ ]);
375
+ await adb([
376
+ "shell",
377
+ "svc",
378
+ "data",
379
+ verb
380
+ ]);
381
+ let wifi = null;
382
+ let data = null;
383
+ for (let attempt = 0; attempt < NETWORK_READS; attempt++) {
384
+ if (attempt > 0) await d.sleep(NETWORK_READ_INTERVAL_MS);
385
+ wifi = parseSettingFlag(await adb([
386
+ "shell",
387
+ "settings",
388
+ "get",
389
+ "global",
390
+ "wifi_on"
391
+ ]));
392
+ data = parseSettingFlag(await adb([
393
+ "shell",
394
+ "settings",
395
+ "get",
396
+ "global",
397
+ "mobile_data"
398
+ ]));
399
+ if (wifi === request.enabled && data === request.enabled) break;
400
+ }
401
+ if (wifi !== request.enabled || data !== request.enabled) return failure(FAILED, `${target.deviceName} reports Wi-Fi ${wifi === null ? "unknown" : wifi ? "on" : "off"} and mobile data ${data === null ? "unknown" : data ? "on" : "off"} after asking for ${request.enabled ? "on" : "off"}.`, "Run `adb shell settings get global wifi_on` and `mobile_data` to read the device state.");
402
+ return {
403
+ ...base,
404
+ tool: "network",
405
+ enabled: request.enabled,
406
+ readBack: {
407
+ wifi,
408
+ data
409
+ }
410
+ };
411
+ }
412
+ return failure(UNSUPPORTED, `Android has no ${request.tool} tool.`, null);
413
+ }
414
+ function summary(result) {
415
+ const device = `${result.deviceName} (${result.deviceId})`;
416
+ switch (result.tool) {
417
+ case "location": return result.action === "clear" ? `cleared the simulated location on ${device}.` : `set the simulated location on ${device} to ${result.latitude},${result.longitude}.`;
418
+ case "permission": return `${result.action} ${result.service} for ${result.appId} on ${device}.`;
419
+ case "push": return `sent a push to ${result.appId} on ${device}.`;
420
+ case "network": return `turned the network ${result.enabled ? "on" : "off"} on ${device}.`;
421
+ }
422
+ }
423
+ async function runDeviceTool(request, opts = {}, overrides = {}) {
424
+ const d = {
425
+ ...DEFAULT_DEPS$1,
426
+ ...overrides
427
+ };
428
+ const report = (problem) => {
429
+ d.note(chalk.red(`${problem.code}: ${problem.message}`));
430
+ if (problem.remedy) d.note(chalk.dim(problem.remedy));
431
+ if (opts.json) d.out(JSON.stringify(problem));
432
+ return problem;
433
+ };
434
+ if ("code" in request) return report(request);
435
+ const root = d.findProjectRoot(process.cwd());
436
+ if (!root) return report(NO_PROJECT_REFUSAL);
437
+ let target;
438
+ try {
439
+ target = await resolveToolTarget(request.platform, opts.slot ?? "default", root, d);
440
+ } catch (error) {
441
+ return report(failure(FAILED, `Stim could not inspect this workspace's ${request.platform} device: ${describeError(error)}`, "Run `stim status` and retry when simctl or adb responds."));
442
+ }
443
+ if ("code" in target) return report(target);
444
+ let result;
445
+ try {
446
+ result = request.platform === "ios" ? await runIos(request, target, d) : await runAndroid(request, target, d);
447
+ } catch (error) {
448
+ const reason = describeError(error);
449
+ result = failure(FAILED, `${request.platform === "ios" ? "simctl" : "adb"} did not complete the ${request.tool} change on ${target.deviceName}: ${reason}`, request.tool === "push" && /not authorized/i.test(reason) ? "The app has no notification permission on this simulator, and `simctl privacy` cannot grant it. Open the app, accept its notification prompt, and send the push again." : `Check that ${target.deviceName} is responsive with \`stim status\`, then retry.`);
450
+ }
451
+ if ("code" in result) return report(result);
452
+ d.out(opts.json ? JSON.stringify(result) : chalk.green(summary(result)));
453
+ return result;
454
+ }
455
+ const finish = (result) => {
456
+ if ("code" in result) process.exit(1);
457
+ };
458
+ function registerDeviceTools(device, deps = {}) {
459
+ device.command("location").argument("<platform>", "ios or android").description("Set or clear the simulated location of this workspace's own simulator or emulator. Android cannot clear one.").option("--set <lat,lng>", "latitude and longitude in degrees, as one value such as 45.5,-73.6").option("--clear", "stop simulating a location (iOS only)").option("--slot <name>", "Use this workspace device slot", parseDeviceSlotOption).option("--json", "print the result as JSON on stdout").action(async (platform, opts) => finish(await runDeviceTool(parseLocationRequest(platform, opts), opts, deps)));
460
+ device.command("permission").argument("<platform>", "ios or android").argument("<action>", "grant, revoke or reset").argument("<service>", "the permission service, such as camera, photos or location").description("Grant, revoke or reset one privacy permission for the app on this workspace's own simulator or emulator. Android reads the grant state back; iOS has no read.").option("--slot <name>", "Use this workspace device slot", parseDeviceSlotOption).option("--json", "print the result as JSON on stdout").action(async (platform, action, service, opts) => finish(await runDeviceTool(parsePermissionRequest(platform, action, service), opts, deps)));
461
+ device.command("push").argument("<platform>", "ios").description("Send a test push notification to the app on this workspace's own iOS simulator.").requiredOption("--payload <json>", "the APNs payload: a JSON object with an \"aps\" key, at most 4096 bytes").option("--slot <name>", "Use this workspace device slot", parseDeviceSlotOption).option("--json", "print the result as JSON on stdout").action(async (platform, opts) => finish(await runDeviceTool(parsePushRequest(platform, opts), opts, deps)));
462
+ device.command("network").argument("<platform>", "android").argument("<state>", "on or off").description("Turn Wi-Fi and mobile data on or off on this workspace's own Android emulator, and read them back.").option("--slot <name>", "Use this workspace device slot", parseDeviceSlotOption).option("--json", "print the result as JSON on stdout").action(async (platform, state, opts) => finish(await runDeviceTool(parseNetworkRequest(platform, state), opts, deps)));
463
+ }
464
+ //#endregion
465
+ //#region src/commands/device.ts
466
+ const DEFAULT_FOR = "5m";
467
+ const DEFAULT_DEPS = {
468
+ findProjectRoot: findProjectRoot$1,
469
+ getProject,
470
+ avdNameOf: getAvdNameForSerialAsync,
471
+ listIosDevices,
472
+ listAdbDevices: listAdbDevicesAsync,
473
+ physicalDeviceModel,
474
+ probeEmulatorSerial,
475
+ takeLease,
476
+ releaseLeases: releaseWorkspaceLeases,
477
+ waitForDevice,
478
+ selectFromPool,
479
+ io: fileLeaseIo,
480
+ now: () => Date.now(),
481
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
482
+ out: (line) => console.log(line),
483
+ note: (line) => console.error(line)
484
+ };
485
+ function grantLine(facts, durationText) {
486
+ 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}` : ""}.`;
487
+ }
488
+ function releasedLine(lease) {
489
+ return `unlocked ${lease.deviceName ? `${lease.deviceName} (${lease.id})` : lease.id} -- the ${lease.platform} lease ran until ${clockTime(lease.expiresAt)}.`;
490
+ }
491
+ function readPlatform(value) {
492
+ return value === "ios" || value === "android" ? value : null;
493
+ }
494
+ async function resolveDevice(platform, requested, d) {
495
+ if (platform === "ios") {
496
+ const resolved = resolveIosPhysicalDevice(requested, await d.listIosDevices());
497
+ if (!resolved.udid) return {
498
+ code: "STIM_NO_DEVICE",
499
+ message: resolved.error,
500
+ remedy: resolved.remedy ?? null
501
+ };
502
+ return {
503
+ id: resolved.udid,
504
+ deviceName: resolved.name ?? resolved.udid
505
+ };
506
+ }
507
+ const adb = await d.listAdbDevices();
508
+ const resolved = await emulatorProbeSession(d.probeEmulatorSerial)((isEmulator) => resolvePhysicalDevice(requested, adb, isEmulator));
509
+ if (!resolved.serial) return {
510
+ code: "STIM_NO_DEVICE",
511
+ message: resolved.error,
512
+ remedy: resolved.remedy ?? null
513
+ };
514
+ return {
515
+ id: resolved.serial,
516
+ deviceName: await d.physicalDeviceModel(resolved.serial) ?? resolved.serial
517
+ };
518
+ }
519
+ async function findOwnedVirtualDevice(project, platform, id, avdNameOf) {
520
+ let running;
521
+ for (const { slot, platforms } of projectDeviceSlots(project)) {
522
+ if (platform === "ios") {
523
+ const sim = platforms.ios;
524
+ if (sim?.owned && sim.deviceUdid && sim.deviceUdid.toLowerCase() === id.toLowerCase()) return {
525
+ id: sim.deviceUdid,
526
+ deviceName: sim.deviceName ?? null,
527
+ slot
528
+ };
529
+ continue;
530
+ }
531
+ const emulator = platforms.android;
532
+ if (!emulator?.owned || !emulator.avdName || !/^emulator-\d+$/.test(id)) continue;
533
+ running ??= { name: await avdNameOf(id) };
534
+ if (running.name === emulator.avdName) return {
535
+ id,
536
+ deviceName: emulator.deviceName ?? emulator.avdName,
537
+ slot
538
+ };
539
+ }
540
+ return null;
541
+ }
542
+ function isFailure(value) {
543
+ return "code" in value;
544
+ }
545
+ async function poolDevice(platform, idLabel, waitSeconds, deadline, root, d, slot) {
546
+ const probed = emulatorProbeSession(d.probeEmulatorSerial);
547
+ const pooled = await d.selectFromPool({
548
+ ...slot ? { slot } : {},
549
+ root,
550
+ platform,
551
+ idLabel,
552
+ list: async () => {
553
+ if (platform === "ios") return iosPoolCandidates(await d.listIosDevices()).map((entry) => ({
554
+ id: entry.udid,
555
+ name: entry.name,
556
+ fallback: isWirelessIosDevice(entry)
557
+ }));
558
+ const adb = await d.listAdbDevices();
559
+ return (await probed((isEmulator) => androidPoolCandidates(adb, isEmulator))).map((entry) => ({ id: entry.serial }));
560
+ },
561
+ noCandidates: async () => {
562
+ let resolved;
563
+ if (platform === "ios") resolved = iosPoolNoCandidatesRefusal(await d.listIosDevices());
564
+ else {
565
+ const adb = await d.listAdbDevices();
566
+ resolved = await probed((isEmulator) => androidPoolNoCandidatesRefusal(adb, isEmulator));
567
+ }
568
+ return {
569
+ message: resolved.error,
570
+ remedy: resolved.remedy
571
+ };
572
+ },
573
+ waitSeconds,
574
+ deadline,
575
+ now: d.now,
576
+ sleep: d.sleep,
577
+ warn: (line) => d.note(chalk.yellow(line)),
578
+ io: d.io
579
+ });
580
+ if (pooled.status === "refused") return {
581
+ code: pooled.refusal.code,
582
+ message: pooled.refusal.message,
583
+ remedy: pooled.refusal.remedy,
584
+ ...pooled.refusal.lease === null ? {} : { lease: pooled.refusal.lease }
585
+ };
586
+ const name = pooled.candidate.name ?? (platform === "ios" ? pooled.candidate.id : await d.physicalDeviceModel(pooled.candidate.id));
587
+ return {
588
+ id: pooled.candidate.id,
589
+ deviceName: name ?? pooled.candidate.id
590
+ };
591
+ }
592
+ async function runLock(platformArg, idArg, opts = {}, overrides = {}) {
593
+ const d = {
594
+ ...DEFAULT_DEPS,
595
+ ...overrides
596
+ };
597
+ const json = Boolean(opts.json);
598
+ const report = (failure) => {
599
+ d.note(chalk.red(`${failure.code}: ${failure.message}`));
600
+ if (failure.remedy) d.note(chalk.dim(failure.remedy));
601
+ if (json) d.out(JSON.stringify({
602
+ code: failure.code,
603
+ message: failure.message,
604
+ remedy: failure.remedy,
605
+ ...failure.lease === void 0 ? {} : { lease: failure.lease }
606
+ }));
607
+ return failure;
608
+ };
609
+ const platform = readPlatform(platformArg);
610
+ if (!platform) return report({
611
+ code: "STIM_BAD_ARG",
612
+ message: `\`stim device lock\` takes ios or android, not ${JSON.stringify(platformArg)}.`,
613
+ remedy: "Run `stim device lock ios` or `stim device lock android`."
614
+ });
615
+ const root = d.findProjectRoot(process.cwd());
616
+ if (!root) return report(NO_PROJECT_REFUSAL);
617
+ const durationText = String(opts.for ?? DEFAULT_FOR).trim();
618
+ const duration = parseLeaseDuration(durationText);
619
+ if ("error" in duration) return report({
620
+ code: "STIM_BAD_ARG",
621
+ message: duration.error,
622
+ remedy: "Pass a whole number of seconds or minutes from 10s to 30m, e.g. --for 90s or --for 10m."
623
+ });
624
+ const wait = parseDeviceWait(opts.wait);
625
+ if ("error" in wait) return report({
626
+ code: "STIM_BAD_ARG",
627
+ message: wait.error,
628
+ remedy: "Pass a whole number of seconds, e.g. --wait 90. `--wait 0` refuses a leased device at once."
629
+ });
630
+ const idLabel = platform === "ios" ? "udid" : "serial";
631
+ const deadline = d.now() + wait.seconds * 1e3;
632
+ const lastLine = { at: null };
633
+ const owned = idArg ? await findOwnedVirtualDevice(d.getProject(root), platform, idArg, d.avdNameOf) : null;
634
+ if (owned && opts.slot && opts.slot !== owned.slot) return report({
635
+ code: "STIM_BAD_ARG",
636
+ message: `${owned.id} is this workspace's ${platform} device in slot ${owned.slot}, not ${opts.slot}.`,
637
+ remedy: `Run \`stim device lock ${platform} ${owned.id}\` without --slot, or with --slot ${owned.slot}.`
638
+ });
639
+ const held = owned ? d.io.readHolder(root)[deviceSlotKey(platform, owned.slot)] : void 0;
640
+ if (owned && held && held.id !== owned.id) return report({
641
+ code: "STIM_DEVICE_BUSY",
642
+ message: `This workspace already leases ${held.id} for ${platform} in slot ${owned.slot}, and a workspace holds one lease per platform and slot.`,
643
+ remedy: `Release it with \`stim device unlock ${platform}${owned.slot === "default" ? "" : ` --slot ${owned.slot}`}\` once nothing depends on it, then lock ${owned.id}.`
644
+ });
645
+ const slot = owned ? owned.slot === "default" ? void 0 : owned.slot : opts.slot;
646
+ const device = owned ? owned : idArg ? await resolveDevice(platform, idArg, d) : await poolDevice(platform, idLabel, wait.seconds, deadline, root, d, opts.slot);
647
+ if (isFailure(device)) return report(device);
648
+ for (;;) {
649
+ const outcome = await d.waitForDevice({
650
+ ...slot ? { slot } : {},
651
+ root,
652
+ platform,
653
+ id: device.id,
654
+ idLabel,
655
+ waitSeconds: wait.seconds,
656
+ now: d.now,
657
+ sleep: d.sleep,
658
+ warn: (line) => d.note(chalk.yellow(line)),
659
+ io: d.io,
660
+ deadline,
661
+ lastLine
662
+ });
663
+ if (outcome.status === "refused") return report({
664
+ ...outcome.refusal,
665
+ lease: outcome.refusal.lease
666
+ });
667
+ const taken = d.takeLease({
668
+ ...slot ? { slot } : {},
669
+ root,
670
+ platform,
671
+ id: device.id,
672
+ deviceName: device.deviceName,
673
+ kind: "declared",
674
+ durationMs: duration.ms
675
+ }, d.io);
676
+ if (taken.status === "unreadable") return report({
677
+ code: "STIM_DEVICE_BUSY",
678
+ message: `The lease file at ${taken.path} does not parse, so Stim cannot tell who holds that device.`,
679
+ remedy: `Read ${taken.path} and, once you know no run depends on it, delete it. \`stim gc\` reports it on every run.`,
680
+ lease: {
681
+ platform: null,
682
+ id: null,
683
+ deviceName: null,
684
+ holder: null,
685
+ expiresAt: null
686
+ }
687
+ });
688
+ if (taken.status === "held") continue;
689
+ const facts = {
690
+ ...taken.lease.slot ? { slot: taken.lease.slot } : {},
691
+ platform,
692
+ id: taken.lease.id,
693
+ deviceName: taken.lease.deviceName,
694
+ holder: taken.lease.holder,
695
+ kind: "declared",
696
+ grantedAt: taken.lease.grantedAt,
697
+ expiresAt: taken.lease.expiresAt,
698
+ leaseSeconds: Math.round(duration.ms / 1e3)
699
+ };
700
+ if (json) d.out(JSON.stringify(facts));
701
+ else d.out(chalk.green(grantLine(facts, durationText)));
702
+ return facts;
703
+ }
704
+ }
705
+ async function runUnlock(platformArg, opts = {}, overrides = {}) {
706
+ const d = {
707
+ ...DEFAULT_DEPS,
708
+ ...overrides
709
+ };
710
+ const json = Boolean(opts.json);
711
+ const report = (failure) => {
712
+ d.note(chalk.red(`${failure.code}: ${failure.message}`));
713
+ if (failure.remedy) d.note(chalk.dim(failure.remedy));
714
+ if (json) d.out(JSON.stringify({
715
+ code: failure.code,
716
+ message: failure.message,
717
+ remedy: failure.remedy
718
+ }));
719
+ return failure;
720
+ };
721
+ const platform = platformArg === void 0 ? null : readPlatform(platformArg);
722
+ if (platformArg !== void 0 && !platform) return report({
723
+ code: "STIM_BAD_ARG",
724
+ message: `\`stim device unlock\` takes ios or android, not ${JSON.stringify(platformArg)}.`,
725
+ remedy: "Run `stim device unlock` for every lease this workspace holds, or name one platform."
726
+ });
727
+ const root = d.findProjectRoot(process.cwd());
728
+ if (!root) return report(NO_PROJECT_REFUSAL);
729
+ const released = d.releaseLeases(root, {
730
+ platform,
731
+ ...opts.slot ? { slot: opts.slot } : {}
732
+ }, d.io);
733
+ 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.`));
734
+ if (json) d.out(JSON.stringify(released.map((lease) => ({
735
+ ...lease,
736
+ holder: root
737
+ }))));
738
+ else for (const lease of released) d.out(chalk.green(releasedLine(lease)));
739
+ return released;
740
+ }
741
+ function registerDevice(program, deps = {}) {
742
+ const device = program.command("device").description("Lease a connected physical device to this workspace, give it back, provision iOS signing for it, or change the workspace's own simulator or emulator");
743
+ device.command("lock").argument("<platform>", "ios or android").argument("[id]", "the UDID or serial to lease, a connected device or this workspace's own simulator or emulator; without one, the first free connected device is used").description("Lease a connected physical device, or this workspace's own simulator or emulator, to this workspace for a declared time, so another workspace's `--device` run waits and agents see the device as driven. 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", parseDeviceSlotOption).option("--json", "print the lease as JSON on stdout").action(async (platform, id, opts) => {
744
+ if ("code" in await runLock(platform, id, opts, deps)) process.exit(1);
745
+ });
746
+ 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", parseDeviceSlotOption).option("--json", "print the released leases as JSON on stdout").action(async (platform, opts) => {
747
+ const result = await runUnlock(platform, opts, deps);
748
+ if (!Array.isArray(result)) process.exit(1);
749
+ });
750
+ device.command("provision").argument("<platform>", "ios").argument("[udid]", "the connected iPhone or iPad to provision for; required when more than one is connected").description("Register a connected iPhone with the project's Apple Developer team and create or refresh its iOS App Development profile, through xcodebuild -allowProvisioningUpdates. It lists the account changes and asks first; no build ever does this.").option("--scheme <name>", "Xcode scheme whose signed targets to provision (default: ios.scheme, then detected)").option("--configuration <name>", "build configuration to provision (default: ios.configuration, then Debug)").option("--yes", "confirm the listed Apple Developer account changes without a prompt").option("--json", "print the plan and result, or the refusal, as JSON on stdout").action(async (platform, udid, opts) => {
751
+ const root = findProjectRoot$1(process.cwd()) ?? process.cwd();
752
+ const { value: result, interrupted } = await runProcessOperation(root, (operation) => runProvision(platform, udid, opts, { operation }));
753
+ if ("code" in result) process.exit(interrupted ?? 1);
754
+ });
755
+ registerDeviceTools(device);
756
+ }
757
+ function deviceCommand(program) {
758
+ registerDevice(program);
759
+ }
760
+ //#endregion
761
+ export { deviceCommand as default, grantLine, registerDevice, releasedLine, runLock, runUnlock };