@openqa/cli 2.0.0 → 2.1.1

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 CHANGED
@@ -50,12 +50,30 @@ OpenQA is a **truly autonomous** QA testing agent that thinks, codes, and execut
50
50
 
51
51
  ## 🚀 Quick Start
52
52
 
53
- ### One-Line Installation
53
+ ### Development (Local)
54
54
 
55
55
  ```bash
56
+ # One-line installation
56
57
  curl -fsSL https://openqa.orkajs.com/install.sh | bash
58
+
59
+ # Or via npm
60
+ npx @openqa/cli start
61
+ ```
62
+
63
+ ### Production Deployment
64
+
65
+ ```bash
66
+ # Interactive production installer
67
+ curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
57
68
  ```
58
69
 
70
+ **Supports:**
71
+ - 🐳 **Docker** (recommended)
72
+ - 🖥️ **VPS/Bare Metal** (Ubuntu/Debian with systemd)
73
+ - ☁️ **Cloud Platforms** (Railway, Render, Fly.io)
74
+
75
+ 📖 **[Full Deployment Guide](./DEPLOYMENT.md)** - Complete production setup instructions
76
+
59
77
  ### Configure Your SaaS (3 lines!)
60
78
 
61
79
  ```bash
@@ -105,9 +123,68 @@ openqa start --daemon
105
123
 
106
124
  Once started, open your browser:
107
125
 
108
- - **DevTools**: http://localhost:3000 - Monitor agent activity in real-time
109
- - **Kanban**: http://localhost:3000/kanban - View and manage QA tickets
110
- - **Config**: http://localhost:3000/config - Configure OpenQA settings
126
+ - **Dashboard**: http://localhost:4242 - Main dashboard with real-time monitoring
127
+ - **Kanban**: http://localhost:4242/kanban - View and manage QA tickets
128
+ - **Config**: http://localhost:4242/config - Configure OpenQA settings
129
+
130
+ ### 🔐 Dashboard Authentication
131
+
132
+ OpenQA includes a secure authentication system to protect your dashboard:
133
+
134
+ #### First-Time Setup
135
+
136
+ On first launch, you'll be redirected to `/setup` to create an admin account:
137
+
138
+ 1. Visit http://localhost:4242
139
+ 2. Create your admin username and password (min 8 characters)
140
+ 3. You'll be automatically logged in
141
+
142
+ #### Login
143
+
144
+ After setup, access the dashboard at http://localhost:4242/login with your credentials.
145
+
146
+ #### User Management (Admin Only)
147
+
148
+ Admins can manage users via the API:
149
+
150
+ ```bash
151
+ # List all users
152
+ curl http://localhost:4242/api/accounts \
153
+ -H "Authorization: Bearer YOUR_TOKEN"
154
+
155
+ # Create a viewer account
156
+ curl -X POST http://localhost:4242/api/accounts \
157
+ -H "Authorization: Bearer YOUR_TOKEN" \
158
+ -H "Content-Type: application/json" \
159
+ -d '{"username": "viewer1", "password": "securepass123", "role": "viewer"}'
160
+
161
+ # Change password
162
+ curl -X POST http://localhost:4242/api/auth/change-password \
163
+ -H "Authorization: Bearer YOUR_TOKEN" \
164
+ -H "Content-Type: application/json" \
165
+ -d '{"currentPassword": "old", "newPassword": "newsecure123"}'
166
+ ```
167
+
168
+ **Roles:**
169
+ - **admin** - Full access (manage users, configure, run tests)
170
+ - **viewer** - Read-only access (view tests, bugs, sessions)
171
+
172
+ **Security Features:**
173
+ - JWT-based authentication with httpOnly cookies
174
+ - Scrypt password hashing
175
+ - Rate limiting on auth endpoints
176
+ - CSRF protection via SameSite cookies
177
+
178
+ #### Disable Authentication (Development Only)
179
+
180
+ For local development, you can disable authentication:
181
+
182
+ ```bash
183
+ export OPENQA_AUTH_DISABLED=true
184
+ openqa start
185
+ ```
186
+
187
+ ⚠️ **Never disable authentication in production!**
111
188
 
112
189
  ### CLI Commands
113
190
 
@@ -330,7 +407,127 @@ curl -X POST http://localhost:3000/api/brain/analyze
330
407
  # }
331
408
  ```
