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 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; break 2
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
- if [ $FOUND_IS_ENTRY -eq 1 ]; then
323
- node "$FOUND_CMD" --staged
333
+ USE_WINPTY=0
334
+ if command -v uname >/dev/null 2>&1; then
335
+ KERNEL=$(uname -s)
324
336
  else
325
- "$FOUND_CMD" --staged
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
- // Windows 兼容:提供 CMD 包装器,调用 bash 执行同名脚本
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
- 'set HOOK=%~dp0pre-commit',
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(); })();