kodevu 0.1.5 → 0.1.7

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
@@ -12,8 +12,8 @@ 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 `reports/`
16
- 5. Update `data/state.json` so the same change is not reviewed twice.
15
+ - write the result to `~/.kodevu/`
16
+ 5. Update `~/.kodevu/state.json` so the same change is not reviewed twice.
17
17
 
18
18
  ## Setup
19
19
 
@@ -22,6 +22,7 @@ npx kodevu init
22
22
  ```
23
23
 
24
24
  This creates `config.json` in the current directory from the packaged `config.example.json`.
25
+ You only need this when you want to override defaults such as `reviewer` or output paths.
25
26
 
26
27
  If you want a different path:
27
28
 
@@ -29,68 +30,69 @@ If you want a different path:
29
30
  npx kodevu init --config ./config.current.json
30
31
  ```
31
32
 
32
- Then edit `config.json` and set `target`.
33
+ Then edit `config.json` if you want custom settings.
33
34
 
34
- `config.json` is the default config file. If you do not pass `--config`, Kodevu will load `./config.json` from the current directory.
35
+ If you do not pass `--config`, Kodevu will try to load `./config.json` from the current directory only when that file exists. Otherwise it runs with built-in defaults.
35
36
 
36
37
  ## Run
37
38
 
38
- Run one cycle:
39
+ Run once:
39
40
 
40
41
  ```bash
41
- npx kodevu --once
42
+ npx kodevu /path/to/your/repo
42
43
  ```
43
44
 
44
- Run one cycle with debug logs:
45
+ Run once with debug logs:
45
46
 
46
47
  ```bash
47
- npx kodevu --once --debug
48
+ npx kodevu /path/to/your/repo --debug
48
49
  ```
49
50
 
50
- Start the scheduler:
51
+ Use a custom config path only when needed:
51
52
 
52
53
  ```bash
53
- npx kodevu
54
+ npx kodevu --config ./config.current.json
54
55
  ```
55
56
 
56
- Use a custom config path only when needed:
57
+ Or combine a config file with a positional target override:
57
58
 
58
59
  ```bash
59
- npx kodevu --config ./config.current.json --once
60
+ npx kodevu /path/to/your/repo --config ./config.current.json
60
61
  ```
61
62
 
62
63
  `--debug` / `-d` is a CLI-only switch. It is not read from `config.json`.
63
64
 
64
65
  ## Config
65
66
 
66
- - `target`: required repository target
67
- - `reviewer`: `codex` or `gemini`; default `codex`
68
- - `pollCron`: cron schedule, default every 10 minutes
67
+ - `target`: required repository target; can be provided by config or as the CLI positional argument
68
+ - `reviewer`: `codex`, `gemini`, or `auto`; default `auto`
69
69
  - `reviewPrompt`: saved into the report as review context
70
- - `outputDir`: report output directory; default `./reports`
70
+ - `outputDir`: report output directory; default `~/.kodevu`
71
+ - `stateFilePath`: review state file path; default `~/.kodevu/state.json`
71
72
  - `commandTimeoutMs`: timeout for a single review command execution in milliseconds
72
73
  - `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
73
74
 
74
75
  Internal defaults:
75
76
 
76
- - review state is always stored in `./data/state.json`, and first run starts from the current latest change instead of replaying full history
77
- - Kodevu invokes `git`, `svn`, and the configured reviewer CLI from `PATH`; debug logging is enabled only by passing `--debug` or `-d`
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
+ - 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`; 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`
78
80
 
79
81
  ## Target Rules
80
82
 
81
83
  - For SVN, `target` can be a working copy path or repository URL.
82
84
  - For Git, `target` must be a local repository path or a subdirectory inside a local repository.
83
85
  - The tool tries Git first for existing local paths, then falls back to SVN.
84
- - Legacy `svnTarget` is still accepted for backward compatibility.
85
86
 
86
87
  ## Notes
87
88
 
88
89
  - `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
89
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.
90
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.
91
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.
92
94
  - For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
93
95
  - SVN reports keep the `r123.md` naming style.
94
96
  - Git reports are written as `git-<short-commit-hash>.md`.
95
- - `data/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
97
+ - `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
96
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,9 +1,9 @@
1
1
  {
2
2
  "target": "C:/path/to/your/repository-or-subdirectory",
3
- "reviewer": "codex",
4
- "pollCron": "*/10 * * * *",
3
+ "reviewer": "auto",
5
4
  "reviewPrompt": "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
6
- "outputDir": "./reports",
5
+ "outputDir": "~/.kodevu",
6
+ "stateFilePath": "~/.kodevu/state.json",
7
7
  "commandTimeoutMs": 600000,
8
8
  "maxRevisionsPerRun": 5
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "start": "node src/index.js",
16
- "once": "node src/index.js --once",
16
+ "once": "node src/index.js",
17
17
  "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
18
  },
