@deeeed/metamask-harness 0.4.0 → 0.5.1
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 +33 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +24 -23
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/resolve-farmslot-ports-core.mjs +196 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- 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 +317 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +140 -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/CLI-SPEC.md +26 -3
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +1 -1
- package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +5 -1
- package/src/adapters/core/surface.ts +15 -0
- package/src/adapters/extension/surface.ts +20 -3
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/runtime-decision.ts +8 -1
- package/src/adapters/mobile/surface.ts +16 -4
- package/src/adapters/resolve-farmslot-ports.ts +13 -0
- package/src/adapters/slot-ports.ts +18 -25
- package/src/adapters/surface.ts +35 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +149 -6
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +52 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const MOBILE_PRODUCT_MARKERS = [
|
|
2
|
+
"node_modules/.yarn-state.yml",
|
|
3
|
+
"app/core/InpageBridgeWeb3.js",
|
|
4
|
+
"docs/assets/termsOfUse.html",
|
|
5
|
+
"app/util/termsOfUse/termsOfUseContent.ts",
|
|
6
|
+
"node_modules/.bin/anvil"
|
|
7
|
+
];
|
|
8
|
+
const MOBILE_IOS_NATIVE_MARKERS = ["ios/Podfile.lock"];
|
|
9
|
+
const MOBILE_ANDROID_NATIVE_MARKERS = ["android/gradle.properties"];
|
|
10
|
+
function mobileProductMarkers(platform) {
|
|
11
|
+
const markers = [...MOBILE_PRODUCT_MARKERS];
|
|
12
|
+
const resolved = (platform ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM ?? "").trim().toLowerCase();
|
|
13
|
+
if (resolved === "ios") markers.push(...MOBILE_IOS_NATIVE_MARKERS);
|
|
14
|
+
else if (resolved === "android") markers.push(...MOBILE_ANDROID_NATIVE_MARKERS);
|
|
15
|
+
return markers;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
MOBILE_ANDROID_NATIVE_MARKERS,
|
|
19
|
+
MOBILE_IOS_NATIVE_MARKERS,
|
|
20
|
+
MOBILE_PRODUCT_MARKERS,
|
|
21
|
+
mobileProductMarkers
|
|
22
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { recordDepsBaseline } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
3
|
+
import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
|
|
4
|
+
import { runnerDir } from "../../paths.js";
|
|
5
|
+
import {
|
|
6
|
+
decideMobileReadiness
|
|
7
|
+
} from "./runtime-decision.js";
|
|
8
|
+
const POD_PROBE_ENV = {
|
|
9
|
+
FORCE_COLOR: "0",
|
|
10
|
+
NO_COLOR: "1",
|
|
11
|
+
LANG: process.env.LANG?.includes("UTF-8") ? process.env.LANG : "en_US.UTF-8",
|
|
12
|
+
LC_ALL: process.env.LC_ALL?.includes("UTF-8") ? process.env.LC_ALL : "en_US.UTF-8"
|
|
13
|
+
};
|
|
14
|
+
async function mobileRuntimeStatus(target, opts = {}) {
|
|
15
|
+
return decideMobileReadiness(target, {
|
|
16
|
+
watcherPort: opts.watcherPort,
|
|
17
|
+
metroLog: opts.metroLog,
|
|
18
|
+
platform: opts.platform,
|
|
19
|
+
record: opts.record,
|
|
20
|
+
preflightMode: opts.preflightMode
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function prepareMobile(target, opts = {}) {
|
|
24
|
+
const json = opts.json ?? false;
|
|
25
|
+
const platform = opts.platform ?? "ios";
|
|
26
|
+
const preflightMode = opts.preflightMode ?? "fast";
|
|
27
|
+
const clearMetro = opts.clearMetro ?? false;
|
|
28
|
+
const report = await decideMobileReadiness(target, {
|
|
29
|
+
watcherPort: opts.watcherPort,
|
|
30
|
+
metroLog: opts.metroLog,
|
|
31
|
+
platform,
|
|
32
|
+
record: opts.record,
|
|
33
|
+
preflightMode
|
|
34
|
+
});
|
|
35
|
+
if (report.decision === "blocked") {
|
|
36
|
+
const reasons = report.reasons.join(" ");
|
|
37
|
+
const next = report.userAction ?? "fix the bundle error in app code before retrying recipe up.";
|
|
38
|
+
const msg = `mobile prepare blocked: ${reasons}
|
|
39
|
+
Next: ${next}`;
|
|
40
|
+
if (!json) process.stderr.write(`${msg}
|
|
41
|
+
`);
|
|
42
|
+
return { status: EXIT.runtime, output: msg };
|
|
43
|
+
}
|
|
44
|
+
if (report.decision === "ready") {
|
|
45
|
+
return { status: 0, output: "" };
|
|
46
|
+
}
|
|
47
|
+
if (report.decision === "unknown") {
|
|
48
|
+
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
49
|
+
if (!json) process.stderr.write(`${msg}
|
|
50
|
+
`);
|
|
51
|
+
return { status: EXIT.runtime, output: msg };
|
|
52
|
+
}
|
|
53
|
+
const actions = clearMetro ? report.actions.map(
|
|
54
|
+
(a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
|
|
55
|
+
) : report.actions;
|
|
56
|
+
for (const action of actions) {
|
|
57
|
+
const result = await dispatchAction(action, target, platform, json, preflightMode);
|
|
58
|
+
if (result.status !== 0) return result;
|
|
59
|
+
if (action.id === "yarn-setup") recordDepsBaseline(path.resolve(target));
|
|
60
|
+
}
|
|
61
|
+
if (report.decision === "install" && !process.env["RECIPE_UP_INSTALL_ATTEMPTED"]) {
|
|
62
|
+
process.env["RECIPE_UP_INSTALL_ATTEMPTED"] = "1";
|
|
63
|
+
const postInstall = await decideMobileReadiness(target, {
|
|
64
|
+
watcherPort: opts.watcherPort,
|
|
65
|
+
metroLog: opts.metroLog,
|
|
66
|
+
platform,
|
|
67
|
+
preflightMode
|
|
68
|
+
});
|
|
69
|
+
switch (postInstall.decision) {
|
|
70
|
+
case "install": {
|
|
71
|
+
const msg = `mobile prepare: dependencies installed but runtime is still unresolved (${postInstall.reasons.join("; ")})
|
|
72
|
+
Next: inspect the checkout \u2014 node_modules may be incomplete or yarn.lock drifted.`;
|
|
73
|
+
if (!json) process.stderr.write(`${msg}
|
|
74
|
+
`);
|
|
75
|
+
return { status: EXIT.runtime, output: msg };
|
|
76
|
+
}
|
|
77
|
+
case "ready": {
|
|
78
|
+
const bridge = await dispatchAction({ id: "wait-for-bridge", cwd: target }, target, platform, json, preflightMode);
|
|
79
|
+
if (bridge.status !== 0) return bridge;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "launch": {
|
|
83
|
+
for (const action of postInstall.actions) {
|
|
84
|
+
const result = await dispatchAction(action, target, platform, json, preflightMode);
|
|
85
|
+
if (result.status !== 0) return result;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
default: {
|
|
90
|
+
const msg = `mobile prepare: post-install state is ${postInstall.decision}: ${postInstall.reasons.join("; ")}
|
|
91
|
+
Next: run mm-harness verify --adapter mobile --target <checkout>`;
|
|
92
|
+
if (!json) process.stderr.write(`${msg}
|
|
93
|
+
`);
|
|
94
|
+
return { status: EXIT.runtime, output: msg };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { status: 0, output: "" };
|
|
99
|
+
}
|
|
100
|
+
async function dispatchAction(action, target, platform, json, preflightMode = "fast") {
|
|
101
|
+
const cwd = action.cwd ?? target;
|
|
102
|
+
switch (action.id) {
|
|
103
|
+
case "yarn-setup": {
|
|
104
|
+
const leaf = path.join(runnerDir, "adapters/mobile/yarn-setup.sh");
|
|
105
|
+
return spawnScriptStreaming(leaf, ["--target", cwd], target, POD_PROBE_ENV);
|
|
106
|
+
}
|
|
107
|
+
case "start-metro": {
|
|
108
|
+
const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
|
|
109
|
+
const extra = action.argv ?? [];
|
|
110
|
+
return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
|
|
111
|
+
}
|
|
112
|
+
case "prewarm-bundle": {
|
|
113
|
+
const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
114
|
+
return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
|
|
115
|
+
}
|
|
116
|
+
case "wait-for-bridge": {
|
|
117
|
+
const leaf = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
118
|
+
return spawnScriptStreaming(leaf, ["--target", cwd], target);
|
|
119
|
+
}
|
|
120
|
+
case "clear-metro-cache": {
|
|
121
|
+
const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
|
|
122
|
+
return spawnScriptStreaming(leaf, ["--target", cwd, "--clear"], target);
|
|
123
|
+
}
|
|
124
|
+
case "launch-mobile-runtime": {
|
|
125
|
+
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
126
|
+
return spawnScriptStreaming(
|
|
127
|
+
leaf,
|
|
128
|
+
["--platform", platform, "--target", cwd, "--preflight-mode", preflightMode],
|
|
129
|
+
target,
|
|
130
|
+
POD_PROBE_ENV
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
case "rebuild-native-dev-client": {
|
|
134
|
+
const msg = "rebuild-native-dev-client required: native module mismatch detected.\n Next: yarn start:ios or yarn start:android to rebuild the native dev client, then re-run mm-harness launch.";
|
|
135
|
+
if (!json) process.stderr.write(`${msg}
|
|
136
|
+
`);
|
|
137
|
+
return { status: EXIT.runtime, output: msg };
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
return { status: 0, output: "" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
mobileRuntimeStatus,
|
|
145
|
+
prepareMobile
|
|
146
|
+
};
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { recipeRuntimeDir } from "../../paths.js";
|
|
7
|
+
const RUNWAY_IOS_METADATA = {
|
|
8
|
+
artifactName: "ios-app-main-dev-expo",
|
|
9
|
+
workflow: "expo-dev-build.yml",
|
|
10
|
+
bundleId: "io.metamask.MetaMask",
|
|
11
|
+
appDirName: "MetaMask.app",
|
|
12
|
+
fallbackRepo: "MetaMask/metamask-mobile"
|
|
13
|
+
};
|
|
14
|
+
async function provisionRunwayMobile(target, options) {
|
|
15
|
+
const resolvedTarget = path.resolve(target);
|
|
16
|
+
const platform = options.platform ?? "ios";
|
|
17
|
+
const slot = readSlotContext(resolvedTarget, options.runtimeDir);
|
|
18
|
+
if (options.slot) slot.slotId = options.slot;
|
|
19
|
+
if (options.watcherPort) slot.watcherPort = options.watcherPort;
|
|
20
|
+
const command = options.rerunCommand;
|
|
21
|
+
if (platform !== "ios") {
|
|
22
|
+
return fail(resolvedTarget, platform, "UNSUPPORTED_PLATFORM", "runway provisioning currently installs the iOS .app artifact only.", command, slot);
|
|
23
|
+
}
|
|
24
|
+
const simulator = options.simulator ?? slot.simulator ?? process.env.IOS_SIMULATOR;
|
|
25
|
+
if (!simulator && !options.resolveOnly) {
|
|
26
|
+
return fail(resolvedTarget, platform, "SIMULATOR_MISSING", "no simulator was resolved from agentic-runtime.json, --simulator, or IOS_SIMULATOR.", command, slot);
|
|
27
|
+
}
|
|
28
|
+
const runtime = options.runtime ?? slot.runtime ?? process.env.IOS_RUNTIME;
|
|
29
|
+
const deviceType = options.deviceType ?? slot.deviceType ?? process.env.IOS_DEVICE_TYPE;
|
|
30
|
+
const repo = githubRepo(resolvedTarget);
|
|
31
|
+
const defaultBranch = options.defaultBranch ?? slot.defaultBranch ?? "main";
|
|
32
|
+
const branch = options.branch ?? slot.gitBranch ?? gitBranch(resolvedTarget) ?? defaultBranch;
|
|
33
|
+
try {
|
|
34
|
+
if (options.resolveOnly) {
|
|
35
|
+
log(options, `runway: resolving Runway artifact for ${branch} (default ${defaultBranch})`);
|
|
36
|
+
const resolved2 = resolveArtifactRun(repo, branch, defaultBranch, options.run);
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
command: "provision",
|
|
40
|
+
adapter: "mobile",
|
|
41
|
+
target: resolvedTarget,
|
|
42
|
+
platform,
|
|
43
|
+
status: "pass",
|
|
44
|
+
exitCode: 0,
|
|
45
|
+
slot,
|
|
46
|
+
resolveOnly: true,
|
|
47
|
+
artifact: resolved2,
|
|
48
|
+
installed: false,
|
|
49
|
+
skipped: true
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const sim = ensureSimulator(simulator, runtime, deviceType);
|
|
53
|
+
if (!options.force && appInstalled(sim.udid ?? sim.name)) {
|
|
54
|
+
const baselinePath2 = writeRunwayBaseline(resolvedTarget, slot, platform, void 0, void 0, sim, true, options.runtimeDir);
|
|
55
|
+
return {
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
command: "provision",
|
|
58
|
+
adapter: "mobile",
|
|
59
|
+
target: resolvedTarget,
|
|
60
|
+
platform,
|
|
61
|
+
status: "pass",
|
|
62
|
+
exitCode: 0,
|
|
63
|
+
slot,
|
|
64
|
+
simulator: sim,
|
|
65
|
+
skipped: true,
|
|
66
|
+
installed: false,
|
|
67
|
+
baselinePath: baselinePath2
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
log(options, `runway: resolving Runway artifact for ${branch} (default ${defaultBranch})`);
|
|
71
|
+
const resolved = resolveArtifactRun(repo, branch, defaultBranch, options.run);
|
|
72
|
+
const cacheRoot = options.cacheRoot ?? defaultRunwayCacheRoot();
|
|
73
|
+
const cache = ensureCachedArtifact(repo, resolved.branch, resolved.runId, cacheRoot, options);
|
|
74
|
+
log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
|
|
75
|
+
execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
76
|
+
const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
command: "provision",
|
|
80
|
+
adapter: "mobile",
|
|
81
|
+
target: resolvedTarget,
|
|
82
|
+
platform,
|
|
83
|
+
status: "pass",
|
|
84
|
+
exitCode: 0,
|
|
85
|
+
slot,
|
|
86
|
+
artifact: cache.artifact,
|
|
87
|
+
cache: { status: cache.status, path: cache.artifact.appPath, root: cacheRoot },
|
|
88
|
+
simulator: sim,
|
|
89
|
+
installed: true,
|
|
90
|
+
skipped: false,
|
|
91
|
+
baselinePath
|
|
92
|
+
};
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return fail(resolvedTarget, platform, "PROVISION_FAILED", errorMessage(error), command, slot);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function runwayBaselinePath(target, runtimeDir) {
|
|
98
|
+
return path.join(target, resolveRuntimeDir(runtimeDir), "runway-provision.json");
|
|
99
|
+
}
|
|
100
|
+
function hasRunwayProvisionBaseline(target) {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(fs.readFileSync(runwayBaselinePath(target), "utf8"));
|
|
103
|
+
if (data.appInstalled !== true) return false;
|
|
104
|
+
const simulators = baselineSimulatorCandidates(data, readSlotContext(path.resolve(target)));
|
|
105
|
+
return simulators.some((simulator) => appInstalled(simulator));
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function fail(target, platform, code, message, rerunCommand, slot) {
|
|
111
|
+
return {
|
|
112
|
+
schemaVersion: 1,
|
|
113
|
+
command: "provision",
|
|
114
|
+
adapter: "mobile",
|
|
115
|
+
target,
|
|
116
|
+
platform,
|
|
117
|
+
status: "fail",
|
|
118
|
+
exitCode: 1,
|
|
119
|
+
slot,
|
|
120
|
+
error: { code, message, userAction: rerunCommand }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function readSlotContext(target, runtimeDir) {
|
|
124
|
+
const candidates = [
|
|
125
|
+
process.env.RECIPE_RUNTIME_CONTEXT,
|
|
126
|
+
runtimeContextPath(target, runtimeDir),
|
|
127
|
+
path.join(target, recipeRuntimeDir(), "agentic-runtime.json"),
|
|
128
|
+
path.join(target, "temp/recipe/runtime/agentic-runtime.json"),
|
|
129
|
+
path.join(target, "temp/agentic/recipe-harness/agentic-runtime.json")
|
|
130
|
+
].filter((value) => Boolean(value));
|
|
131
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
132
|
+
try {
|
|
133
|
+
const data = JSON.parse(fs.readFileSync(candidate, "utf8"));
|
|
134
|
+
return {
|
|
135
|
+
path: candidate,
|
|
136
|
+
slotId: stringField(data, "slotId"),
|
|
137
|
+
platform: stringField(data, "platform"),
|
|
138
|
+
simulator: stringField(data, "simulator") ?? stringField(data, "iosSimulator"),
|
|
139
|
+
runtime: stringField(data, "runtime") ?? stringField(data, "iosRuntime"),
|
|
140
|
+
deviceType: stringField(data, "deviceType") ?? stringField(data, "iosDeviceType"),
|
|
141
|
+
watcherPort: stringField(data, "watcherPort") ?? stringField(data, "metroPort") ?? stringField(data, "devServerPort"),
|
|
142
|
+
gitBranch: stringField(data, "gitBranch") ?? stringField(data, "prepareRef"),
|
|
143
|
+
defaultBranch: stringField(data, "defaultBranch") ?? stringField(data, "prepareDefaultRef")
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
function runtimeContextPath(target, runtimeDir) {
|
|
151
|
+
if (!runtimeDir) return void 0;
|
|
152
|
+
try {
|
|
153
|
+
return path.join(target, resolveRuntimeDir(runtimeDir), "agentic-runtime.json");
|
|
154
|
+
} catch {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function stringField(data, key) {
|
|
159
|
+
const value = data[key];
|
|
160
|
+
return typeof value === "string" && value ? value : void 0;
|
|
161
|
+
}
|
|
162
|
+
function githubRepo(target) {
|
|
163
|
+
try {
|
|
164
|
+
const remote = execFileSync("git", ["-C", target, "config", "--get", "remote.origin.url"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
165
|
+
const match = /github\.com[:/]([^/]+\/[^/.]+)(?:\.git)?$/u.exec(remote);
|
|
166
|
+
if (match) return match[1];
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
return RUNWAY_IOS_METADATA.fallbackRepo;
|
|
170
|
+
}
|
|
171
|
+
function gitBranch(target) {
|
|
172
|
+
try {
|
|
173
|
+
return execFileSync("git", ["-C", target, "branch", "--show-current"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || void 0;
|
|
174
|
+
} catch {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function resolveArtifactRun(repo, branch, defaultBranch, runOverride) {
|
|
179
|
+
if (runOverride) {
|
|
180
|
+
assertValidRunId(runOverride);
|
|
181
|
+
const artifact = findRunArtifact(repo, runOverride, RUNWAY_IOS_METADATA.artifactName);
|
|
182
|
+
if (!artifact) throw new Error(`Runway run ${runOverride} does not contain ${RUNWAY_IOS_METADATA.artifactName}.`);
|
|
183
|
+
const revision = runRevision(repo, runOverride) ?? runOverride;
|
|
184
|
+
return resolvedArtifact(repo, branch, runOverride, revision, artifact);
|
|
185
|
+
}
|
|
186
|
+
const tried = [.../* @__PURE__ */ new Set([branch, defaultBranch])];
|
|
187
|
+
for (const candidate of tried) {
|
|
188
|
+
const run = latestRunWithArtifact(repo, candidate, RUNWAY_IOS_METADATA.artifactName);
|
|
189
|
+
if (run) return resolvedArtifact(repo, candidate, run.runId, run.revision, run.artifact);
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`no Runway artifact found for ${RUNWAY_IOS_METADATA.artifactName} on ${tried.join(", ")}.`);
|
|
192
|
+
}
|
|
193
|
+
function latestRunWithArtifact(repo, branch, artifactName) {
|
|
194
|
+
let runs;
|
|
195
|
+
try {
|
|
196
|
+
runs = execJson("gh", [
|
|
197
|
+
"run",
|
|
198
|
+
"list",
|
|
199
|
+
"--repo",
|
|
200
|
+
repo,
|
|
201
|
+
`--workflow=${RUNWAY_IOS_METADATA.workflow}`,
|
|
202
|
+
`--branch=${branch}`,
|
|
203
|
+
"--status=success",
|
|
204
|
+
"--limit=30",
|
|
205
|
+
"--json",
|
|
206
|
+
"databaseId,headSha"
|
|
207
|
+
]);
|
|
208
|
+
} catch {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
for (const run of runs) {
|
|
212
|
+
const runId = run.databaseId ? String(run.databaseId) : "";
|
|
213
|
+
if (!runId) continue;
|
|
214
|
+
const artifact = findRunArtifact(repo, runId, artifactName);
|
|
215
|
+
if (artifact) return { runId, revision: run.headSha || runId, artifact };
|
|
216
|
+
}
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
function assertValidRunId(runId) {
|
|
220
|
+
if (!/^\d+$/u.test(runId)) throw new Error(`invalid Runway run id: ${runId}.`);
|
|
221
|
+
}
|
|
222
|
+
function findRunArtifact(repo, runId, artifactName) {
|
|
223
|
+
assertValidRunId(runId);
|
|
224
|
+
try {
|
|
225
|
+
const data = execJson("gh", [
|
|
226
|
+
"api",
|
|
227
|
+
`repos/${repo}/actions/runs/${runId}/artifacts`,
|
|
228
|
+
"--paginate"
|
|
229
|
+
]);
|
|
230
|
+
return data.artifacts?.find((artifact) => artifact.expired === false && artifact.name === artifactName);
|
|
231
|
+
} catch {
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function runRevision(repo, runId) {
|
|
236
|
+
try {
|
|
237
|
+
const data = execJson("gh", ["run", "view", runId, "--repo", repo, "--json", "headSha"]);
|
|
238
|
+
return data.headSha;
|
|
239
|
+
} catch {
|
|
240
|
+
return void 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function resolvedArtifact(repo, branch, runId, revision, artifact) {
|
|
244
|
+
return {
|
|
245
|
+
repo,
|
|
246
|
+
branch,
|
|
247
|
+
runId,
|
|
248
|
+
artifactName: RUNWAY_IOS_METADATA.artifactName,
|
|
249
|
+
revision,
|
|
250
|
+
archiveSizeBytes: artifact.size_in_bytes,
|
|
251
|
+
archiveDownloadUrl: artifact.archive_download_url,
|
|
252
|
+
createdAt: artifact.created_at,
|
|
253
|
+
updatedAt: artifact.updated_at
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function ensureCachedArtifact(repo, branch, runId, cacheRoot, options) {
|
|
257
|
+
const dir = path.join(cacheRoot, runId);
|
|
258
|
+
const appPath = path.join(dir, RUNWAY_IOS_METADATA.appDirName);
|
|
259
|
+
const metaPath = path.join(dir, "metadata.json");
|
|
260
|
+
const valid = validateCache(appPath, metaPath, RUNWAY_IOS_METADATA.artifactName, runId);
|
|
261
|
+
if (valid) {
|
|
262
|
+
return { status: "hit", artifact: { repo, branch, runId, artifactName: RUNWAY_IOS_METADATA.artifactName, revision: runId, appPath, sizeBytes: valid.sizeBytes, sha256: valid.sha256 } };
|
|
263
|
+
}
|
|
264
|
+
const redownload = fs.existsSync(dir);
|
|
265
|
+
if (redownload) {
|
|
266
|
+
const corruptDir = path.join(cacheRoot, `${runId}.corrupt-${Date.now()}`);
|
|
267
|
+
fs.renameSync(dir, corruptDir);
|
|
268
|
+
}
|
|
269
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
270
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "mm-runway-artifact-"));
|
|
271
|
+
try {
|
|
272
|
+
log(options, `runway: cache ${redownload ? "corrupt" : "miss"} for run ${runId}; downloading once`);
|
|
273
|
+
execFileSync("gh", ["run", "download", runId, "--repo", repo, "--name", RUNWAY_IOS_METADATA.artifactName, "--dir", tmp], { stdio: ["ignore", "ignore", "pipe"] });
|
|
274
|
+
const downloaded = findAppOrExtractArchive(tmp);
|
|
275
|
+
if (!downloaded) throw new Error("downloaded artifact did not contain an iOS .app bundle.");
|
|
276
|
+
movePath(downloaded, appPath);
|
|
277
|
+
} finally {
|
|
278
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
const digest = hashPath(appPath);
|
|
281
|
+
const meta = { artifactName: RUNWAY_IOS_METADATA.artifactName, runId, sizeBytes: digest.sizeBytes, sha256: digest.sha256 };
|
|
282
|
+
fs.writeFileSync(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
283
|
+
`);
|
|
284
|
+
return { status: redownload ? "redownload" : "miss", artifact: { repo, branch, runId, artifactName: RUNWAY_IOS_METADATA.artifactName, revision: runId, appPath, ...digest } };
|
|
285
|
+
}
|
|
286
|
+
function validateCache(appPath, metaPath, artifactName, runId) {
|
|
287
|
+
if (!fs.existsSync(appPath) || !fs.existsSync(metaPath)) return null;
|
|
288
|
+
try {
|
|
289
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
290
|
+
if (meta.artifactName !== artifactName || meta.runId !== runId) return null;
|
|
291
|
+
const digest = hashPath(appPath);
|
|
292
|
+
return digest.sizeBytes === meta.sizeBytes && digest.sha256 === meta.sha256 ? digest : null;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function findApp(root) {
|
|
298
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
299
|
+
let app;
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const full = path.join(root, entry.name);
|
|
302
|
+
if (entry.isDirectory() && entry.name === RUNWAY_IOS_METADATA.appDirName) return full;
|
|
303
|
+
if (entry.isDirectory() && entry.name.endsWith(".app")) {
|
|
304
|
+
app ??= full;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (entry.isDirectory()) {
|
|
308
|
+
const nested = findApp(full);
|
|
309
|
+
if (nested) return nested;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return app;
|
|
313
|
+
}
|
|
314
|
+
function findAppOrExtractArchive(root) {
|
|
315
|
+
return findAppOrExtractArchiveInner(root, /* @__PURE__ */ new Set(), 0);
|
|
316
|
+
}
|
|
317
|
+
function findAppOrExtractArchiveInner(root, extracted, depth) {
|
|
318
|
+
const app = findApp(root);
|
|
319
|
+
if (app) return app;
|
|
320
|
+
if (depth >= 3) return void 0;
|
|
321
|
+
for (const archive of findZipArchives(root)) {
|
|
322
|
+
const realArchive = fs.realpathSync(archive);
|
|
323
|
+
if (extracted.has(realArchive)) continue;
|
|
324
|
+
extracted.add(realArchive);
|
|
325
|
+
const extractDir = fs.mkdtempSync(path.join(root, ".mm-runway-unzip-"));
|
|
326
|
+
execFileSync("unzip", ["-q", archive, "-d", extractDir], { stdio: ["ignore", "ignore", "pipe"] });
|
|
327
|
+
const nested = findAppOrExtractArchiveInner(extractDir, extracted, depth + 1);
|
|
328
|
+
if (nested) return nested;
|
|
329
|
+
}
|
|
330
|
+
return void 0;
|
|
331
|
+
}
|
|
332
|
+
function findZipArchives(root) {
|
|
333
|
+
const archives = [];
|
|
334
|
+
const visit = (dir) => {
|
|
335
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
336
|
+
for (const entry of entries) {
|
|
337
|
+
const full = path.join(dir, entry.name);
|
|
338
|
+
if (entry.isDirectory()) {
|
|
339
|
+
if (!entry.name.endsWith(".app")) visit(full);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".zip")) archives.push(full);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
visit(root);
|
|
346
|
+
return archives.sort();
|
|
347
|
+
}
|
|
348
|
+
function movePath(source, destination) {
|
|
349
|
+
try {
|
|
350
|
+
fs.renameSync(source, destination);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof Error && "code" in error && error.code === "EXDEV") {
|
|
353
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
354
|
+
fs.rmSync(source, { recursive: true, force: true });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function hashPath(root) {
|
|
361
|
+
const hash = crypto.createHash("sha256");
|
|
362
|
+
let sizeBytes = 0;
|
|
363
|
+
const visitDirectory = (dir, relative = "") => {
|
|
364
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const full = path.join(dir, entry.name);
|
|
367
|
+
const rel = path.join(relative, entry.name);
|
|
368
|
+
if (entry.isDirectory()) {
|
|
369
|
+
visitDirectory(full, rel);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (!entry.isFile()) continue;
|
|
373
|
+
hash.update(rel);
|
|
374
|
+
const data = fs.readFileSync(full);
|
|
375
|
+
sizeBytes += data.length;
|
|
376
|
+
hash.update(data);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
visitDirectory(root);
|
|
380
|
+
return { sizeBytes, sha256: hash.digest("hex") };
|
|
381
|
+
}
|
|
382
|
+
function ensureSimulator(name, runtime, deviceType) {
|
|
383
|
+
const existing = findSimulator(name);
|
|
384
|
+
if (existing) return { name, udid: existing, created: false, runtime, deviceType };
|
|
385
|
+
if (!runtime || !deviceType) {
|
|
386
|
+
throw new Error(`simulator ${name} is missing and runtime/device type could not be resolved. Pass --runtime and --device-type, or write them to agentic-runtime.json.`);
|
|
387
|
+
}
|
|
388
|
+
const udid = execFileSync("xcrun", ["simctl", "create", name, deviceType, runtime], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
389
|
+
return { name, udid: udid || name, created: true, runtime, deviceType };
|
|
390
|
+
}
|
|
391
|
+
function findSimulator(name) {
|
|
392
|
+
const data = execJson("xcrun", ["simctl", "list", "devices", "--json"]);
|
|
393
|
+
for (const devices of Object.values(data.devices ?? {})) {
|
|
394
|
+
const found = devices.find((device) => device.name === name || device.udid === name);
|
|
395
|
+
if (found?.udid) return found.udid;
|
|
396
|
+
}
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
function baselineSimulatorCandidates(data, current) {
|
|
400
|
+
const candidates = [];
|
|
401
|
+
if (current.simulator) candidates.push(current.simulator);
|
|
402
|
+
const simulator = data.simulator;
|
|
403
|
+
if (simulator && typeof simulator === "object" && !Array.isArray(simulator)) {
|
|
404
|
+
const record = simulator;
|
|
405
|
+
for (const key of ["udid", "name"]) {
|
|
406
|
+
const value = record[key];
|
|
407
|
+
if (typeof value === "string" && value) candidates.push(value);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return [...new Set(candidates)];
|
|
411
|
+
}
|
|
412
|
+
function appInstalled(device) {
|
|
413
|
+
try {
|
|
414
|
+
execFileSync("xcrun", ["simctl", "get_app_container", device, RUNWAY_IOS_METADATA.bundleId, "app"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
415
|
+
return true;
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function writeRunwayBaseline(target, slot, platform, resolved, artifact, simulator, alreadyInstalled, runtimeDir) {
|
|
421
|
+
const file = runwayBaselinePath(target, runtimeDir);
|
|
422
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
423
|
+
fs.writeFileSync(file, `${JSON.stringify({
|
|
424
|
+
schemaVersion: 1,
|
|
425
|
+
appInstalled: true,
|
|
426
|
+
deps: "pending",
|
|
427
|
+
platform,
|
|
428
|
+
slotId: slot.slotId ?? null,
|
|
429
|
+
watcherPort: slot.watcherPort ?? null,
|
|
430
|
+
simulator,
|
|
431
|
+
artifact: resolved && artifact ? { ...resolved, path: artifact.appPath, sizeBytes: artifact.sizeBytes, sha256: artifact.sha256 } : null,
|
|
432
|
+
alreadyInstalled,
|
|
433
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
434
|
+
}, null, 2)}
|
|
435
|
+
`);
|
|
436
|
+
return file;
|
|
437
|
+
}
|
|
438
|
+
function resolveRuntimeDir(runtimeDir) {
|
|
439
|
+
if (runtimeDir === void 0) return recipeRuntimeDir();
|
|
440
|
+
if (!runtimeDir || path.isAbsolute(runtimeDir)) throw new Error(`--runtime-dir must be a non-empty relative path: ${runtimeDir}`);
|
|
441
|
+
if (!/^[A-Za-z0-9._/-]+$/u.test(runtimeDir)) throw new Error(`--runtime-dir contains unsupported characters: ${runtimeDir}`);
|
|
442
|
+
for (const part of runtimeDir.split("/")) {
|
|
443
|
+
if (!part || part === "." || part === "..") throw new Error(`--runtime-dir contains unsafe path component: ${runtimeDir}`);
|
|
444
|
+
}
|
|
445
|
+
return runtimeDir;
|
|
446
|
+
}
|
|
447
|
+
function defaultRunwayCacheRoot() {
|
|
448
|
+
return path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"), "metamask-harness", "runway");
|
|
449
|
+
}
|
|
450
|
+
function execJson(bin, args) {
|
|
451
|
+
const out = execFileSync(bin, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
452
|
+
return JSON.parse(out);
|
|
453
|
+
}
|
|
454
|
+
function errorMessage(error) {
|
|
455
|
+
return error instanceof Error ? error.message : String(error);
|
|
456
|
+
}
|
|
457
|
+
function log(options, message) {
|
|
458
|
+
if (!options.json) process.stderr.write(`${message}
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
export {
|
|
462
|
+
hasRunwayProvisionBaseline,
|
|
463
|
+
provisionRunwayMobile,
|
|
464
|
+
runwayBaselinePath
|
|
465
|
+
};
|