@deeeed/metamask-harness 0.14.5 → 0.15.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 +96 -6
- package/adapters/extension/ensure-browser.sh +4 -2
- package/adapters/extension/launch-browser.cjs +73 -34
- package/adapters/extension/live.sh +21 -10
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/sidepanel-toggle.sh +10 -4
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/verify.sh +19 -2
- package/adapters/extension/wallet-fixture-state.cjs +14 -1
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
- package/adapters/mobile/launch-metro.cjs +74 -0
- package/adapters/mobile/lib/tmux-viewer.sh +4 -4
- package/adapters/mobile/open-device.sh +3 -1
- package/adapters/mobile/start-metro.sh +12 -18
- package/adapters/mobile/stop-metro.sh +1 -0
- package/adapters/mobile/verify.sh +5 -5
- package/adapters/shared/install-repo-deps.sh +29 -0
- package/adapters/shared/open-debug.mjs +35 -4
- package/adapters/shared/open-log-window.sh +1 -1
- package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
- package/adapters/shared/resolve-slot-ports.sh +21 -17
- package/adapters/shared/tmux-session.sh +0 -5
- package/adapters/shared/tmux-viewer.sh +7 -2
- package/dist/adapters/core/surface.js +3 -0
- package/dist/adapters/extension/ensure-ready.js +0 -7
- package/dist/adapters/extension/runtime-decision.js +93 -3
- package/dist/adapters/extension/surface.js +17 -6
- package/dist/adapters/mobile/metro-env.js +67 -0
- package/dist/adapters/mobile/perps-env.js +101 -0
- package/dist/adapters/mobile/prepare.js +32 -14
- package/dist/adapters/mobile/runtime-decision.js +21 -2
- package/dist/adapters/mobile/source-freshness.js +116 -0
- package/dist/adapters/mobile/surface.js +5 -1
- package/dist/adapters/resolve-slot-ports.js +4 -0
- package/dist/adapters/slot-ports.js +131 -49
- package/dist/adapters.js +7 -2
- package/dist/checkout-lock.js +72 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -0
- package/dist/commands/call.js +91 -54
- package/dist/commands/check.js +266 -46
- package/dist/commands/checklist.js +136 -0
- package/dist/commands/debug.js +21 -2
- package/dist/commands/doctor.js +233 -18
- package/dist/commands/farmslot-ready.js +25 -0
- package/dist/commands/fixtures.js +177 -42
- package/dist/commands/launch/extension.js +6 -1
- package/dist/commands/launch/index.js +54 -22
- package/dist/commands/logs.js +24 -4
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run-engine.js +223 -7
- package/dist/commands/run.js +67 -50
- package/dist/commands/shared.js +86 -5
- package/dist/commands/stop.js +1 -2
- package/dist/doctor.js +39 -17
- package/dist/harness.js +5 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/mm-harness-cli.js +41 -12
- package/dist/runner.js +35 -5
- package/dist/runtime-context.js +228 -0
- package/docs/CLI-SPEC.md +15 -11
- package/docs/MENTAL-MODEL.md +6 -6
- package/library/actions/extension/perps/perps.mjs +29 -8
- package/library/actions/extension/platform/cdp.mjs +128 -28
- package/library/actions/extension/ui/navigate.mjs +1 -1
- package/library/actions/mobile/perps/perps.mjs +13 -1
- package/library/actions/mobile/platform/bridge.mjs +302 -29
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
- package/library/manifests/extension.action-manifest.json +32 -12
- package/library/manifests/mobile.action-manifest.json +25 -3
- package/package.json +4 -4
- package/scripts/completions.sh +7 -4
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
# resolved, skip the viewer entirely (Metro still runs detached to the log).
|
|
12
12
|
#
|
|
13
13
|
# Inputs (env/args): $1 = metro log path; PORT, LOG_DIR, and optional `slot` from
|
|
14
|
-
# the caller
|
|
14
|
+
# the caller.
|
|
15
15
|
|
|
16
|
-
# Session resolution (RECIPE_TMUX_SESSION → agentic-runtime.json session
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# Session resolution (RECIPE_TMUX_SESSION → agentic-runtime.json session). Sourced
|
|
17
|
+
# relative to this lib; harness-path fallback for the installed layout where shared
|
|
18
|
+
# libs live beside this file.
|
|
19
19
|
_tv_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
20
20
|
# shellcheck disable=SC1091
|
|
21
21
|
for _tv_src in "$_tv_dir/tmux-session.sh" "$_tv_dir/../../shared/tmux-session.sh"; do
|
|
@@ -220,7 +220,9 @@ if [ "$PLATFORM" = "ios" ]; then
|
|
|
220
220
|
DEV_CLIENT_SCHEME="${IOS_DEV_CLIENT_SCHEME:-expo-metamask}"
|
|
221
221
|
|
|
222
222
|
boot_simulator_if_needed "$SIM_TARGET" || exit 1
|
|
223
|
-
|
|
223
|
+
if [ "${MM_HARNESS_FOCUS_SIMULATOR:-0}" = "1" ]; then
|
|
224
|
+
show_simulator "$SIM_TARGET"
|
|
225
|
+
fi
|
|
224
226
|
|
|
225
227
|
LAUNCHED=false
|
|
226
228
|
for BUNDLE_ID in "${BUNDLE_IDS[@]}"; do
|
|
@@ -89,11 +89,8 @@ start_console_forwarder() {
|
|
|
89
89
|
# Only one debugger can own a React Native page. Reap any forwarder already
|
|
90
90
|
# attached to this Metro port, even if it was launched from another checkout or
|
|
91
91
|
# package install path and therefore does not own this runtime dir's pid file.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
pid="${line%% *}"
|
|
95
|
-
cmd="${line#* }"
|
|
96
|
-
[ "$pid" != "$line" ] || continue
|
|
92
|
+
local pid cmd
|
|
93
|
+
while read -r pid cmd; do
|
|
97
94
|
case "$cmd" in
|
|
98
95
|
*console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
|
|
99
96
|
kill "$pid" 2>/dev/null || true
|
|
@@ -227,19 +224,16 @@ if ! start_metro_tmux; then
|
|
|
227
224
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
228
225
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
229
226
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
echo "$metro_pid" > "$PID_FILE"
|
|
241
|
-
disown "$metro_pid" 2>/dev/null || true
|
|
242
|
-
)
|
|
227
|
+
launcher_args=(
|
|
228
|
+
--target "$TARGET"
|
|
229
|
+
--port "$PORT"
|
|
230
|
+
--log "$LOG_FILE"
|
|
231
|
+
--pid-file "$PID_FILE"
|
|
232
|
+
--workers "$METRO_WORKERS"
|
|
233
|
+
)
|
|
234
|
+
[ "$CLEAR" = true ] && launcher_args+=(--clear)
|
|
235
|
+
node "$SCRIPT_DIR/launch-metro.cjs" "${launcher_args[@]}"
|
|
236
|
+
) || exit 1
|
|
243
237
|
|
|
244
238
|
start_viewer_window "$LOG_FILE"
|
|
245
239
|
fi
|
|
@@ -61,6 +61,7 @@ fi
|
|
|
61
61
|
# incidental non-zero (platform-dependent) can never turn success into exit 1.
|
|
62
62
|
set +e
|
|
63
63
|
rm -f "$PID_FILE"
|
|
64
|
+
rm -f "$LOG_DIR/metro-launch.json"
|
|
64
65
|
|
|
65
66
|
# start-metro runs a per-checkout console-forwarder holding the device debugger
|
|
66
67
|
# slot; Metro going down must take it too, or the orphan keeps polling and later
|
|
@@ -283,7 +283,7 @@ const fs = require('fs');
|
|
|
283
283
|
const path = require('path');
|
|
284
284
|
const name = 'agentic overlay matches runner (HUD freshness)';
|
|
285
285
|
const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
|
|
286
|
-
const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', '
|
|
286
|
+
const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'dev-tools', 'AgenticService');
|
|
287
287
|
function readTrimmed(file) {
|
|
288
288
|
try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
|
|
289
289
|
}
|
|
@@ -298,7 +298,7 @@ if (!runnerDir) {
|
|
|
298
298
|
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
|
|
299
299
|
process.exit(0);
|
|
300
300
|
}
|
|
301
|
-
const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', '
|
|
301
|
+
const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'dev-tools', 'AgenticService');
|
|
302
302
|
let checked = 0;
|
|
303
303
|
const drifted = [];
|
|
304
304
|
const missing = [];
|
|
@@ -318,13 +318,13 @@ try {
|
|
|
318
318
|
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
|
|
319
319
|
process.exit(0);
|
|
320
320
|
}
|
|
321
|
-
if (
|
|
322
|
-
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
323
|
-
} else if (drifted.length || missing.length) {
|
|
321
|
+
if (drifted.length || missing.length) {
|
|
324
322
|
const parts = [];
|
|
325
323
|
if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
|
|
326
324
|
if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
|
|
327
325
|
process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
|
|
326
|
+
} else if (checked === 0) {
|
|
327
|
+
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
328
328
|
} else {
|
|
329
329
|
process.stdout.write(JSON.stringify({ name, status: 'pass' }));
|
|
330
330
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
TARGET=""
|
|
5
|
+
while [[ $# -gt 0 ]]; do
|
|
6
|
+
case "$1" in
|
|
7
|
+
--target)
|
|
8
|
+
[[ $# -ge 2 ]] || { echo "Usage: install-repo-deps.sh --target <checkout>" >&2; exit 2; }
|
|
9
|
+
TARGET="$2"
|
|
10
|
+
shift 2
|
|
11
|
+
;;
|
|
12
|
+
-h|--help)
|
|
13
|
+
echo "Usage: install-repo-deps.sh --target <checkout>"
|
|
14
|
+
exit 0
|
|
15
|
+
;;
|
|
16
|
+
*) echo "Unknown flag: $1" >&2; exit 2 ;;
|
|
17
|
+
esac
|
|
18
|
+
done
|
|
19
|
+
|
|
20
|
+
[[ -n "$TARGET" ]] || { echo "install-repo-deps: --target is required" >&2; exit 2; }
|
|
21
|
+
TARGET="$(cd "$TARGET" && pwd)"
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
23
|
+
|
|
24
|
+
# shellcheck disable=SC1091
|
|
25
|
+
. "$SCRIPT_DIR/activate-repo-node.sh"
|
|
26
|
+
require_repo_node "$TARGET"
|
|
27
|
+
|
|
28
|
+
cd "$TARGET"
|
|
29
|
+
corepack yarn install --immutable
|
|
@@ -75,6 +75,12 @@ function emit(result, json) {
|
|
|
75
75
|
if (result.ok) {
|
|
76
76
|
const detail = result.method ? ` via ${result.method}` : '';
|
|
77
77
|
const where = result.port ? ` (port ${result.port})` : result.cdpPort ? ` (cdp ${result.cdpPort})` : '';
|
|
78
|
+
if (result.opened === false) {
|
|
79
|
+
if (result.endpoint) console.log(`Resolved debugger endpoint${detail}${where}: ${result.endpoint}`);
|
|
80
|
+
else console.log(`Debugger action not invoked${detail}${where}`);
|
|
81
|
+
if (result.detail) console.log(result.detail);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
78
84
|
console.log(`Opened debugger${detail}${where}`);
|
|
79
85
|
if (result.target?.url) console.log(`Target: ${result.target.url}`);
|
|
80
86
|
if (result.configureHint) console.log(`Configure chrome://inspect target: ${result.configureHint}`);
|
|
@@ -144,16 +150,30 @@ async function openMobileDebugger(port, action, shouldOpen) {
|
|
|
144
150
|
if (!Number.isInteger(numericPort) || numericPort <= 0) {
|
|
145
151
|
return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
|
|
146
152
|
}
|
|
153
|
+
const endpoint = `http://127.0.0.1:${numericPort}${urlPath}`;
|
|
154
|
+
if (!shouldOpen) {
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
adapter: 'mobile',
|
|
158
|
+
action: normalizedAction,
|
|
159
|
+
method: 'metro-http',
|
|
160
|
+
endpoint,
|
|
161
|
+
port: numericPort,
|
|
162
|
+
opened: false,
|
|
163
|
+
detail: 'Open action not invoked (--no-open).',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
147
166
|
|
|
148
167
|
let metroError = null;
|
|
149
168
|
try {
|
|
150
|
-
const opened = await fetchOk(
|
|
169
|
+
const opened = await fetchOk(endpoint, { method: 'POST' });
|
|
151
170
|
if (opened) {
|
|
152
171
|
return {
|
|
153
172
|
ok: true,
|
|
154
173
|
adapter: 'mobile',
|
|
155
174
|
action: normalizedAction,
|
|
156
175
|
method: 'metro-http',
|
|
176
|
+
endpoint,
|
|
157
177
|
port: numericPort,
|
|
158
178
|
opened: shouldOpen,
|
|
159
179
|
};
|
|
@@ -171,6 +191,9 @@ async function openMobileDebugger(port, action, shouldOpen) {
|
|
|
171
191
|
adapter: 'mobile',
|
|
172
192
|
action: normalizedAction,
|
|
173
193
|
method: 'cdp-eval',
|
|
194
|
+
endpoint: normalizedAction === 'dev-menu'
|
|
195
|
+
? 'NativeModules.DevSettings.show()'
|
|
196
|
+
: 'NativeModules.DevSettings.openDebugger()',
|
|
174
197
|
port: numericPort,
|
|
175
198
|
opened: shouldOpen,
|
|
176
199
|
};
|
|
@@ -181,6 +204,7 @@ async function openMobileDebugger(port, action, shouldOpen) {
|
|
|
181
204
|
adapter: 'mobile',
|
|
182
205
|
action: normalizedAction,
|
|
183
206
|
method: 'metro-http',
|
|
207
|
+
endpoint,
|
|
184
208
|
port: numericPort,
|
|
185
209
|
error: metroError,
|
|
186
210
|
hint: 'Metro may be down or the debug target is not ready. Check metro status and try again.',
|
|
@@ -214,9 +238,11 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
214
238
|
|
|
215
239
|
const inspectUrl = 'chrome://inspect/#devices';
|
|
216
240
|
const configureHint = `127.0.0.1:${numericPort}`;
|
|
241
|
+
const versionEndpoint = `http://127.0.0.1:${numericPort}/json/version`;
|
|
242
|
+
const listEndpoint = `http://127.0.0.1:${numericPort}/json/list`;
|
|
217
243
|
|
|
218
244
|
try {
|
|
219
|
-
await fetchJson(
|
|
245
|
+
await fetchJson(versionEndpoint);
|
|
220
246
|
} catch (error) {
|
|
221
247
|
return {
|
|
222
248
|
ok: false,
|
|
@@ -224,6 +250,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
224
250
|
targetKind,
|
|
225
251
|
cdpPort: numericPort,
|
|
226
252
|
error: 'cdp-unreachable',
|
|
253
|
+
endpoint: versionEndpoint,
|
|
227
254
|
inspectUrl,
|
|
228
255
|
configureHint,
|
|
229
256
|
hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
|
|
@@ -233,7 +260,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
233
260
|
|
|
234
261
|
let targets;
|
|
235
262
|
try {
|
|
236
|
-
targets = await fetchJson(
|
|
263
|
+
targets = await fetchJson(listEndpoint);
|
|
237
264
|
} catch (error) {
|
|
238
265
|
return {
|
|
239
266
|
ok: false,
|
|
@@ -241,6 +268,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
241
268
|
targetKind,
|
|
242
269
|
cdpPort: numericPort,
|
|
243
270
|
error: 'cdp-list-failed',
|
|
271
|
+
endpoint: listEndpoint,
|
|
244
272
|
inspectUrl,
|
|
245
273
|
configureHint,
|
|
246
274
|
hint: `CDP is up but /json/list failed. Open chrome://inspect and add ${configureHint}.`,
|
|
@@ -255,6 +283,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
255
283
|
targetKind,
|
|
256
284
|
cdpPort: numericPort,
|
|
257
285
|
error: 'cdp-list-invalid',
|
|
286
|
+
endpoint: listEndpoint,
|
|
258
287
|
inspectUrl,
|
|
259
288
|
configureHint,
|
|
260
289
|
hint: `Open chrome://inspect and add ${configureHint}.`,
|
|
@@ -269,6 +298,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
269
298
|
adapter: 'extension',
|
|
270
299
|
targetKind,
|
|
271
300
|
method: 'devtools-frontend',
|
|
301
|
+
endpoint: picked.devtoolsFrontendUrl,
|
|
272
302
|
cdpPort: numericPort,
|
|
273
303
|
target: {
|
|
274
304
|
type: picked.type,
|
|
@@ -291,6 +321,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
|
|
|
291
321
|
adapter: 'extension',
|
|
292
322
|
targetKind,
|
|
293
323
|
method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'chrome-inspect',
|
|
324
|
+
endpoint: picked?.devtoolsFrontendUrl || inspectUrl,
|
|
294
325
|
cdpPort: numericPort,
|
|
295
326
|
target: picked
|
|
296
327
|
? { type: picked.type, url: picked.url, title: picked.title || null }
|
|
@@ -341,4 +372,4 @@ main().catch((error) => {
|
|
|
341
372
|
console.error(payload.error);
|
|
342
373
|
}
|
|
343
374
|
process.exit(1);
|
|
344
|
-
});
|
|
375
|
+
});
|
|
@@ -45,7 +45,7 @@ session=""
|
|
|
45
45
|
command -v resolve_run_tmux_session >/dev/null 2>&1 && session="$(resolve_run_tmux_session "$RUNTIME_DIR")"
|
|
46
46
|
if [ -z "$session" ] || ! tmux has-session -t "=$session" 2>/dev/null; then
|
|
47
47
|
echo "open-log-window: no run-owned tmux session to place the window in." >&2
|
|
48
|
-
echo " Next:
|
|
48
|
+
echo " Next: set RECIPE_TMUX_SESSION=<session>, or configure agentic-runtime.json session, then retry" >&2
|
|
49
49
|
exit 1
|
|
50
50
|
fi
|
|
51
51
|
|
|
@@ -139,8 +139,77 @@ export function resolveDefaultExtensionPorts(repo) {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
function parseKvLines(output) {
|
|
143
|
+
if (!output?.trim()) return {};
|
|
144
|
+
const kv = {};
|
|
145
|
+
for (const line of output.split('\n')) {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed) continue;
|
|
148
|
+
const eq = trimmed.indexOf('=');
|
|
149
|
+
if (eq <= 0) continue;
|
|
150
|
+
const key = trimmed.slice(0, eq);
|
|
151
|
+
const val = trimmed.slice(eq + 1);
|
|
152
|
+
if (val === 'null' || val === 'undefined' || val === '') continue;
|
|
153
|
+
kv[key] = val;
|
|
154
|
+
}
|
|
155
|
+
return kv;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mergeKvLayers(...layers) {
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const layer of layers) {
|
|
161
|
+
const source = typeof layer === 'string' ? layer : layer?.source;
|
|
162
|
+
const overwrite = typeof layer === 'string' ? false : Boolean(layer?.overwrite);
|
|
163
|
+
const kv = parseKvLines(source);
|
|
164
|
+
for (const [key, val] of Object.entries(kv)) {
|
|
165
|
+
if (overwrite || result[key] === undefined) result[key] = val;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return Object.keys(result).length ? formatKvLines(result) : '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function extensionRuntimeContextPath(repo) {
|
|
172
|
+
const repoRoot = path.resolve(repo);
|
|
173
|
+
const envCtx = process.env.RECIPE_RUNTIME_CONTEXT;
|
|
174
|
+
if (envCtx) {
|
|
175
|
+
const resolved = path.resolve(envCtx);
|
|
176
|
+
if (resolved === repoRoot || resolved.startsWith(`${repoRoot}${path.sep}`)) {
|
|
177
|
+
return resolved;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return path.join(repoRoot, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function resolveExtensionRuntimeContext(repo) {
|
|
184
|
+
const ctxPath = extensionRuntimeContextPath(repo);
|
|
185
|
+
if (!fs.existsSync(ctxPath)) return null;
|
|
186
|
+
try {
|
|
187
|
+
const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
|
|
188
|
+
const cdp = c.cdpPort;
|
|
189
|
+
const watcher = c.devServerPort ?? c.watcherPort ?? c.metroPort;
|
|
190
|
+
if ((cdp == null || cdp === '') && (watcher == null || watcher === '') && !c.slotId) return null;
|
|
191
|
+
return formatKvLines({
|
|
192
|
+
...(cdp != null && cdp !== '' ? { CDP_PORT: cdp } : {}),
|
|
193
|
+
...(watcher != null && watcher !== '' ? { WATCHER_PORT: watcher } : {}),
|
|
194
|
+
...(c.slotId ? { SLOT_ID: c.slotId } : {}),
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Checkout-scoped extension ports: runtime context (authoritative) → pool → suffix formula.
|
|
202
|
+
export function formatExtensionSlotPorts(repo) {
|
|
203
|
+
const realRepo = path.resolve(repo);
|
|
204
|
+
return mergeKvLayers(
|
|
205
|
+
{ source: resolveExtensionRuntimeContext(realRepo), overwrite: true },
|
|
206
|
+
{ source: resolveSlotPortsByRepo(realRepo), overwrite: false },
|
|
207
|
+
{ source: resolveDefaultExtensionPorts(realRepo), overwrite: false },
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
142
211
|
export function resolveExtensionRuntimePorts(repo) {
|
|
143
|
-
return
|
|
212
|
+
return formatExtensionSlotPorts(repo);
|
|
144
213
|
}
|
|
145
214
|
|
|
146
215
|
export function resolveMobileSlotDefaults(repo) {
|
|
@@ -216,6 +285,8 @@ export const cliFns = {
|
|
|
216
285
|
// Back-compat cli key for one release.
|
|
217
286
|
resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
|
|
218
287
|
resolve_default_extension_ports: resolveDefaultExtensionPorts,
|
|
288
|
+
resolve_extension_runtime_context: resolveExtensionRuntimeContext,
|
|
289
|
+
resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
|
|
219
290
|
resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
|
|
220
291
|
resolve_mobile_runtime_context: resolveMobileRuntimeContext,
|
|
221
292
|
resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
|
|
@@ -43,33 +43,37 @@ resolve_extension_runtime_ports() {
|
|
|
43
43
|
_resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
resolve_extension_slot_ports() {
|
|
47
|
+
_resolve_ports_cli resolve_extension_slot_ports "${1:-.}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Apply the checkout-scoped extension port ladder (context → pool → formula).
|
|
51
|
+
apply_extension_slot_ports() {
|
|
52
|
+
local repo="${1:-.}" line key val resolved="" ctx_file="" runtime_rel="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
|
|
53
|
+
repo="$(cd "$repo" && pwd)"
|
|
54
|
+
ctx_file="$repo/$runtime_rel/agentic-runtime.json"
|
|
55
|
+
if [ -f "$ctx_file" ]; then
|
|
56
|
+
export RECIPE_RUNTIME_CONTEXT="$ctx_file"
|
|
52
57
|
else
|
|
53
|
-
|
|
58
|
+
unset RECIPE_RUNTIME_CONTEXT
|
|
54
59
|
fi
|
|
60
|
+
resolved="$(_resolve_ports_cli resolve_extension_slot_ports "$repo" 2>/dev/null)" || return 0
|
|
55
61
|
while IFS= read -r line; do
|
|
56
62
|
[ -n "$line" ] || continue
|
|
57
63
|
key="${line%%=*}"
|
|
58
64
|
val="${line#*=}"
|
|
59
65
|
case "$key" in
|
|
60
|
-
CDP_PORT)
|
|
61
|
-
|
|
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
|
-
;;
|
|
66
|
+
CDP_PORT) CDP_PORT="$val" ;;
|
|
67
|
+
WATCHER_PORT) WATCHER_PORT="$val" ;;
|
|
68
|
+
SLOT_ID) RECIPE_SLOT_ID="$val" ;;
|
|
69
69
|
esac
|
|
70
70
|
done <<< "$resolved"
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
apply_resolved_extension_ports() {
|
|
74
|
+
apply_extension_slot_ports "$@"
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
resolve_mobile_slot_defaults() {
|
|
74
78
|
_resolve_ports_cli resolve_mobile_slot_defaults "$1"
|
|
75
79
|
}
|
|
@@ -107,4 +111,4 @@ apply_resolved_mobile_ports() {
|
|
|
107
111
|
;;
|
|
108
112
|
esac
|
|
109
113
|
done <<< "$resolved"
|
|
110
|
-
}
|
|
114
|
+
}
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
# tail -F) in the wrong place. Resolution ladder, first hit wins:
|
|
8
8
|
# 1. RECIPE_TMUX_SESSION — the orchestrator's explicit hand-off.
|
|
9
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
10
|
# Prints the resolved name (empty when none). Callers still gate on has-session
|
|
13
11
|
# before creating a window.
|
|
14
12
|
|
|
@@ -28,8 +26,5 @@ resolve_run_tmux_session() {
|
|
|
28
26
|
return 0
|
|
29
27
|
fi
|
|
30
28
|
fi
|
|
31
|
-
if [ -n "${TMUX:-}" ]; then
|
|
32
|
-
tmux display-message -p '#S' 2>/dev/null || true
|
|
33
|
-
fi
|
|
34
29
|
return 0
|
|
35
30
|
}
|
|
@@ -53,9 +53,14 @@ tmux_viewer_close_marker() {
|
|
|
53
53
|
local marker_file="$1"
|
|
54
54
|
command -v tmux >/dev/null 2>&1 || { rm -f "$marker_file"; return 0; }
|
|
55
55
|
if [ -f "$marker_file" ]; then
|
|
56
|
-
local target
|
|
56
|
+
local target marker_session owned_session runtime_dir
|
|
57
57
|
target="$(cat "$marker_file" 2>/dev/null || true)"
|
|
58
|
-
|
|
58
|
+
marker_session="${target%%:*}"
|
|
59
|
+
runtime_dir="$(dirname "$marker_file")"
|
|
60
|
+
owned_session="$(resolve_run_tmux_session "$runtime_dir")"
|
|
61
|
+
if [ -n "$target" ] && [ -n "$owned_session" ] && [ "$marker_session" = "$owned_session" ]; then
|
|
62
|
+
tmux kill-window -t "$target" >/dev/null 2>&1 || true
|
|
63
|
+
fi
|
|
59
64
|
fi
|
|
60
65
|
rm -f "$marker_file"
|
|
61
66
|
}
|
|
@@ -27,12 +27,6 @@ async function closeTab(port, id) {
|
|
|
27
27
|
} catch {
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async function activateTab(port, id) {
|
|
31
|
-
try {
|
|
32
|
-
await fetch(`http://127.0.0.1:${port}/json/activate/${id}`, { signal: AbortSignal.timeout(4e3) });
|
|
33
|
-
} catch {
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
30
|
function isStrayTab(url) {
|
|
37
31
|
if (typeof url !== "string") return false;
|
|
38
32
|
return /^chrome:\/\/newtab\/?$/.test(url) || /^chrome:\/\/new-tab-page\b/.test(url) || url === "about:blank";
|
|
@@ -131,7 +125,6 @@ async function ensureExtensionReady(target, options) {
|
|
|
131
125
|
for (const t of convergedTargets) {
|
|
132
126
|
if (t.type === "page" && t.id && isStrayTab(t.url)) await closeTab(cdpPort, String(t.id));
|
|
133
127
|
}
|
|
134
|
-
if (homesNow[0]?.id) await activateTab(cdpPort, String(homesNow[0].id));
|
|
135
128
|
}
|
|
136
129
|
const finalListing = await jsonList(cdpPort);
|
|
137
130
|
if (!finalListing.ok) {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
INSTALL_MARKERS,
|
|
8
8
|
recordDepsBaseline
|
|
9
9
|
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
10
|
-
import { recipeHarnessPath, recipeWatchLogCandidates } from "../../paths.js";
|
|
10
|
+
import { recipeHarnessPath, recipeRuntimeDir, recipeWatchLogCandidates } from "../../paths.js";
|
|
11
11
|
const WEBPACK_DIRECT_INPUTS = ["package.json", "yarn.lock", ".yarnrc.yml", ".tool-versions"];
|
|
12
12
|
const WEBPACK_RECURSIVE_INPUTS = ["development/webpack"];
|
|
13
13
|
const WEBPACK_CACHE_DIR = "node_modules/.cache/webpack";
|
|
@@ -152,6 +152,80 @@ function latestDistMtime(target) {
|
|
|
152
152
|
walk2(distDir);
|
|
153
153
|
return latest;
|
|
154
154
|
}
|
|
155
|
+
function runtimeDistCheck(target) {
|
|
156
|
+
const dist = path.join(target, "dist/chrome");
|
|
157
|
+
const runtimeDist = path.join(
|
|
158
|
+
target,
|
|
159
|
+
recipeRuntimeDir(),
|
|
160
|
+
process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"
|
|
161
|
+
);
|
|
162
|
+
if (!fs.existsSync(path.join(runtimeDist, "manifest.json"))) return { status: "missing" };
|
|
163
|
+
let output;
|
|
164
|
+
try {
|
|
165
|
+
output = execFileSync("rsync", [
|
|
166
|
+
"-rni",
|
|
167
|
+
"--exclude",
|
|
168
|
+
"_metadata",
|
|
169
|
+
"--out-format=%n",
|
|
170
|
+
`${dist}${path.sep}`,
|
|
171
|
+
`${runtimeDist}${path.sep}`
|
|
172
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code === "ENOENT") {
|
|
175
|
+
try {
|
|
176
|
+
const modified2 = filesystemSnapshotDiff(dist, runtimeDist).slice(0, 10);
|
|
177
|
+
return modified2.length > 0 ? { status: "stale", comparison: "filesystem-quick", modified: modified2 } : { status: "fresh", comparison: "filesystem-quick" };
|
|
178
|
+
} catch (fallbackError) {
|
|
179
|
+
return {
|
|
180
|
+
status: "unknown",
|
|
181
|
+
error: `snapshot comparison failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const status = error.status;
|
|
186
|
+
return {
|
|
187
|
+
status: "unknown",
|
|
188
|
+
error: `rsync snapshot comparison failed${typeof status === "number" ? ` with exit ${status}` : ""}`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 10);
|
|
192
|
+
return modified.length > 0 ? { status: "stale", comparison: "rsync-quick", modified } : { status: "fresh", comparison: "rsync-quick" };
|
|
193
|
+
}
|
|
194
|
+
function filesystemSnapshotDiff(source, snapshot) {
|
|
195
|
+
const sourceEntries = snapshotEntries(source);
|
|
196
|
+
const snapshotEntriesByPath = snapshotEntries(snapshot);
|
|
197
|
+
const paths = [.../* @__PURE__ */ new Set([...sourceEntries.keys(), ...snapshotEntriesByPath.keys()])].sort();
|
|
198
|
+
return paths.filter((relative) => {
|
|
199
|
+
const left = sourceEntries.get(relative);
|
|
200
|
+
const right = snapshotEntriesByPath.get(relative);
|
|
201
|
+
return left === void 0 || right === void 0 || JSON.stringify(left) !== JSON.stringify(right);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function snapshotEntries(root) {
|
|
205
|
+
const entries = /* @__PURE__ */ new Map();
|
|
206
|
+
const walk2 = (directory) => {
|
|
207
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
208
|
+
if (entry.name === "_metadata") continue;
|
|
209
|
+
const absolute = path.join(directory, entry.name);
|
|
210
|
+
const relative = path.relative(root, absolute);
|
|
211
|
+
const stat = fs.lstatSync(absolute);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
entries.set(relative, { type: "directory" });
|
|
214
|
+
walk2(absolute);
|
|
215
|
+
} else if (entry.isSymbolicLink()) {
|
|
216
|
+
entries.set(relative, { type: "symlink", link: fs.readlinkSync(absolute) });
|
|
217
|
+
} else if (entry.isFile()) {
|
|
218
|
+
entries.set(relative, {
|
|
219
|
+
type: "file",
|
|
220
|
+
size: stat.size,
|
|
221
|
+
mtimeSeconds: Math.floor(stat.mtimeMs / 1e3)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk2(root);
|
|
227
|
+
return entries;
|
|
228
|
+
}
|
|
155
229
|
function distCheck(target) {
|
|
156
230
|
const manifestPath = path.join(target, "dist/chrome/manifest.json");
|
|
157
231
|
if (!fs.existsSync(manifestPath)) return { status: "no-build" };
|
|
@@ -206,8 +280,9 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
206
280
|
const webpackCache = webpackCacheCheck(resolved);
|
|
207
281
|
const buildLog = buildLogCheck(resolved, options.watchLog);
|
|
208
282
|
const dist = distCheck(resolved);
|
|
283
|
+
const runtimeDist = runtimeDistCheck(resolved);
|
|
209
284
|
const cdp = await cdpCheck(resolved, options.cdpPort, options.pageMode);
|
|
210
|
-
const checks = { deps, webpackCache, buildLog, dist, cdp };
|
|
285
|
+
const checks = { deps, webpackCache, buildLog, dist, runtimeDist, cdp };
|
|
211
286
|
const install = [{ id: "yarn-install", argv: ["yarn", "install", "--immutable"], cwd: resolved }];
|
|
212
287
|
const relaunch = [{ id: "relaunch-browser" }];
|
|
213
288
|
const cacheStale = buildLog.reason === "stale-cache";
|
|
@@ -259,6 +334,20 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
259
334
|
reasons: [dist.reason === "uncommitted-source" ? `Uncommitted source since the build (${dist.modified?.length ?? 0} file(s)); rebuild.` : `dist git id ${dist.distGitId} != HEAD ${dist.head}; rebuild.`],
|
|
260
335
|
actions: buildAction(resolved, false)
|
|
261
336
|
},
|
|
337
|
+
{
|
|
338
|
+
when: runtimeDist.status === "unknown",
|
|
339
|
+
decision: "blocked",
|
|
340
|
+
reasonCode: "runtime-dist-check-failed",
|
|
341
|
+
reasons: [runtimeDist.error ?? "The loaded runtime snapshot could not be compared with dist/chrome."],
|
|
342
|
+
actions: []
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
when: runtimeDist.status !== "fresh",
|
|
346
|
+
decision: "relaunch",
|
|
347
|
+
reasonCode: runtimeDist.status === "missing" ? "runtime-dist-missing" : "runtime-dist-stale",
|
|
348
|
+
reasons: [runtimeDist.status === "missing" ? "The loaded runtime snapshot is missing; launch before proving behavior." : `The loaded runtime snapshot differs from dist/chrome (${runtimeDist.modified?.length ?? 0} file(s)); reload before proving behavior.`],
|
|
349
|
+
actions: relaunch
|
|
350
|
+
},
|
|
262
351
|
{
|
|
263
352
|
when: cdp.status === "pass",
|
|
264
353
|
decision: "ready",
|
|
@@ -305,5 +394,6 @@ function buildAction(target, clean) {
|
|
|
305
394
|
export {
|
|
306
395
|
decideExtensionReadiness,
|
|
307
396
|
isExtensionDistStale,
|
|
308
|
-
recordReadinessBaseline
|
|
397
|
+
recordReadinessBaseline,
|
|
398
|
+
runtimeDistCheck
|
|
309
399
|
};
|