@deeeed/metamask-harness 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +24 -23
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/resolve-farmslot-ports-core.mjs +196 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +317 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +140 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/CLI-SPEC.md +26 -3
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +1 -1
- package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +5 -1
- package/src/adapters/core/surface.ts +15 -0
- package/src/adapters/extension/surface.ts +20 -3
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/runtime-decision.ts +8 -1
- package/src/adapters/mobile/surface.ts +16 -4
- package/src/adapters/resolve-farmslot-ports.ts +13 -0
- package/src/adapters/slot-ports.ts +18 -25
- package/src/adapters/surface.ts +35 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +149 -6
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +52 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 - 2026-07-05
|
|
4
|
+
|
|
5
|
+
Fresh-install hotfixes found in live validation of 0.5.0.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **First launch on a provisioned slot targets the slot's own simulator** — the mobile resolution ladder now reads the `runway-provision.json` baseline (simulator name/udid, watcher port, slot id) when `agentic-runtime.json` doesn't exist yet, and boots that simulator. Previously a provisioned-but-unprepared slot degraded to the simctl `booted` alias, missed the installed dev client, and guessed the wrong Metro port.
|
|
9
|
+
- **`call` works from published installs** — library actions load harness helpers through a dist-preferring bridge instead of importing `src/index.ts` (node refuses to type-strip `.ts` under `node_modules`). The bridge is deliberately narrow (paths + run-recording) to avoid an import cycle through `adapters.ts`.
|
|
10
|
+
- **deps-not-ready guidance matches the caller's state** — on a checkout without node_modules the taught command now leads with `yarn install --immutable` (yarn cannot run scripts at all on a bare checkout), and always ends with the launch re-run.
|
|
11
|
+
|
|
12
|
+
## Unreleased
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **resolve-farmslot-ports is TypeScript-first** — pool/context/formula port resolution lives in self-contained `adapters/shared/resolve-farmslot-ports-core.mjs` (works from runner and injected overlay copies); `src/adapters/resolve-farmslot-ports.ts` re-exports it for `slot-ports.ts`. `resolve-farmslot-ports.mjs` is the node leaf; `resolve-farmslot-ports.sh` remains a thin bash-compat wrapper for sourced callers.
|
|
16
|
+
|
|
17
|
+
## 0.5.0 - 2026-07-05
|
|
18
|
+
Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
|
|
19
|
+
### Added
|
|
20
|
+
- **`mm-harness provision runway`** — provisions a mobile slot with exactly: resolve the latest Runway (expo-dev-build) artifact, download it ONCE into a shared machine cache (`~/.cache/metamask-harness/runway/<runId>/`, sha256+size integrity, corrupt entries re-downloaded, never a slot failure), create the simulator if missing, install the dev client on it. Zero yarn/deps/Metro — those stay dispatch-time (lazy, presence-authoritative readiness unchanged). `install --runway` is the overlay-path equivalent (covered by its own contract test). Baseline recorded so doctor reports provisioned-but-deps-pending as a normal state.
|
|
21
|
+
- **Compiled `dist/` for published installs** — the npm tarball ships esbuild-compiled JS; `npm i -g` installs run the dist entry (~0.21s cold vs ~0.37s tsx). Source checkouts without `dist/` keep the tsx dev path; `bin/mm-harness` prefers dist when present. Packaging contract test (pack → global install → dist entry runs).
|
|
22
|
+
### Changed
|
|
23
|
+
- Runway provisioning callers (farm installer, mobile-farm `runway` prepare profile) delegate to the harness; the profile no longer runs a deps phase and a contract test enforces that.
|
|
24
|
+
|
|
3
25
|
## 0.4.0 - 2026-07-04
|
|
4
26
|
|
|
5
27
|
Minor release: the adapter-surface architecture plus the day's feature wave (0.3.6-0.3.9 shipped fixes as they were validated live; this cut marks the surface additions honestly).
|
|
@@ -34,7 +56,18 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
|
|
|
34
56
|
|
|
35
57
|
## Unreleased
|
|
36
58
|
|
|
59
|
+
### Added
|
|
60
|
+
- Add `mm-harness provision` for thin mobile Runway artifact provisioning with a shared integrity-checked cache and simulator install path.
|
|
61
|
+
- Add `mm-harness provision runway ios --resolve-only` to return Runway run/revision/artifact metadata without touching simulator, cache, download, install, or baseline state.
|
|
62
|
+
- **Dist publish path** — `npm run build` compiles `src/` to `dist/`; `npm pack` / global install runs the compiled CLI entry (`dist/mm-harness-cli.js`) while a source checkout without `dist/` keeps the tsx+src dev path. Contracts: `packaging-dist-install`, `packaging-dev-checkout`.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- **Leaf stderr hints use mm-harness verbs** — Metro/webpack compact-log prefixes, log-tail hints, and speed-ladder copy now point at `mm-harness logs`, `mm-harness doctor`, `mm-harness launch`, and `mm-harness install` instead of retired `recipe *` commands. Contract: `leaf-branding-retired`.
|
|
66
|
+
|
|
37
67
|
### Fixed
|
|
68
|
+
- `mm-harness provision runway ios` now routes through the same Runway provisioning surface as `install --runway`, so overlay dispatch accepts the compatibility command instead of rejecting it as an unsupported harness action.
|
|
69
|
+
- Runway provisioning now accepts farm context flags (`--slot`, `--watcher-port`, `--runtime-dir`), records slot/watcher/runtime-dir baselines correctly, ignores stale provision baselines unless the app is still installed on the simulator, and emits rerunnable `Next:` commands from the actual invocation.
|
|
70
|
+
- Runway iOS artifact extraction now handles the live nested zip layout and app bundle names other than `MetaMask.app` while keeping the cached app directory integrity hash/size contract.
|
|
38
71
|
- **Mobile preflight no longer re-runs `yarn setup` on every launch** — the deps readiness decision escalated an mtime hint to `stale` and ran the FULL `yarn setup` (submodules + inpage bridge + jetify + pods) inside the launch phase on every invocation in orchestrated (farmslot) contexts. `depsCheck` reports `stale` for a no-baseline checkout whenever a manifest is newer than the install markers; the orchestrator's git phase refreshes tracked-file mtimes on every sync while its deps phase leaves `node_modules` untouched, so "manifest newer than markers" is normal and not proof of drift — yet it duplicated the orchestrator's deps work and turned a fast preflight into 15+ minutes (the behavior that also looked like a hang before 0.3.8's streaming). **Presence is now authoritative:** without a recorded baseline an mtime-only `stale` is trusted as `current` (with a one-line stderr warning), so a churned-but-present checkout launches with zero `yarn` in both tiers. Genuine drift is still caught by the recorded-baseline fingerprint. `mm-harness launch` (quick tier / no `--build`) also dropped `preflightMode` before deciding, so the fast contract never took effect; it is threaded through now.
|
|
39
72
|
- **Fast tier never installs; genuine gaps teach the orchestrator** — in the fast tier (quick launch / orchestrated), deps that genuinely need work (absent markers, an absent required package, a fingerprint drift, or Metro unable to resolve a module) return a teaching block naming the orchestrator deps/prepare phase (plus the standalone `yarn setup:expo` / `launch --build` remedies) instead of an implicit setup — deps are the orchestrator's contract. The standalone/full tier still installs.
|
|
40
73
|
- **Deps baseline recorded the instant a setup leaf succeeds** — `prepareMobile` records the deps fingerprint baseline immediately after any successful `yarn-setup`, unconditionally (previously only a later `record: true` re-decide did, gated on the original decision being `install` and on `RECIPE_UP_INSTALL_ATTEMPTED`). Genuine-drift detection is fingerprint-based and self-sustaining after one good install.
|
|
@@ -108,6 +108,8 @@ copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join
|
|
|
108
108
|
copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
|
|
109
109
|
copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
|
|
110
110
|
copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
|
|
111
|
+
copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
|
|
112
|
+
copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
|
|
111
113
|
copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
|
|
112
114
|
copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
|
|
113
115
|
copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
@@ -97,7 +97,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
|
|
|
97
97
|
exit 1
|
|
98
98
|
}
|
|
99
99
|
else
|
|
100
|
-
echo "[refresh] activate-repo-node.sh missing from harness; run:
|
|
100
|
+
echo "[refresh] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
|
|
101
101
|
exit 1
|
|
102
102
|
fi
|
|
103
103
|
|
|
@@ -186,5 +186,5 @@ kill "$BUILD_PID" 2>/dev/null || true
|
|
|
186
186
|
wait "$BUILD_PID" 2>/dev/null || true
|
|
187
187
|
trap - EXIT
|
|
188
188
|
|
|
189
|
-
echo "[refresh] Build refreshed and frozen — run:
|
|
189
|
+
echo "[refresh] Build refreshed and frozen — run: mm-harness launch"
|
|
190
190
|
echo "[refresh] Log kept at: $LOG"
|
|
@@ -82,7 +82,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
|
|
|
82
82
|
exit 1
|
|
83
83
|
}
|
|
84
84
|
else
|
|
85
|
-
echo "[recipe-harness] activate-repo-node.sh missing from harness; run:
|
|
85
|
+
echo "[recipe-harness] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
|
|
86
86
|
exit 1
|
|
87
87
|
fi
|
|
88
88
|
if [ -z "$WATCHER_PORT" ]; then
|
|
@@ -233,7 +233,7 @@ for _lib in "$SCRIPT_DIR/lib" "$SCRIPT_DIR/../shared"; do
|
|
|
233
233
|
done
|
|
234
234
|
unset _lib
|
|
235
235
|
echo "[recipe-harness] Starting yarn start (watcher-port=${WATCHER_PORT:-default})"
|
|
236
|
-
echo "[recipe-harness] compact build view - full log: $watch_log (
|
|
236
|
+
echo "[recipe-harness] compact build view - full log: $watch_log (mm-harness logs --full)"
|
|
237
237
|
started_pid=""
|
|
238
238
|
# Launch the watcher detached writing to the log; the tmux window is a read-only tail viewer.
|
|
239
239
|
path_prefix="${REPO_NODE_PATH_PREFIX:-}"
|
package/adapters/manifest.json
CHANGED
|
@@ -308,12 +308,28 @@
|
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"id": "lib/resolve-farmslot-ports",
|
|
311
|
-
"entry": "adapters/shared/resolve-farmslot-ports.
|
|
312
|
-
"kind": "
|
|
311
|
+
"entry": "adapters/shared/resolve-farmslot-ports.mjs",
|
|
312
|
+
"kind": "node",
|
|
313
313
|
"purpose": "Resolve CDP + watcher ports from farmslot pool JSON by repo path, else 6660+N / 9010+N.",
|
|
314
|
-
"inputs": "resolve_extension_runtime_ports <repo>",
|
|
314
|
+
"inputs": "resolve-farmslot-ports.mjs resolve_extension_runtime_ports <repo>",
|
|
315
315
|
"outputs": "CDP_PORT / WATCHER_PORT / SLOT_ID lines on stdout"
|
|
316
316
|
},
|
|
317
|
+
{
|
|
318
|
+
"id": "lib/resolve-farmslot-ports-core",
|
|
319
|
+
"entry": "adapters/shared/resolve-farmslot-ports-core.mjs",
|
|
320
|
+
"kind": "lib",
|
|
321
|
+
"purpose": "Self-contained pool/context/formula port resolution imported by the node leaf and TS surface.",
|
|
322
|
+
"inputs": "ESM import from resolve-farmslot-ports.mjs or resolve-farmslot-ports.ts",
|
|
323
|
+
"outputs": "resolveFarmslotPortsByRepo and related functions"
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"id": "lib/resolve-farmslot-ports-sh",
|
|
327
|
+
"entry": "adapters/shared/resolve-farmslot-ports.sh",
|
|
328
|
+
"kind": "lib",
|
|
329
|
+
"purpose": "Bash-compat sourced wrapper over resolve-farmslot-ports.mjs for stop-metro and legacy callers.",
|
|
330
|
+
"inputs": "source + resolve_* / apply_resolved_* function calls",
|
|
331
|
+
"outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
|
|
332
|
+
},
|
|
317
333
|
{
|
|
318
334
|
"id": "lib/sync-wallet-fixture",
|
|
319
335
|
"entry": "adapters/shared/sync-wallet-fixture.sh",
|
|
@@ -102,7 +102,7 @@ CLEAR_SUFFIX=""
|
|
|
102
102
|
|
|
103
103
|
printf 'Starting Metro on port %s (workers=%s%s)\n' \
|
|
104
104
|
"$PORT" "$METRO_WORKERS" "${CLEAR:+, clear}" >&2
|
|
105
|
-
printf '(Full log: %s —
|
|
105
|
+
printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
|
|
106
106
|
|
|
107
107
|
# Metro runs detached, writing to the log. The tmux window is a read-only tail.
|
|
108
108
|
(
|
|
@@ -296,7 +296,7 @@ if (checked === 0) {
|
|
|
296
296
|
const parts = [];
|
|
297
297
|
if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
|
|
298
298
|
if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
|
|
299
|
-
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or
|
|
299
|
+
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
|
|
300
300
|
} else {
|
|
301
301
|
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
302
302
|
}
|
|
@@ -134,7 +134,7 @@ activate_repo_node() {
|
|
|
134
134
|
require_repo_node() {
|
|
135
135
|
local target="${1:-.}"
|
|
136
136
|
if ! command -v activate_repo_node >/dev/null 2>&1; then
|
|
137
|
-
echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (
|
|
137
|
+
echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (mm-harness install)" >&2
|
|
138
138
|
return 1
|
|
139
139
|
fi
|
|
140
140
|
activate_repo_node "$target" || {
|
|
@@ -67,7 +67,7 @@ recipe_instant_hint() {
|
|
|
67
67
|
local message="$1"
|
|
68
68
|
[ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && return 0
|
|
69
69
|
[ -t 2 ] || return 0
|
|
70
|
-
local prefix="${RECIPE_CLI_PREFIX:-
|
|
70
|
+
local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
|
|
71
71
|
printf '%s: %s\n' \
|
|
72
72
|
"$(recipe_color label "$prefix")" \
|
|
73
73
|
"$(recipe_color dim "$message")" >&2
|
|
@@ -92,7 +92,7 @@ recipe_done() {
|
|
|
92
92
|
recipe_failure_playbook() {
|
|
93
93
|
local verb="$1" reason="$2"
|
|
94
94
|
shift 2
|
|
95
|
-
local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-
|
|
95
|
+
local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
|
|
96
96
|
while [ "$#" -gt 0 ]; do
|
|
97
97
|
case "$1" in
|
|
98
98
|
--log) log_path="$2"; shift 2 ;;
|
|
@@ -106,31 +106,32 @@ recipe_failure_playbook() {
|
|
|
106
106
|
"$(recipe_color cmd "$verb")" \
|
|
107
107
|
"$(recipe_color err failed)" \
|
|
108
108
|
"$(recipe_color warn "$reason")" >&2
|
|
109
|
-
printf '%s\n' "$(recipe_color label next:)" >&2
|
|
109
|
+
printf '%s\n' "$(recipe_color label "next:")" >&2
|
|
110
110
|
case "$verb" in
|
|
111
111
|
rebuild|up|watch)
|
|
112
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
113
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
114
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
115
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
112
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
|
|
113
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
|
|
114
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
|
|
115
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness install")" >&2
|
|
116
116
|
;;
|
|
117
117
|
refresh)
|
|
118
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
119
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
120
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
118
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
|
|
119
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
|
|
120
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness launch --build")" >&2
|
|
121
121
|
;;
|
|
122
122
|
metro|ios|android|setup-wallet)
|
|
123
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
124
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
125
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
123
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
|
|
124
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
|
|
125
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
|
|
126
126
|
;;
|
|
127
127
|
*)
|
|
128
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
129
|
-
printf ' %s\n' "$(recipe_color cmd "
|
|
128
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
|
|
129
|
+
printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
|
|
130
130
|
;;
|
|
131
131
|
esac
|
|
132
132
|
[ -n "$log_path" ] && printf '%s\n' "$(recipe_color path "log: $log_path")" >&2
|
|
133
133
|
[ -n "$artifact" ] && printf '%s\n' "$(recipe_color path "artifact: $artifact")" >&2
|
|
134
|
+
return 0
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
recipe_check_harness_stale() {
|
|
@@ -152,12 +153,12 @@ recipe_check_harness_stale() {
|
|
|
152
153
|
|
|
153
154
|
recipe_warn_harness_stale() {
|
|
154
155
|
local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
|
|
155
|
-
local prefix="${RECIPE_CLI_PREFIX:-
|
|
156
|
+
local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
|
|
156
157
|
if recipe_check_harness_stale "$adapter" "$root" "$harness_root" "$runner_root"; then
|
|
157
158
|
printf '%s: %s - run: %s\n' \
|
|
158
159
|
"$(recipe_color label "$prefix")" \
|
|
159
160
|
"$(recipe_color warn 'harness scripts are stale')" \
|
|
160
|
-
"$(recipe_color cmd "
|
|
161
|
+
"$(recipe_color cmd "mm-harness install")" >&2
|
|
161
162
|
return 1
|
|
162
163
|
fi
|
|
163
164
|
return 0
|
|
@@ -190,7 +191,7 @@ recipe_try_farmslot_fixture_sync() {
|
|
|
190
191
|
recipe_sync_wallet_fixture() {
|
|
191
192
|
local root="$1" runtime_dir="$2" cdp_port="${3:-}" seed_script="${4:-}" farmslot_slot="${5:-}"
|
|
192
193
|
local canonical="$root/$runtime_dir/wallet-fixture.json"
|
|
193
|
-
local candidate resolved prefix="${RECIPE_CLI_PREFIX:-
|
|
194
|
+
local candidate resolved prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
|
|
194
195
|
|
|
195
196
|
mkdir -p "$root/$runtime_dir"
|
|
196
197
|
if [ -f "$canonical" ]; then
|
|
@@ -239,10 +240,10 @@ recipe_sync_wallet_fixture() {
|
|
|
239
240
|
recipe_speed_ladder() {
|
|
240
241
|
cat >&2 <<'EOF'
|
|
241
242
|
Speed ladder:
|
|
242
|
-
FAST (seconds):
|
|
243
|
-
SLOW (minutes):
|
|
244
|
-
INSPECT:
|
|
245
|
-
PROOF:
|
|
246
|
-
SYNC CONTEXT:
|
|
243
|
+
FAST (seconds): mm-harness launch (extension quick relaunch)
|
|
244
|
+
SLOW (minutes): mm-harness launch --build
|
|
245
|
+
INSPECT: mm-harness doctor | mm-harness logs | mm-harness logs --full | mm-harness debug
|
|
246
|
+
PROOF: mm-harness run <recipe.json>
|
|
247
|
+
SYNC CONTEXT: mm-harness fixtures sync
|
|
247
248
|
EOF
|
|
248
249
|
}
|
|
@@ -153,9 +153,9 @@ function readNewContent(file, offset) {
|
|
|
153
153
|
|
|
154
154
|
function renderHeader(label, logPath, title) {
|
|
155
155
|
return [
|
|
156
|
-
`${color('label', '
|
|
157
|
-
`${color('label', '
|
|
158
|
-
`${color('label', '
|
|
156
|
+
`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
|
|
157
|
+
`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
|
|
158
|
+
`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: mm-harness logs --full', { stream: process.stderr })}`,
|
|
159
159
|
'',
|
|
160
160
|
];
|
|
161
161
|
}
|
|
@@ -232,7 +232,7 @@ async function watchLog(options) {
|
|
|
232
232
|
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
233
233
|
if (mode !== 'quiet') {
|
|
234
234
|
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
235
|
-
process.stderr.write(`${color('label', '
|
|
235
|
+
process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
|
|
236
236
|
}
|
|
237
237
|
return 0;
|
|
238
238
|
}
|
|
@@ -221,7 +221,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
221
221
|
error: 'cdp-unreachable',
|
|
222
222
|
inspectUrl,
|
|
223
223
|
configureHint,
|
|
224
|
-
hint: `Start the extension runtime first (
|
|
224
|
+
hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
|
|
225
225
|
detail: error instanceof Error ? error.message : String(error),
|
|
226
226
|
};
|
|
227
227
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Farmslot pool + slot-suffix port resolution — pure JS for runner and overlay leaves.
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const coreDir = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
function pathDefault(key) {
|
|
11
|
+
for (const candidate of [
|
|
12
|
+
path.join(coreDir, 'path-defaults.json'),
|
|
13
|
+
path.join(coreDir, '../../adapters/shared/path-defaults.json'),
|
|
14
|
+
]) {
|
|
15
|
+
if (!fs.existsSync(candidate)) continue;
|
|
16
|
+
const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
|
|
17
|
+
if (value) return value;
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Missing path default: ${key}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recipeRuntimeDir() {
|
|
23
|
+
return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatKvLines(kv) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
if (kv.CDP_PORT !== undefined) lines.push(`CDP_PORT=${kv.CDP_PORT}`);
|
|
29
|
+
if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
|
|
30
|
+
if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
|
|
31
|
+
if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
|
|
32
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function realRepoPath(repo) {
|
|
36
|
+
if (!repo) return null;
|
|
37
|
+
try {
|
|
38
|
+
return fs.realpathSync(repo);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function inferSlotSuffix(repo) {
|
|
45
|
+
const base = path.basename(repo);
|
|
46
|
+
const m = /-(\d+)$/u.exec(base);
|
|
47
|
+
return m ? m[1] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function scoreSlot(slot, slotSuffix) {
|
|
51
|
+
const resources = slot.resources ?? {};
|
|
52
|
+
const cdp = resources.browser?.cdp_port;
|
|
53
|
+
const port = resources['dev-server']?.port;
|
|
54
|
+
const session = slot.session ?? '';
|
|
55
|
+
const slotId = slot.id ?? '';
|
|
56
|
+
let score = 0;
|
|
57
|
+
if (cdp !== undefined) score += 100;
|
|
58
|
+
if (port !== undefined) score += 10;
|
|
59
|
+
if (slotSuffix) {
|
|
60
|
+
if (session === `mme-${slotSuffix}`) score += 50;
|
|
61
|
+
if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
|
|
62
|
+
}
|
|
63
|
+
if (!slotId.toLowerCase().includes('demo')) score += 5;
|
|
64
|
+
return [score, cdp, port, slotId];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveFarmslotPortsByRepo(repo) {
|
|
68
|
+
const realRepo = realRepoPath(repo);
|
|
69
|
+
if (!realRepo) return null;
|
|
70
|
+
|
|
71
|
+
const slotSuffix = inferSlotSuffix(realRepo);
|
|
72
|
+
const roots = [];
|
|
73
|
+
if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
|
|
74
|
+
roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
|
|
75
|
+
|
|
76
|
+
for (const root of roots) {
|
|
77
|
+
if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
|
|
78
|
+
const poolDir = path.join(root, 'pool');
|
|
79
|
+
let best = null;
|
|
80
|
+
|
|
81
|
+
for (const name of fs.readdirSync(poolDir).sort()) {
|
|
82
|
+
if (!name.endsWith('.json') || name.includes('.bak.')) continue;
|
|
83
|
+
let data;
|
|
84
|
+
try {
|
|
85
|
+
data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const slot of data.slots ?? []) {
|
|
90
|
+
const slotRepo = slot.repo ?? '';
|
|
91
|
+
if (!slotRepo) continue;
|
|
92
|
+
let slotReal;
|
|
93
|
+
try {
|
|
94
|
+
slotReal = fs.realpathSync(slotRepo);
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (slotReal !== realRepo) continue;
|
|
99
|
+
const scored = scoreSlot(slot, slotSuffix);
|
|
100
|
+
if (!best || scored[0] > best[0]) best = scored;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (best) {
|
|
105
|
+
const [, cdp, port, slotId] = best;
|
|
106
|
+
return formatKvLines({
|
|
107
|
+
...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
|
|
108
|
+
...(port !== undefined ? { WATCHER_PORT: port } : {}),
|
|
109
|
+
...(slotId ? { SLOT_ID: slotId } : {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resolveDefaultExtensionPorts(repo) {
|
|
117
|
+
const n = inferSlotSuffix(repo);
|
|
118
|
+
if (!n) return null;
|
|
119
|
+
return formatKvLines({
|
|
120
|
+
CDP_PORT: 6660 + Number(n),
|
|
121
|
+
WATCHER_PORT: 9010 + Number(n),
|
|
122
|
+
SLOT_ID: `local-extension-${n}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveExtensionRuntimePorts(repo) {
|
|
127
|
+
return resolveFarmslotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveMobileSlotDefaults(repo) {
|
|
131
|
+
const n = inferSlotSuffix(repo);
|
|
132
|
+
if (!n) return null;
|
|
133
|
+
return formatKvLines({
|
|
134
|
+
WATCHER_PORT: 8060 + Number(n),
|
|
135
|
+
IOS_SIMULATOR: `mm-${n}`,
|
|
136
|
+
SLOT_ID: `local-mobile-${n}`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveMobileRuntimeContext(repo) {
|
|
141
|
+
const ctxPath = path.join(repo, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
142
|
+
if (fs.existsSync(ctxPath)) {
|
|
143
|
+
try {
|
|
144
|
+
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
145
|
+
if (c.simulator || c.metroPort) {
|
|
146
|
+
return formatKvLines({
|
|
147
|
+
...(c.metroPort !== undefined ? { WATCHER_PORT: c.metroPort } : {}),
|
|
148
|
+
...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
|
|
149
|
+
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
/* unreadable context falls through to the provision baseline */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return resolveMobileProvisionBaseline(repo);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
|
|
160
|
+
// provision baseline records the same authoritative simulator/port identity —
|
|
161
|
+
// without it, a fresh slot's first launch degrades to the simctl `booted`
|
|
162
|
+
// alias and misses the installed dev client entirely.
|
|
163
|
+
function resolveMobileProvisionBaseline(repo) {
|
|
164
|
+
const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
|
|
165
|
+
if (!fs.existsSync(basePath)) return null;
|
|
166
|
+
try {
|
|
167
|
+
const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
|
|
168
|
+
const simulator = c.simulator?.name || c.simulator?.udid;
|
|
169
|
+
if (!simulator && c.watcherPort === undefined) return null;
|
|
170
|
+
return formatKvLines({
|
|
171
|
+
...(c.watcherPort !== undefined ? { WATCHER_PORT: c.watcherPort } : {}),
|
|
172
|
+
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
173
|
+
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function resolveMobileRuntimePorts(repo) {
|
|
181
|
+
return (
|
|
182
|
+
resolveMobileRuntimeContext(repo)
|
|
183
|
+
?? resolveFarmslotPortsByRepo(repo)
|
|
184
|
+
?? resolveMobileSlotDefaults(repo)
|
|
185
|
+
?? ''
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const cliFns = {
|
|
190
|
+
resolve_farmslot_ports_by_repo: resolveFarmslotPortsByRepo,
|
|
191
|
+
resolve_default_extension_ports: resolveDefaultExtensionPorts,
|
|
192
|
+
resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
|
|
193
|
+
resolve_mobile_runtime_context: resolveMobileRuntimeContext,
|
|
194
|
+
resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
|
|
195
|
+
resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
|
|
196
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node leaf for port resolution — self-contained for runner and injected overlay copies.
|
|
3
|
+
|
|
4
|
+
import { cliFns } from './resolve-farmslot-ports-core.mjs';
|
|
5
|
+
|
|
6
|
+
const [, , fn, repo] = process.argv;
|
|
7
|
+
if (!fn || fn === '-h' || fn === '--help') {
|
|
8
|
+
process.stderr.write('Usage: resolve-farmslot-ports.mjs <fn> <repo>\n');
|
|
9
|
+
process.exit(fn ? 0 : 2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handler = cliFns[fn];
|
|
13
|
+
if (!handler) {
|
|
14
|
+
process.stderr.write(`resolve-farmslot-ports: unknown fn: ${fn}\n`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const out = handler(repo ?? '.');
|
|
19
|
+
if (!out?.trim()) process.exit(1);
|
|
20
|
+
process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);
|