@deeeed/metamask-harness 0.3.9 → 0.5.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.
- package/CHANGELOG.md +45 -1
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +315 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/slot-ports.js +146 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/ADAPTER-SURFACE.md +119 -0
- package/docs/CLI-SPEC.md +26 -3
- package/docs/UX-PRINCIPLES.md +3 -0
- package/package.json +10 -2
- package/src/adapters/core/surface.ts +71 -0
- package/src/adapters/extension/surface.ts +88 -0
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/surface.ts +71 -0
- package/src/adapters/slot-ports.ts +165 -0
- package/src/adapters/surface.ts +117 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +239 -49
- package/src/commands/debug.ts +3 -1
- package/src/commands/fixtures.ts +13 -8
- package/src/commands/launch.ts +7 -156
- package/src/commands/logs.ts +29 -13
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +71 -18
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
clearDecisionState,
|
|
6
|
+
depsCheck,
|
|
7
|
+
recordDepsBaseline
|
|
8
|
+
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
9
|
+
import {
|
|
10
|
+
analyzeBundleLog,
|
|
11
|
+
evaluatePersistentBundleError,
|
|
12
|
+
supersededErrorCapture
|
|
13
|
+
} from "@farmslot/recipe-harness/runtime/log-analysis";
|
|
14
|
+
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
15
|
+
import { recipeRuntimePath } from "../../paths.js";
|
|
16
|
+
import { mobileProductMarkers } from "./deps-markers.js";
|
|
17
|
+
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
18
|
+
const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u;
|
|
19
|
+
const NATIVE_MODULE_STALE = /\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
|
|
20
|
+
function resolveMetroLog(target, metroLog) {
|
|
21
|
+
const abs = metroLog ? path.isAbsolute(metroLog) ? metroLog : path.join(target, metroLog) : recipeRuntimePath(target, "metro.log");
|
|
22
|
+
return fs.existsSync(abs) ? abs : null;
|
|
23
|
+
}
|
|
24
|
+
function isMetroLogStale(target, logAbs) {
|
|
25
|
+
try {
|
|
26
|
+
const logMtime = fs.statSync(logAbs).mtimeMs;
|
|
27
|
+
const INSTALL_MARKERS = ["node_modules/.yarn-state.yml", ".yarn/install-state.gz"];
|
|
28
|
+
return INSTALL_MARKERS.some((rel) => {
|
|
29
|
+
try {
|
|
30
|
+
return logMtime < fs.statSync(path.join(target, rel)).mtimeMs;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function metroLogCheck(target, metroLog) {
|
|
40
|
+
const abs = resolveMetroLog(target, metroLog);
|
|
41
|
+
if (!abs) return { status: "no-log" };
|
|
42
|
+
if (isMetroLogStale(target, abs)) return { status: "no-log" };
|
|
43
|
+
const logText = fs.readFileSync(abs, "utf8");
|
|
44
|
+
return analyzeBundleLog({
|
|
45
|
+
target,
|
|
46
|
+
logText,
|
|
47
|
+
errorPattern: BUNDLE_ERR,
|
|
48
|
+
okPattern: BUNDLE_OK
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function missingRequiredDeps(target) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(target, "package.json"), "utf8"));
|
|
54
|
+
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
55
|
+
return Object.keys(all).filter(
|
|
56
|
+
(name) => !fs.existsSync(path.join(target, "node_modules", name))
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const installActions = (target) => [
|
|
63
|
+
{ id: "yarn-setup", argv: ["yarn", "setup"], cwd: target }
|
|
64
|
+
];
|
|
65
|
+
function appRunningOnDevice(platform) {
|
|
66
|
+
try {
|
|
67
|
+
if (platform === "android") {
|
|
68
|
+
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
69
|
+
const args = serial ? ["-s", serial] : [];
|
|
70
|
+
const out2 = execFileSync("adb", [...args, "shell", "ps", "-A"], {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
73
|
+
timeout: 5e3
|
|
74
|
+
});
|
|
75
|
+
return out2.includes("io.metamask");
|
|
76
|
+
}
|
|
77
|
+
const device = process.env.IOS_SIMULATOR || "booted";
|
|
78
|
+
const out = execFileSync("xcrun", ["simctl", "spawn", device, "launchctl", "list"], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
81
|
+
timeout: 5e3
|
|
82
|
+
});
|
|
83
|
+
return out.toLowerCase().includes("io.metamask");
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const launchActions = (target, clearMetro = false) => {
|
|
89
|
+
const actions = [];
|
|
90
|
+
actions.push({
|
|
91
|
+
id: "start-metro",
|
|
92
|
+
argv: clearMetro ? ["--clear"] : [],
|
|
93
|
+
cwd: target
|
|
94
|
+
});
|
|
95
|
+
actions.push({ id: "prewarm-bundle", cwd: target });
|
|
96
|
+
actions.push({ id: "launch-mobile-runtime" });
|
|
97
|
+
actions.push({ id: "wait-for-bridge", cwd: target });
|
|
98
|
+
return actions;
|
|
99
|
+
};
|
|
100
|
+
async function decideMobileReadiness(target, options = {}) {
|
|
101
|
+
const resolved = path.resolve(target);
|
|
102
|
+
if (options.record) recordDepsBaseline(resolved);
|
|
103
|
+
const fast = options.preflightMode === "fast";
|
|
104
|
+
const report = await computeMobileReadiness(resolved, options, fast);
|
|
105
|
+
if (fast && report.decision === "install") {
|
|
106
|
+
return {
|
|
107
|
+
...report,
|
|
108
|
+
decision: "blocked",
|
|
109
|
+
reasonCode: "deps-not-ready",
|
|
110
|
+
reasons: [
|
|
111
|
+
"Fast preflight found dependencies not ready and does not install them (the orchestrator deps phase owns installation).",
|
|
112
|
+
...report.reasons
|
|
113
|
+
],
|
|
114
|
+
userAction: "run the slot deps/prepare phase; standalone: `yarn setup:expo --no-build-ios --no-build-android` in the checkout, or `mm-harness launch <platform> --build` to install and build",
|
|
115
|
+
actions: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return report;
|
|
119
|
+
}
|
|
120
|
+
async function computeMobileReadiness(resolved, options, fast) {
|
|
121
|
+
const rawDeps = depsCheck(resolved, {
|
|
122
|
+
productMarkers: mobileProductMarkers(options.platform)
|
|
123
|
+
});
|
|
124
|
+
let deps = rawDeps;
|
|
125
|
+
if (rawDeps.status === "stale" && !rawDeps.hasBaseline) {
|
|
126
|
+
deps = { installed: rawDeps.installed, status: "current", hasBaseline: false };
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
"[runtime-decision] manifest is newer than install markers (no recorded baseline); trusting installed node_modules \u2014 run yarn setup manually if deps truly changed.\n"
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (deps.status === "current") {
|
|
132
|
+
const absent = missingRequiredDeps(resolved);
|
|
133
|
+
if (absent.length > 0) {
|
|
134
|
+
deps = { installed: deps.installed, status: "partial", hasBaseline: deps.hasBaseline, missingProducts: absent };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const metroLog = metroLogCheck(resolved, options.metroLog);
|
|
138
|
+
const metro = options.watcherPort ? await probeMetroPackager(options.watcherPort) : { status: "skipped" };
|
|
139
|
+
if (metroLog.status === "ok") {
|
|
140
|
+
clearDecisionState(resolved, "bundle-error-state.json");
|
|
141
|
+
}
|
|
142
|
+
const checks = { deps, metroLog, metro };
|
|
143
|
+
if (deps.status === "missing") {
|
|
144
|
+
return {
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
adapter: "mobile",
|
|
147
|
+
target: resolved,
|
|
148
|
+
decision: "install",
|
|
149
|
+
reasonCode: "deps-missing",
|
|
150
|
+
reasons: ["Dependencies are not installed (no yarn install-state markers)."],
|
|
151
|
+
checks,
|
|
152
|
+
actions: installActions(resolved)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (deps.status === "partial") {
|
|
156
|
+
const missing = deps.missingProducts?.join(", ") ?? "product markers";
|
|
157
|
+
return {
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
adapter: "mobile",
|
|
160
|
+
target: resolved,
|
|
161
|
+
decision: "install",
|
|
162
|
+
reasonCode: "deps-partial",
|
|
163
|
+
reasons: [`node_modules tree is incomplete (missing: ${missing}).`],
|
|
164
|
+
checks,
|
|
165
|
+
actions: installActions(resolved)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (deps.status === "stale") {
|
|
169
|
+
return {
|
|
170
|
+
schemaVersion: 1,
|
|
171
|
+
adapter: "mobile",
|
|
172
|
+
target: resolved,
|
|
173
|
+
decision: "install",
|
|
174
|
+
reasonCode: "deps-stale",
|
|
175
|
+
reasons: ["package.json/yarn.lock changed since the recorded install baseline."],
|
|
176
|
+
checks,
|
|
177
|
+
actions: installActions(resolved)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const metroLogAbs = resolveMetroLog(resolved, options.metroLog);
|
|
181
|
+
const metroLogText = metroLogAbs && !isMetroLogStale(resolved, metroLogAbs) ? fs.readFileSync(metroLogAbs, "utf8") : "";
|
|
182
|
+
const staleNativeModule = supersededErrorCapture(metroLogText, {
|
|
183
|
+
errorPattern: NATIVE_MODULE_STALE,
|
|
184
|
+
okPattern: BUNDLE_OK
|
|
185
|
+
});
|
|
186
|
+
if (staleNativeModule) {
|
|
187
|
+
return {
|
|
188
|
+
schemaVersion: 1,
|
|
189
|
+
adapter: "mobile",
|
|
190
|
+
target: resolved,
|
|
191
|
+
decision: "launch",
|
|
192
|
+
reasonCode: "native-module-stale",
|
|
193
|
+
reasons: [
|
|
194
|
+
`Installed dev client is missing native module "${staleNativeModule}" (JS deps are current). Rebuild the native app (yarn start:ios / yarn start:android or --preflight-mode rebuild-native).`,
|
|
195
|
+
"Metro may report a successful bundle while the installed dev client binary predates the native Nitro module link."
|
|
196
|
+
],
|
|
197
|
+
checks,
|
|
198
|
+
actions: [
|
|
199
|
+
{ id: "rebuild-native-dev-client" },
|
|
200
|
+
...launchActions(resolved, true)
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (metroLog.status === "errors") {
|
|
205
|
+
const depsSatisfied = deps.status === "current";
|
|
206
|
+
if (depsSatisfied && (metroLog.reason === "stale-bundle-error" || metroLog.reason === "bundle-error")) {
|
|
207
|
+
const excerpt = metroLog.excerpt ?? "";
|
|
208
|
+
const persistent = evaluatePersistentBundleError(resolved, excerpt);
|
|
209
|
+
if (persistent.blocked) {
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: 1,
|
|
212
|
+
adapter: "mobile",
|
|
213
|
+
target: resolved,
|
|
214
|
+
decision: "blocked",
|
|
215
|
+
reasonCode: "bundle-error-persistent",
|
|
216
|
+
reasons: [
|
|
217
|
+
"Metro bundle keeps failing with the same error after a cache-cleared relaunch was already suggested; fix the bundle error in app code before retrying recipe up.",
|
|
218
|
+
...excerpt ? [excerpt] : []
|
|
219
|
+
],
|
|
220
|
+
checks,
|
|
221
|
+
actions: []
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
schemaVersion: 1,
|
|
226
|
+
adapter: "mobile",
|
|
227
|
+
target: resolved,
|
|
228
|
+
decision: "launch",
|
|
229
|
+
reasonCode: metroLog.reason === "stale-bundle-error" ? "metro-stale-log" : "bundle-errors-recoverable",
|
|
230
|
+
reasons: [
|
|
231
|
+
metroLog.reason === "stale-bundle-error" ? "Metro log shows prior bundle failures but cited modules are installed; restart Metro with a cleared cache." : "Metro bundle is failing with current deps; restart Metro (clear cache) before relaunching the dev client.",
|
|
232
|
+
...excerpt ? [excerpt] : []
|
|
233
|
+
],
|
|
234
|
+
checks,
|
|
235
|
+
actions: launchActions(resolved, true)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
schemaVersion: 1,
|
|
240
|
+
adapter: "mobile",
|
|
241
|
+
target: resolved,
|
|
242
|
+
decision: "install",
|
|
243
|
+
reasonCode: metroLog.reason === "unresolved-module" ? "deps-unresolved-module" : "bundle-errors",
|
|
244
|
+
reasons: [
|
|
245
|
+
metroLog.reason === "unresolved-module" ? "Metro cannot resolve modules that are absent from node_modules; run yarn setup." : "Metro bundle is failing; reinstall deps or clear Metro cache before retrying.",
|
|
246
|
+
...metroLog.excerpt ? [metroLog.excerpt] : []
|
|
247
|
+
],
|
|
248
|
+
checks,
|
|
249
|
+
actions: installActions(resolved)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (metro.status === "down") {
|
|
253
|
+
return {
|
|
254
|
+
schemaVersion: 1,
|
|
255
|
+
adapter: "mobile",
|
|
256
|
+
target: resolved,
|
|
257
|
+
decision: "launch",
|
|
258
|
+
reasonCode: "metro-down",
|
|
259
|
+
reasons: ["Metro is not reachable; start Metro and launch the dev client."],
|
|
260
|
+
checks,
|
|
261
|
+
actions: launchActions(resolved)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (metroLog.status === "bundling" || metroLog.status === "no-log") {
|
|
265
|
+
return {
|
|
266
|
+
schemaVersion: 1,
|
|
267
|
+
adapter: "mobile",
|
|
268
|
+
target: resolved,
|
|
269
|
+
decision: "launch",
|
|
270
|
+
reasonCode: metroLog.status === "bundling" ? "bundle-in-progress" : "runtime-cold",
|
|
271
|
+
reasons: [
|
|
272
|
+
metroLog.status === "bundling" ? "Metro bundle is still in progress; launch or wait for a successful bundle." : "No successful Metro bundle recorded; start Metro and launch the dev client."
|
|
273
|
+
],
|
|
274
|
+
checks,
|
|
275
|
+
actions: launchActions(resolved)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (metro.status === "up" && metroLog.status === "ok") {
|
|
279
|
+
if (!appRunningOnDevice(options.platform)) {
|
|
280
|
+
return {
|
|
281
|
+
schemaVersion: 1,
|
|
282
|
+
adapter: "mobile",
|
|
283
|
+
target: resolved,
|
|
284
|
+
decision: "launch",
|
|
285
|
+
reasonCode: "app-not-running",
|
|
286
|
+
reasons: ["Metro is healthy but the dev client is not running on the target device; launching it."],
|
|
287
|
+
checks,
|
|
288
|
+
actions: launchActions(resolved)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
adapter: "mobile",
|
|
294
|
+
target: resolved,
|
|
295
|
+
decision: "ready",
|
|
296
|
+
reasonCode: "healthy",
|
|
297
|
+
reasons: ["Dependencies are current, Metro reports a successful bundle, and the dev client is running. Verify bridge before replay."],
|
|
298
|
+
checks,
|
|
299
|
+
actions: []
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
schemaVersion: 1,
|
|
304
|
+
adapter: "mobile",
|
|
305
|
+
target: resolved,
|
|
306
|
+
decision: "launch",
|
|
307
|
+
reasonCode: "runtime-unverified",
|
|
308
|
+
reasons: ["Dependencies are current; start or refresh Metro + dev client before replay."],
|
|
309
|
+
checks,
|
|
310
|
+
actions: launchActions(resolved)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export {
|
|
314
|
+
decideMobileReadiness
|
|
315
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
4
|
+
import { resolveMobileSlotPorts } from "../slot-ports.js";
|
|
5
|
+
import { mobileRuntimeStatus } from "./prepare.js";
|
|
6
|
+
import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
|
|
7
|
+
const mobileSurface = {
|
|
8
|
+
adapter: "mobile",
|
|
9
|
+
headless: false,
|
|
10
|
+
resolveSlotPorts(target) {
|
|
11
|
+
resolveMobileSlotPorts(target);
|
|
12
|
+
},
|
|
13
|
+
async runtimeStatus(target) {
|
|
14
|
+
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
15
|
+
const report = await mobileRuntimeStatus(target, { watcherPort });
|
|
16
|
+
const runwayProvisioned = hasRunwayProvisionBaseline(target);
|
|
17
|
+
const depsPending = runwayProvisioned && report.checks?.deps?.status !== "current";
|
|
18
|
+
return {
|
|
19
|
+
decision: depsPending ? "launch" : report.decision,
|
|
20
|
+
reasonCode: depsPending ? "app-installed-deps-pending" : report.reasonCode,
|
|
21
|
+
reasons: depsPending ? ["Runway app is installed; JavaScript dependencies are pending until dispatch-time launch."] : report.reasons,
|
|
22
|
+
deps: runwayProvisioned && report.checks?.deps?.status !== "current" ? "pending" : report.checks?.deps?.status,
|
|
23
|
+
devServer: { label: "metro", status: report.checks?.metro?.status ?? "unprobed" }
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
runwayProvision: {
|
|
27
|
+
async run(target, options) {
|
|
28
|
+
return provisionRunwayMobile(target, options);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
devServer: {
|
|
32
|
+
describe: () => "Metro dev server",
|
|
33
|
+
stop(target) {
|
|
34
|
+
const leaf = path.join(runnerDir, "adapters", "mobile", "stop-metro.sh");
|
|
35
|
+
const args = ["--target", target];
|
|
36
|
+
if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
|
|
37
|
+
const result = spawnSync("bash", [leaf, ...args], { encoding: "utf8" });
|
|
38
|
+
const status = result.status ?? 1;
|
|
39
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
40
|
+
const summary = status === 0 ? `stopped Metro dev server for ${target}` : `failed to stop Metro for ${target}`;
|
|
41
|
+
return { kind: "stopped", status, summary, output };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
logSources(target) {
|
|
45
|
+
return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
|
|
46
|
+
},
|
|
47
|
+
hints: {
|
|
48
|
+
launch: "mm-harness launch ios",
|
|
49
|
+
relaunch: "mm-harness launch ios"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
mobileSurface
|
|
54
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.js";
|
|
5
|
+
import { recipeRuntimeDir, runnerDir } from "../paths.js";
|
|
6
|
+
function applyKVLines(output, overwrite) {
|
|
7
|
+
for (const line of output.split("\n")) {
|
|
8
|
+
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
9
|
+
if (!m) continue;
|
|
10
|
+
const [, key, val] = m;
|
|
11
|
+
switch (key) {
|
|
12
|
+
case "WATCHER_PORT":
|
|
13
|
+
if (overwrite || !process.env["WATCHER_PORT"]) {
|
|
14
|
+
process.env["WATCHER_PORT"] = val;
|
|
15
|
+
process.env["METRO_PORT"] = val;
|
|
16
|
+
process.env["RECIPE_WATCHER_PORT"] = val;
|
|
17
|
+
}
|
|
18
|
+
break;
|
|
19
|
+
case "IOS_SIMULATOR":
|
|
20
|
+
if (overwrite || !process.env["IOS_SIMULATOR"]) process.env["IOS_SIMULATOR"] = val;
|
|
21
|
+
break;
|
|
22
|
+
case "SLOT_ID":
|
|
23
|
+
if (overwrite || !process.env["RECIPE_SLOT_ID"]) process.env["RECIPE_SLOT_ID"] = val;
|
|
24
|
+
break;
|
|
25
|
+
case "CDP_PORT":
|
|
26
|
+
if (overwrite || !process.env["CDP_PORT"]) {
|
|
27
|
+
process.env["CDP_PORT"] = val;
|
|
28
|
+
process.env["RECIPE_CDP_PORT"] = val;
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function sourceResolve(fn, target) {
|
|
35
|
+
const resolveScript = path.join(runnerDir, "adapters/shared/resolve-farmslot-ports.sh");
|
|
36
|
+
try {
|
|
37
|
+
return execFileSync("bash", ["-c", `source "${resolveScript}" && ${fn} "${target}"`], {
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
timeout: 5e3,
|
|
40
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function resolveMobileSlotPorts(target) {
|
|
47
|
+
const ctxOut = sourceResolve("resolve_mobile_runtime_context", target);
|
|
48
|
+
if (ctxOut.trim()) {
|
|
49
|
+
applyKVLines(ctxOut, true);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
|
|
53
|
+
if (poolOut.trim()) {
|
|
54
|
+
applyKVLines(poolOut, true);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const defOut = sourceResolve("resolve_mobile_slot_defaults", target);
|
|
58
|
+
if (defOut.trim()) applyKVLines(defOut, false);
|
|
59
|
+
}
|
|
60
|
+
function resolveExtensionSlotPorts(target) {
|
|
61
|
+
const contextPath = resolveRuntimeContextPath(target);
|
|
62
|
+
const cdp = readRuntimeContextField(contextPath, "cdpPort");
|
|
63
|
+
if (cdp) {
|
|
64
|
+
process.env["CDP_PORT"] = cdp;
|
|
65
|
+
process.env["RECIPE_CDP_PORT"] = cdp;
|
|
66
|
+
}
|
|
67
|
+
const dev = readRuntimeContextField(contextPath, "devServerPort");
|
|
68
|
+
if (dev) {
|
|
69
|
+
process.env["WATCHER_PORT"] = dev;
|
|
70
|
+
process.env["RECIPE_WATCHER_PORT"] = dev;
|
|
71
|
+
}
|
|
72
|
+
if (process.env["CDP_PORT"]) return;
|
|
73
|
+
const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
|
|
74
|
+
if (poolOut.trim()) {
|
|
75
|
+
applyKVLines(poolOut, true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const defOut = sourceResolve("resolve_default_extension_ports", target);
|
|
79
|
+
if (defOut.trim()) applyKVLines(defOut, false);
|
|
80
|
+
}
|
|
81
|
+
function stopExtensionWatcher(target) {
|
|
82
|
+
const runtimeAbs = path.join(target, recipeRuntimeDir());
|
|
83
|
+
const webpackPidFile = path.join(runtimeAbs, "recipe-harness-webpack.pid");
|
|
84
|
+
let signalled = 0;
|
|
85
|
+
try {
|
|
86
|
+
const pid = fs.readFileSync(webpackPidFile, "utf8").trim();
|
|
87
|
+
if (/^\d+$/u.test(pid)) {
|
|
88
|
+
try {
|
|
89
|
+
process.kill(Number(pid), "SIGTERM");
|
|
90
|
+
signalled += 1;
|
|
91
|
+
} catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
fs.rmSync(webpackPidFile, { force: true });
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const psOut = execFileSync("ps", ["-axo", "pid=,command="], { encoding: "utf8" });
|
|
99
|
+
const orphanPids = [];
|
|
100
|
+
for (const line of psOut.split("\n")) {
|
|
101
|
+
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
102
|
+
if (!match) continue;
|
|
103
|
+
const [, pidStr, cmd] = match;
|
|
104
|
+
const isWatcher = cmd.includes("yarn start") || cmd.includes("webpack --watch") || cmd.includes("development/webpack/launch.ts --watch");
|
|
105
|
+
if (!isWatcher) continue;
|
|
106
|
+
if (cmd.includes(target)) {
|
|
107
|
+
orphanPids.push(Number(pidStr));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const cwd = execFileSync("lsof", ["-a", `-p${pidStr}`, "-dcwd", "-Fn"], {
|
|
112
|
+
encoding: "utf8",
|
|
113
|
+
timeout: 2e3
|
|
114
|
+
});
|
|
115
|
+
if (cwd.split("\n").some((l) => l.startsWith("n") && l.slice(1) === target)) {
|
|
116
|
+
orphanPids.push(Number(pidStr));
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (orphanPids.length > 0) {
|
|
122
|
+
for (const pid of orphanPids) {
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, "SIGTERM");
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
|
|
129
|
+
for (const pid of orphanPids) {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, "SIGKILL");
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
signalled += orphanPids.length;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
return signalled;
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
applyKVLines,
|
|
143
|
+
resolveExtensionSlotPorts,
|
|
144
|
+
resolveMobileSlotPorts,
|
|
145
|
+
stopExtensionWatcher
|
|
146
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { coreSurface } from "./core/surface.js";
|
|
2
|
+
import { extensionSurface } from "./extension/surface.js";
|
|
3
|
+
import { mobileSurface } from "./mobile/surface.js";
|
|
4
|
+
const SURFACES = {
|
|
5
|
+
mobile: mobileSurface,
|
|
6
|
+
extension: extensionSurface,
|
|
7
|
+
core: coreSurface
|
|
8
|
+
};
|
|
9
|
+
function getAdapterSurface(adapter) {
|
|
10
|
+
return SURFACES[adapter];
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
getAdapterSurface
|
|
14
|
+
};
|