332
409
 
333
- ### Docker Deployment
410
+ ## 🚀 Production Deployment
411
+
412
+ ### Quick Deploy (5 minutes)
413
+
414
+ ```bash
415
+ # Interactive installer - Choose Docker, VPS, or Cloud
416
+ curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
417
+ ```
418
+
419
+ ### Deployment Options
420
+
421
+ | Method | Time | Difficulty | Best For |
422
+ |--------|------|------------|----------|
423
+ | 🐳 **Docker** | 5 min | Easy | VPS, Local servers |
424
+ | 🖥️ **VPS/Systemd** | 15 min | Medium | Full control |
425
+ | ☁️ **Railway** | 3 min | Easiest | Quick deploy |
426
+ | 🎨 **Render** | 2 min | Easiest | Free tier |
427
+ | 🪰 **Fly.io** | 5 min | Easy | Global edge |
428
+
429
+ ### Docker (Recommended)
430
+
431
+ ```bash
432
+ # 1. Clone and configure
433
+ git clone https://github.com/Orka-Community/OpenQA.git
434
+ cd OpenQA
435
+ cp .env.production .env
436
+
437
+ # 2. Edit .env - Add your API keys
438
+ nano .env
439
+ # Required: OPENAI_API_KEY, OPENQA_JWT_SECRET, SAAS_URL
440
+
441
+ # 3. Start with Docker Compose
442
+ docker-compose -f docker-compose.production.yml up -d
443
+
444
+ # 4. Access at http://localhost:4242
445
+ ```
446
+
447
+ **With HTTPS (Nginx):**
448
+ ```bash
449
+ # Update nginx.conf with your domain
450
+ nano nginx.conf
451
+
452
+ # Get SSL certificate
453
+ sudo certbot certonly --standalone -d your-domain.com
454
+
455
+ # Start with Nginx
456
+ docker-compose -f docker-compose.production.yml --profile with-nginx up -d
457
+ ```
458
+
459
+ ### Cloud Platforms
460
+
461
+ **Railway:**
462
+ ```bash
463
+ railway init && railway up
464
+ # Set env vars in dashboard: OPENAI_API_KEY, OPENQA_JWT_SECRET, SAAS_URL
465
+ ```
466
+
467
+ **Render:**
468
+ - Fork repo → Connect to Render → Auto-deploys with `render.yaml`
469
+
470
+ **Fly.io:**
471
+ ```bash
472
+ flyctl launch
473
+ flyctl secrets set OPENAI_API_KEY=sk-xxx OPENQA_JWT_SECRET=$(openssl rand -hex 32)
474
+ flyctl deploy
475
+ ```
476
+
477
+ ### VPS/Bare Metal
478
+
479
+ ```bash
480
+ # Automated installer
481
+ curl -fsSL https://openqa.orkajs.com/install-production.sh | bash
482
+ # Choose option 2 (VPS/Bare Metal)
483
+ ```
484
+
485
+ **Manual installation:**
486
+ ```bash
487
+ # Install Node.js 20
488
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
489
+ sudo apt install -y nodejs build-essential git
490
+
491
+ # Install OpenQA
492
+ sudo useradd -r -m openqa
493
+ sudo -u openqa git clone https://github.com/Orka-Community/OpenQA.git /opt/openqa
494
+ cd /opt/openqa
495
+ sudo -u openqa npm ci --only=production
496
+ sudo -u openqa npm run build
497
+
498
+ # Configure
499
+ sudo -u openqa cp .env.production .env
500
+ sudo nano /opt/openqa/.env
501
+
502
+ # Install systemd service
503
+ sudo cp openqa.service /etc/systemd/system/
504
+ sudo systemctl enable openqa
505
+ sudo systemctl start openqa
506
+ ```
507
+
508
+ ### 🔒 Security Checklist
509
+
510
+ Before going live:
511
+
512
+ - [ ] Set strong `OPENQA_JWT_SECRET` (generate: `openssl rand -hex 32`)
513
+ - [ ] Use strong admin password (min 12 chars)
514
+ - [ ] Enable HTTPS (SSL certificate)
515
+ - [ ] Never set `OPENQA_AUTH_DISABLED=true` in production
516
+ - [ ] Set `NODE_ENV=production`
517
+ - [ ] Restrict CORS origins
518
+ - [ ] Enable firewall (ports 80, 443 only)
519
+ - [ ] Setup automated backups
520
+
521
+ ### 📚 Deployment Documentation
522
+
523
+ - **[DEPLOYMENT.md](./DEPLOYMENT.md)** - Complete deployment guide
524
+ - **[QUICKSTART-PRODUCTION.md](./QUICKSTART-PRODUCTION.md)** - 5-minute quick start
525
+ - **[Docker Compose](./docker-compose.production.yml)** - Production configuration
526
+ - **[Systemd Service](./openqa.service)** - Service configuration
527
+
528
+ ### Development Deployment
529
+
530
+ For local development only:
334
531
 
