@rahul05ranjan/dhruv-cli 0.0.0-development

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 (69) hide show
  1. package/.eslintignore +1 -0
  2. package/.eslintrc.cjs +43 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  6. package/.github/auto_assign.yml +10 -0
  7. package/.github/copilot-instructions.md +3 -0
  8. package/.github/dependabot.yml +15 -0
  9. package/.github/labeler.yml +27 -0
  10. package/.github/workflows/auto-assign.yml +14 -0
  11. package/.github/workflows/ci.yml +53 -0
  12. package/.github/workflows/contribution.yml +51 -0
  13. package/.github/workflows/dependabot-auto-merge.yml +19 -0
  14. package/.github/workflows/labeler.yml +13 -0
  15. package/.github/workflows/stale.yml +17 -0
  16. package/.releaserc +12 -0
  17. package/CHANGELOG.md +56 -0
  18. package/CODE_OF_CONDUCT.md +19 -0
  19. package/CONTRIBUTING.md +60 -0
  20. package/README.md +61 -0
  21. package/SECURITY.md +8 -0
  22. package/dist/commands/explain.d.ts +1 -0
  23. package/dist/commands/explain.js +46 -0
  24. package/dist/commands/fix.d.ts +1 -0
  25. package/dist/commands/fix.js +40 -0
  26. package/dist/commands/generate.d.ts +1 -0
  27. package/dist/commands/generate.js +62 -0
  28. package/dist/commands/init.d.ts +1 -0
  29. package/dist/commands/init.js +54 -0
  30. package/dist/commands/menu.d.ts +1 -0
  31. package/dist/commands/menu.js +33 -0
  32. package/dist/commands/optimize.d.ts +1 -0
  33. package/dist/commands/optimize.js +42 -0
  34. package/dist/commands/review.d.ts +1 -0
  35. package/dist/commands/review.js +54 -0
  36. package/dist/commands/security-check.d.ts +1 -0
  37. package/dist/commands/security-check.js +48 -0
  38. package/dist/commands/suggest.d.ts +1 -0
  39. package/dist/commands/suggest.js +41 -0
  40. package/dist/config/config.d.ts +8 -0
  41. package/dist/config/config.js +20 -0
  42. package/dist/core/ai.d.ts +5 -0
  43. package/dist/core/ai.js +43 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +143 -0
  46. package/dist/utils/projectType.d.ts +1 -0
  47. package/dist/utils/projectType.js +17 -0
  48. package/dist/utils/ux.d.ts +5 -0
  49. package/dist/utils/ux.js +44 -0
  50. package/docs/index.html +537 -0
  51. package/node-fetch.d.ts +4 -0
  52. package/package.json +55 -0
  53. package/plugins/hello.js +10 -0
  54. package/src/commands/explain.ts +45 -0
  55. package/src/commands/fix.ts +34 -0
  56. package/src/commands/generate.ts +55 -0
  57. package/src/commands/init.ts +54 -0
  58. package/src/commands/menu.ts +35 -0
  59. package/src/commands/optimize.ts +36 -0
  60. package/src/commands/review.ts +47 -0
  61. package/src/commands/security-check.ts +41 -0
  62. package/src/commands/suggest.ts +35 -0
  63. package/src/config/config.ts +32 -0
  64. package/src/core/ai.test.js +40 -0
  65. package/src/core/ai.ts +41 -0
  66. package/src/index.ts +154 -0
  67. package/src/utils/projectType.ts +14 -0
  68. package/src/utils/ux.ts +50 -0
  69. package/tsconfig.json +17 -0
