@nerviq/cli 1.20.1 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -23
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/activity.js +1039 -1039
- package/src/adoption-advisor.js +299 -299
- package/src/aider/config-parser.js +166 -166
- package/src/aider/context.js +4 -1
- package/src/aider/deep-review.js +316 -316
- package/src/aider/domain-packs.js +303 -303
- package/src/aider/freshness.js +93 -93
- package/src/aider/governance.js +253 -253
- package/src/aider/interactive.js +334 -334
- package/src/aider/mcp-packs.js +329 -329
- package/src/aider/patch.js +214 -214
- package/src/aider/plans.js +186 -186
- package/src/aider/premium.js +360 -360
- package/src/aider/setup.js +404 -404
- package/src/aider/techniques.js +312 -67
- package/src/analyze.js +951 -951
- package/src/anti-patterns.js +485 -485
- package/src/audit/instruction-files.js +180 -180
- package/src/audit/recommendations.js +577 -577
- package/src/auto-suggest.js +154 -154
- package/src/badge.js +13 -13
- package/src/behavioral-drift.js +801 -801
- package/src/benchmark.js +67 -67
- package/src/catalog.js +103 -103
- package/src/certification.js +128 -128
- package/src/codex/config-parser.js +183 -183
- package/src/codex/context.js +223 -223
- package/src/codex/deep-review.js +493 -493
- package/src/codex/domain-packs.js +394 -394
- package/src/codex/freshness.js +84 -84
- package/src/codex/governance.js +192 -192
- package/src/codex/interactive.js +618 -618
- package/src/codex/mcp-packs.js +914 -914
- package/src/codex/patch.js +209 -209
- package/src/codex/plans.js +251 -251
- package/src/codex/premium.js +614 -614
- package/src/codex/setup.js +591 -591
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/deep-review.js +346 -346
- package/src/copilot/domain-packs.js +372 -372
- package/src/copilot/freshness.js +57 -57
- package/src/copilot/governance.js +222 -222
- package/src/copilot/interactive.js +406 -406
- package/src/copilot/mcp-packs.js +826 -826
- package/src/copilot/plans.js +253 -253
- package/src/copilot/premium.js +451 -451
- package/src/copilot/setup.js +488 -488
- package/src/cost-tracking.js +61 -61
- package/src/cursor/activity.js +301 -301
- package/src/cursor/config-parser.js +265 -265
- package/src/cursor/context.js +256 -256
- package/src/cursor/deep-review.js +334 -334
- package/src/cursor/domain-packs.js +368 -368
- package/src/cursor/freshness.js +65 -65
- package/src/cursor/governance.js +229 -229
- package/src/cursor/interactive.js +391 -391
- package/src/cursor/mcp-packs.js +828 -828
- package/src/cursor/plans.js +254 -254
- package/src/cursor/premium.js +469 -469
- package/src/cursor/setup.js +488 -488
- package/src/dashboard.js +493 -493
- package/src/deep-review.js +428 -428
- package/src/deprecation.js +98 -98
- package/src/diff-only.js +280 -280
- package/src/doctor.js +119 -119
- package/src/domain-pack-expansion.js +1033 -1033
- package/src/domain-packs.js +387 -387
- package/src/feedback.js +178 -178
- package/src/fix-engine.js +783 -783
- package/src/fix-prompts.js +122 -122
- package/src/formatters/sarif.js +115 -115
- package/src/freshness.js +74 -74
- package/src/gemini/config-parser.js +275 -275
- package/src/gemini/deep-review.js +559 -559
- package/src/gemini/domain-packs.js +393 -393
- package/src/gemini/freshness.js +66 -66
- package/src/gemini/governance.js +201 -201
- package/src/gemini/interactive.js +860 -860
- package/src/gemini/mcp-packs.js +915 -915
- package/src/gemini/plans.js +269 -269
- package/src/gemini/premium.js +760 -760
- package/src/gemini/setup.js +692 -692
- package/src/governance.js +72 -72
- package/src/harmony/add.js +68 -68
- package/src/harmony/advisor.js +333 -333
- package/src/harmony/canon.js +565 -565
- package/src/harmony/cli.js +591 -591
- package/src/harmony/drift.js +401 -401
- package/src/harmony/governance.js +313 -313
- package/src/harmony/memory.js +239 -239
- package/src/harmony/sync.js +475 -475
- package/src/harmony/watch.js +370 -370
- package/src/hook-validation.js +342 -342
- package/src/index.js +271 -271
- package/src/init.js +184 -184
- package/src/instruction-surfaces.js +185 -185
- package/src/integrations.js +144 -144
- package/src/interactive.js +118 -118
- package/src/locales/en.json +1 -1
- package/src/locales/es.json +1 -1
- package/src/mcp-packs.js +830 -830
- package/src/mcp-server.js +726 -726
- package/src/mcp-validation.js +337 -337
- package/src/nerviq-sync.json +7 -7
- package/src/opencode/config-parser.js +109 -109
- package/src/opencode/context.js +247 -247
- package/src/opencode/deep-review.js +313 -313
- package/src/opencode/domain-packs.js +262 -262
- package/src/opencode/freshness.js +66 -66
- package/src/opencode/governance.js +159 -159
- package/src/opencode/interactive.js +392 -392
- package/src/opencode/mcp-packs.js +705 -705
- package/src/opencode/patch.js +184 -184
- package/src/opencode/plans.js +231 -231
- package/src/opencode/premium.js +413 -413
- package/src/opencode/setup.js +449 -449
- package/src/opencode/techniques.js +27 -27
- package/src/operating-profile.js +574 -574
- package/src/org.js +152 -152
- package/src/permission-rules.js +218 -218
- package/src/plans.js +839 -839
- package/src/platform-change-manifest.js +86 -86
- package/src/plugins.js +110 -110
- package/src/policy-layers.js +210 -210
- package/src/profiles.js +124 -124
- package/src/prompt-injection.js +74 -74
- package/src/public-api.js +173 -173
- package/src/recommendation-rules.js +84 -84
- package/src/repo-archetype.js +386 -386
- package/src/secret-patterns.js +39 -39
- package/src/server.js +527 -527
- package/src/setup/analysis.js +607 -607
- package/src/setup/runtime.js +172 -172
- package/src/setup.js +677 -677
- package/src/shared/capabilities.js +194 -194
- package/src/source-urls.js +132 -132
- package/src/stack-checks.js +565 -565
- package/src/supplemental-checks.js +13 -13
- package/src/synergy/adaptive.js +261 -261
- package/src/synergy/compensation.js +137 -137
- package/src/synergy/evidence.js +193 -193
- package/src/synergy/learning.js +199 -199
- package/src/synergy/patterns.js +227 -227
- package/src/synergy/ranking.js +83 -83
- package/src/synergy/report.js +165 -165
- package/src/synergy/routing.js +146 -146
- package/src/techniques/api.js +407 -407
- package/src/techniques/automation.js +316 -316
- package/src/techniques/compliance.js +257 -257
- package/src/techniques/hygiene.js +294 -294
- package/src/techniques/instructions.js +243 -243
- package/src/techniques/observability.js +226 -226
- package/src/techniques/optimization.js +142 -142
- package/src/techniques/quality.js +318 -318
- package/src/techniques/security.js +237 -237
- package/src/techniques/shared.js +443 -443
- package/src/techniques/stacks.js +2294 -2294
- package/src/techniques/tools.js +106 -106
- package/src/techniques/workflow.js +413 -413
- package/src/techniques.js +81 -81
- package/src/terminology.js +73 -73
- package/src/token-estimate.js +35 -35
- package/src/usage-patterns.js +99 -99
- package/src/verification-metadata.js +145 -145
- package/src/watch.js +247 -247
- package/src/windsurf/activity.js +302 -302
- package/src/windsurf/config-parser.js +267 -267
- package/src/windsurf/deep-review.js +337 -337
- package/src/windsurf/domain-packs.js +370 -370
- package/src/windsurf/freshness.js +36 -36
- package/src/windsurf/governance.js +231 -231
- package/src/windsurf/interactive.js +388 -388
- package/src/windsurf/mcp-packs.js +792 -792
- package/src/windsurf/plans.js +247 -247
- package/src/windsurf/premium.js +468 -468
- package/src/windsurf/setup.js +471 -471
- package/src/workspace.js +375 -375
package/src/diff-only.js
CHANGED
|
@@ -1,280 +1,280 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { spawnSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
const COLORS = {
|
|
7
|
-
reset: '\x1b[0m',
|
|
8
|
-
bold: '\x1b[1m',
|
|
9
|
-
dim: '\x1b[2m',
|
|
10
|
-
red: '\x1b[31m',
|
|
11
|
-
green: '\x1b[32m',
|
|
12
|
-
yellow: '\x1b[33m',
|
|
13
|
-
blue: '\x1b[36m',
|
|
14
|
-
magenta: '\x1b[35m',
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
18
|
-
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
19
|
-
|
|
20
|
-
const CATEGORY_HINTS = [
|
|
21
|
-
{
|
|
22
|
-
pattern: /^(CLAUDE\.md|\.claude\/)/i,
|
|
23
|
-
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'quality'],
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
pattern: /^(AGENTS\.md|\.codex\/)/i,
|
|
27
|
-
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'local'],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
pattern: /^(GEMINI\.md|\.gemini\/)/i,
|
|
31
|
-
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools'],
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
pattern: /^(\.cursor\/|\.cursorrules$)/i,
|
|
35
|
-
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
pattern: /^(\.windsurf\/|\.windsurfrules$|\.cascadeignore$)/i,
|
|
39
|
-
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
pattern: /^(\.github\/copilot-instructions\.md|\.github\/instructions\/|\.github\/prompts\/|\.vscode\/mcp\.json$)/i,
|
|
43
|
-
categories: ['workflow', 'prompting', 'tools', 'review', 'devops'],
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
pattern: /^(opencode\.jsonc?$|\.opencode\/)/i,
|
|
47
|
-
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'tools', 'review'],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
pattern: /^\.mcp\.json$/i,
|
|
51
|
-
categories: ['tools', 'security'],
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
pattern: /^(\.gitignore$|package\.json$|pyproject\.toml$|requirements.*\.txt$|\.github\/workflows\/)/i,
|
|
55
|
-
categories: ['hygiene', 'devops', 'security', 'quality', 'tools'],
|
|
56
|
-
},
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
function c(text, color) {
|
|
60
|
-
return `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function normalizeRelativePath(filePath) {
|
|
64
|
-
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function runGit(dir, args) {
|
|
68
|
-
return spawnSync('git', args, {
|
|
69
|
-
cwd: dir,
|
|
70
|
-
encoding: 'utf8',
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function collectOutputLines(result) {
|
|
75
|
-
return `${result.stdout || ''}`
|
|
76
|
-
.split(/\r?\n/)
|
|
77
|
-
.map((line) => normalizeRelativePath(line.trim()))
|
|
78
|
-
.filter(Boolean);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function uniquePaths(paths) {
|
|
82
|
-
return [...new Set((paths || []).map(normalizeRelativePath).filter(Boolean))].sort();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getRangeOptions(options = {}) {
|
|
86
|
-
const base = options.diffBase ||
|
|
87
|
-
process.env.NERVIQ_DIFF_BASE ||
|
|
88
|
-
process.env.GITHUB_BASE_SHA ||
|
|
89
|
-
process.env.GIT_BASE_SHA ||
|
|
90
|
-
null;
|
|
91
|
-
const head = options.diffHead ||
|
|
92
|
-
process.env.NERVIQ_DIFF_HEAD ||
|
|
93
|
-
process.env.GITHUB_SHA ||
|
|
94
|
-
process.env.GIT_HEAD_SHA ||
|
|
95
|
-
'HEAD';
|
|
96
|
-
return { base, head };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function getChangedFiles(dir, options = {}) {
|
|
100
|
-
const repoCheck = runGit(dir, ['rev-parse', '--is-inside-work-tree']);
|
|
101
|
-
if (repoCheck.status !== 0) {
|
|
102
|
-
return {
|
|
103
|
-
mode: 'unavailable',
|
|
104
|
-
changedFiles: [],
|
|
105
|
-
message: 'Diff-only mode requires a git repository.',
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const { base, head } = getRangeOptions(options);
|
|
110
|
-
if (base) {
|
|
111
|
-
const rangeResult = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', `${base}..${head}`]);
|
|
112
|
-
if (rangeResult.status === 0) {
|
|
113
|
-
return {
|
|
114
|
-
mode: 'range',
|
|
115
|
-
base,
|
|
116
|
-
head,
|
|
117
|
-
changedFiles: uniquePaths(collectOutputLines(rangeResult)),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const unstaged = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', 'HEAD', '--']);
|
|
123
|
-
const staged = runGit(dir, ['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB', '--']);
|
|
124
|
-
const untracked = runGit(dir, ['ls-files', '--others', '--exclude-standard']);
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
mode: 'working-tree',
|
|
128
|
-
changedFiles: uniquePaths([
|
|
129
|
-
...collectOutputLines(unstaged),
|
|
130
|
-
...collectOutputLines(staged),
|
|
131
|
-
...collectOutputLines(untracked),
|
|
132
|
-
]),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function matchesChangedFile(filePath, changedFiles) {
|
|
137
|
-
if (!filePath) return false;
|
|
138
|
-
const normalized = normalizeRelativePath(filePath).replace(/\/$/, '');
|
|
139
|
-
|
|
140
|
-
return changedFiles.some((changed) => {
|
|
141
|
-
const target = changed.replace(/\/$/, '');
|
|
142
|
-
return normalized === target ||
|
|
143
|
-
normalized.startsWith(`${target}/`) ||
|
|
144
|
-
target.startsWith(`${normalized}/`);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function inferRelevantCategories(changedFiles) {
|
|
149
|
-
const categories = new Set();
|
|
150
|
-
|
|
151
|
-
for (const filePath of changedFiles) {
|
|
152
|
-
for (const hint of CATEGORY_HINTS) {
|
|
153
|
-
if (hint.pattern.test(filePath)) {
|
|
154
|
-
hint.categories.forEach((category) => categories.add(category));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return categories;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function isRelevantResult(result, changedFiles, changedCategories) {
|
|
163
|
-
if (!result || result.deprecated) return false;
|
|
164
|
-
if (matchesChangedFile(result.file, changedFiles)) return true;
|
|
165
|
-
if (!result.file && result.category && changedCategories.has(result.category)) return true;
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function buildFallbackActions(failed) {
|
|
170
|
-
return failed
|
|
171
|
-
.slice()
|
|
172
|
-
.sort((a, b) => (IMPACT_ORDER[b.impact] || 0) - (IMPACT_ORDER[a.impact] || 0) || a.name.localeCompare(b.name))
|
|
173
|
-
.slice(0, 5)
|
|
174
|
-
.map((item) => ({
|
|
175
|
-
key: item.key,
|
|
176
|
-
name: item.name,
|
|
177
|
-
impact: item.impact,
|
|
178
|
-
category: item.category,
|
|
179
|
-
fix: item.fix,
|
|
180
|
-
file: item.file || null,
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function buildDiffOnlyAuditView(auditResult, diffInfo) {
|
|
185
|
-
const changedFiles = uniquePaths(diffInfo.changedFiles || []);
|
|
186
|
-
const changedCategories = inferRelevantCategories(changedFiles);
|
|
187
|
-
const relevantResults = (auditResult.results || []).filter((item) => isRelevantResult(item, changedFiles, changedCategories));
|
|
188
|
-
const applicable = relevantResults.filter((item) => item.passed !== null && item.deprecated !== true);
|
|
189
|
-
const skipped = relevantResults.filter((item) => item.passed === null && item.deprecated !== true);
|
|
190
|
-
const passed = applicable.filter((item) => item.passed === true);
|
|
191
|
-
const failed = applicable.filter((item) => item.passed === false);
|
|
192
|
-
const maxScore = applicable.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
193
|
-
const earnedScore = passed.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
194
|
-
const score = applicable.length > 0 && maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : null;
|
|
195
|
-
const relevantKeys = new Set(relevantResults.map((item) => item.key));
|
|
196
|
-
const topNextActions = (auditResult.topNextActions || []).filter((item) => relevantKeys.has(item.key)).slice(0, 5);
|
|
197
|
-
const fallbackActions = buildFallbackActions(failed);
|
|
198
|
-
|
|
199
|
-
const message = changedFiles.length === 0
|
|
200
|
-
? (diffInfo.message || 'No changed files detected for diff-only mode.')
|
|
201
|
-
: 'Diff-only mode filters to changed files plus linked governance/config surfaces. Run `nerviq audit` for complete repo posture.';
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
platform: auditResult.platform,
|
|
205
|
-
platformLabel: auditResult.platformLabel,
|
|
206
|
-
diffOnly: true,
|
|
207
|
-
score,
|
|
208
|
-
scoreType: 'diff-only changed-file audit',
|
|
209
|
-
passed: passed.length,
|
|
210
|
-
failed: failed.length,
|
|
211
|
-
skipped: skipped.length,
|
|
212
|
-
checkCount: applicable.length,
|
|
213
|
-
changedFiles,
|
|
214
|
-
changedFilesCount: changedFiles.length,
|
|
215
|
-
diffMode: diffInfo.mode,
|
|
216
|
-
diffBase: diffInfo.base || null,
|
|
217
|
-
diffHead: diffInfo.head || null,
|
|
218
|
-
fullAuditScore: auditResult.score,
|
|
219
|
-
message,
|
|
220
|
-
results: relevantResults,
|
|
221
|
-
topNextActions: topNextActions.length > 0 ? topNextActions : fallbackActions,
|
|
222
|
-
suggestedNextCommand: 'nerviq audit',
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function printDiffOnlyAudit(result) {
|
|
227
|
-
const lines = [''];
|
|
228
|
-
lines.push(c(' nerviq diff-only audit', 'bold'));
|
|
229
|
-
lines.push(c(' ═══════════════════════════════════════', 'dim'));
|
|
230
|
-
lines.push('');
|
|
231
|
-
|
|
232
|
-
if (result.diffMode === 'range' && result.diffBase) {
|
|
233
|
-
lines.push(c(` Diff range: ${result.diffBase}..${result.diffHead}`, 'dim'));
|
|
234
|
-
} else {
|
|
235
|
-
lines.push(c(' Diff source: working tree vs HEAD', 'dim'));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (result.changedFilesCount === 0) {
|
|
239
|
-
lines.push(c(` ${result.message}`, 'yellow'));
|
|
240
|
-
lines.push(c(' Tip: commit or stage a change first, or provide a PR base SHA via --diff-base.', 'dim'));
|
|
241
|
-
lines.push('');
|
|
242
|
-
return lines.join('\n');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
lines.push(c(` Changed files (${result.changedFilesCount}):`, 'bold'));
|
|
246
|
-
result.changedFiles.slice(0, 8).forEach((filePath) => {
|
|
247
|
-
lines.push(` ${c('•', 'blue')} ${filePath}`);
|
|
248
|
-
});
|
|
249
|
-
if (result.changedFilesCount > 8) {
|
|
250
|
-
lines.push(c(` ...and ${result.changedFilesCount - 8} more`, 'dim'));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
lines.push('');
|
|
254
|
-
lines.push(` Score: ${result.score === null ? c('n/a', 'yellow') : c(`${result.score}/100`, 'bold')}`);
|
|
255
|
-
lines.push(c(` Score type: ${result.scoreType} (not the full repo score of ${result.fullAuditScore}/100).`, 'dim'));
|
|
256
|
-
lines.push(c(` ${result.message}`, 'dim'));
|
|
257
|
-
lines.push('');
|
|
258
|
-
|
|
259
|
-
if (result.topNextActions && result.topNextActions.length > 0) {
|
|
260
|
-
lines.push(c(' Top diff-relevant actions:', 'magenta'));
|
|
261
|
-
result.topNextActions.slice(0, 5).forEach((item, index) => {
|
|
262
|
-
lines.push(` ${index + 1}. ${c(item.name, 'bold')} ${c(`[${item.impact}]`, 'dim')}`);
|
|
263
|
-
if (item.fix) {
|
|
264
|
-
lines.push(c(` ${item.fix}`, 'dim'));
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
lines.push('');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
lines.push(` Relevant checks: ${result.checkCount} | Failed: ${result.failed} | Passed: ${result.passed} | Skipped: ${result.skipped}`);
|
|
271
|
-
lines.push(c(' Run `nerviq audit` for the complete repo posture.', 'dim'));
|
|
272
|
-
lines.push('');
|
|
273
|
-
return lines.join('\n');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
module.exports = {
|
|
277
|
-
getChangedFiles,
|
|
278
|
-
buildDiffOnlyAuditView,
|
|
279
|
-
printDiffOnlyAudit,
|
|
280
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const COLORS = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bold: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
green: '\x1b[32m',
|
|
12
|
+
yellow: '\x1b[33m',
|
|
13
|
+
blue: '\x1b[36m',
|
|
14
|
+
magenta: '\x1b[35m',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const WEIGHTS = { critical: 15, high: 10, medium: 5, low: 2 };
|
|
18
|
+
const IMPACT_ORDER = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
19
|
+
|
|
20
|
+
const CATEGORY_HINTS = [
|
|
21
|
+
{
|
|
22
|
+
pattern: /^(CLAUDE\.md|\.claude\/)/i,
|
|
23
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'quality'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pattern: /^(AGENTS\.md|\.codex\/)/i,
|
|
27
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools', 'git', 'local'],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
pattern: /^(GEMINI\.md|\.gemini\/)/i,
|
|
31
|
+
categories: ['memory', 'workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'review', 'tools'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /^(\.cursor\/|\.cursorrules$)/i,
|
|
35
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /^(\.windsurf\/|\.windsurfrules$|\.cascadeignore$)/i,
|
|
39
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'tools', 'review'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
pattern: /^(\.github\/copilot-instructions\.md|\.github\/instructions\/|\.github\/prompts\/|\.vscode\/mcp\.json$)/i,
|
|
43
|
+
categories: ['workflow', 'prompting', 'tools', 'review', 'devops'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
pattern: /^(opencode\.jsonc?$|\.opencode\/)/i,
|
|
47
|
+
categories: ['workflow', 'security', 'automation', 'prompting', 'features', 'skills', 'agents', 'tools', 'review'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: /^\.mcp\.json$/i,
|
|
51
|
+
categories: ['tools', 'security'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /^(\.gitignore$|package\.json$|pyproject\.toml$|requirements.*\.txt$|\.github\/workflows\/)/i,
|
|
55
|
+
categories: ['hygiene', 'devops', 'security', 'quality', 'tools'],
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function c(text, color) {
|
|
60
|
+
return `${COLORS[color] || ''}${text}${COLORS.reset}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeRelativePath(filePath) {
|
|
64
|
+
return String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runGit(dir, args) {
|
|
68
|
+
return spawnSync('git', args, {
|
|
69
|
+
cwd: dir,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function collectOutputLines(result) {
|
|
75
|
+
return `${result.stdout || ''}`
|
|
76
|
+
.split(/\r?\n/)
|
|
77
|
+
.map((line) => normalizeRelativePath(line.trim()))
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function uniquePaths(paths) {
|
|
82
|
+
return [...new Set((paths || []).map(normalizeRelativePath).filter(Boolean))].sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getRangeOptions(options = {}) {
|
|
86
|
+
const base = options.diffBase ||
|
|
87
|
+
process.env.NERVIQ_DIFF_BASE ||
|
|
88
|
+
process.env.GITHUB_BASE_SHA ||
|
|
89
|
+
process.env.GIT_BASE_SHA ||
|
|
90
|
+
null;
|
|
91
|
+
const head = options.diffHead ||
|
|
92
|
+
process.env.NERVIQ_DIFF_HEAD ||
|
|
93
|
+
process.env.GITHUB_SHA ||
|
|
94
|
+
process.env.GIT_HEAD_SHA ||
|
|
95
|
+
'HEAD';
|
|
96
|
+
return { base, head };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getChangedFiles(dir, options = {}) {
|
|
100
|
+
const repoCheck = runGit(dir, ['rev-parse', '--is-inside-work-tree']);
|
|
101
|
+
if (repoCheck.status !== 0) {
|
|
102
|
+
return {
|
|
103
|
+
mode: 'unavailable',
|
|
104
|
+
changedFiles: [],
|
|
105
|
+
message: 'Diff-only mode requires a git repository.',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { base, head } = getRangeOptions(options);
|
|
110
|
+
if (base) {
|
|
111
|
+
const rangeResult = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', `${base}..${head}`]);
|
|
112
|
+
if (rangeResult.status === 0) {
|
|
113
|
+
return {
|
|
114
|
+
mode: 'range',
|
|
115
|
+
base,
|
|
116
|
+
head,
|
|
117
|
+
changedFiles: uniquePaths(collectOutputLines(rangeResult)),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const unstaged = runGit(dir, ['diff', '--name-only', '--diff-filter=ACMRTUXB', 'HEAD', '--']);
|
|
123
|
+
const staged = runGit(dir, ['diff', '--cached', '--name-only', '--diff-filter=ACMRTUXB', '--']);
|
|
124
|
+
const untracked = runGit(dir, ['ls-files', '--others', '--exclude-standard']);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
mode: 'working-tree',
|
|
128
|
+
changedFiles: uniquePaths([
|
|
129
|
+
...collectOutputLines(unstaged),
|
|
130
|
+
...collectOutputLines(staged),
|
|
131
|
+
...collectOutputLines(untracked),
|
|
132
|
+
]),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function matchesChangedFile(filePath, changedFiles) {
|
|
137
|
+
if (!filePath) return false;
|
|
138
|
+
const normalized = normalizeRelativePath(filePath).replace(/\/$/, '');
|
|
139
|
+
|
|
140
|
+
return changedFiles.some((changed) => {
|
|
141
|
+
const target = changed.replace(/\/$/, '');
|
|
142
|
+
return normalized === target ||
|
|
143
|
+
normalized.startsWith(`${target}/`) ||
|
|
144
|
+
target.startsWith(`${normalized}/`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function inferRelevantCategories(changedFiles) {
|
|
149
|
+
const categories = new Set();
|
|
150
|
+
|
|
151
|
+
for (const filePath of changedFiles) {
|
|
152
|
+
for (const hint of CATEGORY_HINTS) {
|
|
153
|
+
if (hint.pattern.test(filePath)) {
|
|
154
|
+
hint.categories.forEach((category) => categories.add(category));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return categories;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isRelevantResult(result, changedFiles, changedCategories) {
|
|
163
|
+
if (!result || result.deprecated) return false;
|
|
164
|
+
if (matchesChangedFile(result.file, changedFiles)) return true;
|
|
165
|
+
if (!result.file && result.category && changedCategories.has(result.category)) return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildFallbackActions(failed) {
|
|
170
|
+
return failed
|
|
171
|
+
.slice()
|
|
172
|
+
.sort((a, b) => (IMPACT_ORDER[b.impact] || 0) - (IMPACT_ORDER[a.impact] || 0) || a.name.localeCompare(b.name))
|
|
173
|
+
.slice(0, 5)
|
|
174
|
+
.map((item) => ({
|
|
175
|
+
key: item.key,
|
|
176
|
+
name: item.name,
|
|
177
|
+
impact: item.impact,
|
|
178
|
+
category: item.category,
|
|
179
|
+
fix: item.fix,
|
|
180
|
+
file: item.file || null,
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildDiffOnlyAuditView(auditResult, diffInfo) {
|
|
185
|
+
const changedFiles = uniquePaths(diffInfo.changedFiles || []);
|
|
186
|
+
const changedCategories = inferRelevantCategories(changedFiles);
|
|
187
|
+
const relevantResults = (auditResult.results || []).filter((item) => isRelevantResult(item, changedFiles, changedCategories));
|
|
188
|
+
const applicable = relevantResults.filter((item) => item.passed !== null && item.deprecated !== true);
|
|
189
|
+
const skipped = relevantResults.filter((item) => item.passed === null && item.deprecated !== true);
|
|
190
|
+
const passed = applicable.filter((item) => item.passed === true);
|
|
191
|
+
const failed = applicable.filter((item) => item.passed === false);
|
|
192
|
+
const maxScore = applicable.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
193
|
+
const earnedScore = passed.reduce((sum, item) => sum + (WEIGHTS[item.impact] || 5), 0);
|
|
194
|
+
const score = applicable.length > 0 && maxScore > 0 ? Math.round((earnedScore / maxScore) * 100) : null;
|
|
195
|
+
const relevantKeys = new Set(relevantResults.map((item) => item.key));
|
|
196
|
+
const topNextActions = (auditResult.topNextActions || []).filter((item) => relevantKeys.has(item.key)).slice(0, 5);
|
|
197
|
+
const fallbackActions = buildFallbackActions(failed);
|
|
198
|
+
|
|
199
|
+
const message = changedFiles.length === 0
|
|
200
|
+
? (diffInfo.message || 'No changed files detected for diff-only mode.')
|
|
201
|
+
: 'Diff-only mode filters to changed files plus linked governance/config surfaces. Run `nerviq audit` for complete repo posture.';
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
platform: auditResult.platform,
|
|
205
|
+
platformLabel: auditResult.platformLabel,
|
|
206
|
+
diffOnly: true,
|
|
207
|
+
score,
|
|
208
|
+
scoreType: 'diff-only changed-file audit',
|
|
209
|
+
passed: passed.length,
|
|
210
|
+
failed: failed.length,
|
|
211
|
+
skipped: skipped.length,
|
|
212
|
+
checkCount: applicable.length,
|
|
213
|
+
changedFiles,
|
|
214
|
+
changedFilesCount: changedFiles.length,
|
|
215
|
+
diffMode: diffInfo.mode,
|
|
216
|
+
diffBase: diffInfo.base || null,
|
|
217
|
+
diffHead: diffInfo.head || null,
|
|
218
|
+
fullAuditScore: auditResult.score,
|
|
219
|
+
message,
|
|
220
|
+
results: relevantResults,
|
|
221
|
+
topNextActions: topNextActions.length > 0 ? topNextActions : fallbackActions,
|
|
222
|
+
suggestedNextCommand: 'nerviq audit',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function printDiffOnlyAudit(result) {
|
|
227
|
+
const lines = [''];
|
|
228
|
+
lines.push(c(' nerviq diff-only audit', 'bold'));
|
|
229
|
+
lines.push(c(' ═══════════════════════════════════════', 'dim'));
|
|
230
|
+
lines.push('');
|
|
231
|
+
|
|
232
|
+
if (result.diffMode === 'range' && result.diffBase) {
|
|
233
|
+
lines.push(c(` Diff range: ${result.diffBase}..${result.diffHead}`, 'dim'));
|
|
234
|
+
} else {
|
|
235
|
+
lines.push(c(' Diff source: working tree vs HEAD', 'dim'));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (result.changedFilesCount === 0) {
|
|
239
|
+
lines.push(c(` ${result.message}`, 'yellow'));
|
|
240
|
+
lines.push(c(' Tip: commit or stage a change first, or provide a PR base SHA via --diff-base.', 'dim'));
|
|
241
|
+
lines.push('');
|
|
242
|
+
return lines.join('\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
lines.push(c(` Changed files (${result.changedFilesCount}):`, 'bold'));
|
|
246
|
+
result.changedFiles.slice(0, 8).forEach((filePath) => {
|
|
247
|
+
lines.push(` ${c('•', 'blue')} ${filePath}`);
|
|
248
|
+
});
|
|
249
|
+
if (result.changedFilesCount > 8) {
|
|
250
|
+
lines.push(c(` ...and ${result.changedFilesCount - 8} more`, 'dim'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push(` Score: ${result.score === null ? c('n/a', 'yellow') : c(`${result.score}/100`, 'bold')}`);
|
|
255
|
+
lines.push(c(` Score type: ${result.scoreType} (not the full repo score of ${result.fullAuditScore}/100).`, 'dim'));
|
|
256
|
+
lines.push(c(` ${result.message}`, 'dim'));
|
|
257
|
+
lines.push('');
|
|
258
|
+
|
|
259
|
+
if (result.topNextActions && result.topNextActions.length > 0) {
|
|
260
|
+
lines.push(c(' Top diff-relevant actions:', 'magenta'));
|
|
261
|
+
result.topNextActions.slice(0, 5).forEach((item, index) => {
|
|
262
|
+
lines.push(` ${index + 1}. ${c(item.name, 'bold')} ${c(`[${item.impact}]`, 'dim')}`);
|
|
263
|
+
if (item.fix) {
|
|
264
|
+
lines.push(c(` ${item.fix}`, 'dim'));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
lines.push('');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lines.push(` Relevant checks: ${result.checkCount} | Failed: ${result.failed} | Passed: ${result.passed} | Skipped: ${result.skipped}`);
|
|
271
|
+
lines.push(c(' Run `nerviq audit` for the complete repo posture.', 'dim'));
|
|
272
|
+
lines.push('');
|
|
273
|
+
return lines.join('\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
module.exports = {
|
|
277
|
+
getChangedFiles,
|
|
278
|
+
buildDiffOnlyAuditView,
|
|
279
|
+
printDiffOnlyAudit,
|
|
280
|
+
};
|