@nerviq/cli 1.13.0 → 1.15.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 +5 -2
- package/bin/cli.js +1067 -1008
- package/package.json +1 -1
- package/src/audit/recommendations.js +46 -0
- package/src/audit.js +127 -127
- package/src/context.js +23 -1
- package/src/harmony/cli.js +235 -0
- package/src/index.js +2 -0
- package/src/mcp-server.js +631 -314
- package/src/techniques/hygiene.js +2 -2
- package/src/techniques/quality.js +6 -5
package/bin/cli.js
CHANGED
|
@@ -8,15 +8,16 @@ const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, ren
|
|
|
8
8
|
const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
|
|
9
9
|
const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
10
|
const { collectFeedback } = require('../src/feedback');
|
|
11
|
+
const { collectAnonymousEvent } = require('../src/telemetry');
|
|
11
12
|
const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
|
|
12
13
|
const { startServer } = require('../src/server');
|
|
13
14
|
const { auditWorkspaces } = require('../src/workspace');
|
|
14
15
|
const { scanOrg } = require('../src/org');
|
|
15
16
|
const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
|
|
16
17
|
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');
|
|
18
|
+
const { init: initI18n, t } = require('../src/i18n');
|
|
19
|
+
const { version } = require('../package.json');
|
|
20
|
+
const { SNAPSHOT_MILESTONES } = require('../src/activity');
|
|
20
21
|
|
|
21
22
|
const args = process.argv.slice(2);
|
|
22
23
|
const COMMAND_ALIASES = {
|
|
@@ -29,7 +30,7 @@ const COMMAND_ALIASES = {
|
|
|
29
30
|
gov: 'governance',
|
|
30
31
|
outcome: 'feedback',
|
|
31
32
|
};
|
|
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'];
|
|
33
|
+
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
34
|
|
|
34
35
|
function levenshtein(a, b) {
|
|
35
36
|
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
@@ -48,7 +49,7 @@ function levenshtein(a, b) {
|
|
|
48
49
|
return matrix[a.length][b.length];
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function suggestCommand(input) {
|
|
52
|
+
function suggestCommand(input) {
|
|
52
53
|
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
53
54
|
let best = null;
|
|
54
55
|
let bestDistance = Infinity;
|
|
@@ -59,33 +60,33 @@ function suggestCommand(input) {
|
|
|
59
60
|
bestDistance = distance;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
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';
|
|
63
|
+
return bestDistance <= 3 ? best : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseNonNegativeIntegerFlag(value, flagName) {
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
69
|
+
throw new Error(`${flagName} requires a non-negative integer`);
|
|
70
|
+
}
|
|
71
|
+
return parsed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseWebhookHeader(rawValue) {
|
|
75
|
+
const separator = rawValue.indexOf(':');
|
|
76
|
+
if (separator <= 0) {
|
|
77
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
78
|
+
}
|
|
79
|
+
const name = rawValue.slice(0, separator).trim();
|
|
80
|
+
const value = rawValue.slice(separator + 1).trim();
|
|
81
|
+
if (!name || !value) {
|
|
82
|
+
throw new Error('--webhook-header requires NAME: VALUE');
|
|
83
|
+
}
|
|
84
|
+
return { name, value };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseArgs(rawArgs) {
|
|
88
|
+
const flags = [];
|
|
89
|
+
let command = 'audit';
|
|
89
90
|
let threshold = null;
|
|
90
91
|
let out = null;
|
|
91
92
|
let planFile = null;
|
|
@@ -98,47 +99,48 @@ function parseArgs(rawArgs) {
|
|
|
98
99
|
let feedbackEffect = null;
|
|
99
100
|
let feedbackNotes = null;
|
|
100
101
|
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
|
|
108
|
-
let
|
|
109
|
-
let
|
|
110
|
-
let
|
|
111
|
-
let
|
|
112
|
-
let
|
|
113
|
-
let
|
|
114
|
-
let
|
|
115
|
-
let
|
|
116
|
-
let
|
|
117
|
-
let
|
|
118
|
-
let
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
let
|
|
102
|
+
let feedbackScoreDelta = null;
|
|
103
|
+
let platform = 'claude';
|
|
104
|
+
let platformExplicit = false;
|
|
105
|
+
let format = null;
|
|
106
|
+
let port = null;
|
|
107
|
+
let workspace = null;
|
|
108
|
+
let targetDir = null;
|
|
109
|
+
let webhookUrl = null;
|
|
110
|
+
let webhookHeaders = [];
|
|
111
|
+
let webhookRetries = null;
|
|
112
|
+
let snapshotTags = [];
|
|
113
|
+
let snapshotMilestone = null;
|
|
114
|
+
let campaigns = [];
|
|
115
|
+
let diffBase = null;
|
|
116
|
+
let diffHead = null;
|
|
117
|
+
let driftMode = null;
|
|
118
|
+
let exceptionOwner = null;
|
|
119
|
+
let exceptionReason = null;
|
|
120
|
+
let exceptionExpires = null;
|
|
121
|
+
let exceptionScope = null;
|
|
122
|
+
let exceptionClass = null;
|
|
123
|
+
let commandSet = false;
|
|
122
124
|
let extraArgs = [];
|
|
123
125
|
let convertFrom = null;
|
|
124
126
|
let convertTo = null;
|
|
125
127
|
let migrateFrom = null;
|
|
126
128
|
let migrateTo = null;
|
|
127
129
|
let checkVersion = null;
|
|
128
|
-
let external = null;
|
|
129
|
-
let repos = [];
|
|
130
|
-
let teamProfile = null;
|
|
131
|
-
let lang = null;
|
|
132
|
-
let commandExplicit = false;
|
|
130
|
+
let external = null;
|
|
131
|
+
let repos = [];
|
|
132
|
+
let teamProfile = null;
|
|
133
|
+
let lang = null;
|
|
134
|
+
let commandExplicit = false;
|
|
133
135
|
|
|
134
136
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
135
137
|
const arg = rawArgs[i];
|
|
136
138
|
|
|
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
|
-
}
|
|
139
|
+
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 === '--dir' || 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') {
|
|
140
|
+
const value = rawArgs[i + 1];
|
|
141
|
+
if (!value || value.startsWith('--')) {
|
|
142
|
+
throw new Error(`${arg} requires a value`);
|
|
143
|
+
}
|
|
142
144
|
if (arg === '--threshold') threshold = value;
|
|
143
145
|
if (arg === '--out') out = value;
|
|
144
146
|
if (arg === '--plan') planFile = value;
|
|
@@ -152,33 +154,34 @@ function parseArgs(rawArgs) {
|
|
|
152
154
|
if (arg === '--notes') feedbackNotes = value;
|
|
153
155
|
if (arg === '--source') feedbackSource = value.trim();
|
|
154
156
|
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
155
|
-
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
157
|
+
if (arg === '--platform') { platform = value.trim().toLowerCase(); platformExplicit = true; }
|
|
158
|
+
if (arg === '--dir') targetDir = require('path').resolve(value.trim());
|
|
156
159
|
if (arg === '--format') format = value.trim().toLowerCase();
|
|
157
160
|
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
158
161
|
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
159
162
|
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
|
-
}
|
|
163
|
+
if (arg === '--workspace') workspace = value.trim();
|
|
164
|
+
if (arg === '--check-version') checkVersion = value.trim();
|
|
165
|
+
if (arg === '--webhook') webhookUrl = value.trim();
|
|
166
|
+
if (arg === '--webhook-header') webhookHeaders.push(parseWebhookHeader(value));
|
|
167
|
+
if (arg === '--webhook-retries') webhookRetries = parseNonNegativeIntegerFlag(value.trim(), '--webhook-retries');
|
|
168
|
+
if (arg === '--external') external = value.trim();
|
|
169
|
+
if (arg === '--team-profile') teamProfile = value.trim();
|
|
170
|
+
if (arg === '--lang') lang = value.trim().toLowerCase();
|
|
171
|
+
if (arg === '--tag') snapshotTags.push(value.trim());
|
|
172
|
+
if (arg === '--milestone') snapshotMilestone = value.trim().toLowerCase();
|
|
173
|
+
if (arg === '--campaign') campaigns = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
174
|
+
if (arg === '--diff-base') diffBase = value.trim();
|
|
175
|
+
if (arg === '--diff-head') diffHead = value.trim();
|
|
176
|
+
if (arg === '--drift-mode') driftMode = value.trim().toLowerCase();
|
|
177
|
+
if (arg === '--owner') exceptionOwner = value.trim();
|
|
178
|
+
if (arg === '--reason') exceptionReason = value;
|
|
179
|
+
if (arg === '--expires') exceptionExpires = value.trim();
|
|
180
|
+
if (arg === '--scope') exceptionScope = value.trim().toLowerCase();
|
|
181
|
+
if (arg === '--class') exceptionClass = value.trim().toLowerCase();
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
182
185
|
|
|
183
186
|
if (arg.startsWith('--lang=')) {
|
|
184
187
|
lang = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -190,65 +193,65 @@ function parseArgs(rawArgs) {
|
|
|
190
193
|
continue;
|
|
191
194
|
}
|
|
192
195
|
|
|
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
|
-
}
|
|
196
|
+
if (arg.startsWith('--external=')) {
|
|
197
|
+
external = arg.split('=').slice(1).join('=').trim();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (arg.startsWith('--tag=')) {
|
|
202
|
+
snapshotTags.push(arg.split('=').slice(1).join('=').trim());
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (arg.startsWith('--milestone=')) {
|
|
207
|
+
snapshotMilestone = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (arg.startsWith('--campaign=')) {
|
|
212
|
+
campaigns = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (arg.startsWith('--diff-base=')) {
|
|
217
|
+
diffBase = arg.split('=').slice(1).join('=').trim();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (arg.startsWith('--diff-head=')) {
|
|
222
|
+
diffHead = arg.split('=').slice(1).join('=').trim();
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (arg.startsWith('--drift-mode=')) {
|
|
227
|
+
driftMode = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (arg.startsWith('--owner=')) {
|
|
232
|
+
exceptionOwner = arg.split('=').slice(1).join('=').trim();
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (arg.startsWith('--reason=')) {
|
|
237
|
+
exceptionReason = arg.split('=').slice(1).join('=');
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (arg.startsWith('--expires=')) {
|
|
242
|
+
exceptionExpires = arg.split('=').slice(1).join('=').trim();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (arg.startsWith('--scope=')) {
|
|
247
|
+
exceptionScope = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (arg.startsWith('--class=')) {
|
|
252
|
+
exceptionClass = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
252
255
|
|
|
253
256
|
if (arg === '--repos') {
|
|
254
257
|
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
@@ -331,11 +334,16 @@ function parseArgs(rawArgs) {
|
|
|
331
334
|
continue;
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
if (arg.startsWith('--platform=')) {
|
|
335
|
-
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
336
|
-
platformExplicit = true;
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
337
|
+
if (arg.startsWith('--platform=')) {
|
|
338
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
339
|
+
platformExplicit = true;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (arg.startsWith('--dir=')) {
|
|
344
|
+
targetDir = require('path').resolve(arg.split('=').slice(1).join('=').trim());
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
339
347
|
|
|
340
348
|
if (arg.startsWith('--format=')) {
|
|
341
349
|
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
@@ -352,113 +360,113 @@ function parseArgs(rawArgs) {
|
|
|
352
360
|
continue;
|
|
353
361
|
}
|
|
354
362
|
|
|
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;
|
|
363
|
+
if (arg.startsWith('--check-version=')) {
|
|
364
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (arg.startsWith('--webhook=')) {
|
|
369
|
+
webhookUrl = arg.split('=').slice(1).join('=').trim();
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (arg.startsWith('--webhook-header=')) {
|
|
374
|
+
webhookHeaders.push(parseWebhookHeader(arg.split('=').slice(1).join('=')));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (arg.startsWith('--webhook-retries=')) {
|
|
379
|
+
webhookRetries = parseNonNegativeIntegerFlag(arg.split('=').slice(1).join('=').trim(), '--webhook-retries');
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (arg.startsWith('--')) {
|
|
384
|
+
flags.push(arg);
|
|
385
|
+
continue;
|
|
378
386
|
}
|
|
379
387
|
|
|
380
|
-
if (!commandSet) {
|
|
381
|
-
command = arg;
|
|
382
|
-
commandSet = true;
|
|
383
|
-
commandExplicit = true;
|
|
384
|
-
} else {
|
|
385
|
-
extraArgs.push(arg);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
+
if (!commandSet) {
|
|
389
|
+
command = arg;
|
|
390
|
+
commandSet = true;
|
|
391
|
+
commandExplicit = true;
|
|
392
|
+
} else {
|
|
393
|
+
extraArgs.push(arg);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
388
396
|
|
|
389
397
|
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
390
398
|
|
|
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;
|
|
399
|
+
return { flags, command, commandExplicit, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, platformExplicit, format, port, workspace, targetDir, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, webhookHeaders, webhookRetries, external, repos, teamProfile, lang, snapshotTags, snapshotMilestone, campaigns, diffBase, diffHead, driftMode, exceptionOwner, exceptionReason, exceptionExpires, exceptionScope, exceptionClass };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function printWorkspaceSummary(summary, options) {
|
|
403
|
+
if (options.json) {
|
|
404
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
405
|
+
return;
|
|
452
406
|
}
|
|
453
407
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
console.log(
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
console.log(
|
|
408
|
+
const rootScore = summary.rootGovernance?.score === null ? 'ERR' : `${summary.rootGovernance?.score ?? 0}/100`;
|
|
409
|
+
const workspaceAverage = summary.workspaceAggregate?.score ?? summary.averageScore;
|
|
410
|
+
|
|
411
|
+
console.log('');
|
|
412
|
+
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
413
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
414
|
+
console.log(` Root: ${summary.rootDir}`);
|
|
415
|
+
console.log(` Platform: ${summary.platform}`);
|
|
416
|
+
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
417
|
+
if (summary.patterns?.length > 0) {
|
|
418
|
+
console.log(` Selection: ${summary.patterns.join(', ')}`);
|
|
419
|
+
}
|
|
420
|
+
console.log(` Root governance audit: \x1b[1m${rootScore}\x1b[0m`);
|
|
421
|
+
console.log(` Workspace audit average: \x1b[1m${workspaceAverage}/100\x1b[0m`);
|
|
422
|
+
if (summary.profileBreakdown?.length > 0) {
|
|
423
|
+
const profileLine = summary.profileBreakdown
|
|
424
|
+
.map((item) => `${item.profileLabel} (${item.workspaceCount})`)
|
|
425
|
+
.join(', ');
|
|
426
|
+
console.log(` Workspace profiles: ${profileLine}`);
|
|
427
|
+
}
|
|
428
|
+
console.log(' Score semantics: root governance shows shared repo policy health; workspace average shows package-level coverage across the selected workspaces.');
|
|
429
|
+
console.log(' Aggregate vs package: per-workspace scores can legitimately trail the root repo score in a monorepo.');
|
|
430
|
+
console.log(' Stack-specific checks: Go, Python, Node, and other workspace types can have different applicable totals.');
|
|
431
|
+
console.log('');
|
|
432
|
+
console.log('\x1b[1m Workspace Profile Audit Pass Total Top action\x1b[0m');
|
|
433
|
+
console.log(' ' + '─'.repeat(96));
|
|
434
|
+
for (const item of summary.workspaces) {
|
|
435
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
436
|
+
const topAction = item.error || item.topAction || '-';
|
|
437
|
+
const profile = (item.workspaceProfile?.label || 'General workspace').slice(0, 20);
|
|
438
|
+
console.log(` ${item.workspace.padEnd(26)} ${profile.padEnd(20)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
|
|
439
|
+
if (item.stackLabels?.length > 0) {
|
|
440
|
+
console.log(`\x1b[2m Stacks: ${item.stackLabels.join(', ')}\x1b[0m`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
console.log('');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function printCompareCheckSection(title, items, prefix) {
|
|
447
|
+
if (!Array.isArray(items) || items.length === 0) return;
|
|
448
|
+
console.log(` ${title} (${items.length}):`);
|
|
449
|
+
for (const item of items) {
|
|
450
|
+
const impact = item.impact ? ` [${item.impact}]` : '';
|
|
451
|
+
const category = item.category ? ` — ${item.category}` : '';
|
|
452
|
+
console.log(` ${prefix} ${item.key}${impact}: ${item.name}${category}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function printScanDetail(summary, options) {
|
|
457
|
+
if (options.json) {
|
|
458
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
console.log('');
|
|
463
|
+
console.log('\x1b[1m nerviq scan — per-repo comparison\x1b[0m');
|
|
464
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
465
|
+
console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
466
|
+
if (summary.scoreSemantics?.note) {
|
|
467
|
+
console.log(` Score semantics: ${summary.scoreSemantics.note}`);
|
|
468
|
+
}
|
|
469
|
+
console.log('');
|
|
462
470
|
|
|
463
471
|
for (const item of summary.repos) {
|
|
464
472
|
if (item.error) {
|
|
@@ -467,12 +475,12 @@ function printScanDetail(summary, options) {
|
|
|
467
475
|
continue;
|
|
468
476
|
}
|
|
469
477
|
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
|
|
478
|
+
console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
|
|
479
|
+
if (item.policyCoverage?.layerKeys?.length > 0) {
|
|
480
|
+
console.log(` \x1b[2mPolicy layers: ${item.policyCoverage.layerKeys.join(' -> ')}\x1b[0m`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Show per-category breakdown if result is available
|
|
476
484
|
if (item.result && item.result.results) {
|
|
477
485
|
const STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
478
486
|
const categories = {};
|
|
@@ -501,86 +509,86 @@ function printScanDetail(summary, options) {
|
|
|
501
509
|
}
|
|
502
510
|
}
|
|
503
511
|
|
|
504
|
-
function printOrgSummary(summary, options) {
|
|
505
|
-
if (options.json) {
|
|
506
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
512
|
+
function printOrgSummary(summary, options) {
|
|
513
|
+
if (options.json) {
|
|
514
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
509
517
|
|
|
510
518
|
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
|
|
519
|
+
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
520
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
521
|
+
console.log(` Platform: ${summary.platform}`);
|
|
522
|
+
console.log(` Repos: ${summary.repoCount}`);
|
|
523
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
524
|
+
if (summary.scoreSemantics?.note) {
|
|
525
|
+
console.log(` Score semantics: ${summary.scoreSemantics.note}`);
|
|
526
|
+
}
|
|
527
|
+
if (summary.policyCoverage) {
|
|
528
|
+
console.log(` Policy coverage: org=${summary.policyCoverage.orgPolicyRepos} team=${summary.policyCoverage.teamPolicyRepos} repo=${summary.policyCoverage.repoPolicyRepos}`);
|
|
529
|
+
}
|
|
530
|
+
if (summary.scoreBands) {
|
|
531
|
+
console.log(` Bands: strong=${summary.scoreBands.strong} developing=${summary.scoreBands.developing} bootstrap=${summary.scoreBands.bootstrap} unknown=${summary.scoreBands.unknown}`);
|
|
532
|
+
}
|
|
533
|
+
console.log('');
|
|
534
|
+
console.log('\x1b[1m Repo Platform Score Policy Top action\x1b[0m');
|
|
535
|
+
console.log(' ' + '─'.repeat(72));
|
|
536
|
+
for (const item of summary.repos) {
|
|
537
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
538
|
+
const topAction = item.error || item.topAction || '-';
|
|
539
|
+
const policy = item.policyCoverage?.layerKeys?.length > 0 ? item.policyCoverage.layerKeys.join('/') : '-';
|
|
540
|
+
console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${policy.padEnd(12)} ${topAction}`);
|
|
541
|
+
}
|
|
542
|
+
if (Array.isArray(summary.topEvidence) && summary.topEvidence.length > 0) {
|
|
543
|
+
console.log('');
|
|
544
|
+
console.log(' Common top evidence:');
|
|
545
|
+
for (const item of summary.topEvidence) {
|
|
546
|
+
console.log(` - ${item.key} (${item.repoCount} repos)`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
console.log('');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function writeStdout(text) {
|
|
553
|
+
return new Promise((resolve, reject) => {
|
|
554
|
+
process.stdout.write(text, (error) => {
|
|
555
|
+
if (error) {
|
|
556
|
+
reject(error);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
resolve();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const HELP = `
|
|
565
|
+
nerviq v${version}
|
|
566
|
+
The intelligent nervous system for AI coding agents.
|
|
567
|
+
Audit, align, and amplify every platform on every project.
|
|
568
|
+
New here? Run: nerviq --beginner
|
|
569
|
+
|
|
570
|
+
DISCOVER
|
|
571
|
+
nerviq audit Quick scan: score + top 3 gaps (default)
|
|
572
|
+
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
573
|
+
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
574
|
+
nerviq audit --json Machine-readable JSON output (for CI)
|
|
575
|
+
nerviq audit --workspace packages/* Audit monorepo workspaces with stack-specific package profiles
|
|
576
|
+
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
577
|
+
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
578
|
+
nerviq org policy [dir] Inspect resolved org/team/repo policy layers
|
|
571
579
|
nerviq catalog Full check catalog (all 8 platforms)
|
|
572
580
|
nerviq catalog --json Export full check catalog as JSON
|
|
573
581
|
nerviq anti-patterns Detect anti-patterns in current project
|
|
574
582
|
nerviq anti-patterns --all Show full anti-pattern catalog
|
|
575
583
|
|
|
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
|
+
SETUP
|
|
585
|
+
nerviq setup Generate starter-safe baseline config files
|
|
586
|
+
nerviq setup --auto Apply all generated files without prompts
|
|
587
|
+
nerviq interactive Step-by-step guided wizard
|
|
588
|
+
nerviq baseline init Lock the first managed Nerviq baseline for continuous ops
|
|
589
|
+
nerviq baseline status Show the current managed baseline contract
|
|
590
|
+
nerviq check-health Detect regressions + platform format changes between snapshots
|
|
591
|
+
nerviq doctor Self-diagnostics: Node, deps, freshness, MCP, hook runtime
|
|
584
592
|
|
|
585
593
|
FIX
|
|
586
594
|
nerviq fix Show fixable checks and manual-fix guidance
|
|
@@ -593,53 +601,58 @@ const HELP = `
|
|
|
593
601
|
nerviq rollback --list Show available rollback points
|
|
594
602
|
nerviq rollback --dry-run Preview what would be deleted
|
|
595
603
|
|
|
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
|
|
604
|
+
IMPROVE
|
|
605
|
+
nerviq augment Improvement plan (no writes)
|
|
606
|
+
nerviq suggest-only Structured report for sharing (no writes)
|
|
607
|
+
nerviq plan Export proposal bundles with diffs
|
|
608
|
+
nerviq plan --campaign X Export a named upgrade campaign slice
|
|
609
|
+
nerviq plan --out plan.json Save plan to file
|
|
610
|
+
nerviq apply Apply proposals selectively with rollback
|
|
611
|
+
nerviq apply --campaign X Apply a named upgrade campaign
|
|
612
|
+
nerviq apply --dry-run Preview changes without writing
|
|
605
613
|
|
|
606
614
|
GOVERN
|
|
607
|
-
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
615
|
+
nerviq governance Permission profiles + hooks + policy packs (the rollout safety layer)
|
|
608
616
|
nerviq governance --json Machine-readable governance summary
|
|
609
|
-
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
617
|
+
nerviq benchmark Baseline vs projected score in isolated temp copy
|
|
610
618
|
nerviq benchmark --external /path Benchmark an external repo
|
|
611
619
|
nerviq freshness Show verification freshness for all checks
|
|
612
620
|
nerviq certify Generate certification badge for your project
|
|
613
621
|
|
|
614
622
|
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
|
|
623
|
+
nerviq harmony-audit Drift detection across all active platforms (GA)
|
|
624
|
+
nerviq harmony-sync Preview cross-platform sync (dry run, GA)
|
|
625
|
+
nerviq harmony-sync --fix Apply cross-platform sync (write files, GA)
|
|
626
|
+
nerviq harmony-sync --json JSON output for CI/automation
|
|
627
|
+
nerviq harmony-score Standalone Harmony Score (0-100) with badge + CI gate
|
|
628
|
+
nerviq harmony-score --badge Include shields.io badge markdown
|
|
629
|
+
nerviq harmony-score --threshold 70 CI gate: exit 1 if score < threshold
|
|
630
|
+
nerviq harmony-score --quiet Score number only (for piping)
|
|
631
|
+
nerviq harmony-demo Zero-setup demo — see Harmony in action instantly
|
|
632
|
+
nerviq harmony-add <platform> Add a new platform to the project
|
|
633
|
+
nerviq synergy-report [EXPERIMENTAL] Static-rule multi-agent amplification report
|
|
621
634
|
nerviq convert --from X --to Y Convert configs between platforms
|
|
622
635
|
nerviq migrate --platform X Platform version migration helper
|
|
623
636
|
nerviq migrate --platform cursor --from v2 --to v3
|
|
624
637
|
|
|
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
|
|
638
|
+
MONITOR
|
|
639
|
+
nerviq dashboard Generate static dashboard from latest audit snapshot (or live audit if none)
|
|
640
|
+
nerviq dashboard --out F Save dashboard to custom file
|
|
641
|
+
nerviq dashboard --open Open dashboard in browser after generating
|
|
642
|
+
nerviq watch Live config monitoring (re-audits on file change)
|
|
643
|
+
nerviq audit --diff-only --drift-mode ci PR / CI drift review against the managed baseline
|
|
644
|
+
nerviq history Audit snapshot history from saved snapshots
|
|
645
|
+
nerviq compare Detailed per-check diff between latest two audit snapshots
|
|
646
|
+
nerviq trend Audit snapshot trend over time
|
|
647
|
+
nerviq trend --out report.md Export trend report as markdown
|
|
648
|
+
nerviq audit --snapshot --milestone baseline --tag "baseline" Save a lifecycle checkpoint
|
|
649
|
+
nerviq feedback Record recommendation outcomes
|
|
650
|
+
|
|
651
|
+
EXCEPTIONS
|
|
652
|
+
nerviq exception add --key permissionDeny --owner team --reason "migration in progress" --expires 2026-05-01
|
|
653
|
+
nerviq exception add --class policy-drift --scope ci --owner team --reason "temporary rollout" --expires 2026-05-01
|
|
654
|
+
nerviq exception list Show active and expired exceptions
|
|
655
|
+
nerviq exception prune Remove expired exceptions
|
|
643
656
|
|
|
644
657
|
TEAM PROFILES
|
|
645
658
|
nerviq profile save <name> Save current preferences as a named profile
|
|
@@ -648,9 +661,9 @@ const HELP = `
|
|
|
648
661
|
nerviq profile export <name> Export profile JSON for sharing
|
|
649
662
|
|
|
650
663
|
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 HTTP API server + OpenAPI contract
|
|
664
|
+
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
665
|
+
nerviq deep-review --behavioral Local behavioral drift review (opt-in, no API)
|
|
666
|
+
nerviq serve --port 3000 Start local Nerviq HTTP API server + OpenAPI contract
|
|
654
667
|
nerviq badge Generate shields.io badge markdown
|
|
655
668
|
nerviq rules-export Export recommendation rules as JSON
|
|
656
669
|
nerviq rules-export --out F Save rules to file
|
|
@@ -658,6 +671,7 @@ const HELP = `
|
|
|
658
671
|
|
|
659
672
|
OPTIONS
|
|
660
673
|
--platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
|
|
674
|
+
--dir PATH Target directory to audit (default: current directory)
|
|
661
675
|
--threshold N Exit code 1 if score < N (CI gate)
|
|
662
676
|
--require A,B Exit code 1 if named checks fail
|
|
663
677
|
--out FILE Write output to file (JSON or markdown)
|
|
@@ -665,99 +679,101 @@ const HELP = `
|
|
|
665
679
|
--only A,B Limit plan/apply to selected proposal IDs
|
|
666
680
|
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
667
681
|
--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)
|
|
682
|
+
--mcp-pack A,B Merge MCP packs into setup (live tool connectors; e.g. context7-docs,next-devtools)
|
|
683
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
684
|
+
--format NAME Output format: json | sarif | otel
|
|
685
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
686
|
+
--webhook-header H Add a custom webhook header (repeat; format: Name: Value)
|
|
687
|
+
--webhook-retries N Retry transient webhook failures N times (default: 2)
|
|
688
|
+
--external PATH Benchmark an external repo instead of cwd
|
|
689
|
+
--port N Port for \`serve\` (default: 3000)
|
|
690
|
+
--workspace GLOBS Audit workspaces separately with root/package score semantics and stack-specific profiles
|
|
691
|
+
--diff-only Audit only changed files / linked config surfaces from git diff
|
|
692
|
+
--drift-mode M Continuous posture mode: ci | pr | watch
|
|
693
|
+
--diff-base SHA Base SHA for diff-only mode (defaults to PR env vars when present)
|
|
694
|
+
--diff-head SHA Head SHA for diff-only mode (defaults to GITHUB_SHA or HEAD)
|
|
695
|
+
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
696
|
+
--tag LABEL Tag the saved snapshot (use with --snapshot; repeat or comma-separate for more)
|
|
697
|
+
--milestone NAME Snapshot lifecycle milestone: baseline | post-fix | pre-upgrade | release
|
|
698
|
+
--campaign A,B Limit plan/apply to named upgrade campaigns
|
|
699
|
+
--full Show full audit output (all checks, weakest areas, badge)
|
|
686
700
|
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
687
701
|
--dry-run Preview changes without writing files
|
|
688
702
|
--config-only Only write config files (.claude/, rules, hooks) — never source code
|
|
689
703
|
--verbose Full audit + medium-priority recommendations
|
|
690
|
-
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
691
|
-
--json Output as JSON
|
|
692
|
-
--
|
|
693
|
-
--
|
|
694
|
-
--
|
|
695
|
-
--
|
|
696
|
-
--
|
|
697
|
-
--
|
|
698
|
-
--
|
|
699
|
-
--
|
|
700
|
-
--
|
|
701
|
-
--
|
|
702
|
-
--
|
|
703
|
-
--
|
|
704
|
-
--
|
|
705
|
-
--
|
|
706
|
-
--
|
|
707
|
-
--
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
npx nerviq
|
|
712
|
-
npx nerviq
|
|
704
|
+
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
705
|
+
--json Output as JSON
|
|
706
|
+
--agent-mode Non-interactive JSON output for AI agents (setup/audit)
|
|
707
|
+
--auto Apply all generated files without prompting
|
|
708
|
+
--beginner Show only the 5 starter commands for first-time users
|
|
709
|
+
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
710
|
+
--status VALUE Feedback: accepted | rejected | deferred
|
|
711
|
+
--effect VALUE Feedback: positive | neutral | negative
|
|
712
|
+
--score-delta N Feedback: observed score delta
|
|
713
|
+
--owner NAME Exception owner
|
|
714
|
+
--reason TEXT Exception reason
|
|
715
|
+
--expires DATE Exception expiry (ISO date or date-time)
|
|
716
|
+
--scope NAME Exception scope: all | ci | watch | pr
|
|
717
|
+
--class NAME Exception target class: policy-drift | config-drift | platform-drift | maturity-opportunity
|
|
718
|
+
--behavioral Run the opt-in local behavioral drift / outcome-layer review
|
|
719
|
+
--history With deep-review --behavioral, show behavioral snapshot history
|
|
720
|
+
--compare With deep-review --behavioral, compare the latest two behavioral snapshots
|
|
721
|
+
--help Show this help
|
|
722
|
+
--version Show version
|
|
723
|
+
|
|
724
|
+
EXAMPLES
|
|
725
|
+
npx nerviq --beginner
|
|
726
|
+
npx nerviq
|
|
727
|
+
npx nerviq --lite
|
|
713
728
|
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
|
|
729
|
+
npx nerviq audit --workspace packages/*
|
|
730
|
+
npx nerviq baseline init
|
|
731
|
+
npx nerviq audit --diff-only --drift-mode ci
|
|
732
|
+
npx nerviq --platform codex augment
|
|
733
|
+
npx nerviq org scan ./app ./api ./infra
|
|
734
|
+
npx nerviq org policy
|
|
720
735
|
npx nerviq scan ./app ./api ./infra
|
|
721
736
|
npx nerviq harmony-audit
|
|
722
737
|
npx nerviq convert --from claude --to codex
|
|
723
738
|
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
|
|
739
|
+
npx nerviq setup --mcp-pack context7-docs
|
|
740
|
+
npx nerviq plan --campaign governance-hardening
|
|
741
|
+
npx nerviq apply --plan plan.json --only hooks,commands
|
|
727
742
|
npx nerviq serve --port 4000
|
|
728
743
|
npx nerviq --json --threshold 70
|
|
729
744
|
npx nerviq catalog --json --out catalog.json
|
|
730
745
|
npx nerviq feedback --key permissionDeny --status accepted --effect positive
|
|
731
746
|
|
|
732
747
|
EXIT CODES
|
|
733
|
-
0 Success
|
|
734
|
-
1
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
nerviq
|
|
746
|
-
nerviq
|
|
747
|
-
nerviq
|
|
748
|
-
nerviq
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
748
|
+
0 Success (score meets threshold, or no threshold set)
|
|
749
|
+
1 Threshold not met (score below --threshold)
|
|
750
|
+
2 Runtime error (unknown command, missing files, crash)
|
|
751
|
+
`;
|
|
752
|
+
|
|
753
|
+
const BEGINNER_HELP = `
|
|
754
|
+
nerviq v${version}
|
|
755
|
+
Start here.
|
|
756
|
+
|
|
757
|
+
If this is your first time, learn just these 5 commands:
|
|
758
|
+
|
|
759
|
+
STARTER COMMANDS
|
|
760
|
+
nerviq audit Score the repo and show the top gaps
|
|
761
|
+
nerviq setup Generate a starter-safe baseline
|
|
762
|
+
nerviq fix Fix what can be fixed or show manual fix guidance
|
|
763
|
+
nerviq augment Show an improvement plan without writing
|
|
764
|
+
nerviq doctor Check install health, freshness, platform detection, MCP, and hook runtime
|
|
765
|
+
|
|
766
|
+
SIMPLE PATH
|
|
767
|
+
1. nerviq audit
|
|
768
|
+
2. nerviq setup --auto
|
|
769
|
+
3. nerviq fix --all-critical --auto
|
|
770
|
+
4. nerviq augment
|
|
771
|
+
5. nerviq doctor
|
|
772
|
+
|
|
773
|
+
WHEN YOU ARE READY
|
|
774
|
+
nerviq --help Show the full command set
|
|
775
|
+
Docs: https://nerviq.net/docs/getting-started
|
|
776
|
+
`;
|
|
761
777
|
|
|
762
778
|
async function main() {
|
|
763
779
|
let parsed;
|
|
@@ -765,32 +781,32 @@ async function main() {
|
|
|
765
781
|
parsed = parseArgs(args);
|
|
766
782
|
} catch (err) {
|
|
767
783
|
console.error(`\n Error: ${err.message}\n`);
|
|
768
|
-
process.exit(
|
|
784
|
+
process.exit(2);
|
|
769
785
|
}
|
|
770
786
|
|
|
771
|
-
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
787
|
+
const { flags, command, commandExplicit, normalizedCommand } = parsed;
|
|
772
788
|
|
|
773
789
|
// Initialize i18n with --lang flag or NERVIQ_LANG env var
|
|
774
790
|
if (parsed.lang) {
|
|
775
791
|
initI18n(parsed.lang);
|
|
776
792
|
}
|
|
777
793
|
|
|
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 = {
|
|
794
|
+
if (flags.includes('--version') || command === 'version') {
|
|
795
|
+
console.log(version);
|
|
796
|
+
process.exit(0);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (flags.includes('--beginner') && (!commandExplicit || flags.includes('--help') || command === 'help')) {
|
|
800
|
+
console.log(BEGINNER_HELP);
|
|
801
|
+
process.exit(0);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (flags.includes('--help') || command === 'help') {
|
|
805
|
+
console.log(HELP);
|
|
806
|
+
process.exit(0);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const options = {
|
|
794
810
|
verbose: flags.includes('--verbose'),
|
|
795
811
|
json: flags.includes('--json'),
|
|
796
812
|
auto: flags.includes('--auto'),
|
|
@@ -800,6 +816,9 @@ async function main() {
|
|
|
800
816
|
snapshot: flags.includes('--snapshot'),
|
|
801
817
|
feedback: flags.includes('--feedback'),
|
|
802
818
|
fix: flags.includes('--fix'),
|
|
819
|
+
badge: flags.includes('--badge'),
|
|
820
|
+
quiet: flags.includes('--quiet'),
|
|
821
|
+
agentMode: flags.includes('--agent-mode'),
|
|
803
822
|
autoSync: flags.includes('--auto-sync'),
|
|
804
823
|
dryRun: flags.includes('--dry-run'),
|
|
805
824
|
configOnly: flags.includes('--config-only'),
|
|
@@ -809,82 +828,82 @@ async function main() {
|
|
|
809
828
|
only: parsed.only,
|
|
810
829
|
profile: parsed.profile,
|
|
811
830
|
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(
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
if (options.snapshotMilestone && !options.snapshot) {
|
|
847
|
-
console.error('\n Error: --milestone requires --snapshot.\n');
|
|
848
|
-
process.exit(
|
|
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(
|
|
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(
|
|
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(
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
if (parsed.checkVersion) {
|
|
831
|
+
require: parsed.requireChecks,
|
|
832
|
+
platform: parsed.platform || 'claude',
|
|
833
|
+
platformExplicit: Boolean(parsed.platformExplicit),
|
|
834
|
+
format: parsed.format || null,
|
|
835
|
+
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
836
|
+
workspace: parsed.workspace || null,
|
|
837
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
838
|
+
webhookHeaders: Object.fromEntries((parsed.webhookHeaders || []).map((entry) => [entry.name, entry.value])),
|
|
839
|
+
webhookRetries: parsed.webhookRetries ?? 2,
|
|
840
|
+
lang: parsed.lang || null,
|
|
841
|
+
external: parsed.external || null,
|
|
842
|
+
snapshotTags: parsed.snapshotTags || [],
|
|
843
|
+
snapshotMilestone: parsed.snapshotMilestone || null,
|
|
844
|
+
campaigns: parsed.campaigns || [],
|
|
845
|
+
behavioral: flags.includes('--behavioral'),
|
|
846
|
+
historyView: flags.includes('--history'),
|
|
847
|
+
compareView: flags.includes('--compare'),
|
|
848
|
+
diffOnly: flags.includes('--diff-only'),
|
|
849
|
+
diffBase: parsed.diffBase || null,
|
|
850
|
+
diffHead: parsed.diffHead || null,
|
|
851
|
+
driftMode: parsed.driftMode || null,
|
|
852
|
+
exceptionOwner: parsed.exceptionOwner || null,
|
|
853
|
+
exceptionReason: parsed.exceptionReason || null,
|
|
854
|
+
exceptionExpires: parsed.exceptionExpires || null,
|
|
855
|
+
exceptionScope: parsed.exceptionScope || null,
|
|
856
|
+
exceptionClass: parsed.exceptionClass || null,
|
|
857
|
+
dir: parsed.targetDir || process.cwd()
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
if (options.snapshotTags.length > 0 && !options.snapshot) {
|
|
861
|
+
console.error('\n Error: --tag requires --snapshot.\n');
|
|
862
|
+
process.exit(2);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (options.snapshotMilestone && !options.snapshot) {
|
|
866
|
+
console.error('\n Error: --milestone requires --snapshot.\n');
|
|
867
|
+
process.exit(2);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (options.snapshotMilestone && !SNAPSHOT_MILESTONES.includes(options.snapshotMilestone)) {
|
|
871
|
+
console.error(`\n Error: Unsupported milestone '${options.snapshotMilestone}'. Use one of: ${SNAPSHOT_MILESTONES.join(', ')}.\n`);
|
|
872
|
+
process.exit(2);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (options.diffOnly && options.snapshot) {
|
|
876
|
+
console.error('\n Error: --diff-only cannot be combined with --snapshot because diff-only scores are not comparable to full audit snapshots.\n');
|
|
877
|
+
process.exit(2);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (options.driftMode && !['ci', 'pr', 'watch'].includes(options.driftMode)) {
|
|
881
|
+
console.error(`\n Error: Unsupported drift mode '${options.driftMode}'. Use ci, pr, or watch.\n`);
|
|
882
|
+
process.exit(2);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (parsed.checkVersion) {
|
|
867
886
|
if (parsed.checkVersion !== version) {
|
|
868
887
|
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
869
888
|
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
870
889
|
console.error('');
|
|
871
890
|
}
|
|
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');
|
|
891
|
+
options.checkVersion = parsed.checkVersion;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const {
|
|
895
|
+
resolvePolicyLayers,
|
|
896
|
+
applyPolicyLayersToOptions,
|
|
897
|
+
formatPolicyContract,
|
|
898
|
+
} = require('../src/policy-layers');
|
|
899
|
+
const inheritedPolicyContract = resolvePolicyLayers(options.dir);
|
|
900
|
+
if (inheritedPolicyContract.layers.some((layer) => layer.valid)) {
|
|
901
|
+
Object.assign(options, applyPolicyLayersToOptions(inheritedPolicyContract, options));
|
|
902
|
+
options.policyContract = inheritedPolicyContract;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (parsed.teamProfile) {
|
|
906
|
+
const { loadProfile, applyProfileToOptions } = require('../src/profiles');
|
|
888
907
|
try {
|
|
889
908
|
const teamProf = loadProfile(options.dir, parsed.teamProfile);
|
|
890
909
|
const merged = applyProfileToOptions(teamProf, options);
|
|
@@ -923,15 +942,15 @@ async function main() {
|
|
|
923
942
|
process.exit(1);
|
|
924
943
|
}
|
|
925
944
|
|
|
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
|
-
}
|
|
945
|
+
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
946
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (options.driftMode && options.format !== null) {
|
|
951
|
+
console.error('\n Error: --drift-mode is only supported with normal text output or --json.\n');
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
935
954
|
|
|
936
955
|
if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
|
|
937
956
|
console.error('\n Error: --port must be an integer between 0 and 65535.\n');
|
|
@@ -960,7 +979,7 @@ async function main() {
|
|
|
960
979
|
console.error(' Fix: Run nerviq --help to see all available commands.');
|
|
961
980
|
}
|
|
962
981
|
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
963
|
-
process.exit(
|
|
982
|
+
process.exit(2);
|
|
964
983
|
}
|
|
965
984
|
|
|
966
985
|
if (!require('fs').existsSync(options.dir)) {
|
|
@@ -968,7 +987,7 @@ async function main() {
|
|
|
968
987
|
console.error(' Why: The current working directory does not exist or is not accessible.');
|
|
969
988
|
console.error(' Fix: cd into your project directory first, then run nerviq.');
|
|
970
989
|
console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
|
|
971
|
-
process.exit(
|
|
990
|
+
process.exit(2);
|
|
972
991
|
}
|
|
973
992
|
|
|
974
993
|
if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
|
|
@@ -981,13 +1000,13 @@ async function main() {
|
|
|
981
1000
|
}
|
|
982
1001
|
|
|
983
1002
|
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',
|
|
1003
|
+
const FULL_COMMAND_SET = new Set([
|
|
1004
|
+
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
1005
|
+
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
1006
|
+
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'baseline', 'exception', 'help', 'version',
|
|
1007
|
+
// Harmony + Synergy (cross-platform)
|
|
1008
|
+
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
1009
|
+
'harmony-watch', 'harmony-governance', 'harmony-score', 'harmony-demo', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
991
1010
|
'freshness', 'profile', 'migrate',
|
|
992
1011
|
]);
|
|
993
1012
|
|
|
@@ -995,7 +1014,7 @@ async function main() {
|
|
|
995
1014
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
996
1015
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
|
|
997
1016
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
998
|
-
process.exit(
|
|
1017
|
+
process.exit(2);
|
|
999
1018
|
}
|
|
1000
1019
|
}
|
|
1001
1020
|
|
|
@@ -1003,7 +1022,7 @@ async function main() {
|
|
|
1003
1022
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1004
1023
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
|
|
1005
1024
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1006
|
-
process.exit(
|
|
1025
|
+
process.exit(2);
|
|
1007
1026
|
}
|
|
1008
1027
|
}
|
|
1009
1028
|
|
|
@@ -1011,7 +1030,7 @@ async function main() {
|
|
|
1011
1030
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1012
1031
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
|
|
1013
1032
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1014
|
-
process.exit(
|
|
1033
|
+
process.exit(2);
|
|
1015
1034
|
}
|
|
1016
1035
|
}
|
|
1017
1036
|
|
|
@@ -1019,7 +1038,7 @@ async function main() {
|
|
|
1019
1038
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1020
1039
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
|
|
1021
1040
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1022
|
-
process.exit(
|
|
1041
|
+
process.exit(2);
|
|
1023
1042
|
}
|
|
1024
1043
|
}
|
|
1025
1044
|
|
|
@@ -1028,59 +1047,59 @@ async function main() {
|
|
|
1028
1047
|
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
1029
1048
|
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
|
|
1030
1049
|
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
1031
|
-
process.exit(
|
|
1050
|
+
process.exit(2);
|
|
1032
1051
|
}
|
|
1033
1052
|
}
|
|
1034
1053
|
}
|
|
1035
1054
|
|
|
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(
|
|
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(
|
|
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
|
|
1055
|
+
if (normalizedCommand === 'scan') {
|
|
1056
|
+
const scanDirs = parsed.extraArgs;
|
|
1057
|
+
if (scanDirs.length === 0) {
|
|
1058
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
1059
|
+
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
1060
|
+
process.exit(2);
|
|
1061
|
+
}
|
|
1062
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1063
|
+
printScanDetail(summary, options);
|
|
1064
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
process.exit(0);
|
|
1068
|
+
} else if (normalizedCommand === 'org') {
|
|
1069
|
+
const subcommand = parsed.extraArgs[0];
|
|
1070
|
+
if (subcommand === 'policy') {
|
|
1071
|
+
const targetDir = parsed.extraArgs[1] ? require('path').resolve(parsed.extraArgs[1]) : options.dir;
|
|
1072
|
+
const contract = resolvePolicyLayers(targetDir);
|
|
1073
|
+
if (options.json) {
|
|
1074
|
+
await writeStdout(JSON.stringify(contract, null, 2) + '\n');
|
|
1075
|
+
} else {
|
|
1076
|
+
console.log('');
|
|
1077
|
+
console.log(formatPolicyContract(contract));
|
|
1078
|
+
console.log('');
|
|
1079
|
+
}
|
|
1080
|
+
process.exit(0);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const scanDirs = parsed.extraArgs.slice(1);
|
|
1084
|
+
if (subcommand !== 'scan' || scanDirs.length === 0) {
|
|
1085
|
+
console.error('\n Error: org requires `scan` or `policy`.');
|
|
1086
|
+
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3');
|
|
1087
|
+
console.error(' npx nerviq org policy [dir]\n');
|
|
1088
|
+
process.exit(2);
|
|
1089
|
+
}
|
|
1090
|
+
const summary = await scanOrg(scanDirs, options);
|
|
1091
|
+
if (options.json) {
|
|
1092
|
+
await writeStdout(JSON.stringify(summary, null, 2) + '\n');
|
|
1093
|
+
} else {
|
|
1094
|
+
printOrgSummary(summary, options);
|
|
1095
|
+
}
|
|
1096
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
process.exit(0);
|
|
1100
|
+
} else if (normalizedCommand === 'history') {
|
|
1101
|
+
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
1102
|
+
// Handle --prune N
|
|
1084
1103
|
const pruneIdx = flags.indexOf('--prune');
|
|
1085
1104
|
if (pruneIdx >= 0) {
|
|
1086
1105
|
const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
|
|
@@ -1088,7 +1107,7 @@ async function main() {
|
|
|
1088
1107
|
const pathMod = require('path');
|
|
1089
1108
|
const entries = readSnapshotIndex(options.dir);
|
|
1090
1109
|
if (entries.length <= keepCount) {
|
|
1091
|
-
console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
|
|
1110
|
+
console.log(`\n Nothing to prune (${entries.length} audit snapshots, keeping ${keepCount}).\n`);
|
|
1092
1111
|
} else {
|
|
1093
1112
|
const toRemove = entries.slice(0, entries.length - keepCount);
|
|
1094
1113
|
let removed = 0;
|
|
@@ -1099,78 +1118,78 @@ async function main() {
|
|
|
1099
1118
|
const kept = entries.slice(entries.length - keepCount);
|
|
1100
1119
|
const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
|
|
1101
1120
|
try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
|
|
1102
|
-
console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
|
|
1121
|
+
console.log(`\n Pruned ${removed} audit snapshots, kept ${kept.length}.\n`);
|
|
1103
1122
|
}
|
|
1104
1123
|
process.exit(0);
|
|
1105
1124
|
}
|
|
1106
1125
|
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
|
-
}
|
|
1126
|
+
console.log(formatHistory(options.dir));
|
|
1127
|
+
console.log('');
|
|
1128
|
+
process.exit(0);
|
|
1129
|
+
} else if (normalizedCommand === 'compare') {
|
|
1130
|
+
const { compareLatest, formatSnapshotBootstrap, formatSnapshotTags, formatSnapshotMilestone } = require('../src/activity');
|
|
1131
|
+
const result = compareLatest(options.dir);
|
|
1132
|
+
if (!result) {
|
|
1133
|
+
console.log('');
|
|
1134
|
+
console.log(formatSnapshotBootstrap(options.dir, 'compare'));
|
|
1135
|
+
console.log('');
|
|
1136
|
+
process.exit(0);
|
|
1137
|
+
}
|
|
1119
1138
|
if (options.json) {
|
|
1120
1139
|
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
|
-
}
|
|
1140
|
+
} else {
|
|
1141
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
1142
|
+
console.log('');
|
|
1143
|
+
console.log(` Previous snapshot: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})${formatSnapshotMilestone(result.previous.milestone)}${formatSnapshotTags(result.previous.tags)}`);
|
|
1144
|
+
console.log(` Current snapshot: ${result.current.score}/100 (${result.current.date?.split('T')[0]})${formatSnapshotMilestone(result.current.milestone)}${formatSnapshotTags(result.current.tags)}`);
|
|
1145
|
+
console.log(` Snapshot delta: ${sign}${result.delta.score} points`);
|
|
1146
|
+
console.log(` Trend: ${result.trend}`);
|
|
1147
|
+
if (result.detailedDiffAvailable) {
|
|
1148
|
+
console.log('');
|
|
1149
|
+
console.log(' Detailed check diff:');
|
|
1150
|
+
printCompareCheckSection('Regressions', result.regressionDetails, '🔴');
|
|
1151
|
+
printCompareCheckSection('Improvements', result.improvementDetails, '✅');
|
|
1152
|
+
printCompareCheckSection('Newly applicable', result.newlyApplicableDetails, '🆕');
|
|
1153
|
+
printCompareCheckSection('No longer applicable', result.noLongerApplicableDetails, '↩');
|
|
1154
|
+
if (Array.isArray(result.newChecks) && result.newChecks.length > 0) {
|
|
1155
|
+
printCompareCheckSection('New checks', result.newChecks, '➕');
|
|
1156
|
+
}
|
|
1157
|
+
if (Array.isArray(result.removedChecks) && result.removedChecks.length > 0) {
|
|
1158
|
+
printCompareCheckSection('Removed checks', result.removedChecks, '➖');
|
|
1159
|
+
}
|
|
1160
|
+
if (
|
|
1161
|
+
result.regressionDetails.length === 0 &&
|
|
1162
|
+
result.improvementDetails.length === 0 &&
|
|
1163
|
+
result.newlyApplicableDetails.length === 0 &&
|
|
1164
|
+
result.noLongerApplicableDetails.length === 0 &&
|
|
1165
|
+
result.newChecks.length === 0 &&
|
|
1166
|
+
result.removedChecks.length === 0
|
|
1167
|
+
) {
|
|
1168
|
+
console.log(' No per-check state changes detected.');
|
|
1169
|
+
}
|
|
1170
|
+
} else {
|
|
1171
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
1172
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
1173
|
+
}
|
|
1174
|
+
console.log('');
|
|
1175
|
+
}
|
|
1176
|
+
process.exit(0);
|
|
1177
|
+
} else if (normalizedCommand === 'trend') {
|
|
1178
|
+
const { exportTrendReport, getHistory, formatSnapshotBootstrap } = require('../src/activity');
|
|
1179
|
+
const auditHistory = getHistory(options.dir, 2);
|
|
1180
|
+
if (auditHistory.length < 2) {
|
|
1181
|
+
console.log('');
|
|
1182
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1183
|
+
console.log('');
|
|
1184
|
+
process.exit(0);
|
|
1185
|
+
}
|
|
1186
|
+
const report = exportTrendReport(options.dir);
|
|
1187
|
+
if (!report) {
|
|
1188
|
+
console.log('');
|
|
1189
|
+
console.log(formatSnapshotBootstrap(options.dir, 'trend'));
|
|
1190
|
+
console.log('');
|
|
1191
|
+
process.exit(0);
|
|
1192
|
+
}
|
|
1174
1193
|
if (options.out) {
|
|
1175
1194
|
require('fs').writeFileSync(options.out, report, 'utf8');
|
|
1176
1195
|
console.log(`\n Trend report exported to ${options.out}\n`);
|
|
@@ -1272,11 +1291,11 @@ async function main() {
|
|
|
1272
1291
|
process.exit(0);
|
|
1273
1292
|
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
1274
1293
|
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;
|
|
1294
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
1295
|
+
tags: options.snapshotTags,
|
|
1296
|
+
milestone: options.snapshotMilestone,
|
|
1297
|
+
sourceCommand: normalizedCommand,
|
|
1298
|
+
}) : null;
|
|
1280
1299
|
if (options.out && !options.json) {
|
|
1281
1300
|
const fs = require('fs');
|
|
1282
1301
|
const md = exportMarkdown(report);
|
|
@@ -1407,11 +1426,11 @@ async function main() {
|
|
|
1407
1426
|
fs.writeFileSync(options.out, content, 'utf8');
|
|
1408
1427
|
}
|
|
1409
1428
|
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;
|
|
1429
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
1430
|
+
tags: options.snapshotTags,
|
|
1431
|
+
milestone: options.snapshotMilestone,
|
|
1432
|
+
sourceCommand: normalizedCommand,
|
|
1433
|
+
}) : null;
|
|
1415
1434
|
if (options.out && !options.json) {
|
|
1416
1435
|
console.log(` Governance report written to ${options.out}`);
|
|
1417
1436
|
console.log('');
|
|
@@ -1423,11 +1442,11 @@ async function main() {
|
|
|
1423
1442
|
}
|
|
1424
1443
|
} else if (normalizedCommand === 'benchmark') {
|
|
1425
1444
|
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;
|
|
1445
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
1446
|
+
tags: options.snapshotTags,
|
|
1447
|
+
milestone: options.snapshotMilestone,
|
|
1448
|
+
sourceCommand: normalizedCommand,
|
|
1449
|
+
}) : null;
|
|
1431
1450
|
if (options.out) {
|
|
1432
1451
|
writeBenchmarkReport(report, options.out);
|
|
1433
1452
|
}
|
|
@@ -1444,87 +1463,87 @@ async function main() {
|
|
|
1444
1463
|
} else if (normalizedCommand === 'deep-review') {
|
|
1445
1464
|
const { deepReview } = require('../src/deep-review');
|
|
1446
1465
|
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') {
|
|
1466
|
+
} else if (normalizedCommand === 'interactive') {
|
|
1467
|
+
const { interactive } = require('../src/interactive');
|
|
1468
|
+
await interactive(options);
|
|
1469
|
+
} else if (normalizedCommand === 'baseline') {
|
|
1470
|
+
const {
|
|
1471
|
+
readManagedBaseline,
|
|
1472
|
+
writeManagedBaseline,
|
|
1473
|
+
buildManagedBaselineRecord,
|
|
1474
|
+
formatManagedBaselineStatus,
|
|
1475
|
+
} = require('../src/continuous-ops');
|
|
1476
|
+
const subcommand = parsed.extraArgs[0] || 'status';
|
|
1477
|
+
|
|
1478
|
+
if (subcommand === 'status') {
|
|
1479
|
+
const baseline = readManagedBaseline(options.dir);
|
|
1480
|
+
if (options.json) {
|
|
1481
|
+
console.log(JSON.stringify(baseline, null, 2));
|
|
1482
|
+
} else {
|
|
1483
|
+
console.log('');
|
|
1484
|
+
console.log(formatManagedBaselineStatus(options.dir, baseline));
|
|
1485
|
+
console.log('');
|
|
1486
|
+
}
|
|
1487
|
+
process.exit(0);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (subcommand === 'init') {
|
|
1491
|
+
const existingBaseline = readManagedBaseline(options.dir);
|
|
1492
|
+
if (existingBaseline && !flags.includes('--force')) {
|
|
1493
|
+
console.error('\n Error: Managed baseline already exists. Use `nerviq baseline status` to inspect it, or rerun with --force to replace it.\n');
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const auditResult = await audit({ ...options, silent: true });
|
|
1498
|
+
const analysisReport = await analyzeProject({ ...options, mode: 'augment' });
|
|
1499
|
+
const detectedPlatforms = detectPlatforms(options.dir);
|
|
1500
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', auditResult, {
|
|
1501
|
+
tags: [...options.snapshotTags, 'baseline'],
|
|
1502
|
+
milestone: 'baseline',
|
|
1503
|
+
sourceCommand: 'baseline init',
|
|
1504
|
+
managedBaseline: true,
|
|
1505
|
+
});
|
|
1506
|
+
const baselineRecord = buildManagedBaselineRecord({
|
|
1507
|
+
dir: options.dir,
|
|
1508
|
+
platform: options.platform,
|
|
1509
|
+
auditResult,
|
|
1510
|
+
analysisReport,
|
|
1511
|
+
snapshotArtifact: snapshot,
|
|
1512
|
+
currentPlatforms: detectedPlatforms,
|
|
1513
|
+
});
|
|
1514
|
+
const saved = writeManagedBaseline(options.dir, baselineRecord);
|
|
1515
|
+
|
|
1516
|
+
if (options.json) {
|
|
1517
|
+
console.log(JSON.stringify({
|
|
1518
|
+
...baselineRecord,
|
|
1519
|
+
baselinePath: saved.relativePath,
|
|
1520
|
+
}, null, 2));
|
|
1521
|
+
} else {
|
|
1522
|
+
console.log('');
|
|
1523
|
+
console.log(' nerviq baseline init');
|
|
1524
|
+
console.log(' ═══════════════════════════════════════');
|
|
1525
|
+
console.log(` Managed baseline written: ${saved.relativePath}`);
|
|
1526
|
+
console.log(` Snapshot: ${snapshot.relativePath}`);
|
|
1527
|
+
console.log(` Score: ${baselineRecord.baselineAudit.score}/100`);
|
|
1528
|
+
console.log(` Operating profile: ${baselineRecord.operatingProfile.label || 'n/a'}`);
|
|
1529
|
+
console.log(` Adoption plan: ${baselineRecord.adoptionPlan || 'n/a'}`);
|
|
1530
|
+
console.log(` Active platforms: ${(baselineRecord.detectedPlatforms || []).join(', ') || 'none detected'}`);
|
|
1531
|
+
console.log('');
|
|
1532
|
+
console.log(' Next:');
|
|
1533
|
+
console.log(' - nerviq audit --diff-only --drift-mode ci');
|
|
1534
|
+
console.log(' - nerviq watch');
|
|
1535
|
+
console.log(' - nerviq plan --campaign governance-hardening');
|
|
1536
|
+
console.log('');
|
|
1537
|
+
}
|
|
1538
|
+
process.exit(0);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
console.error('\n Error: baseline supports `init` and `status`.\n');
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
} else if (normalizedCommand === 'watch') {
|
|
1544
|
+
const { watch } = require('../src/watch');
|
|
1545
|
+
await watch(options);
|
|
1546
|
+
} else if (normalizedCommand === 'catalog') {
|
|
1528
1547
|
const { generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
1529
1548
|
if (options.out) {
|
|
1530
1549
|
const result = writeCatalogJson(options.out);
|
|
@@ -1595,11 +1614,11 @@ async function main() {
|
|
|
1595
1614
|
const address = server.address();
|
|
1596
1615
|
const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
|
|
1597
1616
|
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(' MCP hosts should use nerviq-mcp (stdio JSON-RPC 2.0), not this HTTP server.');
|
|
1602
|
-
console.log('');
|
|
1617
|
+
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1618
|
+
console.log(' Endpoints: /api/openapi.json, /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1619
|
+
console.log(` Contract: http://127.0.0.1:${resolvedPort}/api/openapi.json`);
|
|
1620
|
+
console.log(' MCP hosts should use nerviq-mcp (stdio JSON-RPC 2.0), not this HTTP server.');
|
|
1621
|
+
console.log('');
|
|
1603
1622
|
|
|
1604
1623
|
const closeServer = () => {
|
|
1605
1624
|
server.close(() => process.exit(0));
|
|
@@ -1610,6 +1629,7 @@ async function main() {
|
|
|
1610
1629
|
return;
|
|
1611
1630
|
} else if (normalizedCommand === 'harmony-audit') {
|
|
1612
1631
|
const { runHarmonyAudit } = require('../src/harmony/cli');
|
|
1632
|
+
collectAnonymousEvent('harmony-audit', { dir: options.dir });
|
|
1613
1633
|
await runHarmonyAudit(options);
|
|
1614
1634
|
process.exit(0);
|
|
1615
1635
|
} else if (normalizedCommand === 'harmony-sync') {
|
|
@@ -1704,6 +1724,15 @@ async function main() {
|
|
|
1704
1724
|
const { runHarmonyGovernance } = require('../src/harmony/cli');
|
|
1705
1725
|
await runHarmonyGovernance(options);
|
|
1706
1726
|
process.exit(0);
|
|
1727
|
+
} else if (normalizedCommand === 'harmony-score') {
|
|
1728
|
+
const { runHarmonyScore } = require('../src/harmony/cli');
|
|
1729
|
+
const result = await runHarmonyScore(options);
|
|
1730
|
+
const threshold = parseInt(options.threshold, 10) || 0;
|
|
1731
|
+
process.exit(threshold > 0 && !result.pass ? 1 : 0);
|
|
1732
|
+
} else if (normalizedCommand === 'harmony-demo') {
|
|
1733
|
+
const { runHarmonyDemo } = require('../src/harmony/cli');
|
|
1734
|
+
await runHarmonyDemo(options);
|
|
1735
|
+
process.exit(0);
|
|
1707
1736
|
} else if (normalizedCommand === 'harmony-add') {
|
|
1708
1737
|
const { addPlatform } = require('../src/harmony/add');
|
|
1709
1738
|
const platformArg = parsed.extraArgs[0];
|
|
@@ -1824,74 +1853,74 @@ async function main() {
|
|
|
1824
1853
|
}
|
|
1825
1854
|
}
|
|
1826
1855
|
process.exit(0);
|
|
1827
|
-
} else if (normalizedCommand === 'suggest-rules') {
|
|
1828
|
-
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1829
|
-
const suggestions = analyzeSuggestions(options.dir);
|
|
1830
|
-
if (options.json) {
|
|
1831
|
-
console.log(JSON.stringify(suggestions, null, 2));
|
|
1832
|
-
} else {
|
|
1833
|
-
console.log('');
|
|
1834
|
-
console.log(formatSuggestions(suggestions));
|
|
1835
|
-
console.log('');
|
|
1836
|
-
}
|
|
1837
|
-
process.exit(0);
|
|
1838
|
-
} else if (normalizedCommand === 'exception') {
|
|
1839
|
-
const {
|
|
1840
|
-
listExceptions,
|
|
1841
|
-
addException,
|
|
1842
|
-
pruneExpiredExceptions,
|
|
1843
|
-
formatExceptionsList,
|
|
1844
|
-
} = require('../src/continuous-ops');
|
|
1845
|
-
const subcommand = parsed.extraArgs[0] || 'list';
|
|
1846
|
-
|
|
1847
|
-
if (subcommand === 'list') {
|
|
1848
|
-
const records = listExceptions(options.dir);
|
|
1849
|
-
if (options.json) {
|
|
1850
|
-
console.log(JSON.stringify(records, null, 2));
|
|
1851
|
-
} else {
|
|
1852
|
-
console.log('');
|
|
1853
|
-
console.log(formatExceptionsList(records));
|
|
1854
|
-
console.log('');
|
|
1855
|
-
}
|
|
1856
|
-
process.exit(0);
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
if (subcommand === 'add') {
|
|
1860
|
-
const result = addException(options.dir, {
|
|
1861
|
-
key: parsed.feedbackKey || null,
|
|
1862
|
-
watchClass: options.exceptionClass,
|
|
1863
|
-
owner: options.exceptionOwner,
|
|
1864
|
-
reason: options.exceptionReason,
|
|
1865
|
-
expiresAt: options.exceptionExpires,
|
|
1866
|
-
scope: options.exceptionScope || 'all',
|
|
1867
|
-
});
|
|
1868
|
-
if (options.json) {
|
|
1869
|
-
console.log(JSON.stringify(result.record, null, 2));
|
|
1870
|
-
} else {
|
|
1871
|
-
console.log('');
|
|
1872
|
-
console.log(` Exception added: ${result.record.id}`);
|
|
1873
|
-
console.log(` Target: ${result.record.key || result.record.watchClass}`);
|
|
1874
|
-
console.log(` Owner: ${result.record.owner}`);
|
|
1875
|
-
console.log(` Scope: ${result.record.scope}`);
|
|
1876
|
-
console.log(` Expires: ${result.record.expiresAt}`);
|
|
1877
|
-
console.log('');
|
|
1878
|
-
}
|
|
1879
|
-
process.exit(0);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
if (subcommand === 'prune') {
|
|
1883
|
-
const result = pruneExpiredExceptions(options.dir);
|
|
1884
|
-
if (options.json) {
|
|
1885
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1886
|
-
} else {
|
|
1887
|
-
console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
|
|
1888
|
-
}
|
|
1889
|
-
process.exit(0);
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
|
|
1893
|
-
process.exit(1);
|
|
1894
|
-
} else if (normalizedCommand === 'profile') {
|
|
1856
|
+
} else if (normalizedCommand === 'suggest-rules') {
|
|
1857
|
+
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1858
|
+
const suggestions = analyzeSuggestions(options.dir);
|
|
1859
|
+
if (options.json) {
|
|
1860
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
1861
|
+
} else {
|
|
1862
|
+
console.log('');
|
|
1863
|
+
console.log(formatSuggestions(suggestions));
|
|
1864
|
+
console.log('');
|
|
1865
|
+
}
|
|
1866
|
+
process.exit(0);
|
|
1867
|
+
} else if (normalizedCommand === 'exception') {
|
|
1868
|
+
const {
|
|
1869
|
+
listExceptions,
|
|
1870
|
+
addException,
|
|
1871
|
+
pruneExpiredExceptions,
|
|
1872
|
+
formatExceptionsList,
|
|
1873
|
+
} = require('../src/continuous-ops');
|
|
1874
|
+
const subcommand = parsed.extraArgs[0] || 'list';
|
|
1875
|
+
|
|
1876
|
+
if (subcommand === 'list') {
|
|
1877
|
+
const records = listExceptions(options.dir);
|
|
1878
|
+
if (options.json) {
|
|
1879
|
+
console.log(JSON.stringify(records, null, 2));
|
|
1880
|
+
} else {
|
|
1881
|
+
console.log('');
|
|
1882
|
+
console.log(formatExceptionsList(records));
|
|
1883
|
+
console.log('');
|
|
1884
|
+
}
|
|
1885
|
+
process.exit(0);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (subcommand === 'add') {
|
|
1889
|
+
const result = addException(options.dir, {
|
|
1890
|
+
key: parsed.feedbackKey || null,
|
|
1891
|
+
watchClass: options.exceptionClass,
|
|
1892
|
+
owner: options.exceptionOwner,
|
|
1893
|
+
reason: options.exceptionReason,
|
|
1894
|
+
expiresAt: options.exceptionExpires,
|
|
1895
|
+
scope: options.exceptionScope || 'all',
|
|
1896
|
+
});
|
|
1897
|
+
if (options.json) {
|
|
1898
|
+
console.log(JSON.stringify(result.record, null, 2));
|
|
1899
|
+
} else {
|
|
1900
|
+
console.log('');
|
|
1901
|
+
console.log(` Exception added: ${result.record.id}`);
|
|
1902
|
+
console.log(` Target: ${result.record.key || result.record.watchClass}`);
|
|
1903
|
+
console.log(` Owner: ${result.record.owner}`);
|
|
1904
|
+
console.log(` Scope: ${result.record.scope}`);
|
|
1905
|
+
console.log(` Expires: ${result.record.expiresAt}`);
|
|
1906
|
+
console.log('');
|
|
1907
|
+
}
|
|
1908
|
+
process.exit(0);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
if (subcommand === 'prune') {
|
|
1912
|
+
const result = pruneExpiredExceptions(options.dir);
|
|
1913
|
+
if (options.json) {
|
|
1914
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1915
|
+
} else {
|
|
1916
|
+
console.log(`\n Pruned ${result.removedCount} expired exception(s). Kept ${result.keptCount} active record(s).\n`);
|
|
1917
|
+
}
|
|
1918
|
+
process.exit(0);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
console.error('\n Error: exception supports `add`, `list`, and `prune`.\n');
|
|
1922
|
+
process.exit(1);
|
|
1923
|
+
} else if (normalizedCommand === 'profile') {
|
|
1895
1924
|
const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
|
|
1896
1925
|
const subcommand = parsed.extraArgs[0];
|
|
1897
1926
|
const profileArg = parsed.extraArgs[1];
|
|
@@ -2432,118 +2461,148 @@ async function main() {
|
|
|
2432
2461
|
await runInit(options.dir, flags);
|
|
2433
2462
|
process.exit(0);
|
|
2434
2463
|
} else if (normalizedCommand === 'setup') {
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
if (options.
|
|
2459
|
-
const {
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
});
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2464
|
+
collectAnonymousEvent('setup', { platform: options.platform, dir: options.dir });
|
|
2465
|
+
const setupResult = await setup({ ...options, silent: options.agentMode || options.json });
|
|
2466
|
+
if (options.agentMode) {
|
|
2467
|
+
// Agent-mode: structured JSON output with next steps
|
|
2468
|
+
const postAudit = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2469
|
+
const agentOutput = {
|
|
2470
|
+
status: setupResult.created > 0 ? 'files_created' : 'already_configured',
|
|
2471
|
+
created: setupResult.created,
|
|
2472
|
+
skipped: setupResult.skipped,
|
|
2473
|
+
written_files: setupResult.writtenFiles,
|
|
2474
|
+
preserved_files: setupResult.preservedFiles,
|
|
2475
|
+
detected_stacks: setupResult.stacks.map(s => s.key),
|
|
2476
|
+
rollback_id: setupResult.rollbackId,
|
|
2477
|
+
post_setup_score: postAudit.score,
|
|
2478
|
+
next_commands: [
|
|
2479
|
+
'npx @nerviq/cli audit --json',
|
|
2480
|
+
setupResult.created > 0 ? `npx @nerviq/cli augment --platform ${options.platform}` : null,
|
|
2481
|
+
postAudit.score < 70 ? 'npx @nerviq/cli plan' : null,
|
|
2482
|
+
].filter(Boolean),
|
|
2483
|
+
};
|
|
2484
|
+
console.log(JSON.stringify(agentOutput, null, 2));
|
|
2485
|
+
process.exit(0);
|
|
2486
|
+
}
|
|
2487
|
+
if (options.snapshot) {
|
|
2488
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
2489
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
2490
|
+
tags: options.snapshotTags,
|
|
2491
|
+
milestone: options.snapshotMilestone,
|
|
2492
|
+
sourceCommand: 'setup',
|
|
2493
|
+
});
|
|
2494
|
+
if (!options.json) {
|
|
2495
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
} else {
|
|
2499
|
+
if (options.workspace) {
|
|
2500
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
2501
|
+
printWorkspaceSummary(summary, options);
|
|
2502
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
2503
|
+
process.exit(1);
|
|
2504
|
+
}
|
|
2505
|
+
process.exit(0);
|
|
2506
|
+
}
|
|
2507
|
+
let result;
|
|
2508
|
+
const renderAuditJsonLocally = options.json && Boolean(options.driftMode);
|
|
2509
|
+
if (options.diffOnly) {
|
|
2510
|
+
const { getChangedFiles, buildDiffOnlyAuditView, printDiffOnlyAudit } = require('../src/diff-only');
|
|
2511
|
+
const fullResult = await audit({ ...options, silent: true });
|
|
2512
|
+
const diffInfo = getChangedFiles(options.dir, {
|
|
2513
|
+
diffBase: options.diffBase,
|
|
2514
|
+
diffHead: options.diffHead,
|
|
2515
|
+
});
|
|
2516
|
+
result = buildDiffOnlyAuditView(fullResult, diffInfo);
|
|
2517
|
+
} else {
|
|
2518
|
+
result = renderAuditJsonLocally
|
|
2519
|
+
? await audit({ ...options, silent: true })
|
|
2520
|
+
: await audit(options);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// ── Telemetry (opt-in, local only) ──
|
|
2524
|
+
collectAnonymousEvent('audit', {
|
|
2525
|
+
platform: result.platform || options.platform,
|
|
2526
|
+
score: result.score,
|
|
2527
|
+
checkCount: Array.isArray(result.results) ? result.results.length : null,
|
|
2528
|
+
dir: options.dir,
|
|
2529
|
+
});
|
|
2530
|
+
|
|
2531
|
+
if (options.driftMode) {
|
|
2532
|
+
const { buildContinuousStatus, formatContinuousStatus } = require('../src/continuous-ops');
|
|
2533
|
+
let campaigns = [];
|
|
2534
|
+
try {
|
|
2535
|
+
const planBundle = await buildProposalBundle({
|
|
2536
|
+
dir: options.dir,
|
|
2537
|
+
platform: options.platform,
|
|
2538
|
+
profile: options.profile,
|
|
2539
|
+
mcpPacks: options.mcpPacks,
|
|
2540
|
+
campaigns: [],
|
|
2541
|
+
});
|
|
2542
|
+
campaigns = planBundle.campaigns || [];
|
|
2543
|
+
} catch {
|
|
2544
|
+
campaigns = [];
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
result = {
|
|
2548
|
+
...result,
|
|
2549
|
+
continuousStatus: buildContinuousStatus({
|
|
2550
|
+
dir: options.dir,
|
|
2551
|
+
auditResult: result,
|
|
2552
|
+
mode: options.driftMode,
|
|
2553
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
2554
|
+
campaigns,
|
|
2555
|
+
}),
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
if (options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2560
|
+
result = {
|
|
2561
|
+
...result,
|
|
2562
|
+
policyLayers: options.policyContract,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (options.diffOnly) {
|
|
2567
|
+
const { printDiffOnlyAudit } = require('../src/diff-only');
|
|
2568
|
+
if (options.json) {
|
|
2569
|
+
console.log(JSON.stringify({
|
|
2570
|
+
version,
|
|
2571
|
+
timestamp: new Date().toISOString(),
|
|
2572
|
+
...result,
|
|
2573
|
+
}, null, 2));
|
|
2574
|
+
} else {
|
|
2575
|
+
console.log(printDiffOnlyAudit(result));
|
|
2576
|
+
if (result.continuousStatus) {
|
|
2577
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2578
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2579
|
+
console.log('');
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
} else if (renderAuditJsonLocally) {
|
|
2583
|
+
console.log(JSON.stringify({
|
|
2584
|
+
version,
|
|
2585
|
+
timestamp: new Date().toISOString(),
|
|
2586
|
+
...result,
|
|
2587
|
+
}, null, 2));
|
|
2588
|
+
} else {
|
|
2589
|
+
if (!options.json && options.policyContract && options.policyContract.layers.some((layer) => layer.valid)) {
|
|
2590
|
+
console.log('');
|
|
2591
|
+
console.log(formatPolicyContract(options.policyContract));
|
|
2592
|
+
console.log('');
|
|
2593
|
+
}
|
|
2594
|
+
if (!options.json && result.continuousStatus) {
|
|
2595
|
+
const { formatContinuousStatus } = require('../src/continuous-ops');
|
|
2596
|
+
console.log('');
|
|
2597
|
+
console.log(formatContinuousStatus(result.continuousStatus));
|
|
2598
|
+
console.log('');
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
if (options.out) {
|
|
2602
|
+
const fs = require('fs');
|
|
2603
|
+
const path = require('path');
|
|
2604
|
+
const outPath = path.resolve(options.out);
|
|
2605
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
2547
2606
|
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
2548
2607
|
if (!options.json) {
|
|
2549
2608
|
console.log(`\n Audit report written to ${options.out}\n`);
|
|
@@ -2551,39 +2610,39 @@ async function main() {
|
|
|
2551
2610
|
}
|
|
2552
2611
|
if (options.webhookUrl) {
|
|
2553
2612
|
try {
|
|
2554
|
-
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2613
|
+
const { sendWebhook, formatSlackMessage, formatGenericAuditWebhookEvent } = require('../src/integrations');
|
|
2555
2614
|
// Auto-detect Slack vs generic by URL pattern
|
|
2556
2615
|
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
2557
2616
|
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
2558
2617
|
let payload;
|
|
2559
|
-
if (isSlack) {
|
|
2560
|
-
payload = formatSlackMessage(result);
|
|
2561
|
-
} else if (isDiscord) {
|
|
2562
|
-
const { formatDiscordMessage } = require('../src/integrations');
|
|
2563
|
-
payload = formatDiscordMessage(result);
|
|
2564
|
-
} else {
|
|
2565
|
-
payload = formatGenericAuditWebhookEvent(result);
|
|
2566
|
-
}
|
|
2567
|
-
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2568
|
-
headers: options.webhookHeaders,
|
|
2569
|
-
retries: options.webhookRetries,
|
|
2570
|
-
});
|
|
2571
|
-
if (!options.json) {
|
|
2572
|
-
if (webhookResp.ok) {
|
|
2573
|
-
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2574
|
-
console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
|
|
2575
|
-
} else {
|
|
2576
|
-
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2577
|
-
console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
2578
|
-
}
|
|
2579
|
-
}
|
|
2580
|
-
} catch (webhookErr) {
|
|
2581
|
-
if (!options.json) {
|
|
2582
|
-
const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
|
|
2583
|
-
console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2618
|
+
if (isSlack) {
|
|
2619
|
+
payload = formatSlackMessage(result);
|
|
2620
|
+
} else if (isDiscord) {
|
|
2621
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
2622
|
+
payload = formatDiscordMessage(result);
|
|
2623
|
+
} else {
|
|
2624
|
+
payload = formatGenericAuditWebhookEvent(result);
|
|
2625
|
+
}
|
|
2626
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload, {
|
|
2627
|
+
headers: options.webhookHeaders,
|
|
2628
|
+
retries: options.webhookRetries,
|
|
2629
|
+
});
|
|
2630
|
+
if (!options.json) {
|
|
2631
|
+
if (webhookResp.ok) {
|
|
2632
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2633
|
+
console.log(` Webhook sent${retryNote}: ${options.webhookUrl} (${webhookResp.status})`);
|
|
2634
|
+
} else {
|
|
2635
|
+
const retryNote = webhookResp.attempts > 1 ? ` after ${webhookResp.attempts} attempts` : '';
|
|
2636
|
+
console.error(` Webhook failed${retryNote}: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
} catch (webhookErr) {
|
|
2640
|
+
if (!options.json) {
|
|
2641
|
+
const retryNote = webhookErr.attempts > 1 ? ` after ${webhookErr.attempts} attempts` : '';
|
|
2642
|
+
console.error(` Webhook error${retryNote}: ${webhookErr.message}`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2587
2646
|
if (options.feedback && !options.json && options.format === null) {
|
|
2588
2647
|
const feedbackTargets = options.lite
|
|
2589
2648
|
? (result.liteSummary?.topNextActions || [])
|
|
@@ -2603,37 +2662,37 @@ async function main() {
|
|
|
2603
2662
|
console.log('');
|
|
2604
2663
|
}
|
|
2605
2664
|
}
|
|
2606
|
-
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2607
|
-
tags: options.snapshotTags,
|
|
2608
|
-
milestone: options.snapshotMilestone,
|
|
2609
|
-
sourceCommand: normalizedCommand,
|
|
2610
|
-
}) : null;
|
|
2665
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
2666
|
+
tags: options.snapshotTags,
|
|
2667
|
+
milestone: options.snapshotMilestone,
|
|
2668
|
+
sourceCommand: normalizedCommand,
|
|
2669
|
+
}) : null;
|
|
2611
2670
|
if (snapshot && !options.json) {
|
|
2612
2671
|
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
2613
2672
|
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
2614
2673
|
console.log('');
|
|
2615
2674
|
}
|
|
2616
|
-
if (options.threshold !== null && result.score < options.threshold) {
|
|
2617
|
-
if (!options.json) {
|
|
2618
|
-
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2619
|
-
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2620
|
-
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2675
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
2676
|
+
if (!options.json) {
|
|
2677
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
2678
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
2679
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
2621
2680
|
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
2622
|
-
}
|
|
2623
|
-
process.exit(1);
|
|
2624
|
-
}
|
|
2625
|
-
if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
|
|
2626
|
-
if (!options.json) {
|
|
2627
|
-
console.error('\n Error: Continuous drift gate failed.');
|
|
2628
|
-
console.error(` Why: ${result.continuousStatus.gateLabel}.`);
|
|
2629
|
-
console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
|
|
2630
|
-
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
2631
|
-
}
|
|
2632
|
-
process.exit(1);
|
|
2633
|
-
}
|
|
2634
|
-
if (options.require && options.require.length > 0) {
|
|
2635
|
-
const failedRequired = options.require.filter(key => {
|
|
2636
|
-
const check = result.results.find(r => r.key === key);
|
|
2681
|
+
}
|
|
2682
|
+
process.exit(1);
|
|
2683
|
+
}
|
|
2684
|
+
if (result.continuousStatus && result.continuousStatus.gate === 'fail') {
|
|
2685
|
+
if (!options.json) {
|
|
2686
|
+
console.error('\n Error: Continuous drift gate failed.');
|
|
2687
|
+
console.error(` Why: ${result.continuousStatus.gateLabel}.`);
|
|
2688
|
+
console.error(' Fix: review the blocking drift items or add a temporary exception with owner/reason/expiry.');
|
|
2689
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
2690
|
+
}
|
|
2691
|
+
process.exit(1);
|
|
2692
|
+
}
|
|
2693
|
+
if (options.require && options.require.length > 0) {
|
|
2694
|
+
const failedRequired = options.require.filter(key => {
|
|
2695
|
+
const check = result.results.find(r => r.key === key);
|
|
2637
2696
|
return !check || check.passed !== true;
|
|
2638
2697
|
});
|
|
2639
2698
|
if (failedRequired.length > 0) {
|
|
@@ -2648,7 +2707,7 @@ async function main() {
|
|
|
2648
2707
|
} catch (err) {
|
|
2649
2708
|
console.error(`\n Error: ${err.message}`);
|
|
2650
2709
|
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
2651
|
-
process.exit(
|
|
2710
|
+
process.exit(2);
|
|
2652
2711
|
}
|
|
2653
2712
|
}
|
|
2654
2713
|
|