@deeeed/metamask-harness 0.2.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 +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli-version.mjs — detect stale installed harness scripts vs the active runner.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
|
|
10
|
+
function readJson(file) {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readPackageVersion(dir) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
21
|
+
return pkg.version || 'unknown';
|
|
22
|
+
} catch {
|
|
23
|
+
return 'unknown';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function gitHead(dir) {
|
|
28
|
+
const result = spawnSync('git', ['-C', dir, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
29
|
+
return result.status === 0 ? result.stdout.trim() : 'unknown';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function checkHarness({ runnerRoot, manifestPath, installedShaPath }) {
|
|
33
|
+
const manifest = readJson(manifestPath);
|
|
34
|
+
const reasons = [];
|
|
35
|
+
if (!manifest) {
|
|
36
|
+
return { stale: false, reasons: ['no harness manifest'] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentRevision = gitHead(runnerRoot);
|
|
40
|
+
const currentVersion = readPackageVersion(runnerRoot);
|
|
41
|
+
const installedRevision = manifest.source?.runnerRevision || null;
|
|
42
|
+
const installedVersion = manifest.runnerPackageVersion || null;
|
|
43
|
+
|
|
44
|
+
if (installedRevision && installedRevision !== 'unknown' && installedRevision !== currentRevision) {
|
|
45
|
+
reasons.push(`runner revision ${installedRevision.slice(0, 8)} != ${currentRevision.slice(0, 8)}`);
|
|
46
|
+
}
|
|
47
|
+
if (installedVersion && installedVersion !== currentVersion) {
|
|
48
|
+
reasons.push(`runner package ${installedVersion} != ${currentVersion}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
stale: reasons.length > 0,
|
|
53
|
+
reasons,
|
|
54
|
+
installedRevision,
|
|
55
|
+
currentRevision,
|
|
56
|
+
installedVersion,
|
|
57
|
+
currentVersion,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function usage() {
|
|
62
|
+
console.error(`Usage:
|
|
63
|
+
cli-version.mjs check --runner-root <dir> --manifest <path> [--installed-sha <path>] [--json]`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
68
|
+
const { values } = parseArgs({
|
|
69
|
+
args: rest,
|
|
70
|
+
allowPositionals: false,
|
|
71
|
+
options: {
|
|
72
|
+
'runner-root': { type: 'string' },
|
|
73
|
+
manifest: { type: 'string' },
|
|
74
|
+
'installed-sha': { type: 'string' },
|
|
75
|
+
json: { type: 'boolean', default: false },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!command || command === '--help' || command === '-h') {
|
|
80
|
+
usage();
|
|
81
|
+
process.exit(command ? 0 : 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (command !== 'check') {
|
|
85
|
+
usage();
|
|
86
|
+
process.exit(2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const runnerRoot = values['runner-root'];
|
|
90
|
+
const manifestPath = values.manifest;
|
|
91
|
+
if (!runnerRoot || !manifestPath) {
|
|
92
|
+
usage();
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const payload = {
|
|
97
|
+
...checkHarness({
|
|
98
|
+
runnerRoot,
|
|
99
|
+
manifestPath,
|
|
100
|
+
installedShaPath: values['installed-sha'],
|
|
101
|
+
}),
|
|
102
|
+
manifest: manifestPath,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
if (values.json) {
|
|
106
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (payload.stale) {
|
|
111
|
+
process.stderr.write(`recipe: harness stale (${payload.reasons.join('; ')})\n`);
|
|
112
|
+
process.stderr.write('recipe: run: recipe sync\n');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
process.stdout.write('harness up to date\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (import.meta.url === new URL(process.argv[1], 'file:').href) {
|
|
119
|
+
main().catch((error) => {
|
|
120
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ensure-runner-deps.sh — idempotent install of runner npm dependencies.
|
|
3
|
+
#
|
|
4
|
+
# Source checkout runs TypeScript via Node/tsx and imports @farmslot/recipe-harness.
|
|
5
|
+
# Cloned runner checkouts under product temp/ often have no node_modules; parent
|
|
6
|
+
# product repos may ship a .yarnrc.yml that the runner's pinned Yarn rejects, so
|
|
7
|
+
# use npm here (not yarn install).
|
|
8
|
+
#
|
|
9
|
+
# Usage: ensure-runner-deps.sh [RUNNER_DIR]
|
|
10
|
+
# Env: FARMSLOT_ROOT / METAMASK_RUNNER_PROTOCOL_ROOT — optional local link target
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
RUNNER_DIR="${1:-}"
|
|
14
|
+
if [ -z "$RUNNER_DIR" ]; then
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
RUNNER_DIR="$(cd "$SCRIPT_DIR/../.." && pwd -P)"
|
|
17
|
+
fi
|
|
18
|
+
RUNNER_DIR="$(cd "$RUNNER_DIR" && pwd -P)"
|
|
19
|
+
|
|
20
|
+
deps_ready() {
|
|
21
|
+
[ -f "$RUNNER_DIR/node_modules/@farmslot/recipe-harness/package.json" ]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if deps_ready; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ ! -f "$RUNNER_DIR/package.json" ]; then
|
|
29
|
+
echo "mm-harness: runner package.json missing: $RUNNER_DIR" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
PROTOCOL_ROOT="${METAMASK_RUNNER_PROTOCOL_ROOT:-${METAMASK_RUNNER_FARMSLOT_ROOT:-${FARMSLOT_ROOT:-}}}"
|
|
34
|
+
if [ -z "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/.farmslot-root" ]; then
|
|
35
|
+
PROTOCOL_ROOT="$(cat "$RUNNER_DIR/.farmslot-root")"
|
|
36
|
+
fi
|
|
37
|
+
if [ -n "$PROTOCOL_ROOT" ] && [ -f "$RUNNER_DIR/scripts/link-local-farmslot.mjs" ]; then
|
|
38
|
+
if [ -f "$PROTOCOL_ROOT/packages/recipe-harness/package.json" ]; then
|
|
39
|
+
FARMSLOT_ROOT="$(cd "$PROTOCOL_ROOT" && pwd -P)" \
|
|
40
|
+
node "$RUNNER_DIR/scripts/link-local-farmslot.mjs" >&2 || true
|
|
41
|
+
if deps_ready; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "mm-harness: installing runner dependencies via npm in $RUNNER_DIR" >&2
|
|
48
|
+
(
|
|
49
|
+
cd "$RUNNER_DIR"
|
|
50
|
+
npm install --ignore-scripts >&2
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if ! deps_ready; then
|
|
54
|
+
echo "mm-harness: runner dependencies still missing after npm install: $RUNNER_DIR" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# harness-path.sh — shared, configurable recipe-harness injection root.
|
|
2
|
+
# Sourced (not executed) by both adapters and wrappers so every host uses one
|
|
3
|
+
# definition. Override RECIPE_HARNESS_ROOT (relative to the target repo);
|
|
4
|
+
# defaults to the path in path-defaults.json (under the gitignored temp/, so installs
|
|
5
|
+
# need no extra git-exclude).
|
|
6
|
+
#
|
|
7
|
+
# An empty/unset value falls back to the default; a set value is validated
|
|
8
|
+
# (relative, safe charset, no '.'/'..' components) so a hostile/typo'd value
|
|
9
|
+
# can't make install/cleanup write or rm -rf outside the target, and is safe to
|
|
10
|
+
# embed in shell/JSON without quoting surprises.
|
|
11
|
+
# Returns non-zero on an invalid value; callers run under `set -e`.
|
|
12
|
+
_recipe_path_defaults_file="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/path-defaults.json"
|
|
13
|
+
|
|
14
|
+
_recipe_path_default() {
|
|
15
|
+
local key="$1"
|
|
16
|
+
sed -n 's/.*"'"$key"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_recipe_path_defaults_file" | head -1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
harness_root() {
|
|
20
|
+
local root="${RECIPE_HARNESS_ROOT:-$(_recipe_path_default recipeHarnessRoot)}"
|
|
21
|
+
case "$root" in
|
|
22
|
+
""|/*) echo "RECIPE_HARNESS_ROOT must be a non-empty relative path: '$root'" >&2; return 1 ;;
|
|
23
|
+
*[!A-Za-z0-9._/-]*) echo "RECIPE_HARNESS_ROOT may only contain A-Za-z0-9 and . _ / - : '$root'" >&2; return 1 ;;
|
|
24
|
+
esac
|
|
25
|
+
local IFS=/ part
|
|
26
|
+
for part in $root; do
|
|
27
|
+
case "$part" in
|
|
28
|
+
.|..) echo "RECIPE_HARNESS_ROOT must not contain '.' or '..' path components: '$root'" >&2; return 1 ;;
|
|
29
|
+
esac
|
|
30
|
+
done
|
|
31
|
+
printf '%s' "$root"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# harness_dir <target> [adapter] -> absolute install dir for the adapter.
|
|
35
|
+
harness_dir() {
|
|
36
|
+
printf '%s/%s/%s' "$1" "$(harness_root)" "${2:-extension}"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# recipe_runtime_dir -> relative runtime state dir for recipe-local config/secrets.
|
|
41
|
+
# Override RECIPE_RUNTIME_DIR (relative to target repo); default lives in path-defaults.json.
|
|
42
|
+
recipe_runtime_dir() {
|
|
43
|
+
local root="${RECIPE_RUNTIME_DIR:-$(_recipe_path_default recipeRuntimeDir)}"
|
|
44
|
+
case "$root" in
|
|
45
|
+
""|/*) echo "RECIPE_RUNTIME_DIR must be a non-empty relative path: '$root'" >&2; return 1 ;;
|
|
46
|
+
*[!A-Za-z0-9._/-]*) echo "RECIPE_RUNTIME_DIR may only contain A-Za-z0-9 and . _ / - : '$root'" >&2; return 1 ;;
|
|
47
|
+
esac
|
|
48
|
+
local IFS=/ part
|
|
49
|
+
for part in $root; do
|
|
50
|
+
case "$part" in
|
|
51
|
+
.|..) echo "RECIPE_RUNTIME_DIR must not contain '.' or '..' path components: '$root'" >&2; return 1 ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
printf '%s' "$root"
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
digest_file() {
|
|
4
|
+
local file="$1"
|
|
5
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
6
|
+
shasum -a 256 "$file" | awk '{print $1}'
|
|
7
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
8
|
+
sha256sum "$file" | awk '{print $1}'
|
|
9
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
10
|
+
openssl dgst -sha256 "$file" | awk '{print $NF}'
|
|
11
|
+
else
|
|
12
|
+
echo "Error: need shasum, sha256sum, or openssl for mobile harness hashing." >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
digest_stdin() {
|
|
18
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
19
|
+
shasum -a 256 | awk '{print $1}'
|
|
20
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
21
|
+
sha256sum | awk '{print $1}'
|
|
22
|
+
elif command -v openssl >/dev/null 2>&1; then
|
|
23
|
+
openssl dgst -sha256 | awk '{print $NF}'
|
|
24
|
+
else
|
|
25
|
+
echo "Error: need shasum, sha256sum, or openssl for mobile harness hashing." >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
hash_path() {
|
|
31
|
+
local rel="$1"
|
|
32
|
+
if [ ! -e "$TARGET/$rel" ]; then
|
|
33
|
+
printf 'MISSING'
|
|
34
|
+
elif [ -d "$TARGET/$rel" ]; then
|
|
35
|
+
(
|
|
36
|
+
cd "$TARGET"
|
|
37
|
+
find "$rel" -type f | LC_ALL=C sort | while IFS= read -r file; do
|
|
38
|
+
printf '%s %s\n' "$(digest_file "$file")" "$file"
|
|
39
|
+
done | digest_stdin
|
|
40
|
+
)
|
|
41
|
+
else
|
|
42
|
+
(cd "$TARGET" && digest_file "$rel")
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# json-field.sh — shared JSON-field reader for recipe-harness scripts.
|
|
2
|
+
# Sourced (not executed). read_runtime_context_field <json-path> <dotted.field>
|
|
3
|
+
# prints the value (empty string if absent); returns 1 if the file is missing.
|
|
4
|
+
read_runtime_context_field() {
|
|
5
|
+
local context_path="$1"
|
|
6
|
+
local field="$2"
|
|
7
|
+
[ -f "$context_path" ] || return 1
|
|
8
|
+
node -e '
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const [path, field] = process.argv.slice(-2);
|
|
11
|
+
try {
|
|
12
|
+
const data = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
13
|
+
const value = field.split(".").reduce((node, key) => {
|
|
14
|
+
if (node === undefined || node === null) return undefined;
|
|
15
|
+
return node[key];
|
|
16
|
+
}, data);
|
|
17
|
+
if (value !== undefined && value !== null && value !== "") process.stdout.write(String(value));
|
|
18
|
+
} catch (error) {
|
|
19
|
+
process.stderr.write(String(error && error.message ? error.message : error) + "\n");
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
' "$context_path" "$field"
|
|
23
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* log-tui.mjs — compact build-log UX: last N events + pointer to full log.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { color, colorLogEvent } from './cli-color.mjs';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_EVENTS = 10;
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
console.error(`Usage:
|
|
13
|
+
log-tui.mjs watch --log <path> [--events <n>] [--mode compact|full|quiet]
|
|
14
|
+
[--success-pattern <regex>] [--poll-ms <ms>] [--label <text>]
|
|
15
|
+
log-tui.mjs tail --log <path> [--events <n>] [--follow] [--mode compact|full]
|
|
16
|
+
log-tui.mjs events --log <path> [--events <n>] [--json]`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSignificantLine(line) {
|
|
20
|
+
const text = String(line || '').trim();
|
|
21
|
+
if (!text) return false;
|
|
22
|
+
if (/^<s> \[webpack\.Progress\]/u.test(text)) {
|
|
23
|
+
return /(9[0-9]%|100%|emitting|sealing after seal|done plugins)/u.test(text);
|
|
24
|
+
}
|
|
25
|
+
if (/^\[webpack\.Progress\]/u.test(text)) return false;
|
|
26
|
+
return (
|
|
27
|
+
/\[recipe-harness\]/u.test(text)
|
|
28
|
+
|| /\[fixture\]/u.test(text)
|
|
29
|
+
|| /MetaMask/u.test(text)
|
|
30
|
+
|| /compiled successfully/u.test(text)
|
|
31
|
+
|| /compiled with [0-9]+ warning/u.test(text)
|
|
32
|
+
|| /Module build failed/u.test(text)
|
|
33
|
+
|| /^ERROR in /u.test(text)
|
|
34
|
+
|| /Bundle end:/u.test(text)
|
|
35
|
+
|| /BUILD FAILED/u.test(text)
|
|
36
|
+
|| /webpack.*compiled/u.test(text)
|
|
37
|
+
|| /(iOS|Android).*[0-9]+%/u.test(text)
|
|
38
|
+
|| /Bundl(ed|ing)/u.test(text)
|
|
39
|
+
|| /Launching Extension/u.test(text)
|
|
40
|
+
|| /Extension harness/u.test(text)
|
|
41
|
+
|| /phase [0-9]\/[0-9]/u.test(text)
|
|
42
|
+
|| /snapshotting dist/u.test(text)
|
|
43
|
+
|| /runtime-dist snapshot/u.test(text)
|
|
44
|
+
|| /yarn start compiled/u.test(text)
|
|
45
|
+
|| /prepare pipeline/u.test(text)
|
|
46
|
+
|| /Extension harness launch pass/u.test(text)
|
|
47
|
+
|| /Extension harness verify pass/u.test(text)
|
|
48
|
+
|| /Extension live validation summary/u.test(text)
|
|
49
|
+
|| /PASS runtime-launch/u.test(text)
|
|
50
|
+
|| /dist-freshness:/u.test(text)
|
|
51
|
+
|| /build-health:/u.test(text)
|
|
52
|
+
|| /Fixture status:/u.test(text)
|
|
53
|
+
|| /recorded deps\/cache baseline/u.test(text)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatEvent(line) {
|
|
58
|
+
const text = String(line || '').trim().replace(/\s+/gu, ' ');
|
|
59
|
+
if (/^<s> \[webpack\.Progress\]/u.test(text)) {
|
|
60
|
+
const match = text.match(/\[webpack\.Progress\] (.*)$/u);
|
|
61
|
+
return match ? `webpack ${match[1]}` : text;
|
|
62
|
+
}
|
|
63
|
+
return text
|
|
64
|
+
.replace(/^<s> /u, '')
|
|
65
|
+
.replace(/^\[recipe-harness\] /u, '')
|
|
66
|
+
.replace(/^\[fixture\] /u, 'fixture: ')
|
|
67
|
+
.replace(/^[•*]\s+/u, '');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractEvents(text, limit) {
|
|
71
|
+
const events = [];
|
|
72
|
+
for (const raw of text.split(/\r?\n/u)) {
|
|
73
|
+
if (!isSignificantLine(raw)) continue;
|
|
74
|
+
const formatted = formatEvent(raw);
|
|
75
|
+
if (!formatted) continue;
|
|
76
|
+
if (events.length > 0 && events[events.length - 1] === formatted) continue;
|
|
77
|
+
events.push(formatted);
|
|
78
|
+
}
|
|
79
|
+
return events.slice(-limit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readNewContent(file, offset) {
|
|
83
|
+
if (!fs.existsSync(file)) return { text: '', offset: 0 };
|
|
84
|
+
const stat = fs.statSync(file);
|
|
85
|
+
if (stat.size < offset) offset = 0;
|
|
86
|
+
const fd = fs.openSync(file, 'r');
|
|
87
|
+
try {
|
|
88
|
+
const length = stat.size - offset;
|
|
89
|
+
if (length <= 0) return { text: '', offset };
|
|
90
|
+
const buffer = Buffer.alloc(length);
|
|
91
|
+
fs.readSync(fd, buffer, 0, length, offset);
|
|
92
|
+
return { text: buffer.toString('utf8'), offset: stat.size };
|
|
93
|
+
} finally {
|
|
94
|
+
fs.closeSync(fd);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderHeader(label, logPath, title) {
|
|
99
|
+
return [
|
|
100
|
+
`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
|
|
101
|
+
`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
|
|
102
|
+
`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: recipe logs --full', { stream: process.stderr })}`,
|
|
103
|
+
'',
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderEventLines(events, { stream = process.stderr } = {}) {
|
|
108
|
+
if (events.length === 0) {
|
|
109
|
+
return [` ${color('dim', '*', { stream })} ${color('comment', '(waiting for first build event...)', { stream })}`];
|
|
110
|
+
}
|
|
111
|
+
return events.map((event, index) => {
|
|
112
|
+
const latest = index === events.length - 1;
|
|
113
|
+
return ` ${color('dim', '*', { stream })} ${colorLogEvent(event, { latest, stream })}`;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderCompact({ label, logPath, events, quiet }) {
|
|
118
|
+
if (quiet) return;
|
|
119
|
+
const header = renderHeader(label, logPath, label);
|
|
120
|
+
const body = renderEventLines(events, { stream: process.stderr });
|
|
121
|
+
const block = [...header, ...body].join('\n');
|
|
122
|
+
if (process.stderr.isTTY) {
|
|
123
|
+
process.stderr.write(`\x1b[2J\x1b[H${block}\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
process.stderr.write(`${block}\n`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function renderTail({ logPath, events, follow }) {
|
|
130
|
+
const title = follow ? 'tailing last events' : 'recent build events';
|
|
131
|
+
const lines = [
|
|
132
|
+
...renderHeader('build', logPath, title),
|
|
133
|
+
...renderEventLines(events, { stream: process.stdout }),
|
|
134
|
+
'',
|
|
135
|
+
];
|
|
136
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function matchesPattern(text, pattern) {
|
|
140
|
+
if (!pattern) return false;
|
|
141
|
+
return new RegExp(pattern, 'iu').test(text);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function watchLog(options) {
|
|
145
|
+
const {
|
|
146
|
+
logPath,
|
|
147
|
+
eventsLimit,
|
|
148
|
+
mode,
|
|
149
|
+
successPattern,
|
|
150
|
+
pollMs,
|
|
151
|
+
label,
|
|
152
|
+
} = options;
|
|
153
|
+
|
|
154
|
+
if (mode === 'full') {
|
|
155
|
+
const { spawn } = await import('node:child_process');
|
|
156
|
+
const child = spawn('tail', ['-n', '20', '-F', logPath], { stdio: 'inherit' });
|
|
157
|
+
await new Promise((resolve) => child.on('exit', resolve));
|
|
158
|
+
return child.exitCode ?? 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let offset = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
|
|
162
|
+
let lastRendered = '';
|
|
163
|
+
const started = Date.now();
|
|
164
|
+
|
|
165
|
+
while (true) {
|
|
166
|
+
const chunk = readNewContent(logPath, offset);
|
|
167
|
+
offset = chunk.offset;
|
|
168
|
+
const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
169
|
+
const events = extractEvents(allText, eventsLimit);
|
|
170
|
+
|
|
171
|
+
if (matchesPattern(allText, 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error|FAIL runtime-launch|Extension harness launch fail')) {
|
|
172
|
+
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (matchesPattern(allText, successPattern)) {
|
|
176
|
+
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
177
|
+
if (mode !== 'quiet') {
|
|
178
|
+
const elapsed = Math.round((Date.now() - started) / 1000);
|
|
179
|
+
process.stderr.write(`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
|
|
180
|
+
}
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const snapshot = JSON.stringify(events);
|
|
185
|
+
if (snapshot !== lastRendered) {
|
|
186
|
+
renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
|
|
187
|
+
lastRendered = snapshot;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function tailLog(options) {
|
|
195
|
+
const { logPath, eventsLimit, follow, mode } = options;
|
|
196
|
+
if (mode === 'full' && follow) {
|
|
197
|
+
const { spawn } = await import('node:child_process');
|
|
198
|
+
const child = spawn('tail', ['-f', logPath], { stdio: 'inherit' });
|
|
199
|
+
await new Promise((resolve) => child.on('exit', resolve));
|
|
200
|
+
return child.exitCode ?? 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!follow) {
|
|
204
|
+
const text = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
205
|
+
renderTail({ logPath, events: extractEvents(text, eventsLimit), follow: false });
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let offset = fs.existsSync(logPath) ? fs.statSync(logPath).size : 0;
|
|
210
|
+
let lastRendered = '';
|
|
211
|
+
while (true) {
|
|
212
|
+
const chunk = readNewContent(logPath, offset);
|
|
213
|
+
offset = chunk.offset;
|
|
214
|
+
const allText = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
215
|
+
const events = extractEvents(allText, eventsLimit);
|
|
216
|
+
const snapshot = JSON.stringify(events);
|
|
217
|
+
if (snapshot !== lastRendered) {
|
|
218
|
+
renderTail({ logPath, events, follow: true });
|
|
219
|
+
lastRendered = snapshot;
|
|
220
|
+
}
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function main() {
|
|
226
|
+
const { positionals, values } = parseArgs({
|
|
227
|
+
allowPositionals: true,
|
|
228
|
+
args: process.argv.slice(2),
|
|
229
|
+
options: {
|
|
230
|
+
help: { type: 'boolean', short: 'h' },
|
|
231
|
+
log: { type: 'string' },
|
|
232
|
+
events: { type: 'string', default: String(DEFAULT_EVENTS) },
|
|
233
|
+
mode: { type: 'string', default: process.env.RECIPE_LOG_UI || 'compact' },
|
|
234
|
+
'success-pattern': {
|
|
235
|
+
type: 'string',
|
|
236
|
+
default: 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end:',
|
|
237
|
+
},
|
|
238
|
+
'poll-ms': { type: 'string', default: '2000' },
|
|
239
|
+
label: { type: 'string', default: 'build' },
|
|
240
|
+
follow: { type: 'boolean', default: false },
|
|
241
|
+
json: { type: 'boolean', default: false },
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const command = positionals[0];
|
|
246
|
+
if (values.help || command === '--help' || command === '-h') {
|
|
247
|
+
usage();
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
250
|
+
if (!command) {
|
|
251
|
+
usage();
|
|
252
|
+
process.exit(2);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const logPath = values.log;
|
|
256
|
+
if (!logPath) {
|
|
257
|
+
usage();
|
|
258
|
+
process.exit(2);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const eventsLimit = Number.parseInt(values.events, 10) || DEFAULT_EVENTS;
|
|
262
|
+
const pollMs = Number.parseInt(values['poll-ms'], 10) || 2000;
|
|
263
|
+
|
|
264
|
+
if (command === 'watch') {
|
|
265
|
+
const code = await watchLog({
|
|
266
|
+
logPath,
|
|
267
|
+
eventsLimit,
|
|
268
|
+
mode: values.mode,
|
|
269
|
+
successPattern: values['success-pattern'],
|
|
270
|
+
pollMs,
|
|
271
|
+
label: values.label,
|
|
272
|
+
});
|
|
273
|
+
process.exit(code);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (command === 'tail') {
|
|
277
|
+
const code = await tailLog({
|
|
278
|
+
logPath,
|
|
279
|
+
eventsLimit,
|
|
280
|
+
follow: values.follow,
|
|
281
|
+
mode: values.mode,
|
|
282
|
+
});
|
|
283
|
+
process.exit(code);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (command === 'events') {
|
|
287
|
+
const text = fs.existsSync(logPath) ? fs.readFileSync(logPath, 'utf8') : '';
|
|
288
|
+
const events = extractEvents(text, eventsLimit);
|
|
289
|
+
if (values.json) {
|
|
290
|
+
process.stdout.write(`${JSON.stringify({ log: logPath, events }, null, 2)}\n`);
|
|
291
|
+
} else {
|
|
292
|
+
renderTail({ logPath, events, follow: false });
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
usage();
|
|
298
|
+
process.exit(2);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
main().catch((error) => {
|
|
302
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
303
|
+
process.exit(1);
|
|
304
|
+
});
|