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 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
- npm install
22
- copy config.example.json config.json
21
+ npx kodevu init
23
22
  ```
24
23
 
25
- Then edit `config.json` and set `target`.
24
+ This creates `config.json` in the current directory from the packaged `config.example.json`.
26
25
 
27
- Install as a CLI package:
26
+ If you want a different path:
28
27
 
29
28
  ```bash
30
- npm install -g kodevu
31
- copy config.example.json config.json
29
+ npx kodevu init --config ./config.current.json
32
30
  ```
33
31
 
34
- ## Run
32
+ Then edit `config.json` and set `target`.
35
33
 
36
- Run one cycle:
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
- ```bash
39
- npm run once
40
- ```
36
+ ## Run
41
37
 
42
- Or run directly as the published CLI:
38
+ Run one cycle:
43
39
 
44
40
  ```bash
45
- kodevu --once
41
+ npx kodevu --once
46
42
  ```
47
43
 
48
- Use `npx` without installing globally:
44
+ Run one cycle with debug logs:
49
45
 
50
46
  ```bash
51
- npx kodevu --once --config ./config.json
47
+ npx kodevu --once --debug
52
48
  ```
53
49
 
54
50
  Start the scheduler:
55
51
 
56
52
  ```bash
57
- npm start
53
+ npx kodevu
58
54
  ```
59
55
 
60
- Published CLI form:
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
- Use a custom config path:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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": {
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
- help: false
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
- args.configPath = argv[index + 1];
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
@@ -31,6 +31,7 @@ async function statPath(targetPath) {
31
31
  async function runGit(config, args, options = {}) {
32
32
  return await runCommand(GIT_COMMAND, args, {
33
33
  encoding: COMMAND_ENCODING,
34
+ debug: config.debug,
34
35
  ...options
35
36
  });
36
37
  }
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
- const cliArgs = parseCliArgs(process.argv.slice(2));
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
- const config = await loadConfig(cliArgs.configPath);
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
 
@@ -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;