codepolisher-cli 1.1.0 → 1.2.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/README.md CHANGED
@@ -1,250 +1,256 @@
1
- <div align="center">
2
-
3
- <img src="https://capsule-render.vercel.app/api?type=waving&color=0:6366f1,100:a855f7&height=160&section=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
- [![Node.js 18+](https://img.shields.io/badge/node.js-18+-6366f1?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
8
- [![License: MIT](https://img.shields.io/badge/license-MIT-a855f7?style=for-the-badge)](LICENSE)
9
- [![Tests](https://img.shields.io/badge/tests-4%20passing-22c55e?style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/v1ral-ITS/codepolisher-cli/actions/workflows/test.yml)
10
- [![Providers](https://img.shields.io/badge/AI%20providers-8-f59e0b?style=for-the-badge)](#supported-providers)
11
- [![Version](https://img.shields.io/badge/version-1.1.0-3b82f6?style=for-the-badge)](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
-
1
+ <div align="center">
2
+
3
+ <img src="https://capsule-render.vercel.app/api?type=waving&color=0:6366f1,100:a855f7&height=160&section=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
+ [![Node.js 18+](https://img.shields.io/badge/node.js-18+-6366f1?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-a855f7?style=for-the-badge)](LICENSE)
9
+ [![Tests](https://img.shields.io/badge/tests-10%20passing-22c55e?style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/v1ral-ITS/codepolisher-cli/actions/workflows/test.yml)
10
+ [![Providers](https://img.shields.io/badge/AI%20providers-8-f59e0b?style=for-the-badge)](#supported-providers)
11
+ [![npm](https://img.shields.io/npm/v/codepolisher-cli?style=for-the-badge&logo=npm&logoColor=white&color=cb3837)](https://www.npmjs.com/package/codepolisher-cli)
12
+ [![npm downloads](https://img.shields.io/npm/dm/codepolisher-cli?style=for-the-badge&color=3b82f6)](https://www.npmjs.com/package/codepolisher-cli)
13
+
14
+ <br/>
15
+
16
+ > **Polish code. Catch issues. Ship with confidence.**
17
+ > Review files or piped source with the AI provider you already use—without tying your workflow to a hosted backend.
18
+
19
+ <br/>
20
+
21
+ </div>
22
+
23
+ ---
24
+
25
+ ## Table of Contents
26
+
27
+ - [How it works](#how-it-works)
28
+ - [Supported Providers](#supported-providers)
29
+ - [Install](#install)
30
+ - [Usage](#usage)
31
+ - [Configuration](#configuration)
32
+ - [Architecture](#architecture)
33
+ - [Security](#security)
34
+ - [Development](#development)
35
+
36
+ ---
37
+
38
+ ## How it works
39
+
40
+ 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. Every review renders the original and the AI-suggested correction side by side, line by line.
41
+
42
+ ```
43
+ Source Code Review Pipeline Result
44
+ ┌──────────────┐ ┌────────────────────┐ ┌─────────────────┐
45
+ file.js │────▶│ Detect language │────▶│ Summary
46
+ script.py │────▶│ Apply focus/rules │────▶│ Inline findings
47
+ │ stdin pipe │────▶│ Call your provider │────▶│ JSON or terminal│
48
+ └──────────────┘ └────────────────────┘ └─────────────────┘
49
+ ```
50
+
51
+ Your provider key stays under your control. CodePolisher CLI does not require Base44 and does not proxy reviews through the CodePolisher website.
52
+
53
+ ---
54
+
55
+ ## Supported Providers
56
+
57
+ | Provider | Config name | Environment variable | Default model |
58
+ |----------|-------------|----------------------|---------------|
59
+ | OpenAI | `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
60
+ | Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | `claude-3-5-haiku-20241022` |
61
+ | Google Gemini | `gemini` | `GEMINI_API_KEY` | `gemini-1.5-flash` |
62
+ | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-chat` |
63
+ | Venice.ai | `venice` | `VENICE_API_KEY` | `zai-org-glm-5-1` |
64
+ | Groq | `groq` | `GROQ_API_KEY` | `openai/gpt-oss-20b` |
65
+ | OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | Account default |
66
+ | Ollama | `ollama` | None required | `llama3.2` |
67
+
68
+ Larger files automatically use the provider's configured large-file model unless you set an explicit model.
69
+
70
+ ---
71
+
72
+ ## Install
73
+
74
+ ### From npm
75
+
76
+ CodePolisher CLI is officially published on the public npm registry.
77
+
78
+ ```bash
79
+ npm install -g codepolisher-cli
80
+ ```
81
+
82
+ ### From source
83
+
84
+ ```bash
85
+ git clone https://github.com/v1ral-ITS/codepolisher-cli.git
86
+ cd codepolisher-cli
87
+ npm install
88
+ npm link
89
+ ```
90
+
91
+ > **Requires:** Node.js 18 or newer
92
+
93
+ ---
94
+
95
+ ## Usage
96
+
97
+ ```bash
98
+ # Review a file
99
+ codepolisher review src/index.js
100
+
101
+ # Review piped code
102
+ cat src/index.js | codepolisher review --language javascript
103
+
104
+ # Focus the review
105
+ codepolisher review app.py --focus security,performance
106
+
107
+ # Run a security audit
108
+ codepolisher review server.js --security
109
+
110
+ # Apply project-specific rules
111
+ codepolisher review api.ts --rules "Require error handling and input validation."
112
+
110
113
  # Emit machine-readable output for CI
111
114
  codepolisher review app.js --output json --fail-on-critical
112
- ```
113
-
114
- ### Review controls
115
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 |
116
+ ```
117
+
118
+ ### Review controls
119
+
120
+ | Option | Purpose |
121
+ |--------|---------|
122
+ | `--language <lang>` | Supply a language hint |
123
+ | `--focus <areas>` | Focus on security, performance, readability, error handling, best practices, or testing |
124
+ | `--rules <text>` | Add custom review rules |
125
+ | `--strict` | Flag every issue |
126
+ | `--security` | Run a security-focused audit |
123
127
  | `--output json` | Produce JSON for scripts and CI |
124
128
  | `--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
129
+
130
+ ---
131
+
132
+ ## Configuration
133
+
134
+ ```bash
135
+ # Choose a provider
136
+ codepolisher config set provider openai
137
+
138
+ # Save a provider-specific API key
139
+ codepolisher config set api-key <your-key>
140
+
141
+ # Optionally override the model
142
+ codepolisher config set model gpt-4o
143
+
144
+ # Inspect or clear configuration
145
+ codepolisher config get
146
+ codepolisher config clear
147
+ ```
148
+
149
+ Environment variables work without a local configuration file, so CI can inject provider keys through repository secrets.
150
+
151
+ For Ollama, no API key is required:
152
+
153
+ ```bash
154
+ codepolisher config set provider ollama
155
+ codepolisher config set model llama3.2
156
+ codepolisher config set ollama-host http://localhost:11434
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Architecture
162
+
163
+ <details>
164
+ <summary>View project structure</summary>
165
+
166
+ ```
167
+ bin/
168
+ └── codepolisher.js # Executable entry point
169
+ src/
170
+ ├── index.js # Commander program and commands
171
+ ├── api.js # Provider adapters and response parsing
168
172
  ├── config.js # Provider detection and secure local config
169
173
  ├── prompt.js # Review and security prompt builders
174
+ ├── comparison.js # Line alignment and safe side-by-side comparisons
170
175
  ├── display.js # Human-readable terminal output
171
- └── commands/
172
- ├── review.js # File/stdin review workflow
173
- └── config-cmd.js # Configuration commands
176
+ └── commands/
177
+ ├── review.js # File/stdin review workflow
178
+ └── config-cmd.js # Configuration commands
174
179
  test/
180
+ ├── comparison.test.js # Comparison, normalization, and terminal safety tests
175
181
  └── 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) &nbsp;·&nbsp; 🌐 [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&section=footer" width="100%" />
249
-
250
- </div>
182
+ ```
183
+
184
+ </details>
185
+
186
+ ### Design principles
187
+
188
+ - 🔌 **Provider-independent** — use a cloud provider, an aggregator, or local Ollama
189
+ - 🔐 **Keys stay yours** — environment variables are first-class and saved keys are provider-specific
190
+ - 🧰 **Terminal-native** — review files, pipes, and CI jobs without a browser
191
+ - 🤖 **Automation-ready** — structured JSON and meaningful failure exit codes
192
+ - 🧩 **Extensible** — provider adapters and prompts are isolated by responsibility
193
+
194
+ ---
195
+
196
+ ## Security
197
+
198
+ 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.
199
+
200
+ For CI and shared machines, prefer provider environment variables and your platform's encrypted secret store.
201
+
202
+ ---
203
+
204
+ ## Development
205
+
206
+ ```bash
207
+ # Install exact dependencies
208
+ npm ci
209
+
210
+ # Run the test suite
211
+ npm test
212
+
213
+ # Exercise the CLI locally
214
+ node bin/codepolisher.js --help
215
+
216
+ # Verify the package contents before publishing
217
+ npm pack --dry-run
218
+ ```
219
+
220
+ Tests run on Node.js 18, 20, and 22 through GitHub Actions.
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ [MIT](LICENSE) — use it however you want.
227
+
228
+ ---
229
+
230
+ <div align="center">
231
+
232
+ <br/>
233
+
234
+ <img src="https://i.ibb.co/gFJwwVL4/ITSolutions-LOGO.jpg" alt="ImPerial TeK. Solutions" width="120" />
235
+
236
+ <br/>
237
+
238
+ **Bear Carrington**
239
+
240
+ *Founder | ImPerial TeK. Solutions (ITSolutions)*
241
+
242
+ 📧 [ITSolutions_MGNT@proton.me](mailto:ITSolutions_MGNT@proton.me) &nbsp;·&nbsp; 🌐 [codepolisher.app](https://codepolisher.app)
243
+
244
+ <br/>
245
+
246
+ *Innovating technology with precision and integrity.*
247
+
248
+ © ImPerial TeK. Solutions — All Rights Reserved
249
+
250
+ <br/>
251
+
252
+ *If CodePolisher CLI improved your workflow, consider giving it a star ⭐*
253
+
254
+ <img src="https://capsule-render.vercel.app/api?type=waving&color=0:a855f7,100:6366f1&height=80&section=footer" width="100%" />
255
+
256
+ </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codepolisher-cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Provider-independent AI code review in your terminal",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,77 +1,87 @@
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
- }
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 { normalizeCorrectedCode } from '../comparison.js';
8
+ import {
9
+ printHeader,
10
+ printSummary,
11
+ printComments,
12
+ printComparison,
13
+ printFooter,
14
+ printError,
15
+ } from '../display.js';
16
+
17
+ export async function reviewCommand(filePath, options) {
18
+ const config = requireConfig();
19
+
20
+ // Read code
21
+ let code, filename;
22
+ if (filePath) {
23
+ try {
24
+ code = readFileSync(filePath, 'utf8');
25
+ filename = basename(filePath);
26
+ } catch {
27
+ printError(`Could not read file: ${filePath}`);
28
+ process.exit(1);
29
+ }
30
+ } else {
31
+ // Read from stdin
32
+ code = readFileSync(0, 'utf8');
33
+ filename = null;
34
+ }
35
+
36
+ if (!code.trim()) {
37
+ printError('No code to review.');
38
+ process.exit(1);
39
+ }
40
+
41
+ // Build settings from flags
42
+ const settings = {
43
+ focusAreas: options.focus ? options.focus.split(',').map(s => s.trim()) : [],
44
+ customRules: options.rules || '',
45
+ strictMode: !!options.strict,
46
+ };
47
+
48
+ const prompt = options.security
49
+ ? buildSecurityPrompt(code, options.language || 'auto', filename)
50
+ : buildReviewPrompt(code, options.language || 'auto', filename, settings);
51
+
52
+ const codeLines = code.split('\n').length;
53
+
54
+ const spinner = ora(`Reviewing with ${config.provider}…`).start();
55
+
56
+ let results;
57
+ try {
58
+ results = await invokeLLM(config, {
59
+ prompt,
60
+ codeLines,
61
+ });
62
+ spinner.stop();
63
+ } catch (err) {
64
+ spinner.stop();
65
+ printError(`Review failed: ${err.message || err}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ // JSON output mode
70
+ if (options.output === 'json') {
71
+ console.log(JSON.stringify(results, null, 2));
72
+ return;
73
+ }
74
+
75
+ printHeader(filename, results.detected_language);
76
+ printSummary(results);
77
+ printComments(results.comments);
78
+ const correctedCode = normalizeCorrectedCode(results.corrected_code);
79
+ printComparison(code, correctedCode);
80
+ printFooter();
81
+
82
+ // Exit with non-zero if critical issues found
83
+ const hasCritical = results.comments?.some(c => c.severity === 'critical' || c.severity === 'security');
84
+ if (hasCritical && options.failOnCritical) {
85
+ process.exit(1);
86
+ }
87
+ }
@@ -0,0 +1,126 @@
1
+ const MAX_LCS_CELLS = 2_000_000;
2
+ const ANSI_SEQUENCE = /\u001B(?:\][^\u0007]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/g;
3
+
4
+ export function sanitizeTerminalText(value) {
5
+ return String(value ?? '')
6
+ .replace(ANSI_SEQUENCE, '')
7
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, '');
8
+ }
9
+
10
+ export function normalizeCorrectedCode(value) {
11
+ const code = String(value ?? '');
12
+ const fenced = code.match(/^\s*```[^\n]*\n([\s\S]*?)\n```\s*$/);
13
+ return fenced ? fenced[1] : code;
14
+ }
15
+
16
+ export function hasMeaningfulChanges(original, corrected) {
17
+ if (!String(corrected ?? '').trim()) return false;
18
+ const normalize = (value) => String(value ?? '').replace(/\r\n/g, '\n');
19
+ return normalize(original) !== normalize(corrected);
20
+ }
21
+
22
+ function splitLines(value) {
23
+ return sanitizeTerminalText(value).replace(/\r\n/g, '\n').split('\n');
24
+ }
25
+
26
+ function fallbackRows(left, right) {
27
+ const rows = [];
28
+ const length = Math.max(left.length, right.length);
29
+ for (let index = 0; index < length; index += 1) {
30
+ const before = index < left.length ? left[index] : null;
31
+ const after = index < right.length ? right[index] : null;
32
+ rows.push({
33
+ kind: before === after ? 'same' : before === null ? 'added' : after === null ? 'removed' : 'changed',
34
+ before: before === null ? null : { number: index + 1, text: before },
35
+ after: after === null ? null : { number: index + 1, text: after },
36
+ });
37
+ }
38
+ return rows;
39
+ }
40
+
41
+ export function compareLines(original, corrected) {
42
+ const left = splitLines(original);
43
+ const right = splitLines(corrected);
44
+ if (left.length * right.length > MAX_LCS_CELLS) return fallbackRows(left, right);
45
+
46
+ const width = right.length + 1;
47
+ const table = new Uint32Array((left.length + 1) * width);
48
+ for (let i = left.length - 1; i >= 0; i -= 1) {
49
+ for (let j = right.length - 1; j >= 0; j -= 1) {
50
+ table[(i * width) + j] = left[i] === right[j]
51
+ ? table[((i + 1) * width) + j + 1] + 1
52
+ : Math.max(table[((i + 1) * width) + j], table[(i * width) + j + 1]);
53
+ }
54
+ }
55
+
56
+ const operations = [];
57
+ let i = 0;
58
+ let j = 0;
59
+ while (i < left.length && j < right.length) {
60
+ if (left[i] === right[j]) {
61
+ operations.push({ kind: 'same', number: i + 1, otherNumber: j + 1, text: left[i] });
62
+ i += 1;
63
+ j += 1;
64
+ } else if (table[((i + 1) * width) + j] >= table[(i * width) + j + 1]) {
65
+ operations.push({ kind: 'removed', number: i + 1, text: left[i] });
66
+ i += 1;
67
+ } else {
68
+ operations.push({ kind: 'added', number: j + 1, text: right[j] });
69
+ j += 1;
70
+ }
71
+ }
72
+ while (i < left.length) operations.push({ kind: 'removed', number: ++i, text: left[i - 1] });
73
+ while (j < right.length) operations.push({ kind: 'added', number: ++j, text: right[j - 1] });
74
+
75
+ const rows = [];
76
+ for (let cursor = 0; cursor < operations.length;) {
77
+ const operation = operations[cursor];
78
+ if (operation.kind === 'same') {
79
+ rows.push({
80
+ kind: 'same',
81
+ before: { number: operation.number, text: operation.text },
82
+ after: { number: operation.otherNumber, text: operation.text },
83
+ });
84
+ cursor += 1;
85
+ continue;
86
+ }
87
+
88
+ const removed = [];
89
+ const added = [];
90
+ while (cursor < operations.length && operations[cursor].kind !== 'same') {
91
+ const change = operations[cursor];
92
+ (change.kind === 'removed' ? removed : added).push(change);
93
+ cursor += 1;
94
+ }
95
+ const changeLength = Math.max(removed.length, added.length);
96
+ for (let changeIndex = 0; changeIndex < changeLength; changeIndex += 1) {
97
+ const before = removed[changeIndex] || null;
98
+ const after = added[changeIndex] || null;
99
+ rows.push({
100
+ kind: before && after ? 'changed' : before ? 'removed' : 'added',
101
+ before: before ? { number: before.number, text: before.text } : null,
102
+ after: after ? { number: after.number, text: after.text } : null,
103
+ });
104
+ }
105
+ }
106
+ return rows;
107
+ }
108
+
109
+ export function changedHunks(rows, context = 2) {
110
+ const selected = new Set();
111
+ rows.forEach((row, index) => {
112
+ if (row.kind === 'same') return;
113
+ for (let nearby = Math.max(0, index - context); nearby <= Math.min(rows.length - 1, index + context); nearby += 1) {
114
+ selected.add(nearby);
115
+ }
116
+ });
117
+
118
+ const output = [];
119
+ let previous = -2;
120
+ for (const index of [...selected].sort((a, b) => a - b)) {
121
+ if (index > previous + 1) output.push({ kind: 'separator', before: null, after: null });
122
+ output.push(rows[index]);
123
+ previous = index;
124
+ }
125
+ return output;
126
+ }
package/src/display.js CHANGED
@@ -1,86 +1,151 @@
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
- }
1
+ import chalk from 'chalk';
2
+ import {
3
+ compareLines,
4
+ sanitizeTerminalText,
5
+ } from './comparison.js';
6
+
7
+ const SEVERITY_COLOR = {
8
+ critical: (s) => chalk.bgRed.white.bold(` ${s.toUpperCase()} `),
9
+ security: (s) => chalk.bgMagenta.white.bold(` ${s.toUpperCase()} `),
10
+ warning: (s) => chalk.bgYellow.black.bold(` ${s.toUpperCase()} `),
11
+ suggestion: (s) => chalk.bgBlue.white(` ${s.toUpperCase()} `),
12
+ performance: (s) => chalk.bgCyan.black(` ${s.toUpperCase()} `),
13
+ good: (s) => chalk.bgGreen.black(` ${s.toUpperCase()} `),
14
+ };
15
+
16
+ function badge(severity) {
17
+ const normalized = typeof severity === 'string' && severity ? severity : 'unknown';
18
+ const fn = SEVERITY_COLOR[normalized] || ((s) => chalk.bgGray.white(` ${s.toUpperCase()} `));
19
+ return fn(normalized);
20
+ }
21
+
22
+ export function printHeader(filename, language) {
23
+ const lang = language ? chalk.cyan(sanitizeTerminalText(language)) : '';
24
+ const file = chalk.bold(sanitizeTerminalText(filename || 'stdin'));
25
+ console.log('');
26
+ console.log(chalk.dim('─'.repeat(60)));
27
+ console.log(` ${chalk.bold.white('CodePolisher')} ${file} ${lang}`);
28
+ console.log(chalk.dim('─'.repeat(60)));
29
+ }
30
+
31
+ export function printSummary(results) {
32
+ const { summary, correction_summary: correctionSummary } = results;
33
+ const comments = Array.isArray(results.comments) ? results.comments : [];
34
+
35
+ const counts = {
36
+ critical: comments.filter(c => c.severity === 'critical').length,
37
+ security: comments.filter(c => c.severity === 'security').length,
38
+ warning: comments.filter(c => c.severity === 'warning').length,
39
+ suggestion: comments.filter(c => c.severity === 'suggestion' || c.severity === 'performance').length,
40
+ good: comments.filter(c => c.severity === 'good').length,
41
+ };
42
+
43
+ console.log('');
44
+ console.log(
45
+ ` ${chalk.red.bold(counts.critical)} critical ` +
46
+ `${chalk.magenta.bold(counts.security)} security ` +
47
+ `${chalk.yellow.bold(counts.warning)} warnings ` +
48
+ `${chalk.blue.bold(counts.suggestion)} suggestions ` +
49
+ `${chalk.green.bold(counts.good)} good`
50
+ );
51
+ console.log('');
52
+
53
+ if (summary) {
54
+ console.log(chalk.dim(' Summary'));
55
+ console.log(` ${chalk.white(sanitizeTerminalText(summary))}`);
56
+ console.log('');
57
+ }
58
+
59
+ if (correctionSummary) {
60
+ console.log(chalk.dim(' Correction approach'));
61
+ console.log(` ${chalk.white(sanitizeTerminalText(correctionSummary))}`);
62
+ console.log('');
63
+ }
64
+ }
65
+
66
+ export function printComments(comments = []) {
67
+ if (!Array.isArray(comments)) comments = [];
68
+ if (!comments.length) {
69
+ console.log(chalk.green(' No issues found.'));
70
+ return;
71
+ }
72
+
73
+ const order = ['critical', 'security', 'warning', 'performance', 'suggestion', 'good'];
74
+ const sorted = [...comments].sort((a, b) => {
75
+ return order.indexOf(a.severity) - order.indexOf(b.severity);
76
+ });
77
+
78
+ console.log(chalk.dim(' Issues'));
79
+ console.log('');
80
+
81
+ for (const c of sorted) {
82
+ const line = c.line ? chalk.dim(`line ${c.line}`) : chalk.dim('general');
83
+ console.log(` ${badge(c.severity)} ${line}`);
84
+ console.log(` ${chalk.white(sanitizeTerminalText(c.message))}`);
85
+ if (c.impact) {
86
+ console.log(` ${chalk.dim('why it matters:')} ${chalk.white(sanitizeTerminalText(c.impact))}`);
87
+ }
88
+ if (c.fix) {
89
+ console.log(` ${chalk.dim('recommended change:')} ${chalk.cyan(sanitizeTerminalText(c.fix))}`);
90
+ }
91
+ console.log('');
92
+ }
93
+ }
94
+
95
+ function truncate(value, width) {
96
+ if (value.length <= width) return value.padEnd(width);
97
+ if (width <= 1) return value.slice(0, width);
98
+ return `${value.slice(0, width - 1)}…`;
99
+ }
100
+
101
+ function comparisonCell(line, marker, width) {
102
+ if (!line) return ' '.repeat(width);
103
+ const prefix = `${String(line.number).padStart(4)} ${marker} `;
104
+ return prefix + truncate(line.text.replace(/\t/g, ' '), Math.max(1, width - prefix.length));
105
+ }
106
+
107
+ export function printComparison(original, corrected, columns = process.stdout.columns || 120) {
108
+ if (!String(corrected ?? '').trim()) {
109
+ console.log(chalk.yellow(' The provider did not return a corrected script.'));
110
+ console.log('');
111
+ return;
112
+ }
113
+
114
+ const rows = compareLines(original, corrected);
115
+ const visibleRows = rows.length <= 400
116
+ ? rows
117
+ : [
118
+ ...rows.slice(0, 200),
119
+ { kind: 'separator', before: null, after: null, omitted: rows.length - 400 },
120
+ ...rows.slice(-200),
121
+ ];
122
+ const displayWidth = Math.min(Math.max(columns, 80), 200);
123
+ const cellWidth = Math.max(36, Math.floor((displayWidth - 5) / 2));
124
+ console.log(chalk.dim(' Side-by-side comparison (original vs. AI-suggested correction)'));
125
+ console.log(` ${chalk.bold(truncate('ORIGINAL', cellWidth))} | ${chalk.bold(truncate('CORRECTED', cellWidth))}`);
126
+ console.log(` ${'-'.repeat(cellWidth)}--+--${'-'.repeat(cellWidth)}`);
127
+
128
+ for (const row of visibleRows) {
129
+ if (row.kind === 'separator') {
130
+ const label = row.omitted ? `… ${row.omitted} comparison rows omitted …` : '…';
131
+ console.log(chalk.dim(` ${truncate(label, cellWidth)} | ${truncate(label, cellWidth)}`));
132
+ continue;
133
+ }
134
+ const left = comparisonCell(row.before, row.kind === 'same' ? ' ' : '-', cellWidth);
135
+ const right = comparisonCell(row.after, row.kind === 'same' ? ' ' : '+', cellWidth);
136
+ const color = row.kind === 'same' ? chalk.dim : row.kind === 'changed' ? chalk.yellow : chalk.cyan;
137
+ console.log(` ${color(left)} | ${color(right)}`);
138
+ }
139
+ console.log('');
140
+ console.log(chalk.dim(' Review this suggestion before using it in production.'));
141
+ console.log('');
142
+ }
143
+
144
+ export function printFooter() {
145
+ console.log(chalk.dim('─'.repeat(60)));
146
+ console.log('');
147
+ }
148
+
149
+ export function printError(msg) {
150
+ console.error(`\n${chalk.red.bold('Error:')} ${sanitizeTerminalText(msg)}\n`);
151
+ }
package/src/index.js CHANGED
@@ -1,28 +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);
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.2.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 CHANGED
@@ -2,16 +2,18 @@ export const REVIEW_SCHEMA = {
2
2
  type: "object",
3
3
  properties: {
4
4
  detected_language: { type: "string" },
5
- summary: { type: "string" },
5
+ summary: { type: "string" },
6
+ correction_summary: { type: "string" },
6
7
  comments: {
7
8
  type: "array",
8
9
  items: {
9
10
  type: "object",
10
11
  properties: {
11
12
  severity: { type: "string" },
12
- line: { type: "number" },
13
- message: { type: "string" },
14
- fix: { type: "string" },
13
+ line: { type: ["number", "null"] },
14
+ message: { type: "string" },
15
+ impact: { type: ["string", "null"] },
16
+ fix: { type: ["string", "null"] },
15
17
  },
16
18
  },
17
19
  },
@@ -66,14 +68,16 @@ ${code}
66
68
 
67
69
  Provide your review as a JSON object with this exact structure:
68
70
  - "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
+ - "summary": a 2-3 sentence overall assessment of the code quality
72
+ - "correction_summary": a 2-4 sentence explanation of the correction strategy, including important behavior changes or tradeoffs
73
+ - "comments": an array of review comments, each with:
71
74
  - "severity": one of "critical", "warning", "suggestion", "performance", "security", "good"
72
75
  - "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
- }
76
+ - "message": a detailed, plain-language explanation of the issue or suggestion
77
+ - "impact": why the issue matters in practice (string or null)
78
+ - "fix": a concrete code fix or recommendation (string or null)
79
+ - "corrected_code": the complete, runnable corrected script with all fixes applied; return code only, without Markdown fences`;
80
+ }
77
81
 
78
82
  export function buildSecurityPrompt(code, language = "auto", filename = null) {
79
83
  return buildReviewPrompt(code, language, filename, {