@rahul05ranjan/dhruv-cli 1.4.6 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.github/workflows/ci.yml +18 -238
  2. package/.github/workflows/contribution.yml +6 -141
  3. package/.github/workflows/dependabot-auto-merge.yml +1 -0
  4. package/.github/workflows/labeler.yml +1 -0
  5. package/.github/workflows/security.yml +3 -0
  6. package/CHANGELOG.md +9 -4
  7. package/README.md +145 -40
  8. package/__tests__/cli-contract.test.ts +104 -0
  9. package/__tests__/core.test.ts +121 -0
  10. package/__tests__/diagnostics.test.ts +155 -0
  11. package/__tests__/file-workflows.test.ts +195 -0
  12. package/__tests__/interactive.test.ts +119 -0
  13. package/__tests__/setup.ts +1 -0
  14. package/__tests__/workflows.test.ts +27 -4
  15. package/dist/commands/generate.d.ts +6 -1
  16. package/dist/commands/generate.js +18 -4
  17. package/dist/commands/health.d.ts +4 -1
  18. package/dist/commands/health.js +59 -16
  19. package/dist/commands/init.js +11 -2
  20. package/dist/commands/menu.js +125 -100
  21. package/dist/commands/metrics.d.ts +5 -1
  22. package/dist/commands/metrics.js +37 -8
  23. package/dist/commands/optimize.js +1 -1
  24. package/dist/commands/review.d.ts +4 -1
  25. package/dist/commands/review.js +48 -8
  26. package/dist/commands/security-check.d.ts +4 -1
  27. package/dist/commands/security-check.js +67 -6
  28. package/dist/commands/status.js +40 -2
  29. package/dist/config/config.d.ts +5 -1
  30. package/dist/config/config.js +24 -7
  31. package/dist/core/ai.d.ts +11 -0
  32. package/dist/core/ai.js +33 -9
  33. package/dist/core/command-catalog.d.ts +10 -0
  34. package/dist/core/command-catalog.js +27 -0
  35. package/dist/core/command-runner.js +100 -21
  36. package/dist/core/logger.js +1 -0
  37. package/dist/core/metrics.d.ts +28 -0
  38. package/dist/core/metrics.js +78 -0
  39. package/dist/index.js +46 -24
  40. package/dist/utils/projectType.d.ts +1 -1
  41. package/dist/utils/projectType.js +26 -7
  42. package/docs/api/assets/highlight.css +4 -4
  43. package/docs/api/index.html +161 -39
  44. package/docs/api/media/CONTRIBUTING.md +60 -0
  45. package/docs/api/media/SECURITY.md +8 -0
  46. package/docs/api/media/dhruv-cli-preview.svg +42 -0
  47. package/docs/api/media/publishing-fix.md +34 -0
  48. package/docs/dhruv-cli-preview.svg +42 -0
  49. package/docs/index.html +631 -533
  50. package/docs/publishing-fix.md +34 -0
  51. package/package.json +1 -1
  52. package/src/commands/generate.ts +23 -4
  53. package/src/commands/health.ts +62 -17
  54. package/src/commands/init.ts +11 -2
  55. package/src/commands/menu.ts +54 -30
  56. package/src/commands/metrics.ts +42 -7
  57. package/src/commands/optimize.ts +1 -1
  58. package/src/commands/review.ts +53 -8
  59. package/src/commands/security-check.ts +80 -6
  60. package/src/commands/status.ts +39 -3
  61. package/src/config/config.ts +26 -7
  62. package/src/core/ai.ts +36 -8
  63. package/src/core/command-catalog.ts +37 -0
  64. package/src/core/command-runner.ts +102 -22
  65. package/src/core/logger.ts +1 -0
  66. package/src/core/metrics.ts +103 -0
  67. package/src/index.ts +45 -24
  68. package/src/utils/projectType.ts +22 -7
  69. package/tsconfig.json +1 -1
  70. package/.github/workflows/auto-assign.yml +0 -14
  71. package/.github/workflows/build-publish.yml +0 -154
  72. package/.github/workflows/deploy.yml +0 -336
  73. package/.github/workflows/monitoring.yml +0 -270
  74. package/PUBLISHING_FIX.md +0 -92
  75. package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +0 -15
  76. package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +0 -15
@@ -8,114 +8,139 @@ import { optimize } from './optimize.js';
8
8
  import { securityCheck } from './security-check.js';
9
9
  import { generate } from './generate.js';
10
10
  import { init } from './init.js';
11
+ import { status } from './status.js';
12
+ import { health } from './health.js';
13
+ import { metrics } from './metrics.js';
11
14
  import { detectProjectType } from '../utils/projectType.js';
12
15
  import chalk from 'chalk';
