kodevu 0.1.29 → 0.1.31

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
@@ -1,63 +1,55 @@
1
1
  # Kodevu
2
2
 
3
- A Node.js tool that polls new SVN revisions or Git commits, fetches each change diff directly from the repository, sends the diff to a supported reviewer CLI, and writes review results to report files.
3
+ A Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
4
4
 
5
5
  ## Workflow
6
6
 
7
7
  1. Detect the repository type automatically (Git or SVN).
8
- 2. Read the latest change from `target`.
9
- 3. Find changes that have not been reviewed yet.
10
- 4. For each change:
11
- - load metadata and changed paths from SVN or Git
12
- - generate a unified diff for that single revision or commit
13
- - send the diff and change metadata to the configured reviewer CLI
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 `~/.kodevu/` (Markdown by default; optional JSON via config)
16
- 5. Update `~/.kodevu/state.json` so the same change is not reviewed twice.
8
+ 2. Fetch the specified revision(s) as requested by the user:
9
+ - A single specific revision/commit via `--rev`.
10
+ - The latest $N$ revisions/commits via `--last` (default: 1).
11
+ 3. For each change:
12
+ - Load metadata and changed paths from SVN or Git.
13
+ - Generate a unified diff for that single revision or commit.
14
+ - Send the diff and change metadata to the configured reviewer CLI.
15
+ - Allow the reviewer to inspect related local repository files in read-only mode when a local workspace is available.
16
+ - Write the result to `~/.kodevu/` (Markdown by default; optional JSON via config).
17
+
18
+ **Note**: Kodevu is stateless. It does not track which changes have been reviewed previously.
17
19
 
18
20
  ## Quick start
19
21
 
22
+ Review the latest commit in your repository:
23
+
20
24
  ```bash
21
25
  npx kodevu /path/to/your/repo
22
26
  ```
23
27
 
24
- No config file is required for the default flow.
25
- Review reports are written to `~/.kodevu/` as Markdown (`.md`) by default, and review state is stored at `~/.kodevu/state.json`.
26
-
27
- If you want a config file, run `npx kodevu init` to create `./config.json` in the current directory.
28
-
29
- ## Setup
28
+ Review the latest 3 commits:
30
29
 
31
30
  ```bash
32
- npx kodevu init
31
+ npx kodevu /path/to/your/repo --last 3
33
32
  ```
34
33
 
35
- This creates `config.json` in the current directory from built-in defaults.
36
- You only need this when you want to override defaults such as `reviewer` or output paths.
37
-
38
- If you want a different path:
34
+ Review a specific commit:
39
35
 
40
36
  ```bash
41
- npx kodevu init --config ./config.current.json
37
+ npx kodevu /path/to/your/repo --rev abc1234
42
38
  ```
43
39
 
44
- Then edit `config.json` if you want custom settings.
40
+ Review reports are written to `~/.kodevu/` as Markdown (`.md`) by default.
45
41
 
46
- 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.
47
-
48
- ## Run
42
+ ## Setup
49
43
 
50
- Run:
44
+ If you want to customize settings beyond the defaults:
51
45
 
52
46
  ```bash
53
- npx kodevu /path/to/your/repo
47
+ npx kodevu init
54
48
  ```
55
49
 
56
- Run with debug logs:
50
+ This creates `config.json` in the current directory. You only need this when you want to override defaults such as `reviewer` or output paths.
57
51
 
58
- ```bash
59
- npx kodevu /path/to/your/repo --debug
60
- ```
52
+ ## Run
61
53
 
62
54
  Specify the output language (e.g., Chinese):
63
55
 
@@ -65,60 +57,43 @@ Specify the output language (e.g., Chinese):
65
57
  npx kodevu /path/to/your/repo --lang zh
66
58
  ```
67
59
 
68
- Use a custom config path only when needed:
60
+ Run with debug logs:
69
61
 
70
62
  ```bash
71
- npx kodevu --config ./config.current.json
63
+ npx kodevu /path/to/your/repo --debug
72
64
  ```
73
65
 
74
- Or combine a config file with a positional target override:
66
+ Use a custom config path:
75
67
 
76
68
  ```bash
