@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/harness.ts
DELETED
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from './leaf-invoke.ts';
|
|
6
|
-
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from './paths.ts';
|
|
7
|
-
import type { MetaMaskRecipeAdapter } from './types.ts';
|
|
8
|
-
import { prepareMobile } from './adapters/mobile/prepare.ts';
|
|
9
|
-
|
|
10
|
-
// The `harness` command moves the recipe-harness skill's runner-locate + dispatch
|
|
11
|
-
// bash (resolve-runner-source.sh + recipe-harness.sh dispatch_adapter_action)
|
|
12
|
-
// into typed runner code. The skill resolved an *external* runner source; inside
|
|
13
|
-
// the runner the root is known, so "resolve runner root" collapses to runnerDir.
|
|
14
|
-
// Everything else — the per-subcommand/platform orchestration file mapping,
|
|
15
|
-
// candidate search order, installed-overlay/installed-script preference, the
|
|
16
|
-
// core delegate, extension runtime-context env, and verbatim arg passthrough —
|
|
17
|
-
// is reproduced exactly so the skill can become a thin caller.
|
|
18
|
-
|
|
19
|
-
type HarnessAction = 'install' | 'provision' | 'verify' | 'cleanup' | 'live';
|
|
20
|
-
|
|
21
|
-
const HARNESS_ACTIONS: readonly HarnessAction[] = ['install', 'provision', 'verify', 'cleanup', 'live'];
|
|
22
|
-
const ADAPTERS: readonly MetaMaskRecipeAdapter[] = ['mobile', 'extension', 'core'];
|
|
23
|
-
|
|
24
|
-
// Mirror of scripts/lib/cli-common.sh valid_adapter_action, restricted to the
|
|
25
|
-
// subcommands this command exposes (core has no app/live lifecycle).
|
|
26
|
-
function isValidAdapterAction(adapter: MetaMaskRecipeAdapter, action: HarnessAction): boolean {
|
|
27
|
-
if (adapter === 'core') return action === 'install' || action === 'provision' || action === 'verify' || action === 'cleanup';
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function harnessUsage(): void {
|
|
32
|
-
console.error(`mm-harness — install and validate the MetaMask recipe runtime in a checkout.
|
|
33
|
-
|
|
34
|
-
Run it from inside a MetaMask checkout; the platform (mobile | extension | core)
|
|
35
|
-
is auto-detected from the repo. Pass --platform only to override.
|
|
36
|
-
|
|
37
|
-
Commands (one copy-pasteable example each):
|
|
38
|
-
install Install the recipe harness runtime overlay into the checkout.
|
|
39
|
-
mm-harness install
|
|
40
|
-
provision Install the cached Runway mobile dev client (same path as install --runway).
|
|
41
|
-
mm-harness provision runway ios --adapter mobile
|
|
42
|
-
verify Check the harness/runtime is present and healthy (no app launch).
|
|
43
|
-
mm-harness verify
|
|
44
|
-
cleanup Remove the installed harness overlay and restore the checkout.
|
|
45
|
-
mm-harness cleanup
|
|
46
|
-
|
|
47
|
-
Options:
|
|
48
|
-
--platform <mobile|extension|core> Override auto-detection (alias: --adapter).
|
|
49
|
-
--target <repo> Checkout to operate on (default: current dir).
|
|
50
|
-
--json Machine-readable summary for agents/scripts.
|
|
51
|
-
-- <args> Forward the rest verbatim to the underlying
|
|
52
|
-
script, e.g. -- --platform ios --preflight-mode fast
|
|
53
|
-
(mobile) or -- --cdp-port 6665 (extension).
|
|
54
|
-
|
|
55
|
-
Examples:
|
|
56
|
-
mm-harness install # inside a checkout, auto-detected
|
|
57
|
-
mm-harness verify --platform extension
|
|
58
|
-
mm-harness cleanup --target ../metamask-mobile
|
|
59
|
-
|
|
60
|
-
core supports install, verify, and cleanup (headless; no live).`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function failureHint(adapter: MetaMaskRecipeAdapter, action: HarnessAction): string {
|
|
64
|
-
if (adapter === 'core') {
|
|
65
|
-
if (action === 'verify') {
|
|
66
|
-
return `Install the core harness first: mm-harness install --platform core`;
|
|
67
|
-
}
|
|
68
|
-
return 'Read the error above for the specific cause, then re-run this command.';
|
|
69
|
-
}
|
|
70
|
-
const helper = adapter === 'mobile' ? 'mm-harness launch ios # or launch android' : 'mm-harness launch';
|
|
71
|
-
return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// has_arg: matches an exact flag or its `flag=value` form.
|
|
75
|
-
function hasArg(args: string[], needle: string): boolean {
|
|
76
|
-
return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// arg_value: value for `flag value` or `flag=value`, else undefined.
|
|
80
|
-
function argValue(args: string[], needle: string): string | undefined {
|
|
81
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
82
|
-
if (args[i] === needle) return args[i + 1];
|
|
83
|
-
if (args[i].startsWith(`${needle}=`)) return args[i].slice(needle.length + 1);
|
|
84
|
-
}
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function shellQuote(value: string): string {
|
|
89
|
-
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function isAdapter(value: string | undefined): value is MetaMaskRecipeAdapter {
|
|
93
|
-
return value === 'mobile' || value === 'extension' || value === 'core';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
interface ParsedHarnessArgs {
|
|
97
|
-
// Kept as raw string so invalid values (e.g. --adapter garbage) are not silently
|
|
98
|
-
// cast to MetaMaskRecipeAdapter; narrowed via isAdapter() at the validation site.
|
|
99
|
-
adapter?: string;
|
|
100
|
-
json: boolean;
|
|
101
|
-
forward: string[];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Split the adapter selector and --json out of the remaining argv so they are not
|
|
105
|
-
// forwarded to the orchestration script (the .sh/.mjs scripts reject unknown
|
|
106
|
-
// flags; Mobile's live/verify use --platform ios|android, so only the first
|
|
107
|
-
// --platform whose value is an adapter is consumed here and a later --platform is
|
|
108
|
-
// passed through). `--` forces everything after it to passthrough.
|
|
109
|
-
function parseHarnessArgs(args: string[]): ParsedHarnessArgs {
|
|
110
|
-
const forward: string[] = [];
|
|
111
|
-
let adapter: string | undefined;
|
|
112
|
-
let json = false;
|
|
113
|
-
let separator = false;
|
|
114
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
115
|
-
const arg = args[i];
|
|
116
|
-
// POSIX: everything after `--` is literal; check separator FIRST so that
|
|
117
|
-
// --json appearing after `--` is forwarded verbatim to the delegate.
|
|
118
|
-
if (separator) {
|
|
119
|
-
forward.push(arg);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (arg === '--') {
|
|
123
|
-
separator = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (arg === '--json') {
|
|
127
|
-
json = true;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (arg === '--adapter' || arg === '--platform') {
|
|
131
|
-
const value = args[i + 1];
|
|
132
|
-
if (arg === '--adapter') {
|
|
133
|
-
adapter = value;
|
|
134
|
-
i += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (adapter === undefined && isAdapter(value)) {
|
|
138
|
-
adapter = value;
|
|
139
|
-
i += 1;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
forward.push(arg);
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (arg.startsWith('--adapter=')) {
|
|
146
|
-
adapter = arg.slice('--adapter='.length);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (arg.startsWith('--platform=')) {
|
|
150
|
-
const value = arg.slice('--platform='.length);
|
|
151
|
-
if (adapter === undefined && isAdapter(value)) {
|
|
152
|
-
adapter = value;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
forward.push(arg);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
forward.push(arg);
|
|
159
|
-
}
|
|
160
|
-
return { adapter, json, forward };
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Mirror of scripts/lib/cli-common.sh detect_adapter for the auto-detect path.
|
|
164
|
-
// Exported so `doctor` shares the exact same detect-from-target logic as the
|
|
165
|
-
// overlay commands (verify/install/cleanup) — one auto-detect story, no drift.
|
|
166
|
-
// Both escapes from a failed repo-type detection: where to run it (checkout/target)
|
|
167
|
-
// and how to force it (adapter). A single source so every detect-failure teaches
|
|
168
|
-
// the same reachable next step.
|
|
169
|
-
export const ADAPTER_DETECT_NEXT =
|
|
170
|
-
'cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>';
|
|
171
|
-
|
|
172
|
-
export function detectAdapter(target: string): MetaMaskRecipeAdapter | undefined {
|
|
173
|
-
let remote = '';
|
|
174
|
-
try {
|
|
175
|
-
remote = execFileSync('git', ['-C', target, 'config', '--get', 'remote.origin.url'], {
|
|
176
|
-
encoding: 'utf8',
|
|
177
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
178
|
-
}).trim();
|
|
179
|
-
} catch {
|
|
180
|
-
remote = '';
|
|
181
|
-
}
|
|
182
|
-
if (remote.includes('metamask-extension')) return 'extension';
|
|
183
|
-
if (remote.includes('metamask-mobile')) return 'mobile';
|
|
184
|
-
|
|
185
|
-
const exists = (rel: string): boolean => fs.existsSync(path.join(target, rel));
|
|
186
|
-
const isDir = (rel: string): boolean => {
|
|
187
|
-
try {
|
|
188
|
-
return fs.statSync(path.join(target, rel)).isDirectory();
|
|
189
|
-
} catch {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
if (exists('development/skills-sync.ts') || (isDir('ui') && isDir('app/scripts'))) return 'extension';
|
|
195
|
-
if (exists('scripts/skills-sync.mts') || (isDir('ios') && isDir('android') && isDir('app/core'))) return 'mobile';
|
|
196
|
-
if (isDir('packages/perps-controller') && exists('yarn.lock')) return 'core';
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Resolve the agentic-runtime.json path for a checkout: the RECIPE_RUNTIME_CONTEXT
|
|
201
|
-
// override wins, else the per-checkout runtime dir. Single source shared by the
|
|
202
|
-
// extension pre-dispatch hydration and doctor's runtime-context report.
|
|
203
|
-
export function resolveRuntimeContextPath(target: string): string {
|
|
204
|
-
return process.env.RECIPE_RUNTIME_CONTEXT ?? path.join(target, recipeRuntimeDir(), 'agentic-runtime.json');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// read_runtime_context_field: dotted lookup returning the value only when it is a
|
|
208
|
-
// present, non-empty scalar; undefined when the file/field is missing.
|
|
209
|
-
export function readRuntimeContextField(contextPath: string, field: string): string | undefined {
|
|
210
|
-
let data: unknown;
|
|
211
|
-
try {
|
|
212
|
-
data = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
213
|
-
} catch {
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
let node: unknown = data;
|
|
217
|
-
for (const key of field.split('.')) {
|
|
218
|
-
if (node === undefined || node === null || typeof node !== 'object') return undefined;
|
|
219
|
-
node = (node as Record<string, unknown>)[key];
|
|
220
|
-
}
|
|
221
|
-
if (node === undefined || node === null || node === '') return undefined;
|
|
222
|
-
return String(node);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Port of recipe-harness.sh's extension pre-dispatch block: hydrate runtime env
|
|
226
|
-
// from agentic-runtime.json and append --cdp-port / --prepare-cmd to the args
|
|
227
|
-
// exactly as the skill did (extension launch/live/verify; here live/verify).
|
|
228
|
-
function applyExtensionRuntimeEnv(target: string, action: HarnessAction, args: string[]): string[] {
|
|
229
|
-
const contextPath = resolveRuntimeContextPath(target);
|
|
230
|
-
const contextExists = fs.existsSync(contextPath);
|
|
231
|
-
|
|
232
|
-
if (contextExists) {
|
|
233
|
-
process.env.RECIPE_RUNTIME_CONTEXT = contextPath;
|
|
234
|
-
if (!process.env.RECIPE_SLOT_ID) {
|
|
235
|
-
const slotId = readRuntimeContextField(contextPath, 'slotId');
|
|
236
|
-
if (slotId) process.env.RECIPE_SLOT_ID = slotId;
|
|
237
|
-
}
|
|
238
|
-
if (!process.env.RECIPE_HARNESS_EXTENSION_ID) {
|
|
239
|
-
const extensionId = readRuntimeContextField(contextPath, 'extensionId');
|
|
240
|
-
if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
|
|
241
|
-
}
|
|
242
|
-
if (process.env.RECIPE_RUNTIME_START_APPROVED === undefined) {
|
|
243
|
-
const approved = readRuntimeContextField(contextPath, 'runtimeStart.approved');
|
|
244
|
-
if (approved === 'true' || approved === 'True' || approved === '1') process.env.RECIPE_RUNTIME_START_APPROVED = '1';
|
|
245
|
-
else if (approved === 'false' || approved === 'False' || approved === '0') process.env.RECIPE_RUNTIME_START_APPROVED = '0';
|
|
246
|
-
}
|
|
247
|
-
if (!process.env.RECIPE_RUNTIME_START_CMD) {
|
|
248
|
-
const command = readRuntimeContextField(contextPath, 'runtimeStart.command');
|
|
249
|
-
if (command) process.env.RECIPE_RUNTIME_START_CMD = command;
|
|
250
|
-
}
|
|
251
|
-
if (!process.env.RECIPE_RUNTIME_READY_URL) {
|
|
252
|
-
const readyUrl = readRuntimeContextField(contextPath, 'runtimeStart.readyUrl');
|
|
253
|
-
if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
let result = [...args];
|
|
258
|
-
if (!hasArg(result, '--cdp-port')) {
|
|
259
|
-
const contextPort = contextExists ? readRuntimeContextField(contextPath, 'cdpPort') : undefined;
|
|
260
|
-
const cdpPort = contextPort ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
261
|
-
if (cdpPort) {
|
|
262
|
-
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
263
|
-
process.env.CDP_PORT = cdpPort;
|
|
264
|
-
result = [...result, '--cdp-port', cdpPort];
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (action === 'live' && !hasArg(result, '--prepare-cmd')) {
|
|
268
|
-
if (process.env.RECIPE_RUNTIME_START_APPROVED === '1' && process.env.RECIPE_RUNTIME_START_CMD) {
|
|
269
|
-
result = [...result, '--prepare-cmd', process.env.RECIPE_RUNTIME_START_CMD];
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return result;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// runner_executable_entry / runner_file_entry: first candidate that exists (as an
|
|
276
|
-
// executable, or as any file) wins; otherwise the first candidate is returned so
|
|
277
|
-
// the exec error names it.
|
|
278
|
-
function resolveEntry(base: string, candidates: string[], mode: 'exec' | 'file'): string {
|
|
279
|
-
for (const candidate of candidates) {
|
|
280
|
-
const full = path.join(base, candidate);
|
|
281
|
-
try {
|
|
282
|
-
const stat = fs.statSync(full);
|
|
283
|
-
if (!stat.isFile()) continue;
|
|
284
|
-
if (mode === 'file' || (stat.mode & 0o111) !== 0) return full;
|
|
285
|
-
} catch {
|
|
286
|
-
// missing candidate; try the next one
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
return path.join(base, candidates[0]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Content of an installed overlay's runner source pointer, if present. Command
|
|
293
|
-
// substitution in the skill stripped trailing newlines, so trim here too.
|
|
294
|
-
// Validates the path exists so stale stamps (e.g. from a renamed package dir)
|
|
295
|
-
// fall back to the current runnerDir rather than producing ENOENT at cleanup.
|
|
296
|
-
function installedRunnerSource(target: string, adapter: MetaMaskRecipeAdapter): string | undefined {
|
|
297
|
-
const pointer = path.join(recipeHarnessPath(target, adapter), 'runner', '.runner-source');
|
|
298
|
-
if (!fs.existsSync(pointer)) return undefined;
|
|
299
|
-
const value = fs.readFileSync(pointer, 'utf8').trim();
|
|
300
|
-
if (!value || !fs.existsSync(value)) return undefined;
|
|
301
|
-
return value;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function isExecutable(file: string): boolean {
|
|
305
|
-
try {
|
|
306
|
-
const stat = fs.statSync(file);
|
|
307
|
-
return stat.isFile() && (stat.mode & 0o111) !== 0;
|
|
308
|
-
} catch {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
interface HarnessDispatch {
|
|
314
|
-
command: string;
|
|
315
|
-
prefixArgs: string[];
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const INJECT_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
319
|
-
mobile: { entry: 'adapters/mobile/inject.sh', fallback: 'scripts/inject-mobile-harness.sh' },
|
|
320
|
-
extension: { entry: 'adapters/extension/inject.mjs', fallback: 'scripts/inject-extension-harness.mjs' },
|
|
321
|
-
core: { entry: 'adapters/core/inject.sh', fallback: 'scripts/inject-core-harness.sh' },
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const CLEANUP_CANDIDATES: Record<MetaMaskRecipeAdapter, { entry: string; fallback: string }> = {
|
|
325
|
-
mobile: { entry: 'adapters/mobile/cleanup.sh', fallback: 'scripts/cleanup-mobile-harness.sh' },
|
|
326
|
-
extension: { entry: 'adapters/extension/cleanup.mjs', fallback: 'scripts/cleanup-extension-harness.mjs' },
|
|
327
|
-
core: { entry: 'adapters/core/cleanup.sh', fallback: 'scripts/cleanup-core-harness.sh' },
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// Resolve the exact command + fixed prefix args to exec, mirroring
|
|
331
|
-
// recipe-harness.sh dispatch_adapter_action. forwardArgs are appended by the
|
|
332
|
-
// caller. Returns a string message for the one fail-fast case (missing core
|
|
333
|
-
// delegate) that the skill handled inline.
|
|
334
|
-
function resolveHarnessDispatch(
|
|
335
|
-
adapter: MetaMaskRecipeAdapter,
|
|
336
|
-
action: HarnessAction,
|
|
337
|
-
target: string,
|
|
338
|
-
): HarnessDispatch | { error: string } {
|
|
339
|
-
if (action === 'install') {
|
|
340
|
-
const { entry, fallback } = INJECT_CANDIDATES[adapter];
|
|
341
|
-
if (adapter === 'extension') {
|
|
342
|
-
return { command: process.execPath, prefixArgs: [resolveEntry(runnerDir, [entry, fallback], 'file')] };
|
|
343
|
-
}
|
|
344
|
-
// Shell leaves are invoked through bash; file existence is the only requirement.
|
|
345
|
-
return { command: resolveEntry(runnerDir, [entry, fallback], 'file'), prefixArgs: [] };
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (action === 'cleanup') {
|
|
349
|
-
const { entry, fallback } = CLEANUP_CANDIDATES[adapter];
|
|
350
|
-
const base = installedRunnerSource(target, adapter) ?? runnerDir;
|
|
351
|
-
if (adapter === 'extension') {
|
|
352
|
-
return { command: process.execPath, prefixArgs: [resolveEntry(base, [entry, fallback], 'file')] };
|
|
353
|
-
}
|
|
354
|
-
return { command: resolveEntry(base, [entry, fallback], 'file'), prefixArgs: [] };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (adapter === 'core' && action === 'verify') {
|
|
358
|
-
const delegate = path.join(recipeHarnessPath(target, 'core'), 'runner', 'bin', 'mm-harness');
|
|
359
|
-
if (!isExecutable(delegate)) {
|
|
360
|
-
return {
|
|
361
|
-
error:
|
|
362
|
-
`core recipe harness not installed: missing delegate ${delegate}.\n` +
|
|
363
|
-
`Next: mm-harness install --platform core --target ${target}`,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
// --target is carried in forwardArgs (injected when the caller omits it), so
|
|
367
|
-
// the delegate still receives `doctor --adapter core --target <t>`.
|
|
368
|
-
return { command: delegate, prefixArgs: ['doctor', '--adapter', 'core'] };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// mobile/extension live|verify: prefer the installed helper (any mode), else
|
|
372
|
-
// the runner's own adapters/scripts candidate. Bash invocation makes +x irrelevant.
|
|
373
|
-
const installedScript = path.join(recipeHarnessPath(target, adapter), 'scripts', `${action}.sh`);
|
|
374
|
-
if (fs.existsSync(installedScript)) {
|
|
375
|
-
return { command: installedScript, prefixArgs: [] };
|
|
376
|
-
}
|
|
377
|
-
const command = resolveEntry(
|
|
378
|
-
runnerDir,
|
|
379
|
-
[`adapters/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
|
|
380
|
-
'file',
|
|
381
|
-
);
|
|
382
|
-
return { command, prefixArgs: [] };
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Mobile live: prepare device in-process then run the verify leaf.
|
|
386
|
-
// Replaces the retired live.sh → launch.sh → mm-harness-mobile-prepare chain.
|
|
387
|
-
// Control flows down only: TS calls prepareMobile(), then spawns verify.sh.
|
|
388
|
-
async function handleMobileLive(
|
|
389
|
-
target: string,
|
|
390
|
-
forwardArgs: string[],
|
|
391
|
-
json: boolean,
|
|
392
|
-
autoDetected: boolean,
|
|
393
|
-
): Promise<number> {
|
|
394
|
-
const platform = argValue(forwardArgs, '--platform') ?? 'ios';
|
|
395
|
-
const watcherPortStr = argValue(forwardArgs, '--watcher-port') ?? process.env.WATCHER_PORT;
|
|
396
|
-
const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : undefined;
|
|
397
|
-
const start = Date.now();
|
|
398
|
-
|
|
399
|
-
if (!json) {
|
|
400
|
-
const detected = autoDetected ? ', auto-detected' : '';
|
|
401
|
-
console.error(`→ live (mobile${detected}) — target: ${target}`);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const prepResult = await prepareMobile(target, { platform, json, watcherPort });
|
|
405
|
-
if (prepResult.status !== 0) {
|
|
406
|
-
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
407
|
-
if (json) {
|
|
408
|
-
console.log(
|
|
409
|
-
harnessSummary('live', 'mobile', target, 'fail', prepResult.status, autoDetected, {
|
|
410
|
-
code: 'MOBILE_PREPARE_FAILED',
|
|
411
|
-
message: `mobile prepare failed (exit ${prepResult.status})`,
|
|
412
|
-
userAction: failureHint('mobile', 'live'),
|
|
413
|
-
}),
|
|
414
|
-
);
|
|
415
|
-
} else {
|
|
416
|
-
console.error(
|
|
417
|
-
`✗ live mobile failed (exit ${prepResult.status}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
return prepResult.status;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Verify leaf: --no-auto-start because launch already started the app.
|
|
424
|
-
const installedVerify = path.join(recipeHarnessPath(target, 'mobile'), 'scripts', 'verify.sh');
|
|
425
|
-
const verifySh = fs.existsSync(installedVerify)
|
|
426
|
-
? installedVerify
|
|
427
|
-
: resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'file');
|
|
428
|
-
|
|
429
|
-
const verifyArgs = hasArg(forwardArgs, '--no-auto-start')
|
|
430
|
-
? [...forwardArgs]
|
|
431
|
-
: ['--no-auto-start', ...forwardArgs];
|
|
432
|
-
|
|
433
|
-
if (shellLeafMissing(verifySh)) {
|
|
434
|
-
const message = missingShellLeafMessage(verifySh);
|
|
435
|
-
if (json) {
|
|
436
|
-
console.log(
|
|
437
|
-
harnessSummary('live', 'mobile', target, 'fail', 1, autoDetected, {
|
|
438
|
-
code: 'HARNESS_SPAWN_FAILED',
|
|
439
|
-
message,
|
|
440
|
-
userAction: failureHint('mobile', 'live'),
|
|
441
|
-
}),
|
|
442
|
-
);
|
|
443
|
-
} else {
|
|
444
|
-
console.error(`✗ ${message}`);
|
|
445
|
-
}
|
|
446
|
-
return 1;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
|
|
450
|
-
const result = spawnSync(verifyBin, verifySpawnArgs, {
|
|
451
|
-
stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
|
|
452
|
-
env: process.env,
|
|
453
|
-
});
|
|
454
|
-
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
455
|
-
const exitCode = result.status ?? 1;
|
|
456
|
-
|
|
457
|
-
if (json) {
|
|
458
|
-
console.log(
|
|
459
|
-
harnessSummary(
|
|
460
|
-
'live',
|
|
461
|
-
'mobile',
|
|
462
|
-
target,
|
|
463
|
-
exitCode === 0 ? 'pass' : 'fail',
|
|
464
|
-
exitCode,
|
|
465
|
-
autoDetected,
|
|
466
|
-
exitCode === 0
|
|
467
|
-
? undefined
|
|
468
|
-
: { code: 'MOBILE_VERIFY_FAILED', message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint('mobile', 'live') },
|
|
469
|
-
),
|
|
470
|
-
);
|
|
471
|
-
} else if (exitCode === 0) {
|
|
472
|
-
console.error(`✓ live mobile passed (${elapsed}s)`);
|
|
473
|
-
} else {
|
|
474
|
-
console.error(
|
|
475
|
-
`✗ live mobile failed (exit ${exitCode}, ${elapsed}s)\n ${failureHint('mobile', 'live')}`,
|
|
476
|
-
);
|
|
477
|
-
}
|
|
478
|
-
return exitCode;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export async function handleHarness(argv: string[]): Promise<number> {
|
|
482
|
-
const action = argv[0];
|
|
483
|
-
if (!action || action === '-h' || action === '--help') {
|
|
484
|
-
harnessUsage();
|
|
485
|
-
return action ? 0 : 2;
|
|
486
|
-
}
|
|
487
|
-
if (!HARNESS_ACTIONS.includes(action as HarnessAction)) {
|
|
488
|
-
harnessUsage();
|
|
489
|
-
console.error(`unsupported harness subcommand: ${action}`);
|
|
490
|
-
return 2;
|
|
491
|
-
}
|
|
492
|
-
const harnessAction = action as HarnessAction;
|
|
493
|
-
|
|
494
|
-
const { adapter: parsedAdapter, json, forward } = parseHarnessArgs(argv.slice(1));
|
|
495
|
-
const rawTarget = argValue(forward, '--target');
|
|
496
|
-
const target = path.resolve(rawTarget ?? process.cwd());
|
|
497
|
-
|
|
498
|
-
const adapter = parsedAdapter ?? detectAdapter(target);
|
|
499
|
-
// isAdapter() narrows adapter to MetaMaskRecipeAdapter for all code below.
|
|
500
|
-
if (!adapter || !isAdapter(adapter)) {
|
|
501
|
-
if (json) {
|
|
502
|
-
// parsedAdapter is string|undefined here; not yet validated as a real
|
|
503
|
-
// adapter, so pass undefined rather than forwarding a garbage string.
|
|
504
|
-
const detectError = adapter
|
|
505
|
-
? { code: 'UNSUPPORTED_PLATFORM', message: `unsupported platform: ${adapter}`, userAction: 'pass --adapter <mobile|extension|core> to specify a supported adapter' }
|
|
506
|
-
: { code: 'ADAPTER_DETECTION_FAILED', message: `could not detect the MetaMask repo type for ${target}`, userAction: ADAPTER_DETECT_NEXT };
|
|
507
|
-
console.log(harnessSummary(harnessAction, undefined, target, 'fail', 2, false, detectError));
|
|
508
|
-
} else {
|
|
509
|
-
harnessUsage();
|
|
510
|
-
console.error(
|
|
511
|
-
adapter
|
|
512
|
-
? `\n✗ unsupported platform: ${adapter}`
|
|
513
|
-
: `\n✗ could not detect the MetaMask repo type for ${target}\n Next: ${ADAPTER_DETECT_NEXT}`,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
return 2;
|
|
517
|
-
}
|
|
518
|
-
if (!isValidAdapterAction(adapter, harnessAction)) {
|
|
519
|
-
if (json) {
|
|
520
|
-
console.log(
|
|
521
|
-
harnessSummary(harnessAction, adapter, target, 'fail', 2, parsedAdapter === undefined, {
|
|
522
|
-
code: 'UNSUPPORTED_ACTION',
|
|
523
|
-
message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
|
|
524
|
-
userAction: 'run mm-harness doctor to see which actions are supported for this adapter',
|
|
525
|
-
}),
|
|
526
|
-
);
|
|
527
|
-
} else {
|
|
528
|
-
console.error(
|
|
529
|
-
`✗ ${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
return 2;
|
|
533
|
-
}
|
|
534
|
-
const autoDetected = parsedAdapter === undefined;
|
|
535
|
-
|
|
536
|
-
// Forward args carry --target so the orchestration script resolves the same
|
|
537
|
-
// checkout; inject the resolved path only when the caller omitted it.
|
|
538
|
-
let forwardArgs = hasArg(forward, '--target') ? [...forward] : ['--target', target, ...forward];
|
|
539
|
-
|
|
540
|
-
if (harnessAction === 'provision') {
|
|
541
|
-
return handleRunwayInstall(adapter, target, provisionRunwayForward(forward), json, 'provision');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (harnessAction === 'install' && hasArg(forward, '--runway')) {
|
|
545
|
-
return handleRunwayInstall(adapter, target, forward, json, 'install');
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (adapter === 'extension' && (harnessAction === 'live' || harnessAction === 'verify')) {
|
|
549
|
-
forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Mobile live is orchestrated in-process (prepareMobile → verify.sh leaf).
|
|
553
|
-
if (adapter === 'mobile' && harnessAction === 'live') {
|
|
554
|
-
return handleMobileLive(target, forwardArgs, json, autoDetected);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
|
|
558
|
-
if ('error' in dispatch) {
|
|
559
|
-
if (json) {
|
|
560
|
-
console.log(
|
|
561
|
-
harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
|
|
562
|
-
code: 'DISPATCH_UNAVAILABLE',
|
|
563
|
-
message: dispatch.error,
|
|
564
|
-
// The dispatch error embeds "Next: <hint>" — extract it so --json consumers
|
|
565
|
-
// get a clean programmatic escape without parsing the human message.
|
|
566
|
-
userAction: dispatch.error.split('\nNext: ')[1] ?? 'run mm-harness install to complete setup, then retry',
|
|
567
|
-
}),
|
|
568
|
-
);
|
|
569
|
-
} else {
|
|
570
|
-
console.error(`✗ ${harnessAction} ${adapter} failed\n ${dispatch.error.replace(/\n/gu, '\n ')}`);
|
|
571
|
-
}
|
|
572
|
-
return 1;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (!json) {
|
|
576
|
-
const detected = autoDetected ? ', auto-detected' : '';
|
|
577
|
-
console.error(`→ ${harnessAction} (${adapter}${detected}) — target: ${target}`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const start = Date.now();
|
|
581
|
-
|
|
582
|
-
// Shell leaves need bash invocation; bash exits 127 (not ENOENT) for missing files.
|
|
583
|
-
if (shellLeafMissing(dispatch.command)) {
|
|
584
|
-
const message = missingShellLeafMessage(dispatch.command);
|
|
585
|
-
if (json) {
|
|
586
|
-
console.log(
|
|
587
|
-
harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
|
|
588
|
-
code: 'HARNESS_SPAWN_FAILED',
|
|
589
|
-
message,
|
|
590
|
-
userAction: failureHint(adapter, harnessAction),
|
|
591
|
-
}),
|
|
592
|
-
);
|
|
593
|
-
} else {
|
|
594
|
-
console.error(`✗ ${message}`);
|
|
595
|
-
}
|
|
596
|
-
return 1;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const { bin: dispatchBin, args: dispatchArgs } = resolveLeafInvoke(dispatch.command, [
|
|
600
|
-
...dispatch.prefixArgs,
|
|
601
|
-
...forwardArgs,
|
|
602
|
-
]);
|
|
603
|
-
const result = spawnSync(dispatchBin, dispatchArgs, {
|
|
604
|
-
// Default: stream child output verbatim (byte-identical to the skill path).
|
|
605
|
-
// --json: route child output to our stderr so stdout carries only the summary.
|
|
606
|
-
stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
|
|
607
|
-
env: process.env,
|
|
608
|
-
});
|
|
609
|
-
const seconds = ((Date.now() - start) / 1000).toFixed(1);
|
|
610
|
-
|
|
611
|
-
if (result.error) {
|
|
612
|
-
if (json) {
|
|
613
|
-
console.log(
|
|
614
|
-
harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
|
|
615
|
-
code: 'HARNESS_SPAWN_FAILED',
|
|
616
|
-
message: `${harnessAction} ${adapter} could not start: ${result.error.message}`,
|
|
617
|
-
userAction: failureHint(adapter, harnessAction),
|
|
618
|
-
}),
|
|
619
|
-
);
|
|
620
|
-
} else {
|
|
621
|
-
console.error(`✗ ${harnessAction} ${adapter} could not start: ${result.error.message}\n ${failureHint(adapter, harnessAction)}`);
|
|
622
|
-
}
|
|
623
|
-
return 1;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const exitCode = result.status ?? 1;
|
|
627
|
-
if (json) {
|
|
628
|
-
console.log(
|
|
629
|
-
harnessSummary(
|
|
630
|
-
harnessAction,
|
|
631
|
-
adapter,
|
|
632
|
-
target,
|
|
633
|
-
exitCode === 0 ? 'pass' : 'fail',
|
|
634
|
-
exitCode,
|
|
635
|
-
autoDetected,
|
|
636
|
-
exitCode === 0
|
|
637
|
-
? undefined
|
|
638
|
-
: { code: 'HARNESS_FAILED', message: `${harnessAction} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(adapter, harnessAction) },
|
|
639
|
-
),
|
|
640
|
-
);
|
|
641
|
-
} else if (exitCode === 0) {
|
|
642
|
-
console.error(`✓ ${harnessAction} ${adapter} passed (${seconds}s)`);
|
|
643
|
-
} else {
|
|
644
|
-
console.error(`✗ ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)\n ${failureHint(adapter, harnessAction)}`);
|
|
645
|
-
}
|
|
646
|
-
return exitCode;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async function handleRunwayInstall(
|
|
650
|
-
adapter: MetaMaskRecipeAdapter,
|
|
651
|
-
target: string,
|
|
652
|
-
forward: string[],
|
|
653
|
-
json: boolean,
|
|
654
|
-
action: 'install' | 'provision',
|
|
655
|
-
): Promise<number> {
|
|
656
|
-
const { getAdapterSurface } = await import('./adapters/surface.ts');
|
|
657
|
-
const rerunCommand = action === 'provision'
|
|
658
|
-
? runwayProvisionRerunCommand(adapter, target, forward, json)
|
|
659
|
-
: runwayInstallRerunCommand(adapter, target, forward, json);
|
|
660
|
-
const surface = getAdapterSurface(adapter);
|
|
661
|
-
const runtimeDir = argValue(forward, '--runtime-dir');
|
|
662
|
-
const watcherPort = argValue(forward, '--watcher-port');
|
|
663
|
-
const result = await surface.runwayProvision.run(target, {
|
|
664
|
-
json,
|
|
665
|
-
platform: argValue(forward, '--platform') ?? 'ios',
|
|
666
|
-
branch: argValue(forward, '--branch'),
|
|
667
|
-
defaultBranch: argValue(forward, '--default-branch'),
|
|
668
|
-
run: argValue(forward, '--run'),
|
|
669
|
-
cacheRoot: argValue(forward, '--cache-root'),
|
|
670
|
-
simulator: argValue(forward, '--simulator') ?? argValue(forward, '--device'),
|
|
671
|
-
runtime: argValue(forward, '--runtime'),
|
|
672
|
-
deviceType: argValue(forward, '--device-type'),
|
|
673
|
-
slot: argValue(forward, '--slot'),
|
|
674
|
-
watcherPort,
|
|
675
|
-
runtimeDir,
|
|
676
|
-
force: hasArg(forward, '--force'),
|
|
677
|
-
resolveOnly: hasArg(forward, '--resolve-only'),
|
|
678
|
-
rerunCommand,
|
|
679
|
-
});
|
|
680
|
-
if (json) {
|
|
681
|
-
console.log(JSON.stringify(result, null, 2));
|
|
682
|
-
} else if (result.status === 'pass') {
|
|
683
|
-
const cache = typeof result.cache === 'object' && result.cache ? result.cache as Record<string, unknown> : undefined;
|
|
684
|
-
const simulator = typeof result.simulator === 'object' && result.simulator ? result.simulator as Record<string, unknown> : undefined;
|
|
685
|
-
const artifact = typeof result.artifact === 'object' && result.artifact ? result.artifact as Record<string, unknown> : undefined;
|
|
686
|
-
if (result.resolveOnly) {
|
|
687
|
-
console.error(`✓ resolved Runway app for ${adapter} ${result.platform ?? ''} run=${artifact?.runId ?? 'unknown'} revision=${artifact?.revision ?? 'unknown'} artifact=${artifact?.artifactName ?? 'unknown'}`);
|
|
688
|
-
} else {
|
|
689
|
-
const action = result.skipped ? 'already provisioned' : 'installed Runway app';
|
|
690
|
-
console.error(`✓ ${action} for ${adapter} ${result.platform ?? ''} simulator=${simulator?.name ?? 'unknown'} cache=${cache?.status ?? 'skip'}`);
|
|
691
|
-
}
|
|
692
|
-
} else {
|
|
693
|
-
const label = action === 'provision' ? 'provision' : 'install --runway';
|
|
694
|
-
console.error(`✗ mm-harness ${label}: ${result.error?.message ?? 'runway install failed'}\n Next: ${result.error?.userAction ?? rerunCommand}`);
|
|
695
|
-
}
|
|
696
|
-
return result.exitCode;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function provisionRunwayForward(forward: string[]): string[] {
|
|
700
|
-
const normalized: string[] = [];
|
|
701
|
-
let index = 0;
|
|
702
|
-
if (forward[index] === 'runway') index += 1;
|
|
703
|
-
if (forward[index] && !forward[index].startsWith('-')) {
|
|
704
|
-
if (!hasArg(forward, '--platform')) normalized.push('--platform', forward[index]);
|
|
705
|
-
index += 1;
|
|
706
|
-
}
|
|
707
|
-
return [...normalized, ...forward.slice(index)];
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function runwayInstallRerunCommand(
|
|
711
|
-
adapter: MetaMaskRecipeAdapter,
|
|
712
|
-
target: string,
|
|
713
|
-
forward: string[],
|
|
714
|
-
json: boolean,
|
|
715
|
-
): string {
|
|
716
|
-
const parts = ['mm-harness', 'install', '--runway', '--adapter', adapter, '--target', shellQuote(target)];
|
|
717
|
-
const valueFlags = [
|
|
718
|
-
'--platform',
|
|
719
|
-
'--branch',
|
|
720
|
-
'--default-branch',
|
|
721
|
-
'--run',
|
|
722
|
-
'--cache-root',
|
|
723
|
-
'--simulator',
|
|
724
|
-
'--device',
|
|
725
|
-
'--slot',
|
|
726
|
-
'--watcher-port',
|
|
727
|
-
'--runtime-dir',
|
|
728
|
-
'--runtime',
|
|
729
|
-
'--device-type',
|
|
730
|
-
];
|
|
731
|
-
for (const flag of valueFlags) {
|
|
732
|
-
const value = argValue(forward, flag);
|
|
733
|
-
if (value) parts.push(flag, shellQuote(value));
|
|
734
|
-
}
|
|
735
|
-
if (hasArg(forward, '--force')) parts.push('--force');
|
|
736
|
-
if (hasArg(forward, '--resolve-only')) parts.push('--resolve-only');
|
|
737
|
-
if (json) parts.push('--json');
|
|
738
|
-
return parts.join(' ');
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function runwayProvisionRerunCommand(
|
|
742
|
-
adapter: MetaMaskRecipeAdapter,
|
|
743
|
-
target: string,
|
|
744
|
-
forward: string[],
|
|
745
|
-
json: boolean,
|
|
746
|
-
): string {
|
|
747
|
-
const parts = ['mm-harness', 'provision', 'runway', shellQuote(argValue(forward, '--platform') ?? 'ios'), '--adapter', adapter, '--target', shellQuote(target)];
|
|
748
|
-
const valueFlags = [
|
|
749
|
-
'--branch',
|
|
750
|
-
'--default-branch',
|
|
751
|
-
'--run',
|
|
752
|
-
'--cache-root',
|
|
753
|
-
'--simulator',
|
|
754
|
-
'--device',
|
|
755
|
-
'--slot',
|
|
756
|
-
'--watcher-port',
|
|
757
|
-
'--runtime-dir',
|
|
758
|
-
'--runtime',
|
|
759
|
-
'--device-type',
|
|
760
|
-
];
|
|
761
|
-
for (const flag of valueFlags) {
|
|
762
|
-
const value = argValue(forward, flag);
|
|
763
|
-
if (value) parts.push(flag, shellQuote(value));
|
|
764
|
-
}
|
|
765
|
-
if (hasArg(forward, '--force')) parts.push('--force');
|
|
766
|
-
if (hasArg(forward, '--resolve-only')) parts.push('--resolve-only');
|
|
767
|
-
if (json) parts.push('--json');
|
|
768
|
-
return parts.join(' ');
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
// userAction is required whenever an error object is present so every --json
|
|
772
|
-
// failure carries a machine-readable escape path — parallel to the `usageOut`
|
|
773
|
-
// enforcement on the CLI layer. Omitting userAction is a compile-time error.
|
|
774
|
-
function harnessSummary(
|
|
775
|
-
action: HarnessAction,
|
|
776
|
-
adapter: MetaMaskRecipeAdapter | undefined,
|
|
777
|
-
target: string,
|
|
778
|
-
status: 'pass' | 'fail',
|
|
779
|
-
exitCode: number,
|
|
780
|
-
autoDetected: boolean,
|
|
781
|
-
error?: { code: string; message: string; userAction: string },
|
|
782
|
-
): string {
|
|
783
|
-
return JSON.stringify({
|
|
784
|
-
schemaVersion: 1,
|
|
785
|
-
command: 'harness',
|
|
786
|
-
action,
|
|
787
|
-
adapter: adapter ?? null,
|
|
788
|
-
target,
|
|
789
|
-
autoDetected,
|
|
790
|
-
status,
|
|
791
|
-
exitCode,
|
|
792
|
-
// Error contract: every --json failure carries a stable machine code + human
|
|
793
|
-
// message (CLI-SPEC.md §5.1). userAction is included when present so callers
|
|
794
|
-
// can surface the reachable escape without parsing the human message.
|
|
795
|
-
...(status === 'fail' && error ? { error } : {}),
|
|
796
|
-
});
|
|
797
|
-
}
|