codepolisher-cli 1.2.0 → 1.3.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 CHANGED
@@ -1,21 +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.
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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
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
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)
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
10
  [![Providers](https://img.shields.io/badge/AI%20providers-8-f59e0b?style=for-the-badge)](#supported-providers)
11
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
12
  [![npm downloads](https://img.shields.io/npm/dm/codepolisher-cli?style=for-the-badge&color=3b82f6)](https://www.npmjs.com/package/codepolisher-cli)
@@ -37,7 +37,7 @@
37
37
 
38
38
  ## How it works
39
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.
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 terminal review shows the original and AI-suggested correction side by side. After reviewing the comparison, you can save the corrected script under a new filename without changing the original.
41
41
 
42
42
  ```
43
43
  Source Code Review Pipeline Result
@@ -110,11 +110,15 @@ codepolisher review server.js --security
110
110
  # Apply project-specific rules
111
111
  codepolisher review api.ts --rules "Require error handling and input validation."
112
112
 
113
- # Emit machine-readable output for CI
114
- codepolisher review app.js --output json --fail-on-critical
115
-
113
+ # Emit machine-readable output for CI
114
+ codepolisher review app.js --output json --fail-on-critical
115
+
116
+ # Save the suggested correction without an interactive prompt
117
+ codepolisher review app.js --save-as app.fixed.js
116
118
  ```
117
119
 
120
+ For an interactive file review, CodePolisher asks whether you want to save the suggested correction after displaying it. Press Enter to keep the original unchanged, or answer `y` and accept the suggested `*.polished.*` filename (or enter another new filename). Existing files are never overwritten.
121
+
118
122
  ### Review controls
119
123
 
120
124
  | Option | Purpose |
@@ -124,8 +128,9 @@ codepolisher review app.js --output json --fail-on-critical
124
128
  | `--rules <text>` | Add custom review rules |
125
129
  | `--strict` | Flag every issue |
126
130
  | `--security` | Run a security-focused audit |
127
- | `--output json` | Produce JSON for scripts and CI |
128
- | `--fail-on-critical` | Exit with code 1 when critical issues are found |
131
+ | `--output json` | Produce JSON for scripts and CI |
132
+ | `--save-as <file>` | Save the suggested correction to a new file without prompting |
133
+ | `--fail-on-critical` | Exit with code 1 when critical issues are found |
129
134
 
130
135
  ---
131
136
 
@@ -169,16 +174,18 @@ bin/
169
174
  src/
170
175
  ├── index.js # Commander program and commands
171
176
  ├── api.js # Provider adapters and response parsing
172
- ├── config.js # Provider detection and secure local config
173
- ├── prompt.js # Review and security prompt builders
174
- ├── comparison.js # Line alignment and safe side-by-side comparisons
175
- ├── display.js # Human-readable terminal output
177
+ ├── config.js # Provider detection and secure local config
178
+ ├── prompt.js # Review and security prompt builders
179
+ ├── comparison.js # Line alignment and safe side-by-side comparisons
180
+ ├── display.js # Human-readable terminal output
181
+ ├── save-correction.js # Safe corrected-script filenames and writes
176
182
  └── commands/
177
183
  ├── review.js # File/stdin review workflow
178
184
  └── config-cmd.js # Configuration commands
179
- test/
180
- ├── comparison.test.js # Comparison, normalization, and terminal safety tests
181
- └── providers.test.js # Provider endpoint and environment tests
185
+ test/
186
+ ├── comparison.test.js # Comparison, normalization, and terminal safety tests
187
+ ├── providers.test.js # Provider endpoint and environment tests
188
+ └── save-correction.test.js # Corrected-script save safety tests
182
189
  ```
183
190
 
184
191
  </details>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codepolisher-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Provider-independent AI code review in your terminal",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,87 +1,104 @@
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
- }
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 { hasMeaningfulChanges, normalizeCorrectedCode } from '../comparison.js';
8
+ import { promptToSaveCorrection, resolveSaveDestination, saveCorrectedCode } from '../save-correction.js';
9
+ import {
10
+ printHeader,
11
+ printSummary,
12
+ printComments,
13
+ printComparison,
14
+ printFooter,
15
+ printError,
16
+ } from '../display.js';
17
+
18
+ export async function reviewCommand(filePath, options) {
19
+ const config = requireConfig();
20
+
21
+ // Read code
22
+ let code, filename;
23
+ if (filePath) {
24
+ try {
25
+ code = readFileSync(filePath, 'utf8');
26
+ filename = basename(filePath);
27
+ } catch {
28
+ printError(`Could not read file: ${filePath}`);
29
+ process.exit(1);
30
+ }
31
+ } else {
32
+ // Read from stdin
33
+ code = readFileSync(0, 'utf8');
34
+ filename = null;
35
+ }
36
+
37
+ if (!code.trim()) {
38
+ printError('No code to review.');
39
+ process.exit(1);
40
+ }
41
+
42
+ // Build settings from flags
43
+ const settings = {
44
+ focusAreas: options.focus ? options.focus.split(',').map(s => s.trim()) : [],
45
+ customRules: options.rules || '',
46
+ strictMode: !!options.strict,
47
+ };
48
+
49
+ const prompt = options.security
50
+ ? buildSecurityPrompt(code, options.language || 'auto', filename)
51
+ : buildReviewPrompt(code, options.language || 'auto', filename, settings);
52
+
53
+ const codeLines = code.split('\n').length;
54
+
55
+ const spinner = ora(`Reviewing with ${config.provider}…`).start();
56
+
57
+ let results;
58
+ try {
59
+ results = await invokeLLM(config, {
60
+ prompt,
61
+ codeLines,
62
+ });
63
+ spinner.stop();
64
+ } catch (err) {
65
+ spinner.stop();
66
+ printError(`Review failed: ${err.message || err}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ // JSON output mode
71
+ if (options.output === 'json') {
72
+ console.log(JSON.stringify(results, null, 2));
73
+ return;
74
+ }
75
+
76
+ printHeader(filename, results.detected_language);
77
+ printSummary(results);
78
+ printComments(results.comments);
79
+ const correctedCode = normalizeCorrectedCode(results.corrected_code);
80
+ printComparison(code, correctedCode);
81
+ printFooter();
82
+
83
+ if (hasMeaningfulChanges(code, correctedCode)) {
84
+ try {
85
+ let savedPath = null;
86
+ if (options.saveAs) {
87
+ const destination = resolveSaveDestination(options.saveAs, filePath);
88
+ savedPath = await saveCorrectedCode(correctedCode, destination, filePath);
89
+ } else {
90
+ savedPath = await promptToSaveCorrection({ correctedCode, sourcePath: filePath });
91
+ }
92
+ if (savedPath) console.log(`Saved corrected script to ${savedPath}\n`);
93
+ } catch (error) {
94
+ printError(`Could not save corrected script: ${error.message || error}`);
95
+ process.exitCode = 1;
96
+ }
97
+ }
98
+
99
+ // Exit with non-zero if critical issues found
100
+ const hasCritical = results.comments?.some(c => c.severity === 'critical' || c.severity === 'security');
101
+ if (hasCritical && options.failOnCritical) {
102
+ process.exit(1);
103
+ }
104
+ }
package/src/comparison.js CHANGED
@@ -1,126 +1,128 @@
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
- }
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 (output.length > 0 && index > previous + 1) {
122
+ output.push({ kind: 'separator', before: null, after: null });
123
+ }
124
+ output.push(rows[index]);
125
+ previous = index;
126
+ }
127
+ return output;
128
+ }
package/src/display.js CHANGED
@@ -1,151 +1,151 @@
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
- }
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,29 @@
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);
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.3.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('--save-as <file>', 'Save the AI-suggested correction to a new file')
23
+ .option('--fail-on-critical', 'Exit code 1 if critical/security issues found')
24
+ .action(reviewCommand);
25
+
26
+ // config command
27
+ configCommand(program);
28
+
29
+ program.parse(process.argv);
package/src/prompt.js CHANGED
@@ -2,18 +2,18 @@ export const REVIEW_SCHEMA = {
2
2
  type: "object",
3
3
  properties: {
4
4
  detected_language: { type: "string" },
5
- summary: { type: "string" },
6
- correction_summary: { type: "string" },
5
+ summary: { type: "string" },
6
+ correction_summary: { type: "string" },
7
7
  comments: {
8
8
  type: "array",
9
9
  items: {
10
10
  type: "object",
11
11
  properties: {
12
12
  severity: { type: "string" },
13
- line: { type: ["number", "null"] },
14
- message: { type: "string" },
15
- impact: { type: ["string", "null"] },
16
- fix: { type: ["string", "null"] },
13
+ line: { type: ["number", "null"] },
14
+ message: { type: "string" },
15
+ impact: { type: ["string", "null"] },
16
+ fix: { type: ["string", "null"] },
17
17
  },
18
18
  },
19
19
  },
@@ -68,16 +68,16 @@ ${code}
68
68
 
69
69
  Provide your review as a JSON object with this exact structure:
70
70
  - "detected_language": the language you detected (string)
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
+ - "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:
74
74
  - "severity": one of "critical", "warning", "suggestion", "performance", "security", "good"
75
75
  - "line": the line number (number or null if general)
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
- }
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
+ }
81
81
 
