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