argusqa-os 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.mcp.json +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +879 -0
  4. package/package.json +69 -0
  5. package/src/adapters/browser.js +82 -0
  6. package/src/argus.js +8 -0
  7. package/src/batch-runner.js +8 -0
  8. package/src/cli/init.js +314 -0
  9. package/src/config/schema.js +108 -0
  10. package/src/config/targets.js +309 -0
  11. package/src/domain/finding.js +25 -0
  12. package/src/mcp-server.js +156 -0
  13. package/src/orchestration/crawl-and-report.js +16 -0
  14. package/src/orchestration/dispatcher.js +263 -0
  15. package/src/orchestration/env-comparison.js +498 -0
  16. package/src/orchestration/orchestrator.js +1128 -0
  17. package/src/orchestration/report-processor.js +134 -0
  18. package/src/orchestration/slack-notifier.js +337 -0
  19. package/src/orchestration/watch-mode.js +316 -0
  20. package/src/registry.js +18 -0
  21. package/src/server/index.js +94 -0
  22. package/src/server/interaction-handler.js +126 -0
  23. package/src/server/slash-command-handler.js +185 -0
  24. package/src/utils/api-frequency.js +128 -0
  25. package/src/utils/baseline-manager.js +255 -0
  26. package/src/utils/codebase-analyzer.js +299 -0
  27. package/src/utils/content-analyzer.js +155 -0
  28. package/src/utils/contract-validator.js +178 -0
  29. package/src/utils/css-analyzer.js +407 -0
  30. package/src/utils/diff.js +189 -0
  31. package/src/utils/flakiness-detector.js +82 -0
  32. package/src/utils/flow-runner.js +572 -0
  33. package/src/utils/github-reporter.js +310 -0
  34. package/src/utils/hover-analyzer.js +214 -0
  35. package/src/utils/html-reporter.js +301 -0
  36. package/src/utils/issues-analyzer.js +171 -0
  37. package/src/utils/keyboard-analyzer.js +141 -0
  38. package/src/utils/lighthouse-checker.js +120 -0
  39. package/src/utils/logger.js +39 -0
  40. package/src/utils/login-orchestrator.js +99 -0
  41. package/src/utils/mcp-client.js +264 -0
  42. package/src/utils/mcp-parsers.js +57 -0
  43. package/src/utils/memory-analyzer.js +270 -0
  44. package/src/utils/network-timing-analyzer.js +76 -0
  45. package/src/utils/parallel-crawler.js +28 -0
  46. package/src/utils/responsive-analyzer.js +253 -0
  47. package/src/utils/retry.js +36 -0
  48. package/src/utils/route-discoverer.js +306 -0
  49. package/src/utils/security-analyzer.js +302 -0
  50. package/src/utils/seo-analyzer.js +164 -0
  51. package/src/utils/session-manager.js +12 -0
  52. package/src/utils/session-persistence.js +214 -0
  53. package/src/utils/severity-overrides.js +91 -0
  54. package/src/utils/slack-guard.js +18 -0
  55. package/src/utils/slug.js +8 -0
  56. package/src/utils/snapshot-analyzer.js +330 -0
  57. package/src/utils/telemetry.js +190 -0
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "argusqa-os",
3
+ "version": "9.2.0",
4
+ "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
5
+ "keywords": [
6
+ "mcp",
7
+ "mcp-server",
8
+ "claude",
9
+ "qa",
10
+ "testing",
11
+ "accessibility",
12
+ "automation",
13
+ "chrome-devtools",
14
+ "web-testing"
15
+ ],
16
+ "author": "ironclawdevs",
17
+ "license": "MIT",
18
+ "homepage": "https://argus-qa.com",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/ironclawdevs27/Argus.git"
22
+ },
23
+ "files": [
24
+ "src/",
25
+ ".mcp.json"
26
+ ],
27
+ "type": "module",
28
+ "engines": {
29
+ "node": ">=20.19.0"
30
+ },
31
+ "bin": {
32
+ "argus": "src/cli/init.js",
33
+ "argus-mcp": "src/mcp-server.js",
34
+ "argusqa-os": "src/mcp-server.js"
35
+ },
36
+ "scripts": {
37
+ "setup": "node -e \"import('fs').then(fs => fs.default.mkdirSync('./reports', { recursive: true }))\"",
38
+ "init": "node src/cli/init.js",
39
+ "crawl": "node src/orchestration/crawl-and-report.js",
40
+ "compare": "node src/orchestration/env-comparison.js",
41
+ "watch": "node src/orchestration/watch-mode.js",
42
+ "server": "node src/server/index.js",
43
+ "harness": "node test-harness/server.js",
44
+ "harness:staging": "PORT=3101 node test-harness/server.js",
45
+ "test:harness": "node test-harness/validate.js",
46
+ "test:unit": "vitest run test/unit",
47
+ "test": "npm run test:unit && npm run test:harness",
48
+ "report:html": "node src/utils/html-reporter.js",
49
+ "mcp-server": "node src/mcp-server.js"
50
+ },
51
+ "dependencies": {
52
+ "@modelcontextprotocol/sdk": "^1.29.0",
53
+ "@opentelemetry/api": "^1.9.1",
54
+ "@opentelemetry/sdk-node": "^0.218.0",
55
+ "@slack/web-api": "^7.3.4",
56
+ "chrome": "^0.1.0",
57
+ "dotenv": "^16.4.5",
58
+ "express": "^4.19.2",
59
+ "pino": "^10.3.1",
60
+ "pino-pretty": "^13.1.3",
61
+ "pixelmatch": "^5.3.0",
62
+ "pngjs": "^7.0.0",
63
+ "sharp": "^0.33.4",
64
+ "zod": "^4.4.3"
65
+ },
66
+ "devDependencies": {
67
+ "vitest": "^4.1.7"
68
+ }
69
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * CdpBrowserAdapter — facade over chrome-devtools-mcp.
3
+ *
4
+ * All analyzer modules call browser.* methods instead of mcp.* directly.
5
+ * This single file is the only place that knows the chrome-devtools-mcp
6
+ * API shape, so any future API change (parameter renames, new tool versions)
7
+ * requires editing exactly one file.
8
+ *
9
+ * listConsole() and listNetwork() parse the markdown-text format that
10
+ * chrome-devtools-mcp@latest returns, so callers always receive structured
11
+ * arrays rather than raw text.
12
+ *
13
+ * listConsoleRaw(args) passes arbitrary args through unparsed — used by
14
+ * issues-analyzer.js which calls list_console_messages({ types: ['issue'] }).
15
+ */
16
+
17
+ import { parseConsoleMsgResponse, parseNetworkReqResponse } from '../utils/mcp-parsers.js';
18
+ import { withRetry } from '../utils/retry.js';
19
+
20
+ export class CdpBrowserAdapter {
21
+ constructor(mcp) { this._mcp = mcp; }
22
+
23
+ // ── Navigation ──────────────────────────────────────────────────────────────
24
+ navigate(url) { return withRetry(() => this._mcp.navigate_page({ url }), { label: `navigate(${url})` }); }
25
+
26
+ // ── Evaluation & snapshots ──────────────────────────────────────────────────
27
+ evaluate(fn) { return this._mcp.evaluate_script({ function: fn }); }
28
+ snapshot() { return this._mcp.take_snapshot(); }
29
+ screenshot(opts = {}) { return this._mcp.take_screenshot(opts); }
30
+ heapSnapshot(opts = {}) { return this._mcp.take_memory_snapshot(opts); }
31
+
32
+ // ── Interactions ────────────────────────────────────────────────────────────
33
+ // click is intentionally NOT retried — it is not idempotent (submits forms,
34
+ // toggles state, triggers deletions). A retry after an ambiguous MCP timeout
35
+ // cannot distinguish "Chrome never received the click" from "Chrome processed
36
+ // it but the pipe dropped the response" — firing twice causes duplicate actions.
37
+ click(uid) { return this._mcp.click({ uid }); }
38
+ fill(uid, value) { return withRetry(() => this._mcp.fill({ uid, value }), { label: `fill(${uid})` }); }
39
+ type(text) { return this._mcp.type_text({ text }); }
40
+ pressKey(key) { return this._mcp.press_key({ key }); }
41
+ hover(uid) { return this._mcp.hover({ uid }); }
42
+ drag(src, tgt) { return this._mcp.drag({ from_uid: src, to_uid: tgt }); }
43
+ uploadFile(uid, filePath) { return this._mcp.upload_file({ uid, filePath }); }
44
+ handleDialog(accept, promptText = '') { return this._mcp.handle_dialog({ accept, promptText }); }
45
+ waitFor(opts) { return this._mcp.wait_for(opts); }
46
+
47
+ // ── Viewport ────────────────────────────────────────────────────────────────
48
+ emulate(viewport) { return this._mcp.emulate({ viewport }); }
49
+ emulateCpu(rate) { return this._mcp.emulate_cpu({ throttlingRate: rate }); }
50
+ resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
51
+
52
+ // ── Network & performance ───────────────────────────────────────────────────
53
+ getNetworkRequest(reqId) { return this._mcp.get_network_request({ requestId: reqId }); }
54
+ lighthouse(url, opts = {}) { return this._mcp.lighthouse_audit({ url, ...opts }); }
55
+ startTrace() { return this._mcp.performance_start_trace({}); }
56
+ stopTrace() { return this._mcp.performance_stop_trace({}); }
57
+ analyzeInsight(opts) { return this._mcp.performance_analyze_insight(opts); }
58
+
59
+ // ── Lifecycle ───────────────────────────────────────────────────────────────
60
+ close() { return this._mcp.close(); }
61
+
62
+ // ── Console & network lists (text-parsed) ───────────────────────────────────
63
+
64
+ /** Returns structured array from list_console_messages (parses markdown text). */
65
+ async listConsole() {
66
+ const raw = await this._mcp.list_console_messages({});
67
+ return parseConsoleMsgResponse(raw);
68
+ }
69
+
70
+ /** Returns structured array from list_network_requests (parses markdown text). */
71
+ async listNetwork() {
72
+ const raw = await this._mcp.list_network_requests({});
73
+ return parseNetworkReqResponse(raw);
74
+ }
75
+
76
+ /**
77
+ * Raw pass-through to list_console_messages with custom args.
78
+ * Used by issues-analyzer.js for the DevTools Issues panel
79
+ * (types: ['issue']) which returns structured data, not text.
80
+ */
81
+ listConsoleRaw(args = {}) { return this._mcp.list_console_messages(args); }
82
+ }
package/src/argus.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Argus — single-page audit entry point.
3
+ * Re-exports the main crawl pipeline from crawl-and-report.js.
4
+ * Run via: npm run crawl (or node src/argus.js)
5
+ *
6
+ * Config validation (Zod) runs inside runCrawl() — src/config/schema.js.
7
+ */
8
+ export * from './orchestration/crawl-and-report.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Argus — multi-page batch audit entry point.
3
+ * Re-exports the main crawl pipeline from crawl-and-report.js.
4
+ * Run via: node src/batch-runner.js
5
+ *
6
+ * Config validation (Zod) runs inside runCrawl() — src/config/schema.js.
7
+ */
8
+ export * from './orchestration/crawl-and-report.js';
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Argus Phase C4 — argus init CLI
4
+ *
5
+ * Guides first-time Argus setup against a target app:
6
+ * 1. Collect target URLs
7
+ * 2. Detect framework from source dir (Next.js / React Router / unknown)
8
+ * 3. Auto-discover routes using C3 route-discoverer
9
+ * 4. Prompt for Slack + GitHub config (fully optional)
10
+ * 5. Write .env with collected values
11
+ * 6. Write src/config/targets.js pre-filled with discovered routes
12
+ *
13
+ * Pure helpers (detectFramework, generateTargetsJs, generateEnvFile) are
14
+ * exported so the test harness can unit-test them without side effects.
15
+ * The interactive main() only runs when this file is the process entry point.
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import readline from 'readline';
21
+ import { fileURLToPath } from 'url';
22
+ import { discoverRoutes } from '../utils/route-discoverer.js';
23
+ import { childLogger } from '../utils/logger.js';
24
+
25
+ const logger = childLogger('init');
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+
29
+ // ── C4.1 Framework detection ─────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Detect the frontend framework used in the given project root by reading its
33
+ * package.json. Returns 'nextjs', 'react-router', or 'unknown'.
34
+ *
35
+ * @param {string} projectRoot absolute path to the target app root
36
+ * @returns {'nextjs' | 'react-router' | 'unknown'}
37
+ */
38
+ export function detectFramework(projectRoot) {
39
+ if (!projectRoot || !fs.existsSync(projectRoot)) return 'unknown';
40
+ const pkgPath = path.join(projectRoot, 'package.json');
41
+ if (!fs.existsSync(pkgPath)) return 'unknown';
42
+ let pkg;
43
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { return 'unknown'; }
44
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
45
+ if (deps['next']) return 'nextjs';
46
+ if (deps['react-router-dom'] || deps['react-router']) return 'react-router';
47
+ return 'unknown';
48
+ }
49
+
50
+ // ── C4.2 targets.js generator ────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Generate a pre-filled src/config/targets.js from discovered routes.
54
+ *
55
+ * @param {Array<{path:string, name:string, critical:boolean, waitFor:string|null}>} routes
56
+ * @param {{ framework?: string, sourceDir?: string|null, envFile?: string|null }} options
57
+ * @returns {string}
58
+ */
59
+ export function generateTargetsJs(routes, options = {}) {
60
+ const { framework = 'unknown', sourceDir = null, envFile = null } = options;
61
+
62
+ // Escape for single-quoted JS string literals: backslash → \\ , CR → \r , LF → \n , ' → \'
63
+ const esc = s => String(s ?? '').replace(/\\/g, '\\\\').replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/'/g, "\\'");
64
+ const routeLines = routes.length
65
+ ? routes.map(r => {
66
+ const wf = r.waitFor ? `'${esc(r.waitFor)}'` : 'null';
67
+ return ` { path: '${esc(r.path)}', name: '${esc(r.name)}', critical: ${!!r.critical}, waitFor: ${wf} },`;
68
+ }).join('\n')
69
+ : ` { path: '/', name: 'Home', critical: true, waitFor: 'main' },`;
70
+
71
+ const autoDiscoverComment =
72
+ framework === 'nextjs'
73
+ ? '// Next.js detected — nextjs discovery enabled'
74
+ : framework === 'react-router'
75
+ ? '// React Router detected — reactRouter discovery enabled (experimental)'
76
+ : '// Set sitemap: true to fetch /sitemap.xml from TARGET_DEV_URL';
77
+
78
+ const sourceDirVal = sourceDir
79
+ ? `process.env.ARGUS_SOURCE_DIR ?? '${sourceDir}'`
80
+ : `process.env.ARGUS_SOURCE_DIR ?? null`;
81
+ const envFileVal = envFile
82
+ ? `process.env.ARGUS_ENV_FILE ?? '${envFile}'`
83
+ : `process.env.ARGUS_ENV_FILE ?? null`;
84
+
85
+ return `/**
86
+ * ARGUS Target Configuration — generated by argus init
87
+ *
88
+ * Edit to add routes, flows, and API contracts.
89
+ * Run \`npm run crawl\` to start auditing.
90
+ */
91
+
92
+ export const config = {
93
+ pageSettleMs: 2000,
94
+ screenshotQuality: 90,
95
+ screenshotDiffThreshold: parseFloat(process.env.SCREENSHOT_DIFF_THRESHOLD ?? '0.5'),
96
+ outputDir: process.env.REPORT_OUTPUT_DIR ?? './reports',
97
+ };
98
+
99
+ export const routes = [
100
+ ${routeLines}
101
+ ];
102
+
103
+ export const comparisonRoutes = routes.filter(r => r.critical);
104
+
105
+ export const apiContracts = [];
106
+
107
+ export const severityOverrides = {};
108
+
109
+ export const auth = null;
110
+
111
+ export const flows = [];
112
+
113
+ export const codebase = {
114
+ sourceDir: ${sourceDirVal},
115
+ envFile: ${envFileVal},
116
+ };
117
+
118
+ ${autoDiscoverComment}
119
+ export const autoDiscover = {
120
+ sitemap: true,
121
+ nextjs: ${framework === 'nextjs'},
122
+ reactRouter: ${framework === 'react-router'},
123
+ };
124
+ `;
125
+ }
126
+
127
+ // ── C4.3 .env generator ──────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Generate a .env file with user-supplied configuration values substituted in.
131
+ * Any field that is blank/falsy is rendered as a commented-out placeholder.
132
+ *
133
+ * @param {{ devUrl?:string, stagingUrl?:string, slackToken?:string,
134
+ * slackSecret?:string, slackCritical?:string, slackWarnings?:string,
135
+ * slackDigest?:string, githubToken?:string, githubRepo?:string,
136
+ * sourceDir?:string, envFile?:string }} options
137
+ * @returns {string}
138
+ */
139
+ export function generateEnvFile(options = {}) {
140
+ const {
141
+ devUrl = 'http://localhost:3000',
142
+ stagingUrl = '',
143
+ slackToken = '',
144
+ slackSecret = '',
145
+ slackCritical = '',
146
+ slackWarnings = '',
147
+ slackDigest = '',
148
+ githubToken = '',
149
+ githubRepo = '',
150
+ sourceDir = '',
151
+ envFile = '',
152
+ } = options;
153
+
154
+ const stagingLine = stagingUrl
155
+ ? `TARGET_STAGING_URL=${stagingUrl}`
156
+ : `# TARGET_STAGING_URL=`;
157
+
158
+ const slackSection = slackToken
159
+ ? `SLACK_BOT_TOKEN=${slackToken}
160
+ SLACK_SIGNING_SECRET=${slackSecret}
161
+ SLACK_CHANNEL_CRITICAL=${slackCritical}
162
+ SLACK_CHANNEL_WARNINGS=${slackWarnings}
163
+ ${slackDigest ? `SLACK_CHANNEL_DIGEST=${slackDigest}` : '# SLACK_CHANNEL_DIGEST='}`
164
+ : `# SLACK_BOT_TOKEN=xoxb-...
165
+ # SLACK_SIGNING_SECRET=...
166
+ # SLACK_CHANNEL_CRITICAL=
167
+ # SLACK_CHANNEL_WARNINGS=
168
+ # SLACK_CHANNEL_DIGEST=`;
169
+
170
+ const githubSection = githubToken
171
+ ? `GITHUB_TOKEN=${githubToken}
172
+ GITHUB_REPOSITORY=${githubRepo}`
173
+ : `# GITHUB_TOKEN=ghp_...
174
+ # GITHUB_REPOSITORY=owner/repo`;
175
+
176
+ const sourceDirLine = sourceDir ? `ARGUS_SOURCE_DIR=${sourceDir}` : `# ARGUS_SOURCE_DIR=../my-app/src`;
177
+ const envFileLine = envFile ? `ARGUS_ENV_FILE=${envFile}` : `# ARGUS_ENV_FILE=../my-app/.env`;
178
+
179
+ return `# Argus configuration — generated by argus init
180
+
181
+ # Target URLs (required)
182
+ TARGET_DEV_URL=${devUrl}
183
+ ${stagingLine}
184
+
185
+ # Slack (optional — omit entirely to use local report.html instead)
186
+ ${slackSection}
187
+
188
+ # GitHub PR integration (optional)
189
+ ${githubSection}
190
+
191
+ # Codebase analysis + route discovery (optional — enables C1 + C3)
192
+ ${sourceDirLine}
193
+ ${envFileLine}
194
+
195
+ # Auth credentials (only needed when auth is configured in targets.js)
196
+ # ARGUS_AUTH_EMAIL=qa@example.com
197
+ # ARGUS_AUTH_PASSWORD=...
198
+
199
+ # Output settings
200
+ REPORT_OUTPUT_DIR=./reports
201
+ SCREENSHOT_DIFF_THRESHOLD=0.5
202
+ `;
203
+ }
204
+
205
+ // ── C4.4 Interactive setup wizard ────────────────────────────────────────────
206
+
207
+ function createPrompter() {
208
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
209
+ const ask = q => new Promise(res => rl.question(q, res));
210
+ return { ask, close: () => rl.close() };
211
+ }
212
+
213
+ const tick = msg => process.stdout.write(` ✓ ${msg}\n`);
214
+ const note = msg => process.stdout.write(` ${msg}\n`);
215
+ const step = msg => process.stdout.write(`\n▶ ${msg}\n`);
216
+
217
+ async function main() {
218
+ process.stdout.write('\n');
219
+ process.stdout.write(' ╔' + '═'.repeat(42) + '╗\n');
220
+ process.stdout.write(' ║ Argus init — first-time setup ║\n');
221
+ process.stdout.write(' ╚' + '═'.repeat(42) + '╝\n\n');
222
+
223
+ const { ask, close } = createPrompter();
224
+
225
+ try {
226
+ // Step 1 — Target URLs
227
+ step('Step 1/4 — Target URLs');
228
+ const devUrl = (await ask(' Dev URL [http://localhost:3000]: ')).trim() || 'http://localhost:3000';
229
+ const stagingUrl = (await ask(' Staging URL (blank to skip): ')).trim();
230
+
231
+ // Step 2 — Source code
232
+ step('Step 2/4 — Source Code (enables C1 env-var audit + C3 file-system discovery)');
233
+ const rawSourceDir = (await ask(' App source directory (blank to skip): ')).trim();
234
+ const sourceDir = rawSourceDir ? path.resolve(rawSourceDir) : '';
235
+ const rawEnvFile = (await ask(' App .env file (blank to skip): ')).trim();
236
+ const envFilePath = rawEnvFile ? path.resolve(rawEnvFile) : '';
237
+
238
+ // Step 3 — Route discovery
239
+ step('Step 3/4 — Route Discovery');
240
+
241
+ const framework = sourceDir ? detectFramework(sourceDir) : 'unknown';
242
+ if (sourceDir) note(`Detected framework: ${framework}`);
243
+
244
+ const autoDiscoverConfig = {
245
+ sitemap: true,
246
+ nextjs: framework === 'nextjs',
247
+ reactRouter: framework === 'react-router',
248
+ };
249
+
250
+ const seedRoutes = [
251
+ { path: '/', name: 'Home', critical: true, waitFor: 'main' },
252
+ { path: '/login', name: 'Login', critical: true, waitFor: 'form' },
253
+ ];
254
+
255
+ note('Discovering routes (sitemap + framework structure)...');
256
+ let finalRoutes;
257
+ try {
258
+ finalRoutes = await discoverRoutes(devUrl, sourceDir || null, autoDiscoverConfig, seedRoutes);
259
+ const added = finalRoutes.filter(r => r.discovered).length;
260
+ tick(`Found ${finalRoutes.length} route(s) total (${added} auto-discovered)`);
261
+ } catch (err) {
262
+ note(`Route discovery skipped (${err.message}) — using seed routes`);
263
+ finalRoutes = seedRoutes;
264
+ }
265
+
266
+ // Step 4 — Slack + GitHub
267
+ step('Step 4/4 — Slack & GitHub (press Enter to skip each)');
268
+ const slackToken = (await ask(' Slack bot token (xoxb-..., blank to skip): ')).trim();
269
+ let slackSecret = '', slackCritical = '', slackWarnings = '', slackDigest = '';
270
+ if (slackToken) {
271
+ slackSecret = (await ask(' Slack signing secret: ')).trim();
272
+ slackCritical = (await ask(' Slack critical channel ID (C...): ')).trim();
273
+ slackWarnings = (await ask(' Slack warnings channel ID (C...): ')).trim();
274
+ slackDigest = (await ask(' Slack digest channel ID (C...): ')).trim();
275
+ }
276
+ const githubToken = (await ask(' GitHub token (ghp_..., blank to skip): ')).trim();
277
+ let githubRepo = '';
278
+ if (githubToken) {
279
+ githubRepo = (await ask(' GitHub repository (owner/repo): ')).trim();
280
+ }
281
+
282
+ // Write files
283
+ process.stdout.write('\n');
284
+
285
+ const envContent = generateEnvFile({ devUrl, stagingUrl, slackToken, slackSecret,
286
+ slackCritical, slackWarnings, slackDigest,
287
+ githubToken, githubRepo, sourceDir, envFile: envFilePath });
288
+ const targetsContent = generateTargetsJs(finalRoutes, { framework, sourceDir, envFile: envFilePath });
289
+
290
+ if (fs.existsSync('.env')) {
291
+ logger.warn(' ⚠ .env already exists — skipping write to preserve existing credentials. Delete it manually to regenerate.');
292
+ } else {
293
+ fs.writeFileSync('.env', envContent, 'utf8');
294
+ tick('Wrote .env');
295
+ }
296
+
297
+ const targetsPath = path.join('src', 'config', 'targets.js');
298
+ fs.mkdirSync(path.dirname(targetsPath), { recursive: true });
299
+ fs.writeFileSync(targetsPath, targetsContent, 'utf8');
300
+ tick(`Wrote ${targetsPath}`);
301
+
302
+ process.stdout.write('\n');
303
+ process.stdout.write(' ╔' + '═'.repeat(48) + '╗\n');
304
+ process.stdout.write(' ║ Done! Run `npm run crawl` to start auditing. ║\n');
305
+ process.stdout.write(' ╚' + '═'.repeat(48) + '╝\n\n');
306
+
307
+ } finally {
308
+ close();
309
+ }
310
+ }
311
+
312
+ if (process.argv[1] === __filename) {
313
+ main().catch(err => { console.error('argus init failed:', err.message); process.exit(1); });
314
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Argus Config Schema (v9.1.6)
3
+ *
4
+ * Zod validation for src/config/targets.js exports.
5
+ * Called at startup (before any crawl begins) so misconfiguration is caught
6
+ * immediately with a clear error rather than a silent runtime failure mid-crawl.
7
+ *
8
+ * Usage:
9
+ * import * as targets from './config/targets.js';
10
+ * import { validateConfig } from './config/schema.js';
11
+ * validateConfig(targets);
12
+ */
13
+
14
+ import { z } from 'zod';
15
+
16
+ // ── Route ─────────────────────────────────────────────────────────────────────
17
+
18
+ const RouteSchema = z.object({
19
+ path: z.string().startsWith('/'),
20
+ name: z.string().min(1),
21
+ critical: z.boolean().optional(),
22
+ waitFor: z.string().nullable().optional(),
23
+ discovered: z.boolean().optional(),
24
+ }).passthrough();
25
+
26
+ // ── Thresholds ────────────────────────────────────────────────────────────────
27
+
28
+ const LighthouseCategorySchema = z.object({
29
+ critical: z.number().min(0).max(100),
30
+ warning: z.number().min(0).max(100),
31
+ });
32
+
33
+ const ThresholdsSchema = z.object({
34
+ perf: z.object({
35
+ LCP: z.number().positive(),
36
+ CLS: z.number().positive(),
37
+ FID: z.number().positive(),
38
+ TTFB: z.number().positive(),
39
+ }),
40
+ network: z.object({
41
+ slowWarning: z.number().positive(),
42
+ slowCritical: z.number().positive(),
43
+ sizeWarning: z.number().positive(),
44
+ sizeCritical: z.number().positive(),
45
+ }),
46
+ memory: z.object({
47
+ detachedWarning: z.number().nonnegative(),
48
+ detachedCritical: z.number().nonnegative(),
49
+ heapGrowthWarning: z.number().nonnegative(),
50
+ heapGrowthCritical: z.number().nonnegative(),
51
+ }),
52
+ hover: z.object({
53
+ waitMs: z.number().nonnegative(),
54
+ maxDropdowns: z.number().int().positive(),
55
+ maxTooltips: z.number().int().positive(),
56
+ }),
57
+ security: z.object({
58
+ headTimeoutMs: z.number().positive(),
59
+ }),
60
+ apiFrequency: z.object({
61
+ warningCount: z.number().int().positive(),
62
+ criticalCount: z.number().int().positive(),
63
+ }),
64
+ lighthouse: z.object({
65
+ accessibility: LighthouseCategorySchema,
66
+ performance: LighthouseCategorySchema,
67
+ seo: LighthouseCategorySchema,
68
+ 'best-practices': LighthouseCategorySchema,
69
+ }),
70
+ });
71
+
72
+ // ── Top-level Config ──────────────────────────────────────────────────────────
73
+
74
+ export const ConfigSchema = z.object({
75
+ config: z.object({
76
+ pageSettleMs: z.number().positive(),
77
+ screenshotQuality: z.number().min(1).max(100).optional(),
78
+ screenshotDiffThreshold: z.number().min(0).optional(),
79
+ outputDir: z.string().optional(),
80
+ }).passthrough(),
81
+ routes: z.array(RouteSchema),
82
+ thresholds: ThresholdsSchema,
83
+ comparisonRoutes: z.array(z.any()).optional(),
84
+ apiContracts: z.array(z.any()).optional(),
85
+ severityOverrides: z.record(z.string()).optional(),
86
+ auth: z.any().nullable().optional(),
87
+ flows: z.array(z.any()).optional(),
88
+ codebase: z.any().nullable().optional(),
89
+ autoDiscover: z.any().nullable().optional(),
90
+ }).passthrough();
91
+
92
+ // ── Public API ────────────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Validate the targets.js namespace against ConfigSchema.
96
+ * Throws a descriptive Error (wrapping the ZodError) on any schema violation.
97
+ *
98
+ * @param {object} targets - The targets.js module namespace (import * as targets)
99
+ */
100
+ export function validateConfig(targets) {
101
+ const result = ConfigSchema.safeParse(targets);
102
+ if (!result.success) {
103
+ const issues = result.error.issues
104
+ .map(i => ` ${i.path.join('.')}: ${i.message}`)
105
+ .join('\n');
106
+ throw new Error(`[ARGUS] Invalid targets.js configuration:\n${issues}`);
107
+ }
108
+ }