@nerviq/cli 1.10.0 → 1.12.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 +176 -47
- package/bin/cli.js +842 -287
- package/package.json +2 -2
- package/src/activity.js +225 -59
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +28 -25
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +131 -1
- package/src/anti-patterns.js +17 -2
- package/src/audit.js +197 -96
- package/src/behavioral-drift.js +801 -0
- package/src/benchmark.js +15 -10
- package/src/continuous-ops.js +681 -0
- package/src/cost-tracking.js +61 -0
- 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/governance.js +72 -50
- package/src/hook-validation.js +342 -0
- package/src/index.js +7 -1
- package/src/integrations.js +144 -60
- package/src/mcp-validation.js +337 -0
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/permission-rules.js +218 -0
- 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/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +36 -2
- 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 -5611
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/watch.js +18 -0
- package/src/windsurf/techniques.js +17 -12
- package/src/workspace.js +105 -8
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));
|
|
@@ -47,7 +48,7 @@ function levenshtein(a, b) {
|
|
|
47
48
|
return matrix[a.length][b.length];
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function suggestCommand(input) {
|
|
51
|
+
function suggestCommand(input) {
|
|
51
52
|
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
52
53
|
let best = null;
|
|
53
54
|
let bestDistance = Infinity;
|
|
@@ -58,9 +59,30 @@ function suggestCommand(input) {
|
|
|
58
59
|
bestDistance = distance;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
|
-
return bestDistance <= 3 ? best : null;
|
|
62
|
-
}
|
|
63
|
-
|
|
62
|
+
return bestDistance <= 3 ? best : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseNonNegativeIntegerFlag(value, flagName) {
|
|
66
|
+
const parsed = Number(value);
|
|
67
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
68
|
+
throw new Error(`${flagName} requires a non-negative integer`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseWebhookHeader(rawValue) {
|
|
74
|
+
const separator = rawValue.indexOf(':');
|
|
75
|
+
if (separator <= 0) {
|
|
76
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
77
|
+
}
|
|
78
|
+
const name = rawValue.slice(0, separator).trim();
|
|
79
|
+
const value = rawValue.slice(separator + 1).trim();
|
|
80
|
+
if (!name || !value) {
|
|
81
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
82
|
+
}
|
|
83
|
+
return { name, value };
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
function parseArgs(rawArgs) {
|
|
65
87
|
const flags = [];
|
|
66
88
|
let command = 'audit';
|
|
@@ -76,13 +98,27 @@ function parseArgs(rawArgs) {
|
|
|
76
98
|
let feedbackEffect = null;
|
|
77
99
|
let feedbackNotes = null;
|
|
78
100
|
let feedbackSource = null;
|
|
79
|
-
let feedbackScoreDelta = null;
|
|
80
|
-
let platform = 'claude';
|
|
81
|
-
let
|
|
82
|
-
let
|
|
83
|
-
let
|
|
84
|
-
let
|
|
85
|
-
let
|
|
101
|
+
let feedbackScoreDelta = null;
|
|
102
|
+
let platform = 'claude';
|
|
103
|
+
let platformExplicit = false;
|
|
104
|
+
let format = null;
|
|
105
|
+
let port = null;
|
|
106
|
+
let workspace = null;
|
|
107
|
+
let webhookUrl = null;
|
|
108
|
+
let webhookHeaders = [];
|
|
109
|
+
let webhookRetries = null;
|
|
110
|
+
let snapshotTags = [];
|
|
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;
|
|
86
122
|
let extraArgs = [];
|
|
87
123
|
let convertFrom = null;
|
|
88
124
|
let convertTo = null;
|
|
@@ -98,11 +134,11 @@ function parseArgs(rawArgs) {
|
|
|
98
134
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
99
135
|
const arg = rawArgs[i];
|
|
100
136
|
|
|
101
|
-
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 === '--external' || arg === '--team-profile' || arg === '--lang') {
|
|
102
|
-
const value = rawArgs[i + 1];
|
|
103
|
-
if (!value || value.startsWith('--')) {
|
|
104
|
-
throw new Error(`${arg} requires a value`);
|
|
105
|
-
}
|
|
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') {
|
|
138
|
+
const value = rawArgs[i + 1];
|
|
139
|
+
if (!value || value.startsWith('--')) {
|
|
140
|
+
throw new Error(`${arg} requires a value`);
|
|
141
|
+
}
|
|
106
142
|
if (arg === '--threshold') threshold = value;
|
|
107
143
|
if (arg === '--out') out = value;
|
|
108
144
|
if (arg === '--plan') planFile = value;
|
|
@@ -116,20 +152,33 @@ function parseArgs(rawArgs) {
|
|
|
116
152
|
if (arg === '--notes') feedbackNotes = value;
|
|
117
153
|
if (arg === '--source') feedbackSource = value.trim();
|
|
118
154
|
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
119
|
-
if (arg === '--platform') platform = value.trim().toLowerCase();
|
|
155
|
+
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
120
156
|
if (arg === '--format') format = value.trim().toLowerCase();
|
|
121
157
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
122
158
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
123
159
|
if (arg === '--port') port = value.trim();
|
|
124
|
-
if (arg === '--workspace') workspace = value.trim();
|
|
125
|
-
if (arg === '--check-version') checkVersion = value.trim();
|
|
126
|
-
if (arg === '--webhook') webhookUrl = value.trim();
|
|
127
|
-
if (arg === '--
|
|
128
|
-
if (arg === '--
|
|
129
|
-
if (arg === '--
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
160
|
+
if (arg === '--workspace') workspace = value.trim();
|
|
161
|
+
if (arg === '--check-version') checkVersion = value.trim();
|
|
162
|
+
if (arg === '--webhook') webhookUrl = value.trim();
|
|
163
|
+
if (arg === '--webhook-header') webhookHeaders.push(parseWebhookHeader(value));
|
|
164
|
+
if (arg === '--webhook-retries') webhookRetries = parseNonNegativeIntegerFlag(value.trim(), '--webhook-retries');
|
|
165
|
+
if (arg === '--external') external = value.trim();
|
|
166
|
+
if (arg === '--team-profile') teamProfile = value.trim();
|
|
167
|
+
if (arg === '--lang') lang = value.trim().toLowerCase();
|
|
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();
|
|
179
|
+
i++;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
133
182
|
|
|
134
183
|
if (arg.startsWith('--lang=')) {
|
|
135
184
|
lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -141,10 +190,65 @@ function parseArgs(rawArgs) {
|
|
|
141
190
|
continue;
|
|
142
191
|
}
|
|
143
192
|
|
|
144
|
-
if (arg.startsWith('--external=')) {
|
|
145
|
-
external = arg.split('=').slice(1).join('=').trim();
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
193
|
+
if (arg.startsWith('--external=')) {
|
|
194
|
+
external = arg.split('=').slice(1).join('=').trim();
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (arg.startsWith('--tag=')) {
|
|
199
|
+
snapshotTags.push(arg.split('=').slice(1).join('=').trim());
|
|
200
|
+
continue;
|
|
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
|
+
}
|
|
148
252
|
|
|
149
253
|
if (arg === '--repos') {
|
|
150
254
|
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
@@ -227,10 +331,11 @@ function parseArgs(rawArgs) {
|
|
|
227
331
|
continue;
|
|
228
332
|
}
|
|
229
333
|
|
|
230
|
-
if (arg.startsWith('--platform=')) {
|
|
231
|
-
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
232
|
-
|
|
233
|
-
|
|
334
|
+
if (arg.startsWith('--platform=')) {
|
|
335
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
336
|
+
platformExplicit = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
234
339
|
|
|
235
340
|
if (arg.startsWith('--format=')) {
|
|
236
341
|
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -247,14 +352,29 @@ function parseArgs(rawArgs) {
|
|
|
247
352
|
continue;
|
|
248
353
|
}
|
|
249
354
|
|
|
250
|
-
if (arg.startsWith('--check-version=')) {
|
|
251
|
-
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (arg.startsWith('--')) {
|
|
256
|
-
|
|
257
|
-
continue;
|
|
355
|
+
if (arg.startsWith('--check-version=')) {
|
|
356
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (arg.startsWith('--webhook=')) {
|
|
361
|
+
webhookUrl = arg.split('=').slice(1).join('=').trim();
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (arg.startsWith('--webhook-header=')) {
|
|
366
|
+
webhookHeaders.push(parseWebhookHeader(arg.split('=').slice(1).join('=')));
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (arg.startsWith('--webhook-retries=')) {
|
|
371
|
+
webhookRetries = parseNonNegativeIntegerFlag(arg.split('=').slice(1).join('=').trim(), '--webhook-retries');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (arg.startsWith('--')) {
|
|
376
|
+
flags.push(arg);
|
|
377
|
+
continue;
|
|
258
378
|
}
|
|
259
379
|
|
|
260
380
|
if (!commandSet) {
|
|
@@ -268,7 +388,7 @@ function parseArgs(rawArgs) {
|
|
|
268
388
|
|
|
269
389
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
270
390
|
|
|
271
|
-
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, external, repos, teamProfile, lang };
|
|
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 };
|
|
272
392
|
}
|
|
273
393
|
|
|
274
394
|
function printWorkspaceSummary(summary, options) {
|
|
@@ -291,30 +411,54 @@ function printWorkspaceSummary(summary, options) {
|
|
|
291
411
|
}
|
|
292
412
|
console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
|
|
293
413
|
console.log(` Workspace audit average: \x1b[1m${workspaceAverage}/100\x1b[0m`);
|
|
414
|
+
if (summary.profileBreakdown?.length > 0) {
|
|
415
|
+
const profileLine = summary.profileBreakdown
|
|
416
|
+
.map((item) => `${item.profileLabel} (${item.workspaceCount})`)
|
|
417
|
+
.join(', ');
|
|
418
|
+
console.log(` Workspace profiles: ${profileLine}`);
|
|
419
|
+
}
|
|
294
420
|
console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
|
|
295
421
|
console.log(' Aggregate vs package: per-workspace scores can legitimately trail the root repo score in a monorepo.');
|
|
422
|
+
console.log(' Stack-specific checks: Go, Python, Node, and other workspace types can have different applicable totals.');
|
|
296
423
|
console.log('');
|
|
297
|
-
console.log('\x1b[1m Workspace Audit Pass Total Top action\x1b[0m');
|
|
298
|
-
console.log(' ' + '─'.repeat(
|
|
424
|
+
console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
|
|
425
|
+
console.log(' ' + '─'.repeat(96));
|
|
299
426
|
for (const item of summary.workspaces) {
|
|
300
427
|
const score = item.score === null ? 'ERR' : String(item.score);
|
|
301
|
-
const topAction = item.error || item.topAction || '-';
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
428
|
+
const topAction = item.error || item.topAction || '-';
|
|
429
|
+
const profile = (item.workspaceProfile?.label || 'General workspace').slice(0, 20);
|
|
430
|
+
console.log(` ${item.workspace.padEnd(26)} ${profile.padEnd(20)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
|
|
431
|
+
if (item.stackLabels?.length > 0) {
|
|
432
|
+
console.log(`\x1b[2m Stacks: ${item.stackLabels.join(', ')}\x1b[0m`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
console.log('');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function printCompareCheckSection(title, items, prefix) {
|
|
439
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
440
|
+
console.log(` ${title} (${items.length}):`);
|
|
441
|
+
for (const item of items) {
|
|
442
|
+
const impact = item.impact ? ` [${item.impact}]` : '';
|
|
443
|
+
const category = item.category ? ` — ${item.category}` : '';
|
|
444
|
+
console.log(` ${prefix} ${item.key}${impact}: ${item.name}${category}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function printScanDetail(summary, options) {
|
|
449
|
+
if (options.json) {
|
|
450
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
451
|
+
return;
|
|
311
452
|
}
|
|
312
453
|
|
|
313
|
-
console.log('');
|
|
314
|
-
console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
|
|
315
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
316
|
-
console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
317
|
-
|
|
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('');
|
|
318
462
|
|
|
319
463
|
for (const item of summary.repos) {
|
|
320
464
|
if (item.error) {
|
|
@@ -323,9 +467,12 @@ function printScanDetail(summary, options) {
|
|
|
323
467
|
continue;
|
|
324
468
|
}
|
|
325
469
|
const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
|
|
326
|
-
console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
|
|
327
|
-
|
|
328
|
-
|
|
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
|
|
329
476
|
if (item.result && item.result.results) {
|
|
330
477
|
const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
331
478
|
const categories = {};
|
|
@@ -354,28 +501,57 @@ function printScanDetail(summary, options) {
|
|
|
354
501
|
}
|
|
355
502
|
}
|
|
356
503
|
|
|
357
|
-
function printOrgSummary(summary, options) {
|
|
358
|
-
if (options.json) {
|
|
359
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
504
|
+
function printOrgSummary(summary, options) {
|
|
505
|
+
if (options.json) {
|
|
506
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
362
509
|
|
|
363
510
|
console.log('');
|
|
364
|
-
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
365
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
366
|
-
console.log(` Platform: ${summary.platform}`);
|
|
367
|
-
console.log(` Repos: ${summary.repoCount}`);
|
|
368
|
-
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
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
|
+
}
|
|
379
555
|
|
|
380
556
|
const HELP = `
|
|
381
557
|
nerviq v${version}
|
|
@@ -388,20 +564,23 @@ const HELP = `
|
|
|
388
564
|
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
389
565
|
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
390
566
|
nerviq audit --json Machine-readable JSON output (for CI)
|
|
391
|
-
nerviq audit --workspace packages/* Audit monorepo workspaces with
|
|
392
|
-
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
393
|
-
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
567
|
+
nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
|
|
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
|
|
394
571
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
395
572
|
nerviq catalog --json Export full check catalog as JSON
|
|
396
573
|
nerviq anti-patterns Detect anti-patterns in current project
|
|
397
574
|
nerviq anti-patterns --all Show full anti-pattern catalog
|
|
398
575
|
|
|
399
|
-
SETUP
|
|
400
|
-
nerviq setup Generate starter-safe baseline config files
|
|
401
|
-
nerviq setup --auto Apply all generated files without prompts
|
|
402
|
-
nerviq interactive Step-by-step guided wizard
|
|
403
|
-
nerviq
|
|
404
|
-
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
|
|
405
584
|
|
|
406
585
|
FIX
|
|
407
586
|
nerviq fix Show fixable checks and manual-fix guidance
|
|
@@ -414,16 +593,18 @@ const HELP = `
|
|
|
414
593
|
nerviq rollback --list Show available rollback points
|
|
415
594
|
nerviq rollback --dry-run Preview what would be deleted
|
|
416
595
|
|
|
417
|
-
IMPROVE
|
|
418
|
-
nerviq augment Improvement plan (no writes)
|
|
419
|
-
nerviq suggest-only Structured report for sharing (no writes)
|
|
420
|
-
nerviq plan Export proposal bundles with diffs
|
|
421
|
-
nerviq plan --
|
|
422
|
-
nerviq
|
|
423
|
-
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
|
|
424
605
|
|
|
425
606
|
GOVERN
|
|
426
|
-
nerviq governance Permission profiles + hooks + policy packs
|
|
607
|
+
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
427
608
|
nerviq governance --json Machine-readable governance summary
|
|
428
609
|
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
429
610
|
nerviq benchmark --external /path Benchmark an external repo
|
|
@@ -441,16 +622,24 @@ const HELP = `
|
|
|
441
622
|
nerviq migrate --platform X Platform version migration helper
|
|
442
623
|
nerviq migrate --platform cursor --from v2 --to v3
|
|
443
624
|
|
|
444
|
-
MONITOR
|
|
625
|
+
MONITOR
|
|
445
626
|
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
446
|
-
nerviq dashboard --out F Save dashboard to custom file
|
|
447
|
-
nerviq dashboard --open Open dashboard in browser after generating
|
|
448
|
-
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
|
|
449
631
|
nerviq history Audit snapshot history from saved snapshots
|
|
450
|
-
nerviq compare
|
|
632
|
+
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
451
633
|
nerviq trend Audit snapshot trend over time
|
|
452
|
-
nerviq trend --out report.md Export trend report as markdown
|
|
453
|
-
nerviq
|
|
634
|
+
nerviq trend --out report.md Export trend report as markdown
|
|
635
|
+
nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
|
|
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
|
|
454
643
|
|
|
455
644
|
TEAM PROFILES
|
|
456
645
|
nerviq profile save <name> Save current preferences as a named profile
|
|
@@ -459,8 +648,9 @@ const HELP = `
|
|
|
459
648
|
nerviq profile export <name> Export profile JSON for sharing
|
|
460
649
|
|
|
461
650
|
ADVANCED
|
|
462
|
-
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
463
|
-
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 REST API server + OpenAPI contract
|
|
464
654
|
nerviq badge Generate shields.io badge markdown
|
|
465
655
|
nerviq rules-export Export recommendation rules as JSON
|
|
466
656
|
nerviq rules-export --out F Save rules to file
|
|
@@ -475,15 +665,24 @@ const HELP = `
|
|
|
475
665
|
--only A,B Limit plan/apply to selected proposal IDs
|
|
476
666
|
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
477
667
|
--team-profile N Load a saved team profile for audit (overrides threshold/platform)
|
|
478
|
-
--mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
|
|
479
|
-
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
480
|
-
--format NAME Output format: json | sarif | otel
|
|
481
|
-
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
482
|
-
--
|
|
483
|
-
--
|
|
484
|
-
--
|
|
485
|
-
--
|
|
486
|
-
--
|
|
668
|
+
--mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
|
|
669
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
670
|
+
--format NAME Output format: json | sarif | otel
|
|
671
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
672
|
+
--webhook-header H Add a custom webhook header (repeat; format: Name: Value)
|
|
673
|
+
--webhook-retries N Retry transient webhook failures N times (default: 2)
|
|
674
|
+
--external PATH Benchmark an external repo instead of cwd
|
|
675
|
+
--port N Port for \`serve\` (default: 3000)
|
|
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)
|
|
681
|
+
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
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
|
|
685
|
+
--full Show full audit output (all checks, weakest areas, badge)
|
|
487
686
|
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
488
687
|
--dry-run Preview changes without writing files
|
|
489
688
|
--config-only Only write config files (.claude/, rules, hooks) — never source code
|
|
@@ -493,26 +692,38 @@ const HELP = `
|
|
|
493
692
|
--auto Apply all generated files without prompting
|
|
494
693
|
--beginner Show only the 5 starter commands for first-time users
|
|
495
694
|
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
496
|
-
--status VALUE Feedback: accepted | rejected | deferred
|
|
497
|
-
--effect VALUE Feedback: positive | neutral | negative
|
|
498
|
-
--score-delta N Feedback: observed score delta
|
|
499
|
-
--
|
|
500
|
-
--
|
|
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
|
|
501
708
|
|
|
502
709
|
EXAMPLES
|
|
503
710
|
npx nerviq --beginner
|
|
504
711
|
npx nerviq
|
|
505
712
|
npx nerviq --lite
|
|
506
713
|
npx nerviq --platform cursor
|
|
507
|
-
npx nerviq audit --workspace packages/*
|
|
508
|
-
npx nerviq
|
|
509
|
-
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
|
|
510
720
|
npx nerviq scan ./app ./api ./infra
|
|
511
721
|
npx nerviq harmony-audit
|
|
512
722
|
npx nerviq convert --from claude --to codex
|
|
513
723
|
npx nerviq migrate --platform cursor --from v2 --to v3
|
|
514
|
-
npx nerviq setup --mcp-pack context7-docs
|
|
515
|
-
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
|
|
516
727
|
npx nerviq serve --port 4000
|
|
517
728
|
npx nerviq --json --threshold 70
|
|
518
729
|
npx nerviq catalog --json --out catalog.json
|
|
@@ -534,7 +745,7 @@ const BEGINNER_HELP = `
|
|
|
534
745
|
nerviq setup Generate a starter-safe baseline
|
|
535
746
|
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
536
747
|
nerviq augment Show an improvement plan without writing
|
|
537
|
-
nerviq doctor Check install health, freshness, and
|
|
748
|
+
nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
|
|
538
749
|
|
|
539
750
|
SIMPLE PATH
|
|
540
751
|
1. nerviq audit
|
|
@@ -579,7 +790,7 @@ async function main() {
|
|
|
579
790
|
process.exit(0);
|
|
580
791
|
}
|
|
581
792
|
|
|
582
|
-
const options = {
|
|
793
|
+
const options = {
|
|
583
794
|
verbose: flags.includes('--verbose'),
|
|
584
795
|
json: flags.includes('--json'),
|
|
585
796
|
auto: flags.includes('--auto'),
|
|
@@ -598,28 +809,82 @@ async function main() {
|
|
|
598
809
|
only: parsed.only,
|
|
599
810
|
profile: parsed.profile,
|
|
600
811
|
mcpPacks: parsed.mcpPacks,
|
|
601
|
-
require: parsed.requireChecks,
|
|
602
|
-
platform: parsed.platform || 'claude',
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
812
|
+
require: parsed.requireChecks,
|
|
813
|
+
platform: parsed.platform || 'claude',
|
|
814
|
+
platformExplicit: Boolean(parsed.platformExplicit),
|
|
815
|
+
format: parsed.format || null,
|
|
816
|
+
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
817
|
+
workspace: parsed.workspace || null,
|
|
818
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
819
|
+
webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
|
|
820
|
+
webhookRetries: parsed.webhookRetries ?? 2,
|
|
821
|
+
lang: parsed.lang || null,
|
|
822
|
+
external: parsed.external || null,
|
|
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,
|
|
838
|
+
dir: process.cwd()
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
if (options.snapshotTags.length > 0 && !options.snapshot) {
|
|
842
|
+
console.error('\n Error: --tag requires --snapshot.\n');
|
|
843
|
+
process.exit(1);
|
|
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
|
+
}
|
|
865
|
+
|
|
866
|
+
if (parsed.checkVersion) {
|
|
613
867
|
if (parsed.checkVersion !== version) {
|
|
614
868
|
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
615
869
|
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
616
870
|
console.error('');
|
|
617
871
|
}
|
|
618
|
-
options.checkVersion = parsed.checkVersion;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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');
|
|
623
888
|
try {
|
|
624
889
|
const teamProf = loadProfile(options.dir, parsed.teamProfile);
|
|
625
890
|
const merged = applyProfileToOptions(teamProf, options);
|
|
@@ -658,10 +923,15 @@ async function main() {
|
|
|
658
923
|
process.exit(1);
|
|
659
924
|
}
|
|
660
925
|
|
|
661
|
-
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
662
|
-
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
663
|
-
process.exit(1);
|
|
664
|
-
}
|
|
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
|
+
}
|
|
665
935
|
|
|
666
936
|
if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
|
|
667
937
|
console.error('\n Error: --port must be an integer between 0 and 65535.\n');
|
|
@@ -711,13 +981,13 @@ async function main() {
|
|
|
711
981
|
}
|
|
712
982
|
|
|
713
983
|
try {
|
|
714
|
-
const FULL_COMMAND_SET = new Set([
|
|
715
|
-
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
716
|
-
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
717
|
-
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
|
|
718
|
-
// Harmony + Synergy (cross-platform)
|
|
719
|
-
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
720
|
-
'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',
|
|
721
991
|
'freshness', 'profile', 'migrate',
|
|
722
992
|
]);
|
|
723
993
|
|
|
@@ -763,33 +1033,51 @@ async function main() {
|
|
|
763
1033
|
}
|
|
764
1034
|
}
|
|
765
1035
|
|
|
766
|
-
if (normalizedCommand === 'scan') {
|
|
767
|
-
const scanDirs = parsed.extraArgs;
|
|
768
|
-
if (scanDirs.length === 0) {
|
|
769
|
-
console.error('\n Error: scan requires at least one directory argument.');
|
|
770
|
-
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
771
|
-
process.exit(1);
|
|
772
|
-
}
|
|
773
|
-
const summary = await scanOrg(scanDirs, options
|
|
774
|
-
printScanDetail(summary, options);
|
|
775
|
-
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
776
|
-
process.exit(1);
|
|
777
|
-
}
|
|
778
|
-
process.exit(0);
|
|
779
|
-
} else if (normalizedCommand === 'org') {
|
|
780
|
-
const subcommand = parsed.extraArgs[0];
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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);
|
|
793
1081
|
} else if (normalizedCommand === 'history') {
|
|
794
1082
|
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
795
1083
|
// Handle --prune N
|
|
@@ -820,7 +1108,7 @@ async function main() {
|
|
|
820
1108
|
console.log('');
|
|
821
1109
|
process.exit(0);
|
|
822
1110
|
} else if (normalizedCommand === 'compare') {
|
|
823
|
-
const { compareLatest, formatSnapshotBootstrap } = require('../src/activity');
|
|
1111
|
+
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
|
|
824
1112
|
const result = compareLatest(options.dir);
|
|
825
1113
|
if (!result) {
|
|
826
1114
|
console.log('');
|
|
@@ -830,16 +1118,41 @@ async function main() {
|
|
|
830
1118
|
}
|
|
831
1119
|
if (options.json) {
|
|
832
1120
|
console.log(JSON.stringify(result, null, 2));
|
|
833
|
-
} else {
|
|
834
|
-
const sign = result.delta.score >= 0 ? '+' : '';
|
|
835
|
-
console.log('');
|
|
836
|
-
console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
|
|
837
|
-
console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
|
|
1121
|
+
} else {
|
|
1122
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
1123
|
+
console.log('');
|
|
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)}`);
|
|
838
1126
|
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
839
1127
|
console.log(` Trend: ${result.trend}`);
|
|
840
|
-
if (result.
|
|
841
|
-
|
|
842
|
-
|
|
1128
|
+
if (result.detailedDiffAvailable) {
|
|
1129
|
+
console.log('');
|
|
1130
|
+
console.log(' Detailed check diff:');
|
|
1131
|
+
printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
|
|
1132
|
+
printCompareCheckSection('Improvements', result.improvementDetails, '✅');
|
|
1133
|
+
printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
|
|
1134
|
+
printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
|
|
1135
|
+
if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
|
|
1136
|
+
printCompareCheckSection('New checks', result.newChecks, '➕');
|
|
1137
|
+
}
|
|
1138
|
+
if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
|
|
1139
|
+
printCompareCheckSection('Removed checks', result.removedChecks, '➖');
|
|
1140
|
+
}
|
|
1141
|
+
if (
|
|
1142
|
+
result.regressionDetails.length === 0 &&
|
|
1143
|
+
result.improvementDetails.length === 0 &&
|
|
1144
|
+
result.newlyApplicableDetails.length === 0 &&
|
|
1145
|
+
result.noLongerApplicableDetails.length === 0 &&
|
|
1146
|
+
result.newChecks.length === 0 &&
|
|
1147
|
+
result.removedChecks.length === 0
|
|
1148
|
+
) {
|
|
1149
|
+
console.log(' No per-check state changes detected.');
|
|
1150
|
+
}
|
|
1151
|
+
} else {
|
|
1152
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
1153
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
1154
|
+
}
|
|
1155
|
+
console.log('');
|
|
843
1156
|
}
|
|
844
1157
|
process.exit(0);
|
|
845
1158
|
} else if (normalizedCommand === 'trend') {
|
|
@@ -959,9 +1272,11 @@ async function main() {
|
|
|
959
1272
|
process.exit(0);
|
|
960
1273
|
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
961
1274
|
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
962
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
963
|
-
|
|
964
|
-
|
|
1275
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1276
|
+
tags: options.snapshotTags,
|
|
1277
|
+
milestone: options.snapshotMilestone,
|
|
1278
|
+
sourceCommand: normalizedCommand,
|
|
1279
|
+
}) : null;
|
|
965
1280
|
if (options.out && !options.json) {
|
|
966
1281
|
const fs = require('fs');
|
|
967
1282
|
const md = exportMarkdown(report);
|
|
@@ -1092,9 +1407,11 @@ async function main() {
|
|
|
1092
1407
|
fs.writeFileSync(options.out, content, 'utf8');
|
|
1093
1408
|
}
|
|
1094
1409
|
printGovernanceSummary(summary, options);
|
|
1095
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1096
|
-
|
|
1097
|
-
|
|
1410
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1411
|
+
tags: options.snapshotTags,
|
|
1412
|
+
milestone: options.snapshotMilestone,
|
|
1413
|
+
sourceCommand: normalizedCommand,
|
|
1414
|
+
}) : null;
|
|
1098
1415
|
if (options.out && !options.json) {
|
|
1099
1416
|
console.log(` Governance report written to ${options.out}`);
|
|
1100
1417
|
console.log('');
|
|
@@ -1106,9 +1423,11 @@ async function main() {
|
|
|
1106
1423
|
}
|
|
1107
1424
|
} else if (normalizedCommand === 'benchmark') {
|
|
1108
1425
|
const report = await runBenchmark(options);
|
|
1109
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1110
|
-
|
|
1111
|
-
|
|
1426
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1427
|
+
tags: options.snapshotTags,
|
|
1428
|
+
milestone: options.snapshotMilestone,
|
|
1429
|
+
sourceCommand: normalizedCommand,
|
|
1430
|
+
}) : null;
|
|
1112
1431
|
if (options.out) {
|
|
1113
1432
|
writeBenchmarkReport(report, options.out);
|
|
1114
1433
|
}
|
|
@@ -1125,13 +1444,87 @@ async function main() {
|
|
|
1125
1444
|
} else if (normalizedCommand === 'deep-review') {
|
|
1126
1445
|
const { deepReview } = require('../src/deep-review');
|
|
1127
1446
|
await deepReview(options);
|
|
1128
|
-
} else if (normalizedCommand === 'interactive') {
|
|
1129
|
-
const { interactive } = require('../src/interactive');
|
|
1130
|
-
await interactive(options);
|
|
1131
|
-
} else if (normalizedCommand === '
|
|
1132
|
-
const {
|
|
1133
|
-
|
|
1134
|
-
|
|
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') {
|
|
1135
1528
|
const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
1136
1529
|
if (options.out) {
|
|
1137
1530
|
const result = writeCatalogJson(options.out);
|
|
@@ -1202,9 +1595,10 @@ async function main() {
|
|
|
1202
1595
|
const address = server.address();
|
|
1203
1596
|
const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
|
|
1204
1597
|
console.log('');
|
|
1205
|
-
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1206
|
-
console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1207
|
-
console.log(
|
|
1598
|
+
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1599
|
+
console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1600
|
+
console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
|
|
1601
|
+
console.log('');
|
|
1208
1602
|
|
|
1209
1603
|
const closeServer = () => {
|
|
1210
1604
|
server.close(() => process.exit(0));
|
|
@@ -1429,18 +1823,74 @@ async function main() {
|
|
|
1429
1823
|
}
|
|
1430
1824
|
}
|
|
1431
1825
|
process.exit(0);
|
|
1432
|
-
} else if (normalizedCommand === 'suggest-rules') {
|
|
1433
|
-
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1434
|
-
const suggestions = analyzeSuggestions(options.dir);
|
|
1435
|
-
if (options.json) {
|
|
1436
|
-
console.log(JSON.stringify(suggestions, null, 2));
|
|
1437
|
-
} else {
|
|
1438
|
-
console.log('');
|
|
1439
|
-
console.log(formatSuggestions(suggestions));
|
|
1440
|
-
console.log('');
|
|
1441
|
-
}
|
|
1442
|
-
process.exit(0);
|
|
1443
|
-
} else if (normalizedCommand === '
|
|
1826
|
+
} else if (normalizedCommand === 'suggest-rules') {
|
|
1827
|
+
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1828
|
+
const suggestions = analyzeSuggestions(options.dir);
|
|
1829
|
+
if (options.json) {
|
|
1830
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
1831
|
+
} else {
|
|
1832
|
+
console.log('');
|
|
1833
|
+
console.log(formatSuggestions(suggestions));
|
|
1834
|
+
console.log('');
|
|
1835
|
+
}
|
|
1836
|
+
process.exit(0);
|
|
1837
|
+
} else if (normalizedCommand === 'exception') {
|
|
1838
|
+
const {
|
|
1839
|
+
listExceptions,
|
|
1840
|
+
addException,
|
|
1841
|
+
pruneExpiredExceptions,
|
|
1842
|
+
formatExceptionsList,
|
|
1843
|
+
} = require('../src/continuous-ops');
|
|
1844
|
+
const subcommand = parsed.extraArgs[0] || 'list';
|
|
1845
|
+
|
|
1846
|
+
if (subcommand === 'list') {
|
|
1847
|
+
const records = listExceptions(options.dir);
|
|
1848
|
+
if (options.json) {
|
|
1849
|
+
console.log(JSON.stringify(records, null, 2));
|
|
1850
|
+
} else {
|
|
1851
|
+
console.log('');
|
|
1852
|
+
console.log(formatExceptionsList(records));
|
|
1853
|
+
console.log('');
|
|
1854
|
+
}
|
|
1855
|
+
process.exit(0);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (subcommand === 'add') {
|
|
1859
|
+
const result = addException(options.dir, {
|
|
1860
|
+
key: parsed.feedbackKey || null,
|
|
1861
|
+
watchClass: options.exceptionClass,
|
|
1862
|
+
owner: options.exceptionOwner,
|
|
1863
|
+
reason: options.exceptionReason,
|
|
1864
|
+
expiresAt: options.exceptionExpires,
|
|
1865
|
+
scope: options.exceptionScope || 'all',
|
|
1866
|
+
});
|
|
1867
|
+
if (options.json) {
|
|
1868
|
+
console.log(JSON.stringify(result.record, null, 2));
|
|
1869
|
+
} else {
|
|
1870
|
+
console.log('');
|
|
1871
|
+
console.log(` Exception added: ${result.record.id}`);
|
|
1872
|
+
console.log(` Target: ${result.record.key || result.record.watchClass}`);
|
|
1873
|
+
console.log(` Owner: ${result.record.owner}`);
|
|
1874
|
+
console.log(` Scope: ${result.record.scope}`);
|
|
1875
|
+
console.log(` Expires: ${result.record.expiresAt}`);
|
|
1876
|
+
console.log('');
|
|
1877
|
+
}
|
|
1878
|
+
process.exit(0);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (subcommand === 'prune') {
|
|
1882
|
+
const result = pruneExpiredExceptions(options.dir);
|
|
1883
|
+
if (options.json) {
|
|
1884
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1885
|
+
} else {
|
|
1886
|
+
console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
|
|
1887
|
+
}
|
|
1888
|
+
process.exit(0);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
|
|
1892
|
+
process.exit(1);
|
|
1893
|
+
} else if (normalizedCommand === 'profile') {
|
|
1444
1894
|
const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
|
|
1445
1895
|
const subcommand = parsed.extraArgs[0];
|
|
1446
1896
|
const profileArg = parsed.extraArgs[1];
|
|
@@ -1982,30 +2432,117 @@ async function main() {
|
|
|
1982
2432
|
process.exit(0);
|
|
1983
2433
|
} else if (normalizedCommand === 'setup') {
|
|
1984
2434
|
await setup(options);
|
|
1985
|
-
if (options.snapshot) {
|
|
1986
|
-
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1987
|
-
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
if (options.workspace) {
|
|
1996
|
-
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
1997
|
-
printWorkspaceSummary(summary, options);
|
|
1998
|
-
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1999
|
-
process.exit(1);
|
|
2000
|
-
}
|
|
2001
|
-
process.exit(0);
|
|
2435
|
+
if (options.snapshot) {
|
|
2436
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2437
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
2438
|
+
tags: options.snapshotTags,
|
|
2439
|
+
milestone: options.snapshotMilestone,
|
|
2440
|
+
sourceCommand: 'setup',
|
|
2441
|
+
});
|
|
2442
|
+
if (!options.json) {
|
|
2443
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2444
|
+
}
|
|
2002
2445
|
}
|
|
2003
|
-
|
|
2004
|
-
if (options.
|
|
2005
|
-
const
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2446
|
+
} else {
|
|
2447
|
+
if (options.workspace) {
|
|
2448
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
2449
|
+
printWorkspaceSummary(summary, options);
|
|
2450
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
2453
|
+
process.exit(0);
|
|
2454
|
+
}
|
|
2455
|
+
let result;
|
|
2456
|
+
const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
|
|
2457
|
+
if (options.diffOnly) {
|
|
2458
|
+
const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
|
|
2459
|
+
const fullResult = await audit({ ...options, silent: true });
|
|
2460
|
+
const diffInfo = getChangedFiles(options.dir, {
|
|
2461
|
+
diffBase: options.diffBase,
|
|
2462
|
+
diffHead: options.diffHead,
|
|
2463
|
+
});
|
|
2464
|
+
result = buildDiffOnlyAuditView(fullResult, diffInfo);
|
|
2465
|
+
} else {
|
|
2466
|
+
result = renderAuditJsonLocally
|
|
2467
|
+
? await audit({ ...options, silent: true })
|
|
2468
|
+
: await audit(options);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (options.driftMode) {
|
|
2472
|
+
const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
|
|
2473
|
+
let campaigns = [];
|
|
2474
|
+
try {
|
|
2475
|
+
const planBundle = await buildProposalBundle({
|
|
2476
|
+
dir: options.dir,
|
|
2477
|
+
platform: options.platform,
|
|
2478
|
+
profile: options.profile,
|
|
2479
|
+
mcpPacks: options.mcpPacks,
|
|
2480
|
+
campaigns: [],
|
|
2481
|
+
});
|
|
2482
|
+
campaigns = planBundle.campaigns || [];
|
|
2483
|
+
} catch {
|
|
2484
|
+
campaigns = [];
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
result = {
|
|
2488
|
+
...result,
|
|
2489
|
+
continuousStatus: buildContinuousStatus({
|
|
2490
|
+
dir: options.dir,
|
|
2491
|
+
auditResult: result,
|
|
2492
|
+
mode: options.driftMode,
|
|
2493
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
2494
|
+
campaigns,
|
|
2495
|
+
}),
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2500
|
+
result = {
|
|
2501
|
+
...result,
|
|
2502
|
+
policyLayers: options.policyContract,
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
if (options.diffOnly) {
|
|
2507
|
+
const { printDiffOnlyAudit } = require('../src/diff-only');
|
|
2508
|
+
if (options.json) {
|
|
2509
|
+
console.log(JSON.stringify({
|
|
2510
|
+
version,
|
|
2511
|
+
timestamp: new Date().toISOString(),
|
|
2512
|
+
...result,
|
|
2513
|
+
}, null, 2));
|
|
2514
|
+
} else {
|
|
2515
|
+
console.log(printDiffOnlyAudit(result));
|
|
2516
|
+
if (result.continuousStatus) {
|
|
2517
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2518
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2519
|
+
console.log('');
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
} else if (renderAuditJsonLocally) {
|
|
2523
|
+
console.log(JSON.stringify({
|
|
2524
|
+
version,
|
|
2525
|
+
timestamp: new Date().toISOString(),
|
|
2526
|
+
...result,
|
|
2527
|
+
}, null, 2));
|
|
2528
|
+
} else {
|
|
2529
|
+
if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2530
|
+
console.log('');
|
|
2531
|
+
console.log(formatPolicyContract(options.policyContract));
|
|
2532
|
+
console.log('');
|
|
2533
|
+
}
|
|
2534
|
+
if (!options.json && result.continuousStatus) {
|
|
2535
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2536
|
+
console.log('');
|
|
2537
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2538
|
+
console.log('');
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
if (options.out) {
|
|
2542
|
+
const fs = require('fs');
|
|
2543
|
+
const path = require('path');
|
|
2544
|
+
const outPath = path.resolve(options.out);
|
|
2545
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2009
2546
|
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
2010
2547
|
if (!options.json) {
|
|
2011
2548
|
console.log(`\n Audit report written to ${options.out}\n`);
|
|
@@ -2013,32 +2550,39 @@ async function main() {
|
|
|
2013
2550
|
}
|
|
2014
2551
|
if (options.webhookUrl) {
|
|
2015
2552
|
try {
|
|
2016
|
-
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
|
2553
|
+
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2017
2554
|
// Auto-detect Slack vs generic by URL pattern
|
|
2018
2555
|
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
2019
2556
|
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
2020
2557
|
let payload;
|
|
2021
|
-
if (isSlack) {
|
|
2022
|
-
payload = formatSlackMessage(result);
|
|
2023
|
-
} else if (isDiscord) {
|
|
2024
|
-
const { formatDiscordMessage } = require('../src/integrations');
|
|
2025
|
-
payload = formatDiscordMessage(result);
|
|
2026
|
-
} else {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2558
|
+
if (isSlack) {
|
|
2559
|
+
payload = formatSlackMessage(result);
|
|
2560
|
+
} else if (isDiscord) {
|
|
2561
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
2562
|
+
payload = formatDiscordMessage(result);
|
|
2563
|
+
} else {
|
|
2564
|
+
payload = formatGenericAuditWebhookEvent(result);
|
|
2565
|
+
}
|
|
2566
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2567
|
+
headers: options.webhookHeaders,
|
|
2568
|
+
retries: options.webhookRetries,
|
|
2569
|
+
});
|
|
2570
|
+
if (!options.json) {
|
|
2571
|
+
if (webhookResp.ok) {
|
|
2572
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2573
|
+
console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
|
|
2574
|
+
} else {
|
|
2575
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2576
|
+
console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
} catch (webhookErr) {
|
|
2580
|
+
if (!options.json) {
|
|
2581
|
+
const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
|
|
2582
|
+
console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2042
2586
|
if (options.feedback && !options.json && options.format === null) {
|
|
2043
2587
|
const feedbackTargets = options.lite
|
|
2044
2588
|
? (result.liteSummary?.topNextActions || [])
|
|
@@ -2058,26 +2602,37 @@ async function main() {
|
|
|
2058
2602
|
console.log('');
|
|
2059
2603
|
}
|
|
2060
2604
|
}
|
|
2061
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2062
|
-
|
|
2063
|
-
|
|
2605
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2606
|
+
tags: options.snapshotTags,
|
|
2607
|
+
milestone: options.snapshotMilestone,
|
|
2608
|
+
sourceCommand: normalizedCommand,
|
|
2609
|
+
}) : null;
|
|
2064
2610
|
if (snapshot && !options.json) {
|
|
2065
2611
|
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2066
2612
|
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
2067
2613
|
console.log('');
|
|
2068
2614
|
}
|
|
2069
|
-
if (options.threshold !== null && result.score < options.threshold) {
|
|
2070
|
-
if (!options.json) {
|
|
2071
|
-
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2072
|
-
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2073
|
-
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2615
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
2616
|
+
if (!options.json) {
|
|
2617
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2618
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2619
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2074
2620
|
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
2075
|
-
}
|
|
2076
|
-
process.exit(1);
|
|
2077
|
-
}
|
|
2078
|
-
if (
|
|
2079
|
-
|
|
2080
|
-
|
|
2621
|
+
}
|
|
2622
|
+
process.exit(1);
|
|
2623
|
+
}
|
|
2624
|
+
if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
|
|
2625
|
+
if (!options.json) {
|
|
2626
|
+
console.error('\n Error: Continuous drift gate failed.');
|
|
2627
|
+
console.error(` Why: ${result.continuousStatus.gateLabel}.`);
|
|
2628
|
+
console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
|
|
2629
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
2630
|
+
}
|
|
2631
|
+
process.exit(1);
|
|
2632
|
+
}
|
|
2633
|
+
if (options.require && options.require.length > 0) {
|
|
2634
|
+
const failedRequired = options.require.filter(key => {
|
|
2635
|
+
const check = result.results.find(r => r.key === key);
|
|
2081
2636
|
return !check || check.passed !== true;
|
|
2082
2637
|
});
|
|
2083
2638
|
if (failedRequired.length > 0) {
|