@nerviq/cli 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -26,7 +26,7 @@ const COMMAND_ALIASES = {
26
26
  gov: 'governance',
27
27
  outcome: 'feedback',
28
28
  };
29
- const KNOWN_COMMANDS = ['audit', 'org', 'setup', 'augment', 'suggest-only', 'plan', 'apply', 'fix', 'governance', 'benchmark', 'deep-review', 'interactive', 'watch', 'badge', 'insights', 'history', 'compare', 'trend', 'scan', 'feedback', 'doctor', 'convert', 'migrate', 'catalog', 'certify', 'serve', 'check-health', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
29
+ const KNOWN_COMMANDS = ['audit', 'org', 'setup', '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', 'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise', 'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export', 'freshness', 'help', 'version'];
30
30
 
31
31
  function levenshtein(a, b) {
32
32
  const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
@@ -313,6 +313,9 @@ const HELP = `
313
313
  nerviq fix <key> Auto-fix a specific check (with score impact)
314
314
  nerviq fix --all-critical Fix all critical issues at once
315
315
  nerviq fix --dry-run Preview fixes without writing
316
+ nerviq rollback Undo the most recent apply (delete created files)
317
+ nerviq rollback --list Show available rollback points
318
+ nerviq rollback --dry-run Preview what would be deleted
316
319
 
317
320
  IMPROVE
318
321
  nerviq augment Improvement plan (no writes)
@@ -609,7 +612,30 @@ async function main() {
609
612
  }
610
613
  process.exit(0);
611
614
  } else if (normalizedCommand === 'history') {
612
- const { formatHistory } = require('../src/activity');
615
+ const { formatHistory, readSnapshotIndex } = require('../src/activity');
616
+ // Handle --prune N
617
+ const pruneIdx = flags.indexOf('--prune');
618
+ if (pruneIdx >= 0) {
619
+ const keepCount = parseInt(flags[pruneIdx + 1] || parsed.extraArgs[0], 10) || 10;
620
+ const fsMod = require('fs');
621
+ const pathMod = require('path');
622
+ const entries = readSnapshotIndex(options.dir);
623
+ if (entries.length <= keepCount) {
624
+ console.log(`\n Nothing to prune (${entries.length} snapshots, keeping ${keepCount}).\n`);
625
+ } else {
626
+ const toRemove = entries.slice(0, entries.length - keepCount);
627
+ let removed = 0;
628
+ for (const entry of toRemove) {
629
+ const fp = pathMod.join(options.dir, entry.relativePath);
630
+ try { fsMod.unlinkSync(fp); removed++; } catch {}
631
+ }
632
+ const kept = entries.slice(entries.length - keepCount);
633
+ const indexPath = pathMod.join(options.dir, '.nerviq', 'snapshots', 'index.json');
634
+ try { fsMod.writeFileSync(indexPath, JSON.stringify(kept, null, 2), 'utf8'); } catch {}
635
+ console.log(`\n Pruned ${removed} snapshots, kept ${kept.length}.\n`);
636
+ }
637
+ process.exit(0);
638
+ }
613
639
  console.log('');
614
640
  console.log(formatHistory(options.dir));
615
641
  console.log('');
@@ -761,6 +787,88 @@ async function main() {
761
787
  }
762
788
  console.log('');
763
789
  }
790
+ } else if (normalizedCommand === 'rollback') {
791
+ const fsMod = require('fs');
792
+ const pathMod = require('path');
793
+ const rollbackDir = pathMod.join(options.dir, '.nerviq', 'rollbacks');
794
+
795
+ if (!fsMod.existsSync(rollbackDir)) {
796
+ console.log('\n No rollback artifacts found. Run `nerviq apply` first to create rollback data.\n');
797
+ process.exit(0);
798
+ }
799
+
800
+ const rollbackFiles = fsMod.readdirSync(rollbackDir)
801
+ .filter(f => f.endsWith('.json'))
802
+ .sort()
803
+ .reverse();
804
+
805
+ if (rollbackFiles.length === 0) {
806
+ console.log('\n No rollback artifacts found.\n');
807
+ process.exit(0);
808
+ }
809
+
810
+ // --list mode
811
+ if (flags.includes('--list')) {
812
+ console.log(`\n Rollback points (${rollbackFiles.length}):\n`);
813
+ for (const f of rollbackFiles) {
814
+ try {
815
+ const data = JSON.parse(fsMod.readFileSync(pathMod.join(rollbackDir, f), 'utf8'));
816
+ const created = (data.createdFiles || []).length;
817
+ const patched = (data.patchedFiles || []).length;
818
+ console.log(` ${f.replace('.json', '')} (${created} created, ${patched} patched)`);
819
+ } catch {
820
+ console.log(` ${f} (unreadable)`);
821
+ }
822
+ }
823
+ console.log(`\n Run \`nerviq rollback\` to undo the most recent.\n`);
824
+ process.exit(0);
825
+ }
826
+
827
+ // Execute rollback of most recent
828
+ const latestFile = rollbackFiles[0];
829
+ const latestPath = pathMod.join(rollbackDir, latestFile);
830
+ let rollbackData;
831
+ try {
832
+ rollbackData = JSON.parse(fsMod.readFileSync(latestPath, 'utf8'));
833
+ } catch (e) {
834
+ console.error(`\n Error: Cannot parse rollback file: ${e.message}\n`);
835
+ process.exit(1);
836
+ }
837
+
838
+ const createdFiles = rollbackData.createdFiles || [];
839
+ if (createdFiles.length === 0) {
840
+ console.log('\n Rollback artifact has no files to remove.\n');
841
+ process.exit(0);
842
+ }
843
+
844
+ if (options.dryRun) {
845
+ console.log(`\n [dry-run] Would delete ${createdFiles.length} files:\n`);
846
+ for (const f of createdFiles) {
847
+ console.log(` - ${f}`);
848
+ }
849
+ console.log('');
850
+ process.exit(0);
851
+ }
852
+
853
+ let deleted = 0;
854
+ let missing = 0;
855
+ console.log('');
856
+ for (const relPath of createdFiles) {
857
+ const fullPath = pathMod.join(options.dir, relPath);
858
+ if (fsMod.existsSync(fullPath)) {
859
+ fsMod.unlinkSync(fullPath);
860
+ console.log(` 🗑️ Deleted: ${relPath}`);
861
+ deleted++;
862
+ } else {
863
+ missing++;
864
+ }
865
+ }
866
+
867
+ // Remove rollback artifact after use
868
+ fsMod.unlinkSync(latestPath);
869
+
870
+ console.log(`\n Rollback complete: ${deleted} files deleted${missing > 0 ? `, ${missing} already missing` : ''}.\n`);
871
+
764
872
  } else if (normalizedCommand === 'apply') {
765
873
  if (flags.includes('--rollback')) {
766
874
  console.error('\n Error: --rollback is not yet supported as a flag.');
@@ -1037,16 +1145,24 @@ async function main() {
1037
1145
  } else if (normalizedCommand === 'rules-export') {
1038
1146
  const { generateRecommendationRules } = require('../src/recommendation-rules');
1039
1147
  const rules = generateRecommendationRules();
1040
- const output = JSON.stringify(rules, null, 2);
1041
- if (options.out) {
1042
- require('fs').writeFileSync(options.out, output, 'utf8');
1043
- if (!options.json) {
1044
- console.log(`\n Rules exported to ${options.out} (${rules.totalRules} rules)\n`);
1045
- } else {
1046
- console.log(output);
1047
- }
1148
+ if (options.json) {
1149
+ console.log(JSON.stringify(rules, null, 2));
1150
+ } else if (options.out) {
1151
+ require('fs').writeFileSync(options.out, JSON.stringify(rules, null, 2), 'utf8');
1152
+ console.log(`\n Rules exported to ${options.out} (${rules.totalRules} rules)\n`);
1048
1153
  } else {
1049
- console.log(output);
1154
+ // Human-readable summary
1155
+ console.log(`\n Nerviq Recommendation Rules (${rules.totalRules} rules)\n`);
1156
+ const byCategory = {};
1157
+ for (const rule of (rules.rules || [])) {
1158
+ const cat = rule.category || 'other';
1159
+ if (!byCategory[cat]) byCategory[cat] = 0;
1160
+ byCategory[cat]++;
1161
+ }
1162
+ for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
1163
+ console.log(` ${cat.padEnd(20)} ${count} rules`);
1164
+ }
1165
+ console.log(`\n Use --json for full output or --out <file> to save.\n`);
1050
1166
  }
1051
1167
  process.exit(0);
1052
1168
  } else if (normalizedCommand === 'check-health') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.6.4",
3
+ "version": "1.7.0",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/benchmark.js CHANGED
@@ -145,7 +145,9 @@ function buildExecutiveSummary(before, after, workflowEvidence) {
145
145
  const scoreDelta = after.score - before.score;
146
146
  const organicDelta = after.organicScore - before.organicScore;
147
147
  const workflowCoverage = workflowEvidence.summary.coverageScore;
148
- let headline = 'Benchmark did not improve the score in this run.';
148
+ let headline = before.score >= 60
149
+ ? 'Setup is already applied — benchmark shows no additional improvement. Run benchmark on a project before running setup to see the full delta.'
150
+ : 'Benchmark did not improve the score in this run.';
149
151
 
150
152
  if (scoreDelta < 0) {
151
153
  headline = `Warning: score decreased by ${Math.abs(scoreDelta)} points. Setup may have introduced a regression.`;
package/src/governance.js CHANGED
@@ -433,22 +433,35 @@ function printGovernanceSummary(summary, options = {}) {
433
433
  }
434
434
  console.log('');
435
435
 
436
- console.log(' Domain Packs');
437
- if ((summary.domainPacks || []).length === 0) {
436
+ const domainPacks = summary.domainPacks || [];
437
+ const mcpPacks = summary.mcpPacks || [];
438
+ const compact = !options.verbose;
439
+ const COMPACT_LIMIT = 5;
440
+
441
+ console.log(` Domain Packs (${domainPacks.length})`);
442
+ if (domainPacks.length === 0) {
438
443
  console.log(' - none shipped yet for this platform');
439
444
  }
440
- for (const pack of summary.domainPacks || []) {
445
+ const domainShow = compact ? domainPacks.slice(0, COMPACT_LIMIT) : domainPacks;
446
+ for (const pack of domainShow) {
441
447
  console.log(` - ${pack.label}: ${pack.useWhen}`);
442
448
  }
449
+ if (compact && domainPacks.length > COMPACT_LIMIT) {
450
+ console.log(` ... and ${domainPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
451
+ }
443
452
  console.log('');
444
453
 
445
- console.log(' MCP Packs');
446
- if ((summary.mcpPacks || []).length === 0) {
454
+ console.log(` MCP Packs (${mcpPacks.length})`);
455
+ if (mcpPacks.length === 0) {
447
456
  console.log(' - none shipped yet for this platform');
448
457
  }
449
- for (const pack of summary.mcpPacks || []) {
458
+ const mcpShow = compact ? mcpPacks.slice(0, COMPACT_LIMIT) : mcpPacks;
459
+ for (const pack of mcpShow) {
450
460
  console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
451
461
  }
462
+ if (compact && mcpPacks.length > COMPACT_LIMIT) {
463
+ console.log(` ... and ${mcpPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
464
+ }
452
465
  console.log('');
453
466
 
454
467
  if (Array.isArray(summary.platformCaveats) && summary.platformCaveats.length > 0) {
package/src/setup.js CHANGED
@@ -1022,7 +1022,7 @@ Prepare a release candidate for: $ARGUMENTS
1022
1022
  const hasTS = stacks.some(s => s.key === 'typescript');
1023
1023
  const hasPython = stacks.some(s => s.key === 'python');
1024
1024
  const hasFrontend = stacks.some(s => ['react', 'vue', 'angular', 'svelte', 'nextjs'].includes(s.key));
1025
- const hasBackend = stacks.some(s => ['go', 'python', 'django', 'fastapi', 'rust', 'java'].includes(s.key));
1025
+ const hasBackend = stacks.some(s => ['go', 'python', 'django', 'fastapi', 'rust', 'java', 'node', 'nestjs'].includes(s.key));
1026
1026
 
1027
1027
  if (hasFrontend || (hasTS && !hasBackend)) {
1028
1028
  rules['frontend.md'] = `When editing JavaScript/TypeScript files (*.ts, *.tsx, *.js, *.jsx, *.vue):