kodevu 0.1.19 → 0.1.21

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
@@ -32,7 +32,7 @@ If you want a config file, run `npx kodevu init` to create `./config.json` in th
32
32
  npx kodevu init
33
33
  ```
34
34
 
35
- This creates `config.json` in the current directory from the packaged `config.example.json`.
35
+ This creates `config.json` in the current directory from built-in defaults.
36
36
  You only need this when you want to override defaults such as `reviewer` or output paths.
37
37
 
38
38
  If you want a different path:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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": {
@@ -8,8 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "src",
11
- "README.md",
12
- "config.example.json"
11
+ "README.md"
13
12
  ],
14
13
  "scripts": {
15
14
  "start": "node src/index.js",
package/src/config.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
- import { constants as fsConstants } from "node:fs";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
4
  import { findCommandOnPath } from "./shell.js";
7
5
 
8
6
  const defaultStorageDir = path.join(os.homedir(), ".kodevu");
@@ -16,10 +14,21 @@ const defaultConfig = {
16
14
  commandTimeoutMs: 600000,
17
15
  prompt:
18
16
  "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
19
- maxRevisionsPerRun: 20,
17
+ maxRevisionsPerRun: 5,
20
18
  outputFormats: ["markdown"]
21
19
  };
22
20
 
21
+ const configTemplate = {
22
+ target: "C:/path/to/your/repository-or-subdirectory",
23
+ reviewer: defaultConfig.reviewer,
24
+ prompt: defaultConfig.prompt,
25
+ outputDir: "~/.kodevu",
26
+ stateFilePath: "~/.kodevu/state.json",
27
+ commandTimeoutMs: defaultConfig.commandTimeoutMs,
28
+ maxRevisionsPerRun: defaultConfig.maxRevisionsPerRun,
29
+ outputFormats: defaultConfig.outputFormats
30
+ };
31
+
23
32
  function resolveConfigPath(baseDir, value) {
24
33
  if (!value) {
25
34
  return value;
@@ -264,12 +273,12 @@ Config highlights:
264
273
 
265
274
  export async function initConfig(targetPath = "config.json") {
266
275
  const absoluteTargetPath = path.resolve(targetPath);
267
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
268
- const templatePath = path.join(packageRoot, "config.example.json");
269
276
 
270
277
  await fs.mkdir(path.dirname(absoluteTargetPath), { recursive: true });
278
+
279
+ const content = JSON.stringify(configTemplate, null, 2) + "\n";
271
280
  try {
272
- await fs.copyFile(templatePath, absoluteTargetPath, fsConstants.COPYFILE_EXCL);
281
+ await fs.writeFile(absoluteTargetPath, content, { flag: "wx" });
273
282
  } catch (error) {
274
283
  if (error?.code === "EEXIST") {
275
284
  throw new Error(`Config file already exists: ${absoluteTargetPath}`);
@@ -1,9 +1,3 @@
1
- import readline from "node:readline";
2
-
3
- const SPINNER_FRAMES = ["|", "/", "-", "\\"];
4
- const ELLIPSIS = "...";
5
- const MIN_DYNAMIC_WIDTH = 60;
6
-
7
1
  function clampProgress(value) {
8
2
  if (!Number.isFinite(value)) {
9
3
  return 0;
@@ -12,78 +6,6 @@ function clampProgress(value) {
12
6
  return Math.max(0, Math.min(1, value));
13
7
  }
14
8
 
15
- function getCharacterWidth(character) {
16
- const codePoint = character.codePointAt(0);
17
-
18
- if (!codePoint || codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
19
- return 0;
20
- }
21
-
22
- if (
23
- codePoint >= 0x1100 &&
24
- (
25
- codePoint <= 0x115f ||
26
- codePoint === 0x2329 ||
27
- codePoint === 0x232a ||
28
- (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
29
- (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
30
- (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
31
- (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
32
- (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
33
- (codePoint >= 0xff00 && codePoint <= 0xff60) ||
34
- (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
35
- (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
36
- (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
37
- (codePoint >= 0x20000 && codePoint <= 0x3fffd)
38
- )
39
- ) {
40
- return 2;
41
- }
42
-
43
- return 1;
44
- }
45
-
46
- function getDisplayWidth(text) {
47
- let width = 0;
48
-
49
- for (const character of text) {
50
- width += getCharacterWidth(character);
51
- }
52
-
53
- return width;
54
- }
55
-
56
- function truncateLine(line, maxWidth) {
57
- if (!Number.isFinite(maxWidth) || maxWidth <= 0) {
58
- return "";
59
- }
60
-
61
- if (getDisplayWidth(line) <= maxWidth) {
62
- return line;
63
- }
64
-
65
- if (maxWidth <= 3) {
66
- return ".".repeat(maxWidth);
67
- }
68
-
69
- const targetWidth = maxWidth - getDisplayWidth(ELLIPSIS);
70
- let result = "";
71
- let width = 0;
72
-
73
- for (const character of line) {
74
- const nextWidth = width + getCharacterWidth(character);
75
-
76
- if (nextWidth > targetWidth) {
77
- break;
78
- }
79
-
80
- result += character;
81
- width = nextWidth;
82
- }
83
-
84
- return `${result}${ELLIPSIS}`;
85
- }
86
-
87
9
  class ProgressItem {
88
10
  constructor(display, label) {
89
11
  this.display = display;
@@ -91,20 +13,13 @@ class ProgressItem {
91
13
  this.progress = 0;
92
14
  this.stage = "";
93
15
  this.active = false;
94
- this.lastFallbackLine = "";
16
+ this.lastStatusLine = "";
95
17
  }
96
18
 
97
19
  start(stage = "starting") {
98
20
  this.active = true;
99
21
  this.stage = stage;
100
-
101
- if (!this.display.canRenderDynamically()) {
102
- this.writeFallback(`... ${this.label}: ${stage}`);
103
- return;
104
- }
105
-
106
- this.display.start();
107
- this.display.activate(this);
22
+ this.writeStatus();
108
23
  }
109
24
 
110
25
  update(progress, stage) {
@@ -114,17 +29,11 @@ class ProgressItem {
114
29
  this.stage = stage;
115
30
  }
116
31
 
117
- if (!this.display.canRenderDynamically()) {
118
- this.writeFallback(`... ${this.label}: ${this.stage}`);
119
- return;
120
- }
121
-
122
- this.display.start();
123
- this.display.activate(this);
32
+ this.writeStatus();
124
33
  }
125
34
 
126
35
  log(message) {
127
- this.display.log(message);
36
+ this.display.writeLine(message);
128
37
  }
129
38
 
130
39
  succeed(message) {
@@ -138,189 +47,36 @@ class ProgressItem {
138
47
  finish(prefix, progress, message) {
139
48
  this.progress = clampProgress(progress);
140
49
  this.active = false;
141
- this.display.deactivate(this);
142
- this.display.writeStaticLine(this.buildFinalLine(prefix, message));
50
+ this.display.writeLine(`${prefix} ${message}`);
143
51
  }
144
52
 
145
- renderLine(frameIndex) {
146
- return this.buildStatusLine(SPINNER_FRAMES[frameIndex], this.stage);
147
- }
53
+ writeStatus() {
54
+ const line = this.buildStatusLine();
148
55
 
149
- buildFinalLine(prefix, message) {
150
- return this.buildStatusLine(prefix, message);
151
- }
152
-
153
- buildStatusLine(prefix, suffix) {
154
- const availableWidth = this.display.getAvailableWidth();
155
- const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
156
- const suffixText = suffix ? ` | ${suffix}` : "";
157
- return truncateLine(`${prefix} ${pct}% ${this.label}${suffixText}`, availableWidth);
158
- }
159
-
160
- writeFallback(line) {
161
- if (line === this.lastFallbackLine) {
56
+ if (line === this.lastStatusLine) {
162
57
  return;
163
58
  }
164
59
 
165
- this.lastFallbackLine = line;
166
- this.display.stream.write(`${line}\n`);
60
+ this.lastStatusLine = line;
61
+ this.display.writeLine(line);
62
+ }
63
+
64
+ buildStatusLine() {
65
+ const pct = `${Math.round(this.progress * 100)}`.padStart(3, " ");
66
+ return `[progress] ${pct}% ${this.label}${this.stage ? ` | ${this.stage}` : ""}`;
167
67
  }
168
68
  }
169
69
 
170
70
  export class ProgressDisplay {
171
71
  constructor(options = {}) {
172
72
  this.stream = options.stream || process.stdout;
173
- this.enabled = Boolean(this.stream.isTTY);
174
- this.frameIndex = 0;
175
- this.timer = null;
176
- this.items = [];
177
- this.currentItem = null;
178
- this.statusVisible = false;
179
- this.lastStatusWidth = 0;
180
- this.resizeAttached = false;
181
- this.handleResize = this.handleResize.bind(this);
182
73
  }
183
74
 
184
75
  createItem(label) {
185
- const item = new ProgressItem(this, label);
186
- this.items.push(item);
187
- return item;
188
- }
189
-
190
- start() {
191
- if (!this.enabled || this.timer) {
192
- return;
193
- }
194
-
195
- this.attachResizeHandler();
196
- this.timer = setInterval(() => {
197
- this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
198
- this.render();
199
- }, 120);
200
- }
201
-
202
- activate(item) {
203
- this.currentItem = item;
204
- this.render();
205
- }
206
-
207
- deactivate(item) {
208
- if (this.currentItem === item) {
209
- this.currentItem = this.items.findLast((candidate) => candidate.active) || null;
210
- }
211
-
212
- this.stopIfIdle();
213
- this.clearStatusLine();
214
-
215
- if (this.currentItem) {
216
- this.render();
217
- }
218
- }
219
-
220
- log(message) {
221
- if (!this.canRenderDynamically()) {
222
- this.stream.write(`${message}\n`);
223
- return;
224
- }
225
-
226
- this.clearStatusLine();
227
- this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
228
- this.render();
229
- }
230
-
231
- writeStaticLine(message) {
232
- if (!this.canRenderDynamically()) {
233
- this.stream.write(`${message}\n`);
234
- return;
235
- }
236
-
237
- this.clearStatusLine();
238
- this.stream.write(`${truncateLine(message, this.getAvailableWidth())}\n`);
239
- this.render();
240
- }
241
-
242
- render() {
243
- if (!this.canRenderDynamically() || !this.currentItem?.active) {
244
- this.clearStatusLine();
245
- return;
246
- }
247
-
248
- this.clearStatusLine();
249
- const line = this.currentItem.renderLine(this.frameIndex);
250
- this.stream.write(line);
251
- this.statusVisible = true;
252
- this.lastStatusWidth = getDisplayWidth(line);
253
- }
254
-
255
- clearStatusLine() {
256
- if (!this.enabled || !this.statusVisible) {
257
- return;
258
- }
259
-
260
- const rows = Math.max(1, Math.ceil(this.lastStatusWidth / Math.max(this.stream.columns || 1, 1)));
261
-
262
- readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
263
-
264
- for (let index = 0; index < rows; index += 1) {
265
- readline.clearLine(this.stream, 0);
266
- readline.cursorTo(this.stream, 0);
267
-
268
- if (index < rows - 1) {
269
- readline.moveCursor(this.stream, 0, 1);
270
- }
271
- }
272
-
273
- readline.moveCursor(this.stream, 0, -Math.max(rows - 1, 0));
274
- readline.cursorTo(this.stream, 0);
275
- this.statusVisible = false;
276
- this.lastStatusWidth = 0;
277
- }
278
-
279
- stopIfIdle() {
280
- if (this.items.some((item) => item.active)) {
281
- return;
282
- }
283
-
284
- if (this.timer) {
285
- clearInterval(this.timer);
286
- this.timer = null;
287
- }
288
-
289
- this.detachResizeHandler();
290
- }
291
-
292
- getAvailableWidth() {
293
- const columns = this.stream.columns || 80;
294
- return Math.max(columns - 1, 1);
295
- }
296
-
297
- canRenderDynamically() {
298
- return this.enabled && this.getAvailableWidth() >= MIN_DYNAMIC_WIDTH;
76
+ return new ProgressItem(this, label);
299
77
  }
300
78
 
301
- handleResize() {
302
- if (!this.enabled) {
303
- return;
304
- }
305
-
306
- this.render();
307
- }
308
-
309
- attachResizeHandler() {
310
- if (typeof this.stream.on !== "function" || this.resizeAttached) {
311
- return;
312
- }
313
-
314
- this.stream.on("resize", this.handleResize);
315
- this.resizeAttached = true;
316
- }
317
-
318
- detachResizeHandler() {
319
- if (typeof this.stream.off !== "function" || !this.resizeAttached) {
320
- return;
321
- }
322
-
323
- this.stream.off("resize", this.handleResize);
324
- this.resizeAttached = false;
79
+ writeLine(message) {
80
+ this.stream.write(`${message}\n`);
325
81
  }
326
82
  }
@@ -1,12 +0,0 @@
1
- {
2
- "target": "C:/path/to/your/repository-or-subdirectory",
3
- "reviewer": "auto",
4
- "prompt": "请严格审查当前变更,优先指出 bug、回归风险、兼容性问题、安全问题、边界条件缺陷和缺失测试。请使用简体中文输出 Markdown;如果没有明确缺陷,请写“未发现明确缺陷”,并补充剩余风险。",
5
- "outputDir": "~/.kodevu",
6
- "stateFilePath": "~/.kodevu/state.json",
7
- "commandTimeoutMs": 600000,
8
- "maxRevisionsPerRun": 5,
9
- "outputFormats": [
10
- "markdown"
11
- ]
12
- }