@deeeed/metamask-harness 0.17.5 → 0.19.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 +1059 -0
- package/README.md +91 -140
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +7 -13
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +2 -2
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +91 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +34 -83
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -18
- package/dist/command-contract.js +462 -0
- package/dist/command-journal.js +263 -0
- package/dist/commands/call.js +252 -57
- package/dist/commands/check.js +10 -4
- package/dist/commands/completion-candidates.js +17 -22
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +142 -35
- package/dist/commands/last.js +60 -0
- package/dist/commands/launch/extension.js +40 -17
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +187 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +286 -35
- package/dist/commands/parse-args.js +68 -4
- package/dist/commands/provision.js +10 -4
- package/dist/commands/run-engine.js +772 -264
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +378 -85
- package/dist/commands/shared.js +76 -4
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +7 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +146 -73
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +140 -9
- package/docs/CONTRIBUTING.md +136 -0
- package/docs/QA.md +184 -0
- package/docs/RECIPES.md +122 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +7 -3
- package/library/actions/core/perps/_controller.mjs +43 -56
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1214 -396
- package/library/manifests/extension.action-manifest.json +1635 -728
- package/library/manifests/mobile.action-manifest.json +1740 -727
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
- package/library/recipes/runner/smoke.core.recipe.json +25 -0
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
- package/package.json +12 -5
- package/scripts/completions.sh +8 -11
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -62
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
- package/library/flows/perps.flows.json +0 -64
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const [cacheFile, timeoutText, endpoint] = process.argv.slice(2);
|
|
6
|
+
const timeoutMs = Number.parseInt(timeoutText ?? '', 10);
|
|
7
|
+
|
|
8
|
+
function stableVersion(value) {
|
|
9
|
+
const match = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/u.exec(value);
|
|
10
|
+
if (!match) return null;
|
|
11
|
+
return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
if (!cacheFile || !endpoint || !Number.isInteger(timeoutMs) || timeoutMs < 1) return;
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(endpoint, { signal: controller.signal });
|
|
20
|
+
if (!response.ok) return;
|
|
21
|
+
const data = await response.json();
|
|
22
|
+
if (!data || typeof data.latest !== 'string' || data.latest.length === 0) return;
|
|
23
|
+
const latest = stableVersion(data.latest);
|
|
24
|
+
if (!latest) return;
|
|
25
|
+
let lastCheck = Date.now();
|
|
26
|
+
try {
|
|
27
|
+
const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
28
|
+
if (typeof cached.lastCheck === 'number') lastCheck = cached.lastCheck;
|
|
29
|
+
} catch {
|
|
30
|
+
// The parent may have exited before creating the cache; recreate it below.
|
|
31
|
+
}
|
|
32
|
+
fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
|
|
33
|
+
const temporary = `${cacheFile}.${process.pid}.tmp`;
|
|
34
|
+
fs.writeFileSync(temporary, JSON.stringify({ lastCheck, latest }));
|
|
35
|
+
fs.renameSync(temporary, cacheFile);
|
|
36
|
+
} catch {
|
|
37
|
+
// Passive checks are intentionally silent; the explicit update command teaches failures.
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await main();
|
package/bin/mm-harness
CHANGED
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
# end-state surface; it resolves node/tsx/dist the same way whether the package is
|
|
6
6
|
# installed as a dependency or run from a source checkout.
|
|
7
7
|
set -euo pipefail
|
|
8
|
+
INVOKED_AS="$0"
|
|
9
|
+
if [[ "$INVOKED_AS" == */* ]]; then
|
|
10
|
+
INVOKED_AS="$(cd "$(dirname "$INVOKED_AS")" && pwd -L)/${INVOKED_AS##*/}"
|
|
11
|
+
else
|
|
12
|
+
RESOLVED_INVOKED_AS="$(command -v "$INVOKED_AS" 2>/dev/null || true)"
|
|
13
|
+
if [[ -z "$RESOLVED_INVOKED_AS" ]]; then
|
|
14
|
+
echo "ERROR: Cannot resolve the invoked mm-harness executable: $INVOKED_AS" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
INVOKED_AS="$RESOLVED_INVOKED_AS"
|
|
18
|
+
fi
|
|
8
19
|
SOURCE="${BASH_SOURCE[0]}"
|
|
9
20
|
while [ -L "$SOURCE" ]; do
|
|
10
21
|
SOURCE_DIR="$(cd "$(dirname "$SOURCE")" && pwd -P)"
|
|
@@ -26,7 +37,7 @@ RUNNER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
26
37
|
# already points at this script — including via a symlink (both sides are resolved
|
|
27
38
|
# to their real paths before comparing).
|
|
28
39
|
if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
29
|
-
self_real="$SCRIPT_DIR/$
|
|
40
|
+
self_real="$SCRIPT_DIR/${SOURCE##*/}"
|
|
30
41
|
# Resolve MM_HARNESS_BIN through any symlink chain so a symlink-to-self is caught.
|
|
31
42
|
mm_bin_src="$MM_HARNESS_BIN"
|
|
32
43
|
while [ -L "$mm_bin_src" ]; do
|
|
@@ -39,7 +50,7 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
|
39
50
|
fi
|
|
40
51
|
done
|
|
41
52
|
bin_real_dir="$(cd "$(dirname "$mm_bin_src")" 2>/dev/null && pwd -P || true)"
|
|
42
|
-
bin_real="${bin_real_dir:+$bin_real_dir/$
|
|
53
|
+
bin_real="${bin_real_dir:+$bin_real_dir/${mm_bin_src##*/}}"
|
|
43
54
|
if [ "$bin_real" != "$self_real" ]; then
|
|
44
55
|
if [ ! -x "$MM_HARNESS_BIN" ]; then
|
|
45
56
|
echo "mm-harness: MM_HARNESS_BIN is not an executable: $MM_HARNESS_BIN" >&2
|
|
@@ -50,6 +61,14 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
|
50
61
|
fi
|
|
51
62
|
fi
|
|
52
63
|
|
|
64
|
+
# Internal executable identity (not part of the public env surface): the CLI
|
|
65
|
+
# embeds this in emitted recovery commands so they run verbatim even when
|
|
66
|
+
# mm-harness is not on PATH (task-local installs). Set AFTER the MM_HARNESS_BIN
|
|
67
|
+
# delegation above so the final executable entered always overwrites any
|
|
68
|
+
# inherited/stale value with its own resolved self path.
|
|
69
|
+
export MM_HARNESS_EXECUTABLE="$SCRIPT_DIR/${SOURCE##*/}"
|
|
70
|
+
export MM_HARNESS_INVOKED_AS="$INVOKED_AS"
|
|
71
|
+
|
|
53
72
|
ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
|
|
54
73
|
ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
|
|
55
74
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
3
|
+
import { shellQuote } from "../../commands/parse-args.js";
|
|
3
4
|
const coreSurface = {
|
|
4
5
|
adapter: "core",
|
|
5
6
|
headless: true,
|
|
@@ -14,6 +15,7 @@ const coreSurface = {
|
|
|
14
15
|
reasons: [
|
|
15
16
|
ready ? "Core is headless; dependencies are installed. Run recipes with mm-harness run." : "Core is headless; dependencies are not fully installed."
|
|
16
17
|
],
|
|
18
|
+
nextAction: ready ? void 0 : `cd ${shellQuote(path.resolve(target))} && yarn install --immutable`,
|
|
17
19
|
deps: deps.status
|
|
18
20
|
};
|
|
19
21
|
},
|
|
@@ -49,7 +51,8 @@ const coreSurface = {
|
|
|
49
51
|
},
|
|
50
52
|
hints: {
|
|
51
53
|
launch: "mm-harness run <recipe> # run recipes against the headless core",
|
|
52
|
-
relaunch: "mm-harness verify"
|
|
54
|
+
relaunch: "mm-harness verify",
|
|
55
|
+
runtimeProbeRecovery: (target) => `mm-harness verify --adapter core --target ${shellQuote(path.resolve(target))}`
|
|
53
56
|
}
|
|
54
57
|
};
|
|
55
58
|
export {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
5
|
+
async function ensureExtensionConsoleCapture(target) {
|
|
6
|
+
const cdpPort = process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
7
|
+
if (!cdpPort) throw new Error("Extension console capture requires a resolved CDP port.");
|
|
8
|
+
const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
|
|
9
|
+
signal: AbortSignal.timeout(3e3)
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) throw new Error(`Extension CDP ${cdpPort} is not ready for console capture.`);
|
|
12
|
+
const extensionLog = recipeRuntimePath(target, "extension-console.log");
|
|
13
|
+
const dappLog = recipeRuntimePath(target, "dapp-console.log");
|
|
14
|
+
const collectorLog = recipeRuntimePath(target, "console-tail.log");
|
|
15
|
+
const pidFile = recipeRuntimePath(target, "console-tail.pid");
|
|
16
|
+
fs.mkdirSync(path.dirname(extensionLog), { recursive: true });
|
|
17
|
+
const existingPid = readPid(pidFile);
|
|
18
|
+
if (existingPid && collectorOwnsLog(existingPid, extensionLog)) return;
|
|
19
|
+
if (existingPid) {
|
|
20
|
+
try {
|
|
21
|
+
process.kill(existingPid, "SIGTERM");
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
fs.rmSync(pidFile, { force: true });
|
|
26
|
+
fs.writeFileSync(extensionLog, "");
|
|
27
|
+
fs.writeFileSync(dappLog, "");
|
|
28
|
+
const script = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
|
|
29
|
+
const output = fs.openSync(collectorLog, "a");
|
|
30
|
+
let child;
|
|
31
|
+
try {
|
|
32
|
+
child = spawn(process.execPath, [
|
|
33
|
+
script,
|
|
34
|
+
"--cdp-port",
|
|
35
|
+
cdpPort,
|
|
36
|
+
"--extension-log",
|
|
37
|
+
extensionLog,
|
|
38
|
+
"--dapp-log",
|
|
39
|
+
dappLog
|
|
40
|
+
], {
|
|
41
|
+
cwd: target,
|
|
42
|
+
detached: true,
|
|
43
|
+
stdio: ["ignore", output, output]
|
|
44
|
+
});
|
|
45
|
+
} finally {
|
|
46
|
+
fs.closeSync(output);
|
|
47
|
+
}
|
|
48
|
+
if (!child.pid) throw new Error("Extension console collector did not start.");
|
|
49
|
+
child.unref();
|
|
50
|
+
fs.writeFileSync(pidFile, `${child.pid}
|
|
51
|
+
`);
|
|
52
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
53
|
+
if (!processAlive(child.pid)) break;
|
|
54
|
+
if (collectorOwnsLog(child.pid, extensionLog) && containsAttachment(extensionLog, dappLog)) return;
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Extension console collector ${child.pid} did not attach to CDP ${cdpPort}.`);
|
|
58
|
+
}
|
|
59
|
+
function processAlive(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readPid(pidFile) {
|
|
68
|
+
try {
|
|
69
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
|
|
70
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function collectorOwnsLog(pid, extensionLog) {
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 0);
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
82
|
+
return result.status === 0 && result.stdout.includes("console-tail.mjs") && result.stdout.includes(extensionLog);
|
|
83
|
+
}
|
|
84
|
+
function containsAttachment(...logs) {
|
|
85
|
+
return logs.some((log) => {
|
|
86
|
+
try {
|
|
87
|
+
return fs.readFileSync(log, "utf8").includes("[attached]");
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
ensureExtensionConsoleCapture
|
|
95
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { shellQuote } from "../../commands/parse-args.js";
|
|
4
|
+
function isConfiguredInfuraId(value, placeholder) {
|
|
5
|
+
return value.length > 0 && value !== placeholder && value !== "true" && value !== "false" && value !== "null";
|
|
6
|
+
}
|
|
7
|
+
function extensionProductConfigBlock(target) {
|
|
8
|
+
const template = path.join(target, ".metamaskrc.dist");
|
|
9
|
+
const templateSnapshot = readConfigSnapshot(template);
|
|
10
|
+
if (templateSnapshot.kind === "missing") return null;
|
|
11
|
+
if (templateSnapshot.kind === "invalid") {
|
|
12
|
+
return invalidConfigBlock(target, ".metamaskrc.dist", templateSnapshot.reason);
|
|
13
|
+
}
|
|
14
|
+
const placeholder = readInfuraProjectId(templateSnapshot.text) ?? "";
|
|
15
|
+
const configuredFromEnvironment = process.env.INFURA_PROJECT_ID;
|
|
16
|
+
if (configuredFromEnvironment !== void 0) {
|
|
17
|
+
if (isConfiguredInfuraId(configuredFromEnvironment, placeholder)) return null;
|
|
18
|
+
return {
|
|
19
|
+
message: "Extension product configuration is blocked by an unusable exported INFURA_PROJECT_ID (the value is never displayed).",
|
|
20
|
+
userAction: `unset INFURA_PROJECT_ID; cd ${shellQuote(path.resolve(target))} && cp -n .metamaskrc.dist .metamaskrc && \${EDITOR:-vi} .metamaskrc`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const productionConfig = readConfigSnapshot(path.join(target, ".metamaskprodrc"));
|
|
24
|
+
if (productionConfig.kind === "invalid") {
|
|
25
|
+
return invalidConfigBlock(target, ".metamaskprodrc", productionConfig.reason);
|
|
26
|
+
}
|
|
27
|
+
if (productionConfig.kind === "file") {
|
|
28
|
+
const value2 = readInfuraProjectId(productionConfig.text);
|
|
29
|
+
if (value2 !== void 0) {
|
|
30
|
+
if (isConfiguredInfuraId(value2, placeholder)) return null;
|
|
31
|
+
return {
|
|
32
|
+
message: "Extension product configuration is incomplete: .metamaskprodrc shadows .metamaskrc but does not contain a usable INFURA_PROJECT_ID (the value is never displayed).",
|
|
33
|
+
userAction: `cd ${shellQuote(path.resolve(target))} && \${EDITOR:-vi} .metamaskprodrc`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const config = path.join(target, ".metamaskrc");
|
|
38
|
+
const configSnapshot = readConfigSnapshot(config);
|
|
39
|
+
if (configSnapshot.kind === "missing") {
|
|
40
|
+
return {
|
|
41
|
+
message: "Extension product configuration is required before launch: create .metamaskrc and set a non-placeholder INFURA_PROJECT_ID (the value is never displayed).",
|
|
42
|
+
userAction: `cd ${shellQuote(path.resolve(target))} && cp .metamaskrc.dist .metamaskrc && \${EDITOR:-vi} .metamaskrc`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (configSnapshot.kind === "invalid") {
|
|
46
|
+
return invalidConfigBlock(target, ".metamaskrc", configSnapshot.reason);
|
|
47
|
+
}
|
|
48
|
+
const value = readInfuraProjectId(configSnapshot.text) ?? "";
|
|
49
|
+
if (isConfiguredInfuraId(value, placeholder)) return null;
|
|
50
|
+
return {
|
|
51
|
+
message: "Extension product configuration is incomplete: .metamaskrc must contain a non-placeholder INFURA_PROJECT_ID (the value is never displayed).",
|
|
52
|
+
userAction: `cd ${shellQuote(path.resolve(target))} && \${EDITOR:-vi} .metamaskrc`
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function readConfigSnapshot(file) {
|
|
56
|
+
let descriptor;
|
|
57
|
+
try {
|
|
58
|
+
descriptor = fs.openSync(
|
|
59
|
+
file,
|
|
60
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK
|
|
61
|
+
);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code === "ENOENT") return { kind: "missing" };
|
|
65
|
+
if (code === "ELOOP") return { kind: "invalid", reason: "symlink" };
|
|
66
|
+
return { kind: "invalid", reason: "unreadable" };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.fstatSync(descriptor).isFile()) return { kind: "invalid", reason: "not-regular" };
|
|
70
|
+
return { kind: "file", text: fs.readFileSync(descriptor, "utf8") };
|
|
71
|
+
} finally {
|
|
72
|
+
fs.closeSync(descriptor);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function invalidConfigBlock(target, name, reason) {
|
|
76
|
+
const resolved = shellQuote(path.resolve(target));
|
|
77
|
+
if (name === ".metamaskrc.dist") {
|
|
78
|
+
return {
|
|
79
|
+
message: `Extension product configuration is invalid: ${name} is ${reason === "symlink" ? "a symbolic link" : reason === "not-regular" ? "not a regular file" : "unreadable"}.`,
|
|
80
|
+
userAction: `cd ${resolved} && ls -l ${name}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (reason === "unreadable") {
|
|
84
|
+
return {
|
|
85
|
+
message: `Extension product configuration could not be read from ${name}.`,
|
|
86
|
+
userAction: `cd ${resolved} && ls -l ${name} && \${EDITOR:-vi} ${name}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
message: reason === "symlink" ? `Extension product configuration is invalid: ${name} must not be a symbolic link.` : `Extension product configuration is invalid: ${name} must be a regular file, not its current file type.`,
|
|
91
|
+
userAction: `cd ${resolved} && rm ${name} && ${name === ".metamaskrc" ? "cp .metamaskrc.dist .metamaskrc && " : ""}\${EDITOR:-vi} ${name}`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function readInfuraProjectId(text) {
|
|
95
|
+
const lines = text.replace(/\r\n?/gu, "\n");
|
|
96
|
+
const pattern = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gmu;
|
|
97
|
+
let result;
|
|
98
|
+
for (const match of lines.matchAll(pattern)) {
|
|
99
|
+
if (match[1] !== "INFURA_PROJECT_ID") continue;
|
|
100
|
+
let value = (match[2] ?? "").trim();
|
|
101
|
+
const quote = value[0];
|
|
102
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/u, "$2");
|
|
103
|
+
if (quote === '"') value = value.replace(/\\n/gu, "\n").replace(/\\r/gu, "\r");
|
|
104
|
+
result = value;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
export {
|
|
109
|
+
extensionProductConfigBlock
|
|
110
|
+
};
|
|
@@ -166,6 +166,10 @@ function runtimeDistCheck(target) {
|
|
|
166
166
|
"-rnic",
|
|
167
167
|
"--exclude",
|
|
168
168
|
"_metadata",
|
|
169
|
+
"--exclude",
|
|
170
|
+
"home.html",
|
|
171
|
+
"--exclude",
|
|
172
|
+
"sidepanel.html",
|
|
169
173
|
"--out-format=%n",
|
|
170
174
|
`${dist}${path.sep}`,
|
|
171
175
|
`${runtimeDist}${path.sep}`
|
|
@@ -173,8 +177,22 @@ function runtimeDistCheck(target) {
|
|
|
173
177
|
} catch {
|
|
174
178
|
return { status: "stale", modified: ["<snapshot comparison failed>"] };
|
|
175
179
|
}
|
|
176
|
-
const modified = output.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
177
|
-
|
|
180
|
+
const modified = output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
181
|
+
for (const name of ["home.html", "sidepanel.html"]) {
|
|
182
|
+
const source = path.join(dist, name);
|
|
183
|
+
const loaded = path.join(runtimeDist, name);
|
|
184
|
+
if (normalizedRuntimeHtml(source) !== normalizedRuntimeHtml(loaded)) modified.push(name);
|
|
185
|
+
}
|
|
186
|
+
const boundedModified = [...new Set(modified)].slice(0, 10);
|
|
187
|
+
return boundedModified.length > 0 ? { status: "stale", modified: boundedModified } : { status: "fresh" };
|
|
188
|
+
}
|
|
189
|
+
function normalizedRuntimeHtml(file) {
|
|
190
|
+
if (!fs.existsSync(file)) return null;
|
|
191
|
+
const source = fs.readFileSync(file, "utf8");
|
|
192
|
+
return source.replace(/<title>([^<]*)<\/title>/iu, (_match, title) => {
|
|
193
|
+
const base = String(title || "MetaMask").replace(/^.+?\s+[—–-]\s+/u, "") || "MetaMask";
|
|
194
|
+
return `<title>${base}</title>`;
|
|
195
|
+
});
|
|
178
196
|
}
|
|
179
197
|
function distCheck(target) {
|
|
180
198
|
const manifestPath = path.join(target, "dist/chrome/manifest.json");
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
4
|
+
import { shellQuote } from "../../commands/parse-args.js";
|
|
4
5
|
import {
|
|
5
6
|
isExtensionWatcherLive,
|
|
6
7
|
resolveExtensionSlotPorts,
|
|
7
8
|
stopExtensionRuntime
|
|
8
9
|
} from "../slot-ports.js";
|
|
9
10
|
import { decideExtensionReadiness } from "./runtime-decision.js";
|
|
11
|
+
import { extensionProductConfigBlock } from "./product-config.js";
|
|
10
12
|
function watcherStatus(buildLog) {
|
|
11
13
|
if (buildLog === "ok") return "up";
|
|
12
14
|
if (buildLog === "no-watch") return "down";
|
|
@@ -22,10 +24,25 @@ const extensionSurface = {
|
|
|
22
24
|
const cdpPort = process.env.CDP_PORT ? parseInt(process.env.CDP_PORT, 10) : void 0;
|
|
23
25
|
const report = await decideExtensionReadiness(target, { cdpPort, pageMode: "home-or-sidepanel" });
|
|
24
26
|
const watcherLive = isExtensionWatcherLive(target);
|
|
27
|
+
const productConfigBlock = report.decision === "ready" ? null : extensionProductConfigBlock(target);
|
|
28
|
+
if (productConfigBlock) {
|
|
29
|
+
return {
|
|
30
|
+
decision: "blocked",
|
|
31
|
+
reasonCode: "product-config-required",
|
|
32
|
+
reasons: [productConfigBlock.message],
|
|
33
|
+
nextAction: productConfigBlock.userAction,
|
|
34
|
+
deps: report.checks.deps.status,
|
|
35
|
+
devServer: {
|
|
36
|
+
label: "webpack",
|
|
37
|
+
status: watcherLive ? watcherStatus(report.checks.buildLog.status) : "down"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
25
41
|
return {
|
|
26
42
|
decision: report.decision,
|
|
27
43
|
reasonCode: report.reasonCode,
|
|
28
44
|
reasons: report.reasons,
|
|
45
|
+
nextAction: report.decision === "ready" ? void 0 : `mm-harness launch --adapter extension --target ${shellQuote(path.resolve(target))}`,
|
|
29
46
|
deps: report.checks.deps.status,
|
|
30
47
|
devServer: {
|
|
31
48
|
label: "webpack",
|
|
@@ -65,15 +82,17 @@ const extensionSurface = {
|
|
|
65
82
|
return [
|
|
66
83
|
{ label: "webpack", path: recipeRuntimePath(target, "webpack.log") },
|
|
67
84
|
{ label: "watcher", path: recipeRuntimePath(target, "recipe-harness-webpack.log") },
|
|
68
|
-
{ label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") }
|
|
85
|
+
{ label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") },
|
|
86
|
+
{ label: "dapp", path: recipeRuntimePath(target, "dapp-console.log") }
|
|
69
87
|
];
|
|
70
88
|
},
|
|
71
89
|
appLogSource(target) {
|
|
72
|
-
return { label: "
|
|
90
|
+
return { label: "extension", path: recipeRuntimePath(target, "extension-console.log") };
|
|
73
91
|
},
|
|
74
92
|
hints: {
|
|
75
93
|
launch: "mm-harness launch",
|
|
76
|
-
relaunch: "mm-harness launch --build"
|
|
94
|
+
relaunch: "mm-harness launch --build",
|
|
95
|
+
runtimeProbeRecovery: (target) => `mm-harness launch --adapter extension --target ${shellQuote(path.resolve(target))}`
|
|
77
96
|
}
|
|
78
97
|
};
|
|
79
98
|
export {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { recipeHarnessPath, runnerDir } from "../paths.js";
|
|
6
|
+
async function ensureHarnessFresh(target, adapter) {
|
|
7
|
+
if (!fs.existsSync(path.join(target, "package.json")) || !hasCheckoutHead(target)) return;
|
|
8
|
+
let current;
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import(pathToFileURL(path.join(runnerDir, "adapters/shared/harness-source-fingerprint.mjs")).href);
|
|
11
|
+
current = mod.harnessSourceFingerprint(runnerDir, adapter);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
14
|
+
throw new Error(`could not fingerprint the ${adapter} harness source${detail}`);
|
|
15
|
+
}
|
|
16
|
+
const manifestPath = recipeHarnessPath(target, adapter, "manifest.json");
|
|
17
|
+
const installed = readInstalledFingerprint(manifestPath);
|
|
18
|
+
if (installed === current) return;
|
|
19
|
+
const reason = installed === null ? "harness not installed" : "runner changed";
|
|
20
|
+
const injector = path.join(runnerDir, `adapters/${adapter}/inject.${adapter === "extension" ? "mjs" : "sh"}`);
|
|
21
|
+
try {
|
|
22
|
+
execFileSync(adapter === "extension" ? process.execPath : "bash", [injector, "--target", target], {
|
|
23
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
24
|
+
});
|
|
25
|
+
console.error(`\u2192 refreshed ${adapter} harness overlay (${reason})`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
28
|
+
throw new Error(`could not refresh the ${adapter} harness overlay (${reason})${detail}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hasCheckoutHead(target) {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync("git", ["-C", target, "rev-parse", "--verify", "HEAD"], { stdio: "ignore" });
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readInstalledFingerprint(manifestPath) {
|
|
40
|
+
try {
|
|
41
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
42
|
+
return typeof manifest.sourceFingerprint === "string" ? manifest.sourceFingerprint : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
ensureHarnessFresh
|
|
49
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
const DEFAULTS = {
|
|
@@ -24,14 +25,53 @@ function ensureMobilePerpsEnvironment(target) {
|
|
|
24
25
|
(key) => settingForKey(current, key).status === "missing"
|
|
25
26
|
);
|
|
26
27
|
if (missing.length === 0) return [];
|
|
27
|
-
const file = path.join(target, current.file);
|
|
28
|
-
const
|
|
28
|
+
const file = path.join(fs.realpathSync(target), current.file);
|
|
29
|
+
const snapshot = readEnvironmentSnapshot(file);
|
|
30
|
+
const before = snapshot?.text ?? "";
|
|
29
31
|
const separator = before.length > 0 && !before.endsWith("\n") ? "\n" : "";
|
|
30
32
|
const additions = missing.map((key) => `export ${key}="${DEFAULTS[key]}"`).join("\n");
|
|
31
|
-
|
|
33
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
34
|
+
let descriptor = null;
|
|
35
|
+
try {
|
|
36
|
+
descriptor = fs.openSync(
|
|
37
|
+
temporary,
|
|
38
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
39
|
+
snapshot?.mode ?? 384
|
|
40
|
+
);
|
|
41
|
+
fs.writeFileSync(descriptor, `${before}${separator}${additions}
|
|
32
42
|
`);
|
|
43
|
+
if (snapshot) fs.fchmodSync(descriptor, snapshot.mode);
|
|
44
|
+
fs.closeSync(descriptor);
|
|
45
|
+
descriptor = null;
|
|
46
|
+
fs.renameSync(temporary, file);
|
|
47
|
+
} finally {
|
|
48
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
49
|
+
fs.rmSync(temporary, { force: true });
|
|
50
|
+
}
|
|
33
51
|
return missing;
|
|
34
52
|
}
|
|
53
|
+
function readEnvironmentSnapshot(file) {
|
|
54
|
+
let descriptor;
|
|
55
|
+
try {
|
|
56
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const code = error.code;
|
|
59
|
+
if (code === "ENOENT") return null;
|
|
60
|
+
if (code === "ELOOP") {
|
|
61
|
+
throw new Error(`.js.env must not be a symbolic link. Next: replace ${file} with a regular file.`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.fstatSync(descriptor);
|
|
67
|
+
if (!stat.isFile()) {
|
|
68
|
+
throw new Error(`.js.env must be a regular file. Next: replace ${file} with a regular file.`);
|
|
69
|
+
}
|
|
70
|
+
return { text: fs.readFileSync(descriptor, "utf8"), mode: stat.mode & 511 };
|
|
71
|
+
} finally {
|
|
72
|
+
fs.closeSync(descriptor);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
35
75
|
function readSetting(source, key) {
|
|
36
76
|
const assignment = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=(.*)$`, "u");
|
|
37
77
|
let value = null;
|
|
@@ -49,6 +49,8 @@ async function prepareMobile(target, opts = {}) {
|
|
|
49
49
|
return { status: EXIT.runtime, output: msg };
|
|
50
50
|
}
|
|
51
51
|
if (report.decision === "ready") {
|
|
52
|
+
const surface = await ensureMobileHumanSurface(target, platform, json);
|
|
53
|
+
if (surface.status !== 0) return surface;
|
|
52
54
|
const confirm = await dispatchAction(
|
|
53
55
|
{ id: "wait-for-bridge", cwd: target, argv: ["--max-polls", String(READY_BRIDGE_CONFIRM_POLLS)] },
|
|
54
56
|
target,
|
|
@@ -56,14 +58,21 @@ async function prepareMobile(target, opts = {}) {
|
|
|
56
58
|
json,
|
|
57
59
|
preflightMode
|
|
58
60
|
);
|
|
59
|
-
if (confirm.status === 0) return
|
|
61
|
+
if (confirm.status === 0) return startMobileConsoleForwarder(target, platform, json);
|
|
60
62
|
if (!json) {
|
|
61
63
|
process.stderr.write(
|
|
62
64
|
`launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
|
|
63
65
|
`
|
|
64
66
|
);
|
|
65
67
|
}
|
|
66
|
-
|
|
68
|
+
const launch = await dispatchActionSequence(
|
|
69
|
+
launchActions(path.resolve(target)),
|
|
70
|
+
target,
|
|
71
|
+
platform,
|
|
72
|
+
json,
|
|
73
|
+
preflightMode
|
|
74
|
+
);
|
|
75
|
+
return launch.status === 0 ? startMobileConsoleForwarder(target, platform, json) : launch;
|
|
67
76
|
}
|
|
68
77
|
if (report.decision === "unknown") {
|
|
69
78
|
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
@@ -93,6 +102,8 @@ async function prepareMobile(target, opts = {}) {
|
|
|
93
102
|
return { status: EXIT.runtime, output: msg };
|
|
94
103
|
}
|
|
95
104
|
case "ready": {
|
|
105
|
+
const surface = await ensureMobileHumanSurface(target, platform, json);
|
|
106
|
+
if (surface.status !== 0) return surface;
|
|
96
107
|
const bridge = await dispatchAction({ id: "wait-for-bridge", cwd: target }, target, platform, json, preflightMode);
|
|
97
108
|
if (bridge.status !== 0) return bridge;
|
|
98
109
|
break;
|
|
@@ -111,7 +122,19 @@ async function prepareMobile(target, opts = {}) {
|
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
}
|
|
114
|
-
return
|
|
125
|
+
return startMobileConsoleForwarder(target, platform, json);
|
|
126
|
+
}
|
|
127
|
+
function startMobileConsoleForwarder(target, platform, json) {
|
|
128
|
+
return dispatchAction(
|
|
129
|
+
{ id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
|
|
130
|
+
target,
|
|
131
|
+
platform,
|
|
132
|
+
json
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function ensureMobileHumanSurface(target, platform, json) {
|
|
136
|
+
if (platform !== "ios") return Promise.resolve({ status: 0, output: "" });
|
|
137
|
+
return dispatchAction({ id: "ensure-device-ui", cwd: target }, target, platform, json);
|
|
115
138
|
}
|
|
116
139
|
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
|
|
117
140
|
const resolved = path.resolve(target);
|
|
@@ -147,6 +170,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
147
170
|
const extra = action.argv ?? [];
|
|
148
171
|
return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
|
|
149
172
|
}
|
|
173
|
+
case "start-console-forwarder": {
|
|
174
|
+
const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
|
|
175
|
+
return spawnScriptStreaming(leaf, ["--target", cwd, ...action.argv ?? []], target);
|
|
176
|
+
}
|
|
150
177
|
case "prewarm-bundle": {
|
|
151
178
|
const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
152
179
|
return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
|
|
@@ -156,10 +183,6 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
156
183
|
const extra = action.argv ?? [];
|
|
157
184
|
return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
|
|
158
185
|
}
|
|
159
|
-
case "clear-metro-cache": {
|
|
160
|
-
const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
|
|
161
|
-
return spawnScriptStreaming(leaf, ["--target", cwd, "--clear"], target);
|
|
162
|
-
}
|
|
163
186
|
case "launch-mobile-runtime": {
|
|
164
187
|
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
165
188
|
return spawnScriptStreaming(
|
|
@@ -169,6 +192,15 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
|
|
|
169
192
|
POD_PROBE_ENV
|
|
170
193
|
);
|
|
171
194
|
}
|
|
195
|
+
case "ensure-device-ui": {
|
|
196
|
+
const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
197
|
+
return spawnScriptStreaming(
|
|
198
|
+
leaf,
|
|
199
|
+
["--platform", platform, "--target", cwd, "--ui-only"],
|
|
200
|
+
target,
|
|
201
|
+
POD_PROBE_ENV
|
|
202
|
+
);
|
|
203
|
+
}
|
|
172
204
|
case "rebuild-native-dev-client": {
|
|
173
205
|
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.";
|
|
174
206
|
if (!json) process.stderr.write(`${msg}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
4
|
+
import { shellQuote } from "../../commands/parse-args.js";
|
|
4
5
|
import { resolveMobileSlotPorts } from "../slot-ports.js";
|
|
5
6
|
import { mobileRuntimeStatus } from "./prepare.js";
|
|
6
7
|
import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
|
|
@@ -20,6 +21,7 @@ const mobileSurface = {
|
|
|
20
21
|
decision: depsPending ? "launch" : report.decision,
|
|
21
22
|
reasonCode: depsPending ? "app-installed-deps-pending" : report.reasonCode,
|
|
22
23
|
reasons: depsPending ? ["Runway app is installed; JavaScript dependencies are pending until dispatch-time launch."] : report.reasons,
|
|
24
|
+
nextAction: report.decision === "ready" && !depsPending ? void 0 : `mm-harness launch ${platform} --adapter mobile --target ${shellQuote(path.resolve(target))}`,
|
|
23
25
|
deps: runwayProvisioned && report.checks?.deps?.status !== "current" ? "pending" : report.checks?.deps?.status,
|
|
24
26
|
devServer: { label: "metro", status: report.checks?.metro?.status ?? "unprobed" }
|
|
25
27
|
};
|
|
@@ -47,11 +49,12 @@ const mobileSurface = {
|
|
|
47
49
|
return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
|
|
48
50
|
},
|
|
49
51
|
appLogSource(target) {
|
|
50
|
-
return { label: "app", path: recipeRuntimePath(target, "
|
|
52
|
+
return { label: "app", path: recipeRuntimePath(target, "app-console.log") };
|
|
51
53
|
},
|
|
52
54
|
hints: {
|
|
53
55
|
launch: "mm-harness launch ios",
|
|
54
|
-
relaunch: "mm-harness launch ios"
|
|
56
|
+
relaunch: "mm-harness launch ios",
|
|
57
|
+
runtimeProbeRecovery: (target) => `mm-harness status --target ${shellQuote(path.resolve(target))} --json`
|
|
55
58
|
}
|
|
56
59
|
};
|
|
57
60
|
export {
|