stim 1.15.0 → 1.17.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 (123) hide show
  1. package/dist/{activity-B2-kb7k0.mjs → activity-IRvJY_ru.mjs} +49 -16
  2. package/dist/agent-device-usage-output-BK41vnzN.mjs +411 -0
  3. package/dist/{android-Cu_L4RpL.mjs → android--GorydsK.mjs} +692 -321
  4. package/dist/{android-Db2eEMaf.mjs → android-B_YNZy61.mjs} +25 -60
  5. package/dist/{android-CYmq0jNi.mjs → android-DE4v59Mb.mjs} +12 -3
  6. package/dist/{android-cas-PnKfAdB2.mjs → android-cas-ZFpjJBL_.mjs} +6 -5
  7. package/dist/{app-install-CAAplXAt.mjs → app-install-VPkI5WEg.mjs} +52 -8
  8. package/dist/attempt-Bk2ouQVL.mjs +39 -0
  9. package/dist/{budget-CMiqghig.mjs → budget-DM984mnd.mjs} +372 -366
  10. package/dist/{build-plan-aeGMFX7e.mjs → build-plan-CmQCoyT-.mjs} +346 -88
  11. package/dist/{build-slots-Df1ApSuH.mjs → build-slots-41v_d2x9.mjs} +36 -21
  12. package/dist/{cli-0cX5s4NM.mjs → cli-Daxug5-K.mjs} +23 -17
  13. package/dist/cli.mjs +1 -1
  14. package/dist/collector-run.mjs +5 -192
  15. package/dist/{command-output-CliHHn7a.mjs → command-output-CDAIFD-8.mjs} +3 -3
  16. package/dist/{created-devices-BwVb3liH.mjs → created-devices-CpvzBL25.mjs} +1 -1
  17. package/dist/{dependency-state-BmsjKZ2H.mjs → dependency-state-CLcBHH2M.mjs} +1 -1
  18. package/dist/{deps-BFlKbOIp.mjs → deps-DNWaQs4V.mjs} +2 -2
  19. package/dist/{dev-client-CjzLoNs0.mjs → dev-client-B-XC4uRH.mjs} +3 -40
  20. package/dist/{device-BZ1IyS5A.mjs → device-C8IOoJPv.mjs} +7 -6
  21. package/dist/device-capacity-D0GWHnTM.mjs +580 -0
  22. package/dist/device-host-worker.mjs +587 -110
  23. package/dist/{device-ios-DMQBUPus.mjs → device-ios-Bc7vbGwl.mjs} +75 -44
  24. package/dist/{device-lease-W5Pi1q38.mjs → device-lease-DpoS36_u.mjs} +2 -2
  25. package/dist/{device-pool-B2r38YjS.mjs → device-lease-run-B8YltIH_.mjs} +5 -125
  26. package/dist/device-pool-3o7s5lo4.mjs +125 -0
  27. package/dist/{device-remote-9ulPsdhY.mjs → device-remote-hFSkpfQu.mjs} +13 -13
  28. package/dist/{doctor-BcE5xsBn.mjs → doctor-Cnk9d7Hm.mjs} +86 -59
  29. package/dist/{error-diagnostics-Cb0P1WLS.mjs → error-diagnostics-C-NMnhrk.mjs} +11 -15
  30. package/dist/{gc-BuL1Genr.mjs → gc-DxCL2A46.mjs} +478 -33
  31. package/dist/{gradle-CmdOBq1d.mjs → gradle-D7-L2Th3.mjs} +4 -4
  32. package/dist/{guide-cCSGRFd3.mjs → guide-DjrTQGik.mjs} +2198 -566
  33. package/dist/{guide-status-CIoWK6mG.mjs → guide-status-Du-ZQ3FD.mjs} +1 -1
  34. package/dist/hosted-android-DwLS4Mse.mjs +32 -0
  35. package/dist/hosted-client-wk1W_CNW.mjs +196 -0
  36. package/dist/hosted-ios-aMo7Cljf.mjs +15 -0
  37. package/dist/hosted-logs-DqYmWtXc.mjs +187 -0
  38. package/dist/hosted-macos-dlHXEeF5.mjs +135 -0
  39. package/dist/hosted-native-C9xJ9rpA.mjs +255 -0
  40. package/dist/{build-progress-C2Jv1AtA.mjs → idle-B2m-Ovod.mjs} +300 -9
  41. package/dist/{idle-shutdown-B5LPaNyY.mjs → idle-shutdown-_3oOV1V_.mjs} +25 -17
  42. package/dist/{in-use-CndPMilN.mjs → in-use-CvGOUn-x.mjs} +30 -10
  43. package/dist/ios-Cr1h5gnO.mjs +189 -0
  44. package/dist/{ios-Chob8ny7.mjs → ios-DE6duQoz.mjs} +819 -460
  45. package/dist/{ios-3CtCRhwr.mjs → ios-DSfnPfse.mjs} +4 -4
  46. package/dist/{ios-device-DERmMmk-.mjs → ios-device-pAN4t54-.mjs} +1 -1
  47. package/dist/ios-state-8L3lgum6.mjs +89 -0
  48. package/dist/{launch-verify-CLlofxRt.mjs → launch-verify-lTnWZikR.mjs} +3 -3
  49. package/dist/logs-DUoKlDoS.mjs +384 -0
  50. package/dist/{client-C2IS36Qf.mjs → machines-C2XiNwh9.mjs} +330 -37
  51. package/dist/{macos-BwtbUtTN.mjs → macos-32po3kUq.mjs} +23 -18
  52. package/dist/macos-run.mjs +1 -1
  53. package/dist/maintenance-run.d.mts +4 -0
  54. package/dist/maintenance-run.mjs +254 -0
  55. package/dist/{metro-Di8EN2Tb.mjs → metro-g8NqiJp-.mjs} +1 -1
  56. package/dist/metro-gateway-DmslUM30.mjs +103 -0
  57. package/dist/{named-ports-Dj_XBNaL.mjs → named-ports-C8m8EQs2.mjs} +2 -2
  58. package/dist/{native-run-DFDoIRbY.mjs → native-run-D1Q56rAD.mjs} +17 -6
  59. package/dist/{native-runtime-i6OkVg-w.mjs → native-runtime-DhJI3IVQ.mjs} +6 -6
  60. package/dist/{ndjson-CDY9IkVC.mjs → ndjson-PclOIwVY.mjs} +2 -2
  61. package/dist/offload-worker.d.mts +1 -0
  62. package/dist/offload-worker.mjs +13 -8
  63. package/dist/{teardown-BYuQafiA.mjs → ownership-BMLqyBgJ.mjs} +639 -53
  64. package/dist/{ownership-BkYxUtuj.mjs → ownership-DyRQru0q.mjs} +2 -2
  65. package/dist/{ports-DddwttKM.mjs → ports-4nYqxPWC.mjs} +2 -2
  66. package/dist/{prebuild-BvxDlD6C.mjs → prebuild-D7RUMkwN.mjs} +4 -4
  67. package/dist/preview-CxPRIEht.mjs +318 -0
  68. package/dist/{project-CV3tDFHl.mjs → project-D47Rh0ly.mjs} +72 -3
  69. package/dist/{recordings-GEXp4iBT.mjs → recordings-BNgsD1Tr.mjs} +1 -1
  70. package/dist/{reload-ChPiwvot.mjs → reload-qZei6iWn.mjs} +77 -13
  71. package/dist/{remote-cache-CO2elm_b.mjs → remote-cache-Ci4DQYYj.mjs} +4 -4
  72. package/dist/{run-B-xqRrsg.mjs → run-CkE9y0nc.mjs} +2 -2
  73. package/dist/{server-bare-X7PUgizL.mjs → server-bare-BkrSxSsZ.mjs} +1 -1
  74. package/dist/{server-expo-DhtX6bom.mjs → server-expo-CHTyXD0G.mjs} +4 -4
  75. package/dist/server-expo-CeakhI8P.mjs +2 -0
  76. package/dist/settings-BBpMcDbD.mjs +25 -0
  77. package/dist/settings-Cfd-0r2d.mjs +1440 -0
  78. package/dist/{settings-DbL6uhH8.mjs → settings-SR9OmW9h.mjs} +10 -4
  79. package/dist/settings.schema.json +472 -50
  80. package/dist/{simslim-Ciluxlq4.mjs → simslim-BWNrc3M3.mjs} +3 -3
  81. package/dist/{slot-launch-BY0BrOLh.mjs → slot-launch-DmjBmpQV.mjs} +10 -7
  82. package/dist/{spawn-entry-DncpJvWL.mjs → spawn-entry-jw19m8Nl.mjs} +1 -0
  83. package/dist/{start-CPO4wRg7.mjs → start-BopZcr4n.mjs} +1 -1
  84. package/dist/{start-CUD9nasA.mjs → start-uee1wWjY.mjs} +21 -17
  85. package/dist/{state-CaeaHbDA.mjs → state-BKa8wNN5.mjs} +1 -1
  86. package/dist/{state-BxCgeTQu.mjs → state-DyaNTgfR.mjs} +2 -2
  87. package/dist/{state-DFm77wRn.mjs → state-PeQiJuIu.mjs} +5 -5
  88. package/dist/{stats-DBLLFmKd.mjs → stats-Bq4fJOdB.mjs} +19 -5
  89. package/dist/{status-l84Gbvp2.mjs → status-CzS4bPO9.mjs} +5 -3
  90. package/dist/status-DlKvVJBE.mjs +49 -0
  91. package/dist/{status-DYXxNOT0.mjs → status-dXX2JX1C.mjs} +244 -50
  92. package/dist/{stop-CceYh1yG.mjs → stop-BCwvvs7F.mjs} +2 -2
  93. package/dist/{stop-Bad61z_2.mjs → stop-BjizrQwv.mjs} +83 -21
  94. package/dist/{stop-Bvf1MflS.mjs → stop-Cr1d8DSE.mjs} +4 -4
  95. package/dist/stop-cause-rhmGfwvt.mjs +97 -0
  96. package/dist/supervisor-run.d.mts +7 -4
  97. package/dist/supervisor-run.mjs +156 -27
  98. package/dist/{support-WLOzVKUv.mjs → support-BJPgYCu-.mjs} +61 -11
  99. package/dist/{support-CDQbDkL5.mjs → support-F9-sr0Oi.mjs} +6 -5
  100. package/dist/{toolchain-DmeYjXD9.mjs → toolchain-CNIBTIOv.mjs} +11 -11
  101. package/dist/trigger-Bemu8ytw.mjs +65 -0
  102. package/dist/trigger-CXts6qt1.mjs +2 -0
  103. package/dist/{warm-progress-DVHLN4uC.mjs → warm-progress-DF4JRvYI.mjs} +274 -5
  104. package/dist/watchman-APYjXSQa.mjs +48 -0
  105. package/dist/{web-k6w8-HnE.mjs → web-exQtGkoc.mjs} +14 -16
  106. package/dist/web-run.mjs +5 -5
  107. package/dist/{workspace-process-lock-BL7Q0HCO.mjs → workspace-process-lock-2mNzOlaB.mjs} +3 -2
  108. package/dist/{workspace-state-CL2V2Ons.mjs → workspace-state-DqcCbpE7.mjs} +34 -34
  109. package/dist/workspaces-Bcib97-6.mjs +306 -0
  110. package/dist/{worktree-BT_uULFx.mjs → worktree-Dc98cE6_.mjs} +1 -1
  111. package/dist/{worktree-qXKzADSC.mjs → worktree-ZpZR3S1k.mjs} +601 -33
  112. package/package.json +5 -5
  113. package/dist/device-capacity-DMuKC7nx.mjs +0 -136
  114. package/dist/hosted-macos-DwowNU7W.mjs +0 -447
  115. package/dist/logs-h9h6MUvE.mjs +0 -568
  116. package/dist/machine-usage-D_YYMzfR.mjs +0 -283
  117. package/dist/machines-CYtOCqcj.mjs +0 -259
  118. package/dist/merge-state-BnxLNQiM.mjs +0 -272
  119. package/dist/server-expo-KJeD109I.mjs +0 -2
  120. package/dist/settings-CduVcoxj.mjs +0 -680
  121. package/dist/stop-cause-CZ_Mr-v9.mjs +0 -665
  122. package/dist/watchman-Bx0wceoF.mjs +0 -238
  123. package/dist/worktree-C0c5Udwa.mjs +0 -753
