kodevu 0.1.32 → 0.1.34
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 +45 -55
- package/package.json +1 -1
- package/src/config.js +112 -213
- package/src/index.js +3 -16
package/README.md
CHANGED
|
@@ -2,98 +2,88 @@
|
|
|
2
2
|
|
|
3
3
|
A Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Pure & Zero Config
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. Fetch the specified revision(s) as requested by the user:
|
|
9
|
-
- A single specific revision/commit via `--rev`.
|
|
10
|
-
- The latest $N$ revisions/commits via `--last` (default: 1).
|
|
11
|
-
3. For each change:
|
|
12
|
-
- Load metadata and changed paths from SVN or Git.
|
|
13
|
-
- Generate a unified diff for that single revision or commit.
|
|
14
|
-
- Send the diff and change metadata to the configured reviewer CLI.
|
|
15
|
-
- Allow the reviewer to inspect related local repository files in read-only mode when a local workspace is available.
|
|
16
|
-
- Write the result to `~/.kodevu/` (Markdown by default; optional JSON via config).
|
|
7
|
+
Kodevu is designed to be stateless and requires no configuration files. It relies entirely on command-line arguments and environment variables.
|
|
17
8
|
|
|
18
|
-
|
|
9
|
+
1. **Automatic Detection**: Detects repository type (Git/SVN), language, and available reviewers.
|
|
10
|
+
2. **Stateless**: Does not track history; reviews exactly what you ask for.
|
|
11
|
+
3. **Flexible**: Every setting can be overridden via CLI flags or ENV vars.
|
|
19
12
|
|
|
20
|
-
## Quick
|
|
13
|
+
## Quick Start
|
|
21
14
|
|
|
22
15
|
Review the latest commit in your repository:
|
|
23
16
|
|
|
24
17
|
```bash
|
|
25
|
-
npx kodevu
|
|
18
|
+
npx kodevu .
|
|
26
19
|
```
|
|
27
20
|
|
|
28
21
|
Review the latest 3 commits:
|
|
29
22
|
|
|
30
23
|
```bash
|
|
31
|
-
npx kodevu
|
|
24
|
+
npx kodevu . --last 3
|
|
32
25
|
```
|
|
33
26
|
|
|
34
27
|
Review a specific commit:
|
|
35
28
|
|
|
36
29
|
```bash
|
|
37
|
-
npx kodevu
|
|
30
|
+
npx kodevu . --rev abc1234
|
|
38
31
|
```
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
Reports are written to `~/.kodevu/` by default.
|
|
41
34
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
If you want to customize settings beyond the defaults:
|
|
35
|
+
## Usage
|
|
45
36
|
|
|
46
37
|
```bash
|
|
47
|
-
npx kodevu
|
|
38
|
+
npx kodevu [target] [options]
|
|
48
39
|
```
|
|
49
40
|
|
|
50
|
-
|
|
41
|
+
### Options
|
|
51
42
|
|
|
52
|
-
|
|
43
|
+
- `target`: Repository path (Git) or SVN URL/Working copy (default: `.`).
|
|
44
|
+
- `--reviewer, -r`: `codex`, `gemini`, `copilot`, or `auto` (default: `auto`).
|
|
45
|
+
- `--rev, -v`: A specific revision or commit hash to review.
|
|
46
|
+
- `--last, -n`: Number of latest revisions to review (default: 1).
|
|
47
|
+
- `--lang, -l`: Output language (e.g., `zh`, `en`, `auto`).
|
|
48
|
+
- `--prompt, -p`: Additional instructions for the reviewer. Use `@file.txt` to read from a file.
|
|
49
|
+
- `--output, -o`: Report output directory (default: `~/.kodevu`).
|
|
50
|
+
- `--format, -f`: Output formats (e.g., `markdown`, `json`, or `markdown,json`).
|
|
51
|
+
- `--debug, -d`: Print debug information.
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
### Environment Variables
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
npx kodevu /path/to/your/repo --lang zh
|
|
58
|
-
```
|
|
55
|
+
You can set these in your shell to change default behavior without typing flags every time:
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
- `KODEVU_REVIEWER`: Default reviewer.
|
|
58
|
+
- `KODEVU_LANG`: Default language.
|
|
59
|
+
- `KODEVU_OUTPUT_DIR`: Default output directory.
|
|
60
|
+
- `KODEVU_PROMPT`: Default prompt instructions.
|
|
61
|
+
- `KODEVU_TIMEOUT`: Reviewer execution timeout in milliseconds.
|
|
61
62
|
|
|
63
|
+
## Examples
|
|
64
|
+
|
|
65
|
+
**Review with a custom prompt from a file:**
|
|
62
66
|
```bash
|
|
63
|
-
npx kodevu
|
|
67
|
+
npx kodevu . --prompt @my-rules.txt
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
**Generate JSON reports in a local folder:**
|
|
68
71
|
```bash
|
|
69
|
-
npx kodevu --
|
|
72
|
+
npx kodevu . --format json --output ./review-reports
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- `last`: Number of latest revisions to review (default: 1 if `rev` is not set).
|
|
78
|
-
- `lang`: Output language for the review (e.g., `zh`, `en`, `auto`).
|
|
79
|
-
- `prompt`: Additional instructions for the reviewer.
|
|
80
|
-
- `outputDir`: Report output directory (default: `~/.kodevu`).
|
|
81
|
-
- `outputFormats`: Report formats to generate (supports `markdown` and `json`; default: `["markdown"]`).
|
|
82
|
-
- `commandTimeoutMs`: Timeout for a single review command execution in milliseconds.
|
|
83
|
-
- `maxRevisionsPerRun`: Cap the number of revisions handled in one run.
|
|
84
|
-
|
|
85
|
-
## Target Rules
|
|
86
|
-
|
|
87
|
-
- For SVN, `target` can be a working copy path or repository URL.
|
|
88
|
-
- For Git, `target` must be a local repository path or a subdirectory inside a local repository.
|
|
89
|
-
- The tool tries Git first, then falls back to SVN.
|
|
75
|
+
**Set a persistent reviewer via ENV:**
|
|
76
|
+
```bash
|
|
77
|
+
export KODEVU_REVIEWER=gemini
|
|
78
|
+
npx kodevu .
|
|
79
|
+
```
|
|
90
80
|
|
|
91
|
-
##
|
|
81
|
+
## How it Works
|
|
92
82
|
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
83
|
+
- **Git Targets**: `target` must be a local repository or subdirectory.
|
|
84
|
+
- **SVN Targets**: `target` can be a working copy path or repository URL.
|
|
85
|
+
- **Reviewer "auto"**: Probes `codex`, `gemini`, and `copilot` in your `PATH` and selects one.
|
|
86
|
+
- **Contextual Review**: For local repositories, the reviewer can inspect related files beyond the diff to provide deeper insights.
|
|
97
87
|
|
|
98
88
|
## License
|
|
99
89
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -6,7 +6,6 @@ import { findCommandOnPath } from "./shell.js";
|
|
|
6
6
|
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
7
7
|
const SUPPORTED_REVIEWERS = ["codex", "gemini", "copilot"];
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
const defaultConfig = {
|
|
11
10
|
reviewer: "auto",
|
|
12
11
|
target: "",
|
|
@@ -21,61 +20,36 @@ const defaultConfig = {
|
|
|
21
20
|
last: 0
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
|
|
33
|
-
outputFormats: defaultConfig.outputFormats,
|
|
34
|
-
rev: "",
|
|
35
|
-
last: 1
|
|
23
|
+
const ENV_MAP = {
|
|
24
|
+
KODEVU_REVIEWER: "reviewer",
|
|
25
|
+
KODEVU_LANG: "lang",
|
|
26
|
+
KODEVU_OUTPUT_DIR: "outputDir",
|
|
27
|
+
KODEVU_PROMPT: "prompt",
|
|
28
|
+
KODEVU_TIMEOUT: "commandTimeoutMs",
|
|
29
|
+
KODEVU_MAX_REVISIONS: "maxRevisionsPerRun",
|
|
30
|
+
KODEVU_FORMATS: "outputFormats"
|
|
36
31
|
};
|
|
37
32
|
|
|
38
|
-
function
|
|
39
|
-
if (!value)
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (typeof value !== "string") {
|
|
44
|
-
return path.resolve(baseDir, String(value));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (value === "~") {
|
|
48
|
-
return os.homedir();
|
|
49
|
-
}
|
|
50
|
-
|
|
33
|
+
function resolvePath(value) {
|
|
34
|
+
if (!value) return value;
|
|
35
|
+
if (value === "~") return os.homedir();
|
|
51
36
|
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
52
37
|
return path.join(os.homedir(), value.slice(2));
|
|
53
38
|
}
|
|
54
|
-
|
|
55
|
-
return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
|
|
39
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
56
40
|
}
|
|
57
41
|
|
|
58
|
-
|
|
59
|
-
function normalizeOutputFormats(outputFormats, loadedConfigPath) {
|
|
42
|
+
function normalizeOutputFormats(outputFormats) {
|
|
60
43
|
const source = outputFormats == null ? ["markdown"] : outputFormats;
|
|
61
|
-
const values = Array.isArray(source) ? source :
|
|
44
|
+
const values = Array.isArray(source) ? source : String(source).split(",");
|
|
62
45
|
const normalized = [...new Set(values.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean))];
|
|
63
46
|
const supported = ["markdown", "json"];
|
|
64
47
|
const invalid = normalized.filter((item) => !supported.includes(item));
|
|
65
48
|
|
|
66
49
|
if (invalid.length > 0) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`"outputFormats" contains unsupported value(s): ${invalid.join(", ")}. Use any of: ${supported.join(", ")}${
|
|
69
|
-
loadedConfigPath ? ` in ${loadedConfigPath}` : ""
|
|
70
|
-
}`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (normalized.length === 0) {
|
|
75
|
-
throw new Error(`"outputFormats" must include at least one format${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
50
|
+
throw new Error(`Unsupported output format(s): ${invalid.join(", ")}. Use: ${supported.join(", ")}`);
|
|
76
51
|
}
|
|
77
|
-
|
|
78
|
-
return normalized;
|
|
52
|
+
return normalized.length === 0 ? ["markdown"] : normalized;
|
|
79
53
|
}
|
|
80
54
|
|
|
81
55
|
function detectLanguage() {
|
|
@@ -88,46 +62,28 @@ function detectLanguage() {
|
|
|
88
62
|
}
|
|
89
63
|
})();
|
|
90
64
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (envLang) {
|
|
99
|
-
if (envLang.startsWith("zh")) return "zh";
|
|
100
|
-
if (envLang.startsWith("en")) return "en";
|
|
101
|
-
return envLang.split(/[._-]/)[0];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (intlLocale) {
|
|
105
|
-
if (intlLocale.startsWith("zh")) return "zh";
|
|
106
|
-
if (intlLocale.startsWith("en")) return "en";
|
|
107
|
-
return intlLocale.split("-")[0];
|
|
108
|
-
}
|
|
109
|
-
|
|
65
|
+
if (os.platform() === "win32" && intlLocale.startsWith("zh")) return "zh";
|
|
66
|
+
if (envLang.startsWith("zh")) return "zh";
|
|
67
|
+
if (envLang.startsWith("en")) return "en";
|
|
68
|
+
if (envLang) return envLang.split(/[._-]/)[0];
|
|
69
|
+
if (intlLocale.startsWith("zh")) return "zh";
|
|
70
|
+
if (intlLocale.startsWith("en")) return "en";
|
|
71
|
+
if (intlLocale) return intlLocale.split("-")[0];
|
|
110
72
|
return "en";
|
|
111
73
|
}
|
|
112
74
|
|
|
113
|
-
async function resolveAutoReviewers(debug
|
|
75
|
+
async function resolveAutoReviewers(debug) {
|
|
114
76
|
const availableReviewers = [];
|
|
115
|
-
|
|
116
77
|
for (const reviewerName of SUPPORTED_REVIEWERS) {
|
|
117
78
|
const commandPath = await findCommandOnPath(reviewerName, { debug });
|
|
118
|
-
if (commandPath) {
|
|
119
|
-
availableReviewers.push({ reviewerName, commandPath });
|
|
120
|
-
}
|
|
79
|
+
if (commandPath) availableReviewers.push({ reviewerName, commandPath });
|
|
121
80
|
}
|
|
122
81
|
|
|
123
82
|
if (availableReviewers.length === 0) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`No reviewer CLI was found in PATH for "reviewer": "auto". Install one of: ${SUPPORTED_REVIEWERS.join(", ")}${
|
|
126
|
-
loadedConfigPath ? ` (${loadedConfigPath})` : ""
|
|
127
|
-
}`
|
|
128
|
-
);
|
|
83
|
+
throw new Error(`No reviewer CLI found in PATH. Install one of: ${SUPPORTED_REVIEWERS.join(", ")}`);
|
|
129
84
|
}
|
|
130
85
|
|
|
86
|
+
// Shuffle for variety
|
|
131
87
|
for (let i = availableReviewers.length - 1; i > 0; i--) {
|
|
132
88
|
const j = Math.floor(Math.random() * (i + 1));
|
|
133
89
|
[availableReviewers[i], availableReviewers[j]] = [availableReviewers[j], availableReviewers[i]];
|
|
@@ -138,9 +94,6 @@ async function resolveAutoReviewers(debug, loadedConfigPath) {
|
|
|
138
94
|
|
|
139
95
|
export function parseCliArgs(argv) {
|
|
140
96
|
const args = {
|
|
141
|
-
command: "run",
|
|
142
|
-
configPath: "config.json",
|
|
143
|
-
configExplicitlySet: false,
|
|
144
97
|
target: "",
|
|
145
98
|
debug: false,
|
|
146
99
|
help: false,
|
|
@@ -149,18 +102,13 @@ export function parseCliArgs(argv) {
|
|
|
149
102
|
prompt: "",
|
|
150
103
|
rev: "",
|
|
151
104
|
last: "",
|
|
152
|
-
|
|
105
|
+
outputDir: "",
|
|
106
|
+
outputFormats: ""
|
|
153
107
|
};
|
|
154
108
|
|
|
155
109
|
for (let index = 0; index < argv.length; index += 1) {
|
|
156
110
|
const value = argv[index];
|
|
157
111
|
|
|
158
|
-
if (value === "init" && !args.commandExplicitlySet && index === 0) {
|
|
159
|
-
args.command = "init";
|
|
160
|
-
args.commandExplicitlySet = true;
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
112
|
if (value === "--help" || value === "-h") {
|
|
165
113
|
args.help = true;
|
|
166
114
|
continue;
|
|
@@ -171,68 +119,59 @@ export function parseCliArgs(argv) {
|
|
|
171
119
|
continue;
|
|
172
120
|
}
|
|
173
121
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (!configPath || configPath.startsWith("-")) {
|
|
177
|
-
throw new Error(`Missing value for ${value}`);
|
|
178
|
-
}
|
|
179
|
-
args.configPath = configPath;
|
|
180
|
-
args.configExplicitlySet = true;
|
|
181
|
-
index += 1;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
122
|
+
const nextValue = argv[index + 1];
|
|
123
|
+
const hasNextValue = nextValue && !nextValue.startsWith("-");
|
|
184
124
|
|
|
185
125
|
if (value === "--reviewer" || value === "-r") {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw new Error(`Missing value for ${value}`);
|
|
189
|
-
}
|
|
190
|
-
args.reviewer = reviewer;
|
|
126
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
127
|
+
args.reviewer = nextValue;
|
|
191
128
|
index += 1;
|
|
192
129
|
continue;
|
|
193
130
|
}
|
|
194
131
|
|
|
195
132
|
if (value === "--prompt" || value === "-p") {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
throw new Error(`Missing value for ${value}`);
|
|
199
|
-
}
|
|
200
|
-
args.prompt = prompt;
|
|
133
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
134
|
+
args.prompt = nextValue;
|
|
201
135
|
index += 1;
|
|
202
136
|
continue;
|
|
203
137
|
}
|
|
204
138
|
|
|
205
139
|
if (value === "--lang" || value === "-l") {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
throw new Error(`Missing value for ${value}`);
|
|
209
|
-
}
|
|
210
|
-
args.lang = lang;
|
|
140
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
141
|
+
args.lang = nextValue;
|
|
211
142
|
index += 1;
|
|
212
143
|
continue;
|
|
213
144
|
}
|
|
214
145
|
|
|
215
146
|
if (value === "--rev" || value === "-v") {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
throw new Error(`Missing value for ${value}`);
|
|
219
|
-
}
|
|
220
|
-
args.rev = rev;
|
|
147
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
148
|
+
args.rev = nextValue;
|
|
221
149
|
index += 1;
|
|
222
150
|
continue;
|
|
223
151
|
}
|
|
224
152
|
|
|
225
153
|
if (value === "--last" || value === "-n") {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
154
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
155
|
+
args.last = nextValue;
|
|
156
|
+
index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (value === "--output" || value === "-o") {
|
|
161
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
162
|
+
args.outputDir = nextValue;
|
|
231
163
|
index += 1;
|
|
232
164
|
continue;
|
|
233
165
|
}
|
|
234
166
|
|
|
235
|
-
if (
|
|
167
|
+
if (value === "--format" || value === "-f") {
|
|
168
|
+
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
169
|
+
args.outputFormats = nextValue;
|
|
170
|
+
index += 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!value.startsWith("-") && !args.target) {
|
|
236
175
|
args.target = value;
|
|
237
176
|
continue;
|
|
238
177
|
}
|
|
@@ -240,100 +179,79 @@ export function parseCliArgs(argv) {
|
|
|
240
179
|
throw new Error(`Unexpected argument: ${value}`);
|
|
241
180
|
}
|
|
242
181
|
|
|
243
|
-
delete args.commandExplicitlySet;
|
|
244
182
|
return args;
|
|
245
183
|
}
|
|
246
184
|
|
|
247
|
-
export async function
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
255
|
-
loadedConfig = JSON.parse(raw);
|
|
256
|
-
loadedConfigPath = absoluteConfigPath;
|
|
257
|
-
baseDir = path.dirname(absoluteConfigPath);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
if (!(error?.code === "ENOENT" && !cliArgs.configExplicitlySet)) {
|
|
260
|
-
throw error;
|
|
185
|
+
export async function resolveConfig(cliArgs = {}) {
|
|
186
|
+
const config = { ...defaultConfig };
|
|
187
|
+
|
|
188
|
+
// 1. Merge Environment Variables
|
|
189
|
+
for (const [envVar, configKey] of Object.entries(ENV_MAP)) {
|
|
190
|
+
if (process.env[envVar] !== undefined) {
|
|
191
|
+
config[configKey] = process.env[envVar];
|
|
261
192
|
}
|
|
262
193
|
}
|
|
263
194
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
195
|
+
// 2. Merge CLI Arguments
|
|
196
|
+
const cliMapping = {
|
|
197
|
+
target: "target",
|
|
198
|
+
reviewer: "reviewer",
|
|
199
|
+
prompt: "prompt",
|
|
200
|
+
lang: "lang",
|
|
201
|
+
rev: "rev",
|
|
202
|
+
last: "last",
|
|
203
|
+
outputDir: "outputDir",
|
|
204
|
+
outputFormats: "outputFormats"
|
|
267
205
|
};
|
|
268
206
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (cliArgs.reviewer) {
|
|
274
|
-
config.reviewer = cliArgs.reviewer;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (cliArgs.prompt) {
|
|
278
|
-
config.prompt = cliArgs.prompt;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (cliArgs.lang) {
|
|
282
|
-
config.lang = cliArgs.lang;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (cliArgs.rev) {
|
|
286
|
-
config.rev = cliArgs.rev;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (cliArgs.last) {
|
|
290
|
-
config.last = cliArgs.last;
|
|
207
|
+
for (const [cliKey, configKey] of Object.entries(cliMapping)) {
|
|
208
|
+
if (cliArgs[cliKey]) {
|
|
209
|
+
config[configKey] = cliArgs[cliKey];
|
|
210
|
+
}
|
|
291
211
|
}
|
|
292
212
|
|
|
293
213
|
if (!config.target) {
|
|
294
|
-
|
|
214
|
+
config.target = process.cwd();
|
|
295
215
|
}
|
|
296
216
|
|
|
217
|
+
config.baseDir = process.cwd();
|
|
297
218
|
config.debug = Boolean(cliArgs.debug);
|
|
298
219
|
config.reviewer = String(config.reviewer || "auto").toLowerCase();
|
|
299
220
|
config.lang = String(config.lang || "auto").toLowerCase();
|
|
300
|
-
|
|
301
221
|
config.resolvedLang = config.lang === "auto" ? detectLanguage() : config.lang;
|
|
302
222
|
|
|
223
|
+
// Handle @file prompt
|
|
224
|
+
if (config.prompt.startsWith("@")) {
|
|
225
|
+
const promptPath = resolvePath(config.prompt.slice(1));
|
|
226
|
+
try {
|
|
227
|
+
config.prompt = await fs.readFile(promptPath, "utf8");
|
|
228
|
+
} catch (err) {
|
|
229
|
+
throw new Error(`Failed to read prompt file: ${promptPath} (${err.message})`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
303
233
|
if (config.reviewer === "auto") {
|
|
304
|
-
const availableReviewers = await resolveAutoReviewers(config.debug
|
|
234
|
+
const availableReviewers = await resolveAutoReviewers(config.debug);
|
|
305
235
|
const selectedReviewer = availableReviewers[0];
|
|
306
236
|
config.reviewer = selectedReviewer.reviewerName;
|
|
307
237
|
config.reviewerCommandPath = selectedReviewer.commandPath;
|
|
308
238
|
config.fallbackReviewers = availableReviewers.map(r => r.reviewerName).slice(1);
|
|
309
239
|
config.reviewerWasAutoSelected = true;
|
|
310
240
|
} else if (!SUPPORTED_REVIEWERS.includes(config.reviewer)) {
|
|
311
|
-
throw new Error(
|
|
312
|
-
`"reviewer" must be one of "codex", "gemini", "copilot", or "auto"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`
|
|
313
|
-
);
|
|
241
|
+
throw new Error(`"reviewer" must be one of: ${SUPPORTED_REVIEWERS.join(", ")}, or "auto"`);
|
|
314
242
|
}
|
|
315
243
|
|
|
316
|
-
config.
|
|
317
|
-
config.
|
|
318
|
-
config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
|
|
319
|
-
config.logsDir = resolveConfigPath(config.baseDir, config.logsDir);
|
|
244
|
+
config.outputDir = resolvePath(config.outputDir);
|
|
245
|
+
config.logsDir = path.join(config.outputDir, "logs");
|
|
320
246
|
config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
|
|
321
247
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
322
248
|
config.last = Number(config.last);
|
|
323
|
-
config.outputFormats = normalizeOutputFormats(config.outputFormats
|
|
249
|
+
config.outputFormats = normalizeOutputFormats(config.outputFormats);
|
|
324
250
|
|
|
325
251
|
if (!config.rev && (isNaN(config.last) || config.last <= 0)) {
|
|
326
252
|
config.last = 1;
|
|
327
253
|
}
|
|
328
254
|
|
|
329
|
-
if (!Number.isInteger(config.maxRevisionsPerRun) || config.maxRevisionsPerRun <= 0) {
|
|
330
|
-
throw new Error(`"maxRevisionsPerRun" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (!Number.isInteger(config.commandTimeoutMs) || config.commandTimeoutMs <= 0) {
|
|
334
|
-
throw new Error(`"commandTimeoutMs" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
255
|
return config;
|
|
338
256
|
}
|
|
339
257
|
|
|
@@ -341,44 +259,25 @@ export function printHelp() {
|
|
|
341
259
|
console.log(`Kodevu
|
|
342
260
|
|
|
343
261
|
Usage:
|
|
344
|
-
kodevu
|
|
345
|
-
npx kodevu init
|
|
346
|
-
kodevu <target> [--debug]
|
|
347
|
-
npx kodevu <target> [--debug]
|
|
348
|
-
kodevu [--config config.json]
|
|
349
|
-
npx kodevu [--config config.json]
|
|
262
|
+
npx kodevu [target] [options]
|
|
350
263
|
|
|
351
264
|
Options:
|
|
352
|
-
--
|
|
353
|
-
--reviewer, -r
|
|
354
|
-
--prompt, -p
|
|
355
|
-
--lang, -l
|
|
356
|
-
--rev, -v
|
|
357
|
-
--last, -n
|
|
358
|
-
--
|
|
359
|
-
--
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
265
|
+
--target, <path> Target repository path (default: current directory)
|
|
266
|
+
--reviewer, -r Reviewer (codex | gemini | copilot | auto, default: auto)
|
|
267
|
+
--prompt, -p Additional instructions or @file.txt to read from file
|
|
268
|
+
--lang, -l Output language (e.g. zh, en, auto)
|
|
269
|
+
--rev, -v Review a specific revision or commit hash
|
|
270
|
+
--last, -n Review the latest N revisions (default: 1)
|
|
271
|
+
--output, -o Output directory (default: ~/.kodevu)
|
|
272
|
+
--format, -f Output formats (markdown, json, comma-separated)
|
|
273
|
+
--debug, -d Print extra debug information
|
|
274
|
+
--help, -h Show help
|
|
275
|
+
|
|
276
|
+
Environment Variables:
|
|
277
|
+
KODEVU_REVIEWER Default reviewer
|
|
278
|
+
KODEVU_LANG Default language
|
|
279
|
+
KODEVU_OUTPUT_DIR Default output directory
|
|
280
|
+
KODEVU_PROMPT Default prompt text
|
|
281
|
+
KODEVU_TIMEOUT Reviewer timeout in ms
|
|
365
282
|
`);
|
|
366
283
|
}
|
|
367
|
-
|
|
368
|
-
export async function initConfig(targetPath = "config.json") {
|
|
369
|
-
const absoluteTargetPath = path.resolve(targetPath);
|
|
370
|
-
|
|
371
|
-
await fs.mkdir(path.dirname(absoluteTargetPath), { recursive: true });
|
|
372
|
-
|
|
373
|
-
const content = JSON.stringify(configTemplate, null, 2) + "\n";
|
|
374
|
-
try {
|
|
375
|
-
await fs.writeFile(absoluteTargetPath, content, { flag: "wx" });
|
|
376
|
-
} catch (error) {
|
|
377
|
-
if (error?.code === "EEXIST") {
|
|
378
|
-
throw new Error(`Config file already exists: ${absoluteTargetPath}`);
|
|
379
|
-
}
|
|
380
|
-
throw error;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return absoluteTargetPath;
|
|
384
|
-
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { resolveConfig, parseCliArgs, printHelp } from "./config.js";
|
|
4
4
|
import { runReviewCycle } from "./review-runner.js";
|
|
5
5
|
import { logger } from "./logger.js";
|
|
6
6
|
|
|
@@ -19,19 +19,8 @@ if (cliArgs.help) {
|
|
|
19
19
|
process.exit(0);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
if (cliArgs.command === "init") {
|
|
23
|
-
try {
|
|
24
|
-
const createdPath = await initConfig(cliArgs.configPath);
|
|
25
|
-
console.log(`Created config: ${createdPath}`);
|
|
26
|
-
process.exit(0);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(error?.stack || String(error));
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
try {
|
|
34
|
-
const config = await
|
|
23
|
+
const config = await resolveConfig(cliArgs);
|
|
35
24
|
logger.init(config);
|
|
36
25
|
|
|
37
26
|
if (config.reviewerWasAutoSelected) {
|
|
@@ -42,8 +31,7 @@ try {
|
|
|
42
31
|
|
|
43
32
|
if (config.debug) {
|
|
44
33
|
logger.debug(
|
|
45
|
-
`
|
|
46
|
-
configPath: config.configPath,
|
|
34
|
+
`Resolved config: ${JSON.stringify({
|
|
47
35
|
reviewer: config.reviewer,
|
|
48
36
|
reviewerCommandPath: config.reviewerCommandPath,
|
|
49
37
|
reviewerWasAutoSelected: config.reviewerWasAutoSelected,
|
|
@@ -59,7 +47,6 @@ try {
|
|
|
59
47
|
await runReviewCycle(config);
|
|
60
48
|
logger.info("Session completed successfully.");
|
|
61
49
|
} catch (error) {
|
|
62
|
-
// If config was loaded, logger might be initialized, otherwise it will fall back to stderr
|
|
63
50
|
logger.error("Session failed with error", error);
|
|
64
51
|
process.exitCode = 1;
|
|
65
52
|
}
|