kodevu 0.1.16 → 0.1.18

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.16",
3
+ "version": "0.1.18",
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": {
@@ -1,7 +1,7 @@
1
1
  import readline from "node:readline";
2
2
 
3
3
  const SPINNER_FRAMES = ["|", "/", "-", "\\"];
4
- const DEFAULT_BAR_WIDTH = 24;
4
+ const ELLIPSIS = "...";
5
5
 
6
6
  function clampProgress(value) {
7
7
  if (!Number.isFinite(value)) {
@@ -11,19 +11,45 @@ function clampProgress(value) {
11
11
  return Math.max(0, Math.min(1, value));
12
12
  }
13
13
 
14
- function buildBar(progress, width) {
15
- const safeProgress = clampProgress(progress);
16
- const filled = Math.max(Math.round(safeProgress * width), safeProgress > 0 ? 1 : 0);
14
+ function getCharacterWidth(character) {
15
+ const codePoint = character.codePointAt(0);
17
16
 
18
- if (filled >= width) {
19
- return `[${"=".repeat(width)}]`;
17
+ if (!codePoint || codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
18
+ return 0;
19
+ }
20
+
21
+ if (
22
+ codePoint >= 0x1100 &&
23
+ (
24
+ codePoint <= 0x115f ||
25
+ codePoint === 0x2329 ||
26
+ codePoint === 0x232a ||
27
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
28
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
29
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
30
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
31
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
32
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
33
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
34
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
35
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
36
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd)
37
+ )
38
+ ) {
39
+ return 2;
20
40
  }
21
41
 
22
- if (filled === 0) {
23
- return `[${" ".repeat(width)}]`;
42
+ return 1;
43
+ }
44
+
45
+ function getDisplayWidth(text) {
46
+ let width = 0;
47
+
48
+ for (const character of text) {
49
+ width += getCharacterWidth(character);
24
50
  }
25
51
 
26
- return `[${"=".repeat(Math.max(filled - 1, 0))}>${" ".repeat(width - filled)}]`;
52
+ return width;
27
53
  }
28
54
 
29
55
  function truncateLine(line, maxWidth) {
@@ -31,7 +57,7 @@ function truncateLine(line, maxWidth) {
31
57
  return "";
32
58
  }
33
59
 
34
- if (line.length <= maxWidth) {
60
+ if (getDisplayWidth(line) <= maxWidth) {
35
61
  return line;
36
62
  }
37
63
 
@@ -39,14 +65,28 @@ function truncateLine(line, maxWidth) {
39
65
  return ".".repeat(maxWidth);
40
66
  }
41
67
 
42
- return `${line.slice(0, maxWidth - 3)}...`;
68
+ const targetWidth = maxWidth - getDisplayWidth(ELLIPSIS);
69
+ let result = "";
70
+ let width = 0;
71
+
72
+ for (const character of line) {
73
+ const nextWidth = width + getCharacterWidth(character);
74
+
75
+ if (nextWidth > targetWidth) {
76
+ break;
77
+ }
78
+
79
+ result += character;
80
+ width = nextWidth;
81
+ }
82
+
83
+ return `${result}${ELLIPSIS}`;
43
84
  }
44
85
 
45
86
  class ProgressItem {
46
- constructor(display, label, options = {}) {
87
+ constructor(display, label) {
47
88
  this.display = display;
48
89
  this.label = label;
49
- this.barWidth = options.barWidth || DEFAULT_BAR_WIDTH;
50
90
  this.progress = 0;
51
91
  this.stage = "";
52
92
  this.active = false;
@@ -54,8 +94,8 @@ class ProgressItem {
54
94
  }
55
95
 
56
96
  start(stage = "starting") {
57
- this.stage = stage;
58
97
  this.active = true;
98
+ this.stage = stage;
59
99
 
60
100
  if (!this.display.enabled) {
61
101
  this.writeFallback(`... ${this.label}: ${stage}`);
@@ -63,7 +103,7 @@ class ProgressItem {
63
103
  }
64
104
 
65
105
  this.display.start();
66
- this.display.render();
106
+ this.display.activate(this);
67
107
  }
68
108
 
69
109
  update(progress, stage) {
@@ -78,7 +118,7 @@ class ProgressItem {
78
118
  return;
79
119
  }
80
120
 
81
- this.display.render();
121
+ this.display.activate(this);
82
122
  }
83
123
 
84
124
  log(message) {
@@ -96,46 +136,23 @@ class ProgressItem {
96
136
  finish(prefix, progress, message) {
97
137
  this.progress = clampProgress(progress);
98
138
  this.active = false;
99
- this.display.stopIfIdle();
100
- this.display.writeStaticLine(this.buildLine(prefix, message));
139
+ this.display.deactivate(this);
140
+ this.display.writeStaticLine(this.buildFinalLine(prefix, message));
101
141
  }
102
142
 
103
143
  renderLine(frameIndex) {
104
- const spinner = SPINNER_FRAMES[frameIndex];
105
- return this.buildLine(spinner, this.stage);
144
+ return this.buildStatusLine(SPINNER_FRAMES[frameIndex], this.stage);
106
145
  }
107
146
 
108
- buildLine(prefix, suffix) {
147
+ buildFinalLine(prefix, message) {
148
+ return this.buildStatusLine(prefix, message);
149
+ }
150
+
151
+ buildStatusLine(prefix, suffix) {
109
152
  const availableWidth = this.display.getAvailableWidth();
110
153
  const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
111
- const parts = [prefix, this.label];
112
- const suffixText = suffix ? ` ${suffix}` : "";
113
-
114
- if (availableWidth < 10) {
115
- return truncateLine(`${prefix} ${pct}%`, availableWidth);
116
- }
117
-
118
- const fullReservedWidth = prefix.length + this.label.length + pct.length + suffixText.length + 4;
119
- const fullBarWidth = availableWidth - fullReservedWidth;
120
-
121
- if (fullBarWidth >= 4) {
122
- return truncateLine(
123
- `${parts.join(" ")} ${buildBar(this.progress, Math.min(this.barWidth, fullBarWidth))} ${pct}%${suffixText}`,
124
- availableWidth
125
- );
126
- }
127
-
128
- const compactReservedWidth = prefix.length + pct.length + suffixText.length + 4;
129
- const compactBarWidth = availableWidth - compactReservedWidth;
130
-
131
- if (compactBarWidth >= 4) {
132
- return truncateLine(
133
- `${prefix} ${buildBar(this.progress, compactBarWidth)} ${pct}%${suffixText}`,
134
- availableWidth
135
- );
136
- }
137
-
138
- return truncateLine(`${prefix} ${pct}%${suffixText}`, availableWidth);
154
+ const suffixText = suffix ? ` | ${suffix}` : "";
155
+ return truncateLine(`${prefix} ${pct}% ${this.label}${suffixText}`, availableWidth);
139
156
  }
140
157
 
141
158
  writeFallback(line) {
@@ -155,12 +172,15 @@ export class ProgressDisplay {
155
172
  this.frameIndex = 0;
156
173
  this.timer = null;
157
174
  this.items = [];
158
- this.renderedLineCount = 0;
175
+ this.currentItem = null;
176
+ this.statusVisible = false;
177
+ this.lastStatusWidth = 0;
178
+ this.resizeAttached = false;
159
179
  this.handleResize = this.handleResize.bind(this);
160
180
  }
161
181
 
162
- createItem(label, options = {}) {
163
- const item = new ProgressItem(this, label, options);
182
+ createItem(label) {
183
+ const item = new ProgressItem(this, label);
164
184
  this.items.push(item);
165
185
  return item;
166
186
  }
@@ -171,21 +191,38 @@ export class ProgressDisplay {
171
191
  }
172
192
 
173
193
  this.attachResizeHandler();
174
-
175
194
  this.timer = setInterval(() => {
176
195
  this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
177
196
  this.render();
178
197
  }, 120);
179
198
  }
180
199
 
200
+ activate(item) {
201
+ this.currentItem = item;
202
+ this.render();
203
+ }
204
+
205
+ deactivate(item) {
206
+ if (this.currentItem === item) {
207
+ this.currentItem = this.items.findLast((candidate) => candidate.active) || null;
208
+ }
209
+
210
+ this.stopIfIdle();
211
+ this.clearStatusLine();
212
+
213
+ if (this.currentItem) {
214
+ this.render();
215
+ }
216
+ }
217
+
181
218
  log(message) {
182
219
  if (!this.enabled) {
183
220
  this.stream.write(`${message}\n`);
184
221
  return;
185
222
  }
186
223
 
187
- this.clearRender();
188
- this.stream.write(`${message}\n`);
224
+ this.clearStatusLine();
225
+ this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
189
226
  this.render();
190
227
  }
191
228
 
@@ -195,48 +232,45 @@ export class ProgressDisplay {
195
232
  return;
196
233
  }
197
234
 
198
- this.clearRender();
235
+ this.clearStatusLine();
199
236
  this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
200
237
  this.render();
201
238
  }
202
239
 
203
240
  render() {
204
- if (!this.enabled) {
241
+ if (!this.enabled || !this.currentItem?.active) {
205
242
  return;
206
243
  }
207
244
 
208
- const activeItems = this.items.filter((item) => item.active);
209
- this.clearRender();
210
-
211
- if (activeItems.length === 0) {
212
- this.renderedLineCount = 0;
213
- return;
214
- }
215
-
216
- const lines = activeItems.map((item) => item.renderLine(this.frameIndex));
217
- this.stream.write(lines.join("\n"));
218
- this.renderedLineCount = lines.length;
245
+ this.clearStatusLine();
246
+ const line = this.currentItem.renderLine(this.frameIndex);
247
+ this.stream.write(line);
248
+ this.statusVisible = true;
249
+ this.lastStatusWidth = getDisplayWidth(line);
219
250
  }
220
251
 
221
- clearRender() {
222
- if (!this.enabled || this.renderedLineCount === 0) {
252
+ clearStatusLine() {
253
+ if (!this.enabled || !this.statusVisible) {
223
254
  return;
224
255
  }
225
256
 
226
- readline.moveCursor(this.stream, 0, -Math.max(this.renderedLineCount - 1, 0));
257
+ const rows = Math.max(1, Math.ceil(this.lastStatusWidth / Math.max(this.stream.columns || 1, 1)));
258
+
259
+ readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
227
260
 
228
- for (let index = 0; index < this.renderedLineCount; index += 1) {
261
+ for (let index = 0; index < rows; index += 1) {
229
262
  readline.clearLine(this.stream, 0);
230
263
  readline.cursorTo(this.stream, 0);
231
264
 
232
- if (index < this.renderedLineCount - 1) {
265
+ if (index < rows - 1) {
233
266
  readline.moveCursor(this.stream, 0, 1);
234
267
  }
235
268
  }
236
269
 
237
- readline.moveCursor(this.stream, 0, -Math.max(this.renderedLineCount - 1, 0));
270
+ readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
238
271
  readline.cursorTo(this.stream, 0);
239
- this.renderedLineCount = 0;
272
+ this.statusVisible = false;
273
+ this.lastStatusWidth = 0;
240
274
  }
241
275
 
242
276
  stopIfIdle() {
@@ -592,24 +592,16 @@ export async function runReviewCycle(config) {
592
592
 
593
593
  console.log(`Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`);
594
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");
595
+ const progress = progressDisplay.createItem(`${backend.displayName} ${backend.changeName} batch`);
596
+ progress.start("0/" + changeIdsToReview.length + " completed");
600
597
 
601
598
  for (const [index, changeId] of changeIdsToReview.entries()) {
602
599
  debugLog(config, `Starting review for ${backend.formatChangeId(changeId)}.`);
603
600
  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}`);
601
+ updateOverallProgress(progress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
609
602
 
610
603
  const syncOverallProgress = (fraction, stage) => {
611
- progress.update(fraction, stage);
612
- updateOverallProgress(overallProgress, index, changeIdsToReview.length, fraction, `${displayId} | ${stage}`);
604
+ updateOverallProgress(progress, index, changeIdsToReview.length, fraction, `${displayId} | ${stage}`);
613
605
  };
614
606
 
615
607
  let result;
@@ -618,8 +610,7 @@ export async function runReviewCycle(config) {
618
610
  result = await reviewChange(config, backend, targetInfo, changeId, { update: syncOverallProgress, log: (message) => progress.log(message) });
619
611
  syncOverallProgress(0.94, "saving checkpoint");
620
612
  } catch (error) {
621
- progress.fail(`failed at ${displayId}`);
622
- overallProgress.fail(`failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
613
+ progress.fail(`failed at ${displayId} (${index}/${changeIdsToReview.length} completed)`);
623
614
  throw error;
624
615
  }
625
616
 
@@ -631,11 +622,11 @@ export async function runReviewCycle(config) {
631
622
  await saveState(config.stateFilePath, updateProjectState(stateFile, targetInfo, nextProjectState));
632
623
  stateFile.projects[targetInfo.stateKey] = nextProjectState;
633
624
  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}`);
625
+ progress.log(`[done] reviewed ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
626
+ updateOverallProgress(progress, index + 1, changeIdsToReview.length, 0, `finished ${displayId}`);
636
627
  }
637
628
 
638
- overallProgress.succeed(`completed ${changeIdsToReview.length}/${changeIdsToReview.length}`);
629
+ progress.succeed(`completed ${changeIdsToReview.length}/${changeIdsToReview.length}`);
639
630
 
640
631
  const remainingChanges = await backend.getPendingChangeIds(
641
632
  config,