kodevu 0.1.6 → 0.1.8

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 CHANGED
@@ -65,7 +65,7 @@ npx kodevu /path/to/your/repo --config ./config.current.json
65
65
  ## Config
66
66
 
67
67
  - `target`: required repository target; can be provided by config or as the CLI positional argument
68
- - `reviewer`: `codex` or `gemini`; default `codex`
68
+ - `reviewer`: `codex`, `gemini`, or `auto`; default `auto`
69
69
  - `reviewPrompt`: saved into the report as review context
70
70
  - `outputDir`: report output directory; default `~/.kodevu`
71
71
  - `stateFilePath`: review state file path; default `~/.kodevu/state.json`
@@ -76,7 +76,7 @@ Internal defaults:
76
76
 
77
77
  - by default, review reports and state are stored under `~/.kodevu`; first run starts from the current latest change instead of replaying full history
78
78
  - if `./config.json` is absent, Kodevu still runs with built-in defaults as long as you pass a positional `target`
79
- - Kodevu invokes `git`, `svn`, and the configured reviewer CLI from `PATH`; debug logging is enabled only by passing `--debug` or `-d`
79
+ - Kodevu invokes `git`, `svn`, and the configured reviewer CLI from `PATH`; when `reviewer` is `auto`, it randomly selects one from the installed reviewer CLIs it can find in `PATH`; debug logging is enabled only by passing `--debug` or `-d`
80
80
 
81
81
  ## Target Rules
82
82
 
@@ -88,10 +88,11 @@ Internal defaults:
88
88
 
89
89
  - `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
90
90
  - `reviewer: "gemini"` uses `gemini -p` in non-interactive mode.
91
+ - `reviewer: "auto"` probes `codex` and `gemini` in `PATH`, then randomly chooses one of the available CLIs for this run.
91
92
  - Large diffs are truncated before being sent to the reviewer or written into the report once they exceed the configured line or character limits.
92
93
  - 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.
93
94
  - For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
94
- - SVN reports keep the `r123.md` naming style.
95
- - Git reports are written as `git-<short-commit-hash>.md`.
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`.
96
97
  - `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
97
98
  - 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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "target": "C:/path/to/your/repository-or-subdirectory",
3
- "reviewer": "codex",
3
+ "reviewer": "auto",
4
4
  "reviewPrompt": "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
5
5
  "outputDir": "~/.kodevu",
6
6
  "stateFilePath": "~/.kodevu/state.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write Markdown review reports.",
6
6
  "bin": {
@@ -13,7 +13,6 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
- "once": "node src/index.js",
17
16
  "check": "node --check src/index.js && node --check src/config.js && node --check src/review-runner.js && node --check src/svn-client.js && node --check src/git-client.js && node --check src/vcs-client.js && node --check src/shell.js"
18
17
  },
19
18
  "engines": {
package/src/config.js CHANGED
@@ -3,11 +3,13 @@ import { constants as fsConstants } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { findCommandOnPath } from "./shell.js";
6
7
 
7
8
  const defaultStorageDir = path.join(os.homedir(), ".kodevu");
9
+ const SUPPORTED_REVIEWERS = ["codex", "gemini"];
8
10
 
9
11
  const defaultConfig = {
10
- reviewer: "codex",
12
+ reviewer: "auto",
11
13
  target: "",
12
14
  outputDir: defaultStorageDir,
13
15
  stateFilePath: path.join(defaultStorageDir, "state.json"),
@@ -37,6 +39,27 @@ function resolveConfigPath(baseDir, value) {
37
39
  return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
38
40
  }
39
41
 
42
+ async function resolveAutoReviewer(debug, loadedConfigPath) {
43
+ const availableReviewers = [];
44
+
45
+ for (const reviewerName of SUPPORTED_REVIEWERS) {
46
+ const commandPath = await findCommandOnPath(reviewerName, { debug });
47
+ if (commandPath) {
48
+ availableReviewers.push({ reviewerName, commandPath });
49
+ }
50
+ }
51
+
52
+ if (availableReviewers.length === 0) {
53
+ throw new Error(
54
+ `No reviewer CLI was found in PATH for "reviewer": "auto". Install one of: ${SUPPORTED_REVIEWERS.join(", ")}${
55
+ loadedConfigPath ? ` (${loadedConfigPath})` : ""
56
+ }`
57
+ );
58
+ }
59
+
60
+ return availableReviewers[Math.floor(Math.random() * availableReviewers.length)];
61
+ }
62
+
40
63
  export function parseCliArgs(argv) {
41
64
  const args = {
42
65
  command: "run",
@@ -120,11 +143,18 @@ export async function loadConfig(configPath, cliArgs = {}) {
120
143
  throw new Error('Missing target. Pass `npx kodevu <repo-path>` or set "target" in config.json.');
121
144
  }
122
145
 
123
- config.reviewer = String(config.reviewer || "codex").toLowerCase();
124
146
  config.debug = Boolean(cliArgs.debug);
125
-
126
- if (!["codex", "gemini"].includes(config.reviewer)) {
127
- throw new Error(`"reviewer" must be one of "codex" or "gemini"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
147
+ config.reviewer = String(config.reviewer || "auto").toLowerCase();
148
+
149
+ if (config.reviewer === "auto") {
150
+ const selectedReviewer = await resolveAutoReviewer(config.debug, loadedConfigPath);
151
+ config.reviewer = selectedReviewer.reviewerName;
152
+ config.reviewerCommandPath = selectedReviewer.commandPath;
153
+ config.reviewerWasAutoSelected = true;
154
+ } else if (!SUPPORTED_REVIEWERS.includes(config.reviewer)) {
155
+ throw new Error(
156
+ `"reviewer" must be one of "codex", "gemini", or "auto"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`
157
+ );
128
158
  }
