@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
package/src/commands/launch.ts
DELETED
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
// launch — start the app (Metro/build + surface), auto-ensure the runtime overlay,
|
|
2
|
-
// and heal transport within bounds. Owns launch grammar, build-tier selection,
|
|
3
|
-
// quick-launch change detection, and the --json contract. Device orchestration
|
|
4
|
-
// composes adapters/ scripts directly; --verify reuses the overlay `live`
|
|
5
|
-
// sequence via handleHarness.
|
|
6
|
-
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import fs from 'node:fs';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
|
|
11
|
-
import { color } from '../cli-color.ts';
|
|
12
|
-
import { handleHarness } from '../harness.ts';
|
|
13
|
-
import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
14
|
-
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
15
|
-
import { prepareMobile } from '../adapters/mobile/prepare.ts';
|
|
16
|
-
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
17
|
-
import { stopExtensionWatcher } from '../adapters/slot-ports.ts';
|
|
18
|
-
import {
|
|
19
|
-
ADAPTER_DETECT_NEXT,
|
|
20
|
-
EXIT,
|
|
21
|
-
flag,
|
|
22
|
-
parseFlags,
|
|
23
|
-
resolveAdapter,
|
|
24
|
-
spawnScript,
|
|
25
|
-
spawnScriptStreaming,
|
|
26
|
-
str,
|
|
27
|
-
targetOf,
|
|
28
|
-
usageOut,
|
|
29
|
-
} from './shared.ts';
|
|
30
|
-
import type { DeviceAdapter, ScriptResult } from './shared.ts';
|
|
31
|
-
import {
|
|
32
|
-
RECOVERY_CODE,
|
|
33
|
-
checkHealBounds,
|
|
34
|
-
ensureOverlay,
|
|
35
|
-
newHealState,
|
|
36
|
-
parseHeal,
|
|
37
|
-
} from '../heal-bounds.ts';
|
|
38
|
-
import type { HealState } from '../heal-bounds.ts';
|
|
39
|
-
|
|
40
|
-
interface LaunchFailure {
|
|
41
|
-
code: string;
|
|
42
|
-
message: string;
|
|
43
|
-
recoverable: boolean;
|
|
44
|
-
userAction?: string;
|
|
45
|
-
exitCode: number;
|
|
46
|
-
// The original script output, surfaced verbatim (never replaced by the note).
|
|
47
|
-
originalError?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const LAUNCH_BOOLEANS = new Set([
|
|
51
|
-
'build',
|
|
52
|
-
'watch',
|
|
53
|
-
'verify',
|
|
54
|
-
'sidepanel',
|
|
55
|
-
'fullscreen',
|
|
56
|
-
'runway',
|
|
57
|
-
'json',
|
|
58
|
-
'jsonStream',
|
|
59
|
-
'yes',
|
|
60
|
-
]);
|
|
61
|
-
|
|
62
|
-
export async function handleLaunch(argv: string[]): Promise<number> {
|
|
63
|
-
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
64
|
-
const json = flag(options, 'json');
|
|
65
|
-
const target = targetOf(options);
|
|
66
|
-
|
|
67
|
-
const heal = parseHeal(options, 'auto');
|
|
68
|
-
if (typeof heal !== 'string') return usageOut(json, 'launch', heal.error, 'use --heal off|infra-only|auto');
|
|
69
|
-
|
|
70
|
-
// Grammar: mobile requires an ios|android positional (also disambiguates the
|
|
71
|
-
// adapter); extension takes no positional; core is headless (teaching error).
|
|
72
|
-
const posToken = positional[0];
|
|
73
|
-
const mobileTargetToken = posToken === 'ios' || posToken === 'android' ? posToken : undefined;
|
|
74
|
-
const platformFlag = str(options, 'platform');
|
|
75
|
-
const platformMobileToken =
|
|
76
|
-
platformFlag === 'ios' || platformFlag === 'android' ? platformFlag : undefined;
|
|
77
|
-
const mobileTarget = mobileTargetToken ?? platformMobileToken;
|
|
78
|
-
const adapterHint: MetaMaskRecipeAdapter | undefined = mobileTarget ? 'mobile' : undefined;
|
|
79
|
-
const adapter = resolveAdapter(options, target, adapterHint);
|
|
80
|
-
|
|
81
|
-
if (!adapter) {
|
|
82
|
-
return usageOut(json, 'launch', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
83
|
-
}
|
|
84
|
-
if (adapter === 'core') {
|
|
85
|
-
return usageOut(json, 'launch', 'core is headless; there is nothing to launch.', 'mm-harness verify');
|
|
86
|
-
}
|
|
87
|
-
if (adapter === 'mobile' && !mobileTarget) {
|
|
88
|
-
return usageOut(
|
|
89
|
-
json,
|
|
90
|
-
'launch',
|
|
91
|
-
'target is required for mobile.',
|
|
92
|
-
'mm-harness launch ios or mm-harness launch android',
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const wantBuild = flag(options, 'build');
|
|
97
|
-
const wantVerify = flag(options, 'verify');
|
|
98
|
-
const wantWatch = flag(options, 'watch');
|
|
99
|
-
const wantRunway = flag(options, 'runway');
|
|
100
|
-
|
|
101
|
-
// --runway is mobile-only.
|
|
102
|
-
if (wantRunway && adapter !== 'mobile') {
|
|
103
|
-
return usageOut(json, 'launch', 'runway is mobile-only.', 'drop --runway for the extension');
|
|
104
|
-
}
|
|
105
|
-
// --fullscreen is the explicit override when both display flags are passed.
|
|
106
|
-
const displayMode =
|
|
107
|
-
flag(options, 'sidepanel') && !flag(options, 'fullscreen') ? 'sidepanel' : 'fullscreen';
|
|
108
|
-
|
|
109
|
-
// Env-gap flags (docs/CLI-SPEC.md Part 4): --device / --cdp-port / --watcher-port.
|
|
110
|
-
// Resolution is flag > env > config/default: setting the env from the flag makes
|
|
111
|
-
// the flag win; an absent flag leaves the agent/CI env untouched; the porcelain
|
|
112
|
-
// owns config/default. Ports must be numeric (teaching error otherwise).
|
|
113
|
-
for (const portFlag of ['cdpPort', 'watcherPort'] as const) {
|
|
114
|
-
const value = str(options, portFlag);
|
|
115
|
-
if (value !== undefined && !/^\d+$/u.test(value)) {
|
|
116
|
-
return usageOut(
|
|
117
|
-
json,
|
|
118
|
-
'launch',
|
|
119
|
-
`--${portFlag === 'cdpPort' ? 'cdp-port' : 'watcher-port'} must be numeric (got: ${value}).`,
|
|
120
|
-
`pass a numeric port, e.g. --${portFlag === 'cdpPort' ? 'cdp-port 6663' : 'watcher-port 8081'}`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
125
|
-
|
|
126
|
-
const tier = wantVerify ? 'verify' : wantBuild ? 'build' : 'quick';
|
|
127
|
-
const state = newHealState();
|
|
128
|
-
|
|
129
|
-
// Quick-launch change detection: refuse a quick relaunch on a stale native
|
|
130
|
-
// build (native inputs changed since the recorded build baseline).
|
|
131
|
-
if (tier === 'quick' && nativeInputsChanged(target, adapter)) {
|
|
132
|
-
return usageOut(
|
|
133
|
-
json,
|
|
134
|
-
'launch',
|
|
135
|
-
'native build inputs changed since last build.',
|
|
136
|
-
`mm-harness launch ${adapter === 'mobile' ? `${mobileTarget} ` : ''}--build`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Auto-ensure overlay (install phase).
|
|
141
|
-
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
142
|
-
if (!ensured.ok) {
|
|
143
|
-
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
144
|
-
code: 'OVERLAY_INSTALL_FAILED',
|
|
145
|
-
message: ensured.error ?? 'overlay install failed',
|
|
146
|
-
recoverable: false,
|
|
147
|
-
exitCode: EXIT.infra,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// --verify absorbs the retired `live` sequence: overlay ensured above → launch
|
|
152
|
-
// → CDP/bridge poll → smoke verify, all via the existing overlay orchestration.
|
|
153
|
-
if (tier === 'verify') {
|
|
154
|
-
const liveArgs = ['live', '--adapter', adapter, '--target', target];
|
|
155
|
-
if (adapter === 'mobile' && mobileTarget) liveArgs.push('--platform', mobileTarget);
|
|
156
|
-
if (json) liveArgs.push('--json');
|
|
157
|
-
const code = await handleHarness(liveArgs);
|
|
158
|
-
// handleHarness owns its own summary in --json mode; the overlay mutations we
|
|
159
|
-
// performed are surfaced to stderr in human mode above.
|
|
160
|
-
return code;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Compose the launch tier: mobile runs in-process; extension spawns adapters/ scripts.
|
|
164
|
-
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
165
|
-
|
|
166
|
-
if (attempt.status === 0) {
|
|
167
|
-
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Bounds are ALWAYS enforced regardless of --heal.
|
|
171
|
-
if (heal === 'off') {
|
|
172
|
-
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
173
|
-
code: 'LAUNCH_FAILED',
|
|
174
|
-
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed (healing off)`,
|
|
175
|
-
recoverable: false,
|
|
176
|
-
exitCode: EXIT.infra,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
// Healing bounds (shared module — policy, not per-command). null = ok to proceed.
|
|
180
|
-
const bound = checkHealBounds(target, attempt.output, state);
|
|
181
|
-
if (bound !== null) {
|
|
182
|
-
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
183
|
-
code: bound.code,
|
|
184
|
-
message: bound.message,
|
|
185
|
-
recoverable: false,
|
|
186
|
-
userAction: bound.userAction,
|
|
187
|
-
exitCode: bound.exitCode,
|
|
188
|
-
originalError: bound.originalError,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Infra recovery: attempt the SAME launch exactly once (Metro restarts /
|
|
193
|
-
// Chrome reopens). Push recoveryCode BEFORE the attempt so any subsequent
|
|
194
|
-
// checkHealBounds call sees it and refuses a second loop.
|
|
195
|
-
const recoveryCode = RECOVERY_CODE[adapter];
|
|
196
|
-
state.attemptedRecoveries.push(recoveryCode);
|
|
197
|
-
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
198
|
-
if (attempt.status === 0) {
|
|
199
|
-
state.recovered.push(recoveryCode);
|
|
200
|
-
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
201
|
-
}
|
|
202
|
-
// Second attempt failed — same-recovery-twice bound hit.
|
|
203
|
-
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
204
|
-
code: 'SAME_RECOVERY_TWICE',
|
|
205
|
-
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed again after ${recoveryCode} recovery — refusing to loop.`,
|
|
206
|
-
recoverable: false,
|
|
207
|
-
exitCode: EXIT.bounded,
|
|
208
|
-
originalError: attempt.output.trim() || undefined,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Map the env-gap flags onto the env vars the leaf scripts read. For mobile, slot
|
|
213
|
-
// port/simulator defaults are resolved first (pool-wins, then formula-fills-empty)
|
|
214
|
-
// so that the harness respects slot isolation; explicit CLI flags applied below
|
|
215
|
-
// win over everything. Extension: flag > env > config/default.
|
|
216
|
-
function applyLaunchEnvOverrides(
|
|
217
|
-
options: Record<string, string | boolean>,
|
|
218
|
-
adapter: DeviceAdapter,
|
|
219
|
-
mobileTarget: string | undefined,
|
|
220
|
-
target: string,
|
|
221
|
-
): void {
|
|
222
|
-
// Slot isolation: resolve ports/device from the checkout's own slot context,
|
|
223
|
-
// the farmslot pool, or the directory-suffix formula BEFORE applying explicit
|
|
224
|
-
// flags — extension needs this as much as mobile (CDP_PORT), so neither
|
|
225
|
-
// adapter hard-fails on a value its slot already knows. The surface owns the
|
|
226
|
-
// per-platform resolution; this command never branches on adapter for it.
|
|
227
|
-
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
228
|
-
|
|
229
|
-
const device = str(options, 'device');
|
|
230
|
-
if (device && adapter === 'mobile') {
|
|
231
|
-
if (mobileTarget === 'android') {
|
|
232
|
-
process.env.ADB_SERIAL = device;
|
|
233
|
-
process.env.ANDROID_SERIAL = device;
|
|
234
|
-
process.env.ANDROID_DEVICE = device;
|
|
235
|
-
} else {
|
|
236
|
-
process.env.IOS_SIMULATOR = device;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const cdpPort = str(options, 'cdpPort');
|
|
240
|
-
if (cdpPort) {
|
|
241
|
-
process.env.CDP_PORT = cdpPort;
|
|
242
|
-
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
243
|
-
}
|
|
244
|
-
const watcherPort = str(options, 'watcherPort');
|
|
245
|
-
if (watcherPort) {
|
|
246
|
-
process.env.WATCHER_PORT = watcherPort;
|
|
247
|
-
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
248
|
-
process.env.METRO_PORT = watcherPort;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Native build inputs changed vs the recorded build baseline (git diff heuristic,
|
|
253
|
-
// mirrors the farm-layer native-diff). No baseline recorded → cannot be stale →
|
|
254
|
-
// allow the quick launch.
|
|
255
|
-
function nativeInputsChanged(target: string, adapter: MetaMaskRecipeAdapter): boolean {
|
|
256
|
-
const baselineFile = recipeRuntimePath(target, '.last-build-ref');
|
|
257
|
-
let baseline: string;
|
|
258
|
-
try {
|
|
259
|
-
baseline = fs.readFileSync(baselineFile, 'utf8').trim();
|
|
260
|
-
} catch {
|
|
261
|
-
return false;
|
|
262
|
-
}
|
|
263
|
-
if (!baseline) return false;
|
|
264
|
-
const paths =
|
|
265
|
-
adapter === 'mobile'
|
|
266
|
-
? ['ios', 'android', 'package.json', 'yarn.lock']
|
|
267
|
-
: ['package.json', 'yarn.lock', 'webpack.config.js'];
|
|
268
|
-
try {
|
|
269
|
-
const diff = execFileSync('git', ['-C', target, 'diff', '--name-only', baseline, '--', ...paths], {
|
|
270
|
-
encoding: 'utf8',
|
|
271
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
272
|
-
}).trim();
|
|
273
|
-
return diff.length > 0;
|
|
274
|
-
} catch {
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Execute the launch composition for (adapter, tier). Mobile runs prepareMobile()
|
|
280
|
-
// in-process (no shell orchestrator); extension arms spawn adapters/ scripts directly.
|
|
281
|
-
//
|
|
282
|
-
// mobile quick|build → prepareMobile() in-process → leaf scripts
|
|
283
|
-
// extension rebuild → kill watcher + clear rebuild.log + live.sh --start-watch
|
|
284
|
-
// extension build → adapters/extension/start-watch.sh
|
|
285
|
-
// extension watch → adapters/extension/start-watch.sh
|
|
286
|
-
async function executeComposition(
|
|
287
|
-
adapter: DeviceAdapter,
|
|
288
|
-
mobileTarget: string | undefined,
|
|
289
|
-
tier: 'quick' | 'build' | 'verify',
|
|
290
|
-
wantWatch: boolean,
|
|
291
|
-
target: string,
|
|
292
|
-
json: boolean,
|
|
293
|
-
): Promise<ScriptResult> {
|
|
294
|
-
if (adapter === 'mobile') {
|
|
295
|
-
const platform = mobileTarget ?? 'ios';
|
|
296
|
-
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : undefined;
|
|
297
|
-
const preflightMode = tier === 'build' ? 'auto' : 'fast';
|
|
298
|
-
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Extension rebuild (quick relaunch): kill watcher → clear rebuild.log → live.sh --start-watch.
|
|
302
|
-
if (!wantWatch && tier === 'quick') {
|
|
303
|
-
return extensionRebuild(target, json);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Extension build or watch: full webpack compile via start-watch.sh. Streams
|
|
307
|
-
// live — a webpack compile is minutes long and must never look hung.
|
|
308
|
-
const startWatchSh = path.join(runnerDir, 'adapters/extension/start-watch.sh');
|
|
309
|
-
const watchArgs = ['--target', target];
|
|
310
|
-
if (process.env.WATCHER_PORT) watchArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
311
|
-
console.error(`→ extension ${wantWatch ? 'watch' : 'build'} — webpack :${process.env.WATCHER_PORT ?? 'default'} (output streams below)`);
|
|
312
|
-
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Extension rebuild: kill the harness-owned webpack watcher (pid file + ps-scan
|
|
316
|
-
// for orphans), clear the rebuild log, then drive the installed overlay's
|
|
317
|
-
// live.sh --start-watch and tee its output into the rebuild log.
|
|
318
|
-
async function extensionRebuild(target: string, json: boolean): Promise<ScriptResult> {
|
|
319
|
-
const runtimeDirRel = recipeRuntimeDir();
|
|
320
|
-
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
321
|
-
const rebuildLog = path.join(runtimeAbs, 'rebuild.log');
|
|
322
|
-
|
|
323
|
-
// E1a: Kill harness-owned watcher via pid file, then ps-scan for orphans.
|
|
324
|
-
stopExtensionWatcher(target);
|
|
325
|
-
|
|
326
|
-
// E1c: Clear the rebuild log (directory must exist for tee).
|
|
327
|
-
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
328
|
-
fs.writeFileSync(rebuildLog, '');
|
|
329
|
-
|
|
330
|
-
const liveScript = recipeHarnessPath(target, 'extension', 'scripts', 'live.sh');
|
|
331
|
-
const liveArgs = ['--target', target, '--start-watch'];
|
|
332
|
-
if (process.env.CDP_PORT) liveArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
333
|
-
if (process.env.WATCHER_PORT) liveArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
334
|
-
// Streams live: the watcher restart + Chromium boot runs for minutes and the
|
|
335
|
-
// command must show progress immediately, not a silent prompt.
|
|
336
|
-
console.error(`→ extension quick relaunch — webpack :${process.env.WATCHER_PORT ?? 'default'} · CDP :${process.env.CDP_PORT ?? 'default'} (output streams below)`);
|
|
337
|
-
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
338
|
-
// Tee live.sh output into rebuild.log for post-mortem diagnosis.
|
|
339
|
-
if (result.output) {
|
|
340
|
-
try { fs.appendFileSync(rebuildLog, result.output); } catch { /* best-effort */ }
|
|
341
|
-
}
|
|
342
|
-
return result;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Apply the extension display mode AFTER the base launch succeeded, then emit
|
|
346
|
-
// the pass summary. Fullscreen = the plain rebuild/build/watch launch above
|
|
347
|
-
// (default popup/home window). Sidepanel additionally drives the
|
|
348
|
-
// chrome.sidePanel mechanism via adapters/extension/sidepanel-toggle.sh.
|
|
349
|
-
// --sidepanel and --fullscreen compose DIFFERENT script calls and the display
|
|
350
|
-
// mode actually reaches the browser. Mobile has no side panel; the flag is a
|
|
351
|
-
// no-op there.
|
|
352
|
-
function finishLaunch(
|
|
353
|
-
json: boolean,
|
|
354
|
-
adapter: DeviceAdapter,
|
|
355
|
-
mobileTarget: string | undefined,
|
|
356
|
-
tier: string,
|
|
357
|
-
displayMode: string,
|
|
358
|
-
target: string,
|
|
359
|
-
state: HealState,
|
|
360
|
-
): number {
|
|
361
|
-
if (adapter === 'extension' && displayMode === 'sidepanel') {
|
|
362
|
-
const sidepanelSh = path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh');
|
|
363
|
-
const sidepanelArgs = ['open'];
|
|
364
|
-
if (process.env.CDP_PORT) sidepanelArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
365
|
-
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
|
|
366
|
-
if (sidepanel.status !== 0) {
|
|
367
|
-
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
368
|
-
code: 'SIDEPANEL_OPEN_FAILED',
|
|
369
|
-
message: 'app launched but opening the side panel failed.',
|
|
370
|
-
recoverable: false,
|
|
371
|
-
userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? '<CDP_PORT>'}`,
|
|
372
|
-
exitCode: EXIT.runtime,
|
|
373
|
-
originalError: sidepanel.output.trim() || undefined,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
return launchPass(json, adapter, mobileTarget, tier, displayMode, state);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function launchPass(
|
|
381
|
-
json: boolean,
|
|
382
|
-
adapter: DeviceAdapter,
|
|
383
|
-
mobileTarget: string | undefined,
|
|
384
|
-
tier: string,
|
|
385
|
-
displayMode: string,
|
|
386
|
-
state: HealState,
|
|
387
|
-
): number {
|
|
388
|
-
if (json) {
|
|
389
|
-
console.log(
|
|
390
|
-
JSON.stringify(
|
|
391
|
-
{
|
|
392
|
-
schemaVersion: 1,
|
|
393
|
-
command: 'launch',
|
|
394
|
-
adapter,
|
|
395
|
-
platform: mobileTarget ?? (adapter === 'extension' ? displayMode : null),
|
|
396
|
-
tier,
|
|
397
|
-
status: 'pass',
|
|
398
|
-
phase: 'launch',
|
|
399
|
-
recovered: state.recovered,
|
|
400
|
-
mutations: state.mutations,
|
|
401
|
-
exitCode: EXIT.ok,
|
|
402
|
-
},
|
|
403
|
-
null,
|
|
404
|
-
2,
|
|
405
|
-
),
|
|
406
|
-
);
|
|
407
|
-
} else {
|
|
408
|
-
// The summary must say what actually happened where: the target device,
|
|
409
|
-
// what the tier meant, and that the app+bridge are up — "(quick) ready"
|
|
410
|
-
// alone reads as a no-op even when a launch occurred.
|
|
411
|
-
const device =
|
|
412
|
-
adapter === 'mobile'
|
|
413
|
-
? process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || 'booted device'
|
|
414
|
-
: displayMode;
|
|
415
|
-
const tierNote = tier === 'quick' ? 'quick relaunch, no native build' : tier;
|
|
416
|
-
const devNote = process.env.MM_HARNESS_BIN ? ` ${color('dim', '[dev: MM_HARNESS_BIN]')}` : '';
|
|
417
|
-
console.error(
|
|
418
|
-
`${color('ok', '✓')} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} — ${color('ok', String(device))} · ${tierNote} · app + bridge ready${devNote}`,
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
return EXIT.ok;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function launchFail(
|
|
425
|
-
json: boolean,
|
|
426
|
-
adapter: DeviceAdapter,
|
|
427
|
-
mobileTarget: string | undefined,
|
|
428
|
-
tier: string,
|
|
429
|
-
state: HealState,
|
|
430
|
-
failure: LaunchFailure,
|
|
431
|
-
): number {
|
|
432
|
-
if (json) {
|
|
433
|
-
console.log(
|
|
434
|
-
JSON.stringify(
|
|
435
|
-
{
|
|
436
|
-
schemaVersion: 1,
|
|
437
|
-
command: 'launch',
|
|
438
|
-
adapter,
|
|
439
|
-
platform: mobileTarget ?? null,
|
|
440
|
-
tier,
|
|
441
|
-
status: 'fail',
|
|
442
|
-
phase: state.attemptedRecoveries.length ? 'recover' : 'launch',
|
|
443
|
-
recovered: state.recovered,
|
|
444
|
-
mutations: state.mutations,
|
|
445
|
-
recoverable: failure.recoverable,
|
|
446
|
-
attemptedRecoveries: state.attemptedRecoveries,
|
|
447
|
-
exitCode: failure.exitCode,
|
|
448
|
-
error: {
|
|
449
|
-
code: failure.code,
|
|
450
|
-
message: failure.message,
|
|
451
|
-
retryable: failure.recoverable,
|
|
452
|
-
userAction: failure.userAction ?? null,
|
|
453
|
-
// The classification note is in `message`; the REAL failure text lives
|
|
454
|
-
// here verbatim so it is never lost.
|
|
455
|
-
originalError: failure.originalError ?? null,
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
null,
|
|
459
|
-
2,
|
|
460
|
-
),
|
|
461
|
-
);
|
|
462
|
-
} else {
|
|
463
|
-
console.error(
|
|
464
|
-
`✗ launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ''} failed\n ${failure.message}` +
|
|
465
|
-
(failure.originalError ? `\n --- original failure ---\n${failure.originalError}` : '') +
|
|
466
|
-
(failure.userAction ? `\n Next: ${failure.userAction}` : ''),
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
return failure.exitCode;
|
|
470
|
-
}
|
package/src/commands/logs.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
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 { runnerDir } from '../paths.ts';
|
|
9
|
-
import { getAdapterSurface } from '../adapters/surface.ts';
|
|
10
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
11
|
-
|
|
12
|
-
const LOGS_BOOLEANS = new Set(['full', 'json']);
|
|
13
|
-
|
|
14
|
-
export async function handleLogs(argv: string[]): Promise<number> {
|
|
15
|
-
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
16
|
-
const json = flag(options, 'json');
|
|
17
|
-
const target = targetOf(options);
|
|
18
|
-
const adapter = resolveAdapter(options, target);
|
|
19
|
-
|
|
20
|
-
if (!adapter) {
|
|
21
|
-
return usageOut(json, 'logs', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
22
|
-
}
|
|
23
|
-
const surface = getAdapterSurface(adapter);
|
|
24
|
-
if (surface.headless) {
|
|
25
|
-
return usageOut(json, 'logs', 'core is headless; it has no dev server logs.', surface.hints.launch);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Source selection is platform-scoped: the valid names and the default come
|
|
29
|
-
// from THIS adapter's dev-server logs (mobile: metro; extension: webpack), plus
|
|
30
|
-
// the app-log source. No adapter's vocabulary is hardcoded here — every
|
|
31
|
-
// non-headless adapter provides at least one log source (headless core returned
|
|
32
|
-
// above), so the default is that adapter's first source.
|
|
33
|
-
const logSources = surface.logSources(target);
|
|
34
|
-
const sourceLabels = logSources.map((entry) => entry.label);
|
|
35
|
-
const defaultSource = sourceLabels[0];
|
|
36
|
-
const source = str(options, 'source') ?? defaultSource;
|
|
37
|
-
const validSources = [...sourceLabels, 'app'];
|
|
38
|
-
if (!validSources.includes(source)) {
|
|
39
|
-
return usageOut(json, 'logs', `--source must be one of: ${validSources.join(', ')}.`, `mm-harness logs --source ${defaultSource}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
|
|
43
|
-
// read from RECIPE_LOG_EVENTS. Resolution is flag > env > default: the flag wins
|
|
44
|
-
// by setting the env; an absent flag leaves the agent/CI env alone.
|
|
45
|
-
const events = str(options, 'events');
|
|
46
|
-
if (events !== undefined) {
|
|
47
|
-
if (!/^\d+$/u.test(events)) {
|
|
48
|
-
return usageOut(json, 'logs', `--events must be numeric (got: ${events}).`, 'mm-harness logs --events 20');
|
|
49
|
-
}
|
|
50
|
-
process.env.RECIPE_LOG_EVENTS = events;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Nothing running → teaching error pointing at launch. A dev-server log file is
|
|
54
|
-
// the signal that the dev server has been started for this checkout; the surface
|
|
55
|
-
// owns which files a platform writes (mobile: metro.log; extension: webpack +
|
|
56
|
-
// watcher + rebuild logs). A `--source` naming a specific dev-server log is
|
|
57
|
-
// preferred; otherwise the most-relevant existing candidate is tailed.
|
|
58
|
-
const requested = logSources.find((entry) => entry.label === source);
|
|
59
|
-
const ordered = requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
|
|
60
|
-
const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
|
|
61
|
-
if (!logFile) {
|
|
62
|
-
const names = logSources.map((entry) => path.basename(entry.path)).join(' / ');
|
|
63
|
-
return usageOut(
|
|
64
|
-
json,
|
|
65
|
-
'logs',
|
|
66
|
-
`nothing running for this checkout (no ${names}).`,
|
|
67
|
-
surface.hints.launch,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const full = flag(options, 'full');
|
|
72
|
-
if (full) {
|
|
73
|
-
// Raw follow: the human explicitly wants unfiltered output.
|
|
74
|
-
const result = spawnScript('tail', ['-n', '+1', '-F', logFile], target, json);
|
|
75
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Compact mode: last N events + pointer to full log.
|
|
79
|
-
const logTui = path.join(runnerDir, 'adapters/shared/log-tui.mjs');
|
|
80
|
-
const eventCount = process.env.RECIPE_LOG_EVENTS ?? '20';
|
|
81
|
-
const uiMode = process.env.RECIPE_LOG_UI_MODE ?? 'compact';
|
|
82
|
-
const result = spawnScript(
|
|
83
|
-
process.execPath,
|
|
84
|
-
[logTui, 'tail', '--log', logFile, '--events', eventCount, '--follow', '--mode', uiMode],
|
|
85
|
-
target,
|
|
86
|
-
json,
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
if (json) {
|
|
90
|
-
console.log(
|
|
91
|
-
JSON.stringify(
|
|
92
|
-
{ schemaVersion: 1, command: 'logs', adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
93
|
-
null,
|
|
94
|
-
2,
|
|
95
|
-
),
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
99
|
-
}
|