@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/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
- export function runtimeContextSummary(target: string): MetaMaskRuntimeContextReport {
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
- for (const spec of RUNTIME_CONTEXT_FIELDS) {
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
- ? `runtime-context: ${runtimeContext.file} (present)`
144
- : `runtime-context: ${runtimeContext.file} (absent — written by farmslot prepare/dispatch)`,
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 value = field.value ?? '(unset)';
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
- lines.push(` ${key.padEnd(22)} ${value} [${origin}] ${field.customize}`);
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
  }
@@ -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 unlock',
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: 'unlock' resolves to 'metamask.wallet.unlock' if unique; ambiguous = exit 2.
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 unlock --adapter extension
79
- mm-harness call command --arg cmd="echo hi" --adapter core --json`,
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 <metro|app> Log source (default metro)
224
- --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
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('Grammar: mm-harness <command> [target] [flags] (target is a positional: ios | android; flags add agent depth; --json is the agent contract)');
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('');