@nerviq/cli 1.10.0 → 1.12.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/README.md +176 -47
- package/bin/cli.js +842 -287
- package/package.json +2 -2
- package/src/activity.js +225 -59
- package/src/adoption-advisor.js +299 -0
- package/src/aider/freshness.js +28 -25
- package/src/aider/techniques.js +16 -11
- package/src/analyze.js +131 -1
- package/src/anti-patterns.js +17 -2
- package/src/audit.js +197 -96
- package/src/behavioral-drift.js +801 -0
- package/src/benchmark.js +15 -10
- package/src/continuous-ops.js +681 -0
- package/src/cost-tracking.js +61 -0
- package/src/cursor/techniques.js +17 -12
- package/src/deep-review.js +83 -0
- package/src/diff-only.js +280 -0
- package/src/doctor.js +118 -55
- package/src/governance.js +72 -50
- package/src/hook-validation.js +342 -0
- package/src/index.js +7 -1
- package/src/integrations.js +144 -60
- package/src/mcp-validation.js +337 -0
- package/src/opencode/techniques.js +12 -7
- package/src/operating-profile.js +574 -0
- package/src/org.js +97 -13
- package/src/permission-rules.js +218 -0
- package/src/plans.js +192 -8
- package/src/platform-change-manifest.js +86 -0
- package/src/policy-layers.js +210 -0
- package/src/profiles.js +4 -1
- package/src/prompt-injection.js +74 -0
- package/src/repo-archetype.js +386 -0
- package/src/secret-patterns.js +9 -0
- package/src/server.js +398 -3
- package/src/setup.js +36 -2
- package/src/source-urls.js +132 -132
- package/src/supplemental-checks.js +13 -12
- package/src/techniques/api.js +407 -0
- package/src/techniques/automation.js +316 -0
- package/src/techniques/compliance.js +257 -0
- package/src/techniques/hygiene.js +294 -0
- package/src/techniques/instructions.js +243 -0
- package/src/techniques/observability.js +226 -0
- package/src/techniques/optimization.js +142 -0
- package/src/techniques/quality.js +317 -0
- package/src/techniques/security.js +237 -0
- package/src/techniques/shared.js +443 -0
- package/src/techniques/stacks.js +2294 -0
- package/src/techniques/tools.js +106 -0
- package/src/techniques/workflow.js +413 -0
- package/src/techniques.js +78 -5611
- package/src/terminology.js +73 -0
- package/src/token-estimate.js +35 -0
- package/src/watch.js +18 -0
- package/src/windsurf/techniques.js +17 -12
- package/src/workspace.js +105 -8
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TERMINOLOGY = {
|
|
4
|
+
governance: {
|
|
5
|
+
label: 'governance',
|
|
6
|
+
description: 'the rollout safety layer: permissions, hooks, profiles, and policy packs',
|
|
7
|
+
},
|
|
8
|
+
hooks: {
|
|
9
|
+
label: 'hooks',
|
|
10
|
+
description: 'auto-run checks or scripts triggered before or after agent tool actions',
|
|
11
|
+
},
|
|
12
|
+
denyRules: {
|
|
13
|
+
label: 'deny rules',
|
|
14
|
+
description: 'explicit blocks for risky reads or commands like .env access or rm -rf',
|
|
15
|
+
},
|
|
16
|
+
mcp: {
|
|
17
|
+
label: 'MCP',
|
|
18
|
+
description: 'live external tool connectors for docs, APIs, databases, and other systems',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const TERM_ORDER = ['governance', 'hooks', 'denyRules', 'mcp'];
|
|
23
|
+
|
|
24
|
+
function normalizeTermKeys(keys = []) {
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
if (!TERMINOLOGY[key]) continue;
|
|
28
|
+
seen.add(key);
|
|
29
|
+
}
|
|
30
|
+
return TERM_ORDER.filter((key) => seen.has(key));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectAuditTerminology(result = {}) {
|
|
34
|
+
const terms = new Set();
|
|
35
|
+
const texts = [];
|
|
36
|
+
|
|
37
|
+
for (const item of result.topNextActions || []) {
|
|
38
|
+
texts.push(item.name || '', item.fix || '', item.why || '', item.module || '', ...(item.signals || []));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const item of result.results || []) {
|
|
42
|
+
if (item.passed === false) {
|
|
43
|
+
texts.push(item.key || '', item.name || '', item.fix || '', item.category || '');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const blob = texts.join('\n');
|
|
48
|
+
if (/\bhook/i.test(blob)) terms.add('hooks');
|
|
49
|
+
if (/\bdeny rules?\b|permissions?\.deny|bypasspermissions|\.env access|rm -rf/i.test(blob)) terms.add('denyRules');
|
|
50
|
+
if (/\bmcp\b|context7|external tool/i.test(blob)) terms.add('mcp');
|
|
51
|
+
if (/\bgovernance\b|policy pack|permission profile/i.test(blob) || terms.size > 0) terms.add('governance');
|
|
52
|
+
|
|
53
|
+
return normalizeTermKeys([...terms]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatTerminologyLines(keys, options = {}) {
|
|
57
|
+
const normalized = normalizeTermKeys(keys);
|
|
58
|
+
if (normalized.length === 0) return [];
|
|
59
|
+
const title = options.title || ' Terms used here:';
|
|
60
|
+
const indent = options.indent || ' ';
|
|
61
|
+
const bullet = options.bullet || '-';
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
title,
|
|
65
|
+
...normalized.map((key) => `${indent}${bullet} ${TERMINOLOGY[key].label}: ${TERMINOLOGY[key].description}`),
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
TERMINOLOGY,
|
|
71
|
+
collectAuditTerminology,
|
|
72
|
+
formatTerminologyLines,
|
|
73
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function splitIdentifierSegments(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
4
|
+
.replace(/[_./:-]+/g, ' ')
|
|
5
|
+
.split(/\s+/)
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function estimateSegmentTokens(segment) {
|
|
10
|
+
const charCount = [...String(segment || '')].length;
|
|
11
|
+
return Math.max(1, Math.ceil(charCount / 4));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function estimateTokenCount(text) {
|
|
15
|
+
if (typeof text !== 'string' || !text) return 0;
|
|
16
|
+
|
|
17
|
+
const parts = text.match(/[\p{L}\p{N}_]+|[^\s]/gu) || [];
|
|
18
|
+
let total = 0;
|
|
19
|
+
|
|
20
|
+
for (const part of parts) {
|
|
21
|
+
if (/^[\p{L}\p{N}_]+$/u.test(part)) {
|
|
22
|
+
const segments = splitIdentifierSegments(part);
|
|
23
|
+
total += segments.reduce((sum, segment) => sum + estimateSegmentTokens(segment), 0);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
total += 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return total;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
estimateTokenCount,
|
|
35
|
+
};
|
package/src/watch.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const { audit } = require('./audit');
|
|
10
|
+
const { detectPlatforms } = require('./public-api');
|
|
11
|
+
const { buildContinuousStatus, formatContinuousStatus } = require('./continuous-ops');
|
|
10
12
|
|
|
11
13
|
const COLORS = {
|
|
12
14
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
@@ -131,12 +133,14 @@ function closeWatchers(watchers) {
|
|
|
131
133
|
|
|
132
134
|
async function watch(options) {
|
|
133
135
|
const recursiveSupported = supportsNativeRecursiveWatch();
|
|
136
|
+
const watchMode = options.driftMode || 'watch';
|
|
134
137
|
|
|
135
138
|
console.log('');
|
|
136
139
|
console.log(c(' nerviq watch mode', 'bold'));
|
|
137
140
|
console.log(c(' ═══════════════════════════════════════', 'dim'));
|
|
138
141
|
console.log(c(` Watching: ${options.dir}`, 'dim'));
|
|
139
142
|
console.log(c(` Mode: ${recursiveSupported ? 'native recursive directories' : 'expanded directory fallback (cross-platform safe)'}`, 'dim'));
|
|
143
|
+
console.log(c(` Continuous mode: ${watchMode}`, 'dim'));
|
|
140
144
|
console.log(c(' Press Ctrl+C to stop', 'dim'));
|
|
141
145
|
console.log('');
|
|
142
146
|
|
|
@@ -147,6 +151,13 @@ async function watch(options) {
|
|
|
147
151
|
lastScore = result.score;
|
|
148
152
|
console.log(` ${c('Initial score:', 'bold')} ${scoreColor(result.score)}`);
|
|
149
153
|
console.log(` ${result.passed} / ${result.passed + result.failed} checks passing`);
|
|
154
|
+
const continuousStatus = buildContinuousStatus({
|
|
155
|
+
dir: options.dir,
|
|
156
|
+
auditResult: result,
|
|
157
|
+
mode: watchMode,
|
|
158
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
159
|
+
});
|
|
160
|
+
console.log(formatContinuousStatus(continuousStatus, { compact: true }));
|
|
150
161
|
console.log('');
|
|
151
162
|
} catch (e) {
|
|
152
163
|
console.log(c(` Initial audit failed: ${e.message}`, 'dim'));
|
|
@@ -184,8 +195,15 @@ async function watch(options) {
|
|
|
184
195
|
const result = await audit({ ...options, silent: true });
|
|
185
196
|
const delta = lastScore !== null ? result.score - lastScore : 0;
|
|
186
197
|
const arrow = delta > 0 ? c(`+${delta}`, 'green') : delta < 0 ? c(String(delta), 'yellow') : '';
|
|
198
|
+
const continuousStatus = buildContinuousStatus({
|
|
199
|
+
dir: options.dir,
|
|
200
|
+
auditResult: result,
|
|
201
|
+
mode: watchMode,
|
|
202
|
+
currentPlatforms: detectPlatforms(options.dir),
|
|
203
|
+
});
|
|
187
204
|
|
|
188
205
|
console.log(` Score: ${scoreColor(result.score)} ${arrow} (${result.passed}/${result.passed + result.failed} passing)`);
|
|
206
|
+
console.log(formatContinuousStatus(continuousStatus, { compact: true }));
|
|
189
207
|
|
|
190
208
|
if (lastScore !== null && result.score > lastScore) {
|
|
191
209
|
console.log(c(' Nice improvement!', 'green'));
|
|
@@ -26,11 +26,12 @@
|
|
|
26
26
|
const fs = require('fs');
|
|
27
27
|
const os = require('os');
|
|
28
28
|
const path = require('path');
|
|
29
|
-
const { WindsurfProjectContext } = require('./context');
|
|
30
|
-
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
31
|
-
const { attachSourceUrls } = require('../source-urls');
|
|
32
|
-
const { buildStackChecks } = require('../stack-checks');
|
|
33
|
-
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
29
|
+
const { WindsurfProjectContext } = require('./context');
|
|
30
|
+
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
31
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
32
|
+
const { buildStackChecks } = require('../stack-checks');
|
|
33
|
+
const { isApiProject, isDatabaseProject, isAuthProject, isMonitoringRelevant } = require('../supplemental-checks');
|
|
34
|
+
const { hasCostBudgetOrUsageTracking } = require('../cost-tracking');
|
|
34
35
|
const { tryParseJson, validateMcpEnvVars } = require('./config-parser');
|
|
35
36
|
|
|
36
37
|
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
@@ -2161,13 +2162,17 @@ const WINDSURF_TECHNIQUES = {
|
|
|
2161
2162
|
fix: 'Document prompt caching strategy to reduce Windsurf API costs.',
|
|
2162
2163
|
template: null, file: () => 'AGENTS.md', line: () => null,
|
|
2163
2164
|
},
|
|
2164
|
-
wsCostBudgetDefined: {
|
|
2165
|
-
id: 'WS-T48', name: 'AI cost budget or usage
|
|
2166
|
-
check: (ctx) => {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2165
|
+
wsCostBudgetDefined: {
|
|
2166
|
+
id: 'WS-T48', name: 'AI cost budget or per-run usage tracking documented',
|
|
2167
|
+
check: (ctx) => {
|
|
2168
|
+
const docs = (ctx.fileContent('AGENTS.md') || '') + (ctx.fileContent('README.md') || '');
|
|
2169
|
+
if (!docs.trim() && !hasCostBudgetOrUsageTracking('', ctx)) return null;
|
|
2170
|
+
return hasCostBudgetOrUsageTracking(docs, ctx);
|
|
2171
|
+
},
|
|
2172
|
+
impact: 'low', rating: 2, category: 'cost-optimization',
|
|
2173
|
+
fix: 'Document AI cost guardrails or per-run usage tracking so Windsurf usage is measurable over time.',
|
|
2174
|
+
template: null, file: () => 'README.md', line: () => null,
|
|
2175
|
+
},
|
|
2171
2176
|
|
|
2172
2177
|
// ============================================================
|
|
2173
2178
|
// === PYTHON STACK CHECKS (category: 'python') ===============
|
package/src/workspace.js
CHANGED
|
@@ -177,6 +177,104 @@ function summarizeAuditResult(result, scoreType, scope) {
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
function summarizeWorkspaceEntry(result, workspacePath, absPath, platform) {
|
|
181
|
+
const stackKeys = (result.stacks || []).map((item) => item.key);
|
|
182
|
+
const stackLabels = (result.stacks || []).map((item) => item.label);
|
|
183
|
+
return {
|
|
184
|
+
name: path.basename(workspacePath),
|
|
185
|
+
workspace: workspacePath,
|
|
186
|
+
dir: absPath,
|
|
187
|
+
platform,
|
|
188
|
+
stackKeys,
|
|
189
|
+
stackLabels,
|
|
190
|
+
workspaceProfile: classifyWorkspaceProfile(stackKeys),
|
|
191
|
+
...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function classifyWorkspaceProfile(stackKeys) {
|
|
196
|
+
const keys = new Set(Array.isArray(stackKeys) ? stackKeys : []);
|
|
197
|
+
const matchAny = (candidates) => candidates.some((candidate) => keys.has(candidate));
|
|
198
|
+
|
|
199
|
+
if (matchAny(['go'])) {
|
|
200
|
+
return { key: 'go-workspace', label: 'Go workspace' };
|
|
201
|
+
}
|
|
202
|
+
if (matchAny(['python', 'django', 'fastapi'])) {
|
|
203
|
+
return { key: 'python-workspace', label: 'Python workspace' };
|
|
204
|
+
}
|
|
205
|
+
if (matchAny(['dotnet'])) {
|
|
206
|
+
return { key: 'dotnet-workspace', label: '.NET workspace' };
|
|
207
|
+
}
|
|
208
|
+
if (matchAny(['java', 'spring'])) {
|
|
209
|
+
return { key: 'java-workspace', label: 'Java workspace' };
|
|
210
|
+
}
|
|
211
|
+
if (matchAny(['flutter', 'dart'])) {
|
|
212
|
+
return { key: 'flutter-workspace', label: 'Flutter workspace' };
|
|
213
|
+
}
|
|
214
|
+
if (matchAny(['swift'])) {
|
|
215
|
+
return { key: 'swift-workspace', label: 'Swift workspace' };
|
|
216
|
+
}
|
|
217
|
+
if (matchAny(['kotlin'])) {
|
|
218
|
+
return { key: 'kotlin-workspace', label: 'Kotlin workspace' };
|
|
219
|
+
}
|
|
220
|
+
if (matchAny(['react', 'nextjs', 'node', 'typescript', 'javascript', 'nestjs', 'vue', 'angular', 'svelte'])) {
|
|
221
|
+
return { key: 'node-workspace', label: 'Node / JS workspace' };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { key: 'general-workspace', label: 'General workspace' };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function buildProfileBreakdown(results) {
|
|
228
|
+
const grouped = new Map();
|
|
229
|
+
|
|
230
|
+
for (const item of results) {
|
|
231
|
+
const profileKey = item.workspaceProfile?.key || 'general-workspace';
|
|
232
|
+
const profileLabel = item.workspaceProfile?.label || 'General workspace';
|
|
233
|
+
if (!grouped.has(profileKey)) {
|
|
234
|
+
grouped.set(profileKey, {
|
|
235
|
+
profileKey,
|
|
236
|
+
profileLabel,
|
|
237
|
+
scoreType: 'workspace-live-audit',
|
|
238
|
+
workspaceCount: 0,
|
|
239
|
+
workspaces: [],
|
|
240
|
+
stackLabels: new Set(),
|
|
241
|
+
scores: [],
|
|
242
|
+
totals: [],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const entry = grouped.get(profileKey);
|
|
247
|
+
entry.workspaceCount += 1;
|
|
248
|
+
entry.workspaces.push(item.workspace);
|
|
249
|
+
for (const label of item.stackLabels || []) {
|
|
250
|
+
entry.stackLabels.add(label);
|
|
251
|
+
}
|
|
252
|
+
if (typeof item.score === 'number') {
|
|
253
|
+
entry.scores.push(item.score);
|
|
254
|
+
}
|
|
255
|
+
if (typeof item.total === 'number') {
|
|
256
|
+
entry.totals.push(item.total);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return [...grouped.values()]
|
|
261
|
+
.map((entry) => ({
|
|
262
|
+
profileKey: entry.profileKey,
|
|
263
|
+
profileLabel: entry.profileLabel,
|
|
264
|
+
scoreType: 'workspace-live-audit',
|
|
265
|
+
workspaceCount: entry.workspaceCount,
|
|
266
|
+
averageScore: entry.scores.length > 0
|
|
267
|
+
? Math.round(entry.scores.reduce((sum, value) => sum + value, 0) / entry.scores.length)
|
|
268
|
+
: 0,
|
|
269
|
+
averageTotal: entry.totals.length > 0
|
|
270
|
+
? Math.round(entry.totals.reduce((sum, value) => sum + value, 0) / entry.totals.length)
|
|
271
|
+
: 0,
|
|
272
|
+
stackLabels: [...entry.stackLabels].sort(),
|
|
273
|
+
workspaces: entry.workspaces.sort(),
|
|
274
|
+
}))
|
|
275
|
+
.sort((left, right) => left.profileLabel.localeCompare(right.profileLabel));
|
|
276
|
+
}
|
|
277
|
+
|
|
180
278
|
async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
181
279
|
const { audit } = require('./audit');
|
|
182
280
|
const rootDir = path.resolve(dir);
|
|
@@ -207,14 +305,7 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
207
305
|
const absPath = path.join(rootDir, workspacePath);
|
|
208
306
|
try {
|
|
209
307
|
const result = await audit({ dir: absPath, platform, silent: true });
|
|
210
|
-
results.push(
|
|
211
|
-
name: path.basename(workspacePath),
|
|
212
|
-
workspace: workspacePath,
|
|
213
|
-
dir: absPath,
|
|
214
|
-
platform,
|
|
215
|
-
...summarizeAuditResult(result, 'workspace-live-audit', 'workspace-package'),
|
|
216
|
-
result,
|
|
217
|
-
});
|
|
308
|
+
results.push(summarizeWorkspaceEntry(result, workspacePath, absPath, platform));
|
|
218
309
|
} catch (error) {
|
|
219
310
|
results.push({
|
|
220
311
|
name: path.basename(workspacePath),
|
|
@@ -227,6 +318,9 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
227
318
|
passed: 0,
|
|
228
319
|
total: 0,
|
|
229
320
|
topAction: null,
|
|
321
|
+
stackKeys: [],
|
|
322
|
+
stackLabels: [],
|
|
323
|
+
workspaceProfile: { key: 'general-workspace', label: 'General workspace' },
|
|
230
324
|
error: error.message,
|
|
231
325
|
});
|
|
232
326
|
}
|
|
@@ -238,6 +332,7 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
238
332
|
: 0;
|
|
239
333
|
const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
|
|
240
334
|
const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
|
|
335
|
+
const profileBreakdown = buildProfileBreakdown(results);
|
|
241
336
|
|
|
242
337
|
return {
|
|
243
338
|
summaryType: 'monorepo-workspace-audit',
|
|
@@ -254,10 +349,12 @@ async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
|
254
349
|
maxScore,
|
|
255
350
|
minScore,
|
|
256
351
|
},
|
|
352
|
+
profileBreakdown,
|
|
257
353
|
scoreSemantics: {
|
|
258
354
|
rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
|
|
259
355
|
workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
|
|
260
356
|
workspaceEntries: 'Each workspace row is a package-level live audit. Package scores can differ from the root governance score for legitimate reasons.',
|
|
357
|
+
workspaceProfiles: 'Workspace totals can differ because each package uses a stack-specific check profile based on detected languages and frameworks.',
|
|
261
358
|
},
|
|
262
359
|
workspaces: results,
|
|
263
360
|
detectedWorkspaces: workspacePaths,
|