@nerviq/cli 1.8.7 → 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/README.md +0 -13
- package/bin/cli.js +129 -32
- package/package.json +1 -1
- package/src/context.js +27 -1
- package/src/convert.js +6 -2
- package/src/setup.js +47 -9
- 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,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
|
-
|
|
713
|
+
printScanDetail(summary, options);
|
|
652
714
|
if (options.threshold !== null && summary.averageScore < options.threshold) {
|
|
653
715
|
process.exit(1);
|
|
654
716
|
}
|
|
@@ -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
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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
|
-
//
|
|
1727
|
-
if (!
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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 && !
|
|
1851
|
+
if (rollbackId && !isDryRun) {
|
|
1755
1852
|
console.log(` Rollback available: nerviq rollback --id ${rollbackId}`);
|
|
1756
1853
|
}
|
|
1757
|
-
} else if (fixed > 0 && !
|
|
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.
|
|
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": {
|
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/setup.js
CHANGED
|
@@ -810,7 +810,7 @@ process.stdin.on('end', () => {
|
|
|
810
810
|
console.log(JSON.stringify({ decision: 'allow' }));
|
|
811
811
|
}
|
|
812
812
|
} catch (e) {
|
|
813
|
-
console.log(JSON.stringify({ decision: '
|
|
813
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Hook error - blocking for safety' }));
|
|
814
814
|
}
|
|
815
815
|
});
|
|
816
816
|
`,
|
|
@@ -1062,7 +1062,14 @@ Prepare a release candidate for: $ARGUMENTS
|
|
|
1062
1062
|
- Mock external dependencies, not internal logic
|
|
1063
1063
|
- Include both happy path and edge case tests
|
|
1064
1064
|
`;
|
|
1065
|
-
rules['repository.md'] =
|
|
1065
|
+
rules['repository.md'] = hasPython
|
|
1066
|
+
? `When changing release, packaging, or workflow files:
|
|
1067
|
+
- Keep pyproject.toml (or requirements.txt), CHANGELOG.md, README.md, and docs in sync
|
|
1068
|
+
- Prefer tagged release references over floating branch references in public docs
|
|
1069
|
+
- Preserve backward compatibility in CLI flags where practical
|
|
1070
|
+
- Any automation that writes files must document rollback expectations
|
|
1071
|
+
`
|
|
1072
|
+
: `When changing release, packaging, or workflow files:
|
|
1066
1073
|
- Keep package.json, CHANGELOG.md, README.md, and docs in sync
|
|
1067
1074
|
- Prefer tagged release references over floating branch references in public docs
|
|
1068
1075
|
- Preserve backward compatibility in CLI flags where practical
|
|
@@ -1115,6 +1122,18 @@ async function setup(options) {
|
|
|
1115
1122
|
if (options.platform === 'codex') {
|
|
1116
1123
|
return setupCodex(options);
|
|
1117
1124
|
}
|
|
1125
|
+
if (options.platform === 'windsurf') {
|
|
1126
|
+
const { setupWindsurf } = require('./windsurf/setup');
|
|
1127
|
+
return setupWindsurf(options);
|
|
1128
|
+
}
|
|
1129
|
+
if (options.platform === 'aider') {
|
|
1130
|
+
const { setupAider } = require('./aider/setup');
|
|
1131
|
+
return setupAider(options);
|
|
1132
|
+
}
|
|
1133
|
+
if (options.platform === 'cursor') {
|
|
1134
|
+
const { setupCursor } = require('./cursor/setup');
|
|
1135
|
+
return setupCursor(options);
|
|
1136
|
+
}
|
|
1118
1137
|
|
|
1119
1138
|
const ctx = new ProjectContext(options.dir);
|
|
1120
1139
|
const stacks = ctx.detectStacks(STACKS);
|
|
@@ -1218,21 +1237,40 @@ async function setup(options) {
|
|
|
1218
1237
|
}
|
|
1219
1238
|
}
|
|
1220
1239
|
|
|
1221
|
-
// Auto-register hooks in settings
|
|
1240
|
+
// Auto-register hooks in settings — always merge hooks into settings.json
|
|
1222
1241
|
const hooksDir = path.join(options.dir, '.claude/hooks');
|
|
1223
1242
|
const settingsPath = path.join(options.dir, '.claude/settings.json');
|
|
1224
|
-
if (fs.existsSync(hooksDir)
|
|
1243
|
+
if (fs.existsSync(hooksDir)) {
|
|
1225
1244
|
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
|
|
1226
1245
|
if (hookFiles.length > 0) {
|
|
1227
|
-
const
|
|
1246
|
+
const newSettings = buildSettingsForProfile({
|
|
1228
1247
|
profileKey: options.profile || 'safe-write',
|
|
1229
1248
|
hookFiles,
|
|
1230
1249
|
mcpPackKeys: options.mcpPacks || [],
|
|
1231
1250
|
});
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1251
|
+
// Merge new settings into existing settings.json, preserving all fields
|
|
1252
|
+
let existingSettings = {};
|
|
1253
|
+
if (fs.existsSync(settingsPath)) {
|
|
1254
|
+
try {
|
|
1255
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1256
|
+
} catch (_) {
|
|
1257
|
+
// If settings.json is malformed, start fresh
|
|
1258
|
+
existingSettings = {};
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// Merge all fields from newSettings into existing, preserving existing values
|
|
1262
|
+
if (newSettings.hooks) existingSettings.hooks = newSettings.hooks;
|
|
1263
|
+
if (newSettings.permissions) existingSettings.permissions = { ...existingSettings.permissions, ...newSettings.permissions };
|
|
1264
|
+
if (newSettings.mcpServers) existingSettings.mcpServers = { ...existingSettings.mcpServers, ...newSettings.mcpServers };
|
|
1265
|
+
if (newSettings.nerviqSetup) existingSettings.nerviqSetup = { ...existingSettings.nerviqSetup, ...newSettings.nerviqSetup };
|
|
1266
|
+
fs.writeFileSync(settingsPath, JSON.stringify(existingSettings, null, 2), 'utf8');
|
|
1267
|
+
if (!writtenFiles.includes('.claude/settings.json') && !preservedFiles.includes('.claude/settings.json')) {
|
|
1268
|
+
writtenFiles.push('.claude/settings.json');
|
|
1269
|
+
log(` \x1b[32m✅\x1b[0m Updated .claude/settings.json (hooks registered)`);
|
|
1270
|
+
created++;
|
|
1271
|
+
} else {
|
|
1272
|
+
log(` \x1b[32m✅\x1b[0m Merged hooks into existing .claude/settings.json`);
|
|
1273
|
+
}
|
|
1236
1274
|
}
|
|
1237
1275
|
}
|
|
1238
1276
|
|
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++' },
|