@deeeed/metamask-harness 0.34.3 → 0.35.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 +21 -0
- package/adapters/core/inject.sh +3 -3
- package/adapters/extension/inject.mjs +3 -4
- package/adapters/mobile/inject.sh +4 -3
- package/adapters/mobile/verify.sh +37 -0
- package/dist/adapters/extension/runtime.js +60 -15
- package/dist/adapters/extension/surface.js +6 -1
- package/dist/adapters/mobile/source-freshness.js +51 -14
- package/dist/adapters/mobile/surface.js +6 -1
- package/dist/doctor.js +30 -1
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +38 -11
- package/site/architecture.html +14 -7
- package/site/assets/metamask-fox.svg +24 -0
- package/site/assets/progress.mjs +52 -1
- package/site/assets/style.css +152 -25
- package/site/cheatsheet.html +5 -6
- package/site/ecosystem.html +162 -0
- package/site/how-it-works.html +8 -9
- package/site/index.html +104 -39
- package/site/perps.html +103 -45
- package/site/recipes.html +5 -6
- package/site/reviewers.html +5 -6
- package/site/tutorials/index.html +5 -6
- package/site/tutorials/v1.html +5 -6
- package/site/tutorials/v2.html +6 -8
- package/site/tutorials/v3.html +5 -6
- package/site/tutorials/v4.html +5 -6
- package/site/tutorials/v5.html +5 -6
- package/site/tutorials/v6.html +5 -6
- package/site/tutorials/v7.html +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.35.0 - 2026-08-12
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Make Extension and Mobile launch verification fail when their in-app EVM network client cannot complete a live RPC, with exact Infura configuration and retry guidance.
|
|
10
|
+
- Make Extension doctor require checkout-pinned Playwright Chromium and report the exact install command when it is missing.
|
|
11
|
+
- Make doctor report the exact dependency-install command when `--fix` finds stale or missing dependencies instead of implying they were repaired.
|
|
12
|
+
- Make the setup prompt reuse configured skill sources and require consent before cloning or editing shell startup files.
|
|
13
|
+
- Keep installed overlays delegated to the source runner that installed them before falling back to a potentially stale global runner.
|
|
14
|
+
|
|
15
|
+
## 0.34.4 - 2026-08-12
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Split the site quick start into one-time setup and task-focused workflow tabs.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Update the transitive `tar` dependency from 7.5.19 to 7.5.22.
|
|
24
|
+
- Update transitive `js-yaml`, `nanoid`, and `postcss` dependencies.
|
|
25
|
+
|
|
5
26
|
## 0.34.3 - 2026-08-12
|
|
6
27
|
|
|
7
28
|
### Added
|
package/adapters/core/inject.sh
CHANGED
|
@@ -88,11 +88,11 @@ install_v1_runner_assets() {
|
|
|
88
88
|
if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
|
|
89
89
|
printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
|
|
90
90
|
fi
|
|
91
|
-
#
|
|
92
|
-
#
|
|
91
|
+
# The recorded runner owns the installed overlay and must remain ahead of an
|
|
92
|
+
# unrelated global version. MM_HARNESS_BIN is the explicit override.
|
|
93
93
|
printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi'
|
|
94
|
-
printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
|
|
95
94
|
printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$runner_exec_q" "$runner_exec_q"
|
|
95
|
+
printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
|
|
96
96
|
printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2'
|
|
97
97
|
printf '%s\n' 'exit 127'
|
|
98
98
|
} > "$HARNESS_DIR/runner/bin/mm-harness"
|
|
@@ -69,17 +69,16 @@ fs.mkdirSync(path.join(harnessDir, 'runner/recipes'), { recursive: true });
|
|
|
69
69
|
fs.rmSync(path.join(harnessDir, 'scripts'), { recursive: true, force: true });
|
|
70
70
|
fs.mkdirSync(path.join(harnessDir, 'scripts/lib'), { recursive: true });
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// the runner recorded at install time as a last resort; teach install on miss.
|
|
72
|
+
// Prefer the runner that installed the overlay so its runtime checks match the
|
|
73
|
+
// installed scripts. Explicit overrides still support controlled delegation.
|
|
75
74
|
const pinnedBin = shellQuote(path.join(runnerDir, 'bin/mm-harness'));
|
|
76
75
|
const delegate = [
|
|
77
76
|
'#!/usr/bin/env bash',
|
|
78
77
|
'set -euo pipefail',
|
|
79
78
|
protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
|
|
80
79
|
'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi',
|
|
81
|
-
'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
|
|
82
80
|
`if [ -x ${pinnedBin} ]; then exec ${pinnedBin} "$@"; fi`,
|
|
81
|
+
'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
|
|
83
82
|
'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2',
|
|
84
83
|
'exit 127',
|
|
85
84
|
].filter(Boolean).join('\n') + '\n';
|
|
@@ -44,7 +44,7 @@ for rel in "$HARNESS_REL" "$HARNESS_REL/runner" "$HARNESS_REL/scripts"; do
|
|
|
44
44
|
done
|
|
45
45
|
done
|
|
46
46
|
|
|
47
|
-
rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts"
|
|
47
|
+
rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts" "$HARNESS_DIR/bridge-runtime"
|
|
48
48
|
mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes" "$HARNESS_DIR/scripts/lib"
|
|
49
49
|
|
|
50
50
|
RUNNER_EXEC_Q="$(printf '%q' "$RUNNER_DIR/bin/mm-harness")"
|
|
@@ -62,8 +62,8 @@ PROTOCOL_ROOT_Q="$(printf '%q' "$PROTOCOL_ROOT")"
|
|
|
62
62
|
printf 'export METAMASK_RUNNER_PROTOCOL_ROOT=${METAMASK_RUNNER_PROTOCOL_ROOT:-%s}\n' "$PROTOCOL_ROOT_Q"
|
|
63
63
|
fi
|
|
64
64
|
printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "$MM_HARNESS_BIN" ]; then exec "$MM_HARNESS_BIN" "$@"; fi'
|
|
65
|
-
printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
|
|
66
65
|
printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$RUNNER_EXEC_Q" "$RUNNER_EXEC_Q"
|
|
66
|
+
printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
|
|
67
67
|
printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness@latest" >&2' 'exit 127'
|
|
68
68
|
} > "$HARNESS_DIR/runner/bin/mm-harness"
|
|
69
69
|
chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
|
|
@@ -77,6 +77,7 @@ rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
|
|
|
77
77
|
rm -rf "$HARNESS_DIR/runner/flows"
|
|
78
78
|
rm -f "$HARNESS_DIR/runner/library.json"
|
|
79
79
|
cp "$RUNNER_DIR/adapters/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
|
|
80
|
+
cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
|
|
80
81
|
cp "$RUNNER_DIR/adapters/shared/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
|
|
81
82
|
cp "$RUNNER_DIR/adapters/shared/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
|
|
82
83
|
chmod -R u+rwX,go+rX "$HARNESS_DIR"
|
|
@@ -114,7 +115,7 @@ fs.writeFileSync(manifestPath, JSON.stringify({
|
|
|
114
115
|
actionManifestPath: `${harnessRel}/action-manifest.json`,
|
|
115
116
|
runnerEntrypoint: `${harnessRel}/runner/bin/mm-harness`,
|
|
116
117
|
runtimeHelpers: { verify: `${harnessRel}/scripts/verify.sh` },
|
|
117
|
-
harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/action-manifest.json`],
|
|
118
|
+
harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/bridge-runtime`, `${harnessRel}/action-manifest.json`],
|
|
118
119
|
productOwnedPaths: ['app/dev-tools/AgenticService', 'app/core/NavigationService/NavigationService.ts'],
|
|
119
120
|
cleanupCommand,
|
|
120
121
|
productDiffExcludes: [`:(exclude)${harnessRoot}`],
|
|
@@ -325,6 +325,35 @@ JSON
|
|
|
325
325
|
) > "$log_path" 2>&1
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
evm_rpc_ready() {
|
|
329
|
+
local log_path="$1"
|
|
330
|
+
local bridge="$HARNESS_DIR/bridge-runtime/cdp-bridge.cjs"
|
|
331
|
+
local expression
|
|
332
|
+
expression="(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()"
|
|
333
|
+
[ -f "$bridge" ] || {
|
|
334
|
+
printf 'Mobile bridge is missing at %s\n' "$bridge" > "$log_path"
|
|
335
|
+
return 1
|
|
336
|
+
}
|
|
337
|
+
if ! run_with_timeout "$log_path" 25 env \
|
|
338
|
+
APP_ROOT="$TARGET" \
|
|
339
|
+
WATCHER_PORT="$port" \
|
|
340
|
+
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
341
|
+
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
342
|
+
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
343
|
+
node "$bridge" eval-async "$expression"; then
|
|
344
|
+
return 1
|
|
345
|
+
fi
|
|
346
|
+
node - "$log_path" <<'NODE'
|
|
347
|
+
const fs = require('fs');
|
|
348
|
+
try {
|
|
349
|
+
const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
350
|
+
process.exit(result.ok === true ? 0 : 1);
|
|
351
|
+
} catch {
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
NODE
|
|
355
|
+
}
|
|
356
|
+
|
|
328
357
|
ensure_live_runtime() {
|
|
329
358
|
local device_target="$1"
|
|
330
359
|
local attempt
|
|
@@ -419,6 +448,14 @@ JSON
|
|
|
419
448
|
add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
|
|
420
449
|
status="fail"
|
|
421
450
|
fi
|
|
451
|
+
|
|
452
|
+
if evm_rpc_ready "$ARTIFACTS/logs/evm-rpc-readiness.log"; then
|
|
453
|
+
checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"pass\"}")
|
|
454
|
+
else
|
|
455
|
+
checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"fail\",\"detail\":\"see logs/evm-rpc-readiness.log\"}")
|
|
456
|
+
add_note "EVM RPC readiness probe failed. Infura configured ≠ RPC reachable. Next: confirm $TARGET/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch $PLATFORM --verify --target '$TARGET'."
|
|
457
|
+
status="fail"
|
|
458
|
+
fi
|
|
422
459
|
fi
|
|
423
460
|
|
|
424
461
|
RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
|
|
@@ -153,12 +153,26 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
|
153
153
|
if (runtime.backgroundUnresponsive === true) {
|
|
154
154
|
findings.push("Extension UI reports background connection unresponsive.");
|
|
155
155
|
}
|
|
156
|
+
if (runtime.ethereumConnectionUnavailable === true) {
|
|
157
|
+
findings.push('Extension UI reports "Unable to connect to Ethereum".');
|
|
158
|
+
}
|
|
159
|
+
if (runtime.evmRpcProbeOk !== true) {
|
|
160
|
+
const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
|
|
161
|
+
findings.push(
|
|
162
|
+
`EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
156
165
|
if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
|
|
157
166
|
warnings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
|
|
158
167
|
}
|
|
159
168
|
const extensionId = safeExtensionId(target);
|
|
169
|
+
const evmRpcUnreachable = runtime.ethereumConnectionUnavailable === true || runtime.evmRpcProbeOk !== true;
|
|
160
170
|
return {
|
|
161
171
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
172
|
+
...evmRpcUnreachable ? {
|
|
173
|
+
errorCode: "EVM_RPC_UNREACHABLE",
|
|
174
|
+
userAction: evmRpcRecoveryAction(projectRoot)
|
|
175
|
+
} : {},
|
|
162
176
|
warnings,
|
|
163
177
|
cdpPort,
|
|
164
178
|
targetUrl: target.url,
|
|
@@ -219,21 +233,42 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
219
233
|
const storeState = hooks.store?.getState?.() || {};
|
|
220
234
|
const manager = hooks.getPerpsStreamManager?.();
|
|
221
235
|
const accountCache = manager?.account?.cache;
|
|
236
|
+
const networkConnectionBanner = storeState.metamask?.networkConnectionBanner;
|
|
237
|
+
const networkClientId = storeState.metamask?.selectedNetworkClientId || networkConnectionBanner?.networkClientId;
|
|
222
238
|
return Promise.race([
|
|
223
239
|
(async () => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
const [backgroundProbe, evmRpcProbe] = await Promise.all([
|
|
241
|
+
(async () => {
|
|
242
|
+
if (typeof hooks.submitRequestToBackground !== 'function') {
|
|
243
|
+
return { ok: false, error: 'submitRequestToBackground is not a function' };
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
|
|
247
|
+
const ok = Boolean(accountState && typeof accountState === 'object');
|
|
248
|
+
return { ok, error: ok ? null : 'perpsGetAccountState returned an empty result' };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return { ok: false, error: String(error?.message || error) };
|
|
251
|
+
}
|
|
252
|
+
})(),
|
|
253
|
+
(async () => {
|
|
254
|
+
if (typeof hooks.submitRequestToBackground !== 'function') {
|
|
255
|
+
return { ok: false, error: 'submitRequestToBackground is not a function' };
|
|
256
|
+
}
|
|
257
|
+
if (typeof networkClientId !== 'string' || networkClientId.length === 0) {
|
|
258
|
+
return { ok: false, error: 'selected EVM network client is unavailable' };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const code = await hooks.submitRequestToBackground('getCode', [
|
|
262
|
+
'0x0000000000000000000000000000000000000000',
|
|
263
|
+
networkClientId,
|
|
264
|
+
]);
|
|
265
|
+
const ok = typeof code === 'string' && code.startsWith('0x');
|
|
266
|
+
return { ok, error: ok ? null : 'background EVM RPC returned an invalid result' };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return { ok: false, error: String(error?.message || error) };
|
|
269
|
+
}
|
|
270
|
+
})(),
|
|
271
|
+
]);
|
|
237
272
|
return {
|
|
238
273
|
href: location.href,
|
|
239
274
|
title: document.title,
|
|
@@ -242,14 +277,17 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
242
277
|
hasStore: Boolean(hooks.store),
|
|
243
278
|
hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
|
|
244
279
|
backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
|
|
280
|
+
ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
|
|
245
281
|
activeProvider: storeState.metamask?.activeProvider || null,
|
|
246
282
|
isTestnet: Boolean(storeState.metamask?.isTestnet),
|
|
247
283
|
perpsManagerInitialized: Boolean(manager?.isInitialized?.()),
|
|
248
284
|
positionsCacheIsArray: Array.isArray(manager?.positions?.cache),
|
|
249
285
|
ordersCacheIsArray: Array.isArray(manager?.orders?.cache),
|
|
250
286
|
accountCachePresent: Boolean(accountCache && typeof accountCache === 'object'),
|
|
251
|
-
backgroundProbeOk,
|
|
252
|
-
backgroundProbeError,
|
|
287
|
+
backgroundProbeOk: backgroundProbe.ok,
|
|
288
|
+
backgroundProbeError: backgroundProbe.error,
|
|
289
|
+
evmRpcProbeOk: evmRpcProbe.ok,
|
|
290
|
+
evmRpcProbeError: evmRpcProbe.error,
|
|
253
291
|
};
|
|
254
292
|
})(),
|
|
255
293
|
new Promise((resolve) => setTimeout(() => resolve({
|
|
@@ -260,8 +298,11 @@ async function evaluateHealth(session, timeoutMs) {
|
|
|
260
298
|
hasStore: Boolean(hooks.store),
|
|
261
299
|
hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
|
|
262
300
|
backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
|
|
301
|
+
ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
|
|
263
302
|
backgroundProbeOk: false,
|
|
264
303
|
backgroundProbeError: 'perpsGetAccountState timed out after ${backgroundProbeTimeoutMs}ms',
|
|
304
|
+
evmRpcProbeOk: false,
|
|
305
|
+
evmRpcProbeError: 'background EVM RPC timed out after ${backgroundProbeTimeoutMs}ms',
|
|
265
306
|
}), ${backgroundProbeTimeoutMs})),
|
|
266
307
|
]);
|
|
267
308
|
})()`,
|
|
@@ -293,6 +334,9 @@ function compositorRecoveryAction(projectRoot) {
|
|
|
293
334
|
const recovery = `mm-harness launch --verify --target ${JSON.stringify(projectRoot)}`;
|
|
294
335
|
return process.platform === "darwin" ? `Unlock the macOS session, then rerun: ${recovery}` : `Restore an active display compositor, then rerun: ${recovery}`;
|
|
295
336
|
}
|
|
337
|
+
function evmRpcRecoveryAction(projectRoot) {
|
|
338
|
+
return `Confirm ${shellQuote(path.join(projectRoot, ".metamaskrc"))} has a real INFURA_PROJECT_ID (not 00000000000), then rerun: mm-harness launch --verify --target ${shellQuote(projectRoot)}`;
|
|
339
|
+
}
|
|
296
340
|
function resolveCdpPort(rawPort, slot) {
|
|
297
341
|
const raw = rawPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT ?? slot?.cdpPort;
|
|
298
342
|
const port = Number(raw);
|
|
@@ -759,6 +803,7 @@ export {
|
|
|
759
803
|
assertHealthyExtensionRuntime,
|
|
760
804
|
checkExtensionRuntimeHealth,
|
|
761
805
|
compositorRecoveryAction,
|
|
806
|
+
evmRpcRecoveryAction,
|
|
762
807
|
extensionBackgroundProbeTimeoutMs,
|
|
763
808
|
formatHealthFailure,
|
|
764
809
|
prepareExtensionRuntime
|
|
@@ -14,6 +14,10 @@ function watcherStatus(buildLog) {
|
|
|
14
14
|
if (buildLog === "no-watch") return "down";
|
|
15
15
|
return buildLog;
|
|
16
16
|
}
|
|
17
|
+
function extensionRuntimeNextAction(reasonCode, target) {
|
|
18
|
+
const resolved = shellQuote(path.resolve(target));
|
|
19
|
+
return reasonCode === "deps-missing" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn install --immutable` : `mm-harness launch --adapter extension --target ${resolved}`;
|
|
20
|
+
}
|
|
17
21
|
const extensionSurface = {
|
|
18
22
|
adapter: "extension",
|
|
19
23
|
headless: false,
|
|
@@ -42,7 +46,7 @@ const extensionSurface = {
|
|
|
42
46
|
decision: report.decision,
|
|
43
47
|
reasonCode: report.reasonCode,
|
|
44
48
|
reasons: report.reasons,
|
|
45
|
-
nextAction: report.decision === "ready" ? void 0 :
|
|
49
|
+
nextAction: report.decision === "ready" ? void 0 : extensionRuntimeNextAction(report.reasonCode, target),
|
|
46
50
|
deps: report.checks.deps.status,
|
|
47
51
|
devServer: {
|
|
48
52
|
label: "webpack",
|
|
@@ -96,5 +100,6 @@ const extensionSurface = {
|
|
|
96
100
|
}
|
|
97
101
|
};
|
|
98
102
|
export {
|
|
103
|
+
extensionRuntimeNextAction,
|
|
99
104
|
extensionSurface
|
|
100
105
|
};
|
|
@@ -21,20 +21,28 @@ const ENV_INPUTS = [".js.env", ".env", ".env.local"];
|
|
|
21
21
|
const BASELINE_FILE = "mobile-source-baseline.json";
|
|
22
22
|
function mobileSourceFingerprint(target) {
|
|
23
23
|
const hash = createHash("sha256");
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
...SOURCE_PATHS
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
const head = tryGit(target, ["rev-parse", "HEAD"]);
|
|
25
|
+
if (head === null) {
|
|
26
|
+
hash.update("non-git\0");
|
|
27
|
+
for (const relative of SOURCE_PATHS) {
|
|
28
|
+
hashPath(hash, target, relative);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
hash.update(head);
|
|
32
|
+
hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
|
|
33
|
+
const untracked = git(target, [
|
|
34
|
+
"ls-files",
|
|
35
|
+
"--others",
|
|
36
|
+
"--exclude-standard",
|
|
37
|
+
"-z",
|
|
38
|
+
"--",
|
|
39
|
+
...SOURCE_PATHS
|
|
40
|
+
]).toString("utf8").split("\0").filter(Boolean).sort();
|
|
41
|
+
for (const relative of untracked) {
|
|
42
|
+
hash.update(`untracked\0${relative}\0`);
|
|
43
|
+
hash.update(fs.readFileSync(path.join(target, relative)));
|
|
44
|
+
hash.update("\0");
|
|
45
|
+
}
|
|
38
46
|
}
|
|
39
47
|
for (const relative of ENV_INPUTS) {
|
|
40
48
|
const absolute = path.join(target, relative);
|
|
@@ -84,6 +92,35 @@ function git(target, args) {
|
|
|
84
92
|
stdio: ["ignore", "pipe", "ignore"]
|
|
85
93
|
});
|
|
86
94
|
}
|
|
95
|
+
function tryGit(target, args) {
|
|
96
|
+
try {
|
|
97
|
+
return git(target, args);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function hashPath(hash, root, relative) {
|
|
103
|
+
const absolute = path.join(root, relative);
|
|
104
|
+
if (!fs.existsSync(absolute)) {
|
|
105
|
+
hash.update(`absent\0${relative}\0`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const stat = fs.lstatSync(absolute);
|
|
109
|
+
if (stat.isDirectory()) {
|
|
110
|
+
hash.update(`directory\0${relative}\0`);
|
|
111
|
+
for (const child of fs.readdirSync(absolute).sort()) {
|
|
112
|
+
hashPath(hash, root, path.join(relative, child));
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (stat.isSymbolicLink()) {
|
|
117
|
+
hash.update(`symlink\0${relative}\0${fs.readlinkSync(absolute)}\0`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
hash.update(`file\0${relative}\0`);
|
|
121
|
+
hash.update(fs.readFileSync(absolute));
|
|
122
|
+
hash.update("\0");
|
|
123
|
+
}
|
|
87
124
|
function readBaseline(target) {
|
|
88
125
|
try {
|
|
89
126
|
const parsed = JSON.parse(
|
|
@@ -5,6 +5,10 @@ import { shellQuote } from "../../commands/parse-args.js";
|
|
|
5
5
|
import { resolveMobileSlotPorts } from "../slot-ports.js";
|
|
6
6
|
import { mobileRuntimeStatus } from "./prepare.js";
|
|
7
7
|
import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
|
|
8
|
+
function mobileRuntimeNextAction(reasonCode, target, platform) {
|
|
9
|
+
const resolved = shellQuote(path.resolve(target));
|
|
10
|
+
return reasonCode === "deps-missing" || reasonCode === "deps-partial" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn setup` : `mm-harness launch ${platform} --adapter mobile --target ${resolved}`;
|
|
11
|
+
}
|
|
8
12
|
const mobileSurface = {
|
|
9
13
|
adapter: "mobile",
|
|
10
14
|
headless: false,
|
|
@@ -21,7 +25,7 @@ const mobileSurface = {
|
|
|
21
25
|
decision: depsPending ? "launch" : report.decision,
|
|
22
26
|
reasonCode: depsPending ? "app-installed-deps-pending" : report.reasonCode,
|
|
23
27
|
reasons: depsPending ? ["Runway app is installed; JavaScript dependencies are pending until dispatch-time launch."] : report.reasons,
|
|
24
|
-
nextAction: report.decision === "ready" && !depsPending ? void 0 :
|
|
28
|
+
nextAction: report.decision === "ready" && !depsPending ? void 0 : mobileRuntimeNextAction(report.reasonCode, target, platform),
|
|
25
29
|
deps: runwayProvisioned && report.checks?.deps?.status !== "current" ? "pending" : report.checks?.deps?.status,
|
|
26
30
|
devServer: { label: "metro", status: report.checks?.metro?.status ?? "unprobed" }
|
|
27
31
|
};
|
|
@@ -58,5 +62,6 @@ const mobileSurface = {
|
|
|
58
62
|
}
|
|
59
63
|
};
|
|
60
64
|
export {
|
|
65
|
+
mobileRuntimeNextAction,
|
|
61
66
|
mobileSurface
|
|
62
67
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { color } from "./cli-color.js";
|
|
4
5
|
import {
|
|
@@ -148,7 +149,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
148
149
|
required: false,
|
|
149
150
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
150
151
|
},
|
|
151
|
-
...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : []
|
|
152
|
+
...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : [],
|
|
153
|
+
...adapter === "extension" ? [extensionBrowserDoctorCheck(target)] : []
|
|
152
154
|
];
|
|
153
155
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
154
156
|
return {
|
|
@@ -169,6 +171,32 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
169
171
|
manifestValidation: manifestValidation.summary
|
|
170
172
|
};
|
|
171
173
|
}
|
|
174
|
+
function extensionBrowserDoctorCheck(target) {
|
|
175
|
+
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
176
|
+
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
177
|
+
try {
|
|
178
|
+
const playwright = requireFromTarget(packageName);
|
|
179
|
+
const executable = playwright.chromium?.executablePath?.();
|
|
180
|
+
if (executable && fs.existsSync(executable)) {
|
|
181
|
+
return {
|
|
182
|
+
id: "playwright-chromium",
|
|
183
|
+
status: "pass",
|
|
184
|
+
required: true,
|
|
185
|
+
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
186
|
+
detail: executable
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id: "playwright-chromium",
|
|
194
|
+
status: "fail",
|
|
195
|
+
required: true,
|
|
196
|
+
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
197
|
+
userAction: "yarn playwright install chromium"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
172
200
|
function mobileToolDoctorChecks(platform) {
|
|
173
201
|
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
174
202
|
{ tool: "adb", platform: "Android" },
|
|
@@ -250,6 +278,7 @@ function readJsonObject(file) {
|
|
|
250
278
|
export {
|
|
251
279
|
compatibilityMode,
|
|
252
280
|
createDoctorReport,
|
|
281
|
+
extensionBrowserDoctorCheck,
|
|
253
282
|
fixtureFileSummary,
|
|
254
283
|
fixtureSummary,
|
|
255
284
|
mobileToolDoctorChecks,
|
package/package.json
CHANGED
|
@@ -146,6 +146,7 @@ const REVEAL = String.raw`
|
|
|
146
146
|
const d = document.getElementById(l.getAttribute('aria-controls'));
|
|
147
147
|
if (d) d.hidden = false;
|
|
148
148
|
});
|
|
149
|
+
document.querySelectorAll('[role="tabpanel"][hidden]').forEach((p) => { p.hidden = false; });
|
|
149
150
|
return true;
|
|
150
151
|
})()
|
|
151
152
|
`;
|
|
@@ -331,6 +332,9 @@ async function auditPage(browserEndpoint, url) {
|
|
|
331
332
|
async function checkBehavior(browserEndpoint, port, pages) {
|
|
332
333
|
const base = `http://127.0.0.1:${port}/`;
|
|
333
334
|
await withPage(browserEndpoint, `${base}index.html`, async (page) => {
|
|
335
|
+
// Step 1 is the hero call to action; it was a collapsible before, so accept
|
|
336
|
+
// either shape. Step 2 is a tabbed set of prompts, one per kind of work —
|
|
337
|
+
// every one of them has to survive a copy, including the hidden panels.
|
|
334
338
|
const copied = await page.evaluate(String.raw`
|
|
335
339
|
(async () => {
|
|
336
340
|
Object.defineProperty(navigator, 'clipboard', {
|
|
@@ -342,22 +346,45 @@ async function checkBehavior(browserEndpoint, port, pages) {
|
|
|
342
346
|
},
|
|
343
347
|
},
|
|
344
348
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
async function grab(button) {
|
|
350
|
+
if (!button) throw new Error('copy button was not initialized');
|
|
351
|
+
window.__siteCopied = null;
|
|
352
|
+
button.click();
|
|
353
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
354
|
+
return window.__siteCopied;
|
|
355
|
+
}
|
|
356
|
+
const out = { setup: await grab(document.querySelector('.cmd-hero .copy, .prompt .copy')), work: [] };
|
|
357
|
+
for (const tab of document.querySelectorAll('[role="tab"]')) {
|
|
358
|
+
tab.click();
|
|
359
|
+
const panel = document.getElementById(tab.getAttribute('aria-controls'));
|
|
360
|
+
if (!panel || panel.hidden) throw new Error('tab ' + tab.id + ' did not reveal its panel');
|
|
361
|
+
// Panels ship visible so a dead module degrades to readable prompts
|
|
362
|
+
// rather than dead controls — which also means "revealed" alone
|
|
363
|
+
// proves nothing. Selecting one has to hide the others.
|
|
364
|
+
const shown = [...document.querySelectorAll('[role="tabpanel"]')].filter((p) => !p.hidden);
|
|
365
|
+
if (shown.length !== 1) {
|
|
366
|
+
throw new Error('tab ' + tab.id + ' left ' + shown.length + ' panels visible');
|
|
367
|
+
}
|
|
368
|
+
out.work.push(await grab(panel.querySelector('.cmd .copy')));
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
352
371
|
})()
|
|
353
372
|
`);
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
const expected = [
|
|
357
|
-
|
|
373
|
+
// Setting up installs the harness and the skills; the work prompts invoke
|
|
374
|
+
// them by their exact installed names.
|
|
375
|
+
const expected = [
|
|
376
|
+
['npm i -g @deeeed/metamask-harness@latest', copied.setup],
|
|
377
|
+
['yarn skills', copied.setup],
|
|
378
|
+
['/mms-recipe-pr-qa-review', copied.work.join('\n')],
|
|
379
|
+
['/mms-recipe-cook', copied.work.join('\n')],
|
|
380
|
+
];
|
|
381
|
+
const missing = expected.filter(([line, text]) => !text?.includes(line)).map(([line]) => line);
|
|
358
382
|
if (missing.length) {
|
|
359
383
|
throw new Error(`copy prompt omitted ${missing.map((m) => `"${m}"`).join(', ')}`);
|
|
360
384
|
}
|
|
385
|
+
if (copied.work.length < 3) {
|
|
386
|
+
throw new Error(`expected three work prompts, copied ${copied.work.length}`);
|
|
387
|
+
}
|
|
361
388
|
|
|
362
389
|
// The landing page is the lobby: one prompt, one way onward, nothing else.
|
|
363
390
|
const lobby = await page.evaluate(String.raw`
|
package/site/architecture.html
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Architecture — how the pieces fit</title>
|
|
7
7
|
<meta name="description" content="Skills, mm-harness, team recipe libraries, product repos: what lives where, the three workflows, and the mistakes newcomers make.">
|
|
8
|
-
<link rel="
|
|
8
|
+
<link rel="icon" href="assets/metamask-fox.svg" type="image/svg+xml">
|
|
9
|
+
<link rel="stylesheet" href="assets/style.css?v=4">
|
|
9
10
|
</head>
|
|
10
11
|
<body data-progress-page="architecture">
|
|
11
12
|
<a class="skip" href="#stack">Skip to the stack map</a>
|
|
@@ -13,18 +14,16 @@
|
|
|
13
14
|
<header class="topbar">
|
|
14
15
|
<div class="wrap topbar-inner">
|
|
15
16
|
<a class="brand" href="index.html">
|
|
16
|
-
<
|
|
17
|
+
<img class="brand-mark" src="assets/metamask-fox.svg" alt="" width="22" height="22">
|
|
17
18
|
<span class="brand-name">recipes</span>
|
|
18
19
|
</a>
|
|
19
20
|
<nav class="nav" aria-label="Main">
|
|
20
21
|
<a class="nav-cta" href="index.html#prompt">Quick start</a>
|
|
21
22
|
<a href="how-it-works.html">How it works</a>
|
|
22
|
-
<a href="
|
|
23
|
-
<a href="perps.html">Perps</a>
|
|
23
|
+
<a href="perps.html">Team / Perps</a>
|
|
24
24
|
<a href="cheatsheet.html">Cheatsheet</a>
|
|
25
25
|
<a href="architecture.html" aria-current="page">Architecture</a>
|
|
26
26
|
<a href="tutorials/index.html">Tutorials</a>
|
|
27
|
-
<a href="reviewers.html">For Reviewers</a>
|
|
28
27
|
</nav>
|
|
29
28
|
</div>
|
|
30
29
|
</header>
|
|
@@ -471,8 +470,16 @@
|
|
|
471
470
|
</ol>
|
|
472
471
|
|
|
473
472
|
<hr class="sep">
|
|
473
|
+
<h2>Which repo is which</h2>
|
|
474
|
+
<p>
|
|
475
|
+
The layers above are the design. The <a href="ecosystem.html">Ecosystem map</a> is the concrete
|
|
476
|
+
version: the six repositories and packages behind a run, each with its URL, and which way the
|
|
477
|
+
dependencies point.
|
|
478
|
+
</p>
|
|
479
|
+
|
|
474
480
|
<div class="btn-row">
|
|
475
|
-
<a class="btn btn-primary" href="
|
|
481
|
+
<a class="btn btn-primary" href="ecosystem.html">See the repo map →</a>
|
|
482
|
+
<a class="btn btn-ghost" href="how-it-works.html#steps">Do the walkthrough</a>
|
|
476
483
|
<a class="btn btn-ghost" href="reviewers.html">Read an evidence bundle</a>
|
|
477
484
|
</div>
|
|
478
485
|
</section>
|
|
@@ -485,6 +492,6 @@
|
|
|
485
492
|
</div>
|
|
486
493
|
</footer>
|
|
487
494
|
|
|
488
|
-
<script type="module" src="assets/progress.mjs"></script>
|
|
495
|
+
<script type="module" src="assets/progress.mjs?v=4"></script>
|
|
489
496
|
</body>
|
|
490
497
|
</html>
|