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