@byh3071/vhk 0.5.0 → 0.5.2

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.
Files changed (3) hide show
  1. package/README.md +36 -9
  2. package/dist/index.js +770 -482
  3. package/package.json +56 -56
package/dist/index.js CHANGED
@@ -485,8 +485,7 @@ var require_ignore = __commonJS({
485
485
 
486
486
  // src/index.ts
487
487
  import { Command, Help } from "commander";
488
- import chalk15 from "chalk";
489
- import inquirer7 from "inquirer";
488
+ import inquirer8 from "inquirer";
490
489
 
491
490
  // src/lib/nlp-router.ts
492
491
  function normalize(input) {
@@ -495,8 +494,8 @@ function normalize(input) {
495
494
  var NLP_KEYWORDS = {
496
495
  save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
497
496
  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", "\uD655\uC778"],
499
- diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
497
+ status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"],
498
+ diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uBC14\uB00C\uC5C8", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
500
499
  };
501
500
  function matchesKeywords(text, command) {
502
501
  const keywords = NLP_KEYWORDS[command];
@@ -524,11 +523,29 @@ var RULES = [
524
523
  confidence: "high",
525
524
  test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|하네스|초기화/.test(t2) || /^시작$/.test(t2)
526
525
  },
526
+ {
527
+ command: "secure",
528
+ explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548)",
529
+ confidence: "high",
530
+ test: (t2) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t2)
531
+ },
532
+ {
533
+ command: "check",
534
+ explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
535
+ confidence: "high",
536
+ test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
537
+ },
538
+ {
539
+ command: "doctor",
540
+ explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
541
+ confidence: "high",
542
+ test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단|확인)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
543
+ },
527
544
  {
528
545
  command: "diff",
529
546
  explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
530
547
  confidence: "high",
531
- test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /변경사항|수정\s*내역|차이\s*보/.test(t2)) && !/저장|커밋|push|푸시|상태|현황|세이브|commit/.test(t2)
548
+ test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /변경사항|수정\s*내역|차이\s*보|뭐\s*바뀌/.test(t2)) && !/저장|커밋|push|푸시|상태|현황|세이브|commit/.test(t2)
532
549
  },
533
550
  {
534
551
  command: "undo",
@@ -540,7 +557,7 @@ var RULES = [
540
557
  command: "status",
541
558
  explanation: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778 (vhk \uC0C1\uD0DC)",
542
559
  confidence: "high",
543
- test: (t2) => matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태/.test(t2)
560
+ test: (t2) => (matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태|프로젝트\s*상태/.test(t2)) && !/보안|시크릿|규칙|점검|린트|환경|진단|doctor|secure|check|스캔|설치/.test(t2)
544
561
  },
545
562
  {
546
563
  command: "save",
@@ -554,30 +571,12 @@ var RULES = [
554
571
  confidence: "high",
555
572
  test: (t2) => /오늘.*(정리|기록)|한\s*일|세션|회고|recap|정리해/.test(t2)
556
573
  },
557
- {
558
- command: "doctor",
559
- explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
560
- confidence: "high",
561
- test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
562
- },
563
574
  {
564
575
  command: "gate",
565
576
  explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
566
577
  confidence: "high",
567
578
  test: (t2) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t2)
568
579
  },
569
- {
570
- command: "secure",
571
- explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548 scan)",
572
- confidence: "high",
573
- test: (t2) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t2)
574
- },
575
- {
576
- command: "check",
577
- explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
578
- confidence: "high",
579
- test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
580
- },
581
580
  {
582
581
  command: "sync",
583
582
  explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
@@ -611,6 +610,69 @@ function extractNotionUrl(input) {
611
610
  return m?.[0];
612
611
  }
613
612
 
613
+ // src/lib/cli-args.ts
614
+ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
615
+ "gate",
616
+ "\uAC80\uC99D",
617
+ "\uC544\uC774\uB514\uC5B4",
618
+ "init",
619
+ "\uC2DC\uC791",
620
+ "\uB9CC\uB4E4\uAE30",
621
+ "recap",
622
+ "\uC815\uB9AC",
623
+ "\uC624\uB298",
624
+ "sync",
625
+ "\uB9DE\uCD94\uAE30",
626
+ "\uADDC\uCE59",
627
+ "check",
628
+ "\uC810\uAC80",
629
+ "\uB9B0\uD2B8",
630
+ "secure",
631
+ "\uBCF4\uC548",
632
+ "scan",
633
+ "\uC2A4\uCE94",
634
+ "ship",
635
+ "\uBC30\uD3EC",
636
+ "\uB9B4\uB9AC\uC988",
637
+ "doctor",
638
+ "\uD658\uACBD",
639
+ "\uC9C4\uB2E8",
640
+ "save",
641
+ "\uC800\uC7A5",
642
+ "undo",
643
+ "\uB418\uB3CC\uB9AC\uAE30",
644
+ "status",
645
+ "\uC0C1\uD0DC",
646
+ "\uD604\uD669",
647
+ "diff",
648
+ "\uBCC0\uACBD",
649
+ "\uCC28\uC774",
650
+ "help"
651
+ ]);
652
+ function isOptionToken(token) {
653
+ return token.startsWith("-");
654
+ }
655
+ function detectNaturalLanguageInput(argv) {
656
+ const rest = argv.slice(2);
657
+ if (rest.length === 0) return null;
658
+ const first = rest[0];
659
+ if (isOptionToken(first)) return null;
660
+ const input = rest.join(" ").trim();
661
+ if (!input) return null;
662
+ const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
663
+ if (firstIsKnown && rest.length === 1) return null;
664
+ if (firstIsKnown && rest.slice(1).every(isOptionToken)) return null;
665
+ if (firstIsKnown && rest.length > 1) {
666
+ if (routeNaturalLanguage(input)) return input;
667
+ return null;
668
+ }
669
+ return input;
670
+ }
671
+
672
+ // src/lib/nlp-run.ts
673
+ import chalk16 from "chalk";
674
+ import inquirer7 from "inquirer";
675
+
614
676
  // src/i18n/ko.ts