335
532
  ```bash
336
533
  docker-compose up -d
@@ -1486,13 +1486,9 @@ var BrowserTools = class {
1486
1486
  {
1487
1487
  name: "navigate_to_page",
1488
1488
  description: "Navigate to a specific URL in the application",
1489
- parameters: {
1490
- type: "object",
1491
- properties: {
1492
- url: { type: "string", description: "The URL to navigate to" }
1493
- },
1494
- required: ["url"]
1495
- },
1489
+ parameters: [
1490
+ { name: "url", type: "string", description: "The URL to navigate to", required: true }
1491
+ ],
1496
1492
  execute: async ({ url }) => {
1497
1493
  if (!this.page) await this.initialize();
1498
1494
  try {
@@ -1505,24 +1501,20 @@ var BrowserTools = class {
1505
1501
  input: url,
1506
1502
  output: `Page title: ${title}`
1507
1503
  });
1508
- return `Successfully navigated to ${url}. Page title: "${title}"`;
1504
+ return { output: `Successfully navigated to ${url}. Page title: "${title}"` };
1509
1505
  } catch (error) {
1510
- return `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`;
1506
+ return { output: `Failed to navigate: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1511
1507
  }
1512
1508
  }
1513
1509
  },
1514
1510
  {
1515
1511
  name: "click_element",
1516
1512
  description: "Click on an element using a CSS selector",
1517
- parameters: {
1518
- type: "object",
1519
- properties: {
1520
- selector: { type: "string", description: "CSS selector of the element to click" }
1521
- },
1522
- required: ["selector"]
1523
- },
1513
+ parameters: [
1514
+ { name: "selector", type: "string", description: "CSS selector of the element to click", required: true }
1515
+ ],
1524
1516
  execute: async ({ selector }) => {
1525
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1517
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1526
1518
  try {
1527
1519
  await this.page.click(selector, { timeout: 5e3 });
1528
1520
  this.db.createAction({
@@ -1531,25 +1523,21 @@ var BrowserTools = class {
1531
1523
  description: `Clicked element: ${selector}`,
1532
1524
  input: selector
1533
1525
  });
1534
- return `Successfully clicked element: ${selector}`;
1526
+ return { output: `Successfully clicked element: ${selector}` };
1535
1527
  } catch (error) {
1536
- return `Failed to click element: ${error instanceof Error ? error.message : String(error)}`;
1528
+ return { output: `Failed to click element: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1537
1529
  }
1538
1530
  }
1539
1531
  },
1540
1532
  {
1541
1533
  name: "fill_input",
1542
1534
  description: "Fill an input field with text",
1543
- parameters: {
1544
- type: "object",
1545
- properties: {
1546
- selector: { type: "string", description: "CSS selector of the input field" },
1547
- text: { type: "string", description: "Text to fill in the input" }
1548
- },
1549
- required: ["selector", "text"]
1550
- },
1535
+ parameters: [
1536
+ { name: "selector", type: "string", description: "CSS selector of the input field", required: true },
1537
+ { name: "text", type: "string", description: "Text to fill in the input", required: true }
1538
+ ],
1551
1539
  execute: async ({ selector, text }) => {
1552
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1540
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1553
1541
  try {
1554
1542
  await this.page.fill(selector, text);
1555
1543
  this.db.createAction({
@@ -1558,24 +1546,20 @@ var BrowserTools = class {
1558
1546
  description: `Filled input ${selector}`,
1559
1547
  input: `${selector} = ${text}`
1560
1548
  });
1561
- return `Successfully filled input ${selector} with text`;
1549
+ return { output: `Successfully filled input ${selector} with text` };
1562
1550
  } catch (error) {
1563
- return `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`;
1551
+ return { output: `Failed to fill input: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1564
1552
  }
1565
1553
  }
1566
1554
  },
1567
1555
  {
1568
1556
  name: "take_screenshot",
1569
1557
  description: "Take a screenshot of the current page for evidence",
1570
- parameters: {
1571
- type: "object",
1572
- properties: {
1573
- name: { type: "string", description: "Name for the screenshot file" }
1574
- },
1575
- required: ["name"]
1576
- },
1558
+ parameters: [
1559
+ { name: "name", type: "string", description: "Name for the screenshot file", required: true }
1560
+ ],
1577
1561
  execute: async ({ name }) => {
1578
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1562
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1579
1563
  try {
1580
1564
  const filename = `${Date.now()}_${name}.png`;
1581
1565
  const path2 = join4(this.screenshotDir, filename);
@@ -1586,38 +1570,32 @@ var BrowserTools = class {
1586
1570
  description: `Screenshot: ${name}`,
1587
1571
  screenshot_path: path2
1588
1572
  });
1589
- return `Screenshot saved: ${path2}`;
1573
+ return { output: `Screenshot saved: ${path2}` };
1590
1574
  } catch (error) {
1591
- return `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`;
1575
+ return { output: `Failed to take screenshot: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1592
1576
  }
1593
1577
  }
1594
1578
  },
1595
1579
  {
1596
1580
  name: "get_page_content",
1597
1581
  description: "Get the text content of the current page",
1598
- parameters: {
1599
- type: "object",
1600
- properties: {}
1601
- },
1582
+ parameters: [],
1602
1583
  execute: async () => {
1603
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1584
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1604
1585
  try {
1605
1586
  const content = await this.page.textContent("body");
1606
- return content?.slice(0, 1e3) || "No content found";
1587
+ return { output: content?.slice(0, 1e3) || "No content found" };
1607
1588
  } catch (error) {
1608
- return `Failed to get content: ${error instanceof Error ? error.message : String(error)}`;
1589
+ return { output: `Failed to get content: ${error instanceof Error ? error.message : String(error)}`, error: error instanceof Error ? error.message : String(error) };
1609
1590
  }
1610
1591
  }
1611
1592
  },
1612
1593
  {
1613
1594
  name: "check_console_errors",
1614
1595
  description: "Check for JavaScript console errors on the page",
1615
- parameters: {
1616
- type: "object",
1617
- properties: {}
1618
- },
1596
+ parameters: [],
1619
1597
  execute: async () => {
1620
- if (!this.page) return "Browser not initialized. Navigate to a page first.";
1598
+ if (!this.page) return { output: "Browser not initialized. Navigate to a page first.", error: "Browser not initialized" };
1621
1599
  const errors = [];
1622
1600
  this.page.on("console", (msg) => {
1623
1601
  if (msg.type() === "error") {
@@ -1626,10 +1604,10 @@ var BrowserTools = class {
1626
1604
  });
1627
1605
  await this.page.waitForTimeout(2e3);
1628
1606
  if (errors.length > 0) {
1629
- return `Found ${errors.length} console errors:
1630
- ${errors.join("\n")}`;
1607
+ return { output: `Found ${errors.length} console errors:
1608
+ ${errors.join("\n")}` };
1631
1609
  }
1632
- return "No console errors detected";
1610
+ return { output: "No console errors detected" };
1633
1611
  }
1634
1612
  }
1635
1613
  ];