129
159
 
130
160
  config.configPath = loadedConfigPath;
@@ -162,7 +192,7 @@ Options:
162
192
  --help, -h Show help
163
193
 
164
194
  Config highlights:
165
- reviewer codex | gemini
195
+ reviewer codex | gemini | auto
166
196
  target Repository target path (Git) or SVN working copy / URL; CLI positional target overrides config
167
197
  `);
168
198
  }
package/src/index.js CHANGED
@@ -31,11 +31,19 @@ if (cliArgs.command === "init") {
31
31
 
32
32
  const config = await loadConfig(cliArgs.configPath, cliArgs);
33
33
 
34
+ if (config.reviewerWasAutoSelected) {
35
+ console.log(
36
+ `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
37
+ );
38
+ }
39
+
34
40
  if (config.debug) {
35
41
  console.error(
36
42
  `[debug] Loaded config: ${JSON.stringify({
37
43
  configPath: config.configPath,
38
44
  reviewer: config.reviewer,
45
+ reviewerCommandPath: config.reviewerCommandPath,
46
+ reviewerWasAutoSelected: config.reviewerWasAutoSelected,
39
47
  target: config.target,
40
48
  outputDir: config.outputDir,
41
49
  debug: config.debug
package/src/shell.js CHANGED
@@ -103,3 +103,23 @@ export async function runCommand(command, args = [], options = {}) {
103
103
  }
104
104
  });
105
105
  }
106
+
107
+ export async function findCommandOnPath(command, options = {}) {
108
+ const locator = process.platform === "win32" ? "where" : "which";
109
+ const result = await runCommand(locator, [command], {
110
+ allowFailure: true,
111
+ trim: true,
112
+ debug: options.debug
113
+ });
114
+
115
+ if (result.code !== 0 || result.timedOut || !result.stdout) {
116
+ return null;
117
+ }
118
+
119
+ return (
120
+ result.stdout
121
+ .split(/\r?\n/)
122
+ .map((item) => item.trim())
123
+ .find(Boolean) || null
124
+ );
125
+ }
package/src/vcs-client.js CHANGED
@@ -25,8 +25,17 @@ function createSvnBackend() {
25
25
  return `r${revision}`;
26
26
  },
27
27
  getReportFileName(revision) {
28
- return `r${revision}.md`;
28
+ const now = new Date();
29
+ const datePrefix = now.getFullYear() +
30
+ String(now.getMonth() + 1).padStart(2, '0') +
31
+ String(now.getDate()).padStart(2, '0') +
32
+ '-' +
33
+ String(now.getHours()).padStart(2, '0') +
34
+ String(now.getMinutes()).padStart(2, '0') +
35
+ String(now.getSeconds()).padStart(2, '0');
36
+ return `${datePrefix}-svn-r${revision}.md`;
29
37
  },
38
+
30
39
  async getTargetInfo(config) {
31
40
  return await svnClient.getTargetInfo(config);
32
41
  },
@@ -86,8 +95,17 @@ function createGitBackend() {
86
95
  return commitHash.slice(0, 12);
87
96
  },
88
97
  getReportFileName(commitHash) {
89
- return `git-${commitHash.slice(0, 12)}.md`;
98
+ const now = new Date();
99
+ const datePrefix = now.getFullYear() +
100
+ String(now.getMonth() + 1).padStart(2, '0') +
101
+ String(now.getDate()).padStart(2, '0') +
102
+ '-' +
103
+ String(now.getHours()).padStart(2, '0') +
104
+ String(now.getMinutes()).padStart(2, '0') +
105
+ String(now.getSeconds()).padStart(2, '0');
106
+ return `${datePrefix}-git-${commitHash.slice(0, 12)}.md`;
90
107
  },
108
+
91
109
  async getTargetInfo(config) {
92
110
  return await gitClient.getTargetInfo(config);
93
111
  },