@deeeed/metamask-harness 0.2.0 → 0.3.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 +17 -0
- package/README.md +20 -17
- package/{orchestration → adapters}/core/cleanup.sh +2 -2
- package/{orchestration → adapters}/core/inject.sh +8 -8
- package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
- package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
- package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
- package/{orchestration → adapters}/extension/inject.mjs +27 -31
- package/{orchestration → adapters}/extension/launch.sh +4 -4
- package/{orchestration → adapters}/extension/live.sh +5 -5
- package/{orchestration → adapters}/extension/readiness.mjs +12 -2
- package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
- package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
- package/{orchestration → adapters}/extension/start-watch.sh +7 -7
- package/{runner → adapters}/extension/verify.sh +11 -11
- package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
- package/{orchestration → adapters}/manifest.json +96 -96
- package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
- package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
- package/{orchestration → adapters}/mobile/inject.sh +22 -24
- package/adapters/mobile/open-device.sh +250 -0
- package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
- package/adapters/mobile/prewarm-bundle.sh +95 -0
- package/adapters/mobile/start-metro.sh +228 -0
- package/{runner → adapters}/mobile/verify.sh +6 -6
- package/adapters/mobile/wait-for-bridge.sh +108 -0
- package/adapters/mobile/yarn-setup.sh +20 -0
- package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
- package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
- package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
- package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
- package/adapters/shared/sync-wallet-fixture.sh +78 -0
- package/bin/mm-harness +5 -6
- package/docs/CLI-SPEC.md +41 -2
- package/docs/architecture.md +69 -38
- package/docs/runtime-file-conventions.md +3 -3
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -2
- package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +1 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +2 -2
- package/library/library.json +1 -1
- package/package.json +8 -8
- package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
- package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
- package/src/adapters/mobile/prepare.ts +210 -0
- package/{orchestration → src/adapters}/mobile/runtime-decision.ts +11 -3
- package/{runner/src → src}/adapters.ts +3 -3
- package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
- package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
- package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
- package/{runner/src → src}/cli.ts +25 -18
- package/src/commands/debug.ts +61 -0
- package/src/commands/fixtures.ts +196 -0
- package/{runner/src → src}/commands/launch.ts +185 -41
- package/src/commands/logs.ts +86 -0
- package/{runner/src → src}/commands/shared.ts +26 -26
- package/{runner/src → src}/harness.ts +97 -10
- package/src/index.ts +15 -0
- package/{runner/src → src}/live-adapter-contract.ts +1 -1
- package/{runner/src → src}/mm-harness-cli.ts +19 -3
- package/{runner/src → src}/paths.ts +2 -2
- package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
- package/docs/extension-runtime-commands.md +0 -60
- package/docs/harness-cli.md +0 -43
- package/orchestration/compat-overlays/mobile/README.md +0 -13
- package/orchestration/doctor.mjs +0 -72
- package/orchestration/lib/cli-home.mjs +0 -354
- package/orchestration/lib/recipe-paths.mjs +0 -26
- package/orchestration/mobile/launch.sh +0 -137
- package/orchestration/mobile/live.sh +0 -125
- package/orchestration/porcelain/metamask-recipe +0 -99
- package/orchestration/porcelain/mm-recipe +0 -1591
- package/orchestration/porcelain/mme-recipe +0 -1181
- package/runner/src/commands/debug.ts +0 -44
- package/runner/src/commands/fixtures.ts +0 -99
- package/runner/src/commands/logs.ts +0 -60
- package/runner/src/index.ts +0 -6
- /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
- /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
- /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
- /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
- /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
- /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
- /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
- /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
- /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
- /package/{runner/src → src}/completions-cache.ts +0 -0
- /package/{runner/src → src}/doctor.ts +0 -0
- /package/{runner/src → src}/heal-bounds.ts +0 -0
- /package/{runner/src → src}/manifest.ts +0 -0
- /package/{runner/src → src}/recording-target.ts +0 -0
- /package/{runner/src → src}/run-recording.ts +0 -0
- /package/{runner/src → src}/runner.ts +0 -0
- /package/{runner/src → src}/types.ts +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
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 { runnerDir, walletFixturePath } from '../paths.ts';
|
|
9
|
+
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
10
|
+
import { EXIT, flag, parseFlags, resolveAdapter, spawnScript, 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
|
+
// Patterns indicating setup-wallet failed due to a Metro/bridge issue (not a bad
|
|
27
|
+
// fixture). A clean Metro restart + relaunch is likely to resolve these.
|
|
28
|
+
// Mirrors baseline mm-recipe:820-824 setup_wallet_failure_recoverable patterns.
|
|
29
|
+
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
30
|
+
'CDP not reachable',
|
|
31
|
+
'CDP bridge',
|
|
32
|
+
'is not installed',
|
|
33
|
+
'Engine.context.KeyringController not available',
|
|
34
|
+
'Engine does not exist',
|
|
35
|
+
"Cannot read property 'transactions'",
|
|
36
|
+
'bridge-not-ready',
|
|
37
|
+
'debug-target-missing',
|
|
38
|
+
'CDP eval failed',
|
|
39
|
+
'CDP eval-async failed',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function isRecoverableSetupWalletFailure(output: string): boolean {
|
|
43
|
+
return RECOVERABLE_SETUP_WALLET_PATTERNS.some((p) => output.includes(p));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleFixtures(argv: string[], deps: CommandDeps): Promise<number> {
|
|
47
|
+
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
48
|
+
const json = flag(options, 'json');
|
|
49
|
+
const sub = positional[0];
|
|
50
|
+
if (sub !== 'sync' && sub !== 'set') {
|
|
51
|
+
return usageOut(json, 'fixtures', 'fixtures requires a subcommand: mm-harness fixtures <sync|set>\n Next: mm-harness fixtures sync or mm-harness fixtures set');
|
|
52
|
+
}
|
|
53
|
+
const target = targetOf(options);
|
|
54
|
+
const adapter = resolveAdapter(options, target);
|
|
55
|
+
if (!adapter) {
|
|
56
|
+
return usageOut(
|
|
57
|
+
json,
|
|
58
|
+
'fixtures',
|
|
59
|
+
`could not detect the MetaMask repo type for ${target}\n Next: pass --target <checkout> or cd into the checkout, then re-run: mm-harness fixtures ${sub}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (adapter === 'core') return usageOut(json, 'fixtures', 'core is headless; it has no wallet fixture.\n Next: mm-harness run <recipe> # run recipes against the headless core');
|
|
63
|
+
|
|
64
|
+
const canonicalFixture = walletFixturePath(target);
|
|
65
|
+
|
|
66
|
+
if (sub === 'sync') {
|
|
67
|
+
const exitCode = fixturesSync(adapter, target, json);
|
|
68
|
+
if (json) {
|
|
69
|
+
console.log(
|
|
70
|
+
JSON.stringify(
|
|
71
|
+
{ schemaVersion: 1, command: 'fixtures', action: 'sync', adapter, fixture: canonicalFixture, exitCode },
|
|
72
|
+
null,
|
|
73
|
+
2,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return exitCode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// set — apply THE canonical wallet fixture (password read FROM the fixture).
|
|
81
|
+
// Env-gap resolution (docs/CLI-SPEC.md Part 4): flag > env RECIPE_WALLET_FIXTURE
|
|
82
|
+
// > the slot's canonical fixture.
|
|
83
|
+
const fixturePath = path.resolve(str(options, 'fixture') ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
84
|
+
let status: 'pass' | 'fail';
|
|
85
|
+
if (adapter === 'mobile') {
|
|
86
|
+
// Compose setup-wallet.sh directly (requires bridge to be live).
|
|
87
|
+
const setupWalletSh = path.join(runnerDir, 'adapters/mobile/bridge-runtime/setup-wallet.sh');
|
|
88
|
+
const previousAppRoot = process.env.APP_ROOT;
|
|
89
|
+
process.env.APP_ROOT = target;
|
|
90
|
+
try {
|
|
91
|
+
let result = spawnScript(setupWalletSh, ['--fixture', fixturePath], target, json);
|
|
92
|
+
// One clean-Metro auto-retry on recoverable failures (CDP unreachable, bridge
|
|
93
|
+
// not installed). RECIPE_SETUP_WALLET_RETRIED guards against infinite loops.
|
|
94
|
+
if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env['RECIPE_SETUP_WALLET_RETRIED']) {
|
|
95
|
+
process.env['RECIPE_SETUP_WALLET_RETRIED'] = '1';
|
|
96
|
+
if (!json) process.stderr.write(' setup-wallet: recoverable failure — restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n');
|
|
97
|
+
const { prepareMobile } = await import('../adapters/mobile/prepare.ts');
|
|
98
|
+
const platform = str(options, 'platform') ?? process.env['MOBILE_PLATFORM'] ?? 'ios';
|
|
99
|
+
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: 'auto', clearMetro: true });
|
|
100
|
+
if (relaunchResult.status === 0) {
|
|
101
|
+
result = spawnScript(setupWalletSh, ['--fixture', fixturePath], target, json);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
status = result.status === 0 ? 'pass' : 'fail';
|
|
105
|
+
} finally {
|
|
106
|
+
delete process.env['RECIPE_SETUP_WALLET_RETRIED'];
|
|
107
|
+
if (previousAppRoot === undefined) delete process.env.APP_ROOT;
|
|
108
|
+
else process.env.APP_ROOT = previousAppRoot;
|
|
109
|
+
}
|
|
110
|
+
if (!json && status === 'fail') {
|
|
111
|
+
console.error(' Next: mm-harness launch ios # relaunch the dev client, then retry: mm-harness fixtures set');
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// Extension has no standalone set arm — reuse call's one-node machinery via
|
|
115
|
+
// the real engine path (metamask.wallet.setup in the manifest).
|
|
116
|
+
if (!fs.existsSync(fixturePath)) {
|
|
117
|
+
return usageOut(
|
|
118
|
+
json,
|
|
119
|
+
'fixtures',
|
|
120
|
+
`no wallet fixture at ${fixturePath}.\n Next: create it (or pass --fixture <path>), then re-run: mm-harness fixtures set`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
124
|
+
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
125
|
+
try {
|
|
126
|
+
const result = await deps.runOneNode('extension', 'metamask.wallet.setup', {}, target, str(options, 'actionManifest'));
|
|
127
|
+
status = result.status;
|
|
128
|
+
} finally {
|
|
129
|
+
if (previousFixtureEnv === undefined) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
130
|
+
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
131
|
+
}
|
|
132
|
+
if (!json && status === 'fail') {
|
|
133
|
+
console.error(' Next: mm-harness launch --build # rebuild and relaunch, then retry: mm-harness fixtures set');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Always teach where to customize the wallet.
|
|
138
|
+
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
|
|
139
|
+
if (json) {
|
|
140
|
+
console.log(
|
|
141
|
+
JSON.stringify(
|
|
142
|
+
{
|
|
143
|
+
schemaVersion: 1,
|
|
144
|
+
command: 'fixtures',
|
|
145
|
+
action: 'set',
|
|
146
|
+
adapter,
|
|
147
|
+
fixture: fixturePath,
|
|
148
|
+
canonicalFixture,
|
|
149
|
+
status,
|
|
150
|
+
exitCode: status === 'pass' ? EXIT.ok : EXIT.runtime,
|
|
151
|
+
message: teaching,
|
|
152
|
+
error: status === 'fail' ? {
|
|
153
|
+
code: 'SETUP_WALLET_FAILED',
|
|
154
|
+
message: 'wallet fixture setup failed',
|
|
155
|
+
userAction: adapter === 'mobile'
|
|
156
|
+
? 'mm-harness launch ios # relaunch the dev client, then retry: mm-harness fixtures set'
|
|
157
|
+
: 'mm-harness launch --build # rebuild and relaunch, then retry: mm-harness fixtures set',
|
|
158
|
+
} : null,
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
console.error(teaching);
|
|
166
|
+
}
|
|
167
|
+
return status === 'pass' ? EXIT.ok : EXIT.runtime;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Sync the overlay installation + wallet fixture for the checkout.
|
|
171
|
+
// Re-runs the adapter inject to refresh patched files, then copies the wallet
|
|
172
|
+
// fixture to the canonical runtime location via sync-wallet-fixture.sh.
|
|
173
|
+
function fixturesSync(adapter: 'mobile' | 'extension', target: string, json: boolean): number {
|
|
174
|
+
const mmHarnessBin = path.join(runnerDir, 'bin/mm-harness');
|
|
175
|
+
const installResult = spawnScript(
|
|
176
|
+
mmHarnessBin,
|
|
177
|
+
['install', '--adapter', adapter, '--target', target],
|
|
178
|
+
target,
|
|
179
|
+
json,
|
|
180
|
+
);
|
|
181
|
+
if (installResult.status !== 0) {
|
|
182
|
+
if (!json) {
|
|
183
|
+
console.error(`✗ mm-harness fixtures sync: overlay reinstall failed.\n Next: mm-harness install --target ${target} # diagnose the install failure`);
|
|
184
|
+
}
|
|
185
|
+
return EXIT.runtime;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const syncFixtureSh = path.join(runnerDir, 'adapters/shared/sync-wallet-fixture.sh');
|
|
189
|
+
const syncArgs = ['--target', target];
|
|
190
|
+
if (process.env.CDP_PORT) syncArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
191
|
+
if (process.env.RECIPE_SLOT_ID) syncArgs.push('--slot-id', process.env.RECIPE_SLOT_ID);
|
|
192
|
+
spawnScript(syncFixtureSh, syncArgs, target, json);
|
|
193
|
+
// Non-zero from sync-wallet-fixture.sh is informational (no fixture source);
|
|
194
|
+
// the install succeeded so the overlay is fresh.
|
|
195
|
+
return EXIT.ok;
|
|
196
|
+
}
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
// launch — start the app (Metro/build + surface), auto-ensure the runtime overlay,
|
|
2
2
|
// and heal transport within bounds. Owns launch grammar, build-tier selection,
|
|
3
|
-
// quick-launch change detection, and the --json contract. Device orchestration
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// quick-launch change detection, and the --json contract. Device orchestration
|
|
4
|
+
// composes adapters/ scripts directly; --verify reuses the overlay `live`
|
|
5
|
+
// sequence via handleHarness.
|
|
6
6
|
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
9
10
|
|
|
10
11
|
import { handleHarness } from '../harness.ts';
|
|
11
|
-
import { recipeRuntimePath } from '../paths.ts';
|
|
12
|
+
import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
12
13
|
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
14
|
+
import { prepareMobile } from '../adapters/mobile/prepare.ts';
|
|
13
15
|
import {
|
|
14
16
|
EXIT,
|
|
15
17
|
flag,
|
|
16
18
|
parseFlags,
|
|
17
19
|
resolveAdapter,
|
|
18
|
-
|
|
20
|
+
spawnScript,
|
|
19
21
|
str,
|
|
20
22
|
targetOf,
|
|
21
23
|
usageOut,
|
|
22
24
|
} from './shared.ts';
|
|
23
|
-
import type { DeviceAdapter } from './shared.ts';
|
|
25
|
+
import type { DeviceAdapter, ScriptResult } from './shared.ts';
|
|
24
26
|
import {
|
|
25
27
|
RECOVERY_CODE,
|
|
26
28
|
checkHealBounds,
|
|
@@ -36,7 +38,7 @@ interface LaunchFailure {
|
|
|
36
38
|
recoverable: boolean;
|
|
37
39
|
userAction?: string;
|
|
38
40
|
exitCode: number;
|
|
39
|
-
// The original
|
|
41
|
+
// The original script output, surfaced verbatim (never replaced by the note).
|
|
40
42
|
originalError?: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -112,7 +114,7 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
112
114
|
return usageOut(json, 'launch', `--${portFlag === 'cdpPort' ? 'cdp-port' : 'watcher-port'} must be numeric (got: ${value}).`);
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
|
-
applyLaunchEnvOverrides(options, adapter, mobileTarget);
|
|
117
|
+
applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
116
118
|
|
|
117
119
|
const tier = wantVerify ? 'verify' : wantBuild ? 'build' : 'quick';
|
|
118
120
|
const state = newHealState();
|
|
@@ -152,9 +154,8 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
152
154
|
return code;
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
// Compose the launch tier
|
|
156
|
-
|
|
157
|
-
let attempt = spawnPorcelain(adapter, launchArgs, target, json);
|
|
157
|
+
// Compose the launch tier: mobile runs in-process; extension spawns adapters/ scripts.
|
|
158
|
+
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
158
159
|
|
|
159
160
|
if (attempt.status === 0) {
|
|
160
161
|
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
@@ -182,12 +183,12 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
182
183
|
});
|
|
183
184
|
}
|
|
184
185
|
|
|
185
|
-
// Infra recovery: attempt the SAME launch exactly once (
|
|
186
|
-
//
|
|
187
|
-
//
|
|
186
|
+
// Infra recovery: attempt the SAME launch exactly once (Metro restarts /
|
|
187
|
+
// Chrome reopens). Push recoveryCode BEFORE the attempt so any subsequent
|
|
188
|
+
// checkHealBounds call sees it and refuses a second loop.
|
|
188
189
|
const recoveryCode = RECOVERY_CODE[adapter];
|
|
189
190
|
state.attemptedRecoveries.push(recoveryCode);
|
|
190
|
-
attempt =
|
|
191
|
+
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
191
192
|
if (attempt.status === 0) {
|
|
192
193
|
state.recovered.push(recoveryCode);
|
|
193
194
|
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
@@ -202,15 +203,70 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
202
203
|
});
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
206
|
+
// Apply KEY=VALUE lines from slot resolution to process.env.
|
|
207
|
+
// overwrite=true → pool match, always overrides existing env.
|
|
208
|
+
// overwrite=false → formula match, only fills vars that are unset.
|
|
209
|
+
function applyKVLines(output: string, overwrite: boolean): void {
|
|
210
|
+
for (const line of output.split('\n')) {
|
|
211
|
+
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
212
|
+
if (!m) continue;
|
|
213
|
+
const [, key, val] = m;
|
|
214
|
+
switch (key) {
|
|
215
|
+
case 'WATCHER_PORT':
|
|
216
|
+
if (overwrite || !process.env['WATCHER_PORT']) {
|
|
217
|
+
process.env['WATCHER_PORT'] = val;
|
|
218
|
+
process.env['METRO_PORT'] = val;
|
|
219
|
+
process.env['RECIPE_WATCHER_PORT'] = val;
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case 'IOS_SIMULATOR':
|
|
223
|
+
if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
|
|
224
|
+
break;
|
|
225
|
+
case 'SLOT_ID':
|
|
226
|
+
if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resolve mobile slot port/simulator from the farmslot pool (pool-wins-over-env)
|
|
233
|
+
// or from the slot-suffix formula (only fills unset vars). Called before explicit
|
|
234
|
+
// CLI flag overrides so flags always win at the top.
|
|
235
|
+
function resolveMobileSlotPorts(target: string): void {
|
|
236
|
+
const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
|
|
237
|
+
try {
|
|
238
|
+
// Pool match always wins — overwrite whatever is in the environment.
|
|
239
|
+
const poolOut = execFileSync('bash', [
|
|
240
|
+
'-c', `source "${resolveScript}" && resolve_farmslot_ports_by_repo "${target}"`,
|
|
241
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
242
|
+
if (poolOut.trim()) {
|
|
243
|
+
applyKVLines(poolOut, true);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
} catch { /* no pool match — fall through to formula */ }
|
|
247
|
+
try {
|
|
248
|
+
// Formula match only fills unset vars (never overrides explicit env/pool).
|
|
249
|
+
const defOut = execFileSync('bash', [
|
|
250
|
+
'-c', `source "${resolveScript}" && resolve_mobile_slot_defaults "${target}"`,
|
|
251
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
252
|
+
if (defOut.trim()) applyKVLines(defOut, false);
|
|
253
|
+
} catch { /* no slot suffix in dir name — stays at env defaults */ }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Map the env-gap flags onto the env vars the leaf scripts read. For mobile, slot
|
|
257
|
+
// port/simulator defaults are resolved first (pool-wins, then formula-fills-empty)
|
|
258
|
+
// so that the harness respects slot isolation; explicit CLI flags applied below
|
|
259
|
+
// win over everything. Extension: flag > env > config/default.
|
|
209
260
|
function applyLaunchEnvOverrides(
|
|
210
261
|
options: Record<string, string | boolean>,
|
|
211
262
|
adapter: DeviceAdapter,
|
|
212
263
|
mobileTarget: string | undefined,
|
|
264
|
+
target: string,
|
|
213
265
|
): void {
|
|
266
|
+
// Mobile slot isolation: resolve WATCHER_PORT / IOS_SIMULATOR / SLOT_ID from
|
|
267
|
+
// the farmslot pool or directory-suffix formula BEFORE applying explicit flags.
|
|
268
|
+
if (adapter === 'mobile') resolveMobileSlotPorts(target);
|
|
269
|
+
|
|
214
270
|
const device = str(options, 'device');
|
|
215
271
|
if (device && adapter === 'mobile') {
|
|
216
272
|
if (mobileTarget === 'android') {
|
|
@@ -261,35 +317,119 @@ function nativeInputsChanged(target: string, adapter: MetaMaskRecipeAdapter): bo
|
|
|
261
317
|
}
|
|
262
318
|
}
|
|
263
319
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// extension
|
|
269
|
-
// extension
|
|
270
|
-
|
|
320
|
+
// Execute the launch composition for (adapter, tier). Mobile runs prepareMobile()
|
|
321
|
+
// in-process (no shell orchestrator); extension arms spawn adapters/ scripts directly.
|
|
322
|
+
//
|
|
323
|
+
// mobile quick|build → prepareMobile() in-process → leaf scripts
|
|
324
|
+
// extension rebuild → kill watcher + clear rebuild.log + live.sh --start-watch
|
|
325
|
+
// extension build → adapters/extension/start-watch.sh
|
|
326
|
+
// extension watch → adapters/extension/start-watch.sh
|
|
327
|
+
async function executeComposition(
|
|
271
328
|
adapter: DeviceAdapter,
|
|
272
329
|
mobileTarget: string | undefined,
|
|
273
330
|
tier: 'quick' | 'build' | 'verify',
|
|
274
331
|
wantWatch: boolean,
|
|
275
|
-
|
|
332
|
+
target: string,
|
|
333
|
+
json: boolean,
|
|
334
|
+
): Promise<ScriptResult> {
|
|
276
335
|
if (adapter === 'mobile') {
|
|
277
|
-
|
|
278
|
-
|
|
336
|
+
const platform = mobileTarget ?? 'ios';
|
|
337
|
+
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : undefined;
|
|
338
|
+
const preflightMode = tier === 'build' ? 'auto' : 'fast';
|
|
339
|
+
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Extension rebuild (quick relaunch): kill watcher → clear rebuild.log → live.sh --start-watch.
|
|
343
|
+
if (!wantWatch && tier === 'quick') {
|
|
344
|
+
return extensionRebuild(target, json);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Extension build or watch: full webpack compile via start-watch.sh.
|
|
348
|
+
const startWatchSh = path.join(runnerDir, 'adapters/extension/start-watch.sh');
|
|
349
|
+
const watchArgs = ['--target', target];
|
|
350
|
+
if (process.env.WATCHER_PORT) watchArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
351
|
+
return spawnScript(startWatchSh, watchArgs, target, json);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Extension rebuild: kill the harness-owned webpack watcher (pid file + ps-scan
|
|
355
|
+
// for orphans), clear the rebuild log, then drive the installed overlay's
|
|
356
|
+
// live.sh --start-watch and tee its output into the rebuild log.
|
|
357
|
+
function extensionRebuild(target: string, json: boolean): ScriptResult {
|
|
358
|
+
const runtimeDirRel = recipeRuntimeDir();
|
|
359
|
+
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
360
|
+
const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
|
|
361
|
+
const rebuildLog = path.join(runtimeAbs, 'rebuild.log');
|
|
362
|
+
|
|
363
|
+
// E1a: Kill harness-owned watcher via pid file, then ps-scan for orphans.
|
|
364
|
+
try {
|
|
365
|
+
const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
|
|
366
|
+
if (/^\d+$/u.test(pid)) {
|
|
367
|
+
try { process.kill(Number(pid), 'SIGTERM'); } catch { /* already dead */ }
|
|
368
|
+
}
|
|
369
|
+
fs.rmSync(webpackPidFile, { force: true });
|
|
370
|
+
} catch { /* no pid file */ }
|
|
371
|
+
// Scan for any remaining orphan webpack/yarn-start processes in this checkout.
|
|
372
|
+
try {
|
|
373
|
+
const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
|
|
374
|
+
const orphanPids: number[] = [];
|
|
375
|
+
for (const line of psOut.split('\n')) {
|
|
376
|
+
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
377
|
+
if (!match) continue;
|
|
378
|
+
const [, pidStr, cmd] = match;
|
|
379
|
+
const isWatcher =
|
|
380
|
+
cmd.includes('yarn start') ||
|
|
381
|
+
cmd.includes('webpack --watch') ||
|
|
382
|
+
cmd.includes('development/webpack/launch.ts --watch');
|
|
383
|
+
if (!isWatcher) continue;
|
|
384
|
+
if (cmd.includes(target)) {
|
|
385
|
+
orphanPids.push(Number(pidStr));
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
// lsof cwd fallback for processes that don't embed the path in argv.
|
|
389
|
+
try {
|
|
390
|
+
const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
|
|
391
|
+
encoding: 'utf8',
|
|
392
|
+
timeout: 2000,
|
|
393
|
+
});
|
|
394
|
+
if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
|
|
395
|
+
orphanPids.push(Number(pidStr));
|
|
396
|
+
}
|
|
397
|
+
} catch { /* lsof unavailable or permission denied */ }
|
|
398
|
+
}
|
|
399
|
+
if (orphanPids.length > 0) {
|
|
400
|
+
for (const pid of orphanPids) {
|
|
401
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
402
|
+
}
|
|
403
|
+
// Brief pause then force-kill survivors.
|
|
404
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
405
|
+
for (const pid of orphanPids) {
|
|
406
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch { /* ps not available */ }
|
|
410
|
+
|
|
411
|
+
// E1c: Clear the rebuild log (directory must exist for tee).
|
|
412
|
+
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
413
|
+
fs.writeFileSync(rebuildLog, '');
|
|
414
|
+
|
|
415
|
+
const liveScript = recipeHarnessPath(target, 'extension', 'scripts', 'live.sh');
|
|
416
|
+
const liveArgs = ['--target', target, '--start-watch'];
|
|
417
|
+
if (process.env.CDP_PORT) liveArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
418
|
+
if (process.env.WATCHER_PORT) liveArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
419
|
+
const result = spawnScript(liveScript, liveArgs, target, json);
|
|
420
|
+
// Tee live.sh output into rebuild.log for post-mortem diagnosis.
|
|
421
|
+
if (result.output) {
|
|
422
|
+
try { fs.appendFileSync(rebuildLog, result.output); } catch { /* best-effort */ }
|
|
279
423
|
}
|
|
280
|
-
|
|
281
|
-
if (wantWatch) return ['watch'];
|
|
282
|
-
if (tier === 'build') return ['build'];
|
|
283
|
-
return ['rebuild'];
|
|
424
|
+
return result;
|
|
284
425
|
}
|
|
285
426
|
|
|
286
|
-
// Apply the extension display mode AFTER the base launch succeeded, then emit
|
|
287
|
-
// pass summary. Fullscreen = the plain rebuild/build/watch launch above
|
|
288
|
-
// default popup/home window). Sidepanel additionally drives the
|
|
289
|
-
// mechanism via
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// mode actually reaches orchestration. Mobile has no side panel, so the flag is a
|
|
427
|
+
// Apply the extension display mode AFTER the base launch succeeded, then emit
|
|
428
|
+
// the pass summary. Fullscreen = the plain rebuild/build/watch launch above
|
|
429
|
+
// (default popup/home window). Sidepanel additionally drives the
|
|
430
|
+
// chrome.sidePanel mechanism via adapters/extension/sidepanel-toggle.sh.
|
|
431
|
+
// --sidepanel and --fullscreen compose DIFFERENT script calls and the display
|
|
432
|
+
// mode actually reaches the browser. Mobile has no side panel; the flag is a
|
|
293
433
|
// no-op there.
|
|
294
434
|
function finishLaunch(
|
|
295
435
|
json: boolean,
|
|
@@ -301,12 +441,16 @@ function finishLaunch(
|
|
|
301
441
|
state: HealState,
|
|
302
442
|
): number {
|
|
303
443
|
if (adapter === 'extension' && displayMode === 'sidepanel') {
|
|
304
|
-
const
|
|
444
|
+
const sidepanelSh = path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh');
|
|
445
|
+
const sidepanelArgs = ['open'];
|
|
446
|
+
if (process.env.CDP_PORT) sidepanelArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
447
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
|
|
305
448
|
if (sidepanel.status !== 0) {
|
|
306
449
|
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
307
450
|
code: 'SIDEPANEL_OPEN_FAILED',
|
|
308
451
|
message: 'app launched but opening the side panel failed.',
|
|
309
452
|
recoverable: false,
|
|
453
|
+
userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? '<CDP_PORT>'}`,
|
|
310
454
|
exitCode: EXIT.runtime,
|
|
311
455
|
originalError: sidepanel.output.trim() || undefined,
|
|
312
456
|
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// logs — tail Metro/webpack + app logs by composing log-tui.mjs (compact) or
|
|
2
|
+
// plain `tail -f` (--full). Teaches (exit 2) when nothing is running for the
|
|
3
|
+
// checkout.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
9
|
+
import { EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
10
|
+
|
|
11
|
+
const LOGS_BOOLEANS = new Set(['full', 'json']);
|
|
12
|
+
|
|
13
|
+
export async function handleLogs(argv: string[]): Promise<number> {
|
|
14
|
+
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
15
|
+
const json = flag(options, 'json');
|
|
16
|
+
const target = targetOf(options);
|
|
17
|
+
const adapter = resolveAdapter(options, target);
|
|
18
|
+
|
|
19
|
+
if (!adapter) {
|
|
20
|
+
return usageOut(
|
|
21
|
+
json,
|
|
22
|
+
'logs',
|
|
23
|
+
`could not detect the MetaMask repo type for ${target}\n Next: pass --target <checkout> or cd into the checkout, then re-run: mm-harness logs`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (adapter === 'core') {
|
|
27
|
+
return usageOut(json, 'logs', 'core is headless; there are no app/Metro logs.\n Next: mm-harness run <recipe> # run recipes against the headless core');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const source = str(options, 'source') ?? 'metro';
|
|
31
|
+
if (source !== 'metro' && source !== 'app') {
|
|
32
|
+
return usageOut(json, 'logs', '--source must be metro or app.\n Next: mm-harness logs --source metro or mm-harness logs --source app');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
|
|
36
|
+
// read from RECIPE_LOG_EVENTS. Resolution is flag > env > default: the flag wins
|
|
37
|
+
// by setting the env; an absent flag leaves the agent/CI env alone.
|
|
38
|
+
const events = str(options, 'events');
|
|
39
|
+
if (events !== undefined) {
|
|
40
|
+
if (!/^\d+$/u.test(events)) {
|
|
41
|
+
return usageOut(json, 'logs', `--events must be numeric (got: ${events}).\n Next: mm-harness logs --events 20`);
|
|
42
|
+
}
|
|
43
|
+
process.env.RECIPE_LOG_EVENTS = events;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Nothing running → teaching error pointing at launch. The log file is the
|
|
47
|
+
// signal that Metro/webpack has been started for this checkout.
|
|
48
|
+
const logFile = recipeRuntimePath(target, adapter === 'mobile' ? 'metro.log' : 'webpack.log');
|
|
49
|
+
if (!fs.existsSync(logFile)) {
|
|
50
|
+
const launchHint = adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness launch';
|
|
51
|
+
return usageOut(
|
|
52
|
+
json,
|
|
53
|
+
'logs',
|
|
54
|
+
`nothing running for this checkout (no ${path.basename(logFile)}).\n Next: ${launchHint}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const full = flag(options, 'full');
|
|
59
|
+
if (full) {
|
|
60
|
+
// Raw follow: the human explicitly wants unfiltered output.
|
|
61
|
+
const result = spawnScript('tail', ['-n', '+1', '-F', logFile], target, json);
|
|
62
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Compact mode: last N events + pointer to full log.
|
|
66
|
+
const logTui = path.join(runnerDir, 'adapters/shared/log-tui.mjs');
|
|
67
|
+
const eventCount = process.env.RECIPE_LOG_EVENTS ?? '20';
|
|
68
|
+
const uiMode = process.env.RECIPE_LOG_UI_MODE ?? 'compact';
|
|
69
|
+
const result = spawnScript(
|
|
70
|
+
process.execPath,
|
|
71
|
+
[logTui, 'tail', '--log', logFile, '--events', eventCount, '--follow', '--mode', uiMode],
|
|
72
|
+
target,
|
|
73
|
+
json,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (json) {
|
|
77
|
+
console.log(
|
|
78
|
+
JSON.stringify(
|
|
79
|
+
{ schemaVersion: 1, command: 'logs', adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
80
|
+
null,
|
|
81
|
+
2,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
86
|
+
}
|