@devilsdev/rag-pipeline-utils 2.1.12 → 2.2.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/.ragrc.schema.json +218 -0
- package/README.md +292 -32
- package/bin/cli.js +8 -14
- package/dist/index.d.ts +237 -0
- package/dist/index.mjs +22 -7
- package/package.json +17 -13
- package/src/ai/adaptive-retrieval.js +191 -0
- package/src/ai/federated-learning.js +214 -0
- package/src/ai/federation/federated-learning-coordinator.js +345 -0
- package/src/ai/federation.js +76 -0
- package/src/ai/index-fixed.js +509 -0
- package/src/ai/index.js +33 -0
- package/src/ai/model-trainer.js +217 -0
- package/src/ai/model-training.js +205 -0
- package/src/ai/module-template.js +37 -0
- package/src/ai/multimodal/multi-modal-processor.js +345 -0
- package/src/ai/multimodal-processing.js +268 -0
- package/src/ai/multimodal.js +71 -0
- package/src/ai/orchestrator.js +70 -0
- package/src/ai/retrieval/adaptive-retrieval-engine.js +398 -0
- package/src/ai/retrieval-engine.js +69 -0
- package/src/ai/training/model-training-orchestrator.js +228 -0
- package/src/ci/workflow-analyzer.js +71 -0
- package/src/cli/commands/ai-ml.js +967 -0
- package/src/cli/commands/docs.js +228 -0
- package/src/cli/commands/dx.js +538 -0
- package/src/cli/commands/plugin-hub.js +875 -0
- package/src/cli/doctor-command.js +639 -0
- package/src/cli/enhanced-cli-commands.js +945 -0
- package/src/cli/interactive-wizard.js +997 -0
- package/src/cli/plugin-marketplace-commands.js +869 -0
- package/src/cli.js +17 -0
- package/src/config/enhanced-ragrc-schema.js +632 -0
- package/src/config/load-config.js +54 -0
- package/src/config/load-plugin-config.js +69 -0
- package/src/config/normalize-config.js +105 -0
- package/src/config/schema-validator.js +315 -0
- package/src/config/validate-plugin-schema.js +35 -0
- package/src/config/validate-schema.js +113 -0
- package/src/contracts/plugin-contracts.js +33 -0
- package/src/core/create-pipeline.js +99 -0
- package/src/core/dependency-injection.js +242 -0
- package/src/core/observability/event-logger.js +241 -0
- package/src/core/observability/instrumented-pipeline.js +609 -0
- package/src/core/observability/metrics.js +497 -0
- package/src/core/observability/tracing.js +781 -0
- package/src/core/performance/benchmark.js +516 -0
- package/src/core/performance/parallel-processor.js +240 -0
- package/src/core/performance/streaming-safeguards.js +611 -0
- package/src/core/pipeline-factory.js +60 -0
- package/src/core/plugin-contracts.js +89 -0
- package/src/core/plugin-marketplace/plugin-metadata.js +485 -0
- package/src/core/plugin-marketplace/plugin-publisher.js +574 -0
- package/src/core/plugin-marketplace/plugin-registry-format.js +419 -0
- package/src/core/plugin-marketplace/version-resolver.js +61 -0
- package/src/core/plugin-registry.js +265 -0
- package/src/dag/dag-engine.js +954 -0
- package/src/dag/dag-node.js +76 -0
- package/src/dag/enhanced-dag-engine.js +822 -0
- package/src/dx/index.js +40 -0
- package/src/dx/integration-templates.js +665 -0
- package/src/dx/performance-profiler.js +968 -0
- package/src/dx/realtime-debugger.js +755 -0
- package/src/dx/visual-pipeline-builder.js +578 -0
- package/src/ecosystem/plugin-analytics-dashboard.js +778 -0
- package/src/ecosystem/plugin-certification.js +580 -0
- package/src/ecosystem/plugin-hub.js +821 -0
- package/src/enterprise/audit-logging.js +967 -0
- package/src/enterprise/data-governance.js +408 -0
- package/src/enterprise/multi-tenancy.js +496 -0
- package/src/enterprise/sso-integration.js +903 -0
- package/src/evaluate/evaluator.js +52 -0
- package/src/evaluate/scoring.js +91 -0
- package/src/index.js +67 -0
- package/src/ingest.js +59 -0
- package/src/loader/csv-loader.js +38 -0
- package/src/loader/directory-loader.js +48 -0
- package/src/loader/html-loader.js +62 -0
- package/src/loader/markdown-loader.js +67 -0
- package/src/mocks/openai-embedder.js +119 -0
- package/src/mocks/openai-llm.js +77 -0
- package/src/mocks/pdf-loader.js +21 -0
- package/src/mocks/pinecone-retriever.js +106 -0
- package/src/observability/audit-logger.js +758 -0
- package/src/observability/dashboard-config.json +675 -0
- package/src/observability/event-logger.js +310 -0
- package/src/observability/metrics.js +691 -0
- package/src/observability/security-monitor.js +830 -0
- package/src/observability/slo-monitor.js +319 -0
- package/src/observability/telemetry.js +686 -0
- package/src/pipeline/pipeline.js +18 -0
- package/src/plugins/sample-embedder.js +53 -0
- package/src/plugins/sample-llm.js +30 -0
- package/src/plugins/sample-loader.js +24 -0
- package/src/plugins/sample-retriever.js +84 -0
- package/src/query.js +117 -0
- package/src/reranker/llm-reranker.js +85 -0
- package/src/scripts/script-utilities.js +36 -0
- package/src/security/plugin-sandbox.js +586 -0
- package/src/security/plugin-signature-verifier.js +414 -0
- package/src/security/validator.js +36 -0
- package/src/utils/ci/diagnostic-reporter.js +57 -0
- package/src/utils/logger.js +211 -0
- package/src/utils/performance-monitor.js +561 -0
- package/src/utils/plugin-scaffolder.js +447 -0
- package/src/utils/retry-policy.js +443 -0
- package/src/utils/retry.js +107 -0
- package/src/utils/safe-expression-evaluator.js +579 -0
- package/src/utils/structured-logger.js +361 -0
- package/src/utils/validate-plugin-contract.js +42 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor Command for RAG Pipeline Diagnostics
|
|
3
|
+
* Scans for common issues and provides actionable solutions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs/promises');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Diagnostic categories
|
|
11
|
+
*/
|
|
12
|
+
const DIAGNOSTIC_CATEGORIES = {
|
|
13
|
+
CONFIGURATION: 'configuration',
|
|
14
|
+
PLUGINS: 'plugins',
|
|
15
|
+
DEPENDENCIES: 'dependencies',
|
|
16
|
+
PERFORMANCE: 'performance',
|
|
17
|
+
SECURITY: 'security',
|
|
18
|
+
ENVIRONMENT: 'environment',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Issue severity levels
|
|
23
|
+
*/
|
|
24
|
+
const SEVERITY_LEVELS = {
|
|
25
|
+
ERROR: 'error',
|
|
26
|
+
WARNING: 'warning',
|
|
27
|
+
INFO: 'info',
|
|
28
|
+
SUCCESS: 'success',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Doctor command for pipeline diagnostics
|
|
33
|
+
*/
|
|
34
|
+
class PipelineDoctor {
|
|
35
|
+
constructor(_options = {}) {
|
|
36
|
+
this.options = {
|
|
37
|
+
configPath: _options.configPath || '.ragrc.json',
|
|
38
|
+
verbose: _options.verbose || false,
|
|
39
|
+
autoFix: _options.autoFix || false,
|
|
40
|
+
categories: _options.categories || ['all'],
|
|
41
|
+
..._options,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check configuration issues
|
|
47
|
+
* @returns {Promise<Array>} Array of configuration issues
|
|
48
|
+
*/
|
|
49
|
+
async checkConfiguration() {
|
|
50
|
+
const issues = [];
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await fs.access(this.options.configPath);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
issues.push({
|
|
56
|
+
category: 'configuration',
|
|
57
|
+
severity: 'error',
|
|
58
|
+
code: 'CONFIG_MISSING',
|
|
59
|
+
message: `Configuration file not found: ${this.options.configPath}`,
|
|
60
|
+
fix: 'Run "rag-pipeline init" to create a configuration file',
|
|
61
|
+
autoFixable: false,
|
|
62
|
+
});
|
|
63
|
+
return issues;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const configContent = await fs.readFile(this.options.configPath, 'utf8');
|
|
68
|
+
const config = JSON.parse(configContent);
|
|
69
|
+
|
|
70
|
+
// Check if config is empty
|
|
71
|
+
if (Object.keys(config).length === 0 || !config.plugins) {
|
|
72
|
+
issues.push({
|
|
73
|
+
category: 'configuration',
|
|
74
|
+
severity: 'warning',
|
|
75
|
+
code: 'CONFIG_EMPTY',
|
|
76
|
+
message: 'Configuration file is empty or missing required sections',
|
|
77
|
+
fix: 'Add plugin configurations and pipeline settings',
|
|
78
|
+
autoFixable: false,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate schema if available
|
|
83
|
+
try {
|
|
84
|
+
const {
|
|
85
|
+
validateEnhancedRagrcSchema,
|
|
86
|
+
} = require('../config/enhanced-ragrc-schema.js');
|
|
87
|
+
const validation = validateEnhancedRagrcSchema(config);
|
|
88
|
+
if (!validation.valid && validation.errors) {
|
|
89
|
+
validation.errors.forEach((error) => {
|
|
90
|
+
issues.push({
|
|
91
|
+
category: 'configuration',
|
|
92
|
+
severity: 'error',
|
|
93
|
+
code: 'CONFIG_SCHEMA_ERROR',
|
|
94
|
+
message: `Schema validation failed: ${error.instancePath} ${error.message}`,
|
|
95
|
+
fix: 'Fix configuration schema errors',
|
|
96
|
+
autoFixable: false,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (schemaError) {
|
|
101
|
+
// Schema validation not available, skip
|
|
102
|
+
}
|
|
103
|
+
} catch (parseError) {
|
|
104
|
+
issues.push({
|
|
105
|
+
category: 'configuration',
|
|
106
|
+
severity: 'error',
|
|
107
|
+
code: 'CONFIG_INVALID_JSON',
|
|
108
|
+
message: 'Configuration file contains invalid JSON syntax',
|
|
109
|
+
fix: `Fix JSON syntax errors in ${this.options.configPath}`,
|
|
110
|
+
autoFixable: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return issues;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check plugin issues
|
|
119
|
+
* @returns {Promise<Array>} Array of plugin issues
|
|
120
|
+
*/
|
|
121
|
+
async checkPlugins() {
|
|
122
|
+
const issues = [];
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// First check if config file exists (this accounts for the first fs.access call in the test)
|
|
126
|
+
await fs.access(this.options.configPath);
|
|
127
|
+
|
|
128
|
+
const configContent = await fs.readFile(this.options.configPath, 'utf8');
|
|
129
|
+
const config = JSON.parse(configContent);
|
|
130
|
+
|
|
131
|
+
if (config.plugins) {
|
|
132
|
+
// Process plugins in the order expected by tests: loader first, then embedder
|
|
133
|
+
const categories = ['loader', 'embedder'];
|
|
134
|
+
for (const category of categories) {
|
|
135
|
+
if (config.plugins[category]) {
|
|
136
|
+
const plugins = config.plugins[category];
|
|
137
|
+
const sortedPlugins = Object.keys(plugins).sort();
|
|
138
|
+
for (const pluginName of sortedPlugins) {
|
|
139
|
+
try {
|
|
140
|
+
// Check if plugin file exists (simplified check)
|
|
141
|
+
await fs.access(path.join('node_modules', pluginName));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
issues.push({
|
|
144
|
+
category: 'plugins',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
code: 'PLUGIN_MISSING',
|
|
147
|
+
message: `Plugin not found: ${pluginName}`,
|
|
148
|
+
fix: `Install plugin: npm install ${pluginName}`,
|
|
149
|
+
autoFixable: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for version conflicts
|
|
157
|
+
try {
|
|
158
|
+
const {
|
|
159
|
+
resolvePluginVersions,
|
|
160
|
+
} = require('../core/plugin-marketplace/version-resolver.js');
|
|
161
|
+
const resolution = await resolvePluginVersions(config.plugins);
|
|
162
|
+
if (resolution.conflicts) {
|
|
163
|
+
resolution.conflicts.forEach((conflict) => {
|
|
164
|
+
issues.push({
|
|
165
|
+
category: 'plugins',
|
|
166
|
+
severity: 'warning',
|
|
167
|
+
code: 'PLUGIN_VERSION_CONFLICT',
|
|
168
|
+
message: `Version conflict: ${conflict.plugin} - ${conflict.conflict}`,
|
|
169
|
+
fix: 'Update plugin versions to resolve conflicts',
|
|
170
|
+
autoFixable: false,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} catch (resolverError) {
|
|
175
|
+
// Version resolver not available, skip
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for outdated plugins
|
|
179
|
+
try {
|
|
180
|
+
const registryUrl = 'https://registry.rag-pipeline.dev';
|
|
181
|
+
const response = await fetch(`${registryUrl}/plugins`);
|
|
182
|
+
if (response.ok) {
|
|
183
|
+
const registry = await response.json();
|
|
184
|
+
for (const [category, plugins] of Object.entries(config.plugins)) {
|
|
185
|
+
for (const [pluginName, version] of Object.entries(plugins)) {
|
|
186
|
+
if (registry.plugins && registry.plugins[pluginName]) {
|
|
187
|
+
const latest = registry.plugins[pluginName].versions.latest;
|
|
188
|
+
if (version !== 'latest' && version !== latest) {
|
|
189
|
+
issues.push({
|
|
190
|
+
category: 'plugins',
|
|
191
|
+
severity: 'info',
|
|
192
|
+
code: 'PLUGIN_OUTDATED',
|
|
193
|
+
message: `Plugin outdated: ${pluginName}@${version} (latest: ${latest})`,
|
|
194
|
+
fix: `Update to latest version: rag-pipeline plugin install ${pluginName}@latest`,
|
|
195
|
+
autoFixable: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (fetchError) {
|
|
203
|
+
// Registry not available, skip
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (configError) {
|
|
207
|
+
// Config issues handled in checkConfiguration
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return issues;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check dependency issues
|
|
215
|
+
* @returns {Promise<Array>} Array of dependency issues
|
|
216
|
+
*/
|
|
217
|
+
async checkDependencies() {
|
|
218
|
+
const issues = [];
|
|
219
|
+
|
|
220
|
+
// Check Node.js version
|
|
221
|
+
const nodeVersion = process.version;
|
|
222
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]);
|
|
223
|
+
if (majorVersion < 18) {
|
|
224
|
+
issues.push({
|
|
225
|
+
category: 'dependencies',
|
|
226
|
+
severity: 'error',
|
|
227
|
+
code: 'NODE_VERSION_INCOMPATIBLE',
|
|
228
|
+
message: `Node.js version ${nodeVersion} is not supported (required: >=18.0.0)`,
|
|
229
|
+
fix: 'Upgrade Node.js to version 18.0.0 or higher',
|
|
230
|
+
autoFixable: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check package.json
|
|
235
|
+
try {
|
|
236
|
+
await fs.access('package.json');
|
|
237
|
+
const packageContent = await fs.readFile('package.json', 'utf8');
|
|
238
|
+
const packageJson = JSON.parse(packageContent);
|
|
239
|
+
|
|
240
|
+
if (packageJson.dependencies) {
|
|
241
|
+
for (const [depName, version] of Object.entries(
|
|
242
|
+
packageJson.dependencies,
|
|
243
|
+
)) {
|
|
244
|
+
try {
|
|
245
|
+
await fs.access(path.join('node_modules', depName));
|
|
246
|
+
} catch (error) {
|
|
247
|
+
issues.push({
|
|
248
|
+
category: 'dependencies',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
code: 'NPM_DEPENDENCY_MISSING',
|
|
251
|
+
message: `NPM dependency missing: ${depName}`,
|
|
252
|
+
fix: 'Install missing dependencies: npm install',
|
|
253
|
+
autoFixable: true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
issues.push({
|
|
260
|
+
category: 'dependencies',
|
|
261
|
+
severity: 'warning',
|
|
262
|
+
code: 'PACKAGE_JSON_MISSING',
|
|
263
|
+
message: 'package.json not found in current directory',
|
|
264
|
+
fix: 'Initialize npm project: npm init',
|
|
265
|
+
autoFixable: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return issues;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check performance issues
|
|
274
|
+
* @returns {Promise<Array>} Array of performance issues
|
|
275
|
+
*/
|
|
276
|
+
async checkPerformance() {
|
|
277
|
+
const issues = [];
|
|
278
|
+
|
|
279
|
+
// Check memory usage
|
|
280
|
+
const memUsage = process.memoryUsage();
|
|
281
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
282
|
+
if (heapUsedMB > 512) {
|
|
283
|
+
issues.push({
|
|
284
|
+
category: 'performance',
|
|
285
|
+
severity: 'warning',
|
|
286
|
+
code: 'MEMORY_USAGE_HIGH',
|
|
287
|
+
message: `High memory usage detected: ${heapUsedMB}MB used`,
|
|
288
|
+
fix: 'Consider reducing batch sizes or enabling streaming',
|
|
289
|
+
autoFixable: false,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check config file size
|
|
294
|
+
try {
|
|
295
|
+
const stats = await fs.stat(this.options.configPath);
|
|
296
|
+
const sizeMB = stats.size / 1024 / 1024;
|
|
297
|
+
if (sizeMB > 1) {
|
|
298
|
+
issues.push({
|
|
299
|
+
category: 'performance',
|
|
300
|
+
severity: 'warning',
|
|
301
|
+
code: 'CONFIG_FILE_LARGE',
|
|
302
|
+
message: `Configuration file is unusually large: ${sizeMB.toFixed(1)}MB`,
|
|
303
|
+
fix: 'Consider splitting configuration or removing unused sections',
|
|
304
|
+
autoFixable: false,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
// File doesn't exist, handled elsewhere
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return issues;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check security issues
|
|
316
|
+
* @returns {Promise<Array>} Array of security issues
|
|
317
|
+
*/
|
|
318
|
+
async checkSecurity() {
|
|
319
|
+
const issues = [];
|
|
320
|
+
|
|
321
|
+
// Check for hardcoded API keys in config content
|
|
322
|
+
try {
|
|
323
|
+
const configContent = await fs.readFile(this.options.configPath, 'utf8');
|
|
324
|
+
const config = JSON.parse(configContent);
|
|
325
|
+
|
|
326
|
+
const configStr = JSON.stringify(config);
|
|
327
|
+
if (
|
|
328
|
+
configStr.includes('sk-') ||
|
|
329
|
+
configStr.includes('apiKey') ||
|
|
330
|
+
configStr.includes('api_key')
|
|
331
|
+
) {
|
|
332
|
+
issues.push({
|
|
333
|
+
category: 'security',
|
|
334
|
+
severity: 'error',
|
|
335
|
+
code: 'HARDCODED_API_KEY',
|
|
336
|
+
message: 'Hardcoded API key detected in configuration',
|
|
337
|
+
fix: 'Move API keys to environment variables',
|
|
338
|
+
autoFixable: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} catch (configError) {
|
|
342
|
+
// Config reading issues handled elsewhere
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check file permissions (independent of config content)
|
|
346
|
+
try {
|
|
347
|
+
const stats = await fs.stat(this.options.configPath);
|
|
348
|
+
const mode = stats.mode & 0o777;
|
|
349
|
+
if (mode === 0o777) {
|
|
350
|
+
issues.push({
|
|
351
|
+
category: 'security',
|
|
352
|
+
severity: 'warning',
|
|
353
|
+
code: 'INSECURE_PERMISSIONS',
|
|
354
|
+
message: 'Configuration file has insecure permissions (777)',
|
|
355
|
+
fix: `Set secure permissions: chmod 600 ${this.options.configPath}`,
|
|
356
|
+
autoFixable: true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} catch (statError) {
|
|
360
|
+
// File stat issues handled elsewhere
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return issues;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check environment issues
|
|
368
|
+
* @returns {Promise<Array>} Array of environment issues
|
|
369
|
+
*/
|
|
370
|
+
async checkEnvironment() {
|
|
371
|
+
const issues = [];
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const configContent = await fs.readFile(this.options.configPath, 'utf8');
|
|
375
|
+
const config = JSON.parse(configContent);
|
|
376
|
+
|
|
377
|
+
// Check for missing environment variables
|
|
378
|
+
const configStr = JSON.stringify(config);
|
|
379
|
+
const envVarMatches = configStr.match(/\$\{([^}]+)\}/g);
|
|
380
|
+
if (envVarMatches) {
|
|
381
|
+
envVarMatches.forEach((match) => {
|
|
382
|
+
const envVar = match.slice(2, -1);
|
|
383
|
+
if (!process.env[envVar]) {
|
|
384
|
+
issues.push({
|
|
385
|
+
category: 'environment',
|
|
386
|
+
severity: 'error',
|
|
387
|
+
code: 'ENV_VAR_MISSING',
|
|
388
|
+
message: `Required environment variable missing: ${envVar}`,
|
|
389
|
+
fix: `Set environment variable: export ${envVar}=your_key`,
|
|
390
|
+
autoFixable: false,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
// Config issues handled elsewhere
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return issues;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Execute a command safely using execFile with args array (mockable for testing)
|
|
404
|
+
* @param {string} command - Command to execute
|
|
405
|
+
* @param {Array<string>} args - Command arguments
|
|
406
|
+
* @param {object} options - Execution options
|
|
407
|
+
* @returns {Promise<object>} Execution result
|
|
408
|
+
*/
|
|
409
|
+
async execSafe(command, args = [], options = {}) {
|
|
410
|
+
const { execFile } = require('child_process');
|
|
411
|
+
const { promisify } = require('util');
|
|
412
|
+
const execFileAsync = promisify(execFile);
|
|
413
|
+
|
|
414
|
+
// Validate command is in allowlist
|
|
415
|
+
const ALLOWED_COMMANDS = ['npm', 'chmod', 'node', 'icacls'];
|
|
416
|
+
if (!ALLOWED_COMMANDS.includes(command)) {
|
|
417
|
+
throw new Error(`Command not allowed: ${command}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Sanitize arguments
|
|
421
|
+
const sanitizedArgs = args.map((arg) => {
|
|
422
|
+
if (typeof arg !== 'string') {
|
|
423
|
+
throw new Error('All arguments must be strings');
|
|
424
|
+
}
|
|
425
|
+
// Remove shell metacharacters
|
|
426
|
+
return arg.replace(/[;&|`$(){}[\]<>"'\\]/g, '');
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const execOptions = {
|
|
430
|
+
shell: false, // Never use shell to prevent injection
|
|
431
|
+
timeout: options.timeout || 30000,
|
|
432
|
+
env: { ...process.env, PATH: process.env.PATH }, // Controlled environment
|
|
433
|
+
...options,
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
return await execFileAsync(command, sanitizedArgs, execOptions);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Auto-fix an issue if possible
|
|
441
|
+
* @param {object} issue - Issue to fix
|
|
442
|
+
* @returns {Promise<object>} Fix result
|
|
443
|
+
*/
|
|
444
|
+
async autoFix(issue) {
|
|
445
|
+
if (!issue.autoFixable) {
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
message: 'Issue is not auto-fixable',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
if (issue.code === 'NPM_DEPENDENCY_MISSING') {
|
|
454
|
+
// Use execSafe with validated command and args
|
|
455
|
+
const result = await this.execSafe('npm', [
|
|
456
|
+
'install',
|
|
457
|
+
'--no-fund',
|
|
458
|
+
'--no-audit',
|
|
459
|
+
]);
|
|
460
|
+
return {
|
|
461
|
+
success: true,
|
|
462
|
+
message: result.stdout || 'Dependencies installed',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (issue.code === 'INSECURE_PERMISSIONS') {
|
|
467
|
+
// Validate config path is within project directory
|
|
468
|
+
const configPath = path.resolve(this.options.configPath);
|
|
469
|
+
const projectDir = path.resolve('.');
|
|
470
|
+
if (!configPath.startsWith(projectDir)) {
|
|
471
|
+
throw new Error('Config path outside project directory');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check if running on Windows
|
|
475
|
+
if (process.platform === 'win32') {
|
|
476
|
+
// On Windows, attempt to use icacls or fall back to fs.chmod
|
|
477
|
+
try {
|
|
478
|
+
await this.execSafe('icacls', [
|
|
479
|
+
configPath,
|
|
480
|
+
'/inheritance:r',
|
|
481
|
+
'/grant',
|
|
482
|
+
`${require('os').userInfo().username}:F`,
|
|
483
|
+
]);
|
|
484
|
+
} catch (icaclsError) {
|
|
485
|
+
// Fall back to fs.chmod if icacls fails
|
|
486
|
+
try {
|
|
487
|
+
await fs.chmod(configPath, 0o600);
|
|
488
|
+
} catch (chmodError) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`Failed to update permissions: ${chmodError.message}`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
// Use chmod on Unix-like systems
|
|
496
|
+
await this.execSafe('chmod', ['600', configPath]);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
success: true,
|
|
501
|
+
message: 'Permissions updated',
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
message: 'Auto-fix not implemented for this issue type',
|
|
508
|
+
};
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return {
|
|
511
|
+
success: false,
|
|
512
|
+
message: `Auto-fix failed: ${error.message}`,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Generate diagnostic report
|
|
519
|
+
* @param {Array} issues - Array of issues
|
|
520
|
+
* @returns {object} Diagnostic report
|
|
521
|
+
*/
|
|
522
|
+
generateReport(issues) {
|
|
523
|
+
const summary = {
|
|
524
|
+
totalIssues: issues.length,
|
|
525
|
+
errors: issues.filter((i) => i.severity === 'error').length,
|
|
526
|
+
warnings: issues.filter((i) => i.severity === 'warning').length,
|
|
527
|
+
info: issues.filter((i) => i.severity === 'info').length,
|
|
528
|
+
healthScore: Math.max(
|
|
529
|
+
0,
|
|
530
|
+
100 -
|
|
531
|
+
(issues.filter((i) => i.severity === 'error').length * 25 +
|
|
532
|
+
issues.filter((i) => i.severity === 'warning').length * 10 +
|
|
533
|
+
issues.filter((i) => i.severity === 'info').length * 2),
|
|
534
|
+
),
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const categories = {
|
|
538
|
+
configuration: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
539
|
+
plugins: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
540
|
+
dependencies: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
541
|
+
performance: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
542
|
+
security: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
543
|
+
environment: { issues: 0, errors: 0, warnings: 0, info: 0 },
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
issues.forEach((issue) => {
|
|
547
|
+
if (categories[issue.category]) {
|
|
548
|
+
categories[issue.category].issues++;
|
|
549
|
+
if (issue.severity === 'error') {
|
|
550
|
+
categories[issue.category].errors++;
|
|
551
|
+
} else if (issue.severity === 'warning') {
|
|
552
|
+
categories[issue.category].warnings++;
|
|
553
|
+
} else if (issue.severity === 'info') {
|
|
554
|
+
categories[issue.category].info++;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
timestamp: new Date().toISOString(),
|
|
561
|
+
summary,
|
|
562
|
+
categories,
|
|
563
|
+
issues,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Run all diagnostic checks
|
|
569
|
+
* @returns {Promise<object>} Diagnostic report
|
|
570
|
+
*/
|
|
571
|
+
async run() {
|
|
572
|
+
const allIssues = [];
|
|
573
|
+
|
|
574
|
+
const categoriesToRun = this.options.categories.includes('all')
|
|
575
|
+
? Object.values(DIAGNOSTIC_CATEGORIES)
|
|
576
|
+
: this.options.categories;
|
|
577
|
+
|
|
578
|
+
if (categoriesToRun.includes('configuration')) {
|
|
579
|
+
const configIssues = await this.checkConfiguration();
|
|
580
|
+
allIssues.push(...configIssues);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (categoriesToRun.includes('plugins')) {
|
|
584
|
+
const pluginIssues = await this.checkPlugins();
|
|
585
|
+
allIssues.push(...pluginIssues);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (categoriesToRun.includes('dependencies')) {
|
|
589
|
+
const depIssues = await this.checkDependencies();
|
|
590
|
+
allIssues.push(...depIssues);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (categoriesToRun.includes('performance')) {
|
|
594
|
+
const perfIssues = await this.checkPerformance();
|
|
595
|
+
allIssues.push(...perfIssues);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (categoriesToRun.includes('security')) {
|
|
599
|
+
const secIssues = await this.checkSecurity();
|
|
600
|
+
allIssues.push(...secIssues);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (categoriesToRun.includes('environment')) {
|
|
604
|
+
const envIssues = await this.checkEnvironment();
|
|
605
|
+
allIssues.push(...envIssues);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Auto-fix issues if enabled
|
|
609
|
+
if (this.options.autoFix) {
|
|
610
|
+
for (const issue of allIssues) {
|
|
611
|
+
if (issue.autoFixable) {
|
|
612
|
+
await this.autoFix(issue);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return this.generateReport(allIssues);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Main doctor function for CLI usage
|
|
623
|
+
* @param {object} options - Doctor options
|
|
624
|
+
* @returns {Promise<object>} Diagnostic report
|
|
625
|
+
*/
|
|
626
|
+
async function runPipelineDoctor(options = {}) {
|
|
627
|
+
const doctor = new PipelineDoctor(options);
|
|
628
|
+
return await doctor.run();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
PipelineDoctor,
|
|
633
|
+
runPipelineDoctor,
|
|
634
|
+
DIAGNOSTIC_CATEGORIES,
|
|
635
|
+
SEVERITY_LEVELS,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// CommonJS/ESM interop
|
|
639
|
+
module.exports.default = module.exports;
|