@deeeed/metamask-harness 0.58.0 → 0.60.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 +21 -0
- package/README.md +48 -0
- package/adapters/extension/build-handoff.cjs +4 -0
- package/adapters/extension/inject.mjs +0 -1
- package/adapters/extension/live.sh +26 -25
- package/adapters/extension/pin-remote-flags.cjs +69 -30
- package/adapters/manifest.json +4 -4
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +69 -2
- package/adapters/shared/resolve-slot-ports-core.mjs +47 -19
- package/dist/adapters/extension/feature-flags.js +41 -0
- package/dist/adapters/extension/runtime-decision.js +2 -2
- package/dist/adapters/extension/runtime.js +4 -3
- package/dist/adapters/extension/verify.js +608 -0
- package/dist/adapters/mobile/feature-flags.js +127 -0
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters.js +6 -1
- package/dist/cli.js +1 -1
- package/dist/command-contract.js +6 -3
- package/dist/commands/doctor.js +9 -1
- package/dist/commands/fixtures.js +45 -3
- package/dist/commands/launch/extension.js +28 -6
- package/dist/commands/launch/index.js +128 -17
- package/dist/commands/launch/mobile.js +26 -2
- package/dist/commands/mobile-device-view.js +8 -1
- package/dist/commands/prepare.js +20 -3
- package/dist/commands/run-engine.js +10 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status-probe.js +14 -1
- package/dist/commands/status.js +13 -0
- package/dist/doctor.js +9 -1
- package/dist/feature-flags.js +193 -0
- package/dist/harness.js +3 -15
- package/dist/mm-harness-cli.js +15 -2
- package/library/actions/core/feature_flags/_unsupported.mjs +29 -0
- package/library/actions/core/feature_flags/clear.mjs +8 -0
- package/library/actions/core/feature_flags/read.mjs +8 -0
- package/library/actions/core/feature_flags/set.mjs +8 -0
- package/library/actions/extension/feature_flags/clear.mjs +48 -0
- package/library/actions/extension/feature_flags/read.mjs +27 -0
- package/library/actions/extension/feature_flags/set.mjs +36 -0
- package/library/actions/extension/feature_flags/state.mjs +129 -0
- package/library/actions/harness-exports.mjs +14 -0
- package/library/actions/mobile/feature_flags/clear.mjs +20 -0
- package/library/actions/mobile/feature_flags/controller.mjs +120 -0
- package/library/actions/mobile/feature_flags/read.mjs +20 -0
- package/library/actions/mobile/feature_flags/set.mjs +25 -0
- package/library/actions/mobile/wallet/reset.mjs +22 -1
- package/library/manifests/core.action-manifest.json +90 -0
- package/library/manifests/extension.action-manifest.json +70 -0
- package/library/manifests/mobile.action-manifest.json +90 -0
- package/package.json +1 -1
- package/adapters/extension/verify.sh +0 -572
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.60.0 - 2026-09-19
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Site: a Features page (`features.html`) catalogues every harness capability with its command or action and the clients it runs on, linked from the lobby and every page's nav. A feature PR adds or updates its row next to its changelog bullet.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Remote feature flags are a first-class capability with the same signature on both clients. `mm-harness launch --remote-flag KEY=VALUE[,KEY=VALUE]` pins a flag for the launch and `prepare` forwards it. Mobile applies it through the running `RemoteFeatureFlagController` and fails the launch when a key does not read back; the Extension writes it into the per-run dist snapshot manifest and then re-reads that manifest, so the launch reports the pin it proved rather than the one it requested. The wallet fixture takes an optional `remoteFeatureFlags` object: every launch on both clients makes the override set exactly that baseline merged with the `--remote-flag` map, so a plain launch keeps the fixture's variant and drops the previous run's (Mobile clears the controller's persisted overrides first; the Extension re-creates the snapshot unless its harness-owned record, `_harness.remoteFeatureFlags`, already equals the request, which keeps the build's own `.manifest-flags.json` flags from ever reading as a stale pin). `fixtures set` applies the block the same way and takes `--remote-flag` for a one-run override, so `prepare --remote-flag` keeps its pin through the fixtures step. The record is written on every snapshot (an empty map on a plain launch), a snapshot without it is re-created rather than trusted, and a reload in place (`reattach`, the watcher's sync, a recipe's runtime recovery) carries it across the rebuilt snapshot. `--watch` refuses `--remote-flag` because the watcher never re-creates the snapshot; a verified release artifact refuses any pin rather than testing the wrong arm; a Mobile launch that could not prove its override set reports `remoteFeatureFlags: null`. Three actions on every adapter: `metamask.feature_flags.set`, `.read`, and `.clear`. The Extension exposes no runtime override RPC (`setFlagOverride` is never registered in `metamask-controller.js` `getApi()`), so its `set` reports the relaunch that does pin the flag and exits non-zero rather than claiming a change it cannot make; core answers `supported: false`. `doctor` and `status` print a `feature flags:` line and carry the object in `--json`, distinguishing no runtime and an unreadable snapshot from a genuine zero. Mobile reads it on the live probe that already runs, so neither command spawns a second bridge evaluation. Value grammar is parsed in exactly one place (`src/feature-flags.ts`) and every leaf receives the resolved map as JSON: a bare word is a named A/B variant, `true`/`false` is a boolean, and a value opening with `{` or `[` is JSON.
|
|
14
|
+
|
|
15
|
+
## 0.59.0 - 2026-09-18
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Extension `verify` is runner-owned TypeScript: `adapters/extension/verify.sh` (572 lines, with node heredocs assembling `summary.json`, the fixture status and the CDP holder probe) is now `src/adapters/extension/verify.ts`, run in-process by `mm-harness verify --adapter extension`. Same flags, env, exit codes, check names and artifacts (`summary.json`, `artifact-manifest.json`, `logs/`, `runner-smoke/`); a live run against the same checkout produced a byte-identical normalised summary. Deliberate differences: `install` no longer copies a `scripts/verify.sh` into the checkout and dispatch no longer prefers one, so an older overlay cannot shadow the current runner's verify; the two shell-lib preflights (`harness-path.sh`, `json-field.sh`) are gone because the typed helpers cannot be missing; usage now names `mm-harness verify --adapter extension`; a `--target` that does not exist is a named exit-1 refusal instead of a bare `cd` failure; and `live.sh` reaches verify through the checkout's runner delegate, refusing early when that delegate is absent. Three more deliberate differences: `--artifacts-dir` is resolved to an absolute path once (the shell resolved a relative value against the cwd for its own files and against the target for the smoke run); `readiness.mjs` runs under the harness's own node instead of `PATH` `node`; and `live.sh`, having just installed the overlay, tells verify to skip the overlay re-ensure (`MM_HARNESS_SKIP_OVERLAY_ENSURE=1`) instead of reinstalling it between launch and the live probe.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Slot identity resolves against this machine's pool first. Every farm node keeps the same checkout paths, so a repo path matched a slot in every `pool/*.json` and the alphabetically first pool won: `doctor --fix` on macwork stamped `metamask-extension-2` as `macpro-mme-2` on CDP 7662 instead of `macwork-mmedev-2` on 6662. Pools whose `machine` or `host` is this host are consulted first; other machines' pools only when none of ours names the repo. A malformed pool file is skipped like the port resolver already did, and `*.bak` files never count.
|
|
24
|
+
- An invalid `RECIPE_RUNTIME_DIR` no longer crashes every command at startup with a stack trace. The extension runtime decision read it while loading; it reads it when a build-log check needs it, so `--help` still works and the command that needs the value reports it.
|
|
25
|
+
|
|
5
26
|
## 0.58.0 - 2026-09-18
|
|
6
27
|
|
|
7
28
|
### Changed
|
package/README.md
CHANGED
|
@@ -98,6 +98,54 @@ Extension `launch` keeps its incremental watcher running. Refresh the active
|
|
|
98
98
|
page after a successful rebuild; use `launch --build` only for an explicit
|
|
99
99
|
production-like LavaMoat rebuild.
|
|
100
100
|
|
|
101
|
+
### Remote feature flags
|
|
102
|
+
|
|
103
|
+
`--remote-flag KEY=VALUE[,KEY=VALUE]` pins a remote feature flag for the launch,
|
|
104
|
+
which is how a screen gated behind an A/B variant becomes reachable:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
mm-harness launch --remote-flag perpsTAT3938AbtestScreenVsBottomSheet=treatment
|
|
108
|
+
mm-harness launch ios --remote-flag perpsEnabled=true,perpsConfig='{"tier":2}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A bare word is a named variant (`treatment` becomes `{ "name": "treatment" }`),
|
|
112
|
+
`true`/`false` is a boolean, and a value opening with `{` or `[` is parsed as
|
|
113
|
+
JSON. The grammar is parsed once and every leaf receives the resolved map, so a
|
|
114
|
+
boolean stays a boolean rather than becoming the string `"true"`.
|
|
115
|
+
|
|
116
|
+
Every launch makes the app's override set exactly the wallet fixture's
|
|
117
|
+
`remoteFeatureFlags` object (the baseline every run shares) merged with the
|
|
118
|
+
`--remote-flag` map (a one-launch override), on both clients. A plain `launch`
|
|
119
|
+
therefore drops a previous run's `--remote-flag` variant and keeps the fixture's.
|
|
120
|
+
Mobile clears and re-applies the set through the running
|
|
121
|
+
`RemoteFeatureFlagController` and fails the launch if it does not read back. The
|
|
122
|
+
Extension writes it into the per-run dist snapshot manifest and re-reads the
|
|
123
|
+
harness-owned record there (`_harness.remoteFeatureFlags`, written on every
|
|
124
|
+
snapshot), so the launch reports the pin it proved; reuse of a running browser
|
|
125
|
+
is allowed only while that record already equals the request, a reload in
|
|
126
|
+
place carries the record across the rebuilt snapshot, and `--watch` (which
|
|
127
|
+
never re-creates the snapshot) refuses `--remote-flag`. A recipe's own recovery
|
|
128
|
+
relaunch carries the live pin rather than dropping it mid-run. A verified
|
|
129
|
+
release artifact is never rewritten, so a launch that would have to pin it
|
|
130
|
+
(`--remote-flag` or a fixture that declares flags) refuses instead of testing
|
|
131
|
+
the wrong arm.
|
|
132
|
+
|
|
133
|
+
Read or change flags on a running app with the actions. `fixtures set` follows
|
|
134
|
+
the same rule as launch: the fixture's block is the baseline and
|
|
135
|
+
`fixtures set --remote-flag KEY=VALUE` layers a one-run override on it, which is
|
|
136
|
+
how `prepare --remote-flag` keeps its pin through the fixtures step.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
mm-harness call metamask.feature_flags.read
|
|
140
|
+
mm-harness call metamask.feature_flags.set --arg 'flags={"perpsEnabled":true}' # mobile
|
|
141
|
+
mm-harness call metamask.feature_flags.clear # mobile
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`doctor` and `status` print a `feature flags:` line naming every active
|
|
145
|
+
override, so a slot never runs a proof under an unnamed variant. The line
|
|
146
|
+
distinguishes `(no runtime)` and an unreadable snapshot from a genuine
|
|
147
|
+
`0 override(s)`.
|
|
148
|
+
|
|
101
149
|
Log sources stay separate:
|
|
102
150
|
|
|
103
151
|
```bash
|
|
@@ -7,6 +7,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
7
7
|
const { connectBrowserViaCdp, evaluatePageViaCdp } = require('./lib/playwright-cdp.cjs');
|
|
8
8
|
const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = require('./lib/chrome-args.cjs');
|
|
9
9
|
const { stampRuntimeTitles } = require('./stamp-runtime-title.cjs');
|
|
10
|
+
const { carryHarnessPin } = require('./pin-remote-flags.cjs');
|
|
10
11
|
|
|
11
12
|
function acquireBuildHandoff(runtimeDir) {
|
|
12
13
|
const lock = path.join(runtimeDir, 'extension-build-handoff.lock');
|
|
@@ -142,6 +143,9 @@ async function handoffRuntimeBuild({ target, runtimeDist, dist, cdpPort }) {
|
|
|
142
143
|
const configured = spawnSync(process.execPath, [path.join(__dirname, 'configure-runtime-manifest.cjs'), '--target', target, '--manifest', path.join(runtimeDist, 'manifest.json')], { encoding: 'utf8' });
|
|
143
144
|
if (configured.status !== 0) throw new Error(`Extension runtime configuration failed: ${configured.stderr || configured.stdout}`);
|
|
144
145
|
stampRuntimeTitles({ target, runtimeDist });
|
|
146
|
+
// The rsync replaced the manifest with the build's own; the pin the
|
|
147
|
+
// running slot was launched under must survive a reload in place.
|
|
148
|
+
carryHarnessPin(oldManifest, path.join(runtimeDist, 'manifest.json'));
|
|
145
149
|
}
|
|
146
150
|
await setEnabled(true);
|
|
147
151
|
for (const entry of pages) {
|
|
@@ -114,7 +114,6 @@ copyFile(path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh'), path.jo
|
|
|
114
114
|
copyFile(path.join(runnerDir, 'adapters/extension/reattach.sh'), path.join(harnessDir, 'scripts/reattach.sh'));
|
|
115
115
|
copyFile(path.join(runnerDir, 'adapters/extension/readiness.mjs'), path.join(harnessDir, 'scripts/readiness.mjs'));
|
|
116
116
|
copyFile(path.join(runnerDir, 'adapters/extension/wallet-fixture-state.cjs'), path.join(harnessDir, 'scripts/wallet-fixture-state.cjs'));
|
|
117
|
-
copyFile(path.join(runnerDir, 'adapters/extension/verify.sh'), path.join(harnessDir, 'scripts/verify.sh'));
|
|
118
117
|
copyFile(path.join(runnerDir, 'adapters/extension/refresh-build.sh'), path.join(harnessDir, 'scripts/refresh-build.sh'));
|
|
119
118
|
copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join(harnessDir, 'scripts/reopen-browser.sh'));
|
|
120
119
|
copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# Purpose:
|
|
7
7
|
# Builds the per-run prepare command from the named feature scripts
|
|
8
8
|
# (watch, dist snapshot, fixture seed, detached chrome, CDP poll), then
|
|
9
|
-
# runs launch.sh and, unless --launch-only is set, verify
|
|
10
|
-
# timestamped artifact dir.
|
|
9
|
+
# runs launch.sh and, unless --launch-only is set, `mm-harness verify` under
|
|
10
|
+
# a timestamped artifact dir.
|
|
11
11
|
#
|
|
12
12
|
# Inputs (flags / env):
|
|
13
13
|
# --target <metamask-extension> (default $PWD)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# --launch-only (skip recipe verification; used by fixture application while
|
|
18
18
|
# its checkout lock is held)
|
|
19
19
|
# --dist-dir <rel> (default dist/chrome), --chrome-user-data-dir <dir>,
|
|
20
|
-
# --remote-
|
|
20
|
+
# --remote-flags-json <json-object> (pins manifest _flags into the
|
|
21
21
|
# ephemeral runtime-dist snapshot before launch; omit for default behavior)
|
|
22
22
|
# --out <recipes-dir>, --artifacts-dir <dir>
|
|
23
23
|
# env RECIPE_HARNESS_CHROME_BIN (validated executable; else Playwright
|
|
@@ -69,8 +69,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
69
69
|
--launch-only) RUN_VERIFY=false; shift ;;
|
|
70
70
|
--dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
|
|
71
71
|
--chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
|
|
72
|
-
--remote-
|
|
73
|
-
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file>|--prepare-cmd <cmd>] [--preserve-profile|--reset-profile] [--launch-only] [--dist-dir dist/chrome] [--remote-
|
|
72
|
+
--remote-flags-json) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
|
|
73
|
+
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--external-artifact --dist-dir <absolute-dir> --artifact-provenance <absolute-file>|--prepare-cmd <cmd>] [--preserve-profile|--reset-profile] [--launch-only] [--dist-dir dist/chrome] [--remote-flags-json '<normalised-json-map>'] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
74
74
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
75
75
|
esac
|
|
76
76
|
done
|
|
@@ -128,17 +128,6 @@ case "$CDP_PORT" in
|
|
|
128
128
|
*[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
|
|
129
129
|
esac
|
|
130
130
|
|
|
131
|
-
# verify.sh is a recipe-tree feature: co-located in installed copies, under
|
|
132
|
-
# adapters/extension in a repo checkout.
|
|
133
|
-
VERIFY_SH=""
|
|
134
|
-
for _v in "$SCRIPT_DIR/verify.sh" "$SCRIPT_DIR/../../adapters/extension/verify.sh"; do
|
|
135
|
-
[ -f "$_v" ] && { VERIFY_SH="$_v"; break; }
|
|
136
|
-
done
|
|
137
|
-
unset _v
|
|
138
|
-
if [ -z "$VERIFY_SH" ]; then
|
|
139
|
-
echo "mm-harness: extension verify.sh not found next to $SCRIPT_DIR; reinstall the runner." >&2
|
|
140
|
-
exit 1
|
|
141
|
-
fi
|
|
142
131
|
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
143
132
|
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
144
133
|
case "$RUNTIME_DIST_DIR" in
|
|
@@ -152,6 +141,12 @@ CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
|
|
|
152
141
|
# to defer the cache-clear DECISION to `runtime-decision` (single source) and to
|
|
153
142
|
# record the deps/cache baseline after a confirmed-good build.
|
|
154
143
|
RUNNER_BIN="$(harness_dir "$TARGET" extension)/runner/bin/mm-harness"
|
|
144
|
+
# Verify is runner-owned typed code reached through the runner delegate, not a
|
|
145
|
+
# shipped leaf this sequencer could resolve next to itself.
|
|
146
|
+
if $RUN_VERIFY && [ ! -x "$RUNNER_BIN" ]; then
|
|
147
|
+
echo "mm-harness: runner delegate missing at $RUNNER_BIN; run mm-harness install --adapter extension --target $TARGET." >&2
|
|
148
|
+
exit 1
|
|
149
|
+
fi
|
|
155
150
|
OUT="${OUT:-$(harness_root)/extension/runner/recipes}"
|
|
156
151
|
ARTIFACTS="${ARTIFACTS:-$(harness_dir "$TARGET" extension)/live/$(date -u +%Y%m%dT%H%M%SZ)}"
|
|
157
152
|
mkdir -p "$ARTIFACTS/logs"
|
|
@@ -332,14 +327,18 @@ NODE
|
|
|
332
327
|
if ! $EXTERNAL_ARTIFACT; then
|
|
333
328
|
prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
|
|
334
329
|
fi
|
|
335
|
-
#
|
|
336
|
-
# manifest._flags wins over the fetched ClientConfigApi value
|
|
337
|
-
#
|
|
338
|
-
|
|
330
|
+
# Remote feature-flag pinning: patch the ephemeral snapshot manifest so
|
|
331
|
+
# manifest._flags wins over the fetched ClientConfigApi value, and record the
|
|
332
|
+
# harness-owned pin on EVERY snapshot (an empty map without --remote-flags-json)
|
|
333
|
+
# so launch can tell a snapshot it wrote from one it never did. The snapshot is
|
|
334
|
+
# re-created each run, so no revert.
|
|
335
|
+
if ! $EXTERNAL_ARTIFACT; then
|
|
339
336
|
quoted_pin_flags="$(printf '%q' "$SCRIPT_DIR/pin-remote-flags.cjs")"
|
|
340
|
-
|
|
337
|
+
# The empty default is spelled literally: printf %q renders {} differently
|
|
338
|
+
# across bash versions, and the sequencer golden must not depend on that.
|
|
339
|
+
if [ -n "$REMOTE_FLAGS" ]; then quoted_remote_flags="$(printf '%q' "$REMOTE_FLAGS")"; else quoted_remote_flags="'{}'"; fi
|
|
341
340
|
quoted_runtime_manifest="$(printf '%q' "$RUNTIME_DIST_ABS/manifest.json")"
|
|
342
|
-
prepare_parts+=("node ${quoted_pin_flags} ${quoted_runtime_manifest} ${quoted_remote_flags}")
|
|
341
|
+
prepare_parts+=("node ${quoted_pin_flags} ${quoted_runtime_manifest} --flags-json ${quoted_remote_flags}")
|
|
343
342
|
REMOTE_FLAGS_APPLIED=true
|
|
344
343
|
fi
|
|
345
344
|
if [ -n "$WALLET_FIXTURE_ABS" ]; then
|
|
@@ -359,7 +358,7 @@ NODE
|
|
|
359
358
|
fi
|
|
360
359
|
|
|
361
360
|
if [ -n "$REMOTE_FLAGS" ] && [ "$REMOTE_FLAGS_APPLIED" != "true" ]; then
|
|
362
|
-
echo "[recipe-harness] WARN: --remote-
|
|
361
|
+
echo "[recipe-harness] WARN: --remote-flags-json '$REMOTE_FLAGS' had no effect: flag pinning only applies to the snapshot launch path (--launch-existing-dist/--start-watch/--build-lavamoat without a custom --prepare-cmd)." >&2
|
|
363
362
|
fi
|
|
364
363
|
|
|
365
364
|
echo "Extension live validation command:"
|
|
@@ -369,7 +368,7 @@ $BUILD_LAVAMOAT && display_args+=(--build-lavamoat)
|
|
|
369
368
|
$EXTERNAL_ARTIFACT && display_args+=(--external-artifact --dist-dir "$DIST_DIR" --artifact-provenance "$ARTIFACT_PROVENANCE")
|
|
370
369
|
$RESET_PROFILE && display_args+=(--reset-profile)
|
|
371
370
|
$RESET_PROFILE || display_args+=(--preserve-profile)
|
|
372
|
-
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-
|
|
371
|
+
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flags-json "$REMOTE_FLAGS")
|
|
373
372
|
[ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
|
|
374
373
|
printf ' '
|
|
375
374
|
printf '%q ' "${display_args[@]}"
|
|
@@ -390,7 +389,9 @@ verify_status=0
|
|
|
390
389
|
if [ "$launch_status" -eq 0 ] && $RUN_VERIFY; then
|
|
391
390
|
echo "[recipe-harness] phase 2/2: live verify (readiness + smoke recipe)" >&2
|
|
392
391
|
set +e
|
|
393
|
-
|
|
392
|
+
# The overlay was installed by this sequencer moments ago; verify must not
|
|
393
|
+
# reinstall it between launch and the live probe.
|
|
394
|
+
MM_HARNESS_SKIP_OVERLAY_ENSURE=1 "$RUNNER_BIN" verify --adapter extension --target "$TARGET" -- --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
|
|
394
395
|
verify_status=$?
|
|
395
396
|
set -e
|
|
396
397
|
elif [ "$launch_status" -ne 0 ]; then
|
|
@@ -1,45 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// pin-remote-flags.cjs <manifest.json> <
|
|
2
|
+
// pin-remote-flags.cjs <manifest.json> --flags-json '<normalised-map>'
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// `_flags.remoteFeatureFlags`. MetaMask resolves the active flags as
|
|
4
|
+
// Writes an already-normalised remote feature-flag map into an extension
|
|
5
|
+
// manifest's `_flags.remoteFeatureFlags`. MetaMask resolves the active flags as
|
|
6
6
|
// getRemoteFeatureFlags = merge({}, fetchedState, manifest._flags.remoteFeatureFlags)
|
|
7
|
-
// with the manifest winning, so this deterministically forces
|
|
8
|
-
// live browser regardless of the real ClientConfigApi response —
|
|
9
|
-
// overwrites any seeded controller state on refetch.
|
|
7
|
+
// with the manifest winning, so this deterministically forces the requested
|
|
8
|
+
// values in the live browser regardless of the real ClientConfigApi response —
|
|
9
|
+
// which otherwise overwrites any seeded controller state on refetch.
|
|
10
|
+
//
|
|
11
|
+
// This leaf does NOT parse the KEY=VALUE grammar. Value normalisation lives in
|
|
12
|
+
// exactly one place, src/feature-flags.ts, and every caller hands the result
|
|
13
|
+
// here as JSON. A second parser here silently disagreed with it: a spec of
|
|
14
|
+
// `flagA=true,flagB={"enabled":true}` used to pin the STRINGS
|
|
15
|
+
// {"flagA":{"name":"true"},"flagB":{"name":"{\"enabled\":true}"}}.
|
|
16
|
+
//
|
|
17
|
+
// The extension build can author `_flags.remoteFeatureFlags` itself (from
|
|
18
|
+
// .manifest-flags.json), so the pin merges over that map and ALSO records
|
|
19
|
+
// exactly what the harness pinned under `_harness.remoteFeatureFlags` — on
|
|
20
|
+
// every snapshot, an empty map included. Launch, fixtures, status and reuse
|
|
21
|
+
// compare against that record, so a build's own flags are reported as live
|
|
22
|
+
// state and never mistaken for a stale pin, a previous run's pin can never
|
|
23
|
+
// hide behind them, and a snapshot without the record is one this harness
|
|
24
|
+
// never wrote (pre-upgrade or foreign) and is re-created rather than trusted.
|
|
10
25
|
//
|
|
11
26
|
// Intended for the per-run runtime-dist SNAPSHOT (ephemeral, re-created from
|
|
12
27
|
// dist/chrome on every launch), so it never mutates the real dist/chrome build
|
|
13
|
-
// and needs no revert bookkeeping.
|
|
14
|
-
// (
|
|
28
|
+
// and needs no revert bookkeeping. Every path that rsyncs dist over the
|
|
29
|
+
// snapshot again (build-handoff.cjs: reattach, the watcher's sync monitor)
|
|
30
|
+
// carries the record across with carryHarnessPin().
|
|
15
31
|
'use strict';
|
|
16
32
|
|
|
17
33
|
const fs = require('node:fs');
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
if (!
|
|
21
|
-
|
|
35
|
+
function assertFlagMap(flags, where) {
|
|
36
|
+
if (!flags || typeof flags !== 'object' || Array.isArray(flags)) {
|
|
37
|
+
throw new Error(`${where} must be a JSON object of flag key to already-normalised value.`);
|
|
38
|
+
}
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
manifest._flags
|
|
32
|
-
const
|
|
41
|
+
/** Merge `flags` into the manifest's effective map and record it as the harness pin. */
|
|
42
|
+
function pinRemoteFlags(manifestPath, flags) {
|
|
43
|
+
assertFlagMap(flags, 'flags');
|
|
44
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
45
|
+
const keys = Object.keys(flags);
|
|
46
|
+
if (keys.length > 0) {
|
|
47
|
+
manifest._flags = manifest._flags || {};
|
|
48
|
+
const effective = manifest._flags.remoteFeatureFlags || (manifest._flags.remoteFeatureFlags = {});
|
|
49
|
+
for (const key of keys) effective[key] = flags[key];
|
|
50
|
+
}
|
|
51
|
+
manifest._harness = { ...(manifest._harness || {}), remoteFeatureFlags: flags };
|
|
52
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
53
|
+
return keys;
|
|
54
|
+
}
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Re-apply the pin a previous snapshot manifest recorded to a freshly synced
|
|
58
|
+
* one. A manifest without the record contributes an empty pin: the fresh
|
|
59
|
+
* snapshot then carries `_harness` so the next launch can trust it.
|
|
60
|
+
*/
|
|
61
|
+
function carryHarnessPin(previousManifest, manifestPath) {
|
|
62
|
+
const pinned = previousManifest && previousManifest._harness && previousManifest._harness.remoteFeatureFlags;
|
|
63
|
+
return pinRemoteFlags(manifestPath, pinned && typeof pinned === 'object' && !Array.isArray(pinned) ? pinned : {});
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
module.exports = { pinRemoteFlags, carryHarnessPin };
|
|
67
|
+
|
|
68
|
+
if (require.main === module) {
|
|
69
|
+
const [, , manifestPath, flagOption, flagsJson] = process.argv;
|
|
70
|
+
if (!manifestPath || flagOption !== '--flags-json' || !flagsJson) {
|
|
71
|
+
throw new Error("usage: pin-remote-flags.cjs <manifest.json> --flags-json '<json-object>'");
|
|
72
|
+
}
|
|
73
|
+
let flags;
|
|
74
|
+
try {
|
|
75
|
+
flags = JSON.parse(flagsJson);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`--flags-json is not valid JSON: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
assertFlagMap(flags, '--flags-json');
|
|
80
|
+
const keys = pinRemoteFlags(manifestPath, flags);
|
|
81
|
+
console.log(keys.length > 0
|
|
82
|
+
? `[flags] Pinned ${keys.join(', ')} into ${manifestPath}`
|
|
83
|
+
: `[flags] Recorded an empty remote feature-flag pin in ${manifestPath}`);
|
|
84
|
+
}
|
package/adapters/manifest.json
CHANGED
|
@@ -164,10 +164,10 @@
|
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"id": "extension/verify",
|
|
167
|
-
"entry": "adapters/extension/verify.
|
|
168
|
-
"kind": "
|
|
169
|
-
"purpose": "Verify the extension harness/runtime is present and healthy (no app launch).",
|
|
170
|
-
"inputs": "--target --out --cdp-port --static-only",
|
|
167
|
+
"entry": "src/adapters/extension/verify.ts",
|
|
168
|
+
"kind": "module",
|
|
169
|
+
"purpose": "Verify the extension harness/runtime is present and healthy (no app launch). Runner-owned: mm-harness verify runs it in-process.",
|
|
170
|
+
"inputs": "--target --out --cdp-port --static-only --artifacts-dir",
|
|
171
171
|
"outputs": "verify report + evidence; exit 0/1/2"
|
|
172
172
|
},
|
|
173
173
|
{
|
|
@@ -431,11 +431,27 @@ const COMMANDS = {
|
|
|
431
431
|
var keyringUnlocked = false;
|
|
432
432
|
try { account = globalThis.__AGENTIC__?.getSelectedAccount() || null; } catch(e) {}
|
|
433
433
|
try { keyringUnlocked = globalThis.Engine?.context?.KeyringController?.isUnlocked?.() === true; } catch(e) {}
|
|
434
|
+
// Remote feature-flag overrides ride along on this one evaluation: status
|
|
435
|
+
// and doctor must not spawn a second bridge probe to name the variant a
|
|
436
|
+
// slot is pinned to. null means the controller did not answer, which is
|
|
437
|
+
// not the same as "no overrides".
|
|
438
|
+
var featureFlags = null;
|
|
439
|
+
try {
|
|
440
|
+
var flagController = globalThis.Engine?.context?.RemoteFeatureFlagController;
|
|
441
|
+
var flagState = flagController && flagController.state;
|
|
442
|
+
if (flagState) {
|
|
443
|
+
featureFlags = {
|
|
444
|
+
effective: flagState.remoteFeatureFlags || {},
|
|
445
|
+
overrides: flagState.localOverrides || {},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
} catch(e) {}
|
|
434
449
|
return {
|
|
435
450
|
route: route,
|
|
436
451
|
account: account,
|
|
437
452
|
agenticPresent: agenticPresent,
|
|
438
453
|
keyringUnlocked: keyringUnlocked,
|
|
454
|
+
featureFlags: featureFlags,
|
|
439
455
|
};
|
|
440
456
|
})()`;
|
|
441
457
|
const snapshot = await cdpEval(client, expr);
|
|
@@ -47,6 +47,7 @@ export CDP_DISCOVERY_RETRIES="${CDP_DISCOVERY_RETRIES:-3}"
|
|
|
47
47
|
WALLET_SETUP_SETTLE_TIMEOUT="${WALLET_SETUP_SETTLE_TIMEOUT:-180}"
|
|
48
48
|
CDP="node $SCRIPTS/cdp-bridge.cjs"
|
|
49
49
|
FIXTURE_PATH=""
|
|
50
|
+
REMOTE_FEATURE_FLAGS=""
|
|
50
51
|
|
|
51
52
|
cdp_json_command() {
|
|
52
53
|
local mode="$1"
|
|
@@ -70,7 +71,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
70
71
|
--fixture)
|
|
71
72
|
[ $# -ge 2 ] || { echo "ERROR: --fixture requires a path argument"; exit 2; }
|
|
72
73
|
FIXTURE_PATH="$2"; shift 2 ;;
|
|
73
|
-
-
|
|
74
|
+
--remote-flags-json)
|
|
75
|
+
[ $# -ge 2 ] || { echo "ERROR: --remote-flags-json requires a JSON argument"; exit 2; }
|
|
76
|
+
REMOTE_FEATURE_FLAGS="$2"; shift 2 ;;
|
|
77
|
+
-h|--help) echo "Usage: setup-wallet.sh [--fixture path.json] [--remote-flags-json '<normalised-json-map>']"; exit 0 ;;
|
|
74
78
|
*) echo "Unknown arg: $1"; exit 2 ;;
|
|
75
79
|
esac
|
|
76
80
|
done
|
|
@@ -167,7 +171,17 @@ echo "Vault backup guard intent: $DISABLE_BACKUP"
|
|
|
167
171
|
wait_for_engine
|
|
168
172
|
|
|
169
173
|
# Read fixture JSON and escape it for safe embedding in a JS string literal.
|
|
170
|
-
|
|
174
|
+
# remoteFeatureFlags is a harness field: __AGENTIC__.setupWallet/applyWalletFixture
|
|
175
|
+
# know nothing about it, so strip it here and apply it separately below against
|
|
176
|
+
# RemoteFeatureFlagController once the wallet is up.
|
|
177
|
+
#
|
|
178
|
+
# The VALUES come from --remote-flags-json, already normalised by
|
|
179
|
+
# src/feature-flags.ts. This script must not read them from the fixture itself:
|
|
180
|
+
# a second reader here disagreed with that grammar (a bare "treatment" stayed a
|
|
181
|
+
# string instead of becoming { "name": "treatment" }).
|
|
182
|
+
[ -n "$REMOTE_FEATURE_FLAGS" ] || REMOTE_FEATURE_FLAGS="{}"
|
|
183
|
+
echo "$REMOTE_FEATURE_FLAGS" | jq empty 2>/dev/null || { echo "ERROR: --remote-flags-json is not valid JSON"; exit 2; }
|
|
184
|
+
FIXTURE_JSON=$(jq -c 'del(.remoteFeatureFlags)' "$FIXTURE_PATH")
|
|
171
185
|
ESCAPED_FIXTURE=$(node -p "JSON.stringify(JSON.stringify(JSON.parse(process.argv[1])))" "$FIXTURE_JSON")
|
|
172
186
|
EXPECTED_ADDRESSES=$(node - "$FIXTURE_PATH" <<'NODE'
|
|
173
187
|
const fs = require('fs');
|
|
@@ -440,6 +454,59 @@ case "$ROUTE_NAME" in
|
|
|
440
454
|
exit 1
|
|
441
455
|
;;
|
|
442
456
|
esac
|
|
457
|
+
# -- Apply the fixture's remote feature-flag overrides --
|
|
458
|
+
# setFlagOverride writes localOverrides and recomputes remoteFeatureFlags, so the
|
|
459
|
+
# override persists and wins over every later ClientConfigApi refetch. Applied on
|
|
460
|
+
# every `fixtures set`, like the rest of the fixture.
|
|
461
|
+
if [ "$REMOTE_FEATURE_FLAGS" = "{}" ]; then
|
|
462
|
+
# The fixture is authoritative: a fixture that declares no flags leaves the
|
|
463
|
+
# slot with no overrides, so one run's variant never leaks into the next.
|
|
464
|
+
CLEAR_RESULT=$(cdp_eval "(function(){ try { var ctx = globalThis.Engine && globalThis.Engine.context; var c = ctx && ctx.RemoteFeatureFlagController; if (!c || typeof c.clearAllFlagOverrides !== 'function') { return JSON.stringify({ok:true, proven:false, skipped:'controller-unavailable'}); } var before = Object.keys((c.state || {}).localOverrides || {}); c.clearAllFlagOverrides(); return JSON.stringify({ok:true, proven:true, cleared: before}); } catch (e) { return JSON.stringify({ok:false, error: e && (e.message || String(e))}); } })()")
|
|
465
|
+
if [ "$(echo "$CLEAR_RESULT" | jq -r '.ok // false')" != "true" ]; then
|
|
466
|
+
echo "ERROR: could not clear remote feature-flag overrides — $(echo "$CLEAR_RESULT" | jq -r '.error // "unknown error"')"
|
|
467
|
+
exit 1
|
|
468
|
+
fi
|
|
469
|
+
if [ "$(echo "$CLEAR_RESULT" | jq -r '.proven // false')" != "true" ]; then
|
|
470
|
+
# Not an error: an app without the controller has no overrides to carry. It
|
|
471
|
+
# is not proof of a clean slot either, so say which it is.
|
|
472
|
+
echo "WARN: remote feature-flag overrides were not cleared — $(echo "$CLEAR_RESULT" | jq -r '.skipped // "unknown reason"'); the app exposes no RemoteFeatureFlagController to prove the slot is clean."
|
|
473
|
+
else
|
|
474
|
+
CLEARED=$(echo "$CLEAR_RESULT" | jq -r '.cleared // [] | join(", ")')
|
|
475
|
+
[ -n "$CLEARED" ] && echo "Cleared remote feature-flag override(s): $CLEARED"
|
|
476
|
+
fi
|
|
477
|
+
else
|
|
478
|
+
ESCAPED_FLAGS=$(node -p "JSON.stringify(JSON.stringify(JSON.parse(process.argv[1])))" "$REMOTE_FEATURE_FLAGS")
|
|
479
|
+
echo "Applying remote feature-flag overrides: $REMOTE_FEATURE_FLAGS"
|
|
480
|
+
# Clear first: the controller persists overrides, so the set must become
|
|
481
|
+
# exactly the fixture's map, never the fixture's plus a previous run's.
|
|
482
|
+
FLAG_RESULT=$(cdp_eval "(function(){ try { var ctx = globalThis.Engine && globalThis.Engine.context; var c = ctx && ctx.RemoteFeatureFlagController; if (!c || typeof c.setFlagOverride !== 'function') { return JSON.stringify({ok:false, error:'RemoteFeatureFlagController.setFlagOverride is unavailable on the running app'}); } if (typeof c.clearAllFlagOverrides === 'function') c.clearAllFlagOverrides(); var requested = JSON.parse($ESCAPED_FLAGS); var applied = []; for (var k in requested) { if (Object.prototype.hasOwnProperty.call(requested, k)) { c.setFlagOverride(k, requested[k]); applied.push(k); } } var st = c.state || {}; return JSON.stringify({ok:true, applied: applied, effective: st.remoteFeatureFlags || {}, overrides: st.localOverrides || {}}); } catch (e) { return JSON.stringify({ok:false, error: e && (e.message || String(e))}); } })()")
|
|
483
|
+
FLAG_OK=$(echo "$FLAG_RESULT" | jq -r '.ok // false')
|
|
484
|
+
if [ "$FLAG_OK" != "true" ]; then
|
|
485
|
+
echo "ERROR: remote feature-flag overrides were not applied — $(echo "$FLAG_RESULT" | jq -r '.error // "unknown error"')"
|
|
486
|
+
exit 1
|
|
487
|
+
fi
|
|
488
|
+
# Read back: a flag nobody can observe is not applied.
|
|
489
|
+
FLAG_MISMATCH=$(node - "$REMOTE_FEATURE_FLAGS" "$FLAG_RESULT" <<'NODE'
|
|
490
|
+
const requested = JSON.parse(process.argv[2]);
|
|
491
|
+
const result = JSON.parse(process.argv[3]);
|
|
492
|
+
const effective = result.effective || {};
|
|
493
|
+
const overrides = result.overrides || {};
|
|
494
|
+
// A requested key that did not read back, or an override that survived the clear.
|
|
495
|
+
const mismatched = [
|
|
496
|
+
...Object.keys(requested).filter((key) => JSON.stringify(effective[key]) !== JSON.stringify(requested[key])),
|
|
497
|
+
...Object.keys(overrides).filter((key) => !(key in requested)),
|
|
498
|
+
];
|
|
499
|
+
process.stdout.write(mismatched.join(', '));
|
|
500
|
+
NODE
|
|
501
|
+
)
|
|
502
|
+
if [ -n "$FLAG_MISMATCH" ]; then
|
|
503
|
+
echo "ERROR: remote feature-flag override(s) did not read back: $FLAG_MISMATCH"
|
|
504
|
+
echo "$FLAG_RESULT" | jq .
|
|
505
|
+
exit 1
|
|
506
|
+
fi
|
|
507
|
+
echo "Remote feature flags applied: $(echo "$FLAG_RESULT" | jq -r '.applied | join(", ")')"
|
|
508
|
+
fi
|
|
509
|
+
|
|
443
510
|
echo ""
|
|
444
511
|
echo "=== Wallet Ready ==="
|
|
445
512
|
echo "Route: ${ROUTE_NAME:-unknown}"
|
|
@@ -73,6 +73,35 @@ function scoreSlot(slot, slotSuffix) {
|
|
|
73
73
|
return [score, cdp, port, slotId, simulator, adbSerial, androidDevice];
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// Pool files that describe this machine, then the rest. Every farm node keeps
|
|
77
|
+
// the same checkout paths, so a repo path alone matches a slot in every pool;
|
|
78
|
+
// the pool whose `machine`/`host` is this host is the one that owns the slot.
|
|
79
|
+
export function poolFilesForHost(poolDir, hostname = os.hostname()) {
|
|
80
|
+
// macOS may report a capitalised or FQDN hostname; pools carry lowercase names.
|
|
81
|
+
const canonical = (value) => String(value ?? '').toLowerCase().replace(/\.$/u, '');
|
|
82
|
+
const full = canonical(hostname);
|
|
83
|
+
const short = full.split('.')[0];
|
|
84
|
+
const names = (value) => {
|
|
85
|
+
const name = canonical(value);
|
|
86
|
+
return name ? [name, name.split('.')[0]] : [];
|
|
87
|
+
};
|
|
88
|
+
const local = [];
|
|
89
|
+
const other = [];
|
|
90
|
+
for (const name of fs.readdirSync(poolDir).sort()) {
|
|
91
|
+
// Backups (`*.json.bak.<ts>`, `*.bak`) never define a live slot.
|
|
92
|
+
if (!name.endsWith('.json') || name.includes('.bak')) continue;
|
|
93
|
+
let data;
|
|
94
|
+
try {
|
|
95
|
+
data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const isLocal = [...names(data.machine), ...names(data.host)].some((name) => name === full || name === short);
|
|
100
|
+
(isLocal ? local : other).push({ name, data });
|
|
101
|
+
}
|
|
102
|
+
return { local, other };
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
export function resolveSlotPortsByRepo(repo) {
|
|
77
106
|
const realRepo = realRepoPath(repo);
|
|
78
107
|
if (!realRepo) return null;
|
|
@@ -91,29 +120,28 @@ export function resolveSlotPortsByRepo(repo) {
|
|
|
91
120
|
for (const root of roots) {
|
|
92
121
|
if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
|
|
93
122
|
const poolDir = path.join(root, 'pool');
|
|
123
|
+
const pools = poolFilesForHost(poolDir);
|
|
94
124
|
let best = null;
|
|
95
125
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
slotReal
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
// This host's pools decide first; other machines' pools only when none of
|
|
127
|
+
// ours names the repo (single-node and demo setups).
|
|
128
|
+
for (const group of [pools.local, pools.other]) {
|
|
129
|
+
for (const { data } of group) {
|
|
130
|
+
for (const slot of data.slots ?? []) {
|
|
131
|
+
const slotRepo = slot.repo ?? '';
|
|
132
|
+
if (!slotRepo) continue;
|
|
133
|
+
let slotReal;
|
|
134
|
+
try {
|
|
135
|
+
slotReal = fs.realpathSync(slotRepo);
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (slotReal !== realRepo) continue;
|
|
140
|
+
const scored = scoreSlot(slot, slotSuffix);
|
|
141
|
+
if (!best || scored[0] > best[0]) best = scored;
|
|
112
142
|
}
|
|
113
|
-
if (slotReal !== realRepo) continue;
|
|
114
|
-
const scored = scoreSlot(slot, slotSuffix);
|
|
115
|
-
if (!best || scored[0] > best[0]) best = scored;
|
|
116
143
|
}
|
|
144
|
+
if (best) break;
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
if (best) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeFeatureFlagRecord } from "../../feature-flags.js";
|
|
4
|
+
import { recipeRuntimeDir } from "../../paths.js";
|
|
5
|
+
import { releaseArtifactState } from "./runtime-decision.js";
|
|
6
|
+
function extensionRuntimeManifestPath(target) {
|
|
7
|
+
return path.join(
|
|
8
|
+
target,
|
|
9
|
+
recipeRuntimeDir(),
|
|
10
|
+
process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist",
|
|
11
|
+
"manifest.json"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function readExtensionPinnedFlags(target) {
|
|
15
|
+
const manifestPath = extensionRuntimeManifestPath(target);
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = fs.readFileSync(manifestPath, "utf8");
|
|
19
|
+
} catch {
|
|
20
|
+
return { overrides: null, pinned: null };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const manifest = JSON.parse(raw);
|
|
24
|
+
return {
|
|
25
|
+
overrides: normalizeFeatureFlagRecord(manifest._flags?.remoteFeatureFlags, manifestPath),
|
|
26
|
+
pinned: manifest._harness?.remoteFeatureFlags !== void 0 ? normalizeFeatureFlagRecord(manifest._harness.remoteFeatureFlags, manifestPath) : releaseArtifactState(target).status === "valid" ? {} : null,
|
|
27
|
+
sourcePath: manifestPath
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
overrides: null,
|
|
32
|
+
pinned: null,
|
|
33
|
+
error: error instanceof Error ? error.message : String(error),
|
|
34
|
+
sourcePath: manifestPath
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
extensionRuntimeManifestPath,
|
|
40
|
+
readExtensionPinnedFlags
|
|
41
|
+
};
|