@nerviq/cli 1.29.0 → 1.29.1
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/CHANGELOG.md +1527 -1493
- package/README.md +550 -538
- package/SECURITY.md +82 -82
- package/bin/cli.js +2562 -2558
- package/docs/api-reference.md +356 -356
- package/docs/audit-fix.md +109 -0
- package/docs/autofix.md +3 -62
- package/docs/getting-started.md +1 -1
- package/docs/index.html +592 -592
- package/docs/integration-contracts.md +287 -287
- package/docs/maintenance.md +128 -128
- package/docs/new-platform-guide.md +202 -202
- package/docs/release-process.md +63 -0
- package/docs/shallow-risk.md +244 -244
- package/docs/why-nerviq.md +82 -82
- package/package.json +67 -67
- package/src/aider/activity.js +226 -226
- package/src/aider/context.js +162 -162
- package/src/aider/freshness.js +123 -123
- package/src/aider/techniques.js +3465 -3465
- package/src/audit/layers.js +180 -180
- package/src/audit.js +1032 -1032
- package/src/benchmark.js +299 -299
- package/src/codex/activity.js +324 -324
- package/src/codex/freshness.js +142 -142
- package/src/codex/techniques.js +4895 -4895
- package/src/context.js +326 -326
- package/src/continuous-ops.js +11 -1
- package/src/convert.js +340 -340
- package/src/copilot/config-parser.js +280 -280
- package/src/copilot/context.js +218 -218
- package/src/copilot/freshness.js +177 -177
- package/src/copilot/patch.js +238 -238
- package/src/copilot/techniques.js +3578 -3578
- package/src/cursor/freshness.js +194 -194
- package/src/cursor/patch.js +243 -243
- package/src/cursor/techniques.js +3735 -3735
- package/src/doctor.js +201 -201
- package/src/fix-engine.js +511 -8
- package/src/formatters/csv.js +86 -86
- package/src/formatters/junit.js +123 -123
- package/src/formatters/markdown.js +164 -164
- package/src/formatters/otel.js +151 -151
- package/src/freshness.js +156 -156
- package/src/gemini/activity.js +402 -402
- package/src/gemini/context.js +290 -290
- package/src/gemini/freshness.js +183 -183
- package/src/gemini/patch.js +229 -229
- package/src/gemini/techniques.js +3811 -3811
- package/src/governance.js +533 -533
- package/src/harmony/audit.js +306 -306
- package/src/i18n.js +63 -63
- package/src/insights.js +119 -119
- package/src/integrations.js +134 -134
- package/src/locales/en.json +33 -33
- package/src/locales/es.json +33 -33
- package/src/migrate.js +354 -354
- package/src/opencode/activity.js +286 -286
- package/src/opencode/freshness.js +137 -137
- package/src/opencode/techniques.js +3450 -3450
- package/src/setup/analysis.js +12 -12
- package/src/setup.js +7 -6
- package/src/shallow-risk/index.js +56 -56
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
- package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
- package/src/shallow-risk/shared.js +648 -648
- package/src/source-urls.js +295 -295
- package/src/state-paths.js +85 -85
- package/src/supplemental-checks.js +805 -805
- package/src/telemetry.js +160 -160
- package/src/windsurf/context.js +359 -359
- package/src/windsurf/freshness.js +194 -194
- package/src/windsurf/patch.js +231 -231
- package/src/windsurf/techniques.js +3779 -3779
package/src/fix-engine.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const readline = require('readline');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
4
5
|
|
|
5
6
|
const { ProjectContext } = require('./context');
|
|
6
7
|
const { TECHNIQUES, STACKS } = require('./techniques');
|
|
@@ -33,7 +34,10 @@ const CUSTOM_FIXER_KEYS = new Set([
|
|
|
33
34
|
'changelog',
|
|
34
35
|
'contributing',
|
|
35
36
|
'gitIgnoreEnv',
|
|
37
|
+
'gitIgnoreClaudeLocal',
|
|
38
|
+
'gitignoreClaudeLocal',
|
|
36
39
|
'secretsProtection',
|
|
40
|
+
'editorconfig',
|
|
37
41
|
]);
|
|
38
42
|
|
|
39
43
|
const AUDIT_FIX_KEYS = new Set([
|
|
@@ -45,6 +49,11 @@ const AUDIT_FIX_KEYS = new Set([
|
|
|
45
49
|
'license',
|
|
46
50
|
'changelog',
|
|
47
51
|
'contributing',
|
|
52
|
+
'gitIgnoreEnv',
|
|
53
|
+
'gitIgnoreClaudeLocal',
|
|
54
|
+
'gitignoreClaudeLocal',
|
|
55
|
+
'secretsProtection',
|
|
56
|
+
'editorconfig',
|
|
48
57
|
]);
|
|
49
58
|
|
|
50
59
|
const INSTRUCTION_KEYS = new Set([
|
|
@@ -62,6 +71,25 @@ const QUALITY_COMMAND_KEYS = new Set([
|
|
|
62
71
|
'buildCommand',
|
|
63
72
|
]);
|
|
64
73
|
|
|
74
|
+
const AUDIT_FIX_ALLOWED_PATHS = new Set([
|
|
75
|
+
'.claude/CLAUDE.md',
|
|
76
|
+
'.claude/settings.json',
|
|
77
|
+
'.editorconfig',
|
|
78
|
+
'.gitignore',
|
|
79
|
+
'.codex/AGENTS.md',
|
|
80
|
+
'AGENTS.md',
|
|
81
|
+
'CHANGELOG.md',
|
|
82
|
+
'CLAUDE.md',
|
|
83
|
+
'CONTRIBUTING.md',
|
|
84
|
+
'LICENSE',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const GITIGNORE_ENTRIES_BY_KEY = {
|
|
88
|
+
gitIgnoreEnv: ['.env', '.env.*'],
|
|
89
|
+
gitIgnoreClaudeLocal: ['.claude/settings.local.json'],
|
|
90
|
+
gitignoreClaudeLocal: ['CLAUDE.local.md'],
|
|
91
|
+
};
|
|
92
|
+
|
|
65
93
|
function normalizeNewlines(content) {
|
|
66
94
|
return String(content || '').replace(/\r\n/g, '\n');
|
|
67
95
|
}
|
|
@@ -301,6 +329,23 @@ function buildContributingTemplate(ctx, commands) {
|
|
|
301
329
|
].join('\n');
|
|
302
330
|
}
|
|
303
331
|
|
|
332
|
+
function buildEditorConfigTemplate() {
|
|
333
|
+
return [
|
|
334
|
+
'root = true',
|
|
335
|
+
'',
|
|
336
|
+
'[*]',
|
|
337
|
+
'charset = utf-8',
|
|
338
|
+
'end_of_line = lf',
|
|
339
|
+
'indent_style = space',
|
|
340
|
+
'indent_size = 2',
|
|
341
|
+
'insert_final_newline = true',
|
|
342
|
+
'trim_trailing_whitespace = true',
|
|
343
|
+
'',
|
|
344
|
+
'[*.md]',
|
|
345
|
+
'trim_trailing_whitespace = false',
|
|
346
|
+
].join('\n');
|
|
347
|
+
}
|
|
348
|
+
|
|
304
349
|
function buildInstructionOperation({ ctx, stacks, failedByKey, platform, targetKeys }) {
|
|
305
350
|
const keys = targetKeys.filter((key) => INSTRUCTION_KEYS.has(key));
|
|
306
351
|
if (keys.length === 0) return null;
|
|
@@ -356,10 +401,20 @@ function buildSimpleCreateOperation(filePath, content, keys, failedByKey) {
|
|
|
356
401
|
};
|
|
357
402
|
}
|
|
358
403
|
|
|
359
|
-
function buildGitIgnoreOperation(ctx, failedByKey) {
|
|
404
|
+
function buildGitIgnoreOperation(ctx, failedByKey, keys = ['gitIgnoreEnv']) {
|
|
360
405
|
const existing = ctx.fileContent('.gitignore');
|
|
361
406
|
const normalized = normalizeNewlines(existing || '');
|
|
362
|
-
|
|
407
|
+
const normalizedKeys = unique(keys);
|
|
408
|
+
const entries = unique(normalizedKeys.flatMap((key) => GITIGNORE_ENTRIES_BY_KEY[key] || []));
|
|
409
|
+
const existingEntries = new Set(
|
|
410
|
+
normalized
|
|
411
|
+
.split('\n')
|
|
412
|
+
.map((line) => line.trim())
|
|
413
|
+
.filter((line) => line && !line.startsWith('#')),
|
|
414
|
+
);
|
|
415
|
+
const missingEntries = entries.filter((entry) => !existingEntries.has(entry));
|
|
416
|
+
|
|
417
|
+
if (missingEntries.length === 0) {
|
|
363
418
|
return null;
|
|
364
419
|
}
|
|
365
420
|
|
|
@@ -369,9 +424,9 @@ function buildGitIgnoreOperation(ctx, failedByKey) {
|
|
|
369
424
|
path: '.gitignore',
|
|
370
425
|
action: existing === null ? 'create' : 'patch',
|
|
371
426
|
before: existing,
|
|
372
|
-
after: ensureTrailingNewline(`${normalized}${prefix}.
|
|
373
|
-
keys:
|
|
374
|
-
impact: highestImpact(
|
|
427
|
+
after: ensureTrailingNewline(`${normalized}${prefix}${missingEntries.join('\n')}\n`),
|
|
428
|
+
keys: normalizedKeys,
|
|
429
|
+
impact: highestImpact(normalizedKeys, failedByKey),
|
|
375
430
|
};
|
|
376
431
|
}
|
|
377
432
|
|
|
@@ -389,7 +444,14 @@ function buildSecretsProtectionOperation(ctx, failedByKey) {
|
|
|
389
444
|
|
|
390
445
|
settings.permissions = settings.permissions || {};
|
|
391
446
|
settings.permissions.deny = Array.isArray(settings.permissions.deny) ? settings.permissions.deny : [];
|
|
392
|
-
const denyEntries = [
|
|
447
|
+
const denyEntries = [
|
|
448
|
+
'Read(.env)',
|
|
449
|
+
'Read(.env.*)',
|
|
450
|
+
'Read(**/.env)',
|
|
451
|
+
'Read(**/.env.*)',
|
|
452
|
+
'Read(**/*.pem)',
|
|
453
|
+
'Read(**/secrets/**)',
|
|
454
|
+
];
|
|
393
455
|
for (const entry of denyEntries) {
|
|
394
456
|
if (!settings.permissions.deny.includes(entry)) {
|
|
395
457
|
settings.permissions.deny.push(entry);
|
|
@@ -418,6 +480,14 @@ function buildSecretsProtectionOperation(ctx, failedByKey) {
|
|
|
418
480
|
};
|
|
419
481
|
}
|
|
420
482
|
|
|
483
|
+
function buildEditorConfigOperation(ctx, failedByKey) {
|
|
484
|
+
if (ctx.fileContent('.editorconfig') !== null) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return buildSimpleCreateOperation('.editorconfig', buildEditorConfigTemplate(), ['editorconfig'], failedByKey);
|
|
489
|
+
}
|
|
490
|
+
|
|
421
491
|
function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
|
|
422
492
|
const ctx = new ProjectContext(dir);
|
|
423
493
|
const stacks = ctx.detectStacks(STACKS);
|
|
@@ -457,8 +527,10 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
|
|
|
457
527
|
failedByKey,
|
|
458
528
|
));
|
|
459
529
|
}
|
|
460
|
-
|
|
461
|
-
|
|
530
|
+
const gitIgnoreKeys = ['gitIgnoreEnv', 'gitIgnoreClaudeLocal', 'gitignoreClaudeLocal']
|
|
531
|
+
.filter((key) => customKeys.includes(key));
|
|
532
|
+
if (gitIgnoreKeys.length > 0) {
|
|
533
|
+
const gitIgnoreOperation = buildGitIgnoreOperation(ctx, failedByKey, gitIgnoreKeys);
|
|
462
534
|
if (gitIgnoreOperation) {
|
|
463
535
|
operations.push(gitIgnoreOperation);
|
|
464
536
|
}
|
|
@@ -469,6 +541,12 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
|
|
|
469
541
|
operations.push(secretsOperation);
|
|
470
542
|
}
|
|
471
543
|
}
|
|
544
|
+
if (customKeys.includes('editorconfig')) {
|
|
545
|
+
const editorconfigOperation = buildEditorConfigOperation(ctx, failedByKey);
|
|
546
|
+
if (editorconfigOperation) {
|
|
547
|
+
operations.push(editorconfigOperation);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
472
550
|
|
|
473
551
|
for (const key of templateKeys) {
|
|
474
552
|
operations.push({
|
|
@@ -486,6 +564,211 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
|
|
|
486
564
|
});
|
|
487
565
|
}
|
|
488
566
|
|
|
567
|
+
function isAuditAllowedPath(filePath) {
|
|
568
|
+
return AUDIT_FIX_ALLOWED_PATHS.has(String(filePath || '').replace(/\\/g, '/'));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function normalizeOperationForAudit(operation, failedByKey) {
|
|
572
|
+
if (!operation || operation.type !== 'file' || !isAuditAllowedPath(operation.path)) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const evidence = unique((operation.keys || []).map((key) => {
|
|
577
|
+
const failed = failedByKey.get(key);
|
|
578
|
+
const file = failed?.file || operation.path;
|
|
579
|
+
const line = Number.isFinite(failed?.line) ? failed.line : 1;
|
|
580
|
+
return `${key}|${file}|${line}`;
|
|
581
|
+
})).map((entry) => {
|
|
582
|
+
const [key, file, rawLine] = entry.split('|');
|
|
583
|
+
const failed = failedByKey.get(key) || {};
|
|
584
|
+
return {
|
|
585
|
+
key,
|
|
586
|
+
name: failed.name || key,
|
|
587
|
+
fix: failed.fix || null,
|
|
588
|
+
file: file || operation.path,
|
|
589
|
+
line: Number.isFinite(Number(rawLine)) ? Number(rawLine) : 1,
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
const summaryLocation = evidence[0]
|
|
594
|
+
? `${evidence[0].file}:${evidence[0].line}`
|
|
595
|
+
: `${operation.path}:1`;
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
...operation,
|
|
599
|
+
evidence,
|
|
600
|
+
summaryLocation,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function buildAuditFixPlan({ dir, platform, auditResult, targetKeys }) {
|
|
605
|
+
const failedResults = ((auditResult && auditResult.results) || []).filter((item) => item && item.passed === false);
|
|
606
|
+
const failedByKey = new Map(failedResults.map((item) => [item.key, item]));
|
|
607
|
+
const requestedKeys = unique(targetKeys).filter((key) => AUDIT_FIX_KEYS.has(key));
|
|
608
|
+
const filePlan = buildFixPlan({
|
|
609
|
+
dir,
|
|
610
|
+
platform,
|
|
611
|
+
auditResult,
|
|
612
|
+
targetKeys: requestedKeys,
|
|
613
|
+
})
|
|
614
|
+
.map((operation) => normalizeOperationForAudit(operation, failedByKey))
|
|
615
|
+
.filter(Boolean);
|
|
616
|
+
|
|
617
|
+
const plannedKeySet = new Set(filePlan.flatMap((operation) => operation.keys || []));
|
|
618
|
+
const advisoryOnly = failedResults
|
|
619
|
+
.filter((item) => !plannedKeySet.has(item.key))
|
|
620
|
+
.map((item) => ({
|
|
621
|
+
key: item.key,
|
|
622
|
+
name: item.name || item.key,
|
|
623
|
+
impact: item.impact || 'medium',
|
|
624
|
+
fix: item.fix || 'Manual fix required.',
|
|
625
|
+
file: item.file || null,
|
|
626
|
+
line: Number.isFinite(item.line) ? item.line : null,
|
|
627
|
+
}))
|
|
628
|
+
.sort(sortFailedResults);
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
requestedKeys,
|
|
632
|
+
plan: filePlan,
|
|
633
|
+
advisoryOnly,
|
|
634
|
+
failedByKey,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function trimTrailingEmptyLine(lines) {
|
|
639
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
640
|
+
return lines.slice(0, -1);
|
|
641
|
+
}
|
|
642
|
+
return lines;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function formatUnifiedDiff(operation) {
|
|
646
|
+
const beforeLines = trimTrailingEmptyLine(
|
|
647
|
+
operation.before === null ? [] : normalizeNewlines(operation.before).split('\n'),
|
|
648
|
+
);
|
|
649
|
+
const afterLines = trimTrailingEmptyLine(normalizeNewlines(operation.after).split('\n'));
|
|
650
|
+
|
|
651
|
+
let start = 0;
|
|
652
|
+
while (
|
|
653
|
+
start < beforeLines.length &&
|
|
654
|
+
start < afterLines.length &&
|
|
655
|
+
beforeLines[start] === afterLines[start]
|
|
656
|
+
) {
|
|
657
|
+
start += 1;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let endBefore = beforeLines.length - 1;
|
|
661
|
+
let endAfter = afterLines.length - 1;
|
|
662
|
+
while (
|
|
663
|
+
endBefore >= start &&
|
|
664
|
+
endAfter >= start &&
|
|
665
|
+
beforeLines[endBefore] === afterLines[endAfter]
|
|
666
|
+
) {
|
|
667
|
+
endBefore -= 1;
|
|
668
|
+
endAfter -= 1;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const removed = beforeLines.slice(start, endBefore + 1);
|
|
672
|
+
const added = afterLines.slice(start, endAfter + 1);
|
|
673
|
+
const oldStart = operation.before === null ? 0 : start + 1;
|
|
674
|
+
const oldCount = operation.before === null ? 0 : removed.length;
|
|
675
|
+
const newStart = start + 1;
|
|
676
|
+
const newCount = added.length;
|
|
677
|
+
|
|
678
|
+
return [
|
|
679
|
+
`diff --git a/${operation.path} b/${operation.path}`,
|
|
680
|
+
operation.action === 'create' ? 'new file mode 100644' : null,
|
|
681
|
+
`--- ${operation.before === null ? '/dev/null' : `a/${operation.path}`}`,
|
|
682
|
+
`+++ b/${operation.path}`,
|
|
683
|
+
`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
|
|
684
|
+
...removed.map((line) => `-${line}`),
|
|
685
|
+
...added.map((line) => `+${line}`),
|
|
686
|
+
].filter(Boolean).join('\n');
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function renderAuditFixPatch(plan) {
|
|
690
|
+
if (!Array.isArray(plan) || plan.length === 0) {
|
|
691
|
+
return '';
|
|
692
|
+
}
|
|
693
|
+
return `${plan.map((operation) => formatUnifiedDiff(operation)).join('\n\n')}\n`;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function resolvePatchOutputPath(dir, outputPath) {
|
|
697
|
+
if (outputPath === '-') {
|
|
698
|
+
return '-';
|
|
699
|
+
}
|
|
700
|
+
if (outputPath) {
|
|
701
|
+
return path.isAbsolute(outputPath) ? outputPath : path.join(dir, outputPath);
|
|
702
|
+
}
|
|
703
|
+
return path.join(dir, 'audit-fix.patch');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function writeAuditFixPatch({ dir, outputPath, patch, logger }) {
|
|
707
|
+
const targetPath = resolvePatchOutputPath(dir, outputPath);
|
|
708
|
+
if (targetPath === '-') {
|
|
709
|
+
logger.log('');
|
|
710
|
+
logger.log(patch.trimEnd());
|
|
711
|
+
logger.log('');
|
|
712
|
+
return {
|
|
713
|
+
filePath: null,
|
|
714
|
+
relativePath: 'stdout',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
719
|
+
fs.writeFileSync(targetPath, patch, 'utf8');
|
|
720
|
+
return {
|
|
721
|
+
filePath: targetPath,
|
|
722
|
+
relativePath: path.relative(dir, targetPath),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function formatAuditFixSummary(plan) {
|
|
727
|
+
return plan.map((operation) => {
|
|
728
|
+
const status = operation.action === 'create' ? 'A ' : 'M ';
|
|
729
|
+
const keys = (operation.keys || []).join(', ');
|
|
730
|
+
return ` ${status} ${operation.path} (${operation.summaryLocation}) [${keys}]`;
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function formatAdvisoryItem(item) {
|
|
735
|
+
const where = item.file ? `${item.file}${item.line ? `:${item.line}` : ''}` : 'repo-level';
|
|
736
|
+
return ` - ${item.key} (${item.impact}) at ${where}: ${item.fix}`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function runGit(args, dir) {
|
|
740
|
+
return spawnSync('git', args, {
|
|
741
|
+
cwd: dir,
|
|
742
|
+
encoding: 'utf8',
|
|
743
|
+
timeout: 30000,
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function createAuditFixBranch(dir) {
|
|
748
|
+
const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '').toLowerCase();
|
|
749
|
+
const branchName = `nerviq/autofix-${stamp}`;
|
|
750
|
+
let result = runGit(['switch', '-c', branchName], dir);
|
|
751
|
+
if (result.status !== 0) {
|
|
752
|
+
result = runGit(['checkout', '-b', branchName], dir);
|
|
753
|
+
}
|
|
754
|
+
if (result.status !== 0) {
|
|
755
|
+
throw new Error(result.stderr || result.stdout || 'Failed to create autofix branch.');
|
|
756
|
+
}
|
|
757
|
+
return branchName;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function stageAuditFixFiles(dir, plan, patchPath) {
|
|
761
|
+
const paths = unique([
|
|
762
|
+
...plan.map((operation) => operation.path),
|
|
763
|
+
patchPath && patchPath !== 'stdout' ? patchPath : null,
|
|
764
|
+
]);
|
|
765
|
+
if (paths.length === 0) return;
|
|
766
|
+
const result = runGit(['add', '--', ...paths], dir);
|
|
767
|
+
if (result.status !== 0) {
|
|
768
|
+
throw new Error(result.stderr || result.stdout || 'Failed to stage autofix files.');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
489
772
|
function formatDiff(filePath, before, after) {
|
|
490
773
|
const beforeLines = before === null ? [] : normalizeNewlines(before).split('\n');
|
|
491
774
|
const afterLines = normalizeNewlines(after).split('\n');
|
|
@@ -773,11 +1056,231 @@ async function applyFixes({
|
|
|
773
1056
|
};
|
|
774
1057
|
}
|
|
775
1058
|
|
|
1059
|
+
async function runAuditFixWorkflow({
|
|
1060
|
+
dir,
|
|
1061
|
+
platform,
|
|
1062
|
+
auditResult,
|
|
1063
|
+
targetKeys,
|
|
1064
|
+
auto = false,
|
|
1065
|
+
apply = false,
|
|
1066
|
+
pr = false,
|
|
1067
|
+
outputPath = null,
|
|
1068
|
+
logger = console,
|
|
1069
|
+
}) {
|
|
1070
|
+
const { requestedKeys, plan, advisoryOnly, failedByKey } = buildAuditFixPlan({
|
|
1071
|
+
dir,
|
|
1072
|
+
platform,
|
|
1073
|
+
auditResult,
|
|
1074
|
+
targetKeys,
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
if (requestedKeys.length === 0 || plan.length === 0) {
|
|
1078
|
+
logger.log('');
|
|
1079
|
+
logger.log(' No deterministic audit autofixes are available for this repo.');
|
|
1080
|
+
if (advisoryOnly.length > 0) {
|
|
1081
|
+
logger.log(' Advisory only — manual fix required:');
|
|
1082
|
+
for (const item of advisoryOnly.slice(0, 12)) {
|
|
1083
|
+
logger.log(formatAdvisoryItem(item));
|
|
1084
|
+
}
|
|
1085
|
+
if (advisoryOnly.length > 12) {
|
|
1086
|
+
logger.log(` ... and ${advisoryOnly.length - 12} more advisory findings.`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
logger.log('');
|
|
1090
|
+
return {
|
|
1091
|
+
exitCode: 2,
|
|
1092
|
+
requestedKeys,
|
|
1093
|
+
plan,
|
|
1094
|
+
advisoryOnly,
|
|
1095
|
+
patchArtifact: null,
|
|
1096
|
+
rollbackArtifact: null,
|
|
1097
|
+
reAudit: auditResult,
|
|
1098
|
+
unresolvedKeys: [],
|
|
1099
|
+
branchName: null,
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (apply && !auto && !pr) {
|
|
1104
|
+
logger.error('\n Error: `nerviq audit --fix --apply` requires `--auto`.\n');
|
|
1105
|
+
return {
|
|
1106
|
+
exitCode: 2,
|
|
1107
|
+
requestedKeys,
|
|
1108
|
+
plan,
|
|
1109
|
+
advisoryOnly,
|
|
1110
|
+
patchArtifact: null,
|
|
1111
|
+
rollbackArtifact: null,
|
|
1112
|
+
reAudit: auditResult,
|
|
1113
|
+
unresolvedKeys: [],
|
|
1114
|
+
branchName: null,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const patch = renderAuditFixPatch(plan);
|
|
1119
|
+
const patchArtifact = writeAuditFixPatch({
|
|
1120
|
+
dir,
|
|
1121
|
+
outputPath,
|
|
1122
|
+
patch,
|
|
1123
|
+
logger,
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
logger.log('');
|
|
1127
|
+
logger.log(' Audit autofix plan');
|
|
1128
|
+
logger.log(' ═══════════════════════════════════════');
|
|
1129
|
+
for (const line of formatAuditFixSummary(plan)) {
|
|
1130
|
+
logger.log(line);
|
|
1131
|
+
}
|
|
1132
|
+
logger.log('');
|
|
1133
|
+
logger.log(` Patch: ${patchArtifact.relativePath}`);
|
|
1134
|
+
if (advisoryOnly.length > 0) {
|
|
1135
|
+
logger.log('');
|
|
1136
|
+
logger.log(' Advisory only — manual fix required:');
|
|
1137
|
+
for (const item of advisoryOnly.slice(0, 12)) {
|
|
1138
|
+
logger.log(formatAdvisoryItem(item));
|
|
1139
|
+
}
|
|
1140
|
+
if (advisoryOnly.length > 12) {
|
|
1141
|
+
logger.log(` ... and ${advisoryOnly.length - 12} more advisory findings.`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
if (!apply && !pr) {
|
|
1146
|
+
logger.log('');
|
|
1147
|
+
logger.log(' Dry run complete. No files were written.');
|
|
1148
|
+
logger.log(' Run `nerviq audit --fix --apply --auto` to apply these changes.');
|
|
1149
|
+
logger.log(' Run `nerviq audit --fix --pr` to create a local autofix branch and stage the files.');
|
|
1150
|
+
logger.log('');
|
|
1151
|
+
return {
|
|
1152
|
+
exitCode: 0,
|
|
1153
|
+
requestedKeys,
|
|
1154
|
+
plan,
|
|
1155
|
+
advisoryOnly,
|
|
1156
|
+
patchArtifact,
|
|
1157
|
+
rollbackArtifact: null,
|
|
1158
|
+
reAudit: auditResult,
|
|
1159
|
+
unresolvedKeys: [],
|
|
1160
|
+
branchName: null,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
let branchName = null;
|
|
1165
|
+
const createdFiles = [];
|
|
1166
|
+
const patchedFiles = [];
|
|
1167
|
+
const warnings = [];
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
if (pr) {
|
|
1171
|
+
const repoCheck = runGit(['rev-parse', '--is-inside-work-tree'], dir);
|
|
1172
|
+
if (repoCheck.status !== 0) {
|
|
1173
|
+
throw new Error('`--pr` requires a git repository.');
|
|
1174
|
+
}
|
|
1175
|
+
branchName = createAuditFixBranch(dir);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
for (const operation of plan) {
|
|
1179
|
+
if (hasDoNotAutoEditMarker(operation.before)) {
|
|
1180
|
+
const warning = `Skipped ${operation.path}: DO NOT AUTOEDIT marker found.`;
|
|
1181
|
+
warnings.push(warning);
|
|
1182
|
+
logger.warn(` Warning: ${warning}`);
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (!isAuditAllowedPath(operation.path)) {
|
|
1187
|
+
const warning = `Skipped ${operation.path}: outside audit autofix allowlist.`;
|
|
1188
|
+
warnings.push(warning);
|
|
1189
|
+
logger.warn(` Warning: ${warning}`);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const fullPath = path.join(dir, operation.path);
|
|
1194
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
1195
|
+
fs.writeFileSync(fullPath, operation.after, 'utf8');
|
|
1196
|
+
if (operation.action === 'create') {
|
|
1197
|
+
createdFiles.push(operation.path);
|
|
1198
|
+
} else {
|
|
1199
|
+
patchedFiles.push({ path: operation.path, previousContent: operation.before });
|
|
1200
|
+
}
|
|
1201
|
+
logger.log(` Applied ${operation.action === 'create' ? 'create' : 'patch'}: ${operation.path}`);
|
|
1202
|
+
}
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
const rollbackArtifact = createRollbackArtifact(dir, createdFiles, patchedFiles, 'audit-fix');
|
|
1205
|
+
logger.error(`\n Error: ${error.message}\n`);
|
|
1206
|
+
return {
|
|
1207
|
+
exitCode: 1,
|
|
1208
|
+
requestedKeys,
|
|
1209
|
+
plan,
|
|
1210
|
+
advisoryOnly,
|
|
1211
|
+
patchArtifact,
|
|
1212
|
+
rollbackArtifact,
|
|
1213
|
+
reAudit: auditResult,
|
|
1214
|
+
unresolvedKeys: requestedKeys,
|
|
1215
|
+
branchName,
|
|
1216
|
+
warnings,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const rollbackArtifact = createRollbackArtifact(dir, createdFiles, patchedFiles, 'audit-fix');
|
|
1221
|
+
const reAudit = await audit({ dir, platform, silent: true });
|
|
1222
|
+
const unresolvedKeys = requestedKeys.filter((key) => {
|
|
1223
|
+
const planned = plan.some((operation) => (operation.keys || []).includes(key));
|
|
1224
|
+
if (!planned) return false;
|
|
1225
|
+
const failed = failedByKey.get(key);
|
|
1226
|
+
if (!failed) return false;
|
|
1227
|
+
const check = (reAudit.results || []).find((item) => item.key === key);
|
|
1228
|
+
return !check || check.passed !== true;
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
if (pr) {
|
|
1232
|
+
stageAuditFixFiles(dir, plan, patchArtifact.relativePath);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
logger.log('');
|
|
1236
|
+
logger.log(` Re-audit score: ${auditResult.score} -> ${reAudit.score}`);
|
|
1237
|
+
if (rollbackArtifact) {
|
|
1238
|
+
logger.log(` Rollback: ${rollbackArtifact.relativePath}`);
|
|
1239
|
+
}
|
|
1240
|
+
if (branchName) {
|
|
1241
|
+
logger.log(` Branch: ${branchName}`);
|
|
1242
|
+
logger.log(' Files are staged for review.');
|
|
1243
|
+
}
|
|
1244
|
+
if (unresolvedKeys.length > 0) {
|
|
1245
|
+
logger.log(` Unresolved targeted checks: ${unresolvedKeys.join(', ')}`);
|
|
1246
|
+
logger.log('');
|
|
1247
|
+
return {
|
|
1248
|
+
exitCode: 1,
|
|
1249
|
+
requestedKeys,
|
|
1250
|
+
plan,
|
|
1251
|
+
advisoryOnly,
|
|
1252
|
+
patchArtifact,
|
|
1253
|
+
rollbackArtifact,
|
|
1254
|
+
reAudit,
|
|
1255
|
+
unresolvedKeys,
|
|
1256
|
+
branchName,
|
|
1257
|
+
warnings,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
logger.log(' Audit autofix completed successfully.');
|
|
1262
|
+
logger.log('');
|
|
1263
|
+
return {
|
|
1264
|
+
exitCode: 0,
|
|
1265
|
+
requestedKeys,
|
|
1266
|
+
plan,
|
|
1267
|
+
advisoryOnly,
|
|
1268
|
+
patchArtifact,
|
|
1269
|
+
rollbackArtifact,
|
|
1270
|
+
reAudit,
|
|
1271
|
+
unresolvedKeys,
|
|
1272
|
+
branchName,
|
|
1273
|
+
warnings,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
776
1277
|
module.exports = {
|
|
777
1278
|
AUDIT_FIX_KEYS,
|
|
778
1279
|
CUSTOM_FIXER_KEYS,
|
|
779
1280
|
applyFixes,
|
|
780
1281
|
buildFixPlan,
|
|
1282
|
+
buildAuditFixPlan,
|
|
781
1283
|
getFixableFailedResults,
|
|
782
1284
|
isFixableKey,
|
|
1285
|
+
runAuditFixWorkflow,
|
|
783
1286
|
};
|