kodevu 0.1.24 → 0.1.25
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/package.json +1 -1
- package/src/config.js +3 -0
- package/src/index.js +26 -21
- package/src/logger.js +107 -0
- package/src/review-runner.js +14 -19
- package/src/shell.js +3 -9
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const defaultConfig = {
|
|
|
11
11
|
target: "",
|
|
12
12
|
outputDir: defaultStorageDir,
|
|
13
13
|
stateFilePath: path.join(defaultStorageDir, "state.json"),
|
|
14
|
+
logsDir: path.join(defaultStorageDir, "logs"),
|
|
14
15
|
commandTimeoutMs: 600000,
|
|
15
16
|
prompt:
|
|
16
17
|
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
@@ -24,6 +25,7 @@ const configTemplate = {
|
|
|
24
25
|
prompt: defaultConfig.prompt,
|
|
25
26
|
outputDir: "~/.kodevu",
|
|
26
27
|
stateFilePath: "~/.kodevu/state.json",
|
|
28
|
+
logsDir: "~/.kodevu/logs",
|
|
27
29
|
commandTimeoutMs: defaultConfig.commandTimeoutMs,
|
|
28
30
|
maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
|
|
29
31
|
outputFormats: defaultConfig.outputFormats
|
|
@@ -231,6 +233,7 @@ export async function loadConfig(configPath, cliArgs = {}) {
|
|
|
231
233
|
config.baseDir = baseDir;
|
|
232
234
|
config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
|
|
233
235
|
config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
|
|
236
|
+
config.logsDir = resolveConfigPath(config.baseDir, config.logsDir);
|
|
234
237
|
config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
|
|
235
238
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
236
239
|
config.outputFormats = normalizeOutputFormats(config.outputFormats, loadedConfigPath);
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { initConfig, loadConfig, parseCliArgs, printHelp } from "./config.js";
|
|
4
4
|
import { runReviewCycle } from "./review-runner.js";
|
|
5
|
+
import { logger } from "./logger.js";
|
|
5
6
|
|
|
6
7
|
let cliArgs;
|
|
7
8
|
|
|
@@ -29,32 +30,36 @@ if (cliArgs.command === "init") {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
const config = await loadConfig(cliArgs.configPath, cliArgs);
|
|
35
|
+
logger.init(config);
|
|
33
36
|
|
|
34
|
-
if (config.reviewerWasAutoSelected) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
37
|
+
if (config.reviewerWasAutoSelected) {
|
|
38
|
+
logger.info(
|
|
39
|
+
`Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
39
42
|
|
|
40
|
-
if (config.debug) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
43
|
+
if (config.debug) {
|
|
44
|
+
logger.debug(
|
|
45
|
+
`Loaded config: ${JSON.stringify({
|
|
46
|
+
configPath: config.configPath,
|
|
47
|
+
reviewer: config.reviewer,
|
|
48
|
+
reviewerCommandPath: config.reviewerCommandPath,
|
|
49
|
+
reviewerWasAutoSelected: config.reviewerWasAutoSelected,
|
|
50
|
+
target: config.target,
|
|
51
|
+
outputDir: config.outputDir,
|
|
52
|
+
debug: config.debug
|
|
53
|
+
})}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
logger.info(`Session started. Target: ${config.target}`);
|
|
55
58
|
await runReviewCycle(config);
|
|
59
|
+
logger.info("Session completed successfully.");
|
|
56
60
|
} catch (error) {
|
|
57
|
-
|
|
61
|
+
// If config was loaded, logger might be initialized, otherwise it will fall back to stderr
|
|
62
|
+
logger.error("Session failed with error", error);
|
|
58
63
|
process.exitCode = 1;
|
|
59
64
|
}
|
|
60
65
|
process.exit(process.exitCode || 0);
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
class Logger {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.config = null;
|
|
7
|
+
this.logFile = null;
|
|
8
|
+
this.progressDisplay = null;
|
|
9
|
+
this.initialized = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
init(config) {
|
|
13
|
+
if (this.initialized) return;
|
|
14
|
+
this.config = config;
|
|
15
|
+
|
|
16
|
+
if (config.logsDir) {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(config.logsDir)) {
|
|
19
|
+
fs.mkdirSync(config.logsDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
const date = new Date().toISOString().split("T")[0];
|
|
22
|
+
this.logFile = path.join(config.logsDir, `run-${date}.log`);
|
|
23
|
+
|
|
24
|
+
// Simple rotation: Clean up logs older than 7 days
|
|
25
|
+
this._cleanupOldLogs(config.logsDir);
|
|
26
|
+
this.initialized = true;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`[logger] Failed to initialize log file: ${err.message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setProgressDisplay(pd) {
|
|
34
|
+
this.progressDisplay = pd;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
info(message) {
|
|
38
|
+
this._log("INFO", message);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
error(message, error) {
|
|
42
|
+
let msg = message;
|
|
43
|
+
if (error) {
|
|
44
|
+
msg += `\n${error.stack || error}`;
|
|
45
|
+
}
|
|
46
|
+
this._log("ERROR", msg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
debug(message) {
|
|
50
|
+
if (this.config?.debug) {
|
|
51
|
+
this._log("DEBUG", message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_log(level, message) {
|
|
56
|
+
const timestamp = new Date().toISOString();
|
|
57
|
+
const logLine = `[${timestamp}] [${level}] ${message}`;
|
|
58
|
+
|
|
59
|
+
// Write to file
|
|
60
|
+
if (this.logFile) {
|
|
61
|
+
try {
|
|
62
|
+
fs.appendFileSync(this.logFile, logLine + "\n");
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Ignore file errors during logging to prevent crashes
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Console output
|
|
69
|
+
const isDebug = level === "DEBUG";
|
|
70
|
+
const isError = level === "ERROR";
|
|
71
|
+
|
|
72
|
+
// If it's debug and debug mode is off, skip console
|
|
73
|
+
if (isDebug && !this.config?.debug) return;
|
|
74
|
+
|
|
75
|
+
if (this.progressDisplay) {
|
|
76
|
+
this.progressDisplay.log(logLine);
|
|
77
|
+
} else {
|
|
78
|
+
if (isError) {
|
|
79
|
+
console.error(logLine);
|
|
80
|
+
} else {
|
|
81
|
+
console.error(logLine);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_cleanupOldLogs(logsDir) {
|
|
87
|
+
try {
|
|
88
|
+
const files = fs.readdirSync(logsDir);
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
91
|
+
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
if (file.startsWith("run-") && file.endsWith(".log")) {
|
|
94
|
+
const filePath = path.join(logsDir, file);
|
|
95
|
+
const stats = fs.statSync(filePath);
|
|
96
|
+
if (now - stats.mtimeMs > MAX_AGE_MS) {
|
|
97
|
+
fs.unlinkSync(filePath);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Ignore cleanup errors
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const logger = new Logger();
|
package/src/review-runner.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { ProgressDisplay } from "./progress-ui.js";
|
|
5
5
|
import { runCommand } from "./shell.js";
|
|
6
6
|
import { resolveRepositoryContext } from "./vcs-client.js";
|
|
7
|
+
import { logger } from "./logger.js";
|
|
7
8
|
|
|
8
9
|
const DIFF_LIMITS = {
|
|
9
10
|
review: {
|
|
@@ -17,11 +18,6 @@ const DIFF_LIMITS = {
|
|
|
17
18
|
tailLines: 200
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
function debugLog(config, message) {
|
|
21
|
-
if (config.debug) {
|
|
22
|
-
console.error(`[debug] ${message}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
21
|
|
|
26
22
|
function estimateTokenCount(text) {
|
|
27
23
|
if (!text) {
|
|
@@ -634,7 +630,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
634
630
|
|
|
635
631
|
for (const reviewerName of reviewersToTry) {
|
|
636
632
|
currentReviewerConfig = { ...config, reviewer: reviewerName };
|
|
637
|
-
|
|
633
|
+
logger.debug(`Trying reviewer: ${reviewerName}`);
|
|
638
634
|
progress?.update(0.45, `running reviewer ${reviewerName}`);
|
|
639
635
|
|
|
640
636
|
const res = await runReviewerPrompt(
|
|
@@ -659,7 +655,7 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
659
655
|
}
|
|
660
656
|
|
|
661
657
|
progress?.update(0.82, "writing report");
|
|
662
|
-
|
|
658
|
+
logger.debug(`Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
|
|
663
659
|
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
|
|
664
660
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
665
661
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
@@ -708,16 +704,14 @@ export async function runReviewCycle(config) {
|
|
|
708
704
|
await ensureDir(config.outputDir);
|
|
709
705
|
|
|
710
706
|
const { backend, targetInfo } = await resolveRepositoryContext(config);
|
|
711
|
-
|
|
712
|
-
config,
|
|
707
|
+
logger.debug(
|
|
713
708
|
`Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target} stateKey=${targetInfo.stateKey}`
|
|
714
709
|
);
|
|
715
710
|
const latestChangeId = await backend.getLatestChangeId(config, targetInfo);
|
|
716
711
|
const stateFile = await loadState(config.stateFilePath);
|
|
717
712
|
const projectState = getProjectState(stateFile, targetInfo);
|
|
718
713
|
let lastReviewedId = readLastReviewedId(projectState, backend, targetInfo);
|
|
719
|
-
|
|
720
|
-
config,
|
|
714
|
+
logger.debug(
|
|
721
715
|
`Checkpoint status: latest=${backend.formatChangeId(latestChangeId)} lastReviewed=${lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)"}`
|
|
722
716
|
);
|
|
723
717
|
|
|
@@ -725,7 +719,7 @@ export async function runReviewCycle(config) {
|
|
|
725
719
|
const checkpointIsValid = await backend.isValidCheckpoint(config, targetInfo, lastReviewedId, latestChangeId);
|
|
726
720
|
|
|
727
721
|
if (!checkpointIsValid) {
|
|
728
|
-
|
|
722
|
+
logger.info("Saved review state no longer matches repository history. Resetting checkpoint.");
|
|
729
723
|
lastReviewedId = null;
|
|
730
724
|
}
|
|
731
725
|
}
|
|
@@ -734,7 +728,7 @@ export async function runReviewCycle(config) {
|
|
|
734
728
|
|
|
735
729
|
if (!lastReviewedId) {
|
|
736
730
|
changeIdsToReview = [latestChangeId];
|
|
737
|
-
|
|
731
|
+
logger.info(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
|
|
738
732
|
} else {
|
|
739
733
|
changeIdsToReview = await backend.getPendingChangeIds(
|
|
740
734
|
config,
|
|
@@ -745,21 +739,22 @@ export async function runReviewCycle(config) {
|
|
|
745
739
|
);
|
|
746
740
|
}
|
|
747
741
|
|
|
748
|
-
|
|
742
|
+
logger.debug(`Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
|
|
749
743
|
|
|
750
744
|
if (changeIdsToReview.length === 0) {
|
|
751
745
|
const lastKnownId = lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)";
|
|
752
|
-
|
|
746
|
+
logger.info(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
|
|
753
747
|
return;
|
|
754
748
|
}
|
|
755
749
|
|
|
756
|
-
|
|
750
|
+
logger.info(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
|
|
757
751
|
const progressDisplay = new ProgressDisplay();
|
|
752
|
+
logger.setProgressDisplay(progressDisplay);
|
|
758
753
|
const progress = progressDisplay.createItem(`${backend.displayName} ${backend.changeName} batch`);
|
|
759
754
|
progress.start("0/" + changeIdsToReview.length + " completed");
|
|
760
755
|
|
|
761
756
|
for (const [index, changeId] of changeIdsToReview.entries()) {
|
|
762
|
-
|
|
757
|
+
logger.debug(`Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
763
758
|
const displayId = backend.formatChangeId(changeId);
|
|
764
759
|
updateOverallProgress(progress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
|
|
765
760
|
|
|
@@ -784,7 +779,7 @@ export async function runReviewCycle(config) {
|
|
|
784
779
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
785
780
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
786
781
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
787
|
-
|
|
782
|
+
logger.debug(`Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
|
|
788
783
|
progress.log(`[done] reviewed ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
789
784
|
updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
|
|
790
785
|
}
|
|
@@ -800,6 +795,6 @@ export async function runReviewCycle(config) {
|
|
|
800
795
|
);
|
|
801
796
|
|
|
802
797
|
if (remainingChanges.length > 0) {
|
|
803
|
-
|
|
798
|
+
logger.info(`Backlog remains. Latest ${backend.changeName} is ${backend.formatChangeId(latestChangeId)}.`);
|
|
804
799
|
}
|
|
805
800
|
}
|
package/src/shell.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import iconv from "iconv-lite";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
3
4
|
|
|
4
|
-
function debugLog(enabled, message) {
|
|
5
|
-
if (enabled) {
|
|
6
|
-
console.error(`[debug] ${message}`);
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
5
|
|
|
10
6
|
function summarizeOutput(text) {
|
|
11
7
|
if (!text) {
|
|
@@ -28,8 +24,7 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
28
24
|
debug = false
|
|
29
25
|
} = options;
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
debug,
|
|
27
|
+
logger.debug(
|
|
33
28
|
`run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}`
|
|
34
29
|
);
|
|
35
30
|
|
|
@@ -72,8 +67,7 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
72
67
|
stderr: trim ? stderr.trim() : stderr
|
|
73
68
|
};
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
debug,
|
|
70
|
+
logger.debug(
|
|
77
71
|
`exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`
|
|
78
72
|
);
|
|
79
73
|
|