@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/workspace.js
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
function normalizePath(value) {
|
|
5
|
-
return value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
function readJsonSafe(filePath) {
|
|
9
|
-
try {
|
|
10
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
11
|
-
} catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function readTextSafe(filePath) {
|
|
17
|
-
try {
|
|
18
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function unique(items) {
|
|
25
|
-
return [...new Set(items.filter(Boolean).map(normalizePath))];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function hasWorkspaceConfig(dir) {
|
|
29
|
-
return [
|
|
30
|
-
'turbo.json',
|
|
31
|
-
'lerna.json',
|
|
32
|
-
'pnpm-workspace.yaml',
|
|
33
|
-
].some((file) => fs.existsSync(path.join(dir, file))) ||
|
|
34
|
-
Boolean(readJsonSafe(path.join(dir, 'package.json'))?.workspaces);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function packageWorkspacePatterns(dir) {
|
|
38
|
-
const pkg = readJsonSafe(path.join(dir, 'package.json')) || {};
|
|
39
|
-
const workspaces = pkg.workspaces;
|
|
40
|
-
|
|
41
|
-
if (Array.isArray(workspaces)) {
|
|
42
|
-
return workspaces;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (workspaces && Array.isArray(workspaces.packages)) {
|
|
46
|
-
return workspaces.packages;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return [];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function lernaWorkspacePatterns(dir) {
|
|
53
|
-
const lerna = readJsonSafe(path.join(dir, 'lerna.json')) || {};
|
|
54
|
-
return Array.isArray(lerna.packages) ? lerna.packages : [];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pnpmWorkspacePatterns(dir) {
|
|
58
|
-
const content = readTextSafe(path.join(dir, 'pnpm-workspace.yaml'));
|
|
59
|
-
if (!content) return [];
|
|
60
|
-
|
|
61
|
-
const matches = [];
|
|
62
|
-
for (const line of content.split(/\r?\n/)) {
|
|
63
|
-
const match = line.match(/^\s*-\s*["']?([^"']+)["']?\s*$/);
|
|
64
|
-
if (match) {
|
|
65
|
-
matches.push(match[1]);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return matches;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function turboWorkspacePatterns(dir) {
|
|
72
|
-
if (!fs.existsSync(path.join(dir, 'turbo.json'))) {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const commonPatterns = [];
|
|
77
|
-
for (const candidate of ['apps', 'packages', 'services']) {
|
|
78
|
-
if (fs.existsSync(path.join(dir, candidate)) && fs.statSync(path.join(dir, candidate)).isDirectory()) {
|
|
79
|
-
commonPatterns.push(`${candidate}/*`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return commonPatterns;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function listDirectories(rootDir) {
|
|
87
|
-
const found = [];
|
|
88
|
-
const queue = [''];
|
|
89
|
-
|
|
90
|
-
while (queue.length > 0) {
|
|
91
|
-
const relative = queue.shift();
|
|
92
|
-
const full = path.join(rootDir, relative);
|
|
93
|
-
|
|
94
|
-
let entries = [];
|
|
95
|
-
try {
|
|
96
|
-
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
97
|
-
} catch {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for (const entry of entries) {
|
|
102
|
-
if (!entry.isDirectory()) continue;
|
|
103
|
-
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next' || entry.name === 'dist') continue;
|
|
104
|
-
const child = normalizePath(path.join(relative, entry.name));
|
|
105
|
-
found.push(child);
|
|
106
|
-
queue.push(child);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return found;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function globToRegExp(pattern) {
|
|
114
|
-
const normalized = normalizePath(pattern)
|
|
115
|
-
.replace(/\*\*/g, '__DOUBLE_STAR__')
|
|
116
|
-
.replace(/\*/g, '__SINGLE_STAR__')
|
|
117
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
118
|
-
.replace(/__DOUBLE_STAR__/g, '.*')
|
|
119
|
-
.replace(/__SINGLE_STAR__/g, '[^/]+');
|
|
120
|
-
return new RegExp(`^${normalized}$`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function expandWorkspacePatterns(dir, patterns) {
|
|
124
|
-
const normalizedPatterns = unique(Array.isArray(patterns) ? patterns : []);
|
|
125
|
-
if (normalizedPatterns.length === 0) {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const allDirs = listDirectories(dir);
|
|
130
|
-
const matches = [];
|
|
131
|
-
|
|
132
|
-
for (const pattern of normalizedPatterns) {
|
|
133
|
-
const fullPath = path.join(dir, pattern);
|
|
134
|
-
if (!pattern.includes('*') && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
|
135
|
-
matches.push(normalizePath(pattern));
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const matcher = globToRegExp(pattern);
|
|
140
|
-
for (const candidate of allDirs) {
|
|
141
|
-
if (matcher.test(candidate)) {
|
|
142
|
-
matches.push(candidate);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return unique(matches);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function detectWorkspaceGlobs(dir) {
|
|
151
|
-
return unique([
|
|
152
|
-
...packageWorkspacePatterns(dir),
|
|
153
|
-
...lernaWorkspacePatterns(dir),
|
|
154
|
-
...pnpmWorkspacePatterns(dir),
|
|
155
|
-
...turboWorkspacePatterns(dir),
|
|
156
|
-
]);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function detectWorkspaces(dir) {
|
|
160
|
-
return expandWorkspacePatterns(dir, detectWorkspaceGlobs(dir));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function parseWorkspaceSelection(value) {
|
|
164
|
-
if (!value) return [];
|
|
165
|
-
if (Array.isArray(value)) return unique(value);
|
|
166
|
-
return unique(String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function summarizeAuditResult(result, scoreType, scope) {
|
|
170
|
-
return {
|
|
171
|
-
scope,
|
|
172
|
-
scoreType,
|
|
173
|
-
score: typeof result?.score === 'number' ? result.score : null,
|
|
174
|
-
passed: typeof result?.passed === 'number' ? result.passed : 0,
|
|
175
|
-
total: typeof result?.checkCount === 'number' ? result.checkCount : 0,
|
|
176
|
-
topAction: result?.topNextActions?.[0]?.name || null,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
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
|
-
|
|
278
|
-
async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
279
|
-
const { audit } = require('./audit');
|
|
280
|
-
const rootDir = path.resolve(dir);
|
|
281
|
-
const selectedPatterns = parseWorkspaceSelection(workspaceGlobs);
|
|
282
|
-
const sourcePatterns = selectedPatterns.length > 0 ? selectedPatterns : detectWorkspaceGlobs(rootDir);
|
|
283
|
-
const workspacePaths = selectedPatterns.length > 0
|
|
284
|
-
? expandWorkspacePatterns(rootDir, selectedPatterns)
|
|
285
|
-
: detectWorkspaces(rootDir);
|
|
286
|
-
const results = [];
|
|
287
|
-
let rootGovernance;
|
|
288
|
-
|
|
289
|
-
try {
|
|
290
|
-
const rootResult = await audit({ dir: rootDir, platform, silent: true });
|
|
291
|
-
rootGovernance = summarizeAuditResult(rootResult, 'root-live-audit', 'root-governance');
|
|
292
|
-
} catch (error) {
|
|
293
|
-
rootGovernance = {
|
|
294
|
-
scope: 'root-governance',
|
|
295
|
-
scoreType: 'root-live-audit',
|
|
296
|
-
score: null,
|
|
297
|
-
passed: 0,
|
|
298
|
-
total: 0,
|
|
299
|
-
topAction: null,
|
|
300
|
-
error: error.message,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
for (const workspacePath of workspacePaths) {
|
|
305
|
-
const absPath = path.join(rootDir, workspacePath);
|
|
306
|
-
try {
|
|
307
|
-
const result = await audit({ dir: absPath, platform, silent: true });
|
|
308
|
-
results.push(summarizeWorkspaceEntry(result, workspacePath, absPath, platform));
|
|
309
|
-
} catch (error) {
|
|
310
|
-
results.push({
|
|
311
|
-
name: path.basename(workspacePath),
|
|
312
|
-
workspace: workspacePath,
|
|
313
|
-
dir: absPath,
|
|
314
|
-
platform,
|
|
315
|
-
scope: 'workspace-package',
|
|
316
|
-
scoreType: 'workspace-live-audit',
|
|
317
|
-
score: null,
|
|
318
|
-
passed: 0,
|
|
319
|
-
total: 0,
|
|
320
|
-
topAction: null,
|
|
321
|
-
stackKeys: [],
|
|
322
|
-
stackLabels: [],
|
|
323
|
-
workspaceProfile: { key: 'general-workspace', label: 'General workspace' },
|
|
324
|
-
error: error.message,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const validScores = results.filter((item) => typeof item.score === 'number').map((item) => item.score);
|
|
330
|
-
const averageScore = validScores.length > 0
|
|
331
|
-
? Math.round(validScores.reduce((sum, value) => sum + value, 0) / validScores.length)
|
|
332
|
-
: 0;
|
|
333
|
-
const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
|
|
334
|
-
const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
|
|
335
|
-
const profileBreakdown = buildProfileBreakdown(results);
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
summaryType: 'monorepo-workspace-audit',
|
|
339
|
-
rootDir,
|
|
340
|
-
platform,
|
|
341
|
-
selectionMode: selectedPatterns.length > 0 ? 'explicit-patterns' : 'detected-workspaces',
|
|
342
|
-
patterns: sourcePatterns,
|
|
343
|
-
rootGovernance,
|
|
344
|
-
workspaceAggregate: {
|
|
345
|
-
scope: 'workspace-aggregate',
|
|
346
|
-
scoreType: 'workspace-average-live-audit',
|
|
347
|
-
score: averageScore,
|
|
348
|
-
workspaceCount: workspacePaths.length,
|
|
349
|
-
maxScore,
|
|
350
|
-
minScore,
|
|
351
|
-
},
|
|
352
|
-
profileBreakdown,
|
|
353
|
-
scoreSemantics: {
|
|
354
|
-
rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
|
|
355
|
-
workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
|
|
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.',
|
|
358
|
-
},
|
|
359
|
-
workspaces: results,
|
|
360
|
-
detectedWorkspaces: workspacePaths,
|
|
361
|
-
workspaceCount: workspacePaths.length,
|
|
362
|
-
averageScoreType: 'workspace-average-live-audit',
|
|
363
|
-
averageScore,
|
|
364
|
-
maxScore,
|
|
365
|
-
minScore,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
module.exports = {
|
|
370
|
-
hasWorkspaceConfig,
|
|
371
|
-
detectWorkspaceGlobs,
|
|
372
|
-
detectWorkspaces,
|
|
373
|
-
parseWorkspaceSelection,
|
|
374
|
-
auditWorkspaces,
|
|
375
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function normalizePath(value) {
|
|
5
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readJsonSafe(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readTextSafe(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unique(items) {
|
|
25
|
+
return [...new Set(items.filter(Boolean).map(normalizePath))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hasWorkspaceConfig(dir) {
|
|
29
|
+
return [
|
|
30
|
+
'turbo.json',
|
|
31
|
+
'lerna.json',
|
|
32
|
+
'pnpm-workspace.yaml',
|
|
33
|
+
].some((file) => fs.existsSync(path.join(dir, file))) ||
|
|
34
|
+
Boolean(readJsonSafe(path.join(dir, 'package.json'))?.workspaces);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function packageWorkspacePatterns(dir) {
|
|
38
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json')) || {};
|
|
39
|
+
const workspaces = pkg.workspaces;
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(workspaces)) {
|
|
42
|
+
return workspaces;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (workspaces && Array.isArray(workspaces.packages)) {
|
|
46
|
+
return workspaces.packages;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lernaWorkspacePatterns(dir) {
|
|
53
|
+
const lerna = readJsonSafe(path.join(dir, 'lerna.json')) || {};
|
|
54
|
+
return Array.isArray(lerna.packages) ? lerna.packages : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pnpmWorkspacePatterns(dir) {
|
|
58
|
+
const content = readTextSafe(path.join(dir, 'pnpm-workspace.yaml'));
|
|
59
|
+
if (!content) return [];
|
|
60
|
+
|
|
61
|
+
const matches = [];
|
|
62
|
+
for (const line of content.split(/\r?\n/)) {
|
|
63
|
+
const match = line.match(/^\s*-\s*["']?([^"']+)["']?\s*$/);
|
|
64
|
+
if (match) {
|
|
65
|
+
matches.push(match[1]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return matches;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function turboWorkspacePatterns(dir) {
|
|
72
|
+
if (!fs.existsSync(path.join(dir, 'turbo.json'))) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const commonPatterns = [];
|
|
77
|
+
for (const candidate of ['apps', 'packages', 'services']) {
|
|
78
|
+
if (fs.existsSync(path.join(dir, candidate)) && fs.statSync(path.join(dir, candidate)).isDirectory()) {
|
|
79
|
+
commonPatterns.push(`${candidate}/*`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return commonPatterns;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function listDirectories(rootDir) {
|
|
87
|
+
const found = [];
|
|
88
|
+
const queue = [''];
|
|
89
|
+
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const relative = queue.shift();
|
|
92
|
+
const full = path.join(rootDir, relative);
|
|
93
|
+
|
|
94
|
+
let entries = [];
|
|
95
|
+
try {
|
|
96
|
+
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (!entry.isDirectory()) continue;
|
|
103
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next' || entry.name === 'dist') continue;
|
|
104
|
+
const child = normalizePath(path.join(relative, entry.name));
|
|
105
|
+
found.push(child);
|
|
106
|
+
queue.push(child);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return found;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function globToRegExp(pattern) {
|
|
114
|
+
const normalized = normalizePath(pattern)
|
|
115
|
+
.replace(/\*\*/g, '__DOUBLE_STAR__')
|
|
116
|
+
.replace(/\*/g, '__SINGLE_STAR__')
|
|
117
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
118
|
+
.replace(/__DOUBLE_STAR__/g, '.*')
|
|
119
|
+
.replace(/__SINGLE_STAR__/g, '[^/]+');
|
|
120
|
+
return new RegExp(`^${normalized}$`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function expandWorkspacePatterns(dir, patterns) {
|
|
124
|
+
const normalizedPatterns = unique(Array.isArray(patterns) ? patterns : []);
|
|
125
|
+
if (normalizedPatterns.length === 0) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const allDirs = listDirectories(dir);
|
|
130
|
+
const matches = [];
|
|
131
|
+
|
|
132
|
+
for (const pattern of normalizedPatterns) {
|
|
133
|
+
const fullPath = path.join(dir, pattern);
|
|
134
|
+
if (!pattern.includes('*') && fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
|
|
135
|
+
matches.push(normalizePath(pattern));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const matcher = globToRegExp(pattern);
|
|
140
|
+
for (const candidate of allDirs) {
|
|
141
|
+
if (matcher.test(candidate)) {
|
|
142
|
+
matches.push(candidate);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return unique(matches);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function detectWorkspaceGlobs(dir) {
|
|
151
|
+
return unique([
|
|
152
|
+
...packageWorkspacePatterns(dir),
|
|
153
|
+
...lernaWorkspacePatterns(dir),
|
|
154
|
+
...pnpmWorkspacePatterns(dir),
|
|
155
|
+
...turboWorkspacePatterns(dir),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function detectWorkspaces(dir) {
|
|
160
|
+
return expandWorkspacePatterns(dir, detectWorkspaceGlobs(dir));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseWorkspaceSelection(value) {
|
|
164
|
+
if (!value) return [];
|
|
165
|
+
if (Array.isArray(value)) return unique(value);
|
|
166
|
+
return unique(String(value).split(',').map((item) => item.trim()).filter(Boolean));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function summarizeAuditResult(result, scoreType, scope) {
|
|
170
|
+
return {
|
|
171
|
+
scope,
|
|
172
|
+
scoreType,
|
|
173
|
+
score: typeof result?.score === 'number' ? result.score : null,
|
|
174
|
+
passed: typeof result?.passed === 'number' ? result.passed : 0,
|
|
175
|
+
total: typeof result?.checkCount === 'number' ? result.checkCount : 0,
|
|
176
|
+
topAction: result?.topNextActions?.[0]?.name || null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
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
|
+
|
|
278
|
+
async function auditWorkspaces(dir, workspaceGlobs, platform = 'claude') {
|
|
279
|
+
const { audit } = require('./audit');
|
|
280
|
+
const rootDir = path.resolve(dir);
|
|
281
|
+
const selectedPatterns = parseWorkspaceSelection(workspaceGlobs);
|
|
282
|
+
const sourcePatterns = selectedPatterns.length > 0 ? selectedPatterns : detectWorkspaceGlobs(rootDir);
|
|
283
|
+
const workspacePaths = selectedPatterns.length > 0
|
|
284
|
+
? expandWorkspacePatterns(rootDir, selectedPatterns)
|
|
285
|
+
: detectWorkspaces(rootDir);
|
|
286
|
+
const results = [];
|
|
287
|
+
let rootGovernance;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const rootResult = await audit({ dir: rootDir, platform, silent: true });
|
|
291
|
+
rootGovernance = summarizeAuditResult(rootResult, 'root-live-audit', 'root-governance');
|
|
292
|
+
} catch (error) {
|
|
293
|
+
rootGovernance = {
|
|
294
|
+
scope: 'root-governance',
|
|
295
|
+
scoreType: 'root-live-audit',
|
|
296
|
+
score: null,
|
|
297
|
+
passed: 0,
|
|
298
|
+
total: 0,
|
|
299
|
+
topAction: null,
|
|
300
|
+
error: error.message,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const workspacePath of workspacePaths) {
|
|
305
|
+
const absPath = path.join(rootDir, workspacePath);
|
|
306
|
+
try {
|
|
307
|
+
const result = await audit({ dir: absPath, platform, silent: true });
|
|
308
|
+
results.push(summarizeWorkspaceEntry(result, workspacePath, absPath, platform));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
results.push({
|
|
311
|
+
name: path.basename(workspacePath),
|
|
312
|
+
workspace: workspacePath,
|
|
313
|
+
dir: absPath,
|
|
314
|
+
platform,
|
|
315
|
+
scope: 'workspace-package',
|
|
316
|
+
scoreType: 'workspace-live-audit',
|
|
317
|
+
score: null,
|
|
318
|
+
passed: 0,
|
|
319
|
+
total: 0,
|
|
320
|
+
topAction: null,
|
|
321
|
+
stackKeys: [],
|
|
322
|
+
stackLabels: [],
|
|
323
|
+
workspaceProfile: { key: 'general-workspace', label: 'General workspace' },
|
|
324
|
+
error: error.message,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const validScores = results.filter((item) => typeof item.score === 'number').map((item) => item.score);
|
|
330
|
+
const averageScore = validScores.length > 0
|
|
331
|
+
? Math.round(validScores.reduce((sum, value) => sum + value, 0) / validScores.length)
|
|
332
|
+
: 0;
|
|
333
|
+
const maxScore = validScores.length > 0 ? Math.max(...validScores) : 0;
|
|
334
|
+
const minScore = validScores.length > 0 ? Math.min(...validScores) : 0;
|
|
335
|
+
const profileBreakdown = buildProfileBreakdown(results);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
summaryType: 'monorepo-workspace-audit',
|
|
339
|
+
rootDir,
|
|
340
|
+
platform,
|
|
341
|
+
selectionMode: selectedPatterns.length > 0 ? 'explicit-patterns' : 'detected-workspaces',
|
|
342
|
+
patterns: sourcePatterns,
|
|
343
|
+
rootGovernance,
|
|
344
|
+
workspaceAggregate: {
|
|
345
|
+
scope: 'workspace-aggregate',
|
|
346
|
+
scoreType: 'workspace-average-live-audit',
|
|
347
|
+
score: averageScore,
|
|
348
|
+
workspaceCount: workspacePaths.length,
|
|
349
|
+
maxScore,
|
|
350
|
+
minScore,
|
|
351
|
+
},
|
|
352
|
+
profileBreakdown,
|
|
353
|
+
scoreSemantics: {
|
|
354
|
+
rootGovernance: 'Root repo live audit for shared instructions, hooks, permissions, and top-level governance files.',
|
|
355
|
+
workspaceAggregate: 'Average of the selected workspace live audit scores. This is a package coverage rollup, not the root repo score.',
|
|
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.',
|
|
358
|
+
},
|
|
359
|
+
workspaces: results,
|
|
360
|
+
detectedWorkspaces: workspacePaths,
|
|
361
|
+
workspaceCount: workspacePaths.length,
|
|
362
|
+
averageScoreType: 'workspace-average-live-audit',
|
|
363
|
+
averageScore,
|
|
364
|
+
maxScore,
|
|
365
|
+
minScore,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
hasWorkspaceConfig,
|
|
371
|
+
detectWorkspaceGlobs,
|
|
372
|
+
detectWorkspaces,
|
|
373
|
+
parseWorkspaceSelection,
|
|
374
|
+
auditWorkspaces,
|
|
375
|
+
};
|