@rahul05ranjan/dhruv-cli 1.4.6 → 1.6.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 (76) hide show
  1. package/.github/workflows/ci.yml +18 -238
  2. package/.github/workflows/contribution.yml +6 -141
  3. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  4. package/.github/workflows/labeler.yml +1 -0
  5. package/.github/workflows/security.yml +3 -0
  6. package/CHANGELOG.md +16 -4
  7. package/README.md +145 -40
  8. package/__tests__/cli-contract.test.ts +170 -0
  9. package/__tests__/core.test.ts +193 -1
  10. package/__tests__/diagnostics.test.ts +179 -0
  11. package/__tests__/file-workflows.test.ts +234 -0
  12. package/__tests__/interactive.test.ts +134 -0
  13. package/__tests__/setup.ts +1 -0
  14. package/__tests__/workflows.test.ts +27 -4
  15. package/dist/commands/generate.d.ts +6 -1
  16. package/dist/commands/generate.js +44 -11
  17. package/dist/commands/health.d.ts +4 -1
  18. package/dist/commands/health.js +59 -16
  19. package/dist/commands/init.js +18 -7
  20. package/dist/commands/menu.js +125 -100
  21. package/dist/commands/metrics.d.ts +5 -1
  22. package/dist/commands/metrics.js +49 -10
  23. package/dist/commands/optimize.js +1 -1
  24. package/dist/commands/review.d.ts +4 -1
  25. package/dist/commands/review.js +66 -9
  26. package/dist/commands/security-check.d.ts +4 -1
  27. package/dist/commands/security-check.js +100 -6
  28. package/dist/commands/status.js +44 -4
  29. package/dist/config/config.d.ts +5 -1
  30. package/dist/config/config.js +24 -7
  31. package/dist/core/ai.d.ts +11 -0
  32. package/dist/core/ai.js +33 -9
  33. package/dist/core/command-catalog.d.ts +10 -0
  34. package/dist/core/command-catalog.js +27 -0
  35. package/dist/core/command-runner.js +106 -21
  36. package/dist/core/logger.js +1 -0
  37. package/dist/core/metrics.d.ts +28 -0
  38. package/dist/core/metrics.js +79 -0
  39. package/dist/index.js +98 -24
  40. package/dist/utils/projectType.d.ts +7 -1
  41. package/dist/utils/projectType.js +91 -13
  42. package/docs/api/assets/highlight.css +4 -4
  43. package/docs/api/index.html +161 -39
  44. package/docs/api/media/CONTRIBUTING.md +60 -0
  45. package/docs/api/media/SECURITY.md +8 -0
  46. package/docs/api/media/dhruv-cli-preview.svg +42 -0
  47. package/docs/api/media/publishing-fix.md +34 -0
  48. package/docs/dhruv-cli-preview.svg +42 -0
  49. package/docs/index.html +631 -533
  50. package/docs/publishing-fix.md +34 -0
  51. package/package.json +1 -1
  52. package/src/commands/generate.ts +50 -11
  53. package/src/commands/health.ts +62 -17
  54. package/src/commands/init.ts +18 -7
  55. package/src/commands/menu.ts +54 -30
  56. package/src/commands/metrics.ts +53 -9
  57. package/src/commands/optimize.ts +1 -1
  58. package/src/commands/review.ts +72 -9
  59. package/src/commands/security-check.ts +111 -6
  60. package/src/commands/status.ts +43 -5
  61. package/src/config/config.ts +26 -7
  62. package/src/core/ai.ts +36 -8
  63. package/src/core/command-catalog.ts +37 -0
  64. package/src/core/command-runner.ts +108 -22
  65. package/src/core/logger.ts +1 -0
  66. package/src/core/metrics.ts +105 -0
  67. package/src/index.ts +97 -24
  68. package/src/utils/projectType.ts +85 -9
  69. package/tsconfig.json +1 -1
  70. package/.github/workflows/auto-assign.yml +0 -14
  71. package/.github/workflows/build-publish.yml +0 -154
  72. package/.github/workflows/deploy.yml +0 -336
  73. package/.github/workflows/monitoring.yml +0 -270
  74. package/PUBLISHING_FIX.md +0 -92
  75. package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +0 -15
  76. package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +0 -15
