kodevu 0.1.10 → 0.1.13
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 +19 -6
- package/config.example.json +4 -1
- package/package.json +2 -2
- package/src/config.js +27 -1
- package/src/review-runner.js +102 -20
- package/src/vcs-client.js +4 -30
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Kodevu
|
|
2
2
|
|
|
3
|
-
A Node.js tool that polls new SVN revisions or Git commits, fetches each change diff directly from the repository, sends the diff to a supported reviewer CLI, and writes
|
|
3
|
+
A Node.js tool that polls new SVN revisions or Git commits, fetches each change diff directly from the repository, sends the diff to a supported reviewer CLI, and writes review results to report files.
|
|
4
4
|
|
|
5
5
|
## Workflow
|
|
6
6
|
|
|
@@ -12,9 +12,20 @@ A Node.js tool that polls new SVN revisions or Git commits, fetches each change
|
|
|
12
12
|
- generate a unified diff for that single revision or commit
|
|
13
13
|
- send the diff and change metadata to the configured reviewer CLI
|
|
14
14
|
- allow the reviewer to inspect related local repository files in read-only mode when a local workspace is available
|
|
15
|
-
- write the result to `~/.kodevu/`
|
|
15
|
+
- write the result to `~/.kodevu/` (Markdown by default; optional JSON via config)
|
|
16
16
|
5. Update `~/.kodevu/state.json` so the same change is not reviewed twice.
|
|
17
17
|
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx kodevu /path/to/your/repo
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
No config file is required for the default flow.
|
|
25
|
+
Review reports are written to `~/.kodevu/` as Markdown (`.md`) by default, and review state is stored at `~/.kodevu/state.json`.
|
|
26
|
+
|
|
27
|
+
If you want a config file, run `npx kodevu init` to create `./config.json` in the current directory.
|
|
28
|
+
|
|
18
29
|
## Setup
|
|
19
30
|
|
|
20
31
|
```bash
|
|
@@ -36,13 +47,13 @@ If you do not pass `--config`, Kodevu will try to load `./config.json` from the
|
|
|
36
47
|
|
|
37
48
|
## Run
|
|
38
49
|
|
|
39
|
-
Run
|
|
50
|
+
Run:
|
|
40
51
|
|
|
41
52
|
```bash
|
|
42
53
|
npx kodevu /path/to/your/repo
|
|
43
54
|
```
|
|
44
55
|
|
|
45
|
-
Run
|
|
56
|
+
Run with debug logs:
|
|
46
57
|
|
|
47
58
|
```bash
|
|
48
59
|
npx kodevu /path/to/your/repo --debug
|
|
@@ -68,6 +79,7 @@ npx kodevu /path/to/your/repo --config ./config.current.json
|
|
|
68
79
|
- `reviewer`: `codex`, `gemini`, or `auto`; default `auto`
|
|
69
80
|
- `prompt`: saved into the report as review context
|
|
70
81
|
- `outputDir`: report output directory; default `~/.kodevu`
|
|
82
|
+
- `outputFormats`: report formats to generate; supports `markdown` and `json`; default `["markdown"]`
|
|
71
83
|
- `stateFilePath`: review state file path; default `~/.kodevu/state.json`
|
|
72
84
|
- `commandTimeoutMs`: timeout for a single review command execution in milliseconds
|
|
73
85
|
- `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
|
|
@@ -92,7 +104,8 @@ Internal defaults:
|
|
|
92
104
|
- Large diffs are truncated before being sent to the reviewer or written into the report once they exceed the configured line or character limits.
|
|
93
105
|
- For Git targets and local SVN working copies, the reviewer command runs from the repository workspace so it can inspect related files beyond the diff when needed.
|
|
94
106
|
- For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
|
|
95
|
-
- SVN reports are written as `<YYYYMMDD-HHmmss>-svn-r<revision>.md`.
|
|
96
|
-
- Git reports are written as `<YYYYMMDD-HHmmss>-git-<short-commit-hash>.md`.
|
|
107
|
+
- By default, SVN reports are written as `<YYYYMMDD-HHmmss>-svn-r<revision>.md`.
|
|
108
|
+
- By default, Git reports are written as `<YYYYMMDD-HHmmss>-git-<short-commit-hash>.md`.
|
|
109
|
+
- If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
|
|
97
110
|
- `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
|
|
98
111
|
- If the reviewer command exits non-zero or times out, the report is still written, but the state is not advanced so the change can be retried later.
|
package/config.example.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodevu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write
|
|
5
|
+
"description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kodevu": "./src/index.js"
|
|
8
8
|
},
|
package/src/config.js
CHANGED
|
@@ -16,7 +16,8 @@ const defaultConfig = {
|
|
|
16
16
|
commandTimeoutMs: 600000,
|
|
17
17
|
prompt:
|
|
18
18
|
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
19
|
-
maxRevisionsPerRun: 20
|
|
19
|
+
maxRevisionsPerRun: 20,
|
|
20
|
+
outputFormats: ["markdown"]
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
function resolveConfigPath(baseDir, value) {
|
|
@@ -39,6 +40,29 @@ function resolveConfigPath(baseDir, value) {
|
|
|
39
40
|
return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
|
|
44
|
+
function normalizeOutputFormats(outputFormats, loadedConfigPath) {
|
|
45
|
+
const source = outputFormats == null ? ["markdown"] : outputFormats;
|
|
46
|
+
const values = Array.isArray(source) ? source : [source];
|
|
47
|
+
const normalized = [...new Set(values.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean))];
|
|
48
|
+
const supported = ["markdown", "json"];
|
|
49
|
+
const invalid = normalized.filter((item) => !supported.includes(item));
|
|
50
|
+
|
|
51
|
+
if (invalid.length > 0) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`"outputFormats" contains unsupported value(s): ${invalid.join(", ")}. Use any of: ${supported.join(", ")}${
|
|
54
|
+
loadedConfigPath ? ` in ${loadedConfigPath}` : ""
|
|
55
|
+
}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (normalized.length === 0) {
|
|
60
|
+
throw new Error(`"outputFormats" must include at least one format${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
async function resolveAutoReviewers(debug, loadedConfigPath) {
|
|
43
67
|
const availableReviewers = [];
|
|
44
68
|
|
|
@@ -200,6 +224,7 @@ export async function loadConfig(configPath, cliArgs = {}) {
|
|
|
200
224
|
config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
|
|
201
225
|
config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
|
|
202
226
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
227
|
+
config.outputFormats = normalizeOutputFormats(config.outputFormats, loadedConfigPath);
|
|
203
228
|
|
|
204
229
|
if (!Number.isInteger(config.maxRevisionsPerRun) || config.maxRevisionsPerRun <= 0) {
|
|
205
230
|
throw new Error(`"maxRevisionsPerRun" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
@@ -233,6 +258,7 @@ Options:
|
|
|
233
258
|
Config highlights:
|
|
234
259
|
reviewer codex | gemini | auto
|
|
235
260
|
target Repository target path (Git) or SVN working copy / URL; CLI positional target overrides config
|
|
261
|
+
outputFormats ["markdown"] by default; set to include "json" when needed
|
|
236
262
|
`);
|
|
237
263
|
}
|
|
238
264
|
|
package/src/review-runner.js
CHANGED
|
@@ -154,6 +154,15 @@ async function writeTextFile(filePath, contents) {
|
|
|
154
154
|
await fs.writeFile(filePath, contents, "utf8");
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
async function writeJsonFile(filePath, payload) {
|
|
158
|
+
await ensureDir(path.dirname(filePath));
|
|
159
|
+
await fs.writeFile(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function shouldWriteFormat(config, format) {
|
|
163
|
+
return Array.isArray(config.outputFormats) && config.outputFormats.includes(format);
|
|
164
|
+
}
|
|
165
|
+
|
|
157
166
|
function formatChangedPaths(changedPaths) {
|
|
158
167
|
if (changedPaths.length === 0) {
|
|
159
168
|
return "_No changed files captured._";
|
|
@@ -351,6 +360,47 @@ function buildReport(config, backend, targetInfo, details, diffPayloads, reviewe
|
|
|
351
360
|
return `${lines.join("\n")}\n`;
|
|
352
361
|
}
|
|
353
362
|
|
|
363
|
+
function buildJsonReport(config, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult) {
|
|
364
|
+
return {
|
|
365
|
+
repositoryType: backend.displayName,
|
|
366
|
+
target: targetInfo.targetDisplay || config.target,
|
|
367
|
+
changeId: details.displayId,
|
|
368
|
+
author: details.author,
|
|
369
|
+
commitDate: details.date || "unknown",
|
|
370
|
+
generatedAt: new Date().toISOString(),
|
|
371
|
+
reviewer: {
|
|
372
|
+
name: reviewer.displayName,
|
|
373
|
+
exitCode: reviewerResult.code,
|
|
374
|
+
timedOut: Boolean(reviewerResult.timedOut)
|
|
375
|
+
},
|
|
376
|
+
changedFiles: details.changedPaths.map((item) => ({
|
|
377
|
+
action: item.action,
|
|
378
|
+
path: item.relativePath,
|
|
379
|
+
previousPath: item.previousPath || null
|
|
380
|
+
})),
|
|
381
|
+
commitMessage: details.message || "",
|
|
382
|
+
reviewContext: buildPrompt(config, backend, targetInfo, details, diffPayloads.review),
|
|
383
|
+
diffHandling: {
|
|
384
|
+
reviewerInput: {
|
|
385
|
+
originalLines: diffPayloads.review.originalLineCount,
|
|
386
|
+
originalChars: diffPayloads.review.originalCharCount,
|
|
387
|
+
includedLines: diffPayloads.review.outputLineCount,
|
|
388
|
+
includedChars: diffPayloads.review.outputCharCount,
|
|
389
|
+
truncated: diffPayloads.review.wasTruncated
|
|
390
|
+
},
|
|
391
|
+
reportDiff: {
|
|
392
|
+
originalLines: diffPayloads.report.originalLineCount,
|
|
393
|
+
originalChars: diffPayloads.report.originalCharCount,
|
|
394
|
+
includedLines: diffPayloads.report.outputLineCount,
|
|
395
|
+
includedChars: diffPayloads.report.outputCharCount,
|
|
396
|
+
truncated: diffPayloads.report.wasTruncated
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
diff: diffPayloads.report.text.trim(),
|
|
400
|
+
reviewerResponse: reviewerResult.message?.trim() ? reviewerResult.message.trim() : reviewer.emptyResponseText
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
354
404
|
async function runReviewerPrompt(config, backend, targetInfo, details, diffText) {
|
|
355
405
|
const reviewer = REVIEWERS[config.reviewer];
|
|
356
406
|
const reviewWorkspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
|
|
@@ -364,26 +414,14 @@ async function runReviewerPrompt(config, backend, targetInfo, details, diffText)
|
|
|
364
414
|
}
|
|
365
415
|
|
|
366
416
|
function readLastReviewedId(state, backend, targetInfo) {
|
|
367
|
-
if (state.vcs && state.vcs !== backend.kind) {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (state.targetKey && state.targetKey !== targetInfo.stateKey) {
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
417
|
return backend.fromStateValue(state);
|
|
376
418
|
}
|
|
377
419
|
|
|
378
420
|
function buildStateSnapshot(backend, targetInfo, changeId) {
|
|
379
|
-
|
|
380
|
-
vcs: backend.kind,
|
|
381
|
-
targetKey: targetInfo.stateKey,
|
|
421
|
+
return {
|
|
382
422
|
lastReviewedId: backend.toStateValue(changeId),
|
|
383
423
|
updatedAt: new Date().toISOString()
|
|
384
424
|
};
|
|
385
|
-
|
|
386
|
-
return backend.extendState(state, changeId);
|
|
387
425
|
}
|
|
388
426
|
|
|
389
427
|
async function reviewChange(config, backend, targetInfo, changeId) {
|
|
@@ -396,9 +434,29 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
396
434
|
"No file changes were captured for this change under the configured target."
|
|
397
435
|
].join("\n");
|
|
398
436
|
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
437
|
+
const markdownReportFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
438
|
+
const jsonReportFile = markdownReportFile.replace(/\.md$/i, ".json");
|
|
439
|
+
|
|
440
|
+
if (shouldWriteFormat(config, "markdown")) {
|
|
441
|
+
await writeTextFile(markdownReportFile, `${skippedReport}\n`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (shouldWriteFormat(config, "json")) {
|
|
445
|
+
await writeJsonFile(jsonReportFile, {
|
|
446
|
+
repositoryType: backend.displayName,
|
|
447
|
+
target: targetInfo.targetDisplay || config.target,
|
|
448
|
+
changeId: details.displayId,
|
|
449
|
+
generatedAt: new Date().toISOString(),
|
|
450
|
+
skipped: true,
|
|
451
|
+
message: "No file changes were captured for this change under the configured target."
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
success: true,
|
|
457
|
+
outputFile: shouldWriteFormat(config, "markdown") ? markdownReportFile : null,
|
|
458
|
+
jsonOutputFile: shouldWriteFormat(config, "json") ? jsonReportFile : null
|
|
459
|
+
};
|
|
402
460
|
}
|
|
403
461
|
|
|
404
462
|
const diffText = await backend.getChangeDiff(config, targetInfo, changeId);
|
|
@@ -435,13 +493,33 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
435
493
|
|
|
436
494
|
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult);
|
|
437
495
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
438
|
-
|
|
496
|
+
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
497
|
+
|
|
498
|
+
if (shouldWriteFormat(config, "markdown")) {
|
|
499
|
+
await writeTextFile(outputFile, report);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (shouldWriteFormat(config, "json")) {
|
|
503
|
+
await writeJsonFile(
|
|
504
|
+
jsonOutputFile,
|
|
505
|
+
buildJsonReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult)
|
|
506
|
+
);
|
|
507
|
+
}
|
|
439
508
|
|
|
440
509
|
if (reviewerResult.code !== 0 || reviewerResult.timedOut) {
|
|
441
|
-
throw new Error(
|
|
510
|
+
throw new Error(
|
|
511
|
+
`${reviewer.displayName} failed for ${details.displayId}; report written to ${outputFile}${
|
|
512
|
+
shouldWriteFormat(config, "json") ? ` and ${jsonOutputFile}` : ""
|
|
513
|
+
}`
|
|
514
|
+
);
|
|
442
515
|
}
|
|
443
516
|
|
|
444
|
-
return {
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
outputFile: shouldWriteFormat(config, "markdown") ? outputFile : null,
|
|
520
|
+
jsonOutputFile: shouldWriteFormat(config, "json") ? jsonOutputFile : null,
|
|
521
|
+
details
|
|
522
|
+
};
|
|
445
523
|
}
|
|
446
524
|
|
|
447
525
|
function formatChangeList(backend, changeIds) {
|
|
@@ -502,7 +580,11 @@ export async function runReviewCycle(config) {
|
|
|
502
580
|
for (const changeId of changeIdsToReview) {
|
|
503
581
|
debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
504
582
|
const result = await reviewChange(config, backend, targetInfo, changeId);
|
|
505
|
-
|
|
583
|
+
const outputLabels = [
|
|
584
|
+
result.outputFile ? `md: ${result.outputFile}` : null,
|
|
585
|
+
result.jsonOutputFile ? `json: ${result.jsonOutputFile}` : null
|
|
586
|
+
].filter(Boolean);
|
|
587
|
+
console.log(`Reviewed ${backend.formatChangeId(changeId)}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
506
588
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
507
589
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
508
590
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
package/src/vcs-client.js
CHANGED
|
@@ -67,21 +67,8 @@ function createSvnBackend() {
|
|
|
67
67
|
return Number(revision);
|
|
68
68
|
},
|
|
69
69
|
fromStateValue(state) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (Number.isInteger(state.lastReviewedRevision)) {
|
|
75
|
-
return state.lastReviewedRevision;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return null;
|
|
79
|
-
},
|
|
80
|
-
extendState(state, revision) {
|
|
81
|
-
return {
|
|
82
|
-
...state,
|
|
83
|
-
lastReviewedRevision: Number(revision)
|
|
84
|
-
};
|
|
70
|
+
const id = state.lastReviewedId;
|
|
71
|
+
return Number.isInteger(id) ? id : null;
|
|
85
72
|
}
|
|
86
73
|
};
|
|
87
74
|
}
|
|
@@ -137,21 +124,8 @@ function createGitBackend() {
|
|
|
137
124
|
return String(commitHash);
|
|
138
125
|
},
|
|
139
126
|
fromStateValue(state) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (typeof state.lastReviewedCommit === "string" && state.lastReviewedCommit) {
|
|
145
|
-
return state.lastReviewedCommit;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
},
|
|
150
|
-
extendState(state, commitHash) {
|
|
151
|
-
return {
|
|
152
|
-
...state,
|
|
153
|
-
lastReviewedCommit: String(commitHash)
|
|
154
|
-
};
|
|
127
|
+
const id = state.lastReviewedId;
|
|
128
|
+
return typeof id === "string" && id ? id : null;
|
|
155
129
|
}
|
|
156
130
|
};
|
|
157
131
|
}
|