@deeeed/metamask-harness 0.5.1 → 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.
Files changed (89) hide show
  1. package/CHANGELOG.md +37 -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/stop-metro.sh +20 -0
  12. package/adapters/shared/cli-ux.sh +20 -2
  13. package/adapters/shared/ensure-runner-deps.sh +30 -0
  14. package/adapters/shared/reap-checkout-metros.sh +53 -0
  15. package/adapters/shared/recipe-harness-root.mjs +23 -0
  16. package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
  17. package/bin/mm-harness +45 -4
  18. package/dist/adapters/extension/runtime.js +3 -1
  19. package/dist/adapters/mobile/provision.js +34 -1
  20. package/dist/adapters/slot-ports.js +3 -8
  21. package/dist/cli.js +32 -1463
  22. package/dist/commands/call.js +183 -0
  23. package/dist/commands/completion-candidates.js +58 -0
  24. package/dist/commands/doctor.js +101 -0
  25. package/dist/commands/ensure-ready.js +24 -0
  26. package/dist/commands/flows.js +62 -0
  27. package/dist/commands/launch/extension.js +40 -0
  28. package/dist/commands/{launch.js → launch/index.js} +15 -47
  29. package/dist/commands/launch/mobile.js +10 -0
  30. package/dist/commands/manifest.js +72 -0
  31. package/dist/commands/parse-args.js +189 -0
  32. package/dist/commands/provision.js +136 -0
  33. package/dist/commands/resolve-extension.js +23 -0
  34. package/dist/commands/run-engine.js +341 -0
  35. package/dist/commands/run.js +217 -0
  36. package/dist/commands/runtime-decision.js +58 -0
  37. package/dist/commands/runtime-health.js +25 -0
  38. package/dist/commands/runtime-launch.js +139 -0
  39. package/dist/commands/self-test.js +52 -0
  40. package/dist/commands/stop.js +52 -0
  41. package/dist/harness.js +8 -48
  42. package/dist/mm-harness-cli.js +13 -8
  43. package/docs/CLI-SPEC.md +1 -1
  44. package/docs/CODE-MAP.md +62 -0
  45. package/library/README.md +14 -0
  46. package/library/actions/extension/platform/cdp.mjs +1 -1
  47. package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
  48. package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
  49. package/package.json +6 -7
  50. package/src/adapters/core/surface.ts +0 -71
  51. package/src/adapters/extension/ensure-ready.ts +0 -185
  52. package/src/adapters/extension/extension-id.ts +0 -107
  53. package/src/adapters/extension/runtime-decision.ts +0 -445
  54. package/src/adapters/extension/runtime.ts +0 -407
  55. package/src/adapters/extension/surface.ts +0 -88
  56. package/src/adapters/mobile/deps-markers.ts +0 -21
  57. package/src/adapters/mobile/prepare.ts +0 -246
  58. package/src/adapters/mobile/provision.ts +0 -594
  59. package/src/adapters/mobile/runtime-decision.ts +0 -466
  60. package/src/adapters/mobile/surface.ts +0 -71
  61. package/src/adapters/resolve-farmslot-ports.ts +0 -13
  62. package/src/adapters/slot-ports.ts +0 -158
  63. package/src/adapters/surface.ts +0 -117
  64. package/src/adapters.ts +0 -601
  65. package/src/cli-color.ts +0 -92
  66. package/src/cli-commands.ts +0 -250
  67. package/src/cli-version.ts +0 -141
  68. package/src/cli.ts +0 -2091
  69. package/src/commands/debug.ts +0 -65
  70. package/src/commands/fixtures.ts +0 -198
  71. package/src/commands/launch.ts +0 -470
  72. package/src/commands/logs.ts +0 -99
  73. package/src/commands/shared.ts +0 -235
  74. package/src/commands/update.ts +0 -316
  75. package/src/completions-cache.ts +0 -86
  76. package/src/doctor.ts +0 -215
  77. package/src/harness.ts +0 -797
  78. package/src/heal-bounds.ts +0 -198
  79. package/src/index.ts +0 -15
  80. package/src/leaf-invoke.ts +0 -28
  81. package/src/live-adapter-contract.ts +0 -274
  82. package/src/manifest.ts +0 -47
  83. package/src/mm-harness-cli.ts +0 -655
  84. package/src/paths.ts +0 -198
  85. package/src/progress.ts +0 -117
  86. package/src/recording-target.ts +0 -147
  87. package/src/run-recording.ts +0 -329
  88. package/src/runner.ts +0 -108
  89. 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
- }