@deeeed/metamask-harness 0.35.0 → 0.36.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 +15 -0
- package/README.md +9 -1
- package/adapters/extension/check-infura-readiness.cjs +102 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/live.sh +25 -10
- package/adapters/extension/start-watch.sh +15 -0
- package/adapters/extension/wallet-fixture-state.cjs +3 -1
- package/adapters/manifest.json +16 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
- package/adapters/mobile/reset-app-data.sh +154 -0
- package/dist/adapters/extension/product-config.js +29 -1
- package/dist/adapters.js +18 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +1 -1
- package/dist/commands/device-target.js +5 -0
- package/dist/commands/fixtures.js +106 -31
- package/dist/commands/launch/extension.js +39 -3
- package/dist/commands/launch/index.js +13 -0
- package/dist/mm-harness-cli.js +6 -3
- package/dist/recipe-security.js +3 -0
- package/docs/RECIPES.md +29 -0
- package/library/actions/extension/wallet/import.mjs +234 -0
- package/library/actions/extension/wallet/reset.mjs +98 -0
- package/library/actions/extension/wallet/state.mjs +1 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
- package/library/actions/mobile/analytics/set_consent.mjs +4 -112
- package/library/actions/mobile/platform/bridge.mjs +8 -0
- package/library/actions/mobile/wallet/import.mjs +259 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
- package/library/actions/mobile/wallet/reset.mjs +7 -0
- package/library/actions/shared/wallet/import-source.mjs +101 -0
- package/library/manifests/extension.action-manifest.json +112 -0
- package/library/manifests/mobile.action-manifest.json +108 -0
- package/library/recipes/wallet/import.recipe.json +83 -0
- package/library/recipes/wallet/reset-import.recipe.json +88 -0
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.36.0 - 2026-08-13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add a composable Mobile and Extension wallet import recipe that either verifies an existing fixture-backed profile or imports the primary mnemonic through visible onboarding UI, with secret-safe inputs, privacy-preserving defaults, and configurable MetaMetrics and Mobile interests.
|
|
10
|
+
- Add `fixtures reset` plus a shared Mobile/Extension reset-import recipe. Extension resets its owned browser profile; Mobile clears only the selected app's data while preserving its installed build. The visible recipe separately proves the real user reset and import flow.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Restart a harness-owned Extension watcher when its effective Infura credential differs from the current product configuration, and preflight compiled Ethereum and Linea credentials before Chrome starts, preventing a forced-red validation from poisoning later launches or opening a native HTTP authentication dialog.
|
|
15
|
+
- Make normal Extension launch and runtime recovery use the product's `yarn start` development build, while keeping production-like LavaMoat builds behind explicit `launch --build`.
|
|
16
|
+
- Require wallet profile reuse to match the requested account, wait for Mobile onboarding to reach a terminal route, and resolve the exact Mobile reset device and prewarm its bundle before relaunching after app-data reset.
|
|
17
|
+
- Refuse ambiguous installed Mobile app variants during reset, prefer complete environment wallet credentials without parsing a stale fixture, and reject bad Extension RPC configuration before replacing the loaded runtime snapshot.
|
|
18
|
+
- Validate recovery credentials, fixture structure, and platform-specific onboarding choices before destructive resets; prove Mobile reset by the visible import control; honor persisted MetaMetrics consent through Security settings; and keep file-backed wallet secrets out of bridge errors.
|
|
19
|
+
|
|
5
20
|
## 0.35.0 - 2026-08-12
|
|
6
21
|
|
|
7
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -88,10 +88,14 @@ mm-harness stop
|
|
|
88
88
|
mm-harness logs
|
|
89
89
|
mm-harness debug
|
|
90
90
|
mm-harness fixtures set
|
|
91
|
+
|
|
92
|
+
# Delete the current wallet, then reapply the canonical fixture from clean state.
|
|
93
|
+
mm-harness fixtures reset
|
|
91
94
|
```
|
|
92
95
|
|
|
93
96
|
Extension `launch` keeps its incremental watcher running. Refresh the active
|
|
94
|
-
page after a successful rebuild; use `launch --build` only for
|
|
97
|
+
page after a successful rebuild; use `launch --build` only for an explicit
|
|
98
|
+
production-like LavaMoat rebuild.
|
|
95
99
|
|
|
96
100
|
Log sources stay separate:
|
|
97
101
|
|
|
@@ -115,6 +119,10 @@ mm-harness call metamask.wallet.ensure_unlocked
|
|
|
115
119
|
mm-harness run --list
|
|
116
120
|
mm-harness run perps.clean-market-testnet --describe
|
|
117
121
|
mm-harness run wallet.smoke --describe
|
|
122
|
+
mm-harness run wallet.import method=auto
|
|
123
|
+
|
|
124
|
+
# Use visible client UI to delete and import the wallet again.
|
|
125
|
+
mm-harness run wallet.reset-import
|
|
118
126
|
mm-harness run path/to/recipe.json market=ETH --plan
|
|
119
127
|
mm-harness run path/to/recipe.json
|
|
120
128
|
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const args = {};
|
|
9
|
+
for (let index = 2; index < process.argv.length; index += 1) {
|
|
10
|
+
const key = process.argv[index];
|
|
11
|
+
if (key === '--help' || key === '-h') {
|
|
12
|
+
process.stdout.write('Usage: check-infura-readiness.cjs --target <metamask-extension> --runtime-dist <dist>\n');
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
if (!key.startsWith('--') || index + 1 >= process.argv.length) {
|
|
16
|
+
throw new Error(`invalid argument: ${key}`);
|
|
17
|
+
}
|
|
18
|
+
args[key.slice(2)] = process.argv[++index];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const key of ['target', 'runtime-dist']) {
|
|
22
|
+
if (!args[key]) throw new Error(`--${key} is required`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const target = path.resolve(args.target);
|
|
26
|
+
const runtimeDist = path.resolve(args['runtime-dist']);
|
|
27
|
+
const config = require('./lib/product-config.cjs');
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
|
|
31
|
+
if (resolution.kind !== 'configured') {
|
|
32
|
+
fail('the effective Extension product configuration has no usable Infura project ID');
|
|
33
|
+
}
|
|
34
|
+
if (!compiledScriptsContain(runtimeDist, resolution.value)) {
|
|
35
|
+
fail('the compiled Extension scripts do not match the effective Infura project ID');
|
|
36
|
+
}
|
|
37
|
+
for (const host of ['mainnet.infura.io', 'linea-mainnet.infura.io']) {
|
|
38
|
+
const response = await rpcProbe(host, resolution.value);
|
|
39
|
+
if (response.statusCode < 200 || response.statusCode >= 300 || response.jsonRpcResult !== true) {
|
|
40
|
+
fail(`${host} rejected the effective Infura project ID (HTTP ${response.statusCode})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write('Infura readiness preflight passed for Ethereum and Linea.\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compiledScriptsContain(root, value) {
|
|
47
|
+
const pending = [root];
|
|
48
|
+
while (pending.length > 0) {
|
|
49
|
+
const current = pending.pop();
|
|
50
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
51
|
+
const file = path.join(current, entry.name);
|
|
52
|
+
if (entry.isDirectory()) pending.push(file);
|
|
53
|
+
else if (entry.isFile() && entry.name.endsWith('.js') && fs.readFileSync(file, 'utf8').includes(value)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rpcProbe(host, projectId) {
|
|
62
|
+
const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] });
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const request = https.request({
|
|
65
|
+
hostname: host,
|
|
66
|
+
path: `/v3/${encodeURIComponent(projectId)}`,
|
|
67
|
+
method: 'POST',
|
|
68
|
+
timeout: 10_000,
|
|
69
|
+
headers: {
|
|
70
|
+
'content-type': 'application/json',
|
|
71
|
+
'content-length': Buffer.byteLength(body),
|
|
72
|
+
},
|
|
73
|
+
}, (response) => {
|
|
74
|
+
let text = '';
|
|
75
|
+
response.setEncoding('utf8');
|
|
76
|
+
response.on('data', (chunk) => { text += chunk; });
|
|
77
|
+
response.on('end', () => {
|
|
78
|
+
let jsonRpcResult = false;
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(text);
|
|
81
|
+
jsonRpcResult = typeof parsed.result === 'string' && parsed.result.startsWith('0x');
|
|
82
|
+
} catch {
|
|
83
|
+
jsonRpcResult = false;
|
|
84
|
+
}
|
|
85
|
+
resolve({ statusCode: response.statusCode || 0, jsonRpcResult });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
request.on('timeout', () => request.destroy(new Error(`${host} timed out`)));
|
|
89
|
+
request.on('error', reject);
|
|
90
|
+
request.end(body);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function fail(detail) {
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`EVM_RPC_UNREACHABLE: Infura readiness preflight failed before browser launch: ${detail}.\n` +
|
|
97
|
+
`Next: stop the stale watcher, confirm ${path.join(target, '.metamaskrc')} has a real INFURA_PROJECT_ID, then rerun mm-harness launch --verify --target ${JSON.stringify(target)}.\n`,
|
|
98
|
+
);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
@@ -102,6 +102,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/build-lavamoat.sh'), path.join
|
|
|
102
102
|
copyFile(path.join(runnerDir, 'adapters/extension/launch-webpack.cjs'), path.join(harnessDir, 'scripts/launch-webpack.cjs'));
|
|
103
103
|
copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.join(harnessDir, 'scripts/sync-webpack-dist.cjs'));
|
|
104
104
|
copyFile(path.join(runnerDir, 'adapters/extension/configure-runtime-manifest.cjs'), path.join(harnessDir, 'scripts/configure-runtime-manifest.cjs'));
|
|
105
|
+
copyFile(path.join(runnerDir, 'adapters/extension/check-infura-readiness.cjs'), path.join(harnessDir, 'scripts/check-infura-readiness.cjs'));
|
|
105
106
|
copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
|
|
106
107
|
copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
|
|
107
108
|
copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
|
|
@@ -6,13 +6,16 @@
|
|
|
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 verify.sh under a
|
|
9
|
+
# runs launch.sh and, unless --launch-only is set, verify.sh under a
|
|
10
|
+
# timestamped artifact dir.
|
|
10
11
|
#
|
|
11
12
|
# Inputs (flags / env):
|
|
12
13
|
# --target <metamask-extension> (default $PWD)
|
|
13
14
|
# --cdp-port <port> (optional; inferred from checkout context/pool when omitted)
|
|
14
15
|
# --launch-existing-dist | --start-watch | --build-lavamoat | --prepare-cmd <cmd>
|
|
15
16
|
# (env RECIPE_HARNESS_EXTENSION_LAUNCH_CMD)
|
|
17
|
+
# --launch-only (skip recipe verification; used by fixture application while
|
|
18
|
+
# its checkout lock is held)
|
|
16
19
|
# --dist-dir <rel> (default dist/chrome), --chrome-user-data-dir <dir>,
|
|
17
20
|
# --remote-flag <KEY=VARIANT[,KEY=VARIANT...]> (pins manifest _flags into the
|
|
18
21
|
# ephemeral runtime-dist snapshot before launch; omit for default behavior)
|
|
@@ -44,6 +47,7 @@ DIST_DIR="dist/chrome"
|
|
|
44
47
|
CHROME_USER_DATA_DIR="${CHROME_USER_DATA_DIR:-}"
|
|
45
48
|
REMOTE_FLAGS=""
|
|
46
49
|
REMOTE_FLAGS_APPLIED=false
|
|
50
|
+
RUN_VERIFY=true
|
|
47
51
|
while [ "$#" -gt 0 ]; do
|
|
48
52
|
case "$1" in
|
|
49
53
|
--target) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
|
|
@@ -55,10 +59,11 @@ while [ "$#" -gt 0 ]; do
|
|
|
55
59
|
--start-url) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; START_URL="$2"; shift 2 ;;
|
|
56
60
|
--start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
57
61
|
--build-lavamoat) BUILD_LAVAMOAT=true; LAUNCH_EXISTING_DIST=true; shift ;;
|
|
62
|
+
--launch-only) RUN_VERIFY=false; shift ;;
|
|
58
63
|
--dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
|
|
59
64
|
--chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
|
|
60
65
|
--remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
|
|
61
|
-
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd <cmd>] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
66
|
+
-h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] [--cdp-port <port>] [--launch-existing-dist|--start-watch|--build-lavamoat|--prepare-cmd <cmd>] [--launch-only] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
|
|
62
67
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
63
68
|
esac
|
|
64
69
|
done
|
|
@@ -177,6 +182,7 @@ NODE
|
|
|
177
182
|
quoted_build_lavamoat="$(printf '%q' "$SCRIPT_DIR/build-lavamoat.sh")"
|
|
178
183
|
quoted_snapshot_dist="$(printf '%q' "$SCRIPT_DIR/snapshot-dist.sh")"
|
|
179
184
|
quoted_configure_manifest="$(printf '%q' "$SCRIPT_DIR/configure-runtime-manifest.cjs")"
|
|
185
|
+
quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
|
|
180
186
|
quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
|
|
181
187
|
quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
|
|
182
188
|
quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
|
|
@@ -257,9 +263,10 @@ NODE
|
|
|
257
263
|
if $BUILD_LAVAMOAT; then
|
|
258
264
|
prepare_parts+=("bash ${quoted_build_lavamoat} --target ${quoted_target} --runtime-dir ${quoted_runtime_dir}")
|
|
259
265
|
fi
|
|
260
|
-
prepare_parts+=("node ${
|
|
266
|
+
prepare_parts+=("node ${quoted_check_infura} --target ${quoted_target} --runtime-dist ${quoted_dist}")
|
|
261
267
|
prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
|
|
262
268
|
prepare_parts+=("node ${quoted_configure_manifest} --target ${quoted_target} --manifest ${quoted_runtime_dist}/manifest.json")
|
|
269
|
+
prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --reset-profile 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
|
|
263
270
|
prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
|
|
264
271
|
# Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
|
|
265
272
|
# manifest._flags wins over the fetched ClientConfigApi value. No-op unless
|
|
@@ -301,7 +308,7 @@ printf ' '
|
|
|
301
308
|
printf '%q ' "${display_args[@]}"
|
|
302
309
|
printf '\n'
|
|
303
310
|
echo "Launch artifacts: $ARTIFACTS/launch"
|
|
304
|
-
echo "Verify artifacts: $ARTIFACTS/verify"
|
|
311
|
+
$RUN_VERIFY && echo "Verify artifacts: $ARTIFACTS/verify"
|
|
305
312
|
|
|
306
313
|
launch_args=(--target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/launch")
|
|
307
314
|
[ -n "$PREPARE_CMD" ] && launch_args+=(--prepare-cmd "$PREPARE_CMD")
|
|
@@ -312,18 +319,21 @@ set +e
|
|
|
312
319
|
launch_status=$?
|
|
313
320
|
set -e
|
|
314
321
|
|
|
315
|
-
verify_status=
|
|
316
|
-
if [ "$launch_status" -eq 0 ]; then
|
|
322
|
+
verify_status=0
|
|
323
|
+
if [ "$launch_status" -eq 0 ] && $RUN_VERIFY; then
|
|
317
324
|
echo "[recipe-harness] phase 2/2: live verify (readiness + smoke recipe)" >&2
|
|
318
325
|
set +e
|
|
319
326
|
"$VERIFY_SH" --target "$TARGET" --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
|
|
320
327
|
verify_status=$?
|
|
321
328
|
set -e
|
|
322
|
-
|
|
329
|
+
elif [ "$launch_status" -ne 0 ]; then
|
|
330
|
+
verify_status=1
|
|
323
331
|
echo "Skipping Extension live verify because launch failed; see $ARTIFACTS/launch/summary.json" >&2
|
|
332
|
+
else
|
|
333
|
+
echo "Skipping Extension live verify for launch-only fixture application." >&2
|
|
324
334
|
fi
|
|
325
335
|
|
|
326
|
-
TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" node <<'NODE'
|
|
336
|
+
TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" RUN_VERIFY="$RUN_VERIFY" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" BUILD_LAVAMOAT="$BUILD_LAVAMOAT" node <<'NODE'
|
|
327
337
|
const fs = require('fs');
|
|
328
338
|
const path = require('path');
|
|
329
339
|
const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
|
|
@@ -331,6 +341,7 @@ const launchSummary = path.join(artifacts, 'launch', 'summary.json');
|
|
|
331
341
|
const verifySummary = path.join(artifacts, 'verify', 'summary.json');
|
|
332
342
|
const launchStatus = Number(process.env.LAUNCH_STATUS);
|
|
333
343
|
const verifyStatus = Number(process.env.VERIFY_STATUS);
|
|
344
|
+
const runVerify = process.env.RUN_VERIFY === 'true';
|
|
334
345
|
fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
335
346
|
adapter: 'extension',
|
|
336
347
|
action: 'live',
|
|
@@ -341,9 +352,13 @@ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
|
341
352
|
startWatch: process.env.START_WATCH === 'true',
|
|
342
353
|
buildLavaMoat: process.env.BUILD_LAVAMOAT === 'true',
|
|
343
354
|
launch: { exitCode: launchStatus, summaryPath: fs.existsSync(launchSummary) ? launchSummary : null },
|
|
344
|
-
verify:
|
|
355
|
+
verify: runVerify
|
|
356
|
+
? { status: verifyStatus === 0 ? 'pass' : 'fail', exitCode: verifyStatus, summaryPath: fs.existsSync(verifySummary) ? verifySummary : null }
|
|
357
|
+
: { status: 'skipped', exitCode: null, summaryPath: null },
|
|
345
358
|
easyCommand: `mm-harness launch --verify --target <repo>`,
|
|
346
|
-
note:
|
|
359
|
+
note: runVerify
|
|
360
|
+
? 'Runs launch then live verify so a developer can validate browser startup, CDP readiness, recipe bridge, screenshots/fallback classification, and sample recipes from one runner-owned command.'
|
|
361
|
+
: 'Runs launch validation only for fixture application while the checkout lock is held.',
|
|
347
362
|
generatedAt: new Date().toISOString(),
|
|
348
363
|
}, null, 2)}\n`);
|
|
349
364
|
NODE
|
|
@@ -142,6 +142,21 @@ trap finish EXIT
|
|
|
142
142
|
|
|
143
143
|
mkdir -p "$RUNTIME_DIR"
|
|
144
144
|
|
|
145
|
+
PRODUCT_CONFIG_MODULE="$SCRIPT_DIR/lib/product-config.cjs" \
|
|
146
|
+
node - "$PWD" "$RUNTIME_DIR/extension-product-config.sha256" <<'NODE'
|
|
147
|
+
const crypto = require('node:crypto');
|
|
148
|
+
const fs = require('node:fs');
|
|
149
|
+
const config = require(process.env.PRODUCT_CONFIG_MODULE);
|
|
150
|
+
const [target, destination] = process.argv.slice(2);
|
|
151
|
+
const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
|
|
152
|
+
if (resolution.kind !== 'configured') {
|
|
153
|
+
fs.rmSync(destination, { force: true });
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
const fingerprint = crypto.createHash('sha256').update(resolution.value).digest('hex');
|
|
157
|
+
fs.writeFileSync(destination, `${fingerprint}\n`);
|
|
158
|
+
NODE
|
|
159
|
+
|
|
145
160
|
watcher_pids() {
|
|
146
161
|
ps -axo pid=,command= | while read -r current_pid command; do
|
|
147
162
|
[ -n "${current_pid:-}" ] || continue
|
|
@@ -658,7 +658,9 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
658
658
|
}
|
|
659
659
|
|
|
660
660
|
for (const candidate of candidates) {
|
|
661
|
-
const page =
|
|
661
|
+
const page = context.pages().find(
|
|
662
|
+
(candidatePage) => extensionIdFromUrl(candidatePage.url()) === candidate.id,
|
|
663
|
+
) || await context.newPage();
|
|
662
664
|
try {
|
|
663
665
|
await page.goto(`chrome-extension://${candidate.id}/home.html`, {
|
|
664
666
|
waitUntil: 'domcontentloaded',
|
package/adapters/manifest.json
CHANGED
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"inputs": "--platform ios|android --target --port; env MOBILE_BUNDLE_PREWARM, MOBILE_BUNDLE_PREWARM_TIMEOUT",
|
|
59
59
|
"outputs": "progress on stderr; exit 0 bundle ready / 1 timeout / 2 bad args"
|
|
60
60
|
},
|
|
61
|
+
{
|
|
62
|
+
"id": "mobile/reset-app-data",
|
|
63
|
+
"entry": "adapters/mobile/reset-app-data.sh",
|
|
64
|
+
"kind": "bash",
|
|
65
|
+
"purpose": "Reset exactly one installed MetaMask Mobile app while preserving its install for fixture reapplication.",
|
|
66
|
+
"inputs": "--platform ios|android --target --simulator --adb-serial; env IOS_BUNDLE_ID, ANDROID_PACKAGE_ID",
|
|
67
|
+
"outputs": "selected app data cleared and app restored on iOS / cleared on Android; exit 0/1/2"
|
|
68
|
+
},
|
|
61
69
|
{
|
|
62
70
|
"id": "mobile/open-device",
|
|
63
71
|
"entry": "adapters/mobile/open-device.sh",
|
|
@@ -162,6 +170,14 @@
|
|
|
162
170
|
"inputs": "--target --manifest",
|
|
163
171
|
"outputs": "runtime manifest _flags.testing.infuraProjectId; exit 0/1"
|
|
164
172
|
},
|
|
173
|
+
{
|
|
174
|
+
"id": "extension/check-infura-readiness",
|
|
175
|
+
"entry": "adapters/extension/check-infura-readiness.cjs",
|
|
176
|
+
"kind": "node",
|
|
177
|
+
"purpose": "Reject compiled Infura credentials that are stale or unreachable on Ethereum or Linea before Chromium starts.",
|
|
178
|
+
"inputs": "--target --runtime-dist",
|
|
179
|
+
"outputs": "readiness result without credential values; exit 0/1/2"
|
|
180
|
+
},
|
|
165
181
|
{
|
|
166
182
|
"id": "extension/stamp-runtime-title",
|
|
167
183
|
"entry": "adapters/extension/stamp-runtime-title.cjs",
|
|
@@ -84,6 +84,65 @@ const NESTED_ROUTE_PARENTS = {
|
|
|
84
84
|
DeveloperOptions: 'Settings', NotificationsSettings: 'Settings',
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
async function setInput(client, testId, value, { deviceName } = {}, redact = false) {
|
|
88
|
+
if (!testId) {
|
|
89
|
+
throw new Error('Usage: set-input <testId> <value>');
|
|
90
|
+
}
|
|
91
|
+
const expr = `(function() {
|
|
92
|
+
if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
|
|
93
|
+
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
94
|
+
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
95
|
+
var renderers = hook.renderers;
|
|
96
|
+
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
97
|
+
var getFiberRoots = hook.getFiberRoots;
|
|
98
|
+
function findByTestId(fiber) {
|
|
99
|
+
if (!fiber) return null;
|
|
100
|
+
var props = fiber.memoizedProps;
|
|
101
|
+
if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
|
|
102
|
+
return findByTestId(fiber.child) || findByTestId(fiber.sibling);
|
|
103
|
+
}
|
|
104
|
+
for (var [id] of renderers) {
|
|
105
|
+
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
106
|
+
if (!roots) continue;
|
|
107
|
+
var result = null;
|
|
108
|
+
roots.forEach(function(r) {
|
|
109
|
+
if (result) return;
|
|
110
|
+
var fiber = findByTestId(r.current);
|
|
111
|
+
if (!fiber) return;
|
|
112
|
+
var cur = fiber;
|
|
113
|
+
while (cur) {
|
|
114
|
+
if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
|
|
115
|
+
cur.memoizedProps.onChangeText(${JSON.stringify(value)});
|
|
116
|
+
result = { ok: true, testId: ${JSON.stringify(testId)} };
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
cur = cur.return || null;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
if (result) return result;
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
|
|
125
|
+
})()`;
|
|
126
|
+
let result;
|
|
127
|
+
try {
|
|
128
|
+
result = await cdpEval(client, expr);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (redact) {
|
|
131
|
+
throw new Error(`Secret input could not be applied for testID ${testId}.`);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
if (redact && result?.ok === false) {
|
|
136
|
+
result = { ok: false, error: 'Secret input could not be applied.' };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...result,
|
|
140
|
+
testId,
|
|
141
|
+
value: redact ? '<redacted>' : value,
|
|
142
|
+
deviceName,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
87
146
|
const COMMANDS = {
|
|
88
147
|
async navigate(client, args, { deviceName, platform } = {}) {
|
|
89
148
|
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
@@ -608,50 +667,21 @@ const COMMANDS = {
|
|
|
608
667
|
};
|
|
609
668
|
},
|
|
610
669
|
|
|
611
|
-
async 'set-input'(client, args,
|
|
670
|
+
async 'set-input'(client, args, context = {}) {
|
|
671
|
+
return setInput(client, args[0], args.slice(1).join(' '), context);
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
async 'set-input-file'(client, args, context = {}) {
|
|
612
675
|
const testId = args[0];
|
|
613
|
-
const
|
|
614
|
-
if (!testId) {
|
|
615
|
-
throw new Error('Usage: set-input <testId> <value>');
|
|
676
|
+
const file = args[1];
|
|
677
|
+
if (!testId || !file) {
|
|
678
|
+
throw new Error('Usage: set-input-file <testId> <value-file>');
|
|
616
679
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
var renderers = hook.renderers;
|
|
623
|
-
if (!renderers) return { ok: false, error: 'No renderers' };
|
|
624
|
-
var getFiberRoots = hook.getFiberRoots;
|
|
625
|
-
function findByTestId(fiber) {
|
|
626
|
-
if (!fiber) return null;
|
|
627
|
-
var props = fiber.memoizedProps;
|
|
628
|
-
if (props && props.testID === ${JSON.stringify(testId)}) return fiber;
|
|
629
|
-
return findByTestId(fiber.child) || findByTestId(fiber.sibling);
|
|
630
|
-
}
|
|
631
|
-
for (var [id] of renderers) {
|
|
632
|
-
var roots = getFiberRoots ? getFiberRoots(id) : undefined;
|
|
633
|
-
if (!roots) continue;
|
|
634
|
-
var result = null;
|
|
635
|
-
roots.forEach(function(r) {
|
|
636
|
-
if (result) return;
|
|
637
|
-
var fiber = findByTestId(r.current);
|
|
638
|
-
if (!fiber) return;
|
|
639
|
-
var cur = fiber;
|
|
640
|
-
while (cur) {
|
|
641
|
-
if (cur.memoizedProps && typeof cur.memoizedProps.onChangeText === 'function') {
|
|
642
|
-
cur.memoizedProps.onChangeText(${JSON.stringify(value)});
|
|
643
|
-
result = { ok: true, testId: ${JSON.stringify(testId)}, value: ${JSON.stringify(value)} };
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
cur = cur.return || null;
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
if (result) return result;
|
|
650
|
-
}
|
|
651
|
-
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onChangeText' };
|
|
652
|
-
})()`;
|
|
653
|
-
const result = await cdpEval(client, expr);
|
|
654
|
-
return { ...result, testId, value, deviceName };
|
|
680
|
+
const stat = fs.lstatSync(file);
|
|
681
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
682
|
+
throw new Error('set-input-file requires a regular value file.');
|
|
683
|
+
}
|
|
684
|
+
return setInput(client, testId, fs.readFileSync(file, 'utf8'), context, true);
|
|
655
685
|
},
|
|
656
686
|
|
|
657
687
|
async 'sentry-debug'(client, args, { deviceName } = {}) {
|
|
@@ -977,6 +1007,7 @@ Commands:
|
|
|
977
1007
|
Scroll a ScrollView/FlatList
|
|
978
1008
|
measure-scroll-transition <json> Scroll and measure a visible target using one CDP session
|
|
979
1009
|
set-input <testId> <value> Set text input value by testID (calls onChangeText)
|
|
1010
|
+
set-input-file <testId> <value-file> Set text input from a regular file and redact the value
|
|
980
1011
|
sentry-debug [enable|disable] Patch Sentry to log errors to console with [SENTRY-DEBUG] prefix
|
|
981
1012
|
unlock <password> Unlock wallet (inject password + press login button via fiber tree)
|
|
982
1013
|
profiler-start Start Hermes sampling profiler
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Reset one installed MetaMask Mobile app without rebuilding it.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PLATFORM="${PLATFORM:-ios}"
|
|
6
|
+
TARGET="$PWD"
|
|
7
|
+
SIMULATOR="${SIM_UDID:-${IOS_SIMULATOR:-booted}}"
|
|
8
|
+
ADB_SERIAL_ARG="${ADB_SERIAL:-${ANDROID_SERIAL:-}}"
|
|
9
|
+
ADB_BIN="${MM_HARNESS_ADB_PATH:-}"
|
|
10
|
+
MM_RESET_TEMP_ROOT=""
|
|
11
|
+
MM_RESET_STAGING_DIR=""
|
|
12
|
+
|
|
13
|
+
while [ "$#" -gt 0 ]; do
|
|
14
|
+
case "$1" in
|
|
15
|
+
--platform) PLATFORM="$2"; shift 2 ;;
|
|
16
|
+
--target) TARGET="$2"; shift 2 ;;
|
|
17
|
+
--simulator) SIMULATOR="$2"; shift 2 ;;
|
|
18
|
+
--adb-serial) ADB_SERIAL_ARG="$2"; shift 2 ;;
|
|
19
|
+
-h|--help)
|
|
20
|
+
printf 'Usage: reset-app-data.sh --platform ios|android [--target <dir>] [--simulator <udid|name>] [--adb-serial <serial>]\n'
|
|
21
|
+
exit 0
|
|
22
|
+
;;
|
|
23
|
+
*) printf 'reset-app-data: unknown arg: %s\n' "$1" >&2; exit 2 ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
TARGET="$(cd "$TARGET" && pwd -P)"
|
|
28
|
+
|
|
29
|
+
reset_ios() {
|
|
30
|
+
local bundle_id="" app_source="" candidate="" staged_app=""
|
|
31
|
+
local temp_root="${TMPDIR:-/tmp}" staging_dir=""
|
|
32
|
+
local -a candidates installed_ids installed_paths
|
|
33
|
+
|
|
34
|
+
temp_root="${temp_root%/}"
|
|
35
|
+
[ -n "$temp_root" ] && [ "$temp_root" != "/" ] && [ -d "$temp_root" ] && [ ! -L "$temp_root" ] || {
|
|
36
|
+
printf 'reset-app-data: unsafe temporary directory: %s\n' "$temp_root" >&2
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
if [ -n "${IOS_BUNDLE_ID:-}" ]; then
|
|
40
|
+
candidates=("$IOS_BUNDLE_ID")
|
|
41
|
+
else
|
|
42
|
+
candidates=("io.metamask.MetaMask" "io.metamask" "io.metamask.MetaMask-Flask" "io.metamask.qa")
|
|
43
|
+
fi
|
|
44
|
+
for candidate in "${candidates[@]}"; do
|
|
45
|
+
if app_source="$(xcrun simctl get_app_container "$SIMULATOR" "$candidate" app 2>/dev/null)"; then
|
|
46
|
+
installed_ids+=("$candidate")
|
|
47
|
+
installed_paths+=("$app_source")
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
if [ "${#installed_ids[@]}" -gt 1 ]; then
|
|
51
|
+
printf 'reset-app-data: multiple MetaMask iOS variants are installed on simulator %s: %s.\n' "$SIMULATOR" "${installed_ids[*]}" >&2
|
|
52
|
+
printf 'Next: set IOS_BUNDLE_ID to the exact variant and retry.\n' >&2
|
|
53
|
+
return 1
|
|
54
|
+
fi
|
|
55
|
+
bundle_id="${installed_ids[0]:-}"
|
|
56
|
+
app_source="${installed_paths[0]:-}"
|
|
57
|
+
[ -n "$bundle_id" ] && [ -n "$app_source" ] || {
|
|
58
|
+
printf 'reset-app-data: no installed MetaMask app was found on iOS simulator %s.\n' "$SIMULATOR" >&2
|
|
59
|
+
printf "Next: mm-harness launch --adapter mobile --platform ios --target '%s'\n" "$TARGET" >&2
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
case "$app_source" in
|
|
63
|
+
*/Library/Developer/CoreSimulator/Devices/*/data/Containers/Bundle/Application/*/*.app) ;;
|
|
64
|
+
*) printf 'reset-app-data: refusing unexpected iOS app path: %s\n' "$app_source" >&2; return 1 ;;
|
|
65
|
+
esac
|
|
66
|
+
[ -d "$app_source" ] && [ ! -L "$app_source" ] || {
|
|
67
|
+
printf 'reset-app-data: iOS app is not a real directory: %s\n' "$app_source" >&2
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
staging_dir="$(mktemp -d "$temp_root/mm-harness-ios-reset.XXXXXX")"
|
|
72
|
+
case "$staging_dir" in
|
|
73
|
+
"$temp_root"/mm-harness-ios-reset.*) ;;
|
|
74
|
+
*) printf 'reset-app-data: refusing unexpected staging path: %s\n' "$staging_dir" >&2; return 1 ;;
|
|
75
|
+
esac
|
|
76
|
+
[ -d "$staging_dir" ] && [ ! -L "$staging_dir" ] || return 1
|
|
77
|
+
staged_app="$staging_dir/$(basename "$app_source")"
|
|
78
|
+
MM_RESET_TEMP_ROOT="$temp_root"
|
|
79
|
+
MM_RESET_STAGING_DIR="$staging_dir"
|
|
80
|
+
cleanup_staging() {
|
|
81
|
+
case "$MM_RESET_STAGING_DIR" in
|
|
82
|
+
"$MM_RESET_TEMP_ROOT"/mm-harness-ios-reset.*)
|
|
83
|
+
[ -d "$MM_RESET_STAGING_DIR" ] && [ ! -L "$MM_RESET_STAGING_DIR" ] && rm -r -- "$MM_RESET_STAGING_DIR"
|
|
84
|
+
;;
|
|
85
|
+
*) printf 'reset-app-data: staging cleanup refused: %s\n' "$MM_RESET_STAGING_DIR" >&2 ;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
trap cleanup_staging EXIT HUP INT TERM
|
|
89
|
+
|
|
90
|
+
ditto "$app_source" "$staged_app"
|
|
91
|
+
[ -d "$staged_app" ] && [ ! -L "$staged_app" ] || {
|
|
92
|
+
printf 'reset-app-data: failed to preserve the installed iOS app.\n' >&2
|
|
93
|
+
return 1
|
|
94
|
+
}
|
|
95
|
+
printf 'Resetting iOS app data: simulator=%s bundle=%s\n' "$SIMULATOR" "$bundle_id" >&2
|
|
96
|
+
xcrun simctl terminate "$SIMULATOR" "$bundle_id" >/dev/null 2>&1 || true
|
|
97
|
+
xcrun simctl uninstall "$SIMULATOR" "$bundle_id"
|
|
98
|
+
xcrun simctl install "$SIMULATOR" "$staged_app"
|
|
99
|
+
xcrun simctl get_app_container "$SIMULATOR" "$bundle_id" app >/dev/null
|
|
100
|
+
printf 'Mobile app data reset; preserved installed iOS build %s.\n' "$bundle_id" >&2
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reset_android() {
|
|
104
|
+
local package_id="" candidate="" clear_output=""
|
|
105
|
+
local -a candidates adb_target installed_ids
|
|
106
|
+
if [ -z "$ADB_BIN" ]; then
|
|
107
|
+
ADB_BIN="$(command -v adb 2>/dev/null || true)"
|
|
108
|
+
fi
|
|
109
|
+
[ -n "$ADB_BIN" ] && [ "${ADB_BIN#/}" != "$ADB_BIN" ] && [ -x "$ADB_BIN" ] || {
|
|
110
|
+
printf 'reset-app-data: Android SDK Platform-Tools (adb) is required.\n' >&2
|
|
111
|
+
printf 'Next: brew install android-platform-tools\n' >&2
|
|
112
|
+
return 1
|
|
113
|
+
}
|
|
114
|
+
[ -n "$ADB_SERIAL_ARG" ] || {
|
|
115
|
+
printf 'reset-app-data: an Android serial is required.\n' >&2
|
|
116
|
+
printf 'Next: mm-harness devices --adapter mobile --json\n' >&2
|
|
117
|
+
return 1
|
|
118
|
+
}
|
|
119
|
+
adb_target=(-s "$ADB_SERIAL_ARG")
|
|
120
|
+
if [ -n "${ANDROID_PACKAGE_ID:-}" ]; then
|
|
121
|
+
candidates=("$ANDROID_PACKAGE_ID")
|
|
122
|
+
else
|
|
123
|
+
candidates=("io.metamask" "io.metamask.flask" "io.metamask.qa")
|
|
124
|
+
fi
|
|
125
|
+
for candidate in "${candidates[@]}"; do
|
|
126
|
+
if "$ADB_BIN" "${adb_target[@]}" shell pm path "$candidate" 2>/dev/null | grep -q '^package:'; then
|
|
127
|
+
installed_ids+=("$candidate")
|
|
128
|
+
fi
|
|
129
|
+
done
|
|
130
|
+
if [ "${#installed_ids[@]}" -gt 1 ]; then
|
|
131
|
+
printf 'reset-app-data: multiple MetaMask Android variants are installed on device %s: %s.\n' "$ADB_SERIAL_ARG" "${installed_ids[*]}" >&2
|
|
132
|
+
printf 'Next: set ANDROID_PACKAGE_ID to the exact variant and retry.\n' >&2
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
135
|
+
package_id="${installed_ids[0]:-}"
|
|
136
|
+
[ -n "$package_id" ] || {
|
|
137
|
+
printf 'reset-app-data: no installed MetaMask app was found on Android device %s.\n' "$ADB_SERIAL_ARG" >&2
|
|
138
|
+
printf "Next: mm-harness launch --adapter mobile --platform android --target '%s' --device '%s'\n" "$TARGET" "$ADB_SERIAL_ARG" >&2
|
|
139
|
+
return 1
|
|
140
|
+
}
|
|
141
|
+
printf 'Resetting Android app data: device=%s package=%s\n' "$ADB_SERIAL_ARG" "$package_id" >&2
|
|
142
|
+
clear_output="$("$ADB_BIN" "${adb_target[@]}" shell pm clear "$package_id")"
|
|
143
|
+
[ "$clear_output" = "Success" ] || {
|
|
144
|
+
printf 'reset-app-data: Android package clear failed: %s\n' "$clear_output" >&2
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
printf 'Mobile app data reset; preserved installed Android build %s.\n' "$package_id" >&2
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "$PLATFORM" in
|
|
151
|
+
ios) reset_ios ;;
|
|
152
|
+
android) reset_android ;;
|
|
153
|
+
*) printf 'reset-app-data: --platform must be ios or android (got: %s)\n' "$PLATFORM" >&2; exit 2 ;;
|
|
154
|
+
esac
|