@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,33 +1,146 @@
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
  const commands = [
4
- { name: 'Explain', value: 'explain' },
5
- { name: 'Suggest', value: 'suggest' },
6
- { name: 'Fix', value: 'fix' },
7
- { name: 'Review', value: 'review' },
8
- { name: 'Optimize', value: 'optimize' },
9
- { name: 'Security Check', value: 'security-check' },
10
- { name: 'Generate', value: 'generate' },
11
- { name: 'Init (Setup)', value: 'init' },
12
- { name: 'Project Type', value: 'project-type' },
13
- { name: 'Exit', value: 'exit' }
18
+ ...commandCatalog.map(({ menuLabel, name }) => ({ name: menuLabel, value: name })),
19
+ { name: 'Exit', value: 'exit' },
14
20
  ];
15
21
  export async function menu() {
16
- let running = true;
17
- while (running) {
18
- const { cmd } = await inquirer.prompt([
19
- {
20
- type: 'list',
21
- name: 'cmd',
22
- message: themed('What do you want to do?', 'primary'),
23
- choices: commands
22
+ try {
23
+ while (true) {
24
+ const { filter = '' } = await inquirer.prompt([
25
+ {
26
+ type: 'input',
27
+ name: 'filter',
28
+ message: 'Filter commands (press enter to show all):',
29
+ },
30
+ ]);
31
+ const normalizedFilter = String(filter).trim().toLowerCase();
32
+ const filteredCommands = normalizedFilter
33
+ ? commands.filter((command) => command.name.toLowerCase().includes(normalizedFilter) || command.value.includes(normalizedFilter))
34
+ : commands;
35
+ const { cmd } = await inquirer.prompt([
36
+ {
37
+ type: 'list',
38
+ name: 'cmd',
39
+ message: themed('What do you want to do?', 'primary'),
40
+ choices: filteredCommands.length > 0 ? filteredCommands : [{ name: 'No matching commands — Exit', value: 'exit' }],
41
+ }
42
+ ]);
43
+ if (cmd === 'exit') {
44
+ break;
24
45
  }
25
- ]);
26
- if (cmd === 'exit') {
27
- running = false;
28
- break;
46
+ try {
47
+ switch (cmd) {
48
+ case 'explain': {
49
+ const { query } = await inquirer.prompt([
50
+ { type: 'input', name: 'query', message: 'What would you like me to explain?' }
51
+ ]);
52
+ if (query)
53
+ await explain(query);
54
+ break;
55
+ }
56
+ case 'suggest': {
57
+ const { query } = await inquirer.prompt([
58
+ { type: 'input', name: 'query', message: 'What would you like suggestions for?' }
59
+ ]);
60
+ if (query)
61
+ await suggest(query);
62
+ break;
63
+ }
64
+ case 'fix': {
65
+ const { query } = await inquirer.prompt([
66
+ { type: 'input', name: 'query', message: 'Describe the issue you need help fixing:' }
67
+ ]);
68
+ if (query)
69
+ await fix(query);
70
+ break;
71
+ }
72
+ case 'review': {
73
+ const { fileOrDir } = await inquirer.prompt([
74
+ { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to review:' }
75
+ ]);
76
+ if (fileOrDir)
77
+ 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)
85
+ await optimize(file);
86
+ break;
87
+ }
88
+ case 'security-check': {
89
+ const { fileOrDir } = await inquirer.prompt([
90
+ { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to check (or press enter for current directory):', default: '.' }
91
+ ]);
92
+ await securityCheck(fileOrDir);
93
+ break;
94
+ }
95
+ case 'generate': {
96
+ const answers = await inquirer.prompt([
97
+ {
98
+ type: 'list',
99
+ name: 'type',
100
+ message: 'What would you like to generate?',
101
+ choices: ['tests', 'documentation', 'docs', 'component']
102
+ },
103
+ { type: 'input', name: 'target', message: 'Enter target file path:' }
104
+ ]);
105
+ if (answers.target)
106
+ await generate(answers.type, answers.target);
107
+ break;
108
+ }
109
+ case 'init': {
110
+ await init();
111
+ break;
112
+ }
113
+ case 'project-type': {
114
+ const type = detectProjectType();
115
+ console.log(chalk.blue(`Detected project type: ${type}`));
116
+ break;
117
+ }
118
+ case 'status':
119
+ await status();
120
+ break;
121
+ case 'health':
122
+ await health();
123
+ break;
124
+ case 'metrics':
125
+ await metrics();
126
+ break;
127
+ case 'completion':
128
+ console.log(themed('Run `dhruv completion <bash|zsh|fish>` to install shell completion.', 'accent'));
129
+ break;
130
+ default:
131
+ console.log(themed(`You selected: ${cmd}`, 'accent'));
132
+ }
133
+ }
134
+ catch (error) {
135
+ console.error(chalk.red(`Error executing ${cmd}: ${error.message}`));
136
+ }
137
+ console.log(''); // Add spacing between commands
29
138
  }
30
- // For demo, just print the command. In real use, you would call the command handler.
31
- console.log(themed(`You selected: ${cmd}`, 'accent'));
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ const cancelled = /cancel|force closed|exitprompt/i.test(message);
143
+ process.exitCode = cancelled ? 130 : 1;
144
+ console.error(chalk.red(cancelled ? 'Interactive menu cancelled.' : `Interactive menu failed: ${message}`));
32
145
  }
33
146
  }
@@ -0,0 +1,5 @@
1
+ export interface MetricsOptions {
2
+ raw?: boolean;
3
+ reset?: boolean;
4
+ }
5
+ export declare function metrics(options?: MetricsOptions): Promise<void>;
@@ -0,0 +1,80 @@
1
+ import chalk from 'chalk';
2
+ import { printSuccess, printError, printInfo } from '../utils/ux.js';
3
+ import { metricsCollector } from '../core/metrics.js';
4
+ import { logger } from '../core/logger.js';
5
+ import { loadConfig } from '../config/config.js';
6
+ export async function metrics(options = {}) {
7
+ try {
8
+ if (options.reset) {
9
+ metricsCollector.resetPersistent();
10
+ if (loadConfig().responseFormat === 'json') {
11
+ process.stdout.write(`${JSON.stringify({ ok: true, command: 'metrics', reset: true, summary: metricsCollector.getSummary() })}\n`);
12
+ }
13
+ else {
14
+ printSuccess('Local metrics reset.');
15
+ }
16
+ return;
17
+ }
18
+ // Get metrics data
19
+ const metricsData = await metricsCollector.getMetricsJSON();
20
+ const summary = metricsCollector.getSummary();
21
+ if (loadConfig().responseFormat === 'json') {
22
+ process.stdout.write(`${JSON.stringify({
23
+ ok: true,
24
+ command: 'metrics',
25
+ summary,
26
+ metrics: metricsData,
27
+ })}\n`);
28
+ return;
29
+ }
30
+ console.log(chalk.blue.bold('📊 Dhruv CLI Metrics\n'));
31
+ console.log(chalk.cyan('📌 Local summary:'));
32
+ console.log(` Sessions: ${chalk.green(summary.sessions)}`);
33
+ Object.entries(summary.commands).forEach(([command, data]) => {
34
+ console.log(` ${chalk.yellow(command)}: ${data.runs} runs, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
35
+ });
36
+ Object.entries(summary.models).forEach(([model, data]) => {
37
+ console.log(` ${chalk.yellow(model)}: ${data.requests} requests, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
38
+ });
39
+ console.log(` Cache: ${chalk.green(summary.cache.hits)} hits, ${chalk.yellow(summary.cache.misses)} misses`);
40
+ if (metricsData.length === 0) {
41
+ printInfo('No metrics data available yet. Metrics are collected during CLI usage.');
42
+ return;
43
+ }
44
+ // Display metrics by category
45
+ const categories = {
46
+ 'Command Metrics': ['dhruv_command', 'dhruv_session'],
47
+ 'AI Service Metrics': ['dhruv_ai_request', 'dhruv_ai_tokens', 'dhruv_cache'],
48
+ 'Performance Metrics': ['dhruv_memory', 'dhruv_performance'],
49
+ 'Error Metrics': ['dhruv_error'],
50
+ 'Plugin Metrics': ['dhruv_plugin']
51
+ };
52
+ Object.entries(categories).forEach(([category, prefixes]) => {
53
+ const categoryMetrics = metricsData.filter(metric => prefixes.some(prefix => metric.name.startsWith(prefix)));
54
+ if (categoryMetrics.length > 0) {
55
+ console.log(chalk.cyan(`\n📈 ${category}:`));
56
+ categoryMetrics.forEach(metric => {
57
+ const name = metric.name.replace('dhruv_', '').replace(/_/g, ' ');
58
+ const value = metric.values?.[0]?.value || 0;
59
+ const labels = metric.values?.[0]?.labels || {};
60
+ console.log(` ${chalk.yellow(name)}: ${chalk.green(value)}`);
61
+ // Display labels if available
62
+ const labelEntries = Object.entries(labels);
63
+ if (labelEntries.length > 0) {
64
+ console.log(` ${chalk.gray('Labels:')} ${labelEntries.map(([k, v]) => `${k}=${v}`).join(', ')}`);
65
+ }
66
+ });
67
+ }
68
+ });
69
+ if (options.raw) {
70
+ const rawMetrics = await metricsCollector.getMetrics();
71
+ process.stdout.write(rawMetrics);
72
+ }
73
+ logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
74
+ }
75
+ catch (error) {
76
+ printError('Failed to retrieve metrics');
77
+ console.error(chalk.red(error.message));
78
+ logger.error('Metrics command failed', error);
79
+ }
80
+ }
@@ -1,42 +1,50 @@
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
+ function optimizationType(file) {
7
+ const ext = path.extname(file).toLowerCase();
8
+ const fileName = path.basename(file);
9
+ if (fileName === 'package.json')
10
+ return 'package.json configuration';
11
+ if (ext === '.js' || ext === '.ts')
12
+ return 'JavaScript/TypeScript code';
13
+ if (ext === '.json')
14
+ return 'JSON configuration';
15
+ if (ext === '.css')
16
+ return 'CSS styles';
17
+ if (ext === '.html')
18
+ return 'HTML markup';
19
+ return 'general code';
20
+ }
6
21
  export async function optimize(file) {
7
- const config = loadConfig();
8
- let content = '';
9
- if (fs.existsSync(file)) {
10
- content = fs.readFileSync(file, 'utf-8');
22
+ if (!file || file.trim().length === 0) {
23
+ printError('Please provide a file path to optimize.');
24
+ return;
11
25
  }
12
- let streamed = '';
26
+ if (!fs.existsSync(file)) {
27
+ printError(`File "${file}" does not exist.`);
28
+ return;
29
+ }
30
+ let content;
13
31
  try {
14
- process.stdout.write(chalk.green('Optimization suggestion: '));
15
- await askOllama({
16
- prompt: `Optimize this file:\n${content}`,
17
- model: config.model,
18
- onToken: (token) => {
19
- streamed += token;
20
- process.stdout.write(chalk.cyan(token));
21
- }
22
- });
23
- process.stdout.write('\n');
24
- if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
25
- const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
26
- for (const block of codeBlocks) {
27
- const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
28
- if (code)
29
- try {
30
- console.log(highlightCode(code, lang || 'js'));
31
- }
32
- catch (err) {
33
- console.error('Highlight error:', err);
34
- }
35
- }
36
- }
32
+ content = fs.readFileSync(file, 'utf-8');
37
33
  }
38
34
  catch (err) {
39
- printError('Failed to optimize.');
40
- console.error(chalk.red(err.message));
35
+ printError(`Error reading file "${file}": ${err.message}`);
36
+ return;
41
37
  }
38
+ const type = optimizationType(file);
39
+ await runCommand({
40
+ name: 'optimize',
41
+ input: { file },
42
+ header: `⚡ Optimization suggestions for ${type}: `,
43
+ buildRequest: (input, model) => ({
44
+ 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.`,
45
+ systemMessage: getSystemMessage('optimize'),
46
+ model,
47
+ }),
48
+ footer: `🔍 Want a code review? Try: dhruv review ${file}`,
49
+ });
42
50
  }
@@ -1 +1,4 @@
1
- export declare function review(fileOrDir: string): Promise<void>;
1
+ export interface ReviewOptions {
2
+ diff?: boolean;
3
+ }
4
+ export declare function review(fileOrDir: string, options?: ReviewOptions): Promise<void>;
@@ -1,54 +1,98 @@
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';
6
- export async function review(fileOrDir) {
7
- const config = loadConfig();
8
- let code = '';
9
- if (fs.existsSync(fileOrDir)) {
10
- const stat = fs.statSync(fileOrDir);
11
- if (stat.isDirectory()) {
12
- const files = fs.readdirSync(fileOrDir);
13
- const bar = createProgressBar(files.length);
14
- for (const f of files) {
15
- code += fs.readFileSync(`${fileOrDir}/${f}`, 'utf-8') + '\n';
16
- bar.increment();
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';
8
+ const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
9
+ const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
10
+ /** Reads a file or up to 10 code files from a directory tree. */
11
+ function readCode(fileOrDir) {
12
+ // Read first, branch on the error: no separate existence check to race against.
13
+ let content;
14
+ try {
15
+ content = fs.readFileSync(fileOrDir, 'utf-8');
16
+ }
17
+ catch (err) {
18
+ const code = err.code;
19
+ if (code === 'EISDIR') {
20
+ return readDirectory(fileOrDir);
21
+ }
22
+ printError(`Path "${fileOrDir}" does not exist or could not be read.`);
23
+ return undefined;
24
+ }
25
+ return content;
26
+ }
27
+ function readDirectory(dir) {
28
+ const files = [];
29
+ function collect(current) {
30
+ if (files.length >= 10)
31
+ return;
32
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
33
+ if (files.length >= 10)
34
+ return;
35
+ const absolute = path.join(current, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (!IGNORED_DIRECTORIES.has(entry.name))
38
+ collect(absolute);
39
+ }
40
+ else if (entry.isFile() && CODE_FILE.test(entry.name)) {
41
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
17
42
  }
18
- bar.stop();
19
43
  }
20
- else {
21
- code = fs.readFileSync(fileOrDir, 'utf-8');
44
+ }
45
+ collect(dir);
46
+ if (files.length === 0) {
47
+ printError(`No code files found in directory "${dir}".`);
48
+ return undefined;
49
+ }
50
+ let code = '';
51
+ for (const f of files) {
52
+ try {
53
+ code += `\n// File: ${f}\n${fs.readFileSync(path.join(dir, f), 'utf-8')}\n`;
54
+ }
55
+ catch (err) {
56
+ console.error(`Error reading file ${f}:`, err);
22
57
  }
23
58
  }
24
- let streamed = '';
59
+ return code;
60
+ }
61
+ function readGitDiff(fileOrDir) {
62
+ const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
25
63
  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) => {
31
- streamed += token;
32
- process.stdout.write(chalk.cyan(token));
33
- }
64
+ const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
65
+ cwd: root,
66
+ encoding: 'utf8',
67
+ stdio: ['ignore', 'pipe', 'ignore'],
34
68
  });
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)
41
- try {
42
- console.log(highlightCode(code, lang || 'js'));
43
- }
44
- catch (err) {
45
- console.error('Highlight error:', err);
46
- }
47
- }
69
+ if (!diff.trim()) {
70
+ printError(`No uncommitted changes found in "${fileOrDir}".`);
71
+ return undefined;
48
72
  }
73
+ return diff;
49
74
  }
50
- catch (err) {
51
- printError('Failed to review code.');
52
- console.error(chalk.red(err.message));
75
+ catch {
76
+ printError(`Could not read a git diff for "${fileOrDir}".`);
77
+ return undefined;
53
78
  }
54
79
  }
80
+ export async function review(fileOrDir, options = {}) {
81
+ const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
82
+ if (code === undefined)
83
+ return;
84
+ const projectRoot = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
85
+ const projectType = detectProjectType(projectRoot);
86
+ const scope = options.diff ? 'the current uncommitted git diff' : 'the supplied source files';
87
+ await runCommand({
88
+ name: 'review',
89
+ input: { fileOrDir },
90
+ header: '🔍 Code Review: ',
91
+ buildRequest: (input, model) => ({
92
+ 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`,
93
+ systemMessage: getSystemMessage('review'),
94
+ model,
95
+ }),
96
+ footer: `🛡️ Security check? Try: dhruv security-check ${fileOrDir}`,
97
+ });
98
+ }
@@ -1 +1,4 @@
1
- export declare function securityCheck(fileOrDir?: string): Promise<void>;
1
+ export interface SecurityCheckOptions {
2
+ strict?: boolean;
3
+ }
4
+ export declare function securityCheck(fileOrDir?: string, options?: SecurityCheckOptions): Promise<void>;
@@ -1,48 +1,119 @@
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';
6
- export async function securityCheck(fileOrDir = '.') {
7
- const config = loadConfig();
8
- let code = '';
9
- if (fs.existsSync(fileOrDir)) {
10
- const stat = fs.statSync(fileOrDir);
11
- if (stat.isDirectory()) {
12
- code = fs.readdirSync(fileOrDir).map(f => fs.readFileSync(`${fileOrDir}/${f}`, 'utf-8')).join('\n');
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
+ const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
7
+ const IGNORED_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
8
+ function redactSensitiveContent(content) {
9
+ return content
10
+ .replace(/(\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`])[^"'`\r\n]+(["'`])/gi, '$1[REDACTED]$2')
11
+ .replace(/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/gi, '[REDACTED]')
12
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]');
13
+ }
14
+ function findHighConfidenceFindings(content) {
15
+ const findings = [];
16
+ const lines = content.split(/\r?\n/);
17
+ lines.forEach((line, index) => {
18
+ if (/\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`][^"'`\r\n]+["'`]/i.test(line)) {
19
+ findings.push({
20
+ line: index + 1,
21
+ severity: 'high',
22
+ description: 'credential-like value assigned in source',
23
+ remediation: 'rotate the credential and load it from a secret manager or environment variable',
24
+ });
13
25
  }
14
- else {
15
- code = fs.readFileSync(fileOrDir, 'utf-8');
26
+ else if (/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/i.test(line)) {
27
+ findings.push({
28
+ line: index + 1,
29
+ severity: 'high',
30
+ description: 'credential-like API key detected',
31
+ remediation: 'rotate the credential and remove it from source control',
32
+ });
16
33
  }
17
- }
18
- let streamed = '';
34
+ else if (/\bBearer\s+[A-Za-z0-9._~+/=-]+/i.test(line)) {
35
+ findings.push({
36
+ line: index + 1,
37
+ severity: 'high',
38
+ description: 'bearer token detected',
39
+ remediation: 'revoke the token and use a secure runtime secret store',
40
+ });
41
+ }
42
+ });
43
+ return findings;
44
+ }
45
+ /** Reads a file or the code files of a directory (up to 10), concatenated. */
46
+ function readCode(fileOrDir) {
47
+ // Read first, branch on the error: no separate existence check to race against.
48
+ let content;
19
49
  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) => {
25
- streamed += token;
26
- process.stdout.write(chalk.cyan(token));
50
+ content = fs.readFileSync(fileOrDir, 'utf-8');
51
+ }
52
+ catch (err) {
53
+ const code = err.code;
54
+ if (code === 'EISDIR') {
55
+ return readDirectory(fileOrDir);
56
+ }
57
+ printError(`Path "${fileOrDir}" does not exist or could not be read.`);
58
+ return undefined;
59
+ }
60
+ return content;
61
+ }
62
+ function readDirectory(dir) {
63
+ const files = [];
64
+ function collect(current) {
65
+ if (files.length >= 10)
66
+ return;
67
+ for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
68
+ if (files.length >= 10)
69
+ return;
70
+ const absolute = path.join(current, entry.name);
71
+ if (entry.isDirectory()) {
72
+ if (!IGNORED_DIRECTORIES.has(entry.name))
73
+ collect(absolute);
27
74
  }
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)
35
- try {
36
- console.log(highlightCode(code, lang || 'js'));
37
- }
38
- catch (err) {
39
- console.error('Highlight error:', err);
40
- }
75
+ else if (entry.isFile() && CODE_FILE.test(entry.name)) {
76
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
41
77
  }
42
78
  }
43
79
  }
44
- catch (err) {
45
- printError('Failed to run security check.');
46
- console.error(chalk.red(err.message));
80
+ collect(dir);
81
+ if (files.length === 0) {
82
+ printError(`No code files found in directory "${dir}".`);
83
+ return undefined;
84
+ }
85
+ let code = '';
86
+ for (const f of files) {
87
+ try {
88
+ code += `\n// File: ${f}\n${fs.readFileSync(path.join(dir, f), 'utf-8')}\n`;
89
+ }
90
+ catch (err) {
91
+ console.error(`Error reading file ${f}:`, err);
92
+ }
93
+ }
94
+ return code;
95
+ }
96
+ export async function securityCheck(fileOrDir = '.', options = {}) {
97
+ const code = readCode(fileOrDir);
98
+ if (code === undefined)
99
+ return;
100
+ const findings = findHighConfidenceFindings(code);
101
+ const safeCode = redactSensitiveContent(code);
102
+ const findingSummary = findings.length === 0
103
+ ? 'none'
104
+ : findings.map((finding) => `- ${finding.severity} at line ${finding.line}: ${finding.description}; remediation: ${finding.remediation}`).join('\n');
105
+ if (options.strict && findings.length > 0) {
106
+ process.exitCode = 1;
47
107
  }
108
+ await runCommand({
109
+ name: 'security-check',
110
+ input: { fileOrDir },
111
+ header: '🛡️ Security Analysis: ',
112
+ buildRequest: (input, model) => ({
113
+ 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}`,
114
+ systemMessage: getSystemMessage('security'),
115
+ model,
116
+ }),
117
+ footer: `🔧 Need fixes? Try: dhruv fix <security issue>`,
118
+ });
48
119
  }
@@ -0,0 +1 @@
1
+ export declare function status(): Promise<void>;