@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.
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +43 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/auto_assign.yml +10 -0
- package/.github/copilot-instructions.md +3 -0
- package/.github/dependabot.yml +15 -0
- package/.github/labeler.yml +27 -0
- package/.github/workflows/auto-assign.yml +14 -0
- package/.github/workflows/ci.yml +53 -0
- package/.github/workflows/contribution.yml +51 -0
- package/.github/workflows/dependabot-auto-merge.yml +19 -0
- package/.github/workflows/labeler.yml +13 -0
- package/.github/workflows/stale.yml +17 -0
- package/.releaserc +12 -0
- package/CHANGELOG.md +56 -0
- package/CODE_OF_CONDUCT.md +19 -0
- package/CONTRIBUTING.md +60 -0
- package/README.md +61 -0
- package/SECURITY.md +8 -0
- package/dist/commands/explain.d.ts +1 -0
- package/dist/commands/explain.js +46 -0
- package/dist/commands/fix.d.ts +1 -0
- package/dist/commands/fix.js +40 -0
- package/dist/commands/generate.d.ts +1 -0
- package/dist/commands/generate.js +62 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -0
- package/dist/commands/menu.d.ts +1 -0
- package/dist/commands/menu.js +33 -0
- package/dist/commands/optimize.d.ts +1 -0
- package/dist/commands/optimize.js +42 -0
- package/dist/commands/review.d.ts +1 -0
- package/dist/commands/review.js +54 -0
- package/dist/commands/security-check.d.ts +1 -0
- package/dist/commands/security-check.js +48 -0
- package/dist/commands/suggest.d.ts +1 -0
- package/dist/commands/suggest.js +41 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +20 -0
- package/dist/core/ai.d.ts +5 -0
- package/dist/core/ai.js +43 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +143 -0
- package/dist/utils/projectType.d.ts +1 -0
- package/dist/utils/projectType.js +17 -0
- package/dist/utils/ux.d.ts +5 -0
- package/dist/utils/ux.js +44 -0
- package/docs/index.html +537 -0
- package/node-fetch.d.ts +4 -0
- package/package.json +55 -0
- package/plugins/hello.js +10 -0
- package/src/commands/explain.ts +45 -0
- package/src/commands/fix.ts +34 -0
- package/src/commands/generate.ts +55 -0
- package/src/commands/init.ts +54 -0
- package/src/commands/menu.ts +35 -0
- package/src/commands/optimize.ts +36 -0
- package/src/commands/review.ts +47 -0
- package/src/commands/security-check.ts +41 -0
- package/src/commands/suggest.ts +35 -0
- package/src/config/config.ts +32 -0
- package/src/core/ai.test.js +40 -0
- package/src/core/ai.ts +41 -0
- package/src/index.ts +154 -0
- package/src/utils/projectType.ts +14 -0
- package/src/utils/ux.ts +50 -0
- 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
|
package/dist/core/ai.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
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;
|
package/dist/utils/ux.js
ADDED
|
@@ -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
|
+
}
|