@@ -0,0 +1,34 @@
1
+ # npm publishing issue resolution
2
+
3
+ ## Issue
4
+
5
+ GitHub Actions was failing with:
6
+
7
+ ```text
8
+ npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/@rahul05ranjan%2fdhruv-cli - You cannot publish over the previously published versions: 0.0.0-development.
9
+ ```
10
+
11
+ ## Resolution
12
+
13
+ - Updated the package version from `0.0.0-development` to `1.4.0`.
14
+ - Added semantic-release configuration and plugins.
15
+ - Removed direct npm publishing from the CI workflow.
16
+ - Added a dedicated publishing workflow with version-conflict checks.
17
+ - Enabled trusted publishing with npm provenance.
18
+
19
+ ## Release conventions
20
+
21
+ Use conventional commit messages so semantic-release can determine the next version:
22
+
23
+ ```bash
24
+ # Patch
25
+ git commit -m "fix: resolve a connection issue"
26
+
27
+ # Minor
28
+ git commit -m "feat: add a new command"
29
+
30
+ # Major
31
+ git commit -m "feat!: change the command output contract"
32
+ ```
33
+
34
+ The authoritative release configuration lives in [`.releaserc.json`](../.releaserc.json) and the workflows live in [`.github/workflows/`](../.github/workflows/).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahul05ranjan/dhruv-cli",
3
- "version": "1.4.6",
3
+ "version": "1.6.0",
4
4
  "description": "AI-powered CLI assistant for developers using Ollama",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,29 +1,57 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
2
3
  import { runCommand } from '../core/command-runner.js';
3
4
  import { getSystemMessage } from '../core/prompts.js';
4
- import { printError, printSuccess } from '../utils/ux.js';
5
+ import { printError, printSuccess, printInfo } from '../utils/ux.js';
6
+ import { loadConfig } from '../config/config.js';
5
7
 
