@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +6 -103
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -55
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -58
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -107
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -43
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -1,441 +0,0 @@
1
- const bool = () => ({ kind: "boolean" });
2
- const value = (choices) => ({ kind: "value", ...choices ? { choices } : {} });
3
- const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { choices } : {} });
4
- const options = (...groups) => Object.assign({}, ...groups);
5
- const HELP = { "--help": bool(), "-h": bool() };
6
- const JSON = { "--json": bool() };
7
- const JSON_STREAM = { "--json-stream": bool() };
8
- const TARGET = { "--target": value(), "--project-root": value() };
9
- const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
10
- const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
11
- const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "ios", "android"]) };
12
- const DEVICE = { "--device": value() };
13
- const RUNTIME_PORTS = {
14
- "--cdp-port": value(),
15
- "--watcher-port": value(),
16
- "--metro-port": value(),
17
- "--runtime-dir": value()
18
- };
19
- const RECIPE_RUNTIME = {
20
- "--artifacts-dir": value(),
21
- "--action-manifest": value(),
22
- "--library": value(),
23
- "--heal": value(["off", "infra-only", "auto"]),
24
- "--slot": value(),
25
- "--validation-runtime-dir": value(),
26
- "--launch-existing-dist": bool(),
27
- "--record-video": optionalValue(["full-run", "off"]),
28
- "--record": bool()
29
- };
30
- const RUNWAY = {
31
- "--branch": value(),
32
- "--default-branch": value(),
33
- "--run": value(),
34
- "--cache-root": value(),
35
- "--simulator": value(),
36
- "--runtime": value(),
37
- "--device-type": value(),
38
- "--slot": value(),
39
- "--watcher-port": value(),
40
- "--runtime-dir": value(),
41
- "--force": bool(),
42
- "--resolve-only": bool()
43
- };
44
- const PUBLIC_COMMAND_CONTRACTS = {
45
- status: {
46
- aliases: ["health", "home"],
47
- options: options(HELP, JSON, TARGET, DEVICE, {
48
- "--fast": bool(),
49
- "--all-devices": bool()
50
- })
51
- },
52
- checklist: {
53
- usage: "mm-harness checklist mark <task-dir> <step> [options]",
54
- options: options(HELP, {
55
- "--mark-last": bool(),
56
- "--already-fixed": bool(),
57
- "--no-self-review": bool(),
58
- "--skip-learnings": bool(),
59
- "--skip-checklist": bool(),
60
- "--reason": value(),
61
- "--checklist": value(),
62
- "--signal": value()
63
- }),
64
- positionals: [
65
- { label: "action", choices: ["mark"] },
66
- { label: "task-dir" },
67
- {
68
- label: "step",
69
- validate: (step) => {
70
- if (["start", "complete", "no-change", "blocked"].includes(step)) return true;
71
- const numeric = Number(step);
72
- return Number.isInteger(numeric) && numeric >= 1;
73
- },
74
- validDescription: "start|complete|no-change|blocked|a positive numeric step"
75
- }
76
- ],
77
- minimumPositionals: 3
78
- },
79
- actions: {
80
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
81
- "--action": value(),
82
- "--categories": bool(),
83
- "--category": value(),
84
- "--action-manifest": value(),
85
- "--raw": bool()
86
- }),
87
- positionals: [{ label: "query" }],
88
- noPositionalsWith: ["--action", "--categories", "--raw"]
89
- },
90
- stop: {
91
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
92
- "--port": value(),
93
- "--watcher-port": value()
94
- })
95
- },
96
- call: {
97
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
98
- "--list": bool(),
99
- "--arg": value()
100
- }),
101
- positionals: [{ label: "action" }],
102
- minimumPositionals: 0,
103
- requiredUnless: ["--list"],
104
- noPositionalsWith: ["--list"],
105
- leadingPositionals: 1,
106
- variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
107
- },
108
- flows: {
109
- options: options(HELP, JSON, TARGET, {
110
- "--library": value(),
111
- "--from": value(),
112
- "--flow": value(),
113
- "--to": value(),
114
- "--domain": value(),
115
- "--run": value(),
116
- "--force": bool()
117
- }),
118
- positionals: [{ label: "action", choices: ["list", "promote"] }]
119
- },
120
- run: {
121
- options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
122
- "--list": bool(),
123
- "--plan": bool()
124
- }),
125
- positionals: [{ label: "recipe" }],
126
- minimumPositionals: 1,
127
- requiredUnless: ["--list"],
128
- noPositionalsWith: ["--list"]
129
- },
130
- last: {
131
- options: options(HELP, JSON, TARGET, {
132
- "--runtime-dir": value()
133
- })
134
- },
135
- doctor: {
136
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
137
- "--fix": bool(),
138
- "--expect-live": bool(),
139
- "--print-ready": bool(),
140
- "--all-devices": bool(),
141
- "--action-manifest": value(),
142
- "--port": value()
143
- })
144
- },
145
- check: {
146
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
147
- "--base": value(),
148
- "--profile": value(["fast", "full"]),
149
- "--fix": bool(),
150
- "--artifacts-dir": value()
151
- }),
152
- positionals: [{ label: "action", choices: ["diff"] }],
153
- minimumPositionals: 1
154
- },
155
- "recipe-quality": {
156
- options: options(HELP, JSON, {
157
- "--input": value(),
158
- "--output": value()
159
- }),
160
- positionals: [{ label: "action", choices: ["build"] }]
161
- },
162
- provision: {
163
- options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, RUNWAY, {
164
- "--platform": value(["ios"]),
165
- "--device-platform": value(["ios"])
166
- }),
167
- positionals: [
168
- { label: "source", choices: ["runway", "ios"] },
169
- { label: "platform", choices: ["ios"] }
170
- ]
171
- },
172
- install: {
173
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, RUNWAY, {
174
- "--runway": bool()
175
- }),
176
- allowPassthrough: true
177
- },
178
- verify: {
179
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, RUNTIME_PORTS),
180
- allowPassthrough: true
181
- },
182
- cleanup: {
183
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM),
184
- allowPassthrough: true
185
- },
186
- launch: {
187
- options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, {
188
- "--build": bool(),
189
- "--verify": bool(),
190
- "--runway": bool(),
191
- "--watch": bool(),
192
- "--sidepanel": bool(),
193
- "--fullscreen": bool(),
194
- "--url": value(),
195
- "--heal": value(["off", "infra-only", "auto"])
196
- }),
197
- positionals: [{ label: "platform", choices: ["ios", "android"] }]
198
- },
199
- logs: {
200
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
201
- "--full": bool(),
202
- "--window": bool(),
203
- "--events": value(),
204
- "--source": value()
205
- })
206
- },
207
- debug: {
208
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
209
- "--worker": bool(),
210
- "--dev-menu": bool(),
211
- "--no-open": bool()
212
- })
213
- },
214
- update: {
215
- options: options(HELP, JSON, { "--check": bool() })
216
- },
217
- fixtures: {
218
- options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
219
- "--from": value(),
220
- "--dev": bool(),
221
- "--force": bool(),
222
- "--fixture": value(),
223
- "--out": value(),
224
- "--state": value(),
225
- "--cdp-port": value(),
226
- "--extension-dir": value(),
227
- "--extension-id-file": value(),
228
- "--action-manifest": value()
229
- }),
230
- positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
231
- minimumPositionals: 1
232
- }
233
- };
234
- function validatePublicInvocation(argv, examples = {}) {
235
- const token = argv[0];
236
- if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
237
- const resolved = resolveCommand(token);
238
- if (!resolved) {
239
- const names = publicCommandTokens();
240
- const suggestion = closest(token, names);
241
- if (token.startsWith("-")) {
242
- return {
243
- code: "CLI_UNKNOWN_OPTION",
244
- command: "mm-harness",
245
- message: `unknown top-level option '${token}'. Valid top-level options: --help, --version.`,
246
- userAction: actionFor(token, closest(token, ["--help", "--version"]), "mm-harness --help")
247
- };
248
- }
249
- return {
250
- code: "CLI_UNKNOWN_COMMAND",
251
- command: token,
252
- message: `unknown command '${token}'. Valid commands: ${Object.keys(PUBLIC_COMMAND_CONTRACTS).join(", ")}.`,
253
- userAction: actionFor(token, suggestion, suggestion ? exampleFor(resolveCommand(suggestion)?.name, examples) : "mm-harness --help")
254
- };
255
- }
256
- const { name, contract } = resolved;
257
- const tokens = argv.slice(1);
258
- const positionals = [];
259
- const seenOptions = /* @__PURE__ */ new Set();
260
- const validOptions = Object.keys(contract.options);
261
- for (let index = 0; index < tokens.length; index += 1) {
262
- const argument = tokens[index] ?? "";
263
- if (argument === "--") {
264
- if (!contract.allowPassthrough) {
265
- return usageFailure("CLI_UNEXPECTED_PASSTHROUGH", name, "this command does not accept `--` passthrough.", contract, examples);
266
- }
267
- break;
268
- }
269
- if (!argument.startsWith("-") || argument === "-") {
270
- positionals.push(argument);
271
- continue;
272
- }
273
- const equals = argument.indexOf("=");
274
- const optionName = equals === -1 ? argument : argument.slice(0, equals);
275
- const inlineValue = equals === -1 ? void 0 : argument.slice(equals + 1);
276
- const spec = contract.options[optionName];
277
- if (!spec) {
278
- const suggestion = closest(optionName, validOptions);
279
- return {
280
- code: "CLI_UNKNOWN_OPTION",
281
- command: name,
282
- message: `unknown option '${optionName}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
283
- userAction: actionFor(optionName, suggestion, exampleFor(name, examples))
284
- };
285
- }
286
- seenOptions.add(optionName);
287
- if (spec.kind === "boolean") {
288
- if (inlineValue !== void 0) {
289
- return usageFailure("CLI_INVALID_OPTION_VALUE", name, `${optionName} is a flag and does not take a value.`, contract, examples);
290
- }
291
- continue;
292
- }
293
- if (spec.kind === "optional-value" && inlineValue === void 0) continue;
294
- const optionValue = inlineValue ?? tokens[index + 1];
295
- const nextTokenIsOption = inlineValue === void 0 && (optionValue === "-h" || optionValue === "--" || optionValue?.startsWith("--"));
296
- if (optionValue === void 0 || spec.kind === "value" && optionValue === "" || nextTokenIsOption) {
297
- return usageFailure("CLI_MISSING_OPTION_VALUE", name, `${optionName} requires a value.`, contract, examples);
298
- }
299
- if (inlineValue === void 0) index += 1;
300
- if (spec.choices && !spec.choices.includes(optionValue)) {
301
- const suggestion = closest(optionValue, spec.choices);
302
- return {
303
- code: "CLI_INVALID_OPTION_VALUE",
304
- command: name,
305
- message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'. Valid options for mm-harness ${name}: ${formatOptions(contract)}.`,
306
- userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
307
- };
308
- }
309
- }
310
- if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
311
- if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
312
- const missing = contract.positionals?.[0]?.label ?? "argument";
313
- return usageFailure("CLI_MISSING_POSITIONAL", name, `${name} requires <${missing}> first.`, contract, examples);
314
- }
315
- const forbiddingOption = contract.noPositionalsWith?.find((option) => seenOptions.has(option));
316
- if (forbiddingOption && positionals.length > 0) {
317
- return usageFailure(
318
- "CLI_EXCESS_POSITIONAL",
319
- name,
320
- `${forbiddingOption} does not accept positional '${positionals[0]}'.`,
321
- contract,
322
- examples
323
- );
324
- }
325
- const minimum = contract.requiredUnless?.some((option) => seenOptions.has(option)) ? 0 : contract.minimumPositionals ?? 0;
326
- if (positionals.length < minimum) {
327
- const missingSpec = contract.positionals?.[positionals.length];
328
- const missing = missingSpec?.label ?? "argument";
329
- const valid = missingSpec?.choices?.length ? ` Choose one: ${missingSpec.choices.join("|")}.` : "";
330
- const usage = contract.usage ? ` Usage: ${contract.usage}.` : "";
331
- return usageFailure("CLI_MISSING_POSITIONAL", name, `missing required <${missing}>.${valid}${usage}`, contract, examples);
332
- }
333
- const fixed = contract.positionals ?? [];
334
- for (let index = 0; index < Math.min(positionals.length, fixed.length); index += 1) {
335
- const invalid = invalidPositional(positionals[index] ?? "", fixed[index]);
336
- if (invalid) return positionalFailure(name, positionals[index] ?? "", fixed[index], contract, examples);
337
- }
338
- if (positionals.length > fixed.length && !contract.variadic) {
339
- const unexpected = positionals[fixed.length] ?? "";
340
- return usageFailure("CLI_EXCESS_POSITIONAL", name, `unexpected positional '${unexpected}'; this command accepts ${formatPositionals(contract)}.`, contract, examples);
341
- }
342
- if (contract.variadic) {
343
- for (const positional of positionals.slice(fixed.length)) {
344
- if (invalidPositional(positional, contract.variadic)) {
345
- return positionalFailure(name, positional, contract.variadic, contract, examples);
346
- }
347
- }
348
- }
349
- return null;
350
- }
351
- function publicCommandTokens() {
352
- const tokens = [];
353
- for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
354
- tokens.push(name, ...contract.aliases ?? []);
355
- }
356
- return tokens;
357
- }
358
- function publicCommandNames() {
359
- return Object.keys(PUBLIC_COMMAND_CONTRACTS);
360
- }
361
- function resolveCommand(token) {
362
- const direct = PUBLIC_COMMAND_CONTRACTS[token];
363
- if (direct) return { name: token, contract: direct };
364
- for (const [name, contract] of Object.entries(PUBLIC_COMMAND_CONTRACTS)) {
365
- if (contract.aliases?.includes(token)) return { name, contract };
366
- }
367
- return null;
368
- }
369
- function usageFailure(code, command, message, contract, examples) {
370
- return {
371
- code,
372
- command,
373
- message: `${message} Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
374
- userAction: `Try: ${exampleFor(command, examples)}. Inspect options: mm-harness ${command} --help`
375
- };
376
- }
377
- function positionalFailure(command, received, spec, contract, examples) {
378
- const valid = spec.validDescription ?? (spec.choices ? spec.choices.join(", ") : spec.pattern || spec.validate ? `a valid <${spec.label}>` : `<${spec.label}>`);
379
- const suggestion = spec.choices ? closest(received, spec.choices) : void 0;
380
- return {
381
- code: "CLI_INVALID_POSITIONAL",
382
- command,
383
- message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}. Valid options for mm-harness ${command}: ${formatOptions(contract)}.`,
384
- userAction: actionFor(received, suggestion, exampleFor(command, examples))
385
- };
386
- }
387
- function invalidPositional(value2, spec) {
388
- if (!spec) return true;
389
- if (spec.choices && !spec.choices.includes(value2)) return true;
390
- if (spec.validate && !spec.validate(value2)) return true;
391
- return Boolean(spec.pattern && !spec.pattern.test(value2));
392
- }
393
- function formatOptions(contract) {
394
- return Object.entries(contract.options).map(([name, spec]) => {
395
- if (spec.kind === "boolean") return name;
396
- const valueLabel = spec.choices?.join("|") ?? "value";
397
- return spec.kind === "optional-value" ? `${name}[=${valueLabel}]` : `${name} <${valueLabel}>`;
398
- }).join(", ");
399
- }
400
- function formatChoiceList(choices) {
401
- if (choices.length === 1) return choices[0] ?? "";
402
- if (choices.length === 2) return `${choices[0]} or ${choices[1]}`;
403
- return `${choices.slice(0, -1).join(", ")}, or ${choices[choices.length - 1]}`;
404
- }
405
- function formatPositionals(contract) {
406
- const fixed = (contract.positionals ?? []).map((spec) => `<${spec.label}>`);
407
- if (contract.variadic) fixed.push(`[<${contract.variadic.label}> ...]`);
408
- return fixed.length > 0 ? fixed.join(" ") : "no positionals";
409
- }
410
- function actionFor(received, suggestion, example) {
411
- return `${suggestion ? `Did you mean '${suggestion}' instead of '${received}'? ` : ""}Try: ${example}`;
412
- }
413
- function exampleFor(command, examples) {
414
- return command ? examples[command] ?? `mm-harness ${command} --help` : "mm-harness --help";
415
- }
416
- function closest(input, candidates) {
417
- if (candidates.length === 0) return void 0;
418
- const ranked = candidates.map((candidate) => ({ candidate, distance: levenshtein(input, candidate) })).sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate));
419
- const best = ranked[0];
420
- if (!best) return void 0;
421
- const threshold = Math.max(2, Math.floor(Math.max(input.length, best.candidate.length) / 3));
422
- return best.distance <= threshold ? best.candidate : void 0;
423
- }
424
- function levenshtein(left, right) {
425
- const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
426
- for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
427
- const current = [leftIndex];
428
- for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
429
- const substitution = prior[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
430
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
431
- }
432
- for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
433
- }
434
- return prior[right.length] ?? right.length;
435
- }
436
- export {
437
- PUBLIC_COMMAND_CONTRACTS,
438
- publicCommandNames,
439
- publicCommandTokens,
440
- validatePublicInvocation
441
- };
@@ -1,225 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { recipeRuntimeDir } from "./paths.js";
5
- const COMMAND_JOURNAL_FILE = "last-command.json";
6
- const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
7
- "call",
8
- "check",
9
- "checklist",
10
- "cleanup",
11
- "doctor",
12
- "fixtures",
13
- "install",
14
- "launch",
15
- "provision",
16
- "recipe-quality",
17
- "run",
18
- "stop",
19
- "verify"
20
- ]);
21
- const SENSITIVE_KEY = /(?:auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token)/iu;
22
- const EVIDENCE_OPTIONS = /* @__PURE__ */ new Set(["--artifacts-dir", "--out", "--output"]);
23
- async function withCommandJournal(command, argv, execute) {
24
- if (!JOURNALED_COMMANDS.has(command)) return execute();
25
- const handle = tryBeginCommandJournal(command, argv);
26
- try {
27
- const exitCode = await execute();
28
- tryFinishCommandJournal(handle, exitCode);
29
- return exitCode;
30
- } catch (error) {
31
- tryFinishCommandJournal(handle, 1);
32
- throw error;
33
- }
34
- }
35
- function commandJournalPath(target, runtimeDir) {
36
- const relative = runtimeDir ?? recipeRuntimeDir();
37
- assertRelativeRuntimeDir(relative);
38
- return path.join(path.resolve(target), relative, COMMAND_JOURNAL_FILE);
39
- }
40
- function readCommandJournal(target, runtimeDir) {
41
- const file = commandJournalPath(target, runtimeDir);
42
- let descriptor;
43
- try {
44
- const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
45
- descriptor = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
46
- const descriptorStat = fs.fstatSync(descriptor, { bigint: true });
47
- const pathStat = fs.lstatSync(file, { bigint: true });
48
- if (!descriptorStat.isFile() || !pathStat.isFile() || pathStat.isSymbolicLink() || pathStat.dev === 0n && pathStat.ino === 0n || descriptorStat.dev !== pathStat.dev || descriptorStat.ino !== pathStat.ino) {
49
- return { file, record: null };
50
- }
51
- const parsed = JSON.parse(fs.readFileSync(descriptor, "utf8"));
52
- return isCommandJournalRecord(parsed) ? { file, record: parsed } : { file, record: null };
53
- } catch {
54
- return { file, record: null };
55
- } finally {
56
- if (descriptor !== void 0) fs.closeSync(descriptor);
57
- }
58
- }
59
- function redactCommandArgs(argv) {
60
- const redacted = [];
61
- for (let index = 0; index < argv.length; index += 1) {
62
- const token = argv[index] ?? "";
63
- const equals = token.indexOf("=");
64
- if (token.startsWith("--") && equals !== -1) {
65
- const option = token.slice(0, equals);
66
- const value = token.slice(equals + 1);
67
- redacted.push(`${option}=${option === "--arg" ? redactAssignment(value) : SENSITIVE_KEY.test(option) ? "<redacted>" : redactUrl(value)}`);
68
- continue;
69
- }
70
- if (token.startsWith("--") && SENSITIVE_KEY.test(token)) {
71
- redacted.push(token);
72
- const value = argv[index + 1];
73
- if (value !== void 0 && !value.startsWith("--")) {
74
- redacted.push("<redacted>");
75
- index += 1;
76
- }
77
- continue;
78
- }
79
- if (token === "--arg") {
80
- redacted.push(token);
81
- const value = argv[index + 1];
82
- if (value !== void 0) {
83
- redacted.push(redactAssignment(value));
84
- index += 1;
85
- }
86
- continue;
87
- }
88
- redacted.push(redactAssignment(redactUrl(token)));
89
- }
90
- return redacted;
91
- }
92
- function tryBeginCommandJournal(command, argv) {
93
- try {
94
- const target = invocationTarget(argv);
95
- if (!fs.existsSync(target)) return void 0;
96
- const runtimeDir = optionValue(argv, "--runtime-dir");
97
- const file = commandJournalPath(target, runtimeDir);
98
- const now = (/* @__PURE__ */ new Date()).toISOString();
99
- const record = {
100
- schemaVersion: 1,
101
- command,
102
- args: redactCommandArgs(argv.slice(1)),
103
- target,
104
- verdict: "running",
105
- exitCode: null,
106
- evidencePaths: evidencePaths(argv),
107
- startedAt: now,
108
- finishedAt: null
109
- };
110
- writeAtomic(file, record);
111
- return { file, record };
112
- } catch (error) {
113
- process.stderr.write(`mm-harness: resumability journal unavailable: ${errorMessage(error)}
114
- `);
115
- return void 0;
116
- }
117
- }
118
- function tryFinishCommandJournal(handle, exitCode) {
119
- if (!handle) return;
120
- try {
121
- writeAtomic(handle.file, {
122
- ...handle.record,
123
- verdict: exitCode === 0 ? "pass" : "fail",
124
- exitCode,
125
- finishedAt: (/* @__PURE__ */ new Date()).toISOString()
126
- });
127
- } catch (error) {
128
- process.stderr.write(`mm-harness: resumability journal could not finalize: ${errorMessage(error)}
129
- `);
130
- }
131
- }
132
- function writeAtomic(file, record) {
133
- fs.mkdirSync(path.dirname(file), { recursive: true });
134
- const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
135
- let descriptor;
136
- try {
137
- descriptor = fs.openSync(temporary, "wx", 384);
138
- fs.writeFileSync(descriptor, `${JSON.stringify(record, null, 2)}
139
- `);
140
- fs.fsyncSync(descriptor);
141
- fs.closeSync(descriptor);
142
- descriptor = void 0;
143
- fs.renameSync(temporary, file);
144
- } finally {
145
- if (descriptor !== void 0) fs.closeSync(descriptor);
146
- fs.rmSync(temporary, { force: true });
147
- }
148
- }
149
- function invocationTarget(argv) {
150
- return path.resolve(optionValue(argv, "--target") ?? optionValue(argv, "--project-root") ?? process.cwd());
151
- }
152
- function evidencePaths(argv) {
153
- const values = /* @__PURE__ */ new Set();
154
- for (const option of EVIDENCE_OPTIONS) {
155
- const value = optionValue(argv, option);
156
- if (value) values.add(path.resolve(value));
157
- }
158
- return [...values];
159
- }
160
- function optionValue(argv, option) {
161
- const divider = argv.indexOf("--");
162
- const end = divider === -1 ? argv.length : divider;
163
- for (let index = 1; index < end; index += 1) {
164
- const token = argv[index] ?? "";
165
- if (token === option) {
166
- const value = argv[index + 1];
167
- return value && !value.startsWith("--") ? value : void 0;
168
- }
169
- if (token.startsWith(`${option}=`)) return token.slice(option.length + 1);
170
- }
171
- return void 0;
172
- }
173
- function redactAssignment(token) {
174
- const equals = token.indexOf("=");
175
- if (equals <= 0) return token;
176
- const key = token.slice(0, equals);
177
- const value = token.slice(equals + 1);
178
- return `${key}=${SENSITIVE_KEY.test(key) ? "<redacted>" : redactValue(value)}`;
179
- }
180
- function redactUrl(token) {
181
- return token.replace(/(\w+:\/\/)[^/@\s:]+:[^/@\s]+@/gu, "$1<redacted>@");
182
- }
183
- function redactValue(value) {
184
- try {
185
- return JSON.stringify(redactStructuredValue(JSON.parse(value)));
186
- } catch {
187
- return value.replace(
188
- /((?:auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token)=)[^\s]+/giu,
189
- "$1<redacted>"
190
- );
191
- }
192
- }
193
- function redactStructuredValue(value) {
194
- if (Array.isArray(value)) return value.map(redactStructuredValue);
195
- if (!value || typeof value !== "object") return value;
196
- return Object.fromEntries(
197
- Object.entries(value).map(([key, entry]) => [
198
- key,
199
- SENSITIVE_KEY.test(key) ? "<redacted>" : redactStructuredValue(entry)
200
- ])
201
- );
202
- }
203
- function assertRelativeRuntimeDir(value) {
204
- if (!value || path.isAbsolute(value) || !/^[A-Za-z0-9._/-]+$/u.test(value)) {
205
- throw new Error(`--runtime-dir must be a safe relative path: ${value}`);
206
- }
207
- if (value.split("/").some((part) => !part || part === "." || part === "..")) {
208
- throw new Error(`--runtime-dir contains an unsafe path component: ${value}`);
209
- }
210
- }
211
- function isCommandJournalRecord(value) {
212
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
213
- const record = value;
214
- return record.schemaVersion === 1 && typeof record.command === "string" && Array.isArray(record.args) && record.args.every((entry) => typeof entry === "string") && typeof record.target === "string" && (record.verdict === "running" || record.verdict === "pass" || record.verdict === "fail") && (record.exitCode === null || typeof record.exitCode === "number") && Array.isArray(record.evidencePaths) && record.evidencePaths.every((entry) => typeof entry === "string") && typeof record.startedAt === "string" && (record.finishedAt === null || typeof record.finishedAt === "string");
215
- }
216
- function errorMessage(error) {
217
- return error instanceof Error ? error.message : String(error);
218
- }
219
- export {
220
- COMMAND_JOURNAL_FILE,
221
- commandJournalPath,
222
- readCommandJournal,
223
- redactCommandArgs,
224
- withCommandJournal
225
- };