@rahul05ranjan/dhruv-cli 1.3.0 → 1.5.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.
Files changed (121) hide show
  1. package/.github/ENTERPRISE.md +275 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +45 -17
  3. package/.github/ISSUE_TEMPLATE/documentation_issue.md +61 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +61 -9
  5. package/.github/ISSUE_TEMPLATE/security_vulnerability.md +74 -0
  6. package/.github/dependabot.yml +42 -2
  7. package/.github/pull_request_template.md +13 -0
  8. package/.github/workflows/ci.yml +51 -38
  9. package/.github/workflows/contribution.yml +41 -34
  10. package/.github/workflows/dependabot-auto-merge.yml +62 -2
  11. package/.github/workflows/labeler.yml +1 -0
  12. package/.github/workflows/release.yml +229 -0
  13. package/.github/workflows/security.yml +201 -0
  14. package/.releaserc.json +50 -0
  15. package/AGENTS.md +13 -0
  16. package/CHANGELOG.md +13 -0
  17. package/README.md +145 -40
  18. package/__tests__/cli-contract.test.ts +104 -0
  19. package/__tests__/core.test.ts +439 -0
  20. package/__tests__/diagnostics.test.ts +155 -0
  21. package/__tests__/file-workflows.test.ts +195 -0
  22. package/__tests__/interactive.test.ts +119 -0
  23. package/__tests__/setup.ts +62 -0
  24. package/__tests__/workflows.test.ts +118 -0
  25. package/dist/commands/explain.js +13 -44
  26. package/dist/commands/fix.js +13 -38
  27. package/dist/commands/generate.d.ts +6 -1
  28. package/dist/commands/generate.js +60 -55
  29. package/dist/commands/health.d.ts +4 -0
  30. package/dist/commands/health.js +419 -0
  31. package/dist/commands/init.js +56 -42
  32. package/dist/commands/menu.js +137 -24
  33. package/dist/commands/metrics.d.ts +5 -0
  34. package/dist/commands/metrics.js +80 -0
  35. package/dist/commands/optimize.js +42 -34
  36. package/dist/commands/review.d.ts +4 -1
  37. package/dist/commands/review.js +87 -43
  38. package/dist/commands/security-check.d.ts +4 -1
  39. package/dist/commands/security-check.js +109 -38
  40. package/dist/commands/status.d.ts +1 -0
  41. package/dist/commands/status.js +83 -0
  42. package/dist/commands/suggest.js +13 -39
  43. package/dist/config/config.d.ts +5 -1
  44. package/dist/config/config.js +53 -8
  45. package/dist/core/ai.d.ts +88 -2
  46. package/dist/core/ai.js +231 -30
  47. package/dist/core/command-catalog.d.ts +10 -0
  48. package/dist/core/command-catalog.js +27 -0
  49. package/dist/core/command-runner.d.ts +17 -0
  50. package/dist/core/command-runner.js +157 -0
  51. package/dist/core/logger.d.ts +40 -0
  52. package/dist/core/logger.js +139 -0
  53. package/dist/core/metrics.d.ts +62 -0
  54. package/dist/core/metrics.js +284 -0
  55. package/dist/core/prompts.d.ts +1 -0
  56. package/dist/core/prompts.js +121 -0
  57. package/dist/core/security.d.ts +34 -0
  58. package/dist/core/security.js +197 -0
  59. package/dist/index.js +84 -22
  60. package/dist/utils/projectType.d.ts +1 -1
  61. package/dist/utils/projectType.js +26 -7
  62. package/dist/utils/ux.d.ts +3 -0
  63. package/dist/utils/ux.js +15 -0
  64. package/docs/agents/domain.md +51 -0
  65. package/docs/agents/issue-tracker.md +45 -0
  66. package/docs/agents/triage-labels.md +15 -0
  67. package/docs/api/.nojekyll +1 -0
  68. package/docs/api/assets/hierarchy.js +1 -0
  69. package/docs/api/assets/highlight.css +71 -0
  70. package/docs/api/assets/icons.js +18 -0
  71. package/docs/api/assets/icons.svg +1 -0
  72. package/docs/api/assets/main.js +60 -0
  73. package/docs/api/assets/navigation.js +1 -0
  74. package/docs/api/assets/search.js +1 -0
  75. package/docs/api/assets/style.css +1633 -0
  76. package/docs/api/hierarchy.html +1 -0
  77. package/docs/api/index.html +161 -0
  78. package/docs/api/media/CONTRIBUTING.md +60 -0
  79. package/docs/api/media/SECURITY.md +8 -0
  80. package/docs/api/media/dhruv-cli-preview.svg +42 -0
  81. package/docs/api/media/publishing-fix.md +34 -0
  82. package/docs/api/modules.html +1 -0
  83. package/docs/dhruv-cli-preview.svg +42 -0
  84. package/docs/index.html +631 -533
  85. package/docs/publishing-fix.md +34 -0
  86. package/eslint.config.js +170 -0
  87. package/jest.config.json +37 -0
  88. package/lighthouserc.json +22 -0
  89. package/package.json +62 -8
  90. package/src/commands/explain.ts +13 -42
  91. package/src/commands/fix.ts +13 -31
  92. package/src/commands/generate.ts +68 -48
  93. package/src/commands/health.ts +485 -0
  94. package/src/commands/init.ts +57 -42
  95. package/src/commands/menu.ts +132 -24
  96. package/src/commands/metrics.ts +100 -0
  97. package/src/commands/optimize.ts +40 -28
  98. package/src/commands/review.ts +96 -37
  99. package/src/commands/security-check.ts +127 -33
  100. package/src/commands/status.ts +83 -0
  101. package/src/commands/suggest.ts +13 -32
  102. package/src/config/config.ts +60 -8
  103. package/src/core/ai.ts +265 -26
  104. package/src/core/command-catalog.ts +37 -0
  105. package/src/core/command-runner.ts +185 -0
  106. package/src/core/logger.ts +195 -0
  107. package/src/core/metrics.ts +335 -0
  108. package/src/core/prompts.ts +128 -0
  109. package/src/core/security.ts +243 -0
  110. package/src/index.ts +90 -22
  111. package/src/utils/projectType.ts +22 -7
  112. package/src/utils/ux.ts +18 -0
  113. package/test-suite.sh +147 -0
  114. package/tsconfig.json +4 -3
  115. package/typedoc.json +44 -0
  116. package/types/global.d.ts +13 -0
  117. package/validate-workflows.sh +270 -0
  118. package/.eslintignore +0 -1
  119. package/.eslintrc.cjs +0 -43
  120. package/.github/workflows/auto-assign.yml +0 -14
  121. package/src/core/ai.test.js +0 -40
