@byh3071/vhk 1.6.0 → 1.6.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.
package/dist/index.js CHANGED
@@ -21,11 +21,13 @@ import {
21
21
  scanProjectForSecrets,
22
22
  startMcpServer,
23
23
  t
24
- } from "./chunk-O3A6SO7G.js";
24
+ } from "./chunk-ACJN723Q.js";
25
25
 
26
26
  // src/index.ts
27
27
  import { Command, Help } from "commander";
28
- import inquirer12 from "inquirer";
28
+ import { pathToFileURL } from "url";
29
+ import chalk35 from "chalk";
30
+ import inquirer14 from "inquirer";
29
31
 
30
32
  // src/lib/nlp-router.ts
31
33
  function normalize(input) {
@@ -43,6 +45,14 @@ function matchesKeywords(text, command) {
43
45
  return keywords.some((kw) => text.includes(kw.toLowerCase()));
44
46
  }
45
47
  var RULES = [
48
+ // restore 는 cloud-pull/undo 보다 먼저 평가 — "백업 복원/되돌려" 가 클라우드 복원이나
49
+ // 커밋 되돌리기로 새지 않도록. "백업" 한정이라 bare "복원해"(=cloud-pull)·"되돌려"(=undo)는 안 가로챔.
50
+ {
51
+ command: "restore",
52
+ explanation: "sync \uBC31\uC5C5 \uBCF5\uC6D0 (vhk restore)",
53
+ confidence: "high",
54
+ test: (t2) => /백업/.test(t2) && /(복원|복구|되돌려|되살려|롤백|restore)/.test(t2)
55
+ },
46
56
  // 영문 `vhk cloud push|pull [id]` 은 commander 가 직접 처리(가로채기 금지) — 한국어 표현만 매칭.
47
57
  {
48
58
  command: "cloud-pull",
@@ -69,6 +79,28 @@ var RULES = [
69
79
  confidence: "high",
70
80
  test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|새\s*프로젝트|^시작$|마법사|기획.*(끝|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2) && !/디자인|design|팔레트|palette|테마|theme|레퍼런스|reference|다크\s*모드|라이트\s*모드|색상\s*모드|브리핑|brief|컨텍스트|context|맥락|기억|memory|^초기화$|하네스.*만/.test(t2)
71
81
  },
82
+ // 도움말 — 초보자가 "뭐부터/도움말/명령어" 라고 물으면 읽기전용 quick actions 를 출력.
83
+ // (적대 리뷰 HIGH 수정: 이전엔 start 마법사로 라우팅돼 도움말이 scaffold 를 유발했음.
84
+ // 도움말은 절대 상태를 바꾸지 않는다.) 실제 서브커맨드는 cli-args R1 가드가 먼저 commander 로 보냄.
85
+ {
86
+ command: "help",
87
+ explanation: "\uC790\uC5F0\uC5B4\uB85C vhk \uC4F0\uB294 \uBC95 \u2014 quick actions \uCD9C\uB825(\uC0C1\uD0DC\uBCC0\uACBD \uC5C6\uC74C)",
88
+ confidence: "high",
89
+ test: (t2) => /도움말|사용법|help|^명령어$|뭐\s*(할\s*수\s*있|하면\s*(돼|되|좋)|해야)|처음\s*(뭐|어떻게|시작|할)|어떻게\s*시작|뭐부터/.test(t2)
90
+ },
91
+ // Safety Mode — 위험 작업 가드 강도 조회/변경.
92
+ {
93
+ command: "mode",
94
+ explanation: "Safety Mode \uC870\uD68C/\uBCC0\uACBD (vhk mode)",
95
+ confidence: "high",
96
+ test: (t2) => /안전\s*모드|safety\s*mode|모드\s*(바꿔|변경|설정|확인|보여|뭐)|위험\s*작업\s*(가드|모드)/.test(t2)
97
+ },
98
+ {
99
+ command: "verify",
100
+ explanation: "\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C (vhk verify)",
101
+ confidence: "high",
102
+ test: (t2) => /검증\s*묶음|사전\s*검증|저장\s*전\s*(검증|확인)|^verify$/.test(t2)
103
+ },
72
104
  {
73
105
  command: "init",
74
106
  explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
@@ -294,6 +326,28 @@ function extractNotionUrl(input) {
294
326
  return m?.[0];
295
327
  }
296
328
 
329
+ // src/lib/command-registry.ts
330
+ var CONTAINER_SUBCOMMANDS = {
331
+ goal: ["list", "next", "check", "init", "done"],
332
+ ref: ["add", "list", "open"],
333
+ memory: ["add", "list", "remove"],
334
+ cloud: ["push", "pull"],
335
+ secure: ["scan"],
336
+ design: ["palette"],
337
+ env: ["check"],
338
+ mode: ["lite", "standard", "strict"]
339
+ };
340
+ var CONTAINER_ALIASES = {
341
+ \uBAA9\uD45C: "goal",
342
+ \uB808\uD37C\uB7F0\uC2A4: "ref",
343
+ \uAE30\uC5B5: "memory",
344
+ \uD074\uB77C\uC6B0\uB4DC: "cloud",
345
+ \uBCF4\uC548: "secure",
346
+ \uB514\uC790\uC778: "design",
347
+ \uD658\uACBD\uBCC0\uC218: "env",
348
+ \uBAA8\uB4DC: "mode"
349
+ };
350
+
297
351
  // src/lib/cli-args.ts
298
352
  var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
299
353
  "gate",
@@ -326,6 +380,8 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
326
380
  "\uC800\uC7A5",
327
381
  "undo",
328
382
  "\uB418\uB3CC\uB9AC\uAE30",
383
+ "restore",
384
+ "\uBCF5\uC6D0",
329
385
  "status",
330
386
  "\uC0C1\uD0DC",
331
387
  "\uD604\uD669",
@@ -378,11 +434,28 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
378
434
  "\uAD50\uD6C8",
379
435
  "resume",
380
436
  "\uC7AC\uAC1C",
437
+ "mode",
438
+ "\uBAA8\uB4DC",
439
+ "verify",
440
+ "\uC0AC\uC804\uC810\uAC80",
381
441
  "help"
382
442
  ]);
383
443
  function isOptionToken(token) {
384
444
  return token.startsWith("-");
385
445
  }
446
+ var COMMAND_SUBCOMMANDS = (() => {
447
+ const map = { ...CONTAINER_SUBCOMMANDS };
448
+ for (const [alias, canonical] of Object.entries(CONTAINER_ALIASES)) {
449
+ const subs = CONTAINER_SUBCOMMANDS[canonical];
450
+ if (subs) map[alias] = subs;
451
+ }
452
+ return map;
453
+ })();
454
+ function isRealSubcommandPath(first, second) {
455
+ if (second === void 0) return false;
456
+ const subs = COMMAND_SUBCOMMANDS[first];
457
+ return subs !== void 0 && subs.includes(second);
458
+ }
386
459
  function detectNaturalLanguageInput(argv) {
387
460
  const rest = argv.slice(2);
388
461
  if (rest.length === 0) return null;
@@ -394,6 +467,7 @@ function detectNaturalLanguageInput(argv) {
394
467
  const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
395
468
  if (firstIsKnown && rest.length === 1) return null;
396
469
  if (firstIsKnown && rest.length > 1) {
470
+ if (isRealSubcommandPath(first, rest[1])) return null;
397
471
  if (routeNaturalLanguage(input)) return input;
398
472
  return null;
399
473
  }
@@ -401,8 +475,8 @@ function detectNaturalLanguageInput(argv) {
401
475
  }
402
476
 
403
477
  // src/lib/nlp-run.ts
404
- import chalk27 from "chalk";
405
- import inquirer11 from "inquirer";
478
+ import chalk33 from "chalk";
479
+ import inquirer13 from "inquirer";
406
480
 
407
481
  // src/commands/gate.ts
408
482
  import inquirer from "inquirer";
@@ -431,7 +505,7 @@ async function gate() {
431
505
  console.log(chalk.bold(`
432
506
  ${ko.gate.title}
433
507
  `));
434
- const { mode } = await inquirer.prompt([{
508
+ const { mode: mode2 } = await inquirer.prompt([{
435
509
  type: "list",
436
510
  name: "mode",
437
511
  message: ko.gate.modePrompt,
@@ -441,7 +515,7 @@ ${ko.gate.title}
441
515
  { name: ko.gate.modeSkipLabel, value: "skip" }
442
516
  ]
443
517
  }]);
444
- if (mode === "skip") {
518
+ if (mode2 === "skip") {
445
519
  const { source } = await inquirer.prompt([{
446
520
  type: "input",
447
521
  name: "source",
@@ -452,9 +526,9 @@ ${ko.gate.skipGo}`));
452
526
  console.log(chalk.dim(ko.gate.skipSourceLabel(source)));
453
527
  return;
454
528
  }
455
- const questions = mode === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
529
+ const questions = mode2 === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
456
530
  const total = questions.length;
457
- const header = mode === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
531
+ const header = mode2 === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
458
532
  console.log(chalk.dim(`
459
533
  ${header} ${ko.gate.modeCountSuffix(total)}
460
534
  `));
@@ -540,9 +614,14 @@ import chalk3 from "chalk";
540
614
  import fs2 from "fs";
541
615
  import path2 from "path";
542
616
 
617
+ // src/lib/date.ts
618
+ function localDate(d = /* @__PURE__ */ new Date()) {
619
+ return d.toLocaleDateString("sv-SE");
620
+ }
621
+
543
622
  // src/templates/claude-md.ts
544
623
  function CLAUDE_MD_TEMPLATE(name, _stack) {
545
- const d = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
624
+ const d = localDate();
546
625
  const slug = name.toLowerCase().replace(/\s+/g, "-");
547
626
  return [
548
627
  "---",
@@ -554,12 +633,12 @@ function CLAUDE_MD_TEMPLATE(name, _stack) {
554
633
  "# \uAE30\uB85D \uADDC\uCE59 (" + name + ")",
555
634
  "",
556
635
  "> \uC774 \uD30C\uC77C\uC740 \uAE30\uB85D/\uC6B4\uC601 \uC804\uC6A9. \uCF54\uB529/\uB514\uC790\uC778 \u2192 .cursorrules \uCC38\uC870.",
557
- "> See also: AGENTS.md",
636
+ "> See also: AGENTS.md (`vhk sync` \uB85C \uC0DD\uC131 \u2014 Codex/OpenAI \uACC4\uC5F4 \uD638\uD658).",
558
637
  "",
559
638
  "## \uD604\uC7AC \uC0C1\uD0DC",
560
639
  "- **Phase:** Phase 1 \u2014 MVP",
561
640
  "- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C",
562
- "- **\uB2E4\uC74C \uC561\uC158:** __FILL__",
641
+ "- **\uB2E4\uC74C \uC561\uC158:** **FILL**",
563
642
  "- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** " + d,
564
643
  "",
565
644
  "## ADR",
@@ -613,8 +692,39 @@ function CURSORRULES_TEMPLATE(name, desc, stack) {
613
692
  ].join("\n");
614
693
  }
615
694
 
695
+ // src/templates/rules-md.ts
696
+ function RULES_MD_TEMPLATE(name, description, stack) {
697
+ const stackList = stack.split(" + ").map((s) => "- " + s).join("\n");
698
+ return [
699
+ "# " + name + " \u2014 Rules",
700
+ "",
701
+ "> \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 \uB2E8\uC77C \uC18C\uC2A4(SoT). \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 \uC774 \uD30C\uC77C\uC5D0\uC11C\uB9CC.",
702
+ "> `vhk sync` \uAC00 Cursor\xB7Claude\xB7Windsurf\xB7Copilot\xB7Antigravity \uADDC\uCE59\uC73C\uB85C \uC804\uD30C\uD569\uB2C8\uB2E4.",
703
+ "",
704
+ "## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131",
705
+ "- \uD55C \uC904 \uC124\uBA85: " + description,
706
+ "- \uC2A4\uD0DD: " + stack,
707
+ "",
708
+ "## \uAE30\uC220 \uC2A4\uD0DD",
709
+ stackList,
710
+ "",
711
+ "## \uCF54\uB529 \uADDC\uCE59",
712
+ "- TypeScript strict (any \uAE08\uC9C0)",
713
+ "- try-catch \uD544\uC218, \uBE48 catch \uAE08\uC9C0",
714
+ "- console.log \uD504\uB85C\uB355\uC158 \uC81C\uAC70",
715
+ "- \uD30C\uC77C\uBA85\uC740 kebab-case",
716
+ "",
717
+ "## \uCEE4\uBC0B \uCEE8\uBCA4\uC158",
718
+ "- feat: / fix: / refactor: / docs: / chore:",
719
+ "",
720
+ "## \uAE30\uB85D \uADDC\uCE59",
721
+ "- \uC138\uC158 \uC885\uB8CC \uC2DC docs/log/YYYY-MM-DD-{\uC791\uC5C5\uBA85}.md \uC0DD\uC131",
722
+ "- \uAE30\uC220 \uC120\uD0DD \uC2DC docs/adr/ADR-{\uBC88\uD638}-{\uC81C\uBAA9}.md \uC0DD\uC131"
723
+ ].join("\n");
724
+ }
725
+
616
726
  // src/templates/prd.ts
617
- var FILL = "__FILL__";
727
+ var FILL = "**FILL**";
618
728
  function fill(value, fallback = FILL) {
619
729
  return value?.trim() || fallback;
620
730
  }
@@ -683,12 +793,12 @@ function ARCHITECTURE_TEMPLATE(name, stack) {
683
793
  "## \uB370\uC774\uD130 \uBAA8\uB378",
684
794
  "| \uD14C\uC774\uBE14 | \uD575\uC2EC \uCEEC\uB7FC | \uC124\uBA85 |",
685
795
  "|--------|----------|------|",
686
- "| __FILL__ | | |",
796
+ "| **FILL** | | |",
687
797
  "",
688
798
  "## \uC678\uBD80 \uC11C\uBE44\uC2A4",
689
799
  "| \uC11C\uBE44\uC2A4 | \uC6A9\uB3C4 |",
690
800
  "|--------|------|",
691
- "| __FILL__ | |"
801
+ "| **FILL** | |"
692
802
  ].join("\n");
693
803
  }
694
804
 
@@ -705,21 +815,22 @@ function ADR_TEMPLATE() {
705
815
  "# ADR-000: \uC81C\uBAA9",
706
816
  "",
707
817
  "## \uB9E5\uB77D (Context)",
708
- "__FILL__",
818
+ "**FILL**",
709
819
  "",
710
820
  "## \uACB0\uC815 (Decision)",
711
- "__FILL__",
821
+ "**FILL**",
712
822
  "",
713
823
  "## \uB300\uC548 (Alternatives)",
714
- "__FILL__",
824
+ "**FILL**",
715
825
  "",
716
826
  "## \uACB0\uACFC (Consequences)",
717
- "__FILL__"
827
+ "**FILL**"
718
828
  ].join("\n");
719
829
  }
720
830
 
721
831
  // src/templates/commands-md.ts
722
- function COMMANDS_MD_TEMPLATE() {
832
+ function COMMANDS_MD_TEMPLATE(opts = {}) {
833
+ const buildTestRow = opts.hasTest ? '| \uBE4C\uB4DC+\uD14C\uC2A4\uD2B8 | `pnpm build; pnpm test --run` | "\uBE4C\uB4DC\uD558\uACE0 \uD14C\uC2A4\uD2B8 \uB3CC\uB824" |' : '| \uBE4C\uB4DC | `pnpm build` | "\uBE4C\uB4DC\uD574" |';
723
834
  return [
724
835
  "# \u{1F4CB} \uD55C\uAD6D\uC5B4 \uBA85\uB839\uC5B4 \uAC00\uC774\uB4DC",
725
836
  "",
@@ -742,7 +853,7 @@ function COMMANDS_MD_TEMPLATE() {
742
853
  '| \uC624\uB298 \uC815\uB9AC | `vhk \uC815\uB9AC` | "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574" |',
743
854
  '| \uADDC\uCE59 \uC810\uAC80 | `vhk \uC810\uAC80` | "\uADDC\uCE59 \uC810\uAC80\uD574" |',
744
855
  '| \uBCF4\uC548 \uC2A4\uCE94 | `vhk \uBCF4\uC548 scan` | "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824" |',
745
- '| \uBE4C\uB4DC+\uD14C\uC2A4\uD2B8 | `pnpm build; pnpm test --run` | "\uBE4C\uB4DC\uD558\uACE0 \uD14C\uC2A4\uD2B8 \uB3CC\uB824" |',
856
+ buildTestRow,
746
857
  '| \uBC30\uD3EC | `vhk \uBC30\uD3EC` | "\uBC30\uD3EC\uD574" |',
747
858
  "",
748
859
  "## \uD658\uACBD \uC810\uAC80",
@@ -762,7 +873,8 @@ function VHK_README_TEMPLATE() {
762
873
  "# `.vhk/` \u2014 VHK runtime state",
763
874
  "",
764
875
  "\uC774 \uB514\uB809\uD1A0\uB9AC\uB294 VHK\uAC00 \uD504\uB85C\uC81D\uD2B8\uBCC4 \uC0C1\uD0DC\uB97C \uC800\uC7A5\uD558\uB294 \uACF3\uC785\uB2C8\uB2E4.",
765
- "\uC804\uCCB4 \uADDC\uACA9\uC740 `docs/spec.md` (spec_version 1.0) \uCC38\uC870.",
876
+ // VHK-006: 생성 프로젝트엔 docs/spec.md 없음 → vhk 저장소의 규격 문서를 외부 링크로 참조.
877
+ "\uC804\uCCB4 \uADDC\uACA9\uC740 [vhk \uADDC\uACA9 \uBB38\uC11C](https://github.com/byh3071-cpu/vhk/blob/main/docs/spec.md) (spec_version 1.0) \uCC38\uC870.",
766
878
  "",
767
879
  "## \uD2B8\uB798\uD0B9 \uC815\uCC45",
768
880
  "",
@@ -782,10 +894,12 @@ function VHK_README_TEMPLATE() {
782
894
  }
783
895
  function VHK_GITIGNORE_TEMPLATE() {
784
896
  return [
785
- "# VHK \uB85C\uCEEC \uC804\uC6A9 \u2014 \uAC1C\uC778 \uBA54\uBAA8/\uCC38\uACE0\uB9C1\uD06C/\uC548\uC804\uC2E0\uD638 (docs/spec.md \uD2B8\uB798\uD0B9 \uC815\uCC45)",
897
+ "# VHK \uB85C\uCEEC \uC804\uC6A9 \u2014 \uAC1C\uC778 \uBA54\uBAA8/\uCC38\uACE0\uB9C1\uD06C/\uC548\uC804\uC2E0\uD638 (.vhk/README.md \uD2B8\uB798\uD0B9 \uC815\uCC45)",
786
898
  "memory.json",
787
899
  "refs.json",
788
900
  "HARD_STOP",
901
+ "# sync \uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uBC31\uC5C5 (\uB85C\uCEEC \uBCF5\uAD6C\uC6A9 \u2014 vhk restore). \uCD94\uC801/\uD074\uB77C\uC6B0\uB4DC \uC81C\uC678.",
902
+ "backups/",
789
903
  ""
790
904
  ].join("\n");
791
905
  }
@@ -988,6 +1102,122 @@ async function fetchPrdFromNotion(urlOrId) {
988
1102
  };
989
1103
  }
990
1104
 
1105
+ // src/lib/rules-import.ts
1106
+ import { existsSync, readFileSync } from "fs";
1107
+ import { join } from "path";
1108
+ var ADOPT_SOURCES = [
1109
+ ".cursorrules",
1110
+ "CLAUDE.md",
1111
+ "AGENTS.md",
1112
+ ".windsurfrules",
1113
+ ".github/copilot-instructions.md"
1114
+ ];
1115
+ var PREAMBLE_TITLE = "\uC11C\uBB38";
1116
+ function detectExistingRuleFiles(cwd) {
1117
+ const found = [];
1118
+ for (const rel of ADOPT_SOURCES) {
1119
+ const full = join(cwd, rel);
1120
+ if (existsSync(full)) {
1121
+ try {
1122
+ found.push({ path: rel, content: readFileSync(full, "utf-8") });
1123
+ } catch {
1124
+ }
1125
+ }
1126
+ }
1127
+ return found;
1128
+ }
1129
+ function splitSections(content) {
1130
+ const sections = [];
1131
+ let title = "";
1132
+ let buf = [];
1133
+ const preamble = [];
1134
+ let sawHeading = false;
1135
+ for (const line of content.split("\n")) {
1136
+ if (line.startsWith("## ")) {
1137
+ sawHeading = true;
1138
+ if (title) sections.push({ title, content: buf.join("\n").trim() });
1139
+ title = line.replace("## ", "").trim();
1140
+ buf = [];
1141
+ } else if (title) {
1142
+ buf.push(line);
1143
+ } else if (!sawHeading) {
1144
+ preamble.push(line);
1145
+ }
1146
+ }
1147
+ if (title) sections.push({ title, content: buf.join("\n").trim() });
1148
+ const pre = preamble.join("\n").trim();
1149
+ if (pre) sections.unshift({ title: PREAMBLE_TITLE, content: pre });
1150
+ return sections;
1151
+ }
1152
+ function buildAdoptedRules(files, projectName) {
1153
+ const order = [];
1154
+ const byTitle = /* @__PURE__ */ new Map();
1155
+ for (const file of files) {
1156
+ for (const sec of splitSections(file.content)) {
1157
+ let merged = byTitle.get(sec.title);
1158
+ if (!merged) {
1159
+ merged = { title: sec.title, parts: [] };
1160
+ byTitle.set(sec.title, merged);
1161
+ order.push(sec.title);
1162
+ }
1163
+ merged.parts.push({ source: file.path, content: sec.content });
1164
+ }
1165
+ }
1166
+ const lines = [
1167
+ `# ${projectName} \u2014 Rules`,
1168
+ "",
1169
+ "> \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 \uB2E8\uC77C \uC18C\uC2A4(SoT). \uAE30\uC874 \uADDC\uCE59\uC744 `vhk init` adopt \uB85C \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.",
1170
+ "> \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 \uC774 \uD30C\uC77C\uC5D0\uC11C\uB9CC \u2014 `vhk sync` \uB85C \uAC01 \uB3C4\uAD6C\uC5D0 \uC804\uD30C\uB429\uB2C8\uB2E4.",
1171
+ ""
1172
+ ];
1173
+ for (const title of order) {
1174
+ const merged = byTitle.get(title);
1175
+ const nonEmpty = merged.parts.filter((p) => p.content.trim());
1176
+ if (!nonEmpty.length) continue;
1177
+ lines.push(`## ${title}`);
1178
+ for (const part of nonEmpty) {
1179
+ lines.push(`<!-- \uCD9C\uCC98: ${part.source} -->`);
1180
+ lines.push(part.content);
1181
+ }
1182
+ lines.push("");
1183
+ }
1184
+ return lines.join("\n");
1185
+ }
1186
+
1187
+ // src/lib/stack-detect.ts
1188
+ import { join as join2 } from "path";
1189
+ function detectStackFromDeps(deps) {
1190
+ const stack = [];
1191
+ if (deps.next) stack.push("Next.js");
1192
+ else if (deps.nuxt) stack.push("Nuxt");
1193
+ else if (deps.vite || deps["@vitejs/plugin-react"]) stack.push("Vite");
1194
+ if (deps.react) stack.push("React");
1195
+ else if (deps.vue) stack.push("Vue");
1196
+ else if (deps.svelte) stack.push("Svelte");
1197
+ if (deps.typescript) stack.push("TypeScript");
1198
+ if (deps.tailwindcss || deps["@tailwindcss/vite"]) stack.push("Tailwind CSS");
1199
+ if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"]) stack.push("Supabase");
1200
+ if (deps["@prisma/client"] || deps.prisma) stack.push("Prisma");
1201
+ if (deps["drizzle-orm"]) stack.push("Drizzle");
1202
+ if (deps["@anthropic-ai/sdk"]) stack.push("Anthropic SDK");
1203
+ if (deps.openai) stack.push("OpenAI SDK");
1204
+ if (deps.zod) stack.push("zod");
1205
+ if (deps["@tanstack/react-query"]) stack.push("TanStack Query");
1206
+ return stack;
1207
+ }
1208
+ function detectProjectStack(cwd = ".") {
1209
+ let pkg;
1210
+ try {
1211
+ pkg = readJsonFile(join2(cwd, "package.json"));
1212
+ } catch {
1213
+ return null;
1214
+ }
1215
+ const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1216
+ if (Object.keys(all).length === 0) return null;
1217
+ const stack = detectStackFromDeps(all);
1218
+ return stack.length ? stack : null;
1219
+ }
1220
+
991
1221
  // src/commands/init.ts
992
1222
  var PROJECT_TYPES = [
993
1223
  { name: "\u{1F310} \uC6F9 \uC571 (Next.js + Supabase + Vercel)", value: "webapp" },
@@ -1060,7 +1290,9 @@ ${ko.init.title}
1060
1290
  log.error("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984, \uC124\uBA85, \uC720\uD615\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
1061
1291
  process.exit(1);
1062
1292
  }
1063
- const stack = STACK_PRESETS[answers.type];
1293
+ const detected = detectProjectStack(process.cwd());
1294
+ const stack = detected ?? STACK_PRESETS[answers.type];
1295
+ if (detected) console.log(chalk3.dim(" \u{1F50E} package.json \uC758\uC874\uC131\uC5D0\uC11C \uC2E4\uC81C \uC2A4\uD0DD \uAC10\uC9C0"));
1064
1296
  console.log(chalk3.dim(`
1065
1297
  ${ko.init.recommendedStack} ${stack.join(" + ")}
1066
1298
  `));
@@ -1077,7 +1309,27 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1077
1309
  }
1078
1310
  }
1079
1311
  const cwd = process.cwd();
1312
+ let adoptedRules = null;
1313
+ if (!options.yes && !options.fromNotion) {
1314
+ const existingRules = detectExistingRuleFiles(cwd);
1315
+ if (existingRules.length > 0) {
1316
+ const { adopt } = await inquirer2.prompt([{
1317
+ type: "confirm",
1318
+ name: "adopt",
1319
+ message: ko.init.adoptPrompt(
1320
+ existingRules.length,
1321
+ existingRules.map((f) => f.path).join(", ")
1322
+ ),
1323
+ default: true
1324
+ }]);
1325
+ if (adopt) {
1326
+ adoptedRules = buildAdoptedRules(existingRules, answers.name);
1327
+ console.log(chalk3.dim(` ${ko.init.adoptPreview(existingRules.length)}`));
1328
+ }
1329
+ }
1330
+ }
1080
1331
  const files = generateFiles(answers.name, answers.description, stack, prdContent, answers.type);
1332
+ if (adoptedRules) files["RULES.md"] = adoptedRules;
1081
1333
  log.step(ko.init.filesGenerating);
1082
1334
  for (const [filePath, content] of Object.entries(files)) {
1083
1335
  const fullPath = path2.join(cwd, filePath);
@@ -1131,6 +1383,8 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
1131
1383
  return {
1132
1384
  "CLAUDE.md": CLAUDE_MD_TEMPLATE(name, stackStr),
1133
1385
  ".cursorrules": CURSORRULES_TEMPLATE(name, description, stackStr),
1386
+ // RULES.md — 규칙 SoT. init 이 항상 생성해 sync 와 흐름을 연결한다.
1387
+ "RULES.md": RULES_MD_TEMPLATE(name, description, stackStr),
1134
1388
  "docs/PRD.md": PRD_TEMPLATE(name, description, prd),
1135
1389
  "docs/ARCHITECTURE.md": ARCHITECTURE_TEMPLATE(name, stackStr),
1136
1390
  "docs/adr/ADR-000-template.md": ADR_TEMPLATE(),
@@ -1138,8 +1392,9 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
1138
1392
  "docs/troubleshooting/.gitkeep": "",
1139
1393
  "docs/til.md": `# TIL (Today I Learned)
1140
1394
 
1141
- - [${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
1395
+ - [${localDate()}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
1142
1396
  `,
1397
+ // VHK-019
1143
1398
  "BACKLOG.md": `# BACKLOG
1144
1399
 
1145
1400
  > v1 OUT \uAE30\uB2A5\uC740 \uC5EC\uAE30\uC5D0 \uAE30\uB85D. \uBC94\uC704 \uC218\uBE44 \uD544\uC218.
@@ -1197,8 +1452,19 @@ function enhancePackageScripts(projectDir) {
1197
1452
  fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1198
1453
  return true;
1199
1454
  }
1455
+ function projectHasTestScript(projectDir) {
1456
+ const pkgPath = path2.join(projectDir, "package.json");
1457
+ if (!fs2.existsSync(pkgPath)) return false;
1458
+ try {
1459
+ const pkg = readJsonFile(pkgPath);
1460
+ return Boolean(pkg.scripts?.test?.trim());
1461
+ } catch {
1462
+ return false;
1463
+ }
1464
+ }
1200
1465
  async function writeInitExtras(projectDir) {
1201
1466
  const commandsPath = path2.join(projectDir, "COMMANDS.md");
1467
+ const hasTest = projectHasTestScript(projectDir);
1202
1468
  if (fileExists(commandsPath)) {
1203
1469
  const { overwrite } = await inquirer2.prompt([{
1204
1470
  type: "confirm",
@@ -1209,7 +1475,7 @@ async function writeInitExtras(projectDir) {
1209
1475
  if (!overwrite) {
1210
1476
  log.warn(ko.init.skipped("COMMANDS.md"));
1211
1477
  } else {
1212
- writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
1478
+ writeFile(commandsPath, COMMANDS_MD_TEMPLATE({ hasTest }));
1213
1479
  log.success(ko.init.commandsMdDone);
1214
1480
  }
1215
1481
  } else {
@@ -1229,7 +1495,7 @@ async function writeInitExtras(projectDir) {
1229
1495
 
1230
1496
  // src/commands/recap.ts
1231
1497
  import inquirer3 from "inquirer";
1232
- import chalk4 from "chalk";
1498
+ import chalk6 from "chalk";
1233
1499
  import fs4 from "fs";
1234
1500
  import path5 from "path";
1235
1501
 
@@ -1273,10 +1539,13 @@ function buildSessionDiffFromSummary(diffSummary) {
1273
1539
  files
1274
1540
  };
1275
1541
  }
1542
+ var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1276
1543
  async function getSessionDiff(since) {
1277
- const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1544
+ const sinceDate = since || localDate();
1278
1545
  try {
1279
- const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
1546
+ const boundary = (await git.raw(["rev-list", "-1", `--before=${sinceDate}`, "HEAD"])).trim();
1547
+ const base = boundary || EMPTY_TREE_SHA;
1548
+ const diffSummary = await git.diffSummary([`${base}..HEAD`]);
1280
1549
  const normalized = diffSummary.files.map((f) => ({
1281
1550
  file: f.file,
1282
1551
  insertions: "insertions" in f ? f.insertions : 0,
@@ -1379,7 +1648,7 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
1379
1648
  const adrDir = path4.join(cwd, "docs", "adr");
1380
1649
  if (!fs3.existsSync(adrDir)) fs3.mkdirSync(adrDir, { recursive: true });
1381
1650
  const num = nextAdrNumber(adrDir);
1382
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1651
+ const today = localDate();
1383
1652
  const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
1384
1653
  const filePath = path4.join(adrDir, fileName);
1385
1654
  const content = [
@@ -1411,50 +1680,191 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
1411
1680
  return filePath;
1412
1681
  }
1413
1682
 
1683
+ // src/lib/interactive.ts
1684
+ import chalk4 from "chalk";
1685
+ function ensureInteractive(hint = "") {
1686
+ if (process.stdin.isTTY) return true;
1687
+ console.error(chalk4.yellow(" \u26A0\uFE0F \uC774 \uBA85\uB839\uC740 \uB300\uD654\uD615 \uC785\uB825\uC774 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uBE44-TTY/\uD30C\uC774\uD504 \uD658\uACBD\uC5D0\uC11C\uB294 \uC2E4\uD589\uD560 \uC218 \uC5C6\uC5B4\uC694."));
1688
+ if (hint) console.error(chalk4.dim(` ${hint}`));
1689
+ process.exitCode = 1;
1690
+ return false;
1691
+ }
1692
+ function isPromptAbortError(err) {
1693
+ const msg = err instanceof Error ? err.message : String(err);
1694
+ return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
1695
+ }
1696
+
1697
+ // src/lib/hard-stop-guard.ts
1698
+ import chalk5 from "chalk";
1699
+
1700
+ // src/lib/state-files.ts
1701
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync, rmSync } from "fs";
1702
+ import { join as join3 } from "path";
1703
+ var STATE_DIR = "docs/state";
1704
+ var BLOCKERS_PATH = join3(STATE_DIR, "blockers.md");
1705
+ var LEARNINGS_PATH = join3(STATE_DIR, "learnings.md");
1706
+ var VHK_DIR = ".vhk";
1707
+ var HARD_STOP_PATH = join3(VHK_DIR, "HARD_STOP");
1708
+ var HARD_STOP_BLOCKER_THRESHOLD = 3;
1709
+ function ensureStateDir() {
1710
+ mkdirSync(STATE_DIR, { recursive: true });
1711
+ }
1712
+ function ensureVhkDir() {
1713
+ mkdirSync(VHK_DIR, { recursive: true });
1714
+ }
1715
+ function isoDate() {
1716
+ return localDate();
1717
+ }
1718
+ var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
1719
+ function countActiveBlockers(content) {
1720
+ let count = 0;
1721
+ for (const line of content.split(/\r?\n/)) {
1722
+ if (ACTIVE_BLOCKER_RE.test(line)) count++;
1723
+ }
1724
+ return count;
1725
+ }
1726
+ function appendBlocker(description, goalId) {
1727
+ ensureStateDir();
1728
+ const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
1729
+ const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
1730
+ if (!existsSync2(BLOCKERS_PATH)) {
1731
+ writeFileSync(
1732
+ BLOCKERS_PATH,
1733
+ `# Blockers
1734
+
1735
+ _Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
1736
+
1737
+ ${line}
1738
+ `,
1739
+ "utf-8"
1740
+ );
1741
+ } else {
1742
+ appendFileSync(BLOCKERS_PATH, `${line}
1743
+ `, "utf-8");
1744
+ }
1745
+ const current = readFileSync2(BLOCKERS_PATH, "utf-8");
1746
+ const count = countActiveBlockers(current);
1747
+ let hardStopTripped = false;
1748
+ if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync2(HARD_STOP_PATH)) {
1749
+ writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
1750
+ hardStopTripped = true;
1751
+ }
1752
+ return { count, hardStopTripped };
1753
+ }
1754
+ function appendLearning(lesson, goalId) {
1755
+ ensureStateDir();
1756
+ const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
1757
+ const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
1758
+ if (!existsSync2(LEARNINGS_PATH)) {
1759
+ writeFileSync(
1760
+ LEARNINGS_PATH,
1761
+ `# Learnings
1762
+
1763
+ _Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
1764
+
1765
+ ${line}
1766
+ `,
1767
+ "utf-8"
1768
+ );
1769
+ } else {
1770
+ appendFileSync(LEARNINGS_PATH, `${line}
1771
+ `, "utf-8");
1772
+ }
1773
+ }
1774
+ function getRecentLearnings(limit = 3) {
1775
+ if (!existsSync2(LEARNINGS_PATH)) return [];
1776
+ const lines = readFileSync2(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
1777
+ const entries = lines.filter((l) => l.startsWith("- ["));
1778
+ return entries.slice(-limit);
1779
+ }
1780
+ function getActiveBlockers(limit = 3) {
1781
+ if (!existsSync2(BLOCKERS_PATH)) return [];
1782
+ const lines = readFileSync2(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
1783
+ const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
1784
+ return entries.slice(-limit);
1785
+ }
1786
+ function writeHardStop(reason) {
1787
+ ensureVhkDir();
1788
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1789
+ writeFileSync(HARD_STOP_PATH, `${ts}
1790
+ ${reason}
1791
+ `, "utf-8");
1792
+ }
1793
+ function isHardStopActive() {
1794
+ return existsSync2(HARD_STOP_PATH);
1795
+ }
1796
+ function readHardStopReason() {
1797
+ if (!existsSync2(HARD_STOP_PATH)) return null;
1798
+ try {
1799
+ return readFileSync2(HARD_STOP_PATH, "utf-8").trim();
1800
+ } catch {
1801
+ return null;
1802
+ }
1803
+ }
1804
+ function clearHardStop() {
1805
+ if (!existsSync2(HARD_STOP_PATH)) return false;
1806
+ rmSync(HARD_STOP_PATH, { force: true });
1807
+ return true;
1808
+ }
1809
+
1810
+ // src/lib/hard-stop-guard.ts
1811
+ function ensureNotHardStopped(action) {
1812
+ if (!isHardStopActive()) return true;
1813
+ console.error(chalk5.red.bold(`
1814
+ \u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
1815
+ const reason = readHardStopReason();
1816
+ if (reason) console.error(chalk5.dim(` \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}`));
1817
+ console.error(chalk5.dim(" \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)"));
1818
+ process.exitCode = 1;
1819
+ return false;
1820
+ }
1821
+
1414
1822
  // src/commands/recap.ts
1415
1823
  async function recap(options = {}) {
1416
- console.log(chalk4.bold(`
1824
+ if (!ensureNotHardStopped("recap")) return;
1825
+ console.log(chalk6.bold(`
1417
1826
  ${ko.recap.title}
1418
1827
  `));
1419
1828
  if (!await isGitRepo()) {
1420
- console.log(chalk4.red(ko.recap.noRepo));
1829
+ console.log(chalk6.red(ko.recap.noRepo));
1421
1830
  return;
1422
1831
  }
1423
1832
  if (!await hasAnyCommits()) {
1424
- console.log(chalk4.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
1425
- console.log(chalk4.gray(" \uD30C\uC77C\uC744 \uCD94\uAC00\uD558\uACE0 `vhk save` \uB610\uB294 `git commit`\uC73C\uB85C \uCCAB \uCEE4\uBC0B\uC744 \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
1833
+ console.log(chalk6.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
1834
+ console.log(chalk6.gray(" \uD30C\uC77C\uC744 \uCD94\uAC00\uD558\uACE0 `vhk save` \uB610\uB294 `git commit`\uC73C\uB85C \uCCAB \uCEE4\uBC0B\uC744 \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
1426
1835
  return;
1427
1836
  }
1428
1837
  printSecurityWarnings();
1429
- console.log(chalk4.dim(`${ko.recap.analyzing}
1838
+ console.log(chalk6.dim(`${ko.recap.analyzing}
1430
1839
  `));
1431
- const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1840
+ const since = options.since || localDate();
1432
1841
  const diff2 = await getSessionDiff(since);
1433
1842
  const commits = await getRecentCommits(10, since);
1434
1843
  if (diff2.filesChanged === 0 && commits.length === 0) {
1435
- console.log(chalk4.yellow(ko.recap.noChanges));
1844
+ console.log(chalk6.yellow(ko.recap.noChanges));
1436
1845
  return;
1437
1846
  }
1438
- console.log(chalk4.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1439
- console.log(` \uD30C\uC77C: ${chalk4.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1440
- console.log(` \uCD94\uAC00: ${chalk4.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk4.red("-" + diff2.deletions)}`);
1847
+ console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1848
+ console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1849
+ console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
1441
1850
  if (diff2.files.length > 0) {
1442
- console.log(chalk4.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1851
+ console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1443
1852
  diff2.files.slice(0, 15).forEach((f) => {
1444
- const icon = f.status === "new" ? chalk4.green("\u{1F195}") : f.status === "deleted" ? chalk4.red("\u{1F5D1}\uFE0F") : chalk4.yellow("\u270F\uFE0F");
1853
+ const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
1445
1854
  console.log(` ${icon} ${f.file}`);
1446
1855
  });
1447
1856
  if (diff2.files.length > 15) {
1448
- console.log(chalk4.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1857
+ console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1449
1858
  }
1450
1859
  }
1451
1860
  if (commits.length > 0) {
1452
- console.log(chalk4.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1861
+ console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1453
1862
  commits.slice(0, 5).forEach((c) => {
1454
- console.log(chalk4.dim(` \u2022 ${c.message}`));
1863
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1455
1864
  });
1456
1865
  }
1457
1866
  console.log("");
1867
+ if (!ensureInteractive("\uD68C\uACE0 \uC785\uB825\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
1458
1868
  const answers = await inquirer3.prompt([
1459
1869
  {
1460
1870
  type: "input",
@@ -1479,7 +1889,7 @@ ${ko.recap.title}
1479
1889
  default: "\uC5C6\uC74C"
1480
1890
  }
1481
1891
  ]);
1482
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1892
+ const today = localDate();
1483
1893
  const logDir = path5.join(process.cwd(), "docs", "log");
1484
1894
  if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true });
1485
1895
  const existing = fs4.readdirSync(logDir).filter((f) => f.startsWith(today));
@@ -1519,11 +1929,11 @@ ${ko.recap.title}
1519
1929
  fs4.writeFileSync(filePath, content, "utf-8");
1520
1930
  const adrCandidates = detectAdrCandidates(diff2);
1521
1931
  if (adrCandidates.length > 0) {
1522
- console.log(chalk4.cyan.bold(`
1932
+ console.log(chalk6.cyan.bold(`
1523
1933
  ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1524
1934
  for (const candidate of adrCandidates) {
1525
- console.log(chalk4.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
1526
- candidate.files.forEach((f) => console.log(chalk4.dim(` ${f}`)));
1935
+ console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
1936
+ candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
1527
1937
  }
1528
1938
  const { createAdr } = await inquirer3.prompt([{
1529
1939
  type: "confirm",
@@ -1553,17 +1963,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1553
1963
  adrAnswers.decision,
1554
1964
  adrAnswers.consequences
1555
1965
  );
1556
- console.log(chalk4.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
1966
+ console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
1557
1967
  }
1558
1968
  }
1559
1969
  }
1560
1970
  const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
1561
1971
  const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
1562
1972
  if (troubleCommits.length > 0) {
1563
- console.log(chalk4.yellow.bold(`
1973
+ console.log(chalk6.yellow.bold(`
1564
1974
  ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1565
1975
  troubleCommits.forEach((c) => {
1566
- console.log(chalk4.dim(` \u2022 ${c.message}`));
1976
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1567
1977
  });
1568
1978
  const { createTroubleshoot } = await inquirer3.prompt([{
1569
1979
  type: "confirm",
@@ -1614,12 +2024,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1614
2024
  `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1615
2025
  ].join("\n");
1616
2026
  fs4.writeFileSync(tsFilePath, tsContent, "utf-8");
1617
- console.log(chalk4.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
2027
+ console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
1618
2028
  }
1619
2029
  }
1620
- console.log(chalk4.green.bold(`
2030
+ console.log(chalk6.green.bold(`
1621
2031
  ${ko.recap.done}`));
1622
- console.log(chalk4.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
2032
+ console.log(chalk6.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
1623
2033
  const claudeMdPath = path5.join(process.cwd(), "CLAUDE.md");
1624
2034
  if (fs4.existsSync(claudeMdPath)) {
1625
2035
  const { updateClaude } = await inquirer3.prompt([{
@@ -1639,7 +2049,7 @@ ${ko.recap.done}`));
1639
2049
  `- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
1640
2050
  );
1641
2051
  fs4.writeFileSync(claudeMdPath, claudeContent, "utf-8");
1642
- console.log(chalk4.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
2052
+ console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
1643
2053
  }
1644
2054
  }
1645
2055
  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"';
@@ -1651,38 +2061,249 @@ ${ko.recap.done}`));
1651
2061
  }
1652
2062
 
1653
2063
  // src/commands/sync.ts
1654
- import chalk5 from "chalk";
2064
+ import chalk7 from "chalk";
2065
+ import fs7 from "fs";
2066
+ import path8 from "path";
2067
+ import inquirer4 from "inquirer";
2068
+
2069
+ // src/lib/drift.ts
1655
2070
  import fs5 from "fs";
1656
2071
  import path6 from "path";
1657
- var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
1658
- var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
1659
- function parseRulesMd(content) {
1660
- const sections = [];
1661
- const lines = content.split("\n");
1662
- let currentTitle = "";
1663
- let currentContent = [];
1664
- for (const line of lines) {
1665
- if (line.startsWith("## ")) {
1666
- if (currentTitle) {
1667
- sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
1668
- }
1669
- currentTitle = line.replace("## ", "").trim();
1670
- currentContent = [];
1671
- } else {
1672
- currentContent.push(line);
1673
- }
2072
+
2073
+ // src/lib/git-repo.ts
2074
+ import { execFileSync } from "child_process";
2075
+ function getGitRoot(cwd = process.cwd()) {
2076
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
2077
+ encoding: "utf-8",
2078
+ cwd,
2079
+ stdio: ["pipe", "pipe", "pipe"]
2080
+ }).trim();
2081
+ }
2082
+ function gitOut(args, cwd) {
2083
+ return execFileSync("git", args, {
2084
+ encoding: "utf-8",
2085
+ cwd,
2086
+ stdio: ["pipe", "pipe", "pipe"]
2087
+ });
2088
+ }
2089
+ function gitRun(args, cwd) {
2090
+ execFileSync("git", args, { stdio: "pipe", cwd });
2091
+ }
2092
+ function getExecErrorMessage(err) {
2093
+ if (err && typeof err === "object" && "stderr" in err) {
2094
+ const stderr = err.stderr;
2095
+ if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
2096
+ if (typeof stderr === "string") return stderr.trim();
1674
2097
  }
1675
- if (currentTitle) {
1676
- sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
2098
+ return err instanceof Error ? err.message : String(err);
2099
+ }
2100
+ function hasGitRemote(cwd) {
2101
+ try {
2102
+ return gitOut(["remote"], cwd).trim().length > 0;
2103
+ } catch {
2104
+ return false;
1677
2105
  }
1678
- return sections;
1679
2106
  }
1680
- function buildCodingDoc(headerTitle, sections, projectName) {
1681
- const codingSections = sections.filter(
1682
- (s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
1683
- );
1684
- const lines = [
1685
- `# ${projectName} \u2014 ${headerTitle}`,
2107
+ function countLocalCommits(cwd) {
2108
+ try {
2109
+ const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
2110
+ return parseInt(out, 10) || 0;
2111
+ } catch {
2112
+ return 0;
2113
+ }
2114
+ }
2115
+
2116
+ // src/lib/drift.ts
2117
+ function normalizeForCompare(s) {
2118
+ return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
2119
+ }
2120
+ function checkRuleDrift(rootDir) {
2121
+ const rulesPath = path6.join(rootDir, "RULES.md");
2122
+ if (!fs5.existsSync(rulesPath)) return { checked: false, results: [] };
2123
+ const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
2124
+ const sections = parseRulesMd(rulesContent);
2125
+ const projectName = deriveProjectName(rulesContent);
2126
+ const results = [];
2127
+ for (const target of SYNC_TARGETS) {
2128
+ const fullPath = path6.join(rootDir, target.path);
2129
+ if (!fs5.existsSync(fullPath)) {
2130
+ results.push({ path: target.path, status: "missing" });
2131
+ continue;
2132
+ }
2133
+ const expected = normalizeForCompare(target.generate(sections, projectName));
2134
+ const actual = normalizeForCompare(fs5.readFileSync(fullPath, "utf-8"));
2135
+ results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
2136
+ }
2137
+ return { checked: true, results };
2138
+ }
2139
+ var CONTEXT_GIT_MARKER = "vhk-context-git";
2140
+ var CONTEXT_PATH = ".vhk/context.md";
2141
+ function extractContextSha(content) {
2142
+ const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
2143
+ return m ? m[1] : null;
2144
+ }
2145
+ var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
2146
+ function contextSourcesChanged(generatedSha, rootDir) {
2147
+ const content = gitOut(
2148
+ ["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
2149
+ rootDir
2150
+ ).trim();
2151
+ if (content) return true;
2152
+ const structural = gitOut(
2153
+ ["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
2154
+ rootDir
2155
+ ).trim();
2156
+ return structural.length > 0;
2157
+ }
2158
+ function checkContextDrift(rootDir) {
2159
+ const ctxPath = path6.join(rootDir, CONTEXT_PATH);
2160
+ if (!fs5.existsSync(ctxPath)) return { checked: false, stale: false };
2161
+ const generatedSha = extractContextSha(fs5.readFileSync(ctxPath, "utf-8"));
2162
+ if (!generatedSha) return { checked: false, stale: false };
2163
+ let currentSha;
2164
+ try {
2165
+ currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
2166
+ } catch {
2167
+ return { checked: false, stale: false };
2168
+ }
2169
+ if (!currentSha) return { checked: false, stale: false };
2170
+ if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
2171
+ return { checked: true, stale: false, generatedSha, currentSha };
2172
+ }
2173
+ let stale;
2174
+ try {
2175
+ stale = contextSourcesChanged(generatedSha, rootDir);
2176
+ } catch {
2177
+ return { checked: false, stale: false };
2178
+ }
2179
+ return { checked: true, stale, generatedSha, currentSha };
2180
+ }
2181
+
2182
+ // src/lib/backup.ts
2183
+ import fs6 from "fs";
2184
+ import path7 from "path";
2185
+ var BACKUPS_REL = path7.join(".vhk", "backups");
2186
+ var VHK_GITIGNORE_REL = path7.join(".vhk", ".gitignore");
2187
+ function fsSafeStamp(d) {
2188
+ return d.toISOString().replace(/[:.]/g, "-");
2189
+ }
2190
+ function ensureVhkIgnored(rootDir, ...entries) {
2191
+ const giPath = path7.join(rootDir, VHK_GITIGNORE_REL);
2192
+ fs6.mkdirSync(path7.dirname(giPath), { recursive: true });
2193
+ let content = fs6.existsSync(giPath) ? fs6.readFileSync(giPath, "utf-8") : "";
2194
+ const present = new Set(content.split("\n").map((l) => l.trim().replace(/\/$/, "")));
2195
+ const missing = entries.filter((e) => !present.has(e.trim().replace(/\/$/, "")));
2196
+ if (missing.length === 0) return;
2197
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
2198
+ content += missing.join("\n") + "\n";
2199
+ fs6.writeFileSync(giPath, content, "utf-8");
2200
+ }
2201
+ function walkRelFiles(baseDir, cur = baseDir) {
2202
+ const out = [];
2203
+ for (const entry of fs6.readdirSync(cur)) {
2204
+ const full = path7.join(cur, entry);
2205
+ if (fs6.statSync(full).isDirectory()) {
2206
+ out.push(...walkRelFiles(baseDir, full));
2207
+ } else {
2208
+ out.push(path7.relative(baseDir, full).split(path7.sep).join("/"));
2209
+ }
2210
+ }
2211
+ return out;
2212
+ }
2213
+ function saveBackup(files, rootDir, stamp) {
2214
+ const baseId = stamp ?? fsSafeStamp(/* @__PURE__ */ new Date());
2215
+ let id = baseId;
2216
+ let n = 1;
2217
+ while (fs6.existsSync(path7.join(rootDir, BACKUPS_REL, id))) {
2218
+ id = `${baseId}-${String(n++).padStart(3, "0")}`;
2219
+ }
2220
+ const backupDir = path7.join(rootDir, BACKUPS_REL, id);
2221
+ const saved = [];
2222
+ for (const rel of files) {
2223
+ const src = path7.join(rootDir, rel);
2224
+ if (!fs6.existsSync(src)) continue;
2225
+ const dest = path7.join(backupDir, rel);
2226
+ fs6.mkdirSync(path7.dirname(dest), { recursive: true });
2227
+ fs6.copyFileSync(src, dest);
2228
+ saved.push(rel);
2229
+ }
2230
+ ensureVhkIgnored(rootDir, "backups/");
2231
+ return { id, dir: backupDir, files: saved };
2232
+ }
2233
+ function backupOrderKey(id) {
2234
+ const m = /^(.*Z)(?:-(\d+))?$/.exec(id);
2235
+ return m ? [m[1], m[2] ? parseInt(m[2], 10) : 0] : [id, 0];
2236
+ }
2237
+ function listBackups(rootDir) {
2238
+ const root = path7.join(rootDir, BACKUPS_REL);
2239
+ if (!fs6.existsSync(root)) return [];
2240
+ return fs6.readdirSync(root).filter((e) => fs6.statSync(path7.join(root, e)).isDirectory()).sort((a, b) => {
2241
+ const [ba, na] = backupOrderKey(a);
2242
+ const [bb, nb] = backupOrderKey(b);
2243
+ if (ba !== bb) return ba < bb ? 1 : -1;
2244
+ return nb - na;
2245
+ }).map((id) => {
2246
+ const dir = path7.join(root, id);
2247
+ return { id, dir, files: walkRelFiles(dir) };
2248
+ });
2249
+ }
2250
+ function restoreBackup(id, rootDir) {
2251
+ const backupDir = path7.join(rootDir, BACKUPS_REL, id);
2252
+ if (!fs6.existsSync(backupDir)) {
2253
+ throw new Error(`\uBC31\uC5C5 \uC5C6\uC74C: ${id}`);
2254
+ }
2255
+ const rels = walkRelFiles(backupDir);
2256
+ for (const rel of rels) {
2257
+ const src = path7.join(backupDir, rel);
2258
+ const dest = path7.join(rootDir, rel);
2259
+ fs6.mkdirSync(path7.dirname(dest), { recursive: true });
2260
+ fs6.copyFileSync(src, dest);
2261
+ }
2262
+ return rels;
2263
+ }
2264
+ function pruneBackups(keepN, rootDir) {
2265
+ const all = listBackups(rootDir);
2266
+ const toDelete = all.slice(Math.max(0, keepN));
2267
+ for (const b of toDelete) {
2268
+ fs6.rmSync(b.dir, { recursive: true, force: true });
2269
+ }
2270
+ return toDelete.map((b) => b.id);
2271
+ }
2272
+
2273
+ // src/commands/sync.ts
2274
+ var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
2275
+ var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
2276
+ function findUnmappedSections(sections) {
2277
+ const allKeys = [...CURSORRULES_KEYS, ...CLAUDE_MD_KEYS];
2278
+ return sections.filter((s) => s.title !== PREAMBLE_TITLE && !allKeys.some((k) => s.title.includes(k))).map((s) => s.title);
2279
+ }
2280
+ function parseRulesMd(content) {
2281
+ const sections = [];
2282
+ const lines = content.split("\n");
2283
+ let currentTitle = "";
2284
+ let currentContent = [];
2285
+ for (const line of lines) {
2286
+ if (line.startsWith("## ")) {
2287
+ if (currentTitle) {
2288
+ sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
2289
+ }
2290
+ currentTitle = line.replace("## ", "").trim();
2291
+ currentContent = [];
2292
+ } else {
2293
+ currentContent.push(line);
2294
+ }
2295
+ }
2296
+ if (currentTitle) {
2297
+ sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
2298
+ }
2299
+ return sections;
2300
+ }
2301
+ function buildCodingDoc(headerTitle, sections, projectName) {
2302
+ const codingSections = sections.filter(
2303
+ (s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
2304
+ );
2305
+ const lines = [
2306
+ `# ${projectName} \u2014 ${headerTitle}`,
1686
2307
  "",
1687
2308
  "> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
1688
2309
  "> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
@@ -1731,21 +2352,51 @@ function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
1731
2352
  function toAntigravityRules(sections, projectName) {
1732
2353
  return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
1733
2354
  }
2355
+ var CLAUDE_AUTOGEN_BANNER = "> \u26A1 \uC544\uB798 \uADDC\uCE59 \uC139\uC158\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.";
1734
2356
  function toClaudeMd(sections, existing) {
1735
2357
  const recordSections = sections.filter(
1736
2358
  (s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
1737
2359
  );
1738
- const statusMatch = existing.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
1739
- const statusSection = statusMatch ? statusMatch[0] : "";
1740
- const header = existing.split("## ")[0].trim();
2360
+ const cleaned = existing.split("\n").filter((line) => line.trim() !== CLAUDE_AUTOGEN_BANNER).join("\n");
2361
+ const statusMatch = cleaned.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
2362
+ const statusSection = statusMatch ? statusMatch[0].trimEnd() : "";
2363
+ const header = cleaned.split("## ")[0].trim();
1741
2364
  const lines = [
1742
2365
  header,
1743
2366
  "",
1744
2367
  statusSection,
1745
2368
  "",
1746
- "> \u26A1 \uC544\uB798 \uADDC\uCE59 \uC139\uC158\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
2369
+ CLAUDE_AUTOGEN_BANNER,
2370
+ ""
2371
+ ];
2372
+ for (const section of recordSections) {
2373
+ lines.push(`## ${section.title}`);
2374
+ lines.push(section.content);
2375
+ lines.push("");
2376
+ }
2377
+ return lines.join("\n");
2378
+ }
2379
+ function toAgentsMd(sections, projectName) {
2380
+ const codingSections = sections.filter((s) => CURSORRULES_KEYS.some((k) => s.title.includes(k)));
2381
+ const recordSections = sections.filter((s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k)));
2382
+ const lines = [
2383
+ `# ${projectName} \u2014 AGENTS.md (\uC5D0\uC774\uC804\uD2B8 \uC791\uB3D9 \uADDC\uC57D)`,
2384
+ "",
2385
+ "> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
2386
+ "> \uBE60\uB978 \uC2DC\uC791(\uD1A0\uD070 \uC808\uAC10): `docs/context/agent-compact.md` \uB97C \uBA3C\uC800 \uC77D\uC73C\uC138\uC694.",
2387
+ "",
2388
+ "## Loop Protocol",
2389
+ "- \uB8E8\uD504: `context \u2192 goal next \u2192 \uC791\uC5C5 \u2192 goal check \u2192 goal done`",
2390
+ "- \uC791\uC5C5 \uC2DC\uC791 \uC2DC `.vhk/HARD_STOP` \uD655\uC778 \u2014 \uC788\uC73C\uBA74 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC989\uC2DC \uC911\uB2E8.",
2391
+ "- active goal \uB9CC \uC791\uC5C5. `docs/state`(next-task/blockers/learnings)\uB294 SoT, append-only.",
2392
+ "- \uAC8C\uC774\uD2B8(tsc / test:run / build) \uD1B5\uACFC\uD574\uC57C\uB9CC `vhk goal done`.",
1747
2393
  ""
1748
2394
  ];
2395
+ for (const section of codingSections) {
2396
+ lines.push(`## ${section.title}`);
2397
+ lines.push(section.content);
2398
+ lines.push("");
2399
+ }
1749
2400
  for (const section of recordSections) {
1750
2401
  lines.push(`## ${section.title}`);
1751
2402
  lines.push(section.content);
@@ -1761,56 +2412,173 @@ var SYNC_TARGETS = [
1761
2412
  { path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
1762
2413
  { path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
1763
2414
  { path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
1764
- { path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone }
2415
+ { path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone },
2416
+ // AGENTS.md — 6번째 타겟. 항목 1개 추가로 sync·드리프트·백업 가드가 자동 반영된다.
2417
+ { path: "AGENTS.md", generate: toAgentsMd, doneMessage: ko.sync.agentsDone }
1765
2418
  ];
1766
- async function sync() {
1767
- console.log(chalk5.bold(`
2419
+ var BACKUP_KEEP = 10;
2420
+ var SYNCED_MARKER_REL = path8.join(".vhk", ".synced");
2421
+ function buildSyncPlan(rootDir, sections, projectName) {
2422
+ const plan = [];
2423
+ for (const target of SYNC_TARGETS) {
2424
+ const fullPath = path8.join(rootDir, target.path);
2425
+ const exists = fs7.existsSync(fullPath);
2426
+ const newContent = target.generate(sections, projectName);
2427
+ const drift = exists ? normalizeForCompare(fs7.readFileSync(fullPath, "utf-8")) !== normalizeForCompare(newContent) : false;
2428
+ plan.push({ path: target.path, newContent, doneMessage: target.doneMessage, exists, drift });
2429
+ }
2430
+ const claudePath = path8.join(rootDir, "CLAUDE.md");
2431
+ const claudeExists = fs7.existsSync(claudePath);
2432
+ const existingClaude = claudeExists ? fs7.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
2433
+
2434
+ ## \uD604\uC7AC \uC0C1\uD0DC
2435
+ - **Phase:** **FILL**
2436
+ - **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
2437
+ - **\uB2E4\uC74C \uC561\uC158:** **FILL**
2438
+ - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${localDate()}`;
2439
+ const claudeNew = toClaudeMd(sections, existingClaude);
2440
+ const claudeDrift = claudeExists ? normalizeForCompare(existingClaude) !== normalizeForCompare(claudeNew) : false;
2441
+ plan.push({
2442
+ path: "CLAUDE.md",
2443
+ newContent: claudeNew,
2444
+ doneMessage: ko.sync.claudeDone,
2445
+ exists: claudeExists,
2446
+ drift: claudeDrift
2447
+ });
2448
+ return plan;
2449
+ }
2450
+ async function syncCore(rootDir, opts, confirmOverwrite) {
2451
+ const rulesContent = fs7.readFileSync(path8.join(rootDir, "RULES.md"), "utf-8");
2452
+ const sections = parseRulesMd(rulesContent);
2453
+ const projectName = deriveProjectName(rulesContent);
2454
+ const plan = buildSyncPlan(rootDir, sections, projectName);
2455
+ const firstSync = !fs7.existsSync(path8.join(rootDir, SYNCED_MARKER_REL));
2456
+ const unmapped = findUnmappedSections(sections);
2457
+ if (opts.dryRun) {
2458
+ return {
2459
+ dryRun: true,
2460
+ firstSync,
2461
+ backupId: null,
2462
+ backedUp: [],
2463
+ written: [],
2464
+ skipped: [],
2465
+ truncated: [],
2466
+ plan,
2467
+ unmapped
2468
+ };
2469
+ }
2470
+ const toBackup = plan.filter((p) => p.exists && (p.drift || firstSync)).map((p) => p.path);
2471
+ let backupId = null;
2472
+ let backedUp = [];
2473
+ if (toBackup.length) {
2474
+ const info = saveBackup(toBackup, rootDir);
2475
+ pruneBackups(BACKUP_KEEP, rootDir);
2476
+ backupId = info.id;
2477
+ backedUp = info.files;
2478
+ }
2479
+ const drifted = plan.filter((p) => p.drift);
2480
+ const overwriteDrift = drifted.length ? await confirmOverwrite(drifted) : true;
2481
+ const written = [];
2482
+ const skipped = [];
2483
+ const truncated = [];
2484
+ for (const item of plan) {
2485
+ if (item.drift && !overwriteDrift) {
2486
+ skipped.push(item.path);
2487
+ continue;
2488
+ }
2489
+ const fullPath = path8.join(rootDir, item.path);
2490
+ fs7.mkdirSync(path8.dirname(fullPath), { recursive: true });
2491
+ fs7.writeFileSync(fullPath, item.newContent, "utf-8");
2492
+ written.push(item.path);
2493
+ if (item.newContent.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
2494
+ truncated.push(item.path);
2495
+ }
2496
+ }
2497
+ fs7.mkdirSync(path8.join(rootDir, ".vhk"), { recursive: true });
2498
+ fs7.writeFileSync(path8.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
2499
+ ensureVhkIgnored(rootDir, ".synced");
2500
+ return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped };
2501
+ }
2502
+ async function sync(opts = {}) {
2503
+ console.log(chalk7.bold(`
1768
2504
  ${ko.sync.title}
1769
2505
  `));
1770
2506
  const cwd = process.cwd();
1771
- const rulesPath = path6.join(cwd, "RULES.md");
1772
- if (!fs5.existsSync(rulesPath)) {
1773
- console.log(chalk5.yellow(ko.sync.noRules));
1774
- console.log(chalk5.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
1775
- console.log(chalk5.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
2507
+ const rulesPath = path8.join(cwd, "RULES.md");
2508
+ if (!fs7.existsSync(rulesPath)) {
2509
+ console.log(chalk7.yellow(ko.sync.noRules));
2510
+ console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
2511
+ console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
1776
2512
  console.log("");
1777
- console.log(chalk5.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
1778
- console.log(chalk5.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
1779
- console.log(chalk5.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
1780
- console.log(chalk5.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
1781
- console.log(chalk5.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
1782
- console.log(chalk5.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
2513
+ console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
2514
+ console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
2515
+ console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
2516
+ console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
2517
+ console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
2518
+ console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
1783
2519
  return;
1784
2520
  }
1785
- const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
1786
- const sections = parseRulesMd(rulesContent);
1787
- console.log(chalk5.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
1788
- const projectName = deriveProjectName(rulesContent);
1789
- for (const target of SYNC_TARGETS) {
1790
- const fullPath = path6.join(cwd, target.path);
1791
- fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
1792
- const content = target.generate(sections, projectName);
1793
- fs5.writeFileSync(fullPath, content, "utf-8");
1794
- console.log(chalk5.green(` ${target.doneMessage}`));
1795
- if (content.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
1796
- console.log(chalk5.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
2521
+ const sections = parseRulesMd(fs7.readFileSync(rulesPath, "utf-8"));
2522
+ console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
2523
+ const interactive = !!process.stdout.isTTY && !opts.yes;
2524
+ const confirmOverwrite = async (drifted) => {
2525
+ if (!interactive) return true;
2526
+ for (const d of drifted) console.log(chalk7.yellow(` ${ko.sync.driftWarn(d.path)}`));
2527
+ const { confirm } = await inquirer4.prompt([
2528
+ {
2529
+ type: "confirm",
2530
+ name: "confirm",
2531
+ message: ko.sync.driftConfirm(drifted.length),
2532
+ default: false
2533
+ }
2534
+ ]);
2535
+ return confirm;
2536
+ };
2537
+ const result = await syncCore(cwd, opts, confirmOverwrite);
2538
+ if (result.unmapped.length) {
2539
+ console.error(
2540
+ chalk7.yellow(
2541
+ ` \u26A0\uFE0F ${result.unmapped.length}\uAC1C \uC139\uC158\uC774 \uC5B4\uB290 \uD0C0\uAE43\uC5D0\uB3C4 \uB9E4\uD551 \uC548 \uB3FC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC81C\uC678\uB428: ${result.unmapped.join(", ")}
2542
+ (\uCF54\uB529 \uADDC\uCE59/\uAE30\uC220 \uC2A4\uD0DD/\uCEE4\uBC0B/\uAE30\uB85D \uB4F1 \uD45C\uC900 \uC81C\uBAA9\uC744 \uC4F0\uAC70\uB098, \uC774 \uC139\uC158\uC740 RULES.md \uC5D0\uB9CC \uBCF4\uC874\uB429\uB2C8\uB2E4.)`
2543
+ )
2544
+ );
2545
+ }
2546
+ if (result.dryRun) {
2547
+ console.log(chalk7.cyan(`
2548
+ ${ko.sync.dryRunHeader}`));
2549
+ for (const item of result.plan) {
2550
+ console.log(ko.sync.dryRunWouldWrite(item.path, item.exists && item.drift));
2551
+ }
2552
+ const wouldBackup = result.plan.filter((p) => p.exists && (p.drift || result.firstSync)).map((p) => p.path);
2553
+ if (wouldBackup.length) {
2554
+ console.log(chalk7.dim(`
2555
+ \uBC31\uC5C5 \uC608\uC815(${wouldBackup.length}): ${wouldBackup.join(", ")}`));
1797
2556
  }
2557
+ return;
1798
2558
  }
1799
- const claudePath = path6.join(cwd, "CLAUDE.md");
1800
- const existingClaude = fs5.existsSync(claudePath) ? fs5.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
1801
-
1802
- ## \uD604\uC7AC \uC0C1\uD0DC
1803
- - **Phase:** __FILL__
1804
- - **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
1805
- - **\uB2E4\uC74C \uC561\uC158:** __FILL__
1806
- - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
1807
- fs5.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
1808
- console.log(chalk5.green(` ${ko.sync.claudeDone}`));
1809
- console.log(chalk5.bold.green(`
2559
+ if (result.backupId) {
2560
+ if (result.firstSync) console.log(chalk7.cyan(` ${ko.sync.firstSync}`));
2561
+ if (!process.stdout.isTTY) {
2562
+ console.log(chalk7.yellow(` ${ko.sync.nonTtyAuto(result.backedUp.length, result.backupId)}`));
2563
+ } else {
2564
+ console.log(chalk7.cyan(` ${ko.sync.backupSaved(result.backedUp.length, result.backupId)}`));
2565
+ }
2566
+ }
2567
+ for (const p of result.written) {
2568
+ const item = result.plan.find((i) => i.path === p);
2569
+ if (item) console.log(chalk7.green(` ${item.doneMessage}`));
2570
+ }
2571
+ for (const _ of result.truncated) {
2572
+ console.log(chalk7.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
2573
+ }
2574
+ for (const p of result.skipped) {
2575
+ console.log(chalk7.gray(` ${ko.sync.skipped(p)}`));
2576
+ }
2577
+ console.log(chalk7.bold.green(`
1810
2578
  ${ko.sync.done}`));
1811
- console.log(chalk5.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
1812
- console.log(chalk5.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
1813
- console.log(chalk5.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
2579
+ console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
2580
+ console.log(chalk7.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
2581
+ console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
1814
2582
  printNextStep({
1815
2583
  message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
1816
2584
  command: "vhk \uC810\uAC80",
@@ -1819,68 +2587,72 @@ ${ko.sync.done}`));
1819
2587
  }
1820
2588
 
1821
2589
  // src/commands/check.ts
1822
- import chalk7 from "chalk";
1823
- import path8 from "path";
1824
- import fs7 from "fs";
2590
+ import chalk9 from "chalk";
2591
+ import path10 from "path";
2592
+ import fs9 from "fs";
1825
2593
 
1826
2594
  // src/lib/rules-parser.ts
1827
- import fs6 from "fs";
1828
- import path7 from "path";
2595
+ import fs8 from "fs";
2596
+ import path9 from "path";
1829
2597
  function parseRules(rulesPath) {
1830
- if (!fs6.existsSync(rulesPath)) return [];
1831
- const content = fs6.readFileSync(rulesPath, "utf-8");
2598
+ if (!fs8.existsSync(rulesPath)) return [];
2599
+ const content = fs8.readFileSync(rulesPath, "utf-8");
1832
2600
  const lines = content.split("\n");
1833
2601
  const rules = [];
1834
2602
  let currentSection = "";
1835
- let ruleIndex = 0;
1836
- for (const line of lines) {
2603
+ for (let i = 0; i < lines.length; i++) {
2604
+ const line = lines[i];
2605
+ const lineNo = i + 1;
1837
2606
  if (line.startsWith("## ")) {
1838
2607
  currentSection = line.replace("## ", "").trim();
1839
2608
  continue;
1840
2609
  }
1841
2610
  const bulletMatch = line.match(/^[-*]\s+(.+)/);
1842
2611
  if (!bulletMatch) continue;
2612
+ if (isMetaSection(currentSection)) continue;
1843
2613
  const ruleText = bulletMatch[1];
1844
- ruleIndex++;
1845
2614
  if (/kebab[- ]?case/i.test(ruleText)) {
1846
- rules.push(createNamingRule(
1847
- `naming-${ruleIndex}`,
1848
- currentSection,
1849
- ruleText,
1850
- "kebab-case"
1851
- ));
2615
+ rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "kebab-case"));
1852
2616
  } else if (/camel[- ]?case/i.test(ruleText)) {
1853
- rules.push(createNamingRule(
1854
- `naming-${ruleIndex}`,
1855
- currentSection,
1856
- ruleText,
1857
- "camelCase"
1858
- ));
2617
+ rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "camelCase"));
1859
2618
  }
1860
- const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
1861
- if (pathMatch) {
1862
- rules.push(createStructureRule(
1863
- `structure-${ruleIndex}`,
1864
- currentSection,
1865
- ruleText,
1866
- pathMatch[1]
1867
- ));
1868
- }
1869
- if (/금지|사용하지|쓰지 마|하지 않는다|never use|do not use/i.test(ruleText)) {
1870
- const backtickContent = ruleText.match(/`([^`]+)`/);
1871
- if (backtickContent) {
1872
- rules.push(createContentRule(
1873
- `ban-${ruleIndex}`,
1874
- currentSection,
1875
- ruleText,
1876
- backtickContent[1],
1877
- "banned"
1878
- ));
2619
+ if (isStructureSection(currentSection)) {
2620
+ const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
2621
+ if (pathMatch) {
2622
+ rules.push(createStructureRule(`structure-L${lineNo}`, currentSection, ruleText, pathMatch[1]));
1879
2623
  }
1880
2624
  }
2625
+ const banToken = extractBanToken(ruleText);
2626
+ if (banToken) {
2627
+ rules.push(createContentRule(`ban-L${lineNo}`, currentSection, ruleText, banToken, "banned"));
2628
+ }
1881
2629
  }
1882
2630
  return rules;
1883
2631
  }
2632
+ function isMetaSection(section) {
2633
+ const s = section.toLowerCase();
2634
+ return ["\uAE30\uB85D", "\uB85C\uADF8", "adr", "\uD2B8\uB7EC\uBE14", "til", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8", "\uC9C0\uCE68", "\uC774\uC288", "\uC6B4\uC601", "\uCEE4\uBC0B"].some(
2635
+ (k) => s.includes(k)
2636
+ );
2637
+ }
2638
+ function isStructureSection(section) {
2639
+ const s = section.toLowerCase();
2640
+ return ["\uC544\uD0A4\uD14D\uCC98", "\uAD6C\uC870", "\uB514\uB809\uD130\uB9AC", "\uD3F4\uB354", "architecture", "structure"].some((k) => s.includes(k));
2641
+ }
2642
+ function extractBanToken(ruleText) {
2643
+ const banKw = ruleText.search(/금지|사용하지|쓰지\s*마|하지\s*않는다|never use|do not use/i);
2644
+ if (banKw < 0) return null;
2645
+ let token = null;
2646
+ const before = [...ruleText.slice(0, banKw).matchAll(/`([^`]+)`/g)].pop();
2647
+ if (before) token = before[1];
2648
+ else {
2649
+ const after = ruleText.slice(banKw).match(/^(?:금지|사용하지|쓰지\s*마|do not use|never use)\s*[::]?\s*`([^`]+)`/i);
2650
+ if (after) token = after[1];
2651
+ }
2652
+ if (!token) return null;
2653
+ if (/:\/\/|www\.|\.(com|io|dev|net|org|app)\b|\//.test(token)) return null;
2654
+ return token;
2655
+ }
1884
2656
  function createNamingRule(id, section, desc, convention) {
1885
2657
  return {
1886
2658
  id,
@@ -1889,17 +2661,17 @@ function createNamingRule(id, section, desc, convention) {
1889
2661
  description: desc,
1890
2662
  check: (cwd) => {
1891
2663
  const violations = [];
1892
- const srcDir = path7.join(cwd, "src");
1893
- if (!fs6.existsSync(srcDir)) return violations;
2664
+ const srcDir = path9.join(cwd, "src");
2665
+ if (!fs8.existsSync(srcDir)) return violations;
1894
2666
  walkFiles(srcDir, (filePath) => {
1895
- const name = path7.basename(filePath, path7.extname(filePath));
2667
+ const name = path9.basename(filePath, path9.extname(filePath));
1896
2668
  if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
1897
2669
  if (!["index", "vite.config", "tsconfig"].includes(name)) {
1898
2670
  violations.push({
1899
2671
  ruleId: id,
1900
2672
  severity: "warning",
1901
2673
  message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
1902
- file: path7.relative(cwd, filePath)
2674
+ file: path9.relative(cwd, filePath)
1903
2675
  });
1904
2676
  }
1905
2677
  }
@@ -1915,8 +2687,8 @@ function createStructureRule(id, section, desc, expectedPath) {
1915
2687
  type: "structure",
1916
2688
  description: desc,
1917
2689
  check: (cwd) => {
1918
- const fullPath = path7.join(cwd, expectedPath);
1919
- if (!fs6.existsSync(fullPath)) {
2690
+ const fullPath = path9.join(cwd, expectedPath);
2691
+ if (!fs8.existsSync(fullPath)) {
1920
2692
  return [{
1921
2693
  ruleId: id,
1922
2694
  severity: "error",
@@ -1936,11 +2708,11 @@ function createContentRule(id, section, desc, pattern, type) {
1936
2708
  pattern: new RegExp(escapeRegex(pattern), "i"),
1937
2709
  check: (cwd) => {
1938
2710
  const violations = [];
1939
- const srcDir = path7.join(cwd, "src");
1940
- if (!fs6.existsSync(srcDir)) return violations;
2711
+ const srcDir = path9.join(cwd, "src");
2712
+ if (!fs8.existsSync(srcDir)) return violations;
1941
2713
  const regex = new RegExp(escapeRegex(pattern), "i");
1942
2714
  walkFiles(srcDir, (filePath) => {
1943
- const fileContent = fs6.readFileSync(filePath, "utf-8");
2715
+ const fileContent = fs8.readFileSync(filePath, "utf-8");
1944
2716
  const fileLines = fileContent.split("\n");
1945
2717
  fileLines.forEach((line, idx) => {
1946
2718
  if (regex.test(line)) {
@@ -1948,7 +2720,7 @@ function createContentRule(id, section, desc, pattern, type) {
1948
2720
  ruleId: id,
1949
2721
  severity: type === "banned" ? "error" : "warning",
1950
2722
  message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
1951
- file: path7.relative(cwd, filePath),
2723
+ file: path9.relative(cwd, filePath),
1952
2724
  line: idx + 1
1953
2725
  });
1954
2726
  }
@@ -1960,9 +2732,9 @@ function createContentRule(id, section, desc, pattern, type) {
1960
2732
  };
1961
2733
  }
1962
2734
  function walkFiles(dir, callback) {
1963
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
2735
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
1964
2736
  for (const entry of entries) {
1965
- const fullPath = path7.join(dir, entry.name);
2737
+ const fullPath = path9.join(dir, entry.name);
1966
2738
  if (entry.isDirectory()) {
1967
2739
  if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
1968
2740
  walkFiles(fullPath, callback);
@@ -1977,13 +2749,13 @@ function escapeRegex(str) {
1977
2749
  }
1978
2750
 
1979
2751
  // src/commands/goal.ts
1980
- import { existsSync as existsSync2, mkdirSync, writeFileSync, readFileSync as readFileSync2 } from "fs";
1981
- import { join as join2 } from "path";
1982
- import chalk6 from "chalk";
2752
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
2753
+ import { join as join5 } from "path";
2754
+ import chalk8 from "chalk";
1983
2755
 
1984
2756
  // src/lib/goal-frontmatter.ts
1985
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
1986
- import { join } from "path";
2757
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
2758
+ import { join as join4 } from "path";
1987
2759
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1988
2760
  function parseFrontmatter(content) {
1989
2761
  const m = content.match(FRONTMATTER_RE);
@@ -2015,9 +2787,9 @@ function parseSimpleYaml(yaml) {
2015
2787
  return out;
2016
2788
  }
2017
2789
  function parseGoalFile(filePath) {
2018
- if (!existsSync(filePath)) return null;
2790
+ if (!existsSync3(filePath)) return null;
2019
2791
  try {
2020
- const content = readFileSync(filePath, "utf-8");
2792
+ const content = readFileSync3(filePath, "utf-8");
2021
2793
  const { frontmatter, body } = parseFrontmatter(content);
2022
2794
  return { filePath, frontmatter, body };
2023
2795
  } catch {
@@ -2025,7 +2797,7 @@ function parseGoalFile(filePath) {
2025
2797
  }
2026
2798
  }
2027
2799
  function listGoals(goalsDir) {
2028
- if (!existsSync(goalsDir)) return [];
2800
+ if (!existsSync3(goalsDir)) return [];
2029
2801
  let entries;
2030
2802
  try {
2031
2803
  entries = readdirSync(goalsDir);
@@ -2036,7 +2808,7 @@ function listGoals(goalsDir) {
2036
2808
  for (const name of entries) {
2037
2809
  if (!name.endsWith(".md")) continue;
2038
2810
  if (name === "_meta.md") continue;
2039
- const fp = join(goalsDir, name);
2811
+ const fp = join4(goalsDir, name);
2040
2812
  try {
2041
2813
  if (!statSync(fp).isFile()) continue;
2042
2814
  } catch {
@@ -2102,7 +2874,7 @@ ${body}`;
2102
2874
 
2103
2875
  // src/commands/goal.ts
2104
2876
  var GOALS_DIR = "goals";
2105
- var STATE_DIR = "docs/state";
2877
+ var STATE_DIR2 = "docs/state";
2106
2878
  var SCRIPTS_DIR = "scripts";
2107
2879
  var STATUS_ICON = {
2108
2880
  NOT_STARTED: "\u26AA",
@@ -2128,13 +2900,13 @@ function resolveGoalId(optId, goals) {
2128
2900
  return selectActiveId(goals);
2129
2901
  }
2130
2902
  async function goalList() {
2131
- console.log(chalk6.bold(`
2903
+ console.log(chalk8.bold(`
2132
2904
  ${ko.goal.listTitle}
2133
2905
  `));
2134
2906
  const goals = listGoals(GOALS_DIR);
2135
2907
  if (goals.length === 0) {
2136
- console.log(chalk6.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2137
- console.log(chalk6.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
2908
+ console.log(chalk8.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2909
+ console.log(chalk8.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
2138
2910
  return;
2139
2911
  }
2140
2912
  for (const g of goals) {
@@ -2151,17 +2923,22 @@ ${ko.goal.listTitle}
2151
2923
  const dups = findDuplicateIds(goals);
2152
2924
  if (dups.length > 0) {
2153
2925
  console.log("");
2154
- console.log(chalk6.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
2926
+ console.log(chalk8.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
2155
2927
  }
2156
2928
  }
2157
2929
  async function goalNext() {
2158
- console.log(chalk6.bold(`
2930
+ console.log(chalk8.bold(`
2159
2931
  ${ko.goal.nextTitle}
2160
2932
  `));
2161
2933
  const goals = listGoals(GOALS_DIR);
2934
+ if (goals.length === 0) {
2935
+ console.log(chalk8.yellow(" \u{1F4ED} \uC815\uC758\uB41C goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2936
+ console.log(chalk8.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
2937
+ return;
2938
+ }
2162
2939
  const activeId = selectActiveId(goals);
2163
2940
  if (activeId === null) {
2164
- console.log(chalk6.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
2941
+ console.log(chalk8.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
2165
2942
  return;
2166
2943
  }
2167
2944
  const active = goals.find((g) => g.frontmatter.id === activeId);
@@ -2180,10 +2957,10 @@ ${ko.goal.nextTitle}
2180
2957
  "```",
2181
2958
  ""
2182
2959
  ].join("\n");
2183
- mkdirSync(STATE_DIR, { recursive: true });
2184
- writeFileSync(join2(STATE_DIR, "next-task.md"), text, "utf-8");
2960
+ mkdirSync2(STATE_DIR2, { recursive: true });
2961
+ writeFileSync2(join5(STATE_DIR2, "next-task.md"), text, "utf-8");
2185
2962
  console.log(
2186
- chalk6.green(
2963
+ chalk8.green(
2187
2964
  ` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
2188
2965
  )
2189
2966
  );
@@ -2207,30 +2984,30 @@ var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C
2207
2984
  var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
2208
2985
  var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
2209
2986
  async function goalInit() {
2210
- console.log(chalk6.bold(`
2987
+ console.log(chalk8.bold(`
2211
2988
  ${ko.goal.initTitle}
2212
2989
  `));
2213
2990
  const targets = [
2214
- { path: join2(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
2215
- { path: join2(STATE_DIR, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
2216
- { path: join2(STATE_DIR, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
2217
- { path: join2(STATE_DIR, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
2991
+ { path: join5(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
2992
+ { path: join5(STATE_DIR2, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
2993
+ { path: join5(STATE_DIR2, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
2994
+ { path: join5(STATE_DIR2, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
2218
2995
  ];
2219
- mkdirSync(GOALS_DIR, { recursive: true });
2220
- mkdirSync(STATE_DIR, { recursive: true });
2996
+ mkdirSync2(GOALS_DIR, { recursive: true });
2997
+ mkdirSync2(STATE_DIR2, { recursive: true });
2221
2998
  let created = 0;
2222
2999
  let skipped = 0;
2223
3000
  for (const t2 of targets) {
2224
- if (existsSync2(t2.path)) {
2225
- console.log(chalk6.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
3001
+ if (existsSync4(t2.path)) {
3002
+ console.log(chalk8.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
2226
3003
  skipped++;
2227
3004
  } else {
2228
- writeFileSync(t2.path, t2.content, "utf-8");
2229
- console.log(chalk6.green(` \u2713 created: ${t2.path}`));
3005
+ writeFileSync2(t2.path, t2.content, "utf-8");
3006
+ console.log(chalk8.green(` \u2713 created: ${t2.path}`));
2230
3007
  created++;
2231
3008
  }
2232
3009
  }
2233
- console.log(chalk6.bold(`
3010
+ console.log(chalk8.bold(`
2234
3011
  \u{1F4CA} created=${created} skipped=${skipped}`));
2235
3012
  if (created > 0) {
2236
3013
  printNextStep({
@@ -2241,10 +3018,10 @@ ${ko.goal.initTitle}
2241
3018
  }
2242
3019
  }
2243
3020
  function findGateScript(id) {
2244
- const mjs = join2(SCRIPTS_DIR, `check-goal-${id}.mjs`);
2245
- if (existsSync2(mjs)) return mjs;
2246
- const sh = join2(SCRIPTS_DIR, `check-goal-${id}.sh`);
2247
- if (existsSync2(sh)) return sh;
3021
+ const mjs = join5(SCRIPTS_DIR, `check-goal-${id}.mjs`);
3022
+ if (existsSync4(mjs)) return mjs;
3023
+ const sh = join5(SCRIPTS_DIR, `check-goal-${id}.sh`);
3024
+ if (existsSync4(sh)) return sh;
2248
3025
  return null;
2249
3026
  }
2250
3027
  function runGate(scriptPath) {
@@ -2254,68 +3031,68 @@ function runGate(scriptPath) {
2254
3031
  return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
2255
3032
  }
2256
3033
  async function goalCheck(opts) {
2257
- console.log(chalk6.bold(`
3034
+ console.log(chalk8.bold(`
2258
3035
  ${ko.goal.checkTitle}
2259
3036
  `));
2260
3037
  const goals = listGoals(GOALS_DIR);
2261
3038
  const id = resolveGoalId(opts.id, goals);
2262
3039
  if (id === null) {
2263
3040
  console.log(
2264
- chalk6.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
3041
+ chalk8.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
2265
3042
  );
2266
3043
  process.exitCode = 1;
2267
3044
  return;
2268
3045
  }
2269
3046
  if (!goals.some((g) => g.frontmatter.id === id)) {
2270
- console.log(chalk6.red(` \u274C ${ko.goal.notFound(id)}`));
3047
+ console.log(chalk8.red(` \u274C ${ko.goal.notFound(id)}`));
2271
3048
  process.exitCode = 1;
2272
3049
  return;
2273
3050
  }
2274
3051
  const scriptPath = findGateScript(id);
2275
3052
  if (!scriptPath) {
2276
3053
  console.log(
2277
- chalk6.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
3054
+ chalk8.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
2278
3055
  );
2279
3056
  process.exitCode = 1;
2280
3057
  return;
2281
3058
  }
2282
3059
  const gate2 = runGate(scriptPath);
2283
- console.log(chalk6.dim(` \u25B6 ${gate2.runner} ${scriptPath}
3060
+ console.log(chalk8.dim(` \u25B6 ${gate2.runner} ${scriptPath}
2284
3061
  `));
2285
3062
  if (gate2.out) console.log(gate2.out);
2286
3063
  if (gate2.ok) {
2287
- console.log(chalk6.green(`
3064
+ console.log(chalk8.green(`
2288
3065
  \u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
2289
3066
  } else {
2290
- console.log(chalk6.red(`
3067
+ console.log(chalk8.red(`
2291
3068
  \u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
2292
- if (gate2.err && !gate2.out) console.log(chalk6.dim(gate2.err.slice(0, 500)));
3069
+ if (gate2.err && !gate2.out) console.log(chalk8.dim(gate2.err.slice(0, 500)));
2293
3070
  process.exitCode = 1;
2294
3071
  }
2295
3072
  }
2296
3073
  async function goalDone(opts) {
2297
- console.log(chalk6.bold(`
3074
+ console.log(chalk8.bold(`
2298
3075
  ${ko.goal.doneTitle}
2299
3076
  `));
2300
3077
  const goals = listGoals(GOALS_DIR);
2301
3078
  const id = resolveGoalId(opts.id, goals);
2302
3079
  if (id === null) {
2303
3080
  console.log(
2304
- chalk6.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
3081
+ chalk8.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
2305
3082
  );
2306
3083
  process.exitCode = 1;
2307
3084
  return;
2308
3085
  }
2309
3086
  const target = goals.find((g) => g.frontmatter.id === id);
2310
3087
  if (!target) {
2311
- console.log(chalk6.red(` \u274C ${ko.goal.notFound(id)}`));
3088
+ console.log(chalk8.red(` \u274C ${ko.goal.notFound(id)}`));
2312
3089
  process.exitCode = 1;
2313
3090
  return;
2314
3091
  }
2315
3092
  const scriptPath = findGateScript(id);
2316
3093
  if (!scriptPath) {
2317
3094
  console.log(
2318
- chalk6.red(
3095
+ chalk8.red(
2319
3096
  ` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
2320
3097
  )
2321
3098
  );
@@ -2323,12 +3100,12 @@ ${ko.goal.doneTitle}
2323
3100
  return;
2324
3101
  }
2325
3102
  const gate2 = runGate(scriptPath);
2326
- console.log(chalk6.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
3103
+ console.log(chalk8.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
2327
3104
  `));
2328
3105
  if (gate2.out) console.log(gate2.out);
2329
3106
  if (!gate2.ok) {
2330
3107
  console.log(
2331
- chalk6.red(
3108
+ chalk8.red(
2332
3109
  `
2333
3110
  \u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
2334
3111
  )
@@ -2336,11 +3113,11 @@ ${ko.goal.doneTitle}
2336
3113
  process.exitCode = 1;
2337
3114
  return;
2338
3115
  }
2339
- const content = readFileSync2(target.filePath, "utf-8");
2340
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3116
+ const content = readFileSync4(target.filePath, "utf-8");
3117
+ const today = localDate();
2341
3118
  const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
2342
- writeFileSync(target.filePath, updated, "utf-8");
2343
- console.log(chalk6.green(`
3119
+ writeFileSync2(target.filePath, updated, "utf-8");
3120
+ console.log(chalk8.green(`
2344
3121
  \u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
2345
3122
  printNextStep({
2346
3123
  message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
@@ -2357,22 +3134,22 @@ async function check(opts = {}) {
2357
3134
  return checkRules();
2358
3135
  }
2359
3136
  async function checkRules() {
2360
- console.log(chalk7.bold(`
3137
+ console.log(chalk9.bold(`
2361
3138
  ${ko.check.title}
2362
3139
  `));
2363
3140
  const cwd = process.cwd();
2364
- const rulesPath = path8.join(cwd, "RULES.md");
2365
- if (!fs7.existsSync(rulesPath)) {
2366
- console.log(chalk7.yellow(ko.check.noRules));
2367
- console.log(chalk7.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
3141
+ const rulesPath = path10.join(cwd, "RULES.md");
3142
+ if (!fs9.existsSync(rulesPath)) {
3143
+ console.log(chalk9.yellow(ko.check.noRules));
3144
+ console.log(chalk9.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
2368
3145
  return;
2369
3146
  }
2370
3147
  const rules = parseRules(rulesPath);
2371
- console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
3148
+ console.log(chalk9.dim(` \u{1F4CF} \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${rules.length}\uAC1C \uAC10\uC9C0 (\uB098\uBA38\uC9C0 \uADDC\uCE59\uC740 \uC218\uB3D9/\uB3C4\uAD6C \uD655\uC778)
2372
3149
  `));
2373
3150
  if (rules.length === 0) {
2374
- console.log(chalk7.yellow(ko.check.noAutoRules));
2375
- 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."));
3151
+ console.log(chalk9.yellow(ko.check.noAutoRules));
3152
+ console.log(chalk9.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."));
2376
3153
  return;
2377
3154
  }
2378
3155
  const allViolations = [];
@@ -2380,13 +3157,14 @@ ${ko.check.title}
2380
3157
  for (const rule of rules) {
2381
3158
  const violations = rule.check(cwd);
2382
3159
  if (violations.length === 0) {
2383
- console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
3160
+ const patternHint = rule.type === "content" && rule.pattern ? chalk9.dim(` [\uAC80\uC0AC: ${rule.pattern.source}]`) : "";
3161
+ console.log(chalk9.green(` \u2705 ${rule.id}`) + chalk9.dim(` \u2014 ${rule.description.slice(0, 60)}`) + patternHint);
2384
3162
  passCount++;
2385
3163
  } else {
2386
- console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
3164
+ console.log(chalk9.red(` \u274C ${rule.id}`) + chalk9.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2387
3165
  violations.forEach((v) => {
2388
- const loc = v.file ? chalk7.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
2389
- const icon = v.severity === "error" ? chalk7.red("\u2716") : v.severity === "warning" ? chalk7.yellow("\u26A0") : chalk7.blue("\u2139");
3166
+ const loc = v.file ? chalk9.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
3167
+ const icon = v.severity === "error" ? chalk9.red("\u2716") : v.severity === "warning" ? chalk9.yellow("\u26A0") : chalk9.blue("\u2139");
2390
3168
  console.log(` ${icon} ${v.message}${loc}`);
2391
3169
  });
2392
3170
  allViolations.push(...violations);
@@ -2396,17 +3174,18 @@ ${ko.check.title}
2396
3174
  const errors = allViolations.filter((v) => v.severity === "error").length;
2397
3175
  const warnings = allViolations.filter((v) => v.severity === "warning").length;
2398
3176
  if (allViolations.length === 0) {
2399
- console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
3177
+ console.log(chalk9.green.bold(`\u2705 \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${passCount}\uAC1C \uD1B5\uACFC`));
3178
+ console.log(chalk9.dim(" (RULES.md \uC758 \uB098\uBA38\uC9C0 \uADDC\uCE59\uC740 \uCF54\uB4DC \uC790\uB3D9 \uAC80\uC0AC \uBD88\uAC00 \u2014 \uC9C1\uC811/\uB3C4\uAD6C\uB85C \uD655\uC778\uD558\uC138\uC694.)"));
2400
3179
  printNextStep({
2401
3180
  message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
2402
3181
  command: "vhk \uBCF4\uC548 scan",
2403
3182
  cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
2404
3183
  });
2405
3184
  } else {
2406
- console.log(chalk7.bold(ko.check.summary));
2407
- 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`);
2408
- if (errors > 0) console.log(` ${chalk7.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
2409
- if (warnings > 0) console.log(` ${chalk7.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
3185
+ console.log(chalk9.bold(ko.check.summary));
3186
+ console.log(` \uADDC\uCE59: ${chalk9.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk9.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk9.red(String(allViolations.length))}\uAC74`);
3187
+ if (errors > 0) console.log(` ${chalk9.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
3188
+ if (warnings > 0) console.log(` ${chalk9.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
2410
3189
  printNextStep({
2411
3190
  message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
2412
3191
  command: "vhk \uC810\uAC80",
@@ -2419,36 +3198,36 @@ ${ko.check.title}
2419
3198
  }
2420
3199
 
2421
3200
  // src/commands/secure.ts
2422
- import chalk8 from "chalk";
2423
- import fs8 from "fs";
2424
- import path9 from "path";
3201
+ import chalk10 from "chalk";
3202
+ import fs10 from "fs";
3203
+ import path11 from "path";
2425
3204
  async function secure() {
2426
- console.log(chalk8.bold(`
3205
+ console.log(chalk10.bold(`
2427
3206
  ${ko.secure.title}
2428
3207
  `));
2429
3208
  const cwd = process.cwd();
2430
- const gitignorePath = path9.join(cwd, ".gitignore");
2431
- const hasGitignore = fs8.existsSync(gitignorePath);
3209
+ const gitignorePath = path11.join(cwd, ".gitignore");
3210
+ const hasGitignore = fs10.existsSync(gitignorePath);
2432
3211
  if (!hasGitignore) {
2433
- console.log(chalk8.yellow(` ${ko.secure.noGitignore}`));
2434
- console.log(chalk8.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
3212
+ console.log(chalk10.yellow(` ${ko.secure.noGitignore}`));
3213
+ console.log(chalk10.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
2435
3214
  } else {
2436
- const gitignoreContent = fs8.readFileSync(gitignorePath, "utf-8");
3215
+ const gitignoreContent = fs10.readFileSync(gitignorePath, "utf-8");
2437
3216
  if (!gitignoreContent.includes(".env")) {
2438
- console.log(chalk8.yellow(` ${ko.secure.noEnvInGitignore}`));
2439
- console.log(chalk8.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
3217
+ console.log(chalk10.yellow(` ${ko.secure.noEnvInGitignore}`));
3218
+ console.log(chalk10.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
2440
3219
  }
2441
3220
  }
2442
- console.log(chalk8.dim(` ${ko.secure.scanning}
3221
+ console.log(chalk10.dim(` ${ko.secure.scanning}
2443
3222
  `));
2444
3223
  const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
2445
- 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)`));
3224
+ console.log(chalk10.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
2446
3225
  if (truncated) {
2447
- console.log(chalk8.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.`));
3226
+ console.log(chalk10.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.`));
2448
3227
  }
2449
3228
  console.log("");
2450
3229
  if (findings.length === 0) {
2451
- console.log(chalk8.green.bold(` ${ko.secure.clean}`));
3230
+ console.log(chalk10.green.bold(` ${ko.secure.clean}`));
2452
3231
  printNextStep({
2453
3232
  message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
2454
3233
  command: "vhk \uC815\uB9AC",
@@ -2460,140 +3239,46 @@ ${ko.secure.title}
2460
3239
  const high = findings.filter((f) => f.severity === "high");
2461
3240
  const medium = findings.filter((f) => f.severity === "medium");
2462
3241
  if (critical.length > 0) {
2463
- console.log(chalk8.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
3242
+ console.log(chalk10.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2464
3243
  critical.forEach((f) => {
2465
- console.log(chalk8.red(` \u2716 ${f.patternName}`));
2466
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
3244
+ console.log(chalk10.red(` \u2716 ${f.patternName}`));
3245
+ console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2467
3246
  });
2468
3247
  console.log("");
2469
3248
  }
2470
3249
  if (high.length > 0) {
2471
- console.log(chalk8.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
3250
+ console.log(chalk10.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2472
3251
  high.forEach((f) => {
2473
- console.log(chalk8.yellow(` \u26A0 ${f.patternName}`));
2474
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
3252
+ console.log(chalk10.yellow(` \u26A0 ${f.patternName}`));
3253
+ console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2475
3254
  });
2476
3255
  console.log("");
2477
3256
  }
2478
3257
  if (medium.length > 0) {
2479
- console.log(chalk8.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
3258
+ console.log(chalk10.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2480
3259
  medium.forEach((f) => {
2481
- console.log(chalk8.blue(` \u2139 ${f.patternName}`));
2482
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
3260
+ console.log(chalk10.blue(` \u2139 ${f.patternName}`));
3261
+ console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2483
3262
  });
2484
3263
  console.log("");
2485
3264
  }
2486
- console.log(chalk8.bold(` ${ko.secure.summary}`));
2487
- console.log(` \uCD1D ${chalk8.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
3265
+ console.log(chalk10.bold(` ${ko.secure.summary}`));
3266
+ console.log(` \uCD1D ${chalk10.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
2488
3267
  console.log("");
2489
- console.log(chalk8.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
2490
- 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"));
2491
- console.log(chalk8.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
2492
- console.log(chalk8.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
3268
+ console.log(chalk10.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
3269
+ console.log(chalk10.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
3270
+ console.log(chalk10.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
3271
+ console.log(chalk10.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
2493
3272
  if (critical.length > 0 || high.length > 0) {
2494
3273
  process.exitCode = 1;
2495
3274
  }
2496
3275
  }
2497
3276
 
2498
3277
  // src/commands/doctor.ts
2499
- import chalk9 from "chalk";
2500
- import fs10 from "fs";
2501
- import path11 from "path";
3278
+ import chalk11 from "chalk";
3279
+ import fs11 from "fs";
3280
+ import path12 from "path";
2502
3281
  import { fileURLToPath } from "url";
2503
-
2504
- // src/lib/drift.ts
2505
- import fs9 from "fs";
2506
- import path10 from "path";
2507
-
2508
- // src/lib/git-repo.ts
2509
- import { execFileSync } from "child_process";
2510
- function getGitRoot(cwd = process.cwd()) {
2511
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
2512
- encoding: "utf-8",
2513
- cwd,
2514
- stdio: ["pipe", "pipe", "pipe"]
2515
- }).trim();
2516
- }
2517
- function gitOut(args, cwd) {
2518
- return execFileSync("git", args, {
2519
- encoding: "utf-8",
2520
- cwd,
2521
- stdio: ["pipe", "pipe", "pipe"]
2522
- });
2523
- }
2524
- function gitRun(args, cwd) {
2525
- execFileSync("git", args, { stdio: "pipe", cwd });
2526
- }
2527
- function getExecErrorMessage(err) {
2528
- if (err && typeof err === "object" && "stderr" in err) {
2529
- const stderr = err.stderr;
2530
- if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
2531
- if (typeof stderr === "string") return stderr.trim();
2532
- }
2533
- return err instanceof Error ? err.message : String(err);
2534
- }
2535
- function hasGitRemote(cwd) {
2536
- try {
2537
- return gitOut(["remote"], cwd).trim().length > 0;
2538
- } catch {
2539
- return false;
2540
- }
2541
- }
2542
- function countLocalCommits(cwd) {
2543
- try {
2544
- const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
2545
- return parseInt(out, 10) || 0;
2546
- } catch {
2547
- return 0;
2548
- }
2549
- }
2550
-
2551
- // src/lib/drift.ts
2552
- function normalizeForCompare(s) {
2553
- return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
2554
- }
2555
- function checkRuleDrift(rootDir) {
2556
- const rulesPath = path10.join(rootDir, "RULES.md");
2557
- if (!fs9.existsSync(rulesPath)) return { checked: false, results: [] };
2558
- const rulesContent = fs9.readFileSync(rulesPath, "utf-8");
2559
- const sections = parseRulesMd(rulesContent);
2560
- const projectName = deriveProjectName(rulesContent);
2561
- const results = [];
2562
- for (const target of SYNC_TARGETS) {
2563
- const fullPath = path10.join(rootDir, target.path);
2564
- if (!fs9.existsSync(fullPath)) {
2565
- results.push({ path: target.path, status: "missing" });
2566
- continue;
2567
- }
2568
- const expected = normalizeForCompare(target.generate(sections, projectName));
2569
- const actual = normalizeForCompare(fs9.readFileSync(fullPath, "utf-8"));
2570
- results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
2571
- }
2572
- return { checked: true, results };
2573
- }
2574
- var CONTEXT_GIT_MARKER = "vhk-context-git";
2575
- var CONTEXT_PATH = ".vhk/context.md";
2576
- function extractContextSha(content) {
2577
- const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
2578
- return m ? m[1] : null;
2579
- }
2580
- function checkContextDrift(rootDir) {
2581
- const ctxPath = path10.join(rootDir, CONTEXT_PATH);
2582
- if (!fs9.existsSync(ctxPath)) return { checked: false, stale: false };
2583
- const generatedSha = extractContextSha(fs9.readFileSync(ctxPath, "utf-8"));
2584
- if (!generatedSha) return { checked: false, stale: false };
2585
- let currentSha;
2586
- try {
2587
- currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
2588
- } catch {
2589
- return { checked: false, stale: false };
2590
- }
2591
- if (!currentSha) return { checked: false, stale: false };
2592
- const stale = !(currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha));
2593
- return { checked: true, stale, generatedSha, currentSha };
2594
- }
2595
-
2596
- // src/commands/doctor.ts
2597
3282
  function checkCommand(name, command, hint) {
2598
3283
  const result = safeExecFile(command, ["--version"]);
2599
3284
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2601,14 +3286,14 @@ function checkCommand(name, command, hint) {
2601
3286
  return { name, command, version, ok: true, hint };
2602
3287
  }
2603
3288
  function getVhkVersion2() {
2604
- const dir = path11.dirname(fileURLToPath(import.meta.url));
3289
+ const dir = path12.dirname(fileURLToPath(import.meta.url));
2605
3290
  const candidates = [
2606
- path11.join(dir, "../package.json"),
2607
- path11.join(dir, "../../package.json")
3291
+ path12.join(dir, "../package.json"),
3292
+ path12.join(dir, "../../package.json")
2608
3293
  ];
2609
3294
  for (const pkgPath of candidates) {
2610
3295
  try {
2611
- if (fs10.existsSync(pkgPath)) {
3296
+ if (fs11.existsSync(pkgPath)) {
2612
3297
  const pkg = readJsonFile(pkgPath);
2613
3298
  return pkg.version;
2614
3299
  }
@@ -2636,7 +3321,7 @@ function compareSemver(a, b) {
2636
3321
  return a3 - b3;
2637
3322
  }
2638
3323
  async function doctor() {
2639
- console.log(chalk9.bold(`
3324
+ console.log(chalk11.bold(`
2640
3325
  ${ko.doctor.title}
2641
3326
  `));
2642
3327
  const checks = [
@@ -2648,30 +3333,30 @@ ${ko.doctor.title}
2648
3333
  let allOk = true;
2649
3334
  for (const check2 of checks) {
2650
3335
  if (check2.ok) {
2651
- console.log(chalk9.green(` \u2705 ${check2.name}`) + chalk9.dim(` \u2014 ${check2.version}`));
3336
+ console.log(chalk11.green(` \u2705 ${check2.name}`) + chalk11.dim(` \u2014 ${check2.version}`));
2652
3337
  } else {
2653
- console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2654
- console.log(chalk9.dim(` \u2192 ${check2.hint}`));
3338
+ console.log(chalk11.red(` \u274C ${check2.name} \uC5C6\uC74C`));
3339
+ console.log(chalk11.dim(` \u2192 ${check2.hint}`));
2655
3340
  allOk = false;
2656
3341
  }
2657
3342
  }
2658
3343
  console.log("");
2659
3344
  const vhkVersion = getVhkVersion2();
2660
3345
  if (vhkVersion) {
2661
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
3346
+ console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(` \u2014 v${vhkVersion}`));
2662
3347
  } else {
2663
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(" \u2014 \uC124\uCE58\uB428"));
3348
+ console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(" \u2014 \uC124\uCE58\uB428"));
2664
3349
  }
2665
3350
  if (vhkVersion) {
2666
3351
  const latest = fetchLatestNpmVersion("@byh3071/vhk");
2667
3352
  if (latest && compareSemver(latest, vhkVersion) > 0) {
2668
- console.log(chalk9.yellow(` ${ko.doctor.updateAvailable(latest)}`));
3353
+ console.log(chalk11.yellow(` ${ko.doctor.updateAvailable(latest)}`));
2669
3354
  } else if (latest) {
2670
- console.log(chalk9.dim(` ${ko.doctor.updateCurrent}`));
3355
+ console.log(chalk11.dim(` ${ko.doctor.updateCurrent}`));
2671
3356
  }
2672
3357
  }
2673
3358
  console.log("");
2674
- console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
3359
+ console.log(chalk11.bold(` ${ko.doctor.projectFiles}`));
2675
3360
  const cwd = process.cwd();
2676
3361
  const projectFiles = [
2677
3362
  { name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
@@ -2681,49 +3366,51 @@ ${ko.doctor.title}
2681
3366
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2682
3367
  ];
2683
3368
  for (const file of projectFiles) {
2684
- const exists = fs10.existsSync(path11.join(cwd, file.name));
3369
+ const exists = fs11.existsSync(path12.join(cwd, file.name));
2685
3370
  if (exists) {
2686
- console.log(chalk9.green(` \u2705 ${file.name}`));
3371
+ console.log(chalk11.green(` \u2705 ${file.name}`));
2687
3372
  if (file.name === ".env") {
2688
- const gitignorePath = path11.join(cwd, ".gitignore");
2689
- if (fs10.existsSync(gitignorePath)) {
2690
- const gitignore = fs10.readFileSync(gitignorePath, "utf-8");
3373
+ const gitignorePath = path12.join(cwd, ".gitignore");
3374
+ if (fs11.existsSync(gitignorePath)) {
3375
+ const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
2691
3376
  if (!gitignore.includes(".env")) {
2692
- console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
3377
+ console.log(chalk11.yellow(` ${ko.doctor.envNotIgnored}`));
2693
3378
  }
2694
3379
  }
2695
3380
  }
3381
+ } else if (file.name === ".env" && fs11.existsSync(path12.join(cwd, ".env.local"))) {
3382
+ console.log(chalk11.green(" \u2705 .env.local") + chalk11.dim(" \u2014 \uB85C\uCEEC env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
2696
3383
  } else {
2697
- console.log(chalk9.dim(` \u2B1A ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
3384
+ console.log(chalk11.dim(` \u2B1A ${file.name}`) + chalk11.dim(` \u2014 ${file.hint}`));
2698
3385
  }
2699
3386
  }
2700
3387
  console.log("");
2701
- console.log(chalk9.bold(` ${ko.doctor.driftTitle}`));
3388
+ console.log(chalk11.bold(` ${ko.doctor.driftTitle}`));
2702
3389
  const ruleDrift = checkRuleDrift(cwd);
2703
3390
  if (!ruleDrift.checked) {
2704
- console.log(chalk9.dim(` ${ko.doctor.driftNoRules}`));
3391
+ console.log(chalk11.dim(` ${ko.doctor.driftNoRules}`));
2705
3392
  } else {
2706
3393
  const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
2707
3394
  if (drifted.length === 0) {
2708
- console.log(chalk9.green(` ${ko.doctor.driftRuleClean}`));
3395
+ console.log(chalk11.green(` ${ko.doctor.driftRuleClean}`));
2709
3396
  } else {
2710
- console.log(chalk9.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
3397
+ console.log(chalk11.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
2711
3398
  }
2712
3399
  }
2713
3400
  const ctxDrift = checkContextDrift(cwd);
2714
3401
  if (ctxDrift.checked && ctxDrift.stale) {
2715
- console.log(chalk9.yellow(` ${ko.doctor.driftContextWarn}`));
3402
+ console.log(chalk11.yellow(` ${ko.doctor.driftContextWarn}`));
2716
3403
  }
2717
3404
  console.log("");
2718
3405
  if (allOk) {
2719
- console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
3406
+ console.log(chalk11.green.bold(` ${ko.doctor.allOk}`));
2720
3407
  printNextStep({
2721
3408
  message: ko.doctor.nextOkMessage,
2722
3409
  command: "vhk \uC2DC\uC791",
2723
3410
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
2724
3411
  });
2725
3412
  } else {
2726
- console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
3413
+ console.log(chalk11.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2727
3414
  printNextStep({
2728
3415
  message: ko.doctor.nextRetryMessage,
2729
3416
  command: "vhk doctor",
@@ -2734,10 +3421,10 @@ ${ko.doctor.title}
2734
3421
  }
2735
3422
 
2736
3423
  // src/commands/ship.ts
2737
- import chalk10 from "chalk";
2738
- import inquirer4 from "inquirer";
2739
- import fs11 from "fs";
2740
- import path12 from "path";
3424
+ import chalk12 from "chalk";
3425
+ import inquirer5 from "inquirer";
3426
+ import fs12 from "fs";
3427
+ import path13 from "path";
2741
3428
  var CHECKLIST = [
2742
3429
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
2743
3430
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -2750,9 +3437,9 @@ function sanitizeVersion(version) {
2750
3437
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2751
3438
  }
2752
3439
  function updateChangelogUnreleased(cwd, version, date) {
2753
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
2754
- if (!fs11.existsSync(changelogPath)) return { status: "missing" };
2755
- const content = fs11.readFileSync(changelogPath, "utf-8");
3440
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
3441
+ if (!fs12.existsSync(changelogPath)) return { status: "missing" };
3442
+ const content = fs12.readFileSync(changelogPath, "utf-8");
2756
3443
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2757
3444
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2758
3445
  const blankUnreleased = [
@@ -2769,35 +3456,36 @@ function updateChangelogUnreleased(cwd, version, date) {
2769
3456
  `## [${version}] \u2014 ${date}`
2770
3457
  ].join("\n");
2771
3458
  const updated = content.replace(unreleasedHeading, blankUnreleased);
2772
- fs11.writeFileSync(changelogPath, updated, "utf-8");
3459
+ fs12.writeFileSync(changelogPath, updated, "utf-8");
2773
3460
  return { status: "updated", version };
2774
3461
  }
2775
3462
  async function ship() {
2776
- console.log(chalk10.bold(`
3463
+ if (!ensureNotHardStopped("ship")) return;
3464
+ console.log(chalk12.bold(`
2777
3465
  ${ko.ship.title}
2778
3466
  `));
2779
3467
  const cwd = process.cwd();
2780
- console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
3468
+ console.log(chalk12.cyan.bold(` ${ko.ship.checklist}
2781
3469
  `));
2782
- const { passed } = await inquirer4.prompt([{
3470
+ const { passed } = await inquirer5.prompt([{
2783
3471
  type: "checkbox",
2784
3472
  name: "passed",
2785
3473
  message: ko.ship.checkboxPrompt,
2786
3474
  choices: CHECKLIST.map((c) => ({
2787
- name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
3475
+ name: `${ko.ship[c.questionKey]} ${chalk12.dim(`(${ko.ship[c.hintKey]})`)}`,
2788
3476
  value: c.id
2789
3477
  }))
2790
3478
  }]);
2791
3479
  const allPassed = passed.length === CHECKLIST.length;
2792
3480
  const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
2793
3481
  if (!allPassed) {
2794
- console.log(chalk10.yellow(`
3482
+ console.log(chalk12.yellow(`
2795
3483
  ${ko.ship.incompleteHeader}`));
2796
3484
  skipped.forEach((s) => {
2797
- console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2798
- console.log(chalk10.dim(` \u2192 ${ko.ship[s.hintKey]}`));
3485
+ console.log(chalk12.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
3486
+ console.log(chalk12.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2799
3487
  });
2800
- const { proceed } = await inquirer4.prompt([{
3488
+ const { proceed } = await inquirer5.prompt([{
2801
3489
  type: "confirm",
2802
3490
  name: "proceed",
2803
3491
  message: ko.ship.proceedConfirm,
@@ -2812,26 +3500,26 @@ ${ko.ship.title}
2812
3500
  return;
2813
3501
  }
2814
3502
  } else {
2815
- console.log(chalk10.green(`
3503
+ console.log(chalk12.green(`
2816
3504
  ${ko.ship.allPassed}
2817
3505
  `));
2818
3506
  }
2819
- console.log(chalk10.cyan.bold(` ${ko.ship.retro}
3507
+ console.log(chalk12.cyan.bold(` ${ko.ship.retro}
2820
3508
  `));
2821
- console.log(chalk10.dim(` ${ko.ship.versionHint}`));
2822
- const retro = await inquirer4.prompt([
3509
+ console.log(chalk12.dim(` ${ko.ship.versionHint}`));
3510
+ const retro = await inquirer5.prompt([
2823
3511
  { type: "input", name: "version", message: ko.ship.versionPrompt },
2824
3512
  { type: "input", name: "whatWentWell", message: ko.ship.questionWell },
2825
3513
  { type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
2826
3514
  { type: "input", name: "learned", message: ko.ship.questionLearned },
2827
3515
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2828
3516
  ]);
2829
- const buildLogDir = path12.join(cwd, "docs", "build-log");
2830
- if (!fs11.existsSync(buildLogDir)) fs11.mkdirSync(buildLogDir, { recursive: true });
2831
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3517
+ const buildLogDir = path13.join(cwd, "docs", "build-log");
3518
+ if (!fs12.existsSync(buildLogDir)) fs12.mkdirSync(buildLogDir, { recursive: true });
3519
+ const today = localDate();
2832
3520
  const versionSlug = sanitizeVersion(retro.version);
2833
3521
  const fileName = `${today}-v${versionSlug}.md`;
2834
- const filePath = path12.join(buildLogDir, fileName);
3522
+ const filePath = path13.join(buildLogDir, fileName);
2835
3523
  const content = [
2836
3524
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
2837
3525
  "",
@@ -2860,9 +3548,9 @@ ${ko.ship.title}
2860
3548
  "---",
2861
3549
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2862
3550
  ].join("\n");
2863
- fs11.writeFileSync(filePath, content, "utf-8");
2864
- console.log(chalk10.green(`
2865
- ${ko.ship.buildLogDone(path12.relative(cwd, filePath))}`));
3551
+ fs12.writeFileSync(filePath, content, "utf-8");
3552
+ console.log(chalk12.green(`
3553
+ ${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
2866
3554
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
2867
3555
  if (changelogResult.status === "updated") {
2868
3556
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -2881,9 +3569,9 @@ ${ko.ship.title}
2881
3569
 
2882
3570
  // src/commands/save.ts
2883
3571
  import { execFileSync as execFileSync2 } from "child_process";
2884
- import chalk11 from "chalk";
3572
+ import chalk13 from "chalk";
2885
3573
  import ora from "ora";
2886
- import inquirer5 from "inquirer";
3574
+ import inquirer6 from "inquirer";
2887
3575
 
2888
3576
  // src/lib/git-porcelain.ts
2889
3577
  function normalizePorcelain(raw) {
@@ -2909,54 +3597,54 @@ function statusIcon(code) {
2909
3597
  return "\u{1F4C4}";
2910
3598
  }
2911
3599
  async function save() {
2912
- console.log(chalk11.bold(`
3600
+ console.log(chalk13.bold(`
2913
3601
  \u{1F4BE} ${t("save.title")}`));
2914
- console.log(chalk11.gray("\u2500".repeat(40)));
3602
+ console.log(chalk13.gray("\u2500".repeat(40)));
2915
3603
  let gitRoot;
2916
3604
  try {
2917
3605
  execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
2918
3606
  gitRoot = getGitRoot();
2919
3607
  } catch {
2920
- console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
3608
+ console.log(chalk13.red(`\u274C ${t("save.notGitRepo")}`));
2921
3609
  return;
2922
3610
  }
2923
- console.log(chalk11.cyan(`
3611
+ console.log(chalk13.cyan(`
2924
3612
  \u{1F512} ${t("save.securityWarnHeader")}`));
2925
3613
  printSecurityWarnings(gitRoot);
2926
3614
  const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
2927
3615
  if (severe.length > 0) {
2928
- console.log(chalk11.red(`
3616
+ console.log(chalk13.red(`
2929
3617
  \u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
2930
3618
  severe.slice(0, 5).forEach((f) => {
2931
- console.log(chalk11.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
3619
+ console.log(chalk13.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
2932
3620
  });
2933
3621
  if (severe.length > 5) {
2934
- console.log(chalk11.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
3622
+ console.log(chalk13.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
2935
3623
  }
2936
- const { proceed } = await inquirer5.prompt([{
3624
+ const { proceed } = await inquirer6.prompt([{
2937
3625
  type: "confirm",
2938
3626
  name: "proceed",
2939
3627
  message: t("save.secretsConfirm"),
2940
3628
  default: false
2941
3629
  }]);
2942
3630
  if (!proceed) {
2943
- console.log(chalk11.gray(t("save.cancelled")));
3631
+ console.log(chalk13.gray(t("save.cancelled")));
2944
3632
  return;
2945
3633
  }
2946
3634
  }
2947
3635
  const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
2948
3636
  if (lines.length === 0) {
2949
- console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
3637
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
2950
3638
  return;
2951
3639
  }
2952
- console.log(chalk11.cyan(`
3640
+ console.log(chalk13.cyan(`
2953
3641
  \u{1F4CB} ${t("save.filesHeader", lines.length)}`));
2954
3642
  lines.forEach((line) => {
2955
3643
  const code = line.substring(0, 2);
2956
3644
  const name = line.substring(3);
2957
3645
  console.log(` ${statusIcon(code)} ${name}`);
2958
3646
  });
2959
- const { message } = await inquirer5.prompt([{
3647
+ const { message } = await inquirer6.prompt([{
2960
3648
  type: "input",
2961
3649
  name: "message",
2962
3650
  message: t("save.commitMessage"),
@@ -2971,21 +3659,21 @@ async function save() {
2971
3659
  spinner.text = t("save.pushing");
2972
3660
  if (!hasGitRemote(gitRoot)) {
2973
3661
  spinner.succeed(t("save.successLocal"));
2974
- console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
3662
+ console.log(chalk13.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
2975
3663
  } else {
2976
3664
  try {
2977
3665
  gitRun(["push"], gitRoot);
2978
3666
  spinner.succeed(t("save.successWithPush"));
2979
3667
  } catch (pushErr) {
2980
3668
  spinner.fail(t("save.pushFailed"));
2981
- console.log(chalk11.red(getExecErrorMessage(pushErr)));
2982
- console.log(chalk11.yellow(`
3669
+ console.log(chalk13.red(getExecErrorMessage(pushErr)));
3670
+ console.log(chalk13.yellow(`
2983
3671
  \u{1F4A1} ${t("save.commitOkPushFailed")}`));
2984
3672
  process.exitCode = 1;
2985
3673
  }
2986
3674
  }
2987
3675
  if (process.exitCode !== 1) {
2988
- console.log(chalk11.green(`
3676
+ console.log(chalk13.green(`
2989
3677
  \u2705 ${t("save.done", lines.length)}`));
2990
3678
  printNextStep({
2991
3679
  message: t("save.nextOkMessage"),
@@ -2993,7 +3681,7 @@ async function save() {
2993
3681
  cursorHint: t("save.nextOkCursor")
2994
3682
  });
2995
3683
  } else {
2996
- console.log(chalk11.green(`
3684
+ console.log(chalk13.green(`
2997
3685
  \u2705 ${t("save.doneLocalOnly", lines.length)}`));
2998
3686
  printNextStep({
2999
3687
  message: t("save.nextPushFailMessage"),
@@ -3003,12 +3691,12 @@ async function save() {
3003
3691
  }
3004
3692
  } catch (err) {
3005
3693
  spinner.fail(t("save.failed"));
3006
- console.log(chalk11.red(getExecErrorMessage(err)));
3694
+ console.log(chalk13.red(getExecErrorMessage(err)));
3007
3695
  if (didAdd) {
3008
3696
  try {
3009
3697
  const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
3010
3698
  if (staged) {
3011
- console.log(chalk11.yellow(`
3699
+ console.log(chalk13.yellow(`
3012
3700
  \u{1F4A1} ${t("save.stagedAfterFail")}`));
3013
3701
  }
3014
3702
  } catch {
@@ -3020,8 +3708,8 @@ async function save() {
3020
3708
 
3021
3709
  // src/commands/undo.ts
3022
3710
  import { execFileSync as execFileSync3 } from "child_process";
3023
- import chalk12 from "chalk";
3024
- import inquirer6 from "inquirer";
3711
+ import chalk14 from "chalk";
3712
+ import inquirer7 from "inquirer";
3025
3713
  function parseRecentCommits(logOutput) {
3026
3714
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3027
3715
  }
@@ -3044,36 +3732,36 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
3044
3732
  return false;
3045
3733
  }
3046
3734
  async function undo() {
3047
- console.log(chalk12.bold(`
3735
+ console.log(chalk14.bold(`
3048
3736
  \u23EA ${t("undo.title")}`));
3049
- console.log(chalk12.gray("\u2500".repeat(40)));
3737
+ console.log(chalk14.gray("\u2500".repeat(40)));
3050
3738
  let gitRoot;
3051
3739
  try {
3052
3740
  execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3053
3741
  gitRoot = getGitRoot();
3054
3742
  } catch {
3055
- console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
3743
+ console.log(chalk14.red(`\u274C ${t("undo.notGitRepo")}`));
3056
3744
  return;
3057
3745
  }
3058
3746
  let logOutput;
3059
3747
  try {
3060
3748
  logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
3061
3749
  } catch {
3062
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3750
+ console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3063
3751
  return;
3064
3752
  }
3065
3753
  const commits = parseRecentCommits(logOutput);
3066
3754
  if (commits.length === 0) {
3067
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3755
+ console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3068
3756
  return;
3069
3757
  }
3070
- console.log(chalk12.cyan(`
3758
+ console.log(chalk14.cyan(`
3071
3759
  ${t("undo.recentHeader")}`));
3072
3760
  commits.forEach((c, i) => {
3073
3761
  console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
3074
3762
  });
3075
3763
  const maxUndo = commits.length;
3076
- const { count } = await inquirer6.prompt([{
3764
+ const { count } = await inquirer7.prompt([{
3077
3765
  type: "number",
3078
3766
  name: "count",
3079
3767
  message: t("undo.howMany"),
@@ -3084,7 +3772,7 @@ ${t("undo.recentHeader")}`));
3084
3772
  const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
3085
3773
  const headCount = countLocalCommits(gitRoot);
3086
3774
  if (undoCount >= headCount) {
3087
- console.log(chalk12.yellow(`
3775
+ console.log(chalk14.yellow(`
3088
3776
  \u{1F4ED} ${t("undo.rootCommit")}`));
3089
3777
  return;
3090
3778
  }
@@ -3093,30 +3781,30 @@ ${t("undo.recentHeader")}`));
3093
3781
  const risky = isUndoRisky(undoCount, unpushed, remote);
3094
3782
  if (risky) {
3095
3783
  if (unpushed < 0) {
3096
- console.log(chalk12.red(`
3784
+ console.log(chalk14.red(`
3097
3785
  \u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
3098
3786
  } else {
3099
- console.log(chalk12.red(`
3787
+ console.log(chalk14.red(`
3100
3788
  \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
3101
3789
  }
3102
3790
  }
3103
- const { confirm } = await inquirer6.prompt([{
3791
+ const { confirm } = await inquirer7.prompt([{
3104
3792
  type: "confirm",
3105
3793
  name: "confirm",
3106
3794
  message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
3107
3795
  default: false
3108
3796
  }]);
3109
3797
  if (!confirm) {
3110
- console.log(chalk12.gray(t("undo.cancelled")));
3798
+ console.log(chalk14.gray(t("undo.cancelled")));
3111
3799
  return;
3112
3800
  }
3113
3801
  try {
3114
3802
  gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
3115
- console.log(chalk12.green(`
3803
+ console.log(chalk14.green(`
3116
3804
  \u2705 ${t("undo.success")}`));
3117
- console.log(chalk12.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3805
+ console.log(chalk14.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3118
3806
  if (risky) {
3119
- console.log(chalk12.yellow(`
3807
+ console.log(chalk14.yellow(`
3120
3808
  \u{1F4A1} ${t("undo.forcePushHint")}`));
3121
3809
  }
3122
3810
  printNextStep({
@@ -3125,18 +3813,73 @@ ${t("undo.recentHeader")}`));
3125
3813
  cursorHint: t("undo.nextCursor")
3126
3814
  });
3127
3815
  } catch (err) {
3128
- console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3816
+ console.log(chalk14.red(`\u274C ${t("undo.failed")}`));
3129
3817
  const msg = err instanceof Error ? err.message : String(err);
3130
- console.log(chalk12.red(msg));
3818
+ console.log(chalk14.red(msg));
3819
+ process.exitCode = 1;
3820
+ }
3821
+ }
3822
+
3823
+ // src/commands/restore.ts
3824
+ import chalk15 from "chalk";
3825
+ import inquirer8 from "inquirer";
3826
+ async function restore(id) {
3827
+ console.log(chalk15.bold(`
3828
+ ${ko.restore.title}`));
3829
+ console.log(chalk15.gray("\u2500".repeat(40)));
3830
+ console.log(chalk15.dim(` ${ko.restore.notGitNote}`));
3831
+ const cwd = process.cwd();
3832
+ const backups = listBackups(cwd);
3833
+ if (backups.length === 0) {
3834
+ console.log(chalk15.yellow(`
3835
+ ${ko.restore.noBackups}`));
3836
+ return;
3837
+ }
3838
+ let targetId = id;
3839
+ if (!targetId) {
3840
+ if (!process.stdout.isTTY) {
3841
+ console.log(chalk15.cyan(`
3842
+ ${ko.restore.listHeader}`));
3843
+ for (const b of backups) console.log(` ${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`);
3844
+ console.log(chalk15.yellow(`
3845
+ ${ko.restore.nonTtyHint}`));
3846
+ return;
3847
+ }
3848
+ const { picked } = await inquirer8.prompt([
3849
+ {
3850
+ type: "list",
3851
+ name: "picked",
3852
+ message: ko.restore.selectPrompt,
3853
+ choices: backups.map((b) => ({
3854
+ name: `${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`,
3855
+ value: b.id
3856
+ }))
3857
+ }
3858
+ ]);
3859
+ targetId = picked;
3860
+ }
3861
+ try {
3862
+ const restored = restoreBackup(targetId, cwd);
3863
+ console.log(chalk15.green(`
3864
+ ${ko.restore.restored(restored.length, targetId)}`));
3865
+ for (const r of restored) console.log(chalk15.gray(` ${r}`));
3866
+ printNextStep({
3867
+ message: "\uBC31\uC5C5 \uBCF5\uC6D0 \uC644\uB8CC! \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.",
3868
+ command: "vhk diff",
3869
+ cursorHint: "\uBCC0\uACBD\uC0AC\uD56D \uBCF4\uC5EC\uC918"
3870
+ });
3871
+ } catch {
3872
+ console.log(chalk15.red(`
3873
+ ${ko.restore.notFound(targetId)}`));
3131
3874
  process.exitCode = 1;
3132
3875
  }
3133
3876
  }
3134
3877
 
3135
3878
  // src/commands/status.ts
3136
3879
  import { execFileSync as execFileSync4 } from "child_process";
3137
- import fs12 from "fs";
3138
- import path13 from "path";
3139
- import chalk13 from "chalk";
3880
+ import fs13 from "fs";
3881
+ import path14 from "path";
3882
+ import chalk16 from "chalk";
3140
3883
  function countFileChanges(porcelain) {
3141
3884
  const lines = porcelain.split("\n").filter(Boolean);
3142
3885
  let staged = 0;
@@ -3174,8 +3917,8 @@ function parseRecentCommitLines(logOutput) {
3174
3917
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3175
3918
  }
3176
3919
  function readProjectPackage(cwd = process.cwd()) {
3177
- const pkgPath = path13.join(cwd, "package.json");
3178
- if (!fs12.existsSync(pkgPath)) return null;
3920
+ const pkgPath = path14.join(cwd, "package.json");
3921
+ if (!fs13.existsSync(pkgPath)) return null;
3179
3922
  try {
3180
3923
  const pkg = readJsonFile(pkgPath);
3181
3924
  if (!pkg.name && !pkg.version) return null;
@@ -3187,6 +3930,21 @@ function readProjectPackage(cwd = process.cwd()) {
3187
3930
  return null;
3188
3931
  }
3189
3932
  }
3933
+ function selectStatusNextStep(hasChanges) {
3934
+ if (hasChanges) {
3935
+ return {
3936
+ message: t("status.nextWithChangesMessage"),
3937
+ command: "vhk diff",
3938
+ cursorHint: t("status.nextWithChangesCursor"),
3939
+ alternative: t("status.nextWithChangesAlt")
3940
+ };
3941
+ }
3942
+ return {
3943
+ message: t("status.nextCleanMessage"),
3944
+ command: "vhk goal next",
3945
+ cursorHint: t("status.nextCleanCursor")
3946
+ };
3947
+ }
3190
3948
  function getSyncCounts(gitRoot) {
3191
3949
  try {
3192
3950
  const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
@@ -3196,15 +3954,15 @@ function getSyncCounts(gitRoot) {
3196
3954
  }
3197
3955
  }
3198
3956
  async function status() {
3199
- console.log(chalk13.bold(`
3957
+ console.log(chalk16.bold(`
3200
3958
  \u{1F4CA} ${t("status.title")}`));
3201
- console.log(chalk13.gray("\u2500".repeat(40)));
3959
+ console.log(chalk16.gray("\u2500".repeat(40)));
3202
3960
  let gitRoot;
3203
3961
  try {
3204
3962
  execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3205
3963
  gitRoot = getGitRoot();
3206
3964
  } catch {
3207
- console.log(chalk13.red(`\u274C ${t("status.notGitRepo")}`));
3965
+ console.log(chalk16.red(`\u274C ${t("status.notGitRepo")}`));
3208
3966
  return;
3209
3967
  }
3210
3968
  let branch;
@@ -3223,48 +3981,36 @@ async function status() {
3223
3981
  commits = [];
3224
3982
  }
3225
3983
  const pkg = readProjectPackage();
3226
- console.log(chalk13.cyan(`
3227
- \u{1F33F} ${t("status.branch")}`) + chalk13.white(` ${branch}`));
3984
+ console.log(chalk16.cyan(`
3985
+ \u{1F33F} ${t("status.branch")}`) + chalk16.white(` ${branch}`));
3228
3986
  console.log(
3229
- chalk13.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk13.white(
3987
+ chalk16.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk16.white(
3230
3988
  ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3231
3989
  )
3232
3990
  );
3233
- console.log(chalk13.cyan(`
3234
- \u{1F4CB} ${t("status.recentCommits")}`));
3991
+ console.log(chalk16.cyan(`
3992
+ \u{1F4CB} ${t("status.recentCommits", commits.length)}`));
3235
3993
  if (commits.length === 0) {
3236
- console.log(chalk13.dim(` ${t("status.noCommits")}`));
3994
+ console.log(chalk16.dim(` ${t("status.noCommits")}`));
3237
3995
  } else {
3238
- commits.forEach((c) => console.log(` ${chalk13.dim("\u2022")} ${c}`));
3996
+ commits.forEach((c) => console.log(` ${chalk16.dim("\u2022")} ${c}`));
3239
3997
  }
3240
3998
  console.log(
3241
- chalk13.cyan(`
3242
- \u{1F504} ${t("status.remote")}`) + chalk13.white(` ${formatSyncLabel(sync2)}`)
3999
+ chalk16.cyan(`
4000
+ \u{1F504} ${t("status.remote")}`) + chalk16.white(` ${formatSyncLabel(sync2)}`)
3243
4001
  );
3244
- console.log(chalk13.gray("\n" + "\u2500".repeat(40)));
4002
+ console.log(chalk16.gray("\n" + "\u2500".repeat(40)));
3245
4003
  if (pkg) {
3246
- console.log(chalk13.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk13.white(` ${pkg.name} v${pkg.version}`));
4004
+ console.log(chalk16.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk16.white(` ${pkg.name} v${pkg.version}`));
3247
4005
  } else {
3248
- console.log(chalk13.dim(`\u{1F4E6} ${t("status.noPackage")}`));
4006
+ console.log(chalk16.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3249
4007
  }
3250
4008
  const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
3251
- if (hasChanges) {
3252
- printNextStep({
3253
- message: t("status.nextWithChangesMessage"),
3254
- command: "vhk save",
3255
- cursorHint: t("status.nextWithChangesCursor")
3256
- });
3257
- } else {
3258
- printNextStep({
3259
- message: t("status.nextCleanMessage"),
3260
- command: "vhk goal next",
3261
- cursorHint: t("status.nextCleanCursor")
3262
- });
3263
- }
4009
+ printNextStep(selectStatusNextStep(hasChanges));
3264
4010
  }
3265
4011
 
3266
4012
  // src/commands/diff.ts
3267
- import chalk14 from "chalk";
4013
+ import chalk17 from "chalk";
3268
4014
  function gitOut2(args) {
3269
4015
  const r = safeExecFile("git", args);
3270
4016
  return r.ok ? r.out : "";
@@ -3301,67 +4047,67 @@ function summarizeNumstat(numstat) {
3301
4047
  return { fileCount, totalAdd, totalDel };
3302
4048
  }
3303
4049
  function printFile(f) {
3304
- const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
3305
- const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
4050
+ const adds = f.additions > 0 ? chalk17.green(`+${f.additions}`) : "";
4051
+ const dels = f.deletions > 0 ? chalk17.red(`-${f.deletions}`) : "";
3306
4052
  const change = [adds, dels].filter(Boolean).join(" ");
3307
4053
  console.log(` ${f.name} ${change}`);
3308
4054
  }
3309
4055
  async function diff() {
3310
- console.log(chalk14.bold(`
4056
+ console.log(chalk17.bold(`
3311
4057
  \u{1F50D} ${t("diff.title")}`));
3312
- console.log(chalk14.gray("\u2500".repeat(40)));
4058
+ console.log(chalk17.gray("\u2500".repeat(40)));
3313
4059
  if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
3314
- console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
4060
+ console.log(chalk17.red(`\u274C ${t("diff.notGitRepo")}`));
3315
4061
  return;
3316
4062
  }
3317
4063
  const unstaged = gitOut2(["diff", "--stat"]);
3318
4064
  const staged = gitOut2(["diff", "--cached", "--stat"]);
3319
4065
  const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3320
4066
  if (!unstaged && !staged && !untracked) {
3321
- console.log(chalk14.green(`
4067
+ console.log(chalk17.green(`
3322
4068
  \u2705 ${t("diff.noChanges")}`));
3323
4069
  return;
3324
4070
  }
3325
4071
  if (staged) {
3326
- console.log(chalk14.cyan(`
4072
+ console.log(chalk17.cyan(`
3327
4073
  ${t("diff.stagedHeader")}`));
3328
4074
  parseDiffStat(staged).forEach((f) => printFile(f));
3329
4075
  }
3330
4076
  if (unstaged) {
3331
- console.log(chalk14.cyan(`
4077
+ console.log(chalk17.cyan(`
3332
4078
  ${t("diff.unstagedHeader")}`));
3333
4079
  parseDiffStat(unstaged).forEach((f) => printFile(f));
3334
4080
  }
3335
4081
  if (untracked) {
3336
4082
  const files = untracked.split("\n").filter(Boolean);
3337
- console.log(chalk14.cyan(`
4083
+ console.log(chalk17.cyan(`
3338
4084
  ${t("diff.untrackedHeader", files.length)}`));
3339
- files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
4085
+ files.forEach((f) => console.log(` ${chalk17.green("+")} ${f}`));
3340
4086
  }
3341
4087
  const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3342
4088
  if (numstat) {
3343
4089
  const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3344
- console.log(chalk14.cyan(`
4090
+ console.log(chalk17.cyan(`
3345
4091
  ${t("diff.summaryHeader")}`));
3346
4092
  console.log(` ${t("diff.filesLine", fileCount)}`);
3347
- console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
3348
- console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
4093
+ console.log(` \uCD94\uAC00: ${chalk17.green(`+${totalAdd}`)}\uC904`);
4094
+ console.log(` \uC0AD\uC81C: ${chalk17.red(`-${totalDel}`)}\uC904`);
3349
4095
  }
3350
4096
  console.log("");
3351
4097
  }
3352
4098
 
3353
4099
  // src/commands/mcp-init.ts
3354
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3355
- import { join as join3, dirname } from "path";
4100
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4101
+ import { join as join6, dirname } from "path";
3356
4102
  import { fileURLToPath as fileURLToPath2 } from "url";
3357
- import chalk15 from "chalk";
4103
+ import chalk18 from "chalk";
3358
4104
  function resolveMcpEntryPoint() {
3359
4105
  try {
3360
4106
  const here = fileURLToPath2(import.meta.url);
3361
4107
  const dir = dirname(here);
3362
4108
  for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
3363
- const candidate = join3(dir, ...rel);
3364
- if (existsSync3(candidate)) return candidate;
4109
+ const candidate = join6(dir, ...rel);
4110
+ if (existsSync5(candidate)) return candidate;
3365
4111
  }
3366
4112
  } catch {
3367
4113
  }
@@ -3369,17 +4115,17 @@ function resolveMcpEntryPoint() {
3369
4115
  const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
3370
4116
  if (typeof url === "string") {
3371
4117
  const p = fileURLToPath2(url);
3372
- if (existsSync3(p)) return p;
4118
+ if (existsSync5(p)) return p;
3373
4119
  }
3374
4120
  } catch {
3375
4121
  }
3376
4122
  try {
3377
- const pkgPath = join3(process.cwd(), "package.json");
3378
- if (existsSync3(pkgPath)) {
4123
+ const pkgPath = join6(process.cwd(), "package.json");
4124
+ if (existsSync5(pkgPath)) {
3379
4125
  const pkg = readJsonFile(pkgPath);
3380
4126
  if (pkg.name === "@byh3071/vhk") {
3381
- const local = join3(process.cwd(), "dist", "mcp", "index.js");
3382
- if (existsSync3(local)) return local;
4127
+ const local = join6(process.cwd(), "dist", "mcp", "index.js");
4128
+ if (existsSync5(local)) return local;
3383
4129
  }
3384
4130
  }
3385
4131
  } catch {
@@ -3394,31 +4140,31 @@ function resolveVhkMcpEntry() {
3394
4140
  return { command: "vhk-mcp", args: [] };
3395
4141
  }
3396
4142
  async function mcpInit() {
3397
- console.log(chalk15.bold("\n\u{1F50C} " + t("mcp.initTitle")));
3398
- console.log(chalk15.gray("\u2500".repeat(40)));
3399
- const cursorDir = join3(process.cwd(), ".cursor");
3400
- if (!existsSync3(cursorDir)) {
3401
- mkdirSync2(cursorDir, { recursive: true });
4143
+ console.log(chalk18.bold("\n\u{1F50C} " + t("mcp.initTitle")));
4144
+ console.log(chalk18.gray("\u2500".repeat(40)));
4145
+ const cursorDir = join6(process.cwd(), ".cursor");
4146
+ if (!existsSync5(cursorDir)) {
4147
+ mkdirSync3(cursorDir, { recursive: true });
3402
4148
  }
3403
- const configPath = join3(cursorDir, "mcp.json");
4149
+ const configPath = join6(cursorDir, "mcp.json");
3404
4150
  const vhkEntry = resolveVhkMcpEntry();
3405
4151
  let config;
3406
- if (existsSync3(configPath)) {
4152
+ if (existsSync5(configPath)) {
3407
4153
  try {
3408
4154
  const parsed = readJsonFile(configPath);
3409
4155
  config = {
3410
4156
  mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
3411
4157
  };
3412
4158
  } catch {
3413
- console.log(chalk15.yellow("\u26A0\uFE0F \uAE30\uC874 .cursor/mcp.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uC0C8 \uD30C\uC77C\uB85C \uB36E\uC5B4\uC501\uB2C8\uB2E4."));
4159
+ console.log(chalk18.yellow("\u26A0\uFE0F \uAE30\uC874 .cursor/mcp.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uC0C8 \uD30C\uC77C\uB85C \uB36E\uC5B4\uC501\uB2C8\uB2E4."));
3414
4160
  config = { mcpServers: { vhk: vhkEntry } };
3415
4161
  }
3416
4162
  } else {
3417
4163
  config = { mcpServers: { vhk: vhkEntry } };
3418
4164
  }
3419
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3420
- console.log(chalk15.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
3421
- console.log(chalk15.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
4165
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
4166
+ console.log(chalk18.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
4167
+ console.log(chalk18.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
3422
4168
  console.log(` ${configPath}`);
3423
4169
  printNextStep({
3424
4170
  message: t("mcp.nextMessage"),
@@ -3428,9 +4174,9 @@ async function mcpInit() {
3428
4174
  }
3429
4175
 
3430
4176
  // src/commands/design.ts
3431
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
3432
- import chalk16 from "chalk";
3433
- import inquirer7 from "inquirer";
4177
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
4178
+ import chalk19 from "chalk";
4179
+ import inquirer9 from "inquirer";
3434
4180
  var PALETTES = [
3435
4181
  {
3436
4182
  name: "Minimal",
@@ -3482,7 +4228,36 @@ var PALETTES = [
3482
4228
  }
3483
4229
  ];
3484
4230
  function hasTailwind() {
3485
- return existsSync4("tailwind.config.js") || existsSync4("tailwind.config.ts") || existsSync4("tailwind.config.mjs") || existsSync4("tailwind.config.cjs");
4231
+ return existsSync6("tailwind.config.js") || existsSync6("tailwind.config.ts") || existsSync6("tailwind.config.mjs") || existsSync6("tailwind.config.cjs");
4232
+ }
4233
+ function isTailwindV4Deps(deps) {
4234
+ if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return true;
4235
+ const tw = deps.tailwindcss;
4236
+ return typeof tw === "string" && /^\D*4(\.|$)/.test(tw);
4237
+ }
4238
+ function hasTailwindV4() {
4239
+ try {
4240
+ const pkg = readJsonFile(
4241
+ "package.json"
4242
+ );
4243
+ return isTailwindV4Deps({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
4244
+ } catch {
4245
+ return false;
4246
+ }
4247
+ }
4248
+ function generateTailwindV4Theme(palette) {
4249
+ return [
4250
+ "/* vhk design \u2014 Tailwind v4 @theme \uD1A0\uD070 (CSS-first). \uC9C4\uC785 CSS \uC5D0 @import \uD558\uC138\uC694. */",
4251
+ '@import "tailwindcss";',
4252
+ "",
4253
+ "@theme {",
4254
+ ...Object.entries(palette.colors).map(([k, v]) => ` --color-${k}: ${v};`),
4255
+ "}",
4256
+ "",
4257
+ "/* \uB2E4\uD06C \uBAA8\uB4DC \u2014 .dark \uD074\uB798\uC2A4 \uAE30\uBC18 variant (bg-background \uB4F1\uC774 .dark \uC5D0\uC11C \uC804\uD658) */",
4258
+ "@custom-variant dark (&:where(.dark, .dark *));",
4259
+ ""
4260
+ ].join("\n");
3486
4261
  }
3487
4262
  function generateCSSTokens(palette) {
3488
4263
  const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
@@ -3503,9 +4278,10 @@ export default vhkColors
3503
4278
  `;
3504
4279
  }
3505
4280
  async function design() {
3506
- console.log(chalk16.bold("\n\u{1F3A8} " + t("design.title")));
3507
- console.log(chalk16.gray("\u2500".repeat(40)));
3508
- const { paletteIndex } = await inquirer7.prompt([
4281
+ console.log(chalk19.bold("\n\u{1F3A8} " + t("design.title")));
4282
+ console.log(chalk19.gray("\u2500".repeat(40)));
4283
+ if (!ensureInteractive("\uCEEC\uB7EC \uD314\uB808\uD2B8 \uC120\uD0DD\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
4284
+ const { paletteIndex } = await inquirer9.prompt([
3509
4285
  {
3510
4286
  type: "list",
3511
4287
  name: "paletteIndex",
@@ -3517,32 +4293,37 @@ async function design() {
3517
4293
  }
3518
4294
  ]);
3519
4295
  const palette = PALETTES[paletteIndex];
3520
- console.log(chalk16.cyan(`
4296
+ console.log(chalk19.cyan(`
3521
4297
  \u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
3522
- const targetPath = hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
3523
- const content = hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
3524
- if (existsSync4(targetPath)) {
3525
- const { overwrite } = await inquirer7.prompt([{
4298
+ const v4 = hasTailwindV4();
4299
+ const targetPath = v4 ? "src/styles/theme.css" : hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
4300
+ const content = v4 ? generateTailwindV4Theme(palette) : hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
4301
+ if (existsSync6(targetPath)) {
4302
+ const { overwrite } = await inquirer9.prompt([{
3526
4303
  type: "confirm",
3527
4304
  name: "overwrite",
3528
4305
  message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
3529
4306
  default: false
3530
4307
  }]);
3531
4308
  if (!overwrite) {
3532
- console.log(chalk16.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
4309
+ console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
3533
4310
  return;
3534
4311
  }
3535
4312
  }
3536
- mkdirSync3("src/styles", { recursive: true });
3537
- writeFileSync3(targetPath, content, "utf-8");
3538
- if (hasTailwind()) {
3539
- console.log(chalk16.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
3540
- console.log(chalk16.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
4313
+ mkdirSync4("src/styles", { recursive: true });
4314
+ writeFileSync4(targetPath, content, "utf-8");
4315
+ if (v4) {
4316
+ console.log(chalk19.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (Tailwind v4 @theme)"));
4317
+ console.log(chalk19.gray(' \uC9C4\uC785 CSS(\uC608: src/index.css)\uC5D0 `@import "./styles/theme.css";` \uCD94\uAC00 \u2192 bg-primary \uB4F1 \uC720\uD2F8 \uC0AC\uC6A9.'));
4318
+ console.log(chalk19.gray(" \uB2E4\uD06C \uD1A0\uAE00: \uB8E8\uD2B8 <html>/<body> \uC5D0 `.dark` \uD074\uB798\uC2A4 on/off."));
4319
+ } else if (hasTailwind()) {
4320
+ console.log(chalk19.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
4321
+ console.log(chalk19.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
3541
4322
  } else {
3542
- console.log(chalk16.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
3543
- console.log(chalk16.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
4323
+ console.log(chalk19.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
4324
+ console.log(chalk19.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
3544
4325
  }
3545
- console.log(chalk16.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
4326
+ console.log(chalk19.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
3546
4327
  for (const [key, value] of Object.entries(palette.colors)) {
3547
4328
  console.log(` ${key.padEnd(12)} ${value}`);
3548
4329
  }
@@ -3557,9 +4338,9 @@ async function designPalette() {
3557
4338
  }
3558
4339
 
3559
4340
  // src/commands/theme.ts
3560
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3561
- import chalk17 from "chalk";
3562
- import inquirer8 from "inquirer";
4341
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
4342
+ import chalk20 from "chalk";
4343
+ import inquirer10 from "inquirer";
3563
4344
  function generateDarkCSS() {
3564
4345
  return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
3565
4346
 
@@ -3615,13 +4396,13 @@ export function initTheme(): void {
3615
4396
  `;
3616
4397
  }
3617
4398
  async function theme() {
3618
- console.log(chalk17.bold("\n\u{1F319} " + t("theme.title")));
3619
- console.log(chalk17.gray("\u2500".repeat(40)));
4399
+ console.log(chalk20.bold("\n\u{1F319} " + t("theme.title")));
4400
+ console.log(chalk20.gray("\u2500".repeat(40)));
3620
4401
  const cssPath = "src/styles/theme.css";
3621
4402
  const togglePath = "src/lib/theme-toggle.ts";
3622
- const conflicts = [cssPath, togglePath].filter((p) => existsSync5(p));
4403
+ const conflicts = [cssPath, togglePath].filter((p) => existsSync7(p));
3623
4404
  if (conflicts.length > 0) {
3624
- const { overwrite } = await inquirer8.prompt([{
4405
+ const { overwrite } = await inquirer10.prompt([{
3625
4406
  type: "confirm",
3626
4407
  name: "overwrite",
3627
4408
  message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
@@ -3629,21 +4410,21 @@ async function theme() {
3629
4410
  default: false
3630
4411
  }]);
3631
4412
  if (!overwrite) {
3632
- console.log(chalk17.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
4413
+ console.log(chalk20.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
3633
4414
  return;
3634
4415
  }
3635
4416
  }
3636
- mkdirSync4("src/styles", { recursive: true });
3637
- mkdirSync4("src/lib", { recursive: true });
3638
- writeFileSync4(cssPath, generateDarkCSS(), "utf-8");
3639
- console.log(chalk17.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
3640
- writeFileSync4(togglePath, generateToggleUtil(), "utf-8");
3641
- console.log(chalk17.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
3642
- console.log(chalk17.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
3643
- console.log(chalk17.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
3644
- console.log(chalk17.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
3645
- console.log(chalk17.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
3646
- console.log(chalk17.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
4417
+ mkdirSync5("src/styles", { recursive: true });
4418
+ mkdirSync5("src/lib", { recursive: true });
4419
+ writeFileSync5(cssPath, generateDarkCSS(), "utf-8");
4420
+ console.log(chalk20.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
4421
+ writeFileSync5(togglePath, generateToggleUtil(), "utf-8");
4422
+ console.log(chalk20.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
4423
+ console.log(chalk20.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
4424
+ console.log(chalk20.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
4425
+ console.log(chalk20.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
4426
+ console.log(chalk20.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
4427
+ console.log(chalk20.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
3647
4428
  printNextStep({
3648
4429
  message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
3649
4430
  command: "vhk ref list",
@@ -3652,11 +4433,11 @@ async function theme() {
3652
4433
  }
3653
4434
 
3654
4435
  // src/commands/ref.ts
3655
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3656
- import chalk18 from "chalk";
4436
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4437
+ import chalk21 from "chalk";
3657
4438
  var REFS_PATH = ".vhk/refs.json";
3658
4439
  function loadRefs() {
3659
- if (!existsSync6(REFS_PATH)) return [];
4440
+ if (!existsSync8(REFS_PATH)) return [];
3660
4441
  try {
3661
4442
  const parsed = readJsonFile(REFS_PATH);
3662
4443
  return Array.isArray(parsed) ? parsed : [];
@@ -3665,28 +4446,28 @@ function loadRefs() {
3665
4446
  }
3666
4447
  }
3667
4448
  function saveRefs(refs) {
3668
- mkdirSync5(".vhk", { recursive: true });
3669
- writeFileSync5(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
4449
+ mkdirSync6(".vhk", { recursive: true });
4450
+ writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
3670
4451
  }
3671
4452
  async function refAdd(url, memo = "") {
3672
- console.log(chalk18.bold("\n\u{1F517} " + t("ref.addTitle")));
3673
- console.log(chalk18.gray("\u2500".repeat(40)));
4453
+ console.log(chalk21.bold("\n\u{1F517} " + t("ref.addTitle")));
4454
+ console.log(chalk21.gray("\u2500".repeat(40)));
3674
4455
  if (!url) {
3675
- console.log(chalk18.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
3676
- console.log(chalk18.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
4456
+ console.log(chalk21.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
4457
+ console.log(chalk21.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
3677
4458
  return;
3678
4459
  }
3679
4460
  const refs = loadRefs();
3680
4461
  if (refs.some((r) => r.url === url)) {
3681
- console.log(chalk18.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
4462
+ console.log(chalk21.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
3682
4463
  return;
3683
4464
  }
3684
4465
  refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
3685
4466
  saveRefs(refs);
3686
- console.log(chalk18.green(`
4467
+ console.log(chalk21.green(`
3687
4468
  \u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
3688
- console.log(chalk18.cyan(` ${url}`));
3689
- if (memo) console.log(chalk18.gray(` \u{1F4DD} ${memo}`));
4469
+ console.log(chalk21.cyan(` ${url}`));
4470
+ if (memo) console.log(chalk21.gray(` \u{1F4DD} ${memo}`));
3690
4471
  printNextStep({
3691
4472
  message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
3692
4473
  command: "vhk ref list",
@@ -3694,22 +4475,22 @@ async function refAdd(url, memo = "") {
3694
4475
  });
3695
4476
  }
3696
4477
  async function refList() {
3697
- console.log(chalk18.bold("\n\u{1F4DA} " + t("ref.listTitle")));
3698
- console.log(chalk18.gray("\u2500".repeat(40)));
4478
+ console.log(chalk21.bold("\n\u{1F4DA} " + t("ref.listTitle")));
4479
+ console.log(chalk21.gray("\u2500".repeat(40)));
3699
4480
  const refs = loadRefs();
3700
4481
  if (refs.length === 0) {
3701
- console.log(chalk18.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
3702
- console.log(chalk18.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
4482
+ console.log(chalk21.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
4483
+ console.log(chalk21.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
3703
4484
  return;
3704
4485
  }
3705
- console.log(chalk18.cyan(`
4486
+ console.log(chalk21.cyan(`
3706
4487
  \uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
3707
4488
  `));
3708
4489
  refs.forEach((ref, index) => {
3709
4490
  const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
3710
- console.log(chalk18.white(` [${index + 1}] ${ref.url}`));
3711
- if (ref.memo) console.log(chalk18.gray(` \u{1F4DD} ${ref.memo}`));
3712
- console.log(chalk18.gray(` \u{1F4C5} ${date}`));
4491
+ console.log(chalk21.white(` [${index + 1}] ${ref.url}`));
4492
+ if (ref.memo) console.log(chalk21.gray(` \u{1F4DD} ${ref.memo}`));
4493
+ console.log(chalk21.gray(` \u{1F4C5} ${date}`));
3713
4494
  console.log("");
3714
4495
  });
3715
4496
  }
@@ -3717,7 +4498,7 @@ async function refOpen(indexStr) {
3717
4498
  const refs = loadRefs();
3718
4499
  const idx = parseInt(indexStr, 10) - 1;
3719
4500
  if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
3720
- console.log(chalk18.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
4501
+ console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
3721
4502
  return;
3722
4503
  }
3723
4504
  const ref = refs[idx];
@@ -3725,14 +4506,14 @@ async function refOpen(indexStr) {
3725
4506
  try {
3726
4507
  parsed = new URL(ref.url);
3727
4508
  } catch {
3728
- console.log(chalk18.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
4509
+ console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
3729
4510
  return;
3730
4511
  }
3731
4512
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3732
- console.log(chalk18.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
4513
+ console.log(chalk21.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
3733
4514
  return;
3734
4515
  }
3735
- console.log(chalk18.cyan(`
4516
+ console.log(chalk21.cyan(`
3736
4517
  \u{1F310} \uC5F4\uAE30: ${ref.url}`));
3737
4518
  let result;
3738
4519
  if (process.platform === "darwin") {
@@ -3743,19 +4524,19 @@ async function refOpen(indexStr) {
3743
4524
  result = safeExecFile("xdg-open", [ref.url]);
3744
4525
  }
3745
4526
  if (result.ok) {
3746
- console.log(chalk18.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
4527
+ console.log(chalk21.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
3747
4528
  } else {
3748
- console.log(chalk18.yellow("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. URL\uC744 \uC9C1\uC811 \uBC29\uBB38\uD574\uC8FC\uC138\uC694."));
4529
+ console.log(chalk21.yellow("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. URL\uC744 \uC9C1\uC811 \uBC29\uBB38\uD574\uC8FC\uC138\uC694."));
3749
4530
  }
3750
4531
  }
3751
4532
 
3752
4533
  // src/commands/harness.ts
3753
- import { existsSync as existsSync7 } from "fs";
3754
- import chalk19 from "chalk";
4534
+ import { existsSync as existsSync9 } from "fs";
4535
+ import chalk22 from "chalk";
3755
4536
  import ora2 from "ora";
3756
4537
  function detectPM() {
3757
- if (existsSync7("pnpm-lock.yaml")) return "pnpm";
3758
- if (existsSync7("yarn.lock")) return "yarn";
4538
+ if (existsSync9("pnpm-lock.yaml")) return "pnpm";
4539
+ if (existsSync9("yarn.lock")) return "yarn";
3759
4540
  return "npm";
3760
4541
  }
3761
4542
  function pmRun(pm, script) {
@@ -3773,14 +4554,14 @@ function detectChecks() {
3773
4554
  const pm = detectPM();
3774
4555
  if (s.lint) {
3775
4556
  checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
3776
- } else if (existsSync7(".eslintrc.js") || existsSync7(".eslintrc.json") || existsSync7("eslint.config.js")) {
4557
+ } else if (existsSync9(".eslintrc.js") || existsSync9(".eslintrc.json") || existsSync9("eslint.config.js")) {
3777
4558
  checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
3778
4559
  }
3779
4560
  if (s["type-check"]) {
3780
4561
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
3781
4562
  } else if (s.typecheck) {
3782
4563
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
3783
- } else if (existsSync7("tsconfig.json")) {
4564
+ } else if (existsSync9("tsconfig.json")) {
3784
4565
  checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
3785
4566
  }
3786
4567
  if (s.test) {
@@ -3792,15 +4573,16 @@ function detectChecks() {
3792
4573
  return checks;
3793
4574
  }
3794
4575
  async function harness() {
3795
- console.log(chalk19.bold("\n\u{1F527} " + t("harness.title")));
3796
- console.log(chalk19.gray("\u2500".repeat(40)));
4576
+ if (!ensureNotHardStopped("harness")) return;
4577
+ console.log(chalk22.bold("\n\u{1F527} " + t("harness.title")));
4578
+ console.log(chalk22.gray("\u2500".repeat(40)));
3797
4579
  const checks = detectChecks();
3798
4580
  if (checks.length === 0) {
3799
- console.log(chalk19.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
3800
- console.log(chalk19.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
4581
+ console.log(chalk22.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
4582
+ console.log(chalk22.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
3801
4583
  return;
3802
4584
  }
3803
- console.log(chalk19.cyan(`
4585
+ console.log(chalk22.cyan(`
3804
4586
  \u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
3805
4587
  `));
3806
4588
  const results = [];
@@ -3812,10 +4594,10 @@ async function harness() {
3812
4594
  const duration = Date.now() - start2;
3813
4595
  const sec = (duration / 1e3).toFixed(1);
3814
4596
  if (result.ok) {
3815
- spinner.succeed(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4597
+ spinner.succeed(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
3816
4598
  results.push({ name: check2.name, command: display, passed: true, duration });
3817
4599
  } else {
3818
- spinner.fail(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4600
+ spinner.fail(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
3819
4601
  results.push({
3820
4602
  name: check2.name,
3821
4603
  command: display,
@@ -3825,24 +4607,25 @@ async function harness() {
3825
4607
  });
3826
4608
  }
3827
4609
  }
3828
- console.log(chalk19.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
3829
- console.log(chalk19.gray("\u2500".repeat(40)));
4610
+ console.log(chalk22.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
4611
+ console.log(chalk22.gray("\u2500".repeat(40)));
3830
4612
  for (const r of results) {
3831
- const icon = r.passed ? chalk19.green("\u2705") : chalk19.red("\u274C");
4613
+ const icon = r.passed ? chalk22.green("\u2705") : chalk22.red("\u274C");
3832
4614
  const sec = (r.duration / 1e3).toFixed(1);
3833
- console.log(` ${icon} ${r.name.padEnd(15)} ${chalk19.gray(`${sec}s`)}`);
4615
+ console.log(` ${icon} ${r.name.padEnd(15)} ${chalk22.gray(`${sec}s`)}`);
3834
4616
  }
3835
4617
  const passed = results.filter((r) => r.passed).length;
3836
4618
  const all = passed === results.length;
3837
- console.log(chalk19.gray("\u2500".repeat(40)));
4619
+ console.log(chalk22.gray("\u2500".repeat(40)));
3838
4620
  if (all) {
3839
- console.log(chalk19.green.bold(`
4621
+ console.log(chalk22.green.bold(`
3840
4622
  \u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
3841
4623
  } else {
3842
4624
  console.log(
3843
- chalk19.red.bold(`
4625
+ chalk22.red.bold(`
3844
4626
  \u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
3845
4627
  );
4628
+ process.exitCode = 1;
3846
4629
  }
3847
4630
  printNextStep({
3848
4631
  message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
@@ -3852,9 +4635,9 @@ async function harness() {
3852
4635
  }
3853
4636
 
3854
4637
  // src/commands/migrate.ts
3855
- import { existsSync as existsSync8, unlinkSync, rmSync } from "fs";
3856
- import chalk20 from "chalk";
3857
- import inquirer9 from "inquirer";
4638
+ import { existsSync as existsSync10, unlinkSync, rmSync as rmSync2 } from "fs";
4639
+ import chalk23 from "chalk";
4640
+ import inquirer11 from "inquirer";
3858
4641
  import ora3 from "ora";
3859
4642
  var LOCK_FILES = {
3860
4643
  npm: "package-lock.json",
@@ -3862,26 +4645,26 @@ var LOCK_FILES = {
3862
4645
  pnpm: "pnpm-lock.yaml"
3863
4646
  };
3864
4647
  function detectCurrentPM() {
3865
- if (existsSync8("pnpm-lock.yaml")) return "pnpm";
3866
- if (existsSync8("yarn.lock")) return "yarn";
3867
- if (existsSync8("package-lock.json")) return "npm";
4648
+ if (existsSync10("pnpm-lock.yaml")) return "pnpm";
4649
+ if (existsSync10("yarn.lock")) return "yarn";
4650
+ if (existsSync10("package-lock.json")) return "npm";
3868
4651
  return null;
3869
4652
  }
3870
4653
  function isCLIAvailable(pm) {
3871
4654
  return safeExecFile(pm, ["--version"]).ok;
3872
4655
  }
3873
4656
  async function migrate(target) {
3874
- console.log(chalk20.bold("\n\u{1F504} " + t("migrate.title")));
3875
- console.log(chalk20.gray("\u2500".repeat(40)));
4657
+ console.log(chalk23.bold("\n\u{1F504} " + t("migrate.title")));
4658
+ console.log(chalk23.gray("\u2500".repeat(40)));
3876
4659
  const current = detectCurrentPM();
3877
- console.log(chalk20.cyan(`
4660
+ console.log(chalk23.cyan(`
3878
4661
  \uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
3879
4662
  let targetPM;
3880
4663
  if (target && ["npm", "yarn", "pnpm"].includes(target)) {
3881
4664
  targetPM = target;
3882
4665
  } else {
3883
4666
  const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
3884
- const { selected } = await inquirer9.prompt([
4667
+ const { selected } = await inquirer11.prompt([
3885
4668
  {
3886
4669
  type: "list",
3887
4670
  name: "selected",
@@ -3892,17 +4675,17 @@ async function migrate(target) {
3892
4675
  targetPM = selected;
3893
4676
  }
3894
4677
  if (targetPM === current) {
3895
- console.log(chalk20.yellow(`
4678
+ console.log(chalk23.yellow(`
3896
4679
  \u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
3897
4680
  return;
3898
4681
  }
3899
4682
  if (!isCLIAvailable(targetPM)) {
3900
- console.log(chalk20.red(`
4683
+ console.log(chalk23.red(`
3901
4684
  \u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
3902
- console.log(chalk20.yellow(` npm i -g ${targetPM}`));
4685
+ console.log(chalk23.yellow(` npm i -g ${targetPM}`));
3903
4686
  return;
3904
4687
  }
3905
- const { confirm } = await inquirer9.prompt([
4688
+ const { confirm } = await inquirer11.prompt([
3906
4689
  {
3907
4690
  type: "confirm",
3908
4691
  name: "confirm",
@@ -3911,18 +4694,18 @@ async function migrate(target) {
3911
4694
  }
3912
4695
  ]);
3913
4696
  if (!confirm) {
3914
- console.log(chalk20.gray("\uCDE8\uC18C\uB428"));
4697
+ console.log(chalk23.gray("\uCDE8\uC18C\uB428"));
3915
4698
  return;
3916
4699
  }
3917
4700
  const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
3918
4701
  for (const lockFile of Object.values(LOCK_FILES)) {
3919
- if (existsSync8(lockFile)) {
4702
+ if (existsSync10(lockFile)) {
3920
4703
  unlinkSync(lockFile);
3921
4704
  }
3922
4705
  }
3923
- if (existsSync8("node_modules")) {
4706
+ if (existsSync10("node_modules")) {
3924
4707
  cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
3925
- rmSync("node_modules", { recursive: true, force: true });
4708
+ rmSync2("node_modules", { recursive: true, force: true });
3926
4709
  }
3927
4710
  cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
3928
4711
  const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
@@ -3931,10 +4714,10 @@ async function migrate(target) {
3931
4714
  install.succeed(`${targetPM} install \uC644\uB8CC!`);
3932
4715
  } else {
3933
4716
  install.fail(`${targetPM} install \uC2E4\uD328`);
3934
- console.log(chalk20.red(installResult.err.slice(0, 300)));
4717
+ console.log(chalk23.red(installResult.err.slice(0, 300)));
3935
4718
  return;
3936
4719
  }
3937
- console.log(chalk20.green.bold(`
4720
+ console.log(chalk23.green.bold(`
3938
4721
  \u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
3939
4722
  printNextStep({
3940
4723
  message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
@@ -3944,17 +4727,17 @@ async function migrate(target) {
3944
4727
  }
3945
4728
 
3946
4729
  // src/commands/update.ts
3947
- import { existsSync as existsSync9 } from "fs";
3948
- import { dirname as dirname2, join as join4 } from "path";
4730
+ import { existsSync as existsSync11 } from "fs";
4731
+ import { dirname as dirname2, join as join7 } from "path";
3949
4732
  import { fileURLToPath as fileURLToPath3 } from "url";
3950
- import chalk21 from "chalk";
4733
+ import chalk24 from "chalk";
3951
4734
  import ora4 from "ora";
3952
4735
  var PACKAGE = "@byh3071/vhk";
3953
4736
  function getCurrentVersion() {
3954
4737
  const dir = dirname2(fileURLToPath3(import.meta.url));
3955
- for (const pkgPath of [join4(dir, "../package.json"), join4(dir, "../../package.json")]) {
4738
+ for (const pkgPath of [join7(dir, "../package.json"), join7(dir, "../../package.json")]) {
3956
4739
  try {
3957
- if (existsSync9(pkgPath)) {
4740
+ if (existsSync11(pkgPath)) {
3958
4741
  const pkg = readJsonFile(pkgPath);
3959
4742
  if (pkg.version) return pkg.version;
3960
4743
  }
@@ -3977,32 +4760,32 @@ function isUpToDate(current, latest) {
3977
4760
  return cc >= lc;
3978
4761
  }
3979
4762
  async function update() {
3980
- console.log(chalk21.bold("\n\u2B06\uFE0F " + t("update.title")));
3981
- console.log(chalk21.gray("\u2500".repeat(40)));
4763
+ console.log(chalk24.bold("\n\u2B06\uFE0F " + t("update.title")));
4764
+ console.log(chalk24.gray("\u2500".repeat(40)));
3982
4765
  const current = getCurrentVersion();
3983
- console.log(chalk21.cyan(`
4766
+ console.log(chalk24.cyan(`
3984
4767
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
3985
4768
  const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
3986
4769
  const latest = getLatestVersion();
3987
4770
  if (!latest) {
3988
4771
  spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
3989
- console.log(chalk21.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
3990
- console.log(chalk21.gray(` npm update -g ${PACKAGE}`));
4772
+ console.log(chalk24.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4773
+ console.log(chalk24.gray(` npm update -g ${PACKAGE}`));
3991
4774
  return;
3992
4775
  }
3993
4776
  spinner.stop();
3994
- console.log(chalk21.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
4777
+ console.log(chalk24.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
3995
4778
  if (isUpToDate(current, latest)) {
3996
- console.log(chalk21.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
4779
+ console.log(chalk24.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
3997
4780
  return;
3998
4781
  }
3999
4782
  const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
4000
4783
  const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
4001
4784
  if (upd.ok) {
4002
4785
  updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
4003
- console.log(chalk21.green.bold(`
4786
+ console.log(chalk24.green.bold(`
4004
4787
  \u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
4005
- console.log(chalk21.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
4788
+ console.log(chalk24.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
4006
4789
  printNextStep({
4007
4790
  message: t("update.nextOkMessage"),
4008
4791
  command: "vhk --version",
@@ -4010,9 +4793,9 @@ async function update() {
4010
4793
  });
4011
4794
  } else {
4012
4795
  updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
4013
- console.log(chalk21.red(upd.err.slice(0, 300)));
4014
- console.log(chalk21.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4015
- console.log(chalk21.gray(` npm update -g ${PACKAGE}`));
4796
+ console.log(chalk24.red(upd.err.slice(0, 300)));
4797
+ console.log(chalk24.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4798
+ console.log(chalk24.gray(` npm update -g ${PACKAGE}`));
4016
4799
  printNextStep({
4017
4800
  message: t("update.nextFailMessage"),
4018
4801
  command: "vhk doctor",
@@ -4023,121 +4806,15 @@ async function update() {
4023
4806
 
4024
4807
  // src/commands/context.ts
4025
4808
  import {
4026
- existsSync as existsSync11,
4809
+ existsSync as existsSync12,
4027
4810
  mkdirSync as mkdirSync7,
4028
- readFileSync as readFileSync4,
4811
+ readFileSync as readFileSync5,
4029
4812
  readdirSync as readdirSync2,
4030
4813
  statSync as statSync2,
4031
4814
  writeFileSync as writeFileSync7
4032
4815
  } from "fs";
4033
- import { join as join6 } from "path";
4034
- import chalk22 from "chalk";
4035
-
4036
- // src/lib/state-files.ts
4037
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6, appendFileSync, rmSync as rmSync2 } from "fs";
4038
- import { join as join5 } from "path";
4039
- var STATE_DIR2 = "docs/state";
4040
- var BLOCKERS_PATH = join5(STATE_DIR2, "blockers.md");
4041
- var LEARNINGS_PATH = join5(STATE_DIR2, "learnings.md");
4042
- var VHK_DIR = ".vhk";
4043
- var HARD_STOP_PATH = join5(VHK_DIR, "HARD_STOP");
4044
- var HARD_STOP_BLOCKER_THRESHOLD = 3;
4045
- function ensureStateDir() {
4046
- mkdirSync6(STATE_DIR2, { recursive: true });
4047
- }
4048
- function ensureVhkDir() {
4049
- mkdirSync6(VHK_DIR, { recursive: true });
4050
- }
4051
- function isoDate() {
4052
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4053
- }
4054
- var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
4055
- function countActiveBlockers(content) {
4056
- let count = 0;
4057
- for (const line of content.split(/\r?\n/)) {
4058
- if (ACTIVE_BLOCKER_RE.test(line)) count++;
4059
- }
4060
- return count;
4061
- }
4062
- function appendBlocker(description, goalId) {
4063
- ensureStateDir();
4064
- const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
4065
- const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
4066
- if (!existsSync10(BLOCKERS_PATH)) {
4067
- writeFileSync6(
4068
- BLOCKERS_PATH,
4069
- `# Blockers
4070
-
4071
- _Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
4072
-
4073
- ${line}
4074
- `,
4075
- "utf-8"
4076
- );
4077
- } else {
4078
- appendFileSync(BLOCKERS_PATH, `${line}
4079
- `, "utf-8");
4080
- }
4081
- const current = readFileSync3(BLOCKERS_PATH, "utf-8");
4082
- const count = countActiveBlockers(current);
4083
- let hardStopTripped = false;
4084
- if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync10(HARD_STOP_PATH)) {
4085
- writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
4086
- hardStopTripped = true;
4087
- }
4088
- return { count, hardStopTripped };
4089
- }
4090
- function appendLearning(lesson, goalId) {
4091
- ensureStateDir();
4092
- const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
4093
- const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
4094
- if (!existsSync10(LEARNINGS_PATH)) {
4095
- writeFileSync6(
4096
- LEARNINGS_PATH,
4097
- `# Learnings
4098
-
4099
- _Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
4100
-
4101
- ${line}
4102
- `,
4103
- "utf-8"
4104
- );
4105
- } else {
4106
- appendFileSync(LEARNINGS_PATH, `${line}
4107
- `, "utf-8");
4108
- }
4109
- }
4110
- function getRecentLearnings(limit = 3) {
4111
- if (!existsSync10(LEARNINGS_PATH)) return [];
4112
- const lines = readFileSync3(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
4113
- const entries = lines.filter((l) => l.startsWith("- ["));
4114
- return entries.slice(-limit);
4115
- }
4116
- function writeHardStop(reason) {
4117
- ensureVhkDir();
4118
- const ts = (/* @__PURE__ */ new Date()).toISOString();
4119
- writeFileSync6(HARD_STOP_PATH, `${ts}
4120
- ${reason}
4121
- `, "utf-8");
4122
- }
4123
- function isHardStopActive() {
4124
- return existsSync10(HARD_STOP_PATH);
4125
- }
4126
- function readHardStopReason() {
4127
- if (!existsSync10(HARD_STOP_PATH)) return null;
4128
- try {
4129
- return readFileSync3(HARD_STOP_PATH, "utf-8").trim();
4130
- } catch {
4131
- return null;
4132
- }
4133
- }
4134
- function clearHardStop() {
4135
- if (!existsSync10(HARD_STOP_PATH)) return false;
4136
- rmSync2(HARD_STOP_PATH, { force: true });
4137
- return true;
4138
- }
4139
-
4140
- // src/commands/context.ts
4816
+ import { join as join8 } from "path";
4817
+ import chalk25 from "chalk";
4141
4818
  var CONTEXT_PATH2 = ".vhk/context.md";
4142
4819
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
4143
4820
  "node_modules",
@@ -4162,7 +4839,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
4162
4839
  filtered.forEach((entry, index) => {
4163
4840
  const isLast = index === filtered.length - 1;
4164
4841
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4165
- const fullPath = join6(dir, entry);
4842
+ const fullPath = join8(dir, entry);
4166
4843
  const stat = statSync2(fullPath);
4167
4844
  const isDir = stat.isDirectory();
4168
4845
  lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
@@ -4194,8 +4871,8 @@ function extractTechStack() {
4194
4871
  else if (all.jest) stack["\uD14C\uC2A4\uD2B8"] = "jest";
4195
4872
  if (all.commander) stack["CLI"] = "commander";
4196
4873
  if (all.inquirer) stack["\uC778\uD130\uB799\uD2F0\uBE0C"] = "inquirer";
4197
- if (existsSync11("pnpm-lock.yaml")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "pnpm";
4198
- else if (existsSync11("yarn.lock")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "yarn";
4874
+ if (existsSync12("pnpm-lock.yaml")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "pnpm";
4875
+ else if (existsSync12("yarn.lock")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "yarn";
4199
4876
  else stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "npm";
4200
4877
  if (pkg.name) stack["\uD328\uD0A4\uC9C0 \uC774\uB984"] = pkg.name;
4201
4878
  if (pkg.version) stack["\uBC84\uC804"] = pkg.version;
@@ -4236,11 +4913,12 @@ function getVhkCommands() {
4236
4913
  "mcp-init \u2014 Cursor MCP \uC124\uC815"
4237
4914
  ];
4238
4915
  }
4239
- async function context() {
4240
- console.log(chalk22.bold("\n\u{1F9E0} " + t("context.title")));
4241
- console.log(chalk22.gray("\u2500".repeat(40)));
4916
+ async function context(opts = {}) {
4917
+ const compact = opts.compact === true;
4918
+ console.log(chalk25.bold("\n\u{1F9E0} " + t("context.title")));
4919
+ console.log(chalk25.gray("\u2500".repeat(40)));
4242
4920
  const stack = extractTechStack();
4243
- const tree = buildTree(".").join("\n");
4921
+ const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
4244
4922
  const commands = getVhkCommands();
4245
4923
  const lines = [];
4246
4924
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
@@ -4256,25 +4934,37 @@ async function context() {
4256
4934
  lines.push("");
4257
4935
  lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
4258
4936
  lines.push("");
4259
- lines.push("```");
4937
+ lines.push("```text");
4260
4938
  lines.push(tree);
4261
4939
  lines.push("```");
4262
4940
  lines.push("");
4263
- lines.push("## VHK CLI \uBA85\uB839\uC5B4");
4264
- lines.push("");
4265
- for (const cmd of commands) {
4266
- lines.push(`- \`vhk ${cmd}\``);
4941
+ if (compact) {
4942
+ lines.push("## \uBA85\uB839\uC5B4 (\uC694\uC57D)");
4943
+ lines.push("");
4944
+ lines.push("- \uC804\uCCB4 \uBAA9\uB85D\uC740 `vhk help` \uB610\uB294 `COMMANDS.md` \uCC38\uC870 (compact \uBAA8\uB4DC\uB294 \uC0DD\uB7B5)");
4945
+ lines.push("");
4946
+ } else {
4947
+ lines.push("## VHK CLI \uBA85\uB839\uC5B4");
4948
+ lines.push("");
4949
+ for (const cmd of commands) {
4950
+ lines.push(`- \`vhk ${cmd}\``);
4951
+ }
4952
+ lines.push("");
4267
4953
  }
4268
- lines.push("");
4269
- if (existsSync11(".vhk/memory.json")) {
4954
+ if (existsSync12(".vhk/memory.json")) {
4270
4955
  try {
4271
4956
  const memories = readJsonFile(
4272
4957
  ".vhk/memory.json"
4273
4958
  );
4274
4959
  if (Array.isArray(memories) && memories.length > 0) {
4960
+ const recentMemories = memories.slice(-5);
4275
4961
  lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
4276
4962
  lines.push("");
4277
- for (const m of memories) {
4963
+ if (memories.length > recentMemories.length) {
4964
+ lines.push(`_\uCD5C\uADFC ${recentMemories.length}\uAC1C\uB9CC \uD45C\uC2DC (\uC804\uCCB4 ${memories.length}\uAC1C)_`);
4965
+ lines.push("");
4966
+ }
4967
+ for (const m of recentMemories) {
4278
4968
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4279
4969
  lines.push(`- ${m.content} _(${date})_`);
4280
4970
  }
@@ -4305,6 +4995,24 @@ async function context() {
4305
4995
  for (const r of recent) lines.push(r);
4306
4996
  lines.push("");
4307
4997
  }
4998
+ const activeBlockers = getActiveBlockers(3);
4999
+ if (activeBlockers.length > 0) {
5000
+ lines.push("## Active Blockers");
5001
+ lines.push("");
5002
+ for (const b of activeBlockers) lines.push(b);
5003
+ lines.push("");
5004
+ }
5005
+ if (compact) {
5006
+ lines.push("## \uCC38\uC870 \uBB38\uC11C (\uD544\uC694\uC2DC \uC5F4\uB78C)");
5007
+ lines.push("");
5008
+ lines.push("- \uC791\uB3D9 \uADDC\uC57D(\uC694\uC57D): `docs/context/agent-compact.md`");
5009
+ lines.push("- \uADDC\uC57D \uC0C1\uC138: `AGENTS.md`");
5010
+ lines.push("- \uAE30\uB85D \uADDC\uCE59: `CLAUDE.md`");
5011
+ lines.push("- \uBA85\uB839 \uC0C1\uC138: `COMMANDS.md`");
5012
+ lines.push("- \uAD6C\uC870 \uC0C1\uC138: `docs/ARCHITECTURE.md`");
5013
+ lines.push("- \uD604\uC7AC \uC0C1\uD0DC: `docs/state/next-task.md`");
5014
+ lines.push("");
5015
+ }
4308
5016
  if (isHardStopActive()) {
4309
5017
  lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
4310
5018
  lines.push("");
@@ -4323,10 +5031,10 @@ async function context() {
4323
5031
  lines.push("");
4324
5032
  mkdirSync7(".vhk", { recursive: true });
4325
5033
  writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
4326
- console.log(chalk22.green(`
5034
+ console.log(chalk25.green(`
4327
5035
  \u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
4328
- console.log(chalk22.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
4329
- console.log(chalk22.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
5036
+ console.log(chalk25.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
5037
+ console.log(chalk25.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
4330
5038
  printNextStep({
4331
5039
  message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
4332
5040
  command: "vhk context-show",
@@ -4334,23 +5042,23 @@ async function context() {
4334
5042
  });
4335
5043
  }
4336
5044
  async function contextShow() {
4337
- console.log(chalk22.bold("\n\u{1F4C4} " + t("context.showTitle")));
4338
- console.log(chalk22.gray("\u2500".repeat(40)));
4339
- if (!existsSync11(CONTEXT_PATH2)) {
4340
- console.log(chalk22.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4341
- console.log(chalk22.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
5045
+ console.log(chalk25.bold("\n\u{1F4C4} " + t("context.showTitle")));
5046
+ console.log(chalk25.gray("\u2500".repeat(40)));
5047
+ if (!existsSync12(CONTEXT_PATH2)) {
5048
+ console.log(chalk25.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
5049
+ console.log(chalk25.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
4342
5050
  return;
4343
5051
  }
4344
- const content = readFileSync4(CONTEXT_PATH2, "utf-8");
5052
+ const content = readFileSync5(CONTEXT_PATH2, "utf-8");
4345
5053
  console.log("\n" + content);
4346
5054
  }
4347
5055
 
4348
5056
  // src/commands/memory.ts
4349
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
4350
- import chalk23 from "chalk";
5057
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
5058
+ import chalk26 from "chalk";
4351
5059
  var MEMORY_PATH = ".vhk/memory.json";
4352
5060
  function loadMemories() {
4353
- if (!existsSync12(MEMORY_PATH)) return [];
5061
+ if (!existsSync13(MEMORY_PATH)) return [];
4354
5062
  try {
4355
5063
  const parsed = readJsonFile(MEMORY_PATH);
4356
5064
  return Array.isArray(parsed) ? parsed : [];
@@ -4363,11 +5071,12 @@ function saveMemories(memories) {
4363
5071
  writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
4364
5072
  }
4365
5073
  async function memoryAdd(content, tags) {
4366
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.addTitle")));
4367
- console.log(chalk23.gray("\u2500".repeat(40)));
5074
+ console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.addTitle")));
5075
+ console.log(chalk26.gray("\u2500".repeat(40)));
4368
5076
  if (!content) {
4369
- console.log(chalk23.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
4370
- console.log(chalk23.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
5077
+ console.log(chalk26.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
5078
+ console.log(chalk26.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
5079
+ process.exitCode = 1;
4371
5080
  return;
4372
5081
  }
4373
5082
  const memories = loadMemories();
@@ -4377,9 +5086,9 @@ async function memoryAdd(content, tags) {
4377
5086
  tags: tags && tags.length > 0 ? tags : []
4378
5087
  });
4379
5088
  saveMemories(memories);
4380
- console.log(chalk23.green(`
5089
+ console.log(chalk26.green(`
4381
5090
  \u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
4382
- console.log(chalk23.cyan(` \u{1F4DD} ${content}`));
5091
+ console.log(chalk26.cyan(` \u{1F4DD} ${content}`));
4383
5092
  printNextStep({
4384
5093
  message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
4385
5094
  command: "vhk memory list",
@@ -4387,24 +5096,24 @@ async function memoryAdd(content, tags) {
4387
5096
  });
4388
5097
  }
4389
5098
  async function memoryList() {
4390
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.listTitle")));
4391
- console.log(chalk23.gray("\u2500".repeat(40)));
5099
+ console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.listTitle")));
5100
+ console.log(chalk26.gray("\u2500".repeat(40)));
4392
5101
  const memories = loadMemories();
4393
5102
  if (memories.length === 0) {
4394
- console.log(chalk23.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4395
- console.log(chalk23.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
5103
+ console.log(chalk26.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
5104
+ console.log(chalk26.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
4396
5105
  return;
4397
5106
  }
4398
- console.log(chalk23.cyan(`
5107
+ console.log(chalk26.cyan(`
4399
5108
  \uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
4400
5109
  `));
4401
5110
  memories.forEach((m, index) => {
4402
5111
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4403
- console.log(chalk23.white(` [${index + 1}] ${m.content}`));
5112
+ console.log(chalk26.white(` [${index + 1}] ${m.content}`));
4404
5113
  if (m.tags && m.tags.length > 0) {
4405
- console.log(chalk23.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
5114
+ console.log(chalk26.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
4406
5115
  }
4407
- console.log(chalk23.gray(` \u{1F4C5} ${date}`));
5116
+ console.log(chalk26.gray(` \u{1F4C5} ${date}`));
4408
5117
  console.log("");
4409
5118
  });
4410
5119
  }
@@ -4412,26 +5121,44 @@ async function memoryRemove(indexStr) {
4412
5121
  const memories = loadMemories();
4413
5122
  const idx = parseInt(indexStr, 10) - 1;
4414
5123
  if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
4415
- console.log(chalk23.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
5124
+ console.log(chalk26.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
4416
5125
  return;
4417
5126
  }
4418
5127
  const removed = memories.splice(idx, 1)[0];
4419
5128
  saveMemories(memories);
4420
- console.log(chalk23.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
4421
- console.log(chalk23.gray(` ${removed.content}`));
5129
+ console.log(chalk26.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
5130
+ console.log(chalk26.gray(` ${removed.content}`));
4422
5131
  }
4423
5132
 
4424
5133
  // src/commands/brief.ts
4425
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4426
- import chalk24 from "chalk";
5134
+ import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync6 } from "fs";
5135
+ import chalk27 from "chalk";
4427
5136
  var BRIEF_PATH = ".vhk/brief.md";
5137
+ function readProjectIdentity() {
5138
+ const out = {};
5139
+ try {
5140
+ if (existsSync14("RULES.md")) {
5141
+ const r = readFileSync6("RULES.md", "utf-8");
5142
+ const m = r.split("\n")[0].match(/^#\s*(.+?)(?:\s*—.*)?$/);
5143
+ if (m) out.name = m[1].trim();
5144
+ const d = r.match(/한 줄 설명:\s*(.+)/);
5145
+ if (d) out.description = d[1].trim();
5146
+ }
5147
+ if (!out.name && existsSync14("CLAUDE.md")) {
5148
+ const m = readFileSync6("CLAUDE.md", "utf-8").match(/#\s*기록 규칙\s*\((.+?)\)/);
5149
+ if (m) out.name = m[1].trim();
5150
+ }
5151
+ } catch {
5152
+ }
5153
+ return out;
5154
+ }
4428
5155
  function git2(args) {
4429
5156
  const result = safeExecFile("git", args);
4430
5157
  return result.ok ? result.out : "";
4431
5158
  }
4432
5159
  async function brief() {
4433
- console.log(chalk24.bold("\n\u{1F4CB} " + t("brief.title")));
4434
- console.log(chalk24.gray("\u2500".repeat(40)));
5160
+ console.log(chalk27.bold("\n\u{1F4CB} " + t("brief.title")));
5161
+ console.log(chalk27.gray("\u2500".repeat(40)));
4435
5162
  const lines = [];
4436
5163
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
4437
5164
  lines.push("");
@@ -4439,11 +5166,12 @@ async function brief() {
4439
5166
  lines.push("");
4440
5167
  try {
4441
5168
  const pkg = readJsonFile("package.json");
5169
+ const id = readProjectIdentity();
4442
5170
  lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
4443
5171
  lines.push("");
4444
- lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
5172
+ lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
4445
5173
  lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
4446
- lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
5174
+ lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
4447
5175
  const deps = Object.keys(pkg.dependencies ?? {}).length;
4448
5176
  const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
4449
5177
  lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
@@ -4467,7 +5195,7 @@ async function brief() {
4467
5195
  `- **\uBBF8\uCEE4\uBC0B \uBCC0\uACBD**: ${uncommitted ? `${uncommitted.split("\n").length}\uAC1C \uD30C\uC77C` : "\uC5C6\uC74C \u2705"}`
4468
5196
  );
4469
5197
  lines.push("");
4470
- if (existsSync13(".vhk/memory.json")) {
5198
+ if (existsSync14(".vhk/memory.json")) {
4471
5199
  try {
4472
5200
  const memories = readJsonFile(".vhk/memory.json");
4473
5201
  if (Array.isArray(memories) && memories.length > 0) {
@@ -4484,7 +5212,7 @@ async function brief() {
4484
5212
  } catch {
4485
5213
  }
4486
5214
  }
4487
- if (existsSync13(".vhk/refs.json")) {
5215
+ if (existsSync14(".vhk/refs.json")) {
4488
5216
  try {
4489
5217
  const refs = readJsonFile(".vhk/refs.json");
4490
5218
  if (Array.isArray(refs) && refs.length > 0) {
@@ -4515,7 +5243,7 @@ async function brief() {
4515
5243
  mkdirSync9(".vhk", { recursive: true });
4516
5244
  writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
4517
5245
  console.log("\n" + lines.join("\n"));
4518
- console.log(chalk24.green(`
5246
+ console.log(chalk27.green(`
4519
5247
  \u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
4520
5248
  printNextStep({
4521
5249
  message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
@@ -4525,11 +5253,11 @@ async function brief() {
4525
5253
  }
4526
5254
 
4527
5255
  // src/commands/start.ts
4528
- import chalk25 from "chalk";
4529
- import inquirer10 from "inquirer";
5256
+ import chalk28 from "chalk";
5257
+ import inquirer12 from "inquirer";
4530
5258
  import { simpleGit as simpleGit2 } from "simple-git";
4531
- import { existsSync as existsSync14 } from "fs";
4532
- import { join as join7 } from "path";
5259
+ import { existsSync as existsSync15 } from "fs";
5260
+ import { join as join9 } from "path";
4533
5261
  var VHK_FOOTPRINT_FILES = [
4534
5262
  "CLAUDE.md",
4535
5263
  ".cursorrules",
@@ -4538,7 +5266,7 @@ var VHK_FOOTPRINT_FILES = [
4538
5266
  "docs/PRD.md"
4539
5267
  ];
4540
5268
  function detectExistingFootprint(cwd) {
4541
- return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join7(cwd, rel)));
5269
+ return VHK_FOOTPRINT_FILES.filter((rel) => existsSync15(join9(cwd, rel)));
4542
5270
  }
4543
5271
  async function runGitInit(cwd) {
4544
5272
  try {
@@ -4567,22 +5295,22 @@ async function runStep(label, fn) {
4567
5295
  }
4568
5296
  }
4569
5297
  async function start(options = {}) {
4570
- console.log(chalk25.bold(`
5298
+ console.log(chalk28.bold(`
4571
5299
  ${ko.start.title}
4572
5300
  `));
4573
- console.log(chalk25.dim(ko.start.intro));
4574
- console.log(chalk25.dim(` ${ko.start.step1}`));
4575
- console.log(chalk25.dim(` ${ko.start.step2}`));
4576
- console.log(chalk25.dim(` ${ko.start.step3}`));
4577
- console.log(chalk25.dim(` ${ko.start.step4}`));
5301
+ console.log(chalk28.dim(ko.start.intro));
5302
+ console.log(chalk28.dim(` ${ko.start.step1}`));
5303
+ console.log(chalk28.dim(` ${ko.start.step2}`));
5304
+ console.log(chalk28.dim(` ${ko.start.step3}`));
5305
+ console.log(chalk28.dim(` ${ko.start.step4}`));
4578
5306
  console.log();
4579
5307
  const cwd = process.cwd();
4580
5308
  const footprint = detectExistingFootprint(cwd);
4581
5309
  if (footprint.length > 0 && !options.yes) {
4582
- console.log(chalk25.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
4583
- for (const f of footprint) console.log(chalk25.dim(` - ${f}`));
4584
- console.log(chalk25.dim(" \uACC4\uC18D \uC9C4\uD589\uD558\uBA74 \uC77C\uBD80 \uD30C\uC77C(`.cursor/mcp.json`, `.vhk/context.md`)\uC740 \uAC31\uC2E0\xB7\uB36E\uC5B4\uC4F0\uAE30\uB429\uB2C8\uB2E4."));
4585
- const { proceedExisting } = await inquirer10.prompt([{
5310
+ console.log(chalk28.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
5311
+ for (const f of footprint) console.log(chalk28.dim(` - ${f}`));
5312
+ console.log(chalk28.dim(" \uACC4\uC18D \uC9C4\uD589\uD558\uBA74 \uC77C\uBD80 \uD30C\uC77C(`.cursor/mcp.json`, `.vhk/context.md`)\uC740 \uAC31\uC2E0\xB7\uB36E\uC5B4\uC4F0\uAE30\uB429\uB2C8\uB2E4."));
5313
+ const { proceedExisting } = await inquirer12.prompt([{
4586
5314
  type: "confirm",
4587
5315
  name: "proceedExisting",
4588
5316
  message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
@@ -4593,7 +5321,7 @@ ${ko.start.title}
4593
5321
  return;
4594
5322
  }
4595
5323
  } else if (!options.yes) {
4596
- const { proceed } = await inquirer10.prompt([{
5324
+ const { proceed } = await inquirer12.prompt([{
4597
5325
  type: "confirm",
4598
5326
  name: "proceed",
4599
5327
  message: ko.start.confirmStart,
@@ -4619,7 +5347,7 @@ ${ko.start.title}
4619
5347
  await runStep("[3/4] vhk mcp-init", () => mcpInit());
4620
5348
  log.step(ko.start.step4Header);
4621
5349
  await runStep("[4/4] vhk context", () => context());
4622
- console.log(chalk25.bold.green(`
5350
+ console.log(chalk28.bold.green(`
4623
5351
  ${ko.start.allDone}
4624
5352
  `));
4625
5353
  printNextStep({
@@ -4629,15 +5357,15 @@ ${ko.start.allDone}
4629
5357
  }
4630
5358
 
4631
5359
  // src/commands/cloud.ts
4632
- import fs14 from "fs";
5360
+ import fs15 from "fs";
4633
5361
  import os from "os";
4634
- import path15 from "path";
4635
- import chalk26 from "chalk";
5362
+ import path16 from "path";
5363
+ import chalk29 from "chalk";
4636
5364
 
4637
5365
  // src/lib/vhk-cloud.ts
4638
5366
  var import_ignore = __toESM(require_ignore(), 1);
4639
- import fs13 from "fs";
4640
- import path14 from "path";
5367
+ import fs14 from "fs";
5368
+ import path15 from "path";
4641
5369
  var DEFAULT_CLOUD_EXCLUDES = [
4642
5370
  "memory.json",
4643
5371
  // 개인 의사결정 메모
@@ -4655,17 +5383,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
4655
5383
  function loadVhkignore(rootDir) {
4656
5384
  const ig = (0, import_ignore.default)();
4657
5385
  ig.add(DEFAULT_CLOUD_EXCLUDES);
4658
- const ignorePath = path14.join(rootDir, ".vhkignore");
4659
- if (fs13.existsSync(ignorePath)) {
4660
- ig.add(fs13.readFileSync(ignorePath, "utf-8"));
5386
+ const ignorePath = path15.join(rootDir, ".vhkignore");
5387
+ if (fs14.existsSync(ignorePath)) {
5388
+ ig.add(fs14.readFileSync(ignorePath, "utf-8"));
4661
5389
  }
4662
5390
  return ig;
4663
5391
  }
4664
5392
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
4665
- const vhkDir = path14.join(rootDir, VHK_DIR2);
5393
+ const vhkDir = path15.join(rootDir, VHK_DIR2);
4666
5394
  let entries;
4667
5395
  try {
4668
- entries = fs13.readdirSync(vhkDir, { withFileTypes: true });
5396
+ entries = fs14.readdirSync(vhkDir, { withFileTypes: true });
4669
5397
  } catch {
4670
5398
  return [];
4671
5399
  }
@@ -4681,10 +5409,10 @@ function partitionGistFiles(gistFiles, ig) {
4681
5409
  return { keep, excluded };
4682
5410
  }
4683
5411
  function readCloudConfig(rootDir) {
4684
- const p = path14.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4685
- if (!fs13.existsSync(p)) return null;
5412
+ const p = path15.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5413
+ if (!fs14.existsSync(p)) return null;
4686
5414
  try {
4687
- const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
5415
+ const parsed = JSON.parse(fs14.readFileSync(p, "utf-8"));
4688
5416
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
4689
5417
  return { gistId: parsed.gistId };
4690
5418
  }
@@ -4694,24 +5422,24 @@ function readCloudConfig(rootDir) {
4694
5422
  }
4695
5423
  }
4696
5424
  function writeCloudConfig(rootDir, config) {
4697
- const vhkDir = path14.join(rootDir, VHK_DIR2);
4698
- fs13.mkdirSync(vhkDir, { recursive: true });
4699
- const p = path14.join(vhkDir, CLOUD_CONFIG_FILE);
4700
- fs13.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
5425
+ const vhkDir = path15.join(rootDir, VHK_DIR2);
5426
+ fs14.mkdirSync(vhkDir, { recursive: true });
5427
+ const p = path15.join(vhkDir, CLOUD_CONFIG_FILE);
5428
+ fs14.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
4701
5429
  }
4702
5430
 
4703
5431
  // src/commands/cloud.ts
4704
5432
  function ensureGhReady() {
4705
5433
  const ver = safeExecFile("gh", ["--version"]);
4706
5434
  if (!ver.ok) {
4707
- console.log(chalk26.red(` ${ko.cloud.noGh}`));
4708
- console.log(chalk26.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
5435
+ console.log(chalk29.red(` ${ko.cloud.noGh}`));
5436
+ console.log(chalk29.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
4709
5437
  return false;
4710
5438
  }
4711
5439
  const auth = safeExecFile("gh", ["auth", "status"]);
4712
5440
  if (!auth.ok) {
4713
- console.log(chalk26.red(` ${ko.cloud.noAuth}`));
4714
- console.log(chalk26.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
5441
+ console.log(chalk29.red(` ${ko.cloud.noAuth}`));
5442
+ console.log(chalk29.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
4715
5443
  return false;
4716
5444
  }
4717
5445
  return true;
@@ -4724,29 +5452,29 @@ function parseGistId(output) {
4724
5452
  return null;
4725
5453
  }
4726
5454
  async function cloudPush() {
4727
- console.log(chalk26.bold(`
5455
+ console.log(chalk29.bold(`
4728
5456
  ${ko.cloud.pushTitle}
4729
5457
  `));
4730
5458
  const cwd = process.cwd();
4731
- if (!fs14.existsSync(path15.join(cwd, VHK_DIR2))) {
4732
- console.log(chalk26.yellow(` ${ko.cloud.noVhkDir}`));
5459
+ if (!fs15.existsSync(path16.join(cwd, VHK_DIR2))) {
5460
+ console.log(chalk29.yellow(` ${ko.cloud.noVhkDir}`));
4733
5461
  return;
4734
5462
  }
4735
5463
  const ig = loadVhkignore(cwd);
4736
5464
  const files = collectVhkFiles(cwd, ig);
4737
5465
  if (files.length === 0) {
4738
- console.log(chalk26.yellow(` ${ko.cloud.nothingToSync}`));
5466
+ console.log(chalk29.yellow(` ${ko.cloud.nothingToSync}`));
4739
5467
  return;
4740
5468
  }
4741
5469
  if (!ensureGhReady()) {
4742
5470
  process.exitCode = 1;
4743
5471
  return;
4744
5472
  }
4745
- const filePaths = files.map((f) => path15.join(cwd, VHK_DIR2, f));
4746
- console.log(chalk26.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
5473
+ const filePaths = files.map((f) => path16.join(cwd, VHK_DIR2, f));
5474
+ console.log(chalk29.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
4747
5475
  `));
4748
5476
  const existing = readCloudConfig(cwd);
4749
- const desc = `vhk .vhk backup \u2014 ${path15.basename(cwd)}`;
5477
+ const desc = `vhk .vhk backup \u2014 ${path16.basename(cwd)}`;
4750
5478
  if (existing) {
4751
5479
  const gistFiles = listGistFiles(existing.gistId);
4752
5480
  for (let i = 0; i < files.length; i++) {
@@ -4755,8 +5483,8 @@ ${ko.cloud.pushTitle}
4755
5483
  const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
4756
5484
  const res2 = safeExecFile("gh", args);
4757
5485
  if (!res2.ok) {
4758
- console.log(chalk26.red(` ${ko.cloud.pushFail}: ${name}`));
4759
- console.log(chalk26.dim(` ${res2.err}`));
5486
+ console.log(chalk29.red(` ${ko.cloud.pushFail}: ${name}`));
5487
+ console.log(chalk29.dim(` ${res2.err}`));
4760
5488
  process.exitCode = 1;
4761
5489
  return;
4762
5490
  }
@@ -4771,15 +5499,15 @@ ${ko.cloud.pushTitle}
4771
5499
  if (!purgeFailed.includes(name)) purgeFailed.push(name);
4772
5500
  }
4773
5501
  }
4774
- console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4775
- console.log(chalk26.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
5502
+ console.log(chalk29.green.bold(` ${ko.cloud.pushDone}`));
5503
+ console.log(chalk29.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
4776
5504
  if (excluded.length > 0) {
4777
5505
  const purged = excluded.filter((n) => !purgeFailed.includes(n));
4778
5506
  if (purged.length > 0) {
4779
- console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
5507
+ console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
4780
5508
  }
4781
5509
  if (purgeFailed.length > 0) {
4782
- console.log(chalk26.yellow(` \u26A0\uFE0F \uC81C\uC678 \uB300\uC0C1 \uC81C\uAC70 \uC2E4\uD328: ${purgeFailed.join(", ")} (\uC218\uB3D9 \uC81C\uAC70 \uAD8C\uC7A5 \u2014 pull \uC2DC\uC5D4 \uBCF5\uC6D0 \uC548 \uB428)`));
5510
+ console.log(chalk29.yellow(` \u26A0\uFE0F \uC81C\uC678 \uB300\uC0C1 \uC81C\uAC70 \uC2E4\uD328: ${purgeFailed.join(", ")} (\uC218\uB3D9 \uC81C\uAC70 \uAD8C\uC7A5 \u2014 pull \uC2DC\uC5D4 \uBCF5\uC6D0 \uC548 \uB428)`));
4783
5511
  }
4784
5512
  }
4785
5513
  printPushNext();
@@ -4787,32 +5515,32 @@ ${ko.cloud.pushTitle}
4787
5515
  }
4788
5516
  const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
4789
5517
  if (!res.ok) {
4790
- console.log(chalk26.red(` ${ko.cloud.pushFail}`));
4791
- console.log(chalk26.dim(` ${res.err || res.out}`));
5518
+ console.log(chalk29.red(` ${ko.cloud.pushFail}`));
5519
+ console.log(chalk29.dim(` ${res.err || res.out}`));
4792
5520
  process.exitCode = 1;
4793
5521
  return;
4794
5522
  }
4795
5523
  const gistId = parseGistId(res.out);
4796
5524
  if (!gistId) {
4797
- console.log(chalk26.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
4798
- console.log(chalk26.dim(` \uCD9C\uB825: ${res.out}`));
5525
+ console.log(chalk29.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
5526
+ console.log(chalk29.dim(` \uCD9C\uB825: ${res.out}`));
4799
5527
  process.exitCode = 1;
4800
5528
  return;
4801
5529
  }
4802
5530
  writeCloudConfig(cwd, { gistId });
4803
- console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4804
- console.log(chalk26.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
5531
+ console.log(chalk29.green.bold(` ${ko.cloud.pushDone}`));
5532
+ console.log(chalk29.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
4805
5533
  printPushNext();
4806
5534
  }
4807
5535
  async function cloudPull(gistIdArg) {
4808
- console.log(chalk26.bold(`
5536
+ console.log(chalk29.bold(`
4809
5537
  ${ko.cloud.pullTitle}
4810
5538
  `));
4811
5539
  const cwd = process.cwd();
4812
5540
  const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
4813
5541
  if (!gistId) {
4814
- console.log(chalk26.yellow(` ${ko.cloud.noGistId}`));
4815
- console.log(chalk26.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
5542
+ console.log(chalk29.yellow(` ${ko.cloud.noGistId}`));
5543
+ console.log(chalk29.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
4816
5544
  return;
4817
5545
  }
4818
5546
  if (!ensureGhReady()) {
@@ -4821,34 +5549,34 @@ ${ko.cloud.pullTitle}
4821
5549
  }
4822
5550
  const allNames = listGistFiles(gistId);
4823
5551
  if (allNames.length === 0) {
4824
- console.log(chalk26.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
5552
+ console.log(chalk29.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
4825
5553
  process.exitCode = 1;
4826
5554
  return;
4827
5555
  }
4828
5556
  const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
4829
5557
  if (skipped.length > 0) {
4830
- console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
5558
+ console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
4831
5559
  }
4832
5560
  if (names.length === 0) {
4833
- console.log(chalk26.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
5561
+ console.log(chalk29.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
4834
5562
  return;
4835
5563
  }
4836
- const vhkDir = path15.join(cwd, VHK_DIR2);
4837
- fs14.mkdirSync(vhkDir, { recursive: true });
5564
+ const vhkDir = path16.join(cwd, VHK_DIR2);
5565
+ fs15.mkdirSync(vhkDir, { recursive: true });
4838
5566
  let restored = 0;
4839
5567
  for (const name of names) {
4840
5568
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
4841
5569
  if (!res.ok) {
4842
- console.log(chalk26.red(` ${ko.cloud.pullFail}: ${name}`));
4843
- console.log(chalk26.dim(` ${res.err}`));
5570
+ console.log(chalk29.red(` ${ko.cloud.pullFail}: ${name}`));
5571
+ console.log(chalk29.dim(` ${res.err}`));
4844
5572
  continue;
4845
5573
  }
4846
- fs14.writeFileSync(path15.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
5574
+ fs15.writeFileSync(path16.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4847
5575
  restored++;
4848
5576
  }
4849
5577
  writeCloudConfig(cwd, { gistId });
4850
- console.log(chalk26.green.bold(` ${ko.cloud.pullDone}`));
4851
- console.log(chalk26.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
5578
+ console.log(chalk29.green.bold(` ${ko.cloud.pullDone}`));
5579
+ console.log(chalk29.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
4852
5580
  printNextStep({
4853
5581
  message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
4854
5582
  command: "vhk \uB9E5\uB77D",
@@ -4860,9 +5588,9 @@ function purgeExcludedFromGist(gistId, names) {
4860
5588
  const body = JSON.stringify({
4861
5589
  files: Object.fromEntries(names.map((n) => [n, null]))
4862
5590
  });
4863
- const tmp = path15.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
5591
+ const tmp = path16.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
4864
5592
  try {
4865
- fs14.writeFileSync(tmp, body, "utf-8");
5593
+ fs15.writeFileSync(tmp, body, "utf-8");
4866
5594
  for (let attempt = 0; attempt < 2; attempt++) {
4867
5595
  const res = safeExecFile(
4868
5596
  "gh",
@@ -4874,7 +5602,7 @@ function purgeExcludedFromGist(gistId, names) {
4874
5602
  return false;
4875
5603
  } finally {
4876
5604
  try {
4877
- fs14.unlinkSync(tmp);
5605
+ fs15.unlinkSync(tmp);
4878
5606
  } catch {
4879
5607
  }
4880
5608
  }
@@ -4895,6 +5623,198 @@ function printPushNext() {
4895
5623
  });
4896
5624
  }
4897
5625
 
5626
+ // src/commands/help.ts
5627
+ import chalk30 from "chalk";
5628
+ var QUICK_ACTIONS = [
5629
+ { say: "\uC0C1\uD0DC \uC54C\uB824\uC918", does: "vhk status" },
5630
+ { say: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?", does: "vhk diff" },
5631
+ { say: "\uC800\uC7A5\uD574\uC918", does: "vhk save" },
5632
+ { say: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574\uC918", does: "vhk recap" },
5633
+ { say: "\uB2E4\uC74C\uC5D0 \uBB50 \uD558\uBA74 \uB3FC?", does: "vhk goal next" },
5634
+ { say: "\uADDC\uCE59 \uB3D9\uAE30\uD654\uD574\uC918", does: "vhk sync" },
5635
+ { say: "\uBC31\uC5C5 \uBCF5\uC6D0\uD574\uC918", does: "vhk restore" },
5636
+ { say: "\uBCF4\uC548 \uC810\uAC80\uD574\uC918", does: "vhk secure scan" },
5637
+ { say: "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791", does: "vhk start" },
5638
+ { say: "\uC804\uCCB4 \uBA85\uB839\uC5B4 \uBCF4\uAE30", does: "vhk --help" }
5639
+ ];
5640
+ function quickActions() {
5641
+ console.log(chalk30.bold("\n\u{1F9ED} VHK \u2014 \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4 (quick actions)"));
5642
+ console.log(chalk30.gray("\u2500".repeat(40)));
5643
+ for (const a of QUICK_ACTIONS) {
5644
+ console.log(` "${chalk30.cyan(a.say)}" \u2192 ${chalk30.dim(a.does)}`);
5645
+ }
5646
+ console.log(chalk30.gray("\n \uC804\uCCB4 \uBA85\uB839\uC740 `vhk --help` \uB610\uB294 COMMANDS.md \uB97C \uBCF4\uC138\uC694."));
5647
+ console.log("");
5648
+ }
5649
+
5650
+ // src/commands/mode.ts
5651
+ import chalk31 from "chalk";
5652
+
5653
+ // src/lib/config.ts
5654
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
5655
+ import { join as join10 } from "path";
5656
+
5657
+ // src/lib/safety-mode.ts
5658
+ var SAFETY_MODES = ["lite", "standard", "strict"];
5659
+ var DEFAULT_SAFETY_MODE = "standard";
5660
+ var SAFETY_MODE_DESC = {
5661
+ lite: "\uACBD\uACE0\uB9CC \u2014 \uC704\uD5D8 \uC791\uC5C5\uB3C4 \uB9C9\uC9C0 \uC54A\uACE0 \uACBD\uACE0\uB9CC \uD45C\uC2DC (\uBE60\uB978 \uBC18\uBCF5\uC6A9)",
5662
+ standard: "\uAE30\uBCF8 \u2014 \uC704\uD5D8 \uC791\uC5C5\uC740 CLI \uD655\uC778\xB7MCP/\uC790\uC5F0\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30",
5663
+ strict: "\uC5C4\uACA9 \u2014 \uB354 \uB9CE\uC740 \uC791\uC5C5(\uC800\uC7A5/\uB3D9\uAE30\uD654 \uB4F1)\uC5D0\uB3C4 \uD655\uC778 \uC694\uAD6C"
5664
+ };
5665
+ function isSafetyMode(value) {
5666
+ return typeof value === "string" && SAFETY_MODES.includes(value);
5667
+ }
5668
+
5669
+ // src/lib/config.ts
5670
+ var CONFIG_DIR = ".vhk";
5671
+ var CONFIG_PATH = join10(CONFIG_DIR, "config.json");
5672
+ var DEFAULT_CONFIG = { safetyMode: DEFAULT_SAFETY_MODE };
5673
+ function readConfig(rootDir = process.cwd()) {
5674
+ const full = join10(rootDir, CONFIG_PATH);
5675
+ if (!existsSync16(full)) return { ...DEFAULT_CONFIG };
5676
+ try {
5677
+ const raw = readJsonFile(full);
5678
+ return {
5679
+ safetyMode: isSafetyMode(raw.safetyMode) ? raw.safetyMode : DEFAULT_CONFIG.safetyMode
5680
+ };
5681
+ } catch {
5682
+ return { ...DEFAULT_CONFIG };
5683
+ }
5684
+ }
5685
+ function writeConfig(config, rootDir = process.cwd()) {
5686
+ mkdirSync10(join10(rootDir, CONFIG_DIR), { recursive: true });
5687
+ writeFileSync10(join10(rootDir, CONFIG_PATH), JSON.stringify(config, null, 2) + "\n", "utf-8");
5688
+ }
5689
+
5690
+ // src/commands/mode.ts
5691
+ async function mode(target) {
5692
+ console.log(chalk31.bold("\n\u{1F6E1}\uFE0F Safety Mode"));
5693
+ console.log(chalk31.gray("\u2500".repeat(40)));
5694
+ const current = readConfig().safetyMode;
5695
+ if (!target) {
5696
+ console.log(chalk31.cyan(`
5697
+ \uD604\uC7AC \uBAA8\uB4DC: ${chalk31.bold(current)}`));
5698
+ console.log(chalk31.dim(` ${SAFETY_MODE_DESC[current]}`));
5699
+ console.log("");
5700
+ for (const m of SAFETY_MODES) {
5701
+ const mark = m === current ? "\u25CF" : "\u25CB";
5702
+ console.log(` ${mark} ${m.padEnd(9)} ${chalk31.dim(SAFETY_MODE_DESC[m])}`);
5703
+ }
5704
+ printNextStep({
5705
+ message: "\uBAA8\uB4DC\uB97C \uBC14\uAFB8\uB824\uBA74:",
5706
+ command: "vhk mode strict",
5707
+ cursorHint: "\uC548\uC804 \uBAA8\uB4DC strict\uB85C \uBC14\uAFD4\uC918"
5708
+ });
5709
+ return;
5710
+ }
5711
+ if (!isSafetyMode(target)) {
5712
+ console.log(chalk31.red(`
5713
+ \u274C \uC54C \uC218 \uC5C6\uB294 \uBAA8\uB4DC: ${target}`));
5714
+ console.log(chalk31.dim(` \uAC00\uB2A5: ${SAFETY_MODES.join(" | ")}`));
5715
+ process.exitCode = 1;
5716
+ return;
5717
+ }
5718
+ writeConfig({ ...readConfig(), safetyMode: target });
5719
+ console.log(chalk31.green(`
5720
+ \u2705 Safety Mode \u2192 ${chalk31.bold(target)}`));
5721
+ console.log(chalk31.dim(` ${SAFETY_MODE_DESC[target]}`));
5722
+ }
5723
+
5724
+ // src/commands/verify.ts
5725
+ import chalk32 from "chalk";
5726
+ function verificationChecklist() {
5727
+ return [
5728
+ "\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
5729
+ "\uD14C\uC2A4\uD2B8 \u2014 pnpm run test:run",
5730
+ "\uBE4C\uB4DC \u2014 pnpm run build",
5731
+ "\uBCF4\uC548 \uC2A4\uCE94 \u2014 vhk secure scan"
5732
+ ];
5733
+ }
5734
+ async function verify() {
5735
+ console.log(chalk32.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
5736
+ console.log(chalk32.gray("\u2500".repeat(40)));
5737
+ const mode2 = readConfig().safetyMode;
5738
+ console.log(chalk32.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
5739
+ console.log(chalk32.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
5740
+ for (const item of verificationChecklist()) {
5741
+ console.log(` \u2610 ${item}`);
5742
+ }
5743
+ console.log(chalk32.dim("\n \u203B \uBA54\uD0C0\uB7EC\uB108(\uC790\uB3D9 \uC2E4\uD589) \uC790\uB9AC \u2014 \uD604\uC7AC\uB294 \uBB36\uC74C \uC548\uB0B4\uB9CC(lite)."));
5744
+ printNextStep({
5745
+ message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
5746
+ command: "vhk save",
5747
+ cursorHint: "\uC800\uC7A5\uD574\uC918"
5748
+ });
5749
+ }
5750
+
5751
+ // src/lib/risk-policy.ts
5752
+ var HIGH_RISK_ACTIONS = [
5753
+ "undo",
5754
+ "deploy",
5755
+ "publish",
5756
+ "migrate",
5757
+ "cloud-pull",
5758
+ "resume",
5759
+ "env-write",
5760
+ "delete"
5761
+ ];
5762
+ var STRICT_EXTRA_ACTIONS = /* @__PURE__ */ new Set(["save", "sync"]);
5763
+ var NL_GUARDED_ACTIONS = {
5764
+ undo: "undo",
5765
+ deploy: "deploy",
5766
+ publish: "publish",
5767
+ migrate: "migrate",
5768
+ "cloud-pull": "cloud-pull",
5769
+ env: "env-write",
5770
+ save: "save",
5771
+ sync: "sync"
5772
+ };
5773
+ function isHighRisk(action) {
5774
+ return HIGH_RISK_ACTIONS.includes(action);
5775
+ }
5776
+ function resolveGuard(action, mode2, channel) {
5777
+ const guarded = isHighRisk(action) || mode2 === "strict" && STRICT_EXTRA_ACTIONS.has(action);
5778
+ if (!guarded) return "allow";
5779
+ if (mode2 === "lite") return "warn";
5780
+ return channel === "cli" ? "confirm" : "preview";
5781
+ }
5782
+
5783
+ // src/lib/safety-guard.ts
5784
+ async function runGuarded(action, deps, run) {
5785
+ const mode2 = deps.mode ?? readConfig().safetyMode;
5786
+ const log2 = deps.log ?? (() => {
5787
+ });
5788
+ const guard = resolveGuard(action, mode2, deps.channel);
5789
+ if (guard === "allow") {
5790
+ return { outcome: { ran: true, guard, reason: "low-risk" }, result: await run() };
5791
+ }
5792
+ if (guard === "warn") {
5793
+ log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uBAA8\uB4DC: \uACBD\uACE0\uB9CC \uD558\uACE0 \uC9C4\uD589\uD569\uB2C8\uB2E4.`);
5794
+ return { outcome: { ran: true, guard, reason: "lite-warn" }, result: await run() };
5795
+ }
5796
+ if (guard === "confirm") {
5797
+ if (deps.approved === true) {
5798
+ return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
5799
+ }
5800
+ const tty = deps.isTTY ?? !!process.stdout.isTTY;
5801
+ if (tty && deps.confirm) {
5802
+ const ok = await deps.confirm();
5803
+ if (ok) return { outcome: { ran: true, guard, reason: "confirmed" }, result: await run() };
5804
+ log2(`\uCDE8\uC18C\uB428 \u2014 ${action} \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
5805
+ return { outcome: { ran: false, guard, reason: "declined" } };
5806
+ }
5807
+ log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 \uD655\uC778 \uBD88\uAC00(\uBE44\uB300\uD654\uD615). \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (--yes \uB85C \uBA85\uC2DC \uC2B9\uC778)`);
5808
+ return { outcome: { ran: false, guard, reason: "no-confirm" } };
5809
+ }
5810
+ log2(`\u{1F50E} \uC704\uD5D8 \uC791\uC5C5(${action}) \uBBF8\uB9AC\uBCF4\uAE30 \u2014 \uC2E4\uD589 \uC804 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (Safety Mode: ${mode2})`);
5811
+ if (deps.approved === true) {
5812
+ return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
5813
+ }
5814
+ log2(`\uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uBA85\uC2DC\uC801 \uD655\uC778(\uC2B9\uC778 \uD50C\uB798\uADF8) \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`);
5815
+ return { outcome: { ran: false, guard, reason: "preview-no-approve" } };
5816
+ }
5817
+
4898
5818
  // src/lib/nlp-run.ts
4899
5819
  async function dispatchNlpRoute(route, input) {
4900
5820
  switch (route.command) {
@@ -4925,6 +5845,8 @@ async function dispatchNlpRoute(route, input) {
4925
5845
  return save();
4926
5846
  case "undo":
4927
5847
  return undo();
5848
+ case "restore":
5849
+ return restore(route.args?.[0]);
4928
5850
  case "status":
4929
5851
  return status();
4930
5852
  case "diff":
@@ -4974,111 +5896,167 @@ async function dispatchNlpRoute(route, input) {
4974
5896
  if (sub === "done") return goalDone({});
4975
5897
  return goalList();
4976
5898
  }
5899
+ case "help":
5900
+ return quickActions();
5901
+ case "mode":
5902
+ return mode();
5903
+ case "verify":
5904
+ return verify();
4977
5905
  }
4978
5906
  }
5907
+ var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
5908
+ "start",
5909
+ "init"
5910
+ ]);
5911
+ function requiresConfirmation(route) {
5912
+ return route.confidence === "low" || STATE_CHANGING_COMMANDS.has(route.command);
5913
+ }
4979
5914
  async function runNaturalLanguageRoute(input) {
4980
5915
  const route = routeNaturalLanguage(input);
4981
5916
  if (!route) {
4982
- console.log(chalk27.yellow(`
5917
+ console.log(chalk33.yellow(`
4983
5918
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
4984
5919
  `));
4985
5920
  return;
4986
5921
  }
4987
5922
  console.log("");
4988
- console.log(chalk27.cyan(` \u{1F4AC} "${input}"`));
4989
- console.log(chalk27.cyan(` \u2192 ${route.explanation}`));
4990
- if (route.confidence === "low") {
4991
- const { confirm } = await inquirer11.prompt([{
5923
+ console.log(chalk33.cyan(` \u{1F4AC} "${input}"`));
5924
+ console.log(chalk33.cyan(` \u2192 ${route.explanation}`));
5925
+ if (requiresConfirmation(route)) {
5926
+ const { confirm } = await inquirer13.prompt([{
4992
5927
  type: "confirm",
4993
5928
  name: "confirm",
4994
5929
  message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
4995
5930
  default: true
4996
5931
  }]);
4997
5932
  if (!confirm) {
4998
- console.log(chalk27.dim(` ${ko.nlp.menuHint}`));
5933
+ console.log(chalk33.dim(` ${ko.nlp.menuHint}`));
4999
5934
  return;
5000
5935
  }
5001
5936
  }
5002
5937
  console.log("");
5938
+ const riskAction = NL_GUARDED_ACTIONS[route.command];
5939
+ if (riskAction) {
5940
+ await runGuarded(
5941
+ riskAction,
5942
+ { channel: "nl", approved: false, log: (m) => console.log(chalk33.yellow(` ${m}`)) },
5943
+ () => dispatchNlpRoute(route, input)
5944
+ );
5945
+ return;
5946
+ }
5003
5947
  await dispatchNlpRoute(route, input);
5004
5948
  }
5005
5949
 
5006
5950
  // src/commands/agent.ts
5007
- import chalk28 from "chalk";
5951
+ import chalk34 from "chalk";
5008
5952
  function activeGoalId() {
5009
5953
  const goals = listGoals("goals");
5010
5954
  const id = selectActiveId(goals);
5011
5955
  return id ?? void 0;
5012
5956
  }
5013
5957
  async function blocker(description) {
5014
- console.log(chalk28.bold(`
5958
+ console.log(chalk34.bold(`
5015
5959
  ${ko.agent.blockerTitle}
5016
5960
  `));
5017
5961
  if (!description || !description.trim()) {
5018
- console.log(chalk28.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5019
- console.log(chalk28.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
5962
+ console.log(chalk34.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5963
+ console.log(chalk34.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
5020
5964
  process.exitCode = 1;
5021
5965
  return;
5022
5966
  }
5023
5967
  const goalId = activeGoalId();
5024
5968
  const r = appendBlocker(description, goalId);
5025
- console.log(chalk28.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
5969
+ console.log(chalk34.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
5026
5970
  if (r.hardStopTripped) {
5027
- console.log(chalk28.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5028
- console.log(chalk28.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
5971
+ console.log(chalk34.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5972
+ console.log(chalk34.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
5029
5973
  process.exitCode = 2;
5030
5974
  }
5031
5975
  }
5032
5976
  async function learn(lesson) {
5033
- console.log(chalk28.bold(`
5977
+ console.log(chalk34.bold(`
5034
5978
  ${ko.agent.learnTitle}
5035
5979
  `));
5036
5980
  if (!lesson || !lesson.trim()) {
5037
- console.log(chalk28.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5038
- console.log(chalk28.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
5981
+ console.log(chalk34.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5982
+ console.log(chalk34.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
5039
5983
  process.exitCode = 1;
5040
5984
  return;
5041
5985
  }
5042
5986
  const goalId = activeGoalId();
5043
5987
  appendLearning(lesson, goalId);
5044
- console.log(chalk28.green(" \u2705 learnings.md append."));
5988
+ console.log(chalk34.green(" \u2705 learnings.md append."));
5045
5989
  console.log(
5046
- chalk28.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
5990
+ chalk34.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
5047
5991
  );
5048
5992
  }
5049
5993
  async function resume(opts = {}) {
5050
- console.log(chalk28.bold(`
5994
+ console.log(chalk34.bold(`
5051
5995
  ${ko.agent.resumeTitle}
5052
5996
  `));
5053
5997
  if (!isHardStopActive()) {
5054
- console.log(chalk28.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
5998
+ console.log(chalk34.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
5055
5999
  return;
5056
6000
  }
5057
6001
  const reason = readHardStopReason();
5058
6002
  if (reason) {
5059
- console.log(chalk28.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5060
- console.log(chalk28.dim(` ${reason.split("\n").join("\n ")}`));
6003
+ console.log(chalk34.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
6004
+ console.log(chalk34.dim(` ${reason.split("\n").join("\n ")}`));
5061
6005
  console.log("");
5062
6006
  }
5063
6007
  if (!opts.confirm) {
5064
6008
  console.log(
5065
- chalk28.red(
6009
+ chalk34.red(
5066
6010
  " \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
5067
6011
  )
5068
6012
  );
5069
- console.log(chalk28.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
6013
+ console.log(chalk34.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
5070
6014
  process.exitCode = 1;
5071
6015
  return;
5072
6016
  }
5073
6017
  const removed = clearHardStop();
5074
6018
  if (removed) {
5075
- console.log(chalk28.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
6019
+ console.log(chalk34.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
5076
6020
  } else {
5077
- console.log(chalk28.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
6021
+ console.log(chalk34.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
5078
6022
  }
5079
6023
  }
5080
6024
 
5081
6025
  // src/index.ts
6026
+ async function guardCli(action, approved, run) {
6027
+ if (!ensureNotHardStopped(action)) return;
6028
+ await runGuarded(
6029
+ action,
6030
+ {
6031
+ channel: "cli",
6032
+ approved,
6033
+ confirm: async () => {
6034
+ const { ok } = await inquirer14.prompt([{
6035
+ type: "confirm",
6036
+ name: "ok",
6037
+ message: `\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action})\uC744 \uC2E4\uD589\uD560\uAE4C\uC694?`,
6038
+ default: false
6039
+ }]);
6040
+ return ok;
6041
+ },
6042
+ log: (m) => console.log(chalk35.yellow(` ${m}`))
6043
+ },
6044
+ run
6045
+ );
6046
+ }
6047
+ async function guardCliDefer(action, approved, run) {
6048
+ await runGuarded(
6049
+ action,
6050
+ {
6051
+ channel: "cli",
6052
+ approved,
6053
+ // TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
6054
+ confirm: async () => !!process.stdout.isTTY,
6055
+ log: (m) => console.log(chalk35.yellow(` ${m}`))
6056
+ },
6057
+ run
6058
+ );
6059
+ }
5082
6060
  var program = new Command();
5083
6061
  var defaultHelp = new Help();
5084
6062
  var KO_ALIASES = {
@@ -5093,6 +6071,7 @@ var KO_ALIASES = {
5093
6071
  doctor: "\uD658\uACBD",
5094
6072
  save: "\uC800\uC7A5",
5095
6073
  undo: "\uB418\uB3CC\uB9AC\uAE30",
6074
+ restore: "\uBCF5\uC6D0",
5096
6075
  status: "\uC0C1\uD0DC",
5097
6076
  diff: "\uBCC0\uACBD",
5098
6077
  deploy: "\uBC30\uD3EC",
@@ -5144,7 +6123,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
5144
6123
  program.command("start").alias("\uC2DC\uC791").alias("\uC0C8\uD504\uB85C\uC81D\uD2B8").description("\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC \u2014 git init + \uBB38\uC11C + MCP + \uCEE8\uD14D\uC2A4\uD2B8 \uD55C \uBC88\uC5D0").option("--from-notion <url>", "Notion PRD \uD398\uC774\uC9C0\uC5D0\uC11C import").option("--name <name>", "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984").option("--description <desc>", "\uD55C \uC904 \uC124\uBA85").option("--type <type>", "\uD504\uB85C\uC81D\uD2B8 \uC720\uD615 (webapp|extension|cli|notion|mobile)").option("-y, --yes", "\uBAA8\uB4E0 \uD655\uC778 \uC2A4\uD0B5 (\uC790\uB3D9 yes)").action(start);
5145
6124
  program.command("init").alias("\uCD08\uAE30\uD654").alias("\uB9CC\uB4E4\uAE30").description("\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (git/MCP/context\uB294 \uC81C\uC678) \u2014 \uBCF4\uD1B5 vhk start \uAD8C\uC7A5").option("--skip-gate", "gate \uAC80\uC99D \uC2A4\uD0B5").option("--from-notion <url>", "Notion PRD \uD398\uC774\uC9C0\uC5D0\uC11C import").option("--name <name>", "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984").option("--description <desc>", "\uD55C \uC904 \uC124\uBA85").option("--type <type>", "\uD504\uB85C\uC81D\uD2B8 \uC720\uD615 (webapp|extension|cli|notion|mobile)").option("-y, --yes", "\uC2A4\uD0DD \uD655\uC778 \uC2A4\uD0B5").action(init);
5146
6125
  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);
5147
- program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
6126
+ program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").option("--dry-run", "\uBBF8\uB9AC\uBCF4\uAE30\uB9CC \u2014 \uD30C\uC77C \uBCC0\uACBD \uC5C6\uC74C").option("-y, --yes", "drift \uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5(\uB36E\uC5B4\uC4F0\uAE30 \uB3D9\uC758)").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654 (\uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uBC31\uC5C5)").action(async (opts) => {
6127
+ await guardCli("sync", opts?.yes === true, () => sync(opts));
6128
+ });
5148
6129
  program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").option("--goal <id>", "goal id \uC9C0\uC815 \uC2DC scripts/check-goal-<id>.sh \uAC8C\uC774\uD2B8 \uC2E4\uD589").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC (\uB610\uB294 --goal <id> \uB85C goal \uAC8C\uC774\uD2B8)").action(async (opts) => {
5149
6130
  await check(opts);
5150
6131
  });
@@ -5156,16 +6137,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
5156
6137
  cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
5157
6138
  await cloudPush();
5158
6139
  });
5159
- cloudCmd.command("pull").alias("\uB0B4\uB9AC\uAE30").argument("[gistId]", "\uBCF5\uC6D0\uD560 gist id (\uC0DD\uB7B5 \uC2DC .vhk/cloud.json \uC0AC\uC6A9)").description("gist \uC5D0\uC11C .vhk/ \uBCF5\uC6D0").action(async (gistId) => {
5160
- await cloudPull(gistId);
6140
+ cloudCmd.command("pull").alias("\uB0B4\uB9AC\uAE30").argument("[gistId]", "\uBCF5\uC6D0\uD560 gist id (\uC0DD\uB7B5 \uC2DC .vhk/cloud.json \uC0AC\uC6A9)").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("gist \uC5D0\uC11C .vhk/ \uBCF5\uC6D0").action(async (gistId, opts) => {
6141
+ await guardCli("cloud-pull", opts?.yes === true, () => cloudPull(gistId));
5161
6142
  });
5162
6143
  program.command("ship").alias("\uCD9C\uD558").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
5163
6144
  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);
5164
- program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
5165
- await save();
6145
+ program.command("save").alias("\uC800\uC7A5").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (strict \uBAA8\uB4DC \uAC00\uB4DC \uBA85\uC2DC \uC2B9\uC778)").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async (opts) => {
6146
+ await guardCli("save", opts?.yes === true, () => save());
6147
+ });
6148
+ program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async (opts) => {
6149
+ await guardCliDefer("undo", opts?.yes === true, () => undo());
5166
6150
  });
5167
- program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
5168
- await undo();
6151
+ program.command("restore").alias("\uBCF5\uC6D0").argument("[id]", "\uBCF5\uC6D0\uD560 \uBC31\uC5C5 id (\uC0DD\uB7B5 \uC2DC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD)").description("sync \uBC31\uC5C5 \uBCF5\uC6D0 (.vhk/backups/ \u2014 \uC5B8\uCEE4\uBC0B \uB36E\uC5B4\uC4F0\uAE30 \uBCF5\uAD6C)").action(async (id) => {
6152
+ await restore(id);
5169
6153
  });
5170
6154
  program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
5171
6155
  await status();
@@ -5177,17 +6161,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
5177
6161
  program.command("mcp-init").alias("mcp\uC124\uC815").description("Cursor\xB7Claude Desktop MCP \uC5F0\uB3D9 \uC124\uC815 \uC790\uB3D9 \uC0DD\uC131 (.cursor/mcp.json)").action(async () => {
5178
6162
  await mcpInit();
5179
6163
  });
5180
- program.command("deploy").alias("\uBC30\uD3EC").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async () => {
5181
- await deploy();
6164
+ program.command("deploy").alias("\uBC30\uD3EC").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async (opts) => {
6165
+ await guardCli("deploy", opts?.yes === true, () => deploy());
5182
6166
  });
5183
- program.command("env").alias("\uD658\uACBD\uBCC0\uC218").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async () => {
5184
- await env();
6167
+ program.command("env").alias("\uD658\uACBD\uBCC0\uC218").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async (opts) => {
6168
+ await guardCli("env-write", opts?.yes === true, () => env());
5185
6169
  });
5186
6170
  program.command("env-check").alias("\uD658\uACBD\uBCC0\uC218\uC810\uAC80").description("\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC").action(async () => {
5187
6171
  await envCheck();
5188
6172
  });
5189
- program.command("publish").alias("\uCD9C\uC2DC").description("npm \uBC30\uD3EC (\uBC84\uC804 \uBC94\uD504 \u2192 \uBE4C\uB4DC \u2192 \uD14C\uC2A4\uD2B8 \u2192 publish)").action(async () => {
5190
- await publish();
6173
+ program.command("publish").alias("\uCD9C\uC2DC").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("npm \uBC30\uD3EC (\uBC84\uC804 \uBC94\uD504 \u2192 \uBE4C\uB4DC \u2192 \uD14C\uC2A4\uD2B8 \u2192 publish)").action(async (opts) => {
6174
+ await guardCli("publish", opts?.yes === true, () => publish());
5191
6175
  });
5192
6176
  program.command("design").alias("\uB514\uC790\uC778").description("\uB514\uC790\uC778 \uD1A0\uD070 \uC0DD\uC131 (Tailwind config \uB610\uB294 CSS \uBCC0\uC218)").action(async () => {
5193
6177
  await design();
@@ -5216,14 +6200,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
5216
6200
  program.command("audit").alias("\uAC10\uC0AC").option("--fix", "\uC790\uB3D9 \uC218\uC815 \uC2DC\uB3C4").description("\uBCF4\uC548 \uCDE8\uC57D\uC810 \uAC10\uC0AC (npm audit \uB798\uD551)").action(async (opts) => {
5217
6201
  await audit(opts.fix);
5218
6202
  });
5219
- program.command("migrate [target]").alias("\uC804\uD658").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm)").action(async (target) => {
5220
- await migrate(target);
6203
+ program.command("migrate [target]").alias("\uC804\uD658").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm) \u2014 \uD328\uD0A4\uC9C0\uB9E4\uB2C8\uC800\uB9CC \uBC14\uAFC8, \uC124\uC815 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC544\uB2D8").action(async (target, opts) => {
6204
+ await guardCli("migrate", opts?.yes === true, () => migrate(target));
5221
6205
  });
5222
6206
  program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
5223
6207
  await update();
5224
6208
  });
5225
- program.command("context").alias("\uB9E5\uB77D").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async () => {
5226
- await context();
6209
+ program.command("context").alias("\uB9E5\uB77D").option("--compact", "\uD1A0\uD070 \uC808\uAC10\uD615 \u2014 \uC804\uCCB4 \uBA85\uB839 \uBAA9\uB85D/\uAE4A\uC740 \uD2B8\uB9AC \uC0DD\uB7B5, \uCC38\uC870 \uB9C1\uD06C \uC911\uC2EC").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async (opts) => {
6210
+ await context({ compact: opts.compact });
6211
+ });
6212
+ program.command("mode [target]").alias("\uBAA8\uB4DC").description("Safety Mode \uC870\uD68C/\uBCC0\uACBD (lite|standard|strict) \u2014 \uC704\uD5D8 \uC791\uC5C5 \uAC00\uB4DC \uAC15\uB3C4").action(async (target) => {
6213
+ await mode(target);
6214
+ });
6215
+ program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").description("\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C \uC548\uB0B4 (lite)").action(async () => {
6216
+ await verify();
5227
6217
  });
5228
6218
  program.command("context-show").alias("\uB9E5\uB77D\uBCF4\uAE30").description("\uD604\uC7AC \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uB0B4\uC6A9 \uCD9C\uB825").action(async () => {
5229
6219
  await contextShow();
@@ -5269,7 +6259,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
5269
6259
  await learn(lesson);
5270
6260
  });
5271
6261
  program.command("resume").alias("\uC7AC\uAC1C").option("--confirm", "\uC0AC\uB78C \uD655\uC778 \u2014 \uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0 (Forbidden \uC704\uBC18)").description(".vhk/HARD_STOP \uD574\uC81C (\uC0AC\uC6A9\uC790\uAC00 \uC0AC\uC720 \uD655\uC778 \uD6C4 --confirm \uD544\uC694)").action(async (opts) => {
5272
- await resume(opts);
6262
+ await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
5273
6263
  });
5274
6264
  program.on("command:*", async (operands) => {
5275
6265
  const unknown = operands[0] ?? "";
@@ -5279,7 +6269,7 @@ program.on("command:*", async (operands) => {
5279
6269
  });
5280
6270
  program.action(async () => {
5281
6271
  console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
5282
- const { choice } = await inquirer12.prompt([{
6272
+ const { choice } = await inquirer14.prompt([{
5283
6273
  type: "list",
5284
6274
  name: "choice",
5285
6275
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -5310,24 +6300,40 @@ program.action(async () => {
5310
6300
  case "secure":
5311
6301
  return secure();
5312
6302
  case "sync":
5313
- return sync();
6303
+ return guardCli("sync", false, () => sync());
5314
6304
  case "doctor":
5315
6305
  return doctor();
5316
6306
  case "ship":
5317
6307
  return ship();
5318
6308
  case "save":
5319
- return save();
6309
+ return guardCli("save", false, () => save());
5320
6310
  case "undo":
5321
- return undo();
6311
+ return guardCliDefer("undo", false, () => undo());
5322
6312
  case "status":
5323
6313
  return status();
5324
6314
  case "diff":
5325
6315
  return diff();
5326
6316
  }
5327
6317
  });
5328
- var nlInput = detectNaturalLanguageInput(process.argv);
5329
- if (nlInput !== null) {
5330
- await runNaturalLanguageRoute(nlInput);
5331
- } else {
5332
- await program.parseAsync(process.argv);
6318
+ var isMainModule = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
6319
+ if (isMainModule) {
6320
+ try {
6321
+ const nlInput = detectNaturalLanguageInput(process.argv);
6322
+ if (nlInput !== null) {
6323
+ await runNaturalLanguageRoute(nlInput);
6324
+ } else {
6325
+ await program.parseAsync(process.argv);
6326
+ }
6327
+ } catch (err) {
6328
+ if (isPromptAbortError(err)) {
6329
+ console.error(chalk35.yellow("\n \u26A0\uFE0F \uB300\uD654\uD615 \uC785\uB825\uC774 \uCDE8\uC18C/\uC885\uB8CC\uB410\uC2B5\uB2C8\uB2E4. (\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uD574\uB2F9 \uBA85\uB839\uC744 \uC4F8 \uC218 \uC5C6\uC5B4\uC694)"));
6330
+ } else {
6331
+ console.error(chalk35.red(`
6332
+ \u274C ${err instanceof Error ? err.message : String(err)}`));
6333
+ }
6334
+ process.exitCode = 1;
6335
+ }
5333
6336
  }
6337
+ export {
6338
+ program
6339
+ };