@nerviq/cli 1.11.0 → 1.13.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.
- package/README.md +216 -124
- package/bin/cli.js +620 -183
- package/package.json +3 -2
- package/src/activity.js +49 -9
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +65 -20
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +128 -0
- package/src/anti-patterns.js +13 -0
- package/src/audit/instruction-files.js +180 -0
- package/src/audit/recommendations.js +531 -0
- package/src/audit.js +53 -681
- package/src/behavioral-drift.js +801 -0
- package/src/codex/freshness.js +84 -25
- package/src/continuous-ops.js +681 -0
- package/src/copilot/freshness.js +57 -20
- package/src/cost-tracking.js +61 -0
- package/src/cursor/freshness.js +65 -20
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/freshness.js +74 -21
- package/src/gemini/freshness.js +66 -21
- package/src/governance.js +59 -43
- package/src/hook-validation.js +342 -0
- package/src/index.js +5 -0
- package/src/integrations.js +42 -5
- package/src/mcp-server.js +95 -59
- package/src/mcp-validation.js +337 -0
- package/src/opencode/freshness.js +66 -21
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/setup/analysis.js +619 -0
- package/src/setup/runtime.js +172 -0
- package/src/setup.js +62 -748
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5607
- package/src/watch.js +18 -0
- package/src/windsurf/freshness.js +36 -21
- package/src/windsurf/techniques.js +17 -12
package/bin/cli.js
CHANGED
|
@@ -14,8 +14,9 @@ const { auditWorkspaces } = require('../src/workspace');
|
|
|
14
14
|
const { scanOrg } = require('../src/org');
|
|
15
15
|
const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
|
|
16
16
|
const { VERIFICATION_DATES, getVerificationDate, getVerificationStats } = require('../src/verification-metadata');
|
|
17
|
-
const { init: initI18n, t } = require('../src/i18n');
|
|
18
|
-
const { version } = require('../package.json');
|
|
17
|
+
const { init: initI18n, t } = require('../src/i18n');
|
|
18
|
+
const { version } = require('../package.json');
|
|
19
|
+
const { SNAPSHOT_MILESTONES } = require('../src/activity');
|
|
19
20
|
|
|
20
21
|
const args = process.argv.slice(2);
|
|
21
22
|
const COMMAND_ALIASES = {
|
|
@@ -28,7 +29,7 @@ const COMMAND_ALIASES = {
|
|
|
28
29
|
gov: 'governance',
|
|
29
30
|
outcome: 'feedback',
|
|
30
31
|
};
|
|
31
|
-
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'help', 'version'];
|
|
32
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'baseline', 'exception', 'help', 'version'];
|
|
32
33
|
|
|
33
34
|
function levenshtein(a, b) {
|
|
34
35
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -97,16 +98,27 @@ function parseArgs(rawArgs) {
|
|
|
97
98
|
let feedbackEffect = null;
|
|
98
99
|
let feedbackNotes = null;
|
|
99
100
|
let feedbackSource = null;
|
|
100
|
-
let feedbackScoreDelta = null;
|
|
101
|
-
let platform = 'claude';
|
|
102
|
-
let
|
|
101
|
+
let feedbackScoreDelta = null;
|
|
102
|
+
let platform = 'claude';
|
|
103
|
+
let platformExplicit = false;
|
|
104
|
+
let format = null;
|
|
103
105
|
let port = null;
|
|
104
106
|
let workspace = null;
|
|
105
107
|
let webhookUrl = null;
|
|
106
108
|
let webhookHeaders = [];
|
|
107
109
|
let webhookRetries = null;
|
|
108
110
|
let snapshotTags = [];
|
|
109
|
-
let
|
|
111
|
+
let snapshotMilestone = null;
|
|
112
|
+
let campaigns = [];
|
|
113
|
+
let diffBase = null;
|
|
114
|
+
let diffHead = null;
|
|
115
|
+
let driftMode = null;
|
|
116
|
+
let exceptionOwner = null;
|
|
117
|
+
let exceptionReason = null;
|
|
118
|
+
let exceptionExpires = null;
|
|
119
|
+
let exceptionScope = null;
|
|
120
|
+
let exceptionClass = null;
|
|
121
|
+
let commandSet = false;
|
|
110
122
|
let extraArgs = [];
|
|
111
123
|
let convertFrom = null;
|
|
112
124
|
let convertTo = null;
|
|
@@ -122,7 +134,7 @@ function parseArgs(rawArgs) {
|
|
|
122
134
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
123
135
|
const arg = rawArgs[i];
|
|
124
136
|
|
|
125
|
-
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' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag') {
|
|
137
|
+
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' || arg === '--webhook-header' || arg === '--webhook-retries' || arg === '--external' || arg === '--team-profile' || arg === '--lang' || arg === '--tag' || arg === '--milestone' || arg === '--campaign' || arg === '--diff-base' || arg === '--diff-head' || arg === '--drift-mode' || arg === '--owner' || arg === '--reason' || arg === '--expires' || arg === '--scope' || arg === '--class') {
|
|
126
138
|
const value = rawArgs[i + 1];
|
|
127
139
|
if (!value || value.startsWith('--')) {
|
|
128
140
|
throw new Error(`${arg} requires a value`);
|
|
@@ -140,7 +152,7 @@ function parseArgs(rawArgs) {
|
|
|
140
152
|
if (arg === '--notes') feedbackNotes = value;
|
|
141
153
|
if (arg === '--source') feedbackSource = value.trim();
|
|
142
154
|
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
143
|
-
if (arg === '--platform') platform = value.trim().toLowerCase();
|
|
155
|
+
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
144
156
|
if (arg === '--format') format = value.trim().toLowerCase();
|
|
145
157
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
146
158
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
@@ -154,6 +166,16 @@ function parseArgs(rawArgs) {
|
|
|
154
166
|
if (arg === '--team-profile') teamProfile = value.trim();
|
|
155
167
|
if (arg === '--lang') lang = value.trim().toLowerCase();
|
|
156
168
|
if (arg === '--tag') snapshotTags.push(value.trim());
|
|
169
|
+
if (arg === '--milestone') snapshotMilestone = value.trim().toLowerCase();
|
|
170
|
+
if (arg === '--campaign') campaigns = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
171
|
+
if (arg === '--diff-base') diffBase = value.trim();
|
|
172
|
+
if (arg === '--diff-head') diffHead = value.trim();
|
|
173
|
+
if (arg === '--drift-mode') driftMode = value.trim().toLowerCase();
|
|
174
|
+
if (arg === '--owner') exceptionOwner = value.trim();
|
|
175
|
+
if (arg === '--reason') exceptionReason = value;
|
|
176
|
+
if (arg === '--expires') exceptionExpires = value.trim();
|
|
177
|
+
if (arg === '--scope') exceptionScope = value.trim().toLowerCase();
|
|
178
|
+
if (arg === '--class') exceptionClass = value.trim().toLowerCase();
|
|
157
179
|
i++;
|
|
158
180
|
continue;
|
|
159
181
|
}
|
|
@@ -177,6 +199,56 @@ function parseArgs(rawArgs) {
|
|
|
177
199
|
snapshotTags.push(arg.split('=').slice(1).join('=').trim());
|
|
178
200
|
continue;
|
|
179
201
|
}
|
|
202
|
+
|
|
203
|
+
if (arg.startsWith('--milestone=')) {
|
|
204
|
+
snapshotMilestone = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (arg.startsWith('--campaign=')) {
|
|
209
|
+
campaigns = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (arg.startsWith('--diff-base=')) {
|
|
214
|
+
diffBase = arg.split('=').slice(1).join('=').trim();
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (arg.startsWith('--diff-head=')) {
|
|
219
|
+
diffHead = arg.split('=').slice(1).join('=').trim();
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (arg.startsWith('--drift-mode=')) {
|
|
224
|
+
driftMode = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (arg.startsWith('--owner=')) {
|
|
229
|
+
exceptionOwner = arg.split('=').slice(1).join('=').trim();
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (arg.startsWith('--reason=')) {
|
|
234
|
+
exceptionReason = arg.split('=').slice(1).join('=');
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (arg.startsWith('--expires=')) {
|
|
239
|
+
exceptionExpires = arg.split('=').slice(1).join('=').trim();
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (arg.startsWith('--scope=')) {
|
|
244
|
+
exceptionScope = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (arg.startsWith('--class=')) {
|
|
249
|
+
exceptionClass = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
180
252
|
|
|
181
253
|
if (arg === '--repos') {
|
|
182
254
|
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
@@ -259,10 +331,11 @@ function parseArgs(rawArgs) {
|
|
|
259
331
|
continue;
|
|
260
332
|
}
|
|
261
333
|
|
|
262
|
-
if (arg.startsWith('--platform=')) {
|
|
263
|
-
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
264
|
-
|
|
265
|
-
|
|
334
|
+
if (arg.startsWith('--platform=')) {
|
|
335
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
336
|
+
platformExplicit = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
266
339
|
|
|
267
340
|
if (arg.startsWith('--format=')) {
|
|
268
341
|
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -315,7 +388,7 @@ function parseArgs(rawArgs) {
|
|
|
315
388
|
|
|
316
389
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
317
390
|
|
|
318
|
-
return { flags, command, commandExplicit, 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, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags };
|
|
391
|
+
return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags, snapshotMilestone, campaigns, diffBase, diffHead, driftMode, exceptionOwner, exceptionReason, exceptionExpires, exceptionScope, exceptionClass };
|
|
319
392
|
}
|
|
320
393
|
|
|
321
394
|
function printWorkspaceSummary(summary, options) {
|
|
@@ -373,16 +446,19 @@ function printCompareCheckSection(title, items, prefix) {
|
|
|
373
446
|
}
|
|
374
447
|
|
|
375
448
|
function printScanDetail(summary, options) {
|
|
376
|
-
if (options.json) {
|
|
377
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
378
|
-
return;
|
|
449
|
+
if (options.json) {
|
|
450
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
451
|
+
return;
|
|
379
452
|
}
|
|
380
453
|
|
|
381
|
-
console.log('');
|
|
382
|
-
console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
|
|
383
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
384
|
-
console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
385
|
-
|
|
454
|
+
console.log('');
|
|
455
|
+
console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
|
|
456
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
457
|
+
console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
458
|
+
if (summary.scoreSemantics?.note) {
|
|
459
|
+
console.log(` Score semantics: ${summary.scoreSemantics.note}`);
|
|
460
|
+
}
|
|
461
|
+
console.log('');
|
|
386
462
|
|
|
387
463
|
for (const item of summary.repos) {
|
|
388
464
|
if (item.error) {
|
|
@@ -391,9 +467,12 @@ function printScanDetail(summary, options) {
|
|
|
391
467
|
continue;
|
|
392
468
|
}
|
|
393
469
|
const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
|
|
394
|
-
console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
|
|
395
|
-
|
|
396
|
-
|
|
470
|
+
console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
|
|
471
|
+
if (item.policyCoverage?.layerKeys?.length > 0) {
|
|
472
|
+
console.log(` \x1b[2mPolicy layers: ${item.policyCoverage.layerKeys.join(' -> ')}\x1b[0m`);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Show per-category breakdown if result is available
|
|
397
476
|
if (item.result && item.result.results) {
|
|
398
477
|
const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
399
478
|
const categories = {};
|
|
@@ -422,28 +501,57 @@ function printScanDetail(summary, options) {
|
|
|
422
501
|
}
|
|
423
502
|
}
|
|
424
503
|
|
|
425
|
-
function printOrgSummary(summary, options) {
|
|
426
|
-
if (options.json) {
|
|
427
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
428
|
-
return;
|
|
429
|
-
}
|
|
504
|
+
function printOrgSummary(summary, options) {
|
|
505
|
+
if (options.json) {
|
|
506
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
430
509
|
|
|
431
510
|
console.log('');
|
|
432
|
-
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
433
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
434
|
-
console.log(` Platform: ${summary.platform}`);
|
|
435
|
-
console.log(` Repos: ${summary.repoCount}`);
|
|
436
|
-
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
511
|
+
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
512
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
513
|
+
console.log(` Platform: ${summary.platform}`);
|
|
514
|
+
console.log(` Repos: ${summary.repoCount}`);
|
|
515
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
516
|
+
if (summary.scoreSemantics?.note) {
|
|
517
|
+
console.log(` Score semantics: ${summary.scoreSemantics.note}`);
|
|
518
|
+
}
|
|
519
|
+
if (summary.policyCoverage) {
|
|
520
|
+
console.log(` Policy coverage: org=${summary.policyCoverage.orgPolicyRepos} team=${summary.policyCoverage.teamPolicyRepos} repo=${summary.policyCoverage.repoPolicyRepos}`);
|
|
521
|
+
}
|
|
522
|
+
if (summary.scoreBands) {
|
|
523
|
+
console.log(` Bands: strong=${summary.scoreBands.strong} developing=${summary.scoreBands.developing} bootstrap=${summary.scoreBands.bootstrap} unknown=${summary.scoreBands.unknown}`);
|
|
524
|
+
}
|
|
525
|
+
console.log('');
|
|
526
|
+
console.log('\x1b[1m Repo Platform Score Policy Top action\x1b[0m');
|
|
527
|
+
console.log(' ' + '─'.repeat(72));
|
|
528
|
+
for (const item of summary.repos) {
|
|
529
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
530
|
+
const topAction = item.error || item.topAction || '-';
|
|
531
|
+
const policy = item.policyCoverage?.layerKeys?.length > 0 ? item.policyCoverage.layerKeys.join('/') : '-';
|
|
532
|
+
console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${policy.padEnd(12)} ${topAction}`);
|
|
533
|
+
}
|
|
534
|
+
if (Array.isArray(summary.topEvidence) && summary.topEvidence.length > 0) {
|
|
535
|
+
console.log('');
|
|
536
|
+
console.log(' Common top evidence:');
|
|
537
|
+
for (const item of summary.topEvidence) {
|
|
538
|
+
console.log(` - ${item.key} (${item.repoCount} repos)`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
console.log('');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function writeStdout(text) {
|
|
545
|
+
return new Promise((resolve, reject) => {
|
|
546
|
+
process.stdout.write(text, (error) => {
|
|
547
|
+
if (error) {
|
|
548
|
+
reject(error);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
resolve();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
}
|
|
447
555
|
|
|
448
556
|
const HELP = `
|
|
449
557
|
nerviq v${version}
|
|
@@ -457,19 +565,22 @@ const HELP = `
|
|
|
457
565
|
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
458
566
|
nerviq audit --json Machine-readable JSON output (for CI)
|
|
459
567
|
nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
|
|
460
|
-
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
461
|
-
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
568
|
+
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
569
|
+
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
570
|
+
nerviq org policy [dir] Inspect resolved org/team/repo policy layers
|
|
462
571
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
463
572
|
nerviq catalog --json Export full check catalog as JSON
|
|
464
573
|
nerviq anti-patterns Detect anti-patterns in current project
|
|
465
574
|
nerviq anti-patterns --all Show full anti-pattern catalog
|
|
466
575
|
|
|
467
|
-
SETUP
|
|
468
|
-
nerviq setup Generate starter-safe baseline config files
|
|
469
|
-
nerviq setup --auto Apply all generated files without prompts
|
|
470
|
-
nerviq interactive Step-by-step guided wizard
|
|
471
|
-
nerviq
|
|
472
|
-
nerviq
|
|
576
|
+
SETUP
|
|
577
|
+
nerviq setup Generate starter-safe baseline config files
|
|
578
|
+
nerviq setup --auto Apply all generated files without prompts
|
|
579
|
+
nerviq interactive Step-by-step guided wizard
|
|
580
|
+
nerviq baseline init Lock the first managed Nerviq baseline for continuous ops
|
|
581
|
+
nerviq baseline status Show the current managed baseline contract
|
|
582
|
+
nerviq check-health Detect regressions + platform format changes between snapshots
|
|
583
|
+
nerviq doctor Self-diagnostics: Node, deps, freshness, MCP, hook runtime
|
|
473
584
|
|
|
474
585
|
FIX
|
|
475
586
|
nerviq fix Show fixable checks and manual-fix guidance
|
|
@@ -482,13 +593,15 @@ const HELP = `
|
|
|
482
593
|
nerviq rollback --list Show available rollback points
|
|
483
594
|
nerviq rollback --dry-run Preview what would be deleted
|
|
484
595
|
|
|
485
|
-
IMPROVE
|
|
486
|
-
nerviq augment Improvement plan (no writes)
|
|
487
|
-
nerviq suggest-only Structured report for sharing (no writes)
|
|
488
|
-
nerviq plan Export proposal bundles with diffs
|
|
489
|
-
nerviq plan --
|
|
490
|
-
nerviq
|
|
491
|
-
nerviq apply
|
|
596
|
+
IMPROVE
|
|
597
|
+
nerviq augment Improvement plan (no writes)
|
|
598
|
+
nerviq suggest-only Structured report for sharing (no writes)
|
|
599
|
+
nerviq plan Export proposal bundles with diffs
|
|
600
|
+
nerviq plan --campaign X Export a named upgrade campaign slice
|
|
601
|
+
nerviq plan --out plan.json Save plan to file
|
|
602
|
+
nerviq apply Apply proposals selectively with rollback
|
|
603
|
+
nerviq apply --campaign X Apply a named upgrade campaign
|
|
604
|
+
nerviq apply --dry-run Preview changes without writing
|
|
492
605
|
|
|
493
606
|
GOVERN
|
|
494
607
|
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
@@ -509,17 +622,24 @@ const HELP = `
|
|
|
509
622
|
nerviq migrate --platform X Platform version migration helper
|
|
510
623
|
nerviq migrate --platform cursor --from v2 --to v3
|
|
511
624
|
|
|
512
|
-
MONITOR
|
|
625
|
+
MONITOR
|
|
513
626
|
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
514
|
-
nerviq dashboard --out F Save dashboard to custom file
|
|
515
|
-
nerviq dashboard --open Open dashboard in browser after generating
|
|
516
|
-
nerviq watch Live config monitoring (re-audits on file change)
|
|
627
|
+
nerviq dashboard --out F Save dashboard to custom file
|
|
628
|
+
nerviq dashboard --open Open dashboard in browser after generating
|
|
629
|
+
nerviq watch Live config monitoring (re-audits on file change)
|
|
630
|
+
nerviq audit --diff-only --drift-mode ci PR / CI drift review against the managed baseline
|
|
517
631
|
nerviq history Audit snapshot history from saved snapshots
|
|
518
632
|
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
519
633
|
nerviq trend Audit snapshot trend over time
|
|
520
634
|
nerviq trend --out report.md Export trend report as markdown
|
|
521
|
-
nerviq audit --snapshot --tag "
|
|
635
|
+
nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
|
|
522
636
|
nerviq feedback Record recommendation outcomes
|
|
637
|
+
|
|
638
|
+
EXCEPTIONS
|
|
639
|
+
nerviq exception add --key permissionDeny --owner team --reason "migration in progress" --expires 2026-05-01
|
|
640
|
+
nerviq exception add --class policy-drift --scope ci --owner team --reason "temporary rollout" --expires 2026-05-01
|
|
641
|
+
nerviq exception list Show active and expired exceptions
|
|
642
|
+
nerviq exception prune Remove expired exceptions
|
|
523
643
|
|
|
524
644
|
TEAM PROFILES
|
|
525
645
|
nerviq profile save <name> Save current preferences as a named profile
|
|
@@ -528,8 +648,9 @@ const HELP = `
|
|
|
528
648
|
nerviq profile export <name> Export profile JSON for sharing
|
|
529
649
|
|
|
530
650
|
ADVANCED
|
|
531
|
-
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
532
|
-
nerviq
|
|
651
|
+
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
652
|
+
nerviq deep-review --behavioral Local behavioral drift review (opt-in, no API)
|
|
653
|
+
nerviq serve --port 3000 Start local Nerviq HTTP API server + OpenAPI contract
|
|
533
654
|
nerviq badge Generate shields.io badge markdown
|
|
534
655
|
nerviq rules-export Export recommendation rules as JSON
|
|
535
656
|
nerviq rules-export --out F Save rules to file
|
|
@@ -553,8 +674,14 @@ const HELP = `
|
|
|
553
674
|
--external PATH Benchmark an external repo instead of cwd
|
|
554
675
|
--port N Port for \`serve\` (default: 3000)
|
|
555
676
|
--workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
|
|
677
|
+
--diff-only Audit only changed files / linked config surfaces from git diff
|
|
678
|
+
--drift-mode M Continuous posture mode: ci | pr | watch
|
|
679
|
+
--diff-base SHA Base SHA for diff-only mode (defaults to PR env vars when present)
|
|
680
|
+
--diff-head SHA Head SHA for diff-only mode (defaults to GITHUB_SHA or HEAD)
|
|
556
681
|
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
557
682
|
--tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
|
|
683
|
+
--milestone NAME Snapshot lifecycle milestone: baseline | post-fix | pre-upgrade | release
|
|
684
|
+
--campaign A,B Limit plan/apply to named upgrade campaigns
|
|
558
685
|
--full Show full audit output (all checks, weakest areas, badge)
|
|
559
686
|
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
560
687
|
--dry-run Preview changes without writing files
|
|
@@ -565,26 +692,38 @@ const HELP = `
|
|
|
565
692
|
--auto Apply all generated files without prompting
|
|
566
693
|
--beginner Show only the 5 starter commands for first-time users
|
|
567
694
|
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
568
|
-
--status VALUE Feedback: accepted | rejected | deferred
|
|
569
|
-
--effect VALUE Feedback: positive | neutral | negative
|
|
570
|
-
--score-delta N Feedback: observed score delta
|
|
571
|
-
--
|
|
572
|
-
--
|
|
695
|
+
--status VALUE Feedback: accepted | rejected | deferred
|
|
696
|
+
--effect VALUE Feedback: positive | neutral | negative
|
|
697
|
+
--score-delta N Feedback: observed score delta
|
|
698
|
+
--owner NAME Exception owner
|
|
699
|
+
--reason TEXT Exception reason
|
|
700
|
+
--expires DATE Exception expiry (ISO date or date-time)
|
|
701
|
+
--scope NAME Exception scope: all | ci | watch | pr
|
|
702
|
+
--class NAME Exception target class: policy-drift | config-drift | platform-drift | maturity-opportunity
|
|
703
|
+
--behavioral Run the opt-in local behavioral drift / outcome-layer review
|
|
704
|
+
--history With deep-review --behavioral, show behavioral snapshot history
|
|
705
|
+
--compare With deep-review --behavioral, compare the latest two behavioral snapshots
|
|
706
|
+
--help Show this help
|
|
707
|
+
--version Show version
|
|
573
708
|
|
|
574
709
|
EXAMPLES
|
|
575
710
|
npx nerviq --beginner
|
|
576
711
|
npx nerviq
|
|
577
712
|
npx nerviq --lite
|
|
578
713
|
npx nerviq --platform cursor
|
|
579
|
-
npx nerviq audit --workspace packages/*
|
|
580
|
-
npx nerviq
|
|
581
|
-
npx nerviq
|
|
714
|
+
npx nerviq audit --workspace packages/*
|
|
715
|
+
npx nerviq baseline init
|
|
716
|
+
npx nerviq audit --diff-only --drift-mode ci
|
|
717
|
+
npx nerviq --platform codex augment
|
|
718
|
+
npx nerviq org scan ./app ./api ./infra
|
|
719
|
+
npx nerviq org policy
|
|
582
720
|
npx nerviq scan ./app ./api ./infra
|
|
583
721
|
npx nerviq harmony-audit
|
|
584
722
|
npx nerviq convert --from claude --to codex
|
|
585
723
|
npx nerviq migrate --platform cursor --from v2 --to v3
|
|
586
|
-
npx nerviq setup --mcp-pack context7-docs
|
|
587
|
-
npx nerviq
|
|
724
|
+
npx nerviq setup --mcp-pack context7-docs
|
|
725
|
+
npx nerviq plan --campaign governance-hardening
|
|
726
|
+
npx nerviq apply --plan plan.json --only hooks,commands
|
|
588
727
|
npx nerviq serve --port 4000
|
|
589
728
|
npx nerviq --json --threshold 70
|
|
590
729
|
npx nerviq catalog --json --out catalog.json
|
|
@@ -606,7 +745,7 @@ const BEGINNER_HELP = `
|
|
|
606
745
|
nerviq setup Generate a starter-safe baseline
|
|
607
746
|
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
608
747
|
nerviq augment Show an improvement plan without writing
|
|
609
|
-
nerviq doctor Check install health, freshness, and
|
|
748
|
+
nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
|
|
610
749
|
|
|
611
750
|
SIMPLE PATH
|
|
612
751
|
1. nerviq audit
|
|
@@ -670,9 +809,10 @@ async function main() {
|
|
|
670
809
|
only: parsed.only,
|
|
671
810
|
profile: parsed.profile,
|
|
672
811
|
mcpPacks: parsed.mcpPacks,
|
|
673
|
-
require: parsed.requireChecks,
|
|
674
|
-
platform: parsed.platform || 'claude',
|
|
675
|
-
|
|
812
|
+
require: parsed.requireChecks,
|
|
813
|
+
platform: parsed.platform || 'claude',
|
|
814
|
+
platformExplicit: Boolean(parsed.platformExplicit),
|
|
815
|
+
format: parsed.format || null,
|
|
676
816
|
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
677
817
|
workspace: parsed.workspace || null,
|
|
678
818
|
webhookUrl: parsed.webhookUrl || null,
|
|
@@ -681,6 +821,20 @@ async function main() {
|
|
|
681
821
|
lang: parsed.lang || null,
|
|
682
822
|
external: parsed.external || null,
|
|
683
823
|
snapshotTags: parsed.snapshotTags || [],
|
|
824
|
+
snapshotMilestone: parsed.snapshotMilestone || null,
|
|
825
|
+
campaigns: parsed.campaigns || [],
|
|
826
|
+
behavioral: flags.includes('--behavioral'),
|
|
827
|
+
historyView: flags.includes('--history'),
|
|
828
|
+
compareView: flags.includes('--compare'),
|
|
829
|
+
diffOnly: flags.includes('--diff-only'),
|
|
830
|
+
diffBase: parsed.diffBase || null,
|
|
831
|
+
diffHead: parsed.diffHead || null,
|
|
832
|
+
driftMode: parsed.driftMode || null,
|
|
833
|
+
exceptionOwner: parsed.exceptionOwner || null,
|
|
834
|
+
exceptionReason: parsed.exceptionReason || null,
|
|
835
|
+
exceptionExpires: parsed.exceptionExpires || null,
|
|
836
|
+
exceptionScope: parsed.exceptionScope || null,
|
|
837
|
+
exceptionClass: parsed.exceptionClass || null,
|
|
684
838
|
dir: process.cwd()
|
|
685
839
|
};
|
|
686
840
|
|
|
@@ -688,18 +842,49 @@ async function main() {
|
|
|
688
842
|
console.error('\n Error: --tag requires --snapshot.\n');
|
|
689
843
|
process.exit(1);
|
|
690
844
|
}
|
|
845
|
+
|
|
846
|
+
if (options.snapshotMilestone && !options.snapshot) {
|
|
847
|
+
console.error('\n Error: --milestone requires --snapshot.\n');
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
|
|
852
|
+
console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
|
|
853
|
+
process.exit(1);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (options.diffOnly && options.snapshot) {
|
|
857
|
+
console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
|
|
862
|
+
console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}
|
|
691
865
|
|
|
692
|
-
if (parsed.checkVersion) {
|
|
866
|
+
if (parsed.checkVersion) {
|
|
693
867
|
if (parsed.checkVersion !== version) {
|
|
694
868
|
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
695
869
|
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
696
870
|
console.error('');
|
|
697
871
|
}
|
|
698
|
-
options.checkVersion = parsed.checkVersion;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
872
|
+
options.checkVersion = parsed.checkVersion;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const {
|
|
876
|
+
resolvePolicyLayers,
|
|
877
|
+
applyPolicyLayersToOptions,
|
|
878
|
+
formatPolicyContract,
|
|
879
|
+
} = require('../src/policy-layers');
|
|
880
|
+
const inheritedPolicyContract = resolvePolicyLayers(options.dir);
|
|
881
|
+
if (inheritedPolicyContract.layers.some((layer) => layer.valid)) {
|
|
882
|
+
Object.assign(options, applyPolicyLayersToOptions(inheritedPolicyContract, options));
|
|
883
|
+
options.policyContract = inheritedPolicyContract;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (parsed.teamProfile) {
|
|
887
|
+
const { loadProfile, applyProfileToOptions } = require('../src/profiles');
|
|
703
888
|
try {
|
|
704
889
|
const teamProf = loadProfile(options.dir, parsed.teamProfile);
|
|
705
890
|
const merged = applyProfileToOptions(teamProf, options);
|
|
@@ -738,10 +923,15 @@ async function main() {
|
|
|
738
923
|
process.exit(1);
|
|
739
924
|
}
|
|
740
925
|
|
|
741
|
-
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
742
|
-
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
743
|
-
process.exit(1);
|
|
744
|
-
}
|
|
926
|
+
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
927
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
928
|
+
process.exit(1);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (options.driftMode && options.format !== null) {
|
|
932
|
+
console.error('\n Error: --drift-mode is only supported with normal text output or --json.\n');
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
745
935
|
|
|
746
936
|
if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
|
|
747
937
|
console.error('\n Error: --port must be an integer between 0 and 65535.\n');
|
|
@@ -791,13 +981,13 @@ async function main() {
|
|
|
791
981
|
}
|
|
792
982
|
|
|
793
983
|
try {
|
|
794
|
-
const FULL_COMMAND_SET = new Set([
|
|
795
|
-
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
796
|
-
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
797
|
-
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
|
|
798
|
-
// Harmony + Synergy (cross-platform)
|
|
799
|
-
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
800
|
-
'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
984
|
+
const FULL_COMMAND_SET = new Set([
|
|
985
|
+
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
986
|
+
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
987
|
+
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'baseline', 'exception', 'help', 'version',
|
|
988
|
+
// Harmony + Synergy (cross-platform)
|
|
989
|
+
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
990
|
+
'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
801
991
|
'freshness', 'profile', 'migrate',
|
|
802
992
|
]);
|
|
803
993
|
|
|
@@ -843,33 +1033,51 @@ async function main() {
|
|
|
843
1033
|
}
|
|
844
1034
|
}
|
|
845
1035
|
|
|
846
|
-
if (normalizedCommand === 'scan') {
|
|
847
|
-
const scanDirs = parsed.extraArgs;
|
|
848
|
-
if (scanDirs.length === 0) {
|
|
849
|
-
console.error('\n Error: scan requires at least one directory argument.');
|
|
850
|
-
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
851
|
-
process.exit(1);
|
|
852
|
-
}
|
|
853
|
-
const summary = await scanOrg(scanDirs, options
|
|
854
|
-
printScanDetail(summary, options);
|
|
855
|
-
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
856
|
-
process.exit(1);
|
|
857
|
-
}
|
|
858
|
-
process.exit(0);
|
|
859
|
-
} else if (normalizedCommand === 'org') {
|
|
860
|
-
const subcommand = parsed.extraArgs[0];
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1036
|
+
if (normalizedCommand === 'scan') {
|
|
1037
|
+
const scanDirs = parsed.extraArgs;
|
|
1038
|
+
if (scanDirs.length === 0) {
|
|
1039
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
1040
|
+
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1044
|
+
printScanDetail(summary, options);
|
|
1045
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
process.exit(0);
|
|
1049
|
+
} else if (normalizedCommand === 'org') {
|
|
1050
|
+
const subcommand = parsed.extraArgs[0];
|
|
1051
|
+
if (subcommand === 'policy') {
|
|
1052
|
+
const targetDir = parsed.extraArgs[1] ? require('path').resolve(parsed.extraArgs[1]) : options.dir;
|
|
1053
|
+
const contract = resolvePolicyLayers(targetDir);
|
|
1054
|
+
if (options.json) {
|
|
1055
|
+
await writeStdout(JSON.stringify(contract, null, 2) + '\n');
|
|
1056
|
+
} else {
|
|
1057
|
+
console.log('');
|
|
1058
|
+
console.log(formatPolicyContract(contract));
|
|
1059
|
+
console.log('');
|
|
1060
|
+
}
|
|
1061
|
+
process.exit(0);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const scanDirs = parsed.extraArgs.slice(1);
|
|
1065
|
+
if (subcommand !== 'scan' || scanDirs.length === 0) {
|
|
1066
|
+
console.error('\n Error: org requires `scan` or `policy`.');
|
|
1067
|
+
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
|
|
1068
|
+
console.error(' npx nerviq org policy [dir]\n');
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1072
|
+
if (options.json) {
|
|
1073
|
+
await writeStdout(JSON.stringify(summary, null, 2) + '\n');
|
|
1074
|
+
} else {
|
|
1075
|
+
printOrgSummary(summary, options);
|
|
1076
|
+
}
|
|
1077
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
process.exit(0);
|
|
873
1081
|
} else if (normalizedCommand === 'history') {
|
|
874
1082
|
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
875
1083
|
// Handle --prune N
|
|
@@ -900,7 +1108,7 @@ async function main() {
|
|
|
900
1108
|
console.log('');
|
|
901
1109
|
process.exit(0);
|
|
902
1110
|
} else if (normalizedCommand === 'compare') {
|
|
903
|
-
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags } = require('../src/activity');
|
|
1111
|
+
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
|
|
904
1112
|
const result = compareLatest(options.dir);
|
|
905
1113
|
if (!result) {
|
|
906
1114
|
console.log('');
|
|
@@ -913,8 +1121,8 @@ async function main() {
|
|
|
913
1121
|
} else {
|
|
914
1122
|
const sign = result.delta.score >= 0 ? '+' : '';
|
|
915
1123
|
console.log('');
|
|
916
|
-
console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotTags(result.previous.tags)}`);
|
|
917
|
-
console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotTags(result.current.tags)}`);
|
|
1124
|
+
console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotMilestone(result.previous.milestone)}${formatSnapshotTags(result.previous.tags)}`);
|
|
1125
|
+
console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotMilestone(result.current.milestone)}${formatSnapshotTags(result.current.tags)}`);
|
|
918
1126
|
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
919
1127
|
console.log(` Trend: ${result.trend}`);
|
|
920
1128
|
if (result.detailedDiffAvailable) {
|
|
@@ -1066,6 +1274,7 @@ async function main() {
|
|
|
1066
1274
|
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
1067
1275
|
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1068
1276
|
tags: options.snapshotTags,
|
|
1277
|
+
milestone: options.snapshotMilestone,
|
|
1069
1278
|
sourceCommand: normalizedCommand,
|
|
1070
1279
|
}) : null;
|
|
1071
1280
|
if (options.out && !options.json) {
|
|
@@ -1200,6 +1409,7 @@ async function main() {
|
|
|
1200
1409
|
printGovernanceSummary(summary, options);
|
|
1201
1410
|
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1202
1411
|
tags: options.snapshotTags,
|
|
1412
|
+
milestone: options.snapshotMilestone,
|
|
1203
1413
|
sourceCommand: normalizedCommand,
|
|
1204
1414
|
}) : null;
|
|
1205
1415
|
if (options.out && !options.json) {
|
|
@@ -1215,6 +1425,7 @@ async function main() {
|
|
|
1215
1425
|
const report = await runBenchmark(options);
|
|
1216
1426
|
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1217
1427
|
tags: options.snapshotTags,
|
|
1428
|
+
milestone: options.snapshotMilestone,
|
|
1218
1429
|
sourceCommand: normalizedCommand,
|
|
1219
1430
|
}) : null;
|
|
1220
1431
|
if (options.out) {
|
|
@@ -1233,13 +1444,87 @@ async function main() {
|
|
|
1233
1444
|
} else if (normalizedCommand === 'deep-review') {
|
|
1234
1445
|
const { deepReview } = require('../src/deep-review');
|
|
1235
1446
|
await deepReview(options);
|
|
1236
|
-
} else if (normalizedCommand === 'interactive') {
|
|
1237
|
-
const { interactive } = require('../src/interactive');
|
|
1238
|
-
await interactive(options);
|
|
1239
|
-
} else if (normalizedCommand === '
|
|
1240
|
-
const {
|
|
1241
|
-
|
|
1242
|
-
|
|
1447
|
+
} else if (normalizedCommand === 'interactive') {
|
|
1448
|
+
const { interactive } = require('../src/interactive');
|
|
1449
|
+
await interactive(options);
|
|
1450
|
+
} else if (normalizedCommand === 'baseline') {
|
|
1451
|
+
const {
|
|
1452
|
+
readManagedBaseline,
|
|
1453
|
+
writeManagedBaseline,
|
|
1454
|
+
buildManagedBaselineRecord,
|
|
1455
|
+
formatManagedBaselineStatus,
|
|
1456
|
+
} = require('../src/continuous-ops');
|
|
1457
|
+
const subcommand = parsed.extraArgs[0] || 'status';
|
|
1458
|
+
|
|
1459
|
+
if (subcommand === 'status') {
|
|
1460
|
+
const baseline = readManagedBaseline(options.dir);
|
|
1461
|
+
if (options.json) {
|
|
1462
|
+
console.log(JSON.stringify(baseline, null, 2));
|
|
1463
|
+
} else {
|
|
1464
|
+
console.log('');
|
|
1465
|
+
console.log(formatManagedBaselineStatus(options.dir, baseline));
|
|
1466
|
+
console.log('');
|
|
1467
|
+
}
|
|
1468
|
+
process.exit(0);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (subcommand === 'init') {
|
|
1472
|
+
const existingBaseline = readManagedBaseline(options.dir);
|
|
1473
|
+
if (existingBaseline && !flags.includes('--force')) {
|
|
1474
|
+
console.error('\n Error: Managed baseline already exists. Use `nerviq baseline status` to inspect it, or rerun with --force to replace it.\n');
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const auditResult = await audit({ ...options, silent: true });
|
|
1479
|
+
const analysisReport = await analyzeProject({ ...options, mode: 'augment' });
|
|
1480
|
+
const detectedPlatforms = detectPlatforms(options.dir);
|
|
1481
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', auditResult, {
|
|
1482
|
+
tags: [...options.snapshotTags, 'baseline'],
|
|
1483
|
+
milestone: 'baseline',
|
|
1484
|
+
sourceCommand: 'baseline init',
|
|
1485
|
+
managedBaseline: true,
|
|
1486
|
+
});
|
|
1487
|
+
const baselineRecord = buildManagedBaselineRecord({
|
|
1488
|
+
dir: options.dir,
|
|
1489
|
+
platform: options.platform,
|
|
1490
|
+
auditResult,
|
|
1491
|
+
analysisReport,
|
|
1492
|
+
snapshotArtifact: snapshot,
|
|
1493
|
+
currentPlatforms: detectedPlatforms,
|
|
1494
|
+
});
|
|
1495
|
+
const saved = writeManagedBaseline(options.dir, baselineRecord);
|
|
1496
|
+
|
|
1497
|
+
if (options.json) {
|
|
1498
|
+
console.log(JSON.stringify({
|
|
1499
|
+
...baselineRecord,
|
|
1500
|
+
baselinePath: saved.relativePath,
|
|
1501
|
+
}, null, 2));
|
|
1502
|
+
} else {
|
|
1503
|
+
console.log('');
|
|
1504
|
+
console.log(' nerviq baseline init');
|
|
1505
|
+
console.log(' ═══════════════════════════════════════');
|
|
1506
|
+
console.log(` Managed baseline written: ${saved.relativePath}`);
|
|
1507
|
+
console.log(` Snapshot: ${snapshot.relativePath}`);
|
|
1508
|
+
console.log(` Score: ${baselineRecord.baselineAudit.score}/100`);
|
|
1509
|
+
console.log(` Operating profile: ${baselineRecord.operatingProfile.label || 'n/a'}`);
|
|
1510
|
+
console.log(` Adoption plan: ${baselineRecord.adoptionPlan || 'n/a'}`);
|
|
1511
|
+
console.log(` Active platforms: ${(baselineRecord.detectedPlatforms || []).join(', ') || 'none detected'}`);
|
|
1512
|
+
console.log('');
|
|
1513
|
+
console.log(' Next:');
|
|
1514
|
+
console.log(' - nerviq audit --diff-only --drift-mode ci');
|
|
1515
|
+
console.log(' - nerviq watch');
|
|
1516
|
+
console.log(' - nerviq plan --campaign governance-hardening');
|
|
1517
|
+
console.log('');
|
|
1518
|
+
}
|
|
1519
|
+
process.exit(0);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
console.error('\n Error: baseline supports `init` and `status`.\n');
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
} else if (normalizedCommand === 'watch') {
|
|
1525
|
+
const { watch } = require('../src/watch');
|
|
1526
|
+
await watch(options);
|
|
1527
|
+
} else if (normalizedCommand === 'catalog') {
|
|
1243
1528
|
const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
1244
1529
|
if (options.out) {
|
|
1245
1530
|
const result = writeCatalogJson(options.out);
|
|
@@ -1313,6 +1598,7 @@ async function main() {
|
|
|
1313
1598
|
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1314
1599
|
console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1315
1600
|
console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
|
|
1601
|
+
console.log(' MCP hosts should use nerviq-mcp (stdio JSON-RPC 2.0), not this HTTP server.');
|
|
1316
1602
|
console.log('');
|
|
1317
1603
|
|
|
1318
1604
|
const closeServer = () => {
|
|
@@ -1538,18 +1824,74 @@ async function main() {
|
|
|
1538
1824
|
}
|
|
1539
1825
|
}
|
|
1540
1826
|
process.exit(0);
|
|
1541
|
-
} else if (normalizedCommand === 'suggest-rules') {
|
|
1542
|
-
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1543
|
-
const suggestions = analyzeSuggestions(options.dir);
|
|
1544
|
-
if (options.json) {
|
|
1545
|
-
console.log(JSON.stringify(suggestions, null, 2));
|
|
1546
|
-
} else {
|
|
1547
|
-
console.log('');
|
|
1548
|
-
console.log(formatSuggestions(suggestions));
|
|
1549
|
-
console.log('');
|
|
1550
|
-
}
|
|
1551
|
-
process.exit(0);
|
|
1552
|
-
} else if (normalizedCommand === '
|
|
1827
|
+
} else if (normalizedCommand === 'suggest-rules') {
|
|
1828
|
+
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1829
|
+
const suggestions = analyzeSuggestions(options.dir);
|
|
1830
|
+
if (options.json) {
|
|
1831
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
1832
|
+
} else {
|
|
1833
|
+
console.log('');
|
|
1834
|
+
console.log(formatSuggestions(suggestions));
|
|
1835
|
+
console.log('');
|
|
1836
|
+
}
|
|
1837
|
+
process.exit(0);
|
|
1838
|
+
} else if (normalizedCommand === 'exception') {
|
|
1839
|
+
const {
|
|
1840
|
+
listExceptions,
|
|
1841
|
+
addException,
|
|
1842
|
+
pruneExpiredExceptions,
|
|
1843
|
+
formatExceptionsList,
|
|
1844
|
+
} = require('../src/continuous-ops');
|
|
1845
|
+
const subcommand = parsed.extraArgs[0] || 'list';
|
|
1846
|
+
|
|
1847
|
+
if (subcommand === 'list') {
|
|
1848
|
+
const records = listExceptions(options.dir);
|
|
1849
|
+
if (options.json) {
|
|
1850
|
+
console.log(JSON.stringify(records, null, 2));
|
|
1851
|
+
} else {
|
|
1852
|
+
console.log('');
|
|
1853
|
+
console.log(formatExceptionsList(records));
|
|
1854
|
+
console.log('');
|
|
1855
|
+
}
|
|
1856
|
+
process.exit(0);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
if (subcommand === 'add') {
|
|
1860
|
+
const result = addException(options.dir, {
|
|
1861
|
+
key: parsed.feedbackKey || null,
|
|
1862
|
+
watchClass: options.exceptionClass,
|
|
1863
|
+
owner: options.exceptionOwner,
|
|
1864
|
+
reason: options.exceptionReason,
|
|
1865
|
+
expiresAt: options.exceptionExpires,
|
|
1866
|
+
scope: options.exceptionScope || 'all',
|
|
1867
|
+
});
|
|
1868
|
+
if (options.json) {
|
|
1869
|
+
console.log(JSON.stringify(result.record, null, 2));
|
|
1870
|
+
} else {
|
|
1871
|
+
console.log('');
|
|
1872
|
+
console.log(` Exception added: ${result.record.id}`);
|
|
1873
|
+
console.log(` Target: ${result.record.key || result.record.watchClass}`);
|
|
1874
|
+
console.log(` Owner: ${result.record.owner}`);
|
|
1875
|
+
console.log(` Scope: ${result.record.scope}`);
|
|
1876
|
+
console.log(` Expires: ${result.record.expiresAt}`);
|
|
1877
|
+
console.log('');
|
|
1878
|
+
}
|
|
1879
|
+
process.exit(0);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (subcommand === 'prune') {
|
|
1883
|
+
const result = pruneExpiredExceptions(options.dir);
|
|
1884
|
+
if (options.json) {
|
|
1885
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1886
|
+
} else {
|
|
1887
|
+
console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
|
|
1888
|
+
}
|
|
1889
|
+
process.exit(0);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
|
|
1893
|
+
process.exit(1);
|
|
1894
|
+
} else if (normalizedCommand === 'profile') {
|
|
1553
1895
|
const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
|
|
1554
1896
|
const subcommand = parsed.extraArgs[0];
|
|
1555
1897
|
const profileArg = parsed.extraArgs[1];
|
|
@@ -2095,27 +2437,113 @@ async function main() {
|
|
|
2095
2437
|
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2096
2438
|
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
2097
2439
|
tags: options.snapshotTags,
|
|
2440
|
+
milestone: options.snapshotMilestone,
|
|
2098
2441
|
sourceCommand: 'setup',
|
|
2099
2442
|
});
|
|
2100
2443
|
if (!options.json) {
|
|
2101
2444
|
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2102
2445
|
}
|
|
2103
2446
|
}
|
|
2104
|
-
} else {
|
|
2105
|
-
if (options.workspace) {
|
|
2106
|
-
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
2107
|
-
printWorkspaceSummary(summary, options);
|
|
2108
|
-
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
2109
|
-
process.exit(1);
|
|
2110
|
-
}
|
|
2111
|
-
process.exit(0);
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
const
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2447
|
+
} else {
|
|
2448
|
+
if (options.workspace) {
|
|
2449
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
2450
|
+
printWorkspaceSummary(summary, options);
|
|
2451
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
process.exit(0);
|
|
2455
|
+
}
|
|
2456
|
+
let result;
|
|
2457
|
+
const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
|
|
2458
|
+
if (options.diffOnly) {
|
|
2459
|
+
const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
|
|
2460
|
+
const fullResult = await audit({ ...options, silent: true });
|
|
2461
|
+
const diffInfo = getChangedFiles(options.dir, {
|
|
2462
|
+
diffBase: options.diffBase,
|
|
2463
|
+
diffHead: options.diffHead,
|
|
2464
|
+
});
|
|
2465
|
+
result = buildDiffOnlyAuditView(fullResult, diffInfo);
|
|
2466
|
+
} else {
|
|
2467
|
+
result = renderAuditJsonLocally
|
|
2468
|
+
? await audit({ ...options, silent: true })
|
|
2469
|
+
: await audit(options);
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
if (options.driftMode) {
|
|
2473
|
+
const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
|
|
2474
|
+
let campaigns = [];
|
|
2475
|
+
try {
|
|
2476
|
+
const planBundle = await buildProposalBundle({
|
|
2477
|
+
dir: options.dir,
|
|
2478
|
+
platform: options.platform,
|
|
2479
|
+
profile: options.profile,
|
|
2480
|
+
mcpPacks: options.mcpPacks,
|
|
2481
|
+
campaigns: [],
|
|
2482
|
+
});
|
|
2483
|
+
campaigns = planBundle.campaigns || [];
|
|
2484
|
+
} catch {
|
|
2485
|
+
campaigns = [];
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
result = {
|
|
2489
|
+
...result,
|
|
2490
|
+
continuousStatus: buildContinuousStatus({
|
|
2491
|
+
dir: options.dir,
|
|
2492
|
+
auditResult: result,
|
|
2493
|
+
mode: options.driftMode,
|
|
2494
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
2495
|
+
campaigns,
|
|
2496
|
+
}),
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2501
|
+
result = {
|
|
2502
|
+
...result,
|
|
2503
|
+
policyLayers: options.policyContract,
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
if (options.diffOnly) {
|
|
2508
|
+
const { printDiffOnlyAudit } = require('../src/diff-only');
|
|
2509
|
+
if (options.json) {
|
|
2510
|
+
console.log(JSON.stringify({
|
|
2511
|
+
version,
|
|
2512
|
+
timestamp: new Date().toISOString(),
|
|
2513
|
+
...result,
|
|
2514
|
+
}, null, 2));
|
|
2515
|
+
} else {
|
|
2516
|
+
console.log(printDiffOnlyAudit(result));
|
|
2517
|
+
if (result.continuousStatus) {
|
|
2518
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2519
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2520
|
+
console.log('');
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
} else if (renderAuditJsonLocally) {
|
|
2524
|
+
console.log(JSON.stringify({
|
|
2525
|
+
version,
|
|
2526
|
+
timestamp: new Date().toISOString(),
|
|
2527
|
+
...result,
|
|
2528
|
+
}, null, 2));
|
|
2529
|
+
} else {
|
|
2530
|
+
if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2531
|
+
console.log('');
|
|
2532
|
+
console.log(formatPolicyContract(options.policyContract));
|
|
2533
|
+
console.log('');
|
|
2534
|
+
}
|
|
2535
|
+
if (!options.json && result.continuousStatus) {
|
|
2536
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2537
|
+
console.log('');
|
|
2538
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2539
|
+
console.log('');
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (options.out) {
|
|
2543
|
+
const fs = require('fs');
|
|
2544
|
+
const path = require('path');
|
|
2545
|
+
const outPath = path.resolve(options.out);
|
|
2546
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2119
2547
|
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
2120
2548
|
if (!options.json) {
|
|
2121
2549
|
console.log(`\n Audit report written to ${options.out}\n`);
|
|
@@ -2123,20 +2551,19 @@ async function main() {
|
|
|
2123
2551
|
}
|
|
2124
2552
|
if (options.webhookUrl) {
|
|
2125
2553
|
try {
|
|
2126
|
-
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
|
2554
|
+
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2127
2555
|
// Auto-detect Slack vs generic by URL pattern
|
|
2128
2556
|
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
2129
2557
|
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
2130
2558
|
let payload;
|
|
2131
|
-
if (isSlack) {
|
|
2132
|
-
payload = formatSlackMessage(result);
|
|
2133
|
-
} else if (isDiscord) {
|
|
2134
|
-
const { formatDiscordMessage } = require('../src/integrations');
|
|
2135
|
-
payload = formatDiscordMessage(result);
|
|
2136
|
-
} else {
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
}
|
|
2559
|
+
if (isSlack) {
|
|
2560
|
+
payload = formatSlackMessage(result);
|
|
2561
|
+
} else if (isDiscord) {
|
|
2562
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
2563
|
+
payload = formatDiscordMessage(result);
|
|
2564
|
+
} else {
|
|
2565
|
+
payload = formatGenericAuditWebhookEvent(result);
|
|
2566
|
+
}
|
|
2140
2567
|
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2141
2568
|
headers: options.webhookHeaders,
|
|
2142
2569
|
retries: options.webhookRetries,
|
|
@@ -2178,6 +2605,7 @@ async function main() {
|
|
|
2178
2605
|
}
|
|
2179
2606
|
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2180
2607
|
tags: options.snapshotTags,
|
|
2608
|
+
milestone: options.snapshotMilestone,
|
|
2181
2609
|
sourceCommand: normalizedCommand,
|
|
2182
2610
|
}) : null;
|
|
2183
2611
|
if (snapshot && !options.json) {
|
|
@@ -2185,18 +2613,27 @@ async function main() {
|
|
|
2185
2613
|
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
2186
2614
|
console.log('');
|
|
2187
2615
|
}
|
|
2188
|
-
if (options.threshold !== null && result.score < options.threshold) {
|
|
2189
|
-
if (!options.json) {
|
|
2190
|
-
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2191
|
-
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2192
|
-
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2616
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
2617
|
+
if (!options.json) {
|
|
2618
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2619
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2620
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2193
2621
|
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
2194
|
-
}
|
|
2195
|
-
process.exit(1);
|
|
2196
|
-
}
|
|
2197
|
-
if (
|
|
2198
|
-
|
|
2199
|
-
|
|
2622
|
+
}
|
|
2623
|
+
process.exit(1);
|
|
2624
|
+
}
|
|
2625
|
+
if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
|
|
2626
|
+
if (!options.json) {
|
|
2627
|
+
console.error('\n Error: Continuous drift gate failed.');
|
|
2628
|
+
console.error(` Why: ${result.continuousStatus.gateLabel}.`);
|
|
2629
|
+
console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
|
|
2630
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
2631
|
+
}
|
|
2632
|
+
process.exit(1);
|
|
2633
|
+
}
|
|
2634
|
+
if (options.require && options.require.length > 0) {
|
|
2635
|
+
const failedRequired = options.require.filter(key => {
|
|
2636
|
+
const check = result.results.find(r => r.key === key);
|
|
2200
2637
|
return !check || check.passed !== true;
|
|
2201
2638
|
});
|
|
2202
2639
|
if (failedRequired.length > 0) {
|