kodevu 0.1.11 → 0.1.14
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/package.json +2 -2
- package/src/progress-ui.js +206 -0
- package/src/review-runner.js +52 -18
- package/src/vcs-client.js +4 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodevu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"start": "node src/index.js",
|
|
16
|
-
"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"
|
|
16
|
+
"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 && node --check src/progress-ui.js"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=20"
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
|
|
3
|
+
const SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
|
4
|
+
const DEFAULT_BAR_WIDTH = 24;
|
|
5
|
+
|
|
6
|
+
function clampProgress(value) {
|
|
7
|
+
if (!Number.isFinite(value)) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return Math.max(0, Math.min(1, value));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildBar(progress, width) {
|
|
15
|
+
const safeProgress = clampProgress(progress);
|
|
16
|
+
const filled = Math.max(Math.round(safeProgress * width), safeProgress > 0 ? 1 : 0);
|
|
17
|
+
|
|
18
|
+
if (filled >= width) {
|
|
19
|
+
return `[${"=".repeat(width)}]`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (filled === 0) {
|
|
23
|
+
return `[${" ".repeat(width)}]`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `[${"=".repeat(Math.max(filled - 1, 0))}>${" ".repeat(width - filled)}]`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class ProgressItem {
|
|
30
|
+
constructor(display, label, options = {}) {
|
|
31
|
+
this.display = display;
|
|
32
|
+
this.label = label;
|
|
33
|
+
this.barWidth = options.barWidth || DEFAULT_BAR_WIDTH;
|
|
34
|
+
this.progress = 0;
|
|
35
|
+
this.stage = "";
|
|
36
|
+
this.active = false;
|
|
37
|
+
this.lastFallbackLine = "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
start(stage = "starting") {
|
|
41
|
+
this.stage = stage;
|
|
42
|
+
this.active = true;
|
|
43
|
+
|
|
44
|
+
if (!this.display.enabled) {
|
|
45
|
+
this.writeFallback(`... ${this.label}: ${stage}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.display.start();
|
|
50
|
+
this.display.render();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update(progress, stage) {
|
|
54
|
+
this.progress = clampProgress(progress);
|
|
55
|
+
|
|
56
|
+
if (stage) {
|
|
57
|
+
this.stage = stage;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!this.display.enabled) {
|
|
61
|
+
this.writeFallback(`... ${this.label}: ${this.stage}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.display.render();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log(message) {
|
|
69
|
+
this.display.log(message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
succeed(message) {
|
|
73
|
+
this.finish("[done]", 1, message || `${this.label} complete`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fail(message) {
|
|
77
|
+
this.finish("[fail]", this.progress, message || `${this.label} failed`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
finish(prefix, progress, message) {
|
|
81
|
+
this.progress = clampProgress(progress);
|
|
82
|
+
this.active = false;
|
|
83
|
+
this.display.stopIfIdle();
|
|
84
|
+
this.display.writeStaticLine(
|
|
85
|
+
`${prefix} ${this.label} ${buildBar(this.progress, this.barWidth)} ${Math.round(this.progress * 100)
|
|
86
|
+
.toString()
|
|
87
|
+
.padStart(3, " ")}% ${message}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
renderLine(frameIndex) {
|
|
92
|
+
const spinner = SPINNER_FRAMES[frameIndex];
|
|
93
|
+
const bar = buildBar(this.progress, this.barWidth);
|
|
94
|
+
const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
|
|
95
|
+
return `${spinner} ${this.label} ${bar} ${pct}% ${this.stage}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
writeFallback(line) {
|
|
99
|
+
if (line === this.lastFallbackLine) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.lastFallbackLine = line;
|
|
104
|
+
this.display.stream.write(`${line}\n`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class ProgressDisplay {
|
|
109
|
+
constructor(options = {}) {
|
|
110
|
+
this.stream = options.stream || process.stdout;
|
|
111
|
+
this.enabled = Boolean(this.stream.isTTY);
|
|
112
|
+
this.frameIndex = 0;
|
|
113
|
+
this.timer = null;
|
|
114
|
+
this.items = [];
|
|
115
|
+
this.renderedLineCount = 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
createItem(label, options = {}) {
|
|
119
|
+
const item = new ProgressItem(this, label, options);
|
|
120
|
+
this.items.push(item);
|
|
121
|
+
return item;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
start() {
|
|
125
|
+
if (!this.enabled || this.timer) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.timer = setInterval(() => {
|
|
130
|
+
this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
|
|
131
|
+
this.render();
|
|
132
|
+
}, 120);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
log(message) {
|
|
136
|
+
if (!this.enabled) {
|
|
137
|
+
this.stream.write(`${message}\n`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.clearRender();
|
|
142
|
+
this.stream.write(`${message}\n`);
|
|
143
|
+
this.render();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
writeStaticLine(message) {
|
|
147
|
+
if (!this.enabled) {
|
|
148
|
+
this.stream.write(`${message}\n`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.clearRender();
|
|
153
|
+
this.stream.write(`${message}\n`);
|
|
154
|
+
this.render();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render() {
|
|
158
|
+
if (!this.enabled) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const activeItems = this.items.filter((item) => item.active);
|
|
163
|
+
this.clearRender();
|
|
164
|
+
|
|
165
|
+
if (activeItems.length === 0) {
|
|
166
|
+
this.renderedLineCount = 0;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const lines = activeItems.map((item) => item.renderLine(this.frameIndex));
|
|
171
|
+
this.stream.write(lines.join("\n"));
|
|
172
|
+
this.renderedLineCount = lines.length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
clearRender() {
|
|
176
|
+
if (!this.enabled || this.renderedLineCount === 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
readline.moveCursor(this.stream, 0, -Math.max(this.renderedLineCount - 1, 0));
|
|
181
|
+
|
|
182
|
+
for (let index = 0; index < this.renderedLineCount; index += 1) {
|
|
183
|
+
readline.clearLine(this.stream, 0);
|
|
184
|
+
readline.cursorTo(this.stream, 0);
|
|
185
|
+
|
|
186
|
+
if (index < this.renderedLineCount - 1) {
|
|
187
|
+
readline.moveCursor(this.stream, 0, 1);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
readline.moveCursor(this.stream, 0, -Math.max(this.renderedLineCount - 1, 0));
|
|
192
|
+
readline.cursorTo(this.stream, 0);
|
|
193
|
+
this.renderedLineCount = 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
stopIfIdle() {
|
|
197
|
+
if (this.items.some((item) => item.active)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.timer) {
|
|
202
|
+
clearInterval(this.timer);
|
|
203
|
+
this.timer = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/review-runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { ProgressDisplay } from "./progress-ui.js";
|
|
4
5
|
import { runCommand } from "./shell.js";
|
|
5
6
|
import { resolveRepositoryContext } from "./vcs-client.js";
|
|
6
7
|
|
|
@@ -414,32 +415,22 @@ async function runReviewerPrompt(config, backend, targetInfo, details, diffText)
|
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
function readLastReviewedId(state, backend, targetInfo) {
|
|
417
|
-
if (state.vcs && state.vcs !== backend.kind) {
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (state.targetKey && state.targetKey !== targetInfo.stateKey) {
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
418
|
return backend.fromStateValue(state);
|
|
426
419
|
}
|
|
427
420
|
|
|
428
421
|
function buildStateSnapshot(backend, targetInfo, changeId) {
|
|
429
|
-
|
|
430
|
-
vcs: backend.kind,
|
|
431
|
-
targetKey: targetInfo.stateKey,
|
|
422
|
+
return {
|
|
432
423
|
lastReviewedId: backend.toStateValue(changeId),
|
|
433
424
|
updatedAt: new Date().toISOString()
|
|
434
425
|
};
|
|
435
|
-
|
|
436
|
-
return backend.extendState(state, changeId);
|
|
437
426
|
}
|
|
438
427
|
|
|
439
|
-
async function reviewChange(config, backend, targetInfo, changeId) {
|
|
428
|
+
async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
429
|
+
progress?.update(0.05, "loading change details");
|
|
440
430
|
const details = await backend.getChangeDetails(config, targetInfo, changeId);
|
|
441
431
|
|
|
442
432
|
if (details.changedPaths.length === 0) {
|
|
433
|
+
progress?.update(0.7, "writing skipped report");
|
|
443
434
|
const skippedReport = [
|
|
444
435
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
445
436
|
"",
|
|
@@ -471,6 +462,7 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
471
462
|
};
|
|
472
463
|
}
|
|
473
464
|
|
|
465
|
+
progress?.update(0.2, "loading diff");
|
|
474
466
|
const diffText = await backend.getChangeDiff(config, targetInfo, changeId);
|
|
475
467
|
const reviewersToTry = [config.reviewer, ...(config.fallbackReviewers || [])];
|
|
476
468
|
|
|
@@ -482,6 +474,7 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
482
474
|
for (const reviewerName of reviewersToTry) {
|
|
483
475
|
currentReviewerConfig = { ...config, reviewer: reviewerName };
|
|
484
476
|
debugLog(config, `Trying reviewer: ${reviewerName}`);
|
|
477
|
+
progress?.update(0.45, `running reviewer ${reviewerName}`);
|
|
485
478
|
|
|
486
479
|
const res = await runReviewerPrompt(
|
|
487
480
|
currentReviewerConfig,
|
|
@@ -499,10 +492,11 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
499
492
|
}
|
|
500
493
|
|
|
501
494
|
if (reviewerName !== reviewersToTry[reviewersToTry.length - 1]) {
|
|
502
|
-
|
|
495
|
+
progress?.log(`${reviewer.displayName} failed for ${details.displayId}; trying next reviewer...`);
|
|
503
496
|
}
|
|
504
497
|
}
|
|
505
498
|
|
|
499
|
+
progress?.update(0.82, "writing report");
|
|
506
500
|
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult);
|
|
507
501
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
508
502
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
@@ -538,6 +532,15 @@ function formatChangeList(backend, changeIds) {
|
|
|
538
532
|
return changeIds.map((changeId) => backend.formatChangeId(changeId)).join(", ");
|
|
539
533
|
}
|
|
540
534
|
|
|
535
|
+
function updateOverallProgress(progress, completedCount, totalCount, currentFraction, stage) {
|
|
536
|
+
if (!progress || totalCount <= 0) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const overallFraction = (completedCount + currentFraction) / totalCount;
|
|
541
|
+
progress.update(overallFraction, `${completedCount}/${totalCount} completed${stage ? ` | ${stage}` : ""}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
541
544
|
export async function runReviewCycle(config) {
|
|
542
545
|
await ensureDir(config.outputDir);
|
|
543
546
|
|
|
@@ -588,21 +591,52 @@ export async function runReviewCycle(config) {
|
|
|
588
591
|
}
|
|
589
592
|
|
|
590
593
|
console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
|
|
594
|
+
const progressDisplay = new ProgressDisplay();
|
|
595
|
+
const overallProgress = progressDisplay.createItem(
|
|
596
|
+
`${backend.displayName} ${backend.changeName} batch`,
|
|
597
|
+
{ barWidth: 30 }
|
|
598
|
+
);
|
|
599
|
+
overallProgress.start("0/" + changeIdsToReview.length + " completed");
|
|
591
600
|
|
|
592
|
-
for (const changeId of changeIdsToReview) {
|
|
601
|
+
for (const [index, changeId] of changeIdsToReview.entries()) {
|
|
593
602
|
debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
594
|
-
const
|
|
603
|
+
const displayId = backend.formatChangeId(changeId);
|
|
604
|
+
const progress = progressDisplay.createItem(
|
|
605
|
+
`${backend.displayName} ${backend.changeName} ${displayId} (${index + 1}/${changeIdsToReview.length})`
|
|
606
|
+
);
|
|
607
|
+
progress.start("queued");
|
|
608
|
+
updateOverallProgress(overallProgress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
|
|
609
|
+
|
|
610
|
+
const syncOverallProgress = (fraction, stage) => {
|
|
611
|
+
progress.update(fraction, stage);
|
|
612
|
+
updateOverallProgress(overallProgress, index, changeIdsToReview.length, fraction, `${displayId} | ${stage}`);
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
let result;
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
result = await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress, log: (message) => progress.log(message) });
|
|
619
|
+
syncOverallProgress(0.94, "saving checkpoint");
|
|
620
|
+
} catch (error) {
|
|
621
|
+
progress.fail(`failed at ${displayId}`);
|
|
622
|
+
overallProgress.fail(`failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
|
|
595
626
|
const outputLabels = [
|
|
596
627
|
result.outputFile ? `md: ${result.outputFile}` : null,
|
|
597
628
|
result.jsonOutputFile ? `json: ${result.jsonOutputFile}` : null
|
|
598
629
|
].filter(Boolean);
|
|
599
|
-
console.log(`Reviewed ${backend.formatChangeId(changeId)}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
600
630
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
601
631
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
602
632
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
603
633
|
debugLog(config, `Saved checkpoint for ${backend.formatChangeId(changeId)} to ${config.stateFilePath}.`);
|
|
634
|
+
progress.succeed(`reviewed ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
635
|
+
updateOverallProgress(overallProgress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
|
|
604
636
|
}
|
|
605
637
|
|
|
638
|
+
overallProgress.succeed(`completed ${changeIdsToReview.length}/${changeIdsToReview.length}`);
|
|
639
|
+
|
|
606
640
|
const remainingChanges = await backend.getPendingChangeIds(
|
|
607
641
|
config,
|
|
608
642
|
targetInfo,
|
package/src/vcs-client.js
CHANGED
|
@@ -67,21 +67,8 @@ function createSvnBackend() {
|
|
|
67
67
|
return Number(revision);
|
|
68
68
|
},
|
|
69
69
|
fromStateValue(state) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (Number.isInteger(state.lastReviewedRevision)) {
|
|
75
|
-
return state.lastReviewedRevision;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return null;
|
|
79
|
-
},
|
|
80
|
-
extendState(state, revision) {
|
|
81
|
-
return {
|
|
82
|
-
...state,
|
|
83
|
-
lastReviewedRevision: Number(revision)
|
|
84
|
-
};
|
|
70
|
+
const id = state.lastReviewedId;
|
|
71
|
+
return Number.isInteger(id) ? id : null;
|
|
85
72
|
}
|
|
86
73
|
};
|
|
87
74
|
}
|
|
@@ -137,21 +124,8 @@ function createGitBackend() {
|
|
|
137
124
|
return String(commitHash);
|
|
138
125
|
},
|
|
139
126
|
fromStateValue(state) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (typeof state.lastReviewedCommit === "string" && state.lastReviewedCommit) {
|
|
145
|
-
return state.lastReviewedCommit;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
},
|
|
150
|
-
extendState(state, commitHash) {
|
|
151
|
-
return {
|
|
152
|
-
...state,
|
|
153
|
-
lastReviewedCommit: String(commitHash)
|
|
154
|
-
};
|
|
127
|
+
const id = state.lastReviewedId;
|
|
128
|
+
return typeof id === "string" && id ? id : null;
|
|
155
129
|
}
|
|
156
130
|
};
|
|
157
131
|
}
|