@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
@@ -1,35 +1,143 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { themed } from '../utils/ux.js';
3
+ import { explain } from './explain.js';
4
+ import { suggest } from './suggest.js';
5
+ import { fix } from './fix.js';
6
+ import { review } from './review.js';
7
+ import { optimize } from './optimize.js';
8
+ import { securityCheck } from './security-check.js';
9
+ import { generate } from './generate.js';
10
+ import { init } from './init.js';
11
+ import { status } from './status.js';
12
+ import { health } from './health.js';
13
+ import { metrics } from './metrics.js';
14
+ import { detectProjectType } from '../utils/projectType.js';
15
+ import chalk from 'chalk';
16
+ import { commandCatalog } from '../core/command-catalog.js';
3
17
 
4
18
  const commands = [
5
- { name: 'Explain', value: 'explain' },
6
- { name: 'Suggest', value: 'suggest' },
7
- { name: 'Fix', value: 'fix' },
8
- { name: 'Review', value: 'review' },
9
- { name: 'Optimize', value: 'optimize' },
10
- { name: 'Security Check', value: 'security-check' },
11
- { name: 'Generate', value: 'generate' },
12
- { name: 'Init (Setup)', value: 'init' },
13
- { name: 'Project Type', value: 'project-type' },
14
- { name: 'Exit', value: 'exit' }
19
+ ...commandCatalog.map(({ menuLabel, name }) => ({ name: menuLabel, value: name })),
20
+ { name: 'Exit', value: 'exit' },
15
21
  ];
16
22
 
