@nerviq/cli 1.8.3 → 1.8.4

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