@deeeed/metamask-harness 0.5.1 → 0.6.1

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +47 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +2 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/sidepanel-toggle.sh +33 -12
  8. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  9. package/adapters/manifest.json +13 -0
  10. package/adapters/mobile/inject.sh +1 -0
  11. package/adapters/mobile/start-metro.sh +6 -0
  12. package/adapters/mobile/stop-metro.sh +20 -0
  13. package/adapters/shared/cli-ux.sh +20 -2
  14. package/adapters/shared/ensure-runner-deps.sh +30 -0
  15. package/adapters/shared/reap-checkout-metros.sh +58 -0
  16. package/adapters/shared/recipe-harness-root.mjs +23 -0
  17. package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
  18. package/bin/mm-harness +45 -4
  19. package/dist/adapters/extension/runtime.js +3 -1
  20. package/dist/adapters/mobile/provision.js +34 -1
  21. package/dist/adapters/slot-ports.js +3 -8
  22. package/dist/cli.js +32 -1463
  23. package/dist/commands/call.js +183 -0
  24. package/dist/commands/completion-candidates.js +58 -0
  25. package/dist/commands/doctor.js +101 -0
  26. package/dist/commands/ensure-ready.js +24 -0
  27. package/dist/commands/flows.js +62 -0
  28. package/dist/commands/launch/extension.js +40 -0
  29. package/dist/commands/{launch.js → launch/index.js} +15 -47
  30. package/dist/commands/launch/mobile.js +10 -0
  31. package/dist/commands/manifest.js +72 -0
  32. package/dist/commands/parse-args.js +189 -0
  33. package/dist/commands/provision.js +136 -0
  34. package/dist/commands/resolve-extension.js +23 -0
  35. package/dist/commands/run-engine.js +341 -0
  36. package/dist/commands/run.js +217 -0
  37. package/dist/commands/runtime-decision.js +58 -0
  38. package/dist/commands/runtime-health.js +25 -0
  39. package/dist/commands/runtime-launch.js +139 -0
  40. package/dist/commands/self-test.js +52 -0
  41. package/dist/commands/stop.js +52 -0
  42. package/dist/harness.js +8 -48
  43. package/dist/mm-harness-cli.js +13 -8
  44. package/docs/CLI-SPEC.md +1 -1
  45. package/docs/CODE-MAP.md +62 -0
  46. package/library/README.md +14 -0
  47. package/library/actions/extension/platform/cdp.mjs +1 -1
  48. package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
  49. package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
  50. package/package.json +6 -7
  51. package/src/adapters/core/surface.ts +0 -71
  52. package/src/adapters/extension/ensure-ready.ts +0 -185
  53. package/src/adapters/extension/extension-id.ts +0 -107
  54. package/src/adapters/extension/runtime-decision.ts +0 -445
  55. package/src/adapters/extension/runtime.ts +0 -407
  56. package/src/adapters/extension/surface.ts +0 -88
  57. package/src/adapters/mobile/deps-markers.ts +0 -21
  58. package/src/adapters/mobile/prepare.ts +0 -246
  59. package/src/adapters/mobile/provision.ts +0 -594
  60. package/src/adapters/mobile/runtime-decision.ts +0 -466
  61. package/src/adapters/mobile/surface.ts +0 -71
  62. package/src/adapters/resolve-farmslot-ports.ts +0 -13
  63. package/src/adapters/slot-ports.ts +0 -158
  64. package/src/adapters/surface.ts +0 -117
  65. package/src/adapters.ts +0 -601
  66. package/src/cli-color.ts +0 -92
  67. package/src/cli-commands.ts +0 -250
  68. package/src/cli-version.ts +0 -141
  69. package/src/cli.ts +0 -2091
  70. package/src/commands/debug.ts +0 -65
  71. package/src/commands/fixtures.ts +0 -198
  72. package/src/commands/launch.ts +0 -470
  73. package/src/commands/logs.ts +0 -99
  74. package/src/commands/shared.ts +0 -235
  75. package/src/commands/update.ts +0 -316
  76. package/src/completions-cache.ts +0 -86
  77. package/src/doctor.ts +0 -215
  78. package/src/harness.ts +0 -797
  79. package/src/heal-bounds.ts +0 -198
  80. package/src/index.ts +0 -15
  81. package/src/leaf-invoke.ts +0 -28
  82. package/src/live-adapter-contract.ts +0 -274
  83. package/src/manifest.ts +0 -47
  84. package/src/mm-harness-cli.ts +0 -655
  85. package/src/paths.ts +0 -198
  86. package/src/progress.ts +0 -117
  87. package/src/recording-target.ts +0 -147
  88. package/src/run-recording.ts +0 -329
  89. package/src/runner.ts +0 -108
  90. package/src/types.ts +0 -57
