@nerviq/cli 1.8.6 → 1.8.8

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
@@ -285,6 +285,53 @@ function printWorkspaceSummary(summary, options) {
285
285
  console.log('');
286
286
  }
287
287
 
288
+ function printScanDetail(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 scan — per-repo comparison\x1b[0m');
296
+ console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
297
+ console.log(` Platform: ${summary.platform} | Repos: ${summary.repoCount} | Average: \x1b[1m${summary.averageScore}/100\x1b[0m`);
298
+ console.log('');
299
+
300
+ for (const item of summary.repos) {
301
+ if (item.error) {
302
+ console.log(` \x1b[31m✗ ${item.name}\x1b[0m — ${item.error}`);
303
+ console.log('');
304
+ continue;
305
+ }
306
+ const scoreColor = item.score >= 80 ? '\x1b[32m' : item.score >= 50 ? '\x1b[33m' : '\x1b[31m';
307
+ console.log(` \x1b[1m${item.name}\x1b[0m ${scoreColor}${item.score}/100\x1b[0m (${item.passed}/${item.total} checks passed)`);
308
+
309
+ // Show per-category breakdown if result is available
310
+ if (item.result && item.result.results) {
311
+ const categories = {};
312
+ for (const r of item.result.results) {
313
+ const cat = r.category || 'other';
314
+ if (!categories[cat]) categories[cat] = { passed: 0, total: 0 };
315
+ categories[cat].total++;
316
+ if (r.passed) categories[cat].passed++;
317
+ }
318
+ const catEntries = Object.entries(categories).sort((a, b) => (a[1].passed / a[1].total) - (b[1].passed / b[1].total));
319
+ const catLine = catEntries.map(([cat, v]) => `${cat}: ${v.passed}/${v.total}`).join(' ');
320
+ console.log(` \x1b[2m${catLine}\x1b[0m`);
321
+ }
322
+
323
+ // Show top 3 gaps
324
+ if (item.result && item.result.topNextActions && item.result.topNextActions.length > 0) {
325
+ const gaps = item.result.topNextActions.slice(0, 3);
326
+ console.log(' Top gaps:');
327
+ for (const gap of gaps) {
328
+ console.log(` \x1b[33m→\x1b[0m ${gap.name || gap.key}${gap.impact ? ` \x1b[2m(+${gap.impact})\x1b[0m` : ''}`);
329
+ }
330
+ }
331
+ console.log('');
332
+ }
333
+ }
334
+
288
335
  function printOrgSummary(summary, options) {
289
336
  if (options.json) {
290
337
  console.log(JSON.stringify(summary, null, 2));
@@ -525,6 +572,21 @@ async function main() {
525
572
  }
526
573
  }
527
574
 
575
+ // Apply built-in governance profile (--profile flag) to audit options
576
+ if (parsed.profile && parsed.profile !== 'safe-write') {
577
+ const { getPermissionProfile } = require('../src/governance');
578
+ const govProfile = getPermissionProfile(parsed.profile);
579
+ if (govProfile) {
580
+ options.governanceProfile = govProfile;
581
+ if (govProfile.deny && govProfile.deny.length > 0) {
582
+ options.suppressedChecks = options.suppressedChecks || [];
583
+ }
584
+ if (!options.json) {
585
+ console.log(` Using governance profile: ${govProfile.label} (${govProfile.risk} risk)`);
586
+ }
587
+ }
588
+ }
589
+
528
590
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
529
591
  if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
530
592
  console.error(`\n Error: Unsupported platform '${options.platform}'.`);
@@ -595,7 +657,7 @@ async function main() {
595
657
  // Harmony + Synergy (cross-platform)
596
658
  'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
597
659
  'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
598
- 'freshness', 'profile',
660
+ 'freshness', 'profile', 'migrate',
599
661
  ]);
600
662
 
601
663
  if (options.platform === 'codex') {
@@ -648,7 +710,7 @@ async function main() {
648
710
  process.exit(1);
649
711
  }
650
712
  const summary = await scanOrg(scanDirs, options.platform);
651
- printOrgSummary(summary, options);
713
+ printScanDetail(summary, options);
652
714
  if (options.threshold !== null && summary.averageScore < options.threshold) {
653
715
  process.exit(1);
654
716
  }
@@ -740,7 +802,7 @@ async function main() {
740
802
  process.exit(0);
741
803
  } else if (normalizedCommand === 'insights') {
742
804
  const https = require('https');
743
- const url = 'https://claudex-insights.claudex.workers.dev/v1/stats';
805
+ const url = 'https://nerviq-insights.nerviq.workers.dev/v1/stats';
744
806
  const req = https.get(url, (res) => {
745
807
  let data = '';
746
808
  res.on('data', chunk => data += chunk);
@@ -748,7 +810,7 @@ async function main() {
748
810
  try {
749
811
  const stats = JSON.parse(data);
750
812
  console.log('');
751
- console.log('\x1b[1m CLAUDEX Community Insights\x1b[0m');
813
+ console.log('\x1b[1m NERVIQ Community Insights\x1b[0m');
752
814
  console.log('\x1b[2m ═══════════════════════════════════════\x1b[0m');
753
815
  console.log(` Total audits run: \x1b[1m${stats.totalRuns}\x1b[0m`);
754
816
  console.log(` Average score: \x1b[1m${stats.averageScore}/100\x1b[0m`);
@@ -1341,11 +1403,39 @@ async function main() {
1341
1403
  process.exit(1);
1342
1404
  }
1343
1405
  const profile = loadProfile(options.dir, profileArg);
1406
+
1407
+ // Apply profile settings to .claude/settings.json
1408
+ const fs = require('fs');
1409
+ const settingsPath = require('path').join(options.dir, '.claude', 'settings.json');
1410
+ let settings = {};
1411
+ if (fs.existsSync(settingsPath)) {
1412
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
1413
+ }
1414
+ // Apply deny rules from governance profile if platforms include claude
1415
+ if (profile.platforms && profile.platforms.includes('claude')) {
1416
+ const { getPermissionProfile } = require('../src/governance');
1417
+ const govProfile = getPermissionProfile(profileArg);
1418
+ if (govProfile && govProfile.deny && govProfile.deny.length > 0) {
1419
+ settings.deny = govProfile.deny;
1420
+ }
1421
+ }
1422
+ // Apply threshold and suppressed checks
1423
+ if (profile.threshold != null) {
1424
+ settings.threshold = profile.threshold;
1425
+ }
1426
+ if (profile.suppressedChecks && profile.suppressedChecks.length > 0) {
1427
+ settings.suppressedChecks = profile.suppressedChecks;
1428
+ }
1429
+ const settingsDir = require('path').dirname(settingsPath);
1430
+ fs.mkdirSync(settingsDir, { recursive: true });
1431
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
1432
+
1344
1433
  if (options.json) {
1345
1434
  console.log(JSON.stringify(profile, null, 2));
1346
1435
  } else {
1347
1436
  console.log('');
1348
1437
  console.log(formatProfile(profile));
1438
+ console.log(`\n Settings applied to ${settingsPath}`);
1349
1439
  console.log('');
1350
1440
  }
1351
1441
  process.exit(0);
@@ -1404,7 +1494,8 @@ async function main() {
1404
1494
  const fixKey = parsed.extraArgs[0] || null;
1405
1495
  const allCritical = flags.includes('--all-critical');
1406
1496
  const promptOnly = flags.includes('--prompt');
1407
- const autoApply = options.auto || options.dryRun;
1497
+ const autoApply = options.auto;
1498
+ const isDryRun = options.dryRun;
1408
1499
 
1409
1500
  // Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
1410
1501
  const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
@@ -1447,6 +1538,13 @@ async function main() {
1447
1538
  for (const entry of denyEntries) {
1448
1539
  if (!settings.permissions.deny.includes(entry)) settings.permissions.deny.push(entry);
1449
1540
  }
1541
+ // Remove overly broad allow:["*"] if present
1542
+ if (Array.isArray(settings.permissions.allow) && settings.permissions.allow.includes('*')) {
1543
+ settings.permissions.allow = settings.permissions.allow.filter(a => a !== '*');
1544
+ if (settings.permissions.allow.length === 0) {
1545
+ delete settings.permissions.allow;
1546
+ }
1547
+ }
1450
1548
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
1451
1549
  return true;
1452
1550
  },
@@ -1558,7 +1656,7 @@ async function main() {
1558
1656
  const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
1559
1657
  const predictedDelta = predictedScore - preScore;
1560
1658
 
1561
- if (!autoApply) {
1659
+ if (!autoApply && !isDryRun) {
1562
1660
  console.log('');
1563
1661
  if (allCritical && fixableTargets.length > 1) {
1564
1662
  // Multi-fix summary
@@ -1607,9 +1705,9 @@ async function main() {
1607
1705
  const allCreatedFiles = [];
1608
1706
  const fixResults = []; // { key, name, status, delta }
1609
1707
 
1610
- if (!options.dryRun && targetKeys.length > 0) {
1611
- // Snapshot existing files for rollback
1612
- const snapshotFiles = {};
1708
+ const snapshotFiles = {};
1709
+ if (!isDryRun && targetKeys.length > 0) {
1710
+ // Snapshot existing files for rollback (before applying fixes)
1613
1711
  for (const key of targetKeys) {
1614
1712
  const technique = TECHNIQUES[key];
1615
1713
  if (technique && technique.template && technique.template.path) {
@@ -1619,14 +1717,6 @@ async function main() {
1619
1717
  }
1620
1718
  }
1621
1719
  }
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
1720
  }
1631
1721
 
1632
1722
  // Step 3b: Apply fixes sequentially with progress
@@ -1641,7 +1731,7 @@ async function main() {
1641
1731
  const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
1642
1732
 
1643
1733
  if (technique && technique.template) {
1644
- if (options.dryRun) {
1734
+ if (isDryRun) {
1645
1735
  console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
1646
1736
  fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
1647
1737
  fixed++;
@@ -1671,7 +1761,7 @@ async function main() {
1671
1761
  }
1672
1762
  }
1673
1763
  } else if (INLINE_FIXERS[key]) {
1674
- if (options.dryRun) {
1764
+ if (isDryRun) {
1675
1765
  console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
1676
1766
  fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
1677
1767
  fixed++;
@@ -1716,26 +1806,33 @@ async function main() {
1716
1806
  }
1717
1807
 
1718
1808
  // Record accepted patterns for successfully fixed checks
1719
- if (!options.dryRun) {
1809
+ if (!isDryRun) {
1720
1810
  for (const key of targetKeys) {
1721
1811
  const fr = fixResults.find(r => r.key === key);
1722
1812
  recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
1723
1813
  }
1724
1814
  }
1725
1815
 
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 */ }
1816
+ // Write rollback artifact AFTER fixes are applied (with actual file lists)
1817
+ if (!isDryRun && targetKeys.length > 0 && fixed > 0) {
1818
+ const allPatchedFiles = Object.keys(snapshotFiles);
1819
+ // Also track inline-fixer patched files
1820
+ for (const fr of fixResults) {
1821
+ if (fr.status === 'fixed' && INLINE_FIXERS[fr.key]) {
1822
+ const inlinePath = fr.key === 'gitIgnoreEnv' ? '.gitignore' : fr.key === 'secretsProtection' ? '.claude/settings.json' : null;
1823
+ if (inlinePath && !allPatchedFiles.includes(inlinePath)) {
1824
+ allPatchedFiles.push(inlinePath);
1825
+ }
1826
+ }
1738
1827
  }
1828
+ const rollbackArtifact = writeRollbackArtifact(options.dir, {
1829
+ sourcePlan: 'fix-batch',
1830
+ preSnapshot: snapshotFiles,
1831
+ createdFiles: allCreatedFiles,
1832
+ patchedFiles: allPatchedFiles,
1833
+ rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
1834
+ });
1835
+ rollbackId = rollbackArtifact.id;
1739
1836
  }
1740
1837
 
1741
1838
  // Step 4: Show batch summary or simple score impact
@@ -1751,10 +1848,10 @@ async function main() {
1751
1848
  const totalDelta = runningScore - preScore;
1752
1849
  console.log('');
1753
1850
  console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
1754
- if (rollbackId && !options.dryRun) {
1851
+ if (rollbackId && !isDryRun) {
1755
1852
  console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
1756
1853
  }
1757
- } else if (fixed > 0 && !options.dryRun) {
1854
+ } else if (fixed > 0 && !isDryRun) {
1758
1855
  const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
1759
1856
  const delta = postResult.score - preScore;
1760
1857
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.8.6",
3
+ "version": "1.8.8",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,431 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {