bangonit 0.5.7 → 0.5.9

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 (87) hide show
  1. package/README.md +22 -0
  2. package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
  3. package/app/webapp/.next/standalone/app/webapp/.next/app-path-routes-manifest.json +1 -1
  4. package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +2 -2
  5. package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
  6. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
  7. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
  8. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
  9. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js.nft.json +1 -1
  10. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
  11. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js.nft.json +1 -1
  12. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
  13. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +1 -1
  14. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
  15. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
  16. package/app/webapp/.next/standalone/app/webapp/.next/server/app-paths-manifest.json +2 -2
  17. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/151.js +1 -1
  18. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/{124.js → 373.js} +15 -15
  19. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
  20. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
  21. package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
  22. package/app/webapp/.next/standalone/node_modules/@img/colour/color.cjs +1596 -0
  23. package/app/webapp/.next/standalone/node_modules/@img/colour/index.cjs +1 -0
  24. package/app/webapp/.next/standalone/node_modules/@img/colour/package.json +58 -0
  25. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +1 -0
  26. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  27. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +42 -0
  28. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/versions.json +30 -0
  29. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
  30. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
  31. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
  32. package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
  33. package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/LICENSE +191 -0
  34. package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
  35. package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/package.json +46 -0
  36. package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
  37. package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
  38. package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
  39. package/app/webapp/.next/standalone/node_modules/detect-libc/lib/detect-libc.js +313 -0
  40. package/app/webapp/.next/standalone/node_modules/detect-libc/lib/elf.js +39 -0
  41. package/app/webapp/.next/standalone/node_modules/detect-libc/lib/filesystem.js +51 -0
  42. package/app/webapp/.next/standalone/node_modules/detect-libc/lib/process.js +24 -0
  43. package/app/webapp/.next/standalone/node_modules/detect-libc/package.json +44 -0
  44. package/app/webapp/.next/standalone/node_modules/sharp/lib/channel.js +177 -0
  45. package/app/webapp/.next/standalone/node_modules/sharp/lib/colour.js +195 -0
  46. package/app/webapp/.next/standalone/node_modules/sharp/lib/composite.js +212 -0
  47. package/app/webapp/.next/standalone/node_modules/sharp/lib/constructor.js +499 -0
  48. package/app/webapp/.next/standalone/node_modules/sharp/lib/index.js +16 -0
  49. package/app/webapp/.next/standalone/node_modules/sharp/lib/input.js +809 -0
  50. package/app/webapp/.next/standalone/node_modules/sharp/lib/is.js +143 -0
  51. package/app/webapp/.next/standalone/node_modules/sharp/lib/libvips.js +207 -0
  52. package/app/webapp/.next/standalone/node_modules/sharp/lib/operation.js +1016 -0
  53. package/app/webapp/.next/standalone/node_modules/sharp/lib/output.js +1666 -0
  54. package/app/webapp/.next/standalone/node_modules/sharp/lib/resize.js +595 -0
  55. package/app/webapp/.next/standalone/node_modules/sharp/lib/sharp.js +121 -0
  56. package/app/webapp/.next/standalone/node_modules/sharp/lib/utility.js +291 -0
  57. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/comparator.js +143 -0
  58. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/range.js +557 -0
  59. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/semver.js +333 -0
  60. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/cmp.js +54 -0
  61. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/coerce.js +62 -0
  62. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare.js +7 -0
  63. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/eq.js +5 -0
  64. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/gt.js +5 -0
  65. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/gte.js +5 -0
  66. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/lt.js +5 -0
  67. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/lte.js +5 -0
  68. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/neq.js +5 -0
  69. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/parse.js +18 -0
  70. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/satisfies.js +12 -0
  71. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/constants.js +37 -0
  72. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/debug.js +11 -0
  73. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/identifiers.js +29 -0
  74. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/lrucache.js +42 -0
  75. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/parse-options.js +17 -0
  76. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/re.js +223 -0
  77. package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/package.json +78 -0
  78. package/app/webapp/.next/standalone/node_modules/sharp/package.json +202 -0
  79. package/app/webapp/.next/standalone/package.json +12 -8
  80. package/bin/boi.js +22 -0
  81. package/package.json +12 -8
  82. package/bin/app/desktopapp/src/shared/args.js +0 -22
  83. package/bin/src/cli/bangonit.js +0 -1035
  84. /package/app/webapp/.next/standalone/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_buildManifest.js +0 -0
  85. /package/app/webapp/.next/standalone/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_ssgManifest.js +0 -0
  86. /package/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_buildManifest.js +0 -0
  87. /package/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_ssgManifest.js +0 -0