615
677
  var ko = {
616
678
  status: {
@@ -642,7 +704,15 @@ var ko = {
642
704
  successLocal: "\uB85C\uCEEC \uC800\uC7A5 \uC644\uB8CC!",
643
705
  noRemote: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 push\uB97C \uAC74\uB108\uB6F0\uC5C8\uC2B5\uB2C8\uB2E4.",
644
706
  failed: "\uC800\uC7A5 \uC2E4\uD328",
645
- done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`
707
+ 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",
708
+ securityWarnHeader: "\uC800\uC7A5 \uC804 \uBCF4\uC548 \uD655\uC778:",
709
+ secretsFound: (n) => `\uCF54\uB4DC\uC5D0\uC11C CRITICAL/HIGH \uC2DC\uD06C\uB9BF \uD328\uD134 ${n}\uAC74 \uAC10\uC9C0`,
710
+ secretsConfirm: "\uADF8\uB798\uB3C4 \uCEE4\uBC0B\xB7push\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
711
+ cancelled: "\uC800\uC7A5\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.",
712
+ pushFailed: "push \uC2E4\uD328 (\uB85C\uCEEC \uCEE4\uBC0B\uC740 \uC644\uB8CC\uB428)",
713
+ 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.",
714
+ done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`,
715
+ doneLocalOnly: (n) => `${n}\uAC1C \uD30C\uC77C \uB85C\uCEEC \uC800\uC7A5\uB428 (push\uB294 \uC2E4\uD328)`
646
716
  },
647
717
  undo: {
648
718
  title: "\uB418\uB3CC\uB9AC\uAE30",
@@ -651,10 +721,14 @@ var ko = {
651
721
  recentHeader: "\u{1F4CB} \uCD5C\uADFC \uCEE4\uBC0B:",
652
722
  howMany: "\uBA87 \uAC1C\uC758 \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9B4\uAE4C\uC694?",
653
723
  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.",
724
+ 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.",
654
725
  confirmMessage: "\uCD5C\uADFC \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9AC\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
726
+ 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?`,
655
727
  cancelled: "\uCDE8\uC18C\uB428",
656
728
  success: "\uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC! \uBCC0\uACBD\uC0AC\uD56D\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.",
657
729
  stagedHint: "\uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uB0A8\uC544 \uC788\uC5B4\uC694.",
730
+ rootCommit: "\uCCAB \uCEE4\uBC0B\uB9CC \uC788\uC5B4\uC11C \uB354 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
731
+ 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)",
658
732
  failed: "\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328"
659
733
  },
660
734
  diff: {
@@ -1016,9 +1090,9 @@ ${ko.gate.verdictTitle}
1016
1090
 
1017
1091
  // src/commands/init.ts
1018
1092
  import inquirer2 from "inquirer";
1019
- import chalk4 from "chalk";
1020
- import fs2 from "fs";
1021
- import path2 from "path";
1093
+ import chalk5 from "chalk";
1094
+ import fs3 from "fs";
1095
+ import path3 from "path";
1022
1096
 
1023
1097
  // src/templates/claude-md.ts
1024
1098
  function CLAUDE_MD_TEMPLATE(name, _stack) {
@@ -1228,27 +1302,113 @@ function COMMANDS_MD_TEMPLATE() {
1228
1302
  ].join("\n");
1229
1303
  }
1230
1304
 
1231
- // src/utils/logger.ts
1305
+ // src/lib/check-secure.ts
1306
+ var import_ignore = __toESM(require_ignore(), 1);
1307
+ import fs from "fs";
1308
+ import path from "path";
1232
1309
  import chalk3 from "chalk";
1310
+ function loadGitignore(rootDir) {
1311
+ const ig = (0, import_ignore.default)();
1312
+ const gitignorePath = path.join(rootDir, ".gitignore");
1313
+ if (fs.existsSync(gitignorePath)) {
1314
+ const content = fs.readFileSync(gitignorePath, "utf-8");
1315
+ ig.add(content);
1316
+ }
1317
+ return ig;
1318
+ }
1319
+ function isPathIgnored(ig, relativePath) {
1320
+ const normalized = relativePath.replace(/\\/g, "/");
1321
+ return ig.ignores(normalized);
1322
+ }
1323
+ function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDepth = 8) {
1324
+ const exposed = [];
1325
+ function walk(dir, depth) {
1326
+ if (depth > maxDepth) return;
1327
+ let entries;
1328
+ try {
1329
+ entries = fs.readdirSync(dir, { withFileTypes: true });
1330
+ } catch {
1331
+ return;
1332
+ }
1333
+ for (const entry of entries) {
1334
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
1335
+ const fullPath = path.join(dir, entry.name);
1336
+ const rel = path.relative(rootDir, fullPath).replace(/\\/g, "/");
1337
+ if (entry.isDirectory()) {
1338
+ if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
1339
+ continue;
1340
+ }
1341
+ if (isSensitiveName(entry.name) && !isPathIgnored(ig, rel)) {
1342
+ exposed.push(rel);
1343
+ }
1344
+ }
1345
+ }
1346
+ walk(rootDir, 0);
1347
+ return exposed;
1348
+ }
1349
+ function isSensitiveName(name) {
1350
+ const lower = name.toLowerCase();
1351
+ if (lower === ".env" || lower.startsWith(".env.")) return true;
1352
+ if (lower.endsWith(".pem") || lower.endsWith(".key")) return true;
1353
+ if (lower === "credentials.json" || lower === "secrets.json") return true;
1354
+ if (lower.startsWith("id_rsa")) return true;
1355
+ return false;
1356
+ }
1357
+ function checkProjectSecurity(rootDir = process.cwd()) {
1358
+ const gitignorePath = path.join(rootDir, ".gitignore");
1359
+ const missingGitignore = !fs.existsSync(gitignorePath);
1360
+ const ig = loadGitignore(rootDir);
1361
+ const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
1362
+ const warnings = [];
1363
+ if (missingGitignore) {
1364
+ 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.");
1365
+ }
1366
+ if (exposedPaths.length > 0) {
1367
+ warnings.push(
1368
+ `ignore\uB418\uC9C0 \uC54A\uC740 \uBBFC\uAC10 \uD30C\uC77C ${exposedPaths.length}\uAC1C: ${exposedPaths.join(", ")}`
1369
+ );
1370
+ }
1371
+ return {
1372
+ ok: !missingGitignore && exposedPaths.length === 0,
1373
+ missingGitignore,
1374
+ exposedPaths,
1375
+ warnings
1376
+ };
1377
+ }
1378
+ function printSecurityWarnings(rootDir = process.cwd()) {
1379
+ const result = checkProjectSecurity(rootDir);
1380
+ if (result.ok) return true;
1381
+ for (const w of result.warnings) {
1382
+ console.log(chalk3.yellow(` \u26A0\uFE0F ${w}`));
1383
+ }
1384
+ return false;
1385
+ }
1386
+ function filterTrackedPaths(paths, rootDir = process.cwd()) {
1387
+ const ig = loadGitignore(rootDir);
1388
+ return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
1389
+ }
1390
+
1391
+ // src/utils/logger.ts
1392
+ import chalk4 from "chalk";
1233
1393
  var log = {
1234
- success: (msg) => console.log(chalk3.green(`\u2705 ${msg}`)),
1235
- error: (msg) => console.log(chalk3.red(`\u274C ${msg}`)),
1236
- warn: (msg) => console.log(chalk3.yellow(`\u26A0\uFE0F ${msg}`)),
1237
- info: (msg) => console.log(chalk3.blue(`\u2139\uFE0F ${msg}`)),
1238
- step: (msg) => console.log(chalk3.bold(`
1394
+ success: (msg) => console.log(chalk4.green(`\u2705 ${msg}`)),
1395
+ error: (msg) => console.log(chalk4.red(`\u274C ${msg}`)),
1396
+ warn: (msg) => console.log(chalk4.yellow(`\u26A0\uFE0F ${msg}`)),
1397
+ info: (msg) => console.log(chalk4.blue(`\u2139\uFE0F ${msg}`)),
1398
+ step: (msg) => console.log(chalk4.bold(`
1239
1399
  \u25B8 ${msg}`))
1240
1400
  };
1241
1401
 
1242
1402
  // src/utils/file.ts
1243
- import fs from "fs";
1244
- import path from "path";
1403
+ import fs2 from "fs";
1404
+ import path2 from "path";
1245
1405
  function writeFile(filePath, content) {
1246
- const dir = path.dirname(filePath);
1247
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1248
- fs.writeFileSync(filePath, content, "utf-8");
1406
+ const dir = path2.dirname(filePath);
1407
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
1408
+ fs2.writeFileSync(filePath, content, "utf-8");
1249
1409
  }
1250
1410
  function fileExists(filePath) {
1251
- return fs.existsSync(filePath);
1411
+ return fs2.existsSync(filePath);
1252
1412
  }
1253
1413
 
1254
1414
  // src/lib/notion-import.ts
@@ -1440,13 +1600,14 @@ async function collectAnswers(options, defaults = {}) {
1440
1600
  async function init(options = {}) {
1441
1601
  const skipGate = Boolean(options.skipGate || options.fromNotion);
1442
1602
  if (skipGate) {
1443
- console.log(chalk4.dim(`
1603
+ console.log(chalk5.dim(`
1444
1604
  ${ko.init.skipGate}
1445
1605
  `));
1446
1606
  }
1447
- console.log(chalk4.bold(`
1607
+ console.log(chalk5.bold(`
1448
1608
  ${ko.init.title}
1449
1609
  `));
1610
+ printSecurityWarnings();
1450
1611
  let prdContent = {};
1451
1612
  const defaults = {};
1452
1613
  if (options.fromNotion) {
@@ -1468,7 +1629,7 @@ ${ko.init.title}
1468
1629
  process.exit(1);
1469
1630
  }
1470
1631
  const stack = STACK_PRESETS[answers.type];
1471
- console.log(chalk4.dim(`
1632
+ console.log(chalk5.dim(`
1472
1633
  ${ko.init.recommendedStack} ${stack.join(" + ")}
1473
1634
  `));
1474
1635
  if (!options.yes) {
@@ -1487,7 +1648,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1487
1648
  const files = generateFiles(answers.name, answers.description, stack, prdContent);
1488
1649
  log.step(ko.init.filesGenerating);
1489
1650
  for (const [filePath, content] of Object.entries(files)) {
1490
- const fullPath = path2.join(cwd, filePath);
1651
+ const fullPath = path3.join(cwd, filePath);
1491
1652
  if (fileExists(fullPath)) {
1492
1653
  const { overwrite } = await inquirer2.prompt([{
1493
1654
  type: "confirm",
@@ -1504,21 +1665,21 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1504
1665
  log.success(filePath);
1505
1666
  }
1506
1667
  await writeInitExtras(cwd);
1507
- console.log(chalk4.bold.green(`
1668
+ console.log(chalk5.bold.green(`
1508
1669
  ${ko.init.done}`));
1509
- console.log(chalk4.dim(`
1670
+ console.log(chalk5.dim(`
1510
1671
  ${ko.init.nextSteps}`));
1511
1672
  if (options.fromNotion) {
1512
1673
  console.log(` 1. ${ko.init.notionReviewHint}`);
1513
1674
  console.log(` 2. ${ko.init.gitHintLabel}`);
1514
- console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
1675
+ console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
1515
1676
  console.log(` 3. ${ko.init.startDev}
1516
1677
  `);
1517
1678
  } else {
1518
1679
  console.log(` 1. ${ko.init.fillHint}`);
1519
1680
  console.log(` 2. ${ko.init.prdHint}`);
1520
1681
  console.log(` 3. ${ko.init.gitHintLabel}`);
1521
- console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
1682
+ console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
1522
1683
  console.log(` 4. ${ko.init.startDev}
1523
1684
  `);
1524
1685
  }
@@ -1558,7 +1719,7 @@ function generateFiles(name, description, stack, prdContent = {}) {
1558
1719
  };
1559
1720
  }
1560
1721
  var VHK_PACKAGE_SCRIPTS = {
1561
- save: "git add . && git commit -m",
1722
+ save: "vhk save",
1562
1723
  check: "vhk check",
1563
1724
  scan: "vhk secure scan",
1564
1725
  recap: "vhk recap",
@@ -1566,15 +1727,15 @@ var VHK_PACKAGE_SCRIPTS = {
1566
1727
  doctor: "vhk doctor"
1567
1728
  };
1568
1729
  function enhancePackageScripts(projectDir) {
1569
- const pkgPath = path2.join(projectDir, "package.json");
1570
- if (!fs2.existsSync(pkgPath)) return false;
1571
- const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
1730
+ const pkgPath = path3.join(projectDir, "package.json");
1731
+ if (!fs3.existsSync(pkgPath)) return false;
1732
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1572
1733
  pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
1573
- fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1734
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1574
1735
  return true;
1575
1736
  }
1576
1737
  async function writeInitExtras(projectDir) {
1577
- const commandsPath = path2.join(projectDir, "COMMANDS.md");
1738
+ const commandsPath = path3.join(projectDir, "COMMANDS.md");
1578
1739
  if (fileExists(commandsPath)) {
1579
1740
  const { overwrite } = await inquirer2.prompt([{
1580
1741
  type: "confirm",
@@ -1599,37 +1760,13 @@ async function writeInitExtras(projectDir) {
1599
1760
 
1600
1761
  // src/commands/recap.ts
1601
1762
  import inquirer3 from "inquirer";
1602
- import chalk5 from "chalk";
1763
+ import chalk6 from "chalk";
1603
1764
  import fs5 from "fs";
1604
1765
  import path6 from "path";
1605
1766
 
1606
1767
  // src/lib/git.ts
1607
1768
  import path4 from "path";
1608
1769
  import simpleGit from "simple-git";
1609
-
1610
- // src/lib/check-secure.ts
1611
- var import_ignore = __toESM(require_ignore(), 1);
1612
- import fs3 from "fs";
1613
- import path3 from "path";
1614
- function loadGitignore(rootDir) {
1615
- const ig = (0, import_ignore.default)();
1616
- const gitignorePath = path3.join(rootDir, ".gitignore");
1617
- if (fs3.existsSync(gitignorePath)) {
1618
- const content = fs3.readFileSync(gitignorePath, "utf-8");
1619
- ig.add(content);
1620
- }
1621
- return ig;
1622
- }
1623
- function isPathIgnored(ig, relativePath) {
1624
- const normalized = relativePath.replace(/\\/g, "/");
1625
- return ig.ignores(normalized);
1626
- }
1627
- function filterTrackedPaths(paths, rootDir = process.cwd()) {
1628
- const ig = loadGitignore(rootDir);
1629
- return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
1630
- }
1631
-
1632
- // src/lib/git.ts
1633
1770
  var git = simpleGit();
1634
1771
  function isNoiseRecapPath(filePath) {
1635
1772
  const base = path4.basename(filePath);
@@ -1646,29 +1783,19 @@ function filterRecapFiles(files) {
1646
1783
  const tracked = new Set(filterTrackedPaths(paths));
1647
1784
  return files.filter((f) => tracked.has(f.file) && !isNoiseRecapPath(f.file));
1648
1785
  }
1649
- function fileStatus(workingDir) {
1650
- if (workingDir === "?") return "new";
1651
- if (workingDir === "D") return "deleted";
1652
- if (workingDir === "R") return "renamed";
1786
+ function inferFileStatusFromDiff(insertions, deletions) {
1787
+ if (deletions > 0 && insertions === 0) return "deleted";
1788
+ if (insertions > 0 && deletions === 0) return "new";
1653
1789
  return "modified";
1654
1790
  }
1655
- async function getSessionDiff(since) {
1656
- const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1657
- const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
1658
- const statusResult = await git.status();
1659
- const statByFile = new Map(
1660
- diffSummary.files.map((f) => [f.file, f])
1661
- );
1791
+ function buildSessionDiffFromSummary(diffSummary) {
1662
1792
  const files = filterRecapFiles(
1663
- statusResult.files.map((f) => {
1664
- const stat = statByFile.get(f.path);
1665
- return {
1666
- file: f.path,
1667
- insertions: stat?.insertions ?? 0,
1668
- deletions: stat?.deletions ?? 0,
1669
- status: fileStatus(f.working_dir)
1670
- };
1671
- })
1793
+ diffSummary.files.map((f) => ({
1794
+ file: f.file,
1795
+ insertions: f.insertions,
1796
+ deletions: f.deletions,
1797
+ status: inferFileStatusFromDiff(f.insertions, f.deletions)
1798
+ }))
1672
1799
  );
1673
1800
  return {
1674
1801
  filesChanged: files.length,
@@ -1677,6 +1804,11 @@ async function getSessionDiff(since) {
1677
1804
  files
1678
1805
  };
1679
1806
  }
1807
+ async function getSessionDiff(since) {
1808
+ const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1809
+ const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
1810
+ return buildSessionDiffFromSummary(diffSummary);
1811
+ }
1680
1812
  async function getRecentCommits(count = 10, since) {
1681
1813
  const options = { maxCount: count };
1682
1814
  if (since) options["--since"] = since;
@@ -1787,39 +1919,40 @@ function createAdrFile(cwd, title, context, decision, consequences) {
1787
1919
 
1788
1920
  // src/commands/recap.ts
1789
1921
  async function recap(options = {}) {
1790
- console.log(chalk5.bold(`
1922
+ console.log(chalk6.bold(`
1791
1923
  ${ko.recap.title}
1792
1924
  `));
1793
1925
  if (!await isGitRepo()) {
1794
- console.log(chalk5.red(ko.recap.noRepo));
1926
+ console.log(chalk6.red(ko.recap.noRepo));
1795
1927
  return;
1796
1928
  }
1797
- console.log(chalk5.dim(`${ko.recap.analyzing}
1929
+ printSecurityWarnings();
1930
+ console.log(chalk6.dim(`${ko.recap.analyzing}
1798
1931
  `));
1799
1932
  const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1800
1933
  const diff2 = await getSessionDiff(since);
1801
1934
  const commits = await getRecentCommits(10, since);
1802
1935
  if (diff2.filesChanged === 0 && commits.length === 0) {
1803
- console.log(chalk5.yellow(ko.recap.noChanges));
1936
+ console.log(chalk6.yellow(ko.recap.noChanges));
1804
1937
  return;
1805
1938
  }
1806
- console.log(chalk5.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1807
- console.log(` \uD30C\uC77C: ${chalk5.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1808
- console.log(` \uCD94\uAC00: ${chalk5.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk5.red("-" + diff2.deletions)}`);
1939
+ console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1940
+ console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1941
+ console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
1809
1942
  if (diff2.files.length > 0) {
1810
- console.log(chalk5.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1943
+ console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1811
1944
  diff2.files.slice(0, 15).forEach((f) => {
1812
- const icon = f.status === "new" ? chalk5.green("\u{1F195}") : f.status === "deleted" ? chalk5.red("\u{1F5D1}\uFE0F") : chalk5.yellow("\u270F\uFE0F");
1945
+ const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
1813
1946
  console.log(` ${icon} ${f.file}`);
1814
1947
  });
1815
1948
  if (diff2.files.length > 15) {
1816
- console.log(chalk5.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1949
+ console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1817
1950
  }
1818
1951
  }
1819
1952
  if (commits.length > 0) {
1820
- console.log(chalk5.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1953
+ console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1821
1954
  commits.slice(0, 5).forEach((c) => {
1822
- console.log(chalk5.dim(` \u2022 ${c.message}`));
1955
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1823
1956
  });
1824
1957
  }
1825
1958
  console.log("");
@@ -1887,11 +2020,11 @@ ${ko.recap.title}
1887
2020
  fs5.writeFileSync(filePath, content, "utf-8");
1888
2021
  const adrCandidates = detectAdrCandidates(diff2);
1889
2022
  if (adrCandidates.length > 0) {
1890
- console.log(chalk5.cyan.bold(`
2023
+ console.log(chalk6.cyan.bold(`
1891
2024
  ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1892
2025
  for (const candidate of adrCandidates) {
1893
- console.log(chalk5.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
1894
- candidate.files.forEach((f) => console.log(chalk5.dim(` ${f}`)));
2026
+ console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
2027
+ candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
1895
2028
  }
1896
2029
  const { createAdr } = await inquirer3.prompt([{
1897
2030
  type: "confirm",
@@ -1921,17 +2054,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1921
2054
  adrAnswers.decision,
1922
2055
  adrAnswers.consequences
1923
2056
  );
1924
- console.log(chalk5.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
2057
+ console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
1925
2058
  }
1926
2059
  }
1927
2060
  }
1928
2061
  const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
1929
2062
  const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
1930
2063
  if (troubleCommits.length > 0) {
1931
- console.log(chalk5.yellow.bold(`
2064
+ console.log(chalk6.yellow.bold(`
1932
2065
  ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1933
2066
  troubleCommits.forEach((c) => {
1934
- console.log(chalk5.dim(` \u2022 ${c.message}`));
2067
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1935
2068
  });
1936
2069
  const { createTroubleshoot } = await inquirer3.prompt([{
1937
2070
  type: "confirm",
@@ -1982,12 +2115,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1982
2115
  `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1983
2116
  ].join("\n");
1984
2117
  fs5.writeFileSync(tsFilePath, tsContent, "utf-8");
1985
- console.log(chalk5.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
2118
+ console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
1986
2119
  }
1987
2120
  }
1988
- console.log(chalk5.green.bold(`
2121
+ console.log(chalk6.green.bold(`
1989
2122
  ${ko.recap.done}`));
1990
- console.log(chalk5.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
2123
+ console.log(chalk6.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
1991
2124
  const claudeMdPath = path6.join(process.cwd(), "CLAUDE.md");
1992
2125
  if (fs5.existsSync(claudeMdPath)) {
1993
2126
  const { updateClaude } = await inquirer3.prompt([{
@@ -2007,7 +2140,7 @@ ${ko.recap.done}`));
2007
2140
  `- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
2008
2141
  );
2009
2142
  fs5.writeFileSync(claudeMdPath, claudeContent, "utf-8");
2010
- console.log(chalk5.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
2143
+ console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
2011
2144
  }
2012
2145
  }
2013
2146
  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"';
@@ -2019,7 +2152,7 @@ ${ko.recap.done}`));
2019
2152
  }
2020
2153
 
2021
2154
  // src/commands/sync.ts
2022
- import chalk6 from "chalk";
2155
+ import chalk7 from "chalk";
2023
2156
  import fs6 from "fs";
2024
2157
  import path7 from "path";
2025
2158
  var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
@@ -2089,32 +2222,32 @@ function toClaudeMd(sections, existing) {
2089
2222
  return lines.join("\n");
2090
2223
  }
2091
2224
  async function sync() {
2092
- console.log(chalk6.bold(`
2225
+ console.log(chalk7.bold(`
2093
2226
  ${ko.sync.title}
2094
2227
  `));
2095
2228
  const cwd = process.cwd();
2096
2229
  const rulesPath = path7.join(cwd, "RULES.md");
2097
2230
  if (!fs6.existsSync(rulesPath)) {
2098
- console.log(chalk6.yellow(ko.sync.noRules));
2099
- console.log(chalk6.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
2100
- console.log(chalk6.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
2231
+ console.log(chalk7.yellow(ko.sync.noRules));
2232
+ console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
2233
+ console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
2101
2234
  console.log("");
2102
- console.log(chalk6.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
2103
- console.log(chalk6.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
2104
- console.log(chalk6.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
2105
- console.log(chalk6.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
2106
- console.log(chalk6.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
2107
- console.log(chalk6.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
2235
+ console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
2236
+ console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
2237
+ console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
2238
+ console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
2239
+ console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
2240
+ console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
2108
2241
  return;
2109
2242
  }
2110
2243
  const rulesContent = fs6.readFileSync(rulesPath, "utf-8");
2111
2244
  const sections = parseRulesMd(rulesContent);
2112
- console.log(chalk6.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
2245
+ console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
2113
2246
  const firstLine = rulesContent.split("\n")[0];
2114
2247
  const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
2115
2248
  const cursorrulesPath = path7.join(cwd, ".cursorrules");
2116
2249
  fs6.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
2117
- console.log(chalk6.green(` ${ko.sync.cursorrulesDone}`));
2250
+ console.log(chalk7.green(` ${ko.sync.cursorrulesDone}`));
2118
2251
  const claudePath = path7.join(cwd, "CLAUDE.md");
2119
2252
  const existingClaude = fs6.existsSync(claudePath) ? fs6.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
2120
2253
 
@@ -2124,11 +2257,11 @@ ${ko.sync.title}
2124
2257
  - **\uB2E4\uC74C \uC561\uC158:** __FILL__
2125
2258
  - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
2126
2259
  fs6.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
2127
- console.log(chalk6.green(` ${ko.sync.claudeDone}`));
2128
- console.log(chalk6.bold.green(`
2260
+ console.log(chalk7.green(` ${ko.sync.claudeDone}`));
2261
+ console.log(chalk7.bold.green(`
2129
2262
  ${ko.sync.done}`));
2130
- console.log(chalk6.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
2131
- console.log(chalk6.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
2263
+ console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
2264
+ console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
2132
2265
  printNextStep({
2133
2266
  message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
2134
2267
  command: "vhk \uC810\uAC80",
@@ -2137,7 +2270,7 @@ ${ko.sync.done}`));
2137
2270
  }
2138
2271
 
2139
2272
  // src/commands/check.ts
2140
- import chalk7 from "chalk";
2273
+ import chalk8 from "chalk";
2141
2274
  import path9 from "path";
2142
2275
  import fs8 from "fs";
2143
2276
 
@@ -2196,15 +2329,6 @@ function parseRules(rulesPath) {
2196
2329
  ));
2197
2330
  }
2198
2331
  }
2199
- if (/반드시|필수|항상|must|always|required/i.test(ruleText)) {
2200
- rules.push({
2201
- id: `required-${ruleIndex}`,
2202
- section: currentSection,
2203
- type: "custom",
2204
- description: ruleText,
2205
- check: () => []
2206
- });
2207
- }
2208
2332
  }
2209
2333
  return rules;
2210
2334
  }
@@ -2305,22 +2429,22 @@ function escapeRegex(str) {
2305
2429
 
2306
2430
  // src/commands/check.ts
2307
2431
  async function check() {
2308
- console.log(chalk7.bold(`
2432
+ console.log(chalk8.bold(`
2309
2433
  ${ko.check.title}
2310
2434
  `));
2311
2435
  const cwd = process.cwd();
2312
2436
  const rulesPath = path9.join(cwd, "RULES.md");
2313
2437
  if (!fs8.existsSync(rulesPath)) {
2314
- console.log(chalk7.yellow(ko.check.noRules));
2315
- console.log(chalk7.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
2438
+ console.log(chalk8.yellow(ko.check.noRules));
2439
+ console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
2316
2440
  return;
2317
2441
  }
2318
2442
  const rules = parseRules(rulesPath);
2319
- console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
2443
+ console.log(chalk8.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
2320
2444
  `));
2321
2445
  if (rules.length === 0) {
2322
- console.log(chalk7.yellow(ko.check.noAutoRules));
2323
- console.log(chalk7.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."));
2446
+ console.log(chalk8.yellow(ko.check.noAutoRules));
2447
+ 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."));
2324
2448
  return;
2325
2449
  }
2326
2450
  const allViolations = [];
@@ -2328,13 +2452,13 @@ ${ko.check.title}
2328
2452
  for (const rule of rules) {
2329
2453
  const violations = rule.check(cwd);
2330
2454
  if (violations.length === 0) {
2331
- console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
2455
+ console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`));
2332
2456
  passCount++;
2333
2457
  } else {
2334
- console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2458
+ console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2335
2459
  violations.forEach((v) => {
2336
- const loc = v.file ? chalk7.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
2337
- const icon = v.severity === "error" ? chalk7.red("\u2716") : v.severity === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
2460
+ const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
2461
+ const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
2338
2462
  console.log(` ${icon} ${v.message}${loc}`);
2339
2463
  });
2340
2464
  allViolations.push(...violations);
@@ -2344,17 +2468,17 @@ ${ko.check.title}
2344
2468
  const errors = allViolations.filter((v) => v.severity === "error").length;
2345
2469
  const warnings = allViolations.filter((v) => v.severity === "warning").length;
2346
2470
  if (allViolations.length === 0) {
2347
- console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
2471
+ console.log(chalk8.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
2348
2472
  printNextStep({
2349
2473
  message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
2350
2474
  command: "vhk \uBCF4\uC548 scan",
2351
2475
  cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
2352
2476
  });
2353
2477
  } else {
2354
- console.log(chalk7.bold(ko.check.summary));
2355
- console.log(` \uADDC\uCE59: ${chalk7.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk7.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk7.red(String(allViolations.length))}\uAC74`);
2356
- if (errors > 0) console.log(` ${chalk7.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
2357
- if (warnings > 0) console.log(` ${chalk7.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
2478
+ console.log(chalk8.bold(ko.check.summary));
2479
+ 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`);
2480
+ if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
2481
+ if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
2358
2482
  printNextStep({
2359
2483
  message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
2360
2484
  command: "vhk \uC810\uAC80",
@@ -2367,10 +2491,13 @@ ${ko.check.title}
2367
2491
  }
2368
2492
 
2369
2493
  // src/commands/secure.ts
2370
- import chalk8 from "chalk";
2371
- import fs10 from "fs";
2494
+ import chalk9 from "chalk";
2495
+ import fs11 from "fs";
2372
2496
  import path11 from "path";
2373
2497
 
2498
+ // src/lib/scan-secrets.ts
2499
+ import fs10 from "fs";
2500
+
2374
2501
  // src/lib/secret-patterns.ts
2375
2502
  var SECRET_PATTERNS = [
2376
2503
  {
@@ -2395,7 +2522,7 @@ var SECRET_PATTERNS = [
2395
2522
  id: "notion-token",
2396
2523
  name: "Notion Integration Token",
2397
2524
  severity: "critical",
2398
- pattern: /secret_[A-Za-z0-9]{24,}/
2525
+ pattern: /secret_[A-Za-z0-9]{40,50}/
2399
2526
  },
2400
2527
  {
2401
2528
  id: "github-token",
@@ -2407,7 +2534,7 @@ var SECRET_PATTERNS = [
2407
2534
  id: "openai-key",
2408
2535
  name: "OpenAI API Key",
2409
2536
  severity: "critical",
2410
- pattern: /sk-[A-Za-z0-9]{20,}/
2537
+ pattern: /\bsk-(?:proj-|ant-api03-|live-)[A-Za-z0-9_-]{16,}\b/
2411
2538
  },
2412
2539
  {
2413
2540
  id: "generic-api-key",
@@ -2509,69 +2636,88 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
2509
2636
  walk(rootDir);
2510
2637
  }
2511
2638
 
2512
- // src/commands/secure.ts
2513
- var MAX_FINDINGS = 200;
2639
+ // src/lib/scan-secrets.ts
2640
+ var MAX_SECRET_FINDINGS = 200;
2514
2641
  var MAX_LINE_CHARS = 4e3;
2642
+ function globalPattern(pattern) {
2643
+ const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
2644
+ return new RegExp(pattern.source, flags);
2645
+ }
2646
+ function findSecretsInLine(line, relPath, lineNum) {
2647
+ const found = [];
2648
+ const trimmed = line.trim();
2649
+ if (trimmed.startsWith("//") && trimmed.includes("example")) return found;
2650
+ if (trimmed.startsWith("#") && trimmed.includes("example")) return found;
2651
+ if (line.length > MAX_LINE_CHARS) return found;
2652
+ for (const pattern of SECRET_PATTERNS) {
2653
+ const regex = globalPattern(pattern.pattern);
2654
+ for (const match of line.matchAll(regex)) {
2655
+ found.push({
2656
+ patternId: pattern.id,
2657
+ patternName: pattern.name,
2658
+ severity: pattern.severity,
2659
+ file: relPath,
2660
+ line: lineNum,
2661
+ match: maskSecret(match[0])
2662
+ });
2663
+ }
2664
+ }
2665
+ return found;
2666
+ }
2667
+ function scanProjectForSecrets(cwd) {
2668
+ const findings = [];
2669
+ let scannedFiles = 0;
2670
+ let truncated = false;
2671
+ walkProjectFiles(cwd, (filePath, relPath) => {
2672
+ scannedFiles++;
2673
+ const content = fs10.readFileSync(filePath, "utf-8");
2674
+ const lines = content.split("\n");
2675
+ lines.forEach((line, idx) => {
2676
+ if (truncated) return;
2677
+ const lineFindings = findSecretsInLine(line, relPath, idx + 1);
2678
+ for (const f of lineFindings) {
2679
+ findings.push(f);
2680
+ if (findings.length >= MAX_SECRET_FINDINGS) {
2681
+ truncated = true;
2682
+ return;
2683
+ }
2684
+ }
2685
+ });
2686
+ });
2687
+ return { findings, scannedFiles, truncated };
2688
+ }
2689
+ function filterSevereFindings(findings) {
2690
+ return findings.filter((f) => f.severity === "critical" || f.severity === "high");
2691
+ }
2692
+
2693
+ // src/commands/secure.ts
2515
2694
  async function secure() {
2516
- console.log(chalk8.bold(`
2695
+ console.log(chalk9.bold(`
2517
2696
  ${ko.secure.title}
2518
2697
  `));
2519
2698
  const cwd = process.cwd();
2520
- const findings = [];
2521
- let scannedFiles = 0;
2522
- let truncated = false;
2523
2699
  const gitignorePath = path11.join(cwd, ".gitignore");
2524
- const hasGitignore = fs10.existsSync(gitignorePath);
2700
+ const hasGitignore = fs11.existsSync(gitignorePath);
2525
2701
  if (!hasGitignore) {
2526
- console.log(chalk8.yellow(` ${ko.secure.noGitignore}`));
2527
- console.log(chalk8.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
2702
+ console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
2703
+ console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
2528
2704
  } else {
2529
- const gitignoreContent = fs10.readFileSync(gitignorePath, "utf-8");
2705
+ const gitignoreContent = fs11.readFileSync(gitignorePath, "utf-8");
2530
2706
  if (!gitignoreContent.includes(".env")) {
2531
- console.log(chalk8.yellow(` ${ko.secure.noEnvInGitignore}`));
2532
- console.log(chalk8.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
2707
+ console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
2708
+ console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
2533
2709
  }
2534
2710
  }
2535
- console.log(chalk8.dim(` ${ko.secure.scanning}
2711
+ console.log(chalk9.dim(` ${ko.secure.scanning}
2536
2712
  `));
2537
- walkProjectFiles(cwd, (filePath, relPath) => {
2538
- scannedFiles++;
2539
- const content = fs10.readFileSync(filePath, "utf-8");
2540
- const lines = content.split("\n");
2541
- for (const pattern of SECRET_PATTERNS) {
2542
- if (truncated) break;
2543
- lines.forEach((line, idx) => {
2544
- if (truncated) return;
2545
- if (line.length > MAX_LINE_CHARS) return;
2546
- const trimmed = line.trim();
2547
- if (trimmed.startsWith("//") && trimmed.includes("example")) return;
2548
- if (trimmed.startsWith("#") && trimmed.includes("example")) return;
2549
- const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
2550
- let match;
2551
- while ((match = regex.exec(line)) !== null) {
2552
- findings.push({
2553
- patternId: pattern.id,
2554
- patternName: pattern.name,
2555
- severity: pattern.severity,
2556
- file: relPath,
2557
- line: idx + 1,
2558
- match: maskSecret(match[0])
2559
- });
2560
- if (findings.length >= MAX_FINDINGS) {
2561
- truncated = true;
2562
- return;
2563
- }
2564
- }
2565
- });
2566
- }
2567
- });
2568
- 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)`));
2713
+ const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
2714
+ 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)`));
2569
2715
  if (truncated) {
2570
- console.log(chalk8.yellow(` \u26A0\uFE0F \uACB0\uACFC ${MAX_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.`));
2716
+ 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.`));
2571
2717
  }
2572
2718
  console.log("");
2573
2719
  if (findings.length === 0) {
2574
- console.log(chalk8.green.bold(` ${ko.secure.clean}`));
2720
+ console.log(chalk9.green.bold(` ${ko.secure.clean}`));
2575
2721
  printNextStep({
2576
2722
  message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
2577
2723
  command: "vhk \uC815\uB9AC",
@@ -2583,45 +2729,45 @@ ${ko.secure.title}
2583
2729
  const high = findings.filter((f) => f.severity === "high");
2584
2730
  const medium = findings.filter((f) => f.severity === "medium");
2585
2731
  if (critical.length > 0) {
2586
- console.log(chalk8.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2732
+ console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2587
2733
  critical.forEach((f) => {
2588
- console.log(chalk8.red(` \u2716 ${f.patternName}`));
2589
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2734
+ console.log(chalk9.red(` \u2716 ${f.patternName}`));
2735
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2590
2736
  });
2591
2737
  console.log("");
2592
2738
  }
2593
2739
  if (high.length > 0) {
2594
- console.log(chalk8.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2740
+ console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2595
2741
  high.forEach((f) => {
2596
- console.log(chalk8.yellow(` \u26A0 ${f.patternName}`));
2597
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2742
+ console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
2743
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2598
2744
  });
2599
2745
  console.log("");
2600
2746
  }
2601
2747
  if (medium.length > 0) {
2602
- console.log(chalk8.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2748
+ console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2603
2749
  medium.forEach((f) => {
2604
- console.log(chalk8.blue(` \u2139 ${f.patternName}`));
2605
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2750
+ console.log(chalk9.blue(` \u2139 ${f.patternName}`));
2751
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2606
2752
  });
2607
2753
  console.log("");
2608
2754
  }
2609
- console.log(chalk8.bold(` ${ko.secure.summary}`));
2610
- console.log(` \uCD1D ${chalk8.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
2755
+ console.log(chalk9.bold(` ${ko.secure.summary}`));
2756
+ console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
2611
2757
  console.log("");
2612
- console.log(chalk8.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
2613
- console.log(chalk8.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
2614
- console.log(chalk8.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
2615
- console.log(chalk8.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
2616
- if (critical.length > 0) {
2758
+ console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
2759
+ 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"));
2760
+ console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
2761
+ console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
2762
+ if (critical.length > 0 || high.length > 0) {
2617
2763
  process.exitCode = 1;
2618
2764
  }
2619
2765
  }
2620
2766
 
2621
2767
  // src/commands/doctor.ts
2622
- import chalk9 from "chalk";
2768
+ import chalk10 from "chalk";
2623
2769
  import { execSync } from "child_process";
2624
- import fs11 from "fs";
2770
+ import fs12 from "fs";
2625
2771
  import path12 from "path";
2626
2772
  import { fileURLToPath } from "url";
2627
2773
  function checkCommand(name, command, hint) {
@@ -2640,8 +2786,8 @@ function getVhkVersion() {
2640
2786
  ];
2641
2787
  for (const pkgPath of candidates) {
2642
2788
  try {
2643
- if (fs11.existsSync(pkgPath)) {
2644
- const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2789
+ if (fs12.existsSync(pkgPath)) {
2790
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2645
2791
  return pkg.version;
2646
2792
  }
2647
2793
  } catch {
@@ -2651,7 +2797,7 @@ function getVhkVersion() {
2651
2797
  return void 0;
2652
2798
  }
2653
2799
  async function doctor() {
2654
- console.log(chalk9.bold(`
2800
+ console.log(chalk10.bold(`
2655
2801
  ${ko.doctor.title}
2656
2802
  `));
2657
2803
  const checks = [
@@ -2663,22 +2809,22 @@ ${ko.doctor.title}
2663
2809
  let allOk = true;
2664
2810
  for (const check2 of checks) {
2665
2811
  if (check2.ok) {
2666
- console.log(chalk9.green(` \u2705 ${check2.name}`) + chalk9.dim(` \u2014 ${check2.version}`));
2812
+ console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
2667
2813
  } else {
2668
- console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2669
- console.log(chalk9.dim(` \u2192 ${check2.hint}`));
2814
+ console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2815
+ console.log(chalk10.dim(` \u2192 ${check2.hint}`));
2670
2816
  allOk = false;
2671
2817
  }
2672
2818
  }
2673
2819
  console.log("");
2674
2820
  const vhkVersion = getVhkVersion();
2675
2821
  if (vhkVersion) {
2676
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
2822
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
2677
2823
  } else {
2678
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(" \u2014 \uC124\uCE58\uB428"));
2824
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
2679
2825
  }
2680
2826
  console.log("");
2681
- console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
2827
+ console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
2682
2828
  const cwd = process.cwd();
2683
2829
  const projectFiles = [
2684
2830
  { name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
@@ -2688,32 +2834,32 @@ ${ko.doctor.title}
2688
2834
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2689
2835
  ];
2690
2836
  for (const file of projectFiles) {
2691
- const exists = fs11.existsSync(path12.join(cwd, file.name));
2837
+ const exists = fs12.existsSync(path12.join(cwd, file.name));
2692
2838
  if (exists) {
2693
- console.log(chalk9.green(` \u2705 ${file.name}`));
2839
+ console.log(chalk10.green(` \u2705 ${file.name}`));
2694
2840
  if (file.name === ".env") {
2695
2841
  const gitignorePath = path12.join(cwd, ".gitignore");
2696
- if (fs11.existsSync(gitignorePath)) {
2697
- const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
2842
+ if (fs12.existsSync(gitignorePath)) {
2843
+ const gitignore = fs12.readFileSync(gitignorePath, "utf-8");
2698
2844
  if (!gitignore.includes(".env")) {
2699
- console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
2845
+ console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
2700
2846
  }
2701
2847
  }
2702
2848
  }
2703
2849
  } else {
2704
- console.log(chalk9.dim(` \u2B1A ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
2850
+ console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
2705
2851
  }
2706
2852
  }
2707
2853
  console.log("");
2708
2854
  if (allOk) {
2709
- console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
2855
+ console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
2710
2856
  printNextStep({
2711
2857
  message: ko.doctor.nextOkMessage,
2712
2858
  command: "vhk \uC2DC\uC791",
2713
2859
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
2714
2860
  });
2715
2861
  } else {
2716
- console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2862
+ console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2717
2863
  printNextStep({
2718
2864
  message: ko.doctor.nextRetryMessage,
2719
2865
  command: "vhk doctor",
@@ -2724,9 +2870,9 @@ ${ko.doctor.title}
2724
2870
  }
2725
2871
 
2726
2872
  // src/commands/ship.ts
2727
- import chalk10 from "chalk";
2873
+ import chalk11 from "chalk";
2728
2874
  import inquirer4 from "inquirer";
2729
- import fs12 from "fs";
2875
+ import fs13 from "fs";
2730
2876
  import path13 from "path";
2731
2877
  var CHECKLIST = [
2732
2878
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
@@ -2740,29 +2886,29 @@ function sanitizeVersion(version) {
2740
2886
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2741
2887
  }
2742
2888
  async function ship() {
2743
- console.log(chalk10.bold(`
2889
+ console.log(chalk11.bold(`
2744
2890
  ${ko.ship.title}
2745
2891
  `));
2746
2892
  const cwd = process.cwd();
2747
- console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
2893
+ console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
2748
2894
  `));
2749
2895
  const { passed } = await inquirer4.prompt([{
2750
2896
  type: "checkbox",
2751
2897
  name: "passed",
2752
2898
  message: ko.ship.checkboxPrompt,
2753
2899
  choices: CHECKLIST.map((c) => ({
2754
- name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
2900
+ name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
2755
2901
  value: c.id
2756
2902
  }))
2757
2903
  }]);
2758
2904
  const allPassed = passed.length === CHECKLIST.length;
2759
2905
  const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
2760
2906
  if (!allPassed) {
2761
- console.log(chalk10.yellow(`
2907
+ console.log(chalk11.yellow(`
2762
2908
  ${ko.ship.incompleteHeader}`));
2763
2909
  skipped.forEach((s) => {
2764
- console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2765
- console.log(chalk10.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2910
+ console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2911
+ console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2766
2912
  });
2767
2913
  const { proceed } = await inquirer4.prompt([{
2768
2914
  type: "confirm",
@@ -2779,13 +2925,13 @@ ${ko.ship.title}
2779
2925
  return;
2780
2926
  }
2781
2927
  } else {
2782
- console.log(chalk10.green(`
2928
+ console.log(chalk11.green(`
2783
2929
  ${ko.ship.allPassed}
2784
2930
  `));
2785
2931
  }
2786
- console.log(chalk10.cyan.bold(` ${ko.ship.retro}
2932
+ console.log(chalk11.cyan.bold(` ${ko.ship.retro}
2787
2933
  `));
2788
- console.log(chalk10.dim(` ${ko.ship.versionHint}`));
2934
+ console.log(chalk11.dim(` ${ko.ship.versionHint}`));
2789
2935
  const retro = await inquirer4.prompt([
2790
2936
  { type: "input", name: "version", message: ko.ship.versionPrompt },
2791
2937
  { type: "input", name: "whatWentWell", message: ko.ship.questionWell },
@@ -2794,7 +2940,7 @@ ${ko.ship.title}
2794
2940
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2795
2941
  ]);
2796
2942
  const buildLogDir = path13.join(cwd, "docs", "build-log");
2797
- if (!fs12.existsSync(buildLogDir)) fs12.mkdirSync(buildLogDir, { recursive: true });
2943
+ if (!fs13.existsSync(buildLogDir)) fs13.mkdirSync(buildLogDir, { recursive: true });
2798
2944
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2799
2945
  const versionSlug = sanitizeVersion(retro.version);
2800
2946
  const fileName = `${today}-v${versionSlug}.md`;
@@ -2827,8 +2973,8 @@ ${ko.ship.title}
2827
2973
  "---",
2828
2974
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2829
2975
  ].join("\n");
2830
- fs12.writeFileSync(filePath, content, "utf-8");
2831
- console.log(chalk10.green(`
2976
+ fs13.writeFileSync(filePath, content, "utf-8");
2977
+ console.log(chalk11.green(`
2832
2978
  ${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
2833
2979
  printNextStep({
2834
2980
  message: ko.ship.deployMessage,
@@ -2839,16 +2985,63 @@ ${ko.ship.title}
2839
2985
  }
2840
2986
 
2841
2987
  // src/commands/save.ts
2842
- import { execFileSync, execSync as execSync2 } from "child_process";
2843
- import chalk11 from "chalk";
2988
+ import { execFileSync as execFileSync2 } from "child_process";
2989
+ import chalk12 from "chalk";
2844
2990
  import ora from "ora";
2845
2991
  import inquirer5 from "inquirer";
2846
- function gitOut(args) {
2847
- return execFileSync("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2992
+
2993
+ // src/lib/git-porcelain.ts
2994
+ function normalizePorcelain(raw) {
2995
+ return raw.replace(/\r\n/g, "\n").trimEnd();
2996
+ }
2997
+ function parsePorcelainLines(raw) {
2998
+ return normalizePorcelain(raw).split("\n").filter(Boolean);
2999
+ }
3000
+
3001
+ // src/lib/git-repo.ts
3002
+ import { execFileSync } from "child_process";
3003
+ function getGitRoot(cwd = process.cwd()) {
3004
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
3005
+ encoding: "utf-8",
3006
+ cwd,
3007
+ stdio: ["pipe", "pipe", "pipe"]
3008
+ }).trim();
3009
+ }
3010
+ function gitOut(args, cwd) {
3011
+ return execFileSync("git", args, {
3012
+ encoding: "utf-8",
3013
+ cwd,
3014
+ stdio: ["pipe", "pipe", "pipe"]
3015
+ });
3016
+ }
3017
+ function gitRun(args, cwd) {
3018
+ execFileSync("git", args, { stdio: "pipe", cwd });
3019
+ }
3020
+ function getExecErrorMessage(err) {
3021
+ if (err && typeof err === "object" && "stderr" in err) {
3022
+ const stderr = err.stderr;
3023
+ if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
3024
+ if (typeof stderr === "string") return stderr.trim();
3025
+ }
3026
+ return err instanceof Error ? err.message : String(err);
2848
3027
  }
2849
- function gitRun(args) {
2850
- execFileSync("git", args, { stdio: "pipe" });
3028
+ function hasGitRemote(cwd) {
3029
+ try {
3030
+ return gitOut(["remote"], cwd).trim().length > 0;
3031
+ } catch {
3032
+ return false;
3033
+ }
2851
3034
  }
3035
+ function countLocalCommits(cwd) {
3036
+ try {
3037
+ const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
3038
+ return parseInt(out, 10) || 0;
3039
+ } catch {
3040
+ return 0;
3041
+ }
3042
+ }
3043
+
3044
+ // src/commands/save.ts
2852
3045
  function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
2853
3046
  const y = date.getFullYear();
2854
3047
  const m = String(date.getMonth() + 1).padStart(2, "0");
@@ -2864,24 +3057,49 @@ function statusIcon(code) {
2864
3057
  return "\u{1F4C4}";
2865
3058
  }
2866
3059
  async function save() {
2867
- console.log(chalk11.bold(`
3060
+ console.log(chalk12.bold(`
2868
3061
  \u{1F4BE} ${t("save.title")}`));
2869
- console.log(chalk11.gray("\u2500".repeat(40)));
3062
+ console.log(chalk12.gray("\u2500".repeat(40)));
3063
+ let gitRoot;
2870
3064
  try {
2871
- execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3065
+ execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3066
+ gitRoot = getGitRoot();
2872
3067
  } catch {
2873
- console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
3068
+ console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
2874
3069
  return;
2875
3070
  }
2876
- const status2 = gitOut(["status", "--porcelain"]).trim();
2877
- if (!status2) {
2878
- console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
3071
+ console.log(chalk12.cyan(`
3072
+ \u{1F512} ${t("save.securityWarnHeader")}`));
3073
+ printSecurityWarnings(gitRoot);
3074
+ const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
3075
+ if (severe.length > 0) {
3076
+ console.log(chalk12.red(`
3077
+ \u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
3078
+ severe.slice(0, 5).forEach((f) => {
3079
+ console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
3080
+ });
3081
+ if (severe.length > 5) {
3082
+ console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
3083
+ }
3084
+ const { proceed } = await inquirer5.prompt([{
3085
+ type: "confirm",
3086
+ name: "proceed",
3087
+ message: t("save.secretsConfirm"),
3088
+ default: false
3089
+ }]);
3090
+ if (!proceed) {
3091
+ console.log(chalk12.gray(t("save.cancelled")));
3092
+ return;
3093
+ }
3094
+ }
3095
+ const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
3096
+ if (lines.length === 0) {
3097
+ console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
2879
3098
  return;
2880
3099
  }
2881
- const files = status2.split("\n").filter(Boolean);
2882
- console.log(chalk11.cyan(`
2883
- \u{1F4CB} ${t("save.filesHeader", files.length)}`));
2884
- files.forEach((line) => {
3100
+ console.log(chalk12.cyan(`
3101
+ \u{1F4CB} ${t("save.filesHeader", lines.length)}`));
3102
+ lines.forEach((line) => {
2885
3103
  const code = line.substring(0, 2);
2886
3104
  const name = line.substring(3);
2887
3105
  console.log(` ${statusIcon(code)} ${name}`);
@@ -2893,43 +3111,62 @@ async function save() {
2893
3111
  default: formatDefaultCommitMessage()
2894
3112
  }]);
2895
3113
  const spinner = ora(t("save.saving")).start();
3114
+ let didAdd = false;
2896
3115
  try {
2897
- gitRun(["add", "."]);
2898
- gitRun(["commit", "-m", message]);
3116
+ gitRun(["add", "."], gitRoot);
3117
+ didAdd = true;
3118
+ gitRun(["commit", "-m", message], gitRoot);
2899
3119
  spinner.text = t("save.pushing");
2900
- try {
2901
- gitRun(["push"]);
2902
- spinner.succeed(t("save.successWithPush"));
2903
- } catch {
3120
+ if (!hasGitRemote(gitRoot)) {
2904
3121
  spinner.succeed(t("save.successLocal"));
2905
- console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
3122
+ console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
3123
+ } else {
3124
+ try {
3125
+ gitRun(["push"], gitRoot);
3126
+ spinner.succeed(t("save.successWithPush"));
3127
+ } catch (pushErr) {
3128
+ spinner.fail(t("save.pushFailed"));
3129
+ console.log(chalk12.red(getExecErrorMessage(pushErr)));
3130
+ console.log(chalk12.yellow(`
3131
+ \u{1F4A1} ${t("save.commitOkPushFailed")}`));
3132
+ process.exitCode = 1;
3133
+ }
3134
+ }
3135
+ if (process.exitCode !== 1) {
3136
+ console.log(chalk12.green(`
3137
+ \u2705 ${t("save.done", lines.length)}`));
3138
+ } else {
3139
+ console.log(chalk12.green(`
3140
+ \u2705 ${t("save.doneLocalOnly", lines.length)}`));
2906
3141
  }
2907
- console.log(chalk11.green(`
2908
- \u2705 ${t("save.done", files.length)}`));
2909
3142
  } catch (err) {
2910
3143
  spinner.fail(t("save.failed"));
2911
- const msg = err instanceof Error ? err.message : String(err);
2912
- console.log(chalk11.red(msg));
3144
+ console.log(chalk12.red(getExecErrorMessage(err)));
3145
+ if (didAdd) {
3146
+ try {
3147
+ const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
3148
+ if (staged) {
3149
+ console.log(chalk12.yellow(`
3150
+ \u{1F4A1} ${t("save.stagedAfterFail")}`));
3151
+ }
3152
+ } catch {
3153
+ }
3154
+ }
2913
3155
  process.exitCode = 1;
2914
3156
  }
2915
3157
  }
2916
3158
 
2917
3159
  // src/commands/undo.ts
2918
- import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
2919
- import chalk12 from "chalk";
3160
+ import { execFileSync as execFileSync3 } from "child_process";
3161
+ import chalk13 from "chalk";
2920
3162
  import inquirer6 from "inquirer";
2921
- function gitOut2(args) {
2922
- return execFileSync2("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2923
- }
2924
- function gitRun2(args) {
2925
- execFileSync2("git", args, { stdio: "pipe" });
2926
- }
2927
3163
  function parseRecentCommits(logOutput) {
2928
3164
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
2929
3165
  }
2930
- function countUnpushedCommits() {
3166
+ function countUnpushedCommits(gitRoot) {
3167
+ const cwd = gitRoot ?? process.cwd();
2931
3168
  try {
2932
- const out = gitOut2(["rev-list", "--count", "@{u}..HEAD"]).trim();
3169
+ const out = gitOut(["rev-list", "--count", "@{u}..HEAD"], cwd).trim();
2933
3170
  return parseInt(out, 10) || 0;
2934
3171
  } catch {
2935
3172
  return -1;
@@ -2939,29 +3176,36 @@ function willUndoPushedCommits(undoCount, unpushedCount) {
2939
3176
  if (unpushedCount < 0) return false;
2940
3177
  return undoCount > unpushedCount;
2941
3178
  }
3179
+ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
3180
+ if (willUndoPushedCommits(undoCount, unpushedCount)) return true;
3181
+ if (unpushedCount < 0 && hasRemote) return true;
3182
+ return false;
3183
+ }
2942
3184
  async function undo() {
2943
- console.log(chalk12.bold(`
3185
+ console.log(chalk13.bold(`
2944
3186
  \u23EA ${t("undo.title")}`));
2945
- console.log(chalk12.gray("\u2500".repeat(40)));
3187
+ console.log(chalk13.gray("\u2500".repeat(40)));
3188
+ let gitRoot;
2946
3189
  try {
2947
- execSync3("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3190
+ execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3191
+ gitRoot = getGitRoot();
2948
3192
  } catch {
2949
- console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
3193
+ console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
2950
3194
  return;
2951
3195
  }
2952
3196
  let logOutput;
2953
3197
  try {
2954
- logOutput = gitOut2(["log", "--oneline", "-5"]).trim();
3198
+ logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
2955
3199
  } catch {
2956
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3200
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2957
3201
  return;
2958
3202
  }
2959
3203
  const commits = parseRecentCommits(logOutput);
2960
3204
  if (commits.length === 0) {
2961
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3205
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2962
3206
  return;
2963
3207
  }
2964
- console.log(chalk12.cyan(`
3208
+ console.log(chalk13.cyan(`
2965
3209
  ${t("undo.recentHeader")}`));
2966
3210
  commits.forEach((c, i) => {
2967
3211
  console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
@@ -2976,135 +3220,68 @@ ${t("undo.recentHeader")}`));
2976
3220
  max: maxUndo
2977
3221
  }]);
2978
3222
  const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
2979
- const unpushed = countUnpushedCommits();
2980
- if (willUndoPushedCommits(undoCount, unpushed)) {
2981
- console.log(chalk12.red(`
3223
+ const headCount = countLocalCommits(gitRoot);
3224
+ if (undoCount >= headCount) {
3225
+ console.log(chalk13.yellow(`
3226
+ \u{1F4ED} ${t("undo.rootCommit")}`));
3227
+ return;
3228
+ }
3229
+ const unpushed = countUnpushedCommits(gitRoot);
3230
+ const remote = hasGitRemote(gitRoot);
3231
+ const risky = isUndoRisky(undoCount, unpushed, remote);
3232
+ if (risky) {
3233
+ if (unpushed < 0) {
3234
+ console.log(chalk13.red(`
3235
+ \u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
3236
+ } else {
3237
+ console.log(chalk13.red(`
2982
3238
  \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
3239
+ }
2983
3240
  }
2984
3241
  const { confirm } = await inquirer6.prompt([{
2985
3242
  type: "confirm",
2986
3243
  name: "confirm",
2987
- message: t("undo.confirmMessage", undoCount),
3244
+ message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
2988
3245
  default: false
2989
3246
  }]);
2990
3247
  if (!confirm) {
2991
- console.log(chalk12.gray(t("undo.cancelled")));
3248
+ console.log(chalk13.gray(t("undo.cancelled")));
2992
3249
  return;
2993
3250
  }
2994
3251
  try {
2995
- gitRun2(["reset", "--soft", `HEAD~${undoCount}`]);
2996
- console.log(chalk12.green(`
2997
- \u2705 ${t("undo.success", undoCount)}`));
2998
- console.log(chalk12.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3252
+ gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
3253
+ console.log(chalk13.green(`
3254
+ \u2705 ${t("undo.success")}`));
3255
+ console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3256
+ if (risky) {
3257
+ console.log(chalk13.yellow(`
3258
+ \u{1F4A1} ${t("undo.forcePushHint")}`));
3259
+ }
2999
3260
  } catch (err) {
3000
- console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3261
+ console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
3001
3262
  const msg = err instanceof Error ? err.message : String(err);
3002
- console.log(chalk12.red(msg));
3263
+ console.log(chalk13.red(msg));
3003
3264
  process.exitCode = 1;
3004
3265
  }
3005
3266
  }
3006
3267
 
3007
- // src/commands/diff.ts
3008
- import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
3009
- import chalk13 from "chalk";
3010
- function gitOut3(args) {
3011
- try {
3012
- return execFileSync3("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3013
- } catch {
3014
- return "";
3015
- }
3016
- }
3017
- function parseDiffStat(stat) {
3018
- const files = [];
3019
- const lines = stat.split("\n");
3020
- for (const line of lines) {
3021
- const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
3022
- if (!match) continue;
3023
- const name = match[1].trim();
3024
- if (name.includes("changed") || name.includes("file")) continue;
3025
- const plusMatch = line.match(/(\++)/);
3026
- const minusMatch = line.match(/(\-+)/);
3027
- files.push({
3028
- name,
3029
- additions: plusMatch ? plusMatch[1].length : 0,
3030
- deletions: minusMatch ? minusMatch[1].length : 0
3031
- });
3032
- }
3033
- return files;
3034
- }
3035
- function summarizeNumstat(numstat) {
3036
- let totalAdd = 0;
3037
- let totalDel = 0;
3038
- let fileCount = 0;
3039
- for (const line of numstat.split("\n").filter(Boolean)) {
3040
- const [add, del] = line.split(" ");
3041
- if (add === void 0 || del === void 0) continue;
3042
- totalAdd += parseInt(add, 10) || 0;
3043
- totalDel += parseInt(del, 10) || 0;
3044
- fileCount++;
3045
- }
3046
- return { fileCount, totalAdd, totalDel };
3047
- }
3048
- function printFile(f) {
3049
- const adds = f.additions > 0 ? chalk13.green(`+${f.additions}`) : "";
3050
- const dels = f.deletions > 0 ? chalk13.red(`-${f.deletions}`) : "";
3051
- const change = [adds, dels].filter(Boolean).join(" ");
3052
- console.log(` ${f.name} ${change}`);
3053
- }
3054
- async function diff() {
3055
- console.log(chalk13.bold(`
3056
- \u{1F50D} ${t("diff.title")}`));
3057
- console.log(chalk13.gray("\u2500".repeat(40)));
3058
- try {
3059
- execSync4("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3060
- } catch {
3061
- console.log(chalk13.red(`\u274C ${t("diff.notGitRepo")}`));
3062
- return;
3063
- }
3064
- const unstaged = gitOut3(["diff", "--stat"]);
3065
- const staged = gitOut3(["diff", "--cached", "--stat"]);
3066
- const untracked = gitOut3(["ls-files", "--others", "--exclude-standard"]);
3067
- if (!unstaged && !staged && !untracked) {
3068
- console.log(chalk13.green(`
3069
- \u2705 ${t("diff.noChanges")}`));
3070
- return;
3071
- }
3072
- if (staged) {
3073
- console.log(chalk13.cyan(`
3074
- ${t("diff.stagedHeader")}`));
3075
- parseDiffStat(staged).forEach((f) => printFile(f));
3076
- }
3077
- if (unstaged) {
3078
- console.log(chalk13.cyan(`
3079
- ${t("diff.unstagedHeader")}`));
3080
- parseDiffStat(unstaged).forEach((f) => printFile(f));
3081
- }
3082
- if (untracked) {
3083
- const files = untracked.split("\n").filter(Boolean);
3084
- console.log(chalk13.cyan(`
3085
- ${t("diff.untrackedHeader", files.length)}`));
3086
- files.forEach((f) => console.log(` ${chalk13.green("+")} ${f}`));
3087
- }
3088
- const numstat = gitOut3(["diff", "--numstat", "HEAD"]);
3089
- if (numstat) {
3090
- const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3091
- console.log(chalk13.cyan(`
3092
- ${t("diff.summaryHeader")}`));
3093
- console.log(` ${t("diff.filesLine", fileCount)}`);
3094
- console.log(` \uCD94\uAC00: ${chalk13.green(`+${totalAdd}`)}\uC904`);
3095
- console.log(` \uC0AD\uC81C: ${chalk13.red(`-${totalDel}`)}\uC904`);
3096
- }
3097
- console.log("");
3098
- }
3099
-
3100
3268
  // src/commands/status.ts
3101
- import { execFileSync as execFileSync4, execSync as execSync5 } from "child_process";
3102
- import fs13 from "fs";
3269
+ import { execFileSync as execFileSync4 } from "child_process";
3270
+ import fs15 from "fs";
3103
3271
  import path14 from "path";
3104
3272
  import chalk14 from "chalk";
3105
- function gitOut4(args) {
3106
- return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3273
+
3274
+ // src/lib/read-json.ts
3275
+ import fs14 from "fs";
3276
+ function stripBom(text) {
3277
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
3278
+ }
3279
+ function readJsonFile(filePath) {
3280
+ const raw = stripBom(fs14.readFileSync(filePath, "utf-8"));
3281
+ return JSON.parse(raw);
3107
3282
  }
3283
+
3284
+ // src/commands/status.ts
3108
3285
  function countFileChanges(porcelain) {
3109
3286
  const lines = porcelain.split("\n").filter(Boolean);
3110
3287
  let staged = 0;
@@ -3143,9 +3320,9 @@ function parseRecentCommitLines(logOutput) {
3143
3320
  }
3144
3321
  function readProjectPackage(cwd = process.cwd()) {
3145
3322
  const pkgPath = path14.join(cwd, "package.json");
3146
- if (!fs13.existsSync(pkgPath)) return null;
3323
+ if (!fs15.existsSync(pkgPath)) return null;
3147
3324
  try {
3148
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3325
+ const pkg = readJsonFile(pkgPath);
3149
3326
  if (!pkg.name && !pkg.version) return null;
3150
3327
  return {
3151
3328
  name: pkg.name ?? "(no name)",
@@ -3155,9 +3332,9 @@ function readProjectPackage(cwd = process.cwd()) {
3155
3332
  return null;
3156
3333
  }
3157
3334
  }
3158
- function getSyncCounts() {
3335
+ function getSyncCounts(gitRoot) {
3159
3336
  try {
3160
- const out = gitOut4(["rev-list", "--left-right", "--count", "HEAD...@{u}"]);
3337
+ const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
3161
3338
  return parseSyncCounts(out);
3162
3339
  } catch {
3163
3340
  return { ahead: 0, behind: 0, hasUpstream: false };
@@ -3167,24 +3344,26 @@ async function status() {
3167
3344
  console.log(chalk14.bold(`
3168
3345
  \u{1F4CA} ${t("status.title")}`));
3169
3346
  console.log(chalk14.gray("\u2500".repeat(40)));
3347
+ let gitRoot;
3170
3348
  try {
3171
- execSync5("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3349
+ execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3350
+ gitRoot = getGitRoot();
3172
3351
  } catch {
3173
3352
  console.log(chalk14.red(`\u274C ${t("status.notGitRepo")}`));
3174
3353
  return;
3175
3354
  }
3176
3355
  let branch;
3177
3356
  try {
3178
- branch = gitOut4(["branch", "--show-current"]).trim() || t("status.detached");
3357
+ branch = gitOut(["branch", "--show-current"], gitRoot).trim() || t("status.detached");
3179
3358
  } catch {
3180
3359
  branch = t("status.unknownBranch");
3181
3360
  }
3182
- const porcelain = gitOut4(["status", "--porcelain"]).trim();
3361
+ const porcelain = normalizePorcelain(gitOut(["status", "--porcelain"], gitRoot));
3183
3362
  const counts = countFileChanges(porcelain);
3184
- const sync2 = getSyncCounts();
3363
+ const sync2 = getSyncCounts(gitRoot);
3185
3364
  let commits = [];
3186
3365
  try {
3187
- commits = parseRecentCommitLines(gitOut4(["log", "--oneline", "-3"]).trim());
3366
+ commits = parseRecentCommitLines(gitOut(["log", "--oneline", "-3"], gitRoot).trim());
3188
3367
  } catch {
3189
3368
  commits = [];
3190
3369
  }
@@ -3216,6 +3395,158 @@ async function status() {
3216
3395
  console.log("");
3217
3396
  }
3218
3397
 
3398
+ // src/commands/diff.ts
3399
+ import { execFileSync as execFileSync5, execSync as execSync2 } from "child_process";
3400
+ import chalk15 from "chalk";
3401
+ function gitOut2(args) {
3402
+ try {
3403
+ return execFileSync5("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3404
+ } catch {
3405
+ return "";
3406
+ }
3407
+ }
3408
+ function parseDiffStat(stat) {
3409
+ const files = [];
3410
+ const lines = stat.split("\n");
3411
+ for (const line of lines) {
3412
+ const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
3413
+ if (!match) continue;
3414
+ const name = match[1].trim();
3415
+ if (name.includes("changed") || name.includes("file")) continue;
3416
+ const plusMatch = line.match(/(\++)/);
3417
+ const minusMatch = line.match(/(\-+)/);
3418
+ files.push({
3419
+ name,
3420
+ additions: plusMatch ? plusMatch[1].length : 0,
3421
+ deletions: minusMatch ? minusMatch[1].length : 0
3422
+ });
3423
+ }
3424
+ return files;
3425
+ }
3426
+ function summarizeNumstat(numstat) {
3427
+ let totalAdd = 0;
3428
+ let totalDel = 0;
3429
+ let fileCount = 0;
3430
+ for (const line of numstat.split("\n").filter(Boolean)) {
3431
+ const [add, del] = line.split(" ");
3432
+ if (add === void 0 || del === void 0) continue;
3433
+ totalAdd += parseInt(add, 10) || 0;
3434
+ totalDel += parseInt(del, 10) || 0;
3435
+ fileCount++;
3436
+ }
3437
+ return { fileCount, totalAdd, totalDel };
3438
+ }
3439
+ function printFile(f) {
3440
+ const adds = f.additions > 0 ? chalk15.green(`+${f.additions}`) : "";
3441
+ const dels = f.deletions > 0 ? chalk15.red(`-${f.deletions}`) : "";
3442
+ const change = [adds, dels].filter(Boolean).join(" ");
3443
+ console.log(` ${f.name} ${change}`);
3444
+ }
3445
+ async function diff() {
3446
+ console.log(chalk15.bold(`
3447
+ \u{1F50D} ${t("diff.title")}`));
3448
+ console.log(chalk15.gray("\u2500".repeat(40)));
3449
+ try {
3450
+ execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3451
+ } catch {
3452
+ console.log(chalk15.red(`\u274C ${t("diff.notGitRepo")}`));
3453
+ return;
3454
+ }
3455
+ const unstaged = gitOut2(["diff", "--stat"]);
3456
+ const staged = gitOut2(["diff", "--cached", "--stat"]);
3457
+ const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3458
+ if (!unstaged && !staged && !untracked) {
3459
+ console.log(chalk15.green(`
3460
+ \u2705 ${t("diff.noChanges")}`));
3461
+ return;
3462
+ }
3463
+ if (staged) {
3464
+ console.log(chalk15.cyan(`
3465
+ ${t("diff.stagedHeader")}`));
3466
+ parseDiffStat(staged).forEach((f) => printFile(f));
3467
+ }
3468
+ if (unstaged) {
3469
+ console.log(chalk15.cyan(`
3470
+ ${t("diff.unstagedHeader")}`));
3471
+ parseDiffStat(unstaged).forEach((f) => printFile(f));
3472
+ }
3473
+ if (untracked) {
3474
+ const files = untracked.split("\n").filter(Boolean);
3475
+ console.log(chalk15.cyan(`
3476
+ ${t("diff.untrackedHeader", files.length)}`));
3477
+ files.forEach((f) => console.log(` ${chalk15.green("+")} ${f}`));
3478
+ }
3479
+ const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3480
+ if (numstat) {
3481
+ const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3482
+ console.log(chalk15.cyan(`
3483
+ ${t("diff.summaryHeader")}`));
3484
+ console.log(` ${t("diff.filesLine", fileCount)}`);
3485
+ console.log(` \uCD94\uAC00: ${chalk15.green(`+${totalAdd}`)}\uC904`);
3486
+ console.log(` \uC0AD\uC81C: ${chalk15.red(`-${totalDel}`)}\uC904`);
3487
+ }
3488
+ console.log("");
3489
+ }
3490
+
3491
+ // src/lib/nlp-run.ts
3492
+ async function dispatchNlpRoute(route, input) {
3493
+ switch (route.command) {
3494
+ case "gate":
3495
+ return gate();
3496
+ case "init":
3497
+ return init({
3498
+ skipGate: route.args?.includes("--skip-gate"),
3499
+ fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
3500
+ });
3501
+ case "recap":
3502
+ return recap({});
3503
+ case "sync":
3504
+ return sync();
3505
+ case "check":
3506
+ return check();
3507
+ case "secure":
3508
+ return secure();
3509
+ case "ship":
3510
+ return ship();
3511
+ case "doctor":
3512
+ return doctor();
3513
+ case "save":
3514
+ return save();
3515
+ case "undo":
3516
+ return undo();
3517
+ case "status":
3518
+ return status();
3519
+ case "diff":
3520
+ return diff();
3521
+ }
3522
+ }
3523
+ async function runNaturalLanguageRoute(input) {
3524
+ const route = routeNaturalLanguage(input);
3525
+ if (!route) {
3526
+ console.log(chalk16.yellow(`
3527
+ \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
3528
+ `));
3529
+ return;
3530
+ }
3531
+ console.log("");
3532
+ console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
3533
+ console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
3534
+ if (route.confidence === "low") {
3535
+ const { confirm } = await inquirer7.prompt([{
3536
+ type: "confirm",
3537
+ name: "confirm",
3538
+ message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
3539
+ default: true
3540
+ }]);
3541
+ if (!confirm) {
3542
+ console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
3543
+ return;
3544
+ }
3545
+ }
3546
+ console.log("");
3547
+ await dispatchNlpRoute(route, input);
3548
+ }
3549
+
3219
3550
  // src/index.ts
3220
3551
  var program = new Command();
3221
3552
  var defaultHelp = new Help();
@@ -3233,7 +3564,7 @@ var KO_ALIASES = {
3233
3564
  status: "\uC0C1\uD0DC",
3234
3565
  diff: "\uBCC0\uACBD"
3235
3566
  };
3236
- 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.0");
3567
+ 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.2");
3237
3568
  program.configureHelp({
3238
3569
  formatHelp(cmd, helper) {
3239
3570
  if (cmd.parent) {
@@ -3259,7 +3590,7 @@ program.command("init").alias("\uC2DC\uC791").alias("\uB9CC\uB4E4\uAE30").descri
3259
3590
  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);
3260
3591
  program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
3261
3592
  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);
3262
- var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C");
3593
+ 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);
3263
3594
  secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
3264
3595
  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);
3265
3596
  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);
@@ -3274,62 +3605,14 @@ program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\
3274
3605
  });
3275
3606
  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);
3276
3607
  program.on("command:*", async (operands) => {
3277
- const input = operands.join(" ");
3278
- const route = routeNaturalLanguage(input);
3279
- if (route) {
3280
- console.log("");
3281
- console.log(chalk15.cyan(` \u{1F4AC} "${input}"`));
3282
- console.log(chalk15.cyan(` \u2192 ${route.explanation}`));
3283
- if (route.confidence === "low") {
3284
- const { confirm } = await inquirer7.prompt([{
3285
- type: "confirm",
3286
- name: "confirm",
3287
- message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
3288
- default: true
3289
- }]);
3290
- if (!confirm) {
3291
- console.log(chalk15.dim(` ${ko.nlp.menuHint}`));
3292
- return;
3293
- }
3294
- }
3295
- console.log("");
3296
- switch (route.command) {
3297
- case "gate":
3298
- return gate();
3299
- case "init":
3300
- return init({
3301
- skipGate: route.args?.includes("--skip-gate"),
3302
- fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
3303
- });
3304
- case "recap":
3305
- return recap({});
3306
- case "sync":
3307
- return sync();
3308
- case "check":
3309
- return check();
3310
- case "secure":
3311
- return secure();
3312
- case "ship":
3313
- return ship();
3314
- case "doctor":
3315
- return doctor();
3316
- case "save":
3317
- return save();
3318
- case "undo":
3319
- return undo();
3320
- case "status":
3321
- return status();
3322
- case "diff":
3323
- return diff();
3324
- }
3325
- }
3326
- console.log(chalk15.yellow(`
3327
- \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
3328
- `));
3608
+ const unknown = operands[0] ?? "";
3609
+ const rest = operands.slice(1);
3610
+ const input = [unknown, ...rest].join(" ").trim();
3611
+ await runNaturalLanguageRoute(input);
3329
3612
  });
3330
3613
  program.action(async () => {
3331
3614
  console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
3332
- const { choice } = await inquirer7.prompt([{
3615
+ const { choice } = await inquirer8.prompt([{
3333
3616
  type: "list",
3334
3617
  name: "choice",
3335
3618
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -3375,4 +3658,9 @@ program.action(async () => {
3375
3658
  return diff();
3376
3659
  }
3377
3660
  });
3378
- program.parse();
3661
+ var nlInput = detectNaturalLanguageInput(process.argv);
3662
+ if (nlInput !== null) {
3663
+ await runNaturalLanguageRoute(nlInput);
3664
+ } else {
3665
+ await program.parseAsync(process.argv);
3666
+ }