@@ -1,26 +1,400 @@
1
1
  import { t as getExecutor } from "./exec-CaErVv7t.mjs";
2
+ import { D as withConfigLock$1, _ as loadConfig$1, c as ensureConfig } from "./config-D0DbpZtM.mjs";
2
3
  import { t as process_identity_exports } from "./process-identity-BmRm1qV0.mjs";
3
- import { c as writeWorkspaceState, p as ensureWorkspaceStorage } from "./workspace-state-CL2V2Ons.mjs";
4
- import { A as listAvds, D as hostSystemImageArch, E as getAvdNameForSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, j as listInstalledSystemImages, k as listAvdDeviceProfiles, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-Db2eEMaf.mjs";
5
- import { i as teardownOwnedAvd, o as teardownOwnedIosSim } from "./teardown-BYuQafiA.mjs";
6
- import { C as resolveIosCreation, i as createOwnedIosSim, j as readHostMemoryPressure, n as bootIosSim, u as listAllIosSims } from "./ios-3CtCRhwr.mjs";
7
- import { a as requiredMacosRecord, r as macosProcess } from "./state-BxCgeTQu.mjs";
8
- import { _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, x as launchIosApp } from "./app-install-CAAplXAt.mjs";
9
- import { h as findAapt } from "./support-CDQbDkL5.mjs";
10
- import { n as stopMacosAppHeld } from "./stop-Bvf1MflS.mjs";
11
- import { t as spawnEntry } from "./spawn-entry-DncpJvWL.mjs";
4
+ import { c as writeWorkspaceState, u as ensureWorkspaceStorage } from "./workspace-state-DqcCbpE7.mjs";
5
+ import { t as forgetCreatedDevice } from "./created-devices-CpvzBL25.mjs";
6
+ import { A as listAvds, B as ownedAvdSystemImage, D as hostSystemImageArch, E as getAvdNameForSerial, J as resolveOwnedAvdSerial, O as listAdbDevices, R as ownedAvdName, W as pickDefaultSystemImage, _ as avdStorageRoots, a as androidDeviceAbi, b as createOwnedAvd, d as androidToolPath, f as assertOwnedAvdStopped, h as avdPathExists, j as listInstalledSystemImages, k as listAvdDeviceProfiles, q as resetAdoptedAvd, t as DEFAULT_AVD_DEVICE_PROFILE, tt as waitForBoot, v as bootAndroidEmulator } from "./android-B_YNZy61.mjs";
7
+ import { C as resolveIosCreation, D as uninstallIosApp, S as resetIosPrivacy, i as createOwnedIosSim, j as readHostMemoryPressure, m as listUserApps, n as bootIosSim, u as listAllIosSims, x as resetIosKeychain } from "./ios-DSfnPfse.mjs";
8
+ import { g as teardownOwnedIosSim, m as teardownOwnedAvd } from "./ownership-BMLqyBgJ.mjs";
9
+ import { a as requiredMacosRecord, r as macosProcess } from "./state-DyaNTgfR.mjs";
10
+ import { r as createNdjsonWriter } from "./ndjson-PclOIwVY.mjs";
11
+ import { n as parseLogStreamLine } from "./ios-Cr1h5gnO.mjs";
12
+ import { n as parseLogcatLine, r as parsePidof, t as androidClockOffset } from "./android-DE4v59Mb.mjs";
13
+ import { i as countLiveOwnedDevices } from "./device-capacity-D0GWHnTM.mjs";
14
+ import { C as reverseMetroPorts, _ as installIosApp, b as launchAndroidReleaseApp, h as installAndroidApp, s as androidAppProcess, v as iosAppProcess, w as writeDebugHttpHost, x as launchIosApp, y as launchAndroidApp } from "./app-install-VPkI5WEg.mjs";
15
+ import { h as findAapt } from "./support-F9-sr0Oi.mjs";
16
+ import { n as stopMacosAppHeld } from "./stop-Cr1d8DSE.mjs";
17
+ import { t as spawnEntry } from "./spawn-entry-jw19m8Nl.mjs";
12
18
  import { createHash, randomUUID } from "node:crypto";
13
19
  import { chmodSync, closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statfsSync, symlinkSync, writeFileSync } from "node:fs";
14
20
  import { dirname, isAbsolute, join, posix, relative, sep } from "node:path";
15
- import { configDir, withDirLock } from "@stim-cli/core";
16
- import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedConsolePort, hostedMacosAppSlot, hostedMacosBundleId, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedDevice, readMacosRecord } from "@stim-cli/core/state";
21
+ import { LOG_ROTATE_BYTES, configDir, withDirLock } from "@stim-cli/core";
22
+ import { assertHostedDeviceLedger, hostedAppArea, hostedAppAttempt, hostedAppBlobs, hostedConsolePort, hostedMacosAppSlot, hostedMacosBundleId, hostedNativeLogsDir, isJsonObject, parseHostedOfferRequest, readHostedApp, readHostedAppMetadata, readHostedDevice, readHostedNativeLogsCheckpoint, readMacosRecord, readNdjsonGenerations } from "@stim-cli/core/state";
17
23
  import { availableParallelism, freemem, homedir, loadavg } from "node:os";