77
- npx kodevu /path/to/your/repo --config ./config.current.json
69
+ npx kodevu --config ./my-config.json
78
70
  ```
79
71
 
80
- `--debug` / `-d` is a CLI-only switch. It is not read from `config.json`.
81
-
82
- ## Config
83
-
84
- - `target`: required repository target; can be provided by config or as the CLI positional argument
85
- - `reviewer`: `codex`, `gemini`, `copilot`, or `auto`; default `auto`
86
- - `prompt`: additional instructions for the reviewer; usually empty by default as the core instructions are built-in
87
- - `lang`: output language for the review (e.g., `zh`, `en`, `auto`); default `auto`
88
- - `outputDir`: report output directory; default `~/.kodevu`
89
- - `outputFormats`: report formats to generate; supports `markdown` and `json`; default `["markdown"]`
90
- - `stateFilePath`: review state file path; default `~/.kodevu/state.json`
91
- - `commandTimeoutMs`: timeout for a single review command execution in milliseconds
92
- - `maxRevisionsPerRun`: cap the number of pending changes per polling cycle
93
-
94
- Internal defaults:
72
+ ## Config / CLI Options
95
73
 
96
- - by default, review reports and state are stored under `~/.kodevu`; first run starts from the current latest change instead of replaying full history
97
- - if `./config.json` is absent, Kodevu still runs with built-in defaults as long as you pass a positional `target`
98
- - 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`
74
+ - `target`: Repository target path or SVN URL. (CLI positional argument overrides config)
75
+ - `reviewer`: `codex`, `gemini`, `copilot`, or `auto` (default: `auto`).
76
+ - `rev`: A specific revision or commit hash to review.
77
+ - `last`: Number of latest revisions to review (default: 1 if `rev` is not set).
78
+ - `lang`: Output language for the review (e.g., `zh`, `en`, `auto`).
79
+ - `prompt`: Additional instructions for the reviewer.
80
+ - `outputDir`: Report output directory (default: `~/.kodevu`).
81
+ - `outputFormats`: Report formats to generate (supports `markdown` and `json`; default: `["markdown"]`).
82
+ - `commandTimeoutMs`: Timeout for a single review command execution in milliseconds.
83
+ - `maxRevisionsPerRun`: Cap the number of revisions handled in one run.
99
84
 
100
85
  ## Target Rules
101
86
 
102
87
  - For SVN, `target` can be a working copy path or repository URL.
103
88
  - For Git, `target` must be a local repository path or a subdirectory inside a local repository.
104
- - The tool tries Git first for existing local paths, then falls back to SVN.
89
+ - The tool tries Git first, then falls back to SVN.
105
90
 
106
91
  ## Notes
107
92
 
108
- - `reviewer: "codex"` uses `codex exec` with the diff embedded in the prompt.
109
- - `reviewer: "gemini"` uses `gemini -p` in non-interactive mode.
110
- - `reviewer: "copilot"` uses `copilot -p` in non-interactive mode.
111
- - `reviewer: "auto"` probes `codex`, `gemini`, and `copilot` in `PATH`, then randomly chooses one of the available CLIs for this run.
112
- - Large diffs are truncated before being sent to the reviewer or written into the report once they exceed the configured line or character limits.
113
- - 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.
114
- - For remote SVN URLs without a local working copy, the review still relies on the diff and change metadata only.
115
- - By default, SVN reports are written as `<YYYYMMDD-HHmmss>-svn-r<revision>.md`.
116
- - By default, Git reports are written as `<YYYYMMDD-HHmmss>-git-<short-commit-hash>.md`.
117
- - If `outputFormats` includes `json`, matching `.json` files are generated alongside Markdown reports.
118
- - `~/.kodevu/state.json` stores per-project checkpoints keyed by repository identity; only the v2 multi-project structure is supported.
119
- - 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.
120
- - Review instructions are built into the tool in English to ensure consistent logic across reviewers. The `lang` setting (CLI `--lang` or config `lang`) determines the language AI uses for the resulting review. When set to `auto`, Kodevu detects the language from the system environment (`LANG`, `LC_ALL`, or system locale).
121
- - Each report includes a `Token Usage` section recording token consumption for the review task. When the reviewer CLI outputs token statistics (via stderr), those are used directly (`source: "reviewer"`). Otherwise tokens are estimated at ~4 characters per token (`source: "estimate"`). The JSON report contains a `tokenUsage` object with `inputTokens`, `outputTokens`, `totalTokens`, and `source`.
93
+ - `reviewer: "auto"` probes `codex`, `gemini`, and `copilot` in `PATH`, then selects an available one.
94
+ - Large diffs are truncated based on internal limits to fit AI context windows.
95
+ - 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.
96
+ - If the reviewer command exits non-zero or times out, the report is still written containing the error details.
122
97
 
