@deeeed/metamask-harness 0.6.3 → 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.
Files changed (34) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/adapters/extension/inject.mjs +5 -0
  3. package/adapters/extension/start-watch.sh +11 -17
  4. package/adapters/manifest.json +57 -9
  5. package/adapters/mobile/lib/tmux-viewer.sh +31 -10
  6. package/adapters/mobile/open-device.sh +14 -1
  7. package/adapters/mobile/stop-metro.sh +1 -1
  8. package/adapters/mobile/yarn-setup.sh +15 -1
  9. package/adapters/shared/activate-repo-ruby.sh +124 -0
  10. package/adapters/shared/open-log-window.sh +55 -0
  11. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
  12. package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
  13. package/adapters/shared/resolve-farmslot-ports.sh +6 -104
  14. package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
  15. package/adapters/shared/resolve-slot-ports.mjs +20 -0
  16. package/adapters/shared/resolve-slot-ports.sh +110 -0
  17. package/adapters/shared/tmux-session.sh +35 -0
  18. package/dist/adapters/mobile/provision.js +25 -2
  19. package/dist/adapters/{resolve-farmslot-ports.js → resolve-slot-ports.js} +4 -2
  20. package/dist/adapters/slot-ports.js +8 -10
  21. package/dist/cli-commands.js +1 -1
  22. package/dist/commands/call.js +5 -0
  23. package/dist/commands/doctor.js +21 -1
  24. package/dist/commands/fixtures.js +5 -3
  25. package/dist/commands/launch/extension.js +18 -0
  26. package/dist/commands/launch/index.js +10 -1
  27. package/dist/commands/list-executables.js +48 -0
  28. package/dist/commands/logs.js +25 -2
  29. package/dist/commands/manifest.js +27 -7
  30. package/dist/commands/parse-args.js +1 -0
  31. package/dist/commands/run.js +3 -4
  32. package/dist/live-adapter-contract.js +30 -9
  33. package/dist/paths.js +4 -1
  34. package/package.json +1 -1