24
+ //#region src/device-host/android-app.ts
25
+ async function digest$1(path) {
26
+ const hash = createHash("sha256");
27
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
28
+ return hash.digest("hex");
29
+ }
30
+ /** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
31
+ async function installHostedAndroidApp(home, session, attempt, device, metro) {
32
+ const record = readHostedApp(session, attempt, home);
33
+ const area = hostedAppArea(session, attempt, home);
34
+ if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
35
+ const entry = record.files[0];
36
+ if (record.files.length !== 1 || !entry || entry.path !== "App.apk" || entry.kind !== "file") throw new Error("Hosted Android delivery requires a single file entry named App.apk.");
37
+ const manifest = join(hostedAppBlobs(session, home), record.manifest.sha256);
38
+ if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
39
+ const blob = join(hostedAppBlobs(session, home), entry.sha256);
40
+ const stat = lstatSync(blob);
41
+ if (!stat.isFile() || stat.size !== entry.size || await digest$1(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
42
+ const temporary = join(area, "App.apk.tmp");
43
+ const apk = join(area, "App.apk");
44
+ copyFileSync(blob, temporary);
45
+ renameSync(temporary, apk);
46
+ const tool = findAapt();
47
+ if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
48
+ const badging = getExecutor().runFile(tool.path, [
49
+ "dump",
50
+ "badging",
51
+ apk
52
+ ], {
53
+ timeoutMs: 1e4,
54
+ killSignal: "SIGKILL"
55
+ });
56
+ const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
57
+ const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
58
+ const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
59
+ if (identity !== record.bundleId || !minimum || !api || Number(minimum) > Number(api)) throw new Error("The APK package identity or minimum SDK is incompatible with the hosted emulator.");
60
+ const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
61
+ if (nativeCode !== void 0 && !nativeCode.split(/\s+/).includes(`'${device.architecture}'`)) throw new Error("The APK has no native library slice for the hosted emulator architecture.");
62
+ assertHostedAndroidTarget(home, device);
63
+ const sdkExecutor = hostedAndroidExecutor();
64
+ const installed = installAndroidApp({
65
+ serial: device.serial,
66
+ apkPath: apk,
67
+ packageName: record.bundleId
68
+ }, { exec: sdkExecutor });
69
+ if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
70
+ assertHostedAndroidTarget(home, device);
71
+ if (record.mode === "development" && !metro) throw new Error("Hosted Android development launch requires its Metro bridge.");
72
+ const launched = record.mode === "development" && metro ? launchAndroidApp({
73
+ serial: device.serial,
74
+ packageName: record.bundleId,
75
+ metroPort: metro.devicePort,
76
+ bridgePort: metro.bridgePort,
77
+ physical: true,
78
+ devClientScheme: record.devClientScheme
79
+ }, { exec: sdkExecutor }) : launchAndroidReleaseApp({
80
+ serial: device.serial,
81
+ packageName: record.bundleId
82
+ }, { exec: sdkExecutor });
83
+ if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
84
+ if (record.mode === "development") return "unverified";
85
+ for (let tries = 0; tries < 10; tries++) {
86
+ await new Promise((resolve) => setTimeout(resolve, 200));
87
+ if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
88
+ }
89
+ return "unverified";
90
+ }
91
+ function assertHostedAndroidSerial(home, device) {
92
+ assertHostedDeviceLedger(home, device.avdName, "android");
93
+ const current = readHostedDevice(home, "android");
94
+ if (current.avdName !== device.avdName || current.serial !== device.serial || current.systemImage !== device.systemImage || getAvdNameForSerial(device.serial, { timeoutMs: 2e3 }) !== device.avdName) throw new Error("The hosted Android device identity changed.");
95
+ }
96
+ function assertHostedAndroidTarget(home, device) {
97
+ assertHostedAndroidSerial(home, device);
98
+ if (androidDeviceAbi(device.serial) !== device.architecture) throw new Error("The hosted Android device identity or running ABI changed.");
99
+ }
100
+ function hostedAndroidExecutor() {
101
+ const executor = getExecutor();
102
+ const adb = androidToolPath("adb");
103
+ return {
104
+ ...executor,
105
+ runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
106
+ };
107
+ }
108
+ function restoreHostedAndroidMetro(home, session, attempt, device, metro) {
109
+ const record = readHostedApp(session, attempt, home);
110
+ if (record.state !== "installed" || record.mode !== "development") throw new Error("Metro restore requires an installed development app.");
111
+ assertHostedAndroidTarget(home, device);
112
+ const exec = hostedAndroidExecutor();
113
+ const reversed = reverseMetroPorts({
114
+ serial: device.serial,
115
+ metroPort: metro.bridgePort,
116
+ devicePorts: [metro.devicePort]
117
+ }, { exec });
118
+ if (reversed.failed) throw new Error(reversed.reason);
119
+ assertHostedAndroidTarget(home, device);
120
+ writeDebugHttpHost({
121
+ serial: device.serial,
122
+ packageName: record.bundleId,
123
+ metroPort: metro.devicePort,
124
+ physical: true
125
+ }, { exec });
126
+ }
127
+ //#endregion
128
+ //#region src/device-host/native-logs.ts
129
+ function collectHostedNativeLogs({ home, platform, since, final, query, parse, tailOnly = false, deadline = Date.now() + 1e4, identity = {} }) {
130
+ const directory = hostedNativeLogsDir(home, platform);
131
+ const previous = readHostedNativeLogsCheckpoint(home, platform);
132
+ const beginning = Math.floor(since / 1e3) * 1e3;
133
+ let newest;
134
+ if (!previous) {
135
+ for (const entry of readNdjsonGenerations(join(directory, "device.ndjson"))) if (typeof entry.ts === "number" && (newest === void 0 || entry.ts > newest)) newest = entry.ts;
136
+ }
137
+ const checkpoint = previous?.until ?? Math.max(beginning, Math.floor((newest ?? beginning) / 1e3) * 1e3);
138
+ let from = Math.max(beginning, previous ? checkpoint - 5e3 : checkpoint);
139
+ const now = Math.floor(Date.now() / 1e3) * 1e3;
140
+ if (now < checkpoint) return false;
141
+ const backlogMs = Math.max(1e3, now - checkpoint);
142
+ let windowMs = !final && !tailOnly && previous?.windowMs ? Math.min(previous.windowMs * 2, backlogMs) : backlogMs;
143
+ let until = Math.min(now, checkpoint + windowMs);
144
+ let end = until === now ? until + 1e3 : until;
145
+ let output;
146
+ for (;;) {
147
+ const remaining = deadline - Date.now();
148
+ if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
149
+ try {
150
+ output = query(from, end, Math.min(remaining, 4e3));
151
+ break;
152
+ } catch (error) {
153
+ const span = final || tailOnly ? until - from : until - checkpoint;
154
+ if (span <= 1e3) throw error;
155
+ windowMs = Math.max(1e3, Math.floor(span / 4 / 1e3) * 1e3);
156
+ if (final || tailOnly) from = Math.max(beginning, now - windowMs);
157
+ else until = Math.min(now, checkpoint + windowMs);
158
+ end = until === now ? until + 1e3 : until;
159
+ if (previous) {
160
+ mkdirSync(directory, {
161
+ recursive: true,
162
+ mode: 448
163
+ });
164
+ withDirLock(join(directory, "query.lock"), () => {
165
+ const temporary = join(directory, "checkpoint.json.tmp");
166
+ writeFileSync(temporary, JSON.stringify({
167
+ ...previous,
168
+ until: checkpoint,
169
+ windowMs
170
+ }), { mode: 384 });
171
+ renameSync(temporary, join(directory, "checkpoint.json"));
172
+ });
173
+ }
174
+ }
175
+ }
176
+ const seen = /* @__PURE__ */ new Map();
177
+ for (const digest of previous?.boundary ?? []) seen.set(digest, (seen.get(digest) ?? 0) + 1);
178
+ const boundary = [];
179
+ const records = [];
180
+ if (from > checkpoint) records.push({
181
+ ts: from,
182
+ src: "device",
183
+ platform,
184
+ level: "warn",
185
+ msg: `Hosted ${platform === "ios" ? "iOS" : "Android"} native log gap: dropped interval [${new Date(checkpoint).toISOString()}, ${new Date(from).toISOString()}) to collect the tail within its budget.`
186
+ });
187
+ for (const line of output.split("\n")) {
188
+ const event = parse(line);
189
+ if (!event) continue;
190
+ const { record: parsed, digest } = event;
191
+ if (typeof parsed.ts !== "number" || parsed.ts < from || parsed.ts > end) continue;
192
+ if (parsed.ts >= until - 5e3) boundary.push(digest);
193
+ if (newest !== void 0 && parsed.ts <= newest) continue;
194
+ const count = seen.get(digest) ?? 0;
195
+ if (count) {
196
+ seen.set(digest, count - 1);
197
+ continue;
198
+ }
199
+ records.push({
200
+ ...parsed,
201
+ platform
202
+ });
203
+ }
204
+ mkdirSync(directory, {
205
+ recursive: true,
206
+ mode: 448
207
+ });
208
+ withDirLock(join(directory, "query.lock"), () => {
209
+ const writer = createNdjsonWriter(join(directory, "device.ndjson"), { maxBytes: LOG_ROTATE_BYTES });
210
+ try {
211
+ for (const entry of records) if (!writer.write(entry)) throw new Error("Could not persist hosted native logs.");
212
+ } finally {
213
+ writer.close();
214
+ }
215
+ const temporary = join(directory, "checkpoint.json.tmp");
216
+ try {
217
+ writeFileSync(temporary, JSON.stringify({
218
+ until,
219
+ boundary,
220
+ ...identity,
221
+ ...until < now ? { windowMs } : {}
222
+ }), { mode: 384 });
223
+ renameSync(temporary, join(directory, "checkpoint.json"));
224
+ } finally {
225
+ rmSync(temporary, { force: true });
226
+ }
227
+ });
228
+ return until < now;
229
+ }
230
+ //#endregion
231
+ //#region src/device-host/android-logs.ts
232
+ /** Captures bounded logcat output from the installed app on this home's exact ledger-owned serial. */
233
+ function collectHostedAndroidLogs(home, session, attempt, since, final = false) {
234
+ const deadline = Date.now() + 1e4;
235
+ const device = readHostedDevice(home, "android");
236
+ assertHostedAndroidSerial(home, device);
237
+ const app = readHostedAppMetadata(session, attempt, home);
238
+ if (app.state !== "installed") return false;
239
+ const exec = getExecutor();
240
+ const adb = androidToolPath("adb");
241
+ const clockOffsetMs = androidClockOffset(device.serial, { exec: {
242
+ ...exec,
243
+ runFile: (file, args, options) => exec.runFile(file === "adb" ? adb : file, args, options)
244
+ } }) ?? 0;
245
+ const previous = readHostedNativeLogsCheckpoint(home, "android");
246
+ let current = null;
247
+ try {
248
+ current = parsePidof(exec.runFile(adb, [
249
+ "-s",
250
+ device.serial,
251
+ "shell",
252
+ "pidof",
253
+ app.bundleId
254
+ ], {
255
+ timeoutMs: 2e3,
256
+ killSignal: "SIGKILL"
257
+ }));
258
+ } catch (error) {
259
+ const failure = error;
260
+ if (failure.status !== 1 || failure.signal != null || failure.stdout !== "" || failure.stderr !== "") throw error;
261
+ }
262
+ const prior = previous?.appAttempt === attempt ? previous.pid : void 0;
263
+ const pids = new Set([current, prior].filter((pid) => typeof pid === "number"));
264
+ if (!pids.size) return false;
265
+ return collectHostedNativeLogs({
266
+ home,
267
+ platform: "android",
268
+ since,
269
+ final,
270
+ deadline,
271
+ tailOnly: true,
272
+ identity: {
273
+ appAttempt: attempt,
274
+ pid: current ?? void 0
275
+ },
276
+ query: (from, _end, timeoutMs) => [...pids].map((pid) => {
277
+ const remaining = deadline - Date.now();
278
+ if (remaining <= 0) throw new Error("Hosted native log query exceeded its collection budget.");
279
+ return exec.runFile(adb, [
280
+ "-s",
281
+ device.serial,
282
+ "logcat",
283
+ "-d",
284
+ "-v",
285
+ "epoch",
286
+ "-T",
287
+ ((from - clockOffsetMs) / 1e3).toFixed(3),
288
+ "--pid",
289
+ String(pid)
290
+ ], {
291
+ timeoutMs: Math.min(remaining, timeoutMs),
292
+ killSignal: "SIGKILL"
293
+ });
294
+ }).join("\n"),
295
+ parse: (line) => {
296
+ const record = parseLogcatLine(line, { clockOffsetMs });
297
+ if (!record || !pids.has(Number(/\((\d+)\)$/.exec(String(record.proc))?.[1]))) return null;
298
+ return {
299
+ record,
300
+ digest: createHash("sha256").update(line.trim()).digest("hex")
301
+ };
302
+ }
303
+ });
304
+ }
305
+ //#endregion
306
+ //#region src/device-host/ios-logs.ts
307
+ function logDate(ms) {
308
+ return new Date(ms).toISOString().replace("T", " ").replace(".000Z", "+0000");
309
+ }
310
+ /** Captures a bounded unified-log window only from this home's exact ledger-owned simulator. */
311
+ function collectHostedIosLogs(home, session, attempt, since, final = false) {
312
+ const device = readHostedDevice(home);
313
+ assertHostedDeviceLedger(home, device.udid);
314
+ if (readHostedAppMetadata(session, attempt, home).state !== "installed") return false;
315
+ const deadline = Date.now() + 1e4;
316
+ const exec = getExecutor();
317
+ const executable = exec.runFile("/usr/libexec/PlistBuddy", [
318
+ "-c",
319
+ "Print :CFBundleExecutable",
320
+ join(hostedAppArea(session, attempt, home), "App.app", "Info.plist")
321
+ ], {
322
+ timeoutMs: 2e3,
323
+ killSignal: "SIGKILL"
324
+ });
325
+ if (!executable || /["\\/\0\r\n]/.test(executable)) throw new Error("The app executable cannot form a log predicate.");
326
+ return collectHostedNativeLogs({
327
+ home,
328
+ platform: "ios",
329
+ deadline,
330
+ since,
331
+ final,
332
+ query: (from, end, timeoutMs) => exec.runFile("xcrun", [
333
+ "simctl",
334
+ "spawn",
335
+ device.udid,
336
+ "log",
337
+ "show",
338
+ "--style",
339
+ "ndjson",
340
+ "--predicate",
341
+ `processImagePath ENDSWITH "/App.app/${executable}"`,
342
+ "--info",
343
+ "--start",
344
+ logDate(from),
345
+ "--end",
346
+ logDate(end)
347
+ ], {
348
+ timeoutMs,
349
+ killSignal: "SIGKILL"
350
+ }),
351
+ parse: (line) => {
352
+ const record = parseLogStreamLine(line);
353
+ if (!record) return null;
354
+ const entry = JSON.parse(line);
355
+ return {
356
+ record,
357
+ digest: createHash("sha256").update(JSON.stringify(Object.entries(entry).toSorted(([a], [b]) => a.localeCompare(b)))).digest("hex")
358
+ };
359
+ }
360
+ });
361
+ }
362
+ //#endregion
18
363
  //#region src/device-host/app.ts
19
364
  function inside(root, path) {
20
365
  const diff = relative(root, path);
21
366
  return diff !== "" && diff !== ".." && !diff.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(diff);
22
367
  }
23
- async function digest$1(path) {
368
+ function removeHostedAppData(home, { keepBlobs = false } = {}) {
369
+ const area = realpathSync(join(home, ".."));
370
+ const assertInside = (path) => {
371
+ if (!inside(area, realpathSync(path))) throw new Error("Hosted app data resolves outside its private area.");
372
+ };
373
+ const remove = (path) => {
374
+ if (!lstatSync(path, { throwIfNoEntry: false })) return;
375
+ assertInside(path);
376
+ rmSync(path, {
377
+ recursive: true,
378
+ force: true
379
+ });
380
+ };
381
+ assertInside(home);
382
+ const apps = join(area, "apps");
383
+ if (existsSync(apps)) {
384
+ assertInside(apps);
385
+ for (const attempt of readdirSync(apps)) {
386
+ const directory = join(apps, attempt);
387
+ if (!lstatSync(directory).isDirectory()) continue;
388
+ assertInside(directory);
389
+ remove(join(directory, "App.app"));
390
+ remove(join(directory, "App.apk"));
391
+ remove(join(directory, "App.apk.tmp"));
392
+ remove(join(directory, "blobs"));
393
+ }
394
+ }
395
+ if (!keepBlobs) remove(join(area, "blobs"));
396
+ }
397
+ async function digest(path) {
24
398
  const hash = createHash("sha256");
25
399
  for await (const chunk of createReadStream(path)) hash.update(chunk);
26
400
  return hash.digest("hex");
@@ -42,19 +416,20 @@ function newer(value, supported) {
42
416
  async function materializeHostedApp(home, session, attempt) {
43
417
  const record = readHostedApp(session, attempt, home);
44
418
  const area = hostedAppArea(session, attempt, home);
419
+ const blobs = hostedAppBlobs(session, home);
45
420
  const app = join(area, "App.app");
46
421
  if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
47
- const manifest = join(area, "blobs", record.manifest.sha256);
48
- if (lstatSync(manifest).size !== record.manifest.size || await digest$1(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
422
+ const manifest = join(blobs, record.manifest.sha256);
423
+ if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted app manifest differs from its digest.");
49
424
  rmSync(app, {
50
425
  recursive: true,
51
426
  force: true
52
427
  });
53
428
  mkdirSync(app, { mode: 448 });
54
429
  for (const file of record.files) {
55
- const blob = join(area, "blobs", file.sha256);
430
+ const blob = join(blobs, file.sha256);
56
431
  const stat = lstatSync(blob);
57
- if (!stat.isFile() || stat.size !== file.size || await digest$1(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
432
+ if (!stat.isFile() || stat.size !== file.size || await digest(blob) !== file.sha256) throw new Error(`App content is incomplete or differs from its digest: ${file.path}`);
58
433
  if (file.kind === "link") continue;
59
434
  const target = join(app, file.path);
60
435
  mkdirSync(dirname(target), {
@@ -65,7 +440,7 @@ async function materializeHostedApp(home, session, attempt) {
65
440
  chmodSync(target, file.kind === "exec" ? 493 : 420);
66
441
  }
67
442
  for (const file of record.files.filter((each) => each.kind === "link")) {
68
- const bytes = readFileSync(join(area, "blobs", file.sha256));
443
+ const bytes = readFileSync(join(blobs, file.sha256));
69
444
  const target = bytes.toString("utf8");
70
445
  const destination = posix.normalize(posix.join(posix.dirname(file.path), target));
71
446
  if (!target || !Buffer.from(target).equals(bytes) || /[\\\0\r\n]/.test(target) || posix.isAbsolute(target) || destination === "." || destination === ".." || destination.startsWith("../") || file.path.startsWith(`${destination}/`)) throw new Error(`The app link escapes its bundle or forms an ancestor cycle: ${file.path}`);
@@ -80,7 +455,7 @@ async function materializeHostedApp(home, session, attempt) {
80
455
  for (const file of record.files) {
81
456
  const path = realpathSync(join(app, file.path));
82
457
  if (!inside(root, path)) throw new Error(`The app entry resolves outside its bundle: ${file.path}`);
83
- if (file.kind !== "link" && await digest$1(path) !== file.sha256) throw new Error("App entries alias different content.");
458
+ if (file.kind !== "link" && await digest(path) !== file.sha256) throw new Error("App entries alias different content.");
84
459
  }
85
460
  return {
86
461
  app,
@@ -201,8 +576,57 @@ async function runHostedDevice(mode, selectors, app) {
201
576
  };
202
577
  }
203
578
  device = readHostedDevice(home);
204
- assertHostedDeviceLedger(home, device.udid);
579
+ const ledger = assertHostedDeviceLedger(home, device.udid, "ios", { allowEmpty: mode === "stop" });
205
580
  const current = inventory().find((sim) => sim.udid === device.udid);
581
+ if (mode === "inspect" || mode === "adopt") {
582
+ if (!current || !current.available || !current.name.startsWith("stim-") || current.name !== device.name || current.state !== "Shutdown") throw new Error("The parked hosted simulator is missing, renamed or not Shutdown.");
583
+ let choice;
584
+ try {
585
+ choice = selectHostedIosDevice(selectors);
586
+ } catch (error) {
587
+ if (mode === "inspect") return {
588
+ state: "incompatible",
589
+ device,
590
+ notice: error.message
591
+ };
592
+ throw error;
593
+ }
594
+ if (choice.deviceTypeId !== device.deviceTypeId || choice.runtimeId !== device.runtimeId || choice.architecture !== device.architecture) {
595
+ if (mode === "inspect") return {
596
+ state: "incompatible",
597
+ device
598
+ };
599
+ throw new Error("The parked hosted simulator does not match the requested selectors.");
600
+ }
601
+ if (mode === "inspect") return {
602
+ state: "compatible",
603
+ device
604
+ };
605
+ await bootIosSim(device.udid, { openViewer: false });
606
+ if (inventory().find((sim) => sim.udid === device.udid)?.state !== "Booted") throw new Error("Hosted simulator boot could not be verified.");
607
+ resetIosPrivacy(device.udid);
608
+ resetIosKeychain(device.udid);
609
+ for (const bundleId of listUserApps(device.udid)) uninstallIosApp(device.udid, bundleId);
610
+ withDirLock(join(home, "hosted-device.lock"), () => {
611
+ const temporary = join(home, "hosted-device.json.tmp");
612
+ writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
613
+ renameSync(temporary, join(home, "hosted-device.json"));
614
+ });
615
+ removeHostedAppData(home, { keepBlobs: true });
616
+ return {
617
+ state: "ready",
618
+ device
619
+ };
620
+ }
621
+ if (mode === "park") {
622
+ const outcome = teardownOwnedIosSim(device.udid);
623
+ if (outcome.status !== "torn-down" || inventory().find((sim) => sim.udid === device.udid)?.state !== "Shutdown") throw new Error(outcome.reason ?? "Hosted simulator shutdown could not be verified.");
624
+ removeHostedAppData(home, { keepBlobs: true });
625
+ return {
626
+ state: "parked",
627
+ device
628
+ };
629
+ }
206
630
  if (mode === "install") {
207
631
  if (!app || current?.state !== "Booted") throw new Error("Hosted app installation requires its booted owned simulator.");
208
632
  const launched = await installHostedApp(home, app.session, app.attempt, device, app.metroPort);
@@ -212,97 +636,33 @@ async function runHostedDevice(mode, selectors, app) {
212
636
  launched
213
637
  };
214
638
  }
215
- if (!current) return {
216
- state: "stopped",
217
- device
218
- };
219
- const outcome = teardownOwnedIosSim(device.udid);
639
+ if (ledger === "empty") {
640
+ if (current) throw new Error("The hosted simulator remains without ledger ownership; it was kept.");
641
+ removeHostedAppData(home);
642
+ return {
643
+ state: "stopped",
644
+ device
645
+ };
646
+ }
647
+ const outcome = teardownOwnedIosSim(device.udid, { del: true });
220
648
  if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted simulator teardown was not established.");
221
649
  const after = inventory().find((sim) => sim.udid === device.udid);
222
- if (after && after.state !== "Shutdown") throw new Error("Hosted simulator shutdown could not be verified.");
650
+ if (after) throw new Error(`Hosted simulator ${device.udid} is still listed (${after.state}); deletion could not be verified. If its runtime is unavailable, delete it with xcrun simctl delete and stop the session again.`);
651
+ forgetCreatedDevice("ios", device.udid);
652
+ removeHostedAppData(home);
223
653
  return {
224
654
  state: "stopped",
225
655
  device
226
656
  };
227
657
  } catch (error) {
228
658
  return {
229
- state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
659
+ state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
230
660
  device,
231
661
  notice: error.message
232
662
  };
233
663
  }
234
664
  }
235
665
  //#endregion
236
- //#region src/device-host/android-app.ts
237
- async function digest(path) {
238
- const hash = createHash("sha256");
239
- for await (const chunk of createReadStream(path)) hash.update(chunk);
240
- return hash.digest("hex");
241
- }
242
- /** Installs only a verified single APK into the session's exact running ledger-owned AVD. */
243
- async function installHostedAndroidApp(home, session, attempt, device) {
244
- const record = readHostedApp(session, attempt, home);
245
- const area = hostedAppArea(session, attempt, home);
246
- if (record.state !== "installing") throw new Error("This app attempt has not been admitted for installation.");
247
- const entry = record.files[0];
248
- if (record.files.length !== 1 || !entry || entry.path !== "App.apk" || entry.kind !== "file") throw new Error("Hosted Android delivery requires a single file entry named App.apk.");
249
- const manifest = join(area, "blobs", record.manifest.sha256);
250
- if (lstatSync(manifest).size !== record.manifest.size || await digest(manifest) !== record.manifest.sha256) throw new Error("The hosted APK manifest differs from its digest.");
251
- const blob = join(area, "blobs", entry.sha256);
252
- const stat = lstatSync(blob);
253
- if (!stat.isFile() || stat.size !== entry.size || await digest(blob) !== entry.sha256) throw new Error("APK content is incomplete or differs from its digest.");
254
- const temporary = join(area, "App.apk.tmp");
255
- const apk = join(area, "App.apk");
256
- copyFileSync(blob, temporary);
257
- renameSync(temporary, apk);
258
- const tool = findAapt();
259
- if (!tool) throw new Error("No installed Android build-tools can inspect the APK.");
260
- const badging = getExecutor().runFile(tool.path, [
261
- "dump",
262
- "badging",
263
- apk
264
- ], {
265
- timeoutMs: 1e4,
266
- killSignal: "SIGKILL"
267
- });
268
- const identity = /^package:\s+name='([^']+)'/m.exec(badging)?.[1];
269
- const minimum = /^(?:sdkVersion|minSdkVersion):'(\d+)'\s*$/m.exec(badging)?.[1];
270
- const api = /^system-images;android-(\d+);/.exec(device.systemImage)?.[1];
271
- if (identity !== record.bundleId || !minimum || !api || Number(minimum) > Number(api)) throw new Error("The APK package identity or minimum SDK is incompatible with the hosted emulator.");
272
- const nativeCode = /^native-code:\s*(.*)$/m.exec(badging)?.[1];
273
- if (nativeCode !== void 0 && !nativeCode.split(/\s+/).includes(`'${device.architecture}'`)) throw new Error("The APK has no native library slice for the hosted emulator architecture.");
274
- const assertTarget = () => {
275
- assertHostedDeviceLedger(home, device.avdName, "android");
276
- const current = readHostedDevice(home, "android");
277
- if (current.avdName !== device.avdName || current.serial !== device.serial || current.systemImage !== device.systemImage || getAvdNameForSerial(device.serial) !== device.avdName || androidDeviceAbi(device.serial) !== device.architecture) throw new Error("The hosted Android device identity or running ABI changed.");
278
- };
279
- assertTarget();
280
- const executor = getExecutor();
281
- const adb = androidToolPath("adb");
282
- const sdkExecutor = {
283
- ...executor,
284
- runFile: (file, args, options) => executor.runFile(file === "adb" ? adb : file, args, options)
285
- };
286
- const installed = installAndroidApp({
287
- serial: device.serial,
288
- apkPath: apk,
289
- packageName: record.bundleId
290
- }, { exec: sdkExecutor });
291
- if (!installed.ok) throw new Error(installed.reason ?? "Hosted APK installation was not established.");
292
- assertTarget();
293
- const launched = launchAndroidReleaseApp({
294
- serial: device.serial,
295
- packageName: record.bundleId
296
- }, { exec: sdkExecutor });
297
- if (!launched.ok) throw new Error(launched.reason ?? "Hosted APK launch was not established.");
298
- if (record.mode === "development") return "unverified";
299
- for (let tries = 0; tries < 10; tries++) {
300
- await new Promise((resolve) => setTimeout(resolve, 200));
301
- if (androidAppProcess(device.serial, record.bundleId, { exec: sdkExecutor })) return true;
302
- }
303
- return "unverified";
304
- }
305
- //#endregion
306
666
  //#region src/device-host/android.ts
307
667
  function portIsOccupied(port) {
308
668
  const devices = listAdbDevices({ timeoutMs: 5e3 });
@@ -327,10 +687,23 @@ async function runHostedAndroidDevice(mode, request, app) {
327
687
  let device = null;
328
688
  let creationStarted = false;
329
689
  try {
330
- if (mode === "install") {
331
- if (!app) throw new Error("Hosted Android installation needs its admitted app attempt.");
690
+ if (mode === "install" || mode === "reverse") {
691
+ if (!app) throw new Error("Hosted Android native work needs its admitted app attempt.");
332
692
  device = readHostedDevice(home, "android");
333
- const launched = await installHostedAndroidApp(home, request.session, app.attempt, device);
693
+ if (!hostedConsolePort(request.consolePort) || device.consolePort !== request.consolePort) throw new Error("The hosted Android device identity differs from its session console port.");
694
+ const metro = app.metroPort && app.clientMetroPort ? {
695
+ bridgePort: app.metroPort,
696
+ devicePort: app.clientMetroPort
697
+ } : void 0;
698
+ if (mode === "reverse") {
699
+ if (!metro) throw new Error("Hosted Android Metro restore needs its bridge and device ports.");
700
+ restoreHostedAndroidMetro(home, request.session, app.attempt, device, metro);
701
+ return {
702
+ state: "ready",
703
+ device
704
+ };
705
+ }
706
+ const launched = await installHostedAndroidApp(home, request.session, app.attempt, device, metro);
334
707
  return {
335
708
  state: "installed",
336
709
  device,
@@ -381,17 +754,88 @@ async function runHostedAndroidDevice(mode, request, app) {
381
754
  };
382
755
  }
383
756
  device = readHostedDevice(home, "android");
384
- assertHostedDeviceLedger(home, device.avdName, "android");
385
- const outcome = teardownOwnedAvd(device.avdName);
386
- if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted Android shutdown was not established.");
757
+ const ledger = assertHostedDeviceLedger(home, device.avdName, "android", { allowEmpty: mode === "stop" });
758
+ if (mode === "inspect" || mode === "adopt") {
759
+ if (device.avdName !== ownedAvdName(`hosted-${request.session}`) || !listAvds({ timeoutMs: 5e3 }).includes(device.avdName)) throw new Error("The parked hosted AVD is missing or names another session.");
760
+ if (!resolveOwnedAvdSerial(device.avdName, { timeoutMs: 5e3 }).notRunning) throw new Error("The parked hosted AVD is missing, not owned or running.");
761
+ assertOwnedAvdStopped(device.avdName);
762
+ if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The parked hosted emulator is still running.");
763
+ if (ownedAvdSystemImage(device.avdName) !== device.systemImage) throw new Error("The parked hosted AVD system image no longer matches its device record.");
764
+ let choice;
765
+ try {
766
+ choice = selectHostedAndroidDevice(request);
767
+ } catch (error) {
768
+ if (mode === "inspect") return {
769
+ state: "incompatible",
770
+ device,
771
+ notice: error.message
772
+ };
773
+ throw error;
774
+ }
775
+ if (choice.systemImage !== device.systemImage || choice.deviceProfile !== device.deviceProfile || choice.architecture !== device.architecture) {
776
+ if (mode === "inspect") return {
777
+ state: "incompatible",
778
+ device
779
+ };
780
+ throw new Error("The parked hosted emulator does not match the requested selectors.");
781
+ }
782
+ if (mode === "inspect") return {
783
+ state: "compatible",
784
+ device
785
+ };
786
+ if (!hostedConsolePort(request.consolePort) || portIsOccupied(request.consolePort)) throw new Error("The selected Android console port is invalid or occupied.");
787
+ device = {
788
+ ...device,
789
+ consolePort: request.consolePort,
790
+ serial: `emulator-${request.consolePort}`
791
+ };
792
+ withDirLock(join(home, "hosted-device.lock"), () => {
793
+ const temporary = join(home, "hosted-device.json.tmp");
794
+ writeFileSync(temporary, JSON.stringify(device), { mode: 384 });
795
+ renameSync(temporary, join(home, "hosted-device.json"));
796
+ });
797
+ bootAndroidEmulator(device.avdName, device.consolePort, {
798
+ openViewer: false,
799
+ logFile: join(home, "emulator.log")
800
+ });
801
+ if (!(await waitForBoot(device.serial, 24e4, { commandTimeoutMs: 5e3 })).ok || getAvdNameForSerial(device.serial) !== device.avdName || androidDeviceAbi(device.serial) !== device.architecture) throw new Error("Hosted Android boot could not be verified for its exact owned AVD.");
802
+ await resetAdoptedAvd(device.avdName, device.serial, "");
803
+ removeHostedAppData(home, { keepBlobs: true });
804
+ return {
805
+ state: "ready",
806
+ device
807
+ };
808
+ }
809
+ if (mode === "park") {
810
+ const outcome = teardownOwnedAvd(device.avdName, {});
811
+ if (outcome.status !== "torn-down") throw new Error(outcome.reason ?? "Hosted Android shutdown was not established.");
812
+ assertOwnedAvdStopped(device.avdName);
813
+ if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
814
+ removeHostedAppData(home, { keepBlobs: true });
815
+ return {
816
+ state: "parked",
817
+ device
818
+ };
819
+ }
820
+ if (ledger === "listed") {
821
+ withConfigLock$1(() => {
822
+ if (!loadConfig$1()) ensureConfig();
823
+ });
824
+ const outcome = teardownOwnedAvd(device.avdName, { del: true });
825
+ if (outcome.status !== "torn-down" && outcome.status !== "missing") throw new Error(outcome.reason ?? "Hosted Android deletion was not established.");
826
+ }
827
+ const name = device.avdName;
828
+ if (listAvds({ timeoutMs: 5e3 }).includes(name) || avdStorageRoots().some((root) => avdPathExists(join(root, `${name}.ini`)) || avdPathExists(join(root, `${name}.avd`)))) throw new Error("The hosted AVD or its data still exists; deletion could not be verified.");
387
829
  if (getAvdNameForSerial(device.serial) === device.avdName) throw new Error("The hosted Android emulator is still running.");
830
+ if (ledger === "listed") forgetCreatedDevice("android", device.avdName);
831
+ removeHostedAppData(home);
388
832
  return {
389
833
  state: "stopped",
390
834
  device
391
835
  };
392
836
  } catch (error) {
393
837
  return {
394
- state: mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
838
+ state: mode === "inspect" ? "unusable" : mode === "prepare" && !creationStarted && !device ? "stopped" : "unknown",
395
839
  device,
396
840
  notice: error.message
397
841
  };
@@ -504,6 +948,7 @@ async function runHostedMacosApp(mode, request, app) {
504
948
  removeAppData(area, join(directory, "blobs"));
505
949
  }
506
950
  }
951
+ removeAppData(area, join(home, "..", "blobs"));
507
952
  return {
508
953
  state: "stopped",
509
954
  device
@@ -667,7 +1112,7 @@ async function runHostedMacosApp(mode, request, app) {
667
1112
  }
668
1113
  //#endregion
669
1114
  //#region src/device-host/offer.ts
670
- function inspectHostedDevice(request) {
1115
+ function inspectHostedDevice(request, options) {
671
1116
  const cpus = availableParallelism();
672
1117
  let workerDiskFreeBytes = null;
673
1118
  try {
@@ -679,7 +1124,8 @@ function inspectHostedDevice(request) {
679
1124
  loadPerCore: loadavg()[1] / cpus,
680
1125
  memoryFreeBytes: freemem(),
681
1126
  memoryPressure: readHostMemoryPressure(),
682
- workerDiskFreeBytes
1127
+ workerDiskFreeBytes,
1128
+ ...options ? { localDevices: countLiveOwnedDevices({}, options) } : {}
683
1129
  };
684
1130
  try {
685
1131
  if (process.platform !== "darwin") throw new Error("Hosted device sessions require a Mac.");
@@ -724,12 +1170,27 @@ async function main() {
724
1170
  "prepare",
725
1171
  "stop",
726
1172
  "install",
727
- "offer"
1173
+ "offer",
1174
+ "count",
1175
+ "logs",
1176
+ "reverse",
1177
+ "park",
1178
+ "inspect",
1179
+ "adopt"
728
1180
  ].includes(String(input.mode))) throw new Error("Invalid hosted worker request.");
1181
+ let exclude;
1182
+ if (input.exclude !== void 0) {
1183
+ if (!Array.isArray(input.exclude) || input.exclude.some((device) => !isJsonObject(device) || device.platform !== "ios" && device.platform !== "android" || device.key !== void 0 && typeof device.key !== "string")) throw new Error("Invalid hosted device exclusions.");
1184
+ exclude = input.exclude;
1185
+ }
1186
+ if (input.mode === "count") {
1187
+ process.stdout.write(`${JSON.stringify(countLiveOwnedDevices({}, { exclude }))}\n`);
1188
+ return;
1189
+ }
729
1190
  if (input.mode === "offer") {
730
1191
  const request = parseHostedOfferRequest(input);
731
1192
  if (!request) throw new Error("Invalid hosted offer selectors.");
732
- process.stdout.write(`${JSON.stringify(inspectHostedDevice(request))}\n`);
1193
+ process.stdout.write(`${JSON.stringify(inspectHostedDevice(request, exclude ? { exclude } : void 0))}\n`);
733
1194
  return;
734
1195
  }
735
1196
  if (process.platform !== "darwin") {
@@ -740,9 +1201,21 @@ async function main() {
740
1201
  })}\n`);
741
1202
  return;
742
1203
  }
743
- if (input.mode === "install" && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
744
- if (input.metroPort !== void 0 && (typeof input.metroPort !== "number" || !Number.isInteger(input.metroPort) || input.metroPort < 1 || input.metroPort > 65535)) throw new Error("Invalid hosted Metro port.");
1204
+ if ((input.mode === "install" || input.mode === "logs" || input.mode === "reverse") && (typeof input.session !== "string" || !/^[a-f0-9-]{36}$/.test(input.session) || !hostedAppAttempt(input.attempt))) throw new Error("Invalid hosted app request.");
1205
+ for (const port of [input.metroPort, input.clientMetroPort]) if (port !== void 0 && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("Invalid hosted Metro port.");
1206
+ if (input.mode === "reverse" && input.platform !== "android") throw new Error("Metro reverse requires Android.");
1207
+ if (input.platform === "macos" && [
1208
+ "park",
1209
+ "inspect",
1210
+ "adopt"
1211
+ ].includes(String(input.mode))) throw new Error("Hosted parking requires iOS or Android.");
745
1212
  if (input.platform === "macos" && !hostedMacosAppSlot(input.appSlot)) throw new Error("Invalid hosted macOS app slot.");
1213
+ if (input.mode === "logs") {
1214
+ if (input.platform !== "ios" && input.platform !== "android" || !process.env.STIM_HOME || typeof input.since !== "number" || !Number.isFinite(input.since)) throw new Error("Invalid hosted native log request.");
1215
+ const more = (input.platform === "android" ? collectHostedAndroidLogs : collectHostedIosLogs)(process.env.STIM_HOME, input.session, input.attempt, input.since, input.final === true);
1216
+ process.stdout.write(`${JSON.stringify({ more })}\n`);
1217
+ return;
1218
+ }
746
1219
  const result = input.platform === "macos" ? await runHostedMacosApp(input.mode, {
747
1220
  session: typeof input.session === "string" ? input.session : "",
748
1221
  appSlot: input.appSlot
@@ -751,7 +1224,11 @@ async function main() {
751
1224
  consolePort: input.consolePort,
752
1225
  ...typeof input.systemImage === "string" ? { systemImage: input.systemImage } : {},
753
1226
  ...typeof input.deviceProfile === "string" ? { deviceProfile: input.deviceProfile } : {}
754
- }, input.mode === "install" ? { attempt: input.attempt } : void 0) : await runHostedDevice(input.mode, {
1227
+ }, input.mode === "install" || input.mode === "reverse" ? {
1228
+ attempt: input.attempt,
1229
+ metroPort: input.metroPort,
1230
+ clientMetroPort: input.clientMetroPort
1231
+ } : void 0) : await runHostedDevice(input.mode, {
755
1232
  ...typeof input.deviceType === "string" ? { deviceType: input.deviceType } : {},
756
1233
  ...typeof input.runtime === "string" ? { runtime: input.runtime } : {}
757
1234
  }, input.mode === "install" ? {