@nerviq/cli 1.17.3 → 1.19.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 +4 -4
- package/bin/cli.js +61 -274
- package/package.json +60 -60
- 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 +158 -158
- 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 +16 -16
- 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/context.js +320 -320
- package/src/continuous-ops.js +681 -681
- package/src/copilot/activity.js +309 -309
- package/src/copilot/config-parser.js +280 -226
- package/src/copilot/context.js +218 -197
- 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/copilot/techniques.js +219 -78
- 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 -0
- 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/context.js +221 -221
- 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/gemini/techniques.js +14 -14
- 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/context.js +249 -249
- 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/windsurf/techniques.js +17 -17
- package/src/workspace.js +375 -375
package/src/policy-layers.js
CHANGED
|
@@ -1,210 +1,210 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { applyProfileToOptions } = require('./profiles');
|
|
4
|
-
|
|
5
|
-
const POLICY_FILES = {
|
|
6
|
-
org: path.join('.nerviq', 'org-policy.json'),
|
|
7
|
-
team: path.join('.nerviq', 'team-policy.json'),
|
|
8
|
-
repo: path.join('.nerviq', 'repo-policy.json'),
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
function normalizeArray(values) {
|
|
12
|
-
const seen = new Set();
|
|
13
|
-
const result = [];
|
|
14
|
-
for (const value of Array.isArray(values) ? values : []) {
|
|
15
|
-
const normalized = `${value || ''}`.trim();
|
|
16
|
-
if (!normalized || seen.has(normalized)) continue;
|
|
17
|
-
seen.add(normalized);
|
|
18
|
-
result.push(normalized);
|
|
19
|
-
}
|
|
20
|
-
return result;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readPolicyFile(filePath, layer) {
|
|
24
|
-
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
25
|
-
|
|
26
|
-
let raw;
|
|
27
|
-
try {
|
|
28
|
-
raw = fs.readFileSync(filePath, 'utf8');
|
|
29
|
-
} catch {
|
|
30
|
-
return {
|
|
31
|
-
layer,
|
|
32
|
-
path: filePath,
|
|
33
|
-
valid: false,
|
|
34
|
-
error: 'could not read policy file',
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let parsed;
|
|
39
|
-
try {
|
|
40
|
-
parsed = JSON.parse(raw);
|
|
41
|
-
} catch {
|
|
42
|
-
return {
|
|
43
|
-
layer,
|
|
44
|
-
path: filePath,
|
|
45
|
-
valid: false,
|
|
46
|
-
error: 'invalid JSON',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
layer,
|
|
52
|
-
path: filePath,
|
|
53
|
-
valid: true,
|
|
54
|
-
policy: {
|
|
55
|
-
name: parsed.name || `${layer} policy`,
|
|
56
|
-
description: parsed.description || '',
|
|
57
|
-
platforms: normalizeArray(parsed.platforms || []),
|
|
58
|
-
threshold: parsed.threshold != null ? Number(parsed.threshold) : null,
|
|
59
|
-
requireChecks: normalizeArray(parsed.requireChecks || []),
|
|
60
|
-
suppressedChecks: normalizeArray(parsed.suppressedChecks || []),
|
|
61
|
-
priorityBoosts: normalizeArray(parsed.priorityBoosts || []),
|
|
62
|
-
customWeights: parsed.customWeights && typeof parsed.customWeights === 'object' ? parsed.customWeights : {},
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function findAncestorOrgPolicy(dir) {
|
|
68
|
-
let current = path.resolve(dir);
|
|
69
|
-
let lastFound = null;
|
|
70
|
-
|
|
71
|
-
while (true) {
|
|
72
|
-
const candidate = path.join(current, POLICY_FILES.org);
|
|
73
|
-
if (fs.existsSync(candidate)) {
|
|
74
|
-
lastFound = candidate;
|
|
75
|
-
}
|
|
76
|
-
const parent = path.dirname(current);
|
|
77
|
-
if (parent === current) break;
|
|
78
|
-
current = parent;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return lastFound;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function resolvePolicyLayers(dir) {
|
|
85
|
-
const absoluteDir = path.resolve(dir);
|
|
86
|
-
const layers = [];
|
|
87
|
-
|
|
88
|
-
const orgLayer = readPolicyFile(findAncestorOrgPolicy(absoluteDir), 'org');
|
|
89
|
-
if (orgLayer) layers.push(orgLayer);
|
|
90
|
-
|
|
91
|
-
const teamLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.team), 'team');
|
|
92
|
-
if (teamLayer) layers.push(teamLayer);
|
|
93
|
-
|
|
94
|
-
const repoLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.repo), 'repo');
|
|
95
|
-
if (repoLayer) layers.push(repoLayer);
|
|
96
|
-
|
|
97
|
-
const resolved = {
|
|
98
|
-
platforms: [],
|
|
99
|
-
threshold: null,
|
|
100
|
-
requireChecks: [],
|
|
101
|
-
suppressedChecks: [],
|
|
102
|
-
priorityBoosts: [],
|
|
103
|
-
customWeights: {},
|
|
104
|
-
description: '',
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const fieldSources = {
|
|
108
|
-
platforms: [],
|
|
109
|
-
threshold: null,
|
|
110
|
-
requireChecks: [],
|
|
111
|
-
suppressedChecks: [],
|
|
112
|
-
priorityBoosts: [],
|
|
113
|
-
customWeights: [],
|
|
114
|
-
description: null,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
for (const layer of layers) {
|
|
118
|
-
if (!layer.valid || !layer.policy) continue;
|
|
119
|
-
const { policy } = layer;
|
|
120
|
-
|
|
121
|
-
if (policy.platforms.length > 0) {
|
|
122
|
-
resolved.platforms = [...policy.platforms];
|
|
123
|
-
fieldSources.platforms = [layer.layer];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (policy.threshold != null && Number.isFinite(policy.threshold)) {
|
|
127
|
-
resolved.threshold = policy.threshold;
|
|
128
|
-
fieldSources.threshold = layer.layer;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (policy.requireChecks.length > 0) {
|
|
132
|
-
resolved.requireChecks = normalizeArray([...resolved.requireChecks, ...policy.requireChecks]);
|
|
133
|
-
fieldSources.requireChecks = normalizeArray([...fieldSources.requireChecks, layer.layer]);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (policy.suppressedChecks.length > 0) {
|
|
137
|
-
resolved.suppressedChecks = normalizeArray([...resolved.suppressedChecks, ...policy.suppressedChecks]);
|
|
138
|
-
fieldSources.suppressedChecks = normalizeArray([...fieldSources.suppressedChecks, layer.layer]);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (policy.priorityBoosts.length > 0) {
|
|
142
|
-
resolved.priorityBoosts = normalizeArray([...resolved.priorityBoosts, ...policy.priorityBoosts]);
|
|
143
|
-
fieldSources.priorityBoosts = normalizeArray([...fieldSources.priorityBoosts, layer.layer]);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (Object.keys(policy.customWeights).length > 0) {
|
|
147
|
-
resolved.customWeights = { ...resolved.customWeights, ...policy.customWeights };
|
|
148
|
-
fieldSources.customWeights = normalizeArray([...fieldSources.customWeights, layer.layer]);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (policy.description) {
|
|
152
|
-
resolved.description = policy.description;
|
|
153
|
-
fieldSources.description = layer.layer;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
dir: absoluteDir,
|
|
159
|
-
overrideOrder: ['org', 'team', 'repo', 'explicit-cli'],
|
|
160
|
-
layers,
|
|
161
|
-
resolved,
|
|
162
|
-
fieldSources,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function applyPolicyLayersToOptions(contract, options) {
|
|
167
|
-
if (!contract || !contract.resolved) {
|
|
168
|
-
return { ...options };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return applyProfileToOptions(contract.resolved, options);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function formatPolicyContract(contract) {
|
|
175
|
-
if (!contract || !Array.isArray(contract.layers) || contract.layers.length === 0) {
|
|
176
|
-
return ' No org/team/repo policy layers found.';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const lines = [
|
|
180
|
-
' Policy layers (override order: org -> team -> repo -> explicit CLI):',
|
|
181
|
-
];
|
|
182
|
-
|
|
183
|
-
for (const layer of contract.layers) {
|
|
184
|
-
if (!layer.valid) {
|
|
185
|
-
lines.push(` - ${layer.layer}: ${layer.path} [invalid: ${layer.error}]`);
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const policy = layer.policy || {};
|
|
190
|
-
const details = [];
|
|
191
|
-
if (policy.platforms?.length > 0) details.push(`platforms=${policy.platforms.join(', ')}`);
|
|
192
|
-
if (policy.threshold != null) details.push(`threshold=${policy.threshold}`);
|
|
193
|
-
if (policy.requireChecks?.length > 0) details.push(`require=${policy.requireChecks.join(', ')}`);
|
|
194
|
-
lines.push(` - ${layer.layer}: ${path.relative(contract.dir, layer.path) || layer.path}${details.length > 0 ? ` (${details.join('; ')})` : ''}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const resolved = contract.resolved || {};
|
|
198
|
-
lines.push(' Resolved policy:');
|
|
199
|
-
lines.push(` - Platforms: ${resolved.platforms?.length > 0 ? resolved.platforms.join(', ') : 'default CLI platform'}`);
|
|
200
|
-
lines.push(` - Threshold: ${resolved.threshold != null ? resolved.threshold : 'default'}`);
|
|
201
|
-
lines.push(` - Required checks: ${resolved.requireChecks?.length > 0 ? resolved.requireChecks.join(', ') : 'none'}`);
|
|
202
|
-
return lines.join('\n');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
module.exports = {
|
|
206
|
-
POLICY_FILES,
|
|
207
|
-
resolvePolicyLayers,
|
|
208
|
-
applyPolicyLayersToOptions,
|
|
209
|
-
formatPolicyContract,
|
|
210
|
-
};
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { applyProfileToOptions } = require('./profiles');
|
|
4
|
+
|
|
5
|
+
const POLICY_FILES = {
|
|
6
|
+
org: path.join('.nerviq', 'org-policy.json'),
|
|
7
|
+
team: path.join('.nerviq', 'team-policy.json'),
|
|
8
|
+
repo: path.join('.nerviq', 'repo-policy.json'),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function normalizeArray(values) {
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const result = [];
|
|
14
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
15
|
+
const normalized = `${value || ''}`.trim();
|
|
16
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
17
|
+
seen.add(normalized);
|
|
18
|
+
result.push(normalized);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readPolicyFile(filePath, layer) {
|
|
24
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
25
|
+
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
29
|
+
} catch {
|
|
30
|
+
return {
|
|
31
|
+
layer,
|
|
32
|
+
path: filePath,
|
|
33
|
+
valid: false,
|
|
34
|
+
error: 'could not read policy file',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
layer,
|
|
44
|
+
path: filePath,
|
|
45
|
+
valid: false,
|
|
46
|
+
error: 'invalid JSON',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
layer,
|
|
52
|
+
path: filePath,
|
|
53
|
+
valid: true,
|
|
54
|
+
policy: {
|
|
55
|
+
name: parsed.name || `${layer} policy`,
|
|
56
|
+
description: parsed.description || '',
|
|
57
|
+
platforms: normalizeArray(parsed.platforms || []),
|
|
58
|
+
threshold: parsed.threshold != null ? Number(parsed.threshold) : null,
|
|
59
|
+
requireChecks: normalizeArray(parsed.requireChecks || []),
|
|
60
|
+
suppressedChecks: normalizeArray(parsed.suppressedChecks || []),
|
|
61
|
+
priorityBoosts: normalizeArray(parsed.priorityBoosts || []),
|
|
62
|
+
customWeights: parsed.customWeights && typeof parsed.customWeights === 'object' ? parsed.customWeights : {},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function findAncestorOrgPolicy(dir) {
|
|
68
|
+
let current = path.resolve(dir);
|
|
69
|
+
let lastFound = null;
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const candidate = path.join(current, POLICY_FILES.org);
|
|
73
|
+
if (fs.existsSync(candidate)) {
|
|
74
|
+
lastFound = candidate;
|
|
75
|
+
}
|
|
76
|
+
const parent = path.dirname(current);
|
|
77
|
+
if (parent === current) break;
|
|
78
|
+
current = parent;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return lastFound;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvePolicyLayers(dir) {
|
|
85
|
+
const absoluteDir = path.resolve(dir);
|
|
86
|
+
const layers = [];
|
|
87
|
+
|
|
88
|
+
const orgLayer = readPolicyFile(findAncestorOrgPolicy(absoluteDir), 'org');
|
|
89
|
+
if (orgLayer) layers.push(orgLayer);
|
|
90
|
+
|
|
91
|
+
const teamLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.team), 'team');
|
|
92
|
+
if (teamLayer) layers.push(teamLayer);
|
|
93
|
+
|
|
94
|
+
const repoLayer = readPolicyFile(path.join(absoluteDir, POLICY_FILES.repo), 'repo');
|
|
95
|
+
if (repoLayer) layers.push(repoLayer);
|
|
96
|
+
|
|
97
|
+
const resolved = {
|
|
98
|
+
platforms: [],
|
|
99
|
+
threshold: null,
|
|
100
|
+
requireChecks: [],
|
|
101
|
+
suppressedChecks: [],
|
|
102
|
+
priorityBoosts: [],
|
|
103
|
+
customWeights: {},
|
|
104
|
+
description: '',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const fieldSources = {
|
|
108
|
+
platforms: [],
|
|
109
|
+
threshold: null,
|
|
110
|
+
requireChecks: [],
|
|
111
|
+
suppressedChecks: [],
|
|
112
|
+
priorityBoosts: [],
|
|
113
|
+
customWeights: [],
|
|
114
|
+
description: null,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
for (const layer of layers) {
|
|
118
|
+
if (!layer.valid || !layer.policy) continue;
|
|
119
|
+
const { policy } = layer;
|
|
120
|
+
|
|
121
|
+
if (policy.platforms.length > 0) {
|
|
122
|
+
resolved.platforms = [...policy.platforms];
|
|
123
|
+
fieldSources.platforms = [layer.layer];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (policy.threshold != null && Number.isFinite(policy.threshold)) {
|
|
127
|
+
resolved.threshold = policy.threshold;
|
|
128
|
+
fieldSources.threshold = layer.layer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (policy.requireChecks.length > 0) {
|
|
132
|
+
resolved.requireChecks = normalizeArray([...resolved.requireChecks, ...policy.requireChecks]);
|
|
133
|
+
fieldSources.requireChecks = normalizeArray([...fieldSources.requireChecks, layer.layer]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (policy.suppressedChecks.length > 0) {
|
|
137
|
+
resolved.suppressedChecks = normalizeArray([...resolved.suppressedChecks, ...policy.suppressedChecks]);
|
|
138
|
+
fieldSources.suppressedChecks = normalizeArray([...fieldSources.suppressedChecks, layer.layer]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (policy.priorityBoosts.length > 0) {
|
|
142
|
+
resolved.priorityBoosts = normalizeArray([...resolved.priorityBoosts, ...policy.priorityBoosts]);
|
|
143
|
+
fieldSources.priorityBoosts = normalizeArray([...fieldSources.priorityBoosts, layer.layer]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (Object.keys(policy.customWeights).length > 0) {
|
|
147
|
+
resolved.customWeights = { ...resolved.customWeights, ...policy.customWeights };
|
|
148
|
+
fieldSources.customWeights = normalizeArray([...fieldSources.customWeights, layer.layer]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (policy.description) {
|
|
152
|
+
resolved.description = policy.description;
|
|
153
|
+
fieldSources.description = layer.layer;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
dir: absoluteDir,
|
|
159
|
+
overrideOrder: ['org', 'team', 'repo', 'explicit-cli'],
|
|
160
|
+
layers,
|
|
161
|
+
resolved,
|
|
162
|
+
fieldSources,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function applyPolicyLayersToOptions(contract, options) {
|
|
167
|
+
if (!contract || !contract.resolved) {
|
|
168
|
+
return { ...options };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return applyProfileToOptions(contract.resolved, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatPolicyContract(contract) {
|
|
175
|
+
if (!contract || !Array.isArray(contract.layers) || contract.layers.length === 0) {
|
|
176
|
+
return ' No org/team/repo policy layers found.';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lines = [
|
|
180
|
+
' Policy layers (override order: org -> team -> repo -> explicit CLI):',
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const layer of contract.layers) {
|
|
184
|
+
if (!layer.valid) {
|
|
185
|
+
lines.push(` - ${layer.layer}: ${layer.path} [invalid: ${layer.error}]`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const policy = layer.policy || {};
|
|
190
|
+
const details = [];
|
|
191
|
+
if (policy.platforms?.length > 0) details.push(`platforms=${policy.platforms.join(', ')}`);
|
|
192
|
+
if (policy.threshold != null) details.push(`threshold=${policy.threshold}`);
|
|
193
|
+
if (policy.requireChecks?.length > 0) details.push(`require=${policy.requireChecks.join(', ')}`);
|
|
194
|
+
lines.push(` - ${layer.layer}: ${path.relative(contract.dir, layer.path) || layer.path}${details.length > 0 ? ` (${details.join('; ')})` : ''}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const resolved = contract.resolved || {};
|
|
198
|
+
lines.push(' Resolved policy:');
|
|
199
|
+
lines.push(` - Platforms: ${resolved.platforms?.length > 0 ? resolved.platforms.join(', ') : 'default CLI platform'}`);
|
|
200
|
+
lines.push(` - Threshold: ${resolved.threshold != null ? resolved.threshold : 'default'}`);
|
|
201
|
+
lines.push(` - Required checks: ${resolved.requireChecks?.length > 0 ? resolved.requireChecks.join(', ') : 'none'}`);
|
|
202
|
+
return lines.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
POLICY_FILES,
|
|
207
|
+
resolvePolicyLayers,
|
|
208
|
+
applyPolicyLayersToOptions,
|
|
209
|
+
formatPolicyContract,
|
|
210
|
+
};
|
package/src/profiles.js
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Team profiles — save and share org-specific check weights and preferences.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
|
|
8
|
-
const PROFILES_DIR = 'profiles';
|
|
9
|
-
|
|
10
|
-
function profilesDir(dir) {
|
|
11
|
-
return path.join(dir, '.nerviq', PROFILES_DIR);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function profilePath(dir, name) {
|
|
15
|
-
return path.join(profilesDir(dir), `${name}.json`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function validateProfileName(name) {
|
|
19
|
-
if (!name || typeof name !== 'string') throw new Error('Profile name is required.');
|
|
20
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
21
|
-
throw new Error(`Invalid profile name '${name}'. Use only letters, numbers, hyphens, and underscores.`);
|
|
22
|
-
}
|
|
23
|
-
if (name.length > 64) throw new Error('Profile name must be 64 characters or fewer.');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function saveProfile(dir, profileName, options = {}) {
|
|
27
|
-
validateProfileName(profileName);
|
|
28
|
-
const profileDir = profilesDir(dir);
|
|
29
|
-
fs.mkdirSync(profileDir, { recursive: true });
|
|
30
|
-
|
|
31
|
-
const profile = {
|
|
32
|
-
name: profileName,
|
|
33
|
-
created: new Date().toISOString().split('T')[0],
|
|
34
|
-
platforms: options.platforms || ['claude'],
|
|
35
|
-
threshold: options.threshold != null ? Number(options.threshold) : 70,
|
|
36
|
-
suppressedChecks: options.suppressedChecks || [],
|
|
37
|
-
priorityBoosts: options.priorityBoosts || [],
|
|
38
|
-
customWeights: options.customWeights || {},
|
|
39
|
-
description: options.description || '',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const filePath = profilePath(dir, profileName);
|
|
43
|
-
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
|
|
44
|
-
return { saved: true, path: filePath, profile };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function loadProfile(dir, profileName) {
|
|
48
|
-
validateProfileName(profileName);
|
|
49
|
-
const filePath = profilePath(dir, profileName);
|
|
50
|
-
if (!fs.existsSync(filePath)) {
|
|
51
|
-
throw new Error(`Profile '${profileName}' not found. Run 'nerviq profile list' to see available profiles.`);
|
|
52
|
-
}
|
|
53
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
54
|
-
let profile;
|
|
55
|
-
try {
|
|
56
|
-
profile = JSON.parse(raw);
|
|
57
|
-
} catch {
|
|
58
|
-
throw new Error(`Profile '${profileName}' contains invalid JSON.`);
|
|
59
|
-
}
|
|
60
|
-
return profile;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function listProfiles(dir) {
|
|
64
|
-
const profDir = profilesDir(dir);
|
|
65
|
-
if (!fs.existsSync(profDir)) return [];
|
|
66
|
-
return fs.readdirSync(profDir)
|
|
67
|
-
.filter(f => f.endsWith('.json'))
|
|
68
|
-
.map(f => {
|
|
69
|
-
const name = f.replace(/\.json$/, '');
|
|
70
|
-
try {
|
|
71
|
-
const data = JSON.parse(fs.readFileSync(path.join(profDir, f), 'utf8'));
|
|
72
|
-
return { name, description: data.description || '', platforms: data.platforms || [], threshold: data.threshold };
|
|
73
|
-
} catch {
|
|
74
|
-
return { name, description: '(invalid)', platforms: [], threshold: null };
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function exportProfile(dir, profileName) {
|
|
80
|
-
const profile = loadProfile(dir, profileName);
|
|
81
|
-
return JSON.stringify(profile, null, 2);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function applyProfileToOptions(profile, options) {
|
|
85
|
-
const merged = { ...options };
|
|
86
|
-
if (profile.threshold != null && merged.threshold == null) {
|
|
87
|
-
merged.threshold = profile.threshold;
|
|
88
|
-
}
|
|
89
|
-
if (profile.platforms && profile.platforms.length > 0 && !options.platformExplicit) {
|
|
90
|
-
merged.platform = profile.platforms[0];
|
|
91
|
-
}
|
|
92
|
-
if ((profile.requireChecks || []).length > 0 && (!Array.isArray(merged.require) || merged.require.length === 0)) {
|
|
93
|
-
merged.require = [...profile.requireChecks];
|
|
94
|
-
}
|
|
95
|
-
merged.suppressedChecks = profile.suppressedChecks || [];
|
|
96
|
-
merged.priorityBoosts = profile.priorityBoosts || [];
|
|
97
|
-
merged.customWeights = profile.customWeights || {};
|
|
98
|
-
return merged;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function formatProfileList(profiles) {
|
|
102
|
-
if (profiles.length === 0) return ' No profiles found. Create one with: nerviq profile save <name>';
|
|
103
|
-
const lines = profiles.map(p => {
|
|
104
|
-
const desc = p.description ? ` — ${p.description}` : '';
|
|
105
|
-
const plats = p.platforms.length > 0 ? ` [${p.platforms.join(', ')}]` : '';
|
|
106
|
-
return ` ${p.name}${plats}${desc}`;
|
|
107
|
-
});
|
|
108
|
-
return lines.join('\n');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatProfile(profile) {
|
|
112
|
-
const lines = [
|
|
113
|
-
` Name: ${profile.name}`,
|
|
114
|
-
` Created: ${profile.created || 'unknown'}`,
|
|
115
|
-
` Platforms: ${(profile.platforms || []).join(', ') || 'any'}`,
|
|
116
|
-
` Threshold: ${profile.threshold != null ? profile.threshold : 'default'}`,
|
|
117
|
-
` Suppressed: ${(profile.suppressedChecks || []).join(', ') || 'none'}`,
|
|
118
|
-
` Boosted: ${(profile.priorityBoosts || []).join(', ') || 'none'}`,
|
|
119
|
-
` Description: ${profile.description || '(none)'}`,
|
|
120
|
-
];
|
|
121
|
-
return lines.join('\n');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
module.exports = { saveProfile, loadProfile, listProfiles, exportProfile, applyProfileToOptions, formatProfileList, formatProfile, validateProfileName };
|
|
1
|
+
/**
|
|
2
|
+
* Team profiles — save and share org-specific check weights and preferences.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const PROFILES_DIR = 'profiles';
|
|
9
|
+
|
|
10
|
+
function profilesDir(dir) {
|
|
11
|
+
return path.join(dir, '.nerviq', PROFILES_DIR);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function profilePath(dir, name) {
|
|
15
|
+
return path.join(profilesDir(dir), `${name}.json`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateProfileName(name) {
|
|
19
|
+
if (!name || typeof name !== 'string') throw new Error('Profile name is required.');
|
|
20
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
21
|
+
throw new Error(`Invalid profile name '${name}'. Use only letters, numbers, hyphens, and underscores.`);
|
|
22
|
+
}
|
|
23
|
+
if (name.length > 64) throw new Error('Profile name must be 64 characters or fewer.');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveProfile(dir, profileName, options = {}) {
|
|
27
|
+
validateProfileName(profileName);
|
|
28
|
+
const profileDir = profilesDir(dir);
|
|
29
|
+
fs.mkdirSync(profileDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
const profile = {
|
|
32
|
+
name: profileName,
|
|
33
|
+
created: new Date().toISOString().split('T')[0],
|
|
34
|
+
platforms: options.platforms || ['claude'],
|
|
35
|
+
threshold: options.threshold != null ? Number(options.threshold) : 70,
|
|
36
|
+
suppressedChecks: options.suppressedChecks || [],
|
|
37
|
+
priorityBoosts: options.priorityBoosts || [],
|
|
38
|
+
customWeights: options.customWeights || {},
|
|
39
|
+
description: options.description || '',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const filePath = profilePath(dir, profileName);
|
|
43
|
+
fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
|
|
44
|
+
return { saved: true, path: filePath, profile };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadProfile(dir, profileName) {
|
|
48
|
+
validateProfileName(profileName);
|
|
49
|
+
const filePath = profilePath(dir, profileName);
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
throw new Error(`Profile '${profileName}' not found. Run 'nerviq profile list' to see available profiles.`);
|
|
52
|
+
}
|
|
53
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
let profile;
|
|
55
|
+
try {
|
|
56
|
+
profile = JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`Profile '${profileName}' contains invalid JSON.`);
|
|
59
|
+
}
|
|
60
|
+
return profile;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function listProfiles(dir) {
|
|
64
|
+
const profDir = profilesDir(dir);
|
|
65
|
+
if (!fs.existsSync(profDir)) return [];
|
|
66
|
+
return fs.readdirSync(profDir)
|
|
67
|
+
.filter(f => f.endsWith('.json'))
|
|
68
|
+
.map(f => {
|
|
69
|
+
const name = f.replace(/\.json$/, '');
|
|
70
|
+
try {
|
|
71
|
+
const data = JSON.parse(fs.readFileSync(path.join(profDir, f), 'utf8'));
|
|
72
|
+
return { name, description: data.description || '', platforms: data.platforms || [], threshold: data.threshold };
|
|
73
|
+
} catch {
|
|
74
|
+
return { name, description: '(invalid)', platforms: [], threshold: null };
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function exportProfile(dir, profileName) {
|
|
80
|
+
const profile = loadProfile(dir, profileName);
|
|
81
|
+
return JSON.stringify(profile, null, 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function applyProfileToOptions(profile, options) {
|
|
85
|
+
const merged = { ...options };
|
|
86
|
+
if (profile.threshold != null && merged.threshold == null) {
|
|
87
|
+
merged.threshold = profile.threshold;
|
|
88
|
+
}
|
|
89
|
+
if (profile.platforms && profile.platforms.length > 0 && !options.platformExplicit) {
|
|
90
|
+
merged.platform = profile.platforms[0];
|
|
91
|
+
}
|
|
92
|
+
if ((profile.requireChecks || []).length > 0 && (!Array.isArray(merged.require) || merged.require.length === 0)) {
|
|
93
|
+
merged.require = [...profile.requireChecks];
|
|
94
|
+
}
|
|
95
|
+
merged.suppressedChecks = profile.suppressedChecks || [];
|
|
96
|
+
merged.priorityBoosts = profile.priorityBoosts || [];
|
|
97
|
+
merged.customWeights = profile.customWeights || {};
|
|
98
|
+
return merged;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatProfileList(profiles) {
|
|
102
|
+
if (profiles.length === 0) return ' No profiles found. Create one with: nerviq profile save <name>';
|
|
103
|
+
const lines = profiles.map(p => {
|
|
104
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
105
|
+
const plats = p.platforms.length > 0 ? ` [${p.platforms.join(', ')}]` : '';
|
|
106
|
+
return ` ${p.name}${plats}${desc}`;
|
|
107
|
+
});
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatProfile(profile) {
|
|
112
|
+
const lines = [
|
|
113
|
+
` Name: ${profile.name}`,
|
|
114
|
+
` Created: ${profile.created || 'unknown'}`,
|
|
115
|
+
` Platforms: ${(profile.platforms || []).join(', ') || 'any'}`,
|
|
116
|
+
` Threshold: ${profile.threshold != null ? profile.threshold : 'default'}`,
|
|
117
|
+
` Suppressed: ${(profile.suppressedChecks || []).join(', ') || 'none'}`,
|
|
118
|
+
` Boosted: ${(profile.priorityBoosts || []).join(', ') || 'none'}`,
|
|
119
|
+
` Description: ${profile.description || '(none)'}`,
|
|
120
|
+
];
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { saveProfile, loadProfile, listProfiles, exportProfile, applyProfileToOptions, formatProfileList, formatProfile, validateProfileName };
|