@@ -0,0 +1,55 @@
1
+ import { askOllama } from '../core/ai.js';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/config.js';
4
+ import fs from 'fs';
5
+ import { highlightCode, printError, printSuccess } from '../utils/ux.js';
6
+
7
+ export async function generate(type: string, target: string) {
8
+ const config = loadConfig();
9
+ let content = '';
10
+ if (fs.existsSync(target)) {
11
+ content = fs.readFileSync(target, 'utf-8');
12
+ }
13
+ let streamed = '';
14
+ try {
15
+ process.stdout.write(chalk.green('Generated code: '));
16
+ await askOllama({
17
+ prompt: `Generate only a valid JavaScript ${type} file for this code, no explanations, no Markdown, just the code.\n${content}`,
18
+ model: config.model,
19
+ onToken: (token: string) => {
20
+ streamed += token;
21
+ process.stdout.write(chalk.cyan(token));
22
+ }
23
+ });
24
+ process.stdout.write('\n');
25
+ // Robust code extraction for tests
26
+ let codeToSave = streamed;
27
+ if (type === 'tests' && target && typeof streamed === 'string') {
28
+ const codeBlockMatch = streamed.match(/```(?:[a-z]*)?\n([\s\S]*?)```/);
29
+ if (codeBlockMatch && codeBlockMatch[1]) {
30
+ codeToSave = codeBlockMatch[1].trim();
31
+ } else {
32
+ // Remove Markdown, explanations, and keep only lines that look like code
33
+ codeToSave = streamed
34
+ .replace(/```[a-z]*\n|```/g, '') // remove code block markers
35
+ .split('\n')
36
+ .filter(line => line.trim() && !/^\s*#|^\s*\/\//.test(line) && /[;{}()=]/.test(line))
37
+ .join('\n')
38
+ .trim();
39
+ }
40
+ const testFile = target.replace(/\.[^.]+$/, '.test.js');
41
+ fs.writeFileSync(testFile, codeToSave);
42
+ printSuccess(`Test file saved: ${testFile}`);
43
+ }
44
+ if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
45
+ const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
46
+ for (const block of codeBlocks) {
47
+ const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
48
+ if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
49
+ }
50
+ }
51
+ } catch (err) {
52
+ printError('Failed to generate code.');
53
+ console.error(chalk.red((err as Error).message));
54
+ }
55
+ }
@@ -0,0 +1,54 @@
1
+ import inquirer from 'inquirer';
2
+ import { saveConfig, loadConfig } from '../config/config.js';
3
+ import chalk from 'chalk';
4
+
5
+ export async function init() {
6
+ const current = loadConfig();
7
+ let modelChoices = [current.model];
8
+ try {
9
+ // Dynamically import node-fetch for compatibility
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const fetch: any = (await import('node-fetch')).default;
12
+ // Fetch models from Ollama REST API
13
+ const res = await fetch('http://localhost:11434/api/tags');
14
+ if (res.ok) {
15
+ const data = (await res.json()) as { models?: { name: string }[] };
16
+ if (Array.isArray(data.models) && data.models.length > 0) {
17
+ modelChoices = data.models.map((m) => m.name);
18
+ }
19
+ }
20
+ } catch {
21
+ // If Ollama is not running or fails, fallback to current model
22
+ }
23
+ const answers = await inquirer.prompt([
24
+ {
25
+ type: 'list',
26
+ name: 'model',
27
+ message: 'Which Ollama model do you want to use?',
28
+ choices: modelChoices,
29
+ default: current.model,
30
+ },
31
+ {
32
+ type: 'list',
33
+ name: 'responseFormat',
34
+ message: 'Preferred response format?',
35
+ choices: ['text', 'json', 'markdown'],
36
+ default: current.responseFormat,
37
+ },
38
+ {
39
+ type: 'confirm',
40
+ name: 'verbose',
41
+ message: 'Enable verbose output?',
42
+ default: current.verbose,
43
+ },
44
+ {
45
+ type: 'list',
46
+ name: 'theme',
47
+ message: 'Choose a color theme:',
48
+ choices: ['default', 'dark', 'light', 'mono'],
49
+ default: current.theme || 'default',
50
+ },
51
+ ]);
52
+ saveConfig(answers);
53
+ console.log(chalk.green('Configuration saved!'));
54
+ }
@@ -0,0 +1,35 @@
1
+ import inquirer from 'inquirer';
2
+ import { themed } from '../utils/ux.js';
3
+
4
+ const commands = [
5
+ { name: 'Explain', value: 'explain' },
6
+ { name: 'Suggest', value: 'suggest' },
7
+ { name: 'Fix', value: 'fix' },
8
+ { name: 'Review', value: 'review' },
9
+ { name: 'Optimize', value: 'optimize' },
10
+ { name: 'Security Check', value: 'security-check' },
11
+ { name: 'Generate', value: 'generate' },
12
+ { name: 'Init (Setup)', value: 'init' },
13
+ { name: 'Project Type', value: 'project-type' },
14
+ { name: 'Exit', value: 'exit' }
15
+ ];
16
+
17
+ export async function menu() {
18
+ let running = true;
19
+ while (running) {
20
+ const { cmd } = await inquirer.prompt([
21
+ {
22
+ type: 'list',
23
+ name: 'cmd',
24
+ message: themed('What do you want to do?', 'primary'),
25
+ choices: commands
26
+ }
27
+ ]);
28
+ if (cmd === 'exit') {
29
+ running = false;
30
+ break;
31
+ }
32
+ // For demo, just print the command. In real use, you would call the command handler.
33
+ console.log(themed(`You selected: ${cmd}`, 'accent'));
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ import { askOllama } from '../core/ai.js';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/config.js';
4
+ import fs from 'fs';
5
+ import { highlightCode, printError } from '../utils/ux.js';
6
+
7
+ export async function optimize(file: string) {
8
+ const config = loadConfig();
9
+ let content = '';
10
+ if (fs.existsSync(file)) {
11
+ content = fs.readFileSync(file, 'utf-8');
12
+ }
13
+ let streamed = '';
14
+ try {
15
+ process.stdout.write(chalk.green('Optimization suggestion: '));
16
+ await askOllama({
17
+ prompt: `Optimize this file:\n${content}`,
18
+ model: config.model,
19
+ onToken: (token: string) => {
20
+ streamed += token;
21
+ process.stdout.write(chalk.cyan(token));
22
+ }
23
+ });
24
+ process.stdout.write('\n');
25
+ if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
26
+ const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
27
+ for (const block of codeBlocks) {
28
+ const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
29
+ if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
30
+ }
31
+ }
32
+ } catch (err) {
33
+ printError('Failed to optimize.');
34
+ console.error(chalk.red((err as Error).message));
35
+ }
36
+ }
@@ -0,0 +1,47 @@
1
+ import { askOllama } from '../core/ai.js';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/config.js';
4
+ import fs from 'fs';
5
+ import { highlightCode, printError, createProgressBar } from '../utils/ux.js';
6
+
7
+ export async function review(fileOrDir: string) {
8
+ const config = loadConfig();
9
+ let code = '';
10
+ if (fs.existsSync(fileOrDir)) {
11
+ const stat = fs.statSync(fileOrDir);
12
+ if (stat.isDirectory()) {
13
+ const files = fs.readdirSync(fileOrDir);
14
+ const bar = createProgressBar(files.length);
15
+ for (const f of files) {
16
+ code += fs.readFileSync(`${fileOrDir}/${f}`,'utf-8') + '\n';
17
+ bar.increment();
18
+ }
19
+ bar.stop();
20
+ } else {
21
+ code = fs.readFileSync(fileOrDir, 'utf-8');
22
+ }
23
+ }
24
+ let streamed = '';
25
+ try {
26
+ process.stdout.write(chalk.green('Review: '));
27
+ await askOllama({
28
+ prompt: `Review this code:\n${code}`,
29
+ model: config.model,
30
+ onToken: (token: string) => {
31
+ streamed += token;
32
+ process.stdout.write(chalk.cyan(token));
33
+ }
34
+ });
35
+ process.stdout.write('\n');
36
+ if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
37
+ const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
38
+ for (const block of codeBlocks) {
39
+ const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
40
+ if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
41
+ }
42
+ }
43
+ } catch (err) {
44
+ printError('Failed to review code.');
45
+ console.error(chalk.red((err as Error).message));
46
+ }
47
+ }
@@ -0,0 +1,41 @@
1
+ import { askOllama } from '../core/ai.js';
2
+ import chalk from 'chalk';
3
+ import { loadConfig } from '../config/config.js';
4
+ import fs from 'fs';
5
+ import { highlightCode, printError } from '../utils/ux.js';
6
+
7
+ export async function securityCheck(fileOrDir: string = '.') {
8
+ const config = loadConfig();
9
+ let code = '';
10
+ if (fs.existsSync(fileOrDir)) {
11
+ const stat = fs.statSync(fileOrDir);
12
+ if (stat.isDirectory()) {
13
+ code = fs.readdirSync(fileOrDir).map(f => fs.readFileSync(`${fileOrDir}/${f}`,'utf-8')).join('\n');
14
+ } else {
15
+ code = fs.readFileSync(fileOrDir, 'utf-8');
16
+ }
17
+ }
18
+ let streamed = '';
19
+ try {
20
+ process.stdout.write(chalk.green('Security check result: '));
21
+ await askOllama({
22
+ prompt: `Security check for this code:\n${code}`,
23
+ model: config.model,
24
+ onToken: (token: string) => {
25
+ streamed += token;
26
+ process.stdout.write(chalk.cyan(token));
27
+ }
28
+ });
29
+ process.stdout.write('\n');
30
+ if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
31
+ const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
32
+ for (const block of codeBlocks) {
33
+ const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
34
+ if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
35
+ }
36
+ }
37
+ } catch (err) {
38
+ printError('Failed to run security check.');
39
+ console.error(chalk.red((err as Error).message));
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ import { askOllama } from '../core/ai.js';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { loadConfig } from '../config/config.js';
5
+ import { highlightCode, printError } from '../utils/ux.js';
6
+
7
+ export async function suggest(query: string) {
8
+ const config = loadConfig();
9
+ const spinner = ora('Generating suggestions...').start();
10
+ let streamed = '';
11
+ try {
12
+ spinner.stop();
13
+ process.stdout.write(chalk.green('Suggestions: '));
14
+ await askOllama({
15
+ prompt: `Suggest: ${query}`,
16
+ model: config.model,
17
+ onToken: (token: string) => {
18
+ streamed += token;
19
+ process.stdout.write(chalk.cyan(token));
20
+ }
21
+ });
22
+ process.stdout.write('\n');
23
+ if (typeof highlightCode === 'function' && streamed.match(/```[a-z]*[\s\S]*?```/)) {
24
+ // Highlight code blocks if present
25
+ const codeBlocks = streamed.match(/```([a-z]*)\n([\s\S]*?)```/g) || [];
26
+ for (const block of codeBlocks) {
27
+ const [, lang, code] = block.match(/```([a-z]*)\n([\s\S]*?)```/) || [];
28
+ if (code) try { console.log(highlightCode(code, lang || 'js')); } catch (err) { console.error('Highlight error:', err); }
29
+ }
30
+ }
31
+ } catch (err) {
32
+ printError('Failed to get suggestions.');
33
+ console.error(chalk.red((err as Error).message));
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const CONFIG_FILE = path.join(process.cwd(), '.dhruv-config.json');
5
+
6
+ export interface DhruvConfig {
7
+ model: string;
8
+ verbose: boolean;
9
+ responseFormat: 'text' | 'json' | 'markdown';
10
+ theme?: 'default' | 'dark' | 'light' | 'mono';
11
+ }
12
+
13
+ const defaultConfig: DhruvConfig = {
14
+ model: 'codellama',
15
+ verbose: false,
16
+ responseFormat: 'text',
17
+ theme: 'default',
18
+ };
19
+
20
+ export function loadConfig(): DhruvConfig {
21
+ if (fs.existsSync(CONFIG_FILE)) {
22
+ return { ...defaultConfig, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')) };
23
+ }
24
+ return defaultConfig;
25
+ }
26
+
27
+ export function saveConfig(config: Partial<DhruvConfig>) {
28
+ const current = loadConfig();
29
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2));
30
+ }
31
+
32
+ // Use .js extension for ESM compatibility if imported elsewhere
@@ -0,0 +1,40 @@
1
+ /* global process */
2
+ import { Ollama } from 'ollama';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import fs from 'fs';
6
+
7
+ const CACHE_DIR = path.join(process.cwd(), '.dhruv-cache');
8
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR);
9
+
10
+ function getCacheKey(prompt, model) {
11
+ const hash = crypto.createHash('sha256').update(`${model || 'default'}:${prompt}`).digest('hex');
12
+ return path.join(CACHE_DIR, hash);
13
+ }
14
+
15
+ export async function askOllama({ prompt, model, onToken }) {
16
+ const cacheKey = getCacheKey(prompt, model);
17
+ if (fs.existsSync(cacheKey)) {
18
+ const cached = fs.readFileSync(cacheKey, 'utf-8');
19
+ if (onToken) onToken(cached);
20
+ return cached;
21
+ }
22
+ try {
23
+ let result = '';
24
+ const ollama = new Ollama();
25
+ for await (const chunk of ollama.generate(model || 'codellama', prompt)) {
26
+ let token = '';
27
+ if (typeof chunk === 'object' && chunk !== null && 'response' in chunk) {
28
+ token = chunk.response;
29
+ } else if (typeof chunk === 'string') {
30
+ token = chunk;
31
+ }
32
+ if (onToken) onToken(token);
33
+ result += token;
34
+ }
35
+ fs.writeFileSync(cacheKey, result.trim());
36
+ return result.trim();
37
+ } catch (err) {
38
+ throw new Error('Ollama AI error: ' + (err && err.message ? err.message : err));
39
+ }
40
+ }
package/src/core/ai.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { Ollama } from 'ollama';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+
6
+ const CACHE_DIR = path.join(process.cwd(), '.dhruv-cache');
7
+ if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR);
8
+
9
+ function getCacheKey(prompt: string, model?: string) {
10
+ const hash = crypto.createHash('sha256').update(`${model || 'default'}:${prompt}`).digest('hex');
11
+ return path.join(CACHE_DIR, hash);
12
+ }
13
+
14
+ export async function askOllama({ prompt, model, onToken }: { prompt: string; model?: string; onToken?: (token: string) => void }) {
15
+ const cacheKey = getCacheKey(prompt, model);
16
+ if (fs.existsSync(cacheKey)) {
17
+ const cached = fs.readFileSync(cacheKey, 'utf-8');
18
+ if (onToken) onToken(cached);
19
+ return cached;
20
+ }
21
+ try {
22
+ let result = '';
23
+ const ollama = new Ollama();
24
+ // Await the iterator, then stream
25
+ const iterator = await ollama.generate({ model: model || 'codellama', prompt, stream: true });
26
+ for await (const chunk of iterator) {
27
+ let token = '';
28
+ if (typeof chunk === 'object' && chunk !== null && 'response' in chunk) {
29
+ token = (chunk as { response: string }).response;
30
+ } else if (typeof chunk === 'string') {
31
+ token = chunk;
32
+ }
33
+ if (onToken) onToken(token);
34
+ result += token;
35
+ }
36
+ fs.writeFileSync(cacheKey, result.trim());
37
+ return result.trim();
38
+ } catch (err) {
39
+ throw new Error('Ollama AI error: ' + (err as Error).message);
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { explain } from './commands/explain.js';
7
+ import { suggest } from './commands/suggest.js';
8
+ import { fix } from './commands/fix.js';
9
+ import { review } from './commands/review.js';
10
+ import { optimize } from './commands/optimize.js';
11
+ import { securityCheck } from './commands/security-check.js';
12
+ import { generate } from './commands/generate.js';
13
+ import { init } from './commands/init.js';
14
+ import { detectProjectType } from './utils/projectType.js';
15
+ import { menu } from './commands/menu.js';
16
+ import { createRequire } from 'module';
17
+ const require = createRequire(import.meta.url);
18
+ const pkg = require('../package.json');
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('dhruv')
24
+ .description('AI-powered CLI assistant for developers using Ollama')
25
+ .version(pkg.version);
26
+
27
+ program
28
+ .command('explain <query>')
29
+ .description('Explain a concept or command')
30
+ .action(explain);
31
+
32
+ program
33
+ .command('suggest <query>')
34
+ .description('Get AI-powered suggestions')
35
+ .action(suggest);
36
+
37
+ program
38
+ .command('fix <query>')
39
+ .description('Get a fix for a coding issue or error')
40
+ .action(fix);
41
+
42
+ program
43
+ .command('review <fileOrDir>')
44
+ .description('Review code in a file or directory')
45
+ .action(review);
46
+
47
+ program
48
+ .command('optimize <file>')
49
+ .description('Optimize a file (e.g., package.json)')
50
+ .action(optimize);
51
+
52
+ program
53
+ .command('security-check [fileOrDir]')
54
+ .description('Run a security check on code')
55
+ .action(securityCheck);
56
+
57
+ program
58
+ .command('generate <type> <target>')
59
+ .description('Generate code/tests for a file')
60
+ .action(generate);
61
+
62
+ program
63
+ .command('init')
64
+ .description('Interactive setup/configuration wizard')
65
+ .action(init);
66
+
67
+ program
68
+ .command('project-type')
69
+ .description('Detect and print the current project type')
70
+ .action(() => {
71
+ const type = detectProjectType();
72
+ console.log(chalk.blue(`Detected project type: ${type}`));
73
+ });
74
+
75
+ program
76
+ .command('menu')
77
+ .description('Interactive command palette')
78
+ .action(menu);
79
+
80
+ program
81
+ .option('--model <model>', 'Set Ollama model')
82
+ .option('--verbose', 'Enable verbose output')
83
+ .option('--json', 'Output in JSON format')
84
+ .hook('preAction', async (thisCommand) => {
85
+ const opts = thisCommand.opts();
86
+ if (opts.model || opts.verbose || opts.json) {
87
+ const config: Record<string, unknown> = {};
88
+ if (opts.model) config.model = opts.model;
89
+ if (opts.verbose) config.verbose = true;
90
+ if (opts.json) config.responseFormat = 'json';
91
+ // Save config for session
92
+ const configModule = await import('./config/config.js');
93
+ configModule.saveConfig(config);
94
+ }
95
+ });
96
+
97
+ async function loadPlugins(program: unknown) {
98
+ const PLUGIN_DIR = path.join(process.cwd(), 'plugins');
99
+ if (fs.existsSync(PLUGIN_DIR)) {
100
+ const files = fs.readdirSync(PLUGIN_DIR).filter(f => f.endsWith('.js'));
101
+ for (const file of files) {
102
+ try {
103
+ const pluginPath = path.join(PLUGIN_DIR, file).replace(/\\/g, '/');
104
+ const pluginUrl = new URL('file://' + (pluginPath.startsWith('/') ? '' : '/') + pluginPath);
105
+ const plugin = await import(pluginUrl.href);
106
+ if (typeof plugin.default === 'function') plugin.default(program);
107
+ else if (typeof plugin === 'function') plugin(program);
108
+ } catch (e) {
109
+ console.error(chalk.red(`Failed to load plugin ${file}: ${(e as Error).message}`));
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ (async () => {
116
+ await loadPlugins(program);
117
+ program.parse(process.argv);
118
+ })();
119
+
120
+ // Autocomplete: Generate shell completion scripts
121
+ program
122
+ .command('completion')
123
+ .description('Generate shell completion script')
124
+ .argument('[shell]', 'shell type (bash|zsh|fish)', 'bash')
125
+ .action((shell: string) => {
126
+ let script = '';
127
+ switch (shell) {
128
+ case 'zsh':
129
+ script = `#compdef dhruv\n_dhruv_completion() {\n reply=( $(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}') )\n}\ncompctl -K _dhruv_completion dhruv`;
130
+ break;
131
+ case 'fish':
132
+ script = `function __fish_dhruv_complete\n dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}'\nend\ncomplete -c dhruv -a '(__fish_dhruv_complete)'`;
133
+ break;
134
+ default:
135
+ script = String.raw`#!/bin/bash
136
+ _dhruv_completion() {
137
+ COMPREPLY=( $(compgen -W "$(dhruv --help | awk '/Commands:/,/^$/ {if(NR>1)print $1}')" -- \${COMP_WORDS[1]}) )
138
+ }
139
+ complete -F _dhruv_completion dhruv`;
140
+ }
141
+ console.log(script);
142
+ console.log(`\n# To enable tab completion, add the above to your shell profile or source it directly.`);
143
+ });
144
+
145
+ process.on('uncaughtException', async (err) => {
146
+ const ux = await import('./utils/ux.js');
147
+ ux.printError('Uncaught error: ' + err.message);
148
+ process.exit(1);
149
+ });
150
+ process.on('unhandledRejection', async (reason: unknown) => {
151
+ const ux = await import('./utils/ux.js');
152
+ ux.printError('Unhandled rejection: ' + ((reason as Error)?.message || reason));
153
+ process.exit(1);
154
+ });
@@ -0,0 +1,14 @@
1
+ // Use .js extension for ESM compatibility
2
+ import fs from 'fs';
3
+
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';
10
+ }
11
+ if (fs.existsSync('requirements.txt')) return 'python';
12
+ if (fs.existsSync('pyproject.toml')) return 'python';
13
+ return 'unknown';
14
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+ // @ts-expect-error: cli-progress has no type definitions
3
+ import cliProgress from 'cli-progress';
4
+ import { highlight } from 'cli-highlight';
5
+ import { loadConfig } from '../config/config.js';
6
+
7
+ function getTheme() {
8
+ const config = loadConfig();
9
+ switch (config.theme) {
10
+ case 'dark':
11
+ return { primary: chalk.cyanBright, accent: chalk.magentaBright, error: chalk.redBright, success: chalk.greenBright };
12
+ case 'light':
13
+ return { primary: chalk.blue, accent: chalk.yellow, error: chalk.red, success: chalk.green };
14
+ case 'mono':
15
+ return { primary: chalk.white, accent: chalk.gray, error: chalk.white.bgRed, success: chalk.white.bgGreen };
16
+ default:
17
+ return { primary: chalk.cyan, accent: chalk.green, error: chalk.bgRed.white, success: chalk.bgGreen.white };
18
+ }
19
+ }
20
+
21
+ export function highlightCode(code: string, lang = 'js') {
22
+ return highlight(code, { language: lang, ignoreIllegals: true });
23
+ }
24
+
25
+ export function printError(message: string) {
26
+ const { error } = getTheme();
27
+ console.error(error(' ERROR '), error(message));
28
+ }
29
+
30
+ export function printSuccess(message: string) {
31
+ const { success } = getTheme();
32
+ console.log(success(' SUCCESS '), success(message));
33
+ }
34
+
35
+ export function createProgressBar(total: number) {
36
+ const { primary } = getTheme();
37
+ const bar = new cliProgress.SingleBar({
38
+ format: primary('Progress') + ' |{bar}| {percentage}% | {value}/{total}',
39
+ barCompleteChar: '\u2588',
40
+ barIncompleteChar: '\u2591',
41
+ hideCursor: true
42
+ });
43
+ bar.start(total, 0);
44
+ return bar;
45
+ }
46
+
47
+ export function themed(text: string, type: 'primary' | 'accent' = 'primary') {
48
+ const theme = getTheme();
49
+ return theme[type](text);
50
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "esModuleInterop": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true
14
+ },
15
+ "include": ["src/**/*", "node-fetch.d.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }