@nerviq/cli 1.8.7 → 1.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -13
- package/bin/cli.js +186 -35
- package/package.json +1 -1
- package/src/benchmark.js +7 -3
- package/src/certification.js +32 -4
- package/src/context.js +27 -1
- package/src/convert.js +6 -2
- package/src/governance.js +2 -2
- package/src/mcp-server.js +1 -1
- package/src/server.js +14 -5
- package/src/setup.js +102 -11
- package/src/techniques.js +6 -2
package/README.md
CHANGED
|
@@ -347,16 +347,3 @@ If Nerviq helped you, consider giving it a ⭐ on [GitHub](https://github.com/ne
|
|
|
347
347
|
| `BETA` | Works but has limited real-world testing. API may change |
|
|
348
348
|
| `EXPERIMENTAL` | Early stage, static rules, results may vary |
|
|
349
349
|
|
|
350
|
-
## Previously nerviq-cli
|
|
351
|
-
|
|
352
|
-
Nerviq was previously published as `nerviq-cli`. If you were using it:
|
|
353
|
-
|
|
354
|
-
```bash
|
|
355
|
-
# Old
|
|
356
|
-
npx nerviq-cli
|
|
357
|
-
|
|
358
|
-
# New
|
|
359
|
-
npx @nerviq/cli audit
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
All features are preserved and expanded.
|
package/bin/cli.js
CHANGED
|
@@ -285,6 +285,56 @@ 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 STACK_LANGUAGES = new Set(['python', 'go', 'rust', 'java', 'ruby', 'dotnet', 'php', 'flutter', 'swift', 'kotlin']);
|
|
312
|
+
const categories = {};
|
|
313
|
+
for (const r of item.result.results) {
|
|
314
|
+
const cat = r.category || 'other';
|
|
315
|
+
if (!categories[cat]) categories[cat] = { passed: 0, total: 0 };
|
|
316
|
+
categories[cat].total++;
|
|
317
|
+
if (r.passed) categories[cat].passed++;
|
|
318
|
+
}
|
|
319
|
+
const catEntries = Object.entries(categories)
|
|
320
|
+
.filter(([cat, v]) => v.passed > 0 || !STACK_LANGUAGES.has(cat))
|
|
321
|
+
.sort((a, b) => (a[1].passed / a[1].total) - (b[1].passed / b[1].total));
|
|
322
|
+
const catLine = catEntries.map(([cat, v]) => `${cat}: ${v.passed}/${v.total}`).join(' ');
|
|
323
|
+
console.log(` \x1b[2m${catLine}\x1b[0m`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Show top 3 gaps
|
|
327
|
+
if (item.result && item.result.topNextActions && item.result.topNextActions.length > 0) {
|
|
328
|
+
const gaps = item.result.topNextActions.slice(0, 3);
|
|
329
|
+
console.log(' Top gaps:');
|
|
330
|
+
for (const gap of gaps) {
|
|
331
|
+
console.log(` \x1b[33m→\x1b[0m ${gap.name || gap.key}${gap.impact ? ` \x1b[2m(+${gap.impact})\x1b[0m` : ''}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
console.log('');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
288
338
|
function printOrgSummary(summary, options) {
|
|
289
339
|
if (options.json) {
|
|
290
340
|
console.log(JSON.stringify(summary, null, 2));
|
|
@@ -525,6 +575,21 @@ async function main() {
|
|
|
525
575
|
}
|
|
526
576
|
}
|
|
527
577
|
|
|
578
|
+
// Apply built-in governance profile (--profile flag) to audit options
|
|
579
|
+
if (parsed.profile && parsed.profile !== 'safe-write') {
|
|
580
|
+
const { getPermissionProfile } = require('../src/governance');
|
|
581
|
+
const govProfile = getPermissionProfile(parsed.profile);
|
|
582
|
+
if (govProfile) {
|
|
583
|
+
options.governanceProfile = govProfile;
|
|
584
|
+
if (govProfile.deny && govProfile.deny.length > 0) {
|
|
585
|
+
options.suppressedChecks = options.suppressedChecks || [];
|
|
586
|
+
}
|
|
587
|
+
if (!options.json) {
|
|
588
|
+
console.log(` Using governance profile: ${govProfile.label} (${govProfile.risk} risk)`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
528
593
|
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'copilot', 'cursor', 'windsurf', 'aider', 'opencode'];
|
|
529
594
|
if (!SUPPORTED_PLATFORMS.includes(options.platform)) {
|
|
530
595
|
console.error(`\n Error: Unsupported platform '${options.platform}'.`);
|
|
@@ -595,7 +660,7 @@ async function main() {
|
|
|
595
660
|
// Harmony + Synergy (cross-platform)
|
|
596
661
|
'harmony-audit', 'harmony-sync', 'harmony-drift', 'harmony-advise',
|
|
597
662
|
'harmony-watch', 'harmony-governance', 'harmony-add', 'synergy-report', 'anti-patterns', 'rules-export',
|
|
598
|
-
'freshness', 'profile',
|
|
663
|
+
'freshness', 'profile', 'migrate',
|
|
599
664
|
]);
|
|
600
665
|
|
|
601
666
|
if (options.platform === 'codex') {
|
|
@@ -648,7 +713,7 @@ async function main() {
|
|
|
648
713
|
process.exit(1);
|
|
649
714
|
}
|
|
650
715
|
const summary = await scanOrg(scanDirs, options.platform);
|
|
651
|
-
|
|
716
|
+
printScanDetail(summary, options);
|
|
652
717
|
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
653
718
|
process.exit(1);
|
|
654
719
|
}
|
|
@@ -1340,12 +1405,54 @@ async function main() {
|
|
|
1340
1405
|
console.error('\n Error: Profile name required. Usage: nerviq profile load <name>\n');
|
|
1341
1406
|
process.exit(1);
|
|
1342
1407
|
}
|
|
1343
|
-
|
|
1408
|
+
let profile;
|
|
1409
|
+
try {
|
|
1410
|
+
profile = loadProfile(options.dir, profileArg);
|
|
1411
|
+
} catch {
|
|
1412
|
+
// Not found as a user-saved profile — try built-in governance profiles
|
|
1413
|
+
const { getPermissionProfile } = require('../src/governance');
|
|
1414
|
+
const builtIn = getPermissionProfile(profileArg);
|
|
1415
|
+
if (builtIn && builtIn.key === profileArg) {
|
|
1416
|
+
profile = { name: builtIn.label, platforms: ['claude'], threshold: builtIn.threshold || 0, ...builtIn };
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (!profile) {
|
|
1420
|
+
console.error(`\n Error: Profile '${profileArg}' not found. Run 'nerviq profile list' to see available profiles.\n`);
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Apply profile settings to .claude/settings.json
|
|
1425
|
+
const fs = require('fs');
|
|
1426
|
+
const settingsPath = require('path').join(options.dir, '.claude', 'settings.json');
|
|
1427
|
+
let settings = {};
|
|
1428
|
+
if (fs.existsSync(settingsPath)) {
|
|
1429
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch {}
|
|
1430
|
+
}
|
|
1431
|
+
// Apply deny rules from governance profile if platforms include claude
|
|
1432
|
+
if (profile.platforms && profile.platforms.includes('claude')) {
|
|
1433
|
+
const { getPermissionProfile } = require('../src/governance');
|
|
1434
|
+
const govProfile = getPermissionProfile(profileArg);
|
|
1435
|
+
if (govProfile && govProfile.deny && govProfile.deny.length > 0) {
|
|
1436
|
+
settings.deny = govProfile.deny;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
// Apply threshold and suppressed checks
|
|
1440
|
+
if (profile.threshold != null) {
|
|
1441
|
+
settings.threshold = profile.threshold;
|
|
1442
|
+
}
|
|
1443
|
+
if (profile.suppressedChecks && profile.suppressedChecks.length > 0) {
|
|
1444
|
+
settings.suppressedChecks = profile.suppressedChecks;
|
|
1445
|
+
}
|
|
1446
|
+
const settingsDir = require('path').dirname(settingsPath);
|
|
1447
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
1448
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
1449
|
+
|
|
1344
1450
|
if (options.json) {
|
|
1345
1451
|
console.log(JSON.stringify(profile, null, 2));
|
|
1346
1452
|
} else {
|
|
1347
1453
|
console.log('');
|
|
1348
1454
|
console.log(formatProfile(profile));
|
|
1455
|
+
console.log(`\n Settings applied to ${settingsPath}`);
|
|
1349
1456
|
console.log('');
|
|
1350
1457
|
}
|
|
1351
1458
|
process.exit(0);
|
|
@@ -1368,8 +1475,35 @@ async function main() {
|
|
|
1368
1475
|
process.exit(1);
|
|
1369
1476
|
}
|
|
1370
1477
|
} else if (normalizedCommand === 'synergy-report') {
|
|
1371
|
-
|
|
1372
|
-
|
|
1478
|
+
const { formatSynergyReport } = require('../src/synergy/report');
|
|
1479
|
+
const { detectActivePlatforms: detectSynergyPlatforms } = require('../src/harmony/canon');
|
|
1480
|
+
const presentPlatforms = detectSynergyPlatforms(options.dir).map(p => p.platform);
|
|
1481
|
+
if (presentPlatforms.length === 0) {
|
|
1482
|
+
console.log('\n No platform configurations detected.');
|
|
1483
|
+
console.log(' Run "nerviq harmony-audit" first, or "nerviq setup" to bootstrap a platform.\n');
|
|
1484
|
+
process.exit(0);
|
|
1485
|
+
}
|
|
1486
|
+
const platformAudits = {};
|
|
1487
|
+
const activePlatforms = [];
|
|
1488
|
+
for (const plat of presentPlatforms) {
|
|
1489
|
+
try {
|
|
1490
|
+
const result = await audit({ dir: options.dir, silent: true, platform: plat });
|
|
1491
|
+
if (result && typeof result.score === 'number') {
|
|
1492
|
+
platformAudits[plat] = result;
|
|
1493
|
+
activePlatforms.push(plat);
|
|
1494
|
+
}
|
|
1495
|
+
} catch (_e) { /* platform not available */ }
|
|
1496
|
+
}
|
|
1497
|
+
if (activePlatforms.length === 0) {
|
|
1498
|
+
console.log('\n No auditable platforms found. Run "nerviq harmony-audit" first.\n');
|
|
1499
|
+
process.exit(0);
|
|
1500
|
+
}
|
|
1501
|
+
const report = formatSynergyReport({ platformAudits, activePlatforms });
|
|
1502
|
+
if (options.json) {
|
|
1503
|
+
console.log(JSON.stringify({ activePlatforms, platformAudits }, null, 2));
|
|
1504
|
+
} else {
|
|
1505
|
+
console.log(report);
|
|
1506
|
+
}
|
|
1373
1507
|
process.exit(0);
|
|
1374
1508
|
} else if (normalizedCommand === 'doctor') {
|
|
1375
1509
|
const { runDoctor } = require('../src/doctor');
|
|
@@ -1404,7 +1538,8 @@ async function main() {
|
|
|
1404
1538
|
const fixKey = parsed.extraArgs[0] || null;
|
|
1405
1539
|
const allCritical = flags.includes('--all-critical');
|
|
1406
1540
|
const promptOnly = flags.includes('--prompt');
|
|
1407
|
-
const autoApply = options.auto
|
|
1541
|
+
const autoApply = options.auto;
|
|
1542
|
+
const isDryRun = options.dryRun;
|
|
1408
1543
|
|
|
1409
1544
|
// Step 1: Run silent audit to find failed checks (only actual failures, not skipped/null)
|
|
1410
1545
|
const auditResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
@@ -1447,6 +1582,13 @@ async function main() {
|
|
|
1447
1582
|
for (const entry of denyEntries) {
|
|
1448
1583
|
if (!settings.permissions.deny.includes(entry)) settings.permissions.deny.push(entry);
|
|
1449
1584
|
}
|
|
1585
|
+
// Remove overly broad allow:["*"] if present
|
|
1586
|
+
if (Array.isArray(settings.permissions.allow) && settings.permissions.allow.includes('*')) {
|
|
1587
|
+
settings.permissions.allow = settings.permissions.allow.filter(a => a !== '*');
|
|
1588
|
+
if (settings.permissions.allow.length === 0) {
|
|
1589
|
+
delete settings.permissions.allow;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1450
1592
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
1451
1593
|
return true;
|
|
1452
1594
|
},
|
|
@@ -1558,7 +1700,7 @@ async function main() {
|
|
|
1558
1700
|
const predictedScore = maxScore > 0 ? Math.round((simulatedEarned / maxScore) * 100) : 0;
|
|
1559
1701
|
const predictedDelta = predictedScore - preScore;
|
|
1560
1702
|
|
|
1561
|
-
if (!autoApply) {
|
|
1703
|
+
if (!autoApply && !isDryRun) {
|
|
1562
1704
|
console.log('');
|
|
1563
1705
|
if (allCritical && fixableTargets.length > 1) {
|
|
1564
1706
|
// Multi-fix summary
|
|
@@ -1607,9 +1749,9 @@ async function main() {
|
|
|
1607
1749
|
const allCreatedFiles = [];
|
|
1608
1750
|
const fixResults = []; // { key, name, status, delta }
|
|
1609
1751
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1752
|
+
const snapshotFiles = {};
|
|
1753
|
+
if (!isDryRun && targetKeys.length > 0) {
|
|
1754
|
+
// Snapshot existing files for rollback (before applying fixes)
|
|
1613
1755
|
for (const key of targetKeys) {
|
|
1614
1756
|
const technique = TECHNIQUES[key];
|
|
1615
1757
|
if (technique && technique.template && technique.template.path) {
|
|
@@ -1619,14 +1761,6 @@ async function main() {
|
|
|
1619
1761
|
}
|
|
1620
1762
|
}
|
|
1621
1763
|
}
|
|
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
1764
|
}
|
|
1631
1765
|
|
|
1632
1766
|
// Step 3b: Apply fixes sequentially with progress
|
|
@@ -1641,7 +1775,7 @@ async function main() {
|
|
|
1641
1775
|
const progress = isBatch ? `${i + 1}/${targetKeys.length}: ` : '';
|
|
1642
1776
|
|
|
1643
1777
|
if (technique && technique.template) {
|
|
1644
|
-
if (
|
|
1778
|
+
if (isDryRun) {
|
|
1645
1779
|
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1646
1780
|
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1647
1781
|
fixed++;
|
|
@@ -1671,7 +1805,7 @@ async function main() {
|
|
|
1671
1805
|
}
|
|
1672
1806
|
}
|
|
1673
1807
|
} else if (INLINE_FIXERS[key]) {
|
|
1674
|
-
if (
|
|
1808
|
+
if (isDryRun) {
|
|
1675
1809
|
console.log(` [dry-run] Would fix: ${progress}${failedCheck.name} (${key})`);
|
|
1676
1810
|
fixResults.push({ key, name: failedCheck.name, status: 'dry-run', delta: 0 });
|
|
1677
1811
|
fixed++;
|
|
@@ -1716,26 +1850,33 @@ async function main() {
|
|
|
1716
1850
|
}
|
|
1717
1851
|
|
|
1718
1852
|
// Record accepted patterns for successfully fixed checks
|
|
1719
|
-
if (!
|
|
1853
|
+
if (!isDryRun) {
|
|
1720
1854
|
for (const key of targetKeys) {
|
|
1721
1855
|
const fr = fixResults.find(r => r.key === key);
|
|
1722
1856
|
recordPattern(options.dir, key, fr && fr.status === 'fixed' ? 'accepted' : 'rejected');
|
|
1723
1857
|
}
|
|
1724
1858
|
}
|
|
1725
1859
|
|
|
1726
|
-
//
|
|
1727
|
-
if (!
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
} catch { /* best effort */ }
|
|
1860
|
+
// Write rollback artifact AFTER fixes are applied (with actual file lists)
|
|
1861
|
+
if (!isDryRun && targetKeys.length > 0 && fixed > 0) {
|
|
1862
|
+
const allPatchedFiles = Object.keys(snapshotFiles);
|
|
1863
|
+
// Also track inline-fixer patched files
|
|
1864
|
+
for (const fr of fixResults) {
|
|
1865
|
+
if (fr.status === 'fixed' && INLINE_FIXERS[fr.key]) {
|
|
1866
|
+
const inlinePath = fr.key === 'gitIgnoreEnv' ? '.gitignore' : fr.key === 'secretsProtection' ? '.claude/settings.json' : null;
|
|
1867
|
+
if (inlinePath && !allPatchedFiles.includes(inlinePath)) {
|
|
1868
|
+
allPatchedFiles.push(inlinePath);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1738
1871
|
}
|
|
1872
|
+
const rollbackArtifact = writeRollbackArtifact(options.dir, {
|
|
1873
|
+
sourcePlan: 'fix-batch',
|
|
1874
|
+
preSnapshot: snapshotFiles,
|
|
1875
|
+
createdFiles: allCreatedFiles,
|
|
1876
|
+
patchedFiles: allPatchedFiles,
|
|
1877
|
+
rollbackInstructions: ['Use nerviq rollback to undo these fixes'],
|
|
1878
|
+
});
|
|
1879
|
+
rollbackId = rollbackArtifact.id;
|
|
1739
1880
|
}
|
|
1740
1881
|
|
|
1741
1882
|
// Step 4: Show batch summary or simple score impact
|
|
@@ -1751,10 +1892,10 @@ async function main() {
|
|
|
1751
1892
|
const totalDelta = runningScore - preScore;
|
|
1752
1893
|
console.log('');
|
|
1753
1894
|
console.log(` Score: ${preScore} → ${runningScore} (${totalDelta >= 0 ? '+' : ''}${totalDelta})`);
|
|
1754
|
-
if (rollbackId && !
|
|
1895
|
+
if (rollbackId && !isDryRun) {
|
|
1755
1896
|
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1756
1897
|
}
|
|
1757
|
-
} else if (fixed > 0 && !
|
|
1898
|
+
} else if (fixed > 0 && !isDryRun) {
|
|
1758
1899
|
const postResult = await audit({ dir: options.dir, silent: true, platform: options.platform });
|
|
1759
1900
|
const delta = postResult.score - preScore;
|
|
1760
1901
|
console.log('');
|
|
@@ -1791,6 +1932,16 @@ async function main() {
|
|
|
1791
1932
|
process.exit(0);
|
|
1792
1933
|
}
|
|
1793
1934
|
const result = await audit(options);
|
|
1935
|
+
if (options.out) {
|
|
1936
|
+
const fs = require('fs');
|
|
1937
|
+
const path = require('path');
|
|
1938
|
+
const outPath = path.resolve(options.out);
|
|
1939
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1940
|
+
fs.writeFileSync(outPath, JSON.stringify(result, null, 2), 'utf8');
|
|
1941
|
+
if (!options.json) {
|
|
1942
|
+
console.log(`\n Audit report written to ${options.out}\n`);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1794
1945
|
if (options.webhookUrl) {
|
|
1795
1946
|
try {
|
|
1796
1947
|
const { sendWebhook, formatSlackMessage } = require('../src/integrations');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.9",
|
|
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": {
|
package/src/benchmark.js
CHANGED
|
@@ -321,9 +321,13 @@ function printBenchmark(report, options = {}) {
|
|
|
321
321
|
console.log(' ═══════════════════════════════════════');
|
|
322
322
|
console.log(' Runs in an isolated temp copy. Your current repo is not modified.');
|
|
323
323
|
console.log('');
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
console.log(`
|
|
324
|
+
const orgDeltaSign = report.delta.organicScore >= 0 ? '+' : '';
|
|
325
|
+
const totalDeltaSign = report.delta.score >= 0 ? '+' : '';
|
|
326
|
+
console.log(` Organic improvement: \x1b[1m${orgDeltaSign}${report.delta.organicScore} points\x1b[0m (your actual config quality)`);
|
|
327
|
+
console.log(` Total with nerviq setup: ${totalDeltaSign}${report.delta.score} points`);
|
|
328
|
+
console.log('');
|
|
329
|
+
console.log(` Before: organic ${report.before.organicScore}/100, total ${report.before.score}/100`);
|
|
330
|
+
console.log(` After: organic ${report.after.organicScore}/100, total ${report.after.score}/100`);
|
|
327
331
|
console.log('');
|
|
328
332
|
console.log(` ${report.executiveSummary.headline}`);
|
|
329
333
|
console.log(` Recommendation: ${report.executiveSummary.decisionGuidance}`);
|
package/src/certification.js
CHANGED
|
@@ -37,10 +37,14 @@ async function certifyProject(dir) {
|
|
|
37
37
|
|
|
38
38
|
// Run per-platform audits
|
|
39
39
|
const platformScores = {};
|
|
40
|
+
const allAuditResults = [];
|
|
40
41
|
for (const platform of platforms) {
|
|
41
42
|
try {
|
|
42
43
|
const result = await audit({ dir: resolvedDir, platform, silent: true });
|
|
43
44
|
platformScores[platform] = result.score;
|
|
45
|
+
if (Array.isArray(result.results)) {
|
|
46
|
+
allAuditResults.push(...result.results);
|
|
47
|
+
}
|
|
44
48
|
} catch {
|
|
45
49
|
platformScores[platform] = 0;
|
|
46
50
|
}
|
|
@@ -55,18 +59,36 @@ async function certifyProject(dir) {
|
|
|
55
59
|
harmonyScore = 0;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
// Determine certification level
|
|
62
|
+
// Determine certification level with security gates
|
|
59
63
|
const scores = Object.values(platformScores);
|
|
60
64
|
const allAbove70 = scores.length > 0 && scores.every(s => s >= 70);
|
|
61
65
|
const allAbove50 = scores.length > 0 && scores.every(s => s >= 50);
|
|
62
66
|
const anyAbove40 = scores.some(s => s >= 40);
|
|
63
67
|
|
|
68
|
+
// Security gate helpers — check whether specific audit checks passed
|
|
69
|
+
const checkPassed = (key) => {
|
|
70
|
+
const match = allAuditResults.find(r => r.key === key);
|
|
71
|
+
return match ? match.passed === true : false;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const gitIgnoreOk = checkPassed('gitIgnoreEnv');
|
|
75
|
+
const secretsOk = checkPassed('secretsProtection');
|
|
76
|
+
const criticalAntiPatterns = allAuditResults.filter(
|
|
77
|
+
r => r.passed === false && r.impact === 'critical'
|
|
78
|
+
);
|
|
79
|
+
const noCriticalAntiPatterns = criticalAntiPatterns.length === 0;
|
|
80
|
+
|
|
81
|
+
// Bronze gate: score >= 40 AND basic security (gitignore + secrets protection)
|
|
82
|
+
const bronzeSecurityGate = gitIgnoreOk && secretsOk;
|
|
83
|
+
// Silver gate: Bronze requirements AND no critical anti-patterns
|
|
84
|
+
const silverSecurityGate = bronzeSecurityGate && noCriticalAntiPatterns;
|
|
85
|
+
|
|
64
86
|
let level;
|
|
65
|
-
if (harmonyScore >= 80 && allAbove70) {
|
|
87
|
+
if (harmonyScore >= 80 && allAbove70 && silverSecurityGate) {
|
|
66
88
|
level = LEVELS.GOLD;
|
|
67
|
-
} else if (harmonyScore >= 60 && allAbove50) {
|
|
89
|
+
} else if (harmonyScore >= 60 && allAbove50 && silverSecurityGate) {
|
|
68
90
|
level = LEVELS.SILVER;
|
|
69
|
-
} else if (anyAbove40) {
|
|
91
|
+
} else if (anyAbove40 && bronzeSecurityGate) {
|
|
70
92
|
level = LEVELS.BRONZE;
|
|
71
93
|
} else {
|
|
72
94
|
level = LEVELS.NONE;
|
|
@@ -80,6 +102,12 @@ async function certifyProject(dir) {
|
|
|
80
102
|
platformScores,
|
|
81
103
|
platforms,
|
|
82
104
|
badge,
|
|
105
|
+
securityGates: {
|
|
106
|
+
gitIgnoreEnv: gitIgnoreOk,
|
|
107
|
+
secretsProtection: secretsOk,
|
|
108
|
+
noCriticalAntiPatterns,
|
|
109
|
+
criticalAntiPatternCount: criticalAntiPatterns.length,
|
|
110
|
+
},
|
|
83
111
|
};
|
|
84
112
|
}
|
|
85
113
|
|
package/src/context.js
CHANGED
|
@@ -185,12 +185,38 @@ class ProjectContext {
|
|
|
185
185
|
return deps;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Recursively check if a file or directory name exists anywhere under a given base directory.
|
|
190
|
+
* Searches up to maxDepth levels deep.
|
|
191
|
+
*/
|
|
192
|
+
_findInSubdirs(name, baseDir, maxDepth = 3) {
|
|
193
|
+
if (maxDepth <= 0) return false;
|
|
194
|
+
try {
|
|
195
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (entry.name === 'node_modules' || entry.name === '__pycache__' || entry.name === '.git') continue;
|
|
198
|
+
if (entry.name === name || entry.name.endsWith(name)) return true;
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
if (this._findInSubdirs(name, path.join(baseDir, entry.name), maxDepth - 1)) return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// directory not readable
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
188
209
|
detectStacks(STACKS) {
|
|
189
210
|
const detected = [];
|
|
190
211
|
for (const [key, stack] of Object.entries(STACKS)) {
|
|
191
|
-
|
|
212
|
+
// Check root-level files first (fast path)
|
|
213
|
+
let hasFile = stack.files.some(f => {
|
|
192
214
|
return this.files.some(pf => pf.startsWith(f));
|
|
193
215
|
});
|
|
216
|
+
// If not found at root, search subdirectories (up to 3 levels deep)
|
|
217
|
+
if (!hasFile) {
|
|
218
|
+
hasFile = stack.files.some(f => this._findInSubdirs(f, this.dir));
|
|
219
|
+
}
|
|
194
220
|
if (!hasFile) continue;
|
|
195
221
|
|
|
196
222
|
let contentMatch = true;
|
package/src/convert.js
CHANGED
|
@@ -98,7 +98,7 @@ function readSourceConfig(dir, from) {
|
|
|
98
98
|
if (descMatch) desc = descMatch[1].trim();
|
|
99
99
|
}
|
|
100
100
|
canonical.rules.push({
|
|
101
|
-
name: file.replace(
|
|
101
|
+
name: file.replace(/\.(mdc|md|txt)$/i, ''),
|
|
102
102
|
content,
|
|
103
103
|
alwaysOn,
|
|
104
104
|
glob,
|
|
@@ -165,7 +165,11 @@ function readSourceConfig(dir, from) {
|
|
|
165
165
|
|
|
166
166
|
function buildTargetOutput(canonical, to, { dryRun = false } = {}) {
|
|
167
167
|
const outputs = []; // Array of { path, content }
|
|
168
|
-
|
|
168
|
+
// Strip MDC frontmatter from rule content for non-cursor targets to prevent leaking
|
|
169
|
+
const stripFrontmatter = (text) => text.replace(/^---[\s\S]*?---\n/m, '').trim();
|
|
170
|
+
const combinedContent = to === 'cursor'
|
|
171
|
+
? canonical.rules.map(r => r.content).join('\n\n')
|
|
172
|
+
: canonical.rules.map(r => stripFrontmatter(r.content)).join('\n\n');
|
|
169
173
|
|
|
170
174
|
if (to === 'claude') {
|
|
171
175
|
// Extract or create CLAUDE.md from combined rules
|
package/src/governance.js
CHANGED
|
@@ -55,7 +55,7 @@ const HOOK_REGISTRY = [
|
|
|
55
55
|
key: 'protect-secrets',
|
|
56
56
|
file: '.claude/hooks/protect-secrets.sh',
|
|
57
57
|
triggerPoint: 'PreToolUse',
|
|
58
|
-
matcher: 'Read|Write|Edit',
|
|
58
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
59
59
|
purpose: 'Blocks direct access to secret or credential files before a tool runs.',
|
|
60
60
|
filesTouched: [],
|
|
61
61
|
sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
|
|
@@ -322,7 +322,7 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
322
322
|
const secretsFile = uniqueFiles.find(isSecrets);
|
|
323
323
|
if (secretsFile) {
|
|
324
324
|
hookConfig.PreToolUse = [{
|
|
325
|
-
matcher: 'Read|Write|Edit',
|
|
325
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
326
326
|
hooks: [{
|
|
327
327
|
type: 'command',
|
|
328
328
|
command: hookCommand(secretsFile),
|
package/src/mcp-server.js
CHANGED
package/src/server.js
CHANGED
|
@@ -18,6 +18,10 @@ const SUPPORTED_PLATFORMS = new Set([
|
|
|
18
18
|
'opencode',
|
|
19
19
|
]);
|
|
20
20
|
|
|
21
|
+
function envelope(data) {
|
|
22
|
+
return { data, meta: { version, timestamp: new Date().toISOString() } };
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
function sendJson(res, statusCode, payload) {
|
|
22
26
|
const body = JSON.stringify(payload, null, 2);
|
|
23
27
|
res.writeHead(statusCode, {
|
|
@@ -57,6 +61,11 @@ function createServer(options = {}) {
|
|
|
57
61
|
const baseDir = path.resolve(options.baseDir || process.cwd());
|
|
58
62
|
|
|
59
63
|
return http.createServer(async (req, res) => {
|
|
64
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
65
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
66
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
67
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
68
|
+
|
|
60
69
|
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
61
70
|
|
|
62
71
|
if (req.method !== 'GET') {
|
|
@@ -66,16 +75,16 @@ function createServer(options = {}) {
|
|
|
66
75
|
|
|
67
76
|
try {
|
|
68
77
|
if (requestUrl.pathname === '/api/health') {
|
|
69
|
-
sendJson(res, 200, {
|
|
78
|
+
sendJson(res, 200, envelope({
|
|
70
79
|
status: 'ok',
|
|
71
80
|
version,
|
|
72
81
|
checks: getCatalog().length,
|
|
73
|
-
});
|
|
82
|
+
}));
|
|
74
83
|
return;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
if (requestUrl.pathname === '/api/catalog') {
|
|
78
|
-
sendJson(res, 200, getCatalog());
|
|
87
|
+
sendJson(res, 200, envelope(getCatalog()));
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -83,14 +92,14 @@ function createServer(options = {}) {
|
|
|
83
92
|
const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
|
|
84
93
|
const platform = normalizePlatform(requestUrl.searchParams.get('platform'));
|
|
85
94
|
const result = await audit({ dir, platform, silent: true });
|
|
86
|
-
sendJson(res, 200, result);
|
|
95
|
+
sendJson(res, 200, envelope(result));
|
|
87
96
|
return;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
if (requestUrl.pathname === '/api/harmony') {
|
|
91
100
|
const dir = resolveRequestDir(baseDir, requestUrl.searchParams.get('dir'));
|
|
92
101
|
const result = await harmonyAudit({ dir, silent: true });
|
|
93
|
-
sendJson(res, 200, result);
|
|
102
|
+
sendJson(res, 200, envelope(result));
|
|
94
103
|
return;
|
|
95
104
|
}
|
|
96
105
|
|
package/src/setup.js
CHANGED
|
@@ -10,6 +10,7 @@ const { ProjectContext } = require('./context');
|
|
|
10
10
|
const { audit } = require('./audit');
|
|
11
11
|
const { buildSettingsForProfile } = require('./governance');
|
|
12
12
|
const { getMcpPackPreflight } = require('./mcp-packs');
|
|
13
|
+
const { writeRollbackArtifact } = require('./activity');
|
|
13
14
|
const { setupCodex } = require('./codex/setup');
|
|
14
15
|
|
|
15
16
|
// ============================================================
|
|
@@ -797,20 +798,27 @@ try {
|
|
|
797
798
|
} catch (e) { /* linter not available or failed - non-blocking */ }
|
|
798
799
|
`,
|
|
799
800
|
'protect-secrets.js': `#!/usr/bin/env node
|
|
800
|
-
// PreToolUse hook - blocks reads of secret files
|
|
801
|
+
// PreToolUse hook - blocks reads of secret files (Read/Write/Edit AND Bash)
|
|
801
802
|
let input = '';
|
|
802
803
|
process.stdin.on('data', d => input += d);
|
|
803
804
|
process.stdin.on('end', () => {
|
|
804
805
|
try {
|
|
805
806
|
const data = JSON.parse(input);
|
|
807
|
+
// Check file_path (for Read/Write/Edit)
|
|
806
808
|
const fp = (data.tool_input && data.tool_input.file_path) || '';
|
|
807
|
-
|
|
809
|
+
// Check command (for Bash)
|
|
810
|
+
const cmd = (data.tool_input && data.tool_input.command) || '';
|
|
811
|
+
|
|
812
|
+
const secretPattern = /\\.env($|\\.)|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i;
|
|
813
|
+
const bashSecretPattern = /\\bcat\\s+\\.env|\\bless\\s+\\.env|\\bhead\\s+\\.env|\\btail\\s+\\.env|\\bgrep\\b.*\\.env|\\bcp\\s+\\.env|\\bmv\\s+\\.env|\\bbase64\\s+\\.env|\\bxxd\\s+\\.env|secrets\\/|credentials|\\.pem\\b|\\.key\\b/i;
|
|
814
|
+
|
|
815
|
+
if (secretPattern.test(fp) || bashSecretPattern.test(cmd)) {
|
|
808
816
|
console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
|
|
809
817
|
} else {
|
|
810
818
|
console.log(JSON.stringify({ decision: 'allow' }));
|
|
811
819
|
}
|
|
812
820
|
} catch (e) {
|
|
813
|
-
console.log(JSON.stringify({ decision: '
|
|
821
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Hook error - blocking for safety' }));
|
|
814
822
|
}
|
|
815
823
|
});
|
|
816
824
|
`,
|
|
@@ -1062,7 +1070,14 @@ Prepare a release candidate for: $ARGUMENTS
|
|
|
1062
1070
|
- Mock external dependencies, not internal logic
|
|
1063
1071
|
- Include both happy path and edge case tests
|
|
1064
1072
|
`;
|
|
1065
|
-
rules['repository.md'] =
|
|
1073
|
+
rules['repository.md'] = hasPython
|
|
1074
|
+
? `When changing release, packaging, or workflow files:
|
|
1075
|
+
- Keep pyproject.toml (or requirements.txt), CHANGELOG.md, README.md, and docs in sync
|
|
1076
|
+
- Prefer tagged release references over floating branch references in public docs
|
|
1077
|
+
- Preserve backward compatibility in CLI flags where practical
|
|
1078
|
+
- Any automation that writes files must document rollback expectations
|
|
1079
|
+
`
|
|
1080
|
+
: `When changing release, packaging, or workflow files:
|
|
1066
1081
|
- Keep package.json, CHANGELOG.md, README.md, and docs in sync
|
|
1067
1082
|
- Prefer tagged release references over floating branch references in public docs
|
|
1068
1083
|
- Preserve backward compatibility in CLI flags where practical
|
|
@@ -1115,6 +1130,18 @@ async function setup(options) {
|
|
|
1115
1130
|
if (options.platform === 'codex') {
|
|
1116
1131
|
return setupCodex(options);
|
|
1117
1132
|
}
|
|
1133
|
+
if (options.platform === 'windsurf') {
|
|
1134
|
+
const { setupWindsurf } = require('./windsurf/setup');
|
|
1135
|
+
return setupWindsurf(options);
|
|
1136
|
+
}
|
|
1137
|
+
if (options.platform === 'aider') {
|
|
1138
|
+
const { setupAider } = require('./aider/setup');
|
|
1139
|
+
return setupAider(options);
|
|
1140
|
+
}
|
|
1141
|
+
if (options.platform === 'cursor') {
|
|
1142
|
+
const { setupCursor } = require('./cursor/setup');
|
|
1143
|
+
return setupCursor(options);
|
|
1144
|
+
}
|
|
1118
1145
|
|
|
1119
1146
|
const ctx = new ProjectContext(options.dir);
|
|
1120
1147
|
const stacks = ctx.detectStacks(STACKS);
|
|
@@ -1124,6 +1151,17 @@ async function setup(options) {
|
|
|
1124
1151
|
const mcpPreflightWarnings = getMcpPackPreflight(options.mcpPacks || [])
|
|
1125
1152
|
.filter(item => item.missingEnvVars.length > 0);
|
|
1126
1153
|
|
|
1154
|
+
// Snapshot settings.json before any changes for rollback support
|
|
1155
|
+
const settingsPathForSnapshot = path.join(options.dir, '.claude/settings.json');
|
|
1156
|
+
let settingsSnapshotBefore = null;
|
|
1157
|
+
if (fs.existsSync(settingsPathForSnapshot)) {
|
|
1158
|
+
try {
|
|
1159
|
+
settingsSnapshotBefore = fs.readFileSync(settingsPathForSnapshot, 'utf8');
|
|
1160
|
+
} catch (_) {
|
|
1161
|
+
// Ignore read errors
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1127
1165
|
function log(message = '') {
|
|
1128
1166
|
if (!silent) {
|
|
1129
1167
|
console.log(message);
|
|
@@ -1218,21 +1256,50 @@ async function setup(options) {
|
|
|
1218
1256
|
}
|
|
1219
1257
|
}
|
|
1220
1258
|
|
|
1221
|
-
// Auto-register hooks in settings
|
|
1259
|
+
// Auto-register hooks in settings — always merge hooks into settings.json
|
|
1222
1260
|
const hooksDir = path.join(options.dir, '.claude/hooks');
|
|
1223
1261
|
const settingsPath = path.join(options.dir, '.claude/settings.json');
|
|
1224
|
-
if (fs.existsSync(hooksDir)
|
|
1262
|
+
if (fs.existsSync(hooksDir)) {
|
|
1225
1263
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
|
|
1226
1264
|
if (hookFiles.length > 0) {
|
|
1227
|
-
const
|
|
1265
|
+
const newSettings = buildSettingsForProfile({
|
|
1228
1266
|
profileKey: options.profile || 'safe-write',
|
|
1229
1267
|
hookFiles,
|
|
1230
1268
|
mcpPackKeys: options.mcpPacks || [],
|
|
1231
1269
|
});
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1270
|
+
// Merge new settings into existing settings.json, preserving all fields
|
|
1271
|
+
let existingSettings = {};
|
|
1272
|
+
if (fs.existsSync(settingsPath)) {
|
|
1273
|
+
try {
|
|
1274
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1275
|
+
} catch (_) {
|
|
1276
|
+
// If settings.json is malformed, start fresh
|
|
1277
|
+
existingSettings = {};
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
// Merge all fields from newSettings into existing, preserving existing values
|
|
1281
|
+
if (newSettings.hooks) existingSettings.hooks = newSettings.hooks;
|
|
1282
|
+
if (newSettings.permissions) {
|
|
1283
|
+
existingSettings.permissions = existingSettings.permissions || {};
|
|
1284
|
+
// MERGE deny rules: keep existing + add new (deduplicate)
|
|
1285
|
+
const existingDeny = existingSettings.permissions.deny || [];
|
|
1286
|
+
const newDeny = newSettings.permissions.deny || [];
|
|
1287
|
+
existingSettings.permissions.deny = [...new Set([...existingDeny, ...newDeny])];
|
|
1288
|
+
// Only set defaultMode if not already set
|
|
1289
|
+
if (!existingSettings.permissions.defaultMode && newSettings.permissions.defaultMode) {
|
|
1290
|
+
existingSettings.permissions.defaultMode = newSettings.permissions.defaultMode;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (newSettings.mcpServers) existingSettings.mcpServers = { ...existingSettings.mcpServers, ...newSettings.mcpServers };
|
|
1294
|
+
if (newSettings.nerviqSetup) existingSettings.nerviqSetup = { ...existingSettings.nerviqSetup, ...newSettings.nerviqSetup };
|
|
1295
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
|
|
1296
|
+
if (!writtenFiles.includes('.claude/settings.json') && !preservedFiles.includes('.claude/settings.json')) {
|
|
1297
|
+
writtenFiles.push('.claude/settings.json');
|
|
1298
|
+
log(` \x1b[32m✅\x1b[0m Updated .claude/settings.json (hooks registered)`);
|
|
1299
|
+
created++;
|
|
1300
|
+
} else {
|
|
1301
|
+
log(` \x1b[32m✅\x1b[0m Merged hooks into existing .claude/settings.json`);
|
|
1302
|
+
}
|
|
1236
1303
|
}
|
|
1237
1304
|
}
|
|
1238
1305
|
|
|
@@ -1264,6 +1331,29 @@ async function setup(options) {
|
|
|
1264
1331
|
log(' Run \x1b[1mnpx nerviq audit\x1b[0m to check your score.');
|
|
1265
1332
|
log('');
|
|
1266
1333
|
|
|
1334
|
+
// Write rollback artifact so setup can be undone
|
|
1335
|
+
let rollbackId = null;
|
|
1336
|
+
if (writtenFiles.length > 0) {
|
|
1337
|
+
const patchedFiles = [];
|
|
1338
|
+
// If settings.json was modified (not newly created), record the before-snapshot
|
|
1339
|
+
if (settingsSnapshotBefore !== null && writtenFiles.includes('.claude/settings.json')) {
|
|
1340
|
+
patchedFiles.push({
|
|
1341
|
+
file: '.claude/settings.json',
|
|
1342
|
+
before: settingsSnapshotBefore,
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
const rollbackArtifact = writeRollbackArtifact(options.dir, {
|
|
1346
|
+
sourcePlan: 'setup',
|
|
1347
|
+
createdFiles: writtenFiles.filter(f => {
|
|
1348
|
+
// Exclude patched files from createdFiles list
|
|
1349
|
+
return !patchedFiles.some(p => p.file === f);
|
|
1350
|
+
}),
|
|
1351
|
+
patchedFiles,
|
|
1352
|
+
rollbackInstructions: ['Use nerviq rollback to undo this setup'],
|
|
1353
|
+
});
|
|
1354
|
+
rollbackId = rollbackArtifact.id;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1267
1357
|
return {
|
|
1268
1358
|
created,
|
|
1269
1359
|
skipped,
|
|
@@ -1271,6 +1361,7 @@ async function setup(options) {
|
|
|
1271
1361
|
preservedFiles,
|
|
1272
1362
|
stacks,
|
|
1273
1363
|
mcpPreflightWarnings,
|
|
1364
|
+
rollbackId,
|
|
1274
1365
|
};
|
|
1275
1366
|
}
|
|
1276
1367
|
|
package/src/techniques.js
CHANGED
|
@@ -759,7 +759,11 @@ const TECHNIQUES = {
|
|
|
759
759
|
const settings = ctx.jsonFile('.claude/settings.json') || ctx.jsonFile('.claude/settings.local.json');
|
|
760
760
|
if (!settings || !settings.permissions) return false;
|
|
761
761
|
const deny = JSON.stringify(settings.permissions.deny || []);
|
|
762
|
-
|
|
762
|
+
const hasDeny = deny.includes('.env') || deny.includes('secrets');
|
|
763
|
+
// Fail if allow includes "*" (overly broad — bypasses deny rules)
|
|
764
|
+
const allow = settings.permissions.allow || [];
|
|
765
|
+
if (Array.isArray(allow) && allow.includes('*')) return false;
|
|
766
|
+
return hasDeny;
|
|
763
767
|
},
|
|
764
768
|
impact: 'critical',
|
|
765
769
|
rating: 5,
|
|
@@ -5474,7 +5478,7 @@ const STACKS = {
|
|
|
5474
5478
|
ruby: { files: ['Gemfile'], content: {}, label: 'Ruby' },
|
|
5475
5479
|
java: { files: ['pom.xml'], content: {}, label: 'Java' },
|
|
5476
5480
|
kotlin: { files: ['build.gradle.kts'], content: {}, label: 'Kotlin' },
|
|
5477
|
-
swift: { files: ['Package.swift'], content: {}, label: 'Swift' },
|
|
5481
|
+
swift: { files: ['Package.swift', '.xcodeproj'], content: {}, label: 'Swift' },
|
|
5478
5482
|
terraform: { files: ['main.tf', 'terraform'], content: {}, label: 'Terraform' },
|
|
5479
5483
|
kubernetes: { files: ['k8s', 'kubernetes', 'helm'], content: {}, label: 'Kubernetes' },
|
|
5480
5484
|
cpp: { files: ['CMakeLists.txt', 'Makefile', '.clang-format'], content: {}, label: 'C++' },
|