@nerviq/cli 1.12.0 → 1.14.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 +227 -210
- package/bin/cli.js +1014 -997
- package/package.json +3 -2
- package/src/aider/freshness.js +65 -20
- package/src/audit/instruction-files.js +180 -0
- package/src/audit/recommendations.js +531 -0
- package/src/audit.js +121 -824
- package/src/codex/freshness.js +84 -25
- package/src/copilot/freshness.js +57 -20
- package/src/cursor/freshness.js +65 -20
- package/src/freshness.js +74 -21
- package/src/gemini/freshness.js +66 -21
- package/src/harmony/cli.js +235 -0
- package/src/index.js +2 -0
- package/src/mcp-server.js +95 -59
- package/src/opencode/freshness.js +66 -21
- package/src/setup/analysis.js +619 -0
- package/src/setup/runtime.js +172 -0
- package/src/setup.js +28 -748
- package/src/windsurf/freshness.js +36 -21
package/bin/cli.js
CHANGED
|
@@ -14,9 +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');
|
|
19
|
-
const { SNAPSHOT_MILESTONES } = require('../src/activity');
|
|
17
|
+
const { init: initI18n, t } = require('../src/i18n');
|
|
18
|
+
const { version } = require('../package.json');
|
|
19
|
+
const { SNAPSHOT_MILESTONES } = require('../src/activity');
|
|
20
20
|
|
|
21
21
|
const args = process.argv.slice(2);
|
|
22
22
|
const COMMAND_ALIASES = {
|
|
@@ -29,7 +29,7 @@ const COMMAND_ALIASES = {
|
|
|
29
29
|
gov: 'governance',
|
|
30
30
|
outcome: 'feedback',
|
|
31
31
|
};
|
|
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
|
+
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-score', 'harmony-demo', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'baseline', 'exception', 'help', 'version'];
|
|
33
33
|
|
|
34
34
|
function levenshtein(a, b) {
|
|
35
35
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -48,7 +48,7 @@ function levenshtein(a, b) {
|
|
|
48
48
|
return matrix[a.length][b.length];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function suggestCommand(input) {
|
|
51
|
+
function suggestCommand(input) {
|
|
52
52
|
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
53
53
|
let best = null;
|
|
54
54
|
let bestDistance = Infinity;
|
|
@@ -59,33 +59,33 @@ function suggestCommand(input) {
|
|
|
59
59
|
bestDistance = distance;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
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
|
-
|
|
86
|
-
function parseArgs(rawArgs) {
|
|
87
|
-
const flags = [];
|
|
88
|
-
let command = 'audit';
|
|
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
|
+
|
|
86
|
+
function parseArgs(rawArgs) {
|
|
87
|
+
const flags = [];
|
|
88
|
+
let command = 'audit';
|
|
89
89
|
let threshold = null;
|
|
90
90
|
let out = null;
|
|
91
91
|
let planFile = null;
|
|
@@ -98,47 +98,47 @@ function parseArgs(rawArgs) {
|
|
|
98
98
|
let feedbackEffect = null;
|
|
99
99
|
let feedbackNotes = null;
|
|
100
100
|
let feedbackSource = null;
|
|
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;
|
|
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;
|
|
122
122
|
let extraArgs = [];
|
|
123
123
|
let convertFrom = null;
|
|
124
124
|
let convertTo = null;
|
|
125
125
|
let migrateFrom = null;
|
|
126
126
|
let migrateTo = null;
|
|
127
127
|
let checkVersion = null;
|
|
128
|
-
let external = null;
|
|
129
|
-
let repos = [];
|
|
130
|
-
let teamProfile = null;
|
|
131
|
-
let lang = null;
|
|
132
|
-
let commandExplicit = false;
|
|
128
|
+
let external = null;
|
|
129
|
+
let repos = [];
|
|
130
|
+
let teamProfile = null;
|
|
131
|
+
let lang = null;
|
|
132
|
+
let commandExplicit = false;
|
|
133
133
|
|
|
134
134
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
135
135
|
const arg = rawArgs[i];
|
|
136
136
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
142
142
|
if (arg === '--threshold') threshold = value;
|
|
143
143
|
if (arg === '--out') out = value;
|
|
144
144
|
if (arg === '--plan') planFile = value;
|
|
@@ -152,33 +152,33 @@ function parseArgs(rawArgs) {
|
|
|
152
152
|
if (arg === '--notes') feedbackNotes = value;
|
|
153
153
|
if (arg === '--source') feedbackSource = value.trim();
|
|
154
154
|
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
155
|
-
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
155
|
+
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
156
156
|
if (arg === '--format') format = value.trim().toLowerCase();
|
|
157
157
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
158
158
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
159
159
|
if (arg === '--port') port = value.trim();
|
|
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
|
-
}
|
|
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
|
+
}
|
|
182
182
|
|
|
183
183
|
if (arg.startsWith('--lang=')) {
|
|
184
184
|
lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -190,65 +190,65 @@ function parseArgs(rawArgs) {
|
|
|
190
190
|
continue;
|
|
191
191
|
}
|
|
192
192
|
|
|
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
|
-
}
|
|
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
|
+
}
|
|
252
252
|
|
|
253
253
|
if (arg === '--repos') {
|
|
254
254
|
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
@@ -331,11 +331,11 @@ function parseArgs(rawArgs) {
|
|
|
331
331
|
continue;
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
if (arg.startsWith('--platform=')) {
|
|
335
|
-
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
336
|
-
platformExplicit = true;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
334
|
+
if (arg.startsWith('--platform=')) {
|
|
335
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
336
|
+
platformExplicit = true;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
339
|
|
|
340
340
|
if (arg.startsWith('--format=')) {
|
|
341
341
|
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -352,113 +352,113 @@ function parseArgs(rawArgs) {
|
|
|
352
352
|
continue;
|
|
353
353
|
}
|
|
354
354
|
|
|
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;
|
|
355
|
+
if (arg.startsWith('--check-version=')) {
|
|
356
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
357
|
+
continue;
|
|
378
358
|
}
|
|
379
359
|
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!commandSet) {
|
|
381
|
+
command = arg;
|
|
382
|
+
commandSet = true;
|
|
383
|
+
commandExplicit = true;
|
|
384
|
+
} else {
|
|
385
|
+
extraArgs.push(arg);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
388
|
|
|
389
389
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
390
390
|
|
|
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 };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function printWorkspaceSummary(summary, options) {
|
|
395
|
-
if (options.json) {
|
|
396
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
|
|
401
|
-
const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
|
|
402
|
-
|
|
403
|
-
console.log('');
|
|
404
|
-
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
405
|
-
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
406
|
-
console.log(` Root: ${summary.rootDir}`);
|
|
407
|
-
console.log(` Platform: ${summary.platform}`);
|
|
408
|
-
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
409
|
-
if (summary.patterns?.length > 0) {
|
|
410
|
-
console.log(` Selection: ${summary.patterns.join(', ')}`);
|
|
411
|
-
}
|
|
412
|
-
console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
|
|
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
|
-
}
|
|
420
|
-
console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
|
|
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.');
|
|
423
|
-
console.log('');
|
|
424
|
-
console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
|
|
425
|
-
console.log(' ' + '─'.repeat(96));
|
|
426
|
-
for (const item of summary.workspaces) {
|
|
427
|
-
const score = item.score === null ? 'ERR' : String(item.score);
|
|
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;
|
|
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 };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function printWorkspaceSummary(summary, options) {
|
|
395
|
+
if (options.json) {
|
|
396
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
|
|
401
|
+
const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
|
|
402
|
+
|
|
403
|
+
console.log('');
|
|
404
|
+
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
405
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
406
|
+
console.log(` Root: ${summary.rootDir}`);
|
|
407
|
+
console.log(` Platform: ${summary.platform}`);
|
|
408
|
+
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
409
|
+
if (summary.patterns?.length > 0) {
|
|
410
|
+
console.log(` Selection: ${summary.patterns.join(', ')}`);
|
|
411
|
+
}
|
|
412
|
+
console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
|
|
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
|
+
}
|
|
420
|
+
console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
|
|
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.');
|
|
423
|
+
console.log('');
|
|
424
|
+
console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
|
|
425
|
+
console.log(' ' + '─'.repeat(96));
|
|
426
|
+
for (const item of summary.workspaces) {
|
|
427
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
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}`);
|
|
452
445
|
}
|
|
446
|
+
}
|
|
453
447
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
console.log('');
|
|
448
|
+
function printScanDetail(summary, options) {
|
|
449
|
+
if (options.json) {
|
|
450
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
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('');
|
|
462
462
|
|
|
463
463
|
for (const item of summary.repos) {
|
|
464
464
|
if (item.error) {
|
|
@@ -467,12 +467,12 @@ function printScanDetail(summary, options) {
|
|
|
467
467
|
continue;
|
|
468
468
|
}
|
|
469
469
|
const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
|
|
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
|
|
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
|
|
476
476
|
if (item.result && item.result.results) {
|
|
477
477
|
const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
478
478
|
const categories = {};
|
|
@@ -501,86 +501,86 @@ function printScanDetail(summary, options) {
|
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
function printOrgSummary(summary, options) {
|
|
505
|
-
if (options.json) {
|
|
506
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
504
|
+
function printOrgSummary(summary, options) {
|
|
505
|
+
if (options.json) {
|
|
506
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
509
|
|
|
510
510
|
console.log('');
|
|
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
|
-
}
|
|
555
|
-
|
|
556
|
-
const HELP = `
|
|
557
|
-
nerviq v${version}
|
|
558
|
-
The intelligent nervous system for AI coding agents.
|
|
559
|
-
Audit, align, and amplify every platform on every project.
|
|
560
|
-
New here? Run: nerviq --beginner
|
|
561
|
-
|
|
562
|
-
DISCOVER
|
|
563
|
-
nerviq audit Quick scan: score + top 3 gaps (default)
|
|
564
|
-
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
565
|
-
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
566
|
-
nerviq audit --json Machine-readable JSON output (for CI)
|
|
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
|
|
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
|
+
}
|
|
555
|
+
|
|
556
|
+
const HELP = `
|
|
557
|
+
nerviq v${version}
|
|
558
|
+
The intelligent nervous system for AI coding agents.
|
|
559
|
+
Audit, align, and amplify every platform on every project.
|
|
560
|
+
New here? Run: nerviq --beginner
|
|
561
|
+
|
|
562
|
+
DISCOVER
|
|
563
|
+
nerviq audit Quick scan: score + top 3 gaps (default)
|
|
564
|
+
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
565
|
+
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
566
|
+
nerviq audit --json Machine-readable JSON output (for CI)
|
|
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
|
|
571
571
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
572
572
|
nerviq catalog --json Export full check catalog as JSON
|
|
573
573
|
nerviq anti-patterns Detect anti-patterns in current project
|
|
574
574
|
nerviq anti-patterns --all Show full anti-pattern catalog
|
|
575
575
|
|
|
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
|
|
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
|
|
584
584
|
|
|
585
585
|
FIX
|
|
586
586
|
nerviq fix Show fixable checks and manual-fix guidance
|
|
@@ -593,53 +593,58 @@ const HELP = `
|
|
|
593
593
|
nerviq rollback --list Show available rollback points
|
|
594
594
|
nerviq rollback --dry-run Preview what would be deleted
|
|
595
595
|
|
|
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
|
|
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
|
|
605
605
|
|
|
606
606
|
GOVERN
|
|
607
|
-
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
607
|
+
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
608
608
|
nerviq governance --json Machine-readable governance summary
|
|
609
|
-
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
609
|
+
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
610
610
|
nerviq benchmark --external /path Benchmark an external repo
|
|
611
611
|
nerviq freshness Show verification freshness for all checks
|
|
612
612
|
nerviq certify Generate certification badge for your project
|
|
613
613
|
|
|
614
614
|
CROSS-PLATFORM
|
|
615
|
-
nerviq harmony-audit Drift detection across all active platforms (GA)
|
|
616
|
-
nerviq harmony-sync Preview cross-platform sync (dry run, GA)
|
|
617
|
-
nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
|
|
618
|
-
nerviq harmony-sync --json JSON output for CI/automation
|
|
619
|
-
nerviq harmony-
|
|
620
|
-
nerviq
|
|
615
|
+
nerviq harmony-audit Drift detection across all active platforms (GA)
|
|
616
|
+
nerviq harmony-sync Preview cross-platform sync (dry run, GA)
|
|
617
|
+
nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
|
|
618
|
+
nerviq harmony-sync --json JSON output for CI/automation
|
|
619
|
+
nerviq harmony-score Standalone Harmony Score (0-100) with badge + CI gate
|
|
620
|
+
nerviq harmony-score --badge Include shields.io badge markdown
|
|
621
|
+
nerviq harmony-score --threshold 70 CI gate: exit 1 if score < threshold
|
|
622
|
+
nerviq harmony-score --quiet Score number only (for piping)
|
|
623
|
+
nerviq harmony-demo Zero-setup demo — see Harmony in action instantly
|
|
624
|
+
nerviq harmony-add <platform> Add a new platform to the project
|
|
625
|
+
nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
|
|
621
626
|
nerviq convert --from X --to Y Convert configs between platforms
|
|
622
627
|
nerviq migrate --platform X Platform version migration helper
|
|
623
628
|
nerviq migrate --platform cursor --from v2 --to v3
|
|
624
629
|
|
|
625
|
-
MONITOR
|
|
626
|
-
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
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
|
|
631
|
-
nerviq history Audit snapshot history from saved snapshots
|
|
632
|
-
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
633
|
-
nerviq trend Audit snapshot trend over time
|
|
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
|
|
630
|
+
MONITOR
|
|
631
|
+
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
632
|
+
nerviq dashboard --out F Save dashboard to custom file
|
|
633
|
+
nerviq dashboard --open Open dashboard in browser after generating
|
|
634
|
+
nerviq watch Live config monitoring (re-audits on file change)
|
|
635
|
+
nerviq audit --diff-only --drift-mode ci PR / CI drift review against the managed baseline
|
|
636
|
+
nerviq history Audit snapshot history from saved snapshots
|
|
637
|
+
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
638
|
+
nerviq trend Audit snapshot trend over time
|
|
639
|
+
nerviq trend --out report.md Export trend report as markdown
|
|
640
|
+
nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
|
|
641
|
+
nerviq feedback Record recommendation outcomes
|
|
642
|
+
|
|
643
|
+
EXCEPTIONS
|
|
644
|
+
nerviq exception add --key permissionDeny --owner team --reason "migration in progress" --expires 2026-05-01
|
|
645
|
+
nerviq exception add --class policy-drift --scope ci --owner team --reason "temporary rollout" --expires 2026-05-01
|
|
646
|
+
nerviq exception list Show active and expired exceptions
|
|
647
|
+
nerviq exception prune Remove expired exceptions
|
|
643
648
|
|
|
644
649
|
TEAM PROFILES
|
|
645
650
|
nerviq profile save <name> Save current preferences as a named profile
|
|
@@ -648,9 +653,9 @@ const HELP = `
|
|
|
648
653
|
nerviq profile export <name> Export profile JSON for sharing
|
|
649
654
|
|
|
650
655
|
ADVANCED
|
|
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
|
|
656
|
+
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
657
|
+
nerviq deep-review --behavioral Local behavioral drift review (opt-in, no API)
|
|
658
|
+
nerviq serve --port 3000 Start local Nerviq HTTP API server + OpenAPI contract
|
|
654
659
|
nerviq badge Generate shields.io badge markdown
|
|
655
660
|
nerviq rules-export Export recommendation rules as JSON
|
|
656
661
|
nerviq rules-export --out F Save rules to file
|
|
@@ -665,99 +670,99 @@ const HELP = `
|
|
|
665
670
|
--only A,B Limit plan/apply to selected proposal IDs
|
|
666
671
|
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
667
672
|
--team-profile N Load a saved team profile for audit (overrides threshold/platform)
|
|
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)
|
|
673
|
+
--mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
|
|
674
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
675
|
+
--format NAME Output format: json | sarif | otel
|
|
676
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
677
|
+
--webhook-header H Add a custom webhook header (repeat; format: Name: Value)
|
|
678
|
+
--webhook-retries N Retry transient webhook failures N times (default: 2)
|
|
679
|
+
--external PATH Benchmark an external repo instead of cwd
|
|
680
|
+
--port N Port for \`serve\` (default: 3000)
|
|
681
|
+
--workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
|
|
682
|
+
--diff-only Audit only changed files / linked config surfaces from git diff
|
|
683
|
+
--drift-mode M Continuous posture mode: ci | pr | watch
|
|
684
|
+
--diff-base SHA Base SHA for diff-only mode (defaults to PR env vars when present)
|
|
685
|
+
--diff-head SHA Head SHA for diff-only mode (defaults to GITHUB_SHA or HEAD)
|
|
686
|
+
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
687
|
+
--tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
|
|
688
|
+
--milestone NAME Snapshot lifecycle milestone: baseline | post-fix | pre-upgrade | release
|
|
689
|
+
--campaign A,B Limit plan/apply to named upgrade campaigns
|
|
690
|
+
--full Show full audit output (all checks, weakest areas, badge)
|
|
686
691
|
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
687
692
|
--dry-run Preview changes without writing files
|
|
688
693
|
--config-only Only write config files (.claude/, rules, hooks) — never source code
|
|
689
694
|
--verbose Full audit + medium-priority recommendations
|
|
690
|
-
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
691
|
-
--json Output as JSON
|
|
692
|
-
--auto Apply all generated files without prompting
|
|
693
|
-
--beginner Show only the 5 starter commands for first-time users
|
|
694
|
-
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
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
|
|
708
|
-
|
|
709
|
-
EXAMPLES
|
|
710
|
-
npx nerviq --beginner
|
|
711
|
-
npx nerviq
|
|
712
|
-
npx nerviq --lite
|
|
695
|
+
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
696
|
+
--json Output as JSON
|
|
697
|
+
--auto Apply all generated files without prompting
|
|
698
|
+
--beginner Show only the 5 starter commands for first-time users
|
|
699
|
+
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
700
|
+
--status VALUE Feedback: accepted | rejected | deferred
|
|
701
|
+
--effect VALUE Feedback: positive | neutral | negative
|
|
702
|
+
--score-delta N Feedback: observed score delta
|
|
703
|
+
--owner NAME Exception owner
|
|
704
|
+
--reason TEXT Exception reason
|
|
705
|
+
--expires DATE Exception expiry (ISO date or date-time)
|
|
706
|
+
--scope NAME Exception scope: all | ci | watch | pr
|
|
707
|
+
--class NAME Exception target class: policy-drift | config-drift | platform-drift | maturity-opportunity
|
|
708
|
+
--behavioral Run the opt-in local behavioral drift / outcome-layer review
|
|
709
|
+
--history With deep-review --behavioral, show behavioral snapshot history
|
|
710
|
+
--compare With deep-review --behavioral, compare the latest two behavioral snapshots
|
|
711
|
+
--help Show this help
|
|
712
|
+
--version Show version
|
|
713
|
+
|
|
714
|
+
EXAMPLES
|
|
715
|
+
npx nerviq --beginner
|
|
716
|
+
npx nerviq
|
|
717
|
+
npx nerviq --lite
|
|
713
718
|
npx nerviq --platform cursor
|
|
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
|
|
719
|
+
npx nerviq audit --workspace packages/*
|
|
720
|
+
npx nerviq baseline init
|
|
721
|
+
npx nerviq audit --diff-only --drift-mode ci
|
|
722
|
+
npx nerviq --platform codex augment
|
|
723
|
+
npx nerviq org scan ./app ./api ./infra
|
|
724
|
+
npx nerviq org policy
|
|
720
725
|
npx nerviq scan ./app ./api ./infra
|
|
721
726
|
npx nerviq harmony-audit
|
|
722
727
|
npx nerviq convert --from claude --to codex
|
|
723
728
|
npx nerviq migrate --platform cursor --from v2 --to v3
|
|
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
|
|
729
|
+
npx nerviq setup --mcp-pack context7-docs
|
|
730
|
+
npx nerviq plan --campaign governance-hardening
|
|
731
|
+
npx nerviq apply --plan plan.json --only hooks,commands
|
|
727
732
|
npx nerviq serve --port 4000
|
|
728
733
|
npx nerviq --json --threshold 70
|
|
729
734
|
npx nerviq catalog --json --out catalog.json
|
|
730
735
|
npx nerviq feedback --key permissionDeny --status accepted --effect positive
|
|
731
736
|
|
|
732
737
|
EXIT CODES
|
|
733
|
-
0 Success
|
|
734
|
-
1 Error, unknown command, or score below --threshold
|
|
735
|
-
`;
|
|
736
|
-
|
|
737
|
-
const BEGINNER_HELP = `
|
|
738
|
-
nerviq v${version}
|
|
739
|
-
Start here.
|
|
740
|
-
|
|
741
|
-
If this is your first time, learn just these 5 commands:
|
|
742
|
-
|
|
743
|
-
STARTER COMMANDS
|
|
744
|
-
nerviq audit Score the repo and show the top gaps
|
|
745
|
-
nerviq setup Generate a starter-safe baseline
|
|
746
|
-
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
747
|
-
nerviq augment Show an improvement plan without writing
|
|
748
|
-
nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
|
|
749
|
-
|
|
750
|
-
SIMPLE PATH
|
|
751
|
-
1. nerviq audit
|
|
752
|
-
2. nerviq setup --auto
|
|
753
|
-
3. nerviq fix --all-critical --auto
|
|
754
|
-
4. nerviq augment
|
|
755
|
-
5. nerviq doctor
|
|
756
|
-
|
|
757
|
-
WHEN YOU ARE READY
|
|
758
|
-
nerviq --help Show the full command set
|
|
759
|
-
Docs: https://nerviq.net/docs/getting-started
|
|
760
|
-
`;
|
|
738
|
+
0 Success
|
|
739
|
+
1 Error, unknown command, or score below --threshold
|
|
740
|
+
`;
|
|
741
|
+
|
|
742
|
+
const BEGINNER_HELP = `
|
|
743
|
+
nerviq v${version}
|
|
744
|
+
Start here.
|
|
745
|
+
|
|
746
|
+
If this is your first time, learn just these 5 commands:
|
|
747
|
+
|
|
748
|
+
STARTER COMMANDS
|
|
749
|
+
nerviq audit Score the repo and show the top gaps
|
|
750
|
+
nerviq setup Generate a starter-safe baseline
|
|
751
|
+
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
752
|
+
nerviq augment Show an improvement plan without writing
|
|
753
|
+
nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
|
|
754
|
+
|
|
755
|
+
SIMPLE PATH
|
|
756
|
+
1. nerviq audit
|
|
757
|
+
2. nerviq setup --auto
|
|
758
|
+
3. nerviq fix --all-critical --auto
|
|
759
|
+
4. nerviq augment
|
|
760
|
+
5. nerviq doctor
|
|
761
|
+
|
|
762
|
+
WHEN YOU ARE READY
|
|
763
|
+
nerviq --help Show the full command set
|
|
764
|
+
Docs: https://nerviq.net/docs/getting-started
|
|
765
|
+
`;
|
|
761
766
|
|
|
762
767
|
async function main() {
|
|
763
768
|
let parsed;
|
|
@@ -768,29 +773,29 @@ async function main() {
|
|
|
768
773
|
process.exit(1);
|
|
769
774
|
}
|
|
770
775
|
|
|
771
|
-
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
776
|
+
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
772
777
|
|
|
773
778
|
// Initialize i18n with --lang flag or NERVIQ_LANG env var
|
|
774
779
|
if (parsed.lang) {
|
|
775
780
|
initI18n(parsed.lang);
|
|
776
781
|
}
|
|
777
782
|
|
|
778
|
-
if (flags.includes('--version') || command === 'version') {
|
|
779
|
-
console.log(version);
|
|
780
|
-
process.exit(0);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
|
|
784
|
-
console.log(BEGINNER_HELP);
|
|
785
|
-
process.exit(0);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
if (flags.includes('--help') || command === 'help') {
|
|
789
|
-
console.log(HELP);
|
|
790
|
-
process.exit(0);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const options = {
|
|
783
|
+
if (flags.includes('--version') || command === 'version') {
|
|
784
|
+
console.log(version);
|
|
785
|
+
process.exit(0);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
|
|
789
|
+
console.log(BEGINNER_HELP);
|
|
790
|
+
process.exit(0);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (flags.includes('--help') || command === 'help') {
|
|
794
|
+
console.log(HELP);
|
|
795
|
+
process.exit(0);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const options = {
|
|
794
799
|
verbose: flags.includes('--verbose'),
|
|
795
800
|
json: flags.includes('--json'),
|
|
796
801
|
auto: flags.includes('--auto'),
|
|
@@ -800,6 +805,8 @@ async function main() {
|
|
|
800
805
|
snapshot: flags.includes('--snapshot'),
|
|
801
806
|
feedback: flags.includes('--feedback'),
|
|
802
807
|
fix: flags.includes('--fix'),
|
|
808
|
+
badge: flags.includes('--badge'),
|
|
809
|
+
quiet: flags.includes('--quiet'),
|
|
803
810
|
autoSync: flags.includes('--auto-sync'),
|
|
804
811
|
dryRun: flags.includes('--dry-run'),
|
|
805
812
|
configOnly: flags.includes('--config-only'),
|
|
@@ -809,82 +816,82 @@ async function main() {
|
|
|
809
816
|
only: parsed.only,
|
|
810
817
|
profile: parsed.profile,
|
|
811
818
|
mcpPacks: parsed.mcpPacks,
|
|
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) {
|
|
819
|
+
require: parsed.requireChecks,
|
|
820
|
+
platform: parsed.platform || 'claude',
|
|
821
|
+
platformExplicit: Boolean(parsed.platformExplicit),
|
|
822
|
+
format: parsed.format || null,
|
|
823
|
+
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
824
|
+
workspace: parsed.workspace || null,
|
|
825
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
826
|
+
webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
|
|
827
|
+
webhookRetries: parsed.webhookRetries ?? 2,
|
|
828
|
+
lang: parsed.lang || null,
|
|
829
|
+
external: parsed.external || null,
|
|
830
|
+
snapshotTags: parsed.snapshotTags || [],
|
|
831
|
+
snapshotMilestone: parsed.snapshotMilestone || null,
|
|
832
|
+
campaigns: parsed.campaigns || [],
|
|
833
|
+
behavioral: flags.includes('--behavioral'),
|
|
834
|
+
historyView: flags.includes('--history'),
|
|
835
|
+
compareView: flags.includes('--compare'),
|
|
836
|
+
diffOnly: flags.includes('--diff-only'),
|
|
837
|
+
diffBase: parsed.diffBase || null,
|
|
838
|
+
diffHead: parsed.diffHead || null,
|
|
839
|
+
driftMode: parsed.driftMode || null,
|
|
840
|
+
exceptionOwner: parsed.exceptionOwner || null,
|
|
841
|
+
exceptionReason: parsed.exceptionReason || null,
|
|
842
|
+
exceptionExpires: parsed.exceptionExpires || null,
|
|
843
|
+
exceptionScope: parsed.exceptionScope || null,
|
|
844
|
+
exceptionClass: parsed.exceptionClass || null,
|
|
845
|
+
dir: process.cwd()
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
if (options.snapshotTags.length > 0 && !options.snapshot) {
|
|
849
|
+
console.error('\n Error: --tag requires --snapshot.\n');
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (options.snapshotMilestone && !options.snapshot) {
|
|
854
|
+
console.error('\n Error: --milestone requires --snapshot.\n');
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
|
|
859
|
+
console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (options.diffOnly && options.snapshot) {
|
|
864
|
+
console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
|
|
869
|
+
console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (parsed.checkVersion) {
|
|
867
874
|
if (parsed.checkVersion !== version) {
|
|
868
875
|
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
869
876
|
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
870
877
|
console.error('');
|
|
871
878
|
}
|
|
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');
|
|
879
|
+
options.checkVersion = parsed.checkVersion;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const {
|
|
883
|
+
resolvePolicyLayers,
|
|
884
|
+
applyPolicyLayersToOptions,
|
|
885
|
+
formatPolicyContract,
|
|
886
|
+
} = require('../src/policy-layers');
|
|
887
|
+
const inheritedPolicyContract = resolvePolicyLayers(options.dir);
|
|
888
|
+
if (inheritedPolicyContract.layers.some((layer) => layer.valid)) {
|
|
889
|
+
Object.assign(options, applyPolicyLayersToOptions(inheritedPolicyContract, options));
|
|
890
|
+
options.policyContract = inheritedPolicyContract;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (parsed.teamProfile) {
|
|
894
|
+
const { loadProfile, applyProfileToOptions } = require('../src/profiles');
|
|
888
895
|
try {
|
|
889
896
|
const teamProf = loadProfile(options.dir, parsed.teamProfile);
|
|
890
897
|
const merged = applyProfileToOptions(teamProf, options);
|
|
@@ -923,15 +930,15 @@ async function main() {
|
|
|
923
930
|
process.exit(1);
|
|
924
931
|
}
|
|
925
932
|
|
|
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
|
-
}
|
|
933
|
+
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
934
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (options.driftMode && options.format !== null) {
|
|
939
|
+
console.error('\n Error: --drift-mode is only supported with normal text output or --json.\n');
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
935
942
|
|
|
936
943
|
if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
|
|
937
944
|
console.error('\n Error: --port must be an integer between 0 and 65535.\n');
|
|
@@ -981,13 +988,13 @@ async function main() {
|
|
|
981
988
|
}
|
|
982
989
|
|
|
983
990
|
try {
|
|
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',
|
|
991
|
+
const FULL_COMMAND_SET = new Set([
|
|
992
|
+
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
993
|
+
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
994
|
+
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'baseline', 'exception', 'help', 'version',
|
|
995
|
+
// Harmony + Synergy (cross-platform)
|
|
996
|
+
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
997
|
+
'harmony-watch', 'harmony-governance', 'harmony-score', 'harmony-demo', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
991
998
|
'freshness', 'profile', 'migrate',
|
|
992
999
|
]);
|
|
993
1000
|
|
|
@@ -1033,54 +1040,54 @@ async function main() {
|
|
|
1033
1040
|
}
|
|
1034
1041
|
}
|
|
1035
1042
|
|
|
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);
|
|
1081
|
-
} else if (normalizedCommand === 'history') {
|
|
1082
|
-
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
1083
|
-
// Handle --prune N
|
|
1043
|
+
if (normalizedCommand === 'scan') {
|
|
1044
|
+
const scanDirs = parsed.extraArgs;
|
|
1045
|
+
if (scanDirs.length === 0) {
|
|
1046
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
1047
|
+
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1051
|
+
printScanDetail(summary, options);
|
|
1052
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1053
|
+
process.exit(1);
|
|
1054
|
+
}
|
|
1055
|
+
process.exit(0);
|
|
1056
|
+
} else if (normalizedCommand === 'org') {
|
|
1057
|
+
const subcommand = parsed.extraArgs[0];
|
|
1058
|
+
if (subcommand === 'policy') {
|
|
1059
|
+
const targetDir = parsed.extraArgs[1] ? require('path').resolve(parsed.extraArgs[1]) : options.dir;
|
|
1060
|
+
const contract = resolvePolicyLayers(targetDir);
|
|
1061
|
+
if (options.json) {
|
|
1062
|
+
await writeStdout(JSON.stringify(contract, null, 2) + '\n');
|
|
1063
|
+
} else {
|
|
1064
|
+
console.log('');
|
|
1065
|
+
console.log(formatPolicyContract(contract));
|
|
1066
|
+
console.log('');
|
|
1067
|
+
}
|
|
1068
|
+
process.exit(0);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const scanDirs = parsed.extraArgs.slice(1);
|
|
1072
|
+
if (subcommand !== 'scan' || scanDirs.length === 0) {
|
|
1073
|
+
console.error('\n Error: org requires `scan` or `policy`.');
|
|
1074
|
+
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
|
|
1075
|
+
console.error(' npx nerviq org policy [dir]\n');
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1079
|
+
if (options.json) {
|
|
1080
|
+
await writeStdout(JSON.stringify(summary, null, 2) + '\n');
|
|
1081
|
+
} else {
|
|
1082
|
+
printOrgSummary(summary, options);
|
|
1083
|
+
}
|
|
1084
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
process.exit(0);
|
|
1088
|
+
} else if (normalizedCommand === 'history') {
|
|
1089
|
+
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
1090
|
+
// Handle --prune N
|
|
1084
1091
|
const pruneIdx = flags.indexOf('--prune');
|
|
1085
1092
|
if (pruneIdx >= 0) {
|
|
1086
1093
|
const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
|
|
@@ -1088,7 +1095,7 @@ async function main() {
|
|
|
1088
1095
|
const pathMod = require('path');
|
|
1089
1096
|
const entries = readSnapshotIndex(options.dir);
|
|
1090
1097
|
if (entries.length <= keepCount) {
|
|
1091
|
-
console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
|
|
1098
|
+
console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
|
|
1092
1099
|
} else {
|
|
1093
1100
|
const toRemove = entries.slice(0, entries.length - keepCount);
|
|
1094
1101
|
let removed = 0;
|
|
@@ -1099,78 +1106,78 @@ async function main() {
|
|
|
1099
1106
|
const kept = entries.slice(entries.length - keepCount);
|
|
1100
1107
|
const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
|
|
1101
1108
|
try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
|
|
1102
|
-
console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
|
|
1109
|
+
console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
|
|
1103
1110
|
}
|
|
1104
1111
|
process.exit(0);
|
|
1105
1112
|
}
|
|
1106
1113
|
console.log('');
|
|
1107
|
-
console.log(formatHistory(options.dir));
|
|
1108
|
-
console.log('');
|
|
1109
|
-
process.exit(0);
|
|
1110
|
-
} else if (normalizedCommand === 'compare') {
|
|
1111
|
-
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
|
|
1112
|
-
const result = compareLatest(options.dir);
|
|
1113
|
-
if (!result) {
|
|
1114
|
-
console.log('');
|
|
1115
|
-
console.log(formatSnapshotBootstrap(options.dir, 'compare'));
|
|
1116
|
-
console.log('');
|
|
1117
|
-
process.exit(0);
|
|
1118
|
-
}
|
|
1114
|
+
console.log(formatHistory(options.dir));
|
|
1115
|
+
console.log('');
|
|
1116
|
+
process.exit(0);
|
|
1117
|
+
} else if (normalizedCommand === 'compare') {
|
|
1118
|
+
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
|
|
1119
|
+
const result = compareLatest(options.dir);
|
|
1120
|
+
if (!result) {
|
|
1121
|
+
console.log('');
|
|
1122
|
+
console.log(formatSnapshotBootstrap(options.dir, 'compare'));
|
|
1123
|
+
console.log('');
|
|
1124
|
+
process.exit(0);
|
|
1125
|
+
}
|
|
1119
1126
|
if (options.json) {
|
|
1120
1127
|
console.log(JSON.stringify(result, null, 2));
|
|
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)}`);
|
|
1126
|
-
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
1127
|
-
console.log(` Trend: ${result.trend}`);
|
|
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('');
|
|
1156
|
-
}
|
|
1157
|
-
process.exit(0);
|
|
1158
|
-
} else if (normalizedCommand === 'trend') {
|
|
1159
|
-
const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
|
|
1160
|
-
const auditHistory = getHistory(options.dir, 2);
|
|
1161
|
-
if (auditHistory.length < 2) {
|
|
1162
|
-
console.log('');
|
|
1163
|
-
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1164
|
-
console.log('');
|
|
1165
|
-
process.exit(0);
|
|
1166
|
-
}
|
|
1167
|
-
const report = exportTrendReport(options.dir);
|
|
1168
|
-
if (!report) {
|
|
1169
|
-
console.log('');
|
|
1170
|
-
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1171
|
-
console.log('');
|
|
1172
|
-
process.exit(0);
|
|
1173
|
-
}
|
|
1128
|
+
} else {
|
|
1129
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
1130
|
+
console.log('');
|
|
1131
|
+
console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotMilestone(result.previous.milestone)}${formatSnapshotTags(result.previous.tags)}`);
|
|
1132
|
+
console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotMilestone(result.current.milestone)}${formatSnapshotTags(result.current.tags)}`);
|
|
1133
|
+
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
1134
|
+
console.log(` Trend: ${result.trend}`);
|
|
1135
|
+
if (result.detailedDiffAvailable) {
|
|
1136
|
+
console.log('');
|
|
1137
|
+
console.log(' Detailed check diff:');
|
|
1138
|
+
printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
|
|
1139
|
+
printCompareCheckSection('Improvements', result.improvementDetails, '✅');
|
|
1140
|
+
printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
|
|
1141
|
+
printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
|
|
1142
|
+
if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
|
|
1143
|
+
printCompareCheckSection('New checks', result.newChecks, '➕');
|
|
1144
|
+
}
|
|
1145
|
+
if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
|
|
1146
|
+
printCompareCheckSection('Removed checks', result.removedChecks, '➖');
|
|
1147
|
+
}
|
|
1148
|
+
if (
|
|
1149
|
+
result.regressionDetails.length === 0 &&
|
|
1150
|
+
result.improvementDetails.length === 0 &&
|
|
1151
|
+
result.newlyApplicableDetails.length === 0 &&
|
|
1152
|
+
result.noLongerApplicableDetails.length === 0 &&
|
|
1153
|
+
result.newChecks.length === 0 &&
|
|
1154
|
+
result.removedChecks.length === 0
|
|
1155
|
+
) {
|
|
1156
|
+
console.log(' No per-check state changes detected.');
|
|
1157
|
+
}
|
|
1158
|
+
} else {
|
|
1159
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
1160
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
1161
|
+
}
|
|
1162
|
+
console.log('');
|
|
1163
|
+
}
|
|
1164
|
+
process.exit(0);
|
|
1165
|
+
} else if (normalizedCommand === 'trend') {
|
|
1166
|
+
const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
|
|
1167
|
+
const auditHistory = getHistory(options.dir, 2);
|
|
1168
|
+
if (auditHistory.length < 2) {
|
|
1169
|
+
console.log('');
|
|
1170
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1171
|
+
console.log('');
|
|
1172
|
+
process.exit(0);
|
|
1173
|
+
}
|
|
1174
|
+
const report = exportTrendReport(options.dir);
|
|
1175
|
+
if (!report) {
|
|
1176
|
+
console.log('');
|
|
1177
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1178
|
+
console.log('');
|
|
1179
|
+
process.exit(0);
|
|
1180
|
+
}
|
|
1174
1181
|
if (options.out) {
|
|
1175
1182
|
require('fs').writeFileSync(options.out, report, 'utf8');
|
|
1176
1183
|
console.log(`\n Trend report exported to ${options.out}\n`);
|
|
@@ -1272,11 +1279,11 @@ async function main() {
|
|
|
1272
1279
|
process.exit(0);
|
|
1273
1280
|
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
1274
1281
|
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
1275
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1276
|
-
tags: options.snapshotTags,
|
|
1277
|
-
milestone: options.snapshotMilestone,
|
|
1278
|
-
sourceCommand: normalizedCommand,
|
|
1279
|
-
}) : null;
|
|
1282
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1283
|
+
tags: options.snapshotTags,
|
|
1284
|
+
milestone: options.snapshotMilestone,
|
|
1285
|
+
sourceCommand: normalizedCommand,
|
|
1286
|
+
}) : null;
|
|
1280
1287
|
if (options.out && !options.json) {
|
|
1281
1288
|
const fs = require('fs');
|
|
1282
1289
|
const md = exportMarkdown(report);
|
|
@@ -1407,11 +1414,11 @@ async function main() {
|
|
|
1407
1414
|
fs.writeFileSync(options.out, content, 'utf8');
|
|
1408
1415
|
}
|
|
1409
1416
|
printGovernanceSummary(summary, options);
|
|
1410
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1411
|
-
tags: options.snapshotTags,
|
|
1412
|
-
milestone: options.snapshotMilestone,
|
|
1413
|
-
sourceCommand: normalizedCommand,
|
|
1414
|
-
}) : null;
|
|
1417
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1418
|
+
tags: options.snapshotTags,
|
|
1419
|
+
milestone: options.snapshotMilestone,
|
|
1420
|
+
sourceCommand: normalizedCommand,
|
|
1421
|
+
}) : null;
|
|
1415
1422
|
if (options.out && !options.json) {
|
|
1416
1423
|
console.log(` Governance report written to ${options.out}`);
|
|
1417
1424
|
console.log('');
|
|
@@ -1423,11 +1430,11 @@ async function main() {
|
|
|
1423
1430
|
}
|
|
1424
1431
|
} else if (normalizedCommand === 'benchmark') {
|
|
1425
1432
|
const report = await runBenchmark(options);
|
|
1426
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1427
|
-
tags: options.snapshotTags,
|
|
1428
|
-
milestone: options.snapshotMilestone,
|
|
1429
|
-
sourceCommand: normalizedCommand,
|
|
1430
|
-
}) : null;
|
|
1433
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1434
|
+
tags: options.snapshotTags,
|
|
1435
|
+
milestone: options.snapshotMilestone,
|
|
1436
|
+
sourceCommand: normalizedCommand,
|
|
1437
|
+
}) : null;
|
|
1431
1438
|
if (options.out) {
|
|
1432
1439
|
writeBenchmarkReport(report, options.out);
|
|
1433
1440
|
}
|
|
@@ -1444,87 +1451,87 @@ async function main() {
|
|
|
1444
1451
|
} else if (normalizedCommand === 'deep-review') {
|
|
1445
1452
|
const { deepReview } = require('../src/deep-review');
|
|
1446
1453
|
await deepReview(options);
|
|
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') {
|
|
1454
|
+
} else if (normalizedCommand === 'interactive') {
|
|
1455
|
+
const { interactive } = require('../src/interactive');
|
|
1456
|
+
await interactive(options);
|
|
1457
|
+
} else if (normalizedCommand === 'baseline') {
|
|
1458
|
+
const {
|
|
1459
|
+
readManagedBaseline,
|
|
1460
|
+
writeManagedBaseline,
|
|
1461
|
+
buildManagedBaselineRecord,
|
|
1462
|
+
formatManagedBaselineStatus,
|
|
1463
|
+
} = require('../src/continuous-ops');
|
|
1464
|
+
const subcommand = parsed.extraArgs[0] || 'status';
|
|
1465
|
+
|
|
1466
|
+
if (subcommand === 'status') {
|
|
1467
|
+
const baseline = readManagedBaseline(options.dir);
|
|
1468
|
+
if (options.json) {
|
|
1469
|
+
console.log(JSON.stringify(baseline, null, 2));
|
|
1470
|
+
} else {
|
|
1471
|
+
console.log('');
|
|
1472
|
+
console.log(formatManagedBaselineStatus(options.dir, baseline));
|
|
1473
|
+
console.log('');
|
|
1474
|
+
}
|
|
1475
|
+
process.exit(0);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
if (subcommand === 'init') {
|
|
1479
|
+
const existingBaseline = readManagedBaseline(options.dir);
|
|
1480
|
+
if (existingBaseline && !flags.includes('--force')) {
|
|
1481
|
+
console.error('\n Error: Managed baseline already exists. Use `nerviq baseline status` to inspect it, or rerun with --force to replace it.\n');
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const auditResult = await audit({ ...options, silent: true });
|
|
1486
|
+
const analysisReport = await analyzeProject({ ...options, mode: 'augment' });
|
|
1487
|
+
const detectedPlatforms = detectPlatforms(options.dir);
|
|
1488
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', auditResult, {
|
|
1489
|
+
tags: [...options.snapshotTags, 'baseline'],
|
|
1490
|
+
milestone: 'baseline',
|
|
1491
|
+
sourceCommand: 'baseline init',
|
|
1492
|
+
managedBaseline: true,
|
|
1493
|
+
});
|
|
1494
|
+
const baselineRecord = buildManagedBaselineRecord({
|
|
1495
|
+
dir: options.dir,
|
|
1496
|
+
platform: options.platform,
|
|
1497
|
+
auditResult,
|
|
1498
|
+
analysisReport,
|
|
1499
|
+
snapshotArtifact: snapshot,
|
|
1500
|
+
currentPlatforms: detectedPlatforms,
|
|
1501
|
+
});
|
|
1502
|
+
const saved = writeManagedBaseline(options.dir, baselineRecord);
|
|
1503
|
+
|
|
1504
|
+
if (options.json) {
|
|
1505
|
+
console.log(JSON.stringify({
|
|
1506
|
+
...baselineRecord,
|
|
1507
|
+
baselinePath: saved.relativePath,
|
|
1508
|
+
}, null, 2));
|
|
1509
|
+
} else {
|
|
1510
|
+
console.log('');
|
|
1511
|
+
console.log(' nerviq baseline init');
|
|
1512
|
+
console.log(' ═══════════════════════════════════════');
|
|
1513
|
+
console.log(` Managed baseline written: ${saved.relativePath}`);
|
|
1514
|
+
console.log(` Snapshot: ${snapshot.relativePath}`);
|
|
1515
|
+
console.log(` Score: ${baselineRecord.baselineAudit.score}/100`);
|
|
1516
|
+
console.log(` Operating profile: ${baselineRecord.operatingProfile.label || 'n/a'}`);
|
|
1517
|
+
console.log(` Adoption plan: ${baselineRecord.adoptionPlan || 'n/a'}`);
|
|
1518
|
+
console.log(` Active platforms: ${(baselineRecord.detectedPlatforms || []).join(', ') || 'none detected'}`);
|
|
1519
|
+
console.log('');
|
|
1520
|
+
console.log(' Next:');
|
|
1521
|
+
console.log(' - nerviq audit --diff-only --drift-mode ci');
|
|
1522
|
+
console.log(' - nerviq watch');
|
|
1523
|
+
console.log(' - nerviq plan --campaign governance-hardening');
|
|
1524
|
+
console.log('');
|
|
1525
|
+
}
|
|
1526
|
+
process.exit(0);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
console.error('\n Error: baseline supports `init` and `status`.\n');
|
|
1530
|
+
process.exit(1);
|
|
1531
|
+
} else if (normalizedCommand === 'watch') {
|
|
1532
|
+
const { watch } = require('../src/watch');
|
|
1533
|
+
await watch(options);
|
|
1534
|
+
} else if (normalizedCommand === 'catalog') {
|
|
1528
1535
|
const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
1529
1536
|
if (options.out) {
|
|
1530
1537
|
const result = writeCatalogJson(options.out);
|
|
@@ -1595,10 +1602,11 @@ async function main() {
|
|
|
1595
1602
|
const address = server.address();
|
|
1596
1603
|
const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
|
|
1597
1604
|
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('');
|
|
1605
|
+
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1606
|
+
console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1607
|
+
console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
|
|
1608
|
+
console.log(' MCP hosts should use nerviq-mcp (stdio JSON-RPC 2.0), not this HTTP server.');
|
|
1609
|
+
console.log('');
|
|
1602
1610
|
|
|
1603
1611
|
const closeServer = () => {
|
|
1604
1612
|
server.close(() => process.exit(0));
|
|
@@ -1703,6 +1711,15 @@ async function main() {
|
|
|
1703
1711
|
const { runHarmonyGovernance } = require('../src/harmony/cli');
|
|
1704
1712
|
await runHarmonyGovernance(options);
|
|
1705
1713
|
process.exit(0);
|
|
1714
|
+
} else if (normalizedCommand === 'harmony-score') {
|
|
1715
|
+
const { runHarmonyScore } = require('../src/harmony/cli');
|
|
1716
|
+
const result = await runHarmonyScore(options);
|
|
1717
|
+
const threshold = parseInt(options.threshold, 10) || 0;
|
|
1718
|
+
process.exit(threshold > 0 && !result.pass ? 1 : 0);
|
|
1719
|
+
} else if (normalizedCommand === 'harmony-demo') {
|
|
1720
|
+
const { runHarmonyDemo } = require('../src/harmony/cli');
|
|
1721
|
+
await runHarmonyDemo(options);
|
|
1722
|
+
process.exit(0);
|
|
1706
1723
|
} else if (normalizedCommand === 'harmony-add') {
|
|
1707
1724
|
const { addPlatform } = require('../src/harmony/add');
|
|
1708
1725
|
const platformArg = parsed.extraArgs[0];
|
|
@@ -1823,74 +1840,74 @@ async function main() {
|
|
|
1823
1840
|
}
|
|
1824
1841
|
}
|
|
1825
1842
|
process.exit(0);
|
|
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') {
|
|
1843
|
+
} else if (normalizedCommand === 'suggest-rules') {
|
|
1844
|
+
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1845
|
+
const suggestions = analyzeSuggestions(options.dir);
|
|
1846
|
+
if (options.json) {
|
|
1847
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
1848
|
+
} else {
|
|
1849
|
+
console.log('');
|
|
1850
|
+
console.log(formatSuggestions(suggestions));
|
|
1851
|
+
console.log('');
|
|
1852
|
+
}
|
|
1853
|
+
process.exit(0);
|
|
1854
|
+
} else if (normalizedCommand === 'exception') {
|
|
1855
|
+
const {
|
|
1856
|
+
listExceptions,
|
|
1857
|
+
addException,
|
|
1858
|
+
pruneExpiredExceptions,
|
|
1859
|
+
formatExceptionsList,
|
|
1860
|
+
} = require('../src/continuous-ops');
|
|
1861
|
+
const subcommand = parsed.extraArgs[0] || 'list';
|
|
1862
|
+
|
|
1863
|
+
if (subcommand === 'list') {
|
|
1864
|
+
const records = listExceptions(options.dir);
|
|
1865
|
+
if (options.json) {
|
|
1866
|
+
console.log(JSON.stringify(records, null, 2));
|
|
1867
|
+
} else {
|
|
1868
|
+
console.log('');
|
|
1869
|
+
console.log(formatExceptionsList(records));
|
|
1870
|
+
console.log('');
|
|
1871
|
+
}
|
|
1872
|
+
process.exit(0);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
if (subcommand === 'add') {
|
|
1876
|
+
const result = addException(options.dir, {
|
|
1877
|
+
key: parsed.feedbackKey || null,
|
|
1878
|
+
watchClass: options.exceptionClass,
|
|
1879
|
+
owner: options.exceptionOwner,
|
|
1880
|
+
reason: options.exceptionReason,
|
|
1881
|
+
expiresAt: options.exceptionExpires,
|
|
1882
|
+
scope: options.exceptionScope || 'all',
|
|
1883
|
+
});
|
|
1884
|
+
if (options.json) {
|
|
1885
|
+
console.log(JSON.stringify(result.record, null, 2));
|
|
1886
|
+
} else {
|
|
1887
|
+
console.log('');
|
|
1888
|
+
console.log(` Exception added: ${result.record.id}`);
|
|
1889
|
+
console.log(` Target: ${result.record.key || result.record.watchClass}`);
|
|
1890
|
+
console.log(` Owner: ${result.record.owner}`);
|
|
1891
|
+
console.log(` Scope: ${result.record.scope}`);
|
|
1892
|
+
console.log(` Expires: ${result.record.expiresAt}`);
|
|
1893
|
+
console.log('');
|
|
1894
|
+
}
|
|
1895
|
+
process.exit(0);
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
if (subcommand === 'prune') {
|
|
1899
|
+
const result = pruneExpiredExceptions(options.dir);
|
|
1900
|
+
if (options.json) {
|
|
1901
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1902
|
+
} else {
|
|
1903
|
+
console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
|
|
1904
|
+
}
|
|
1905
|
+
process.exit(0);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
|
|
1909
|
+
process.exit(1);
|
|
1910
|
+
} else if (normalizedCommand === 'profile') {
|
|
1894
1911
|
const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
|
|
1895
1912
|
const subcommand = parsed.extraArgs[0];
|
|
1896
1913
|
const profileArg = parsed.extraArgs[1];
|
|
@@ -2432,117 +2449,117 @@ async function main() {
|
|
|
2432
2449
|
process.exit(0);
|
|
2433
2450
|
} else if (normalizedCommand === 'setup') {
|
|
2434
2451
|
await setup(options);
|
|
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
|
-
}
|
|
2445
|
-
}
|
|
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 });
|
|
2452
|
+
if (options.snapshot) {
|
|
2453
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2454
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
2455
|
+
tags: options.snapshotTags,
|
|
2456
|
+
milestone: options.snapshotMilestone,
|
|
2457
|
+
sourceCommand: 'setup',
|
|
2458
|
+
});
|
|
2459
|
+
if (!options.json) {
|
|
2460
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
} else {
|
|
2464
|
+
if (options.workspace) {
|
|
2465
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
2466
|
+
printWorkspaceSummary(summary, options);
|
|
2467
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
2468
|
+
process.exit(1);
|
|
2469
|
+
}
|
|
2470
|
+
process.exit(0);
|
|
2471
|
+
}
|
|
2472
|
+
let result;
|
|
2473
|
+
const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
|
|
2474
|
+
if (options.diffOnly) {
|
|
2475
|
+
const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
|
|
2476
|
+
const fullResult = await audit({ ...options, silent: true });
|
|
2477
|
+
const diffInfo = getChangedFiles(options.dir, {
|
|
2478
|
+
diffBase: options.diffBase,
|
|
2479
|
+
diffHead: options.diffHead,
|
|
2480
|
+
});
|
|
2481
|
+
result = buildDiffOnlyAuditView(fullResult, diffInfo);
|
|
2482
|
+
} else {
|
|
2483
|
+
result = renderAuditJsonLocally
|
|
2484
|
+
? await audit({ ...options, silent: true })
|
|
2485
|
+
: await audit(options);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
if (options.driftMode) {
|
|
2489
|
+
const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
|
|
2490
|
+
let campaigns = [];
|
|
2491
|
+
try {
|
|
2492
|
+
const planBundle = await buildProposalBundle({
|
|
2493
|
+
dir: options.dir,
|
|
2494
|
+
platform: options.platform,
|
|
2495
|
+
profile: options.profile,
|
|
2496
|
+
mcpPacks: options.mcpPacks,
|
|
2497
|
+
campaigns: [],
|
|
2498
|
+
});
|
|
2499
|
+
campaigns = planBundle.campaigns || [];
|
|
2500
|
+
} catch {
|
|
2501
|
+
campaigns = [];
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
result = {
|
|
2505
|
+
...result,
|
|
2506
|
+
continuousStatus: buildContinuousStatus({
|
|
2507
|
+
dir: options.dir,
|
|
2508
|
+
auditResult: result,
|
|
2509
|
+
mode: options.driftMode,
|
|
2510
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
2511
|
+
campaigns,
|
|
2512
|
+
}),
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2517
|
+
result = {
|
|
2518
|
+
...result,
|
|
2519
|
+
policyLayers: options.policyContract,
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
if (options.diffOnly) {
|
|
2524
|
+
const { printDiffOnlyAudit } = require('../src/diff-only');
|
|
2525
|
+
if (options.json) {
|
|
2526
|
+
console.log(JSON.stringify({
|
|
2527
|
+
version,
|
|
2528
|
+
timestamp: new Date().toISOString(),
|
|
2529
|
+
...result,
|
|
2530
|
+
}, null, 2));
|
|
2531
|
+
} else {
|
|
2532
|
+
console.log(printDiffOnlyAudit(result));
|
|
2533
|
+
if (result.continuousStatus) {
|
|
2534
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2535
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2536
|
+
console.log('');
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
} else if (renderAuditJsonLocally) {
|
|
2540
|
+
console.log(JSON.stringify({
|
|
2541
|
+
version,
|
|
2542
|
+
timestamp: new Date().toISOString(),
|
|
2543
|
+
...result,
|
|
2544
|
+
}, null, 2));
|
|
2545
|
+
} else {
|
|
2546
|
+
if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2547
|
+
console.log('');
|
|
2548
|
+
console.log(formatPolicyContract(options.policyContract));
|
|
2549
|
+
console.log('');
|
|
2550
|
+
}
|
|
2551
|
+
if (!options.json && result.continuousStatus) {
|
|
2552
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2553
|
+
console.log('');
|
|
2554
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2555
|
+
console.log('');
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
if (options.out) {
|
|
2559
|
+
const fs = require('fs');
|
|
2560
|
+
const path = require('path');
|
|
2561
|
+
const outPath = path.resolve(options.out);
|
|
2562
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2546
2563
|
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
2547
2564
|
if (!options.json) {
|
|
2548
2565
|
console.log(`\n Audit report written to ${options.out}\n`);
|
|
@@ -2550,39 +2567,39 @@ async function main() {
|
|
|
2550
2567
|
}
|
|
2551
2568
|
if (options.webhookUrl) {
|
|
2552
2569
|
try {
|
|
2553
|
-
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2570
|
+
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2554
2571
|
// Auto-detect Slack vs generic by URL pattern
|
|
2555
2572
|
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
2556
2573
|
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
2557
2574
|
let payload;
|
|
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
|
-
}
|
|
2575
|
+
if (isSlack) {
|
|
2576
|
+
payload = formatSlackMessage(result);
|
|
2577
|
+
} else if (isDiscord) {
|
|
2578
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
2579
|
+
payload = formatDiscordMessage(result);
|
|
2580
|
+
} else {
|
|
2581
|
+
payload = formatGenericAuditWebhookEvent(result);
|
|
2582
|
+
}
|
|
2583
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2584
|
+
headers: options.webhookHeaders,
|
|
2585
|
+
retries: options.webhookRetries,
|
|
2586
|
+
});
|
|
2587
|
+
if (!options.json) {
|
|
2588
|
+
if (webhookResp.ok) {
|
|
2589
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2590
|
+
console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
|
|
2591
|
+
} else {
|
|
2592
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2593
|
+
console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
} catch (webhookErr) {
|
|
2597
|
+
if (!options.json) {
|
|
2598
|
+
const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
|
|
2599
|
+
console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2586
2603
|
if (options.feedback && !options.json && options.format === null) {
|
|
2587
2604
|
const feedbackTargets = options.lite
|
|
2588
2605
|
? (result.liteSummary?.topNextActions || [])
|
|
@@ -2602,37 +2619,37 @@ async function main() {
|
|
|
2602
2619
|
console.log('');
|
|
2603
2620
|
}
|
|
2604
2621
|
}
|
|
2605
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2606
|
-
tags: options.snapshotTags,
|
|
2607
|
-
milestone: options.snapshotMilestone,
|
|
2608
|
-
sourceCommand: normalizedCommand,
|
|
2609
|
-
}) : null;
|
|
2622
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2623
|
+
tags: options.snapshotTags,
|
|
2624
|
+
milestone: options.snapshotMilestone,
|
|
2625
|
+
sourceCommand: normalizedCommand,
|
|
2626
|
+
}) : null;
|
|
2610
2627
|
if (snapshot && !options.json) {
|
|
2611
2628
|
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2612
2629
|
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
2613
2630
|
console.log('');
|
|
2614
2631
|
}
|
|
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.');
|
|
2632
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
2633
|
+
if (!options.json) {
|
|
2634
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2635
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2636
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2620
2637
|
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
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);
|
|
2638
|
+
}
|
|
2639
|
+
process.exit(1);
|
|
2640
|
+
}
|
|
2641
|
+
if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
|
|
2642
|
+
if (!options.json) {
|
|
2643
|
+
console.error('\n Error: Continuous drift gate failed.');
|
|
2644
|
+
console.error(` Why: ${result.continuousStatus.gateLabel}.`);
|
|
2645
|
+
console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
|
|
2646
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
2647
|
+
}
|
|
2648
|
+
process.exit(1);
|
|
2649
|
+
}
|
|
2650
|
+
if (options.require && options.require.length > 0) {
|
|
2651
|
+
const failedRequired = options.require.filter(key => {
|
|
2652
|
+
const check = result.results.find(r => r.key === key);
|
|
2636
2653
|
return !check || check.passed !== true;
|
|
2637
2654
|
});
|
|
2638
2655
|
if (failedRequired.length > 0) {
|