6
- function buildPrompt(type: string, content: string): string {
8
+ function getLanguageForFile(target: string): { name: string; testFramework: string } {
9
+ const ext = path.extname(target).toLowerCase();
10
+ switch (ext) {
11
+ case '.py':
12
+ return { name: 'Python', testFramework: 'pytest or unittest' };
13
+ case '.go':
14
+ return { name: 'Go', testFramework: 'standard testing package' };
15
+ case '.rs':
16
+ return { name: 'Rust', testFramework: 'standard Rust test framework' };
17
+ case '.ts':
18
+ case '.tsx':
19
+ return { name: 'TypeScript', testFramework: 'Jest or Vitest' };
20
+ case '.java':
21
+ return { name: 'Java', testFramework: 'JUnit 5' };
22
+ default:
23
+ return { name: 'JavaScript', testFramework: 'Jest or Mocha' };
24
+ }
25
+ }
26
+
27
+ function buildPrompt(type: string, content: string, target: string): string {
28
+ const lang = getLanguageForFile(target);
7
29
  if (type === 'tests' || type === 'test') {
8
- return `Generate comprehensive unit tests for the following JavaScript code. Use Jest or Mocha syntax. Only return the test code without explanations:\n\n${content}`;
30
+ return `Generate comprehensive unit tests for the following ${lang.name} code. Use ${lang.testFramework} syntax. Only return the test code without explanations:\n\n${content}`;
9
31
  }
10
32
  if (type === 'documentation' || type === 'docs') {
11
- return `Generate JSDoc documentation for the following code:\n\n${content}`;
33
+ return `Generate ${lang.name === 'Python' ? 'docstrings' : 'JSDoc/documentation'} for the following code:\n\n${content}`;
12
34
  }
13
35
  return `Generate ${type} for this code:\n\n${content}`;
14
36
  }
15
37
 
16
38
  /** Extracts test code from the response: a fenced block if present, else the raw response. */
17
39
  function extractTestCode(response: string): string {
18
- const fenced = response.match(/```(?:javascript|js)?\s*\n([\s\S]*?)```/);
40
+ const fenced = response.match(/```(?:javascript|js|typescript|ts|python|py|go|rust|rs|java)?\s*\n([\s\S]*?)```/i);
19
41
  if (fenced?.[1]) return fenced[1].trim();
20
42
  return response
21
- .replace(/^.*?(?=const|describe|test|it\s*\()/s, '')
22
- .replace(/```[a-z]*\n?/g, '')
43
+ .replace(/^.*?(?=const|describe|test|it\s*\(|def test_|func Test|#\[test\])/s, '')
44
+ .replace(/```[a-z]*\n?/gi, '')
23
45
  .trim();
24
46
  }
25
47
 
26
- export async function generate(type: string, target: string) {
48
+ export interface GenerateOptions {
49
+ apply?: boolean;
50
+ output?: string;
51
+ overwrite?: boolean;
52
+ }
53
+
54
+ export async function generate(type: string, target: string, options: GenerateOptions = {}) {
27
55
  if (!fs.existsSync(target)) {
28
56
  printError(`Target file "${target}" does not exist.`);
29
57
  return;
@@ -36,7 +64,7 @@ export async function generate(type: string, target: string) {
36
64
  input: { type, target },
37
65
  header: `🔨 Generating ${type}: `,
38
66
  buildRequest: (input, model) => ({
39
- prompt: buildPrompt(input.type, content),
67
+ prompt: buildPrompt(input.type, content, input.target),
40
68
  systemMessage: getSystemMessage('generate'),
41
69
  model,
42
70
  }),
@@ -47,9 +75,20 @@ export async function generate(type: string, target: string) {
47
75
  printError('No valid test code generated.');
48
76
  return;
49
77
  }
50
- const testFile = target.replace(/\.[^.]+$/, '.test.js');
78
+ const extension = path.extname(target) || '.js';
79
+ const testFile = options.output ?? target.replace(/\.[^.]+$/, `.test${extension}`);
80
+ if (!options.apply && !options.output) {
81
+ if (loadConfig().responseFormat !== 'json') {
82
+ printInfo(`Preview only. Use --apply to write ${testFile}, or --output <path> to choose a destination.`);
83
+ }
84
+ return;
85
+ }
86
+ if (fs.existsSync(testFile) && !options.overwrite) {
87
+ printError(`Test file "${testFile}" already exists. Use --overwrite to replace it.`);
88
+ return;
89
+ }
51
90
  fs.writeFileSync(testFile, codeToSave);
52
- printSuccess(`Test file saved: ${testFile}`);
91
+ if (loadConfig().responseFormat !== 'json') printSuccess(`Test file saved: ${testFile}`);
53
92
  },
54
93
  footer: `🔍 Want a review? Try: dhruv review ${target}`,
55
94
  });
@@ -28,20 +28,26 @@ interface SystemInfo {
28
28
  cpuCount: number;
29
29
  }
30
30
 
31
- export async function health(): Promise<void> {
32
- console.log(chalk.blue.bold('🔍 Dhruv CLI Health Check\n'));
31
+ export interface HealthOptions {
32
+ details?: boolean;
33
+ }
33
34
 
35
+ export async function health(options: HealthOptions = {}): Promise<void> {
36
+ const jsonOutput = loadConfig().responseFormat === 'json';
34
37
  const results: HealthCheckResult[] = [];
35
38
  const startTime = Date.now();
36
39
 
37
40
  try {
38
41
  // System Information
39
42
  const systemInfo = getSystemInfo();
40
- console.log(chalk.cyan('📊 System Information:'));
41
- Object.entries(systemInfo).forEach(([key, value]) => {
42
- console.log(` ${key}: ${chalk.yellow(value)}`);
43
- });
44
- console.log();
43
+ if (!jsonOutput) {
44
+ console.log(chalk.blue.bold('🔍 Dhruv CLI Health Check\n'));
45
+ console.log(chalk.cyan('📊 System Information:'));
46
+ Object.entries(systemInfo).forEach(([key, value]) => {
47
+ console.log(` ${key}: ${chalk.yellow(value)}`);
48
+ });
49
+ console.log();
50
+ }
45
51
 
46
52
  // Configuration Check
47
53
  results.push(...await checkConfiguration());
@@ -64,15 +70,37 @@ export async function health(): Promise<void> {
64
70
  // Plugin System Check
65
71
  results.push(...await checkPlugins());
66
72
 
67
- // Display Results
68
- displayResults(results);
69
-
70
73
  const duration = Date.now() - startTime;
74
+ const summary = summarizeResults(results);
75
+ if (jsonOutput) {
76
+ process.exitCode = summary.fail > 0 ? 1 : 0;
77
+ process.stdout.write(`${JSON.stringify({
78
+ ok: summary.fail === 0,
79
+ command: 'health',
80
+ system: systemInfo,
81
+ results,
82
+ summary,
83
+ durationMs: duration,
84
+ })}\n`);
85
+ } else {
86
+ if (options.details) displayResults(results);
87
+ else displayConciseResults(results);
88
+ }
89
+
71
90
  logger.info('Health check completed', { duration, results: results.length });
72
91
 
73
92
  } catch (error) {
74
- printError('Health check failed');
75
- console.error(chalk.red((error as Error).message));
93
+ process.exitCode = 1;
94
+ if (jsonOutput) {
95
+ process.stdout.write(`${JSON.stringify({
96
+ ok: false,
97
+ command: 'health',
98
+ error: (error as Error).message,
99
+ })}\n`);
100
+ } else {
101
+ printError('Health check failed');
102
+ console.error(chalk.red((error as Error).message));
103
+ }
76
104
  logger.error('Health check failed', error as Error);
77
105
  }
78
106
  }
@@ -421,11 +449,7 @@ function displayResults(results: HealthCheckResult[]): void {
421
449
  });
422
450
 
423
451
  // Summary
424
- const summary = {
425
- pass: results.filter(r => r.status === 'pass').length,
426
- warn: results.filter(r => r.status === 'warn').length,
427
- fail: results.filter(r => r.status === 'fail').length
428
- };
452
+ const summary = summarizeResults(results);
429
453
 
430
454
  console.log(chalk.blue.bold('\n📊 Summary:'));
431
455
  console.log(` ✅ Passed: ${chalk.green(summary.pass)}`);
@@ -438,3 +462,24 @@ function displayResults(results: HealthCheckResult[]): void {
438
462
 
439
463
  console.log(`\n${statusIcon} ${statusColor('Overall Status: ' + overallStatus.toUpperCase())}`);
440
464
  }
465
+
466
+ function displayConciseResults(results: HealthCheckResult[]): void {
467
+ const summary = summarizeResults(results);
468
+ const overallStatus = summary.fail > 0 ? 'fail' : summary.warn > 0 ? 'warn' : 'pass';
469
+ const icon = overallStatus === 'pass' ? '✅' : overallStatus === 'warn' ? '⚠️' : '❌';
470
+
471
+ console.log(chalk.blue.bold(`\n${icon} Health: ${overallStatus.toUpperCase()}`));
472
+ console.log(` ${chalk.green(`${summary.pass} passed`)}, ${chalk.yellow(`${summary.warn} warnings`)}, ${chalk.red(`${summary.fail} failed`)}`);
473
+ results
474
+ .filter((result) => result.status !== 'pass')
475
+ .forEach((result) => console.log(` ${result.category}: ${result.message}${result.recommendation ? ` — ${result.recommendation}` : ''}`));
476
+ console.log(chalk.dim('Run `dhruv health --details` for full diagnostics.'));
477
+ }
478
+
479
+ function summarizeResults(results: HealthCheckResult[]) {
480
+ return {
481
+ pass: results.filter(r => r.status === 'pass').length,
482
+ warn: results.filter(r => r.status === 'warn').length,
483
+ fail: results.filter(r => r.status === 'fail').length,
484
+ };
485
+ }
@@ -13,8 +13,9 @@ export async function init() {
13
13
  modelChoices = models;
14
14
  }
15
15
  } catch {
16
- console.log(chalk.yellow('Warning: Could not fetch available models from Ollama.'));
17
- console.log(chalk.yellow('Using default model choices.'));
16
+ console.log(chalk.yellow('Warning: Could not connect to Ollama.'));
17
+ console.log(chalk.yellow('💡 Start Ollama with: ollama serve'));
18
+ console.log(chalk.yellow(`💡 Install default model with: ollama pull ${current.model}\n`));
18
19
  }
19
20
 
20
21
  try {
@@ -22,10 +23,17 @@ export async function init() {
22
23
  {
23
24
  type: 'list',
24
25
  name: 'model',
25
- message: 'Which Ollama model do you want to use?',
26
+ message: `Which Ollama model do you want to use? (default: ${current.model})`,
26
27
  choices: modelChoices,
27
28
  default: current.model,
28
29
  },
30
+ {
31
+ type: 'list',
32
+ name: 'scope',
33
+ message: 'Where should Dhruv save these settings?',
34
+ choices: ['local', 'global'],
35
+ default: 'local',
36
+ },
29
37
  {
30
38
  type: 'list',
31
39
  name: 'responseFormat',
@@ -48,13 +56,16 @@ export async function init() {
48
56
  },
49
57
  ]);
50
58
 
51
- saveConfig(answers);
52
- console.log(chalk.green('Configuration saved!'));
59
+ const { scope, ...settings } = answers;
60
+ saveConfig(settings, { scope });
61
+ console.log(chalk.green(`Configuration saved ${scope === 'global' ? 'for your user account' : 'in this project'}!`));
53
62
  } catch (error) {
54
- if ((error as { isTtyError?: boolean })?.isTtyError) {
63
+ const isTty = Boolean((error as { isTtyError?: boolean })?.isTtyError);
64
+ process.exitCode = isTty ? 1 : 130;
65
+ if (isTty) {
55
66
  console.log(chalk.red('This command requires an interactive terminal.'));
56
67
  } else {
57
- console.log(chalk.red('Configuration cancelled or failed.'));
68
+ console.log(chalk.red('Configuration cancelled.'));
58
69
  }
59
70
  }
60
71
  }
@@ -8,41 +8,47 @@ import { optimize } from './optimize.js';
8
8
  import { securityCheck } from './security-check.js';
9
9
  import { generate } from './generate.js';
10
10
  import { init } from './init.js';
11
+ import { status } from './status.js';
12
+ import { health } from './health.js';
13
+ import { metrics } from './metrics.js';
11
14
  import { detectProjectType } from '../utils/projectType.js';
12
15
  import chalk from 'chalk';
16
+ import { commandCatalog } from '../core/command-catalog.js';
13
17
 
14
18
  const commands = [
15
- { name: 'Explain', value: 'explain' },
16
- { name: 'Suggest', value: 'suggest' },
17
- { name: 'Fix', value: 'fix' },
18
- { name: 'Review', value: 'review' },
19
- { name: 'Optimize', value: 'optimize' },
20
- { name: 'Security Check', value: 'security-check' },
21
- { name: 'Generate', value: 'generate' },
22
- { name: 'Init (Setup)', value: 'init' },
23
- { name: 'Project Type', value: 'project-type' },
24
- { name: 'Exit', value: 'exit' }
19
+ ...commandCatalog.map(({ menuLabel, name }) => ({ name: menuLabel, value: name })),
20
+ { name: 'Exit', value: 'exit' },
25
21
  ];
26
22
 
27
23
  export async function menu() {
28
- let running = true;
29
- while (running) {
30
- const { cmd } = await inquirer.prompt([
31
- {
32
- type: 'list',
33
- name: 'cmd',
34
- message: themed('What do you want to do?', 'primary'),
35
- choices: commands
36
- }
37
- ]);
24
+ try {
25
+ while (true) {
26
+ const { filter = '' } = await inquirer.prompt([
27
+ {
28
+ type: 'input',
29
+ name: 'filter',
30
+ message: 'Filter commands (press enter to show all):',
31
+ },
32
+ ]);
33
+ const normalizedFilter = String(filter).trim().toLowerCase();
34
+ const filteredCommands = normalizedFilter
35
+ ? commands.filter((command) => command.name.toLowerCase().includes(normalizedFilter) || command.value.includes(normalizedFilter))
36
+ : commands;
37
+ const { cmd } = await inquirer.prompt([
38
+ {
39
+ type: 'list',
40
+ name: 'cmd',
41
+ message: themed('What do you want to do?', 'primary'),
42
+ choices: filteredCommands.length > 0 ? filteredCommands : [{ name: 'No matching commands — Exit', value: 'exit' }],
43
+ }
44
+ ]);
38
45
 
39
- if (cmd === 'exit') {
40
- running = false;
41
- break;
42
- }
46
+ if (cmd === 'exit') {
47
+ break;
48
+ }
43
49
 
44
- try {
45
- switch (cmd) {
50
+ try {
51
+ switch (cmd) {
46
52
  case 'explain': {
47
53
  const { query } = await inquirer.prompt([
48
54
  { type: 'input', name: 'query', message: 'What would you like me to explain?' }
@@ -107,13 +113,31 @@ export async function menu() {
107
113
  console.log(chalk.blue(`Detected project type: ${type}`));
108
114
  break;
109
115
  }
116
+ case 'status':
117
+ await status();
118
+ break;
119
+ case 'health':
120
+ await health();
121
+ break;
122
+ case 'metrics':
123
+ await metrics();
124
+ break;
125
+ case 'completion':
126
+ console.log(themed('Run `dhruv completion <bash|zsh|fish>` to install shell completion.', 'accent'));
127
+ break;
110
128
  default:
111
129
  console.log(themed(`You selected: ${cmd}`, 'accent'));
130
+ }
131
+ } catch (error) {
132
+ console.error(chalk.red(`Error executing ${cmd}: ${(error as Error).message}`));
112
133
  }
113
- } catch (error) {
114
- console.error(chalk.red(`Error executing ${cmd}: ${(error as Error).message}`));
115
- }
116
134
 
117
- console.log(''); // Add spacing between commands
135
+ console.log(''); // Add spacing between commands
136
+ }
137
+ } catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ const cancelled = /cancel|force closed|exitprompt/i.test(message);
140
+ process.exitCode = cancelled ? 130 : 1;
141
+ console.error(chalk.red(cancelled ? 'Interactive menu cancelled.' : `Interactive menu failed: ${message}`));
118
142
  }
119
143
  }
@@ -3,13 +3,49 @@ import chalk from 'chalk';
3
3
  import { printSuccess, printError, printInfo } from '../utils/ux.js';
4
4
  import { metricsCollector } from '../core/metrics.js';
5
5
  import { logger } from '../core/logger.js';
6
+ import { loadConfig } from '../config/config.js';
6
7
 
7
- export async function metrics(): Promise<void> {
8
- console.log(chalk.blue.bold('📊 Dhruv CLI Metrics\n'));
8
+ export interface MetricsOptions {
9
+ raw?: boolean;
10
+ reset?: boolean;
11
+ }
9
12
 
13
+ export async function metrics(options: MetricsOptions = {}): Promise<void> {
10
14
  try {
15
+ if (options.reset) {
16
+ metricsCollector.resetPersistent();
17
+ if (loadConfig().responseFormat === 'json') {
18
+ process.stdout.write(`${JSON.stringify({ ok: true, command: 'metrics', reset: true, summary: metricsCollector.getSummary() })}\n`);
19
+ } else {
20
+ printSuccess('Local metrics reset.');
21
+ }
22
+ return;
23
+ }
24
+
11
25
  // Get metrics data
12
26
  const metricsData = await metricsCollector.getMetricsJSON();
27
+ const summary = metricsCollector.getSummary();
28
+
29
+ if (loadConfig().responseFormat === 'json') {
30
+ process.stdout.write(`${JSON.stringify({
31
+ ok: true,
32
+ command: 'metrics',
33
+ summary,
34
+ metrics: metricsData,
35
+ })}\n`);
36
+ return;
37
+ }
38
+
39
+ console.log(chalk.blue.bold('📊 Dhruv CLI Metrics\n'));
40
+ console.log(chalk.cyan('📌 Local summary:'));
41
+ console.log(` Sessions: ${chalk.green(summary.sessions)}`);
42
+ Object.entries(summary.commands).forEach(([command, data]) => {
43
+ console.log(` ${chalk.yellow(command)}: ${data.runs} runs, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
44
+ });
45
+ Object.entries(summary.models).forEach(([model, data]) => {
46
+ console.log(` ${chalk.yellow(model)}: ${data.requests} requests, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
47
+ });
48
+ console.log(` Cache: ${chalk.green(summary.cache.hits)} hits, ${chalk.yellow(summary.cache.misses)} misses`);
13
49
 
14
50
  if (metricsData.length === 0) {
15
51
  printInfo('No metrics data available yet. Metrics are collected during CLI usage.');
@@ -49,17 +85,25 @@ export async function metrics(): Promise<void> {
49
85
  }
50
86
  });
51
87
 
52
- // Display raw Prometheus metrics
53
- console.log(chalk.cyan('\n📋 Raw Prometheus Metrics:'));
54
- console.log(chalk.gray('─'.repeat(50)));
55
- const rawMetrics = await metricsCollector.getMetrics();
56
- console.log(rawMetrics);
88
+ if (options.raw) {
89
+ const rawMetrics = await metricsCollector.getMetrics();
90
+ process.stdout.write(rawMetrics);
91
+ }
57
92
 
58
93
  logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
59
94
 
60
95
  } catch (error) {
61
- printError('Failed to retrieve metrics');
62
- console.error(chalk.red((error as Error).message));
96
+ process.exitCode = 1;
97
+ if (loadConfig().responseFormat === 'json') {
98
+ process.stdout.write(`${JSON.stringify({
99
+ ok: false,
100
+ command: 'metrics',
101
+ error: (error as Error).message,
102
+ })}\n`);
103
+ } else {
104
+ printError('Failed to retrieve metrics');
105
+ console.error(chalk.red((error as Error).message));
106
+ }
63
107
  logger.error('Metrics command failed', error as Error);
64
108
  }
65
109
  }
@@ -39,7 +39,7 @@ export async function optimize(file: string) {
39
39
  input: { file },
40
40
  header: `⚡ Optimization suggestions for ${type}: `,
41
41
  buildRequest: (input, model) => ({
42
- prompt: `Please analyze and provide optimization suggestions for this ${type}:\n\n${content}\n\nPlease provide:\n1. Specific optimization recommendations\n2. Performance improvements\n3. Best practices to implement\n4. Code examples of improvements\n5. Potential issues to fix\n\nFocus on actionable, practical improvements.`,
42
+ prompt: `Please analyze and provide optimization suggestions for this ${type}. For every recommendation, explain the expected impact, how to measure it, and the trade-offs or risks before applying it.\n\n${content}\n\nPlease provide:\n1. Specific optimization recommendations\n2. Performance improvements\n3. Best practices to implement\n4. Code examples of improvements\n5. Potential issues to fix\n\nFocus on actionable, practical improvements.`,
43
43
  systemMessage: getSystemMessage('optimize'),
44
44
  model,
45
45
  }),
@@ -1,10 +1,29 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { execFileSync } from 'child_process';
3
4
  import { runCommand } from '../core/command-runner.js';
4
5
  import { getSystemMessage } from '../core/prompts.js';
5
- import { printError } from '../utils/ux.js';
6
+ import { printError, printInfo } from '../utils/ux.js';
7
+ import { detectProjectType } from '../utils/projectType.js';
6
8
 
7
- /** Reads a file or the code files of a directory (up to 10), concatenated. */
9
+ const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
10
+ const IGNORED_DIRECTORIES = new Set([
11
+ '.git',
12
+ 'node_modules',
13
+ 'dist',
14
+ 'build',
15
+ 'coverage',
16
+ '.dhruv-cache',
17
+ 'logs',
18
+ '.next',
19
+ '.turbo',
20
+ '__pycache__',
21
+ '.pytest_cache',
22
+ 'target',
23
+ 'vendor',
24
+ ]);
25
+
26
+ /** Reads a file or up to 10 code files from a directory tree. */
8
27
  function readCode(fileOrDir: string): string | undefined {
9
28
  // Read first, branch on the error: no separate existence check to race against.
10
29
  let content: string;
@@ -22,16 +41,34 @@ function readCode(fileOrDir: string): string | undefined {
22
41
  }
23
42
 
24
43
  function readDirectory(dir: string): string | undefined {
25
- const files = fs
26
- .readdirSync(dir)
27
- .filter((f) => f.match(/\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/))
28
- .slice(0, 10);
44
+ const files: string[] = [];
45
+
46
+ function collect(current: string): void {
47
+ if (files.length >= 10) return;
48
+
49
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
50
+ if (files.length >= 10) return;
51
+ const absolute = path.join(current, entry.name);
52
+
53
+ if (entry.isDirectory()) {
54
+ if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
55
+ } else if (entry.isFile() && CODE_FILE.test(entry.name)) {
56
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
57
+ }
58
+ }
59
+ }
60
+
61
+ collect(dir);
29
62
 
30
63
  if (files.length === 0) {
31
64
  printError(`No code files found in directory "${dir}".`);
32
65
  return undefined;
33
66
  }
34
67
 
68
+ if (files.length >= 10) {
69
+ printInfo('Note: Directory review is capped at the first 10 source files.');
70
+ }
71
+
35
72
  let code = '';
36
73
  for (const f of files) {
37
74
  try {
@@ -43,16 +80,42 @@ function readDirectory(dir: string): string | undefined {
43
80
  return code;
44
81
  }
45
82
 
46
- export async function review(fileOrDir: string) {
47
- const code = readCode(fileOrDir);
83
+ function readGitDiff(fileOrDir: string): string | undefined {
84
+ const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
85
+ try {
86
+ const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
87
+ cwd: root,
88
+ encoding: 'utf8',
89
+ stdio: ['ignore', 'pipe', 'ignore'],
90
+ });
91
+ if (!diff.trim()) {
92
+ printError(`No uncommitted changes found in "${fileOrDir}".`);
93
+ return undefined;
94
+ }
95
+ return diff;
96
+ } catch {
97
+ printError(`Could not read a git diff for "${fileOrDir}".`);
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ export interface ReviewOptions {
103
+ diff?: boolean;
104
+ }
105
+
106
+ export async function review(fileOrDir: string, options: ReviewOptions = {}) {
107
+ const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
48
108
  if (code === undefined) return;
109
+ const projectRoot = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
110
+ const projectType = detectProjectType(projectRoot);
111
+ const scope = options.diff ? 'the current uncommitted git diff' : 'the supplied source files';
49
112
 
50
113
  await runCommand({
51
114
  name: 'review',
52
115
  input: { fileOrDir },
53
116
  header: '🔍 Code Review: ',
54
117
  buildRequest: (input, model) => ({
55
- prompt: `Please review this code and provide feedback on code quality, best practices, potential issues, and suggestions for improvement. Here is the code to review:\n\nCODE_START\n${code}\nCODE_END\n\nPlease provide your review in a structured format with clear categories and actionable feedback.`,
118
+ prompt: `Please review ${scope} for a ${projectType} project. Provide feedback on code quality, best practices, potential issues, and suggestions for improvement. For every finding, include the file, line or region, severity, explanation, and an actionable recommendation. Here is the code to review:\n\nCODE_START\n${code}\nCODE_END`,
56
119
  systemMessage: getSystemMessage('review'),
57
120
  model,
58
121
  }),