@deeeed/metamask-harness 0.46.0 → 0.47.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/adapters/manifest.json +12 -4
- package/adapters/mobile/wait-for-bridge.cjs +550 -0
- package/adapters/mobile/wait-for-bridge.sh +11 -148
- package/bin/mm-harness +7 -2
- package/dist/adapters/mobile/prepare.js +26 -18
- package/dist/adapters/mobile/runtime-decision.js +1 -0
- package/dist/cli-commands.js +2 -0
- package/dist/command-contract.js +7 -0
- package/dist/commands/checklist.js +1 -0
- package/dist/commands/help.js +76 -0
- package/dist/commands/parse-args.js +2 -1
- package/dist/commands/recipe-quality.js +1 -1
- package/dist/commands/tutorial.js +46 -0
- package/dist/mm-harness-cli.js +45 -6
- package/docs/CONTRIBUTING.md +2 -0
- package/docs/QA.md +2 -2
- package/library/actions/mobile/perps/perps.mjs +1 -1
- package/package.json +2 -2
- package/scripts/site-contrast.mjs +6 -2
- package/site/assets/help-recipes.json +113 -0
- package/site/architecture.html +0 -497
- package/site/assets/metamask-fox.svg +0 -24
- package/site/assets/progress.mjs +0 -323
- package/site/assets/style.css +0 -1066
- package/site/cheatsheet.html +0 -307
- package/site/ecosystem.html +0 -162
- package/site/how-it-works.html +0 -692
- package/site/index.html +0 -184
- package/site/perps-advanced-orders-qa.html +0 -96
- package/site/perps.html +0 -265
- package/site/recipes.html +0 -423
- package/site/reviewers.html +0 -375
- package/site/tutorials/index.html +0 -181
- package/site/tutorials/v1.html +0 -212
- package/site/tutorials/v2.html +0 -207
- package/site/tutorials/v3.html +0 -258
- package/site/tutorials/v4.html +0 -196
- package/site/tutorials/v5.html +0 -164
- package/site/tutorials/v6.html +0 -166
- package/site/tutorials/v7.html +0 -185
|
@@ -1,52 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# Calls cdp-bridge.cjs status in a poll loop. Emits structured diagnostics on
|
|
5
|
-
# each probe failure: Metro reachability, React Native target visibility, bundle
|
|
6
|
-
# progress. Fails with non-zero if the bridge is not ready within the poll limit.
|
|
7
|
-
#
|
|
8
|
-
# Inputs:
|
|
9
|
-
# --target <metamask-mobile dir> (default $PWD)
|
|
10
|
-
# --port <number> (default WATCHER_PORT env, else METRO_PORT, else 8081)
|
|
11
|
-
# --max-polls <n> (default MOBILE_BRIDGE_READY_POLLS env, else 90; 2s each = 180s)
|
|
12
|
-
# --platform ios|android bind the required platform: the answering target
|
|
13
|
-
# must belong to it, regardless of ambient device env
|
|
14
|
-
# Outputs:
|
|
15
|
-
# Progress on stderr; exit 0 bridge ready; 1 timeout.
|
|
16
|
-
#
|
|
17
|
-
# Single device op: wait for bridge readiness. Does not start Metro or open the dev client.
|
|
2
|
+
# Resolve the checkout runtime directory, then delegate the bounded bridge wait
|
|
3
|
+
# to Node so one monotonic deadline covers HTTP, CDP, and retry delays.
|
|
18
4
|
set -uo pipefail
|
|
19
5
|
|
|
20
|
-
TARGET="$PWD"
|
|
21
|
-
PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
|
|
22
|
-
MAX_POLLS="${MOBILE_BRIDGE_READY_POLLS:-90}"
|
|
23
|
-
REQUIRE_PLATFORM=""
|
|
24
|
-
|
|
25
|
-
while [ "$#" -gt 0 ]; do
|
|
26
|
-
case "$1" in
|
|
27
|
-
--target) [ "$#" -ge 2 ] || { echo "Missing value for --target" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
|
|
28
|
-
--port) [ "$#" -ge 2 ] || { echo "Missing value for --port" >&2; exit 2; }; PORT="$2"; shift 2 ;;
|
|
29
|
-
--max-polls) [ "$#" -ge 2 ] || { echo "Missing value for --max-polls" >&2; exit 2; }; MAX_POLLS="$2"; shift 2 ;;
|
|
30
|
-
--platform) [ "$#" -ge 2 ] || { echo "Missing value for --platform" >&2; exit 2; }; REQUIRE_PLATFORM="$2"; shift 2 ;;
|
|
31
|
-
-h|--help)
|
|
32
|
-
printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>] [--platform ios|android]\n'
|
|
33
|
-
exit 0
|
|
34
|
-
;;
|
|
35
|
-
*) printf 'wait-for-bridge: unknown arg: %s\n' "$1" >&2; exit 2 ;;
|
|
36
|
-
esac
|
|
37
|
-
done
|
|
38
|
-
|
|
39
|
-
case "$MAX_POLLS" in
|
|
40
|
-
''|*[!0-9]*) echo "wait-for-bridge: --max-polls must be an integer of at least 2." >&2; exit 2 ;;
|
|
41
|
-
esac
|
|
42
|
-
[ "$MAX_POLLS" -ge 2 ] || { echo "wait-for-bridge: --max-polls must be at least 2 for stable readiness." >&2; exit 2; }
|
|
43
|
-
|
|
44
|
-
TARGET="$(cd "$TARGET" && pwd -P)"
|
|
45
|
-
|
|
46
|
-
# The matcher reads the required platform from the environment so the value survives
|
|
47
|
-
# into the node subprocess that runs the shared matcher against the status output.
|
|
48
|
-
export WAIT_FOR_BRIDGE_PLATFORM="$REQUIRE_PLATFORM"
|
|
49
|
-
|
|
50
6
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
51
7
|
# shellcheck disable=SC1091
|
|
52
8
|
. "$SCRIPT_DIR/../shared/harness-path.sh"
|
|
@@ -54,109 +10,16 @@ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
|
|
|
54
10
|
echo "wait-for-bridge: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
|
|
55
11
|
exit 1
|
|
56
12
|
fi
|
|
57
|
-
LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
|
|
58
|
-
mkdir -p "$LOG_DIR"
|
|
59
|
-
|
|
60
|
-
BRIDGE_CJS="$(dirname "$0")/bridge-runtime/cdp-bridge.cjs"
|
|
61
|
-
MATCH_LIB="$(dirname "$0")/bridge-runtime/lib/match-bridge-target.cjs"
|
|
62
|
-
STATUS_LOG="$LOG_DIR/bridge-status.log"
|
|
63
|
-
METRO_LOG="$LOG_DIR/metro.log"
|
|
64
13
|
|
|
65
|
-
|
|
66
|
-
|
|
14
|
+
WAIT_CJS="$SCRIPT_DIR/wait-for-bridge.cjs"
|
|
15
|
+
if [ ! -f "$WAIT_CJS" ]; then
|
|
16
|
+
printf 'wait-for-bridge: helper not found at %s\n' "$WAIT_CJS" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
20
|
+
echo "wait-for-bridge: node is required." >&2
|
|
67
21
|
exit 1
|
|
68
22
|
fi
|
|
69
23
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
bundle_progress() {
|
|
75
|
-
local log="$1"
|
|
76
|
-
[ -f "$log" ] || return 0
|
|
77
|
-
local p
|
|
78
|
-
p="$(grep -E '^[[:space:]]*(iOS|Android).*index\.(js|tsx?).*%|Bundl(ed|ing)|Finished' "$log" 2>/dev/null | tail -1 | tr -d '\r' || true)"
|
|
79
|
-
[ -n "$p" ] && printf ' (%s)' "$p"
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
bridge_wait_reason() {
|
|
83
|
-
if ! metro_ready; then
|
|
84
|
-
printf 'Metro not reachable on port %s' "$PORT"
|
|
85
|
-
return
|
|
86
|
-
fi
|
|
87
|
-
local targets
|
|
88
|
-
targets="$(curl -sf --max-time 2 "http://localhost:${PORT}/json/list" 2>/dev/null || true)"
|
|
89
|
-
if [ "$targets" = "[]" ] || [ -z "$targets" ]; then
|
|
90
|
-
printf 'Metro running; no React Native debug target yet'
|
|
91
|
-
bundle_progress "$METRO_LOG"
|
|
92
|
-
return
|
|
93
|
-
fi
|
|
94
|
-
if [ -s "$STATUS_LOG" ] && grep -qxF '[]' "$STATUS_LOG"; then
|
|
95
|
-
printf 'React Native target visible; in-app bridge not ready yet'
|
|
96
|
-
bundle_progress "$METRO_LOG"
|
|
97
|
-
return
|
|
98
|
-
fi
|
|
99
|
-
printf 'bridge probe failed'
|
|
100
|
-
bundle_progress "$METRO_LOG"
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# Matcher is the shared boundary-safe module (match-bridge-target.cjs), so the
|
|
104
|
-
# launch ready-path confirm and this poll enforce identical platform/device rules.
|
|
105
|
-
bridge_has_route() {
|
|
106
|
-
(cd "$TARGET" && APP_ROOT="$TARGET" node "$BRIDGE_CJS" status > "$STATUS_LOG" 2>&1) \
|
|
107
|
-
&& node -e '
|
|
108
|
-
const { hasMatchingRoute } = require(process.argv[1]);
|
|
109
|
-
const fs = require("fs");
|
|
110
|
-
try {
|
|
111
|
-
const value = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
112
|
-
process.exit(hasMatchingRoute(value) ? 0 : 1);
|
|
113
|
-
} catch { process.exit(1); }
|
|
114
|
-
' "$MATCH_LIB" "$STATUS_LOG"
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
# The device/platform this run is pinned to — the bridge target must belong to it.
|
|
118
|
-
requested_target() {
|
|
119
|
-
node -e 'process.stdout.write(require(process.argv[1]).describeRequested())' "$MATCH_LIB" 2>/dev/null \
|
|
120
|
-
|| printf 'any platform'
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
# Summarise the targets that DID answer (platform / deviceName), so a timeout teaches
|
|
124
|
-
# what responded vs what was requested — e.g. an iOS target answering an android launch.
|
|
125
|
-
answered_targets() {
|
|
126
|
-
[ -s "$STATUS_LOG" ] || { printf 'none'; return; }
|
|
127
|
-
node -e '
|
|
128
|
-
const { describeTargets } = require(process.argv[1]);
|
|
129
|
-
const fs = require("fs");
|
|
130
|
-
try {
|
|
131
|
-
process.stdout.write(describeTargets(JSON.parse(fs.readFileSync(process.argv[2], "utf8"))));
|
|
132
|
-
} catch { process.stdout.write("none"); }
|
|
133
|
-
' "$MATCH_LIB" "$STATUS_LOG" 2>/dev/null || printf 'none'
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
READY_STREAK=0
|
|
137
|
-
for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
|
|
138
|
-
if bridge_has_route; then
|
|
139
|
-
READY_STREAK=$((READY_STREAK + 1))
|
|
140
|
-
# The bridge can appear briefly while React Native is still completing its
|
|
141
|
-
# startup navigation/reload. Require two consecutive healthy snapshots so a
|
|
142
|
-
# following command never inherits that transient gap.
|
|
143
|
-
if [ "$READY_STREAK" -ge 2 ]; then
|
|
144
|
-
printf 'Mobile bridge ready\n' >&2
|
|
145
|
-
exit 0
|
|
146
|
-
fi
|
|
147
|
-
else
|
|
148
|
-
READY_STREAK=0
|
|
149
|
-
fi
|
|
150
|
-
if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
|
|
151
|
-
printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
|
|
152
|
-
fi
|
|
153
|
-
[ "$ATTEMPT" -ge "$MAX_POLLS" ] || sleep 2
|
|
154
|
-
done
|
|
155
|
-
|
|
156
|
-
cat "$STATUS_LOG" >&2 || true
|
|
157
|
-
printf 'wait-for-bridge: no bridge target matched the requested %s on port %s (%s polls × 2s = %ss)\n' \
|
|
158
|
-
"$(requested_target)" "$PORT" "$MAX_POLLS" "$((MAX_POLLS * 2))" >&2
|
|
159
|
-
printf ' requested: %s\n' "$(requested_target)" >&2
|
|
160
|
-
printf ' answered: %s\n' "$(answered_targets)" >&2
|
|
161
|
-
printf ' Next: check %s — or run mm-harness status --json\n' "$STATUS_LOG" >&2
|
|
162
|
-
exit 1
|
|
24
|
+
MM_HARNESS_WAIT_RUNTIME_DIR="$(recipe_runtime_dir)" \
|
|
25
|
+
exec node "$WAIT_CJS" "$@"
|
package/bin/mm-harness
CHANGED
|
@@ -144,8 +144,13 @@ if [ -f "$ENTRY_DIST" ] && [ -f "$ENTRY_TS" ]; then
|
|
|
144
144
|
done < <(find "$RUNNER_DIR/src" -name '*.ts' -newer "$ENTRY_DIST" -print 2>/dev/null)
|
|
145
145
|
if [ -n "$newer_source" ]; then
|
|
146
146
|
export MM_HARNESS_RUN_MODE="src"
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
case "${1:-}" in
|
|
148
|
+
''|-h|--help|-v|--version|help|tutorial) ;;
|
|
149
|
+
*)
|
|
150
|
+
echo "mm-harness: dist/ is older than src/ — running the current source checkout." >&2
|
|
151
|
+
echo " Next: npm run build to refresh dist/" >&2
|
|
152
|
+
;;
|
|
153
|
+
esac
|
|
149
154
|
else
|
|
150
155
|
USE_DIST=1
|
|
151
156
|
export MM_HARNESS_RUN_MODE="dist"
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
mobileMetroEnvCheck,
|
|
13
13
|
recordMobileMetroEnvBaseline
|
|
14
14
|
} from "./metro-env.js";
|
|
15
|
-
const
|
|
15
|
+
const READY_BRIDGE_CONFIRM_TIMEOUT_MS = 6e3;
|
|
16
16
|
const POD_PROBE_ENV = {
|
|
17
17
|
FORCE_COLOR: "0",
|
|
18
18
|
NO_COLOR: "1",
|
|
@@ -69,7 +69,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
69
69
|
preflightMode,
|
|
70
70
|
opts.watcherPort
|
|
71
71
|
);
|
|
72
|
-
return launch
|
|
72
|
+
return launch;
|
|
73
73
|
}
|
|
74
74
|
if (report.decision === "ready" && clearMetro) {
|
|
75
75
|
const launch = await dispatchActionSequence(
|
|
@@ -83,7 +83,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
83
83
|
preflightMode,
|
|
84
84
|
opts.watcherPort
|
|
85
85
|
);
|
|
86
|
-
return launch
|
|
86
|
+
return launch;
|
|
87
87
|
}
|
|
88
88
|
if (report.decision === "ready") {
|
|
89
89
|
if (restartApp || iosAccessibilityChanged) {
|
|
@@ -95,7 +95,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
95
95
|
preflightMode,
|
|
96
96
|
opts.watcherPort
|
|
97
97
|
);
|
|
98
|
-
return launch2
|
|
98
|
+
return launch2;
|
|
99
99
|
}
|
|
100
100
|
const surface = await ensureMobileHumanSurface(
|
|
101
101
|
target,
|
|
@@ -104,8 +104,19 @@ async function prepareMobile(target, opts = {}) {
|
|
|
104
104
|
opts.watcherPort
|
|
105
105
|
);
|
|
106
106
|
if (surface.status !== 0) return surface;
|
|
107
|
+
const forwarder = await startMobileConsoleForwarder(
|
|
108
|
+
target,
|
|
109
|
+
platform,
|
|
110
|
+
json,
|
|
111
|
+
opts.watcherPort
|
|
112
|
+
);
|
|
113
|
+
if (forwarder.status !== 0) return forwarder;
|
|
107
114
|
const confirm = await dispatchAction(
|
|
108
|
-
{
|
|
115
|
+
{
|
|
116
|
+
id: "wait-for-bridge",
|
|
117
|
+
cwd: target,
|
|
118
|
+
argv: ["--timeout-ms", String(READY_BRIDGE_CONFIRM_TIMEOUT_MS)]
|
|
119
|
+
},
|
|
109
120
|
target,
|
|
110
121
|
platform,
|
|
111
122
|
json,
|
|
@@ -113,12 +124,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
113
124
|
opts.watcherPort
|
|
114
125
|
);
|
|
115
126
|
if (confirm.status === 0) {
|
|
116
|
-
return
|
|
117
|
-
target,
|
|
118
|
-
platform,
|
|
119
|
-
json,
|
|
120
|
-
opts.watcherPort
|
|
121
|
-
);
|
|
127
|
+
return confirm;
|
|
122
128
|
}
|
|
123
129
|
if (!json) {
|
|
124
130
|
process.stderr.write(
|
|
@@ -134,7 +140,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
134
140
|
preflightMode,
|
|
135
141
|
opts.watcherPort
|
|
136
142
|
);
|
|
137
|
-
return launch
|
|
143
|
+
return launch;
|
|
138
144
|
}
|
|
139
145
|
if (report.decision === "unknown") {
|
|
140
146
|
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
@@ -181,6 +187,13 @@ async function prepareMobile(target, opts = {}) {
|
|
|
181
187
|
opts.watcherPort
|
|
182
188
|
);
|
|
183
189
|
if (surface.status !== 0) return surface;
|
|
190
|
+
const forwarder = await startMobileConsoleForwarder(
|
|
191
|
+
target,
|
|
192
|
+
platform,
|
|
193
|
+
json,
|
|
194
|
+
opts.watcherPort
|
|
195
|
+
);
|
|
196
|
+
if (forwarder.status !== 0) return forwarder;
|
|
184
197
|
const bridge = await dispatchAction(
|
|
185
198
|
{ id: "wait-for-bridge", cwd: target },
|
|
186
199
|
target,
|
|
@@ -216,12 +229,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
231
|
}
|
|
219
|
-
return
|
|
220
|
-
target,
|
|
221
|
-
platform,
|
|
222
|
-
json,
|
|
223
|
-
opts.watcherPort
|
|
224
|
-
);
|
|
232
|
+
return actionResult;
|
|
225
233
|
}
|
|
226
234
|
function startMobileConsoleForwarder(target, platform, json, watcherPort) {
|
|
227
235
|
return dispatchAction(
|
|
@@ -96,6 +96,7 @@ const launchActions = (target, clearMetro = false) => {
|
|
|
96
96
|
});
|
|
97
97
|
actions.push({ id: "prewarm-bundle", cwd: target });
|
|
98
98
|
actions.push({ id: "launch-mobile-runtime" });
|
|
99
|
+
actions.push({ id: "start-console-forwarder", cwd: target });
|
|
99
100
|
actions.push({ id: "wait-for-bridge", cwd: target });
|
|
100
101
|
return actions;
|
|
101
102
|
};
|
package/dist/cli-commands.js
CHANGED
|
@@ -8,6 +8,8 @@ const SPEC = {
|
|
|
8
8
|
{ name: "help", aliases: ["-h", "--help"], desc: "Show usage" }
|
|
9
9
|
],
|
|
10
10
|
shared: [
|
|
11
|
+
{ name: "help", desc: "Load version-matched recipe guidance", flags: ["--json", "--adapter", "--target"] },
|
|
12
|
+
{ name: "tutorial", desc: "Open the visual recipe tutorial", flags: ["--json", "--no-open"] },
|
|
11
13
|
{ name: "setup-base", desc: "Bootstrap numbered product checkouts", flags: ["--dir", "--counts", "--only", "--dry-run", "--force", "--json", "--show-config", "--reset-config", "--skip-harness-update"] },
|
|
12
14
|
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json", "--fast"] },
|
|
13
15
|
{ name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
|
package/dist/command-contract.js
CHANGED
|
@@ -4,6 +4,7 @@ const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { cho
|
|
|
4
4
|
const options = (...groups) => Object.assign({}, ...groups);
|
|
5
5
|
const HELP = { "--help": bool(), "-h": bool() };
|
|
6
6
|
const JSON = { "--json": bool() };
|
|
7
|
+
const NO_OPEN = { "--no-open": bool() };
|
|
7
8
|
const JSON_STREAM = { "--json-stream": bool() };
|
|
8
9
|
const TARGET = { "--target": value() };
|
|
9
10
|
const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
|
|
@@ -56,6 +57,12 @@ const REMOVED_OPTION_REPLACEMENTS = {
|
|
|
56
57
|
"--record": "--record-video"
|
|
57
58
|
};
|
|
58
59
|
const PUBLIC_COMMAND_CONTRACTS = {
|
|
60
|
+
help: {
|
|
61
|
+
options: options(HELP, JSON, TARGET, ADAPTER)
|
|
62
|
+
},
|
|
63
|
+
tutorial: {
|
|
64
|
+
options: options(HELP, JSON, NO_OPEN)
|
|
65
|
+
},
|
|
59
66
|
"setup-base": {
|
|
60
67
|
options: options(HELP, JSON, {
|
|
61
68
|
"--dir": value(),
|
|
@@ -194,6 +194,7 @@ function teachRecipeCompletion(taskDir, step) {
|
|
|
194
194
|
}
|
|
195
195
|
if (missingQuality) {
|
|
196
196
|
const compactPath = path.join(artifactsDir, "recipe-quality-input.json");
|
|
197
|
+
console.error(" First run: mm-harness help");
|
|
197
198
|
console.error(` - ${qualityPath}: write ${compactPath} as:`);
|
|
198
199
|
console.error(' {"verdict":"pass|warn|fail","reasons":["evidence-backed reason"]}');
|
|
199
200
|
console.error(` then run: mm-harness recipe-quality build --input ${compactPath} --output ${qualityPath}`);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { detectAdapter } from "../harness.js";
|
|
4
|
+
import { optionString, parseArgs, targetPath } from "./parse-args.js";
|
|
5
|
+
const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core>";
|
|
6
|
+
function loadRecipeHelp(packageRoot) {
|
|
7
|
+
const file = path.join(packageRoot, "site", "assets", "help-recipes.json");
|
|
8
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
9
|
+
if (value.schemaVersion !== 1 || value.topic !== "recipes" || typeof value.title !== "string" || typeof value.summary !== "string" || typeof value.capabilities !== "string" || typeof value.scopeNotice !== "string" || typeof value.recipeProtocolVersion !== "string" || typeof value.safety !== "string" || !Array.isArray(value.sections) || value.sections.length !== 8 || value.sections.some(
|
|
10
|
+
(section) => !section || typeof section.id !== "string" || typeof section.title !== "string" || typeof section.instruction !== "string" || !Array.isArray(section.details) || section.details.some((detail) => typeof detail !== "string") || !Array.isArray(section.commands) || section.commands.some((command) => typeof command !== "string")
|
|
11
|
+
)) {
|
|
12
|
+
throw new Error(`Invalid recipe help data: ${file}`);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function recipeHelpPayload(topic, harnessVersion, adapter, target) {
|
|
17
|
+
const commands = Array.from(new Set(topic.sections.flatMap((section) => section.commands)));
|
|
18
|
+
const discoveryCommands = topic.sections.find((section) => section.id === "discover")?.commands ?? [];
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: topic.schemaVersion,
|
|
21
|
+
command: "help",
|
|
22
|
+
harnessVersion,
|
|
23
|
+
recipeProtocolVersion: topic.recipeProtocolVersion,
|
|
24
|
+
adapter,
|
|
25
|
+
...adapter === "unscoped" ? { next: UNSCOPED_NEXT } : {},
|
|
26
|
+
target,
|
|
27
|
+
topic: topic.topic,
|
|
28
|
+
title: topic.title,
|
|
29
|
+
summary: topic.summary,
|
|
30
|
+
capabilities: topic.capabilities,
|
|
31
|
+
scopeNotice: topic.scopeNotice,
|
|
32
|
+
sections: topic.sections,
|
|
33
|
+
commands,
|
|
34
|
+
discoveryCommands,
|
|
35
|
+
safety: topic.safety
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function renderRecipeHelp(topic, harnessVersion, adapter) {
|
|
39
|
+
const lines = [
|
|
40
|
+
`mm-harness ${harnessVersion} recipe guide`,
|
|
41
|
+
`Context: Recipe Protocol v${topic.recipeProtocolVersion}, adapter ${adapter}`,
|
|
42
|
+
topic.summary,
|
|
43
|
+
topic.capabilities,
|
|
44
|
+
topic.scopeNotice,
|
|
45
|
+
"Visual tutorial: mm-harness tutorial"
|
|
46
|
+
];
|
|
47
|
+
if (adapter === "unscoped") lines.push(`No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`);
|
|
48
|
+
for (const [index, section] of topic.sections.entries()) {
|
|
49
|
+
lines.push("", `${index + 1}. ${section.title}`, ` ${section.instruction}`);
|
|
50
|
+
for (const detail of section.details) lines.push(` - ${detail}`);
|
|
51
|
+
for (const command of section.commands) lines.push(` $ ${command}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push("", `Safety: ${topic.safety}`);
|
|
54
|
+
return `${lines.join("\n")}
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
function handleHelp(argv, context) {
|
|
58
|
+
const parsed = parseArgs(argv, "help");
|
|
59
|
+
const target = targetPath(parsed.options);
|
|
60
|
+
const adapter = optionString(parsed.options, "adapter") ?? detectAdapter(target) ?? "unscoped";
|
|
61
|
+
const topic = loadRecipeHelp(context.packageRoot);
|
|
62
|
+
const payload = recipeHelpPayload(topic, context.harnessVersion, adapter, target);
|
|
63
|
+
if (parsed.options.json === true) {
|
|
64
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
65
|
+
`);
|
|
66
|
+
} else {
|
|
67
|
+
process.stdout.write(renderRecipeHelp(topic, context.harnessVersion, adapter));
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
handleHelp,
|
|
73
|
+
loadRecipeHelp,
|
|
74
|
+
recipeHelpPayload,
|
|
75
|
+
renderRecipeHelp
|
|
76
|
+
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { EXIT, usageOut } from "./shared.js";
|
|
7
7
|
import { optionFlag, optionString, parseArgs } from "./parse-args.js";
|
|
8
8
|
const BUILD_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]";
|
|
9
|
-
const SHORTHAND_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]\n Shorthand: mm-harness recipe-quality --input <compact.json> --output <path> [--json]\n Note:
|
|
9
|
+
const SHORTHAND_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]\n Shorthand: mm-harness recipe-quality --input <compact.json> --output <path> [--json]\n Note: apply the quality bar from mm-harness help, then build its compact verdict here.";
|
|
10
10
|
function errorMessage(error) {
|
|
11
11
|
return error instanceof Error ? error.message : String(error);
|
|
12
12
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { parseArgs } from "./parse-args.js";
|
|
3
|
+
const PROTECTED_TUTORIAL_URL = "https://glowing-chainsaw-gwzrn8k.pages.github.io/recipes.html#agent-method";
|
|
4
|
+
function recipeTutorialUrl(runMode = process.env.MM_HARNESS_RUN_MODE) {
|
|
5
|
+
if (runMode === "src") {
|
|
6
|
+
return "http://127.0.0.1:8765/site/recipes.html#agent-method";
|
|
7
|
+
}
|
|
8
|
+
return PROTECTED_TUTORIAL_URL;
|
|
9
|
+
}
|
|
10
|
+
function openUrl(url) {
|
|
11
|
+
const command = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
|
|
12
|
+
const result = spawnSync(command[0], command.slice(1), { stdio: "ignore" });
|
|
13
|
+
return result.status === 0;
|
|
14
|
+
}
|
|
15
|
+
function handleTutorial(argv, context) {
|
|
16
|
+
const parsed = parseArgs(argv, "tutorial");
|
|
17
|
+
const url = recipeTutorialUrl();
|
|
18
|
+
const noOpen = parsed.options.noOpen === true;
|
|
19
|
+
const opened = noOpen ? false : (context.open ?? openUrl)(url);
|
|
20
|
+
const payload = {
|
|
21
|
+
schemaVersion: 1,
|
|
22
|
+
command: "tutorial",
|
|
23
|
+
harnessVersion: context.harnessVersion,
|
|
24
|
+
url,
|
|
25
|
+
opened
|
|
26
|
+
};
|
|
27
|
+
if (parsed.options.json === true) {
|
|
28
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
29
|
+
`);
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
if (!noOpen && !opened) {
|
|
33
|
+
process.stderr.write(`Could not open the browser.
|
|
34
|
+
Next: open ${url}
|
|
35
|
+
`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
process.stdout.write(`${opened ? "Opened" : "Recipe tutorial"} for mm-harness ${context.harnessVersion}:
|
|
39
|
+
${url}
|
|
40
|
+
`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
handleTutorial,
|
|
45
|
+
recipeTutorialUrl
|
|
46
|
+
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -8,6 +8,8 @@ import { withCommandJournal } from "./command-journal.js";
|
|
|
8
8
|
import { JsonStreamWriter } from "./json-stream.js";
|
|
9
9
|
import { handleUpdate, maybeNudge } from "./commands/update.js";
|
|
10
10
|
import { handleCallHelp } from "./commands/call.js";
|
|
11
|
+
import { handleHelp } from "./commands/help.js";
|
|
12
|
+
import { handleTutorial } from "./commands/tutorial.js";
|
|
11
13
|
import { handleSetupBase } from "./commands/setup-base.js";
|
|
12
14
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
13
15
|
import { detectAdapter } from "./harness.js";
|
|
@@ -21,6 +23,37 @@ globalThis.__MM_HARNESS_WRAPPER__ = true;
|
|
|
21
23
|
const { main: recipeMain } = await import("./cli.js");
|
|
22
24
|
const rawArgv = process.argv.slice(2);
|
|
23
25
|
const REAL = [
|
|
26
|
+
{
|
|
27
|
+
name: "help",
|
|
28
|
+
summary: "Load the version-matched recipe guide for a person or agent.",
|
|
29
|
+
example: "mm-harness help",
|
|
30
|
+
helpText: `mm-harness help [flags]
|
|
31
|
+
|
|
32
|
+
Loads the complete recipe guide shipped with this harness.
|
|
33
|
+
|
|
34
|
+
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
35
|
+
--target <path> Checkout path (default: cwd)
|
|
36
|
+
--json Load the same guide as structured agent context
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
mm-harness help
|
|
40
|
+
mm-harness help --json`
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "tutorial",
|
|
44
|
+
summary: "Open the protected visual recipe tutorial.",
|
|
45
|
+
example: "mm-harness tutorial",
|
|
46
|
+
helpText: `mm-harness tutorial [flags]
|
|
47
|
+
|
|
48
|
+
Source checkouts open the local tutorial on port 8765. Installed releases
|
|
49
|
+
open the protected MetaMask GitHub Pages site.
|
|
50
|
+
|
|
51
|
+
--no-open Print the tutorial URL without opening it
|
|
52
|
+
--json Print version, URL, and open status as JSON
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
mm-harness tutorial`
|
|
56
|
+
},
|
|
24
57
|
{
|
|
25
58
|
name: "setup-base",
|
|
26
59
|
summary: "Bootstrap numbered MetaMask product checkouts and install their dependencies.",
|
|
@@ -334,9 +367,9 @@ Example:
|
|
|
334
367
|
written \u2014 an invalid input never reaches disk. Shorthand without "build" is
|
|
335
368
|
also accepted when --input/--output are present.
|
|
336
369
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
370
|
+
Apply the quality bar from mm-harness help to the real graph and
|
|
371
|
+
evidence, then use this command to build the canonical artifact from that
|
|
372
|
+
compact verdict. An optional independent reviewer may still critique it.
|
|
340
373
|
|
|
341
374
|
--input <compact.json> Compact verdict JSON: { "verdict": "pass|warn|fail", "reasons": [..],
|
|
342
375
|
"betterVersionGuidance"?: [..], "dimensions"?: {..}, "trainingFields"?: {..}, \u2026 }
|
|
@@ -669,7 +702,7 @@ const HELP_GROUPS = [
|
|
|
669
702
|
{
|
|
670
703
|
title: "DISCOVER",
|
|
671
704
|
blurb: "discover atomic actions and reusable recipes (--json is the agent-primary form)",
|
|
672
|
-
commands: ["actions", "call", "execution-template"]
|
|
705
|
+
commands: ["help", "tutorial", "actions", "call", "execution-template"]
|
|
673
706
|
},
|
|
674
707
|
{
|
|
675
708
|
title: "PROVE",
|
|
@@ -782,7 +815,7 @@ const pkgVersion = (() => {
|
|
|
782
815
|
}
|
|
783
816
|
})();
|
|
784
817
|
const program = new Command();
|
|
785
|
-
program.name("mm-harness").description("the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay").version(pkgVersion, "-v, --version", "Print the mm-harness version").helpOption("-h, --help", "Show grouped help").showHelpAfterError("(run `mm-harness --help` for the full surface)").configureHelp({ formatHelp: () => groupedHelp() });
|
|
818
|
+
program.name("mm-harness").description("the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay").version(pkgVersion, "-v, --version", "Print the mm-harness version").addHelpCommand(false).helpOption("-h, --help", "Show grouped help").showHelpAfterError("(run `mm-harness --help` for the full surface)").configureHelp({ formatHelp: () => groupedHelp() });
|
|
786
819
|
for (const command of REAL) {
|
|
787
820
|
const registered = program.command(command.name).description(command.summary).allowUnknownOption().helpOption("-h, --help", "Show command help");
|
|
788
821
|
if (command.aliases?.length) registered.aliases(command.aliases);
|
|
@@ -791,6 +824,12 @@ for (const command of REAL) {
|
|
|
791
824
|
if (command.name === "update") {
|
|
792
825
|
process.exit(await handleUpdate(rawArgv.slice(1)));
|
|
793
826
|
}
|
|
827
|
+
if (command.name === "help") {
|
|
828
|
+
process.exit(handleHelp(rawArgv.slice(1), { packageRoot, harnessVersion: pkgVersion }));
|
|
829
|
+
}
|
|
830
|
+
if (command.name === "tutorial") {
|
|
831
|
+
process.exit(handleTutorial(rawArgv.slice(1), { harnessVersion: pkgVersion }));
|
|
832
|
+
}
|
|
794
833
|
process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
|
|
795
834
|
});
|
|
796
835
|
}
|
|
@@ -812,7 +851,7 @@ program.command("completions").description("Install/print bundled shell tab-comp
|
|
|
812
851
|
const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
|
|
813
852
|
process.exit(result.status ?? 1);
|
|
814
853
|
});
|
|
815
|
-
const NUDGE_SKIP = ["setup-base", "update", "completions", "completion-candidates"];
|
|
854
|
+
const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "completions", "completion-candidates"];
|
|
816
855
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
817
856
|
maybeNudge();
|
|
818
857
|
}
|
package/docs/CONTRIBUTING.md
CHANGED
package/docs/QA.md
CHANGED
|
@@ -16,7 +16,7 @@ mm-harness --version
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
- [ ] The executable and dependencies resolve inside the isolated prefix.
|
|
19
|
-
- [ ] No dependency is a symlink or resolves through a local Farmslot checkout.
|
|
19
|
+
- [ ] No dependency is a symlink or resolves through a local [Farmslot](https://farmslot.io) checkout.
|
|
20
20
|
- [ ] Record the tarball SHA-256 and product SHAs.
|
|
21
21
|
- [ ] Product trees start and finish without tracked changes.
|
|
22
22
|
|
|
@@ -179,7 +179,7 @@ Before release:
|
|
|
179
179
|
- [ ] Known limits are explicit: Extension requires product Infura setup;
|
|
180
180
|
Mobile requires its normal dev-client/device setup; video requires optional
|
|
181
181
|
`capture-helper`; the harness never invents funded fixtures.
|
|
182
|
-
- [ ] Existing Farmslot slots remain compatible.
|
|
182
|
+
- [ ] Existing [Farmslot](https://farmslot.io) slots remain compatible.
|
|
183
183
|
|
|
184
184
|
When a slot manager is available, repeat `runner.smoke` in one existing managed
|
|
185
185
|
checkout per product and verify its profile/device, ports, processes, and fixture
|
|
@@ -1677,7 +1677,7 @@ async function assertReadyToTrade(input, config) {
|
|
|
1677
1677
|
if (config.readyToTrade === undefined || config.readyToTrade === false) return { skipped: true };
|
|
1678
1678
|
const ready = await evalAsync(
|
|
1679
1679
|
input,
|
|
1680
|
-
`(function(){ var c=Engine.context.PerpsController; var id=c.state.activeProvider; var p=c.providers && c.providers.get ? c.providers.get(id) : c.getActiveProvider && c.getActiveProvider(); if(!p || typeof p.isReadyToTrade !== 'function') return Promise.resolve(JSON.stringify({ready:false,activeProvider:id || null,error:'provider unavailable'})); return p.isReadyToTrade().then(function(r){ return JSON.stringify({ready:!!(r && r.ready), activeProvider:id || null, authenticatedAddress:(r&&r.authenticatedAddress)||null}); }); })()`,
|
|
1680
|
+
`(function(){ var c=Engine.context.PerpsController; var id=c.state.activeProvider; var p=c.providers && c.providers.get ? c.providers.get(id) : c.getActiveProvider && c.getActiveProvider(); if(!p || typeof p.isReadyToTrade !== 'function') return Promise.resolve(JSON.stringify({ready:false,activeProvider:id || null,error:'provider unavailable'})); return p.isReadyToTrade().then(function(r){ return JSON.stringify({ready:!!(r && r.ready), activeProvider:id || null, authenticatedAddress:(r&&r.authenticatedAddress)||null, error:(r&&r.error)||null, walletConnected:r&&typeof r.walletConnected==='boolean'?r.walletConnected:null, networkSupported:r&&typeof r.networkSupported==='boolean'?r.networkSupported:null}); }); })()`,
|
|
1681
1681
|
);
|
|
1682
1682
|
if (!ready.ready) throw new Error(`Perps provider is not ready to trade: ${JSON.stringify(ready)}`);
|
|
1683
1683
|
return ready;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"dist",
|
|
65
65
|
"adapters",
|
|
66
66
|
"library",
|
|
67
|
-
"site",
|
|
67
|
+
"site/assets/help-recipes.json",
|
|
68
68
|
"scripts/completions.sh",
|
|
69
69
|
"scripts/install-completions.sh",
|
|
70
70
|
"scripts/site-contrast.mjs",
|