@@ -1,205 +1,3 @@
1
- // Farmslot pool + slot-suffix port resolution pure JS for runner and overlay leaves.
2
-
3
- import fs from 'node:fs';
4
- import os from 'node:os';
5
- import path from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
-
8
- const coreDir = path.dirname(fileURLToPath(import.meta.url));
9
-
10
- function pathDefault(key) {
11
- for (const candidate of [
12
- path.join(coreDir, 'path-defaults.json'),
13
- path.join(coreDir, '../../adapters/shared/path-defaults.json'),
14
- ]) {
15
- if (!fs.existsSync(candidate)) continue;
16
- const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
17
- if (value) return value;
18
- }
19
- throw new Error(`Missing path default: ${key}`);
20
- }
21
-
22
- function recipeRuntimeDir() {
23
- return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
24
- }
25
-
26
- export function formatKvLines(kv) {
27
- const lines = [];
28
- if (kv.CDP_PORT !== undefined) lines.push(`CDP_PORT=${kv.CDP_PORT}`);
29
- if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
30
- if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
31
- if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
32
- return lines.length ? `${lines.join('\n')}\n` : '';
33
- }
34
-
35
- export function realRepoPath(repo) {
36
- if (!repo) return null;
37
- try {
38
- return fs.realpathSync(repo);
39
- } catch {
40
- return null;
41
- }
42
- }
43
-
44
- export function inferSlotSuffix(repo) {
45
- const base = path.basename(repo);
46
- const m = /-(\d+)$/u.exec(base);
47
- return m ? m[1] : null;
48
- }
49
-
50
- function scoreSlot(slot, slotSuffix) {
51
- const resources = slot.resources ?? {};
52
- const cdp = resources.browser?.cdp_port;
53
- const port = resources['dev-server']?.port;
54
- const simulator = resources['ios-sim']?.simulator;
55
- const session = slot.session ?? '';
56
- const slotId = slot.id ?? '';
57
- let score = 0;
58
- if (cdp !== undefined) score += 100;
59
- if (port !== undefined) score += 10;
60
- if (slotSuffix) {
61
- if (session === `mme-${slotSuffix}`) score += 50;
62
- if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
63
- }
64
- if (!slotId.toLowerCase().includes('demo')) score += 5;
65
- return [score, cdp, port, slotId, simulator];
66
- }
67
-
68
- export function resolveFarmslotPortsByRepo(repo) {
69
- const realRepo = realRepoPath(repo);
70
- if (!realRepo) return null;
71
-
72
- const slotSuffix = inferSlotSuffix(realRepo);
73
- const roots = [];
74
- // A slot checkout lives at <workspace>/repos/<slot>; that workspace's own
75
- // pool is the authoritative one for this repo — machine-global fallbacks
76
- // (env root, dev checkout) apply only when the repo is not workspace-shaped.
77
- if (path.basename(path.dirname(realRepo)) === 'repos') {
78
- roots.push(path.join(path.dirname(path.dirname(realRepo)), 'farmslot'));
79
- }
80
- if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
81
- roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
82
-
83
- for (const root of roots) {
84
- if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
85
- const poolDir = path.join(root, 'pool');
86
- let best = null;
87
-
88
- for (const name of fs.readdirSync(poolDir).sort()) {
89
- if (!name.endsWith('.json') || name.includes('.bak.')) continue;
90
- let data;
91
- try {
92
- data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
93
- } catch {
94
- continue;
95
- }
96
- for (const slot of data.slots ?? []) {
97
- const slotRepo = slot.repo ?? '';
98
- if (!slotRepo) continue;
99
- let slotReal;
100
- try {
101
- slotReal = fs.realpathSync(slotRepo);
102
- } catch {
103
- continue;
104
- }
105
- if (slotReal !== realRepo) continue;
106
- const scored = scoreSlot(slot, slotSuffix);
107
- if (!best || scored[0] > best[0]) best = scored;
108
- }
109
- }
110
-
111
- if (best) {
112
- const [, cdp, port, slotId, simulator] = best;
113
- return formatKvLines({
114
- ...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
115
- ...(port !== undefined ? { WATCHER_PORT: port } : {}),
116
- ...(slotId ? { SLOT_ID: slotId } : {}),
117
- ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
118
- });
119
- }
120
- }
121
- return null;
122
- }
123
-
124
- export function resolveDefaultExtensionPorts(repo) {
125
- const n = inferSlotSuffix(repo);
126
- if (!n) return null;
127
- return formatKvLines({
128
- CDP_PORT: 6660 + Number(n),
129
- WATCHER_PORT: 9010 + Number(n),
130
- SLOT_ID: `local-extension-${n}`,
131
- });
132
- }
133
-
134
- export function resolveExtensionRuntimePorts(repo) {
135
- return resolveFarmslotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
136
- }
137
-
138
- export function resolveMobileSlotDefaults(repo) {
139
- const n = inferSlotSuffix(repo);
140
- if (!n) return null;
141
- return formatKvLines({
142
- WATCHER_PORT: 8060 + Number(n),
143
- IOS_SIMULATOR: `mm-${n}`,
144
- SLOT_ID: `local-mobile-${n}`,
145
- });
146
- }
147
-
148
- export function resolveMobileRuntimeContext(repo) {
149
- const ctxPath = path.join(repo, recipeRuntimeDir(), 'agentic-runtime.json');
150
- if (fs.existsSync(ctxPath)) {
151
- try {
152
- const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
153
- if (c.simulator || (c.metroPort != null && c.metroPort !== '')) {
154
- return formatKvLines({
155
- ...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
156
- ...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
157
- ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
158
- });
159
- }
160
- } catch {
161
- /* unreadable context falls through to the provision baseline */
162
- }
163
- }
164
- return resolveMobileProvisionBaseline(repo);
165
- }
166
-
167
- // A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
168
- // provision baseline records the same authoritative simulator/port identity —
169
- // without it, a fresh slot's first launch degrades to the simctl `booted`
170
- // alias and misses the installed dev client entirely.
171
- function resolveMobileProvisionBaseline(repo) {
172
- const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
173
- if (!fs.existsSync(basePath)) return null;
174
- try {
175
- const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
176
- const simulator = c.simulator?.name || c.simulator?.udid;
177
- const watcherPort = c.watcherPort != null && c.watcherPort !== '' ? c.watcherPort : undefined;
178
- if (!simulator && watcherPort === undefined) return null;
179
- return formatKvLines({
180
- ...(watcherPort !== undefined ? { WATCHER_PORT: watcherPort } : {}),
181
- ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
182
- ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
183
- });
184
- } catch {
185
- return null;
186
- }
187
- }
188
-
189
- export function resolveMobileRuntimePorts(repo) {
190
- return (
191
- resolveMobileRuntimeContext(repo)
192
- ?? resolveFarmslotPortsByRepo(repo)
193
- ?? resolveMobileSlotDefaults(repo)
194
- ?? ''
195
- );
196
- }
197
-
198
- export const cliFns = {
199
- resolve_farmslot_ports_by_repo: resolveFarmslotPortsByRepo,
200
- resolve_default_extension_ports: resolveDefaultExtensionPorts,
201
- resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
202
- resolve_mobile_runtime_context: resolveMobileRuntimeContext,
203
- resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
204
- resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
205
- };
1
+ // Back-compat shim: the port-resolution core is now resolve-slot-ports-core.mjs.
2
+ // Re-exports it verbatim for one release so importers of the old path keep resolving.
3
+ export * from './resolve-slot-ports-core.mjs';
@@ -1,20 +1,5 @@
1
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-farmslot-ports-core.mjs';
5
-
6
- const [, , fn, repo] = process.argv;
7
- if (!fn || fn === '-h' || fn === '--help') {
8
- process.stderr.write('Usage: resolve-farmslot-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-farmslot-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`);
2
+ // Back-compat shim: this leaf is now resolve-slot-ports.mjs. Delegates to it for one
3
+ // release so `node resolve-farmslot-ports.mjs <fn> <repo>` keeps working (the new CLI
4
+ // reads the same argv positions on import).
5
+ import './resolve-slot-ports.mjs';
@@ -1,105 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # resolve-farmslot-ports.sh bash-compat wrapper over resolve-farmslot-ports.mjs
3
- #
4
- # Farmslot pool JSON is authoritative when a slot repo matches; otherwise fall back
5
- # to the local-extension-N formula (6660+N / 9010+N).
6
- #
7
- # Usage:
8
- # . resolve-farmslot-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-farmslot-ports.mjs"
14
-
15
- _resolve_ports_cli() {
16
- node "$_RESOLVE_PORTS_MJS" "$1" "${2:-.}"
17
- }
18
-
19
- resolve_farmslot_ports_by_repo() {
20
- _resolve_ports_cli resolve_farmslot_ports_by_repo "$1"
21
- }
22
-
23
- infer_extension_slot_suffix() {
24
- local repo="$1" base
25
- base="$(basename "$repo")"
26
- if [[ "$base" =~ -([0-9]+)$ ]]; then
27
- printf '%s' "${BASH_REMATCH[1]}"
28
- return 0
29
- fi
30
- return 1
31
- }
32
-
33
- resolve_default_extension_ports() {
34
- _resolve_ports_cli resolve_default_extension_ports "$1"
35
- }
36
-
37
- resolve_extension_runtime_ports() {
38
- _resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
39
- }
40
-
41
- apply_resolved_extension_ports() {
42
- local repo="${1:-.}" line key val from_pool=false resolved=""
43
- if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
44
- from_pool=true
45
- elif resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)"; then
46
- from_pool=false
47
- else
48
- return 0
49
- fi
50
- while IFS= read -r line; do
51
- [ -n "$line" ] || continue
52
- key="${line%%=*}"
53
- val="${line#*=}"
54
- case "$key" in
55
- CDP_PORT)
56
- if [ "$from_pool" = true ] || [ -z "${CDP_PORT:-}" ]; then CDP_PORT="$val"; fi
57
- ;;
58
- WATCHER_PORT)
59
- if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
60
- ;;
61
- SLOT_ID)
62
- if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
63
- ;;
64
- esac
65
- done <<< "$resolved"
66
- }
67
-
68
- resolve_mobile_slot_defaults() {
69
- _resolve_ports_cli resolve_mobile_slot_defaults "$1"
70
- }
71
-
72
- resolve_mobile_runtime_context() {
73
- _resolve_ports_cli resolve_mobile_runtime_context "$1"
74
- }
75
-
76
- resolve_mobile_runtime_ports() {
77
- _resolve_ports_cli resolve_mobile_runtime_ports "${1:-.}"
78
- }
79
-
80
- apply_resolved_mobile_ports() {
81
- local repo="${1:-.}" line key val from_pool=false resolved=""
82
- if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
83
- from_pool=true
84
- elif resolved="$(_resolve_ports_cli resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
85
- from_pool=false
86
- else
87
- return 0
88
- fi
89
- while IFS= read -r line; do
90
- [ -n "$line" ] || continue
91
- key="${line%%=*}"
92
- val="${line#*=}"
93
- case "$key" in
94
- WATCHER_PORT)
95
- if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
96
- ;;
97
- IOS_SIMULATOR)
98
- if [ "$from_pool" = true ] || [ -z "${IOS_SIMULATOR:-}" ]; then IOS_SIMULATOR="$val"; fi
99
- ;;
100
- SLOT_ID)
101
- if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
102
- ;;
103
- esac
104
- done <<< "$resolved"
105
- }
2
+ # Back-compat shim: this wrapper is now resolve-slot-ports.sh. Sources it for one
3
+ # release so old callers keep the same sourced function names (including the
4
+ # resolve_farmslot_ports_by_repo alias defined there).
5
+ _SHIM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ # shellcheck disable=SC1091
7
+ . "$_SHIM_DIR/resolve-slot-ports.sh"
@@ -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
+ }