@deeeed/metamask-harness 0.12.0 → 0.14.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 +26 -5
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +4 -4
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +2 -4
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +106 -0
- package/adapters/mobile/bridge-runtime/lib/console-format.cjs +53 -0
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +82 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +17 -13
- package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +10 -5
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +22 -1
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/mobile/wait-for-bridge.sh +41 -23
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/dist/adapters/mobile/prepare.js +23 -2
- package/dist/adapters/mobile/runtime-decision.js +2 -1
- package/dist/adapters.js +33 -10
- package/dist/cli-commands.js +5 -4
- package/dist/commands/call.js +138 -3
- package/dist/commands/device-target.js +10 -3
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/manifest.js +2 -0
- package/dist/commands/run-engine.js +71 -4
- package/dist/commands/run.js +2 -0
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/status.js +19 -4
- package/dist/mm-harness-cli.js +19 -5
- package/dist/recipe-files.js +14 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/actions/mobile/platform/bridge.mjs +35 -4
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.14.0 - 2026-07-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `mm-harness call` accepts action fields as direct `key=value` shorthand in addition to repeated `--arg k=v`, so commands like `mm-harness call ui.navigate page=perps` now match the examples agents naturally write.
|
|
7
|
+
- Packaged recipes are grouped by domain under `library/recipes/<domain>/` and still resolve by dotted names (`perps.smoke`, `runner.smoke`) with legacy aliases for the former flat names.
|
|
8
|
+
- Packaged `perps.smoke` recipes for mobile, extension, and core provide an explicit non-mutating Perps domain smoke. The existing bare `smoke` alias remains a runner/transport smoke for compatibility.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `status` now hydrates slot runtime context before device enumeration, so a Farmslot checkout pinned to `IOS_SIMULATOR=mm-1` reports only the selected slot simulator instead of every booted simulator/device on the machine. Live status also updates `Next:` to `mm-harness logs` when the selected bridge is reachable.
|
|
12
|
+
- The tmux Metro window opened by `launch ios|android` now streams the Metro output it writes to `metro.log` instead of opening a blank server window.
|
|
13
|
+
- `log-tui watch` now matches success/failure patterns only against log content emitted after the watcher starts, so stale markers already present in long-lived Metro/webpack logs cannot false-pass readiness checks.
|
|
14
|
+
- `start-metro` now reaps any existing mobile console-forwarder attached to the same Metro port before spawning this slot's forwarder, even when the old process was launched from another checkout or package install path.
|
|
15
|
+
|
|
16
|
+
## 0.13.0 - 2026-07-08
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Typed cdp-bridge failure codes (`NO_TARGET`, `CDP_TIMEOUT`, `WS_CLOSED`, `METRO_UNREACHABLE`): the bridge classifies a failure at its source and reports it three ways a caller can recover — an `ERROR[<CODE>]:` stderr marker, a code-specific exit status (10–13), and a `Next:` teaching line for the caller's actual situation. `bridge.mjs` attaches the code to the error it throws, and `app.hud` lifecycle-skip classification (`isMobileHudLifecycleSkip`) branches on the code first, keeping the substring needles only as a fallback for output from a bridge that predates the codes. The needle fallback is now case-insensitive, closing a gap where target-discovery's `Pinned Android device …` (capital A) missed a lowercase needle.
|
|
20
|
+
- Console-forwarder now expands object/array console arguments in `metro.log` from the Hermes-supplied inline `preview` (e.g. `{ symbol: "BTC", px: 42 }` instead of the literal `Object`), bounded one level deep and rendered synchronously so the stream never blocks; objects without a preview degrade to their description.
|
|
21
|
+
- `mm-harness call <action> --help` now renders the named action's own field schema (name, type, required, description, example — from the action manifest) above the generic call flags, instead of printing only the generic call help. Fuzzy short-name resolves like `call` (an ambiguous prefix shows every match); an unresolvable name falls back to the generic help plus a pointer to `mm-harness actions`. `run <recipe> --help` is unchanged (recipe-specific help is a separate, larger surface — recipes are not in the action manifest).
|
|
4
22
|
|
|
5
23
|
### Fixed
|
|
6
24
|
- Runner construction no longer fails with `Manifest action app.lifecycle has no registered adapter` when a manifest declaring `app.lifecycle` is used with a non-mobile adapter (e.g. `call --adapter core --action-manifest library/manifests/mobile.action-manifest.json`): lifecycle adapter registration is manifest-driven instead of gated on the mobile adapter. Executing the action outside a mobile run still fails explicitly at target resolution.
|
|
25
|
+
- Mobile run teardown always clears any HUD step left painted on-device, so a failed run no longer strands a FAIL banner for the next run. Best-effort and bounded: a down/transitioning bridge simply has nothing to clear and never masks the run's real outcome.
|
|
26
|
+
- start-metro quick-launch now applies the dev Sentry DSN remap (`MM_SENTRY_DSN` defaults to `MM_SENTRY_DSN_DEV` when unset) at both launch paths. The quick-launch runs `expo start` directly and bypasses `scripts/build.sh`, which normally performs this remap, so without it Sentry never initialized in quick-launched dev clients.
|
|
27
|
+
- `launch ios|android` no longer claims `app + bridge ready` when the app is not actually up for the requested platform. On a dual-platform slot (one iOS simulator + one Android device sharing a Metro), a "ready" verdict could be reached from a shared successful bundle plus a cached OS process, and an answering bridge target belonging to the OTHER platform satisfied the claim — so `launch android` printed ✓ while nothing ran on the Android device. The quick-relaunch path now confirms a bridge target bound to the REQUESTED platform before claiming ready: the platform is passed explicitly to `wait-for-bridge` (`--platform ios|android`), so a slot-injected `IOS_SIMULATOR`/`ADB_SERIAL` for the other platform can no longer decide the match. The boundary-safe platform/device matcher is now a single shared module (`bridge-runtime/lib/match-bridge-target.cjs`) used by both `wait-for-bridge` and the confirm. If no matched target answers it launches the app for real, and if the platform-matched target still cannot be brought up it fails loudly. `wait-for-bridge` timeouts now name what was requested vs what answered.
|
|
7
28
|
|
|
8
29
|
## 0.12.0 - 2026-07-08
|
|
9
30
|
|
|
@@ -21,7 +42,7 @@
|
|
|
21
42
|
### Added
|
|
22
43
|
- Mobile recipes can now use the standard outer `app.lifecycle` action for deterministic performance start states without rebuilding (Android background/foreground/terminate/restart, iOS simulator launch/terminate/restart through the shared Farmslot lifecycle adapter).
|
|
23
44
|
- `fixtures set --device <serial|udid|name>` now pins mobile fixture sync to the requested device using the same targeting rules as `run`, `call`, and `doctor`.
|
|
24
|
-
- New lifecycle-controlled measured flows in the packaged library: `app
|
|
45
|
+
- New lifecycle-controlled measured flows in the packaged library: `app.lifecycle.android-smoke`, `perps.performance.warm-start`, `perps.performance.background-resume`, `perps.performance.cold-start`; performance recipes now keep lifecycle/unlock in `setup[]`/`startState` so measured nodes carry only CUF timings.
|
|
25
46
|
|
|
26
47
|
### Changed
|
|
27
48
|
- `@farmslot/recipe-harness` dependency raised to `^0.4.0` (ships the `app.lifecycle` adapter — the `FARMSLOT_ROOT` local-source fallback is no longer needed at runtime).
|
|
@@ -38,9 +59,9 @@
|
|
|
38
59
|
## 0.10.0 - 2026-07-07
|
|
39
60
|
|
|
40
61
|
### Added
|
|
41
|
-
- **Canonical `perps
|
|
62
|
+
- **Canonical `perps.performance` measured flow** (`library/recipes/perps/performance.mobile.recipe.json`): unlock → open the Perps market list → read live state → open a market detail — one node per user-visible step with stable node names, so the per-node `duration`s in `trace.json` are the timings you monitor. Run pinned: `mm-harness run perps.performance --device <serial> --heal off`. Device-proven end-to-end on a physical Pixel.
|
|
42
63
|
- **`run <name>` resolves personal/team recipe libraries.** Previously only the packaged library was probed by name; custom recipes ran by path. Sources resolve in library-precedence order (personal > team shadow the packaged canonical — a same-named personal recipe wins), path-shaped args never probe libraries, and a miss teaches which sources were searched. Zero-flag default: `$FARMSLOT_HOME/recipe-library`.
|
|
43
|
-
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps
|
|
64
|
+
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps.performance-recipe` contract test, so the doc cannot drift from reality.
|
|
44
65
|
- **`call` declared on mobile and extension manifests** (was core-only) with self-discovery metadata — personal `flows/` segments are now usable from mobile/extension recipes. Honest limitation documented: flows carry no adapter dimension in the protocol yet, so a cross-adapter `call` fails at live-run rather than plan time.
|
|
45
66
|
|
|
46
67
|
### Fixed
|
|
@@ -63,7 +84,7 @@
|
|
|
63
84
|
## 0.8.0 - 2026-07-07
|
|
64
85
|
|
|
65
86
|
### Added
|
|
66
|
-
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `smoke.mobile.recipe.json`), then adapterless (`run perps
|
|
87
|
+
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `library/recipes/runner/smoke.mobile.recipe.json`), then adapterless (`run perps.lifecycle` → `library/recipes/perps/lifecycle.recipe.json`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
|
|
67
88
|
- **`--device <udid|serial|name>` — first-class mobile device targeting on `run`, `call`, and `doctor`.** Resolves against connected devices (adb + booted simulators; exact id first, then exact name with an ambiguity teaching error) and sets the same env `launch --device` does. Mobile `run`/`call` **without** `--device` while more than one targetable device is connected (across android + ios; android state `device`, iOS `Booted`) fail fast listing the devices and the `--device` hint, so a recipe never lands on the wrong device silently. `run --plan`/`--list` are exempt; extension/core teach. Errors honor `--json` with structured envelopes.
|
|
68
89
|
- **`status` gains `devices[]`** (additive): `{platform, id, name, state, selected}` for connected android devices and booted iOS simulators on mobile checkouts — the `yarn a:status` successor; `selected` reflects the current env pinning.
|
|
69
90
|
|
package/adapters/core/cleanup.sh
CHANGED
|
File without changes
|
package/adapters/core/inject.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -88,7 +88,7 @@ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
|
|
|
88
88
|
# --out (optional): a task-local recipes dir. Resolve it safely within the target
|
|
89
89
|
# (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
|
|
90
90
|
# `live --out <dir>` does not silently fall back to the installed default.
|
|
91
|
-
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/smoke.extension.recipe.json"
|
|
91
|
+
SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/runner/smoke.extension.recipe.json"
|
|
92
92
|
if [ -n "$OUT" ]; then
|
|
93
93
|
# Fail fast on a task-local --out that does not contain the requested recipe.
|
|
94
94
|
# Silently falling back to the installed default would validate a different
|
|
@@ -98,11 +98,11 @@ if [ -n "$OUT" ]; then
|
|
|
98
98
|
echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
|
|
99
99
|
exit 2
|
|
100
100
|
fi
|
|
101
|
-
if [ ! -f "$OUT_ABS/smoke.extension.recipe.json" ]; then
|
|
102
|
-
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
101
|
+
if [ ! -f "$OUT_ABS/runner/smoke.extension.recipe.json" ]; then
|
|
102
|
+
echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no runner/smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
|
|
103
103
|
exit 2
|
|
104
104
|
fi
|
|
105
|
-
SMOKE_RECIPE="$OUT_ABS/smoke.extension.recipe.json"
|
|
105
|
+
SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
|
|
106
106
|
fi
|
|
107
107
|
ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
|
|
108
108
|
mkdir -p "$ARTIFACTS/logs"
|
|
File without changes
|
|
@@ -21,6 +21,12 @@ const path = require('node:path');
|
|
|
21
21
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
22
|
const { discoverTarget } = require('./lib/target-discovery.cjs');
|
|
23
23
|
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
24
|
+
const {
|
|
25
|
+
EXIT_CODE_BY_ERROR_CODE,
|
|
26
|
+
TEACHING_BY_ERROR_CODE,
|
|
27
|
+
classifyBridgeErrorMessage,
|
|
28
|
+
formatErrorMarker,
|
|
29
|
+
} = require('./lib/bridge-errors.cjs');
|
|
24
30
|
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
25
31
|
const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
|
|
26
32
|
|
|
@@ -746,6 +752,16 @@ Environment:
|
|
|
746
752
|
}
|
|
747
753
|
|
|
748
754
|
main().catch((err) => {
|
|
755
|
+
// Typed failure: a code stamped at the throw site wins; otherwise classify the
|
|
756
|
+
// message here (still at the bridge, not by a needle far away in adapters.ts).
|
|
757
|
+
// The marker + teaching go to stderr and the exit code carries the class, so a
|
|
758
|
+
// caller recovers the code from either channel.
|
|
759
|
+
const code = err && err.code ? err.code : classifyBridgeErrorMessage(err && err.message);
|
|
760
|
+
if (code) {
|
|
761
|
+
console.error(formatErrorMarker(code, err.message));
|
|
762
|
+
if (TEACHING_BY_ERROR_CODE[code]) console.error(TEACHING_BY_ERROR_CODE[code]);
|
|
763
|
+
process.exit(EXIT_CODE_BY_ERROR_CODE[code] || 1);
|
|
764
|
+
}
|
|
749
765
|
console.error(`ERROR: ${err.message}`);
|
|
750
766
|
process.exit(1);
|
|
751
767
|
});
|
|
@@ -28,6 +28,7 @@ const fs = require('node:fs');
|
|
|
28
28
|
const http = require('node:http');
|
|
29
29
|
const path = require('node:path');
|
|
30
30
|
const { rankRuntimeCandidates } = require('./lib/target-discovery.cjs');
|
|
31
|
+
const { formatArgs: formatConsoleArgs } = require('./lib/console-format.cjs');
|
|
31
32
|
|
|
32
33
|
// Built-in WebSocket (Node 22+), same zero-dependency transport choice as
|
|
33
34
|
// lib/ws-client.cjs — the published package must not depend on `ws`.
|
|
@@ -201,10 +202,7 @@ function levelLabel(type) {
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
function formatArgs(args) {
|
|
204
|
-
|
|
205
|
-
.map((a) => (a.value !== undefined ? String(a.value) : (a.description ?? a.type ?? '')))
|
|
206
|
-
.join(' ');
|
|
207
|
-
return text.length > MAX_LINE_CHARS ? `${text.slice(0, MAX_LINE_CHARS)}…` : text;
|
|
205
|
+
return formatConsoleArgs(args, MAX_LINE_CHARS);
|
|
208
206
|
}
|
|
209
207
|
|
|
210
208
|
function deviceNameFromTitle(title) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Typed bridge failure codes — the single source of truth for classifying a
|
|
4
|
+
// cdp-bridge failure. Substring needles on a critical path are brittle; the
|
|
5
|
+
// bridge classifies at the source and stamps a code, so callers (bridge.mjs,
|
|
6
|
+
// adapters.ts) branch on the code and only fall back to needles for output
|
|
7
|
+
// produced by an older bridge that predates the codes.
|
|
8
|
+
//
|
|
9
|
+
// NO_TARGET no debug target answered (app backgrounded / not attached
|
|
10
|
+
// / a device pin matched nothing).
|
|
11
|
+
// CDP_TIMEOUT the CDP connection or a single CDP message timed out.
|
|
12
|
+
// WS_CLOSED the Hermes debug socket closed mid-command (app reload).
|
|
13
|
+
// METRO_UNREACHABLE Metro's inspector HTTP endpoint could not be reached.
|
|
14
|
+
const BRIDGE_ERROR_CODES = {
|
|
15
|
+
NO_TARGET: 'NO_TARGET',
|
|
16
|
+
CDP_TIMEOUT: 'CDP_TIMEOUT',
|
|
17
|
+
WS_CLOSED: 'WS_CLOSED',
|
|
18
|
+
METRO_UNREACHABLE: 'METRO_UNREACHABLE',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Process exit code the bridge returns per failure code so a caller that only
|
|
22
|
+
// sees the child's exit status (no stderr) can still recover the code. Kept
|
|
23
|
+
// clear of exit 1 (unknown/uncoded) and 2 (usage).
|
|
24
|
+
const EXIT_CODE_BY_ERROR_CODE = {
|
|
25
|
+
NO_TARGET: 10,
|
|
26
|
+
CDP_TIMEOUT: 11,
|
|
27
|
+
WS_CLOSED: 12,
|
|
28
|
+
METRO_UNREACHABLE: 13,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ERROR_CODE_BY_EXIT_CODE = Object.fromEntries(
|
|
32
|
+
Object.entries(EXIT_CODE_BY_ERROR_CODE).map(([code, exit]) => [exit, code]),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Every coded failure teaches the escape for the caller's ACTUAL situation — a
|
|
36
|
+
// down/transitioning target is recoverable, so the Next: line names the action
|
|
37
|
+
// that brings it back rather than leaving the operator to guess.
|
|
38
|
+
const TEACHING_BY_ERROR_CODE = {
|
|
39
|
+
NO_TARGET:
|
|
40
|
+
'Next: bring the app to the foreground with `mm-harness launch <platform>` so a debug target is available.',
|
|
41
|
+
CDP_TIMEOUT:
|
|
42
|
+
'Next: the app is reloading or busy — retry, or `mm-harness launch <platform>` to foreground it.',
|
|
43
|
+
WS_CLOSED:
|
|
44
|
+
'Next: the Hermes debug page closed (app reload/backgrounded) — retry once the app settles.',
|
|
45
|
+
METRO_UNREACHABLE:
|
|
46
|
+
'Next: start Metro with `mm-harness start-metro` (if it is already up, check the slot WATCHER_PORT).',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Needle → code fallback for uncoded output (older bridge). Case-insensitive,
|
|
50
|
+
// first match wins, so order the more specific Metro/no-target needles ahead of
|
|
51
|
+
// the generic 'timed out'.
|
|
52
|
+
const NEEDLE_CODES = [
|
|
53
|
+
['cannot reach metro', BRIDGE_ERROR_CODES.METRO_UNREACHABLE],
|
|
54
|
+
['is metro running', BRIDGE_ERROR_CODES.METRO_UNREACHABLE],
|
|
55
|
+
['timeout fetching', BRIDGE_ERROR_CODES.METRO_UNREACHABLE],
|
|
56
|
+
['no responding bridge target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
57
|
+
['no debug targets found', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
58
|
+
['no suitable debug target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
59
|
+
['did not match any metro target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
60
|
+
['pinned android device', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
61
|
+
['no react native bridge target', BRIDGE_ERROR_CODES.NO_TARGET],
|
|
62
|
+
['websocket closed', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
63
|
+
['websocket error', BRIDGE_ERROR_CODES.WS_CLOSED],
|
|
64
|
+
['cdp connection timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
65
|
+
['cdp message timeout', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
66
|
+
['evaluation timed out', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
67
|
+
['timed out', BRIDGE_ERROR_CODES.CDP_TIMEOUT],
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function classifyBridgeErrorMessage(message) {
|
|
71
|
+
const text = String(message == null ? '' : message).toLowerCase();
|
|
72
|
+
for (const [needle, code] of NEEDLE_CODES) {
|
|
73
|
+
if (text.includes(needle)) return code;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Attach a code to an error at its throw site (source classification).
|
|
79
|
+
function coded(error, code) {
|
|
80
|
+
if (error && typeof error === 'object') error.code = code;
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The bridge prints this marker on stderr so a caller recovers the code without
|
|
85
|
+
// relying on the exit status alone.
|
|
86
|
+
const MARKER = /^ERROR\[([A-Z_]+)\]:/mu;
|
|
87
|
+
|
|
88
|
+
function formatErrorMarker(code, message) {
|
|
89
|
+
return `ERROR[${code}]: ${message}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseErrorMarker(text) {
|
|
93
|
+
const match = MARKER.exec(String(text == null ? '' : text));
|
|
94
|
+
return match ? match[1] : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
BRIDGE_ERROR_CODES,
|
|
99
|
+
EXIT_CODE_BY_ERROR_CODE,
|
|
100
|
+
ERROR_CODE_BY_EXIT_CODE,
|
|
101
|
+
TEACHING_BY_ERROR_CODE,
|
|
102
|
+
classifyBridgeErrorMessage,
|
|
103
|
+
coded,
|
|
104
|
+
formatErrorMarker,
|
|
105
|
+
parseErrorMarker,
|
|
106
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Render CDP Runtime.consoleAPICalled args (RemoteObjects) into a log line.
|
|
4
|
+
// Primitives carry `value`; objects/arrays do not — a bare console.log(obj)
|
|
5
|
+
// arrived as the literal "Object". Hermes ships an inline `preview` (the same
|
|
6
|
+
// one React Native DevTools renders in its console), so objects expand from it
|
|
7
|
+
// synchronously — no extra CDP round-trip, so the stream never blocks. Depth is
|
|
8
|
+
// bounded because previews nest one level via `valuePreview`; beyond that the
|
|
9
|
+
// child preview's own description is used.
|
|
10
|
+
|
|
11
|
+
const MAX_PREVIEW_DEPTH = 2;
|
|
12
|
+
|
|
13
|
+
function formatPropertyValue(prop, depth) {
|
|
14
|
+
if (prop.valuePreview && depth < MAX_PREVIEW_DEPTH) {
|
|
15
|
+
return formatPreview(prop.valuePreview, depth + 1);
|
|
16
|
+
}
|
|
17
|
+
if (prop.type === 'string' && prop.value !== undefined) return JSON.stringify(prop.value);
|
|
18
|
+
if (prop.value !== undefined) return String(prop.value);
|
|
19
|
+
return prop.subtype || prop.type || '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatPreview(preview, depth) {
|
|
23
|
+
const properties = Array.isArray(preview.properties) ? preview.properties : [];
|
|
24
|
+
const overflow = preview.overflow ? ', …' : '';
|
|
25
|
+
if (preview.subtype === 'array') {
|
|
26
|
+
return `[${properties.map((p) => formatPropertyValue(p, depth)).join(', ')}${overflow}]`;
|
|
27
|
+
}
|
|
28
|
+
const body = properties.map((p) => `${p.name}: ${formatPropertyValue(p, depth)}`).join(', ');
|
|
29
|
+
// Name a non-plain constructor (Error, Map, custom class) so the class is not lost.
|
|
30
|
+
const ctor = preview.description && preview.description !== 'Object' ? `${preview.description} ` : '';
|
|
31
|
+
return properties.length > 0 || overflow ? `${ctor}{ ${body}${overflow} }` : `${ctor}{}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatRemoteObject(a) {
|
|
35
|
+
if (!a || typeof a !== 'object') return String(a == null ? '' : a);
|
|
36
|
+
// Primitives (number/boolean/string) carry a directly-usable value.
|
|
37
|
+
if (a.type !== 'object' && a.type !== 'function' && a.value !== undefined) return String(a.value);
|
|
38
|
+
if (a.type === 'undefined') return 'undefined';
|
|
39
|
+
if (a.subtype === 'null') return 'null';
|
|
40
|
+
if (a.preview) return formatPreview(a.preview, 1);
|
|
41
|
+
// Functions, errors, and objects with no preview: the description is the best
|
|
42
|
+
// available text (className / Error stack / etc.); fall back to bare value/type.
|
|
43
|
+
if (a.description !== undefined) return a.description;
|
|
44
|
+
if (a.value !== undefined) return String(a.value);
|
|
45
|
+
return a.type || '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatArgs(args, maxLineChars) {
|
|
49
|
+
const text = (args || []).map(formatRemoteObject).join(' ');
|
|
50
|
+
return maxLineChars && text.length > maxLineChars ? `${text.slice(0, maxLineChars)}…` : text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { formatArgs, formatRemoteObject, formatPreview };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Boundary-safe matcher for CDP bridge targets, shared by wait-for-bridge and the
|
|
4
|
+
// launch ready-path confirm. A launch (or its confirm) may only accept a target
|
|
5
|
+
// belonging to the REQUESTED platform — and the requested device when one is pinned.
|
|
6
|
+
//
|
|
7
|
+
// The requested platform is authoritative and is passed explicitly
|
|
8
|
+
// (WAIT_FOR_BRIDGE_PLATFORM), so it wins over env inference. On a dual-platform slot
|
|
9
|
+
// the other platform's device identity is ambiently injected (a slot-context
|
|
10
|
+
// IOS_SIMULATOR while launching android, or vice versa); without an explicit platform
|
|
11
|
+
// the matcher would fall back to that ambient identity and confirm against the wrong
|
|
12
|
+
// platform's target. Binding the platform up front closes that corner.
|
|
13
|
+
|
|
14
|
+
function matchesBridgeTarget(target, env) {
|
|
15
|
+
const e = env || process.env;
|
|
16
|
+
if (!target || typeof target !== 'object') return false;
|
|
17
|
+
|
|
18
|
+
const requirePlatform = e.WAIT_FOR_BRIDGE_PLATFORM || '';
|
|
19
|
+
// Requested platform is authoritative: never accept the other platform's target,
|
|
20
|
+
// whatever device env is ambiently present.
|
|
21
|
+
if (requirePlatform && target.platform !== requirePlatform) return false;
|
|
22
|
+
|
|
23
|
+
const androidName = e.ANDROID_TARGET_DEVICE_NAME || e.ANDROID_DEVICE || '';
|
|
24
|
+
const adbSerial = e.ADB_SERIAL || e.ANDROID_SERIAL || '';
|
|
25
|
+
const iosSimulator = e.IOS_SIMULATOR || '';
|
|
26
|
+
|
|
27
|
+
// Android device pin applies only when android is requested (or nothing constrains
|
|
28
|
+
// the platform); it must never redirect a platform-bound iOS confirm — a slot-context
|
|
29
|
+
// ADB_SERIAL while launching iOS must not reject every iOS target. Symmetric with the
|
|
30
|
+
// iOS-simulator branch below.
|
|
31
|
+
if ((adbSerial || androidName) && (!requirePlatform || requirePlatform === 'android')) {
|
|
32
|
+
// Require android + boundary-safe device-name match.
|
|
33
|
+
if (target.platform !== 'android') return false;
|
|
34
|
+
if (!androidName) return true;
|
|
35
|
+
const deviceName = String(target.deviceName || '');
|
|
36
|
+
return deviceName === androidName || deviceName.startsWith(`${androidName} -`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// iOS simulator pin applies only when iOS is requested (or nothing constrains the
|
|
40
|
+
// platform); it must never redirect a platform-bound android confirm.
|
|
41
|
+
if (iosSimulator && (!requirePlatform || requirePlatform === 'ios')) {
|
|
42
|
+
return target.platform !== 'android' && target.deviceName === iosSimulator;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// No device pin: the platform gate above is the only constraint.
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// True when at least one answering target matches the request AND carries a route
|
|
50
|
+
// (an in-app agentic bridge is live), not merely a registered debug target.
|
|
51
|
+
function hasMatchingRoute(value, env) {
|
|
52
|
+
const targets = Array.isArray(value) ? value : [value];
|
|
53
|
+
return targets.some((t) => matchesBridgeTarget(t, env) && t && t.route);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Human summary of the targets that answered (platform / deviceName), for teaching on
|
|
57
|
+
// a timeout — e.g. an iOS target answering an android launch.
|
|
58
|
+
function describeTargets(value) {
|
|
59
|
+
const targets = (Array.isArray(value) ? value : [value]).filter((t) => t && typeof t === 'object');
|
|
60
|
+
if (!targets.length) return 'none';
|
|
61
|
+
return targets
|
|
62
|
+
.map((t) => `${t.platform || '?'}${t.deviceName ? `/'${t.deviceName}'` : ''}${t.route ? '' : ' (no route)'}`)
|
|
63
|
+
.join(', ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Human summary of what the run is pinned to, for the same teaching line.
|
|
67
|
+
function describeRequested(env) {
|
|
68
|
+
const e = env || process.env;
|
|
69
|
+
const platform = e.WAIT_FOR_BRIDGE_PLATFORM || '';
|
|
70
|
+
const androidName = e.ANDROID_TARGET_DEVICE_NAME || e.ANDROID_DEVICE || '';
|
|
71
|
+
const adbSerial = e.ADB_SERIAL || e.ANDROID_SERIAL || '';
|
|
72
|
+
const iosSimulator = e.IOS_SIMULATOR || '';
|
|
73
|
+
// Explicit requested platform wins over device-env inference: an iOS-bound confirm
|
|
74
|
+
// with an ambient ADB_SERIAL is still iOS.
|
|
75
|
+
if (platform === 'android') return `android${androidName ? ` / ${androidName}` : ''}`;
|
|
76
|
+
if (platform === 'ios') return `ios${iosSimulator ? ` / ${iosSimulator}` : ''}`;
|
|
77
|
+
if (adbSerial || androidName) return `android${androidName ? ` / ${androidName}` : ''}`;
|
|
78
|
+
if (iosSimulator) return `ios${iosSimulator ? ` / ${iosSimulator}` : ''}`;
|
|
79
|
+
return 'any platform';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { matchesBridgeTarget, hasMatchingRoute, describeTargets, describeRequested };
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const http = require('node:http');
|
|
4
4
|
const { loadSimulatorName, loadAndroidDevice, loadAndroidTargetDeviceName } = require('./config.cjs');
|
|
5
5
|
const { createWSClient } = require('./ws-client.cjs');
|
|
6
|
+
const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
|
|
6
7
|
|
|
7
8
|
const FETCH_TIMEOUT_MS = Number.parseInt(process.env.CDP_TIMEOUT || '30000', 10);
|
|
8
9
|
const FETCH_RETRIES = Number.parseInt(process.env.CDP_DISCOVERY_RETRIES || '3', 10);
|
|
@@ -117,13 +118,14 @@ async function discoverTarget(port) {
|
|
|
117
118
|
try {
|
|
118
119
|
targets = await fetchJSON(listUrl);
|
|
119
120
|
} catch (e) {
|
|
120
|
-
throw
|
|
121
|
-
`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}
|
|
121
|
+
throw coded(
|
|
122
|
+
new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
|
|
123
|
+
BRIDGE_ERROR_CODES.METRO_UNREACHABLE,
|
|
122
124
|
);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
126
|
-
throw new Error(`No debug targets found at ${listUrl}`);
|
|
128
|
+
throw coded(new Error(`No debug targets found at ${listUrl}`), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// Filter to React Native / Hermes targets with a WebSocket URL
|
|
@@ -181,25 +183,25 @@ async function discoverTarget(port) {
|
|
|
181
183
|
const ambiguousList = androidFiltered
|
|
182
184
|
.map((t) => ` deviceName=${JSON.stringify(t.deviceName || '')} ws=${t.webSocketDebuggerUrl || ''}`)
|
|
183
185
|
.join('\n');
|
|
184
|
-
throw new Error(
|
|
186
|
+
throw coded(new Error(
|
|
185
187
|
`Pinned Android device is ambiguous: model '${androidTargetName}' matches ${androidFiltered.length} Metro targets.\n` +
|
|
186
188
|
` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
|
|
187
189
|
` Matching Metro targets:\n${ambiguousList}\n` +
|
|
188
190
|
` Set ANDROID_DEVICE to the exact Metro deviceName to disambiguate.`,
|
|
189
|
-
);
|
|
191
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
190
192
|
}
|
|
191
193
|
if (androidFiltered.length === 0) {
|
|
192
194
|
// Pinned device could not be matched — never silently pick another target.
|
|
193
195
|
const candidateList = targets
|
|
194
196
|
.map((t) => ` deviceName=${JSON.stringify(t.deviceName || '')} ws=${t.webSocketDebuggerUrl || ''}`)
|
|
195
197
|
.join('\n');
|
|
196
|
-
throw new Error(
|
|
198
|
+
throw coded(new Error(
|
|
197
199
|
`Pinned Android device did not match any Metro target.\n` +
|
|
198
200
|
` Requested --device (ADB_SERIAL): ${adbSerial || '(not set)'}\n` +
|
|
199
201
|
` Resolved model (ANDROID_TARGET_DEVICE_NAME): ${androidTargetName}\n` +
|
|
200
202
|
` ANDROID_DEVICE: ${androidDevice || '(not set)'}\n` +
|
|
201
203
|
` Metro /json/list candidates:\n${candidateList}`,
|
|
202
|
-
);
|
|
204
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
203
205
|
}
|
|
204
206
|
candidates = androidFiltered;
|
|
205
207
|
} else if (androidDevice) {
|
|
@@ -211,11 +213,11 @@ async function discoverTarget(port) {
|
|
|
211
213
|
const candidateList = targets
|
|
212
214
|
.map((t) => ` deviceName=${JSON.stringify(t.deviceName || '')} ws=${t.webSocketDebuggerUrl || ''}`)
|
|
213
215
|
.join('\n');
|
|
214
|
-
throw new Error(
|
|
216
|
+
throw coded(new Error(
|
|
215
217
|
`Pinned Android device (ANDROID_DEVICE='${androidDevice}') did not match any Metro target.\n` +
|
|
216
218
|
` ADB_SERIAL: ${adbSerial || '(not set)'}\n` +
|
|
217
219
|
` Metro /json/list candidates:\n${candidateList}`,
|
|
218
|
-
);
|
|
220
|
+
), BRIDGE_ERROR_CODES.NO_TARGET);
|
|
219
221
|
}
|
|
220
222
|
candidates = androidFiltered;
|
|
221
223
|
}
|
|
@@ -226,8 +228,9 @@ async function discoverTarget(port) {
|
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
if (candidates.length === 0) {
|
|
229
|
-
throw
|
|
230
|
-
`No suitable debug target found. Targets:\n${JSON.stringify(targets, null, 2)}
|
|
231
|
+
throw coded(
|
|
232
|
+
new Error(`No suitable debug target found. Targets:\n${JSON.stringify(targets, null, 2)}`),
|
|
233
|
+
BRIDGE_ERROR_CODES.NO_TARGET,
|
|
231
234
|
);
|
|
232
235
|
}
|
|
233
236
|
|
|
@@ -263,8 +266,9 @@ async function discoverAllTargets(port) {
|
|
|
263
266
|
try {
|
|
264
267
|
targets = await fetchJSON(listUrl);
|
|
265
268
|
} catch (e) {
|
|
266
|
-
throw
|
|
267
|
-
`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}
|
|
269
|
+
throw coded(
|
|
270
|
+
new Error(`Cannot reach Metro at ${listUrl}. Is Metro running?\n ${e.message}`),
|
|
271
|
+
BRIDGE_ERROR_CODES.METRO_UNREACHABLE,
|
|
268
272
|
);
|
|
269
273
|
}
|
|
270
274
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/* global globalThis */
|
|
4
4
|
|
|
5
|
+
const { BRIDGE_ERROR_CODES, coded } = require('./bridge-errors.cjs');
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Minimal CDP client using the built-in ws-like interface over raw WebSocket.
|
|
7
9
|
* Node 22+ has a built-in WebSocket; for older versions we use the ws package
|
|
@@ -32,7 +34,7 @@ function createWSClient(wsUrl, timeout) {
|
|
|
32
34
|
|
|
33
35
|
const timer = setTimeout(() => {
|
|
34
36
|
ws.close();
|
|
35
|
-
reject(new Error(`CDP connection timeout after ${timeout}ms`));
|
|
37
|
+
reject(coded(new Error(`CDP connection timeout after ${timeout}ms`), BRIDGE_ERROR_CODES.CDP_TIMEOUT));
|
|
36
38
|
}, timeout);
|
|
37
39
|
|
|
38
40
|
ws.onopen = () => {
|
|
@@ -45,8 +47,11 @@ function createWSClient(wsUrl, timeout) {
|
|
|
45
47
|
const timer = setTimeout(() => {
|
|
46
48
|
pending.delete(id);
|
|
47
49
|
rej(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
coded(
|
|
51
|
+
new Error(
|
|
52
|
+
`CDP message timeout after ${msgTimeout}ms for ${method}`,
|
|
53
|
+
),
|
|
54
|
+
BRIDGE_ERROR_CODES.CDP_TIMEOUT,
|
|
50
55
|
),
|
|
51
56
|
);
|
|
52
57
|
}, msgTimeout);
|
|
@@ -92,13 +97,13 @@ function createWSClient(wsUrl, timeout) {
|
|
|
92
97
|
|
|
93
98
|
ws.onerror = (err) => {
|
|
94
99
|
clearTimeout(timer);
|
|
95
|
-
reject(new Error(`WebSocket error: ${err.message || err}`));
|
|
100
|
+
reject(coded(new Error(`WebSocket error: ${err.message || err}`), BRIDGE_ERROR_CODES.WS_CLOSED));
|
|
96
101
|
};
|
|
97
102
|
|
|
98
103
|
ws.onclose = () => {
|
|
99
104
|
clearTimeout(timer);
|
|
100
105
|
for (const [, { reject: rej }] of pending) {
|
|
101
|
-
rej(new Error('WebSocket closed'));
|
|
106
|
+
rej(coded(new Error('WebSocket closed'), BRIDGE_ERROR_CODES.WS_CLOSED));
|
|
102
107
|
}
|
|
103
108
|
pending.clear();
|
|
104
109
|
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -86,6 +86,20 @@ start_console_forwarder() {
|
|
|
86
86
|
local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
|
|
87
87
|
[ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
|
|
88
88
|
{ [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
|
|
89
|
+
# Only one debugger can own a React Native page. Reap any forwarder already
|
|
90
|
+
# attached to this Metro port, even if it was launched from another checkout or
|
|
91
|
+
# package install path and therefore does not own this runtime dir's pid file.
|
|
92
|
+
while IFS= read -r line; do
|
|
93
|
+
local pid cmd
|
|
94
|
+
pid="${line%% *}"
|
|
95
|
+
cmd="${line#* }"
|
|
96
|
+
[ "$pid" != "$line" ] || continue
|
|
97
|
+
case "$cmd" in
|
|
98
|
+
*console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
|
|
99
|
+
kill "$pid" 2>/dev/null || true
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
done < <(ps -axo pid=,command= 2>/dev/null || true)
|
|
89
103
|
if [ -f "$fwd_pid_file" ]; then
|
|
90
104
|
local old_fwd
|
|
91
105
|
old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
|
|
@@ -120,10 +134,13 @@ set -a
|
|
|
120
134
|
[ ! -f .env.local ] || . ./.env.local
|
|
121
135
|
set +a
|
|
122
136
|
[ -n "\${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
|
|
137
|
+
# build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds; quick-launch
|
|
138
|
+
# bypasses it, so without this Sentry never initializes in the dev client.
|
|
139
|
+
[ -n "\${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="\${MM_SENTRY_DSN_DEV:-}"
|
|
123
140
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
124
141
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
125
142
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
126
|
-
|
|
143
|
+
yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
127
144
|
EOF
|
|
128
145
|
chmod +x "$runner"
|
|
129
146
|
}
|
|
@@ -203,6 +220,10 @@ if ! start_metro_tmux; then
|
|
|
203
220
|
# rejects. Default to the main dev client (what runway installs / launch targets)
|
|
204
221
|
# so already-installed slots work without re-syncing fixtures.
|
|
205
222
|
[ -n "${METAMASK_BUILD_TYPE:-}" ] || export METAMASK_BUILD_TYPE=main
|
|
223
|
+
# build.sh remaps MM_SENTRY_DSN from MM_SENTRY_DSN_DEV for dev builds (start:ios
|
|
224
|
+
# = build.sh ios main dev); quick-launch bypasses build.sh, so without this the
|
|
225
|
+
# dev client's MM_SENTRY_DSN stays empty and Sentry never initializes.
|
|
226
|
+
[ -n "${MM_SENTRY_DSN:-}" ] || export MM_SENTRY_DSN="${MM_SENTRY_DSN_DEV:-}"
|
|
206
227
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
207
228
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
208
229
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
File without changes
|