@nerviq/cli 1.18.0 → 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 +2 -2
- package/bin/cli.js +130 -130
- 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 +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/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/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
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const claudeFreshness = require('./freshness');
|
|
4
|
-
const codexFreshness = require('./codex/freshness');
|
|
5
|
-
const cursorFreshness = require('./cursor/freshness');
|
|
6
|
-
const copilotFreshness = require('./copilot/freshness');
|
|
7
|
-
const geminiFreshness = require('./gemini/freshness');
|
|
8
|
-
const windsurfFreshness = require('./windsurf/freshness');
|
|
9
|
-
const aiderFreshness = require('./aider/freshness');
|
|
10
|
-
const opencodeFreshness = require('./opencode/freshness');
|
|
11
|
-
|
|
12
|
-
const DAILY_FRESHNESS_WORKFLOW = {
|
|
13
|
-
workflow: '.github/workflows/freshness-check.yml',
|
|
14
|
-
cadence: 'Daily at 06:00 UTC plus manual dispatch',
|
|
15
|
-
issuePolicy: 'Open or refresh GitHub issues for stale P0 sources without failing the main CI pipeline.',
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const PLATFORM_CHANGE_MANIFEST = [
|
|
19
|
-
{ key: 'claude', label: 'Claude Code', modulePath: 'src/freshness.js', freshness: claudeFreshness },
|
|
20
|
-
{ key: 'codex', label: 'Codex', modulePath: 'src/codex/freshness.js', freshness: codexFreshness },
|
|
21
|
-
{ key: 'cursor', label: 'Cursor', modulePath: 'src/cursor/freshness.js', freshness: cursorFreshness },
|
|
22
|
-
{ key: 'copilot', label: 'Copilot', modulePath: 'src/copilot/freshness.js', freshness: copilotFreshness },
|
|
23
|
-
{ key: 'gemini', label: 'Gemini CLI', modulePath: 'src/gemini/freshness.js', freshness: geminiFreshness },
|
|
24
|
-
{ key: 'windsurf', label: 'Windsurf', modulePath: 'src/windsurf/freshness.js', freshness: windsurfFreshness },
|
|
25
|
-
{ key: 'aider', label: 'Aider', modulePath: 'src/aider/freshness.js', freshness: aiderFreshness },
|
|
26
|
-
{ key: 'opencode', label: 'OpenCode', modulePath: 'src/opencode/freshness.js', freshness: opencodeFreshness },
|
|
27
|
-
].map((entry) => {
|
|
28
|
-
const sources = (entry.freshness.P0_SOURCES || []).map((source) => ({
|
|
29
|
-
key: source.key,
|
|
30
|
-
label: source.label,
|
|
31
|
-
url: source.url,
|
|
32
|
-
stalenessThresholdDays: source.stalenessThresholdDays,
|
|
33
|
-
verifiedAt: source.verifiedAt || null,
|
|
34
|
-
}));
|
|
35
|
-
const thresholds = [...new Set(sources.map((source) => source.stalenessThresholdDays))].sort((a, b) => a - b);
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
key: entry.key,
|
|
39
|
-
label: entry.label,
|
|
40
|
-
modulePath: entry.modulePath,
|
|
41
|
-
trackedSources: sources,
|
|
42
|
-
trackedSourceCount: sources.length,
|
|
43
|
-
reviewCadence: {
|
|
44
|
-
automation: DAILY_FRESHNESS_WORKFLOW.cadence,
|
|
45
|
-
thresholdsDays: thresholds,
|
|
46
|
-
manualExpectation: thresholds.includes(14)
|
|
47
|
-
? 'Review high-volatility sources weekly and verify any stale source immediately.'
|
|
48
|
-
: 'Review sources at least monthly and verify any stale source immediately.',
|
|
49
|
-
},
|
|
50
|
-
freshnessWorkflow: {
|
|
51
|
-
...DAILY_FRESHNESS_WORKFLOW,
|
|
52
|
-
manualTrigger: true,
|
|
53
|
-
},
|
|
54
|
-
updateTriggers: (entry.freshness.PROPAGATION_CHECKLIST || []).map((item) => ({
|
|
55
|
-
trigger: item.trigger,
|
|
56
|
-
targets: item.targets || [],
|
|
57
|
-
})),
|
|
58
|
-
};
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
function getPlatformChangeManifest() {
|
|
62
|
-
return JSON.parse(JSON.stringify(PLATFORM_CHANGE_MANIFEST));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function summarizePlatformChangeManifest() {
|
|
66
|
-
const manifest = getPlatformChangeManifest();
|
|
67
|
-
return {
|
|
68
|
-
platformCount: manifest.length,
|
|
69
|
-
trackedSourceCount: manifest.reduce((sum, entry) => sum + entry.trackedSourceCount, 0),
|
|
70
|
-
workflow: DAILY_FRESHNESS_WORKFLOW,
|
|
71
|
-
platforms: manifest.map((entry) => ({
|
|
72
|
-
key: entry.key,
|
|
73
|
-
label: entry.label,
|
|
74
|
-
trackedSourceCount: entry.trackedSourceCount,
|
|
75
|
-
thresholdDays: entry.reviewCadence.thresholdsDays,
|
|
76
|
-
updateTriggerCount: entry.updateTriggers.length,
|
|
77
|
-
})),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
module.exports = {
|
|
82
|
-
DAILY_FRESHNESS_WORKFLOW,
|
|
83
|
-
PLATFORM_CHANGE_MANIFEST,
|
|
84
|
-
getPlatformChangeManifest,
|
|
85
|
-
summarizePlatformChangeManifest,
|
|
86
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const claudeFreshness = require('./freshness');
|
|
4
|
+
const codexFreshness = require('./codex/freshness');
|
|
5
|
+
const cursorFreshness = require('./cursor/freshness');
|
|
6
|
+
const copilotFreshness = require('./copilot/freshness');
|
|
7
|
+
const geminiFreshness = require('./gemini/freshness');
|
|
8
|
+
const windsurfFreshness = require('./windsurf/freshness');
|
|
9
|
+
const aiderFreshness = require('./aider/freshness');
|
|
10
|
+
const opencodeFreshness = require('./opencode/freshness');
|
|
11
|
+
|
|
12
|
+
const DAILY_FRESHNESS_WORKFLOW = {
|
|
13
|
+
workflow: '.github/workflows/freshness-check.yml',
|
|
14
|
+
cadence: 'Daily at 06:00 UTC plus manual dispatch',
|
|
15
|
+
issuePolicy: 'Open or refresh GitHub issues for stale P0 sources without failing the main CI pipeline.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const PLATFORM_CHANGE_MANIFEST = [
|
|
19
|
+
{ key: 'claude', label: 'Claude Code', modulePath: 'src/freshness.js', freshness: claudeFreshness },
|
|
20
|
+
{ key: 'codex', label: 'Codex', modulePath: 'src/codex/freshness.js', freshness: codexFreshness },
|
|
21
|
+
{ key: 'cursor', label: 'Cursor', modulePath: 'src/cursor/freshness.js', freshness: cursorFreshness },
|
|
22
|
+
{ key: 'copilot', label: 'Copilot', modulePath: 'src/copilot/freshness.js', freshness: copilotFreshness },
|
|
23
|
+
{ key: 'gemini', label: 'Gemini CLI', modulePath: 'src/gemini/freshness.js', freshness: geminiFreshness },
|
|
24
|
+
{ key: 'windsurf', label: 'Windsurf', modulePath: 'src/windsurf/freshness.js', freshness: windsurfFreshness },
|
|
25
|
+
{ key: 'aider', label: 'Aider', modulePath: 'src/aider/freshness.js', freshness: aiderFreshness },
|
|
26
|
+
{ key: 'opencode', label: 'OpenCode', modulePath: 'src/opencode/freshness.js', freshness: opencodeFreshness },
|
|
27
|
+
].map((entry) => {
|
|
28
|
+
const sources = (entry.freshness.P0_SOURCES || []).map((source) => ({
|
|
29
|
+
key: source.key,
|
|
30
|
+
label: source.label,
|
|
31
|
+
url: source.url,
|
|
32
|
+
stalenessThresholdDays: source.stalenessThresholdDays,
|
|
33
|
+
verifiedAt: source.verifiedAt || null,
|
|
34
|
+
}));
|
|
35
|
+
const thresholds = [...new Set(sources.map((source) => source.stalenessThresholdDays))].sort((a, b) => a - b);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
key: entry.key,
|
|
39
|
+
label: entry.label,
|
|
40
|
+
modulePath: entry.modulePath,
|
|
41
|
+
trackedSources: sources,
|
|
42
|
+
trackedSourceCount: sources.length,
|
|
43
|
+
reviewCadence: {
|
|
44
|
+
automation: DAILY_FRESHNESS_WORKFLOW.cadence,
|
|
45
|
+
thresholdsDays: thresholds,
|
|
46
|
+
manualExpectation: thresholds.includes(14)
|
|
47
|
+
? 'Review high-volatility sources weekly and verify any stale source immediately.'
|
|
48
|
+
: 'Review sources at least monthly and verify any stale source immediately.',
|
|
49
|
+
},
|
|
50
|
+
freshnessWorkflow: {
|
|
51
|
+
...DAILY_FRESHNESS_WORKFLOW,
|
|
52
|
+
manualTrigger: true,
|
|
53
|
+
},
|
|
54
|
+
updateTriggers: (entry.freshness.PROPAGATION_CHECKLIST || []).map((item) => ({
|
|
55
|
+
trigger: item.trigger,
|
|
56
|
+
targets: item.targets || [],
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function getPlatformChangeManifest() {
|
|
62
|
+
return JSON.parse(JSON.stringify(PLATFORM_CHANGE_MANIFEST));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summarizePlatformChangeManifest() {
|
|
66
|
+
const manifest = getPlatformChangeManifest();
|
|
67
|
+
return {
|
|
68
|
+
platformCount: manifest.length,
|
|
69
|
+
trackedSourceCount: manifest.reduce((sum, entry) => sum + entry.trackedSourceCount, 0),
|
|
70
|
+
workflow: DAILY_FRESHNESS_WORKFLOW,
|
|
71
|
+
platforms: manifest.map((entry) => ({
|
|
72
|
+
key: entry.key,
|
|
73
|
+
label: entry.label,
|
|
74
|
+
trackedSourceCount: entry.trackedSourceCount,
|
|
75
|
+
thresholdDays: entry.reviewCadence.thresholdsDays,
|
|
76
|
+
updateTriggerCount: entry.updateTriggers.length,
|
|
77
|
+
})),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
DAILY_FRESHNESS_WORKFLOW,
|
|
83
|
+
PLATFORM_CHANGE_MANIFEST,
|
|
84
|
+
getPlatformChangeManifest,
|
|
85
|
+
summarizePlatformChangeManifest,
|
|
86
|
+
};
|
package/src/plugins.js
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin system for Nerviq.
|
|
3
|
-
* Allows users to extend audits with custom checks via nerviq.config.js.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
const REQUIRED_CHECK_FIELDS = ['id', 'name', 'check', 'impact', 'category', 'fix'];
|
|
10
|
-
const VALID_IMPACTS = ['critical', 'high', 'medium', 'low'];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Validate a single plugin object.
|
|
14
|
-
* Returns { valid: true } or { valid: false, errors: [...] }.
|
|
15
|
-
*/
|
|
16
|
-
function validatePlugin(plugin) {
|
|
17
|
-
const errors = [];
|
|
18
|
-
|
|
19
|
-
if (!plugin || typeof plugin !== 'object') {
|
|
20
|
-
return { valid: false, errors: ['Plugin must be a non-null object'] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
24
|
-
errors.push('Plugin must have a non-empty string "name" field');
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!plugin.checks || typeof plugin.checks !== 'object' || Array.isArray(plugin.checks)) {
|
|
28
|
-
errors.push('Plugin must have a "checks" object');
|
|
29
|
-
return { valid: false, errors };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const [key, check] of Object.entries(plugin.checks)) {
|
|
33
|
-
for (const field of REQUIRED_CHECK_FIELDS) {
|
|
34
|
-
if (check[field] === undefined || check[field] === null) {
|
|
35
|
-
errors.push(`Check "${key}" is missing required field "${field}"`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (typeof check.check !== 'function') {
|
|
40
|
-
errors.push(`Check "${key}" field "check" must be a function`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (check.impact && !VALID_IMPACTS.includes(check.impact)) {
|
|
44
|
-
errors.push(`Check "${key}" has invalid impact "${check.impact}". Must be one of: ${VALID_IMPACTS.join(', ')}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Load plugins from nerviq.config.js in the given directory.
|
|
53
|
-
* Returns an array of plugin objects, or [] if no config file exists.
|
|
54
|
-
*/
|
|
55
|
-
function loadPlugins(dir) {
|
|
56
|
-
const configPath = path.join(dir, 'nerviq.config.js');
|
|
57
|
-
|
|
58
|
-
if (!fs.existsSync(configPath)) {
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let config;
|
|
63
|
-
try {
|
|
64
|
-
config = require(configPath);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
console.error(`Failed to load nerviq.config.js: ${err.message}`);
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (!config || !Array.isArray(config.plugins)) {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const validPlugins = [];
|
|
75
|
-
for (const plugin of config.plugins) {
|
|
76
|
-
const result = validatePlugin(plugin);
|
|
77
|
-
if (result.valid) {
|
|
78
|
-
validPlugins.push(plugin);
|
|
79
|
-
} else {
|
|
80
|
-
console.error(`Plugin "${plugin && plugin.name || 'unknown'}" is invalid: ${result.errors.join('; ')}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return validPlugins;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Merge plugin checks into the existing techniques object.
|
|
89
|
-
* Plugin checks are prefixed with "plugin:" to avoid key collisions.
|
|
90
|
-
* Returns a new merged techniques object (does not mutate the original).
|
|
91
|
-
*/
|
|
92
|
-
function mergePluginChecks(techniques, plugins) {
|
|
93
|
-
const merged = { ...techniques };
|
|
94
|
-
|
|
95
|
-
for (const plugin of plugins) {
|
|
96
|
-
for (const [key, check] of Object.entries(plugin.checks)) {
|
|
97
|
-
const prefixedKey = `plugin:${plugin.name}:${key}`;
|
|
98
|
-
merged[prefixedKey] = {
|
|
99
|
-
...check,
|
|
100
|
-
pluginName: plugin.name,
|
|
101
|
-
sourceUrl: check.sourceUrl || null,
|
|
102
|
-
confidence: check.confidence !== undefined ? check.confidence : 0.5,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return merged;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
module.exports = { loadPlugins, mergePluginChecks, validatePlugin };
|
|
1
|
+
/**
|
|
2
|
+
* Plugin system for Nerviq.
|
|
3
|
+
* Allows users to extend audits with custom checks via nerviq.config.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const REQUIRED_CHECK_FIELDS = ['id', 'name', 'check', 'impact', 'category', 'fix'];
|
|
10
|
+
const VALID_IMPACTS = ['critical', 'high', 'medium', 'low'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a single plugin object.
|
|
14
|
+
* Returns { valid: true } or { valid: false, errors: [...] }.
|
|
15
|
+
*/
|
|
16
|
+
function validatePlugin(plugin) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
|
|
19
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
20
|
+
return { valid: false, errors: ['Plugin must be a non-null object'] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
24
|
+
errors.push('Plugin must have a non-empty string "name" field');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!plugin.checks || typeof plugin.checks !== 'object' || Array.isArray(plugin.checks)) {
|
|
28
|
+
errors.push('Plugin must have a "checks" object');
|
|
29
|
+
return { valid: false, errors };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [key, check] of Object.entries(plugin.checks)) {
|
|
33
|
+
for (const field of REQUIRED_CHECK_FIELDS) {
|
|
34
|
+
if (check[field] === undefined || check[field] === null) {
|
|
35
|
+
errors.push(`Check "${key}" is missing required field "${field}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof check.check !== 'function') {
|
|
40
|
+
errors.push(`Check "${key}" field "check" must be a function`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (check.impact && !VALID_IMPACTS.includes(check.impact)) {
|
|
44
|
+
errors.push(`Check "${key}" has invalid impact "${check.impact}". Must be one of: ${VALID_IMPACTS.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load plugins from nerviq.config.js in the given directory.
|
|
53
|
+
* Returns an array of plugin objects, or [] if no config file exists.
|
|
54
|
+
*/
|
|
55
|
+
function loadPlugins(dir) {
|
|
56
|
+
const configPath = path.join(dir, 'nerviq.config.js');
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(configPath)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let config;
|
|
63
|
+
try {
|
|
64
|
+
config = require(configPath);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Failed to load nerviq.config.js: ${err.message}`);
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!config || !Array.isArray(config.plugins)) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const validPlugins = [];
|
|
75
|
+
for (const plugin of config.plugins) {
|
|
76
|
+
const result = validatePlugin(plugin);
|
|
77
|
+
if (result.valid) {
|
|
78
|
+
validPlugins.push(plugin);
|
|
79
|
+
} else {
|
|
80
|
+
console.error(`Plugin "${plugin && plugin.name || 'unknown'}" is invalid: ${result.errors.join('; ')}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return validPlugins;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Merge plugin checks into the existing techniques object.
|
|
89
|
+
* Plugin checks are prefixed with "plugin:" to avoid key collisions.
|
|
90
|
+
* Returns a new merged techniques object (does not mutate the original).
|
|
91
|
+
*/
|
|
92
|
+
function mergePluginChecks(techniques, plugins) {
|
|
93
|
+
const merged = { ...techniques };
|
|
94
|
+
|
|
95
|
+
for (const plugin of plugins) {
|
|
96
|
+
for (const [key, check] of Object.entries(plugin.checks)) {
|
|
97
|
+
const prefixedKey = `plugin:${plugin.name}:${key}`;
|
|
98
|
+
merged[prefixedKey] = {
|
|
99
|
+
...check,
|
|
100
|
+
pluginName: plugin.name,
|
|
101
|
+
sourceUrl: check.sourceUrl || null,
|
|
102
|
+
confidence: check.confidence !== undefined ? check.confidence : 0.5,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { loadPlugins, mergePluginChecks, validatePlugin };
|