devfix-cli 1.0.0 β 1.0.2
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/README.md +108 -0
- package/bin/index.js +17 -0
- package/package.json +1 -1
- package/src/utils/autoError.js +76 -0
- package/src/utils/prompt.js +2 -2
- package/src/utils/run.js +240 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# DevFix CLI π
|
|
2
|
+
AI-powered CLI tool that helps you debug errors, logs, and DevOps issues directly from your terminal.
|
|
3
|
+
|
|
4
|
+
DevFix analyzes your error output (Node, Docker, Kubernetes, Minikube, Git, etc.) and suggests the most likely fix with copy-paste commands.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## β¨ Features
|
|
9
|
+
|
|
10
|
+
- π Login once (saved locally for 7 days)
|
|
11
|
+
- π€ Uses OpenRouter API (supports many models)
|
|
12
|
+
- π§ Auto-detects stack (Node, Docker, Kubernetes, Git, Python, React)
|
|
13
|
+
- π¦ Optional auto context mode (`--context`)
|
|
14
|
+
- π¬ Animated DevFix logo during analysis (Minikube-style)
|
|
15
|
+
- π§Ύ Clean Markdown output in terminal
|
|
16
|
+
- π§© Works on macOS / Linux / Windows
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
### 1) Install DevFix globally
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g devfix
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Check installation:
|
|
26
|
+
```bash
|
|
27
|
+
devfix --version
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### 2) Login (required)
|
|
33
|
+
|
|
34
|
+
DevFix needs your OpenRouter API key to work.
|
|
35
|
+
|
|
36
|
+
Run:
|
|
37
|
+
```bash
|
|
38
|
+
devfix login
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It will ask for:
|
|
42
|
+
- Username
|
|
43
|
+
- Email
|
|
44
|
+
- OpenRouter API Key
|
|
45
|
+
|
|
46
|
+
Your login is saved locally for **7 days**, so you donβt need to login again daily.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### 3) Start using DevFix
|
|
51
|
+
|
|
52
|
+
Analyze an error directly:
|
|
53
|
+
```bash
|
|
54
|
+
devfix analyze "npm install failing"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Analyze with automatic context collection (recommended):
|
|
58
|
+
```bash
|
|
59
|
+
devfix analyze "minikube ingress not working" --context
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Analyze a log file:
|
|
63
|
+
```bash
|
|
64
|
+
devfix analyze --file error.log
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 4) Session commands
|
|
70
|
+
|
|
71
|
+
Check current login:
|
|
72
|
+
```bash
|
|
73
|
+
devfix whoami
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Logout:
|
|
77
|
+
```bash
|
|
78
|
+
devfix logout
|
|
79
|
+
```
|
|
80
|
+
---
|
|
81
|
+
### 5) Run a command and auto-capture errors for AI fixing
|
|
82
|
+
1. Context command -> Include project/system context
|
|
83
|
+
```bash
|
|
84
|
+
devfix run <Command> --context
|
|
85
|
+
```
|
|
86
|
+
---
|
|
87
|
+
for example : devfix run kubectl get pods --context
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
2. Stack Command -> Force stack type
|
|
91
|
+
```bash
|
|
92
|
+
devfix run <Command> --stack <StackName>
|
|
93
|
+
```
|
|
94
|
+
---
|
|
95
|
+
for example : devfix run kubectl get pods --stack kubernetes
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
Model Command -> OpenRouter model override
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
devfix run <Command> --model <ModelName>
|
|
102
|
+
```
|
|
103
|
+
---
|
|
104
|
+
for example : devfix run kubectl get pods --model openai/gpt-4o-mini
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
package/bin/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loginCommand } from "../src/commands/login.js";
|
|
|
5
5
|
import { logoutCommand } from "../src/commands/logout.js";
|
|
6
6
|
import { whoamiCommand } from "../src/commands/whoami.js";
|
|
7
7
|
import { analyzeCommand } from "../src/commands/analyze.js";
|
|
8
|
+
import { runCommand } from "../src/utils/run.js";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const program = new Command();
|
|
@@ -38,4 +39,20 @@ program
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
program
|
|
43
|
+
.command("run")
|
|
44
|
+
.description("Run a command and auto-capture errors for AI fixing")
|
|
45
|
+
.option("-c, --context", "Include project/system context")
|
|
46
|
+
.option("-s, --stack <stack>", "Force stack type")
|
|
47
|
+
.option("-m, --model <model>", "OpenRouter model override")
|
|
48
|
+
.argument("<cmd...>", "Command to run")
|
|
49
|
+
.action((cmd, options) => {
|
|
50
|
+
runCommand(cmd, {
|
|
51
|
+
context: options.context,
|
|
52
|
+
stack: options.stack,
|
|
53
|
+
model: options.model,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
41
58
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
function getLatestFile(dir, ext = ".log") {
|
|
6
|
+
try {
|
|
7
|
+
if (!fs.existsSync(dir)) return null;
|
|
8
|
+
|
|
9
|
+
const files = fs
|
|
10
|
+
.readdirSync(dir)
|
|
11
|
+
.filter((f) => f.endsWith(ext))
|
|
12
|
+
.map((f) => ({
|
|
13
|
+
name: f,
|
|
14
|
+
full: path.join(dir, f),
|
|
15
|
+
time: fs.statSync(path.join(dir, f)).mtimeMs,
|
|
16
|
+
}))
|
|
17
|
+
.sort((a, b) => b.time - a.time);
|
|
18
|
+
|
|
19
|
+
return files.length ? files[0].full : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractNpmError(logText) {
|
|
26
|
+
const lines = logText.split("\n");
|
|
27
|
+
|
|
28
|
+
// Most npm errors appear in these patterns
|
|
29
|
+
const keywords = ["npm ERR!", "error", "ERR_", "EACCES", "ENOTFOUND", "ECONNRESET"];
|
|
30
|
+
|
|
31
|
+
// Find first error-like line
|
|
32
|
+
let startIndex = -1;
|
|
33
|
+
for (let i = 0; i < lines.length; i++) {
|
|
34
|
+
const lower = lines[i].toLowerCase();
|
|
35
|
+
if (keywords.some((k) => lower.includes(k.toLowerCase()))) {
|
|
36
|
+
startIndex = i;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If no errors found, return null
|
|
42
|
+
if (startIndex === -1) return null;
|
|
43
|
+
|
|
44
|
+
// Take a clean block after the error line
|
|
45
|
+
const extracted = lines.slice(startIndex, startIndex + 60).join("\n");
|
|
46
|
+
|
|
47
|
+
// Remove useless "silly" lines
|
|
48
|
+
return extracted
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter((l) => !l.includes("silly"))
|
|
51
|
+
.slice(0, 60)
|
|
52
|
+
.join("\n")
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function collectAutoError() {
|
|
57
|
+
const npmLogsDir = path.join(os.homedir(), ".npm", "_logs");
|
|
58
|
+
const latestNpmLog = getLatestFile(npmLogsDir, ".log");
|
|
59
|
+
|
|
60
|
+
if (latestNpmLog) {
|
|
61
|
+
const raw = fs.readFileSync(latestNpmLog, "utf-8");
|
|
62
|
+
const extracted = extractNpmError(raw);
|
|
63
|
+
|
|
64
|
+
if (extracted) {
|
|
65
|
+
return {
|
|
66
|
+
source: "npm" | "kubernetes" | "docker" | "git",
|
|
67
|
+
title: "Short readable error title",
|
|
68
|
+
file: "...optional",
|
|
69
|
+
extracted: "error lines",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
}
|
package/src/utils/prompt.js
CHANGED
|
@@ -4,8 +4,8 @@ You are DevFix, a CLI debugging assistant.
|
|
|
4
4
|
|
|
5
5
|
Hard rules:
|
|
6
6
|
- Be concise.
|
|
7
|
-
- Max
|
|
8
|
-
- Max
|
|
7
|
+
- Max 3 fix steps.
|
|
8
|
+
- Max 3 commands.
|
|
9
9
|
- No long paragraphs.
|
|
10
10
|
- If log indicates success (no error), say so and ask for the real error.
|
|
11
11
|
- If ambiguous, ask ONLY 1 question.
|
package/src/utils/run.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import boxen from "boxen";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
|
|
7
|
+
import { marked } from "marked";
|
|
8
|
+
import TerminalRenderer from "marked-terminal";
|
|
9
|
+
|
|
10
|
+
import { readConfig } from "../utils/config.js";
|
|
11
|
+
import { isSessionValid } from "../utils/session.js";
|
|
12
|
+
import { decrypt } from "../utils/cryptoStore.js";
|
|
13
|
+
|
|
14
|
+
import { collectContext } from "../utils/context.js";
|
|
15
|
+
import { detectStack } from "../utils/detectStack.js";
|
|
16
|
+
import { buildPrompt } from "../utils/prompt.js";
|
|
17
|
+
import { askAI } from "../utils/ai.js";
|
|
18
|
+
|
|
19
|
+
// Markdown renderer
|
|
20
|
+
marked.setOptions({
|
|
21
|
+
renderer: new TerminalRenderer(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export async function runCommand(cmdArgs, options) {
|
|
25
|
+
const config = readConfig();
|
|
26
|
+
|
|
27
|
+
if (!isSessionValid(config)) {
|
|
28
|
+
console.log(chalk.red("\nβ Not logged in or session expired.\nRun: devfix login\n"));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiKey = decrypt(config.apiKeyEncrypted);
|
|
33
|
+
if (!apiKey) {
|
|
34
|
+
console.log(chalk.red("\nβ API key missing. Run: devfix login\n"));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!cmdArgs || cmdArgs.length === 0) {
|
|
39
|
+
console.log(chalk.red("\nβ Please provide a command.\nExample: devfix run kubectl get pods\n"));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const command = cmdArgs[0];
|
|
44
|
+
const args = cmdArgs.slice(1);
|
|
45
|
+
const fullCmd = `${command} ${args.join(" ")}`.trim();
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
boxen(
|
|
49
|
+
`${chalk.bold.cyan("βΆ DevFix Run")}\n\n${chalk.white("Command:")} ${chalk.yellow(fullCmd)}\n${
|
|
50
|
+
options.context ? chalk.green("Context: ON") : chalk.red("Context: OFF")
|
|
51
|
+
}`,
|
|
52
|
+
{ padding: 1, borderStyle: "round" }
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// β
No shell=true (removes the security warning)
|
|
57
|
+
const child = spawn(command, args, { stdio: ["inherit", "pipe", "pipe"] });
|
|
58
|
+
|
|
59
|
+
let stdout = "";
|
|
60
|
+
let stderr = "";
|
|
61
|
+
|
|
62
|
+
child.stdout.on("data", (d) => {
|
|
63
|
+
const text = d.toString();
|
|
64
|
+
stdout += text;
|
|
65
|
+
process.stdout.write(text);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.stderr.on("data", (d) => {
|
|
69
|
+
const text = d.toString();
|
|
70
|
+
stderr += text;
|
|
71
|
+
process.stderr.write(text);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.on("close", async (code) => {
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
console.log(chalk.green("\nβ
Command succeeded.\n"));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const errorText = (stderr || stdout || "").trim();
|
|
81
|
+
|
|
82
|
+
console.log(chalk.red(`\nβ Command failed (exit code: ${code}).\n`));
|
|
83
|
+
|
|
84
|
+
if (!errorText) {
|
|
85
|
+
console.log(chalk.red("No error output captured.\n"));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
boxen(chalk.bold.red("β οΈ Captured Error"), {
|
|
91
|
+
padding: 1,
|
|
92
|
+
borderStyle: "round",
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log(chalk.red(errorText.slice(0, 1200)));
|
|
97
|
+
if (errorText.length > 1200) console.log(chalk.gray("\n...trimmed...\n"));
|
|
98
|
+
|
|
99
|
+
const { confirm } = await inquirer.prompt([
|
|
100
|
+
{
|
|
101
|
+
name: "confirm",
|
|
102
|
+
type: "confirm",
|
|
103
|
+
message: "Send this error to DevFix AI for a fix?",
|
|
104
|
+
default: true,
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
if (!confirm) {
|
|
109
|
+
console.log(chalk.yellow("\nβ Not sent to AI.\n"));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const context = options.context ? collectContext() : {};
|
|
114
|
+
const stack = options.stack || detectStack(errorText);
|
|
115
|
+
const usedModel = options.model || "openai/gpt-4o-mini";
|
|
116
|
+
|
|
117
|
+
// π₯ Send a better bundle (so AI never asks useless questions)
|
|
118
|
+
const errorBundle = `
|
|
119
|
+
Command:
|
|
120
|
+
${fullCmd}
|
|
121
|
+
|
|
122
|
+
Exit Code:
|
|
123
|
+
${code}
|
|
124
|
+
|
|
125
|
+
Error Output:
|
|
126
|
+
${errorText}
|
|
127
|
+
`.trim();
|
|
128
|
+
|
|
129
|
+
console.log(
|
|
130
|
+
boxen(
|
|
131
|
+
`${chalk.bold.white("Stack:")} ${chalk.cyan(stack)}\n${chalk.bold.white("Model:")} ${chalk.magenta(
|
|
132
|
+
usedModel
|
|
133
|
+
)}`,
|
|
134
|
+
{ padding: 1, borderStyle: "round" }
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const spinner = ora("DevFix AI is analyzing...").start();
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const prompt = buildPrompt({
|
|
142
|
+
stack,
|
|
143
|
+
input: errorBundle,
|
|
144
|
+
context,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const answer = await askAI({
|
|
148
|
+
apiKey,
|
|
149
|
+
model: usedModel,
|
|
150
|
+
prompt,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
spinner.succeed("Analysis complete");
|
|
154
|
+
|
|
155
|
+
console.log(
|
|
156
|
+
boxen(chalk.bold.green("β
DevFix Suggested Fix"), {
|
|
157
|
+
padding: 1,
|
|
158
|
+
borderStyle: "round",
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// β
Pretty output (markdown rendered)
|
|
163
|
+
console.log(marked(answer));
|
|
164
|
+
console.log();
|
|
165
|
+
} catch (err) {
|
|
166
|
+
spinner.fail("AI request failed");
|
|
167
|
+
|
|
168
|
+
console.log(chalk.red("\nβ Error:\n"));
|
|
169
|
+
console.log(err?.response?.data || err.message);
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
child.on("error", async (err) => {
|
|
174
|
+
if (err.code === "ENOENT") {
|
|
175
|
+
console.log(chalk.red(`\nβ Command not found: ${command}\n`));
|
|
176
|
+
console.log(chalk.gray("Tip: Did you mean `kubectl`?\n"));
|
|
177
|
+
|
|
178
|
+
const { confirm } = await inquirer.prompt([
|
|
179
|
+
{
|
|
180
|
+
name: "confirm",
|
|
181
|
+
type: "confirm",
|
|
182
|
+
message: "Send this error to DevFix AI for a fix?",
|
|
183
|
+
default: true,
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
if (!confirm) {
|
|
188
|
+
console.log(chalk.yellow("\nβ Not sent to AI.\n"));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const errorText = `Command not found: ${command}\nTried to run: ${fullCmd}`;
|
|
193
|
+
const context = options.context ? collectContext() : {};
|
|
194
|
+
const stack = options.stack || detectStack(errorText);
|
|
195
|
+
const usedModel = options.model || "openai/gpt-4o-mini";
|
|
196
|
+
|
|
197
|
+
const spinner = ora("DevFix AI is analyzing...").start();
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const prompt = buildPrompt({
|
|
201
|
+
stack,
|
|
202
|
+
input: errorText,
|
|
203
|
+
context,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const answer = await askAI({
|
|
207
|
+
apiKey,
|
|
208
|
+
model: usedModel,
|
|
209
|
+
prompt,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
spinner.succeed("Analysis complete");
|
|
213
|
+
|
|
214
|
+
console.log(
|
|
215
|
+
boxen(chalk.bold.green("β
DevFix Suggested Fix"), {
|
|
216
|
+
padding: 1,
|
|
217
|
+
borderStyle: "round",
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
console.log(marked(answer));
|
|
222
|
+
console.log();
|
|
223
|
+
} catch (e) {
|
|
224
|
+
spinner.fail("AI request failed");
|
|
225
|
+
console.log(chalk.red("\nβ Error:\n"));
|
|
226
|
+
console.log(e?.response?.data || e.message);
|
|
227
|
+
console.log();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(chalk.red("\nβ Failed to run command:\n"));
|
|
234
|
+
console.log(err.message);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|