kodevu 0.1.61 → 0.1.63
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 +1 -1
- package/src/config.js +30 -23
- package/src/index.js +5 -2
- package/src/review-runner.js +8 -1
- package/src/shell.js +9 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -31,6 +31,13 @@ const defaultConfig = {
|
|
|
31
31
|
openaiProject: ""
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
function mkCliError(message) {
|
|
35
|
+
const e = new Error(message);
|
|
36
|
+
// exit code 2 = usage / configuration errors
|
|
37
|
+
e.exitCode = 2;
|
|
38
|
+
return e;
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
const ENV_MAP = {
|
|
35
42
|
KODEVU_REVIEWER: "reviewer",
|
|
36
43
|
KODEVU_LANG: "lang",
|
|
@@ -65,16 +72,16 @@ async function loadConfigFile(configPath = defaultConfigFilePath) {
|
|
|
65
72
|
content = await fs.readFile(resolvedPath, "utf8");
|
|
66
73
|
} catch (err) {
|
|
67
74
|
if (err.code === "ENOENT") return {};
|
|
68
|
-
throw
|
|
75
|
+
throw mkCliError(`Failed to read config file ${resolvedPath}: ${err.message}`);
|
|
69
76
|
}
|
|
70
77
|
let parsed;
|
|
71
78
|
try {
|
|
72
79
|
parsed = JSON.parse(content);
|
|
73
80
|
} catch (err) {
|
|
74
|
-
throw
|
|
81
|
+
throw mkCliError(`Invalid JSON in config file ${resolvedPath}: ${err.message}`);
|
|
75
82
|
}
|
|
76
83
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
77
|
-
throw
|
|
84
|
+
throw mkCliError(`Config file ${resolvedPath} must contain a JSON object`);
|
|
78
85
|
}
|
|
79
86
|
const result = {};
|
|
80
87
|
for (const [key, value] of Object.entries(parsed)) {
|
|
@@ -93,7 +100,7 @@ function normalizeOutputFormats(outputFormats) {
|
|
|
93
100
|
const invalid = normalized.filter((item) => !supported.includes(item));
|
|
94
101
|
|
|
95
102
|
if (invalid.length > 0) {
|
|
96
|
-
throw
|
|
103
|
+
throw mkCliError(`Unsupported output format(s): ${invalid.join(", ")}. Use: ${supported.join(", ")}`);
|
|
97
104
|
}
|
|
98
105
|
return normalized.length === 0 ? ["markdown"] : normalized;
|
|
99
106
|
}
|
|
@@ -132,7 +139,7 @@ async function resolveAutoReviewers(debug) {
|
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
if (availableReviewers.length === 0) {
|
|
135
|
-
throw
|
|
142
|
+
throw mkCliError(`No reviewer CLI found in PATH. Install one of: ${AUTO_SUPPORTED_REVIEWERS.join(", ")}`);
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
// Shuffle for variety
|
|
@@ -187,28 +194,28 @@ export function parseCliArgs(argv) {
|
|
|
187
194
|
const hasNextValue = nextValue && !nextValue.startsWith("-");
|
|
188
195
|
|
|
189
196
|
if (value === "--reviewer" || value === "-r") {
|
|
190
|
-
if (!hasNextValue) throw
|
|
197
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
191
198
|
args.reviewer = nextValue;
|
|
192
199
|
index += 1;
|
|
193
200
|
continue;
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
if (value === "--prompt" || value === "-p") {
|
|
197
|
-
if (!hasNextValue) throw
|
|
204
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
198
205
|
args.prompt = nextValue;
|
|
199
206
|
index += 1;
|
|
200
207
|
continue;
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
if (value === "--lang" || value === "-l") {
|
|
204
|
-
if (!hasNextValue) throw
|
|
211
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
205
212
|
args.lang = nextValue;
|
|
206
213
|
index += 1;
|
|
207
214
|
continue;
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
if (value === "--rev" || value === "-v") {
|
|
211
|
-
if (!hasNextValue) throw
|
|
218
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
212
219
|
args.rev = nextValue;
|
|
213
220
|
index += 1;
|
|
214
221
|
continue;
|
|
@@ -216,7 +223,7 @@ export function parseCliArgs(argv) {
|
|
|
216
223
|
|
|
217
224
|
if (value === "--last" || value === "-n") {
|
|
218
225
|
const hasLastValue = nextValue !== undefined && /^-?\d+$/.test(nextValue);
|
|
219
|
-
if (!hasLastValue) throw
|
|
226
|
+
if (!hasLastValue) throw mkCliError(`Missing value for ${value}`);
|
|
220
227
|
args.last = nextValue;
|
|
221
228
|
index += 1;
|
|
222
229
|
continue;
|
|
@@ -228,49 +235,49 @@ export function parseCliArgs(argv) {
|
|
|
228
235
|
}
|
|
229
236
|
|
|
230
237
|
if (value === "--output" || value === "-o") {
|
|
231
|
-
if (!hasNextValue) throw
|
|
238
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
232
239
|
args.outputDir = nextValue;
|
|
233
240
|
index += 1;
|
|
234
241
|
continue;
|
|
235
242
|
}
|
|
236
243
|
|
|
237
244
|
if (value === "--format" || value === "-f") {
|
|
238
|
-
if (!hasNextValue) throw
|
|
245
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
239
246
|
args.outputFormats = nextValue;
|
|
240
247
|
index += 1;
|
|
241
248
|
continue;
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
if (value === "--openai-api-key") {
|
|
245
|
-
if (!hasNextValue) throw
|
|
252
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
246
253
|
args.openaiApiKey = nextValue;
|
|
247
254
|
index += 1;
|
|
248
255
|
continue;
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
if (value === "--openai-base-url") {
|
|
252
|
-
if (!hasNextValue) throw
|
|
259
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
253
260
|
args.openaiBaseUrl = nextValue;
|
|
254
261
|
index += 1;
|
|
255
262
|
continue;
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
if (value === "--openai-model") {
|
|
259
|
-
if (!hasNextValue) throw
|
|
266
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
260
267
|
args.openaiModel = nextValue;
|
|
261
268
|
index += 1;
|
|
262
269
|
continue;
|
|
263
270
|
}
|
|
264
271
|
|
|
265
272
|
if (value === "--openai-org") {
|
|
266
|
-
if (!hasNextValue) throw
|
|
273
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
267
274
|
args.openaiOrganization = nextValue;
|
|
268
275
|
index += 1;
|
|
269
276
|
continue;
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
if (value === "--openai-project") {
|
|
273
|
-
if (!hasNextValue) throw
|
|
280
|
+
if (!hasNextValue) throw mkCliError(`Missing value for ${value}`);
|
|
274
281
|
args.openaiProject = nextValue;
|
|
275
282
|
index += 1;
|
|
276
283
|
continue;
|
|
@@ -281,7 +288,7 @@ export function parseCliArgs(argv) {
|
|
|
281
288
|
continue;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
throw
|
|
291
|
+
throw mkCliError(`Unexpected argument: ${value}`);
|
|
285
292
|
}
|
|
286
293
|
|
|
287
294
|
return args;
|
|
@@ -328,11 +335,11 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
328
335
|
}
|
|
329
336
|
|
|
330
337
|
if (cliArgs.rev && cliArgs.last) {
|
|
331
|
-
throw
|
|
338
|
+
throw mkCliError("Parameters --rev and --last are mutually exclusive. Please specify only one.");
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
if (cliArgs.uncommitted && (cliArgs.rev || cliArgs.last)) {
|
|
335
|
-
throw
|
|
342
|
+
throw mkCliError("Parameter --uncommitted is mutually exclusive with --rev and --last.");
|
|
336
343
|
}
|
|
337
344
|
|
|
338
345
|
if (!config.target) {
|
|
@@ -351,7 +358,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
351
358
|
try {
|
|
352
359
|
config.prompt = await fs.readFile(promptPath, "utf8");
|
|
353
360
|
} catch (err) {
|
|
354
|
-
throw
|
|
361
|
+
throw mkCliError(`Failed to read prompt file: ${promptPath} (${err.message})`);
|
|
355
362
|
}
|
|
356
363
|
}
|
|
357
364
|
|
|
@@ -363,7 +370,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
363
370
|
config.fallbackReviewers = availableReviewers.map(r => r.reviewerName).slice(1);
|
|
364
371
|
config.reviewerWasAutoSelected = true;
|
|
365
372
|
} else if (!SUPPORTED_REVIEWERS.includes(config.reviewer)) {
|
|
366
|
-
throw
|
|
373
|
+
throw mkCliError(`"reviewer" must be one of: ${SUPPORTED_REVIEWERS.join(", ")}, or "auto"`);
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
config.outputDir = resolvePath(config.outputDir);
|
|
@@ -384,7 +391,7 @@ export async function resolveConfig(cliArgs = {}) {
|
|
|
384
391
|
}
|
|
385
392
|
|
|
386
393
|
if (config.reviewer === "openai" && !config.openaiApiKey) {
|
|
387
|
-
throw
|
|
394
|
+
throw mkCliError('Reviewer "openai" requires an API key. Set KODEVU_OPENAI_API_KEY or pass --openai-api-key.');
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
return config;
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,9 @@ try {
|
|
|
11
11
|
} catch (error) {
|
|
12
12
|
console.error(error?.message || String(error));
|
|
13
13
|
printHelp();
|
|
14
|
-
|
|
14
|
+
// CLI/usage errors -> exit code 2
|
|
15
|
+
const code = Number(error?.exitCode) || 2;
|
|
16
|
+
process.exit(code);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
if (cliArgs.help) {
|
|
@@ -81,6 +83,7 @@ try {
|
|
|
81
83
|
scope: "session",
|
|
82
84
|
console: true
|
|
83
85
|
});
|
|
84
|
-
process
|
|
86
|
+
// Map any attached exitCode (from thrown errors) to process exitCode, fallback to 1
|
|
87
|
+
process.exitCode = Number(error?.exitCode) || 1;
|
|
85
88
|
}
|
|
86
89
|
process.exit(process.exitCode || 0);
|
package/src/review-runner.js
CHANGED
|
@@ -137,7 +137,14 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
|
|
|
137
137
|
reasonParts.push(`\n${detail}`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
const err = new Error(reasonParts.join(" "));
|
|
141
|
+
// Prefer reviewer-provided exit code when available (HTTP status or child exit code), otherwise 3
|
|
142
|
+
try {
|
|
143
|
+
err.exitCode = reviewerResult?.code || 3;
|
|
144
|
+
} catch {
|
|
145
|
+
err.exitCode = 3;
|
|
146
|
+
}
|
|
147
|
+
throw err;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
progress?.update(0.82, "writing report");
|
package/src/shell.js
CHANGED
|
@@ -65,6 +65,13 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
65
65
|
args,
|
|
66
66
|
cwd: cwd || ""
|
|
67
67
|
});
|
|
68
|
+
// Mark spawn/runtime errors as runtime failures (exit code 3)
|
|
69
|
+
try {
|
|
70
|
+
if (!err || typeof err !== "object") err = new Error(String(err));
|
|
71
|
+
if (err.exitCode == null) err.exitCode = 3;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
68
75
|
reject(err);
|
|
69
76
|
});
|
|
70
77
|
|
|
@@ -108,6 +115,8 @@ export async function runCommand(command, args = [], options = {}) {
|
|
|
108
115
|
`Command failed: ${command} ${args.join(" ")}\n${result.stderr || result.stdout}`.trim()
|
|
109
116
|
);
|
|
110
117
|
error.result = result;
|
|
118
|
+
// Map command failures to exit code 3 (runtime / external command failure)
|
|
119
|
+
error.exitCode = 3;
|
|
111
120
|
reject(error);
|
|
112
121
|
return;
|
|
113
122
|
}
|