@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.
- 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 +9 -4
- package/README.md +145 -40
- package/__tests__/cli-contract.test.ts +104 -0
- package/__tests__/core.test.ts +121 -0
- package/__tests__/diagnostics.test.ts +155 -0
- package/__tests__/file-workflows.test.ts +195 -0
- package/__tests__/interactive.test.ts +119 -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 +18 -4
- package/dist/commands/health.d.ts +4 -1
- package/dist/commands/health.js +59 -16
- package/dist/commands/init.js +11 -2
- package/dist/commands/menu.js +125 -100
- package/dist/commands/metrics.d.ts +5 -1
- package/dist/commands/metrics.js +37 -8
- package/dist/commands/optimize.js +1 -1
- package/dist/commands/review.d.ts +4 -1
- package/dist/commands/review.js +48 -8
- package/dist/commands/security-check.d.ts +4 -1
- package/dist/commands/security-check.js +67 -6
- package/dist/commands/status.js +40 -2
- 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 +100 -21
- package/dist/core/logger.js +1 -0
- package/dist/core/metrics.d.ts +28 -0
- package/dist/core/metrics.js +78 -0
- package/dist/index.js +46 -24
- package/dist/utils/projectType.d.ts +1 -1
- package/dist/utils/projectType.js +26 -7
- 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 +23 -4
- package/src/commands/health.ts +62 -17
- package/src/commands/init.ts +11 -2
- package/src/commands/menu.ts +54 -30
- package/src/commands/metrics.ts +42 -7
- package/src/commands/optimize.ts +1 -1
- package/src/commands/review.ts +53 -8
- package/src/commands/security-check.ts +80 -6
- package/src/commands/status.ts +39 -3
- 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 +102 -22
- package/src/core/logger.ts +1 -0
- package/src/core/metrics.ts +103 -0
- package/src/index.ts +45 -24
- package/src/utils/projectType.ts +22 -7
- 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,7 +1,9 @@
|
|
|
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
8
|
function buildPrompt(type: string, content: string): string {
|
|
7
9
|
if (type === 'tests' || type === 'test') {
|
|
@@ -23,7 +25,13 @@ function extractTestCode(response: string): string {
|
|
|
23
25
|
.trim();
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
export
|
|
28
|
+
export interface GenerateOptions {
|
|
29
|
+
apply?: boolean;
|
|
30
|
+
output?: string;
|
|
31
|
+
overwrite?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function generate(type: string, target: string, options: GenerateOptions = {}) {
|
|
27
35
|
if (!fs.existsSync(target)) {
|
|
28
36
|
printError(`Target file "${target}" does not exist.`);
|
|
29
37
|
return;
|
|
@@ -47,9 +55,20 @@ export async function generate(type: string, target: string) {
|
|
|
47
55
|
printError('No valid test code generated.');
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
50
|
-
const
|
|
58
|
+
const extension = path.extname(target) || '.js';
|
|
59
|
+
const testFile = options.output ?? target.replace(/\.[^.]+$/, `.test${extension}`);
|
|
60
|
+
if (!options.apply && !options.output) {
|
|
61
|
+
if (loadConfig().responseFormat !== 'json') {
|
|
62
|
+
printInfo(`Preview only. Use --apply to write ${testFile}, or --output <path> to choose a destination.`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (fs.existsSync(testFile) && !options.overwrite) {
|
|
67
|
+
printError(`Test file "${testFile}" already exists. Use --overwrite to replace it.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
51
70
|
fs.writeFileSync(testFile, codeToSave);
|
|
52
|
-
printSuccess(`Test file saved: ${testFile}`);
|
|
71
|
+
if (loadConfig().responseFormat !== 'json') printSuccess(`Test file saved: ${testFile}`);
|
|
53
72
|
},
|
|
54
73
|
footer: `🔍 Want a review? Try: dhruv review ${target}`,
|
|
55
74
|
});
|
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
|
@@ -26,6 +26,13 @@ export async function init() {
|
|
|
26
26
|
choices: modelChoices,
|
|
27
27
|
default: current.model,
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
type: 'list',
|
|
31
|
+
name: 'scope',
|
|
32
|
+
message: 'Where should Dhruv save these settings?',
|
|
33
|
+
choices: ['local', 'global'],
|
|
34
|
+
default: 'local',
|
|
35
|
+
},
|
|
29
36
|
{
|
|
30
37
|
type: 'list',
|
|
31
38
|
name: 'responseFormat',
|
|
@@ -48,9 +55,11 @@ export async function init() {
|
|
|
48
55
|
},
|
|
49
56
|
]);
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
const { scope, ...settings } = answers;
|
|
59
|
+
saveConfig(settings, { scope });
|
|
60
|
+
console.log(chalk.green(`Configuration saved ${scope === 'global' ? 'for your user account' : 'in this project'}!`));
|
|
53
61
|
} catch (error) {
|
|
62
|
+
process.exitCode = 130;
|
|
54
63
|
if ((error as { isTtyError?: boolean })?.isTtyError) {
|
|
55
64
|
console.log(chalk.red('This command requires an interactive terminal.'));
|
|
56
65
|
} else {
|
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,11 +85,10 @@ 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
|
|
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,15 @@
|
|
|
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';
|
|
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(['.git', 'node_modules', 'dist', 'build', 'coverage', '.dhruv-cache', 'logs']);
|
|
11
|
+
|
|
12
|
+
/** Reads a file or up to 10 code files from a directory tree. */
|
|
8
13
|
function readCode(fileOrDir: string): string | undefined {
|
|
9
14
|
// Read first, branch on the error: no separate existence check to race against.
|
|
10
15
|
let content: string;
|
|
@@ -22,10 +27,24 @@ function readCode(fileOrDir: string): string | undefined {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
function readDirectory(dir: string): string | undefined {
|
|
25
|
-
const files =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
30
|
+
const files: string[] = [];
|
|
31
|
+
|
|
32
|
+
function collect(current: string): void {
|
|
33
|
+
if (files.length >= 10) return;
|
|
34
|
+
|
|
35
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
36
|
+
if (files.length >= 10) return;
|
|
37
|
+
const absolute = path.join(current, entry.name);
|
|
38
|
+
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
|
|
41
|
+
} else if (entry.isFile() && CODE_FILE.test(entry.name)) {
|
|
42
|
+
files.push(path.relative(dir, absolute).split(path.sep).join('/'));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
collect(dir);
|
|
29
48
|
|
|
30
49
|
if (files.length === 0) {
|
|
31
50
|
printError(`No code files found in directory "${dir}".`);
|
|
@@ -43,16 +62,42 @@ function readDirectory(dir: string): string | undefined {
|
|
|
43
62
|
return code;
|
|
44
63
|
}
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
const
|
|
65
|
+
function readGitDiff(fileOrDir: string): string | undefined {
|
|
66
|
+
const root = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
67
|
+
try {
|
|
68
|
+
const diff = execFileSync('git', ['diff', '--no-ext-diff', '--unified=80', '--'], {
|
|
69
|
+
cwd: root,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
72
|
+
});
|
|
73
|
+
if (!diff.trim()) {
|
|
74
|
+
printError(`No uncommitted changes found in "${fileOrDir}".`);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return diff;
|
|
78
|
+
} catch {
|
|
79
|
+
printError(`Could not read a git diff for "${fileOrDir}".`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ReviewOptions {
|
|
85
|
+
diff?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function review(fileOrDir: string, options: ReviewOptions = {}) {
|
|
89
|
+
const code = options.diff ? readGitDiff(fileOrDir) : readCode(fileOrDir);
|
|
48
90
|
if (code === undefined) return;
|
|
91
|
+
const projectRoot = fs.existsSync(fileOrDir) && fs.statSync(fileOrDir).isDirectory() ? fileOrDir : path.dirname(fileOrDir);
|
|
92
|
+
const projectType = detectProjectType(projectRoot);
|
|
93
|
+
const scope = options.diff ? 'the current uncommitted git diff' : 'the supplied source files';
|
|
49
94
|
|
|
50
95
|
await runCommand({
|
|
51
96
|
name: 'review',
|
|
52
97
|
input: { fileOrDir },
|
|
53
98
|
header: '🔍 Code Review: ',
|
|
54
99
|
buildRequest: (input, model) => ({
|
|
55
|
-
prompt: `Please review
|
|
100
|
+
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
101
|
systemMessage: getSystemMessage('review'),
|
|
57
102
|
model,
|
|
58
103
|
}),
|
|
@@ -4,6 +4,59 @@ import { runCommand } from '../core/command-runner.js';
|
|
|
4
4
|
import { getSystemMessage } from '../core/prompts.js';
|
|
5
5
|
import { printError } from '../utils/ux.js';
|
|
6
6
|
|
|
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']);
|
|
9
|
+
|
|
10
|
+
function redactSensitiveContent(content: string): string {
|
|
11
|
+
return content
|
|
12
|
+
.replace(/(\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`])[^"'`\r\n]+(["'`])/gi, '$1[REDACTED]$2')
|
|
13
|
+
.replace(/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/gi, '[REDACTED]')
|
|
14
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SecurityFinding {
|
|
18
|
+
line: number;
|
|
19
|
+
severity: 'high';
|
|
20
|
+
description: string;
|
|
21
|
+
remediation: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findHighConfidenceFindings(content: string): SecurityFinding[] {
|
|
25
|
+
const findings: SecurityFinding[] = [];
|
|
26
|
+
const lines = content.split(/\r?\n/);
|
|
27
|
+
|
|
28
|
+
lines.forEach((line, index) => {
|
|
29
|
+
if (/\b(?:api[_-]?key|secret|token|password|authorization)\s*[:=]\s*["'`][^"'`\r\n]+["'`]/i.test(line)) {
|
|
30
|
+
findings.push({
|
|
31
|
+
line: index + 1,
|
|
32
|
+
severity: 'high',
|
|
33
|
+
description: 'credential-like value assigned in source',
|
|
34
|
+
remediation: 'rotate the credential and load it from a secret manager or environment variable',
|
|
35
|
+
});
|
|
36
|
+
} else if (/\b(?:sk|pk)-[a-z0-9_-]{8,}\b/i.test(line)) {
|
|
37
|
+
findings.push({
|
|
38
|
+
line: index + 1,
|
|
39
|
+
severity: 'high',
|
|
40
|
+
description: 'credential-like API key detected',
|
|
41
|
+
remediation: 'rotate the credential and remove it from source control',
|
|
42
|
+
});
|
|
43
|
+
} else if (/\bBearer\s+[A-Za-z0-9._~+/=-]+/i.test(line)) {
|
|
44
|
+
findings.push({
|
|
45
|
+
line: index + 1,
|
|
46
|
+
severity: 'high',
|
|
47
|
+
description: 'bearer token detected',
|
|
48
|
+
remediation: 'revoke the token and use a secure runtime secret store',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SecurityCheckOptions {
|
|
57
|
+
strict?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
7
60
|
/** Reads a file or the code files of a directory (up to 10), concatenated. */
|
|
8
61
|
function readCode(fileOrDir: string): string | undefined {
|
|
9
62
|
// Read first, branch on the error: no separate existence check to race against.
|
|
@@ -22,10 +75,22 @@ function readCode(fileOrDir: string): string | undefined {
|
|
|
22
75
|
}
|
|
23
76
|
|
|
24
77
|
function readDirectory(dir: string): string | undefined {
|
|
25
|
-
const files =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
78
|
+
const files: string[] = [];
|
|
79
|
+
|
|
80
|
+
function collect(current: string): void {
|
|
81
|
+
if (files.length >= 10) return;
|
|
82
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
83
|
+
if (files.length >= 10) return;
|
|
84
|
+
const absolute = path.join(current, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
if (!IGNORED_DIRECTORIES.has(entry.name)) collect(absolute);
|
|
87
|
+
} else if (entry.isFile() && CODE_FILE.test(entry.name)) {
|
|
88
|
+
files.push(path.relative(dir, absolute).split(path.sep).join('/'));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
collect(dir);
|
|
29
94
|
|
|
30
95
|
if (files.length === 0) {
|
|
31
96
|
printError(`No code files found in directory "${dir}".`);
|
|
@@ -43,16 +108,25 @@ function readDirectory(dir: string): string | undefined {
|
|
|
43
108
|
return code;
|
|
44
109
|
}
|
|
45
110
|
|
|
46
|
-
export async function securityCheck(fileOrDir: string = '.') {
|
|
111
|
+
export async function securityCheck(fileOrDir: string = '.', options: SecurityCheckOptions = {}) {
|
|
47
112
|
const code = readCode(fileOrDir);
|
|
48
113
|
if (code === undefined) return;
|
|
114
|
+
const findings = findHighConfidenceFindings(code);
|
|
115
|
+
const safeCode = redactSensitiveContent(code);
|
|
116
|
+
const findingSummary = findings.length === 0
|
|
117
|
+
? 'none'
|
|
118
|
+
: findings.map((finding) => `- ${finding.severity} at line ${finding.line}: ${finding.description}; remediation: ${finding.remediation}`).join('\n');
|
|
119
|
+
|
|
120
|
+
if (options.strict && findings.length > 0) {
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
49
123
|
|
|
50
124
|
await runCommand({
|
|
51
125
|
name: 'security-check',
|
|
52
126
|
input: { fileOrDir },
|
|
53
127
|
header: '🛡️ Security Analysis: ',
|
|
54
128
|
buildRequest: (input, model) => ({
|
|
55
|
-
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${
|
|
129
|
+
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}`,
|
|
56
130
|
systemMessage: getSystemMessage('security'),
|
|
57
131
|
model,
|
|
58
132
|
}),
|