@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/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, command) => review(fileOrDir, command.opts()));
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, command) => securityCheck(fileOrDir, command.opts()));
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, command) => generate(type, target, command.opts()));
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((command) => health(command.opts()));
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((command) => metrics(command.opts()));
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\n_dhruv_completion() {\n _arguments '1:command:(${commands})' '*:option:(${options})'\n}\ncompdef _dhruv_completion 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="${commands}"
180
- local options="${options}"
181
- local choices="$commands $options"
182
- COMPREPLY=( $(compgen -W "$choices" -- "\${COMP_WORDS[COMP_CWORD]}") )
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 +1,7 @@
1
+ export interface ProjectContext {
2
+ type: string;
3
+ framework?: string;
4
+ diagnostic?: string;
5
+ }
6
+ export declare function detectProjectDetails(directory?: string): ProjectContext;
1
7
  export declare function detectProjectType(directory?: string): string;
@@ -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
- export function detectProjectType(directory = process.cwd()) {
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
- return 'unknown';
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
- return 'java';
35
- return 'unknown';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahul05ranjan/dhruv-cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "AI-powered CLI assistant for developers using Ollama",
5
5
  "keywords": [
6
6
  "ai",
@@ -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 buildPrompt(type: string, content: string): string {
8
+ function getLanguageForFile(target: string): { name: string; testFramework: string } {
9
+ const ext = path.extname(target).toLowerCase();
10
+ switch (ext) {
11
+ case '.py':
12
+ return { name: 'Python', testFramework: 'pytest or unittest' };
13
+ case '.go':
14
+ return { name: 'Go', testFramework: 'standard testing package' };
15
+ case '.rs':
16
+ return { name: 'Rust', testFramework: 'standard Rust test framework' };
17
+ case '.ts':
18
+ case '.tsx':
19
+ return { name: 'TypeScript', testFramework: 'Jest or Vitest' };
20
+ case '.java':
21
+ return { name: 'Java', testFramework: 'JUnit 5' };
22
+ default:
23
+ return { name: 'JavaScript', testFramework: 'Jest or Mocha' };
24
+ }
25
+ }
26
+
27
+ function buildPrompt(type: string, content: string, target: string): string {
28
+ const lang = getLanguageForFile(target);
9
29
  if (type === 'tests' || type === 'test') {
10
- return `Generate comprehensive unit tests for the following JavaScript code. Use Jest or Mocha syntax. Only return the test code without explanations:\n\n${content}`;
30
+ return `Generate comprehensive unit tests for the following ${lang.name} code. Use ${lang.testFramework} syntax. Only return the test code without explanations:\n\n${content}`;
11
31
  }
12
32
  if (type === 'documentation' || type === 'docs') {
13
- return `Generate JSDoc documentation for the following code:\n\n${content}`;
33
+ return `Generate ${lang.name === 'Python' ? 'docstrings' : 'JSDoc/documentation'} for the following code:\n\n${content}`;
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?/g, '')
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
  }),
@@ -13,8 +13,9 @@ export async function init() {
13
13
  modelChoices = models;
14
14
  }
15
15
  } catch {
16
- console.log(chalk.yellow('Warning: Could not fetch available models from Ollama.'));
17
- console.log(chalk.yellow('Using default model choices.'));
16
+ console.log(chalk.yellow('Warning: Could not connect to Ollama.'));
17
+ console.log(chalk.yellow('💡 Start Ollama with: ollama serve'));
18
+ console.log(chalk.yellow(`💡 Install default model with: ollama pull ${current.model}\n`));
18
19
  }
19
20
 
20
21
  try {
@@ -22,7 +23,7 @@ export async function init() {
22
23
  {
23
24
  type: 'list',
24
25
  name: 'model',
25
- message: 'Which Ollama model do you want to use?',
26
+ message: `Which Ollama model do you want to use? (default: ${current.model})`,
26
27
  choices: modelChoices,
27
28
  default: current.model,
28
29
  },
@@ -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
- process.exitCode = 130;
63
- if ((error as { isTtyError?: boolean })?.isTtyError) {
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 or failed.'));
68
+ console.log(chalk.red('Configuration cancelled.'));
67
69
  }
68
70
  }
69
71
  }
@@ -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
- printError('Failed to retrieve metrics');
97
- console.error(chalk.red((error as Error).message));
96
+ process.exitCode = 1;
97
+ if (loadConfig().responseFormat === 'json') {
98
+ process.stdout.write(`${JSON.stringify({
99
+ ok: false,
100
+ command: 'metrics',
101
+ error: (error as Error).message,
102
+ })}\n`);
103
+ } else {
104
+ printError('Failed to retrieve metrics');
105
+ console.error(chalk.red((error as Error).message));
106
+ }
98
107
  logger.error('Metrics command failed', error as Error);
99
108
  }
100
109
  }
@@ -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(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
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(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
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
 
@@ -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('\nTo start Ollama, run: ollama serve'));
81
- console.log(chalk.yellow('To install a model, run: ollama pull llama2'));
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
- return error instanceof Error ? error.message : String(error);
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
 
@@ -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') throw error;
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, command: Command) => review(fileOrDir, command.opts()));
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, command: Command) => securityCheck(fileOrDir, command.opts()));
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, command: Command) => generate(type, target, command.opts()));
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((command: Command) => health(command.opts()));
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((command: Command) => metrics(command.opts()));
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\n_dhruv_completion() {\n _arguments '1:command:(${commands})' '*:option:(${options})'\n}\ncompdef _dhruv_completion 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="${commands}"
196
- local options="${options}"
197
- local choices="$commands $options"
198
- COMPREPLY=( $(compgen -W "$choices" -- "\${COMP_WORDS[COMP_CWORD]}") )
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;