bangonit 0.2.1 → 0.3.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 (43) hide show
  1. package/README.md +18 -29
  2. package/app/desktopapp/package.json +1 -38
  3. package/app/webapp/.next/BUILD_ID +1 -1
  4. package/app/webapp/.next/app-build-manifest.json +6 -6
  5. package/app/webapp/.next/build-manifest.json +3 -3
  6. package/app/webapp/.next/next-minimal-server.js.nft.json +1 -1
  7. package/app/webapp/.next/next-server.js.nft.json +1 -1
  8. package/app/webapp/.next/prerender-manifest.json +1 -1
  9. package/app/webapp/.next/required-server-files.json +1 -1
  10. package/app/webapp/.next/server/app/_not-found/page.js +1 -1
  11. package/app/webapp/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/app/webapp/.next/server/app/_not-found.html +1 -1
  13. package/app/webapp/.next/server/app/_not-found.rsc +1 -1
  14. package/app/webapp/.next/server/app/api/chat/route.js +1 -1
  15. package/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
  16. package/app/webapp/.next/server/app/app/page.js +2 -2
  17. package/app/webapp/.next/server/app/app/page_client-reference-manifest.js +1 -1
  18. package/app/webapp/.next/server/app/app.html +1 -1
  19. package/app/webapp/.next/server/app/app.rsc +2 -2
  20. package/app/webapp/.next/server/app/index.html +1 -1
  21. package/app/webapp/.next/server/app/index.rsc +1 -1
  22. package/app/webapp/.next/server/app/page.js +1 -1
  23. package/app/webapp/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/app/webapp/.next/server/chunks/679.js +1 -1
  25. package/app/webapp/.next/server/middleware-build-manifest.js +1 -1
  26. package/app/webapp/.next/server/pages/404.html +1 -1
  27. package/app/webapp/.next/server/pages/500.html +1 -1
  28. package/app/webapp/.next/server/server-reference-manifest.json +1 -1
  29. package/app/webapp/.next/static/chunks/app/app/{page-41ee880e93966e12.js → page-d38c1e48d37def82.js} +1 -1
  30. package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +1 -0
  31. package/app/webapp/.next/static/chunks/{main-app-76384b941f0b51cb.js → main-app-106dd83f859b9dfa.js} +1 -1
  32. package/app/webapp/.next/trace +2 -2
  33. package/app/webapp/.next/types/app/api/chat/route.ts +1 -1
  34. package/app/webapp/.next/types/app/api/screenshot/route.ts +1 -1
  35. package/app/webapp/.next/types/app/app/page.ts +1 -1
  36. package/app/webapp/.next/types/app/layout.ts +1 -1
  37. package/app/webapp/.next/types/app/page.ts +1 -1
  38. package/bin/bangonit.js +108 -220
  39. package/package.json +8 -2
  40. package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +0 -1
  41. package/scripts/regen-replays.sh +0 -46
  42. /package/app/webapp/.next/static/{BkuEihcexhIRjEt7SHF91 → TaLpPsk5rC30wNNcyfUN3}/_buildManifest.js +0 -0
  43. /package/app/webapp/.next/static/{BkuEihcexhIRjEt7SHF91 → TaLpPsk5rC30wNNcyfUN3}/_ssgManifest.js +0 -0
@@ -1,4 +1,4 @@
1
- // File: /home/floydophone/repos/domyjob/app/webapp/src/app/api/screenshot/route.ts
1
+ // File: /Users/pete/repos/growthgirl/app/webapp/src/app/api/screenshot/route.ts
2
2
  import * as entry from '../../../../../src/app/api/screenshot/route.js'
3
3
  import type { NextRequest } from 'next/server.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/floydophone/repos/domyjob/app/webapp/src/app/app/page.tsx
1
+ // File: /Users/pete/repos/growthgirl/app/webapp/src/app/app/page.tsx
2
2
  import * as entry from '../../../../src/app/app/page.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/floydophone/repos/domyjob/app/webapp/src/app/layout.tsx
1
+ // File: /Users/pete/repos/growthgirl/app/webapp/src/app/layout.tsx
2
2
  import * as entry from '../../../src/app/layout.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
