kodevu 0.1.24 → 0.1.26

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
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
- const config = await loadConfig(cliArgs.configPath, cliArgs);
33
+ try {
34
+ const config = await loadConfig(cliArgs.configPath, cliArgs);
35
+ logger.init(config);
33
36
 
34
- if (config.reviewerWasAutoSelected) {
35
- console.log(
36
- `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
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
- console.error(
42
- `[debug] Loaded config: ${JSON.stringify({
43
- configPath: config.configPath,
44
- reviewer: config.reviewer,
45
- reviewerCommandPath: config.reviewerCommandPath,
46
- reviewerWasAutoSelected: config.reviewerWasAutoSelected,
47
- target: config.target,
48
- outputDir: config.outputDir,
49
- debug: config.debug
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
- try {
57
+ logger.info(`Session started. Target: ${config.target}`);
55
58
  await runReviewCycle(config);
59
+ logger.info("Session completed successfully.");
56
60
  } catch (error) {
57
- console.error(error?.stack || String(error));
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();
@@ -79,4 +79,8 @@ export class ProgressDisplay {
79
79
  writeLine(message) {
80
80
  this.stream.write(`${message}\n`);
81
81
  }
82
+
83
+ log(message) {
84
+ this.writeLine(message);
85
+ }
82
86
  }
@@ -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
- debugLog(config, `Trying reviewer: ${reviewerName}`);
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
- debugLog(config, `Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
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
- debugLog(
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
- debugLog(
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
- console.log("Saved review state no longer matches repository history. Resetting checkpoint.");
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
- console.log(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
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
- debugLog(config, `Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
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
- console.log(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
746
+ logger.info(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
753
747
  return;
754
748
  }
755
749
 
756
- console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
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
- debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
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
- debugLog(config, `Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
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
- console.log(`Backlog remains. Latest ${backend.changeName} is ${backend.formatChangeId(latestChangeId)}.`);
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
- debugLog(
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
- debugLog(
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