@nerviq/cli 1.9.0 → 1.11.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 +106 -60
- package/bin/cli.js +382 -208
- package/package.json +1 -1
- package/src/activity.js +245 -63
- package/src/aider/freshness.js +149 -146
- package/src/analyze.js +3 -1
- package/src/anti-patterns.js +17 -13
- package/src/audit.js +106 -79
- package/src/auto-suggest.js +62 -9
- package/src/benchmark.js +67 -51
- package/src/dashboard.js +36 -14
- package/src/governance.js +13 -7
- package/src/index.js +2 -1
- package/src/instruction-surfaces.js +185 -0
- package/src/integrations.js +102 -55
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/permission-rules.js +218 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +2 -2
- package/src/stack-checks.js +1 -1
- package/src/synergy/report.js +1 -0
- package/src/techniques.js +102 -103
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/workspace.js +155 -13
package/bin/cli.js
CHANGED
|
@@ -47,7 +47,7 @@ function levenshtein(a, b) {
|
|
|
47
47
|
return matrix[a.length][b.length];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function suggestCommand(input) {
|
|
50
|
+
function suggestCommand(input) {
|
|
51
51
|
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
52
52
|
let best = null;
|
|
53
53
|
let bestDistance = Infinity;
|
|
@@ -58,12 +58,33 @@ function suggestCommand(input) {
|
|
|
58
58
|
bestDistance = distance;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
return bestDistance <= 3 ? best : null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
61
|
+
return bestDistance <= 3 ? best : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseNonNegativeIntegerFlag(value, flagName) {
|
|
65
|
+
const parsed = Number(value);
|
|
66
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
67
|
+
throw new Error(`${flagName} requires a non-negative integer`);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseWebhookHeader(rawValue) {
|
|
73
|
+
const separator = rawValue.indexOf(':');
|
|
74
|
+
if (separator <= 0) {
|
|
75
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
76
|
+
}
|
|
77
|
+
const name = rawValue.slice(0, separator).trim();
|
|
78
|
+
const value = rawValue.slice(separator + 1).trim();
|
|
79
|
+
if (!name || !value) {
|
|
80
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
81
|
+
}
|
|
82
|
+
return { name, value };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseArgs(rawArgs) {
|
|
86
|
+
const flags = [];
|
|
87
|
+
let command = 'audit';
|
|
67
88
|
let threshold = null;
|
|
68
89
|
let out = null;
|
|
69
90
|
let planFile = null;
|
|
@@ -79,9 +100,12 @@ function parseArgs(rawArgs) {
|
|
|
79
100
|
let feedbackScoreDelta = null;
|
|
80
101
|
let platform = 'claude';
|
|
81
102
|
let format = null;
|
|
82
|
-
let port = null;
|
|
83
|
-
let workspace = null;
|
|
84
|
-
let webhookUrl = null;
|
|
103
|
+
let port = null;
|
|
104
|
+
let workspace = null;
|
|
105
|
+
let webhookUrl = null;
|
|
106
|
+
let webhookHeaders = [];
|
|
107
|
+
let webhookRetries = null;
|
|
108
|
+
let snapshotTags = [];
|
|
85
109
|
let commandSet = false;
|
|
86
110
|
let extraArgs = [];
|
|
87
111
|
let convertFrom = null;
|
|
@@ -89,19 +113,20 @@ function parseArgs(rawArgs) {
|
|
|
89
113
|
let migrateFrom = null;
|
|
90
114
|
let migrateTo = null;
|
|
91
115
|
let checkVersion = null;
|
|
92
|
-
let external = null;
|
|
93
|
-
let repos = [];
|
|
94
|
-
let teamProfile = null;
|
|
95
|
-
let lang = null;
|
|
116
|
+
let external = null;
|
|
117
|
+
let repos = [];
|
|
118
|
+
let teamProfile = null;
|
|
119
|
+
let lang = null;
|
|
120
|
+
let commandExplicit = false;
|
|
96
121
|
|
|
97
122
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
98
123
|
const arg = rawArgs[i];
|
|
99
124
|
|
|
100
|
-
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') {
|
|
101
|
-
const value = rawArgs[i + 1];
|
|
102
|
-
if (!value || value.startsWith('--')) {
|
|
103
|
-
throw new Error(`${arg} requires a value`);
|
|
104
|
-
}
|
|
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') {
|
|
126
|
+
const value = rawArgs[i + 1];
|
|
127
|
+
if (!value || value.startsWith('--')) {
|
|
128
|
+
throw new Error(`${arg} requires a value`);
|
|
129
|
+
}
|
|
105
130
|
if (arg === '--threshold') threshold = value;
|
|
106
131
|
if (arg === '--out') out = value;
|
|
107
132
|
if (arg === '--plan') planFile = value;
|
|
@@ -120,15 +145,18 @@ function parseArgs(rawArgs) {
|
|
|
120
145
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
121
146
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
122
147
|
if (arg === '--port') port = value.trim();
|
|
123
|
-
if (arg === '--workspace') workspace = value.trim();
|
|
124
|
-
if (arg === '--check-version') checkVersion = value.trim();
|
|
125
|
-
if (arg === '--webhook') webhookUrl = value.trim();
|
|
126
|
-
if (arg === '--
|
|
127
|
-
if (arg === '--
|
|
128
|
-
if (arg === '--
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
if (arg === '--workspace') workspace = value.trim();
|
|
149
|
+
if (arg === '--check-version') checkVersion = value.trim();
|
|
150
|
+
if (arg === '--webhook') webhookUrl = value.trim();
|
|
151
|
+
if (arg === '--webhook-header') webhookHeaders.push(parseWebhookHeader(value));
|
|
152
|
+
if (arg === '--webhook-retries') webhookRetries = parseNonNegativeIntegerFlag(value.trim(), '--webhook-retries');
|
|
153
|
+
if (arg === '--external') external = value.trim();
|
|
154
|
+
if (arg === '--team-profile') teamProfile = value.trim();
|
|
155
|
+
if (arg === '--lang') lang = value.trim().toLowerCase();
|
|
156
|
+
if (arg === '--tag') snapshotTags.push(value.trim());
|
|
157
|
+
i++;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
132
160
|
|
|
133
161
|
if (arg.startsWith('--lang=')) {
|
|
134
162
|
lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -140,10 +168,15 @@ function parseArgs(rawArgs) {
|
|
|
140
168
|
continue;
|
|
141
169
|
}
|
|
142
170
|
|
|
143
|
-
if (arg.startsWith('--external=')) {
|
|
144
|
-
external = arg.split('=').slice(1).join('=').trim();
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
171
|
+
if (arg.startsWith('--external=')) {
|
|
172
|
+
external = arg.split('=').slice(1).join('=').trim();
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (arg.startsWith('--tag=')) {
|
|
177
|
+
snapshotTags.push(arg.split('=').slice(1).join('=').trim());
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
147
180
|
|
|
148
181
|
if (arg === '--repos') {
|
|
149
182
|
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
@@ -246,54 +279,100 @@ function parseArgs(rawArgs) {
|
|
|
246
279
|
continue;
|
|
247
280
|
}
|
|
248
281
|
|
|
249
|
-
if (arg.startsWith('--check-version=')) {
|
|
250
|
-
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
251
|
-
continue;
|
|
282
|
+
if (arg.startsWith('--check-version=')) {
|
|
283
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (arg.startsWith('--webhook=')) {
|
|
288
|
+
webhookUrl = arg.split('=').slice(1).join('=').trim();
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (arg.startsWith('--webhook-header=')) {
|
|
293
|
+
webhookHeaders.push(parseWebhookHeader(arg.split('=').slice(1).join('=')));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (arg.startsWith('--webhook-retries=')) {
|
|
298
|
+
webhookRetries = parseNonNegativeIntegerFlag(arg.split('=').slice(1).join('=').trim(), '--webhook-retries');
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (arg.startsWith('--')) {
|
|
303
|
+
flags.push(arg);
|
|
304
|
+
continue;
|
|
252
305
|
}
|
|
253
306
|
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
} else {
|
|
263
|
-
extraArgs.push(arg);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
307
|
+
if (!commandSet) {
|
|
308
|
+
command = arg;
|
|
309
|
+
commandSet = true;
|
|
310
|
+
commandExplicit = true;
|
|
311
|
+
} else {
|
|
312
|
+
extraArgs.push(arg);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
266
315
|
|
|
267
316
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
268
317
|
|
|
269
|
-
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, external, repos, teamProfile, lang };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function printWorkspaceSummary(summary, options) {
|
|
273
|
-
if (options.json) {
|
|
274
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
console.log(
|
|
282
|
-
console.log(
|
|
283
|
-
console.log(
|
|
284
|
-
console.log(`
|
|
285
|
-
console.log(
|
|
286
|
-
console.log(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
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 };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function printWorkspaceSummary(summary, options) {
|
|
322
|
+
if (options.json) {
|
|
323
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
|
|
328
|
+
const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
|
|
329
|
+
|
|
330
|
+
console.log('');
|
|
331
|
+
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
332
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
333
|
+
console.log(` Root: ${summary.rootDir}`);
|
|
334
|
+
console.log(` Platform: ${summary.platform}`);
|
|
335
|
+
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
336
|
+
if (summary.patterns?.length > 0) {
|
|
337
|
+
console.log(` Selection: ${summary.patterns.join(', ')}`);
|
|
338
|
+
}
|
|
339
|
+
console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
|
|
340
|
+
console.log(` Workspace audit average: \x1b[1m${workspaceAverage}/100\x1b[0m`);
|
|
341
|
+
if (summary.profileBreakdown?.length > 0) {
|
|
342
|
+
const profileLine = summary.profileBreakdown
|
|
343
|
+
.map((item) => `${item.profileLabel} (${item.workspaceCount})`)
|
|
344
|
+
.join(', ');
|
|
345
|
+
console.log(` Workspace profiles: ${profileLine}`);
|
|
346
|
+
}
|
|
347
|
+
console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
|
|
348
|
+
console.log(' Aggregate vs package: per-workspace scores can legitimately trail the root repo score in a monorepo.');
|
|
349
|
+
console.log(' Stack-specific checks: Go, Python, Node, and other workspace types can have different applicable totals.');
|
|
350
|
+
console.log('');
|
|
351
|
+
console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
|
|
352
|
+
console.log(' ' + '─'.repeat(96));
|
|
353
|
+
for (const item of summary.workspaces) {
|
|
354
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
355
|
+
const topAction = item.error || item.topAction || '-';
|
|
356
|
+
const profile = (item.workspaceProfile?.label || 'General workspace').slice(0, 20);
|
|
357
|
+
console.log(` ${item.workspace.padEnd(26)} ${profile.padEnd(20)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
|
|
358
|
+
if (item.stackLabels?.length > 0) {
|
|
359
|
+
console.log(`\x1b[2m Stacks: ${item.stackLabels.join(', ')}\x1b[0m`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
console.log('');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function printCompareCheckSection(title, items, prefix) {
|
|
366
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
367
|
+
console.log(` ${title} (${items.length}):`);
|
|
368
|
+
for (const item of items) {
|
|
369
|
+
const impact = item.impact ? ` [${item.impact}]` : '';
|
|
370
|
+
const category = item.category ? ` — ${item.category}` : '';
|
|
371
|
+
console.log(` ${prefix} ${item.key}${impact}: ${item.name}${category}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function printScanDetail(summary, options) {
|
|
297
376
|
if (options.json) {
|
|
298
377
|
console.log(JSON.stringify(summary, null, 2));
|
|
299
378
|
return;
|
|
@@ -366,17 +445,18 @@ function printOrgSummary(summary, options) {
|
|
|
366
445
|
console.log('');
|
|
367
446
|
}
|
|
368
447
|
|
|
369
|
-
const HELP = `
|
|
370
|
-
nerviq v${version}
|
|
371
|
-
The intelligent nervous system for AI coding agents.
|
|
372
|
-
Audit, align, and amplify every platform on every project.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
nerviq audit
|
|
377
|
-
nerviq audit --
|
|
378
|
-
nerviq audit --
|
|
379
|
-
nerviq audit --
|
|
448
|
+
const HELP = `
|
|
449
|
+
nerviq v${version}
|
|
450
|
+
The intelligent nervous system for AI coding agents.
|
|
451
|
+
Audit, align, and amplify every platform on every project.
|
|
452
|
+
New here? Run: nerviq --beginner
|
|
453
|
+
|
|
454
|
+
DISCOVER
|
|
455
|
+
nerviq audit Quick scan: score + top 3 gaps (default)
|
|
456
|
+
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
457
|
+
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
458
|
+
nerviq audit --json Machine-readable JSON output (for CI)
|
|
459
|
+
nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
|
|
380
460
|
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
381
461
|
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
382
462
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
@@ -411,34 +491,35 @@ const HELP = `
|
|
|
411
491
|
nerviq apply --dry-run Preview changes without writing
|
|
412
492
|
|
|
413
493
|
GOVERN
|
|
414
|
-
nerviq governance Permission profiles + hooks + policy packs
|
|
494
|
+
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
415
495
|
nerviq governance --json Machine-readable governance summary
|
|
416
|
-
nerviq benchmark
|
|
496
|
+
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
417
497
|
nerviq benchmark --external /path Benchmark an external repo
|
|
418
498
|
nerviq freshness Show verification freshness for all checks
|
|
419
499
|
nerviq certify Generate certification badge for your project
|
|
420
500
|
|
|
421
501
|
CROSS-PLATFORM
|
|
422
|
-
nerviq harmony-audit Drift detection across all active platforms
|
|
423
|
-
nerviq harmony-sync Preview cross-platform sync (dry run)
|
|
424
|
-
nerviq harmony-sync --fix Apply cross-platform sync (write files)
|
|
425
|
-
nerviq harmony-sync --json JSON output for CI/automation
|
|
426
|
-
nerviq harmony-add <platform> Add a new platform to the project
|
|
427
|
-
nerviq synergy-report
|
|
502
|
+
nerviq harmony-audit Drift detection across all active platforms (GA)
|
|
503
|
+
nerviq harmony-sync Preview cross-platform sync (dry run, GA)
|
|
504
|
+
nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
|
|
505
|
+
nerviq harmony-sync --json JSON output for CI/automation
|
|
506
|
+
nerviq harmony-add <platform> Add a new platform to the project
|
|
507
|
+
nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
|
|
428
508
|
nerviq convert --from X --to Y Convert configs between platforms
|
|
429
509
|
nerviq migrate --platform X Platform version migration helper
|
|
430
510
|
nerviq migrate --platform cursor --from v2 --to v3
|
|
431
511
|
|
|
432
512
|
MONITOR
|
|
433
|
-
nerviq dashboard Generate static
|
|
513
|
+
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
434
514
|
nerviq dashboard --out F Save dashboard to custom file
|
|
435
515
|
nerviq dashboard --open Open dashboard in browser after generating
|
|
436
516
|
nerviq watch Live config monitoring (re-audits on file change)
|
|
437
|
-
nerviq history
|
|
438
|
-
nerviq compare
|
|
439
|
-
nerviq trend
|
|
440
|
-
nerviq trend --out report.md Export trend report as markdown
|
|
441
|
-
nerviq
|
|
517
|
+
nerviq history Audit snapshot history from saved snapshots
|
|
518
|
+
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
519
|
+
nerviq trend Audit snapshot trend over time
|
|
520
|
+
nerviq trend --out report.md Export trend report as markdown
|
|
521
|
+
nerviq audit --snapshot --tag "pre-refactor" Save a named audit snapshot
|
|
522
|
+
nerviq feedback Record recommendation outcomes
|
|
442
523
|
|
|
443
524
|
TEAM PROFILES
|
|
444
525
|
nerviq profile save <name> Save current preferences as a named profile
|
|
@@ -448,7 +529,7 @@ const HELP = `
|
|
|
448
529
|
|
|
449
530
|
ADVANCED
|
|
450
531
|
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
451
|
-
nerviq serve --port 3000 Start local Nerviq REST API server
|
|
532
|
+
nerviq serve --port 3000 Start local Nerviq REST API server + OpenAPI contract
|
|
452
533
|
nerviq badge Generate shields.io badge markdown
|
|
453
534
|
nerviq rules-export Export recommendation rules as JSON
|
|
454
535
|
nerviq rules-export --out F Save rules to file
|
|
@@ -463,32 +544,37 @@ const HELP = `
|
|
|
463
544
|
--only A,B Limit plan/apply to selected proposal IDs
|
|
464
545
|
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
465
546
|
--team-profile N Load a saved team profile for audit (overrides threshold/platform)
|
|
466
|
-
--mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
|
|
467
|
-
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
468
|
-
--format NAME Output format: json | sarif | otel
|
|
469
|
-
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
470
|
-
--
|
|
471
|
-
--
|
|
472
|
-
--
|
|
473
|
-
--
|
|
474
|
-
--
|
|
547
|
+
--mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
|
|
548
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
549
|
+
--format NAME Output format: json | sarif | otel
|
|
550
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
551
|
+
--webhook-header H Add a custom webhook header (repeat; format: Name: Value)
|
|
552
|
+
--webhook-retries N Retry transient webhook failures N times (default: 2)
|
|
553
|
+
--external PATH Benchmark an external repo instead of cwd
|
|
554
|
+
--port N Port for \`serve\` (default: 3000)
|
|
555
|
+
--workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
|
|
556
|
+
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
557
|
+
--tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
|
|
558
|
+
--full Show full audit output (all checks, weakest areas, badge)
|
|
475
559
|
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
476
560
|
--dry-run Preview changes without writing files
|
|
477
561
|
--config-only Only write config files (.claude/, rules, hooks) — never source code
|
|
478
562
|
--verbose Full audit + medium-priority recommendations
|
|
479
|
-
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
480
|
-
--json Output as JSON
|
|
481
|
-
--auto Apply all generated files without prompting
|
|
482
|
-
--
|
|
563
|
+
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
564
|
+
--json Output as JSON
|
|
565
|
+
--auto Apply all generated files without prompting
|
|
566
|
+
--beginner Show only the 5 starter commands for first-time users
|
|
567
|
+
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
483
568
|
--status VALUE Feedback: accepted | rejected | deferred
|
|
484
569
|
--effect VALUE Feedback: positive | neutral | negative
|
|
485
570
|
--score-delta N Feedback: observed score delta
|
|
486
571
|
--help Show this help
|
|
487
572
|
--version Show version
|
|
488
573
|
|
|
489
|
-
EXAMPLES
|
|
490
|
-
npx nerviq
|
|
491
|
-
npx nerviq
|
|
574
|
+
EXAMPLES
|
|
575
|
+
npx nerviq --beginner
|
|
576
|
+
npx nerviq
|
|
577
|
+
npx nerviq --lite
|
|
492
578
|
npx nerviq --platform cursor
|
|
493
579
|
npx nerviq audit --workspace packages/*
|
|
494
580
|
npx nerviq --platform codex augment
|
|
@@ -505,9 +591,34 @@ const HELP = `
|
|
|
505
591
|
npx nerviq feedback --key permissionDeny --status accepted --effect positive
|
|
506
592
|
|
|
507
593
|
EXIT CODES
|
|
508
|
-
0 Success
|
|
509
|
-
1 Error, unknown command, or score below --threshold
|
|
510
|
-
`;
|
|
594
|
+
0 Success
|
|
595
|
+
1 Error, unknown command, or score below --threshold
|
|
596
|
+
`;
|
|
597
|
+
|
|
598
|
+
const BEGINNER_HELP = `
|
|
599
|
+
nerviq v${version}
|
|
600
|
+
Start here.
|
|
601
|
+
|
|
602
|
+
If this is your first time, learn just these 5 commands:
|
|
603
|
+
|
|
604
|
+
STARTER COMMANDS
|
|
605
|
+
nerviq audit Score the repo and show the top gaps
|
|
606
|
+
nerviq setup Generate a starter-safe baseline
|
|
607
|
+
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
608
|
+
nerviq augment Show an improvement plan without writing
|
|
609
|
+
nerviq doctor Check install health, freshness, and platform detection
|
|
610
|
+
|
|
611
|
+
SIMPLE PATH
|
|
612
|
+
1. nerviq audit
|
|
613
|
+
2. nerviq setup --auto
|
|
614
|
+
3. nerviq fix --all-critical --auto
|
|
615
|
+
4. nerviq augment
|
|
616
|
+
5. nerviq doctor
|
|
617
|
+
|
|
618
|
+
WHEN YOU ARE READY
|
|
619
|
+
nerviq --help Show the full command set
|
|
620
|
+
Docs: https://nerviq.net/docs/getting-started
|
|
621
|
+
`;
|
|
511
622
|
|
|
512
623
|
async function main() {
|
|
513
624
|
let parsed;
|
|
@@ -518,24 +629,29 @@ async function main() {
|
|
|
518
629
|
process.exit(1);
|
|
519
630
|
}
|
|
520
631
|
|
|
521
|
-
const { flags, command, normalizedCommand } = parsed;
|
|
632
|
+
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
522
633
|
|
|
523
634
|
// Initialize i18n with --lang flag or NERVIQ_LANG env var
|
|
524
635
|
if (parsed.lang) {
|
|
525
636
|
initI18n(parsed.lang);
|
|
526
637
|
}
|
|
527
638
|
|
|
528
|
-
if (flags.includes('--
|
|
529
|
-
console.log(
|
|
530
|
-
process.exit(0);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (flags.includes('--
|
|
534
|
-
console.log(
|
|
535
|
-
process.exit(0);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
639
|
+
if (flags.includes('--version') || command === 'version') {
|
|
640
|
+
console.log(version);
|
|
641
|
+
process.exit(0);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
|
|
645
|
+
console.log(BEGINNER_HELP);
|
|
646
|
+
process.exit(0);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (flags.includes('--help') || command === 'help') {
|
|
650
|
+
console.log(HELP);
|
|
651
|
+
process.exit(0);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const options = {
|
|
539
655
|
verbose: flags.includes('--verbose'),
|
|
540
656
|
json: flags.includes('--json'),
|
|
541
657
|
auto: flags.includes('--auto'),
|
|
@@ -557,13 +673,21 @@ async function main() {
|
|
|
557
673
|
require: parsed.requireChecks,
|
|
558
674
|
platform: parsed.platform || 'claude',
|
|
559
675
|
format: parsed.format || null,
|
|
560
|
-
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
561
|
-
workspace: parsed.workspace || null,
|
|
562
|
-
webhookUrl: parsed.webhookUrl || null,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
676
|
+
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
677
|
+
workspace: parsed.workspace || null,
|
|
678
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
679
|
+
webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
|
|
680
|
+
webhookRetries: parsed.webhookRetries ?? 2,
|
|
681
|
+
lang: parsed.lang || null,
|
|
682
|
+
external: parsed.external || null,
|
|
683
|
+
snapshotTags: parsed.snapshotTags || [],
|
|
684
|
+
dir: process.cwd()
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
if (options.snapshotTags.length > 0 && !options.snapshot) {
|
|
688
|
+
console.error('\n Error: --tag requires --snapshot.\n');
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
567
691
|
|
|
568
692
|
if (parsed.checkVersion) {
|
|
569
693
|
if (parsed.checkVersion !== version) {
|
|
@@ -746,9 +870,9 @@ async function main() {
|
|
|
746
870
|
process.exit(1);
|
|
747
871
|
}
|
|
748
872
|
process.exit(0);
|
|
749
|
-
} else if (normalizedCommand === 'history') {
|
|
750
|
-
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
751
|
-
// Handle --prune N
|
|
873
|
+
} else if (normalizedCommand === 'history') {
|
|
874
|
+
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
875
|
+
// Handle --prune N
|
|
752
876
|
const pruneIdx = flags.indexOf('--prune');
|
|
753
877
|
if (pruneIdx >= 0) {
|
|
754
878
|
const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
|
|
@@ -756,7 +880,7 @@ async function main() {
|
|
|
756
880
|
const pathMod = require('path');
|
|
757
881
|
const entries = readSnapshotIndex(options.dir);
|
|
758
882
|
if (entries.length <= keepCount) {
|
|
759
|
-
console.log(`\n Nothing to prune (${entries.length} snapshots, keeping ${keepCount}).\n`);
|
|
883
|
+
console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
|
|
760
884
|
} else {
|
|
761
885
|
const toRemove = entries.slice(0, entries.length - keepCount);
|
|
762
886
|
let removed = 0;
|
|
@@ -767,42 +891,78 @@ async function main() {
|
|
|
767
891
|
const kept = entries.slice(entries.length - keepCount);
|
|
768
892
|
const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
|
|
769
893
|
try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
|
|
770
|
-
console.log(`\n Pruned ${removed} snapshots, kept ${kept.length}.\n`);
|
|
894
|
+
console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
|
|
771
895
|
}
|
|
772
896
|
process.exit(0);
|
|
773
897
|
}
|
|
774
898
|
console.log('');
|
|
775
|
-
console.log(formatHistory(options.dir));
|
|
776
|
-
console.log('');
|
|
777
|
-
process.exit(0);
|
|
778
|
-
} else if (normalizedCommand === 'compare') {
|
|
779
|
-
const { compareLatest } = require('../src/activity');
|
|
780
|
-
const result = compareLatest(options.dir);
|
|
781
|
-
if (!result) {
|
|
782
|
-
console.log('
|
|
783
|
-
|
|
784
|
-
|
|
899
|
+
console.log(formatHistory(options.dir));
|
|
900
|
+
console.log('');
|
|
901
|
+
process.exit(0);
|
|
902
|
+
} else if (normalizedCommand === 'compare') {
|
|
903
|
+
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags } = require('../src/activity');
|
|
904
|
+
const result = compareLatest(options.dir);
|
|
905
|
+
if (!result) {
|
|
906
|
+
console.log('');
|
|
907
|
+
console.log(formatSnapshotBootstrap(options.dir, 'compare'));
|
|
908
|
+
console.log('');
|
|
909
|
+
process.exit(0);
|
|
910
|
+
}
|
|
785
911
|
if (options.json) {
|
|
786
912
|
console.log(JSON.stringify(result, null, 2));
|
|
787
|
-
} else {
|
|
788
|
-
const sign = result.delta.score >= 0 ? '+' : '';
|
|
789
|
-
console.log('');
|
|
790
|
-
console.log(` Previous: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
|
|
791
|
-
console.log(` Current: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
|
|
792
|
-
console.log(`
|
|
793
|
-
console.log(` Trend: ${result.trend}`);
|
|
794
|
-
if (result.
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
913
|
+
} else {
|
|
914
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
915
|
+
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)}`);
|
|
918
|
+
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
919
|
+
console.log(` Trend: ${result.trend}`);
|
|
920
|
+
if (result.detailedDiffAvailable) {
|
|
921
|
+
console.log('');
|
|
922
|
+
console.log(' Detailed check diff:');
|
|
923
|
+
printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
|
|
924
|
+
printCompareCheckSection('Improvements', result.improvementDetails, '✅');
|
|
925
|
+
printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
|
|
926
|
+
printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
|
|
927
|
+
if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
|
|
928
|
+
printCompareCheckSection('New checks', result.newChecks, '➕');
|
|
929
|
+
}
|
|
930
|
+
if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
|
|
931
|
+
printCompareCheckSection('Removed checks', result.removedChecks, '➖');
|
|
932
|
+
}
|
|
933
|
+
if (
|
|
934
|
+
result.regressionDetails.length === 0 &&
|
|
935
|
+
result.improvementDetails.length === 0 &&
|
|
936
|
+
result.newlyApplicableDetails.length === 0 &&
|
|
937
|
+
result.noLongerApplicableDetails.length === 0 &&
|
|
938
|
+
result.newChecks.length === 0 &&
|
|
939
|
+
result.removedChecks.length === 0
|
|
940
|
+
) {
|
|
941
|
+
console.log(' No per-check state changes detected.');
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
945
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
946
|
+
}
|
|
947
|
+
console.log('');
|
|
948
|
+
}
|
|
949
|
+
process.exit(0);
|
|
950
|
+
} else if (normalizedCommand === 'trend') {
|
|
951
|
+
const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
|
|
952
|
+
const auditHistory = getHistory(options.dir, 2);
|
|
953
|
+
if (auditHistory.length < 2) {
|
|
954
|
+
console.log('');
|
|
955
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
956
|
+
console.log('');
|
|
957
|
+
process.exit(0);
|
|
958
|
+
}
|
|
959
|
+
const report = exportTrendReport(options.dir);
|
|
960
|
+
if (!report) {
|
|
961
|
+
console.log('');
|
|
962
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
963
|
+
console.log('');
|
|
964
|
+
process.exit(0);
|
|
965
|
+
}
|
|
806
966
|
if (options.out) {
|
|
807
967
|
require('fs').writeFileSync(options.out, report, 'utf8');
|
|
808
968
|
console.log(`\n Trend report exported to ${options.out}\n`);
|
|
@@ -904,9 +1064,10 @@ async function main() {
|
|
|
904
1064
|
process.exit(0);
|
|
905
1065
|
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
906
1066
|
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
907
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
908
|
-
|
|
909
|
-
|
|
1067
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1068
|
+
tags: options.snapshotTags,
|
|
1069
|
+
sourceCommand: normalizedCommand,
|
|
1070
|
+
}) : null;
|
|
910
1071
|
if (options.out && !options.json) {
|
|
911
1072
|
const fs = require('fs');
|
|
912
1073
|
const md = exportMarkdown(report);
|
|
@@ -1037,9 +1198,10 @@ async function main() {
|
|
|
1037
1198
|
fs.writeFileSync(options.out, content, 'utf8');
|
|
1038
1199
|
}
|
|
1039
1200
|
printGovernanceSummary(summary, options);
|
|
1040
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1041
|
-
|
|
1042
|
-
|
|
1201
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1202
|
+
tags: options.snapshotTags,
|
|
1203
|
+
sourceCommand: normalizedCommand,
|
|
1204
|
+
}) : null;
|
|
1043
1205
|
if (options.out && !options.json) {
|
|
1044
1206
|
console.log(` Governance report written to ${options.out}`);
|
|
1045
1207
|
console.log('');
|
|
@@ -1051,9 +1213,10 @@ async function main() {
|
|
|
1051
1213
|
}
|
|
1052
1214
|
} else if (normalizedCommand === 'benchmark') {
|
|
1053
1215
|
const report = await runBenchmark(options);
|
|
1054
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1055
|
-
|
|
1056
|
-
|
|
1216
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1217
|
+
tags: options.snapshotTags,
|
|
1218
|
+
sourceCommand: normalizedCommand,
|
|
1219
|
+
}) : null;
|
|
1057
1220
|
if (options.out) {
|
|
1058
1221
|
writeBenchmarkReport(report, options.out);
|
|
1059
1222
|
}
|
|
@@ -1147,9 +1310,10 @@ async function main() {
|
|
|
1147
1310
|
const address = server.address();
|
|
1148
1311
|
const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
|
|
1149
1312
|
console.log('');
|
|
1150
|
-
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1151
|
-
console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1152
|
-
console.log(
|
|
1313
|
+
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1314
|
+
console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1315
|
+
console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
|
|
1316
|
+
console.log('');
|
|
1153
1317
|
|
|
1154
1318
|
const closeServer = () => {
|
|
1155
1319
|
server.close(() => process.exit(0));
|
|
@@ -1927,14 +2091,15 @@ async function main() {
|
|
|
1927
2091
|
process.exit(0);
|
|
1928
2092
|
} else if (normalizedCommand === 'setup') {
|
|
1929
2093
|
await setup(options);
|
|
1930
|
-
if (options.snapshot) {
|
|
1931
|
-
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1932
|
-
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2094
|
+
if (options.snapshot) {
|
|
2095
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2096
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
2097
|
+
tags: options.snapshotTags,
|
|
2098
|
+
sourceCommand: 'setup',
|
|
2099
|
+
});
|
|
2100
|
+
if (!options.json) {
|
|
2101
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2102
|
+
}
|
|
1938
2103
|
}
|
|
1939
2104
|
} else {
|
|
1940
2105
|
if (options.workspace) {
|
|
@@ -1972,18 +2137,26 @@ async function main() {
|
|
|
1972
2137
|
// Generic webhook: send full JSON audit result
|
|
1973
2138
|
payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
|
|
1974
2139
|
}
|
|
1975
|
-
const webhookResp = await sendWebhook(options.webhookUrl, payload
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2140
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2141
|
+
headers: options.webhookHeaders,
|
|
2142
|
+
retries: options.webhookRetries,
|
|
2143
|
+
});
|
|
2144
|
+
if (!options.json) {
|
|
2145
|
+
if (webhookResp.ok) {
|
|
2146
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2147
|
+
console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
|
|
2148
|
+
} else {
|
|
2149
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2150
|
+
console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
} catch (webhookErr) {
|
|
2154
|
+
if (!options.json) {
|
|
2155
|
+
const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
|
|
2156
|
+
console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
1987
2160
|
if (options.feedback && !options.json && options.format === null) {
|
|
1988
2161
|
const feedbackTargets = options.lite
|
|
1989
2162
|
? (result.liteSummary?.topNextActions || [])
|
|
@@ -2003,9 +2176,10 @@ async function main() {
|
|
|
2003
2176
|
console.log('');
|
|
2004
2177
|
}
|
|
2005
2178
|
}
|
|
2006
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2007
|
-
|
|
2008
|
-
|
|
2179
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2180
|
+
tags: options.snapshotTags,
|
|
2181
|
+
sourceCommand: normalizedCommand,
|
|
2182
|
+
}) : null;
|
|
2009
2183
|
if (snapshot && !options.json) {
|
|
2010
2184
|
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2011
2185
|
console.log(` Snapshot index: ${snapshot.indexPath}`);
|