@@ -0,0 +1,128 @@
1
+ /**
2
+ * System-message templates per command type. Pure data: no AI plumbing.
3
+ */
4
+ const SYSTEM_MESSAGES: Record<string, string> = {
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
+
23
+ suggest: `You are an expert software architect and senior developer providing actionable recommendations and best practices.
24
+
25
+ Guidelines:
26
+ - Provide specific, actionable suggestions
27
+ - Prioritize recommendations by importance
28
+ - Include practical implementation steps
29
+ - Mention relevant tools, libraries, or patterns
30
+ - Consider performance, maintainability, and scalability
31
+ - Provide concrete examples where helpful
32
+ - Focus on industry best practices
33
+
34
+ Output Format:
35
+ - Use clear numbered or bulleted lists
36
+ - Separate different types of suggestions
37
+ - Provide brief explanations for each recommendation
38
+ - Use plain text formatting for better readability`,
39
+
40
+ fix: `You are a debugging expert and problem-solving specialist helping developers resolve technical issues.
41
+
42
+ Guidelines:
43
+ - Analyze the problem systematically
44
+ - Identify the root cause
45
+ - Provide step-by-step solutions
46
+ - Include preventive measures
47
+ - Show corrected code examples
48
+ - Explain why the fix works
49
+ - Suggest testing approaches
50
+
51
+ Output Format:
52
+ - Start with problem analysis
53
+ - Provide clear solution steps
54
+ - Show before/after code examples
55
+ - Use plain text with clear structure
56
+ - Avoid complex markdown formatting`,
57
+
58
+ review: `You are a senior code reviewer focused on code quality, best practices, and maintainability.
59
+
60
+ Guidelines:
61
+ - Analyze code structure and patterns
62
+ - Identify potential issues or improvements
63
+ - Comment on performance implications
64
+ - Suggest refactoring opportunities
65
+ - Check for security considerations
66
+ - Evaluate readability and maintainability
67
+ - Provide constructive feedback
68
+
69
+ Output Format:
70
+ - Organize feedback by categories (Structure, Performance, Security, etc.)
71
+ - Use clear, actionable language
72
+ - Provide specific line-by-line suggestions where relevant
73
+ - Use plain text formatting for better CLI readability`,
74
+
75
+ optimize: `You are a performance optimization expert specializing in code efficiency and best practices.
76
+
77
+ Guidelines:
78
+ - Focus on measurable performance improvements
79
+ - Consider different types of optimization (runtime, memory, bundle size, etc.)
80
+ - Provide specific, implementable suggestions
81
+ - Explain the impact of each optimization
82
+ - Consider trade-offs between performance and maintainability
83
+ - Suggest profiling and measurement approaches
84
+
85
+ Output Format:
86
+ - Categorize optimizations by type
87
+ - Provide clear before/after examples
88
+ - Include estimated impact where possible
89
+ - Use structured plain text formatting`,
90
+
91
+ security: `You are a cybersecurity expert specializing in application security and secure coding practices.
92
+
93
+ Guidelines:
94
+ - Identify potential security vulnerabilities
95
+ - Provide remediation steps
96
+ - Suggest secure coding patterns
97
+ - Consider common attack vectors
98
+ - Recommend security tools and practices
99
+ - Focus on practical security measures
100
+ - Explain security implications
101
+
102
+ Output Format:
103
+ - Categorize findings by severity
104
+ - Provide clear remediation steps
105
+ - Use plain text formatting for better readability
106
+ - Include references to security standards where relevant`,
107
+
108
+ generate: `You are a code generation specialist creating high-quality, well-structured code.
109
+
110
+ Guidelines:
111
+ - Generate clean, readable code
112
+ - Follow established conventions
113
+ - Include appropriate comments
114
+ - Consider edge cases
115
+ - Use proper error handling
116
+ - Follow best practices for the target language
117
+ - Generate comprehensive test cases when requested
118
+
119
+ Output Format:
120
+ - Provide clean code without excessive markdown
121
+ - Use minimal formatting for better CLI display
122
+ - Include brief explanations only when necessary
123
+ - Focus on practical, working code`,
124
+ };
125
+
126
+ export function getSystemMessage(type: string): string {
127
+ return SYSTEM_MESSAGES[type] ?? SYSTEM_MESSAGES.explain;
128
+ }
@@ -0,0 +1,243 @@
1
+ import Joi from 'joi';
2
+ import validator from 'validator';
3
+ import { logger, logSecurity } from './logger.js';
4
+ import { metricsCollector } from './metrics.js';
5
+
6
+ export interface SecurityConfig {
7
+ enableInputValidation: boolean;
8
+ enableRateLimiting: boolean;
9
+ maxInputLength: number;
10
+ allowedCommands: string[];
11
+ blockedPatterns: RegExp[];
12
+ maxRequestsPerMinute: number;
13
+ }
14
+
15
+ export class SecurityManager {
16
+ private static instance: SecurityManager;
17
+ private config: SecurityConfig;
18
+ private requestCounts: Map<string, { count: number; resetTime: number }> = new Map();
19
+ private schemas: { [key: string]: Joi.ObjectSchema } = {};
20
+
21
+ private constructor() {
22
+ this.config = {
23
+ enableInputValidation: process.env.DHRUV_SECURITY_VALIDATION !== 'false',
24
+ enableRateLimiting: process.env.DHRUV_SECURITY_RATE_LIMIT !== 'false',
25
+ maxInputLength: parseInt(process.env.DHRUV_MAX_INPUT_LENGTH || '10000'),
26
+ allowedCommands: [
27
+ 'explain', 'suggest', 'fix', 'review', 'optimize',
28
+ 'security-check', 'generate', 'init', 'status',
29
+ 'project-type', 'menu', 'completion', 'help'
30
+ ],
31
+ blockedPatterns: [
32
+ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, // XSS patterns
33
+ /\b(eval|exec|system|shell_exec|passthru|proc_open|popen)\b/i, // Code execution
34
+ /\b(rm\s+(-rf|--force)\s+|del\s+)/i, // File deletion
35
+ /\b(format|fdisk|mkfs)\b/i, // Disk operations
36
+ /password\s*=\s*['"][^'"]*['"]/i, // Password exposure
37
+ /\b(ssh|scp|rsync)\s+.*@\w+/i, // SSH operations
38
+ ],
39
+ maxRequestsPerMinute: parseInt(process.env.DHRUV_RATE_LIMIT || '60')
40
+ };
41
+
42
+ this.initializeSchemas();
43
+
44
+ logger.info('Security manager initialized', {
45
+ config: {
46
+ ...this.config,
47
+ blockedPatterns: this.config.blockedPatterns.map(p => p.toString())
48
+ }
49
+ });
50
+ }
51
+
52
+ private initializeSchemas(): void {
53
+ this.schemas = {
54
+ explain: Joi.object({
55
+ query: Joi.string()
56
+ .min(1)
57
+ .max(this.config.maxInputLength)
58
+ .required()
59
+ .messages({
60
+ 'string.empty': 'Query cannot be empty',
61
+ 'string.max': `Query too long (max ${this.config.maxInputLength} characters)`
62
+ })
63
+ }),
64
+
65
+ suggest: Joi.object({
66
+ query: Joi.string()
67
+ .min(1)
68
+ .max(this.config.maxInputLength)
69
+ .required()
70
+ }),
71
+
72
+ fix: Joi.object({
73
+ query: Joi.string()
74
+ .min(1)
75
+ .max(this.config.maxInputLength)
76
+ .required()
77
+ }),
78
+
79
+ review: Joi.object({
80
+ fileOrDir: Joi.string()
81
+ .min(1)
82
+ .max(500)
83
+ .pattern(/^[^<>&|;$`]*$/) // Prevent shell injection
84
+ .required()
85
+ }),
86
+
87
+ optimize: Joi.object({
88
+ file: Joi.string()
89
+ .min(1)
90
+ .max(500)
91
+ .pattern(/^[^<>&|;$`]*$/)
92
+ .required()
93
+ }),
94
+
95
+ generate: Joi.object({
96
+ type: Joi.string()
97
+ .valid('tests', 'documentation', 'docs', 'component')
98
+ .required(),
99
+ target: Joi.string()
100
+ .min(1)
101
+ .max(500)
102
+ .pattern(/^[^<>&|;$`]*$/)
103
+ .required()
104
+ })
105
+ };
106
+ }
107
+
108
+ public static getInstance(): SecurityManager {
109
+ if (!SecurityManager.instance) {
110
+ SecurityManager.instance = new SecurityManager();
111
+ }
112
+ return SecurityManager.instance;
113
+ }
114
+
115
+ public validateInput(command: string, input: any): { valid: boolean; error?: string } {
116
+ if (!this.config.enableInputValidation) {
117
+ return { valid: true };
118
+ }
119
+
120
+ try {
121
+ // Check command is allowed
122
+ if (!this.config.allowedCommands.includes(command)) {
123
+ const error = `Command '${command}' is not allowed`;
124
+ logSecurity('blocked_command', { command, input });
125
+ metricsCollector.recordError('blocked_command', command);
126
+ return { valid: false, error };
127
+ }
128
+
129
+ // Get schema for command
130
+ const schema = this.schemas[command];
131
+ if (!schema) {
132
+ return { valid: true }; // No specific validation for this command
133
+ }
134
+
135
+ // Validate input
136
+ const { error } = schema.validate(input, { abortEarly: false });
137
+ if (error) {
138
+ const errorMessage = error.details.map((d: any) => d.message).join(', ');
139
+ logSecurity('invalid_input', { command, input, error: errorMessage });
140
+ metricsCollector.recordError('invalid_input', command);
141
+ return { valid: false, error: errorMessage };
142
+ }
143
+
144
+ // Check for blocked patterns
145
+ const inputString = JSON.stringify(input);
146
+ for (const pattern of this.config.blockedPatterns) {
147
+ if (pattern.test(inputString)) {
148
+ const error = 'Input contains blocked patterns';
149
+ logSecurity('blocked_pattern', { command, input, pattern: pattern.toString() });
150
+ metricsCollector.recordError('blocked_pattern', command);
151
+ return { valid: false, error };
152
+ }
153
+ }
154
+
155
+ // Sanitize input
156
+ const sanitized = this.sanitizeInput(input);
157
+ if (JSON.stringify(sanitized) !== JSON.stringify(input)) {
158
+ logger.warn('Input was sanitized', { command, original: input, sanitized });
159
+ }
160
+
161
+ return { valid: true };
162
+ } catch (err) {
163
+ const error = `Validation error: ${(err as Error).message}`;
164
+ logSecurity('validation_error', { command, input, error });
165
+ metricsCollector.recordError('validation_error', command);
166
+ return { valid: false, error };
167
+ }
168
+ }
169
+
170
+ public checkRateLimit(identifier: string): { allowed: boolean; remainingRequests: number } {
171
+ if (!this.config.enableRateLimiting) {
172
+ return { allowed: true, remainingRequests: this.config.maxRequestsPerMinute };
173
+ }
174
+
175
+ const now = Date.now();
176
+ const windowStart = Math.floor(now / 60000) * 60000; // 1-minute windows
177
+ const windowKey = `${identifier}:${windowStart}`;
178
+
179
+ const current = this.requestCounts.get(windowKey) || { count: 0, resetTime: windowStart + 60000 };
180
+
181
+ if (now > current.resetTime) {
182
+ // Reset window
183
+ this.requestCounts.set(windowKey, { count: 1, resetTime: windowStart + 60000 });
184
+ return { allowed: true, remainingRequests: this.config.maxRequestsPerMinute - 1 };
185
+ }
186
+
187
+ if (current.count >= this.config.maxRequestsPerMinute) {
188
+ logSecurity('rate_limit_exceeded', { identifier, count: current.count });
189
+ metricsCollector.recordError('rate_limit_exceeded', 'unknown');
190
+ return { allowed: false, remainingRequests: 0 };
191
+ }
192
+
193
+ current.count++;
194
+ const remaining = Math.max(0, this.config.maxRequestsPerMinute - current.count);
195
+ return { allowed: true, remainingRequests: remaining };
196
+ }
197
+
198
+ public sanitizeInput(input: any): any {
199
+ if (typeof input === 'string') {
200
+ return validator.escape(input);
201
+ }
202
+
203
+ if (Array.isArray(input)) {
204
+ return input.map(item => this.sanitizeInput(item));
205
+ }
206
+
207
+ if (typeof input === 'object' && input !== null) {
208
+ const sanitized: any = {};
209
+ for (const [key, value] of Object.entries(input)) {
210
+ sanitized[key] = this.sanitizeInput(value);
211
+ }
212
+ return sanitized;
213
+ }
214
+
215
+ return input;
216
+ }
217
+
218
+ public auditLog(action: string, details: Record<string, any>): void {
219
+ logSecurity(action, {
220
+ timestamp: new Date().toISOString(),
221
+ ...details
222
+ });
223
+ }
224
+
225
+ public getSecurityStatus(): {
226
+ config: SecurityConfig;
227
+ activeRateLimits: number;
228
+ blockedPatternsCount: number;
229
+ } {
230
+ return {
231
+ config: this.config,
232
+ activeRateLimits: this.requestCounts.size,
233
+ blockedPatternsCount: this.config.blockedPatterns.length
234
+ };
235
+ }
236
+
237
+ public resetRateLimits(): void {
238
+ this.requestCounts.clear();
239
+ logger.info('Rate limits reset');
240
+ }
241
+ }
242
+
243
+ export const securityManager = SecurityManager.getInstance();
package/src/index.ts CHANGED
@@ -11,9 +11,18 @@ 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 pkg from '../package.json' assert { type: 'json' };
19
+ import { createRequire } from 'module';
20
+ import { logger, logCommand, logInfo, logError } from './core/logger.js';
21
+ import { metricsCollector } from './core/metrics.js';
22
+ import { securityManager } from './core/security.js';
23
+ import { commandDescription, completionCommands, completionOptions } from './core/command-catalog.js';
24
+ const require = createRequire(import.meta.url);
25
+ const pkg = require('../package.json');
17
26
 
18
27
  const program = new Command();
19
28
 
@@ -24,47 +33,73 @@ program
24
33
 
25
34
  program
26
35
  .command('explain <query>')
27
- .description('Explain a concept or command')
36
+ .description(commandDescription('explain'))
37
+ .addHelpText('after', '\nExamples:\n $ dhruv explain "What is async/await?"\n $ dhruv explain "Docker containers vs VMs"')
28
38
  .action(explain);
29
39
 
30
40
  program
31
41
  .command('suggest <query>')
32
- .description('Get AI-powered suggestions')
42
+ .description(commandDescription('suggest'))
43
+ .addHelpText('after', '\nExamples:\n $ dhruv suggest "React performance optimization"\n $ dhruv suggest "Node.js project structure"')
33
44
  .action(suggest);
34
45
 
35
46
  program
36
47
  .command('fix <query>')
37
- .description('Get a fix for a coding issue or error')
48
+ .description(commandDescription('fix'))
49
+ .addHelpText('after', '\nExamples:\n $ dhruv fix "TypeError: Cannot read property of undefined"\n $ dhruv fix "CORS error in Express.js"')
38
50
  .action(fix);
39
51
 
40
52
  program
41
53
  .command('review <fileOrDir>')
42
- .description('Review code in a file or directory')
43
- .action(review);
54
+ .description(commandDescription('review'))
55
+ .option('--diff', 'Review the current uncommitted git diff')
56
+ .action((fileOrDir: string, command: Command) => review(fileOrDir, command.opts()));
44
57
 
45
58
  program
46
59
  .command('optimize <file>')
47
- .description('Optimize a file (e.g., package.json)')
60
+ .description(commandDescription('optimize'))
48
61
  .action(optimize);
49
62
 
50
63
  program
51
64
  .command('security-check [fileOrDir]')
52
- .description('Run a security check on code')
53
- .action(securityCheck);
65
+ .description(commandDescription('security-check'))
66
+ .option('--strict', 'Exit with failure when high-confidence findings are detected')
67
+ .action((fileOrDir: string | undefined, command: Command) => securityCheck(fileOrDir, command.opts()));
54
68
 
55
69
  program
56
70
  .command('generate <type> <target>')
57
- .description('Generate code/tests for a file')
58
- .action(generate);
71
+ .description(commandDescription('generate'))
72
+ .option('--apply', 'Write generated tests to disk (preview is the default)')
73
+ .option('--output <path>', 'Write generated tests to this path')
74
+ .option('--overwrite', 'Allow replacing an existing output file')
75
+ .action((type: string, target: string, command: Command) => generate(type, target, command.opts()));
59
76
 
60
77
  program
61
78
  .command('init')
62
- .description('Interactive setup/configuration wizard')
79
+ .description(commandDescription('init'))
63
80
  .action(init);
64
81
 
82
+ program
83
+ .command('status')
84
+ .description(commandDescription('status'))
85
+ .action(status);
86
+
87
+ program
88
+ .command('health')
89
+ .description(commandDescription('health'))
90
+ .option('--details', 'Show every health check and diagnostic detail')
91
+ .action((command: Command) => health(command.opts()));
92
+
93
+ program
94
+ .command('metrics')
95
+ .description(commandDescription('metrics'))
96
+ .option('--raw', 'Export raw Prometheus metrics')
97
+ .option('--reset', 'Clear persisted local metrics')
98
+ .action((command: Command) => metrics(command.opts()));
99
+
65
100
  program
66
101
  .command('project-type')
67
- .description('Detect and print the current project type')
102
+ .description(commandDescription('project-type'))
68
103
  .action(() => {
69
104
  const type = detectProjectType();
70
105
  console.log(chalk.blue(`Detected project type: ${type}`));
@@ -72,20 +107,22 @@ program
72
107
 
73
108
  program
74
109
  .command('menu')
75
- .description('Interactive command palette')
110
+ .description(commandDescription('menu'))
76
111
  .action(menu);
77
112
 
78
113
  program
79
114
  .option('--model <model>', 'Set Ollama model')
80
115
  .option('--verbose', 'Enable verbose output')
81
116
  .option('--json', 'Output in JSON format')
117
+ .option('--timeout <milliseconds>', 'Set the AI request timeout')
82
118
  .hook('preAction', async (thisCommand) => {
83
119
  const opts = thisCommand.opts();
84
- if (opts.model || opts.verbose || opts.json) {
120
+ if (opts.model || opts.verbose || opts.json || opts.timeout) {
85
121
  const config: Record<string, unknown> = {};
86
122
  if (opts.model) config.model = opts.model;
87
123
  if (opts.verbose) config.verbose = true;
88
124
  if (opts.json) config.responseFormat = 'json';
125
+ if (opts.timeout) config.timeoutMs = Number(opts.timeout);
89
126
  // Save config for session
90
127
  const configModule = await import('./config/config.js');
91
128
  configModule.saveConfig(config);
@@ -111,30 +148,61 @@ async function loadPlugins(program: unknown) {
111
148
  }
112
149
 
113
150
  (async () => {
114
- await loadPlugins(program);
115
- program.parse(process.argv);
151
+ // Initialize enterprise features
152
+ try {
153
+ logInfo('Dhruv CLI starting', {
154
+ version: pkg.version,
155
+ nodeVersion: process.version,
156
+ platform: process.platform
157
+ });
158
+
159
+ // Record session start
160
+ metricsCollector.recordSession();
161
+
162
+ await loadPlugins(program);
163
+ program.parse(process.argv);
164
+
165
+ // Record successful session
166
+ const sessionId = logger.getSessionId();
167
+ logInfo('CLI session completed', { sessionId });
168
+
169
+ } catch (error) {
170
+ logError('CLI startup failed', error as Error);
171
+ console.error(chalk.red('Failed to start Dhruv CLI:'), (error as Error).message);
172
+ process.exit(1);
173
+ }
116
174
  })();
117
175
 
118
176
  // Autocomplete: Generate shell completion scripts
119
177
  program
120
178
  .command('completion')
121
- .description('Generate shell completion script')
179
+ .description(commandDescription('completion'))
122
180
  .argument('[shell]', 'shell type (bash|zsh|fish)', 'bash')
123
181
  .action((shell: string) => {
182
+ const commands = completionCommands();
183
+ const options = completionOptions();
124
184
  let script = '';
125
185
  switch (shell) {
126
186
  case 'zsh':
127
- script = `#compdef dhruv\n_dhruv_completion() {\n reply=( $(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}') )\n}\ncompctl -K _dhruv_completion dhruv`;
187
+ script = `#compdef dhruv\n_dhruv_completion() {\n _arguments '1:command:(${commands})' '*:option:(${options})'\n}\ncompdef _dhruv_completion dhruv`;
128
188
  break;
129
189
  case 'fish':
130
- script = `function __fish_dhruv_complete\n dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}'\nend\ncomplete -c dhruv -a '(__fish_dhruv_complete)'`;
190
+ script = `complete -c dhruv -f -n '__fish_use_subcommand' -a '${commands}'\ncomplete -c dhruv -f -n 'not __fish_use_subcommand' -a '${options}'`;
131
191
  break;
132
- default:
192
+ case 'bash':
133
193
  script = String.raw`#!/bin/bash
134
194
  _dhruv_completion() {
135
- COMPREPLY=( $(compgen -W "$(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}')" -- \${COMP_WORDS[1]}) )
195
+ local commands="${commands}"
196
+ local options="${options}"
197
+ local choices="$commands $options"
198
+ COMPREPLY=( $(compgen -W "$choices" -- "\${COMP_WORDS[COMP_CWORD]}") )
136
199
  }
137
200
  complete -F _dhruv_completion dhruv`;
201
+ break;
202
+ default:
203
+ console.error(chalk.red(`Unsupported shell "${shell}". Choose bash, zsh, or fish.`));
204
+ process.exitCode = 2;
205
+ return;
138
206
  }
139
207
  console.log(script);
140
208
  console.log(`\n# To enable tab completion, add the above to your shell profile or source it directly.`);
@@ -1,14 +1,29 @@
1
1
  // Use .js extension for ESM compatibility
2
2
  import fs from 'fs';
3
+ import path from 'path';
3
4
 
4
- export function detectProjectType(): string {
5
- if (fs.existsSync('package.json')) {
6
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
7
- if (pkg.dependencies?.react || pkg.devDependencies?.react) return 'react';
8
- if (pkg.dependencies?.express || pkg.devDependencies?.express) return 'node-express';
5
+ export function detectProjectType(directory: string = process.cwd()): string {
6
+ const file = (name: string) => path.join(directory, name);
7
+
8
+ if (fs.existsSync(file('package.json'))) {
9
+ let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
10
+ try {
11
+ pkg = JSON.parse(fs.readFileSync(file('package.json'), 'utf-8'));
12
+ } catch {
13
+ return 'unknown';
14
+ }
15
+
16
+ const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
17
+ if (dependencies.react) return 'react';
18
+ if (dependencies.next) return 'nextjs';
19
+ if (dependencies.express) return 'node-express';
20
+ if (dependencies.typescript || fs.existsSync(file('tsconfig.json'))) return 'node-typescript';
9
21
  return 'node';
10
22
  }
11
- if (fs.existsSync('requirements.txt')) return 'python';
12
- if (fs.existsSync('pyproject.toml')) return 'python';
23
+ if (fs.existsSync(file('requirements.txt'))) return 'python';
24
+ if (fs.existsSync(file('pyproject.toml'))) return 'python';
25
+ if (fs.existsSync(file('go.mod'))) return 'go';
26
+ if (fs.existsSync(file('Cargo.toml'))) return 'rust';
27
+ if (fs.existsSync(file('pom.xml')) || fs.existsSync(file('build.gradle'))) return 'java';
13
28
  return 'unknown';
14
29
  }
package/src/utils/ux.ts 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
 
7
8
  function getTheme() {
@@ -32,6 +33,23 @@ export function printSuccess(message: string) {
32
33
  console.log(success(' SUCCESS '), success(message));
33
34
  }
34
35
 
36
+ export function printWarning(message: string) {
37
+ const { accent } = getTheme();
38
+ console.warn(chalk.bgYellow.black(' WARNING '), accent(message));
39
+ }
40
+
41
+ export function printInfo(message: string) {
42
+ const { primary } = getTheme();
43
+ console.log(chalk.bgBlue.white(' INFO '), primary(message));
44
+ }
45
+
46
+ export function createSpinner(text: string) {
47
+ return ora({
48
+ text: themed(text, 'primary'),
49
+ color: 'cyan',
50
+ });
51
+ }
52
+
35
53
  export function createProgressBar(total: number) {
36
54
  const { primary } = getTheme();
37
55
  const bar = new cliProgress.SingleBar({