16
+ import { commandCatalog } from '../core/command-catalog.js';
13
17
  const commands = [
14
- { name: 'Explain', value: 'explain' },
15
- { name: 'Suggest', value: 'suggest' },
16
- { name: 'Fix', value: 'fix' },
17
- { name: 'Review', value: 'review' },
18
- { name: 'Optimize', value: 'optimize' },
19
- { name: 'Security Check', value: 'security-check' },
20
- { name: 'Generate', value: 'generate' },
21
- { name: 'Init (Setup)', value: 'init' },
22
- { name: 'Project Type', value: 'project-type' },
23
- { name: 'Exit', value: 'exit' }
18
+ ...commandCatalog.map(({ menuLabel, name }) => ({ name: menuLabel, value: name })),
19
+ { name: 'Exit', value: 'exit' },
24
20
  ];
25
21
  export async function menu() {
26
- let running = true;
27
- while (running) {
28
- const { cmd } = await inquirer.prompt([
29
- {
30
- type: 'list',
31
- name: 'cmd',
32
- message: themed('What do you want to do?', 'primary'),
33
- choices: commands
34
- }
35
- ]);
36
- if (cmd === 'exit') {
37
- running = false;
38
- break;
39
- }
40
- try {
41
- switch (cmd) {
42
- case 'explain': {
43
- const { query } = await inquirer.prompt([
44
- { type: 'input', name: 'query', message: 'What would you like me to explain?' }
45
- ]);
46
- if (query)
47
- await explain(query);
48
- break;
49
- }
50
- case 'suggest': {
51
- const { query } = await inquirer.prompt([
52
- { type: 'input', name: 'query', message: 'What would you like suggestions for?' }
53
- ]);
54
- if (query)
55
- await suggest(query);
56
- break;
57
- }
58
- case 'fix': {
59
- const { query } = await inquirer.prompt([
60
- { type: 'input', name: 'query', message: 'Describe the issue you need help fixing:' }
61
- ]);
62
- if (query)
63
- await fix(query);
64
- break;
65
- }
66
- case 'review': {
67
- const { fileOrDir } = await inquirer.prompt([
68
- { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to review:' }
69
- ]);
70
- if (fileOrDir)
71
- await review(fileOrDir);
72
- break;
73
- }
74
- case 'optimize': {
75
- const { file } = await inquirer.prompt([
76
- { type: 'input', name: 'file', message: 'Enter file path to optimize:' }
77
- ]);
78
- if (file)
79
- await optimize(file);
80
- break;
81
- }
82
- case 'security-check': {
83
- const { fileOrDir } = await inquirer.prompt([
84
- { type: 'input', name: 'fileOrDir', message: 'Enter file or directory path to check (or press enter for current directory):', default: '.' }
85
- ]);
86
- await securityCheck(fileOrDir);
87
- break;
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' }],
88
41
  }
89
- case 'generate': {
90
- const answers = await inquirer.prompt([
91
- {
92
- type: 'list',
93
- name: 'type',
94
- message: 'What would you like to generate?',
95
- choices: ['tests', 'documentation', 'docs', 'component']
96
- },
97
- { type: 'input', name: 'target', message: 'Enter target file path:' }
98
- ]);
99
- if (answers.target)
100
- await generate(answers.type, answers.target);
101
- break;
102
- }
103
- case 'init': {
104
- await init();
105
- break;
106
- }
107
- case 'project-type': {
108
- const type = detectProjectType();
109
- console.log(chalk.blue(`Detected project type: ${type}`));
110
- break;
42
+ ]);
43
+ if (cmd === 'exit') {
44
+ break;
45
+ }
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'));
111
132
  }
112
- default:
113
- console.log(themed(`You selected: ${cmd}`, 'accent'));
114
133
  }
134
+ catch (error) {
135
+ console.error(chalk.red(`Error executing ${cmd}: ${error.message}`));
136
+ }
137
+ console.log(''); // Add spacing between commands
115
138
  }
116
- catch (error) {
117
- console.error(chalk.red(`Error executing ${cmd}: ${error.message}`));
118
- }
119
- console.log(''); // Add spacing between commands
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}`));
120
145
  }
121
146
  }
@@ -1 +1,5 @@
1
- export declare function metrics(): Promise<void>;
1
+ export interface MetricsOptions {
2
+ raw?: boolean;
3
+ reset?: boolean;
4
+ }
5
+ export declare function metrics(options?: MetricsOptions): Promise<void>;
@@ -1,12 +1,42 @@
1
1
  import chalk from 'chalk';
2
- import { printError, printInfo } from '../utils/ux.js';
2
+ import { printSuccess, printError, printInfo } from '../utils/ux.js';
3
3
  import { metricsCollector } from '../core/metrics.js';
4
4
  import { logger } from '../core/logger.js';
5
- export async function metrics() {
6
- console.log(chalk.blue.bold('📊 Dhruv CLI Metrics\n'));
5
+ import { loadConfig } from '../config/config.js';
6
+ export async function metrics(options = {}) {
7
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
+ }
8
18
  // Get metrics data
9
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`);
10
40
  if (metricsData.length === 0) {
11
41
  printInfo('No metrics data available yet. Metrics are collected during CLI usage.');
12
42
  return;
@@ -36,11 +66,10 @@ export async function metrics() {
36
66
  });
