@deeeed/metamask-harness 0.3.4 → 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.
- package/CHANGELOG.md +8 -0
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/verify.sh +0 -0
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-metro.sh +0 -0
- package/adapters/mobile/verify.sh +0 -0
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- package/src/commands/shared.ts +20 -6
- package/src/harness.ts +51 -10
- package/src/leaf-invoke.ts +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
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
|
+
|
|
3
11
|
## 0.3.4 - 2026-07-04
|
|
4
12
|
|
|
5
13
|
### Fixed
|
package/adapters/core/cleanup.sh
CHANGED
|
File without changes
|
package/adapters/core/inject.sh
CHANGED
|
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
|
+
"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
|
+
}
|
package/scripts/completions.sh
CHANGED
|
File without changes
|
|
File without changes
|
package/src/commands/shared.ts
CHANGED
|
@@ -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,23 +108,36 @@ 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
|
|
116
|
-
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// never a mystery exit-1. For node invocations the leaf is args[0], not node.
|
|
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.
|
|
127
141
|
const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
|
|
128
142
|
const code = (result.error as NodeJS.ErrnoException).code ?? 'ESPAWN';
|
|
129
143
|
const message =
|
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
|
-
|
|
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], '
|
|
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
|
|
364
|
-
// own adapters/scripts
|
|
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 (
|
|
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
|
-
'
|
|
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 =
|
|
419
|
+
const verifySh = fs.existsSync(installedVerify)
|
|
418
420
|
? installedVerify
|
|
419
|
-
: resolveEntry(runnerDir, ['adapters/mobile/verify.sh', 'scripts/mobile/verify.sh'], '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|