@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.
@@ -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
- *"$runtime_dir/"*) kill "$pid" 2>/dev/null || true ;;
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
- command yarn "$@" "$MM_HARNESS_BUILD_ANDROID_EXPO_DEVICE"
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, stopProfileProcessesSync } = processOwnership;
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 extraProfile = process.env.CHROME_USER_DATA_DIR ? path.resolve(process.env.CHROME_USER_DATA_DIR) : null;
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 === process.pid) continue;
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,
@@ -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: "Install harness (+ optional validate)", flags: ["--target", "--runtime-dir", "--json"] },
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}`);
@@ -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
- const target = JSON.parse(
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(checklistPath(taskDir), "utf8");
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
+ };