@@ -1,1035 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- // Bang On It! CLI — starts the webapp and Electron app, forwards all args.
4
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
- if (k2 === undefined) k2 = k;
6
- var desc = Object.getOwnPropertyDescriptor(m, k);
7
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
- desc = { enumerable: true, get: function() { return m[k]; } };
9
- }
10
- Object.defineProperty(o, k2, desc);
11
- }) : (function(o, m, k, k2) {
12
- if (k2 === undefined) k2 = k;
13
- o[k2] = m[k];
14
- }));
15
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
- Object.defineProperty(o, "default", { enumerable: true, value: v });
17
- }) : function(o, v) {
18
- o["default"] = v;
19
- });
20
- var __importStar = (this && this.__importStar) || (function () {
21
- var ownKeys = function(o) {
22
- ownKeys = Object.getOwnPropertyNames || function (o) {
23
- var ar = [];
24
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
- return ar;
26
- };
27
- return ownKeys(o);
28
- };
29
- return function (mod) {
30
- if (mod && mod.__esModule) return mod;
31
- var result = {};
32
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
- __setModuleDefault(result, mod);
34
- return result;
35
- };
36
- })();
37
- var __importDefault = (this && this.__importDefault) || function (mod) {
38
- return (mod && mod.__esModule) ? mod : { "default": mod };
39
- };
40
- Object.defineProperty(exports, "__esModule", { value: true });
41
- const child_process_1 = require("child_process");
42
- const path = __importStar(require("path"));
43
- const fs = __importStar(require("fs"));
44
- const net = __importStar(require("net"));
45
- const readline = __importStar(require("readline"));
46
- const TOML = __importStar(require("@iarna/toml"));
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"));
51
- const dotenv = __importStar(require("dotenv"));
52
- const args_1 = require("../../app/desktopapp/src/shared/args");
53
- const ROOT = path.resolve(__dirname, "..", "..", "..");
54
- const WEBAPP_DIR = path.join(ROOT, "app", "webapp");
55
- const DESKTOP_DIR = path.join(ROOT, "app", "desktopapp");
56
- const LOGS_DIR = path.join(process.cwd(), "logs");
57
- // Colors
58
- const c = {
59
- reset: "\x1b[0m",
60
- bold: "\x1b[1m",
61
- dim: "\x1b[2m",
62
- red: "\x1b[31m",
63
- green: "\x1b[32m",
64
- yellow: "\x1b[33m",
65
- blue: "\x1b[34m",
66
- magenta: "\x1b[35m",
67
- cyan: "\x1b[36m",
68
- };
69
- function die(msg) {
70
- console.error(`${c.red}${msg}${c.reset}`);
71
- process.exit(1);
72
- }
73
- function getFreePort() {
74
- return new Promise((resolve, reject) => {
75
- const server = net.createServer();
76
- server.once("error", reject);
77
- server.listen(0, () => {
78
- const port = server.address().port;
79
- server.close(() => resolve(port));
80
- });
81
- });
82
- }
83
- function isPortInUse(port) {
84
- return new Promise((resolve) => {
85
- const client = net.connect({ port, host: "127.0.0.1" }, () => {
86
- client.destroy();
87
- resolve(true);
88
- });
89
- client.on("error", () => resolve(false));
90
- });
91
- }
92
- async function waitForPort(port, timeoutMs = 30000) {
93
- const start = Date.now();
94
- while (Date.now() - start < timeoutMs) {
95
- const inUse = await isPortInUse(port);
96
- if (inUse)
97
- return;
98
- await new Promise((r) => setTimeout(r, 500));
99
- }
100
- die(`Timed out waiting for port ${port}`);
101
- }
102
- function loadEnv() {
103
- dotenv.config({ quiet: true });
104
- }
105
- // Interpolate ${ENV_VAR} references in string values throughout an object
106
- function interpolateEnv(obj) {
107
- if (typeof obj === "string") {
108
- return obj.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, name) => {
109
- return process.env[name] || "";
110
- });
111
- }
112
- if (Array.isArray(obj))
113
- return obj.map(interpolateEnv);
114
- if (obj && typeof obj === "object") {
115
- const result = {};
116
- for (const [k, v] of Object.entries(obj)) {
117
- result[k] = interpolateEnv(v);
118
- }
119
- return result;
120
- }
121
- return obj;
122
- }
123
- // Walk up from cwd to find .bangonit/ directory, stopping at .git or filesystem root
124
- function findProjectDir() {
125
- let dir = process.cwd();
126
- while (true) {
127
- if (fs.existsSync(path.join(dir, ".bangonit")))
128
- return dir;
129
- if (fs.existsSync(path.join(dir, ".git")))
130
- return null;
131
- const parent = path.dirname(dir);
132
- if (parent === dir)
133
- return null; // filesystem root
134
- dir = parent;
135
- }
136
- }
137
- function loadConfig(configPath) {
138
- if (configPath) {
139
- if (!fs.existsSync(configPath))
140
- die(`Config file not found: ${configPath}`);
141
- try {
142
- const raw = TOML.parse(fs.readFileSync(configPath, "utf-8"));
143
- return interpolateEnv(raw);
144
- }
145
- catch (err) {
146
- die(`Error reading config ${configPath}: ${err.message}`);
147
- }
148
- }
149
- const projectDir = findProjectDir();
150
- if (!projectDir)
151
- return {};
152
- const filePath = path.join(projectDir, ".bangonit", "config.toml");
153
- if (!fs.existsSync(filePath))
154
- return {};
155
- try {
156
- const raw = TOML.parse(fs.readFileSync(filePath, "utf-8"));
157
- return interpolateEnv(raw);
158
- }
159
- catch (err) {
160
- die(`Error reading config ${filePath}: ${err.message}`);
161
- }
162
- }
163
- // --- Test plan discovery ---
164
- function findTestPlans(dir, filter) {
165
- const results = [];
166
- if (!fs.existsSync(dir))
167
- return results;
168
- function walk(d) {
169
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
170
- const full = path.join(d, entry.name);
171
- if (entry.isDirectory()) {
172
- walk(full);
173
- }
174
- else if (entry.name.endsWith(".md")) {
175
- results.push(full);
176
- }
177
- }
178
- }
179
- walk(dir);
180
- if (filter) {
181
- const lower = filter.toLowerCase();
182
- return results.filter((f) => path.basename(f).toLowerCase().includes(lower));
183
- }
184
- return results;
185
- }
186
- function createPrompter() {
187
- let closed = false;
188
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
189
- rl.on("close", () => {
190
- closed = true;
191
- });
192
- return {
193
- ask(question, defaultVal) {
194
- if (closed)
195
- return Promise.resolve(defaultVal || "");
196
- return new Promise((resolve) => {
197
- const defStr = defaultVal ? `${c.dim} [${defaultVal}]${c.reset}` : "";
198
- rl.question(` ${c.cyan}?${c.reset} ${question}${defStr} `, (answer) => {
199
- resolve(answer.trim() || defaultVal || "");
200
- });
201
- });
202
- },
203
- askChoice(question, choices, defaultVal) {
204
- if (closed)
205
- return Promise.resolve(defaultVal);
206
- const choiceStr = choices.map((ch) => (ch === defaultVal ? `${c.bold}${ch}${c.reset}${c.dim}` : ch)).join("/");
207
- return new Promise((resolve) => {
208
- rl.question(` ${c.cyan}?${c.reset} ${question} ${c.dim}(${choiceStr})${c.reset} `, (answer) => {
209
- const val = answer.trim().toLowerCase() || defaultVal;
210
- resolve(choices.includes(val) ? val : defaultVal);
211
- });
212
- });
213
- },
214
- close() {
215
- rl.close();
216
- },
217
- };
218
- }
219
- // --- init command ---
220
- async function initProject() {
221
- const p = createPrompter();
222
- console.log(`\n ${c.bold}${c.magenta}Bang On It! init${c.reset}\n`);
223
- const testplans = await p.ask("Test plans directory", "testplans");
224
- const recordingsDir = await p.ask("Recordings directory", "recordings");
225
- // --- Config file ---
226
- const toml = `# Bang On It! configuration
227
- # Docs: https://bangonit.dev/docs/config
228
-
229
- testplans = "${testplans}"
230
- recordings_dir = "${recordingsDir}"
231
-
232
- # EDIT: Uncomment and set your Anthropic API key, or set ANTHROPIC_API_KEY env var
233
- # anthropic_api_key = "sk-ant-..."
234
- `;
235
- const bangDir = path.join(process.cwd(), ".bangonit");
236
- fs.mkdirSync(bangDir, { recursive: true });
237
- const configOutPath = path.join(bangDir, "config.toml");
238
- fs.writeFileSync(configOutPath, toml);
239
- console.log(`\n ${c.green}Created${c.reset} .bangonit/config.toml`);
240
- // --- System prompt script ---
241
- const systemPromptPath = path.join(bangDir, "system_prompt.sh");
242
- if (!fs.existsSync(systemPromptPath)) {
243
- fs.writeFileSync(systemPromptPath, `#!/bin/bash
244
- # This script is executed before each test run.
245
- # Its stdout becomes the project-level system prompt.
246
- # Environment variables are available for interpolation.
247
-
248
- # Example:
249
- # echo "The app is running on http://localhost:\${DEV_SERVER_PORT}"
250
- `);
251
- fs.chmodSync(systemPromptPath, 0o755);
252
- console.log(` ${c.green}Created${c.reset} .bangonit/system_prompt.sh`);
253
- }
254
- // --- Test plan directories ---
255
- const testplanBase = path.join(process.cwd(), testplans);
256
- const dirs = ["smoke", "acceptance", "regression"];
257
- for (const dir of dirs) {
258
- const dirPath = path.join(testplanBase, dir);
259
- fs.mkdirSync(dirPath, { recursive: true });
260
- const gitkeep = path.join(dirPath, ".gitkeep");
261
- if (!fs.existsSync(gitkeep))
262
- fs.writeFileSync(gitkeep, "");
263
- console.log(` ${c.green}Created${c.reset} ${testplans}/${dir}/`);
264
- }
265
- // --- Claude Code skills ---
266
- const claudeSkillsDir = path.join(process.cwd(), ".claude", "skills");
267
- const testSkillDir = path.join(claudeSkillsDir, "boi-test");
268
- fs.mkdirSync(testSkillDir, { recursive: true });
269
- fs.writeFileSync(path.join(testSkillDir, "SKILL.md"), `---
270
- name: boi-test
271
- description: Run Bang On It! E2E tests locally. Pass test plan files or a filter as $ARGUMENTS (e.g. "testplans/smoke/" or "-t login"). With no arguments, runs all test plans.
272
- tools: Bash, Read
273
- ---
274
-
275
- # Run E2E Tests
276
-
277
- Run Bang On It! end-to-end tests locally.
278
-
279
- **Arguments:** $ARGUMENTS
280
-
281
- ## Instructions
282
-
283
- 1. If $ARGUMENTS is empty, run all test plans:
284
- \`\`\`bash
285
- boi run --record
286
- \`\`\`
287
-
288
- 2. If $ARGUMENTS contains file paths or directories, run those:
289
- \`\`\`bash
290
- boi run $ARGUMENTS --record
291
- \`\`\`
292
-
293
- 3. If $ARGUMENTS contains a filter (e.g. "login", "checkout"), run with filter:
294
- \`\`\`bash
295
- boi run -t $ARGUMENTS --record
296
- \`\`\`
297
-
298
- 4. Wait for tests to complete. Report results and recording paths.
299
-
300
- 5. If tests fail, read the test plan file and the output to diagnose the failure. Suggest whether the test plan needs updating or there's a real bug.
301
- `);
302
- console.log(` ${c.green}Created${c.reset} .claude/skills/boi-test/SKILL.md`);
303
- const createTestSkillDir = path.join(claudeSkillsDir, "boi-create-test");
304
- fs.mkdirSync(createTestSkillDir, { recursive: true });
305
- fs.writeFileSync(path.join(createTestSkillDir, "SKILL.md"), `---
306
- name: boi-create-test
307
- description: Create new Bang On It! test plan(s). Pass a description of what to test as $ARGUMENTS, or omit to auto-generate from git changes.
308
- tools: Bash, Read, Write, Glob, Grep
309
- ---
310
-
311
- # Create Test Plan
312
-
313
- Create new Bang On It! test plan files.
314
-
315
- **What to test:** $ARGUMENTS
316
-
317
- ## Instructions
318
-
319
- ### Step 0: Determine what to test
320
-
321
- - If $ARGUMENTS is provided, use it as the description of what to test.
322
- - If $ARGUMENTS is empty, auto-discover from git changes:
323
- 1. Run \`git log master..HEAD --oneline\` and \`git diff master...HEAD --stat\` to see what changed on this branch.
324
- 2. If no branch divergence, run \`git diff HEAD --stat\` and \`git diff HEAD\` for uncommitted changes.
325
- 3. If still nothing, run \`git log -1 --format="%H %s"\` and \`git show HEAD --stat\` for the latest commit.
326
- 4. Analyze the changes and create test plan(s) covering them. Bug fixes get regression tests, new features get acceptance tests.
327
- 5. Skip changes that are already covered by existing test plans, pure refactors, docs, CI, or dependency updates.
328
-
329
- ### Step 1: Determine which directory the test belongs in
330
- - \`${testplans}/smoke/\` — Quick sanity checks (app loads, critical path works). Keep smoke tests minimal — they run on every commit so they must be fast. Only add here if it tests truly fundamental functionality. Prefer acceptance/ for most tests.
331
- - \`${testplans}/acceptance/\` — Core user journeys and happy paths. This is the default for most new tests.
332
- - \`${testplans}/regression/\` — Bug fixes and edge cases. Use when the description references a bug or issue.
333
-
334
- 2. Read existing test plans in that directory to understand conventions:
335
- \`\`\`bash
336
- ls ${testplans}/smoke/ ${testplans}/acceptance/ ${testplans}/regression/
337
- \`\`\`
338
-
339
- 3. Read the codebase to understand what UI elements and flows are involved. Look at routes, components, and pages relevant to the test.
340
-
341
- 4. Create the test plan file:
342
- - Filename: kebab-case, e.g. \`password-reset.md\`
343
- - Use this format:
344
-
345
- \`\`\`markdown
346
- ---
347
- name: Descriptive test name
348
- retries: 1
349
- ---
350
-
351
- ## Steps
352
- 1. Navigate to the relevant page
353
- 2. Perform the action being tested
354
- 3. Verify the expected outcome
355
- \`\`\`
356
-
357
- 5. Keep steps concise and actionable. Write from the user's perspective — describe what to click, type, and verify. Don't reference CSS selectors or implementation details.
358
-
359
- 6. Output the path to the created file.
360
- `);
361
- console.log(` ${c.green}Created${c.reset} .claude/skills/boi-create-test/SKILL.md`);
362
- // --- CI setup ---
363
- console.log("");
364
- const setupCi = await p.askChoice("Set up GitHub Actions?", ["y", "n"], "y");
365
- if (setupCi === "y") {
366
- const smokeWorkflow = `# Bang On It! — Smoke Tests
367
- # Runs on every push to main/master and on pull requests.
368
- #
369
- # EDIT: Review the steps below and adjust for your project:
370
- # - node-version: set to your project's Node.js version
371
- # - "Setup project": your install/build commands
372
- # - "Start server": command to start your app in the background
373
- # - "Wait for server": URL your app serves on
374
- # - "Run smoke tests": timeout and test plan path
375
- #
376
- # REQUIRED SECRET:
377
- # ANTHROPIC_API_KEY — your Anthropic API key
378
- # Set at: https://github.com/<owner>/<repo>/settings/secrets/actions
379
- #
380
- # OPTIONAL — to upload recordings to S3 and link them in PR comments:
381
- # 1. Add secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
382
- # 2. Uncomment the "Upload recordings to S3" step below
383
- # 3. Set BANGONIT_S3_BASE_URL in the env section
384
-
385
- name: "Bang On It! Smoke Tests"
386
-
387
- on:
388
- push:
389
- branches: [main, master]
390
- pull_request:
391
-
392
- permissions:
393
- contents: write
394
- pull-requests: write
395
-
396
- jobs:
397
- smoke:
398
- runs-on: ubuntu-latest
399
- timeout-minutes: 10
400
- env:
401
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
402
- # EDIT: Uncomment for S3 recording uploads
403
- # AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
404
- # AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
405
- # BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
406
-
407
- steps:
408
- - uses: actions/checkout@v4
409
-
410
- - uses: actions/setup-node@v4
411
- with:
412
- node-version: '24' # EDIT: your Node.js version
413
-
414
- - name: Install system dependencies
415
- run: |
416
- sudo apt-get update
417
- 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
418
-
419
- - name: Setup project
420
- run: npm install && npm run build # EDIT: your install/build commands
421
-
422
- - name: Start server
423
- run: npm start & # EDIT: command to start your web server
424
-
425
- - name: Wait for server
426
- run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
427
-
428
- - name: Install bangonit
429
- run: npm install -g bangonit
430
-
431
- - name: Comment test starting
432
- id: start-comment
433
- if: github.event_name == 'pull_request'
434
- run: boi ci comment-starting --repo \${{ github.repository }} --pr \${{ github.event.pull_request.number }} >> "$GITHUB_OUTPUT"
435
- env:
436
- GH_TOKEN: \${{ github.token }}
437
-
438
- - name: Run smoke tests
439
- run: |
440
- xvfb-run --auto-servernum boi run ${testplans}/smoke/*.md \\
441
- --timeout 300 \\
442
- --output \${{ github.workspace }}/bangonit-output.json --record
443
-
444
- # EDIT: Uncomment to upload recordings to S3-compatible storage.
445
- # Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY secrets.
446
- # - name: Upload recordings to S3
447
- # if: always()
448
- # run: >-
449
- # boi ci upload-recordings
450
- # --bucket MY-BUCKET --prefix bangonit
451
- # --endpoint-url https://s3.us-east-1.amazonaws.com
452
-
453
- - name: Comment test results
454
- if: always()
455
- run: |
456
- ARGS="--repo \${{ github.repository }}"
457
- ARGS="$ARGS --output \${{ github.workspace }}/bangonit-output.json"
458
- ARGS="$ARGS --workflow-name \\"\${{ github.workflow }}\\""
459
- ARGS="$ARGS --sha \${{ github.sha }}"
460
- ARGS="$ARGS --run-url \\"\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }}\\""
461
- if [ -n "\${{ github.event.pull_request.number }}" ]; then
462
- ARGS="$ARGS --pr \${{ github.event.pull_request.number }}"
463
- fi
464
- if [ -n "\${{ steps.start-comment.outputs.comment_id }}" ]; then
465
- ARGS="$ARGS --comment-id \${{ steps.start-comment.outputs.comment_id }}"
466
- fi
467
- eval "boi ci comment-results $ARGS"
468
- env:
469
- GH_TOKEN: \${{ github.token }}
470
-
471
- - name: Upload test results
472
- if: always()
473
- uses: actions/upload-artifact@v4
474
- with:
475
- name: bangonit-smoke-results
476
- path: |
477
- \${{ github.workspace }}/bangonit-output.json
478
- recordings/
479
- if-no-files-found: ignore
480
- `;
481
- const fullWorkflow = `# Bang On It! — Full Tests
482
- # Runs all test plans daily at 6 PM US/Eastern (23:00 UTC) and on manual trigger.
483
- # Edit the cron schedule below to change the time or timezone.
484
- #
485
- # Same setup as smoke tests — see bangonit-smoke.yml for EDIT instructions.
486
-
487
- name: "Bang On It! Full Tests"
488
-
489
- on:
490
- schedule:
491
- - cron: '0 23 * * *' # EDIT: daily at 6 PM US/Eastern (23:00 UTC)
492
- workflow_dispatch:
493
-
494
- permissions:
495
- contents: write
496
-
497
- jobs:
498
- full:
499
- runs-on: ubuntu-latest
500
- timeout-minutes: 30
501
- env:
502
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
503
- # EDIT: Uncomment for S3 recording uploads (see bangonit-smoke.yml)
504
- # AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
505
- # AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
506
- # BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
507
-
508
- steps:
509
- - uses: actions/checkout@v4
510
-
511
- - uses: actions/setup-node@v4
512
- with:
513
- node-version: '24' # EDIT: your Node.js version
514
-
515
- - name: Install system dependencies
516
- run: |
517
- sudo apt-get update
518
- 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
519
-
520
- - name: Setup project
521
- run: npm install && npm run build # EDIT: your install/build commands
522
-
523
- - name: Start server
524
- run: npm start & # EDIT: command to start your web server
525
-
526
- - name: Wait for server
527
- run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
528
-
529
- - name: Install bangonit
530
- run: npm install -g bangonit
531
-
532
- - name: Run all tests
533
- run: |
534
- xvfb-run --auto-servernum boi run \\
535
- --timeout 300 \\
536
- --output \${{ github.workspace }}/bangonit-output.json --record
537
-
538
- # EDIT: Uncomment to upload recordings to S3 (see bangonit-smoke.yml for full example)
539
- # - name: Upload recordings to S3
540
- # if: always()
541
- # run: >-
542
- # boi ci upload-recordings
543
- # --bucket MY-BUCKET --prefix bangonit
544
- # --endpoint-url https://s3.us-east-1.amazonaws.com
545
-
546
- - name: Comment test results on commit
547
- if: always()
548
- run: >-
549
- boi ci comment-results
550
- --repo \${{ github.repository }}
551
- --output \${{ github.workspace }}/bangonit-output.json
552
- --workflow-name "\${{ github.workflow }}"
553
- --sha \${{ github.sha }}
554
- --run-url "\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }}"
555
- env:
556
- GH_TOKEN: \${{ github.token }}
557
-
558
- - name: Upload test results
559
- if: always()
560
- uses: actions/upload-artifact@v4
561
- with:
562
- name: bangonit-full-results
563
- path: |
564
- \${{ github.workspace }}/bangonit-output.json
565
- recordings/
566
- if-no-files-found: ignore
567
- `;
568
- const outDir = path.join(process.cwd(), ".github", "workflows");
569
- fs.mkdirSync(outDir, { recursive: true });
570
- const smokePath = path.join(outDir, "bangonit-smoke.yml");
571
- fs.writeFileSync(smokePath, smokeWorkflow);
572
- console.log(`\n ${c.green}Created${c.reset} ${path.relative(process.cwd(), smokePath)}`);
573
- const fullPath = path.join(outDir, "bangonit-full.yml");
574
- fs.writeFileSync(fullPath, fullWorkflow);
575
- console.log(` ${c.green}Created${c.reset} ${path.relative(process.cwd(), fullPath)}`);
576
- console.log(`\n ${c.yellow}Next steps:${c.reset}`);
577
- console.log(` 1. Edit the workflow files — look for ${c.bold}EDIT${c.reset} comments`);
578
- console.log(` 2. Add your ${c.bold}ANTHROPIC_API_KEY${c.reset} secret to GitHub`);
579
- console.log(` ${c.dim}https://github.com/<owner>/<repo>/settings/secrets/actions${c.reset}`);
580
- console.log(` 3. Commit and push to trigger your first run`);
581
- }
582
- p.close();
583
- console.log("");
584
- }
585
- async function run(argv, config) {
586
- loadEnv();
587
- // Config can provide the API key (supports ${ENV_VAR} interpolation)
588
- if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
589
- process.env.ANTHROPIC_API_KEY = config.anthropic_api_key;
590
- }
591
- if (!process.env.ANTHROPIC_API_KEY) {
592
- die("Error: ANTHROPIC_API_KEY is not set.\nSet it in your environment, .env file, or .bangonit/config.toml.");
593
- }
594
- const recordingsDir = config.recordings_dir
595
- ? path.resolve(process.cwd(), config.recordings_dir)
596
- : path.join(process.cwd(), "recordings");
597
- // Validate test plan files exist and expand directories
598
- const expandedFiles = [];
599
- for (const file of argv.files) {
600
- const absPath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
601
- if (!fs.existsSync(absPath)) {
602
- die(`Test plan file not found: ${file}`);
603
- }
604
- if (fs.statSync(absPath).isDirectory()) {
605
- expandedFiles.push(...findTestPlans(absPath, argv.filter));
606
- }
607
- else {
608
- expandedFiles.push(file);
609
- }
610
- }
611
- // Discover test plans if no files/plan specified
612
- const files = [...expandedFiles];
613
- if (files.length === 0 && !argv.plan && config.testplans) {
614
- const testDirPath = path.resolve(process.cwd(), config.testplans);
615
- const plans = findTestPlans(testDirPath, argv.filter);
616
- if (plans.length > 0) {
617
- files.push(...plans);
618
- }
619
- else if (argv.filter) {
620
- die(`No test plans matching "${argv.filter}" found in ${config.testplans}/`);
621
- }
622
- }
623
- else if (files.length === 0 && !argv.plan && argv.filter) {
624
- die(`--filter requires a testplans directory configured in .bangonit/config.toml`);
625
- }
626
- // Hint when launching interactive UI with no config
627
- if (files.length === 0 && !argv.plan) {
628
- if (!config.testplans) {
629
- console.log(`${c.dim}No test plans specified. Launching interactive UI.${c.reset}`);
630
- console.log(`${c.dim}Tip: Run ${c.reset}boi init${c.dim} to set up a config file, or pass test plan files directly.${c.reset}\n`);
631
- }
632
- }
633
- // Build structured args for Electron (passed via BANGONIT_ARGS env var)
634
- const electronArgs = args_1.electronArgsSchema.parse({
635
- testPlanFiles: files,
636
- headless: argv.headless,
637
- exit: argv.exit,
638
- keepOpen: argv.keepOpen,
639
- json: argv.json,
640
- console: argv.console,
641
- record: argv.record,
642
- retries: argv.retries ?? 0,
643
- output: argv.output ? path.resolve(process.cwd(), argv.output) : null,
644
- plan: argv.plan || null,
645
- prompt: argv.additionalSystemPrompt || null,
646
- concurrency: argv.concurrency ?? 1,
647
- timeout: argv.timeout ?? 0,
648
- cwd: process.cwd(),
649
- recordingsDir: argv.record ? recordingsDir : null,
650
- });
651
- const PORT = await getFreePort();
652
- fs.mkdirSync(LOGS_DIR, { recursive: true });
653
- const standaloneServer = path.join(WEBAPP_DIR, ".next", "standalone", "app", "webapp", "server.js");
654
- const nextDir = path.join(WEBAPP_DIR, ".next");
655
- const isBuilt = fs.existsSync(nextDir);
656
- let webappProc;
657
- if (fs.existsSync(standaloneServer)) {
658
- webappProc = (0, child_process_1.spawn)("node", [standaloneServer], {
659
- cwd: path.dirname(standaloneServer),
660
- env: { ...process.env, NODE_ENV: "production", PORT: String(PORT), HOSTNAME: "0.0.0.0" },
661
- stdio: ["ignore", "pipe", "pipe"],
662
- });
663
- }
664
- else if (isBuilt) {
665
- webappProc = (0, child_process_1.spawn)("npx", ["next", "start", "-p", String(PORT)], {
666
- cwd: WEBAPP_DIR,
667
- env: { ...process.env, NODE_ENV: "production" },
668
- stdio: ["ignore", "pipe", "pipe"],
669
- });
670
- }
671
- else {
672
- webappProc = (0, child_process_1.spawn)("npx", ["next", "dev", "-p", String(PORT)], {
673
- cwd: WEBAPP_DIR,
674
- env: { ...process.env },
675
- stdio: ["ignore", "pipe", "pipe"],
676
- });
677
- }
678
- const webappLog = fs.createWriteStream(path.join(LOGS_DIR, "webapp.log"));
679
- webappProc.stdout.pipe(webappLog);
680
- webappProc.stderr.pipe(webappLog);
681
- let webappCrashed = false;
682
- webappProc.on("exit", (code) => {
683
- if (!webappCrashed && code !== null && code !== 0) {
684
- webappCrashed = true;
685
- die(`Webapp server crashed (exit code ${code}). Check logs/webapp.log for details.`);
686
- }
687
- });
688
- // Save terminal state before Electron (which inherits stdin) can modify it.
689
- // stty is not available on Windows.
690
- let savedTtyState = null;
691
- if (process.platform !== "win32" && process.stdin.isTTY) {
692
- try {
693
- savedTtyState = (0, child_process_1.execSync)("stty -g", { stdio: ["inherit", "pipe", "ignore"] })
694
- .toString()
695
- .trim();
696
- }
697
- catch (e) {
698
- console.error("[cli] stty save failed:", e.message);
699
- }
700
- }
701
- let electronProc = null;
702
- const cleanup = () => {
703
- webappCrashed = true; // suppress crash message during normal shutdown
704
- try {
705
- electronProc?.kill();
706
- }
707
- catch (e) {
708
- console.error("[cli] electronProc.kill failed:", e.message);
709
- }
710
- try {
711
- webappProc.kill();
712
- }
713
- catch (e) {
714
- console.error("[cli] webappProc.kill failed:", e.message);
715
- }
716
- // Restore terminal state — Electron may have changed raw mode, echo, etc.
717
- if (savedTtyState) {
718
- try {
719
- (0, child_process_1.execSync)(`stty ${savedTtyState}`, { stdio: ["inherit", "ignore", "ignore"] });
720
- }
721
- catch (e) {
722
- console.error("[cli] stty restore failed:", e.message);
723
- }
724
- }
725
- };
726
- process.on("exit", cleanup);
727
- process.on("SIGINT", () => {
728
- cleanup();
729
- process.exit(1);
730
- });
731
- process.on("SIGTERM", () => {
732
- cleanup();
733
- process.exit(1);
734
- });
735
- await waitForPort(PORT);
736
- const electronMain = path.join(DESKTOP_DIR, "dist", "main", "index.js");
737
- if (!fs.existsSync(electronMain)) {
738
- try {
739
- (0, child_process_1.execSync)("npx tsc", { cwd: DESKTOP_DIR, stdio: "inherit" });
740
- }
741
- catch {
742
- cleanup();
743
- die("Failed to compile Electron app");
744
- }
745
- }
746
- let electronPath;
747
- try {
748
- electronPath = require(path.join(DESKTOP_DIR, "node_modules", "electron"));
749
- }
750
- catch {
751
- try {
752
- electronPath = require("electron");
753
- }
754
- catch {
755
- cleanup();
756
- die("Error: electron not found. Run `npm install` in app/desktopapp.");
757
- }
758
- }
759
- const electronExtraArgs = process.env.CI ? ["--no-sandbox", "--disable-gpu"] : [];
760
- electronProc = (0, child_process_1.spawn)(electronPath, [".", ...electronExtraArgs], {
761
- cwd: DESKTOP_DIR,
762
- env: {
763
- ...process.env,
764
- [args_1.ELECTRON_ARGS_ENV]: JSON.stringify(electronArgs),
765
- WEBAPP_URL: `http://localhost:${PORT}/app`,
766
- NODE_ENV: process.env.NODE_ENV || "production",
767
- },
768
- stdio: ["inherit", "inherit", "pipe"],
769
- });
770
- const electronLog = fs.createWriteStream(path.join(LOGS_DIR, "electron.log"));
771
- electronProc.stderr.pipe(electronLog);
772
- electronProc.stderr.pipe(process.stderr, { end: false });
773
- electronProc.on("exit", (code) => {
774
- cleanup();
775
- process.exit(code ?? 1);
776
- });
777
- }
778
- // --- ci commands ---
779
- function ghExec(args) {
780
- try {
781
- return (0, child_process_1.execSync)(`gh ${args}`, { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
782
- }
783
- catch (err) {
784
- const stderr = err.stderr?.toString().trim() || err.message;
785
- die(`gh command failed: ${stderr}`);
786
- }
787
- }
788
- function ciCommentStarting(argv) {
789
- const commentId = ghExec(`api repos/${argv.repo}/issues/${argv.pr}/comments --method POST --field "body=🧪 **Bang On It!** tests starting..." --jq '.id'`);
790
- // Output in GitHub Actions format so the workflow can capture it
791
- process.stdout.write(`comment_id=${commentId}\n`);
792
- }
793
- function ciCommentResults(argv) {
794
- let body;
795
- if (!fs.existsSync(argv.output)) {
796
- const logsLink = argv.runUrl ? ` Check the [workflow logs](${argv.runUrl}).` : "";
797
- body = `## ${argv.workflowName} — ❌ Failed\n\nNo test output was produced.${logsLink}`;
798
- }
799
- else {
800
- const data = JSON.parse(fs.readFileSync(argv.output, "utf-8"));
801
- const passed = data.status === "pass";
802
- const header = `## ${argv.workflowName} — ${passed ? "✅ Passed" : "❌ Failed"}`;
803
- const rows = [];
804
- for (const test of data.tests || []) {
805
- const emoji = test.status === "pass" ? "✅" : "❌";
806
- const duration = (test.duration / 1000).toFixed(1);
807
- let recording = "";
808
- // Check output JSON for recording URLs first
809
- const rec = (data.recordings || []).find((r) => r.name && test.name && r.name.includes(test.name));
810
- if (rec?.url) {
811
- recording = `[View recording](${rec.url})`;
812
- }
813
- else if (argv.s3BaseUrl && fs.existsSync(argv.recordingsDir)) {
814
- // Fall back to constructing URL from local recording dirs
815
- try {
816
- const dirs = fs
817
- .readdirSync(argv.recordingsDir)
818
- .filter((d) => fs.existsSync(path.join(argv.recordingsDir, d, "index.html")));
819
- if (dirs.length > 0) {
820
- recording = `[View recording](${argv.s3BaseUrl}/${dirs[0]}/index.html)`;
821
- }
822
- }
823
- catch {
824
- // ignore
825
- }
826
- }
827
- rows.push(`| ${test.name} | ${emoji} ${test.status} | ${duration}s | ${recording} |`);
828
- }
829
- const table = `| Test | Status | Duration | Recording |\n|------|--------|----------|-----------|${rows.length > 0 ? "\n" + rows.join("\n") : ""}`;
830
- body = `${header}\n\n${table}`;
831
- }
832
- // Post the comment via gh, passing body on stdin to avoid shell escaping issues
833
- if (argv.pr && argv.commentId) {
834
- (0, child_process_1.execSync)(`gh api repos/${argv.repo}/issues/comments/${argv.commentId} --method PATCH --field "body=@-"`, {
835
- input: body,
836
- stdio: ["pipe", "inherit", "inherit"],
837
- });
838
- }
839
- else if (argv.pr) {
840
- (0, child_process_1.execSync)(`gh api repos/${argv.repo}/issues/${argv.pr}/comments --method POST --field "body=@-"`, {
841
- input: body,
842
- stdio: ["pipe", "inherit", "inherit"],
843
- });
844
- }
845
- else if (argv.sha) {
846
- (0, child_process_1.execSync)(`gh api repos/${argv.repo}/commits/${argv.sha}/comments --method POST --field "body=@-"`, {
847
- input: body,
848
- stdio: ["pipe", "inherit", "inherit"],
849
- });
850
- }
851
- else {
852
- // No target — print to stdout
853
- console.log(body);
854
- }
855
- }
856
- async function uploadDirToS3(client, localDir, bucket, prefix) {
857
- const uploaded = [];
858
- const entries = fs.readdirSync(localDir, { withFileTypes: true });
859
- for (const entry of entries) {
860
- const fullPath = path.join(localDir, entry.name);
861
- const objectName = `${prefix}/${entry.name}`;
862
- if (entry.isDirectory()) {
863
- uploaded.push(...(await uploadDirToS3(client, fullPath, bucket, objectName)));
864
- }
865
- else {
866
- await client.fPutObject(bucket, objectName, fullPath, {});
867
- uploaded.push(objectName);
868
- }
869
- }
870
- return uploaded;
871
- }
872
- async function setObjectPublicRead(client, bucket, objectName) {
873
- const c = client;
874
- await new Promise((resolve, reject) => {
875
- c.makeRequest({
876
- method: "PUT",
877
- bucketName: bucket,
878
- objectName,
879
- query: "acl",
880
- headers: { "x-amz-acl": "public-read" },
881
- }, "", [200], "", true, (err) => {
882
- if (err)
883
- reject(err);
884
- else
885
- resolve();
886
- });
887
- });
888
- }
889
- async function ciUploadRecordings(argv) {
890
- if (!fs.existsSync(argv.recordingsDir)) {
891
- console.log("No recordings directory found, skipping upload.");
892
- return;
893
- }
894
- const dirs = fs.readdirSync(argv.recordingsDir).filter((d) => {
895
- const full = path.join(argv.recordingsDir, d);
896
- return fs.statSync(full).isDirectory();
897
- });
898
- if (dirs.length === 0) {
899
- console.log("No recording directories found, skipping upload.");
900
- return;
901
- }
902
- const accessKey = argv.accessKey || process.env.AWS_ACCESS_KEY_ID || "";
903
- const secretKey = argv.secretKey || process.env.AWS_SECRET_ACCESS_KEY || "";
904
- const region = argv.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
905
- let client;
906
- if (argv.endpointUrl) {
907
- const useSSL = !argv.endpointUrl.startsWith("http://");
908
- const endPoint = argv.endpointUrl.replace(/^https?:\/\//, "");
909
- client = new Minio.Client({ endPoint, useSSL, accessKey, secretKey, region });
910
- }
911
- else {
912
- client = new Minio.Client({ endPoint: "s3.amazonaws.com", useSSL: true, accessKey, secretKey, region });
913
- }
914
- for (const dir of dirs) {
915
- const localPath = path.join(argv.recordingsDir, dir);
916
- console.log(`Uploading ${dir}...`);
917
- const uploaded = await uploadDirToS3(client, localPath, argv.bucket, `${argv.prefix}/${dir}`);
918
- for (const key of uploaded) {
919
- await setObjectPublicRead(client, argv.bucket, key);
920
- }
921
- }
922
- console.log(`Uploaded ${dirs.length} recording(s).`);
923
- process.exit(0);
924
- }
925
- // --- main ---
926
- const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
927
- (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
928
- .scriptName("boi")
929
- .usage("Usage: $0 <command> [options]")
930
- .command("init", "Set up Bang On It! (config, test plans, and optionally CI)", {}, () => {
931
- initProject();
932
- })
933
- .command(["run [files..]", "$0"], "Run test plans (or launch interactive UI)", (y) => y
934
- .positional("files", { type: "string", array: true, default: [], describe: "Test plan files" })
935
- .option("filter", { alias: "t", type: "string", describe: "Filter test plans by name substring" })
936
- .option("config", { type: "string", describe: "Path to config file (default: .bangonit/config.toml)" })
937
- .option("plan", { type: "string", describe: "Inline test plan (instead of file)" })
938
- .option("additional-system-prompt", {
939
- type: "string",
940
- describe: "Additional system prompt text appended to test plan",
941
- })
942
- .option("record", { type: "boolean", default: false, describe: "Record session replay" })
943
- .option("retries", { type: "number", describe: "Retry failed tests N times (overrides test plan frontmatter)" })
944
- .option("headless", {
945
- type: "boolean",
946
- default: ciDefaults.headless ?? false,
947
- describe: "Run without showing the browser window",
948
- })
949
- .option("exit", {
950
- type: "boolean",
951
- default: ciDefaults.exit ?? false,
952
- describe: "Exit immediately after tests complete",
953
- })
954
- .option("keep-open", {
955
- type: "boolean",
956
- default: false,
957
- describe: "Keep the browser window open after tests pass (for inspection)",
958
- })
959
- .option("json", { type: "boolean", default: false, describe: "Stream NDJSON events to stdout" })
960
- .option("console", { type: "boolean", default: false, describe: "Forward browser console logs to stdout" })
961
- .option("output", { type: "string", describe: "Write JSON results to file" })
962
- .option("concurrency", { type: "number", describe: "Number of parallel agents (default: 1)" })
963
- .option("timeout", { type: "number", describe: "Test timeout in seconds (0 = none)" }), (argv) => {
964
- const config = loadConfig(argv.config);
965
- run({
966
- files: argv.files,
967
- filter: argv.filter,
968
- plan: argv.plan,
969
- additionalSystemPrompt: argv.additionalSystemPrompt,
970
- record: argv.record,
971
- retries: argv.retries,
972
- headless: argv.headless,
973
- exit: argv.exit,
974
- keepOpen: argv.keepOpen,
975
- json: argv.json,
976
- console: argv.console,
977
- output: argv.output,
978
- concurrency: argv.concurrency,
979
- timeout: argv.timeout,
980
- }, config);
981
- })
982
- .command("ci", "CI helper commands (used by generated GitHub Actions workflows)", (y) => y
983
- .command("comment-starting", "Post a 'tests starting' comment on a PR", (y) => y
984
- .option("repo", { type: "string", demandOption: true, describe: "GitHub repo (owner/repo)" })
985
- .option("pr", { type: "number", demandOption: true, describe: "PR number" }), (argv) => ciCommentStarting({ repo: argv.repo, pr: argv.pr }))
986
- .command("comment-results", "Post test results as a PR or commit comment", (y) => y
987
- .option("repo", { type: "string", demandOption: true, describe: "GitHub repo (owner/repo)" })
988
- .option("output", { type: "string", demandOption: true, describe: "Path to bangonit-output.json" })
989
- .option("workflow-name", { type: "string", demandOption: true, describe: "Workflow name for the header" })
990
- .option("pr", { type: "number", describe: "PR number (for PR comments)" })
991
- .option("comment-id", { type: "string", describe: "Existing comment ID to edit" })
992
- .option("sha", { type: "string", describe: "Commit SHA (for commit comments)" })
993
- .option("run-url", { type: "string", describe: "URL to workflow run logs" })
994
- .option("s3-base-url", {
995
- type: "string",
996
- describe: "Base URL for S3 recording links",
997
- default: process.env.BANGONIT_S3_BASE_URL || "",
998
- })
999
- .option("recordings-dir", { type: "string", describe: "Recordings directory", default: "recordings" }), (argv) => ciCommentResults({
1000
- repo: argv.repo,
1001
- output: argv.output,
1002
- workflowName: argv.workflowName,
1003
- pr: argv.pr,
1004
- commentId: argv.commentId,
1005
- sha: argv.sha,
1006
- runUrl: argv.runUrl,
1007
- s3BaseUrl: argv.s3BaseUrl || undefined,
1008
- recordingsDir: argv.recordingsDir,
1009
- }))
1010
- .command("upload-recordings", "Upload recordings to S3-compatible storage", (y) => y
1011
- .option("bucket", { type: "string", demandOption: true, describe: "S3 bucket name" })
1012
- .option("prefix", { type: "string", demandOption: true, describe: "S3 key prefix" })
1013
- .option("recordings-dir", { type: "string", describe: "Local recordings directory", default: "recordings" })
1014
- .option("endpoint-url", { type: "string", describe: "S3 endpoint URL" })
1015
- .option("access-key", { type: "string", describe: "S3 access key (default: AWS_ACCESS_KEY_ID env)" })
1016
- .option("secret-key", { type: "string", describe: "S3 secret key (default: AWS_SECRET_ACCESS_KEY env)" })
1017
- .option("region", { type: "string", describe: "S3 region (default: us-east-1)" }), (argv) => ciUploadRecordings({
1018
- bucket: argv.bucket,
1019
- prefix: argv.prefix,
1020
- recordingsDir: argv.recordingsDir,
1021
- endpointUrl: argv.endpointUrl,
1022
- accessKey: argv.accessKey,
1023
- secretKey: argv.secretKey,
1024
- region: argv.region,
1025
- }))
1026
- .demandCommand(1, "Specify a ci subcommand: comment-starting, comment-results, upload-recordings"))
1027
- .example("$0 run test.md", "Run a test plan file")
1028
- .example("$0 run --plan 'test login flow'", "Run an inline test plan")
1029
- .example("$0 run -t checkout", "Run test plans matching 'checkout'")
1030
- .example("$0 run", "Launch interactive UI")
1031
- .example("$0 init", "Set up config, test plans, and CI")
1032
- .strict()
1033
- .help()
1034
- .alias("h", "help")
1035
- .parseAsync();