37
67
  }
38
68
  });
39
- // Display raw Prometheus metrics
40
- console.log(chalk.cyan('\n📋 Raw Prometheus Metrics:'));
41
- console.log(chalk.gray('─'.repeat(50)));
42
- const rawMetrics = await metricsCollector.getMetrics();
43
- console.log(rawMetrics);
69
+ if (options.raw) {
70
+ const rawMetrics = await metricsCollector.getMetrics();
71
+ process.stdout.write(rawMetrics);
72
+ }
44
73
  logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
45
74
  }
46
75
  catch (error) {
@@ -41,7 +41,7 @@ export async function optimize(file) {
41
41
  input: { file },
42
42
  header: `⚡ Optimization suggestions for ${type}: `,
43
43
  buildRequest: (input, model) => ({
44
- prompt: `Please analyze and provide optimization suggestions for this ${type}:\n\n${content}\n\nPlease provide:\n1. Specific optimization recommendations\n2. Performance improvements\n3. Best practices to implement\n4. Code examples of improvements\n5. Potential issues to fix\n\nFocus on actionable, practical improvements.`,
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
45
  systemMessage: getSystemMessage('optimize'),
46
46
  model,
47
47
  }),
@@ -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,9 +1,13 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { execFileSync } from 'child_process';
3
4
  import { runCommand } from '../core/command-runner.js';
4
5
  import { getSystemMessage } from '../core/prompts.js';
5
6
  import { printError } from '../utils/ux.js';
6
- /** Reads a file or the code files of a directory (up to 10), concatenated. */
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. */
7
11
  function readCode(fileOrDir) {
8
12
  // Read first, branch on the error: no separate existence check to race against.
9
13
  let content;
@@ -21,10 +25,24 @@ function readCode(fileOrDir) {
21
25
  return content;
22
26
  }
23
27
  function readDirectory(dir) {
24
- const files = fs
25
- .readdirSync(dir)
26
- .filter((f) => f.match(/\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/))
27
- .slice(0, 10);
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('/'));
42
+ }
43
+ }
44
+ }
45
+ collect(dir);
28
46
  if (files.length === 0) {
29
47
  printError(`No code files found in directory "${dir}".`);
30
48
  return undefined;
@@ -40,16 +58,38 @@ function readDirectory(dir) {
40
58
  }
41
59
  return code;
42
60
  }
