@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,44 @@
|
|
|
1
|
+
// debug — open the debug console by composing the porcelain: extension `debug`
|
|
2
|
+
// (--worker → `debug worker`); mobile `debug` (--dev-menu → `dev-menu`).
|
|
3
|
+
// Cross-platform flag misuse teaches (exit 2).
|
|
4
|
+
|
|
5
|
+
import { EXIT, flag, parseFlags, resolveAdapter, spawnPorcelain, targetOf, usageOut } from './shared.ts';
|
|
6
|
+
|
|
7
|
+
const DEBUG_BOOLEANS = new Set(['worker', 'devMenu', 'json']);
|
|
8
|
+
|
|
9
|
+
export async function handleDebug(argv: string[]): Promise<number> {
|
|
10
|
+
const { options } = parseFlags(argv, DEBUG_BOOLEANS);
|
|
11
|
+
const json = flag(options, 'json');
|
|
12
|
+
const target = targetOf(options);
|
|
13
|
+
const adapter = resolveAdapter(options, target);
|
|
14
|
+
|
|
15
|
+
if (!adapter) return usageOut(json, 'debug', `could not detect the MetaMask repo type for ${target}`);
|
|
16
|
+
if (adapter === 'core') return usageOut(json, 'debug', 'core is headless; there is no debug console.');
|
|
17
|
+
|
|
18
|
+
const worker = flag(options, 'worker');
|
|
19
|
+
const devMenu = flag(options, 'devMenu');
|
|
20
|
+
if (adapter === 'mobile' && worker) {
|
|
21
|
+
return usageOut(json, 'debug', '--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.');
|
|
22
|
+
}
|
|
23
|
+
if (adapter === 'extension' && devMenu) {
|
|
24
|
+
return usageOut(json, 'debug', '--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let args: string[];
|
|
28
|
+
if (adapter === 'extension') {
|
|
29
|
+
args = worker ? ['debug', 'worker'] : ['debug'];
|
|
30
|
+
} else {
|
|
31
|
+
args = devMenu ? ['dev-menu'] : ['debug'];
|
|
32
|
+
}
|
|
33
|
+
const result = spawnPorcelain(adapter, args, target, json);
|
|
34
|
+
if (json) {
|
|
35
|
+
console.log(
|
|
36
|
+
JSON.stringify(
|
|
37
|
+
{ schemaVersion: 1, command: 'debug', adapter, mode: worker ? 'worker' : devMenu ? 'dev-menu' : 'default', exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
38
|
+
null,
|
|
39
|
+
2,
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
44
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// fixtures <sync|set> — manage wallet DATA (never overlay/runtime state). `sync`
|
|
2
|
+
// re-renders the wallet fixture files; `set` applies THE canonical fixture (SRP/
|
|
3
|
+
// password read FROM the fixture, never typed) and always teaches the edit path.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { walletFixturePath } from '../paths.ts';
|
|
9
|
+
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
10
|
+
import { EXIT, flag, parseFlags, resolveAdapter, spawnPorcelain, str, targetOf, usageOut } from './shared.ts';
|
|
11
|
+
|
|
12
|
+
// One node injected from cli.ts so `fixtures set` can reuse the real engine path
|
|
13
|
+
// (`call`'s one-node machinery) without a circular import.
|
|
14
|
+
export interface CommandDeps {
|
|
15
|
+
runOneNode: (
|
|
16
|
+
adapter: MetaMaskRecipeAdapter,
|
|
17
|
+
action: string,
|
|
18
|
+
args: Record<string, string>,
|
|
19
|
+
target: string,
|
|
20
|
+
actionManifest: string | undefined,
|
|
21
|
+
) => Promise<{ status: 'pass' | 'fail' }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIXTURES_BOOLEANS = new Set(['json']);
|
|
25
|
+
|
|
26
|
+
export async function handleFixtures(argv: string[], deps: CommandDeps): Promise<number> {
|
|
27
|
+
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
28
|
+
const json = flag(options, 'json');
|
|
29
|
+
const sub = positional[0];
|
|
30
|
+
if (sub !== 'sync' && sub !== 'set') {
|
|
31
|
+
return usageOut(json, 'fixtures', 'fixtures requires a subcommand: mm-harness fixtures <sync|set>');
|
|
32
|
+
}
|
|
33
|
+
const target = targetOf(options);
|
|
34
|
+
const adapter = resolveAdapter(options, target);
|
|
35
|
+
if (!adapter) return usageOut(json, 'fixtures', `could not detect the MetaMask repo type for ${target}`);
|
|
36
|
+
if (adapter === 'core') return usageOut(json, 'fixtures', 'core is headless; it has no wallet fixture.');
|
|
37
|
+
|
|
38
|
+
const canonicalFixture = walletFixturePath(target);
|
|
39
|
+
|
|
40
|
+
if (sub === 'sync') {
|
|
41
|
+
// Re-render the wallet fixture files onto the checkout (wallet DATA only).
|
|
42
|
+
const result = spawnPorcelain(adapter, ['sync'], target, json);
|
|
43
|
+
if (json) {
|
|
44
|
+
console.log(
|
|
45
|
+
JSON.stringify(
|
|
46
|
+
{ schemaVersion: 1, command: 'fixtures', action: 'sync', adapter, fixture: canonicalFixture, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// set — apply THE canonical wallet fixture (password read FROM the fixture).
|
|
56
|
+
// Env-gap resolution (docs/CLI-SPEC.md Part 4): flag > env RECIPE_WALLET_FIXTURE
|
|
57
|
+
// > the slot's canonical fixture.
|
|
58
|
+
const fixturePath = path.resolve(str(options, 'fixture') ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
59
|
+
let status: 'pass' | 'fail';
|
|
60
|
+
if (adapter === 'mobile') {
|
|
61
|
+
// Grounded porcelain: mm-recipe setup-wallet [<fixture>].
|
|
62
|
+
const result = spawnPorcelain('mobile', ['setup-wallet', fixturePath], target, json);
|
|
63
|
+
status = result.status === 0 ? 'pass' : 'fail';
|
|
64
|
+
} else {
|
|
65
|
+
// Extension has no standalone porcelain set arm — reuse call's one-node
|
|
66
|
+
// machinery via the real engine path (metamask.wallet.setup in the manifest).
|
|
67
|
+
if (!fs.existsSync(fixturePath)) {
|
|
68
|
+
return usageOut(
|
|
69
|
+
json,
|
|
70
|
+
'fixtures',
|
|
71
|
+
`no wallet fixture at ${fixturePath}.\n Create it (or pass --fixture <path>), then re-run: mm-harness fixtures set`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
75
|
+
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
76
|
+
try {
|
|
77
|
+
const result = await deps.runOneNode('extension', 'metamask.wallet.setup', {}, target, str(options, 'actionManifest'));
|
|
78
|
+
status = result.status;
|
|
79
|
+
} finally {
|
|
80
|
+
if (previousFixtureEnv === undefined) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
81
|
+
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Always teach where to customize the wallet.
|
|
86
|
+
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
|
|
87
|
+
if (json) {
|
|
88
|
+
console.log(
|
|
89
|
+
JSON.stringify(
|
|
90
|
+
{ schemaVersion: 1, command: 'fixtures', action: 'set', adapter, fixture: fixturePath, canonicalFixture, status, exitCode: status === 'pass' ? EXIT.ok : EXIT.runtime, message: teaching },
|
|
91
|
+
null,
|
|
92
|
+
2,
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
console.error(teaching);
|
|
97
|
+
}
|
|
98
|
+
return status === 'pass' ? EXIT.ok : EXIT.runtime;
|
|
99
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// launch — start the app (Metro/build + surface), auto-ensure the runtime overlay,
|
|
2
|
+
// and heal transport within bounds. Owns launch grammar, build-tier selection,
|
|
3
|
+
// quick-launch change detection, and the --json contract. Device orchestration is
|
|
4
|
+
// not rewritten: each tier composes the same porcelain the standalone bins expose,
|
|
5
|
+
// and --verify reuses the overlay `live` sequence via handleHarness.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
|
|
10
|
+
import { handleHarness } from '../harness.ts';
|
|
11
|
+
import { recipeRuntimePath } from '../paths.ts';
|
|
12
|
+
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
13
|
+
import {
|
|
14
|
+
EXIT,
|
|
15
|
+
flag,
|
|
16
|
+
parseFlags,
|
|
17
|
+
resolveAdapter,
|
|
18
|
+
spawnPorcelain,
|
|
19
|
+
str,
|
|
20
|
+
targetOf,
|
|
21
|
+
usageOut,
|
|
22
|
+
} from './shared.ts';
|
|
23
|
+
import type { DeviceAdapter } from './shared.ts';
|
|
24
|
+
import {
|
|
25
|
+
RECOVERY_CODE,
|
|
26
|
+
checkHealBounds,
|
|
27
|
+
ensureOverlay,
|
|
28
|
+
newHealState,
|
|
29
|
+
parseHeal,
|
|
30
|
+
} from '../heal-bounds.ts';
|
|
31
|
+
import type { HealState } from '../heal-bounds.ts';
|
|
32
|
+
|
|
33
|
+
interface LaunchFailure {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
recoverable: boolean;
|
|
37
|
+
userAction?: string;
|
|
38
|
+
exitCode: number;
|
|
39
|
+
// The original porcelain output, surfaced verbatim (never replaced by the note).
|
|
40
|
+
originalError?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const LAUNCH_BOOLEANS = new Set([
|
|
44
|
+
'build',
|
|
45
|
+
'watch',
|
|
46
|
+
'verify',
|
|
47
|
+
'sidepanel',
|
|
48
|
+
'fullscreen',
|
|
49
|
+
'runway',
|
|
50
|
+
'json',
|
|
51
|
+
'jsonStream',
|
|
52
|
+
'yes',
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export async function handleLaunch(argv: string[]): Promise<number> {
|
|
56
|
+
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
57
|
+
const json = flag(options, 'json');
|
|
58
|
+
const target = targetOf(options);
|
|
59
|
+
|
|
60
|
+
const heal = parseHeal(options, 'auto');
|
|
61
|
+
if (typeof heal !== 'string') return usageOut(json, 'launch', heal.error);
|
|
62
|
+
|
|
63
|
+
// Grammar: mobile requires an ios|android positional (also disambiguates the
|
|
64
|
+
// adapter); extension takes no positional; core is headless (teaching error).
|
|
65
|
+
const posToken = positional[0];
|
|
66
|
+
const mobileTargetToken = posToken === 'ios' || posToken === 'android' ? posToken : undefined;
|
|
67
|
+
const platformFlag = str(options, 'platform');
|
|
68
|
+
const platformMobileToken =
|
|
69
|
+
platformFlag === 'ios' || platformFlag === 'android' ? platformFlag : undefined;
|
|
70
|
+
const mobileTarget = mobileTargetToken ?? platformMobileToken;
|
|
71
|
+
const adapterHint: MetaMaskRecipeAdapter | undefined = mobileTarget ? 'mobile' : undefined;
|
|
72
|
+
const adapter = resolveAdapter(options, target, adapterHint);
|
|
73
|
+
|
|
74
|
+
if (!adapter) {
|
|
75
|
+
return usageOut(
|
|
76
|
+
json,
|
|
77
|
+
'launch',
|
|
78
|
+
`could not detect the MetaMask repo type for ${target}\n Next: pass --adapter mobile|extension, or run inside a checkout.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (adapter === 'core') {
|
|
82
|
+
return usageOut(json, 'launch', 'core is headless; there is nothing to launch. Use: mm-harness verify');
|
|
83
|
+
}
|
|
84
|
+
if (adapter === 'mobile' && !mobileTarget) {
|
|
85
|
+
return usageOut(
|
|
86
|
+
json,
|
|
87
|
+
'launch',
|
|
88
|
+
'target is required for mobile.\n Run: mm-harness launch ios or mm-harness launch android',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const wantBuild = flag(options, 'build');
|
|
93
|
+
const wantVerify = flag(options, 'verify');
|
|
94
|
+
const wantWatch = flag(options, 'watch');
|
|
95
|
+
const wantRunway = flag(options, 'runway');
|
|
96
|
+
|
|
97
|
+
// --runway is mobile-only.
|
|
98
|
+
if (wantRunway && adapter !== 'mobile') {
|
|
99
|
+
return usageOut(json, 'launch', 'runway is mobile-only. Drop --runway for the extension.');
|
|
100
|
+
}
|
|
101
|
+
// --fullscreen is the explicit override when both display flags are passed.
|
|
102
|
+
const displayMode =
|
|
103
|
+
flag(options, 'sidepanel') && !flag(options, 'fullscreen') ? 'sidepanel' : 'fullscreen';
|
|
104
|
+
|
|
105
|
+
// Env-gap flags (docs/CLI-SPEC.md Part 4): --device / --cdp-port / --watcher-port.
|
|
106
|
+
// Resolution is flag > env > config/default: setting the env from the flag makes
|
|
107
|
+
// the flag win; an absent flag leaves the agent/CI env untouched; the porcelain
|
|
108
|
+
// owns config/default. Ports must be numeric (teaching error otherwise).
|
|
109
|
+
for (const portFlag of ['cdpPort', 'watcherPort'] as const) {
|
|
110
|
+
const value = str(options, portFlag);
|
|
111
|
+
if (value !== undefined && !/^\d+$/u.test(value)) {
|
|
112
|
+
return usageOut(json, 'launch', `--${portFlag === 'cdpPort' ? 'cdp-port' : 'watcher-port'} must be numeric (got: ${value}).`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
applyLaunchEnvOverrides(options, adapter, mobileTarget);
|
|
116
|
+
|
|
117
|
+
const tier = wantVerify ? 'verify' : wantBuild ? 'build' : 'quick';
|
|
118
|
+
const state = newHealState();
|
|
119
|
+
|
|
120
|
+
// Quick-launch change detection: refuse a quick relaunch on a stale native
|
|
121
|
+
// build (native inputs changed since the recorded build baseline).
|
|
122
|
+
if (tier === 'quick' && nativeInputsChanged(target, adapter)) {
|
|
123
|
+
return usageOut(
|
|
124
|
+
json,
|
|
125
|
+
'launch',
|
|
126
|
+
`native build inputs changed since last build — run: mm-harness launch ${
|
|
127
|
+
adapter === 'mobile' ? `${mobileTarget} ` : ''
|
|
128
|
+
}--build`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Auto-ensure overlay (install phase).
|
|
133
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
134
|
+
if (!ensured.ok) {
|
|
135
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
136
|
+
code: 'OVERLAY_INSTALL_FAILED',
|
|
137
|
+
message: ensured.error ?? 'overlay install failed',
|
|
138
|
+
recoverable: false,
|
|
139
|
+
exitCode: EXIT.infra,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --verify absorbs the retired `live` sequence: overlay ensured above → launch
|
|
144
|
+
// → CDP/bridge poll → smoke verify, all via the existing overlay orchestration.
|
|
145
|
+
if (tier === 'verify') {
|
|
146
|
+
const liveArgs = ['live', '--adapter', adapter, '--target', target];
|
|
147
|
+
if (adapter === 'mobile' && mobileTarget) liveArgs.push('--platform', mobileTarget);
|
|
148
|
+
if (json) liveArgs.push('--json');
|
|
149
|
+
const code = await handleHarness(liveArgs);
|
|
150
|
+
// handleHarness owns its own summary in --json mode; the overlay mutations we
|
|
151
|
+
// performed are surfaced to stderr in human mode above.
|
|
152
|
+
return code;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Compose the launch tier via the porcelain (same path the porcelain commands use).
|
|
156
|
+
const launchArgs = launchComposition(adapter, mobileTarget, tier, wantWatch);
|
|
157
|
+
let attempt = spawnPorcelain(adapter, launchArgs, target, json);
|
|
158
|
+
|
|
159
|
+
if (attempt.status === 0) {
|
|
160
|
+
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Bounds are ALWAYS enforced regardless of --heal.
|
|
164
|
+
if (heal === 'off') {
|
|
165
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
166
|
+
code: 'LAUNCH_FAILED',
|
|
167
|
+
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed (healing off)`,
|
|
168
|
+
recoverable: false,
|
|
169
|
+
exitCode: EXIT.infra,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// Healing bounds (shared module — policy, not per-command). null = ok to proceed.
|
|
173
|
+
const bound = checkHealBounds(target, attempt.output, state);
|
|
174
|
+
if (bound !== null) {
|
|
175
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
176
|
+
code: bound.code,
|
|
177
|
+
message: bound.message,
|
|
178
|
+
recoverable: false,
|
|
179
|
+
userAction: bound.userAction,
|
|
180
|
+
exitCode: bound.exitCode,
|
|
181
|
+
originalError: bound.originalError,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Infra recovery: attempt the SAME launch exactly once (the porcelain restarts
|
|
186
|
+
// Metro / reopens Chrome). Push recoveryCode BEFORE the attempt so any
|
|
187
|
+
// subsequent checkHealBounds call sees it and refuses a second loop.
|
|
188
|
+
const recoveryCode = RECOVERY_CODE[adapter];
|
|
189
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
190
|
+
attempt = spawnPorcelain(adapter, launchArgs, target, json);
|
|
191
|
+
if (attempt.status === 0) {
|
|
192
|
+
state.recovered.push(recoveryCode);
|
|
193
|
+
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
194
|
+
}
|
|
195
|
+
// Second attempt failed — same-recovery-twice bound hit.
|
|
196
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
197
|
+
code: 'SAME_RECOVERY_TWICE',
|
|
198
|
+
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed again after ${recoveryCode} recovery — refusing to loop.`,
|
|
199
|
+
recoverable: false,
|
|
200
|
+
exitCode: EXIT.bounded,
|
|
201
|
+
originalError: attempt.output.trim() || undefined,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Map the env-gap flags onto the env vars the porcelain reads. Only the canonical
|
|
206
|
+
// var plus its documented aliases are set, and only when the flag is present, so
|
|
207
|
+
// flag > env > config/default holds. --device targets the simulator (ios) or the
|
|
208
|
+
// adb serial (android); ports are set with their runtime aliases.
|
|
209
|
+
function applyLaunchEnvOverrides(
|
|
210
|
+
options: Record<string, string | boolean>,
|
|
211
|
+
adapter: DeviceAdapter,
|
|
212
|
+
mobileTarget: string | undefined,
|
|
213
|
+
): void {
|
|
214
|
+
const device = str(options, 'device');
|
|
215
|
+
if (device && adapter === 'mobile') {
|
|
216
|
+
if (mobileTarget === 'android') {
|
|
217
|
+
process.env.ADB_SERIAL = device;
|
|
218
|
+
process.env.ANDROID_SERIAL = device;
|
|
219
|
+
process.env.ANDROID_DEVICE = device;
|
|
220
|
+
} else {
|
|
221
|
+
process.env.IOS_SIMULATOR = device;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const cdpPort = str(options, 'cdpPort');
|
|
225
|
+
if (cdpPort) {
|
|
226
|
+
process.env.CDP_PORT = cdpPort;
|
|
227
|
+
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
228
|
+
}
|
|
229
|
+
const watcherPort = str(options, 'watcherPort');
|
|
230
|
+
if (watcherPort) {
|
|
231
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
232
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
233
|
+
process.env.METRO_PORT = watcherPort;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Native build inputs changed vs the recorded build baseline (git diff heuristic,
|
|
238
|
+
// mirrors the farm-layer native-diff). No baseline recorded → cannot be stale →
|
|
239
|
+
// allow the quick launch.
|
|
240
|
+
function nativeInputsChanged(target: string, adapter: MetaMaskRecipeAdapter): boolean {
|
|
241
|
+
const baselineFile = recipeRuntimePath(target, '.last-build-ref');
|
|
242
|
+
let baseline: string;
|
|
243
|
+
try {
|
|
244
|
+
baseline = fs.readFileSync(baselineFile, 'utf8').trim();
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
if (!baseline) return false;
|
|
249
|
+
const paths =
|
|
250
|
+
adapter === 'mobile'
|
|
251
|
+
? ['ios', 'android', 'package.json', 'yarn.lock']
|
|
252
|
+
: ['package.json', 'yarn.lock', 'webpack.config.js'];
|
|
253
|
+
try {
|
|
254
|
+
const diff = execFileSync('git', ['-C', target, 'diff', '--name-only', baseline, '--', ...paths], {
|
|
255
|
+
encoding: 'utf8',
|
|
256
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
257
|
+
}).trim();
|
|
258
|
+
return diff.length > 0;
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Map (adapter, tier) → the grounded porcelain sub-command.
|
|
265
|
+
// mobile quick : mm-recipe <ios|android> (quick relaunch)
|
|
266
|
+
// mobile build : mm-recipe up (decision-aware full build)
|
|
267
|
+
// extension quick: mme-recipe rebuild (snapshot dist + reopen)
|
|
268
|
+
// extension build: mme-recipe build (webpack build)
|
|
269
|
+
// extension watch: mme-recipe watch (webpack watch)
|
|
270
|
+
function launchComposition(
|
|
271
|
+
adapter: DeviceAdapter,
|
|
272
|
+
mobileTarget: string | undefined,
|
|
273
|
+
tier: 'quick' | 'build' | 'verify',
|
|
274
|
+
wantWatch: boolean,
|
|
275
|
+
): string[] {
|
|
276
|
+
if (adapter === 'mobile') {
|
|
277
|
+
if (tier === 'build') return ['up'];
|
|
278
|
+
return [mobileTarget ?? 'ios'];
|
|
279
|
+
}
|
|
280
|
+
// extension
|
|
281
|
+
if (wantWatch) return ['watch'];
|
|
282
|
+
if (tier === 'build') return ['build'];
|
|
283
|
+
return ['rebuild'];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Apply the extension display mode AFTER the base launch succeeded, then emit the
|
|
287
|
+
// pass summary. Fullscreen = the plain rebuild/build/watch launch above (the
|
|
288
|
+
// default popup/home window). Sidepanel additionally drives the chrome.sidePanel
|
|
289
|
+
// mechanism via the porcelain: `mme-recipe sidepanel open`
|
|
290
|
+
// (orchestration/porcelain/mme-recipe → orchestration/extension/sidepanel-toggle.sh `open`), so
|
|
291
|
+
// --sidepanel and --fullscreen compose DIFFERENT porcelain calls and the display
|
|
292
|
+
// mode actually reaches orchestration. Mobile has no side panel, so the flag is a
|
|
293
|
+
// no-op there.
|
|
294
|
+
function finishLaunch(
|
|
295
|
+
json: boolean,
|
|
296
|
+
adapter: DeviceAdapter,
|
|
297
|
+
mobileTarget: string | undefined,
|
|
298
|
+
tier: string,
|
|
299
|
+
displayMode: string,
|
|
300
|
+
target: string,
|
|
301
|
+
state: HealState,
|
|
302
|
+
): number {
|
|
303
|
+
if (adapter === 'extension' && displayMode === 'sidepanel') {
|
|
304
|
+
const sidepanel = spawnPorcelain('extension', ['sidepanel', 'open'], target, json);
|
|
305
|
+
if (sidepanel.status !== 0) {
|
|
306
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
307
|
+
code: 'SIDEPANEL_OPEN_FAILED',
|
|
308
|
+
message: 'app launched but opening the side panel failed.',
|
|
309
|
+
recoverable: false,
|
|
310
|
+
exitCode: EXIT.runtime,
|
|
311
|
+
originalError: sidepanel.output.trim() || undefined,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return launchPass(json, adapter, mobileTarget, tier, displayMode, state);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function launchPass(
|
|
319
|
+
json: boolean,
|
|
320
|
+
adapter: DeviceAdapter,
|
|
321
|
+
mobileTarget: string | undefined,
|
|
322
|
+
tier: string,
|
|
323
|
+
displayMode: string,
|
|
324
|
+
state: HealState,
|
|
325
|
+
): number {
|
|
326
|
+
if (json) {
|
|
327
|
+
console.log(
|
|
328
|
+
JSON.stringify(
|
|
329
|
+
{
|
|
330
|
+
schemaVersion: 1,
|
|
331
|
+
command: 'launch',
|
|
332
|
+
adapter,
|
|
333
|
+
platform: mobileTarget ?? (adapter === 'extension' ? displayMode : null),
|
|
334
|
+
tier,
|
|
335
|
+
status: 'pass',
|
|
336
|
+
phase: 'launch',
|
|
337
|
+
recovered: state.recovered,
|
|
338
|
+
mutations: state.mutations,
|
|
339
|
+
exitCode: EXIT.ok,
|
|
340
|
+
},
|
|
341
|
+
null,
|
|
342
|
+
2,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
console.error(`✓ launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} (${tier}) ready`);
|
|
347
|
+
}
|
|
348
|
+
return EXIT.ok;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function launchFail(
|
|
352
|
+
json: boolean,
|
|
353
|
+
adapter: DeviceAdapter,
|
|
354
|
+
mobileTarget: string | undefined,
|
|
355
|
+
tier: string,
|
|
356
|
+
state: HealState,
|
|
357
|
+
failure: LaunchFailure,
|
|
358
|
+
): number {
|
|
359
|
+
if (json) {
|
|
360
|
+
console.log(
|
|
361
|
+
JSON.stringify(
|
|
362
|
+
{
|
|
363
|
+
schemaVersion: 1,
|
|
364
|
+
command: 'launch',
|
|
365
|
+
adapter,
|
|
366
|
+
platform: mobileTarget ?? null,
|
|
367
|
+
tier,
|
|
368
|
+
status: 'fail',
|
|
369
|
+
phase: state.attemptedRecoveries.length ? 'recover' : 'launch',
|
|
370
|
+
recovered: state.recovered,
|
|
371
|
+
mutations: state.mutations,
|
|
372
|
+
recoverable: failure.recoverable,
|
|
373
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
374
|
+
exitCode: failure.exitCode,
|
|
375
|
+
error: {
|
|
376
|
+
code: failure.code,
|
|
377
|
+
message: failure.message,
|
|
378
|
+
retryable: failure.recoverable,
|
|
379
|
+
userAction: failure.userAction ?? null,
|
|
380
|
+
// The classification note is in `message`; the REAL failure text lives
|
|
381
|
+
// here verbatim so it is never lost.
|
|
382
|
+
originalError: failure.originalError ?? null,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
null,
|
|
386
|
+
2,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
} else {
|
|
390
|
+
console.error(
|
|
391
|
+
`✗ launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed\n ${failure.message}` +
|
|
392
|
+
(failure.originalError ? `\n --- original failure ---\n${failure.originalError}` : '') +
|
|
393
|
+
(failure.userAction ? `\n Next: ${failure.userAction}` : ''),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
return failure.exitCode;
|
|
397
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// logs — tail Metro/webpack + app logs by composing the porcelain `logs` (+
|
|
2
|
+
// --full). Teaches (exit 2) when nothing is running for the checkout.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { recipeRuntimePath } from '../paths.ts';
|
|
8
|
+
import { EXIT, flag, parseFlags, resolveAdapter, spawnPorcelain, str, targetOf, usageOut } from './shared.ts';
|
|
9
|
+
|
|
10
|
+
const LOGS_BOOLEANS = new Set(['full', 'json']);
|
|
11
|
+
|
|
12
|
+
export async function handleLogs(argv: string[]): Promise<number> {
|
|
13
|
+
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
14
|
+
const json = flag(options, 'json');
|
|
15
|
+
const target = targetOf(options);
|
|
16
|
+
const adapter = resolveAdapter(options, target);
|
|
17
|
+
|
|
18
|
+
if (!adapter) return usageOut(json, 'logs', `could not detect the MetaMask repo type for ${target}`);
|
|
19
|
+
if (adapter === 'core') return usageOut(json, 'logs', 'core is headless; there are no app/Metro logs.');
|
|
20
|
+
|
|
21
|
+
const source = str(options, 'source') ?? 'metro';
|
|
22
|
+
if (source !== 'metro' && source !== 'app') {
|
|
23
|
+
return usageOut(json, 'logs', '--source must be metro or app.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
|
|
27
|
+
// the porcelain reads from RECIPE_LOG_EVENTS. Resolution is flag > env > default:
|
|
28
|
+
// the flag wins by setting the env; an absent flag leaves the agent/CI env alone.
|
|
29
|
+
const events = str(options, 'events');
|
|
30
|
+
if (events !== undefined) {
|
|
31
|
+
if (!/^\d+$/u.test(events)) return usageOut(json, 'logs', `--events must be numeric (got: ${events}).`);
|
|
32
|
+
process.env.RECIPE_LOG_EVENTS = events;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Nothing running → teaching error pointing at launch. The log file is the
|
|
36
|
+
// signal that Metro/webpack has been started for this checkout.
|
|
37
|
+
const logFile = recipeRuntimePath(target, adapter === 'mobile' ? 'metro.log' : 'webpack.log');
|
|
38
|
+
if (!fs.existsSync(logFile)) {
|
|
39
|
+
const launchHint = adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness launch';
|
|
40
|
+
return usageOut(
|
|
41
|
+
json,
|
|
42
|
+
'logs',
|
|
43
|
+
`nothing running for this checkout (no ${path.basename(logFile)}).\n Start it first: ${launchHint}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const args = ['logs'];
|
|
48
|
+
if (flag(options, 'full')) args.push('--full');
|
|
49
|
+
const result = spawnPorcelain(adapter, args, target, json);
|
|
50
|
+
if (json) {
|
|
51
|
+
console.log(
|
|
52
|
+
JSON.stringify(
|
|
53
|
+
{ schemaVersion: 1, command: 'logs', adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
60
|
+
}
|