17
23
  export async function menu() {
18
- let running = true;
19
- while (running) {
20
- const { cmd } = await inquirer.prompt([
21
- {
22
- type: 'list',
23
- name: 'cmd',
24
- message: themed('What do you want to do?', 'primary'),
25
- choices: commands
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
+ ]);
45
+
46
+ if (cmd === 'exit') {
47
+ break;
26
48
  }
27
- ]);
28
- if (cmd === 'exit') {
29
- running = false;
30
- break;
49
+
50
+ try {
51
+ switch (cmd) {
52
+ case 'explain': {
53
+ const { query } = await inquirer.prompt([
54
+ { type: 'input', name: 'query', message: 'What would you like me to explain?' }
55
+ ]);
56
+ if (query) await explain(query);
57
+ break;
58
+ }
59
+ case 'suggest': {
60
+ const { query } = await inquirer.prompt([
61
+ { type: 'input', name: 'query', message: 'What would you like suggestions for?' }
62
+ ]);
63
+ if (query) await suggest(query);
64
+ break;
65
+ }
66
+ case 'fix': {
67
+ const { query } = await inquirer.prompt([
68
+ { type: 'input', name: 'query', message: 'Describe the issue you need help fixing:' }
69
+ ]);
70
+ if (query) await fix(query);
71
+ break;
72
+ }
73
+ case 'review': {
74
+ const { fileOrDir } = await inquirer.prompt([
75
+ { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to review:' }
76
+ ]);
77
+ if (fileOrDir) await review(fileOrDir);
78
+ break;
79
+ }
80
+ case 'optimize': {
81
+ const { file } = await inquirer.prompt([
82
+ { type: 'input', name: 'file', message: 'Enter file path to optimize:' }
83
+ ]);
84
+ if (file) await optimize(file);
85
+ break;
86
+ }
87
+ case 'security-check': {
88
+ const { fileOrDir } = await inquirer.prompt([
89
+ { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to check (or press enter for current directory):', default: '.' }
90
+ ]);
91
+ await securityCheck(fileOrDir);
92
+ break;
93
+ }
94
+ case 'generate': {
95
+ const answers = await inquirer.prompt([
96
+ {
97
+ type: 'list',
98
+ name: 'type',
99
+ message: 'What would you like to generate?',
100
+ choices: ['tests', 'documentation', 'docs', 'component']
101
+ },
102
+ { type: 'input', name: 'target', message: 'Enter target file path:' }
103
+ ]);
104
+ if (answers.target) await generate(answers.type, answers.target);
105
+ break;
106
+ }
107
+ case 'init': {
108
+ await init();
109
+ break;
110
+ }
111
+ case 'project-type': {
112
+ const type = detectProjectType();
113
+ console.log(chalk.blue(`Detected project type: ${type}`));
114
+ break;
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;
128
+ default:
129
+ console.log(themed(`You selected: ${cmd}`, 'accent'));
130
+ }
131
+ } catch (error) {
132
+ console.error(chalk.red(`Error executing ${cmd}: ${(error as Error).message}`));
133
+ }
134
+
135
+ console.log(''); // Add spacing between commands
31
136
  }
32
- // For demo, just print the command. In real use, you would call the command handler.
33
- console.log(themed(`You selected: ${cmd}`, 'accent'));
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}`));
34
142
  }
35
143
  }
@@ -0,0 +1,100 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { printSuccess, printError, printInfo } from '../utils/ux.js';
4
+ import { metricsCollector } from '../core/metrics.js';
5
+ import { logger } from '../core/logger.js';
6
+ import { loadConfig } from '../config/config.js';
7
+
8
+ export interface MetricsOptions {
9
+ raw?: boolean;
10
+ reset?: boolean;
11
+ }
12
+
13
+ export async function metrics(options: MetricsOptions = {}): Promise<void> {
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
+
25
+ // Get metrics data
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`);
49
+
50
+ if (metricsData.length === 0) {
51
+ printInfo('No metrics data available yet. Metrics are collected during CLI usage.');
52
+ return;
53
+ }
54
+
55
+ // Display metrics by category
56
+ const categories = {
57
+ 'Command Metrics': ['dhruv_command', 'dhruv_session'],
58
+ 'AI Service Metrics': ['dhruv_ai_request', 'dhruv_ai_tokens', 'dhruv_cache'],
59
+ 'Performance Metrics': ['dhruv_memory', 'dhruv_performance'],
60
+ 'Error Metrics': ['dhruv_error'],
61
+ 'Plugin Metrics': ['dhruv_plugin']
62
+ };
63
+
64
+ Object.entries(categories).forEach(([category, prefixes]) => {
65
+ const categoryMetrics = metricsData.filter(metric =>
66
+ prefixes.some(prefix => metric.name.startsWith(prefix))
67
+ );
68
+
69
+ if (categoryMetrics.length > 0) {
70
+ console.log(chalk.cyan(`\n📈 ${category}:`));
71
+
72
+ categoryMetrics.forEach(metric => {
73
+ const name = metric.name.replace('dhruv_', '').replace(/_/g, ' ');
74
+ const value = metric.values?.[0]?.value || 0;
75
+ const labels = metric.values?.[0]?.labels || {};
76
+
77
+ console.log(` ${chalk.yellow(name)}: ${chalk.green(value)}`);
78
+
79
+ // Display labels if available
80
+ const labelEntries = Object.entries(labels);
81
+ if (labelEntries.length > 0) {
82
+ console.log(` ${chalk.gray('Labels:')} ${labelEntries.map(([k, v]) => `${k}=${v}`).join(', ')}`);
83
+ }
84
+ });
85
+ }
86
+ });
87
+
88
+ if (options.raw) {
89
+ const rawMetrics = await metricsCollector.getMetrics();
90
+ process.stdout.write(rawMetrics);
91
+ }
92
+
93
+ logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
94
+
95
+ } catch (error) {
96
+ printError('Failed to retrieve metrics');
97
+ console.error(chalk.red((error as Error).message));
98
+ logger.error('Metrics command failed', error as Error);
99
+ }
100
+ }
@@ -1,36 +1,48 @@
1
- import { askOllama } from '../core/ai.js';
2
- import chalk from 'chalk';
3
- import { loadConfig } from '../config/config.js';
4
1
  import fs from 'fs';
5
- import { highlightCode, printError } from '../utils/ux.js';
2
+ import path from 'path';
3
+ import { runCommand } from '../core/command-runner.js';
4
+ import { getSystemMessage } from '../core/prompts.js';
5
+ import { printError } from '../utils/ux.js';
6
+
7
+ function optimizationType(file: string): string {
8
+ const ext = path.extname(file).toLowerCase();
9
+ const fileName = path.basename(file);
10
+ if (fileName === 'package.json') return 'package.json configuration';
11
+ if (ext === '.js' || ext === '.ts') return 'JavaScript/TypeScript code';
12
+ if (ext === '.json') return 'JSON configuration';
13
+ if (ext === '.css') return 'CSS styles';
14
+ if (ext === '.html') return 'HTML markup';
15
+ return 'general code';
16
+ }
6
17
 
