@deeeed/metamask-harness 0.59.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 +10 -0
- package/README.md +48 -0
- package/adapters/extension/build-handoff.cjs +4 -0
- package/adapters/extension/live.sh +15 -11
- package/adapters/extension/pin-remote-flags.cjs +69 -30
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +69 -2
- package/dist/adapters/extension/feature-flags.js +41 -0
- package/dist/adapters/extension/runtime-decision.js +1 -0
- package/dist/adapters/mobile/feature-flags.js +127 -0
- package/dist/adapters.js +6 -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/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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## 0.59.0 - 2026-09-18
|
|
6
16
|
|
|
7
17
|
### 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) {
|
|
@@ -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
|
|
@@ -327,14 +327,18 @@ NODE
|
|
|
327
327
|
if ! $EXTERNAL_ARTIFACT; then
|
|
328
328
|
prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
|
|
329
329
|
fi
|
|
330
|
-
#
|
|
331
|
-
# manifest._flags wins over the fetched ClientConfigApi value
|
|
332
|
-
#
|
|
333
|
-
|
|
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
|
|
334
336
|
quoted_pin_flags="$(printf '%q' "$SCRIPT_DIR/pin-remote-flags.cjs")"
|
|
335
|
-
|
|
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
|
|
336
340
|
quoted_runtime_manifest="$(printf '%q' "$RUNTIME_DIST_ABS/manifest.json")"
|
|
337
|
-
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}")
|
|
338
342
|
REMOTE_FLAGS_APPLIED=true
|
|
339
343
|
fi
|
|
340
344
|
if [ -n "$WALLET_FIXTURE_ABS" ]; then
|
|
@@ -354,7 +358,7 @@ NODE
|
|
|
354
358
|
fi
|
|
355
359
|
|
|
356
360
|
if [ -n "$REMOTE_FLAGS" ] && [ "$REMOTE_FLAGS_APPLIED" != "true" ]; then
|
|
357
|
-
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
|
|
358
362
|
fi
|
|
359
363
|
|
|
360
364
|
echo "Extension live validation command:"
|
|
@@ -364,7 +368,7 @@ $BUILD_LAVAMOAT && display_args+=(--build-lavamoat)
|
|
|
364
368
|
$EXTERNAL_ARTIFACT && display_args+=(--external-artifact --dist-dir "$DIST_DIR" --artifact-provenance "$ARTIFACT_PROVENANCE")
|
|
365
369
|
$RESET_PROFILE && display_args+=(--reset-profile)
|
|
366
370
|
$RESET_PROFILE || display_args+=(--preserve-profile)
|
|
367
|
-
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-
|
|
371
|
+
[ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flags-json "$REMOTE_FLAGS")
|
|
368
372
|
[ -n "$CHROME_USER_DATA_DIR" ] && display_args+=(--chrome-user-data-dir "$CHROME_USER_DATA_DIR")
|
|
369
373
|
printf ' '
|
|
370
374
|
printf '%q ' "${display_args[@]}"
|
|
@@ -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
|
+
}
|
|
@@ -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}"
|
|
@@ -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
|
+
};
|
|
@@ -313,6 +313,7 @@ function normalizedRuntimeManifest(file) {
|
|
|
313
313
|
delete manifest._flags.remoteFeatureFlags;
|
|
314
314
|
if (Object.keys(manifest._flags).length === 0) delete manifest._flags;
|
|
315
315
|
}
|
|
316
|
+
delete manifest._harness;
|
|
316
317
|
return JSON.stringify(manifest);
|
|
317
318
|
} catch {
|
|
318
319
|
return null;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runnerDir } from "../../paths.js";
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
6
|
+
function bridgeScriptPath() {
|
|
7
|
+
return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT ?? path.join(runnerDir, "adapters", "mobile", "bridge-runtime", "cdp-bridge.cjs");
|
|
8
|
+
}
|
|
9
|
+
const CONTROLLER_PREAMBLE = `
|
|
10
|
+
var ctx = globalThis.Engine && globalThis.Engine.context;
|
|
11
|
+
var c = ctx && ctx.RemoteFeatureFlagController;
|
|
12
|
+
if (!c) return JSON.stringify({ ok: false, error: 'Engine.context.RemoteFeatureFlagController is not available on the running app.' });
|
|
13
|
+
var snap = function () {
|
|
14
|
+
var st = c.state || {};
|
|
15
|
+
return { effective: st.remoteFeatureFlags || {}, overrides: st.localOverrides || {} };
|
|
16
|
+
};
|
|
17
|
+
`;
|
|
18
|
+
function evalExpression(body) {
|
|
19
|
+
return `(function(){ try {${CONTROLLER_PREAMBLE}${body} } catch (e) { return JSON.stringify({ ok: false, error: e && (e.message || String(e)) }); } })()`;
|
|
20
|
+
}
|
|
21
|
+
function runBridgeEval(target, expression, timeoutMs) {
|
|
22
|
+
const script = bridgeScriptPath();
|
|
23
|
+
if (!fs.existsSync(script)) return Promise.resolve(null);
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let settled = false;
|
|
26
|
+
let stdout = "";
|
|
27
|
+
const child = spawn(process.execPath, [script, "eval", expression], {
|
|
28
|
+
cwd: target,
|
|
29
|
+
env: { ...process.env, APP_ROOT: target, CDP_TIMEOUT: String(timeoutMs) },
|
|
30
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
31
|
+
});
|
|
32
|
+
const finish = (value) => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve(value);
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.kill("SIGTERM");
|
|
40
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 500);
|
|
41
|
+
child.once("close", () => clearTimeout(killTimer));
|
|
42
|
+
finish(null);
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
child.stdout.on("data", (chunk) => {
|
|
45
|
+
stdout += chunk;
|
|
46
|
+
});
|
|
47
|
+
child.on("close", (code) => {
|
|
48
|
+
if (code !== 0) return finish(null);
|
|
49
|
+
const trimmed = stdout.trim();
|
|
50
|
+
if (!trimmed) return finish(null);
|
|
51
|
+
try {
|
|
52
|
+
const first = JSON.parse(trimmed);
|
|
53
|
+
const parsed = typeof first === "string" ? JSON.parse(first) : first;
|
|
54
|
+
return finish(parsed);
|
|
55
|
+
} catch {
|
|
56
|
+
return finish(null);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.on("error", () => finish(null));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function readMobileFeatureFlags(target, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
63
|
+
const result = await runBridgeEval(
|
|
64
|
+
target,
|
|
65
|
+
evalExpression("var s = snap(); return JSON.stringify({ ok: true, effective: s.effective, overrides: s.overrides });"),
|
|
66
|
+
timeoutMs
|
|
67
|
+
);
|
|
68
|
+
if (!result || result.ok !== true) return null;
|
|
69
|
+
return { effective: result.effective ?? {}, overrides: result.overrides ?? {} };
|
|
70
|
+
}
|
|
71
|
+
async function pinMobileFeatureFlags(target, flags, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
72
|
+
const payload = JSON.stringify(JSON.stringify(flags));
|
|
73
|
+
const result = await runBridgeEval(
|
|
74
|
+
target,
|
|
75
|
+
evalExpression(`
|
|
76
|
+
if (typeof c.setFlagOverride !== 'function') {
|
|
77
|
+
return JSON.stringify({ ok: false, error: 'RemoteFeatureFlagController.setFlagOverride is unavailable in this build.' });
|
|
78
|
+
}
|
|
79
|
+
var requested = JSON.parse(${payload});
|
|
80
|
+
if (typeof c.clearAllFlagOverrides === 'function') c.clearAllFlagOverrides();
|
|
81
|
+
var applied = [];
|
|
82
|
+
for (var k in requested) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(requested, k)) { c.setFlagOverride(k, requested[k]); applied.push(k); }
|
|
84
|
+
}
|
|
85
|
+
var s = snap();
|
|
86
|
+
return JSON.stringify({ ok: true, applied: applied, effective: s.effective, overrides: s.overrides });
|
|
87
|
+
`),
|
|
88
|
+
timeoutMs
|
|
89
|
+
);
|
|
90
|
+
if (!result) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: "the mobile bridge did not answer, so no remote feature flag was applied.",
|
|
94
|
+
applied: [],
|
|
95
|
+
effective: {},
|
|
96
|
+
overrides: {},
|
|
97
|
+
mismatched: Object.keys(flags)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (result.ok !== true) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: result.error ?? "the mobile runtime refused the remote feature-flag override.",
|
|
104
|
+
applied: [],
|
|
105
|
+
effective: {},
|
|
106
|
+
overrides: {},
|
|
107
|
+
mismatched: Object.keys(flags)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const effective = result.effective ?? {};
|
|
111
|
+
const overrides = result.overrides ?? {};
|
|
112
|
+
const mismatched = [
|
|
113
|
+
...Object.keys(flags).filter((key) => JSON.stringify(effective[key]) !== JSON.stringify(flags[key])),
|
|
114
|
+
...Object.keys(overrides).filter((key) => !(key in flags))
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
ok: mismatched.length === 0,
|
|
118
|
+
applied: result.applied ?? [],
|
|
119
|
+
effective,
|
|
120
|
+
overrides,
|
|
121
|
+
mismatched
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
pinMobileFeatureFlags,
|
|
126
|
+
readMobileFeatureFlags
|
|
127
|
+
};
|
package/dist/adapters.js
CHANGED
|
@@ -105,6 +105,11 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
105
105
|
"metamask.wallet.list_accounts",
|
|
106
106
|
"metamask.wallet.read_state"
|
|
107
107
|
]);
|
|
108
|
+
const LIVE_ONLY_FEATURE_FLAG_ACTIONS = /* @__PURE__ */ new Set([
|
|
109
|
+
"metamask.feature_flags.set",
|
|
110
|
+
"metamask.feature_flags.read",
|
|
111
|
+
"metamask.feature_flags.clear"
|
|
112
|
+
]);
|
|
108
113
|
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
109
114
|
"ui.navigate",
|
|
110
115
|
"app.network_capture",
|
|
@@ -113,7 +118,7 @@ const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
113
118
|
"app.performance_assert"
|
|
114
119
|
]);
|
|
115
120
|
function requiresLiveAdapter(platform, action) {
|
|
116
|
-
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
121
|
+
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_FEATURE_FLAG_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
117
122
|
}
|
|
118
123
|
function liveRuntimeConfigured(platform, node) {
|
|
119
124
|
if (platform === "mobile" || platform === "core") return true;
|
package/dist/command-contract.js
CHANGED
|
@@ -154,7 +154,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
154
154
|
"--platform": value(["mobile", "extension", "core"]),
|
|
155
155
|
"--mobile-platform": value(["ios", "android"]),
|
|
156
156
|
"--clear-metro": bool(),
|
|
157
|
-
"--artifacts-dir": value()
|
|
157
|
+
"--artifacts-dir": value(),
|
|
158
|
+
"--remote-flag": value()
|
|
158
159
|
})
|
|
159
160
|
},
|
|
160
161
|
task: {
|
|
@@ -329,7 +330,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
329
330
|
"--sidepanel": bool(),
|
|
330
331
|
"--fullscreen": bool(),
|
|
331
332
|
"--url": value(),
|
|
332
|
-
"--heal": value(["off", "infra-only", "auto"])
|
|
333
|
+
"--heal": value(["off", "infra-only", "auto"]),
|
|
334
|
+
"--remote-flag": value()
|
|
333
335
|
}),
|
|
334
336
|
positionals: [{ label: "platform", choices: ["ios", "android"] }]
|
|
335
337
|
},
|
|
@@ -383,7 +385,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
383
385
|
"--cdp-port": value(),
|
|
384
386
|
"--extension-dir": value(),
|
|
385
387
|
"--extension-id-file": value(),
|
|
386
|
-
"--action-manifest": value()
|
|
388
|
+
"--action-manifest": value(),
|
|
389
|
+
"--remote-flag": value()
|
|
387
390
|
}),
|
|
388
391
|
positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
|
|
389
392
|
minimumPositionals: 0
|