@deeeed/metamask-harness 0.74.0 → 0.76.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 (96) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/adapters/shared/reap-checkout-metros.sh +9 -0
  5. package/dist/action-values.js +15 -0
  6. package/dist/adapters/core/actions.js +20 -0
  7. package/dist/adapters/core/live-script-env.js +36 -0
  8. package/dist/adapters/core/overlay.js +34 -0
  9. package/dist/adapters/core/readiness.js +8 -0
  10. package/dist/adapters/core/run.js +54 -0
  11. package/dist/adapters/core/surface.js +12 -1
  12. package/dist/adapters/extension/action-inputs.js +206 -0
  13. package/dist/adapters/extension/actions.js +51 -0
  14. package/dist/adapters/extension/console-capture.js +13 -1
  15. package/dist/adapters/extension/doctor-checks.js +135 -0
  16. package/dist/adapters/extension/fixtures.js +103 -0
  17. package/dist/adapters/extension/launch.js +315 -0
  18. package/dist/adapters/extension/launched-browser.js +51 -0
  19. package/dist/adapters/extension/overlay.js +55 -0
  20. package/dist/adapters/extension/readiness.js +32 -0
  21. package/dist/adapters/extension/reload.js +40 -0
  22. package/dist/adapters/extension/run.js +105 -0
  23. package/dist/adapters/extension/surface.js +31 -3
  24. package/dist/adapters/harness-freshness.js +4 -2
  25. package/dist/adapters/mobile/actions.js +54 -0
  26. package/dist/adapters/mobile/devices.js +21 -0
  27. package/dist/adapters/mobile/doctor-checks.js +40 -0
  28. package/dist/adapters/mobile/fixtures.js +141 -0
  29. package/dist/adapters/mobile/launch.js +192 -0
  30. package/dist/adapters/mobile/lifecycle.js +300 -0
  31. package/dist/adapters/mobile/overlay.js +27 -0
  32. package/dist/adapters/mobile/prepare-target.js +38 -0
  33. package/dist/adapters/mobile/readiness.js +39 -0
  34. package/dist/adapters/mobile/reload.js +15 -0
  35. package/dist/adapters/mobile/run.js +156 -0
  36. package/dist/adapters/mobile/runtime-identity.js +34 -0
  37. package/dist/adapters/mobile/surface.js +39 -3
  38. package/dist/adapters/mobile/ui.js +701 -0
  39. package/dist/adapters/performance/cdp-trace.js +24 -6
  40. package/dist/adapters/performance/device-platform.js +10 -0
  41. package/dist/adapters/registry.js +1 -1
  42. package/dist/adapters/terminal/actions.js +43 -0
  43. package/dist/adapters/terminal/console-capture.js +14 -1
  44. package/dist/adapters/terminal/doctor-checks.js +22 -0
  45. package/dist/adapters/terminal/overlay.js +18 -0
  46. package/dist/adapters/terminal/readiness.js +11 -0
  47. package/dist/adapters/terminal/run.js +30 -0
  48. package/dist/adapters/terminal/surface.js +21 -3
  49. package/dist/adapters.js +32 -824
  50. package/dist/cli-commands.js +1 -0
  51. package/dist/cli.js +3 -1
  52. package/dist/command-contract.js +3 -0
  53. package/dist/commands/call.js +2 -5
  54. package/dist/commands/debug.js +6 -25
  55. package/dist/commands/device-target.js +2 -1
  56. package/dist/commands/doctor.js +26 -137
  57. package/dist/commands/fixtures.js +13 -217
  58. package/dist/commands/launch/device.js +2 -6
  59. package/dist/commands/launch/extension.js +3 -303
  60. package/dist/commands/launch/index.js +1 -1
  61. package/dist/commands/launch/mobile.js +2 -190
  62. package/dist/commands/launch/quote.js +7 -0
  63. package/dist/commands/logs.js +1 -1
  64. package/dist/commands/parse-args.js +9 -4
  65. package/dist/commands/pin.js +226 -0
  66. package/dist/commands/prepare.js +23 -53
  67. package/dist/commands/provision.js +3 -7
  68. package/dist/commands/reload.js +8 -47
  69. package/dist/commands/run-engine.js +38 -138
  70. package/dist/commands/run-report.js +4 -48
  71. package/dist/commands/run.js +6 -48
  72. package/dist/commands/runtime-decision.js +5 -27
  73. package/dist/commands/self-test.js +1 -3
  74. package/dist/commands/status.js +20 -28
  75. package/dist/commands/stop.js +1 -1
  76. package/dist/doctor.js +12 -120
  77. package/dist/execution-provenance.js +3 -2
  78. package/dist/harness-pin.js +10 -0
  79. package/dist/harness.js +31 -100
  80. package/dist/heal-bounds.js +3 -2
  81. package/dist/live-adapter-contract.js +8 -34
  82. package/dist/manifest.js +3 -5
  83. package/dist/metamask-action-validation.js +5 -196
  84. package/dist/mm-harness-cli.js +30 -4
  85. package/dist/network-observation.js +10 -3
  86. package/dist/paths.js +8 -2
  87. package/dist/performance-observation.js +15 -22
  88. package/dist/pr-body/render.js +3 -1
  89. package/dist/recording-target.js +4 -39
  90. package/dist/run-diagnostics.js +21 -60
  91. package/dist/run-recording.js +5 -4
  92. package/dist/runner.js +20 -368
  93. package/dist/runtime-context.js +12 -17
  94. package/docs/CONTRIBUTING.md +29 -7
  95. package/package.json +2 -1
  96. package/scripts/completions.sh +2 -1
