@deeeed/metamask-harness 0.3.3 → 0.3.5

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 (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/adapters/core/cleanup.sh +0 -0
  3. package/adapters/core/inject.sh +0 -0
  4. package/adapters/extension/cleanup.mjs +0 -0
  5. package/adapters/extension/ensure-browser.sh +0 -0
  6. package/adapters/extension/inject.mjs +0 -0
  7. package/adapters/extension/launch-browser.cjs +0 -0
  8. package/adapters/extension/launch.sh +0 -0
  9. package/adapters/extension/live.sh +0 -0
  10. package/adapters/extension/readiness.mjs +0 -0
  11. package/adapters/extension/refresh-build.sh +0 -0
  12. package/adapters/extension/seed-fixture.sh +0 -0
  13. package/adapters/extension/sidepanel-toggle.sh +0 -0
  14. package/adapters/extension/snapshot-dist.sh +0 -0
  15. package/adapters/extension/start-watch.sh +0 -0
  16. package/adapters/extension/verify.sh +0 -0
  17. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  18. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  19. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  20. package/adapters/mobile/cleanup.sh +0 -0
  21. package/adapters/mobile/inject.sh +0 -0
  22. package/adapters/mobile/lib/metro-listener.sh +0 -0
  23. package/adapters/mobile/open-device.sh +0 -0
  24. package/adapters/mobile/prewarm-bundle.sh +0 -0
  25. package/adapters/mobile/start-metro.sh +0 -0
  26. package/adapters/mobile/verify.sh +0 -0
  27. package/adapters/mobile/wait-for-bridge.sh +0 -0
  28. package/adapters/mobile/yarn-setup.sh +0 -0
  29. package/adapters/shared/activate-repo-node.sh +0 -0
  30. package/adapters/shared/cli-ux.sh +0 -0
  31. package/adapters/shared/ensure-runner-deps.sh +0 -0
  32. package/adapters/shared/harness-path.sh +0 -0
  33. package/adapters/shared/hash-helpers.sh +0 -0
  34. package/adapters/shared/json-field.sh +0 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +0 -0
  36. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  37. package/package.json +2 -2
  38. package/scripts/completions.sh +0 -0
  39. package/scripts/install-completions.sh +0 -0
  40. package/src/commands/shared.ts +27 -3
  41. package/src/harness.ts +51 -10
  42. package/src/leaf-invoke.ts +28 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 - 2026-07-04
4
+
5
+ ### Fixed
6
+ - **IMP-24b: shell leaves run even when packed without the exec bit** — `yarn pack` (used by `yarn npm publish`) strips the executable bit off shipped `*.sh` leaves to `644`, even though git stores `100755` and `npm pack` preserves `755`. The 0.3.4 fix (git `100755` + an `npm pack` tarball test) was therefore insufficient: the published `yarn`-packed tarball still shipped `644` leaves and a fresh `npm i -g` hit `EACCES` on the first leaf. `spawnScript` now invokes shell leaves through `bash <leaf>` instead of exec'ing them directly, so the leaf's file mode is irrelevant (node leaves were already interpreter-invoked via `process.execPath`). Env overlays (`FORCE_COLOR`/`RECIPE_RUNTIME_DIR`), arg passing, cwd, JSON envelope, and exit codes are unchanged. New contract test `tests/contract/leaf-644-runs.test.sh` forces the device leaves to mode `644` and drives them through the real `mm-harness` spawn path, asserting they still run — the regression the `npm pack` tarball test could not catch.
7
+
8
+ ### Changed
9
+ - **Publish with `npm publish`, not `yarn npm publish`** — `npm publish` preserves the git `100755` mode on shipped `*.sh` leaves (defense-in-depth on top of the interpreter invocation above). Documented in `AGENTS.md`; git exec bits remain `100755`.
10
+
11
+ ## 0.3.4 - 2026-07-04
12
+
13
+ ### Fixed
14
+ - **IMP-24: published tarball stripped exec bits from shell leaves** — the harness execs `adapters/**/*.sh` leaves directly, but six shipped leaves (`adapters/mobile/lib/metro-listener.sh`, `adapters/shared/activate-repo-node.sh`, `cli-ux.sh`, `harness-path.sh`, `hash-helpers.sh`, `resolve-farmslot-ports.sh`) were committed without the git exec bit (mode `100644`), so npm packed them `644`. A fresh `npm i -g` then hit `EACCES` on the first leaf. All shipped `*.sh` are now stored `100755` in git, so the published tarball packs them executable. Added contract test `tests/contract/packaging-exec-bits.test.sh` — it runs `npm pack` to produce the actual `.tgz`, then asserts via `tar tzvf` that every shipped `*.sh` entry has owner-execute set; fails with the offending path(s) if any is `644`, so this cannot silently regress.
15
+ - **IMP-24: spawn failures are surfaced, not swallowed** — `spawnScript` returned on `result.error` (a leaf that is missing/`ENOENT` or not executable/`EACCES`) before its human-mode stderr forward, so a leaf that could not start produced a silent exit-1 (surfaced upstream as `MOBILE_PREPARE_FAILED`). The `result.error` branch now always writes a teaching diagnostic (`leaf could not start: <leaf> (<errno>)` + a `Next: reinstall mm-harness` hint) to stderr before returning. Covered by `tests/contract/spawn-error-surfaced.test.sh`.
16
+
3
17
  ## 0.3.3 - 2026-07-04
4
18
 
5
19
  ### Fixed
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -56,4 +56,4 @@
56
56
  "url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
57
57
  },
