@deeeed/metamask-harness 0.50.6 → 0.51.1
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 +12 -9
- package/README.md +31 -0
- package/adapters/extension/ensure-browser.sh +5 -18
- package/adapters/extension/launch-browser.cjs +59 -139
- package/adapters/extension/lib/macos-focus.cjs +12 -177
- package/adapters/extension/lib/validation-process-ownership.cjs +2 -49
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/stop-viewers.sh +2 -1
- package/adapters/mobile/open-device.sh +41 -1
- package/adapters/mobile/verify.sh +24 -0
- package/dist/adapters/extension/validation-process-ownership.js +2 -3
- package/dist/adapters/slot-ports.js +13 -59
- package/dist/cli-commands.js +2 -1
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +43 -0
- package/dist/commands/checklist.js +9 -17
- package/dist/commands/prepare.js +236 -0
- package/dist/commands/task-init.js +51 -0
- package/dist/doctor.js +3 -4
- package/dist/mm-harness-cli.js +82 -2
- package/dist/paths.js +4 -0
- package/package.json +5 -5
|
@@ -228,6 +228,7 @@ NODE
|
|
|
228
228
|
quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
|
|
229
229
|
quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
|
|
230
230
|
quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
|
|
231
|
+
quoted_build_handoff="$(printf '%q' "$SCRIPT_DIR/build-handoff.cjs")"
|
|
231
232
|
quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
|
|
232
233
|
quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
|
|
233
234
|
quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
|
|
@@ -352,6 +353,7 @@ NODE
|
|
|
352
353
|
fi
|
|
353
354
|
prepare_parts+=("$chrome_launch_cmd")
|
|
354
355
|
prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
|
|
356
|
+
prepare_parts+=("node ${quoted_build_handoff} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --cdp-port ${CDP_PORT}")
|
|
355
357
|
if [ -n "$WALLET_FIXTURE_ABS" ]; then
|
|
356
358
|
prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
|
|
357
359
|
fi
|
|
@@ -29,6 +29,7 @@ done
|
|
|
29
29
|
unset _tv_src
|
|
30
30
|
|
|
31
31
|
runtime_dir="$TARGET/$(recipe_runtime_dir)"
|
|
32
|
+
console_log="$runtime_dir/extension-console.log"
|
|
32
33
|
set +e
|
|
33
34
|
|
|
34
35
|
if command -v tmux_viewer_close_marker >/dev/null 2>&1; then
|
|
@@ -47,7 +48,7 @@ ps -axo pid=,command= 2>/dev/null | while read -r pid command; do
|
|
|
47
48
|
*) continue ;;
|
|
48
49
|
esac
|
|
49
50
|
case "$command" in
|
|
50
|
-
*"$
|
|
51
|
+
*"--log $console_log"*|*"$console_log"*) kill "$pid" 2>/dev/null || true ;;
|
|
51
52
|
esac
|
|
52
53
|
done
|
|
53
54
|
|
|
@@ -126,6 +126,31 @@ source() {
|
|
|
126
126
|
mm_harness_restore_build_target
|
|
127
127
|
return "$status"
|
|
128
128
|
}
|
|
129
|
+
# The harness owns Metro on WATCHER_PORT. Expo's own bundler management
|
|
130
|
+
# re-checks that port after the build with lsof under a 350 ms budget, which a
|
|
131
|
+
# loaded machine misses, and then aborts with "Port became busy". Build only:
|
|
132
|
+
# --no-bundler, which Expo refuses next to --port, so the port argument goes.
|
|
133
|
+
mm_harness_expo_build_args() {
|
|
134
|
+
local arg skip=false
|
|
135
|
+
MM_HARNESS_EXPO_ARGS=()
|
|
136
|
+
for arg in "$@"; do
|
|
137
|
+
if [ "$skip" = true ]; then skip=false; continue; fi
|
|
138
|
+
case "$arg" in
|
|
139
|
+
--port) skip=true; continue ;;
|
|
140
|
+
--port=*) continue ;;
|
|
141
|
+
esac
|
|
142
|
+
MM_HARNESS_EXPO_ARGS+=("$arg")
|
|
143
|
+
done
|
|
144
|
+
MM_HARNESS_EXPO_ARGS+=(--no-bundler)
|
|
145
|
+
}
|
|
146
|
+
yarn() {
|
|
147
|
+
if [ "${1:-}" = expo ] && [ "${2:-}" = run:ios ]; then
|
|
148
|
+
mm_harness_expo_build_args "$@"
|
|
149
|
+
command yarn "${MM_HARNESS_EXPO_ARGS[@]}"
|
|
150
|
+
else
|
|
151
|
+
command yarn "$@"
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
129
154
|
mm_harness_restore_build_target
|
|
130
155
|
BUILD_ENV
|
|
131
156
|
} > "$build_env"
|
|
@@ -317,9 +342,24 @@ source() {
|
|
|
317
342
|
mm_harness_restore_android_build_target
|
|
318
343
|
return "$status"
|
|
319
344
|
}
|
|
345
|
+
# --no-bundler without --port: the harness owns Metro on WATCHER_PORT (see the
|
|
346
|
+
# iOS build env for why).
|
|
347
|
+
mm_harness_expo_build_args() {
|
|
348
|
+
local arg skip=false
|
|
349
|
+
MM_HARNESS_EXPO_ARGS=()
|
|
350
|
+
for arg in "$@"; do
|
|
351
|
+
if [ "$skip" = true ]; then skip=false; continue; fi
|
|
352
|
+
case "$arg" in
|
|
353
|
+
--port) skip=true; continue ;;
|
|
354
|
+
--port=*) continue ;;
|
|
355
|
+
esac
|
|
356
|
+
MM_HARNESS_EXPO_ARGS+=("$arg")
|
|
357
|
+
done
|
|
358
|
+
}
|
|
320
359
|
yarn() {
|
|
321
360
|
if [ "${1:-}" = expo ] && [ "${2:-}" = run:android ]; then
|
|
322
|
-
|
|
361
|
+
mm_harness_expo_build_args "$@"
|
|
362
|
+
command yarn "${MM_HARNESS_EXPO_ARGS[@]}" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE" --no-bundler
|
|
323
363
|
else
|
|
324
364
|
command yarn "$@"
|
|
325
365
|
fi
|
|
@@ -414,6 +414,30 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
414
414
|
|
|
415
415
|
port_holder_json "$port" > "$ARTIFACTS/logs/port-holder.json"
|
|
416
416
|
|
|
417
|
+
# Right after a launch or a fixture unlock the app's JS thread can stay busy
|
|
418
|
+
# for a while (account hydration); a single CDP probe then times out although
|
|
419
|
+
# the runtime is healthy. When Metro holds the port, let wait-for-bridge
|
|
420
|
+
# absorb that settle before the smoke. Best effort and bounded: the smoke
|
|
421
|
+
# below is the check that counts. The installed overlay carries only this
|
|
422
|
+
# script, so the waiter is resolved from the runner source it was installed
|
|
423
|
+
# from.
|
|
424
|
+
if [ "$status" != "fail" ] && node -e 'process.exit(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).listening ? 0 : 1)' "$ARTIFACTS/logs/port-holder.json" 2>/dev/null; then
|
|
425
|
+
wait_sh=""
|
|
426
|
+
for candidate in "$SCRIPT_DIR/wait-for-bridge.sh" "$(cat "$HARNESS_DIR/runner/.runner-source" 2>/dev/null || true)/adapters/mobile/wait-for-bridge.sh"; do
|
|
427
|
+
[ -f "$candidate" ] && { wait_sh="$candidate"; break; }
|
|
428
|
+
done
|
|
429
|
+
if [ -z "$wait_sh" ]; then
|
|
430
|
+
add_note "Mobile bridge settle skipped: wait-for-bridge.sh not found beside verify.sh or under runner/.runner-source; reinstall the runner."
|
|
431
|
+
elif MOBILE_BRIDGE_READY_TIMEOUT_MS="${MOBILE_BRIDGE_READY_TIMEOUT_MS:-90000}" \
|
|
432
|
+
MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS="${MOBILE_BRIDGE_UNRESPONSIVE_GRACE_MS:-90000}" \
|
|
433
|
+
bash "$wait_sh" --target "$TARGET" --port "$port" --platform "$PLATFORM" \
|
|
434
|
+
> "$ARTIFACTS/logs/wait-for-bridge.log" 2>&1; then
|
|
435
|
+
add_note "Mobile bridge settled before the live smoke (logs/wait-for-bridge.log)."
|
|
436
|
+
else
|
|
437
|
+
add_note "Mobile bridge settle did not complete (waiter exit $?); running the live smoke anyway (logs/wait-for-bridge.log)."
|
|
438
|
+
fi
|
|
439
|
+
fi
|
|
440
|
+
|
|
417
441
|
cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
|
|
418
442
|
{
|
|
419
443
|
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
@@ -3,9 +3,8 @@ const require2 = createRequire(import.meta.url);
|
|
|
3
3
|
const processOwnership = require2(
|
|
4
4
|
"../../../adapters/extension/lib/validation-process-ownership.cjs"
|
|
5
5
|
);
|
|
6
|
-
const { profileProcessPids, stopProfileProcesses
|
|
6
|
+
const { profileProcessPids, stopProfileProcesses } = processOwnership;
|
|
7
7
|
export {
|
|
8
8
|
profileProcessPids,
|
|
9
|
-
stopProfileProcesses
|
|
10
|
-
stopProfileProcessesSync
|
|
9
|
+
stopProfileProcesses
|
|
11
10
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { recipeRuntimeDir } from "../paths.js";
|
|
6
5
|
import {
|
|
@@ -132,53 +131,11 @@ function stopExtensionWatcher(target) {
|
|
|
132
131
|
}
|
|
133
132
|
return ownedPids.length;
|
|
134
133
|
}
|
|
135
|
-
function commandFlagValue(command, flag) {
|
|
136
|
-
const escaped = escapeRegex(flag);
|
|
137
|
-
const patterns = [
|
|
138
|
-
new RegExp(`(?:^|\\s)${escaped}=([^\\s"]+)`, "u"),
|
|
139
|
-
new RegExp(`(?:^|\\s)${escaped}="([^"]+)"`, "u"),
|
|
140
|
-
new RegExp(`(?:^|\\s)${escaped}='([^']+)'`, "u"),
|
|
141
|
-
new RegExp(`(?:^|\\s)${escaped}\\s+([^\\s"]+)`, "u")
|
|
142
|
-
];
|
|
143
|
-
for (const pattern of patterns) {
|
|
144
|
-
const match = pattern.exec(command);
|
|
145
|
-
if (match?.[1]) return match[1];
|
|
146
|
-
}
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
|
-
function pathIsInsideDir(candidate, root) {
|
|
150
|
-
const resolved = path.resolve(candidate);
|
|
151
|
-
const base = path.resolve(root);
|
|
152
|
-
return resolved === base || resolved.startsWith(`${base}${path.sep}`);
|
|
153
|
-
}
|
|
154
|
-
function chromeUserDataDirOwnedByCheckout(command, runtimeAbs) {
|
|
155
|
-
const value = commandFlagValue(command, "--user-data-dir");
|
|
156
|
-
if (!value) return false;
|
|
157
|
-
return pathIsInsideDir(value, runtimeAbs);
|
|
158
|
-
}
|
|
159
|
-
function commandLoadsCheckoutExtension(command, targetAbs) {
|
|
160
|
-
const loaded = commandFlagValue(command, "--load-extension") ?? commandFlagValue(command, "--disable-extensions-except");
|
|
161
|
-
if (!loaded) return false;
|
|
162
|
-
return pathIsInsideDir(loaded, targetAbs);
|
|
163
|
-
}
|
|
164
|
-
function isSharedOrDefaultBrowserProfile(profile) {
|
|
165
|
-
const resolved = path.resolve(profile);
|
|
166
|
-
const home = os.homedir();
|
|
167
|
-
const osDefaults = [
|
|
168
|
-
path.join(home, "Library/Application Support/Google/Chrome"),
|
|
169
|
-
path.join(home, "Library/Application Support/Chromium"),
|
|
170
|
-
path.join(home, "Library/Application Support/Microsoft Edge"),
|
|
171
|
-
path.join(home, ".config/google-chrome"),
|
|
172
|
-
path.join(home, ".config/chromium"),
|
|
173
|
-
path.join(home, ".config/microsoft-edge")
|
|
174
|
-
];
|
|
175
|
-
const underDefault = (dir) => resolved === dir || resolved.startsWith(`${dir}${path.sep}`);
|
|
176
|
-
return resolved === home || resolved.includes(`${path.sep}.chrome-farmslot`) || osDefaults.some(underDefault);
|
|
177
|
-
}
|
|
178
134
|
function stopExtensionRuntime(target) {
|
|
179
135
|
const resolved = path.resolve(target);
|
|
180
136
|
const runtimeAbs = path.join(resolved, recipeRuntimeDir());
|
|
181
|
-
const
|
|
137
|
+
const configuredProfile = process.env.CHROME_USER_DATA_DIR || path.join(runtimeAbs, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
138
|
+
const profiles = process.env.CHROME_USER_DATA_DIR ? [path.resolve(configuredProfile)] : [configuredProfile, path.join(runtimeAbs, "chrome-profile-recipe"), path.join(runtimeAbs, "chrome-profile-pw")].map((value) => path.resolve(value));
|
|
182
139
|
let signalled = stopExtensionWatcher(resolved);
|
|
183
140
|
const ownedBrowserPids = /* @__PURE__ */ new Set();
|
|
184
141
|
try {
|
|
@@ -187,16 +144,7 @@ function stopExtensionRuntime(target) {
|
|
|
187
144
|
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
188
145
|
if (!match) continue;
|
|
189
146
|
const pid = Number(match[1]);
|
|
190
|
-
if (pid
|
|
191
|
-
if (chromeUserDataDirOwnedByCheckout(match[2], runtimeAbs)) {
|
|
192
|
-
ownedBrowserPids.add(pid);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (!extraProfile || pathIsInsideDir(extraProfile, runtimeAbs) || isSharedOrDefaultBrowserProfile(extraProfile)) continue;
|
|
196
|
-
const configured = commandFlagValue(match[2], "--user-data-dir");
|
|
197
|
-
if (configured && path.resolve(configured) === extraProfile && commandLoadsCheckoutExtension(match[2], resolved)) {
|
|
198
|
-
ownedBrowserPids.add(pid);
|
|
199
|
-
}
|
|
147
|
+
if (pid !== process.pid && profiles.some((profile) => commandHasExactArg(match[2], "--user-data-dir", profile))) ownedBrowserPids.add(pid);
|
|
200
148
|
}
|
|
201
149
|
} catch {
|
|
202
150
|
}
|
|
@@ -255,16 +203,22 @@ function processAlive(pid) {
|
|
|
255
203
|
return error.code === "EPERM";
|
|
256
204
|
}
|
|
257
205
|
}
|
|
206
|
+
function commandHasExactArg(command, flag, value) {
|
|
207
|
+
const escapedFlag = escapeRegex(flag);
|
|
208
|
+
const escapedValue = escapeRegex(value);
|
|
209
|
+
return [
|
|
210
|
+
new RegExp(`(?:^|\\s)${escapedFlag}=${escapedValue}(?=\\s|$)`, "u"),
|
|
211
|
+
new RegExp(`(?:^|\\s)${escapedFlag}="${escapedValue}"(?=\\s|$)`, "u"),
|
|
212
|
+
new RegExp(`(?:^|\\s)${escapedFlag}='${escapedValue}'(?=\\s|$)`, "u"),
|
|
213
|
+
new RegExp(`(?:^|\\s)${escapedFlag}\\s+${escapedValue}(?=\\s|$)`, "u")
|
|
214
|
+
].some((pattern) => pattern.test(command));
|
|
215
|
+
}
|
|
258
216
|
function escapeRegex(value) {
|
|
259
217
|
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
260
218
|
}
|
|
261
219
|
export {
|
|
262
220
|
applyKVLines,
|
|
263
|
-
chromeUserDataDirOwnedByCheckout,
|
|
264
|
-
commandFlagValue,
|
|
265
221
|
isExtensionWatcherLive,
|
|
266
|
-
isSharedOrDefaultBrowserProfile,
|
|
267
|
-
pathIsInsideDir,
|
|
268
222
|
resolveExtensionSlotPorts,
|
|
269
223
|
resolveMobileSlotPorts,
|
|
270
224
|
stopExtensionRuntime,
|
package/dist/cli-commands.js
CHANGED
|
@@ -25,7 +25,8 @@ const SPEC = {
|
|
|
25
25
|
{ name: "execution-template", desc: "Discover and materialize shared agent checklists", args: ["list", "materialize", "lint", "new"], flags: ["--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--flow", "--run-mode", "--platform", "--domain", "--id", "--provenance", "--include-shadowed", "--no-include-shadowed", "--title", "--force", "--json"] },
|
|
26
26
|
{ name: "recipe-quality", desc: "Build the recipe-quality artifact from compact JSON", args: ["build"], flags: ["--input", "--output", "--json"] },
|
|
27
27
|
{ name: "interactive", aliases: ["menu"], desc: "Interactive command menu" },
|
|
28
|
-
{ name: "prepare", desc: "
|
|
28
|
+
{ name: "prepare", desc: "Run the readiness chain and write the readiness record", flags: ["--target", "--platform", "--mobile-platform", "--artifacts-dir", "--runtime-dir", "--json"] },
|
|
29
|
+
{ name: "task", desc: "Create a task directory from a shared execution template", args: ["init"], flags: ["--flow", "--run-mode", "--platform", "--template", "--title", "--task-text", "--task-file", "--ticket", "--source-ref", "--surface", "--project", "--repo", "--attempt-id", "--var", "--task-dir-label", "--mode-preamble", "--addendum-file", "--mark-command", "--dir", "--domain-dir", "--project-worker", "--project-name", "--package-templates", "--package-id", "--domain", "--json"] },
|
|
29
30
|
{ name: "runtime-status", desc: "Structured runtime status JSON", flags: ["--json", "--target", "--cdp-port", "--runtime-dir"] }
|
|
30
31
|
],
|
|
31
32
|
extension: [
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ import { handleStatus } from "./commands/status.js";
|
|
|
22
22
|
import { handleCheck } from "./commands/check.js";
|
|
23
23
|
import { handleChecklist } from "./commands/checklist.js";
|
|
24
24
|
import { handleExecutionTemplate } from "./commands/execution-template.js";
|
|
25
|
+
import { handlePrepare } from "./commands/prepare.js";
|
|
26
|
+
import { handleTaskInit } from "./commands/task-init.js";
|
|
25
27
|
import { handleLast } from "./commands/last.js";
|
|
26
28
|
import { parseArgs, targetPath } from "./commands/parse-args.js";
|
|
27
29
|
const COMMANDS = {
|
|
@@ -127,6 +129,8 @@ async function main(argv) {
|
|
|
127
129
|
if (command === "check") return handleCheck(argv.slice(1));
|
|
128
130
|
if (command === "checklist") return handleChecklist(argv.slice(1));
|
|
129
131
|
if (command === "execution-template") return handleExecutionTemplate(argv.slice(1));
|
|
132
|
+
if (command === "task") return handleTaskInit(argv.slice(1));
|
|
133
|
+
if (command === "prepare") return handlePrepare(argv.slice(1));
|
|
130
134
|
if (command === "last") return handleLast(parseArgs(argv.slice(1), command));
|
|
131
135
|
const handler = COMMANDS[command];
|
|
132
136
|
if (!handler) throw new Error(`Unknown command: ${command}`);
|
package/dist/command-contract.js
CHANGED
|
@@ -113,6 +113,49 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
113
113
|
],
|
|
114
114
|
minimumPositionals: 2
|
|
115
115
|
},
|
|
116
|
+
prepare: {
|
|
117
|
+
usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--artifacts-dir <dir>] [--json]",
|
|
118
|
+
options: options(HELP, JSON, TARGET, {
|
|
119
|
+
"--platform": value(["mobile", "extension", "core"]),
|
|
120
|
+
"--mobile-platform": value(["ios", "android"]),
|
|
121
|
+
"--artifacts-dir": value()
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
task: {
|
|
125
|
+
usage: "mm-harness task init <task-dir> --flow f --run-mode m --template id --title t [options]",
|
|
126
|
+
options: options(HELP, JSON, {
|
|
127
|
+
"--flow": value(),
|
|
128
|
+
"--run-mode": value(["autonomous", "interactive", "validation"]),
|
|
129
|
+
"--platform": value(),
|
|
130
|
+
"--template": value(),
|
|
131
|
+
"--title": value(),
|
|
132
|
+
"--task-text": value(),
|
|
133
|
+
"--task-file": value(),
|
|
134
|
+
"--ticket": value(),
|
|
135
|
+
"--source-ref": value(),
|
|
136
|
+
"--surface": value(),
|
|
137
|
+
"--project": value(),
|
|
138
|
+
"--repo": value(),
|
|
139
|
+
"--attempt-id": value(),
|
|
140
|
+
"--var": value(),
|
|
141
|
+
"--task-dir-label": value(),
|
|
142
|
+
"--mode-preamble": value(),
|
|
143
|
+
"--addendum-file": value(),
|
|
144
|
+
"--mark-command": value(),
|
|
145
|
+
"--dir": value(),
|
|
146
|
+
"--domain-dir": value(),
|
|
147
|
+
"--project-worker": value(),
|
|
148
|
+
"--project-name": value(),
|
|
149
|
+
"--package-templates": value(),
|
|
150
|
+
"--package-id": value(),
|
|
151
|
+
"--domain": value()
|
|
152
|
+
}),
|
|
153
|
+
positionals: [
|
|
154
|
+
{ label: "action", choices: ["init"] },
|
|
155
|
+
{ label: "task-dir" }
|
|
156
|
+
],
|
|
157
|
+
minimumPositionals: 2
|
|
158
|
+
},
|
|
116
159
|
"execution-template": {
|
|
117
160
|
usage: "mm-harness execution-template <list|materialize|lint|new> [target] [options]",
|
|
118
161
|
options: options(HELP, JSON, {
|
|
@@ -7,6 +7,9 @@ const require2 = createRequire(import.meta.url);
|
|
|
7
7
|
const MARK_SCRIPT = require2.resolve(
|
|
8
8
|
"@farmslot/agent-runtime/scripts/mark-checklist-step.cjs"
|
|
9
9
|
);
|
|
10
|
+
const { resolveChecklistPaths } = require2(
|
|
11
|
+
"@farmslot/agent-runtime/scripts/checklist-target.cjs"
|
|
12
|
+
);
|
|
10
13
|
const USAGE = "mm-harness checklist <mark <task-dir> <step> | closeout <task-dir>> [options]";
|
|
11
14
|
const CHECKLIST_VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
12
15
|
"--reason",
|
|
@@ -98,16 +101,14 @@ function stageLearningPackage(taskDir, step, markArgs) {
|
|
|
98
101
|
);
|
|
99
102
|
}
|
|
100
103
|
function terminalSignalStatus(taskDir) {
|
|
104
|
+
const { signalPath } = resolveChecklistPaths(taskDir);
|
|
105
|
+
let signal;
|
|
101
106
|
try {
|
|
102
|
-
|
|
103
|
-
fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
|
|
104
|
-
);
|
|
105
|
-
const signalPath = path.resolve(taskDir, target.signal ?? "SIGNAL.json");
|
|
106
|
-
const signal = JSON.parse(fs.readFileSync(signalPath, "utf8"));
|
|
107
|
-
return signal.status === "complete" || signal.status === "blocked" ? signal.status : null;
|
|
107
|
+
signal = JSON.parse(fs.readFileSync(signalPath, "utf8"));
|
|
108
108
|
} catch {
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
|
+
return signal.status === "complete" || signal.status === "blocked" ? signal.status : null;
|
|
111
112
|
}
|
|
112
113
|
function preserveTerminalSignal(taskDir, step) {
|
|
113
114
|
if (step !== "start" && numericMarkStep(step) === null) return false;
|
|
@@ -122,16 +123,6 @@ function numericMarkStep(step) {
|
|
|
122
123
|
const stepNumber = Number(step);
|
|
123
124
|
return Number.isInteger(stepNumber) && stepNumber >= 1 ? stepNumber : null;
|
|
124
125
|
}
|
|
125
|
-
function checklistPath(taskDir) {
|
|
126
|
-
try {
|
|
127
|
-
const target = JSON.parse(
|
|
128
|
-
fs.readFileSync(path.join(taskDir, "checklist-target.json"), "utf8")
|
|
129
|
-
);
|
|
130
|
-
return path.resolve(taskDir, target.checklist ?? "CHECKLIST.md");
|
|
131
|
-
} catch {
|
|
132
|
-
return path.join(taskDir, "CHECKLIST.md");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
126
|
function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
136
127
|
let inFence = false;
|
|
137
128
|
let seen = 0;
|
|
@@ -151,9 +142,10 @@ function checklistStepRequiresDiffPass(checklist, stepNumber) {
|
|
|
151
142
|
function checkDiffGateReady(taskDir, step) {
|
|
152
143
|
const stepNumber = numericMarkStep(step);
|
|
153
144
|
if (stepNumber === null) return true;
|
|
145
|
+
const { taskPath } = resolveChecklistPaths(taskDir);
|
|
154
146
|
let checklist = "";
|
|
155
147
|
try {
|
|
156
|
-
checklist = fs.readFileSync(
|
|
148
|
+
checklist = fs.readFileSync(taskPath, "utf8");
|
|
157
149
|
} catch {
|
|
158
150
|
return true;
|
|
159
151
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
READINESS_RECORD
|
|
6
|
+
} from "@farmslot/protocol";
|
|
7
|
+
import { runnerProvenance } from "../doctor.js";
|
|
8
|
+
import { detectAdapter } from "../harness.js";
|
|
9
|
+
import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
10
|
+
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
11
|
+
import { EXIT } from "./shared.js";
|
|
12
|
+
const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--artifacts-dir <dir>] [--json]";
|
|
13
|
+
const REPORT_DIR = "prepare";
|
|
14
|
+
const SKIPPED_AFTER_FAILURE = "previous step failed";
|
|
15
|
+
function reportPathFor(id) {
|
|
16
|
+
return path.join(REPORT_DIR, `${id}.json`);
|
|
17
|
+
}
|
|
18
|
+
function reportedError(stdout) {
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(stdout ?? "");
|
|
22
|
+
} catch {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
const message = parsed?.error?.message;
|
|
26
|
+
return typeof message === "string" && message ? message : void 0;
|
|
27
|
+
}
|
|
28
|
+
function skippedStep(id, command, reason) {
|
|
29
|
+
return { id, command, status: "skipped", exitCode: 0, reason };
|
|
30
|
+
}
|
|
31
|
+
const STEP_MARK = { pass: "\u2713", fail: "\u2717", skipped: "\u2013" };
|
|
32
|
+
function stepLine(step) {
|
|
33
|
+
return `${STEP_MARK[step.status]} ${step.id}: ${step.status}${step.reason ? ` \u2014 ${step.reason}` : ""}`;
|
|
34
|
+
}
|
|
35
|
+
function runStep(executable, target, artifactsDir, id, args, { json }) {
|
|
36
|
+
const command = [executable, ...args].join(" ");
|
|
37
|
+
const result = spawnSync(executable, [...args], {
|
|
38
|
+
cwd: target,
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
// The update nudge is an interactive courtesy; it has no place in the
|
|
41
|
+
// stream a recorded readiness step captures.
|
|
42
|
+
env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
|
|
43
|
+
});
|
|
44
|
+
if (result.error) {
|
|
45
|
+
return { id, command, status: "fail", exitCode: 1, reason: result.error.message };
|
|
46
|
+
}
|
|
47
|
+
const report = reportPathFor(id);
|
|
48
|
+
const file = path.join(artifactsDir, report);
|
|
49
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
fs.writeFileSync(file, result.stdout ?? "");
|
|
51
|
+
const exitCode = result.status ?? 1;
|
|
52
|
+
if (exitCode === 0) {
|
|
53
|
+
return { id, command, status: "pass", exitCode, reportPath: report };
|
|
54
|
+
}
|
|
55
|
+
if (!json && result.stderr) process.stderr.write(result.stderr);
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
command,
|
|
59
|
+
status: "fail",
|
|
60
|
+
exitCode,
|
|
61
|
+
reportPath: report,
|
|
62
|
+
reason: reportedError(result.stdout) ?? `exited ${exitCode}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function readDevices(artifactsDir) {
|
|
66
|
+
const file = path.join(artifactsDir, reportPathFor("status"));
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const devices = parsed?.devices;
|
|
74
|
+
return Array.isArray(devices) ? devices : [];
|
|
75
|
+
}
|
|
76
|
+
function readRuntimeContextTarget(target) {
|
|
77
|
+
const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
81
|
+
} catch {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
const record = parsed ?? {};
|
|
85
|
+
const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
|
|
86
|
+
return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
|
|
87
|
+
}
|
|
88
|
+
function mobilePlatformFromConfiguredTarget(env, context) {
|
|
89
|
+
const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
|
|
90
|
+
const android = Boolean(
|
|
91
|
+
env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
|
|
92
|
+
);
|
|
93
|
+
if (ios === android) return void 0;
|
|
94
|
+
return ios ? "ios" : "android";
|
|
95
|
+
}
|
|
96
|
+
function mobilePlatformFromDevices(devices) {
|
|
97
|
+
const selected = devices.filter((device) => device?.selected === true);
|
|
98
|
+
const candidates = selected.length > 0 ? selected : devices;
|
|
99
|
+
const platforms = [
|
|
100
|
+
...new Set(
|
|
101
|
+
candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
|
|
102
|
+
)
|
|
103
|
+
];
|
|
104
|
+
return platforms.length === 1 ? platforms[0] : void 0;
|
|
105
|
+
}
|
|
106
|
+
function mobileTargetAmbiguityError() {
|
|
107
|
+
const choices = [
|
|
108
|
+
"1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
|
|
109
|
+
"2. Android mm-harness prepare --platform mobile --mobile-platform android"
|
|
110
|
+
];
|
|
111
|
+
return {
|
|
112
|
+
code: "PREPARE_MOBILE_TARGET_AMBIGUOUS",
|
|
113
|
+
message: "could not infer the mobile target from the connected devices",
|
|
114
|
+
userAction: `choose the mobile target, then rerun this command:
|
|
115
|
+
${choices.join("\n")}`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function ambiguousMobileTarget(json) {
|
|
119
|
+
const { code, message, userAction } = mobileTargetAmbiguityError();
|
|
120
|
+
if (json) {
|
|
121
|
+
console.log(
|
|
122
|
+
JSON.stringify(
|
|
123
|
+
{
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
command: "prepare",
|
|
126
|
+
status: "fail",
|
|
127
|
+
exitCode: EXIT.runtime,
|
|
128
|
+
error: { code, message, userAction }
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
2
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
console.error(`\u2717 mm-harness prepare: ${message}`);
|
|
136
|
+
console.error(` Next: ${userAction}`);
|
|
137
|
+
}
|
|
138
|
+
return EXIT.runtime;
|
|
139
|
+
}
|
|
140
|
+
function mobilePlatformOption(value) {
|
|
141
|
+
if (value === void 0) return void 0;
|
|
142
|
+
if (value === "ios") return "ios";
|
|
143
|
+
if (value === "android") return "android";
|
|
144
|
+
throw usageError(
|
|
145
|
+
`--mobile-platform must be ios or android; received '${value}'
|
|
146
|
+
Next: ${USAGE}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function resolvePlatform(requested, target) {
|
|
150
|
+
const platform = requested ?? detectAdapter(target);
|
|
151
|
+
if (platform === "mobile" || platform === "extension" || platform === "core") {
|
|
152
|
+
return platform;
|
|
153
|
+
}
|
|
154
|
+
throw usageError(
|
|
155
|
+
`could not detect the platform of ${target}
|
|
156
|
+
Next: ${USAGE}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
async function handlePrepare(argv) {
|
|
160
|
+
const { options } = parseArgs(argv, "prepare");
|
|
161
|
+
const json = optionFlag(options, "json");
|
|
162
|
+
const target = targetPath(options);
|
|
163
|
+
const platform = resolvePlatform(optionString(options, "platform"), target);
|
|
164
|
+
const artifactsDir = path.resolve(
|
|
165
|
+
optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
|
|
166
|
+
);
|
|
167
|
+
const executable = harnessExecutable();
|
|
168
|
+
const targetArgs = ["--target", target];
|
|
169
|
+
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
170
|
+
const steps = [];
|
|
171
|
+
const failed = () => steps.some((step) => step.status === "fail");
|
|
172
|
+
const addStep = (step) => {
|
|
173
|
+
steps.push(step);
|
|
174
|
+
if (!json) console.log(stepLine(step));
|
|
175
|
+
return step;
|
|
176
|
+
};
|
|
177
|
+
const run = (id, args) => addStep(
|
|
178
|
+
failed() ? skippedStep(id, [executable, ...args].join(" "), SKIPPED_AFTER_FAILURE) : runStep(executable, target, artifactsDir, id, args, { json })
|
|
179
|
+
);
|
|
180
|
+
run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
181
|
+
run("status", ["status", "--json", ...targetArgs]);
|
|
182
|
+
if (platform === "mobile" && !mobilePlatform && !failed()) {
|
|
183
|
+
mobilePlatform = mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(readDevices(artifactsDir));
|
|
184
|
+
if (!mobilePlatform) return ambiguousMobileTarget(json);
|
|
185
|
+
}
|
|
186
|
+
if (platform === "core") {
|
|
187
|
+
const reason = "core is headless: no app surface to launch or seed";
|
|
188
|
+
addStep(skippedStep("launch", `${executable} launch`, reason));
|
|
189
|
+
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
190
|
+
} else if (platform === "mobile") {
|
|
191
|
+
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
192
|
+
run("launch", ["launch", ...device, "--verify", "--json", ...targetArgs]);
|
|
193
|
+
run("fixtures", [
|
|
194
|
+
"fixtures",
|
|
195
|
+
"set",
|
|
196
|
+
...mobilePlatform ? ["--platform", mobilePlatform] : [],
|
|
197
|
+
"--json",
|
|
198
|
+
...targetArgs
|
|
199
|
+
]);
|
|
200
|
+
} else {
|
|
201
|
+
run("launch", ["launch", "--verify", "--json", ...targetArgs]);
|
|
202
|
+
run("fixtures", ["fixtures", "set", "--json", ...targetArgs]);
|
|
203
|
+
}
|
|
204
|
+
run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
205
|
+
const provenance = runnerProvenance(manifestPath(platform));
|
|
206
|
+
const record = {
|
|
207
|
+
schemaVersion: 1,
|
|
208
|
+
harness: {
|
|
209
|
+
name: provenance.packageName,
|
|
210
|
+
version: provenance.version,
|
|
211
|
+
source: `${provenance.installKind}/${provenance.packageSource}`,
|
|
212
|
+
executable: provenance.executablePath
|
|
213
|
+
},
|
|
214
|
+
platform,
|
|
215
|
+
...mobilePlatform ? { mobilePlatform } : {},
|
|
216
|
+
steps,
|
|
217
|
+
ready: steps.every((step) => step.status !== "fail"),
|
|
218
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
219
|
+
};
|
|
220
|
+
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
221
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
222
|
+
fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
|
|
223
|
+
`);
|
|
224
|
+
if (json) {
|
|
225
|
+
console.log(JSON.stringify(record, null, 2));
|
|
226
|
+
} else {
|
|
227
|
+
console.log(`readiness record: ${recordPath}`);
|
|
228
|
+
}
|
|
229
|
+
return record.ready ? EXIT.ok : EXIT.runtime;
|
|
230
|
+
}
|
|
231
|
+
export {
|
|
232
|
+
handlePrepare,
|
|
233
|
+
mobilePlatformFromConfiguredTarget,
|
|
234
|
+
mobilePlatformFromDevices,
|
|
235
|
+
mobileTargetAmbiguityError
|
|
236
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { detectAdapter } from "../harness.js";
|
|
4
|
+
import { harnessExecutable } from "../paths.js";
|
|
5
|
+
import { EXIT } from "./shared.js";
|
|
6
|
+
const require2 = createRequire(import.meta.url);
|
|
7
|
+
const USAGE = "mm-harness task init <task-dir> --flow f --run-mode m --template id --title t [options]";
|
|
8
|
+
function hasOption(argv, name) {
|
|
9
|
+
return argv.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
10
|
+
}
|
|
11
|
+
function metamaskDefaults(argv) {
|
|
12
|
+
const defaults = [];
|
|
13
|
+
if (!hasOption(argv, "--surface")) defaults.push("--surface", "skill");
|
|
14
|
+
if (!hasOption(argv, "--mark-command")) {
|
|
15
|
+
defaults.push("--mark-command", `${harnessExecutable()} checklist mark`);
|
|
16
|
+
}
|
|
17
|
+
if (!hasOption(argv, "--platform")) {
|
|
18
|
+
const adapter = detectAdapter(process.cwd());
|
|
19
|
+
if (adapter) defaults.push("--platform", adapter);
|
|
20
|
+
}
|
|
21
|
+
return defaults;
|
|
22
|
+
}
|
|
23
|
+
async function handleTaskInit(argv) {
|
|
24
|
+
if (argv[0] !== "init") {
|
|
25
|
+
console.error(`usage: ${USAGE}`);
|
|
26
|
+
return EXIT.usage;
|
|
27
|
+
}
|
|
28
|
+
const forwarded = argv.slice(1);
|
|
29
|
+
let entrypoint;
|
|
30
|
+
try {
|
|
31
|
+
entrypoint = require2.resolve("@farmslot/agent-runtime/scripts/task-init-cli.mjs");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(
|
|
34
|
+
`mm-harness task init: installed @farmslot/agent-runtime does not expose the task-directory command: ${error instanceof Error ? error.message : String(error)}`
|
|
35
|
+
);
|
|
36
|
+
return EXIT.runtime;
|
|
37
|
+
}
|
|
38
|
+
const result = spawnSync(
|
|
39
|
+
process.execPath,
|
|
40
|
+
[entrypoint, ...forwarded, ...metamaskDefaults(forwarded)],
|
|
41
|
+
{ env: process.env, stdio: "inherit" }
|
|
42
|
+
);
|
|
43
|
+
if (result.error) {
|
|
44
|
+
console.error(`mm-harness task init: ${result.error.message}`);
|
|
45
|
+
return EXIT.runtime;
|
|
46
|
+
}
|
|
47
|
+
return result.status ?? EXIT.runtime;
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
handleTaskInit
|
|
51
|
+
};
|