kodevu 0.1.0 → 0.1.2
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 +16 -29
- package/package.json +1 -1
- package/src/config.js +47 -4
- package/src/git-client.js +1 -0
- package/src/index.js +37 -3
- package/src/review-runner.js +22 -2
- package/src/shell.js +27 -1
- package/src/svn-client.js +6 -5
package/README.md
CHANGED
|
@@ -18,62 +18,48 @@ A Node.js tool that polls new SVN revisions or Git commits, fetches each change
|
|
|
18
18
|
## Setup
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
|
|
22
|
-
copy config.example.json config.json
|
|
21
|
+
npx kodevu init
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
This creates `config.json` in the current directory from the packaged `config.example.json`.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
If you want a different path:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
|
-
|
|
31
|
-
copy config.example.json config.json
|
|
29
|
+
npx kodevu init --config ./config.current.json
|
|
32
30
|
```
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
Then edit `config.json` and set `target`.
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
`config.json` is the default config file. If you do not pass `--config`, Kodevu will load `./config.json` from the current directory.
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
npm run once
|
|
40
|
-
```
|
|
36
|
+
## Run
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
Run one cycle:
|
|
43
39
|
|
|
44
40
|
```bash
|
|
45
|
-
kodevu --once
|
|
41
|
+
npx kodevu --once
|
|
46
42
|
```
|
|
47
43
|
|
|
48
|
-
|
|
44
|
+
Run one cycle with debug logs:
|
|
49
45
|
|
|
50
46
|
```bash
|
|
51
|
-
npx kodevu --once --
|
|
47
|
+
npx kodevu --once --debug
|
|
52
48
|
```
|
|
53
49
|
|
|
54
50
|
Start the scheduler:
|
|
55
51
|
|
|
56
52
|
```bash
|
|
57
|
-
|
|
53
|
+
npx kodevu
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
Use a custom config path only when needed:
|
|
61
57
|
|
|
62
58
|
```bash
|
|
63
|
-
kodevu --config ./config.json
|
|
59
|
+
npx kodevu --config ./config.current.json --once
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
node src/index.js --config ./config.json --once
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Equivalent `npx` usage:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npx kodevu --config ./config.json --once
|
|
76
|
-
```
|
|
62
|
+
`--debug` / `-d` is a CLI-only switch. It is not read from `config.json`.
|
|
77
63
|
|
|
78
64
|
## Config
|
|
79
65
|
|
|
@@ -92,6 +78,7 @@ Internal defaults:
|
|
|
92
78
|
- review state is always stored in `./data/state.json`
|
|
93
79
|
- the tool always invokes `git`, `svn`, and the configured reviewer CLI from `PATH`
|
|
94
80
|
- command output is decoded as `utf8`
|
|
81
|
+
- debug logging is enabled only by passing `--debug` or `-d`
|
|
95
82
|
|
|
96
83
|
## Target Rules
|
|
97
84
|
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
|
|
4
6
|
const defaultConfig = {
|
|
5
7
|
vcs: "auto",
|
|
@@ -16,14 +18,23 @@ const defaultConfig = {
|
|
|
16
18
|
|
|
17
19
|
export function parseCliArgs(argv) {
|
|
18
20
|
const args = {
|
|
21
|
+
command: "run",
|
|
19
22
|
configPath: "config.json",
|
|
20
23
|
once: false,
|
|
21
|
-
|
|
24
|
+
debug: false,
|
|
25
|
+
help: false,
|
|
26
|
+
commandExplicitlySet: false
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
for (let index = 0; index < argv.length; index += 1) {
|
|
25
30
|
const value = argv[index];
|
|
26
31
|
|
|
32
|
+
if (value === "init" && !args.commandExplicitlySet && index === 0) {
|
|
33
|
+
args.command = "init";
|
|
34
|
+
args.commandExplicitlySet = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
if (value === "--once") {
|
|
28
39
|
args.once = true;
|
|
29
40
|
continue;
|
|
@@ -34,17 +45,27 @@ export function parseCliArgs(argv) {
|
|
|
34
45
|
continue;
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
if (value === "--debug" || value === "-d") {
|
|
49
|
+
args.debug = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
if (value === "--config" || value === "-c") {
|
|
38
|
-
|
|
54
|
+
const configPath = argv[index + 1];
|
|
55
|
+
if (!configPath || configPath.startsWith("-")) {
|
|
56
|
+
throw new Error(`Missing value for ${value}`);
|
|
57
|
+
}
|
|
58
|
+
args.configPath = configPath;
|
|
39
59
|
index += 1;
|
|
40
60
|
continue;
|
|
41
61
|
}
|
|
42
62
|
}
|
|
43
63
|
|
|
64
|
+
delete args.commandExplicitlySet;
|
|
44
65
|
return args;
|
|
45
66
|
}
|
|
46
67
|
|
|
47
|
-
export async function loadConfig(configPath) {
|
|
68
|
+
export async function loadConfig(configPath, cliArgs = {}) {
|
|
48
69
|
const absoluteConfigPath = path.resolve(configPath);
|
|
49
70
|
const raw = await fs.readFile(absoluteConfigPath, "utf8");
|
|
50
71
|
const config = {
|
|
@@ -62,6 +83,7 @@ export async function loadConfig(configPath) {
|
|
|
62
83
|
|
|
63
84
|
config.vcs = String(config.vcs || "auto").toLowerCase();
|
|
64
85
|
config.reviewer = String(config.reviewer || "codex").toLowerCase();
|
|
86
|
+
config.debug = Boolean(cliArgs.debug);
|
|
65
87
|
|
|
66
88
|
if (!["auto", "svn", "git"].includes(config.vcs)) {
|
|
67
89
|
throw new Error(`"vcs" must be one of "auto", "svn", or "git" in ${absoluteConfigPath}`);
|
|
@@ -93,11 +115,14 @@ export function printHelp() {
|
|
|
93
115
|
console.log(`Kodevu
|
|
94
116
|
|
|
95
117
|
Usage:
|
|
118
|
+
kodevu init
|
|
119
|
+
npx kodevu init
|
|
96
120
|
kodevu [--config config.json] [--once]
|
|
97
121
|
npx kodevu [--config config.json] [--once]
|
|
98
122
|
|
|
99
123
|
Options:
|
|
100
|
-
--config, -c Path to config json. Default: ./config.json
|
|
124
|
+
--config, -c Path to config json. Default: ./config.json in the current directory
|
|
125
|
+
--debug, -d Print extra debug information to the console
|
|
101
126
|
--once Run one polling cycle and exit
|
|
102
127
|
--help, -h Show help
|
|
103
128
|
|
|
@@ -107,3 +132,21 @@ Config highlights:
|
|
|
107
132
|
target Repository target path (Git) or SVN working copy / URL
|
|
108
133
|
`);
|
|
109
134
|
}
|
|
135
|
+
|
|
136
|
+
export async function initConfig(targetPath = "config.json") {
|
|
137
|
+
const absoluteTargetPath = path.resolve(targetPath);
|
|
138
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
139
|
+
const templatePath = path.join(packageRoot, "config.example.json");
|
|
140
|
+
|
|
141
|
+
await fs.mkdir(path.dirname(absoluteTargetPath), { recursive: true });
|
|
142
|
+
try {
|
|
143
|
+
await fs.copyFile(templatePath, absoluteTargetPath, fsConstants.COPYFILE_EXCL);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error?.code === "EEXIST") {
|
|
146
|
+
throw new Error(`Config file already exists: ${absoluteTargetPath}`);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return absoluteTargetPath;
|
|
152
|
+
}
|
package/src/git-client.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import cron from "node-cron";
|
|
4
|
-
import { loadConfig, parseCliArgs, printHelp } from "./config.js";
|
|
4
|
+
import { initConfig, loadConfig, parseCliArgs, printHelp } from "./config.js";
|
|
5
5
|
import { runReviewCycle } from "./review-runner.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let cliArgs;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
cliArgs = parseCliArgs(process.argv.slice(2));
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error(error?.message || String(error));
|
|
13
|
+
printHelp();
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
8
16
|
|
|
9
17
|
if (cliArgs.help) {
|
|
10
18
|
printHelp();
|
|
11
19
|
process.exit(0);
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
if (cliArgs.command === "init") {
|
|
23
|
+
try {
|
|
24
|
+
const createdPath = await initConfig(cliArgs.configPath);
|
|
25
|
+
console.log(`Created config: ${createdPath}`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error?.stack || String(error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const config = await loadConfig(cliArgs.configPath, cliArgs);
|
|
34
|
+
|
|
35
|
+
if (config.debug) {
|
|
36
|
+
console.error(
|
|
37
|
+
`[debug] Loaded config: ${JSON.stringify({
|
|
38
|
+
configPath: config.configPath,
|
|
39
|
+
vcs: config.vcs,
|
|
40
|
+
reviewer: config.reviewer,
|
|
41
|
+
target: config.target,
|
|
42
|
+
pollCron: config.pollCron,
|
|
43
|
+
outputDir: config.outputDir,
|
|
44
|
+
debug: config.debug,
|
|
45
|
+
once: cliArgs.once
|
|
46
|
+
})}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
15
49
|
|
|
16
50
|
let running = false;
|
|
17
51
|
|
package/src/review-runner.js
CHANGED
|
@@ -4,6 +4,12 @@ import path from "node:path";
|
|
|
4
4
|
import { runCommand } from "./shell.js";
|
|
5
5
|
import { resolveRepositoryContext } from "./vcs-client.js";
|
|
6
6
|
|
|
7
|
+
function debugLog(config, message) {
|
|
8
|
+
if (config.debug) {
|
|
9
|
+
console.error(`[debug] ${message}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
const REVIEWERS = {
|
|
8
14
|
codex: {
|
|
9
15
|
displayName: "Codex",
|
|
@@ -29,7 +35,8 @@ const REVIEWERS = {
|
|
|
29
35
|
cwd: workingDir,
|
|
30
36
|
input: [promptText, "Unified diff:", diffText].join("\n\n"),
|
|
31
37
|
allowFailure: true,
|
|
32
|
-
timeoutMs: config.commandTimeoutMs
|
|
38
|
+
timeoutMs: config.commandTimeoutMs,
|
|
39
|
+
debug: config.debug
|
|
33
40
|
});
|
|
34
41
|
|
|
35
42
|
let message = "";
|
|
@@ -58,7 +65,8 @@ const REVIEWERS = {
|
|
|
58
65
|
cwd: workingDir,
|
|
59
66
|
input: ["Unified diff:", diffText].join("\n\n"),
|
|
60
67
|
allowFailure: true,
|
|
61
|
-
timeoutMs: config.commandTimeoutMs
|
|
68
|
+
timeoutMs: config.commandTimeoutMs,
|
|
69
|
+
debug: config.debug
|
|
62
70
|
});
|
|
63
71
|
|
|
64
72
|
return {
|
|
@@ -294,10 +302,18 @@ export async function runReviewCycle(config) {
|
|
|
294
302
|
await ensureDir(config.outputDir);
|
|
295
303
|
|
|
296
304
|
const { backend, targetInfo } = await resolveRepositoryContext(config);
|
|
305
|
+
debugLog(
|
|
306
|
+
config,
|
|
307
|
+
`Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target} stateKey=${targetInfo.stateKey}`
|
|
308
|
+
);
|
|
297
309
|
const latestChangeId = await backend.getLatestChangeId(config, targetInfo);
|
|
298
310
|
const stateFile = await loadState(config.stateFilePath);
|
|
299
311
|
const projectState = getProjectState(stateFile, targetInfo);
|
|
300
312
|
let lastReviewedId = readLastReviewedId(projectState, backend, targetInfo);
|
|
313
|
+
debugLog(
|
|
314
|
+
config,
|
|
315
|
+
`Checkpoint status: latest=${backend.formatChangeId(latestChangeId)} lastReviewed=${lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)"}`
|
|
316
|
+
);
|
|
301
317
|
|
|
302
318
|
if (lastReviewedId) {
|
|
303
319
|
const checkpointIsValid = await backend.isValidCheckpoint(config, targetInfo, lastReviewedId, latestChangeId);
|
|
@@ -323,6 +339,8 @@ export async function runReviewCycle(config) {
|
|
|
323
339
|
);
|
|
324
340
|
}
|
|
325
341
|
|
|
342
|
+
debugLog(config, `Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
|
|
343
|
+
|
|
326
344
|
if (changeIdsToReview.length === 0) {
|
|
327
345
|
const lastKnownId = lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)";
|
|
328
346
|
console.log(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
|
|
@@ -332,11 +350,13 @@ export async function runReviewCycle(config) {
|
|
|
332
350
|
console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
|
|
333
351
|
|
|
334
352
|
for (const changeId of changeIdsToReview) {
|
|
353
|
+
debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
335
354
|
const result = await reviewChange(config, backend, targetInfo, changeId);
|
|
336
355
|
console.log(`Reviewed ${backend.formatChangeId(changeId)}: ${result.outputFile}`);
|
|
337
356
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
338
357
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
339
358
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
359
|
+
debugLog(config, `Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
|
|
340
360
|
}
|
|
341
361
|
|
|
342
362
|
const remainingChanges = await backend.getPendingChangeIds(
|
package/src/shell.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import iconv from "iconv-lite";
|
|
3
3
|
|
|
4
|
+
function debugLog(enabled, message) {
|
|
5
|
+
if (enabled) {
|
|
6
|
+
console.error(`[debug] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function summarizeOutput(text) {
|
|
11
|
+
if (!text) {
|
|
12
|
+
return "(empty)";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
16
|
+
return normalized.length > 400 ? `${normalized.slice(0, 400)}...` : normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
export async function runCommand(command, args = [], options = {}) {
|
|
5
20
|
const {
|
|
6
21
|
cwd,
|
|
@@ -9,9 +24,15 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
9
24
|
encoding = "utf8",
|
|
10
25
|
allowFailure = false,
|
|
11
26
|
timeoutMs = 0,
|
|
12
|
-
trim = false
|
|
27
|
+
trim = false,
|
|
28
|
+
debug = false
|
|
13
29
|
} = options;
|
|
14
30
|
|
|
31
|
+
debugLog(
|
|
32
|
+
debug,
|
|
33
|
+
`run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}`
|
|
34
|
+
);
|
|
35
|
+
|
|
15
36
|
return await new Promise((resolve, reject) => {
|
|
16
37
|
const child = spawn(command, args, {
|
|
17
38
|
cwd,
|
|
@@ -51,6 +72,11 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
51
72
|
stderr: trim ? stderr.trim() : stderr
|
|
52
73
|
};
|
|
53
74
|
|
|
75
|
+
debugLog(
|
|
76
|
+
debug,
|
|
77
|
+
`exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`
|
|
78
|
+
);
|
|
79
|
+
|
|
54
80
|
if ((result.code !== 0 || result.timedOut) && !allowFailure) {
|
|
55
81
|
const error = new Error(
|
|
56
82
|
`Command failed: ${command} ${args.join(" ")}\n${result.stderr || result.stdout}`.trim()
|
package/src/svn-client.js
CHANGED
|
@@ -38,7 +38,8 @@ function repoPathFromUrl(rootUrl, url) {
|
|
|
38
38
|
export async function getTargetInfo(config) {
|
|
39
39
|
const result = await runCommand(SVN_COMMAND, ["info", "--xml", config.target], {
|
|
40
40
|
encoding: COMMAND_ENCODING,
|
|
41
|
-
trim: true
|
|
41
|
+
trim: true,
|
|
42
|
+
debug: config.debug
|
|
42
43
|
});
|
|
43
44
|
const parsed = xmlParser.parse(result.stdout);
|
|
44
45
|
const entry = parsed?.info?.entry;
|
|
@@ -70,7 +71,7 @@ export async function getLatestRevision(config, targetInfo) {
|
|
|
70
71
|
const result = await runCommand(
|
|
71
72
|
SVN_COMMAND,
|
|
72
73
|
["log", "--xml", "-r", "HEAD:1", "-l", "1", getRemoteTarget(targetInfo, config)],
|
|
73
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
74
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
74
75
|
);
|
|
75
76
|
const parsed = xmlParser.parse(result.stdout);
|
|
76
77
|
const entry = parsed?.log?.logentry;
|
|
@@ -100,7 +101,7 @@ export async function getPendingRevisions(config, targetInfo, startExclusive, en
|
|
|
100
101
|
`${startRevision}:${endInclusive}`,
|
|
101
102
|
getRemoteTarget(targetInfo, config)
|
|
102
103
|
],
|
|
103
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
104
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
104
105
|
);
|
|
105
106
|
const parsed = xmlParser.parse(result.stdout);
|
|
106
107
|
|
|
@@ -115,7 +116,7 @@ export async function getRevisionDiff(config, revision) {
|
|
|
115
116
|
const result = await runCommand(
|
|
116
117
|
SVN_COMMAND,
|
|
117
118
|
["diff", "--git", "--internal-diff", "--ignore-properties", "-c", String(revision), config.target],
|
|
118
|
-
{ encoding: COMMAND_ENCODING, trim: false }
|
|
119
|
+
{ encoding: COMMAND_ENCODING, trim: false, debug: config.debug }
|
|
119
120
|
);
|
|
120
121
|
|
|
121
122
|
return result.stdout;
|
|
@@ -145,7 +146,7 @@ export async function getRevisionDetails(config, targetInfo, revision) {
|
|
|
145
146
|
const result = await runCommand(
|
|
146
147
|
SVN_COMMAND,
|
|
147
148
|
["log", "--xml", "-v", "-c", String(revision), getRemoteTarget(targetInfo, config)],
|
|
148
|
-
{ encoding: COMMAND_ENCODING, trim: true }
|
|
149
|
+
{ encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
|
|
149
150
|
);
|
|
150
151
|
const parsed = xmlParser.parse(result.stdout);
|
|
151
152
|
const entry = parsed?.log?.logentry;
|