@deeeed/metamask-harness 0.14.0 → 0.14.2
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 +31 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +15 -3
- package/adapters/extension/lib/extension-id.cjs +36 -0
- package/adapters/extension/live.sh +5 -3
- package/adapters/extension/readiness.mjs +48 -0
- package/adapters/extension/reattach.sh +163 -55
- package/adapters/extension/sidepanel-toggle.sh +96 -24
- package/adapters/extension/verify.sh +11 -0
- package/adapters/extension/wallet-fixture-state.cjs +6 -11
- package/adapters/mobile/open-device.sh +32 -5
- package/adapters/shared/resolve-slot-ports-core.mjs +14 -4
- package/dist/adapters/core/surface.js +1 -0
- package/dist/adapters/extension/runtime.js +14 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/adapters/mobile/provision.js +49 -4
- package/dist/adapters/mobile/surface.js +1 -0
- package/dist/adapters/slot-ports.js +11 -4
- package/dist/cli.js +4 -0
- package/dist/commands/call.js +19 -2
- package/dist/commands/check.js +326 -0
- package/dist/commands/core-readiness.js +75 -0
- package/dist/commands/device-target.js +113 -10
- package/dist/commands/doctor.js +30 -18
- package/dist/commands/launch/extension.js +105 -9
- package/dist/commands/launch/index.js +116 -15
- package/dist/commands/mobile-device-view.js +140 -0
- package/dist/commands/parse-args.js +4 -1
- package/dist/commands/recipe-quality.js +6 -2
- package/dist/commands/run-engine.js +28 -2
- package/dist/commands/run-report.js +115 -0
- package/dist/commands/run.js +113 -8
- package/dist/commands/shared.js +2 -1
- package/dist/commands/status-probe.js +9 -3
- package/dist/commands/status.js +40 -60
- package/dist/live-adapter-contract.js +5 -1
- package/dist/mm-harness-cli.js +61 -22
- package/docs/CLI-SPEC.md +25 -0
- package/library/actions/extension/platform/cdp.mjs +7 -4
- package/package.json +4 -4
|
@@ -36,6 +36,11 @@ fi
|
|
|
36
36
|
shift || true
|
|
37
37
|
|
|
38
38
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
+
# shellcheck disable=SC1091
|
|
40
|
+
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
|
|
41
|
+
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
42
|
+
done
|
|
43
|
+
unset _hp
|
|
39
44
|
REPO="${REPO:-}"
|
|
40
45
|
if [ -z "$REPO" ]; then
|
|
41
46
|
d="$SCRIPT_DIR"
|
|
@@ -52,6 +57,12 @@ if [ -z "$REPO" ]; then
|
|
|
52
57
|
exit 1
|
|
53
58
|
fi
|
|
54
59
|
cd "$REPO"
|
|
60
|
+
if command -v recipe_runtime_dir >/dev/null 2>&1; then
|
|
61
|
+
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
62
|
+
else
|
|
63
|
+
RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
|
|
64
|
+
fi
|
|
65
|
+
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
55
66
|
|
|
56
67
|
CDP_PORT="${CDP_PORT:-}"
|
|
57
68
|
EXT_ID="${EXT_ID:-}"
|
|
@@ -94,14 +105,33 @@ find_sidepanel_id() {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
resolve_ext_id() {
|
|
97
|
-
if [
|
|
108
|
+
if [[ "$EXT_ID" =~ ^[a-p]{32}$ ]]; then
|
|
98
109
|
printf '%s\n' "$EXT_ID"
|
|
99
110
|
return
|
|
100
111
|
fi
|
|
101
|
-
|
|
112
|
+
|
|
113
|
+
local manifest_id
|
|
114
|
+
manifest_id="$(
|
|
115
|
+
SCRIPT_DIR="$SCRIPT_DIR" REPO="$REPO" RUNTIME_DIR="$RUNTIME_DIR" RUNTIME_DIST_DIR="$RUNTIME_DIST_DIR" node <<'NODE' 2>/dev/null || true
|
|
116
|
+
const path = require('node:path');
|
|
117
|
+
const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
|
|
118
|
+
const id = extensionIdFromManifestFile(path.join(process.env.REPO, process.env.RUNTIME_DIR, process.env.RUNTIME_DIST_DIR, 'manifest.json'));
|
|
119
|
+
if (id) process.stdout.write(id);
|
|
120
|
+
NODE
|
|
121
|
+
)"
|
|
122
|
+
if [[ "$manifest_id" =~ ^[a-p]{32}$ ]]; then
|
|
123
|
+
printf '%s\n' "$manifest_id"
|
|
124
|
+
return
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
for idf in "$REPO/$RUNTIME_DIR/extension.id" "$AGENT_DIR/extension.id"; do
|
|
102
128
|
if [ -f "$idf" ]; then
|
|
103
|
-
|
|
104
|
-
|
|
129
|
+
local marker_id
|
|
130
|
+
marker_id="$(tr -d '[:space:]' < "$idf")"
|
|
131
|
+
if [[ "$marker_id" =~ ^[a-p]{32}$ ]]; then
|
|
132
|
+
printf '%s\n' "$marker_id"
|
|
133
|
+
return
|
|
134
|
+
fi
|
|
105
135
|
fi
|
|
106
136
|
done
|
|
107
137
|
json_list | python3 -c "import json,re,sys; d=json.load(sys.stdin); ids=[]; [ids.extend(re.findall(r'^chrome-extension://([^/]+)/', t.get('url',''))) for t in d]; print(ids[0] if ids else '')"
|
|
@@ -142,6 +172,32 @@ print_targets() {
|
|
|
142
172
|
json_list | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f' {t.get(\"type\",\"?\")[:18]:18s} {t.get(\"url\",\"\")[:100]}') for t in d]"
|
|
143
173
|
}
|
|
144
174
|
|
|
175
|
+
activate_dapp_tab() {
|
|
176
|
+
local target_id
|
|
177
|
+
target_id="$(
|
|
178
|
+
json_list | python3 -c "import json,sys; d=json.load(sys.stdin); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None); print(target.get('id','') if target else '')"
|
|
179
|
+
)"
|
|
180
|
+
if [ -n "$target_id" ]; then
|
|
181
|
+
curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
|
|
182
|
+
fi
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
cleanup_sidepanel_tabs() {
|
|
186
|
+
local ext_id close_ids
|
|
187
|
+
ext_id="$(resolve_ext_id)"
|
|
188
|
+
[ -n "$ext_id" ] || return 0
|
|
189
|
+
close_ids="$(
|
|
190
|
+
json_list | EXT_ID="$ext_id" python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); has_panel=any(t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'sidepanel.html' in t.get('url','') for t in d); has_dapp=any(t.get('type')=='page' and (t.get('url','').startswith('http://') or t.get('url','').startswith('https://')) for t in d); print('\\n'.join(t.get('id','') for t in d if has_panel and has_dapp and t.get('type')=='page' and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'home.html' in t.get('url','')))"
|
|
191
|
+
)"
|
|
192
|
+
if [ -n "$close_ids" ]; then
|
|
193
|
+
while IFS= read -r target_id; do
|
|
194
|
+
[ -n "$target_id" ] || continue
|
|
195
|
+
curl -s "http://127.0.0.1:${CDP_PORT}/json/close/${target_id}" >/dev/null || true
|
|
196
|
+
done <<< "$close_ids"
|
|
197
|
+
fi
|
|
198
|
+
activate_dapp_tab
|
|
199
|
+
}
|
|
200
|
+
|
|
145
201
|
status_sidepanel() {
|
|
146
202
|
local sp
|
|
147
203
|
sp="$(find_sidepanel_id)"
|
|
@@ -165,6 +221,7 @@ close_sidepanel() {
|
|
|
165
221
|
|
|
166
222
|
open_sidepanel() {
|
|
167
223
|
if [ -n "$(find_sidepanel_id)" ]; then
|
|
224
|
+
cleanup_sidepanel_tabs
|
|
168
225
|
echo "[sidepanel] already open"
|
|
169
226
|
return 0
|
|
170
227
|
fi
|
|
@@ -186,7 +243,6 @@ const { chromium } = require('playwright');
|
|
|
186
243
|
const extId = process.env.EXT_ID;
|
|
187
244
|
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
188
245
|
const context = browser.contexts()[0];
|
|
189
|
-
let createdPage = false;
|
|
190
246
|
let page = context
|
|
191
247
|
.pages()
|
|
192
248
|
.find(
|
|
@@ -197,7 +253,6 @@ const { chromium } = require('playwright');
|
|
|
197
253
|
|
|
198
254
|
if (!page) {
|
|
199
255
|
page = await context.newPage();
|
|
200
|
-
createdPage = true;
|
|
201
256
|
await page.goto(`chrome-extension://${extId}/home.html`, {
|
|
202
257
|
waitUntil: 'domcontentloaded',
|
|
203
258
|
timeout: 15000,
|
|
@@ -241,23 +296,35 @@ const { chromium } = require('playwright');
|
|
|
241
296
|
document.getElementById('__recipe_open_sidepanel__')?.remove();
|
|
242
297
|
});
|
|
243
298
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
299
|
+
const deadline = Date.now() + 5000;
|
|
300
|
+
let sidepanelPage;
|
|
301
|
+
while (Date.now() < deadline) {
|
|
302
|
+
sidepanelPage = context
|
|
303
|
+
.pages()
|
|
304
|
+
.find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`) && candidate.url().includes('/sidepanel.html'));
|
|
305
|
+
if (sidepanelPage) break;
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Only collapse fullscreen extension tabs after Chrome exposes the sidepanel
|
|
310
|
+
// target. chrome.sidePanel.open can return before the panel page appears; if we
|
|
311
|
+
// close home first, a slow/failed panel leaves the operator with no visible
|
|
312
|
+
// wallet page and the next readiness probe has to reopen from scratch.
|
|
313
|
+
if (sidepanelPage) {
|
|
314
|
+
for (const candidate of context.pages()) {
|
|
315
|
+
if (
|
|
316
|
+
candidate.url().startsWith(`chrome-extension://${extId}/`) &&
|
|
317
|
+
!candidate.url().includes('/sidepanel.html')
|
|
318
|
+
) {
|
|
319
|
+
try {
|
|
320
|
+
await candidate.close();
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.warn(
|
|
323
|
+
`[sidepanel] extension page close failed after successful open: ${
|
|
324
|
+
error && error.message ? error.message : error
|
|
325
|
+
}`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
261
328
|
}
|
|
262
329
|
}
|
|
263
330
|
}
|
|
@@ -271,7 +338,11 @@ const { chromium } = require('playwright');
|
|
|
271
338
|
// focus is cosmetic; the panel is already open
|
|
272
339
|
}
|
|
273
340
|
}
|
|
274
|
-
|
|
341
|
+
if (typeof browser.disconnect === 'function') {
|
|
342
|
+
await browser.disconnect();
|
|
343
|
+
} else {
|
|
344
|
+
await browser.close();
|
|
345
|
+
}
|
|
275
346
|
})().catch((error) => {
|
|
276
347
|
console.error(`FAIL: ${error.message || error}`);
|
|
277
348
|
process.exit(4);
|
|
@@ -279,6 +350,7 @@ const { chromium } = require('playwright');
|
|
|
279
350
|
NODE
|
|
280
351
|
|
|
281
352
|
if wait_for_sidepanel; then
|
|
353
|
+
cleanup_sidepanel_tabs
|
|
282
354
|
echo "[sidepanel] opened"
|
|
283
355
|
return 0
|
|
284
356
|
fi
|
|
@@ -385,6 +385,14 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
385
385
|
else
|
|
386
386
|
live_mode="live"
|
|
387
387
|
cdp_holder_json "$CDP_PORT" > "$ARTIFACTS/logs/cdp-holder.json"
|
|
388
|
+
if "$RUNNER_BIN" ensure-ready --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-ensure-ready.json" 2>&1; then
|
|
389
|
+
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"pass\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
390
|
+
refresh_extension_id
|
|
391
|
+
else
|
|
392
|
+
checks+=("{\"name\":\"home-tab convergence\",\"status\":\"fail\",\"detail\":\"see logs/extension-ensure-ready.json\"}")
|
|
393
|
+
status="fail"
|
|
394
|
+
fi
|
|
395
|
+
|
|
388
396
|
if node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
|
|
389
397
|
checks+=("{\"name\":\"live extension readiness\",\"status\":\"pass\"}")
|
|
390
398
|
# extension-readiness.mjs may repair $RUNTIME_DIR/extension.id when the
|
|
@@ -438,6 +446,8 @@ try { distFreshness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/dist
|
|
|
438
446
|
try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
|
|
439
447
|
try { cdpHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/cdp-holder.json'), 'utf8')); } catch {}
|
|
440
448
|
try { readinessReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
|
|
449
|
+
let ensureReadyReport = null;
|
|
450
|
+
try { ensureReadyReport = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-ensure-ready.json'), 'utf8')); } catch {}
|
|
441
451
|
function runGit(args) {
|
|
442
452
|
try {
|
|
443
453
|
return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
@@ -498,6 +508,7 @@ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
|
|
|
498
508
|
},
|
|
499
509
|
fixtureStatus,
|
|
500
510
|
cdpHolder,
|
|
511
|
+
ensureReady: ensureReadyReport,
|
|
501
512
|
cdpTarget,
|
|
502
513
|
checks: parsedChecks,
|
|
503
514
|
generatedAt: new Date().toISOString(),
|
|
@@ -20,6 +20,7 @@ const crypto = require('node:crypto');
|
|
|
20
20
|
const fs = require('node:fs');
|
|
21
21
|
const http = require('node:http');
|
|
22
22
|
const path = require('node:path');
|
|
23
|
+
const { extensionIdFromManifestKey } = require('./lib/extension-id.cjs');
|
|
23
24
|
|
|
24
25
|
const EOA_METHODS = [
|
|
25
26
|
'personal_sign',
|
|
@@ -573,16 +574,6 @@ function extensionIdFromUrl(url) {
|
|
|
573
574
|
return String(url).split('/')[2] || '';
|
|
574
575
|
}
|
|
575
576
|
|
|
576
|
-
function extensionIdFromManifestKey(key) {
|
|
577
|
-
if (!key) {
|
|
578
|
-
return '';
|
|
579
|
-
}
|
|
580
|
-
const digest = crypto.createHash('sha256').update(Buffer.from(key, 'base64')).digest();
|
|
581
|
-
return [...digest.subarray(0, 16)]
|
|
582
|
-
.map((byte) => `${'abcdefghijklmnop'[byte >> 4]}${'abcdefghijklmnop'[byte & 0x0f]}`)
|
|
583
|
-
.join('');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
577
|
function versionedStorageState(fixtureState) {
|
|
587
578
|
return fixtureState.data
|
|
588
579
|
? { data: fixtureState.data, meta: { ...(fixtureState.meta || {}), storageKind: 'data' } }
|
|
@@ -639,6 +630,10 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
639
630
|
const expectedServiceWorker = manifest.background?.service_worker || '';
|
|
640
631
|
const manifestId = extensionIdFromManifestKey(manifest.key);
|
|
641
632
|
const rejected = new Set();
|
|
633
|
+
if (manifestId && extensionIdFile) {
|
|
634
|
+
fs.mkdirSync(path.dirname(extensionIdFile), { recursive: true });
|
|
635
|
+
fs.writeFileSync(extensionIdFile, `${manifestId}\n`);
|
|
636
|
+
}
|
|
642
637
|
|
|
643
638
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
644
639
|
const candidates = [];
|
|
@@ -648,10 +643,10 @@ async function detectExtension(context, extensionDir, extensionIdFile) {
|
|
|
648
643
|
}
|
|
649
644
|
candidates.push({ id, reason });
|
|
650
645
|
};
|
|
646
|
+
push(manifestId, 'manifest key');
|
|
651
647
|
if (extensionIdFile && fs.existsSync(extensionIdFile)) {
|
|
652
648
|
push(fs.readFileSync(extensionIdFile, 'utf8').trim(), 'extension id marker');
|
|
653
649
|
}
|
|
654
|
-
push(manifestId, 'manifest key');
|
|
655
650
|
for (const worker of context.serviceWorkers()) {
|
|
656
651
|
const id = extensionIdFromUrl(worker.url());
|
|
657
652
|
if (expectedServiceWorker && worker.url().endsWith(`/${expectedServiceWorker}`)) {
|
|
@@ -106,20 +106,47 @@ sim_udid_for_target() {
|
|
|
106
106
|
if printf '%s' "$target" | grep -Eq '^[0-9A-Fa-f-]{36}$'; then
|
|
107
107
|
printf '%s\n' "$target"; return 0
|
|
108
108
|
fi
|
|
109
|
-
xcrun simctl list devices available 2>/dev/null
|
|
110
|
-
|
|
109
|
+
xcrun simctl list devices available --json 2>/dev/null | TARGET_SIMULATOR="$target" node -e '
|
|
110
|
+
let input = "";
|
|
111
|
+
process.stdin.setEncoding("utf8");
|
|
112
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
113
|
+
process.stdin.on("end", () => {
|
|
114
|
+
const target = process.env.TARGET_SIMULATOR || "";
|
|
115
|
+
try {
|
|
116
|
+
const parsed = JSON.parse(input);
|
|
117
|
+
for (const devices of Object.values(parsed.devices || {})) {
|
|
118
|
+
const found = Array.isArray(devices)
|
|
119
|
+
? devices.find((device) => device && device.isAvailable !== false && device.name === target)
|
|
120
|
+
: null;
|
|
121
|
+
if (found && found.udid) {
|
|
122
|
+
process.stdout.write(`${found.udid}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Empty stdout means unresolved; callers print the teaching error.
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
'
|
|
111
131
|
}
|
|
112
132
|
|
|
113
133
|
boot_simulator_if_needed() {
|
|
114
134
|
local target="$1" state=""
|
|
115
135
|
[ "$target" != "booted" ] || return 0
|
|
136
|
+
local udid=""
|
|
137
|
+
udid="$(sim_udid_for_target "$target")"
|
|
138
|
+
if [ -z "$udid" ]; then
|
|
139
|
+
printf "open-device: configured iOS simulator '%s' does not exist.\n" "$target" >&2
|
|
140
|
+
printf "Next: mm-harness provision runway ios --adapter mobile --target '%s' --device '%s'\n" "$TARGET" "$target" >&2
|
|
141
|
+
return 1
|
|
142
|
+
fi
|
|
116
143
|
local line=""
|
|
117
144
|
line="$(xcrun simctl list devices available 2>/dev/null | grep -F "${target} (" | head -1 || true)"
|
|
118
145
|
case "$line" in *"(Booted)"*) state="Booted" ;; *"(Shutdown)"*) state="Shutdown" ;; esac
|
|
119
146
|
[ "$state" = "Booted" ] && return 0
|
|
120
147
|
printf 'Booting iOS simulator %s%s\n' "$target" "${state:+ ($state)}" >&2
|
|
121
|
-
xcrun simctl boot "$
|
|
122
|
-
xcrun simctl bootstatus "$
|
|
148
|
+
xcrun simctl boot "$udid" 2>/dev/null || true
|
|
149
|
+
xcrun simctl bootstatus "$udid" -b >/dev/null
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
show_simulator() {
|
|
@@ -192,7 +219,7 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
192
219
|
# Expo dev-client URL scheme — drives both the deep link and the scheme-approval key.
|
|
193
220
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
194
221
|
|
|
195
|
-
boot_simulator_if_needed "$SIM_TARGET"
|
|
222
|
+
boot_simulator_if_needed "$SIM_TARGET" || exit 1
|
|
196
223
|
show_simulator "$SIM_TARGET"
|
|
197
224
|
|
|
198
225
|
LAUNCHED=false
|
|
@@ -31,6 +31,8 @@ export function formatKvLines(kv) {
|
|
|
31
31
|
if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
|
|
32
32
|
if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
|
|
33
33
|
if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
|
|
34
|
+
if (kv.ADB_SERIAL) lines.push(`ADB_SERIAL=${kv.ADB_SERIAL}`);
|
|
35
|
+
if (kv.ANDROID_DEVICE) lines.push(`ANDROID_DEVICE=${kv.ANDROID_DEVICE}`);
|
|
34
36
|
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -54,6 +56,8 @@ function scoreSlot(slot, slotSuffix) {
|
|
|
54
56
|
const cdp = resources.browser?.cdp_port;
|
|
55
57
|
const port = resources['dev-server']?.port;
|
|
56
58
|
const simulator = resources['ios-sim']?.simulator;
|
|
59
|
+
const adbSerial = resources['android-emu']?.adb_serial ?? resources.android?.adb_serial;
|
|
60
|
+
const androidDevice = resources['android-emu']?.device ?? resources.android?.device;
|
|
57
61
|
const session = slot.session ?? '';
|
|
58
62
|
const slotId = slot.id ?? '';
|
|
59
63
|
let score = 0;
|
|
@@ -64,7 +68,7 @@ function scoreSlot(slot, slotSuffix) {
|
|
|
64
68
|
if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
|
|
65
69
|
}
|
|
66
70
|
if (!slotId.toLowerCase().includes('demo')) score += 5;
|
|
67
|
-
return [score, cdp, port, slotId, simulator];
|
|
71
|
+
return [score, cdp, port, slotId, simulator, adbSerial, androidDevice];
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export function resolveSlotPortsByRepo(repo) {
|
|
@@ -111,12 +115,14 @@ export function resolveSlotPortsByRepo(repo) {
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
if (best) {
|
|
114
|
-
const [, cdp, port, slotId, simulator] = best;
|
|
118
|
+
const [, cdp, port, slotId, simulator, adbSerial, androidDevice] = best;
|
|
115
119
|
return formatKvLines({
|
|
116
120
|
...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
|
|
117
121
|
...(port !== undefined ? { WATCHER_PORT: port } : {}),
|
|
118
122
|
...(slotId ? { SLOT_ID: slotId } : {}),
|
|
119
123
|
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
124
|
+
...(adbSerial ? { ADB_SERIAL: adbSerial } : {}),
|
|
125
|
+
...(androidDevice ? { ANDROID_DEVICE: androidDevice } : {}),
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
}
|
|
@@ -152,10 +158,14 @@ export function resolveMobileRuntimeContext(repo) {
|
|
|
152
158
|
if (fs.existsSync(ctxPath)) {
|
|
153
159
|
try {
|
|
154
160
|
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
155
|
-
|
|
161
|
+
const adbSerial = c.adbSerial ?? c.androidSerial;
|
|
162
|
+
const androidDevice = c.androidDevice;
|
|
163
|
+
if (c.simulator || adbSerial || androidDevice || (c.metroPort != null && c.metroPort !== '')) {
|
|
156
164
|
return formatKvLines({
|
|
157
165
|
...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
|
|
158
166
|
...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
|
|
167
|
+
...(adbSerial ? { ADB_SERIAL: adbSerial } : {}),
|
|
168
|
+
...(androidDevice ? { ANDROID_DEVICE: androidDevice } : {}),
|
|
159
169
|
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
160
170
|
});
|
|
161
171
|
}
|
|
@@ -210,4 +220,4 @@ export const cliFns = {
|
|
|
210
220
|
resolve_mobile_runtime_context: resolveMobileRuntimeContext,
|
|
211
221
|
resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
|
|
212
222
|
resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
|
|
213
|
-
};
|
|
223
|
+
};
|
|
@@ -25,7 +25,8 @@ async function prepareExtensionRuntime(options) {
|
|
|
25
25
|
const health = await assertHealthyExtensionRuntime({
|
|
26
26
|
projectRoot,
|
|
27
27
|
cdpPort,
|
|
28
|
-
timeoutMs: options.healthTimeoutMs
|
|
28
|
+
timeoutMs: options.healthTimeoutMs,
|
|
29
|
+
pageMode: options.pageMode
|
|
29
30
|
});
|
|
30
31
|
return { launch, health };
|
|
31
32
|
}
|
|
@@ -34,14 +35,16 @@ async function assertHealthyExtensionRuntime(options) {
|
|
|
34
35
|
const deadline = Date.now() + timeoutMs;
|
|
35
36
|
let lastReport = null;
|
|
36
37
|
while (Date.now() <= deadline) {
|
|
37
|
-
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort
|
|
38
|
+
lastReport = await checkExtensionRuntimeHealth(options.projectRoot, options.cdpPort, {
|
|
39
|
+
pageMode: options.pageMode
|
|
40
|
+
});
|
|
38
41
|
if (lastReport.status === "PASS") return lastReport;
|
|
39
42
|
await sleep(500);
|
|
40
43
|
}
|
|
41
44
|
const report = lastReport ?? failReport(options.cdpPort, ["CDP runtime was not probed."], {});
|
|
42
45
|
throw new Error(formatHealthFailure(report, options.projectRoot));
|
|
43
46
|
}
|
|
44
|
-
async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
|
|
47
|
+
async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
|
|
45
48
|
const findings = [];
|
|
46
49
|
const warnings = [];
|
|
47
50
|
let targets = [];
|
|
@@ -52,11 +55,17 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
|
|
|
52
55
|
} catch (error) {
|
|
53
56
|
return failReport(cdpPort, [`CDP is not reachable on port ${cdpPort}: ${messageOf(error)}`], {});
|
|
54
57
|
}
|
|
55
|
-
const
|
|
58
|
+
const pageMode = options.pageMode ?? "home";
|
|
59
|
+
const homeTargets = targets.filter(
|
|
56
60
|
(target2) => target2.type === "page" && String(target2.url ?? "").startsWith("chrome-extension://") && String(target2.url ?? "").includes("/home.html") && Boolean(target2.webSocketDebuggerUrl)
|
|
57
61
|
);
|
|
62
|
+
const sidepanelTargets = targets.filter(
|
|
63
|
+
(target2) => target2.type === "page" && String(target2.url ?? "").startsWith("chrome-extension://") && String(target2.url ?? "").includes("/sidepanel.html") && Boolean(target2.webSocketDebuggerUrl)
|
|
64
|
+
);
|
|
65
|
+
const extensionTargets = pageMode === "home-or-sidepanel" && homeTargets.length === 0 ? sidepanelTargets : homeTargets;
|
|
58
66
|
if (extensionTargets.length !== 1) {
|
|
59
|
-
|
|
67
|
+
const expected = pageMode === "home-or-sidepanel" ? "exactly one MetaMask extension home page target, or one sidepanel target when home is intentionally closed" : "exactly one MetaMask extension home page target";
|
|
68
|
+
findings.push(`Expected ${expected}, found ${extensionTargets.length}.`);
|
|
60
69
|
}
|
|
61
70
|
const target = extensionTargets[0];
|
|
62
71
|
if (!target?.webSocketDebuggerUrl) {
|
|
@@ -457,11 +457,13 @@ function preapproveDeepLinkScheme(device, bundleId, options) {
|
|
|
457
457
|
function ensureSimulator(name, runtime, deviceType) {
|
|
458
458
|
const existing = findSimulator(name);
|
|
459
459
|
if (existing) return { name, udid: existing, created: false, runtime, deviceType };
|
|
460
|
-
|
|
461
|
-
|
|
460
|
+
const resolvedRuntime = runtime || latestIosRuntime();
|
|
461
|
+
const resolvedDeviceType = deviceType || preferredIphoneDeviceType();
|
|
462
|
+
if (!resolvedRuntime || !resolvedDeviceType) {
|
|
463
|
+
throw new Error(`simulator ${name} is missing and no available iOS simulator runtime/device type could be resolved from xcrun simctl.`);
|
|
462
464
|
}
|
|
463
|
-
const udid = execFileSync("xcrun", ["simctl", "create", name,
|
|
464
|
-
return { name, udid: udid || name, created: true, runtime, deviceType };
|
|
465
|
+
const udid = execFileSync("xcrun", ["simctl", "create", name, resolvedDeviceType, resolvedRuntime], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
466
|
+
return { name, udid: udid || name, created: true, runtime: resolvedRuntime, deviceType: resolvedDeviceType };
|
|
465
467
|
}
|
|
466
468
|
function findSimulator(name) {
|
|
467
469
|
const data = execJson("xcrun", ["simctl", "list", "devices", "--json"]);
|
|
@@ -471,6 +473,49 @@ function findSimulator(name) {
|
|
|
471
473
|
}
|
|
472
474
|
return void 0;
|
|
473
475
|
}
|
|
476
|
+
function latestIosRuntime() {
|
|
477
|
+
const data = execJson(
|
|
478
|
+
"xcrun",
|
|
479
|
+
["simctl", "list", "runtimes", "--json"]
|
|
480
|
+
);
|
|
481
|
+
const runtimes = (data.runtimes ?? []).filter((runtime) => runtime.isAvailable !== false && runtime.identifier && (runtime.platform === "iOS" || /(^|[.-])iOS([.-]|$)/u.test(runtime.identifier) || /^iOS\b/u.test(runtime.name ?? ""))).sort((a, b) => compareVersions(runtimeVersion(b), runtimeVersion(a)));
|
|
482
|
+
return runtimes[0]?.identifier;
|
|
483
|
+
}
|
|
484
|
+
function preferredIphoneDeviceType() {
|
|
485
|
+
const data = execJson("xcrun", ["simctl", "list", "devicetypes", "--json"]);
|
|
486
|
+
const devices = (data.devicetypes ?? []).filter((device) => device.identifier && /^iPhone\b/u.test(device.name ?? "")).sort((a, b) => {
|
|
487
|
+
const generation = iphoneGeneration(b) - iphoneGeneration(a);
|
|
488
|
+
if (generation !== 0) return generation;
|
|
489
|
+
const proScore = iphoneProScore(b) - iphoneProScore(a);
|
|
490
|
+
if (proScore !== 0) return proScore;
|
|
491
|
+
return String(b.name ?? "").localeCompare(String(a.name ?? ""));
|
|
492
|
+
});
|
|
493
|
+
return devices[0]?.identifier;
|
|
494
|
+
}
|
|
495
|
+
function runtimeVersion(runtime) {
|
|
496
|
+
const raw = runtime.version || /iOS[-. ]([0-9][0-9A-Za-z_.-]*)/u.exec(runtime.identifier ?? "")?.[1] || runtime.name || "";
|
|
497
|
+
const parts = raw.match(/\d+/gu)?.map((part) => Number(part)) ?? [];
|
|
498
|
+
return parts.length ? parts : [0];
|
|
499
|
+
}
|
|
500
|
+
function compareVersions(a, b) {
|
|
501
|
+
const length = Math.max(a.length, b.length);
|
|
502
|
+
for (let i = 0; i < length; i += 1) {
|
|
503
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
504
|
+
if (diff !== 0) return diff;
|
|
505
|
+
}
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
function iphoneGeneration(device) {
|
|
509
|
+
const match = /^iPhone\s+(\d+)/u.exec(device.name ?? "");
|
|
510
|
+
return match ? Number(match[1]) : 0;
|
|
511
|
+
}
|
|
512
|
+
function iphoneProScore(device) {
|
|
513
|
+
const name = device.name ?? "";
|
|
514
|
+
if (/\bPro Max\b/u.test(name)) return 3;
|
|
515
|
+
if (/\bPro\b/u.test(name)) return 2;
|
|
516
|
+
if (/\bPlus\b/u.test(name)) return 1;
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
474
519
|
function baselineSimulatorCandidates(data, current) {
|
|
475
520
|
const candidates = [];
|
|
476
521
|
if (current.simulator) candidates.push(current.simulator);
|
|
@@ -10,10 +10,8 @@ import {
|
|
|
10
10
|
resolveMobileSlotDefaults
|
|
11
11
|
} from "./resolve-slot-ports.js";
|
|
12
12
|
function applyKVLines(output, overwrite) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (!m) continue;
|
|
16
|
-
const [, key, val] = m;
|
|
13
|
+
const entries = output.split("\n").map((line) => /^([A-Z_]+)=(.+)$/u.exec(line.trim())).filter((match) => match !== null).map((match) => ({ key: match[1], val: match[2] }));
|
|
14
|
+
for (const { key, val } of entries) {
|
|
17
15
|
if (val === "null" || val === "undefined" || val === "") continue;
|
|
18
16
|
switch (key) {
|
|
19
17
|
case "WATCHER_PORT":
|
|
@@ -26,6 +24,15 @@ function applyKVLines(output, overwrite) {
|
|
|
26
24
|
case "IOS_SIMULATOR":
|
|
27
25
|
if (overwrite || !process.env["IOS_SIMULATOR"]) process.env["IOS_SIMULATOR"] = val;
|
|
28
26
|
break;
|
|
27
|
+
case "ADB_SERIAL":
|
|
28
|
+
if (overwrite || !process.env["ADB_SERIAL"]) {
|
|
29
|
+
process.env["ADB_SERIAL"] = val;
|
|
30
|
+
process.env["ANDROID_SERIAL"] = val;
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case "ANDROID_DEVICE":
|
|
34
|
+
if (overwrite || !process.env["ANDROID_DEVICE"]) process.env["ANDROID_DEVICE"] = val;
|
|
35
|
+
break;
|
|
29
36
|
case "SLOT_ID":
|
|
30
37
|
if (overwrite || !process.env["RECIPE_SLOT_ID"]) process.env["RECIPE_SLOT_ID"] = val;
|
|
31
38
|
break;
|
package/dist/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ import { handleDebug } from "./commands/debug.js";
|
|
|
19
19
|
import { handleFixtures } from "./commands/fixtures.js";
|
|
20
20
|
import { handleRecipeQuality } from "./commands/recipe-quality.js";
|
|
21
21
|
import { handleStatus } from "./commands/status.js";
|
|
22
|
+
import { handleCheck } from "./commands/check.js";
|
|
22
23
|
import { parseArgs, targetPath } from "./commands/parse-args.js";
|
|
23
24
|
import { runOneNode } from "./commands/run-engine.js";
|
|
24
25
|
const COMMANDS = {
|
|
@@ -56,6 +57,8 @@ PROVE \u2014 run recipes and inspect capabilities:
|
|
|
56
57
|
mm-harness flows list
|
|
57
58
|
doctor Readiness check for a checkout (no app launch).
|
|
58
59
|
mm-harness doctor
|
|
60
|
+
check Run bounded repo-local checks for an active diff.
|
|
61
|
+
mm-harness check diff --profile fast --artifacts-dir artifacts/validation
|
|
59
62
|
actions Describe the actions a manifest declares.
|
|
60
63
|
mm-harness actions --adapter mobile
|
|
61
64
|
manifest Print/validate the action manifest for an adapter.
|
|
@@ -127,6 +130,7 @@ async function main(argv) {
|
|
|
127
130
|
if (command === "debug") return handleDebug(argv.slice(1));
|
|
128
131
|
if (command === "fixtures") return handleFixtures(argv.slice(1), { runOneNode });
|
|
129
132
|
if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
|
|
133
|
+
if (command === "check") return handleCheck(argv.slice(1));
|
|
130
134
|
const handler = COMMANDS[command];
|
|
131
135
|
if (!handler) throw new Error(`Unknown command: ${command}`);
|
|
132
136
|
return handler(parseArgs(argv.slice(1), command));
|
package/dist/commands/call.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { loadActionManifest } from "../manifest.js";
|
|
6
6
|
import { importRecipeProtocol } from "../paths.js";
|
|
7
7
|
import { recipeRunning } from "../heal-bounds.js";
|
|
8
|
+
import { color } from "../cli-color.js";
|
|
8
9
|
import { EXIT } from "./shared.js";
|
|
9
10
|
import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
|
|
10
11
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
20
21
|
import { handleListExecutables } from "./list-executables.js";
|
|
21
22
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
23
|
+
import { coreDependencyBlock } from "./core-readiness.js";
|
|
22
24
|
import {
|
|
23
25
|
emitHealViolation,
|
|
24
26
|
executeWithHealBounds,
|
|
@@ -96,6 +98,16 @@ async function handleCall(argv) {
|
|
|
96
98
|
return EXIT.usage;
|
|
97
99
|
}
|
|
98
100
|
const resolvedAction = resolution.resolved;
|
|
101
|
+
const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
|
|
102
|
+
if (depsBlock) {
|
|
103
|
+
if (json) {
|
|
104
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, status: "fail", exitCode: EXIT.usage, error: depsBlock }, null, 2));
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`\u2717 call: ${depsBlock.message}`);
|
|
107
|
+
console.error(` Next: ${depsBlock.userAction}`);
|
|
108
|
+
}
|
|
109
|
+
return EXIT.usage;
|
|
110
|
+
}
|
|
99
111
|
const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
|
|
100
112
|
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
101
113
|
if (validation.status === "invalid") {
|
|
@@ -157,8 +169,13 @@ async function handleCall(argv) {
|
|
|
157
169
|
const rendered = callOutput !== void 0 ? `
|
|
158
170
|
Result:
|
|
159
171
|
${formatCallOutput(callOutput)}` : "";
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
173
|
+
console.log(
|
|
174
|
+
`${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
|
|
175
|
+
${out("label", "Result:")}
|
|
176
|
+
${formatCallOutput(callOutput)}` : ""}
|
|
177
|
+
${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}`
|
|
178
|
+
);
|
|
162
179
|
}
|
|
163
180
|
return result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
164
181
|
}
|