@deeeed/metamask-harness 0.2.0 → 0.3.1
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 +33 -0
- package/README.md +47 -17
- package/{orchestration → adapters}/core/cleanup.sh +2 -2
- package/{orchestration → adapters}/core/inject.sh +8 -8
- package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
- package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
- package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
- package/{orchestration → adapters}/extension/inject.mjs +27 -31
- package/{orchestration → adapters}/extension/launch.sh +4 -4
- package/{orchestration → adapters}/extension/live.sh +5 -5
- package/{orchestration → adapters}/extension/readiness.mjs +12 -2
- package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
- package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
- package/{orchestration → adapters}/extension/start-watch.sh +7 -7
- package/{runner → adapters}/extension/verify.sh +11 -11
- package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
- package/{orchestration → adapters}/manifest.json +96 -96
- package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
- package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
- package/{orchestration → adapters}/mobile/inject.sh +22 -24
- package/adapters/mobile/open-device.sh +260 -0
- package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
- package/adapters/mobile/prewarm-bundle.sh +95 -0
- package/adapters/mobile/start-metro.sh +228 -0
- package/{runner → adapters}/mobile/verify.sh +6 -6
- package/adapters/mobile/wait-for-bridge.sh +108 -0
- package/adapters/mobile/yarn-setup.sh +20 -0
- package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
- package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
- package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
- package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
- package/adapters/shared/sync-wallet-fixture.sh +78 -0
- package/bin/mm-harness +37 -6
- package/docs/CLI-SPEC.md +41 -2
- package/docs/architecture.md +69 -38
- package/docs/runtime-file-conventions.md +3 -3
- package/library/actions/core/perps/_controller.mjs +1 -1
- package/library/actions/extension/platform/cdp.mjs +2 -2
- package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/platform/bridge.mjs +1 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
- package/library/actions/mobile/wallet/setup.mjs +2 -2
- package/library/library.json +1 -1
- package/package.json +8 -8
- package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
- package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
- package/src/adapters/mobile/prepare.ts +210 -0
- package/{orchestration → src/adapters}/mobile/runtime-decision.ts +105 -5
- package/{runner/src → src}/adapters.ts +3 -3
- package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
- package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
- package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
- package/{runner/src → src}/cli.ts +48 -40
- package/src/commands/debug.ts +63 -0
- package/src/commands/fixtures.ts +193 -0
- package/{runner/src → src}/commands/launch.ts +200 -54
- package/src/commands/logs.ts +83 -0
- package/{runner/src → src}/commands/shared.ts +40 -30
- package/src/commands/update.ts +316 -0
- package/{runner/src → src}/harness.ts +119 -16
- package/{runner/src → src}/heal-bounds.ts +20 -1
- package/src/index.ts +15 -0
- package/{runner/src → src}/live-adapter-contract.ts +1 -1
- package/{runner/src → src}/mm-harness-cli.ts +64 -3
- package/{runner/src → src}/paths.ts +2 -2
- package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
- package/docs/extension-runtime-commands.md +0 -60
- package/docs/harness-cli.md +0 -43
- package/orchestration/compat-overlays/mobile/README.md +0 -13
- package/orchestration/doctor.mjs +0 -72
- package/orchestration/lib/cli-home.mjs +0 -354
- package/orchestration/lib/recipe-paths.mjs +0 -26
- package/orchestration/mobile/launch.sh +0 -137
- package/orchestration/mobile/live.sh +0 -125
- package/orchestration/porcelain/metamask-recipe +0 -99
- package/orchestration/porcelain/mm-recipe +0 -1591
- package/orchestration/porcelain/mme-recipe +0 -1181
- package/runner/src/commands/debug.ts +0 -44
- package/runner/src/commands/fixtures.ts +0 -99
- package/runner/src/commands/logs.ts +0 -60
- package/runner/src/index.ts +0 -6
- /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
- /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
- /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
- /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
- /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
- /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
- /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
- /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
- /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
- /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
- /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
- /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
- /package/{runner/src → src}/completions-cache.ts +0 -0
- /package/{runner/src → src}/doctor.ts +0 -0
- /package/{runner/src → src}/manifest.ts +0 -0
- /package/{runner/src → src}/recording-target.ts +0 -0
- /package/{runner/src → src}/run-recording.ts +0 -0
- /package/{runner/src → src}/runner.ts +0 -0
- /package/{runner/src → src}/types.ts +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// fixtures <sync|set> — manage wallet DATA (never overlay/runtime state). `sync`
|
|
2
|
+
// re-renders the wallet fixture files; `set` applies THE canonical fixture (SRP/
|
|
3
|
+
// password read FROM the fixture, never typed) and always teaches the edit path.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { runnerDir, walletFixturePath } from '../paths.ts';
|
|
9
|
+
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
10
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
11
|
+
|
|
12
|
+
// One node injected from cli.ts so `fixtures set` can reuse the real engine path
|
|
13
|
+
// (`call`'s one-node machinery) without a circular import.
|
|
14
|
+
export interface CommandDeps {
|
|
15
|
+
runOneNode: (
|
|
16
|
+
adapter: MetaMaskRecipeAdapter,
|
|
17
|
+
action: string,
|
|
18
|
+
args: Record<string, string>,
|
|
19
|
+
target: string,
|
|
20
|
+
actionManifest: string | undefined,
|
|
21
|
+
) => Promise<{ status: 'pass' | 'fail' }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FIXTURES_BOOLEANS = new Set(['json']);
|
|
25
|
+
|
|
26
|
+
// Patterns indicating setup-wallet failed due to a Metro/bridge issue (not a bad
|
|
27
|
+
// fixture). A clean Metro restart + relaunch is likely to resolve these.
|
|
28
|
+
// Mirrors baseline mm-recipe:820-824 setup_wallet_failure_recoverable patterns.
|
|
29
|
+
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
30
|
+
'CDP not reachable',
|
|
31
|
+
'CDP bridge',
|
|
32
|
+
'is not installed',
|
|
33
|
+
'Engine.context.KeyringController not available',
|
|
34
|
+
'Engine does not exist',
|
|
35
|
+
"Cannot read property 'transactions'",
|
|
36
|
+
'bridge-not-ready',
|
|
37
|
+
'debug-target-missing',
|
|
38
|
+
'CDP eval failed',
|
|
39
|
+
'CDP eval-async failed',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function isRecoverableSetupWalletFailure(output: string): boolean {
|
|
43
|
+
return RECOVERABLE_SETUP_WALLET_PATTERNS.some((p) => output.includes(p));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function handleFixtures(argv: string[], deps: CommandDeps): Promise<number> {
|
|
47
|
+
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
48
|
+
const json = flag(options, 'json');
|
|
49
|
+
const sub = positional[0];
|
|
50
|
+
if (sub !== 'sync' && sub !== 'set') {
|
|
51
|
+
return usageOut(json, 'fixtures', 'fixtures requires a subcommand: mm-harness fixtures <sync|set>', 'mm-harness fixtures sync or mm-harness fixtures set');
|
|
52
|
+
}
|
|
53
|
+
const target = targetOf(options);
|
|
54
|
+
const adapter = resolveAdapter(options, target);
|
|
55
|
+
if (!adapter) {
|
|
56
|
+
return usageOut(json, 'fixtures', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
57
|
+
}
|
|
58
|
+
if (adapter === 'core') return usageOut(json, 'fixtures', 'core is headless; it has no wallet fixture.', 'mm-harness run <recipe> # run recipes against the headless core');
|
|
59
|
+
|
|
60
|
+
const canonicalFixture = walletFixturePath(target);
|
|
61
|
+
|
|
62
|
+
if (sub === 'sync') {
|
|
63
|
+
const exitCode = fixturesSync(adapter, target, json);
|
|
64
|
+
if (json) {
|
|
65
|
+
console.log(
|
|
66
|
+
JSON.stringify(
|
|
67
|
+
{ schemaVersion: 1, command: 'fixtures', action: 'sync', adapter, fixture: canonicalFixture, exitCode },
|
|
68
|
+
null,
|
|
69
|
+
2,
|
|
70
|
+
),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return exitCode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// set — apply THE canonical wallet fixture (password read FROM the fixture).
|
|
77
|
+
// Env-gap resolution (docs/CLI-SPEC.md Part 4): flag > env RECIPE_WALLET_FIXTURE
|
|
78
|
+
// > the slot's canonical fixture.
|
|
79
|
+
const fixturePath = path.resolve(str(options, 'fixture') ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
80
|
+
let status: 'pass' | 'fail';
|
|
81
|
+
if (adapter === 'mobile') {
|
|
82
|
+
// Compose setup-wallet.sh directly (requires bridge to be live).
|
|
83
|
+
const setupWalletSh = path.join(runnerDir, 'adapters/mobile/bridge-runtime/setup-wallet.sh');
|
|
84
|
+
const previousAppRoot = process.env.APP_ROOT;
|
|
85
|
+
process.env.APP_ROOT = target;
|
|
86
|
+
try {
|
|
87
|
+
let result = spawnScript(setupWalletSh, ['--fixture', fixturePath], target, json);
|
|
88
|
+
// One clean-Metro auto-retry on recoverable failures (CDP unreachable, bridge
|
|
89
|
+
// not installed). RECIPE_SETUP_WALLET_RETRIED guards against infinite loops.
|
|
90
|
+
if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env['RECIPE_SETUP_WALLET_RETRIED']) {
|
|
91
|
+
process.env['RECIPE_SETUP_WALLET_RETRIED'] = '1';
|
|
92
|
+
if (!json) process.stderr.write(' setup-wallet: recoverable failure — restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n');
|
|
93
|
+
const { prepareMobile } = await import('../adapters/mobile/prepare.ts');
|
|
94
|
+
const platform = str(options, 'platform') ?? process.env['MOBILE_PLATFORM'] ?? 'ios';
|
|
95
|
+
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: 'auto', clearMetro: true });
|
|
96
|
+
if (relaunchResult.status === 0) {
|
|
97
|
+
result = spawnScript(setupWalletSh, ['--fixture', fixturePath], target, json);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
status = result.status === 0 ? 'pass' : 'fail';
|
|
101
|
+
} finally {
|
|
102
|
+
delete process.env['RECIPE_SETUP_WALLET_RETRIED'];
|
|
103
|
+
if (previousAppRoot === undefined) delete process.env.APP_ROOT;
|
|
104
|
+
else process.env.APP_ROOT = previousAppRoot;
|
|
105
|
+
}
|
|
106
|
+
if (!json && status === 'fail') {
|
|
107
|
+
console.error(' Next: mm-harness launch ios # relaunch the dev client, then retry: mm-harness fixtures set');
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Extension has no standalone set arm — reuse call's one-node machinery via
|
|
111
|
+
// the real engine path (metamask.wallet.setup in the manifest).
|
|
112
|
+
if (!fs.existsSync(fixturePath)) {
|
|
113
|
+
return usageOut(
|
|
114
|
+
json,
|
|
115
|
+
'fixtures',
|
|
116
|
+
`no wallet fixture at ${fixturePath}.`,
|
|
117
|
+
'create it (or pass --fixture <path>), then re-run: mm-harness fixtures set',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
121
|
+
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
122
|
+
try {
|
|
123
|
+
const result = await deps.runOneNode('extension', 'metamask.wallet.setup', {}, target, str(options, 'actionManifest'));
|
|
124
|
+
status = result.status;
|
|
125
|
+
} finally {
|
|
126
|
+
if (previousFixtureEnv === undefined) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
127
|
+
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
128
|
+
}
|
|
129
|
+
if (!json && status === 'fail') {
|
|
130
|
+
console.error(' Next: mm-harness launch --build # rebuild and relaunch, then retry: mm-harness fixtures set');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Always teach where to customize the wallet.
|
|
135
|
+
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
|
|
136
|
+
if (json) {
|
|
137
|
+
console.log(
|
|
138
|
+
JSON.stringify(
|
|
139
|
+
{
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
command: 'fixtures',
|
|
142
|
+
action: 'set',
|
|
143
|
+
adapter,
|
|
144
|
+
fixture: fixturePath,
|
|
145
|
+
canonicalFixture,
|
|
146
|
+
status,
|
|
147
|
+
exitCode: status === 'pass' ? EXIT.ok : EXIT.runtime,
|
|
148
|
+
message: teaching,
|
|
149
|
+
error: status === 'fail' ? {
|
|
150
|
+
code: 'SETUP_WALLET_FAILED',
|
|
151
|
+
message: 'wallet fixture setup failed',
|
|
152
|
+
userAction: adapter === 'mobile'
|
|
153
|
+
? 'mm-harness launch ios # relaunch the dev client, then retry: mm-harness fixtures set'
|
|
154
|
+
: 'mm-harness launch --build # rebuild and relaunch, then retry: mm-harness fixtures set',
|
|
155
|
+
} : null,
|
|
156
|
+
},
|
|
157
|
+
null,
|
|
158
|
+
2,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
console.error(teaching);
|
|
163
|
+
}
|
|
164
|
+
return status === 'pass' ? EXIT.ok : EXIT.runtime;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sync the overlay installation + wallet fixture for the checkout.
|
|
168
|
+
// Re-runs the adapter inject to refresh patched files, then copies the wallet
|
|
169
|
+
// fixture to the canonical runtime location via sync-wallet-fixture.sh.
|
|
170
|
+
function fixturesSync(adapter: 'mobile' | 'extension', target: string, json: boolean): number {
|
|
171
|
+
const mmHarnessBin = path.join(runnerDir, 'bin/mm-harness');
|
|
172
|
+
const installResult = spawnScript(
|
|
173
|
+
mmHarnessBin,
|
|
174
|
+
['install', '--adapter', adapter, '--target', target],
|
|
175
|
+
target,
|
|
176
|
+
json,
|
|
177
|
+
);
|
|
178
|
+
if (installResult.status !== 0) {
|
|
179
|
+
if (!json) {
|
|
180
|
+
console.error(`✗ mm-harness fixtures sync: overlay reinstall failed.\n Next: mm-harness install --target ${target} # diagnose the install failure`);
|
|
181
|
+
}
|
|
182
|
+
return EXIT.runtime;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const syncFixtureSh = path.join(runnerDir, 'adapters/shared/sync-wallet-fixture.sh');
|
|
186
|
+
const syncArgs = ['--target', target];
|
|
187
|
+
if (process.env.CDP_PORT) syncArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
188
|
+
if (process.env.RECIPE_SLOT_ID) syncArgs.push('--slot-id', process.env.RECIPE_SLOT_ID);
|
|
189
|
+
spawnScript(syncFixtureSh, syncArgs, target, json);
|
|
190
|
+
// Non-zero from sync-wallet-fixture.sh is informational (no fixture source);
|
|
191
|
+
// the install succeeded so the overlay is fresh.
|
|
192
|
+
return EXIT.ok;
|
|
193
|
+
}
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
// launch — start the app (Metro/build + surface), auto-ensure the runtime overlay,
|
|
2
2
|
// and heal transport within bounds. Owns launch grammar, build-tier selection,
|
|
3
|
-
// quick-launch change detection, and the --json contract. Device orchestration
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// quick-launch change detection, and the --json contract. Device orchestration
|
|
4
|
+
// composes adapters/ scripts directly; --verify reuses the overlay `live`
|
|
5
|
+
// sequence via handleHarness.
|
|
6
6
|
|
|
7
7
|
import { execFileSync } from 'node:child_process';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
9
10
|
|
|
10
11
|
import { handleHarness } from '../harness.ts';
|
|
11
|
-
import { recipeRuntimePath } from '../paths.ts';
|
|
12
|
+
import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
12
13
|
import type { MetaMaskRecipeAdapter } from '../types.ts';
|
|
14
|
+
import { prepareMobile } from '../adapters/mobile/prepare.ts';
|
|
13
15
|
import {
|
|
16
|
+
ADAPTER_DETECT_NEXT,
|
|
14
17
|
EXIT,
|
|
15
18
|
flag,
|
|
16
19
|
parseFlags,
|
|
17
20
|
resolveAdapter,
|
|
18
|
-
|
|
21
|
+
spawnScript,
|
|
19
22
|
str,
|
|
20
23
|
targetOf,
|
|
21
24
|
usageOut,
|
|
22
25
|
} from './shared.ts';
|
|
23
|
-
import type { DeviceAdapter } from './shared.ts';
|
|
26
|
+
import type { DeviceAdapter, ScriptResult } from './shared.ts';
|
|
24
27
|
import {
|
|
25
28
|
RECOVERY_CODE,
|
|
26
29
|
checkHealBounds,
|
|
@@ -36,7 +39,7 @@ interface LaunchFailure {
|
|
|
36
39
|
recoverable: boolean;
|
|
37
40
|
userAction?: string;
|
|
38
41
|
exitCode: number;
|
|
39
|
-
// The original
|
|
42
|
+
// The original script output, surfaced verbatim (never replaced by the note).
|
|
40
43
|
originalError?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -58,7 +61,7 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
58
61
|
const target = targetOf(options);
|
|
59
62
|
|
|
60
63
|
const heal = parseHeal(options, 'auto');
|
|
61
|
-
if (typeof heal !== 'string') return usageOut(json, 'launch', heal.error);
|
|
64
|
+
if (typeof heal !== 'string') return usageOut(json, 'launch', heal.error, 'use --heal off|infra-only|auto');
|
|
62
65
|
|
|
63
66
|
// Grammar: mobile requires an ios|android positional (also disambiguates the
|
|
64
67
|
// adapter); extension takes no positional; core is headless (teaching error).
|
|
@@ -72,20 +75,17 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
72
75
|
const adapter = resolveAdapter(options, target, adapterHint);
|
|
73
76
|
|
|
74
77
|
if (!adapter) {
|
|
75
|
-
return usageOut(
|
|
76
|
-
json,
|
|
77
|
-
'launch',
|
|
78
|
-
`could not detect the MetaMask repo type for ${target}\n Next: pass --adapter mobile|extension, or run inside a checkout.`,
|
|
79
|
-
);
|
|
78
|
+
return usageOut(json, 'launch', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
80
79
|
}
|
|
81
80
|
if (adapter === 'core') {
|
|
82
|
-
return usageOut(json, 'launch', 'core is headless; there is nothing to launch.
|
|
81
|
+
return usageOut(json, 'launch', 'core is headless; there is nothing to launch.', 'mm-harness verify');
|
|
83
82
|
}
|
|
84
83
|
if (adapter === 'mobile' && !mobileTarget) {
|
|
85
84
|
return usageOut(
|
|
86
85
|
json,
|
|
87
86
|
'launch',
|
|
88
|
-
'target is required for mobile
|
|
87
|
+
'target is required for mobile.',
|
|
88
|
+
'mm-harness launch ios or mm-harness launch android',
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -96,7 +96,7 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
96
96
|
|
|
97
97
|
// --runway is mobile-only.
|
|
98
98
|
if (wantRunway && adapter !== 'mobile') {
|
|
99
|
-
return usageOut(json, 'launch', 'runway is mobile-only.
|
|
99
|
+
return usageOut(json, 'launch', 'runway is mobile-only.', 'drop --runway for the extension');
|
|
100
100
|
}
|
|
101
101
|
// --fullscreen is the explicit override when both display flags are passed.
|
|
102
102
|
const displayMode =
|
|
@@ -109,10 +109,15 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
109
109
|
for (const portFlag of ['cdpPort', 'watcherPort'] as const) {
|
|
110
110
|
const value = str(options, portFlag);
|
|
111
111
|
if (value !== undefined && !/^\d+$/u.test(value)) {
|
|
112
|
-
return usageOut(
|
|
112
|
+
return usageOut(
|
|
113
|
+
json,
|
|
114
|
+
'launch',
|
|
115
|
+
`--${portFlag === 'cdpPort' ? 'cdp-port' : 'watcher-port'} must be numeric (got: ${value}).`,
|
|
116
|
+
`pass a numeric port, e.g. --${portFlag === 'cdpPort' ? 'cdp-port 6663' : 'watcher-port 8081'}`,
|
|
117
|
+
);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
|
-
applyLaunchEnvOverrides(options, adapter, mobileTarget);
|
|
120
|
+
applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
116
121
|
|
|
117
122
|
const tier = wantVerify ? 'verify' : wantBuild ? 'build' : 'quick';
|
|
118
123
|
const state = newHealState();
|
|
@@ -123,9 +128,8 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
123
128
|
return usageOut(
|
|
124
129
|
json,
|
|
125
130
|
'launch',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}--build`,
|
|
131
|
+
'native build inputs changed since last build.',
|
|
132
|
+
`mm-harness launch ${adapter === 'mobile' ? `${mobileTarget} ` : ''}--build`,
|
|
129
133
|
);
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -152,9 +156,8 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
152
156
|
return code;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
// Compose the launch tier
|
|
156
|
-
|
|
157
|
-
let attempt = spawnPorcelain(adapter, launchArgs, target, json);
|
|
159
|
+
// Compose the launch tier: mobile runs in-process; extension spawns adapters/ scripts.
|
|
160
|
+
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
158
161
|
|
|
159
162
|
if (attempt.status === 0) {
|
|
160
163
|
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
@@ -182,12 +185,12 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
182
185
|
});
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
// Infra recovery: attempt the SAME launch exactly once (
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
+
// Infra recovery: attempt the SAME launch exactly once (Metro restarts /
|
|
189
|
+
// Chrome reopens). Push recoveryCode BEFORE the attempt so any subsequent
|
|
190
|
+
// checkHealBounds call sees it and refuses a second loop.
|
|
188
191
|
const recoveryCode = RECOVERY_CODE[adapter];
|
|
189
192
|
state.attemptedRecoveries.push(recoveryCode);
|
|
190
|
-
attempt =
|
|
193
|
+
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
191
194
|
if (attempt.status === 0) {
|
|
192
195
|
state.recovered.push(recoveryCode);
|
|
193
196
|
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
@@ -202,15 +205,70 @@ export async function handleLaunch(argv: string[]): Promise<number> {
|
|
|
202
205
|
});
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
208
|
+
// Apply KEY=VALUE lines from slot resolution to process.env.
|
|
209
|
+
// overwrite=true → pool match, always overrides existing env.
|
|
210
|
+
// overwrite=false → formula match, only fills vars that are unset.
|
|
211
|
+
function applyKVLines(output: string, overwrite: boolean): void {
|
|
212
|
+
for (const line of output.split('\n')) {
|
|
213
|
+
const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
|
|
214
|
+
if (!m) continue;
|
|
215
|
+
const [, key, val] = m;
|
|
216
|
+
switch (key) {
|
|
217
|
+
case 'WATCHER_PORT':
|
|
218
|
+
if (overwrite || !process.env['WATCHER_PORT']) {
|
|
219
|
+
process.env['WATCHER_PORT'] = val;
|
|
220
|
+
process.env['METRO_PORT'] = val;
|
|
221
|
+
process.env['RECIPE_WATCHER_PORT'] = val;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case 'IOS_SIMULATOR':
|
|
225
|
+
if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
|
|
226
|
+
break;
|
|
227
|
+
case 'SLOT_ID':
|
|
228
|
+
if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Resolve mobile slot port/simulator from the farmslot pool (pool-wins-over-env)
|
|
235
|
+
// or from the slot-suffix formula (only fills unset vars). Called before explicit
|
|
236
|
+
// CLI flag overrides so flags always win at the top.
|
|
237
|
+
function resolveMobileSlotPorts(target: string): void {
|
|
238
|
+
const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
|
|
239
|
+
try {
|
|
240
|
+
// Pool match always wins — overwrite whatever is in the environment.
|
|
241
|
+
const poolOut = execFileSync('bash', [
|
|
242
|
+
'-c', `source "${resolveScript}" && resolve_farmslot_ports_by_repo "${target}"`,
|
|
243
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
244
|
+
if (poolOut.trim()) {
|
|
245
|
+
applyKVLines(poolOut, true);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
} catch { /* no pool match — fall through to formula */ }
|
|
249
|
+
try {
|
|
250
|
+
// Formula match only fills unset vars (never overrides explicit env/pool).
|
|
251
|
+
const defOut = execFileSync('bash', [
|
|
252
|
+
'-c', `source "${resolveScript}" && resolve_mobile_slot_defaults "${target}"`,
|
|
253
|
+
], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
254
|
+
if (defOut.trim()) applyKVLines(defOut, false);
|
|
255
|
+
} catch { /* no slot suffix in dir name — stays at env defaults */ }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Map the env-gap flags onto the env vars the leaf scripts read. For mobile, slot
|
|
259
|
+
// port/simulator defaults are resolved first (pool-wins, then formula-fills-empty)
|
|
260
|
+
// so that the harness respects slot isolation; explicit CLI flags applied below
|
|
261
|
+
// win over everything. Extension: flag > env > config/default.
|
|
209
262
|
function applyLaunchEnvOverrides(
|
|
210
263
|
options: Record<string, string | boolean>,
|
|
211
264
|
adapter: DeviceAdapter,
|
|
212
265
|
mobileTarget: string | undefined,
|
|
266
|
+
target: string,
|
|
213
267
|
): void {
|
|
268
|
+
// Mobile slot isolation: resolve WATCHER_PORT / IOS_SIMULATOR / SLOT_ID from
|
|
269
|
+
// the farmslot pool or directory-suffix formula BEFORE applying explicit flags.
|
|
270
|
+
if (adapter === 'mobile') resolveMobileSlotPorts(target);
|
|
271
|
+
|
|
214
272
|
const device = str(options, 'device');
|
|
215
273
|
if (device && adapter === 'mobile') {
|
|
216
274
|
if (mobileTarget === 'android') {
|
|
@@ -261,35 +319,119 @@ function nativeInputsChanged(target: string, adapter: MetaMaskRecipeAdapter): bo
|
|
|
261
319
|
}
|
|
262
320
|
}
|
|
263
321
|
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// extension
|
|
269
|
-
// extension
|
|
270
|
-
|
|
322
|
+
// Execute the launch composition for (adapter, tier). Mobile runs prepareMobile()
|
|
323
|
+
// in-process (no shell orchestrator); extension arms spawn adapters/ scripts directly.
|
|
324
|
+
//
|
|
325
|
+
// mobile quick|build → prepareMobile() in-process → leaf scripts
|
|
326
|
+
// extension rebuild → kill watcher + clear rebuild.log + live.sh --start-watch
|
|
327
|
+
// extension build → adapters/extension/start-watch.sh
|
|
328
|
+
// extension watch → adapters/extension/start-watch.sh
|
|
329
|
+
async function executeComposition(
|
|
271
330
|
adapter: DeviceAdapter,
|
|
272
331
|
mobileTarget: string | undefined,
|
|
273
332
|
tier: 'quick' | 'build' | 'verify',
|
|
274
333
|
wantWatch: boolean,
|
|
275
|
-
|
|
334
|
+
target: string,
|
|
335
|
+
json: boolean,
|
|
336
|
+
): Promise<ScriptResult> {
|
|
276
337
|
if (adapter === 'mobile') {
|
|
277
|
-
|
|
278
|
-
|
|
338
|
+
const platform = mobileTarget ?? 'ios';
|
|
339
|
+
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : undefined;
|
|
340
|
+
const preflightMode = tier === 'build' ? 'auto' : 'fast';
|
|
341
|
+
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Extension rebuild (quick relaunch): kill watcher → clear rebuild.log → live.sh --start-watch.
|
|
345
|
+
if (!wantWatch && tier === 'quick') {
|
|
346
|
+
return extensionRebuild(target, json);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Extension build or watch: full webpack compile via start-watch.sh.
|
|
350
|
+
const startWatchSh = path.join(runnerDir, 'adapters/extension/start-watch.sh');
|
|
351
|
+
const watchArgs = ['--target', target];
|
|
352
|
+
if (process.env.WATCHER_PORT) watchArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
353
|
+
return spawnScript(startWatchSh, watchArgs, target, json);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Extension rebuild: kill the harness-owned webpack watcher (pid file + ps-scan
|
|
357
|
+
// for orphans), clear the rebuild log, then drive the installed overlay's
|
|
358
|
+
// live.sh --start-watch and tee its output into the rebuild log.
|
|
359
|
+
function extensionRebuild(target: string, json: boolean): ScriptResult {
|
|
360
|
+
const runtimeDirRel = recipeRuntimeDir();
|
|
361
|
+
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
362
|
+
const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
|
|
363
|
+
const rebuildLog = path.join(runtimeAbs, 'rebuild.log');
|
|
364
|
+
|
|
365
|
+
// E1a: Kill harness-owned watcher via pid file, then ps-scan for orphans.
|
|
366
|
+
try {
|
|
367
|
+
const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
|
|
368
|
+
if (/^\d+$/u.test(pid)) {
|
|
369
|
+
try { process.kill(Number(pid), 'SIGTERM'); } catch { /* already dead */ }
|
|
370
|
+
}
|
|
371
|
+
fs.rmSync(webpackPidFile, { force: true });
|
|
372
|
+
} catch { /* no pid file */ }
|
|
373
|
+
// Scan for any remaining orphan webpack/yarn-start processes in this checkout.
|
|
374
|
+
try {
|
|
375
|
+
const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
|
|
376
|
+
const orphanPids: number[] = [];
|
|
377
|
+
for (const line of psOut.split('\n')) {
|
|
378
|
+
const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
|
|
379
|
+
if (!match) continue;
|
|
380
|
+
const [, pidStr, cmd] = match;
|
|
381
|
+
const isWatcher =
|
|
382
|
+
cmd.includes('yarn start') ||
|
|
383
|
+
cmd.includes('webpack --watch') ||
|
|
384
|
+
cmd.includes('development/webpack/launch.ts --watch');
|
|
385
|
+
if (!isWatcher) continue;
|
|
386
|
+
if (cmd.includes(target)) {
|
|
387
|
+
orphanPids.push(Number(pidStr));
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// lsof cwd fallback for processes that don't embed the path in argv.
|
|
391
|
+
try {
|
|
392
|
+
const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
|
|
393
|
+
encoding: 'utf8',
|
|
394
|
+
timeout: 2000,
|
|
395
|
+
});
|
|
396
|
+
if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
|
|
397
|
+
orphanPids.push(Number(pidStr));
|
|
398
|
+
}
|
|
399
|
+
} catch { /* lsof unavailable or permission denied */ }
|
|
400
|
+
}
|
|
401
|
+
if (orphanPids.length > 0) {
|
|
402
|
+
for (const pid of orphanPids) {
|
|
403
|
+
try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
|
|
404
|
+
}
|
|
405
|
+
// Brief pause then force-kill survivors.
|
|
406
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
|
|
407
|
+
for (const pid of orphanPids) {
|
|
408
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} catch { /* ps not available */ }
|
|
412
|
+
|
|
413
|
+
// E1c: Clear the rebuild log (directory must exist for tee).
|
|
414
|
+
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
415
|
+
fs.writeFileSync(rebuildLog, '');
|
|
416
|
+
|
|
417
|
+
const liveScript = recipeHarnessPath(target, 'extension', 'scripts', 'live.sh');
|
|
418
|
+
const liveArgs = ['--target', target, '--start-watch'];
|
|
419
|
+
if (process.env.CDP_PORT) liveArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
420
|
+
if (process.env.WATCHER_PORT) liveArgs.push('--watcher-port', process.env.WATCHER_PORT);
|
|
421
|
+
const result = spawnScript(liveScript, liveArgs, target, json);
|
|
422
|
+
// Tee live.sh output into rebuild.log for post-mortem diagnosis.
|
|
423
|
+
if (result.output) {
|
|
424
|
+
try { fs.appendFileSync(rebuildLog, result.output); } catch { /* best-effort */ }
|
|
279
425
|
}
|
|
280
|
-
|
|
281
|
-
if (wantWatch) return ['watch'];
|
|
282
|
-
if (tier === 'build') return ['build'];
|
|
283
|
-
return ['rebuild'];
|
|
426
|
+
return result;
|
|
284
427
|
}
|
|
285
428
|
|
|
286
|
-
// Apply the extension display mode AFTER the base launch succeeded, then emit
|
|
287
|
-
// pass summary. Fullscreen = the plain rebuild/build/watch launch above
|
|
288
|
-
// default popup/home window). Sidepanel additionally drives the
|
|
289
|
-
// mechanism via
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// mode actually reaches orchestration. Mobile has no side panel, so the flag is a
|
|
429
|
+
// Apply the extension display mode AFTER the base launch succeeded, then emit
|
|
430
|
+
// the pass summary. Fullscreen = the plain rebuild/build/watch launch above
|
|
431
|
+
// (default popup/home window). Sidepanel additionally drives the
|
|
432
|
+
// chrome.sidePanel mechanism via adapters/extension/sidepanel-toggle.sh.
|
|
433
|
+
// --sidepanel and --fullscreen compose DIFFERENT script calls and the display
|
|
434
|
+
// mode actually reaches the browser. Mobile has no side panel; the flag is a
|
|
293
435
|
// no-op there.
|
|
294
436
|
function finishLaunch(
|
|
295
437
|
json: boolean,
|
|
@@ -301,12 +443,16 @@ function finishLaunch(
|
|
|
301
443
|
state: HealState,
|
|
302
444
|
): number {
|
|
303
445
|
if (adapter === 'extension' && displayMode === 'sidepanel') {
|
|
304
|
-
const
|
|
446
|
+
const sidepanelSh = path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh');
|
|
447
|
+
const sidepanelArgs = ['open'];
|
|
448
|
+
if (process.env.CDP_PORT) sidepanelArgs.push('--cdp-port', process.env.CDP_PORT);
|
|
449
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
|
|
305
450
|
if (sidepanel.status !== 0) {
|
|
306
451
|
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
307
452
|
code: 'SIDEPANEL_OPEN_FAILED',
|
|
308
453
|
message: 'app launched but opening the side panel failed.',
|
|
309
454
|
recoverable: false,
|
|
455
|
+
userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? '<CDP_PORT>'}`,
|
|
310
456
|
exitCode: EXIT.runtime,
|
|
311
457
|
originalError: sidepanel.output.trim() || undefined,
|
|
312
458
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// logs — tail Metro/webpack + app logs by composing log-tui.mjs (compact) or
|
|
2
|
+
// plain `tail -f` (--full). Teaches (exit 2) when nothing is running for the
|
|
3
|
+
// checkout.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { recipeRuntimePath, runnerDir } from '../paths.ts';
|
|
9
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from './shared.ts';
|
|
10
|
+
|
|
11
|
+
const LOGS_BOOLEANS = new Set(['full', 'json']);
|
|
12
|
+
|
|
13
|
+
export async function handleLogs(argv: string[]): Promise<number> {
|
|
14
|
+
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
15
|
+
const json = flag(options, 'json');
|
|
16
|
+
const target = targetOf(options);
|
|
17
|
+
const adapter = resolveAdapter(options, target);
|
|
18
|
+
|
|
19
|
+
if (!adapter) {
|
|
20
|
+
return usageOut(json, 'logs', `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
21
|
+
}
|
|
22
|
+
if (adapter === 'core') {
|
|
23
|
+
return usageOut(json, 'logs', 'core is headless; there are no app/Metro logs.', 'mm-harness run <recipe> # run recipes against the headless core');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const source = str(options, 'source') ?? 'metro';
|
|
27
|
+
if (source !== 'metro' && source !== 'app') {
|
|
28
|
+
return usageOut(json, 'logs', '--source must be metro or app.', 'mm-harness logs --source metro or mm-harness logs --source app');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Env-gap flag (docs/CLI-SPEC.md Part 4): --events sets the compact event count
|
|
32
|
+
// read from RECIPE_LOG_EVENTS. Resolution is flag > env > default: the flag wins
|
|
33
|
+
// by setting the env; an absent flag leaves the agent/CI env alone.
|
|
34
|
+
const events = str(options, 'events');
|
|
35
|
+
if (events !== undefined) {
|
|
36
|
+
if (!/^\d+$/u.test(events)) {
|
|
37
|
+
return usageOut(json, 'logs', `--events must be numeric (got: ${events}).`, 'mm-harness logs --events 20');
|
|
38
|
+
}
|
|
39
|
+
process.env.RECIPE_LOG_EVENTS = events;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Nothing running → teaching error pointing at launch. The log file is the
|
|
43
|
+
// signal that Metro/webpack has been started for this checkout.
|
|
44
|
+
const logFile = recipeRuntimePath(target, adapter === 'mobile' ? 'metro.log' : 'webpack.log');
|
|
45
|
+
if (!fs.existsSync(logFile)) {
|
|
46
|
+
const launchHint = adapter === 'mobile' ? 'mm-harness launch ios' : 'mm-harness launch';
|
|
47
|
+
return usageOut(
|
|
48
|
+
json,
|
|
49
|
+
'logs',
|
|
50
|
+
`nothing running for this checkout (no ${path.basename(logFile)}).`,
|
|
51
|
+
launchHint,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const full = flag(options, 'full');
|
|
56
|
+
if (full) {
|
|
57
|
+
// Raw follow: the human explicitly wants unfiltered output.
|
|
58
|
+
const result = spawnScript('tail', ['-n', '+1', '-F', logFile], target, json);
|
|
59
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Compact mode: last N events + pointer to full log.
|
|
63
|
+
const logTui = path.join(runnerDir, 'adapters/shared/log-tui.mjs');
|
|
64
|
+
const eventCount = process.env.RECIPE_LOG_EVENTS ?? '20';
|
|
65
|
+
const uiMode = process.env.RECIPE_LOG_UI_MODE ?? 'compact';
|
|
66
|
+
const result = spawnScript(
|
|
67
|
+
process.execPath,
|
|
68
|
+
[logTui, 'tail', '--log', logFile, '--events', eventCount, '--follow', '--mode', uiMode],
|
|
69
|
+
target,
|
|
70
|
+
json,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (json) {
|
|
74
|
+
console.log(
|
|
75
|
+
JSON.stringify(
|
|
76
|
+
{ schemaVersion: 1, command: 'logs', adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
77
|
+
null,
|
|
78
|
+
2,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
83
|
+
}
|