bangonit 0.4.3 → 0.5.2

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 (76) hide show
  1. package/README.md +2 -1
  2. package/app/desktopapp/dist/main/index.js +10 -4
  3. package/app/desktopapp/dist/main/ipc.js +9 -24
  4. package/app/desktopapp/dist/main/preload.js +0 -7
  5. package/app/desktopapp/dist/main/tabs.js +10 -5
  6. package/app/desktopapp/package.json +0 -1
  7. package/app/replay/dist/replay.js +2 -2
  8. package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
  9. package/app/webapp/.next/standalone/app/webapp/.next/app-build-manifest.json +11 -11
  10. package/app/webapp/.next/standalone/app/webapp/.next/app-path-routes-manifest.json +1 -1
  11. package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +3 -3
  12. package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
  13. package/app/webapp/.next/standalone/app/webapp/.next/required-server-files.json +1 -1
  14. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page.js +1 -1
  15. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
  17. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
  18. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
  19. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
  20. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page.js +6 -6
  21. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page_client-reference-manifest.js +1 -1
  22. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
  23. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +2 -2
  24. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
  25. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
  26. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page.js +1 -1
  27. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page_client-reference-manifest.js +1 -1
  28. package/app/webapp/.next/standalone/app/webapp/.next/server/app-paths-manifest.json +1 -1
  29. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/679.js +1 -1
  30. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/708.js +3 -3
  31. package/app/webapp/.next/standalone/app/webapp/.next/server/middleware-build-manifest.js +1 -1
  32. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
  33. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
  34. package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
  35. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
  36. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
  37. package/app/webapp/.next/{static/chunks/main-app-106dd83f859b9dfa.js → standalone/app/webapp/.next/static/chunks/main-app-76384b941f0b51cb.js} +1 -1
  38. package/app/webapp/.next/standalone/app/webapp/package.json +2 -6
  39. package/app/webapp/.next/standalone/app/webapp/server.js +1 -1
  40. package/app/webapp/.next/standalone/package.json +18 -6
  41. package/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
  42. package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
  43. package/app/webapp/.next/{standalone/app/webapp/.next/static/chunks/main-app-106dd83f859b9dfa.js → static/chunks/main-app-76384b941f0b51cb.js} +1 -1
  44. package/app/webapp/package.json +2 -6
  45. package/app/webapp/skills/document-review.md +1 -0
  46. package/app/webapp/skills/gmail.md +2 -0
  47. package/app/webapp/src/app/globals.css +8 -3
  48. package/app/webapp/src/app/layout.tsx +2 -8
  49. package/app/webapp/src/shared/api/chat.ts +49 -25
  50. package/app/webapp/src/shared/api/screenshot.ts +11 -10
  51. package/app/webapp/src/shared/components/AppShell.tsx +80 -109
  52. package/app/webapp/src/shared/components/SessionView.tsx +335 -248
  53. package/app/webapp/src/shared/components/VirtualCursor.tsx +13 -14
  54. package/app/webapp/src/shared/lib/browser/cursor.ts +2 -7
  55. package/app/webapp/src/shared/lib/browser/index.ts +56 -36
  56. package/app/webapp/src/shared/lib/browser/mouse.ts +86 -21
  57. package/app/webapp/src/shared/lib/browser/navigate.ts +1 -4
  58. package/app/webapp/src/shared/lib/browser/recorder.ts +12 -5
  59. package/app/webapp/src/shared/lib/browser/screenshot.ts +4 -4
  60. package/app/webapp/src/shared/lib/browser/snapshot.ts +9 -5
  61. package/app/webapp/src/shared/lib/browser/tabs.ts +1 -1
  62. package/app/webapp/src/shared/lib/browser/types.ts +3 -2
  63. package/app/webapp/src/shared/lib/browser/wait.ts +1 -1
  64. package/app/webapp/src/shared/lib/recorder/session-recorder.ts +1 -1
  65. package/app/webapp/src/shared/types/global.d.ts +8 -19
  66. package/app/webapp/tailwind.config.js +1 -3
  67. package/bin/src/cli/bangonit.js +270 -177
  68. package/package.json +18 -6
  69. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
  70. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
  71. package/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
  72. package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
  73. /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
  74. /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_ssgManifest.js +0 -0
  75. /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
  76. /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_ssgManifest.js +0 -0
@@ -44,7 +44,6 @@ const fs = __importStar(require("fs"));
44
44
  const net = __importStar(require("net"));
45
45
  const readline = __importStar(require("readline"));
46
46
  const TOML = __importStar(require("@iarna/toml"));
47
- const Minio = __importStar(require("minio"));
48
47
  const yargs_1 = __importDefault(require("yargs"));