43
- export async function review(fileOrDir) {
44
- const code = readCode(fileOrDir);
61
+ function readGitDiff(fileOrDir) {
62
+ const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
63
+ try {
64
+ const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
65
+ cwd: root,
66
+ encoding: 'utf8',
67
+ stdio: ['ignore', 'pipe', 'ignore'],
68
+ });
69
+ if (!diff.trim()) {
70
+ printError(`No uncommitted changes found in "${fileOrDir}".`);
71
+ return undefined;
72
+ }
73
+ return diff;
74
+ }
75
+ catch {
76
+ printError(`Could not read a git diff for "${fileOrDir}".`);
77
+ return undefined;
78
+ }
79
+ }
80
+ export async function review(fileOrDir, options = {}) {
81
+ const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
45
82
  if (code === undefined)
46
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';
47
87
  await runCommand({
48
88
  name: 'review',
49
89
  input: { fileOrDir },
50
90
  header: '🔍 Code Review: ',
51
91
  buildRequest: (input, model) => ({
52
- prompt: `Please review this code and provide feedback on code quality, best practices, potential issues, and suggestions for improvement. Here is the code to review:\n\nCODE_START\n${code}\nCODE_END\n\nPlease provide your review in a structured format with clear categories and actionable feedback.`,
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`,
53
93
  systemMessage: getSystemMessage('review'),
54
94
  model,
55
95
  }),
@@ -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>;
@@ -3,6 +3,45 @@ import path from 'path';
3
3
  import { runCommand } from '../core/command-runner.js';
4
4
  import { getSystemMessage } from '../core/prompts.js';
5
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
+ });
25
+ }
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
+ });
33
+ }
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
+ }
6
45
  /** Reads a file or the code files of a directory (up to 10), concatenated. */
7
46
  function readCode(fileOrDir) {
8
47
  // Read first, branch on the error: no separate existence check to race against.
@@ -21,10 +60,24 @@ function readCode(fileOrDir) {
21
60
  return content;
22
61
  }
23
62
  function readDirectory(dir) {
24
- const files = fs
25
- .readdirSync(dir)
26
- .filter((f) => f.match(/\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/))
27
- .slice(0, 10);
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);
74
+ }
75
+ else if (entry.isFile() && CODE_FILE.test(entry.name)) {
76
+ files.push(path.relative(dir, absolute).split(path.sep).join('/'));
77
+ }
78
+ }
79
+ }
80
+ collect(dir);
28
81
  if (files.length === 0) {
29
82
  printError(`No code files found in directory "${dir}".`);
30
83
  return undefined;
@@ -40,16 +93,24 @@ function readDirectory(dir) {
40
93
  }
41
94
  return code;
42
95
  }
43
- export async function securityCheck(fileOrDir = '.') {
96
+ export async function securityCheck(fileOrDir = '.', options = {}) {
44
97
  const code = readCode(fileOrDir);
45
98
  if (code === undefined)
46
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;
107
+ }
47
108
  await runCommand({
48
109
  name: 'security-check',
49
110
  input: { fileOrDir },
50
111
  header: '🛡️ Security Analysis: ',
51
112
  buildRequest: (input, model) => ({
52
- prompt: `Perform a security analysis on this code. Look for common security vulnerabilities, unsafe practices, potential injection attacks, and provide recommendations for improvement:\n\n${code}`,
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}`,
53
114
  systemMessage: getSystemMessage('security'),
54
115
  model,
55
116
  }),
@@ -1,10 +1,47 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadConfig } from '../config/config.js';
3
3
  import { printSuccess, printError, printInfo } from '../utils/ux.js';
4
- import { listModels } from '../core/ai.js';
4
+ import { getOllamaStatus, listModels } from '../core/ai.js';
5
5
  export async function status() {
6
- console.log(chalk.blue('🔍 Dhruv CLI Status Check\n'));
7
6
  const config = loadConfig();
7
+ if (config.responseFormat === 'json') {
8
+ try {
9
+ const models = await listModels();
10
+ const server = await getOllamaStatus();
11
+ const configuredModelAvailable = models.includes(config.model);
12
+ process.stdout.write(`${JSON.stringify({
13
+ ok: configuredModelAvailable,
14
+ command: 'status',
15
+ model: config.model,
16
+ responseFormat: config.responseFormat,
17
+ verbose: config.verbose,
18
+ theme: config.theme,
19
+ availableModels: models,
20
+ configuredModelAvailable,
21
+ endpoint: server.endpoint,
22
+ version: server.version ?? null,
23
+ ollama: 'connected',
24
+ nextSteps: configuredModelAvailable ? [] : [`ollama pull ${config.model}`],
25
+ })}\n`);
26
+ if (!configuredModelAvailable)
27
+ process.exitCode = 1;
28
+ }
29
+ catch (error) {
30
+ process.exitCode = 1;
31
+ process.stdout.write(`${JSON.stringify({
32
+ ok: false,
33
+ command: 'status',
34
+ model: config.model,
35
+ ollama: 'unavailable',
36
+ error: error.message,
37
+ })}\n`);
38
+ }
39
+ return;
40
+ }
41
+ console.log(chalk.blue('🔍 Dhruv CLI Status Check\n'));
42
+ const server = await getOllamaStatus();
43
+ printInfo(`Ollama endpoint: ${server.endpoint}`);
44
+ printInfo(`Ollama version: ${server.version ?? 'unavailable'}\n`);
8
45
  printInfo(`Current configuration:`);
9
46
  console.log(` Model: ${config.model}`);
10
47
  console.log(` Response Format: ${config.responseFormat}`);
@@ -30,6 +67,7 @@ export async function status() {
30
67
  printSuccess(`✓ Configured model '${config.model}' is available`);
31
68
  }
32
69
  else {
70
+ process.exitCode = 1;
33
71
  printError(`✗ Configured model '${config.model}' is not available`);
34
72
  if (models.length > 0) {
35
73
  console.log(chalk.yellow(`Available models: ${models.join(', ')}`));
@@ -1,8 +1,12 @@
1
+ export type ConfigScope = 'local' | 'global';
1
2
  export interface DhruvConfig {
2
3
  model: string;
3
4
  verbose: boolean;
4
5
  responseFormat: 'text' | 'json' | 'markdown';
6
+ timeoutMs: number;
5
7
  theme?: 'default' | 'dark' | 'light' | 'mono';
6
8
  }
7
9
  export declare function loadConfig(): DhruvConfig;
8
- export declare function saveConfig(config: Partial<DhruvConfig>): void;
10
+ export declare function saveConfig(config: Partial<DhruvConfig>, options?: {
11
+ scope?: ConfigScope;
12
+ }): void;