@byh3071/vhk 0.5.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.
Files changed (3) hide show
  1. package/README.md +36 -9
  2. package/dist/index.js +573 -375
  3. package/package.json +56 -56
package/dist/index.js CHANGED
@@ -485,7 +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";
488
+ import chalk16 from "chalk";
489
489
  import inquirer7 from "inquirer";
490
490
 
491
491
  // src/lib/nlp-router.ts
@@ -495,7 +495,7 @@ function normalize(input) {
495
495
  var NLP_KEYWORDS = {
496
496
  save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
497
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", "\uD655\uC778"],
498
+ status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"],
499
499
  diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
500
500
  };
501
501
  function matchesKeywords(text, command) {
@@ -524,6 +524,24 @@ var RULES = [
524
524
  confidence: "high",
525
525
  test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|하네스|초기화/.test(t2) || /^시작$/.test(t2)
526
526
  },
527
+ {
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)",
536
+ confidence: "high",
537
+ test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
538
+ },
539
+ {
540
+ command: "doctor",
541
+ explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
542
+ confidence: "high",
543
+ test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단|확인)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
544
+ },
527
545
  {
528
546
  command: "diff",
529
547
  explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
@@ -540,7 +558,7 @@ var RULES = [
540
558
  command: "status",
541
559
  explanation: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778 (vhk \uC0C1\uD0DC)",
542
560
  confidence: "high",
543
- test: (t2) => matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태/.test(t2)
561
+ test: (t2) => (matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태|프로젝트\s*상태/.test(t2)) && !/보안|시크릿|규칙|점검|린트|환경|진단|doctor|secure|check|스캔|설치/.test(t2)
544
562
  },
545
563
  {
546
564
  command: "save",
@@ -554,30 +572,12 @@ var RULES = [
554
572
  confidence: "high",
555
573
  test: (t2) => /오늘.*(정리|기록)|한\s*일|세션|회고|recap|정리해/.test(t2)
556
574
  },
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
575
  {
564
576
  command: "gate",
565
577
  explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
566
578
  confidence: "high",
567
579
  test: (t2) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t2)
568
580
  },
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
581
  {
582
582
  command: "sync",
583
583
  explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
@@ -642,7 +642,15 @@ var ko = {
642
642
  successLocal: "\uB85C\uCEEC \uC800\uC7A5 \uC644\uB8CC!",
643
643
  noRemote: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 push\uB97C \uAC74\uB108\uB6F0\uC5C8\uC2B5\uB2C8\uB2E4.",
644
644
  failed: "\uC800\uC7A5 \uC2E4\uD328",
645
- done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`
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)`
646
654
  },
647
655
  undo: {
648
656
  title: "\uB418\uB3CC\uB9AC\uAE30",
@@ -651,10 +659,14 @@ var ko = {
651
659
  recentHeader: "\u{1F4CB} \uCD5C\uADFC \uCEE4\uBC0B:",
652
660
  howMany: "\uBA87 \uAC1C\uC758 \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9B4\uAE4C\uC694?",
653
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.",
654
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?`,
655
665
  cancelled: "\uCDE8\uC18C\uB428",
656
666
  success: "\uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC! \uBCC0\uACBD\uC0AC\uD56D\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.",
657
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)",
658
670
  failed: "\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328"
659
671
  },
660
672
  diff: {
@@ -1016,9 +1028,9 @@ ${ko.gate.verdictTitle}
1016
1028
 
1017
1029
  // src/commands/init.ts
1018
1030
  import inquirer2 from "inquirer";
1019
- import chalk4 from "chalk";
1020
- import fs2 from "fs";
1021
- import path2 from "path";
1031
+ import chalk5 from "chalk";
1032
+ import fs3 from "fs";
1033
+ import path3 from "path";
1022
1034
 
1023
1035
  // src/templates/claude-md.ts
1024
1036
  function CLAUDE_MD_TEMPLATE(name, _stack) {
@@ -1228,27 +1240,113 @@ function COMMANDS_MD_TEMPLATE() {
1228
1240
  ].join("\n");
1229
1241
  }
1230
1242
 
1231
- // src/utils/logger.ts
1243
+ // src/lib/check-secure.ts
1244
+ var import_ignore = __toESM(require_ignore(), 1);
1245
+ import fs from "fs";
1246
+ import path from "path";
1232
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";
1233
1331
  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(`
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(`
1239
1337
  \u25B8 ${msg}`))
1240
1338
  };
1241
1339
 
1242
1340
  // src/utils/file.ts
1243
- import fs from "fs";
1244
- import path from "path";
1341
+ import fs2 from "fs";
1342
+ import path2 from "path";
1245
1343
  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");
1344
+ const dir = path2.dirname(filePath);
1345
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
1346
+ fs2.writeFileSync(filePath, content, "utf-8");
1249
1347
  }
1250
1348
  function fileExists(filePath) {
1251
- return fs.existsSync(filePath);
1349
+ return fs2.existsSync(filePath);
1252
1350
  }
1253
1351
 
1254
1352
  // src/lib/notion-import.ts
@@ -1440,13 +1538,14 @@ async function collectAnswers(options, defaults = {}) {
1440
1538
  async function init(options = {}) {
1441
1539
  const skipGate = Boolean(options.skipGate || options.fromNotion);
1442
1540
  if (skipGate) {
1443
- console.log(chalk4.dim(`
1541
+ console.log(chalk5.dim(`
1444
1542
  ${ko.init.skipGate}
1445
1543
  `));
1446
1544
  }
1447
- console.log(chalk4.bold(`
1545
+ console.log(chalk5.bold(`
1448
1546
  ${ko.init.title}
1449
1547
  `));
1548
+ printSecurityWarnings();
1450
1549
  let prdContent = {};
1451
1550
  const defaults = {};
1452
1551
  if (options.fromNotion) {
@@ -1468,7 +1567,7 @@ ${ko.init.title}
1468
1567
  process.exit(1);
1469
1568
  }
1470
1569
  const stack = STACK_PRESETS[answers.type];
1471
- console.log(chalk4.dim(`
1570
+ console.log(chalk5.dim(`
1472
1571
  ${ko.init.recommendedStack} ${stack.join(" + ")}
1473
1572
  `));
1474
1573
  if (!options.yes) {
@@ -1487,7 +1586,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1487
1586
  const files = generateFiles(answers.name, answers.description, stack, prdContent);
1488
1587
  log.step(ko.init.filesGenerating);
1489
1588
  for (const [filePath, content] of Object.entries(files)) {
1490
- const fullPath = path2.join(cwd, filePath);
1589
+ const fullPath = path3.join(cwd, filePath);
1491
1590
  if (fileExists(fullPath)) {
1492
1591
  const { overwrite } = await inquirer2.prompt([{
1493
1592
  type: "confirm",
@@ -1504,21 +1603,21 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1504
1603
  log.success(filePath);
1505
1604
  }
1506
1605
  await writeInitExtras(cwd);
1507
- console.log(chalk4.bold.green(`
1606
+ console.log(chalk5.bold.green(`
1508
1607
  ${ko.init.done}`));
1509
- console.log(chalk4.dim(`
1608
+ console.log(chalk5.dim(`
1510
1609
  ${ko.init.nextSteps}`));
1511
1610
  if (options.fromNotion) {
1512
1611
  console.log(` 1. ${ko.init.notionReviewHint}`);
1513
1612
  console.log(` 2. ${ko.init.gitHintLabel}`);
1514
- console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
1613
+ console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
1515
1614
  console.log(` 3. ${ko.init.startDev}
1516
1615
  `);
1517
1616
  } else {
1518
1617
  console.log(` 1. ${ko.init.fillHint}`);
1519
1618
  console.log(` 2. ${ko.init.prdHint}`);
1520
1619
  console.log(` 3. ${ko.init.gitHintLabel}`);
1521
- console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
1620
+ console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
1522
1621
  console.log(` 4. ${ko.init.startDev}
1523
1622
  `);
1524
1623
  }
@@ -1558,7 +1657,7 @@ function generateFiles(name, description, stack, prdContent = {}) {
1558
1657
  };
1559
1658
  }
1560
1659
  var VHK_PACKAGE_SCRIPTS = {
1561
- save: "git add . && git commit -m",
1660
+ save: "vhk save",
1562
1661
  check: "vhk check",
1563
1662
  scan: "vhk secure scan",
1564
1663
  recap: "vhk recap",
@@ -1566,15 +1665,15 @@ var VHK_PACKAGE_SCRIPTS = {
1566
1665
  doctor: "vhk doctor"
1567
1666
  };
1568
1667
  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"));
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"));
1572
1671
  pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
1573
- fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1672
+ fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1574
1673
  return true;
1575
1674
  }
1576
1675
  async function writeInitExtras(projectDir) {
1577
- const commandsPath = path2.join(projectDir, "COMMANDS.md");
1676
+ const commandsPath = path3.join(projectDir, "COMMANDS.md");
1578
1677
  if (fileExists(commandsPath)) {
1579
1678
  const { overwrite } = await inquirer2.prompt([{
1580
1679
  type: "confirm",
@@ -1599,37 +1698,13 @@ async function writeInitExtras(projectDir) {
1599
1698
 
1600
1699
  // src/commands/recap.ts
1601
1700
  import inquirer3 from "inquirer";
1602
- import chalk5 from "chalk";
1701
+ import chalk6 from "chalk";
1603
1702
  import fs5 from "fs";
1604
1703
  import path6 from "path";
1605
1704
 
1606
1705
  // src/lib/git.ts
1607
1706
  import path4 from "path";
1608
1707
  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
1708
  var git = simpleGit();
1634
1709
  function isNoiseRecapPath(filePath) {
1635
1710
  const base = path4.basename(filePath);
@@ -1646,29 +1721,19 @@ function filterRecapFiles(files) {
1646
1721
  const tracked = new Set(filterTrackedPaths(paths));
1647
1722
  return files.filter((f) => tracked.has(f.file) && !isNoiseRecapPath(f.file));
1648
1723
  }
1649
- function fileStatus(workingDir) {
1650
- if (workingDir === "?") return "new";
1651
- if (workingDir === "D") return "deleted";
1652
- 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";
1653
1727
  return "modified";
1654
1728
  }
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
- );
1729
+ function buildSessionDiffFromSummary(diffSummary) {
1662
1730
  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
- })
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
+ }))
1672
1737
  );
1673
1738
  return {
1674
1739
  filesChanged: files.length,
@@ -1677,6 +1742,11 @@ async function getSessionDiff(since) {
1677
1742
  files
1678
1743
  };
1679
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
+ }
1680
1750
  async function getRecentCommits(count = 10, since) {
1681
1751
  const options = { maxCount: count };
1682
1752
  if (since) options["--since"] = since;
@@ -1787,39 +1857,40 @@ function createAdrFile(cwd, title, context, decision, consequences) {
1787
1857
 
1788
1858
  // src/commands/recap.ts
1789
1859
  async function recap(options = {}) {
1790
- console.log(chalk5.bold(`
1860
+ console.log(chalk6.bold(`
1791
1861
  ${ko.recap.title}
1792
1862
  `));
1793
1863
  if (!await isGitRepo()) {
1794
- console.log(chalk5.red(ko.recap.noRepo));
1864
+ console.log(chalk6.red(ko.recap.noRepo));
1795
1865
  return;
1796
1866
  }
1797
- console.log(chalk5.dim(`${ko.recap.analyzing}
1867
+ printSecurityWarnings();
1868
+ console.log(chalk6.dim(`${ko.recap.analyzing}
1798
1869
  `));
1799
1870
  const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1800
1871
  const diff2 = await getSessionDiff(since);
1801
1872
  const commits = await getRecentCommits(10, since);
1802
1873
  if (diff2.filesChanged === 0 && commits.length === 0) {
1803
- console.log(chalk5.yellow(ko.recap.noChanges));
1874
+ console.log(chalk6.yellow(ko.recap.noChanges));
1804
1875
  return;
1805
1876
  }
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)}`);
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)}`);
1809
1880
  if (diff2.files.length > 0) {
1810
- console.log(chalk5.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1881
+ console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1811
1882
  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");
1883
+ const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
1813
1884
  console.log(` ${icon} ${f.file}`);
1814
1885
  });
1815
1886
  if (diff2.files.length > 15) {
1816
- console.log(chalk5.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1887
+ console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1817
1888
  }
1818
1889
  }
1819
1890
  if (commits.length > 0) {
1820
- console.log(chalk5.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1891
+ console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1821
1892
  commits.slice(0, 5).forEach((c) => {
1822
- console.log(chalk5.dim(` \u2022 ${c.message}`));
1893
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1823
1894
  });
1824
1895
  }
1825
1896
  console.log("");
@@ -1887,11 +1958,11 @@ ${ko.recap.title}
1887
1958
  fs5.writeFileSync(filePath, content, "utf-8");
1888
1959
  const adrCandidates = detectAdrCandidates(diff2);
1889
1960
  if (adrCandidates.length > 0) {
1890
- console.log(chalk5.cyan.bold(`
1961
+ console.log(chalk6.cyan.bold(`
1891
1962
  ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1892
1963
  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}`)));
1964
+ console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
1965
+ candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
1895
1966
  }
1896
1967
  const { createAdr } = await inquirer3.prompt([{
1897
1968
  type: "confirm",
@@ -1921,17 +1992,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1921
1992
  adrAnswers.decision,
1922
1993
  adrAnswers.consequences
1923
1994
  );
1924
- console.log(chalk5.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
1995
+ console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
1925
1996
  }
1926
1997
  }
1927
1998
  }
1928
1999
  const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
1929
2000
  const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
1930
2001
  if (troubleCommits.length > 0) {
1931
- console.log(chalk5.yellow.bold(`
2002
+ console.log(chalk6.yellow.bold(`
1932
2003
  ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1933
2004
  troubleCommits.forEach((c) => {
1934
- console.log(chalk5.dim(` \u2022 ${c.message}`));
2005
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1935
2006
  });
1936
2007
  const { createTroubleshoot } = await inquirer3.prompt([{
1937
2008
  type: "confirm",
@@ -1982,12 +2053,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1982
2053
  `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1983
2054
  ].join("\n");
1984
2055
  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)}`));
2056
+ console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
1986
2057
  }
1987
2058
  }
1988
- console.log(chalk5.green.bold(`
2059
+ console.log(chalk6.green.bold(`
1989
2060
  ${ko.recap.done}`));
1990
- console.log(chalk5.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
2061
+ console.log(chalk6.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
1991
2062
  const claudeMdPath = path6.join(process.cwd(), "CLAUDE.md");
1992
2063
  if (fs5.existsSync(claudeMdPath)) {
1993
2064
  const { updateClaude } = await inquirer3.prompt([{
@@ -2007,7 +2078,7 @@ ${ko.recap.done}`));
2007
2078
  `- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
2008
2079
  );
2009
2080
  fs5.writeFileSync(claudeMdPath, claudeContent, "utf-8");
2010
- console.log(chalk5.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
2081
+ console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
2011
2082
  }
2012
2083
  }
2013
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"';
@@ -2019,7 +2090,7 @@ ${ko.recap.done}`));
2019
2090
  }
2020
2091
 
2021
2092
  // src/commands/sync.ts
2022
- import chalk6 from "chalk";
2093
+ import chalk7 from "chalk";
2023
2094
  import fs6 from "fs";
2024
2095
  import path7 from "path";
2025
2096
  var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
@@ -2089,32 +2160,32 @@ function toClaudeMd(sections, existing) {
2089
2160
  return lines.join("\n");
2090
2161
  }
2091
2162
  async function sync() {
2092
- console.log(chalk6.bold(`
2163
+ console.log(chalk7.bold(`
2093
2164
  ${ko.sync.title}
2094
2165
  `));
2095
2166
  const cwd = process.cwd();
2096
2167
  const rulesPath = path7.join(cwd, "RULES.md");
2097
2168
  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."));
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."));
2101
2172
  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"));
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"));
2108
2179
  return;
2109
2180
  }
2110
2181
  const rulesContent = fs6.readFileSync(rulesPath, "utf-8");
2111
2182
  const sections = parseRulesMd(rulesContent);
2112
- console.log(chalk6.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
2183
+ console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
2113
2184
  const firstLine = rulesContent.split("\n")[0];
2114
2185
  const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
2115
2186
  const cursorrulesPath = path7.join(cwd, ".cursorrules");
2116
2187
  fs6.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
2117
- console.log(chalk6.green(` ${ko.sync.cursorrulesDone}`));
2188
+ console.log(chalk7.green(` ${ko.sync.cursorrulesDone}`));
2118
2189
  const claudePath = path7.join(cwd, "CLAUDE.md");
2119
2190
  const existingClaude = fs6.existsSync(claudePath) ? fs6.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
2120
2191
 
@@ -2124,11 +2195,11 @@ ${ko.sync.title}
2124
2195
  - **\uB2E4\uC74C \uC561\uC158:** __FILL__
2125
2196
  - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
2126
2197
  fs6.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
2127
- console.log(chalk6.green(` ${ko.sync.claudeDone}`));
2128
- console.log(chalk6.bold.green(`
2198
+ console.log(chalk7.green(` ${ko.sync.claudeDone}`));
2199
+ console.log(chalk7.bold.green(`
2129
2200
  ${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."));
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."));
2132
2203
  printNextStep({
2133
2204
  message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
2134
2205
  command: "vhk \uC810\uAC80",
@@ -2137,7 +2208,7 @@ ${ko.sync.done}`));
2137
2208
  }
2138
2209
 
2139
2210
  // src/commands/check.ts
2140
- import chalk7 from "chalk";
2211
+ import chalk8 from "chalk";
2141
2212
  import path9 from "path";
2142
2213
  import fs8 from "fs";
2143
2214
 
@@ -2196,15 +2267,6 @@ function parseRules(rulesPath) {
2196
2267
  ));
2197
2268
  }
2198
2269
  }
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
2270
  }
2209
2271
  return rules;
2210
2272
  }
@@ -2305,22 +2367,22 @@ function escapeRegex(str) {
2305
2367
 
2306
2368
  // src/commands/check.ts
2307
2369
  async function check() {
2308
- console.log(chalk7.bold(`
2370
+ console.log(chalk8.bold(`
2309
2371
  ${ko.check.title}
2310
2372
  `));
2311
2373
  const cwd = process.cwd();
2312
2374
  const rulesPath = path9.join(cwd, "RULES.md");
2313
2375
  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."));
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."));
2316
2378
  return;
2317
2379
  }
2318
2380
  const rules = parseRules(rulesPath);
2319
- console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
2381
+ console.log(chalk8.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
2320
2382
  `));
2321
2383
  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."));
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."));
2324
2386
  return;
2325
2387
  }
2326
2388
  const allViolations = [];
@@ -2328,13 +2390,13 @@ ${ko.check.title}
2328
2390
  for (const rule of rules) {
2329
2391
  const violations = rule.check(cwd);
2330
2392
  if (violations.length === 0) {
2331
- console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
2393
+ console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`));
2332
2394
  passCount++;
2333
2395
  } else {
2334
- console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2396
+ console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2335
2397
  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");
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");
2338
2400
  console.log(` ${icon} ${v.message}${loc}`);
2339
2401
  });
2340
2402
  allViolations.push(...violations);
@@ -2344,17 +2406,17 @@ ${ko.check.title}
2344
2406
  const errors = allViolations.filter((v) => v.severity === "error").length;
2345
2407
  const warnings = allViolations.filter((v) => v.severity === "warning").length;
2346
2408
  if (allViolations.length === 0) {
2347
- console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
2409
+ console.log(chalk8.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
2348
2410
  printNextStep({
2349
2411
  message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
2350
2412
  command: "vhk \uBCF4\uC548 scan",
2351
2413
  cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
2352
2414
  });
2353
2415
  } 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`)}`);
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`)}`);
2358
2420
  printNextStep({
2359
2421
  message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
2360
2422
  command: "vhk \uC810\uAC80",
@@ -2367,10 +2429,13 @@ ${ko.check.title}
2367
2429
  }
2368
2430
 
2369
2431
  // src/commands/secure.ts
2370
- import chalk8 from "chalk";
2371
- import fs10 from "fs";
2432
+ import chalk9 from "chalk";
2433
+ import fs11 from "fs";
2372
2434
  import path11 from "path";
2373
2435
 
2436
+ // src/lib/scan-secrets.ts
2437
+ import fs10 from "fs";
2438
+
2374
2439
  // src/lib/secret-patterns.ts
2375
2440
  var SECRET_PATTERNS = [
2376
2441
  {
@@ -2395,7 +2460,7 @@ var SECRET_PATTERNS = [
2395
2460
  id: "notion-token",
2396
2461
  name: "Notion Integration Token",
2397
2462
  severity: "critical",
2398
- pattern: /secret_[A-Za-z0-9]{24,}/
2463
+ pattern: /secret_[A-Za-z0-9]{40,50}/
2399
2464
  },
2400
2465
  {
2401
2466
  id: "github-token",
@@ -2407,7 +2472,7 @@ var SECRET_PATTERNS = [
2407
2472
  id: "openai-key",
2408
2473
  name: "OpenAI API Key",
2409
2474
  severity: "critical",
2410
- pattern: /sk-[A-Za-z0-9]{20,}/
2475
+ pattern: /\bsk-(?:proj-|ant-api03-|live-)[A-Za-z0-9_-]{16,}\b/
2411
2476
  },
2412
2477
  {
2413
2478
  id: "generic-api-key",
@@ -2509,69 +2574,88 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
2509
2574
  walk(rootDir);
2510
2575
  }
2511
2576
 
2512
- // src/commands/secure.ts
2513
- var MAX_FINDINGS = 200;
2577
+ // src/lib/scan-secrets.ts
2578
+ var MAX_SECRET_FINDINGS = 200;
2514
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
2515
2632
  async function secure() {
2516
- console.log(chalk8.bold(`
2633
+ console.log(chalk9.bold(`
2517
2634
  ${ko.secure.title}
2518
2635
  `));
2519
2636
  const cwd = process.cwd();
2520
- const findings = [];
2521
- let scannedFiles = 0;
2522
- let truncated = false;
2523
2637
  const gitignorePath = path11.join(cwd, ".gitignore");
2524
- const hasGitignore = fs10.existsSync(gitignorePath);
2638
+ const hasGitignore = fs11.existsSync(gitignorePath);
2525
2639
  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"));
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"));
2528
2642
  } else {
2529
- const gitignoreContent = fs10.readFileSync(gitignorePath, "utf-8");
2643
+ const gitignoreContent = fs11.readFileSync(gitignorePath, "utf-8");
2530
2644
  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"));
2645
+ console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
2646
+ console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
2533
2647
  }
2534
2648
  }
