@deeeed/metamask-harness 0.5.0 → 0.6.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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -26,7 +26,16 @@ recipe_color() {
26
26
  }
27
27
 
28
28
  recipe_progress_script() {
29
- printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/progress.ts}"
29
+ if [ -n "${RECIPE_PROGRESS_SCRIPT:-}" ]; then
30
+ printf '%s' "$RECIPE_PROGRESS_SCRIPT"
31
+ return 0
32
+ fi
33
+ local root="${RECIPE_RUNNER_ROOT:?}"
34
+ if [ -f "$root/dist/progress.js" ]; then
35
+ printf '%s' "$root/dist/progress.js"
36
+ else
37
+ printf '%s' "$root/src/progress.ts"
38
+ fi
30
39
  }
31
40
 
32
41
  recipe_log_tui_script() {
@@ -34,7 +43,16 @@ recipe_log_tui_script() {
34
43
  }
35
44
 
36
45
  recipe_version_script() {
37
- printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/cli-version.ts}"
46
+ if [ -n "${RECIPE_VERSION_SCRIPT:-}" ]; then
47
+ printf '%s' "$RECIPE_VERSION_SCRIPT"
48
+ return 0
49
+ fi
50
+ local root="${RECIPE_RUNNER_ROOT:?}"
51
+ if [ -f "$root/dist/cli-version.js" ]; then
52
+ printf '%s' "$root/dist/cli-version.js"
53
+ else
54
+ printf '%s' "$root/src/cli-version.ts"
55
+ fi
38
56
  }
39
57
 
40
58
  recipe_log_ui_mode() {
@@ -67,7 +85,7 @@ recipe_instant_hint() {
67
85
  local message="$1"
68
86
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && return 0
69
87
  [ -t 2 ] || return 0
70
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
88
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
71
89
  printf '%s: %s\n' \
72
90
  "$(recipe_color label "$prefix")" \
73
91
  "$(recipe_color dim "$message")" >&2
@@ -92,7 +110,7 @@ recipe_done() {
92
110
  recipe_failure_playbook() {
93
111
  local verb="$1" reason="$2"
94
112
  shift 2
95
- local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-recipe}"
113
+ local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
96
114
  while [ "$#" -gt 0 ]; do
97
115
  case "$1" in
98
116
  --log) log_path="$2"; shift 2 ;;
@@ -106,31 +124,32 @@ recipe_failure_playbook() {
106
124
  "$(recipe_color cmd "$verb")" \
107
125
  "$(recipe_color err failed)" \
108
126
  "$(recipe_color warn "$reason")" >&2
109
- printf '%s\n' "$(recipe_color label next:)" >&2
127
+ printf '%s\n' "$(recipe_color label "next:")" >&2
110
128
  case "$verb" in
111
129
  rebuild|up|watch)
112
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
113
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
114
- printf ' %s\n' "$(recipe_color cmd "$prefix doctor")" >&2
115
- printf ' %s\n' "$(recipe_color cmd "$prefix sync")" >&2
130
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
131
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
132
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
133
+ printf ' %s\n' "$(recipe_color cmd "mm-harness install")" >&2
116
134
  ;;
117
135
  refresh)
118
- printf ' %s\n' "$(recipe_color cmd "$prefix watch")" >&2
119
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
120
- printf ' %s\n' "$(recipe_color cmd "$prefix rebuild")" >&2
136
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
137
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
138
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch --build")" >&2
121
139
  ;;
122
140
  metro|ios|android|setup-wallet)
123
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
124
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
125
- printf ' %s\n' "$(recipe_color cmd "$prefix up")" >&2
141
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
142
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
143
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
126
144
  ;;
127
145
  *)
128
- printf ' %s\n' "$(recipe_color cmd "$prefix status")" >&2
129
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
146
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
147
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
130
148
  ;;
131
149
  esac
132
150
  [ -n "$log_path" ] && printf '%s\n' "$(recipe_color path "log: $log_path")" >&2
133
151
  [ -n "$artifact" ] && printf '%s\n' "$(recipe_color path "artifact: $artifact")" >&2
152
+ return 0
134
153
  }
135
154
 