@@ -1,4 +1,4 @@
1
- // File: /home/floydophone/repos/domyjob/app/webapp/src/app/page.tsx
1
+ // File: /Users/pete/repos/growthgirl/app/webapp/src/app/page.tsx
2
2
  import * as entry from '../../../src/app/page.js'
3
3
  import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
4
4
 
package/bin/bangonit.js CHANGED
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  return result;
35
35
  };
36
36
  })();
37
+ var __importDefault = (this && this.__importDefault) || function (mod) {
38
+ return (mod && mod.__esModule) ? mod : { "default": mod };
39
+ };
37
40
  Object.defineProperty(exports, "__esModule", { value: true });
38
41
  const child_process_1 = require("child_process");
39
42
  const path = __importStar(require("path"));
@@ -42,6 +45,9 @@ const net = __importStar(require("net"));
42
45
  const readline = __importStar(require("readline"));
43
46
  const TOML = __importStar(require("@iarna/toml"));
44
47
  const Minio = __importStar(require("minio"));
48
+ const yargs_1 = __importDefault(require("yargs"));
49
+ const helpers_1 = require("yargs/helpers");
50
+ const is_ci_1 = __importDefault(require("is-ci"));
45
51
  const ROOT = path.resolve(__dirname, "..");
46
52
  const WEBAPP_DIR = path.join(ROOT, "app", "webapp");
47
53
  const DESKTOP_DIR = path.join(ROOT, "app", "desktopapp");
