@rahul05ranjan/dhruv-cli 1.5.0 → 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.
- package/CHANGELOG.md +12 -5
- package/__tests__/cli-contract.test.ts +66 -0
- package/__tests__/core.test.ts +72 -1
- package/__tests__/diagnostics.test.ts +24 -0
- package/__tests__/file-workflows.test.ts +39 -0
- package/__tests__/interactive.test.ts +15 -0
- package/dist/commands/generate.js +26 -7
- package/dist/commands/init.js +8 -6
- package/dist/commands/metrics.js +12 -2
- package/dist/commands/review.js +19 -2
- package/dist/commands/security-check.js +35 -2
- package/dist/commands/status.js +4 -2
- package/dist/core/command-runner.js +8 -2
- package/dist/core/metrics.js +3 -2
- package/dist/index.js +63 -11
- package/dist/utils/projectType.d.ts +6 -0
- package/dist/utils/projectType.js +76 -17
- package/package.json +1 -1
- package/src/commands/generate.ts +27 -7
- package/src/commands/init.ts +8 -6
- package/src/commands/metrics.ts +11 -2
- package/src/commands/review.ts +20 -2
- package/src/commands/security-check.ts +33 -2
- package/src/commands/status.ts +4 -2
- package/src/core/command-runner.ts +8 -2
- package/src/core/metrics.ts +3 -1
- package/src/index.ts +63 -11
- package/src/utils/projectType.ts +75 -14
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ program
|
|
|
46
46
|
.command('review <fileOrDir>')
|
|
47
47
|
.description(commandDescription('review'))
|
|
48
48
|
.option('--diff', 'Review the current uncommitted git diff')
|
|
49
|
-
.action((fileOrDir,
|
|
49
|
+
.action((fileOrDir, options) => review(fileOrDir, options));
|
|
50
50
|
program
|
|
51
51
|
.command('optimize <file>')
|
|
52
52
|
.description(commandDescription('optimize'))
|
|
@@ -55,14 +55,14 @@ program
|
|
|
55
55
|
.command('security-check [fileOrDir]')
|
|
56
56
|
.description(commandDescription('security-check'))
|
|
57
57
|
.option('--strict', 'Exit with failure when high-confidence findings are detected')
|
|
58
|
-
.action((fileOrDir,
|
|
58
|
+
.action((fileOrDir, options) => securityCheck(fileOrDir, options));
|
|
59
59
|
program
|
|
60
60
|
.command('generate <type> <target>')
|
|
61
61
|
.description(commandDescription('generate'))
|
|
62
62
|
.option('--apply', 'Write generated tests to disk (preview is the default)')
|
|
63
63
|
.option('--output <path>', 'Write generated tests to this path')
|
|
64
64
|
.option('--overwrite', 'Allow replacing an existing output file')
|
|
65
|
-
.action((type, target,
|
|
65
|
+
.action((type, target, options) => generate(type, target, options));
|
|
66
66
|
program
|
|
67
67
|
.command('init')
|
|
68
68
|
.description(commandDescription('init'))
|
|
@@ -75,13 +75,13 @@ program
|
|
|
75
75
|
.command('health')
|
|
76
76
|
.description(commandDescription('health'))
|
|
77
77
|
.option('--details', 'Show every health check and diagnostic detail')
|
|
78
|
-
.action((
|
|
78
|
+
.action((options) => health(options));
|
|
79
79
|
program
|
|
80
80
|
.command('metrics')
|
|
81
81
|
.description(commandDescription('metrics'))
|
|
82
82
|
.option('--raw', 'Export raw Prometheus metrics')
|
|
83
83
|
.option('--reset', 'Clear persisted local metrics')
|
|
84
|
-
.action((
|
|
84
|
+
.action((options) => metrics(options));
|
|
85
85
|
program
|
|
86
86
|
.command('project-type')
|
|
87
87
|
.description(commandDescription('project-type'))
|
|
@@ -168,18 +168,70 @@ program
|
|
|
168
168
|
let script = '';
|
|
169
169
|
switch (shell) {
|
|
170
170
|
case 'zsh':
|
|
171
|
-
script = `#compdef dhruv
|
|
171
|
+
script = `#compdef dhruv
|
|
172
|
+
_dhruv_completion() {
|
|
173
|
+
local -a commands
|
|
174
|
+
commands=(${commands})
|
|
175
|
+
_arguments -C \\
|
|
176
|
+
'1:command:->cmds' \\
|
|
177
|
+
'*::options:->args'
|
|
178
|
+
case "$state" in
|
|
179
|
+
cmds)
|
|
180
|
+
_describe -t commands 'dhruv command' commands
|
|
181
|
+
;;
|
|
182
|
+
args)
|
|
183
|
+
case $words[1] in
|
|
184
|
+
generate)
|
|
185
|
+
_arguments '1:type:(tests documentation docs component)' '*:file:_files'
|
|
186
|
+
;;
|
|
187
|
+
review|optimize|security-check)
|
|
188
|
+
_arguments '*:file:_files'
|
|
189
|
+
;;
|
|
190
|
+
completion)
|
|
191
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
192
|
+
;;
|
|
193
|
+
*)
|
|
194
|
+
_arguments '*:options:(${options})'
|
|
195
|
+
;;
|
|
196
|
+
esac
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
compdef _dhruv_completion dhruv`;
|
|
172
201
|
break;
|
|
173
202
|
case 'fish':
|
|
174
|
-
script = `complete -c dhruv -f -n '__fish_use_subcommand' -a '${commands}'\ncomplete -c dhruv -f -n 'not __fish_use_subcommand' -a '${options}'`;
|
|
203
|
+
script = `complete -c dhruv -f -n '__fish_use_subcommand' -a '${commands}'\ncomplete -c dhruv -f -n '__fish_seen_subcommand_from generate' -a 'tests documentation docs component'\ncomplete -c dhruv -f -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'\ncomplete -c dhruv -f -n 'not __fish_use_subcommand' -a '${options}'`;
|
|
175
204
|
break;
|
|
176
205
|
case 'bash':
|
|
177
206
|
script = String.raw `#!/bin/bash
|
|
178
207
|
_dhruv_completion() {
|
|
179
|
-
local commands
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
208
|
+
local cur prev commands options
|
|
209
|
+
COMPREPLY=()
|
|
210
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
211
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
212
|
+
commands="${commands}"
|
|
213
|
+
options="${options}"
|
|
214
|
+
|
|
215
|
+
if [[ "$prev" == "generate" ]]; then
|
|
216
|
+
COMPREPLY=( $(compgen -W "tests documentation docs component" -- "$cur") )
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
if [[ "$prev" == "completion" ]]; then
|
|
220
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
221
|
+
return 0
|
|
222
|
+
fi
|
|
223
|
+
if [[ "$prev" == "review" || "$prev" == "optimize" || "$prev" == "security-check" ]]; then
|
|
224
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
225
|
+
return 0
|
|
226
|
+
fi
|
|
227
|
+
|
|
228
|
+
if [[ "$cur" == -* ]]; then
|
|
229
|
+
COMPREPLY=( $(compgen -W "$options" -- "$cur") )
|
|
230
|
+
elif [[ $COMP_CWORD -eq 1 ]]; then
|
|
231
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
232
|
+
else
|
|
233
|
+
COMPREPLY=( $(compgen -W "$commands $options" -- "$cur") )
|
|
234
|
+
fi
|
|
183
235
|
}
|
|
184
236
|
complete -F _dhruv_completion dhruv`;
|
|
185
237
|
break;
|
|
@@ -1,36 +1,95 @@
|
|
|
1
1
|
// Use .js extension for ESM compatibility
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
export function detectProjectDetails(directory = process.cwd()) {
|
|
5
6
|
const file = (name) => path.join(directory, name);
|
|
6
7
|
if (fs.existsSync(file('package.json'))) {
|
|
7
8
|
let pkg;
|
|
8
9
|
try {
|
|
9
10
|
pkg = JSON.parse(fs.readFileSync(file('package.json'), 'utf-8'));
|
|
10
11
|
}
|
|
11
|
-
catch {
|
|
12
|
-
|
|
12
|
+
catch (err) {
|
|
13
|
+
const diagnostic = `Malformed package.json in ${directory}: ${err.message}`;
|
|
14
|
+
logger.warn(diagnostic);
|
|
15
|
+
return { type: 'unknown', diagnostic };
|
|
13
16
|
}
|
|
14
17
|
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
15
18
|
if (dependencies.react)
|
|
16
|
-
return 'react';
|
|
19
|
+
return { type: 'node', framework: 'react' };
|
|
17
20
|
if (dependencies.next)
|
|
18
|
-
return 'nextjs';
|
|
21
|
+
return { type: 'node', framework: 'nextjs' };
|
|
22
|
+
if (dependencies.vue)
|
|
23
|
+
return { type: 'node', framework: 'vue' };
|
|
24
|
+
if (dependencies['@angular/core'])
|
|
25
|
+
return { type: 'node', framework: 'angular' };
|
|
26
|
+
if (dependencies.svelte)
|
|
27
|
+
return { type: 'node', framework: 'svelte' };
|
|
28
|
+
if (dependencies['@nestjs/core'])
|
|
29
|
+
return { type: 'node', framework: 'nestjs' };
|
|
19
30
|
if (dependencies.express)
|
|
20
|
-
return 'node-express';
|
|
31
|
+
return { type: 'node', framework: 'node-express' };
|
|
21
32
|
if (dependencies.typescript || fs.existsSync(file('tsconfig.json')))
|
|
22
|
-
return 'node-typescript';
|
|
23
|
-
return 'node';
|
|
33
|
+
return { type: 'node-typescript' };
|
|
34
|
+
return { type: 'node' };
|
|
35
|
+
}
|
|
36
|
+
if (fs.existsSync(file('tsconfig.json'))) {
|
|
37
|
+
return { type: 'node-typescript' };
|
|
38
|
+
}
|
|
39
|
+
if (fs.existsSync(file('requirements.txt')) || fs.existsSync(file('pyproject.toml')) || fs.existsSync(file('Pipfile')) || fs.existsSync(file('setup.py'))) {
|
|
40
|
+
let framework;
|
|
41
|
+
if (fs.existsSync(file('manage.py'))) {
|
|
42
|
+
framework = 'django';
|
|
43
|
+
}
|
|
44
|
+
else if (fs.existsSync(file('requirements.txt'))) {
|
|
45
|
+
try {
|
|
46
|
+
const reqs = fs.readFileSync(file('requirements.txt'), 'utf-8');
|
|
47
|
+
if (/fastapi/i.test(reqs))
|
|
48
|
+
framework = 'fastapi';
|
|
49
|
+
else if (/flask/i.test(reqs))
|
|
50
|
+
framework = 'flask';
|
|
51
|
+
else if (/django/i.test(reqs))
|
|
52
|
+
framework = 'django';
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const diagnostic = `Error reading requirements.txt: ${err.message}`;
|
|
56
|
+
logger.warn(diagnostic);
|
|
57
|
+
return { type: 'python', diagnostic };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { type: 'python', framework };
|
|
24
61
|
}
|
|
25
|
-
if (fs.existsSync(file('requirements.txt')))
|
|
26
|
-
return 'python';
|
|
27
|
-
if (fs.existsSync(file('pyproject.toml')))
|
|
28
|
-
return 'python';
|
|
29
62
|
if (fs.existsSync(file('go.mod')))
|
|
30
|
-
return 'go';
|
|
63
|
+
return { type: 'go' };
|
|
31
64
|
if (fs.existsSync(file('Cargo.toml')))
|
|
32
|
-
return 'rust';
|
|
33
|
-
if (fs.existsSync(file('pom.xml')) || fs.existsSync(file('build.gradle')))
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
return { type: 'rust' };
|
|
66
|
+
if (fs.existsSync(file('pom.xml')) || fs.existsSync(file('build.gradle')) || fs.existsSync(file('build.gradle.kts'))) {
|
|
67
|
+
let framework;
|
|
68
|
+
try {
|
|
69
|
+
const pomPath = file('pom.xml');
|
|
70
|
+
const gradlePath = file('build.gradle');
|
|
71
|
+
const content = fs.existsSync(pomPath)
|
|
72
|
+
? fs.readFileSync(pomPath, 'utf-8')
|
|
73
|
+
: (fs.existsSync(gradlePath) ? fs.readFileSync(gradlePath, 'utf-8') : '');
|
|
74
|
+
if (/spring-boot/i.test(content))
|
|
75
|
+
framework = 'spring-boot';
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// safe fallback
|
|
79
|
+
}
|
|
80
|
+
return { type: 'java', framework };
|
|
81
|
+
}
|
|
82
|
+
return { type: 'unknown' };
|
|
83
|
+
}
|
|
84
|
+
export function detectProjectType(directory = process.cwd()) {
|
|
85
|
+
const details = detectProjectDetails(directory);
|
|
86
|
+
if (details.type === 'unknown')
|
|
87
|
+
return 'unknown';
|
|
88
|
+
if (details.framework) {
|
|
89
|
+
if (details.framework === 'react' || details.framework === 'nextjs' || details.framework === 'node-express') {
|
|
90
|
+
return details.framework;
|
|
91
|
+
}
|
|
92
|
+
return `${details.type}-${details.framework}`;
|
|
93
|
+
}
|
|
94
|
+
return details.type;
|
|
36
95
|
}
|
package/package.json
CHANGED
package/src/commands/generate.ts
CHANGED
|
@@ -5,23 +5,43 @@ import { getSystemMessage } from '../core/prompts.js';
|
|
|
5
5
|
import { printError, printSuccess, printInfo } from '../utils/ux.js';
|
|
6
6
|
import { loadConfig } from '../config/config.js';
|
|
7
7
|
|
|
8
|
-
function
|
|
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);
|
|
9
29
|
if (type === 'tests' || type === 'test') {
|
|
10
|
-
return `Generate comprehensive unit tests for the following
|
|
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}`;
|
|
11
31
|
}
|
|
12
32
|
if (type === 'documentation' || type === 'docs') {
|
|
13
|
-
return `Generate JSDoc
|
|
33
|
+
return `Generate ${lang.name === 'Python' ? 'docstrings' : 'JSDoc/documentation'} for the following code:\n\n${content}`;
|
|
14
34
|
}
|
|
15
35
|
return `Generate ${type} for this code:\n\n${content}`;
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
/** Extracts test code from the response: a fenced block if present, else the raw response. */
|
|
19
39
|
function extractTestCode(response: string): string {
|
|
20
|
-
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);
|
|
21
41
|
if (fenced?.[1]) return fenced[1].trim();
|
|
22
42
|
return response
|
|
23
|
-
.replace(/^.*?(?=const|describe|test|it\s*\()/s, '')
|
|
24
|
-
.replace(/```[a-z]*\n?/
|
|
43
|
+
.replace(/^.*?(?=const|describe|test|it\s*\(|def test_|func Test|#\[test\])/s, '')
|
|
44
|
+
.replace(/```[a-z]*\n?/gi, '')
|
|
25
45
|
.trim();
|
|
26
46
|
}
|
|
27
47
|
|
|
@@ -44,7 +64,7 @@ export async function generate(type: string, target: string, options: GenerateOp
|
|
|
44
64
|
input: { type, target },
|
|
45
65
|
header: `🔨 Generating ${type}: `,
|
|
46
66
|
buildRequest: (input, model) => ({
|
|
47
|
-
prompt: buildPrompt(input.type, content),
|
|
67
|
+
prompt: buildPrompt(input.type, content, input.target),
|
|
48
68
|
systemMessage: getSystemMessage('generate'),
|
|
49
69
|
model,
|
|
50
70
|
}),
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
17
|
-
console.log(chalk.yellow('
|
|
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,7 +23,7 @@ export async function init() {
|
|
|
22
23
|
{
|
|
23
24
|
type: 'list',
|
|
24
25
|
name: 'model',
|
|
25
|
-
message:
|
|
26
|
+
message: `Which Ollama model do you want to use? (default: ${current.model})`,
|
|
26
27
|
choices: modelChoices,
|
|
27
28
|
default: current.model,
|
|
28
29
|
},
|
|
@@ -59,11 +60,12 @@ export async function init() {
|
|
|
59
60
|
saveConfig(settings, { scope });
|
|
60
61
|
console.log(chalk.green(`Configuration saved ${scope === 'global' ? 'for your user account' : 'in this project'}!`));
|
|
61
62
|
} catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const isTty = Boolean((error as { isTtyError?: boolean })?.isTtyError);
|
|
64
|
+
process.exitCode = isTty ? 1 : 130;
|
|
65
|
+
if (isTty) {
|
|
64
66
|
console.log(chalk.red('This command requires an interactive terminal.'));
|
|
65
67
|
} else {
|
|
66
|
-
console.log(chalk.red('Configuration cancelled
|
|
68
|
+
console.log(chalk.red('Configuration cancelled.'));
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
}
|
package/src/commands/metrics.ts
CHANGED
|
@@ -93,8 +93,17 @@ export async function metrics(options: MetricsOptions = {}): Promise<void> {
|
|
|
93
93
|
logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
|
|
94
94
|
|
|
95
95
|
} catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
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
|
+
}
|
|
98
107
|
logger.error('Metrics command failed', error as Error);
|
|
99
108
|
}
|
|
100
109
|
}
|
package/src/commands/review.ts
CHANGED
|
@@ -3,11 +3,25 @@ import path from 'path';
|
|
|
3
3
|
import { execFileSync } from 'child_process';
|
|
4
4
|
import { runCommand } from '../core/command-runner.js';
|
|
5
5
|
import { getSystemMessage } from '../core/prompts.js';
|
|
6
|
-
import { printError } from '../utils/ux.js';
|
|
6
|
+
import { printError, printInfo } from '../utils/ux.js';
|
|
7
7
|
import { detectProjectType } from '../utils/projectType.js';
|
|
8
8
|
|
|
9
9
|
const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
|
|
10
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
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
|
+
]);
|
|
11
25
|
|
|
12
26
|
/** Reads a file or up to 10 code files from a directory tree. */
|
|
13
27
|
function readCode(fileOrDir: string): string | undefined {
|
|
@@ -51,6 +65,10 @@ function readDirectory(dir: string): string | undefined {
|
|
|
51
65
|
return undefined;
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
if (files.length >= 10) {
|
|
69
|
+
printInfo('Note: Directory review is capped at the first 10 source files.');
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
let code = '';
|
|
55
73
|
for (const f of files) {
|
|
56
74
|
try {
|
|
@@ -5,13 +5,30 @@ import { getSystemMessage } from '../core/prompts.js';
|
|
|
5
5
|
import { printError } from '../utils/ux.js';
|
|
6
6
|
|
|
7
7
|
const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
|
|
8
|
-
const IGNORED_DIRECTORIES = new Set([
|
|
8
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
9
|
+
'.git',
|
|
10
|
+
'node_modules',
|
|
11
|
+
'dist',
|
|
12
|
+
'build',
|
|
13
|
+
'coverage',
|
|
14
|
+
'.dhruv-cache',
|
|
15
|
+
'logs',
|
|
16
|
+
'.next',
|
|
17
|
+
'.turbo',
|
|
18
|
+
'__pycache__',
|
|
19
|
+
'.pytest_cache',
|
|
20
|
+
'target',
|
|
21
|
+
'vendor',
|
|
22
|
+
]);
|
|
9
23
|
|
|
10
24
|
function redactSensitiveContent(content: string): string {
|
|
11
25
|
return content
|
|
12
26
|
.replace(/(\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`])[^"'`\r\n]+(["'`])/gi, '$1[REDACTED]$2')
|
|
13
27
|
.replace(/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/gi, '[REDACTED]')
|
|
14
|
-
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')
|
|
28
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')
|
|
29
|
+
.replace(/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\b/g, '[REDACTED]')
|
|
30
|
+
.replace(/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED]')
|
|
31
|
+
.replace(/-----BEGIN (?:RSA|OPENSSH|EC|PGP|DSA)? PRIVATE KEY-----[\s\S]*?-----END (?:RSA|OPENSSH|EC|PGP|DSA)? PRIVATE KEY-----/g, '[REDACTED PRIVATE KEY]');
|
|
15
32
|
}
|
|
16
33
|
|
|
17
34
|
interface SecurityFinding {
|
|
@@ -47,6 +64,20 @@ function findHighConfidenceFindings(content: string): SecurityFinding[] {
|
|
|
47
64
|
description: 'bearer token detected',
|
|
48
65
|
remediation: 'revoke the token and use a secure runtime secret store',
|
|
49
66
|
});
|
|
67
|
+
} else if (/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\b/.test(line)) {
|
|
68
|
+
findings.push({
|
|
69
|
+
line: index + 1,
|
|
70
|
+
severity: 'high',
|
|
71
|
+
description: 'GitHub token detected',
|
|
72
|
+
remediation: 'revoke the GitHub token and store it in GitHub Secrets or environment variables',
|
|
73
|
+
});
|
|
74
|
+
} else if (/\bAKIA[0-9A-Z]{16}\b/.test(line)) {
|
|
75
|
+
findings.push({
|
|
76
|
+
line: index + 1,
|
|
77
|
+
severity: 'high',
|
|
78
|
+
description: 'AWS access key ID detected',
|
|
79
|
+
remediation: 'rotate the AWS access key and use IAM roles or AWS Secrets Manager',
|
|
80
|
+
});
|
|
50
81
|
}
|
|
51
82
|
});
|
|
52
83
|
|
package/src/commands/status.ts
CHANGED
|
@@ -70,14 +70,16 @@ export async function status() {
|
|
|
70
70
|
} else {
|
|
71
71
|
process.exitCode = 1;
|
|
72
72
|
printError(`✗ Configured model '${config.model}' is not available`);
|
|
73
|
+
console.log(chalk.yellow(`💡 Install the model: ollama pull ${config.model}`));
|
|
73
74
|
if (models.length > 0) {
|
|
74
75
|
console.log(chalk.yellow(`Available models: ${models.join(', ')}`));
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
} catch (error) {
|
|
79
|
+
process.exitCode = 1;
|
|
78
80
|
printError('✗ Ollama connection failed');
|
|
79
81
|
console.log(chalk.red((error as Error).message));
|
|
80
|
-
console.log(chalk.yellow('\
|
|
81
|
-
console.log(chalk.yellow(
|
|
82
|
+
console.log(chalk.yellow('\n💡 To start Ollama, run: ollama serve'));
|
|
83
|
+
console.log(chalk.yellow(`💡 To install the configured model, run: ollama pull ${config.model}`));
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -43,7 +43,14 @@ export interface CommandSpec {
|
|
|
43
43
|
/** Maps typed AI errors to user-facing hints — once, not per command. */
|
|
44
44
|
function describeAIError(error: unknown, model: string): string {
|
|
45
45
|
if (!error || typeof error !== 'object' || !('kind' in error)) {
|
|
46
|
-
|
|
46
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
47
|
+
if (/econnrefused|failed to connect|fetch failed/i.test(msg)) {
|
|
48
|
+
return `💡 Make sure Ollama is running: ollama serve`;
|
|
49
|
+
}
|
|
50
|
+
if (/model.*not found/i.test(msg)) {
|
|
51
|
+
return `💡 Install the model: ollama pull ${model}`;
|
|
52
|
+
}
|
|
53
|
+
return msg;
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
const typedError = error as AIError;
|
|
@@ -149,7 +156,6 @@ export async function runCommand(spec: CommandSpec): Promise<void> {
|
|
|
149
156
|
} else {
|
|
150
157
|
if (!streamed) process.stdout.write(response);
|
|
151
158
|
process.stdout.write('\n');
|
|
152
|
-
console.log('\n');
|
|
153
159
|
if (spec.footer) console.log(chalk.dim(spec.footer));
|
|
154
160
|
}
|
|
155
161
|
|
package/src/core/metrics.ts
CHANGED
|
@@ -286,7 +286,9 @@ export class MetricsCollector {
|
|
|
286
286
|
try {
|
|
287
287
|
fs.unlinkSync(this.persistentPath());
|
|
288
288
|
} catch (error) {
|
|
289
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
|
|
289
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
290
|
+
logger.debug('Failed to unlink persistent metrics', { error: (error as Error).message });
|
|
291
|
+
}
|
|
290
292
|
}
|
|
291
293
|
}
|
|
292
294
|
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ program
|
|
|
53
53
|
.command('review <fileOrDir>')
|
|
54
54
|
.description(commandDescription('review'))
|
|
55
55
|
.option('--diff', 'Review the current uncommitted git diff')
|
|
56
|
-
.action((fileOrDir: string,
|
|
56
|
+
.action((fileOrDir: string, options: Record<string, any>) => review(fileOrDir, options));
|
|
57
57
|
|
|
58
58
|
program
|
|
59
59
|
.command('optimize <file>')
|
|
@@ -64,7 +64,7 @@ program
|
|
|
64
64
|
.command('security-check [fileOrDir]')
|
|
65
65
|
.description(commandDescription('security-check'))
|
|
66
66
|
.option('--strict', 'Exit with failure when high-confidence findings are detected')
|
|
67
|
-
.action((fileOrDir: string | undefined,
|
|
67
|
+
.action((fileOrDir: string | undefined, options: Record<string, any>) => securityCheck(fileOrDir, options));
|
|
68
68
|
|
|
69
69
|
program
|
|
70
70
|
.command('generate <type> <target>')
|
|
@@ -72,7 +72,7 @@ program
|
|
|
72
72
|
.option('--apply', 'Write generated tests to disk (preview is the default)')
|
|
73
73
|
.option('--output <path>', 'Write generated tests to this path')
|
|
74
74
|
.option('--overwrite', 'Allow replacing an existing output file')
|
|
75
|
-
.action((type: string, target: string,
|
|
75
|
+
.action((type: string, target: string, options: Record<string, any>) => generate(type, target, options));
|
|
76
76
|
|
|
77
77
|
program
|
|
78
78
|
.command('init')
|
|
@@ -88,14 +88,14 @@ program
|
|
|
88
88
|
.command('health')
|
|
89
89
|
.description(commandDescription('health'))
|
|
90
90
|
.option('--details', 'Show every health check and diagnostic detail')
|
|
91
|
-
.action((
|
|
91
|
+
.action((options: Record<string, any>) => health(options));
|
|
92
92
|
|
|
93
93
|
program
|
|
94
94
|
.command('metrics')
|
|
95
95
|
.description(commandDescription('metrics'))
|
|
96
96
|
.option('--raw', 'Export raw Prometheus metrics')
|
|
97
97
|
.option('--reset', 'Clear persisted local metrics')
|
|
98
|
-
.action((
|
|
98
|
+
.action((options: Record<string, any>) => metrics(options));
|
|
99
99
|
|
|
100
100
|
program
|
|
101
101
|
.command('project-type')
|
|
@@ -184,18 +184,70 @@ program
|
|
|
184
184
|
let script = '';
|
|
185
185
|
switch (shell) {
|
|
186
186
|
case 'zsh':
|
|
187
|
-
script = `#compdef dhruv
|
|
187
|
+
script = `#compdef dhruv
|
|
188
|
+
_dhruv_completion() {
|
|
189
|
+
local -a commands
|
|
190
|
+
commands=(${commands})
|
|
191
|
+
_arguments -C \\
|
|
192
|
+
'1:command:->cmds' \\
|
|
193
|
+
'*::options:->args'
|
|
194
|
+
case "$state" in
|
|
195
|
+
cmds)
|
|
196
|
+
_describe -t commands 'dhruv command' commands
|
|
197
|
+
;;
|
|
198
|
+
args)
|
|
199
|
+
case $words[1] in
|
|
200
|
+
generate)
|
|
201
|
+
_arguments '1:type:(tests documentation docs component)' '*:file:_files'
|
|
202
|
+
;;
|
|
203
|
+
review|optimize|security-check)
|
|
204
|
+
_arguments '*:file:_files'
|
|
205
|
+
;;
|
|
206
|
+
completion)
|
|
207
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
208
|
+
;;
|
|
209
|
+
*)
|
|
210
|
+
_arguments '*:options:(${options})'
|
|
211
|
+
;;
|
|
212
|
+
esac
|
|
213
|
+
;;
|
|
214
|
+
esac
|
|
215
|
+
}
|
|
216
|
+
compdef _dhruv_completion dhruv`;
|
|
188
217
|
break;
|
|
189
218
|
case 'fish':
|
|
190
|
-
script = `complete -c dhruv -f -n '__fish_use_subcommand' -a '${commands}'\ncomplete -c dhruv -f -n 'not __fish_use_subcommand' -a '${options}'`;
|
|
219
|
+
script = `complete -c dhruv -f -n '__fish_use_subcommand' -a '${commands}'\ncomplete -c dhruv -f -n '__fish_seen_subcommand_from generate' -a 'tests documentation docs component'\ncomplete -c dhruv -f -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'\ncomplete -c dhruv -f -n 'not __fish_use_subcommand' -a '${options}'`;
|
|
191
220
|
break;
|
|
192
221
|
case 'bash':
|
|
193
222
|
script = String.raw`#!/bin/bash
|
|
194
223
|
_dhruv_completion() {
|
|
195
|
-
local commands
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
224
|
+
local cur prev commands options
|
|
225
|
+
COMPREPLY=()
|
|
226
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
227
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
228
|
+
commands="${commands}"
|
|
229
|
+
options="${options}"
|
|
230
|
+
|
|
231
|
+
if [[ "$prev" == "generate" ]]; then
|
|
232
|
+
COMPREPLY=( $(compgen -W "tests documentation docs component" -- "$cur") )
|
|
233
|
+
return 0
|
|
234
|
+
fi
|
|
235
|
+
if [[ "$prev" == "completion" ]]; then
|
|
236
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
|
|
237
|
+
return 0
|
|
238
|
+
fi
|
|
239
|
+
if [[ "$prev" == "review" || "$prev" == "optimize" || "$prev" == "security-check" ]]; then
|
|
240
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
241
|
+
return 0
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
if [[ "$cur" == -* ]]; then
|
|
245
|
+
COMPREPLY=( $(compgen -W "$options" -- "$cur") )
|
|
246
|
+
elif [[ $COMP_CWORD -eq 1 ]]; then
|
|
247
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
248
|
+
else
|
|
249
|
+
COMPREPLY=( $(compgen -W "$commands $options" -- "$cur") )
|
|
250
|
+
fi
|
|
199
251
|
}
|
|
200
252
|
complete -F _dhruv_completion dhruv`;
|
|
201
253
|
break;
|