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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/argusqa-os?color=7C3AED)](https://www.npmjs.com/package/argusqa-os)
6
6
  [![MCP Server](https://glama.ai/mcp/servers/ironclawdevs27/Argus/badges/card.svg)](https://glama.ai/mcp/servers/ironclawdevs27/Argus)
7
- [![Harness](https://img.shields.io/badge/harness-641%2F644-4ADE80)](test-harness/)
7
+ [![Harness](https://img.shields.io/badge/harness-679%2F679-4ADE80)](test-harness/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- 31 analysis engines, 138 distinct issue types, zero test-file maintenance:
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 # 61 unit tests — no Chrome required
211
- npm run test:harness # 138-block correctness harness — requires Chrome
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
- 3 permanent test failures (`641/644`) are MCP-layer restrictions not fixable in Argus code:
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/ — 30+ analysis engines (accessibility, security, performance, etc.)
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/init.js — argus init interactive setup wizard
354
- test-harness/ 138-block correctness harness, 62 fixture pages
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). 137 test blocks, 634 hard assertions, 63 detection categories.",
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.6.5",
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
- "scripts": {
40
- "setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
41
- "init": "node src/cli/init.js",
42
- "crawl": "node src/orchestration/crawl-and-report.js",
43
- "compare": "node src/orchestration/env-comparison.js",
44
- "watch": "node src/orchestration/watch-mode.js",
45
- "server": "node src/server/index.js",
46
- "harness": "node test-harness/server.js",
47
- "harness:staging": "PORT=3101 node test-harness/server.js",
48
- "test:harness": "node test-harness/validate.js",
49
- "test:unit": "vitest run test/unit",
50
- "test": "npm run test:unit && npm run test:harness",
51
- "report:html": "node src/utils/html-reporter.js",
52
- "mcp-server": "node src/mcp-server.js"
53
- },
54
- "dependencies": {
55
- "@modelcontextprotocol/sdk": "^1.29.0",
56
- "@opentelemetry/api": "^1.9.1",
57
- "@opentelemetry/sdk-node": "^0.218.0",
58
- "@slack/web-api": "^7.16.0",
59
- "axe-core": "^4.12.0",
60
- "dotenv": "^17.4.2",
61
- "express": "^5.2.1",
62
- "pino": "^10.3.1",
63
- "pino-pretty": "^13.1.3",
64
- "pixelmatch": "^7.2.0",
65
- "pngjs": "^7.0.0",
66
- "zod": "^4.4.3"
67
- },
68
- "devDependencies": {
69
- "vitest": "^4.1.8"
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
+ }
@@ -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']) which returns structured data, not text.
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
+ }
@@ -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 { parsePrUrl, fetchPrFiles, mapFilesToRoutes } from '../utils/pr-diff-analyzer.js';
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 (!r.path.startsWith('/')) {
254
- console.log(`::warning::Route path "${r.path}" has no leading slash — normalizing to "/${r.path}". Update argus.routes.json to use a leading slash.`);
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
- console.error(`::error::Argus PR Validator: ${summary.critical} critical finding(s) found. Merge blocked (block-on=${blockOn}).`);
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