@deeeed/metamask-harness 0.18.0 → 0.19.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 +187 -0
- package/README.md +4 -7
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/inject.mjs +1 -4
- package/adapters/extension/verify.sh +1 -1
- package/adapters/mobile/inject.sh +5 -4
- package/adapters/mobile/verify.sh +15 -19
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/cli.js +0 -4
- package/dist/command-contract.js +51 -40
- package/dist/command-journal.js +50 -12
- package/dist/commands/call.js +95 -20
- package/dist/commands/check.js +1 -1
- package/dist/commands/completion-candidates.js +7 -19
- package/dist/commands/fixtures.js +50 -1
- package/dist/commands/last.js +9 -1
- package/dist/commands/launch/extension.js +2 -2
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +56 -20
- package/dist/commands/manifest.js +28 -12
- package/dist/commands/parse-args.js +55 -3
- package/dist/commands/provision.js +0 -1
- package/dist/commands/run-engine.js +514 -336
- package/dist/commands/run.js +25 -37
- package/dist/commands/shared.js +1 -1
- package/dist/heal-bounds.js +5 -0
- package/dist/mm-harness-cli.js +22 -30
- package/dist/run-diagnostics.js +1 -1
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +24 -2
- package/docs/CONTRIBUTING.md +2 -3
- package/docs/QA.md +0 -1
- package/docs/RECIPES.md +52 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1170 -413
- package/library/manifests/extension.action-manifest.json +1495 -641
- package/library/manifests/mobile.action-manifest.json +1704 -769
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
- package/library/recipes/runner/smoke.core.recipe.json +18 -20
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
- package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
- package/package.json +3 -3
- package/scripts/completions.sh +1 -4
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -91
- package/library/flows/perps.flows.json +0 -64
package/README.md
CHANGED
|
@@ -5,8 +5,7 @@ reviewable recipe evidence. Run it inside a checkout; the product, slot, ports,
|
|
|
5
5
|
and runtime paths are detected automatically.
|
|
6
6
|
|
|
7
7
|
- **Action:** one typed operation.
|
|
8
|
-
- **
|
|
9
|
-
- **Recipe:** the complete proof for a task or regression.
|
|
8
|
+
- **Recipe:** a reusable, parameterized graph of actions and called recipes.
|
|
10
9
|
|
|
11
10
|
The generic graph engine and evidence schemas live in Farmslot packages.
|
|
12
11
|
`mm-harness` owns MetaMask runtime control and domain capabilities.
|
|
@@ -71,12 +70,11 @@ mm-harness logs --source metro # Mobile bundler
|
|
|
71
70
|
mm-harness actions positions
|
|
72
71
|
mm-harness actions --action metamask.wallet.ensure_unlocked
|
|
73
72
|
mm-harness call metamask.wallet.ensure_unlocked
|
|
74
|
-
mm-harness flows
|
|
75
|
-
mm-harness flows describe perps.clean_market_testnet
|
|
76
73
|
|
|
77
74
|
mm-harness run --list
|
|
75
|
+
mm-harness run perps.clean-market-testnet --describe
|
|
78
76
|
mm-harness run wallet.smoke --describe
|
|
79
|
-
mm-harness run path/to/recipe.json --plan
|
|
77
|
+
mm-harness run path/to/recipe.json market=ETH --plan
|
|
80
78
|
mm-harness run path/to/recipe.json
|
|
81
79
|
```
|
|
82
80
|
|
|
@@ -92,11 +90,10 @@ line-buffered JSONL progress and a terminal event.
|
|
|
92
90
|
|
|
93
91
|
```bash
|
|
94
92
|
export RECIPE_LIBRARY_PATH="team=$HOME/shared-library/team-recipes"
|
|
95
|
-
mm-harness flows
|
|
96
93
|
mm-harness run --list
|
|
97
94
|
```
|
|
98
95
|
|
|
99
|
-
Shared libraries hold durable actions
|
|
96
|
+
Shared libraries hold durable actions and composable recipes. Task acceptance
|
|
100
97
|
criteria remain task-local. See [Recipes](docs/RECIPES.md).
|
|
101
98
|
|
|
102
99
|
## Recover
|
package/adapters/core/inject.sh
CHANGED
|
@@ -110,12 +110,7 @@ install_v1_runner_assets() {
|
|
|
110
110
|
if [ -d "$METAMASK_RUNNER_DIR/library/recipes" ]; then
|
|
111
111
|
rsync -a --delete "$METAMASK_RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
|
|
112
112
|
fi
|
|
113
|
-
|
|
114
|
-
# harness mirrors the runner's library layout.
|
|
115
|
-
if [ -d "$METAMASK_RUNNER_DIR/library/flows" ]; then
|
|
116
|
-
mkdir -p "$HARNESS_DIR/runner/flows"
|
|
117
|
-
rsync -a --delete "$METAMASK_RUNNER_DIR/library/flows/" "$HARNESS_DIR/runner/flows/"
|
|
118
|
-
fi
|
|
113
|
+
rm -rf "$HARNESS_DIR/runner/flows"
|
|
119
114
|
if [ -f "$METAMASK_RUNNER_DIR/library/library.json" ]; then
|
|
120
115
|
cp "$METAMASK_RUNNER_DIR/library/library.json" "$HARNESS_DIR/runner/library.json"
|
|
121
116
|
fi
|
|
@@ -90,11 +90,8 @@ copyFile(path.join(runnerDir, 'library/manifests/mobile.action-manifest.json'),
|
|
|
90
90
|
copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'runner/manifests/extension.action-manifest.json'));
|
|
91
91
|
copyFile(path.join(runnerDir, 'library/manifests/extension.action-manifest.json'), path.join(harnessDir, 'action-manifest.json'));
|
|
92
92
|
copyDir(path.join(runnerDir, 'library/recipes'), path.join(harnessDir, 'runner/recipes'));
|
|
93
|
-
// Canonical recipe-library snapshot: flows/ + library.json so the installed
|
|
94
|
-
// harness mirrors the runner's library layout.
|
|
95
93
|
copyFile(path.join(runnerDir, 'library/library.json'), path.join(harnessDir, 'runner/library.json'));
|
|
96
|
-
fs.
|
|
97
|
-
copyDir(path.join(runnerDir, 'library/flows'), path.join(harnessDir, 'runner/flows'));
|
|
94
|
+
fs.rmSync(path.join(harnessDir, 'runner/flows'), { recursive: true, force: true });
|
|
98
95
|
// The installed overlay has a flat scripts/ runtime API independent of the
|
|
99
96
|
// package's source layout.
|
|
100
97
|
copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-browser.cjs'));
|
|
@@ -434,7 +434,7 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
434
434
|
|
|
435
435
|
if (
|
|
436
436
|
cd "$TARGET"
|
|
437
|
-
"$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --
|
|
437
|
+
"$RUNNER_BIN" run "$SMOKE_RECIPE" --adapter extension --target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/runner-smoke" --json
|
|
438
438
|
) > "$ARTIFACTS/logs/runner-smoke.log" 2>&1; then
|
|
439
439
|
checks+=("{\"name\":\"runner v1 smoke\",\"status\":\"pass\"}")
|
|
440
440
|
else
|
|
@@ -75,8 +75,7 @@ cp "$RUNNER_DIR/library/manifests/mobile.action-manifest.json" "$HARNESS_DIR/run
|
|
|
75
75
|
cp "$RUNNER_DIR/library/manifests/extension.action-manifest.json" "$HARNESS_DIR/runner/manifests/extension.action-manifest.json"
|
|
76
76
|
rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
|
|
77
77
|
cp "$RUNNER_DIR/library/library.json" "$HARNESS_DIR/runner/library.json"
|
|
78
|
-
|
|
79
|
-
rsync -a --delete "$RUNNER_DIR/library/flows/" "$HARNESS_DIR/runner/flows/"
|
|
78
|
+
rm -rf "$HARNESS_DIR/runner/flows"
|
|
80
79
|
cp "$RUNNER_DIR/adapters/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
|
|
81
80
|
cp "$RUNNER_DIR/adapters/shared/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
|
|
82
81
|
cp "$RUNNER_DIR/adapters/shared/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
|
|
@@ -96,18 +95,20 @@ if [ "$GIT_EXCLUDE" = true ]; then
|
|
|
96
95
|
fi
|
|
97
96
|
|
|
98
97
|
RUNNER_REV="$(git -C "$RUNNER_DIR" rev-parse HEAD 2>/dev/null || echo unknown)"
|
|
98
|
+
SOURCE_FINGERPRINT="$(node "$RUNNER_DIR/adapters/shared/harness-source-fingerprint.mjs" "$RUNNER_DIR" mobile)"
|
|
99
99
|
CLEANUP_Q="$(printf '%q' "$SCRIPT_DIR/cleanup.sh")"
|
|
100
100
|
TARGET_CLEANUP_Q="$(printf '%q' "$TARGET")"
|
|
101
101
|
CLEANUP_COMMAND="RECIPE_HARNESS_ROOT=$(printf '%q' "$HARNESS_ROOT") $CLEANUP_Q --target $TARGET_CLEANUP_Q"
|
|
102
|
-
node - "$HARNESS_DIR/manifest.json" "$TARGET_REV" "$RUNNER_DIR" "$RUNNER_REV" "$TARGET" "$HARNESS_REL" "$HARNESS_ROOT" "$CLEANUP_COMMAND" <<'NODE'
|
|
102
|
+
node - "$HARNESS_DIR/manifest.json" "$TARGET_REV" "$RUNNER_DIR" "$RUNNER_REV" "$SOURCE_FINGERPRINT" "$TARGET" "$HARNESS_REL" "$HARNESS_ROOT" "$CLEANUP_COMMAND" <<'NODE'
|
|
103
103
|
const fs = require('node:fs');
|
|
104
|
-
const [manifestPath, targetRevision, runnerDir, runnerRevision, target, harnessRel, harnessRoot, cleanupCommand] = process.argv.slice(2);
|
|
104
|
+
const [manifestPath, targetRevision, runnerDir, runnerRevision, sourceFingerprint, target, harnessRel, harnessRoot, cleanupCommand] = process.argv.slice(2);
|
|
105
105
|
fs.writeFileSync(manifestPath, JSON.stringify({
|
|
106
106
|
adapter: 'mobile',
|
|
107
107
|
installMode: 'product-owned',
|
|
108
108
|
targetRevision,
|
|
109
109
|
installedAt: new Date().toISOString(),
|
|
110
110
|
source: { runnerDir, runnerRevision },
|
|
111
|
+
sourceFingerprint,
|
|
111
112
|
target,
|
|
112
113
|
protocolVersion: 'v1',
|
|
113
114
|
actionManifestPath: `${harnessRel}/action-manifest.json`,
|
|
@@ -305,16 +305,14 @@ live_status_ok() {
|
|
|
305
305
|
local device_target="$2"
|
|
306
306
|
cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
|
|
307
307
|
{
|
|
308
|
-
"
|
|
308
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
309
309
|
"title": "Mobile runner status smoke",
|
|
310
310
|
"description": "Checks that the installed runner can read Mobile app status before live verification.",
|
|
311
|
-
"
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
"done": { "action": "end", "status": "pass" }
|
|
317
|
-
}
|
|
311
|
+
"workflow": {
|
|
312
|
+
"entry": "status",
|
|
313
|
+
"nodes": {
|
|
314
|
+
"status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
|
|
315
|
+
"done": { "action": "end", "status": "pass" }
|
|
318
316
|
}
|
|
319
317
|
}
|
|
320
318
|
}
|
|
@@ -323,7 +321,7 @@ JSON
|
|
|
323
321
|
cd "$TARGET"
|
|
324
322
|
device_args=()
|
|
325
323
|
[ -n "$device_target" ] && device_args=(--device "$device_target")
|
|
326
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --
|
|
324
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
|
|
327
325
|
) > "$log_path" 2>&1
|
|
328
326
|
}
|
|
329
327
|
|
|
@@ -389,17 +387,15 @@ if [ "$STATIC_ONLY" = false ]; then
|
|
|
389
387
|
|
|
390
388
|
cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
|
|
391
389
|
{
|
|
392
|
-
"
|
|
390
|
+
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
393
391
|
"title": "Mobile v1 runner live bridge smoke",
|
|
394
392
|
"description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
|
|
395
|
-
"
|
|
396
|
-
"
|
|
397
|
-
|
|
398
|
-
"
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"done": { "action": "end", "status": "pass" }
|
|
402
|
-
}
|
|
393
|
+
"workflow": {
|
|
394
|
+
"entry": "status",
|
|
395
|
+
"nodes": {
|
|
396
|
+
"status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
|
|
397
|
+
"cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 15000, "next": "done" },
|
|
398
|
+
"done": { "action": "end", "status": "pass" }
|
|
403
399
|
}
|
|
404
400
|
}
|
|
405
401
|
}
|
|
@@ -415,7 +411,7 @@ JSON
|
|
|
415
411
|
IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
|
|
416
412
|
ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
|
|
417
413
|
ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
|
|
418
|
-
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --
|
|
414
|
+
"$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
|
|
419
415
|
) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
|
|
420
416
|
checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
|
|
421
417
|
else
|
|
@@ -12,27 +12,29 @@
|
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import crypto from 'node:crypto';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
export function harnessSourceFingerprint(runnerDir, adapter = 'extension') {
|
|
18
|
+
if (adapter !== 'extension' && adapter !== 'mobile') {
|
|
19
|
+
throw new Error(`Unsupported harness fingerprint adapter: ${adapter}`);
|
|
20
|
+
}
|
|
21
|
+
const sourceDirs = [`adapters/${adapter}`, 'adapters/shared', 'library'];
|
|
21
22
|
const entries = [];
|
|
22
|
-
for (const rel of
|
|
23
|
+
for (const rel of sourceDirs) {
|
|
23
24
|
collect(path.join(runnerDir, rel), runnerDir, entries);
|
|
24
25
|
}
|
|
25
26
|
entries.sort();
|
|
26
27
|
return crypto.createHash('sha256').update(entries.join('\n')).digest('hex');
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
31
|
+
process.stdout.write(`${harnessSourceFingerprint(path.resolve(process.argv[2] ?? '.'), process.argv[3])}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
function collect(dir, root, out) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} catch {
|
|
34
|
-
return; // absent tree contributes nothing
|
|
35
|
-
}
|
|
35
|
+
// These trees are required runner inputs. Fail closed instead of producing a
|
|
36
|
+
// weaker fingerprint that could make a stale injected harness appear current.
|
|
37
|
+
const dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
36
38
|
for (const dirent of dirents) {
|
|
37
39
|
// Skip hidden noise (.DS_Store, editor swap files) so the fingerprint tracks
|
|
38
40
|
// only real source and does not thrash re-injects.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { recipeHarnessPath, runnerDir } from "../paths.js";
|
|
6
|
+
async function ensureHarnessFresh(target, adapter) {
|
|
7
|
+
if (!fs.existsSync(path.join(target, "package.json")) || !hasCheckoutHead(target)) return;
|
|
8
|
+
let current;
|
|
9
|
+
try {
|
|
10
|
+
const mod = await import(pathToFileURL(path.join(runnerDir, "adapters/shared/harness-source-fingerprint.mjs")).href);
|
|
11
|
+
current = mod.harnessSourceFingerprint(runnerDir, adapter);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
14
|
+
throw new Error(`could not fingerprint the ${adapter} harness source${detail}`);
|
|
15
|
+
}
|
|
16
|
+
const manifestPath = recipeHarnessPath(target, adapter, "manifest.json");
|
|
17
|
+
const installed = readInstalledFingerprint(manifestPath);
|
|
18
|
+
if (installed === current) return;
|
|
19
|
+
const reason = installed === null ? "harness not installed" : "runner changed";
|
|
20
|
+
const injector = path.join(runnerDir, `adapters/${adapter}/inject.${adapter === "extension" ? "mjs" : "sh"}`);
|
|
21
|
+
try {
|
|
22
|
+
execFileSync(adapter === "extension" ? process.execPath : "bash", [injector, "--target", target], {
|
|
23
|
+
stdio: ["ignore", "ignore", "inherit"]
|
|
24
|
+
});
|
|
25
|
+
console.error(`\u2192 refreshed ${adapter} harness overlay (${reason})`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const detail = error instanceof Error ? `: ${error.message}` : "";
|
|
28
|
+
throw new Error(`could not refresh the ${adapter} harness overlay (${reason})${detail}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function hasCheckoutHead(target) {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync("git", ["-C", target, "rev-parse", "--verify", "HEAD"], { stdio: "ignore" });
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function readInstalledFingerprint(manifestPath) {
|
|
40
|
+
try {
|
|
41
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
42
|
+
return typeof manifest.sourceFingerprint === "string" ? manifest.sourceFingerprint : null;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
ensureHarnessFresh
|
|
49
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,6 @@ import { handleRun } from "./commands/run.js";
|
|
|
11
11
|
import { handleSelfTest } from "./commands/self-test.js";
|
|
12
12
|
import { handleCall } from "./commands/call.js";
|
|
13
13
|
import { handleStop } from "./commands/stop.js";
|
|
14
|
-
import { handleFlows } from "./commands/flows.js";
|
|
15
14
|
import { handleCompletionCandidates, invalidateCompletionCache } from "./commands/completion-candidates.js";
|
|
16
15
|
import { handleLaunch } from "./commands/launch/index.js";
|
|
17
16
|
import { handleLogs } from "./commands/logs.js";
|
|
@@ -53,8 +52,6 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
|
|
|
53
52
|
PROVE \u2014 run recipes and inspect capabilities:
|
|
54
53
|
run Run a recipe and write evidence (summary/trace/artifacts).
|
|
55
54
|
mm-harness run recipe.json
|
|
56
|
-
flows List/promote recipe library flows (personal/team win over canonical).
|
|
57
|
-
mm-harness flows list
|
|
58
55
|
doctor Readiness check for a checkout (no app launch).
|
|
59
56
|
mm-harness doctor
|
|
60
57
|
check Run bounded repo-local checks for an active diff.
|
|
@@ -92,7 +89,6 @@ async function main(argv) {
|
|
|
92
89
|
if (command === "status" || command === "health" || command === "home") {
|
|
93
90
|
return handleStatus(parseArgs(argv.slice(1), "status"));
|
|
94
91
|
}
|
|
95
|
-
if (command === "flows") return handleFlows(argv.slice(1));
|
|
96
92
|
if (command === "call") return handleCall(argv.slice(1));
|
|
97
93
|
if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
|
|
98
94
|
if (command === "provision") return handleProvision(parseArgs(argv.slice(1), command));
|
package/dist/command-contract.js
CHANGED
|
@@ -5,7 +5,7 @@ const options = (...groups) => Object.assign({}, ...groups);
|
|
|
5
5
|
const HELP = { "--help": bool(), "-h": bool() };
|
|
6
6
|
const JSON = { "--json": bool() };
|
|
7
7
|
const JSON_STREAM = { "--json-stream": bool() };
|
|
8
|
-
const TARGET = { "--target": value()
|
|
8
|
+
const TARGET = { "--target": value() };
|
|
9
9
|
const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
|
|
10
10
|
const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
|
|
11
11
|
const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "ios", "android"]) };
|
|
@@ -25,9 +25,8 @@ const RECIPE_RUNTIME = {
|
|
|
25
25
|
"--validation-runtime-dir": value(),
|
|
26
26
|
"--launch-existing-dist": bool(),
|
|
27
27
|
"--record-video": optionalValue(["full-run", "off"]),
|
|
28
|
-
"--record": bool(),
|
|
29
28
|
"--source-trust": value(["trusted", "untrusted", "unknown"]),
|
|
30
|
-
"--source-kind": value(["bundled", "operator", "task", "recipe-file", "
|
|
29
|
+
"--source-kind": value(["bundled", "operator", "task", "recipe-file", "library", "custom-adapter"]),
|
|
31
30
|
"--source-name": value(),
|
|
32
31
|
"--source-digest": value(),
|
|
33
32
|
"--approve-plan": value()
|
|
@@ -46,6 +45,10 @@ const RUNWAY = {
|
|
|
46
45
|
"--force": bool(),
|
|
47
46
|
"--resolve-only": bool()
|
|
48
47
|
};
|
|
48
|
+
const REMOVED_OPTION_REPLACEMENTS = {
|
|
49
|
+
"--project-root": "--target",
|
|
50
|
+
"--record": "--record-video"
|
|
51
|
+
};
|
|
49
52
|
const PUBLIC_COMMAND_CONTRACTS = {
|
|
50
53
|
status: {
|
|
51
54
|
aliases: ["health", "home"],
|
|
@@ -100,7 +103,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
100
103
|
})
|
|
101
104
|
},
|
|
102
105
|
call: {
|
|
103
|
-
options: options(HELP, JSON, TARGET, ADAPTER,
|
|
106
|
+
options: options(HELP, JSON, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
|
|
104
107
|
"--list": bool(),
|
|
105
108
|
"--arg": value()
|
|
106
109
|
}),
|
|
@@ -111,23 +114,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
111
114
|
leadingPositionals: 1,
|
|
112
115
|
variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
|
|
113
116
|
},
|
|
114
|
-
flows: {
|
|
115
|
-
options: options(HELP, JSON, TARGET, {
|
|
116
|
-
"--library": value(),
|
|
117
|
-
"--from": value(),
|
|
118
|
-
"--flow": value(),
|
|
119
|
-
"--to": value(),
|
|
120
|
-
"--domain": value(),
|
|
121
|
-
"--run": value(),
|
|
122
|
-
"--force": bool()
|
|
123
|
-
}),
|
|
124
|
-
positionals: [
|
|
125
|
-
{ label: "action", choices: ["list", "describe", "promote"] },
|
|
126
|
-
{ label: "ref" }
|
|
127
|
-
]
|
|
128
|
-
},
|
|
129
117
|
run: {
|
|
130
|
-
options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER,
|
|
118
|
+
options: options(HELP, JSON, JSON_STREAM, TARGET, ADAPTER, MOBILE_PLATFORM, DEVICE, RUNTIME_PORTS, RECIPE_RUNTIME, {
|
|
131
119
|
"--list": bool(),
|
|
132
120
|
"--plan": bool(),
|
|
133
121
|
"--describe": bool()
|
|
@@ -135,7 +123,8 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
135
123
|
positionals: [{ label: "recipe" }],
|
|
136
124
|
minimumPositionals: 1,
|
|
137
125
|
requiredUnless: ["--list"],
|
|
138
|
-
noPositionalsWith: ["--list"]
|
|
126
|
+
noPositionalsWith: ["--list"],
|
|
127
|
+
variadic: { label: "key=value", pattern: /^[^=\s]+=.*/u }
|
|
139
128
|
},
|
|
140
129
|
last: {
|
|
141
130
|
options: options(HELP, JSON, TARGET, {
|
|
@@ -238,7 +227,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
238
227
|
"--action-manifest": value()
|
|
239
228
|
}),
|
|
240
229
|
positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
|
|
241
|
-
minimumPositionals:
|
|
230
|
+
minimumPositionals: 0
|
|
242
231
|
}
|
|
243
232
|
};
|
|
244
233
|
function validatePublicInvocation(argv, examples = {}) {
|
|
@@ -285,11 +274,11 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
285
274
|
const inlineValue = equals === -1 ? void 0 : argument.slice(equals + 1);
|
|
286
275
|
const spec = contract.options[optionName];
|
|
287
276
|
if (!spec) {
|
|
288
|
-
const suggestion = closest(optionName, validOptions);
|
|
277
|
+
const suggestion = REMOVED_OPTION_REPLACEMENTS[optionName] ?? closest(optionName, validOptions);
|
|
289
278
|
return {
|
|
290
279
|
code: "CLI_UNKNOWN_OPTION",
|
|
291
280
|
command: name,
|
|
292
|
-
message: `unknown option '${optionName}'
|
|
281
|
+
message: `unknown option '${optionName}'.`,
|
|
293
282
|
userAction: actionFor(optionName, suggestion, exampleFor(name, examples))
|
|
294
283
|
};
|
|
295
284
|
}
|
|
@@ -312,7 +301,7 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
312
301
|
return {
|
|
313
302
|
code: "CLI_INVALID_OPTION_VALUE",
|
|
314
303
|
command: name,
|
|
315
|
-
message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'
|
|
304
|
+
message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'.`,
|
|
316
305
|
userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
|
|
317
306
|
};
|
|
318
307
|
}
|
|
@@ -320,7 +309,7 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
320
309
|
if (seenOptions.has("--help") || seenOptions.has("-h")) return null;
|
|
321
310
|
if (contract.leadingPositionals && !contract.requiredUnless?.some((option) => seenOptions.has(option)) && tokens.slice(0, contract.leadingPositionals).some((argument) => !argument || argument.startsWith("-"))) {
|
|
322
311
|
const missing = contract.positionals?.[0]?.label ?? "argument";
|
|
323
|
-
return
|
|
312
|
+
return missingPositionalFailure(name, `${name} requires <${missing}> first.`, contract, examples);
|
|
324
313
|
}
|
|
325
314
|
const forbiddingOption = contract.noPositionalsWith?.find((option) => seenOptions.has(option));
|
|
326
315
|
if (forbiddingOption && positionals.length > 0) {
|
|
@@ -338,7 +327,7 @@ function validatePublicInvocation(argv, examples = {}) {
|
|
|
338
327
|
const missing = missingSpec?.label ?? "argument";
|
|
339
328
|
const valid = missingSpec?.choices?.length ? ` Choose one: ${missingSpec.choices.join("|")}.` : "";
|
|
340
329
|
const usage = contract.usage ? ` Usage: ${contract.usage}.` : "";
|
|
341
|
-
return
|
|
330
|
+
return missingPositionalFailure(name, `missing required <${missing}>.${valid}${usage}`, contract, examples);
|
|
342
331
|
}
|
|
343
332
|
const fixed = contract.positionals ?? [];
|
|
344
333
|
for (let index = 0; index < Math.min(positionals.length, fixed.length); index += 1) {
|
|
@@ -376,21 +365,44 @@ function resolveCommand(token) {
|
|
|
376
365
|
}
|
|
377
366
|
return null;
|
|
378
367
|
}
|
|
379
|
-
function usageFailure(code, command, message,
|
|
368
|
+
function usageFailure(code, command, message, _contract, examples) {
|
|
380
369
|
return {
|
|
381
370
|
code,
|
|
382
371
|
command,
|
|
383
|
-
message
|
|
372
|
+
message,
|
|
384
373
|
userAction: `Try: ${exampleFor(command, examples)}. Inspect options: mm-harness ${command} --help`
|
|
385
374
|
};
|
|
386
375
|
}
|
|
387
|
-
function
|
|
376
|
+
function missingPositionalFailure(command, message, contract, examples) {
|
|
377
|
+
if (command === "run") {
|
|
378
|
+
return {
|
|
379
|
+
code: "CLI_MISSING_POSITIONAL",
|
|
380
|
+
command,
|
|
381
|
+
message,
|
|
382
|
+
userAction: `List recipes: mm-harness run --list
|
|
383
|
+
Example: ${exampleFor(command, examples)}
|
|
384
|
+
More: mm-harness run --help`
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (command === "call") {
|
|
388
|
+
return {
|
|
389
|
+
code: "CLI_MISSING_POSITIONAL",
|
|
390
|
+
command,
|
|
391
|
+
message,
|
|
392
|
+
userAction: `List actions: mm-harness call --list
|
|
393
|
+
Example: ${exampleFor(command, examples)}
|
|
394
|
+
More: mm-harness call --help`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return usageFailure("CLI_MISSING_POSITIONAL", command, message, contract, examples);
|
|
398
|
+
}
|
|
399
|
+
function positionalFailure(command, received, spec, _contract, examples) {
|
|
388
400
|
const valid = spec.validDescription ?? (spec.choices ? spec.choices.join(", ") : spec.pattern || spec.validate ? `a valid <${spec.label}>` : `<${spec.label}>`);
|
|
389
401
|
const suggestion = spec.choices ? closest(received, spec.choices) : void 0;
|
|
390
402
|
return {
|
|
391
403
|
code: "CLI_INVALID_POSITIONAL",
|
|
392
404
|
command,
|
|
393
|
-
message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}
|
|
405
|
+
message: `invalid <${spec.label}> '${received}'. Valid values: ${valid}.`,
|
|
394
406
|
userAction: actionFor(received, suggestion, exampleFor(command, examples))
|
|
395
407
|
};
|
|
396
408
|
}
|
|
@@ -400,13 +412,6 @@ function invalidPositional(value2, spec) {
|
|
|
400
412
|
if (spec.validate && !spec.validate(value2)) return true;
|
|
401
413
|
return Boolean(spec.pattern && !spec.pattern.test(value2));
|
|
402
414
|
}
|
|
403
|
-
function formatOptions(contract) {
|
|
404
|
-
return Object.entries(contract.options).map(([name, spec]) => {
|
|
405
|
-
if (spec.kind === "boolean") return name;
|
|
406
|
-
const valueLabel = spec.choices?.join("|") ?? "value";
|
|
407
|
-
return spec.kind === "optional-value" ? `${name}[=${valueLabel}]` : `${name} <${valueLabel}>`;
|
|
408
|
-
}).join(", ");
|
|
409
|
-
}
|
|
410
415
|
function formatChoiceList(choices) {
|
|
411
416
|
if (choices.length === 1) return choices[0] ?? "";
|
|
412
417
|
if (choices.length === 2) return `${choices[0]} or ${choices[1]}`;
|
|
@@ -432,19 +437,25 @@ function closest(input, candidates) {
|
|
|
432
437
|
return best.distance <= threshold ? best.candidate : void 0;
|
|
433
438
|
}
|
|
434
439
|
function levenshtein(left, right) {
|
|
435
|
-
|
|
440
|
+
let twoBack;
|
|
441
|
+
let prior = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
436
442
|
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
437
443
|
const current = [leftIndex];
|
|
438
444
|
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
439
445
|
const substitution = prior[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
440
446
|
current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
|
|
447
|
+
if (twoBack && leftIndex > 1 && rightIndex > 1 && left[leftIndex - 1] === right[rightIndex - 2] && left[leftIndex - 2] === right[rightIndex - 1]) {
|
|
448
|
+
current[rightIndex] = Math.min(current[rightIndex] ?? 0, (twoBack[rightIndex - 2] ?? 0) + 1);
|
|
449
|
+
}
|
|
441
450
|
}
|
|
442
|
-
|
|
451
|
+
twoBack = prior;
|
|
452
|
+
prior = current;
|
|
443
453
|
}
|
|
444
454
|
return prior[right.length] ?? right.length;
|
|
445
455
|
}
|
|
446
456
|
export {
|
|
447
457
|
PUBLIC_COMMAND_CONTRACTS,
|
|
458
|
+
closest,
|
|
448
459
|
publicCommandNames,
|
|
449
460
|
publicCommandTokens,
|
|
450
461
|
validatePublicInvocation
|