@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat >&2 <<'EOF'
|
|
6
|
+
Usage:
|
|
7
|
+
mm-recipe home status screen (default)
|
|
8
|
+
mm-recipe status [--json] readiness summary + suggested next commands
|
|
9
|
+
mm-recipe ports [--json] slot ports, runtime paths, device hints
|
|
10
|
+
mm-recipe up [--json] [--dry-run] decide + run minimum work to reach ready
|
|
11
|
+
mm-recipe decision [--json] runtime-decision (deps + Metro bundle gate)
|
|
12
|
+
mm-recipe ios start/reuse Metro, launch iOS dev client
|
|
13
|
+
mm-recipe android start/reuse Metro, launch Android dev client
|
|
14
|
+
mm-recipe sync [--json] refresh mobile harness + canonicalize wallet fixture
|
|
15
|
+
mm-recipe completion [install] install shell tab completion (bash + zsh)
|
|
16
|
+
mm-recipe prepare install harness, start/reuse runtime, optionally setup wallet
|
|
17
|
+
--target <repo> --platform ios|android --preflight-mode fast|auto|default|rebuild-native|clean --port <port> --simulator <sim> --adb-serial <serial> --runtime-dir <dir> --wallet-setup [--wallet-fixture <json>]
|
|
18
|
+
mm-recipe setup:ios ios + setup wallet fixture
|
|
19
|
+
mm-recipe setup:android android + setup wallet fixture
|
|
20
|
+
mm-recipe refresh start/reuse Metro, relaunch detected dev client
|
|
21
|
+
mm-recipe reload alias for refresh
|
|
22
|
+
mm-recipe logs [--full] tail last Metro events (default) or full log
|
|
23
|
+
mm-recipe interactive interactive menu
|
|
24
|
+
mm-recipe runtime-status [--json] structured Metro/bundle/bridge status
|
|
25
|
+
mm-recipe route current route
|
|
26
|
+
mm-recipe navigate <Route> [json] navigate by React Navigation route
|
|
27
|
+
mm-recipe unlock [password] unlock with fixture password if omitted
|
|
28
|
+
mm-recipe setup-wallet [fixture] apply wallet fixture
|
|
29
|
+
mm-recipe accounts list accounts
|
|
30
|
+
mm-recipe select-account <addr> switch account
|
|
31
|
+
mm-recipe screenshot [path] simulator/device screenshot
|
|
32
|
+
mm-recipe debug [--json] open React Native DevTools / Hermes debugger
|
|
33
|
+
mm-recipe dev-menu [--json] open the React Native developer menu
|
|
34
|
+
mm-recipe stop stop this slot's Metro
|
|
35
|
+
mm-recipe actions [--json]
|
|
36
|
+
mm-recipe doctor [--json]
|
|
37
|
+
mm-recipe run <recipe.json> [args] run recipe with detected slot env
|
|
38
|
+
mm-recipe <recipe.json> [args] shortcut for run
|
|
39
|
+
|
|
40
|
+
Detects Mobile root, .js.env, WATCHER_PORT, IOS_SIMULATOR/ANDROID_DEVICE.
|
|
41
|
+
EOF
|
|
42
|
+
recipe_speed_ladder
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
46
|
+
runner="$script_dir/metamask-recipe"
|
|
47
|
+
# This script lives at <root>/orchestration/porcelain/; the runner root is two up.
|
|
48
|
+
runner_root="$(cd "$script_dir/../.." && pwd -P)"
|
|
49
|
+
# shellcheck disable=SC1091
|
|
50
|
+
. "$runner_root/orchestration/lib/harness-path.sh"
|
|
51
|
+
. "$runner_root/orchestration/lib/cli-ux.sh"
|
|
52
|
+
RECIPE_RUNNER_ROOT="$runner_root"
|
|
53
|
+
bridge="$runner_root/library/actions/mobile/bridge-runtime/cdp-bridge.cjs"
|
|
54
|
+
setup_wallet_script="$runner_root/library/actions/mobile/bridge-runtime/setup-wallet.sh"
|
|
55
|
+
open_debug_script="$runner_root/orchestration/lib/open-debug.mjs"
|
|
56
|
+
|
|
57
|
+
find_mobile_root() {
|
|
58
|
+
local dir="$(pwd -P)"
|
|
59
|
+
while [ "$dir" != "/" ]; do
|
|
60
|
+
if [ -f "$dir/package.json" ] && { grep -q 'metamask-mobile' "$dir/package.json" 2>/dev/null || [[ "$(basename "$dir")" == metamask-mobile* ]]; }; then
|
|
61
|
+
printf '%s\n' "$dir"
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
dir="$(dirname "$dir")"
|
|
65
|
+
done
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
load_env_file() {
|
|
70
|
+
local file="$1"
|
|
71
|
+
[ -f "$file" ] || return 0
|
|
72
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
73
|
+
line="${line#export }"
|
|
74
|
+
case "$line" in
|
|
75
|
+
WATCHER_PORT=*|CDP_PORT=*|IOS_SIMULATOR=*|SIM_UDID=*|ANDROID_DEVICE=*|ADB_SERIAL=*|ANDROID_SERIAL=*)
|
|
76
|
+
key="${line%%=*}"
|
|
77
|
+
val="${line#*=}"
|
|
78
|
+
val="${val%%#*}"
|
|
79
|
+
val="$(printf '%s' "$val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
|
|
80
|
+
[ -n "${!key:-}" ] || export "$key=$val"
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
done < "$file"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
infer_slot_number() {
|
|
87
|
+
local root="$1" base
|
|
88
|
+
base="$(basename "$root")"
|
|
89
|
+
[[ "$base" =~ -([0-9]+)$ ]] && printf '%s\n' "${BASH_REMATCH[1]}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
infer_mobile_platform() {
|
|
93
|
+
if [ -n "${ANDROID_DEVICE:-${ADB_SERIAL:-${ANDROID_SERIAL:-}}}" ]; then
|
|
94
|
+
printf 'android\n'
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
local platform="${RECIPE_HARNESS_PLATFORM:-${PLATFORM:-}}"
|
|
98
|
+
case "$platform" in
|
|
99
|
+
android|ios) printf '%s\n' "$platform"; return 0 ;;
|
|
100
|
+
esac
|
|
101
|
+
printf 'ios\n'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
init_context() {
|
|
105
|
+
root="$(find_mobile_root)" || { echo "mm-recipe: run from a metamask-mobile checkout" >&2; exit 2; }
|
|
106
|
+
load_env_file "$root/.js.env"
|
|
107
|
+
load_env_file "$root/.env"
|
|
108
|
+
load_env_file "$root/.env.local"
|
|
109
|
+
slot="$(infer_slot_number "$root" || true)"
|
|
110
|
+
if [ -n "$slot" ]; then
|
|
111
|
+
[ -n "${WATCHER_PORT:-}" ] || export WATCHER_PORT="806$slot"
|
|
112
|
+
[ -n "${IOS_SIMULATOR:-}" ] || export IOS_SIMULATOR="mm-$slot"
|
|
113
|
+
fi
|
|
114
|
+
# Bare (non-slot) checkout: default to Metro's well-known port so the launch
|
|
115
|
+
# path never trips `set -u` on an unbound WATCHER_PORT. Farm dispatch sets the
|
|
116
|
+
# slot-derived port above; both are the single source read everywhere below.
|
|
117
|
+
[ -n "${WATCHER_PORT:-}" ] || export WATCHER_PORT=8081
|
|
118
|
+
[ -n "${CDP_PORT:-}" ] || export CDP_PORT="${WATCHER_PORT:-}"
|
|
119
|
+
runtime_dir="$(recipe_runtime_dir)"
|
|
120
|
+
export RECIPE_RUNTIME_DIR="$runtime_dir"
|
|
121
|
+
mkdir -p "$root/$runtime_dir" "$root/temp/recipe-artifacts"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
pretty_json() {
|
|
126
|
+
node -e '
|
|
127
|
+
let d="";
|
|
128
|
+
process.stdin.on("data", c => d += c).on("end", () => {
|
|
129
|
+
const color = process.env.RECIPE_COLOR === "1";
|
|
130
|
+
const c = (code, s) => color ? `\x1b[${code}m${s}\x1b[0m` : s;
|
|
131
|
+
try {
|
|
132
|
+
const v = JSON.parse(d);
|
|
133
|
+
if (v.decision) {
|
|
134
|
+
console.log(`${c("1;33", "Decision")}: ${c("1;32", v.decision)}${v.reasonCode ? ` ${c("2", "-")} ${c("0;37", v.reasonCode)}` : ""}`);
|
|
135
|
+
if (Array.isArray(v.reasons)) for (const r of v.reasons) console.log(` ${c("2", "-")} ${r}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (v.adapter && Array.isArray(v.actions) && v.actions.every(a => a && a.name)) {
|
|
139
|
+
console.log(`${c("1;36", "Actions")} ${c("2", `(${v.adapter})`)}:`);
|
|
140
|
+
for (const a of v.actions) {
|
|
141
|
+
console.log(` ${c("1;32", a.name)}${a.description ? ` ${c("2", "-")} ${c("0;37", a.description)}` : ""}`);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (v.status && v.adapter) {
|
|
146
|
+
const status = String(v.status).toUpperCase();
|
|
147
|
+
const code = status === "PASS" || status === "OK" ? "1;32" : status === "FAIL" || status === "ERROR" ? "1;31" : "1;33";
|
|
148
|
+
console.log(`${c(code, status)} ${c("1;36", v.adapter)}${v.compatibilityMode ? ` ${c("2", "-")} ${v.compatibilityMode}` : ""}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (v.account || v.route || v.deviceName) {
|
|
152
|
+
console.log(`${c("1;36", "Device")}: ${v.deviceName || "?"}${v.platform ? ` ${c("2", `(${v.platform})`)}` : ""}`);
|
|
153
|
+
console.log(`${c("1;36", "Route")}: ${v.route && v.route.name ? v.route.name : JSON.stringify(v.route || null)}`);
|
|
154
|
+
if (v.account) console.log(`${c("1;36", "Account")}: ${v.account.name || "?"} ${c("2", v.account.address || "")}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log(JSON.stringify(v, null, 2));
|
|
158
|
+
} catch {
|
|
159
|
+
process.stdout.write(d);
|
|
160
|
+
}
|
|
161
|
+
})'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
json_requested() {
|
|
165
|
+
for arg in "$@"; do [ "$arg" = "--json" ] && return 0; done
|
|
166
|
+
return 1
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
full_logs_requested() {
|
|
170
|
+
for arg in "$@"; do
|
|
171
|
+
case "$arg" in
|
|
172
|
+
--full|-f) return 0 ;;
|
|
173
|
+
esac
|
|
174
|
+
done
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dry_run_requested() {
|
|
179
|
+
for arg in "$@"; do
|
|
180
|
+
case "$arg" in
|
|
181
|
+
--dry-run|--explain|-n) return 0 ;;
|
|
182
|
+
esac
|
|
183
|
+
done
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
color_requested() {
|
|
188
|
+
[ "${RECIPE_NO_COLOR:-}" != "1" ] && { [ "${RECIPE_COLOR:-}" = "1" ] || [ "${FORCE_COLOR:-}" = "1" ] || [ -t 1 ]; }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pipe_pretty() {
|
|
192
|
+
if color_requested; then (export RECIPE_COLOR=1; pretty_json); else pretty_json; fi
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
run_pretty() {
|
|
196
|
+
if json_requested "$@"; then "$@"; else "$@" | pipe_pretty; fi
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
run_json_pretty() {
|
|
200
|
+
if json_requested "$@"; then "$@"; else "$@" --json | pipe_pretty; fi
|
|
201
|
+
}
|
|
202
|
+
log() { printf 'mm-recipe: %s\n' "$*" >&2; }
|
|
203
|
+
|
|
204
|
+
shell_quote() {
|
|
205
|
+
printf '%q' "$1"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
resolve_mobile_harness_script() {
|
|
209
|
+
local candidate
|
|
210
|
+
|
|
211
|
+
for candidate in \
|
|
212
|
+
"${FARMSLOT_RECIPE_HARNESS_SCRIPT:-}" \
|
|
213
|
+
"${RECIPE_HARNESS_SCRIPT:-}"; do
|
|
214
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
215
|
+
printf '%s\n' "$candidate"
|
|
216
|
+
return 0
|
|
217
|
+
fi
|
|
218
|
+
done
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
return 1
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
install_mobile_harness() {
|
|
226
|
+
local harness_script runner_inject
|
|
227
|
+
if harness_script="$(resolve_mobile_harness_script)"; then
|
|
228
|
+
log "Installing mobile recipe harness via ${harness_script}"
|
|
229
|
+
RECIPE_HARNESS_ROOT="$(harness_root)" "$harness_script" mobile install --target "$root"
|
|
230
|
+
return 0
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
runner_inject="$runner_root/orchestration/mobile/inject.sh"
|
|
234
|
+
if [ -x "$runner_inject" ]; then
|
|
235
|
+
log "Installing mobile recipe harness via ${runner_inject}"
|
|
236
|
+
RECIPE_HARNESS_ROOT="$(harness_root)" "$runner_inject" --target "$root"
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
cat >&2 <<EOF
|
|
241
|
+
mm-recipe: mobile recipe harness installer not found.
|
|
242
|
+
|
|
243
|
+
Prepare requires either this runner's mobile injector or an explicit
|
|
244
|
+
recipe-harness script. Set FARMSLOT_RECIPE_HARNESS_SCRIPT or
|
|
245
|
+
RECIPE_HARNESS_SCRIPT to <recipe-harness>/scripts/recipe-harness.
|
|
246
|
+
EOF
|
|
247
|
+
return 1
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
mobile_harness_patched() {
|
|
251
|
+
[ -f "$root/app/dev-tools/AgenticService/AgenticService.ts" ] \
|
|
252
|
+
&& grep -q "AgenticService.install" "$root/app/core/NavigationService/NavigationService.ts" 2>/dev/null \
|
|
253
|
+
&& grep -q "AgentStepHud" "$root/app/components/Nav/App/App.tsx" 2>/dev/null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
ensure_mobile_harness_patched() {
|
|
257
|
+
init_context
|
|
258
|
+
if mobile_harness_patched; then
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
local had_metro=false
|
|
263
|
+
if metro_ready; then
|
|
264
|
+
had_metro=true
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
log "Mobile recipe harness patches missing or stale; reinstalling before launch"
|
|
268
|
+
install_mobile_harness
|
|
269
|
+
|
|
270
|
+
if ! mobile_harness_patched; then
|
|
271
|
+
echo "mm-recipe: mobile recipe harness install completed but required patch markers are still missing" >&2
|
|
272
|
+
return 1
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
if [ "$had_metro" = true ]; then
|
|
276
|
+
log "Restarting Metro so the rebuilt graph includes the refreshed harness patches"
|
|
277
|
+
stop_metro
|
|
278
|
+
fi
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Fast, bounded teaching failure when dependencies are not installed. Metro/yarn
|
|
282
|
+
# can never come up without them, so launching — or retrying the launch — only
|
|
283
|
+
# loops. Grounded in the primary mobile deps marker runtime-decision reads
|
|
284
|
+
# (node_modules/.yarn-state.yml, orchestration/mobile/deps-markers.ts).
|
|
285
|
+
require_mobile_deps() {
|
|
286
|
+
[ -e "$root/node_modules/.yarn-state.yml" ] && return 0
|
|
287
|
+
log "dependencies not installed — run yarn install / yarn setup first."
|
|
288
|
+
log "Next: cd $(shell_quote "$root") && yarn setup"
|
|
289
|
+
return 2
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
metro_ready() {
|
|
293
|
+
curl -sf --max-time 2 "http://localhost:${WATCHER_PORT:-8081}/status" 2>/dev/null | grep -q 'packager-status:running'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
metro_listener_pids() {
|
|
297
|
+
lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
metro_listener_valid() {
|
|
301
|
+
local pids pid command required_var
|
|
302
|
+
pids="$(metro_listener_pids)"
|
|
303
|
+
[ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 1
|
|
304
|
+
|
|
305
|
+
for pid in $pids; do
|
|
306
|
+
command="$(ps eww -p "$pid" -o command= 2>/dev/null || true)"
|
|
307
|
+
case "$command" in
|
|
308
|
+
*"$root"*) ;;
|
|
309
|
+
*)
|
|
310
|
+
log "Metro listener on port ${WATCHER_PORT} is not owned by this checkout (pid ${pid}); restarting it"
|
|
311
|
+
return 1
|
|
312
|
+
;;
|
|
313
|
+
esac
|
|
314
|
+
for required_var in ${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID}; do
|
|
315
|
+
if ! printf '%s\n' "$command" | grep -Eq "(^|[[:space:]])${required_var}="; then
|
|
316
|
+
log "Metro listener on port ${WATCHER_PORT} is missing required env ${required_var} (pid ${pid}); restarting it"
|
|
317
|
+
return 1
|
|
318
|
+
fi
|
|
319
|
+
done
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
return 0
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
metro_start_command() {
|
|
326
|
+
local metro_workers="$1" clear_arg="$2"
|
|
327
|
+
local quoted_port quoted_workers
|
|
328
|
+
quoted_port="$(shell_quote "$WATCHER_PORT")"
|
|
329
|
+
quoted_workers="$(shell_quote "$metro_workers")"
|
|
330
|
+
# No redirect here: the caller redirects the whole command to the log so that
|
|
331
|
+
# pre-exec output (env sourcing, set -e failures) is captured too, not just
|
|
332
|
+
# the bundler's own stdout.
|
|
333
|
+
cat <<EOF
|
|
334
|
+
set -e
|
|
335
|
+
set -a
|
|
336
|
+
[ ! -f .js.env ] || . ./.js.env
|
|
337
|
+
[ ! -f .env ] || . ./.env
|
|
338
|
+
[ ! -f .env.local ] || . ./.env.local
|
|
339
|
+
set +a
|
|
340
|
+
export EXPO_NO_TYPESCRIPT_SETUP=1 WATCHER_PORT=${quoted_port} METRO_PORT=${quoted_port} METRO_MAX_WORKERS=${quoted_workers}
|
|
341
|
+
exec yarn expo start --port ${quoted_port}${clear_arg}
|
|
342
|
+
EOF
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
clear_stale_metro_listener() {
|
|
347
|
+
local pids
|
|
348
|
+
pids="$(metro_listener_pids)"
|
|
349
|
+
[ -n "$pids" ] || return 0
|
|
350
|
+
log "Clearing stale Metro listener on port ${WATCHER_PORT}: $pids"
|
|
351
|
+
# shellcheck disable=SC2086
|
|
352
|
+
kill $pids 2>/dev/null || true
|
|
353
|
+
for _ in $(seq 1 20); do
|
|
354
|
+
pids="$(metro_listener_pids)"
|
|
355
|
+
[ -z "$pids" ] && return 0
|
|
356
|
+
sleep 0.25
|
|
357
|
+
done
|
|
358
|
+
log "Metro listener still held on port ${WATCHER_PORT}; forcing: $pids"
|
|
359
|
+
# shellcheck disable=SC2086
|
|
360
|
+
kill -KILL $pids 2>/dev/null || true
|
|
361
|
+
for _ in $(seq 1 20); do
|
|
362
|
+
pids="$(metro_listener_pids)"
|
|
363
|
+
[ -z "$pids" ] && return 0
|
|
364
|
+
sleep 0.25
|
|
365
|
+
done
|
|
366
|
+
echo "mm-recipe: port ${WATCHER_PORT:-8081} is still occupied after killing stale Metro: $pids" >&2
|
|
367
|
+
return 1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
start_metro_viewer_window() {
|
|
372
|
+
local metro_log_abs="$1"
|
|
373
|
+
local session window metro_tmux
|
|
374
|
+
command -v tmux >/dev/null 2>&1 || return 0
|
|
375
|
+
window="metro-${WATCHER_PORT}"
|
|
376
|
+
metro_tmux="$root/$(recipe_runtime_dir)/metro.tmux"
|
|
377
|
+
session=""
|
|
378
|
+
if [ -n "${slot:-}" ] && tmux has-session -t "=mm-${slot}" 2>/dev/null; then
|
|
379
|
+
session="mm-${slot}"
|
|
380
|
+
fi
|
|
381
|
+
if [ -z "$session" ]; then
|
|
382
|
+
session="$(tmux display-message -p '#S' 2>/dev/null || true)"
|
|
383
|
+
fi
|
|
384
|
+
[ -n "$session" ] || return 0
|
|
385
|
+
tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
|
|
386
|
+
tmux new-window -d -t "$session" -n "$window" "exec tail -n +1 -F $(shell_quote "$metro_log_abs")"
|
|
387
|
+
printf '%s:%s\n' "$session" "$window" > "$metro_tmux"
|
|
388
|
+
log "Metro log viewer in tmux window ${session}:${window}"
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
default_metro_workers() {
|
|
392
|
+
if command -v sysctl >/dev/null 2>&1; then
|
|
393
|
+
local cores
|
|
394
|
+
cores="$(sysctl -n hw.perflevel0.physicalcpu 2>/dev/null || sysctl -n hw.physicalcpu 2>/dev/null || true)"
|
|
395
|
+
if [[ "$cores" =~ ^[0-9]+$ ]] && [ "$cores" -gt 0 ]; then
|
|
396
|
+
if [ "$cores" -gt 6 ]; then printf '6\n'; else printf '%s\n' "$cores"; fi
|
|
397
|
+
return 0
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
printf '4\n'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
start_metro() {
|
|
404
|
+
init_context
|
|
405
|
+
if metro_ready; then
|
|
406
|
+
if [ "${MOBILE_METRO_CLEAR:-0}" = "1" ]; then
|
|
407
|
+
log "Restarting Metro on port ${WATCHER_PORT} with cleared cache (MOBILE_METRO_CLEAR=1)"
|
|
408
|
+
stop_metro
|
|
409
|
+
elif metro_listener_valid; then
|
|
410
|
+
log "Metro already running on port ${WATCHER_PORT}"
|
|
411
|
+
return 0
|
|
412
|
+
else
|
|
413
|
+
stop_metro
|
|
414
|
+
fi
|
|
415
|
+
fi
|
|
416
|
+
clear_stale_metro_listener
|
|
417
|
+
local metro_workers="${METRO_MAX_WORKERS:-$(default_metro_workers)}"
|
|
418
|
+
local runtime_dir="$(recipe_runtime_dir)"
|
|
419
|
+
local metro_log="$runtime_dir/metro.log"
|
|
420
|
+
local metro_log_abs="$root/$metro_log"
|
|
421
|
+
local metro_pid="$runtime_dir/metro.pid"
|
|
422
|
+
local tail_pid=""
|
|
423
|
+
local clear_arg=""
|
|
424
|
+
[ "${MOBILE_METRO_CLEAR:-0}" = "1" ] && clear_arg=" --clear"
|
|
425
|
+
local metro_cmd
|
|
426
|
+
metro_cmd="$(metro_start_command "$metro_workers" "$clear_arg")"
|
|
427
|
+
log "Starting Metro on port ${WATCHER_PORT} (METRO_MAX_WORKERS=${metro_workers}${MOBILE_METRO_CLEAR:+, clear=${MOBILE_METRO_CLEAR}})"
|
|
428
|
+
log "compact build events below (full log: ${metro_log}, recipe logs --full)"
|
|
429
|
+
# Metro runs detached writing to the log; the tmux window is a read-only tail viewer.
|
|
430
|
+
(
|
|
431
|
+
cd "$root"
|
|
432
|
+
mkdir -p "$(dirname "$metro_log")"
|
|
433
|
+
: > "$metro_log_abs"
|
|
434
|
+
bash -c "$metro_cmd" </dev/null > "$metro_log_abs" 2>&1 &
|
|
435
|
+
echo $! > "$metro_pid"
|
|
436
|
+
)
|
|
437
|
+
start_metro_viewer_window "$metro_log_abs"
|
|
438
|
+
if [ "$(recipe_log_ui_mode)" != "quiet" ]; then
|
|
439
|
+
node "$(recipe_log_tui_script)" watch \
|
|
440
|
+
--log "$metro_log_abs" \
|
|
441
|
+
--events "$(recipe_log_events_limit)" \
|
|
442
|
+
--mode "$(recipe_log_ui_mode)" \
|
|
443
|
+
--label "Metro bundler" \
|
|
444
|
+
--success-pattern 'Dev server ready' &
|
|
445
|
+
tail_pid=$!
|
|
446
|
+
fi
|
|
447
|
+
for _ in $(seq 1 90); do
|
|
448
|
+
if metro_ready; then
|
|
449
|
+
[ -n "$tail_pid" ] && kill "$tail_pid" 2>/dev/null && wait "$tail_pid" 2>/dev/null || true
|
|
450
|
+
log "Metro ready: http://localhost:${WATCHER_PORT}/status"
|
|
451
|
+
log "Metro log: $metro_log (tail with: recipe logs)"
|
|
452
|
+
return 0
|
|
453
|
+
fi
|
|
454
|
+
sleep 1
|
|
455
|
+
done
|
|
456
|
+
[ -n "$tail_pid" ] && kill "$tail_pid" 2>/dev/null && wait "$tail_pid" 2>/dev/null || true
|
|
457
|
+
node "$(recipe_log_tui_script)" events --log "$metro_log_abs" --events "$(recipe_log_events_limit)" >&2 || true
|
|
458
|
+
recipe_failure_playbook metro "Metro did not become ready on port ${WATCHER_PORT}" \
|
|
459
|
+
--prefix mm-recipe --log "$metro_log"
|
|
460
|
+
return 1
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
bundle_url() {
|
|
464
|
+
local platform="$1"
|
|
465
|
+
printf 'http://localhost:%s/index.bundle?platform=%s&dev=true&hot=false&lazy=true&transform.engine=hermes&transform.bytecode=1&transform.routerRoot=app&unstable_transformProfile=hermes-stable\n' \
|
|
466
|
+
"${WATCHER_PORT:-8081}" \
|
|
467
|
+
"$platform"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
prewarm_bundle() {
|
|
471
|
+
local platform="$1"
|
|
472
|
+
local timeout="${MOBILE_BUNDLE_PREWARM_TIMEOUT:-900}"
|
|
473
|
+
local runtime_dir="$(recipe_runtime_dir)"
|
|
474
|
+
local metro_log="$root/$runtime_dir/metro.log"
|
|
475
|
+
local url bundle_pid elapsed=0 last_progress=""
|
|
476
|
+
[ "${MOBILE_BUNDLE_PREWARM:-1}" != "0" ] || { log "Skipping ${platform} bundle prewarm (MOBILE_BUNDLE_PREWARM=0)"; return 0; }
|
|
477
|
+
url="$(bundle_url "$platform")"
|
|
478
|
+
log "Prewarming ${platform} bundle before launching dev client (set MOBILE_BUNDLE_PREWARM=0 to skip)"
|
|
479
|
+
curl -sfL --connect-timeout 5 --max-time "$timeout" -o /dev/null "$url" &
|
|
480
|
+
bundle_pid=$!
|
|
481
|
+
while kill -0 "$bundle_pid" 2>/dev/null; do
|
|
482
|
+
local progress=""
|
|
483
|
+
if [ -f "$metro_log" ]; then
|
|
484
|
+
progress="$(grep -E "^[[:space:]]*(iOS|Android).*index\\.(js|tsx?).*%|Bundl(ed|ing)|Finished" "$metro_log" | tail -1 | tr -d '\r' || true)"
|
|
485
|
+
fi
|
|
486
|
+
if [ -n "$progress" ] && [ "$progress" != "$last_progress" ]; then
|
|
487
|
+
log "Bundle progress: $progress"
|
|
488
|
+
last_progress="$progress"
|
|
489
|
+
elif [ $((elapsed % 30)) -eq 0 ]; then
|
|
490
|
+
log "Waiting for ${platform} bundle (${elapsed}s/${timeout}s)${last_progress:+ — $last_progress}"
|
|
491
|
+
fi
|
|
492
|
+
if [ "$elapsed" -ge "$timeout" ]; then
|
|
493
|
+
kill "$bundle_pid" 2>/dev/null || true
|
|
494
|
+
wait "$bundle_pid" 2>/dev/null || true
|
|
495
|
+
echo "mm-recipe: ${platform} bundle did not become ready before launch timeout (${timeout}s)" >&2
|
|
496
|
+
tail -80 "$metro_log" >&2 || true
|
|
497
|
+
return 1
|
|
498
|
+
fi
|
|
499
|
+
sleep 3
|
|
500
|
+
elapsed=$((elapsed + 3))
|
|
501
|
+
done
|
|
502
|
+
if wait "$bundle_pid"; then
|
|
503
|
+
log "${platform} bundle ready"
|
|
504
|
+
return 0
|
|
505
|
+
fi
|
|
506
|
+
echo "mm-recipe: ${platform} bundle did not become ready before launch" >&2
|
|
507
|
+
tail -80 "$metro_log" >&2 || true
|
|
508
|
+
return 1
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
sim_target() { printf '%s\n' "${SIM_UDID:-${IOS_SIMULATOR:-booted}}"; }
|
|
512
|
+
|
|
513
|
+
sim_udid_for_target() {
|
|
514
|
+
local target="$1"
|
|
515
|
+
if [[ "$target" =~ ^[0-9A-Fa-f-]{36}$ ]]; then
|
|
516
|
+
printf '%s\n' "$target"
|
|
517
|
+
return 0
|
|
518
|
+
fi
|
|
519
|
+
xcrun simctl list devices available | sed -n "s/.*${target} (\([0-9A-Fa-f-]*\)) (.*/\1/p" | head -1
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
ios_app_installed() {
|
|
524
|
+
local target="$1" bundle_id="$2"
|
|
525
|
+
xcrun simctl get_app_container "$target" "$bundle_id" app >/dev/null 2>&1
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
run_ios_native_build() {
|
|
529
|
+
local target="$1" reason="$2"
|
|
530
|
+
log "${reason}; installing with yarn start:ios (WATCHER_PORT=${WATCHER_PORT})"
|
|
531
|
+
(
|
|
532
|
+
cd "$root"
|
|
533
|
+
env WATCHER_PORT="$WATCHER_PORT" METRO_PORT="${METRO_PORT:-$WATCHER_PORT}" IOS_SIMULATOR="$target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
android_serial_args() {
|
|
538
|
+
if [ -n "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-}}}" ]; then
|
|
539
|
+
printf '%s\n' "-s" "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE}}}"
|
|
540
|
+
fi
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
android_app_installed() {
|
|
544
|
+
local package_id="$1"
|
|
545
|
+
local serial_args=()
|
|
546
|
+
# shellcheck disable=SC2207
|
|
547
|
+
serial_args=($(android_serial_args))
|
|
548
|
+
adb "${serial_args[@]}" shell pm path "$package_id" 2>/dev/null | grep -q '^package:'
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
run_android_native_build() {
|
|
552
|
+
local reason="$1"
|
|
553
|
+
local serial="${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-}}}"
|
|
554
|
+
log "${reason}; installing with yarn start:android (WATCHER_PORT=${WATCHER_PORT}${serial:+, adb=${serial}})"
|
|
555
|
+
(
|
|
556
|
+
cd "$root"
|
|
557
|
+
env WATCHER_PORT="$WATCHER_PORT" METRO_PORT="${METRO_PORT:-$WATCHER_PORT}" \
|
|
558
|
+
${serial:+ADB_SERIAL="$serial" ANDROID_SERIAL="$serial"} \
|
|
559
|
+
EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:android
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
ensure_android_app_for_mode() {
|
|
564
|
+
local package_id="$1" mode="$2"
|
|
565
|
+
case "$mode" in
|
|
566
|
+
fast)
|
|
567
|
+
if android_app_installed "$package_id"; then
|
|
568
|
+
return 0
|
|
569
|
+
fi
|
|
570
|
+
cat >&2 <<EOF
|
|
571
|
+
mm-recipe: fast mobile prepare requires an installed Android dev client (${package_id}).
|
|
572
|
+
|
|
573
|
+
Fast mode only starts/reuses Metro, prewarms the JS bundle, opens the existing
|
|
574
|
+
dev client, and waits for the bridge. Run with --preflight-mode auto or
|
|
575
|
+
--preflight-mode rebuild-native when the native app needs to be installed.
|
|
576
|
+
EOF
|
|
577
|
+
return 1
|
|
578
|
+
;;
|
|
579
|
+
auto|default)
|
|
580
|
+
if android_app_installed "$package_id"; then
|
|
581
|
+
return 0
|
|
582
|
+
fi
|
|
583
|
+
run_android_native_build "Android dev client ${package_id} is not installed"
|
|
584
|
+
;;
|
|
585
|
+
rebuild-native|clean)
|
|
586
|
+
run_android_native_build "Rebuilding Android dev client ${package_id} (--preflight-mode ${mode})"
|
|
587
|
+
;;
|
|
588
|
+
*)
|
|
589
|
+
echo "mm-recipe: unknown mobile preflight mode: $mode" >&2
|
|
590
|
+
return 2
|
|
591
|
+
;;
|
|
592
|
+
esac
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
ensure_ios_app_for_mode() {
|
|
596
|
+
local target="$1" bundle_id="$2" mode="$3"
|
|
597
|
+
case "$mode" in
|
|
598
|
+
fast)
|
|
599
|
+
if ios_app_installed "$target" "$bundle_id"; then
|
|
600
|
+
return 0
|
|
601
|
+
fi
|
|
602
|
+
cat >&2 <<EOF
|
|
603
|
+
mm-recipe: fast mobile prepare requires an installed iOS dev client (${bundle_id}) on ${target}.
|
|
604
|
+
|
|
605
|
+
Fast mode only starts/reuses Metro, prewarms the JS bundle, opens the existing
|
|
606
|
+
dev client, and waits for the bridge. Run with --preflight-mode auto or
|
|
607
|
+
--preflight-mode rebuild-native when the native app needs to be installed.
|
|
608
|
+
EOF
|
|
609
|
+
return 1
|
|
610
|
+
;;
|
|
611
|
+
auto|default)
|
|
612
|
+
if ios_app_installed "$target" "$bundle_id"; then
|
|
613
|
+
return 0
|
|
614
|
+
fi
|
|
615
|
+
run_ios_native_build "$target" "iOS dev client ${bundle_id} is not installed on ${target}"
|
|
616
|
+
;;
|
|
617
|
+
rebuild-native|clean)
|
|
618
|
+
run_ios_native_build "$target" "Rebuilding iOS dev client ${bundle_id} on ${target} (--preflight-mode ${mode})"
|
|
619
|
+
;;
|
|
620
|
+
*)
|
|
621
|
+
echo "mm-recipe: unknown mobile preflight mode: $mode" >&2
|
|
622
|
+
return 2
|
|
623
|
+
;;
|
|
624
|
+
esac
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
show_simulator() {
|
|
628
|
+
local target="$1" udid=""
|
|
629
|
+
if [ "$target" = "booted" ]; then
|
|
630
|
+
log "Opening Simulator UI"
|
|
631
|
+
open -a Simulator >/dev/null 2>&1 || true
|
|
632
|
+
osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
|
|
633
|
+
return 0
|
|
634
|
+
fi
|
|
635
|
+
udid="$(sim_udid_for_target "$target")"
|
|
636
|
+
if [ -n "$udid" ]; then
|
|
637
|
+
log "Opening Simulator UI for ${target}"
|
|
638
|
+
open -a Simulator --args -CurrentDeviceUDID "$udid" >/dev/null 2>&1 || true
|
|
639
|
+
else
|
|
640
|
+
log "Opening Simulator UI"
|
|
641
|
+
open -a Simulator >/dev/null 2>&1 || true
|
|
642
|
+
fi
|
|
643
|
+
osascript -e 'tell application "Simulator" to activate' >/dev/null 2>&1 || true
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
boot_simulator_if_needed() {
|
|
647
|
+
local target="$1" state=""
|
|
648
|
+
[ "$target" != "booted" ] || return 0
|
|
649
|
+
local line=""
|
|
650
|
+
line="$(xcrun simctl list devices available | grep -F "${target} (" | head -1 || true)"
|
|
651
|
+
case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
|
|
652
|
+
if [ "$state" = "Booted" ]; then return 0; fi
|
|
653
|
+
log "Booting iOS simulator ${target}${state:+ (${state})}"
|
|
654
|
+
xcrun simctl boot "$target" 2>/dev/null || true
|
|
655
|
+
xcrun simctl bootstatus "$target" -b >/dev/null
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
launch_ios() {
|
|
659
|
+
init_context
|
|
660
|
+
require_mobile_deps || return
|
|
661
|
+
ensure_mobile_harness_patched
|
|
662
|
+
start_metro || return
|
|
663
|
+
prewarm_bundle ios || return 1
|
|
664
|
+
local target url encoded bundle_id
|
|
665
|
+
target="$(sim_target)"
|
|
666
|
+
boot_simulator_if_needed "$target"
|
|
667
|
+
show_simulator "$target"
|
|
668
|
+
bundle_id="${IOS_BUNDLE_ID:-io.metamask.MetaMask}"
|
|
669
|
+
ensure_ios_app_for_mode "$target" "$bundle_id" "${MOBILE_PREFLIGHT_MODE:-fast}" || return $?
|
|
670
|
+
xcrun simctl spawn "$target" defaults write "$bundle_id" EXDevMenuIsOnboardingFinished -bool YES 2>/dev/null || true
|
|
671
|
+
encoded="$(python3 - <<PY
|
|
672
|
+
import urllib.parse
|
|
673
|
+
print(urllib.parse.quote('http://localhost:${WATCHER_PORT}?disableOnboarding=1', safe=''))
|
|
674
|
+
PY
|
|
675
|
+
)"
|
|
676
|
+
url="expo-metamask://expo-development-client/?url=${encoded}"
|
|
677
|
+
log "Launching iOS dev client on ${target}"
|
|
678
|
+
xcrun simctl openurl "$target" "$url"
|
|
679
|
+
log "Next: recipe logs # tail Metro"
|
|
680
|
+
log "Next: recipe status # app route/account once loaded"
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
launch_android() {
|
|
684
|
+
init_context
|
|
685
|
+
require_mobile_deps || return
|
|
686
|
+
ensure_mobile_harness_patched
|
|
687
|
+
start_metro || return
|
|
688
|
+
prewarm_bundle android || return 1
|
|
689
|
+
local serial_args=() package_id encoded url
|
|
690
|
+
package_id="${ANDROID_PACKAGE_ID:-io.metamask}"
|
|
691
|
+
ensure_android_app_for_mode "$package_id" "${MOBILE_PREFLIGHT_MODE:-fast}" || return $?
|
|
692
|
+
# shellcheck disable=SC2207
|
|
693
|
+
serial_args=($(android_serial_args))
|
|
694
|
+
adb "${serial_args[@]}" reverse "tcp:${WATCHER_PORT}" "tcp:${WATCHER_PORT}" >/dev/null 2>&1 || true
|
|
695
|
+
encoded="$(python3 - <<PY
|
|
696
|
+
import urllib.parse
|
|
697
|
+
print(urllib.parse.quote('http://localhost:${WATCHER_PORT}?disableOnboarding=1', safe=''))
|
|
698
|
+
PY
|
|
699
|
+
)"
|
|
700
|
+
url="expo-metamask://expo-development-client/?url=${encoded}"
|
|
701
|
+
log "Launching Android dev client"
|
|
702
|
+
adb "${serial_args[@]}" shell am start -a android.intent.action.VIEW -d "$url" >/dev/null
|
|
703
|
+
log "Next: recipe logs # tail Metro"
|
|
704
|
+
log "Next: recipe status # app route/account once loaded"
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
bridge_cmd() {
|
|
708
|
+
init_context
|
|
709
|
+
(cd "$root" && APP_ROOT="$root" node "$bridge" "$@")
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
report_open_debug() {
|
|
713
|
+
local payload="$1" want_json="$2"
|
|
714
|
+
if [ "$want_json" = true ]; then
|
|
715
|
+
printf '%s\n' "$payload"
|
|
716
|
+
return 0
|
|
717
|
+
fi
|
|
718
|
+
node - "$payload" <<'NODE'
|
|
719
|
+
const [text] = process.argv.slice(2);
|
|
720
|
+
try {
|
|
721
|
+
const v = JSON.parse(text);
|
|
722
|
+
if (v.ok) {
|
|
723
|
+
const detail = v.method ? ` via ${v.method}` : '';
|
|
724
|
+
const where = v.port ? ` (port ${v.port})` : '';
|
|
725
|
+
console.log(`mm-recipe: opened debugger${detail}${where}`);
|
|
726
|
+
if (v.target?.url) console.log(`mm-recipe: target ${v.target.url}`);
|
|
727
|
+
process.exit(0);
|
|
728
|
+
}
|
|
729
|
+
console.error(`mm-recipe: ${v.error || 'failed to open debugger'}`);
|
|
730
|
+
if (v.hint) console.error(`mm-recipe: ${v.hint}`);
|
|
731
|
+
process.exit(1);
|
|
732
|
+
} catch (error) {
|
|
733
|
+
console.error(`mm-recipe: ${error.message}`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
NODE
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
_open_debug_ui() {
|
|
740
|
+
local action="$1"
|
|
741
|
+
shift
|
|
742
|
+
local want_json=false
|
|
743
|
+
while [ "$#" -gt 0 ]; do
|
|
744
|
+
case "$1" in
|
|
745
|
+
--json) want_json=true; shift ;;
|
|
746
|
+
-h|--help)
|
|
747
|
+
if [ "$action" = "dev-menu" ]; then
|
|
748
|
+
echo "Usage: mm-recipe dev-menu [--json]"
|
|
749
|
+
else
|
|
750
|
+
echo "Usage: mm-recipe debug [--json]"
|
|
751
|
+
fi
|
|
752
|
+
return 0
|
|
753
|
+
;;
|
|
754
|
+
*) echo "mm-recipe debug: unknown arg: $1" >&2; return 2 ;;
|
|
755
|
+
esac
|
|
756
|
+
done
|
|
757
|
+
init_context
|
|
758
|
+
local payload exit_code=0
|
|
759
|
+
payload="$(node "$open_debug_script" --adapter mobile --port "${WATCHER_PORT:-8081}" --action "$action" --json 2>/dev/null)" || exit_code=$?
|
|
760
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
761
|
+
report_open_debug "$payload" "$want_json"
|
|
762
|
+
return 0
|
|
763
|
+
fi
|
|
764
|
+
local fallback=""
|
|
765
|
+
fallback="$(node - "$payload" <<'NODE'
|
|
766
|
+
const [text] = process.argv.slice(2);
|
|
767
|
+
try {
|
|
768
|
+
const v = JSON.parse(text);
|
|
769
|
+
if (v.fallback) process.stdout.write(String(v.fallback));
|
|
770
|
+
} catch {}
|
|
771
|
+
NODE
|
|
772
|
+
)"
|
|
773
|
+
if [ "$fallback" = "cdp-eval" ]; then
|
|
774
|
+
log "Metro HTTP failed; falling back to CDP eval"
|
|
775
|
+
case "$action" in
|
|
776
|
+
dev-menu)
|
|
777
|
+
if [ "$want_json" = true ]; then bridge_cmd eval "NativeModules?.DevSettings?.show?.() ?? 'not supported'"; else bridge_cmd eval "NativeModules?.DevSettings?.show?.() ?? 'not supported'" | pipe_pretty; fi
|
|
778
|
+
;;
|
|
779
|
+
*)
|
|
780
|
+
if [ "$want_json" = true ]; then bridge_cmd eval "NativeModules?.DevSettings?.openDebugger?.() ?? 'not supported'"; else bridge_cmd eval "NativeModules?.DevSettings?.openDebugger?.() ?? 'not supported'" | pipe_pretty; fi
|
|
781
|
+
;;
|
|
782
|
+
esac
|
|
783
|
+
return 0
|
|
784
|
+
fi
|
|
785
|
+
report_open_debug "$payload" "$want_json" || return 1
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
open_debug_ui() {
|
|
789
|
+
_open_debug_ui debug "$@"
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
open_dev_menu() {
|
|
793
|
+
_open_debug_ui dev-menu "$@"
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
fixture_password() {
|
|
797
|
+
init_context
|
|
798
|
+
node - "$root" <<'NODE'
|
|
799
|
+
const fs = require('fs');
|
|
800
|
+
const path = require('path');
|
|
801
|
+
const root = process.argv[2];
|
|
802
|
+
const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
|
|
803
|
+
if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
|
|
804
|
+
for (const rel of [`${runtimeDir}/wallet-fixture.json`]) {
|
|
805
|
+
try {
|
|
806
|
+
const data = JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
|
|
807
|
+
if (data.password) { process.stdout.write(String(data.password)); process.exit(0); }
|
|
808
|
+
} catch {}
|
|
809
|
+
}
|
|
810
|
+
process.exit(1);
|
|
811
|
+
NODE
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
setup_wallet() {
|
|
815
|
+
init_context
|
|
816
|
+
local fixture="${1:-$(recipe_runtime_dir)/wallet-fixture.json}"
|
|
817
|
+
(cd "$root" && APP_ROOT="$root" "$setup_wallet_script" --fixture "$fixture")
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
setup_wallet_failure_recoverable() {
|
|
821
|
+
local setup_log="$1"
|
|
822
|
+
local metro_log="$root/$(recipe_runtime_dir)/metro.log"
|
|
823
|
+
grep -Eq 'CDP not reachable|Engine\.context\.KeyringController not available|Engine does not exist|Cannot read property '\''transactions'\''|bridge-not-ready|debug-target-missing' "$setup_log" 2>/dev/null && return 0
|
|
824
|
+
tail -200 "$metro_log" 2>/dev/null | grep -Eq 'Cannot read property '\''transactions'\''|Engine does not exist' && return 0
|
|
825
|
+
return 1
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
setup_wallet_with_runtime_recovery() {
|
|
829
|
+
init_context
|
|
830
|
+
local fixture="${1:-$(recipe_runtime_dir)/wallet-fixture.json}"
|
|
831
|
+
local platform="${2:-}"
|
|
832
|
+
local setup_log="$root/$(recipe_runtime_dir)/setup-wallet.last.log"
|
|
833
|
+
mkdir -p "$(dirname "$setup_log")"
|
|
834
|
+
|
|
835
|
+
if setup_wallet "$fixture" > "$setup_log" 2>&1; then
|
|
836
|
+
cat "$setup_log"
|
|
837
|
+
return 0
|
|
838
|
+
fi
|
|
839
|
+
|
|
840
|
+
local rc=$?
|
|
841
|
+
cat "$setup_log" >&2
|
|
842
|
+
if [ "${MOBILE_SETUP_CLEAN_RETRY:-1}" != "1" ] || ! setup_wallet_failure_recoverable "$setup_log"; then
|
|
843
|
+
return "$rc"
|
|
844
|
+
fi
|
|
845
|
+
|
|
846
|
+
log "Wallet setup failed during runtime startup; retrying once with a clean Metro cache for slot port ${WATCHER_PORT}"
|
|
847
|
+
stop_metro
|
|
848
|
+
case "$platform" in
|
|
849
|
+
ios) MOBILE_METRO_CLEAR=1 launch_ios ;;
|
|
850
|
+
android) MOBILE_METRO_CLEAR=1 launch_android ;;
|
|
851
|
+
*) MOBILE_METRO_CLEAR=1 refresh_mobile ;;
|
|
852
|
+
esac
|
|
853
|
+
if setup_wallet "$fixture" > "$setup_log" 2>&1; then
|
|
854
|
+
cat "$setup_log"
|
|
855
|
+
return 0
|
|
856
|
+
fi
|
|
857
|
+
rc=$?
|
|
858
|
+
cat "$setup_log" >&2
|
|
859
|
+
return "$rc"
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
screenshot() {
|
|
863
|
+
init_context
|
|
864
|
+
local out="${1:-temp/recipe-artifacts/screenshot-$(date -u +%Y%m%dT%H%M%SZ).png}"
|
|
865
|
+
mkdir -p "$root/$(dirname "$out")"
|
|
866
|
+
if [ -n "${ANDROID_DEVICE:-${ADB_SERIAL:-${ANDROID_SERIAL:-}}}" ]; then
|
|
867
|
+
local serial_args=()
|
|
868
|
+
[ -n "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-}}}" ] && serial_args=(-s "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE}}}")
|
|
869
|
+
adb "${serial_args[@]}" exec-out screencap -p > "$root/$out"
|
|
870
|
+
else
|
|
871
|
+
xcrun simctl io "$(sim_target)" screenshot "$root/$out" >/dev/null
|
|
872
|
+
fi
|
|
873
|
+
log "screenshot: $root/$out"
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
tail_logs() {
|
|
877
|
+
init_context
|
|
878
|
+
local log_file="$root/$(recipe_runtime_dir)/metro.log"
|
|
879
|
+
[ -f "$log_file" ] || { echo "mm-recipe: Metro log missing: $(recipe_runtime_dir)/metro.log. Start with: recipe ios|android" >&2; return 1; }
|
|
880
|
+
if full_logs_requested "$@"; then
|
|
881
|
+
log "full log: $(recipe_runtime_dir)/metro.log (Ctrl-C to stop)"
|
|
882
|
+
tail -f "$log_file"
|
|
883
|
+
return 0
|
|
884
|
+
fi
|
|
885
|
+
log "compact tail — full stream: recipe logs --full"
|
|
886
|
+
exec node "$(recipe_log_tui_script)" tail \
|
|
887
|
+
--log "$log_file" \
|
|
888
|
+
--events "$(recipe_log_events_limit)" \
|
|
889
|
+
--follow \
|
|
890
|
+
--mode compact
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
refresh_mobile() {
|
|
894
|
+
if [ -n "${ANDROID_DEVICE:-${ADB_SERIAL:-${ANDROID_SERIAL:-}}}" ]; then
|
|
895
|
+
launch_android
|
|
896
|
+
else
|
|
897
|
+
launch_ios
|
|
898
|
+
fi
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
status_home() {
|
|
902
|
+
init_context
|
|
903
|
+
if ! json_requested "$@"; then
|
|
904
|
+
recipe_instant_hint "checking mobile runtime..."
|
|
905
|
+
recipe_progress status start "probing Metro, bridge, and readiness"
|
|
906
|
+
fi
|
|
907
|
+
local rs_file="$root/$(recipe_runtime_dir)/runtime-status.json"
|
|
908
|
+
local harness_stale=false harness_root
|
|
909
|
+
harness_root="$(harness_root)"
|
|
910
|
+
mkdir -p "$root/$(recipe_runtime_dir)"
|
|
911
|
+
recipe_warn_harness_stale mobile "$root" "$harness_root" "$runner_root" && harness_stale=false || harness_stale=true
|
|
912
|
+
runtime_status --json > "$rs_file"
|
|
913
|
+
local home_args=(
|
|
914
|
+
--adapter mobile --target "$root" --runtime-dir "$(recipe_runtime_dir)"
|
|
915
|
+
--watcher-port "${WATCHER_PORT:-8081}"
|
|
916
|
+
--runtime-status-file "$rs_file"
|
|
917
|
+
)
|
|
918
|
+
[ "$harness_stale" = true ] && home_args+=(--harness-stale)
|
|
919
|
+
if json_requested "$@"; then
|
|
920
|
+
node "$(recipe_home_script)" "${home_args[@]}" --json
|
|
921
|
+
else
|
|
922
|
+
node "$(recipe_home_script)" "${home_args[@]}"
|
|
923
|
+
fi
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
show_ports() {
|
|
927
|
+
init_context
|
|
928
|
+
local slot_id="" runtime_json="$root/$(recipe_runtime_dir)/agentic-runtime.json"
|
|
929
|
+
if [ -f "$runtime_json" ]; then
|
|
930
|
+
slot_id="$(node - "$runtime_json" <<'NODE'
|
|
931
|
+
const fs = require('fs');
|
|
932
|
+
try {
|
|
933
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
934
|
+
if (data.slotId) process.stdout.write(String(data.slotId));
|
|
935
|
+
} catch {}
|
|
936
|
+
NODE
|
|
937
|
+
)"
|
|
938
|
+
fi
|
|
939
|
+
local args=(
|
|
940
|
+
--mode ports --adapter mobile --target "$root" --runtime-dir "$(recipe_runtime_dir)"
|
|
941
|
+
--watcher-port "${WATCHER_PORT:-8081}" --slot-id "${slot_id:-}"
|
|
942
|
+
)
|
|
943
|
+
if json_requested "$@"; then
|
|
944
|
+
node "$(recipe_home_script)" "${args[@]}" --json
|
|
945
|
+
else
|
|
946
|
+
node "$(recipe_home_script)" "${args[@]}"
|
|
947
|
+
fi
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
decision_field() {
|
|
951
|
+
node - "$1" "$2" <<'NODE'
|
|
952
|
+
const [text, field] = process.argv.slice(2);
|
|
953
|
+
try {
|
|
954
|
+
const value = JSON.parse(text);
|
|
955
|
+
const result = field.split('.').reduce((node, key) => node?.[key], value);
|
|
956
|
+
if (result !== undefined && result !== null) process.stdout.write(String(result));
|
|
957
|
+
} catch {
|
|
958
|
+
process.exit(1);
|
|
959
|
+
}
|
|
960
|
+
NODE
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
runtime_decision_report() {
|
|
964
|
+
init_context
|
|
965
|
+
local metro_log="$root/$(recipe_runtime_dir)/metro.log"
|
|
966
|
+
"$runner" runtime-decision --adapter mobile --target "$root" \
|
|
967
|
+
--watcher-port "${WATCHER_PORT:-8081}" \
|
|
968
|
+
--metro-log "$metro_log" \
|
|
969
|
+
--platform "$(infer_mobile_platform)" \
|
|
970
|
+
"$@"
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
decision() {
|
|
974
|
+
init_context
|
|
975
|
+
run_json_pretty "$runner" runtime-decision --adapter mobile --target "$root" \
|
|
976
|
+
--watcher-port "${WATCHER_PORT:-8081}" \
|
|
977
|
+
--metro-log "$root/$(recipe_runtime_dir)/metro.log" \
|
|
978
|
+
--platform "$(infer_mobile_platform)" \
|
|
979
|
+
"$@"
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
run_mobile_install() {
|
|
983
|
+
init_context
|
|
984
|
+
log "Installing mobile dependencies (yarn setup)"
|
|
985
|
+
if ! (
|
|
986
|
+
cd "$root"
|
|
987
|
+
export PLATFORM="$(infer_mobile_platform)"
|
|
988
|
+
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 yarn setup
|
|
989
|
+
); then
|
|
990
|
+
echo "mm-recipe: yarn setup failed" >&2
|
|
991
|
+
return 1
|
|
992
|
+
fi
|
|
993
|
+
if metro_ready; then
|
|
994
|
+
log "Stopping Metro after yarn setup so the next launch rebundles with fresh deps"
|
|
995
|
+
stop_metro
|
|
996
|
+
fi
|
|
997
|
+
export MOBILE_METRO_CLEAR=1
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
record_mobile_deps_baseline() {
|
|
1001
|
+
init_context
|
|
1002
|
+
local metro_log="$root/$(recipe_runtime_dir)/metro.log"
|
|
1003
|
+
if ! "$runner" runtime-decision --adapter mobile --target "$root" \
|
|
1004
|
+
--watcher-port "${WATCHER_PORT:-8081}" \
|
|
1005
|
+
--metro-log "$metro_log" \
|
|
1006
|
+
--platform "$(infer_mobile_platform)" \
|
|
1007
|
+
--record-baseline --json >/dev/null; then
|
|
1008
|
+
log "failed to record mobile deps baseline"
|
|
1009
|
+
return 1
|
|
1010
|
+
fi
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
up_emit_json() {
|
|
1014
|
+
local report="$1"
|
|
1015
|
+
shift
|
|
1016
|
+
if json_requested "$@"; then
|
|
1017
|
+
printf '%s\n' "$report"
|
|
1018
|
+
fi
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
up_final_json_report() {
|
|
1022
|
+
runtime_decision_report --json --platform "$(infer_mobile_platform)"
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
do_up() {
|
|
1026
|
+
init_context
|
|
1027
|
+
local start=$SECONDS exit_code=0 report action metro_log
|
|
1028
|
+
metro_log="$root/$(recipe_runtime_dir)/metro.log"
|
|
1029
|
+
recipe_progress up start "evaluating mobile runtime"
|
|
1030
|
+
report="$(runtime_decision_report --json)"
|
|
1031
|
+
action="$(decision_field "$report" decision || true)"
|
|
1032
|
+
if dry_run_requested "$@"; then
|
|
1033
|
+
if json_requested "$@"; then
|
|
1034
|
+
printf '%s\n' "$report"
|
|
1035
|
+
else
|
|
1036
|
+
printf '%s\n' "$report" | pipe_pretty
|
|
1037
|
+
fi
|
|
1038
|
+
case "$action" in
|
|
1039
|
+
ready) log "dry-run: would wait for in-app bridge and show route/account" ;;
|
|
1040
|
+
launch) log "dry-run: would start Metro + launch dev client ($(recipe_runtime_dir)/metro.log)" ;;
|
|
1041
|
+
install) log "dry-run: would run yarn setup, then re-check" ;;
|
|
1042
|
+
blocked|unknown|'') log "dry-run: blocked — inspect recipe status" ;;
|
|
1043
|
+
*) log "dry-run: unsupported decision ($action)" ;;
|
|
1044
|
+
esac
|
|
1045
|
+
return 0
|
|
1046
|
+
fi
|
|
1047
|
+
if ! json_requested "$@"; then
|
|
1048
|
+
printf '%s\n' "$report" | pipe_pretty
|
|
1049
|
+
fi
|
|
1050
|
+
case "$action" in
|
|
1051
|
+
ready)
|
|
1052
|
+
recipe_progress up bridge "waiting for in-app bridge"
|
|
1053
|
+
wait_for_bridge || exit_code=$?
|
|
1054
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
1055
|
+
record_mobile_deps_baseline || log "warning: deps baseline not recorded after bridge ready"
|
|
1056
|
+
recipe_done up pass "mobile runtime ready" $((SECONDS - start))
|
|
1057
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1058
|
+
if ! json_requested "$@"; then
|
|
1059
|
+
bridge_cmd status | pipe_pretty
|
|
1060
|
+
fi
|
|
1061
|
+
return 0
|
|
1062
|
+
fi
|
|
1063
|
+
recipe_done up fail "bridge not ready" $((SECONDS - start))
|
|
1064
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1065
|
+
recipe_failure_playbook up "bridge not ready" \
|
|
1066
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1067
|
+
return "$exit_code"
|
|
1068
|
+
;;
|
|
1069
|
+
launch)
|
|
1070
|
+
case "$(decision_field "$report" reasonCode || true)" in
|
|
1071
|
+
native-module-stale) export MOBILE_PREFLIGHT_MODE=rebuild-native MOBILE_METRO_CLEAR=1 ;;
|
|
1072
|
+
metro-stale-log|bundle-errors-recoverable) export MOBILE_METRO_CLEAR=1 ;;
|
|
1073
|
+
esac
|
|
1074
|
+
recipe_progress up metro "starting Metro and dev client"
|
|
1075
|
+
refresh_mobile || {
|
|
1076
|
+
recipe_done up fail "Metro/dev client launch failed" $((SECONDS - start))
|
|
1077
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1078
|
+
recipe_failure_playbook metro "Metro/dev client launch failed" \
|
|
1079
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1080
|
+
return $?
|
|
1081
|
+
}
|
|
1082
|
+
recipe_progress up bridge "waiting for in-app bridge"
|
|
1083
|
+
wait_for_bridge || exit_code=$?
|
|
1084
|
+
if [ "$exit_code" -eq 0 ]; then
|
|
1085
|
+
record_mobile_deps_baseline || log "warning: deps baseline not recorded after bridge ready"
|
|
1086
|
+
recipe_done up pass "mobile runtime ready" $((SECONDS - start))
|
|
1087
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1088
|
+
if ! json_requested "$@"; then
|
|
1089
|
+
bridge_cmd status | pipe_pretty
|
|
1090
|
+
fi
|
|
1091
|
+
return 0
|
|
1092
|
+
fi
|
|
1093
|
+
recipe_done up fail "bridge not ready" $((SECONDS - start))
|
|
1094
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1095
|
+
recipe_failure_playbook up "bridge not ready" \
|
|
1096
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1097
|
+
return "$exit_code"
|
|
1098
|
+
;;
|
|
1099
|
+
install)
|
|
1100
|
+
if [ "${RECIPE_UP_INSTALL_ATTEMPTED:-0}" = "1" ]; then
|
|
1101
|
+
report="$(runtime_decision_report --json)"
|
|
1102
|
+
local reason_code
|
|
1103
|
+
reason_code="$(decision_field "$report" reasonCode || true)"
|
|
1104
|
+
recipe_done up fail "install did not resolve runtime (${reason_code:-install})" $((SECONDS - start))
|
|
1105
|
+
up_emit_json "$report" "$@"
|
|
1106
|
+
if ! json_requested "$@"; then
|
|
1107
|
+
printf '%s\n' "$report" | pipe_pretty
|
|
1108
|
+
fi
|
|
1109
|
+
recipe_failure_playbook up "yarn setup completed but runtime-decision is still install" \
|
|
1110
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1111
|
+
return 2
|
|
1112
|
+
fi
|
|
1113
|
+
recipe_progress up install "installing dependencies"
|
|
1114
|
+
export RECIPE_UP_INSTALL_ATTEMPTED=1
|
|
1115
|
+
run_mobile_install || {
|
|
1116
|
+
recipe_done up fail "yarn setup failed" $((SECONDS - start))
|
|
1117
|
+
up_emit_json "$(up_final_json_report)" "$@"
|
|
1118
|
+
return 1
|
|
1119
|
+
}
|
|
1120
|
+
record_mobile_deps_baseline || log "warning: deps baseline not recorded after yarn setup"
|
|
1121
|
+
RECIPE_UP_INSTALL_ATTEMPTED=1 do_up "$@"
|
|
1122
|
+
;;
|
|
1123
|
+
blocked|unknown|'')
|
|
1124
|
+
recipe_done up fail "blocked (${action:-unknown})" $((SECONDS - start))
|
|
1125
|
+
up_emit_json "$report" "$@"
|
|
1126
|
+
recipe_failure_playbook up "blocked (${action:-unknown})" \
|
|
1127
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1128
|
+
return 2
|
|
1129
|
+
;;
|
|
1130
|
+
*)
|
|
1131
|
+
recipe_done up fail "unsupported decision: $action" $((SECONDS - start))
|
|
1132
|
+
up_emit_json "$report" "$@"
|
|
1133
|
+
recipe_failure_playbook up "unsupported decision: $action" \
|
|
1134
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1135
|
+
return 2
|
|
1136
|
+
;;
|
|
1137
|
+
esac
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
stop_metro() {
|
|
1141
|
+
init_context
|
|
1142
|
+
local pids=""
|
|
1143
|
+
local metro_pid="$root/$(recipe_runtime_dir)/metro.pid"
|
|
1144
|
+
local metro_tmux="$root/$(recipe_runtime_dir)/metro.tmux"
|
|
1145
|
+
local marker=""
|
|
1146
|
+
[ -f "$metro_pid" ] && marker="$(cat "$metro_pid" 2>/dev/null || true)"
|
|
1147
|
+
case "$marker" in
|
|
1148
|
+
tmux:*)
|
|
1149
|
+
# Back-compat: Metro started by a pre-upgrade binary stored the window here.
|
|
1150
|
+
tmux kill-window -t "${marker#tmux:}" >/dev/null 2>&1 || true
|
|
1151
|
+
;;
|
|
1152
|
+
tmux-session:*)
|
|
1153
|
+
tmux kill-session -t "=${marker#tmux-session:}" >/dev/null 2>&1 || true
|
|
1154
|
+
;;
|
|
1155
|
+
''|*[!0-9]*)
|
|
1156
|
+
;;
|
|
1157
|
+
*)
|
|
1158
|
+
pids="$marker"
|
|
1159
|
+
;;
|
|
1160
|
+
esac
|
|
1161
|
+
if [ -f "$metro_tmux" ]; then
|
|
1162
|
+
local viewer
|
|
1163
|
+
viewer="$(cat "$metro_tmux" 2>/dev/null || true)"
|
|
1164
|
+
[ -n "$viewer" ] && { log "Closing Metro log viewer ${viewer}"; tmux kill-window -t "$viewer" >/dev/null 2>&1 || true; }
|
|
1165
|
+
rm -f "$metro_tmux"
|
|
1166
|
+
fi
|
|
1167
|
+
pids="$pids $(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
|
|
1168
|
+
if [ -n "$(printf '%s' "$pids" | tr -d ' ')" ]; then
|
|
1169
|
+
log "Stopping Metro on port ${WATCHER_PORT}: $pids"
|
|
1170
|
+
kill $pids 2>/dev/null || true
|
|
1171
|
+
for _ in $(seq 1 20); do
|
|
1172
|
+
pids="$(lsof -nP -iTCP:${WATCHER_PORT:-8081} -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true)"
|
|
1173
|
+
[ -z "$pids" ] && break
|
|
1174
|
+
sleep 0.25
|
|
1175
|
+
done
|
|
1176
|
+
if [ -n "$(printf '%s' "$pids" | tr -d ' ')" ]; then
|
|
1177
|
+
log "Metro listener still held on port ${WATCHER_PORT}; forcing: $pids"
|
|
1178
|
+
kill -KILL $pids 2>/dev/null || true
|
|
1179
|
+
fi
|
|
1180
|
+
else
|
|
1181
|
+
log "No Metro listener on port ${WATCHER_PORT:-8081}"
|
|
1182
|
+
fi
|
|
1183
|
+
rm -f "$metro_pid"
|
|
1184
|
+
log "next: recipe up | recipe ios | recipe android"
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
wait_for_bridge() {
|
|
1188
|
+
init_context
|
|
1189
|
+
local log_file="$root/$(recipe_runtime_dir)/bridge-status.log"
|
|
1190
|
+
local metro_log="$root/$(recipe_runtime_dir)/metro.log"
|
|
1191
|
+
mkdir -p "$(dirname "$log_file")"
|
|
1192
|
+
local attempt max_polls
|
|
1193
|
+
max_polls="${MOBILE_BRIDGE_READY_POLLS:-90}"
|
|
1194
|
+
for attempt in $(seq 1 "$max_polls"); do
|
|
1195
|
+
if bridge_cmd status > "$log_file" 2>&1 && node - "$log_file" <<'NODE'
|
|
1196
|
+
const fs = require('fs');
|
|
1197
|
+
const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
1198
|
+
const targets = Array.isArray(value) ? value : [value];
|
|
1199
|
+
if (!targets.some((target) => target && typeof target === 'object' && target.route)) process.exit(1);
|
|
1200
|
+
NODE
|
|
1201
|
+
then
|
|
1202
|
+
log "Mobile bridge ready"
|
|
1203
|
+
return 0
|
|
1204
|
+
fi
|
|
1205
|
+
if [ "$attempt" = "1" ] || [ $((attempt % 5)) -eq 0 ]; then
|
|
1206
|
+
log "Waiting for Mobile bridge (${attempt}/${max_polls}): $(bridge_wait_reason "$log_file" "$metro_log")"
|
|
1207
|
+
fi
|
|
1208
|
+
sleep 2
|
|
1209
|
+
done
|
|
1210
|
+
cat "$log_file" >&2 || true
|
|
1211
|
+
recipe_failure_playbook up "bridge did not become ready on port ${WATCHER_PORT:-8081}" \
|
|
1212
|
+
--prefix mm-recipe --log "$(recipe_runtime_dir)/metro.log"
|
|
1213
|
+
return 1
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
bridge_wait_reason() {
|
|
1217
|
+
local status_file="$1"
|
|
1218
|
+
local metro_log="$2"
|
|
1219
|
+
if ! metro_ready; then
|
|
1220
|
+
printf 'Metro is not reachable on port %s' "${WATCHER_PORT:-8081}"
|
|
1221
|
+
return 0
|
|
1222
|
+
fi
|
|
1223
|
+
local targets
|
|
1224
|
+
targets="$(curl -sf --max-time 2 "http://localhost:${WATCHER_PORT:-8081}/json/list" 2>/dev/null || true)"
|
|
1225
|
+
if [ "$targets" = "[]" ] || [ -z "$targets" ]; then
|
|
1226
|
+
printf 'Metro is running; no React Native debug target yet'
|
|
1227
|
+
bridge_bundle_progress "$metro_log"
|
|
1228
|
+
return 0
|
|
1229
|
+
fi
|
|
1230
|
+
if [ -s "$status_file" ] && grep -qxF '[]' "$status_file"; then
|
|
1231
|
+
printf 'React Native target is visible; in-app bridge is not ready yet'
|
|
1232
|
+
bridge_bundle_progress "$metro_log"
|
|
1233
|
+
return 0
|
|
1234
|
+
fi
|
|
1235
|
+
printf 'bridge probe failed'
|
|
1236
|
+
bridge_bundle_progress "$metro_log"
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
bridge_bundle_progress() {
|
|
1240
|
+
local metro_log="$1"
|
|
1241
|
+
[ -f "$metro_log" ] || return 0
|
|
1242
|
+
local progress
|
|
1243
|
+
progress="$(grep -E '^[[:space:]]*(iOS|Android).*index\\.(js|tsx?).*%|Bundl(ed|ing)|Finished' "$metro_log" | tail -1 | tr -d '\r' || true)"
|
|
1244
|
+
[ -n "$progress" ] && printf ' (%s)' "$progress"
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
runtime_status() {
|
|
1248
|
+
local target="" port="" runtime_dir_arg=""
|
|
1249
|
+
while [ "$#" -gt 0 ]; do
|
|
1250
|
+
case "$1" in
|
|
1251
|
+
--target) target="$2"; shift 2 ;;
|
|
1252
|
+
--port|--cdp-port) port="$2"; shift 2 ;;
|
|
1253
|
+
--runtime-dir) runtime_dir_arg="$2"; shift 2 ;;
|
|
1254
|
+
--json) shift ;;
|
|
1255
|
+
-h|--help)
|
|
1256
|
+
echo "Usage: mm-recipe runtime-status [--target <repo>] [--port <port>] [--runtime-dir <dir>] [--json]"
|
|
1257
|
+
return 0
|
|
1258
|
+
;;
|
|
1259
|
+
*) echo "mm-recipe runtime-status: unknown arg: $1" >&2; return 2 ;;
|
|
1260
|
+
esac
|
|
1261
|
+
done
|
|
1262
|
+
[ -n "$target" ] && cd "$target"
|
|
1263
|
+
[ -n "$runtime_dir_arg" ] && export RECIPE_RUNTIME_DIR="$runtime_dir_arg"
|
|
1264
|
+
[ -n "$port" ] && export WATCHER_PORT="$port" CDP_PORT="${CDP_PORT:-$port}"
|
|
1265
|
+
init_context
|
|
1266
|
+
local runtime_dir="$(recipe_runtime_dir)"
|
|
1267
|
+
local runtime_abs="$root/$runtime_dir"
|
|
1268
|
+
local status_file="$runtime_abs/runtime-status.json"
|
|
1269
|
+
local metro_log="$runtime_abs/metro.log"
|
|
1270
|
+
mkdir -p "$runtime_abs"
|
|
1271
|
+
node - "$root" "$runtime_dir" "${WATCHER_PORT:-8081}" "$metro_log" "$bridge" "$status_file" <<'NODE'
|
|
1272
|
+
const fs = require('fs');
|
|
1273
|
+
const http = require('http');
|
|
1274
|
+
const cp = require('child_process');
|
|
1275
|
+
const [target, runtimeDir, port, metroLog, bridge, statusFile] = process.argv.slice(2);
|
|
1276
|
+
|
|
1277
|
+
function get(path) {
|
|
1278
|
+
return new Promise((resolve) => {
|
|
1279
|
+
const req = http.get(`http://localhost:${port}${path}`, { timeout: 1500 }, (res) => {
|
|
1280
|
+
let body = '';
|
|
1281
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
1282
|
+
res.on('end', () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, body }));
|
|
1283
|
+
});
|
|
1284
|
+
req.on('timeout', () => { req.destroy(); resolve({ ok: false, body: '' }); });
|
|
1285
|
+
req.on('error', () => resolve({ ok: false, body: '' }));
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function bundleStatus() {
|
|
1290
|
+
if (!fs.existsSync(metroLog)) return { phase: 'unknown', source: null };
|
|
1291
|
+
const lines = fs.readFileSync(metroLog, 'utf8').split(/\r?\n/u).filter(Boolean);
|
|
1292
|
+
const latest = [...lines].reverse().find((line) =>
|
|
1293
|
+
/^\s*(iOS|Android).*index\.(js|tsx?).*%/u.test(line) ||
|
|
1294
|
+
/Bundl(ed|ing)|Finished/u.test(line)
|
|
1295
|
+
);
|
|
1296
|
+
if (!latest) return { phase: 'unknown', source: metroLog };
|
|
1297
|
+
const progress = /^(?:\s*)(iOS|Android).*?([0-9]+(?:\.[0-9]+)?)%/u.exec(latest);
|
|
1298
|
+
if (progress) {
|
|
1299
|
+
const percent = Number(progress[2]);
|
|
1300
|
+
return {
|
|
1301
|
+
phase: percent >= 100 ? 'bundled' : 'bundling',
|
|
1302
|
+
platform: progress[1].toLowerCase(),
|
|
1303
|
+
percent,
|
|
1304
|
+
detail: latest.trim(),
|
|
1305
|
+
source: metroLog,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
return { phase: /Finished|Bundled/u.test(latest) ? 'bundled' : 'bundling', detail: latest.trim(), source: metroLog };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function bridgeStatus() {
|
|
1312
|
+
try {
|
|
1313
|
+
const out = cp.execFileSync(process.execPath, [bridge, 'status'], {
|
|
1314
|
+
cwd: target,
|
|
1315
|
+
env: { ...process.env, APP_ROOT: target, WATCHER_PORT: port },
|
|
1316
|
+
encoding: 'utf8',
|
|
1317
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1318
|
+
timeout: 5000,
|
|
1319
|
+
});
|
|
1320
|
+
const value = JSON.parse(out || 'null');
|
|
1321
|
+
const targets = Array.isArray(value) ? value : [value];
|
|
1322
|
+
const readyTarget = targets.find((item) => item && typeof item === 'object' && item.route);
|
|
1323
|
+
return { ready: Boolean(readyTarget), targetCount: targets.filter(Boolean).length, status: value, error: null };
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
return { ready: false, targetCount: 0, status: null, error: error.stderr?.toString?.() || error.message };
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
(async () => {
|
|
1330
|
+
const metroRaw = await get('/status');
|
|
1331
|
+
const metroReachable = metroRaw.ok && /packager-status:running/u.test(metroRaw.body);
|
|
1332
|
+
const listRaw = await get('/json/list');
|
|
1333
|
+
let targets = [];
|
|
1334
|
+
let debugTargetError = null;
|
|
1335
|
+
try {
|
|
1336
|
+
targets = JSON.parse(listRaw.body || '[]');
|
|
1337
|
+
} catch (error) {
|
|
1338
|
+
debugTargetError = error.message;
|
|
1339
|
+
targets = [];
|
|
1340
|
+
}
|
|
1341
|
+
if (!Array.isArray(targets)) {
|
|
1342
|
+
debugTargetError = 'Metro /json/list did not return an array';
|
|
1343
|
+
targets = [];
|
|
1344
|
+
}
|
|
1345
|
+
const bridge = bridgeStatus();
|
|
1346
|
+
const bundle = bundleStatus();
|
|
1347
|
+
const reason = !metroReachable
|
|
1348
|
+
? 'metro-down'
|
|
1349
|
+
: targets.length === 0
|
|
1350
|
+
? (bundle.phase === 'bundling' ? 'bundle-in-progress' : 'debug-target-missing')
|
|
1351
|
+
: !bridge.ready
|
|
1352
|
+
? 'bridge-not-ready'
|
|
1353
|
+
: 'ready';
|
|
1354
|
+
const status = {
|
|
1355
|
+
schemaVersion: 1,
|
|
1356
|
+
adapter: 'mobile',
|
|
1357
|
+
target,
|
|
1358
|
+
runtimeDir,
|
|
1359
|
+
updatedAt: new Date().toISOString(),
|
|
1360
|
+
ready: reason === 'ready',
|
|
1361
|
+
reason,
|
|
1362
|
+
metro: { reachable: metroReachable, port: Number(port) },
|
|
1363
|
+
bundle,
|
|
1364
|
+
debugTarget: { present: targets.length > 0, count: targets.length, error: debugTargetError },
|
|
1365
|
+
bridge: { ready: bridge.ready, targetCount: bridge.targetCount, error: bridge.error, status: bridge.status },
|
|
1366
|
+
};
|
|
1367
|
+
fs.writeFileSync(statusFile, `${JSON.stringify(status, null, 2)}\n`);
|
|
1368
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1369
|
+
})();
|
|
1370
|
+
NODE
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
assert_runtime_ready() {
|
|
1374
|
+
init_context
|
|
1375
|
+
local status_json attempt max_polls
|
|
1376
|
+
max_polls="${MOBILE_RUNTIME_READY_POLLS:-60}"
|
|
1377
|
+
for attempt in $(seq 1 "$max_polls"); do
|
|
1378
|
+
status_json="$(runtime_status --target "$root" --port "${WATCHER_PORT:-}" --runtime-dir "$(recipe_runtime_dir)" --json)"
|
|
1379
|
+
if STATUS_JSON="$status_json" python3 -c 'import json, os, sys
|
|
1380
|
+
status = json.loads(os.environ["STATUS_JSON"])
|
|
1381
|
+
if status.get("ready"):
|
|
1382
|
+
sys.exit(0)
|
|
1383
|
+
print(
|
|
1384
|
+
"mm-recipe: runtime is not ready after prepare "
|
|
1385
|
+
f"(reason={status.get('"'"'reason'"'"')}, metro={status.get('"'"'metro'"'"')}, "
|
|
1386
|
+
f"debugTarget={status.get('"'"'debugTarget'"'"')}, bridge={status.get('"'"'bridge'"'"')})",
|
|
1387
|
+
file=sys.stderr,
|
|
1388
|
+
)
|
|
1389
|
+
sys.exit(1)'
|
|
1390
|
+
then
|
|
1391
|
+
log "Runtime ready after prepare"
|
|
1392
|
+
return 0
|
|
1393
|
+
fi
|
|
1394
|
+
if [ "$attempt" = "1" ] || [ $((attempt % 5)) -eq 0 ]; then
|
|
1395
|
+
log "Waiting for runtime ready after prepare (${attempt}/${max_polls})"
|
|
1396
|
+
fi
|
|
1397
|
+
sleep 2
|
|
1398
|
+
done
|
|
1399
|
+
STATUS_JSON="$status_json" python3 -c 'import json, os, sys
|
|
1400
|
+
status = json.loads(os.environ["STATUS_JSON"])
|
|
1401
|
+
print(
|
|
1402
|
+
"mm-recipe: runtime is not ready after prepare "
|
|
1403
|
+
f"(reason={status.get('"'"'reason'"'"')}, metro={status.get('"'"'metro'"'"')}, "
|
|
1404
|
+
f"debugTarget={status.get('"'"'debugTarget'"'"')}, bridge={status.get('"'"'bridge'"'"')})",
|
|
1405
|
+
file=sys.stderr,
|
|
1406
|
+
)'
|
|
1407
|
+
return 1
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
do_completion() {
|
|
1411
|
+
local install_script="$runner_root/scripts/install-completions.sh"
|
|
1412
|
+
bash "$install_script" "${1:-install}" "${@:2}"
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
do_sync() {
|
|
1416
|
+
init_context
|
|
1417
|
+
local start=$SECONDS farmslot_slot="" fixture_ready=false summary_file="" runtime_json=""
|
|
1418
|
+
RECIPE_CLI_PREFIX=mm-recipe
|
|
1419
|
+
recipe_progress sync start "refreshing mobile harness + fixture context"
|
|
1420
|
+
runtime_json="$root/$(recipe_runtime_dir)/agentic-runtime.json"
|
|
1421
|
+
if [ -f "$runtime_json" ]; then
|
|
1422
|
+
farmslot_slot="$(node - "$runtime_json" <<'NODE'
|
|
1423
|
+
const fs = require('fs');
|
|
1424
|
+
try {
|
|
1425
|
+
const data = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
1426
|
+
if (data.slotId) process.stdout.write(String(data.slotId));
|
|
1427
|
+
} catch {}
|
|
1428
|
+
NODE
|
|
1429
|
+
)"
|
|
1430
|
+
fi
|
|
1431
|
+
[ -n "$farmslot_slot" ] || { [ -n "${slot:-}" ] && farmslot_slot="local-mobile-${slot}"; }
|
|
1432
|
+
log "sync: refreshing harness scripts"
|
|
1433
|
+
if ! install_mobile_harness; then
|
|
1434
|
+
recipe_done sync fail "harness install failed" $((SECONDS - start))
|
|
1435
|
+
return 1
|
|
1436
|
+
fi
|
|
1437
|
+
log "sync: canonicalizing wallet fixture"
|
|
1438
|
+
if recipe_sync_wallet_fixture "$root" "$(recipe_runtime_dir)" "${CDP_PORT:-}" "" "$farmslot_slot"; then
|
|
1439
|
+
fixture_ready=true
|
|
1440
|
+
log "sync: fixture ready at $(recipe_runtime_dir)/wallet-fixture.json"
|
|
1441
|
+
else
|
|
1442
|
+
log "sync: fixture missing - create $(recipe_runtime_dir)/wallet-fixture.json"
|
|
1443
|
+
fi
|
|
1444
|
+
if recipe_check_harness_stale mobile "$root" "$(harness_root)" "$runner_root"; then
|
|
1445
|
+
log "sync: warning - harness still reports stale after install"
|
|
1446
|
+
else
|
|
1447
|
+
log "sync: harness up to date"
|
|
1448
|
+
fi
|
|
1449
|
+
summary_file="$root/$(recipe_runtime_dir)/sync-summary.json"
|
|
1450
|
+
node - "$summary_file" "$fixture_ready" <<'NODE'
|
|
1451
|
+
const fs = require('fs');
|
|
1452
|
+
const [file, fixtureReady] = process.argv.slice(2);
|
|
1453
|
+
fs.writeFileSync(file, `${JSON.stringify({
|
|
1454
|
+
schemaVersion: 1,
|
|
1455
|
+
adapter: 'mobile',
|
|
1456
|
+
status: 'pass',
|
|
1457
|
+
harnessRefreshed: true,
|
|
1458
|
+
fixtureReady: fixtureReady === 'true',
|
|
1459
|
+
generatedAt: new Date().toISOString(),
|
|
1460
|
+
}, null, 2)}\n`);
|
|
1461
|
+
NODE
|
|
1462
|
+
if json_requested "$@"; then
|
|
1463
|
+
cat "$summary_file"
|
|
1464
|
+
fi
|
|
1465
|
+
if $fixture_ready; then
|
|
1466
|
+
recipe_done sync pass "harness refreshed; fixture ready" $((SECONDS - start))
|
|
1467
|
+
else
|
|
1468
|
+
recipe_done sync pass "harness refreshed; fixture missing" $((SECONDS - start))
|
|
1469
|
+
fi
|
|
1470
|
+
return 0
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
prepare_mobile() {
|
|
1474
|
+
local target="" platform="${PLATFORM:-ios}" port="${WATCHER_PORT:-${METRO_PORT:-}}" simulator="" adb_serial="" wallet_setup=false wallet_fixture="" runtime_dir="$(recipe_runtime_dir)" preflight_mode="${RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE:-fast}"
|
|
1475
|
+
while [ "$#" -gt 0 ]; do
|
|
1476
|
+
case "$1" in
|
|
1477
|
+
--target) target="$2"; shift 2 ;;
|
|
1478
|
+
--platform) platform="$2"; shift 2 ;;
|
|
1479
|
+
--preflight-mode|--mode) preflight_mode="$2"; shift 2 ;;
|
|
1480
|
+
--port) port="$2"; shift 2 ;;
|
|
1481
|
+
--simulator) simulator="$2"; shift 2 ;;
|
|
1482
|
+
--adb-serial) adb_serial="$2"; shift 2 ;;
|
|
1483
|
+
--runtime-dir) runtime_dir="$2"; shift 2 ;;
|
|
1484
|
+
--wallet-setup) wallet_setup=true; shift ;;
|
|
1485
|
+
--wallet-fixture) wallet_fixture="$2"; shift 2 ;;
|
|
1486
|
+
-h|--help)
|
|
1487
|
+
echo "Usage: mm-recipe prepare [--target <repo>] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--port <port>] [--simulator <sim>] [--adb-serial <serial>] [--runtime-dir <dir>] [--wallet-setup] [--wallet-fixture <json>]"
|
|
1488
|
+
return 0
|
|
1489
|
+
;;
|
|
1490
|
+
*) echo "mm-recipe prepare: unknown arg: $1" >&2; return 2 ;;
|
|
1491
|
+
esac
|
|
1492
|
+
done
|
|
1493
|
+
case "$preflight_mode" in fast|auto|default|rebuild-native|clean) ;; *) echo "mm-recipe prepare: unknown --preflight-mode: $preflight_mode" >&2; return 2 ;; esac
|
|
1494
|
+
[ -n "$target" ] && cd "$target"
|
|
1495
|
+
export RECIPE_RUNTIME_DIR="$runtime_dir"
|
|
1496
|
+
export RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE="$preflight_mode"
|
|
1497
|
+
export MOBILE_PREFLIGHT_MODE="$preflight_mode"
|
|
1498
|
+
[ -n "$port" ] && export WATCHER_PORT="$port" METRO_PORT="$port" CDP_PORT="${CDP_PORT:-$port}"
|
|
1499
|
+
[ -n "$simulator" ] && export IOS_SIMULATOR="$simulator"
|
|
1500
|
+
if [ -n "$adb_serial" ] && [ "$adb_serial" != "{{adb_serial}}" ]; then
|
|
1501
|
+
export ADB_SERIAL="$adb_serial" ANDROID_SERIAL="$adb_serial"
|
|
1502
|
+
fi
|
|
1503
|
+
init_context
|
|
1504
|
+
install_mobile_harness
|
|
1505
|
+
case "$platform" in
|
|
1506
|
+
ios) launch_ios ;;
|
|
1507
|
+
android) launch_android ;;
|
|
1508
|
+
*) echo "mm-recipe prepare: unknown platform: $platform" >&2; return 2 ;;
|
|
1509
|
+
esac
|
|
1510
|
+
wait_for_bridge
|
|
1511
|
+
if $wallet_setup; then
|
|
1512
|
+
setup_wallet_with_runtime_recovery "${wallet_fixture:-$(recipe_runtime_dir)/wallet-fixture.json}" "$platform"
|
|
1513
|
+
fi
|
|
1514
|
+
assert_runtime_ready
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
run_recipe() {
|
|
1518
|
+
ensure_mobile_harness_patched
|
|
1519
|
+
local recipe="$1"; shift
|
|
1520
|
+
[ -f "$recipe" ] || { echo "mm-recipe: recipe not found: $recipe" >&2; exit 2; }
|
|
1521
|
+
local out="$root/temp/recipe-artifacts/$(basename "$recipe" .json)-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
1522
|
+
log "root=$root port=${WATCHER_PORT:-?} sim=${IOS_SIMULATOR:-${ANDROID_DEVICE:-${ADB_SERIAL:-?}}} out=$out"
|
|
1523
|
+
exec "$runner" run "$recipe" --adapter mobile --project-root "$root" --watcher-port "$WATCHER_PORT" --artifacts-dir "$out" "$@"
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
interactive() {
|
|
1527
|
+
init_context
|
|
1528
|
+
echo "mm-recipe (${root}, port=${WATCHER_PORT:-?}, sim=${IOS_SIMULATOR:-${ANDROID_DEVICE:-${ADB_SERIAL:-?}}})"
|
|
1529
|
+
PS3='choose> '
|
|
1530
|
+
select choice in status ports up refresh ios android logs runtime-status setup-wallet unlock accounts screenshot debug dev-menu doctor actions stop quit; do
|
|
1531
|
+
case "$choice" in
|
|
1532
|
+
status) status_home ;;
|
|
1533
|
+
ports) show_ports ;;
|
|
1534
|
+
up) do_up ;;
|
|
1535
|
+
refresh) refresh_mobile ;;
|
|
1536
|
+
ios) launch_ios ;;
|
|
1537
|
+
android) launch_android ;;
|
|
1538
|
+
logs) tail_logs ;;
|
|
1539
|
+
runtime-status) runtime_status | pipe_pretty ;;
|
|
1540
|
+
setup-wallet) setup_wallet_with_runtime_recovery ;;
|
|
1541
|
+
unlock) bridge_cmd unlock "$(fixture_password)" ;;
|
|
1542
|
+
accounts) bridge_cmd list-accounts | pipe_pretty ;;
|
|
1543
|
+
screenshot) screenshot ;;
|
|
1544
|
+
debug) open_debug_ui ;;
|
|
1545
|
+
dev-menu) open_dev_menu ;;
|
|
1546
|
+
doctor) run_json_pretty "$runner" doctor --adapter mobile --target "$root" ;;
|
|
1547
|
+
actions) run_json_pretty "$runner" actions --adapter mobile ;;
|
|
1548
|
+
stop) stop_metro ;;
|
|
1549
|
+
quit|'') break ;;
|
|
1550
|
+
esac
|
|
1551
|
+
echo
|
|
1552
|
+
done
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
cmd="${1:-status}"
|
|
1556
|
+
[ "$#" -gt 0 ] && shift
|
|
1557
|
+
case "$cmd" in
|
|
1558
|
+
-h|--help) usage; exit 0 ;;
|
|
1559
|
+
interactive|menu) interactive ;;
|
|
1560
|
+
ports) show_ports "$@" ;;
|
|
1561
|
+
up) do_up "$@" ;;
|
|
1562
|
+
decision) decision "$@" ;;
|
|
1563
|
+
sync) do_sync "$@" ;;
|
|
1564
|
+
completion|complete) do_completion "$@" ;;
|
|
1565
|
+
prepare) prepare_mobile "$@" ;;
|
|
1566
|
+
runtime-status) runtime_status "$@" ;;
|
|
1567
|
+
ios|start) launch_ios ;;
|
|
1568
|
+
android) launch_android ;;
|
|
1569
|
+
setup:ios) launch_ios; setup_wallet_with_runtime_recovery "$(recipe_runtime_dir)/wallet-fixture.json" ios ;;
|
|
1570
|
+
setup:android) launch_android; setup_wallet_with_runtime_recovery "$(recipe_runtime_dir)/wallet-fixture.json" android ;;
|
|
1571
|
+
refresh|reload|relaunch) refresh_mobile ;;
|
|
1572
|
+
logs|tail) tail_logs "$@" ;;
|
|
1573
|
+
status|home) status_home "$@" ;;
|
|
1574
|
+
app-status|bridge-status) bridge_cmd status | pipe_pretty ;;
|
|
1575
|
+
route|get-route) bridge_cmd get-route | pipe_pretty ;;
|
|
1576
|
+
navigate) bridge_cmd navigate "$@" ;;
|
|
1577
|
+
back|go-back) bridge_cmd go-back ;;
|
|
1578
|
+
unlock) bridge_cmd unlock "${1:-$(fixture_password)}" ;;
|
|
1579
|
+
setup-wallet|wallet-setup) setup_wallet_with_runtime_recovery "${1:-$(recipe_runtime_dir)/wallet-fixture.json}" ;;
|
|
1580
|
+
accounts|list-accounts) bridge_cmd list-accounts | pipe_pretty ;;
|
|
1581
|
+
select-account|switch-account) bridge_cmd switch-account "${1:?address required}" ;;
|
|
1582
|
+
screenshot) screenshot "${1:-}" ;;
|
|
1583
|
+
debug) open_debug_ui "$@" ;;
|
|
1584
|
+
dev-menu|devmenu|menu) open_dev_menu "$@" ;;
|
|
1585
|
+
stop) stop_metro ;;
|
|
1586
|
+
actions) init_context; run_json_pretty "$runner" actions --adapter mobile "$@" ;;
|
|
1587
|
+
doctor) init_context; run_json_pretty "$runner" doctor --adapter mobile --target "$root" "$@" ;;
|
|
1588
|
+
run) recipe="${1:-}"; [ -n "$recipe" ] || { usage; exit 2; }; shift; run_recipe "$recipe" "$@" ;;
|
|
1589
|
+
*.json) run_recipe "$cmd" "$@" ;;
|
|
1590
|
+
*) echo "mm-recipe: unknown command or missing recipe: $cmd" >&2; usage; exit 2 ;;
|
|
1591
|
+
esac
|