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 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 configured repository type, or use the explicit `vcs` setting.
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 `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,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` 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
- - `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 `./reports`
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 is always stored in `./data/state.json`, and first run starts from the current latest change instead of replaying full history
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
- - When `vcs` is `auto`, the tool tries Git first for existing local paths, then falls back to SVN.
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
- - `data/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
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.
@@ -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": "./reports",
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.4",
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 --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,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
- pollCron: "*/10 * * * *",
11
- outputDir: "./reports",
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
- once: false,
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
- const raw = await fs.readFile(absoluteConfigPath, "utf8");
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
- ...JSON.parse(raw)
112
+ ...loadedConfig
73
113
  };
74
114
 
75
- if (!config.target && config.svnTarget) {
76
- config.target = config.svnTarget;
115
+ if (cliArgs.target) {
116
+ config.target = cliArgs.target;
77
117
  }
78
118
 
79
119
  if (!config.target) {
80
- throw new Error(`Missing required config field "target" (or legacy "svnTarget") in ${absoluteConfigPath}`);
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 ${absoluteConfigPath}`);
127
+ throw new Error(`"reviewer" must be one of "codex" or "gemini"${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
93
128
  }
94
129
 
95
- config.configPath = absoluteConfigPath;
96
- config.baseDir = path.dirname(absoluteConfigPath);
97
- config.outputDir = path.resolve(config.baseDir, config.outputDir);
98
- config.stateFilePath = path.resolve(config.baseDir, "./data/state.json");
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 ${absoluteConfigPath}`);
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 ${absoluteConfigPath}`);
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 [--config config.json] [--once]
120
- npx kodevu [--config config.json] [--once]
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 Path to config json. Default: ./config.json in the current directory
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
- let running = false;
51
-
52
- async function runOnce() {
53
- if (running) {
54
- console.log("A review cycle is already running, skipping this trigger.");
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))) {