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