@deeeed/metamask-harness 0.17.5 → 0.18.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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- 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 +1 -1
- 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 +90 -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 +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- 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/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 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- 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 +2 -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 +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- 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/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- 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/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
|
@@ -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 {
|
|
@@ -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 {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatKvLines,
|
|
3
|
+
formatExtensionIdentityPorts,
|
|
3
4
|
formatExtensionSlotPorts,
|
|
4
5
|
inferSlotSuffix,
|
|
5
6
|
realRepoPath,
|
|
@@ -7,12 +8,12 @@ import {
|
|
|
7
8
|
resolveExtensionRuntimeContext,
|
|
8
9
|
resolveExtensionRuntimePorts,
|
|
9
10
|
resolveSlotPortsByRepo,
|
|
10
|
-
resolveFarmslotPortsByRepo,
|
|
11
11
|
resolveMobileRuntimeContext,
|
|
12
12
|
resolveMobileRuntimePorts,
|
|
13
13
|
resolveMobileSlotDefaults
|
|
14
14
|
} from "../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
15
15
|
export {
|
|
16
|
+
formatExtensionIdentityPorts,
|
|
16
17
|
formatExtensionSlotPorts,
|
|
17
18
|
formatKvLines,
|
|
18
19
|
inferSlotSuffix,
|
|
@@ -20,7 +21,6 @@ export {
|
|
|
20
21
|
resolveDefaultExtensionPorts,
|
|
21
22
|
resolveExtensionRuntimeContext,
|
|
22
23
|
resolveExtensionRuntimePorts,
|
|
23
|
-
resolveFarmslotPortsByRepo,
|
|
24
24
|
resolveMobileRuntimeContext,
|
|
25
25
|
resolveMobileRuntimePorts,
|
|
26
26
|
resolveMobileSlotDefaults,
|
|
@@ -3,8 +3,8 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { recipeRuntimeDir } from "../paths.js";
|
|
5
5
|
import {
|
|
6
|
+
formatExtensionIdentityPorts,
|
|
6
7
|
resolveDefaultExtensionPorts,
|
|
7
|
-
resolveExtensionRuntimeContext,
|
|
8
8
|
resolveMobileRuntimeContext,
|
|
9
9
|
resolveSlotPortsByRepo,
|
|
10
10
|
resolveMobileSlotDefaults
|
|
@@ -55,12 +55,10 @@ function resolveMobileSlotPorts(target) {
|
|
|
55
55
|
}
|
|
56
56
|
function resolveExtensionSlotPorts(target) {
|
|
57
57
|
const resolved = path.resolve(target);
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
const defOut = resolveDefaultExtensionPorts(resolved);
|
|
63
|
-
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
58
|
+
const identity = formatExtensionIdentityPorts(resolved);
|
|
59
|
+
if (identity.trim()) applyKVLines(identity, true);
|
|
60
|
+
const fallback = resolveDefaultExtensionPorts(resolved);
|
|
61
|
+
if (fallback?.trim()) applyKVLines(fallback, false);
|
|
64
62
|
}
|
|
65
63
|
function extensionWatcherPids(target) {
|
|
66
64
|
const resolved = path.resolve(target);
|
|
@@ -78,7 +76,9 @@ function extensionWatcherPids(target) {
|
|
|
78
76
|
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
79
77
|
if (!match) continue;
|
|
80
78
|
const [, pidStr, cmd] = match;
|
|
81
|
-
const
|
|
79
|
+
const executable = path.basename(cmd.trim().split(/\s+/u)[0] ?? "");
|
|
80
|
+
const isNodeProcess = executable === "node" || executable === "nodejs" || executable === "yarn";
|
|
81
|
+
const isWatcher = isNodeProcess && (/(?:^|\/)yarn start(?:\s|$)/u.test(cmd) || /(?:^|\/)yarn\.js webpack --watch(?:\s|$)/u.test(cmd) || /development\/webpack\/launch\.ts --watch(?:\s|$)/u.test(cmd));
|
|
82
82
|
if (!isWatcher) continue;
|
|
83
83
|
if (cmd.includes(resolved)) {
|
|
84
84
|
ownedPids.add(Number(pidStr));
|
|
@@ -127,9 +127,8 @@ function stopExtensionWatcher(target) {
|
|
|
127
127
|
function stopExtensionRuntime(target) {
|
|
128
128
|
const resolved = path.resolve(target);
|
|
129
129
|
const runtimeAbs = path.join(resolved, recipeRuntimeDir());
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
);
|
|
130
|
+
const configuredProfile = process.env.CHROME_USER_DATA_DIR || path.join(runtimeAbs, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
131
|
+
const profiles = process.env.CHROME_USER_DATA_DIR ? [path.resolve(configuredProfile)] : [configuredProfile, path.join(runtimeAbs, "chrome-profile-recipe"), path.join(runtimeAbs, "chrome-profile-pw")].map((value) => path.resolve(value));
|
|
133
132
|
let signalled = stopExtensionWatcher(resolved);
|
|
134
133
|
const ownedBrowserPids = /* @__PURE__ */ new Set();
|
|
135
134
|
try {
|
|
@@ -138,9 +137,7 @@ function stopExtensionRuntime(target) {
|
|
|
138
137
|
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
139
138
|
if (!match) continue;
|
|
140
139
|
const pid = Number(match[1]);
|
|
141
|
-
if (pid !== process.pid && commandHasExactArg(match[2], "--user-data-dir", profile))
|
|
142
|
-
ownedBrowserPids.add(pid);
|
|
143
|
-
}
|
|
140
|
+
if (pid !== process.pid && profiles.some((profile) => commandHasExactArg(match[2], "--user-data-dir", profile))) ownedBrowserPids.add(pid);
|
|
144
141
|
}
|
|
145
142
|
} catch {
|
|
146
143
|
}
|
|
@@ -195,8 +192,8 @@ function processAlive(pid) {
|
|
|
195
192
|
try {
|
|
196
193
|
process.kill(pid, 0);
|
|
197
194
|
return true;
|
|
198
|
-
} catch {
|
|
199
|
-
return
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return error.code === "EPERM";
|
|
200
197
|
}
|
|
201
198
|
}
|
|
202
199
|
function commandHasExactArg(command, flag, value) {
|