@deeeed/metamask-harness 0.17.4 → 0.17.5
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 +6 -103
- package/README.md +136 -96
- package/adapters/extension/console-tail.mjs +24 -55
- package/adapters/extension/ensure-browser.sh +7 -1
- package/adapters/extension/inject.mjs +15 -5
- package/adapters/extension/launch-browser.cjs +4 -3
- package/adapters/extension/launch.sh +11 -8
- package/adapters/extension/lib/chrome-args.cjs +24 -0
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +0 -1
- package/adapters/extension/seed-fixture.sh +12 -4
- package/adapters/extension/sidepanel-toggle.sh +1 -4
- package/adapters/extension/start-watch.sh +4 -17
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +45 -53
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
- package/adapters/mobile/cleanup.sh +176 -16
- package/adapters/mobile/inject.sh +671 -90
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/adapters/mobile/overlay/compat/README.md +28 -0
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
- package/adapters/mobile/start-metro.sh +41 -0
- package/adapters/mobile/verify.sh +61 -4
- package/adapters/mobile/wait-for-bridge.sh +8 -22
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
- package/adapters/shared/resolve-farmslot-ports.sh +7 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
- package/adapters/shared/resolve-slot-ports.sh +7 -2
- package/bin/mm-harness +0 -7
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/runtime-decision.js +4 -73
- package/dist/adapters/extension/surface.js +2 -3
- package/dist/adapters/mobile/perps-env.js +3 -43
- package/dist/adapters/mobile/prepare.js +7 -22
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +0 -16
- package/dist/adapters.js +5 -12
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +12 -6
- package/dist/commands/call.js +22 -56
- package/dist/commands/check.js +3 -9
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/debug.js +31 -38
- package/dist/commands/device-target.js +12 -27
- package/dist/commands/doctor.js +23 -77
- package/dist/commands/fixtures.js +17 -27
- package/dist/commands/launch/extension.js +2 -0
- package/dist/commands/launch/index.js +59 -167
- package/dist/commands/logs.js +6 -8
- package/dist/commands/manifest.js +25 -151
- package/dist/commands/parse-args.js +0 -2
- package/dist/commands/provision.js +3 -10
- package/dist/commands/run-engine.js +69 -71
- package/dist/commands/run-report.js +3 -12
- package/dist/commands/run.js +35 -245
- package/dist/commands/shared.js +3 -55
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +2 -7
- package/dist/commands/update.js +28 -16
- package/dist/doctor.js +4 -1
- package/dist/harness.js +134 -43
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +20 -6
- package/dist/mm-harness-cli.js +48 -123
- package/dist/paths.js +5 -2
- package/dist/runner.js +2 -37
- package/dist/runtime-context.js +8 -26
- package/docs/ADAPTER-SURFACE.md +112 -19
- package/docs/CHEATSHEET.md +52 -39
- package/docs/CLI-SPEC.md +1098 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/DEBUG-HANDOVER.md +36 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/UX-PRINCIPLES.md +1 -3
- package/docs/architecture.md +375 -65
- package/docs/live-adapter-contract.md +2 -4
- package/docs/recipe-libraries.md +275 -45
- package/library/README.md +0 -4
- package/library/actions/core/perps/_controller.mjs +55 -10
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/manifests/core.action-manifest.json +7 -7
- package/library/manifests/mobile.action-manifest.json +3 -28
- package/package.json +1 -4
- package/adapters/extension/launch-webpack.cjs +0 -83
- package/adapters/extension/stamp-runtime-title.cjs +0 -58
- package/adapters/extension/sync-webpack-dist.cjs +0 -107
- package/adapters/mobile/launch-console-forwarder.cjs +0 -39
- package/adapters/mobile/start-console-forwarder.sh +0 -70
- package/adapters/shared/update-check-worker.mjs +0 -43
- package/dist/adapters/extension/console-capture.js +0 -93
- package/dist/command-contract.js +0 -441
- package/dist/command-journal.js +0 -225
- package/dist/commands/last.js +0 -52
- package/dist/json-stream.js +0 -57
- package/dist/run-diagnostics.js +0 -261
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/library/recipes/runner/smoke.core.recipe.json +0 -27
- package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
- package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
- package/scripts/validate-human-outcomes.mjs +0 -169
package/dist/commands/run.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { walletFixturePath } from "../paths.js";
|
|
4
4
|
import { color } from "../cli-color.js";
|
|
5
5
|
import { recipeRunning } from "../heal-bounds.js";
|
|
6
|
-
import {
|
|
6
|
+
import { EXIT } from "./shared.js";
|
|
7
7
|
import {
|
|
8
8
|
optionFlag,
|
|
9
9
|
optionString,
|
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
resolveAdapter,
|
|
12
12
|
runtimeOptionsFromCli,
|
|
13
13
|
isRecord,
|
|
14
|
-
shellQuote,
|
|
15
|
-
targetPath,
|
|
16
14
|
usageError
|
|
17
15
|
} from "./parse-args.js";
|
|
18
16
|
import {
|
|
@@ -30,119 +28,46 @@ import { getAdapterSurface } from "../adapters/surface.js";
|
|
|
30
28
|
import { coreDependencyBlock } from "./core-readiness.js";
|
|
31
29
|
import { writeRunReport } from "./run-report.js";
|
|
32
30
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
async function handleRun(parsed) {
|
|
36
|
-
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
37
|
-
const restoreStdout = stream.isolateStdout();
|
|
38
|
-
const target = targetPath(parsed.options);
|
|
39
|
-
try {
|
|
40
|
-
const exitCode = await handleRunInner(parsed, stream);
|
|
41
|
-
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
42
|
-
return exitCode;
|
|
43
|
-
} catch (error) {
|
|
44
|
-
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
45
|
-
stream.error({
|
|
46
|
-
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "RUN_FAILED",
|
|
47
|
-
message: error instanceof Error ? error.message : String(error),
|
|
48
|
-
userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
|
|
49
|
-
});
|
|
50
|
-
stream.complete("fail", exitCode);
|
|
51
|
-
throw error;
|
|
52
|
-
} finally {
|
|
53
|
-
restoreStdout();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function handleRunInner({ positional, options }, stream) {
|
|
57
|
-
if (optionFlag(options, "list")) {
|
|
58
|
-
if (stream.enabled) {
|
|
59
|
-
const message = "--json-stream is for recipe execution; use run --list --json for discovery.";
|
|
60
|
-
stream.error({ code: "JSON_STREAM_UNSUPPORTED_MODE", message, userAction: "mm-harness run --list --json" });
|
|
61
|
-
return EXIT.usage;
|
|
62
|
-
}
|
|
63
|
-
return handleListExecutables("run", options);
|
|
64
|
-
}
|
|
31
|
+
async function handleRun({ positional, options }) {
|
|
32
|
+
if (optionFlag(options, "list")) return handleListExecutables("run", options);
|
|
65
33
|
const targetRecipe = positional[0];
|
|
66
34
|
if (!targetRecipe) throw usageError("run requires <recipe.json>.");
|
|
67
|
-
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options
|
|
35
|
+
if (optionFlag(options, "plan")) return handleRunPlan(targetRecipe, options);
|
|
68
36
|
const { adapter, target } = resolveAdapter(options);
|
|
69
37
|
const json = optionFlag(options, "json");
|
|
70
|
-
const jsonOutput = json && !stream.enabled;
|
|
71
|
-
const machine = json || stream.enabled;
|
|
72
|
-
stream.phase("resolve", { adapter, target, recipe: targetRecipe });
|
|
73
|
-
if (!fs.existsSync(target)) {
|
|
74
|
-
const message = `target does not exist: ${target}`;
|
|
75
|
-
const userAction = "pass --target <metamask-checkout> pointing to an existing checkout";
|
|
76
|
-
if (stream.enabled) {
|
|
77
|
-
stream.error({ code: "TARGET_NOT_FOUND", message, userAction });
|
|
78
|
-
return EXIT.usage;
|
|
79
|
-
}
|
|
80
|
-
return usageOut(jsonOutput, "run", message, userAction);
|
|
81
|
-
}
|
|
82
|
-
writeInteractiveProgress(machine, `\u2192 recipe run \u2014 validating ${targetRecipe} \xB7 ${adapter}`);
|
|
83
38
|
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
84
39
|
const dtResult = applyDeviceTargeting("run", adapter, options, { gate: true, rerun: "" });
|
|
85
40
|
if ("code" in dtResult) {
|
|
86
|
-
return emitRunUsageError(
|
|
41
|
+
return emitRunUsageError(json, adapter, targetRecipe, dtResult.code, dtResult.message);
|
|
87
42
|
}
|
|
88
|
-
if (recipeRunning(target)) return emitRunRecipeRunning(
|
|
43
|
+
if (recipeRunning(target)) return emitRunRecipeRunning(json);
|
|
89
44
|
if (optionString(options, "artifactsDir") === void 0 && runArgLooksLikeRecipeFile(targetRecipe)) {
|
|
90
45
|
requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
91
46
|
}
|
|
92
|
-
stream.phase("validate");
|
|
93
47
|
const validated = await validateRunRecipeStatic(targetRecipe, adapter, options);
|
|
94
48
|
if (validated.usageError) {
|
|
95
|
-
|
|
96
|
-
validated.usageError.code,
|
|
97
|
-
validated.usageError.message,
|
|
98
|
-
adapter,
|
|
99
|
-
validated.recipeFile
|
|
100
|
-
);
|
|
101
|
-
return emitRunUsageError(
|
|
102
|
-
jsonOutput,
|
|
103
|
-
stream,
|
|
104
|
-
adapter,
|
|
105
|
-
validated.recipeFile,
|
|
106
|
-
validated.usageError.code,
|
|
107
|
-
validated.usageError.message,
|
|
108
|
-
userAction
|
|
109
|
-
);
|
|
49
|
+
return emitRunUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
110
50
|
}
|
|
111
51
|
if (validated.errorCount > 0) {
|
|
112
|
-
return emitRunValidationError(
|
|
52
|
+
return emitRunValidationError(json, adapter, validated.recipeFile, validated.findings, validated.errorCount);
|
|
113
53
|
}
|
|
114
54
|
const depsBlock = adapter === "core" && recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
115
55
|
if (depsBlock) {
|
|
116
|
-
return emitRunUsageError(
|
|
56
|
+
return emitRunUsageError(json, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
117
57
|
}
|
|
118
58
|
const artifactsDir = requiredOption(options, "artifactsDir", "run requires --artifacts-dir <dir>.");
|
|
119
59
|
const lock = acquireCheckoutLock(target, "run");
|
|
120
|
-
if ("message" in lock)
|
|
121
|
-
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
122
|
-
stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
|
|
123
|
-
return checkoutBusyOut(jsonOutput, "run", lock.message, lock.path);
|
|
124
|
-
}
|
|
60
|
+
if ("message" in lock) return emitRunRecipeRunning(json, lock.message);
|
|
125
61
|
try {
|
|
126
|
-
const prepared = await prepareHeal(adapter, target, options,
|
|
127
|
-
|
|
128
|
-
});
|
|
129
|
-
if (typeof prepared === "number") {
|
|
130
|
-
stream.error({
|
|
131
|
-
code: "RUN_PREPARE_FAILED",
|
|
132
|
-
message: "runtime preparation failed; inspect stderr for the exact probe",
|
|
133
|
-
userAction: `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`
|
|
134
|
-
});
|
|
135
|
-
return prepared;
|
|
136
|
-
}
|
|
62
|
+
const prepared = await prepareHeal(adapter, target, options, json);
|
|
63
|
+
if (typeof prepared === "number") return prepared;
|
|
137
64
|
const { state, heal } = prepared;
|
|
138
65
|
const librarySources = validated.librarySources;
|
|
139
66
|
const runtimeOptions = {
|
|
140
67
|
...runtimeOptionsFromCli(options),
|
|
141
68
|
...librarySources ? { librarySources } : {},
|
|
142
|
-
stdoutIsMachineContract:
|
|
143
|
-
onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
|
|
69
|
+
stdoutIsMachineContract: json
|
|
144
70
|
};
|
|
145
|
-
stream.phase("execute");
|
|
146
71
|
const { result, violation } = await executeWithHealBounds(
|
|
147
72
|
// validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
|
|
148
73
|
// that only the resolver knows how to turn into a file.
|
|
@@ -151,42 +76,12 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
151
76
|
target,
|
|
152
77
|
heal,
|
|
153
78
|
state,
|
|
154
|
-
() => recoverRunInfra(adapter, target,
|
|
155
|
-
(code) => stream.phase("recover", { code })
|
|
79
|
+
() => recoverRunInfra(adapter, target, json)
|
|
156
80
|
);
|
|
157
|
-
|
|
158
|
-
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
159
|
-
if (violation !== null) {
|
|
160
|
-
const userAction = violation.userAction ?? `inspect ${shellQuote(result.summaryPath)} and ${shellQuote(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
161
|
-
stream.error({
|
|
162
|
-
code: violation.code,
|
|
163
|
-
message: violation.message,
|
|
164
|
-
userAction,
|
|
165
|
-
originalError: violation.originalError ?? null
|
|
166
|
-
});
|
|
167
|
-
return emitHealViolation(jsonOutput, "run", result, violation, state, adapter);
|
|
168
|
-
}
|
|
81
|
+
if (violation !== null) return emitHealViolation(json, "run", result, violation, state, adapter);
|
|
169
82
|
const report = writeRunReport(result);
|
|
170
83
|
const exitCode = result.status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
171
|
-
|
|
172
|
-
if (stream.enabled) {
|
|
173
|
-
if (result.status === "fail") {
|
|
174
|
-
stream.error({
|
|
175
|
-
code: "RECIPE_EXECUTION_FAILED",
|
|
176
|
-
message: "recipe execution failed; inspect the persisted result and evidence paths",
|
|
177
|
-
userAction: failureUserAction
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
stream.complete(result.status === "pass" ? "pass" : "fail", exitCode, {
|
|
181
|
-
adapter,
|
|
182
|
-
reportPath: report.path,
|
|
183
|
-
summaryPath: result.summaryPath,
|
|
184
|
-
tracePath: result.tracePath,
|
|
185
|
-
artifactManifestPath: result.artifactManifestPath,
|
|
186
|
-
recovered: state.recovered,
|
|
187
|
-
mutations: state.mutations
|
|
188
|
-
});
|
|
189
|
-
} else if (json) {
|
|
84
|
+
if (json) {
|
|
190
85
|
console.log(
|
|
191
86
|
JSON.stringify(
|
|
192
87
|
{
|
|
@@ -198,8 +93,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
198
93
|
recovered: state.recovered,
|
|
199
94
|
mutations: state.mutations,
|
|
200
95
|
reportPath: report.path,
|
|
201
|
-
result
|
|
202
|
-
...result.status === "fail" ? { error: { code: "RECIPE_EXECUTION_FAILED", message: "recipe execution failed; inspect the persisted result and evidence paths", userAction: failureUserAction } } : {}
|
|
96
|
+
result
|
|
203
97
|
},
|
|
204
98
|
null,
|
|
205
99
|
2
|
|
@@ -212,61 +106,14 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
212
106
|
console.log(out("label", "summary:"));
|
|
213
107
|
for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
|
|
214
108
|
}
|
|
215
|
-
|
|
216
|
-
console.log(out("label",
|
|
217
|
-
for (const artifact of artifacts) {
|
|
218
|
-
console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
|
|
219
|
-
}
|
|
220
|
-
if (result.status === "fail") {
|
|
221
|
-
console.error(` Next: ${failureUserAction}`);
|
|
222
|
-
}
|
|
223
|
-
console.log(out("label", "diagnostics:"));
|
|
224
|
-
const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
|
|
225
|
-
for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
|
|
226
|
-
console.log(` ${formatDiagnosticLine(line, out)}`);
|
|
227
|
-
}
|
|
109
|
+
console.log(`${out("label", "report:")} ${out("path", report.path)}`);
|
|
110
|
+
console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
|
|
228
111
|
}
|
|
229
112
|
return exitCode;
|
|
230
113
|
} finally {
|
|
231
114
|
lock.release();
|
|
232
115
|
}
|
|
233
116
|
}
|
|
234
|
-
function formatDiagnosticLine(line, out) {
|
|
235
|
-
const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
|
|
236
|
-
if (!match) return line;
|
|
237
|
-
const status = match[1];
|
|
238
|
-
const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
|
|
239
|
-
return `${out(style, status)}${match[2]}`;
|
|
240
|
-
}
|
|
241
|
-
function runArtifactInventory(manifestPathValue) {
|
|
242
|
-
const manifestPath = path.resolve(String(manifestPathValue));
|
|
243
|
-
const root = path.dirname(manifestPath);
|
|
244
|
-
let manifest;
|
|
245
|
-
try {
|
|
246
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
247
|
-
} catch {
|
|
248
|
-
return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
|
|
249
|
-
}
|
|
250
|
-
const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
251
|
-
const artifacts = [];
|
|
252
|
-
for (const entry of entries) {
|
|
253
|
-
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
|
|
254
|
-
const absolutePath = path.resolve(root, entry.path);
|
|
255
|
-
const basename = path.basename(absolutePath);
|
|
256
|
-
const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
|
|
257
|
-
artifacts.push({ absolutePath, basename, label });
|
|
258
|
-
}
|
|
259
|
-
artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
|
|
260
|
-
const priority = /* @__PURE__ */ new Map([
|
|
261
|
-
["diagnostics.json", 0],
|
|
262
|
-
["report.md", 1],
|
|
263
|
-
["summary.json", 2],
|
|
264
|
-
["trace.json", 3],
|
|
265
|
-
["recipe.json", 4],
|
|
266
|
-
["artifact-manifest.json", 100]
|
|
267
|
-
]);
|
|
268
|
-
return artifacts.filter((artifact, index, all) => all.findIndex((candidate) => candidate.absolutePath === artifact.absolutePath) === index).sort((left, right) => (priority.get(left.basename) ?? 50) - (priority.get(right.basename) ?? 50));
|
|
269
|
-
}
|
|
270
117
|
function formatPreviewLine(line, out) {
|
|
271
118
|
const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
|
|
272
119
|
if (!match) return line;
|
|
@@ -338,29 +185,12 @@ function loadFlowCatalogs(librarySources) {
|
|
|
338
185
|
function actionUsesCoreController(action) {
|
|
339
186
|
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
340
187
|
}
|
|
341
|
-
async function handleRunPlan(recipeArg, options
|
|
188
|
+
async function handleRunPlan(recipeArg, options) {
|
|
342
189
|
const json = optionFlag(options, "json");
|
|
343
|
-
const jsonOutput = json && !stream.enabled;
|
|
344
190
|
const { adapter, target } = resolveAdapter(options);
|
|
345
|
-
stream.phase("resolve", { adapter, target, recipe: recipeArg });
|
|
346
|
-
stream.phase("validate");
|
|
347
191
|
const validated = await validateRunRecipeStatic(recipeArg, adapter, options);
|
|
348
192
|
if (validated.usageError) {
|
|
349
|
-
|
|
350
|
-
validated.usageError.code,
|
|
351
|
-
validated.usageError.message,
|
|
352
|
-
adapter,
|
|
353
|
-
validated.recipeFile
|
|
354
|
-
);
|
|
355
|
-
return emitPlanUsageError(
|
|
356
|
-
jsonOutput,
|
|
357
|
-
stream,
|
|
358
|
-
adapter,
|
|
359
|
-
validated.recipeFile,
|
|
360
|
-
validated.usageError.code,
|
|
361
|
-
validated.usageError.message,
|
|
362
|
-
userAction
|
|
363
|
-
);
|
|
193
|
+
return emitPlanUsageError(json, adapter, validated.recipeFile, validated.usageError.code, validated.usageError.message);
|
|
364
194
|
}
|
|
365
195
|
const { recipe, recipeFile, findings, errorCount, manifestOk, schemaValid } = validated;
|
|
366
196
|
const status = errorCount === 0 ? "pass" : "fail";
|
|
@@ -414,27 +244,9 @@ async function handleRunPlan(recipeArg, options, stream) {
|
|
|
414
244
|
detail: nodeCount === void 0 ? "would execute the recipe nodes" : `would execute ${nodeCount} recipe node(s)`
|
|
415
245
|
}
|
|
416
246
|
];
|
|
417
|
-
|
|
418
|
-
schemaVersion: 1,
|
|
419
|
-
|
|
420
|
-
mode: "plan",
|
|
421
|
-
status,
|
|
422
|
-
adapter,
|
|
423
|
-
recipe: recipeFile,
|
|
424
|
-
findings,
|
|
425
|
-
plan
|
|
426
|
-
};
|
|
427
|
-
if (status === "fail") {
|
|
428
|
-
payload.error = {
|
|
429
|
-
code: "RECIPE_VALIDATION_FAILED",
|
|
430
|
-
message: `recipe validation found ${errorCount} error(s)`,
|
|
431
|
-
userAction: runPlanProbe(adapter, recipeFile)
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
if (stream.enabled) {
|
|
435
|
-
if (status === "fail") stream.error(payload.error);
|
|
436
|
-
stream.complete(status, status === "pass" ? EXIT.ok : EXIT.validation, { mode: "plan", adapter, recipe: recipeFile, findings, plan });
|
|
437
|
-
} else if (json) {
|
|
247
|
+
if (json) {
|
|
248
|
+
const payload = { schemaVersion: 1, command: "run", mode: "plan", status, adapter, recipe: recipeFile, findings, plan };
|
|
249
|
+
if (status === "fail") payload.error = { code: "RECIPE_VALIDATION_FAILED", message: `recipe validation found ${errorCount} error(s)` };
|
|
438
250
|
console.log(JSON.stringify(payload, null, 2));
|
|
439
251
|
} else {
|
|
440
252
|
console.log(`plan ${status} \u2014 ${adapter} \u2014 ${recipeFile}`);
|
|
@@ -448,61 +260,52 @@ async function handleRunPlan(recipeArg, options, stream) {
|
|
|
448
260
|
console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
449
261
|
}
|
|
450
262
|
}
|
|
451
|
-
if (status === "fail") console.error(` Next: ${runPlanProbe(adapter, recipeFile)}`);
|
|
452
263
|
}
|
|
453
264
|
return status === "pass" ? EXIT.ok : EXIT.validation;
|
|
454
265
|
}
|
|
455
|
-
function emitPlanUsageError(json,
|
|
456
|
-
stream.error({ code, message, userAction, mode: "plan", adapter, recipe: recipeFile });
|
|
266
|
+
function emitPlanUsageError(json, adapter, recipeFile, code, message) {
|
|
457
267
|
if (json) {
|
|
458
268
|
console.log(
|
|
459
269
|
JSON.stringify(
|
|
460
|
-
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message
|
|
270
|
+
{ schemaVersion: 1, command: "run", mode: "plan", status: "fail", adapter, recipe: recipeFile, error: { code, message } },
|
|
461
271
|
null,
|
|
462
272
|
2
|
|
463
273
|
)
|
|
464
274
|
);
|
|
465
275
|
} else {
|
|
466
|
-
console.error(`\u2717 run --plan: ${message}
|
|
467
|
-
Next: ${userAction}`);
|
|
276
|
+
console.error(`\u2717 run --plan: ${message}`);
|
|
468
277
|
}
|
|
469
278
|
return EXIT.usage;
|
|
470
279
|
}
|
|
471
280
|
function runArgLooksLikeRecipeFile(value) {
|
|
472
281
|
return path.isAbsolute(value) || value.includes("/") || value.includes(path.sep) || value.endsWith(".json") || fs.existsSync(path.resolve(value));
|
|
473
282
|
}
|
|
474
|
-
function emitRunRecipeRunning(json,
|
|
283
|
+
function emitRunRecipeRunning(json, detail) {
|
|
475
284
|
const message = detail ?? "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
476
|
-
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuote(target)} --json; retry after the active recipe finishes`;
|
|
477
|
-
stream.error({ code: "RECIPE_RUNNING", message, userAction, recoverable: false });
|
|
478
285
|
if (json) {
|
|
479
|
-
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message
|
|
286
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message } }, null, 2));
|
|
480
287
|
} else {
|
|
481
|
-
console.error(`\u2717 mm-harness: ${message}
|
|
482
|
-
Next: ${userAction}`);
|
|
288
|
+
console.error(`\u2717 mm-harness: ${message}`);
|
|
483
289
|
}
|
|
484
290
|
return EXIT.bounded;
|
|
485
291
|
}
|
|
486
|
-
function emitRunUsageError(json,
|
|
487
|
-
stream.error({ code, message, userAction });
|
|
292
|
+
function emitRunUsageError(json, adapter, recipeFile, code, message, userAction) {
|
|
488
293
|
if (json) {
|
|
489
294
|
console.log(
|
|
490
295
|
JSON.stringify(
|
|
491
|
-
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, userAction } },
|
|
296
|
+
{ schemaVersion: 1, command: "run", adapter, status: "fail", exitCode: EXIT.usage, recipe: recipeFile, error: { code, message, ...userAction ? { userAction } : {} } },
|
|
492
297
|
null,
|
|
493
298
|
2
|
|
494
299
|
)
|
|
495
300
|
);
|
|
496
301
|
} else {
|
|
497
302
|
console.error(`\u2717 run: ${message}`);
|
|
498
|
-
console.error(` Next: ${userAction}`);
|
|
303
|
+
if (userAction) console.error(` Next: ${userAction}`);
|
|
499
304
|
}
|
|
500
305
|
return EXIT.usage;
|
|
501
306
|
}
|
|
502
|
-
function emitRunValidationError(json,
|
|
307
|
+
function emitRunValidationError(json, adapter, recipeFile, findings, errorCount) {
|
|
503
308
|
const message = `recipe validation found ${errorCount} error(s)`;
|
|
504
|
-
const userAction = runPlanProbe(adapter, recipeFile);
|
|
505
|
-
stream.error({ code: "RECIPE_VALIDATION_FAILED", message, userAction, findings });
|
|
506
309
|
if (json) {
|
|
507
310
|
console.log(
|
|
508
311
|
JSON.stringify(
|
|
@@ -516,7 +319,7 @@ function emitRunValidationError(json, stream, adapter, recipeFile, findings, err
|
|
|
516
319
|
mutations: [],
|
|
517
320
|
recipe: recipeFile,
|
|
518
321
|
findings,
|
|
519
|
-
error: { code: "RECIPE_VALIDATION_FAILED", message
|
|
322
|
+
error: { code: "RECIPE_VALIDATION_FAILED", message }
|
|
520
323
|
},
|
|
521
324
|
null,
|
|
522
325
|
2
|
|
@@ -527,22 +330,9 @@ function emitRunValidationError(json, stream, adapter, recipeFile, findings, err
|
|
|
527
330
|
for (const finding of findings) {
|
|
528
331
|
if (finding.severity === "error") console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
529
332
|
}
|
|
530
|
-
console.error(` Next: ${userAction}`);
|
|
531
333
|
}
|
|
532
334
|
return EXIT.validation;
|
|
533
335
|
}
|
|
534
|
-
function runPlanProbe(adapter, recipeFile) {
|
|
535
|
-
return `mm-harness run --plan ${shellQuote(recipeFile)} --adapter ${adapter} --json`;
|
|
536
|
-
}
|
|
537
|
-
function runUsageRecovery(code, message, adapter, recipeFile) {
|
|
538
|
-
const actionCommand = /This is an action, not a recipe\. Use: (mm-harness call .+)\.$/u.exec(message)?.[1];
|
|
539
|
-
if (actionCommand) return actionCommand;
|
|
540
|
-
if (code === "RECIPE_NOT_FOUND") return `mm-harness run --list --adapter ${adapter} --json`;
|
|
541
|
-
if (code === "RECIPE_UNPARSEABLE") {
|
|
542
|
-
return `fix the JSON syntax in ${shellQuote(recipeFile)}, then retry: ${runPlanProbe(adapter, recipeFile)}`;
|
|
543
|
-
}
|
|
544
|
-
return runPlanProbe(adapter, recipeFile);
|
|
545
|
-
}
|
|
546
336
|
export {
|
|
547
337
|
handleRun
|
|
548
338
|
};
|
package/dist/commands/shared.js
CHANGED
|
@@ -3,15 +3,6 @@ import path from "node:path";
|
|
|
3
3
|
import { detectAdapter } from "../harness.js";
|
|
4
4
|
import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
|
|
5
5
|
const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
|
|
6
|
-
function writeInteractiveProgress(json, message, {
|
|
7
|
-
stdoutIsTTY = Boolean(process.stdout.isTTY),
|
|
8
|
-
stream = process.stderr
|
|
9
|
-
} = {}) {
|
|
10
|
-
if (json || !stdoutIsTTY) return false;
|
|
11
|
-
stream.write(`${message}
|
|
12
|
-
`);
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
6
|
function parseFlags(argv, booleans) {
|
|
16
7
|
const positional = [];
|
|
17
8
|
const options = {};
|
|
@@ -147,16 +138,11 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
147
138
|
let settled = false;
|
|
148
139
|
let didTimeout = false;
|
|
149
140
|
let escalation;
|
|
150
|
-
const parentSignals = ["SIGINT", "SIGTERM"];
|
|
151
|
-
const removeParentSignalHandlers = () => {
|
|
152
|
-
for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
|
|
153
|
-
};
|
|
154
141
|
const finish = (result) => {
|
|
155
142
|
if (settled) return;
|
|
156
143
|
settled = true;
|
|
157
144
|
if (timer) clearTimeout(timer);
|
|
158
|
-
if (escalation) clearTimeout(escalation);
|
|
159
|
-
removeParentSignalHandlers();
|
|
145
|
+
if (escalation && !didTimeout) clearTimeout(escalation);
|
|
160
146
|
resolve(result);
|
|
161
147
|
};
|
|
162
148
|
const signalChildTree = (signal) => {
|
|
@@ -166,14 +152,6 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
166
152
|
} catch {
|
|
167
153
|
}
|
|
168
154
|
};
|
|
169
|
-
const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
|
|
170
|
-
signalChildTree(signal);
|
|
171
|
-
removeParentSignalHandlers();
|
|
172
|
-
process.kill(process.pid, signal);
|
|
173
|
-
}]));
|
|
174
|
-
if (ownsProcessGroup) {
|
|
175
|
-
for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
|
|
176
|
-
}
|
|
177
155
|
const timeoutMs = spawnOptions.timeoutMs;
|
|
178
156
|
const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
|
|
179
157
|
didTimeout = true;
|
|
@@ -204,13 +182,8 @@ function spawnScriptStreaming(script, args, cwd, options) {
|
|
|
204
182
|
`);
|
|
205
183
|
finish({ status: 1, output: message });
|
|
206
184
|
});
|
|
207
|
-
child.on("exit", () => {
|
|
208
|
-
if (!didTimeout) return;
|
|
209
|
-
signalChildTree("SIGKILL");
|
|
210
|
-
finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
|
|
211
|
-
});
|
|
212
185
|
child.on("close", (status) => {
|
|
213
|
-
if (didTimeout)
|
|
186
|
+
if (didTimeout && escalation) return;
|
|
214
187
|
finish({
|
|
215
188
|
status: didTimeout ? 1 : status ?? 1,
|
|
216
189
|
output,
|
|
@@ -235,33 +208,9 @@ function usageOut(json, command, message, userAction) {
|
|
|
235
208
|
}
|
|
236
209
|
return EXIT.usage;
|
|
237
210
|
}
|
|
238
|
-
function checkoutBusyOut(json, command, message, lockPath) {
|
|
239
|
-
const userAction = `wait for the current owner, or inspect ${lockPath} if its process has exited`;
|
|
240
|
-
if (json) {
|
|
241
|
-
console.log(
|
|
242
|
-
JSON.stringify(
|
|
243
|
-
{
|
|
244
|
-
schemaVersion: 1,
|
|
245
|
-
command,
|
|
246
|
-
status: "fail",
|
|
247
|
-
exitCode: EXIT.bounded,
|
|
248
|
-
recoverable: false,
|
|
249
|
-
error: { code: "SANDBOX_BUSY", message, userAction }
|
|
250
|
-
},
|
|
251
|
-
null,
|
|
252
|
-
2
|
|
253
|
-
)
|
|
254
|
-
);
|
|
255
|
-
} else {
|
|
256
|
-
console.error(`\u2717 mm-harness ${command}: ${message}
|
|
257
|
-
Next: ${userAction}`);
|
|
258
|
-
}
|
|
259
|
-
return EXIT.bounded;
|
|
260
|
-
}
|
|
261
211
|
export {
|
|
262
212
|
ADAPTER_DETECT_NEXT,
|
|
263
213
|
EXIT,
|
|
264
|
-
checkoutBusyOut,
|
|
265
214
|
flag,
|
|
266
215
|
parseFlags,
|
|
267
216
|
resolveAdapter,
|
|
@@ -271,6 +220,5 @@ export {
|
|
|
271
220
|
spawnScriptStreaming,
|
|
272
221
|
str,
|
|
273
222
|
targetOf,
|
|
274
|
-
usageOut
|
|
275
|
-
writeInteractiveProgress
|
|
223
|
+
usageOut
|
|
276
224
|
};
|
|
@@ -34,7 +34,7 @@ function parseBridgeEntries(raw) {
|
|
|
34
34
|
deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
|
|
35
35
|
platform: typeof e.platform === "string" ? e.platform : "",
|
|
36
36
|
// agenticPresent is only set when the bridge explicitly sends it (false = absent).
|
|
37
|
-
//
|
|
37
|
+
// Omitted by older bridge versions → leave undefined so caller treats as present.
|
|
38
38
|
...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
|
|
39
39
|
}));
|
|
40
40
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -31,7 +31,7 @@ async function handleStatus({ options }) {
|
|
|
31
31
|
if (adapter === "mobile") {
|
|
32
32
|
const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
|
|
33
33
|
if ("code" in dtResult) {
|
|
34
|
-
return usageOut(json, "status", dtResult.message,
|
|
34
|
+
return usageOut(json, "status", dtResult.message, "mm-harness status --device <id> or --all-devices");
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
const next = surface.hints.relaunch;
|
package/dist/commands/stop.js
CHANGED
|
@@ -5,8 +5,7 @@ import {
|
|
|
5
5
|
parseArgs,
|
|
6
6
|
optionFlag,
|
|
7
7
|
optionString,
|
|
8
|
-
resolveAdapter
|
|
9
|
-
shellQuote
|
|
8
|
+
resolveAdapter
|
|
10
9
|
} from "./parse-args.js";
|
|
11
10
|
async function handleStop(argv) {
|
|
12
11
|
const { options } = parseArgs(argv, "stop");
|
|
@@ -23,8 +22,6 @@ async function handleStop(argv) {
|
|
|
23
22
|
if (stop.kind === "headless") {
|
|
24
23
|
return usageOut(json, "stop", stop.message, stop.userAction);
|
|
25
24
|
}
|
|
26
|
-
const next = adapter === "extension" ? `${surface.hints.launch} --target ${shellQuote(target)}` : void 0;
|
|
27
|
-
const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
|
|
28
25
|
if (json) {
|
|
29
26
|
console.log(
|
|
30
27
|
JSON.stringify(
|
|
@@ -36,8 +33,7 @@ async function handleStop(argv) {
|
|
|
36
33
|
status: stop.status === 0 ? "pass" : "fail",
|
|
37
34
|
...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
|
|
38
35
|
exitCode: stop.status,
|
|
39
|
-
...stop.output ? { output: stop.output } : {}
|
|
40
|
-
...stop.status === 0 ? next ? { next } : {} : { error: { code: "DEV_SERVER_STOP_FAILED", message: stop.summary, userAction } }
|
|
36
|
+
...stop.output ? { output: stop.output } : {}
|
|
41
37
|
},
|
|
42
38
|
null,
|
|
43
39
|
2
|
|
@@ -47,7 +43,6 @@ async function handleStop(argv) {
|
|
|
47
43
|
if (stop.output) process.stderr.write(`${stop.output}
|
|
48
44
|
`);
|
|
49
45
|
console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
|
|
50
|
-
if (stop.status !== 0 || next) console.error(` Next: ${stop.status === 0 ? next : userAction}`);
|
|
51
46
|
}
|
|
52
47
|
return stop.status;
|
|
53
48
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
@@ -6,8 +6,7 @@ import { runnerDir } from "../paths.js";
|
|
|
6
6
|
import { EXIT, flag, parseFlags } from "./shared.js";
|
|
7
7
|
const PACKAGE_NAME = "@deeeed/metamask-harness";
|
|
8
8
|
const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9
|
-
const NUDGE_FETCH_TIMEOUT_MS =
|
|
10
|
-
const REGISTRY_DIST_TAGS_ENDPOINT = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
|
|
9
|
+
const NUDGE_FETCH_TIMEOUT_MS = 300;
|
|
11
10
|
function currentVersion() {
|
|
12
11
|
try {
|
|
13
12
|
const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
|
|
@@ -57,6 +56,24 @@ function fetchLatest(timeoutMs) {
|
|
|
57
56
|
if (!latest) return { error: { kind: "unreachable", message: "npm returned no dist-tag for latest" } };
|
|
58
57
|
return { latest };
|
|
59
58
|
}
|
|
59
|
+
async function fetchLatestAsync(timeoutMs) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
|
|
64
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
65
|
+
if (!res.ok) return { error: { kind: "unreachable", message: `registry returned HTTP ${res.status}` } };
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
const latest = data["latest"];
|
|
68
|
+
if (!latest) return { error: { kind: "unreachable", message: "npm registry returned no latest dist-tag" } };
|
|
69
|
+
return { latest };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const msg = err.name === "AbortError" ? `registry fetch timed out after ${timeoutMs}ms` : err.message;
|
|
72
|
+
return { error: { kind: "unreachable", message: msg } };
|
|
73
|
+
} finally {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
function teachFailure(json, current, code, message, userAction) {
|
|
61
78
|
if (json) {
|
|
62
79
|
console.log(
|
|
@@ -194,28 +211,23 @@ function nudgeDisabled() {
|
|
|
194
211
|
function nudgeLine(current, latest) {
|
|
195
212
|
return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
|
|
196
213
|
}
|
|
197
|
-
function maybeNudge(now = Date.now()) {
|
|
214
|
+
async function maybeNudge(now = Date.now()) {
|
|
198
215
|
if (nudgeDisabled()) return;
|
|
199
216
|
const file = cacheFile();
|
|
200
217
|
const cache = readCache(file);
|
|
201
218
|
const current = currentVersion();
|
|
219
|
+
let latest = cache?.latest ?? "";
|
|
202
220
|
process.once("exit", () => {
|
|
203
|
-
const line = nudgeLine(current,
|
|
221
|
+
const line = nudgeLine(current, latest);
|
|
204
222
|
if (line) process.stderr.write(`${line}
|
|
205
223
|
`);
|
|
206
224
|
});
|
|
207
225
|
if (!cache || now - cache.lastCheck >= NUDGE_INTERVAL_MS) {
|
|
208
|
-
writeCache(file, { lastCheck: now, latest
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
process.execPath,
|
|
214
|
-
[worker, file, String(NUDGE_FETCH_TIMEOUT_MS), REGISTRY_DIST_TAGS_ENDPOINT],
|
|
215
|
-
{ detached: true, stdio: "ignore" }
|
|
216
|
-
);
|
|
217
|
-
child.unref();
|
|
218
|
-
} catch {
|
|
226
|
+
writeCache(file, { lastCheck: now, latest });
|
|
227
|
+
const fetched = await fetchLatestAsync(NUDGE_FETCH_TIMEOUT_MS);
|
|
228
|
+
if (fetched.latest) {
|
|
229
|
+
latest = fetched.latest;
|
|
230
|
+
writeCache(file, { lastCheck: now, latest });
|
|
219
231
|
}
|
|
220
232
|
}
|
|
221
233
|
}
|