Setting the file. One moment.
Codegen · Autobrowse · browserbase/skills · Skills Docs
ContentsBack to the top of the page function anthropic
— line 260
This file
Number 2.4
Position 4 of 22
Type JavaScript
Size 22 KB
Lines 515 scripts/ codegen.mjs
JavaScript · 515 lines · 22 KB
* 4. Drop the framework's scaffold files (package.json, tsconfig, …).
16 * 5. If --verify: invoke the framework's runner against a fresh Browserbase
17 * session. On failure, feed the error back into a rewrite call up to
18 * --max-retries times.
19 *
20 * One JSON status line per framework on stdout. Non-zero exit if any selected
21 * framework's final state is fail.
22 *
23 * Usage:
24 * node scripts/codegen.mjs --task <name> [options]
25 *
26 * Options:
27 * --task <name> task name under <workspace>/tasks/ (required)
28 * --workspace <dir> default ./autobrowse
29 * --run <id> default: latest run-NNN with success: true
30 * --frameworks <a,b,...> default: playwright
31 * --verify | --no-verify default: --verify
32 * --max-retries <N> rewrite-on-verify-failure cap (default: 2)
33 * --cache-dir <dir> default <workspace>/codegen-cache
34 * --out <dir> default <workspace>/tasks/<name>/<framework>
35 * --prompt-template <path> custom framework prompt (pair with --frameworks custom)
36 * --force bust cache
37 * --dry-run estimate cost without LLM call
38 * --cache-only error if cache miss (no LLM call)
39 * --model <name> override Claude model
40 * --help
41 */
42
43 import "dotenv/config" ;
44 import Anthropic from "@anthropic-ai/sdk" ;
45 import * as fs from "node:fs" ;
46 import * as path from "node:path" ;
47 import { execFileSync, spawnSync } from "node:child_process" ;
48 import { fileURLToPath } from "node:url" ;
49 import crypto from "node:crypto" ;
50
51 const __dirname = path. dirname ( fileURLToPath ( import . meta .url));
52 const SKILL_DIR = path. resolve (__dirname, ".." );
53 const PROMPT_TEMPLATE_VERSION = "2" ; // bump to invalidate cache after prompt edits or scaffold/runner contract changes
54
55 const DEFAULT_MODEL = "claude-sonnet-4-6" ;
56 const DEFAULT_MAX_TOKENS = 8192 ;
57
58 // ── CLI ────────────────────────────────────────────────────────────
59
60 function getArg ( name , fallback ) {
61 const i = process.argv. indexOf ( `--${ name }` );
62 return i !== - 1 && process.argv[i + 1 ] ? process.argv[i + 1 ] : fallback;
63 }
64 const hasFlag = ( n ) => process.argv. includes ( `--${ n }` );
65
66 if ( hasFlag ( "help" ) || hasFlag ( "h" )) {
67 console. log ( `autobrowse codegen — produce runnable scripts from a converged trace
68
69 Usage: node scripts/codegen.mjs --task <name> [options]
70
71 Options:
72 --task <name> task name under <workspace>/tasks/ (required)
73 --workspace <dir> default: ./autobrowse
74 --run <id> specific run-NNN (default: newest passing)
75 --frameworks <a,b,...> comma list; default: playwright
76 builtins: playwright, stagehand
77 --verify | --no-verify run the script in a fresh BB session (default: --verify)
78 --max-retries <N> cap rewrite-on-verify-fail loop (default: 2)
79 --cache-dir <dir> default: <workspace>/codegen-cache
80 --out <dir> default: <workspace>/tasks/<name>/<framework>
81 --prompt-template <path> custom prompt template (pair with --frameworks=custom)
82 --force ignore cache, regenerate
83 --dry-run estimate cost; don't call the LLM
84 --cache-only error if cache miss
85 --model <name> default: ${ DEFAULT_MODEL }
86
87 Env:
88 ANTHROPIC_API_KEY required for LLM call
89 BROWSERBASE_API_KEY required for --verify
90
91 Exits 0 if all selected frameworks ended in pass (or --no-verify), 2 if any
92 failed, 1 on harness error.` );
93 process. exit ( 0 );
94 }
95
96 const TASK = getArg ( "task" );
97 if ( ! TASK ) {
98 console. error ( "ERROR: --task <name> is required. Pass --help for usage." );
99 process. exit ( 1 );
100 }
101 const WORKSPACE = path. resolve ( getArg ( "workspace" , "autobrowse" ));
102 const FORCED_RUN = getArg ( "run" );
103 const FRAMEWORKS = getArg ( "frameworks" , "playwright" ). split ( "," ). map (( s ) => s. trim ()). filter (Boolean);
104 const VERIFY = ! hasFlag ( "no-verify" );
105 const MAX_RETRIES = parseInt ( getArg ( "max-retries" , "2" ), 10 );
106 const CACHE_DIR = path. resolve ( getArg ( "cache-dir" , path. join ( WORKSPACE , "codegen-cache" )));
107 const OUT_OVERRIDE = getArg ( "out" );
108 const PROMPT_TEMPLATE_OVERRIDE = getArg ( "prompt-template" );
109 const FORCE = hasFlag ( "force" );
110 const DRY_RUN = hasFlag ( "dry-run" );
111 const CACHE_ONLY = hasFlag ( "cache-only" );
112 const MODEL = getArg ( "model" , DEFAULT_MODEL );
113
114 // ── Inputs ─────────────────────────────────────────────────────────
115
116 const taskDir = path. join ( WORKSPACE , "tasks" , TASK );
117 const tracesDir = path. join ( WORKSPACE , "traces" , TASK );
118 const taskFile = path. join (taskDir, "task.md" );
119
120 for ( const [ label , file ] of [[ "task.md" , taskFile]]) {
121 if ( ! fs. existsSync (file)) {
122 console. error ( `ERROR: ${ label } not found at ${ file }. Run autobrowse first.` );
123 process. exit ( 1 );
124 }
125 }
126
127 function pickRun () {
128 if ( FORCED_RUN ) {
129 // --run was passed; still confirm the directory exists. Without this we'd
130 // happily call codegen with empty trace/events/descriptors and the LLM
131 // would invent a script from just task.md + strategy.md, while logs
132 // still report the forced run id as if it were a real input.
133 const forcedDir = path. join (tracesDir, FORCED_RUN );
134 if ( ! fs. existsSync (forcedDir)) return null ;
135 return FORCED_RUN ;
136 }
137 if ( ! fs. existsSync (tracesDir)) return null ;
138 const runs = fs. readdirSync (tracesDir)
139 . filter (( d ) => / ^ run- \d +$ / . test (d))
140 . sort ()
141 . reverse ();
142 for ( const r of runs) {
143 const summary = path. join (tracesDir, r, "summary.md" );
144 if ( ! fs. existsSync (summary)) continue ;
145 const text = fs. readFileSync (summary, "utf-8" );
146 if ( /success: \s * true/ . test (text) || /"success" \s * : \s * true/ . test (text)) return r;
147 }
148 return null ;
149 }
150
151 const RUN_ID = pickRun ();
152 if ( ! RUN_ID ) {
153 if ( FORCED_RUN ) {
154 console. error ( `ERROR: --run ${ FORCED_RUN } not found at ${ path . join ( tracesDir , FORCED_RUN ) }.` );
155 } else {
156 console. error ( `ERROR: no passing run found under ${ tracesDir }. Pass --run <id> to force, or run autobrowse first.` );
157 }
158 process. exit ( 1 );
159 }
160 const runDir = path. join (tracesDir, RUN_ID );
161
162 // Try multiple candidate paths for each input — autobrowse layouts have
163 // shifted over time and we want this to be robust to both modern and legacy.
164 function readFirstExisting ( ... candidates ) {
165 for ( const p of candidates) {
166 if (p && fs. existsSync (p)) return { path: p, content: fs. readFileSync (p, "utf-8" ) };
167 }
168 return null ;
169 }
170
171 const taskMd = fs. readFileSync (taskFile, "utf-8" );
172 const strategyMd = readFirstExisting (path. join (taskDir, "strategy.md" ))?.content || "" ;
173 const traceJson = readFirstExisting (path. join (runDir, "trace.json" ))?.content || "" ;
174 const unifiedEvents = readFirstExisting (path. join (runDir, "unified-events.jsonl" ))?.content || "" ;
175 const descriptors = readFirstExisting (
176 path. join (runDir, ".o11y" , RUN_ID , "cdp" , "descriptors.ndjson" ),
177 path. join (runDir, "cdp" , "descriptors.ndjson" ),
178 )?.content || "" ;
179
180 // ── Framework registry ────────────────────────────────────────────
181
182 const CODEGEN_DIR = path. join ( SKILL_DIR , "codegen" );
183 const REFERENCES_DIR = path. join ( SKILL_DIR , "references" );
184
185 function frameworkConfig ( framework ) {
186 const promptPath = PROMPT_TEMPLATE_OVERRIDE && framework === "custom"
187 ? path. resolve ( PROMPT_TEMPLATE_OVERRIDE )
188 : path. join ( CODEGEN_DIR , "prompts" , `${ framework }.md` );
189 const scaffoldDir = path. join ( CODEGEN_DIR , "scaffolds" , framework);
190 const runnerPath = path. join ( CODEGEN_DIR , "runners" , `${ framework }.mjs` );
191 const extByFramework = { playwright: "ts" , stagehand: "ts" , puppeteer: "js" , selenium: "py" };
192 const ext = extByFramework[framework] || "ts" ;
193 return { promptPath, scaffoldDir, runnerPath, ext };
194 }
195
196 // ── Context builder ───────────────────────────────────────────────
197
198 // Trim a stringified blob to a budget while keeping head + tail.
199 function clip ( text , maxBytes ) {
200 if (text. length <= maxBytes) return text;
201 const head = Math. floor (maxBytes * 0.7 );
202 const tail = maxBytes - head - 64 ;
203 return text. slice ( 0 , head) + ` \n\n …[truncated ${ text . length - head - tail } bytes]… \n\n ` + text. slice ( - tail);
204 }
205
206 function buildContext ({ promptTemplate , cdpBridgeDoc , previousAttempt , verifyFailure }) {
207 const parts = [];
208 parts. push ( "# Task \n\n " + taskMd. trim ());
209 if (strategyMd. trim ()) parts. push ( "# Strategy notes \n\n " + strategyMd. trim ());
210 if (cdpBridgeDoc) parts. push ( "# Reference: Playwright ↔ Browserbase bridge \n\n " + cdpBridgeDoc. trim ());
211 if (unifiedEvents. trim ()) {
212 parts. push ( "# Unified events (agent + browser, time-ordered) \n\n ``` \n " + clip (unifiedEvents, 32_000 ) + " \n ```" );
213 } else if (traceJson. trim ()) {
214 parts. push ( "# Trace (agent turns) \n\n ```json \n " + clip (traceJson, 32_000 ) + " \n ```" );
215 }
216 if (descriptors. trim ()) {
217 parts. push ( "# Descriptors (per-command DOM target) \n\n ``` \n " + clip (descriptors, 16_000 ) + " \n ```" );
218 }
219 if (previousAttempt && verifyFailure) {
220 parts. push (
221 "# Previous attempt and the verify failure \n\n Your previous attempt was: \n\n ``` \n " +
222 clip (previousAttempt, 12_000 ) +
223 " \n ``` \n\n It failed verification with: \n\n ``` \n " +
224 clip (verifyFailure, 4_000 ) +
225 " \n ``` \n\n Fix the issue and emit a complete corrected script." ,
226 );
227 }
228 return promptTemplate. trim () + " \n\n " + parts. join ( " \n\n " );
229 }
230
231 // ── Cache ─────────────────────────────────────────────────────────
232
233 function hashContent ( s ) {
234 return crypto. createHash ( "sha256" ). update (s). digest ( "hex" ). slice ( 0 , 16 );
235 }
236 function cacheKey ( framework , promptTemplate ) {
237 return hashContent ([
238 "v" + PROMPT_TEMPLATE_VERSION ,
239 framework,
240 hashContent (promptTemplate),
241 hashContent (taskMd),
242 hashContent (traceJson),
243 hashContent (unifiedEvents),
244 hashContent (descriptors),
245 hashContent (strategyMd),
246 ]. join ( "|" ));
247 }
248 function readCache ( key ) {
249 const p = path. join ( CACHE_DIR , `${ key }.txt` );
250 return fs. existsSync (p) ? fs. readFileSync (p, "utf-8" ) : null ;
251 }
252 function writeCache ( key , content ) {
253 fs. mkdirSync ( CACHE_DIR , { recursive: true });
254 fs. writeFileSync (path. join ( CACHE_DIR , `${ key }.txt` ), content);
255 }
256
257 // ── LLM call ──────────────────────────────────────────────────────
258
259 let _anthropic = null ;
260 function anthropic () {
261 if ( ! _anthropic) {
262 if ( ! process.env. ANTHROPIC_API_KEY && ! process.env. ANTHROPIC_AUTH_TOKEN ) {
263 throw new Error ( "ANTHROPIC_API_KEY (or ANTHROPIC_AUTH_TOKEN) is required for codegen." );
264 }
265 _anthropic = new Anthropic ();
266 }
267 return _anthropic;
268 }
269
270 async function callLlm ( systemPrompt , userMessage ) {
271 const res = await anthropic ().messages. create ({
272 model: MODEL ,
273 max_tokens: DEFAULT_MAX_TOKENS ,
274 system: systemPrompt,
275 messages: [{ role: "user" , content: userMessage }],
276 });
277 const text = res.content
278 . filter (( b ) => b.type === "text" )
279 . map (( b ) => b.text)
280 . join ( " \n " );
281 // The agent might emit fences anyway; strip a single outer code block.
282 const fenced = text. match ( / ^ ``` [\w-] * \n ( [\s\S] *? ) \n ``` \s *$ / );
283 const code = fenced ? fenced[ 1 ] : text. trim ();
284 const cost = (res.usage?.input_tokens ?? 0 ) * 3e-6 + (res.usage?.output_tokens ?? 0 ) * 15e-6 ;
285 return { code, cost, tokens: res.usage };
286 }
287
288 // ── Scaffold + write output ───────────────────────────────────────
289
290 // Scaffold version pins. Each framework's scaffold/package.json references
291 // these via {{PLAYWRIGHT_VERSION}} / {{STAGEHAND_VERSION}} / etc. so callers
292 // can canary a new release without forking — set the corresponding env var.
293 // Loose semver guard rejects shell-injection shapes before they hit npm.
294 const VERSION_RE = / ^ \d + \. \d + \. \d + (?:- [A-Za-z0-9.-] + ) ?$ / ;
295 function resolveVersion ( envName , fallback ) {
296 const raw = process.env[envName];
297 if ( ! raw) return fallback;
298 if ( ! VERSION_RE . test (raw)) {
299 throw new Error ( `${ envName }="${ raw }" is not a valid X.Y.Z[-tag] version` );
300 }
301 return raw;
302 }
303 const SCAFFOLD_VERSIONS = {
304 PLAYWRIGHT_VERSION: resolveVersion ( "PLAYWRIGHT_VERSION" , "1.50.0" ),
305 STAGEHAND_VERSION: resolveVersion ( "STAGEHAND_VERSION" , "3.4.0" ),
306 TSX_VERSION: resolveVersion ( "TSX_VERSION" , "4.22.3" ),
307 ZOD_VERSION: resolveVersion ( "ZOD_VERSION" , "4.4.3" ),
308 DOTENV_VERSION: resolveVersion ( "DOTENV_VERSION" , "16.4.5" ),
309 };
310
311 function templateInterpolate ( content , vars ) {
312 return Object. entries (vars). reduce (
313 ( acc , [ k , v ]) => acc. replaceAll ( `{{${ k }}}` , v),
314 content,
315 );
316 }
317
318 function dropScaffold ( scaffoldDir , outDir , taskName , scriptBasename ) {
319 if ( ! fs. existsSync (scaffoldDir)) return ;
320 // Two distinct template vars: TASK is the slug (used in package name),
321 // SCRIPT is the actual filename (used in the start script). They diverge
322 // in --out mode where files are named <framework>.ts but TASK is the
323 // task slug — without SCRIPT, `npm start` would invoke a missing file.
324 const vars = { TASK: taskName, SCRIPT: scriptBasename, ... SCAFFOLD_VERSIONS };
325 for ( const entry of fs. readdirSync (scaffoldDir)) {
326 const src = path. join (scaffoldDir, entry);
327 const dst = path. join (outDir, entry);
328 const content = templateInterpolate (fs. readFileSync (src, "utf-8" ), vars);
329 // Special-case package.json: when --out is shared across frameworks (e.g.
330 // browse.sh passes one dir for playwright+stagehand), the first framework
331 // writes its package.json and the second must MERGE its dependencies in,
332 // not skip. Otherwise the second framework's `node_modules` lacks its own
333 // runtime deps (e.g. @browserbasehq/stagehand) and verify can never pass.
334 if (entry === "package.json" && fs. existsSync (dst)) {
335 try {
336 const existing = JSON . parse (fs. readFileSync (dst, "utf-8" ));
337 const incoming = JSON . parse (content);
338 existing.dependencies = {
339 ... (existing.dependencies || {}),
340 ... (incoming.dependencies || {}),
341 };
342 existing.devDependencies = {
343 ... (existing.devDependencies || {}),
344 ... (incoming.devDependencies || {}),
345 };
346 fs. writeFileSync (dst, JSON . stringify (existing, null , 2 ) + " \n " );
347 continue ;
348 } catch {
349 // Fall through to never-overwrite policy if either side is malformed.
350 }
351 }
352 if (fs. existsSync (dst)) continue ; // never overwrite a user's file
353 fs. writeFileSync (dst, content);
354 }
355 }
356
357 // ── Verify ────────────────────────────────────────────────────────
358
359 function verify ( framework , outDir , scriptBasename ) {
360 const { runnerPath } = frameworkConfig (framework);
361 if ( ! fs. existsSync (runnerPath)) {
362 return { passed: false , error: `no runner for framework "${ framework }" at ${ runnerPath }` , runner_missing: true };
363 }
364 // The parent timeout must exceed the runner's worst case: tsx-runner allows
365 // up to 3min for npm install + 5min for the tsx run = 8min, plus slack for
366 // process startup and the trailing-JSON parse. 10min keeps us safely above
367 // that so a healthy slow run isn't killed mid-flight.
368 const res = spawnSync ( "node" , [runnerPath, "--out-dir" , outDir, "--script" , scriptBasename], {
369 encoding: "utf-8" ,
370 stdio: [ "ignore" , "pipe" , "pipe" ],
371 env: process.env,
372 timeout: 10 * 60 * 1000 ,
373 });
374 const stdout = res.stdout || "" ;
375 const stderr = res.stderr || "" ;
376 // Runners must emit a final JSON line: {"passed":true,...} or {"passed":false,...}
377 const lastLine = stdout. trim (). split ( " \n " ). pop () || "" ;
378 let parsed = null ;
379 try { parsed = JSON . parse (lastLine); } catch {}
380 if (parsed && typeof parsed.passed === "boolean" ) {
381 return { ... parsed, stdout, stderr };
382 }
383 return { passed: false , error: `runner did not emit a {passed:boolean} JSON line; exit=${ res . status }` , stdout, stderr };
384 }
385
386 // ── Per-framework pipeline ────────────────────────────────────────
387
388 async function generateOne ( framework ) {
389 const cfg = frameworkConfig (framework);
390 if ( ! fs. existsSync (cfg.promptPath)) {
391 return { framework, passed: false , error: `no prompt template for "${ framework }" at ${ cfg . promptPath }` };
392 }
393 const promptTemplate = fs. readFileSync (cfg.promptPath, "utf-8" );
394 const cdpBridgeDoc = fs. existsSync (path. join ( REFERENCES_DIR , "playwright-cdp-bridge.md" ))
395 ? fs. readFileSync (path. join ( REFERENCES_DIR , "playwright-cdp-bridge.md" ), "utf-8" )
396 : "" ;
397
398 // Filename + outDir convention:
399 // - default mode (--out unset): per-framework subdir, file named after the
400 // task, so the dir feels like a standalone project — e.g.
401 // tasks/<task>/playwright/<task>.ts with its own package.json.
402 // - --out mode: caller is flattening into someone else's tree (e.g.
403 // browse.sh's /tmp/skill/{domain}/{task}/), so we use the framework
404 // name as the filename — playwright.ts + stagehand.ts in the same dir,
405 // no collision.
406 const outDir = OUT_OVERRIDE ? path. resolve ( OUT_OVERRIDE ) : path. join (taskDir, framework);
407 const scriptBasename = OUT_OVERRIDE ? `${ framework }.${ cfg . ext }` : `${ TASK }.${ cfg . ext }` ;
408 fs. mkdirSync (outDir, { recursive: true });
409 const scriptPath = path. join (outDir, scriptBasename);
410
411 // Cache lookup
412 const key = cacheKey (framework, promptTemplate);
413 let cached = ! FORCE ? readCache (key) : null ;
414 if ( CACHE_ONLY && ! cached) {
415 return { framework, passed: false , error: `--cache-only set but no cached output for key ${ key }` };
416 }
417
418 if ( DRY_RUN ) {
419 const ctx = buildContext ({ promptTemplate, cdpBridgeDoc });
420 const bytes = ctx. length ;
421 const estCost = (bytes / 4 ) * 3e-6 ; // ~4 chars/token, $3/M in
422 return { framework, dryRun: true , prompt_bytes: bytes, estimated_cost_usd: Number (estCost. toFixed ( 4 )) };
423 }
424
425 // `attempts` counts emitted-script-versions. Cached and uncached both start
426 // at 1 (the script-on-disk is one version, whether the LLM just wrote it or
427 // we restored it from cache). The retry loop below then increments per
428 // rewrite, bounded by --max-retries. Initializing to 0 on a cache hit gave
429 // cached runs one extra rewrite vs uncached — caught by Bugbot.
430 let code, cost = 0 , attempts = 1 ;
431 if (cached) {
432 code = cached;
433 } else {
434 const ctx = buildContext ({ promptTemplate, cdpBridgeDoc });
435 const { code : c , cost : k } = await callLlm (
436 "You are an expert browser-automation engineer. Output ONLY the contents of the script file — no preamble, no explanation, no markdown fences. The script must be runnable as-is." ,
437 ctx,
438 );
439 code = c;
440 cost += k;
441 writeCache (key, code);
442 }
443
444 fs. writeFileSync (scriptPath, code);
445 dropScaffold (cfg.scaffoldDir, outDir, TASK , scriptBasename);
446
447 if ( ! VERIFY ) {
448 return { framework, passed: true , scriptPath, cached: !! cached, verify_skipped: true , cost_usd: cost };
449 }
450
451 // Verify loop with rewrite-on-failure
452 let lastVerify = verify (framework, outDir, scriptBasename);
453 while ( ! lastVerify.passed && attempts < MAX_RETRIES + 1 ) {
454 if (lastVerify.runner_missing) break ;
455 // --cache-only forbids ANY LLM call, including the rewrite path. Without
456 // this guard a cached script that fails verify would still burn quota
457 // through the rewrite loop, contradicting the documented "no LLM call"
458 // CI behavior.
459 if ( CACHE_ONLY ) break ;
460 attempts ++ ;
461 const previousCode = code;
462 const failureContext =
463 (lastVerify.error || "" ) +
464 " \n\n stderr: \n " + (lastVerify.stderr || "" ). slice ( - 2000 ) +
465 " \n stdout: \n " + (lastVerify.stdout || "" ). slice ( - 2000 );
466 const ctx = buildContext ({
467 promptTemplate,
468 cdpBridgeDoc,
469 previousAttempt: previousCode,
470 verifyFailure: failureContext,
471 });
472 const { code : c , cost : k } = await callLlm (
473 "You are an expert browser-automation engineer. Output ONLY the corrected script file — no preamble, no explanation, no markdown fences." ,
474 ctx,
475 );
476 code = c;
477 cost += k;
478 fs. writeFileSync (scriptPath, code);
479 writeCache (key, code); // overwrite cache with the latest attempt
480 lastVerify = verify (framework, outDir, scriptBasename);
481 }
482
483 return {
484 framework,
485 passed: lastVerify.passed,
486 scriptPath,
487 cached: !! cached && cost === 0 ,
488 verify_attempts: attempts,
489 last_error: lastVerify.passed ? null : (lastVerify.error || lastVerify.stderr?. slice ( - 200 ) || null ),
490 cost_usd: Number (cost. toFixed ( 4 )),
491 };
492 }
493
494 // ── Main ──────────────────────────────────────────────────────────
495
496 async function main () {
497 console. error ( `[codegen] task=${ TASK } run=${ RUN_ID } frameworks=[${ FRAMEWORKS . join ( "," ) }] verify=${ VERIFY }` );
498 let anyFailed = false ;
499 for ( const framework of FRAMEWORKS ) {
500 try {
501 const result = await generateOne (framework);
502 console. log ( JSON . stringify (result));
503 if (result.passed === false ) anyFailed = true ;
504 } catch (err) {
505 console. log ( JSON . stringify ({ framework, passed: false , error: err.message }));
506 anyFailed = true ;
507 }
508 }
509 process. exit (anyFailed ? 2 : 0 );
510 }
511
512 main (). catch (( err ) => {
513 console. error ( "FATAL:" , err.stack || err.message);
514 process. exit ( 1 );
515 });