argusqa-os 9.6.6 → 9.7.4
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 +394 -384
- package/glama.json +2 -2
- package/package.json +77 -71
- package/src/adapters/browser.js +11 -3
- package/src/cli/chrome-launcher.js +175 -0
- package/src/cli/doctor.js +133 -0
- package/src/cli/pr-validate.js +25 -6
- package/src/mcp-server.js +27 -9
- package/src/orchestration/orchestrator.js +9 -7
- package/src/orchestration/report-processor.js +33 -1
- package/src/orchestration/watch-mode.js +20 -0
- package/src/utils/a11y-deep-analyzer.js +1 -1
- package/src/utils/contract-validator.js +27 -2
- 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/mcp-parsers.js +20 -0
- 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 +11 -2
- package/src/utils/root-cause-linker.js +175 -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/glama.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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). 142 test blocks, 688 hard assertions, 67 detection categories.",
|
|
5
5
|
"maintainers": ["ironclawdevs27"],
|
|
6
6
|
"tools": [
|
|
7
7
|
{
|
|
8
8
|
"name": "argus_audit",
|
|
9
|
-
"description": "Fast QA audit — JS errors, network failures (4xx/5xx), API frequency loops,
|
|
9
|
+
"description": "Fast QA audit — JS errors, network failures (4xx/5xx), CORS, API frequency loops, slow/blocking third-party requests, API contract violations, SEO violations, security headers, content quality, DevTools Issues panel, and HTTPS enforcement. Returns { findings, summary }. Supports cache: true to skip re-crawl on repeat calls."
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"name": "argus_audit_full",
|
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.4",
|
|
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": "node test-harness/server.js --port=3101 --staging",
|
|
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
|
@@ -52,15 +52,22 @@ export class CdpBrowserAdapter {
|
|
|
52
52
|
resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
|
|
53
53
|
|
|
54
54
|
// ── Network & performance ───────────────────────────────────────────────────
|
|
55
|
-
|
|
55
|
+
// chrome-devtools-mcp expects the wire parameter "reqid" (sending "requestId"
|
|
56
|
+
// is rejected with an Unknown-argument error). Callers still pass the numeric
|
|
57
|
+
// requestId parsed from list_network_requests.
|
|
58
|
+
getNetworkRequest(reqId) { return this._mcp.get_network_request({ reqid: reqId }); }
|
|
56
59
|
lighthouse(url, opts = {}) { return this._mcp.lighthouse_audit({ url, ...opts }); }
|
|
57
60
|
startTrace() { return this._mcp.performance_start_trace({}); }
|
|
58
61
|
stopTrace() { return this._mcp.performance_stop_trace({}); }
|
|
59
62
|
analyzeInsight(opts) { return this._mcp.performance_analyze_insight(opts); }
|
|
60
63
|
|
|
61
64
|
// ── Tab management ─────────────────────────────────────────────────────────
|
|
65
|
+
// list_pages returns markdown text ("## Pages\n1: <url> [selected]") like all
|
|
66
|
+
// MCP responses — callers parse with parseListPagesResponse (mcp-parsers.js).
|
|
62
67
|
listPages() { return this._mcp.list_pages({}); }
|
|
63
|
-
|
|
68
|
+
// select_page validates pageId as a number — coerce so callers may pass the
|
|
69
|
+
// string tabId they received from an MCP tool argument.
|
|
70
|
+
selectPage(tabId) { return this._mcp.select_page({ pageId: Number(tabId) }); }
|
|
64
71
|
|
|
65
72
|
// ── Lifecycle ───────────────────────────────────────────────────────────────
|
|
66
73
|
close() { return this._mcp.close(); }
|
|
@@ -82,7 +89,8 @@ export class CdpBrowserAdapter {
|
|
|
82
89
|
/**
|
|
83
90
|
* Raw pass-through to list_console_messages with custom args.
|
|
84
91
|
* Used by issues-analyzer.js for the DevTools Issues panel
|
|
85
|
-
* (types: ['issue'])
|
|
92
|
+
* (types: ['issue']). Like all MCP responses, the result is markdown
|
|
93
|
+
* text ("msgid=N [issue] text") — parse with parseConsoleMsgResponse.
|
|
86
94
|
*/
|
|
87
95
|
listConsoleRaw(args = {}) { return this._mcp.list_console_messages(args); }
|
|
88
96
|
}
|
|
@@ -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 — create it with: {"mcpServers":{"chrome-devtools":{"command":"npx","args":["-y","chrome-devtools-mcp@1.1.1"]}}}`,
|
|
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 entry in mcpServers — add: "chrome-devtools": {"command":"npx","args":["-y","chrome-devtools-mcp@1.1.1"]}' };
|
|
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() {
|
|
@@ -215,6 +230,11 @@ async function main() {
|
|
|
215
230
|
const files = await fetchPrFiles(prUrl, token);
|
|
216
231
|
changedFiles.push(...files);
|
|
217
232
|
console.log(`[argus] ${files.length} changed file(s)`);
|
|
233
|
+
if (files.length >= 300) {
|
|
234
|
+
// CI annotation lives here (CLI owns stdout) — fetchPrFiles itself only
|
|
235
|
+
// logs to stderr so the MCP server's JSON-RPC stdout stays clean.
|
|
236
|
+
console.log('::warning::PR has 300+ changed files — Argus analyzed the first 300. Routes affected by later files may be missed.');
|
|
237
|
+
}
|
|
218
238
|
|
|
219
239
|
// Step 2: Map changed files to affected routes
|
|
220
240
|
const routes = await loadRoutes();
|
|
@@ -249,10 +269,9 @@ async function main() {
|
|
|
249
269
|
|
|
250
270
|
// Normalize route paths — crawlRouteCheap builds URLs via string concat (baseUrl + route.path)
|
|
251
271
|
// 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}` };
|
|
272
|
+
const normalizedAffected = normalizeRoutePaths(affected).map((r, i) => {
|
|
273
|
+
if (r.path !== affected[i].path) {
|
|
274
|
+
console.log(`::warning::Route path "${affected[i].path}" has no leading slash — normalizing. Update argus.routes.json to use a leading slash.`);
|
|
256
275
|
}
|
|
257
276
|
return r;
|
|
258
277
|
});
|