@deeeed/metamask-harness 0.52.0 → 0.53.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 +20 -0
- package/README.md +5 -0
- package/dist/checkout-view.js +112 -0
- package/dist/cli-commands.js +1 -1
- package/dist/command-contract.js +5 -2
- package/dist/commands/check.js +21 -7
- package/dist/commands/doctor.js +5 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/prepare.js +7 -2
- package/dist/commands/status-watch.js +216 -0
- package/dist/commands/status.js +18 -1
- package/dist/commands/task-init.js +2 -1
- package/dist/commands/update.js +7 -1
- package/dist/doctor.js +12 -3
- package/dist/mm-harness-cli.js +32 -7
- package/dist/review/knowledge.js +34 -3
- package/dist/task-view.js +247 -0
- package/docs/RECIPES.md +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.53.0 - 2026-09-17
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `doctor` and `status` print one checkout view, also carried under `view` in their `--json`: the runtime-context fields that keep this checkout off other slots' ports and devices (mobile adds `watcherPort`, `metroPort`, `devServerPort`, `simulator`, `simulatorUdid`; extension adds `devServerPort`), the recipe libraries this machine resolves with their revision and recipe/checklist counts, and the wallet fixture the checkout last applied. Values are an allowlist — no fixture body, key, or phrase is ever rendered.
|
|
10
|
+
- `prepare --clear-metro` passes the flag to its launch step, so the stale-Metro recovery `doctor` teaches is reachable through the sanctioned prepare path. Nothing clears the transform cache on its own; the flag stays an explicit operator decision, and it is refused on extension and core.
|
|
11
|
+
- `status --watch` shows a live read-only view of the task a checkout is working through: slot and device isolation, template and flow, fixture and sandbox readiness, and the checklist with the time each row took and how long the current row has been running. `--task <dir>` selects a task directory instead of the most recently marked one under `temp/tasks/`, and prints the view once without `--watch`. It reads task files only, so watching never probes a device or disturbs the run. Piped output logs one line per change; `--json` emits the view model, one object per change while watching. When nothing is in progress it says `no ongoing task detected` with the reason (finished, silent, or no task yet) and whether the checked-out branch carries the task's ticket, and it keeps waiting for a new task instead of exiting on an old finished one. Rows are grouped by checklist phase.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The base review, which every `review checklist` starts with in any repo and domain, gains a signal-over-noise rule: comments that say why and stay short, no ticket keys or tool mentions in source, no leftover scaffolding, swallowed errors, one-caller abstractions or padded tests and PR text.
|
|
16
|
+
- `task init` no longer requires `--run-mode`. It defaults to `autonomous`; the distinction belongs to a control plane, and an engineer running a skill steers by hand.
|
|
17
|
+
- Every library this machine resolves — engineer config, a sibling checkout, the fetch cache — now reaches recipe, action, and template discovery, not only the ones `RECIPE_LIBRARY_PATH` names. An engineer who never exported the variable sees the same recipes as an orchestrator that injects it. Entries already in the variable keep their precedence and order, the personal library still applies, and nothing is fetched.
|
|
18
|
+
- A runtime-context field overridden from the environment now names the variable that actually supplied the value instead of the canonical one.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `check diff` runs a changed `<name>.<layer>.test.*` file under `jest.config.<layer>.*` when the repo has one. MetaMask Mobile keeps view and integration tests out of its default Jest config, so a task that changed a view test got "No tests found" and a failed gate.
|
|
23
|
+
- `mm-harness update` and `update --check` no longer report `REGISTRY_UNREACHABLE` inside a checkout whose `.npmrc` sets `offline = true`. The registry lookup and the global install run from a neutral directory, so a project can no longer answer for a global install.
|
|
24
|
+
|
|
5
25
|
## 0.52.0 - 2026-09-17
|
|
6
26
|
|
|
7
27
|
### Added
|
package/README.md
CHANGED
|
@@ -189,6 +189,11 @@ mm-harness run --list
|
|
|
189
189
|
Shared libraries hold durable actions and composable recipes. Task acceptance
|
|
190
190
|
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
191
191
|
|
|
192
|
+
Exporting is not required: a library registered with `mm-harness config set
|
|
193
|
+
libraries.<name> <path>`, or checked out beside the product checkout, reaches
|
|
194
|
+
`run`, `actions`, and template discovery too. `mm-harness status` lists what this
|
|
195
|
+
machine resolves, with each location's revision and recipe count.
|
|
196
|
+
|
|
192
197
|
Teams maintain their library checkouts. Update them before starting a task;
|
|
193
198
|
the harness records local revisions and recipe digests but does not fetch or
|
|
194
199
|
check for upstream updates. Keep the selected library unchanged during a run.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { color } from "./cli-color.js";
|
|
4
|
+
import { renderRuntimeContext, runtimeContextSummary } from "./doctor.js";
|
|
5
|
+
import { recipeRuntimeDir } from "./paths.js";
|
|
6
|
+
import {
|
|
7
|
+
configPath,
|
|
8
|
+
declaredLibraryPathEnv,
|
|
9
|
+
discoverLibraries,
|
|
10
|
+
readConfig
|
|
11
|
+
} from "./review/knowledge.js";
|
|
12
|
+
const NO_LIBRARIES = "none registered. Learn how: docs/RECIPES.md, or run: mm-harness config set libraries.<name> <path>";
|
|
13
|
+
const FIXTURE_REPORT = path.join("prepare", "fixtures.json");
|
|
14
|
+
function collectCheckoutView(target, adapter) {
|
|
15
|
+
const declared = declaredLibraryPathEnv();
|
|
16
|
+
const libraries = discoverLibraries({
|
|
17
|
+
target,
|
|
18
|
+
config: readConfig(configPath()),
|
|
19
|
+
...declared ? { env: { ...process.env, RECIPE_LIBRARY_PATH: declared.value } } : {}
|
|
20
|
+
}).map((library) => ({
|
|
21
|
+
name: library.name,
|
|
22
|
+
root: library.root,
|
|
23
|
+
source: library.source,
|
|
24
|
+
revision: library.revision ?? null,
|
|
25
|
+
recipes: countFiles(library.root, "recipes", ".json"),
|
|
26
|
+
checklists: countFiles(library.root, "checklists", ".md")
|
|
27
|
+
}));
|
|
28
|
+
const fixtures = readFixtureView(target);
|
|
29
|
+
return {
|
|
30
|
+
isolation: runtimeContextSummary(target, adapter),
|
|
31
|
+
libraries,
|
|
32
|
+
...fixtures ? { fixtures } : {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function renderCheckoutView(view) {
|
|
36
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
37
|
+
const lines = [renderRuntimeContext(view.isolation)];
|
|
38
|
+
if (view.libraries.length === 0) {
|
|
39
|
+
lines.push(`${out("label", "libraries:")} ${out("dim", NO_LIBRARIES)}`);
|
|
40
|
+
} else {
|
|
41
|
+
lines.push(`${out("label", "libraries:")} ${view.libraries.length}`);
|
|
42
|
+
for (const library of view.libraries) {
|
|
43
|
+
const pin = library.revision ? `${library.source} ${library.revision}` : library.source;
|
|
44
|
+
lines.push(
|
|
45
|
+
` ${library.name.padEnd(22)} ${out("ok", library.root)} ${out("accent", `[${pin}]`)} ` + out("dim", `\u2014 ${library.recipes} recipes, ${library.checklists} checklists`)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (view.fixtures) {
|
|
50
|
+
lines.push(`${out("label", "fixtures:")}`);
|
|
51
|
+
for (const [key, value] of Object.entries(view.fixtures)) {
|
|
52
|
+
const isSet = value !== null && value !== "";
|
|
53
|
+
lines.push(` ${key.padEnd(22)} ${isSet ? out("ok", String(value)) : out("dim", "(unset)")}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
function countFiles(root, directory, extension) {
|
|
59
|
+
const base = path.join(root, directory);
|
|
60
|
+
if (!isDirectory(base)) return 0;
|
|
61
|
+
return fs.readdirSync(base, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(extension)).length;
|
|
62
|
+
}
|
|
63
|
+
function readFixtureView(target) {
|
|
64
|
+
const newest = fixtureReports(target).map((file) => ({ file, modified: modifiedAt(file) })).filter((entry) => entry.modified !== void 0).sort((left, right) => right.modified - left.modified)[0];
|
|
65
|
+
if (!newest) return void 0;
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(fs.readFileSync(newest.file, "utf8"));
|
|
69
|
+
} catch {
|
|
70
|
+
return { fixture: null, canonicalFixture: null, status: "unreadable", walletReset: null };
|
|
71
|
+
}
|
|
72
|
+
const record = parsed ?? {};
|
|
73
|
+
const text = (key) => typeof record[key] === "string" ? record[key] : null;
|
|
74
|
+
return {
|
|
75
|
+
fixture: text("fixture"),
|
|
76
|
+
canonicalFixture: text("canonicalFixture"),
|
|
77
|
+
status: text("status"),
|
|
78
|
+
walletReset: typeof record.walletReset === "boolean" ? record.walletReset : null
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function fixtureReports(target) {
|
|
82
|
+
const reports = [path.join(target, recipeRuntimeDir(), FIXTURE_REPORT)];
|
|
83
|
+
const tasksRoot = path.join(target, "temp", "tasks");
|
|
84
|
+
for (const flow of directoryNames(tasksRoot)) {
|
|
85
|
+
for (const task of directoryNames(path.join(tasksRoot, flow))) {
|
|
86
|
+
reports.push(path.join(tasksRoot, flow, task, "artifacts", FIXTURE_REPORT));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return reports;
|
|
90
|
+
}
|
|
91
|
+
function directoryNames(root) {
|
|
92
|
+
if (!isDirectory(root)) return [];
|
|
93
|
+
return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
94
|
+
}
|
|
95
|
+
function isDirectory(candidate) {
|
|
96
|
+
try {
|
|
97
|
+
return fs.statSync(candidate).isDirectory();
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function modifiedAt(file) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.statSync(file).mtimeMs;
|
|
105
|
+
} catch {
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
collectCheckoutView,
|
|
111
|
+
renderCheckoutView
|
|
112
|
+
};
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,7 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "config", desc: "Per-engineer locations: libraries.<name>, references.<adapter>", args: ["list", "path", "get", "set", "unset"], flags: ["--json"] },
|
|
16
16
|
{ name: "tutorial", desc: "Open the visual recipe tutorial", flags: ["--json", "--no-open"] },
|
|
17
17
|
{ name: "setup-base", desc: "Bootstrap numbered product checkouts", flags: ["--dir", "--counts", "--only", "--dry-run", "--force", "--json", "--show-config", "--reset-config", "--skip-harness-update"] },
|
|
18
|
-
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json", "--fast"] },
|
|
18
|
+
{ name: "status", aliases: ["health", "home"], desc: "Home status + next commands", flags: ["--json", "--fast", "--watch", "--task"] },
|
|
19
19
|
{ name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
|
|
20
20
|
{ name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
|
|
21
21
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
package/dist/command-contract.js
CHANGED
|
@@ -112,7 +112,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
112
112
|
aliases: ["health", "home"],
|
|
113
113
|
options: options(HELP, JSON, TARGET, DEVICE, {
|
|
114
114
|
"--fast": bool(),
|
|
115
|
-
"--all-devices": bool()
|
|
115
|
+
"--all-devices": bool(),
|
|
116
|
+
"--watch": bool(),
|
|
117
|
+
"--task": value()
|
|
116
118
|
})
|
|
117
119
|
},
|
|
118
120
|
checklist: {
|
|
@@ -147,10 +149,11 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
147
149
|
minimumPositionals: 2
|
|
148
150
|
},
|
|
149
151
|
prepare: {
|
|
150
|
-
usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--artifacts-dir <dir>] [--json]",
|
|
152
|
+
usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]",
|
|
151
153
|
options: options(HELP, JSON, TARGET, {
|
|
152
154
|
"--platform": value(["mobile", "extension", "core"]),
|
|
153
155
|
"--mobile-platform": value(["ios", "android"]),
|
|
156
|
+
"--clear-metro": bool(),
|
|
154
157
|
"--artifacts-dir": value()
|
|
155
158
|
})
|
|
156
159
|
},
|
package/dist/commands/check.js
CHANGED
|
@@ -312,17 +312,18 @@ function runToolCheck(input) {
|
|
|
312
312
|
}
|
|
313
313
|
function runJestChecks(input) {
|
|
314
314
|
if (input.adapter !== "core" || input.files.length === 0) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
315
|
+
const groups2 = groupByJestConfig(input.target, input.files);
|
|
316
|
+
return groups2.map(
|
|
317
|
+
(group) => runToolCheck({
|
|
318
|
+
id: groups2.length === 1 || !group.layer ? "jest" : `jest-${safeId(group.layer)}`,
|
|
319
|
+
label: group.layer ? `Changed ${group.layer} test files` : "Changed test files",
|
|
319
320
|
target: input.target,
|
|
320
321
|
artifactsDir: input.artifactsDir,
|
|
321
|
-
files:
|
|
322
|
+
files: group.files,
|
|
322
323
|
command: executable(input.target, "jest"),
|
|
323
|
-
args: ["--no-coverage"]
|
|
324
|
+
args: group.config ? ["--no-coverage", "-c", group.config] : ["--no-coverage"]
|
|
324
325
|
})
|
|
325
|
-
|
|
326
|
+
);
|
|
326
327
|
}
|
|
327
328
|
const groups = /* @__PURE__ */ new Map();
|
|
328
329
|
const rootFiles = [];
|
|
@@ -367,6 +368,18 @@ function runJestChecks(input) {
|
|
|
367
368
|
}
|
|
368
369
|
return results;
|
|
369
370
|
}
|
|
371
|
+
function groupByJestConfig(target, files) {
|
|
372
|
+
const groups = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const file of files) {
|
|
374
|
+
const layer = /\.([a-z][a-z0-9-]*)\.test\.[cm]?[jt]sx?$/u.exec(path.basename(file))?.[1];
|
|
375
|
+
const config = layer ? ["js", "cjs", "mjs", "ts"].map((ext) => `jest.config.${layer}.${ext}`).find((name) => fs.existsSync(path.join(target, name))) : void 0;
|
|
376
|
+
const key = config ?? "";
|
|
377
|
+
const group = groups.get(key) ?? { ...config ? { layer, config } : {}, files: [] };
|
|
378
|
+
group.files.push(file);
|
|
379
|
+
groups.set(key, group);
|
|
380
|
+
}
|
|
381
|
+
return [...groups.values()].sort((a, b) => (a.config ?? "").localeCompare(b.config ?? ""));
|
|
382
|
+
}
|
|
370
383
|
function resolveWorkspace(target, file) {
|
|
371
384
|
const root = path.resolve(target);
|
|
372
385
|
let current = path.dirname(path.resolve(root, file));
|
|
@@ -555,5 +568,6 @@ function failEnvelope(input) {
|
|
|
555
568
|
return exitCode;
|
|
556
569
|
}
|
|
557
570
|
export {
|
|
571
|
+
groupByJestConfig,
|
|
558
572
|
handleCheck
|
|
559
573
|
};
|
package/dist/commands/doctor.js
CHANGED
|
@@ -3,7 +3,8 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { color } from "../cli-color.js";
|
|
6
|
-
import {
|
|
6
|
+
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
7
|
+
import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
|
|
7
8
|
import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
|
|
8
9
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
9
10
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
@@ -139,6 +140,7 @@ async function handleDoctor({ options }) {
|
|
|
139
140
|
actionManifestPath,
|
|
140
141
|
mobilePlatform
|
|
141
142
|
);
|
|
143
|
+
const view = collectCheckoutView(target, adapter);
|
|
142
144
|
const missingRequiredTool = result.checks.find(
|
|
143
145
|
(check) => check.required && check.status === "fail" && check.id.startsWith("device-tool-") && check.userAction
|
|
144
146
|
);
|
|
@@ -209,7 +211,7 @@ async function handleDoctor({ options }) {
|
|
|
209
211
|
)?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
210
212
|
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
211
213
|
const next = doctorError?.userAction ?? discovery.deviceDiscoveryErrors?.[0]?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
|
|
212
|
-
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
214
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, view, orphanMetros, capture, devices, additionalReachableDevices, ...discovery, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
213
215
|
else {
|
|
214
216
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
215
217
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -230,7 +232,7 @@ async function handleDoctor({ options }) {
|
|
|
230
232
|
} else if (runtimeProbeError) {
|
|
231
233
|
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
232
234
|
}
|
|
233
|
-
console.log(
|
|
235
|
+
console.log(renderCheckoutView(view));
|
|
234
236
|
if (adapter === "mobile") {
|
|
235
237
|
const perps = mobilePerpsEnvironment(target);
|
|
236
238
|
console.log(
|
package/dist/commands/prepare.js
CHANGED
|
@@ -9,7 +9,7 @@ import { detectAdapter } from "../harness.js";
|
|
|
9
9
|
import { harnessExecutable, manifestPath, recipeRuntimeDir } from "../paths.js";
|
|
10
10
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
11
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]";
|
|
12
|
+
const USAGE = "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]";
|
|
13
13
|
const REPORT_DIR = "prepare";
|
|
14
14
|
const SKIPPED_AFTER_FAILURE = "previous step failed";
|
|
15
15
|
function reportPathFor(id) {
|
|
@@ -166,6 +166,11 @@ async function handlePrepare(argv) {
|
|
|
166
166
|
);
|
|
167
167
|
const executable = harnessExecutable();
|
|
168
168
|
const targetArgs = ["--target", target];
|
|
169
|
+
const clearMetro = optionFlag(options, "clearMetro");
|
|
170
|
+
if (clearMetro && platform !== "mobile") {
|
|
171
|
+
throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
|
|
172
|
+
Next: ${USAGE}`);
|
|
173
|
+
}
|
|
169
174
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
170
175
|
const steps = [];
|
|
171
176
|
const failed = () => steps.some((step) => step.status === "fail");
|
|
@@ -189,7 +194,7 @@ async function handlePrepare(argv) {
|
|
|
189
194
|
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
190
195
|
} else if (platform === "mobile") {
|
|
191
196
|
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
192
|
-
run("launch", ["launch", ...device, "--verify", "--json", ...targetArgs]);
|
|
197
|
+
run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], "--verify", "--json", ...targetArgs]);
|
|
193
198
|
run("fixtures", [
|
|
194
199
|
"fixtures",
|
|
195
200
|
"set",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { color } from "../cli-color.js";
|
|
4
|
+
import {
|
|
5
|
+
CHECKLIST_LABEL_LIMIT,
|
|
6
|
+
collectTaskView,
|
|
7
|
+
findTaskDir,
|
|
8
|
+
isTerminalStatus,
|
|
9
|
+
TASK_SILENT_AFTER_MS
|
|
10
|
+
} from "../task-view.js";
|
|
11
|
+
import { EXIT } from "./shared.js";
|
|
12
|
+
const REFRESH_MS = 2e3;
|
|
13
|
+
const CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
14
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
15
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
16
|
+
async function handleStatusTaskView(options) {
|
|
17
|
+
const deps = {
|
|
18
|
+
now: () => Date.now(),
|
|
19
|
+
wait: (ms) => new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, ms);
|
|
21
|
+
}),
|
|
22
|
+
write: (text) => process.stdout.write(text),
|
|
23
|
+
isTty: Boolean(process.stdout.isTTY)
|
|
24
|
+
};
|
|
25
|
+
if (!options.watch || !deps.isTty) return runStatusWatch(options, deps);
|
|
26
|
+
const restore = () => deps.write(SHOW_CURSOR);
|
|
27
|
+
const interrupt = () => {
|
|
28
|
+
restore();
|
|
29
|
+
process.exit(EXIT.ok);
|
|
30
|
+
};
|
|
31
|
+
process.on("SIGINT", interrupt);
|
|
32
|
+
deps.write(HIDE_CURSOR);
|
|
33
|
+
try {
|
|
34
|
+
return await runStatusWatch(options, deps);
|
|
35
|
+
} finally {
|
|
36
|
+
process.off("SIGINT", interrupt);
|
|
37
|
+
restore();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function runStatusWatch(options, deps) {
|
|
41
|
+
const title = `mm-harness status ${options.watch ? "--watch" : "--task"}`;
|
|
42
|
+
let previous;
|
|
43
|
+
let sawInProgress = false;
|
|
44
|
+
for (; ; ) {
|
|
45
|
+
const taskDir = options.taskDir ?? findTaskDir(options.target);
|
|
46
|
+
const view = collectTaskView(options.target, taskDir, deps.now());
|
|
47
|
+
const changes = changedLines(previous, view);
|
|
48
|
+
if (options.json && !options.watch) {
|
|
49
|
+
deps.write(`${JSON.stringify(view, null, 2)}
|
|
50
|
+
`);
|
|
51
|
+
} else if (options.json) {
|
|
52
|
+
if (changes.length > 0) deps.write(`${JSON.stringify(view)}
|
|
53
|
+
`);
|
|
54
|
+
} else if (options.watch && !deps.isTty) {
|
|
55
|
+
for (const line of changes) deps.write(`${line}
|
|
56
|
+
`);
|
|
57
|
+
} else {
|
|
58
|
+
deps.write(`${deps.isTty && options.watch ? CLEAR_SCREEN : ""}${renderTaskView(view, title, options.taskDir !== void 0).join("\n")}
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
previous = view;
|
|
62
|
+
const terminal = isTerminalStatus(view.signal?.status);
|
|
63
|
+
if (view.activity.ongoing) sawInProgress = true;
|
|
64
|
+
if (!options.watch || terminal && (sawInProgress || options.taskDir !== void 0)) return EXIT.ok;
|
|
65
|
+
await deps.wait(REFRESH_MS);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function renderTaskView(view, title, explicitTask = false) {
|
|
69
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
70
|
+
const showTask = view.activity.ongoing || explicitTask;
|
|
71
|
+
const lines = [joinParts([
|
|
72
|
+
out("label", title),
|
|
73
|
+
out("bold", path.basename(view.checkout.target)),
|
|
74
|
+
view.checkout.branch && out("info", view.checkout.branch),
|
|
75
|
+
view.checkout.head && out("dim", `@${view.checkout.head}`),
|
|
76
|
+
showTask && view.elapsedMs !== void 0 && out("dim", `${formatDuration(view.elapsedMs)} elapsed`)
|
|
77
|
+
], " ")];
|
|
78
|
+
const isolation = view.isolation;
|
|
79
|
+
const device = isolation && joinParts([isolation.platform, isolation.simulator, shortUdid(isolation.simulatorUdid)], " ");
|
|
80
|
+
const devServer = isolation?.metroPort ?? isolation?.watcherPort ?? isolation?.devServerPort;
|
|
81
|
+
const runtimeLine = joinParts([
|
|
82
|
+
isolation?.slotId && `slot ${out("ok", isolation.slotId)}`,
|
|
83
|
+
device && out("ok", device),
|
|
84
|
+
devServer !== void 0 && `metro ${out("ok", String(devServer))}`,
|
|
85
|
+
isolation?.cdpPort !== void 0 && `cdp ${out("ok", String(isolation.cdpPort))}`,
|
|
86
|
+
isolation?.extensionId && `extension ${out("ok", isolation.extensionId)}`,
|
|
87
|
+
showTask && view.harness?.version && `harness ${out("info", view.harness.version)}${view.harness.sourceKind ? out("dim", ` (${view.harness.sourceKind})`) : ""}`
|
|
88
|
+
]);
|
|
89
|
+
if (runtimeLine) lines.push(runtimeLine);
|
|
90
|
+
const template = showTask ? view.template : void 0;
|
|
91
|
+
const templateLine = joinParts([
|
|
92
|
+
template?.id && `template ${out("accent", template.id)}${template.sourceId ? out("dim", ` (${template.sourceId})`) : ""}`,
|
|
93
|
+
template?.flow && `flow ${out("info", template.flow)}`,
|
|
94
|
+
template?.domain && `domain ${out("info", template.domain)}`,
|
|
95
|
+
template?.surface && `via ${out("info", template.surface)}`
|
|
96
|
+
]);
|
|
97
|
+
if (templateLine) lines.push(templateLine);
|
|
98
|
+
const taskLine = joinParts([
|
|
99
|
+
template?.ticket && out("bold", template.ticket),
|
|
100
|
+
template?.title && out("info", template.title.trim())
|
|
101
|
+
], " ");
|
|
102
|
+
if (taskLine) lines.push(`${out("label", "task")} ${taskLine}`);
|
|
103
|
+
const contextLine = joinParts([
|
|
104
|
+
view.libraries.length > 0 && `libraries ${out("info", view.libraries.map(libraryLabel).join(" "))}`,
|
|
105
|
+
showTask && view.fixture && `fixture ${out("info", path.basename(view.fixture.fixture ?? "unknown"))} ${statusWord(out, view.fixture.status)}${view.fixture.walletReset ? out("warn", " reset") : ""}`,
|
|
106
|
+
showTask && view.sandbox?.ready !== void 0 && `sandbox ${view.sandbox.ready ? out("ok", "ready") : out("warn", "not ready")}`
|
|
107
|
+
]);
|
|
108
|
+
if (contextLine) lines.push(contextLine);
|
|
109
|
+
lines.push("");
|
|
110
|
+
if (!view.activity.ongoing) {
|
|
111
|
+
lines.push(out("warn", "no ongoing task detected"));
|
|
112
|
+
lines.push(out("dim", idleReason(view)));
|
|
113
|
+
if (!view.taskDir || !explicitTask) return lines;
|
|
114
|
+
lines.push("");
|
|
115
|
+
}
|
|
116
|
+
let phase;
|
|
117
|
+
for (const row of view.checklist) {
|
|
118
|
+
if (row.phase && row.phase !== phase) {
|
|
119
|
+
const inPhase = view.checklist.filter((other) => other.phase === row.phase);
|
|
120
|
+
const done = inPhase.filter((other) => other.checked).length;
|
|
121
|
+
lines.push(out("dim", ` ${row.phase} ${done}/${inPhase.length}`));
|
|
122
|
+
}
|
|
123
|
+
phase = row.phase;
|
|
124
|
+
lines.push(renderRow(row, view.currentStep, out));
|
|
125
|
+
}
|
|
126
|
+
const signal = view.signal;
|
|
127
|
+
if (signal) {
|
|
128
|
+
const age = view.checklist.find((row) => row.runningMs !== void 0)?.runningMs;
|
|
129
|
+
lines.push("");
|
|
130
|
+
lines.push(joinParts([
|
|
131
|
+
`signal ${statusWord(out, signal.status)}`,
|
|
132
|
+
// SIGNAL.json records the last row ticked, not the row in progress.
|
|
133
|
+
signal.step && `last ticked ${out("info", stepLabel(signal.step))}`,
|
|
134
|
+
// A cancelled agent leaves status "running"; the age is the only honest hint.
|
|
135
|
+
age !== void 0 && `no update for ${out(age > TASK_SILENT_AFTER_MS ? "warn" : "dim", formatDuration(age))}`
|
|
136
|
+
]));
|
|
137
|
+
}
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
function idleReason(view) {
|
|
141
|
+
if (!view.taskDir) return "no task directory under temp/tasks";
|
|
142
|
+
const { reason, idleMs, branchCarriesTicket } = view.activity;
|
|
143
|
+
if (reason === "no-task") return `${path.basename(view.taskDir)} holds no task files yet`;
|
|
144
|
+
const what = reason === "finished" ? view.signal?.status ?? "finished" : "silent";
|
|
145
|
+
return joinParts([
|
|
146
|
+
`last task ${path.basename(view.taskDir)}: ${what}`,
|
|
147
|
+
idleMs !== void 0 && `last update ${formatDuration(idleMs)} ago`,
|
|
148
|
+
branchCarriesTicket === false && `checkout is on ${view.checkout.branch}, which does not carry ${view.template?.ticket}`
|
|
149
|
+
], ", ");
|
|
150
|
+
}
|
|
151
|
+
function changedLines(previous, next) {
|
|
152
|
+
if (!previous || previous.taskDir !== next.taskDir) {
|
|
153
|
+
return [joinParts([
|
|
154
|
+
path.basename(next.checkout.target),
|
|
155
|
+
next.taskDir ? path.basename(next.taskDir) : "no-task-dir",
|
|
156
|
+
`status=${next.signal?.status ?? "unknown"}`,
|
|
157
|
+
`step=${next.signal?.step ? stepLabel(next.signal.step) : "none"}`
|
|
158
|
+
], " ")];
|
|
159
|
+
}
|
|
160
|
+
const lines = [];
|
|
161
|
+
const before = new Map(previous.checklist.map((row) => [row.number, row.checked]));
|
|
162
|
+
for (const row of next.checklist) {
|
|
163
|
+
if (!row.checked || before.get(row.number) === true) continue;
|
|
164
|
+
lines.push(`checked ${row.number} ${row.label}${row.durationMs !== void 0 ? ` (${formatDuration(row.durationMs)})` : ""}`);
|
|
165
|
+
}
|
|
166
|
+
if (previous.signal?.step !== next.signal?.step && next.signal?.step) {
|
|
167
|
+
lines.push(`step ${stepLabel(next.signal.step)}`);
|
|
168
|
+
}
|
|
169
|
+
if (previous.signal?.status !== next.signal?.status) {
|
|
170
|
+
lines.push(`status ${next.signal?.status ?? "unknown"}`);
|
|
171
|
+
}
|
|
172
|
+
return lines;
|
|
173
|
+
}
|
|
174
|
+
function formatDuration(ms) {
|
|
175
|
+
const total = Math.max(0, Math.round(ms / 1e3));
|
|
176
|
+
const seconds = String(total % 60).padStart(2, "0");
|
|
177
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
178
|
+
const hours = Math.floor(total / 3600);
|
|
179
|
+
if (hours === 0) return `${minutes}:${seconds}`;
|
|
180
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`;
|
|
181
|
+
}
|
|
182
|
+
function renderRow(row, currentStep, out) {
|
|
183
|
+
const marker = row.checked ? out("ok", "\u2713") : row.number === currentStep ? out("accent", "\u25B6") : out("dim", "\xB7");
|
|
184
|
+
const timing = row.durationMs !== void 0 ? formatDuration(row.durationMs) : row.runningMs !== void 0 ? `${formatDuration(row.runningMs)} \u2026` : "";
|
|
185
|
+
const number = String(row.number).padStart(2);
|
|
186
|
+
const label = timing ? row.label.padEnd(CHECKLIST_LABEL_LIMIT) : row.label;
|
|
187
|
+
const text = row.checked ? out("dim", label) : out("bold", label);
|
|
188
|
+
return ` ${marker} ${out("dim", number)} ${text}${timing ? ` ${out("dim", timing)}` : ""}`;
|
|
189
|
+
}
|
|
190
|
+
function libraryLabel(library) {
|
|
191
|
+
const home = os.homedir();
|
|
192
|
+
const location = home && library.path.startsWith(`${home}/`) ? `~${library.path.slice(home.length)}` : library.path;
|
|
193
|
+
return library.name ? `${library.name}=${location}` : location;
|
|
194
|
+
}
|
|
195
|
+
function statusWord(out, status) {
|
|
196
|
+
if (!status) return out("dim", "unknown");
|
|
197
|
+
const style = ["pass", "ready", "complete", "no-change"].includes(status) ? "ok" : ["running", "started"].includes(status) ? "accent" : "err";
|
|
198
|
+
return out(style, status);
|
|
199
|
+
}
|
|
200
|
+
function stepLabel(step) {
|
|
201
|
+
return /^\s*(\d+)\./u.exec(step)?.[1] ?? step.trim();
|
|
202
|
+
}
|
|
203
|
+
function shortUdid(udid) {
|
|
204
|
+
if (!udid) return void 0;
|
|
205
|
+
return `(${udid.split("-")[0]})`;
|
|
206
|
+
}
|
|
207
|
+
function joinParts(parts, separator = " \xB7 ") {
|
|
208
|
+
return parts.filter((part) => Boolean(part)).join(separator);
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
changedLines,
|
|
212
|
+
formatDuration,
|
|
213
|
+
handleStatusTaskView,
|
|
214
|
+
renderTaskView,
|
|
215
|
+
runStatusWatch
|
|
216
|
+
};
|
package/dist/commands/status.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { color } from "../cli-color.js";
|
|
4
|
+
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
2
5
|
import { detectAdapter } from "../harness.js";
|
|
3
6
|
import { assertAdapter } from "../paths.js";
|
|
4
7
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
@@ -15,10 +18,20 @@ import {
|
|
|
15
18
|
renderMobileLiveBlock
|
|
16
19
|
} from "./mobile-device-view.js";
|
|
17
20
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
18
|
-
import {
|
|
21
|
+
import { handleStatusTaskView } from "./status-watch.js";
|
|
22
|
+
import { optionFlag, optionString, targetPath } from "./parse-args.js";
|
|
19
23
|
async function handleStatus({ options }) {
|
|
20
24
|
const target = targetPath(options);
|
|
21
25
|
const json = optionFlag(options, "json");
|
|
26
|
+
const watch = optionFlag(options, "watch");
|
|
27
|
+
const taskDir = optionString(options, "task");
|
|
28
|
+
if (watch || taskDir) {
|
|
29
|
+
const resolvedTask = taskDir ? path.resolve(taskDir) : void 0;
|
|
30
|
+
if (resolvedTask && !(fs.existsSync(resolvedTask) && fs.statSync(resolvedTask).isDirectory())) {
|
|
31
|
+
return usageOut(json, "status", `--task is not a directory: ${resolvedTask}`, "Pass a task directory under temp/tasks, or omit --task to follow the newest one.");
|
|
32
|
+
}
|
|
33
|
+
return handleStatusTaskView({ target, taskDir: resolvedTask, json, watch });
|
|
34
|
+
}
|
|
22
35
|
const fast = optionFlag(options, "fast");
|
|
23
36
|
const allDevices = optionFlag(options, "allDevices");
|
|
24
37
|
const adapter = detectAdapter(target);
|
|
@@ -26,6 +39,7 @@ async function handleStatus({ options }) {
|
|
|
26
39
|
return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
27
40
|
}
|
|
28
41
|
assertAdapter(adapter);
|
|
42
|
+
const view = collectCheckoutView(target, adapter);
|
|
29
43
|
const surface = getAdapterSurface(adapter);
|
|
30
44
|
surface.resolveSlotPorts(target);
|
|
31
45
|
if (adapter === "mobile") {
|
|
@@ -50,6 +64,7 @@ async function handleStatus({ options }) {
|
|
|
50
64
|
command: "status",
|
|
51
65
|
adapter,
|
|
52
66
|
target,
|
|
67
|
+
view,
|
|
53
68
|
devices: liveView.devicesWithLive,
|
|
54
69
|
...discovery,
|
|
55
70
|
...liveView.additionalReachableDevices.length > 0 ? { additionalReachableDevices: liveView.additionalReachableDevices } : {},
|
|
@@ -69,6 +84,7 @@ async function handleStatus({ options }) {
|
|
|
69
84
|
command: "status",
|
|
70
85
|
adapter,
|
|
71
86
|
target,
|
|
87
|
+
view,
|
|
72
88
|
devices,
|
|
73
89
|
...discovery,
|
|
74
90
|
...runtime ? { runtime } : {},
|
|
@@ -83,6 +99,7 @@ async function handleStatus({ options }) {
|
|
|
83
99
|
}
|
|
84
100
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
85
101
|
console.log(`${out("label", "status")} ${out("bold", adapter)} ${out("dim", target)}`);
|
|
102
|
+
console.log(renderCheckoutView(view));
|
|
86
103
|
if (deviceView) renderMobileDeviceList(deviceView, out);
|
|
87
104
|
if (!fast && adapter === "mobile" && deviceView !== null && deviceView.allConnectedDevices.length > 0) {
|
|
88
105
|
const liveView = await mobileDeviceLiveView(target, deviceView);
|
|
@@ -4,13 +4,14 @@ import { detectAdapter } from "../harness.js";
|
|
|
4
4
|
import { harnessExecutable } from "../paths.js";
|
|
5
5
|
import { EXIT } from "./shared.js";
|
|
6
6
|
const require2 = createRequire(import.meta.url);
|
|
7
|
-
const USAGE = "mm-harness task init <task-dir> --flow f --
|
|
7
|
+
const USAGE = "mm-harness task init <task-dir> --flow f --template id --title t [options]";
|
|
8
8
|
function hasOption(argv, name) {
|
|
9
9
|
return argv.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
10
10
|
}
|
|
11
11
|
function metamaskDefaults(argv) {
|
|
12
12
|
const defaults = [];
|
|
13
13
|
if (!hasOption(argv, "--surface")) defaults.push("--surface", "skill");
|
|
14
|
+
if (!hasOption(argv, "--run-mode")) defaults.push("--run-mode", "autonomous");
|
|
14
15
|
if (!hasOption(argv, "--mark-command")) {
|
|
15
16
|
defaults.push("--mark-command", `${harnessExecutable()} checklist mark`);
|
|
16
17
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -8,6 +8,9 @@ const PACKAGE_NAME = "@deeeed/metamask-harness";
|
|
|
8
8
|
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9
9
|
const NUDGE_FETCH_TIMEOUT_MS = 2e3;
|
|
10
10
|
const REGISTRY_DIST_TAGS_ENDPOINT = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
|
|
11
|
+
function registryCwd() {
|
|
12
|
+
return os.homedir();
|
|
13
|
+
}
|
|
11
14
|
function currentVersion() {
|
|
12
15
|
try {
|
|
13
16
|
const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
|
|
@@ -38,6 +41,7 @@ function isNewer(candidate, base) {
|
|
|
38
41
|
}
|
|
39
42
|
function fetchLatest(timeoutMs) {
|
|
40
43
|
const result = spawnSync("npm", ["view", PACKAGE_NAME, "dist-tags.latest"], {
|
|
44
|
+
cwd: registryCwd(),
|
|
41
45
|
encoding: "utf8",
|
|
42
46
|
timeout: timeoutMs,
|
|
43
47
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -119,6 +123,7 @@ async function handleUpdate(argv) {
|
|
|
119
123
|
}
|
|
120
124
|
const INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
121
125
|
const install = spawnSync("npm", ["i", "-g", `${PACKAGE_NAME}@latest`], {
|
|
126
|
+
cwd: registryCwd(),
|
|
122
127
|
encoding: "utf8",
|
|
123
128
|
timeout: INSTALL_TIMEOUT_MS,
|
|
124
129
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -227,5 +232,6 @@ export {
|
|
|
227
232
|
isNewer,
|
|
228
233
|
maybeNudge,
|
|
229
234
|
nudgeDisabled,
|
|
230
|
-
nudgeLine
|
|
235
|
+
nudgeLine,
|
|
236
|
+
registryCwd
|
|
231
237
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -95,8 +95,14 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
|
95
95
|
return { status: "invalid", path: reportedPath, error: error instanceof Error ? error.message : String(error) };
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
const DEV_SERVER_PORT_ENV = ["WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT"];
|
|
98
99
|
const RUNTIME_CONTEXT_FIELDS = [
|
|
99
100
|
{ key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "doctor --fix creates local identity; Farmslot may override", adapters: ["mobile", "extension", "core"] },
|
|
101
|
+
{ key: "watcherPort", envVars: DEV_SERVER_PORT_ENV, envVar: "WATCHER_PORT", customize: "doctor --fix claims one; --watcher-port overrides", adapters: ["mobile"] },
|
|
102
|
+
{ key: "metroPort", envVars: DEV_SERVER_PORT_ENV, envVar: "WATCHER_PORT", customize: "mirrors watcherPort; --metro-port is its alias", adapters: ["mobile"] },
|
|
103
|
+
{ key: "devServerPort", envVars: DEV_SERVER_PORT_ENV, envVar: "WATCHER_PORT", customize: "mirrors watcherPort for the adapter dev server", adapters: ["mobile", "extension"] },
|
|
104
|
+
{ key: "simulator", envVars: ["IOS_SIMULATOR"], envVar: "IOS_SIMULATOR", customize: "--device pins the matched simulator into the context", adapters: ["mobile"] },
|
|
105
|
+
{ key: "simulatorUdid", envVars: ["SIM_UDID"], envVar: "SIM_UDID", customize: "--device pins the matched udid into the context", adapters: ["mobile"] },
|
|
100
106
|
{ key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "resolved from the built extension", adapters: ["extension"] },
|
|
101
107
|
{ key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] }
|
|
102
108
|
];
|
|
@@ -108,9 +114,12 @@ function runtimeContextSummary(target, adapter) {
|
|
|
108
114
|
const fields = {};
|
|
109
115
|
const specs = adapter ? RUNTIME_CONTEXT_FIELDS.filter((spec) => spec.adapters.includes(adapter)) : RUNTIME_CONTEXT_FIELDS;
|
|
110
116
|
for (const spec of specs) {
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
const envName = spec.envVars.find((name) => {
|
|
118
|
+
const value = process.env[name];
|
|
119
|
+
return value !== void 0 && value !== "";
|
|
120
|
+
});
|
|
121
|
+
if (envName !== void 0) {
|
|
122
|
+
fields[spec.key] = { value: process.env[envName], source: "env", envVar: envName, customize: spec.customize };
|
|
114
123
|
continue;
|
|
115
124
|
}
|
|
116
125
|
const fileValue = fileExists ? readRuntimeContextField(contextPath, spec.key) : void 0;
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { handleTutorial } from "./commands/tutorial.js";
|
|
|
13
13
|
import { handleSetupBase } from "./commands/setup-base.js";
|
|
14
14
|
import { getAdapterSurface } from "./adapters/surface.js";
|
|
15
15
|
import { detectAdapter } from "./harness.js";
|
|
16
|
+
import { configPath, hydrateLibraryPathEnv, readConfig } from "./review/knowledge.js";
|
|
16
17
|
import {
|
|
17
18
|
PUBLIC_COMMAND_CONTRACTS,
|
|
18
19
|
publicCommandNames,
|
|
@@ -177,9 +178,23 @@ Example:
|
|
|
177
178
|
to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
|
|
178
179
|
|
|
179
180
|
--target <path> Checkout path (default: cwd)
|
|
180
|
-
--json Machine-readable envelope { adapter, target, devices[], next }
|
|
181
|
+
--json Machine-readable envelope { adapter, target, view, devices[], next }
|
|
181
182
|
--fast Skip all live-state probes (instant output, safe for scripts)
|
|
182
183
|
--all-devices Mobile only: show every connected device instead of the slot-scoped target
|
|
184
|
+
--watch Live read-only view of the task this checkout is working through (refreshes every 2s, Ctrl-C to leave)
|
|
185
|
+
--task <dir> Task directory for the view (default: the most recently marked one under temp/tasks/)
|
|
186
|
+
|
|
187
|
+
The task view reads task files only \u2014 no probe, no launch, no write \u2014 and shows
|
|
188
|
+
the same facts whether a skill or a control plane started the run: slot and
|
|
189
|
+
device isolation, template and flow, fixture and sandbox readiness, then the
|
|
190
|
+
checklist with the time each row took and how long the current row has been
|
|
191
|
+
running. --task without --watch prints it once. With --json, --watch emits one
|
|
192
|
+
object per change (newline-delimited) instead of redrawing.
|
|
193
|
+
|
|
194
|
+
The checkout view (also under "view" in --json, and printed by doctor) is what
|
|
195
|
+
this checkout is bound to: the runtime-context fields that keep it off other
|
|
196
|
+
slots' ports and devices, the recipe libraries this machine resolves, and the
|
|
197
|
+
wallet fixture it last applied.
|
|
183
198
|
|
|
184
199
|
Human output: static device list prints immediately; live-state lines append
|
|
185
200
|
after the ~2s probe window (screen, wallet, account, fixture per device).
|
|
@@ -202,7 +217,8 @@ Example:
|
|
|
202
217
|
Example:
|
|
203
218
|
mm-harness status
|
|
204
219
|
mm-harness status --json
|
|
205
|
-
mm-harness status --fast
|
|
220
|
+
mm-harness status --fast
|
|
221
|
+
mm-harness status --watch`
|
|
206
222
|
},
|
|
207
223
|
{
|
|
208
224
|
name: "checklist",
|
|
@@ -244,6 +260,7 @@ Example:
|
|
|
244
260
|
--target <path> Checkout path (default: cwd)
|
|
245
261
|
--platform <mobile|extension|core> Force the platform (auto-detected inside a checkout)
|
|
246
262
|
--mobile-platform <ios|android> Mobile target when status cannot decide it
|
|
263
|
+
--clear-metro Mobile only: clear Metro's transform cache in the launch step
|
|
247
264
|
--artifacts-dir <dir> Where sandbox.json and the per-step reports land
|
|
248
265
|
(default: <target>/temp/recipe/runtime)
|
|
249
266
|
--json Print the readiness record
|
|
@@ -266,8 +283,8 @@ Example:
|
|
|
266
283
|
mm-harness checklist mark, and inputs/ (handoff.json, worker-terminal-contract.json).
|
|
267
284
|
checklist-target.json is NOT written \u2014 absent means CHECKLIST.md + SIGNAL.json.
|
|
268
285
|
|
|
269
|
-
Applied when you do not pass them: --surface skill, --
|
|
270
|
-
executable, and --platform from the detected checkout.
|
|
286
|
+
Applied when you do not pass them: --surface skill, --run-mode autonomous,
|
|
287
|
+
--mark-command for this executable, and --platform from the detected checkout.
|
|
271
288
|
|
|
272
289
|
Task:
|
|
273
290
|
--title <t> Task title (required)
|
|
@@ -277,7 +294,7 @@ Task:
|
|
|
277
294
|
|
|
278
295
|
Selection (same catalog as execution-template):
|
|
279
296
|
--flow <flow> Exact flow (required)
|
|
280
|
-
--run-mode <mode> autonomous|interactive|validation (
|
|
297
|
+
--run-mode <mode> autonomous|interactive|validation (default autonomous; set by a control plane)
|
|
281
298
|
--platform <platform> Exact platform
|
|
282
299
|
--template <id> Exact template id (required)
|
|
283
300
|
--domain <domain> Exact domain
|
|
@@ -483,8 +500,9 @@ Example:
|
|
|
483
500
|
example: "mm-harness doctor",
|
|
484
501
|
helpText: `mm-harness doctor [flags]
|
|
485
502
|
|
|
486
|
-
Readiness check for a checkout \u2014 no app launch. Reports the
|
|
487
|
-
|
|
503
|
+
Readiness check for a checkout \u2014 no app launch. Reports the same checkout view
|
|
504
|
+
status does \u2014 runtime-context isolation, resolved recipe libraries, and the last
|
|
505
|
+
applied wallet fixture \u2014 so there is no hunting for files.
|
|
488
506
|
|
|
489
507
|
--fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
|
|
490
508
|
--expect-live Exit-coded liveness gate only (human verdict on stderr). Use without --print-ready when callers need pass/fail without a Farmslot indicator line
|
|
@@ -1027,6 +1045,12 @@ const NUDGE_SKIP = ["help", "tutorial", "setup-base", "update", "completions", "
|
|
|
1027
1045
|
if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
|
|
1028
1046
|
maybeNudge();
|
|
1029
1047
|
}
|
|
1048
|
+
function targetFromArgv(argv) {
|
|
1049
|
+
const index = argv.indexOf("--target");
|
|
1050
|
+
const separate = index !== -1 ? argv[index + 1] : void 0;
|
|
1051
|
+
const inline = argv.find((argument) => argument.startsWith("--target="))?.slice("--target=".length);
|
|
1052
|
+
return path.resolve(separate ?? inline ?? process.cwd());
|
|
1053
|
+
}
|
|
1030
1054
|
function hasPassthroughHelp(argv) {
|
|
1031
1055
|
const divider = argv.indexOf("--");
|
|
1032
1056
|
if (divider === -1) return false;
|
|
@@ -1102,6 +1126,7 @@ if (!preflightBypass.has(rawArgv[0] ?? "")) {
|
|
|
1102
1126
|
);
|
|
1103
1127
|
}
|
|
1104
1128
|
}
|
|
1129
|
+
await hydrateLibraryPathEnv({ target: targetFromArgv(rawArgv), config: readConfig(configPath()) });
|
|
1105
1130
|
if (hasPassthroughHelp(rawArgv)) {
|
|
1106
1131
|
process.exit(await delegate(rawArgv));
|
|
1107
1132
|
}
|
package/dist/review/knowledge.js
CHANGED
|
@@ -140,9 +140,13 @@ function fetchLibrary(name, target) {
|
|
|
140
140
|
excludeCacheDir(path.resolve(target));
|
|
141
141
|
return withRevision({ name, root, source: "cache" });
|
|
142
142
|
}
|
|
143
|
+
let declaredLibraryPath;
|
|
144
|
+
function declaredLibraryPathFor(env) {
|
|
145
|
+
return env === process.env && declaredLibraryPath ? declaredLibraryPath.value : env.RECIPE_LIBRARY_PATH;
|
|
146
|
+
}
|
|
143
147
|
function resolveLibrary(name, options) {
|
|
144
148
|
const env = options.env ?? process.env;
|
|
145
|
-
const fromEnv = parseLibraryPathEnv(env
|
|
149
|
+
const fromEnv = parseLibraryPathEnv(declaredLibraryPathFor(env)).find(
|
|
146
150
|
(entry) => isDir(entry.root) && (entry.name === name || entry.name === void 0 && loadOwnedPaths(entry.root)?.domain === name)
|
|
147
151
|
);
|
|
148
152
|
if (fromEnv) return withRevision({ name, root: fromEnv.root, source: "env" });
|
|
@@ -171,7 +175,7 @@ function discoverLibraries(context) {
|
|
|
171
175
|
seenNames.add(libraryName);
|
|
172
176
|
found.push(withRevision({ name: libraryName, root: resolved, source }));
|
|
173
177
|
};
|
|
174
|
-
for (const entry of parseLibraryPathEnv(env
|
|
178
|
+
for (const entry of parseLibraryPathEnv(declaredLibraryPathFor(env))) add(entry.name, entry.root, "env");
|
|
175
179
|
for (const [name, root] of Object.entries(context.config?.libraries ?? {})) add(name, root, "config");
|
|
176
180
|
const siblingNames = /* @__PURE__ */ new Set();
|
|
177
181
|
for (const root of siblingRoots(context)) {
|
|
@@ -188,6 +192,30 @@ function discoverLibraries(context) {
|
|
|
188
192
|
}
|
|
189
193
|
return found;
|
|
190
194
|
}
|
|
195
|
+
function declaredLibraryPathEnv() {
|
|
196
|
+
return declaredLibraryPath;
|
|
197
|
+
}
|
|
198
|
+
async function hydrateLibraryPathEnv(context) {
|
|
199
|
+
const env = context.env ?? process.env;
|
|
200
|
+
const existing = env.RECIPE_LIBRARY_PATH ?? "";
|
|
201
|
+
if (env === process.env) declaredLibraryPath = { value: existing || void 0 };
|
|
202
|
+
const declared = new Set(
|
|
203
|
+
parseLibraryPathEnv(existing).map(
|
|
204
|
+
(entry) => entry.name ?? loadOwnedPaths(entry.root)?.domain ?? path.basename(entry.root)
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
const discovered = discoverLibraries(context).filter(
|
|
208
|
+
(library) => library.source !== "env" && !declared.has(library.name)
|
|
209
|
+
);
|
|
210
|
+
if (discovered.length === 0) return;
|
|
211
|
+
const entries = discovered.map((library) => `${library.name}=${library.root}`);
|
|
212
|
+
if (!existing) {
|
|
213
|
+
const { personalRecipeLibraryRoot } = await import("@farmslot/recipe-harness");
|
|
214
|
+
const personal = personalRecipeLibraryRoot(env);
|
|
215
|
+
if (isDir(personal)) entries.push(`personal=${personal}`);
|
|
216
|
+
}
|
|
217
|
+
env.RECIPE_LIBRARY_PATH = existing ? `${existing}:${entries.join(":")}` : entries.join(":");
|
|
218
|
+
}
|
|
191
219
|
function resolveReference(adapter, context) {
|
|
192
220
|
const env = context.env ?? process.env;
|
|
193
221
|
const fromEnv = env[referenceEnvName(adapter)];
|
|
@@ -385,7 +413,8 @@ const BASE_REVIEW_STEPS = [
|
|
|
385
413
|
"Tests: each behavior change has a test at its best-fit layer (view, integration, unit); no mocked-away logic that hides the change under test.",
|
|
386
414
|
"Safety: no secrets, no widened permissions or network surface, no logging of user data; new dependencies justified.",
|
|
387
415
|
"Product wiring: i18n keys, telemetry events, feature flags, and error messages follow the repo conventions for the touched area.",
|
|
388
|
-
"Scope and hygiene: diff is minimal for the ask, no unrelated refactors or reformatting, commit and PR title follow conventional commits."
|
|
416
|
+
"Scope and hygiene: diff is minimal for the ask, no unrelated refactors or reformatting, commit and PR title follow conventional commits.",
|
|
417
|
+
"Signal over noise: comments say why in a line or two and never restate the code; no ticket keys, PR numbers or tool mentions in source; no leftover TODOs, debug logs, commented-out code or unused helpers; no catch that swallows, no abstraction with one caller, no padded tests or PR text. Prefer deleting to rewording."
|
|
389
418
|
];
|
|
390
419
|
const VERDICT_STEP = "Post the verdict: APPROVE or REQUEST CHANGES, findings ordered by severity with file:line, nits listed separately; state what was not checked and why.";
|
|
391
420
|
function parityAdapter(adapter) {
|
|
@@ -489,12 +518,14 @@ export {
|
|
|
489
518
|
changedFiles,
|
|
490
519
|
classifyDomain,
|
|
491
520
|
configPath,
|
|
521
|
+
declaredLibraryPathEnv,
|
|
492
522
|
defaultBaseRef,
|
|
493
523
|
discoverLibraries,
|
|
494
524
|
excludeCacheDir,
|
|
495
525
|
fetchLibrary,
|
|
496
526
|
gitExcludeFile,
|
|
497
527
|
globToRegExp,
|
|
528
|
+
hydrateLibraryPathEnv,
|
|
498
529
|
libraryCachePath,
|
|
499
530
|
loadDomainKnowledge,
|
|
500
531
|
loadOwnedPaths,
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveRuntimeContextPath } from "./harness.js";
|
|
5
|
+
import { parseLibraryPathEnv } from "./review/knowledge.js";
|
|
6
|
+
const TASK_SEARCH_DEPTH = 3;
|
|
7
|
+
const CHECKLIST_LABEL_LIMIT = 70;
|
|
8
|
+
const CHECKLIST_ROW = /^\s*-\s\[([ xX])\]\s*(\d+)\.\s*(.*)$/u;
|
|
9
|
+
const CHECKLIST_PHASE = /^##\s+(.+?)\s*$/u;
|
|
10
|
+
const TASK_SILENT_AFTER_MS = 10 * 60 * 1e3;
|
|
11
|
+
function findTaskDir(target) {
|
|
12
|
+
const root = path.join(target, "temp", "tasks");
|
|
13
|
+
let bySignal;
|
|
14
|
+
let byChecklist;
|
|
15
|
+
const consider = (best, dir, file) => {
|
|
16
|
+
const stat = fs.statSync(path.join(dir, file), { throwIfNoEntry: false });
|
|
17
|
+
if (!stat?.isFile()) return best;
|
|
18
|
+
return best && best.mtimeMs >= stat.mtimeMs ? best : { dir, mtimeMs: stat.mtimeMs };
|
|
19
|
+
};
|
|
20
|
+
const visit = (dir, depth) => {
|
|
21
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
if (!entry.isDirectory()) continue;
|
|
23
|
+
const child = path.join(dir, entry.name);
|
|
24
|
+
bySignal = consider(bySignal, child, "SIGNAL.json");
|
|
25
|
+
byChecklist = consider(byChecklist, child, "CHECKLIST.md");
|
|
26
|
+
if (depth < TASK_SEARCH_DEPTH) visit(child, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
if (!fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) return void 0;
|
|
30
|
+
visit(root, 1);
|
|
31
|
+
return (bySignal ?? byChecklist)?.dir;
|
|
32
|
+
}
|
|
33
|
+
function collectTaskView(target, taskDir, now) {
|
|
34
|
+
const base = {
|
|
35
|
+
checkout: readCheckout(target),
|
|
36
|
+
isolation: readIsolation(target),
|
|
37
|
+
libraries: parseLibraryPathEnv(process.env.RECIPE_LIBRARY_PATH).map(
|
|
38
|
+
({ name, root }) => ({ ...name ? { name } : {}, path: root })
|
|
39
|
+
)
|
|
40
|
+
};
|
|
41
|
+
if (!taskDir) return { ...base, checklist: [], activity: { ongoing: false, reason: "no-task" } };
|
|
42
|
+
const handoff = readJsonObject(path.join(taskDir, "inputs", "handoff.json"));
|
|
43
|
+
const provenance = readJsonObject(path.join(taskDir, "artifacts", "harness-provenance.json"));
|
|
44
|
+
const fixtures = readJsonObject(path.join(taskDir, "artifacts", "prepare", "fixtures.json"));
|
|
45
|
+
const sandbox = readJsonObject(path.join(taskDir, "artifacts", "sandbox.json"));
|
|
46
|
+
const signal = readJsonObject(path.join(taskDir, "SIGNAL.json"));
|
|
47
|
+
const checklist = readChecklistRows(path.join(taskDir, "CHECKLIST.md"));
|
|
48
|
+
const template = asObject(handoff?.executionTemplate);
|
|
49
|
+
const task = asObject(handoff?.task);
|
|
50
|
+
const startedAt = parseTime(pickString(handoff, "startedAt"));
|
|
51
|
+
const timing = applyTiming(checklist, signal, startedAt, now);
|
|
52
|
+
const ticket = pickString(task, "ticket");
|
|
53
|
+
const activity = taskActivity({
|
|
54
|
+
status: pickString(signal, "status"),
|
|
55
|
+
lastWrites: [startedAt, parseTime(pickString(signal, "timestamp")), ...readMarkTimes(signal).values()],
|
|
56
|
+
branch: base.checkout.branch,
|
|
57
|
+
ticket,
|
|
58
|
+
now
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
...base,
|
|
62
|
+
taskDir,
|
|
63
|
+
harness: section({
|
|
64
|
+
version: pickString(asObject(provenance?.package), "version"),
|
|
65
|
+
sourceKind: pickString(asObject(provenance?.source), "kind")
|
|
66
|
+
}),
|
|
67
|
+
template: section({
|
|
68
|
+
id: pickString(template, "id"),
|
|
69
|
+
sourceId: pickString(template, "sourceId"),
|
|
70
|
+
flow: pickString(handoff, "flow"),
|
|
71
|
+
domain: pickString(handoff, "domain"),
|
|
72
|
+
surface: pickString(handoff, "surface"),
|
|
73
|
+
title: pickString(task, "title"),
|
|
74
|
+
ticket
|
|
75
|
+
}),
|
|
76
|
+
fixture: section({
|
|
77
|
+
fixture: pickString(fixtures, "fixture"),
|
|
78
|
+
canonicalFixture: pickString(fixtures, "canonicalFixture"),
|
|
79
|
+
status: pickString(fixtures, "status"),
|
|
80
|
+
walletReset: pickBoolean(fixtures, "walletReset")
|
|
81
|
+
}),
|
|
82
|
+
sandbox: section({ ready: pickBoolean(sandbox, "ready") }),
|
|
83
|
+
checklist,
|
|
84
|
+
signal: section({
|
|
85
|
+
status: pickString(signal, "status"),
|
|
86
|
+
step: pickString(signal, "step"),
|
|
87
|
+
timestamp: pickString(signal, "timestamp"),
|
|
88
|
+
attemptId: pickString(signal, "attemptId")
|
|
89
|
+
}),
|
|
90
|
+
...timing,
|
|
91
|
+
activity
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function taskActivity(input) {
|
|
95
|
+
const writes = input.lastWrites.filter((value) => value !== void 0);
|
|
96
|
+
const idleMs = writes.length > 0 ? input.now - Math.max(...writes) : void 0;
|
|
97
|
+
const branchCarriesTicket = input.ticket && input.branch ? input.branch.includes(input.ticket) : void 0;
|
|
98
|
+
const extra = {
|
|
99
|
+
...idleMs !== void 0 ? { idleMs } : {},
|
|
100
|
+
...branchCarriesTicket !== void 0 ? { branchCarriesTicket } : {}
|
|
101
|
+
};
|
|
102
|
+
if (input.status === void 0 && writes.length === 0) return { ongoing: false, reason: "no-task", ...extra };
|
|
103
|
+
if (isTerminalStatus(input.status)) return { ongoing: false, reason: "finished", ...extra };
|
|
104
|
+
if (idleMs !== void 0 && idleMs > TASK_SILENT_AFTER_MS) return { ongoing: false, reason: "silent", ...extra };
|
|
105
|
+
return { ongoing: true, ...extra };
|
|
106
|
+
}
|
|
107
|
+
function isTerminalStatus(status) {
|
|
108
|
+
return status !== void 0 && status !== "running" && status !== "started";
|
|
109
|
+
}
|
|
110
|
+
function applyTiming(rows, signal, startedAt, now) {
|
|
111
|
+
const marks = readMarkTimes(signal);
|
|
112
|
+
let previousAt = startedAt;
|
|
113
|
+
let lastAt;
|
|
114
|
+
let firstAt;
|
|
115
|
+
for (const row of rows) {
|
|
116
|
+
const checkedAt = marks.get(row.number);
|
|
117
|
+
if (checkedAt === void 0) continue;
|
|
118
|
+
if (previousAt !== void 0) row.durationMs = checkedAt - previousAt;
|
|
119
|
+
if (firstAt === void 0) firstAt = checkedAt;
|
|
120
|
+
previousAt = checkedAt;
|
|
121
|
+
lastAt = checkedAt;
|
|
122
|
+
}
|
|
123
|
+
const current = rows.find((row) => !row.checked);
|
|
124
|
+
if (current && lastAt !== void 0) current.runningMs = now - lastAt;
|
|
125
|
+
const origin = startedAt ?? firstAt;
|
|
126
|
+
return {
|
|
127
|
+
...current ? { currentStep: current.number } : {},
|
|
128
|
+
...origin !== void 0 ? { elapsedMs: now - origin } : {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function readMarkTimes(signal) {
|
|
132
|
+
const marks = /* @__PURE__ */ new Map();
|
|
133
|
+
const events = asObject(signal?.checklistTiming)?.events;
|
|
134
|
+
if (!Array.isArray(events)) return marks;
|
|
135
|
+
for (const entry of events) {
|
|
136
|
+
const event = asObject(entry);
|
|
137
|
+
const stepNumber = pickNumber(event, "stepNumber");
|
|
138
|
+
const checkedAt = parseTime(pickString(event, "checkedAt"));
|
|
139
|
+
if (stepNumber === void 0 || checkedAt === void 0) continue;
|
|
140
|
+
marks.set(stepNumber, checkedAt);
|
|
141
|
+
}
|
|
142
|
+
return marks;
|
|
143
|
+
}
|
|
144
|
+
function readChecklistRows(file) {
|
|
145
|
+
const text = readTextFile(file);
|
|
146
|
+
if (text === void 0) return [];
|
|
147
|
+
const rows = [];
|
|
148
|
+
let phase;
|
|
149
|
+
for (const line of text.split("\n")) {
|
|
150
|
+
const heading = CHECKLIST_PHASE.exec(line);
|
|
151
|
+
if (heading) {
|
|
152
|
+
phase = heading[1];
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const match = CHECKLIST_ROW.exec(line);
|
|
156
|
+
if (!match) continue;
|
|
157
|
+
rows.push({
|
|
158
|
+
...phase ? { phase } : {},
|
|
159
|
+
number: Number(match[2]),
|
|
160
|
+
label: trimLabel(match[3]),
|
|
161
|
+
checked: match[1].toLowerCase() === "x"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return rows;
|
|
165
|
+
}
|
|
166
|
+
function trimLabel(text) {
|
|
167
|
+
const plain = text.replace(/\*\*/gu, "").replace(/\s+/gu, " ").trim();
|
|
168
|
+
if (plain.length <= CHECKLIST_LABEL_LIMIT) return plain;
|
|
169
|
+
return `${plain.slice(0, CHECKLIST_LABEL_LIMIT).trimEnd()}\u2026`;
|
|
170
|
+
}
|
|
171
|
+
function readCheckout(target) {
|
|
172
|
+
const branch = gitRef(target, "--abbrev-ref");
|
|
173
|
+
const head = gitRef(target, "--short");
|
|
174
|
+
return { target, ...branch ? { branch } : {}, ...head ? { head } : {} };
|
|
175
|
+
}
|
|
176
|
+
function gitRef(target, mode) {
|
|
177
|
+
const result = spawnSync("git", ["-C", target, "rev-parse", mode, "HEAD"], { encoding: "utf8" });
|
|
178
|
+
if (result.status !== 0) return void 0;
|
|
179
|
+
return result.stdout.trim() || void 0;
|
|
180
|
+
}
|
|
181
|
+
function readIsolation(target) {
|
|
182
|
+
const context = readJsonObject(resolveRuntimeContextPath(target));
|
|
183
|
+
if (!context) return void 0;
|
|
184
|
+
return section({
|
|
185
|
+
slotId: pickString(context, "slotId"),
|
|
186
|
+
platform: pickString(context, "platform"),
|
|
187
|
+
metroPort: pickNumber(context, "metroPort"),
|
|
188
|
+
watcherPort: pickNumber(context, "watcherPort"),
|
|
189
|
+
devServerPort: pickNumber(context, "devServerPort"),
|
|
190
|
+
simulator: pickString(context, "simulator"),
|
|
191
|
+
simulatorUdid: pickString(context, "simulatorUdid"),
|
|
192
|
+
cdpPort: pickNumber(context, "cdpPort"),
|
|
193
|
+
extensionId: pickString(context, "extensionId")
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function readTextFile(file) {
|
|
197
|
+
try {
|
|
198
|
+
return fs.readFileSync(file, "utf8");
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code === "ENOENT") return void 0;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function readJsonObject(file) {
|
|
205
|
+
const raw = readTextFile(file);
|
|
206
|
+
if (raw === void 0) return void 0;
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(raw);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new Error(`${file} is not valid JSON: ${error.message}`);
|
|
212
|
+
}
|
|
213
|
+
const object = asObject(parsed);
|
|
214
|
+
if (!object) throw new Error(`${file} must contain a JSON object.`);
|
|
215
|
+
return object;
|
|
216
|
+
}
|
|
217
|
+
function asObject(value) {
|
|
218
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
function pickString(source, key) {
|
|
222
|
+
const value = source?.[key];
|
|
223
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
224
|
+
}
|
|
225
|
+
function pickNumber(source, key) {
|
|
226
|
+
const value = source?.[key];
|
|
227
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
228
|
+
}
|
|
229
|
+
function pickBoolean(source, key) {
|
|
230
|
+
const value = source?.[key];
|
|
231
|
+
return typeof value === "boolean" ? value : void 0;
|
|
232
|
+
}
|
|
233
|
+
function parseTime(value) {
|
|
234
|
+
if (value === void 0) return void 0;
|
|
235
|
+
const parsed = Date.parse(value);
|
|
236
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
237
|
+
}
|
|
238
|
+
function section(value) {
|
|
239
|
+
return Object.values(value).some((entry) => entry !== void 0) ? value : void 0;
|
|
240
|
+
}
|
|
241
|
+
export {
|
|
242
|
+
CHECKLIST_LABEL_LIMIT,
|
|
243
|
+
TASK_SILENT_AFTER_MS,
|
|
244
|
+
collectTaskView,
|
|
245
|
+
findTaskDir,
|
|
246
|
+
isTerminalStatus
|
|
247
|
+
};
|
package/docs/RECIPES.md
CHANGED
|
@@ -450,6 +450,12 @@ mm-harness run onboarding.smoke --describe
|
|
|
450
450
|
mm-harness run onboarding.smoke --plan
|
|
451
451
|
```
|
|
452
452
|
|
|
453
|
+
A library registered once with `mm-harness config set libraries.wallet <path>`,
|
|
454
|
+
or checked out beside the product checkout, is reachable without exporting
|
|
455
|
+
anything: every location the harness resolves reaches `run`, `actions`, and
|
|
456
|
+
template discovery, and `mm-harness status` lists them. Exported entries keep
|
|
457
|
+
their precedence and order in front of those.
|
|
458
|
+
|
|
453
459
|
Use `--library wallet=/path/to/library` for one command. The alias is the
|
|
454
460
|
source name; without one, the directory name is used. Resolution follows the
|
|
455
461
|
explicit library order, then bundled MetaMask. Adapter-specific variants are
|