@deeeed/metamask-harness 0.3.9 → 0.5.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 (58) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/dist/adapters/core/surface.js +53 -0
  3. package/dist/adapters/extension/ensure-ready.js +109 -0
  4. package/dist/adapters/extension/extension-id.js +62 -0
  5. package/dist/adapters/extension/runtime-decision.js +305 -0
  6. package/dist/adapters/extension/runtime.js +324 -0
  7. package/dist/adapters/extension/surface.js +69 -0
  8. package/dist/adapters/mobile/deps-markers.js +22 -0
  9. package/dist/adapters/mobile/prepare.js +146 -0
  10. package/dist/adapters/mobile/provision.js +465 -0
  11. package/dist/adapters/mobile/runtime-decision.js +315 -0
  12. package/dist/adapters/mobile/surface.js +54 -0
  13. package/dist/adapters/slot-ports.js +146 -0
  14. package/dist/adapters/surface.js +14 -0
  15. package/dist/adapters.js +485 -0
  16. package/dist/cli-color.js +79 -0
  17. package/dist/cli-commands.js +224 -0
  18. package/dist/cli-version.js +111 -0
  19. package/dist/cli.js +1571 -0
  20. package/dist/commands/debug.js +56 -0
  21. package/dist/commands/fixtures.js +153 -0
  22. package/dist/commands/launch.js +325 -0
  23. package/dist/commands/logs.js +73 -0
  24. package/dist/commands/shared.js +157 -0
  25. package/dist/commands/update.js +243 -0
  26. package/dist/completions-cache.js +53 -0
  27. package/dist/doctor.js +169 -0
  28. package/dist/harness.js +627 -0
  29. package/dist/heal-bounds.js +120 -0
  30. package/dist/index.js +25 -0
  31. package/dist/leaf-invoke.js +19 -0
  32. package/dist/live-adapter-contract.js +240 -0
  33. package/dist/manifest.js +37 -0
  34. package/dist/mm-harness-cli.js +521 -0
  35. package/dist/paths.js +179 -0
  36. package/dist/progress.js +94 -0
  37. package/dist/recording-target.js +133 -0
  38. package/dist/run-recording.js +271 -0
  39. package/dist/runner.js +88 -0
  40. package/dist/types.js +0 -0
  41. package/docs/ADAPTER-SURFACE.md +119 -0
  42. package/docs/CLI-SPEC.md +26 -3
  43. package/docs/UX-PRINCIPLES.md +3 -0
  44. package/package.json +10 -2
  45. package/src/adapters/core/surface.ts +71 -0
  46. package/src/adapters/extension/surface.ts +88 -0
  47. package/src/adapters/mobile/provision.ts +594 -0
  48. package/src/adapters/mobile/surface.ts +71 -0
  49. package/src/adapters/slot-ports.ts +165 -0
  50. package/src/adapters/surface.ts +117 -0
  51. package/src/cli-commands.ts +1 -1
  52. package/src/cli.ts +239 -49
  53. package/src/commands/debug.ts +3 -1
  54. package/src/commands/fixtures.ts +13 -8
  55. package/src/commands/launch.ts +7 -156
  56. package/src/commands/logs.ts +29 -13
  57. package/src/harness.ts +140 -3
  58. package/src/mm-harness-cli.ts +71 -18