@@ -192,12 +198,10 @@ function createS3Client(opts) {
192
198
  const secretKey = opts.secretKey || process.env.AWS_SECRET_ACCESS_KEY || "";
193
199
  const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
194
200
  if (opts.endpoint) {
195
- // Custom endpoint (DigitalOcean Spaces, MinIO, etc.)
196
201
  const useSSL = !opts.endpoint.startsWith("http://");
197
202
  const endPoint = opts.endpoint.replace(/^https?:\/\//, "");
198
203
  return new Minio.Client({ endPoint, useSSL, accessKey, secretKey, region });
199
204
  }
200
- // Default: AWS S3
201
205
  return new Minio.Client({
202
206
  endPoint: "s3.amazonaws.com",
203
207
  useSSL: true,
@@ -224,7 +228,6 @@ async function uploadToS3(localDir, opts) {
224
228
  await uploadDir(client, localDir, opts.bucket, opts.prefix);
225
229
  const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
226
230
  if (opts.endpoint) {
227
- // Custom endpoint URL
228
231
  const proto = opts.endpoint.startsWith("http://") ? "http" : "https";
229
232
  const host = opts.endpoint.replace(/^https?:\/\//, "");
230
233
  return `${proto}://${opts.bucket}.${host}/${opts.prefix}/index.html`;
@@ -269,6 +272,7 @@ async function initProject() {
269
272
  console.log(`\n ${c.bold}${c.magenta}Bang On It! init${c.reset}\n`);
270
273
  const testplans = await p.ask("Test plans directory", "testplans");
271
274
  const apiKey = await p.ask("Anthropic API key (empty to use env var)", "");
275
+ const recordingsDir = await p.ask("Recordings directory", "recordings");
272
276
  const s3Bucket = await p.ask("S3 bucket for recordings (empty to skip)", "");
273
277
  let s3Endpoint = "";
274
278
  let s3Region = "";
@@ -280,6 +284,7 @@ async function initProject() {
280
284
  }
281
285
  p.close();
282
286
  let toml = `testplans = "${testplans}"\n`;
287
+ toml += `recordings_dir = "${recordingsDir}"\n`;
283
288
  if (apiKey) {
284
289
  toml += `anthropic_api_key = "${apiKey}"\n`;
285
290
  }
@@ -288,15 +293,12 @@ async function initProject() {
288
293
  }
289
294
  if (s3Bucket) {
290
295
  toml += `\n[s3]\nbucket = "${s3Bucket}"\n`;
291
- if (s3Endpoint) {
296
+ if (s3Endpoint)
292
297
  toml += `endpoint = "${s3Endpoint}"\n`;
293
- }
294
- if (s3Region && s3Region !== "us-east-1") {
298
+ if (s3Region && s3Region !== "us-east-1")
295
299
  toml += `region = "${s3Region}"\n`;
296
- }
297
- if (s3Prefix && s3Prefix !== "bangonit") {
300
+ if (s3Prefix && s3Prefix !== "bangonit")
298
301
  toml += `prefix = "${s3Prefix}"\n`;
299
- }
300
302
  toml += `# access_key = "\${AWS_ACCESS_KEY_ID}"\n`;
301
303
  toml += `# secret_key = "\${AWS_SECRET_ACCESS_KEY}"\n`;
302
304
  }
@@ -322,7 +324,6 @@ async function initCi() {
322
324
  const waitUrl = await p.ask("URL to wait for before testing", "http://localhost:3000");
323
325
  const testDir = await p.ask("Test plan directory", "testplans");
324
326
  const triggerInput = await p.askChoice("Trigger on", ["pr", "push", "both"], "both");
325
- const s3Bucket = await p.ask("S3 bucket for recordings (empty to skip)", "");
326
327
  const timeout = await p.ask("Test timeout in seconds", "300");
327
328
  p.close();
328
329
  const trigger = triggerInput === "pr" ? "pull_request" :
@@ -344,94 +345,6 @@ async function initCi() {
344
345
  push:
345
346
  branches: [main, master]`;
346
347
  }
347
- let s3Steps = "";
348
- let s3Env = "";
349
- let recordFlag = "";
350
- if (s3Bucket) {
351
- recordFlag = " --record";
352
- s3Env = `
353
- AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
354
- AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
355
- AWS_REGION: \${{ secrets.AWS_REGION || 'us-east-1' }}`;
356
- s3Steps = `
357
- - name: Upload recordings to S3
358
- if: always()
359
- run: |
360
- if [ -d recordings ]; then
361
- COMMIT_SHA=\${{ github.sha }}
362
- RUN_ID=\${{ github.run_id }}
363
- for dir in recordings/*/; do
364
- [ -f "\$dir/index.html" ] || continue
365
- DIRNAME=\$(basename "\$dir")
366
- aws s3 cp "\$dir" "s3://${s3Bucket}/bangonit/\$COMMIT_SHA/\$DIRNAME/" --recursive --acl public-read --quiet
367
- done
368
- fi
369
- env:
370
- AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
371
- AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
372
- AWS_REGION: \${{ secrets.AWS_REGION || 'us-east-1' }}
373
-
374
- - name: Comment on PR/commit with results
375
- if: always()
376
- uses: actions/github-script@v7
377
- with:
378
- script: |
379
- const fs = require('fs');
380
- const bucket = '${s3Bucket}';
381
- const sha = context.sha;
382
- const region = process.env.AWS_REGION || 'us-east-1';
383
- const baseUrl = region === 'us-east-1'
384
- ? \`https://\${bucket}.s3.amazonaws.com\`
385
- : \`https://\${bucket}.s3.\${region}.amazonaws.com\`;
386
-
387
- // Read test output
388
- let output = '';
389
- try { output = fs.readFileSync('bangonit-output.json', 'utf-8'); } catch {}
390
- const results = output ? JSON.parse(output) : null;
391
-
392
- // Build recording links
393
- let recordingLinks = '';
394
- try {
395
- const dirs = fs.readdirSync('recordings');
396
- for (const dir of dirs) {
397
- if (fs.existsSync(\`recordings/\${dir}/index.html\`)) {
398
- const url = \`\${baseUrl}/bangonit/\${sha}/\${dir}/index.html\`;
399
- recordingLinks += \`- [\${dir}](\${url})\\n\`;
400
- }
401
- }
402
- } catch {}
403
-
404
- let body = '## Bang On It! Test Results\\n\\n';
405
- if (results) {
406
- const icon = results.status === 'pass' ? ':white_check_mark:' : ':x:';
407
- body += \`\${icon} **\${results.status.toUpperCase()}**\\n\\n\`;
408
- if (results.tests) {
409
- for (const t of results.tests) {
410
- const ti = t.status === 'pass' ? ':white_check_mark:' : ':x:';
411
- body += \`\${ti} \${t.name} (\${(t.duration/1000).toFixed(1)}s)\\n\`;
412
- }
413
- }
414
- }
415
- if (recordingLinks) {
416
- body += \`\\n### Recordings\\n\${recordingLinks}\`;
417
- }
418
-
419
- if (context.payload.pull_request) {
420
- await github.rest.issues.createComment({
421
- owner: context.repo.owner,
422
- repo: context.repo.repo,
423
- issue_number: context.payload.pull_request.number,
424
- body
425
- });
426
- } else {
427
- await github.rest.repos.createCommitComment({
428
- owner: context.repo.owner,
429
- repo: context.repo.repo,
430
- commit_sha: sha,
431
- body
432
- });
433
- }`;
434
- }
435
348
  const workflow = `name: Bang On It! Tests
436
349
 
437
350
  ${triggerYaml}
@@ -441,7 +354,7 @@ jobs:
441
354
  runs-on: ${baseImage}
442
355
  timeout-minutes: ${Math.ceil(parseInt(timeout) / 60) + 5}
443
356
  env:
444
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}${s3Env}
357
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
445
358
 
446
359
  steps:
447
360
  - uses: actions/checkout@v4
@@ -472,8 +385,8 @@ jobs:
472
385
  xvfb-run --auto-servernum boi run ${testDir}/*.md \\
473
386
  --headless --exit \\
474
387
  --timeout ${timeout} \\
475
- --output bangonit-output.json${recordFlag}
476
- ${s3Steps}
388
+ --output bangonit-output.json --record
389
+
477
390
  - name: Upload test results
478
391
  if: always()
479
392
  uses: actions/upload-artifact@v4
@@ -491,15 +404,9 @@ ${s3Steps}
491
404
  console.log(`\n ${c.green}Created${c.reset} ${path.relative(process.cwd(), outPath)}\n`);
492
405
  console.log(` ${c.yellow}Required GitHub secrets:${c.reset}`);
493
406
  console.log(` ${c.dim} ANTHROPIC_API_KEY ${c.reset}— your Anthropic API key`);
494
- if (s3Bucket) {
495
- console.log(` ${c.dim} AWS_ACCESS_KEY_ID ${c.reset}— AWS credentials for S3 upload`);
496
- console.log(` ${c.dim} AWS_SECRET_ACCESS_KEY${c.reset}`);
497
- console.log(` ${c.dim} AWS_REGION ${c.reset}— (optional, defaults to us-east-1)`);
498
- }
499
407
  console.log("");
500
408
  }
501
- // --- run command ---
502
- async function run(args, config) {
409
+ async function run(argv, config) {
503
410
  loadEnv();
504
411
  // Config can provide the API key (supports ${ENV_VAR} interpolation)
505
412
  if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
@@ -508,53 +415,48 @@ async function run(args, config) {
508
415
  if (!process.env.ANTHROPIC_API_KEY) {
509
416
  die("Error: ANTHROPIC_API_KEY is not set.\nSet it in your environment, .env file, or .bangonit/config.toml.");
510
417
  }
511
- // Parse --s3-bucket and --filter from args (consumed here, not forwarded to Electron)
512
- let s3Bucket = null;
513
- let filter = null;
514
- const electronArgs = [];
515
- for (let i = 0; i < args.length; i++) {
516
- if (args[i] === "--s3-bucket" && i + 1 < args.length) {
517
- s3Bucket = args[++i];
518
- }
519
- else if ((args[i] === "--filter" || args[i] === "-t") && i + 1 < args.length) {
520
- filter = args[++i];
521
- }
522
- else {
523
- electronArgs.push(args[i]);
524
- }
525
- }
526
- // Fall back to config for S3
527
- if (!s3Bucket && config.s3?.bucket) {
528
- s3Bucket = config.s3.bucket;
529
- }
530
- const s3Opts = {
531
- region: config.s3?.region,
532
- prefix: config.s3?.prefix || "bangonit",
533
- endpoint: config.s3?.endpoint,
534
- accessKey: config.s3?.access_key,
535
- secretKey: config.s3?.secret_key,
536
- };
537
- // If --s3-bucket is set, ensure --record is also set
538
- if (s3Bucket && !electronArgs.includes("--record")) {
539
- electronArgs.push("--record");
540
- }
541
- // If no test plan files passed and not using --plan/--auto, discover from testplans dir
542
- const hasFiles = electronArgs.some((a) => !a.startsWith("-"));
543
- const hasPlan = electronArgs.includes("--plan");
544
- const hasAuto = electronArgs.includes("--auto");
545
- if (!hasFiles && !hasPlan && !hasAuto && config.testplans) {
418
+ const recordingsDir = config.recordings_dir
419
+ ? path.resolve(process.cwd(), config.recordings_dir)
420
+ : path.join(process.cwd(), "recordings");
421
+ // Discover test plans if no files/plan/auto specified
422
+ const files = [...argv.files];
423
+ if (files.length === 0 && !argv.plan && !argv.auto && config.testplans) {
546
424
  const testDirPath = path.resolve(process.cwd(), config.testplans);
547
- const plans = findTestPlans(testDirPath, filter);
425
+ const plans = findTestPlans(testDirPath, argv.filter);
548
426
  if (plans.length > 0) {
549
- electronArgs.unshift(...plans);
427
+ files.push(...plans);
550
428
  }
551
- else if (filter) {
552
- die(`No test plans matching "${filter}" found in ${config.testplans}/`);
429
+ else if (argv.filter) {
430
+ die(`No test plans matching "${argv.filter}" found in ${config.testplans}/`);
553
431
  }
554
432
  }
555
- else if (!hasFiles && !hasPlan && !hasAuto && filter) {
433
+ else if (files.length === 0 && !argv.plan && !argv.auto && argv.filter) {
556
434
  die(`--filter requires a testplans directory configured in .bangonit/config.toml`);
557
435
  }
436
+ // Build args to forward to Electron
437
+ const electronArgs = [...files];
438
+ if (argv.plan)
439
+ electronArgs.push("--plan", argv.plan);
440
+ if (argv.auto)
441
+ electronArgs.push("--auto");
442
+ if (argv.prompt)
443
+ electronArgs.push("--prompt", argv.prompt);
444
+ if (argv.record)
445
+ electronArgs.push("--record", "--recordings-dir", recordingsDir);
446
+ if (argv.headless)
447
+ electronArgs.push("--headless");
448
+ if (argv.exit)
449
+ electronArgs.push("--exit");
450
+ if (argv.json)
451
+ electronArgs.push("--json");
452
+ if (argv.console)
453
+ electronArgs.push("--console");
454
+ if (argv.output)
455
+ electronArgs.push("--output", argv.output);
456
+ if (argv.concurrency != null)
457
+ electronArgs.push("--concurrency", String(argv.concurrency));
458
+ if (argv.timeout != null)
459
+ electronArgs.push("--timeout", String(argv.timeout));
558
460
  const PORT = await getFreePort();
559
461
  fs.mkdirSync(LOGS_DIR, { recursive: true });
560
462
  const nextDir = path.join(WEBAPP_DIR, ".next");
@@ -626,18 +528,20 @@ async function run(args, config) {
626
528
  });
627
529
  electronProc.on("exit", async (code) => {
628
530
  // Upload recordings to S3 if configured
629
- if (s3Bucket) {
630
- const recordingsDir = path.join(process.cwd(), "recordings");
531
+ if (config.s3?.bucket && argv.record) {
631
532
  if (fs.existsSync(recordingsDir)) {
632
533
  const dirs = fs.readdirSync(recordingsDir).filter((d) => fs.existsSync(path.join(recordingsDir, d, "index.html")));
534
+ const s3Prefix = config.s3.prefix || "bangonit";
633
535
  for (const dir of dirs) {
634
536
  const localPath = path.join(recordingsDir, dir);
635
- const prefix = `${s3Opts.prefix}/${dir}`;
636
537
  try {
637
538
  const url = await uploadToS3(localPath, {
638
- bucket: s3Bucket,
639
- prefix,
640
- ...s3Opts,
539
+ bucket: config.s3.bucket,
540
+ prefix: `${s3Prefix}/${dir}`,
541
+ region: config.s3.region,
542
+ endpoint: config.s3.endpoint,
543
+ accessKey: config.s3.access_key,
544
+ secretKey: config.s3.secret_key,
641
545
  });
642
546
  console.log(`\n${c.green}Recording:${c.reset} ${c.cyan}${url}${c.reset}`);
643
547
  }
@@ -648,72 +552,56 @@ async function run(args, config) {
648
552
  }
649
553
  }
650
554
  cleanup();
651
- process.exit(code || 0);
555
+ process.exit(code ?? 1);
652
556
  });
653
557
  }
654
558
  // --- main ---
655
- async function main() {
656
- const args = process.argv.slice(2);
657
- // Extract --config before other parsing
658
- let configPath = null;
659
- const filteredArgs = [];
660
- for (let i = 0; i < args.length; i++) {
661
- if (args[i] === "--config" && i + 1 < args.length) {
662
- configPath = args[++i];
663
- }
664
- else {
665
- filteredArgs.push(args[i]);
666
- }
667
- }
668
- if (filteredArgs.includes("--help") || filteredArgs.includes("-h")) {
669
- console.log(`Usage: boi <command> [options]
670
-
671
- Commands:
672
- run [files...] [options] Run test plans (or launch interactive UI)
673
- init Create a .bangonit/config.toml config file
674
- init-ci Generate a GitHub Actions workflow
675
-
676
- Run options:
677
- -t, --filter <text> Filter test plans by name substring
678
- --config <path> Path to config file (default: .bangonit/config.toml)
679
- --plan <text> Inline test plan (instead of file)
680
- --auto Auto-generate test plan from git state
681
- --prompt <text> Additional instructions appended to test plan
682
- --record Record session replay to recordings/ directory
683
- --s3-bucket <bucket> Upload recordings to S3 (overrides config)
684
- --headless Run without showing the browser window
685
- --exit Exit immediately after tests complete (for CI)
686
- --json Stream NDJSON events to stdout
687
- --console Forward browser console logs to stdout
688
- --output <file> Write JSON results to file
689
- --concurrency <n> Number of parallel agents (default: 1)
690
- --timeout <seconds> Test timeout in seconds (0 = none)
691
- --help Show this help message
692
-
693
- Aliases: bangonit, bang-on-it, boi
694
-
695
- Examples:
696
- boi run test.md Run a test plan file
697
- boi run --plan "test login flow" Run an inline test plan
698
- boi run --auto Auto-discover from git state
699
- boi run -t checkout Run test plans matching "checkout"
700
- boi run Launch interactive UI
701
- boi init Create .bangonit/config.toml
702
- boi init-ci Set up GitHub Actions`);
703
- process.exit(0);
704
- }
705
- // Route to subcommand
706
- if (filteredArgs[0] === "init" && !filteredArgs[1]) {
707
- await initProject();
708
- }
709
- else if (filteredArgs[0] === "init-ci") {
710
- await initCi();
711
- }
712
- else {
713
- // "run" command (explicit or implicit)
714
- const config = loadConfig(configPath);
715
- const runArgs = filteredArgs[0] === "run" ? filteredArgs.slice(1) : filteredArgs;
716
- await run(runArgs, config);
717
- }
718
- }
719
- main();
559
+ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
560
+ (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
561
+ .scriptName("boi")
562
+ .usage("Usage: $0 <command> [options]")
563
+ .command("init", "Create a .bangonit/config.toml config file", {}, () => { initProject(); })
564
+ .command("init-ci", "Generate a GitHub Actions workflow", {}, () => { initCi(); })
565
+ .command(["run [files..]", "$0"], "Run test plans (or launch interactive UI)", (y) => y
566
+ .positional("files", { type: "string", array: true, default: [], describe: "Test plan files" })
567
+ .option("filter", { alias: "t", type: "string", describe: "Filter test plans by name substring" })
568
+ .option("config", { type: "string", describe: "Path to config file (default: .bangonit/config.toml)" })
569
+ .option("plan", { type: "string", describe: "Inline test plan (instead of file)" })
570
+ .option("auto", { type: "boolean", default: false, describe: "Auto-generate test plan from git state" })
571
+ .option("prompt", { type: "string", describe: "Additional instructions appended to test plan" })
572
+ .option("record", { type: "boolean", default: false, describe: "Record session replay" })
573
+ .option("headless", { type: "boolean", default: ciDefaults.headless ?? false, describe: "Run without showing the browser window" })
574
+ .option("exit", { type: "boolean", default: ciDefaults.exit ?? false, describe: "Exit immediately after tests complete" })
575
+ .option("json", { type: "boolean", default: false, describe: "Stream NDJSON events to stdout" })
576
+ .option("console", { type: "boolean", default: false, describe: "Forward browser console logs to stdout" })
577
+ .option("output", { type: "string", describe: "Write JSON results to file" })
578
+ .option("concurrency", { type: "number", describe: "Number of parallel agents (default: 1)" })
579
+ .option("timeout", { type: "number", describe: "Test timeout in seconds (0 = none)" }), (argv) => {
580
+ const config = loadConfig(argv.config);
581
+ run({
582
+ files: argv.files,
583
+ filter: argv.filter,
584
+ plan: argv.plan,
585
+ auto: argv.auto,
586
+ prompt: argv.prompt,
587
+ record: argv.record,
588
+ headless: argv.headless,
589
+ exit: argv.exit,
590
+ json: argv.json,
591
+ console: argv.console,
592
+ output: argv.output,
593
+ concurrency: argv.concurrency,
594
+ timeout: argv.timeout,
595
+ }, config);
596
+ })
597
+ .example("$0 run test.md", "Run a test plan file")
598
+ .example("$0 run --plan 'test login flow'", "Run an inline test plan")
599
+ .example("$0 run --auto", "Auto-discover from git state")
600
+ .example("$0 run -t checkout", "Run test plans matching 'checkout'")
601
+ .example("$0 run", "Launch interactive UI")
602
+ .example("$0 init", "Create .bangonit/config.toml")
603
+ .example("$0 init-ci", "Set up GitHub Actions")
604
+ .strict()
605
+ .help()
606
+ .alias("h", "help")
607
+ .parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bangonit",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "AI-powered E2E testing tool",
5
5
  "bin": {
6
6
  "bangonit": "bin/bangonit.js",
@@ -49,6 +49,12 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@iarna/toml": "^2.2.5",
52
- "minio": "^8.0.7"
52
+ "is-ci": "^4.1.0",
53
+ "minio": "^8.0.7",
54
+ "yargs": "^18.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@types/is-ci": "^3.0.4",
58
+ "@types/yargs": "^17.0.35"
53
59
  }
54
60
  }
@@ -1 +0,0 @@
1
- (self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{5603:function(n,e,u){Promise.resolve().then(u.t.bind(u,2625,23))},2625:function(){}},function(n){n.O(0,[387,293,528,744],function(){return n(n.s=5603)}),_N_E=n.O()}]);
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Regenerate index.html for all existing recordings using the current replay bundle.
3
- # Usage: ./scripts/regen-replays.sh [recordings-dir]
4
- set -e
5
-
6
- cd "$(dirname "$0")/.."
7
-
8
- RECORDINGS_DIR="${1:-recordings}"
9
- REPLAY_JS="app/replay/dist/replay.js"
10
- REPLAY_CSS="app/replay/dist/replay.css"
11
-
12
- if [ ! -f "$REPLAY_JS" ] || [ ! -f "$REPLAY_CSS" ]; then
13
- echo "Replay bundle not found. Building..."
14
- cd app/replay && npx vite build && cd ../..
15
- fi
16
-
17
- count=0
18
- for dir in "$RECORDINGS_DIR"/*/; do
19
- [ -f "$dir/data.json" ] || continue
20
- python3 -c "
21
- import sys
22
- js = open('$REPLAY_JS').read()
23
- css = open('$REPLAY_CSS').read()
24
- data = open('${dir}data.json').read()
25
- html = '''<!DOCTYPE html>
26
- <html lang=\"en\">
27
- <head>
28
- <meta charset=\"UTF-8\">
29
- <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
30
- <title>Session Replay</title>
31
- <style>''' + css + '''</style>
32
- </head>
33
- <body>
34
- <div id=\"root\"></div>
35
- <script>window.__REPLAY_DATA__ = ''' + data + ''';</script>
36
- <script>''' + js + '''</script>
37
- </body>
38
- </html>'''
39
- with open('${dir}index.html', 'w') as f:
40
- f.write(html)
41
- "
42
- echo " Regenerated ${dir}index.html"
43
- count=$((count + 1))
44
- done
45
-
46
- echo "Done — $count recording(s) updated."