19
19
  "engines": {
@@ -22,7 +22,6 @@
22
22
  "dependencies": {
23
23
  "cross-spawn": "^7.0.6",
24
24
  "fast-xml-parser": "^5.2.5",
25
- "iconv-lite": "^0.6.3",
26
- "node-cron": "^4.2.1"
25
+ "iconv-lite": "^0.6.3"
27
26
  }
28
27
  }
package/src/config.js CHANGED
@@ -1,24 +1,71 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { constants as fsConstants } from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
6
+ import { findCommandOnPath } from "./shell.js";
7
+
8
+ const defaultStorageDir = path.join(os.homedir(), ".kodevu");
9
+ const SUPPORTED_REVIEWERS = ["codex", "gemini"];
5
10
 
6
11
  const defaultConfig = {
7
- reviewer: "codex",
12
+ reviewer: "auto",
8
13
  target: "",
9
- pollCron: "*/10 * * * *",
10
- outputDir: "./reports",
14
+ outputDir: defaultStorageDir,
15
+ stateFilePath: path.join(defaultStorageDir, "state.json"),
11
16
  commandTimeoutMs: 600000,
12
17
  reviewPrompt:
13
18
  "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
14
19
  maxRevisionsPerRun: 20
15
20
  };
16
21
 
22
+ function resolveConfigPath(baseDir, value) {
23
+ if (!value) {
24
+ return value;
25
+ }
26
+
27
+ if (typeof value !== "string") {
28
+ return path.resolve(baseDir, String(value));
29
+ }
30
+
31
+ if (value === "~") {
32
+ return os.homedir();
33
+ }
34
+
35
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
36
+ return path.join(os.homedir(), value.slice(2));
37
+ }
38
+
39
+ return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
40
+ }
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
+
17
63
  export function parseCliArgs(argv) {
18
64
  const args = {
19
65
  command: "run",
20
66
  configPath: "config.json",
21
- once: false,
67
+ configExplicitlySet: false,
68
+ target: "",
22
69
  debug: false,
23
70
  help: false,
24
71
  commandExplicitlySet: false
@@ -33,11 +80,6 @@ export function parseCliArgs(argv) {
33
80
  continue;
34
81
  }
35
82
 
36
- if (value === "--once") {
37
- args.once = true;
38
- continue;
39
- }
40
-
41
83
  if (value === "--help" || value === "-h") {
42
84
  args.help = true;
43
85
  continue;
@@ -54,9 +96,17 @@ export function parseCliArgs(argv) {
54
96
  throw new Error(`Missing value for ${value}`);
55
97
  }
56
98
  args.configPath = configPath;
99
+ args.configExplicitlySet = true;
57
100
  index += 1;
58
101
  continue;
59
102
  }
103
+
104
+ if (!value.startsWith("-") && args.command === "run" && !args.target) {
105
+ args.target = value;
106
+ continue;
107
+ }
108
+
109
+ throw new Error(`Unexpected argument: ${value}`);
60
110
  }
61
111
 
62
112
  delete args.commandExplicitlySet;
@@ -65,40 +115,61 @@ export function parseCliArgs(argv) {
65
115
 
66
116
  export async function loadConfig(configPath, cliArgs = {}) {
67
117
  const absoluteConfigPath = path.resolve(configPath);
68
- const raw = await fs.readFile(absoluteConfigPath, "utf8");
118
+ let loadedConfig = {};
119
+ let loadedConfigPath = null;
120
+ let baseDir = process.cwd();
121
+
122
+ try {
123
+ const raw = await fs.readFile(absoluteConfigPath, "utf8");
124
+ loadedConfig = JSON.parse(raw);
125
+ loadedConfigPath = absoluteConfigPath;
126
+ baseDir = path.dirname(absoluteConfigPath);
127
+ } catch (error) {
128
+ if (!(error?.code === "ENOENT" && !cliArgs.configExplicitlySet)) {
129
+ throw error;
130
+ }
131
+ }
132
+
69
133
  const config = {
70
134
  ...defaultConfig,
71
- ...JSON.parse(raw)
135
+ ...loadedConfig
72
136
  };
73
137
 
74
- if (!config.target && config.svnTarget) {
75
- config.target = config.svnTarget;
138
+ if (cliArgs.target) {
139
+ config.target = cliArgs.target;
76
140
  }
77
141
 
78
142
  if (!config.target) {
79
- throw new Error(`Missing required config field "target" (or legacy "svnTarget") in ${absoluteConfigPath}`);
143
+ throw new Error('Missing target. Pass `npx kodevu <repo-path>` or set "target" in config.json.');
80
144
  }
81
145
 
82
- config.reviewer = String(config.reviewer || "codex").toLowerCase();
83
146
  config.debug = Boolean(cliArgs.debug);
84
-
85
- if (!["codex", "gemini"].includes(config.reviewer)) {
86
- throw new Error(`"reviewer" must be one of "codex" or "gemini" in ${absoluteConfigPath}`);
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
+ );
87
158
  }
88
159
 
89
- config.configPath = absoluteConfigPath;
90
- config.baseDir = path.dirname(absoluteConfigPath);
91
- config.outputDir = path.resolve(config.baseDir, config.outputDir);
92
- config.stateFilePath = path.resolve(config.baseDir, "./data/state.json");
160
+ config.configPath = loadedConfigPath;
161
+ config.baseDir = baseDir;
162
+ config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
163
+ config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
93
164
  config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
94
165
  config.commandTimeoutMs = Number(config.commandTimeoutMs);
95
166
 
96
167
  if (!Number.isInteger(config.maxRevisionsPerRun) || config.maxRevisionsPerRun <= 0) {
97
- throw new Error(`"maxRevisionsPerRun" must be a positive integer in ${absoluteConfigPath}`);
168
+ throw new Error(`"maxRevisionsPerRun" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
98
169
  }
99
170
 
100
171
  if (!Number.isInteger(config.commandTimeoutMs) || config.commandTimeoutMs <= 0) {
101
- throw new Error(`"commandTimeoutMs" must be a positive integer in ${absoluteConfigPath}`);
172
+ throw new Error(`"commandTimeoutMs" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
102
173
  }
103
174
 
104
175
  return config;
@@ -110,18 +181,19 @@ export function printHelp() {
110
181
  Usage:
111
182
  kodevu init
112
183
  npx kodevu init
113
- kodevu [--config config.json] [--once]
114
- npx kodevu [--config config.json] [--once]
184
+ kodevu <target> [--debug]
185
+ npx kodevu <target> [--debug]
186
+ kodevu [--config config.json]
187
+ npx kodevu [--config config.json]
115
188
 
116
189
  Options:
117
- --config, -c Path to config json. Default: ./config.json in the current directory
190
+ --config, -c Optional config json path. If omitted, ./config.json is loaded only when present
118
191
  --debug, -d Print extra debug information to the console
119
- --once Run one polling cycle and exit
120
192
  --help, -h Show help
121
193
 
122
194
  Config highlights:
123
- reviewer codex | gemini
124
- target Repository target path (Git) or SVN working copy / URL
195
+ reviewer codex | gemini | auto
196
+ target Repository target path (Git) or SVN working copy / URL; CLI positional target overrides config
125
197
  `);
126
198
  }
127
199
 
package/src/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import cron from "node-cron";
4
3
  import { initConfig, loadConfig, parseCliArgs, printHelp } from "./config.js";
5
4
  import { runReviewCycle } from "./review-runner.js";
6
5
 
@@ -32,49 +31,30 @@ if (cliArgs.command === "init") {
32
31
 
33
32
  const config = await loadConfig(cliArgs.configPath, cliArgs);
34
33
 
34
+ if (config.reviewerWasAutoSelected) {
35
+ console.log(
36
+ `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
37
+ );
38
+ }
39
+
35
40
  if (config.debug) {
36
41
  console.error(
37
42
  `[debug] Loaded config: ${JSON.stringify({
38
43
  configPath: config.configPath,
39
44
  reviewer: config.reviewer,
45
+ reviewerCommandPath: config.reviewerCommandPath,
46
+ reviewerWasAutoSelected: config.reviewerWasAutoSelected,
40
47
  target: config.target,
41
- pollCron: config.pollCron,
42
48
  outputDir: config.outputDir,
43
- debug: config.debug,
44
- once: cliArgs.once
49
+ debug: config.debug
45
50
  })}`
46
51
  );
47
52
  }
48
53
 
49
- let running = false;
50
-
51
- async function runOnce() {
52
- if (running) {
53
- console.log("A review cycle is already running, skipping this trigger.");
54
- return;
55
- }
56
-
57
- running = true;
58
-
59
- try {
60
- await runReviewCycle(config);
61
- } catch (error) {
62
- console.error(error?.stack || String(error));
63
- process.exitCode = 1;
64
- } finally {
65
- running = false;
66
- }
67
- }
68
-
69
- if (cliArgs.once) {
70
- await runOnce();
71
- process.exit(process.exitCode || 0);
54
+ try {
55
+ await runReviewCycle(config);
56
+ } catch (error) {
57
+ console.error(error?.stack || String(error));
58
+ process.exitCode = 1;
72
59
  }
73
-
74
- await runOnce();
75
-
76
- console.log(`Scheduler started. Cron: ${config.pollCron}`);
77
-
78
- cron.schedule(config.pollCron, () => {
79
- void runOnce();
80
- });
60
+ process.exit(process.exitCode || 0);
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
+ }