@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,107 @@
|
|
|
1
|
+
// utils/fileScanner.js
|
|
2
|
+
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import ignore from "ignore";
|
|
7
|
+
|
|
8
|
+
// Files that must never be sent to an AI API, gitignored or not.
|
|
9
|
+
// .gitignore handles the common cases; this is the safety net for
|
|
10
|
+
// credential-shaped files that a project forgot to ignore.
|
|
11
|
+
const SECRET_FILE_PATTERNS = [
|
|
12
|
+
/\.pem$/i,
|
|
13
|
+
/\.key$/i,
|
|
14
|
+
/\.p12$/i,
|
|
15
|
+
/\.pfx$/i,
|
|
16
|
+
/id_rsa/i,
|
|
17
|
+
/id_ed25519/i,
|
|
18
|
+
/credential/i,
|
|
19
|
+
/secret/i,
|
|
20
|
+
/\.env(\.|$)/i,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
function looksLikeSecretFile(relativePath) {
|
|
24
|
+
return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads and parses .gitignore from the provided directory
|
|
29
|
+
*/
|
|
30
|
+
function loadGitignore(directory) {
|
|
31
|
+
const ig = ignore();
|
|
32
|
+
const gitignorePath = path.join(directory, ".gitignore");
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(gitignorePath)) {
|
|
35
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, "utf-8");
|
|
36
|
+
ig.add(gitignoreContent.split(/\r?\n/));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return ig;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scan project directory for relevant files
|
|
44
|
+
* @param {Object} options
|
|
45
|
+
* @param {string} options.directory - root directory to scan
|
|
46
|
+
* @param {string[]} options.extensions - file extensions to include
|
|
47
|
+
* @param {number} options.maxFileSizeKB - max file size to include
|
|
48
|
+
* @param {number} options.maxFiles - max number of files to include
|
|
49
|
+
* @param {string[]} options.exclude - folders to exclude
|
|
50
|
+
* @returns {Promise<Array<{ name: string, content: string }>>}
|
|
51
|
+
*/
|
|
52
|
+
async function scanFiles({
|
|
53
|
+
directory = ".",
|
|
54
|
+
extensions = ["js", "ts", "json"],
|
|
55
|
+
maxFileSizeKB = 20,
|
|
56
|
+
maxFiles = 50,
|
|
57
|
+
exclude = ["node_modules", ".git", "dist", "build"]
|
|
58
|
+
}) {
|
|
59
|
+
// Match all files with specified extensions
|
|
60
|
+
const patterns = extensions.map((ext) => `**/*.${ext}`);
|
|
61
|
+
const excludePatterns = exclude.map((dir) => `${dir}/**`);
|
|
62
|
+
|
|
63
|
+
// Build ignore patterns like "node_modules/**"
|
|
64
|
+
const gitignore = loadGitignore(directory);
|
|
65
|
+
|
|
66
|
+
const entries = await fg(patterns, {
|
|
67
|
+
cwd: directory,
|
|
68
|
+
ignore: excludePatterns,
|
|
69
|
+
dot: false, // Ignore hidden files like .env, .vscode
|
|
70
|
+
onlyFiles: true,
|
|
71
|
+
absolute: true, // Easier for fs.readFileSync
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const results = [];
|
|
75
|
+
|
|
76
|
+
for (const absolutePath of entries) {
|
|
77
|
+
if (results.length >= maxFiles) {
|
|
78
|
+
console.warn(`⚠️ Context capped at ${maxFiles} files; remaining files skipped.`);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const relativePath = path.relative(directory, absolutePath);
|
|
84
|
+
|
|
85
|
+
if (gitignore.ignores(relativePath)) continue;
|
|
86
|
+
|
|
87
|
+
if (looksLikeSecretFile(relativePath)) {
|
|
88
|
+
console.warn(`⚠️ Skipping ${relativePath}: filename looks credential-related`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const stats = fs.statSync(absolutePath);
|
|
93
|
+
const sizeKB = stats.size / 1024;
|
|
94
|
+
|
|
95
|
+
if (sizeKB <= maxFileSizeKB) {
|
|
96
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
97
|
+
results.push({ name: relativePath, content });
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn(`Skipping file ${absolutePath}: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { scanFiles };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// cli/utils/formatBox.js
|
|
2
|
+
// Boxed rendering for analyze results, ported from the stranded pre-v2
|
|
3
|
+
// "structured boxed output" work (e20e5fe) onto the v2 CLI structure.
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Word-wraps text to fit within a given width, preserving existing line breaks.
|
|
9
|
+
*/
|
|
10
|
+
function wordWrap(text, maxWidth) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
for (const rawLine of text.split('\n')) {
|
|
13
|
+
if (rawLine.length <= maxWidth) {
|
|
14
|
+
lines.push(rawLine);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const words = rawLine.split(' ');
|
|
18
|
+
let current = '';
|
|
19
|
+
for (const word of words) {
|
|
20
|
+
if (current.length + word.length + 1 > maxWidth) {
|
|
21
|
+
lines.push(current);
|
|
22
|
+
current = word;
|
|
23
|
+
} else {
|
|
24
|
+
current = current ? `${current} ${word}` : word;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (current) lines.push(current);
|
|
28
|
+
}
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Renders a boxed analysis result:
|
|
34
|
+
*
|
|
35
|
+
* ┌─ Analysis Result ──────────────────────────────┐
|
|
36
|
+
* │ │
|
|
37
|
+
* │ File: ./brokenFunction.ts │
|
|
38
|
+
* │ Status: 2 issues found │
|
|
39
|
+
* │ │
|
|
40
|
+
* │ [1] Line 14 — Uncaught promise rejection │
|
|
41
|
+
* │ description text... │
|
|
42
|
+
* │ │
|
|
43
|
+
* │ Suggestion: │
|
|
44
|
+
* │ ... │
|
|
45
|
+
* │ │
|
|
46
|
+
* └────────────────────────────────────────────────┘
|
|
47
|
+
*/
|
|
48
|
+
export function renderAnalysisBox({ filePath, issues = [], suggestion = '' }) {
|
|
49
|
+
const termWidth = Math.min(process.stdout.columns || 80, 80);
|
|
50
|
+
const boxWidth = Math.max(termWidth - 4, 40); // leave margin
|
|
51
|
+
const innerWidth = boxWidth - 4; // 2 for "│ " and 2 for " │"
|
|
52
|
+
const pad = 2; // left padding inside box
|
|
53
|
+
|
|
54
|
+
const border = chalk.cyan;
|
|
55
|
+
const padStr = ' '.repeat(pad);
|
|
56
|
+
|
|
57
|
+
// Build content lines (plain strings, padded at render time)
|
|
58
|
+
const contentLines = [''];
|
|
59
|
+
|
|
60
|
+
contentLines.push(`File: ${filePath}`);
|
|
61
|
+
const issueCount = issues.length;
|
|
62
|
+
const statusText = issueCount === 0
|
|
63
|
+
? 'No issues found'
|
|
64
|
+
: `${issueCount} issue${issueCount !== 1 ? 's' : ''} found`;
|
|
65
|
+
contentLines.push(`Status: ${statusText}`);
|
|
66
|
+
contentLines.push('');
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < issues.length; i++) {
|
|
69
|
+
const issue = issues[i];
|
|
70
|
+
const lineRef = issue.line != null ? `Line ${issue.line}` : 'General';
|
|
71
|
+
const header = `[${i + 1}] ${lineRef} — ${issue.title}`;
|
|
72
|
+
|
|
73
|
+
for (const wl of wordWrap(header, innerWidth)) {
|
|
74
|
+
contentLines.push(wl);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (issue.detail) {
|
|
78
|
+
for (const dl of wordWrap(issue.detail, innerWidth - 4)) {
|
|
79
|
+
contentLines.push(` ${dl}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
contentLines.push('');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (suggestion) {
|
|
87
|
+
contentLines.push(chalk.green('Suggestion:'));
|
|
88
|
+
for (const sl of wordWrap(suggestion, innerWidth - 4)) {
|
|
89
|
+
contentLines.push(` ${sl}`);
|
|
90
|
+
}
|
|
91
|
+
contentLines.push('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Row layout: " │" + pad + innerWidth + pad + "│" — so the horizontal
|
|
95
|
+
// span between the corners is pad + innerWidth + pad = boxWidth.
|
|
96
|
+
const titleText = ' Analysis Result ';
|
|
97
|
+
const top = border(` ┌─${titleText}${'─'.repeat(Math.max(boxWidth - 1 - titleText.length, 0))}┐`);
|
|
98
|
+
const bottom = border(` └${'─'.repeat(boxWidth)}┘`);
|
|
99
|
+
|
|
100
|
+
const output = [top];
|
|
101
|
+
|
|
102
|
+
for (const line of contentLines) {
|
|
103
|
+
// Strip ANSI for length calculation
|
|
104
|
+
const plainLine = line.replace(new RegExp(String.fromCharCode(27) + '\[[0-9;]*m', 'g'), '');
|
|
105
|
+
const rightPad = Math.max(innerWidth - plainLine.length, 0);
|
|
106
|
+
output.push(` ${border('│')}${padStr}${line}${' '.repeat(rightPad)}${padStr}${border('│')}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
output.push(bottom);
|
|
110
|
+
|
|
111
|
+
console.log(output.join('\n'));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parses a structured JSON analysis response from the AI.
|
|
116
|
+
* Handles markdown code fences and falls back gracefully to a
|
|
117
|
+
* single-issue shape when the model didn't return valid JSON.
|
|
118
|
+
*/
|
|
119
|
+
export function parseAnalysisJSON(rawText) {
|
|
120
|
+
let text = rawText.trim();
|
|
121
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
122
|
+
if (fenceMatch) {
|
|
123
|
+
text = fenceMatch[1].trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(text);
|
|
128
|
+
if (parsed.issues && Array.isArray(parsed.issues)) {
|
|
129
|
+
return {
|
|
130
|
+
issues: parsed.issues.map(i => ({
|
|
131
|
+
line: i.line ?? null,
|
|
132
|
+
title: i.title || 'Issue',
|
|
133
|
+
detail: i.detail || '',
|
|
134
|
+
})),
|
|
135
|
+
suggestion: parsed.suggestion || '',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Fall through to fallback
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
issues: [{ line: null, title: 'Analysis', detail: rawText.trim() }],
|
|
144
|
+
suggestion: '',
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// utils/fsUtils.js
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
function checkFileExists(filePath) {
|
|
6
|
+
if (!fs.existsSync(filePath)) {
|
|
7
|
+
console.error(chalk.red(`❌ File not found: ${filePath}`));
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { checkFileExists };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getToken } from './configManager.js';
|
|
2
|
+
import { appendLocalHistory } from './localHistory.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Saves a command result to history.
|
|
6
|
+
*
|
|
7
|
+
* Hosted mode: the server already records history inside each AI route,
|
|
8
|
+
* so the CLI does nothing here (posting to /history as well would create
|
|
9
|
+
* duplicate entries — which is exactly what earlier versions did).
|
|
10
|
+
*
|
|
11
|
+
* BYOK mode (no login token): history is saved locally instead.
|
|
12
|
+
*/
|
|
13
|
+
async function saveToHistory({ command, input, output }) {
|
|
14
|
+
if (getToken()) return; // hosted mode — server-side history already saved
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
appendLocalHistory({ command, input, output });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// History must never break the main command, but don't hide the failure
|
|
20
|
+
console.warn(`⚠️ Could not save history: ${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default saveToHistory;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// cli/utils/localHistory.js
|
|
2
|
+
// File-based history for BYOK mode — no account or server required.
|
|
3
|
+
// Hosted-mode history lives server-side; this is the local equivalent.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { CONFIG_DIR, SECRET_FILE_MODE } from './configManager.js';
|
|
8
|
+
|
|
9
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.json');
|
|
10
|
+
const MAX_ENTRIES = 100;
|
|
11
|
+
|
|
12
|
+
function readLocalHistory() {
|
|
13
|
+
if (!fs.existsSync(HISTORY_FILE)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const entries = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf-8'));
|
|
18
|
+
return Array.isArray(entries) ? entries : [];
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appendLocalHistory({ command, input, output }) {
|
|
25
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
26
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const entries = readLocalHistory();
|
|
30
|
+
entries.unshift({
|
|
31
|
+
command,
|
|
32
|
+
input,
|
|
33
|
+
output,
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// History can contain source code — same owner-only mode as the config file.
|
|
38
|
+
fs.writeFileSync(HISTORY_FILE, JSON.stringify(entries.slice(0, MAX_ENTRIES), null, 2), {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
mode: SECRET_FILE_MODE,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { HISTORY_FILE, MAX_ENTRIES, readLocalHistory, appendLocalHistory };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// cli/utils/promptPassword.js
|
|
2
|
+
// Masked password input using raw-mode stdin — no dependency needed.
|
|
3
|
+
// Echoes '*' per keystroke, supports backspace and Ctrl+C.
|
|
4
|
+
|
|
5
|
+
const CTRL_C = '';
|
|
6
|
+
const BACKSPACE = '';
|
|
7
|
+
|
|
8
|
+
function promptPassword(promptText) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const { stdin, stdout } = process;
|
|
11
|
+
|
|
12
|
+
stdout.write(promptText);
|
|
13
|
+
|
|
14
|
+
if (!stdin.isTTY) {
|
|
15
|
+
// Piped input (tests, scripts): read one line without raw mode
|
|
16
|
+
let piped = '';
|
|
17
|
+
stdin.setEncoding('utf-8');
|
|
18
|
+
stdin.on('data', function onData(chunk) {
|
|
19
|
+
piped += chunk;
|
|
20
|
+
const newline = piped.indexOf('\n');
|
|
21
|
+
if (newline !== -1) {
|
|
22
|
+
stdin.removeListener('data', onData);
|
|
23
|
+
stdin.pause();
|
|
24
|
+
resolve(piped.slice(0, newline).replace(/\r$/, ''));
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
stdin.setRawMode(true);
|
|
31
|
+
stdin.resume();
|
|
32
|
+
stdin.setEncoding('utf-8');
|
|
33
|
+
|
|
34
|
+
let password = '';
|
|
35
|
+
|
|
36
|
+
function done(err, value) {
|
|
37
|
+
stdin.setRawMode(false);
|
|
38
|
+
stdin.pause();
|
|
39
|
+
stdin.removeListener('data', onKeypress);
|
|
40
|
+
stdout.write('\n');
|
|
41
|
+
if (err) reject(err);
|
|
42
|
+
else resolve(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function onKeypress(char) {
|
|
46
|
+
switch (char) {
|
|
47
|
+
case '\r':
|
|
48
|
+
case '\n':
|
|
49
|
+
done(null, password);
|
|
50
|
+
break;
|
|
51
|
+
case CTRL_C:
|
|
52
|
+
done(new Error('Cancelled'));
|
|
53
|
+
break;
|
|
54
|
+
case BACKSPACE:
|
|
55
|
+
case '\b':
|
|
56
|
+
if (password.length > 0) {
|
|
57
|
+
password = password.slice(0, -1);
|
|
58
|
+
stdout.write('\b \b');
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
// Ignore other control characters (arrows etc.)
|
|
63
|
+
if (char >= ' ') {
|
|
64
|
+
password += char;
|
|
65
|
+
stdout.write('*');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stdin.on('data', onKeypress);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default promptPassword;
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mattgraba/dev-toolkit",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Dev Toolkit — an AI-assisted developer CLI for analyzing errors, debugging issues, and scaffolding projects. BYOK-first; optional hosted mode.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"devtk": "bin/devtk.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"cli",
|
|
12
|
+
"!cli/tests",
|
|
13
|
+
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev:cli": "node cli/cli.js",
|
|
19
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --detectOpenHandles --forceExit"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"developer-tools",
|
|
24
|
+
"ai",
|
|
25
|
+
"debugging",
|
|
26
|
+
"productivity",
|
|
27
|
+
"openai",
|
|
28
|
+
"byok",
|
|
29
|
+
"open-source"
|
|
30
|
+
],
|
|
31
|
+
"author": "Matt Graba",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/mattgraba/dev-toolkit.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/mattgraba/dev-toolkit/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/mattgraba/dev-toolkit#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"preferGlobal": true,
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"jest": {
|
|
49
|
+
"testEnvironment": "node",
|
|
50
|
+
"transform": {},
|
|
51
|
+
"testPathIgnorePatterns": [
|
|
52
|
+
"/node_modules/",
|
|
53
|
+
"/server/",
|
|
54
|
+
"/client/"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"axios": "^1.10.0",
|
|
59
|
+
"chalk": "^4.1.2",
|
|
60
|
+
"commander": "^14.0.0",
|
|
61
|
+
"dotenv": "^17.2.0",
|
|
62
|
+
"fast-glob": "^3.3.3",
|
|
63
|
+
"ignore": "^7.0.5",
|
|
64
|
+
"openai": "^5.20.3",
|
|
65
|
+
"ora": "^5.4.1"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"jest": "^30.0.4"
|
|
69
|
+
}
|
|
70
|
+
}
|