@deeeed/metamask-harness 0.3.6 → 0.3.8

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8 - 2026-07-04
4
+
5
+ ### Fixed
6
+ - **Mobile launch streams progress in mode** — mobile prepare leaves now run through , which tees the child's stdout+stderr to the parent's stderr live while stdout stays reserved for the JSON envelope. A multi-minute {
7
+ "schemaVersion": 1,
8
+ "command": "launch",
9
+ "status": "fail",
10
+ "exitCode": 2,
11
+ "error": {
12
+ "code": "USAGE",
13
+ "message": "could not detect the MetaMask repo type for /private/tmp/claude-501/-Users-deeeed-dev-metamask-principal-pitch-2025/4645d206-3e7f-482d-a5c1-2274f26a7411/scratchpad/emh-review",
14
+ "userAction": "cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>"
15
+ }
16
+ } is no longer silent (previously buffered leaf output and suppressed it entirely under ). Contract test asserts leaf progress reaches stderr, the envelope stays clean, and the streaming conversion preserves the exit-code contract.
17
+
18
+ ## 0.3.7 - 2026-07-04
19
+
20
+ ### Fixed
21
+ - **Overlay install manifest no longer advertises the retired `launch.sh`/`live.sh`** — mobile launch/live orchestration is in-process now (`mm-harness launch`), and `install_v1_runner_assets` deletes `scripts/launch.sh` and `scripts/live.sh` on both install paths. The product-owned/metadata-only manifest already advertised `verify` only, but the overlay manifest's `runtimeHelpers` still listed `launch: scripts/launch.sh` and `live: scripts/live.sh` — paths the same install had just removed, a manifest that pointed at nonexistent files. The overlay manifest now advertises `verify` only, matching the product-owned path. A new manifest-honesty contract check in `tests/contract/mobile-inject.test.sh` asserts every advertised `runtimeHelpers` path exists on disk and that `launch`/`live` are not advertised, for both the product-owned and a full overlay install, so this cannot recur in either mode.
22
+
3
23
  ## 0.3.6 - 2026-07-04
4
24
 
5
25
  ### Fixed
@@ -669,9 +669,12 @@ node -e '
669
669
  protocolVersion: "v1",
670
670
  actionManifestPath: harnessRel + "/action-manifest.json",
671
671
  runnerEntrypoint: harnessRel + "/runner/bin/mm-harness",
672
+ // launch/live orchestration is in-process (mm-harness launch); only verify
673
+ // remains a shell helper. install_v1_runner_assets deletes scripts/launch.sh
674
+ // and scripts/live.sh, so advertising them here would be a manifest that
675
+ // points at files this same install removed. Keep this in sync with the
676
+ // product-owned block above (verify only).
672
677
  runtimeHelpers: {
673
- launch: harnessRel + "/scripts/launch.sh",
674
- live: harnessRel + "/scripts/live.sh",
675
678
  verify: harnessRel + "/scripts/verify.sh"
676
679
  },