49
48
  const helpers_1 = require("yargs/helpers");
50
49
  const is_ci_1 = __importDefault(require("is-ci"));
@@ -183,54 +182,12 @@ function findTestPlans(dir, filter) {
183
182
  }
184
183
  return results;
185
184
  }
186
- function createS3Client(opts) {
187
- const accessKey = opts.accessKey || process.env.AWS_ACCESS_KEY_ID || "";
188
- const secretKey = opts.secretKey || process.env.AWS_SECRET_ACCESS_KEY || "";
189
- const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
190
- if (opts.endpoint) {
191
- const useSSL = !opts.endpoint.startsWith("http://");
192
- const endPoint = opts.endpoint.replace(/^https?:\/\//, "");
193
- return new Minio.Client({ endPoint, useSSL, accessKey, secretKey, region });
194
- }
195
- return new Minio.Client({
196
- endPoint: "s3.amazonaws.com",
197
- useSSL: true,
198
- accessKey,
199
- secretKey,
200
- region,
201
- });
202
- }
203
- async function uploadDir(client, localDir, bucket, prefix) {
204
- const entries = fs.readdirSync(localDir, { withFileTypes: true });
205
- for (const entry of entries) {
206
- const fullPath = path.join(localDir, entry.name);
207
- const objectName = `${prefix}/${entry.name}`;
208
- if (entry.isDirectory()) {
209
- await uploadDir(client, fullPath, bucket, objectName);
210
- }
211
- else {
212
- await client.fPutObject(bucket, objectName, fullPath, {});
213
- }
214
- }
215
- }
216
- async function uploadToS3(localDir, opts) {
217
- const client = createS3Client(opts);
218
- await uploadDir(client, localDir, opts.bucket, opts.prefix);
219
- const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
220
- if (opts.endpoint) {
221
- const proto = opts.endpoint.startsWith("http://") ? "http" : "https";
222
- const host = opts.endpoint.replace(/^https?:\/\//, "");
223
- return `${proto}://${opts.bucket}.${host}/${opts.prefix}/index.html`;
224
- }
225
- if (region === "us-east-1") {
226
- return `https://${opts.bucket}.s3.amazonaws.com/${opts.prefix}/index.html`;
227
- }
228
- return `https://${opts.bucket}.s3.${region}.amazonaws.com/${opts.prefix}/index.html`;
229
- }
230
185
  function createPrompter() {
231
186
  let closed = false;
232
187
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
233
- rl.on("close", () => { closed = true; });
188
+ rl.on("close", () => {
189
+ closed = true;
190
+ });
234
191
  return {
235
192
  ask(question, defaultVal) {
236
193
  if (closed)
@@ -245,7 +202,7 @@ function createPrompter() {
245
202
  askChoice(question, choices, defaultVal) {
246
203
  if (closed)
247
204
  return Promise.resolve(defaultVal);
248
- const choiceStr = choices.map((ch) => ch === defaultVal ? `${c.bold}${ch}${c.reset}${c.dim}` : ch).join("/");
205
+ const choiceStr = choices.map((ch) => (ch === defaultVal ? `${c.bold}${ch}${c.reset}${c.dim}` : ch)).join("/");
249
206
  return new Promise((resolve) => {
250
207
  rl.question(` ${c.cyan}?${c.reset} ${question} ${c.dim}(${choiceStr})${c.reset} `, (answer) => {
251
208
  const val = answer.trim().toLowerCase() || defaultVal;
@@ -253,55 +210,27 @@ function createPrompter() {
253
210
  });
254
211
  });
255
212
  },
256
- close() { rl.close(); },
213
+ close() {
214
+ rl.close();
215
+ },
257
216
  };
258
217
  }
259
218
  // --- init command ---
260
- // Common timezone offsets from UTC (standard time)
261
- const TIMEZONE_OFFSETS = {
262
- "us/eastern": -5, "us/central": -6, "us/mountain": -7, "us/pacific": -8,
263
- "europe/london": 0, "europe/berlin": 1, "europe/paris": 1,
264
- "asia/tokyo": 9, "asia/shanghai": 8, "asia/kolkata": 5,
265
- "australia/sydney": 11,
266
- };
267
- function localHourToUtc(localHour, utcOffset) {
268
- return ((localHour - utcOffset) % 24 + 24) % 24;
269
- }
270
219
  async function initProject() {
271
220
  const p = createPrompter();
272
221
  console.log(`\n ${c.bold}${c.magenta}Bang On It! init${c.reset}\n`);
273
222
  const testplans = await p.ask("Test plans directory", "testplans");
274
- const apiKey = await p.ask("Anthropic API key (empty to use env var)", "");
275
223
  const recordingsDir = await p.ask("Recordings directory", "recordings");
276
- const s3Bucket = await p.ask("S3 bucket for recordings (empty to skip)", "");
277
- let s3Endpoint = "";
278
- let s3Region = "";
279
- let s3Prefix = "";
280
- if (s3Bucket) {
281
- s3Endpoint = await p.ask("S3 endpoint (empty for AWS, or e.g. nyc3.digitaloceanspaces.com)", "");
282
- s3Region = await p.ask("S3 region", "us-east-1");
283
- s3Prefix = await p.ask("S3 prefix", "bangonit");
284
- }
285
224
  // --- Config file ---
286
- let toml = `testplans = "${testplans}"\n`;
287
- toml += `recordings_dir = "${recordingsDir}"\n`;
288
- if (apiKey) {
289
- toml += `anthropic_api_key = "${apiKey}"\n`;
290
- }
291
- else {
292
- toml += `# anthropic_api_key = "\${ANTHROPIC_API_KEY}"\n`;
293
- }
294
- if (s3Bucket) {
295
- toml += `\n[s3]\nbucket = "${s3Bucket}"\n`;
296
- if (s3Endpoint)
297
- toml += `endpoint = "${s3Endpoint}"\n`;
298
- if (s3Region && s3Region !== "us-east-1")
299
- toml += `region = "${s3Region}"\n`;
300
- if (s3Prefix && s3Prefix !== "bangonit")
301
- toml += `prefix = "${s3Prefix}"\n`;
302
- toml += `# access_key = "\${AWS_ACCESS_KEY_ID}"\n`;
303
- toml += `# secret_key = "\${AWS_SECRET_ACCESS_KEY}"\n`;
304
- }
225
+ const toml = `# Bang On It! configuration
226
+ # Docs: https://bangonit.dev/docs/config
227
+
228
+ testplans = "${testplans}"
229
+ recordings_dir = "${recordingsDir}"
230
+
231
+ # EDIT: Uncomment and set your Anthropic API key, or set ANTHROPIC_API_KEY env var
232
+ # anthropic_api_key = "sk-ant-..."
233
+ `;
305
234
  const bangDir = path.join(process.cwd(), ".bangonit");
306
235
  fs.mkdirSync(bangDir, { recursive: true });
307
236
  const configOutPath = path.join(bangDir, "config.toml");
@@ -431,64 +360,167 @@ Create new Bang On It! test plan files.
431
360
  console.log(` ${c.green}Created${c.reset} .claude/skills/create-test/SKILL.md`);
432
361
  // --- CI setup ---
433
362
  console.log("");
434
- const setupCi = await p.askChoice("Set up GitHub Actions CI?", ["y", "n"], "y");
363
+ const setupCi = await p.askChoice("Set up GitHub Actions?", ["y", "n"], "y");
435
364
  if (setupCi === "y") {
436
- console.log("");
437
- const baseImage = await p.ask("GitHub Actions runner", "ubuntu-latest");
438
- const nodeVersion = await p.ask("Node.js version", "20");
439
- const setupCommand = await p.ask("Setup command", "npm install && npm run build");
440
- const startCommand = await p.ask("Command to start your web server", "npm start &");
441
- const waitUrl = await p.ask("URL to wait for before testing", "http://localhost:3000");
442
- const timeout = await p.ask("Test timeout in seconds", "300");
443
- const tzNames = Object.keys(TIMEZONE_OFFSETS).join(", ");
444
- const tz = await p.ask(`Timezone for full run (${tzNames})`, "us/eastern");
445
- const utcOffset = TIMEZONE_OFFSETS[tz.toLowerCase()] ?? -5;
446
- const fullUtcHour = localHourToUtc(18, utcOffset);
447
- const timeoutMinutes = Math.ceil(parseInt(timeout) / 60) + 5;
448
- const stepsYaml = `
365
+ // Build the comment shell script once, reused by both workflows
366
+ const commentScript = `OUTPUT="\${{ github.workspace }}/bangonit-output.json"
367
+ if [ ! -f "$OUTPUT" ]; then
368
+ BODY="## \${{ github.workflow }} — ❌ Failed\\n\\nNo test output was produced. Check the [workflow logs](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }})."
369
+ else
370
+ STATUS=$(jq -r '.status' "$OUTPUT")
371
+ if [ "$STATUS" = "pass" ]; then
372
+ HEADER="## \${{ github.workflow }} — ✅ Passed"
373
+ else
374
+ HEADER="## \${{ github.workflow }} — ❌ Failed"
375
+ fi
376
+
377
+ TABLE="| Test | Status | Duration | Recording |\\n|------|--------|----------|-----------|"
378
+ for row in $(jq -r '.tests[] | @base64' "$OUTPUT"); do
379
+ NAME=$(echo "$row" | base64 -d | jq -r '.name')
380
+ TEST_STATUS=$(echo "$row" | base64 -d | jq -r '.status')
381
+ DURATION=$(echo "$row" | base64 -d | jq -r '(.duration / 1000 * 10 | round / 10)')
382
+ if [ "$TEST_STATUS" = "pass" ]; then
383
+ EMOJI="✅"
384
+ else
385
+ EMOJI="❌"
386
+ fi
387
+ RECORDING=""
388
+ RECORDING_URL=$(jq -r --arg name "$NAME" '.recordings[]? | select(.name | contains($name)) | .url // empty' "$OUTPUT" 2>/dev/null || true)
389
+ if [ -z "$RECORDING_URL" ] && [ -n "$BANGONIT_S3_BASE_URL" ] && [ -d "recordings" ]; then
390
+ for rdir in recordings/*/index.html; do
391
+ if [ -f "$rdir" ]; then
392
+ RNAME=$(basename "$(dirname "$rdir")")
393
+ RECORDING_URL="$BANGONIT_S3_BASE_URL/$RNAME/index.html"
394
+ break
395
+ fi
396
+ done
397
+ fi
398
+ if [ -n "$RECORDING_URL" ]; then
399
+ RECORDING="[View recording]($RECORDING_URL)"
400
+ fi
401
+ TABLE="$TABLE\\n| $NAME | $EMOJI $TEST_STATUS | \${DURATION}s | $RECORDING |"
402
+ done
403
+
404
+ BODY="$HEADER\\n\\n$TABLE"
405
+ fi`;
406
+ const smokeWorkflow = `# Bang On It! — Smoke Tests
407
+ # Runs on every push to main/master and on pull requests.
408
+ #
409
+ # EDIT: Review the steps below and adjust for your project:
410
+ # - node-version: set to your project's Node.js version
411
+ # - "Setup project": your install/build commands
412
+ # - "Start server": command to start your app in the background
413
+ # - "Wait for server": URL your app serves on
414
+ # - "Run smoke tests": timeout and test plan path
415
+ #
416
+ # REQUIRED SECRET:
417
+ # ANTHROPIC_API_KEY — your Anthropic API key
418
+ # Set at: https://github.com/<owner>/<repo>/settings/secrets/actions
419
+ #
420
+ # OPTIONAL — to upload recordings to S3 and link them in PR comments:
421
+ # 1. Add secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
422
+ # 2. Uncomment the "Upload recordings to S3" step below
423
+ # 3. Set BANGONIT_S3_BASE_URL in the env section
424
+
425
+ name: "Bang On It! Smoke Tests"
426
+
427
+ on:
428
+ push:
429
+ branches: [main, master]
430
+ pull_request:
431
+
432
+ permissions:
433
+ contents: write
434
+ pull-requests: write
435
+
436
+ jobs:
437
+ smoke:
438
+ runs-on: ubuntu-latest
439
+ timeout-minutes: 10
440
+ env:
441
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
442
+ # EDIT: Uncomment for S3 recording uploads
443
+ # AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
444
+ # AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
445
+ # BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
446
+
449
447
  steps:
450
448
  - uses: actions/checkout@v4
451
449
 
452
450
  - uses: actions/setup-node@v4
453
451
  with:
454
- node-version: '${nodeVersion}'
452
+ node-version: '20' # EDIT: your Node.js version
455
453
 
456
454
  - name: Install system dependencies
457
455
  run: |
458
456
  sudo apt-get update
459
- sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2
457
+ sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64
460
458
 
461
459
  - name: Setup project
462
- run: ${setupCommand}
460
+ run: npm install && npm run build # EDIT: your install/build commands
463
461
 
464
462
  - name: Start server
465
- run: ${startCommand}
463
+ run: npm start & # EDIT: command to start your web server
466
464
 
467
465
  - name: Wait for server
468
- run: npx wait-on ${waitUrl} --timeout 30000
466
+ run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
469
467
 
470
468
  - name: Install bangonit
471
- run: npm install -g bangonit`;
472
- const smokeWorkflow = `name: "Bang On It! Smoke Tests"
473
-
474
- on:
475
- push:
476
- branches: [main, master]
477
- pull_request:
469
+ run: npm install -g bangonit
478
470
 
479
- jobs:
480
- smoke:
481
- runs-on: ${baseImage}
482
- timeout-minutes: ${timeoutMinutes}
483
- env:
484
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
485
- ${stepsYaml}
471
+ - name: Comment test starting
472
+ id: start-comment
473
+ if: github.event_name == 'pull_request'
474
+ run: |
475
+ COMMENT_ID=$(gh api repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments \\
476
+ --method POST --field "body=🧪 **Bang On It!** smoke tests starting..." --jq '.id')
477
+ echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
478
+ env:
479
+ GH_TOKEN: \${{ github.token }}
486
480
 
487
481
  - name: Run smoke tests
488
482
  run: |
489
- xvfb-run --auto-servernum boi run ${testplans}/smoke/ \\
490
- --timeout ${timeout} \\
491
- --output bangonit-output.json --record
483
+ xvfb-run --auto-servernum boi run ${testplans}/smoke/*.md \\
484
+ --timeout 300 \\
485
+ --output \${{ github.workspace }}/bangonit-output.json --record
486
+
487
+ # EDIT: Uncomment to upload recordings to S3-compatible storage.
488
+ # Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY secrets.
489
+ # - name: Upload recordings to S3
490
+ # if: always()
491
+ # run: |
492
+ # if [ -d recordings ]; then
493
+ # for dir in recordings/*/; do
494
+ # if [ -d "$dir" ]; then
495
+ # DIRNAME=$(basename "$dir")
496
+ # aws s3 sync "$dir" "s3://MY-BUCKET/bangonit/$DIRNAME/" \\
497
+ # --endpoint-url https://s3.us-east-1.amazonaws.com --no-progress
498
+ # aws s3api list-objects-v2 --bucket MY-BUCKET --prefix "bangonit/$DIRNAME/" \\
499
+ # --endpoint-url https://s3.us-east-1.amazonaws.com --query 'Contents[].Key' --output text | tr '\\t' '\\n' | while read -r key; do
500
+ # aws s3api put-object-acl --bucket MY-BUCKET --key "$key" --acl public-read \\
501
+ # --endpoint-url https://s3.us-east-1.amazonaws.com
502
+ # done
503
+ # fi
504
+ # done
505
+ # fi
506
+
507
+ - name: Comment test results
508
+ if: always()
509
+ run: |
510
+ ${commentScript}
511
+
512
+ if [ "\${{ github.event_name }}" = "pull_request" ] && [ -n "\${{ steps.start-comment.outputs.comment_id }}" ]; then
513
+ printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/issues/comments/\${{ steps.start-comment.outputs.comment_id }} \\
514
+ --method PATCH --field "body=@-"
515
+ elif [ "\${{ github.event_name }}" = "pull_request" ]; then
516
+ printf "%b" "$BODY" | gh pr comment \${{ github.event.pull_request.number }} \\
517
+ --repo \${{ github.repository }} --body-file -
518
+ else
519
+ printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/commits/\${{ github.sha }}/comments \\
520
+ --method POST --field "body=@-"
521
+ fi
522
+ env:
523
+ GH_TOKEN: \${{ github.token }}
492
524
 
493
525
  - name: Upload test results
494
526
  if: always()
@@ -496,30 +528,82 @@ ${stepsYaml}
496
528
  with:
497
529
  name: bangonit-smoke-results
498
530
  path: |
499
- bangonit-output.json
531
+ \${{ github.workspace }}/bangonit-output.json
500
532
  recordings/
501
533
  if-no-files-found: ignore
502
534
  `;
503
- const fullWorkflow = `name: "Bang On It! Full Tests"
535
+ const fullWorkflow = `# Bang On It! Full Tests
536
+ # Runs all test plans daily at 6 PM US/Eastern (23:00 UTC) and on manual trigger.
537
+ # Edit the cron schedule below to change the time or timezone.
538
+ #
539
+ # Same setup as smoke tests — see bangonit-smoke.yml for EDIT instructions.
540
+
541
+ name: "Bang On It! Full Tests"
504
542
 
505
543
  on:
506
544
  schedule:
507
- - cron: '0 ${fullUtcHour} * * *'
545
+ - cron: '0 23 * * *' # EDIT: daily at 6 PM US/Eastern (23:00 UTC)
508
546
  workflow_dispatch:
509
547
 
548
+ permissions:
549
+ contents: write
550
+
510
551
  jobs:
511
552
  full:
512
- runs-on: ${baseImage}
513
- timeout-minutes: ${timeoutMinutes * 3}
553
+ runs-on: ubuntu-latest
554
+ timeout-minutes: 30
514
555
  env:
515
556
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
516
- ${stepsYaml}
557
+ # EDIT: Uncomment for S3 recording uploads (see bangonit-smoke.yml)
558
+ # AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
559
+ # AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
560
+ # BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
561
+
562
+ steps:
563
+ - uses: actions/checkout@v4
564
+
565
+ - uses: actions/setup-node@v4
566
+ with:
567
+ node-version: '20' # EDIT: your Node.js version
568
+
569
+ - name: Install system dependencies
570
+ run: |
571
+ sudo apt-get update
572
+ sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64
573
+
574
+ - name: Setup project
575
+ run: npm install && npm run build # EDIT: your install/build commands
576
+
577
+ - name: Start server
578
+ run: npm start & # EDIT: command to start your web server
579
+
580
+ - name: Wait for server
581
+ run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
582
+
583
+ - name: Install bangonit
584
+ run: npm install -g bangonit
517
585
 
518
586
  - name: Run all tests
519
587
  run: |
520
588
  xvfb-run --auto-servernum boi run \\
521
- --timeout ${timeout} \\
522
- --output bangonit-output.json --record
589
+ --timeout 300 \\
590
+ --output \${{ github.workspace }}/bangonit-output.json --record
591
+
592
+ # EDIT: Uncomment to upload recordings to S3 (see bangonit-smoke.yml for full example)
593
+ # - name: Upload recordings to S3
594
+ # if: always()
595
+ # run: |
596
+ # ...
597
+
598
+ - name: Comment test results on commit
599
+ if: always()
600
+ run: |
601
+ ${commentScript}
602
+
603
+ printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/commits/\${{ github.sha }}/comments \\
604
+ --method POST --field "body=@-"
605
+ env:
606
+ GH_TOKEN: \${{ github.token }}
523
607
 
524
608
  - name: Upload test results
525
609
  if: always()
@@ -527,7 +611,7 @@ ${stepsYaml}
527
611
  with:
528
612
  name: bangonit-full-results
529
613
  path: |
530
- bangonit-output.json
614
+ \${{ github.workspace }}/bangonit-output.json
531
615
  recordings/
532
616
  if-no-files-found: ignore
533
617
  `;
@@ -539,10 +623,11 @@ ${stepsYaml}
539
623
  const fullPath = path.join(outDir, "bangonit-full.yml");
540
624
  fs.writeFileSync(fullPath, fullWorkflow);
541
625
  console.log(` ${c.green}Created${c.reset} ${path.relative(process.cwd(), fullPath)}`);
542
- console.log(`\n ${c.yellow}Required GitHub secret:${c.reset}`);
543
- console.log(` ${c.dim} ANTHROPIC_API_KEY${c.reset} — your Anthropic API key`);
544
- console.log(`\n ${c.dim}Smoke tests run on every push/PR.${c.reset}`);
545
- console.log(` ${c.dim}Full tests run all test plans daily at 6pm ${tz} (${fullUtcHour}:00 UTC).${c.reset}`);
626
+ console.log(`\n ${c.yellow}Next steps:${c.reset}`);
627
+ console.log(` 1. Edit the workflow files — look for ${c.bold}EDIT${c.reset} comments`);
628
+ console.log(` 2. Add your ${c.bold}ANTHROPIC_API_KEY${c.reset} secret to GitHub`);
629
+ console.log(` ${c.dim}https://github.com/<owner>/<repo>/settings/secrets/actions${c.reset}`);
630
+ console.log(` 3. Commit and push to trigger your first run`);
546
631
  }
547
632
  p.close();
548
633
  console.log("");
@@ -559,15 +644,22 @@ async function run(argv, config) {
559
644
  const recordingsDir = config.recordings_dir
560
645
  ? path.resolve(process.cwd(), config.recordings_dir)
561
646
  : path.join(process.cwd(), "recordings");
562
- // Validate test plan files exist before launching anything
647
+ // Validate test plan files exist and expand directories
648
+ const expandedFiles = [];
563
649
  for (const file of argv.files) {
564
650
  const absPath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
565
651
  if (!fs.existsSync(absPath)) {
566
652
  die(`Test plan file not found: ${file}`);
567
653
  }
654
+ if (fs.statSync(absPath).isDirectory()) {
655
+ expandedFiles.push(...findTestPlans(absPath, argv.filter));
656
+ }
657
+ else {
658
+ expandedFiles.push(file);
659
+ }
568
660
  }
569
661
  // Discover test plans if no files/plan specified
570
- const files = [...argv.files];
662
+ const files = [...expandedFiles];
571
663
  if (files.length === 0 && !argv.plan && config.testplans) {
572
664
  const testDirPath = path.resolve(process.cwd(), config.testplans);
573
665
  const plans = findTestPlans(testDirPath, argv.filter);
@@ -597,7 +689,7 @@ async function run(argv, config) {
597
689
  console: argv.console,
598
690
  record: argv.record,
599
691
  retries: argv.retries ?? 0,
600
- output: argv.output || null,
692
+ output: argv.output ? path.resolve(process.cwd(), argv.output) : null,
601
693
  plan: argv.plan || null,
602
694
  prompt: argv.prompt || null,
603
695
  concurrency: argv.concurrency ?? 1,
@@ -642,13 +734,18 @@ async function run(argv, config) {
642
734
  die(`Webapp server crashed (exit code ${code}). Check logs/webapp.log for details.`);
643
735
  }
644
736
  });
645
- // Save terminal state before Electron (which inherits stdin) can modify it
737
+ // Save terminal state before Electron (which inherits stdin) can modify it.
738
+ // stty is not available on Windows.
646
739
  let savedTtyState = null;
647
- try {
648
- savedTtyState = (0, child_process_1.execSync)("stty -g", { stdio: ["inherit", "pipe", "ignore"] }).toString().trim();
649
- }
650
- catch (e) {
651
- console.error("[cli] stty save failed:", e.message);
740
+ if (process.platform !== "win32" && process.stdin.isTTY) {
741
+ try {
742
+ savedTtyState = (0, child_process_1.execSync)("stty -g", { stdio: ["inherit", "pipe", "ignore"] })
743
+ .toString()
744
+ .trim();
745
+ }
746
+ catch (e) {
747
+ console.error("[cli] stty save failed:", e.message);
748
+ }
652
749
  }
653
750
  let electronProc = null;
654
751
  const cleanup = () => {
@@ -676,8 +773,14 @@ async function run(argv, config) {
676
773
  }
677
774
  };
678
775
  process.on("exit", cleanup);
679
- process.on("SIGINT", () => { cleanup(); process.exit(1); });
680
- process.on("SIGTERM", () => { cleanup(); process.exit(1); });
776
+ process.on("SIGINT", () => {
777
+ cleanup();
778
+ process.exit(1);
779
+ });
780
+ process.on("SIGTERM", () => {
781
+ cleanup();
782
+ process.exit(1);
783
+ });
681
784
  await waitForPort(PORT);
682
785
  const electronMain = path.join(DESKTOP_DIR, "dist", "main", "index.js");
683
786
  if (!fs.existsSync(electronMain)) {
@@ -702,7 +805,8 @@ async function run(argv, config) {
702
805
  die("Error: electron not found. Run `npm install` in app/desktopapp.");
703
806
  }
704
807
  }
705
- electronProc = (0, child_process_1.spawn)(electronPath, ["."], {
808
+ const electronExtraArgs = process.env.CI ? ["--no-sandbox", "--disable-gpu"] : [];
809
+ electronProc = (0, child_process_1.spawn)(electronPath, [".", ...electronExtraArgs], {
706
810
  cwd: DESKTOP_DIR,
707
811
  env: {
708
812
  ...process.env,
@@ -710,33 +814,12 @@ async function run(argv, config) {
710
814
  WEBAPP_URL: `http://localhost:${PORT}/app`,
711
815
  NODE_ENV: process.env.NODE_ENV || "production",
712
816
  },
713
- stdio: ["inherit", "inherit", "ignore"],
817
+ stdio: ["inherit", "inherit", "pipe"],
714
818
  });
715
- electronProc.on("exit", async (code) => {
716
- // Upload recordings to S3 if configured
717
- if (config.s3?.bucket && argv.record) {
718
- if (fs.existsSync(recordingsDir)) {
719
- const dirs = fs.readdirSync(recordingsDir).filter((d) => fs.existsSync(path.join(recordingsDir, d, "index.html")));
720
- const s3Prefix = config.s3.prefix || "bangonit";
721
- for (const dir of dirs) {
722
- const localPath = path.join(recordingsDir, dir);
723
- try {
724
- const url = await uploadToS3(localPath, {
725
- bucket: config.s3.bucket,
726
- prefix: `${s3Prefix}/${dir}`,
727
- region: config.s3.region,
728
- endpoint: config.s3.endpoint,
729
- accessKey: config.s3.access_key,
730
- secretKey: config.s3.secret_key,
731
- });
732
- console.log(`\n${c.green}Recording:${c.reset} ${c.cyan}${url}${c.reset}`);
733
- }
734
- catch (err) {
735
- console.error(`\n${c.red}Failed to upload recording:${c.reset} ${err.message}`);
736
- }
737
- }
738
- }
739
- }
819
+ const electronLog = fs.createWriteStream(path.join(LOGS_DIR, "electron.log"));
820
+ electronProc.stderr.pipe(electronLog);
821
+ electronProc.stderr.pipe(process.stderr, { end: false });
822
+ electronProc.on("exit", (code) => {
740
823
  cleanup();
741
824
  process.exit(code ?? 1);
742
825
  });
@@ -746,7 +829,9 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
746
829
  (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
747
830
  .scriptName("boi")
748
831
  .usage("Usage: $0 <command> [options]")
749
- .command("init", "Set up Bang On It! (config, test plans, and optionally CI)", {}, () => { initProject(); })
832
+ .command("init", "Set up Bang On It! (config, test plans, and optionally CI)", {}, () => {
833
+ initProject();
834
+ })
750
835
  .command(["run [files..]", "$0"], "Run test plans (or launch interactive UI)", (y) => y
751
836
  .positional("files", { type: "string", array: true, default: [], describe: "Test plan files" })
752
837
  .option("filter", { alias: "t", type: "string", describe: "Filter test plans by name substring" })
@@ -755,8 +840,16 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
755
840
  .option("prompt", { type: "string", describe: "Additional instructions appended to test plan" })
756
841
  .option("record", { type: "boolean", default: false, describe: "Record session replay" })
757
842
  .option("retries", { type: "number", describe: "Retry failed tests N times (overrides test plan frontmatter)" })
758
- .option("headless", { type: "boolean", default: ciDefaults.headless ?? false, describe: "Run without showing the browser window" })
759
- .option("exit", { type: "boolean", default: ciDefaults.exit ?? false, describe: "Exit immediately after tests complete" })
843
+ .option("headless", {
844
+ type: "boolean",
845
+ default: ciDefaults.headless ?? false,
846
+ describe: "Run without showing the browser window",
847
+ })
848
+ .option("exit", {
849
+ type: "boolean",
850
+ default: ciDefaults.exit ?? false,
851
+ describe: "Exit immediately after tests complete",
852
+ })
760
853
  .option("json", { type: "boolean", default: false, describe: "Stream NDJSON events to stdout" })
761
854
  .option("console", { type: "boolean", default: false, describe: "Forward browser console logs to stdout" })
762
855
  .option("output", { type: "string", describe: "Write JSON results to file" })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bangonit",
3
- "version": "0.4.3",
3
+ "version": "0.5.2",
4
4
  "description": "AI-powered E2E testing tool",
5
5
  "bin": {
6
6
  "bangonit": "bin/src/cli/bangonit.js",
@@ -14,7 +14,16 @@
14
14
  "build:webapp": "cd app/webapp && npm run build && cp -r .next/static .next/standalone/app/webapp/.next/static",
15
15
  "build:electron": "cd app/desktopapp && npx tsc",
16
16
  "build:replay": "cd app/replay && npx vite build",
17
- "test": "node --test test/*.test.ts"
17
+ "test": "node --test test/*.test.ts",
18
+ "format": "prettier --write .",
19
+ "format:check": "prettier --check .",
20
+ "lint": "eslint .",
21
+ "lint:fix": "eslint --fix .",
22
+ "typecheck": "concurrently npm:typecheck:cli npm:typecheck:webapp npm:typecheck:desktop npm:typecheck:replay",
23
+ "typecheck:cli": "tsc --noEmit -p tsconfig.cli.json",
24
+ "typecheck:webapp": "cd app/webapp && npx tsc --noEmit",
25
+ "typecheck:desktop": "cd app/desktopapp && npx tsc --noEmit",
26
+ "typecheck:replay": "cd app/replay && npx tsc --noEmit"
18
27
  },
19
28
  "workspaces": [
20
29
  "app/*"
@@ -52,16 +61,19 @@
52
61
  "@iarna/toml": "^2.2.5",
53
62
  "dotenv": "^17.3.1",
54
63
  "electron": "^40.8.0",
55
- "electron-store": "^8.1.0",
56
64
  "is-ci": "^4.1.0",
57
- "minio": "^8.0.7",
58
65
  "yargs": "^18.0.0",
59
66
  "zod": "^3.25.0"
60
67
  },
61
68
  "devDependencies": {
69
+ "@eslint/js": "^9.39.4",
62
70
  "@types/is-ci": "^3.0.4",
63
71
  "@types/yargs": "^17.0.35",
64
72
  "concurrently": "^9.2.1",
65
- "playwright": "^1.58.2"
73
+ "eslint": "^9.39.4",
74
+ "eslint-plugin-react-hooks": "^7.0.1",
75
+ "playwright": "^1.58.2",
76
+ "prettier": "^3.5.3",
77
+ "typescript-eslint": "^8.57.1"
66
78
  }
67
- }
79
+ }