136
155
  recipe_check_harness_stale() {
@@ -152,12 +171,12 @@ recipe_check_harness_stale() {
152
171
 
153
172
  recipe_warn_harness_stale() {
154
173
  local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
155
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
174
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
156
175
  if recipe_check_harness_stale "$adapter" "$root" "$harness_root" "$runner_root"; then
157
176
  printf '%s: %s - run: %s\n' \
158
177
  "$(recipe_color label "$prefix")" \
159
178
  "$(recipe_color warn 'harness scripts are stale')" \
160
- "$(recipe_color cmd "$prefix sync")" >&2
179
+ "$(recipe_color cmd "mm-harness install")" >&2
161
180
  return 1
162
181
  fi
163
182
  return 0
@@ -190,7 +209,7 @@ recipe_try_farmslot_fixture_sync() {
190
209
  recipe_sync_wallet_fixture() {
191
210
  local root="$1" runtime_dir="$2" cdp_port="${3:-}" seed_script="${4:-}" farmslot_slot="${5:-}"
192
211
  local canonical="$root/$runtime_dir/wallet-fixture.json"
193
- local candidate resolved prefix="${RECIPE_CLI_PREFIX:-recipe}"
212
+ local candidate resolved prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
194
213
 
195
214
  mkdir -p "$root/$runtime_dir"
196
215
  if [ -f "$canonical" ]; then
@@ -239,10 +258,10 @@ recipe_sync_wallet_fixture() {
239
258
  recipe_speed_ladder() {
240
259
  cat >&2 <<'EOF'
241
260
  Speed ladder:
242
- FAST (seconds): recipe watch -> recipe refresh
243
- SLOW (minutes): recipe rebuild
244
- INSPECT: recipe status | recipe ports | recipe logs | recipe logs --full | recipe debug
245
- PROOF: recipe run <recipe.json>
246
- SYNC CONTEXT: recipe sync
261
+ FAST (seconds): mm-harness launch (extension quick relaunch)
262
+ SLOW (minutes): mm-harness launch --build
263
+ INSPECT: mm-harness doctor | mm-harness logs | mm-harness logs --full | mm-harness debug
264
+ PROOF: mm-harness run <recipe.json>
265
+ SYNC CONTEXT: mm-harness fixtures sync
247
266
  EOF
248
267
  }
@@ -21,7 +21,32 @@ deps_ready() {
21
21
  [ -f "$RUNNER_DIR/node_modules/@farmslot/recipe-harness/package.json" ]
22
22
  }
23
23
 
24
+ # Library actions import @deeeed/metamask-harness by package name; a source checkout
25
+ # is not npm-installed into its own node_modules, so symlink self for resolution.
26
+ ensure_self_package_link() {
27
+ local link="$RUNNER_DIR/node_modules/@deeeed/metamask-harness"
28
+ /bin/mkdir -p "$RUNNER_DIR/node_modules/@deeeed"
29
+ if [ ! -e "$link" ]; then
30
+ ln -sf "$RUNNER_DIR" "$link"
31
+ fi
32
+ }
33
+
34
+ ensure_dev_package_condition() {
35
+ if [ -f "$RUNNER_DIR/dist/index.js" ]; then
36
+ return 0
37
+ fi
38
+ if [ ! -f "$RUNNER_DIR/src/index.ts" ]; then
39
+ return 0
40
+ fi
41
+ case " ${NODE_OPTIONS:-} " in
42
+ *" --conditions=development "*) return 0 ;;
43
+ esac
44
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=development"
45
+ }
46
+
24
47
  if deps_ready; then
48
+ ensure_self_package_link
49
+ ensure_dev_package_condition
25
50
  exit 0
26
51
  fi
27
52
 
@@ -39,6 +64,8 @@ if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs"
39
64
  FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
40
65
  node "$RUNNER_DIR/scripts/link-local-farmslot.mjs" >&2 || true
41
66
  if deps_ready; then
67
+ ensure_self_package_link
68
+ ensure_dev_package_condition
42
69
  exit 0
43
70
  fi
44
71
  fi
@@ -54,3 +81,6 @@ if ! deps_ready; then
54
81
  echo "mm-harness: runner dependencies still missing after npm install: $RUNNER_DIR" >&2
55
82
  exit 1
56
83
  fi
84
+
85
+ ensure_self_package_link
86
+ ensure_dev_package_condition
@@ -153,9 +153,9 @@ function readNewContent(file, offset) {
153
153
 
154
154
  function renderHeader(label, logPath, title) {
155
155
  return [
156
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: recipe logs --full', { stream: process.stderr })}`,
156
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: mm-harness logs --full', { stream: process.stderr })}`,
159
159
  '',
160
160
  ];
161
161
  }
@@ -232,7 +232,7 @@ async function watchLog(options) {
232
232
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
233
233
  if (mode !== 'quiet') {
234
234
  const elapsed = Math.round((Date.now() - started) / 1000);
235
- process.stderr.write(`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
235
+ process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
236
236
  }
237
237
  return 0;
238
238
  }
@@ -221,7 +221,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
221
221
  error: 'cdp-unreachable',
222
222
  inspectUrl,
223
223
  configureHint,
224
- hint: `Start the extension runtime first (recipe reopen). Then open chrome://inspect and add ${configureHint}.`,
224
+ hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
225
225
  detail: error instanceof Error ? error.message : String(error),
226
226
  };
227
227
  }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # reap-checkout-metros.sh — kill EVERY Metro/Expo bundler bound to a given
3
+ # checkout, regardless of port. Port-scoped stop leaks a bundler whenever a
4
+ # launch drifts ports or fails to record its pid; this sweeps them by checkout
5
+ # path so `stop` fully cleans up and `launch` starts from zero.
6
+ #
7
+ # reap_checkout_metros <checkout-abs-path> [keep-pid]
8
+ # keep-pid (optional): a pid to spare (the bundler just started/handled).
9
+ reap_checkout_metros() {
10
+ local repo="$1" keep="${2:-}" reaped=""
11
+ [ -n "$repo" ] || return 0
12
+ # Match the bundler's argv: `expo start` / metro under this checkout. ps argv
13
+ # embeds the cwd-launched command; expo/metro run with the repo on the path.
14
+ local pid args
15
+ while IFS= read -r pid; do
16
+ [ -n "$pid" ] || continue
17
+ [ "$pid" = "$keep" ] && continue
18
+ args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
19
+ # Anchor to a path boundary so /repos/mm-1 does not match /repos/mm-10.
20
+ case "$args" in
21
+ *"$repo/"*|*"$repo "*) : ;; # argv references this checkout
22
+ *) continue ;;
23
+ esac
24
+ case "$args" in
25
+ *"expo start"*|*"metro"*|*"react-native/cli"*|*"@react-native-community/cli"*)
26
+ kill "$pid" 2>/dev/null && reaped="$reaped $pid" ;;
27
+ esac
28
+ done <<LIST
29
+ $(pgrep -f "expo start|metro" 2>/dev/null)
30
+ LIST
31
+ if [ -n "$reaped" ]; then
32
+ printf 'Reaped leaked Metro bundler(s) for this checkout:%s\n' "$reaped" >&2
33
+ fi
34
+ return 0
35
+ }
36
+
37
+ # detect_checkout_metros <checkout-abs-path> — print pids of every metro/expo
38
+ # bundler bound to the checkout, one per line, WITHOUT killing. Powers doctor's
39
+ # orphan visibility so leaked bundlers are surfaced without a launch.
40
+ detect_checkout_metros() {
41
+ local repo="$1" pid args
42
+ [ -n "$repo" ] || return 0
43
+ while IFS= read -r pid; do
44
+ [ -n "$pid" ] || continue
45
+ args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
46
+ case "$args" in *"$repo/"*|*"$repo "*) : ;; *) continue ;; esac
47
+ case "$args" in
48
+ *"expo start"*|*"metro"*|*"react-native/cli"*|*"@react-native-community/cli"*) printf '%s\n' "$pid" ;;
49
+ esac
50
+ done <<LIST
51
+ $(pgrep -f "expo start|metro" 2>/dev/null)
52
+ LIST
53
+ }
@@ -0,0 +1,23 @@
1
+ // recipe-harness-root.mjs — overlay harness root path for node leaves (no src/ import).
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
7
+
8
+ function validateRelative(name, value) {
9
+ if (!value || path.isAbsolute(value)) {
10
+ throw new Error(`${name} must be a non-empty relative path: ${value}`);
11
+ }
12
+ return value;
13
+ }
14
+
15
+ function readDefaults() {
16
+ const defaultsPath = path.join(runnerDir, 'adapters/shared/path-defaults.json');
17
+ const parsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf8'));
18
+ return validateRelative('recipeHarnessRoot', parsed.recipeHarnessRoot || 'temp/recipe/harness');
19
+ }
20
+
21
+ export function recipeHarnessRoot() {
22
+ return validateRelative('RECIPE_HARNESS_ROOT', process.env.RECIPE_HARNESS_ROOT || readDefaults());
23
+ }
@@ -0,0 +1,205 @@
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
+ };
@@ -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-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`);