@deeeed/metamask-harness 0.2.0 → 0.3.1
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 +33 -0
- package/README.md +47 -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 +260 -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 +37 -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 +105 -5
- 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 +48 -40
- package/src/commands/debug.ts +63 -0
- package/src/commands/fixtures.ts +193 -0
- package/{runner/src → src}/commands/launch.ts +200 -54
- package/src/commands/logs.ts +83 -0
- package/{runner/src → src}/commands/shared.ts +40 -30
- package/src/commands/update.ts +316 -0
- package/{runner/src → src}/harness.ts +119 -16
- package/{runner/src → src}/heal-bounds.ts +20 -1
- package/src/index.ts +15 -0
- package/{runner/src → src}/live-adapter-contract.ts +1 -1
- package/{runner/src → src}/mm-harness-cli.ts +64 -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}/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,210 @@
|
|
|
1
|
+
// Mobile runtime prepare — in-process decision-and-action loop.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the retired shell orchestrators (prepare-device.sh + launch.sh).
|
|
4
|
+
// All sequencing is in TypeScript; leaf shell scripts handle single OS ops.
|
|
5
|
+
// Control flows down only: TS → adapters/mobile/{yarn-setup,start-metro,open-device}.sh
|
|
6
|
+
// No leaf script may call back into mm-harness (A1).
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
import { EXIT, spawnScript } from '../../commands/shared.ts';
|
|
11
|
+
import type { ScriptResult } from '../../commands/shared.ts';
|
|
12
|
+
import { runnerDir } from '../../paths.ts';
|
|
13
|
+
import {
|
|
14
|
+
decideMobileReadiness,
|
|
15
|
+
type MobileRuntimeDecisionReport,
|
|
16
|
+
type MobileRuntimeDecisionOptions,
|
|
17
|
+
} from './runtime-decision.ts';
|
|
18
|
+
|
|
19
|
+
export { type MobileRuntimeDecisionReport };
|
|
20
|
+
|
|
21
|
+
export interface PrepareMobileOptions extends MobileRuntimeDecisionOptions {
|
|
22
|
+
/** Pass through to leaf-script spawnScript calls so output is suppressed in --json mode. */
|
|
23
|
+
json?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Passed to open-device.sh --preflight-mode.
|
|
26
|
+
* 'fast' = error if not installed; 'auto' = install if missing; 'rebuild-native'/'clean' = always rebuild.
|
|
27
|
+
* Defaults to 'fast'.
|
|
28
|
+
*/
|
|
29
|
+
preflightMode?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Force --clear on start-metro (reset bundle cache).
|
|
32
|
+
* Used by the setup-wallet clean-Metro retry path (M3).
|
|
33
|
+
*/
|
|
34
|
+
clearMetro?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read current runtime readiness without executing any action.
|
|
39
|
+
* No side effects: just calls decideMobileReadiness and returns the report.
|
|
40
|
+
*/
|
|
41
|
+
export async function mobileRuntimeStatus(
|
|
42
|
+
target: string,
|
|
43
|
+
opts: PrepareMobileOptions = {},
|
|
44
|
+
): Promise<MobileRuntimeDecisionReport> {
|
|
45
|
+
return decideMobileReadiness(target, {
|
|
46
|
+
watcherPort: opts.watcherPort,
|
|
47
|
+
metroLog: opts.metroLog,
|
|
48
|
+
platform: opts.platform,
|
|
49
|
+
record: opts.record,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Prepare the mobile runtime: run the readiness decision then execute each
|
|
55
|
+
* action in sequence via single-purpose leaf shell scripts. Returns a
|
|
56
|
+
* ScriptResult so the launch heal loop can classify outcomes uniformly.
|
|
57
|
+
*/
|
|
58
|
+
export async function prepareMobile(
|
|
59
|
+
target: string,
|
|
60
|
+
opts: PrepareMobileOptions = {},
|
|
61
|
+
): Promise<ScriptResult> {
|
|
62
|
+
const json = opts.json ?? false;
|
|
63
|
+
const platform = opts.platform ?? 'ios';
|
|
64
|
+
const preflightMode = opts.preflightMode ?? 'fast';
|
|
65
|
+
const clearMetro = opts.clearMetro ?? false;
|
|
66
|
+
|
|
67
|
+
const report = await decideMobileReadiness(target, {
|
|
68
|
+
watcherPort: opts.watcherPort,
|
|
69
|
+
metroLog: opts.metroLog,
|
|
70
|
+
platform,
|
|
71
|
+
record: opts.record,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (report.decision === 'blocked') {
|
|
75
|
+
const reasons = report.reasons.join(' ');
|
|
76
|
+
const msg = `mobile prepare blocked: ${reasons}\n Next: fix the bundle error in app code before retrying recipe up.`;
|
|
77
|
+
if (!json) process.stderr.write(`${msg}\n`);
|
|
78
|
+
return { status: EXIT.runtime, output: msg };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (report.decision === 'ready') {
|
|
82
|
+
return { status: 0, output: '' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (report.decision === 'unknown') {
|
|
86
|
+
const msg = 'mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>';
|
|
87
|
+
if (!json) process.stderr.write(`${msg}\n`);
|
|
88
|
+
return { status: EXIT.runtime, output: msg };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// decision is 'install' or 'launch' — execute each action in sequence.
|
|
92
|
+
// clearMetro forces --clear on start-metro regardless of what decideMobileReadiness set.
|
|
93
|
+
const actions = clearMetro
|
|
94
|
+
? report.actions.map((a) =>
|
|
95
|
+
a.id === 'start-metro' && !a.argv?.includes('--clear')
|
|
96
|
+
? { ...a, argv: [...(a.argv ?? []), '--clear'] }
|
|
97
|
+
: a,
|
|
98
|
+
)
|
|
99
|
+
: report.actions;
|
|
100
|
+
for (const action of actions) {
|
|
101
|
+
const result = dispatchAction(action, target, platform, json, preflightMode);
|
|
102
|
+
if (result.status !== 0) return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// After install: re-decide and run appropriate actions.
|
|
106
|
+
// RECIPE_UP_INSTALL_ATTEMPTED guards against infinite loops if yarn-setup was a no-op.
|
|
107
|
+
if (report.decision === 'install' && !process.env['RECIPE_UP_INSTALL_ATTEMPTED']) {
|
|
108
|
+
process.env['RECIPE_UP_INSTALL_ATTEMPTED'] = '1';
|
|
109
|
+
const postInstall = await decideMobileReadiness(target, {
|
|
110
|
+
watcherPort: opts.watcherPort,
|
|
111
|
+
metroLog: opts.metroLog,
|
|
112
|
+
platform,
|
|
113
|
+
record: true,
|
|
114
|
+
});
|
|
115
|
+
switch (postInstall.decision) {
|
|
116
|
+
case 'install': {
|
|
117
|
+
// Yarn setup ran but runtime is still unresolved — teach and fail.
|
|
118
|
+
const msg =
|
|
119
|
+
`mobile prepare: dependencies installed but runtime is still unresolved ` +
|
|
120
|
+
`(${postInstall.reasons.join('; ')})\n` +
|
|
121
|
+
` Next: inspect the checkout — node_modules may be incomplete or yarn.lock drifted.`;
|
|
122
|
+
if (!json) process.stderr.write(`${msg}\n`);
|
|
123
|
+
return { status: EXIT.runtime, output: msg };
|
|
124
|
+
}
|
|
125
|
+
case 'ready': {
|
|
126
|
+
// Deps resolved and runtime appears ready — still confirm bridge is live.
|
|
127
|
+
const bridge = dispatchAction({ id: 'wait-for-bridge', cwd: target }, target, platform, json, preflightMode);
|
|
128
|
+
if (bridge.status !== 0) return bridge;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'launch': {
|
|
132
|
+
for (const action of postInstall.actions) {
|
|
133
|
+
const result = dispatchAction(action, target, platform, json, preflightMode);
|
|
134
|
+
if (result.status !== 0) return result;
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
default: {
|
|
139
|
+
// 'blocked' or 'unknown' after install — surface as error.
|
|
140
|
+
const msg =
|
|
141
|
+
`mobile prepare: post-install state is ${postInstall.decision}: ${postInstall.reasons.join('; ')}\n` +
|
|
142
|
+
` Next: run mm-harness verify --adapter mobile --target <checkout>`;
|
|
143
|
+
if (!json) process.stderr.write(`${msg}\n`);
|
|
144
|
+
return { status: EXIT.runtime, output: msg };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { status: 0, output: '' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function dispatchAction(
|
|
153
|
+
action: { id: string; argv?: string[]; cwd?: string },
|
|
154
|
+
target: string,
|
|
155
|
+
platform: string,
|
|
156
|
+
json: boolean,
|
|
157
|
+
preflightMode = 'fast',
|
|
158
|
+
): ScriptResult {
|
|
159
|
+
const cwd = action.cwd ?? target;
|
|
160
|
+
switch (action.id) {
|
|
161
|
+
case 'yarn-setup': {
|
|
162
|
+
// Leaf: install node_modules (deps missing or stale).
|
|
163
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/yarn-setup.sh');
|
|
164
|
+
return spawnScript(leaf, ['--target', cwd], target, json);
|
|
165
|
+
}
|
|
166
|
+
case 'start-metro': {
|
|
167
|
+
// Leaf: ensure Metro is running; argv may carry --clear for cache reset.
|
|
168
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
|
|
169
|
+
const extra: string[] = action.argv ?? [];
|
|
170
|
+
return spawnScript(leaf, ['--target', cwd, ...extra], target, json);
|
|
171
|
+
}
|
|
172
|
+
case 'prewarm-bundle': {
|
|
173
|
+
// Leaf: curl the bundle URL before opening the dev client.
|
|
174
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/prewarm-bundle.sh');
|
|
175
|
+
return spawnScript(leaf, ['--platform', platform, '--target', cwd], target, json);
|
|
176
|
+
}
|
|
177
|
+
case 'wait-for-bridge': {
|
|
178
|
+
// Leaf: poll CDP bridge until a route target registers.
|
|
179
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/wait-for-bridge.sh');
|
|
180
|
+
return spawnScript(leaf, ['--target', cwd], target, json);
|
|
181
|
+
}
|
|
182
|
+
case 'clear-metro-cache': {
|
|
183
|
+
// Legacy alias for start-metro --clear (kept for backward compat).
|
|
184
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
|
|
185
|
+
return spawnScript(leaf, ['--target', cwd, '--clear'], target, json);
|
|
186
|
+
}
|
|
187
|
+
case 'launch-mobile-runtime': {
|
|
188
|
+
// Leaf: open the MetaMask Mobile dev client on the simulator/device.
|
|
189
|
+
const leaf = path.join(runnerDir, 'adapters/mobile/open-device.sh');
|
|
190
|
+
return spawnScript(
|
|
191
|
+
leaf,
|
|
192
|
+
['--platform', platform, '--target', cwd, '--preflight-mode', preflightMode],
|
|
193
|
+
target,
|
|
194
|
+
json,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
case 'rebuild-native-dev-client': {
|
|
198
|
+
// Native rebuild required — the dev client binary predates a native Nitro
|
|
199
|
+
// module. Inform the caller; a full native build is not automated here.
|
|
200
|
+
const msg =
|
|
201
|
+
'rebuild-native-dev-client required: native module mismatch detected.\n' +
|
|
202
|
+
' Next: yarn start:ios or yarn start:android to rebuild the native dev client, ' +
|
|
203
|
+
'then re-run mm-harness launch.';
|
|
204
|
+
if (!json) process.stderr.write(`${msg}\n`);
|
|
205
|
+
return { status: EXIT.runtime, output: msg };
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
return { status: 0, output: '' };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -68,9 +68,31 @@ function resolveMetroLog(target: string, metroLog?: string): string | null {
|
|
|
68
68
|
return fs.existsSync(abs) ? abs : null;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// A metro.log is stale when the yarn install markers are newer — a yarn install
|
|
72
|
+
// happened after the last bundle run, so the log describes a now-superseded module
|
|
73
|
+
// tree. Ignoring it lets launch start fresh instead of deciding off stale content.
|
|
74
|
+
function isMetroLogStale(target: string, logAbs: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const logMtime = fs.statSync(logAbs).mtimeMs;
|
|
77
|
+
// Check against the same install markers as depsCheck; they are updated by
|
|
78
|
+
// yarn on every successful install, making them a reliable freshness signal.
|
|
79
|
+
const INSTALL_MARKERS = ['node_modules/.yarn-state.yml', '.yarn/install-state.gz'];
|
|
80
|
+
return INSTALL_MARKERS.some((rel) => {
|
|
81
|
+
try {
|
|
82
|
+
return logMtime < fs.statSync(path.join(target, rel)).mtimeMs;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
function metroLogCheck(target: string, metroLog?: string): MetroLogCheck {
|
|
72
93
|
const abs = resolveMetroLog(target, metroLog);
|
|
73
94
|
if (!abs) return { status: 'no-log' };
|
|
95
|
+
if (isMetroLogStale(target, abs)) return { status: 'no-log' };
|
|
74
96
|
const logText = fs.readFileSync(abs, 'utf8');
|
|
75
97
|
return analyzeBundleLog({
|
|
76
98
|
target,
|
|
@@ -80,16 +102,72 @@ function metroLogCheck(target: string, metroLog?: string): MetroLogCheck {
|
|
|
80
102
|
});
|
|
81
103
|
}
|
|
82
104
|
|
|
105
|
+
// Returns names of top-level package.json dependencies absent from node_modules.
|
|
106
|
+
// Catches the case where a dep was added in a branch merge but yarn install was not
|
|
107
|
+
// re-run — install markers and fingerprint may both be satisfied while a required
|
|
108
|
+
// package directory is simply missing (real case: 249s Metro bundle discovers it
|
|
109
|
+
// too late).
|
|
110
|
+
function missingRequiredDeps(target: string): string[] {
|
|
111
|
+
try {
|
|
112
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')) as {
|
|
113
|
+
dependencies?: Record<string, unknown>;
|
|
114
|
+
devDependencies?: Record<string, unknown>;
|
|
115
|
+
};
|
|
116
|
+
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
117
|
+
return Object.keys(all).filter(
|
|
118
|
+
(name) => !fs.existsSync(path.join(target, 'node_modules', name)),
|
|
119
|
+
);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Returns true when any manifest input (package.json or yarn.lock) is newer than
|
|
126
|
+
// the install markers written by yarn on a successful install. A reliable signal
|
|
127
|
+
// that deps changed since the last install, even when depsCheck found no recorded
|
|
128
|
+
// baseline drift (e.g. git checkout preserved an old author-date mtime on the
|
|
129
|
+
// manifest, then a new dep was committed, making the manifest newer than markers).
|
|
130
|
+
function depsStaleByMtime(target: string): boolean {
|
|
131
|
+
// Use the same install markers as depsCheck: the files yarn writes on install.
|
|
132
|
+
const INSTALL_MARKERS = ['node_modules/.yarn-state.yml', '.yarn/install-state.gz'];
|
|
133
|
+
let markerMtime = 0;
|
|
134
|
+
for (const rel of INSTALL_MARKERS) {
|
|
135
|
+
try {
|
|
136
|
+
const mt = fs.statSync(path.join(target, rel)).mtimeMs;
|
|
137
|
+
if (mt > markerMtime) markerMtime = mt;
|
|
138
|
+
} catch {
|
|
139
|
+
// marker absent; keep searching
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (markerMtime === 0) return false; // no markers → depsCheck already handles via 'missing'
|
|
143
|
+
for (const file of ['package.json', 'yarn.lock']) {
|
|
144
|
+
try {
|
|
145
|
+
if (fs.statSync(path.join(target, file)).mtimeMs > markerMtime) return true;
|
|
146
|
+
} catch {
|
|
147
|
+
// manifest absent; skip
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
83
153
|
const installActions = (target: string): MobileRuntimeDecisionAction[] => [
|
|
84
154
|
{ id: 'yarn-setup', argv: ['yarn', 'setup'], cwd: target },
|
|
85
155
|
];
|
|
86
156
|
|
|
87
157
|
const launchActions = (target: string, clearMetro = false): MobileRuntimeDecisionAction[] => {
|
|
88
158
|
const actions: MobileRuntimeDecisionAction[] = [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
159
|
+
// start-metro ensures Metro is running; --clear resets the bundle cache.
|
|
160
|
+
actions.push({
|
|
161
|
+
id: 'start-metro',
|
|
162
|
+
argv: clearMetro ? ['--clear'] : [],
|
|
163
|
+
cwd: target,
|
|
164
|
+
});
|
|
165
|
+
// Prewarm the bundle before opening the dev client so the app connects instantly.
|
|
166
|
+
actions.push({ id: 'prewarm-bundle', cwd: target });
|
|
167
|
+
// Open the dev client via the expo-development-client deep link.
|
|
92
168
|
actions.push({ id: 'launch-mobile-runtime' });
|
|
169
|
+
// Poll the CDP bridge until a route target registers (confirms app is bridged).
|
|
170
|
+
actions.push({ id: 'wait-for-bridge', cwd: target });
|
|
93
171
|
return actions;
|
|
94
172
|
};
|
|
95
173
|
|
|
@@ -100,9 +178,28 @@ export async function decideMobileReadiness(
|
|
|
100
178
|
const resolved = path.resolve(target);
|
|
101
179
|
if (options.record) recordDepsBaseline(resolved);
|
|
102
180
|
|
|
103
|
-
const
|
|
181
|
+
const rawDeps = depsCheck(resolved, {
|
|
104
182
|
productMarkers: mobileProductMarkers(options.platform),
|
|
105
183
|
});
|
|
184
|
+
// Mtime-based fallback: when depsCheck found no recorded baseline, supplement
|
|
185
|
+
// with a direct mtime comparison — if package.json or yarn.lock is newer than
|
|
186
|
+
// node_modules, a dep change landed after the last install. Fail fast at
|
|
187
|
+
// pre-flight rather than launching a doomed Metro bundle. Skip when a baseline
|
|
188
|
+
// was found (hasBaseline=true) so fingerprint-verified 'current' is trusted.
|
|
189
|
+
let deps: DepsCheck =
|
|
190
|
+
rawDeps.status === 'current' && !rawDeps.hasBaseline && depsStaleByMtime(resolved)
|
|
191
|
+
? { installed: rawDeps.installed, status: 'stale', hasBaseline: rawDeps.hasBaseline }
|
|
192
|
+
: rawDeps;
|
|
193
|
+
// Required-dep absence: even when fingerprint + mtime reports 'current', a
|
|
194
|
+
// top-level dependency could be absent from node_modules (e.g. added in a
|
|
195
|
+
// branch merge but install not re-run). Catch at pre-flight before Metro
|
|
196
|
+
// discovers it during a 249s bundle run.
|
|
197
|
+
if (deps.status === 'current') {
|
|
198
|
+
const absent = missingRequiredDeps(resolved);
|
|
199
|
+
if (absent.length > 0) {
|
|
200
|
+
deps = { installed: deps.installed, status: 'partial', hasBaseline: deps.hasBaseline, missingProducts: absent };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
106
203
|
const metroLog = metroLogCheck(resolved, options.metroLog);
|
|
107
204
|
const metro = options.watcherPort
|
|
108
205
|
? await probeMetroPackager(options.watcherPort)
|
|
@@ -155,7 +252,10 @@ export async function decideMobileReadiness(
|
|
|
155
252
|
}
|
|
156
253
|
|
|
157
254
|
const metroLogAbs = resolveMetroLog(resolved, options.metroLog);
|
|
158
|
-
const metroLogText =
|
|
255
|
+
const metroLogText =
|
|
256
|
+
metroLogAbs && !isMetroLogStale(resolved, metroLogAbs)
|
|
257
|
+
? fs.readFileSync(metroLogAbs, 'utf8')
|
|
258
|
+
: '';
|
|
159
259
|
const staleNativeModule = supersededErrorCapture(metroLogText, {
|
|
160
260
|
errorPattern: NATIVE_MODULE_STALE,
|
|
161
261
|
okPattern: BUNDLE_OK,
|
|
@@ -2,8 +2,8 @@ import http from 'node:http';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { compatibilityMode, fixtureSummary, repoShape } from './doctor.ts';
|
|
4
4
|
import { runLiveAdapterScript } from './live-adapter-contract.ts';
|
|
5
|
-
import { withExtensionPage } from '
|
|
6
|
-
import { bridgeCommand, evalSync, simulatorScreenshot } from '
|
|
5
|
+
import { withExtensionPage } from '../library/actions/extension/platform/cdp.mjs';
|
|
6
|
+
import { bridgeCommand, evalSync, simulatorScreenshot } from '../library/actions/mobile/platform/bridge.mjs';
|
|
7
7
|
import type {
|
|
8
8
|
ActionAdapter,
|
|
9
9
|
ActionExecutionContext,
|
|
@@ -284,7 +284,7 @@ function mobileProbeOutput(status: unknown, input: MetaMaskUiActionInput, projec
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
function mobileBridgePath(_projectRoot: string): string {
|
|
287
|
-
return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || 'runner:
|
|
287
|
+
return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || 'runner:adapters/mobile/bridge-runtime/cdp-bridge.cjs';
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
async function waitForMobileTarget(input: ReturnType<typeof uiInputFor>, payload: ActionNode) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
* cli-color.
|
|
2
|
+
* cli-color.ts — shared ANSI styling for recipe CLI human output.
|
|
4
3
|
*
|
|
5
4
|
* Honors NO_COLOR, RECIPE_NO_COLOR, FORCE_COLOR, RECIPE_COLOR, and TTY detection.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
type StyleName =
|
|
8
|
+
| 'reset' | 'bold' | 'dim' | 'label' | 'cmd' | 'ok' | 'warn'
|
|
9
|
+
| 'err' | 'info' | 'accent' | 'path' | 'comment' | 'active';
|
|
10
|
+
|
|
11
|
+
const STYLES: Record<StyleName, string> = {
|
|
9
12
|
reset: '\x1b[0m',
|
|
10
13
|
bold: '\x1b[1m',
|
|
11
14
|
dim: '\x1b[2m',
|
|
@@ -21,32 +24,37 @@ const STYLES = {
|
|
|
21
24
|
active: '\x1b[1m',
|
|
22
25
|
};
|
|
23
26
|
|
|
24
|
-
export function colorEnabled(stream = process.stderr) {
|
|
27
|
+
export function colorEnabled(stream: NodeJS.WriteStream = process.stderr): boolean {
|
|
25
28
|
if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '' && process.env.NO_COLOR !== '0') {
|
|
26
29
|
return false;
|
|
27
30
|
}
|
|
28
31
|
if (process.env.RECIPE_NO_COLOR === '1') return false;
|
|
29
32
|
if (process.env.RECIPE_COLOR === '1' || process.env.FORCE_COLOR === '1') return true;
|
|
30
|
-
return Boolean(stream?.isTTY);
|
|
33
|
+
return Boolean((stream as NodeJS.WriteStream & { isTTY?: boolean })?.isTTY);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
export function stripAnsi(text) {
|
|
36
|
+
export function stripAnsi(text: string): string {
|
|
34
37
|
return String(text).replace(/\x1b\[[0-9;]*m/gu, '');
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export function color(style, text, { stream = process.stderr } = {}) {
|
|
40
|
+
export function color(style: string, text: string, { stream = process.stderr }: { stream?: NodeJS.WriteStream } = {}): string {
|
|
38
41
|
const value = String(text);
|
|
39
42
|
if (!colorEnabled(stream)) return value;
|
|
40
|
-
const code = STYLES[style];
|
|
43
|
+
const code = STYLES[style as StyleName];
|
|
41
44
|
if (!code) return value;
|
|
42
45
|
return `${code}${value}${STYLES.reset}`;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
export function colorKv(
|
|
48
|
+
export function colorKv(
|
|
49
|
+
label: string,
|
|
50
|
+
value: string,
|
|
51
|
+
valueStyle = 'info',
|
|
52
|
+
{ stream = process.stderr }: { stream?: NodeJS.WriteStream } = {},
|
|
53
|
+
): string {
|
|
46
54
|
return `${color('label', `${label}:`, { stream })} ${color(valueStyle, value, { stream })}`;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
export function colorStatusWord(word, { stream = process.stderr } = {}) {
|
|
57
|
+
export function colorStatusWord(word: string, { stream = process.stderr }: { stream?: NodeJS.WriteStream } = {}): string {
|
|
50
58
|
const normalized = String(word || '').toLowerCase();
|
|
51
59
|
if (['up', 'ready', 'yes', 'pass', 'running', 'present', 'healthy'].includes(normalized)) {
|
|
52
60
|
return color('ok', word, { stream });
|
|
@@ -60,7 +68,7 @@ export function colorStatusWord(word, { stream = process.stderr } = {}) {
|
|
|
60
68
|
return color('info', word, { stream });
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
export function classifyLogEvent(event) {
|
|
71
|
+
export function classifyLogEvent(event: string): string {
|
|
64
72
|
const text = String(event || '');
|
|
65
73
|
if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
|
|
66
74
|
return 'err';
|
|
@@ -77,8 +85,8 @@ export function classifyLogEvent(event) {
|
|
|
77
85
|
return 'info';
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
export function colorLogEvent(event, { latest = false, stream = process.stderr } = {}) {
|
|
88
|
+
export function colorLogEvent(event: string, { latest = false, stream = process.stderr }: { latest?: boolean; stream?: NodeJS.WriteStream } = {}): string {
|
|
81
89
|
const style = classifyLogEvent(event);
|
|
82
90
|
const painted = color(style === 'label' ? 'label' : style, event, { stream });
|
|
83
91
|
return latest ? color('active', painted, { stream }) : painted;
|
|
84
|
-
}
|
|
92
|
+
}
|