@deeeed/metamask-harness 0.5.0 → 0.6.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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
package/src/paths.ts DELETED
@@ -1,198 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
-
5
- import type {
6
- RecipeHarnessBrowserExtensionModule,
7
- RecipeHarnessCdpModule,
8
- RecipeHarnessModule,
9
- RecipeHarnessReactNativeBridgeModule,
10
- RecipeProtocolModule,
11
- MetaMaskRecipeAdapter,
12
- } from './types.ts';
13
-
14
- export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
- const pathDefaults = readPathDefaults();
16
- export const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
17
- export const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
18
-
19
- export function recipeRuntimeDir() {
20
- return validateRelativeRecipePath('RECIPE_RUNTIME_DIR', process.env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
21
- }
22
-
23
- export function recipeHarnessRoot() {
24
- return validateRelativeRecipePath('RECIPE_HARNESS_ROOT', process.env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
25
- }
26
-
27
-
28
- function readPathDefaults(): { recipeRuntimeDir: string; recipeHarnessRoot: string } {
29
- const defaultsPath = path.join(runnerDir, 'adapters/shared/path-defaults.json');
30
- const parsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')) as Partial<{ recipeRuntimeDir: string; recipeHarnessRoot: string }>;
31
- return {
32
- recipeRuntimeDir: validateRelativeRecipePath('recipeRuntimeDir', parsed.recipeRuntimeDir || ''),
33
- recipeHarnessRoot: validateRelativeRecipePath('recipeHarnessRoot', parsed.recipeHarnessRoot || ''),
34
- };
35
- }
36
-
37
- function validateRelativeRecipePath(name: string, value: string): string {
38
- if (!value || path.isAbsolute(value)) throw new Error(`${name} must be a non-empty relative path: ${value}`);
39
- if (!/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${name} contains unsupported characters: ${value}`);
40
- for (const part of value.split('/')) {
41
- if (!part || part === '.' || part === '..') throw new Error(`${name} contains unsafe path component: ${value}`);
42
- }
43
- return value;
44
- }
45
-
46
- export function recipeRuntimePath(projectRoot: string, ...segments: string[]) {
47
- return path.join(projectRoot, recipeRuntimeDir(), ...segments);
48
- }
49
-
50
- export function recipeHarnessPath(projectRoot: string, ...segments: string[]) {
51
- return path.join(projectRoot, recipeHarnessRoot(), ...segments);
52
- }
53
-
54
- export function walletFixturePath(projectRoot: string) {
55
- return recipeRuntimePath(projectRoot, 'wallet-fixture.json');
56
- }
57
-
58
- export function extensionIdPath(projectRoot: string) {
59
- return recipeRuntimePath(projectRoot, 'extension.id');
60
- }
61
-
62
- export function recipeWatchLogCandidates() {
63
- return [
64
- path.join(recipeRuntimeDir(), 'webpack.log'),
65
- path.join(recipeRuntimeDir(), 'recipe-harness-webpack.log'),
66
- ];
67
- }
68
-
69
- export function resolveLocalProtocolRoot() {
70
- const candidates = [
71
- process.env.FARMSLOT_ROOT,
72
- readConfiguredProtocolRoot(),
73
- findProtocolRoot(runnerDir),
74
- findProtocolRoot(process.cwd()),
75
- ].filter(Boolean);
76
- const root = candidates[0];
77
- return root ? path.resolve(root) : undefined;
78
- }
79
-
80
- export function resolveRequiredLocalProtocolRoot(reason: string) {
81
- const root = resolveLocalProtocolRoot();
82
- if (!root) {
83
- throw new Error(
84
- `${reason} requires a local protocol/runtime checkout. Set FARMSLOT_ROOT or create .farmslot-root for this dev-only path.`,
85
- );
86
- }
87
- return root;
88
- }
89
-
90
- function readConfiguredProtocolRoot() {
91
- const configPath = path.join(runnerDir, '.farmslot-root');
92
- if (!fs.existsSync(configPath)) return undefined;
93
- const value = fs.readFileSync(configPath, 'utf8').trim();
94
- return value || undefined;
95
- }
96
-
97
- function findProtocolRoot(start) {
98
- let dir = path.resolve(start);
99
- while (dir !== path.dirname(dir)) {
100
- if (isProtocolRoot(dir)) return dir;
101
- const sibling = path.join(dir, 'farmslot');
102
- if (isProtocolRoot(sibling)) return sibling;
103
- dir = path.dirname(dir);
104
- }
105
- return undefined;
106
- }
107
-
108
- function isProtocolRoot(candidate) {
109
- return (
110
- fs.existsSync(path.join(candidate, 'packages/recipe-harness/package.json')) &&
111
- fs.existsSync(path.join(candidate, 'packages/protocol/package.json'))
112
- );
113
- }
114
-
115
- export function assertAdapter(adapter: unknown): asserts adapter is MetaMaskRecipeAdapter {
116
- if (adapter !== 'mobile' && adapter !== 'extension' && adapter !== 'core') {
117
- throw new Error('Adapter must be mobile, extension, or core.');
118
- }
119
- }
120
-
121
- export function manifestPath(adapter: MetaMaskRecipeAdapter) {
122
- assertAdapter(adapter);
123
- return path.join(runnerDir, 'library/manifests', `${adapter}.action-manifest.json`);
124
- }
125
-
126
- export function recipePath(name: string) {
127
- return path.join(runnerDir, 'library/recipes', name);
128
- }
129
-
130
- export function readJson(file: string): unknown {
131
- return JSON.parse(fs.readFileSync(file, 'utf8'));
132
- }
133
-
134
- export async function importRecipeHarness(): Promise<RecipeHarnessModule> {
135
- return importProtocolPackage(
136
- '@farmslot/recipe-harness',
137
- 'packages/recipe-harness/src/index.ts',
138
- ) as Promise<RecipeHarnessModule>;
139
- }
140
-
141
- export async function importRecipeHarnessRuntimeCdp(): Promise<RecipeHarnessCdpModule> {
142
- return importProtocolPackage(
143
- '@farmslot/recipe-harness/runtime/cdp',
144
- 'packages/recipe-harness/src/runtime/cdp.ts',
145
- ) as Promise<RecipeHarnessCdpModule>;
146
- }
147
-
148
- export async function importRecipeHarnessRuntimeBrowserExtension(): Promise<RecipeHarnessBrowserExtensionModule> {
149
- return importProtocolPackage(
150
- '@farmslot/recipe-harness/runtime/browser-extension',
151
- 'packages/recipe-harness/src/runtime/browser-extension.ts',
152
- ) as Promise<RecipeHarnessBrowserExtensionModule>;
153
- }
154
-
155
- export async function importRecipeHarnessRuntimeReactNativeBridge(): Promise<RecipeHarnessReactNativeBridgeModule> {
156
- return importProtocolPackage(
157
- '@farmslot/recipe-harness/runtime/react-native-bridge',
158
- 'packages/recipe-harness/src/runtime/react-native-bridge.ts',
159
- ) as Promise<RecipeHarnessReactNativeBridgeModule>;
160
- }
161
-
162
- export async function importRecipeHarnessCli(): Promise<{
163
- runRecipeHarnessCli: (argv: string[]) => Promise<void>;
164
- }> {
165
- return importProtocolPackage(
166
- '@farmslot/recipe-harness/cli',
167
- 'packages/recipe-harness/src/cli/index.ts',
168
- ) as Promise<{ runRecipeHarnessCli: (argv: string[]) => Promise<void> }>;
169
- }
170
-
171
- export async function importRecipeProtocol(): Promise<RecipeProtocolModule> {
172
- return importProtocolPackage(
173
- '@farmslot/protocol',
174
- 'packages/protocol/src/index.ts',
175
- ) as Promise<RecipeProtocolModule>;
176
- }
177
-
178
- async function importProtocolPackage(packageName: string, localSourceEntry: string) {
179
- try {
180
- return await import(packageName);
181
- } catch (error) {
182
- if (!isMissingPackageError(error, packageName)) throw error;
183
- }
184
-
185
- const root = resolveLocalProtocolRoot();
186
- if (!root) {
187
- throw new Error(
188
- `${packageName} is not installed. Install @farmslot/* packages normally, or set FARMSLOT_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`,
189
- );
190
- }
191
- return import(pathToFileURL(path.join(root, localSourceEntry)).href);
192
- }
193
-
194
- function isMissingPackageError(error: unknown, packageName: string) {
195
- if (!(error instanceof Error)) return false;
196
- const code = (error as NodeJS.ErrnoException).code;
197
- return code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(packageName);
198
- }
package/src/progress.ts DELETED
@@ -1,117 +0,0 @@
1
- /**
2
- * progress.ts — structured progress events for long recipe CLI operations.
3
- *
4
- * Human mode: short lines on stderr.
5
- * Machine mode: JSONL on stderr when --json or RECIPE_PROGRESS_JSON=1.
6
- */
7
- import path from 'node:path';
8
- import { parseArgs } from 'node:util';
9
- import { color } from './cli-color.ts';
10
-
11
- function shouldEmitJson(explicit: boolean): boolean {
12
- if (explicit) return true;
13
- const env = process.env.RECIPE_PROGRESS_JSON;
14
- return env === '1' || env === 'true';
15
- }
16
-
17
- interface ProgressEvent {
18
- command?: string;
19
- phase?: string;
20
- message?: string;
21
- status?: string;
22
- elapsedMs?: number;
23
- [key: string]: unknown;
24
- }
25
-
26
- export function emitProgress(event: ProgressEvent, { json = false }: { json?: boolean } = {}): void {
27
- const payload = {
28
- schemaVersion: 1,
29
- type: 'progress',
30
- ts: new Date().toISOString(),
31
- ...event,
32
- };
33
- if (json || shouldEmitJson(false)) {
34
- process.stderr.write(`${JSON.stringify(payload)}\n`);
35
- return;
36
- }
37
- const stream = process.stderr;
38
- const prefix = color('label', 'recipe', { stream });
39
- if (payload.phase === 'done') {
40
- const statusStyle = payload.status === 'pass' ? 'ok' : 'err';
41
- const statusWord = payload.status === 'pass' ? 'done' : 'failed';
42
- const cmd = payload.command ? color('cmd', `${payload.command}`, { stream }) : '';
43
- const elapsed = Number.isFinite(payload.elapsedMs)
44
- ? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
45
- : '';
46
- process.stderr.write(`${prefix}: ${cmd} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
47
- return;
48
- }
49
- const cmd = payload.command ? `${color('cmd', payload.command, { stream })}: ` : '';
50
- const phase = payload.phase ? `${color('accent', `[${payload.phase}]`, { stream })} ` : '';
51
- const elapsed = Number.isFinite(payload.elapsedMs)
52
- ? color('comment', ` (${Math.round((payload.elapsedMs as number) / 1000)}s)`, { stream })
53
- : '';
54
- process.stderr.write(`${prefix}: ${cmd}${phase}${payload.message || ''}${elapsed}\n`);
55
- }
56
-
57
- function usage(): void {
58
- console.error(`Usage:
59
- progress emit --command <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
60
- progress done --command <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
61
- }
62
-
63
- async function main(): Promise<void> {
64
- const [command, ...rest] = process.argv.slice(2);
65
- if (!command || command === '--help' || command === '-h') {
66
- usage();
67
- process.exit(command ? 0 : 2);
68
- }
69
-
70
- const { values } = parseArgs({
71
- args: rest,
72
- options: {
73
- command: { type: 'string' },
74
- phase: { type: 'string' },
75
- message: { type: 'string' },
76
- status: { type: 'string' },
77
- 'elapsed-ms': { type: 'string' },
78
- json: { type: 'boolean', default: false },
79
- },
80
- allowPositionals: false,
81
- });
82
-
83
- const elapsedMs = values['elapsed-ms'] ? Number.parseInt(values['elapsed-ms'], 10) : undefined;
84
- const json = values.json || shouldEmitJson(false);
85
-
86
- if (command === 'emit') {
87
- emitProgress({
88
- command: values.command || 'recipe',
89
- phase: values.phase || 'working',
90
- message: values.message || '',
91
- elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
92
- }, { json });
93
- return;
94
- }
95
-
96
- if (command === 'done') {
97
- emitProgress({
98
- command: values.command || 'recipe',
99
- phase: 'done',
100
- status: values.status || 'pass',
101
- message: values.message || '',
102
- elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
103
- }, { json });
104
- return;
105
- }
106
-
107
- usage();
108
- process.exit(2);
109
- }
110
-
111
- const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename ?? '');
112
- if (isMain) {
113
- main().catch((error: unknown) => {
114
- console.error(error instanceof Error ? error.message : String(error));
115
- process.exit(1);
116
- });
117
- }
@@ -1,147 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
-
5
- import type { RecordingTarget, RecordingTargetContext, RecordingTargetProvider } from '@farmslot/recipe-harness';
6
-
7
- import type { MetaMaskRecipeAdapter } from './types.ts';
8
-
9
- export function createMetaMaskRecordingTargetProvider(
10
- adapter: MetaMaskRecipeAdapter,
11
- ): RecordingTargetProvider {
12
- return {
13
- async resolveRecordingTarget(context: RecordingTargetContext): Promise<RecordingTarget> {
14
- if (adapter === 'extension') {
15
- const pid = resolveExtensionBrowserPid(
16
- context.projectRoot,
17
- context.artifactsDir,
18
- context.env.CDP_PORT ?? context.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT,
19
- );
20
- if (!pid) {
21
- throw new Error(
22
- 'Could not resolve the extension browser PID for --record-video. Ensure the slot browser is running and CDP_PORT is set.',
23
- );
24
- }
25
- return { kind: 'pid', pid };
26
- }
27
- if (adapter === 'mobile') {
28
- const platform = (context.env.PLATFORM ?? process.env.PLATFORM ?? 'ios').trim().toLowerCase();
29
- if (platform === 'android' || context.env.ADB_SERIAL || process.env.ADB_SERIAL) {
30
- throw new Error('--record-video is not implemented for mobile Android replay yet.');
31
- }
32
- const simulator = resolveMobileIosSimulatorName(context.env);
33
- if (!simulator) {
34
- throw new Error(
35
- 'Could not resolve an iOS simulator name for --record-video. Set IOS_SIMULATOR or pass --simulator to the recipe hook.',
36
- );
37
- }
38
- const pid = resolveMobileIosSimulatorPid(simulator);
39
- if (pid) return { kind: 'pid', pid };
40
- return { kind: 'app-window', appName: 'Simulator', windowName: simulator };
41
- }
42
- throw new Error(`--record-video is not implemented for the ${adapter} adapter.`);
43
- },
44
- };
45
- }
46
-
47
- export function captureHelperSupportsRecordSessionSnapshots(projectRoot: string): boolean {
48
- const result = spawnSync(captureHelperPath(), ['version', '--json'], {
49
- cwd: projectRoot,
50
- encoding: 'utf8',
51
- });
52
- if (result.status !== 0) return false;
53
- try {
54
- const parsed = JSON.parse(result.stdout) as { capabilities?: unknown };
55
- return Array.isArray(parsed.capabilities) && parsed.capabilities.includes('record_session_snapshot');
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
- export function resolveMobileIosSimulatorName(
62
- env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
63
- ): string | undefined {
64
- const explicit = env.IOS_SIMULATOR?.trim() || env.SIM_UDID?.trim();
65
- if (explicit) return explicit;
66
- const slot = env.FARMSLOT_SLOT_ID?.trim() || env.SLOT_ID?.trim();
67
- if (!slot) return undefined;
68
- const suffix = slot.includes('-') ? slot.slice(slot.lastIndexOf('-') + 1) : slot;
69
- return suffix || undefined;
70
- }
71
-
72
- export function resolveMobileIosSimulatorPid(simulator: string): number | null {
73
- const result = spawnSync(
74
- captureHelperPath(),
75
- ['resolve', '--app-name', 'Simulator', '--window-name', simulator, '--json'],
76
- { encoding: 'utf8' },
77
- );
78
- if (result.status !== 0) return null;
79
- try {
80
- const parsed = JSON.parse(result.stdout) as { selected?: { pid?: unknown } };
81
- return parsePositivePid(parsed.selected?.pid);
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- export function resolveExtensionBrowserPid(
88
- projectRoot: string,
89
- artifactsDir: string,
90
- cdpPort: string | undefined,
91
- ): number | null {
92
- const explicit = parsePositivePid(process.env.METAMASK_RECIPE_EXTENSION_BROWSER_PID);
93
- if (explicit) return explicit;
94
- if (cdpPort) {
95
- const lsof = spawnSync('lsof', ['-nP', `-iTCP:${cdpPort}`, '-sTCP:LISTEN', '-t'], {
96
- cwd: projectRoot,
97
- encoding: 'utf8',
98
- });
99
- if (lsof.status === 0) {
100
- const pid = parsePositivePid(String(lsof.stdout).split(/\s+/u).find(Boolean));
101
- if (pid) return pid;
102
- }
103
- }
104
- for (const file of [
105
- path.join(artifactsDir, 'extension-runtime/runtime.json'),
106
- path.join(projectRoot, 'temp/recipe/runtime/runtime.json'),
107
- ]) {
108
- const pid = parsePositivePid(readJsonFile(file)?.pid);
109
- if (pid) return pid;
110
- }
111
- for (const file of [
112
- path.join(projectRoot, 'temp/recipe/runtime/chromium.pid'),
113
- path.join(projectRoot, 'temp/recipe/runtime/browser.pid'),
114
- ]) {
115
- const pid = parsePositivePid(readTextFile(file));
116
- if (pid) return pid;
117
- }
118
- return null;
119
- }
120
-
121
- function captureHelperPath(): string {
122
- return process.env.CAPTURE_HELPER_PATH || 'capture-helper';
123
- }
124
-
125
- function parsePositivePid(value: unknown): number | null {
126
- const pid = Number(String(value ?? '').trim());
127
- return Number.isInteger(pid) && pid > 0 ? pid : null;
128
- }
129
-
130
- function readJsonFile(file: string): Record<string, unknown> | null {
131
- if (!fs.existsSync(file)) return null;
132
- try {
133
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
134
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
135
- } catch {
136
- return null;
137
- }
138
- }
139
-
140
- function readTextFile(file: string): string {
141
- if (!fs.existsSync(file)) return '';
142
- try {
143
- return fs.readFileSync(file, 'utf8');
144
- } catch {
145
- return '';
146
- }
147
- }