@deeeed/metamask-harness 0.5.0 → 0.6.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 +51 -20
- package/adapters/core/inject.sh +1 -0
- package/adapters/extension/cleanup.mjs +1 -1
- package/adapters/extension/inject.mjs +4 -1
- package/adapters/extension/launch-browser.cjs +18 -4
- package/adapters/extension/live.sh +14 -2
- package/adapters/extension/refresh-build.sh +2 -2
- package/adapters/extension/sidepanel-toggle.sh +33 -12
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/wallet-fixture-state.cjs +38 -24
- package/adapters/manifest.json +32 -3
- package/adapters/mobile/inject.sh +1 -0
- package/adapters/mobile/start-metro.sh +1 -1
- package/adapters/mobile/stop-metro.sh +20 -0
- package/adapters/mobile/verify.sh +1 -1
- package/adapters/shared/activate-repo-node.sh +1 -1
- package/adapters/shared/cli-ux.sh +44 -25
- package/adapters/shared/ensure-runner-deps.sh +30 -0
- package/adapters/shared/log-tui.mjs +4 -4
- package/adapters/shared/open-debug.mjs +1 -1
- package/adapters/shared/reap-checkout-metros.sh +53 -0
- package/adapters/shared/recipe-harness-root.mjs +23 -0
- package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
- package/adapters/shared/resolve-farmslot-ports.sh +19 -126
- package/bin/mm-harness +45 -4
- package/dist/adapters/extension/runtime.js +3 -1
- package/dist/adapters/mobile/provision.js +34 -1
- package/dist/adapters/mobile/runtime-decision.js +3 -1
- package/dist/adapters/resolve-farmslot-ports.js +22 -0
- package/dist/adapters/slot-ports.js +18 -29
- package/dist/cli.js +32 -1463
- package/dist/commands/call.js +183 -0
- package/dist/commands/completion-candidates.js +58 -0
- package/dist/commands/doctor.js +101 -0
- package/dist/commands/ensure-ready.js +24 -0
- package/dist/commands/flows.js +62 -0
- package/dist/commands/launch/extension.js +40 -0
- package/dist/commands/{launch.js → launch/index.js} +15 -47
- package/dist/commands/launch/mobile.js +10 -0
- package/dist/commands/manifest.js +72 -0
- package/dist/commands/parse-args.js +189 -0
- package/dist/commands/provision.js +136 -0
- package/dist/commands/resolve-extension.js +23 -0
- package/dist/commands/run-engine.js +341 -0
- package/dist/commands/run.js +217 -0
- package/dist/commands/runtime-decision.js +58 -0
- package/dist/commands/runtime-health.js +25 -0
- package/dist/commands/runtime-launch.js +139 -0
- package/dist/commands/self-test.js +52 -0
- package/dist/commands/stop.js +52 -0
- package/dist/harness.js +8 -48
- package/dist/mm-harness-cli.js +13 -8
- package/docs/CLI-SPEC.md +1 -1
- package/docs/CODE-MAP.md +62 -0
- package/library/README.md +14 -0
- 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 +7 -1
- package/library/actions/harness-exports.mjs +27 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
- package/library/actions/mobile/wallet/setup.mjs +1 -1
- package/package.json +6 -7
- package/src/adapters/core/surface.ts +0 -71
- package/src/adapters/extension/ensure-ready.ts +0 -185
- package/src/adapters/extension/extension-id.ts +0 -107
- package/src/adapters/extension/runtime-decision.ts +0 -445
- package/src/adapters/extension/runtime.ts +0 -407
- package/src/adapters/extension/surface.ts +0 -88
- package/src/adapters/mobile/deps-markers.ts +0 -21
- package/src/adapters/mobile/prepare.ts +0 -246
- package/src/adapters/mobile/provision.ts +0 -594
- package/src/adapters/mobile/runtime-decision.ts +0 -459
- package/src/adapters/mobile/surface.ts +0 -71
- package/src/adapters/slot-ports.ts +0 -165
- package/src/adapters/surface.ts +0 -117
- package/src/adapters.ts +0 -601
- package/src/cli-color.ts +0 -92
- package/src/cli-commands.ts +0 -250
- package/src/cli-version.ts +0 -141
- package/src/cli.ts +0 -2091
- package/src/commands/debug.ts +0 -65
- package/src/commands/fixtures.ts +0 -198
- package/src/commands/launch.ts +0 -470
- package/src/commands/logs.ts +0 -99
- package/src/commands/shared.ts +0 -235
- package/src/commands/update.ts +0 -316
- package/src/completions-cache.ts +0 -86
- package/src/doctor.ts +0 -215
- package/src/harness.ts +0 -797
- package/src/heal-bounds.ts +0 -198
- package/src/index.ts +0 -15
- package/src/leaf-invoke.ts +0 -28
- package/src/live-adapter-contract.ts +0 -274
- package/src/manifest.ts +0 -47
- package/src/mm-harness-cli.ts +0 -655
- package/src/paths.ts +0 -198
- package/src/progress.ts +0 -117
- package/src/recording-target.ts +0 -147
- package/src/run-recording.ts +0 -329
- package/src/runner.ts +0 -108
- package/src/types.ts +0 -57
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
clearDecisionState,
|
|
7
|
-
depsCheck,
|
|
8
|
-
recordDepsBaseline,
|
|
9
|
-
type DepsCheck,
|
|
10
|
-
} from '@farmslot/recipe-harness/runtime/deps-readiness';
|
|
11
|
-
import {
|
|
12
|
-
analyzeBundleLog,
|
|
13
|
-
evaluatePersistentBundleError,
|
|
14
|
-
supersededErrorCapture,
|
|
15
|
-
type BundleLogAnalysis,
|
|
16
|
-
} from '@farmslot/recipe-harness/runtime/log-analysis';
|
|
17
|
-
import { probeMetroPackager, type MetroReachabilityCheck } from '@farmslot/recipe-harness/runtime/metro-probe';
|
|
18
|
-
|
|
19
|
-
import { recipeRuntimePath } from '../../paths.ts';
|
|
20
|
-
import { mobileProductMarkers } from './deps-markers.ts';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* MetaMask Mobile runtime-readiness adapter.
|
|
24
|
-
*
|
|
25
|
-
* Generic deps/log/metro primitives live in `@farmslot/recipe-harness/runtime/*`.
|
|
26
|
-
* This file owns MetaMask-specific markers, Nitro staleness regex, and action ids.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
export type MobileReadinessDecision = 'install' | 'launch' | 'ready' | 'blocked' | 'unknown';
|
|
30
|
-
|
|
31
|
-
export interface MobileRuntimeDecisionAction {
|
|
32
|
-
id: string;
|
|
33
|
-
argv?: string[];
|
|
34
|
-
cwd?: string;
|
|
35
|
-
paths?: string[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type MetroLogCheck = BundleLogAnalysis;
|
|
39
|
-
|
|
40
|
-
export interface MobileRuntimeDecisionReport {
|
|
41
|
-
schemaVersion: 1;
|
|
42
|
-
adapter: 'mobile';
|
|
43
|
-
target: string;
|
|
44
|
-
decision: MobileReadinessDecision;
|
|
45
|
-
reasonCode: string;
|
|
46
|
-
reasons: string[];
|
|
47
|
-
// A single teaching escape for the caller's actual situation, set on decisions
|
|
48
|
-
// the runner deliberately refuses to auto-resolve (e.g. fast-mode deps gaps).
|
|
49
|
-
userAction?: string;
|
|
50
|
-
checks: {
|
|
51
|
-
deps: DepsCheck;
|
|
52
|
-
metroLog: MetroLogCheck;
|
|
53
|
-
metro: MetroReachabilityCheck;
|
|
54
|
-
};
|
|
55
|
-
actions: MobileRuntimeDecisionAction[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface MobileRuntimeDecisionOptions {
|
|
59
|
-
watcherPort?: number;
|
|
60
|
-
metroLog?: string;
|
|
61
|
-
platform?: string;
|
|
62
|
-
record?: boolean;
|
|
63
|
-
// 'fast' (quick launch / no --build) makes deps readiness a presence check only:
|
|
64
|
-
// freshness is the orchestrator's deps-phase contract, never re-decided here.
|
|
65
|
-
preflightMode?: string;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
69
|
-
const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u;
|
|
70
|
-
const NATIVE_MODULE_STALE =
|
|
71
|
-
/\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
|
|
72
|
-
|
|
73
|
-
// The metro.log location honors RECIPE_RUNTIME_DIR via the shared recipeRuntimePath
|
|
74
|
-
// resolver (relative, validated), so the decision engine reads metro.log from the
|
|
75
|
-
// same per-run dir the mobile leaf scripts write — two jobs sharing one checkout
|
|
76
|
-
// stay isolated. An explicit metroLog override (a --metro-log flag) still wins.
|
|
77
|
-
function resolveMetroLog(target: string, metroLog?: string): string | null {
|
|
78
|
-
const abs = metroLog
|
|
79
|
-
? (path.isAbsolute(metroLog) ? metroLog : path.join(target, metroLog))
|
|
80
|
-
: recipeRuntimePath(target, 'metro.log');
|
|
81
|
-
return fs.existsSync(abs) ? abs : null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// A metro.log is stale when the yarn install markers are newer — a yarn install
|
|
85
|
-
// happened after the last bundle run, so the log describes a now-superseded module
|
|
86
|
-
// tree. Ignoring it lets launch start fresh instead of deciding off stale content.
|
|
87
|
-
function isMetroLogStale(target: string, logAbs: string): boolean {
|
|
88
|
-
try {
|
|
89
|
-
const logMtime = fs.statSync(logAbs).mtimeMs;
|
|
90
|
-
// Check against the same install markers as depsCheck; they are updated by
|
|
91
|
-
// yarn on every successful install, making them a reliable freshness signal.
|
|
92
|
-
const INSTALL_MARKERS = ['node_modules/.yarn-state.yml', '.yarn/install-state.gz'];
|
|
93
|
-
return INSTALL_MARKERS.some((rel) => {
|
|
94
|
-
try {
|
|
95
|
-
return logMtime < fs.statSync(path.join(target, rel)).mtimeMs;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
} catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function metroLogCheck(target: string, metroLog?: string): MetroLogCheck {
|
|
106
|
-
const abs = resolveMetroLog(target, metroLog);
|
|
107
|
-
if (!abs) return { status: 'no-log' };
|
|
108
|
-
if (isMetroLogStale(target, abs)) return { status: 'no-log' };
|
|
109
|
-
const logText = fs.readFileSync(abs, 'utf8');
|
|
110
|
-
return analyzeBundleLog({
|
|
111
|
-
target,
|
|
112
|
-
logText,
|
|
113
|
-
errorPattern: BUNDLE_ERR,
|
|
114
|
-
okPattern: BUNDLE_OK,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Returns names of top-level package.json dependencies absent from node_modules.
|
|
119
|
-
// Catches the case where a dep was added in a branch merge but yarn install was not
|
|
120
|
-
// re-run — install markers and fingerprint may both be satisfied while a required
|
|
121
|
-
// package directory is simply missing (real case: 249s Metro bundle discovers it
|
|
122
|
-
// too late).
|
|
123
|
-
function missingRequiredDeps(target: string): string[] {
|
|
124
|
-
try {
|
|
125
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(target, 'package.json'), 'utf8')) as {
|
|
126
|
-
dependencies?: Record<string, unknown>;
|
|
127
|
-
devDependencies?: Record<string, unknown>;
|
|
128
|
-
};
|
|
129
|
-
const all = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
130
|
-
return Object.keys(all).filter(
|
|
131
|
-
(name) => !fs.existsSync(path.join(target, 'node_modules', name)),
|
|
132
|
-
);
|
|
133
|
-
} catch {
|
|
134
|
-
return [];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const installActions = (target: string): MobileRuntimeDecisionAction[] => [
|
|
139
|
-
{ id: 'yarn-setup', argv: ['yarn', 'setup'], cwd: target },
|
|
140
|
-
];
|
|
141
|
-
|
|
142
|
-
// True when a MetaMask dev client process is alive on the target device
|
|
143
|
-
// (IOS_SIMULATOR / ADB_SERIAL, falling back to the booted simulator). Probe
|
|
144
|
-
// failures count as not-running: the worst case is an idempotent relaunch,
|
|
145
|
-
// while failing open would declare a dead app "ready".
|
|
146
|
-
function appRunningOnDevice(platform?: string): boolean {
|
|
147
|
-
try {
|
|
148
|
-
if (platform === 'android') {
|
|
149
|
-
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
150
|
-
const args = serial ? ['-s', serial] : [];
|
|
151
|
-
const out = execFileSync('adb', [...args, 'shell', 'ps', '-A'], {
|
|
152
|
-
encoding: 'utf8',
|
|
153
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
154
|
-
timeout: 5_000,
|
|
155
|
-
});
|
|
156
|
-
return out.includes('io.metamask');
|
|
157
|
-
}
|
|
158
|
-
const device = process.env.IOS_SIMULATOR || 'booted';
|
|
159
|
-
const out = execFileSync('xcrun', ['simctl', 'spawn', device, 'launchctl', 'list'], {
|
|
160
|
-
encoding: 'utf8',
|
|
161
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
162
|
-
timeout: 5_000,
|
|
163
|
-
});
|
|
164
|
-
return out.toLowerCase().includes('io.metamask');
|
|
165
|
-
} catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const launchActions = (target: string, clearMetro = false): MobileRuntimeDecisionAction[] => {
|
|
171
|
-
const actions: MobileRuntimeDecisionAction[] = [];
|
|
172
|
-
// start-metro ensures Metro is running; --clear resets the bundle cache.
|
|
173
|
-
actions.push({
|
|
174
|
-
id: 'start-metro',
|
|
175
|
-
argv: clearMetro ? ['--clear'] : [],
|
|
176
|
-
cwd: target,
|
|
177
|
-
});
|
|
178
|
-
// Prewarm the bundle before opening the dev client so the app connects instantly.
|
|
179
|
-
actions.push({ id: 'prewarm-bundle', cwd: target });
|
|
180
|
-
// Open the dev client via the expo-development-client deep link.
|
|
181
|
-
actions.push({ id: 'launch-mobile-runtime' });
|
|
182
|
-
// Poll the CDP bridge until a route target registers (confirms app is bridged).
|
|
183
|
-
actions.push({ id: 'wait-for-bridge', cwd: target });
|
|
184
|
-
return actions;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export async function decideMobileReadiness(
|
|
188
|
-
target: string,
|
|
189
|
-
options: MobileRuntimeDecisionOptions = {},
|
|
190
|
-
): Promise<MobileRuntimeDecisionReport> {
|
|
191
|
-
const resolved = path.resolve(target);
|
|
192
|
-
if (options.record) recordDepsBaseline(resolved);
|
|
193
|
-
|
|
194
|
-
const fast = options.preflightMode === 'fast';
|
|
195
|
-
const report = await computeMobileReadiness(resolved, options, fast);
|
|
196
|
-
|
|
197
|
-
// mtime churn is already neutralized above, so a fast-mode 'install' verdict
|
|
198
|
-
// means deps genuinely need work (absent markers, an absent required package, a
|
|
199
|
-
// fingerprint-baseline drift, or Metro unable to resolve a module). In an
|
|
200
|
-
// orchestrated run the deps phase owns installation; having the runner install
|
|
201
|
-
// mid-launch is inverted authority. Surface a teaching block, not an implicit
|
|
202
|
-
// setup, so a runway preflight never re-does the orchestrator's deps work.
|
|
203
|
-
if (fast && report.decision === 'install') {
|
|
204
|
-
return {
|
|
205
|
-
...report,
|
|
206
|
-
decision: 'blocked',
|
|
207
|
-
reasonCode: 'deps-not-ready',
|
|
208
|
-
reasons: [
|
|
209
|
-
'Fast preflight found dependencies not ready and does not install them (the orchestrator deps phase owns installation).',
|
|
210
|
-
...report.reasons,
|
|
211
|
-
],
|
|
212
|
-
userAction:
|
|
213
|
-
'run the slot deps/prepare phase; standalone: `yarn setup:expo --no-build-ios --no-build-android` in the checkout, or `mm-harness launch <platform> --build` to install and build',
|
|
214
|
-
actions: [],
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
return report;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function computeMobileReadiness(
|
|
221
|
-
resolved: string,
|
|
222
|
-
options: MobileRuntimeDecisionOptions,
|
|
223
|
-
fast: boolean,
|
|
224
|
-
): Promise<MobileRuntimeDecisionReport> {
|
|
225
|
-
const rawDeps = depsCheck(resolved, {
|
|
226
|
-
productMarkers: mobileProductMarkers(options.platform),
|
|
227
|
-
});
|
|
228
|
-
// Presence is authoritative; mtime is only a hint. Without a recorded baseline
|
|
229
|
-
// the only way depsCheck reports 'stale' is mtime drift (a manifest newer than
|
|
230
|
-
// the install markers). In a managed checkout the orchestrator's git phase
|
|
231
|
-
// refreshes tracked-file mtimes on every sync while its deps phase leaves
|
|
232
|
-
// node_modules untouched, so "manifest newer than markers" is normal and NOT
|
|
233
|
-
// proof of drift — treating it as stale re-ran the full yarn setup inside every
|
|
234
|
-
// launch. Trust the installed node_modules and warn; genuine drift is caught by
|
|
235
|
-
// the recorded-baseline fingerprint when one exists (hasBaseline=true stays 'stale').
|
|
236
|
-
let deps: DepsCheck = rawDeps;
|
|
237
|
-
if (rawDeps.status === 'stale' && !rawDeps.hasBaseline) {
|
|
238
|
-
deps = { installed: rawDeps.installed, status: 'current', hasBaseline: false };
|
|
239
|
-
process.stderr.write(
|
|
240
|
-
'[runtime-decision] manifest is newer than install markers (no recorded baseline); ' +
|
|
241
|
-
'trusting installed node_modules — run yarn setup manually if deps truly changed.\n',
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
// Required-dep absence is a presence check (both modes): a top-level dependency
|
|
245
|
-
// can be absent from node_modules even when markers + fingerprint report
|
|
246
|
-
// 'current' (e.g. added in a branch merge but install not re-run). Catch at
|
|
247
|
-
// pre-flight before Metro discovers it during a long bundle run.
|
|
248
|
-
if (deps.status === 'current') {
|
|
249
|
-
const absent = missingRequiredDeps(resolved);
|
|
250
|
-
if (absent.length > 0) {
|
|
251
|
-
deps = { installed: deps.installed, status: 'partial', hasBaseline: deps.hasBaseline, missingProducts: absent };
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const metroLog = metroLogCheck(resolved, options.metroLog);
|
|
255
|
-
const metro = options.watcherPort
|
|
256
|
-
? await probeMetroPackager(options.watcherPort)
|
|
257
|
-
: { status: 'skipped' as const };
|
|
258
|
-
|
|
259
|
-
if (metroLog.status === 'ok') {
|
|
260
|
-
clearDecisionState(resolved, 'bundle-error-state.json');
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const checks = { deps, metroLog, metro };
|
|
264
|
-
|
|
265
|
-
if (deps.status === 'missing') {
|
|
266
|
-
return {
|
|
267
|
-
schemaVersion: 1,
|
|
268
|
-
adapter: 'mobile',
|
|
269
|
-
target: resolved,
|
|
270
|
-
decision: 'install',
|
|
271
|
-
reasonCode: 'deps-missing',
|
|
272
|
-
reasons: ['Dependencies are not installed (no yarn install-state markers).'],
|
|
273
|
-
checks,
|
|
274
|
-
actions: installActions(resolved),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (deps.status === 'partial') {
|
|
279
|
-
const missing = deps.missingProducts?.join(', ') ?? 'product markers';
|
|
280
|
-
return {
|
|
281
|
-
schemaVersion: 1,
|
|
282
|
-
adapter: 'mobile',
|
|
283
|
-
target: resolved,
|
|
284
|
-
decision: 'install',
|
|
285
|
-
reasonCode: 'deps-partial',
|
|
286
|
-
reasons: [`node_modules tree is incomplete (missing: ${missing}).`],
|
|
287
|
-
checks,
|
|
288
|
-
actions: installActions(resolved),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (deps.status === 'stale') {
|
|
293
|
-
return {
|
|
294
|
-
schemaVersion: 1,
|
|
295
|
-
adapter: 'mobile',
|
|
296
|
-
target: resolved,
|
|
297
|
-
decision: 'install',
|
|
298
|
-
reasonCode: 'deps-stale',
|
|
299
|
-
reasons: ['package.json/yarn.lock changed since the recorded install baseline.'],
|
|
300
|
-
checks,
|
|
301
|
-
actions: installActions(resolved),
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const metroLogAbs = resolveMetroLog(resolved, options.metroLog);
|
|
306
|
-
const metroLogText =
|
|
307
|
-
metroLogAbs && !isMetroLogStale(resolved, metroLogAbs)
|
|
308
|
-
? fs.readFileSync(metroLogAbs, 'utf8')
|
|
309
|
-
: '';
|
|
310
|
-
const staleNativeModule = supersededErrorCapture(metroLogText, {
|
|
311
|
-
errorPattern: NATIVE_MODULE_STALE,
|
|
312
|
-
okPattern: BUNDLE_OK,
|
|
313
|
-
});
|
|
314
|
-
if (staleNativeModule) {
|
|
315
|
-
return {
|
|
316
|
-
schemaVersion: 1,
|
|
317
|
-
adapter: 'mobile',
|
|
318
|
-
target: resolved,
|
|
319
|
-
decision: 'launch',
|
|
320
|
-
reasonCode: 'native-module-stale',
|
|
321
|
-
reasons: [
|
|
322
|
-
`Installed dev client is missing native module "${staleNativeModule}" (JS deps are current). Rebuild the native app (yarn start:ios / yarn start:android or --preflight-mode rebuild-native).`,
|
|
323
|
-
'Metro may report a successful bundle while the installed dev client binary predates the native Nitro module link.',
|
|
324
|
-
],
|
|
325
|
-
checks,
|
|
326
|
-
actions: [
|
|
327
|
-
{ id: 'rebuild-native-dev-client' },
|
|
328
|
-
...launchActions(resolved, true),
|
|
329
|
-
],
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (metroLog.status === 'errors') {
|
|
334
|
-
const depsSatisfied = deps.status === 'current';
|
|
335
|
-
if (
|
|
336
|
-
depsSatisfied
|
|
337
|
-
&& (metroLog.reason === 'stale-bundle-error'
|
|
338
|
-
|| metroLog.reason === 'bundle-error')
|
|
339
|
-
) {
|
|
340
|
-
const excerpt = metroLog.excerpt ?? '';
|
|
341
|
-
const persistent = evaluatePersistentBundleError(resolved, excerpt);
|
|
342
|
-
if (persistent.blocked) {
|
|
343
|
-
return {
|
|
344
|
-
schemaVersion: 1,
|
|
345
|
-
adapter: 'mobile',
|
|
346
|
-
target: resolved,
|
|
347
|
-
decision: 'blocked',
|
|
348
|
-
reasonCode: 'bundle-error-persistent',
|
|
349
|
-
reasons: [
|
|
350
|
-
'Metro bundle keeps failing with the same error after a cache-cleared relaunch was already suggested; fix the bundle error in app code before retrying recipe up.',
|
|
351
|
-
...(excerpt ? [excerpt] : []),
|
|
352
|
-
],
|
|
353
|
-
checks,
|
|
354
|
-
actions: [],
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
return {
|
|
358
|
-
schemaVersion: 1,
|
|
359
|
-
adapter: 'mobile',
|
|
360
|
-
target: resolved,
|
|
361
|
-
decision: 'launch',
|
|
362
|
-
reasonCode: metroLog.reason === 'stale-bundle-error' ? 'metro-stale-log' : 'bundle-errors-recoverable',
|
|
363
|
-
reasons: [
|
|
364
|
-
metroLog.reason === 'stale-bundle-error'
|
|
365
|
-
? 'Metro log shows prior bundle failures but cited modules are installed; restart Metro with a cleared cache.'
|
|
366
|
-
: 'Metro bundle is failing with current deps; restart Metro (clear cache) before relaunching the dev client.',
|
|
367
|
-
...(excerpt ? [excerpt] : []),
|
|
368
|
-
],
|
|
369
|
-
checks,
|
|
370
|
-
actions: launchActions(resolved, true),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
return {
|
|
374
|
-
schemaVersion: 1,
|
|
375
|
-
adapter: 'mobile',
|
|
376
|
-
target: resolved,
|
|
377
|
-
decision: 'install',
|
|
378
|
-
reasonCode: metroLog.reason === 'unresolved-module' ? 'deps-unresolved-module' : 'bundle-errors',
|
|
379
|
-
reasons: [
|
|
380
|
-
metroLog.reason === 'unresolved-module'
|
|
381
|
-
? 'Metro cannot resolve modules that are absent from node_modules; run yarn setup.'
|
|
382
|
-
: 'Metro bundle is failing; reinstall deps or clear Metro cache before retrying.',
|
|
383
|
-
...(metroLog.excerpt ? [metroLog.excerpt] : []),
|
|
384
|
-
],
|
|
385
|
-
checks,
|
|
386
|
-
actions: installActions(resolved),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (metro.status === 'down') {
|
|
391
|
-
return {
|
|
392
|
-
schemaVersion: 1,
|
|
393
|
-
adapter: 'mobile',
|
|
394
|
-
target: resolved,
|
|
395
|
-
decision: 'launch',
|
|
396
|
-
reasonCode: 'metro-down',
|
|
397
|
-
reasons: ['Metro is not reachable; start Metro and launch the dev client.'],
|
|
398
|
-
checks,
|
|
399
|
-
actions: launchActions(resolved),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (metroLog.status === 'bundling' || metroLog.status === 'no-log') {
|
|
404
|
-
return {
|
|
405
|
-
schemaVersion: 1,
|
|
406
|
-
adapter: 'mobile',
|
|
407
|
-
target: resolved,
|
|
408
|
-
decision: 'launch',
|
|
409
|
-
reasonCode: metroLog.status === 'bundling' ? 'bundle-in-progress' : 'runtime-cold',
|
|
410
|
-
reasons: [
|
|
411
|
-
metroLog.status === 'bundling'
|
|
412
|
-
? 'Metro bundle is still in progress; launch or wait for a successful bundle.'
|
|
413
|
-
: 'No successful Metro bundle recorded; start Metro and launch the dev client.',
|
|
414
|
-
],
|
|
415
|
-
checks,
|
|
416
|
-
actions: launchActions(resolved),
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (metro.status === 'up' && metroLog.status === 'ok') {
|
|
421
|
-
// Metro health alone is not runtime readiness: the dev client may be
|
|
422
|
-
// installed but not running on the target device (launch must launch).
|
|
423
|
-
// Probe the device before declaring ready; a healthy Metro with a dead
|
|
424
|
-
// app relaunches the client without touching Metro.
|
|
425
|
-
if (!appRunningOnDevice(options.platform)) {
|
|
426
|
-
return {
|
|
427
|
-
schemaVersion: 1,
|
|
428
|
-
adapter: 'mobile',
|
|
429
|
-
target: resolved,
|
|
430
|
-
decision: 'launch',
|
|
431
|
-
reasonCode: 'app-not-running',
|
|
432
|
-
reasons: ['Metro is healthy but the dev client is not running on the target device; launching it.'],
|
|
433
|
-
checks,
|
|
434
|
-
actions: launchActions(resolved),
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
return {
|
|
438
|
-
schemaVersion: 1,
|
|
439
|
-
adapter: 'mobile',
|
|
440
|
-
target: resolved,
|
|
441
|
-
decision: 'ready',
|
|
442
|
-
reasonCode: 'healthy',
|
|
443
|
-
reasons: ['Dependencies are current, Metro reports a successful bundle, and the dev client is running. Verify bridge before replay.'],
|
|
444
|
-
checks,
|
|
445
|
-
actions: [],
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
schemaVersion: 1,
|
|
451
|
-
adapter: 'mobile',
|
|
452
|
-
target: resolved,
|
|
453
|
-
decision: 'launch',
|
|
454
|
-
reasonCode: 'runtime-unverified',
|
|
455
|
-
reasons: ['Dependencies are current; start or refresh Metro + dev client before replay.'],
|
|
456
|
-
checks,
|
|
457
|
-
actions: launchActions(resolved),
|
|
458
|
-
};
|
|
459
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
// Mobile surface: delegates to the existing mobile readiness/port plumbing.
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { recipeRuntimePath, runnerDir } from '../../paths.ts';
|
|
6
|
-
import { resolveMobileSlotPorts } from '../slot-ports.ts';
|
|
7
|
-
import { mobileRuntimeStatus } from './prepare.ts';
|
|
8
|
-
import { hasRunwayProvisionBaseline, provisionRunwayMobile } from './provision.ts';
|
|
9
|
-
import type {
|
|
10
|
-
AdapterDevServerStop,
|
|
11
|
-
AdapterLogSource,
|
|
12
|
-
AdapterRuntimeStatus,
|
|
13
|
-
AdapterRunwayProvisionResult,
|
|
14
|
-
AdapterSurface,
|
|
15
|
-
} from '../surface.ts';
|
|
16
|
-
|
|
17
|
-
export const mobileSurface: AdapterSurface = {
|
|
18
|
-
adapter: 'mobile',
|
|
19
|
-
headless: false,
|
|
20
|
-
|
|
21
|
-
resolveSlotPorts(target: string): void {
|
|
22
|
-
resolveMobileSlotPorts(target);
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
async runtimeStatus(target: string): Promise<AdapterRuntimeStatus> {
|
|
26
|
-
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : undefined;
|
|
27
|
-
const report = await mobileRuntimeStatus(target, { watcherPort });
|
|
28
|
-
const runwayProvisioned = hasRunwayProvisionBaseline(target);
|
|
29
|
-
const depsPending = runwayProvisioned && report.checks?.deps?.status !== 'current';
|
|
30
|
-
return {
|
|
31
|
-
decision: depsPending ? 'launch' : report.decision,
|
|
32
|
-
reasonCode: depsPending ? 'app-installed-deps-pending' : report.reasonCode,
|
|
33
|
-
reasons: depsPending
|
|
34
|
-
? ['Runway app is installed; JavaScript dependencies are pending until dispatch-time launch.']
|
|
35
|
-
: report.reasons,
|
|
36
|
-
deps: runwayProvisioned && report.checks?.deps?.status !== 'current' ? 'pending' : report.checks?.deps?.status,
|
|
37
|
-
devServer: { label: 'metro', status: report.checks?.metro?.status ?? 'unprobed' },
|
|
38
|
-
};
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
runwayProvision: {
|
|
42
|
-
async run(target, options): Promise<AdapterRunwayProvisionResult> {
|
|
43
|
-
return provisionRunwayMobile(target, options) as Promise<AdapterRunwayProvisionResult>;
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
devServer: {
|
|
48
|
-
describe: () => 'Metro dev server',
|
|
49
|
-
stop(target: string): AdapterDevServerStop {
|
|
50
|
-
const leaf = path.join(runnerDir, 'adapters', 'mobile', 'stop-metro.sh');
|
|
51
|
-
const args = ['--target', target];
|
|
52
|
-
if (process.env.WATCHER_PORT) args.push('--port', process.env.WATCHER_PORT);
|
|
53
|
-
const result = spawnSync('bash', [leaf, ...args], { encoding: 'utf8' });
|
|
54
|
-
const status = result.status ?? 1;
|
|
55
|
-
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim();
|
|
56
|
-
const summary = status === 0
|
|
57
|
-
? `stopped Metro dev server for ${target}`
|
|
58
|
-
: `failed to stop Metro for ${target}`;
|
|
59
|
-
return { kind: 'stopped', status, summary, output };
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
logSources(target: string): AdapterLogSource[] {
|
|
64
|
-
return [{ label: 'metro', path: recipeRuntimePath(target, 'metro.log') }];
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
hints: {
|
|
68
|
-
launch: 'mm-harness launch ios',
|
|
69
|
-
relaunch: 'mm-harness launch ios',
|
|
70
|
-
},
|
|
71
|
-
};
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// Slot-port + dev-server process plumbing shared by the per-platform surfaces.
|
|
2
|
-
// Re-homed here (out of commands/launch.ts) so the surface implementations own
|
|
3
|
-
// port/device resolution and the extension watcher-stop without launch.ts and
|
|
4
|
-
// the surface registry forming an import cycle.
|
|
5
|
-
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
|
|
10
|
-
import { readRuntimeContextField, resolveRuntimeContextPath } from '../harness.ts';
|
|
11
|
-
import { recipeRuntimeDir, runnerDir } from '../paths.ts';
|
|
12
|
-
|
|
13
|
-
// Apply KEY=VALUE lines from slot resolution to process.env.
|
|
14
|
-
// overwrite=true → pool/context match, always overrides existing env.
|
|
15
|
-
// overwrite=false → formula match, only fills vars that are unset.
|
|
16
|
-
export function applyKVLines(output: string, overwrite: boolean): void {
|
|
17
|
-
for (const line of output.split('\n')) {
|
|
18
|
-
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
19
|
-
if (!m) continue;
|
|
20
|
-
const [, key, val] = m;
|
|
21
|
-
switch (key) {
|
|
22
|
-
case 'WATCHER_PORT':
|
|
23
|
-
if (overwrite || !process.env['WATCHER_PORT']) {
|
|
24
|
-
process.env['WATCHER_PORT'] = val;
|
|
25
|
-
process.env['METRO_PORT'] = val;
|
|
26
|
-
process.env['RECIPE_WATCHER_PORT'] = val;
|
|
27
|
-
}
|
|
28
|
-
break;
|
|
29
|
-
case 'IOS_SIMULATOR':
|
|
30
|
-
if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
|
|
31
|
-
break;
|
|
32
|
-
case 'SLOT_ID':
|
|
33
|
-
if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
|
|
34
|
-
break;
|
|
35
|
-
case 'CDP_PORT':
|
|
36
|
-
if (overwrite || !process.env['CDP_PORT']) {
|
|
37
|
-
process.env['CDP_PORT'] = val;
|
|
38
|
-
process.env['RECIPE_CDP_PORT'] = val;
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function sourceResolve(fn: string, target: string): string {
|
|
46
|
-
const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
|
|
47
|
-
try {
|
|
48
|
-
return execFileSync('bash', ['-c', `source "${resolveScript}" && ${fn} "${target}"`], {
|
|
49
|
-
encoding: 'utf8',
|
|
50
|
-
timeout: 5000,
|
|
51
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
52
|
-
});
|
|
53
|
-
} catch {
|
|
54
|
-
return '';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Resolve mobile slot port/simulator: the slot context the orchestrator wrote
|
|
59
|
-
// into the checkout wins first, then the farmslot pool (both overwrite env),
|
|
60
|
-
// then the slot-suffix formula (only fills unset vars). Called before explicit
|
|
61
|
-
// CLI flag overrides so flags always win at the top.
|
|
62
|
-
export function resolveMobileSlotPorts(target: string): void {
|
|
63
|
-
// The checkout's own runtime context is authoritative — it names the exact
|
|
64
|
-
// simulator/port this slot was prepared with, surviving pool renames.
|
|
65
|
-
const ctxOut = sourceResolve('resolve_mobile_runtime_context', target);
|
|
66
|
-
if (ctxOut.trim()) {
|
|
67
|
-
applyKVLines(ctxOut, true);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// Pool match always wins — overwrite whatever is in the environment.
|
|
71
|
-
const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
|
|
72
|
-
if (poolOut.trim()) {
|
|
73
|
-
applyKVLines(poolOut, true);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
// Formula match only fills unset vars (never overrides explicit env/pool).
|
|
77
|
-
const defOut = sourceResolve('resolve_mobile_slot_defaults', target);
|
|
78
|
-
if (defOut.trim()) applyKVLines(defOut, false);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Resolve extension slot ports the same way as mobile: the checkout's runtime
|
|
82
|
-
// context first (cdpPort/devServerPort written by the orchestrator's prepare),
|
|
83
|
-
// then the farmslot pool, then the directory-suffix formula (fills unset only).
|
|
84
|
-
export function resolveExtensionSlotPorts(target: string): void {
|
|
85
|
-
// The prepared checkout's context OVERWRITES inherited env (same authority as
|
|
86
|
-
// mobile's context/pool resolution): a stale CDP_PORT from the shell must not
|
|
87
|
-
// hijack the slot's browser. Explicit CLI flags are applied after and win.
|
|
88
|
-
const contextPath = resolveRuntimeContextPath(target);
|
|
89
|
-
const cdp = readRuntimeContextField(contextPath, 'cdpPort');
|
|
90
|
-
if (cdp) {
|
|
91
|
-
process.env['CDP_PORT'] = cdp;
|
|
92
|
-
process.env['RECIPE_CDP_PORT'] = cdp;
|
|
93
|
-
}
|
|
94
|
-
const dev = readRuntimeContextField(contextPath, 'devServerPort');
|
|
95
|
-
if (dev) {
|
|
96
|
-
process.env['WATCHER_PORT'] = dev;
|
|
97
|
-
process.env['RECIPE_WATCHER_PORT'] = dev;
|
|
98
|
-
}
|
|
99
|
-
if (process.env['CDP_PORT']) return;
|
|
100
|
-
const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
|
|
101
|
-
if (poolOut.trim()) {
|
|
102
|
-
applyKVLines(poolOut, true);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const defOut = sourceResolve('resolve_default_extension_ports', target);
|
|
106
|
-
if (defOut.trim()) applyKVLines(defOut, false);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Kill the harness-owned webpack watcher for this checkout: pid file first,
|
|
110
|
-
// then a ps-scan for orphans (argv or lsof-cwd match), TERM then KILL. Scoped
|
|
111
|
-
// to the target checkout — watchers of other slots are never touched. Returns
|
|
112
|
-
// how many processes were signalled so callers can state the outcome.
|
|
113
|
-
export function stopExtensionWatcher(target: string): number {
|
|
114
|
-
const runtimeAbs = path.join(target, recipeRuntimeDir());
|
|
115
|
-
const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
|
|
116
|
-
let signalled = 0;
|
|
117
|
-
try {
|
|
118
|
-
const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
|
|
119
|
-
if (/^\d+$/u.test(pid)) {
|
|
120
|
-
try { process.kill(Number(pid), 'SIGTERM'); signalled += 1; } catch { /* already dead */ }
|
|
121
|
-
}
|
|
122
|
-
fs.rmSync(webpackPidFile, { force: true });
|
|
123
|
-
} catch { /* no pid file */ }
|
|
124
|
-
// Scan for any remaining orphan webpack/yarn-start processes in this checkout.
|
|
125
|
-
try {
|
|
126
|
-
const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
|
|
127
|
-
const orphanPids: number[] = [];
|
|
128
|
-
for (const line of psOut.split('\n')) {
|
|
129
|
-
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
130
|
-
if (!match) continue;
|
|
131
|
-
const [, pidStr, cmd] = match;
|
|
132
|
-
const isWatcher =
|
|
133
|
-
cmd.includes('yarn start') ||
|
|
134
|
-
cmd.includes('webpack --watch') ||
|
|
135
|
-
cmd.includes('development/webpack/launch.ts --watch');
|
|
136
|
-
if (!isWatcher) continue;
|
|
137
|
-
if (cmd.includes(target)) {
|
|
138
|
-
orphanPids.push(Number(pidStr));
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
// lsof cwd fallback for processes that don't embed the path in argv.
|
|
142
|
-
try {
|
|
143
|
-
const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
|
|
144
|
-
encoding: 'utf8',
|
|
145
|
-
timeout: 2000,
|
|
146
|
-
});
|
|
147
|
-
if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
|
|
148
|
-
orphanPids.push(Number(pidStr));
|
|
149
|
-
}
|
|
150
|
-
} catch { /* lsof unavailable or permission denied */ }
|
|
151
|
-
}
|
|
152
|
-
if (orphanPids.length > 0) {
|
|
153
|
-
for (const pid of orphanPids) {
|
|
154
|
-
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
155
|
-
}
|
|
156
|
-
// Brief pause then force-kill survivors.
|
|
157
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
158
|
-
for (const pid of orphanPids) {
|
|
159
|
-
try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
160
|
-
}
|
|
161
|
-
signalled += orphanPids.length;
|
|
162
|
-
}
|
|
163
|
-
} catch { /* ps not available */ }
|
|
164
|
-
return signalled;
|
|
165
|
-
}
|