kodevu 0.1.4 → 0.1.6
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 +21 -21
- package/config.example.json +2 -3
- package/package.json +3 -4
- package/src/config.js +67 -32
- package/src/index.js +7 -36
- package/src/vcs-client.js +0 -14
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A Node.js tool that polls new SVN revisions or Git commits, fetches each change
|
|
|
4
4
|
|
|
5
5
|
## Workflow
|
|
6
6
|
|
|
7
|
-
1. Detect the
|
|
7
|
+
1. Detect the repository type automatically (Git or SVN).
|
|
8
8
|
2. Read the latest change from `target`.
|
|
9
9
|
3. Find changes that have not been reviewed yet.
|
|
10
10
|
4. For each change:
|
|
@@ -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
|
|
16
|
-
5. Update
|
|
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,60 +30,59 @@ If you want a different path:
|
|
|
29
30
|
npx kodevu init --config ./config.current.json
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
Then edit `config.json`
|
|
33
|
+
Then edit `config.json` if you want custom settings.
|
|
33
34
|
|
|
34
|
-
|
|
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
|
|
39
|
+
Run once:
|
|
39
40
|
|
|
40
41
|
```bash
|
|
41
|
-
npx kodevu
|
|
42
|
+
npx kodevu /path/to/your/repo
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
Run
|
|
45
|
+
Run once with debug logs:
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
|
-
npx kodevu
|
|
48
|
+
npx kodevu /path/to/your/repo --debug
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
|
|
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
|
-
|
|
57
|
+
Or combine a config file with a positional target override:
|
|
57
58
|
|
|
58
59
|
```bash
|
|
59
|
-
npx kodevu --config ./config.current.json
|
|
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
|
-
- `vcs`: `auto`, `svn`, or `git`; default `auto`
|
|
67
|
+
- `target`: required repository target; can be provided by config or as the CLI positional argument
|
|
68
68
|
- `reviewer`: `codex` or `gemini`; default `codex`
|
|
69
|
-
- `pollCron`: cron schedule, default every 10 minutes
|
|
70
69
|
- `reviewPrompt`: saved into the report as review context
|
|
71
|
-
- `outputDir`: report output directory; default
|
|
70
|
+
- `outputDir`: report output directory; default `~/.kodevu`
|
|
71
|
+
- `stateFilePath`: review state file path; default `~/.kodevu/state.json`
|
|
72
72
|
- `commandTimeoutMs`: timeout for a single review command execution in milliseconds
|
|
73
73
|
- `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
|
|
74
74
|
|
|
75
75
|
Internal defaults:
|
|
76
76
|
|
|
77
|
-
- review state
|
|
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`
|
|
78
79
|
- Kodevu invokes `git`, `svn`, and the configured reviewer CLI from `PATH`; debug logging is enabled only by passing `--debug` or `-d`
|
|
79
80
|
|
|
80
81
|
## Target Rules
|
|
81
82
|
|
|
82
83
|
- For SVN, `target` can be a working copy path or repository URL.
|
|
83
84
|
- For Git, `target` must be a local repository path or a subdirectory inside a local repository.
|
|
84
|
-
-
|
|
85
|
-
- Legacy `svnTarget` is still accepted for backward compatibility.
|
|
85
|
+
- The tool tries Git first for existing local paths, then falls back to SVN.
|
|
86
86
|
|
|
87
87
|
## Notes
|
|
88
88
|
|
|
@@ -93,5 +93,5 @@ Internal defaults:
|
|
|
93
93
|
- For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
|
|
94
94
|
- SVN reports keep the `r123.md` naming style.
|
|
95
95
|
- Git reports are written as `git-<short-commit-hash>.md`.
|
|
96
|
-
-
|
|
96
|
+
- `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
|
|
97
97
|
- 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
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"target": "C:/path/to/your/repository-or-subdirectory",
|
|
3
|
-
"vcs": "auto",
|
|
4
3
|
"reviewer": "codex",
|
|
5
|
-
"pollCron": "*/10 * * * *",
|
|
6
4
|
"reviewPrompt": "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
7
|
-
"outputDir": "
|
|
5
|
+
"outputDir": "~/.kodevu",
|
|
6
|
+
"stateFilePath": "~/.kodevu/state.json",
|
|
8
7
|
"commandTimeoutMs": 600000,
|
|
9
8
|
"maxRevisionsPerRun": 5
|
|
10
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodevu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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
|
|
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,25 +1,48 @@
|
|
|
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";
|
|
5
6
|
|
|
7
|
+
const defaultStorageDir = path.join(os.homedir(), ".kodevu");
|
|
8
|
+
|
|
6
9
|
const defaultConfig = {
|
|
7
|
-
vcs: "auto",
|
|
8
10
|
reviewer: "codex",
|
|
9
11
|
target: "",
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
outputDir: defaultStorageDir,
|
|
13
|
+
stateFilePath: path.join(defaultStorageDir, "state.json"),
|
|
12
14
|
commandTimeoutMs: 600000,
|
|
13
15
|
reviewPrompt:
|
|
14
16
|
"请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
|
|
15
17
|
maxRevisionsPerRun: 20
|
|
16
18
|
};
|
|
17
19
|
|
|
20
|
+
function resolveConfigPath(baseDir, value) {
|
|
21
|
+
if (!value) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
return path.resolve(baseDir, String(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (value === "~") {
|
|
30
|
+
return os.homedir();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
34
|
+
return path.join(os.homedir(), value.slice(2));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return path.isAbsolute(value) ? value : path.resolve(baseDir, value);
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
export function parseCliArgs(argv) {
|
|
19
41
|
const args = {
|
|
20
42
|
command: "run",
|
|
21
43
|
configPath: "config.json",
|
|
22
|
-
|
|
44
|
+
configExplicitlySet: false,
|
|
45
|
+
target: "",
|
|
23
46
|
debug: false,
|
|
24
47
|
help: false,
|
|
25
48
|
commandExplicitlySet: false
|
|
@@ -34,11 +57,6 @@ export function parseCliArgs(argv) {
|
|
|
34
57
|
continue;
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
if (value === "--once") {
|
|
38
|
-
args.once = true;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
60
|
if (value === "--help" || value === "-h") {
|
|
43
61
|
args.help = true;
|
|
44
62
|
continue;
|
|
@@ -55,9 +73,17 @@ export function parseCliArgs(argv) {
|
|
|
55
73
|
throw new Error(`Missing value for ${value}`);
|
|
56
74
|
}
|
|
57
75
|
args.configPath = configPath;
|
|
76
|
+
args.configExplicitlySet = true;
|
|
58
77
|
index += 1;
|
|
59
78
|
continue;
|
|
60
79
|
}
|
|
80
|
+
|
|
81
|
+
if (!value.startsWith("-") && args.command === "run" && !args.target) {
|
|
82
|
+
args.target = value;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new Error(`Unexpected argument: ${value}`);
|
|
61
87
|
}
|
|
62
88
|
|
|
63
89
|
delete args.commandExplicitlySet;
|
|
@@ -66,45 +92,54 @@ export function parseCliArgs(argv) {
|
|
|
66
92
|
|
|
67
93
|
export async function loadConfig(configPath, cliArgs = {}) {
|
|
68
94
|
const absoluteConfigPath = path.resolve(configPath);
|
|
69
|
-
|
|
95
|
+
let loadedConfig = {};
|
|
96
|
+
let loadedConfigPath = null;
|
|
97
|
+
let baseDir = process.cwd();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
101
|
+
loadedConfig = JSON.parse(raw);
|
|
102
|
+
loadedConfigPath = absoluteConfigPath;
|
|
103
|
+
baseDir = path.dirname(absoluteConfigPath);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (!(error?.code === "ENOENT" && !cliArgs.configExplicitlySet)) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
70
110
|
const config = {
|
|
71
111
|
...defaultConfig,
|
|
72
|
-
...
|
|
112
|
+
...loadedConfig
|
|
73
113
|
};
|
|
74
114
|
|
|
75
|
-
if (
|
|
76
|
-
config.target =
|
|
115
|
+
if (cliArgs.target) {
|
|
116
|
+
config.target = cliArgs.target;
|
|
77
117
|
}
|
|
78
118
|
|
|
79
119
|
if (!config.target) {
|
|
80
|
-
throw new Error(
|
|
120
|
+
throw new Error('Missing target. Pass `npx kodevu <repo-path>` or set "target" in config.json.');
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
config.vcs = String(config.vcs || "auto").toLowerCase();
|
|
84
123
|
config.reviewer = String(config.reviewer || "codex").toLowerCase();
|
|
85
124
|
config.debug = Boolean(cliArgs.debug);
|
|
86
125
|
|
|
87
|
-
if (!["auto", "svn", "git"].includes(config.vcs)) {
|
|
88
|
-
throw new Error(`"vcs" must be one of "auto", "svn", or "git" in ${absoluteConfigPath}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
126
|
if (!["codex", "gemini"].includes(config.reviewer)) {
|
|
92
|
-
throw new Error(`"reviewer" must be one of "codex" or "gemini" in ${
|
|
127
|
+
throw new Error(`"reviewer" must be one of "codex" or "gemini"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
93
128
|
}
|
|
94
129
|
|
|
95
|
-
config.configPath =
|
|
96
|
-
config.baseDir =
|
|
97
|
-
config.outputDir =
|
|
98
|
-
config.stateFilePath =
|
|
130
|
+
config.configPath = loadedConfigPath;
|
|
131
|
+
config.baseDir = baseDir;
|
|
132
|
+
config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
|
|
133
|
+
config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
|
|
99
134
|
config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
|
|
100
135
|
config.commandTimeoutMs = Number(config.commandTimeoutMs);
|
|
101
136
|
|
|
102
137
|
if (!Number.isInteger(config.maxRevisionsPerRun) || config.maxRevisionsPerRun <= 0) {
|
|
103
|
-
throw new Error(`"maxRevisionsPerRun" must be a positive integer in ${
|
|
138
|
+
throw new Error(`"maxRevisionsPerRun" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
104
139
|
}
|
|
105
140
|
|
|
106
141
|
if (!Number.isInteger(config.commandTimeoutMs) || config.commandTimeoutMs <= 0) {
|
|
107
|
-
throw new Error(`"commandTimeoutMs" must be a positive integer in ${
|
|
142
|
+
throw new Error(`"commandTimeoutMs" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
|
|
108
143
|
}
|
|
109
144
|
|
|
110
145
|
return config;
|
|
@@ -116,19 +151,19 @@ export function printHelp() {
|
|
|
116
151
|
Usage:
|
|
117
152
|
kodevu init
|
|
118
153
|
npx kodevu init
|
|
119
|
-
kodevu
|
|
120
|
-
npx kodevu
|
|
154
|
+
kodevu <target> [--debug]
|
|
155
|
+
npx kodevu <target> [--debug]
|
|
156
|
+
kodevu [--config config.json]
|
|
157
|
+
npx kodevu [--config config.json]
|
|
121
158
|
|
|
122
159
|
Options:
|
|
123
|
-
--config, -c
|
|
160
|
+
--config, -c Optional config json path. If omitted, ./config.json is loaded only when present
|
|
124
161
|
--debug, -d Print extra debug information to the console
|
|
125
|
-
--once Run one polling cycle and exit
|
|
126
162
|
--help, -h Show help
|
|
127
163
|
|
|
128
164
|
Config highlights:
|
|
129
|
-
vcs auto | svn | git
|
|
130
165
|
reviewer codex | gemini
|
|
131
|
-
target Repository target path (Git) or SVN working copy / URL
|
|
166
|
+
target Repository target path (Git) or SVN working copy / URL; CLI positional target overrides config
|
|
132
167
|
`);
|
|
133
168
|
}
|
|
134
169
|
|
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
|
|
|
@@ -36,46 +35,18 @@ if (config.debug) {
|
|
|
36
35
|
console.error(
|
|
37
36
|
`[debug] Loaded config: ${JSON.stringify({
|
|
38
37
|
configPath: config.configPath,
|
|
39
|
-
vcs: config.vcs,
|
|
40
38
|
reviewer: config.reviewer,
|
|
41
39
|
target: config.target,
|
|
42
|
-
pollCron: config.pollCron,
|
|
43
40
|
outputDir: config.outputDir,
|
|
44
|
-
debug: config.debug
|
|
45
|
-
once: cliArgs.once
|
|
41
|
+
debug: config.debug
|
|
46
42
|
})}`
|
|
47
43
|
);
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
running = true;
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
await runReviewCycle(config);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(error?.stack || String(error));
|
|
64
|
-
process.exitCode = 1;
|
|
65
|
-
} finally {
|
|
66
|
-
running = false;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (cliArgs.once) {
|
|
71
|
-
await runOnce();
|
|
72
|
-
process.exit(process.exitCode || 0);
|
|
46
|
+
try {
|
|
47
|
+
await runReviewCycle(config);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(error?.stack || String(error));
|
|
50
|
+
process.exitCode = 1;
|
|
73
51
|
}
|
|
74
|
-
|
|
75
|
-
await runOnce();
|
|
76
|
-
|
|
77
|
-
console.log(`Scheduler started. Cron: ${config.pollCron}`);
|
|
78
|
-
|
|
79
|
-
cron.schedule(config.pollCron, () => {
|
|
80
|
-
void runOnce();
|
|
81
|
-
});
|
|
52
|
+
process.exit(process.exitCode || 0);
|
package/src/vcs-client.js
CHANGED
|
@@ -144,20 +144,6 @@ const backends = {
|
|
|
144
144
|
};
|
|
145
145
|
|
|
146
146
|
export async function resolveRepositoryContext(config) {
|
|
147
|
-
if (config.vcs === "svn") {
|
|
148
|
-
return {
|
|
149
|
-
backend: backends.svn,
|
|
150
|
-
targetInfo: await backends.svn.getTargetInfo(config)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (config.vcs === "git") {
|
|
155
|
-
return {
|
|
156
|
-
backend: backends.git,
|
|
157
|
-
targetInfo: await backends.git.getTargetInfo(config)
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
147
|
const candidateTargetPath = path.resolve(config.baseDir, config.target);
|
|
162
148
|
|
|
163
149
|
if (!isLikelyUrl(config.target) && (await pathExists(candidateTargetPath))) {
|