677
680
  installedPaths: [harnessRel + "/runner", harnessRel + "/scripts", harnessRel + "/action-manifest.json", "app/dev-tools/AgenticService"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -7,7 +7,7 @@
7
7
 
8
8
  import path from 'node:path';
9
9
 
10
- import { EXIT, spawnScript } from '../../commands/shared.ts';
10
+ import { EXIT, spawnScriptStreaming } from '../../commands/shared.ts';
11
11
  import type { ScriptResult } from '../../commands/shared.ts';
12
12
  import { runnerDir } from '../../paths.ts';
13
13
  import {
@@ -27,7 +27,11 @@ export { type MobileRuntimeDecisionReport };
27
27
  const POD_PROBE_ENV: Record<string, string> = { FORCE_COLOR: '0', NO_COLOR: '1' };
28
28
 
29
29
  export interface PrepareMobileOptions extends MobileRuntimeDecisionOptions {
30
- /** Pass through to leaf-script spawnScript calls so output is suppressed in --json mode. */
30
+ /**
31
+ * Whether the caller emits a --json envelope on stdout. Leaf progress always
32
+ * streams to stderr (see dispatchAction/spawnScriptStreaming); this only gates
33
+ * the in-process teaching messages so they never pollute the stdout envelope.
34
+ */
31
35
  json?: boolean;
32
36
  /**
33
37
  * Passed to open-device.sh --preflight-mode.
@@ -106,7 +110,7 @@ export async function prepareMobile(
106
110
  )
107
111
  : report.actions;
108
112
  for (const action of actions) {
109
- const result = dispatchAction(action, target, platform, json, preflightMode);
113
+ const result = await dispatchAction(action, target, platform, json, preflightMode);
110
114
  if (result.status !== 0) return result;
111
115
  }
112
116
 
@@ -132,13 +136,13 @@ export async function prepareMobile(
132
136
  }
133
137
  case 'ready': {
134
138
  // Deps resolved and runtime appears ready — still confirm bridge is live.
135
- const bridge = dispatchAction({ id: 'wait-for-bridge', cwd: target }, target, platform, json, preflightMode);
139
+ const bridge = await dispatchAction({ id: 'wait-for-bridge', cwd: target }, target, platform, json, preflightMode);
136
140
  if (bridge.status !== 0) return bridge;
137
141
  break;
138
142
  }
139
143
  case 'launch': {
140
144
  for (const action of postInstall.actions) {
141
- const result = dispatchAction(action, target, platform, json, preflightMode);
145
+ const result = await dispatchAction(action, target, platform, json, preflightMode);
142
146
  if (result.status !== 0) return result;
143
147
  }
144
148
  break;
@@ -157,51 +161,53 @@ export async function prepareMobile(
157
161
  return { status: 0, output: '' };
158
162
  }
159
163
 
160
- function dispatchAction(
164
+ // Long-running leaves stream their output to stderr live (spawnScriptStreaming),
165
+ // so the multi-minute prepare phase is not silent; stdout stays reserved for the
166
+ // caller's --json envelope. Async so the parent can await each leaf in sequence.
167
+ async function dispatchAction(
161
168
  action: { id: string; argv?: string[]; cwd?: string },
162
169
  target: string,
163
170
  platform: string,
164
171
  json: boolean,
165
172
  preflightMode = 'fast',
166
- ): ScriptResult {
173
+ ): Promise<ScriptResult> {
167
174
  const cwd = action.cwd ?? target;
168
175
  switch (action.id) {
169
176
  case 'yarn-setup': {
170
177
  // Leaf: install node_modules (deps missing or stale); `yarn setup` runs pods.
171
178
  const leaf = path.join(runnerDir, 'adapters/mobile/yarn-setup.sh');
172
- return spawnScript(leaf, ['--target', cwd], target, json, POD_PROBE_ENV);
179
+ return spawnScriptStreaming(leaf, ['--target', cwd], target, POD_PROBE_ENV);
173
180
  }
174
181
  case 'start-metro': {
175
182
  // Leaf: ensure Metro is running; argv may carry --clear for cache reset.
176
183
  const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
177
184
  const extra: string[] = action.argv ?? [];
178
- return spawnScript(leaf, ['--target', cwd, ...extra], target, json);
185
+ return spawnScriptStreaming(leaf, ['--target', cwd, ...extra], target);
179
186
  }
180
187
  case 'prewarm-bundle': {
181
188
  // Leaf: curl the bundle URL before opening the dev client.
182
189
  const leaf = path.join(runnerDir, 'adapters/mobile/prewarm-bundle.sh');
183
- return spawnScript(leaf, ['--platform', platform, '--target', cwd], target, json);
190
+ return spawnScriptStreaming(leaf, ['--platform', platform, '--target', cwd], target);
184
191
  }
185
192
  case 'wait-for-bridge': {
186
193
  // Leaf: poll CDP bridge until a route target registers.
187
194
  const leaf = path.join(runnerDir, 'adapters/mobile/wait-for-bridge.sh');
188
- return spawnScript(leaf, ['--target', cwd], target, json);
195
+ return spawnScriptStreaming(leaf, ['--target', cwd], target);
189
196
  }
190
197
  case 'clear-metro-cache': {
191
198
  // Legacy alias for start-metro --clear (kept for backward compat).
192
199
  const leaf = path.join(runnerDir, 'adapters/mobile/start-metro.sh');
193
- return spawnScript(leaf, ['--target', cwd, '--clear'], target, json);
200
+ return spawnScriptStreaming(leaf, ['--target', cwd, '--clear'], target);
194
201
  }
195
202
  case 'launch-mobile-runtime': {
196
203
  // Leaf: open the MetaMask Mobile dev client on the simulator/device. In
197
204
  // auto/rebuild modes this runs the native build (`yarn start:*`), which
198
205
  // triggers pod install — pass the pod-probe env so it stays FORCE_COLOR-safe.
199
206
  const leaf = path.join(runnerDir, 'adapters/mobile/open-device.sh');
200
- return spawnScript(
207
+ return spawnScriptStreaming(
201
208
  leaf,
202
209
  ['--platform', platform, '--target', cwd, '--preflight-mode', preflightMode],
203
210
  target,
204
- json,
205
211
  POD_PROBE_ENV,
206
212
  );
207
213
  }
@@ -7,7 +7,7 @@
7
7
  // For node invocations (bin === process.execPath) the stem is derived from the
8
8
  // script path in args[0], e.g. MM_HARNESS_SCRIPT_BIN_OPEN_DEBUG_MJS.
9
9
 
10
- import { spawnSync } from 'node:child_process';
10
+ import { spawn, spawnSync } from 'node:child_process';
11
11
  import path from 'node:path';
12
12
 
13
13
  import { detectAdapter } from '../harness.ts';
@@ -151,6 +151,67 @@ export function spawnScript(
151
151
  return { status: result.status ?? 1, output };
152
152
  }
153
153
 
154
+ // Streaming variant of spawnScript for long-running leaves (mobile prepare:
155
+ // native build + Metro + health-bridge poll, minutes long). spawnScript buffers
156
+ // via spawnSync and, in --json mode, suppresses output entirely — so those leaves
157
+ // run with zero feedback until exit. This tees the child's stdout+stderr to the
158
+ // parent's STDERR live (so the --json envelope on stdout stays clean) while still
159
+ // capturing the combined output for heal classification. Same seam, leaf-invoke
160
+ // resolution, missing-leaf pre-check, and spawn-error contract as spawnScript.
161
+ export function spawnScriptStreaming(
162
+ script: string,
163
+ args: string[],
164
+ cwd: string,
165
+ env?: Record<string, string>,
166
+ ): Promise<ScriptResult> {
167
+ const isNodeScript = script === process.execPath && args.length > 0;
168
+ const stem = isNodeScript
169
+ ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
170
+ : path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
171
+ const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
172
+ const bin = override ?? script;
173
+ const directArgs = override !== undefined && isNodeScript ? args.slice(1) : args;
174
+
175
+ if (shellLeafMissing(bin)) {
176
+ const message =
177
+ `leaf could not start: ${path.basename(bin)} (ENOENT)\n` +
178
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
179
+ process.stderr.write(`${message}\n`);
180
+ return Promise.resolve({ status: 1, output: message });
181
+ }
182
+
183
+ const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
184
+ return new Promise<ScriptResult>((resolve) => {
185
+ const child = spawn(invokeBin, spawnArgs, {
186
+ cwd,
187
+ env: env ? { ...process.env, ...env } : process.env,
188
+ stdio: ['ignore', 'pipe', 'pipe'],
189
+ });
190
+ let output = '';
191
+ // Route BOTH child streams to the parent's stderr, live: stdout is reserved
192
+ // for the harness --json envelope, so all human/leaf progress goes to stderr.
193
+ const tee = (chunk: Buffer): void => {
194
+ const text = chunk.toString('utf8');
195
+ output += text;
196
+ process.stderr.write(text);
197
+ };
198
+ child.stdout?.on('data', tee);
199
+ child.stderr?.on('data', tee);
200
+ child.on('error', (error: NodeJS.ErrnoException) => {
201
+ const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
202
+ const code = error.code ?? 'ESPAWN';
203
+ const message =
204
+ `leaf could not start: ${leaf} (${code})\n` +
205
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
206
+ process.stderr.write(`${message}\n`);
207
+ resolve({ status: 1, output: message });
208
+ });
209
+ child.on('close', (status) => {
210
+ resolve({ status: status ?? 1, output });
211
+ });
212
+ });
213
+ }
214
+
154
215
  // Both escapes from a failed repo-type detection (defined at the detection source
155
216
  // in harness.ts): where to run it (checkout/target) and how to force it (adapter).
156
217
  export { ADAPTER_DETECT_NEXT } from '../harness.ts';