codepolisher-cli 1.1.0
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/LICENSE +21 -0
- package/README.md +250 -0
- package/bin/codepolisher.js +2 -0
- package/package.json +51 -0
- package/src/api.js +149 -0
- package/src/commands/config-cmd.js +71 -0
- package/src/commands/review.js +77 -0
- package/src/config.js +239 -0
- package/src/display.js +86 -0
- package/src/index.js +28 -0
- package/src/prompt.js +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ImPerial TeK. Solutions
|
|
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,250 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=0:6366f1,100:a855f7&height=160§ion=header&text=CodePolisher%20CLI&fontSize=48&fontColor=ffffff&fontAlignY=38&desc=AI-powered%20code%20review%20from%20your%20terminal&descAlignY=58&descSize=16" width="100%" />
|
|
4
|
+
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://github.com/v1ral-ITS/codepolisher-cli/actions/workflows/test.yml)
|
|
10
|
+
[](#supported-providers)
|
|
11
|
+
[](package.json)
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
> **Polish code. Catch issues. Ship with confidence.**
|
|
16
|
+
> Review files or piped source with the AI provider you already use—without tying your workflow to a hosted backend.
|
|
17
|
+
|
|
18
|
+
<br/>
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [How it works](#how-it-works)
|
|
27
|
+
- [Supported Providers](#supported-providers)
|
|
28
|
+
- [Install](#install)
|
|
29
|
+
- [Usage](#usage)
|
|
30
|
+
- [Configuration](#configuration)
|
|
31
|
+
- [Architecture](#architecture)
|
|
32
|
+
- [Security](#security)
|
|
33
|
+
- [Development](#development)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
CodePolisher reads a file or standard input, builds a focused review prompt, sends it directly to your selected provider, and renders structured findings for a human or CI pipeline.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Source Code Review Pipeline Result
|
|
43
|
+
┌──────────────┐ ┌────────────────────┐ ┌─────────────────┐
|
|
44
|
+
│ file.js │────▶│ Detect language │────▶│ Summary │
|
|
45
|
+
│ script.py │────▶│ Apply focus/rules │────▶│ Inline findings │
|
|
46
|
+
│ stdin pipe │────▶│ Call your provider │────▶│ JSON or terminal│
|
|
47
|
+
└──────────────┘ └────────────────────┘ └─────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Your provider key stays under your control. CodePolisher CLI does not require Base44 and does not proxy reviews through the CodePolisher website.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Supported Providers
|
|
55
|
+
|
|
56
|
+
| Provider | Config name | Environment variable | Default model |
|
|
57
|
+
|----------|-------------|----------------------|---------------|
|
|
58
|
+
| OpenAI | `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
|
59
|
+
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
|
|
60
|
+
| Google Gemini | `gemini` | `GEMINI_API_KEY` | `gemini-1.5-flash` |
|
|
61
|
+
| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-chat` |
|
|
62
|
+
| Venice.ai | `venice` | `VENICE_API_KEY` | `zai-org-glm-5-1` |
|
|
63
|
+
| Groq | `groq` | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
|
|
64
|
+
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | Account default |
|
|
65
|
+
| Ollama | `ollama` | None required | `llama3.2` |
|
|
66
|
+
|
|
67
|
+
Larger files automatically use the provider's configured large-file model unless you set an explicit model.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
### Directly from GitHub
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install -g git+https://github.com/v1ral-ITS/codepolisher-cli.git
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### From source
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git clone https://github.com/v1ral-ITS/codepolisher-cli.git
|
|
83
|
+
cd codepolisher-cli
|
|
84
|
+
npm install
|
|
85
|
+
npm link
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
> **Requires:** Node.js 18 or newer
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Review a file
|
|
96
|
+
codepolisher review src/index.js
|
|
97
|
+
|
|
98
|
+
# Review piped code
|
|
99
|
+
cat src/index.js | codepolisher review --language javascript
|
|
100
|
+
|
|
101
|
+
# Focus the review
|
|
102
|
+
codepolisher review app.py --focus security,performance
|
|
103
|
+
|
|
104
|
+
# Run a security audit
|
|
105
|
+
codepolisher review server.js --security
|
|
106
|
+
|
|
107
|
+
# Apply project-specific rules
|
|
108
|
+
codepolisher review api.ts --rules "Require error handling and input validation."
|
|
109
|
+
|
|
110
|
+
# Emit machine-readable output for CI
|
|
111
|
+
codepolisher review app.js --output json --fail-on-critical
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Review controls
|
|
115
|
+
|
|
116
|
+
| Option | Purpose |
|
|
117
|
+
|--------|---------|
|
|
118
|
+
| `--language <lang>` | Supply a language hint |
|
|
119
|
+
| `--focus <areas>` | Focus on security, performance, readability, error handling, best practices, or testing |
|
|
120
|
+
| `--rules <text>` | Add custom review rules |
|
|
121
|
+
| `--strict` | Flag every issue |
|
|
122
|
+
| `--security` | Run a security-focused audit |
|
|
123
|
+
| `--output json` | Produce JSON for scripts and CI |
|
|
124
|
+
| `--fail-on-critical` | Exit with code 1 when critical issues are found |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Choose a provider
|
|
132
|
+
codepolisher config set provider openai
|
|
133
|
+
|
|
134
|
+
# Save a provider-specific API key
|
|
135
|
+
codepolisher config set api-key <your-key>
|
|
136
|
+
|
|
137
|
+
# Optionally override the model
|
|
138
|
+
codepolisher config set model gpt-4o
|
|
139
|
+
|
|
140
|
+
# Inspect or clear configuration
|
|
141
|
+
codepolisher config get
|
|
142
|
+
codepolisher config clear
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Environment variables work without a local configuration file, so CI can inject provider keys through repository secrets.
|
|
146
|
+
|
|
147
|
+
For Ollama, no API key is required:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
codepolisher config set provider ollama
|
|
151
|
+
codepolisher config set model llama3.2
|
|
152
|
+
codepolisher config set ollama-host http://localhost:11434
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Architecture
|
|
158
|
+
|
|
159
|
+
<details>
|
|
160
|
+
<summary>View project structure</summary>
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
bin/
|
|
164
|
+
└── codepolisher.js # Executable entry point
|
|
165
|
+
src/
|
|
166
|
+
├── index.js # Commander program and commands
|
|
167
|
+
├── api.js # Provider adapters and response parsing
|
|
168
|
+
├── config.js # Provider detection and secure local config
|
|
169
|
+
├── prompt.js # Review and security prompt builders
|
|
170
|
+
├── display.js # Human-readable terminal output
|
|
171
|
+
└── commands/
|
|
172
|
+
├── review.js # File/stdin review workflow
|
|
173
|
+
└── config-cmd.js # Configuration commands
|
|
174
|
+
test/
|
|
175
|
+
└── providers.test.js # Provider endpoint and environment tests
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
### Design principles
|
|
181
|
+
|
|
182
|
+
- 🔌 **Provider-independent** — use a cloud provider, an aggregator, or local Ollama
|
|
183
|
+
- 🔐 **Keys stay yours** — environment variables are first-class and saved keys are provider-specific
|
|
184
|
+
- 🧰 **Terminal-native** — review files, pipes, and CI jobs without a browser
|
|
185
|
+
- 🤖 **Automation-ready** — structured JSON and meaningful failure exit codes
|
|
186
|
+
- 🧩 **Extensible** — provider adapters and prompts are isolated by responsibility
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Security
|
|
191
|
+
|
|
192
|
+
Saved configuration lives in `~/.codepolisher/config.json`. CodePolisher applies restrictive permissions on macOS and Linux and a private current-user ACL on Windows. It fails instead of silently saving when those protections cannot be applied.
|
|
193
|
+
|
|
194
|
+
For CI and shared machines, prefer provider environment variables and your platform's encrypted secret store.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Development
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Install exact dependencies
|
|
202
|
+
npm ci
|
|
203
|
+
|
|
204
|
+
# Run the test suite
|
|
205
|
+
npm test
|
|
206
|
+
|
|
207
|
+
# Exercise the CLI locally
|
|
208
|
+
node bin/codepolisher.js --help
|
|
209
|
+
|
|
210
|
+
# Verify the package contents before publishing
|
|
211
|
+
npm pack --dry-run
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Tests run on Node.js 18, 20, and 22 through GitHub Actions.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
[MIT](LICENSE) — use it however you want.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
<div align="center">
|
|
225
|
+
|
|
226
|
+
<br/>
|
|
227
|
+
|
|
228
|
+
<img src="https://i.ibb.co/gFJwwVL4/ITSolutions-LOGO.jpg" alt="ImPerial TeK. Solutions" width="120" />
|
|
229
|
+
|
|
230
|
+
<br/>
|
|
231
|
+
|
|
232
|
+
**Bear Carrington**
|
|
233
|
+
|
|
234
|
+
*Founder | ImPerial TeK. Solutions (ITSolutions)*
|
|
235
|
+
|
|
236
|
+
📧 [ITSolutions_MGNT@proton.me](mailto:ITSolutions_MGNT@proton.me) · 🌐 [codepolisher.app](https://codepolisher.app)
|
|
237
|
+
|
|
238
|
+
<br/>
|
|
239
|
+
|
|
240
|
+
*Innovating technology with precision and integrity.*
|
|
241
|
+
|
|
242
|
+
© ImPerial TeK. Solutions — All Rights Reserved
|
|
243
|
+
|
|
244
|
+
<br/>
|
|
245
|
+
|
|
246
|
+
*If CodePolisher CLI improved your workflow, consider giving it a star ⭐*
|
|
247
|
+
|
|
248
|
+
<img src="https://capsule-render.vercel.app/api?type=waving&color=0:a855f7,100:6366f1&height=80§ion=footer" width="100%" />
|
|
249
|
+
|
|
250
|
+
</div>
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codepolisher-cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Provider-independent AI code review in your terminal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"cli",
|
|
8
|
+
"code-review",
|
|
9
|
+
"developer-tools",
|
|
10
|
+
"openai",
|
|
11
|
+
"anthropic",
|
|
12
|
+
"gemini",
|
|
13
|
+
"ollama"
|
|
14
|
+
],
|
|
15
|
+
"author": "Bear Carrington (ImPerial TeK. Solutions) <ITSolutions_MGNT@proton.me> (https://codepolisher.app)",
|
|
16
|
+
"homepage": "https://github.com/v1ral-ITS/codepolisher-cli#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/v1ral-ITS/codepolisher-cli/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/v1ral-ITS/codepolisher-cli.git"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"codepolisher": "bin/codepolisher.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin",
|
|
30
|
+
"src",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test",
|
|
35
|
+
"start": "node bin/codepolisher.js",
|
|
36
|
+
"prepublishOnly": "npm test"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.3.0",
|
|
43
|
+
"commander": "^12.0.0",
|
|
44
|
+
"ora": "^8.0.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public",
|
|
48
|
+
"registry": "https://registry.npmjs.org/"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { DEFAULT_MODELS } from './config.js';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// JSON extraction helper — handles plain JSON or markdown-fenced JSON blocks
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function extractJSON(text) {
|
|
7
|
+
// Try direct parse first
|
|
8
|
+
try { return JSON.parse(text); } catch { /**/ }
|
|
9
|
+
// Strip markdown code fence
|
|
10
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
11
|
+
if (match) {
|
|
12
|
+
try { return JSON.parse(match[1].trim()); } catch { /**/ }
|
|
13
|
+
}
|
|
14
|
+
// Last resort: find first { ... } block
|
|
15
|
+
const start = text.indexOf('{');
|
|
16
|
+
const end = text.lastIndexOf('}');
|
|
17
|
+
if (start !== -1 && end !== -1) {
|
|
18
|
+
try { return JSON.parse(text.slice(start, end + 1)); } catch { /**/ }
|
|
19
|
+
}
|
|
20
|
+
throw new Error('Could not parse JSON from model response.\n\nRaw response:\n' + text.slice(0, 500));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function pickModel(config, codeLines) {
|
|
24
|
+
if (config.model) return config.model;
|
|
25
|
+
const tier = codeLines > 400 ? 'large' : 'small';
|
|
26
|
+
return DEFAULT_MODELS[config.provider]?.[tier] || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OPENAI_COMPATIBLE_PROVIDERS = {
|
|
30
|
+
openai: {
|
|
31
|
+
label: 'OpenAI',
|
|
32
|
+
url: 'https://api.openai.com/v1/chat/completions',
|
|
33
|
+
},
|
|
34
|
+
deepseek: {
|
|
35
|
+
label: 'DeepSeek',
|
|
36
|
+
url: 'https://api.deepseek.com/chat/completions',
|
|
37
|
+
},
|
|
38
|
+
venice: {
|
|
39
|
+
label: 'Venice',
|
|
40
|
+
url: 'https://api.venice.ai/api/v1/chat/completions',
|
|
41
|
+
},
|
|
42
|
+
groq: {
|
|
43
|
+
label: 'Groq',
|
|
44
|
+
url: 'https://api.groq.com/openai/v1/chat/completions',
|
|
45
|
+
},
|
|
46
|
+
openrouter: {
|
|
47
|
+
label: 'OpenRouter',
|
|
48
|
+
url: 'https://openrouter.ai/api/v1/chat/completions',
|
|
49
|
+
headers: {
|
|
50
|
+
'HTTP-Referer': 'https://codepolisher.app',
|
|
51
|
+
'X-OpenRouter-Title': 'CodePolisher CLI',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Provider implementations
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
async function callOpenAICompatible(config, prompt, codeLines) {
|
|
61
|
+
const provider = OPENAI_COMPATIBLE_PROVIDERS[config.provider];
|
|
62
|
+
const model = pickModel(config, codeLines);
|
|
63
|
+
const res = await fetch(provider.url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
'Authorization': `Bearer ${config.api_key}`,
|
|
68
|
+
...(provider.headers || {}),
|
|
69
|
+
},
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
...(model ? { model } : {}),
|
|
72
|
+
response_format: { type: 'json_object' },
|
|
73
|
+
messages: [{ role: 'user', content: prompt }],
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw new Error(`${provider.label} error ${res.status}: ${await res.text()}`);
|
|
77
|
+
const data = await res.json();
|
|
78
|
+
return extractJSON(data.choices[0].message.content);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function callAnthropic(config, prompt, codeLines) {
|
|
82
|
+
const model = pickModel(config, codeLines) || 'claude-3-5-haiku-20241022';
|
|
83
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
'x-api-key': config.api_key,
|
|
88
|
+
'anthropic-version': '2023-06-01',
|
|
89
|
+
},
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
model,
|
|
92
|
+
max_tokens: 4096,
|
|
93
|
+
messages: [{ role: 'user', content: prompt }],
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw new Error(`Anthropic error ${res.status}: ${await res.text()}`);
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
return extractJSON(data.content[0].text);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function callGemini(config, prompt, codeLines) {
|
|
102
|
+
const model = pickModel(config, codeLines) || 'gemini-1.5-flash';
|
|
103
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${config.api_key}`;
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
109
|
+
generationConfig: { responseMimeType: 'application/json' },
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) throw new Error(`Gemini error ${res.status}: ${await res.text()}`);
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
return extractJSON(data.candidates[0].content.parts[0].text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function callOllama(config, prompt, codeLines) {
|
|
118
|
+
const model = pickModel(config, codeLines) || 'llama3.2';
|
|
119
|
+
const host = config.ollama_host || 'http://localhost:11434';
|
|
120
|
+
const res = await fetch(`${host}/api/chat`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
model,
|
|
125
|
+
format: 'json',
|
|
126
|
+
stream: false,
|
|
127
|
+
messages: [{ role: 'user', content: prompt }],
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
if (!res.ok) throw new Error(`Ollama error ${res.status}: ${await res.text()}`);
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
return extractJSON(data.message.content);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Public entry point
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
export async function invokeLLM(config, { prompt, codeLines = 0 }) {
|
|
139
|
+
if (OPENAI_COMPATIBLE_PROVIDERS[config.provider]) {
|
|
140
|
+
return callOpenAICompatible(config, prompt, codeLines);
|
|
141
|
+
}
|
|
142
|
+
switch (config.provider) {
|
|
143
|
+
case 'anthropic': return callAnthropic(config, prompt, codeLines);
|
|
144
|
+
case 'gemini': return callGemini(config, prompt, codeLines);
|
|
145
|
+
case 'ollama': return callOllama(config, prompt, codeLines);
|
|
146
|
+
default:
|
|
147
|
+
throw new Error(`Unknown provider "${config.provider}". Run: codepolisher config set provider <name>`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { loadConfig, saveConfig, saveProviderApiKey, PROVIDERS, PROVIDER_ENV_KEYS, DEFAULT_MODELS } from '../config.js';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
const ALLOWED_KEYS = {
|
|
5
|
+
'provider': 'provider',
|
|
6
|
+
'api-key': 'api_key',
|
|
7
|
+
'model': 'model',
|
|
8
|
+
'ollama-host': 'ollama_host',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function configCommand(program) {
|
|
12
|
+
const cmd = program
|
|
13
|
+
.command('config')
|
|
14
|
+
.description('Manage CodePolisher CLI configuration');
|
|
15
|
+
|
|
16
|
+
cmd
|
|
17
|
+
.command('set <key> <value>')
|
|
18
|
+
.description(`Set a config value. Keys: ${Object.keys(ALLOWED_KEYS).join(', ')}`)
|
|
19
|
+
.action((key, value) => {
|
|
20
|
+
if (!ALLOWED_KEYS[key]) {
|
|
21
|
+
console.error(chalk.red(`Unknown key "${key}".`));
|
|
22
|
+
console.error(chalk.dim(`Valid keys: ${Object.keys(ALLOWED_KEYS).join(', ')}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (key === 'provider' && !PROVIDERS.includes(value)) {
|
|
26
|
+
console.error(chalk.red(`Unknown provider "${value}".`));
|
|
27
|
+
console.error(chalk.dim(`Valid providers: ${PROVIDERS.join(', ')}`));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
if (key === 'api-key') {
|
|
31
|
+
saveProviderApiKey(loadConfig().provider, value);
|
|
32
|
+
} else {
|
|
33
|
+
saveConfig({ [ALLOWED_KEYS[key]]: value });
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk.green(`✔ Saved ${key}`));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
cmd
|
|
39
|
+
.command('get')
|
|
40
|
+
.description('Show current config')
|
|
41
|
+
.action(() => {
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
const envKey = PROVIDER_ENV_KEYS[config.provider];
|
|
44
|
+
const defaultModel = DEFAULT_MODELS[config.provider]?.small || '(provider default)';
|
|
45
|
+
console.log('');
|
|
46
|
+
console.log(` ${chalk.dim('provider')} ${chalk.cyan(config.provider)}`);
|
|
47
|
+
const keyStatus = config.api_key
|
|
48
|
+
? config.api_key_source === envKey
|
|
49
|
+
? chalk.green(`[set via ${envKey}]`)
|
|
50
|
+
: chalk.green('[set in config]')
|
|
51
|
+
: envKey
|
|
52
|
+
? chalk.yellow(`(set ${envKey} or run config set api-key)`)
|
|
53
|
+
: chalk.dim('(not required)');
|
|
54
|
+
console.log(` ${chalk.dim('api-key')} ${keyStatus}`);
|
|
55
|
+
console.log(` ${chalk.dim('model')} ${config.model ? chalk.cyan(config.model) : chalk.dim(`(default: ${defaultModel})`)}`);
|
|
56
|
+
if (config.provider === 'ollama') {
|
|
57
|
+
console.log(` ${chalk.dim('ollama-host')} ${chalk.cyan(config.ollama_host)}`);
|
|
58
|
+
}
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(chalk.dim(` Supported providers: ${PROVIDERS.join(', ')}`));
|
|
61
|
+
console.log('');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
cmd
|
|
65
|
+
.command('clear')
|
|
66
|
+
.description('Remove all saved config')
|
|
67
|
+
.action(() => {
|
|
68
|
+
saveConfig({ provider: null, api_key: null, api_keys: null, model: null, ollama_host: null });
|
|
69
|
+
console.log(chalk.yellow('Config cleared.'));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { requireConfig } from '../config.js';
|
|
5
|
+
import { invokeLLM } from '../api.js';
|
|
6
|
+
import { buildReviewPrompt, buildSecurityPrompt } from '../prompt.js';
|
|
7
|
+
import { printHeader, printSummary, printComments, printFooter, printError } from '../display.js';
|
|
8
|
+
|
|
9
|
+
export async function reviewCommand(filePath, options) {
|
|
10
|
+
const config = requireConfig();
|
|
11
|
+
|
|
12
|
+
// Read code
|
|
13
|
+
let code, filename;
|
|
14
|
+
if (filePath) {
|
|
15
|
+
try {
|
|
16
|
+
code = readFileSync(filePath, 'utf8');
|
|
17
|
+
filename = basename(filePath);
|
|
18
|
+
} catch {
|
|
19
|
+
printError(`Could not read file: ${filePath}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
// Read from stdin
|
|
24
|
+
code = readFileSync(0, 'utf8');
|
|
25
|
+
filename = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!code.trim()) {
|
|
29
|
+
printError('No code to review.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build settings from flags
|
|
34
|
+
const settings = {
|
|
35
|
+
focusAreas: options.focus ? options.focus.split(',').map(s => s.trim()) : [],
|
|
36
|
+
customRules: options.rules || '',
|
|
37
|
+
strictMode: !!options.strict,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const prompt = options.security
|
|
41
|
+
? buildSecurityPrompt(code, options.language || 'auto', filename)
|
|
42
|
+
: buildReviewPrompt(code, options.language || 'auto', filename, settings);
|
|
43
|
+
|
|
44
|
+
const codeLines = code.split('\n').length;
|
|
45
|
+
|
|
46
|
+
const spinner = ora(`Reviewing with ${config.provider}…`).start();
|
|
47
|
+
|
|
48
|
+
let results;
|
|
49
|
+
try {
|
|
50
|
+
results = await invokeLLM(config, {
|
|
51
|
+
prompt,
|
|
52
|
+
codeLines,
|
|
53
|
+
});
|
|
54
|
+
spinner.stop();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
spinner.stop();
|
|
57
|
+
printError(`Review failed: ${err.message || err}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// JSON output mode
|
|
62
|
+
if (options.output === 'json') {
|
|
63
|
+
console.log(JSON.stringify(results, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
printHeader(filename, results.detected_language);
|
|
68
|
+
printSummary(results);
|
|
69
|
+
printComments(results.comments);
|
|
70
|
+
printFooter();
|
|
71
|
+
|
|
72
|
+
// Exit with non-zero if critical issues found
|
|
73
|
+
const hasCritical = results.comments?.some(c => c.severity === 'critical' || c.severity === 'security');
|
|
74
|
+
if (hasCritical && options.failOnCritical) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, renameSync, unlinkSync } from 'fs';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = join(homedir(), '.codepolisher');
|
|
8
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
function currentWindowsIdentity() {
|
|
11
|
+
const { USERDOMAIN, USERNAME } = process.env;
|
|
12
|
+
if (USERDOMAIN && USERNAME) return `${USERDOMAIN}\\${USERNAME}`;
|
|
13
|
+
return execFileSync('whoami', [], {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
windowsHide: true,
|
|
16
|
+
}).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function windowsAclSids(path) {
|
|
20
|
+
const script = `
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
23
|
+
$acl = Get-Acl -LiteralPath $env:CODEPOLISHER_SECURE_PATH
|
|
24
|
+
$currentSid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
25
|
+
Write-Output $currentSid
|
|
26
|
+
foreach ($entry in $acl.Access) {
|
|
27
|
+
try {
|
|
28
|
+
Write-Output $entry.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
29
|
+
} catch {
|
|
30
|
+
Write-Output $entry.IdentityReference.Value
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
const encodedCommand = Buffer.from(script, 'utf16le').toString('base64');
|
|
35
|
+
const output = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encodedCommand], {
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
env: { ...process.env, CODEPOLISHER_SECURE_PATH: path },
|
|
39
|
+
});
|
|
40
|
+
const [currentSid, ...aclSids] = output.split(/\r?\n/).map(value => value.trim()).filter(Boolean);
|
|
41
|
+
return { currentSid, aclSids: [...new Set(aclSids)] };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function securePath(path, isDirectory = false) {
|
|
45
|
+
try {
|
|
46
|
+
if (process.platform === 'win32') {
|
|
47
|
+
const identity = currentWindowsIdentity();
|
|
48
|
+
const grant = isDirectory ? `${identity}:(OI)(CI)F` : `${identity}:(F)`;
|
|
49
|
+
execFileSync('icacls', [path, '/inheritance:r', '/grant:r', grant], {
|
|
50
|
+
stdio: 'ignore',
|
|
51
|
+
windowsHide: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const before = windowsAclSids(path);
|
|
55
|
+
for (const sid of before.aclSids) {
|
|
56
|
+
if (sid !== before.currentSid) {
|
|
57
|
+
const account = /^S-\d(?:-\d+)+$/.test(sid) ? `*${sid}` : sid;
|
|
58
|
+
execFileSync('icacls', [path, '/remove', account], {
|
|
59
|
+
stdio: 'ignore',
|
|
60
|
+
windowsHide: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const after = windowsAclSids(path);
|
|
66
|
+
if (after.aclSids.some(sid => sid !== after.currentSid)) {
|
|
67
|
+
throw new Error('Windows retained an access rule for another account');
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
chmodSync(path, isDirectory ? 0o700 : 0o600);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Could not secure CodePolisher config permissions for ${path}: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Supported providers
|
|
78
|
+
export const PROVIDERS = [
|
|
79
|
+
'openai',
|
|
80
|
+
'anthropic',
|
|
81
|
+
'gemini',
|
|
82
|
+
'deepseek',
|
|
83
|
+
'venice',
|
|
84
|
+
'groq',
|
|
85
|
+
'openrouter',
|
|
86
|
+
'ollama',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export const PROVIDER_ENV_KEYS = {
|
|
90
|
+
openai: 'OPENAI_API_KEY',
|
|
91
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
92
|
+
gemini: 'GEMINI_API_KEY',
|
|
93
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
94
|
+
venice: 'VENICE_API_KEY',
|
|
95
|
+
groq: 'GROQ_API_KEY',
|
|
96
|
+
openrouter:'OPENROUTER_API_KEY',
|
|
97
|
+
ollama: null, // local, no key needed
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const DEFAULT_MODELS = {
|
|
101
|
+
openai: { small: 'gpt-4o-mini', large: 'gpt-4o' },
|
|
102
|
+
anthropic: { small: 'claude-3-5-haiku-20241022', large: 'claude-sonnet-4-5' },
|
|
103
|
+
gemini: { small: 'gemini-1.5-flash', large: 'gemini-1.5-pro' },
|
|
104
|
+
deepseek: { small: 'deepseek-chat', large: 'deepseek-chat' },
|
|
105
|
+
venice: { small: 'zai-org-glm-5-1', large: 'zai-org-glm-5-1' },
|
|
106
|
+
groq: { small: 'openai/gpt-oss-20b', large: 'openai/gpt-oss-120b' },
|
|
107
|
+
openrouter:{ small: null, large: null },
|
|
108
|
+
ollama: { small: 'llama3.2', large: 'llama3.2' },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export function loadConfig() {
|
|
112
|
+
let fromFile = {};
|
|
113
|
+
if (existsSync(CONFIG_FILE)) {
|
|
114
|
+
try {
|
|
115
|
+
fromFile = JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore malformed config
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Determine provider (file > env fallback)
|
|
122
|
+
const savedProvider = PROVIDERS.includes(fromFile.provider) ? fromFile.provider : null;
|
|
123
|
+
const provider = savedProvider || _detectProviderFromEnv() || 'openai';
|
|
124
|
+
|
|
125
|
+
// API key: provider-specific file entry > legacy file entry > environment.
|
|
126
|
+
const envKey = PROVIDER_ENV_KEYS[provider];
|
|
127
|
+
const providerApiKey = fromFile.api_keys?.[provider] || null;
|
|
128
|
+
const api_key = providerApiKey || fromFile.api_key || (envKey ? process.env[envKey] : null) || null;
|
|
129
|
+
const api_key_source = providerApiKey
|
|
130
|
+
? 'config'
|
|
131
|
+
: fromFile.api_key
|
|
132
|
+
? 'legacy-config'
|
|
133
|
+
: envKey && process.env[envKey]
|
|
134
|
+
? envKey
|
|
135
|
+
: null;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
provider,
|
|
139
|
+
api_key,
|
|
140
|
+
api_key_source,
|
|
141
|
+
model: fromFile.model || null,
|
|
142
|
+
ollama_host: fromFile.ollama_host || 'http://localhost:11434',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _detectProviderFromEnv() {
|
|
147
|
+
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
148
|
+
if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
|
|
149
|
+
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
150
|
+
if (process.env.DEEPSEEK_API_KEY) return 'deepseek';
|
|
151
|
+
if (process.env.VENICE_API_KEY) return 'venice';
|
|
152
|
+
if (process.env.GROQ_API_KEY) return 'groq';
|
|
153
|
+
if (process.env.OPENROUTER_API_KEY)return 'openrouter';
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function saveConfig(updates) {
|
|
158
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
159
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
160
|
+
}
|
|
161
|
+
securePath(CONFIG_DIR, true);
|
|
162
|
+
let existing = {};
|
|
163
|
+
if (existsSync(CONFIG_FILE)) {
|
|
164
|
+
try { existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { /**/ }
|
|
165
|
+
}
|
|
166
|
+
const next = { ...existing, ...updates };
|
|
167
|
+
const tempFile = join(CONFIG_DIR, `config.${process.pid}.${randomUUID()}.tmp`);
|
|
168
|
+
try {
|
|
169
|
+
writeFileSync(tempFile, JSON.stringify(next, null, 2), {
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
mode: 0o600,
|
|
172
|
+
flag: 'wx',
|
|
173
|
+
});
|
|
174
|
+
securePath(tempFile);
|
|
175
|
+
try {
|
|
176
|
+
renameSync(tempFile, CONFIG_FILE);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (process.platform !== 'win32' || !existsSync(CONFIG_FILE)) throw error;
|
|
179
|
+
unlinkSync(CONFIG_FILE);
|
|
180
|
+
renameSync(tempFile, CONFIG_FILE);
|
|
181
|
+
}
|
|
182
|
+
securePath(CONFIG_FILE);
|
|
183
|
+
} finally {
|
|
184
|
+
if (existsSync(tempFile)) unlinkSync(tempFile);
|
|
185
|
+
}
|
|
186
|
+
return next;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function saveProviderApiKey(provider, apiKey) {
|
|
190
|
+
let existing = {};
|
|
191
|
+
if (existsSync(CONFIG_FILE)) {
|
|
192
|
+
try { existing = JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { /**/ }
|
|
193
|
+
}
|
|
194
|
+
return saveConfig({
|
|
195
|
+
api_key: null,
|
|
196
|
+
api_keys: { ...(existing.api_keys || {}), [provider]: apiKey },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function requireConfig() {
|
|
201
|
+
const config = loadConfig();
|
|
202
|
+
const { provider } = config;
|
|
203
|
+
|
|
204
|
+
// Check if the user has explicitly saved a provider in the config file
|
|
205
|
+
let savedConfig = {};
|
|
206
|
+
if (existsSync(CONFIG_FILE)) {
|
|
207
|
+
try { savedConfig = JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); } catch { /**/ }
|
|
208
|
+
}
|
|
209
|
+
const providerExplicitlySet = PROVIDERS.includes(savedConfig.provider);
|
|
210
|
+
|
|
211
|
+
if (provider === 'ollama') {
|
|
212
|
+
// no key required
|
|
213
|
+
} else {
|
|
214
|
+
if (!config.api_key) {
|
|
215
|
+
// If no provider was explicitly chosen, show the full onboarding list
|
|
216
|
+
if (!providerExplicitlySet) {
|
|
217
|
+
console.error('\nNo AI provider configured. Choose one and get started:\n');
|
|
218
|
+
console.error(' Provider Set API key via Config command');
|
|
219
|
+
console.error(' ─────────────────────────────────────────────────────────────────────────');
|
|
220
|
+
console.error(' openai OPENAI_API_KEY env var codepolisher config set provider openai');
|
|
221
|
+
console.error(' anthropic ANTHROPIC_API_KEY env var codepolisher config set provider anthropic');
|
|
222
|
+
console.error(' gemini GEMINI_API_KEY env var codepolisher config set provider gemini');
|
|
223
|
+
console.error(' deepseek DEEPSEEK_API_KEY env var codepolisher config set provider deepseek');
|
|
224
|
+
console.error(' venice VENICE_API_KEY env var codepolisher config set provider venice');
|
|
225
|
+
console.error(' groq GROQ_API_KEY env var codepolisher config set provider groq');
|
|
226
|
+
console.error(' openrouter OPENROUTER_API_KEY env var codepolisher config set provider openrouter');
|
|
227
|
+
console.error(' ollama (no key needed — runs locally) codepolisher config set provider ollama');
|
|
228
|
+
console.error('\nThen set your key: codepolisher config set api-key <your-key>');
|
|
229
|
+
console.error('Or just export the env var and run again.\n');
|
|
230
|
+
} else {
|
|
231
|
+
const envKey = PROVIDER_ENV_KEYS[provider];
|
|
232
|
+
console.error(`\nNo API key found for provider "${provider}".\nRun: codepolisher config set api-key <key>\nOr set the ${envKey} environment variable.\n`);
|
|
233
|
+
}
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return config;
|
|
239
|
+
}
|
package/src/display.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const SEVERITY_COLOR = {
|
|
4
|
+
critical: (s) => chalk.bgRed.white.bold(` ${s.toUpperCase()} `),
|
|
5
|
+
security: (s) => chalk.bgMagenta.white.bold(` ${s.toUpperCase()} `),
|
|
6
|
+
warning: (s) => chalk.bgYellow.black.bold(` ${s.toUpperCase()} `),
|
|
7
|
+
suggestion: (s) => chalk.bgBlue.white(` ${s.toUpperCase()} `),
|
|
8
|
+
performance: (s) => chalk.bgCyan.black(` ${s.toUpperCase()} `),
|
|
9
|
+
good: (s) => chalk.bgGreen.black(` ${s.toUpperCase()} `),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function badge(severity) {
|
|
13
|
+
const fn = SEVERITY_COLOR[severity] || ((s) => chalk.bgGray.white(` ${s.toUpperCase()} `));
|
|
14
|
+
return fn(severity);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function printHeader(filename, language) {
|
|
18
|
+
const lang = language ? chalk.cyan(language) : '';
|
|
19
|
+
const file = chalk.bold(filename || 'stdin');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
22
|
+
console.log(` ${chalk.bold.white('CodePolisher')} ${file} ${lang}`);
|
|
23
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function printSummary(results) {
|
|
27
|
+
const { comments = [], summary } = results;
|
|
28
|
+
|
|
29
|
+
const counts = {
|
|
30
|
+
critical: comments.filter(c => c.severity === 'critical').length,
|
|
31
|
+
security: comments.filter(c => c.severity === 'security').length,
|
|
32
|
+
warning: comments.filter(c => c.severity === 'warning').length,
|
|
33
|
+
suggestion: comments.filter(c => c.severity === 'suggestion' || c.severity === 'performance').length,
|
|
34
|
+
good: comments.filter(c => c.severity === 'good').length,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(
|
|
39
|
+
` ${chalk.red.bold(counts.critical)} critical ` +
|
|
40
|
+
`${chalk.magenta.bold(counts.security)} security ` +
|
|
41
|
+
`${chalk.yellow.bold(counts.warning)} warnings ` +
|
|
42
|
+
`${chalk.blue.bold(counts.suggestion)} suggestions ` +
|
|
43
|
+
`${chalk.green.bold(counts.good)} good`
|
|
44
|
+
);
|
|
45
|
+
console.log('');
|
|
46
|
+
|
|
47
|
+
if (summary) {
|
|
48
|
+
console.log(chalk.dim(' Summary'));
|
|
49
|
+
console.log(` ${chalk.white(summary)}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function printComments(comments = []) {
|
|
55
|
+
if (!comments.length) {
|
|
56
|
+
console.log(chalk.green(' No issues found.'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const order = ['critical', 'security', 'warning', 'performance', 'suggestion', 'good'];
|
|
61
|
+
const sorted = [...comments].sort((a, b) => {
|
|
62
|
+
return order.indexOf(a.severity) - order.indexOf(b.severity);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(chalk.dim(' Issues'));
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
for (const c of sorted) {
|
|
69
|
+
const line = c.line ? chalk.dim(`line ${c.line}`) : chalk.dim('general');
|
|
70
|
+
console.log(` ${badge(c.severity)} ${line}`);
|
|
71
|
+
console.log(` ${chalk.white(c.message)}`);
|
|
72
|
+
if (c.fix) {
|
|
73
|
+
console.log(` ${chalk.dim('fix:')} ${chalk.cyan(c.fix)}`);
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function printFooter() {
|
|
80
|
+
console.log(chalk.dim('─'.repeat(60)));
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function printError(msg) {
|
|
85
|
+
console.error(`\n${chalk.red.bold('Error:')} ${msg}\n`);
|
|
86
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { reviewCommand } from './commands/review.js';
|
|
3
|
+
import { configCommand } from './commands/config-cmd.js';
|
|
4
|
+
|
|
5
|
+
const program = new Command();
|
|
6
|
+
|
|
7
|
+
program
|
|
8
|
+
.name('codepolisher')
|
|
9
|
+
.description('AI-powered code review in your terminal — by ImPerial TeK. Solutions')
|
|
10
|
+
.version('1.1.0');
|
|
11
|
+
|
|
12
|
+
// review command
|
|
13
|
+
program
|
|
14
|
+
.command('review [file]')
|
|
15
|
+
.description('Review a file (or pipe code via stdin)')
|
|
16
|
+
.option('-l, --language <lang>', 'Language hint (default: auto-detect)')
|
|
17
|
+
.option('-f, --focus <areas>', 'Comma-separated focus areas: security,performance,readability,error_handling,best_practices,testing')
|
|
18
|
+
.option('-r, --rules <text>', 'Custom review rules')
|
|
19
|
+
.option('--strict', 'Strict mode — flag every issue')
|
|
20
|
+
.option('--security', 'Run a full security audit')
|
|
21
|
+
.option('--output <format>', 'Output format: pretty (default) or json')
|
|
22
|
+
.option('--fail-on-critical', 'Exit code 1 if critical/security issues found')
|
|
23
|
+
.action(reviewCommand);
|
|
24
|
+
|
|
25
|
+
// config command
|
|
26
|
+
configCommand(program);
|
|
27
|
+
|
|
28
|
+
program.parse(process.argv);
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export const REVIEW_SCHEMA = {
|
|
2
|
+
type: "object",
|
|
3
|
+
properties: {
|
|
4
|
+
detected_language: { type: "string" },
|
|
5
|
+
summary: { type: "string" },
|
|
6
|
+
comments: {
|
|
7
|
+
type: "array",
|
|
8
|
+
items: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
severity: { type: "string" },
|
|
12
|
+
line: { type: "number" },
|
|
13
|
+
message: { type: "string" },
|
|
14
|
+
fix: { type: "string" },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
corrected_code: { type: "string" },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const FOCUS_AREA_OPTIONS = [
|
|
23
|
+
{ id: "security", label: "Security", description: "Prioritize vulnerabilities, injections, auth issues" },
|
|
24
|
+
{ id: "performance", label: "Performance", description: "Focus on speed, memory, algorithm efficiency" },
|
|
25
|
+
{ id: "readability", label: "Readability", description: "Emphasize naming, structure, documentation" },
|
|
26
|
+
{ id: "error_handling", label: "Error Handling", description: "Highlight missing try/catch, null checks, edge cases" },
|
|
27
|
+
{ id: "best_practices", label: "Best Practices", description: "Modern language features, patterns, standards" },
|
|
28
|
+
{ id: "testing", label: "Testability", description: "Suggest how code can be made more testable" },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function pickReviewModel(code) {
|
|
32
|
+
const lines = (code || "").split("\n").length;
|
|
33
|
+
if (lines > 400) return "claude_sonnet_4_6";
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildReviewPrompt(code, language = "auto", filename = null, settings = {}) {
|
|
38
|
+
const langLabel = language === "auto" ? "auto-detect the language" : language;
|
|
39
|
+
const focusAreas = settings.focusAreas || [];
|
|
40
|
+
const customRules = settings.customRules || "";
|
|
41
|
+
const strictMode = settings.strictMode || false;
|
|
42
|
+
|
|
43
|
+
const focusLines = focusAreas.length > 0
|
|
44
|
+
? `\n\nPRIORITY FOCUS AREAS (give extra weight to these):\n${focusAreas.map(id => {
|
|
45
|
+
const opt = FOCUS_AREA_OPTIONS.find(o => o.id === id);
|
|
46
|
+
return opt ? `- ${opt.label}: ${opt.description}` : `- ${id}`;
|
|
47
|
+
}).join("\n")}`
|
|
48
|
+
: "";
|
|
49
|
+
|
|
50
|
+
const customLines = customRules.trim()
|
|
51
|
+
? `\n\nCUSTOM REVIEW RULES (from user, follow strictly):\n${customRules.trim()}`
|
|
52
|
+
: "";
|
|
53
|
+
|
|
54
|
+
const strictLine = strictMode
|
|
55
|
+
? "\n\nSTRICT MODE: Be thorough and unforgiving. Flag every issue, even minor style problems."
|
|
56
|
+
: "";
|
|
57
|
+
|
|
58
|
+
return `You are an expert code reviewer for ALL scripting and programming languages. Analyze the following script and provide a detailed review.
|
|
59
|
+
|
|
60
|
+
Language: ${langLabel}${filename ? `\nFile: ${filename}` : ""}${focusLines}${customLines}${strictLine}
|
|
61
|
+
|
|
62
|
+
Script:
|
|
63
|
+
\`\`\`
|
|
64
|
+
${code}
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
Provide your review as a JSON object with this exact structure:
|
|
68
|
+
- "detected_language": the language you detected (string)
|
|
69
|
+
- "summary": a 2-3 sentence overall assessment of the code quality
|
|
70
|
+
- "comments": an array of review comments, each with:
|
|
71
|
+
- "severity": one of "critical", "warning", "suggestion", "performance", "security", "good"
|
|
72
|
+
- "line": the line number (number or null if general)
|
|
73
|
+
- "message": a clear explanation of the issue or suggestion
|
|
74
|
+
- "fix": a short code fix or recommendation (string or null)
|
|
75
|
+
- "corrected_code": the full corrected version of the script`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildSecurityPrompt(code, language = "auto", filename = null) {
|
|
79
|
+
return buildReviewPrompt(code, language, filename, {
|
|
80
|
+
focusAreas: ["security"],
|
|
81
|
+
customRules: `SECURITY AUDIT MODE: Focus exclusively on security vulnerabilities.
|
|
82
|
+
Check for: command injection, SQL injection, XSS, hardcoded credentials/secrets/API keys,
|
|
83
|
+
unsafe permissions (chmod 777 etc), path traversal, insecure deserialization,
|
|
84
|
+
improper authentication/authorization, use of deprecated insecure functions,
|
|
85
|
+
unvalidated input, sensitive data exposure, insecure dependencies, and privilege escalation risks.
|
|
86
|
+
Rate every finding by CVSS severity. Be exhaustive — missing a vulnerability is worse than a false positive.`,
|
|
87
|
+
});
|
|
88
|
+
}
|