kodevu 0.1.13 → 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 +51 -5
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
|
|
|
@@ -424,10 +425,12 @@ function buildStateSnapshot(backend, targetInfo, changeId) {
|
|
|
424
425
|
};
|
|
425
426
|
}
|
|
426
427
|
|
|
427
|
-
async function reviewChange(config, backend, targetInfo, changeId) {
|
|
428
|
+
async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
429
|
+
progress?.update(0.05, "loading change details");
|
|
428
430
|
const details = await backend.getChangeDetails(config, targetInfo, changeId);
|
|
429
431
|
|
|
430
432
|
if (details.changedPaths.length === 0) {
|
|
433
|
+
progress?.update(0.7, "writing skipped report");
|
|
431
434
|
const skippedReport = [
|
|
432
435
|
`# ${backend.displayName} Review Report: ${details.displayId}`,
|
|
433
436
|
"",
|
|
@@ -459,6 +462,7 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
459
462
|
};
|
|
460
463
|
}
|
|
461
464
|
|
|
465
|
+
progress?.update(0.2, "loading diff");
|
|
462
466
|
const diffText = await backend.getChangeDiff(config, targetInfo, changeId);
|
|
463
467
|
const reviewersToTry = [config.reviewer, ...(config.fallbackReviewers || [])];
|
|
464
468
|
|
|
@@ -470,6 +474,7 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
470
474
|
for (const reviewerName of reviewersToTry) {
|
|
471
475
|
currentReviewerConfig = { ...config, reviewer: reviewerName };
|
|
472
476
|
debugLog(config, `Trying reviewer: ${reviewerName}`);
|
|
477
|
+
progress?.update(0.45, `running reviewer ${reviewerName}`);
|
|
473
478
|
|
|
474
479
|
const res = await runReviewerPrompt(
|
|
475
480
|
currentReviewerConfig,
|
|
@@ -487,10 +492,11 @@ async function reviewChange(config, backend, targetInfo, changeId) {
|
|
|
487
492
|
}
|
|
488
493
|
|
|
489
494
|
if (reviewerName !== reviewersToTry[reviewersToTry.length - 1]) {
|
|
490
|
-
|
|
495
|
+
progress?.log(`${reviewer.displayName} failed for ${details.displayId}; trying next reviewer...`);
|
|
491
496
|
}
|
|
492
497
|
}
|
|
493
498
|
|
|
499
|
+
progress?.update(0.82, "writing report");
|
|
494
500
|
const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult);
|
|
495
501
|
const outputFile = path.join(config.outputDir, backend.getReportFileName(changeId));
|
|
496
502
|
const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
|
|
@@ -526,6 +532,15 @@ function formatChangeList(backend, changeIds) {
|
|
|
526
532
|
return changeIds.map((changeId) => backend.formatChangeId(changeId)).join(", ");
|
|
527
533
|
}
|
|
528
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
|
+
|
|
529
544
|
export async function runReviewCycle(config) {
|
|
530
545
|
await ensureDir(config.outputDir);
|
|
531
546
|
|
|
@@ -576,21 +591,52 @@ export async function runReviewCycle(config) {
|
|
|
576
591
|
}
|
|
577
592
|
|
|
578
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");
|
|
579
600
|
|
|
580
|
-
for (const changeId of changeIdsToReview) {
|
|
601
|
+
for (const [index, changeId] of changeIdsToReview.entries()) {
|
|
581
602
|
debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
|
|
582
|
-
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
|
+
|
|
583
626
|
const outputLabels = [
|
|
584
627
|
result.outputFile ? `md: ${result.outputFile}` : null,
|
|
585
628
|
result.jsonOutputFile ? `json: ${result.jsonOutputFile}` : null
|
|
586
629
|
].filter(Boolean);
|
|
587
|
-
console.log(`Reviewed ${backend.formatChangeId(changeId)}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
|
|
588
630
|
const nextProjectState = buildStateSnapshot(backend, targetInfo, changeId);
|
|
589
631
|
await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
|
|
590
632
|
stateFile.projects[targetInfo.stateKey] = nextProjectState;
|
|
591
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}`);
|
|
592
636
|
}
|
|
593
637
|
|
|
638
|
+
overallProgress.succeed(`completed ${changeIdsToReview.length}/${changeIdsToReview.length}`);
|
|
639
|
+
|
|
594
640
|
const remainingChanges = await backend.getPendingChangeIds(
|
|
595
641
|
config,
|
|
596
642
|
targetInfo,
|