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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.13",
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
+ }
@@ -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
- console.log(`${reviewer.displayName} failed for ${details.displayId}; trying next reviewer...`);
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 result = await reviewChange(config, backend, targetInfo, changeId);
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,