@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.
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 +16 -4
  7. package/README.md +145 -40
  8. package/__tests__/cli-contract.test.ts +170 -0
  9. package/__tests__/core.test.ts +193 -1
  10. package/__tests__/diagnostics.test.ts +179 -0
  11. package/__tests__/file-workflows.test.ts +234 -0
  12. package/__tests__/interactive.test.ts +134 -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 +44 -11
  17. package/dist/commands/health.d.ts +4 -1
  18. package/dist/commands/health.js +59 -16
  19. package/dist/commands/init.js +18 -7
  20. package/dist/commands/menu.js +125 -100
  21. package/dist/commands/metrics.d.ts +5 -1
  22. package/dist/commands/metrics.js +49 -10
  23. package/dist/commands/optimize.js +1 -1
  24. package/dist/commands/review.d.ts +4 -1
  25. package/dist/commands/review.js +66 -9
  26. package/dist/commands/security-check.d.ts +4 -1
  27. package/dist/commands/security-check.js +100 -6
  28. package/dist/commands/status.js +44 -4
  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 +106 -21
  36. package/dist/core/logger.js +1 -0
  37. package/dist/core/metrics.d.ts +28 -0
  38. package/dist/core/metrics.js +79 -0
  39. package/dist/index.js +98 -24
  40. package/dist/utils/projectType.d.ts +7 -1
  41. package/dist/utils/projectType.js +91 -13
  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 +50 -11
  53. package/src/commands/health.ts +62 -17
  54. package/src/commands/init.ts +18 -7
  55. package/src/commands/menu.ts +54 -30
  56. package/src/commands/metrics.ts +53 -9
  57. package/src/commands/optimize.ts +1 -1
  58. package/src/commands/review.ts +72 -9
  59. package/src/commands/security-check.ts +111 -6
  60. package/src/commands/status.ts +43 -5
  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 +108 -22
  65. package/src/core/logger.ts +1 -0
  66. package/src/core/metrics.ts +105 -0
  67. package/src/index.ts +97 -24
  68. package/src/utils/projectType.ts +85 -9
  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
@@ -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
- switch (error.kind) {
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 ${error.model || model}`;
33
+ return `💡 Install the model: ollama pull ${typedError.model || model}`;
23
34
  case 'empty-response':
24
- return `💡 Model returned nothing. Install it: ollama pull ${error.model || model}`;
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 error.cause;
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
- printError(error);
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.stop();
52
- console.log(chalk.yellowBright('🤖 Dhruv CLI: AI-powered developer assistant'));
53
- console.log(chalk.green.bold(spec.header));
54
- console.log();
55
- const response = await ask(spec.buildRequest(input, config.model));
56
- console.log('\n');
57
- if (spec.footer) {
58
- console.log(chalk.dim(spec.footer));
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
- const duration = Date.now() - startTime;
134
+ metricsCollector.recordAIRequest(config.model, name, Date.now() - aiStartTime, true);
63
135
  logCommand(name, startTime, true, { model: config.model });
64
- logPerformance(name, duration);
65
- metricsCollector.recordCommand(name, duration, true);
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.stop();
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
- printError(`Command failed.`);
72
- console.log(chalk.yellow(describeAIError(err, config.model)));
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);
@@ -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
  }))
@@ -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;
@@ -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('Explain a concept or command')
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('Get AI-powered suggestions')
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('Get a fix for a coding issue or error')
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('Review code in a file or directory')
47
- .action(review);
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('Optimize a file (e.g., package.json)')
52
+ .description(commandDescription('optimize'))
51
53
  .action(optimize);
52
54
  program
53
55
  .command('security-check [fileOrDir]')
54
- .description('Run a security check on code')
55
- .action(securityCheck);
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('Generate code/tests for a file')
59
- .action(generate);
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('Interactive setup/configuration wizard')
68
+ .description(commandDescription('init'))
63
69
  .action(init);
64
70
  program
65
71
  .command('status')
66
- .description('Check Ollama connection and available models')
72
+ .description(commandDescription('status'))
67
73
  .action(status);
68
74
  program
69
75
  .command('health')
70
- .description('Run comprehensive health check')
71
- .action(health);
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('Display CLI usage metrics')
75
- .action(metrics);
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('Detect and print the current project type')
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('Interactive command palette')
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('Generate shell completion script')
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\n_dhruv_completion() {\n reply=( $(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}') )\n}\ncompctl -K _dhruv_completion dhruv`;
171
+ script = `#compdef dhruv
172
+ _dhruv_completion() {
173
+ local -a commands
174
+ commands=(${commands})
175
+ _arguments -C \\
176
+ '1:command:->cmds' \\
177
+ '*::options:->args'
178
+ case "$state" in
179
+ cmds)
180
+ _describe -t commands 'dhruv command' commands
181
+ ;;
182
+ args)
183
+ case $words[1] in
184
+ generate)
185
+ _arguments '1:type:(tests documentation docs component)' '*:file:_files'
186
+ ;;
187
+ review|optimize|security-check)
188
+ _arguments '*:file:_files'
189
+ ;;
190
+ completion)
191
+ _arguments '1:shell:(bash zsh fish)'
192
+ ;;
193
+ *)
194
+ _arguments '*:options:(${options})'
195
+ ;;
196
+ esac
197
+ ;;
198
+ esac
199
+ }
200
+ compdef _dhruv_completion dhruv`;
158
201
  break;
159
202
  case 'fish':
160
- script = `function __fish_dhruv_complete\n dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}'\nend\ncomplete -c dhruv -a '(__fish_dhruv_complete)'`;
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
- default:
205
+ case 'bash':
163
206
  script = String.raw `#!/bin/bash
164
207
  _dhruv_completion() {
165
- COMPREPLY=( $(compgen -W "$(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}')" -- \${COMP_WORDS[1]}) )
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 declare function detectProjectType(): string;
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
- export function detectProjectType() {
4
- if (fs.existsSync('package.json')) {
5
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
6
- if (pkg.dependencies?.react || pkg.devDependencies?.react)
7
- return 'react';
8
- if (pkg.dependencies?.express || pkg.devDependencies?.express)
9
- return 'node-express';
10
- return 'node';
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('requirements.txt'))
13
- return 'python';
14
- if (fs.existsSync('pyproject.toml'))
15
- return 'python';
16
- return 'unknown';
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
  }