smart-review 1.0.3 → 1.0.4
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.en-US.md +4 -0
- package/README.md +4 -1
- package/bin/install.js +181 -14
- package/bin/review.js +425 -251
- package/lib/ai-client-pool.js +36 -7
- package/lib/ai-client.js +91 -2
- package/lib/reviewer.js +1552 -1532
- package/lib/utils/i18n.js +7 -1
- package/package.json +1 -1
package/README.en-US.md
CHANGED
|
@@ -124,6 +124,10 @@ Add to `package.json` if you use Husky:
|
|
|
124
124
|
}
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
#### Interrupt & Terminal Compatibility
|
|
128
|
+
- Works in Git Bash, CMD, and PowerShell
|
|
129
|
+
- Press `q` or `Esc` during review to interrupt and print completed results
|
|
130
|
+
- Interruptions do not fail the review; only blocking risks stop the commit
|
|
127
131
|
## ⚙️ Config
|
|
128
132
|
|
|
129
133
|
Main config `.smart-review/smart-review.json` example:
|
package/README.md
CHANGED
|
@@ -130,7 +130,10 @@ node bin/review.js --files test/src/large-test-file.js
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
```
|
|
133
|
-
|
|
133
|
+
#### 中断与终端兼容
|
|
134
|
+
- 支持在 Git Bash、CMD、PowerShell 中进行交互中断
|
|
135
|
+
- 审查过程中输入 `q` 或按 `Esc` 可中断审查并输出已完成结果
|
|
136
|
+
- 中断不会被视为审查失败,只有存在阻断风险才会阻止提交
|
|
134
137
|
## ⚙️ 配置文件
|
|
135
138
|
|
|
136
139
|
### 主配置文件 `.smart-review/smart-review.json`
|
package/bin/install.js
CHANGED
|
@@ -264,6 +264,8 @@ class Installer {
|
|
|
264
264
|
|
|
265
265
|
echo "${t(loc, 'hook_start_review')}"
|
|
266
266
|
|
|
267
|
+
trap 'echo "${t(loc, 'interrupt_cancelled')}"; exit 0' INT TERM
|
|
268
|
+
|
|
267
269
|
# 获取暂存区文件
|
|
268
270
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
269
271
|
|
|
@@ -283,9 +285,14 @@ ROOT_BIN="$REPO_ROOT/node_modules/.bin/smart-review"
|
|
|
283
285
|
|
|
284
286
|
FOUND_CMD=""
|
|
285
287
|
FOUND_IS_ENTRY=0
|
|
288
|
+
FOUND_ENTRY_CMD=""
|
|
286
289
|
|
|
287
290
|
if [ -f "$ROOT_BIN" ]; then
|
|
288
291
|
FOUND_CMD="$ROOT_BIN"
|
|
292
|
+
ROOT_ENTRY="$REPO_ROOT/node_modules/smart-review/bin/review.js"
|
|
293
|
+
if [ -f "$ROOT_ENTRY" ]; then
|
|
294
|
+
FOUND_ENTRY_CMD="$ROOT_ENTRY"
|
|
295
|
+
fi
|
|
289
296
|
else
|
|
290
297
|
MAX_ASCEND=6
|
|
291
298
|
while IFS= read -r file; do
|
|
@@ -296,9 +303,13 @@ else
|
|
|
296
303
|
candidate_bin="$REPO_ROOT/$dir/node_modules/.bin/smart-review"
|
|
297
304
|
candidate_entry="$REPO_ROOT/$dir/node_modules/smart-review/bin/review.js"
|
|
298
305
|
if [ -f "$candidate_bin" ]; then
|
|
299
|
-
FOUND_CMD="$candidate_bin"; FOUND_IS_ENTRY=0;
|
|
306
|
+
FOUND_CMD="$candidate_bin"; FOUND_IS_ENTRY=0;
|
|
307
|
+
if [ -f "$candidate_entry" ]; then
|
|
308
|
+
FOUND_ENTRY_CMD="$candidate_entry"
|
|
309
|
+
fi
|
|
310
|
+
break 2
|
|
300
311
|
elif [ -f "$candidate_entry" ]; then
|
|
301
|
-
FOUND_CMD="$candidate_entry"; FOUND_IS_ENTRY=1; break 2
|
|
312
|
+
FOUND_CMD="$candidate_entry"; FOUND_IS_ENTRY=1; FOUND_ENTRY_CMD="$candidate_entry"; break 2
|
|
302
313
|
fi
|
|
303
314
|
dir=$(dirname "$dir")
|
|
304
315
|
depth=$((depth + 1))
|
|
@@ -319,13 +330,89 @@ if [ -z "$FOUND_CMD" ]; then
|
|
|
319
330
|
fi
|
|
320
331
|
|
|
321
332
|
echo "${t(loc, 'hook_use_command_prefix')} $FOUND_CMD --staged"
|
|
322
|
-
|
|
323
|
-
|
|
333
|
+
USE_WINPTY=0
|
|
334
|
+
if command -v uname >/dev/null 2>&1; then
|
|
335
|
+
KERNEL=$(uname -s)
|
|
324
336
|
else
|
|
325
|
-
"
|
|
337
|
+
KERNEL=""
|
|
326
338
|
fi
|
|
327
|
-
|
|
339
|
+
IS_MSYS=0
|
|
340
|
+
if [[ "$KERNEL" == MINGW* || "$KERNEL" == MSYS* || -n "$MSYSTEM" ]]; then
|
|
341
|
+
IS_MSYS=1
|
|
342
|
+
fi
|
|
343
|
+
HAS_TTY=0
|
|
344
|
+
if [ -t 0 ] || [ -t 1 ]; then
|
|
345
|
+
HAS_TTY=1
|
|
346
|
+
fi
|
|
347
|
+
TTY_DEVICE=""
|
|
348
|
+
if [ $HAS_TTY -eq 1 ] && [ -r /dev/tty ]; then
|
|
349
|
+
TTY_DEVICE="/dev/tty"
|
|
350
|
+
elif [ $HAS_TTY -eq 1 ] && [ -r "CONIN$" ]; then
|
|
351
|
+
TTY_DEVICE="CONIN$"
|
|
352
|
+
elif [ -r /dev/tty ]; then
|
|
353
|
+
TTY_DEVICE="/dev/tty"
|
|
354
|
+
elif [ -r "CONIN$" ]; then
|
|
355
|
+
TTY_DEVICE="CONIN$"
|
|
356
|
+
fi
|
|
357
|
+
if [ -n "$TTY_DEVICE" ]; then
|
|
358
|
+
export SMART_REVIEW_TTY="$TTY_DEVICE"
|
|
359
|
+
fi
|
|
360
|
+
if [ $IS_MSYS -eq 1 ] && [ $HAS_TTY -eq 1 ]; then
|
|
361
|
+
if command -v winpty >/dev/null 2>&1; then
|
|
362
|
+
USE_WINPTY=1
|
|
363
|
+
fi
|
|
364
|
+
fi
|
|
365
|
+
run_direct() {
|
|
366
|
+
if [ $USE_WINPTY -eq 1 ]; then
|
|
367
|
+
if [ -n "$FOUND_ENTRY_CMD" ]; then
|
|
368
|
+
winpty node "$FOUND_ENTRY_CMD" --staged
|
|
369
|
+
else
|
|
370
|
+
winpty "$FOUND_CMD" --staged
|
|
371
|
+
fi
|
|
372
|
+
else
|
|
373
|
+
if [ -n "$FOUND_ENTRY_CMD" ]; then
|
|
374
|
+
node "$FOUND_ENTRY_CMD" --staged
|
|
375
|
+
else
|
|
376
|
+
"$FOUND_CMD" --staged
|
|
377
|
+
fi
|
|
378
|
+
fi
|
|
379
|
+
}
|
|
380
|
+
run_with_device() {
|
|
381
|
+
if [ -n "$TTY_DEVICE" ]; then
|
|
382
|
+
"$@" < "$TTY_DEVICE"
|
|
383
|
+
else
|
|
384
|
+
"$@"
|
|
385
|
+
fi
|
|
386
|
+
}
|
|
387
|
+
TMP_ERR=""
|
|
388
|
+
if command -v mktemp >/dev/null 2>&1; then
|
|
389
|
+
TMP_ERR=$(mktemp -t smart-review-err.XXXXXX)
|
|
390
|
+
else
|
|
391
|
+
TMP_ERR="/tmp/smart-review-err-$$"
|
|
392
|
+
fi
|
|
393
|
+
run_direct 2> "$TMP_ERR"
|
|
328
394
|
EXIT_CODE=$?
|
|
395
|
+
if [ $EXIT_CODE -ne 0 ] && [ -s "$TMP_ERR" ] && grep -qi "stdin is not a tty" "$TMP_ERR"; then
|
|
396
|
+
if [ $USE_WINPTY -eq 1 ]; then
|
|
397
|
+
if [ -n "$FOUND_ENTRY_CMD" ]; then
|
|
398
|
+
run_with_device winpty node "$FOUND_ENTRY_CMD" --staged
|
|
399
|
+
else
|
|
400
|
+
run_with_device winpty "$FOUND_CMD" --staged
|
|
401
|
+
fi
|
|
402
|
+
else
|
|
403
|
+
if [ -n "$FOUND_ENTRY_CMD" ]; then
|
|
404
|
+
run_with_device node "$FOUND_ENTRY_CMD" --staged
|
|
405
|
+
else
|
|
406
|
+
run_with_device "$FOUND_CMD" --staged
|
|
407
|
+
fi
|
|
408
|
+
fi
|
|
409
|
+
EXIT_CODE=$?
|
|
410
|
+
fi
|
|
411
|
+
rm -f "$TMP_ERR"
|
|
412
|
+
if [ $EXIT_CODE -eq 130 ] || [ $EXIT_CODE -eq 143 ]; then
|
|
413
|
+
echo "${t(loc, 'interrupt_cancelled')}"
|
|
414
|
+
exit 0
|
|
415
|
+
fi
|
|
329
416
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
330
417
|
echo "${t(loc, 'hook_review_fail')}"
|
|
331
418
|
exit 1
|
|
@@ -336,18 +423,98 @@ fi
|
|
|
336
423
|
`;
|
|
337
424
|
|
|
338
425
|
fs.writeFileSync(preCommitHook, hookContent);
|
|
339
|
-
|
|
426
|
+
const escapeCmdText = (value) => String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
427
|
+
const cmdMessages = {
|
|
428
|
+
start: escapeCmdText(t(loc, 'hook_start_review')),
|
|
429
|
+
noStaged: escapeCmdText(t(loc, 'hook_no_staged')),
|
|
430
|
+
foundHeader: escapeCmdText(t(loc, 'hook_found_staged_header')),
|
|
431
|
+
cdFail: escapeCmdText(t(loc, 'hook_cd_repo_fail')),
|
|
432
|
+
cmdNotFound1: escapeCmdText(t(loc, 'hook_cmd_not_found1')),
|
|
433
|
+
cmdNotFound2: escapeCmdText(t(loc, 'hook_cmd_not_found2')),
|
|
434
|
+
cmdMissingContinue: escapeCmdText(t(loc, 'hook_cmd_missing_continue')),
|
|
435
|
+
useCmdPrefix: escapeCmdText(t(loc, 'hook_use_command_prefix')),
|
|
436
|
+
reviewFail: escapeCmdText(t(loc, 'hook_review_fail')),
|
|
437
|
+
reviewPass: escapeCmdText(t(loc, 'hook_review_pass')),
|
|
438
|
+
interruptCancelled: escapeCmdText(t(loc, 'interrupt_cancelled'))
|
|
439
|
+
};
|
|
440
|
+
const cmdNodeScript = [
|
|
441
|
+
"const { execSync, spawnSync } = require('child_process');",
|
|
442
|
+
"const fs = require('fs');",
|
|
443
|
+
"const path = require('path');",
|
|
444
|
+
`const MSG = { start: '${cmdMessages.start}', noStaged: '${cmdMessages.noStaged}', foundHeader: '${cmdMessages.foundHeader}', cdFail: '${cmdMessages.cdFail}', cmdNotFound1: '${cmdMessages.cmdNotFound1}', cmdNotFound2: '${cmdMessages.cmdNotFound2}', cmdMissingContinue: '${cmdMessages.cmdMissingContinue}', useCmdPrefix: '${cmdMessages.useCmdPrefix}', reviewFail: '${cmdMessages.reviewFail}', reviewPass: '${cmdMessages.reviewPass}', interruptCancelled: '${cmdMessages.interruptCancelled}' };`,
|
|
445
|
+
"const log = (value) => { if (value) console.log(value); };",
|
|
446
|
+
"log(MSG.start);",
|
|
447
|
+
"let staged = '';",
|
|
448
|
+
"try { staged = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' }); } catch (e) {}",
|
|
449
|
+
"const stagedFiles = staged.split(/\\r?\\n/).filter(Boolean);",
|
|
450
|
+
"if (!stagedFiles.length) { log(MSG.noStaged); process.exit(0); }",
|
|
451
|
+
"log(MSG.foundHeader);",
|
|
452
|
+
"log(stagedFiles.join('\\n'));",
|
|
453
|
+
"let repoRoot = '';",
|
|
454
|
+
"try { repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim(); } catch (e) {}",
|
|
455
|
+
"if (!repoRoot) { log(MSG.cdFail); process.exit(1); }",
|
|
456
|
+
"const rootBin = path.join(repoRoot, 'node_modules', '.bin', 'smart-review');",
|
|
457
|
+
"const rootEntry = path.join(repoRoot, 'node_modules', 'smart-review', 'bin', 'review.js');",
|
|
458
|
+
"let foundCmd = '';",
|
|
459
|
+
"let foundEntry = '';",
|
|
460
|
+
"if (fs.existsSync(rootBin)) { foundCmd = rootBin; if (fs.existsSync(rootEntry)) { foundEntry = rootEntry; } }",
|
|
461
|
+
"if (!foundCmd) {",
|
|
462
|
+
" const maxAscend = 6;",
|
|
463
|
+
" outer: for (const file of stagedFiles) {",
|
|
464
|
+
" let dir = path.dirname(file);",
|
|
465
|
+
" let depth = 0;",
|
|
466
|
+
" while (dir && dir !== '.' && depth < maxAscend) {",
|
|
467
|
+
" const candidateBin = path.join(repoRoot, dir, 'node_modules', '.bin', 'smart-review');",
|
|
468
|
+
" const candidateEntry = path.join(repoRoot, dir, 'node_modules', 'smart-review', 'bin', 'review.js');",
|
|
469
|
+
" if (fs.existsSync(candidateBin)) { foundCmd = candidateBin; if (fs.existsSync(candidateEntry)) { foundEntry = candidateEntry; } break outer; }",
|
|
470
|
+
" if (fs.existsSync(candidateEntry)) { foundCmd = candidateEntry; foundEntry = candidateEntry; break outer; }",
|
|
471
|
+
" dir = path.dirname(dir);",
|
|
472
|
+
" depth++;",
|
|
473
|
+
" }",
|
|
474
|
+
" }",
|
|
475
|
+
"}",
|
|
476
|
+
"if (!foundCmd) {",
|
|
477
|
+
" try { execSync('where smart-review', { stdio: 'ignore' }); foundCmd = 'smart-review'; } catch (e) {}",
|
|
478
|
+
"}",
|
|
479
|
+
"if (!foundCmd) { log(MSG.cmdNotFound1); log(MSG.cmdNotFound2); log(MSG.cmdMissingContinue); process.exit(0); }",
|
|
480
|
+
"log(MSG.useCmdPrefix + ' ' + foundCmd + ' --staged');",
|
|
481
|
+
"const hasTty = !!(process.stdin && process.stdin.isTTY) || !!(process.stdout && process.stdout.isTTY);",
|
|
482
|
+
"const tryTty = (p) => { try { const fd = fs.openSync(p, 'r'); fs.closeSync(fd); return p; } catch (e) { return ''; } };",
|
|
483
|
+
"const ensureTty = () => {",
|
|
484
|
+
" if (process.env.SMART_REVIEW_TTY) return;",
|
|
485
|
+
" let tty = '';",
|
|
486
|
+
" if (hasTty) { tty = tryTty('\\\\\\\\.\\\\CONIN$') || tryTty('CONIN$'); }",
|
|
487
|
+
" if (!tty) { tty = tryTty('\\\\\\\\.\\\\CONIN$') || tryTty('CONIN$'); }",
|
|
488
|
+
" if (tty) { process.env.SMART_REVIEW_TTY = tty; process.env.SMART_REVIEW_FORCE_TTY = '1'; }",
|
|
489
|
+
"};",
|
|
490
|
+
"const args = ['--staged'];",
|
|
491
|
+
"const runOnce = (capture) => {",
|
|
492
|
+
" if (foundEntry) {",
|
|
493
|
+
" return spawnSync(process.execPath, [foundEntry, ...args], { stdio: capture ? 'pipe' : 'inherit' });",
|
|
494
|
+
" }",
|
|
495
|
+
" return spawnSync(foundCmd, args, { stdio: capture ? 'pipe' : 'inherit', shell: true });",
|
|
496
|
+
"};",
|
|
497
|
+
"let result = runOnce(true);",
|
|
498
|
+
"if (result.stdout) process.stdout.write(result.stdout);",
|
|
499
|
+
"if (result.stderr) process.stderr.write(result.stderr);",
|
|
500
|
+
"let code = Number.isInteger(result.status) ? result.status : (Number.isInteger(result.code) ? result.code : 0);",
|
|
501
|
+
"const errText = result.stderr ? String(result.stderr) : '';",
|
|
502
|
+
"if (code !== 0 && /stdin is not a tty/i.test(errText)) {",
|
|
503
|
+
" ensureTty();",
|
|
504
|
+
" result = runOnce(false);",
|
|
505
|
+
" code = Number.isInteger(result.status) ? result.status : (Number.isInteger(result.code) ? result.code : 0);",
|
|
506
|
+
"}",
|
|
507
|
+
"if (code === 130 || code === 143) { log(MSG.interruptCancelled); process.exit(0); }",
|
|
508
|
+
"if (code !== 0) { log(MSG.reviewFail); process.exit(1); }",
|
|
509
|
+
"log(MSG.reviewPass); process.exit(0);"
|
|
510
|
+
].join('');
|
|
511
|
+
// Windows 兼容:提供 CMD 包装器,避免 bash 在 CMD 下的 TTY 问题
|
|
340
512
|
try {
|
|
341
513
|
const preCommitCmd = path.join(gitHooksDir, 'pre-commit.cmd');
|
|
342
514
|
const cmdContent = [
|
|
343
515
|
'@echo off',
|
|
344
516
|
'SETLOCAL',
|
|
345
|
-
|
|
346
|
-
'if not exist "%HOOK%" (',
|
|
347
|
-
' echo [smart-review] pre-commit hook missing.',
|
|
348
|
-
' exit /b 1',
|
|
349
|
-
')',
|
|
350
|
-
'bash "%HOOK%"',
|
|
517
|
+
`node -e "${cmdNodeScript}"`,
|
|
351
518
|
'exit /b %ERRORLEVEL%\r\n'
|
|
352
519
|
].join('\r\n');
|
|
353
520
|
fs.writeFileSync(preCommitCmd, cmdContent);
|
|
@@ -416,4 +583,4 @@ fi
|
|
|
416
583
|
|
|
417
584
|
// 运行安装
|
|
418
585
|
const installer = new Installer();
|
|
419
|
-
(async () => { await installer.install(); })();
|
|
586
|
+
(async () => { await installer.install(); })();
|