@rahul05ranjan/dhruv-cli 1.3.0 → 1.4.6
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/.github/ENTERPRISE.md +275 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +45 -17
- package/.github/ISSUE_TEMPLATE/documentation_issue.md +61 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +61 -9
- package/.github/ISSUE_TEMPLATE/security_vulnerability.md +74 -0
- package/.github/dependabot.yml +42 -2
- package/.github/pull_request_template.md +13 -0
- package/.github/workflows/build-publish.yml +154 -0
- package/.github/workflows/ci.yml +251 -18
- package/.github/workflows/contribution.yml +169 -27
- package/.github/workflows/dependabot-auto-merge.yml +61 -2
- package/.github/workflows/deploy.yml +336 -0
- package/.github/workflows/monitoring.yml +270 -0
- package/.github/workflows/release.yml +229 -0
- package/.github/workflows/security.yml +198 -0
- package/.releaserc.json +50 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +8 -0
- package/PUBLISHING_FIX.md +92 -0
- package/__tests__/core.test.ts +318 -0
- package/__tests__/setup.ts +61 -0
- package/__tests__/workflows.test.ts +95 -0
- package/dist/commands/explain.js +13 -44
- package/dist/commands/fix.js +13 -38
- package/dist/commands/generate.js +45 -54
- package/dist/commands/health.d.ts +1 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/init.js +47 -42
- package/dist/commands/menu.js +90 -2
- package/dist/commands/metrics.d.ts +1 -0
- package/dist/commands/metrics.js +51 -0
- package/dist/commands/optimize.js +42 -34
- package/dist/commands/review.js +52 -48
- package/dist/commands/security-check.js +52 -42
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +45 -0
- package/dist/commands/suggest.js +13 -39
- package/dist/config/config.js +30 -2
- package/dist/core/ai.d.ts +77 -2
- package/dist/core/ai.js +207 -30
- package/dist/core/command-runner.d.ts +17 -0
- package/dist/core/command-runner.js +78 -0
- package/dist/core/logger.d.ts +40 -0
- package/dist/core/logger.js +138 -0
- package/dist/core/metrics.d.ts +34 -0
- package/dist/core/metrics.js +206 -0
- package/dist/core/prompts.d.ts +1 -0
- package/dist/core/prompts.js +121 -0
- package/dist/core/security.d.ts +34 -0
- package/dist/core/security.js +197 -0
- package/dist/index.js +43 -3
- package/dist/utils/ux.d.ts +3 -0
- package/dist/utils/ux.js +15 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +45 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/api/.nojekyll +1 -0
- package/docs/api/assets/hierarchy.js +1 -0
- package/docs/api/assets/highlight.css +71 -0
- package/docs/api/assets/icons.js +18 -0
- package/docs/api/assets/icons.svg +1 -0
- package/docs/api/assets/main.js +60 -0
- package/docs/api/assets/navigation.js +1 -0
- package/docs/api/assets/search.js +1 -0
- package/docs/api/assets/style.css +1633 -0
- package/docs/api/hierarchy.html +1 -0
- package/docs/api/index.html +39 -0
- package/docs/api/modules.html +1 -0
- package/eslint.config.js +170 -0
- package/jest.config.json +37 -0
- package/lighthouserc.json +22 -0
- package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +15 -0
- package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +15 -0
- package/package.json +62 -8
- package/src/commands/explain.ts +13 -42
- package/src/commands/fix.ts +13 -31
- package/src/commands/generate.ts +47 -46
- package/src/commands/health.ts +440 -0
- package/src/commands/init.ts +48 -42
- package/src/commands/menu.ts +86 -2
- package/src/commands/metrics.ts +65 -0
- package/src/commands/optimize.ts +40 -28
- package/src/commands/review.ts +54 -40
- package/src/commands/security-check.ts +54 -34
- package/src/commands/status.ts +47 -0
- package/src/commands/suggest.ts +13 -32
- package/src/config/config.ts +35 -2
- package/src/core/ai.ts +237 -26
- package/src/core/command-runner.ts +105 -0
- package/src/core/logger.ts +194 -0
- package/src/core/metrics.ts +232 -0
- package/src/core/prompts.ts +128 -0
- package/src/core/security.ts +243 -0
- package/src/index.ts +50 -3
- package/src/utils/ux.ts +18 -0
- package/test-suite.sh +147 -0
- package/tsconfig.json +3 -2
- package/typedoc.json +44 -0
- package/types/global.d.ts +13 -0
- package/validate-workflows.sh +270 -0
- package/.eslintignore +0 -1
- package/.eslintrc.cjs +0 -43
- package/src/core/ai.test.js +0 -40
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import promClient from 'prom-client';
|
|
2
|
+
import { logger } from './logger.js';
|
|
3
|
+
// Create a Registry which registers the metrics
|
|
4
|
+
const register = new promClient.Registry();
|
|
5
|
+
// Add a default label which is added to all metrics
|
|
6
|
+
register.setDefaultLabels({
|
|
7
|
+
app: 'dhruv-cli'
|
|
8
|
+
});
|
|
9
|
+
// Enable the collection of default metrics
|
|
10
|
+
promClient.collectDefaultMetrics({ register });
|
|
11
|
+
// Custom metrics
|
|
12
|
+
export const metrics = {
|
|
13
|
+
// Command execution metrics
|
|
14
|
+
commandDuration: new promClient.Histogram({
|
|
15
|
+
name: 'dhruv_command_duration_seconds',
|
|
16
|
+
help: 'Duration of command execution in seconds',
|
|
17
|
+
labelNames: ['command', 'success'],
|
|
18
|
+
buckets: [0.1, 0.5, 1, 2, 5, 10, 30]
|
|
19
|
+
}),
|
|
20
|
+
commandCount: new promClient.Counter({
|
|
21
|
+
name: 'dhruv_command_total',
|
|
22
|
+
help: 'Total number of commands executed',
|
|
23
|
+
labelNames: ['command', 'success']
|
|
24
|
+
}),
|
|
25
|
+
// AI service metrics
|
|
26
|
+
aiRequestDuration: new promClient.Histogram({
|
|
27
|
+
name: 'dhruv_ai_request_duration_seconds',
|
|
28
|
+
help: 'Duration of AI requests in seconds',
|
|
29
|
+
labelNames: ['model', 'command_type'],
|
|
30
|
+
buckets: [1, 5, 10, 30, 60, 120]
|
|
31
|
+
}),
|
|
32
|
+
aiRequestCount: new promClient.Counter({
|
|
33
|
+
name: 'dhruv_ai_request_total',
|
|
34
|
+
help: 'Total number of AI requests',
|
|
35
|
+
labelNames: ['model', 'command_type', 'success']
|
|
36
|
+
}),
|
|
37
|
+
aiTokensUsed: new promClient.Counter({
|
|
38
|
+
name: 'dhruv_ai_tokens_total',
|
|
39
|
+
help: 'Total number of tokens used in AI requests',
|
|
40
|
+
labelNames: ['model', 'command_type']
|
|
41
|
+
}),
|
|
42
|
+
// Cache metrics
|
|
43
|
+
cacheHitCount: new promClient.Counter({
|
|
44
|
+
name: 'dhruv_cache_hit_total',
|
|
45
|
+
help: 'Total number of cache hits',
|
|
46
|
+
labelNames: ['cache_type']
|
|
47
|
+
}),
|
|
48
|
+
cacheMissCount: new promClient.Counter({
|
|
49
|
+
name: 'dhruv_cache_miss_total',
|
|
50
|
+
help: 'Total number of cache misses',
|
|
51
|
+
labelNames: ['cache_type']
|
|
52
|
+
}),
|
|
53
|
+
cacheSize: new promClient.Gauge({
|
|
54
|
+
name: 'dhruv_cache_size_bytes',
|
|
55
|
+
help: 'Current size of cache in bytes',
|
|
56
|
+
labelNames: ['cache_type']
|
|
57
|
+
}),
|
|
58
|
+
// Error metrics
|
|
59
|
+
errorCount: new promClient.Counter({
|
|
60
|
+
name: 'dhruv_error_total',
|
|
61
|
+
help: 'Total number of errors',
|
|
62
|
+
labelNames: ['error_type', 'command']
|
|
63
|
+
}),
|
|
64
|
+
// Performance metrics
|
|
65
|
+
memoryUsage: new promClient.Gauge({
|
|
66
|
+
name: 'dhruv_memory_usage_bytes',
|
|
67
|
+
help: 'Current memory usage in bytes',
|
|
68
|
+
labelNames: ['type']
|
|
69
|
+
}),
|
|
70
|
+
// User engagement metrics
|
|
71
|
+
sessionCount: new promClient.Counter({
|
|
72
|
+
name: 'dhruv_session_total',
|
|
73
|
+
help: 'Total number of user sessions'
|
|
74
|
+
}),
|
|
75
|
+
pluginLoadedCount: new promClient.Counter({
|
|
76
|
+
name: 'dhruv_plugin_loaded_total',
|
|
77
|
+
help: 'Total number of plugins loaded'
|
|
78
|
+
})
|
|
79
|
+
};
|
|
80
|
+
// Register all metrics
|
|
81
|
+
Object.values(metrics).forEach(metric => {
|
|
82
|
+
register.registerMetric(metric);
|
|
83
|
+
});
|
|
84
|
+
export class MetricsCollector {
|
|
85
|
+
constructor() {
|
|
86
|
+
this.metricsEnabled = process.env.DHRUV_METRICS_ENABLED !== 'false';
|
|
87
|
+
if (this.metricsEnabled) {
|
|
88
|
+
logger.info('Metrics collection enabled');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
static getInstance() {
|
|
92
|
+
if (!MetricsCollector.instance) {
|
|
93
|
+
MetricsCollector.instance = new MetricsCollector();
|
|
94
|
+
}
|
|
95
|
+
return MetricsCollector.instance;
|
|
96
|
+
}
|
|
97
|
+
recordCommand(command, duration, success) {
|
|
98
|
+
if (!this.metricsEnabled)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
metrics.commandDuration.observe({ command, success: success.toString() }, duration / 1000);
|
|
102
|
+
metrics.commandCount.inc({ command, success: success.toString() });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
logger.error('Failed to record command metrics', error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
recordAIRequest(model, commandType, duration, success, tokensUsed) {
|
|
109
|
+
if (!this.metricsEnabled)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
metrics.aiRequestDuration.observe({ model, command_type: commandType }, duration / 1000);
|
|
113
|
+
metrics.aiRequestCount.inc({ model, command_type: commandType, success: success.toString() });
|
|
114
|
+
if (tokensUsed) {
|
|
115
|
+
metrics.aiTokensUsed.inc({ model, command_type: commandType }, tokensUsed);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
logger.error('Failed to record AI request metrics', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
recordCacheHit(cacheType) {
|
|
123
|
+
if (!this.metricsEnabled)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
metrics.cacheHitCount.inc({ cache_type: cacheType });
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
logger.error('Failed to record cache hit metrics', error);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
recordCacheMiss(cacheType) {
|
|
133
|
+
if (!this.metricsEnabled)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
metrics.cacheMissCount.inc({ cache_type: cacheType });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error('Failed to record cache miss metrics', error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
updateCacheSize(cacheType, size) {
|
|
143
|
+
if (!this.metricsEnabled)
|
|
144
|
+
return;
|
|
145
|
+
try {
|
|
146
|
+
metrics.cacheSize.set({ cache_type: cacheType }, size);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
logger.error('Failed to update cache size metrics', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
recordError(errorType, command) {
|
|
153
|
+
if (!this.metricsEnabled)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
metrics.errorCount.inc({ error_type: errorType, command: command || 'unknown' });
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
logger.error('Failed to record error metrics', error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
updateMemoryUsage() {
|
|
163
|
+
if (!this.metricsEnabled)
|
|
164
|
+
return;
|
|
165
|
+
try {
|
|
166
|
+
const memUsage = process.memoryUsage();
|
|
167
|
+
metrics.memoryUsage.set({ type: 'rss' }, memUsage.rss);
|
|
168
|
+
metrics.memoryUsage.set({ type: 'heap_used' }, memUsage.heapUsed);
|
|
169
|
+
metrics.memoryUsage.set({ type: 'heap_total' }, memUsage.heapTotal);
|
|
170
|
+
metrics.memoryUsage.set({ type: 'external' }, memUsage.external);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
logger.error('Failed to update memory usage metrics', error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
recordSession() {
|
|
177
|
+
if (!this.metricsEnabled)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
metrics.sessionCount.inc();
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
logger.error('Failed to record session metrics', error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
recordPluginLoaded() {
|
|
187
|
+
if (!this.metricsEnabled)
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
metrics.pluginLoadedCount.inc();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger.error('Failed to record plugin loaded metrics', error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async getMetrics() {
|
|
197
|
+
return register.metrics();
|
|
198
|
+
}
|
|
199
|
+
async getMetricsJSON() {
|
|
200
|
+
return register.getMetricsAsJSON();
|
|
201
|
+
}
|
|
202
|
+
getRegistry() {
|
|
203
|
+
return register;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export const metricsCollector = MetricsCollector.getInstance();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getSystemMessage(type: string): string;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System-message templates per command type. Pure data: no AI plumbing.
|
|
3
|
+
*/
|
|
4
|
+
const SYSTEM_MESSAGES = {
|
|
5
|
+
explain: `You are a knowledgeable programming instructor and technical expert. Your role is to provide clear, accurate, and practical explanations of technical concepts.
|
|
6
|
+
|
|
7
|
+
Guidelines:
|
|
8
|
+
- Provide structured explanations with clear sections
|
|
9
|
+
- Use practical examples where helpful
|
|
10
|
+
- Focus on concepts that developers need to understand
|
|
11
|
+
- Avoid unnecessary jargon, but use precise technical terminology
|
|
12
|
+
- Keep responses concise but comprehensive
|
|
13
|
+
- Format code examples clearly
|
|
14
|
+
- Provide context for why something matters
|
|
15
|
+
|
|
16
|
+
Output Format:
|
|
17
|
+
- Use plain text with clear structure
|
|
18
|
+
- Use bullet points for lists
|
|
19
|
+
- Use numbered steps for procedures
|
|
20
|
+
- Separate code examples with clear labels
|
|
21
|
+
- Avoid markdown formatting except for code blocks when absolutely necessary`,
|
|
22
|
+
suggest: `You are an expert software architect and senior developer providing actionable recommendations and best practices.
|
|
23
|
+
|
|
24
|
+
Guidelines:
|
|
25
|
+
- Provide specific, actionable suggestions
|
|
26
|
+
- Prioritize recommendations by importance
|
|
27
|
+
- Include practical implementation steps
|
|
28
|
+
- Mention relevant tools, libraries, or patterns
|
|
29
|
+
- Consider performance, maintainability, and scalability
|
|
30
|
+
- Provide concrete examples where helpful
|
|
31
|
+
- Focus on industry best practices
|
|
32
|
+
|
|
33
|
+
Output Format:
|
|
34
|
+
- Use clear numbered or bulleted lists
|
|
35
|
+
- Separate different types of suggestions
|
|
36
|
+
- Provide brief explanations for each recommendation
|
|
37
|
+
- Use plain text formatting for better readability`,
|
|
38
|
+
fix: `You are a debugging expert and problem-solving specialist helping developers resolve technical issues.
|
|
39
|
+
|
|
40
|
+
Guidelines:
|
|
41
|
+
- Analyze the problem systematically
|
|
42
|
+
- Identify the root cause
|
|
43
|
+
- Provide step-by-step solutions
|
|
44
|
+
- Include preventive measures
|
|
45
|
+
- Show corrected code examples
|
|
46
|
+
- Explain why the fix works
|
|
47
|
+
- Suggest testing approaches
|
|
48
|
+
|
|
49
|
+
Output Format:
|
|
50
|
+
- Start with problem analysis
|
|
51
|
+
- Provide clear solution steps
|
|
52
|
+
- Show before/after code examples
|
|
53
|
+
- Use plain text with clear structure
|
|
54
|
+
- Avoid complex markdown formatting`,
|
|
55
|
+
review: `You are a senior code reviewer focused on code quality, best practices, and maintainability.
|
|
56
|
+
|
|
57
|
+
Guidelines:
|
|
58
|
+
- Analyze code structure and patterns
|
|
59
|
+
- Identify potential issues or improvements
|
|
60
|
+
- Comment on performance implications
|
|
61
|
+
- Suggest refactoring opportunities
|
|
62
|
+
- Check for security considerations
|
|
63
|
+
- Evaluate readability and maintainability
|
|
64
|
+
- Provide constructive feedback
|
|
65
|
+
|
|
66
|
+
Output Format:
|
|
67
|
+
- Organize feedback by categories (Structure, Performance, Security, etc.)
|
|
68
|
+
- Use clear, actionable language
|
|
69
|
+
- Provide specific line-by-line suggestions where relevant
|
|
70
|
+
- Use plain text formatting for better CLI readability`,
|
|
71
|
+
optimize: `You are a performance optimization expert specializing in code efficiency and best practices.
|
|
72
|
+
|
|
73
|
+
Guidelines:
|
|
74
|
+
- Focus on measurable performance improvements
|
|
75
|
+
- Consider different types of optimization (runtime, memory, bundle size, etc.)
|
|
76
|
+
- Provide specific, implementable suggestions
|
|
77
|
+
- Explain the impact of each optimization
|
|
78
|
+
- Consider trade-offs between performance and maintainability
|
|
79
|
+
- Suggest profiling and measurement approaches
|
|
80
|
+
|
|
81
|
+
Output Format:
|
|
82
|
+
- Categorize optimizations by type
|
|
83
|
+
- Provide clear before/after examples
|
|
84
|
+
- Include estimated impact where possible
|
|
85
|
+
- Use structured plain text formatting`,
|
|
86
|
+
security: `You are a cybersecurity expert specializing in application security and secure coding practices.
|
|
87
|
+
|
|
88
|
+
Guidelines:
|
|
89
|
+
- Identify potential security vulnerabilities
|
|
90
|
+
- Provide remediation steps
|
|
91
|
+
- Suggest secure coding patterns
|
|
92
|
+
- Consider common attack vectors
|
|
93
|
+
- Recommend security tools and practices
|
|
94
|
+
- Focus on practical security measures
|
|
95
|
+
- Explain security implications
|
|
96
|
+
|
|
97
|
+
Output Format:
|
|
98
|
+
- Categorize findings by severity
|
|
99
|
+
- Provide clear remediation steps
|
|
100
|
+
- Use plain text formatting for better readability
|
|
101
|
+
- Include references to security standards where relevant`,
|
|
102
|
+
generate: `You are a code generation specialist creating high-quality, well-structured code.
|
|
103
|
+
|
|
104
|
+
Guidelines:
|
|
105
|
+
- Generate clean, readable code
|
|
106
|
+
- Follow established conventions
|
|
107
|
+
- Include appropriate comments
|
|
108
|
+
- Consider edge cases
|
|
109
|
+
- Use proper error handling
|
|
110
|
+
- Follow best practices for the target language
|
|
111
|
+
- Generate comprehensive test cases when requested
|
|
112
|
+
|
|
113
|
+
Output Format:
|
|
114
|
+
- Provide clean code without excessive markdown
|
|
115
|
+
- Use minimal formatting for better CLI display
|
|
116
|
+
- Include brief explanations only when necessary
|
|
117
|
+
- Focus on practical, working code`,
|
|
118
|
+
};
|
|
119
|
+
export function getSystemMessage(type) {
|
|
120
|
+
return SYSTEM_MESSAGES[type] ?? SYSTEM_MESSAGES.explain;
|
|
121
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface SecurityConfig {
|
|
2
|
+
enableInputValidation: boolean;
|
|
3
|
+
enableRateLimiting: boolean;
|
|
4
|
+
maxInputLength: number;
|
|
5
|
+
allowedCommands: string[];
|
|
6
|
+
blockedPatterns: RegExp[];
|
|
7
|
+
maxRequestsPerMinute: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class SecurityManager {
|
|
10
|
+
private static instance;
|
|
11
|
+
private config;
|
|
12
|
+
private requestCounts;
|
|
13
|
+
private schemas;
|
|
14
|
+
private constructor();
|
|
15
|
+
private initializeSchemas;
|
|
16
|
+
static getInstance(): SecurityManager;
|
|
17
|
+
validateInput(command: string, input: any): {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
checkRateLimit(identifier: string): {
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
remainingRequests: number;
|
|
24
|
+
};
|
|
25
|
+
sanitizeInput(input: any): any;
|
|
26
|
+
auditLog(action: string, details: Record<string, any>): void;
|
|
27
|
+
getSecurityStatus(): {
|
|
28
|
+
config: SecurityConfig;
|
|
29
|
+
activeRateLimits: number;
|
|
30
|
+
blockedPatternsCount: number;
|
|
31
|
+
};
|
|
32
|
+
resetRateLimits(): void;
|
|
33
|
+
}
|
|
34
|
+
export declare const securityManager: SecurityManager;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import Joi from 'joi';
|
|
2
|
+
import validator from 'validator';
|
|
3
|
+
import { logger, logSecurity } from './logger.js';
|
|
4
|
+
import { metricsCollector } from './metrics.js';
|
|
5
|
+
export class SecurityManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.requestCounts = new Map();
|
|
8
|
+
this.schemas = {};
|
|
9
|
+
this.config = {
|
|
10
|
+
enableInputValidation: process.env.DHRUV_SECURITY_VALIDATION !== 'false',
|
|
11
|
+
enableRateLimiting: process.env.DHRUV_SECURITY_RATE_LIMIT !== 'false',
|
|
12
|
+
maxInputLength: parseInt(process.env.DHRUV_MAX_INPUT_LENGTH || '10000'),
|
|
13
|
+
allowedCommands: [
|
|
14
|
+
'explain', 'suggest', 'fix', 'review', 'optimize',
|
|
15
|
+
'security-check', 'generate', 'init', 'status',
|
|
16
|
+
'project-type', 'menu', 'completion', 'help'
|
|
17
|
+
],
|
|
18
|
+
blockedPatterns: [
|
|
19
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, // XSS patterns
|
|
20
|
+
/\b(eval|exec|system|shell_exec|passthru|proc_open|popen)\b/i, // Code execution
|
|
21
|
+
/\b(rm\s+(-rf|--force)\s+|del\s+)/i, // File deletion
|
|
22
|
+
/\b(format|fdisk|mkfs)\b/i, // Disk operations
|
|
23
|
+
/password\s*=\s*['"][^'"]*['"]/i, // Password exposure
|
|
24
|
+
/\b(ssh|scp|rsync)\s+.*@\w+/i, // SSH operations
|
|
25
|
+
],
|
|
26
|
+
maxRequestsPerMinute: parseInt(process.env.DHRUV_RATE_LIMIT || '60')
|
|
27
|
+
};
|
|
28
|
+
this.initializeSchemas();
|
|
29
|
+
logger.info('Security manager initialized', {
|
|
30
|
+
config: {
|
|
31
|
+
...this.config,
|
|
32
|
+
blockedPatterns: this.config.blockedPatterns.map(p => p.toString())
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
initializeSchemas() {
|
|
37
|
+
this.schemas = {
|
|
38
|
+
explain: Joi.object({
|
|
39
|
+
query: Joi.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(this.config.maxInputLength)
|
|
42
|
+
.required()
|
|
43
|
+
.messages({
|
|
44
|
+
'string.empty': 'Query cannot be empty',
|
|
45
|
+
'string.max': `Query too long (max ${this.config.maxInputLength} characters)`
|
|
46
|
+
})
|
|
47
|
+
}),
|
|
48
|
+
suggest: Joi.object({
|
|
49
|
+
query: Joi.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.max(this.config.maxInputLength)
|
|
52
|
+
.required()
|
|
53
|
+
}),
|
|
54
|
+
fix: Joi.object({
|
|
55
|
+
query: Joi.string()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(this.config.maxInputLength)
|
|
58
|
+
.required()
|
|
59
|
+
}),
|
|
60
|
+
review: Joi.object({
|
|
61
|
+
fileOrDir: Joi.string()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(500)
|
|
64
|
+
.pattern(/^[^<>&|;$`]*$/) // Prevent shell injection
|
|
65
|
+
.required()
|
|
66
|
+
}),
|
|
67
|
+
optimize: Joi.object({
|
|
68
|
+
file: Joi.string()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(500)
|
|
71
|
+
.pattern(/^[^<>&|;$`]*$/)
|
|
72
|
+
.required()
|
|
73
|
+
}),
|
|
74
|
+
generate: Joi.object({
|
|
75
|
+
type: Joi.string()
|
|
76
|
+
.valid('tests', 'documentation', 'docs', 'component')
|
|
77
|
+
.required(),
|
|
78
|
+
target: Joi.string()
|
|
79
|
+
.min(1)
|
|
80
|
+
.max(500)
|
|
81
|
+
.pattern(/^[^<>&|;$`]*$/)
|
|
82
|
+
.required()
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
static getInstance() {
|
|
87
|
+
if (!SecurityManager.instance) {
|
|
88
|
+
SecurityManager.instance = new SecurityManager();
|
|
89
|
+
}
|
|
90
|
+
return SecurityManager.instance;
|
|
91
|
+
}
|
|
92
|
+
validateInput(command, input) {
|
|
93
|
+
if (!this.config.enableInputValidation) {
|
|
94
|
+
return { valid: true };
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Check command is allowed
|
|
98
|
+
if (!this.config.allowedCommands.includes(command)) {
|
|
99
|
+
const error = `Command '${command}' is not allowed`;
|
|
100
|
+
logSecurity('blocked_command', { command, input });
|
|
101
|
+
metricsCollector.recordError('blocked_command', command);
|
|
102
|
+
return { valid: false, error };
|
|
103
|
+
}
|
|
104
|
+
// Get schema for command
|
|
105
|
+
const schema = this.schemas[command];
|
|
106
|
+
if (!schema) {
|
|
107
|
+
return { valid: true }; // No specific validation for this command
|
|
108
|
+
}
|
|
109
|
+
// Validate input
|
|
110
|
+
const { error } = schema.validate(input, { abortEarly: false });
|
|
111
|
+
if (error) {
|
|
112
|
+
const errorMessage = error.details.map((d) => d.message).join(', ');
|
|
113
|
+
logSecurity('invalid_input', { command, input, error: errorMessage });
|
|
114
|
+
metricsCollector.recordError('invalid_input', command);
|
|
115
|
+
return { valid: false, error: errorMessage };
|
|
116
|
+
}
|
|
117
|
+
// Check for blocked patterns
|
|
118
|
+
const inputString = JSON.stringify(input);
|
|
119
|
+
for (const pattern of this.config.blockedPatterns) {
|
|
120
|
+
if (pattern.test(inputString)) {
|
|
121
|
+
const error = 'Input contains blocked patterns';
|
|
122
|
+
logSecurity('blocked_pattern', { command, input, pattern: pattern.toString() });
|
|
123
|
+
metricsCollector.recordError('blocked_pattern', command);
|
|
124
|
+
return { valid: false, error };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Sanitize input
|
|
128
|
+
const sanitized = this.sanitizeInput(input);
|
|
129
|
+
if (JSON.stringify(sanitized) !== JSON.stringify(input)) {
|
|
130
|
+
logger.warn('Input was sanitized', { command, original: input, sanitized });
|
|
131
|
+
}
|
|
132
|
+
return { valid: true };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const error = `Validation error: ${err.message}`;
|
|
136
|
+
logSecurity('validation_error', { command, input, error });
|
|
137
|
+
metricsCollector.recordError('validation_error', command);
|
|
138
|
+
return { valid: false, error };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
checkRateLimit(identifier) {
|
|
142
|
+
if (!this.config.enableRateLimiting) {
|
|
143
|
+
return { allowed: true, remainingRequests: this.config.maxRequestsPerMinute };
|
|
144
|
+
}
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const windowStart = Math.floor(now / 60000) * 60000; // 1-minute windows
|
|
147
|
+
const windowKey = `${identifier}:${windowStart}`;
|
|
148
|
+
const current = this.requestCounts.get(windowKey) || { count: 0, resetTime: windowStart + 60000 };
|
|
149
|
+
if (now > current.resetTime) {
|
|
150
|
+
// Reset window
|
|
151
|
+
this.requestCounts.set(windowKey, { count: 1, resetTime: windowStart + 60000 });
|
|
152
|
+
return { allowed: true, remainingRequests: this.config.maxRequestsPerMinute - 1 };
|
|
153
|
+
}
|
|
154
|
+
if (current.count >= this.config.maxRequestsPerMinute) {
|
|
155
|
+
logSecurity('rate_limit_exceeded', { identifier, count: current.count });
|
|
156
|
+
metricsCollector.recordError('rate_limit_exceeded', 'unknown');
|
|
157
|
+
return { allowed: false, remainingRequests: 0 };
|
|
158
|
+
}
|
|
159
|
+
current.count++;
|
|
160
|
+
const remaining = Math.max(0, this.config.maxRequestsPerMinute - current.count);
|
|
161
|
+
return { allowed: true, remainingRequests: remaining };
|
|
162
|
+
}
|
|
163
|
+
sanitizeInput(input) {
|
|
164
|
+
if (typeof input === 'string') {
|
|
165
|
+
return validator.escape(input);
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(input)) {
|
|
168
|
+
return input.map(item => this.sanitizeInput(item));
|
|
169
|
+
}
|
|
170
|
+
if (typeof input === 'object' && input !== null) {
|
|
171
|
+
const sanitized = {};
|
|
172
|
+
for (const [key, value] of Object.entries(input)) {
|
|
173
|
+
sanitized[key] = this.sanitizeInput(value);
|
|
174
|
+
}
|
|
175
|
+
return sanitized;
|
|
176
|
+
}
|
|
177
|
+
return input;
|
|
178
|
+
}
|
|
179
|
+
auditLog(action, details) {
|
|
180
|
+
logSecurity(action, {
|
|
181
|
+
timestamp: new Date().toISOString(),
|
|
182
|
+
...details
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
getSecurityStatus() {
|
|
186
|
+
return {
|
|
187
|
+
config: this.config,
|
|
188
|
+
activeRateLimits: this.requestCounts.size,
|
|
189
|
+
blockedPatternsCount: this.config.blockedPatterns.length
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
resetRateLimits() {
|
|
193
|
+
this.requestCounts.clear();
|
|
194
|
+
logger.info('Rate limits reset');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export const securityManager = SecurityManager.getInstance();
|
package/dist/index.js
CHANGED
|
@@ -11,9 +11,16 @@ import { optimize } from './commands/optimize.js';
|
|
|
11
11
|
import { securityCheck } from './commands/security-check.js';
|
|
12
12
|
import { generate } from './commands/generate.js';
|
|
13
13
|
import { init } from './commands/init.js';
|
|
14
|
+
import { status } from './commands/status.js';
|
|
15
|
+
import { health } from './commands/health.js';
|
|
16
|
+
import { metrics } from './commands/metrics.js';
|
|
14
17
|
import { detectProjectType } from './utils/projectType.js';
|
|
15
18
|
import { menu } from './commands/menu.js';
|
|
16
|
-
import
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
import { logger, logInfo, logError } from './core/logger.js';
|
|
21
|
+
import { metricsCollector } from './core/metrics.js';
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const pkg = require('../package.json');
|
|
17
24
|
const program = new Command();
|
|
18
25
|
program
|
|
19
26
|
.name('dhruv')
|
|
@@ -22,14 +29,17 @@ program
|
|
|
22
29
|
program
|
|
23
30
|
.command('explain <query>')
|
|
24
31
|
.description('Explain a concept or command')
|
|
32
|
+
.addHelpText('after', '\nExamples:\n $ dhruv explain "What is async/await?"\n $ dhruv explain "Docker containers vs VMs"')
|
|
25
33
|
.action(explain);
|
|
26
34
|
program
|
|
27
35
|
.command('suggest <query>')
|
|
28
36
|
.description('Get AI-powered suggestions')
|
|
37
|
+
.addHelpText('after', '\nExamples:\n $ dhruv suggest "React performance optimization"\n $ dhruv suggest "Node.js project structure"')
|
|
29
38
|
.action(suggest);
|
|
30
39
|
program
|
|
31
40
|
.command('fix <query>')
|
|
32
41
|
.description('Get a fix for a coding issue or error')
|
|
42
|
+
.addHelpText('after', '\nExamples:\n $ dhruv fix "TypeError: Cannot read property of undefined"\n $ dhruv fix "CORS error in Express.js"')
|
|
33
43
|
.action(fix);
|
|
34
44
|
program
|
|
35
45
|
.command('review <fileOrDir>')
|
|
@@ -51,6 +61,18 @@ program
|
|
|
51
61
|
.command('init')
|
|
52
62
|
.description('Interactive setup/configuration wizard')
|
|
53
63
|
.action(init);
|
|
64
|
+
program
|
|
65
|
+
.command('status')
|
|
66
|
+
.description('Check Ollama connection and available models')
|
|
67
|
+
.action(status);
|
|
68
|
+
program
|
|
69
|
+
.command('health')
|
|
70
|
+
.description('Run comprehensive health check')
|
|
71
|
+
.action(health);
|
|
72
|
+
program
|
|
73
|
+
.command('metrics')
|
|
74
|
+
.description('Display CLI usage metrics')
|
|
75
|
+
.action(metrics);
|
|
54
76
|
program
|
|
55
77
|
.command('project-type')
|
|
56
78
|
.description('Detect and print the current project type')
|
|
@@ -102,8 +124,26 @@ async function loadPlugins(program) {
|
|
|
102
124
|
}
|
|
103
125
|
}
|
|
104
126
|
(async () => {
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
// Initialize enterprise features
|
|
128
|
+
try {
|
|
129
|
+
logInfo('Dhruv CLI starting', {
|
|
130
|
+
version: pkg.version,
|
|
131
|
+
nodeVersion: process.version,
|
|
132
|
+
platform: process.platform
|
|
133
|
+
});
|
|
134
|
+
// Record session start
|
|
135
|
+
metricsCollector.recordSession();
|
|
136
|
+
await loadPlugins(program);
|
|
137
|
+
program.parse(process.argv);
|
|
138
|
+
// Record successful session
|
|
139
|
+
const sessionId = logger.getSessionId();
|
|
140
|
+
logInfo('CLI session completed', { sessionId });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
logError('CLI startup failed', error);
|
|
144
|
+
console.error(chalk.red('Failed to start Dhruv CLI:'), error.message);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
107
147
|
})();
|
|
108
148
|
// Autocomplete: Generate shell completion scripts
|
|
109
149
|
program
|
package/dist/utils/ux.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export declare function highlightCode(code: string, lang?: string): string;
|
|
2
2
|
export declare function printError(message: string): void;
|
|
3
3
|
export declare function printSuccess(message: string): void;
|
|
4
|
+
export declare function printWarning(message: string): void;
|
|
5
|
+
export declare function printInfo(message: string): void;
|
|
6
|
+
export declare function createSpinner(text: string): import("ora").Ora;
|
|
4
7
|
export declare function createProgressBar(total: number): any;
|
|
5
8
|
export declare function themed(text: string, type?: 'primary' | 'accent'): string;
|
package/dist/utils/ux.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
// @ts-expect-error: cli-progress has no type definitions
|
|
3
3
|
import cliProgress from 'cli-progress';
|
|
4
4
|
import { highlight } from 'cli-highlight';
|
|
5
|
+
import ora from 'ora';
|
|
5
6
|
import { loadConfig } from '../config/config.js';
|
|
6
7
|
function getTheme() {
|
|
7
8
|
const config = loadConfig();
|
|
@@ -27,6 +28,20 @@ export function printSuccess(message) {
|
|
|
27
28
|
const { success } = getTheme();
|
|
28
29
|
console.log(success(' SUCCESS '), success(message));
|
|
29
30
|
}
|
|
31
|
+
export function printWarning(message) {
|
|
32
|
+
const { accent } = getTheme();
|
|
33
|
+
console.warn(chalk.bgYellow.black(' WARNING '), accent(message));
|
|
34
|
+
}
|
|
35
|
+
export function printInfo(message) {
|
|
36
|
+
const { primary } = getTheme();
|
|
37
|
+
console.log(chalk.bgBlue.white(' INFO '), primary(message));
|
|
38
|
+
}
|
|
39
|
+
export function createSpinner(text) {
|
|
40
|
+
return ora({
|
|
41
|
+
text: themed(text, 'primary'),
|
|
42
|
+
color: 'cyan',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
30
45
|
export function createProgressBar(total) {
|
|
31
46
|
const { primary } = getTheme();
|
|
32
47
|
const bar = new cliProgress.SingleBar({
|