@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.
- package/CHANGELOG.md +45 -1
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +315 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/slot-ports.js +146 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/ADAPTER-SURFACE.md +119 -0
- package/docs/CLI-SPEC.md +26 -3
- package/docs/UX-PRINCIPLES.md +3 -0
- package/package.json +10 -2
- package/src/adapters/core/surface.ts +71 -0
- package/src/adapters/extension/surface.ts +88 -0
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/surface.ts +71 -0
- package/src/adapters/slot-ports.ts +165 -0
- package/src/adapters/surface.ts +117 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +239 -49
- package/src/commands/debug.ts +3 -1
- package/src/commands/fixtures.ts +13 -8
- package/src/commands/launch.ts +7 -156
- package/src/commands/logs.ts +29 -13
- package/src/harness.ts +140 -3
- 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
|
-
|
package/docs/UX-PRINCIPLES.md
CHANGED
|
@@ -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
|
+
"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
|
-
"
|
|
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
|
+
};
|