@openqa/cli 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,533 @@
1
+ // agent/config/index.ts
2
+ import { config as dotenvConfig } from "dotenv";
3
+
4
+ // database/index.ts
5
+ import { Low } from "lowdb";
6
+ import { JSONFile } from "lowdb/node";
7
+ import { dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { mkdirSync } from "fs";
10
+ var __filename = fileURLToPath(import.meta.url);
11
+ var __dirname = dirname(__filename);
12
+ var OpenQADatabase = class {
13
+ constructor(dbPath = "./data/openqa.json") {
14
+ this.dbPath = dbPath;
15
+ this.initialize();
16
+ }
17
+ db = null;
18
+ initialize() {
19
+ const dir = dirname(this.dbPath);
20
+ mkdirSync(dir, { recursive: true });
21
+ const adapter = new JSONFile(this.dbPath);
22
+ this.db = new Low(adapter, {
23
+ config: {},
24
+ test_sessions: [],
25
+ actions: [],
26
+ bugs: [],
27
+ kanban_tickets: []
28
+ });
29
+ this.db.read();
30
+ if (!this.db.data) {
31
+ this.db.data = {
32
+ config: {},
33
+ test_sessions: [],
34
+ actions: [],
35
+ bugs: [],
36
+ kanban_tickets: []
37
+ };
38
+ this.db.write();
39
+ }
40
+ }
41
+ async ensureInitialized() {
42
+ if (!this.db) {
43
+ this.initialize();
44
+ }
45
+ await this.db.read();
46
+ }
47
+ async getConfig(key) {
48
+ await this.ensureInitialized();
49
+ return this.db.data.config[key] || null;
50
+ }
51
+ async setConfig(key, value) {
52
+ await this.ensureInitialized();
53
+ this.db.data.config[key] = value;
54
+ await this.db.write();
55
+ }
56
+ async getAllConfig() {
57
+ await this.ensureInitialized();
58
+ return this.db.data.config;
59
+ }
60
+ async createSession(id, metadata) {
61
+ await this.ensureInitialized();
62
+ const session = {
63
+ id,
64
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
65
+ status: "running",
66
+ total_actions: 0,
67
+ bugs_found: 0,
68
+ metadata: metadata ? JSON.stringify(metadata) : void 0
69
+ };
70
+ this.db.data.test_sessions.push(session);
71
+ await this.db.write();
72
+ return session;
73
+ }
74
+ async getSession(id) {
75
+ await this.ensureInitialized();
76
+ return this.db.data.test_sessions.find((s) => s.id === id) || null;
77
+ }
78
+ async updateSession(id, updates) {
79
+ await this.ensureInitialized();
80
+ const index = this.db.data.test_sessions.findIndex((s) => s.id === id);
81
+ if (index !== -1) {
82
+ this.db.data.test_sessions[index] = { ...this.db.data.test_sessions[index], ...updates };
83
+ await this.db.write();
84
+ }
85
+ }
86
+ async getRecentSessions(limit = 10) {
87
+ await this.ensureInitialized();
88
+ return this.db.data.test_sessions.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime()).slice(0, limit);
89
+ }
90
+ async createAction(action) {
91
+ await this.ensureInitialized();
92
+ const newAction = {
93
+ id: `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
94
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
95
+ ...action
96
+ };
97
+ this.db.data.actions.push(newAction);
98
+ await this.db.write();
99
+ return newAction;
100
+ }
101
+ async getSessionActions(sessionId) {
102
+ await this.ensureInitialized();
103
+ return this.db.data.actions.filter((a) => a.session_id === sessionId).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
104
+ }
105
+ async createBug(bug) {
106
+ await this.ensureInitialized();
107
+ const newBug = {
108
+ id: `bug_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
109
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
110
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
111
+ ...bug
112
+ };
113
+ this.db.data.bugs.push(newBug);
114
+ await this.db.write();
115
+ return newBug;
116
+ }
117
+ async updateBug(id, updates) {
118
+ await this.ensureInitialized();
119
+ const index = this.db.data.bugs.findIndex((b) => b.id === id);
120
+ if (index !== -1) {
121
+ this.db.data.bugs[index] = {
122
+ ...this.db.data.bugs[index],
123
+ ...updates,
124
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
125
+ };
126
+ await this.db.write();
127
+ }
128
+ }
129
+ async getAllBugs() {
130
+ await this.ensureInitialized();
131
+ return this.db.data.bugs.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
132
+ }
133
+ async getBugsByStatus(status) {
134
+ await this.ensureInitialized();
135
+ return this.db.data.bugs.filter((b) => b.status === status).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
136
+ }
137
+ async createKanbanTicket(ticket) {
138
+ await this.ensureInitialized();
139
+ const newTicket = {
140
+ id: `ticket_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
141
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
142
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
143
+ ...ticket
144
+ };
145
+ this.db.data.kanban_tickets.push(newTicket);
146
+ await this.db.write();
147
+ return newTicket;
148
+ }
149
+ async updateKanbanTicket(id, updates) {
150
+ await this.ensureInitialized();
151
+ const index = this.db.data.kanban_tickets.findIndex((t) => t.id === id);
152
+ if (index !== -1) {
153
+ this.db.data.kanban_tickets[index] = {
154
+ ...this.db.data.kanban_tickets[index],
155
+ ...updates,
156
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
157
+ };
158
+ await this.db.write();
159
+ }
160
+ }
161
+ async getKanbanTickets() {
162
+ await this.ensureInitialized();
163
+ return this.db.data.kanban_tickets.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
164
+ }
165
+ async getKanbanTicketsByColumn(column) {
166
+ await this.ensureInitialized();
167
+ return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
168
+ }
169
+ async close() {
170
+ }
171
+ };
172
+
173
+ // agent/config/index.ts
174
+ dotenvConfig();
175
+ var ConfigManager = class {
176
+ db = null;
177
+ envConfig;
178
+ constructor(dbPath) {
179
+ this.envConfig = this.loadFromEnv();
180
+ }
181
+ loadFromEnv() {
182
+ return {
183
+ llm: {
184
+ provider: process.env.LLM_PROVIDER || "openai",
185
+ apiKey: process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY,
186
+ model: process.env.LLM_MODEL,
187
+ baseUrl: process.env.OLLAMA_BASE_URL
188
+ },
189
+ saas: {
190
+ url: process.env.SAAS_URL || "",
191
+ authType: process.env.SAAS_AUTH_TYPE || "none",
192
+ username: process.env.SAAS_USERNAME,
193
+ password: process.env.SAAS_PASSWORD
194
+ },
195
+ github: process.env.GITHUB_TOKEN ? {
196
+ token: process.env.GITHUB_TOKEN,
197
+ owner: process.env.GITHUB_OWNER || "",
198
+ repo: process.env.GITHUB_REPO || ""
199
+ } : void 0,
200
+ agent: {
201
+ intervalMs: parseInt(process.env.AGENT_INTERVAL_MS || "3600000"),
202
+ maxIterations: parseInt(process.env.AGENT_MAX_ITERATIONS || "20"),
203
+ autoStart: process.env.AGENT_AUTO_START === "true"
204
+ },
205
+ web: {
206
+ port: parseInt(process.env.WEB_PORT || "4242"),
207
+ host: process.env.WEB_HOST || "0.0.0.0"
208
+ },
209
+ database: {
210
+ path: process.env.DB_PATH || "./data/openqa.db"
211
+ },
212
+ notifications: {
213
+ slack: process.env.SLACK_WEBHOOK_URL,
214
+ discord: process.env.DISCORD_WEBHOOK_URL
215
+ }
216
+ };
217
+ }
218
+ getDB() {
219
+ if (!this.db) {
220
+ this.db = new OpenQADatabase("./data/openqa.json");
221
+ }
222
+ return this.db;
223
+ }
224
+ async get(key) {
225
+ const dbValue = await this.getDB().getConfig(key);
226
+ if (dbValue) return dbValue;
227
+ const keys = key.split(".");
228
+ let value = this.envConfig;
229
+ for (const k of keys) {
230
+ value = value?.[k];
231
+ }
232
+ return value?.toString() || null;
233
+ }
234
+ async set(key, value) {
235
+ await this.getDB().setConfig(key, value);
236
+ }
237
+ async getAll() {
238
+ const dbConfig = await this.getDB().getAllConfig();
239
+ const merged = { ...this.envConfig };
240
+ for (const [key, value] of Object.entries(dbConfig)) {
241
+ const keys = key.split(".");
242
+ let obj = merged;
243
+ for (let i = 0; i < keys.length - 1; i++) {
244
+ if (!obj[keys[i]]) obj[keys[i]] = {};
245
+ obj = obj[keys[i]];
246
+ }
247
+ obj[keys[keys.length - 1]] = value;
248
+ }
249
+ return merged;
250
+ }
251
+ async getConfig() {
252
+ return await this.getAll();
253
+ }
254
+ // Synchronous version that only uses env vars (no DB)
255
+ getConfigSync() {
256
+ return this.envConfig;
257
+ }
258
+ };
259
+
260
+ // cli/server.ts
261
+ import express from "express";
262
+ import { WebSocketServer } from "ws";
263
+ import chalk from "chalk";
264
+ async function startWebServer() {
265
+ const config = new ConfigManager();
266
+ const cfg = config.getConfigSync();
267
+ const db = new OpenQADatabase("./data/openqa.json");
268
+ const app = express();
269
+ app.use(express.json());
270
+ const wss = new WebSocketServer({ noServer: true });
271
+ app.get("/api/status", async (req, res) => {
272
+ res.json({
273
+ isRunning: true,
274
+ target: cfg.saas.url || "Not configured"
275
+ });
276
+ });
277
+ app.get("/api/sessions", async (req, res) => {
278
+ const limit = parseInt(req.query.limit) || 10;
279
+ const sessions = await db.getRecentSessions(limit);
280
+ res.json(sessions);
281
+ });
282
+ app.get("/api/sessions/:id/actions", async (req, res) => {
283
+ const actions = await db.getSessionActions(req.params.id);
284
+ res.json(actions);
285
+ });
286
+ app.get("/api/bugs", async (req, res) => {
287
+ const status = req.query.status;
288
+ const bugs = status ? await db.getBugsByStatus(status) : await db.getAllBugs();
289
+ res.json(bugs);
290
+ });
291
+ app.get("/api/kanban/tickets", async (req, res) => {
292
+ const column = req.query.column;
293
+ const tickets = column ? await db.getKanbanTicketsByColumn(column) : await db.getKanbanTickets();
294
+ res.json(tickets);
295
+ });
296
+ app.patch("/api/kanban/tickets/:id", async (req, res) => {
297
+ const { id } = req.params;
298
+ const updates = req.body;
299
+ await db.updateKanbanTicket(id, updates);
300
+ res.json({ success: true });
301
+ });
302
+ app.get("/api/config", (req, res) => {
303
+ res.json(cfg);
304
+ });
305
+ app.post("/api/config", async (req, res) => {
306
+ const { key, value } = req.body;
307
+ await config.set(key, value);
308
+ res.json({ success: true });
309
+ });
310
+ app.get("/", (req, res) => {
311
+ res.send(`
312
+ <!DOCTYPE html>
313
+ <html>
314
+ <head>
315
+ <title>OpenQA - Dashboard</title>
316
+ <style>
317
+ body { font-family: system-ui; max-width: 1200px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
318
+ h1 { color: #38bdf8; }
319
+ .card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
320
+ .status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
321
+ .status.running { background: #10b981; color: white; }
322
+ .status.idle { background: #f59e0b; color: white; }
323
+ a { color: #38bdf8; text-decoration: none; }
324
+ a:hover { text-decoration: underline; }
325
+ nav { margin: 20px 0; }
326
+ nav a { margin-right: 20px; }
327
+ </style>
328
+ </head>
329
+ <body>
330
+ <h1>\u{1F916} OpenQA Dashboard</h1>
331
+ <nav>
332
+ <a href="/">Dashboard</a>
333
+ <a href="/kanban">Kanban</a>
334
+ <a href="/config">Config</a>
335
+ </nav>
336
+ <div class="card">
337
+ <h2>Status</h2>
338
+ <p>Agent: <span class="status idle">Idle</span></p>
339
+ <p>Target: ${cfg.saas.url || "Not configured"}</p>
340
+ <p>Auto-start: ${cfg.agent.autoStart ? "Enabled" : "Disabled"}</p>
341
+ </div>
342
+ <div class="card">
343
+ <h2>Quick Links</h2>
344
+ <ul>
345
+ <li><a href="/kanban">View Kanban Board</a></li>
346
+ <li><a href="/config">Configure OpenQA</a></li>
347
+ </ul>
348
+ </div>
349
+ <div class="card">
350
+ <h2>Getting Started</h2>
351
+ <p>Configure your SaaS application target and start testing:</p>
352
+ <ol>
353
+ <li>Set SAAS_URL environment variable or use the <a href="/config">Config page</a></li>
354
+ <li>Enable auto-start: <code>export AGENT_AUTO_START=true</code></li>
355
+ <li>Restart OpenQA</li>
356
+ </ol>
357
+ </div>
358
+ </body>
359
+ </html>
360
+ `);
361
+ });
362
+ app.get("/kanban", (req, res) => {
363
+ res.send(`
364
+ <!DOCTYPE html>
365
+ <html>
366
+ <head>
367
+ <title>OpenQA - Kanban Board</title>
368
+ <style>
369
+ body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
370
+ h1 { color: #38bdf8; }
371
+ nav { margin: 20px 0; }
372
+ nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
373
+ nav a:hover { text-decoration: underline; }
374
+ .board { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 30px; }
375
+ .column { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 15px; }
376
+ .column h3 { margin-top: 0; color: #38bdf8; }
377
+ .ticket { background: #334155; padding: 12px; margin: 10px 0; border-radius: 6px; border-left: 3px solid #38bdf8; }
378
+ .ticket h4 { margin: 0 0 8px 0; font-size: 14px; }
379
+ .ticket p { margin: 0; font-size: 12px; color: #94a3b8; }
380
+ </style>
381
+ </head>
382
+ <body>
383
+ <h1>\u{1F4CB} Kanban Board</h1>
384
+ <nav>
385
+ <a href="/">Dashboard</a>
386
+ <a href="/kanban">Kanban</a>
387
+ <a href="/config">Config</a>
388
+ </nav>
389
+ <div class="board">
390
+ <div class="column">
391
+ <h3>Backlog</h3>
392
+ <p style="color: #64748b;">No tickets yet</p>
393
+ </div>
394
+ <div class="column">
395
+ <h3>To Do</h3>
396
+ <p style="color: #64748b;">No tickets yet</p>
397
+ </div>
398
+ <div class="column">
399
+ <h3>In Progress</h3>
400
+ <p style="color: #64748b;">No tickets yet</p>
401
+ </div>
402
+ <div class="column">
403
+ <h3>Done</h3>
404
+ <p style="color: #64748b;">No tickets yet</p>
405
+ </div>
406
+ </div>
407
+ <p style="margin-top: 40px; color: #64748b;">Tickets will appear here when the agent starts finding bugs and creating tasks.</p>
408
+ </body>
409
+ </html>
410
+ `);
411
+ });
412
+ app.get("/config", (req, res) => {
413
+ res.send(`
414
+ <!DOCTYPE html>
415
+ <html>
416
+ <head>
417
+ <title>OpenQA - Configuration</title>
418
+ <style>
419
+ body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
420
+ h1 { color: #38bdf8; }
421
+ nav { margin: 20px 0; }
422
+ nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
423
+ nav a:hover { text-decoration: underline; }
424
+ .section { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
425
+ .section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
426
+ .config-item { margin: 15px 0; }
427
+ .config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
428
+ .config-item .value { background: #334155; padding: 8px 12px; border-radius: 4px; font-family: monospace; font-size: 14px; }
429
+ code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
430
+ </style>
431
+ </head>
432
+ <body>
433
+ <h1>\u2699\uFE0F Configuration</h1>
434
+ <nav>
435
+ <a href="/">Dashboard</a>
436
+ <a href="/kanban">Kanban</a>
437
+ <a href="/config">Config</a>
438
+ </nav>
439
+
440
+ <div class="section">
441
+ <h2>SaaS Target</h2>
442
+ <div class="config-item">
443
+ <label>URL</label>
444
+ <div class="value">${cfg.saas.url || "Not configured"}</div>
445
+ </div>
446
+ <div class="config-item">
447
+ <label>Auth Type</label>
448
+ <div class="value">${cfg.saas.authType}</div>
449
+ </div>
450
+ </div>
451
+
452
+ <div class="section">
453
+ <h2>LLM Configuration</h2>
454
+ <div class="config-item">
455
+ <label>Provider</label>
456
+ <div class="value">${cfg.llm.provider}</div>
457
+ </div>
458
+ <div class="config-item">
459
+ <label>Model</label>
460
+ <div class="value">${cfg.llm.model || "default"}</div>
461
+ </div>
462
+ </div>
463
+
464
+ <div class="section">
465
+ <h2>Agent Settings</h2>
466
+ <div class="config-item">
467
+ <label>Auto-start</label>
468
+ <div class="value">${cfg.agent.autoStart ? "Enabled" : "Disabled"}</div>
469
+ </div>
470
+ <div class="config-item">
471
+ <label>Interval</label>
472
+ <div class="value">${cfg.agent.intervalMs}ms</div>
473
+ </div>
474
+ <div class="config-item">
475
+ <label>Max Iterations</label>
476
+ <div class="value">${cfg.agent.maxIterations}</div>
477
+ </div>
478
+ </div>
479
+
480
+ <div class="section">
481
+ <h2>How to Configure</h2>
482
+ <p>Set environment variables before starting OpenQA:</p>
483
+ <pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
484
+ export AGENT_AUTO_START=true
485
+ export LLM_PROVIDER=openai
486
+ export OPENAI_API_KEY="your-key"
487
+
488
+ openqa start</code></pre>
489
+ </div>
490
+ </body>
491
+ </html>
492
+ `);
493
+ });
494
+ const server = app.listen(cfg.web.port, cfg.web.host, () => {
495
+ console.log(chalk.cyan("\n\u{1F4CA} OpenQA Status:"));
496
+ console.log(chalk.white(` Agent: ${cfg.agent.autoStart ? "Auto-start enabled" : "Idle"}`));
497
+ console.log(chalk.white(` Target: ${cfg.saas.url || "Not configured"}`));
498
+ console.log(chalk.white(` Dashboard: http://localhost:${cfg.web.port}`));
499
+ console.log(chalk.white(` Kanban: http://localhost:${cfg.web.port}/kanban`));
500
+ console.log(chalk.white(` Config: http://localhost:${cfg.web.port}/config`));
501
+ console.log(chalk.gray("\nPress Ctrl+C to stop\n"));
502
+ if (!cfg.agent.autoStart) {
503
+ console.log(chalk.yellow("\u{1F4A1} Auto-start disabled. Agent is idle."));
504
+ console.log(chalk.cyan(" Set AGENT_AUTO_START=true to enable autonomous mode\n"));
505
+ }
506
+ });
507
+ server.on("upgrade", (request, socket, head) => {
508
+ wss.handleUpgrade(request, socket, head, (ws) => {
509
+ wss.emit("connection", ws, request);
510
+ });
511
+ });
512
+ wss.on("connection", (ws) => {
513
+ console.log("WebSocket client connected");
514
+ ws.on("close", () => {
515
+ console.log("WebSocket client disconnected");
516
+ });
517
+ });
518
+ process.on("SIGTERM", () => {
519
+ console.log("Received SIGTERM, shutting down gracefully...");
520
+ server.close(() => {
521
+ process.exit(0);
522
+ });
523
+ });
524
+ process.on("SIGINT", () => {
525
+ console.log("\nShutting down gracefully...");
526
+ server.close(() => {
527
+ process.exit(0);
528
+ });
529
+ });
530
+ }
531
+ export {
532
+ startWebServer
533
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openqa/cli",
3
- "version": "1.0.12",
3
+ "version": "1.1.0",
4
4
  "description": "Autonomous QA testing agent powered by Orka.js",
5
5
  "type": "module",
6
6
  "main": "./dist/cli/index.js",