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.
- package/README.md +22 -0
- package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/app-path-routes-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +2 -2
- package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js.nft.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app-paths-manifest.json +2 -2
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/151.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/{124.js → 373.js} +15 -15
- package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
- package/app/webapp/.next/standalone/node_modules/@img/colour/color.cjs +1596 -0
- package/app/webapp/.next/standalone/node_modules/@img/colour/index.cjs +1 -0
- package/app/webapp/.next/standalone/node_modules/@img/colour/package.json +58 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +1 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +42 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/versions.json +30 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.17.3 +0 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +42 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/LICENSE +191 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node +0 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linux-x64/package.json +46 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/LICENSE +191 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64.node +0 -0
- package/app/webapp/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +46 -0
- package/app/webapp/.next/standalone/node_modules/detect-libc/lib/detect-libc.js +313 -0
- package/app/webapp/.next/standalone/node_modules/detect-libc/lib/elf.js +39 -0
- package/app/webapp/.next/standalone/node_modules/detect-libc/lib/filesystem.js +51 -0
- package/app/webapp/.next/standalone/node_modules/detect-libc/lib/process.js +24 -0
- package/app/webapp/.next/standalone/node_modules/detect-libc/package.json +44 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/channel.js +177 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/colour.js +195 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/composite.js +212 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/constructor.js +499 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/index.js +16 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/input.js +809 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/is.js +143 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/libvips.js +207 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/operation.js +1016 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/output.js +1666 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/resize.js +595 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/sharp.js +121 -0
- package/app/webapp/.next/standalone/node_modules/sharp/lib/utility.js +291 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/comparator.js +143 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/range.js +557 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/classes/semver.js +333 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/cmp.js +54 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/coerce.js +62 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare.js +7 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/eq.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/gt.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/gte.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/lt.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/lte.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/neq.js +5 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/parse.js +18 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/functions/satisfies.js +12 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/constants.js +37 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/debug.js +11 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/identifiers.js +29 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/lrucache.js +42 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/parse-options.js +17 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/internal/re.js +223 -0
- package/app/webapp/.next/standalone/node_modules/sharp/node_modules/semver/package.json +78 -0
- package/app/webapp/.next/standalone/node_modules/sharp/package.json +202 -0
- package/app/webapp/.next/standalone/package.json +12 -8
- package/bin/boi.js +22 -0
- package/package.json +12 -8
- package/bin/app/desktopapp/src/shared/args.js +0 -22
- package/bin/src/cli/bangonit.js +0 -1035
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_buildManifest.js +0 -0
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_ssgManifest.js +0 -0
- /package/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_buildManifest.js +0 -0
- /package/app/webapp/.next/static/{Ovp2DYnS7hdkdiH-qvRCj → ZoXPKe-6bpxSAS83EaRH6}/_ssgManifest.js +0 -0
package/bin/src/cli/bangonit.js
DELETED
|
@@ -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();
|