82
82
  export function buildSecurityPrompt(code, language = "auto", filename = null) {
83
83
  return buildReviewPrompt(code, language, filename, {
@@ -0,0 +1,67 @@
1
+ import { constants } from 'fs';
2
+ import { access, writeFile } from 'fs/promises';
3
+ import { createInterface } from 'readline/promises';
4
+ import { dirname, parse, resolve } from 'path';
5
+
6
+ export function suggestedOutputPath(sourcePath) {
7
+ const absoluteSource = resolve(sourcePath);
8
+ const { dir, name, ext } = parse(absoluteSource);
9
+ return resolve(dir, `${name}.polished${ext}`);
10
+ }
11
+
12
+ async function pathExists(filePath) {
13
+ try {
14
+ await access(filePath, constants.F_OK);
15
+ return true;
16
+ } catch (error) {
17
+ if (error?.code === 'ENOENT') return false;
18
+ throw error;
19
+ }
20
+ }
21
+
22
+ export async function saveCorrectedCode(correctedCode, destinationPath, sourcePath = null) {
23
+ const destination = resolve(destinationPath);
24
+ if (sourcePath && destination === resolve(sourcePath)) {
25
+ throw new Error('Choose a different filename; the original file will not be overwritten.');
26
+ }
27
+
28
+ if (await pathExists(destination)) {
29
+ throw new Error(`File already exists: ${destination}`);
30
+ }
31
+
32
+ await writeFile(destination, correctedCode, { encoding: 'utf8', flag: 'wx' });
33
+ return destination;
34
+ }
35
+
36
+ export async function promptToSaveCorrection({ correctedCode, sourcePath, input = process.stdin, output = process.stdout }) {
37
+ if (!sourcePath || !input.isTTY || !output.isTTY) return null;
38
+
39
+ const prompt = createInterface({ input, output });
40
+ try {
41
+ const wantsSave = (await prompt.question('Would you like to save the suggested fix as a new file? (y/N) '))
42
+ .trim()
43
+ .toLowerCase();
44
+ if (wantsSave !== 'y' && wantsSave !== 'yes') return null;
45
+
46
+ const defaultPath = suggestedOutputPath(sourcePath);
47
+ while (true) {
48
+ const answer = (await prompt.question(`Save as [${defaultPath}]: `)).trim();
49
+ const destination = answer || defaultPath;
50
+ try {
51
+ return await saveCorrectedCode(correctedCode, destination, sourcePath);
52
+ } catch (error) {
53
+ output.write(`${error.message}\n`);
54
+ }
55
+ }
56
+ } finally {
57
+ prompt.close();
58
+ }
59
+ }
60
+
61
+ export function resolveSaveDestination(value, sourcePath = null) {
62
+ if (!value) return null;
63
+ if (sourcePath && dirname(value) === '.') {
64
+ return resolve(dirname(resolve(sourcePath)), value);
65
+ }
66
+ return resolve(value);
67
+ }