@nerviq/cli 1.0.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +170 -73
- package/package.json +1 -1
- package/src/activity.js +20 -0
- package/src/aider/domain-packs.js +27 -2
- package/src/aider/mcp-packs.js +231 -0
- package/src/aider/techniques.js +3211 -1397
- package/src/audit.js +257 -2
- package/src/catalog.js +18 -2
- package/src/codex/domain-packs.js +23 -1
- package/src/codex/mcp-packs.js +254 -0
- package/src/codex/techniques.js +4738 -3257
- package/src/copilot/domain-packs.js +23 -1
- package/src/copilot/mcp-packs.js +254 -0
- package/src/copilot/techniques.js +3433 -1936
- package/src/cursor/domain-packs.js +23 -1
- package/src/cursor/mcp-packs.js +257 -0
- package/src/cursor/techniques.js +3698 -1869
- package/src/deprecation.js +98 -0
- package/src/domain-pack-expansion.js +571 -0
- package/src/domain-packs.js +25 -2
- package/src/formatters/otel.js +151 -0
- package/src/gemini/domain-packs.js +23 -1
- package/src/gemini/mcp-packs.js +257 -0
- package/src/gemini/techniques.js +3734 -2238
- package/src/integrations.js +194 -0
- package/src/mcp-packs.js +233 -0
- package/src/opencode/domain-packs.js +23 -1
- package/src/opencode/mcp-packs.js +231 -0
- package/src/opencode/techniques.js +3501 -1687
- package/src/org.js +68 -0
- package/src/source-urls.js +410 -260
- package/src/stack-checks.js +565 -0
- package/src/supplemental-checks.js +817 -0
- package/src/techniques.js +2929 -1449
- package/src/telemetry.js +160 -0
- package/src/windsurf/domain-packs.js +23 -1
- package/src/windsurf/mcp-packs.js +257 -0
- package/src/windsurf/techniques.js +3648 -1834
- package/src/workspace.js +233 -0
package/bin/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/b
|
|
|
9
9
|
const { writeSnapshotArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
10
|
const { collectFeedback } = require('../src/feedback');
|
|
11
11
|
const { startServer } = require('../src/server');
|
|
12
|
+
const { auditWorkspaces } = require('../src/workspace');
|
|
13
|
+
const { scanOrg } = require('../src/org');
|
|
12
14
|
const { version } = require('../package.json');
|
|
13
15
|
|
|
14
16
|
const args = process.argv.slice(2);
|
|
@@ -22,7 +24,7 @@ const COMMAND_ALIASES = {
|
|
|
22
24
|
gov: 'governance',
|
|
23
25
|
outcome: 'feedback',
|
|
24
26
|
};
|
|
25
|
-
const KNOWN_COMMANDS = ['audit', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'help', 'version'];
|
|
27
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'help', 'version'];
|
|
26
28
|
|
|
27
29
|
function levenshtein(a, b) {
|
|
28
30
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -74,17 +76,20 @@ function parseArgs(rawArgs) {
|
|
|
74
76
|
let platform = 'claude';
|
|
75
77
|
let format = null;
|
|
76
78
|
let port = null;
|
|
79
|
+
let workspace = null;
|
|
80
|
+
let webhookUrl = null;
|
|
77
81
|
let commandSet = false;
|
|
78
82
|
let extraArgs = [];
|
|
79
83
|
let convertFrom = null;
|
|
80
84
|
let convertTo = null;
|
|
81
85
|
let migrateFrom = null;
|
|
82
86
|
let migrateTo = null;
|
|
87
|
+
let checkVersion = null;
|
|
83
88
|
|
|
84
89
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
85
90
|
const arg = rawArgs[i];
|
|
86
91
|
|
|
87
|
-
if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port') {
|
|
92
|
+
if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook') {
|
|
88
93
|
const value = rawArgs[i + 1];
|
|
89
94
|
if (!value || value.startsWith('--')) {
|
|
90
95
|
throw new Error(`${arg} requires a value`);
|
|
@@ -107,6 +112,9 @@ function parseArgs(rawArgs) {
|
|
|
107
112
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
108
113
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
109
114
|
if (arg === '--port') port = value.trim();
|
|
115
|
+
if (arg === '--workspace') workspace = value.trim();
|
|
116
|
+
if (arg === '--check-version') checkVersion = value.trim();
|
|
117
|
+
if (arg === '--webhook') webhookUrl = value.trim();
|
|
110
118
|
i++;
|
|
111
119
|
continue;
|
|
112
120
|
}
|
|
@@ -191,6 +199,16 @@ function parseArgs(rawArgs) {
|
|
|
191
199
|
continue;
|
|
192
200
|
}
|
|
193
201
|
|
|
202
|
+
if (arg.startsWith('--workspace=')) {
|
|
203
|
+
workspace = arg.split('=').slice(1).join('=').trim();
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (arg.startsWith('--check-version=')) {
|
|
208
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
194
212
|
if (arg.startsWith('--')) {
|
|
195
213
|
flags.push(arg);
|
|
196
214
|
continue;
|
|
@@ -206,7 +224,54 @@ function parseArgs(rawArgs) {
|
|
|
206
224
|
|
|
207
225
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
208
226
|
|
|
209
|
-
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo };
|
|
227
|
+
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function printWorkspaceSummary(summary, options) {
|
|
231
|
+
if (options.json) {
|
|
232
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
console.log('');
|
|
237
|
+
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
238
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
239
|
+
console.log(` Root: ${summary.rootDir}`);
|
|
240
|
+
console.log(` Platform: ${summary.platform}`);
|
|
241
|
+
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
242
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
243
|
+
console.log('');
|
|
244
|
+
console.log('\x1b[1m Workspace Score Pass Total Top action\x1b[0m');
|
|
245
|
+
console.log(' ' + '─'.repeat(72));
|
|
246
|
+
for (const item of summary.workspaces) {
|
|
247
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
248
|
+
const topAction = item.error || item.topAction || '-';
|
|
249
|
+
console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
|
|
250
|
+
}
|
|
251
|
+
console.log('');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function printOrgSummary(summary, options) {
|
|
255
|
+
if (options.json) {
|
|
256
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
262
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
263
|
+
console.log(` Platform: ${summary.platform}`);
|
|
264
|
+
console.log(` Repos: ${summary.repoCount}`);
|
|
265
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log('\x1b[1m Repo Platform Score Top action\x1b[0m');
|
|
268
|
+
console.log(' ' + '─'.repeat(72));
|
|
269
|
+
for (const item of summary.repos) {
|
|
270
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
271
|
+
const topAction = item.error || item.topAction || '-';
|
|
272
|
+
console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${topAction}`);
|
|
273
|
+
}
|
|
274
|
+
console.log('');
|
|
210
275
|
}
|
|
211
276
|
|
|
212
277
|
const HELP = `
|
|
@@ -219,7 +284,9 @@ const HELP = `
|
|
|
219
284
|
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
220
285
|
nerviq audit --lite Quick scan: top 3 gaps + next command
|
|
221
286
|
nerviq audit --json Machine-readable JSON output (for CI)
|
|
287
|
+
nerviq audit --workspace packages/* Audit each workspace in a monorepo
|
|
222
288
|
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
289
|
+
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
223
290
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
224
291
|
nerviq catalog --json Export full check catalog as JSON
|
|
225
292
|
|
|
@@ -272,8 +339,11 @@ const HELP = `
|
|
|
272
339
|
--only A,B Limit plan/apply to selected proposal IDs
|
|
273
340
|
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
274
341
|
--mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
|
|
275
|
-
--
|
|
342
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
343
|
+
--format NAME Output format: json | sarif | otel
|
|
344
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
276
345
|
--port N Port for \`serve\` (default: 3000)
|
|
346
|
+
--workspace GLOBS Audit workspaces separately (e.g. packages/* or apps/web,apps/api)
|
|
277
347
|
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
278
348
|
--lite Short top-3 scan with one clear next step
|
|
279
349
|
--dry-run Preview changes without writing files
|
|
@@ -291,7 +361,9 @@ const HELP = `
|
|
|
291
361
|
npx nerviq
|
|
292
362
|
npx nerviq --lite
|
|
293
363
|
npx nerviq --platform cursor
|
|
364
|
+
npx nerviq audit --workspace packages/*
|
|
294
365
|
npx nerviq --platform codex augment
|
|
366
|
+
npx nerviq org scan ./app ./api ./infra
|
|
295
367
|
npx nerviq scan ./app ./api ./infra
|
|
296
368
|
npx nerviq harmony-audit
|
|
297
369
|
npx nerviq convert --from claude --to codex
|
|
@@ -347,16 +419,31 @@ async function main() {
|
|
|
347
419
|
platform: parsed.platform || 'claude',
|
|
348
420
|
format: parsed.format || null,
|
|
349
421
|
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
422
|
+
workspace: parsed.workspace || null,
|
|
423
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
350
424
|
dir: process.cwd()
|
|
351
425
|
};
|
|
352
426
|
|
|
353
|
-
if (
|
|
354
|
-
|
|
427
|
+
if (parsed.checkVersion) {
|
|
428
|
+
if (parsed.checkVersion !== version) {
|
|
429
|
+
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
430
|
+
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
431
|
+
console.error('');
|
|
432
|
+
}
|
|
433
|
+
options.checkVersion = parsed.checkVersion;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
|
|
437
|
+
if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
|
|
438
|
+
console.error(`\n Error: Unsupported platform '${options.platform}'.`);
|
|
439
|
+
console.error(` Why: Only the following platforms are supported: ${SUPPORTED_PLATFORMS.join(', ')}.`);
|
|
440
|
+
console.error(` Fix: Use --platform with one of the supported values, e.g.: npx nerviq --platform claude`);
|
|
441
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#cross-platform\n');
|
|
355
442
|
process.exit(1);
|
|
356
443
|
}
|
|
357
444
|
|
|
358
|
-
if (options.format !== null && !['json', 'sarif'].includes(options.format)) {
|
|
359
|
-
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json' or '
|
|
445
|
+
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
446
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
360
447
|
process.exit(1);
|
|
361
448
|
}
|
|
362
449
|
|
|
@@ -366,7 +453,10 @@ async function main() {
|
|
|
366
453
|
}
|
|
367
454
|
|
|
368
455
|
if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
|
|
369
|
-
console.error(
|
|
456
|
+
console.error(`\n Error: Invalid threshold value '${parsed.threshold}'.`);
|
|
457
|
+
console.error(' Why: --threshold must be a number between 0 and 100 representing the minimum passing score.');
|
|
458
|
+
console.error(' Fix: Use a valid number, e.g.: npx nerviq --threshold 70');
|
|
459
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
370
460
|
process.exit(1);
|
|
371
461
|
}
|
|
372
462
|
|
|
@@ -377,16 +467,21 @@ async function main() {
|
|
|
377
467
|
if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
|
|
378
468
|
const suggestion = suggestCommand(command);
|
|
379
469
|
console.error(`\n Error: Unknown command '${command}'.`);
|
|
470
|
+
console.error(` Why: '${command}' is not a recognized nerviq command or alias.`);
|
|
380
471
|
if (suggestion) {
|
|
381
|
-
console.error(` Did you mean '${suggestion}'
|
|
472
|
+
console.error(` Fix: Did you mean '${suggestion}'? Run: npx nerviq ${suggestion}`);
|
|
473
|
+
} else {
|
|
474
|
+
console.error(' Fix: Run nerviq --help to see all available commands.');
|
|
382
475
|
}
|
|
383
|
-
console.error('
|
|
476
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
384
477
|
process.exit(1);
|
|
385
478
|
}
|
|
386
479
|
|
|
387
480
|
if (!require('fs').existsSync(options.dir)) {
|
|
388
481
|
console.error(`\n Error: Directory not found: ${options.dir}`);
|
|
389
|
-
console.error('
|
|
482
|
+
console.error(' Why: The current working directory does not exist or is not accessible.');
|
|
483
|
+
console.error(' Fix: cd into your project directory first, then run nerviq.');
|
|
484
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
|
|
390
485
|
process.exit(1);
|
|
391
486
|
}
|
|
392
487
|
|
|
@@ -401,7 +496,7 @@ async function main() {
|
|
|
401
496
|
|
|
402
497
|
try {
|
|
403
498
|
const FULL_COMMAND_SET = new Set([
|
|
404
|
-
'audit', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
499
|
+
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
405
500
|
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
406
501
|
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
|
|
407
502
|
// Harmony + Synergy (cross-platform)
|
|
@@ -458,64 +553,24 @@ async function main() {
|
|
|
458
553
|
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
459
554
|
process.exit(1);
|
|
460
555
|
}
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const dir = pathMod.resolve(rawDir);
|
|
466
|
-
if (!fs.existsSync(dir)) {
|
|
467
|
-
rows.push({ name: pathMod.basename(rawDir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: 'directory not found' });
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
try {
|
|
471
|
-
const result = await audit({ dir, silent: true, platform: options.platform });
|
|
472
|
-
rows.push({
|
|
473
|
-
name: pathMod.basename(dir),
|
|
474
|
-
dir: rawDir,
|
|
475
|
-
score: result.score,
|
|
476
|
-
passed: result.passed,
|
|
477
|
-
failed: result.failed,
|
|
478
|
-
suggested: result.suggestedNextCommand || '-',
|
|
479
|
-
error: null,
|
|
480
|
-
});
|
|
481
|
-
} catch (err) {
|
|
482
|
-
rows.push({ name: pathMod.basename(dir), dir: rawDir, score: null, passed: '-', failed: '-', suggested: '-', error: err.message });
|
|
483
|
-
}
|
|
556
|
+
const summary = await scanOrg(scanDirs, options.platform);
|
|
557
|
+
printOrgSummary(summary, options);
|
|
558
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
559
|
+
process.exit(1);
|
|
484
560
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
499
|
-
console.log('');
|
|
500
|
-
|
|
501
|
-
// Table header
|
|
502
|
-
const nameW = Math.max(8, ...rows.map(r => r.name.length)) + 2;
|
|
503
|
-
const header = ` ${'Project'.padEnd(nameW)} ${'Score'.padStart(5)} ${'Pass'.padStart(4)} ${'Fail'.padStart(4)} Suggested Command`;
|
|
504
|
-
console.log('\x1b[1m' + header + '\x1b[0m');
|
|
505
|
-
console.log(' ' + '─'.repeat(header.trim().length));
|
|
506
|
-
|
|
507
|
-
for (const row of rows) {
|
|
508
|
-
if (row.error) {
|
|
509
|
-
console.log(` ${row.name.padEnd(nameW)} \x1b[31m${('ERR').padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.error}`);
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
const isWeak = weakest && row.name === weakest.name && row.dir === weakest.dir;
|
|
513
|
-
const scoreColor = row.score >= 70 ? '\x1b[32m' : row.score >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
514
|
-
const prefix = isWeak ? '\x1b[31m⚠ ' : ' ';
|
|
515
|
-
const suffix = isWeak ? ' ← weakest\x1b[0m' : '';
|
|
516
|
-
console.log(`${prefix}${row.name.padEnd(nameW)} ${scoreColor}${String(row.score).padStart(5)}\x1b[0m ${String(row.passed).padStart(4)} ${String(row.failed).padStart(4)} ${row.suggested}${suffix}`);
|
|
517
|
-
}
|
|
518
|
-
console.log('');
|
|
561
|
+
process.exit(0);
|
|
562
|
+
} else if (normalizedCommand === 'org') {
|
|
563
|
+
const subcommand = parsed.extraArgs[0];
|
|
564
|
+
const scanDirs = parsed.extraArgs.slice(1);
|
|
565
|
+
if (subcommand !== 'scan' || scanDirs.length === 0) {
|
|
566
|
+
console.error('\n Error: org requires the scan subcommand and at least one directory.');
|
|
567
|
+
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3\n');
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
const summary = await scanOrg(scanDirs, options.platform);
|
|
571
|
+
printOrgSummary(summary, options);
|
|
572
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
573
|
+
process.exit(1);
|
|
519
574
|
}
|
|
520
575
|
process.exit(0);
|
|
521
576
|
} else if (normalizedCommand === 'history') {
|
|
@@ -726,7 +781,7 @@ async function main() {
|
|
|
726
781
|
const { watch } = require('../src/watch');
|
|
727
782
|
await watch(options);
|
|
728
783
|
} else if (normalizedCommand === 'catalog') {
|
|
729
|
-
const { generateCatalog, writeCatalogJson } = require('../src/catalog');
|
|
784
|
+
const { generateCatalog, generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
730
785
|
if (options.out) {
|
|
731
786
|
const result = writeCatalogJson(options.out);
|
|
732
787
|
if (options.json) {
|
|
@@ -737,7 +792,9 @@ async function main() {
|
|
|
737
792
|
} else {
|
|
738
793
|
const catalog = generateCatalog();
|
|
739
794
|
if (options.json) {
|
|
740
|
-
|
|
795
|
+
const envelope = generateCatalogWithVersion();
|
|
796
|
+
if (options.checkVersion) envelope.requestedVersion = options.checkVersion;
|
|
797
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
741
798
|
} else {
|
|
742
799
|
// Print summary table
|
|
743
800
|
const platforms = {};
|
|
@@ -845,7 +902,43 @@ async function main() {
|
|
|
845
902
|
}
|
|
846
903
|
}
|
|
847
904
|
} else {
|
|
905
|
+
if (options.workspace) {
|
|
906
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
907
|
+
printWorkspaceSummary(summary, options);
|
|
908
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
process.exit(0);
|
|
912
|
+
}
|
|
848
913
|
const result = await audit(options);
|
|
914
|
+
if (options.webhookUrl) {
|
|
915
|
+
try {
|
|
916
|
+
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
|
917
|
+
// Auto-detect Slack vs generic by URL pattern
|
|
918
|
+
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
919
|
+
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
920
|
+
let payload;
|
|
921
|
+
if (isSlack) {
|
|
922
|
+
payload = formatSlackMessage(result);
|
|
923
|
+
} else if (isDiscord) {
|
|
924
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
925
|
+
payload = formatDiscordMessage(result);
|
|
926
|
+
} else {
|
|
927
|
+
// Generic webhook: send full JSON audit result
|
|
928
|
+
payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
|
|
929
|
+
}
|
|
930
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload);
|
|
931
|
+
if (!options.json) {
|
|
932
|
+
if (webhookResp.ok) {
|
|
933
|
+
console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
|
|
934
|
+
} else {
|
|
935
|
+
console.error(` Webhook failed: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} catch (webhookErr) {
|
|
939
|
+
if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
849
942
|
if (options.feedback && !options.json && options.format === null) {
|
|
850
943
|
const feedbackTargets = options.lite
|
|
851
944
|
? (result.liteSummary?.topNextActions || [])
|
|
@@ -875,7 +968,10 @@ async function main() {
|
|
|
875
968
|
}
|
|
876
969
|
if (options.threshold !== null && result.score < options.threshold) {
|
|
877
970
|
if (!options.json) {
|
|
878
|
-
console.error(
|
|
971
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
972
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
973
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
974
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
879
975
|
}
|
|
880
976
|
process.exit(1);
|
|
881
977
|
}
|
|
@@ -895,6 +991,7 @@ async function main() {
|
|
|
895
991
|
}
|
|
896
992
|
} catch (err) {
|
|
897
993
|
console.error(`\n Error: ${err.message}`);
|
|
994
|
+
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
898
995
|
process.exit(1);
|
|
899
996
|
}
|
|
900
997
|
}
|
package/package.json
CHANGED
package/src/activity.js
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { version } = require('../package.json');
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Generate a machine-level user identity for audit tracking.
|
|
8
|
+
* Format: hostname:username — no PII, just machine identity.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function getUserId() {
|
|
12
|
+
try {
|
|
13
|
+
const hostname = os.hostname();
|
|
14
|
+
const username = os.userInfo().username;
|
|
15
|
+
return `${hostname}:${username}`;
|
|
16
|
+
} catch {
|
|
17
|
+
return 'unknown:unknown';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
let _lastTimestamp = '';
|
|
6
22
|
let _counter = 0;
|
|
7
23
|
|
|
@@ -42,6 +58,7 @@ function writeActivityArtifact(dir, type, payload) {
|
|
|
42
58
|
id,
|
|
43
59
|
type,
|
|
44
60
|
createdAt: new Date().toISOString(),
|
|
61
|
+
userId: getUserId(),
|
|
45
62
|
...payload,
|
|
46
63
|
});
|
|
47
64
|
return {
|
|
@@ -58,6 +75,7 @@ function writeRollbackArtifact(dir, payload) {
|
|
|
58
75
|
writeJson(filePath, {
|
|
59
76
|
id,
|
|
60
77
|
createdAt: new Date().toISOString(),
|
|
78
|
+
userId: getUserId(),
|
|
61
79
|
rollbackType: 'delete-created-files',
|
|
62
80
|
...payload,
|
|
63
81
|
});
|
|
@@ -161,6 +179,7 @@ function writeSnapshotArtifact(dir, snapshotKind, payload, meta = {}) {
|
|
|
161
179
|
snapshotKind,
|
|
162
180
|
id,
|
|
163
181
|
createdAt: new Date().toISOString(),
|
|
182
|
+
userId: getUserId(),
|
|
164
183
|
generatedBy: `nerviq@${version}`,
|
|
165
184
|
directory: dir,
|
|
166
185
|
summary,
|
|
@@ -511,6 +530,7 @@ function formatRecommendationOutcomeSummary(dir) {
|
|
|
511
530
|
}
|
|
512
531
|
|
|
513
532
|
module.exports = {
|
|
533
|
+
getUserId,
|
|
514
534
|
ensureArtifactDirs,
|
|
515
535
|
writeActivityArtifact,
|
|
516
536
|
writeRollbackArtifact,
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* - Git is the only safety mechanism
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const { buildAdditionalDomainPacks, detectAdditionalDomainPacks } = require('../domain-pack-expansion');
|
|
11
|
+
|
|
12
|
+
const BASE_AIDER_DOMAIN_PACKS = [
|
|
11
13
|
{
|
|
12
14
|
key: 'baseline-general',
|
|
13
15
|
label: 'Baseline General',
|
|
@@ -170,6 +172,13 @@ const AIDER_DOMAIN_PACKS = [
|
|
|
170
172
|
},
|
|
171
173
|
];
|
|
172
174
|
|
|
175
|
+
const AIDER_DOMAIN_PACKS = [
|
|
176
|
+
...BASE_AIDER_DOMAIN_PACKS,
|
|
177
|
+
...buildAdditionalDomainPacks('aider', {
|
|
178
|
+
existingKeys: new Set(BASE_AIDER_DOMAIN_PACKS.map((pack) => pack.key)),
|
|
179
|
+
}),
|
|
180
|
+
];
|
|
181
|
+
|
|
173
182
|
function uniqueByKey(matches) {
|
|
174
183
|
const seen = new Set();
|
|
175
184
|
return matches.filter(m => {
|
|
@@ -181,9 +190,10 @@ function uniqueByKey(matches) {
|
|
|
181
190
|
|
|
182
191
|
function detectAiderDomainPacks(ctx) {
|
|
183
192
|
const matches = [];
|
|
193
|
+
const pkg = typeof ctx.jsonFile === 'function' ? (ctx.jsonFile('package.json') || {}) : {};
|
|
184
194
|
const deps = ctx.allDependencies ? ctx.allDependencies() : {};
|
|
185
195
|
const files = ctx.files || [];
|
|
186
|
-
const
|
|
196
|
+
const stackKeys = new Set();
|
|
187
197
|
|
|
188
198
|
function addMatch(key, reasons) {
|
|
189
199
|
const pack = AIDER_DOMAIN_PACKS.find(p => p.key === key);
|
|
@@ -263,6 +273,21 @@ function detectAiderDomainPacks(ctx) {
|
|
|
263
273
|
addMatch('security-focused', ['Detected security-focused repo signals.']);
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
const hasCi = Boolean(ctx.fileContent('.github/workflows')) || files.some((file) => /^\.github\/workflows\//.test(file));
|
|
277
|
+
|
|
278
|
+
detectAdditionalDomainPacks({
|
|
279
|
+
ctx,
|
|
280
|
+
pkg,
|
|
281
|
+
deps,
|
|
282
|
+
stackKeys,
|
|
283
|
+
addMatch,
|
|
284
|
+
hasBackend: isBackend,
|
|
285
|
+
hasFrontend: isFrontend,
|
|
286
|
+
hasInfra: isInfra,
|
|
287
|
+
hasCi,
|
|
288
|
+
isEnterpriseGoverned: hasPolicyFiles,
|
|
289
|
+
});
|
|
290
|
+
|
|
266
291
|
if (matches.length === 0) {
|
|
267
292
|
addMatch('baseline-general', [
|
|
268
293
|
'No stronger domain signal detected — safe general Aider baseline is the best starting point.',
|