@rahul05ranjan/dhruv-cli 1.4.6 → 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/.github/workflows/ci.yml +18 -238
- package/.github/workflows/contribution.yml +6 -141
- package/.github/workflows/dependabot-auto-merge.yml +1 -0
- package/.github/workflows/labeler.yml +1 -0
- package/.github/workflows/security.yml +3 -0
- package/CHANGELOG.md +16 -4
- package/README.md +145 -40
- package/__tests__/cli-contract.test.ts +170 -0
- package/__tests__/core.test.ts +193 -1
- package/__tests__/diagnostics.test.ts +179 -0
- package/__tests__/file-workflows.test.ts +234 -0
- package/__tests__/interactive.test.ts +134 -0
- package/__tests__/setup.ts +1 -0
- package/__tests__/workflows.test.ts +27 -4
- package/dist/commands/generate.d.ts +6 -1
- package/dist/commands/generate.js +44 -11
- package/dist/commands/health.d.ts +4 -1
- package/dist/commands/health.js +59 -16
- package/dist/commands/init.js +18 -7
- package/dist/commands/menu.js +125 -100
- package/dist/commands/metrics.d.ts +5 -1
- package/dist/commands/metrics.js +49 -10
- package/dist/commands/optimize.js +1 -1
- package/dist/commands/review.d.ts +4 -1
- package/dist/commands/review.js +66 -9
- package/dist/commands/security-check.d.ts +4 -1
- package/dist/commands/security-check.js +100 -6
- package/dist/commands/status.js +44 -4
- package/dist/config/config.d.ts +5 -1
- package/dist/config/config.js +24 -7
- package/dist/core/ai.d.ts +11 -0
- package/dist/core/ai.js +33 -9
- package/dist/core/command-catalog.d.ts +10 -0
- package/dist/core/command-catalog.js +27 -0
- package/dist/core/command-runner.js +106 -21
- package/dist/core/logger.js +1 -0
- package/dist/core/metrics.d.ts +28 -0
- package/dist/core/metrics.js +79 -0
- package/dist/index.js +98 -24
- package/dist/utils/projectType.d.ts +7 -1
- package/dist/utils/projectType.js +91 -13
- package/docs/api/assets/highlight.css +4 -4
- package/docs/api/index.html +161 -39
- package/docs/api/media/CONTRIBUTING.md +60 -0
- package/docs/api/media/SECURITY.md +8 -0
- package/docs/api/media/dhruv-cli-preview.svg +42 -0
- package/docs/api/media/publishing-fix.md +34 -0
- package/docs/dhruv-cli-preview.svg +42 -0
- package/docs/index.html +631 -533
- package/docs/publishing-fix.md +34 -0
- package/package.json +1 -1
- package/src/commands/generate.ts +50 -11
- package/src/commands/health.ts +62 -17
- package/src/commands/init.ts +18 -7
- package/src/commands/menu.ts +54 -30
- package/src/commands/metrics.ts +53 -9
- package/src/commands/optimize.ts +1 -1
- package/src/commands/review.ts +72 -9
- package/src/commands/security-check.ts +111 -6
- package/src/commands/status.ts +43 -5
- package/src/config/config.ts +26 -7
- package/src/core/ai.ts +36 -8
- package/src/core/command-catalog.ts +37 -0
- package/src/core/command-runner.ts +108 -22
- package/src/core/logger.ts +1 -0
- package/src/core/metrics.ts +105 -0
- package/src/index.ts +97 -24
- package/src/utils/projectType.ts +85 -9
- package/tsconfig.json +1 -1
- package/.github/workflows/auto-assign.yml +0 -14
- package/.github/workflows/build-publish.yml +0 -154
- package/.github/workflows/deploy.yml +0 -336
- package/.github/workflows/monitoring.yml +0 -270
- package/PUBLISHING_FIX.md +0 -92
- package/logs/.8a99b6cf655346317fdbf29f4fffcf91131432f3-audit.json +0 -15
- package/logs/.eee104bf8fff5ecd38a6a2842df260de6470a7c3-audit.json +0 -15
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# npm publishing issue resolution
|
|
2
|
+
|
|
3
|
+
## Issue
|
|
4
|
+
|
|
5
|
+
GitHub Actions was failing with:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/@rahul05ranjan%2fdhruv-cli - You cannot publish over the previously published versions: 0.0.0-development.
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Resolution
|
|
12
|
+
|
|
13
|
+
- Updated the package version from `0.0.0-development` to `1.4.0`.
|
|
14
|
+
- Added semantic-release configuration and plugins.
|
|
15
|
+
- Removed direct npm publishing from the CI workflow.
|
|
16
|
+
- Added a dedicated publishing workflow with version-conflict checks.
|
|
17
|
+
- Enabled trusted publishing with npm provenance.
|
|
18
|
+
|
|
19
|
+
## Release conventions
|
|
20
|
+
|
|
21
|
+
Use conventional commit messages so semantic-release can determine the next version:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Patch
|
|
25
|
+
git commit -m "fix: resolve a connection issue"
|
|
26
|
+
|
|
27
|
+
# Minor
|
|
28
|
+
git commit -m "feat: add a new command"
|
|
29
|
+
|
|
30
|
+
# Major
|
|
31
|
+
git commit -m "feat!: change the command output contract"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The authoritative release configuration lives in [`.releaserc.json`](../.releaserc.json) and the workflows live in [`.github/workflows/`](../.github/workflows/).
|
package/package.json
CHANGED
package/src/commands/generate.ts
CHANGED
|
@@ -1,29 +1,57 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { runCommand } from '../core/command-runner.js';
|
|
3
4
|
import { getSystemMessage } from '../core/prompts.js';
|
|
4
|
-
import { printError, printSuccess } from '../utils/ux.js';
|
|
5
|
+
import { printError, printSuccess, printInfo } from '../utils/ux.js';
|
|
6
|
+
import { loadConfig } from '../config/config.js';
|
|
5
7
|
|
|
6
|
-
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);
|
|
7
29
|
if (type === 'tests' || type === 'test') {
|
|
8
|
-
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}`;
|
|
9
31
|
}
|
|
10
32
|
if (type === 'documentation' || type === 'docs') {
|
|
11
|
-
return `Generate JSDoc
|
|
33
|
+
return `Generate ${lang.name === 'Python' ? 'docstrings' : 'JSDoc/documentation'} for the following code:\n\n${content}`;
|
|
12
34
|
}
|
|
13
35
|
return `Generate ${type} for this code:\n\n${content}`;
|
|
14
36
|
}
|
|
15
37
|
|
|
16
38
|
/** Extracts test code from the response: a fenced block if present, else the raw response. */
|
|
17
39
|
function extractTestCode(response: string): string {
|
|
18
|
-
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);
|
|
19
41
|
if (fenced?.[1]) return fenced[1].trim();
|
|
20
42
|
return response
|
|
21
|
-
.replace(/^.*?(?=const|describe|test|it\s*\()/s, '')
|
|
22
|
-
.replace(/```[a-z]*\n?/
|
|
43
|
+
.replace(/^.*?(?=const|describe|test|it\s*\(|def test_|func Test|#\[test\])/s, '')
|
|
44
|
+
.replace(/```[a-z]*\n?/gi, '')
|
|
23
45
|
.trim();
|
|
24
46
|
}
|
|
25
47
|
|
|
26
|
-
export
|
|
48
|
+
export interface GenerateOptions {
|
|
49
|
+
apply?: boolean;
|
|
50
|
+
output?: string;
|
|
51
|
+
overwrite?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function generate(type: string, target: string, options: GenerateOptions = {}) {
|
|
27
55
|
if (!fs.existsSync(target)) {
|
|
28
56
|
printError(`Target file "${target}" does not exist.`);
|
|
29
57
|
return;
|
|
@@ -36,7 +64,7 @@ export async function generate(type: string, target: string) {
|
|
|
36
64
|
input: { type, target },
|
|
37
65
|
header: `🔨 Generating ${type}: `,
|
|
38
66
|
buildRequest: (input, model) => ({
|
|
39
|
-
prompt: buildPrompt(input.type, content),
|
|
67
|
+
prompt: buildPrompt(input.type, content, input.target),
|
|
40
68
|
systemMessage: getSystemMessage('generate'),
|
|
41
69
|
model,
|
|
42
70
|
}),
|
|
@@ -47,9 +75,20 @@ export async function generate(type: string, target: string) {
|
|
|
47
75
|
printError('No valid test code generated.');
|
|
48
76
|
return;
|
|
49
77
|
}
|
|
50
|
-
const
|
|
78
|
+
const extension = path.extname(target) || '.js';
|
|
79
|
+
const testFile = options.output ?? target.replace(/\.[^.]+$/, `.test${extension}`);
|
|
80
|
+
if (!options.apply && !options.output) {
|
|
81
|
+
if (loadConfig().responseFormat !== 'json') {
|
|
82
|
+
printInfo(`Preview only. Use --apply to write ${testFile}, or --output <path> to choose a destination.`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (fs.existsSync(testFile) && !options.overwrite) {
|
|
87
|
+
printError(`Test file "${testFile}" already exists. Use --overwrite to replace it.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
51
90
|
fs.writeFileSync(testFile, codeToSave);
|
|
52
|
-
printSuccess(`Test file saved: ${testFile}`);
|
|
91
|
+
if (loadConfig().responseFormat !== 'json') printSuccess(`Test file saved: ${testFile}`);
|
|
53
92
|
},
|
|
54
93
|
footer: `🔍 Want a review? Try: dhruv review ${target}`,
|
|
55
94
|
});
|
package/src/commands/health.ts
CHANGED
|
@@ -28,20 +28,26 @@ interface SystemInfo {
|
|
|
28
28
|
cpuCount: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export
|
|
32
|
-
|
|
31
|
+
export interface HealthOptions {
|
|
32
|
+
details?: boolean;
|
|
33
|
+
}
|
|
33
34
|
|
|
35
|
+
export async function health(options: HealthOptions = {}): Promise<void> {
|
|
36
|
+
const jsonOutput = loadConfig().responseFormat === 'json';
|
|
34
37
|
const results: HealthCheckResult[] = [];
|
|
35
38
|
const startTime = Date.now();
|
|
36
39
|
|
|
37
40
|
try {
|
|
38
41
|
// System Information
|
|
39
42
|
const systemInfo = getSystemInfo();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
if (!jsonOutput) {
|
|
44
|
+
console.log(chalk.blue.bold('🔍 Dhruv CLI Health Check\n'));
|
|
45
|
+
console.log(chalk.cyan('📊 System Information:'));
|
|
46
|
+
Object.entries(systemInfo).forEach(([key, value]) => {
|
|
47
|
+
console.log(` ${key}: ${chalk.yellow(value)}`);
|
|
48
|
+
});
|
|
49
|
+
console.log();
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
// Configuration Check
|
|
47
53
|
results.push(...await checkConfiguration());
|
|
@@ -64,15 +70,37 @@ export async function health(): Promise<void> {
|
|
|
64
70
|
// Plugin System Check
|
|
65
71
|
results.push(...await checkPlugins());
|
|
66
72
|
|
|
67
|
-
// Display Results
|
|
68
|
-
displayResults(results);
|
|
69
|
-
|
|
70
73
|
const duration = Date.now() - startTime;
|
|
74
|
+
const summary = summarizeResults(results);
|
|
75
|
+
if (jsonOutput) {
|
|
76
|
+
process.exitCode = summary.fail > 0 ? 1 : 0;
|
|
77
|
+
process.stdout.write(`${JSON.stringify({
|
|
78
|
+
ok: summary.fail === 0,
|
|
79
|
+
command: 'health',
|
|
80
|
+
system: systemInfo,
|
|
81
|
+
results,
|
|
82
|
+
summary,
|
|
83
|
+
durationMs: duration,
|
|
84
|
+
})}\n`);
|
|
85
|
+
} else {
|
|
86
|
+
if (options.details) displayResults(results);
|
|
87
|
+
else displayConciseResults(results);
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
logger.info('Health check completed', { duration, results: results.length });
|
|
72
91
|
|
|
73
92
|
} catch (error) {
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
if (jsonOutput) {
|
|
95
|
+
process.stdout.write(`${JSON.stringify({
|
|
96
|
+
ok: false,
|
|
97
|
+
command: 'health',
|
|
98
|
+
error: (error as Error).message,
|
|
99
|
+
})}\n`);
|
|
100
|
+
} else {
|
|
101
|
+
printError('Health check failed');
|
|
102
|
+
console.error(chalk.red((error as Error).message));
|
|
103
|
+
}
|
|
76
104
|
logger.error('Health check failed', error as Error);
|
|
77
105
|
}
|
|
78
106
|
}
|
|
@@ -421,11 +449,7 @@ function displayResults(results: HealthCheckResult[]): void {
|
|
|
421
449
|
});
|
|
422
450
|
|
|
423
451
|
// Summary
|
|
424
|
-
const summary =
|
|
425
|
-
pass: results.filter(r => r.status === 'pass').length,
|
|
426
|
-
warn: results.filter(r => r.status === 'warn').length,
|
|
427
|
-
fail: results.filter(r => r.status === 'fail').length
|
|
428
|
-
};
|
|
452
|
+
const summary = summarizeResults(results);
|
|
429
453
|
|
|
430
454
|
console.log(chalk.blue.bold('\n📊 Summary:'));
|
|
431
455
|
console.log(` ✅ Passed: ${chalk.green(summary.pass)}`);
|
|
@@ -438,3 +462,24 @@ function displayResults(results: HealthCheckResult[]): void {
|
|
|
438
462
|
|
|
439
463
|
console.log(`\n${statusIcon} ${statusColor('Overall Status: ' + overallStatus.toUpperCase())}`);
|
|
440
464
|
}
|
|
465
|
+
|
|
466
|
+
function displayConciseResults(results: HealthCheckResult[]): void {
|
|
467
|
+
const summary = summarizeResults(results);
|
|
468
|
+
const overallStatus = summary.fail > 0 ? 'fail' : summary.warn > 0 ? 'warn' : 'pass';
|
|
469
|
+
const icon = overallStatus === 'pass' ? '✅' : overallStatus === 'warn' ? '⚠️' : '❌';
|
|
470
|
+
|
|
471
|
+
console.log(chalk.blue.bold(`\n${icon} Health: ${overallStatus.toUpperCase()}`));
|
|
472
|
+
console.log(` ${chalk.green(`${summary.pass} passed`)}, ${chalk.yellow(`${summary.warn} warnings`)}, ${chalk.red(`${summary.fail} failed`)}`);
|
|
473
|
+
results
|
|
474
|
+
.filter((result) => result.status !== 'pass')
|
|
475
|
+
.forEach((result) => console.log(` ${result.category}: ${result.message}${result.recommendation ? ` — ${result.recommendation}` : ''}`));
|
|
476
|
+
console.log(chalk.dim('Run `dhruv health --details` for full diagnostics.'));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function summarizeResults(results: HealthCheckResult[]) {
|
|
480
|
+
return {
|
|
481
|
+
pass: results.filter(r => r.status === 'pass').length,
|
|
482
|
+
warn: results.filter(r => r.status === 'warn').length,
|
|
483
|
+
fail: results.filter(r => r.status === 'fail').length,
|
|
484
|
+
};
|
|
485
|
+
}
|
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,10 +23,17 @@ 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
|
},
|
|
30
|
+
{
|
|
31
|
+
type: 'list',
|
|
32
|
+
name: 'scope',
|
|
33
|
+
message: 'Where should Dhruv save these settings?',
|
|
34
|
+
choices: ['local', 'global'],
|
|
35
|
+
default: 'local',
|
|
36
|
+
},
|
|
29
37
|
{
|
|
30
38
|
type: 'list',
|
|
31
39
|
name: 'responseFormat',
|
|
@@ -48,13 +56,16 @@ export async function init() {
|
|
|
48
56
|
},
|
|
49
57
|
]);
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const { scope, ...settings } = answers;
|
|
60
|
+
saveConfig(settings, { scope });
|
|
61
|
+
console.log(chalk.green(`Configuration saved ${scope === 'global' ? 'for your user account' : 'in this project'}!`));
|
|
53
62
|
} catch (error) {
|
|
54
|
-
|
|
63
|
+
const isTty = Boolean((error as { isTtyError?: boolean })?.isTtyError);
|
|
64
|
+
process.exitCode = isTty ? 1 : 130;
|
|
65
|
+
if (isTty) {
|
|
55
66
|
console.log(chalk.red('This command requires an interactive terminal.'));
|
|
56
67
|
} else {
|
|
57
|
-
console.log(chalk.red('Configuration cancelled
|
|
68
|
+
console.log(chalk.red('Configuration cancelled.'));
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
}
|
package/src/commands/menu.ts
CHANGED
|
@@ -8,41 +8,47 @@ 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
|
|
|
14
18
|
const commands = [
|
|
15
|
-
{ name:
|
|
16
|
-
{ name: '
|
|
17
|
-
{ name: 'Fix', value: 'fix' },
|
|
18
|
-
{ name: 'Review', value: 'review' },
|
|
19
|
-
{ name: 'Optimize', value: 'optimize' },
|
|
20
|
-
{ name: 'Security Check', value: 'security-check' },
|
|
21
|
-
{ name: 'Generate', value: 'generate' },
|
|
22
|
-
{ name: 'Init (Setup)', value: 'init' },
|
|
23
|
-
{ name: 'Project Type', value: 'project-type' },
|
|
24
|
-
{ name: 'Exit', value: 'exit' }
|
|
19
|
+
...commandCatalog.map(({ menuLabel, name }) => ({ name: menuLabel, value: name })),
|
|
20
|
+
{ name: 'Exit', value: 'exit' },
|
|
25
21
|
];
|
|
26
22
|
|
|
27
23
|
export async function menu() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
try {
|
|
25
|
+
while (true) {
|
|
26
|
+
const { filter = '' } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'input',
|
|
29
|
+
name: 'filter',
|
|
30
|
+
message: 'Filter commands (press enter to show all):',
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
const normalizedFilter = String(filter).trim().toLowerCase();
|
|
34
|
+
const filteredCommands = normalizedFilter
|
|
35
|
+
? commands.filter((command) => command.name.toLowerCase().includes(normalizedFilter) || command.value.includes(normalizedFilter))
|
|
36
|
+
: commands;
|
|
37
|
+
const { cmd } = await inquirer.prompt([
|
|
38
|
+
{
|
|
39
|
+
type: 'list',
|
|
40
|
+
name: 'cmd',
|
|
41
|
+
message: themed('What do you want to do?', 'primary'),
|
|
42
|
+
choices: filteredCommands.length > 0 ? filteredCommands : [{ name: 'No matching commands — Exit', value: 'exit' }],
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
46
|
+
if (cmd === 'exit') {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
try {
|
|
51
|
+
switch (cmd) {
|
|
46
52
|
case 'explain': {
|
|
47
53
|
const { query } = await inquirer.prompt([
|
|
48
54
|
{ type: 'input', name: 'query', message: 'What would you like me to explain?' }
|
|
@@ -107,13 +113,31 @@ export async function menu() {
|
|
|
107
113
|
console.log(chalk.blue(`Detected project type: ${type}`));
|
|
108
114
|
break;
|
|
109
115
|
}
|
|
116
|
+
case 'status':
|
|
117
|
+
await status();
|
|
118
|
+
break;
|
|
119
|
+
case 'health':
|
|
120
|
+
await health();
|
|
121
|
+
break;
|
|
122
|
+
case 'metrics':
|
|
123
|
+
await metrics();
|
|
124
|
+
break;
|
|
125
|
+
case 'completion':
|
|
126
|
+
console.log(themed('Run `dhruv completion <bash|zsh|fish>` to install shell completion.', 'accent'));
|
|
127
|
+
break;
|
|
110
128
|
default:
|
|
111
129
|
console.log(themed(`You selected: ${cmd}`, 'accent'));
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(chalk.red(`Error executing ${cmd}: ${(error as Error).message}`));
|
|
112
133
|
}
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(chalk.red(`Error executing ${cmd}: ${(error as Error).message}`));
|
|
115
|
-
}
|
|
116
134
|
|
|
117
|
-
|
|
135
|
+
console.log(''); // Add spacing between commands
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
const cancelled = /cancel|force closed|exitprompt/i.test(message);
|
|
140
|
+
process.exitCode = cancelled ? 130 : 1;
|
|
141
|
+
console.error(chalk.red(cancelled ? 'Interactive menu cancelled.' : `Interactive menu failed: ${message}`));
|
|
118
142
|
}
|
|
119
143
|
}
|
package/src/commands/metrics.ts
CHANGED
|
@@ -3,13 +3,49 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { printSuccess, printError, printInfo } from '../utils/ux.js';
|
|
4
4
|
import { metricsCollector } from '../core/metrics.js';
|
|
5
5
|
import { logger } from '../core/logger.js';
|
|
6
|
+
import { loadConfig } from '../config/config.js';
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
-
|
|
8
|
+
export interface MetricsOptions {
|
|
9
|
+
raw?: boolean;
|
|
10
|
+
reset?: boolean;
|
|
11
|
+
}
|
|
9
12
|
|
|
13
|
+
export async function metrics(options: MetricsOptions = {}): Promise<void> {
|
|
10
14
|
try {
|
|
15
|
+
if (options.reset) {
|
|
16
|
+
metricsCollector.resetPersistent();
|
|
17
|
+
if (loadConfig().responseFormat === 'json') {
|
|
18
|
+
process.stdout.write(`${JSON.stringify({ ok: true, command: 'metrics', reset: true, summary: metricsCollector.getSummary() })}\n`);
|
|
19
|
+
} else {
|
|
20
|
+
printSuccess('Local metrics reset.');
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
// Get metrics data
|
|
12
26
|
const metricsData = await metricsCollector.getMetricsJSON();
|
|
27
|
+
const summary = metricsCollector.getSummary();
|
|
28
|
+
|
|
29
|
+
if (loadConfig().responseFormat === 'json') {
|
|
30
|
+
process.stdout.write(`${JSON.stringify({
|
|
31
|
+
ok: true,
|
|
32
|
+
command: 'metrics',
|
|
33
|
+
summary,
|
|
34
|
+
metrics: metricsData,
|
|
35
|
+
})}\n`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.blue.bold('📊 Dhruv CLI Metrics\n'));
|
|
40
|
+
console.log(chalk.cyan('📌 Local summary:'));
|
|
41
|
+
console.log(` Sessions: ${chalk.green(summary.sessions)}`);
|
|
42
|
+
Object.entries(summary.commands).forEach(([command, data]) => {
|
|
43
|
+
console.log(` ${chalk.yellow(command)}: ${data.runs} runs, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
|
|
44
|
+
});
|
|
45
|
+
Object.entries(summary.models).forEach(([model, data]) => {
|
|
46
|
+
console.log(` ${chalk.yellow(model)}: ${data.requests} requests, ${data.successes} succeeded, ${data.failures} failed, ${data.durationMs}ms`);
|
|
47
|
+
});
|
|
48
|
+
console.log(` Cache: ${chalk.green(summary.cache.hits)} hits, ${chalk.yellow(summary.cache.misses)} misses`);
|
|
13
49
|
|
|
14
50
|
if (metricsData.length === 0) {
|
|
15
51
|
printInfo('No metrics data available yet. Metrics are collected during CLI usage.');
|
|
@@ -49,17 +85,25 @@ export async function metrics(): Promise<void> {
|
|
|
49
85
|
}
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
console.log(rawMetrics);
|
|
88
|
+
if (options.raw) {
|
|
89
|
+
const rawMetrics = await metricsCollector.getMetrics();
|
|
90
|
+
process.stdout.write(rawMetrics);
|
|
91
|
+
}
|
|
57
92
|
|
|
58
93
|
logger.info('Metrics displayed successfully', { metricsCount: metricsData.length });
|
|
59
94
|
|
|
60
95
|
} catch (error) {
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
63
107
|
logger.error('Metrics command failed', error as Error);
|
|
64
108
|
}
|
|
65
109
|
}
|
package/src/commands/optimize.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function optimize(file: string) {
|
|
|
39
39
|
input: { file },
|
|
40
40
|
header: `⚡ Optimization suggestions for ${type}: `,
|
|
41
41
|
buildRequest: (input, model) => ({
|
|
42
|
-
prompt: `Please analyze and provide optimization suggestions for this ${type}
|
|
42
|
+
prompt: `Please analyze and provide optimization suggestions for this ${type}. For every recommendation, explain the expected impact, how to measure it, and the trade-offs or risks before applying it.\n\n${content}\n\nPlease provide:\n1. Specific optimization recommendations\n2. Performance improvements\n3. Best practices to implement\n4. Code examples of improvements\n5. Potential issues to fix\n\nFocus on actionable, practical improvements.`,
|
|
43
43
|
systemMessage: getSystemMessage('optimize'),
|
|
44
44
|
model,
|
|
45
45
|
}),
|
package/src/commands/review.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
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
|
-
import { printError } from '../utils/ux.js';
|
|
6
|
+
import { printError, printInfo } from '../utils/ux.js';
|
|
7
|
+
import { detectProjectType } from '../utils/projectType.js';
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
const CODE_FILE = /\.(js|ts|jsx|tsx|py|java|cpp|c|go|rs|rb|php)$/;
|
|
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
|
+
]);
|
|
25
|
+
|
|
26
|
+
/** Reads a file or up to 10 code files from a directory tree. */
|
|
8
27
|
function readCode(fileOrDir: string): string | undefined {
|
|
9
28
|
// Read first, branch on the error: no separate existence check to race against.
|
|
10
29
|
let content: string;
|
|
@@ -22,16 +41,34 @@ function readCode(fileOrDir: string): string | undefined {
|
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
function readDirectory(dir: string): string | undefined {
|
|
25
|
-
const files =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
44
|
+
const files: string[] = [];
|
|
45
|
+
|
|
46
|
+
function collect(current: string): void {
|
|
47
|
+
if (files.length >= 10) return;
|
|
48
|
+
|
|
49
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
50
|
+
if (files.length >= 10) return;
|
|
51
|
+
const absolute = path.join(current, entry.name);
|
|
52
|
+
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
|
|
55
|
+
} else if (entry.isFile() && CODE_FILE.test(entry.name)) {
|
|
56
|
+
files.push(path.relative(dir, absolute).split(path.sep).join('/'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
collect(dir);
|
|
29
62
|
|
|
30
63
|
if (files.length === 0) {
|
|
31
64
|
printError(`No code files found in directory "${dir}".`);
|
|
32
65
|
return undefined;
|
|
33
66
|
}
|
|
34
67
|
|
|
68
|
+
if (files.length >= 10) {
|
|
69
|
+
printInfo('Note: Directory review is capped at the first 10 source files.');
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
let code = '';
|
|
36
73
|
for (const f of files) {
|
|
37
74
|
try {
|
|
@@ -43,16 +80,42 @@ function readDirectory(dir: string): string | undefined {
|
|
|
43
80
|
return code;
|
|
44
81
|
}
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
const
|
|
83
|
+
function readGitDiff(fileOrDir: string): string | undefined {
|
|
84
|
+
const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
85
|
+
try {
|
|
86
|
+
const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
|
|
87
|
+
cwd: root,
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
});
|
|
91
|
+
if (!diff.trim()) {
|
|
92
|
+
printError(`No uncommitted changes found in "${fileOrDir}".`);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return diff;
|
|
96
|
+
} catch {
|
|
97
|
+
printError(`Could not read a git diff for "${fileOrDir}".`);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ReviewOptions {
|
|
103
|
+
diff?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function review(fileOrDir: string, options: ReviewOptions = {}) {
|
|
107
|
+
const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
|
|
48
108
|
if (code === undefined) return;
|
|
109
|
+
const projectRoot = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
110
|
+
const projectType = detectProjectType(projectRoot);
|
|
111
|
+
const scope = options.diff ? 'the current uncommitted git diff' : 'the supplied source files';
|
|
49
112
|
|
|
50
113
|
await runCommand({
|
|
51
114
|
name: 'review',
|
|
52
115
|
input: { fileOrDir },
|
|
53
116
|
header: '🔍 Code Review: ',
|
|
54
117
|
buildRequest: (input, model) => ({
|
|
55
|
-
prompt: `Please review
|
|
118
|
+
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`,
|
|
56
119
|
systemMessage: getSystemMessage('review'),
|
|
57
120
|
model,
|
|
58
121
|
}),
|