@deeeed/metamask-harness 0.6.2 → 0.7.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 +77 -0
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/reattach.sh +210 -0
- package/adapters/extension/start-watch.sh +11 -17
- package/adapters/manifest.json +65 -9
- package/adapters/mobile/lib/tmux-viewer.sh +31 -10
- package/adapters/mobile/open-device.sh +14 -1
- package/adapters/mobile/stop-metro.sh +1 -1
- package/adapters/mobile/yarn-setup.sh +15 -1
- package/adapters/shared/activate-repo-ruby.sh +124 -0
- package/adapters/shared/open-log-window.sh +55 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
- package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
- package/adapters/shared/resolve-farmslot-ports.sh +6 -104
- package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
- package/adapters/shared/resolve-slot-ports.mjs +20 -0
- package/adapters/shared/resolve-slot-ports.sh +110 -0
- package/adapters/shared/tmux-session.sh +35 -0
- package/dist/adapters/extension/runtime-decision.js +4 -0
- package/dist/adapters/mobile/provision.js +25 -2
- package/dist/adapters/{resolve-farmslot-ports.js → resolve-slot-ports.js} +4 -2
- package/dist/adapters/slot-ports.js +8 -10
- package/dist/cli-commands.js +1 -1
- package/dist/commands/call.js +5 -0
- package/dist/commands/doctor.js +21 -1
- package/dist/commands/fixtures.js +5 -3
- package/dist/commands/launch/extension.js +80 -6
- package/dist/commands/launch/index.js +10 -1
- package/dist/commands/list-executables.js +48 -0
- package/dist/commands/logs.js +25 -2
- package/dist/commands/manifest.js +27 -7
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +3 -4
- package/dist/live-adapter-contract.js +30 -9
- package/dist/mm-harness-cli.js +3 -1
- package/dist/paths.js +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Slot-pool + slot-suffix port resolution — pure JS for runner and overlay leaves.
|
|
2
|
+
// Reads the orchestrator's pool JSON (the on-disk `pool/` directory) when a slot
|
|
3
|
+
// repo matches, else the local-extension-N suffix formula.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const coreDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
function pathDefault(key) {
|
|
13
|
+
for (const candidate of [
|
|
14
|
+
path.join(coreDir, 'path-defaults.json'),
|
|
15
|
+
path.join(coreDir, '../../adapters/shared/path-defaults.json'),
|
|
16
|
+
]) {
|
|
17
|
+
if (!fs.existsSync(candidate)) continue;
|
|
18
|
+
const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
|
|
19
|
+
if (value) return value;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Missing path default: ${key}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function recipeRuntimeDir() {
|
|
25
|
+
return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatKvLines(kv) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
if (kv.CDP_PORT !== undefined) lines.push(`CDP_PORT=${kv.CDP_PORT}`);
|
|
31
|
+
if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
|
|
32
|
+
if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
|
|
33
|
+
if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
|
|
34
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function realRepoPath(repo) {
|
|
38
|
+
if (!repo) return null;
|
|
39
|
+
try {
|
|
40
|
+
return fs.realpathSync(repo);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function inferSlotSuffix(repo) {
|
|
47
|
+
const base = path.basename(repo);
|
|
48
|
+
const m = /-(\d+)$/u.exec(base);
|
|
49
|
+
return m ? m[1] : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scoreSlot(slot, slotSuffix) {
|
|
53
|
+
const resources = slot.resources ?? {};
|
|
54
|
+
const cdp = resources.browser?.cdp_port;
|
|
55
|
+
const port = resources['dev-server']?.port;
|
|
56
|
+
const simulator = resources['ios-sim']?.simulator;
|
|
57
|
+
const session = slot.session ?? '';
|
|
58
|
+
const slotId = slot.id ?? '';
|
|
59
|
+
let score = 0;
|
|
60
|
+
if (cdp !== undefined) score += 100;
|
|
61
|
+
if (port !== undefined) score += 10;
|
|
62
|
+
if (slotSuffix) {
|
|
63
|
+
if (session === `mme-${slotSuffix}`) score += 50;
|
|
64
|
+
if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
|
|
65
|
+
}
|
|
66
|
+
if (!slotId.toLowerCase().includes('demo')) score += 5;
|
|
67
|
+
return [score, cdp, port, slotId, simulator];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveSlotPortsByRepo(repo) {
|
|
71
|
+
const realRepo = realRepoPath(repo);
|
|
72
|
+
if (!realRepo) return null;
|
|
73
|
+
|
|
74
|
+
const slotSuffix = inferSlotSuffix(realRepo);
|
|
75
|
+
const roots = [];
|
|
76
|
+
// A slot checkout lives at <workspace>/repos/<slot>; that workspace's own
|
|
77
|
+
// pool is the authoritative one for this repo — machine-global fallbacks
|
|
78
|
+
// (env root, dev checkout) apply only when the repo is not workspace-shaped.
|
|
79
|
+
if (path.basename(path.dirname(realRepo)) === 'repos') {
|
|
80
|
+
roots.push(path.join(path.dirname(path.dirname(realRepo)), 'farmslot'));
|
|
81
|
+
}
|
|
82
|
+
if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
|
|
83
|
+
roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
|
|
84
|
+
|
|
85
|
+
for (const root of roots) {
|
|
86
|
+
if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
|
|
87
|
+
const poolDir = path.join(root, 'pool');
|
|
88
|
+
let best = null;
|
|
89
|
+
|
|
90
|
+
for (const name of fs.readdirSync(poolDir).sort()) {
|
|
91
|
+
if (!name.endsWith('.json') || name.includes('.bak.')) continue;
|
|
92
|
+
let data;
|
|
93
|
+
try {
|
|
94
|
+
data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const slot of data.slots ?? []) {
|
|
99
|
+
const slotRepo = slot.repo ?? '';
|
|
100
|
+
if (!slotRepo) continue;
|
|
101
|
+
let slotReal;
|
|
102
|
+
try {
|
|
103
|
+
slotReal = fs.realpathSync(slotRepo);
|
|
104
|
+
} catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (slotReal !== realRepo) continue;
|
|
108
|
+
const scored = scoreSlot(slot, slotSuffix);
|
|
109
|
+
if (!best || scored[0] > best[0]) best = scored;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (best) {
|
|
114
|
+
const [, cdp, port, slotId, simulator] = best;
|
|
115
|
+
return formatKvLines({
|
|
116
|
+
...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
|
|
117
|
+
...(port !== undefined ? { WATCHER_PORT: port } : {}),
|
|
118
|
+
...(slotId ? { SLOT_ID: slotId } : {}),
|
|
119
|
+
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function resolveDefaultExtensionPorts(repo) {
|
|
127
|
+
const n = inferSlotSuffix(repo);
|
|
128
|
+
if (!n) return null;
|
|
129
|
+
return formatKvLines({
|
|
130
|
+
CDP_PORT: 6660 + Number(n),
|
|
131
|
+
WATCHER_PORT: 9010 + Number(n),
|
|
132
|
+
SLOT_ID: `local-extension-${n}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function resolveExtensionRuntimePorts(repo) {
|
|
137
|
+
return resolveSlotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveMobileSlotDefaults(repo) {
|
|
141
|
+
const n = inferSlotSuffix(repo);
|
|
142
|
+
if (!n) return null;
|
|
143
|
+
return formatKvLines({
|
|
144
|
+
WATCHER_PORT: 8060 + Number(n),
|
|
145
|
+
IOS_SIMULATOR: `mm-${n}`,
|
|
146
|
+
SLOT_ID: `local-mobile-${n}`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveMobileRuntimeContext(repo) {
|
|
151
|
+
const ctxPath = path.join(repo, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
152
|
+
if (fs.existsSync(ctxPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
155
|
+
if (c.simulator || (c.metroPort != null && c.metroPort !== '')) {
|
|
156
|
+
return formatKvLines({
|
|
157
|
+
...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
|
|
158
|
+
...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
|
|
159
|
+
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
/* unreadable context falls through to the provision baseline */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return resolveMobileProvisionBaseline(repo);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
|
|
170
|
+
// provision baseline records the same authoritative simulator/port identity —
|
|
171
|
+
// without it, a fresh slot's first launch degrades to the simctl `booted`
|
|
172
|
+
// alias and misses the installed dev client entirely.
|
|
173
|
+
function resolveMobileProvisionBaseline(repo) {
|
|
174
|
+
const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
|
|
175
|
+
if (!fs.existsSync(basePath)) return null;
|
|
176
|
+
try {
|
|
177
|
+
const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
|
|
178
|
+
const simulator = c.simulator?.name || c.simulator?.udid;
|
|
179
|
+
const watcherPort = c.watcherPort != null && c.watcherPort !== '' ? c.watcherPort : undefined;
|
|
180
|
+
if (!simulator && watcherPort === undefined) return null;
|
|
181
|
+
return formatKvLines({
|
|
182
|
+
...(watcherPort !== undefined ? { WATCHER_PORT: watcherPort } : {}),
|
|
183
|
+
...(simulator ? { IOS_SIMULATOR: simulator } : {}),
|
|
184
|
+
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function resolveMobileRuntimePorts(repo) {
|
|
192
|
+
return (
|
|
193
|
+
resolveMobileRuntimeContext(repo)
|
|
194
|
+
?? resolveSlotPortsByRepo(repo)
|
|
195
|
+
?? resolveMobileSlotDefaults(repo)
|
|
196
|
+
?? ''
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Back-compat: the previous name for resolveSlotPortsByRepo, kept as an alias for
|
|
201
|
+
// one release so existing importers keep resolving.
|
|
202
|
+
export const resolveFarmslotPortsByRepo = resolveSlotPortsByRepo;
|
|
203
|
+
|
|
204
|
+
export const cliFns = {
|
|
205
|
+
resolve_slot_ports_by_repo: resolveSlotPortsByRepo,
|
|
206
|
+
// Back-compat cli key for one release.
|
|
207
|
+
resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
|
|
208
|
+
resolve_default_extension_ports: resolveDefaultExtensionPorts,
|
|
209
|
+
resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
|
|
210
|
+
resolve_mobile_runtime_context: resolveMobileRuntimeContext,
|
|
211
|
+
resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
|
|
212
|
+
resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
|
|
213
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node leaf for port resolution — self-contained for runner and injected overlay copies.
|
|
3
|
+
|
|
4
|
+
import { cliFns } from './resolve-slot-ports-core.mjs';
|
|
5
|
+
|
|
6
|
+
const [, , fn, repo] = process.argv;
|
|
7
|
+
if (!fn || fn === '-h' || fn === '--help') {
|
|
8
|
+
process.stderr.write('Usage: resolve-slot-ports.mjs <fn> <repo>\n');
|
|
9
|
+
process.exit(fn ? 0 : 2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handler = cliFns[fn];
|
|
13
|
+
if (!handler) {
|
|
14
|
+
process.stderr.write(`resolve-slot-ports: unknown fn: ${fn}\n`);
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const out = handler(repo ?? '.');
|
|
19
|
+
if (!out?.trim()) process.exit(1);
|
|
20
|
+
process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# resolve-slot-ports.sh — bash-compat wrapper over resolve-slot-ports.mjs
|
|
3
|
+
#
|
|
4
|
+
# The orchestrator's pool JSON is authoritative when a slot repo matches; otherwise
|
|
5
|
+
# fall back to the local-extension-N formula (6660+N / 9010+N).
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# . resolve-slot-ports.sh
|
|
9
|
+
# resolve_extension_runtime_ports /path/to/metamask-extension-N
|
|
10
|
+
# -> prints CDP_PORT=... WATCHER_PORT=... SLOT_ID=... lines
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
_RESOLVE_PORTS_MJS="$SCRIPT_DIR/resolve-slot-ports.mjs"
|
|
14
|
+
|
|
15
|
+
_resolve_ports_cli() {
|
|
16
|
+
node "$_RESOLVE_PORTS_MJS" "$1" "${2:-.}"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resolve_slot_ports_by_repo() {
|
|
20
|
+
_resolve_ports_cli resolve_slot_ports_by_repo "$1"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Back-compat alias for the previous function name (one release).
|
|
24
|
+
resolve_farmslot_ports_by_repo() {
|
|
25
|
+
resolve_slot_ports_by_repo "$1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
infer_extension_slot_suffix() {
|
|
29
|
+
local repo="$1" base
|
|
30
|
+
base="$(basename "$repo")"
|
|
31
|
+
if [[ "$base" =~ -([0-9]+)$ ]]; then
|
|
32
|
+
printf '%s' "${BASH_REMATCH[1]}"
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resolve_default_extension_ports() {
|
|
39
|
+
_resolve_ports_cli resolve_default_extension_ports "$1"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolve_extension_runtime_ports() {
|
|
43
|
+
_resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
apply_resolved_extension_ports() {
|
|
47
|
+
local repo="${1:-.}" line key val from_pool=false resolved=""
|
|
48
|
+
if resolved="$(_resolve_ports_cli resolve_slot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
49
|
+
from_pool=true
|
|
50
|
+
elif resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)"; then
|
|
51
|
+
from_pool=false
|
|
52
|
+
else
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
while IFS= read -r line; do
|
|
56
|
+
[ -n "$line" ] || continue
|
|
57
|
+
key="${line%%=*}"
|
|
58
|
+
val="${line#*=}"
|
|
59
|
+
case "$key" in
|
|
60
|
+
CDP_PORT)
|
|
61
|
+
if [ "$from_pool" = true ] || [ -z "${CDP_PORT:-}" ]; then CDP_PORT="$val"; fi
|
|
62
|
+
;;
|
|
63
|
+
WATCHER_PORT)
|
|
64
|
+
if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
|
|
65
|
+
;;
|
|
66
|
+
SLOT_ID)
|
|
67
|
+
if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
done <<< "$resolved"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolve_mobile_slot_defaults() {
|
|
74
|
+
_resolve_ports_cli resolve_mobile_slot_defaults "$1"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resolve_mobile_runtime_context() {
|
|
78
|
+
_resolve_ports_cli resolve_mobile_runtime_context "$1"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
resolve_mobile_runtime_ports() {
|
|
82
|
+
_resolve_ports_cli resolve_mobile_runtime_ports "${1:-.}"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
apply_resolved_mobile_ports() {
|
|
86
|
+
local repo="${1:-.}" line key val from_pool=false resolved=""
|
|
87
|
+
if resolved="$(_resolve_ports_cli resolve_slot_ports_by_repo "$repo" 2>/dev/null)"; then
|
|
88
|
+
from_pool=true
|
|
89
|
+
elif resolved="$(_resolve_ports_cli resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
|
|
90
|
+
from_pool=false
|
|
91
|
+
else
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
while IFS= read -r line; do
|
|
95
|
+
[ -n "$line" ] || continue
|
|
96
|
+
key="${line%%=*}"
|
|
97
|
+
val="${line#*=}"
|
|
98
|
+
case "$key" in
|
|
99
|
+
WATCHER_PORT)
|
|
100
|
+
if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
|
|
101
|
+
;;
|
|
102
|
+
IOS_SIMULATOR)
|
|
103
|
+
if [ "$from_pool" = true ] || [ -z "${IOS_SIMULATOR:-}" ]; then IOS_SIMULATOR="$val"; fi
|
|
104
|
+
;;
|
|
105
|
+
SLOT_ID)
|
|
106
|
+
if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
|
|
107
|
+
;;
|
|
108
|
+
esac
|
|
109
|
+
done <<< "$resolved"
|
|
110
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# tmux-session — resolve the run-owned tmux session a log-tail window belongs in.
|
|
3
|
+
#
|
|
4
|
+
# Responsibility split: the orchestrator (farmslot) NAMES the session; the harness
|
|
5
|
+
# only POPULATES windows inside it. The name is therefore never guessed from the
|
|
6
|
+
# slot number — a renamed pool or a manual run would land the window (and its
|
|
7
|
+
# tail -F) in the wrong place. Resolution ladder, first hit wins:
|
|
8
|
+
# 1. RECIPE_TMUX_SESSION — the orchestrator's explicit hand-off.
|
|
9
|
+
# 2. agentic-runtime.json `session` — the prepared checkout's recorded session.
|
|
10
|
+
# 3. the current session — but ONLY inside a tmux client (never the
|
|
11
|
+
# last-attached session reported outside one, which would be a foreign leak).
|
|
12
|
+
# Prints the resolved name (empty when none). Callers still gate on has-session
|
|
13
|
+
# before creating a window.
|
|
14
|
+
|
|
15
|
+
# shellcheck disable=SC2329 # sourced by leaves and by contract tests.
|
|
16
|
+
resolve_run_tmux_session() {
|
|
17
|
+
local runtime_dir="${1:-}" ctx="" session=""
|
|
18
|
+
if [ -n "${RECIPE_TMUX_SESSION:-}" ]; then
|
|
19
|
+
printf '%s\n' "$RECIPE_TMUX_SESSION"
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
ctx="${RECIPE_RUNTIME_CONTEXT:-}"
|
|
23
|
+
if [ -z "$ctx" ] && [ -n "$runtime_dir" ]; then ctx="$runtime_dir/agentic-runtime.json"; fi
|
|
24
|
+
if [ -n "$ctx" ] && [ -f "$ctx" ] && command -v node >/dev/null 2>&1; then
|
|
25
|
+
session="$(RUN_TMUX_CTX="$ctx" node -e 'try{const d=JSON.parse(require("fs").readFileSync(process.env.RUN_TMUX_CTX,"utf8"));const s=d&&d.session;if(s!==undefined&&s!==null&&s!=="")process.stdout.write(String(s));}catch{}' 2>/dev/null || true)"
|
|
26
|
+
if [ -n "$session" ]; then
|
|
27
|
+
printf '%s\n' "$session"
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
if [ -n "${TMUX:-}" ]; then
|
|
32
|
+
tmux display-message -p '#S' 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
@@ -181,6 +181,9 @@ function distCheck(target) {
|
|
|
181
181
|
}
|
|
182
182
|
return { status: "fresh", distGitId: distShort, head: headShort };
|
|
183
183
|
}
|
|
184
|
+
function isExtensionDistStale(target) {
|
|
185
|
+
return distCheck(path.resolve(target)).status === "stale";
|
|
186
|
+
}
|
|
184
187
|
async function cdpCheck(target, cdpPort) {
|
|
185
188
|
if (!cdpPort) return { status: "skipped" };
|
|
186
189
|
try {
|
|
@@ -301,5 +304,6 @@ function buildAction(target, clean) {
|
|
|
301
304
|
}
|
|
302
305
|
export {
|
|
303
306
|
decideExtensionReadiness,
|
|
307
|
+
isExtensionDistStale,
|
|
304
308
|
recordReadinessBaseline
|
|
305
309
|
};
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { recipeRuntimeDir } from "../../paths.js";
|
|
7
|
-
import {
|
|
7
|
+
import { resolveSlotPortsByRepo } from "../../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
8
8
|
const RUNWAY_IOS_METADATA = {
|
|
9
9
|
artifactName: "ios-app-main-dev-expo",
|
|
10
10
|
workflow: "expo-dev-build.yml",
|
|
@@ -13,7 +13,7 @@ const RUNWAY_IOS_METADATA = {
|
|
|
13
13
|
fallbackRepo: "MetaMask/metamask-mobile"
|
|
14
14
|
};
|
|
15
15
|
function resolvePoolIdentity(target) {
|
|
16
|
-
const out =
|
|
16
|
+
const out = resolveSlotPortsByRepo(target);
|
|
17
17
|
const id = {};
|
|
18
18
|
if (!out) return id;
|
|
19
19
|
for (const line of String(out).split("\n")) {
|
|
@@ -95,6 +95,7 @@ async function provisionRunwayMobile(target, options) {
|
|
|
95
95
|
log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
|
|
96
96
|
bootSimulator(sim.udid ?? sim.name, options);
|
|
97
97
|
execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
|
|
98
|
+
preapproveDeepLinkScheme(sim.udid ?? sim.name, RUNWAY_IOS_METADATA.bundleId, options);
|
|
98
99
|
const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
|
|
99
100
|
return {
|
|
100
101
|
schemaVersion: 1,
|
|
@@ -412,6 +413,28 @@ function bootSimulator(device, options) {
|
|
|
412
413
|
log(options, `runway: bootstatus wait for ${device} did not confirm; continuing to install`);
|
|
413
414
|
}
|
|
414
415
|
}
|
|
416
|
+
function preapproveDeepLinkScheme(device, bundleId, options) {
|
|
417
|
+
const scheme = process.env.IOS_DEV_CLIENT_SCHEME ?? "expo-metamask";
|
|
418
|
+
try {
|
|
419
|
+
execFileSync(
|
|
420
|
+
"xcrun",
|
|
421
|
+
[
|
|
422
|
+
"simctl",
|
|
423
|
+
"spawn",
|
|
424
|
+
device,
|
|
425
|
+
"defaults",
|
|
426
|
+
"write",
|
|
427
|
+
"com.apple.launchservices.schemeapproval",
|
|
428
|
+
`com.apple.CoreSimulator.CoreSimulatorBridge-->${scheme}`,
|
|
429
|
+
"-string",
|
|
430
|
+
bundleId
|
|
431
|
+
],
|
|
432
|
+
{ stdio: ["ignore", "ignore", "ignore"], timeout: 15e3 }
|
|
433
|
+
);
|
|
434
|
+
log(options, `runway: pre-approved deep-link scheme ${scheme} \u2192 ${bundleId}`);
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
415
438
|
function ensureSimulator(name, runtime, deviceType) {
|
|
416
439
|
const existing = findSimulator(name);
|
|
417
440
|
if (existing) return { name, udid: existing, created: false, runtime, deviceType };
|
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
realRepoPath,
|
|
5
5
|
resolveDefaultExtensionPorts,
|
|
6
6
|
resolveExtensionRuntimePorts,
|
|
7
|
+
resolveSlotPortsByRepo,
|
|
7
8
|
resolveFarmslotPortsByRepo,
|
|
8
9
|
resolveMobileRuntimeContext,
|
|
9
10
|
resolveMobileRuntimePorts,
|
|
10
11
|
resolveMobileSlotDefaults
|
|
11
|
-
} from "../../adapters/shared/resolve-
|
|
12
|
+
} from "../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
12
13
|
export {
|
|
13
14
|
formatKvLines,
|
|
14
15
|
inferSlotSuffix,
|
|
@@ -18,5 +19,6 @@ export {
|
|
|
18
19
|
resolveFarmslotPortsByRepo,
|
|
19
20
|
resolveMobileRuntimeContext,
|
|
20
21
|
resolveMobileRuntimePorts,
|
|
21
|
-
resolveMobileSlotDefaults
|
|
22
|
+
resolveMobileSlotDefaults,
|
|
23
|
+
resolveSlotPortsByRepo
|
|
22
24
|
};
|
|
@@ -5,10 +5,10 @@ import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.j
|
|
|
5
5
|
import { recipeRuntimeDir } from "../paths.js";
|
|
6
6
|
import {
|
|
7
7
|
resolveDefaultExtensionPorts,
|
|
8
|
-
|
|
8
|
+
resolveSlotPortsByRepo,
|
|
9
9
|
resolveMobileRuntimeContext,
|
|
10
10
|
resolveMobileSlotDefaults
|
|
11
|
-
} from "./resolve-
|
|
11
|
+
} from "./resolve-slot-ports.js";
|
|
12
12
|
function applyKVLines(output, overwrite) {
|
|
13
13
|
for (const line of output.split("\n")) {
|
|
14
14
|
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
@@ -41,7 +41,7 @@ function applyKVLines(output, overwrite) {
|
|
|
41
41
|
function resolveMobileSlotPorts(target) {
|
|
42
42
|
const ctxOut = resolveMobileRuntimeContext(target);
|
|
43
43
|
if (ctxOut?.trim()) applyKVLines(ctxOut, true);
|
|
44
|
-
const poolOut =
|
|
44
|
+
const poolOut = resolveSlotPortsByRepo(target);
|
|
45
45
|
if (poolOut?.trim()) applyKVLines(poolOut, false);
|
|
46
46
|
const defOut = resolveMobileSlotDefaults(target);
|
|
47
47
|
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
@@ -58,14 +58,12 @@ function resolveExtensionSlotPorts(target) {
|
|
|
58
58
|
process.env["WATCHER_PORT"] = dev;
|
|
59
59
|
process.env["RECIPE_WATCHER_PORT"] = dev;
|
|
60
60
|
}
|
|
61
|
-
if (process.env["CDP_PORT"])
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
if (!process.env["CDP_PORT"] || !process.env["WATCHER_PORT"]) {
|
|
62
|
+
const poolOut = resolveSlotPortsByRepo(target);
|
|
63
|
+
if (poolOut?.trim()) applyKVLines(poolOut, false);
|
|
64
|
+
const defOut = resolveDefaultExtensionPorts(target);
|
|
65
|
+
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
66
66
|
}
|
|
67
|
-
const defOut = resolveDefaultExtensionPorts(target);
|
|
68
|
-
if (defOut?.trim()) applyKVLines(defOut, false);
|
|
69
67
|
}
|
|
70
68
|
function stopExtensionWatcher(target) {
|
|
71
69
|
const runtimeAbs = path.join(target, recipeRuntimeDir());
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,7 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
16
16
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json"] },
|
|
17
17
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir"] },
|
|
18
|
-
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"] },
|
|
18
|
+
{ name: "run", desc: "Execute a proof recipe", args: ["recipe.json"], flags: ["--list"] },
|
|
19
19
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
20
20
|
{ name: "prepare", desc: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
|
|
21
21
|
{ name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
|
package/dist/commands/call.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
runtimeOptionsFromCli,
|
|
15
15
|
usageError
|
|
16
16
|
} from "./parse-args.js";
|
|
17
|
+
import { handleListExecutables } from "./list-executables.js";
|
|
17
18
|
import {
|
|
18
19
|
emitHealViolation,
|
|
19
20
|
executeWithHealBounds,
|
|
@@ -24,6 +25,10 @@ import {
|
|
|
24
25
|
validateRecipeAdapterAware
|
|
25
26
|
} from "./run-engine.js";
|
|
26
27
|
async function handleCall(argv) {
|
|
28
|
+
if (argv.includes("--list")) {
|
|
29
|
+
const { options: options2 } = parseArgs(argv, "call");
|
|
30
|
+
return handleListExecutables("call", options2);
|
|
31
|
+
}
|
|
27
32
|
if (argv.length > 0 && argv[0].startsWith("--")) {
|
|
28
33
|
const message = "call requires <action> first: mm-harness call <action> [--arg k=v ...] [flags]";
|
|
29
34
|
console.error(message);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -44,7 +44,8 @@ async function handleDoctor({ options }) {
|
|
|
44
44
|
} catch {
|
|
45
45
|
}
|
|
46
46
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
47
|
-
|
|
47
|
+
const capture = captureHelperHealth();
|
|
48
|
+
if (json) console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture }, null, 2));
|
|
48
49
|
else {
|
|
49
50
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
50
51
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -67,9 +68,28 @@ async function handleDoctor({ options }) {
|
|
|
67
68
|
);
|
|
68
69
|
console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
|
|
69
70
|
}
|
|
71
|
+
if (capture) {
|
|
72
|
+
console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
|
|
73
|
+
if (capture.status !== "pass") {
|
|
74
|
+
if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
|
|
75
|
+
console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
return result.status === "pass" ? 0 : 1;
|
|
72
80
|
}
|
|
81
|
+
function captureHelperHealth() {
|
|
82
|
+
if (process.platform !== "darwin") return null;
|
|
83
|
+
const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
|
|
84
|
+
try {
|
|
85
|
+
const out = execFileSync(bin, ["doctor", "--json"], { encoding: "utf8", timeout: 1e4, stdio: ["ignore", "pipe", "ignore"] });
|
|
86
|
+
const parsed = JSON.parse(out);
|
|
87
|
+
const failing = (parsed.checks ?? []).filter((c) => c.required === true && c.ok === false).map((c) => c.name ?? c.id ?? "check");
|
|
88
|
+
return { status: parsed.ok === true && failing.length === 0 ? "pass" : "warn", failing };
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
73
93
|
function detectOrphanMetros(target, livePort) {
|
|
74
94
|
try {
|
|
75
95
|
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
4
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from "./shared.js";
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
6
6
|
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
7
7
|
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
8
8
|
"CDP not reachable",
|
|
@@ -49,13 +49,15 @@ async function handleFixtures(argv, deps) {
|
|
|
49
49
|
return exitCode;
|
|
50
50
|
}
|
|
51
51
|
const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
52
|
+
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
53
|
+
`);
|
|
52
54
|
let status;
|
|
53
55
|
if (adapter === "mobile") {
|
|
54
56
|
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
55
57
|
const previousAppRoot = process.env.APP_ROOT;
|
|
56
58
|
process.env.APP_ROOT = target;
|
|
57
59
|
try {
|
|
58
|
-
let result =
|
|
60
|
+
let result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
|
|
59
61
|
if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env["RECIPE_SETUP_WALLET_RETRIED"]) {
|
|
60
62
|
process.env["RECIPE_SETUP_WALLET_RETRIED"] = "1";
|
|
61
63
|
if (!json) process.stderr.write(" setup-wallet: recoverable failure \u2014 restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n");
|
|
@@ -63,7 +65,7 @@ async function handleFixtures(argv, deps) {
|
|
|
63
65
|
const platform = str(options, "platform") ?? process.env["MOBILE_PLATFORM"] ?? "ios";
|
|
64
66
|
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: "auto", clearMetro: true });
|
|
65
67
|
if (relaunchResult.status === 0) {
|
|
66
|
-
result =
|
|
68
|
+
result = await spawnScriptStreaming(setupWalletSh, ["--fixture", fixturePath], target);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
status = result.status === 0 ? "pass" : "fail";
|