@deeeed/metamask-harness 0.3.8 → 0.4.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 +60 -11
- package/adapters/extension/start-watch.sh +8 -1
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/lib/tmux-viewer.sh +38 -0
- package/adapters/mobile/start-metro.sh +4 -18
- package/adapters/mobile/stop-metro.sh +66 -0
- package/adapters/mobile/yarn-setup.sh +14 -2
- package/adapters/shared/resolve-farmslot-ports.sh +21 -0
- package/docs/ADAPTER-SURFACE.md +119 -0
- package/docs/UX-PRINCIPLES.md +64 -0
- package/package.json +13 -4
- package/scripts/completions.sh +6 -3
- package/src/adapters/core/surface.ts +56 -0
- package/src/adapters/extension/surface.ts +71 -0
- package/src/adapters/mobile/prepare.ts +22 -3
- package/src/adapters/mobile/runtime-decision.ts +103 -42
- package/src/adapters/mobile/surface.ts +59 -0
- package/src/adapters/slot-ports.ts +165 -0
- package/src/adapters/surface.ts +82 -0
- package/src/cli.ts +128 -8
- package/src/commands/debug.ts +3 -1
- package/src/commands/fixtures.ts +13 -8
- package/src/commands/launch.ts +32 -105
- package/src/commands/logs.ts +29 -13
- package/src/doctor.ts +25 -13
- package/src/mm-harness-cli.ts +72 -13
package/src/doctor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { color } from './cli-color.ts';
|
|
4
5
|
import { readRuntimeContextField, resolveRuntimeContextPath } from './harness.ts';
|
|
5
6
|
import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from './paths.ts';
|
|
6
7
|
import type {
|
|
@@ -100,26 +101,32 @@ const RUNTIME_CONTEXT_FIELDS: ReadonlyArray<{
|
|
|
100
101
|
envVars: readonly string[];
|
|
101
102
|
envVar: string | null;
|
|
102
103
|
customize: string;
|
|
104
|
+
adapters: readonly MetaMaskRecipeAdapter[];
|
|
103
105
|
}> = [
|
|
104
|
-
{ key: 'slotId', envVars: ['RECIPE_SLOT_ID'], envVar: 'RECIPE_SLOT_ID', customize: 'farmslot dispatch writes this' },
|
|
105
|
-
{ key: 'extensionId', envVars: ['RECIPE_HARNESS_EXTENSION_ID'], envVar: 'RECIPE_HARNESS_EXTENSION_ID', customize: 'auto-resolved; edit file to pin' },
|
|
106
|
-
{ key: 'cdpPort', envVars: ['RECIPE_CDP_PORT', 'CDP_PORT'], envVar: 'CDP_PORT', customize: 'edit file or pass --cdp-port' },
|
|
107
|
-
{ key: 'runtimeStart.approved', envVars: ['RECIPE_RUNTIME_START_APPROVED'], envVar: 'RECIPE_RUNTIME_START_APPROVED', customize: 'edit file (true/false)' },
|
|
108
|
-
{ key: 'runtimeStart.command', envVars: [], envVar: null, customize: 'edit file' },
|
|
109
|
-
{ key: 'runtimeStart.readyUrl', envVars: ['RECIPE_RUNTIME_READY_URL'], envVar: 'RECIPE_RUNTIME_READY_URL', customize: 'edit file' },
|
|
106
|
+
{ key: 'slotId', envVars: ['RECIPE_SLOT_ID'], envVar: 'RECIPE_SLOT_ID', customize: 'farmslot dispatch writes this', adapters: ['mobile', 'extension', 'core'] },
|
|
107
|
+
{ key: 'extensionId', envVars: ['RECIPE_HARNESS_EXTENSION_ID'], envVar: 'RECIPE_HARNESS_EXTENSION_ID', customize: 'auto-resolved; edit file to pin', adapters: ['extension'] },
|
|
108
|
+
{ key: 'cdpPort', envVars: ['RECIPE_CDP_PORT', 'CDP_PORT'], envVar: 'CDP_PORT', customize: 'edit file or pass --cdp-port', adapters: ['extension'] },
|
|
109
|
+
{ key: 'runtimeStart.approved', envVars: ['RECIPE_RUNTIME_START_APPROVED'], envVar: 'RECIPE_RUNTIME_START_APPROVED', customize: 'edit file (true/false)', adapters: ['mobile', 'extension'] },
|
|
110
|
+
{ key: 'runtimeStart.command', envVars: [], envVar: null, customize: 'edit file', adapters: ['mobile', 'extension'] },
|
|
111
|
+
{ key: 'runtimeStart.readyUrl', envVars: ['RECIPE_RUNTIME_READY_URL'], envVar: 'RECIPE_RUNTIME_READY_URL', customize: 'edit file', adapters: ['mobile', 'extension'] },
|
|
110
112
|
];
|
|
111
113
|
|
|
112
114
|
// Report every runtime-context field with its current value and where it came from
|
|
113
115
|
// (live env override > file > unset default), reusing the harness's own file reader
|
|
114
116
|
// so doctor and dispatch agree on resolution. Absent file → fileExists:false and
|
|
115
117
|
// every field falls back to env or default; the path shows where it WOULD live.
|
|
116
|
-
|
|
118
|
+
// An adapter scopes the report to that platform's fields — an extension-only row
|
|
119
|
+
// (cdpPort, extensionId) is noise on a mobile or core slot.
|
|
120
|
+
export function runtimeContextSummary(target: string, adapter?: MetaMaskRecipeAdapter): MetaMaskRuntimeContextReport {
|
|
117
121
|
const contextPath = resolveRuntimeContextPath(target);
|
|
118
122
|
const envOverride = process.env.RECIPE_RUNTIME_CONTEXT ?? null;
|
|
119
123
|
const fileExists = fs.existsSync(contextPath);
|
|
120
124
|
const file = envOverride ?? path.relative(target, contextPath);
|
|
121
125
|
const fields: Record<string, MetaMaskRuntimeContextField> = {};
|
|
122
|
-
|
|
126
|
+
const specs = adapter
|
|
127
|
+
? RUNTIME_CONTEXT_FIELDS.filter((spec) => spec.adapters.includes(adapter))
|
|
128
|
+
: RUNTIME_CONTEXT_FIELDS;
|
|
129
|
+
for (const spec of specs) {
|
|
123
130
|
const envValue = spec.envVars
|
|
124
131
|
.map((name) => process.env[name])
|
|
125
132
|
.find((value) => value !== undefined && value !== '');
|
|
@@ -137,16 +144,21 @@ export function runtimeContextSummary(target: string): MetaMaskRuntimeContextRep
|
|
|
137
144
|
|
|
138
145
|
// Human-readable runtime-context section for `doctor` without --json.
|
|
139
146
|
export function renderRuntimeContext(runtimeContext: MetaMaskRuntimeContextReport): string {
|
|
147
|
+
// Human render: set values read bright with their provenance highlighted;
|
|
148
|
+
// unset defaults read dim so the eye lands on what is actually configured.
|
|
149
|
+
const out = (style: string, text: string) => color(style, text, { stream: process.stdout });
|
|
140
150
|
const lines: string[] = [];
|
|
141
151
|
lines.push(
|
|
142
152
|
runtimeContext.fileExists
|
|
143
|
-
?
|
|
144
|
-
:
|
|
153
|
+
? `${out('label', 'runtime-context:')} ${runtimeContext.file} ${out('ok', '(present)')}`
|
|
154
|
+
: `${out('label', 'runtime-context:')} ${runtimeContext.file} ${out('dim', '(absent — written by farmslot prepare/dispatch)')}`,
|
|
145
155
|
);
|
|
146
156
|
for (const [key, field] of Object.entries(runtimeContext.fields)) {
|
|
147
|
-
const
|
|
157
|
+
const isSet = field.value !== undefined && field.value !== null && field.value !== '';
|
|
158
|
+
const value = isSet ? out('ok', String(field.value)) : out('dim', '(unset)');
|
|
148
159
|
const origin = field.source === 'env' && field.envVar ? `env ${field.envVar}` : field.source;
|
|
149
|
-
|
|
160
|
+
const originTag = isSet ? out('accent', `[${origin}]`) : out('dim', `[${origin}]`);
|
|
161
|
+
lines.push(` ${key.padEnd(22)} ${value} ${originTag} ${out('dim', `— ${field.customize}`)}`);
|
|
150
162
|
}
|
|
151
163
|
return lines.join('\n');
|
|
152
164
|
}
|
|
@@ -189,7 +201,7 @@ export function createDoctorReport(
|
|
|
189
201
|
compatibilityMode: mode,
|
|
190
202
|
shape: repoShape(target),
|
|
191
203
|
fixture: fixtureSummary(target),
|
|
192
|
-
runtimeContext: runtimeContextSummary(target),
|
|
204
|
+
runtimeContext: runtimeContextSummary(target, adapter),
|
|
193
205
|
manifestValidation: manifestValidation.summary,
|
|
194
206
|
};
|
|
195
207
|
}
|
package/src/mm-harness-cli.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
// teach; genuinely-unknown commands fall to commander's default.
|
|
8
8
|
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
14
|
import { Command } from 'commander';
|
|
14
15
|
|
|
16
|
+
import { color } from './cli-color.ts';
|
|
15
17
|
import { handleUpdate, maybeNudge } from './commands/update.ts';
|
|
16
18
|
|
|
17
19
|
// …/src → its parent is the package root that holds scripts/.
|
|
@@ -57,14 +59,37 @@ Example:
|
|
|
57
59
|
mm-harness actions --adapter mobile
|
|
58
60
|
mm-harness actions --adapter extension --raw`,
|
|
59
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: 'stop',
|
|
64
|
+
summary: 'Stop the dev server this checkout owns (mobile Metro / extension webpack watcher) and close its log window.',
|
|
65
|
+
example: 'mm-harness stop',
|
|
66
|
+
helpText: `mm-harness stop [flags]
|
|
67
|
+
|
|
68
|
+
Stop the dev server this checkout owns and close its tmux log-tail window,
|
|
69
|
+
scoped to this checkout so concurrent slots are untouched. Idempotent —
|
|
70
|
+
nothing running is success, not an error. Behavior is per platform:
|
|
71
|
+
mobile stop the port-scoped Metro dev server
|
|
72
|
+
extension stop the checkout's webpack watcher (pid file + orphan scan)
|
|
73
|
+
core headless — no dev server to stop (teaching error)
|
|
74
|
+
|
|
75
|
+
--port <port> Dev-server port (default: the checkout's slot context)
|
|
76
|
+
--target <path> Checkout path (default: cwd)
|
|
77
|
+
--json Machine-readable output
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
mm-harness stop
|
|
81
|
+
mm-harness stop --port 8061`,
|
|
82
|
+
},
|
|
60
83
|
{
|
|
61
84
|
name: 'call',
|
|
62
85
|
summary: 'Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).',
|
|
63
|
-
example: 'mm-harness call
|
|
86
|
+
example: 'mm-harness call ensure_unlocked',
|
|
64
87
|
helpText: `mm-harness call <action> [--arg k=v ...] [flags]
|
|
65
88
|
|
|
66
89
|
Run one action in isolation as a one-node recipe through the real engine path.
|
|
67
|
-
Fuzzy short-name: '
|
|
90
|
+
Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
|
|
91
|
+
if unique; ambiguous = exit 2. Actions differ per adapter — list this checkout's
|
|
92
|
+
with: mm-harness actions.
|
|
68
93
|
|
|
69
94
|
--arg k=v Action field value (repeatable)
|
|
70
95
|
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
@@ -74,9 +99,9 @@ Example:
|
|
|
74
99
|
--heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
|
|
75
100
|
--json Machine-readable output
|
|
76
101
|
|
|
77
|
-
Example:
|
|
78
|
-
mm-harness call
|
|
79
|
-
mm-harness call command --arg cmd="echo hi" --adapter core
|
|
102
|
+
Example (real actions; run mm-harness actions for this checkout's full set):
|
|
103
|
+
mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
|
|
104
|
+
mm-harness call command --arg cmd="echo hi" --adapter core # the universal action (all adapters)`,
|
|
80
105
|
},
|
|
81
106
|
{
|
|
82
107
|
name: 'flows',
|
|
@@ -220,8 +245,10 @@ Example:
|
|
|
220
245
|
|
|
221
246
|
--full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
|
|
222
247
|
--events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
|
|
223
|
-
--source <
|
|
224
|
-
|
|
248
|
+
--source <label> Log source per adapter — mobile: metro|app (default metro);
|
|
249
|
+
extension: webpack|watcher|rebuild|app (default webpack).
|
|
250
|
+
Core is headless (teaching error).
|
|
251
|
+
--adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
|
|
225
252
|
--target <path> Checkout path (default: cwd)
|
|
226
253
|
--json Machine-readable output
|
|
227
254
|
|
|
@@ -345,7 +372,7 @@ const HELP_GROUPS: HelpGroup[] = [
|
|
|
345
372
|
{
|
|
346
373
|
title: 'DAILY LOOP',
|
|
347
374
|
blurb: 'what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)',
|
|
348
|
-
commands: ['launch', 'logs', 'debug', 'fixtures'],
|
|
375
|
+
commands: ['launch', 'stop', 'logs', 'debug', 'fixtures'],
|
|
349
376
|
},
|
|
350
377
|
{
|
|
351
378
|
title: 'DISCOVER',
|
|
@@ -375,19 +402,51 @@ function commandMeta(name: string): { summary: string; example: string; planned:
|
|
|
375
402
|
return { summary: '', example: '', planned: false };
|
|
376
403
|
}
|
|
377
404
|
|
|
405
|
+
// Slot context the orchestrator's prepare wrote into the checkout, when the
|
|
406
|
+
// help is run from inside one. Presence-gated: no context, no line.
|
|
407
|
+
function detectedSlotLine(out: (style: string, text: string) => string): string | null {
|
|
408
|
+
const ctxPath = path.join(
|
|
409
|
+
process.cwd(),
|
|
410
|
+
process.env.RECIPE_RUNTIME_DIR || 'temp/recipe/runtime',
|
|
411
|
+
'agentic-runtime.json',
|
|
412
|
+
);
|
|
413
|
+
try {
|
|
414
|
+
const ctx = JSON.parse(fs.readFileSync(ctxPath, 'utf8')) as Record<string, unknown>;
|
|
415
|
+
const parts: string[] = [];
|
|
416
|
+
if (ctx.slotId) parts.push(`slot ${out('ok', String(ctx.slotId))}`);
|
|
417
|
+
if (ctx.simulator) parts.push(`device ${out('ok', String(ctx.simulator))}`);
|
|
418
|
+
if (ctx.metroPort) parts.push(`metro :${out('ok', String(ctx.metroPort))}`);
|
|
419
|
+
if (ctx.gitBranch) parts.push(`branch ${out('info', String(ctx.gitBranch))}`);
|
|
420
|
+
if (parts.length === 0) return null;
|
|
421
|
+
return `${out('label', 'SLOT')} — this checkout is a prepared slot: ${parts.join(' · ')}`;
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
378
427
|
function groupedHelp(): string {
|
|
428
|
+
const out = (style: string, text: string) => color(style, text, { stream: process.stdout });
|
|
379
429
|
const lines: string[] = [];
|
|
380
|
-
lines.push('mm-harness — one front door for the MetaMask recipe loop: launch the app, prove behavior, manage the runtime overlay
|
|
430
|
+
lines.push(`${out('bold', 'mm-harness')} — one front door for the MetaMask recipe loop: launch the app, prove behavior, manage the runtime overlay.`);
|
|
381
431
|
lines.push('Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.');
|
|
382
|
-
lines.push(
|
|
432
|
+
lines.push(`Grammar: ${out('cmd', 'mm-harness <command> [target] [flags]')} (target is a positional: ios | android; flags add agent depth; --json is the agent contract)`);
|
|
433
|
+
if (process.env.MM_HARNESS_BIN) {
|
|
434
|
+
lines.push('');
|
|
435
|
+
lines.push(`${out('warn', 'DEV OVERRIDE ACTIVE')} — this run is served by MM_HARNESS_BIN=${out('path', process.env.MM_HARNESS_BIN)} (unset it to return to the installed/global bin).`);
|
|
436
|
+
}
|
|
437
|
+
const slotLine = detectedSlotLine(out);
|
|
438
|
+
if (slotLine) {
|
|
439
|
+
lines.push('');
|
|
440
|
+
lines.push(slotLine);
|
|
441
|
+
}
|
|
383
442
|
for (const group of HELP_GROUPS) {
|
|
384
443
|
lines.push('');
|
|
385
|
-
lines.push(`${group.title} — ${group.blurb}:`);
|
|
444
|
+
lines.push(`${out('label', group.title)} — ${group.blurb}:`);
|
|
386
445
|
for (const name of group.commands) {
|
|
387
446
|
const meta = commandMeta(name);
|
|
388
447
|
const planned = meta.planned ? ' (planned)' : '';
|
|
389
|
-
lines.push(` ${name.padEnd(10)} ${meta.summary}${planned}`);
|
|
390
|
-
lines.push(` ${meta.example}`);
|
|
448
|
+
lines.push(` ${out('cmd', name.padEnd(10))} ${meta.summary}${planned}`);
|
|
449
|
+
lines.push(` ${out('comment', meta.example)}`);
|
|
391
450
|
}
|
|
392
451
|
}
|
|
393
452
|
lines.push('');
|