package/dist/runner.js ADDED
@@ -0,0 +1,88 @@
1
+ import { execSync } from "node:child_process";
2
+ import { createMetaMaskAdapters, createMetaMaskUiTransport } from "./adapters.js";
3
+ import { loadMetaMaskExtensionActionManifest, loadMetaMaskMobileActionManifest } from "./manifest.js";
4
+ import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
5
+ import { importRecipeHarness, importRecipeHarnessRuntimeCdp, importRecipeHarnessRuntimeReactNativeBridge, runnerDir } from "./paths.js";
6
+ async function createMetaMaskMobileRunner(options = {}) {
7
+ return createMetaMaskRunner(
8
+ "mobile",
9
+ options.actionManifest ?? loadMetaMaskMobileActionManifest()
10
+ );
11
+ }
12
+ async function createMetaMaskExtensionRunner(options = {}) {
13
+ return createMetaMaskRunner(
14
+ "extension",
15
+ options.actionManifest ?? loadMetaMaskExtensionActionManifest()
16
+ );
17
+ }
18
+ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
19
+ const {
20
+ createRecipeRunner,
21
+ createStandardCoreAdapters,
22
+ createStandardUiAdapters
23
+ } = await importRecipeHarness();
24
+ const { createCdpWebUiTransport } = await importRecipeHarnessRuntimeCdp();
25
+ const { createReactNativeBridgeUiTransport } = await importRecipeHarnessRuntimeReactNativeBridge();
26
+ const actions = [
27
+ ...actionManifest.supported_official_actions,
28
+ ...(actionManifest.custom_actions ?? []).map((entry) => entry.name)
29
+ ];
30
+ const declaredActions = new Set(actions);
31
+ const core = createStandardCoreAdapters({ actions });
32
+ const projectOwnedOfficialActions = /* @__PURE__ */ new Set(["app.status"]);
33
+ const ui = createStandardUiAdapters({
34
+ actions: actions.filter((action) => !projectOwnedOfficialActions.has(action)),
35
+ transport: createMetaMaskUiTransport(adapter, {
36
+ createCdpWebUiTransport,
37
+ createReactNativeBridgeUiTransport
38
+ })
39
+ });
40
+ const existing = new Set([...core, ...ui].map((entry) => entry.action));
41
+ const custom = createMetaMaskAdapters(adapter).filter(
42
+ (entry) => declaredActions.has(entry.action) && !existing.has(entry.action)
43
+ );
44
+ const autoHudDisabled = process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false";
45
+ const logger = options.quietStdout ? new console.Console(process.stderr) : console;
46
+ return createRecipeRunner({
47
+ actionManifest,
48
+ adapters: [...core, ...ui, ...custom],
49
+ logger,
50
+ recording: {
51
+ targetProvider: createMetaMaskRecordingTargetProvider(adapter)
52
+ },
53
+ runner: runnerProvenance(),
54
+ hud: autoHudDisabled ? false : {
55
+ enabled: true,
56
+ display: {
57
+ layout: "docked-bottom",
58
+ position: "bottom",
59
+ showTitle: false,
60
+ showDebug: false,
61
+ maxDetailLines: 2
62
+ }
63
+ }
64
+ });
65
+ }
66
+ function runnerProvenance() {
67
+ return {
68
+ source: runnerDir,
69
+ git_ref: runnerGitRef(),
70
+ name: "@metamask/recipe-runner"
71
+ };
72
+ }
73
+ function runnerGitRef() {
74
+ try {
75
+ return execSync("git rev-parse HEAD", {
76
+ cwd: runnerDir,
77
+ encoding: "utf8",
78
+ stdio: ["ignore", "pipe", "ignore"]
79
+ }).trim();
80
+ } catch {
81
+ return "unknown";
82
+ }
83
+ }
84
+ export {
85
+ createMetaMaskExtensionRunner,
86
+ createMetaMaskMobileRunner,
87
+ createMetaMaskRunner
88
+ };
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,119 @@
1
+ # The adapter surface — one seam for per-platform behavior
2
+
3
+ `mm-harness` runs against three platforms (mobile | extension | core). The
4
+ danger is that every command re-derives "what does this platform do?" with its
5
+ own `if (adapter === 'mobile') … else if (adapter === 'extension') …` ladder.
6
+ When it does, a platform gets forgotten in one command but not another — the
7
+ class of bug where `doctor` reported a live runtime section for mobile only,
8
+ `stop` refused every non-mobile adapter, and `logs` knew a single extension log
9
+ file instead of all three. Those are *platform-blind* commands.
10
+
11
+ The **adapter surface** is the fix: one interface per platform, resolved through
12
+ a registry, that owns the behavior a command would otherwise branch on.
13
+
14
+ ## The interface
15
+
16
+ `src/adapters/surface.ts` defines `AdapterSurface` and the registry
17
+ `getAdapterSurface(adapter)`. Each platform ships one implementation:
18
+
19
+ - `src/adapters/mobile/surface.ts`
20
+ - `src/adapters/extension/surface.ts`
21
+ - `src/adapters/core/surface.ts`
22
+
23
+ ```ts
24
+ interface AdapterSurface {
25
+ readonly adapter: MetaMaskRecipeAdapter;
26
+ readonly headless: boolean; // core runs no app/dev server
27
+ resolveSlotPorts(target: string): void; // context > pool > formula (no-op for core)
28
+ runtimeStatus(target: string): Promise<AdapterRuntimeStatus>; // read-only readiness for doctor
29
+ devServer: {
30
+ describe(): string; // "Metro" | "webpack watcher" | "no dev server (headless)"
31
+ stop(target: string): AdapterDevServerStop; // idempotent, slot-scoped; headless = teach
32
+ };
33
+ logSources(target: string): AdapterLogSource[]; // ordered candidate log files (empty for core)
34
+ hints: { launch: string; relaunch: string }; // platform-phrased Next: hints
35
+ }
36
+ ```
37
+
38
+ `runtimeStatus` returns a normalized shape so `doctor` renders one line the same
39
+ way for every platform:
40
+
41
+ ```ts
42
+ interface AdapterRuntimeStatus {
43
+ decision: string;
44
+ reasonCode?: string;
45
+ reasons: string[];
46
+ deps?: string;
47
+ devServer?: { label: string; status: string }; // absent for headless core
48
+ }
49
+ ```
50
+
51
+ The implementations are thin: they delegate to the readiness/port plumbing that
52
+ already existed (`decideExtensionReadiness`, `mobileRuntimeStatus`, the slot-port
53
+ resolvers and the webpack-watcher stop, all re-homed to
54
+ `src/adapters/slot-ports.ts`). The surface is an organizing seam, not a rewrite.
55
+
56
+ ## The rule
57
+
58
+ **A command never branches on adapter for behavior the surface owns.**
59
+
60
+ - Do not write `if (adapter === 'core')` — ask `surface.headless`.
61
+ - Do not write `adapter === 'mobile' ? metroPorts() : extensionPorts()` — call
62
+ `surface.resolveSlotPorts(target)`.
63
+ - Do not print `adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness
64
+ launch'` — use `surface.hints.launch`.
65
+
66
+ A new platform behavior is added by **extending the surface** (a new member on
67
+ the interface plus its three implementations), never by adding another branch to
68
+ a command. TypeScript then makes it impossible to ship a platform that forgot the
69
+ new member, because each implementation is annotated `: AdapterSurface`.
70
+
71
+ What the surface deliberately does **not** own: a command may still branch on
72
+ adapter for a *mechanism* that is genuinely platform-specific and not part of the
73
+ interface — e.g. `fixtures set` uses a shell arm on mobile and the engine path on
74
+ extension. Those branches select a mechanism; they never re-derive readiness,
75
+ ports, log locations, dev-server lifecycle, or Next: phrasing, which are the
76
+ surface's responsibility.
77
+
78
+ ## Migration status
79
+
80
+ Every command that used to branch on adapter now resolves through the surface:
81
+
82
+ | command | surface-backed | uses |
83
+ | --- | --- | --- |
84
+ | `doctor` | yes | `resolveSlotPorts` + `runtimeStatus` (mobile/extension/core runtime section) |
85
+ | `launch` | yes | `resolveSlotPorts` |
86
+ | `stop` | yes | `resolveSlotPorts` + `devServer.stop` (+ headless teaching) |
87
+ | `logs` | yes | `logSources` + `hints.launch` + `headless` |
88
+ | `debug` | yes | `headless` + `hints.relaunch` (core teaching); flag semantics stay per-command |
89
+ | `fixtures` | yes | `headless` + `hints` (retry/launch); the set *mechanism* stays per-platform |
90
+ | `run` / `call` | n/a | engine path; core-headless is handled by the heal contract, not the surface |
91
+ | `flows` | no (by design) | recipe-library flows are adapter-global; not a platform-owned behavior |
92
+ | `completion-candidates` | partial | `actions` scope to the detected checkout adapter; `flows` are adapter-global |
93
+
94
+ `flows` is intentionally not surface-backed: a recipe-library flow can compose
95
+ actions across platforms, so flows are adapter-global by design rather than a
96
+ platform-blind gap. `completion-candidates actions` already resolves the adapter
97
+ from the checkout context (cwd/`--target`); only the adapter-global `flows`
98
+ candidates are unscoped, matching the flows model.
99
+
100
+ ## How this composes with the UX principles
101
+
102
+ `docs/UX-PRINCIPLES.md` principle 1 (*Context-aware by default* — "output is
103
+ scoped to the platform… help, flag lists, and completion candidates shrink to
104
+ what applies here") states the intent. The adapter surface is its **enforcement
105
+ mechanism**: a command that resolves platform behavior through the surface is
106
+ context-aware by construction, and one that hand-rolls an adapter ladder is the
107
+ exact failure principle 1 warns against. When adding a command or output path,
108
+ satisfy principle 1 by going through the surface.
109
+
110
+ ## Adding a platform behavior — checklist
111
+
112
+ 1. Add the member to `AdapterSurface` in `src/adapters/surface.ts`.
113
+ 2. Implement it in all three `src/adapters/*/surface.ts` (TypeScript will not
114
+ compile until you do).
115
+ 3. Have the command call `getAdapterSurface(adapter).<member>` instead of
116
+ branching.
117
+ 4. Cover the closed blind spot with a contract test (see
118
+ `tests/contract/adapter-surface.test.sh`), and keep the registry-completeness
119
+ assertion green.
package/docs/CLI-SPEC.md CHANGED
@@ -304,6 +304,30 @@ Recovery is silent in human mode. With `--json`: `"recovered": ["metro.restarted
304
304
  **Maps-to:** `sync` ← C:`sync`, D:`sync`,`update\|sync-runtime`; `set` ← C:`setup-wallet\|wallet-setup`,`unlock`,`setup:ios/android`(wallet half).
305
305
  **Extension `set`:** [GAP] no standalone path today — the first-run wallet is seeded by the legacy `mme-recipe up`; the end-state home is `fixtures set` (launch / `launch --verify` never seed).
306
306
 
307
+
308
+ ## `provision` (REAL → thin mobile setup path)
309
+
310
+ **Synopsis:** `mm-harness provision [runway ios] [flags]`
311
+
312
+ Installs the cached Runway iOS dev client onto a prepared mobile slot. It does not install JavaScript dependencies or start Metro; launch owns those at dispatch time. The legacy `install --runway` compatibility path maps to the same adapter surface; new farm callers should use `provision`.
313
+
314
+ | Flag | Type | Default | ENV (agent) | Audience | Description |
315
+ |---|---|---|---|---|---|
316
+ | `--adapter <mobile\|extension\|core>` | adapter | auto | — | agent | Mobile installs Runway; extension/core teach |
317
+ | `--target <path>` | path | cwd | — | agent | Slot checkout |
318
+ | `--platform <ios>` | string | `ios` | `PLATFORM` | agent | Provisioned platform |
319
+ | `--simulator <name\|udid>` / `--device <name\|udid>` | string | runtime context | `IOS_SIMULATOR` | agent | Target simulator |
320
+ | `--runtime <id>` | string | runtime context | `IOS_RUNTIME` | agent | iOS runtime for simulator creation |
321
+ | `--device-type <id>` | string | runtime context | `IOS_DEVICE_TYPE` | agent | Device type for simulator creation |
322
+ | `--branch <ref>` / `--default-branch <ref>` / `--run <id>` | string | git branch / main | — | agent | Runway artifact selection |
323
+ | `--cache-root <dir>` | path | XDG cache | — | agent | Shared artifact cache |
324
+ | `--slot <id>` | string | runtime context | — | agent | Farm slot id recorded in the provision baseline |
325
+ | `--watcher-port <port>` | number | runtime context | `WATCHER_PORT`/`METRO_PORT` | agent | Metro/watcher port carried through context and rerun hints |
326
+ | `--runtime-dir <dir>` | relative path | `temp/recipe/runtime` | `RECIPE_RUNTIME_DIR` | agent | Runtime directory containing `agentic-runtime.json` and receiving `runway-provision.json` |
327
+ | `--force` | bool | false | — | agent | Reinstall even when the bundle is already present |
328
+ | `--resolve-only` | bool | false | — | agent | Resolve run/revision/artifact metadata only; no simulator creation, cache write, download, install, or baseline write |
329
+ | `--json` | bool | false | — | agent | Machine envelope on stdout; progress/errors on stderr |
330
+
307
331
  ## `run` (ROUTES-NOW → main PROVE verb)
308
332
 
309
333
  **Synopsis:** `mm-harness run <recipe.json> [flags]` — validate (adapter-aware) + execute a recipe, write evidence.
@@ -714,9 +738,9 @@ a flag + default (`[ENV-GAP]`); internal/agent-only vars are KEEP-INTERNAL;
714
738
  **Implemented:** the `[ENV-GAP]` flags exist with resolution order **flag > env >
715
739
  config/default** (the flag sets the env var the porcelain reads, so it wins; an
716
740
  absent flag leaves the agent/CI env untouched):
717
- - `launch --device` (`IOS_SIMULATOR` for ios · `ADB_SERIAL`/`ANDROID_SERIAL`/`ANDROID_DEVICE` for android)
741
+ - `launch --device` (`IOS_SIMULATOR` for ios · `ADB_SERIAL`/`ANDROID_SERIAL`/`ANDROID_DEVICE` for android); `provision --device` is an alias for `--simulator`
718
742
  - `launch --cdp-port` (`CDP_PORT`/`RECIPE_CDP_PORT`) · `launch --watcher-port` (`WATCHER_PORT`/`METRO_PORT`/`RECIPE_WATCHER_PORT`) — numeric, teaching error otherwise
719
- - `launch --build` covers `MOBILE_PREFLIGHT_MODE` (tier) · `logs --full`/`--events` cover `RECIPE_LOG_UI`/`RECIPE_LOG_EVENTS`
743
+ - `launch --build` covers `MOBILE_PREFLIGHT_MODE` (tier) · `provision --slot --runtime-dir --watcher-port` accepts farm slot context · `logs --full`/`--events` cover `RECIPE_LOG_UI`/`RECIPE_LOG_EVENTS`
720
744
  - `fixtures set --fixture` (`RECIPE_WALLET_FIXTURE`) · `run`/`flows --library` (`RECIPE_LIBRARY_PATH`)
721
745
  - `MM_PASSWORD` stays flag-less by design — `fixtures set` reads the password FROM the fixture.
722
746
 
@@ -951,4 +975,3 @@ cheapest next action so the path converges rather than rebuilding from scratch):
951
975
  tab (closing strays, reopening if needed) and confirms it with the health probe.
952
976
  - **Health probe** — read-only liveness probe for the running extension over CDP;
953
977
  the final gate of convergence and the standalone answer for `verify`.
954
-
@@ -19,6 +19,9 @@ The CLI answers relative to the checkout it is standing in.
19
19
  hardcode pool prefixes (they go stale on rename).
20
20
  - Output is scoped to the platform: an extension-only field is noise on a mobile
21
21
  slot; help, flag lists, and completion candidates shrink to what applies here.
22
+ - Enforcement: platform-specific behavior is resolved through the adapter surface
23
+ (`getAdapterSurface(adapter)`), never a per-command `if (adapter === …)` ladder —
24
+ that ladder is how commands go platform-blind. See docs/ADAPTER-SURFACE.md.
22
25
 
23
26
  ## 2. Never silent, never opaque
24
27
  Long operations show intent immediately and progress continuously.
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.3.9",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
7
7
  },
8
8
  "scripts": {
9
+ "build": "node scripts/build-dist.mjs",
10
+ "prepack": "npm run build",
9
11
  "dev:link-farmslot": "node scripts/link-local-farmslot.mjs",
10
12
  "check": "node scripts/check.mjs",
11
13
  "self-test": "bin/mm-harness self-test",
@@ -26,8 +28,13 @@
26
28
  "ws": "8.21.0"
27
29
  },
28
30
  "devDependencies": {
31
+ "@eslint/js": "^9",
29
32
  "@types/node": "^22.0.0",
30
- "typescript": "^5.6.0"
33
+ "esbuild": "0.28.1",
34
+ "eslint": "^9",
35
+ "globals": "^17.7.0",
36
+ "typescript": "^5.6.0",
37
+ "typescript-eslint": "^8"
31
38
  },
32
39
  "main": "./src/index.ts",
33
40
  "types": "./src/index.ts",
@@ -42,6 +49,7 @@
42
49
  },
43
50
  "files": [
44
51
  "bin",
52
+ "dist",
45
53
  "src",
46
54
  "adapters",
47
55
  "library",
@@ -0,0 +1,71 @@
1
+ // Core surface: headless. No app, no dev server — dependency presence is the
2
+ // only runtime signal, and lifecycle commands teach the reachable headless path.
3
+ import path from 'node:path';
4
+
5
+ import { depsCheck } from '@farmslot/recipe-harness/runtime/deps-readiness';
6
+
7
+ import type {
8
+ AdapterDevServerStop,
9
+ AdapterLogSource,
10
+ AdapterRuntimeStatus,
11
+ AdapterRunwayProvisionResult,
12
+ AdapterSurface,
13
+ } from '../surface.ts';
14
+
15
+ export const coreSurface: AdapterSurface = {
16
+ adapter: 'core',
17
+ headless: true,
18
+
19
+ resolveSlotPorts(): void {
20
+ // Headless: no ports or device to resolve.
21
+ },
22
+
23
+ async runtimeStatus(target: string): Promise<AdapterRuntimeStatus> {
24
+ const deps = depsCheck(path.resolve(target));
25
+ const ready = deps.status === 'current';
26
+ return {
27
+ decision: ready ? 'ready' : 'install',
28
+ reasonCode: ready ? 'deps-present' : `deps-${deps.status}`,
29
+ reasons: [
30
+ ready
31
+ ? 'Core is headless; dependencies are installed. Run recipes with mm-harness run.'
32
+ : 'Core is headless; dependencies are not fully installed.',
33
+ ],
34
+ deps: deps.status,
35
+ };
36
+ },
37
+
38
+ runwayProvision: {
39
+ async run(target, options): Promise<AdapterRunwayProvisionResult> {
40
+ return {
41
+ schemaVersion: 1,
42
+ command: 'provision',
43
+ adapter: 'core',
44
+ target: path.resolve(target),
45
+ status: 'fail',
46
+ exitCode: 2,
47
+ error: { code: 'UNSUPPORTED_ADAPTER', message: 'core is headless and has no mobile simulator to provision.', userAction: options.rerunCommand || 'mm-harness run <recipe> --adapter core' },
48
+ };
49
+ },
50
+ },
51
+
52
+ devServer: {
53
+ describe: () => 'no dev server (headless)',
54
+ stop(): AdapterDevServerStop {
55
+ return {
56
+ kind: 'headless',
57
+ message: 'core is headless — no dev server runs for a core checkout',
58
+ userAction: 'use mm-harness verify (readiness) or mm-harness run (execute a recipe)',
59
+ };
60
+ },
61
+ },
62
+
63
+ logSources(): AdapterLogSource[] {
64
+ return [];
65
+ },
66
+
67
+ hints: {
68
+ launch: 'mm-harness run <recipe> # run recipes against the headless core',
69
+ relaunch: 'mm-harness verify',
70
+ },
71
+ };
@@ -0,0 +1,88 @@
1
+ // Extension surface: delegates to the existing extension readiness/port plumbing
2
+ // and the webpack watcher stop.
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+
6
+ import { recipeRuntimePath } from '../../paths.ts';
7
+ import { resolveExtensionSlotPorts, stopExtensionWatcher } from '../slot-ports.ts';
8
+ import { decideExtensionReadiness } from './runtime-decision.ts';
9
+ import type {
10
+ AdapterDevServerStop,
11
+ AdapterLogSource,
12
+ AdapterRuntimeStatus,
13
+ AdapterRunwayProvisionResult,
14
+ AdapterSurface,
15
+ } from '../surface.ts';
16
+
17
+ // Map the webpack watch-log health onto a dev-server up/down/building/errors
18
+ // status doctor renders the same way as mobile's Metro line.
19
+ function watcherStatus(buildLog: string): string {
20
+ if (buildLog === 'ok') return 'up';
21
+ if (buildLog === 'no-watch') return 'down';
22
+ return buildLog; // 'building' | 'errors'
23
+ }
24
+
25
+ export const extensionSurface: AdapterSurface = {
26
+ adapter: 'extension',
27
+ headless: false,
28
+
29
+ resolveSlotPorts(target: string): void {
30
+ resolveExtensionSlotPorts(target);
31
+ },
32
+
33
+ async runtimeStatus(target: string): Promise<AdapterRuntimeStatus> {
34
+ const cdpPort = process.env.CDP_PORT ? parseInt(process.env.CDP_PORT, 10) : undefined;
35
+ const report = await decideExtensionReadiness(target, { cdpPort });
36
+ return {
37
+ decision: report.decision,
38
+ reasonCode: report.reasonCode,
39
+ reasons: report.reasons,
40
+ deps: report.checks.deps.status,
41
+ devServer: { label: 'webpack', status: watcherStatus(report.checks.buildLog.status) },
42
+ };
43
+ },
44
+
45
+ runwayProvision: {
46
+ async run(target, options): Promise<AdapterRunwayProvisionResult> {
47
+ return {
48
+ schemaVersion: 1,
49
+ command: 'provision',
50
+ adapter: 'extension',
51
+ target: path.resolve(target),
52
+ status: 'fail',
53
+ exitCode: 2,
54
+ error: {
55
+ code: 'UNSUPPORTED_ADAPTER',
56
+ message: 'extension uses a browser runtime; Runway mobile provisioning is not supported.',
57
+ userAction: options.rerunCommand || 'mm-harness provision --adapter extension',
58
+ },
59
+ };
60
+ },
61
+ },
62
+
63
+ devServer: {
64
+ describe: () => 'webpack watcher',
65
+ stop(target: string): AdapterDevServerStop {
66
+ const signalled = stopExtensionWatcher(target);
67
+ const port = process.env.WATCHER_PORT ?? 'default';
68
+ spawnSync('tmux', ['kill-window', '-t', `webpack-${port}`], { stdio: 'ignore', timeout: 2000 });
69
+ const summary = signalled > 0
70
+ ? `stopped webpack watcher (${signalled} process${signalled === 1 ? '' : 'es'}) for ${target}`
71
+ : `webpack watcher not running for ${target} — nothing to stop`;
72
+ return { kind: 'stopped', status: 0, summary, signalled };
73
+ },
74
+ },
75
+
76
+ logSources(target: string): AdapterLogSource[] {
77
+ return [
78
+ { label: 'webpack', path: recipeRuntimePath(target, 'webpack.log') },
79
+ { label: 'watcher', path: recipeRuntimePath(target, 'recipe-harness-webpack.log') },
80
+ { label: 'rebuild', path: recipeRuntimePath(target, 'rebuild.log') },
81
+ ];
82
+ },
83
+
84
+ hints: {
85
+ launch: 'mm-harness launch',
86
+ relaunch: 'mm-harness launch --build',
87
+ },
88
+ };