123
98
  ## License
124
99
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
package/src/config.js CHANGED
@@ -12,12 +12,13 @@ const defaultConfig = {
12
12
  target: "",
13
13
  lang: "auto",
14
14
  outputDir: defaultStorageDir,
15
- stateFilePath: path.join(defaultStorageDir, "state.json"),
16
15
  logsDir: path.join(defaultStorageDir, "logs"),
17
16
  commandTimeoutMs: 600000,
18
17
  prompt: "",
19
18
  maxRevisionsPerRun: 5,
20
- outputFormats: ["markdown"]
19
+ outputFormats: ["markdown"],
20
+ rev: "",
21
+ last: 0
21
22
  };
22
23
 
23
24
  const configTemplate = {
@@ -26,11 +27,12 @@ const configTemplate = {
26
27
  lang: defaultConfig.lang,
27
28
  prompt: defaultConfig.prompt,
28
29
  outputDir: "~/.kodevu",
29
- stateFilePath: "~/.kodevu/state.json",
30
30
  logsDir: "~/.kodevu/logs",
31
31
  commandTimeoutMs: defaultConfig.commandTimeoutMs,
32
32
  maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
33
- outputFormats: defaultConfig.outputFormats
33
+ outputFormats: defaultConfig.outputFormats,
34
+ rev: "",
35
+ last: 1
34
36
  };
35
37
 
36
38
  function resolveConfigPath(baseDir, value) {
@@ -145,6 +147,8 @@ export function parseCliArgs(argv) {
145
147
  reviewer: "",
146
148
  lang: "",
147
149
  prompt: "",
150
+ rev: "",
151
+ last: "",
148
152
  commandExplicitlySet: false
149
153
  };
150
154
 
@@ -208,6 +212,26 @@ export function parseCliArgs(argv) {
208
212
  continue;
209
213
  }
210
214
 
215
+ if (value === "--rev" || value === "-v") {
216
+ const rev = argv[index + 1];
217
+ if (!rev || rev.startsWith("-")) {
218
+ throw new Error(`Missing value for ${value}`);
219
+ }
220
+ args.rev = rev;
221
+ index += 1;
222
+ continue;
223
+ }
224
+
225
+ if (value === "--last" || value === "-n") {
226
+ const last = argv[index + 1];
227
+ if (!last || last.startsWith("-")) {
228
+ throw new Error(`Missing value for ${value}`);
229
+ }
230
+ args.last = last;
231
+ index += 1;
232
+ continue;
233
+ }
234
+
211
235
  if (!value.startsWith("-") && args.command === "run" && !args.target) {
212
236
  args.target = value;
213
237
  continue;
@@ -258,6 +282,14 @@ export async function loadConfig(configPath, cliArgs = {}) {
258
282
  config.lang = cliArgs.lang;
259
283
  }
260
284
 
285
+ if (cliArgs.rev) {
286
+ config.rev = cliArgs.rev;
287
+ }
288
+
289
+ if (cliArgs.last) {
290
+ config.last = cliArgs.last;
291
+ }
292
+
261
293
  if (!config.target) {
262
294
  throw new Error('Missing target. Pass `npx kodevu <repo-path>` or set "target" in config.json.');
263
295
  }
@@ -284,12 +316,16 @@ export async function loadConfig(configPath, cliArgs = {}) {
284
316
  config.configPath = loadedConfigPath;
285
317
  config.baseDir = baseDir;
286
318
  config.outputDir = resolveConfigPath(config.baseDir, config.outputDir);
287
- config.stateFilePath = resolveConfigPath(config.baseDir, config.stateFilePath);
288
319
  config.logsDir = resolveConfigPath(config.baseDir, config.logsDir);
289
320
  config.maxRevisionsPerRun = Number(config.maxRevisionsPerRun);
290
321
  config.commandTimeoutMs = Number(config.commandTimeoutMs);
322
+ config.last = Number(config.last);
291
323
  config.outputFormats = normalizeOutputFormats(config.outputFormats, loadedConfigPath);
292
324
 
325
+ if (!config.rev && (isNaN(config.last) || config.last <= 0)) {
326
+ config.last = 1;
327
+ }
328
+
293
329
  if (!Number.isInteger(config.maxRevisionsPerRun) || config.maxRevisionsPerRun <= 0) {
294
330
  throw new Error(`"maxRevisionsPerRun" must be a positive integer${loadedConfigPath ? ` in ${loadedConfigPath}` : ""}`);
295
331
  }
@@ -317,6 +353,8 @@ Options:
317
353
  --reviewer, -r Override reviewer (codex | gemini | copilot | auto)
318
354
  --prompt, -p Override prompt
319
355
  --lang, -l Override output language (e.g. zh, en, auto)
356
+ --rev, -v Review a specific revision or commit hash
357
+ --last, -n Review the latest N revisions (default: 1)
320
358
  --debug, -d Print extra debug information to the console
321
359
  --help, -h Show help
322
360
 
package/src/git-client.js CHANGED
@@ -124,6 +124,16 @@ export async function getPendingCommits(config, targetInfo, startExclusive, endI
124
124
  return splitLines(result.stdout).slice(0, limit);
125
125
  }
126
126
 
127
+ export async function getLatestCommitIds(config, targetInfo, limit) {
128
+ const result = await runGit(
129
+ config,
130
+ ["rev-list", "-n", String(limit), "HEAD", ...buildPathArgs(targetInfo)],
131
+ { cwd: targetInfo.repoRootPath, trim: true }
132
+ );
133
+ // Reverse to get chronological order (oldest to newest among the latest n)
134
+ return splitLines(result.stdout).reverse();
135
+ }
136
+
127
137
  export async function getCommitDiff(config, targetInfo, commitHash) {
128
138
  const result = await runGit(
129
139
  config,
@@ -13,14 +13,41 @@ export function getReviewWorkspaceRoot(config, backend, targetInfo) {
13
13
  return config.baseDir;
14
14
  }
15
15
 
16
+ function getLanguageDisplayName(lang) {
17
+ if (!lang) return "English";
18
+ const low = lang.toLowerCase();
19
+ if (low.startsWith("zh")) {
20
+ if (low === "zh-tw" || low === "zh-hk") return "Traditional Chinese (繁體中文)";
21
+ return "Simplified Chinese (简体中文)";
22
+ }
23
+ if (low === "jp" || low.startsWith("ja")) return "Japanese (日本語)";
24
+ if (low === "kr" || low.startsWith("ko")) return "Korean (한국어)";
25
+ if (low === "fr") return "French (Français)";
26
+ if (low === "de") return "German (Deutsch)";
27
+ if (low === "es") return "Spanish (Español)";
28
+ if (low === "it") return "Italian (Italiano)";
29
+ if (low === "ru") return "Russian (Русский)";
30
+ return lang;
31
+ }
32
+
16
33
  export function buildPrompt(config, backend, targetInfo, details, reviewDiffPayload) {
17
34
  const fileList = details.changedPaths.map((item) => `${item.action} ${item.relativePath}`).join("\n");
18
35
  const workspaceRoot = getReviewWorkspaceRoot(config, backend, targetInfo);
19
36
  const canReadRelatedFiles = backend.kind === "git" || Boolean(targetInfo.workingCopyPath);
20
37
 
21
- const langInstruction = config.resolvedLang === "zh"
22
- ? "Please use Simplified Chinese for your response."
23
- : `Please use ${config.resolvedLang || "English"} for your response.`;
38
+ const langName = getLanguageDisplayName(config.resolvedLang);
39
+ const lowLang = (config.resolvedLang || "").toLowerCase();
40
+ let langInstruction = `IMPORTANT: Your entire response MUST be in ${langName}. All explanations, comments, and structure should strictly follow the ${langName} language rules.`;
41
+
42
+ if (lowLang.startsWith("zh")) {
43
+ if (lowLang === "zh-tw" || lowLang === "zh-hk") {
44
+ langInstruction += "\n請務必使用繁體中文進行回覆,所有的審查評論和分析都必須以繁體中文呈現。";
45
+ } else {
46
+ langInstruction += "\n请务必使用简体中文进行回复,所有的审查评论和分析都必须以简体中文呈现。";
47
+ }
48
+ }
49
+
50
+
24
51
 
25
52
  return [
26
53
  CORE_REVIEW_INSTRUCTION,
@@ -41,8 +68,18 @@ export function buildPrompt(config, backend, targetInfo, details, reviewDiffPayl
41
68
  `Commit message:\n${details.message || "(empty)"}`,
42
69
  reviewDiffPayload.wasTruncated
43
70
  ? `Diff delivery note: the diff was truncated before being sent to the reviewer to stay within configured size limits. Original diff size was ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars, and the included excerpt is ${reviewDiffPayload.outputLineCount} lines / ${reviewDiffPayload.outputCharCount} chars. Use the changed file list and inspect related workspace files when needed.`
44
- : `Diff delivery note: the full diff is included. Size is ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars.`
71
+ : `Diff delivery note: the full diff is included. Size is ${reviewDiffPayload.originalLineCount} lines / ${reviewDiffPayload.originalCharCount} chars.`,
72
+ `--- IMPORTANT LANGUAGE RULE ---\nYou MUST respond strictly in ${langName}. No other language should be used for the explanation and comments.${
73
+ lowLang.startsWith("zh")
74
+ ? lowLang === "zh-tw" || lowLang === "zh-hk"
75
+ ? "\n請務必完全使用繁體中文進行回覆,所有的審查分析、注釋和總結都必須使用繁體中文。"
76
+ : "\n请务必完全使用简体中文进行回复,所有的审查分析、注释和总结都必须使用简体中文。"
77
+ : ""
78
+ }`
79
+
45
80
  ].filter(Boolean).join("\n\n");
81
+
82
+
46
83
  }
47
84
 
48
85
  export function formatTokenUsage(tokenUsage) {
@@ -7,14 +7,6 @@ import {
7
7
  writeTextFile,
8
8
  writeJsonFile
9
9
  } from "./utils.js";
10
- import {
11
- loadState,
12
- saveState,
13
- getProjectState,
14
- updateProjectState,
15
- readLastReviewedId,
16
- buildStateSnapshot
17
- } from "./state-manager.js";
18
10
  import {
19
11
  shouldWriteFormat,
20
12
  buildReport,
@@ -158,45 +150,19 @@ export async function runReviewCycle(config) {
158
150
 
159
151
  const { backend, targetInfo } = await resolveRepositoryContext(config);
160
152
  logger.debug(
161
- `Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target} stateKey=${targetInfo.stateKey}`
162
- );
163
- const latestChangeId = await backend.getLatestChangeId(config, targetInfo);
164
- const stateFile = await loadState(config.stateFilePath);
165
- const projectState = getProjectState(stateFile, targetInfo);
166
- let lastReviewedId = readLastReviewedId(projectState, backend, targetInfo);
167
- logger.debug(
168
- `Checkpoint status: latest=${backend.formatChangeId(latestChangeId)} lastReviewed=${lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)"}`
153
+ `Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target}`
169
154
  );
170
155
 
171
- if (lastReviewedId) {
172
- const checkpointIsValid = await backend.isValidCheckpoint(config, targetInfo, lastReviewedId, latestChangeId);
173
-
174
- if (!checkpointIsValid) {
175
- logger.info("Saved review state no longer matches repository history. Resetting checkpoint.");
176
- lastReviewedId = null;
177
- }
178
- }
179
-
180
156
  let changeIdsToReview = [];
181
157
 
182
- if (!lastReviewedId) {
183
- changeIdsToReview = [latestChangeId];
184
- logger.info(`Initialized state to review the latest ${backend.changeName} ${backend.formatChangeId(latestChangeId)} first.`);
158
+ if (config.rev) {
159
+ changeIdsToReview = [config.rev];
185
160
  } else {
186
- changeIdsToReview = await backend.getPendingChangeIds(
187
- config,
188
- targetInfo,
189
- lastReviewedId,
190
- latestChangeId,
191
- config.maxRevisionsPerRun
192
- );
161
+ changeIdsToReview = await backend.getLatestChangeIds(config, targetInfo, config.last || 1);
193
162
  }
194
163
 
195
- logger.debug(`Planned ${changeIdsToReview.length} ${backend.changeName}(s) for this cycle.`);
196
-
197
164
  if (changeIdsToReview.length === 0) {
198
- const lastKnownId = lastReviewedId ? backend.formatChangeId(lastReviewedId) : "(none)";
199
- logger.info(`No new ${backend.changeName}s. Last reviewed: ${lastKnownId}`);
165
+ logger.info("No changes found to review.");
200
166
  return;
201
167
  }
202
168
 
@@ -215,20 +181,12 @@ export async function runReviewCycle(config) {
215
181
  updateOverallProgress(progress, index, changeIdsToReview.length, fraction, `${displayId} | ${stage}`);
216
182
  };
217
183
 
218
- let result;
219
-
220
184
  try {
221
- result = await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress, log: (message) => progress.log(message) });
222
- syncOverallProgress(0.94, "saving checkpoint");
185
+ await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress, log: (message) => progress.log(message) });
186
+ updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
223
187
  } catch (error) {
224
188
  progress.fail(`failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
225
189
  throw error;
226
190
  }
227
-
228
- const nextProjectSnapshot = buildStateSnapshot(backend, targetInfo, changeId);
229
- await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectSnapshot));
230
- stateFile.projects[targetInfo.stateKey] = nextProjectSnapshot;
231
- logger.debug(`Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
232
- updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
233
191
  }
234
192
  }
package/src/svn-client.js CHANGED
@@ -112,6 +112,20 @@ export async function getPendingRevisions(config, targetInfo, startExclusive, en
112
112
  .slice(0, limit);
113
113
  }
114
114
 
115
+ export async function getLatestRevisionIds(config, targetInfo, limit) {
116
+ const result = await runCommand(
117
+ SVN_COMMAND,
118
+ ["log", "--xml", "--quiet", "-l", String(limit), "-r", "HEAD:1", getRemoteTarget(targetInfo, config)],
119
+ { encoding: COMMAND_ENCODING, trim: true, debug: config.debug }
120
+ );
121
+ const parsed = xmlParser.parse(result.stdout);
122
+
123
+ return asArray(parsed?.log?.logentry)
124
+ .map((entry) => Number(entry?.revision))
125
+ .filter((revision) => Number.isInteger(revision))
126
+ .sort((left, right) => left - right);
127
+ }
128
+
115
129
  export async function getRevisionDiff(config, revision) {
116
130
  const result = await runCommand(
117
131
  SVN_COMMAND,
package/src/vcs-client.js CHANGED
@@ -42,8 +42,8 @@ function createSvnBackend() {
42
42
  async getLatestChangeId(config, targetInfo) {
43
43
  return await svnClient.getLatestRevision(config, targetInfo);
44
44
  },
45
- async getPendingChangeIds(config, targetInfo, startExclusive, endInclusive, limit) {
46
- return await svnClient.getPendingRevisions(config, targetInfo, startExclusive, endInclusive, limit);
45
+ async getLatestChangeIds(config, targetInfo, limit) {
46
+ return await svnClient.getLatestRevisionIds(config, targetInfo, limit);
47
47
  },
48
48
  async getChangeDiff(config, targetInfo, revision) {
49
49
  return await svnClient.getRevisionDiff(config, revision);
@@ -59,16 +59,6 @@ function createSvnBackend() {
59
59
  message: details.message,
60
60
  changedPaths: details.changedPaths
61
61
  };
62
- },
63
- async isValidCheckpoint() {
64
- return true;
65
- },
66
- toStateValue(revision) {
67
- return Number(revision);
68
- },
69
- fromStateValue(state) {
70
- const id = state.lastReviewedId;
71
- return Number.isInteger(id) ? id : null;
72
62
  }
73
63
  };
74
64
  }
@@ -99,8 +89,8 @@ function createGitBackend() {
99
89
  async getLatestChangeId(config, targetInfo) {
100
90
  return await gitClient.getLatestCommit(config, targetInfo);
101
91
  },
102
- async getPendingChangeIds(config, targetInfo, startExclusive, endInclusive, limit) {
103
- return await gitClient.getPendingCommits(config, targetInfo, startExclusive, endInclusive, limit);
92
+ async getLatestChangeIds(config, targetInfo, limit) {
93
+ return await gitClient.getLatestCommitIds(config, targetInfo, limit);
104
94
  },
105
95
  async getChangeDiff(config, targetInfo, commitHash) {
106
96
  return await gitClient.getCommitDiff(config, targetInfo, commitHash);
@@ -116,16 +106,6 @@ function createGitBackend() {
116
106
  message: details.message,
117
107
  changedPaths: details.changedPaths
118
108
  };
119
- },
120
- async isValidCheckpoint(config, targetInfo, checkpointCommit, latestCommit) {
121
- return await gitClient.isValidCheckpoint(config, targetInfo, checkpointCommit, latestCommit);
122
- },
123
- toStateValue(commitHash) {
124
- return String(commitHash);
125
- },
126
- fromStateValue(state) {
127
- const id = state.lastReviewedId;
128
- return typeof id === "string" && id ? id : null;
129
109
  }
130
110
  };
131
111
  }
@@ -1,61 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { pathExists, ensureDir } from "./utils.js";
4
-
5
- function normalizeStateFile(state) {
6
- if (!state || typeof state !== "object" || Array.isArray(state)) {
7
- throw new Error('State file must be a JSON object with shape {"version":2,"projects":{...}}.');
8
- }
9
-
10
- if (state.version !== 2) {
11
- throw new Error('State file version must be 2.');
12
- }
13
-
14
- if (!state.projects || typeof state.projects !== "object" || Array.isArray(state.projects)) {
15
- throw new Error('State file must contain a "projects" object.');
16
- }
17
-
18
- return {
19
- version: 2,
20
- projects: state.projects
21
- };
22
- }
23
-
24
- export async function loadState(stateFile) {
25
- if (!(await pathExists(stateFile))) {
26
- return { version: 2, projects: {} };
27
- }
28
-
29
- const raw = await fs.readFile(stateFile, "utf8");
30
- return normalizeStateFile(JSON.parse(raw));
31
- }
32
-
33
- export async function saveState(stateFile, state) {
34
- await ensureDir(path.dirname(stateFile));
35
- await fs.writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
36
- }
37
-
38
- export function getProjectState(stateFile, targetInfo) {
39
- return stateFile.projects?.[targetInfo.stateKey] ?? {};
40
- }
41
-
42
- export function updateProjectState(stateFile, targetInfo, projectState) {
43
- return {
44
- version: 2,
45
- projects: {
46
- ...(stateFile.projects || {}),
47
- [targetInfo.stateKey]: projectState
48
- }
49
- };
50
- }
51
-
52
- export function readLastReviewedId(state, backend, targetInfo) {
53
- return backend.fromStateValue(state);
54
- }
55
-
56
- export function buildStateSnapshot(backend, targetInfo, changeId) {
57
- return {
58
- lastReviewedId: backend.toStateValue(changeId),
59
- updatedAt: new Date().toISOString()
60
- };
61
- }