@byh3071/vhk 0.4.0 → 0.5.1
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 +165 -28
- package/dist/index.js +1032 -316
- package/package.json +56 -55
package/dist/index.js
CHANGED
|
@@ -323,7 +323,7 @@ var require_ignore = __commonJS({
|
|
|
323
323
|
// path matching.
|
|
324
324
|
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
325
325
|
// @returns {TestResult} true if a file is ignored
|
|
326
|
-
test(
|
|
326
|
+
test(path15, checkUnignored, mode) {
|
|
327
327
|
let ignored = false;
|
|
328
328
|
let unignored = false;
|
|
329
329
|
let matchedRule;
|
|
@@ -332,7 +332,7 @@ var require_ignore = __commonJS({
|
|
|
332
332
|
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
333
333
|
return;
|
|
334
334
|
}
|
|
335
|
-
const matched = rule[mode].test(
|
|
335
|
+
const matched = rule[mode].test(path15);
|
|
336
336
|
if (!matched) {
|
|
337
337
|
return;
|
|
338
338
|
}
|
|
@@ -353,17 +353,17 @@ var require_ignore = __commonJS({
|
|
|
353
353
|
var throwError = (message, Ctor) => {
|
|
354
354
|
throw new Ctor(message);
|
|
355
355
|
};
|
|
356
|
-
var checkPath = (
|
|
357
|
-
if (!isString(
|
|
356
|
+
var checkPath = (path15, originalPath, doThrow) => {
|
|
357
|
+
if (!isString(path15)) {
|
|
358
358
|
return doThrow(
|
|
359
359
|
`path must be a string, but got \`${originalPath}\``,
|
|
360
360
|
TypeError
|
|
361
361
|
);
|
|
362
362
|
}
|
|
363
|
-
if (!
|
|
363
|
+
if (!path15) {
|
|
364
364
|
return doThrow(`path must not be empty`, TypeError);
|
|
365
365
|
}
|
|
366
|
-
if (checkPath.isNotRelative(
|
|
366
|
+
if (checkPath.isNotRelative(path15)) {
|
|
367
367
|
const r = "`path.relative()`d";
|
|
368
368
|
return doThrow(
|
|
369
369
|
`path should be a ${r} string, but got "${originalPath}"`,
|
|
@@ -372,7 +372,7 @@ var require_ignore = __commonJS({
|
|
|
372
372
|
}
|
|
373
373
|
return true;
|
|
374
374
|
};
|
|
375
|
-
var isNotRelative = (
|
|
375
|
+
var isNotRelative = (path15) => REGEX_TEST_INVALID_PATH.test(path15);
|
|
376
376
|
checkPath.isNotRelative = isNotRelative;
|
|
377
377
|
checkPath.convert = (p) => p;
|
|
378
378
|
var Ignore = class {
|
|
@@ -402,19 +402,19 @@ var require_ignore = __commonJS({
|
|
|
402
402
|
}
|
|
403
403
|
// @returns {TestResult}
|
|
404
404
|
_test(originalPath, cache, checkUnignored, slices) {
|
|
405
|
-
const
|
|
405
|
+
const path15 = originalPath && checkPath.convert(originalPath);
|
|
406
406
|
checkPath(
|
|
407
|
-
|
|
407
|
+
path15,
|
|
408
408
|
originalPath,
|
|
409
409
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
410
410
|
);
|
|
411
|
-
return this._t(
|
|
411
|
+
return this._t(path15, cache, checkUnignored, slices);
|
|
412
412
|
}
|
|
413
|
-
checkIgnore(
|
|
414
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
415
|
-
return this.test(
|
|
413
|
+
checkIgnore(path15) {
|
|
414
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path15)) {
|
|
415
|
+
return this.test(path15);
|
|
416
416
|
}
|
|
417
|
-
const slices =
|
|
417
|
+
const slices = path15.split(SLASH).filter(Boolean);
|
|
418
418
|
slices.pop();
|
|
419
419
|
if (slices.length) {
|
|
420
420
|
const parent = this._t(
|
|
@@ -427,18 +427,18 @@ var require_ignore = __commonJS({
|
|
|
427
427
|
return parent;
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
|
-
return this._rules.test(
|
|
430
|
+
return this._rules.test(path15, false, MODE_CHECK_IGNORE);
|
|
431
431
|
}
|
|
432
|
-
_t(
|
|
433
|
-
if (
|
|
434
|
-
return cache[
|
|
432
|
+
_t(path15, cache, checkUnignored, slices) {
|
|
433
|
+
if (path15 in cache) {
|
|
434
|
+
return cache[path15];
|
|
435
435
|
}
|
|
436
436
|
if (!slices) {
|
|
437
|
-
slices =
|
|
437
|
+
slices = path15.split(SLASH).filter(Boolean);
|
|
438
438
|
}
|
|
439
439
|
slices.pop();
|
|
440
440
|
if (!slices.length) {
|
|
441
|
-
return cache[
|
|
441
|
+
return cache[path15] = this._rules.test(path15, checkUnignored, MODE_IGNORE);
|
|
442
442
|
}
|
|
443
443
|
const parent = this._t(
|
|
444
444
|
slices.join(SLASH) + SLASH,
|
|
@@ -446,29 +446,29 @@ var require_ignore = __commonJS({
|
|
|
446
446
|
checkUnignored,
|
|
447
447
|
slices
|
|
448
448
|
);
|
|
449
|
-
return cache[
|
|
449
|
+
return cache[path15] = parent.ignored ? parent : this._rules.test(path15, checkUnignored, MODE_IGNORE);
|
|
450
450
|
}
|
|
451
|
-
ignores(
|
|
452
|
-
return this._test(
|
|
451
|
+
ignores(path15) {
|
|
452
|
+
return this._test(path15, this._ignoreCache, false).ignored;
|
|
453
453
|
}
|
|
454
454
|
createFilter() {
|
|
455
|
-
return (
|
|
455
|
+
return (path15) => !this.ignores(path15);
|
|
456
456
|
}
|
|
457
457
|
filter(paths) {
|
|
458
458
|
return makeArray(paths).filter(this.createFilter());
|
|
459
459
|
}
|
|
460
460
|
// @returns {TestResult}
|
|
461
|
-
test(
|
|
462
|
-
return this._test(
|
|
461
|
+
test(path15) {
|
|
462
|
+
return this._test(path15, this._testCache, true);
|
|
463
463
|
}
|
|
464
464
|
};
|
|
465
465
|
var factory = (options) => new Ignore(options);
|
|
466
|
-
var isPathValid = (
|
|
466
|
+
var isPathValid = (path15) => checkPath(path15 && checkPath.convert(path15), path15, RETURN_FALSE);
|
|
467
467
|
var setupWindows = () => {
|
|
468
468
|
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
469
469
|
checkPath.convert = makePosix;
|
|
470
470
|
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
471
|
-
checkPath.isNotRelative = (
|
|
471
|
+
checkPath.isNotRelative = (path15) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path15) || isNotRelative(path15);
|
|
472
472
|
};
|
|
473
473
|
if (
|
|
474
474
|
// Detect `process` so that it can run in browsers.
|
|
@@ -485,75 +485,110 @@ var require_ignore = __commonJS({
|
|
|
485
485
|
|
|
486
486
|
// src/index.ts
|
|
487
487
|
import { Command, Help } from "commander";
|
|
488
|
-
import
|
|
489
|
-
import
|
|
488
|
+
import chalk16 from "chalk";
|
|
489
|
+
import inquirer7 from "inquirer";
|
|
490
490
|
|
|
491
491
|
// src/lib/nlp-router.ts
|
|
492
492
|
function normalize(input) {
|
|
493
493
|
return input.trim().toLowerCase().replace(/\s+/g, " ");
|
|
494
494
|
}
|
|
495
|
+
var NLP_KEYWORDS = {
|
|
496
|
+
save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
|
|
497
|
+
undo: ["\uB418\uB3CC\uB824", "\uB418\uB3CC\uB9AC\uAE30", "\uCDE8\uC18C", "\uC6D0\uB798\uB300\uB85C", "\uB864\uBC31", "\uB9AC\uC14B", "reset", "rollback"],
|
|
498
|
+
status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"],
|
|
499
|
+
diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
|
|
500
|
+
};
|
|
501
|
+
function matchesKeywords(text, command) {
|
|
502
|
+
const keywords = NLP_KEYWORDS[command];
|
|
503
|
+
if (!keywords) return false;
|
|
504
|
+
return keywords.some((kw) => text.includes(kw.toLowerCase()));
|
|
505
|
+
}
|
|
495
506
|
var RULES = [
|
|
496
507
|
{
|
|
497
508
|
command: "init",
|
|
498
509
|
explanation: "\uAC80\uC99D \uC2A4\uD0B5\uD558\uACE0 \uBC14\uB85C \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --skip-gate)",
|
|
499
510
|
confidence: "high",
|
|
500
511
|
args: ["--skip-gate"],
|
|
501
|
-
test: (
|
|
512
|
+
test: (t2) => /기획.*(끝|완료)|노션.*(기획|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2)
|
|
502
513
|
},
|
|
503
514
|
{
|
|
504
515
|
command: "init",
|
|
505
516
|
explanation: "Notion\uC5D0\uC11C \uAC00\uC838\uC640 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --from-notion)",
|
|
506
517
|
confidence: "low",
|
|
507
518
|
args: ["--from-notion"],
|
|
508
|
-
test: (
|
|
519
|
+
test: (t2) => /노션|notion/.test(t2) && /(시작|만들|import|가져)/.test(t2)
|
|
509
520
|
},
|
|
510
521
|
{
|
|
511
522
|
command: "init",
|
|
512
523
|
explanation: "\uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791)",
|
|
513
524
|
confidence: "high",
|
|
514
|
-
test: (
|
|
525
|
+
test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|하네스|초기화/.test(t2) || /^시작$/.test(t2)
|
|
515
526
|
},
|
|
516
527
|
{
|
|
517
|
-
command: "
|
|
518
|
-
explanation: "\
|
|
528
|
+
command: "secure",
|
|
529
|
+
explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548)",
|
|
530
|
+
confidence: "high",
|
|
531
|
+
test: (t2) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t2)
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
command: "check",
|
|
535
|
+
explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
|
|
519
536
|
confidence: "high",
|
|
520
|
-
test: (
|
|
537
|
+
test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
|
|
521
538
|
},
|
|
522
539
|
{
|
|
523
540
|
command: "doctor",
|
|
524
541
|
explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
|
|
525
542
|
confidence: "high",
|
|
526
|
-
test: (
|
|
543
|
+
test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단|확인)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
|
|
527
544
|
},
|
|
528
545
|
{
|
|
529
|
-
command: "
|
|
530
|
-
explanation: "\
|
|
546
|
+
command: "diff",
|
|
547
|
+
explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
|
|
531
548
|
confidence: "high",
|
|
532
|
-
test: (
|
|
549
|
+
test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /변경사항|수정\s*내역|차이\s*보/.test(t2)) && !/저장|커밋|push|푸시|상태|현황|세이브|commit/.test(t2)
|
|
533
550
|
},
|
|
534
551
|
{
|
|
535
|
-
command: "
|
|
536
|
-
explanation: "\
|
|
552
|
+
command: "undo",
|
|
553
|
+
explanation: "\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30 (vhk \uB418\uB3CC\uB9AC\uAE30)",
|
|
537
554
|
confidence: "high",
|
|
538
|
-
test: (
|
|
555
|
+
test: (t2) => matchesKeywords(t2, "undo") || /undo|커밋\s*취/.test(t2)
|
|
539
556
|
},
|
|
540
557
|
{
|
|
541
|
-
command: "
|
|
542
|
-
explanation: "\
|
|
558
|
+
command: "status",
|
|
559
|
+
explanation: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778 (vhk \uC0C1\uD0DC)",
|
|
543
560
|
confidence: "high",
|
|
544
|
-
test: (
|
|
561
|
+
test: (t2) => (matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태|프로젝트\s*상태/.test(t2)) && !/보안|시크릿|규칙|점검|린트|환경|진단|doctor|secure|check|스캔|설치/.test(t2)
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
command: "save",
|
|
565
|
+
explanation: "Git\uC5D0 \uC800\uC7A5 (vhk \uC800\uC7A5)",
|
|
566
|
+
confidence: "high",
|
|
567
|
+
test: (t2) => (matchesKeywords(t2, "save") || /깃허브|github/.test(t2)) && !/정리|recap|되돌|취소|rollback|reset|리셋|롤백|원래대로/.test(t2)
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
command: "recap",
|
|
571
|
+
explanation: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC (vhk \uC815\uB9AC)",
|
|
572
|
+
confidence: "high",
|
|
573
|
+
test: (t2) => /오늘.*(정리|기록)|한\s*일|세션|회고|recap|정리해/.test(t2)
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
command: "gate",
|
|
577
|
+
explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
|
|
578
|
+
confidence: "high",
|
|
579
|
+
test: (t2) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t2)
|
|
545
580
|
},
|
|
546
581
|
{
|
|
547
582
|
command: "sync",
|
|
548
583
|
explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
|
|
549
584
|
confidence: "high",
|
|
550
|
-
test: (
|
|
585
|
+
test: (t2) => /규칙.*(맞|동기)|sync|cursorrules|claude\.md.*맞/.test(t2)
|
|
551
586
|
},
|
|
552
587
|
{
|
|
553
588
|
command: "ship",
|
|
554
589
|
explanation: "\uBC30\uD3EC \uCCB4\uD06C + \uD68C\uACE0 (vhk ship)",
|
|
555
590
|
confidence: "high",
|
|
556
|
-
test: (
|
|
591
|
+
test: (t2) => /배포|출시|릴리스|ship|빌드\s*전/.test(t2)
|
|
557
592
|
}
|
|
558
593
|
];
|
|
559
594
|
function routeNaturalLanguage(input) {
|
|
@@ -578,6 +613,72 @@ function extractNotionUrl(input) {
|
|
|
578
613
|
|
|
579
614
|
// src/i18n/ko.ts
|
|
580
615
|
var ko = {
|
|
616
|
+
status: {
|
|
617
|
+
title: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC",
|
|
618
|
+
notGitRepo: "Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2C8\uC5D0\uC694. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
619
|
+
branch: "\uBE0C\uB79C\uCE58:",
|
|
620
|
+
changes: "\uBCC0\uACBD:",
|
|
621
|
+
recentCommits: "\uCD5C\uADFC \uCEE4\uBC0B (3):",
|
|
622
|
+
noCommits: "\uCEE4\uBC0B \uC5C6\uC74C",
|
|
623
|
+
remote: "\uC6D0\uACA9:",
|
|
624
|
+
noUpstream: "upstream \uC5C6\uC74C",
|
|
625
|
+
inSync: "\uB3D9\uAE30\uD654\uB428",
|
|
626
|
+
ahead: (n) => `\u2191${n} ahead`,
|
|
627
|
+
behind: (n) => `\u2193${n} behind`,
|
|
628
|
+
package: "package.json:",
|
|
629
|
+
noPackage: "package.json \uC5C6\uC74C",
|
|
630
|
+
detached: "(detached HEAD)",
|
|
631
|
+
unknownBranch: "(\uC54C \uC218 \uC5C6\uC74C)"
|
|
632
|
+
},
|
|
633
|
+
save: {
|
|
634
|
+
title: "\uC800\uC7A5\uD558\uAE30",
|
|
635
|
+
notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
636
|
+
noChanges: "\uC800\uC7A5\uD560 \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
637
|
+
filesHeader: (n) => `\uBCC0\uACBD\uB41C \uD30C\uC77C (${n}\uAC1C):`,
|
|
638
|
+
commitMessage: "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 (Enter\uB85C \uAE30\uBCF8\uAC12 \uC0AC\uC6A9):",
|
|
639
|
+
saving: "\uC800\uC7A5 \uC911...",
|
|
640
|
+
pushing: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uC5D0 \uC62C\uB9AC\uB294 \uC911...",
|
|
641
|
+
successWithPush: "\uC800\uC7A5 + \uC6D0\uACA9 \uC5C5\uB85C\uB4DC \uC644\uB8CC!",
|
|
642
|
+
successLocal: "\uB85C\uCEEC \uC800\uC7A5 \uC644\uB8CC!",
|
|
643
|
+
noRemote: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 push\uB97C \uAC74\uB108\uB6F0\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
644
|
+
failed: "\uC800\uC7A5 \uC2E4\uD328",
|
|
645
|
+
stagedAfterFail: "\uCEE4\uBC0B\uC740 \uC2E4\uD328\uD588\uC9C0\uB9CC \uD30C\uC77C\uC740 \uC2A4\uD14C\uC774\uC9D5\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uD655\uC778: git status / \uCDE8\uC18C: git reset HEAD",
|
|
646
|
+
securityWarnHeader: "\uC800\uC7A5 \uC804 \uBCF4\uC548 \uD655\uC778:",
|
|
647
|
+
secretsFound: (n) => `\uCF54\uB4DC\uC5D0\uC11C CRITICAL/HIGH \uC2DC\uD06C\uB9BF \uD328\uD134 ${n}\uAC74 \uAC10\uC9C0`,
|
|
648
|
+
secretsConfirm: "\uADF8\uB798\uB3C4 \uCEE4\uBC0B\xB7push\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
649
|
+
cancelled: "\uC800\uC7A5\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.",
|
|
650
|
+
pushFailed: "push \uC2E4\uD328 (\uB85C\uCEEC \uCEE4\uBC0B\uC740 \uC644\uB8CC\uB428)",
|
|
651
|
+
commitOkPushFailed: "\uB85C\uCEEC \uCEE4\uBC0B\uC740 \uB410\uC9C0\uB9CC \uC6D0\uACA9 push\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. git push\uB97C \uC9C1\uC811 \uD655\uC778\uD558\uC138\uC694.",
|
|
652
|
+
done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`,
|
|
653
|
+
doneLocalOnly: (n) => `${n}\uAC1C \uD30C\uC77C \uB85C\uCEEC \uC800\uC7A5\uB428 (push\uB294 \uC2E4\uD328)`
|
|
654
|
+
},
|
|
655
|
+
undo: {
|
|
656
|
+
title: "\uB418\uB3CC\uB9AC\uAE30",
|
|
657
|
+
notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
|
|
658
|
+
noCommits: "\uB418\uB3CC\uB9B4 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
659
|
+
recentHeader: "\u{1F4CB} \uCD5C\uADFC \uCEE4\uBC0B:",
|
|
660
|
+
howMany: "\uBA87 \uAC1C\uC758 \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9B4\uAE4C\uC694?",
|
|
661
|
+
alreadyPushed: "\uC774 \uCEE4\uBC0B\uC740 \uC774\uBBF8 \uC6D0\uACA9\uC5D0 \uC62C\uB77C\uAC14\uC2B5\uB2C8\uB2E4. \uB418\uB3CC\uB9AC\uBA74 \uCDA9\uB3CC\uC774 \uC0DD\uAE38 \uC218 \uC788\uC5B4\uC694.",
|
|
662
|
+
noUpstreamWarning: "upstream \uBE0C\uB79C\uCE58\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 push\uD55C \uCEE4\uBC0B\uC77C \uC218 \uC788\uC5B4\uC694. \uB418\uB3CC\uB9B0 \uB4A4 force push\uAC00 \uD544\uC694\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
663
|
+
confirmMessage: "\uCD5C\uADFC \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9AC\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
|
|
664
|
+
confirmRisky: (n) => `\u26A0\uFE0F \uC704\uD5D8: \uCD5C\uADFC ${n}\uAC1C \uCEE4\uBC0B\uC744 soft reset\uD569\uB2C8\uB2E4. \uC6D0\uACA9\uACFC \uC5B4\uAE0B\uB0A0 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uACC4\uC18D\uD560\uAE4C\uC694?`,
|
|
665
|
+
cancelled: "\uCDE8\uC18C\uB428",
|
|
666
|
+
success: "\uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC! \uBCC0\uACBD\uC0AC\uD56D\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.",
|
|
667
|
+
stagedHint: "\uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uB0A8\uC544 \uC788\uC5B4\uC694.",
|
|
668
|
+
rootCommit: "\uCCAB \uCEE4\uBC0B\uB9CC \uC788\uC5B4\uC11C \uB354 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
669
|
+
forcePushHint: "\uC6D0\uACA9\uACFC \uB9DE\uCD94\uB824\uBA74: git push --force-with-lease (\uD63C\uC790 \uC791\uC5C5\uD55C \uBE0C\uB79C\uCE58\uC5D0\uC11C\uB9CC, \uD300\uACFC \uD569\uC758 \uD6C4)",
|
|
670
|
+
failed: "\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328"
|
|
671
|
+
},
|
|
672
|
+
diff: {
|
|
673
|
+
title: "\uBCC0\uACBD\uC0AC\uD56D \uD655\uC778",
|
|
674
|
+
notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
|
|
675
|
+
noChanges: "\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
676
|
+
stagedHeader: "\u{1F4E6} \uCEE4\uBC0B \uB300\uAE30 (staged):",
|
|
677
|
+
unstagedHeader: "\u270F\uFE0F \uC218\uC815\uB428 (unstaged):",
|
|
678
|
+
untrackedHeader: (n) => `\u2795 \uC0C8 \uD30C\uC77C (${n}\uAC1C):`,
|
|
679
|
+
summaryHeader: "\u{1F4CA} \uCD1D \uBCC0\uACBD \uC694\uC57D (\uC791\uC5C5 \uD2B8\uB9AC vs HEAD)",
|
|
680
|
+
filesLine: (n) => `\uD30C\uC77C: ${n}\uAC1C`
|
|
681
|
+
},
|
|
581
682
|
start: {
|
|
582
683
|
title: "\u{1F527} VHK \u2014 \uBB34\uC5C7\uC744 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
583
684
|
subtitle: "\uBC88\uD638\uB9CC \uACE0\uB974\uBA74 \uB429\uB2C8\uB2E4. \uBA85\uB839\uC5B4\uB97C \uC678\uC6B8 \uD544\uC694 \uC5C6\uC5B4\uC694.",
|
|
@@ -749,6 +850,23 @@ var ko = {
|
|
|
749
850
|
hintCommit: "git status \uD655\uC778"
|
|
750
851
|
}
|
|
751
852
|
};
|
|
853
|
+
function lookup(path15) {
|
|
854
|
+
const parts = path15.split(".");
|
|
855
|
+
let cur = ko;
|
|
856
|
+
for (const part of parts) {
|
|
857
|
+
if (cur === null || typeof cur !== "object") return void 0;
|
|
858
|
+
cur = cur[part];
|
|
859
|
+
}
|
|
860
|
+
return cur;
|
|
861
|
+
}
|
|
862
|
+
function t(key, ...args) {
|
|
863
|
+
const value = lookup(key);
|
|
864
|
+
if (typeof value === "function") {
|
|
865
|
+
return value(...args);
|
|
866
|
+
}
|
|
867
|
+
if (typeof value === "string") return value;
|
|
868
|
+
return key;
|
|
869
|
+
}
|
|
752
870
|
|
|
753
871
|
// src/commands/gate.ts
|
|
754
872
|
import inquirer from "inquirer";
|
|
@@ -861,7 +979,7 @@ ${ko.gate.checklistStart}
|
|
|
861
979
|
name: "answer",
|
|
862
980
|
message: `[${i + 1}/${total}] ${q.stage}: ${q.question}`
|
|
863
981
|
}]);
|
|
864
|
-
const { status } = await inquirer.prompt([{
|
|
982
|
+
const { status: status2 } = await inquirer.prompt([{
|
|
865
983
|
type: "list",
|
|
866
984
|
name: "status",
|
|
867
985
|
message: ko.gate.verdictPrompt(q.failIf),
|
|
@@ -871,10 +989,10 @@ ${ko.gate.checklistStart}
|
|
|
871
989
|
{ name: ko.gate.statusFailChoice, value: "fail" }
|
|
872
990
|
]
|
|
873
991
|
}]);
|
|
874
|
-
if (
|
|
875
|
-
if (
|
|
876
|
-
results.push({ id: q.id, stage: q.stage, status, answer });
|
|
877
|
-
const icon =
|
|
992
|
+
if (status2 === "fail") failCount++;
|
|
993
|
+
if (status2 === "hold") holdCount++;
|
|
994
|
+
results.push({ id: q.id, stage: q.stage, status: status2, answer });
|
|
995
|
+
const icon = status2 === "pass" ? chalk2.green(ko.gate.statusPassLine) : status2 === "hold" ? chalk2.yellow(ko.gate.statusHoldLine) : chalk2.red(ko.gate.statusFailLine);
|
|
878
996
|
console.log(icon);
|
|
879
997
|
}
|
|
880
998
|
console.log(chalk2.bold(`
|
|
@@ -910,9 +1028,9 @@ ${ko.gate.verdictTitle}
|
|
|
910
1028
|
|
|
911
1029
|
// src/commands/init.ts
|
|
912
1030
|
import inquirer2 from "inquirer";
|
|
913
|
-
import
|
|
914
|
-
import
|
|
915
|
-
import
|
|
1031
|
+
import chalk5 from "chalk";
|
|
1032
|
+
import fs3 from "fs";
|
|
1033
|
+
import path3 from "path";
|
|
916
1034
|
|
|
917
1035
|
// src/templates/claude-md.ts
|
|
918
1036
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
@@ -1122,27 +1240,113 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
1122
1240
|
].join("\n");
|
|
1123
1241
|
}
|
|
1124
1242
|
|
|
1125
|
-
// src/
|
|
1243
|
+
// src/lib/check-secure.ts
|
|
1244
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
1245
|
+
import fs from "fs";
|
|
1246
|
+
import path from "path";
|
|
1126
1247
|
import chalk3 from "chalk";
|
|
1248
|
+
function loadGitignore(rootDir) {
|
|
1249
|
+
const ig = (0, import_ignore.default)();
|
|
1250
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1251
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1252
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
1253
|
+
ig.add(content);
|
|
1254
|
+
}
|
|
1255
|
+
return ig;
|
|
1256
|
+
}
|
|
1257
|
+
function isPathIgnored(ig, relativePath) {
|
|
1258
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
1259
|
+
return ig.ignores(normalized);
|
|
1260
|
+
}
|
|
1261
|
+
function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDepth = 8) {
|
|
1262
|
+
const exposed = [];
|
|
1263
|
+
function walk(dir, depth) {
|
|
1264
|
+
if (depth > maxDepth) return;
|
|
1265
|
+
let entries;
|
|
1266
|
+
try {
|
|
1267
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1268
|
+
} catch {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
for (const entry of entries) {
|
|
1272
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1273
|
+
const fullPath = path.join(dir, entry.name);
|
|
1274
|
+
const rel = path.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1275
|
+
if (entry.isDirectory()) {
|
|
1276
|
+
if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (isSensitiveName(entry.name) && !isPathIgnored(ig, rel)) {
|
|
1280
|
+
exposed.push(rel);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
walk(rootDir, 0);
|
|
1285
|
+
return exposed;
|
|
1286
|
+
}
|
|
1287
|
+
function isSensitiveName(name) {
|
|
1288
|
+
const lower = name.toLowerCase();
|
|
1289
|
+
if (lower === ".env" || lower.startsWith(".env.")) return true;
|
|
1290
|
+
if (lower.endsWith(".pem") || lower.endsWith(".key")) return true;
|
|
1291
|
+
if (lower === "credentials.json" || lower === "secrets.json") return true;
|
|
1292
|
+
if (lower.startsWith("id_rsa")) return true;
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
function checkProjectSecurity(rootDir = process.cwd()) {
|
|
1296
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1297
|
+
const missingGitignore = !fs.existsSync(gitignorePath);
|
|
1298
|
+
const ig = loadGitignore(rootDir);
|
|
1299
|
+
const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
|
|
1300
|
+
const warnings = [];
|
|
1301
|
+
if (missingGitignore) {
|
|
1302
|
+
warnings.push(".gitignore \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBBFC\uAC10\uD55C \uD30C\uC77C\uC774 \uC2E4\uC218\uB85C \uC62C\uB77C\uAC08 \uC218 \uC788\uC5B4\uC694.");
|
|
1303
|
+
}
|
|
1304
|
+
if (exposedPaths.length > 0) {
|
|
1305
|
+
warnings.push(
|
|
1306
|
+
`ignore\uB418\uC9C0 \uC54A\uC740 \uBBFC\uAC10 \uD30C\uC77C ${exposedPaths.length}\uAC1C: ${exposedPaths.join(", ")}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
ok: !missingGitignore && exposedPaths.length === 0,
|
|
1311
|
+
missingGitignore,
|
|
1312
|
+
exposedPaths,
|
|
1313
|
+
warnings
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function printSecurityWarnings(rootDir = process.cwd()) {
|
|
1317
|
+
const result = checkProjectSecurity(rootDir);
|
|
1318
|
+
if (result.ok) return true;
|
|
1319
|
+
for (const w of result.warnings) {
|
|
1320
|
+
console.log(chalk3.yellow(` \u26A0\uFE0F ${w}`));
|
|
1321
|
+
}
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1325
|
+
const ig = loadGitignore(rootDir);
|
|
1326
|
+
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/utils/logger.ts
|
|
1330
|
+
import chalk4 from "chalk";
|
|
1127
1331
|
var log = {
|
|
1128
|
-
success: (msg) => console.log(
|
|
1129
|
-
error: (msg) => console.log(
|
|
1130
|
-
warn: (msg) => console.log(
|
|
1131
|
-
info: (msg) => console.log(
|
|
1132
|
-
step: (msg) => console.log(
|
|
1332
|
+
success: (msg) => console.log(chalk4.green(`\u2705 ${msg}`)),
|
|
1333
|
+
error: (msg) => console.log(chalk4.red(`\u274C ${msg}`)),
|
|
1334
|
+
warn: (msg) => console.log(chalk4.yellow(`\u26A0\uFE0F ${msg}`)),
|
|
1335
|
+
info: (msg) => console.log(chalk4.blue(`\u2139\uFE0F ${msg}`)),
|
|
1336
|
+
step: (msg) => console.log(chalk4.bold(`
|
|
1133
1337
|
\u25B8 ${msg}`))
|
|
1134
1338
|
};
|
|
1135
1339
|
|
|
1136
1340
|
// src/utils/file.ts
|
|
1137
|
-
import
|
|
1138
|
-
import
|
|
1341
|
+
import fs2 from "fs";
|
|
1342
|
+
import path2 from "path";
|
|
1139
1343
|
function writeFile(filePath, content) {
|
|
1140
|
-
const dir =
|
|
1141
|
-
if (!
|
|
1142
|
-
|
|
1344
|
+
const dir = path2.dirname(filePath);
|
|
1345
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1346
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
1143
1347
|
}
|
|
1144
1348
|
function fileExists(filePath) {
|
|
1145
|
-
return
|
|
1349
|
+
return fs2.existsSync(filePath);
|
|
1146
1350
|
}
|
|
1147
1351
|
|
|
1148
1352
|
// src/lib/notion-import.ts
|
|
@@ -1176,7 +1380,7 @@ function extractPageId(url) {
|
|
|
1176
1380
|
function getPageTitle(page) {
|
|
1177
1381
|
for (const prop of Object.values(page.properties)) {
|
|
1178
1382
|
if (prop.type === "title") {
|
|
1179
|
-
return prop.title.map((
|
|
1383
|
+
return prop.title.map((t2) => t2.plain_text).join("");
|
|
1180
1384
|
}
|
|
1181
1385
|
}
|
|
1182
1386
|
return "Untitled";
|
|
@@ -1206,7 +1410,7 @@ function extractText(block) {
|
|
|
1206
1410
|
const type = block.type;
|
|
1207
1411
|
const data = block[type];
|
|
1208
1412
|
if (!data?.rich_text) return "";
|
|
1209
|
-
return data.rich_text.map((
|
|
1413
|
+
return data.rich_text.map((t2) => t2.plain_text).join("");
|
|
1210
1414
|
}
|
|
1211
1415
|
function parseBlocks(blocks) {
|
|
1212
1416
|
const sections = {};
|
|
@@ -1298,7 +1502,7 @@ var PROJECT_TYPES = [
|
|
|
1298
1502
|
{ name: "\u{1F916} \uB178\uC158 \uD1B5\uD569/MCP \uC11C\uBC84", value: "notion" },
|
|
1299
1503
|
{ name: "\u{1F4F1} \uBAA8\uBC14\uC77C \uC571 (Flutter)", value: "mobile" }
|
|
1300
1504
|
];
|
|
1301
|
-
var VALID_TYPES = PROJECT_TYPES.map((
|
|
1505
|
+
var VALID_TYPES = PROJECT_TYPES.map((t2) => t2.value);
|
|
1302
1506
|
var STACK_PRESETS = {
|
|
1303
1507
|
webapp: ["Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "Supabase", "Vercel"],
|
|
1304
1508
|
extension: ["Vite", "TypeScript", "@crxjs/vite-plugin", "Chrome Extension Manifest V3"],
|
|
@@ -1334,13 +1538,14 @@ async function collectAnswers(options, defaults = {}) {
|
|
|
1334
1538
|
async function init(options = {}) {
|
|
1335
1539
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
1336
1540
|
if (skipGate) {
|
|
1337
|
-
console.log(
|
|
1541
|
+
console.log(chalk5.dim(`
|
|
1338
1542
|
${ko.init.skipGate}
|
|
1339
1543
|
`));
|
|
1340
1544
|
}
|
|
1341
|
-
console.log(
|
|
1545
|
+
console.log(chalk5.bold(`
|
|
1342
1546
|
${ko.init.title}
|
|
1343
1547
|
`));
|
|
1548
|
+
printSecurityWarnings();
|
|
1344
1549
|
let prdContent = {};
|
|
1345
1550
|
const defaults = {};
|
|
1346
1551
|
if (options.fromNotion) {
|
|
@@ -1362,7 +1567,7 @@ ${ko.init.title}
|
|
|
1362
1567
|
process.exit(1);
|
|
1363
1568
|
}
|
|
1364
1569
|
const stack = STACK_PRESETS[answers.type];
|
|
1365
|
-
console.log(
|
|
1570
|
+
console.log(chalk5.dim(`
|
|
1366
1571
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1367
1572
|
`));
|
|
1368
1573
|
if (!options.yes) {
|
|
@@ -1381,7 +1586,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1381
1586
|
const files = generateFiles(answers.name, answers.description, stack, prdContent);
|
|
1382
1587
|
log.step(ko.init.filesGenerating);
|
|
1383
1588
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1384
|
-
const fullPath =
|
|
1589
|
+
const fullPath = path3.join(cwd, filePath);
|
|
1385
1590
|
if (fileExists(fullPath)) {
|
|
1386
1591
|
const { overwrite } = await inquirer2.prompt([{
|
|
1387
1592
|
type: "confirm",
|
|
@@ -1398,21 +1603,21 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1398
1603
|
log.success(filePath);
|
|
1399
1604
|
}
|
|
1400
1605
|
await writeInitExtras(cwd);
|
|
1401
|
-
console.log(
|
|
1606
|
+
console.log(chalk5.bold.green(`
|
|
1402
1607
|
${ko.init.done}`));
|
|
1403
|
-
console.log(
|
|
1608
|
+
console.log(chalk5.dim(`
|
|
1404
1609
|
${ko.init.nextSteps}`));
|
|
1405
1610
|
if (options.fromNotion) {
|
|
1406
1611
|
console.log(` 1. ${ko.init.notionReviewHint}`);
|
|
1407
1612
|
console.log(` 2. ${ko.init.gitHintLabel}`);
|
|
1408
|
-
console.log(` ${
|
|
1613
|
+
console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
|
|
1409
1614
|
console.log(` 3. ${ko.init.startDev}
|
|
1410
1615
|
`);
|
|
1411
1616
|
} else {
|
|
1412
1617
|
console.log(` 1. ${ko.init.fillHint}`);
|
|
1413
1618
|
console.log(` 2. ${ko.init.prdHint}`);
|
|
1414
1619
|
console.log(` 3. ${ko.init.gitHintLabel}`);
|
|
1415
|
-
console.log(` ${
|
|
1620
|
+
console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
|
|
1416
1621
|
console.log(` 4. ${ko.init.startDev}
|
|
1417
1622
|
`);
|
|
1418
1623
|
}
|
|
@@ -1452,7 +1657,7 @@ function generateFiles(name, description, stack, prdContent = {}) {
|
|
|
1452
1657
|
};
|
|
1453
1658
|
}
|
|
1454
1659
|
var VHK_PACKAGE_SCRIPTS = {
|
|
1455
|
-
save: "
|
|
1660
|
+
save: "vhk save",
|
|
1456
1661
|
check: "vhk check",
|
|
1457
1662
|
scan: "vhk secure scan",
|
|
1458
1663
|
recap: "vhk recap",
|
|
@@ -1460,15 +1665,15 @@ var VHK_PACKAGE_SCRIPTS = {
|
|
|
1460
1665
|
doctor: "vhk doctor"
|
|
1461
1666
|
};
|
|
1462
1667
|
function enhancePackageScripts(projectDir) {
|
|
1463
|
-
const pkgPath =
|
|
1464
|
-
if (!
|
|
1465
|
-
const pkg = JSON.parse(
|
|
1668
|
+
const pkgPath = path3.join(projectDir, "package.json");
|
|
1669
|
+
if (!fs3.existsSync(pkgPath)) return false;
|
|
1670
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1466
1671
|
pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
|
|
1467
|
-
|
|
1672
|
+
fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1468
1673
|
return true;
|
|
1469
1674
|
}
|
|
1470
1675
|
async function writeInitExtras(projectDir) {
|
|
1471
|
-
const commandsPath =
|
|
1676
|
+
const commandsPath = path3.join(projectDir, "COMMANDS.md");
|
|
1472
1677
|
if (fileExists(commandsPath)) {
|
|
1473
1678
|
const { overwrite } = await inquirer2.prompt([{
|
|
1474
1679
|
type: "confirm",
|
|
@@ -1493,37 +1698,13 @@ async function writeInitExtras(projectDir) {
|
|
|
1493
1698
|
|
|
1494
1699
|
// src/commands/recap.ts
|
|
1495
1700
|
import inquirer3 from "inquirer";
|
|
1496
|
-
import
|
|
1701
|
+
import chalk6 from "chalk";
|
|
1497
1702
|
import fs5 from "fs";
|
|
1498
1703
|
import path6 from "path";
|
|
1499
1704
|
|
|
1500
1705
|
// src/lib/git.ts
|
|
1501
1706
|
import path4 from "path";
|
|
1502
1707
|
import simpleGit from "simple-git";
|
|
1503
|
-
|
|
1504
|
-
// src/lib/check-secure.ts
|
|
1505
|
-
var import_ignore = __toESM(require_ignore(), 1);
|
|
1506
|
-
import fs3 from "fs";
|
|
1507
|
-
import path3 from "path";
|
|
1508
|
-
function loadGitignore(rootDir) {
|
|
1509
|
-
const ig = (0, import_ignore.default)();
|
|
1510
|
-
const gitignorePath = path3.join(rootDir, ".gitignore");
|
|
1511
|
-
if (fs3.existsSync(gitignorePath)) {
|
|
1512
|
-
const content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
1513
|
-
ig.add(content);
|
|
1514
|
-
}
|
|
1515
|
-
return ig;
|
|
1516
|
-
}
|
|
1517
|
-
function isPathIgnored(ig, relativePath) {
|
|
1518
|
-
const normalized = relativePath.replace(/\\/g, "/");
|
|
1519
|
-
return ig.ignores(normalized);
|
|
1520
|
-
}
|
|
1521
|
-
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1522
|
-
const ig = loadGitignore(rootDir);
|
|
1523
|
-
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
// src/lib/git.ts
|
|
1527
1708
|
var git = simpleGit();
|
|
1528
1709
|
function isNoiseRecapPath(filePath) {
|
|
1529
1710
|
const base = path4.basename(filePath);
|
|
@@ -1540,29 +1721,19 @@ function filterRecapFiles(files) {
|
|
|
1540
1721
|
const tracked = new Set(filterTrackedPaths(paths));
|
|
1541
1722
|
return files.filter((f) => tracked.has(f.file) && !isNoiseRecapPath(f.file));
|
|
1542
1723
|
}
|
|
1543
|
-
function
|
|
1544
|
-
if (
|
|
1545
|
-
if (
|
|
1546
|
-
if (workingDir === "R") return "renamed";
|
|
1724
|
+
function inferFileStatusFromDiff(insertions, deletions) {
|
|
1725
|
+
if (deletions > 0 && insertions === 0) return "deleted";
|
|
1726
|
+
if (insertions > 0 && deletions === 0) return "new";
|
|
1547
1727
|
return "modified";
|
|
1548
1728
|
}
|
|
1549
|
-
|
|
1550
|
-
const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1551
|
-
const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
|
|
1552
|
-
const statusResult = await git.status();
|
|
1553
|
-
const statByFile = new Map(
|
|
1554
|
-
diffSummary.files.map((f) => [f.file, f])
|
|
1555
|
-
);
|
|
1729
|
+
function buildSessionDiffFromSummary(diffSummary) {
|
|
1556
1730
|
const files = filterRecapFiles(
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
status: fileStatus(f.working_dir)
|
|
1564
|
-
};
|
|
1565
|
-
})
|
|
1731
|
+
diffSummary.files.map((f) => ({
|
|
1732
|
+
file: f.file,
|
|
1733
|
+
insertions: f.insertions,
|
|
1734
|
+
deletions: f.deletions,
|
|
1735
|
+
status: inferFileStatusFromDiff(f.insertions, f.deletions)
|
|
1736
|
+
}))
|
|
1566
1737
|
);
|
|
1567
1738
|
return {
|
|
1568
1739
|
filesChanged: files.length,
|
|
@@ -1571,6 +1742,11 @@ async function getSessionDiff(since) {
|
|
|
1571
1742
|
files
|
|
1572
1743
|
};
|
|
1573
1744
|
}
|
|
1745
|
+
async function getSessionDiff(since) {
|
|
1746
|
+
const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1747
|
+
const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
|
|
1748
|
+
return buildSessionDiffFromSummary(diffSummary);
|
|
1749
|
+
}
|
|
1574
1750
|
async function getRecentCommits(count = 10, since) {
|
|
1575
1751
|
const options = { maxCount: count };
|
|
1576
1752
|
if (since) options["--since"] = since;
|
|
@@ -1621,10 +1797,10 @@ var ADR_RULES = [
|
|
|
1621
1797
|
test: (f) => /\.env\.example$|auth\/|middleware\.(ts|js)$/.test(f)
|
|
1622
1798
|
}
|
|
1623
1799
|
];
|
|
1624
|
-
function detectAdrCandidates(
|
|
1800
|
+
function detectAdrCandidates(diff2) {
|
|
1625
1801
|
const candidates = [];
|
|
1626
1802
|
for (const rule of ADR_RULES) {
|
|
1627
|
-
const matched =
|
|
1803
|
+
const matched = diff2.files.map((f) => f.file).filter(rule.test);
|
|
1628
1804
|
if (matched.length > 0) {
|
|
1629
1805
|
candidates.push({
|
|
1630
1806
|
title: rule.title,
|
|
@@ -1681,39 +1857,40 @@ function createAdrFile(cwd, title, context, decision, consequences) {
|
|
|
1681
1857
|
|
|
1682
1858
|
// src/commands/recap.ts
|
|
1683
1859
|
async function recap(options = {}) {
|
|
1684
|
-
console.log(
|
|
1860
|
+
console.log(chalk6.bold(`
|
|
1685
1861
|
${ko.recap.title}
|
|
1686
1862
|
`));
|
|
1687
1863
|
if (!await isGitRepo()) {
|
|
1688
|
-
console.log(
|
|
1864
|
+
console.log(chalk6.red(ko.recap.noRepo));
|
|
1689
1865
|
return;
|
|
1690
1866
|
}
|
|
1691
|
-
|
|
1867
|
+
printSecurityWarnings();
|
|
1868
|
+
console.log(chalk6.dim(`${ko.recap.analyzing}
|
|
1692
1869
|
`));
|
|
1693
1870
|
const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1694
|
-
const
|
|
1871
|
+
const diff2 = await getSessionDiff(since);
|
|
1695
1872
|
const commits = await getRecentCommits(10, since);
|
|
1696
|
-
if (
|
|
1697
|
-
console.log(
|
|
1873
|
+
if (diff2.filesChanged === 0 && commits.length === 0) {
|
|
1874
|
+
console.log(chalk6.yellow(ko.recap.noChanges));
|
|
1698
1875
|
return;
|
|
1699
1876
|
}
|
|
1700
|
-
console.log(
|
|
1701
|
-
console.log(` \uD30C\uC77C: ${
|
|
1702
|
-
console.log(` \uCD94\uAC00: ${
|
|
1703
|
-
if (
|
|
1704
|
-
console.log(
|
|
1705
|
-
|
|
1706
|
-
const icon = f.status === "new" ?
|
|
1877
|
+
console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1878
|
+
console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1879
|
+
console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
|
|
1880
|
+
if (diff2.files.length > 0) {
|
|
1881
|
+
console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
1882
|
+
diff2.files.slice(0, 15).forEach((f) => {
|
|
1883
|
+
const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
|
|
1707
1884
|
console.log(` ${icon} ${f.file}`);
|
|
1708
1885
|
});
|
|
1709
|
-
if (
|
|
1710
|
-
console.log(
|
|
1886
|
+
if (diff2.files.length > 15) {
|
|
1887
|
+
console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
|
|
1711
1888
|
}
|
|
1712
1889
|
}
|
|
1713
1890
|
if (commits.length > 0) {
|
|
1714
|
-
console.log(
|
|
1891
|
+
console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
1715
1892
|
commits.slice(0, 5).forEach((c) => {
|
|
1716
|
-
console.log(
|
|
1893
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1717
1894
|
});
|
|
1718
1895
|
}
|
|
1719
1896
|
console.log("");
|
|
@@ -1748,7 +1925,7 @@ ${ko.recap.title}
|
|
|
1748
1925
|
const sessionNum = existing.length + 1;
|
|
1749
1926
|
const fileName = `${today}-session-${sessionNum}.md`;
|
|
1750
1927
|
const filePath = path6.join(logDir, fileName);
|
|
1751
|
-
const fileList =
|
|
1928
|
+
const fileList = diff2.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
|
|
1752
1929
|
const commitList = commits.slice(0, 10).map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
1753
1930
|
const content = [
|
|
1754
1931
|
`# \uC138\uC158 \uB85C\uADF8 \u2014 ${today} #${sessionNum}`,
|
|
@@ -1766,7 +1943,7 @@ ${ko.recap.title}
|
|
|
1766
1943
|
answers.blockers,
|
|
1767
1944
|
"",
|
|
1768
1945
|
"## \uBCC0\uACBD \uD30C\uC77C",
|
|
1769
|
-
`\uCD1D ${
|
|
1946
|
+
`\uCD1D ${diff2.filesChanged}\uAC1C \uD30C\uC77C (+${diff2.insertions} -${diff2.deletions})`,
|
|
1770
1947
|
"",
|
|
1771
1948
|
"| \uD30C\uC77C | \uC0C1\uD0DC |",
|
|
1772
1949
|
"|------|------|",
|
|
@@ -1779,13 +1956,13 @@ ${ko.recap.title}
|
|
|
1779
1956
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1780
1957
|
].join("\n");
|
|
1781
1958
|
fs5.writeFileSync(filePath, content, "utf-8");
|
|
1782
|
-
const adrCandidates = detectAdrCandidates(
|
|
1959
|
+
const adrCandidates = detectAdrCandidates(diff2);
|
|
1783
1960
|
if (adrCandidates.length > 0) {
|
|
1784
|
-
console.log(
|
|
1961
|
+
console.log(chalk6.cyan.bold(`
|
|
1785
1962
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
1786
1963
|
for (const candidate of adrCandidates) {
|
|
1787
|
-
console.log(
|
|
1788
|
-
candidate.files.forEach((f) => console.log(
|
|
1964
|
+
console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1965
|
+
candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
|
|
1789
1966
|
}
|
|
1790
1967
|
const { createAdr } = await inquirer3.prompt([{
|
|
1791
1968
|
type: "confirm",
|
|
@@ -1815,17 +1992,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1815
1992
|
adrAnswers.decision,
|
|
1816
1993
|
adrAnswers.consequences
|
|
1817
1994
|
);
|
|
1818
|
-
console.log(
|
|
1995
|
+
console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
|
|
1819
1996
|
}
|
|
1820
1997
|
}
|
|
1821
1998
|
}
|
|
1822
1999
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1823
2000
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1824
2001
|
if (troubleCommits.length > 0) {
|
|
1825
|
-
console.log(
|
|
2002
|
+
console.log(chalk6.yellow.bold(`
|
|
1826
2003
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1827
2004
|
troubleCommits.forEach((c) => {
|
|
1828
|
-
console.log(
|
|
2005
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1829
2006
|
});
|
|
1830
2007
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1831
2008
|
type: "confirm",
|
|
@@ -1876,12 +2053,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1876
2053
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1877
2054
|
].join("\n");
|
|
1878
2055
|
fs5.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1879
|
-
console.log(
|
|
2056
|
+
console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
|
|
1880
2057
|
}
|
|
1881
2058
|
}
|
|
1882
|
-
console.log(
|
|
2059
|
+
console.log(chalk6.green.bold(`
|
|
1883
2060
|
${ko.recap.done}`));
|
|
1884
|
-
console.log(
|
|
2061
|
+
console.log(chalk6.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
|
|
1885
2062
|
const claudeMdPath = path6.join(process.cwd(), "CLAUDE.md");
|
|
1886
2063
|
if (fs5.existsSync(claudeMdPath)) {
|
|
1887
2064
|
const { updateClaude } = await inquirer3.prompt([{
|
|
@@ -1901,7 +2078,7 @@ ${ko.recap.done}`));
|
|
|
1901
2078
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
1902
2079
|
);
|
|
1903
2080
|
fs5.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
1904
|
-
console.log(
|
|
2081
|
+
console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
1905
2082
|
}
|
|
1906
2083
|
}
|
|
1907
2084
|
const gitSaveCmd = process.platform === "win32" ? 'git add .; git commit -m "recap: \uC138\uC158 \uAE30\uB85D"' : 'git add . && git commit -m "recap: \uC138\uC158 \uAE30\uB85D"';
|
|
@@ -1913,7 +2090,7 @@ ${ko.recap.done}`));
|
|
|
1913
2090
|
}
|
|
1914
2091
|
|
|
1915
2092
|
// src/commands/sync.ts
|
|
1916
|
-
import
|
|
2093
|
+
import chalk7 from "chalk";
|
|
1917
2094
|
import fs6 from "fs";
|
|
1918
2095
|
import path7 from "path";
|
|
1919
2096
|
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
@@ -1983,32 +2160,32 @@ function toClaudeMd(sections, existing) {
|
|
|
1983
2160
|
return lines.join("\n");
|
|
1984
2161
|
}
|
|
1985
2162
|
async function sync() {
|
|
1986
|
-
console.log(
|
|
2163
|
+
console.log(chalk7.bold(`
|
|
1987
2164
|
${ko.sync.title}
|
|
1988
2165
|
`));
|
|
1989
2166
|
const cwd = process.cwd();
|
|
1990
2167
|
const rulesPath = path7.join(cwd, "RULES.md");
|
|
1991
2168
|
if (!fs6.existsSync(rulesPath)) {
|
|
1992
|
-
console.log(
|
|
1993
|
-
console.log(
|
|
1994
|
-
console.log(
|
|
2169
|
+
console.log(chalk7.yellow(ko.sync.noRules));
|
|
2170
|
+
console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
2171
|
+
console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
1995
2172
|
console.log("");
|
|
1996
|
-
console.log(
|
|
1997
|
-
console.log(
|
|
1998
|
-
console.log(
|
|
1999
|
-
console.log(
|
|
2000
|
-
console.log(
|
|
2001
|
-
console.log(
|
|
2173
|
+
console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
2174
|
+
console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
2175
|
+
console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
2176
|
+
console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
2177
|
+
console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
2178
|
+
console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
2002
2179
|
return;
|
|
2003
2180
|
}
|
|
2004
2181
|
const rulesContent = fs6.readFileSync(rulesPath, "utf-8");
|
|
2005
2182
|
const sections = parseRulesMd(rulesContent);
|
|
2006
|
-
console.log(
|
|
2183
|
+
console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
2007
2184
|
const firstLine = rulesContent.split("\n")[0];
|
|
2008
2185
|
const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
2009
2186
|
const cursorrulesPath = path7.join(cwd, ".cursorrules");
|
|
2010
2187
|
fs6.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
|
|
2011
|
-
console.log(
|
|
2188
|
+
console.log(chalk7.green(` ${ko.sync.cursorrulesDone}`));
|
|
2012
2189
|
const claudePath = path7.join(cwd, "CLAUDE.md");
|
|
2013
2190
|
const existingClaude = fs6.existsSync(claudePath) ? fs6.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
2014
2191
|
|
|
@@ -2018,11 +2195,11 @@ ${ko.sync.title}
|
|
|
2018
2195
|
- **\uB2E4\uC74C \uC561\uC158:** __FILL__
|
|
2019
2196
|
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
2020
2197
|
fs6.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
|
|
2021
|
-
console.log(
|
|
2022
|
-
console.log(
|
|
2198
|
+
console.log(chalk7.green(` ${ko.sync.claudeDone}`));
|
|
2199
|
+
console.log(chalk7.bold.green(`
|
|
2023
2200
|
${ko.sync.done}`));
|
|
2024
|
-
console.log(
|
|
2025
|
-
console.log(
|
|
2201
|
+
console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
2202
|
+
console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
2026
2203
|
printNextStep({
|
|
2027
2204
|
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
2028
2205
|
command: "vhk \uC810\uAC80",
|
|
@@ -2031,7 +2208,7 @@ ${ko.sync.done}`));
|
|
|
2031
2208
|
}
|
|
2032
2209
|
|
|
2033
2210
|
// src/commands/check.ts
|
|
2034
|
-
import
|
|
2211
|
+
import chalk8 from "chalk";
|
|
2035
2212
|
import path9 from "path";
|
|
2036
2213
|
import fs8 from "fs";
|
|
2037
2214
|
|
|
@@ -2090,15 +2267,6 @@ function parseRules(rulesPath) {
|
|
|
2090
2267
|
));
|
|
2091
2268
|
}
|
|
2092
2269
|
}
|
|
2093
|
-
if (/반드시|필수|항상|must|always|required/i.test(ruleText)) {
|
|
2094
|
-
rules.push({
|
|
2095
|
-
id: `required-${ruleIndex}`,
|
|
2096
|
-
section: currentSection,
|
|
2097
|
-
type: "custom",
|
|
2098
|
-
description: ruleText,
|
|
2099
|
-
check: () => []
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
2270
|
}
|
|
2103
2271
|
return rules;
|
|
2104
2272
|
}
|
|
@@ -2199,22 +2367,22 @@ function escapeRegex(str) {
|
|
|
2199
2367
|
|
|
2200
2368
|
// src/commands/check.ts
|
|
2201
2369
|
async function check() {
|
|
2202
|
-
console.log(
|
|
2370
|
+
console.log(chalk8.bold(`
|
|
2203
2371
|
${ko.check.title}
|
|
2204
2372
|
`));
|
|
2205
2373
|
const cwd = process.cwd();
|
|
2206
2374
|
const rulesPath = path9.join(cwd, "RULES.md");
|
|
2207
2375
|
if (!fs8.existsSync(rulesPath)) {
|
|
2208
|
-
console.log(
|
|
2209
|
-
console.log(
|
|
2376
|
+
console.log(chalk8.yellow(ko.check.noRules));
|
|
2377
|
+
console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
2210
2378
|
return;
|
|
2211
2379
|
}
|
|
2212
2380
|
const rules = parseRules(rulesPath);
|
|
2213
|
-
console.log(
|
|
2381
|
+
console.log(chalk8.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
|
|
2214
2382
|
`));
|
|
2215
2383
|
if (rules.length === 0) {
|
|
2216
|
-
console.log(
|
|
2217
|
-
console.log(
|
|
2384
|
+
console.log(chalk8.yellow(ko.check.noAutoRules));
|
|
2385
|
+
console.log(chalk8.dim(" RULES.md\uC5D0 \uD30C\uC77C \uC774\uB984\xB7\uD3F4\uB354 \uADDC\uCE59\uC744 \uC801\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C \uC810\uAC80\uD574\uC694."));
|
|
2218
2386
|
return;
|
|
2219
2387
|
}
|
|
2220
2388
|
const allViolations = [];
|
|
@@ -2222,13 +2390,13 @@ ${ko.check.title}
|
|
|
2222
2390
|
for (const rule of rules) {
|
|
2223
2391
|
const violations = rule.check(cwd);
|
|
2224
2392
|
if (violations.length === 0) {
|
|
2225
|
-
console.log(
|
|
2393
|
+
console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`));
|
|
2226
2394
|
passCount++;
|
|
2227
2395
|
} else {
|
|
2228
|
-
console.log(
|
|
2396
|
+
console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
2229
2397
|
violations.forEach((v) => {
|
|
2230
|
-
const loc = v.file ?
|
|
2231
|
-
const icon = v.severity === "error" ?
|
|
2398
|
+
const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2399
|
+
const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
|
|
2232
2400
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
2233
2401
|
});
|
|
2234
2402
|
allViolations.push(...violations);
|
|
@@ -2238,17 +2406,17 @@ ${ko.check.title}
|
|
|
2238
2406
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
2239
2407
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
2240
2408
|
if (allViolations.length === 0) {
|
|
2241
|
-
console.log(
|
|
2409
|
+
console.log(chalk8.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
|
|
2242
2410
|
printNextStep({
|
|
2243
2411
|
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
2244
2412
|
command: "vhk \uBCF4\uC548 scan",
|
|
2245
2413
|
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
2246
2414
|
});
|
|
2247
2415
|
} else {
|
|
2248
|
-
console.log(
|
|
2249
|
-
console.log(` \uADDC\uCE59: ${
|
|
2250
|
-
if (errors > 0) console.log(` ${
|
|
2251
|
-
if (warnings > 0) console.log(` ${
|
|
2416
|
+
console.log(chalk8.bold(ko.check.summary));
|
|
2417
|
+
console.log(` \uADDC\uCE59: ${chalk8.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk8.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk8.red(String(allViolations.length))}\uAC74`);
|
|
2418
|
+
if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
2419
|
+
if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
2252
2420
|
printNextStep({
|
|
2253
2421
|
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
2254
2422
|
command: "vhk \uC810\uAC80",
|
|
@@ -2261,10 +2429,13 @@ ${ko.check.title}
|
|
|
2261
2429
|
}
|
|
2262
2430
|
|
|
2263
2431
|
// src/commands/secure.ts
|
|
2264
|
-
import
|
|
2265
|
-
import
|
|
2432
|
+
import chalk9 from "chalk";
|
|
2433
|
+
import fs11 from "fs";
|
|
2266
2434
|
import path11 from "path";
|
|
2267
2435
|
|
|
2436
|
+
// src/lib/scan-secrets.ts
|
|
2437
|
+
import fs10 from "fs";
|
|
2438
|
+
|
|
2268
2439
|
// src/lib/secret-patterns.ts
|
|
2269
2440
|
var SECRET_PATTERNS = [
|
|
2270
2441
|
{
|
|
@@ -2289,7 +2460,7 @@ var SECRET_PATTERNS = [
|
|
|
2289
2460
|
id: "notion-token",
|
|
2290
2461
|
name: "Notion Integration Token",
|
|
2291
2462
|
severity: "critical",
|
|
2292
|
-
pattern: /secret_[A-Za-z0-9]{
|
|
2463
|
+
pattern: /secret_[A-Za-z0-9]{40,50}/
|
|
2293
2464
|
},
|
|
2294
2465
|
{
|
|
2295
2466
|
id: "github-token",
|
|
@@ -2301,7 +2472,7 @@ var SECRET_PATTERNS = [
|
|
|
2301
2472
|
id: "openai-key",
|
|
2302
2473
|
name: "OpenAI API Key",
|
|
2303
2474
|
severity: "critical",
|
|
2304
|
-
pattern:
|
|
2475
|
+
pattern: /\bsk-(?:proj-|ant-api03-|live-)[A-Za-z0-9_-]{16,}\b/
|
|
2305
2476
|
},
|
|
2306
2477
|
{
|
|
2307
2478
|
id: "generic-api-key",
|
|
@@ -2403,69 +2574,88 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
|
2403
2574
|
walk(rootDir);
|
|
2404
2575
|
}
|
|
2405
2576
|
|
|
2406
|
-
// src/
|
|
2407
|
-
var
|
|
2577
|
+
// src/lib/scan-secrets.ts
|
|
2578
|
+
var MAX_SECRET_FINDINGS = 200;
|
|
2408
2579
|
var MAX_LINE_CHARS = 4e3;
|
|
2580
|
+
function globalPattern(pattern) {
|
|
2581
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
2582
|
+
return new RegExp(pattern.source, flags);
|
|
2583
|
+
}
|
|
2584
|
+
function findSecretsInLine(line, relPath, lineNum) {
|
|
2585
|
+
const found = [];
|
|
2586
|
+
const trimmed = line.trim();
|
|
2587
|
+
if (trimmed.startsWith("//") && trimmed.includes("example")) return found;
|
|
2588
|
+
if (trimmed.startsWith("#") && trimmed.includes("example")) return found;
|
|
2589
|
+
if (line.length > MAX_LINE_CHARS) return found;
|
|
2590
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
2591
|
+
const regex = globalPattern(pattern.pattern);
|
|
2592
|
+
for (const match of line.matchAll(regex)) {
|
|
2593
|
+
found.push({
|
|
2594
|
+
patternId: pattern.id,
|
|
2595
|
+
patternName: pattern.name,
|
|
2596
|
+
severity: pattern.severity,
|
|
2597
|
+
file: relPath,
|
|
2598
|
+
line: lineNum,
|
|
2599
|
+
match: maskSecret(match[0])
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return found;
|
|
2604
|
+
}
|
|
2605
|
+
function scanProjectForSecrets(cwd) {
|
|
2606
|
+
const findings = [];
|
|
2607
|
+
let scannedFiles = 0;
|
|
2608
|
+
let truncated = false;
|
|
2609
|
+
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
2610
|
+
scannedFiles++;
|
|
2611
|
+
const content = fs10.readFileSync(filePath, "utf-8");
|
|
2612
|
+
const lines = content.split("\n");
|
|
2613
|
+
lines.forEach((line, idx) => {
|
|
2614
|
+
if (truncated) return;
|
|
2615
|
+
const lineFindings = findSecretsInLine(line, relPath, idx + 1);
|
|
2616
|
+
for (const f of lineFindings) {
|
|
2617
|
+
findings.push(f);
|
|
2618
|
+
if (findings.length >= MAX_SECRET_FINDINGS) {
|
|
2619
|
+
truncated = true;
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
return { findings, scannedFiles, truncated };
|
|
2626
|
+
}
|
|
2627
|
+
function filterSevereFindings(findings) {
|
|
2628
|
+
return findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/commands/secure.ts
|
|
2409
2632
|
async function secure() {
|
|
2410
|
-
console.log(
|
|
2633
|
+
console.log(chalk9.bold(`
|
|
2411
2634
|
${ko.secure.title}
|
|
2412
2635
|
`));
|
|
2413
2636
|
const cwd = process.cwd();
|
|
2414
|
-
const findings = [];
|
|
2415
|
-
let scannedFiles = 0;
|
|
2416
|
-
let truncated = false;
|
|
2417
2637
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
2418
|
-
const hasGitignore =
|
|
2638
|
+
const hasGitignore = fs11.existsSync(gitignorePath);
|
|
2419
2639
|
if (!hasGitignore) {
|
|
2420
|
-
console.log(
|
|
2421
|
-
console.log(
|
|
2640
|
+
console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
|
|
2641
|
+
console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
2422
2642
|
} else {
|
|
2423
|
-
const gitignoreContent =
|
|
2643
|
+
const gitignoreContent = fs11.readFileSync(gitignorePath, "utf-8");
|
|
2424
2644
|
if (!gitignoreContent.includes(".env")) {
|
|
2425
|
-
console.log(
|
|
2426
|
-
console.log(
|
|
2645
|
+
console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2646
|
+
console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
2427
2647
|
}
|
|
2428
2648
|
}
|
|
2429
|
-
console.log(
|
|
2649
|
+
console.log(chalk9.dim(` ${ko.secure.scanning}
|
|
2430
2650
|
`));
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
const content = fs10.readFileSync(filePath, "utf-8");
|
|
2434
|
-
const lines = content.split("\n");
|
|
2435
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
2436
|
-
if (truncated) break;
|
|
2437
|
-
lines.forEach((line, idx) => {
|
|
2438
|
-
if (truncated) return;
|
|
2439
|
-
if (line.length > MAX_LINE_CHARS) return;
|
|
2440
|
-
const trimmed = line.trim();
|
|
2441
|
-
if (trimmed.startsWith("//") && trimmed.includes("example")) return;
|
|
2442
|
-
if (trimmed.startsWith("#") && trimmed.includes("example")) return;
|
|
2443
|
-
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
2444
|
-
let match;
|
|
2445
|
-
while ((match = regex.exec(line)) !== null) {
|
|
2446
|
-
findings.push({
|
|
2447
|
-
patternId: pattern.id,
|
|
2448
|
-
patternName: pattern.name,
|
|
2449
|
-
severity: pattern.severity,
|
|
2450
|
-
file: relPath,
|
|
2451
|
-
line: idx + 1,
|
|
2452
|
-
match: maskSecret(match[0])
|
|
2453
|
-
});
|
|
2454
|
-
if (findings.length >= MAX_FINDINGS) {
|
|
2455
|
-
truncated = true;
|
|
2456
|
-
return;
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
});
|
|
2460
|
-
}
|
|
2461
|
-
});
|
|
2462
|
-
console.log(chalk8.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2651
|
+
const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
|
|
2652
|
+
console.log(chalk9.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2463
2653
|
if (truncated) {
|
|
2464
|
-
console.log(
|
|
2654
|
+
console.log(chalk9.yellow(` \u26A0\uFE0F \uACB0\uACFC ${MAX_SECRET_FINDINGS}\uAC74\uC5D0\uC11C \uCD9C\uB825\uC744 \uC81C\uD55C\uD588\uC2B5\uB2C8\uB2E4. lock \uD30C\uC77C \uB4F1\uC740 \uC790\uB3D9 \uC81C\uC678\uB429\uB2C8\uB2E4.`));
|
|
2465
2655
|
}
|
|
2466
2656
|
console.log("");
|
|
2467
2657
|
if (findings.length === 0) {
|
|
2468
|
-
console.log(
|
|
2658
|
+
console.log(chalk9.green.bold(` ${ko.secure.clean}`));
|
|
2469
2659
|
printNextStep({
|
|
2470
2660
|
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
2471
2661
|
command: "vhk \uC815\uB9AC",
|
|
@@ -2477,45 +2667,45 @@ ${ko.secure.title}
|
|
|
2477
2667
|
const high = findings.filter((f) => f.severity === "high");
|
|
2478
2668
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
2479
2669
|
if (critical.length > 0) {
|
|
2480
|
-
console.log(
|
|
2670
|
+
console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
2481
2671
|
critical.forEach((f) => {
|
|
2482
|
-
console.log(
|
|
2483
|
-
console.log(
|
|
2672
|
+
console.log(chalk9.red(` \u2716 ${f.patternName}`));
|
|
2673
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2484
2674
|
});
|
|
2485
2675
|
console.log("");
|
|
2486
2676
|
}
|
|
2487
2677
|
if (high.length > 0) {
|
|
2488
|
-
console.log(
|
|
2678
|
+
console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
2489
2679
|
high.forEach((f) => {
|
|
2490
|
-
console.log(
|
|
2491
|
-
console.log(
|
|
2680
|
+
console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
|
|
2681
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2492
2682
|
});
|
|
2493
2683
|
console.log("");
|
|
2494
2684
|
}
|
|
2495
2685
|
if (medium.length > 0) {
|
|
2496
|
-
console.log(
|
|
2686
|
+
console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
2497
2687
|
medium.forEach((f) => {
|
|
2498
|
-
console.log(
|
|
2499
|
-
console.log(
|
|
2688
|
+
console.log(chalk9.blue(` \u2139 ${f.patternName}`));
|
|
2689
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2500
2690
|
});
|
|
2501
2691
|
console.log("");
|
|
2502
2692
|
}
|
|
2503
|
-
console.log(
|
|
2504
|
-
console.log(` \uCD1D ${
|
|
2693
|
+
console.log(chalk9.bold(` ${ko.secure.summary}`));
|
|
2694
|
+
console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
2505
2695
|
console.log("");
|
|
2506
|
-
console.log(
|
|
2507
|
-
console.log(
|
|
2508
|
-
console.log(
|
|
2509
|
-
console.log(
|
|
2510
|
-
if (critical.length > 0) {
|
|
2696
|
+
console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
2697
|
+
console.log(chalk9.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
|
|
2698
|
+
console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
2699
|
+
console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
2700
|
+
if (critical.length > 0 || high.length > 0) {
|
|
2511
2701
|
process.exitCode = 1;
|
|
2512
2702
|
}
|
|
2513
2703
|
}
|
|
2514
2704
|
|
|
2515
2705
|
// src/commands/doctor.ts
|
|
2516
|
-
import
|
|
2706
|
+
import chalk10 from "chalk";
|
|
2517
2707
|
import { execSync } from "child_process";
|
|
2518
|
-
import
|
|
2708
|
+
import fs12 from "fs";
|
|
2519
2709
|
import path12 from "path";
|
|
2520
2710
|
import { fileURLToPath } from "url";
|
|
2521
2711
|
function checkCommand(name, command, hint) {
|
|
@@ -2534,8 +2724,8 @@ function getVhkVersion() {
|
|
|
2534
2724
|
];
|
|
2535
2725
|
for (const pkgPath of candidates) {
|
|
2536
2726
|
try {
|
|
2537
|
-
if (
|
|
2538
|
-
const pkg = JSON.parse(
|
|
2727
|
+
if (fs12.existsSync(pkgPath)) {
|
|
2728
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
2539
2729
|
return pkg.version;
|
|
2540
2730
|
}
|
|
2541
2731
|
} catch {
|
|
@@ -2545,7 +2735,7 @@ function getVhkVersion() {
|
|
|
2545
2735
|
return void 0;
|
|
2546
2736
|
}
|
|
2547
2737
|
async function doctor() {
|
|
2548
|
-
console.log(
|
|
2738
|
+
console.log(chalk10.bold(`
|
|
2549
2739
|
${ko.doctor.title}
|
|
2550
2740
|
`));
|
|
2551
2741
|
const checks = [
|
|
@@ -2557,22 +2747,22 @@ ${ko.doctor.title}
|
|
|
2557
2747
|
let allOk = true;
|
|
2558
2748
|
for (const check2 of checks) {
|
|
2559
2749
|
if (check2.ok) {
|
|
2560
|
-
console.log(
|
|
2750
|
+
console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
|
|
2561
2751
|
} else {
|
|
2562
|
-
console.log(
|
|
2563
|
-
console.log(
|
|
2752
|
+
console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
2753
|
+
console.log(chalk10.dim(` \u2192 ${check2.hint}`));
|
|
2564
2754
|
allOk = false;
|
|
2565
2755
|
}
|
|
2566
2756
|
}
|
|
2567
2757
|
console.log("");
|
|
2568
2758
|
const vhkVersion = getVhkVersion();
|
|
2569
2759
|
if (vhkVersion) {
|
|
2570
|
-
console.log(
|
|
2760
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
|
|
2571
2761
|
} else {
|
|
2572
|
-
console.log(
|
|
2762
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2573
2763
|
}
|
|
2574
2764
|
console.log("");
|
|
2575
|
-
console.log(
|
|
2765
|
+
console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
|
|
2576
2766
|
const cwd = process.cwd();
|
|
2577
2767
|
const projectFiles = [
|
|
2578
2768
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -2582,32 +2772,32 @@ ${ko.doctor.title}
|
|
|
2582
2772
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2583
2773
|
];
|
|
2584
2774
|
for (const file of projectFiles) {
|
|
2585
|
-
const exists =
|
|
2775
|
+
const exists = fs12.existsSync(path12.join(cwd, file.name));
|
|
2586
2776
|
if (exists) {
|
|
2587
|
-
console.log(
|
|
2777
|
+
console.log(chalk10.green(` \u2705 ${file.name}`));
|
|
2588
2778
|
if (file.name === ".env") {
|
|
2589
2779
|
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
2590
|
-
if (
|
|
2591
|
-
const gitignore =
|
|
2780
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
2781
|
+
const gitignore = fs12.readFileSync(gitignorePath, "utf-8");
|
|
2592
2782
|
if (!gitignore.includes(".env")) {
|
|
2593
|
-
console.log(
|
|
2783
|
+
console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2594
2784
|
}
|
|
2595
2785
|
}
|
|
2596
2786
|
}
|
|
2597
2787
|
} else {
|
|
2598
|
-
console.log(
|
|
2788
|
+
console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
|
|
2599
2789
|
}
|
|
2600
2790
|
}
|
|
2601
2791
|
console.log("");
|
|
2602
2792
|
if (allOk) {
|
|
2603
|
-
console.log(
|
|
2793
|
+
console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
|
|
2604
2794
|
printNextStep({
|
|
2605
2795
|
message: ko.doctor.nextOkMessage,
|
|
2606
2796
|
command: "vhk \uC2DC\uC791",
|
|
2607
2797
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2608
2798
|
});
|
|
2609
2799
|
} else {
|
|
2610
|
-
console.log(
|
|
2800
|
+
console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2611
2801
|
printNextStep({
|
|
2612
2802
|
message: ko.doctor.nextRetryMessage,
|
|
2613
2803
|
command: "vhk doctor",
|
|
@@ -2618,9 +2808,9 @@ ${ko.doctor.title}
|
|
|
2618
2808
|
}
|
|
2619
2809
|
|
|
2620
2810
|
// src/commands/ship.ts
|
|
2621
|
-
import
|
|
2811
|
+
import chalk11 from "chalk";
|
|
2622
2812
|
import inquirer4 from "inquirer";
|
|
2623
|
-
import
|
|
2813
|
+
import fs13 from "fs";
|
|
2624
2814
|
import path13 from "path";
|
|
2625
2815
|
var CHECKLIST = [
|
|
2626
2816
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
@@ -2634,29 +2824,29 @@ function sanitizeVersion(version) {
|
|
|
2634
2824
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2635
2825
|
}
|
|
2636
2826
|
async function ship() {
|
|
2637
|
-
console.log(
|
|
2827
|
+
console.log(chalk11.bold(`
|
|
2638
2828
|
${ko.ship.title}
|
|
2639
2829
|
`));
|
|
2640
2830
|
const cwd = process.cwd();
|
|
2641
|
-
console.log(
|
|
2831
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
|
|
2642
2832
|
`));
|
|
2643
2833
|
const { passed } = await inquirer4.prompt([{
|
|
2644
2834
|
type: "checkbox",
|
|
2645
2835
|
name: "passed",
|
|
2646
2836
|
message: ko.ship.checkboxPrompt,
|
|
2647
2837
|
choices: CHECKLIST.map((c) => ({
|
|
2648
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
2838
|
+
name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2649
2839
|
value: c.id
|
|
2650
2840
|
}))
|
|
2651
2841
|
}]);
|
|
2652
2842
|
const allPassed = passed.length === CHECKLIST.length;
|
|
2653
2843
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2654
2844
|
if (!allPassed) {
|
|
2655
|
-
console.log(
|
|
2845
|
+
console.log(chalk11.yellow(`
|
|
2656
2846
|
${ko.ship.incompleteHeader}`));
|
|
2657
2847
|
skipped.forEach((s) => {
|
|
2658
|
-
console.log(
|
|
2659
|
-
console.log(
|
|
2848
|
+
console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
2849
|
+
console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2660
2850
|
});
|
|
2661
2851
|
const { proceed } = await inquirer4.prompt([{
|
|
2662
2852
|
type: "confirm",
|
|
@@ -2673,13 +2863,13 @@ ${ko.ship.title}
|
|
|
2673
2863
|
return;
|
|
2674
2864
|
}
|
|
2675
2865
|
} else {
|
|
2676
|
-
console.log(
|
|
2866
|
+
console.log(chalk11.green(`
|
|
2677
2867
|
${ko.ship.allPassed}
|
|
2678
2868
|
`));
|
|
2679
2869
|
}
|
|
2680
|
-
console.log(
|
|
2870
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.retro}
|
|
2681
2871
|
`));
|
|
2682
|
-
console.log(
|
|
2872
|
+
console.log(chalk11.dim(` ${ko.ship.versionHint}`));
|
|
2683
2873
|
const retro = await inquirer4.prompt([
|
|
2684
2874
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2685
2875
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
@@ -2688,7 +2878,7 @@ ${ko.ship.title}
|
|
|
2688
2878
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2689
2879
|
]);
|
|
2690
2880
|
const buildLogDir = path13.join(cwd, "docs", "build-log");
|
|
2691
|
-
if (!
|
|
2881
|
+
if (!fs13.existsSync(buildLogDir)) fs13.mkdirSync(buildLogDir, { recursive: true });
|
|
2692
2882
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2693
2883
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2694
2884
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
@@ -2721,8 +2911,8 @@ ${ko.ship.title}
|
|
|
2721
2911
|
"---",
|
|
2722
2912
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2723
2913
|
].join("\n");
|
|
2724
|
-
|
|
2725
|
-
console.log(
|
|
2914
|
+
fs13.writeFileSync(filePath, content, "utf-8");
|
|
2915
|
+
console.log(chalk11.green(`
|
|
2726
2916
|
${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
|
|
2727
2917
|
printNextStep({
|
|
2728
2918
|
message: ko.ship.deployMessage,
|
|
@@ -2732,6 +2922,498 @@ ${ko.ship.title}
|
|
|
2732
2922
|
});
|
|
2733
2923
|
}
|
|
2734
2924
|
|
|
2925
|
+
// src/commands/save.ts
|
|
2926
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2927
|
+
import chalk12 from "chalk";
|
|
2928
|
+
import ora from "ora";
|
|
2929
|
+
import inquirer5 from "inquirer";
|
|
2930
|
+
|
|
2931
|
+
// src/lib/git-porcelain.ts
|
|
2932
|
+
function normalizePorcelain(raw) {
|
|
2933
|
+
return raw.replace(/\r\n/g, "\n").trimEnd();
|
|
2934
|
+
}
|
|
2935
|
+
function parsePorcelainLines(raw) {
|
|
2936
|
+
return normalizePorcelain(raw).split("\n").filter(Boolean);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// src/lib/git-repo.ts
|
|
2940
|
+
import { execFileSync } from "child_process";
|
|
2941
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
2942
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2943
|
+
encoding: "utf-8",
|
|
2944
|
+
cwd,
|
|
2945
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2946
|
+
}).trim();
|
|
2947
|
+
}
|
|
2948
|
+
function gitOut(args, cwd) {
|
|
2949
|
+
return execFileSync("git", args, {
|
|
2950
|
+
encoding: "utf-8",
|
|
2951
|
+
cwd,
|
|
2952
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
function gitRun(args, cwd) {
|
|
2956
|
+
execFileSync("git", args, { stdio: "pipe", cwd });
|
|
2957
|
+
}
|
|
2958
|
+
function getExecErrorMessage(err) {
|
|
2959
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
2960
|
+
const stderr = err.stderr;
|
|
2961
|
+
if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
|
|
2962
|
+
if (typeof stderr === "string") return stderr.trim();
|
|
2963
|
+
}
|
|
2964
|
+
return err instanceof Error ? err.message : String(err);
|
|
2965
|
+
}
|
|
2966
|
+
function hasGitRemote(cwd) {
|
|
2967
|
+
try {
|
|
2968
|
+
return gitOut(["remote"], cwd).trim().length > 0;
|
|
2969
|
+
} catch {
|
|
2970
|
+
return false;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
function countLocalCommits(cwd) {
|
|
2974
|
+
try {
|
|
2975
|
+
const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
|
|
2976
|
+
return parseInt(out, 10) || 0;
|
|
2977
|
+
} catch {
|
|
2978
|
+
return 0;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/commands/save.ts
|
|
2983
|
+
function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
|
|
2984
|
+
const y = date.getFullYear();
|
|
2985
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2986
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
2987
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
2988
|
+
const min = String(date.getMinutes()).padStart(2, "0");
|
|
2989
|
+
return `\u2728 vhk save: ${y}-${m}-${d} ${h}:${min}`;
|
|
2990
|
+
}
|
|
2991
|
+
function statusIcon(code) {
|
|
2992
|
+
if (code.includes("M")) return "\u270F\uFE0F";
|
|
2993
|
+
if (code.includes("A") || code.includes("?")) return "\u2795";
|
|
2994
|
+
if (code.includes("D")) return "\u{1F5D1}\uFE0F";
|
|
2995
|
+
return "\u{1F4C4}";
|
|
2996
|
+
}
|
|
2997
|
+
async function save() {
|
|
2998
|
+
console.log(chalk12.bold(`
|
|
2999
|
+
\u{1F4BE} ${t("save.title")}`));
|
|
3000
|
+
console.log(chalk12.gray("\u2500".repeat(40)));
|
|
3001
|
+
let gitRoot;
|
|
3002
|
+
try {
|
|
3003
|
+
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3004
|
+
gitRoot = getGitRoot();
|
|
3005
|
+
} catch {
|
|
3006
|
+
console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
console.log(chalk12.cyan(`
|
|
3010
|
+
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
3011
|
+
printSecurityWarnings(gitRoot);
|
|
3012
|
+
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
3013
|
+
if (severe.length > 0) {
|
|
3014
|
+
console.log(chalk12.red(`
|
|
3015
|
+
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
3016
|
+
severe.slice(0, 5).forEach((f) => {
|
|
3017
|
+
console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
3018
|
+
});
|
|
3019
|
+
if (severe.length > 5) {
|
|
3020
|
+
console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
3021
|
+
}
|
|
3022
|
+
const { proceed } = await inquirer5.prompt([{
|
|
3023
|
+
type: "confirm",
|
|
3024
|
+
name: "proceed",
|
|
3025
|
+
message: t("save.secretsConfirm"),
|
|
3026
|
+
default: false
|
|
3027
|
+
}]);
|
|
3028
|
+
if (!proceed) {
|
|
3029
|
+
console.log(chalk12.gray(t("save.cancelled")));
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
3034
|
+
if (lines.length === 0) {
|
|
3035
|
+
console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
console.log(chalk12.cyan(`
|
|
3039
|
+
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
3040
|
+
lines.forEach((line) => {
|
|
3041
|
+
const code = line.substring(0, 2);
|
|
3042
|
+
const name = line.substring(3);
|
|
3043
|
+
console.log(` ${statusIcon(code)} ${name}`);
|
|
3044
|
+
});
|
|
3045
|
+
const { message } = await inquirer5.prompt([{
|
|
3046
|
+
type: "input",
|
|
3047
|
+
name: "message",
|
|
3048
|
+
message: t("save.commitMessage"),
|
|
3049
|
+
default: formatDefaultCommitMessage()
|
|
3050
|
+
}]);
|
|
3051
|
+
const spinner = ora(t("save.saving")).start();
|
|
3052
|
+
let didAdd = false;
|
|
3053
|
+
try {
|
|
3054
|
+
gitRun(["add", "."], gitRoot);
|
|
3055
|
+
didAdd = true;
|
|
3056
|
+
gitRun(["commit", "-m", message], gitRoot);
|
|
3057
|
+
spinner.text = t("save.pushing");
|
|
3058
|
+
if (!hasGitRemote(gitRoot)) {
|
|
3059
|
+
spinner.succeed(t("save.successLocal"));
|
|
3060
|
+
console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
3061
|
+
} else {
|
|
3062
|
+
try {
|
|
3063
|
+
gitRun(["push"], gitRoot);
|
|
3064
|
+
spinner.succeed(t("save.successWithPush"));
|
|
3065
|
+
} catch (pushErr) {
|
|
3066
|
+
spinner.fail(t("save.pushFailed"));
|
|
3067
|
+
console.log(chalk12.red(getExecErrorMessage(pushErr)));
|
|
3068
|
+
console.log(chalk12.yellow(`
|
|
3069
|
+
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
3070
|
+
process.exitCode = 1;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
if (process.exitCode !== 1) {
|
|
3074
|
+
console.log(chalk12.green(`
|
|
3075
|
+
\u2705 ${t("save.done", lines.length)}`));
|
|
3076
|
+
} else {
|
|
3077
|
+
console.log(chalk12.green(`
|
|
3078
|
+
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
3079
|
+
}
|
|
3080
|
+
} catch (err) {
|
|
3081
|
+
spinner.fail(t("save.failed"));
|
|
3082
|
+
console.log(chalk12.red(getExecErrorMessage(err)));
|
|
3083
|
+
if (didAdd) {
|
|
3084
|
+
try {
|
|
3085
|
+
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3086
|
+
if (staged) {
|
|
3087
|
+
console.log(chalk12.yellow(`
|
|
3088
|
+
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
process.exitCode = 1;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
// src/commands/undo.ts
|
|
3098
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3099
|
+
import chalk13 from "chalk";
|
|
3100
|
+
import inquirer6 from "inquirer";
|
|
3101
|
+
function parseRecentCommits(logOutput) {
|
|
3102
|
+
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3103
|
+
}
|
|
3104
|
+
function countUnpushedCommits(gitRoot) {
|
|
3105
|
+
const cwd = gitRoot ?? process.cwd();
|
|
3106
|
+
try {
|
|
3107
|
+
const out = gitOut(["rev-list", "--count", "@{u}..HEAD"], cwd).trim();
|
|
3108
|
+
return parseInt(out, 10) || 0;
|
|
3109
|
+
} catch {
|
|
3110
|
+
return -1;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
function willUndoPushedCommits(undoCount, unpushedCount) {
|
|
3114
|
+
if (unpushedCount < 0) return false;
|
|
3115
|
+
return undoCount > unpushedCount;
|
|
3116
|
+
}
|
|
3117
|
+
function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
3118
|
+
if (willUndoPushedCommits(undoCount, unpushedCount)) return true;
|
|
3119
|
+
if (unpushedCount < 0 && hasRemote) return true;
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
async function undo() {
|
|
3123
|
+
console.log(chalk13.bold(`
|
|
3124
|
+
\u23EA ${t("undo.title")}`));
|
|
3125
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
3126
|
+
let gitRoot;
|
|
3127
|
+
try {
|
|
3128
|
+
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3129
|
+
gitRoot = getGitRoot();
|
|
3130
|
+
} catch {
|
|
3131
|
+
console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
let logOutput;
|
|
3135
|
+
try {
|
|
3136
|
+
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
3137
|
+
} catch {
|
|
3138
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
const commits = parseRecentCommits(logOutput);
|
|
3142
|
+
if (commits.length === 0) {
|
|
3143
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
console.log(chalk13.cyan(`
|
|
3147
|
+
${t("undo.recentHeader")}`));
|
|
3148
|
+
commits.forEach((c, i) => {
|
|
3149
|
+
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
3150
|
+
});
|
|
3151
|
+
const maxUndo = commits.length;
|
|
3152
|
+
const { count } = await inquirer6.prompt([{
|
|
3153
|
+
type: "number",
|
|
3154
|
+
name: "count",
|
|
3155
|
+
message: t("undo.howMany"),
|
|
3156
|
+
default: 1,
|
|
3157
|
+
min: 1,
|
|
3158
|
+
max: maxUndo
|
|
3159
|
+
}]);
|
|
3160
|
+
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
3161
|
+
const headCount = countLocalCommits(gitRoot);
|
|
3162
|
+
if (undoCount >= headCount) {
|
|
3163
|
+
console.log(chalk13.yellow(`
|
|
3164
|
+
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const unpushed = countUnpushedCommits(gitRoot);
|
|
3168
|
+
const remote = hasGitRemote(gitRoot);
|
|
3169
|
+
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3170
|
+
if (risky) {
|
|
3171
|
+
if (unpushed < 0) {
|
|
3172
|
+
console.log(chalk13.red(`
|
|
3173
|
+
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3174
|
+
} else {
|
|
3175
|
+
console.log(chalk13.red(`
|
|
3176
|
+
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
const { confirm } = await inquirer6.prompt([{
|
|
3180
|
+
type: "confirm",
|
|
3181
|
+
name: "confirm",
|
|
3182
|
+
message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
|
|
3183
|
+
default: false
|
|
3184
|
+
}]);
|
|
3185
|
+
if (!confirm) {
|
|
3186
|
+
console.log(chalk13.gray(t("undo.cancelled")));
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
try {
|
|
3190
|
+
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3191
|
+
console.log(chalk13.green(`
|
|
3192
|
+
\u2705 ${t("undo.success")}`));
|
|
3193
|
+
console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3194
|
+
if (risky) {
|
|
3195
|
+
console.log(chalk13.yellow(`
|
|
3196
|
+
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3197
|
+
}
|
|
3198
|
+
} catch (err) {
|
|
3199
|
+
console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
|
|
3200
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3201
|
+
console.log(chalk13.red(msg));
|
|
3202
|
+
process.exitCode = 1;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/commands/diff.ts
|
|
3207
|
+
import { execFileSync as execFileSync4, execSync as execSync2 } from "child_process";
|
|
3208
|
+
import chalk14 from "chalk";
|
|
3209
|
+
function gitOut2(args) {
|
|
3210
|
+
try {
|
|
3211
|
+
return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3212
|
+
} catch {
|
|
3213
|
+
return "";
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
function parseDiffStat(stat) {
|
|
3217
|
+
const files = [];
|
|
3218
|
+
const lines = stat.split("\n");
|
|
3219
|
+
for (const line of lines) {
|
|
3220
|
+
const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
|
|
3221
|
+
if (!match) continue;
|
|
3222
|
+
const name = match[1].trim();
|
|
3223
|
+
if (name.includes("changed") || name.includes("file")) continue;
|
|
3224
|
+
const plusMatch = line.match(/(\++)/);
|
|
3225
|
+
const minusMatch = line.match(/(\-+)/);
|
|
3226
|
+
files.push({
|
|
3227
|
+
name,
|
|
3228
|
+
additions: plusMatch ? plusMatch[1].length : 0,
|
|
3229
|
+
deletions: minusMatch ? minusMatch[1].length : 0
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
return files;
|
|
3233
|
+
}
|
|
3234
|
+
function summarizeNumstat(numstat) {
|
|
3235
|
+
let totalAdd = 0;
|
|
3236
|
+
let totalDel = 0;
|
|
3237
|
+
let fileCount = 0;
|
|
3238
|
+
for (const line of numstat.split("\n").filter(Boolean)) {
|
|
3239
|
+
const [add, del] = line.split(" ");
|
|
3240
|
+
if (add === void 0 || del === void 0) continue;
|
|
3241
|
+
totalAdd += parseInt(add, 10) || 0;
|
|
3242
|
+
totalDel += parseInt(del, 10) || 0;
|
|
3243
|
+
fileCount++;
|
|
3244
|
+
}
|
|
3245
|
+
return { fileCount, totalAdd, totalDel };
|
|
3246
|
+
}
|
|
3247
|
+
function printFile(f) {
|
|
3248
|
+
const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
|
|
3249
|
+
const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
|
|
3250
|
+
const change = [adds, dels].filter(Boolean).join(" ");
|
|
3251
|
+
console.log(` ${f.name} ${change}`);
|
|
3252
|
+
}
|
|
3253
|
+
async function diff() {
|
|
3254
|
+
console.log(chalk14.bold(`
|
|
3255
|
+
\u{1F50D} ${t("diff.title")}`));
|
|
3256
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3257
|
+
try {
|
|
3258
|
+
execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
3259
|
+
} catch {
|
|
3260
|
+
console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
const unstaged = gitOut2(["diff", "--stat"]);
|
|
3264
|
+
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
3265
|
+
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
3266
|
+
if (!unstaged && !staged && !untracked) {
|
|
3267
|
+
console.log(chalk14.green(`
|
|
3268
|
+
\u2705 ${t("diff.noChanges")}`));
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (staged) {
|
|
3272
|
+
console.log(chalk14.cyan(`
|
|
3273
|
+
${t("diff.stagedHeader")}`));
|
|
3274
|
+
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
3275
|
+
}
|
|
3276
|
+
if (unstaged) {
|
|
3277
|
+
console.log(chalk14.cyan(`
|
|
3278
|
+
${t("diff.unstagedHeader")}`));
|
|
3279
|
+
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
3280
|
+
}
|
|
3281
|
+
if (untracked) {
|
|
3282
|
+
const files = untracked.split("\n").filter(Boolean);
|
|
3283
|
+
console.log(chalk14.cyan(`
|
|
3284
|
+
${t("diff.untrackedHeader", files.length)}`));
|
|
3285
|
+
files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
|
|
3286
|
+
}
|
|
3287
|
+
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
3288
|
+
if (numstat) {
|
|
3289
|
+
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
3290
|
+
console.log(chalk14.cyan(`
|
|
3291
|
+
${t("diff.summaryHeader")}`));
|
|
3292
|
+
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
3293
|
+
console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
|
|
3294
|
+
console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
|
|
3295
|
+
}
|
|
3296
|
+
console.log("");
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// src/commands/status.ts
|
|
3300
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3301
|
+
import fs14 from "fs";
|
|
3302
|
+
import path14 from "path";
|
|
3303
|
+
import chalk15 from "chalk";
|
|
3304
|
+
function countFileChanges(porcelain) {
|
|
3305
|
+
const lines = porcelain.split("\n").filter(Boolean);
|
|
3306
|
+
let staged = 0;
|
|
3307
|
+
let unstaged = 0;
|
|
3308
|
+
let untracked = 0;
|
|
3309
|
+
for (const line of lines) {
|
|
3310
|
+
const x = line[0];
|
|
3311
|
+
const y = line[1];
|
|
3312
|
+
if (x === "?" && y === "?") {
|
|
3313
|
+
untracked++;
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
if (x !== " ") staged++;
|
|
3317
|
+
if (y !== " ") unstaged++;
|
|
3318
|
+
}
|
|
3319
|
+
return { staged, unstaged, untracked };
|
|
3320
|
+
}
|
|
3321
|
+
function parseSyncCounts(revListOutput) {
|
|
3322
|
+
const parts = revListOutput.trim().split(/\s+/);
|
|
3323
|
+
return {
|
|
3324
|
+
ahead: parseInt(parts[0] ?? "0", 10) || 0,
|
|
3325
|
+
behind: parseInt(parts[1] ?? "0", 10) || 0,
|
|
3326
|
+
hasUpstream: true
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
function formatSyncLabel(sync2) {
|
|
3330
|
+
if (!sync2.hasUpstream) return t("status.noUpstream");
|
|
3331
|
+
if (sync2.ahead === 0 && sync2.behind === 0) return t("status.inSync");
|
|
3332
|
+
const parts = [];
|
|
3333
|
+
if (sync2.ahead > 0) parts.push(t("status.ahead", sync2.ahead));
|
|
3334
|
+
if (sync2.behind > 0) parts.push(t("status.behind", sync2.behind));
|
|
3335
|
+
return parts.join(" \xB7 ");
|
|
3336
|
+
}
|
|
3337
|
+
function parseRecentCommitLines(logOutput) {
|
|
3338
|
+
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3339
|
+
}
|
|
3340
|
+
function readProjectPackage(cwd = process.cwd()) {
|
|
3341
|
+
const pkgPath = path14.join(cwd, "package.json");
|
|
3342
|
+
if (!fs14.existsSync(pkgPath)) return null;
|
|
3343
|
+
try {
|
|
3344
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
3345
|
+
if (!pkg.name && !pkg.version) return null;
|
|
3346
|
+
return {
|
|
3347
|
+
name: pkg.name ?? "(no name)",
|
|
3348
|
+
version: pkg.version ?? "(no version)"
|
|
3349
|
+
};
|
|
3350
|
+
} catch {
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function getSyncCounts(gitRoot) {
|
|
3355
|
+
try {
|
|
3356
|
+
const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
|
|
3357
|
+
return parseSyncCounts(out);
|
|
3358
|
+
} catch {
|
|
3359
|
+
return { ahead: 0, behind: 0, hasUpstream: false };
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
async function status() {
|
|
3363
|
+
console.log(chalk15.bold(`
|
|
3364
|
+
\u{1F4CA} ${t("status.title")}`));
|
|
3365
|
+
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3366
|
+
let gitRoot;
|
|
3367
|
+
try {
|
|
3368
|
+
execFileSync5("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3369
|
+
gitRoot = getGitRoot();
|
|
3370
|
+
} catch {
|
|
3371
|
+
console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3372
|
+
return;
|
|
3373
|
+
}
|
|
3374
|
+
let branch;
|
|
3375
|
+
try {
|
|
3376
|
+
branch = gitOut(["branch", "--show-current"], gitRoot).trim() || t("status.detached");
|
|
3377
|
+
} catch {
|
|
3378
|
+
branch = t("status.unknownBranch");
|
|
3379
|
+
}
|
|
3380
|
+
const porcelain = normalizePorcelain(gitOut(["status", "--porcelain"], gitRoot));
|
|
3381
|
+
const counts = countFileChanges(porcelain);
|
|
3382
|
+
const sync2 = getSyncCounts(gitRoot);
|
|
3383
|
+
let commits = [];
|
|
3384
|
+
try {
|
|
3385
|
+
commits = parseRecentCommitLines(gitOut(["log", "--oneline", "-3"], gitRoot).trim());
|
|
3386
|
+
} catch {
|
|
3387
|
+
commits = [];
|
|
3388
|
+
}
|
|
3389
|
+
const pkg = readProjectPackage();
|
|
3390
|
+
console.log(chalk15.cyan(`
|
|
3391
|
+
\u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
|
|
3392
|
+
console.log(
|
|
3393
|
+
chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
|
|
3394
|
+
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3395
|
+
)
|
|
3396
|
+
);
|
|
3397
|
+
console.log(chalk15.cyan(`
|
|
3398
|
+
\u{1F4CB} ${t("status.recentCommits")}`));
|
|
3399
|
+
if (commits.length === 0) {
|
|
3400
|
+
console.log(chalk15.dim(` ${t("status.noCommits")}`));
|
|
3401
|
+
} else {
|
|
3402
|
+
commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
|
|
3403
|
+
}
|
|
3404
|
+
console.log(
|
|
3405
|
+
chalk15.cyan(`
|
|
3406
|
+
\u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
|
|
3407
|
+
);
|
|
3408
|
+
console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
|
|
3409
|
+
if (pkg) {
|
|
3410
|
+
console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
|
|
3411
|
+
} else {
|
|
3412
|
+
console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3413
|
+
}
|
|
3414
|
+
console.log("");
|
|
3415
|
+
}
|
|
3416
|
+
|
|
2735
3417
|
// src/index.ts
|
|
2736
3418
|
var program = new Command();
|
|
2737
3419
|
var defaultHelp = new Help();
|
|
@@ -2743,9 +3425,13 @@ var KO_ALIASES = {
|
|
|
2743
3425
|
check: "\uC810\uAC80",
|
|
2744
3426
|
secure: "\uBCF4\uC548",
|
|
2745
3427
|
ship: "\uBC30\uD3EC",
|
|
2746
|
-
doctor: "\uD658\uACBD"
|
|
3428
|
+
doctor: "\uD658\uACBD",
|
|
3429
|
+
save: "\uC800\uC7A5",
|
|
3430
|
+
undo: "\uB418\uB3CC\uB9AC\uAE30",
|
|
3431
|
+
status: "\uC0C1\uD0DC",
|
|
3432
|
+
diff: "\uBCC0\uACBD"
|
|
2747
3433
|
};
|
|
2748
|
-
program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.
|
|
3434
|
+
program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.5.1");
|
|
2749
3435
|
program.configureHelp({
|
|
2750
3436
|
formatHelp(cmd, helper) {
|
|
2751
3437
|
if (cmd.parent) {
|
|
@@ -2753,7 +3439,7 @@ program.configureHelp({
|
|
|
2753
3439
|
}
|
|
2754
3440
|
const subs = helper.visibleCommands(cmd).filter((c) => c.name() !== "help");
|
|
2755
3441
|
const terms = subs.map((c) => `${c.name()} (${KO_ALIASES[c.name()]})`);
|
|
2756
|
-
const termWidth = Math.max(...terms.map((
|
|
3442
|
+
const termWidth = Math.max(...terms.map((t2) => t2.length), 0);
|
|
2757
3443
|
const lines = [
|
|
2758
3444
|
helper.commandDescription(cmd),
|
|
2759
3445
|
"",
|
|
@@ -2771,26 +3457,36 @@ program.command("init").alias("\uC2DC\uC791").alias("\uB9CC\uB4E4\uAE30").descri
|
|
|
2771
3457
|
program.command("recap").alias("\uC815\uB9AC").alias("\uC624\uB298").description("\uC624\uB298 \uD55C \uC77C \uC815\uB9AC + ADR/\uD2B8\uB7EC\uBE14\uC288\uD305 \uC790\uB3D9 \uBD84\uB9AC").option("--since <date>", "\uBD84\uC11D \uC2DC\uC791\uC77C (YYYY-MM-DD)").action(recap);
|
|
2772
3458
|
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
|
|
2773
3459
|
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC").action(check);
|
|
2774
|
-
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C");
|
|
3460
|
+
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C \u2014 scan: \uC2DC\uD06C\uB9BF\xB7\uD0A4 \uC720\uCD9C \uAC80\uC0AC").action(secure);
|
|
2775
3461
|
secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
|
|
2776
3462
|
program.command("ship").alias("\uBC30\uD3EC").alias("\uB9B4\uB9AC\uC988").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
|
|
2777
3463
|
program.command("doctor").alias("\uD658\uACBD").alias("\uC9C4\uB2E8").description("\uAC1C\uBC1C \uD658\uACBD \uC810\uAC80 \u2014 Node/Git/npm \uC0C1\uD0DC \uD655\uC778").action(doctor);
|
|
3464
|
+
program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
|
|
3465
|
+
await save();
|
|
3466
|
+
});
|
|
3467
|
+
program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
|
|
3468
|
+
await undo();
|
|
3469
|
+
});
|
|
3470
|
+
program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
|
|
3471
|
+
await status();
|
|
3472
|
+
});
|
|
3473
|
+
program.command("diff").alias("\uBCC0\uACBD").alias("\uCC28\uC774").description("Git \uBCC0\uACBD\uC0AC\uD56D \uD55C\uAD6D\uC5B4 \uC694\uC57D (staged / unstaged / \uC0C8 \uD30C\uC77C)").action(diff);
|
|
2778
3474
|
program.on("command:*", async (operands) => {
|
|
2779
3475
|
const input = operands.join(" ");
|
|
2780
3476
|
const route = routeNaturalLanguage(input);
|
|
2781
3477
|
if (route) {
|
|
2782
3478
|
console.log("");
|
|
2783
|
-
console.log(
|
|
2784
|
-
console.log(
|
|
3479
|
+
console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
|
|
3480
|
+
console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
|
|
2785
3481
|
if (route.confidence === "low") {
|
|
2786
|
-
const { confirm } = await
|
|
3482
|
+
const { confirm } = await inquirer7.prompt([{
|
|
2787
3483
|
type: "confirm",
|
|
2788
3484
|
name: "confirm",
|
|
2789
3485
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
2790
3486
|
default: true
|
|
2791
3487
|
}]);
|
|
2792
3488
|
if (!confirm) {
|
|
2793
|
-
console.log(
|
|
3489
|
+
console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
|
|
2794
3490
|
return;
|
|
2795
3491
|
}
|
|
2796
3492
|
}
|
|
@@ -2815,15 +3511,23 @@ program.on("command:*", async (operands) => {
|
|
|
2815
3511
|
return ship();
|
|
2816
3512
|
case "doctor":
|
|
2817
3513
|
return doctor();
|
|
3514
|
+
case "save":
|
|
3515
|
+
return save();
|
|
3516
|
+
case "undo":
|
|
3517
|
+
return undo();
|
|
3518
|
+
case "status":
|
|
3519
|
+
return status();
|
|
3520
|
+
case "diff":
|
|
3521
|
+
return diff();
|
|
2818
3522
|
}
|
|
2819
3523
|
}
|
|
2820
|
-
console.log(
|
|
3524
|
+
console.log(chalk16.yellow(`
|
|
2821
3525
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
2822
3526
|
`));
|
|
2823
3527
|
});
|
|
2824
3528
|
program.action(async () => {
|
|
2825
3529
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
2826
|
-
const { choice } = await
|
|
3530
|
+
const { choice } = await inquirer7.prompt([{
|
|
2827
3531
|
type: "list",
|
|
2828
3532
|
name: "choice",
|
|
2829
3533
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
@@ -2835,7 +3539,11 @@ program.action(async () => {
|
|
|
2835
3539
|
{ name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
|
|
2836
3540
|
{ name: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654", value: "sync" },
|
|
2837
3541
|
{ name: "\u{1F680} \uBC30\uD3EC\uD558\uAE30", value: "ship" },
|
|
2838
|
-
{ name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" }
|
|
3542
|
+
{ name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" },
|
|
3543
|
+
{ name: "\u{1F4BE} Git\uC5D0 \uC800\uC7A5\uD558\uAE30", value: "save" },
|
|
3544
|
+
{ name: "\u23EA \uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30", value: "undo" },
|
|
3545
|
+
{ name: "\u{1F50D} \uBCC0\uACBD\uC0AC\uD56D \uBCF4\uAE30", value: "diff" },
|
|
3546
|
+
{ name: "\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uBCF4\uAE30", value: "status" }
|
|
2839
3547
|
]
|
|
2840
3548
|
}]);
|
|
2841
3549
|
switch (choice) {
|
|
@@ -2855,6 +3563,14 @@ program.action(async () => {
|
|
|
2855
3563
|
return doctor();
|
|
2856
3564
|
case "ship":
|
|
2857
3565
|
return ship();
|
|
3566
|
+
case "save":
|
|
3567
|
+
return save();
|
|
3568
|
+
case "undo":
|
|
3569
|
+
return undo();
|
|
3570
|
+
case "status":
|
|
3571
|
+
return status();
|
|
3572
|
+
case "diff":
|
|
3573
|
+
return diff();
|
|
2858
3574
|
}
|
|
2859
3575
|
});
|
|
2860
|
-
program.
|
|
3576
|
+
await program.parseAsync(process.argv);
|