@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
@@ -1,5 +1,38 @@
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
+
6
+ export interface CommandSummary {
7
+ runs: number;
8
+ successes: number;
9
+ failures: number;
10
+ durationMs: number;
11
+ }
12
+
13
+ export interface ModelSummary {
14
+ requests: number;
15
+ successes: number;
16
+ failures: number;
17
+ durationMs: number;
18
+ }
19
+
20
+ export interface CacheSummary {
21
+ hits: number;
22
+ misses: number;
23
+ }
24
+
25
+ export interface MetricsSummary {
26
+ sessions: number;
27
+ commands: Record<string, CommandSummary>;
28
+ errors: Record<string, number>;
29
+ models: Record<string, ModelSummary>;
30
+ cache: CacheSummary;
31
+ }
32
+
33
+ function emptySummary(): MetricsSummary {
34
+ return { sessions: 0, commands: {}, errors: {}, models: {}, cache: { hits: 0, misses: 0 } };
35
+ }
3
36
 
4
37
  // Create a Registry which registers the metrics
5
38
  const register = new promClient.Registry();
@@ -122,6 +155,14 @@ export class MetricsCollector {
122
155
  try {
123
156
  metrics.commandDuration.observe({ command, success: success.toString() }, duration / 1000);
124
157
  metrics.commandCount.inc({ command, success: success.toString() });
158
+ this.updatePersistent((summary) => {
159
+ const current = summary.commands[command] ?? { runs: 0, successes: 0, failures: 0, durationMs: 0 };
160
+ current.runs += 1;
161
+ if (success) current.successes += 1;
162
+ else current.failures += 1;
163
+ current.durationMs += duration;
164
+ summary.commands[command] = current;
165
+ });
125
166
  } catch (error) {
126
167
  logger.error('Failed to record command metrics', error as Error);
127
168
  }
@@ -137,6 +178,14 @@ export class MetricsCollector {
137
178
  if (tokensUsed) {
138
179
  metrics.aiTokensUsed.inc({ model, command_type: commandType }, tokensUsed);
139
180
  }
181
+ this.updatePersistent((summary) => {
182
+ const current = summary.models[model] ?? { requests: 0, successes: 0, failures: 0, durationMs: 0 };
183
+ current.requests += 1;
184
+ if (success) current.successes += 1;
185
+ else current.failures += 1;
186
+ current.durationMs += duration;
187
+ summary.models[model] = current;
188
+ });
140
189
  } catch (error) {
141
190
  logger.error('Failed to record AI request metrics', error as Error);
142
191
  }
@@ -147,6 +196,9 @@ export class MetricsCollector {
147
196
 
148
197
  try {
149
198
  metrics.cacheHitCount.inc({ cache_type: cacheType });
199
+ this.updatePersistent((summary) => {
200
+ summary.cache.hits += 1;
201
+ });
150
202
  } catch (error) {
151
203
  logger.error('Failed to record cache hit metrics', error as Error);
152
204
  }
@@ -157,6 +209,9 @@ export class MetricsCollector {
157
209
 
158
210
  try {
159
211
  metrics.cacheMissCount.inc({ cache_type: cacheType });
212
+ this.updatePersistent((summary) => {
213
+ summary.cache.misses += 1;
214
+ });
160
215
  } catch (error) {
161
216
  logger.error('Failed to record cache miss metrics', error as Error);
162
217
  }
@@ -177,6 +232,10 @@ export class MetricsCollector {
177
232
 
178
233
  try {
179
234
  metrics.errorCount.inc({ error_type: errorType, command: command || 'unknown' });
235
+ this.updatePersistent((summary) => {
236
+ const key = command ? `${errorType}:${command}` : errorType;
237
+ summary.errors[key] = (summary.errors[key] ?? 0) + 1;
238
+ });
180
239
  } catch (error) {
181
240
  logger.error('Failed to record error metrics', error as Error);
182
241
  }
@@ -201,6 +260,9 @@ export class MetricsCollector {
201
260
 
202
261
  try {
203
262
  metrics.sessionCount.inc();
263
+ this.updatePersistent((summary) => {
264
+ summary.sessions += 1;
265
+ });
204
266
  } catch (error) {
205
267
  logger.error('Failed to record session metrics', error as Error);
206
268
  }
@@ -216,6 +278,49 @@ export class MetricsCollector {
216
278
  }
217
279
  }
218
280
 
281
+ public getSummary(): MetricsSummary {
282
+ return this.readPersistent();
283
+ }
284
+
285
+ public resetPersistent(): void {
286
+ try {
287
+ fs.unlinkSync(this.persistentPath());
288
+ } catch (error) {
289
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
290
+ logger.debug('Failed to unlink persistent metrics', { error: (error as Error).message });
291
+ }
292
+ }
293
+ }
294
+
295
+ private persistentPath(): string {
296
+ return path.join(process.cwd(), '.dhruv-metrics.json');
297
+ }
298
+
299
+ private readPersistent(): MetricsSummary {
300
+ try {
301
+ const parsed = JSON.parse(fs.readFileSync(this.persistentPath(), 'utf8')) as Partial<MetricsSummary>;
302
+ return {
303
+ sessions: typeof parsed.sessions === 'number' ? parsed.sessions : 0,
304
+ commands: parsed.commands ?? {},
305
+ errors: parsed.errors ?? {},
306
+ models: parsed.models ?? {},
307
+ cache: parsed.cache ?? { hits: 0, misses: 0 },
308
+ };
309
+ } catch {
310
+ return emptySummary();
311
+ }
312
+ }
313
+
314
+ private updatePersistent(update: (summary: MetricsSummary) => void): void {
315
+ try {
316
+ const summary = this.readPersistent();
317
+ update(summary);
318
+ fs.writeFileSync(this.persistentPath(), JSON.stringify(summary, null, 2));
319
+ } catch (error) {
320
+ logger.debug('Failed to persist metrics', { error: (error as Error).message });
321
+ }
322
+ }
323
+
219
324
  public async getMetrics(): Promise<string> {
220
325
  return register.metrics();
221
326
  }
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ import { createRequire } from 'module';
20
20
  import { logger, logCommand, logInfo, logError } from './core/logger.js';
21
21
  import { metricsCollector } from './core/metrics.js';
22
22
  import { securityManager } from './core/security.js';
23
+ import { commandDescription, completionCommands, completionOptions } from './core/command-catalog.js';
23
24
  const require = createRequire(import.meta.url);
24
25
  const pkg = require('../package.json');
25
26
 
@@ -32,65 +33,73 @@ program
32
33
 
33
34
  program
34
35
  .command('explain <query>')
35
- .description('Explain a concept or command')
36
+ .description(commandDescription('explain'))
36
37
  .addHelpText('after', '\nExamples:\n $ dhruv explain "What is async/await?"\n $ dhruv explain "Docker containers vs VMs"')
37
38
  .action(explain);
38
39
 
39
40
  program
40
41
  .command('suggest <query>')
41
- .description('Get AI-powered suggestions')
42
+ .description(commandDescription('suggest'))
42
43
  .addHelpText('after', '\nExamples:\n $ dhruv suggest "React performance optimization"\n $ dhruv suggest "Node.js project structure"')
43
44
  .action(suggest);
44
45
 
45
46
  program
46
47
  .command('fix <query>')
47
- .description('Get a fix for a coding issue or error')
48
+ .description(commandDescription('fix'))
48
49
  .addHelpText('after', '\nExamples:\n $ dhruv fix "TypeError: Cannot read property of undefined"\n $ dhruv fix "CORS error in Express.js"')
49
50
  .action(fix);
50
51
 
51
52
  program
52
53
  .command('review <fileOrDir>')
53
- .description('Review code in a file or directory')
54
- .action(review);
54
+ .description(commandDescription('review'))
55
+ .option('--diff', 'Review the current uncommitted git diff')
56
+ .action((fileOrDir: string, options: Record<string, any>) => review(fileOrDir, options));
55
57
 
56
58
  program
57
59
  .command('optimize <file>')
58
- .description('Optimize a file (e.g., package.json)')
60
+ .description(commandDescription('optimize'))
59
61
  .action(optimize);
60
62
 
61
63
  program
62
64
  .command('security-check [fileOrDir]')
63
- .description('Run a security check on code')
64
- .action(securityCheck);
65
+ .description(commandDescription('security-check'))
66
+ .option('--strict', 'Exit with failure when high-confidence findings are detected')
67
+ .action((fileOrDir: string | undefined, options: Record<string, any>) => securityCheck(fileOrDir, options));
65
68
 
66
69
  program
67
70
  .command('generate <type> <target>')
68
- .description('Generate code/tests for a file')
69
- .action(generate);
71
+ .description(commandDescription('generate'))
72
+ .option('--apply', 'Write generated tests to disk (preview is the default)')
73
+ .option('--output <path>', 'Write generated tests to this path')
74
+ .option('--overwrite', 'Allow replacing an existing output file')
75
+ .action((type: string, target: string, options: Record<string, any>) => generate(type, target, options));
70
76
 
71
77
  program
72
78
  .command('init')
73
- .description('Interactive setup/configuration wizard')
79
+ .description(commandDescription('init'))
74
80
  .action(init);
75
81
 
76
82
  program
77
83
  .command('status')
78
- .description('Check Ollama connection and available models')
84
+ .description(commandDescription('status'))
79
85
  .action(status);
80
86
 
81
87
  program
82
88
  .command('health')
83
- .description('Run comprehensive health check')
84
- .action(health);
89
+ .description(commandDescription('health'))
90
+ .option('--details', 'Show every health check and diagnostic detail')
91
+ .action((options: Record<string, any>) => health(options));
85
92
 
86
93
  program
87
94
  .command('metrics')
88
- .description('Display CLI usage metrics')
89
- .action(metrics);
95
+ .description(commandDescription('metrics'))
96
+ .option('--raw', 'Export raw Prometheus metrics')
97
+ .option('--reset', 'Clear persisted local metrics')
98
+ .action((options: Record<string, any>) => metrics(options));
90
99
 
91
100
  program
92
101
  .command('project-type')
93
- .description('Detect and print the current project type')
102
+ .description(commandDescription('project-type'))
94
103
  .action(() => {
95
104
  const type = detectProjectType();
96
105
  console.log(chalk.blue(`Detected project type: ${type}`));
@@ -98,20 +107,22 @@ program
98
107
 
99
108
  program
100
109
  .command('menu')
101
- .description('Interactive command palette')
110
+ .description(commandDescription('menu'))
102
111
  .action(menu);
103
112
 
104
113
  program
105
114
  .option('--model <model>', 'Set Ollama model')
106
115
  .option('--verbose', 'Enable verbose output')
107
116
  .option('--json', 'Output in JSON format')
117
+ .option('--timeout <milliseconds>', 'Set the AI request timeout')
108
118
  .hook('preAction', async (thisCommand) => {
109
119
  const opts = thisCommand.opts();
110
- if (opts.model || opts.verbose || opts.json) {
120
+ if (opts.model || opts.verbose || opts.json || opts.timeout) {
111
121
  const config: Record<string, unknown> = {};
112
122
  if (opts.model) config.model = opts.model;
113
123
  if (opts.verbose) config.verbose = true;
114
124
  if (opts.json) config.responseFormat = 'json';
125
+ if (opts.timeout) config.timeoutMs = Number(opts.timeout);
115
126
  // Save config for session
116
127
  const configModule = await import('./config/config.js');
117
128
  configModule.saveConfig(config);
@@ -165,23 +176,85 @@ async function loadPlugins(program: unknown) {
165
176
  // Autocomplete: Generate shell completion scripts
166
177
  program
167
178
  .command('completion')
168
- .description('Generate shell completion script')
179
+ .description(commandDescription('completion'))
169
180
  .argument('[shell]', 'shell type (bash|zsh|fish)', 'bash')
170
181
  .action((shell: string) => {
182
+ const commands = completionCommands();
183
+ const options = completionOptions();
171
184
  let script = '';
172
185
  switch (shell) {
173
186
  case 'zsh':
174
- script = `#compdef dhruv\n_dhruv_completion() {\n reply=( $(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}') )\n}\ncompctl -K _dhruv_completion dhruv`;
187
+ script = `#compdef dhruv
188
+ _dhruv_completion() {
189
+ local -a commands
190
+ commands=(${commands})
191
+ _arguments -C \\
192
+ '1:command:->cmds' \\
193
+ '*::options:->args'
194
+ case "$state" in
195
+ cmds)
196
+ _describe -t commands 'dhruv command' commands
197
+ ;;
198
+ args)
199
+ case $words[1] in
200
+ generate)
201
+ _arguments '1:type:(tests documentation docs component)' '*:file:_files'
202
+ ;;
203
+ review|optimize|security-check)
204
+ _arguments '*:file:_files'
205
+ ;;
206
+ completion)
207
+ _arguments '1:shell:(bash zsh fish)'
208
+ ;;
209
+ *)
210
+ _arguments '*:options:(${options})'
211
+ ;;
212
+ esac
213
+ ;;
214
+ esac
215
+ }
216
+ compdef _dhruv_completion dhruv`;
175
217
  break;
176
218
  case 'fish':
177
- script = `function __fish_dhruv_complete\n dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}'\nend\ncomplete -c dhruv -a '(__fish_dhruv_complete)'`;
219
+ 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}'`;
178
220
  break;
179
- default:
221
+ case 'bash':
180
222
  script = String.raw`#!/bin/bash
181
223
  _dhruv_completion() {
182
- COMPREPLY=( $(compgen -W "$(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}')" -- \${COMP_WORDS[1]}) )
224
+ local cur prev commands options
225
+ COMPREPLY=()
226
+ cur="\${COMP_WORDS[COMP_CWORD]}"
227
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
228
+ commands="${commands}"
229
+ options="${options}"
230
+
231
+ if [[ "$prev" == "generate" ]]; then
232
+ COMPREPLY=( $(compgen -W "tests documentation docs component" -- "$cur") )
233
+ return 0
234
+ fi
235
+ if [[ "$prev" == "completion" ]]; then
236
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") )
237
+ return 0
238
+ fi
239
+ if [[ "$prev" == "review" || "$prev" == "optimize" || "$prev" == "security-check" ]]; then
240
+ COMPREPLY=( $(compgen -f -- "$cur") )
241
+ return 0
242
+ fi
243
+
244
+ if [[ "$cur" == -* ]]; then
245
+ COMPREPLY=( $(compgen -W "$options" -- "$cur") )
246
+ elif [[ $COMP_CWORD -eq 1 ]]; then
247
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
248
+ else
249
+ COMPREPLY=( $(compgen -W "$commands $options" -- "$cur") )
250
+ fi
183
251
  }
184
252
  complete -F _dhruv_completion dhruv`;
253
+ break;
254
+ default:
255
+ console.error(chalk.red(`Unsupported shell "${shell}". Choose bash, zsh, or fish.`));
256
+ process.exitCode = 2;
257
+ return;
185
258
  }
186
259
  console.log(script);
187
260
  console.log(`\n# To enable tab completion, add the above to your shell profile or source it directly.`);
@@ -1,14 +1,90 @@
1
1
  // Use .js extension for ESM compatibility
2
2
  import fs from 'fs';
3
+ import path from 'path';
4
+ import { logger } from '../core/logger.js';
3
5
 
4
- export function detectProjectType(): string {
5
- if (fs.existsSync('package.json')) {
6
- const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
7
- if (pkg.dependencies?.react || pkg.devDependencies?.react) return 'react';
8
- if (pkg.dependencies?.express || pkg.devDependencies?.express) return 'node-express';
9
- return 'node';
6
+ export interface ProjectContext {
7
+ type: string;
8
+ framework?: string;
9
+ diagnostic?: string;
10
+ }
11
+
12
+ export function detectProjectDetails(directory: string = process.cwd()): ProjectContext {
13
+ const file = (name: string) => path.join(directory, name);
14
+
15
+ if (fs.existsSync(file('package.json'))) {
16
+ let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
17
+ try {
18
+ pkg = JSON.parse(fs.readFileSync(file('package.json'), 'utf-8'));
19
+ } catch (err) {
20
+ const diagnostic = `Malformed package.json in ${directory}: ${(err as Error).message}`;
21
+ logger.warn(diagnostic);
22
+ return { type: 'unknown', diagnostic };
23
+ }
24
+
25
+ const dependencies = { ...pkg.dependencies, ...pkg.devDependencies };
26
+ if (dependencies.react) return { type: 'node', framework: 'react' };
27
+ if (dependencies.next) return { type: 'node', framework: 'nextjs' };
28
+ if (dependencies.vue) return { type: 'node', framework: 'vue' };
29
+ if (dependencies['@angular/core']) return { type: 'node', framework: 'angular' };
30
+ if (dependencies.svelte) return { type: 'node', framework: 'svelte' };
31
+ if (dependencies['@nestjs/core']) return { type: 'node', framework: 'nestjs' };
32
+ if (dependencies.express) return { type: 'node', framework: 'node-express' };
33
+ if (dependencies.typescript || fs.existsSync(file('tsconfig.json'))) return { type: 'node-typescript' };
34
+ return { type: 'node' };
35
+ }
36
+
37
+ if (fs.existsSync(file('tsconfig.json'))) {
38
+ return { type: 'node-typescript' };
39
+ }
40
+
41
+ if (fs.existsSync(file('requirements.txt')) || fs.existsSync(file('pyproject.toml')) || fs.existsSync(file('Pipfile')) || fs.existsSync(file('setup.py'))) {
42
+ let framework: string | undefined;
43
+ if (fs.existsSync(file('manage.py'))) {
44
+ framework = 'django';
45
+ } else if (fs.existsSync(file('requirements.txt'))) {
46
+ try {
47
+ const reqs = fs.readFileSync(file('requirements.txt'), 'utf-8');
48
+ if (/fastapi/i.test(reqs)) framework = 'fastapi';
49
+ else if (/flask/i.test(reqs)) framework = 'flask';
50
+ else if (/django/i.test(reqs)) framework = 'django';
51
+ } catch (err) {
52
+ const diagnostic = `Error reading requirements.txt: ${(err as Error).message}`;
53
+ logger.warn(diagnostic);
54
+ return { type: 'python', diagnostic };
55
+ }
56
+ }
57
+ return { type: 'python', framework };
58
+ }
59
+
60
+ if (fs.existsSync(file('go.mod'))) return { type: 'go' };
61
+ if (fs.existsSync(file('Cargo.toml'))) return { type: 'rust' };
62
+ if (fs.existsSync(file('pom.xml')) || fs.existsSync(file('build.gradle')) || fs.existsSync(file('build.gradle.kts'))) {
63
+ let framework: string | undefined;
64
+ try {
65
+ const pomPath = file('pom.xml');
66
+ const gradlePath = file('build.gradle');
67
+ const content = fs.existsSync(pomPath)
68
+ ? fs.readFileSync(pomPath, 'utf-8')
69
+ : (fs.existsSync(gradlePath) ? fs.readFileSync(gradlePath, 'utf-8') : '');
70
+ if (/spring-boot/i.test(content)) framework = 'spring-boot';
71
+ } catch {
72
+ // safe fallback
73
+ }
74
+ return { type: 'java', framework };
75
+ }
76
+
77
+ return { type: 'unknown' };
78
+ }
79
+
80
+ export function detectProjectType(directory: string = process.cwd()): string {
81
+ const details = detectProjectDetails(directory);
82
+ if (details.type === 'unknown') return 'unknown';
83
+ if (details.framework) {
84
+ if (details.framework === 'react' || details.framework === 'nextjs' || details.framework === 'node-express') {
85
+ return details.framework;
86
+ }
87
+ return `${details.type}-${details.framework}`;
10
88
  }
11
- if (fs.existsSync('requirements.txt')) return 'python';
12
- if (fs.existsSync('pyproject.toml')) return 'python';
13
- return 'unknown';
89
+ return details.type;
14
90
  }
package/tsconfig.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
4
  "module": "ESNext",
5
- "moduleResolution": "Node",
5
+ "moduleResolution": "Bundler",
6
6
  "outDir": "./dist",
7
7
  "rootDir": "./src",
8
8
  "esModuleInterop": true,
@@ -1,14 +0,0 @@
1
- name: Auto Assign
2
-
3
- on:
4
- pull_request:
5
- types: [opened, reopened]
6
-
7
- jobs:
8
- add-assignees:
9
- runs-on: ubuntu-latest
10
- steps:
11
- - uses: kentaro-m/auto-assign-action@v1.2.4
12
- with:
13
- repo-token: ${{ secrets.GITHUB_TOKEN }}
14
- configuration-path: .github/auto_assign.yml
@@ -1,154 +0,0 @@
1
- name: Build & Publish
2
-
3
- on:
4
- # Automatic main releases are owned by release.yml.
5
- workflow_dispatch:
6
- inputs:
7
- version_type:
8
- description: 'Version bump type'
9
- required: true
10
- default: 'patch'
11
- type: choice
12
- options:
13
- - patch
14
- - minor
15
- - major
16
- - prerelease
17
-
18
- permissions:
19
- contents: write
20
- packages: write
21
- id-token: write
22
-
23
- jobs:
24
- build-and-publish:
25
- name: Build and Publish
26
- runs-on: ubuntu-latest
27
- timeout-minutes: 20
28
- environment: npm-publish
29
- steps:
30
- - name: Harden Runner
31
- uses: step-security/harden-runner@v2
32
- with:
33
- egress-policy: audit
34
-
35
- - uses: actions/checkout@v4
36
- with:
37
- fetch-depth: 0
38
- token: ${{ secrets.GITHUB_TOKEN }}
39
-
40
- - name: Set up Node.js
41
- uses: actions/setup-node@v4
42
- with:
43
- node-version: '22.14.0'
44
- cache: 'npm'
45
- registry-url: 'https://registry.npmjs.org'
46
-
47
- - name: Install npm with trusted publishing support
48
- run: npm install --global npm@11.17.0
49
-
50
- - name: Install dependencies
51
- run: npm ci --prefer-offline --no-audit
52
-
53
- - name: Run tests
54
- run: npm run test:ci
55
-
56
- - name: Run linting
57
- run: npm run lint
58
-
59
- - name: Build project
60
- run: npm run build
61
-
62
- - name: Generate documentation
63
- run: npm run docs
64
-
65
- - name: Configure Git
66
- run: |
67
- git config --global user.name "GitHub Actions"
68
- git config --global user.email "actions@github.com"
69
-
70
- - name: Check if version needs update
71
- id: check_version
72
- run: |
73
- CURRENT_VERSION=$(node -p "require('./package.json').version")
74
- echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
75
-
76
- # Check if this version exists on NPM
77
- if npm view @rahul05ranjan/dhruv-cli@$CURRENT_VERSION version 2>/dev/null; then
78
- echo "needs_version_bump=true" >> $GITHUB_OUTPUT
79
- echo "Version $CURRENT_VERSION already exists on NPM, needs bump"
80
- else
81
- echo "needs_version_bump=false" >> $GITHUB_OUTPUT
82
- echo "Version $CURRENT_VERSION is new, can publish"
83
- fi
84
-
85
- - name: Manual version bump
86
- if: github.event_name == 'workflow_dispatch'
87
- run: |
88
- npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
89
- git add package.json package-lock.json
90
- git commit -m "chore: bump version to $(node -p "require('./package.json').version") [skip ci]"
91
- git push
92
-
93
- - name: Get final version
94
- id: final_version
95
- run: |
96
- VERSION=$(node -p "require('./package.json').version")
97
- echo "version=$VERSION" >> $GITHUB_OUTPUT
98
- echo "Final version to publish: $VERSION"
99
-
100
- - name: Create Git tag
101
- run: |
102
- TAG="v${{ steps.final_version.outputs.version }}"
103
- if git rev-parse "$TAG" >/dev/null 2>&1; then
104
- echo "Tag $TAG already exists, skipping tag creation"
105
- else
106
- git tag "$TAG"
107
- git push origin "$TAG"
108
- fi
109
-
110
- - name: Publish to NPM
111
- # npm 11.5.1+ uses the workflow's OIDC identity without a stored token.
112
- run: |
113
- echo "Publishing version ${{ steps.final_version.outputs.version }}"
114
- npm publish --access public --tag latest --provenance
115
-
116
- - name: Create GitHub Release
117
- uses: softprops/action-gh-release@v2
118
- with:
119
- tag_name: v${{ steps.final_version.outputs.version }}
120
- name: Release v${{ steps.final_version.outputs.version }}
121
- body: |
122
- ## What's Changed
123
-
124
- Version ${{ steps.final_version.outputs.version }} of dhruv-cli
125
-
126
- ### Installation
127
- ```bash
128
- npm install -g @rahul05ranjan/dhruv-cli@${{ steps.final_version.outputs.version }}
129
- ```
130
-
131
- **Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ steps.check_version.outputs.current_version }}...v${{ steps.final_version.outputs.version }}
132
- files: |
133
- dist/**
134
- docs/api/**
135
- draft: false
136
- prerelease: false
137
- env:
138
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
139
-
140
- - name: Update documentation
141
- uses: peaceiris/actions-gh-pages@v4
142
- with:
143
- github_token: ${{ secrets.GITHUB_TOKEN }}
144
- publish_dir: ./docs/api
145
- publish_branch: gh-pages
146
- enable_jekyll: false
147
-
148
- - name: Notify success
149
- if: success()
150
- run: |
151
- echo "✅ Successfully published @rahul05ranjan/dhruv-cli@${{ steps.final_version.outputs.version }}"
152
- echo "📦 NPM: https://www.npmjs.com/package/@rahul05ranjan/dhruv-cli"
153
- echo "📖 Documentation: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}"
154
- echo "🏷️ Release: https://github.com/${{ github.repository }}/releases/tag/v${{ steps.final_version.outputs.version }}"