@mattgraba/dev-toolkit 2.0.1
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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/bin/devtk.js +5 -0
- package/cli/cli.js +36 -0
- package/cli/commands/analyzeCommand.js +21 -0
- package/cli/commands/analyzeHandlers.js +55 -0
- package/cli/commands/configCommand.js +33 -0
- package/cli/commands/configHandlers.js +96 -0
- package/cli/commands/explainCommand.js +21 -0
- package/cli/commands/explainHandlers.js +48 -0
- package/cli/commands/fixCommand.js +23 -0
- package/cli/commands/fixHandlers.js +54 -0
- package/cli/commands/generateCommand.js +19 -0
- package/cli/commands/generateHandlers.js +38 -0
- package/cli/commands/historyCommand.js +14 -0
- package/cli/commands/historyHandlers.js +56 -0
- package/cli/commands/loginCommand.js +13 -0
- package/cli/commands/loginHandlers.js +48 -0
- package/cli/commands/scaffoldCommand.js +18 -0
- package/cli/commands/scaffoldHandlers.js +38 -0
- package/cli/commands/terminalCommand.js +21 -0
- package/cli/commands/terminalHandlers.js +52 -0
- package/cli/services/localOpenAI.js +140 -0
- package/cli/utils/commandRunner.js +55 -0
- package/cli/utils/configManager.js +144 -0
- package/cli/utils/contextHandlerWrapper.js +32 -0
- package/cli/utils/errorHandler.js +20 -0
- package/cli/utils/fileScanner.js +107 -0
- package/cli/utils/formatBox.js +146 -0
- package/cli/utils/fsUtils.js +13 -0
- package/cli/utils/historySaver.js +24 -0
- package/cli/utils/localHistory.js +44 -0
- package/cli/utils/promptPassword.js +74 -0
- package/package.json +70 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import handleHistory from './historyHandlers.js';
|
|
2
|
+
|
|
3
|
+
export default (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command('history')
|
|
6
|
+
.alias('h')
|
|
7
|
+
.description('View your saved command output history')
|
|
8
|
+
.usage('[--userId <id>]')
|
|
9
|
+
.option('--userId <id>', 'Optional user ID filter for history')
|
|
10
|
+
.showHelpAfterError(true)
|
|
11
|
+
.action((options) => {
|
|
12
|
+
handleHistory(options);
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
|
|
5
|
+
import handleCliError from '../utils/errorHandler.js';
|
|
6
|
+
import { getToken, apiEndpoint } from '../utils/configManager.js';
|
|
7
|
+
import { readLocalHistory, HISTORY_FILE } from '../utils/localHistory.js';
|
|
8
|
+
|
|
9
|
+
function printEntries(entries) {
|
|
10
|
+
entries.forEach((entry, i) => {
|
|
11
|
+
console.log(chalk.cyan(`\nš Entry ${i + 1}: [${entry.command}]`));
|
|
12
|
+
console.log(chalk.gray(`> Input:\n${entry.input}`));
|
|
13
|
+
console.log(chalk.green(`> Output:\n${entry.output}`));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function handleHistory() {
|
|
18
|
+
const token = getToken();
|
|
19
|
+
|
|
20
|
+
// BYOK / not logged in: read the local history file
|
|
21
|
+
if (!token) {
|
|
22
|
+
const entries = readLocalHistory();
|
|
23
|
+
if (!entries.length) {
|
|
24
|
+
console.log(chalk.yellow('\nš No saved history found.'));
|
|
25
|
+
console.log(chalk.dim(`Local history is saved to ${HISTORY_FILE} as you run commands.\n`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.log(chalk.dim(`\nLocal history (${HISTORY_FILE}):`));
|
|
29
|
+
printEntries(entries);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Hosted mode: fetch from the server
|
|
34
|
+
let spinner;
|
|
35
|
+
try {
|
|
36
|
+
spinner = ora('Fetching your history...').start();
|
|
37
|
+
|
|
38
|
+
const res = await axios.get(apiEndpoint('/history'), {
|
|
39
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
spinner.succeed('History retrieved ā
');
|
|
43
|
+
|
|
44
|
+
const entries = res.data;
|
|
45
|
+
if (!entries.length) {
|
|
46
|
+
console.log(chalk.yellow('\nš No saved history found.\n'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
printEntries(entries);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
handleCliError(spinner, err, 'Failed to fetch history');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default handleHistory;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import handleLogin from './loginHandlers.js';
|
|
2
|
+
|
|
3
|
+
export default (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command('login')
|
|
6
|
+
.alias('l')
|
|
7
|
+
.description('Authenticate with your user ID to enable secured commands')
|
|
8
|
+
.usage('')
|
|
9
|
+
.showHelpAfterError(true)
|
|
10
|
+
.action(() => {
|
|
11
|
+
handleLogin();
|
|
12
|
+
});
|
|
13
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import handleCliError from '../utils/errorHandler.js';
|
|
6
|
+
import promptPassword from '../utils/promptPassword.js';
|
|
7
|
+
import { apiEndpoint, setToken } from '../utils/configManager.js';
|
|
8
|
+
|
|
9
|
+
async function handleLogin() {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let spinner;
|
|
16
|
+
try {
|
|
17
|
+
console.log(chalk.cyan('\nš Dev Toolkit Login\n'));
|
|
18
|
+
|
|
19
|
+
const username = await new Promise(resolve =>
|
|
20
|
+
rl.question(chalk.white('Username: '), answer => resolve(answer.trim()))
|
|
21
|
+
);
|
|
22
|
+
rl.close();
|
|
23
|
+
|
|
24
|
+
// Masked input ā the password is never echoed to the terminal
|
|
25
|
+
const password = (await promptPassword(chalk.white('Password: '))).trim();
|
|
26
|
+
|
|
27
|
+
if (!username || !password) {
|
|
28
|
+
console.error(chalk.red('ā Username and password are required.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
spinner = ora('Logging in...').start();
|
|
33
|
+
|
|
34
|
+
const res = await axios.post(apiEndpoint('/auth/login'), { username, password });
|
|
35
|
+
setToken(res.data.token);
|
|
36
|
+
|
|
37
|
+
spinner.succeed(chalk.green('Login successful! Token saved.'));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (spinner) {
|
|
40
|
+
handleCliError(spinner, err, 'Login failed');
|
|
41
|
+
} else {
|
|
42
|
+
console.error(chalk.red('ā Error:'), err.response?.data?.error || err.message);
|
|
43
|
+
}
|
|
44
|
+
rl.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default handleLogin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { handleScaffoldBasic } from './scaffoldHandlers.js';
|
|
2
|
+
|
|
3
|
+
export default (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command('scaffold')
|
|
6
|
+
.alias('s')
|
|
7
|
+
.description('Scaffold a basic project component or template')
|
|
8
|
+
.usage('-n <name> [--output <path>]')
|
|
9
|
+
.requiredOption('-n, --name <text>', 'Name of the component or feature')
|
|
10
|
+
.option('--output <path>', 'Optional file path to save scaffolded output')
|
|
11
|
+
.showHelpAfterError(true)
|
|
12
|
+
.action((options) => {
|
|
13
|
+
const { output, ...rest } = options;
|
|
14
|
+
const fixedOptions = { ...rest, outputPath: output };
|
|
15
|
+
handleScaffoldBasic(fixedOptions);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
import saveToHistory from '../utils/historySaver.js';
|
|
6
|
+
import runAICommand from '../utils/commandRunner.js';
|
|
7
|
+
import { scaffoldCode as scaffoldCodeLocal } from '../services/localOpenAI.js';
|
|
8
|
+
|
|
9
|
+
async function handleScaffoldBasic({ name, outputPath }) {
|
|
10
|
+
const data = await runAICommand({
|
|
11
|
+
spinnerText: `Scaffolding ${name}...`,
|
|
12
|
+
successText: 'Scaffold complete ā
',
|
|
13
|
+
failText: 'Scaffold failed',
|
|
14
|
+
localFn: () => scaffoldCodeLocal({ name }),
|
|
15
|
+
endpoint: '/scaffold',
|
|
16
|
+
payload: { name },
|
|
17
|
+
});
|
|
18
|
+
if (!data) return;
|
|
19
|
+
|
|
20
|
+
const { scaffoldCode } = data;
|
|
21
|
+
if (outputPath) {
|
|
22
|
+
const fullPath = path.resolve(outputPath);
|
|
23
|
+
fs.writeFileSync(fullPath, scaffoldCode, 'utf-8');
|
|
24
|
+
console.log(chalk.blue(`\nā
Saved to ${fullPath}`));
|
|
25
|
+
} else {
|
|
26
|
+
console.log(chalk.green('\nš§± Scaffolded Code:\n'), scaffoldCode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await saveToHistory({
|
|
30
|
+
command: 'scaffold',
|
|
31
|
+
input: name,
|
|
32
|
+
output: scaffoldCode,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
handleScaffoldBasic,
|
|
38
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { handleTerminalBasic, handleTerminalWithContext } from './terminalHandlers.js';
|
|
2
|
+
import handleWithContext from '../utils/contextHandlerWrapper.js';
|
|
3
|
+
|
|
4
|
+
export default (program) => {
|
|
5
|
+
program
|
|
6
|
+
.command('terminal')
|
|
7
|
+
.alias('t')
|
|
8
|
+
.description('Get terminal commands to set up or debug your project')
|
|
9
|
+
.usage('--goal <text> [--context-text <text>] [--context]')
|
|
10
|
+
.requiredOption('-g, --goal <text>', 'Project goal or setup intention')
|
|
11
|
+
.option('--context-text <text>', 'Optional plain text context (e.g. dependencies)')
|
|
12
|
+
.option('--context', 'Include full file context from the project')
|
|
13
|
+
.showHelpAfterError(true)
|
|
14
|
+
.action((options) => {
|
|
15
|
+
handleWithContext({
|
|
16
|
+
options,
|
|
17
|
+
handleBasic: handleTerminalBasic,
|
|
18
|
+
handleWithContext: handleTerminalWithContext,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { scanFiles } from '../utils/fileScanner.js';
|
|
4
|
+
import saveToHistory from '../utils/historySaver.js';
|
|
5
|
+
import runAICommand from '../utils/commandRunner.js';
|
|
6
|
+
import { getTerminalCommands } from '../services/localOpenAI.js';
|
|
7
|
+
|
|
8
|
+
function stripCodeFences(commands) {
|
|
9
|
+
return commands.replace(/```[a-zA-Z]*\n?/, '').replace(/```$/, '').trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function runTerminal({ goal, contextText = '', contextFiles = [] }) {
|
|
13
|
+
const data = await runAICommand({
|
|
14
|
+
spinnerText: `Getting terminal commands${contextFiles.length ? ' with context' : ''}...`,
|
|
15
|
+
successText: 'Terminal generation complete ā
',
|
|
16
|
+
failText: 'Terminal command generation failed',
|
|
17
|
+
localFn: () => getTerminalCommands({ goal, context: contextText, contextFiles }),
|
|
18
|
+
endpoint: '/terminal',
|
|
19
|
+
payload: { goal, context: contextText, contextFiles },
|
|
20
|
+
});
|
|
21
|
+
if (!data) return;
|
|
22
|
+
|
|
23
|
+
if (!data.commands) {
|
|
24
|
+
console.error(chalk.red('ā No terminal commands returned'));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const commands = stripCodeFences(data.commands);
|
|
29
|
+
|
|
30
|
+
console.log(chalk.green('\nš» Suggested terminal commands:\n'));
|
|
31
|
+
console.log(commands);
|
|
32
|
+
|
|
33
|
+
await saveToHistory({
|
|
34
|
+
command: 'terminal',
|
|
35
|
+
input: goal,
|
|
36
|
+
output: commands,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleTerminalBasic({ goal, contextText }) {
|
|
41
|
+
await runTerminal({ goal, contextText });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleTerminalWithContext({ goal, contextText }) {
|
|
45
|
+
const contextFiles = await scanFiles({ directory: '.' });
|
|
46
|
+
await runTerminal({ goal, contextText, contextFiles });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
handleTerminalBasic,
|
|
51
|
+
handleTerminalWithContext,
|
|
52
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { OpenAI } from 'openai';
|
|
2
|
+
import { getOpenAIKey } from '../utils/configManager.js';
|
|
3
|
+
|
|
4
|
+
let openaiClient = null;
|
|
5
|
+
|
|
6
|
+
function getClient() {
|
|
7
|
+
if (!openaiClient) {
|
|
8
|
+
const apiKey = getOpenAIKey();
|
|
9
|
+
if (!apiKey) {
|
|
10
|
+
throw new Error('No OpenAI API key configured. Run: devtk config set-key <your-key>');
|
|
11
|
+
}
|
|
12
|
+
openaiClient = new OpenAI({ apiKey });
|
|
13
|
+
}
|
|
14
|
+
return openaiClient;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resetClient() {
|
|
18
|
+
openaiClient = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function sendPrompt(prompt, systemPrompt = 'You are a senior software engineer.') {
|
|
22
|
+
const completion = await getClient().chat.completions.create({
|
|
23
|
+
model: 'gpt-4',
|
|
24
|
+
messages: [
|
|
25
|
+
{ role: 'system', content: systemPrompt },
|
|
26
|
+
{ role: 'user', content: prompt },
|
|
27
|
+
],
|
|
28
|
+
temperature: 0.7,
|
|
29
|
+
max_tokens: 1500,
|
|
30
|
+
});
|
|
31
|
+
return completion.choices[0].message.content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function analyzeCode({ errorText, language = 'JavaScript', contextFiles = [] }) {
|
|
35
|
+
const contextText = contextFiles.map(f => `// ${f.name}\n${f.content}`).join('\n\n');
|
|
36
|
+
|
|
37
|
+
const prompt = `Analyze the following ${language} code for bugs, errors, and issues.
|
|
38
|
+
|
|
39
|
+
\`\`\`${language}
|
|
40
|
+
${errorText}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
${contextText ? `Context files:\n\n${contextText}\n` : ''}
|
|
44
|
+
Respond ONLY with valid JSON in this exact format (no markdown fences, no extra text):
|
|
45
|
+
{
|
|
46
|
+
"issues": [
|
|
47
|
+
{ "line": <line_number_or_null>, "title": "<short title>", "detail": "<1-2 sentence explanation>" }
|
|
48
|
+
],
|
|
49
|
+
"suggestion": "<concise fix recommendation covering all issues>"
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
const fullResponse = await sendPrompt(prompt, 'You are a senior software engineer. Always respond with valid JSON only.');
|
|
53
|
+
return { raw: fullResponse };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function explainCode({ codeSnippet, language = 'JavaScript', contextFiles = [] }) {
|
|
57
|
+
const contextText = contextFiles.map(f => `// ${f.name}\n${f.content}`).join('\n\n');
|
|
58
|
+
|
|
59
|
+
const prompt = `
|
|
60
|
+
You are an expert software engineer.
|
|
61
|
+
Explain the following ${language} code snippet line by line in beginner-friendly terms:
|
|
62
|
+
|
|
63
|
+
\`\`\`${language}
|
|
64
|
+
${codeSnippet}
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
${contextText ? `Additional context:\n\n${contextText}` : ''}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const explanation = await sendPrompt(prompt);
|
|
71
|
+
return { explanation };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function fixCode({ codeSnippet, language = 'JavaScript', contextFiles = [] }) {
|
|
75
|
+
const contextText = contextFiles.map(f => `// ${f.name}\n${f.content}`).join('\n\n');
|
|
76
|
+
|
|
77
|
+
const prompt = `
|
|
78
|
+
You are a software engineer. Fix the following ${language} code and return only the corrected version:
|
|
79
|
+
|
|
80
|
+
\`\`\`${language}
|
|
81
|
+
${codeSnippet}
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
${contextText ? `Here is additional context:\n\n${contextText}` : ''}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
const fixedCode = await sendPrompt(prompt);
|
|
88
|
+
return { fixedCode };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function generateCode({ description, language = 'JavaScript', context = '', fileType = '' }) {
|
|
92
|
+
const prompt = `
|
|
93
|
+
Generate ${fileType ? fileType + ' ' : ''}code in ${language} for the following description:
|
|
94
|
+
|
|
95
|
+
"${description}"
|
|
96
|
+
|
|
97
|
+
Output preference: code only, no explanation
|
|
98
|
+
|
|
99
|
+
${context ? `\n\nAdditional context:\n${context}` : ''}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const generatedCode = await sendPrompt(prompt);
|
|
103
|
+
return { generatedCode };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function scaffoldCode({ name }) {
|
|
107
|
+
const prompt = `
|
|
108
|
+
Scaffold a modern React component named "${name}" using best practices.
|
|
109
|
+
Include comments and clear structure.
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
const scaffoldCode = await sendPrompt(prompt);
|
|
113
|
+
return { scaffoldCode };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function getTerminalCommands({ goal, context = '', contextFiles = [] }) {
|
|
117
|
+
const contextFileText = contextFiles.map(f => `// ${f.name}\n${f.content}`).join('\n\n');
|
|
118
|
+
|
|
119
|
+
const prompt = `
|
|
120
|
+
You're a DevOps engineer. Provide the terminal commands to accomplish the following:
|
|
121
|
+
|
|
122
|
+
"${goal}"
|
|
123
|
+
|
|
124
|
+
${context ? `\n\nProject context:\n${context}` : ''}
|
|
125
|
+
${contextFileText ? `\n\nProject files:\n${contextFileText}` : ''}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const commands = await sendPrompt(prompt);
|
|
129
|
+
return { commands };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
analyzeCode,
|
|
134
|
+
explainCode,
|
|
135
|
+
fixCode,
|
|
136
|
+
generateCode,
|
|
137
|
+
scaffoldCode,
|
|
138
|
+
getTerminalCommands,
|
|
139
|
+
resetClient,
|
|
140
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// cli/utils/commandRunner.js
|
|
2
|
+
// Shared execution path for every AI command: resolve auth mode (BYOK vs
|
|
3
|
+
// hosted), run the request behind a spinner, handle errors consistently.
|
|
4
|
+
// Replaces the near-identical block previously copy-pasted into all six
|
|
5
|
+
// command handlers.
|
|
6
|
+
|
|
7
|
+
import axios from 'axios';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { hasLocalKey, getToken, apiEndpoint } from './configManager.js';
|
|
11
|
+
import handleCliError from './errorHandler.js';
|
|
12
|
+
|
|
13
|
+
function exitWithAuthGuidance() {
|
|
14
|
+
console.error(chalk.red('ā No API key or login found.'));
|
|
15
|
+
console.log(chalk.dim('Run "devtk config set-key <your-openai-key>" to use your own key.'));
|
|
16
|
+
console.log(chalk.dim('Or, if you self-host the Dev Toolkit server, run "devtk login".'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs one AI command in whichever mode is configured.
|
|
22
|
+
* @param {string} spinnerText - shown while the request runs
|
|
23
|
+
* @param {string} successText - shown when it completes
|
|
24
|
+
* @param {string} failText - shown by the error handler on failure
|
|
25
|
+
* @param {Function} localFn - BYOK path; returns the result object
|
|
26
|
+
* @param {string} endpoint - hosted path, e.g. '/analyze'
|
|
27
|
+
* @param {Object} payload - hosted path request body
|
|
28
|
+
* @returns result data object, or null on failure (already reported)
|
|
29
|
+
*/
|
|
30
|
+
async function runAICommand({ spinnerText, successText, failText, localFn, endpoint, payload }) {
|
|
31
|
+
let spinner;
|
|
32
|
+
try {
|
|
33
|
+
const byok = hasLocalKey();
|
|
34
|
+
const token = byok ? null : getToken();
|
|
35
|
+
if (!byok && !token) {
|
|
36
|
+
exitWithAuthGuidance();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
spinner = ora(spinnerText).start();
|
|
40
|
+
|
|
41
|
+
const data = byok
|
|
42
|
+
? await localFn()
|
|
43
|
+
: (await axios.post(apiEndpoint(endpoint), payload, {
|
|
44
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
45
|
+
})).data;
|
|
46
|
+
|
|
47
|
+
spinner.succeed(successText);
|
|
48
|
+
return data;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
handleCliError(spinner, err, failText);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default runAICommand;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// cli/utils/configManager.js
|
|
2
|
+
// Single source of truth for all local CLI state: the config directory path,
|
|
3
|
+
// config file read/write, auth token, OpenAI key (BYOK), and API URL.
|
|
4
|
+
// (Previously split across getToken.js, apiConfig.js, and inline code in
|
|
5
|
+
// loginHandlers.js ā all reading the same file independently.)
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.dev-toolkit');
|
|
12
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
// One-time migration from the pre-rename config location (~/.dev-helper)
|
|
15
|
+
const LEGACY_CONFIG_FILE = path.join(os.homedir(), '.dev-helper', 'config.json');
|
|
16
|
+
if (!fs.existsSync(CONFIG_FILE) && fs.existsSync(LEGACY_CONFIG_FILE)) {
|
|
17
|
+
try {
|
|
18
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
19
|
+
fs.copyFileSync(LEGACY_CONFIG_FILE, CONFIG_FILE);
|
|
20
|
+
fs.chmodSync(CONFIG_FILE, 0o600);
|
|
21
|
+
} catch {
|
|
22
|
+
// Migration is best-effort; a fresh config is created on next save
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// There is no public hosted service ā server mode requires a self-hosted
|
|
27
|
+
// API, configured via the DEV_TOOLKIT_API_URL env var or `apiUrl` in the
|
|
28
|
+
// config file.
|
|
29
|
+
|
|
30
|
+
// Config holds secrets (token, OpenAI key) ā owner read/write only.
|
|
31
|
+
// Windows ignores POSIX modes; this is best-effort there.
|
|
32
|
+
const SECRET_FILE_MODE = 0o600;
|
|
33
|
+
|
|
34
|
+
function ensureConfigDir() {
|
|
35
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getConfig() {
|
|
41
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveConfig(config) {
|
|
52
|
+
ensureConfigDir();
|
|
53
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
mode: SECRET_FILE_MODE,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setConfigValue(key, value) {
|
|
60
|
+
const config = getConfig();
|
|
61
|
+
config[key] = value;
|
|
62
|
+
saveConfig(config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeConfigValue(key) {
|
|
66
|
+
const config = getConfig();
|
|
67
|
+
delete config[key];
|
|
68
|
+
saveConfig(config);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// āā Auth token (hosted-service mode) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
72
|
+
|
|
73
|
+
function getToken() {
|
|
74
|
+
return getConfig().token || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function setToken(token) {
|
|
78
|
+
setConfigValue('token', token);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// āā OpenAI key (BYOK mode) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
82
|
+
|
|
83
|
+
function getOpenAIKey() {
|
|
84
|
+
// Environment variable takes precedence
|
|
85
|
+
if (process.env.OPENAI_API_KEY) {
|
|
86
|
+
return process.env.OPENAI_API_KEY;
|
|
87
|
+
}
|
|
88
|
+
// Fall back to config file
|
|
89
|
+
const config = getConfig();
|
|
90
|
+
return config.openaiApiKey || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasLocalKey() {
|
|
94
|
+
return !!getOpenAIKey();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function maskKey(key) {
|
|
98
|
+
if (!key || key.length < 8) return '****';
|
|
99
|
+
return key.slice(0, 7) + '...' + key.slice(-4);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// āā API URL (self-hosted server mode) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
103
|
+
|
|
104
|
+
function getApiUrl() {
|
|
105
|
+
// Environment variable takes precedence
|
|
106
|
+
if (process.env.DEV_TOOLKIT_API_URL) {
|
|
107
|
+
return process.env.DEV_TOOLKIT_API_URL;
|
|
108
|
+
}
|
|
109
|
+
const config = getConfig();
|
|
110
|
+
if (config.apiUrl) {
|
|
111
|
+
return config.apiUrl;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function apiEndpoint(endpointPath) {
|
|
117
|
+
const apiUrl = getApiUrl();
|
|
118
|
+
if (!apiUrl) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
'No API URL configured. Server mode requires a self-hosted Dev Toolkit server ā ' +
|
|
121
|
+
'set DEV_TOOLKIT_API_URL or add "apiUrl" to your config. ' +
|
|
122
|
+
'Or skip the server entirely: devtk config set-key <your-openai-key>'
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const baseUrl = apiUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
126
|
+
return `${baseUrl}${endpointPath}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
CONFIG_DIR,
|
|
131
|
+
CONFIG_FILE,
|
|
132
|
+
SECRET_FILE_MODE,
|
|
133
|
+
getConfig,
|
|
134
|
+
saveConfig,
|
|
135
|
+
setConfigValue,
|
|
136
|
+
removeConfigValue,
|
|
137
|
+
getToken,
|
|
138
|
+
setToken,
|
|
139
|
+
getOpenAIKey,
|
|
140
|
+
hasLocalKey,
|
|
141
|
+
maskKey,
|
|
142
|
+
getApiUrl,
|
|
143
|
+
apiEndpoint,
|
|
144
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export default async function handleWithContext({ options, handleBasic, handleWithContext }) {
|
|
2
|
+
const {
|
|
3
|
+
file: filePath,
|
|
4
|
+
name: componentName,
|
|
5
|
+
description,
|
|
6
|
+
language = 'JavaScript',
|
|
7
|
+
fileType,
|
|
8
|
+
output,
|
|
9
|
+
outputPath,
|
|
10
|
+
goal,
|
|
11
|
+
context, // boolean --context flag: include scanned project files
|
|
12
|
+
contextText, // optional plain-text context string
|
|
13
|
+
} = options;
|
|
14
|
+
|
|
15
|
+
const sharedArgs = {
|
|
16
|
+
filePath,
|
|
17
|
+
componentName,
|
|
18
|
+
description,
|
|
19
|
+
language,
|
|
20
|
+
fileType,
|
|
21
|
+
output,
|
|
22
|
+
outputPath,
|
|
23
|
+
goal,
|
|
24
|
+
contextText,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (context) {
|
|
28
|
+
await handleWithContext(sharedArgs);
|
|
29
|
+
} else {
|
|
30
|
+
await handleBasic(sharedArgs);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// utils/errorHandler.js
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
function handleCliError(spinner, err, fallbackMessage) {
|
|
5
|
+
if (spinner && spinner.isSpinning) spinner.fail(fallbackMessage);
|
|
6
|
+
|
|
7
|
+
if (err.response && err.response.data) {
|
|
8
|
+
// Only surface the server's intended error message ā never dump the
|
|
9
|
+
// raw response body (it may contain internals we shouldn't echo).
|
|
10
|
+
const serverMsg = err.response.data.error || err.response.data.message
|
|
11
|
+
|| `Request failed with status ${err.response.status}`;
|
|
12
|
+
console.error(chalk.red('ā Server error:'), serverMsg);
|
|
13
|
+
} else if (err.message) {
|
|
14
|
+
console.error(chalk.red('ā Error:'), err.message);
|
|
15
|
+
} else {
|
|
16
|
+
console.error(chalk.red('ā Unknown failure occurred.'));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default handleCliError;
|