argusqa-os 9.6.5 → 9.7.3
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 +24 -14
- package/glama.json +1 -1
- package/package.json +77 -71
- package/src/adapters/browser.js +2 -1
- package/src/cli/chrome-launcher.js +175 -0
- package/src/cli/doctor.js +133 -0
- package/src/cli/pr-validate.js +24 -7
- package/src/mcp-server.js +3 -3
- package/src/orchestration/orchestrator.js +9 -7
- package/src/orchestration/report-processor.js +33 -1
- package/src/utils/a11y-deep-analyzer.js +1 -1
- package/src/utils/design-fidelity-analyzer.js +1 -1
- package/src/utils/flow-runner.js +16 -2
- package/src/utils/font-analyzer.js +1 -1
- package/src/utils/form-analyzer.js +1 -1
- package/src/utils/har-recorder.js +1 -1
- package/src/utils/issues-analyzer.js +12 -19
- package/src/utils/motion-analyzer.js +1 -1
- package/src/utils/noise-filter.js +159 -0
- package/src/utils/pdf-exporter.js +146 -0
- package/src/utils/pr-diff-analyzer.js +2 -1
- package/src/utils/root-cause-linker.js +169 -0
- package/src/utils/screen-recorder.js +250 -0
- package/src/utils/security-analyzer.js +132 -1
- package/src/utils/theme-analyzer.js +1 -1
- package/src/utils/visual-diff-analyzer.js +1 -1
- package/src/utils/web-vitals-analyzer.js +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/argusqa-os)
|
|
6
6
|
[](https://glama.ai/mcp/servers/ironclawdevs27/Argus)
|
|
7
|
-
[](test-harness/)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
**Argus catches the bugs your test suite misses — visual regressions, API loops, CSS drift, console noise, accessibility failures, and more — and delivers rich reports to Slack (or a local HTML dashboard).**
|
|
@@ -62,7 +62,7 @@ Argus scans your app and either posts findings to Slack or opens a local `report
|
|
|
62
62
|
|
|
63
63
|
## What Argus Catches
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
32 analysis engines, 140 distinct issue types, zero test-file maintenance:
|
|
66
66
|
|
|
67
67
|
| Category | What it detects |
|
|
68
68
|
|---|---|
|
|
@@ -71,7 +71,7 @@ Argus scans your app and either posts findings to Slack or opens a local `report
|
|
|
71
71
|
| **Performance** | LCP > 2500ms, CLS > 0.1, TTFB > 800ms, slow APIs > 1s/3s, payloads > 500KB/2MB, JS bundles > 500KB |
|
|
72
72
|
| **Accessibility** | axe-core (80+ WCAG rules), color-blind simulation, missing ARIA, keyboard focus, heading hierarchy |
|
|
73
73
|
| **SEO** | Missing meta description, OG tags, canonical, viewport, h1 |
|
|
74
|
-
| **Security** | Auth tokens in localStorage/URL, `eval()`, missing CSP/X-Frame-Options, CSP violations |
|
|
74
|
+
| **Security** | Auth tokens in localStorage/URL, `eval()`, missing CSP/X-Frame-Options, CSP violations, missing SRI on external scripts, source map exposure, open redirects, npm CVEs |
|
|
75
75
|
| **CSS** | Cascade overrides, component style leaks, unused rules, React inline style conflicts |
|
|
76
76
|
| **Content** | `null`/`undefined` as visible text, lorem ipsum, broken images, empty data lists |
|
|
77
77
|
| **Responsive** | Horizontal overflow at 375px/768px, touch targets < 44×44px |
|
|
@@ -84,6 +84,13 @@ Argus scans your app and either posts findings to Slack or opens a local `report
|
|
|
84
84
|
| **Network baseline** | New requests, missing requests, status-code regressions vs saved HAR baseline |
|
|
85
85
|
| **Environment diff** | Dev vs staging — screenshot diff, DOM changes, console/network regressions |
|
|
86
86
|
|
|
87
|
+
And every finding is post-processed with:
|
|
88
|
+
|
|
89
|
+
| Post-processor | What it adds |
|
|
90
|
+
|---|---|
|
|
91
|
+
| **Intelligent baseline filtering** | Findings that flip-flop across runs are tagged `noisy` and downgraded to info — pure cross-run heuristics, no API calls (`ARGUS_NOISE_FILTER=0` to disable) |
|
|
92
|
+
| **Root cause linking** | New findings are annotated with the recent git commits and files most likely to have caused them (`ARGUS_ROOT_CAUSE=0` to disable) |
|
|
93
|
+
|
|
87
94
|
> All findings are classified as `critical` / `warning` / `info` and routed to the right Slack channel — or surfaced in the local HTML report. For per-finding severity tables and detection methods, see [REFERENCE.md](REFERENCE.md).
|
|
88
95
|
|
|
89
96
|
---
|
|
@@ -201,14 +208,18 @@ export const routes = [
|
|
|
201
208
|
## CLI Commands
|
|
202
209
|
|
|
203
210
|
```bash
|
|
211
|
+
npm run chrome # Launch Chrome with --remote-debugging-port=9222 (auto-detects binary)
|
|
212
|
+
npm run doctor # Pre-flight check: Chrome reachable, .mcp.json valid, .env has TARGET_DEV_URL
|
|
204
213
|
npm run crawl # Batch audit of all configured routes
|
|
205
214
|
npm run compare # Dev vs staging diff (CSS-only if no staging URL)
|
|
206
215
|
npm run watch # Passive monitor — polls open Chrome tab every 1s
|
|
207
216
|
npm run report:html # Generate reports/report.html from last JSON audit
|
|
217
|
+
npm run report:pdf # Export HTML report to A4 PDF (requires: npm install puppeteer)
|
|
208
218
|
npm run server # Start Slack slash-command server (port 3001)
|
|
209
219
|
npm run init # Interactive setup wizard
|
|
210
|
-
npm run test:unit
|
|
211
|
-
npm run test:harness
|
|
220
|
+
npm run test:unit # 61 unit tests — no Chrome required
|
|
221
|
+
npm run test:harness # 139-block correctness harness — requires Chrome
|
|
222
|
+
npm run test:harness:log # same, but tees full output to harness-results.txt
|
|
212
223
|
```
|
|
213
224
|
|
|
214
225
|
**Watch mode** — live monitoring as you develop:
|
|
@@ -331,12 +342,7 @@ Argus is a **complementary layer**, not a replacement for unit or E2E tests:
|
|
|
331
342
|
|
|
332
343
|
## Known Limitations
|
|
333
344
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
| Tool | Constraint |
|
|
337
|
-
|---|---|
|
|
338
|
-
| `drag` | Uses mouse simulation, not HTML5 DnD — `dragstart`/`dragover`/`drop` events never fire |
|
|
339
|
-
| `list_console_messages({ types: ['issue'] })` | Issues panel returns empty even when violations exist |
|
|
345
|
+
All 679 harness assertions pass (`679/679`) — there are currently no known MCP- or Chrome-layer restrictions. Soft assertions (Lighthouse, performance traces) still require non-headless Chrome and are skipped in headless CI.
|
|
340
346
|
|
|
341
347
|
---
|
|
342
348
|
|
|
@@ -347,11 +353,15 @@ src/
|
|
|
347
353
|
argus.js — single-page audit entry point
|
|
348
354
|
mcp-server.js — 9 MCP tools exposed to Claude / any MCP client
|
|
349
355
|
orchestration/ — crawl loop, Slack/GitHub dispatch, env comparison, watch mode
|
|
350
|
-
utils/ —
|
|
356
|
+
utils/ — 32 analysis engines (accessibility, security, performance, PDF, recording, etc.)
|
|
351
357
|
adapters/browser.js — CdpBrowserAdapter — wraps all chrome-devtools-mcp calls
|
|
352
358
|
config/targets.js — routes, thresholds, auth steps
|
|
353
|
-
cli/
|
|
354
|
-
|
|
359
|
+
cli/
|
|
360
|
+
init.js — argus init interactive setup wizard
|
|
361
|
+
chrome-launcher.js — npm run chrome / argus-chrome — launches Chrome with correct flags
|
|
362
|
+
doctor.js — npm run doctor / argus-doctor — pre-flight checks
|
|
363
|
+
pr-validate.js — headless CI entry point for GitHub Actions
|
|
364
|
+
test-harness/ — 139-block correctness harness, 664 hard assertions, 62 fixture pages
|
|
355
365
|
test/unit/ — 61 Vitest unit tests (no Chrome required)
|
|
356
366
|
landing/ — Product landing page (React 19 + Vite + Tailwind)
|
|
357
367
|
```
|
package/glama.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
|
3
3
|
"name": "argus",
|
|
4
|
-
"description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag).
|
|
4
|
+
"description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). Every finding is post-processed with intelligent baseline filtering (cross-run noise classifier) and root cause linking (recent git commits mapped to new findings). 141 test blocks, 679 hard assertions, 67 detection categories.",
|
|
5
5
|
"maintainers": ["ironclawdevs27"],
|
|
6
6
|
"tools": [
|
|
7
7
|
{
|
package/package.json
CHANGED
|
@@ -1,71 +1,77 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "argusqa-os",
|
|
3
|
-
"version": "9.
|
|
4
|
-
"mcpName": "io.github.ironclawdevs27/argus",
|
|
5
|
-
"description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"mcp",
|
|
8
|
-
"mcp-server",
|
|
9
|
-
"claude",
|
|
10
|
-
"qa",
|
|
11
|
-
"testing",
|
|
12
|
-
"accessibility",
|
|
13
|
-
"automation",
|
|
14
|
-
"chrome-devtools",
|
|
15
|
-
"web-testing"
|
|
16
|
-
],
|
|
17
|
-
"author": "ironclawdevs",
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"homepage": "https://argus-qa.com",
|
|
20
|
-
"repository": {
|
|
21
|
-
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/ironclawdevs27/Argus.git"
|
|
23
|
-
},
|
|
24
|
-
"files": [
|
|
25
|
-
"src/",
|
|
26
|
-
".mcp.json",
|
|
27
|
-
"glama.json"
|
|
28
|
-
],
|
|
29
|
-
"type": "module",
|
|
30
|
-
"engines": {
|
|
31
|
-
"node": ">=20.19.0"
|
|
32
|
-
},
|
|
33
|
-
"bin": {
|
|
34
|
-
"argus": "src/cli/init.js",
|
|
35
|
-
"argus-mcp": "src/mcp-server.js",
|
|
36
|
-
"argusqa-os": "src/mcp-server.js",
|
|
37
|
-
"argus-pr-validate": "src/cli/pr-validate.js"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "argusqa-os",
|
|
3
|
+
"version": "9.7.3",
|
|
4
|
+
"mcpName": "io.github.ironclawdevs27/argus",
|
|
5
|
+
"description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"mcp-server",
|
|
9
|
+
"claude",
|
|
10
|
+
"qa",
|
|
11
|
+
"testing",
|
|
12
|
+
"accessibility",
|
|
13
|
+
"automation",
|
|
14
|
+
"chrome-devtools",
|
|
15
|
+
"web-testing"
|
|
16
|
+
],
|
|
17
|
+
"author": "ironclawdevs",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://argus-qa.com",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ironclawdevs27/Argus.git"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"src/",
|
|
26
|
+
".mcp.json",
|
|
27
|
+
"glama.json"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.19.0"
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"argus": "src/cli/init.js",
|
|
35
|
+
"argus-mcp": "src/mcp-server.js",
|
|
36
|
+
"argusqa-os": "src/mcp-server.js",
|
|
37
|
+
"argus-pr-validate": "src/cli/pr-validate.js",
|
|
38
|
+
"argus-chrome": "src/cli/chrome-launcher.js",
|
|
39
|
+
"argus-doctor": "src/cli/doctor.js"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
|
|
43
|
+
"init": "node src/cli/init.js",
|
|
44
|
+
"chrome": "node src/cli/chrome-launcher.js",
|
|
45
|
+
"doctor": "node src/cli/doctor.js",
|
|
46
|
+
"crawl": "node src/orchestration/crawl-and-report.js",
|
|
47
|
+
"compare": "node src/orchestration/env-comparison.js",
|
|
48
|
+
"watch": "node src/orchestration/watch-mode.js",
|
|
49
|
+
"server": "node src/server/index.js",
|
|
50
|
+
"harness": "node test-harness/server.js",
|
|
51
|
+
"harness:staging": "PORT=3101 node test-harness/server.js",
|
|
52
|
+
"test:harness": "node --env-file=test-harness/.env.harness test-harness/validate.js",
|
|
53
|
+
"test:harness:log": "node test-harness/run-with-log.mjs",
|
|
54
|
+
"test:unit": "vitest run test/unit",
|
|
55
|
+
"test": "npm run test:unit && npm run test:harness",
|
|
56
|
+
"report:html": "node src/utils/html-reporter.js",
|
|
57
|
+
"report:pdf": "node src/utils/pdf-exporter.js",
|
|
58
|
+
"mcp-server": "node src/mcp-server.js"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
62
|
+
"@opentelemetry/api": "^1.9.1",
|
|
63
|
+
"@opentelemetry/sdk-node": "^0.218.0",
|
|
64
|
+
"@slack/web-api": "^7.16.0",
|
|
65
|
+
"axe-core": "^4.12.0",
|
|
66
|
+
"dotenv": "^17.4.2",
|
|
67
|
+
"express": "^5.2.1",
|
|
68
|
+
"pino": "^10.3.1",
|
|
69
|
+
"pino-pretty": "^13.1.3",
|
|
70
|
+
"pixelmatch": "^7.2.0",
|
|
71
|
+
"pngjs": "^7.0.0",
|
|
72
|
+
"zod": "^4.4.3"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"vitest": "^4.1.8"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/adapters/browser.js
CHANGED
|
@@ -82,7 +82,8 @@ export class CdpBrowserAdapter {
|
|
|
82
82
|
/**
|
|
83
83
|
* Raw pass-through to list_console_messages with custom args.
|
|
84
84
|
* Used by issues-analyzer.js for the DevTools Issues panel
|
|
85
|
-
* (types: ['issue'])
|
|
85
|
+
* (types: ['issue']). Like all MCP responses, the result is markdown
|
|
86
|
+
* text ("msgid=N [issue] text") — parse with parseConsoleMsgResponse.
|
|
86
87
|
*/
|
|
87
88
|
listConsoleRaw(args = {}) { return this._mcp.list_console_messages(args); }
|
|
88
89
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Argus Chrome Launcher — npm run chrome / argus-chrome
|
|
4
|
+
*
|
|
5
|
+
* Finds system Chrome / Chromium on Windows, macOS, and Linux,
|
|
6
|
+
* then launches it with the remote debugging port Argus needs.
|
|
7
|
+
*
|
|
8
|
+
* Exported for testing:
|
|
9
|
+
* findChrome() — returns path string or null
|
|
10
|
+
* launchChrome(opts) — returns { chromePath, process }
|
|
11
|
+
*
|
|
12
|
+
* CLI flags:
|
|
13
|
+
* --port=N debugging port (default: 9222)
|
|
14
|
+
* --headless launch headless (default: visible)
|
|
15
|
+
* --url=URL initial URL (default: about:blank)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync, spawn } from 'child_process';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { fileURLToPath } from 'url';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
|
|
26
|
+
const CHROME_PATHS = {
|
|
27
|
+
win32: [
|
|
28
|
+
process.env.LOCALAPPDATA
|
|
29
|
+
? path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'Application', 'chrome.exe')
|
|
30
|
+
: null,
|
|
31
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
32
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
33
|
+
process.env.LOCALAPPDATA
|
|
34
|
+
? path.join(process.env.LOCALAPPDATA, 'Chromium', 'Application', 'chrome.exe')
|
|
35
|
+
: null,
|
|
36
|
+
].filter(Boolean),
|
|
37
|
+
darwin: [
|
|
38
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
39
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
40
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
41
|
+
],
|
|
42
|
+
linux: [
|
|
43
|
+
'/usr/bin/google-chrome',
|
|
44
|
+
'/usr/bin/google-chrome-stable',
|
|
45
|
+
'/usr/bin/chromium-browser',
|
|
46
|
+
'/usr/bin/chromium',
|
|
47
|
+
'/snap/bin/chromium',
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the first Chrome/Chromium binary available on this system.
|
|
53
|
+
* Returns the absolute path string or null if none found.
|
|
54
|
+
*
|
|
55
|
+
* Resolution order:
|
|
56
|
+
* 1. ARGUS_CHROME_PATH env var
|
|
57
|
+
* 2. Known platform-specific paths
|
|
58
|
+
* 3. `which` / `where` fallback
|
|
59
|
+
*
|
|
60
|
+
* @returns {string|null}
|
|
61
|
+
*/
|
|
62
|
+
export function findChrome() {
|
|
63
|
+
if (process.env.ARGUS_CHROME_PATH && fs.existsSync(process.env.ARGUS_CHROME_PATH)) {
|
|
64
|
+
return process.env.ARGUS_CHROME_PATH;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const platform = process.platform;
|
|
68
|
+
const candidates = CHROME_PATHS[platform] ?? CHROME_PATHS.linux;
|
|
69
|
+
for (const p of candidates) {
|
|
70
|
+
if (p && fs.existsSync(p)) return p;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const cmd = platform === 'win32' ? 'where' : 'which';
|
|
75
|
+
const names = platform === 'win32'
|
|
76
|
+
? ['chrome', 'chromium']
|
|
77
|
+
: ['google-chrome', 'google-chrome-stable', 'chromium-browser', 'chromium'];
|
|
78
|
+
for (const name of names) {
|
|
79
|
+
try {
|
|
80
|
+
const found = execFileSync(cmd, [name], {
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
83
|
+
}).trim().split('\n')[0].trim();
|
|
84
|
+
if (found && fs.existsSync(found)) return found;
|
|
85
|
+
} catch { /* binary not in PATH */ }
|
|
86
|
+
}
|
|
87
|
+
} catch { /* which/where not available */ }
|
|
88
|
+
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Launch Chrome with remote debugging enabled and return the spawned process.
|
|
94
|
+
*
|
|
95
|
+
* @param {{ port?: number, headless?: boolean, userDataDir?: string, url?: string }} [options]
|
|
96
|
+
* @returns {{ chromePath: string, process: import('child_process').ChildProcess }}
|
|
97
|
+
* @throws {Error} if Chrome binary cannot be found
|
|
98
|
+
*/
|
|
99
|
+
export function launchChrome(options = {}) {
|
|
100
|
+
const {
|
|
101
|
+
port = 9222,
|
|
102
|
+
headless = false,
|
|
103
|
+
userDataDir = path.join(os.tmpdir(), 'argus-chrome'),
|
|
104
|
+
url = 'about:blank',
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
const chromePath = findChrome();
|
|
108
|
+
if (!chromePath) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'Chrome not found. Install Google Chrome, then retry.\n' +
|
|
111
|
+
' Download: https://www.google.com/chrome/\n' +
|
|
112
|
+
' Or set ARGUS_CHROME_PATH=/path/to/chrome.'
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const flags = [
|
|
117
|
+
`--remote-debugging-port=${port}`,
|
|
118
|
+
`--user-data-dir=${userDataDir}`,
|
|
119
|
+
'--no-first-run',
|
|
120
|
+
'--no-default-browser-check',
|
|
121
|
+
'--disable-background-networking',
|
|
122
|
+
'--disable-sync',
|
|
123
|
+
'--metrics-recording-only',
|
|
124
|
+
'--no-sandbox',
|
|
125
|
+
'--disable-gpu',
|
|
126
|
+
];
|
|
127
|
+
if (headless) flags.push('--headless=new');
|
|
128
|
+
flags.push(url);
|
|
129
|
+
|
|
130
|
+
const child = spawn(chromePath, flags, { stdio: 'ignore', detached: true });
|
|
131
|
+
child.unref();
|
|
132
|
+
return { chromePath, process: child };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseCliArgs() {
|
|
136
|
+
const args = process.argv.slice(2);
|
|
137
|
+
const opts = { port: 9222, headless: false, url: 'about:blank' };
|
|
138
|
+
for (const arg of args) {
|
|
139
|
+
if (arg === '--headless' || arg === '--headless=new') { opts.headless = true; continue; }
|
|
140
|
+
const m = arg.match(/^--(\w[\w-]*)(?:=(.*))?$/);
|
|
141
|
+
if (!m) continue;
|
|
142
|
+
const [, key, val] = m;
|
|
143
|
+
if (key === 'port') { opts.port = parseInt(val, 10) || 9222; continue; }
|
|
144
|
+
if (key === 'url') { opts.url = val ?? opts.url; continue; }
|
|
145
|
+
}
|
|
146
|
+
return opts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (process.argv[1] === __filename) {
|
|
150
|
+
const opts = parseCliArgs();
|
|
151
|
+
const ok = s => process.stdout.write(` ✓ ${s}\n`);
|
|
152
|
+
const fail = s => process.stderr.write(` ✗ ${s}\n`);
|
|
153
|
+
|
|
154
|
+
process.stdout.write('\n');
|
|
155
|
+
process.stdout.write(' ╬ Argus Chrome Launcher\n\n');
|
|
156
|
+
|
|
157
|
+
const chromePath = findChrome();
|
|
158
|
+
if (!chromePath) {
|
|
159
|
+
fail('Chrome not found. Install Google Chrome or set ARGUS_CHROME_PATH.');
|
|
160
|
+
process.stdout.write('\n Download: https://www.google.com/chrome/\n\n');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ok(`Found: ${chromePath}`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const { process: child } = launchChrome(opts);
|
|
168
|
+
ok(`Launched (PID ${child.pid}) — remote debugging on port ${opts.port}`);
|
|
169
|
+
process.stdout.write(`\n CDP endpoint: http://localhost:${opts.port}\n`);
|
|
170
|
+
process.stdout.write(` Run \`npm run doctor\` to verify Argus can reach Chrome.\n\n`);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
fail(err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Argus Doctor — npm run doctor / argus-doctor
|
|
4
|
+
*
|
|
5
|
+
* Pre-flight check before running any Argus audit. Runs three checks:
|
|
6
|
+
* 1. Chrome CDP reachable at localhost:9222 (or ARGUS_CHROME_PORT)
|
|
7
|
+
* 2. .mcp.json exists, is valid JSON, has a chrome-devtools server entry
|
|
8
|
+
* 3. TARGET_DEV_URL is set (in .env or already in process.env)
|
|
9
|
+
*
|
|
10
|
+
* All three check functions are exported as pure async functions so the
|
|
11
|
+
* test harness can unit-test them without a real Chrome instance or disk setup.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check whether Chrome's CDP endpoint is reachable on the given port.
|
|
22
|
+
*
|
|
23
|
+
* @param {number} [port=9222]
|
|
24
|
+
* @returns {Promise<{ ok: boolean, detail: string }>}
|
|
25
|
+
*/
|
|
26
|
+
export async function checkChrome(port = 9222) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`http://localhost:${port}/json/version`, {
|
|
29
|
+
signal: AbortSignal.timeout(3000),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
return { ok: true, detail: data.Browser ?? 'reachable' };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return { ok: false, detail: err.message ?? String(err) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check that .mcp.json exists, is valid JSON, and contains a chrome-devtools
|
|
41
|
+
* MCP server entry in mcpServers.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} [filePath='.mcp.json']
|
|
44
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
45
|
+
*/
|
|
46
|
+
export function checkMcpConfig(filePath = '.mcp.json') {
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
detail: `${path.basename(filePath)} not found — run \`argus init\` or copy .mcp.json from the Argus repo`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { ok: false, detail: `Invalid JSON in ${path.basename(filePath)}: ${err.message}` };
|
|
58
|
+
}
|
|
59
|
+
const servers = parsed?.mcpServers ?? parsed?.servers ?? {};
|
|
60
|
+
const hasCdp = Object.values(servers).some(s => {
|
|
61
|
+
const cmd = String(s.command ?? '');
|
|
62
|
+
const args = Array.isArray(s.args) ? s.args.map(String) : [];
|
|
63
|
+
return cmd.includes('chrome-devtools') || args.some(a => a.includes('chrome-devtools'));
|
|
64
|
+
});
|
|
65
|
+
if (!hasCdp) {
|
|
66
|
+
return { ok: false, detail: 'No chrome-devtools MCP server entry found in mcpServers' };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, detail: `${Object.keys(servers).length} server(s) configured` };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check that TARGET_DEV_URL is set — either already in process.env or present
|
|
73
|
+
* and non-empty in the .env file on disk.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} [envFile='.env']
|
|
76
|
+
* @returns {{ ok: boolean, detail: string }}
|
|
77
|
+
*/
|
|
78
|
+
export function checkEnvKeys(envFile = '.env') {
|
|
79
|
+
if (process.env.TARGET_DEV_URL) {
|
|
80
|
+
return { ok: true, detail: `TARGET_DEV_URL=${process.env.TARGET_DEV_URL}` };
|
|
81
|
+
}
|
|
82
|
+
if (!fs.existsSync(envFile)) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
detail: `.env not found — run \`argus init\` to generate one`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
89
|
+
const match = content.match(/^TARGET_DEV_URL\s*=\s*(.+)$/m);
|
|
90
|
+
if (!match || !match[1].trim()) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
detail: 'TARGET_DEV_URL not set in .env — add TARGET_DEV_URL=http://localhost:3000',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, detail: `TARGET_DEV_URL=${match[1].trim()}` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── CLI entry ─────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const PAD = 42;
|
|
102
|
+
|
|
103
|
+
function row(label, result) {
|
|
104
|
+
const icon = result.ok ? '✓' : '✗';
|
|
105
|
+
const status = result.ok ? 'OK ' : 'FAIL';
|
|
106
|
+
process.stdout.write(` ${icon} ${label.padEnd(PAD)} ${status} ${result.detail}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (process.argv[1] === __filename) {
|
|
110
|
+
const port = parseInt(process.env.ARGUS_CHROME_PORT ?? '9222', 10);
|
|
111
|
+
|
|
112
|
+
process.stdout.write('\n ╬ Argus Doctor — pre-flight check\n\n');
|
|
113
|
+
|
|
114
|
+
const [chromeRes, mcpRes, envRes] = await Promise.all([
|
|
115
|
+
checkChrome(port),
|
|
116
|
+
Promise.resolve(checkMcpConfig()),
|
|
117
|
+
Promise.resolve(checkEnvKeys()),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
row(`Chrome CDP reachable (port ${port})`, chromeRes);
|
|
121
|
+
row('.mcp.json valid + chrome-devtools entry', mcpRes);
|
|
122
|
+
row('TARGET_DEV_URL configured', envRes);
|
|
123
|
+
|
|
124
|
+
const allOk = chromeRes.ok && mcpRes.ok && envRes.ok;
|
|
125
|
+
process.stdout.write('\n');
|
|
126
|
+
if (allOk) {
|
|
127
|
+
process.stdout.write(' All checks passed — run `npm run crawl` to start auditing.\n\n');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} else {
|
|
130
|
+
process.stdout.write(' One or more checks failed — fix the issues above and retry.\n\n');
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/cli/pr-validate.js
CHANGED
|
@@ -26,7 +26,7 @@ import path from 'path';
|
|
|
26
26
|
import { fileURLToPath } from 'url';
|
|
27
27
|
import { createMcpClient } from '../utils/mcp-client.js';
|
|
28
28
|
import { crawlRouteCheap } from '../orchestration/crawl-and-report.js';
|
|
29
|
-
import {
|
|
29
|
+
import { fetchPrFiles, mapFilesToRoutes } from '../utils/pr-diff-analyzer.js';
|
|
30
30
|
|
|
31
31
|
// ── Exported helpers (testable without Chrome) ────────────────────────────────
|
|
32
32
|
|
|
@@ -125,7 +125,7 @@ export function writeStepSummary(markdown) {
|
|
|
125
125
|
|
|
126
126
|
// ── Preflight reachability check ──────────────────────────────────────────────
|
|
127
127
|
|
|
128
|
-
async function checkTargetReachable(url) {
|
|
128
|
+
export async function checkTargetReachable(url) {
|
|
129
129
|
try {
|
|
130
130
|
// fetch throws only on network errors (ECONNREFUSED, ETIMEDOUT, DNS failure).
|
|
131
131
|
// HTTP error status codes (4xx/5xx) still mean the server is up — Argus should
|
|
@@ -137,6 +137,21 @@ async function checkTargetReachable(url) {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Normalize route paths — crawlRouteCheap builds URLs via string concatenation
|
|
142
|
+
* (baseUrl + route.path), so paths without a leading slash produce malformed URLs.
|
|
143
|
+
* @param {Array<{path: string, name?: string}>} routes
|
|
144
|
+
* @returns {Array<{path: string, name?: string}>}
|
|
145
|
+
*/
|
|
146
|
+
export function normalizeRoutePaths(routes) {
|
|
147
|
+
return routes.map(r => {
|
|
148
|
+
if (!r.path.startsWith('/')) {
|
|
149
|
+
return { ...r, path: `/${r.path}` };
|
|
150
|
+
}
|
|
151
|
+
return r;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
// ── Route loader ──────────────────────────────────────────────────────────────
|
|
141
156
|
|
|
142
157
|
async function loadRoutes() {
|
|
@@ -249,10 +264,9 @@ async function main() {
|
|
|
249
264
|
|
|
250
265
|
// Normalize route paths — crawlRouteCheap builds URLs via string concat (baseUrl + route.path)
|
|
251
266
|
// so paths without a leading slash produce malformed URLs like https://example.comlogin
|
|
252
|
-
const normalizedAffected = affected.map(r => {
|
|
253
|
-
if (
|
|
254
|
-
console.log(`::warning::Route path "${
|
|
255
|
-
return { ...r, path: `/${r.path}` };
|
|
267
|
+
const normalizedAffected = normalizeRoutePaths(affected).map((r, i) => {
|
|
268
|
+
if (r.path !== affected[i].path) {
|
|
269
|
+
console.log(`::warning::Route path "${affected[i].path}" has no leading slash — normalizing. Update argus.routes.json to use a leading slash.`);
|
|
256
270
|
}
|
|
257
271
|
return r;
|
|
258
272
|
});
|
|
@@ -329,7 +343,10 @@ async function main() {
|
|
|
329
343
|
console.log(JSON.stringify(result, null, 2));
|
|
330
344
|
|
|
331
345
|
if (blocked) {
|
|
332
|
-
|
|
346
|
+
const blockReason = blockOn === 'warning'
|
|
347
|
+
? `${summary.critical} critical + ${summary.warning} warning finding(s) found`
|
|
348
|
+
: `${summary.critical} critical finding(s) found`;
|
|
349
|
+
console.error(`::error::Argus PR Validator: ${blockReason}. Merge blocked (block-on=${blockOn}).`);
|
|
333
350
|
process.exit(1);
|
|
334
351
|
}
|
|
335
352
|
|