@deeeed/metamask-harness 0.92.22 → 0.92.23

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.
@@ -1,433 +0,0 @@
1
- import { acquireCheckoutLock, trackCheckoutChild, recordCommandStage, color, stripAnsi, harnessExecutable, recipeRuntimeDir, contextAdapter, optionFlag, optionString, parseArgs, targetPath, usageError, EXIT } from "@farmslot/recipe-cli/harness";
2
- import { spawn } from "node:child_process";
3
- import { OperationOutputTail } from "@farmslot/recipe-runner/runtime/operation";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import {
7
- READINESS_RECORD
8
- } from "@farmslot/protocol";
9
- import { runnerProvenance } from "../doctor.js";
10
- import { adapterRegistry } from "../adapters/registry.js";
11
- import { getAdapterSurface } from "../adapters/surface.js";
12
- import { PREPARE_PROGRESS_ARTIFACT } from "../paths.js";
13
- import { formatDuration } from "./status-watch.js";
14
- import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
15
- const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]";
16
- const REPORT_DIR = "prepare";
17
- const SKIPPED_AFTER_FAILURE = "previous step failed";
18
- function reportPathFor(id) {
19
- return path.join(REPORT_DIR, `${id}.json`);
20
- }
21
- function reportedError(stdout) {
22
- let parsed;
23
- try {
24
- parsed = JSON.parse(stdout ?? "");
25
- } catch {
26
- return void 0;
27
- }
28
- const message = parsed?.error?.message;
29
- return typeof message === "string" && message ? message : void 0;
30
- }
31
- function skippedStep(id, command, reason) {
32
- return { id, command, status: "skipped", exitCode: 0, reason, durationMs: 0 };
33
- }
34
- const STEP_MARK = {
35
- pending: "\xB7",
36
- running: "\u2026",
37
- pass: "\u2713",
38
- fail: "\u2717",
39
- skipped: "\u2013"
40
- };
41
- const STEP_STYLE = {
42
- pending: "dim",
43
- running: "accent",
44
- pass: "ok",
45
- fail: "err",
46
- skipped: "dim"
47
- };
48
- const STEP_HINT = {
49
- launch: "can take minutes on Mobile: Metro bundle and app start",
50
- fixtures: "under a minute"
51
- };
52
- function stepChain(surface) {
53
- return surface.headless ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
54
- }
55
- function lastRunDurations(recordPath) {
56
- let parsed;
57
- try {
58
- parsed = JSON.parse(fs.readFileSync(recordPath, "utf8"));
59
- } catch {
60
- return {};
61
- }
62
- const steps = parsed?.steps;
63
- if (!Array.isArray(steps)) return {};
64
- const durations = {};
65
- for (const step of steps) {
66
- if (typeof step?.id === "string" && typeof step.durationMs === "number" && step.durationMs > 0) {
67
- durations[step.id] = step.durationMs;
68
- }
69
- }
70
- return durations;
71
- }
72
- function stepHints(chain, lastRun) {
73
- const hints = {};
74
- for (const id of chain) {
75
- const previous = lastRun[id];
76
- const parts = [];
77
- if (STEP_HINT[id]) parts.push(STEP_HINT[id]);
78
- if (previous !== void 0) parts.push(`last run ${formatDuration(previous)}`);
79
- if (parts.length > 0) hints[id] = parts.join(", ");
80
- }
81
- return hints;
82
- }
83
- function stepRowLine(row, width) {
84
- const out = (style, text) => color(style, text, { stream: process.stdout });
85
- const running = row.state === "pending" || row.state === "running";
86
- const elapsed = row.state === "running" && row.startedAt !== void 0 ? ` ${out("dim", formatDuration(Date.now() - row.startedAt))}` : "";
87
- const note = running ? row.hint : row.durationMs ? formatDuration(row.durationMs) : void 0;
88
- const mark = out(STEP_STYLE[row.state], STEP_MARK[row.state]);
89
- return `${mark} ${out(running ? "bold" : "dim", row.id.padEnd(width))}: ${out(STEP_STYLE[row.state], row.state)}${elapsed}${row.reason ? ` \u2014 ${row.reason}` : ""}${note ? ` ${out("dim", `(${note})`)}` : ""}`;
90
- }
91
- function createStepView(chain, hints) {
92
- const rows = new Map(
93
- chain.map((id) => [id, { id, state: "pending", hint: hints[id] }])
94
- );
95
- const width = Math.max(...chain.map((id) => id.length));
96
- const tty = Boolean(process.stdout.isTTY);
97
- const block = () => {
98
- const terminalColumns = process.stdout.columns;
99
- const columns = tty && typeof terminalColumns === "number" && terminalColumns > 0 ? terminalColumns : Number.POSITIVE_INFINITY;
100
- return [...rows.values()].map((row) => {
101
- const line = stepRowLine(row, width);
102
- return stripAnsi(line).length <= columns ? line : stripAnsi(line).slice(0, columns);
103
- });
104
- };
105
- let painted = 0;
106
- let lastFrame = "";
107
- let ended = false;
108
- let timer;
109
- const repaint = () => {
110
- const lines = block();
111
- const frame = lines.join("\n");
112
- if (frame === lastFrame) return;
113
- lastFrame = frame;
114
- const up = painted > 0 ? `\x1B[${painted}A` : "";
115
- painted = lines.length;
116
- process.stdout.write(`${up}${lines.map((line) => `\x1B[2K${line}`).join("\n")}
117
- `);
118
- };
119
- const writeBlock = () => {
120
- process.stdout.write(`${block().join("\n")}
121
- `);
122
- };
123
- return {
124
- begin() {
125
- if (tty) repaint();
126
- else writeBlock();
127
- },
128
- start(id, startedAt) {
129
- const row = rows.get(id);
130
- if (!row) return;
131
- row.state = "running";
132
- row.startedAt = startedAt;
133
- if (!tty) return;
134
- repaint();
135
- timer = setInterval(repaint, 250);
136
- },
137
- finish(step) {
138
- clearInterval(timer);
139
- timer = void 0;
140
- const row = rows.get(step.id);
141
- if (!row) return;
142
- row.state = step.status;
143
- row.startedAt = void 0;
144
- row.reason = step.reason;
145
- row.durationMs = step.durationMs;
146
- if (tty) repaint();
147
- else process.stdout.write(`${stepRowLine(row, width)}
148
- `);
149
- },
150
- // Idempotent: the caller ends the view where the final block belongs in the
151
- // output, and a `finally` ends it again on the paths that never got there.
152
- // The ticker holds the event loop awake, so this must run either way.
153
- end() {
154
- if (ended) return;
155
- ended = true;
156
- clearInterval(timer);
157
- timer = void 0;
158
- if (!tty) writeBlock();
159
- }
160
- };
161
- }
162
- function spawnStep(executable, args, target) {
163
- return new Promise((resolve) => {
164
- const child = spawn(executable, [...args], {
165
- cwd: target,
166
- stdio: ["ignore", "pipe", "pipe"],
167
- // The update nudge is an interactive courtesy; it has no place in the
168
- // stream a recorded readiness step captures.
169
- env: { ...process.env, MM_HARNESS_NO_UPDATE_CHECK: "1" }
170
- });
171
- if (child.pid) trackCheckoutChild(target, child.pid);
172
- let stdout = "";
173
- const stderr = new OperationOutputTail();
174
- child.stdout?.setEncoding("utf8");
175
- child.stderr?.setEncoding("utf8");
176
- child.stdout?.on("data", (chunk) => {
177
- stdout += chunk;
178
- });
179
- child.stderr?.on("data", (chunk) => {
180
- stderr.append(chunk);
181
- });
182
- child.on("error", (error) => resolve({ error, status: null, stdout, stderr: stderr.toString() }));
183
- child.on("close", (status) => resolve({ status, stdout, stderr: stderr.toString() }));
184
- });
185
- }
186
- async function runStep(executable, target, artifactsDir, id, args, { json, onSpawn }) {
187
- const command = [executable, ...args].join(" ");
188
- const startedAt = Date.now();
189
- onSpawn?.(startedAt);
190
- const result = await spawnStep(executable, args, target);
191
- const durationMs = Date.now() - startedAt;
192
- if (result.error) {
193
- return { id, command, status: "fail", exitCode: 1, reason: result.error.message, durationMs };
194
- }
195
- const report = reportPathFor(id);
196
- const file = path.join(artifactsDir, report);
197
- fs.mkdirSync(path.dirname(file), { recursive: true });
198
- fs.writeFileSync(file, result.stdout);
199
- const exitCode = result.status ?? 1;
200
- if (exitCode === 0) {
201
- return { id, command, status: "pass", exitCode, reportPath: report, durationMs };
202
- }
203
- if (!json && result.stderr) process.stderr.write(result.stderr);
204
- return {
205
- id,
206
- command,
207
- status: "fail",
208
- exitCode,
209
- reportPath: report,
210
- reason: reportedError(result.stdout) ?? `exited ${exitCode}`,
211
- durationMs
212
- };
213
- }
214
- function readDevices(artifactsDir) {
215
- const file = path.join(artifactsDir, reportPathFor("status"));
216
- let parsed;
217
- try {
218
- parsed = JSON.parse(fs.readFileSync(file, "utf8"));
219
- } catch {
220
- return [];
221
- }
222
- const devices = parsed?.devices;
223
- return Array.isArray(devices) ? devices : [];
224
- }
225
- function mobileTargetAmbiguityError() {
226
- const choices = [
227
- "1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
228
- "2. Android mm-harness prepare --platform mobile --mobile-platform android"
229
- ];
230
- return {
231
- code: "PREPARE_MOBILE_TARGET_AMBIGUOUS",
232
- message: "could not infer the mobile target from the connected devices",
233
- userAction: `choose the mobile target, then rerun this command:
234
- ${choices.join("\n")}`
235
- };
236
- }
237
- function ambiguousMobileTarget(json) {
238
- const { code, message, userAction } = mobileTargetAmbiguityError();
239
- if (json) {
240
- console.log(
241
- JSON.stringify(
242
- {
243
- schemaVersion: 1,
244
- command: "prepare",
245
- status: "fail",
246
- exitCode: EXIT.runtime,
247
- error: { code, message, userAction }
248
- },
249
- null,
250
- 2
251
- )
252
- );
253
- } else {
254
- console.error(`\u2717 mm-harness prepare: ${message}`);
255
- console.error(` Next: ${userAction}`);
256
- }
257
- return EXIT.runtime;
258
- }
259
- function mobilePlatformOption(value) {
260
- if (value === void 0) return void 0;
261
- if (value === "ios") return "ios";
262
- if (value === "android") return "android";
263
- throw usageError(
264
- `--mobile-platform must be ios or android; received '${value}'
265
- Next: ${USAGE}`
266
- );
267
- }
268
- function resolvePlatform(target) {
269
- const platform = contextAdapter(target);
270
- const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
271
- if (surface?.prepare) {
272
- return surface;
273
- }
274
- throw usageError(
275
- `could not detect the platform of ${target}
276
- Next: ${USAGE}`
277
- );
278
- }
279
- async function handlePrepare(argv) {
280
- const { options } = parseArgs(argv);
281
- const target = targetPath(options);
282
- if (!fs.existsSync(target)) return handlePrepareLocked(argv);
283
- const lock = acquireCheckoutLock(target, "prepare");
284
- if ("message" in lock) {
285
- if (optionFlag(options, "json")) console.log(JSON.stringify({ status: "fail", error: { code: "SANDBOX_BUSY", message: lock.message }, exitCode: EXIT.runtime }));
286
- else console.error(`${lock.message} Next: inspect the owning operation with mm-harness status --watch`);
287
- return EXIT.runtime;
288
- }
289
- try {
290
- return await handlePrepareLocked(argv);
291
- } finally {
292
- lock.release();
293
- }
294
- }
295
- async function handlePrepareLocked(argv) {
296
- const { options } = parseArgs(argv);
297
- const json = optionFlag(options, "json");
298
- const target = targetPath(options);
299
- const surface = resolvePlatform(target);
300
- const platform = surface.id;
301
- const artifactsDir = path.resolve(
302
- optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
303
- );
304
- const executable = harnessExecutable();
305
- const targetArgs = ["--target", target];
306
- const clearMetro = optionFlag(options, "clearMetro");
307
- if (clearMetro && !surface.prepare.clearMetro) {
308
- throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
309
- Next: ${USAGE}`);
310
- }
311
- const remoteFlag = optionString(options, "remoteFlag");
312
- if (remoteFlag !== void 0) {
313
- if (surface.headless) {
314
- throw usageError(`--remote-flag needs an app runtime; this checkout is ${platform}
315
- Next: ${USAGE}`);
316
- }
317
- try {
318
- parseRemoteFlagSpec(remoteFlag);
319
- } catch (error) {
320
- if (!(error instanceof FeatureFlagSpecError)) throw error;
321
- throw usageError(`${error.message}
322
- Next: ${error.userAction}`);
323
- }
324
- }
325
- const remoteFlagArgs = remoteFlag === void 0 ? [] : ["--remote-flag", remoteFlag];
326
- let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
327
- const recordPath = path.join(artifactsDir, READINESS_RECORD);
328
- const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
329
- const chain = stepChain(surface);
330
- const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
331
- view?.begin();
332
- const steps = [];
333
- let activeStep;
334
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
335
- const writeProgress = () => {
336
- fs.mkdirSync(path.dirname(progressPath), { recursive: true });
337
- const temporaryPath = `${progressPath}.${process.pid}.tmp`;
338
- fs.writeFileSync(temporaryPath, `${JSON.stringify({
339
- schemaVersion: 1,
340
- startedAt,
341
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
342
- total: chain.length,
343
- steps: steps.filter(({ id }) => chain.includes(id)).map(({ id, status, durationMs }) => ({ id, status, durationMs })),
344
- ...activeStep ? { active: activeStep } : {}
345
- })}
346
- `);
347
- fs.renameSync(temporaryPath, progressPath);
348
- };
349
- const failed = () => steps.some((step) => step.status === "fail");
350
- const addStep = (step) => {
351
- steps.push(step);
352
- activeStep = void 0;
353
- writeProgress();
354
- view?.finish(step);
355
- return step;
356
- };
357
- const run = async (id, args) => {
358
- if (failed()) {
359
- return addStep(skippedStep(id, [executable, ...args].join(" "), SKIPPED_AFTER_FAILURE));
360
- }
361
- const step = await runStep(executable, target, artifactsDir, id, args, {
362
- json,
363
- onSpawn: (stepStartedAt) => {
364
- recordCommandStage(id);
365
- activeStep = { id, startedAt: new Date(stepStartedAt).toISOString() };
366
- writeProgress();
367
- view?.start(id, stepStartedAt);
368
- }
369
- });
370
- return addStep(step);
371
- };
372
- const heartbeat = setInterval(() => {
373
- if (activeStep) writeProgress();
374
- }, 1e4);
375
- try {
376
- await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
377
- await run("status", ["status", "--json", ...targetArgs]);
378
- const { devicePlatform } = surface.prepare;
379
- if (devicePlatform && !mobilePlatform && !failed()) {
380
- mobilePlatform = devicePlatform(target, readDevices(artifactsDir));
381
- if (!mobilePlatform) return ambiguousMobileTarget(json);
382
- }
383
- if (surface.headless) {
384
- const reason = `${platform} is headless: no app surface to launch or seed`;
385
- addStep(skippedStep("launch", `${executable} launch`, reason));
386
- addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
387
- } else {
388
- const device = devicePlatform && mobilePlatform ? [mobilePlatform] : [];
389
- await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
390
- await run("fixtures", [
391
- "fixtures",
392
- "set",
393
- ...device.flatMap((platformName) => ["--platform", platformName]),
394
- ...remoteFlagArgs,
395
- "--json",
396
- ...targetArgs
397
- ]);
398
- }
399
- await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
400
- } finally {
401
- clearInterval(heartbeat);
402
- view?.end();
403
- }
404
- const provenance = runnerProvenance(surface.actions.manifestPath());
405
- const record = {
406
- schemaVersion: 1,
407
- harness: {
408
- name: provenance.packageName,
409
- version: provenance.version,
410
- source: `${provenance.installKind}/${provenance.packageSource}`,
411
- executable: provenance.executablePath
412
- },
413
- platform,
414
- ...mobilePlatform ? { mobilePlatform } : {},
415
- steps,
416
- ready: steps.every((step) => step.status !== "fail"),
417
- recordedAt: (/* @__PURE__ */ new Date()).toISOString()
418
- };
419
- fs.mkdirSync(artifactsDir, { recursive: true });
420
- fs.writeFileSync(recordPath, `${JSON.stringify(record, null, 2)}
421
- `);
422
- fs.rmSync(progressPath, { force: true });
423
- if (json) {
424
- console.log(JSON.stringify(record, null, 2));
425
- } else {
426
- console.log(`readiness record: ${recordPath}`);
427
- }
428
- return record.ready ? EXIT.ok : EXIT.runtime;
429
- }
430
- export {
431
- handlePrepare,
432
- mobileTargetAmbiguityError
433
- };
@@ -1,103 +0,0 @@
1
- import { EXIT, usageOut, optionFlag, optionString, parseArgs } from "@farmslot/recipe-cli/harness";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import {
5
- buildRecipeQualityArtifact
6
- } from "@farmslot/agent-runtime";
7
- const BUILD_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]";
8
- const SHORTHAND_USAGE = "mm-harness recipe-quality build --input <compact.json> --output <path> [--json]\n Shorthand: mm-harness recipe-quality --input <compact.json> --output <path> [--json]\n Note: apply the quality bar from mm-harness help, then build its compact verdict here.";
9
- function errorMessage(error) {
10
- return error instanceof Error ? error.message : String(error);
11
- }
12
- function invalidOut(json, message, source) {
13
- const userAction = `fix ${source} (${message}), then re-run: ${BUILD_USAGE}`;
14
- if (json) {
15
- console.log(
16
- JSON.stringify(
17
- {
18
- schemaVersion: 1,
19
- command: "recipe-quality",
20
- action: "build",
21
- status: "fail",
22
- exitCode: EXIT.validation,
23
- error: { code: "RECIPE_QUALITY_INVALID", message, userAction }
24
- },
25
- null,
26
- 2
27
- )
28
- );
29
- } else {
30
- console.error(`\u2717 mm-harness recipe-quality: ${message}
31
- Next: ${userAction}`);
32
- }
33
- return EXIT.validation;
34
- }
35
- async function handleRecipeQuality(argv) {
36
- const { positional, options } = parseArgs(argv);
37
- const json = optionFlag(options, "json");
38
- const action = positional[0];
39
- if (action === "advise" || action === "plan" || action === "advisor-status") {
40
- const { handleRecipeAdvice } = await import("./recipe-advice.js");
41
- return handleRecipeAdvice(options, action === "advisor-status" ? "status" : action === "plan" ? "plan" : "review");
42
- }
43
- if (["advisor", "advisorModel", "advisorStages", "recipe", "task", "evidence", "adapter", "library", "params"].some((key) => options[key] !== void 0)) {
44
- return usageOut(json, "recipe-quality", "advice options require the advise subcommand", "mm-harness recipe-quality advise --recipe <recipe.json> --task <TASK.md> --adapter <adapter> --advisor <provider>");
45
- }
46
- if (!action && (optionString(options, "input") || optionString(options, "output"))) {
47
- return buildArtifact(options, json);
48
- }
49
- if (action !== "build") {
50
- const message = action ? `unknown action '${action}'` : "missing action: use build, plan, or advise";
51
- return usageOut(json, "recipe-quality", message, SHORTHAND_USAGE);
52
- }
53
- return buildArtifact(options, json);
54
- }
55
- function buildArtifact(options, json) {
56
- const input = optionString(options, "input");
57
- const output = optionString(options, "output");
58
- if (!input) return usageOut(json, "recipe-quality", "missing --input <compact.json>", BUILD_USAGE);
59
- if (!output) return usageOut(json, "recipe-quality", "missing --output <path>", BUILD_USAGE);
60
- const inputPath = path.resolve(input);
61
- let raw;
62
- try {
63
- raw = fs.readFileSync(inputPath, "utf8");
64
- } catch (error) {
65
- return usageOut(
66
- json,
67
- "recipe-quality",
68
- `cannot read --input ${input}: ${errorMessage(error)}`,
69
- `write the compact recipe-quality JSON to that path, then: ${BUILD_USAGE}`
70
- );
71
- }
72
- let parsed;
73
- try {
74
- parsed = JSON.parse(raw);
75
- } catch (error) {
76
- return invalidOut(json, `--input ${input} is not valid JSON: ${errorMessage(error)}`, `--input ${input}`);
77
- }
78
- let artifact;
79
- try {
80
- artifact = buildRecipeQualityArtifact(parsed);
81
- } catch (error) {
82
- return invalidOut(json, errorMessage(error), `--input ${input}`);
83
- }
84
- const outputPath = path.resolve(output);
85
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
86
- fs.writeFileSync(outputPath, `${JSON.stringify(artifact, null, 2)}
87
- `);
88
- if (json) {
89
- console.log(
90
- JSON.stringify(
91
- { schemaVersion: 1, command: "recipe-quality", action: "build", status: "ok", outputPath: output, artifact },
92
- null,
93
- 2
94
- )
95
- );
96
- } else {
97
- console.log(`\u2713 recipe-quality build \u2192 ${output} (verdict: ${artifact.verdict})`);
98
- }
99
- return EXIT.ok;
100
- }
101
- export {
102
- handleRecipeQuality
103
- };