7
18
  export async function optimize(file: string) {
8
- const config = loadConfig();
9
- let content = '';
10
- if (fs.existsSync(file)) {
11
- content = fs.readFileSync(file, 'utf-8');
19
+ if (!file || file.trim().length === 0) {
20
+ printError('Please provide a file path to optimize.');
21
+ return;
22
+ }
23
+ if (!fs.existsSync(file)) {
24
+ printError(`File "${file}" does not exist.`);
25
+ return;
12
26
  }
13
- let streamed = '';
27
+
28
+ let content: string;
14
29
  try {
15
- process.stdout.write(chalk.green('Optimization suggestion: '));
16
- await askOllama({
17
- prompt: `Optimize this file:\n${content}`,
18
- model: config.model,
19
- onToken: (token: string) => {
20
- streamed += token;
21
- process.stdout.write(chalk.cyan(token));
22
- }
23
- });
24
- process.stdout.write('\n');
25
- if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
26
- const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
27
- for (const block of codeBlocks) {
28
- const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
29
- if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
30
- }
31
- }
30
+ content = fs.readFileSync(file, 'utf-8');
32
31
  } catch (err) {
33
- printError('Failed to optimize.');
34
- console.error(chalk.red((err as Error).message));
32
+ printError(`Error reading file "${file}": ${(err as Error).message}`);
33
+ return;
35
34
  }
35
+
36
+ const type = optimizationType(file);
37
+ await runCommand({
38
+ name: 'optimize',
39
+ input: { file },
40
+ header: `⚡ Optimization suggestions for ${type}: `,
41
+ buildRequest: (input, model) => ({
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
+ systemMessage: getSystemMessage('optimize'),
44
+ model,
45
+ }),
46
+ footer: `🔍 Want a code review? Try: dhruv review ${file}`,
47
+ });
36
48
  }
@@ -1,47 +1,106 @@
1
- import { askOllama } from '../core/ai.js';
2
- import chalk from 'chalk';
3
- import { loadConfig } from '../config/config.js';
4
1
  import fs from 'fs';
5
- import { highlightCode, printError, createProgressBar } from '../utils/ux.js';
2
+ import path from 'path';
3
+ import { execFileSync } from 'child_process';
4
+ import { runCommand } from '../core/command-runner.js';
5
+ import { getSystemMessage } from '../core/prompts.js';
6
+ import { printError } from '../utils/ux.js';
7
+ import { detectProjectType } from '../utils/projectType.js';
6
8
 
