@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
|
@@ -15,23 +15,46 @@ import { metricsCollector } from '../core/metrics.js';
|
|
|
15
15
|
import { securityManager } from '../core/security.js';
|
|
16
16
|
/** Maps typed AI errors to user-facing hints — once, not per command. */
|
|
17
17
|
function describeAIError(error, model) {
|
|
18
|
-
|
|
18
|
+
if (!error || typeof error !== 'object' || !('kind' in error)) {
|
|
19
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
20
|
+
if (/econnrefused|failed to connect|fetch failed/i.test(msg)) {
|
|
21
|
+
return `💡 Make sure Ollama is running: ollama serve`;
|
|
22
|
+
}
|
|
23
|
+
if (/model.*not found/i.test(msg)) {
|
|
24
|
+
return `💡 Install the model: ollama pull ${model}`;
|
|
25
|
+
}
|
|
26
|
+
return msg;
|
|
27
|
+
}
|
|
28
|
+
const typedError = error;
|
|
29
|
+
switch (typedError.kind) {
|
|
19
30
|
case 'connection':
|
|
20
31
|
return `💡 Make sure Ollama is running: ollama serve`;
|
|
21
32
|
case 'model-not-found':
|
|
22
|
-
return `💡 Install the model: ollama pull ${
|
|
33
|
+
return `💡 Install the model: ollama pull ${typedError.model || model}`;
|
|
23
34
|
case 'empty-response':
|
|
24
|
-
return `💡 Model returned nothing. Install it: ollama pull ${
|
|
35
|
+
return `💡 Model returned nothing. Install it: ollama pull ${typedError.model || model}`;
|
|
36
|
+
case 'timeout':
|
|
37
|
+
return `💡 The request timed out after ${typedError.timeoutMs}ms. Try again, use a smaller prompt, or increase --timeout.`;
|
|
38
|
+
case 'cancelled':
|
|
39
|
+
return '💡 Request cancelled. Run the command again when ready.';
|
|
25
40
|
default:
|
|
26
|
-
return
|
|
41
|
+
return typedError.cause;
|
|
27
42
|
}
|
|
28
43
|
}
|
|
29
44
|
export async function runCommand(spec) {
|
|
30
45
|
const startTime = Date.now();
|
|
31
46
|
const { name, input } = spec;
|
|
32
47
|
const config = loadConfig();
|
|
48
|
+
const jsonOutput = config.responseFormat === 'json';
|
|
49
|
+
const writeJson = (result) => {
|
|
50
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
51
|
+
};
|
|
33
52
|
const fail = (error) => {
|
|
34
|
-
|
|
53
|
+
process.exitCode = 2;
|
|
54
|
+
if (jsonOutput)
|
|
55
|
+
writeJson({ ok: false, command: name, error, model: config.model, durationMs: Date.now() - startTime });
|
|
56
|
+
else
|
|
57
|
+
printError(error);
|
|
35
58
|
logCommand(name, startTime, false, { error });
|
|
36
59
|
metricsCollector.recordCommand(name, Date.now() - startTime, false);
|
|
37
60
|
};
|
|
@@ -46,30 +69,92 @@ export async function runCommand(spec) {
|
|
|
46
69
|
fail('Rate limit exceeded. Please try again later.');
|
|
47
70
|
return;
|
|
48
71
|
}
|
|
49
|
-
const spinner = ora('Thinking...').start();
|
|
72
|
+
const spinner = jsonOutput ? undefined : ora('Thinking...').start();
|
|
73
|
+
let requestTimeout;
|
|
74
|
+
let sigintHandler;
|
|
50
75
|
try {
|
|
51
|
-
spinner
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
spinner?.stop();
|
|
77
|
+
if (!jsonOutput) {
|
|
78
|
+
console.log(chalk.yellowBright('🤖 Dhruv CLI: AI-powered developer assistant'));
|
|
79
|
+
console.log(chalk.green.bold(spec.header));
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
let streamed = false;
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const request = {
|
|
85
|
+
...spec.buildRequest(input, config.model),
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
onToken: (token) => {
|
|
88
|
+
streamed = true;
|
|
89
|
+
if (!jsonOutput)
|
|
90
|
+
process.stdout.write(token);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const aiStartTime = Date.now();
|
|
94
|
+
const responsePromise = ask(request);
|
|
95
|
+
const cancellationPromise = new Promise((_, reject) => {
|
|
96
|
+
sigintHandler = () => {
|
|
97
|
+
controller.abort();
|
|
98
|
+
reject({ kind: 'cancelled' });
|
|
99
|
+
};
|
|
100
|
+
process.once('SIGINT', sigintHandler);
|
|
101
|
+
});
|
|
102
|
+
const response = config.timeoutMs > 0
|
|
103
|
+
? await Promise.race([
|
|
104
|
+
responsePromise,
|
|
105
|
+
cancellationPromise,
|
|
106
|
+
new Promise((_, reject) => {
|
|
107
|
+
requestTimeout = setTimeout(() => {
|
|
108
|
+
controller.abort();
|
|
109
|
+
reject({ kind: 'timeout', timeoutMs: config.timeoutMs });
|
|
110
|
+
}, config.timeoutMs);
|
|
111
|
+
}),
|
|
112
|
+
])
|
|
113
|
+
: await Promise.race([responsePromise, cancellationPromise]);
|
|
114
|
+
if (requestTimeout)
|
|
115
|
+
clearTimeout(requestTimeout);
|
|
116
|
+
if (sigintHandler)
|
|
117
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
118
|
+
if (!response.trim()) {
|
|
119
|
+
throw { kind: 'empty-response', model: config.model };
|
|
120
|
+
}
|
|
121
|
+
const durationMs = Date.now() - startTime;
|
|
122
|
+
if (jsonOutput) {
|
|
123
|
+
writeJson({ ok: true, command: name, model: config.model, response, durationMs });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (!streamed)
|
|
127
|
+
process.stdout.write(response);
|
|
128
|
+
process.stdout.write('\n');
|
|
129
|
+
if (spec.footer)
|
|
130
|
+
console.log(chalk.dim(spec.footer));
|
|
59
131
|
}
|
|
60
132
|
if (spec.onComplete)
|
|
61
133
|
spec.onComplete(response, input);
|
|
62
|
-
|
|
134
|
+
metricsCollector.recordAIRequest(config.model, name, Date.now() - aiStartTime, true);
|
|
63
135
|
logCommand(name, startTime, true, { model: config.model });
|
|
64
|
-
logPerformance(name,
|
|
65
|
-
metricsCollector.recordCommand(name,
|
|
66
|
-
logger.info(`${name} command completed successfully`, { duration });
|
|
136
|
+
logPerformance(name, durationMs);
|
|
137
|
+
metricsCollector.recordCommand(name, durationMs, true);
|
|
138
|
+
logger.info(`${name} command completed successfully`, { duration: durationMs });
|
|
67
139
|
}
|
|
68
140
|
catch (err) {
|
|
69
|
-
spinner
|
|
141
|
+
spinner?.stop();
|
|
142
|
+
if (requestTimeout)
|
|
143
|
+
clearTimeout(requestTimeout);
|
|
144
|
+
if (sigintHandler)
|
|
145
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
146
|
+
const cancelled = typeof err === 'object' && err !== null && 'kind' in err && err.kind === 'cancelled';
|
|
147
|
+
process.exitCode = cancelled ? 130 : 1;
|
|
70
148
|
const duration = Date.now() - startTime;
|
|
71
|
-
|
|
72
|
-
|
|
149
|
+
const hint = describeAIError(err, config.model);
|
|
150
|
+
metricsCollector.recordAIRequest(config.model, name, duration, false);
|
|
151
|
+
if (jsonOutput) {
|
|
152
|
+
writeJson({ ok: false, command: name, model: config.model, error: 'Command failed.', hint, durationMs: duration });
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
printError(`Command failed.`);
|
|
156
|
+
console.log(chalk.yellow(hint));
|
|
157
|
+
}
|
|
73
158
|
logError(`${name} command failed`, err, { command: name });
|
|
74
159
|
logCommand(name, startTime, false, { error: err.message });
|
|
75
160
|
metricsCollector.recordCommand(name, duration, false);
|
package/dist/core/logger.js
CHANGED
|
@@ -31,6 +31,7 @@ class Logger {
|
|
|
31
31
|
// Console transport for development
|
|
32
32
|
new winston.transports.Console({
|
|
33
33
|
level: config.verbose ? 'debug' : 'info',
|
|
34
|
+
stderrLevels: ['error', 'warn', 'info', 'debug'],
|
|
34
35
|
format: winston.format.combine(winston.format.colorize(), winston.format.simple(), winston.format.printf(({ timestamp, level, message }) => {
|
|
35
36
|
return `${timestamp} ${level}: ${message}`;
|
|
36
37
|
}))
|
package/dist/core/metrics.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import promClient from 'prom-client';
|
|
2
|
+
export interface CommandSummary {
|
|
3
|
+
runs: number;
|
|
4
|
+
successes: number;
|
|
5
|
+
failures: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ModelSummary {
|
|
9
|
+
requests: number;
|
|
10
|
+
successes: number;
|
|
11
|
+
failures: number;
|
|
12
|
+
durationMs: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CacheSummary {
|
|
15
|
+
hits: number;
|
|
16
|
+
misses: number;
|
|
17
|
+
}
|
|
18
|
+
export interface MetricsSummary {
|
|
19
|
+
sessions: number;
|
|
20
|
+
commands: Record<string, CommandSummary>;
|
|
21
|
+
errors: Record<string, number>;
|
|
22
|
+
models: Record<string, ModelSummary>;
|
|
23
|
+
cache: CacheSummary;
|
|
24
|
+
}
|
|
2
25
|
export declare const metrics: {
|
|
3
26
|
commandDuration: promClient.Histogram<"success" | "command">;
|
|
4
27
|
commandCount: promClient.Counter<"success" | "command">;
|
|
@@ -27,6 +50,11 @@ export declare class MetricsCollector {
|
|
|
27
50
|
updateMemoryUsage(): void;
|
|
28
51
|
recordSession(): void;
|
|
29
52
|
recordPluginLoaded(): void;
|
|
53
|
+
getSummary(): MetricsSummary;
|
|
54
|
+
resetPersistent(): void;
|
|
55
|
+
private persistentPath;
|
|
56
|
+
private readPersistent;
|
|
57
|
+
private updatePersistent;
|
|
30
58
|
getMetrics(): Promise<string>;
|
|
31
59
|
getMetricsJSON(): Promise<promClient.MetricObjectWithValues<promClient.MetricValue<string>>[]>;
|
|
32
60
|
getRegistry(): promClient.Registry;
|
package/dist/core/metrics.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import promClient from 'prom-client';
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
function emptySummary() {
|
|
6
|
+
return { sessions: 0, commands: {}, errors: {}, models: {}, cache: { hits: 0, misses: 0 } };
|
|
7
|
+
}
|
|
3
8
|
// Create a Registry which registers the metrics
|
|
4
9
|
const register = new promClient.Registry();
|
|
5
10
|
// Add a default label which is added to all metrics
|
|
@@ -100,6 +105,16 @@ export class MetricsCollector {
|
|
|
100
105
|
try {
|
|
101
106
|
metrics.commandDuration.observe({ command, success: success.toString() }, duration / 1000);
|
|
102
107
|
metrics.commandCount.inc({ command, success: success.toString() });
|
|
108
|
+
this.updatePersistent((summary) => {
|
|
109
|
+
const current = summary.commands[command] ?? { runs: 0, successes: 0, failures: 0, durationMs: 0 };
|
|
110
|
+
current.runs += 1;
|
|
111
|
+
if (success)
|
|
112
|
+
current.successes += 1;
|
|
113
|
+
else
|
|
114
|
+
current.failures += 1;
|
|
115
|
+
current.durationMs += duration;
|
|
116
|
+
summary.commands[command] = current;
|
|
117
|
+
});
|
|
103
118
|
}
|
|
104
119
|
catch (error) {
|
|
105
120
|
logger.error('Failed to record command metrics', error);
|
|
@@ -114,6 +129,16 @@ export class MetricsCollector {
|
|
|
114
129
|
if (tokensUsed) {
|
|
115
130
|
metrics.aiTokensUsed.inc({ model, command_type: commandType }, tokensUsed);
|
|
116
131
|
}
|
|
132
|
+
this.updatePersistent((summary) => {
|
|
133
|
+
const current = summary.models[model] ?? { requests: 0, successes: 0, failures: 0, durationMs: 0 };
|
|
134
|
+
current.requests += 1;
|
|
135
|
+
if (success)
|
|
136
|
+
current.successes += 1;
|
|
137
|
+
else
|
|
138
|
+
current.failures += 1;
|
|
139
|
+
current.durationMs += duration;
|
|
140
|
+
summary.models[model] = current;
|
|
141
|
+
});
|
|
117
142
|
}
|
|
118
143
|
catch (error) {
|
|
119
144
|
logger.error('Failed to record AI request metrics', error);
|
|
@@ -124,6 +149,9 @@ export class MetricsCollector {
|
|
|
124
149
|
return;
|
|
125
150
|
try {
|
|
126
151
|
metrics.cacheHitCount.inc({ cache_type: cacheType });
|
|
152
|
+
this.updatePersistent((summary) => {
|
|
153
|
+
summary.cache.hits += 1;
|
|
154
|
+
});
|
|
127
155
|
}
|
|
128
156
|
catch (error) {
|
|
129
157
|
logger.error('Failed to record cache hit metrics', error);
|
|
@@ -134,6 +162,9 @@ export class MetricsCollector {
|
|
|
134
162
|
return;
|
|
135
163
|
try {
|
|
136
164
|
metrics.cacheMissCount.inc({ cache_type: cacheType });
|
|
165
|
+
this.updatePersistent((summary) => {
|
|
166
|
+
summary.cache.misses += 1;
|
|
167
|
+
});
|
|
137
168
|
}
|
|
138
169
|
catch (error) {
|
|
139
170
|
logger.error('Failed to record cache miss metrics', error);
|
|
@@ -154,6 +185,10 @@ export class MetricsCollector {
|
|
|
154
185
|
return;
|
|
155
186
|
try {
|
|
156
187
|
metrics.errorCount.inc({ error_type: errorType, command: command || 'unknown' });
|
|
188
|
+
this.updatePersistent((summary) => {
|
|
189
|
+
const key = command ? `${errorType}:${command}` : errorType;
|
|
190
|
+
summary.errors[key] = (summary.errors[key] ?? 0) + 1;
|
|
191
|
+
});
|
|
157
192
|
}
|
|
158
193
|
catch (error) {
|
|
159
194
|
logger.error('Failed to record error metrics', error);
|
|
@@ -178,6 +213,9 @@ export class MetricsCollector {
|
|
|
178
213
|
return;
|
|
179
214
|
try {
|
|
180
215
|
metrics.sessionCount.inc();
|
|
216
|
+
this.updatePersistent((summary) => {
|
|
217
|
+
summary.sessions += 1;
|
|
218
|
+
});
|
|
181
219
|
}
|
|
182
220
|
catch (error) {
|
|
183
221
|
logger.error('Failed to record session metrics', error);
|
|
@@ -193,6 +231,47 @@ export class MetricsCollector {
|
|
|
193
231
|
logger.error('Failed to record plugin loaded metrics', error);
|
|
194
232
|
}
|
|
195
233
|
}
|
|
234
|
+
getSummary() {
|
|
235
|
+
return this.readPersistent();
|
|
236
|
+
}
|
|
237
|
+
resetPersistent() {
|
|
238
|
+
try {
|
|
239
|
+
fs.unlinkSync(this.persistentPath());
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (error.code !== 'ENOENT') {
|
|
243
|
+
logger.debug('Failed to unlink persistent metrics', { error: error.message });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
persistentPath() {
|
|
248
|
+
return path.join(process.cwd(), '.dhruv-metrics.json');
|
|
249
|
+
}
|
|
250
|
+
readPersistent() {
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(fs.readFileSync(this.persistentPath(), 'utf8'));
|
|
253
|
+
return {
|
|
254
|
+
sessions: typeof parsed.sessions === 'number' ? parsed.sessions : 0,
|
|
255
|
+
commands: parsed.commands ?? {},
|
|
256
|
+
errors: parsed.errors ?? {},
|
|
257
|
+
models: parsed.models ?? {},
|
|
258
|
+
cache: parsed.cache ?? { hits: 0, misses: 0 },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return emptySummary();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
updatePersistent(update) {
|
|
266
|
+
try {
|
|
267
|
+
const summary = this.readPersistent();
|
|
268
|
+
update(summary);
|
|
269
|
+
fs.writeFileSync(this.persistentPath(), JSON.stringify(summary, null, 2));
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
logger.debug('Failed to persist metrics', { error: error.message });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
196
275
|
async getMetrics() {
|
|
197
276
|
return register.metrics();
|
|
198
277
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { menu } from './commands/menu.js';
|
|
|
19
19
|
import { createRequire } from 'module';
|
|
20
20
|
import { logger, logInfo, logError } from './core/logger.js';
|
|
21
21
|
import { metricsCollector } from './core/metrics.js';
|
|
22
|
+
import { commandDescription, completionCommands, completionOptions } from './core/command-catalog.js';
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const pkg = require('../package.json');
|
|
24
25
|
const program = new Command();
|
|
@@ -28,69 +29,78 @@ program
|
|
|
28
29
|
.version(pkg.version);
|
|
29
30
|
program
|
|
30
31
|
.command('explain <query>')
|
|
31
|
-
.description('
|
|
32
|
+
.description(commandDescription('explain'))
|
|
32
33
|
.addHelpText('after', '\nExamples:\n $ dhruv explain "What is async/await?"\n $ dhruv explain "Docker containers vs VMs"')
|
|
33
34
|
.action(explain);
|
|
34
35
|
program
|
|
35
36
|
.command('suggest <query>')
|
|
36
|
-
.description('
|
|
37
|
+
.description(commandDescription('suggest'))
|
|
37
38
|
.addHelpText('after', '\nExamples:\n $ dhruv suggest "React performance optimization"\n $ dhruv suggest "Node.js project structure"')
|
|
38
39
|
.action(suggest);
|
|
39
40
|
program
|
|
40
41
|
.command('fix <query>')
|
|
41
|
-
.description('
|
|
42
|
+
.description(commandDescription('fix'))
|
|
42
43
|
.addHelpText('after', '\nExamples:\n $ dhruv fix "TypeError: Cannot read property of undefined"\n $ dhruv fix "CORS error in Express.js"')
|
|
43
44
|
.action(fix);
|
|
44
45
|
program
|
|
45
46
|
.command('review <fileOrDir>')
|
|
46
|
-
.description('
|
|
47
|
-
.
|
|
47
|
+
.description(commandDescription('review'))
|
|
48
|
+
.option('--diff', 'Review the current uncommitted git diff')
|
|
49
|
+
.action((fileOrDir, options) => review(fileOrDir, options));
|
|
48
50
|
program
|
|
49
51
|
.command('optimize <file>')
|
|
50
|
-
.description(
|
|
52
|
+
.description(commandDescription('optimize'))
|
|
51
53
|
.action(optimize);
|
|
52
54
|
program
|
|
53
55
|
.command('security-check [fileOrDir]')
|
|
54
|
-
.description('
|
|
55
|
-
.
|
|
56
|
+
.description(commandDescription('security-check'))
|
|
57
|
+
.option('--strict', 'Exit with failure when high-confidence findings are detected')
|
|
58
|
+
.action((fileOrDir, options) => securityCheck(fileOrDir, options));
|
|
56
59
|
program
|
|
57
60
|
.command('generate <type> <target>')
|
|
58
|
-
.description('
|
|
59
|
-
.
|
|
61
|
+
.description(commandDescription('generate'))
|
|
62
|
+
.option('--apply', 'Write generated tests to disk (preview is the default)')
|
|
63
|
+
.option('--output <path>', 'Write generated tests to this path')
|
|
64
|
+
.option('--overwrite', 'Allow replacing an existing output file')
|
|
65
|
+
.action((type, target, options) => generate(type, target, options));
|
|
60
66
|
program
|
|
61
67
|
.command('init')
|
|
62
|
-
.description('
|
|
68
|
+
.description(commandDescription('init'))
|
|
63
69
|
.action(init);
|
|
64
70
|
program
|
|
65
71
|
.command('status')
|
|
66
|
-
.description('
|
|
72
|
+
.description(commandDescription('status'))
|
|
67
73
|
.action(status);
|
|
68
74
|
program
|
|
69
75
|
.command('health')
|
|
70
|
-
.description('
|
|
71
|
-
.
|
|
76
|
+
.description(commandDescription('health'))
|
|
77
|
+
.option('--details', 'Show every health check and diagnostic detail')
|
|
78
|
+
.action((options) => health(options));
|
|
72
79
|
program
|
|
73
80
|
.command('metrics')
|
|
74
|
-
.description('
|
|
75
|
-
.
|
|
81
|
+
.description(commandDescription('metrics'))
|
|
82
|
+
.option('--raw', 'Export raw Prometheus metrics')
|
|
83
|
+
.option('--reset', 'Clear persisted local metrics')
|
|
84
|
+
.action((options) => metrics(options));
|
|
76
85
|
program
|
|
77
86
|
.command('project-type')
|
|
78
|
-
.description('
|
|
87
|
+
.description(commandDescription('project-type'))
|
|
79
88
|
.action(() => {
|
|
80
89
|
const type = detectProjectType();
|
|
81
90
|
console.log(chalk.blue(`Detected project type: ${type}`));
|
|
82
91
|
});
|
|
83
92
|
program
|
|
84
93
|
.command('menu')
|
|
85
|
-
.description('
|
|
94
|
+
.description(commandDescription('menu'))
|
|
86
95
|
.action(menu);
|
|
87
96
|
program
|
|
88
97
|
.option('--model <model>', 'Set Ollama model')
|
|
89
98
|
.option('--verbose', 'Enable verbose output')
|
|
90
99
|
.option('--json', 'Output in JSON format')
|
|
100
|
+
.option('--timeout <milliseconds>', 'Set the AI request timeout')
|
|
91
101
|
.hook('preAction', async (thisCommand) => {
|
|
92
102
|
const opts = thisCommand.opts();
|
|
93
|
-
if (opts.model || opts.verbose || opts.json) {
|
|
103
|
+
if (opts.model || opts.verbose || opts.json || opts.timeout) {
|
|
94
104
|
const config = {};
|
|
95
105
|
if (opts.model)
|
|
96
106
|
config.model = opts.model;
|
|
@@ -98,6 +108,8 @@ program
|
|
|
98
108
|
config.verbose = true;
|
|
99
109
|
if (opts.json)
|
|
100
110
|
config.responseFormat = 'json';
|
|
111
|
+
if (opts.timeout)
|
|
112
|
+
config.timeoutMs = Number(opts.timeout);
|
|
101
113
|
// Save config for session
|
|
102
114
|
const configModule = await import('./config/config.js');
|
|
103
115
|
configModule.saveConfig(config);
|
|
@@ -148,23 +160,85 @@ async function loadPlugins(program) {
|
|
|
148
160
|
// Autocomplete: Generate shell completion scripts
|
|
149
161
|
program
|
|
150
162
|
.command('completion')
|
|
151
|
-
.description('
|
|
163
|
+
.description(commandDescription('completion'))
|
|
152
164
|
.argument('[shell]', 'shell type (bash|zsh|fish)', 'bash')
|
|
153
165
|
.action((shell) => {
|
|
166
|
+
const commands = completionCommands();
|
|
167
|
+
const options = completionOptions();
|
|
154
168
|
let script = '';
|
|
155
169
|
switch (shell) {
|
|
156
170
|
case 'zsh':
|
|
157
|
-
script = `#compdef dhruv
|
|
171
|
+
script = `#compdef dhruv
|
|
172
|
+
_dhruv_completion() {
|
|
173
|
+
local -a commands
|
|
174
|
+
commands=(${commands})
|
|
175
|
+
_arguments -C \\
|
|
176
|
+
'1:command:->cmds' \\
|
|
177
|
+
'*::options:->args'
|
|
178
|
+
case "$state" in
|
|
179
|
+
cmds)
|
|
180
|
+
_describe -t commands 'dhruv command' commands
|
|
181
|
+
;;
|
|
182
|
+
args)
|
|
183
|
+
case $words[1] in
|
|
184
|
+
generate)
|
|
185
|
+
_arguments '1:type:(tests documentation docs component)' '*:file:_files'
|
|
186
|
+
;;
|
|
187
|
+
review|optimize|security-check)
|
|
188
|
+
_arguments '*:file:_files'
|
|
189
|
+
;;
|
|
190
|
+
completion)
|
|
191
|
+
_arguments '1:shell:(bash zsh fish)'
|
|
192
|
+
;;
|
|
193
|
+
*)
|
|
194
|
+
_arguments '*:options:(${options})'
|
|
195
|
+
;;
|
|
196
|
+
esac
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
compdef _dhruv_completion dhruv`;
|
|
158
201
|
break;
|
|
159
202
|
case 'fish':
|
|
160
|
-
script = `
|
|
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}'`;
|
|
161
204
|
break;
|
|
162
|
-
|
|
205
|
+
case 'bash':
|
|
163
206
|
script = String.raw `#!/bin/bash
|
|
164
207
|
_dhruv_completion() {
|
|
165
|
-
|
|
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
|
|
166
235
|
}
|
|
167
236
|
complete -F _dhruv_completion dhruv`;
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
console.error(chalk.red(`Unsupported shell "${shell}". Choose bash, zsh, or fish.`));
|
|
240
|
+
process.exitCode = 2;
|
|
241
|
+
return;
|
|
168
242
|
}
|
|
169
243
|
console.log(script);
|
|
170
244
|
console.log(`\n# To enable tab completion, add the above to your shell profile or source it directly.`);
|
|
@@ -1 +1,7 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface ProjectContext {
|
|
2
|
+
type: string;
|
|
3
|
+
framework?: string;
|
|
4
|
+
diagnostic?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function detectProjectDetails(directory?: string): ProjectContext;
|
|
7
|
+
export declare function detectProjectType(directory?: string): string;
|
|
@@ -1,17 +1,95 @@
|
|
|
1
1
|
// Use .js extension for ESM compatibility
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { logger } from '../core/logger.js';
|
|
5
|
+
export function detectProjectDetails(directory = process.cwd()) {
|
|
6
|
+
const file = (name) => path.join(directory, name);
|
|
7
|
+
if (fs.existsSync(file('package.json'))) {
|
|
8
|
+
let pkg;
|
|
9
|
+
try {
|
|
10
|
+
pkg = JSON.parse(fs.readFileSync(file('package.json'), 'utf-8'));
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
const diagnostic = `Malformed package.json in ${directory}: ${err.message}`;
|
|
14
|
+
logger.warn(diagnostic);
|
|
15
|
+
return { type: 'unknown', diagnostic };
|
|
16
|
+
}
|
|
17
|
+
const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
18
|
+
if (dependencies.react)
|
|
19
|
+
return { type: 'node', framework: 'react' };
|
|
20
|
+
if (dependencies.next)
|
|
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' };
|
|
30
|
+
if (dependencies.express)
|
|
31
|
+
return { type: 'node', framework: 'node-express' };
|
|
32
|
+
if (dependencies.typescript || fs.existsSync(file('tsconfig.json')))
|
|
33
|
+
return { type: 'node-typescript' };
|
|
34
|
+
return { type: 'node' };
|
|
11
35
|
}
|
|
12
|
-
if (fs.existsSync('
|
|
13
|
-
return '
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 };
|
|
61
|
+
}
|
|
62
|
+
if (fs.existsSync(file('go.mod')))
|
|
63
|
+
return { type: 'go' };
|
|
64
|
+
if (fs.existsSync(file('Cargo.toml')))
|
|
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;
|
|
17
95
|
}
|