@rahul05ranjan/dhruv-cli 1.4.6 → 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 (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 +9 -4
  7. package/README.md +145 -40
  8. package/__tests__/cli-contract.test.ts +104 -0
  9. package/__tests__/core.test.ts +121 -0
  10. package/__tests__/diagnostics.test.ts +155 -0
  11. package/__tests__/file-workflows.test.ts +195 -0
  12. package/__tests__/interactive.test.ts +119 -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 +18 -4
  17. package/dist/commands/health.d.ts +4 -1
  18. package/dist/commands/health.js +59 -16
  19. package/dist/commands/init.js +11 -2
  20. package/dist/commands/menu.js +125 -100
  21. package/dist/commands/metrics.d.ts +5 -1
  22. package/dist/commands/metrics.js +37 -8
  23. package/dist/commands/optimize.js +1 -1
  24. package/dist/commands/review.d.ts +4 -1
  25. package/dist/commands/review.js +48 -8
  26. package/dist/commands/security-check.d.ts +4 -1
  27. package/dist/commands/security-check.js +67 -6
  28. package/dist/commands/status.js +40 -2
  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 +100 -21
  36. package/dist/core/logger.js +1 -0
  37. package/dist/core/metrics.d.ts +28 -0
  38. package/dist/core/metrics.js +78 -0
  39. package/dist/index.js +46 -24
  40. package/dist/utils/projectType.d.ts +1 -1
  41. package/dist/utils/projectType.js +26 -7
  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 +23 -4
  53. package/src/commands/health.ts +62 -17
  54. package/src/commands/init.ts +11 -2
  55. package/src/commands/menu.ts +54 -30
  56. package/src/commands/metrics.ts +42 -7
  57. package/src/commands/optimize.ts +1 -1
  58. package/src/commands/review.ts +53 -8
  59. package/src/commands/security-check.ts +80 -6
  60. package/src/commands/status.ts +39 -3
  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 +102 -22
  65. package/src/core/logger.ts +1 -0
  66. package/src/core/metrics.ts +103 -0
  67. package/src/index.ts +45 -24
  68. package/src/utils/projectType.ts +22 -7
  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.5.0",
4
4
  "description": "AI-powered CLI assistant for developers using Ollama",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,7 +1,9 @@
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
8
  function buildPrompt(type: string, content: string): string {
7
9
  if (type === 'tests' || type === 'test') {
@@ -23,7 +25,13 @@ function extractTestCode(response: string): string {
23
25
  .trim();
24
26
  }
25
27
 
26
- export async function generate(type: string, target: string) {
28
+ export interface GenerateOptions {
29
+ apply?: boolean;
30
+ output?: string;
31
+ overwrite?: boolean;
32
+ }
33
+
34
+ export async function generate(type: string, target: string, options: GenerateOptions = {}) {
27
35
  if (!fs.existsSync(target)) {
28
36
  printError(`Target file "${target}" does not exist.`);
29
37
  return;
@@ -47,9 +55,20 @@ export async function generate(type: string, target: string) {
47
55
  printError('No valid test code generated.');
48
56
  return;
49
57
  }
50
- const testFile = target.replace(/\.[^.]+$/, '.test.js');
58
+ const extension = path.extname(target) || '.js';
59
+ const testFile = options.output ?? target.replace(/\.[^.]+$/, `.test${extension}`);
60
+ if (!options.apply && !options.output) {
61
+ if (loadConfig().responseFormat !== 'json') {
62
+ printInfo(`Preview only. Use --apply to write ${testFile}, or --output <path> to choose a destination.`);
63
+ }
64
+ return;
65
+ }
66
+ if (fs.existsSync(testFile) && !options.overwrite) {
67
+ printError(`Test file "${testFile}" already exists. Use --overwrite to replace it.`);
68
+ return;
69
+ }
51
70
  fs.writeFileSync(testFile, codeToSave);
52
- printSuccess(`Test file saved: ${testFile}`);
71
+ if (loadConfig().responseFormat !== 'json') printSuccess(`Test file saved: ${testFile}`);
53
72
  },
54
73
  footer: `🔍 Want a review? Try: dhruv review ${target}`,
55
74
  });
@@ -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
+ }
@@ -26,6 +26,13 @@ export async function init() {
26
26
  choices: modelChoices,
27
27
  default: current.model,
28
28
  },
29
+ {
30
+ type: 'list',
31
+ name: 'scope',
32
+ message: 'Where should Dhruv save these settings?',
33
+ choices: ['local', 'global'],
34
+ default: 'local',
35
+ },
29
36
  {
30
37
  type: 'list',
31
38
  name: 'responseFormat',
@@ -48,9 +55,11 @@ export async function init() {
48
55
  },
49
56
  ]);
50
57
 
51
- saveConfig(answers);
52
- console.log(chalk.green('Configuration saved!'));
58
+ const { scope, ...settings } = answers;
59
+ saveConfig(settings, { scope });
60
+ console.log(chalk.green(`Configuration saved ${scope === 'global' ? 'for your user account' : 'in this project'}!`));
53
61
  } catch (error) {
62
+ process.exitCode = 130;
54
63
  if ((error as { isTtyError?: boolean })?.isTtyError) {
55
64
  console.log(chalk.red('This command requires an interactive terminal.'));
56
65
  } else {
@@ -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,11 +85,10 @@ 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
 
@@ -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,15 @@
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
6
  import { printError } 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(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
11
+
12
+ /** Reads a file or up to 10 code files from a directory tree. */
8
13
  function readCode(fileOrDir: string): string | undefined {
9
14
  // Read first, branch on the error: no separate existence check to race against.
10
15
  let content: string;
@@ -22,10 +27,24 @@ function readCode(fileOrDir: string): string | undefined {
22
27
  }
23
28
 
24
29
  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);
30
+ const files: string[] = [];
31
+
32
+ function collect(current: string): void {
33
+ if (files.length >= 10) return;
34
+
35
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
36
+ if (files.length >= 10) return;
37
+ const absolute = path.join(current, entry.name);
38
+
39
+ if (entry.isDirectory()) {
40
+ if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
41
+ } else if (entry.isFile() && CODE_FILE.test(entry.name)) {
42
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
43
+ }
44
+ }
45
+ }
46
+
47
+ collect(dir);
29
48
 
30
49
  if (files.length === 0) {
31
50
  printError(`No code files found in directory "${dir}".`);
@@ -43,16 +62,42 @@ function readDirectory(dir: string): string | undefined {
43
62
  return code;
44
63
  }
45
64
 
46
- export async function review(fileOrDir: string) {
47
- const code = readCode(fileOrDir);
65
+ function readGitDiff(fileOrDir: string): string | undefined {
66
+ const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
67
+ try {
68
+ const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
69
+ cwd: root,
70
+ encoding: 'utf8',
71
+ stdio: ['ignore', 'pipe', 'ignore'],
72
+ });
73
+ if (!diff.trim()) {
74
+ printError(`No uncommitted changes found in "${fileOrDir}".`);
75
+ return undefined;
76
+ }
77
+ return diff;
78
+ } catch {
79
+ printError(`Could not read a git diff for "${fileOrDir}".`);
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ export interface ReviewOptions {
85
+ diff?: boolean;
86
+ }
87
+
88
+ export async function review(fileOrDir: string, options: ReviewOptions = {}) {
89
+ const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
48
90
  if (code === undefined) return;
91
+ const projectRoot = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
92
+ const projectType = detectProjectType(projectRoot);
93
+ const scope = options.diff ? 'the current uncommitted git diff' : 'the supplied source files';
49
94
 
50
95
  await runCommand({
51
96
  name: 'review',
52
97
  input: { fileOrDir },
53
98
  header: '🔍 Code Review: ',
54
99
  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.`,
100
+ 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
101
  systemMessage: getSystemMessage('review'),
57
102
  model,
58
103
  }),
@@ -4,6 +4,59 @@ import { runCommand } from '../core/command-runner.js';
4
4
  import { getSystemMessage } from '../core/prompts.js';
5
5
  import { printError } from '../utils/ux.js';
6
6
 
7
+ const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
8
+ const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
9
+
10
+ function redactSensitiveContent(content: string): string {
11
+ return content
12
+ .replace(/(\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`])[^"'`\r\n]+(["'`])/gi, '$1[REDACTED]$2')
13
+ .replace(/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/gi, '[REDACTED]')
14
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]');
15
+ }
16
+
17
+ interface SecurityFinding {
18
+ line: number;
19
+ severity: 'high';
20
+ description: string;
21
+ remediation: string;
22
+ }
23
+
24
+ function findHighConfidenceFindings(content: string): SecurityFinding[] {
25
+ const findings: SecurityFinding[] = [];
26
+ const lines = content.split(/\r?\n/);
27
+
28
+ lines.forEach((line, index) => {
29
+ if (/\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`][^"'`\r\n]+["'`]/i.test(line)) {
30
+ findings.push({
31
+ line: index + 1,
32
+ severity: 'high',
33
+ description: 'credential-like value assigned in source',
34
+ remediation: 'rotate the credential and load it from a secret manager or environment variable',
35
+ });
36
+ } else if (/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/i.test(line)) {
37
+ findings.push({
38
+ line: index + 1,
39
+ severity: 'high',
40
+ description: 'credential-like API key detected',
41
+ remediation: 'rotate the credential and remove it from source control',
42
+ });
43
+ } else if (/\bBearer\s+[A-Za-z0-9._~+/=-]+/i.test(line)) {
44
+ findings.push({
45
+ line: index + 1,
46
+ severity: 'high',
47
+ description: 'bearer token detected',
48
+ remediation: 'revoke the token and use a secure runtime secret store',
49
+ });
50
+ }
51
+ });
52
+
53
+ return findings;
54
+ }
55
+
56
+ export interface SecurityCheckOptions {
57
+ strict?: boolean;
58
+ }
59
+
7
60
  /** Reads a file or the code files of a directory (up to 10), concatenated. */
8
61
  function readCode(fileOrDir: string): string | undefined {
9
62
  // Read first, branch on the error: no separate existence check to race against.
@@ -22,10 +75,22 @@ function readCode(fileOrDir: string): string | undefined {
22
75
  }
23
76
 
24
77
  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);
78
+ const files: string[] = [];
79
+
80
+ function collect(current: string): void {
81
+ if (files.length >= 10) return;
82
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
83
+ if (files.length >= 10) return;
84
+ const absolute = path.join(current, entry.name);
85
+ if (entry.isDirectory()) {
86
+ if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
87
+ } else if (entry.isFile() && CODE_FILE.test(entry.name)) {
88
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
89
+ }
90
+ }
91
+ }
92
+
93
+ collect(dir);
29
94
 
30
95
  if (files.length === 0) {
31
96
  printError(`No code files found in directory "${dir}".`);
@@ -43,16 +108,25 @@ function readDirectory(dir: string): string | undefined {
43
108
  return code;
44
109
  }
45
110
 
46
- export async function securityCheck(fileOrDir: string = '.') {
111
+ export async function securityCheck(fileOrDir: string = '.', options: SecurityCheckOptions = {}) {
47
112
  const code = readCode(fileOrDir);
48
113
  if (code === undefined) return;
114
+ const findings = findHighConfidenceFindings(code);
115
+ const safeCode = redactSensitiveContent(code);
116
+ const findingSummary = findings.length === 0
117
+ ? 'none'
118
+ : findings.map((finding) => `- ${finding.severity} at line ${finding.line}: ${finding.description}; remediation: ${finding.remediation}`).join('\n');
119
+
120
+ if (options.strict && findings.length > 0) {
121
+ process.exitCode = 1;
122
+ }
49
123
 
50
124
  await runCommand({
51
125
  name: 'security-check',
52
126
  input: { fileOrDir },
53
127
  header: '🛡️ Security Analysis: ',
54
128
  buildRequest: (input, model) => ({
55
- prompt: `Perform a security analysis on this code. Look for common security vulnerabilities, unsafe practices, potential injection attacks, and provide recommendations for improvement:\n\n${code}`,
129
+ prompt: `Perform a security analysis on this code. Look for common security vulnerabilities, unsafe practices, potential injection attacks, and provide recommendations for improvement. High-confidence pre-scan findings:\n${findingSummary}\n\n${safeCode}`,
56
130
  systemMessage: getSystemMessage('security'),
57
131
  model,
58
132
  }),