@@ -0,0 +1,192 @@
1
+ import fs from "node:fs";
2
+ import { applyDeviceTargeting } from "../../commands/device-target.js";
3
+ import { launchMobile } from "../../commands/launch/mobile.js";
4
+ import { shellQuote } from "../../commands/launch/quote.js";
5
+ import { EXIT, str, writeInteractiveProgress } from "../../commands/shared.js";
6
+ import { RECOVERY_CODE } from "../../heal-bounds.js";
7
+ import { recipeRuntimePath } from "../../paths.js";
8
+ function mobileProvisioningBlocked(output) {
9
+ return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
10
+ }
11
+ function missingConfiguredIosSimulator(output) {
12
+ return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
13
+ }
14
+ function ambiguousConfiguredIosSimulator(output) {
15
+ return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
16
+ }
17
+ function mobileBridgeTargetMissing(output) {
18
+ return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
19
+ }
20
+ function mobileProvisionCommand(target, mobileTarget) {
21
+ const platform = mobileTarget === "android" ? "android" : "ios";
22
+ const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
23
+ const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
24
+ if (device) parts.push("--device", shellQuote(device));
25
+ return parts.join(" ");
26
+ }
27
+ function isIosSimulatorUdid(value) {
28
+ return /^[0-9A-Fa-f-]{36}$/u.test(value);
29
+ }
30
+ function launchDeviceLabel(mobileTarget) {
31
+ if (mobileTarget === "android") {
32
+ return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
33
+ }
34
+ if (mobileTarget === "ios") {
35
+ return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
36
+ }
37
+ return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
38
+ }
39
+ function metroLaunchEvidence(target) {
40
+ const evidencePath = recipeRuntimePath(target, "metro-generation.json");
41
+ let descriptor;
42
+ try {
43
+ descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
44
+ const stat = fs.fstatSync(descriptor);
45
+ if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
46
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
47
+ } catch {
48
+ return void 0;
49
+ } finally {
50
+ if (descriptor !== void 0) fs.closeSync(descriptor);
51
+ }
52
+ }
53
+ const mobileLaunchProfile = {
54
+ refuseFlags({ mobileTarget }) {
55
+ if (!mobileTarget) {
56
+ return { message: "target is required for mobile.", userAction: "mm-harness launch ios or mm-harness launch android" };
57
+ }
58
+ return void 0;
59
+ },
60
+ applyEnv({ adapter, options, mobileTarget, target }) {
61
+ const device = str(options, "device");
62
+ if (!device && mobileTarget !== "ios" && mobileTarget !== "android") return void 0;
63
+ const targetingOptions = {
64
+ ...options,
65
+ ...mobileTarget ? { platform: mobileTarget } : {}
66
+ };
67
+ const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
68
+ if ("code" in result) {
69
+ if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
70
+ process.env.IOS_SIMULATOR = device;
71
+ process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
72
+ process.env.ADB_SERIAL = "";
73
+ process.env.ANDROID_SERIAL = "";
74
+ process.env.ANDROID_DEVICE = "";
75
+ process.env.ANDROID_TARGET_DEVICE_NAME = "";
76
+ } else {
77
+ return result;
78
+ }
79
+ }
80
+ if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
81
+ return {
82
+ ok: false,
83
+ code: "DEVICE_WRONG_PLATFORM",
84
+ message: `--device ${device} did not resolve to an Android device for launch android.`,
85
+ userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
86
+ };
87
+ }
88
+ if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
89
+ return {
90
+ ok: false,
91
+ code: "DEVICE_WRONG_PLATFORM",
92
+ message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
93
+ userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
94
+ };
95
+ }
96
+ return void 0;
97
+ },
98
+ start({ options, mobileTarget, machine, tier, wantClearMetro }) {
99
+ const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
100
+ const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
101
+ writeInteractiveProgress(
102
+ machine,
103
+ `\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
104
+ );
105
+ },
106
+ nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
107
+ rebuildCommand: ({ mobileTarget }) => `mm-harness launch ${mobileTarget} --build`,
108
+ compose: ({ target, mobileTarget, machine, remoteFlagRequest }, tier, restartApp, clearMetro) => launchMobile(target, mobileTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags),
109
+ classify(_launch, output) {
110
+ if (output.includes("open-device: Android device is locked.")) {
111
+ return {
112
+ code: "MOBILE_DEVICE_LOCKED",
113
+ message: "the selected Android device is locked.",
114
+ recoverable: false,
115
+ userAction: "Unlock the selected Android device, then retry the launch.",
116
+ exitCode: EXIT.runtime,
117
+ originalError: output.trim()
118
+ };
119
+ }
120
+ if (/open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(output)) {
121
+ return {
122
+ code: "MOBILE_DEVICE_UI_UNAVAILABLE",
123
+ message: "the selected Android display could not be prepared.",
124
+ recoverable: false,
125
+ userAction: "Check adb access and the selected device screen, then retry the launch.",
126
+ exitCode: EXIT.runtime,
127
+ originalError: output.trim()
128
+ };
129
+ }
130
+ return void 0;
131
+ },
132
+ async beforeHeal(launch, attempt, state) {
133
+ const { target, mobileTarget, tier } = launch;
134
+ const output = attempt.output;
135
+ const missingIosSimulator = missingConfiguredIosSimulator(output);
136
+ const ambiguousIosSimulator = ambiguousConfiguredIosSimulator(output);
137
+ if (ambiguousIosSimulator) {
138
+ return launch.fail(state, {
139
+ code: "MOBILE_DEVICE_AMBIGUOUS",
140
+ message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
141
+ recoverable: false,
142
+ userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
143
+ exitCode: EXIT.runtime,
144
+ originalError: output.trim() || void 0
145
+ });
146
+ }
147
+ if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
148
+ return launch.fail(state, {
149
+ code: "MOBILE_DEVICE_NOT_FOUND",
150
+ message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
151
+ recoverable: false,
152
+ userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
153
+ exitCode: EXIT.runtime,
154
+ originalError: output.trim() || void 0
155
+ });
156
+ }
157
+ if (mobileProvisioningBlocked(output)) {
158
+ return launch.fail(state, {
159
+ code: "MOBILE_PROVISION_REQUIRED",
160
+ message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
161
+ recoverable: false,
162
+ userAction: mobileProvisionCommand(target, mobileTarget),
163
+ exitCode: EXIT.runtime,
164
+ originalError: output.trim() || void 0
165
+ });
166
+ }
167
+ if (tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(output)) {
168
+ return launch.fail(state, {
169
+ code: "MOBILE_NATIVE_BUILD_FAILED",
170
+ message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
171
+ recoverable: false,
172
+ exitCode: EXIT.runtime,
173
+ originalError: output.trim() || void 0
174
+ });
175
+ }
176
+ return void 0;
177
+ },
178
+ recovery(_launch, output) {
179
+ const restartApp = mobileBridgeTargetMissing(output);
180
+ return { code: restartApp ? "mobile.app-restarted" : RECOVERY_CODE.mobile, restartApp };
181
+ },
182
+ // Mobile proved its set inside launchMobile (null: nothing requested and no
183
+ // runtime answered).
184
+ provenFlags: (_launch, requested, proven) => ({ flags: proven === void 0 ? requested : proven }),
185
+ verifyArgs: ({ mobileTarget }) => mobileTarget ? ["--platform", mobileTarget] : [],
186
+ platformLabel: ({ mobileTarget }) => mobileTarget ?? null,
187
+ deviceLabel: ({ mobileTarget }) => launchDeviceLabel(mobileTarget),
188
+ evidence: metroLaunchEvidence
189
+ };
190
+ export {
191
+ mobileLaunchProfile
192
+ };
@@ -0,0 +1,300 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ bridgeCommand,
4
+ MOBILE_BRIDGE_ERROR_CODES
5
+ } from "../../../library/actions/mobile/platform/bridge.mjs";
6
+ import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
7
+ import {
8
+ MOBILE_LIFECYCLE_DEADLINE_FIELD,
9
+ MOBILE_LIFECYCLE_PREPARED
10
+ } from "../../app-lifecycle.js";
11
+ import {
12
+ mobileSourceFingerprint,
13
+ recordMobileSourceBaseline
14
+ } from "./source-freshness.js";
15
+ function mobileSourceAwareLifecycleAdapters(lifecycle, readinessProbe = probeMobileLifecycleRuntime, sourceFreshness = {
16
+ fingerprint: mobileSourceFingerprint,
17
+ record: recordMobileSourceBaseline
18
+ }, processIdentity = {
19
+ readIosPid: readIosAppPid,
20
+ readAndroidPid: readAndroidAppPid
21
+ }, runtimeMarker = readMobileLifecycleRuntimeIdentity) {
22
+ return lifecycle.map((entry) => ({
23
+ ...entry,
24
+ async execute(node, context) {
25
+ const command = node.command ?? node.event ?? node.state;
26
+ const opaqueRuntime = context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
27
+ const requiresProcessContinuity = command !== "restart" && node.require_process_continuity === true;
28
+ if (opaqueRuntime && !requiresProcessContinuity) {
29
+ return entry.execute(node, context);
30
+ }
31
+ const reloadsSource = !opaqueRuntime && (command === "launch" || command === "foreground" || command === "restart");
32
+ const recordsSource = !opaqueRuntime && command === "restart";
33
+ const androidLifecycle = isAndroidLifecycle(node, context.env ?? {});
34
+ const checksIosContinuity = command === "foreground" && !androidLifecycle;
35
+ const checksRequiredContinuity = requiresProcessContinuity;
36
+ const recordsProcessIdentity = checksIosContinuity || checksRequiredContinuity;
37
+ const readProcessId = androidLifecycle ? processIdentity.readAndroidPid ?? readAndroidAppPid : processIdentity.readIosPid;
38
+ const processBefore = recordsProcessIdentity ? readProcessId(node, context) : void 0;
39
+ const fingerprint = recordsSource ? sourceFreshness.fingerprint(context.projectRoot) : void 0;
40
+ const previousRuntimeIdentity = recordsSource ? await runtimeMarker(node, context) : void 0;
41
+ const androidRestart = command === "restart" && androidLifecycle;
42
+ const initialResult = await entry.execute(node, context);
43
+ const readinessDeadline = reloadsSource ? mobileLifecycleReadinessDeadline(command, node) : void 0;
44
+ let result = initialResult;
45
+ let runtimeReady = false;
46
+ if (androidRestart) {
47
+ try {
48
+ const remainingMs = Math.max(
49
+ 1,
50
+ (readinessDeadline ?? Date.now()) - Date.now()
51
+ );
52
+ const handoffDeadline = Math.min(
53
+ readinessDeadline ?? Number.POSITIVE_INFINITY,
54
+ Date.now() + Math.min(1e4, Math.max(1, remainingMs / 2))
55
+ );
56
+ await readinessProbe(
57
+ node,
58
+ context,
59
+ previousRuntimeIdentity,
60
+ handoffDeadline
61
+ );
62
+ runtimeReady = true;
63
+ } catch (error) {
64
+ if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
65
+ throw error;
66
+ }
67
+ const foregroundTimeoutMs = Math.max(
68
+ 1,
69
+ (readinessDeadline ?? Date.now() + 1) - Date.now()
70
+ );
71
+ const foregroundResult = await entry.execute(
72
+ {
73
+ ...node,
74
+ command: "foreground",
75
+ settle_ms: 0,
76
+ timeout_ms: foregroundTimeoutMs,
77
+ [MOBILE_LIFECYCLE_DEADLINE_FIELD]: readinessDeadline,
78
+ [MOBILE_LIFECYCLE_PREPARED]: true
79
+ },
80
+ context
81
+ );
82
+ result = mergeAndroidRestartResults(initialResult, foregroundResult);
83
+ }
84
+ }
85
+ if (reloadsSource && !runtimeReady) {
86
+ if (readinessDeadline !== void 0 && Date.now() >= readinessDeadline) {
87
+ throw new Error(
88
+ `Mobile lifecycle ${String(command)} exhausted its runtime readiness deadline before recovery completed.`
89
+ );
90
+ }
91
+ await readinessProbe(
92
+ node,
93
+ context,
94
+ previousRuntimeIdentity,
95
+ readinessDeadline
96
+ );
97
+ }
98
+ if (recordsProcessIdentity) {
99
+ const processAfter = readProcessId(node, context);
100
+ const processPreserved = processBefore !== null && processBefore === processAfter;
101
+ const actualTransition = processPreserved ? command === "background" ? "background_same_process" : "foreground_resume" : processAfter !== null ? "cold_relaunch" : "unknown";
102
+ const processContinuity = processPreserved ? "preserved" : processAfter === null ? "unknown" : "changed";
103
+ const resultRecord = asRecord(result);
104
+ result = {
105
+ ...resultRecord,
106
+ output: {
107
+ ...asRecord(resultRecord.output),
108
+ actualTransition,
109
+ processContinuity,
110
+ processBefore,
111
+ processAfter
112
+ }
113
+ };
114
+ if (checksRequiredContinuity && !processPreserved) {
115
+ const platform = androidLifecycle ? "Android" : "iOS";
116
+ throw new Error(
117
+ `${platform} ${String(command)} did not prove app-process continuity (actualTransition=${actualTransition}, before=${String(processBefore)}, after=${String(processAfter)}); this lifecycle sample cannot be classified as a same-process reconnect.`
118
+ );
119
+ }
120
+ }
121
+ if (fingerprint !== void 0 && !sourceFreshness.record(context.projectRoot, fingerprint)) {
122
+ throw new Error(
123
+ "Mobile source changed while the app was reloading; the loaded-source baseline was not recorded. Next: retry app.lifecycle restart."
124
+ );
125
+ }
126
+ return result;
127
+ }
128
+ }));
129
+ }
130
+ function readAndroidAppPid(node, context) {
131
+ const env = context.env ?? {};
132
+ const device = node.adb_serial ?? node.android_device ?? node.device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
133
+ const packageId = node.package_id ?? node.packageName ?? node.app_id ?? env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID ?? "io.metamask";
134
+ if (typeof device !== "string" || typeof packageId !== "string") return null;
135
+ try {
136
+ const adb = resolveMobileToolPath("adb", { required: true });
137
+ const output = execFileSync(
138
+ adb,
139
+ ["-s", device, "shell", "pidof", packageId],
140
+ { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
141
+ );
142
+ const pid = Number.parseInt(output.trim().split(/\s+/u)[0] ?? "", 10);
143
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function readIosAppPid(node, context) {
149
+ const env = context.env ?? {};
150
+ const device = node.simulator ?? node.ios_simulator ?? env.SIM_UDID ?? env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
151
+ const bundleId = node.bundle_id ?? node.bundleId ?? env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID ?? "io.metamask.MetaMask";
152
+ if (typeof device !== "string" || typeof bundleId !== "string") return null;
153
+ try {
154
+ const output = execFileSync(
155
+ "xcrun",
156
+ ["simctl", "spawn", device, "launchctl", "list"],
157
+ { encoding: "utf8", timeout: 5e3 }
158
+ );
159
+ const labelPrefix = `UIKitApplication:${bundleId}[`;
160
+ const line = output.split("\n").find((candidate) => candidate.includes(labelPrefix));
161
+ if (!line) return null;
162
+ const pid = Number.parseInt(line.trim().split(/\s+/u)[0] ?? "", 10);
163
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ function isAndroidLifecycle(node, env) {
169
+ const platform = node.platform ?? env.PLATFORM ?? process.env.PLATFORM;
170
+ if (platform === "android") return true;
171
+ if (platform === "ios") return false;
172
+ return Boolean(
173
+ node.adb_serial ?? node.android_device ?? env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL
174
+ );
175
+ }
176
+ function mergeAndroidRestartResults(initialResult, foregroundResult) {
177
+ const initial = asRecord(initialResult);
178
+ const foreground = asRecord(foregroundResult);
179
+ const initialOutput = asRecord(initial.output);
180
+ const foregroundOutput = asRecord(foreground.output);
181
+ return {
182
+ ...initial,
183
+ output: {
184
+ ...initialOutput,
185
+ command: "restart",
186
+ calls: [
187
+ ...Array.isArray(initialOutput.calls) ? initialOutput.calls : [],
188
+ ...Array.isArray(foregroundOutput.calls) ? foregroundOutput.calls : []
189
+ ],
190
+ settleMs: foregroundOutput.settleMs ?? initialOutput.settleMs ?? 0,
191
+ foregroundEnsured: true
192
+ }
193
+ };
194
+ }
195
+ async function probeMobileLifecycleRuntime(node, context, previousRuntimeIdentity, deadlineMs) {
196
+ const command = node.command ?? node.event ?? node.state;
197
+ const timeoutMs = resolveMobileLifecycleReadinessTimeout(
198
+ command,
199
+ node.runtime_ready_timeout_ms
200
+ );
201
+ const startedAt = Date.now();
202
+ const deadline = deadlineMs ?? startedAt + timeoutMs;
203
+ let lastError;
204
+ while (Date.now() < deadline) {
205
+ try {
206
+ const status = await bridgeCommand(
207
+ {
208
+ action: "app.lifecycle",
209
+ node: {
210
+ ...node,
211
+ cdp_timeout_ms: Math.min(8e3, Math.max(1, deadline - Date.now()))
212
+ },
213
+ context
214
+ },
215
+ ["status-selected"]
216
+ );
217
+ if (status?.agenticPresent === true && status.route && (!previousRuntimeIdentity || status.runtimeIdentity !== previousRuntimeIdentity)) {
218
+ return status;
219
+ }
220
+ lastError = new Error(
221
+ previousRuntimeIdentity && status?.runtimeIdentity === previousRuntimeIdentity ? "retiring Mobile runtime is still selected" : "selected Mobile runtime is not ready"
222
+ );
223
+ } catch (error) {
224
+ lastError = error;
225
+ }
226
+ const remainingMs = deadline - Date.now();
227
+ if (remainingMs > 0) {
228
+ await new Promise(
229
+ (resolve) => setTimeout(resolve, Math.min(1e3, remainingMs))
230
+ );
231
+ }
232
+ }
233
+ throw new Error(
234
+ `Mobile lifecycle ${String(command)} did not expose a usable pinned __AGENTIC__ runtime within ${Math.max(0, deadline - startedAt)}ms: ${lastError instanceof Error ? lastError.message : String(lastError)}`
235
+ );
236
+ }
237
+ async function readMobileLifecycleRuntimeIdentity(node, context) {
238
+ const deadline = Date.now() + 5e3;
239
+ let lastError;
240
+ do {
241
+ try {
242
+ const status = await bridgeCommand(
243
+ {
244
+ action: "app.lifecycle",
245
+ node: {
246
+ ...node,
247
+ cdp_timeout_ms: Math.min(1e3, Math.max(1, deadline - Date.now()))
248
+ },
249
+ context
250
+ },
251
+ ["target-identity-selected"]
252
+ );
253
+ if (typeof status?.runtimeIdentity === "string") {
254
+ return status.runtimeIdentity;
255
+ }
256
+ lastError = new Error("selected Mobile runtime has no stable identity");
257
+ } catch (error) {
258
+ if (error instanceof Error && "code" in error && error.code === MOBILE_BRIDGE_ERROR_CODES.NO_TARGET) {
259
+ return void 0;
260
+ }
261
+ lastError = error;
262
+ }
263
+ const remainingMs = deadline - Date.now();
264
+ if (remainingMs <= 0) {
265
+ break;
266
+ }
267
+ await new Promise(
268
+ (resolve) => setTimeout(resolve, Math.min(250, remainingMs))
269
+ );
270
+ } while (Date.now() < deadline);
271
+ throw new Error(
272
+ `Mobile lifecycle restart could not identify the retiring runtime: ${lastError instanceof Error ? lastError.message : String(lastError)}`
273
+ );
274
+ }
275
+ function mobileLifecycleReadinessDeadline(command, node) {
276
+ const timeoutMs = resolveMobileLifecycleReadinessTimeout(
277
+ command,
278
+ node.runtime_ready_timeout_ms
279
+ );
280
+ return Date.now() + timeoutMs;
281
+ }
282
+ function resolveMobileLifecycleReadinessTimeout(command, value) {
283
+ if (value === void 0) {
284
+ return command === "launch" || command === "restart" || command === "foreground" ? 6e4 : 3e4;
285
+ }
286
+ const timeout = Number(value);
287
+ if (!Number.isInteger(timeout) || timeout < 1e3 || timeout > 12e4) {
288
+ throw new Error(
289
+ "app.lifecycle.runtime_ready_timeout_ms must be an integer from 1000 through 120000."
290
+ );
291
+ }
292
+ return timeout;
293
+ }
294
+ function asRecord(value) {
295
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
296
+ }
297
+ export {
298
+ mobileSourceAwareLifecycleAdapters,
299
+ resolveMobileLifecycleReadinessTimeout
300
+ };
@@ -0,0 +1,27 @@
1
+ import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, readRuntimeContextTarget } from "./prepare-target.js";
2
+ const mobileHarness = {
3
+ install: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
4
+ cleanup: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh", fromInstalledRunner: true },
5
+ // Runner-owned typed code, not a shipped leaf: no installed overlay copy can shadow it.
6
+ verify: () => ({ inProcess: async () => (await import("./verify.js")).runMobileVerify }),
7
+ restart: "mm-harness launch ios # or launch android"
8
+ };
9
+ const mobileRuntimeContext = {
10
+ forbiddenFields: ["cdpPort", "extensionId"],
11
+ localPorts: { watcher: 4e4 },
12
+ resources: ({ watcherPort, existing }) => ({
13
+ metroPort: watcherPort,
14
+ simulator: process.env.IOS_SIMULATOR || existing.simulator,
15
+ simulatorUdid: process.env.SIM_UDID || existing.simulatorUdid,
16
+ adbSerial: process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial
17
+ })
18
+ };
19
+ const mobilePrepare = {
20
+ clearMetro: true,
21
+ devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices)
22
+ };
23
+ export {
24
+ mobileHarness,
25
+ mobilePrepare,
26
+ mobileRuntimeContext
27
+ };
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recipeRuntimeDir } from "../../paths.js";
4
+ function readRuntimeContextTarget(target) {
5
+ const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
6
+ let parsed;
7
+ try {
8
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
9
+ } catch {
10
+ return {};
11
+ }
12
+ const record = parsed ?? {};
13
+ const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
14
+ return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
15
+ }
16
+ function mobilePlatformFromConfiguredTarget(env, context) {
17
+ const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
18
+ const android = Boolean(
19
+ env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
20
+ );
21
+ if (ios === android) return void 0;
22
+ return ios ? "ios" : "android";
23
+ }
24
+ function mobilePlatformFromDevices(devices) {
25
+ const selected = devices.filter((device) => device?.selected === true);
26
+ const candidates = selected.length > 0 ? selected : devices;
27
+ const platforms = [
28
+ ...new Set(
29
+ candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
30
+ )
31
+ ];
32
+ return platforms.length === 1 ? platforms[0] : void 0;
33
+ }
34
+ export {
35
+ mobilePlatformFromConfiguredTarget,
36
+ mobilePlatformFromDevices,
37
+ readRuntimeContextTarget
38
+ };
@@ -0,0 +1,39 @@
1
+ import { optionFlag, optionString, parsePort } from "../../commands/parse-args.js";
2
+ import { detectOrphanMetros, mobileToolDoctorChecks } from "./doctor-checks.js";
3
+ import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "./perps-env.js";
4
+ const mobileReadiness = {
5
+ compatibilityMode(_target, shape) {
6
+ if (shape.injectedHarness && shape.agenticService) return "runner bridge with injected app bridge";
7
+ if (shape.agenticService) return "runner bridge with app bridge";
8
+ return "runner bridge available; app bridge not installed";
9
+ },
10
+ checks: (_target, platform) => mobileToolDoctorChecks(platform === "ios" || platform === "android" ? platform : void 0),
11
+ environment: (target) => ({ mobilePerps: mobilePerpsEnvironment(target) }),
12
+ lines(target) {
13
+ const perps = mobilePerpsEnvironment(target);
14
+ return [
15
+ `mobile-perps: ${perps.status} (${perps.file}; MM_PERPS_ENABLED=${perps.perpsEnabled.value ?? "missing"}; OVERRIDE_REMOTE_FEATURE_FLAGS=${perps.remoteFeatureFlagOverride.value ?? "missing"})`
16
+ ];
17
+ },
18
+ orphanDevServers: detectOrphanMetros,
19
+ captureProviders: ["simctl", "adb"],
20
+ fixes: [{ id: "mobile-perps-env", apply: (target) => ensureMobilePerpsEnvironment(target).length > 0 }],
21
+ devices: async () => (await import("./devices.js")).mobileDevices
22
+ };
23
+ async function mobileRuntimeDecision(target, options) {
24
+ const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
25
+ const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
26
+ const { decideMobileReadiness } = await import("./runtime-decision.js");
27
+ const report = await decideMobileReadiness(target, {
28
+ watcherPort,
29
+ metroLog: optionString(options, "metroLog"),
30
+ platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
31
+ record: optionFlag(options, "record"),
32
+ preflightMode: optionString(options, "preflightMode")
33
+ });
34
+ return { report, line: `${report.decision} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}` };
35
+ }
36
+ export {
37
+ mobileReadiness,
38
+ mobileRuntimeDecision
39
+ };
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ import { EXIT, spawnScript } from "../../commands/shared.js";
3
+ import { runnerDir } from "../../paths.js";
4
+ function reloadMobile(target, json) {
5
+ const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
6
+ const args = [];
7
+ if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
8
+ if (json) args.push("--json");
9
+ const result = spawnScript(process.execPath, [script, ...args], target, json);
10
+ if (json) process.stdout.write(result.output);
11
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
12
+ }
13
+ export {
14
+ reloadMobile
15
+ };