7
- export async function review(fileOrDir: string) {
8
- const config = loadConfig();
9
- let code = '';
10
- if (fs.existsSync(fileOrDir)) {
11
- const stat = fs.statSync(fileOrDir);
12
- if (stat.isDirectory()) {
13
- const files = fs.readdirSync(fileOrDir);
14
- const bar = createProgressBar(files.length);
15
- for (const f of files) {
16
- code += fs.readFileSync(`${fileOrDir}/${f}`,'utf-8') + '\n';
17
- bar.increment();
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. */
13
+ function readCode(fileOrDir: string): string | undefined {
14
+ // Read first, branch on the error: no separate existence check to race against.
15
+ let content: string;
16
+ try {
17
+ content = fs.readFileSync(fileOrDir, 'utf-8');
18
+ } catch (err) {
19
+ const code = (err as NodeJS.ErrnoException).code;
20
+ if (code === 'EISDIR') {
21
+ return readDirectory(fileOrDir);
22
+ }
23
+ printError(`Path "${fileOrDir}" does not exist or could not be read.`);
24
+ return undefined;
25
+ }
26
+ return content;
27
+ }
28
+
29
+ function readDirectory(dir: string): string | undefined {
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('/'));
18
43
  }
19
- bar.stop();
20
- } else {
21
- code = fs.readFileSync(fileOrDir, 'utf-8');
22
44
  }
23
45
  }
24
- let streamed = '';
46
+
47
+ collect(dir);
48
+
49
+ if (files.length === 0) {
50
+ printError(`No code files found in directory "${dir}".`);
51
+ return undefined;
52
+ }
53
+
54
+ let code = '';
55
+ for (const f of files) {
56
+ try {
57
+ code += `\n// File: ${f}\n${fs.readFileSync(path.join(dir, f), 'utf-8')}\n`;
58
+ } catch (err) {
59
+ console.error(`Error reading file ${f}:`, err);
60
+ }
61
+ }
62
+ return code;
63
+ }
64
+
65
+ function readGitDiff(fileOrDir: string): string | undefined {
66
+ const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
25
67
  try {
26
- process.stdout.write(chalk.green('Review: '));
27
- await askOllama({
28
- prompt: `Review this code:\n${code}`,
29
- model: config.model,
30
- onToken: (token: string) => {
31
- streamed += token;
32
- process.stdout.write(chalk.cyan(token));
33
- }
68
+ const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
69
+ cwd: root,
70
+ encoding: 'utf8',
71
+ stdio: ['ignore', 'pipe', 'ignore'],
34
72
  });
35
- process.stdout.write('\n');
36
- if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
37
- const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
38
- for (const block of codeBlocks) {
39
- const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
40
- if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
41
- }
73
+ if (!diff.trim()) {
74
+ printError(`No uncommitted changes found in "${fileOrDir}".`);
75
+ return undefined;
42
76
  }
43
- } catch (err) {
44
- printError('Failed to review code.');
45
- console.error(chalk.red((err as Error).message));
77
+ return diff;
78
+ } catch {
79
+ printError(`Could not read a git diff for "${fileOrDir}".`);
80
+ return undefined;
46
81
  }
47
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);
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';
94
+
95
+ await runCommand({
96
+ name: 'review',
97
+ input: { fileOrDir },
98
+ header: '🔍 Code Review: ',
99
+ buildRequest: (input, model) => ({
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`,
101
+ systemMessage: getSystemMessage('review'),
102
+ model,
103
+ }),
104
+ footer: `🛡️ Security check? Try: dhruv security-check ${fileOrDir}`,
105
+ });
106
+ }
@@ -1,41 +1,135 @@
1
- import { askOllama } from '../core/ai.js';
2
- import chalk from 'chalk';
3
- import { loadConfig } from '../config/config.js';
4
1
  import fs from 'fs';
5
- import { highlightCode, printError } from '../utils/ux.js';
2
+ import path from 'path';
3
+ import { runCommand } from '../core/command-runner.js';
4
+ import { getSystemMessage } from '../core/prompts.js';
5
+ import { printError } from '../utils/ux.js';
6
6
 
7
- export async function securityCheck(fileOrDir: string = '.') {
8
- const config = loadConfig();
9
- let code = '';
10
- if (fs.existsSync(fileOrDir)) {
11
- const stat = fs.statSync(fileOrDir);
12
- if (stat.isDirectory()) {
13
- code = fs.readdirSync(fileOrDir).map(f => fs.readFileSync(`${fileOrDir}/${f}`,'utf-8')).join('\n');
14
- } else {
15
- code = fs.readFileSync(fileOrDir, 'utf-8');
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
+ });
16
50
  }
17
- }
18
- let streamed = '';
51
+ });
52
+
53
+ return findings;
54
+ }
55
+
56
+ export interface SecurityCheckOptions {
57
+ strict?: boolean;
58
+ }
59
+
60
+ /** Reads a file or the code files of a directory (up to 10), concatenated. */
61
+ function readCode(fileOrDir: string): string | undefined {
62
+ // Read first, branch on the error: no separate existence check to race against.
63
+ let content: string;
19
64
  try {
20
- process.stdout.write(chalk.green('Security check result: '));
21
- await askOllama({
22
- prompt: `Security check for this code:\n${code}`,
23
- model: config.model,
24
- onToken: (token: string) => {
25
- streamed += token;
26
- process.stdout.write(chalk.cyan(token));
27
- }
28
- });
29
- process.stdout.write('\n');
30
- if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
31
- const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
32
- for (const block of codeBlocks) {
33
- const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
34
- if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
65
+ content = fs.readFileSync(fileOrDir, 'utf-8');
66
+ } catch (err) {
67
+ const code = (err as NodeJS.ErrnoException).code;
68
+ if (code === 'EISDIR') {
69
+ return readDirectory(fileOrDir);
70
+ }
71
+ printError(`Path "${fileOrDir}" does not exist or could not be read.`);
72
+ return undefined;
73
+ }
74
+ return content;
75
+ }
76
+
77
+ function readDirectory(dir: string): string | undefined {
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('/'));
35
89
  }
36
90
  }
37
- } catch (err) {
38
- printError('Failed to run security check.');
39
- console.error(chalk.red((err as Error).message));
40
91
  }
92
+
93
+ collect(dir);
94
+
95
+ if (files.length === 0) {
96
+ printError(`No code files found in directory "${dir}".`);
97
+ return undefined;
98
+ }
99
+
100
+ let code = '';
101
+ for (const f of files) {
102
+ try {
103
+ code += `\n// File: ${f}\n${fs.readFileSync(path.join(dir, f), 'utf-8')}\n`;
104
+ } catch (err) {
105
+ console.error(`Error reading file ${f}:`, err);
106
+ }
107
+ }
108
+ return code;
109
+ }
110
+
111
+ export async function securityCheck(fileOrDir: string = '.', options: SecurityCheckOptions = {}) {
112
+ const code = readCode(fileOrDir);
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
+ }
123
+
124
+ await runCommand({
125
+ name: 'security-check',
126
+ input: { fileOrDir },
127
+ header: '🛡️ Security Analysis: ',
128
+ buildRequest: (input, model) => ({
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}`,
130
+ systemMessage: getSystemMessage('security'),
131
+ model,
132
+ }),
133
+ footer: `🔧 Need fixes? Try: dhruv fix <security issue>`,
134
+ });
41
135
  }