package/src/doctor.ts DELETED
@@ -1,215 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import { color } from './cli-color.ts';
5
- import { readRuntimeContextField, resolveRuntimeContextPath } from './harness.ts';
6
- import { manifestPath, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from './paths.ts';
7
- import type {
8
- MetaMaskDoctorReport,
9
- MetaMaskRecipeAdapter,
10
- MetaMaskRuntimeContextField,
11
- MetaMaskRuntimeContextReport,
12
- } from './types.ts';
13
-
14
- export function repoShape(target: string): Record<string, unknown> {
15
- const exists = (rel) => fs.existsSync(path.join(target, rel));
16
- const packageInfo = readPackageInfo(target);
17
- const extensionProject =
18
- packageInfo.name === 'metamask-crx' ||
19
- (exists('app/manifest') && exists('app/scripts') && exists('ui'));
20
- const mobileProject =
21
- packageInfo.name === 'metamask' && exists('app/core') && (exists('ios') || exists('android'));
22
- return {
23
- packageName: packageInfo.name,
24
- packageJsonStatus: packageInfo.status,
25
- packageJsonError: packageInfo.error,
26
- extensionProject,
27
- mobileProject,
28
- agenticService: exists('app/dev-tools/AgenticService/AgenticService.ts'),
29
- mobileProductHarness: false,
30
- mobileBridgeScript: true,
31
- extensionRuntime: exists(`${recipeHarnessRoot()}/extension`),
32
- injectedHarness: exists(`${recipeHarnessRoot()}/mobile`) || exists(`${recipeHarnessRoot()}/extension`),
33
- walletFixture:
34
- exists(`${recipeRuntimeDir()}/wallet-fixture.json`),
35
- };
36
- }
37
-
38
- export function compatibilityMode(adapter: MetaMaskRecipeAdapter, target: string) {
39
- const shape = repoShape(target);
40
- if (adapter === 'core') {
41
- return fs.existsSync(path.join(target, 'packages/perps-controller/src/index.ts'))
42
- ? 'headless controller (no bridge)'
43
- : 'unsupported/no bridge';
44
- }
45
- if (adapter === 'mobile') {
46
- if (shape.injectedHarness && shape.agenticService) return 'runner bridge with injected app bridge';
47
- if (shape.agenticService) return 'runner bridge with app bridge';
48
- return 'runner bridge available; app bridge not installed';
49
- }
50
- if (!shape.extensionProject) return 'unsupported/no bridge';
51
- if (shape.extensionRuntime || shape.injectedHarness) return 'bridge present';
52
- return 'bridge injectable';
53
- }
54
-
55
- function readPackageInfo(target: string) {
56
- const packageJsonPath = path.join(target, 'package.json');
57
- if (!fs.existsSync(packageJsonPath)) {
58
- return { status: 'missing', name: null, error: null };
59
- }
60
- try {
61
- const data = readJsonObject(packageJsonPath);
62
- return {
63
- status: 'valid',
64
- name: typeof data.name === 'string' ? data.name : null,
65
- error: null,
66
- };
67
- } catch (error) {
68
- return {
69
- status: 'invalid',
70
- name: null,
71
- error: error instanceof Error ? error.message : String(error),
72
- };
73
- }
74
- }
75
-
76
- export function fixtureSummary(target: string): Record<string, unknown> {
77
- const candidates = [
78
- `${recipeRuntimeDir()}/wallet-fixture.json`,
79
- ];
80
- const rel = candidates.find((candidate) => fs.existsSync(path.join(target, candidate)));
81
- if (!rel) return { status: 'missing', path: null };
82
- try {
83
- const data = readJsonObject(path.join(target, rel));
84
- return {
85
- status: Array.isArray(data.accounts) && data.accounts.length > 0 ? 'ready' : 'incomplete',
86
- path: rel,
87
- accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
88
- hasPassword: typeof data.password === 'string' && data.password.length > 0,
89
- };
90
- } catch (error) {
91
- return { status: 'invalid', path: rel, error: error instanceof Error ? error.message : String(error) };
92
- }
93
- }
94
-
95
- // The runtime-context fields the harness hydrates from agentic-runtime.json,
96
- // each paired with the env var(s) that override the file value and the one-line
97
- // customize hint. `envVars` lists the harness precedence (first set wins); `envVar`
98
- // is the canonical name reported. Order matches the contract table (CLI-SPEC.md).
99
- const RUNTIME_CONTEXT_FIELDS: ReadonlyArray<{
100
- key: string;
101
- envVars: readonly string[];
102
- envVar: string | null;
103
- customize: string;
104
- adapters: readonly MetaMaskRecipeAdapter[];
105
- }> = [
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'] },
112
- ];
113
-
114
- // Report every runtime-context field with its current value and where it came from
115
- // (live env override > file > unset default), reusing the harness's own file reader
116
- // so doctor and dispatch agree on resolution. Absent file → fileExists:false and
117
- // every field falls back to env or default; the path shows where it WOULD live.
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 {
121
- const contextPath = resolveRuntimeContextPath(target);
122
- const envOverride = process.env.RECIPE_RUNTIME_CONTEXT ?? null;
123
- const fileExists = fs.existsSync(contextPath);
124
- const file = envOverride ?? path.relative(target, contextPath);
125
- const fields: Record<string, MetaMaskRuntimeContextField> = {};
126
- const specs = adapter
127
- ? RUNTIME_CONTEXT_FIELDS.filter((spec) => spec.adapters.includes(adapter))
128
- : RUNTIME_CONTEXT_FIELDS;
129
- for (const spec of specs) {
130
- const envValue = spec.envVars
131
- .map((name) => process.env[name])
132
- .find((value) => value !== undefined && value !== '');
133
- if (envValue !== undefined) {
134
- fields[spec.key] = { value: envValue, source: 'env', envVar: spec.envVar, customize: spec.customize };
135
- continue;
136
- }
137
- const fileValue = fileExists ? readRuntimeContextField(contextPath, spec.key) : undefined;
138
- fields[spec.key] = fileValue !== undefined
139
- ? { value: fileValue, source: 'file', envVar: spec.envVar, customize: spec.customize }
140
- : { value: null, source: 'default', envVar: spec.envVar, customize: spec.customize };
141
- }
142
- return { file, fileExists, envOverride, fields };
143
- }
144
-
145
- // Human-readable runtime-context section for `doctor` without --json.
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 });
150
- const lines: string[] = [];
151
- lines.push(
152
- runtimeContext.fileExists
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)')}`,
155
- );
156
- for (const [key, field] of Object.entries(runtimeContext.fields)) {
157
- const isSet = field.value !== undefined && field.value !== null && field.value !== '';
158
- const value = isSet ? out('ok', String(field.value)) : out('dim', '(unset)');
159
- const origin = field.source === 'env' && field.envVar ? `env ${field.envVar}` : field.source;
160
- const originTag = isSet ? out('accent', `[${origin}]`) : out('dim', `[${origin}]`);
161
- lines.push(` ${key.padEnd(22)} ${value} ${originTag} ${out('dim', `— ${field.customize}`)}`);
162
- }
163
- return lines.join('\n');
164
- }
165
-
166
- export function createDoctorReport(
167
- adapter: MetaMaskRecipeAdapter,
168
- target: string,
169
- manifestValidation: { summary?: { errors?: number } & Record<string, unknown> },
170
- actionManifestPath = manifestPath(adapter),
171
- ): MetaMaskDoctorReport {
172
- const mode = compatibilityMode(adapter, target);
173
- const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
174
- const status = manifestErrors > 0 ? 'fail' : 'pass';
175
- const checks = [
176
- {
177
- id: 'manifest',
178
- status: manifestErrors === 0 ? 'pass' : 'fail',
179
- message: manifestErrors === 0 ? 'Action manifest is valid Recipe v1.' : `Action manifest has ${manifestErrors} validation error(s).`,
180
- },
181
- {
182
- id: 'bridge',
183
- status: mode === 'unsupported/no bridge' ? 'fail' : 'pass',
184
- message: mode === 'unsupported/no bridge' ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`,
185
- },
186
- ] as const;
187
- return {
188
- schemaVersion: 1,
189
- protocolVersion: 'v1',
190
- runner_protocol_version: 1,
191
- status,
192
- checks: [...checks],
193
- adapter,
194
- target,
195
- runner: {
196
- name: '@metamask/recipe-runner',
197
- runnerDir,
198
- actionManifestPath,
199
- harnessPackage: '@farmslot/recipe-harness',
200
- },
201
- compatibilityMode: mode,
202
- shape: repoShape(target),
203
- fixture: fixtureSummary(target),
204
- runtimeContext: runtimeContextSummary(target, adapter),
205
- manifestValidation: manifestValidation.summary,
206
- };
207
- }
208
-
209
- function readJsonObject(file: string): Record<string, unknown> {
210
- const value = readJson(file);
211
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
212
- throw new Error(`Expected JSON object in ${file}`);
213
- }
214
- return value as Record<string, unknown>;
215
- }