58
58
  "homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
59
- }
59
+ }
File without changes
File without changes
@@ -11,6 +11,7 @@ import { spawnSync } from 'node:child_process';
11
11
  import path from 'node:path';
12
12
 
13
13
  import { detectAdapter } from '../harness.ts';
14
+ import { resolveLeafInvoke, shellLeafMissing } from '../leaf-invoke.ts';
14
15
  import { runnerDir } from '../paths.ts';
15
16
  import type { MetaMaskRecipeAdapter } from '../types.ts';
16
17
 
@@ -107,20 +108,43 @@ export function spawnScript(
107
108
  // For node invocations (script === process.execPath) the seam stem is derived
108
109
  // from args[0] so each spawned script has its own override key.
109
110
  const isNodeScript = script === process.execPath && args.length > 0;
111
+ // Seam stem: for node invocations derive from args[0] so each script has its
112
+ // own override key; for shell and other leaves derive from the script basename.
110
113
  const stem = isNodeScript
111
114
  ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
112
115
  : path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
113
116
  const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
114
117
  const bin = override ?? script;
115
- const spawnArgs = override !== undefined && isNodeScript ? args.slice(1) : args;
116
- const result = spawnSync(bin, spawnArgs, {
118
+ const directArgs = override !== undefined && isNodeScript ? args.slice(1) : args;
119
+
120
+ // Shell leaves (.sh) run through bash so file mode need not be executable.
121
+ // Bash exits 127 (not a Node spawn error) for a missing file, so pre-check.
122
+ if (shellLeafMissing(bin)) {
123
+ const message =
124
+ `leaf could not start: ${path.basename(bin)} (ENOENT)\n` +
125
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
126
+ process.stderr.write(`${message}\n`);
127
+ return { status: 1, output: message };
128
+ }
129
+
130
+ const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
131
+ const result = spawnSync(invokeBin, spawnArgs, {
117
132
  cwd,
118
133
  encoding: 'utf8',
119
134
  env: env ? { ...process.env, ...env } : process.env,
120
135
  maxBuffer: 64 * 1024 * 1024,
121
136
  });
122
137
  if (result.error) {
123
- return { status: 1, output: `script could not start (${path.basename(script)}): ${result.error.message}` };
138
+ // Spawn failure for non-shell leaves (ENOENT/EACCES) or when bash itself
139
+ // cannot start. Always emit so a leaf that cannot start is never silent.
140
+ // For node invocations the leaf name comes from args[0], not process.execPath.
141
+ const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
142
+ const code = (result.error as NodeJS.ErrnoException).code ?? 'ESPAWN';
143
+ const message =
144
+ `leaf could not start: ${leaf} (${code})\n` +
145
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
146
+ process.stderr.write(`${message}\n`);
147
+ return { status: 1, output: message };
124
148
  }
125
149
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
126
150
  if (!json && output) process.stderr.write(output);
package/src/harness.ts CHANGED
@@ -2,6 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
+ import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from './leaf-invoke.ts';
5
6
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from './paths.ts';
6
7
  import type { MetaMaskRecipeAdapter } from './types.ts';
7
8
  import { prepareMobile } from './adapters/mobile/prepare.ts';
@@ -334,7 +335,8 @@ function resolveHarnessDispatch(
334
335
  if (adapter === 'extension') {
335
336
  return { command: process.execPath, prefixArgs: [resolveEntry(runnerDir, [entry, fallback], 'file')] };
336
337
  }
337
- return { command: resolveEntry(runnerDir, [entry, fallback], 'exec'), prefixArgs: [] };
338
+ // Shell leaves are invoked through bash; file existence is the only requirement.
339
+ return { command: resolveEntry(runnerDir, [entry, fallback], 'file'), prefixArgs: [] };
338
340
  }
339
341
 
340
342
  if (action === 'cleanup') {
@@ -343,7 +345,7 @@ function resolveHarnessDispatch(
343
345
  if (adapter === 'extension') {
344
346
  return { command: process.execPath, prefixArgs: [resolveEntry(base, [entry, fallback], 'file')] };
345
347
  }
346
- return { command: resolveEntry(base, [entry, fallback], 'exec'), prefixArgs: [] };
348
+ return { command: resolveEntry(base, [entry, fallback], 'file'), prefixArgs: [] };
347
349
  }
348
350
 
349
351
  if (adapter === 'core' && action === 'verify') {
@@ -360,16 +362,16 @@ function resolveHarnessDispatch(
360
362
  return { command: delegate, prefixArgs: ['doctor', '--adapter', 'core'] };
361
363
  }
362
364
 
363
- // mobile/extension live|verify: prefer the installed helper, else the runner's
364
- // own adapters/scripts helper (first executable candidate wins).
365
+ // mobile/extension live|verify: prefer the installed helper (any mode), else
366
+ // the runner's own adapters/scripts candidate. Bash invocation makes +x irrelevant.
365
367
  const installedScript = path.join(recipeHarnessPath(target, adapter), 'scripts', `${action}.sh`);
366
- if (isExecutable(installedScript)) {
368
+ if (fs.existsSync(installedScript)) {
367
369
  return { command: installedScript, prefixArgs: [] };
368
370
  }
369
371
  const command = resolveEntry(
370
372
  runnerDir,
371
373
  [`adapters/${adapter}/${action}.sh`, `scripts/${adapter}/${action}.sh`],
372
- 'exec',
374
+ 'file',
373
375
  );
374
376
  return { command, prefixArgs: [] };
375
377
  }
@@ -414,15 +416,32 @@ async function handleMobileLive(
414
416
 
415
417
  // Verify leaf: --no-auto-start because launch already started the app.
416
418
  const installedVerify = path.join(recipeHarnessPath(target, 'mobile'), 'scripts', 'verify.sh');
417
- const verifySh = isExecutable(installedVerify)
419
+ const verifySh = fs.existsSync(installedVerify)
418
420
  ? installedVerify
419
- : resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'exec');
421
+ : resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], 'file');
420
422
 
421
423
  const verifyArgs = hasArg(forwardArgs, '--no-auto-start')
422
424
  ? [...forwardArgs]
423
425
  : ['--no-auto-start', ...forwardArgs];
424
426
 
425
- const result = spawnSync(verifySh, verifyArgs, {
427
+ if (shellLeafMissing(verifySh)) {
428
+ const message = missingShellLeafMessage(verifySh);
429
+ if (json) {
430
+ console.log(
431
+ harnessSummary('live', 'mobile', target, 'fail', 1, autoDetected, {
432
+ code: 'HARNESS_SPAWN_FAILED',
433
+ message,
434
+ userAction: failureHint('mobile', 'live'),
435
+ }),
436
+ );
437
+ } else {
438
+ console.error(`✗ ${message}`);
439
+ }
440
+ return 1;
441
+ }
442
+
443
+ const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
444
+ const result = spawnSync(verifyBin, verifySpawnArgs, {
426
445
  stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
427
446
  env: process.env,
428
447
  });
@@ -544,7 +563,29 @@ export async function handleHarness(argv: string[]): Promise<number> {
544
563
  }
545
564
 
546
565
  const start = Date.now();
547
- const result = spawnSync(dispatch.command, [...dispatch.prefixArgs, ...forwardArgs], {
566
+
567
+ // Shell leaves need bash invocation; bash exits 127 (not ENOENT) for missing files.
568
+ if (shellLeafMissing(dispatch.command)) {
569
+ const message = missingShellLeafMessage(dispatch.command);
570
+ if (json) {
571
+ console.log(
572
+ harnessSummary(harnessAction, adapter, target, 'fail', 1, autoDetected, {
573
+ code: 'HARNESS_SPAWN_FAILED',
574
+ message,
575
+ userAction: failureHint(adapter, harnessAction),
576
+ }),
577
+ );
578
+ } else {
579
+ console.error(`✗ ${message}`);
580
+ }
581
+ return 1;
582
+ }
583
+
584
+ const { bin: dispatchBin, args: dispatchArgs } = resolveLeafInvoke(dispatch.command, [
585
+ ...dispatch.prefixArgs,
586
+ ...forwardArgs,
587
+ ]);
588
+ const result = spawnSync(dispatchBin, dispatchArgs, {
548
589
  // Default: stream child output verbatim (byte-identical to the skill path).
549
590
  // --json: route child output to our stderr so stdout carries only the summary.
550
591
  stdio: json ? ['inherit', 2, 'inherit'] : 'inherit',
@@ -0,0 +1,28 @@
1
+ // Interpreter routing for adapter shell and node leaves.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ // Returns the interpreter and args for invoking a leaf. Shell leaves (.sh) run
6
+ // through bash so the file mode does not need to be executable. Node leaves
7
+ // are already invoked through process.execPath by their callers and are
8
+ // returned unchanged.
9
+ export function resolveLeafInvoke(command: string, args: string[]): { bin: string; args: string[] } {
10
+ if (command.endsWith('.sh')) return { bin: 'bash', args: [command, ...args] };
11
+ return { bin: command, args };
12
+ }
13
+
14
+ // Teaching diagnostic for a shell leaf that is absent from disk. Bash exits
15
+ // 127 (not a Node spawn error) for a missing file, so callers pre-check and
16
+ // emit this rather than relying on result.error.
17
+ export function missingShellLeafMessage(leafPath: string): string {
18
+ const leaf = path.basename(leafPath);
19
+ return (
20
+ `leaf could not start: ${leaf} (ENOENT)\n` +
21
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`
22
+ );
23
+ }
24
+
25
+ // Returns true when a .sh leaf path does not exist on disk.
26
+ export function shellLeafMissing(leafPath: string): boolean {
27
+ return leafPath.endsWith('.sh') && !fs.existsSync(leafPath);
28
+ }