@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,681 @@
1
+ #!/usr/bin/env bash
2
+ # inject.sh — install the mobile harness overlay into a checkout
3
+ #
4
+ # Purpose:
5
+ # Installs the runner delegate + recipe/script snapshots into the target's
6
+ # ignored harness dir, and (full mode) the AgenticService app overlay with
7
+ # backup/rollback, hash markers, and .git/info/exclude hygiene.
8
+ #
9
+ # Inputs (flags / env):
10
+ # --target <metamask-mobile> (default $PWD)
11
+ # --allow-dirty-harness-paths skip dirty/managed-hash refusals
12
+ # --force-overlay overwrite a product-owned in-app bridge
13
+ # --no-git-exclude skip .git/info/exclude updates
14
+ # env RECIPE_HARNESS_ROOT, METAMASK_RUNNER_PROTOCOL_ROOT > FARMSLOT_ROOT >
15
+ # <runner>/.farmslot-root (protocol root provenance)
16
+ #
17
+ # Outputs:
18
+ # <harness>/manifest.json, <harness>/runner + scripts snapshots,
19
+ # backup dir + managed-hashes.tsv, added-git-exclude record.
20
+ # Exit 0 — installed; 1 — refusal (dirty paths/changed managed paths) or
21
+ # failed install (rolled back); 2 — bad args.
22
+ #
23
+ # Never touches: tracked product files unless --force-overlay (full mode
24
+ # patches NavigationService.ts/App.tsx with backups); nothing outside the
25
+ # target checkout and its .git dir.
26
+ set -euo pipefail
27
+
28
+ TARGET="$PWD"
29
+ ALLOW_DIRTY=false
30
+ FORCE_OVERLAY=false
31
+ GIT_EXCLUDE=true
32
+ while [ "$#" -gt 0 ]; do
33
+ case "$1" in
34
+ --target) TARGET="$2"; shift 2 ;;
35
+ --allow-dirty-harness-paths) ALLOW_DIRTY=true; shift ;;
36
+ --force-overlay) FORCE_OVERLAY=true; shift ;;
37
+ --no-git-exclude) GIT_EXCLUDE=false; shift ;;
38
+ -h|--help) echo "Usage: install.sh [--target <metamask-mobile>] [--allow-dirty-harness-paths] [--force-overlay] [--no-git-exclude]"; exit 0 ;;
39
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
40
+ esac
41
+ done
42
+
43
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
+ RUNNER_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd -P)"
45
+ ADAPTER_DIR="$RUNNER_DIR/library/actions/mobile"
46
+ # compat-overlays: NOT applied here. orchestration/compat-overlays/ holds
47
+ # reversible patches applied only when injecting into HISTORICAL checkouts
48
+ # (replay/eval against old product commits, e.g. pre-RN-0.81 mobile); the
49
+ # operator/recipe-harness skill applies them manually before the rebuild.
50
+ # See orchestration/compat-overlays/README.md.
51
+ # shellcheck disable=SC1091
52
+ . "$SCRIPT_DIR/../lib/hash-helpers.sh"
53
+ # shellcheck disable=SC1091
54
+ for _hp in "$SCRIPT_DIR/../lib/harness-path.sh"; do
55
+ [ -f "$_hp" ] && { . "$_hp"; break; }
56
+ done
57
+ unset _hp
58
+ if ! command -v harness_root >/dev/null 2>&1; then
59
+ echo "mm-harness: shared lib orchestration/lib/harness-path.sh not found; reinstall the runner." >&2
60
+ exit 1
61
+ fi
62
+ TARGET="$(cd "$TARGET" && pwd)"
63
+ METAMASK_RUNNER_DIR="$RUNNER_DIR"
64
+ METAMASK_RUNNER_SOURCE_KIND="runner-self"
65
+ METAMASK_RUNNER_REVISION="$(git -C "$RUNNER_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
66
+ METAMASK_RUNNER_PROTOCOL_ROOT="${METAMASK_RUNNER_PROTOCOL_ROOT:-${METAMASK_RUNNER_FARMSLOT_ROOT:-${FARMSLOT_ROOT:-}}}"
67
+ if [ -z "$METAMASK_RUNNER_PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
68
+ METAMASK_RUNNER_PROTOCOL_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
69
+ fi
70
+ export METAMASK_RUNNER_DIR METAMASK_RUNNER_SOURCE_KIND METAMASK_RUNNER_REVISION METAMASK_RUNNER_PROTOCOL_ROOT
71
+ CLEANUP_SH="$SCRIPT_DIR/cleanup.sh"
72
+ HARNESS_ROOT="$(harness_root)"
73
+ HARNESS_REL="$HARNESS_ROOT/mobile"
74
+
75
+ refuse_symlink_destination() {
76
+ local rel="$1"
77
+ local path_so_far="$TARGET"
78
+ IFS='/' read -r -a parts <<< "$rel"
79
+ for part in "${parts[@]}"; do
80
+ [ -n "$part" ] || continue
81
+ path_so_far="$path_so_far/$part"
82
+ if [ -L "$path_so_far" ]; then
83
+ echo "Refusing mobile recipe harness install: $rel contains symlink component $path_so_far." >&2
84
+ return 1
85
+ fi
86
+ done
87
+ }
88
+
89
+ HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
90
+ # refuse_symlink_destination walks every path component, so the deepest path also
91
+ # guards its parents ($HARNESS_REL covers the root segments).
92
+ refuse_symlink_destination "$HARNESS_REL"
93
+ refuse_symlink_destination "app"
94
+ refuse_symlink_destination "app/core"
95
+ refuse_symlink_destination "app/dev-tools/AgenticService"
96
+ refuse_symlink_destination "app/core/NavigationService"
97
+ refuse_symlink_destination "app/components"
98
+ refuse_symlink_destination "app/components/Nav"
99
+ refuse_symlink_destination "app/components/Nav/App"
100
+ if GIT_BACKUP_PATH="$(git -C "$TARGET" rev-parse --git-path recipe-harness/mobile/backup 2>/dev/null)"; then
101
+ case "$GIT_BACKUP_PATH" in
102
+ /*) BACKUP_DIR="$GIT_BACKUP_PATH" ;;
103
+ *) BACKUP_DIR="$TARGET/$GIT_BACKUP_PATH" ;;
104
+ esac
105
+ else
106
+ BACKUP_DIR="$HARNESS_DIR/backup-store"
107
+ fi
108
+ OLD_BACKUP_DIR="$HARNESS_DIR/backup"
109
+ if [ "$BACKUP_DIR" != "$OLD_BACKUP_DIR" ] && [ -f "$OLD_BACKUP_DIR/state.env" ] && [ ! -e "$BACKUP_DIR" ]; then
110
+ mkdir -p "$(dirname "$BACKUP_DIR")"
111
+ mv "$OLD_BACKUP_DIR" "$BACKUP_DIR"
112
+ fi
113
+ STATE_FILE="$BACKUP_DIR/state.env"
114
+
115
+ install_v1_runner_assets() {
116
+ # refuse_symlink_destination walks every path component, so the deepest paths
117
+ # also guard their parents ($HARNESS_REL covers the root segments).
118
+ refuse_symlink_destination "$HARNESS_REL/runner"
119
+ refuse_symlink_destination "$HARNESS_REL/scripts"
120
+ refuse_symlink_destination "$HARNESS_REL/action-manifest.json"
121
+ mkdir -p "$HARNESS_DIR"
122
+ rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts"
123
+ mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes"
124
+
125
+ # Mobile Metro/Watchman observes the product checkout. Copying the full
126
+ # TypeScript runner tree into the watched checkout while Metro is running can trigger large
127
+ # file-map recrawls and make the React Native debug bridge unresponsive.
128
+ # Keep the target overlay lightweight: install a harness-owned delegate plus
129
+ # manifest/recipe snapshots, and execute the reviewed external runner source
130
+ # recorded in manifest.json. This preserves the injection contract without
131
+ # making the runner repo or the watched product checkout own the runtime.
132
+ # Emit shell-safe lines: %q-quote the interpolated paths (like CLEANUP_COMMAND
133
+ # below) so a protocol/runner path containing a space — or $()/backtick/
134
+ # quote — cannot break the generated wrapper or inject at runtime.
135
+ local runner_protocol_root_q runner_exec_q target_q
136
+ runner_protocol_root_q="$(printf '%q' "$METAMASK_RUNNER_PROTOCOL_ROOT")"
137
+ runner_exec_q="$(printf '%q' "$METAMASK_RUNNER_DIR/orchestration/porcelain/metamask-recipe")"
138
+ target_q="$(printf '%q' "$TARGET")"
139
+ {
140
+ printf '%s\n' '#!/usr/bin/env bash'
141
+ printf '%s\n' 'set -euo pipefail'
142
+ printf 'TARGET_DIR=%s\n' "$target_q"
143
+ printf '%s\n' 'if [ -f "$TARGET_DIR/.js.env" ]; then'
144
+ printf '%s\n' ' set +u'
145
+ printf '%s\n' ' set -a'
146
+ printf '%s\n' ' . "$TARGET_DIR/.js.env"'
147
+ printf '%s\n' ' set +a'
148
+ printf '%s\n' ' set -u'
149
+ printf '%s\n' 'fi'
150
+ printf '%s\n' 'detect_ios_simulator() {'
151
+ printf '%s\n' ' command -v xcrun >/dev/null 2>&1 || return 0'
152
+ printf '%s\n' ' node <<'"'"'NODE'"'"''
153
+ printf '%s\n' "const cp = require('node:child_process');"
154
+ printf '%s\n' 'let report;'
155
+ printf '%s\n' "try { report = JSON.parse(cp.execFileSync('xcrun', ['simctl', 'list', 'devices', 'booted', '-j'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })); } catch { process.exit(0); }"
156
+ printf '%s\n' "const devices = Object.values(report.devices || {}).flat().filter((device) => device && device.state === 'Booted');"
157
+ printf '%s\n' "const bundleIds = ['io.metamask', 'io.metamask.flask', 'io.metamask.qa'];"
158
+ printf '%s\n' 'const withMetaMask = devices.filter((device) => {'
159
+ printf '%s\n' ' try {'
160
+ printf '%s\n' " const apps = cp.execFileSync('xcrun', ['simctl', 'listapps', device.udid], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });"
161
+ printf '%s\n' ' return bundleIds.some((bundleId) => apps.includes(bundleId));'
162
+ printf '%s\n' ' } catch { return false; }'
163
+ printf '%s\n' '});'
164
+ printf '%s\n' 'const candidates = withMetaMask.length > 0 ? withMetaMask : devices;'
165
+ printf '%s\n' "if (candidates.length === 1) process.stdout.write(candidates[0].udid || candidates[0].name || '');"
166
+ printf '%s\n' 'NODE'
167
+ printf '%s\n' '}'
168
+ printf '%s\n' 'if [ -z "${IOS_SIMULATOR:-}" ] && [ -z "${ANDROID_DEVICE:-}" ] && [ -z "${ADB_SERIAL:-}" ] && [ -z "${ANDROID_SERIAL:-}" ]; then'
169
+ printf '%s\n' ' detected_simulator="$(detect_ios_simulator || true)"'
170
+ printf '%s\n' ' if [ -n "$detected_simulator" ]; then'
171
+ printf '%s\n' ' export IOS_SIMULATOR="$detected_simulator"'
172
+ printf '%s\n' ' fi'
173
+ printf '%s\n' 'fi'
174
+ if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
175
+ printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
176
+ fi
177
+ printf 'exec %s "$@"\n' "$runner_exec_q"
178
+ } > "$HARNESS_DIR/runner/bin/metamask-recipe"
179
+ chmod +x "$HARNESS_DIR/runner/bin/metamask-recipe"
180
+ if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
181
+ printf '%s\n' "$METAMASK_RUNNER_PROTOCOL_ROOT" > "$HARNESS_DIR/runner/.farmslot-root"
182
+ fi
183
+ printf '%s\n' "$METAMASK_RUNNER_DIR" > "$HARNESS_DIR/runner/.runner-source"
184
+ cp "$METAMASK_RUNNER_DIR/library/manifests/mobile.action-manifest.json" "$HARNESS_DIR/action-manifest.json"
185
+ cp "$METAMASK_RUNNER_DIR/library/manifests/mobile.action-manifest.json" "$HARNESS_DIR/runner/manifests/mobile.action-manifest.json"
186
+ cp "$METAMASK_RUNNER_DIR/library/manifests/extension.action-manifest.json" "$HARNESS_DIR/runner/manifests/extension.action-manifest.json"
187
+ if [ -d "$METAMASK_RUNNER_DIR/library/recipes" ]; then
188
+ rsync -a --delete "$METAMASK_RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
189
+ fi
190
+ # Canonical recipe-library snapshot: flows/ + library.json so the installed
191
+ # harness mirrors the runner's library layout.
192
+ if [ -d "$METAMASK_RUNNER_DIR/library/flows" ]; then
193
+ mkdir -p "$HARNESS_DIR/runner/flows"
194
+ rsync -a --delete "$METAMASK_RUNNER_DIR/library/flows/" "$HARNESS_DIR/runner/flows/"
195
+ fi
196
+ if [ -f "$METAMASK_RUNNER_DIR/library/library.json" ]; then
197
+ cp "$METAMASK_RUNNER_DIR/library/library.json" "$HARNESS_DIR/runner/library.json"
198
+ fi
199
+ # Installed-layout back-compat (INTENTIONAL): targets keep a flat
200
+ # <harness>/scripts/ subtree assembled from the runner trees, so installed
201
+ # consumers are unaffected by runner-repo layout changes.
202
+ if [ -d "$METAMASK_RUNNER_DIR/orchestration/mobile" ]; then
203
+ mkdir -p "$HARNESS_DIR/scripts"
204
+ cp "$METAMASK_RUNNER_DIR/orchestration/mobile/launch.sh" "$HARNESS_DIR/scripts/launch.sh"
205
+ cp "$METAMASK_RUNNER_DIR/orchestration/mobile/live.sh" "$HARNESS_DIR/scripts/live.sh"
206
+ cp "$METAMASK_RUNNER_DIR/runner/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
207
+ mkdir -p "$HARNESS_DIR/scripts/lib"
208
+ cp "$METAMASK_RUNNER_DIR/orchestration/lib/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
209
+ cp "$METAMASK_RUNNER_DIR/orchestration/lib/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
210
+ cp "$METAMASK_RUNNER_DIR/orchestration/lib/recipe-paths.mjs" "$HARNESS_DIR/scripts/lib/recipe-paths.mjs"
211
+ chmod -R u+rwX,go+rX "$HARNESS_DIR/scripts"
212
+ find "$HARNESS_DIR/scripts" -type f \( -name '*.sh' -o -name '*.cjs' -o -name '*.mjs' \) -exec chmod +x {} +
213
+ fi
214
+ if [ ! -x "$HARNESS_DIR/runner/bin/metamask-recipe" ]; then
215
+ echo "Refusing mobile recipe harness install: failed to make runner executable." >&2
216
+ return 1
217
+ fi
218
+ }
219
+
220
+ git_tracks_any_under() {
221
+ git -C "$TARGET" ls-files -- "$1" 2>/dev/null | grep -q .
222
+ }
223
+
224
+ has_product_owned_mobile_harness() {
225
+ # If the product repo tracks any first-party harness subtree, installing the
226
+ # runner overlay must be metadata-only by default. Requiring every marker to be
227
+ # present is unsafe for partial Mobile checkouts: falling through would
228
+ # rsync --delete tracked product-owned files without --force-overlay.
229
+ git_tracks_any_under "app/dev-tools/AgenticService" && return 0
230
+ # Marker fallback for checkouts where the harness was patched but not tracked.
231
+ grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null \
232
+ && grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null
233
+ }
234
+
235
+ add_git_exclude_entry() {
236
+ local entry="$1"
237
+ local tracking_file="${2:-}"
238
+ local git_dir
239
+ local exclude_file
240
+ if ! git_dir="$(git -C "$TARGET" rev-parse --git-dir 2>/dev/null)"; then
241
+ return 0
242
+ fi
243
+ # Skip if the path is already gitignored (e.g. a temp/-rooted harness under an
244
+ # existing temp/ rule) — no redundant info/exclude entry needed.
245
+ if git -C "$TARGET" check-ignore -q "${entry%/}" 2>/dev/null; then
246
+ return 0
247
+ fi
248
+ case "$git_dir" in
249
+ /*) ;;
250
+ *) git_dir="$TARGET/$git_dir" ;;
251
+ esac
252
+ exclude_file="$git_dir/info/exclude"
253
+ mkdir -p "$(dirname "$exclude_file")"
254
+ touch "$exclude_file"
255
+ if ! grep -qxF "$entry" "$exclude_file"; then
256
+ echo "$entry" >> "$exclude_file"
257
+ if [ -n "$tracking_file" ]; then
258
+ echo "$entry" >> "$tracking_file"
259
+ fi
260
+ fi
261
+ }
262
+
263
+ if [ "$FORCE_OVERLAY" = false ] && has_product_owned_mobile_harness; then
264
+ install_v1_runner_assets
265
+ SOURCE_REV="$(git -C "$RUNNER_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
266
+ CLEANUP_COMMAND="RECIPE_HARNESS_ROOT=$HARNESS_ROOT $(printf '%q' "$CLEANUP_SH") --target $(printf '%q' "$TARGET")"
267
+ if [ "$GIT_EXCLUDE" = true ]; then
268
+ echo "[recipe-harness] Adding local .git/info/exclude entries (removed on cleanup): $HARNESS_ROOT/"
269
+ add_git_exclude_entry "$HARNESS_ROOT/" "$HARNESS_DIR/added-git-exclude"
270
+ else
271
+ echo "[recipe-harness] --no-git-exclude: skipping .git/info/exclude updates; harness overlay paths may show as untracked in git status."
272
+ fi
273
+ node -e '
274
+ const fs = require("fs");
275
+ const [runnerOwnerDir, runnerOwnerRevision, runnerDir, runnerRevision, runnerSourceKind, adapterRuntime, target, scriptDir, manifestPath, harnessRoot, harnessRel, cleanupCommand] = process.argv.slice(1);
276
+ const m = {
277
+ adapter: "mobile",
278
+ installMode: "product-owned",
279
+ installedAt: new Date().toISOString(),
280
+ source: {
281
+ runnerOwnerDir,
282
+ runnerOwnerRevision,
283
+ runnerDir,
284
+ runnerRevision,
285
+ runnerSourceKind,
286
+ adapterRuntime,
287
+ },
288
+ target,
289
+ protocolVersion: "v1",
290
+ actionManifestPath: harnessRel + "/action-manifest.json",
291
+ runnerEntrypoint: harnessRel + "/runner/bin/metamask-recipe",
292
+ runtimeHelpers: {
293
+ launch: harnessRel + "/scripts/launch.sh",
294
+ live: harnessRel + "/scripts/live.sh",
295
+ verify: harnessRel + "/scripts/verify.sh"
296
+ },
297
+ installedPaths: [],
298
+ harnessInstalledPaths: [harnessRel + "/runner", harnessRel + "/scripts", harnessRel + "/action-manifest.json"],
299
+ patchedFiles: [],
300
+ productOwnedPaths: [
301
+ "app/dev-tools/AgenticService",
302
+ "app/core/NavigationService/NavigationService.ts",
303
+ "app/components/Nav/App/App.tsx"
304
+ ],
305
+ backupDir: null,
306
+ cleanupCommand,
307
+ productDiffExcludes: [
308
+ ":(exclude)" + harnessRoot
309
+ ],
310
+ note: "This checkout already contains the Mobile in-app bridge. Runner install only writes recipe-harness metadata and must not overwrite tracked in-app bridge files."
311
+ };
312
+ fs.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
313
+ ' "$RUNNER_DIR" "$SOURCE_REV" "$METAMASK_RUNNER_DIR" "$METAMASK_RUNNER_REVISION" "$METAMASK_RUNNER_SOURCE_KIND" "$ADAPTER_DIR" "$TARGET" "$SCRIPT_DIR" "$HARNESS_DIR/manifest.json" "$HARNESS_ROOT" "$HARNESS_REL" "$CLEANUP_COMMAND"
314
+ echo "Installed mobile recipe harness metadata only (in-app bridge detected): $HARNESS_DIR/manifest.json"
315
+ exit 0
316
+ fi
317
+
318
+ # Full-install path: reached when the checkout lacks the bridge, or when
319
+ # --force-overlay was passed to deliberately overwrite a product-owned bridge.
320
+ if [ "$FORCE_OVERLAY" = true ] && has_product_owned_mobile_harness; then
321
+ echo "[recipe-harness] --force-overlay: OVERWRITING tracked in-app bridge files with the runner overlay:"
322
+ echo " app/dev-tools/AgenticService, app/core/NavigationService/NavigationService.ts, app/components/Nav/App/App.tsx"
323
+ echo "[recipe-harness] Files are backed up and restorable via cleanup. This replaces the checkout's in-app bridge/HUD."
324
+ fi
325
+ if [ "$GIT_EXCLUDE" = false ]; then
326
+ echo "[recipe-harness] --no-git-exclude: skipping .git/info/exclude updates; harness overlay paths may show as untracked in git status."
327
+ fi
328
+
329
+ INSTALLED=false
330
+ INSTALL_MUTATING=false
331
+ ROLLBACK_BACKUP_DIR="$BACKUP_DIR"
332
+ ROLLBACK_STATE_FILE="$STATE_FILE"
333
+ REFRESH_BACKUP_DIR=""
334
+ REBASE_BACKUP=false
335
+
336
+ verify_installed_paths_unchanged() {
337
+ local hash_file="$BACKUP_DIR/managed-hashes.tsv"
338
+ if [ ! -f "$hash_file" ]; then
339
+ return 0
340
+ fi
341
+ local rel expected actual conflicts=0 stale_clean=0
342
+ while IFS=$'\t' read -r rel expected; do
343
+ [ -n "$rel" ] || continue
344
+ actual="$(hash_path "$rel")"
345
+ if [ "$actual" != "$expected" ]; then
346
+ if git -C "$TARGET" rev-parse --git-dir >/dev/null 2>&1 \
347
+ && [ -z "$(git -C "$TARGET" status --porcelain -- "$rel")" ]; then
348
+ echo "Mobile recipe harness managed path changed after install but is clean in git: $rel" >&2
349
+ echo " expected: $expected" >&2
350
+ echo " actual: $actual" >&2
351
+ stale_clean=1
352
+ else
353
+ echo "Refusing to refresh mobile recipe harness: managed path changed after install: $rel" >&2
354
+ echo " expected: $expected" >&2
355
+ echo " actual: $actual" >&2
356
+ conflicts=1
357
+ fi
358
+ fi
359
+ done < "$hash_file"
360
+ if [ "$conflicts" != "0" ]; then
361
+ cat >&2 <<EOF
362
+ Reinstall would bless local edits as harness-managed and make cleanup unsafe.
363
+ Save/stash product changes or rerun with --allow-dirty-harness-paths if you intentionally want to overwrite and refresh harness-managed state.
364
+ EOF
365
+ exit 1
366
+ fi
367
+ if [ "$stale_clean" != "0" ]; then
368
+ cat >&2 <<EOF
369
+ Managed hashes are stale, but all changed harness-managed paths are clean in git.
370
+ This usually means the checkout was reset/rebased after a previous harness install.
371
+ Rebaselining the harness backup to the current clean checkout before refreshing.
372
+ EOF
373
+ REBASE_BACKUP=true
374
+ fi
375
+ }
376
+
377
+ if [ -f "$HARNESS_DIR/manifest.json" ] && [ -f "$STATE_FILE" ]; then
378
+ INSTALLED=true
379
+ echo "Existing mobile recipe harness found; refreshing injected files from source." >&2
380
+ fi
381
+
382
+ if [ "$ALLOW_DIRTY" = false ] && [ "$INSTALLED" = true ]; then
383
+ verify_installed_paths_unchanged
384
+ fi
385
+
386
+ if [ "$ALLOW_DIRTY" = false ] && [ "$INSTALLED" = false ] && git -C "$TARGET" rev-parse --git-dir >/dev/null 2>&1; then
387
+ DIRTY_PATHS="$(git -C "$TARGET" status --porcelain -- app/dev-tools/AgenticService app/core/NavigationService/NavigationService.ts app/components/Nav/App/App.tsx)"
388
+ if [ -n "$DIRTY_PATHS" ]; then
389
+ cat >&2 <<EOF
390
+ Refusing to install mobile recipe harness over dirty harness paths.
391
+ Clean, stash, or rerun with --allow-dirty-harness-paths if you intentionally want backup/restore behavior.
392
+
393
+ $DIRTY_PATHS
394
+ EOF
395
+ exit 1
396
+ fi
397
+ fi
398
+
399
+ mkdir -p "$HARNESS_DIR"
400
+
401
+ backup_path() {
402
+ local rel="$1"
403
+ local var="$2"
404
+ local target_path="$TARGET/$rel"
405
+ local backup_path="$ACTIVE_BACKUP_DIR/$rel"
406
+ if [ -e "$target_path" ]; then
407
+ mkdir -p "$(dirname "$backup_path")"
408
+ cp -a "$target_path" "$backup_path"
409
+ printf '%s=1\n' "$var" >> "$ACTIVE_STATE_FILE"
410
+ else
411
+ printf '%s=0\n' "$var" >> "$ACTIVE_STATE_FILE"
412
+ fi
413
+ }
414
+
415
+ rollback_path() {
416
+ local rel="$1"
417
+ local existed="$2"
418
+ local target_path="$TARGET/$rel"
419
+ local backup_path="$ROLLBACK_BACKUP_DIR/$rel"
420
+ if [ "$existed" = "1" ]; then
421
+ if [ -e "$backup_path" ]; then
422
+ rm -rf "$target_path"
423
+ mkdir -p "$(dirname "$target_path")"
424
+ cp -a "$backup_path" "$target_path"
425
+ else
426
+ echo "Rollback warning: missing backup for $rel at $backup_path" >&2
427
+ fi
428
+ else
429
+ rm -rf "$target_path"
430
+ fi
431
+ }
432
+
433
+ rollback_git_exclude() {
434
+ [ -f "$ROLLBACK_BACKUP_DIR/added-git-exclude" ] || return 0
435
+ local git_dir exclude_file tmp_file entry
436
+ git_dir="$(git -C "$TARGET" rev-parse --git-dir 2>/dev/null || true)"
437
+ [ -n "$git_dir" ] || return 0
438
+ case "$git_dir" in
439
+ /*) ;;
440
+ *) git_dir="$TARGET/$git_dir" ;;
441
+ esac
442
+ exclude_file="$git_dir/info/exclude"
443
+ [ -f "$exclude_file" ] || return 0
444
+ tmp_file="$(mktemp)"
445
+ cp "$exclude_file" "$tmp_file"
446
+ while IFS= read -r entry; do
447
+ [ -n "$entry" ] || continue
448
+ grep -vxF "$entry" "$tmp_file" > "$tmp_file.next" || true
449
+ mv "$tmp_file.next" "$tmp_file"
450
+ done < "$ROLLBACK_BACKUP_DIR/added-git-exclude"
451
+ mv "$tmp_file" "$exclude_file"
452
+ }
453
+
454
+ rollback_failed_install() {
455
+ local code="${1:-1}"
456
+ if [ "$INSTALL_MUTATING" != true ]; then
457
+ exit "$code"
458
+ fi
459
+
460
+ set +e
461
+ echo "Mobile recipe harness install failed; restoring backed-up product files." >&2
462
+ if [ -f "$ROLLBACK_STATE_FILE" ]; then
463
+ while IFS= read -r _line || [ -n "$_line" ]; do
464
+ [[ "$_line" =~ ^[[:space:]]*(#|$) ]] && continue
465
+ _key="${_line%%=*}"; _val="${_line#*=}"
466
+ case "$_key" in
467
+ AGENTIC_SERVICE_EXISTED|NAVIGATION_SERVICE_EXISTED|APP_TSX_EXISTED) ;;
468
+ *) continue ;;
469
+ esac
470
+ export "$_key=$_val"
471
+ done < "$ROLLBACK_STATE_FILE"
472
+ unset _line _key _val
473
+ rollback_path "app/dev-tools/AgenticService" "${AGENTIC_SERVICE_EXISTED:-0}"
474
+ rollback_path "app/core/NavigationService/NavigationService.ts" "${NAVIGATION_SERVICE_EXISTED:-0}"
475
+ rollback_path "app/components/Nav/App/App.tsx" "${APP_TSX_EXISTED:-0}"
476
+ rollback_git_exclude
477
+ else
478
+ echo "Rollback warning: no backup state found at $ROLLBACK_STATE_FILE" >&2
479
+ fi
480
+ if [ "$INSTALLED" = true ]; then
481
+ [ -n "$REFRESH_BACKUP_DIR" ] && rm -rf "$REFRESH_BACKUP_DIR"
482
+ else
483
+ rm -rf "$HARNESS_DIR"
484
+ rm -rf "$BACKUP_DIR"
485
+ fi
486
+ exit "$code"
487
+ }
488
+
489
+ trap 'rollback_failed_install $?' ERR
490
+
491
+ if [ ! -f "$STATE_FILE" ] || [ "$REBASE_BACKUP" = true ]; then
492
+ TMP_BACKUP_DIR="$(mktemp -d "$HARNESS_DIR/backup.tmp.XXXXXX")"
493
+ ACTIVE_BACKUP_DIR="$TMP_BACKUP_DIR"
494
+ ACTIVE_STATE_FILE="$TMP_BACKUP_DIR/state.env"
495
+ : > "$ACTIVE_STATE_FILE"
496
+ backup_path "app/dev-tools/AgenticService" "AGENTIC_SERVICE_EXISTED"
497
+ backup_path "app/core/NavigationService/NavigationService.ts" "NAVIGATION_SERVICE_EXISTED"
498
+ backup_path "app/components/Nav/App/App.tsx" "APP_TSX_EXISTED"
499
+ if [ "$REBASE_BACKUP" = true ] && [ -e "$BACKUP_DIR" ]; then
500
+ ARCHIVE_DIR="$HARNESS_DIR/backup-stale.$(date -u +%Y%m%dT%H%M%SZ)"
501
+ mkdir -p "$(dirname "$ARCHIVE_DIR")"
502
+ mv "$BACKUP_DIR" "$ARCHIVE_DIR"
503
+ echo "Archived stale mobile harness backup: $ARCHIVE_DIR" >&2
504
+ else
505
+ rm -rf "$BACKUP_DIR"
506
+ fi
507
+ mkdir -p "$(dirname "$BACKUP_DIR")"
508
+ mv "$TMP_BACKUP_DIR" "$BACKUP_DIR"
509
+ STATE_FILE="$BACKUP_DIR/state.env"
510
+ ROLLBACK_BACKUP_DIR="$BACKUP_DIR"
511
+ ROLLBACK_STATE_FILE="$STATE_FILE"
512
+ else
513
+ REFRESH_BACKUP_DIR="$(mktemp -d "$HARNESS_DIR/refresh-backup.tmp.XXXXXX")"
514
+ ACTIVE_BACKUP_DIR="$REFRESH_BACKUP_DIR"
515
+ ACTIVE_STATE_FILE="$REFRESH_BACKUP_DIR/state.env"
516
+ : > "$ACTIVE_STATE_FILE"
517
+ backup_path "app/dev-tools/AgenticService" "AGENTIC_SERVICE_EXISTED"
518
+ backup_path "app/core/NavigationService/NavigationService.ts" "NAVIGATION_SERVICE_EXISTED"
519
+ backup_path "app/components/Nav/App/App.tsx" "APP_TSX_EXISTED"
520
+ ROLLBACK_BACKUP_DIR="$REFRESH_BACKUP_DIR"
521
+ ROLLBACK_STATE_FILE="$REFRESH_BACKUP_DIR/state.env"
522
+ fi
523
+
524
+ INSTALL_MUTATING=true
525
+
526
+ install_v1_runner_assets
527
+
528
+ mkdir -p "$TARGET/app/dev-tools/AgenticService"
529
+ while IFS= read -r -d '' overlay_file; do
530
+ rel="${overlay_file#$ADAPTER_DIR/app-overlay/app/dev-tools/AgenticService/}"
531
+ dest_rel="${rel%.patch}"
532
+ dest="$TARGET/app/dev-tools/AgenticService/$dest_rel"
533
+ mkdir -p "$(dirname "$dest")"
534
+ # Skip when the checkout already has identical content (e.g. the committed
535
+ # in-app seam) so install stays a no-op and produces no spurious diff.
536
+ cmp -s "$overlay_file" "$dest" || cp "$overlay_file" "$dest"
537
+ # Exclude overlay *.test.* files: they are source tests for the overlay
538
+ # templates and must not ship into the product checkout, where the product's
539
+ # jest could pick them up on local runs.
540
+ done < <(find "$ADAPTER_DIR/app-overlay/app/dev-tools/AgenticService" -type f ! -name '*.test.*' -print0)
541
+
542
+ node - "$TARGET" <<'NODE'
543
+ const fs = require('fs');
544
+ const path = require('path');
545
+
546
+ const target = process.argv[2];
547
+
548
+ function patchNavigation() {
549
+ const file = path.join(target, 'app/core/NavigationService/NavigationService.ts');
550
+ if (!fs.existsSync(file)) throw new Error(`missing ${file}`);
551
+ const original = fs.readFileSync(file, 'utf8');
552
+ let src = original;
553
+ const devToolsImport = "import('../../dev-tools/AgenticService/AgenticService')";
554
+ if (src.includes('AgenticService.install')) {
555
+ const upgraded = src
556
+ .replaceAll("import('../AgenticService/AgenticService')", devToolsImport)
557
+ .replaceAll("import('../../core/AgenticService/AgenticService')", devToolsImport);
558
+ if (upgraded !== src) {
559
+ fs.writeFileSync(file, upgraded);
560
+ return 'upgraded-import';
561
+ }
562
+ return 'already-present';
563
+ }
564
+ const marker = ' this.#navigation = this.#createReactAwareNavigation(navRef);\n';
565
+ const insert = `${marker}\n if (__DEV__) {\n import('../../dev-tools/AgenticService/AgenticService').then(\n ({ default: AgenticService }) => {\n AgenticService.install(navRef, this.#navigation);\n },\n );\n }\n`;
566
+ if (!src.includes(marker)) {
567
+ throw new Error(`cannot patch NavigationService.ts: marker not found`);
568
+ }
569
+ src = src.replace(marker, insert);
570
+ if (src === original) return 'already-present';
571
+ fs.writeFileSync(file, src);
572
+ return 'patched';
573
+ }
574
+
575
+ function patchApp() {
576
+ const file = path.join(target, 'app/components/Nav/App/App.tsx');
577
+ if (!fs.existsSync(file)) throw new Error(`missing ${file}`);
578
+ const original = fs.readFileSync(file, 'utf8');
579
+ let src = original;
580
+ let importStatus = 'already-present';
581
+ if (src.includes('core/AgenticService/AgentStepHud') && !src.includes('dev-tools/AgenticService/AgentStepHud')) {
582
+ src = src.replaceAll("../../../core/AgenticService/AgentStepHud", "../../../dev-tools/AgenticService/AgentStepHud");
583
+ importStatus = 'upgraded-import';
584
+ }
585
+ if (!src.includes('dev-tools/AgenticService/AgentStepHud')) {
586
+ const marker = "import PerpsWebSocketHealthToast";
587
+ const line = "import AgentStepHud from '../../../dev-tools/AgenticService/AgentStepHud';\n";
588
+ if (src.includes(marker)) {
589
+ src = src.replace(marker, `${line}${marker}`);
590
+ } else {
591
+ throw new Error(`cannot patch App.tsx: import marker not found`);
592
+ }
593
+ importStatus = 'patched';
594
+ }
595
+ let renderStatus = 'already-present';
596
+ if (!src.includes('<AgentStepHud')) {
597
+ const marker = /^(\s*)<ControllerEventToastBridge\b[^\n]*\/>/m;
598
+ const match = src.match(marker);
599
+ if (!match) {
600
+ throw new Error(`cannot patch App.tsx: render marker not found`);
601
+ }
602
+ src = src.replace(marker, `${match[1]}{__DEV__ && <AgentStepHud />}\n${match[0]}`);
603
+ renderStatus = 'patched';
604
+ }
605
+ if (src !== original) fs.writeFileSync(file, src);
606
+ return `${importStatus},${renderStatus}`;
607
+ }
608
+
609
+ console.log(JSON.stringify({
610
+ navigation: patchNavigation(),
611
+ app: patchApp(),
612
+ }));
613
+ NODE
614
+
615
+ if [ "$GIT_EXCLUDE" = true ]; then
616
+ echo "[recipe-harness] Adding local .git/info/exclude entries (removed on cleanup): $HARNESS_ROOT/, app/dev-tools/AgenticService/"
617
+ add_git_exclude_entry "$HARNESS_ROOT/" "$BACKUP_DIR/added-git-exclude"
618
+ add_git_exclude_entry "app/dev-tools/AgenticService/" "$BACKUP_DIR/added-git-exclude"
619
+ fi
620
+
621
+ write_managed_hashes() {
622
+ local hash_file="$BACKUP_DIR/managed-hashes.tsv"
623
+ local rel
624
+ : > "$hash_file"
625
+ for rel in \
626
+ "app/dev-tools/AgenticService" \
627
+ "app/core/NavigationService/NavigationService.ts" \
628
+ "app/components/Nav/App/App.tsx"; do
629
+ printf '%s\t%s\n' "$rel" "$(hash_path "$rel")" >> "$hash_file"
630
+ done
631
+ }
632
+
633
+ write_managed_hashes
634
+ INSTALL_MUTATING=false
635
+ trap - ERR
636
+ [ -n "$REFRESH_BACKUP_DIR" ] && rm -rf "$REFRESH_BACKUP_DIR"
637
+
638
+ SOURCE_REV="$(git -C "$RUNNER_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
639
+ CLEANUP_COMMAND="RECIPE_HARNESS_ROOT=$HARNESS_ROOT $(printf '%q' "$CLEANUP_SH") --target $(printf '%q' "$TARGET")"
640
+ node -e '
641
+ const fs = require("fs");
642
+ const [runnerOwnerDir, runnerOwnerRevision, runnerDir, runnerRevision, runnerSourceKind, adapterRuntime, target, backupDir, scriptDir, manifestPath, mobileAgenticSource, harnessRoot, harnessRel, cleanupCommand] = process.argv.slice(1);
643
+ const m = {
644
+ adapter: "mobile",
645
+ installedAt: new Date().toISOString(),
646
+ source: {
647
+ runnerOwnerDir,
648
+ runnerOwnerRevision,
649
+ runnerDir,
650
+ runnerRevision,
651
+ runnerSourceKind,
652
+ adapterRuntime,
653
+ },
654
+ target,
655
+ protocolVersion: "v1",
656
+ actionManifestPath: harnessRel + "/action-manifest.json",
657
+ runnerEntrypoint: harnessRel + "/runner/bin/metamask-recipe",
658
+ runtimeHelpers: {
659
+ launch: harnessRel + "/scripts/launch.sh",
660
+ live: harnessRel + "/scripts/live.sh",
661
+ verify: harnessRel + "/scripts/verify.sh"
662
+ },
663
+ installedPaths: [harnessRel + "/runner", harnessRel + "/scripts", harnessRel + "/action-manifest.json", "app/dev-tools/AgenticService"],
664
+ patchedFiles: ["app/core/NavigationService/NavigationService.ts", "app/components/Nav/App/App.tsx"],
665
+ backupDir,
666
+ managedHashes: backupDir + "/managed-hashes.tsv",
667
+ cleanupCommand,
668
+ productDiffExcludes: [
669
+ ":(exclude)" + harnessRoot,
670
+ ":(exclude)app/dev-tools/AgenticService"
671
+ ]
672
+ };
673
+ fs.writeFileSync(manifestPath, JSON.stringify(m, null, 2) + "\n");
674
+ ' "$RUNNER_DIR" "$SOURCE_REV" "$METAMASK_RUNNER_DIR" "$METAMASK_RUNNER_REVISION" "$METAMASK_RUNNER_SOURCE_KIND" "$ADAPTER_DIR" "$TARGET" "$BACKUP_DIR" "$SCRIPT_DIR" "$HARNESS_DIR/manifest.json" "${MOBILE_AGENTIC_SOURCE:-}" "$HARNESS_ROOT" "$HARNESS_REL" "$CLEANUP_COMMAND"
675
+
676
+ ENSURE_DEPS="$SCRIPT_DIR/../lib/ensure-runner-deps.sh"
677
+ if [ -f "$ENSURE_DEPS" ]; then
678
+ bash "$ENSURE_DEPS" "$RUNNER_DIR"
679
+ fi
680
+
681
+ echo "Installed mobile recipe harness: $HARNESS_DIR/manifest.json"