@nerviq/cli 1.8.3 → 1.8.5
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 +18 -6
- package/bin/cli.js +1879 -1776
- package/package.json +2 -2
- package/src/audit.js +1 -1
- package/src/auto-suggest.js +101 -0
- package/src/profiles.js +121 -0
- package/src/synergy/report.js +2 -1
package/bin/cli.js
CHANGED
|
@@ -1,1776 +1,1879 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { audit } = require('../src/audit');
|
|
4
|
-
const { setup } = require('../src/setup');
|
|
5
|
-
const { analyzeProject, printAnalysis, exportMarkdown } = require('../src/analyze');
|
|
6
|
-
const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBundle, printApplyResult } = require('../src/plans');
|
|
7
|
-
const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
|
|
8
|
-
const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
|
|
9
|
-
const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
|
-
const { collectFeedback } = require('../src/feedback');
|
|
11
|
-
const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
|
|
12
|
-
const { startServer } = require('../src/server');
|
|
13
|
-
const { auditWorkspaces } = require('../src/workspace');
|
|
14
|
-
const { scanOrg } = require('../src/org');
|
|
15
|
-
const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
|
|
16
|
-
const { VERIFICATION_DATES, getVerificationDate, getVerificationStats } = require('../src/verification-metadata');
|
|
17
|
-
const { version } = require('../package.json');
|
|
18
|
-
|
|
19
|
-
const args = process.argv.slice(2);
|
|
20
|
-
const COMMAND_ALIASES = {
|
|
21
|
-
review: 'deep-review',
|
|
22
|
-
wizard: 'interactive',
|
|
23
|
-
learn: 'insights',
|
|
24
|
-
discover: 'audit',
|
|
25
|
-
starter: 'setup',
|
|
26
|
-
suggest: 'suggest-only',
|
|
27
|
-
gov: 'governance',
|
|
28
|
-
outcome: 'feedback',
|
|
29
|
-
};
|
|
30
|
-
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', 'help', 'version'];
|
|
31
|
-
|
|
32
|
-
function levenshtein(a, b) {
|
|
33
|
-
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
34
|
-
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
|
|
35
|
-
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
|
36
|
-
for (let i = 1; i <= a.length; i++) {
|
|
37
|
-
for (let j = 1; j <= b.length; j++) {
|
|
38
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
39
|
-
matrix[i][j] = Math.min(
|
|
40
|
-
matrix[i - 1][j] + 1,
|
|
41
|
-
matrix[i][j - 1] + 1,
|
|
42
|
-
matrix[i - 1][j - 1] + cost
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return matrix[a.length][b.length];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function suggestCommand(input) {
|
|
50
|
-
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
51
|
-
let best = null;
|
|
52
|
-
let bestDistance = Infinity;
|
|
53
|
-
for (const candidate of candidates) {
|
|
54
|
-
const distance = levenshtein(input, candidate);
|
|
55
|
-
if (distance < bestDistance) {
|
|
56
|
-
best = candidate;
|
|
57
|
-
bestDistance = distance;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return bestDistance <= 3 ? best : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parseArgs(rawArgs) {
|
|
64
|
-
const flags = [];
|
|
65
|
-
let command = 'audit';
|
|
66
|
-
let threshold = null;
|
|
67
|
-
let out = null;
|
|
68
|
-
let planFile = null;
|
|
69
|
-
let only = [];
|
|
70
|
-
let profile = 'safe-write';
|
|
71
|
-
let mcpPacks = [];
|
|
72
|
-
let requireChecks = [];
|
|
73
|
-
let feedbackKey = null;
|
|
74
|
-
let feedbackStatus = null;
|
|
75
|
-
let feedbackEffect = null;
|
|
76
|
-
let feedbackNotes = null;
|
|
77
|
-
let feedbackSource = null;
|
|
78
|
-
let feedbackScoreDelta = null;
|
|
79
|
-
let platform = 'claude';
|
|
80
|
-
let format = null;
|
|
81
|
-
let port = null;
|
|
82
|
-
let workspace = null;
|
|
83
|
-
let webhookUrl = null;
|
|
84
|
-
let commandSet = false;
|
|
85
|
-
let extraArgs = [];
|
|
86
|
-
let convertFrom = null;
|
|
87
|
-
let convertTo = null;
|
|
88
|
-
let migrateFrom = null;
|
|
89
|
-
let migrateTo = null;
|
|
90
|
-
let checkVersion = null;
|
|
91
|
-
let external = null;
|
|
92
|
-
let repos = [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (arg === '--
|
|
104
|
-
if (arg === '--
|
|
105
|
-
if (arg === '--
|
|
106
|
-
if (arg === '--
|
|
107
|
-
if (arg === '--
|
|
108
|
-
if (arg === '--
|
|
109
|
-
if (arg === '--
|
|
110
|
-
if (arg === '--
|
|
111
|
-
if (arg === '--
|
|
112
|
-
if (arg === '--
|
|
113
|
-
if (arg === '--
|
|
114
|
-
if (arg === '--
|
|
115
|
-
if (arg === '--
|
|
116
|
-
if (arg === '--
|
|
117
|
-
if (arg === '--
|
|
118
|
-
if (arg === '--
|
|
119
|
-
if (arg === '--
|
|
120
|
-
if (arg === '--
|
|
121
|
-
if (arg === '--
|
|
122
|
-
if (arg === '--
|
|
123
|
-
if (arg === '--
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
console.log('');
|
|
271
|
-
console.log('\x1b[1m
|
|
272
|
-
console.log(' '
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
console.log('');
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
console.log(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
console.log('
|
|
295
|
-
console.log('
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
console.log('');
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
nerviq
|
|
318
|
-
nerviq
|
|
319
|
-
nerviq
|
|
320
|
-
nerviq
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
nerviq
|
|
324
|
-
nerviq
|
|
325
|
-
nerviq
|
|
326
|
-
nerviq
|
|
327
|
-
nerviq
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
nerviq
|
|
331
|
-
nerviq
|
|
332
|
-
nerviq
|
|
333
|
-
nerviq
|
|
334
|
-
nerviq
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
nerviq
|
|
338
|
-
nerviq
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
nerviq
|
|
342
|
-
nerviq
|
|
343
|
-
nerviq
|
|
344
|
-
nerviq
|
|
345
|
-
nerviq
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
nerviq
|
|
350
|
-
nerviq
|
|
351
|
-
nerviq
|
|
352
|
-
nerviq
|
|
353
|
-
nerviq
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
nerviq
|
|
358
|
-
nerviq
|
|
359
|
-
nerviq
|
|
360
|
-
nerviq
|
|
361
|
-
nerviq
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
nerviq
|
|
365
|
-
nerviq
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
nerviq
|
|
369
|
-
nerviq
|
|
370
|
-
nerviq
|
|
371
|
-
nerviq
|
|
372
|
-
nerviq
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
nerviq
|
|
376
|
-
nerviq
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
nerviq
|
|
380
|
-
nerviq
|
|
381
|
-
nerviq
|
|
382
|
-
nerviq
|
|
383
|
-
nerviq
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
--
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
--
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
--
|
|
401
|
-
--
|
|
402
|
-
--
|
|
403
|
-
--
|
|
404
|
-
--
|
|
405
|
-
--
|
|
406
|
-
--
|
|
407
|
-
--
|
|
408
|
-
--
|
|
409
|
-
--
|
|
410
|
-
--
|
|
411
|
-
--
|
|
412
|
-
--
|
|
413
|
-
--
|
|
414
|
-
--
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
npx nerviq
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (!
|
|
530
|
-
|
|
531
|
-
console.error(
|
|
532
|
-
console.error(`
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
console.error('
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
'
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
process.exit(
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
req.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
console.log(
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
console.log(
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
} else if (normalizedCommand === '
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (!
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
.
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
console.
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
} else {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
console.log('');
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
});
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
console.log(
|
|
1098
|
-
|
|
1099
|
-
console.log('');
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
for (const
|
|
1107
|
-
const
|
|
1108
|
-
console.log(`
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
console.log(
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
console.log(`\n
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
} else {
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
console.log(
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
console.log(
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
console.log(`
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
if (
|
|
1720
|
-
const
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
});
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
console.log(
|
|
1744
|
-
console.log('');
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
console.
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { audit } = require('../src/audit');
|
|
4
|
+
const { setup } = require('../src/setup');
|
|
5
|
+
const { analyzeProject, printAnalysis, exportMarkdown } = require('../src/analyze');
|
|
6
|
+
const { buildProposalBundle, printProposalBundle, writePlanFile, applyProposalBundle, printApplyResult } = require('../src/plans');
|
|
7
|
+
const { getGovernanceSummary, printGovernanceSummary, ensureWritableProfile, renderGovernanceMarkdown } = require('../src/governance');
|
|
8
|
+
const { runBenchmark, printBenchmark, writeBenchmarkReport } = require('../src/benchmark');
|
|
9
|
+
const { writeSnapshotArtifact, writeRollbackArtifact, recordRecommendationOutcome, formatRecommendationOutcomeSummary, getRecommendationOutcomeSummary } = require('../src/activity');
|
|
10
|
+
const { collectFeedback } = require('../src/feedback');
|
|
11
|
+
const { recordPattern, getPriorityAdjustment, formatUsageSummary } = require('../src/usage-patterns');
|
|
12
|
+
const { startServer } = require('../src/server');
|
|
13
|
+
const { auditWorkspaces } = require('../src/workspace');
|
|
14
|
+
const { scanOrg } = require('../src/org');
|
|
15
|
+
const { detectAntiPatterns, printAntiPatterns, printAntiPatternCatalog } = require('../src/anti-patterns');
|
|
16
|
+
const { VERIFICATION_DATES, getVerificationDate, getVerificationStats } = require('../src/verification-metadata');
|
|
17
|
+
const { version } = require('../package.json');
|
|
18
|
+
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const COMMAND_ALIASES = {
|
|
21
|
+
review: 'deep-review',
|
|
22
|
+
wizard: 'interactive',
|
|
23
|
+
learn: 'insights',
|
|
24
|
+
discover: 'audit',
|
|
25
|
+
starter: 'setup',
|
|
26
|
+
suggest: 'suggest-only',
|
|
27
|
+
gov: 'governance',
|
|
28
|
+
outcome: 'feedback',
|
|
29
|
+
};
|
|
30
|
+
const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'init', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'rollback', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'dashboard', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'suggest-rules', 'profile', 'help', 'version'];
|
|
31
|
+
|
|
32
|
+
function levenshtein(a, b) {
|
|
33
|
+
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
34
|
+
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
|
|
35
|
+
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
|
36
|
+
for (let i = 1; i <= a.length; i++) {
|
|
37
|
+
for (let j = 1; j <= b.length; j++) {
|
|
38
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
39
|
+
matrix[i][j] = Math.min(
|
|
40
|
+
matrix[i - 1][j] + 1,
|
|
41
|
+
matrix[i][j - 1] + 1,
|
|
42
|
+
matrix[i - 1][j - 1] + cost
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return matrix[a.length][b.length];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function suggestCommand(input) {
|
|
50
|
+
const candidates = [...KNOWN_COMMANDS, ...Object.keys(COMMAND_ALIASES)];
|
|
51
|
+
let best = null;
|
|
52
|
+
let bestDistance = Infinity;
|
|
53
|
+
for (const candidate of candidates) {
|
|
54
|
+
const distance = levenshtein(input, candidate);
|
|
55
|
+
if (distance < bestDistance) {
|
|
56
|
+
best = candidate;
|
|
57
|
+
bestDistance = distance;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return bestDistance <= 3 ? best : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseArgs(rawArgs) {
|
|
64
|
+
const flags = [];
|
|
65
|
+
let command = 'audit';
|
|
66
|
+
let threshold = null;
|
|
67
|
+
let out = null;
|
|
68
|
+
let planFile = null;
|
|
69
|
+
let only = [];
|
|
70
|
+
let profile = 'safe-write';
|
|
71
|
+
let mcpPacks = [];
|
|
72
|
+
let requireChecks = [];
|
|
73
|
+
let feedbackKey = null;
|
|
74
|
+
let feedbackStatus = null;
|
|
75
|
+
let feedbackEffect = null;
|
|
76
|
+
let feedbackNotes = null;
|
|
77
|
+
let feedbackSource = null;
|
|
78
|
+
let feedbackScoreDelta = null;
|
|
79
|
+
let platform = 'claude';
|
|
80
|
+
let format = null;
|
|
81
|
+
let port = null;
|
|
82
|
+
let workspace = null;
|
|
83
|
+
let webhookUrl = null;
|
|
84
|
+
let commandSet = false;
|
|
85
|
+
let extraArgs = [];
|
|
86
|
+
let convertFrom = null;
|
|
87
|
+
let convertTo = null;
|
|
88
|
+
let migrateFrom = null;
|
|
89
|
+
let migrateTo = null;
|
|
90
|
+
let checkVersion = null;
|
|
91
|
+
let external = null;
|
|
92
|
+
let repos = [];
|
|
93
|
+
let teamProfile = null;
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
96
|
+
const arg = rawArgs[i];
|
|
97
|
+
|
|
98
|
+
if (arg === '--threshold' || arg === '--out' || arg === '--plan' || arg === '--only' || arg === '--profile' || arg === '--mcp-pack' || arg === '--require' || arg === '--key' || arg === '--status' || arg === '--effect' || arg === '--notes' || arg === '--source' || arg === '--score-delta' || arg === '--platform' || arg === '--format' || arg === '--from' || arg === '--to' || arg === '--port' || arg === '--workspace' || arg === '--check-version' || arg === '--webhook' || arg === '--external' || arg === '--team-profile') {
|
|
99
|
+
const value = rawArgs[i + 1];
|
|
100
|
+
if (!value || value.startsWith('--')) {
|
|
101
|
+
throw new Error(`${arg} requires a value`);
|
|
102
|
+
}
|
|
103
|
+
if (arg === '--threshold') threshold = value;
|
|
104
|
+
if (arg === '--out') out = value;
|
|
105
|
+
if (arg === '--plan') planFile = value;
|
|
106
|
+
if (arg === '--only') only = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
107
|
+
if (arg === '--profile') profile = value.trim();
|
|
108
|
+
if (arg === '--mcp-pack') mcpPacks = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
109
|
+
if (arg === '--require') requireChecks = value.split(',').map(item => item.trim()).filter(Boolean);
|
|
110
|
+
if (arg === '--key') feedbackKey = value.trim();
|
|
111
|
+
if (arg === '--status') feedbackStatus = value.trim();
|
|
112
|
+
if (arg === '--effect') feedbackEffect = value.trim();
|
|
113
|
+
if (arg === '--notes') feedbackNotes = value;
|
|
114
|
+
if (arg === '--source') feedbackSource = value.trim();
|
|
115
|
+
if (arg === '--score-delta') feedbackScoreDelta = value.trim();
|
|
116
|
+
if (arg === '--platform') platform = value.trim().toLowerCase();
|
|
117
|
+
if (arg === '--format') format = value.trim().toLowerCase();
|
|
118
|
+
if (arg === '--from') { convertFrom = value.trim(); migrateFrom = value.trim(); }
|
|
119
|
+
if (arg === '--to') { convertTo = value.trim(); migrateTo = value.trim(); }
|
|
120
|
+
if (arg === '--port') port = value.trim();
|
|
121
|
+
if (arg === '--workspace') workspace = value.trim();
|
|
122
|
+
if (arg === '--check-version') checkVersion = value.trim();
|
|
123
|
+
if (arg === '--webhook') webhookUrl = value.trim();
|
|
124
|
+
if (arg === '--external') external = value.trim();
|
|
125
|
+
if (arg === '--team-profile') teamProfile = value.trim();
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (arg.startsWith('--team-profile=')) {
|
|
131
|
+
teamProfile = arg.split('=').slice(1).join('=').trim();
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (arg.startsWith('--external=')) {
|
|
136
|
+
external = arg.split('=').slice(1).join('=').trim();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (arg === '--repos') {
|
|
141
|
+
// Collect all following non-flag args as repo paths (supports comma-separated too)
|
|
142
|
+
while (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('--')) {
|
|
143
|
+
i++;
|
|
144
|
+
repos.push(...rawArgs[i].split(',').map(s => s.trim()).filter(Boolean));
|
|
145
|
+
}
|
|
146
|
+
if (repos.length === 0) throw new Error('--repos requires at least one path');
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (arg.startsWith('--repos=')) {
|
|
151
|
+
repos = arg.split('=').slice(1).join('=').split(',').map(s => s.trim()).filter(Boolean);
|
|
152
|
+
if (repos.length === 0) throw new Error('--repos requires at least one path');
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg.startsWith('--require=')) {
|
|
157
|
+
requireChecks = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (arg.startsWith('--threshold=')) {
|
|
162
|
+
threshold = arg.split('=')[1];
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (arg.startsWith('--out=')) {
|
|
167
|
+
out = arg.split('=').slice(1).join('=');
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (arg.startsWith('--plan=')) {
|
|
172
|
+
planFile = arg.split('=').slice(1).join('=');
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (arg.startsWith('--only=')) {
|
|
177
|
+
only = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (arg.startsWith('--profile=')) {
|
|
182
|
+
profile = arg.split('=').slice(1).join('=').trim();
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (arg.startsWith('--mcp-pack=')) {
|
|
187
|
+
mcpPacks = arg.split('=').slice(1).join('=').split(',').map(item => item.trim()).filter(Boolean);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (arg.startsWith('--key=')) {
|
|
192
|
+
feedbackKey = arg.split('=').slice(1).join('=').trim();
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (arg.startsWith('--status=')) {
|
|
197
|
+
feedbackStatus = arg.split('=').slice(1).join('=').trim();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (arg.startsWith('--effect=')) {
|
|
202
|
+
feedbackEffect = arg.split('=').slice(1).join('=').trim();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (arg.startsWith('--notes=')) {
|
|
207
|
+
feedbackNotes = arg.split('=').slice(1).join('=');
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (arg.startsWith('--source=')) {
|
|
212
|
+
feedbackSource = arg.split('=').slice(1).join('=').trim();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (arg.startsWith('--score-delta=')) {
|
|
217
|
+
feedbackScoreDelta = arg.split('=').slice(1).join('=').trim();
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (arg.startsWith('--platform=')) {
|
|
222
|
+
platform = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (arg.startsWith('--format=')) {
|
|
227
|
+
format = arg.split('=').slice(1).join('=').trim().toLowerCase();
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (arg.startsWith('--port=')) {
|
|
232
|
+
port = arg.split('=').slice(1).join('=').trim();
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (arg.startsWith('--workspace=')) {
|
|
237
|
+
workspace = arg.split('=').slice(1).join('=').trim();
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (arg.startsWith('--check-version=')) {
|
|
242
|
+
checkVersion = arg.split('=').slice(1).join('=').trim();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (arg.startsWith('--')) {
|
|
247
|
+
flags.push(arg);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!commandSet) {
|
|
252
|
+
command = arg;
|
|
253
|
+
commandSet = true;
|
|
254
|
+
} else {
|
|
255
|
+
extraArgs.push(arg);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const normalizedCommand = COMMAND_ALIASES[command] || command;
|
|
260
|
+
|
|
261
|
+
return { flags, command, normalizedCommand, threshold, out, planFile, only, profile, mcpPacks, requireChecks, feedbackKey, feedbackStatus, feedbackEffect, feedbackNotes, feedbackSource, feedbackScoreDelta, platform, format, port, workspace, extraArgs, convertFrom, convertTo, migrateFrom, migrateTo, checkVersion, webhookUrl, external, repos, teamProfile };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function printWorkspaceSummary(summary, options) {
|
|
265
|
+
if (options.json) {
|
|
266
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log('');
|
|
271
|
+
console.log('\x1b[1m nerviq workspace audit\x1b[0m');
|
|
272
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
273
|
+
console.log(` Root: ${summary.rootDir}`);
|
|
274
|
+
console.log(` Platform: ${summary.platform}`);
|
|
275
|
+
console.log(` Workspaces: ${summary.workspaceCount}`);
|
|
276
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
277
|
+
console.log('');
|
|
278
|
+
console.log('\x1b[1m Workspace Score Pass Total Top action\x1b[0m');
|
|
279
|
+
console.log(' ' + '─'.repeat(72));
|
|
280
|
+
for (const item of summary.workspaces) {
|
|
281
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
282
|
+
const topAction = item.error || item.topAction || '-';
|
|
283
|
+
console.log(` ${item.workspace.padEnd(26)} ${score.padStart(5)} ${String(item.passed).padStart(5)} ${String(item.total).padStart(6)} ${topAction}`);
|
|
284
|
+
}
|
|
285
|
+
console.log('');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function printOrgSummary(summary, options) {
|
|
289
|
+
if (options.json) {
|
|
290
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log('\x1b[1m nerviq org scan\x1b[0m');
|
|
296
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
297
|
+
console.log(` Platform: ${summary.platform}`);
|
|
298
|
+
console.log(` Repos: ${summary.repoCount}`);
|
|
299
|
+
console.log(` Average score: \x1b[1m${summary.averageScore}/100\x1b[0m`);
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log('\x1b[1m Repo Platform Score Top action\x1b[0m');
|
|
302
|
+
console.log(' ' + '─'.repeat(72));
|
|
303
|
+
for (const item of summary.repos) {
|
|
304
|
+
const score = item.score === null ? 'ERR' : String(item.score);
|
|
305
|
+
const topAction = item.error || item.topAction || '-';
|
|
306
|
+
console.log(` ${item.name.padEnd(18)} ${item.platform.padEnd(8)} ${score.padStart(5)} ${topAction}`);
|
|
307
|
+
}
|
|
308
|
+
console.log('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const HELP = `
|
|
312
|
+
nerviq v${version}
|
|
313
|
+
The intelligent nervous system for AI coding agents.
|
|
314
|
+
Audit, align, and amplify every platform on every project.
|
|
315
|
+
|
|
316
|
+
DISCOVER
|
|
317
|
+
nerviq audit Quick scan: score + top 3 gaps (default)
|
|
318
|
+
nerviq audit --full Full audit with all checks, weakest areas, badge
|
|
319
|
+
nerviq audit --platform X Audit specific platform (claude|codex|cursor|copilot|gemini|windsurf|aider|opencode)
|
|
320
|
+
nerviq audit --json Machine-readable JSON output (for CI)
|
|
321
|
+
nerviq audit --workspace packages/* Audit each workspace in a monorepo
|
|
322
|
+
nerviq scan dir1 dir2 Compare multiple repos side-by-side
|
|
323
|
+
nerviq org scan dir1 dir2 Aggregate multiple repos into one score table
|
|
324
|
+
nerviq catalog Full check catalog (all 8 platforms)
|
|
325
|
+
nerviq catalog --json Export full check catalog as JSON
|
|
326
|
+
nerviq anti-patterns Detect anti-patterns in current project
|
|
327
|
+
nerviq anti-patterns --all Show full anti-pattern catalog
|
|
328
|
+
|
|
329
|
+
SETUP
|
|
330
|
+
nerviq setup Generate starter-safe baseline config files
|
|
331
|
+
nerviq setup --auto Apply all generated files without prompts
|
|
332
|
+
nerviq interactive Step-by-step guided wizard
|
|
333
|
+
nerviq check-health Detect regressions + platform format changes between snapshots
|
|
334
|
+
nerviq doctor Self-diagnostics: Node, deps, freshness, platform detection
|
|
335
|
+
|
|
336
|
+
FIX
|
|
337
|
+
nerviq fix Show fixable checks and manual-fix guidance
|
|
338
|
+
nerviq fix <key> Auto-fix a specific check (with score impact)
|
|
339
|
+
nerviq fix <key> --prompt Show AI agent prompt for a check (no auto-fix)
|
|
340
|
+
nerviq fix --all-critical Fix all critical issues at once
|
|
341
|
+
nerviq fix --dry-run Preview fixes without writing
|
|
342
|
+
nerviq fix --auto Apply fixes without confirmation prompt
|
|
343
|
+
nerviq rollback Undo the most recent apply (delete created files)
|
|
344
|
+
nerviq rollback --list Show available rollback points
|
|
345
|
+
nerviq rollback --dry-run Preview what would be deleted
|
|
346
|
+
|
|
347
|
+
IMPROVE
|
|
348
|
+
nerviq augment Improvement plan (no writes)
|
|
349
|
+
nerviq suggest-only Structured report for sharing (no writes)
|
|
350
|
+
nerviq plan Export proposal bundles with diffs
|
|
351
|
+
nerviq plan --out plan.json Save plan to file
|
|
352
|
+
nerviq apply Apply proposals selectively with rollback
|
|
353
|
+
nerviq apply --dry-run Preview changes without writing
|
|
354
|
+
|
|
355
|
+
GOVERN
|
|
356
|
+
nerviq governance Permission profiles + hooks + policy packs
|
|
357
|
+
nerviq governance --json Machine-readable governance summary
|
|
358
|
+
nerviq benchmark Before/after score in isolated temp copy
|
|
359
|
+
nerviq benchmark --external /path Benchmark an external repo
|
|
360
|
+
nerviq freshness Show verification freshness for all checks
|
|
361
|
+
nerviq certify Generate certification badge for your project
|
|
362
|
+
|
|
363
|
+
CROSS-PLATFORM
|
|
364
|
+
nerviq harmony-audit Drift detection across all active platforms
|
|
365
|
+
nerviq harmony-sync Preview cross-platform sync (dry run)
|
|
366
|
+
nerviq harmony-sync --fix Apply cross-platform sync (write files)
|
|
367
|
+
nerviq harmony-sync --json JSON output for CI/automation
|
|
368
|
+
nerviq harmony-add <platform> Add a new platform to the project
|
|
369
|
+
nerviq synergy-report Multi-agent amplification opportunities
|
|
370
|
+
nerviq convert --from X --to Y Convert configs between platforms
|
|
371
|
+
nerviq migrate --platform X Platform version migration helper
|
|
372
|
+
nerviq migrate --platform cursor --from v2 --to v3
|
|
373
|
+
|
|
374
|
+
MONITOR
|
|
375
|
+
nerviq dashboard Generate static HTML dashboard report
|
|
376
|
+
nerviq dashboard --out F Save dashboard to custom file
|
|
377
|
+
nerviq dashboard --open Open dashboard in browser after generating
|
|
378
|
+
nerviq watch Live config monitoring (re-audits on file change)
|
|
379
|
+
nerviq history Score history from saved snapshots
|
|
380
|
+
nerviq compare Latest vs previous snapshot diff
|
|
381
|
+
nerviq trend Score trend over time
|
|
382
|
+
nerviq trend --out report.md Export trend report as markdown
|
|
383
|
+
nerviq feedback Record recommendation outcomes
|
|
384
|
+
|
|
385
|
+
TEAM PROFILES
|
|
386
|
+
nerviq profile save <name> Save current preferences as a named profile
|
|
387
|
+
nerviq profile load <name> Load and display a saved profile
|
|
388
|
+
nerviq profile list List available profiles
|
|
389
|
+
nerviq profile export <name> Export profile JSON for sharing
|
|
390
|
+
|
|
391
|
+
ADVANCED
|
|
392
|
+
nerviq deep-review AI-powered config review (opt-in, uses API key)
|
|
393
|
+
nerviq serve --port 3000 Start local Nerviq REST API server
|
|
394
|
+
nerviq badge Generate shields.io badge markdown
|
|
395
|
+
nerviq rules-export Export recommendation rules as JSON
|
|
396
|
+
nerviq rules-export --out F Save rules to file
|
|
397
|
+
nerviq suggest-rules Auto-suggest rules based on usage patterns
|
|
398
|
+
|
|
399
|
+
OPTIONS
|
|
400
|
+
--platform NAME Platform: claude (default), codex, cursor, copilot, gemini, windsurf, aider, opencode
|
|
401
|
+
--threshold N Exit code 1 if score < N (CI gate)
|
|
402
|
+
--require A,B Exit code 1 if named checks fail
|
|
403
|
+
--out FILE Write output to file (JSON or markdown)
|
|
404
|
+
--plan FILE Load previously exported plan file
|
|
405
|
+
--only A,B Limit plan/apply to selected proposal IDs
|
|
406
|
+
--profile NAME Permission profile: read-only | suggest-only | safe-write | power-user
|
|
407
|
+
--team-profile N Load a saved team profile for audit (overrides threshold/platform)
|
|
408
|
+
--mcp-pack A,B Merge MCP packs into setup (e.g. context7-docs,next-devtools)
|
|
409
|
+
--check-version V Pin catalog to a specific version (warn on mismatch)
|
|
410
|
+
--format NAME Output format: json | sarif | otel
|
|
411
|
+
--webhook URL Send audit results to a webhook (Slack/Discord/generic JSON)
|
|
412
|
+
--external PATH Benchmark an external repo instead of cwd
|
|
413
|
+
--port N Port for \`serve\` (default: 3000)
|
|
414
|
+
--workspace GLOBS Audit workspaces separately (e.g. packages/* or apps/web,apps/api)
|
|
415
|
+
--snapshot Save snapshot artifact under .claude/nerviq/snapshots/
|
|
416
|
+
--full Show full audit output (all checks, weakest areas, badge)
|
|
417
|
+
--lite Short top-3 scan (default behavior since v1.5.2)
|
|
418
|
+
--dry-run Preview changes without writing files
|
|
419
|
+
--config-only Only write config files (.claude/, rules, hooks) — never source code
|
|
420
|
+
--verbose Full audit + medium-priority recommendations
|
|
421
|
+
--show-deprecated Show deprecated checks (excluded from scoring)
|
|
422
|
+
--json Output as JSON
|
|
423
|
+
--auto Apply all generated files without prompting
|
|
424
|
+
--key NAME Feedback: recommendation key (e.g. permissionDeny)
|
|
425
|
+
--status VALUE Feedback: accepted | rejected | deferred
|
|
426
|
+
--effect VALUE Feedback: positive | neutral | negative
|
|
427
|
+
--score-delta N Feedback: observed score delta
|
|
428
|
+
--help Show this help
|
|
429
|
+
--version Show version
|
|
430
|
+
|
|
431
|
+
EXAMPLES
|
|
432
|
+
npx nerviq
|
|
433
|
+
npx nerviq --lite
|
|
434
|
+
npx nerviq --platform cursor
|
|
435
|
+
npx nerviq audit --workspace packages/*
|
|
436
|
+
npx nerviq --platform codex augment
|
|
437
|
+
npx nerviq org scan ./app ./api ./infra
|
|
438
|
+
npx nerviq scan ./app ./api ./infra
|
|
439
|
+
npx nerviq harmony-audit
|
|
440
|
+
npx nerviq convert --from claude --to codex
|
|
441
|
+
npx nerviq migrate --platform cursor --from v2 --to v3
|
|
442
|
+
npx nerviq setup --mcp-pack context7-docs
|
|
443
|
+
npx nerviq apply --plan plan.json --only hooks,commands
|
|
444
|
+
npx nerviq serve --port 4000
|
|
445
|
+
npx nerviq --json --threshold 70
|
|
446
|
+
npx nerviq catalog --json --out catalog.json
|
|
447
|
+
npx nerviq feedback --key permissionDeny --status accepted --effect positive
|
|
448
|
+
|
|
449
|
+
EXIT CODES
|
|
450
|
+
0 Success
|
|
451
|
+
1 Error, unknown command, or score below --threshold
|
|
452
|
+
`;
|
|
453
|
+
|
|
454
|
+
async function main() {
|
|
455
|
+
let parsed;
|
|
456
|
+
try {
|
|
457
|
+
parsed = parseArgs(args);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const { flags, command, normalizedCommand } = parsed;
|
|
464
|
+
|
|
465
|
+
if (flags.includes('--help') || command === 'help') {
|
|
466
|
+
console.log(HELP);
|
|
467
|
+
process.exit(0);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (flags.includes('--version') || command === 'version') {
|
|
471
|
+
console.log(version);
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const options = {
|
|
476
|
+
verbose: flags.includes('--verbose'),
|
|
477
|
+
json: flags.includes('--json'),
|
|
478
|
+
auto: flags.includes('--auto'),
|
|
479
|
+
lite: flags.includes('--full') || flags.includes('--verbose') ? false : true,
|
|
480
|
+
full: flags.includes('--full'),
|
|
481
|
+
showDeprecated: flags.includes('--show-deprecated'),
|
|
482
|
+
snapshot: flags.includes('--snapshot'),
|
|
483
|
+
feedback: flags.includes('--feedback'),
|
|
484
|
+
fix: flags.includes('--fix'),
|
|
485
|
+
autoSync: flags.includes('--auto-sync'),
|
|
486
|
+
dryRun: flags.includes('--dry-run'),
|
|
487
|
+
configOnly: flags.includes('--config-only'),
|
|
488
|
+
threshold: parsed.threshold !== null ? Number(parsed.threshold) : null,
|
|
489
|
+
out: parsed.out,
|
|
490
|
+
planFile: parsed.planFile,
|
|
491
|
+
only: parsed.only,
|
|
492
|
+
profile: parsed.profile,
|
|
493
|
+
mcpPacks: parsed.mcpPacks,
|
|
494
|
+
require: parsed.requireChecks,
|
|
495
|
+
platform: parsed.platform || 'claude',
|
|
496
|
+
format: parsed.format || null,
|
|
497
|
+
port: parsed.port !== null ? Number(parsed.port) : null,
|
|
498
|
+
workspace: parsed.workspace || null,
|
|
499
|
+
webhookUrl: parsed.webhookUrl || null,
|
|
500
|
+
external: parsed.external || null,
|
|
501
|
+
dir: process.cwd()
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
if (parsed.checkVersion) {
|
|
505
|
+
if (parsed.checkVersion !== version) {
|
|
506
|
+
console.error(`\n Warning: --check-version ${parsed.checkVersion} does not match installed nerviq version ${version}.`);
|
|
507
|
+
console.error(` Check catalog may differ between versions. To align, run: npm install @nerviq/cli@${parsed.checkVersion}`);
|
|
508
|
+
console.error('');
|
|
509
|
+
}
|
|
510
|
+
options.checkVersion = parsed.checkVersion;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (parsed.teamProfile) {
|
|
514
|
+
const { loadProfile, applyProfileToOptions } = require('../src/profiles');
|
|
515
|
+
try {
|
|
516
|
+
const teamProf = loadProfile(options.dir, parsed.teamProfile);
|
|
517
|
+
const merged = applyProfileToOptions(teamProf, options);
|
|
518
|
+
Object.assign(options, merged);
|
|
519
|
+
if (!options.json) {
|
|
520
|
+
console.log(` Using team profile: ${parsed.teamProfile}`);
|
|
521
|
+
}
|
|
522
|
+
} catch (err) {
|
|
523
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
|
|
529
|
+
if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
|
|
530
|
+
console.error(`\n Error: Unsupported platform '${options.platform}'.`);
|
|
531
|
+
console.error(` Supported platforms: ${SUPPORTED_PLATFORMS.join(', ')}.`);
|
|
532
|
+
console.error(` To get started: npx nerviq setup`);
|
|
533
|
+
console.error(` To diagnose issues: npx nerviq doctor`);
|
|
534
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#cross-platform\n');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (options.format !== null && !['json', 'sarif', 'otel'].includes(options.format)) {
|
|
539
|
+
console.error(`\n Error: Unsupported format '${options.format}'. Use 'json', 'sarif', or 'otel'.\n`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (options.port !== null && (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535)) {
|
|
544
|
+
console.error('\n Error: --port must be an integer between 0 and 65535.\n');
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (options.threshold !== null && (!Number.isFinite(options.threshold) || options.threshold < 0 || options.threshold > 100)) {
|
|
549
|
+
console.error(`\n Error: Invalid threshold value '${parsed.threshold}'.`);
|
|
550
|
+
console.error(' Why: --threshold must be a number between 0 and 100 representing the minimum passing score.');
|
|
551
|
+
console.error(' Fix: Use a valid number, e.g.: npx nerviq --threshold 70');
|
|
552
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (options.require && options.require.length > 0 && normalizedCommand !== 'audit' && !['audit', 'discover'].includes(command)) {
|
|
557
|
+
console.error(`\n Warning: --require is only supported with the audit command. Ignoring for '${normalizedCommand}'.\n`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!KNOWN_COMMANDS.includes(normalizedCommand)) {
|
|
561
|
+
const suggestion = suggestCommand(command);
|
|
562
|
+
console.error(`\n Error: Unknown command '${command}'.`);
|
|
563
|
+
console.error(` Why: '${command}' is not a recognized nerviq command or alias.`);
|
|
564
|
+
if (suggestion) {
|
|
565
|
+
console.error(` Fix: Did you mean '${suggestion}'? Run: npx nerviq ${suggestion}`);
|
|
566
|
+
} else {
|
|
567
|
+
console.error(' Fix: Run nerviq --help to see all available commands.');
|
|
568
|
+
}
|
|
569
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#readme\n');
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!require('fs').existsSync(options.dir)) {
|
|
574
|
+
console.error(`\n Error: Directory not found: ${options.dir}`);
|
|
575
|
+
console.error(' Why: The current working directory does not exist or is not accessible.');
|
|
576
|
+
console.error(' Fix: cd into your project directory first, then run nerviq.');
|
|
577
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#getting-started\n');
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (['setup', 'apply', 'benchmark'].includes(normalizedCommand)) {
|
|
582
|
+
try {
|
|
583
|
+
ensureWritableProfile(options.profile, normalizedCommand, options.dryRun);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const FULL_COMMAND_SET = new Set([
|
|
592
|
+
'audit', 'org', 'scan', 'badge', 'augment', 'suggest-only', 'setup', 'plan', 'apply',
|
|
593
|
+
'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'insights',
|
|
594
|
+
'history', 'compare', 'trend', 'feedback', 'catalog', 'certify', 'serve', 'help', 'version',
|
|
595
|
+
// Harmony + Synergy (cross-platform)
|
|
596
|
+
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
597
|
+
'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
598
|
+
'freshness', 'profile',
|
|
599
|
+
]);
|
|
600
|
+
|
|
601
|
+
if (options.platform === 'codex') {
|
|
602
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
603
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform codex.`);
|
|
604
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (options.platform === 'gemini') {
|
|
610
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
611
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform gemini.`);
|
|
612
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (options.platform === 'copilot') {
|
|
618
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
619
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform copilot.`);
|
|
620
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
if (options.platform === 'cursor') {
|
|
626
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
627
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform cursor.`);
|
|
628
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
for (const plat of ['windsurf', 'aider', 'opencode']) {
|
|
634
|
+
if (options.platform === plat) {
|
|
635
|
+
if (!FULL_COMMAND_SET.has(normalizedCommand)) {
|
|
636
|
+
console.error(`\n Error: '${normalizedCommand}' is not supported for --platform ${plat}.`);
|
|
637
|
+
console.error(' Available: ' + [...FULL_COMMAND_SET].filter(c => c !== 'help' && c !== 'version').join(', ') + '.');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (normalizedCommand === 'scan') {
|
|
644
|
+
const scanDirs = parsed.extraArgs;
|
|
645
|
+
if (scanDirs.length === 0) {
|
|
646
|
+
console.error('\n Error: scan requires at least one directory argument.');
|
|
647
|
+
console.error(' Usage: npx nerviq scan dir1 dir2 dir3\n');
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
const summary = await scanOrg(scanDirs, options.platform);
|
|
651
|
+
printOrgSummary(summary, options);
|
|
652
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
653
|
+
process.exit(1);
|
|
654
|
+
}
|
|
655
|
+
process.exit(0);
|
|
656
|
+
} else if (normalizedCommand === 'org') {
|
|
657
|
+
const subcommand = parsed.extraArgs[0];
|
|
658
|
+
const scanDirs = parsed.extraArgs.slice(1);
|
|
659
|
+
if (subcommand !== 'scan' || scanDirs.length === 0) {
|
|
660
|
+
console.error('\n Error: org requires the scan subcommand and at least one directory.');
|
|
661
|
+
console.error(' Usage: npx nerviq org scan dir1 dir2 dir3\n');
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const summary = await scanOrg(scanDirs, options.platform);
|
|
665
|
+
printOrgSummary(summary, options);
|
|
666
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
process.exit(0);
|
|
670
|
+
} else if (normalizedCommand === 'history') {
|
|
671
|
+
const { formatHistory, readSnapshotIndex } = require('../src/activity');
|
|
672
|
+
// Handle --prune N
|
|
673
|
+
const pruneIdx = flags.indexOf('--prune');
|
|
674
|
+
if (pruneIdx >= 0) {
|
|
675
|
+
const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
|
|
676
|
+
const fsMod = require('fs');
|
|
677
|
+
const pathMod = require('path');
|
|
678
|
+
const entries = readSnapshotIndex(options.dir);
|
|
679
|
+
if (entries.length <= keepCount) {
|
|
680
|
+
console.log(`\n Nothing to prune (${entries.length} snapshots, keeping ${keepCount}).\n`);
|
|
681
|
+
} else {
|
|
682
|
+
const toRemove = entries.slice(0, entries.length - keepCount);
|
|
683
|
+
let removed = 0;
|
|
684
|
+
for (const entry of toRemove) {
|
|
685
|
+
const fp = pathMod.join(options.dir, entry.relativePath);
|
|
686
|
+
try { fsMod.unlinkSync(fp); removed++; } catch {}
|
|
687
|
+
}
|
|
688
|
+
const kept = entries.slice(entries.length - keepCount);
|
|
689
|
+
const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
|
|
690
|
+
try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
|
|
691
|
+
console.log(`\n Pruned ${removed} snapshots, kept ${kept.length}.\n`);
|
|
692
|
+
}
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
console.log('');
|
|
696
|
+
console.log(formatHistory(options.dir));
|
|
697
|
+
console.log('');
|
|
698
|
+
process.exit(0);
|
|
699
|
+
} else if (normalizedCommand === 'compare') {
|
|
700
|
+
const { compareLatest } = require('../src/activity');
|
|
701
|
+
const result = compareLatest(options.dir);
|
|
702
|
+
if (!result) {
|
|
703
|
+
console.log('\n Need at least 2 snapshots to compare. Run `npx nerviq --snapshot` twice.\n');
|
|
704
|
+
process.exit(0);
|
|
705
|
+
}
|
|
706
|
+
if (options.json) {
|
|
707
|
+
console.log(JSON.stringify(result, null, 2));
|
|
708
|
+
} else {
|
|
709
|
+
const sign = result.delta.score >= 0 ? '+' : '';
|
|
710
|
+
console.log('');
|
|
711
|
+
console.log(` Previous: ${result.previous.score}/100 (${result.previous.date?.split('T')[0]})`);
|
|
712
|
+
console.log(` Current: ${result.current.score}/100 (${result.current.date?.split('T')[0]})`);
|
|
713
|
+
console.log(` Delta: ${sign}${result.delta.score} points`);
|
|
714
|
+
console.log(` Trend: ${result.trend}`);
|
|
715
|
+
if (result.improvements.length > 0) console.log(` Fixed: ${result.improvements.join(', ')}`);
|
|
716
|
+
if (result.regressions.length > 0) console.log(` New gaps: ${result.regressions.join(', ')}`);
|
|
717
|
+
console.log('');
|
|
718
|
+
}
|
|
719
|
+
process.exit(0);
|
|
720
|
+
} else if (normalizedCommand === 'trend') {
|
|
721
|
+
const { exportTrendReport } = require('../src/activity');
|
|
722
|
+
const report = exportTrendReport(options.dir);
|
|
723
|
+
if (!report) {
|
|
724
|
+
console.log('\n No snapshots found. Run `npx nerviq --snapshot` to start tracking.\n');
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
if (options.out) {
|
|
728
|
+
require('fs').writeFileSync(options.out, report, 'utf8');
|
|
729
|
+
console.log(`\n Trend report exported to ${options.out}\n`);
|
|
730
|
+
} else {
|
|
731
|
+
console.log(report);
|
|
732
|
+
}
|
|
733
|
+
process.exit(0);
|
|
734
|
+
} else if (normalizedCommand === 'badge') {
|
|
735
|
+
const { getBadgeMarkdown } = require('../src/badge');
|
|
736
|
+
const result = await audit({ ...options, silent: true });
|
|
737
|
+
console.log(getBadgeMarkdown(result.score));
|
|
738
|
+
console.log('');
|
|
739
|
+
console.log('Add this to your README.md');
|
|
740
|
+
process.exit(0);
|
|
741
|
+
} else if (normalizedCommand === 'insights') {
|
|
742
|
+
const https = require('https');
|
|
743
|
+
const url = 'https://claudex-insights.claudex.workers.dev/v1/stats';
|
|
744
|
+
const req = https.get(url, (res) => {
|
|
745
|
+
let data = '';
|
|
746
|
+
res.on('data', chunk => data += chunk);
|
|
747
|
+
res.on('end', () => {
|
|
748
|
+
try {
|
|
749
|
+
const stats = JSON.parse(data);
|
|
750
|
+
console.log('');
|
|
751
|
+
console.log('\x1b[1m CLAUDEX Community Insights\x1b[0m');
|
|
752
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
753
|
+
console.log(` Total audits run: \x1b[1m${stats.totalRuns}\x1b[0m`);
|
|
754
|
+
console.log(` Average score: \x1b[1m${stats.averageScore}/100\x1b[0m`);
|
|
755
|
+
console.log('');
|
|
756
|
+
if (stats.topFailedChecks && stats.topFailedChecks.length > 0) {
|
|
757
|
+
console.log('\x1b[33m Most common gaps:\x1b[0m');
|
|
758
|
+
for (const f of stats.topFailedChecks.slice(0, 5)) {
|
|
759
|
+
console.log(` ${f.pct}% miss: \x1b[1m${f.check}\x1b[0m`);
|
|
760
|
+
}
|
|
761
|
+
console.log('');
|
|
762
|
+
}
|
|
763
|
+
if (stats.topStacks && stats.topStacks.length > 0) {
|
|
764
|
+
console.log('\x1b[36m Popular stacks:\x1b[0m');
|
|
765
|
+
console.log(` ${stats.topStacks.map(s => s.stack).join(', ')}`);
|
|
766
|
+
}
|
|
767
|
+
console.log('');
|
|
768
|
+
} catch (e) {
|
|
769
|
+
console.log(' No community data available yet. Be the first to run: npx nerviq');
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}).on('error', () => {
|
|
773
|
+
console.log(' Could not reach insights server. Run locally: npx nerviq');
|
|
774
|
+
});
|
|
775
|
+
req.setTimeout(10000, () => {
|
|
776
|
+
req.destroy();
|
|
777
|
+
console.log(' Insights request timed out. Run locally: npx nerviq');
|
|
778
|
+
});
|
|
779
|
+
return; // keep process alive for http
|
|
780
|
+
} else if (normalizedCommand === 'feedback') {
|
|
781
|
+
if (flags.includes('--patterns')) {
|
|
782
|
+
if (options.json) {
|
|
783
|
+
const { getUsageSummary } = require('../src/usage-patterns');
|
|
784
|
+
console.log(JSON.stringify(getUsageSummary(options.dir), null, 2));
|
|
785
|
+
} else {
|
|
786
|
+
console.log('');
|
|
787
|
+
console.log(formatUsageSummary(options.dir));
|
|
788
|
+
console.log('');
|
|
789
|
+
}
|
|
790
|
+
process.exit(0);
|
|
791
|
+
}
|
|
792
|
+
if (parsed.feedbackKey) {
|
|
793
|
+
if (!parsed.feedbackStatus) {
|
|
794
|
+
console.error('\n Error: feedback logging requires --status when --key is provided.\n');
|
|
795
|
+
process.exit(1);
|
|
796
|
+
}
|
|
797
|
+
const artifact = recordRecommendationOutcome(options.dir, {
|
|
798
|
+
key: parsed.feedbackKey,
|
|
799
|
+
status: parsed.feedbackStatus,
|
|
800
|
+
effect: parsed.feedbackEffect || 'neutral',
|
|
801
|
+
notes: parsed.feedbackNotes || '',
|
|
802
|
+
source: parsed.feedbackSource || 'manual-cli',
|
|
803
|
+
scoreDelta: parsed.feedbackScoreDelta !== null ? Number(parsed.feedbackScoreDelta) : null,
|
|
804
|
+
});
|
|
805
|
+
const summary = getRecommendationOutcomeSummary(options.dir);
|
|
806
|
+
if (options.json) {
|
|
807
|
+
console.log(JSON.stringify({ artifact, summary }, null, 2));
|
|
808
|
+
} else {
|
|
809
|
+
console.log('');
|
|
810
|
+
console.log(` Feedback recorded for ${parsed.feedbackKey}`);
|
|
811
|
+
console.log(` Artifact: ${artifact.relativePath}`);
|
|
812
|
+
console.log('');
|
|
813
|
+
console.log(formatRecommendationOutcomeSummary(options.dir));
|
|
814
|
+
console.log('');
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
if (options.json) {
|
|
818
|
+
console.log(JSON.stringify(getRecommendationOutcomeSummary(options.dir), null, 2));
|
|
819
|
+
} else {
|
|
820
|
+
console.log('');
|
|
821
|
+
console.log(formatRecommendationOutcomeSummary(options.dir));
|
|
822
|
+
console.log('');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
process.exit(0);
|
|
826
|
+
} else if (normalizedCommand === 'augment' || normalizedCommand === 'suggest-only') {
|
|
827
|
+
const report = await analyzeProject({ ...options, mode: normalizedCommand });
|
|
828
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, normalizedCommand, report, {
|
|
829
|
+
sourceCommand: normalizedCommand,
|
|
830
|
+
}) : null;
|
|
831
|
+
if (options.out && !options.json) {
|
|
832
|
+
const fs = require('fs');
|
|
833
|
+
const md = exportMarkdown(report);
|
|
834
|
+
fs.writeFileSync(options.out, md, 'utf8');
|
|
835
|
+
console.log(`\n Report exported to ${options.out}\n`);
|
|
836
|
+
}
|
|
837
|
+
printAnalysis(report, options);
|
|
838
|
+
if (snapshot && !options.json) {
|
|
839
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
840
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
841
|
+
console.log('');
|
|
842
|
+
}
|
|
843
|
+
} else if (normalizedCommand === 'plan') {
|
|
844
|
+
const bundle = await buildProposalBundle(options);
|
|
845
|
+
let artifact = null;
|
|
846
|
+
if (options.out) {
|
|
847
|
+
artifact = writePlanFile(bundle, options.out);
|
|
848
|
+
}
|
|
849
|
+
printProposalBundle(bundle, options);
|
|
850
|
+
if (options.out && !options.json) {
|
|
851
|
+
console.log(` Plan written to ${options.out}`);
|
|
852
|
+
if (artifact) {
|
|
853
|
+
console.log(` Activity log: ${artifact.relativePath}`);
|
|
854
|
+
}
|
|
855
|
+
console.log('');
|
|
856
|
+
}
|
|
857
|
+
} else if (normalizedCommand === 'rollback') {
|
|
858
|
+
const fsMod = require('fs');
|
|
859
|
+
const pathMod = require('path');
|
|
860
|
+
const rollbackDir = pathMod.join(options.dir, '.nerviq', 'rollbacks');
|
|
861
|
+
|
|
862
|
+
if (!fsMod.existsSync(rollbackDir)) {
|
|
863
|
+
console.log('\n No rollback artifacts found. Run `nerviq apply` first to create rollback data.\n');
|
|
864
|
+
process.exit(0);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const rollbackFiles = fsMod.readdirSync(rollbackDir)
|
|
868
|
+
.filter(f => f.endsWith('.json'))
|
|
869
|
+
.sort()
|
|
870
|
+
.reverse();
|
|
871
|
+
|
|
872
|
+
if (rollbackFiles.length === 0) {
|
|
873
|
+
console.log('\n No rollback artifacts found.\n');
|
|
874
|
+
process.exit(0);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// --list mode
|
|
878
|
+
if (flags.includes('--list')) {
|
|
879
|
+
console.log(`\n Rollback points (${rollbackFiles.length}):\n`);
|
|
880
|
+
for (const f of rollbackFiles) {
|
|
881
|
+
try {
|
|
882
|
+
const data = JSON.parse(fsMod.readFileSync(pathMod.join(rollbackDir, f), 'utf8'));
|
|
883
|
+
const created = (data.createdFiles || []).length;
|
|
884
|
+
const patched = (data.patchedFiles || []).length;
|
|
885
|
+
console.log(` ${f.replace('.json', '')} (${created} created, ${patched} patched)`);
|
|
886
|
+
} catch {
|
|
887
|
+
console.log(` ${f} (unreadable)`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
console.log(`\n Run \`nerviq rollback\` to undo the most recent.\n`);
|
|
891
|
+
process.exit(0);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Execute rollback of most recent
|
|
895
|
+
const latestFile = rollbackFiles[0];
|
|
896
|
+
const latestPath = pathMod.join(rollbackDir, latestFile);
|
|
897
|
+
let rollbackData;
|
|
898
|
+
try {
|
|
899
|
+
rollbackData = JSON.parse(fsMod.readFileSync(latestPath, 'utf8'));
|
|
900
|
+
} catch (e) {
|
|
901
|
+
console.error(`\n Error: Cannot parse rollback file: ${e.message}\n`);
|
|
902
|
+
process.exit(1);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const createdFiles = rollbackData.createdFiles || [];
|
|
906
|
+
if (createdFiles.length === 0) {
|
|
907
|
+
console.log('\n Rollback artifact has no files to remove.\n');
|
|
908
|
+
process.exit(0);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
if (options.dryRun) {
|
|
912
|
+
console.log(`\n [dry-run] Would delete ${createdFiles.length} files:\n`);
|
|
913
|
+
for (const f of createdFiles) {
|
|
914
|
+
console.log(` - ${f}`);
|
|
915
|
+
}
|
|
916
|
+
console.log('');
|
|
917
|
+
process.exit(0);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let deleted = 0;
|
|
921
|
+
let missing = 0;
|
|
922
|
+
console.log('');
|
|
923
|
+
for (const relPath of createdFiles) {
|
|
924
|
+
const fullPath = pathMod.join(options.dir, relPath);
|
|
925
|
+
if (fsMod.existsSync(fullPath)) {
|
|
926
|
+
fsMod.unlinkSync(fullPath);
|
|
927
|
+
console.log(` 🗑️ Deleted: ${relPath}`);
|
|
928
|
+
deleted++;
|
|
929
|
+
} else {
|
|
930
|
+
missing++;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Remove rollback artifact after use
|
|
935
|
+
fsMod.unlinkSync(latestPath);
|
|
936
|
+
|
|
937
|
+
console.log(`\n Rollback complete: ${deleted} files deleted${missing > 0 ? `, ${missing} already missing` : ''}.\n`);
|
|
938
|
+
|
|
939
|
+
} else if (normalizedCommand === 'apply') {
|
|
940
|
+
if (flags.includes('--rollback')) {
|
|
941
|
+
console.error('\n Error: --rollback is not yet supported as a flag.');
|
|
942
|
+
console.error(' Why: Rollback artifacts are saved in .nerviq/rollbacks/ but automatic rollback is not implemented yet.');
|
|
943
|
+
console.error(' Fix: Manually delete the files listed in .nerviq/rollbacks/<latest>.json, or use `nerviq apply --dry-run` to preview before applying.');
|
|
944
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#rollback\n');
|
|
945
|
+
process.exit(1);
|
|
946
|
+
}
|
|
947
|
+
const result = await applyProposalBundle(options);
|
|
948
|
+
printApplyResult(result, options);
|
|
949
|
+
} else if (normalizedCommand === 'governance') {
|
|
950
|
+
const fs = require('fs');
|
|
951
|
+
const path = require('path');
|
|
952
|
+
const summary = getGovernanceSummary(options.platform);
|
|
953
|
+
if (options.out) {
|
|
954
|
+
fs.mkdirSync(path.dirname(options.out), { recursive: true });
|
|
955
|
+
const content = path.extname(options.out).toLowerCase() === '.md'
|
|
956
|
+
? renderGovernanceMarkdown(summary)
|
|
957
|
+
: JSON.stringify(summary, null, 2);
|
|
958
|
+
fs.writeFileSync(options.out, content, 'utf8');
|
|
959
|
+
}
|
|
960
|
+
printGovernanceSummary(summary, options);
|
|
961
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'governance', summary, {
|
|
962
|
+
sourceCommand: normalizedCommand,
|
|
963
|
+
}) : null;
|
|
964
|
+
if (options.out && !options.json) {
|
|
965
|
+
console.log(` Governance report written to ${options.out}`);
|
|
966
|
+
console.log('');
|
|
967
|
+
}
|
|
968
|
+
if (snapshot && !options.json) {
|
|
969
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
970
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
971
|
+
console.log('');
|
|
972
|
+
}
|
|
973
|
+
} else if (normalizedCommand === 'benchmark') {
|
|
974
|
+
const report = await runBenchmark(options);
|
|
975
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'benchmark', report, {
|
|
976
|
+
sourceCommand: normalizedCommand,
|
|
977
|
+
}) : null;
|
|
978
|
+
if (options.out) {
|
|
979
|
+
writeBenchmarkReport(report, options.out);
|
|
980
|
+
}
|
|
981
|
+
printBenchmark(report, options);
|
|
982
|
+
if (options.out && !options.json) {
|
|
983
|
+
console.log(` Benchmark report written to ${options.out}`);
|
|
984
|
+
console.log('');
|
|
985
|
+
}
|
|
986
|
+
if (snapshot && !options.json) {
|
|
987
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
988
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
989
|
+
console.log('');
|
|
990
|
+
}
|
|
991
|
+
} else if (normalizedCommand === 'deep-review') {
|
|
992
|
+
const { deepReview } = require('../src/deep-review');
|
|
993
|
+
await deepReview(options);
|
|
994
|
+
} else if (normalizedCommand === 'interactive') {
|
|
995
|
+
const { interactive } = require('../src/interactive');
|
|
996
|
+
await interactive(options);
|
|
997
|
+
} else if (normalizedCommand === 'watch') {
|
|
998
|
+
const { watch } = require('../src/watch');
|
|
999
|
+
await watch(options);
|
|
1000
|
+
} else if (normalizedCommand === 'catalog') {
|
|
1001
|
+
const { generateCatalog, generateCatalogWithVersion, writeCatalogJson } = require('../src/catalog');
|
|
1002
|
+
if (options.out) {
|
|
1003
|
+
const result = writeCatalogJson(options.out);
|
|
1004
|
+
if (options.json) {
|
|
1005
|
+
console.log(JSON.stringify({ path: result.path, count: result.count }));
|
|
1006
|
+
} else {
|
|
1007
|
+
console.log(`\n Catalog written to ${result.path} (${result.count} checks)\n`);
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
const catalog = generateCatalog();
|
|
1011
|
+
if (options.json) {
|
|
1012
|
+
const envelope = generateCatalogWithVersion();
|
|
1013
|
+
if (options.checkVersion) envelope.requestedVersion = options.checkVersion;
|
|
1014
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
1015
|
+
} else {
|
|
1016
|
+
// Print summary table
|
|
1017
|
+
const platforms = {};
|
|
1018
|
+
for (const entry of catalog) {
|
|
1019
|
+
platforms[entry.platform] = (platforms[entry.platform] || 0) + 1;
|
|
1020
|
+
}
|
|
1021
|
+
console.log('');
|
|
1022
|
+
console.log('\x1b[1m nerviq check catalog\x1b[0m');
|
|
1023
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
1024
|
+
console.log(` Total checks: \x1b[1m${catalog.length}\x1b[0m`);
|
|
1025
|
+
console.log('');
|
|
1026
|
+
for (const [plat, count] of Object.entries(platforms)) {
|
|
1027
|
+
console.log(` ${plat.padEnd(12)} ${count} checks`);
|
|
1028
|
+
}
|
|
1029
|
+
console.log('');
|
|
1030
|
+
console.log(' Use --json for full output or --out catalog.json to write file.');
|
|
1031
|
+
console.log('');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
process.exit(0);
|
|
1035
|
+
} else if (normalizedCommand === 'certify') {
|
|
1036
|
+
const { certifyProject, generateCertBadge } = require('../src/certification');
|
|
1037
|
+
const certResult = await certifyProject(options.dir);
|
|
1038
|
+
if (options.json) {
|
|
1039
|
+
console.log(JSON.stringify(certResult, null, 2));
|
|
1040
|
+
} else {
|
|
1041
|
+
console.log('');
|
|
1042
|
+
console.log('\x1b[1m nerviq certification\x1b[0m');
|
|
1043
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
1044
|
+
console.log('');
|
|
1045
|
+
console.log(` Level: \x1b[1m${certResult.level}\x1b[0m`);
|
|
1046
|
+
console.log(` Harmony Score: ${certResult.harmonyScore}/100`);
|
|
1047
|
+
console.log('');
|
|
1048
|
+
if (Object.keys(certResult.platformScores).length > 0) {
|
|
1049
|
+
console.log(' Platform Scores:');
|
|
1050
|
+
for (const [plat, score] of Object.entries(certResult.platformScores)) {
|
|
1051
|
+
const scoreColor = score >= 70 ? '\x1b[32m' : score >= 40 ? '\x1b[33m' : '\x1b[31m';
|
|
1052
|
+
console.log(` ${plat.padEnd(12)} ${scoreColor}${score}/100\x1b[0m`);
|
|
1053
|
+
}
|
|
1054
|
+
console.log('');
|
|
1055
|
+
}
|
|
1056
|
+
console.log(' Badge:');
|
|
1057
|
+
console.log(` ${certResult.badge}`);
|
|
1058
|
+
console.log('');
|
|
1059
|
+
console.log(' Add the badge to your README.md');
|
|
1060
|
+
console.log('');
|
|
1061
|
+
}
|
|
1062
|
+
process.exit(0);
|
|
1063
|
+
} else if (normalizedCommand === 'serve') {
|
|
1064
|
+
const server = await startServer({
|
|
1065
|
+
port: options.port == null ? 3000 : options.port,
|
|
1066
|
+
baseDir: options.dir,
|
|
1067
|
+
});
|
|
1068
|
+
const address = server.address();
|
|
1069
|
+
const resolvedPort = address && typeof address === 'object' ? address.port : options.port;
|
|
1070
|
+
console.log('');
|
|
1071
|
+
console.log(` nerviq API listening on http://127.0.0.1:${resolvedPort}`);
|
|
1072
|
+
console.log(' Endpoints: /api/health, /api/catalog, /api/audit, /api/harmony');
|
|
1073
|
+
console.log('');
|
|
1074
|
+
|
|
1075
|
+
const closeServer = () => {
|
|
1076
|
+
server.close(() => process.exit(0));
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
process.on('SIGINT', closeServer);
|
|
1080
|
+
process.on('SIGTERM', closeServer);
|
|
1081
|
+
return;
|
|
1082
|
+
} else if (normalizedCommand === 'harmony-audit') {
|
|
1083
|
+
const { runHarmonyAudit } = require('../src/harmony/cli');
|
|
1084
|
+
await runHarmonyAudit(options);
|
|
1085
|
+
process.exit(0);
|
|
1086
|
+
} else if (normalizedCommand === 'harmony-sync') {
|
|
1087
|
+
const { previewHarmonySync, applyHarmonySync } = require('../src/harmony/sync');
|
|
1088
|
+
const dir = options.dir || process.cwd();
|
|
1089
|
+
|
|
1090
|
+
if (options.fix) {
|
|
1091
|
+
// Apply mode: write files
|
|
1092
|
+
const result = applyHarmonySync(dir);
|
|
1093
|
+
if (options.json) {
|
|
1094
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1095
|
+
} else {
|
|
1096
|
+
console.log('');
|
|
1097
|
+
console.log('\x1b[1m Harmony Sync — Apply\x1b[0m');
|
|
1098
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
1099
|
+
console.log('');
|
|
1100
|
+
if (result.applied.length === 0 && result.skipped.length === 0) {
|
|
1101
|
+
console.log(' \x1b[32mAll platforms are already in sync. Nothing to apply.\x1b[0m');
|
|
1102
|
+
} else {
|
|
1103
|
+
for (const item of result.applied) {
|
|
1104
|
+
console.log(` \x1b[32m✓\x1b[0m ${item.action.padEnd(8)} ${item.platform.padEnd(12)} ${item.path}`);
|
|
1105
|
+
}
|
|
1106
|
+
for (const item of result.skipped) {
|
|
1107
|
+
const reason = typeof item === 'string' ? item : (item.reason || item.path);
|
|
1108
|
+
console.log(` \x1b[33m⚠\x1b[0m skipped ${reason}`);
|
|
1109
|
+
}
|
|
1110
|
+
console.log('');
|
|
1111
|
+
if (result.summary) {
|
|
1112
|
+
console.log(` Files: ${result.summary.totalFiles} (${result.summary.creates} created, ${result.summary.patches} patched)`);
|
|
1113
|
+
console.log(` Platforms: ${result.summary.platforms.join(', ')}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1117
|
+
console.log('');
|
|
1118
|
+
for (const w of result.warnings) {
|
|
1119
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
console.log('');
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
// Preview mode (dry run)
|
|
1126
|
+
const plan = previewHarmonySync(dir);
|
|
1127
|
+
if (options.json) {
|
|
1128
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
1129
|
+
} else {
|
|
1130
|
+
console.log('');
|
|
1131
|
+
console.log('\x1b[1m Harmony Sync — Preview\x1b[0m');
|
|
1132
|
+
console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
|
|
1133
|
+
console.log('');
|
|
1134
|
+
if (plan.files.length === 0) {
|
|
1135
|
+
console.log(' \x1b[32mAll platforms are already in sync. No changes needed.\x1b[0m');
|
|
1136
|
+
} else {
|
|
1137
|
+
for (const file of plan.files) {
|
|
1138
|
+
const actionColor = file.action === 'create' ? '\x1b[32m' : '\x1b[36m';
|
|
1139
|
+
console.log(` ${actionColor}${file.action.padEnd(8)}\x1b[0m ${file.platform.padEnd(12)} ${file.path}`);
|
|
1140
|
+
if (file.preview) {
|
|
1141
|
+
console.log(` \x1b[2m${file.preview}\x1b[0m`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
console.log('');
|
|
1145
|
+
console.log(` Total: ${plan.summary.totalFiles} file(s) — ${plan.summary.creates} create, ${plan.summary.patches} patch`);
|
|
1146
|
+
console.log(` Platforms: ${plan.summary.platforms.join(', ')}`);
|
|
1147
|
+
if (plan.summary.recommendedTrust) {
|
|
1148
|
+
console.log(` Recommended trust: ${plan.summary.recommendedTrust}`);
|
|
1149
|
+
}
|
|
1150
|
+
console.log('');
|
|
1151
|
+
console.log(' Run \x1b[1mnerviq harmony-sync --fix\x1b[0m to apply these changes.');
|
|
1152
|
+
}
|
|
1153
|
+
if (plan.warnings && plan.warnings.length > 0) {
|
|
1154
|
+
console.log('');
|
|
1155
|
+
for (const w of plan.warnings) {
|
|
1156
|
+
console.log(` \x1b[33m⚠\x1b[0m ${w}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
console.log('');
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
process.exit(0);
|
|
1163
|
+
} else if (normalizedCommand === 'harmony-drift') {
|
|
1164
|
+
const { runHarmonyDrift } = require('../src/harmony/cli');
|
|
1165
|
+
await runHarmonyDrift(options);
|
|
1166
|
+
process.exit(0);
|
|
1167
|
+
} else if (normalizedCommand === 'harmony-advise') {
|
|
1168
|
+
const { runHarmonyAdvise } = require('../src/harmony/cli');
|
|
1169
|
+
await runHarmonyAdvise(options);
|
|
1170
|
+
process.exit(0);
|
|
1171
|
+
} else if (normalizedCommand === 'harmony-watch') {
|
|
1172
|
+
const { runHarmonyWatch } = require('../src/harmony/cli');
|
|
1173
|
+
await runHarmonyWatch(options);
|
|
1174
|
+
} else if (normalizedCommand === 'harmony-governance') {
|
|
1175
|
+
const { runHarmonyGovernance } = require('../src/harmony/cli');
|
|
1176
|
+
await runHarmonyGovernance(options);
|
|
1177
|
+
process.exit(0);
|
|
1178
|
+
} else if (normalizedCommand === 'harmony-add') {
|
|
1179
|
+
const { addPlatform } = require('../src/harmony/add');
|
|
1180
|
+
const platformArg = parsed.extraArgs[0];
|
|
1181
|
+
if (!platformArg) {
|
|
1182
|
+
console.log('\n Usage: nerviq harmony-add <platform>');
|
|
1183
|
+
console.log(' Available: claude, codex, gemini, copilot, cursor, windsurf, aider, opencode\n');
|
|
1184
|
+
process.exit(1);
|
|
1185
|
+
}
|
|
1186
|
+
const dir = options.dir || process.cwd();
|
|
1187
|
+
const result = addPlatform(dir, platformArg.toLowerCase());
|
|
1188
|
+
if (options.json) {
|
|
1189
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1190
|
+
} else if (result.success) {
|
|
1191
|
+
console.log(`\n \x1b[32m\u2713\x1b[0m Added ${result.platform} to project`);
|
|
1192
|
+
result.created.forEach(f => console.log(` Created: ${f}`));
|
|
1193
|
+
console.log(` Platforms: ${result.beforeCount} \u2192 ${result.afterCount}`);
|
|
1194
|
+
if (result.syncApplied > 0) console.log(` Harmony sync: ${result.syncApplied} file(s) updated`);
|
|
1195
|
+
console.log('');
|
|
1196
|
+
} else {
|
|
1197
|
+
console.log(`\n \x1b[31m\u2717\x1b[0m ${result.error}\n`);
|
|
1198
|
+
process.exit(1);
|
|
1199
|
+
}
|
|
1200
|
+
process.exit(0);
|
|
1201
|
+
} else if (normalizedCommand === 'anti-patterns') {
|
|
1202
|
+
const showAll = flags.includes('--all');
|
|
1203
|
+
if (showAll) {
|
|
1204
|
+
printAntiPatternCatalog(options);
|
|
1205
|
+
} else {
|
|
1206
|
+
const { ProjectContext } = require('../src/context');
|
|
1207
|
+
const ctx = new ProjectContext(options.dir);
|
|
1208
|
+
const detected = detectAntiPatterns(ctx);
|
|
1209
|
+
printAntiPatterns(detected, options);
|
|
1210
|
+
}
|
|
1211
|
+
process.exit(0);
|
|
1212
|
+
} else if (normalizedCommand === 'rules-export') {
|
|
1213
|
+
const { generateRecommendationRules } = require('../src/recommendation-rules');
|
|
1214
|
+
const rules = generateRecommendationRules();
|
|
1215
|
+
if (options.json) {
|
|
1216
|
+
console.log(JSON.stringify(rules, null, 2));
|
|
1217
|
+
} else if (options.out) {
|
|
1218
|
+
require('fs').writeFileSync(options.out, JSON.stringify(rules, null, 2), 'utf8');
|
|
1219
|
+
console.log(`\n Rules exported to ${options.out} (${rules.totalRules} rules)\n`);
|
|
1220
|
+
} else {
|
|
1221
|
+
// Human-readable summary
|
|
1222
|
+
console.log(`\n Nerviq Recommendation Rules (${rules.totalRules} rules)\n`);
|
|
1223
|
+
const byCategory = {};
|
|
1224
|
+
for (const rule of (rules.rules || [])) {
|
|
1225
|
+
const cat = rule.category || 'other';
|
|
1226
|
+
if (!byCategory[cat]) byCategory[cat] = 0;
|
|
1227
|
+
byCategory[cat]++;
|
|
1228
|
+
}
|
|
1229
|
+
for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
|
|
1230
|
+
console.log(` ${cat.padEnd(20)} ${count} rules`);
|
|
1231
|
+
}
|
|
1232
|
+
console.log(`\n Use --json for full output or --out <file> to save.\n`);
|
|
1233
|
+
}
|
|
1234
|
+
process.exit(0);
|
|
1235
|
+
} else if (normalizedCommand === 'dashboard') {
|
|
1236
|
+
const dashFlags = {
|
|
1237
|
+
out: options.out,
|
|
1238
|
+
open: flags.includes('--open'),
|
|
1239
|
+
json: options.json,
|
|
1240
|
+
platform: options.platform,
|
|
1241
|
+
};
|
|
1242
|
+
if (parsed.repos && parsed.repos.length > 0) {
|
|
1243
|
+
const { generatePortfolioDashboard } = require('../src/dashboard');
|
|
1244
|
+
await generatePortfolioDashboard(parsed.repos, dashFlags);
|
|
1245
|
+
} else {
|
|
1246
|
+
const { generateDashboard } = require('../src/dashboard');
|
|
1247
|
+
await generateDashboard(options.dir, dashFlags);
|
|
1248
|
+
}
|
|
1249
|
+
process.exit(0);
|
|
1250
|
+
} else if (normalizedCommand === 'check-health') {
|
|
1251
|
+
const { checkHealth, formatCheckHealth } = require('../src/activity');
|
|
1252
|
+
const report = checkHealth(options.dir);
|
|
1253
|
+
if (options.json) {
|
|
1254
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1255
|
+
} else {
|
|
1256
|
+
console.log('');
|
|
1257
|
+
console.log(formatCheckHealth(report));
|
|
1258
|
+
}
|
|
1259
|
+
process.exit(0);
|
|
1260
|
+
} else if (normalizedCommand === 'freshness') {
|
|
1261
|
+
const { TECHNIQUES } = require('../src/techniques');
|
|
1262
|
+
const stats = getVerificationStats();
|
|
1263
|
+
const allKeys = Object.keys(TECHNIQUES);
|
|
1264
|
+
const verifiedKeys = Object.keys(VERIFICATION_DATES);
|
|
1265
|
+
const neverVerified = allKeys.filter(k => !VERIFICATION_DATES[k]);
|
|
1266
|
+
|
|
1267
|
+
if (options.json) {
|
|
1268
|
+
console.log(JSON.stringify({
|
|
1269
|
+
totalChecks: allKeys.length,
|
|
1270
|
+
verifiedChecks: verifiedKeys.length,
|
|
1271
|
+
neverVerifiedCount: neverVerified.length,
|
|
1272
|
+
newestVerification: stats.newest,
|
|
1273
|
+
oldestVerification: stats.oldest,
|
|
1274
|
+
neverVerified,
|
|
1275
|
+
}, null, 2));
|
|
1276
|
+
} else {
|
|
1277
|
+
console.log('');
|
|
1278
|
+
console.log(' nerviq freshness');
|
|
1279
|
+
console.log(' ═══════════════════════════════════════');
|
|
1280
|
+
console.log(` Total checks: ${allKeys.length}`);
|
|
1281
|
+
console.log(` With verification date: ${verifiedKeys.length}`);
|
|
1282
|
+
console.log(` Never verified: ${neverVerified.length}`);
|
|
1283
|
+
console.log(` Newest verification: ${stats.newest}`);
|
|
1284
|
+
console.log(` Oldest verification: ${stats.oldest}`);
|
|
1285
|
+
console.log('');
|
|
1286
|
+
if (neverVerified.length > 0 && options.verbose) {
|
|
1287
|
+
console.log(' Never verified:');
|
|
1288
|
+
for (const key of neverVerified) {
|
|
1289
|
+
console.log(` - ${key}`);
|
|
1290
|
+
}
|
|
1291
|
+
console.log('');
|
|
1292
|
+
} else if (neverVerified.length > 0) {
|
|
1293
|
+
console.log(` Use --verbose to list all ${neverVerified.length} never-verified checks.`);
|
|
1294
|
+
console.log('');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
process.exit(0);
|
|
1298
|
+
} else if (normalizedCommand === 'suggest-rules') {
|
|
1299
|
+
const { analyzeSuggestions, formatSuggestions } = require('../src/auto-suggest');
|
|
1300
|
+
const suggestions = analyzeSuggestions(options.dir);
|
|
1301
|
+
if (options.json) {
|
|
1302
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
1303
|
+
} else {
|
|
1304
|
+
console.log('');
|
|
1305
|
+
console.log(formatSuggestions(suggestions));
|
|
1306
|
+
console.log('');
|
|
1307
|
+
}
|
|
1308
|
+
process.exit(0);
|
|
1309
|
+
} else if (normalizedCommand === 'profile') {
|
|
1310
|
+
const { saveProfile, loadProfile, listProfiles, exportProfile, formatProfileList, formatProfile } = require('../src/profiles');
|
|
1311
|
+
const subcommand = parsed.extraArgs[0];
|
|
1312
|
+
const profileArg = parsed.extraArgs[1];
|
|
1313
|
+
|
|
1314
|
+
if (!subcommand || subcommand === 'list') {
|
|
1315
|
+
const profiles = listProfiles(options.dir);
|
|
1316
|
+
console.log('');
|
|
1317
|
+
console.log(formatProfileList(profiles));
|
|
1318
|
+
console.log('');
|
|
1319
|
+
process.exit(0);
|
|
1320
|
+
} else if (subcommand === 'save') {
|
|
1321
|
+
if (!profileArg) {
|
|
1322
|
+
console.error('\n Error: Profile name required. Usage: nerviq profile save <name>\n');
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
const result = saveProfile(options.dir, profileArg, {
|
|
1326
|
+
platforms: [options.platform],
|
|
1327
|
+
threshold: options.threshold,
|
|
1328
|
+
suppressedChecks: [],
|
|
1329
|
+
priorityBoosts: [],
|
|
1330
|
+
description: '',
|
|
1331
|
+
});
|
|
1332
|
+
if (options.json) {
|
|
1333
|
+
console.log(JSON.stringify(result.profile, null, 2));
|
|
1334
|
+
} else {
|
|
1335
|
+
console.log(`\n Profile '${profileArg}' saved to ${result.path}\n`);
|
|
1336
|
+
}
|
|
1337
|
+
process.exit(0);
|
|
1338
|
+
} else if (subcommand === 'load') {
|
|
1339
|
+
if (!profileArg) {
|
|
1340
|
+
console.error('\n Error: Profile name required. Usage: nerviq profile load <name>\n');
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
const profile = loadProfile(options.dir, profileArg);
|
|
1344
|
+
if (options.json) {
|
|
1345
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
1346
|
+
} else {
|
|
1347
|
+
console.log('');
|
|
1348
|
+
console.log(formatProfile(profile));
|
|
1349
|
+
console.log('');
|
|
1350
|
+
}
|
|
1351
|
+
process.exit(0);
|
|
1352
|
+
} else if (subcommand === 'export') {
|
|
1353
|
+
if (!profileArg) {
|
|
1354
|
+
console.error('\n Error: Profile name required. Usage: nerviq profile export <name>\n');
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
const json = exportProfile(options.dir, profileArg);
|
|
1358
|
+
if (options.out) {
|
|
1359
|
+
require('fs').writeFileSync(options.out, json, 'utf8');
|
|
1360
|
+
console.log(`\n Profile exported to ${options.out}\n`);
|
|
1361
|
+
} else {
|
|
1362
|
+
console.log(json);
|
|
1363
|
+
}
|
|
1364
|
+
process.exit(0);
|
|
1365
|
+
} else {
|
|
1366
|
+
console.error(`\n Error: Unknown profile subcommand '${subcommand}'.`);
|
|
1367
|
+
console.error(' Usage: nerviq profile save|load|list|export <name>\n');
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
}
|
|
1370
|
+
} else if (normalizedCommand === 'synergy-report') {
|
|
1371
|
+
// Placeholder — synergy report is referenced but may not be implemented yet
|
|
1372
|
+
console.log('\n Synergy report: coming soon.\n');
|
|
1373
|
+
process.exit(0);
|
|
1374
|
+
} else if (normalizedCommand === 'doctor') {
|
|
1375
|
+
const { runDoctor } = require('../src/doctor');
|
|
1376
|
+
const output = await runDoctor({ dir: options.dir, json: options.json, verbose: options.verbose });
|
|
1377
|
+
console.log(output);
|
|
1378
|
+
process.exit(0);
|
|
1379
|
+
} else if (normalizedCommand === 'convert') {
|
|
1380
|
+
const { runConvert } = require('../src/convert');
|
|
1381
|
+
const output = await runConvert({
|
|
1382
|
+
dir: options.dir,
|
|
1383
|
+
from: parsed.convertFrom,
|
|
1384
|
+
to: parsed.convertTo,
|
|
1385
|
+
dryRun: options.dryRun,
|
|
1386
|
+
json: options.json,
|
|
1387
|
+
});
|
|
1388
|
+
console.log(output);
|
|
1389
|
+
process.exit(0);
|
|
1390
|
+
} else if (normalizedCommand === 'migrate') {
|
|
1391
|
+
const { runMigrate } = require('../src/migrate');
|
|
1392
|
+
const output = await runMigrate({
|
|
1393
|
+
dir: options.dir,
|
|
1394
|
+
platform: options.platform || parsed.platform || 'claude',
|
|
1395
|
+
from: parsed.migrateFrom,
|
|
1396
|
+
to: parsed.migrateTo,
|
|
1397
|
+
dryRun: options.dryRun,
|
|
1398
|
+
json: options.json,
|
|
1399
|
+
});
|
|
1400
|
+
console.log(output);
|
|
1401
|
+
process.exit(0);
|
|
1402
|
+
} else if (normalizedCommand === 'fix') {
|
|
1403
|
+
// nerviq fix [key] [--all-critical] [--dry-run] [--auto] [--prompt]
|
|
1404
|
+
const fixKey = parsed.extraArgs[0] || null;
|
|
1405
|
+
const allCritical = flags.includes('--all-critical');
|
|
1406
|
+
const promptOnly = flags.includes('--prompt');
|
|
1407
|
+
const autoApply = options.auto || options.dryRun;
|
|
1408
|
+
|
|
1409
|
+
// Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
|
|
1410
|
+
const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1411
|
+
const failedResults = (auditResult.results || []).filter(r => r.passed === false);
|
|
1412
|
+
|
|
1413
|
+
if (failedResults.length === 0) {
|
|
1414
|
+
console.log('\n ✅ All checks passing — nothing to fix.\n');
|
|
1415
|
+
process.exit(0);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Step 2: Determine which checks to fix
|
|
1419
|
+
const { TECHNIQUES } = require('../src/techniques');
|
|
1420
|
+
const { FIX_PROMPTS, formatFixPrompt } = require('../src/fix-prompts');
|
|
1421
|
+
const fs = require('fs');
|
|
1422
|
+
const pathMod = require('path');
|
|
1423
|
+
|
|
1424
|
+
// Inline fixers for checks without templates but with trivial auto-fixes
|
|
1425
|
+
const INLINE_FIXERS = {
|
|
1426
|
+
gitIgnoreEnv: (dir) => {
|
|
1427
|
+
const gitignorePath = pathMod.join(dir, '.gitignore');
|
|
1428
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1429
|
+
if (!existing.includes('.env')) {
|
|
1430
|
+
const lines = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
1431
|
+
fs.appendFileSync(gitignorePath, `${lines}.env\n.env.*\n`, 'utf8');
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
return false;
|
|
1435
|
+
},
|
|
1436
|
+
secretsProtection: (dir) => {
|
|
1437
|
+
const settingsPath = pathMod.join(dir, '.claude', 'settings.json');
|
|
1438
|
+
const settingsDir = pathMod.join(dir, '.claude');
|
|
1439
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
1440
|
+
let settings = {};
|
|
1441
|
+
if (fs.existsSync(settingsPath)) {
|
|
1442
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { settings = {}; }
|
|
1443
|
+
}
|
|
1444
|
+
if (!settings.permissions) settings.permissions = {};
|
|
1445
|
+
if (!settings.permissions.deny) settings.permissions.deny = [];
|
|
1446
|
+
const denyEntries = ['.env', '.env.*', '**/.env', '**/*.pem', '**/secrets/**'];
|
|
1447
|
+
for (const entry of denyEntries) {
|
|
1448
|
+
if (!settings.permissions.deny.includes(entry)) settings.permissions.deny.push(entry);
|
|
1449
|
+
}
|
|
1450
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1451
|
+
return true;
|
|
1452
|
+
},
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
let targetKeys = [];
|
|
1456
|
+
|
|
1457
|
+
if (fixKey) {
|
|
1458
|
+
// Fix a specific check
|
|
1459
|
+
if (!failedResults.find(r => r.key === fixKey)) {
|
|
1460
|
+
const passed = (auditResult.results || []).find(r => r.key === fixKey && r.passed);
|
|
1461
|
+
if (passed) {
|
|
1462
|
+
console.log(`\n ✅ '${fixKey}' is already passing.\n`);
|
|
1463
|
+
} else {
|
|
1464
|
+
console.log(`\n Error: Unknown check key '${fixKey}'.`);
|
|
1465
|
+
console.log(` Fix: Run 'nerviq audit --full' to see all check keys.\n`);
|
|
1466
|
+
}
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
// --prompt flag: show AI prompt and exit without attempting fix
|
|
1470
|
+
if (promptOnly) {
|
|
1471
|
+
const prompt = FIX_PROMPTS[fixKey];
|
|
1472
|
+
if (prompt) {
|
|
1473
|
+
console.log(formatFixPrompt(fixKey, prompt));
|
|
1474
|
+
} else {
|
|
1475
|
+
const failedCheck = failedResults.find(r => r.key === fixKey);
|
|
1476
|
+
console.log(`\n No AI prompt available for '${fixKey}'.`);
|
|
1477
|
+
console.log(` Manual fix: ${failedCheck ? failedCheck.fix : 'See nerviq audit --full.'}\n`);
|
|
1478
|
+
}
|
|
1479
|
+
process.exit(0);
|
|
1480
|
+
}
|
|
1481
|
+
targetKeys = [fixKey];
|
|
1482
|
+
} else if (allCritical) {
|
|
1483
|
+
targetKeys = failedResults.filter(r => r.impact === 'critical').map(r => r.key);
|
|
1484
|
+
if (targetKeys.length === 0) {
|
|
1485
|
+
console.log('\n ✅ No critical issues found.\n');
|
|
1486
|
+
process.exit(0);
|
|
1487
|
+
}
|
|
1488
|
+
} else {
|
|
1489
|
+
// No key specified — show fixable checks and exit
|
|
1490
|
+
const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
|
|
1491
|
+
const fixable = failedResults.filter(r => (TECHNIQUES[r.key] && TECHNIQUES[r.key].template) || INLINE_FIX_KEYS.has(r.key));
|
|
1492
|
+
const nonFixable = failedResults.filter(r => !(TECHNIQUES[r.key] && TECHNIQUES[r.key].template) && !INLINE_FIX_KEYS.has(r.key));
|
|
1493
|
+
console.log('');
|
|
1494
|
+
console.log(` nerviq fix — ${failedResults.length} failed checks\n`);
|
|
1495
|
+
if (fixable.length > 0) {
|
|
1496
|
+
console.log(` Auto-fixable (${fixable.length}):`);
|
|
1497
|
+
for (const r of fixable) {
|
|
1498
|
+
const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
|
|
1499
|
+
console.log(` ${tier} nerviq fix ${r.key}`);
|
|
1500
|
+
}
|
|
1501
|
+
console.log('');
|
|
1502
|
+
}
|
|
1503
|
+
if (nonFixable.length > 0) {
|
|
1504
|
+
const withPrompt = nonFixable.filter(r => FIX_PROMPTS[r.key]);
|
|
1505
|
+
const withoutPrompt = nonFixable.filter(r => !FIX_PROMPTS[r.key]);
|
|
1506
|
+
if (withPrompt.length > 0) {
|
|
1507
|
+
console.log(` AI prompt available (${withPrompt.length}):`);
|
|
1508
|
+
for (const r of withPrompt.slice(0, 5)) {
|
|
1509
|
+
const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
|
|
1510
|
+
console.log(` ${tier} nerviq fix ${r.key} --prompt`);
|
|
1511
|
+
}
|
|
1512
|
+
if (withPrompt.length > 5) {
|
|
1513
|
+
console.log(` ... and ${withPrompt.length - 5} more`);
|
|
1514
|
+
}
|
|
1515
|
+
console.log('');
|
|
1516
|
+
}
|
|
1517
|
+
if (withoutPrompt.length > 0) {
|
|
1518
|
+
console.log(` Manual fix needed (${withoutPrompt.length}):`);
|
|
1519
|
+
for (const r of withoutPrompt.slice(0, 5)) {
|
|
1520
|
+
const tier = r.impact === 'critical' ? '🔴' : r.impact === 'high' ? '🟡' : '🔵';
|
|
1521
|
+
console.log(` ${tier} ${r.key}: ${r.fix}`);
|
|
1522
|
+
}
|
|
1523
|
+
if (withoutPrompt.length > 5) {
|
|
1524
|
+
console.log(` ... and ${withoutPrompt.length - 5} more (use --full to see all)`);
|
|
1525
|
+
}
|
|
1526
|
+
console.log('');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
if (fixable.length > 0) {
|
|
1530
|
+
console.log(` Quick actions:`);
|
|
1531
|
+
console.log(` nerviq fix ${fixable[0].key} Fix the first auto-fixable check`);
|
|
1532
|
+
console.log(` nerviq fix --all-critical Fix all critical issues at once`);
|
|
1533
|
+
}
|
|
1534
|
+
console.log('');
|
|
1535
|
+
process.exit(0);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Step 2.5: Predict impact and show preview before applying
|
|
1539
|
+
const IMPACT_WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
1540
|
+
const preScore = auditResult.score;
|
|
1541
|
+
const applicableResults = (auditResult.results || []).filter(r => r.passed !== null);
|
|
1542
|
+
const maxScore = applicableResults.reduce((sum, r) => sum + (IMPACT_WEIGHTS[r.impact] || 5), 0);
|
|
1543
|
+
|
|
1544
|
+
// Compute predicted score by simulating target fixes as passing
|
|
1545
|
+
const targetKeySet = new Set(targetKeys);
|
|
1546
|
+
const INLINE_FIX_KEYS = new Set(Object.keys(INLINE_FIXERS));
|
|
1547
|
+
const fixableTargets = targetKeys.filter(k => {
|
|
1548
|
+
const tech = TECHNIQUES[k];
|
|
1549
|
+
return (tech && tech.template) || INLINE_FIX_KEYS.has(k);
|
|
1550
|
+
});
|
|
1551
|
+
const fixableTargetSet = new Set(fixableTargets);
|
|
1552
|
+
const simulatedEarned = applicableResults.reduce((sum, r) => {
|
|
1553
|
+
const w = IMPACT_WEIGHTS[r.impact] || 5;
|
|
1554
|
+
if (r.passed) return sum + w;
|
|
1555
|
+
if (fixableTargetSet.has(r.key)) return sum + w;
|
|
1556
|
+
return sum;
|
|
1557
|
+
}, 0);
|
|
1558
|
+
const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
|
|
1559
|
+
const predictedDelta = predictedScore - preScore;
|
|
1560
|
+
|
|
1561
|
+
if (!autoApply) {
|
|
1562
|
+
console.log('');
|
|
1563
|
+
if (allCritical && fixableTargets.length > 1) {
|
|
1564
|
+
// Multi-fix summary
|
|
1565
|
+
console.log(` ${fixableTargets.length} critical fixes available:`);
|
|
1566
|
+
let runningEarned = applicableResults.reduce((s, r) => s + (r.passed ? (IMPACT_WEIGHTS[r.impact] || 5) : 0), 0);
|
|
1567
|
+
let runningScore = maxScore > 0 ? Math.round((runningEarned / maxScore) * 100) : 0;
|
|
1568
|
+
fixableTargets.forEach((k, idx) => {
|
|
1569
|
+
const r = failedResults.find(fr => fr.key === k);
|
|
1570
|
+
const w = IMPACT_WEIGHTS[r.impact] || 5;
|
|
1571
|
+
const nextEarned = runningEarned + w;
|
|
1572
|
+
const nextScore = maxScore > 0 ? Math.round((nextEarned / maxScore) * 100) : 0;
|
|
1573
|
+
const d = nextScore - runningScore;
|
|
1574
|
+
console.log(` ${idx + 1}. ${(r.key).padEnd(18)} ${runningScore} → ${nextScore} (+${d})`);
|
|
1575
|
+
runningEarned = nextEarned;
|
|
1576
|
+
runningScore = nextScore;
|
|
1577
|
+
});
|
|
1578
|
+
console.log('');
|
|
1579
|
+
console.log(` Total: ${preScore} → ${predictedScore} (+${predictedDelta})`);
|
|
1580
|
+
} else {
|
|
1581
|
+
// Single fix preview
|
|
1582
|
+
const targetCheck = failedResults.find(r => r.key === fixableTargets[0]) || failedResults.find(r => r.key === targetKeys[0]);
|
|
1583
|
+
if (targetCheck) {
|
|
1584
|
+
console.log(` Predicted impact: ${preScore} → ${predictedScore} (+${predictedDelta})`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Prompt for confirmation
|
|
1589
|
+
const readline = require('readline');
|
|
1590
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1591
|
+
const answer = await new Promise(resolve => {
|
|
1592
|
+
rl.question(' Apply? (Y/n) ', resolve);
|
|
1593
|
+
});
|
|
1594
|
+
rl.close();
|
|
1595
|
+
if (answer && answer.trim().toLowerCase() === 'n') {
|
|
1596
|
+
for (const key of targetKeys) {
|
|
1597
|
+
recordPattern(options.dir, key, 'rejected');
|
|
1598
|
+
}
|
|
1599
|
+
console.log('\n Aborted.\n');
|
|
1600
|
+
process.exit(0);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Step 3: Create rollback snapshot before applying fixes
|
|
1605
|
+
const isBatch = allCritical && targetKeys.length > 1;
|
|
1606
|
+
let rollbackId = null;
|
|
1607
|
+
const allCreatedFiles = [];
|
|
1608
|
+
const fixResults = []; // { key, name, status, delta }
|
|
1609
|
+
|
|
1610
|
+
if (!options.dryRun && targetKeys.length > 0) {
|
|
1611
|
+
// Snapshot existing files for rollback
|
|
1612
|
+
const snapshotFiles = {};
|
|
1613
|
+
for (const key of targetKeys) {
|
|
1614
|
+
const technique = TECHNIQUES[key];
|
|
1615
|
+
if (technique && technique.template && technique.template.path) {
|
|
1616
|
+
const tplPath = pathMod.join(options.dir, technique.template.path);
|
|
1617
|
+
if (fs.existsSync(tplPath)) {
|
|
1618
|
+
snapshotFiles[technique.template.path] = fs.readFileSync(tplPath, 'utf8');
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const rollbackArtifact = writeRollbackArtifact(options.dir, {
|
|
1623
|
+
sourcePlan: 'fix-batch',
|
|
1624
|
+
preSnapshot: snapshotFiles,
|
|
1625
|
+
createdFiles: [],
|
|
1626
|
+
patchedFiles: Object.keys(snapshotFiles),
|
|
1627
|
+
rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
|
|
1628
|
+
});
|
|
1629
|
+
rollbackId = rollbackArtifact.id;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Step 3b: Apply fixes sequentially with progress
|
|
1633
|
+
let fixed = 0;
|
|
1634
|
+
let manual = 0;
|
|
1635
|
+
let runningScore = preScore;
|
|
1636
|
+
|
|
1637
|
+
for (let i = 0; i < targetKeys.length; i++) {
|
|
1638
|
+
const key = targetKeys[i];
|
|
1639
|
+
const technique = TECHNIQUES[key];
|
|
1640
|
+
const failedCheck = failedResults.find(r => r.key === key);
|
|
1641
|
+
const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
|
|
1642
|
+
|
|
1643
|
+
if (technique && technique.template) {
|
|
1644
|
+
if (options.dryRun) {
|
|
1645
|
+
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1646
|
+
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1647
|
+
fixed++;
|
|
1648
|
+
} else {
|
|
1649
|
+
try {
|
|
1650
|
+
if (isBatch) console.log(` Fixing ${progress}${key}...`);
|
|
1651
|
+
const setupResult = await setup({ ...options, only: [key], silent: true });
|
|
1652
|
+
if (setupResult && setupResult.writtenFiles) {
|
|
1653
|
+
allCreatedFiles.push(...setupResult.writtenFiles);
|
|
1654
|
+
}
|
|
1655
|
+
const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1656
|
+
const delta = midResult.score - runningScore;
|
|
1657
|
+
fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
|
|
1658
|
+
runningScore = midResult.score;
|
|
1659
|
+
if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1660
|
+
fixed++;
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
|
|
1663
|
+
if (isBatch) {
|
|
1664
|
+
console.log(` ❌ Failed: ${key} — ${err.message}`);
|
|
1665
|
+
console.log(` Stopping batch. ${fixed} fixes applied so far.`);
|
|
1666
|
+
console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
|
|
1667
|
+
break;
|
|
1668
|
+
} else {
|
|
1669
|
+
console.log(` ❌ Failed: ${failedCheck.name} — ${err.message}`);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
} else if (INLINE_FIXERS[key]) {
|
|
1674
|
+
if (options.dryRun) {
|
|
1675
|
+
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1676
|
+
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1677
|
+
fixed++;
|
|
1678
|
+
} else {
|
|
1679
|
+
try {
|
|
1680
|
+
if (isBatch) console.log(` Fixing ${progress}${key}...`);
|
|
1681
|
+
const didFix = INLINE_FIXERS[key](options.dir);
|
|
1682
|
+
if (didFix) {
|
|
1683
|
+
const midResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1684
|
+
const delta = midResult.score - runningScore;
|
|
1685
|
+
fixResults.push({ key, name: failedCheck.name, status: 'fixed', delta });
|
|
1686
|
+
runningScore = midResult.score;
|
|
1687
|
+
if (!isBatch) console.log(` ✅ Fixed: ${failedCheck.name}`);
|
|
1688
|
+
fixed++;
|
|
1689
|
+
} else {
|
|
1690
|
+
fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
|
|
1691
|
+
if (!isBatch) console.log(` ⏭️ Already fixed: ${failedCheck.name}`);
|
|
1692
|
+
}
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
fixResults.push({ key, name: failedCheck.name, status: 'failed', delta: 0 });
|
|
1695
|
+
if (isBatch) {
|
|
1696
|
+
console.log(` ❌ Failed: ${key} — ${err.message}`);
|
|
1697
|
+
console.log(` Stopping batch. ${fixed} fixes applied so far.`);
|
|
1698
|
+
console.log(` Rollback: nerviq rollback --id ${rollbackId}`);
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
} else {
|
|
1704
|
+
if (!isBatch) {
|
|
1705
|
+
const aiPrompt = FIX_PROMPTS[key];
|
|
1706
|
+
if (aiPrompt) {
|
|
1707
|
+
console.log(formatFixPrompt(key, aiPrompt));
|
|
1708
|
+
} else {
|
|
1709
|
+
console.log(` 📋 ${failedCheck.name} (manual fix needed)`);
|
|
1710
|
+
console.log(` ${failedCheck.fix}`);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
fixResults.push({ key, name: failedCheck.name, status: 'skipped', delta: 0 });
|
|
1714
|
+
manual++;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Record accepted patterns for successfully fixed checks
|
|
1719
|
+
if (!options.dryRun) {
|
|
1720
|
+
for (const key of targetKeys) {
|
|
1721
|
+
const fr = fixResults.find(r => r.key === key);
|
|
1722
|
+
recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Update rollback artifact with actual created files
|
|
1727
|
+
if (!options.dryRun && rollbackId && allCreatedFiles.length > 0) {
|
|
1728
|
+
const { ensureArtifactDirs } = require('../src/activity');
|
|
1729
|
+
const { rollbackDir } = ensureArtifactDirs(options.dir);
|
|
1730
|
+
const rbFiles = fs.readdirSync(rollbackDir).filter(f => f.includes(rollbackId));
|
|
1731
|
+
if (rbFiles.length > 0) {
|
|
1732
|
+
const rbPath = pathMod.join(rollbackDir, rbFiles[0]);
|
|
1733
|
+
try {
|
|
1734
|
+
const rbData = JSON.parse(fs.readFileSync(rbPath, 'utf8'));
|
|
1735
|
+
rbData.createdFiles = allCreatedFiles;
|
|
1736
|
+
fs.writeFileSync(rbPath, JSON.stringify(rbData, null, 2), 'utf8');
|
|
1737
|
+
} catch { /* best effort */ }
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Step 4: Show batch summary or simple score impact
|
|
1742
|
+
if (isBatch && fixResults.length > 0) {
|
|
1743
|
+
console.log('');
|
|
1744
|
+
console.log(' Batch fix complete:');
|
|
1745
|
+
for (let i = 0; i < fixResults.length; i++) {
|
|
1746
|
+
const r = fixResults[i];
|
|
1747
|
+
const icon = r.status === 'fixed' ? '✅' : r.status === 'failed' ? '❌' : '⚠ ';
|
|
1748
|
+
const deltaStr = r.status === 'fixed' ? ` (+${r.delta})` : r.status === 'skipped' ? ' (skipped — no auto-fix)' : r.status === 'failed' ? ' (failed)' : ' (dry-run)';
|
|
1749
|
+
console.log(` ${icon} ${i + 1}. ${r.key.padEnd(20)}${deltaStr}`);
|
|
1750
|
+
}
|
|
1751
|
+
const totalDelta = runningScore - preScore;
|
|
1752
|
+
console.log('');
|
|
1753
|
+
console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
|
|
1754
|
+
if (rollbackId && !options.dryRun) {
|
|
1755
|
+
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1756
|
+
}
|
|
1757
|
+
} else if (fixed > 0 && !options.dryRun) {
|
|
1758
|
+
const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1759
|
+
const delta = postResult.score - preScore;
|
|
1760
|
+
console.log('');
|
|
1761
|
+
console.log(` Score: ${preScore} → ${postResult.score} (${delta >= 0 ? '+' : ''}${delta})`);
|
|
1762
|
+
if (rollbackId) {
|
|
1763
|
+
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
console.log(`\n ${fixed} fixed, ${manual} need manual action.\n`);
|
|
1768
|
+
|
|
1769
|
+
} else if (normalizedCommand === 'init') {
|
|
1770
|
+
const { runInit } = require('../src/init');
|
|
1771
|
+
await runInit(options.dir, flags);
|
|
1772
|
+
process.exit(0);
|
|
1773
|
+
} else if (normalizedCommand === 'setup') {
|
|
1774
|
+
await setup(options);
|
|
1775
|
+
if (options.snapshot) {
|
|
1776
|
+
const postSetupResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1777
|
+
const snapshot = writeSnapshotArtifact(options.dir, 'audit', postSetupResult, {
|
|
1778
|
+
sourceCommand: 'setup',
|
|
1779
|
+
});
|
|
1780
|
+
if (!options.json) {
|
|
1781
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
} else {
|
|
1785
|
+
if (options.workspace) {
|
|
1786
|
+
const summary = await auditWorkspaces(options.dir, options.workspace, options.platform);
|
|
1787
|
+
printWorkspaceSummary(summary, options);
|
|
1788
|
+
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
process.exit(0);
|
|
1792
|
+
}
|
|
1793
|
+
const result = await audit(options);
|
|
1794
|
+
if (options.webhookUrl) {
|
|
1795
|
+
try {
|
|
1796
|
+
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
|
1797
|
+
// Auto-detect Slack vs generic by URL pattern
|
|
1798
|
+
const isSlack = options.webhookUrl.includes('hooks.slack.com');
|
|
1799
|
+
const isDiscord = options.webhookUrl.includes('discord.com/api/webhooks');
|
|
1800
|
+
let payload;
|
|
1801
|
+
if (isSlack) {
|
|
1802
|
+
payload = formatSlackMessage(result);
|
|
1803
|
+
} else if (isDiscord) {
|
|
1804
|
+
const { formatDiscordMessage } = require('../src/integrations');
|
|
1805
|
+
payload = formatDiscordMessage(result);
|
|
1806
|
+
} else {
|
|
1807
|
+
// Generic webhook: send full JSON audit result
|
|
1808
|
+
payload = { platform: result.platform, score: result.score, passed: result.passed, failed: result.failed, results: result.results };
|
|
1809
|
+
}
|
|
1810
|
+
const webhookResp = await sendWebhook(options.webhookUrl, payload);
|
|
1811
|
+
if (!options.json) {
|
|
1812
|
+
if (webhookResp.ok) {
|
|
1813
|
+
console.log(` Webhook sent: ${options.webhookUrl} (${webhookResp.status})`);
|
|
1814
|
+
} else {
|
|
1815
|
+
console.error(` Webhook failed: ${webhookResp.status} — ${webhookResp.body.slice(0, 200)}`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
} catch (webhookErr) {
|
|
1819
|
+
if (!options.json) console.error(` Webhook error: ${webhookErr.message}`);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (options.feedback && !options.json && options.format === null) {
|
|
1823
|
+
const feedbackTargets = options.lite
|
|
1824
|
+
? (result.liteSummary?.topNextActions || [])
|
|
1825
|
+
: (result.topNextActions || []);
|
|
1826
|
+
const feedbackResult = await collectFeedback(options.dir, {
|
|
1827
|
+
findings: feedbackTargets,
|
|
1828
|
+
platform: result.platform,
|
|
1829
|
+
sourceCommand: normalizedCommand,
|
|
1830
|
+
score: result.score,
|
|
1831
|
+
});
|
|
1832
|
+
if (feedbackResult.mode === 'skipped-noninteractive') {
|
|
1833
|
+
console.log(' Feedback prompt skipped: interactive terminal required.');
|
|
1834
|
+
console.log('');
|
|
1835
|
+
} else if (feedbackResult.saved > 0) {
|
|
1836
|
+
console.log(` Feedback saved: ${feedbackResult.relativeDir}`);
|
|
1837
|
+
console.log(` Helpful: ${feedbackResult.helpful} | Not helpful: ${feedbackResult.unhelpful}`);
|
|
1838
|
+
console.log('');
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
const snapshot = options.snapshot ? writeSnapshotArtifact(options.dir, 'audit', result, {
|
|
1842
|
+
sourceCommand: normalizedCommand,
|
|
1843
|
+
}) : null;
|
|
1844
|
+
if (snapshot && !options.json) {
|
|
1845
|
+
console.log(` Snapshot saved: ${snapshot.relativePath}`);
|
|
1846
|
+
console.log(` Snapshot index: ${snapshot.indexPath}`);
|
|
1847
|
+
console.log('');
|
|
1848
|
+
}
|
|
1849
|
+
if (options.threshold !== null && result.score < options.threshold) {
|
|
1850
|
+
if (!options.json) {
|
|
1851
|
+
console.error(`\n Error: Threshold not met — score ${result.score}/100 is below required ${options.threshold}/100.`);
|
|
1852
|
+
console.error(' Why: Your project audit score is lower than the minimum threshold set via --threshold.');
|
|
1853
|
+
console.error(' Fix: Run `npx nerviq augment` to see improvement suggestions, then re-audit.');
|
|
1854
|
+
console.error(' Docs: https://github.com/nerviq/nerviq#ci-integration\n');
|
|
1855
|
+
}
|
|
1856
|
+
process.exit(1);
|
|
1857
|
+
}
|
|
1858
|
+
if (options.require && options.require.length > 0) {
|
|
1859
|
+
const failedRequired = options.require.filter(key => {
|
|
1860
|
+
const check = result.results.find(r => r.key === key);
|
|
1861
|
+
return !check || check.passed !== true;
|
|
1862
|
+
});
|
|
1863
|
+
if (failedRequired.length > 0) {
|
|
1864
|
+
if (!options.json) {
|
|
1865
|
+
console.error(`\n Required checks failed: ${failedRequired.join(', ')}`);
|
|
1866
|
+
console.error(' These must pass for CI to succeed.\n');
|
|
1867
|
+
}
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
console.error(`\n Error: ${err.message}`);
|
|
1874
|
+
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
main();
|