@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v2.0.1 – 2026-07-04
|
|
4
|
+
|
|
5
|
+
The actual first release of Dev Toolkit. (A `2.0.0` was briefly published before the hosted-service retirement below landed and has been removed from the registry — it still contained the retired service's default URL.)
|
|
6
|
+
|
|
7
|
+
Includes everything under v2.0.0, plus:
|
|
8
|
+
|
|
9
|
+
- **The public hosted service is retired and removed from the code.** The CLI ships no default API URL; server mode requires a self-hosted server via `DEV_TOOLKIT_API_URL` or `apiUrl` in config. BYOK is unaffected and primary.
|
|
10
|
+
|
|
11
|
+
## v2.0.0 – 2026-07-03
|
|
12
|
+
|
|
13
|
+
Renamed from **Dev Helper** (`@mattgraba/dev-helper`, `dev-helper`) to **Dev Toolkit** (`@mattgraba/dev-toolkit`, `devtk`), alongside a full security and architecture remediation. The audit that motivated this release is documented in [docs/RETROSPECTIVE.md](docs/RETROSPECTIVE.md).
|
|
14
|
+
|
|
15
|
+
### Breaking
|
|
16
|
+
|
|
17
|
+
- npm package: `@mattgraba/dev-helper` → `@mattgraba/dev-toolkit`
|
|
18
|
+
- bin command: `dev-helper` → `devtk`
|
|
19
|
+
- Config dir: `~/.dev-helper/` → `~/.dev-toolkit/` (existing config is migrated automatically on first run)
|
|
20
|
+
- Env var: `DEV_HELPER_API_URL` → `DEV_TOOLKIT_API_URL`
|
|
21
|
+
- Server API: JWTs now identify users by database id rather than username; tokens issued by v1 are not valid against the v2 server
|
|
22
|
+
|
|
23
|
+
### Security
|
|
24
|
+
|
|
25
|
+
- Removed the hardcoded JWT secret fallback; the server now refuses to start without `JWT_SECRET`
|
|
26
|
+
- Global Express error handler; stack traces are no longer exposed outside development
|
|
27
|
+
- Rate limiting is now applied after authentication, so per-user limits actually work (previously always per-IP); register and login have separate brute-force buckets; `/history` is rate-limited
|
|
28
|
+
- Explicit string type-guards on auth inputs (closes NoSQL-operator-shaped payloads)
|
|
29
|
+
- Local config and history files are written with owner-only permissions (0600)
|
|
30
|
+
- Login password input is masked in the terminal
|
|
31
|
+
- Context scanning skips credential-shaped filenames and is capped at 50 files
|
|
32
|
+
- CLI no longer prints raw server response bodies on error
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **History now works in BYOK mode** — saved locally to `~/.dev-toolkit/history.json`; previously it silently did nothing without a login
|
|
37
|
+
- Hosted-mode history was saved twice per command (once by the server route, once by the CLI); now saved once
|
|
38
|
+
- Context file names appeared as `// undefined` in AI prompts due to a field-name mismatch between the scanner and prompt builders
|
|
39
|
+
- History sorting referenced a field that didn't exist in the schema
|
|
40
|
+
- The server Dockerfile and `dev` script pointed at a nonexistent entrypoint
|
|
41
|
+
- The token-budget guard existed but was never invoked; it now runs on every AI request
|
|
42
|
+
- Maintenance script crashed on launch (CommonJS in an ESM package)
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **The public hosted service is retired.** The server and web client remain in the repo as a tested, self-hostable reference; the CLI no longer ships a default API URL (server mode requires `DEV_TOOLKIT_API_URL` or `apiUrl` in config). BYOK is unaffected and is the primary mode.
|
|
47
|
+
- Six copy-pasted CLI command handlers and six copy-pasted server routes now share extracted helpers (`runAICommand`, `saveHistoryIfAuthed`, `withOpenAIErrorHandling`, `validateContextFiles`)
|
|
48
|
+
- CLI config/token I/O consolidated from four independent implementations into one module
|
|
49
|
+
- All prompt templates consolidated into the server's OpenAI service
|
|
50
|
+
- `--context` (scan project files) and `--context-text` (plain text) are now orthogonal flags
|
|
51
|
+
- Docs regenerated from the implementation; README is BYOK-first
|
|
52
|
+
- Dead code removed (unreachable auth controller, unrelated leftover config, abandoned TypeScript stub)
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Structured boxed output for `analyze`: prompts request JSON (`issues[]` + `suggestion`), the CLI renders a Unicode-bordered result box with numbered, line-referenced issues, and the API returns the structured shape (ported from unreleased pre-v2 work that was stranded on a divergent branch)
|
|
57
|
+
- Baseline test suites: 32 tests across server (auth flow, analyze route, per-user rate-limit regression) and CLI (config, local history, analysis parsing)
|
|
58
|
+
- `.env.example` files for CLI and server
|
|
59
|
+
- `docs/RETROSPECTIVE.md` — the v1 audit, kept as a learning record
|
|
60
|
+
|
|
61
|
+
## v1.1.0 – 2026-02-03
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- Bring Your Own OpenAI API Key (BYOK) support
|
|
65
|
+
- `dev-helper config` command for local configuration
|
|
66
|
+
- Optional hosted fallback for users without API keys
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- Authentication is no longer required when using a local API key
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Graba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Dev Toolkit
|
|
2
|
+
|
|
3
|
+
An AI-assisted developer CLI for analyzing errors, explaining and fixing code, generating boilerplate, and getting terminal commands — from your terminal, with your own OpenAI key.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **BYOK-first** — bring your own OpenAI key; no account, no server, your code never touches third-party infrastructure beyond OpenAI itself
|
|
8
|
+
- **Project-aware analysis** — optionally include surrounding files as context (gitignore-respecting, credential-file denylist, capped at 50 files / 20KB each)
|
|
9
|
+
- **Six AI commands** — analyze, explain, fix, generate, scaffold, terminal
|
|
10
|
+
- **Local history** — queries are saved to `~/.dev-toolkit/history.json` (BYOK mode), or server-side if you run the optional self-hosted server
|
|
11
|
+
- **Suggestion-only terminal commands** — the `terminal` command prints suggested shell commands; it never executes anything
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @mattgraba/dev-toolkit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Verify:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
devtk --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start (BYOK)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Set your OpenAI API key (stored locally, owner-only file permissions)
|
|
29
|
+
devtk config set-key sk-your-openai-api-key
|
|
30
|
+
|
|
31
|
+
# Analyze a file
|
|
32
|
+
devtk analyze -f ./src/buggy.js -l javascript
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Your key is stored at `~/.dev-toolkit/config.json` and calls go directly to OpenAI. The `OPENAI_API_KEY` environment variable takes precedence over the config file if both are set.
|
|
36
|
+
|
|
37
|
+
## CLI Reference
|
|
38
|
+
|
|
39
|
+
| Command | Description | Key flags |
|
|
40
|
+
| ---------- | ------------------------------------------------ | ------------------------------- |
|
|
41
|
+
| `config` | Manage configuration (`set-key`, `remove-key`, `show`) | — |
|
|
42
|
+
| `analyze` | Analyze buggy code → explanation + fix | `-f <file>` `-l <language>` |
|
|
43
|
+
| `explain` | Plain-English explanation of code | `-f <file>` `-l <language>` |
|
|
44
|
+
| `fix` | Corrected version of broken code | `-f <file>` `-l <language>` `-o <out>` |
|
|
45
|
+
| `generate` | Generate code from a description | `-d <description>` `-o <out>` |
|
|
46
|
+
| `scaffold` | Scaffold a React component | `-n <name>` `-o <out>` |
|
|
47
|
+
| `terminal` | Suggest terminal commands for a goal | `-g <goal>` |
|
|
48
|
+
| `history` | View past queries (local file or server) | — |
|
|
49
|
+
| `login` | Authenticate with a self-hosted server (optional) | — |
|
|
50
|
+
|
|
51
|
+
### Context-aware mode
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
devtk analyze -f ./src/index.js -l javascript --context
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`--context` scans nearby `.js`/`.ts`/`.json` files and includes them in the prompt. The scan respects `.gitignore`, skips dotfiles and credential-shaped filenames (`*.pem`, `*.key`, `id_rsa*`, `*credential*`, `*secret*`, …), and is capped at 50 files of up to 20KB each.
|
|
58
|
+
|
|
59
|
+
## Optional: Self-Hosted Server
|
|
60
|
+
|
|
61
|
+
The repo includes a full backend (Express API with JWT auth, per-user rate limiting, and MongoDB history) and a React web client. **There is no public hosted instance** — the server exists as a tested, self-hostable reference implementation and a demonstration of the project's backend engineering. If you run your own:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export DEV_TOOLKIT_API_URL=https://your-server.example.com
|
|
65
|
+
devtk login
|
|
66
|
+
devtk analyze -f ./src/buggy.js -l javascript
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for server setup. BYOK is the way to use the tool day to day.
|
|
70
|
+
|
|
71
|
+
| | BYOK | Self-hosted server |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| Account required | No | Yes (on your server) |
|
|
74
|
+
| OpenAI key | Yours, local | Your server's |
|
|
75
|
+
| History | Local file | Server + web UI |
|
|
76
|
+
| Privacy | Key and code stay local (OpenAI only) | Requests pass through your API |
|
|
77
|
+
|
|
78
|
+
## Architecture
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
BYOK: CLI ──────────────────────────────▶ OpenAI API
|
|
82
|
+
|
|
83
|
+
Self-hosted: CLI ──┐
|
|
84
|
+
├──▶ Express API (JWT auth, per-user rate limiting)
|
|
85
|
+
Web client ─┘ │
|
|
86
|
+
├──▶ OpenAI API
|
|
87
|
+
└──▶ MongoDB (users, history)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full picture, and [docs/RETROSPECTIVE.md](docs/RETROSPECTIVE.md) for an honest audit of the v1 codebase and what v2 fixed.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
Config file: `~/.dev-toolkit/config.json` (created with owner-only permissions; migrated automatically from `~/.dev-helper/` if you're upgrading).
|
|
95
|
+
|
|
96
|
+
| Field | Purpose |
|
|
97
|
+
|-------|---------|
|
|
98
|
+
| `openaiApiKey` | Your OpenAI key (BYOK mode) |
|
|
99
|
+
| `token` | JWT from `devtk login` (self-hosted server mode) |
|
|
100
|
+
| `apiUrl` | Your self-hosted server's URL |
|
|
101
|
+
|
|
102
|
+
Environment variables: `OPENAI_API_KEY` (overrides config key), `DEV_TOOLKIT_API_URL` (overrides API URL).
|
|
103
|
+
|
|
104
|
+
For self-hosting the server, see [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
|
|
105
|
+
|
|
106
|
+
## Development
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
npm install # CLI deps
|
|
110
|
+
npm test # CLI tests
|
|
111
|
+
cd server && npm install && npm test # server tests
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Contributing
|
|
115
|
+
|
|
116
|
+
Pull requests are welcome. Please open an issue first to discuss proposed changes.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT © [Matt Graba](https://mattgraba.com)
|
package/bin/devtk.js
ADDED
package/cli/cli.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ^ shebang line allows the script to be run directly from the terminal
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import 'dotenv/config';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
// Import per-command handlers
|
|
8
|
+
import handleAnalyze from './commands/analyzeCommand.js';
|
|
9
|
+
import handleExplain from './commands/explainCommand.js';
|
|
10
|
+
import handleFix from './commands/fixCommand.js';
|
|
11
|
+
import handleGenerate from './commands/generateCommand.js';
|
|
12
|
+
import handleScaffold from './commands/scaffoldCommand.js';
|
|
13
|
+
import handleTerminal from './commands/terminalCommand.js';
|
|
14
|
+
import handleLogin from './commands/loginCommand.js';
|
|
15
|
+
import handleHistory from './commands/historyCommand.js';
|
|
16
|
+
import handleConfig from './commands/configCommand.js';
|
|
17
|
+
|
|
18
|
+
// CLI Metadata: defines basic information when a user runs `devtk --help`
|
|
19
|
+
program
|
|
20
|
+
.name('devtk')
|
|
21
|
+
.description('Dev Toolkit — a developer CLI assistant powered by AI')
|
|
22
|
+
.version('2.0.1');
|
|
23
|
+
|
|
24
|
+
// Register commands
|
|
25
|
+
handleAnalyze(program);
|
|
26
|
+
handleExplain(program);
|
|
27
|
+
handleFix(program);
|
|
28
|
+
handleGenerate(program);
|
|
29
|
+
handleHistory(program);
|
|
30
|
+
handleLogin(program);
|
|
31
|
+
handleScaffold(program);
|
|
32
|
+
handleTerminal(program);
|
|
33
|
+
handleConfig(program);
|
|
34
|
+
|
|
35
|
+
// Triggers the CLI to interpret the command-line arguments passed by the user (e.g., `devtk analyze`)
|
|
36
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { handleAnalyzeBasic, handleAnalyzeWithContext } from './analyzeHandlers.js';
|
|
2
|
+
import handleWithContext from '../utils/contextHandlerWrapper.js';
|
|
3
|
+
|
|
4
|
+
export default (program) => {
|
|
5
|
+
program
|
|
6
|
+
.command('analyze')
|
|
7
|
+
.alias('a')
|
|
8
|
+
.description('Analyze code errors and receive an AI explanation and fix')
|
|
9
|
+
.usage('-f <file> -l <language> [--context]')
|
|
10
|
+
.requiredOption('-f, --file <path>', 'Path to code file with an error')
|
|
11
|
+
.requiredOption('-l, --language <name>', 'Programming language')
|
|
12
|
+
.option('--context', 'Include context from surrounding project files')
|
|
13
|
+
.showHelpAfterError(true)
|
|
14
|
+
.action((options) => {
|
|
15
|
+
handleWithContext({
|
|
16
|
+
options,
|
|
17
|
+
handleBasic: handleAnalyzeBasic,
|
|
18
|
+
handleWithContext: handleAnalyzeWithContext,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import { checkFileExists } from '../utils/fsUtils.js';
|
|
5
|
+
import { scanFiles } from '../utils/fileScanner.js';
|
|
6
|
+
import saveToHistory from '../utils/historySaver.js';
|
|
7
|
+
import runAICommand from '../utils/commandRunner.js';
|
|
8
|
+
import { renderAnalysisBox, parseAnalysisJSON } from '../utils/formatBox.js';
|
|
9
|
+
import { analyzeCode } from '../services/localOpenAI.js';
|
|
10
|
+
|
|
11
|
+
async function runAnalyze({ filePath, language, contextFiles }) {
|
|
12
|
+
const errorText = fs.readFileSync(path.resolve(filePath), 'utf-8');
|
|
13
|
+
const withContext = contextFiles.length > 0;
|
|
14
|
+
|
|
15
|
+
const data = await runAICommand({
|
|
16
|
+
spinnerText: `Analyzing ${filePath}${withContext ? ' with context' : ''}...`,
|
|
17
|
+
successText: withContext ? 'Contextual analysis complete ✅' : 'Analysis complete ✅',
|
|
18
|
+
failText: 'Analysis failed',
|
|
19
|
+
localFn: () => analyzeCode({ errorText, language, contextFiles }),
|
|
20
|
+
endpoint: '/analyze',
|
|
21
|
+
payload: { errorText, language, contextFiles },
|
|
22
|
+
});
|
|
23
|
+
if (!data) return;
|
|
24
|
+
|
|
25
|
+
// BYOK returns the raw model text; the server returns parsed { issues, suggestion }
|
|
26
|
+
const parsed = data.raw
|
|
27
|
+
? parseAnalysisJSON(data.raw)
|
|
28
|
+
: { issues: data.issues || [], suggestion: data.suggestion || '' };
|
|
29
|
+
|
|
30
|
+
console.log('');
|
|
31
|
+
renderAnalysisBox({ filePath, ...parsed });
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
await saveToHistory({
|
|
35
|
+
command: 'analyze',
|
|
36
|
+
input: errorText,
|
|
37
|
+
output: JSON.stringify(parsed),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function handleAnalyzeBasic({ filePath, language }) {
|
|
42
|
+
if (!checkFileExists(filePath)) return;
|
|
43
|
+
await runAnalyze({ filePath, language, contextFiles: [] });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function handleAnalyzeWithContext({ filePath, language }) {
|
|
47
|
+
if (!checkFileExists(filePath)) return;
|
|
48
|
+
const contextFiles = await scanFiles({ directory: path.dirname(filePath) });
|
|
49
|
+
await runAnalyze({ filePath, language, contextFiles });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
handleAnalyzeBasic,
|
|
54
|
+
handleAnalyzeWithContext,
|
|
55
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { handleSetKey, handleRemoveKey, handleShowConfig } from './configHandlers.js';
|
|
2
|
+
|
|
3
|
+
export default (program) => {
|
|
4
|
+
const config = program
|
|
5
|
+
.command('config')
|
|
6
|
+
.description('Manage Dev Toolkit configuration');
|
|
7
|
+
|
|
8
|
+
config
|
|
9
|
+
.command('set-key <api-key>')
|
|
10
|
+
.description('Set your OpenAI API key for BYOK (Bring Your Own Key) mode')
|
|
11
|
+
.action((apiKey) => {
|
|
12
|
+
handleSetKey(apiKey);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
config
|
|
16
|
+
.command('remove-key')
|
|
17
|
+
.description('Remove your stored OpenAI API key')
|
|
18
|
+
.action(() => {
|
|
19
|
+
handleRemoveKey();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
config
|
|
23
|
+
.command('show')
|
|
24
|
+
.description('Show current configuration')
|
|
25
|
+
.action(() => {
|
|
26
|
+
handleShowConfig();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Default action when just running `devtk config`
|
|
30
|
+
config.action(() => {
|
|
31
|
+
handleShowConfig();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import {
|
|
3
|
+
getConfig,
|
|
4
|
+
setConfigValue,
|
|
5
|
+
removeConfigValue,
|
|
6
|
+
getOpenAIKey,
|
|
7
|
+
maskKey,
|
|
8
|
+
CONFIG_FILE,
|
|
9
|
+
} from '../utils/configManager.js';
|
|
10
|
+
|
|
11
|
+
function handleSetKey(apiKey) {
|
|
12
|
+
if (!apiKey || typeof apiKey !== 'string' || !apiKey.trim()) {
|
|
13
|
+
console.error(chalk.red('Please provide a valid API key.'));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const trimmedKey = apiKey.trim();
|
|
18
|
+
|
|
19
|
+
if (!trimmedKey.startsWith('sk-')) {
|
|
20
|
+
console.warn(chalk.yellow('Warning: OpenAI API keys typically start with "sk-".'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
setConfigValue('openaiApiKey', trimmedKey);
|
|
24
|
+
console.log(chalk.green('OpenAI API key saved successfully.'));
|
|
25
|
+
console.log(chalk.dim(`Key: ${maskKey(trimmedKey)}`));
|
|
26
|
+
console.log(chalk.dim(`Location: ${CONFIG_FILE}`));
|
|
27
|
+
console.log(chalk.cyan('\nYou can now use devtk commands without logging in.'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function handleRemoveKey() {
|
|
31
|
+
const config = getConfig();
|
|
32
|
+
if (!config.openaiApiKey) {
|
|
33
|
+
console.log(chalk.yellow('No API key configured.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
removeConfigValue('openaiApiKey');
|
|
38
|
+
console.log(chalk.green('OpenAI API key removed.'));
|
|
39
|
+
console.log(chalk.dim('You will need to login or set a new key to use AI commands.'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleShowConfig() {
|
|
43
|
+
const config = getConfig();
|
|
44
|
+
const envKey = process.env.OPENAI_API_KEY;
|
|
45
|
+
|
|
46
|
+
console.log(chalk.cyan('\nDev Toolkit Configuration\n'));
|
|
47
|
+
|
|
48
|
+
// Token status
|
|
49
|
+
if (config.token) {
|
|
50
|
+
console.log(chalk.green(' Auth token: ') + chalk.dim('configured'));
|
|
51
|
+
} else {
|
|
52
|
+
console.log(chalk.yellow(' Auth token: ') + chalk.dim('not set'));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// API URL
|
|
56
|
+
if (process.env.DEV_TOOLKIT_API_URL) {
|
|
57
|
+
console.log(chalk.green(' API URL: ') + process.env.DEV_TOOLKIT_API_URL + chalk.dim(' (from env var)'));
|
|
58
|
+
} else if (config.apiUrl) {
|
|
59
|
+
console.log(chalk.green(' API URL: ') + config.apiUrl);
|
|
60
|
+
} else {
|
|
61
|
+
console.log(chalk.dim(' API URL: not set (only needed for a self-hosted server)'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// OpenAI key status
|
|
65
|
+
console.log('');
|
|
66
|
+
if (envKey) {
|
|
67
|
+
console.log(chalk.green(' OpenAI key: ') + maskKey(envKey) + chalk.dim(' (from OPENAI_API_KEY env var)'));
|
|
68
|
+
} else if (config.openaiApiKey) {
|
|
69
|
+
console.log(chalk.green(' OpenAI key: ') + maskKey(config.openaiApiKey) + chalk.dim(' (from config file)'));
|
|
70
|
+
} else {
|
|
71
|
+
console.log(chalk.yellow(' OpenAI key: ') + chalk.dim('not set'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Mode explanation
|
|
75
|
+
console.log('');
|
|
76
|
+
const localKey = getOpenAIKey();
|
|
77
|
+
if (localKey) {
|
|
78
|
+
console.log(chalk.green(' Mode: ') + 'BYOK (using your own OpenAI key)');
|
|
79
|
+
console.log(chalk.dim(' AI commands will call OpenAI directly - no login required.'));
|
|
80
|
+
} else if (config.token) {
|
|
81
|
+
console.log(chalk.green(' Mode: ') + 'Self-hosted server (logged in)');
|
|
82
|
+
console.log(chalk.dim(' AI commands will use the configured API URL.'));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.yellow(' Mode: ') + 'Not configured');
|
|
85
|
+
console.log(chalk.dim(' Run "devtk config set-key <key>" to get started.'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(chalk.dim(`Config file: ${CONFIG_FILE}`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
handleSetKey,
|
|
94
|
+
handleRemoveKey,
|
|
95
|
+
handleShowConfig,
|
|
96
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { handleExplainBasic, handleExplainWithContext } from './explainHandlers.js';
|
|
2
|
+
import handleWithContext from '../utils/contextHandlerWrapper.js';
|
|
3
|
+
|
|
4
|
+
export default (program) => {
|
|
5
|
+
program
|
|
6
|
+
.command('explain')
|
|
7
|
+
.alias('e')
|
|
8
|
+
.description('Explain a code file line by line using AI')
|
|
9
|
+
.usage('-f <file> -l <language> [--context]')
|
|
10
|
+
.requiredOption('-f, --file <path>', 'Path to the code file to explain')
|
|
11
|
+
.requiredOption('-l, --language <name>', 'Programming language')
|
|
12
|
+
.option('--context', 'Include context from surrounding project files')
|
|
13
|
+
.showHelpAfterError(true)
|
|
14
|
+
.action((options) => {
|
|
15
|
+
handleWithContext({
|
|
16
|
+
options,
|
|
17
|
+
handleBasic: handleExplainBasic,
|
|
18
|
+
handleWithContext: handleExplainWithContext,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
import { checkFileExists } from '../utils/fsUtils.js';
|
|
6
|
+
import { scanFiles } from '../utils/fileScanner.js';
|
|
7
|
+
import saveToHistory from '../utils/historySaver.js';
|
|
8
|
+
import runAICommand from '../utils/commandRunner.js';
|
|
9
|
+
import { explainCode } from '../services/localOpenAI.js';
|
|
10
|
+
|
|
11
|
+
async function runExplain({ filePath, language, contextFiles }) {
|
|
12
|
+
const codeSnippet = fs.readFileSync(path.resolve(filePath), 'utf-8');
|
|
13
|
+
const withContext = contextFiles.length > 0;
|
|
14
|
+
|
|
15
|
+
const data = await runAICommand({
|
|
16
|
+
spinnerText: `Explaining ${filePath}${withContext ? ' with context' : ''}...`,
|
|
17
|
+
successText: withContext ? 'Contextual explanation complete ✅' : 'Explanation complete ✅',
|
|
18
|
+
failText: 'Explanation failed',
|
|
19
|
+
localFn: () => explainCode({ codeSnippet, language, contextFiles }),
|
|
20
|
+
endpoint: '/explain',
|
|
21
|
+
payload: { codeSnippet, language, contextFiles },
|
|
22
|
+
});
|
|
23
|
+
if (!data) return;
|
|
24
|
+
|
|
25
|
+
console.log(chalk.green('\n📖 Explanation:\n'), data.explanation);
|
|
26
|
+
|
|
27
|
+
await saveToHistory({
|
|
28
|
+
command: 'explain',
|
|
29
|
+
input: codeSnippet,
|
|
30
|
+
output: data.explanation,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handleExplainBasic({ filePath, language }) {
|
|
35
|
+
if (!checkFileExists(filePath)) return;
|
|
36
|
+
await runExplain({ filePath, language, contextFiles: [] });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function handleExplainWithContext({ filePath, language }) {
|
|
40
|
+
if (!checkFileExists(filePath)) return;
|
|
41
|
+
const contextFiles = await scanFiles({ directory: path.dirname(filePath) });
|
|
42
|
+
await runExplain({ filePath, language, contextFiles });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
handleExplainBasic,
|
|
47
|
+
handleExplainWithContext,
|
|
48
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { handleFixBasic, handleFixWithContext } from './fixHandlers.js';
|
|
2
|
+
import handleWithContext from '../utils/contextHandlerWrapper.js';
|
|
3
|
+
|
|
4
|
+
export default (program) => {
|
|
5
|
+
program
|
|
6
|
+
.command('fix')
|
|
7
|
+
.alias('f')
|
|
8
|
+
.description('Fix buggy code using AI-generated improvements')
|
|
9
|
+
.usage('-f <file> -l <language> [--context] [--output <path>]')
|
|
10
|
+
.requiredOption('-f, --file <path>', 'Path to the file containing the buggy code')
|
|
11
|
+
.requiredOption('-l, --language <name>', 'Programming language (e.g., javascript, python)')
|
|
12
|
+
.option('--context', 'Include surrounding project files for more accurate fixes')
|
|
13
|
+
.option('--output <path>', 'Optional path to save the fixed output file')
|
|
14
|
+
.showHelpAfterError(true)
|
|
15
|
+
.action((options) => {
|
|
16
|
+
const { output, ...rest } = options;
|
|
17
|
+
handleWithContext({
|
|
18
|
+
options: { ...rest, outputPath: output },
|
|
19
|
+
handleBasic: handleFixBasic,
|
|
20
|
+
handleWithContext: handleFixWithContext,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
import { checkFileExists } from '../utils/fsUtils.js';
|
|
6
|
+
import { scanFiles } from '../utils/fileScanner.js';
|
|
7
|
+
import saveToHistory from '../utils/historySaver.js';
|
|
8
|
+
import runAICommand from '../utils/commandRunner.js';
|
|
9
|
+
import { fixCode } from '../services/localOpenAI.js';
|
|
10
|
+
|
|
11
|
+
async function runFix({ filePath, language, outputPath, contextFiles }) {
|
|
12
|
+
const codeSnippet = fs.readFileSync(path.resolve(filePath), 'utf-8');
|
|
13
|
+
const withContext = contextFiles.length > 0;
|
|
14
|
+
|
|
15
|
+
const data = await runAICommand({
|
|
16
|
+
spinnerText: `Fixing ${filePath}${withContext ? ' with context' : ''}...`,
|
|
17
|
+
successText: withContext ? 'Fix with context complete ✅' : 'Fix complete ✅',
|
|
18
|
+
failText: 'Fix failed',
|
|
19
|
+
localFn: () => fixCode({ codeSnippet, language, contextFiles }),
|
|
20
|
+
endpoint: '/fix',
|
|
21
|
+
payload: { codeSnippet, language, contextFiles },
|
|
22
|
+
});
|
|
23
|
+
if (!data) return;
|
|
24
|
+
|
|
25
|
+
const { fixedCode } = data;
|
|
26
|
+
if (outputPath) {
|
|
27
|
+
fs.writeFileSync(outputPath, fixedCode, 'utf-8');
|
|
28
|
+
console.log(chalk.blue(`\n✅ Saved to ${outputPath}`));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.green('\n🔧 Fixed Code:\n'), fixedCode);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await saveToHistory({
|
|
34
|
+
command: 'fix',
|
|
35
|
+
input: codeSnippet,
|
|
36
|
+
output: fixedCode,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleFixBasic({ filePath, language, outputPath }) {
|
|
41
|
+
if (!checkFileExists(filePath)) return;
|
|
42
|
+
await runFix({ filePath, language, outputPath, contextFiles: [] });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleFixWithContext({ filePath, language, outputPath }) {
|
|
46
|
+
if (!checkFileExists(filePath)) return;
|
|
47
|
+
const contextFiles = await scanFiles({ directory: path.dirname(filePath) });
|
|
48
|
+
await runFix({ filePath, language, outputPath, contextFiles });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
handleFixBasic,
|
|
53
|
+
handleFixWithContext,
|
|
54
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { handleGenerateBasic } from './generateHandlers.js';
|
|
2
|
+
|
|
3
|
+
export default (program) => {
|
|
4
|
+
program
|
|
5
|
+
.command('generate')
|
|
6
|
+
.alias('g')
|
|
7
|
+
.description('Generate code from an AI-assisted text prompt')
|
|
8
|
+
.usage('-d <description> [--output <path>] [--name <value>] [--template <value>]')
|
|
9
|
+
.requiredOption('-d, --description <text>', 'Natural language description of the desired code')
|
|
10
|
+
.option('--output <path>', 'Optional path to save generated code')
|
|
11
|
+
.option('--name <value>', 'Optional name for generated component (if applicable)')
|
|
12
|
+
.option('--template <value>', 'Optional template or structure type')
|
|
13
|
+
.showHelpAfterError(true)
|
|
14
|
+
.action((options) => {
|
|
15
|
+
const { output, ...rest } = options;
|
|
16
|
+
const fixedOptions = { ...rest, outputPath: output };
|
|
17
|
+
handleGenerateBasic(fixedOptions);
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -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 { generateCode } from '../services/localOpenAI.js';
|
|
8
|
+
|
|
9
|
+
async function handleGenerateBasic({ description, outputPath }) {
|
|
10
|
+
const data = await runAICommand({
|
|
11
|
+
spinnerText: 'Generating code...',
|
|
12
|
+
successText: 'Generation complete ✅',
|
|
13
|
+
failText: 'Generation failed',
|
|
14
|
+
localFn: () => generateCode({ description }),
|
|
15
|
+
endpoint: '/generate',
|
|
16
|
+
payload: { description },
|
|
17
|
+
});
|
|
18
|
+
if (!data) return;
|
|
19
|
+
|
|
20
|
+
const { generatedCode } = data;
|
|
21
|
+
if (outputPath) {
|
|
22
|
+
const fullPath = path.resolve(outputPath);
|
|
23
|
+
fs.writeFileSync(fullPath, generatedCode, 'utf-8');
|
|
24
|
+
console.log(chalk.blue(`\n✅ Saved to ${fullPath}`));
|
|
25
|
+
} else {
|
|
26
|
+
console.log(chalk.green('\n🧠 Generated Code:\n'), generatedCode);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await saveToHistory({
|
|
30
|
+
command: 'generate',
|
|
31
|
+
input: description,
|
|
32
|
+
output: generatedCode,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
handleGenerateBasic,
|
|
38
|
+
};
|