kodevu 0.1.53 → 0.1.54
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 +9 -0
- package/package.json +1 -1
- package/src/config.js +15 -1
- package/src/git-client.js +103 -0
- package/src/review-runner.js +9 -2
- package/src/svn-client.js +52 -0
- package/src/vcs-client.js +40 -0
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ npx kodevu [target] [options]
|
|
|
32
32
|
- `--reviewer, -r`: `codex`, `gemini`, `copilot`, `openai`, or `auto` (default: `auto`).
|
|
33
33
|
- `--rev, -v`: A specific revision or commit hash to review.
|
|
34
34
|
- `--last, -n`: Number of latest revisions to review (default: 1). Use negative values (e.g., `-3`) to review only the 3rd commit from the top.
|
|
35
|
+
- `--uncommitted, -u`: Review current uncommitted changes in the target working tree.
|
|
35
36
|
- `--lang, -l`: Output language (e.g., `zh`, `en`, `auto`).
|
|
36
37
|
- `--prompt, -p`: Additional instructions for the reviewer. Use `@file.txt` to read from a file.
|
|
37
38
|
- `--output, -o`: Report output directory (default: `~/.kodevu`).
|
|
@@ -47,6 +48,9 @@ npx kodevu [target] [options]
|
|
|
47
48
|
> [!IMPORTANT]
|
|
48
49
|
> `--rev` and `--last` are mutually exclusive. Specifying both will result in an error.
|
|
49
50
|
|
|
51
|
+
> [!IMPORTANT]
|
|
52
|
+
> `--uncommitted` is mutually exclusive with `--rev` and `--last`.
|
|
53
|
+
|
|
50
54
|
### Environment Variables
|
|
51
55
|
|
|
52
56
|
You can set these in your shell to change default behavior without typing flags every time:
|
|
@@ -81,6 +85,11 @@ Review a **specific commit** hash:
|
|
|
81
85
|
npx kodevu . --rev abc1234
|
|
82
86
|
```
|
|
83
87
|
|
|
88
|
+
Review **current uncommitted changes** (Git/SVN working copy):
|
|
89
|
+
```bash
|
|
90
|
+
npx kodevu . --uncommitted
|
|
91
|
+
```
|
|
92
|
+
|
|
84
93
|
### Options & Formatting
|
|
85
94
|
|
|
86
95
|
Review using **custom instructions** from a file:
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -23,6 +23,7 @@ const defaultConfig = {
|
|
|
23
23
|
outputFormats: ["markdown"],
|
|
24
24
|
rev: "",
|
|
25
25
|
last: 0,
|
|
26
|
+
uncommitted: false,
|
|
26
27
|
openaiApiKey: "",
|
|
27
28
|
openaiBaseUrl: "https://api.openai.com/v1",
|
|
28
29
|
openaiModel: "gpt-5-mini",
|
|
@@ -124,6 +125,7 @@ export function parseCliArgs(argv) {
|
|
|
124
125
|
prompt: "",
|
|
125
126
|
rev: "",
|
|
126
127
|
last: "",
|
|
128
|
+
uncommitted: false,
|
|
127
129
|
outputDir: "",
|
|
128
130
|
outputFormats: "",
|
|
129
131
|
openaiApiKey: "",
|
|
@@ -190,6 +192,11 @@ export function parseCliArgs(argv) {
|
|
|
190
192
|
continue;
|
|
191
193
|
}
|
|
192
194
|
|
|
195
|
+
if (value === "--uncommitted" || value === "-u") {
|
|
196
|
+
args.uncommitted = true;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
if (value === "--output" || value === "-o") {
|
|
194
201
|
if (!hasNextValue) throw new Error(`Missing value for ${value}`);
|
|
195
202
|
args.outputDir = nextValue;
|
|
@@ -268,6 +275,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
268
275
|
"lang",
|
|
269
276
|
"rev",
|
|
270
277
|
"last",
|
|
278
|
+
"uncommitted",
|
|
271
279
|
"outputDir",
|
|
272
280
|
"outputFormats",
|
|
273
281
|
"openaiApiKey",
|
|
@@ -285,6 +293,10 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
285
293
|
throw new Error("Parameters --rev and --last are mutually exclusive. Please specify only one.");
|
|
286
294
|
}
|
|
287
295
|
|
|
296
|
+
if (cliArgs.uncommitted && (cliArgs.rev || cliArgs.last)) {
|
|
297
|
+
throw new Error("Parameter --uncommitted is mutually exclusive with --rev and --last.");
|
|
298
|
+
}
|
|
299
|
+
|
|
288
300
|
if (!config.target) {
|
|
289
301
|
config.target = process.cwd();
|
|
290
302
|
}
|
|
@@ -321,6 +333,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
321
333
|
config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
|
|
322
334
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
323
335
|
config.last = Number(config.last);
|
|
336
|
+
config.uncommitted = Boolean(config.uncommitted);
|
|
324
337
|
config.outputFormats = normalizeOutputFormats(config.outputFormats);
|
|
325
338
|
config.openaiApiKey = String(config.openaiApiKey || "").trim();
|
|
326
339
|
config.openaiBaseUrl = String(config.openaiBaseUrl || defaultConfig.openaiBaseUrl).trim().replace(/\/+$/, "");
|
|
@@ -328,7 +341,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
328
341
|
config.openaiOrganization = String(config.openaiOrganization || "").trim();
|
|
329
342
|
config.openaiProject = String(config.openaiProject || "").trim();
|
|
330
343
|
|
|
331
|
-
if (!config.rev && (isNaN(config.last) || config.last === 0)) {
|
|
344
|
+
if (!config.uncommitted && !config.rev && (isNaN(config.last) || config.last === 0)) {
|
|
332
345
|
config.last = 1;
|
|
333
346
|
}
|
|
334
347
|
|
|
@@ -352,6 +365,7 @@ Options:
|
|
|
352
365
|
--lang, -l Output language (e.g. zh, en, auto)
|
|
353
366
|
--rev, -v Review specific revision(s), hashes, branches or ranges (comma-separated)
|
|
354
367
|
--last, -n Review the latest N revisions; use negative (-N) to review only the Nth-from-last revision (default: 1)
|
|
368
|
+
--uncommitted, -u Review current uncommitted changes (mutually exclusive with --rev and --last)
|
|
355
369
|
--output, -o Output directory (default: ~/.kodevu)
|
|
356
370
|
--format, -f Output formats (markdown, json, comma-separated)
|
|
357
371
|
--openai-api-key API key used when reviewer=openai
|
package/src/git-client.js
CHANGED
|
@@ -181,6 +181,49 @@ function parseNameStatus(stdout) {
|
|
|
181
181
|
return changedPaths;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
function parseNameStatusLines(stdout) {
|
|
185
|
+
return stdout
|
|
186
|
+
.split(/\r?\n/)
|
|
187
|
+
.map((line) => line.trim())
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
.map((line) => line.split("\t"))
|
|
190
|
+
.map((parts) => {
|
|
191
|
+
const status = (parts[0] || "M").trim();
|
|
192
|
+
const action = status[0] || "M";
|
|
193
|
+
|
|
194
|
+
if ((action === "R" || action === "C") && parts.length >= 3) {
|
|
195
|
+
return {
|
|
196
|
+
action,
|
|
197
|
+
relativePath: parts[2],
|
|
198
|
+
previousPath: parts[1] || null
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
action,
|
|
204
|
+
relativePath: parts[1] || "",
|
|
205
|
+
previousPath: null
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.filter((item) => item.relativePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function mergeChangedPaths(...groups) {
|
|
212
|
+
const merged = [];
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
|
|
215
|
+
for (const group of groups) {
|
|
216
|
+
for (const item of group) {
|
|
217
|
+
const key = `${item.action}|${item.relativePath}|${item.previousPath || ""}`;
|
|
218
|
+
if (seen.has(key)) continue;
|
|
219
|
+
seen.add(key);
|
|
220
|
+
merged.push(item);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return merged;
|
|
225
|
+
}
|
|
226
|
+
|
|
184
227
|
export async function getCommitDetails(config, targetInfo, commitHash) {
|
|
185
228
|
const metaResult = await runGit(
|
|
186
229
|
config,
|
|
@@ -214,3 +257,63 @@ export async function getCommitDetails(config, targetInfo, commitHash) {
|
|
|
214
257
|
changedPaths: parseNameStatus(changedFilesResult.stdout)
|
|
215
258
|
};
|
|
216
259
|
}
|
|
260
|
+
|
|
261
|
+
export async function getUncommittedDiff(config, targetInfo) {
|
|
262
|
+
const unstaged = await runGit(
|
|
263
|
+
config,
|
|
264
|
+
[
|
|
265
|
+
"diff",
|
|
266
|
+
"--find-renames",
|
|
267
|
+
"--find-copies",
|
|
268
|
+
"--no-ext-diff",
|
|
269
|
+
...buildPathArgs(targetInfo)
|
|
270
|
+
],
|
|
271
|
+
{ cwd: targetInfo.repoRootPath, trim: false }
|
|
272
|
+
);
|
|
273
|
+
const staged = await runGit(
|
|
274
|
+
config,
|
|
275
|
+
[
|
|
276
|
+
"diff",
|
|
277
|
+
"--cached",
|
|
278
|
+
"--find-renames",
|
|
279
|
+
"--find-copies",
|
|
280
|
+
"--no-ext-diff",
|
|
281
|
+
...buildPathArgs(targetInfo)
|
|
282
|
+
],
|
|
283
|
+
{ cwd: targetInfo.repoRootPath, trim: false }
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const sections = [];
|
|
287
|
+
if (unstaged.stdout.trim()) {
|
|
288
|
+
sections.push("# Unstaged changes", unstaged.stdout.trimEnd());
|
|
289
|
+
}
|
|
290
|
+
if (staged.stdout.trim()) {
|
|
291
|
+
sections.push("# Staged changes", staged.stdout.trimEnd());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return sections.join("\n\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function getUncommittedDetails(config, targetInfo) {
|
|
298
|
+
const unstagedFiles = await runGit(
|
|
299
|
+
config,
|
|
300
|
+
["diff", "--name-status", "-M", "-C", ...buildPathArgs(targetInfo)],
|
|
301
|
+
{ cwd: targetInfo.repoRootPath, trim: false }
|
|
302
|
+
);
|
|
303
|
+
const stagedFiles = await runGit(
|
|
304
|
+
config,
|
|
305
|
+
["diff", "--cached", "--name-status", "-M", "-C", ...buildPathArgs(targetInfo)],
|
|
306
|
+
{ cwd: targetInfo.repoRootPath, trim: false }
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const unstagedChanged = parseNameStatusLines(unstagedFiles.stdout);
|
|
310
|
+
const stagedChanged = parseNameStatusLines(stagedFiles.stdout);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
commitHash: "UNCOMMITTED",
|
|
314
|
+
author: "working-tree",
|
|
315
|
+
date: new Date().toISOString(),
|
|
316
|
+
message: "Uncommitted changes (staged + unstaged).",
|
|
317
|
+
changedPaths: mergeChangedPaths(unstagedChanged, stagedChanged)
|
|
318
|
+
};
|
|
319
|
+
}
|
package/src/review-runner.js
CHANGED
|
@@ -155,7 +155,9 @@ export async function runReviewCycle(config) {
|
|
|
155
155
|
|
|
156
156
|
let changeIdsToReview = [];
|
|
157
157
|
|
|
158
|
-
if (config.
|
|
158
|
+
if (config.uncommitted) {
|
|
159
|
+
changeIdsToReview = ["UNCOMMITTED"];
|
|
160
|
+
} else if (config.rev) {
|
|
159
161
|
changeIdsToReview = await backend.resolveChangeIds(config, targetInfo, config.rev);
|
|
160
162
|
} else if (config.last < 0) {
|
|
161
163
|
const candidates = await backend.getLatestChangeIds(config, targetInfo, Math.abs(config.last));
|
|
@@ -169,7 +171,12 @@ export async function runReviewCycle(config) {
|
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
const isUncommittedBatch =
|
|
175
|
+
changeIdsToReview.length === 1 && changeIdsToReview[0] === "UNCOMMITTED";
|
|
176
|
+
const batchSummary = isUncommittedBatch
|
|
177
|
+
? `Reviewing ${backend.displayName} uncommitted changes`
|
|
178
|
+
: `Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`;
|
|
179
|
+
logger.info(batchSummary);
|
|
173
180
|
const progress = createProgressReporter(`${backend.displayName} ${backend.changeName} batch`);
|
|
174
181
|
progress.update(0, `0/${changeIdsToReview.length} completed`);
|
|
175
182
|
|
package/src/svn-client.js
CHANGED
|
@@ -128,6 +128,24 @@ function toRelativePath(targetRepoPath, repoPath) {
|
|
|
128
128
|
return repoPath.slice(targetRepoPath.length).replace(/^\/+/, "");
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function parseSvnStatus(statusText) {
|
|
132
|
+
return statusText
|
|
133
|
+
.split(/\r?\n/)
|
|
134
|
+
.map((line) => line.replace(/\r$/, ""))
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map((line) => {
|
|
137
|
+
const action = (line[0] || " ").trim();
|
|
138
|
+
const relativePath = line.length > 8 ? line.slice(8).trim() : "";
|
|
139
|
+
return {
|
|
140
|
+
action,
|
|
141
|
+
relativePath,
|
|
142
|
+
previousPath: null
|
|
143
|
+
};
|
|
144
|
+
})
|
|
145
|
+
.filter((item) => item.relativePath)
|
|
146
|
+
.filter((item) => ["A", "D", "M", "R"].includes(item.action));
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
export async function getRevisionDetails(config, targetInfo, revision) {
|
|
132
150
|
const result = await runCommand(
|
|
133
151
|
SVN_COMMAND,
|
|
@@ -165,3 +183,37 @@ export async function getRevisionDetails(config, targetInfo, revision) {
|
|
|
165
183
|
changedPaths
|
|
166
184
|
};
|
|
167
185
|
}
|
|
186
|
+
|
|
187
|
+
export async function getUncommittedDiff(config, targetInfo) {
|
|
188
|
+
if (!targetInfo.workingCopyPath) {
|
|
189
|
+
throw new Error("SVN --uncommitted requires a working copy path target.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const result = await runCommand(
|
|
193
|
+
SVN_COMMAND,
|
|
194
|
+
["diff", "--git", "--internal-diff", "--ignore-properties", config.target],
|
|
195
|
+
{ encoding: COMMAND_ENCODING, trim: false, debug: config.debug }
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return result.stdout;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function getUncommittedDetails(config, targetInfo) {
|
|
202
|
+
if (!targetInfo.workingCopyPath) {
|
|
203
|
+
throw new Error("SVN --uncommitted requires a working copy path target.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const statusResult = await runCommand(
|
|
207
|
+
SVN_COMMAND,
|
|
208
|
+
["status", config.target],
|
|
209
|
+
{ encoding: COMMAND_ENCODING, trim: false, debug: config.debug }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
revision: "UNCOMMITTED",
|
|
214
|
+
author: "working-copy",
|
|
215
|
+
date: new Date().toISOString(),
|
|
216
|
+
message: "Uncommitted changes in working copy.",
|
|
217
|
+
changedPaths: parseSvnStatus(statusResult.stdout)
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/vcs-client.js
CHANGED
|
@@ -13,9 +13,13 @@ function createSvnBackend() {
|
|
|
13
13
|
displayName: "SVN",
|
|
14
14
|
changeName: "revision",
|
|
15
15
|
formatChangeId(revision) {
|
|
16
|
+
if (revision === "UNCOMMITTED") return "uncommitted";
|
|
16
17
|
return `r${revision}`;
|
|
17
18
|
},
|
|
18
19
|
getReportFileName(revision) {
|
|
20
|
+
if (revision === "UNCOMMITTED") {
|
|
21
|
+
return `${getTimestampPrefix()}-svn-uncommitted.md`;
|
|
22
|
+
}
|
|
19
23
|
return `${getTimestampPrefix()}-svn-r${revision}.md`;
|
|
20
24
|
},
|
|
21
25
|
|
|
@@ -33,9 +37,25 @@ function createSvnBackend() {
|
|
|
33
37
|
return await svnClient.getLatestRevisionIds(config, targetInfo, limit);
|
|
34
38
|
},
|
|
35
39
|
async getChangeDiff(config, targetInfo, revision) {
|
|
40
|
+
if (revision === "UNCOMMITTED") {
|
|
41
|
+
return await svnClient.getUncommittedDiff(config, targetInfo);
|
|
42
|
+
}
|
|
36
43
|
return await svnClient.getRevisionDiff(config, revision);
|
|
37
44
|
},
|
|
38
45
|
async getChangeDetails(config, targetInfo, revision) {
|
|
46
|
+
if (revision === "UNCOMMITTED") {
|
|
47
|
+
const details = await svnClient.getUncommittedDetails(config, targetInfo);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id: "UNCOMMITTED",
|
|
51
|
+
displayId: "uncommitted",
|
|
52
|
+
author: details.author,
|
|
53
|
+
date: details.date,
|
|
54
|
+
message: details.message,
|
|
55
|
+
changedPaths: details.changedPaths
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
const details = await svnClient.getRevisionDetails(config, targetInfo, revision);
|
|
40
60
|
|
|
41
61
|
return {
|
|
@@ -56,9 +76,13 @@ function createGitBackend() {
|
|
|
56
76
|
displayName: "Git",
|
|
57
77
|
changeName: "commit",
|
|
58
78
|
formatChangeId(commitHash) {
|
|
79
|
+
if (commitHash === "UNCOMMITTED") return "uncommitted";
|
|
59
80
|
return commitHash.slice(0, 12);
|
|
60
81
|
},
|
|
61
82
|
getReportFileName(commitHash) {
|
|
83
|
+
if (commitHash === "UNCOMMITTED") {
|
|
84
|
+
return `${getTimestampPrefix()}-git-uncommitted.md`;
|
|
85
|
+
}
|
|
62
86
|
return `${getTimestampPrefix()}-git-${commitHash.slice(0, 12)}.md`;
|
|
63
87
|
},
|
|
64
88
|
|
|
@@ -82,9 +106,25 @@ function createGitBackend() {
|
|
|
82
106
|
return await gitClient.getLatestCommitIds(config, targetInfo, limit);
|
|
83
107
|
},
|
|
84
108
|
async getChangeDiff(config, targetInfo, commitHash) {
|
|
109
|
+
if (commitHash === "UNCOMMITTED") {
|
|
110
|
+
return await gitClient.getUncommittedDiff(config, targetInfo);
|
|
111
|
+
}
|
|
85
112
|
return await gitClient.getCommitDiff(config, targetInfo, commitHash);
|
|
86
113
|
},
|
|
87
114
|
async getChangeDetails(config, targetInfo, commitHash) {
|
|
115
|
+
if (commitHash === "UNCOMMITTED") {
|
|
116
|
+
const details = await gitClient.getUncommittedDetails(config, targetInfo);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: "UNCOMMITTED",
|
|
120
|
+
displayId: "uncommitted",
|
|
121
|
+
author: details.author,
|
|
122
|
+
date: details.date,
|
|
123
|
+
message: details.message,
|
|
124
|
+
changedPaths: details.changedPaths
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
88
128
|
const details = await gitClient.getCommitDetails(config, targetInfo, commitHash);
|
|
89
129
|
|
|
90
130
|
return {
|