2535
- console.log(chalk8.dim(` ${ko.secure.scanning}
2649
+ console.log(chalk9.dim(` ${ko.secure.scanning}
2536
2650
  `));
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)`));
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)`));
2569
2653
  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.`));
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.`));
2571
2655
  }
2572
2656
  console.log("");
2573
2657
  if (findings.length === 0) {
2574
- console.log(chalk8.green.bold(` ${ko.secure.clean}`));
2658
+ console.log(chalk9.green.bold(` ${ko.secure.clean}`));
2575
2659
  printNextStep({
2576
2660
  message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
2577
2661
  command: "vhk \uC815\uB9AC",
@@ -2583,45 +2667,45 @@ ${ko.secure.title}
2583
2667
  const high = findings.filter((f) => f.severity === "high");
2584
2668
  const medium = findings.filter((f) => f.severity === "medium");
2585
2669
  if (critical.length > 0) {
2586
- console.log(chalk8.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2670
+ console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2587
2671
  critical.forEach((f) => {
2588
- console.log(chalk8.red(` \u2716 ${f.patternName}`));
2589
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2672
+ console.log(chalk9.red(` \u2716 ${f.patternName}`));
2673
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2590
2674
  });
2591
2675
  console.log("");
2592
2676
  }
2593
2677
  if (high.length > 0) {
2594
- console.log(chalk8.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2678
+ console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2595
2679
  high.forEach((f) => {
2596
- console.log(chalk8.yellow(` \u26A0 ${f.patternName}`));
2597
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2680
+ console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
2681
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2598
2682
  });
2599
2683
  console.log("");
2600
2684
  }
2601
2685
  if (medium.length > 0) {
2602
- console.log(chalk8.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2686
+ console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2603
2687
  medium.forEach((f) => {
2604
- console.log(chalk8.blue(` \u2139 ${f.patternName}`));
2605
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2688
+ console.log(chalk9.blue(` \u2139 ${f.patternName}`));
2689
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2606
2690
  });
2607
2691
  console.log("");
2608
2692
  }
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}`);
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}`);
2611
2695
  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) {
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) {
2617
2701
  process.exitCode = 1;
2618
2702
  }
2619
2703
  }
2620
2704
 
2621
2705
  // src/commands/doctor.ts
2622
- import chalk9 from "chalk";
2706
+ import chalk10 from "chalk";
2623
2707
  import { execSync } from "child_process";
2624
- import fs11 from "fs";
2708
+ import fs12 from "fs";
2625
2709
  import path12 from "path";
2626
2710
  import { fileURLToPath } from "url";
2627
2711
  function checkCommand(name, command, hint) {
@@ -2640,8 +2724,8 @@ function getVhkVersion() {
2640
2724
  ];
2641
2725
  for (const pkgPath of candidates) {
2642
2726
  try {
2643
- if (fs11.existsSync(pkgPath)) {
2644
- const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2727
+ if (fs12.existsSync(pkgPath)) {
2728
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2645
2729
  return pkg.version;
2646
2730
  }
2647
2731
  } catch {
@@ -2651,7 +2735,7 @@ function getVhkVersion() {
2651
2735
  return void 0;
2652
2736
  }
2653
2737
  async function doctor() {
2654
- console.log(chalk9.bold(`
2738
+ console.log(chalk10.bold(`
2655
2739
  ${ko.doctor.title}
2656
2740
  `));
2657
2741
  const checks = [
@@ -2663,22 +2747,22 @@ ${ko.doctor.title}
2663
2747
  let allOk = true;
2664
2748
  for (const check2 of checks) {
2665
2749
  if (check2.ok) {
2666
- console.log(chalk9.green(` \u2705 ${check2.name}`) + chalk9.dim(` \u2014 ${check2.version}`));
2750
+ console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
2667
2751
  } else {
2668
- console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2669
- console.log(chalk9.dim(` \u2192 ${check2.hint}`));
2752
+ console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2753
+ console.log(chalk10.dim(` \u2192 ${check2.hint}`));
2670
2754
  allOk = false;
2671
2755
  }
2672
2756
  }
2673
2757
  console.log("");
2674
2758
  const vhkVersion = getVhkVersion();
2675
2759
  if (vhkVersion) {
2676
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
2760
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
2677
2761
  } else {
2678
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(" \u2014 \uC124\uCE58\uB428"));
2762
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
2679
2763
  }
2680
2764
  console.log("");
2681
- console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
2765
+ console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
2682
2766
  const cwd = process.cwd();
2683
2767
  const projectFiles = [
2684
2768
  { name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
@@ -2688,32 +2772,32 @@ ${ko.doctor.title}
2688
2772
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2689
2773
  ];
2690
2774
  for (const file of projectFiles) {
2691
- const exists = fs11.existsSync(path12.join(cwd, file.name));
2775
+ const exists = fs12.existsSync(path12.join(cwd, file.name));
2692
2776
  if (exists) {
2693
- console.log(chalk9.green(` \u2705 ${file.name}`));
2777
+ console.log(chalk10.green(` \u2705 ${file.name}`));
2694
2778
  if (file.name === ".env") {
2695
2779
  const gitignorePath = path12.join(cwd, ".gitignore");
2696
- if (fs11.existsSync(gitignorePath)) {
2697
- const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
2780
+ if (fs12.existsSync(gitignorePath)) {
2781
+ const gitignore = fs12.readFileSync(gitignorePath, "utf-8");
2698
2782
  if (!gitignore.includes(".env")) {
2699
- console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
2783
+ console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
2700
2784
  }
2701
2785
  }
2702
2786
  }
2703
2787
  } else {
2704
- console.log(chalk9.dim(` \u2B1A ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
2788
+ console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
2705
2789
  }
2706
2790
  }
2707
2791
  console.log("");
2708
2792
  if (allOk) {
2709
- console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
2793
+ console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
2710
2794
  printNextStep({
2711
2795
  message: ko.doctor.nextOkMessage,
2712
2796
  command: "vhk \uC2DC\uC791",
2713
2797
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
2714
2798
  });
2715
2799
  } else {
2716
- console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2800
+ console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2717
2801
  printNextStep({
2718
2802
  message: ko.doctor.nextRetryMessage,
2719
2803
  command: "vhk doctor",
@@ -2724,9 +2808,9 @@ ${ko.doctor.title}
2724
2808
  }
2725
2809
 
2726
2810
  // src/commands/ship.ts
2727
- import chalk10 from "chalk";
2811
+ import chalk11 from "chalk";
2728
2812
  import inquirer4 from "inquirer";
2729
- import fs12 from "fs";
2813
+ import fs13 from "fs";
2730
2814
  import path13 from "path";
2731
2815
  var CHECKLIST = [
2732
2816
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
@@ -2740,29 +2824,29 @@ function sanitizeVersion(version) {
2740
2824
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2741
2825
  }
2742
2826
  async function ship() {
2743
- console.log(chalk10.bold(`
2827
+ console.log(chalk11.bold(`
2744
2828
  ${ko.ship.title}
2745
2829
  `));
2746
2830
  const cwd = process.cwd();
2747
- console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
2831
+ console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
2748
2832
  `));
2749
2833
  const { passed } = await inquirer4.prompt([{
2750
2834
  type: "checkbox",
2751
2835
  name: "passed",
2752
2836
  message: ko.ship.checkboxPrompt,
2753
2837
  choices: CHECKLIST.map((c) => ({
2754
- name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
2838
+ name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
2755
2839
  value: c.id
2756
2840
  }))
2757
2841
  }]);
2758
2842
  const allPassed = passed.length === CHECKLIST.length;
2759
2843
  const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
2760
2844
  if (!allPassed) {
2761
- console.log(chalk10.yellow(`
2845
+ console.log(chalk11.yellow(`
2762
2846
  ${ko.ship.incompleteHeader}`));
2763
2847
  skipped.forEach((s) => {
2764
- console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2765
- console.log(chalk10.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2848
+ console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2849
+ console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2766
2850
  });
2767
2851
  const { proceed } = await inquirer4.prompt([{
2768
2852
  type: "confirm",
@@ -2779,13 +2863,13 @@ ${ko.ship.title}
2779
2863
  return;
2780
2864
  }
2781
2865
  } else {
2782
- console.log(chalk10.green(`
2866
+ console.log(chalk11.green(`
2783
2867
  ${ko.ship.allPassed}
2784
2868
  `));
2785
2869
  }
2786
- console.log(chalk10.cyan.bold(` ${ko.ship.retro}
2870
+ console.log(chalk11.cyan.bold(` ${ko.ship.retro}
2787
2871
  `));
2788
- console.log(chalk10.dim(` ${ko.ship.versionHint}`));
2872
+ console.log(chalk11.dim(` ${ko.ship.versionHint}`));
2789
2873
  const retro = await inquirer4.prompt([
2790
2874
  { type: "input", name: "version", message: ko.ship.versionPrompt },
2791
2875
  { type: "input", name: "whatWentWell", message: ko.ship.questionWell },
@@ -2794,7 +2878,7 @@ ${ko.ship.title}
2794
2878
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2795
2879
  ]);
2796
2880
  const buildLogDir = path13.join(cwd, "docs", "build-log");
2797
- if (!fs12.existsSync(buildLogDir)) fs12.mkdirSync(buildLogDir, { recursive: true });
2881
+ if (!fs13.existsSync(buildLogDir)) fs13.mkdirSync(buildLogDir, { recursive: true });
2798
2882
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2799
2883
  const versionSlug = sanitizeVersion(retro.version);
2800
2884
  const fileName = `${today}-v${versionSlug}.md`;
@@ -2827,8 +2911,8 @@ ${ko.ship.title}
2827
2911
  "---",
2828
2912
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2829
2913
  ].join("\n");
2830
- fs12.writeFileSync(filePath, content, "utf-8");
2831
- console.log(chalk10.green(`
2914
+ fs13.writeFileSync(filePath, content, "utf-8");
2915
+ console.log(chalk11.green(`
2832
2916
  ${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
2833
2917
  printNextStep({
2834
2918
  message: ko.ship.deployMessage,
@@ -2839,16 +2923,63 @@ ${ko.ship.title}
2839
2923
  }
2840
2924
 
2841
2925
  // src/commands/save.ts
2842
- import { execFileSync, execSync as execSync2 } from "child_process";
2843
- import chalk11 from "chalk";
2926
+ import { execFileSync as execFileSync2 } from "child_process";
2927
+ import chalk12 from "chalk";
2844
2928
  import ora from "ora";
2845
2929
  import inquirer5 from "inquirer";
2846
- function gitOut(args) {
2847
- return execFileSync("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2930
+
2931
+ // src/lib/git-porcelain.ts
2932
+ function normalizePorcelain(raw) {
2933
+ return raw.replace(/\r\n/g, "\n").trimEnd();
2848
2934
  }
2849
- function gitRun(args) {
2850
- execFileSync("git", args, { stdio: "pipe" });
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);
2851
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
2852
2983
  function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
2853
2984
  const y = date.getFullYear();
2854
2985
  const m = String(date.getMonth() + 1).padStart(2, "0");
@@ -2864,24 +2995,49 @@ function statusIcon(code) {
2864
2995
  return "\u{1F4C4}";
2865
2996
  }
2866
2997
  async function save() {
2867
- console.log(chalk11.bold(`
2998
+ console.log(chalk12.bold(`
2868
2999
  \u{1F4BE} ${t("save.title")}`));
2869
- console.log(chalk11.gray("\u2500".repeat(40)));
3000
+ console.log(chalk12.gray("\u2500".repeat(40)));
3001
+ let gitRoot;
2870
3002
  try {
2871
- execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3003
+ execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3004
+ gitRoot = getGitRoot();
2872
3005
  } catch {
2873
- console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
3006
+ console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
2874
3007
  return;
2875
3008
  }
2876
- const status2 = gitOut(["status", "--porcelain"]).trim();
2877
- if (!status2) {
2878
- console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
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")}`));
2879
3036
  return;
2880
3037
  }
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) => {
3038
+ console.log(chalk12.cyan(`
3039
+ \u{1F4CB} ${t("save.filesHeader", lines.length)}`));
3040
+ lines.forEach((line) => {
2885
3041
  const code = line.substring(0, 2);
2886
3042
  const name = line.substring(3);
2887
3043
  console.log(` ${statusIcon(code)} ${name}`);
@@ -2893,43 +3049,62 @@ async function save() {
2893
3049
  default: formatDefaultCommitMessage()
2894
3050
  }]);
2895
3051
  const spinner = ora(t("save.saving")).start();
3052
+ let didAdd = false;
2896
3053
  try {
2897
- gitRun(["add", "."]);
2898
- gitRun(["commit", "-m", message]);
3054
+ gitRun(["add", "."], gitRoot);
3055
+ didAdd = true;
3056
+ gitRun(["commit", "-m", message], gitRoot);
2899
3057
  spinner.text = t("save.pushing");
2900
- try {
2901
- gitRun(["push"]);
2902
- spinner.succeed(t("save.successWithPush"));
2903
- } catch {
3058
+ if (!hasGitRemote(gitRoot)) {
2904
3059
  spinner.succeed(t("save.successLocal"));
2905
- console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
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)}`));
2906
3079
  }
2907
- console.log(chalk11.green(`
2908
- \u2705 ${t("save.done", files.length)}`));
2909
3080
  } catch (err) {
2910
3081
  spinner.fail(t("save.failed"));
2911
- const msg = err instanceof Error ? err.message : String(err);
2912
- console.log(chalk11.red(msg));
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
+ }
2913
3093
  process.exitCode = 1;
2914
3094
  }
2915
3095
  }
2916
3096
 
2917
3097
  // src/commands/undo.ts
2918
- import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
2919
- import chalk12 from "chalk";
3098
+ import { execFileSync as execFileSync3 } from "child_process";
3099
+ import chalk13 from "chalk";
2920
3100
  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
3101
  function parseRecentCommits(logOutput) {
2928
3102
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
2929
3103
  }
2930
- function countUnpushedCommits() {
3104
+ function countUnpushedCommits(gitRoot) {
3105
+ const cwd = gitRoot ?? process.cwd();
2931
3106
  try {
2932
- const out = gitOut2(["rev-list", "--count", "@{u}..HEAD"]).trim();
3107
+ const out = gitOut(["rev-list", "--count", "@{u}..HEAD"], cwd).trim();
2933
3108
  return parseInt(out, 10) || 0;
2934
3109
  } catch {
2935
3110
  return -1;
@@ -2939,29 +3114,36 @@ function willUndoPushedCommits(undoCount, unpushedCount) {
2939
3114
  if (unpushedCount < 0) return false;
2940
3115
  return undoCount > unpushedCount;
2941
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
+ }
2942
3122
  async function undo() {
2943
- console.log(chalk12.bold(`
3123
+ console.log(chalk13.bold(`
2944
3124
  \u23EA ${t("undo.title")}`));
2945
- console.log(chalk12.gray("\u2500".repeat(40)));
3125
+ console.log(chalk13.gray("\u2500".repeat(40)));
3126
+ let gitRoot;
2946
3127
  try {
2947
- execSync3("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3128
+ execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3129
+ gitRoot = getGitRoot();
2948
3130
  } catch {
2949
- console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
3131
+ console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
2950
3132
  return;
2951
3133
  }
2952
3134
  let logOutput;
2953
3135
  try {
2954
- logOutput = gitOut2(["log", "--oneline", "-5"]).trim();
3136
+ logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
2955
3137
  } catch {
2956
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3138
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2957
3139
  return;
2958
3140
  }
2959
3141
  const commits = parseRecentCommits(logOutput);
2960
3142
  if (commits.length === 0) {
2961
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3143
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2962
3144
  return;
2963
3145
  }
2964
- console.log(chalk12.cyan(`
3146
+ console.log(chalk13.cyan(`
2965
3147
  ${t("undo.recentHeader")}`));
2966
3148
  commits.forEach((c, i) => {
2967
3149
  console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
@@ -2976,40 +3158,57 @@ ${t("undo.recentHeader")}`));
2976
3158
  max: maxUndo
2977
3159
  }]);
2978
3160
  const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
2979
- const unpushed = countUnpushedCommits();
2980
- if (willUndoPushedCommits(undoCount, unpushed)) {
2981
- console.log(chalk12.red(`
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(`
2982
3176
  \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
3177
+ }
2983
3178
  }
2984
3179
  const { confirm } = await inquirer6.prompt([{
2985
3180
  type: "confirm",
2986
3181
  name: "confirm",
2987
- message: t("undo.confirmMessage", undoCount),
3182
+ message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
2988
3183
  default: false
2989
3184
  }]);
2990
3185
  if (!confirm) {
2991
- console.log(chalk12.gray(t("undo.cancelled")));
3186
+ console.log(chalk13.gray(t("undo.cancelled")));
2992
3187
  return;
2993
3188
  }
2994
3189
  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")}`));
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
+ }
2999
3198
  } catch (err) {
3000
- console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3199
+ console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
3001
3200
  const msg = err instanceof Error ? err.message : String(err);
3002
- console.log(chalk12.red(msg));
3201
+ console.log(chalk13.red(msg));
3003
3202
  process.exitCode = 1;
3004
3203
  }
3005
3204
  }
3006
3205
 
3007
3206
  // src/commands/diff.ts
3008
- import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
3009
- import chalk13 from "chalk";
3010
- function gitOut3(args) {
3207
+ import { execFileSync as execFileSync4, execSync as execSync2 } from "child_process";
3208
+ import chalk14 from "chalk";
3209
+ function gitOut2(args) {
3011
3210
  try {
3012
- return execFileSync3("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3211
+ return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3013
3212
  } catch {
3014
3213
  return "";
3015
3214
  }
@@ -3046,65 +3245,62 @@ function summarizeNumstat(numstat) {
3046
3245
  return { fileCount, totalAdd, totalDel };
3047
3246
  }
3048
3247
  function printFile(f) {
3049
- const adds = f.additions > 0 ? chalk13.green(`+${f.additions}`) : "";
3050
- const dels = f.deletions > 0 ? chalk13.red(`-${f.deletions}`) : "";
3248
+ const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
3249
+ const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
3051
3250
  const change = [adds, dels].filter(Boolean).join(" ");
3052
3251
  console.log(` ${f.name} ${change}`);
3053
3252
  }
3054
3253
  async function diff() {
3055
- console.log(chalk13.bold(`
3254
+ console.log(chalk14.bold(`
3056
3255
  \u{1F50D} ${t("diff.title")}`));
3057
- console.log(chalk13.gray("\u2500".repeat(40)));
3256
+ console.log(chalk14.gray("\u2500".repeat(40)));
3058
3257
  try {
3059
- execSync4("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3258
+ execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3060
3259
  } catch {
3061
- console.log(chalk13.red(`\u274C ${t("diff.notGitRepo")}`));
3260
+ console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
3062
3261
  return;
3063
3262
  }
3064
- const unstaged = gitOut3(["diff", "--stat"]);
3065
- const staged = gitOut3(["diff", "--cached", "--stat"]);
3066
- const untracked = gitOut3(["ls-files", "--others", "--exclude-standard"]);
3263
+ const unstaged = gitOut2(["diff", "--stat"]);
3264
+ const staged = gitOut2(["diff", "--cached", "--stat"]);
3265
+ const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3067
3266
  if (!unstaged && !staged && !untracked) {
3068
- console.log(chalk13.green(`
3267
+ console.log(chalk14.green(`
3069
3268
  \u2705 ${t("diff.noChanges")}`));
3070
3269
  return;
3071
3270
  }
3072
3271
  if (staged) {
3073
- console.log(chalk13.cyan(`
3272
+ console.log(chalk14.cyan(`
3074
3273
  ${t("diff.stagedHeader")}`));
3075
3274
  parseDiffStat(staged).forEach((f) => printFile(f));
3076
3275
  }
3077
3276
  if (unstaged) {
3078
- console.log(chalk13.cyan(`
3277
+ console.log(chalk14.cyan(`
3079
3278
  ${t("diff.unstagedHeader")}`));
3080
3279
  parseDiffStat(unstaged).forEach((f) => printFile(f));
3081
3280
  }
3082
3281
  if (untracked) {
3083
3282
  const files = untracked.split("\n").filter(Boolean);
3084
- console.log(chalk13.cyan(`
3283
+ console.log(chalk14.cyan(`
3085
3284
  ${t("diff.untrackedHeader", files.length)}`));
3086
- files.forEach((f) => console.log(` ${chalk13.green("+")} ${f}`));
3285
+ files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
3087
3286
  }
3088
- const numstat = gitOut3(["diff", "--numstat", "HEAD"]);
3287
+ const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3089
3288
  if (numstat) {
3090
3289
  const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3091
- console.log(chalk13.cyan(`
3290
+ console.log(chalk14.cyan(`
3092
3291
  ${t("diff.summaryHeader")}`));
3093
3292
  console.log(` ${t("diff.filesLine", fileCount)}`);
3094
- console.log(` \uCD94\uAC00: ${chalk13.green(`+${totalAdd}`)}\uC904`);
3095
- console.log(` \uC0AD\uC81C: ${chalk13.red(`-${totalDel}`)}\uC904`);
3293
+ console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
3294
+ console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
3096
3295
  }
3097
3296
  console.log("");
3098
3297
  }
3099
3298
 
3100
3299
  // src/commands/status.ts
3101
- import { execFileSync as execFileSync4, execSync as execSync5 } from "child_process";
3102
- import fs13 from "fs";
3300
+ import { execFileSync as execFileSync5 } from "child_process";
3301
+ import fs14 from "fs";
3103
3302
  import path14 from "path";
3104
- import chalk14 from "chalk";
3105
- function gitOut4(args) {
3106
- return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3107
- }
3303
+ import chalk15 from "chalk";
3108
3304
  function countFileChanges(porcelain) {
3109
3305
  const lines = porcelain.split("\n").filter(Boolean);
3110
3306
  let staged = 0;
@@ -3143,9 +3339,9 @@ function parseRecentCommitLines(logOutput) {
3143
3339
  }
3144
3340
  function readProjectPackage(cwd = process.cwd()) {
3145
3341
  const pkgPath = path14.join(cwd, "package.json");
3146
- if (!fs13.existsSync(pkgPath)) return null;
3342
+ if (!fs14.existsSync(pkgPath)) return null;
3147
3343
  try {
3148
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3344
+ const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
3149
3345
  if (!pkg.name && !pkg.version) return null;
3150
3346
  return {
3151
3347
  name: pkg.name ?? "(no name)",
@@ -3155,63 +3351,65 @@ function readProjectPackage(cwd = process.cwd()) {
3155
3351
  return null;
3156
3352
  }
3157
3353
  }
3158
- function getSyncCounts() {
3354
+ function getSyncCounts(gitRoot) {
3159
3355
  try {
3160
- const out = gitOut4(["rev-list", "--left-right", "--count", "HEAD...@{u}"]);
3356
+ const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
3161
3357
  return parseSyncCounts(out);
3162
3358
  } catch {
3163
3359
  return { ahead: 0, behind: 0, hasUpstream: false };
3164
3360
  }
3165
3361
  }
3166
3362
  async function status() {
3167
- console.log(chalk14.bold(`
3363
+ console.log(chalk15.bold(`
3168
3364
  \u{1F4CA} ${t("status.title")}`));
3169
- console.log(chalk14.gray("\u2500".repeat(40)));
3365
+ console.log(chalk15.gray("\u2500".repeat(40)));
3366
+ let gitRoot;
3170
3367
  try {
3171
- execSync5("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3368
+ execFileSync5("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3369
+ gitRoot = getGitRoot();
3172
3370
  } catch {
3173
- console.log(chalk14.red(`\u274C ${t("status.notGitRepo")}`));
3371
+ console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
3174
3372
  return;
3175
3373
  }
3176
3374
  let branch;
3177
3375
  try {
3178
- branch = gitOut4(["branch", "--show-current"]).trim() || t("status.detached");
3376
+ branch = gitOut(["branch", "--show-current"], gitRoot).trim() || t("status.detached");
3179
3377
  } catch {
3180
3378
  branch = t("status.unknownBranch");
3181
3379
  }
3182
- const porcelain = gitOut4(["status", "--porcelain"]).trim();
3380
+ const porcelain = normalizePorcelain(gitOut(["status", "--porcelain"], gitRoot));
3183
3381
  const counts = countFileChanges(porcelain);
3184
- const sync2 = getSyncCounts();
3382
+ const sync2 = getSyncCounts(gitRoot);
3185
3383
  let commits = [];
3186
3384
  try {
3187
- commits = parseRecentCommitLines(gitOut4(["log", "--oneline", "-3"]).trim());
3385
+ commits = parseRecentCommitLines(gitOut(["log", "--oneline", "-3"], gitRoot).trim());
3188
3386
  } catch {
3189
3387
  commits = [];
3190
3388
  }
3191
3389
  const pkg = readProjectPackage();
3192
- console.log(chalk14.cyan(`
3193
- \u{1F33F} ${t("status.branch")}`) + chalk14.white(` ${branch}`));
3390
+ console.log(chalk15.cyan(`
3391
+ \u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
3194
3392
  console.log(
3195
- chalk14.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk14.white(
3393
+ chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
3196
3394
  ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3197
3395
  )
3198
3396
  );
3199
- console.log(chalk14.cyan(`
3397
+ console.log(chalk15.cyan(`
3200
3398
  \u{1F4CB} ${t("status.recentCommits")}`));
3201
3399
  if (commits.length === 0) {
3202
- console.log(chalk14.dim(` ${t("status.noCommits")}`));
3400
+ console.log(chalk15.dim(` ${t("status.noCommits")}`));
3203
3401
  } else {
3204
- commits.forEach((c) => console.log(` ${chalk14.dim("\u2022")} ${c}`));
3402
+ commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
3205
3403
  }
3206
3404
  console.log(
3207
- chalk14.cyan(`
3208
- \u{1F504} ${t("status.remote")}`) + chalk14.white(` ${formatSyncLabel(sync2)}`)
3405
+ chalk15.cyan(`
3406
+ \u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
3209
3407
  );
3210
- console.log(chalk14.gray("\n" + "\u2500".repeat(40)));
3408
+ console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
3211
3409
  if (pkg) {
3212
- console.log(chalk14.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk14.white(` ${pkg.name} v${pkg.version}`));
3410
+ console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
3213
3411
  } else {
3214
- console.log(chalk14.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3412
+ console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3215
3413
  }
3216
3414
  console.log("");
3217
3415
  }
@@ -3233,7 +3431,7 @@ var KO_ALIASES = {
3233
3431
  status: "\uC0C1\uD0DC",
3234
3432
  diff: "\uBCC0\uACBD"
3235
3433
  };
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");
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");
3237
3435
  program.configureHelp({
3238
3436
  formatHelp(cmd, helper) {
3239
3437
  if (cmd.parent) {
@@ -3259,7 +3457,7 @@ program.command("init").alias("\uC2DC\uC791").alias("\uB9CC\uB4E4\uAE30").descri
3259
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);
3260
3458
  program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
3261
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);
3262
- 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);
3263
3461
  secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
3264
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);
3265
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);
@@ -3278,8 +3476,8 @@ program.on("command:*", async (operands) => {
3278
3476
  const route = routeNaturalLanguage(input);
3279
3477
  if (route) {
3280
3478
  console.log("");
3281
- console.log(chalk15.cyan(` \u{1F4AC} "${input}"`));
3282
- console.log(chalk15.cyan(` \u2192 ${route.explanation}`));
3479
+ console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
3480
+ console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
3283
3481
  if (route.confidence === "low") {
3284
3482
  const { confirm } = await inquirer7.prompt([{
3285
3483
  type: "confirm",
@@ -3288,7 +3486,7 @@ program.on("command:*", async (operands) => {
3288
3486
  default: true
3289
3487
  }]);
3290
3488
  if (!confirm) {
3291
- console.log(chalk15.dim(` ${ko.nlp.menuHint}`));
3489
+ console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
3292
3490
  return;
3293
3491
  }
3294
3492
  }
@@ -3323,7 +3521,7 @@ program.on("command:*", async (operands) => {
3323
3521
  return diff();
3324
3522
  }
3325
3523
  }
3326
- console.log(chalk15.yellow(`
3524
+ console.log(chalk16.yellow(`
3327
3525
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
3328
3526
  `));
3329
3527
  });
@@ -3375,4 +3573,4 @@ program.action(async () => {
3375
3573
  return diff();
3376
3574
  }
3377
3575
  });
3378
- program.parse();
3576
+ await program.parseAsync(process.argv);