@byh3071/vhk 1.6.1 → 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,19 +2352,21 @@ 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,
1747
2370
  ""
1748
2371
  ];
1749
2372
  for (const section of recordSections) {
@@ -1753,6 +2376,34 @@ function toClaudeMd(sections, existing) {
1753
2376
  }
1754
2377
  return lines.join("\n");
1755
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`.",
2393
+ ""
2394
+ ];
2395
+ for (const section of codingSections) {
2396
+ lines.push(`## ${section.title}`);
2397
+ lines.push(section.content);
2398
+ lines.push("");
2399
+ }
2400
+ for (const section of recordSections) {
2401
+ lines.push(`## ${section.title}`);
2402
+ lines.push(section.content);
2403
+ lines.push("");
2404
+ }
2405
+ return lines.join("\n");
2406
+ }
1756
2407
  function deriveProjectName(rulesContent) {
1757
2408
  const firstLine = rulesContent.split("\n")[0];
1758
2409
  return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
@@ -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));
1797
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(", ")}`));
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,161 +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
- var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
2581
- function contextSourcesChanged(generatedSha, rootDir) {
2582
- const content = gitOut(
2583
- ["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
2584
- rootDir
2585
- ).trim();
2586
- if (content) return true;
2587
- const structural = gitOut(
2588
- ["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
2589
- rootDir
2590
- ).trim();
2591
- return structural.length > 0;
2592
- }
2593
- function checkContextDrift(rootDir) {
2594
- const ctxPath = path10.join(rootDir, CONTEXT_PATH);
2595
- if (!fs9.existsSync(ctxPath)) return { checked: false, stale: false };
2596
- const generatedSha = extractContextSha(fs9.readFileSync(ctxPath, "utf-8"));
2597
- if (!generatedSha) return { checked: false, stale: false };
2598
- let currentSha;
2599
- try {
2600
- currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
2601
- } catch {
2602
- return { checked: false, stale: false };
2603
- }
2604
- if (!currentSha) return { checked: false, stale: false };
2605
- if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
2606
- return { checked: true, stale: false, generatedSha, currentSha };
2607
- }
2608
- let stale;
2609
- try {
2610
- stale = contextSourcesChanged(generatedSha, rootDir);
2611
- } catch {
2612
- return { checked: false, stale: false };
2613
- }
2614
- return { checked: true, stale, generatedSha, currentSha };
2615
- }
2616
-
2617
- // src/commands/doctor.ts
2618
3282
  function checkCommand(name, command, hint) {
2619
3283
  const result = safeExecFile(command, ["--version"]);
2620
3284
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2622,14 +3286,14 @@ function checkCommand(name, command, hint) {
2622
3286
  return { name, command, version, ok: true, hint };
2623
3287
  }
2624
3288
  function getVhkVersion2() {
2625
- const dir = path11.dirname(fileURLToPath(import.meta.url));
3289
+ const dir = path12.dirname(fileURLToPath(import.meta.url));
2626
3290
  const candidates = [
2627
- path11.join(dir, "../package.json"),
2628
- path11.join(dir, "../../package.json")
3291
+ path12.join(dir, "../package.json"),
3292
+ path12.join(dir, "../../package.json")
2629
3293
  ];
2630
3294
  for (const pkgPath of candidates) {
2631
3295
  try {
2632
- if (fs10.existsSync(pkgPath)) {
3296
+ if (fs11.existsSync(pkgPath)) {
2633
3297
  const pkg = readJsonFile(pkgPath);
2634
3298
  return pkg.version;
2635
3299
  }
@@ -2657,7 +3321,7 @@ function compareSemver(a, b) {
2657
3321
  return a3 - b3;
2658
3322
  }
2659
3323
  async function doctor() {
2660
- console.log(chalk9.bold(`
3324
+ console.log(chalk11.bold(`
2661
3325
  ${ko.doctor.title}
2662
3326
  `));
2663
3327
  const checks = [
@@ -2669,30 +3333,30 @@ ${ko.doctor.title}
2669
3333
  let allOk = true;
2670
3334
  for (const check2 of checks) {
2671
3335
  if (check2.ok) {
2672
- 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}`));
2673
3337
  } else {
2674
- console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2675
- 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}`));
2676
3340
  allOk = false;
2677
3341
  }
2678
3342
  }
2679
3343
  console.log("");
2680
3344
  const vhkVersion = getVhkVersion2();
2681
3345
  if (vhkVersion) {
2682
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
3346
+ console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(` \u2014 v${vhkVersion}`));
2683
3347
  } else {
2684
- 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"));
2685
3349
  }
2686
3350
  if (vhkVersion) {
2687
3351
  const latest = fetchLatestNpmVersion("@byh3071/vhk");
2688
3352
  if (latest && compareSemver(latest, vhkVersion) > 0) {
2689
- console.log(chalk9.yellow(` ${ko.doctor.updateAvailable(latest)}`));
3353
+ console.log(chalk11.yellow(` ${ko.doctor.updateAvailable(latest)}`));
2690
3354
  } else if (latest) {
2691
- console.log(chalk9.dim(` ${ko.doctor.updateCurrent}`));
3355
+ console.log(chalk11.dim(` ${ko.doctor.updateCurrent}`));
2692
3356
  }
2693
3357
  }
2694
3358
  console.log("");
2695
- console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
3359
+ console.log(chalk11.bold(` ${ko.doctor.projectFiles}`));
2696
3360
  const cwd = process.cwd();
2697
3361
  const projectFiles = [
2698
3362
  { name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
@@ -2702,49 +3366,51 @@ ${ko.doctor.title}
2702
3366
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2703
3367
  ];
2704
3368
  for (const file of projectFiles) {
2705
- const exists = fs10.existsSync(path11.join(cwd, file.name));
3369
+ const exists = fs11.existsSync(path12.join(cwd, file.name));
2706
3370
  if (exists) {
2707
- console.log(chalk9.green(` \u2705 ${file.name}`));
3371
+ console.log(chalk11.green(` \u2705 ${file.name}`));
2708
3372
  if (file.name === ".env") {
2709
- const gitignorePath = path11.join(cwd, ".gitignore");
2710
- if (fs10.existsSync(gitignorePath)) {
2711
- 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");
2712
3376
  if (!gitignore.includes(".env")) {
2713
- console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
3377
+ console.log(chalk11.yellow(` ${ko.doctor.envNotIgnored}`));
2714
3378
  }
2715
3379
  }
2716
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)"));
2717
3383
  } else {
2718
- 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}`));
2719
3385
  }
2720
3386
  }
2721
3387
  console.log("");
2722
- console.log(chalk9.bold(` ${ko.doctor.driftTitle}`));
3388
+ console.log(chalk11.bold(` ${ko.doctor.driftTitle}`));
2723
3389
  const ruleDrift = checkRuleDrift(cwd);
2724
3390
  if (!ruleDrift.checked) {
2725
- console.log(chalk9.dim(` ${ko.doctor.driftNoRules}`));
3391
+ console.log(chalk11.dim(` ${ko.doctor.driftNoRules}`));
2726
3392
  } else {
2727
3393
  const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
2728
3394
  if (drifted.length === 0) {
2729
- console.log(chalk9.green(` ${ko.doctor.driftRuleClean}`));
3395
+ console.log(chalk11.green(` ${ko.doctor.driftRuleClean}`));
2730
3396
  } else {
2731
- 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(", "))}`));
2732
3398
  }
2733
3399
  }
2734
3400
  const ctxDrift = checkContextDrift(cwd);
2735
3401
  if (ctxDrift.checked && ctxDrift.stale) {
2736
- console.log(chalk9.yellow(` ${ko.doctor.driftContextWarn}`));
3402
+ console.log(chalk11.yellow(` ${ko.doctor.driftContextWarn}`));
2737
3403
  }
2738
3404
  console.log("");
2739
3405
  if (allOk) {
2740
- console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
3406
+ console.log(chalk11.green.bold(` ${ko.doctor.allOk}`));
2741
3407
  printNextStep({
2742
3408
  message: ko.doctor.nextOkMessage,
2743
3409
  command: "vhk \uC2DC\uC791",
2744
3410
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
2745
3411
  });
2746
3412
  } else {
2747
- console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
3413
+ console.log(chalk11.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2748
3414
  printNextStep({
2749
3415
  message: ko.doctor.nextRetryMessage,
2750
3416
  command: "vhk doctor",
@@ -2755,10 +3421,10 @@ ${ko.doctor.title}
2755
3421
  }
2756
3422
 
2757
3423
  // src/commands/ship.ts
2758
- import chalk10 from "chalk";
2759
- import inquirer4 from "inquirer";
2760
- import fs11 from "fs";
2761
- import path12 from "path";
3424
+ import chalk12 from "chalk";
3425
+ import inquirer5 from "inquirer";
3426
+ import fs12 from "fs";
3427
+ import path13 from "path";
2762
3428
  var CHECKLIST = [
2763
3429
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
2764
3430
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -2771,9 +3437,9 @@ function sanitizeVersion(version) {
2771
3437
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2772
3438
  }
2773
3439
  function updateChangelogUnreleased(cwd, version, date) {
2774
- const changelogPath = path12.join(cwd, "CHANGELOG.md");
2775
- if (!fs11.existsSync(changelogPath)) return { status: "missing" };
2776
- 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");
2777
3443
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2778
3444
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2779
3445
  const blankUnreleased = [
@@ -2790,35 +3456,36 @@ function updateChangelogUnreleased(cwd, version, date) {
2790
3456
  `## [${version}] \u2014 ${date}`
2791
3457
  ].join("\n");
2792
3458
  const updated = content.replace(unreleasedHeading, blankUnreleased);
2793
- fs11.writeFileSync(changelogPath, updated, "utf-8");
3459
+ fs12.writeFileSync(changelogPath, updated, "utf-8");
2794
3460
  return { status: "updated", version };
2795
3461
  }
2796
3462
  async function ship() {
2797
- console.log(chalk10.bold(`
3463
+ if (!ensureNotHardStopped("ship")) return;
3464
+ console.log(chalk12.bold(`
2798
3465
  ${ko.ship.title}
2799
3466
  `));
2800
3467
  const cwd = process.cwd();
2801
- console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
3468
+ console.log(chalk12.cyan.bold(` ${ko.ship.checklist}
2802
3469
  `));
2803
- const { passed } = await inquirer4.prompt([{
3470
+ const { passed } = await inquirer5.prompt([{
2804
3471
  type: "checkbox",
2805
3472
  name: "passed",
2806
3473
  message: ko.ship.checkboxPrompt,
2807
3474
  choices: CHECKLIST.map((c) => ({
2808
- name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
3475
+ name: `${ko.ship[c.questionKey]} ${chalk12.dim(`(${ko.ship[c.hintKey]})`)}`,
2809
3476
  value: c.id
2810
3477
  }))
2811
3478
  }]);
2812
3479
  const allPassed = passed.length === CHECKLIST.length;
2813
3480
  const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
2814
3481
  if (!allPassed) {
2815
- console.log(chalk10.yellow(`
3482
+ console.log(chalk12.yellow(`
2816
3483
  ${ko.ship.incompleteHeader}`));
2817
3484
  skipped.forEach((s) => {
2818
- console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2819
- 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]}`));
2820
3487
  });
2821
- const { proceed } = await inquirer4.prompt([{
3488
+ const { proceed } = await inquirer5.prompt([{
2822
3489
  type: "confirm",
2823
3490
  name: "proceed",
2824
3491
  message: ko.ship.proceedConfirm,
@@ -2833,26 +3500,26 @@ ${ko.ship.title}
2833
3500
  return;
2834
3501
  }
2835
3502
  } else {
2836
- console.log(chalk10.green(`
3503
+ console.log(chalk12.green(`
2837
3504
  ${ko.ship.allPassed}
2838
3505
  `));
2839
3506
  }
2840
- console.log(chalk10.cyan.bold(` ${ko.ship.retro}
3507
+ console.log(chalk12.cyan.bold(` ${ko.ship.retro}
2841
3508
  `));
2842
- console.log(chalk10.dim(` ${ko.ship.versionHint}`));
2843
- const retro = await inquirer4.prompt([
3509
+ console.log(chalk12.dim(` ${ko.ship.versionHint}`));
3510
+ const retro = await inquirer5.prompt([
2844
3511
  { type: "input", name: "version", message: ko.ship.versionPrompt },
2845
3512
  { type: "input", name: "whatWentWell", message: ko.ship.questionWell },
2846
3513
  { type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
2847
3514
  { type: "input", name: "learned", message: ko.ship.questionLearned },
2848
3515
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2849
3516
  ]);
2850
- const buildLogDir = path12.join(cwd, "docs", "build-log");
2851
- if (!fs11.existsSync(buildLogDir)) fs11.mkdirSync(buildLogDir, { recursive: true });
2852
- 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();
2853
3520
  const versionSlug = sanitizeVersion(retro.version);
2854
3521
  const fileName = `${today}-v${versionSlug}.md`;
2855
- const filePath = path12.join(buildLogDir, fileName);
3522
+ const filePath = path13.join(buildLogDir, fileName);
2856
3523
  const content = [
2857
3524
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
2858
3525
  "",
@@ -2881,9 +3548,9 @@ ${ko.ship.title}
2881
3548
  "---",
2882
3549
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2883
3550
  ].join("\n");
2884
- fs11.writeFileSync(filePath, content, "utf-8");
2885
- console.log(chalk10.green(`
2886
- ${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))}`));
2887
3554
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
2888
3555
  if (changelogResult.status === "updated") {
2889
3556
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -2902,9 +3569,9 @@ ${ko.ship.title}
2902
3569
 
2903
3570
  // src/commands/save.ts
2904
3571
  import { execFileSync as execFileSync2 } from "child_process";
2905
- import chalk11 from "chalk";
3572
+ import chalk13 from "chalk";
2906
3573
  import ora from "ora";
2907
- import inquirer5 from "inquirer";
3574
+ import inquirer6 from "inquirer";
2908
3575
 
2909
3576
  // src/lib/git-porcelain.ts
2910
3577
  function normalizePorcelain(raw) {
@@ -2930,54 +3597,54 @@ function statusIcon(code) {
2930
3597
  return "\u{1F4C4}";
2931
3598
  }
2932
3599
  async function save() {
2933
- console.log(chalk11.bold(`
3600
+ console.log(chalk13.bold(`
2934
3601
  \u{1F4BE} ${t("save.title")}`));
2935
- console.log(chalk11.gray("\u2500".repeat(40)));
3602
+ console.log(chalk13.gray("\u2500".repeat(40)));
2936
3603
  let gitRoot;
2937
3604
  try {
2938
3605
  execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
2939
3606
  gitRoot = getGitRoot();
2940
3607
  } catch {
2941
- console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
3608
+ console.log(chalk13.red(`\u274C ${t("save.notGitRepo")}`));
2942
3609
  return;
2943
3610
  }
2944
- console.log(chalk11.cyan(`
3611
+ console.log(chalk13.cyan(`
2945
3612
  \u{1F512} ${t("save.securityWarnHeader")}`));
2946
3613
  printSecurityWarnings(gitRoot);
2947
3614
  const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
2948
3615
  if (severe.length > 0) {
2949
- console.log(chalk11.red(`
3616
+ console.log(chalk13.red(`
2950
3617
  \u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
2951
3618
  severe.slice(0, 5).forEach((f) => {
2952
- console.log(chalk11.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
3619
+ console.log(chalk13.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
2953
3620
  });
2954
3621
  if (severe.length > 5) {
2955
- 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)`));
2956
3623
  }
2957
- const { proceed } = await inquirer5.prompt([{
3624
+ const { proceed } = await inquirer6.prompt([{
2958
3625
  type: "confirm",
2959
3626
  name: "proceed",
2960
3627
  message: t("save.secretsConfirm"),
2961
3628
  default: false
2962
3629
  }]);
2963
3630
  if (!proceed) {
2964
- console.log(chalk11.gray(t("save.cancelled")));
3631
+ console.log(chalk13.gray(t("save.cancelled")));
2965
3632
  return;
2966
3633
  }
2967
3634
  }
2968
3635
  const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
2969
3636
  if (lines.length === 0) {
2970
- console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
3637
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
2971
3638
  return;
2972
3639
  }
2973
- console.log(chalk11.cyan(`
3640
+ console.log(chalk13.cyan(`
2974
3641
  \u{1F4CB} ${t("save.filesHeader", lines.length)}`));
2975
3642
  lines.forEach((line) => {
2976
3643
  const code = line.substring(0, 2);
2977
3644
  const name = line.substring(3);
2978
3645
  console.log(` ${statusIcon(code)} ${name}`);
2979
3646
  });
2980
- const { message } = await inquirer5.prompt([{
3647
+ const { message } = await inquirer6.prompt([{
2981
3648
  type: "input",
2982
3649
  name: "message",
2983
3650
  message: t("save.commitMessage"),
@@ -2992,21 +3659,21 @@ async function save() {
2992
3659
  spinner.text = t("save.pushing");
2993
3660
  if (!hasGitRemote(gitRoot)) {
2994
3661
  spinner.succeed(t("save.successLocal"));
2995
- console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
3662
+ console.log(chalk13.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
2996
3663
  } else {
2997
3664
  try {
2998
3665
  gitRun(["push"], gitRoot);
2999
3666
  spinner.succeed(t("save.successWithPush"));
3000
3667
  } catch (pushErr) {
3001
3668
  spinner.fail(t("save.pushFailed"));
3002
- console.log(chalk11.red(getExecErrorMessage(pushErr)));
3003
- console.log(chalk11.yellow(`
3669
+ console.log(chalk13.red(getExecErrorMessage(pushErr)));
3670
+ console.log(chalk13.yellow(`
3004
3671
  \u{1F4A1} ${t("save.commitOkPushFailed")}`));
3005
3672
  process.exitCode = 1;
3006
3673
  }
3007
3674
  }
3008
3675
  if (process.exitCode !== 1) {
3009
- console.log(chalk11.green(`
3676
+ console.log(chalk13.green(`
3010
3677
  \u2705 ${t("save.done", lines.length)}`));
3011
3678
  printNextStep({
3012
3679
  message: t("save.nextOkMessage"),
@@ -3014,7 +3681,7 @@ async function save() {
3014
3681
  cursorHint: t("save.nextOkCursor")
3015
3682
  });
3016
3683
  } else {
3017
- console.log(chalk11.green(`
3684
+ console.log(chalk13.green(`
3018
3685
  \u2705 ${t("save.doneLocalOnly", lines.length)}`));
3019
3686
  printNextStep({
3020
3687
  message: t("save.nextPushFailMessage"),
@@ -3024,12 +3691,12 @@ async function save() {
3024
3691
  }
3025
3692
  } catch (err) {
3026
3693
  spinner.fail(t("save.failed"));
3027
- console.log(chalk11.red(getExecErrorMessage(err)));
3694
+ console.log(chalk13.red(getExecErrorMessage(err)));
3028
3695
  if (didAdd) {
3029
3696
  try {
3030
3697
  const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
3031
3698
  if (staged) {
3032
- console.log(chalk11.yellow(`
3699
+ console.log(chalk13.yellow(`
3033
3700
  \u{1F4A1} ${t("save.stagedAfterFail")}`));
3034
3701
  }
3035
3702
  } catch {
@@ -3041,8 +3708,8 @@ async function save() {
3041
3708
 
3042
3709
  // src/commands/undo.ts
3043
3710
  import { execFileSync as execFileSync3 } from "child_process";
3044
- import chalk12 from "chalk";
3045
- import inquirer6 from "inquirer";
3711
+ import chalk14 from "chalk";
3712
+ import inquirer7 from "inquirer";
3046
3713
  function parseRecentCommits(logOutput) {
3047
3714
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3048
3715
  }
@@ -3065,36 +3732,36 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
3065
3732
  return false;
3066
3733
  }
3067
3734
  async function undo() {
3068
- console.log(chalk12.bold(`
3735
+ console.log(chalk14.bold(`
3069
3736
  \u23EA ${t("undo.title")}`));
3070
- console.log(chalk12.gray("\u2500".repeat(40)));
3737
+ console.log(chalk14.gray("\u2500".repeat(40)));
3071
3738
  let gitRoot;
3072
3739
  try {
3073
3740
  execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3074
3741
  gitRoot = getGitRoot();
3075
3742
  } catch {
3076
- console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
3743
+ console.log(chalk14.red(`\u274C ${t("undo.notGitRepo")}`));
3077
3744
  return;
3078
3745
  }
3079
3746
  let logOutput;
3080
3747
  try {
3081
3748
  logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
3082
3749
  } catch {
3083
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3750
+ console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3084
3751
  return;
3085
3752
  }
3086
3753
  const commits = parseRecentCommits(logOutput);
3087
3754
  if (commits.length === 0) {
3088
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3755
+ console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3089
3756
  return;
3090
3757
  }
3091
- console.log(chalk12.cyan(`
3758
+ console.log(chalk14.cyan(`
3092
3759
  ${t("undo.recentHeader")}`));
3093
3760
  commits.forEach((c, i) => {
3094
3761
  console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
3095
3762
  });
3096
3763
  const maxUndo = commits.length;
3097
- const { count } = await inquirer6.prompt([{
3764
+ const { count } = await inquirer7.prompt([{
3098
3765
  type: "number",
3099
3766
  name: "count",
3100
3767
  message: t("undo.howMany"),
@@ -3105,7 +3772,7 @@ ${t("undo.recentHeader")}`));
3105
3772
  const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
3106
3773
  const headCount = countLocalCommits(gitRoot);
3107
3774
  if (undoCount >= headCount) {
3108
- console.log(chalk12.yellow(`
3775
+ console.log(chalk14.yellow(`
3109
3776
  \u{1F4ED} ${t("undo.rootCommit")}`));
3110
3777
  return;
3111
3778
  }
@@ -3114,30 +3781,30 @@ ${t("undo.recentHeader")}`));
3114
3781
  const risky = isUndoRisky(undoCount, unpushed, remote);
3115
3782
  if (risky) {
3116
3783
  if (unpushed < 0) {
3117
- console.log(chalk12.red(`
3784
+ console.log(chalk14.red(`
3118
3785
  \u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
3119
3786
  } else {
3120
- console.log(chalk12.red(`
3787
+ console.log(chalk14.red(`
3121
3788
  \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
3122
3789
  }
3123
3790
  }
3124
- const { confirm } = await inquirer6.prompt([{
3791
+ const { confirm } = await inquirer7.prompt([{
3125
3792
  type: "confirm",
3126
3793
  name: "confirm",
3127
3794
  message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
3128
3795
  default: false
3129
3796
  }]);
3130
3797
  if (!confirm) {
3131
- console.log(chalk12.gray(t("undo.cancelled")));
3798
+ console.log(chalk14.gray(t("undo.cancelled")));
3132
3799
  return;
3133
3800
  }
3134
3801
  try {
3135
3802
  gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
3136
- console.log(chalk12.green(`
3803
+ console.log(chalk14.green(`
3137
3804
  \u2705 ${t("undo.success")}`));
3138
- console.log(chalk12.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3805
+ console.log(chalk14.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3139
3806
  if (risky) {
3140
- console.log(chalk12.yellow(`
3807
+ console.log(chalk14.yellow(`
3141
3808
  \u{1F4A1} ${t("undo.forcePushHint")}`));
3142
3809
  }
3143
3810
  printNextStep({
@@ -3146,18 +3813,73 @@ ${t("undo.recentHeader")}`));
3146
3813
  cursorHint: t("undo.nextCursor")
3147
3814
  });
3148
3815
  } catch (err) {
3149
- console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3816
+ console.log(chalk14.red(`\u274C ${t("undo.failed")}`));
3150
3817
  const msg = err instanceof Error ? err.message : String(err);
3151
- 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)}`));
3152
3874
  process.exitCode = 1;
3153
3875
  }
3154
3876
  }
3155
3877
 
3156
3878
  // src/commands/status.ts
3157
3879
  import { execFileSync as execFileSync4 } from "child_process";
3158
- import fs12 from "fs";
3159
- import path13 from "path";
3160
- import chalk13 from "chalk";
3880
+ import fs13 from "fs";
3881
+ import path14 from "path";
3882
+ import chalk16 from "chalk";
3161
3883
  function countFileChanges(porcelain) {
3162
3884
  const lines = porcelain.split("\n").filter(Boolean);
3163
3885
  let staged = 0;
@@ -3195,8 +3917,8 @@ function parseRecentCommitLines(logOutput) {
3195
3917
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3196
3918
  }
3197
3919
  function readProjectPackage(cwd = process.cwd()) {
3198
- const pkgPath = path13.join(cwd, "package.json");
3199
- if (!fs12.existsSync(pkgPath)) return null;
3920
+ const pkgPath = path14.join(cwd, "package.json");
3921
+ if (!fs13.existsSync(pkgPath)) return null;
3200
3922
  try {
3201
3923
  const pkg = readJsonFile(pkgPath);
3202
3924
  if (!pkg.name && !pkg.version) return null;
@@ -3208,6 +3930,21 @@ function readProjectPackage(cwd = process.cwd()) {
3208
3930
  return null;
3209
3931
  }
3210
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
+ }
3211
3948
  function getSyncCounts(gitRoot) {
3212
3949
  try {
3213
3950
  const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
@@ -3217,15 +3954,15 @@ function getSyncCounts(gitRoot) {
3217
3954
  }
3218
3955
  }
3219
3956
  async function status() {
3220
- console.log(chalk13.bold(`
3957
+ console.log(chalk16.bold(`
3221
3958
  \u{1F4CA} ${t("status.title")}`));
3222
- console.log(chalk13.gray("\u2500".repeat(40)));
3959
+ console.log(chalk16.gray("\u2500".repeat(40)));
3223
3960
  let gitRoot;
3224
3961
  try {
3225
3962
  execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3226
3963
  gitRoot = getGitRoot();
3227
3964
  } catch {
3228
- console.log(chalk13.red(`\u274C ${t("status.notGitRepo")}`));
3965
+ console.log(chalk16.red(`\u274C ${t("status.notGitRepo")}`));
3229
3966
  return;
3230
3967
  }
3231
3968
  let branch;
@@ -3244,48 +3981,36 @@ async function status() {
3244
3981
  commits = [];
3245
3982
  }
3246
3983
  const pkg = readProjectPackage();
3247
- console.log(chalk13.cyan(`
3248
- \u{1F33F} ${t("status.branch")}`) + chalk13.white(` ${branch}`));
3984
+ console.log(chalk16.cyan(`
3985
+ \u{1F33F} ${t("status.branch")}`) + chalk16.white(` ${branch}`));
3249
3986
  console.log(
3250
- chalk13.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk13.white(
3987
+ chalk16.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk16.white(
3251
3988
  ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3252
3989
  )
3253
3990
  );
3254
- console.log(chalk13.cyan(`
3255
- \u{1F4CB} ${t("status.recentCommits")}`));
3991
+ console.log(chalk16.cyan(`
3992
+ \u{1F4CB} ${t("status.recentCommits", commits.length)}`));
3256
3993
  if (commits.length === 0) {
3257
- console.log(chalk13.dim(` ${t("status.noCommits")}`));
3994
+ console.log(chalk16.dim(` ${t("status.noCommits")}`));
3258
3995
  } else {
3259
- commits.forEach((c) => console.log(` ${chalk13.dim("\u2022")} ${c}`));
3996
+ commits.forEach((c) => console.log(` ${chalk16.dim("\u2022")} ${c}`));
3260
3997
  }
3261
3998
  console.log(
3262
- chalk13.cyan(`
3263
- \u{1F504} ${t("status.remote")}`) + chalk13.white(` ${formatSyncLabel(sync2)}`)
3999
+ chalk16.cyan(`
4000
+ \u{1F504} ${t("status.remote")}`) + chalk16.white(` ${formatSyncLabel(sync2)}`)
3264
4001
  );
3265
- console.log(chalk13.gray("\n" + "\u2500".repeat(40)));
4002
+ console.log(chalk16.gray("\n" + "\u2500".repeat(40)));
3266
4003
  if (pkg) {
3267
- 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}`));
3268
4005
  } else {
3269
- console.log(chalk13.dim(`\u{1F4E6} ${t("status.noPackage")}`));
4006
+ console.log(chalk16.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3270
4007
  }
3271
4008
  const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
3272
- if (hasChanges) {
3273
- printNextStep({
3274
- message: t("status.nextWithChangesMessage"),
3275
- command: "vhk save",
3276
- cursorHint: t("status.nextWithChangesCursor")
3277
- });
3278
- } else {
3279
- printNextStep({
3280
- message: t("status.nextCleanMessage"),
3281
- command: "vhk goal next",
3282
- cursorHint: t("status.nextCleanCursor")
3283
- });
3284
- }
4009
+ printNextStep(selectStatusNextStep(hasChanges));
3285
4010
  }
3286
4011
 
3287
4012
  // src/commands/diff.ts
3288
- import chalk14 from "chalk";
4013
+ import chalk17 from "chalk";
3289
4014
  function gitOut2(args) {
3290
4015
  const r = safeExecFile("git", args);
3291
4016
  return r.ok ? r.out : "";
@@ -3322,67 +4047,67 @@ function summarizeNumstat(numstat) {
3322
4047
  return { fileCount, totalAdd, totalDel };
3323
4048
  }
3324
4049
  function printFile(f) {
3325
- const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
3326
- 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}`) : "";
3327
4052
  const change = [adds, dels].filter(Boolean).join(" ");
3328
4053
  console.log(` ${f.name} ${change}`);
3329
4054
  }
3330
4055
  async function diff() {
3331
- console.log(chalk14.bold(`
4056
+ console.log(chalk17.bold(`
3332
4057
  \u{1F50D} ${t("diff.title")}`));
3333
- console.log(chalk14.gray("\u2500".repeat(40)));
4058
+ console.log(chalk17.gray("\u2500".repeat(40)));
3334
4059
  if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
3335
- console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
4060
+ console.log(chalk17.red(`\u274C ${t("diff.notGitRepo")}`));
3336
4061
  return;
3337
4062
  }
3338
4063
  const unstaged = gitOut2(["diff", "--stat"]);
3339
4064
  const staged = gitOut2(["diff", "--cached", "--stat"]);
3340
4065
  const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3341
4066
  if (!unstaged && !staged && !untracked) {
3342
- console.log(chalk14.green(`
4067
+ console.log(chalk17.green(`
3343
4068
  \u2705 ${t("diff.noChanges")}`));
3344
4069
  return;
3345
4070
  }
3346
4071
  if (staged) {
3347
- console.log(chalk14.cyan(`
4072
+ console.log(chalk17.cyan(`
3348
4073
  ${t("diff.stagedHeader")}`));
3349
4074
  parseDiffStat(staged).forEach((f) => printFile(f));
3350
4075
  }
3351
4076
  if (unstaged) {
3352
- console.log(chalk14.cyan(`
4077
+ console.log(chalk17.cyan(`
3353
4078
  ${t("diff.unstagedHeader")}`));
3354
4079
  parseDiffStat(unstaged).forEach((f) => printFile(f));
3355
4080
  }
3356
4081
  if (untracked) {
3357
4082
  const files = untracked.split("\n").filter(Boolean);
3358
- console.log(chalk14.cyan(`
4083
+ console.log(chalk17.cyan(`
3359
4084
  ${t("diff.untrackedHeader", files.length)}`));
3360
- files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
4085
+ files.forEach((f) => console.log(` ${chalk17.green("+")} ${f}`));
3361
4086
  }
3362
4087
  const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3363
4088
  if (numstat) {
3364
4089
  const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3365
- console.log(chalk14.cyan(`
4090
+ console.log(chalk17.cyan(`
3366
4091
  ${t("diff.summaryHeader")}`));
3367
4092
  console.log(` ${t("diff.filesLine", fileCount)}`);
3368
- console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
3369
- 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`);
3370
4095
  }
3371
4096
  console.log("");
3372
4097
  }
3373
4098
 
3374
4099
  // src/commands/mcp-init.ts
3375
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3376
- 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";
3377
4102
  import { fileURLToPath as fileURLToPath2 } from "url";
3378
- import chalk15 from "chalk";
4103
+ import chalk18 from "chalk";
3379
4104
  function resolveMcpEntryPoint() {
3380
4105
  try {
3381
4106
  const here = fileURLToPath2(import.meta.url);
3382
4107
  const dir = dirname(here);
3383
4108
  for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
3384
- const candidate = join3(dir, ...rel);
3385
- if (existsSync3(candidate)) return candidate;
4109
+ const candidate = join6(dir, ...rel);
4110
+ if (existsSync5(candidate)) return candidate;
3386
4111
  }
3387
4112
  } catch {
3388
4113
  }
@@ -3390,17 +4115,17 @@ function resolveMcpEntryPoint() {
3390
4115
  const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
3391
4116
  if (typeof url === "string") {
3392
4117
  const p = fileURLToPath2(url);
3393
- if (existsSync3(p)) return p;
4118
+ if (existsSync5(p)) return p;
3394
4119
  }
3395
4120
  } catch {
3396
4121
  }
3397
4122
  try {
3398
- const pkgPath = join3(process.cwd(), "package.json");
3399
- if (existsSync3(pkgPath)) {
4123
+ const pkgPath = join6(process.cwd(), "package.json");
4124
+ if (existsSync5(pkgPath)) {
3400
4125
  const pkg = readJsonFile(pkgPath);
3401
4126
  if (pkg.name === "@byh3071/vhk") {
3402
- const local = join3(process.cwd(), "dist", "mcp", "index.js");
3403
- if (existsSync3(local)) return local;
4127
+ const local = join6(process.cwd(), "dist", "mcp", "index.js");
4128
+ if (existsSync5(local)) return local;
3404
4129
  }
3405
4130
  }
3406
4131
  } catch {
@@ -3415,31 +4140,31 @@ function resolveVhkMcpEntry() {
3415
4140
  return { command: "vhk-mcp", args: [] };
3416
4141
  }
3417
4142
  async function mcpInit() {
3418
- console.log(chalk15.bold("\n\u{1F50C} " + t("mcp.initTitle")));
3419
- console.log(chalk15.gray("\u2500".repeat(40)));
3420
- const cursorDir = join3(process.cwd(), ".cursor");
3421
- if (!existsSync3(cursorDir)) {
3422
- 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 });
3423
4148
  }
3424
- const configPath = join3(cursorDir, "mcp.json");
4149
+ const configPath = join6(cursorDir, "mcp.json");
3425
4150
  const vhkEntry = resolveVhkMcpEntry();
3426
4151
  let config;
3427
- if (existsSync3(configPath)) {
4152
+ if (existsSync5(configPath)) {
3428
4153
  try {
3429
4154
  const parsed = readJsonFile(configPath);
3430
4155
  config = {
3431
4156
  mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
3432
4157
  };
3433
4158
  } catch {
3434
- 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."));
3435
4160
  config = { mcpServers: { vhk: vhkEntry } };
3436
4161
  }
3437
4162
  } else {
3438
4163
  config = { mcpServers: { vhk: vhkEntry } };
3439
4164
  }
3440
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3441
- console.log(chalk15.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
3442
- 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:"));
3443
4168
  console.log(` ${configPath}`);
3444
4169
  printNextStep({
3445
4170
  message: t("mcp.nextMessage"),
@@ -3449,9 +4174,9 @@ async function mcpInit() {
3449
4174
  }
3450
4175
 
3451
4176
  // src/commands/design.ts
3452
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
3453
- import chalk16 from "chalk";
3454
- 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";
3455
4180
  var PALETTES = [
3456
4181
  {
3457
4182
  name: "Minimal",
@@ -3503,7 +4228,36 @@ var PALETTES = [
3503
4228
  }
3504
4229
  ];
3505
4230
  function hasTailwind() {
3506
- 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");
3507
4261
  }
3508
4262
  function generateCSSTokens(palette) {
3509
4263
  const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
@@ -3524,9 +4278,10 @@ export default vhkColors
3524
4278
  `;
3525
4279
  }
3526
4280
  async function design() {
3527
- console.log(chalk16.bold("\n\u{1F3A8} " + t("design.title")));
3528
- console.log(chalk16.gray("\u2500".repeat(40)));
3529
- 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([
3530
4285
  {
3531
4286
  type: "list",
3532
4287
  name: "paletteIndex",
@@ -3538,32 +4293,37 @@ async function design() {
3538
4293
  }
3539
4294
  ]);
3540
4295
  const palette = PALETTES[paletteIndex];
3541
- console.log(chalk16.cyan(`
4296
+ console.log(chalk19.cyan(`
3542
4297
  \u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
3543
- const targetPath = hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
3544
- const content = hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
3545
- if (existsSync4(targetPath)) {
3546
- 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([{
3547
4303
  type: "confirm",
3548
4304
  name: "overwrite",
3549
4305
  message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
3550
4306
  default: false
3551
4307
  }]);
3552
4308
  if (!overwrite) {
3553
- 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."));
3554
4310
  return;
3555
4311
  }
3556
4312
  }
3557
- mkdirSync3("src/styles", { recursive: true });
3558
- writeFileSync3(targetPath, content, "utf-8");
3559
- if (hasTailwind()) {
3560
- console.log(chalk16.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
3561
- 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."));
3562
4322
  } else {
3563
- console.log(chalk16.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
3564
- 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."));
3565
4325
  }
3566
- 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:"));
3567
4327
  for (const [key, value] of Object.entries(palette.colors)) {
3568
4328
  console.log(` ${key.padEnd(12)} ${value}`);
3569
4329
  }
@@ -3578,9 +4338,9 @@ async function designPalette() {
3578
4338
  }
3579
4339
 
3580
4340
  // src/commands/theme.ts
3581
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3582
- import chalk17 from "chalk";
3583
- 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";
3584
4344
  function generateDarkCSS() {
3585
4345
  return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
3586
4346
 
@@ -3636,13 +4396,13 @@ export function initTheme(): void {
3636
4396
  `;
3637
4397
  }
3638
4398
  async function theme() {
3639
- console.log(chalk17.bold("\n\u{1F319} " + t("theme.title")));
3640
- 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)));
3641
4401
  const cssPath = "src/styles/theme.css";
3642
4402
  const togglePath = "src/lib/theme-toggle.ts";
3643
- const conflicts = [cssPath, togglePath].filter((p) => existsSync5(p));
4403
+ const conflicts = [cssPath, togglePath].filter((p) => existsSync7(p));
3644
4404
  if (conflicts.length > 0) {
3645
- const { overwrite } = await inquirer8.prompt([{
4405
+ const { overwrite } = await inquirer10.prompt([{
3646
4406
  type: "confirm",
3647
4407
  name: "overwrite",
3648
4408
  message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
@@ -3650,21 +4410,21 @@ async function theme() {
3650
4410
  default: false
3651
4411
  }]);
3652
4412
  if (!overwrite) {
3653
- 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."));
3654
4414
  return;
3655
4415
  }
3656
4416
  }
3657
- mkdirSync4("src/styles", { recursive: true });
3658
- mkdirSync4("src/lib", { recursive: true });
3659
- writeFileSync4(cssPath, generateDarkCSS(), "utf-8");
3660
- console.log(chalk17.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
3661
- writeFileSync4(togglePath, generateToggleUtil(), "utf-8");
3662
- console.log(chalk17.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
3663
- console.log(chalk17.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
3664
- console.log(chalk17.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
3665
- console.log(chalk17.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
3666
- console.log(chalk17.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
3667
- 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"));
3668
4428
  printNextStep({
3669
4429
  message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
3670
4430
  command: "vhk ref list",
@@ -3673,11 +4433,11 @@ async function theme() {
3673
4433
  }
3674
4434
 
3675
4435
  // src/commands/ref.ts
3676
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3677
- import chalk18 from "chalk";
4436
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4437
+ import chalk21 from "chalk";
3678
4438
  var REFS_PATH = ".vhk/refs.json";
3679
4439
  function loadRefs() {
3680
- if (!existsSync6(REFS_PATH)) return [];
4440
+ if (!existsSync8(REFS_PATH)) return [];
3681
4441
  try {
3682
4442
  const parsed = readJsonFile(REFS_PATH);
3683
4443
  return Array.isArray(parsed) ? parsed : [];
@@ -3686,28 +4446,28 @@ function loadRefs() {
3686
4446
  }
3687
4447
  }
3688
4448
  function saveRefs(refs) {
3689
- mkdirSync5(".vhk", { recursive: true });
3690
- 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");
3691
4451
  }
3692
4452
  async function refAdd(url, memo = "") {
3693
- console.log(chalk18.bold("\n\u{1F517} " + t("ref.addTitle")));
3694
- 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)));
3695
4455
  if (!url) {
3696
- console.log(chalk18.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
3697
- 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"'));
3698
4458
  return;
3699
4459
  }
3700
4460
  const refs = loadRefs();
3701
4461
  if (refs.some((r) => r.url === url)) {
3702
- 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."));
3703
4463
  return;
3704
4464
  }
3705
4465
  refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
3706
4466
  saveRefs(refs);
3707
- console.log(chalk18.green(`
4467
+ console.log(chalk21.green(`
3708
4468
  \u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
3709
- console.log(chalk18.cyan(` ${url}`));
3710
- 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}`));
3711
4471
  printNextStep({
3712
4472
  message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
3713
4473
  command: "vhk ref list",
@@ -3715,22 +4475,22 @@ async function refAdd(url, memo = "") {
3715
4475
  });
3716
4476
  }
3717
4477
  async function refList() {
3718
- console.log(chalk18.bold("\n\u{1F4DA} " + t("ref.listTitle")));
3719
- 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)));
3720
4480
  const refs = loadRefs();
3721
4481
  if (refs.length === 0) {
3722
- console.log(chalk18.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
3723
- 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.'));
3724
4484
  return;
3725
4485
  }
3726
- console.log(chalk18.cyan(`
4486
+ console.log(chalk21.cyan(`
3727
4487
  \uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
3728
4488
  `));
3729
4489
  refs.forEach((ref, index) => {
3730
4490
  const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
3731
- console.log(chalk18.white(` [${index + 1}] ${ref.url}`));
3732
- if (ref.memo) console.log(chalk18.gray(` \u{1F4DD} ${ref.memo}`));
3733
- 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}`));
3734
4494
  console.log("");
3735
4495
  });
3736
4496
  }
@@ -3738,7 +4498,7 @@ async function refOpen(indexStr) {
3738
4498
  const refs = loadRefs();
3739
4499
  const idx = parseInt(indexStr, 10) - 1;
3740
4500
  if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
3741
- 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})`));
3742
4502
  return;
3743
4503
  }
3744
4504
  const ref = refs[idx];
@@ -3746,14 +4506,14 @@ async function refOpen(indexStr) {
3746
4506
  try {
3747
4507
  parsed = new URL(ref.url);
3748
4508
  } catch {
3749
- 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}`));
3750
4510
  return;
3751
4511
  }
3752
4512
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3753
- 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})`));
3754
4514
  return;
3755
4515
  }
3756
- console.log(chalk18.cyan(`
4516
+ console.log(chalk21.cyan(`
3757
4517
  \u{1F310} \uC5F4\uAE30: ${ref.url}`));
3758
4518
  let result;
3759
4519
  if (process.platform === "darwin") {
@@ -3764,19 +4524,19 @@ async function refOpen(indexStr) {
3764
4524
  result = safeExecFile("xdg-open", [ref.url]);
3765
4525
  }
3766
4526
  if (result.ok) {
3767
- 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."));
3768
4528
  } else {
3769
- 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."));
3770
4530
  }
3771
4531
  }
3772
4532
 
3773
4533
  // src/commands/harness.ts
3774
- import { existsSync as existsSync7 } from "fs";
3775
- import chalk19 from "chalk";
4534
+ import { existsSync as existsSync9 } from "fs";
4535
+ import chalk22 from "chalk";
3776
4536
  import ora2 from "ora";
3777
4537
  function detectPM() {
3778
- if (existsSync7("pnpm-lock.yaml")) return "pnpm";
3779
- if (existsSync7("yarn.lock")) return "yarn";
4538
+ if (existsSync9("pnpm-lock.yaml")) return "pnpm";
4539
+ if (existsSync9("yarn.lock")) return "yarn";
3780
4540
  return "npm";
3781
4541
  }
3782
4542
  function pmRun(pm, script) {
@@ -3794,14 +4554,14 @@ function detectChecks() {
3794
4554
  const pm = detectPM();
3795
4555
  if (s.lint) {
3796
4556
  checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
3797
- } 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")) {
3798
4558
  checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
3799
4559
  }
3800
4560
  if (s["type-check"]) {
3801
4561
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
3802
4562
  } else if (s.typecheck) {
3803
4563
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
3804
- } else if (existsSync7("tsconfig.json")) {
4564
+ } else if (existsSync9("tsconfig.json")) {
3805
4565
  checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
3806
4566
  }
3807
4567
  if (s.test) {
@@ -3813,15 +4573,16 @@ function detectChecks() {
3813
4573
  return checks;
3814
4574
  }
3815
4575
  async function harness() {
3816
- console.log(chalk19.bold("\n\u{1F527} " + t("harness.title")));
3817
- 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)));
3818
4579
  const checks = detectChecks();
3819
4580
  if (checks.length === 0) {
3820
- console.log(chalk19.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
3821
- 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."));
3822
4583
  return;
3823
4584
  }
3824
- console.log(chalk19.cyan(`
4585
+ console.log(chalk22.cyan(`
3825
4586
  \u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
3826
4587
  `));
3827
4588
  const results = [];
@@ -3833,10 +4594,10 @@ async function harness() {
3833
4594
  const duration = Date.now() - start2;
3834
4595
  const sec = (duration / 1e3).toFixed(1);
3835
4596
  if (result.ok) {
3836
- spinner.succeed(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4597
+ spinner.succeed(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
3837
4598
  results.push({ name: check2.name, command: display, passed: true, duration });
3838
4599
  } else {
3839
- spinner.fail(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4600
+ spinner.fail(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
3840
4601
  results.push({
3841
4602
  name: check2.name,
3842
4603
  command: display,
@@ -3846,24 +4607,25 @@ async function harness() {
3846
4607
  });
3847
4608
  }
3848
4609
  }
3849
- console.log(chalk19.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
3850
- 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)));
3851
4612
  for (const r of results) {
3852
- const icon = r.passed ? chalk19.green("\u2705") : chalk19.red("\u274C");
4613
+ const icon = r.passed ? chalk22.green("\u2705") : chalk22.red("\u274C");
3853
4614
  const sec = (r.duration / 1e3).toFixed(1);
3854
- console.log(` ${icon} ${r.name.padEnd(15)} ${chalk19.gray(`${sec}s`)}`);
4615
+ console.log(` ${icon} ${r.name.padEnd(15)} ${chalk22.gray(`${sec}s`)}`);
3855
4616
  }
3856
4617
  const passed = results.filter((r) => r.passed).length;
3857
4618
  const all = passed === results.length;
3858
- console.log(chalk19.gray("\u2500".repeat(40)));
4619
+ console.log(chalk22.gray("\u2500".repeat(40)));
3859
4620
  if (all) {
3860
- console.log(chalk19.green.bold(`
4621
+ console.log(chalk22.green.bold(`
3861
4622
  \u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
3862
4623
  } else {
3863
4624
  console.log(
3864
- chalk19.red.bold(`
4625
+ chalk22.red.bold(`
3865
4626
  \u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
3866
4627
  );
4628
+ process.exitCode = 1;
3867
4629
  }
3868
4630
  printNextStep({
3869
4631
  message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
@@ -3873,9 +4635,9 @@ async function harness() {
3873
4635
  }
3874
4636
 
3875
4637
  // src/commands/migrate.ts
3876
- import { existsSync as existsSync8, unlinkSync, rmSync } from "fs";
3877
- import chalk20 from "chalk";
3878
- 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";
3879
4641
  import ora3 from "ora";
3880
4642
  var LOCK_FILES = {
3881
4643
  npm: "package-lock.json",
@@ -3883,26 +4645,26 @@ var LOCK_FILES = {
3883
4645
  pnpm: "pnpm-lock.yaml"
3884
4646
  };
3885
4647
  function detectCurrentPM() {
3886
- if (existsSync8("pnpm-lock.yaml")) return "pnpm";
3887
- if (existsSync8("yarn.lock")) return "yarn";
3888
- 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";
3889
4651
  return null;
3890
4652
  }
3891
4653
  function isCLIAvailable(pm) {
3892
4654
  return safeExecFile(pm, ["--version"]).ok;
3893
4655
  }
3894
4656
  async function migrate(target) {
3895
- console.log(chalk20.bold("\n\u{1F504} " + t("migrate.title")));
3896
- 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)));
3897
4659
  const current = detectCurrentPM();
3898
- console.log(chalk20.cyan(`
4660
+ console.log(chalk23.cyan(`
3899
4661
  \uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
3900
4662
  let targetPM;
3901
4663
  if (target && ["npm", "yarn", "pnpm"].includes(target)) {
3902
4664
  targetPM = target;
3903
4665
  } else {
3904
4666
  const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
3905
- const { selected } = await inquirer9.prompt([
4667
+ const { selected } = await inquirer11.prompt([
3906
4668
  {
3907
4669
  type: "list",
3908
4670
  name: "selected",
@@ -3913,17 +4675,17 @@ async function migrate(target) {
3913
4675
  targetPM = selected;
3914
4676
  }
3915
4677
  if (targetPM === current) {
3916
- console.log(chalk20.yellow(`
4678
+ console.log(chalk23.yellow(`
3917
4679
  \u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
3918
4680
  return;
3919
4681
  }
3920
4682
  if (!isCLIAvailable(targetPM)) {
3921
- console.log(chalk20.red(`
4683
+ console.log(chalk23.red(`
3922
4684
  \u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
3923
- console.log(chalk20.yellow(` npm i -g ${targetPM}`));
4685
+ console.log(chalk23.yellow(` npm i -g ${targetPM}`));
3924
4686
  return;
3925
4687
  }
3926
- const { confirm } = await inquirer9.prompt([
4688
+ const { confirm } = await inquirer11.prompt([
3927
4689
  {
3928
4690
  type: "confirm",
3929
4691
  name: "confirm",
@@ -3932,18 +4694,18 @@ async function migrate(target) {
3932
4694
  }
3933
4695
  ]);
3934
4696
  if (!confirm) {
3935
- console.log(chalk20.gray("\uCDE8\uC18C\uB428"));
4697
+ console.log(chalk23.gray("\uCDE8\uC18C\uB428"));
3936
4698
  return;
3937
4699
  }
3938
4700
  const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
3939
4701
  for (const lockFile of Object.values(LOCK_FILES)) {
3940
- if (existsSync8(lockFile)) {
4702
+ if (existsSync10(lockFile)) {
3941
4703
  unlinkSync(lockFile);
3942
4704
  }
3943
4705
  }
3944
- if (existsSync8("node_modules")) {
4706
+ if (existsSync10("node_modules")) {
3945
4707
  cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
3946
- rmSync("node_modules", { recursive: true, force: true });
4708
+ rmSync2("node_modules", { recursive: true, force: true });
3947
4709
  }
3948
4710
  cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
3949
4711
  const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
@@ -3952,10 +4714,10 @@ async function migrate(target) {
3952
4714
  install.succeed(`${targetPM} install \uC644\uB8CC!`);
3953
4715
  } else {
3954
4716
  install.fail(`${targetPM} install \uC2E4\uD328`);
3955
- console.log(chalk20.red(installResult.err.slice(0, 300)));
4717
+ console.log(chalk23.red(installResult.err.slice(0, 300)));
3956
4718
  return;
3957
4719
  }
3958
- console.log(chalk20.green.bold(`
4720
+ console.log(chalk23.green.bold(`
3959
4721
  \u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
3960
4722
  printNextStep({
3961
4723
  message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
@@ -3965,17 +4727,17 @@ async function migrate(target) {
3965
4727
  }
3966
4728
 
3967
4729
  // src/commands/update.ts
3968
- import { existsSync as existsSync9 } from "fs";
3969
- 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";
3970
4732
  import { fileURLToPath as fileURLToPath3 } from "url";
3971
- import chalk21 from "chalk";
4733
+ import chalk24 from "chalk";
3972
4734
  import ora4 from "ora";
3973
4735
  var PACKAGE = "@byh3071/vhk";
3974
4736
  function getCurrentVersion() {
3975
4737
  const dir = dirname2(fileURLToPath3(import.meta.url));
3976
- 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")]) {
3977
4739
  try {
3978
- if (existsSync9(pkgPath)) {
4740
+ if (existsSync11(pkgPath)) {
3979
4741
  const pkg = readJsonFile(pkgPath);
3980
4742
  if (pkg.version) return pkg.version;
3981
4743
  }
@@ -3998,32 +4760,32 @@ function isUpToDate(current, latest) {
3998
4760
  return cc >= lc;
3999
4761
  }
4000
4762
  async function update() {
4001
- console.log(chalk21.bold("\n\u2B06\uFE0F " + t("update.title")));
4002
- 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)));
4003
4765
  const current = getCurrentVersion();
4004
- console.log(chalk21.cyan(`
4766
+ console.log(chalk24.cyan(`
4005
4767
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
4006
4768
  const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
4007
4769
  const latest = getLatestVersion();
4008
4770
  if (!latest) {
4009
4771
  spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4010
- 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:"));
4011
- 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}`));
4012
4774
  return;
4013
4775
  }
4014
4776
  spinner.stop();
4015
- 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}`));
4016
4778
  if (isUpToDate(current, latest)) {
4017
- 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!"));
4018
4780
  return;
4019
4781
  }
4020
4782
  const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
4021
4783
  const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
4022
4784
  if (upd.ok) {
4023
4785
  updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
4024
- console.log(chalk21.green.bold(`
4786
+ console.log(chalk24.green.bold(`
4025
4787
  \u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
4026
- 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."));
4027
4789
  printNextStep({
4028
4790
  message: t("update.nextOkMessage"),
4029
4791
  command: "vhk --version",
@@ -4031,9 +4793,9 @@ async function update() {
4031
4793
  });
4032
4794
  } else {
4033
4795
  updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
4034
- console.log(chalk21.red(upd.err.slice(0, 300)));
4035
- console.log(chalk21.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4036
- 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}`));
4037
4799
  printNextStep({
4038
4800
  message: t("update.nextFailMessage"),
4039
4801
  command: "vhk doctor",
@@ -4044,121 +4806,15 @@ async function update() {
4044
4806
 
4045
4807
  // src/commands/context.ts
4046
4808
  import {
4047
- existsSync as existsSync11,
4809
+ existsSync as existsSync12,
4048
4810
  mkdirSync as mkdirSync7,
4049
- readFileSync as readFileSync4,
4811
+ readFileSync as readFileSync5,
4050
4812
  readdirSync as readdirSync2,
4051
4813
  statSync as statSync2,
4052
4814
  writeFileSync as writeFileSync7
4053
4815
  } from "fs";
4054
- import { join as join6 } from "path";
4055
- import chalk22 from "chalk";
4056
-
4057
- // src/lib/state-files.ts
4058
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6, appendFileSync, rmSync as rmSync2 } from "fs";
4059
- import { join as join5 } from "path";
4060
- var STATE_DIR2 = "docs/state";
4061
- var BLOCKERS_PATH = join5(STATE_DIR2, "blockers.md");
4062
- var LEARNINGS_PATH = join5(STATE_DIR2, "learnings.md");
4063
- var VHK_DIR = ".vhk";
4064
- var HARD_STOP_PATH = join5(VHK_DIR, "HARD_STOP");
4065
- var HARD_STOP_BLOCKER_THRESHOLD = 3;
4066
- function ensureStateDir() {
4067
- mkdirSync6(STATE_DIR2, { recursive: true });
4068
- }
4069
- function ensureVhkDir() {
4070
- mkdirSync6(VHK_DIR, { recursive: true });
4071
- }
4072
- function isoDate() {
4073
- return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4074
- }
4075
- var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
4076
- function countActiveBlockers(content) {
4077
- let count = 0;
4078
- for (const line of content.split(/\r?\n/)) {
4079
- if (ACTIVE_BLOCKER_RE.test(line)) count++;
4080
- }
4081
- return count;
4082
- }
4083
- function appendBlocker(description, goalId) {
4084
- ensureStateDir();
4085
- const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
4086
- const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
4087
- if (!existsSync10(BLOCKERS_PATH)) {
4088
- writeFileSync6(
4089
- BLOCKERS_PATH,
4090
- `# Blockers
4091
-
4092
- _Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
4093
-
4094
- ${line}
4095
- `,
4096
- "utf-8"
4097
- );
4098
- } else {
4099
- appendFileSync(BLOCKERS_PATH, `${line}
4100
- `, "utf-8");
4101
- }
4102
- const current = readFileSync3(BLOCKERS_PATH, "utf-8");
4103
- const count = countActiveBlockers(current);
4104
- let hardStopTripped = false;
4105
- if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync10(HARD_STOP_PATH)) {
4106
- writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
4107
- hardStopTripped = true;
4108
- }
4109
- return { count, hardStopTripped };
4110
- }
4111
- function appendLearning(lesson, goalId) {
4112
- ensureStateDir();
4113
- const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
4114
- const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
4115
- if (!existsSync10(LEARNINGS_PATH)) {
4116
- writeFileSync6(
4117
- LEARNINGS_PATH,
4118
- `# Learnings
4119
-
4120
- _Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
4121
-
4122
- ${line}
4123
- `,
4124
- "utf-8"
4125
- );
4126
- } else {
4127
- appendFileSync(LEARNINGS_PATH, `${line}
4128
- `, "utf-8");
4129
- }
4130
- }
4131
- function getRecentLearnings(limit = 3) {
4132
- if (!existsSync10(LEARNINGS_PATH)) return [];
4133
- const lines = readFileSync3(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
4134
- const entries = lines.filter((l) => l.startsWith("- ["));
4135
- return entries.slice(-limit);
4136
- }
4137
- function writeHardStop(reason) {
4138
- ensureVhkDir();
4139
- const ts = (/* @__PURE__ */ new Date()).toISOString();
4140
- writeFileSync6(HARD_STOP_PATH, `${ts}
4141
- ${reason}
4142
- `, "utf-8");
4143
- }
4144
- function isHardStopActive() {
4145
- return existsSync10(HARD_STOP_PATH);
4146
- }
4147
- function readHardStopReason() {
4148
- if (!existsSync10(HARD_STOP_PATH)) return null;
4149
- try {
4150
- return readFileSync3(HARD_STOP_PATH, "utf-8").trim();
4151
- } catch {
4152
- return null;
4153
- }
4154
- }
4155
- function clearHardStop() {
4156
- if (!existsSync10(HARD_STOP_PATH)) return false;
4157
- rmSync2(HARD_STOP_PATH, { force: true });
4158
- return true;
4159
- }
4160
-
4161
- // src/commands/context.ts
4816
+ import { join as join8 } from "path";
4817
+ import chalk25 from "chalk";
4162
4818
  var CONTEXT_PATH2 = ".vhk/context.md";
4163
4819
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
4164
4820
  "node_modules",
@@ -4183,7 +4839,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
4183
4839
  filtered.forEach((entry, index) => {
4184
4840
  const isLast = index === filtered.length - 1;
4185
4841
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4186
- const fullPath = join6(dir, entry);
4842
+ const fullPath = join8(dir, entry);
4187
4843
  const stat = statSync2(fullPath);
4188
4844
  const isDir = stat.isDirectory();
4189
4845
  lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
@@ -4215,8 +4871,8 @@ function extractTechStack() {
4215
4871
  else if (all.jest) stack["\uD14C\uC2A4\uD2B8"] = "jest";
4216
4872
  if (all.commander) stack["CLI"] = "commander";
4217
4873
  if (all.inquirer) stack["\uC778\uD130\uB799\uD2F0\uBE0C"] = "inquirer";
4218
- if (existsSync11("pnpm-lock.yaml")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "pnpm";
4219
- 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";
4220
4876
  else stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "npm";
4221
4877
  if (pkg.name) stack["\uD328\uD0A4\uC9C0 \uC774\uB984"] = pkg.name;
4222
4878
  if (pkg.version) stack["\uBC84\uC804"] = pkg.version;
@@ -4257,11 +4913,12 @@ function getVhkCommands() {
4257
4913
  "mcp-init \u2014 Cursor MCP \uC124\uC815"
4258
4914
  ];
4259
4915
  }
4260
- async function context() {
4261
- console.log(chalk22.bold("\n\u{1F9E0} " + t("context.title")));
4262
- 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)));
4263
4920
  const stack = extractTechStack();
4264
- const tree = buildTree(".").join("\n");
4921
+ const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
4265
4922
  const commands = getVhkCommands();
4266
4923
  const lines = [];
4267
4924
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
@@ -4277,25 +4934,37 @@ async function context() {
4277
4934
  lines.push("");
4278
4935
  lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
4279
4936
  lines.push("");
4280
- lines.push("```");
4937
+ lines.push("```text");
4281
4938
  lines.push(tree);
4282
4939
  lines.push("```");
4283
4940
  lines.push("");
4284
- lines.push("## VHK CLI \uBA85\uB839\uC5B4");
4285
- lines.push("");
4286
- for (const cmd of commands) {
4287
- 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("");
4288
4953
  }
4289
- lines.push("");
4290
- if (existsSync11(".vhk/memory.json")) {
4954
+ if (existsSync12(".vhk/memory.json")) {
4291
4955
  try {
4292
4956
  const memories = readJsonFile(
4293
4957
  ".vhk/memory.json"
4294
4958
  );
4295
4959
  if (Array.isArray(memories) && memories.length > 0) {
4960
+ const recentMemories = memories.slice(-5);
4296
4961
  lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
4297
4962
  lines.push("");
4298
- 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) {
4299
4968
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4300
4969
  lines.push(`- ${m.content} _(${date})_`);
4301
4970
  }
@@ -4326,6 +4995,24 @@ async function context() {
4326
4995
  for (const r of recent) lines.push(r);
4327
4996
  lines.push("");
4328
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
+ }
4329
5016
  if (isHardStopActive()) {
4330
5017
  lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
4331
5018
  lines.push("");
@@ -4344,10 +5031,10 @@ async function context() {
4344
5031
  lines.push("");
4345
5032
  mkdirSync7(".vhk", { recursive: true });
4346
5033
  writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
4347
- console.log(chalk22.green(`
5034
+ console.log(chalk25.green(`
4348
5035
  \u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
4349
- console.log(chalk22.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
4350
- 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."));
4351
5038
  printNextStep({
4352
5039
  message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
4353
5040
  command: "vhk context-show",
@@ -4355,23 +5042,23 @@ async function context() {
4355
5042
  });
4356
5043
  }
4357
5044
  async function contextShow() {
4358
- console.log(chalk22.bold("\n\u{1F4C4} " + t("context.showTitle")));
4359
- console.log(chalk22.gray("\u2500".repeat(40)));
4360
- if (!existsSync11(CONTEXT_PATH2)) {
4361
- console.log(chalk22.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4362
- 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."));
4363
5050
  return;
4364
5051
  }
4365
- const content = readFileSync4(CONTEXT_PATH2, "utf-8");
5052
+ const content = readFileSync5(CONTEXT_PATH2, "utf-8");
4366
5053
  console.log("\n" + content);
4367
5054
  }
4368
5055
 
4369
5056
  // src/commands/memory.ts
4370
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
4371
- import chalk23 from "chalk";
5057
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
5058
+ import chalk26 from "chalk";
4372
5059
  var MEMORY_PATH = ".vhk/memory.json";
4373
5060
  function loadMemories() {
4374
- if (!existsSync12(MEMORY_PATH)) return [];
5061
+ if (!existsSync13(MEMORY_PATH)) return [];
4375
5062
  try {
4376
5063
  const parsed = readJsonFile(MEMORY_PATH);
4377
5064
  return Array.isArray(parsed) ? parsed : [];
@@ -4384,11 +5071,12 @@ function saveMemories(memories) {
4384
5071
  writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
4385
5072
  }
4386
5073
  async function memoryAdd(content, tags) {
4387
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.addTitle")));
4388
- 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)));
4389
5076
  if (!content) {
4390
- console.log(chalk23.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
4391
- 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;
4392
5080
  return;
4393
5081
  }
4394
5082
  const memories = loadMemories();
@@ -4398,9 +5086,9 @@ async function memoryAdd(content, tags) {
4398
5086
  tags: tags && tags.length > 0 ? tags : []
4399
5087
  });
4400
5088
  saveMemories(memories);
4401
- console.log(chalk23.green(`
5089
+ console.log(chalk26.green(`
4402
5090
  \u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
4403
- console.log(chalk23.cyan(` \u{1F4DD} ${content}`));
5091
+ console.log(chalk26.cyan(` \u{1F4DD} ${content}`));
4404
5092
  printNextStep({
4405
5093
  message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
4406
5094
  command: "vhk memory list",
@@ -4408,24 +5096,24 @@ async function memoryAdd(content, tags) {
4408
5096
  });
4409
5097
  }
4410
5098
  async function memoryList() {
4411
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.listTitle")));
4412
- 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)));
4413
5101
  const memories = loadMemories();
4414
5102
  if (memories.length === 0) {
4415
- console.log(chalk23.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4416
- 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.'));
4417
5105
  return;
4418
5106
  }
4419
- console.log(chalk23.cyan(`
5107
+ console.log(chalk26.cyan(`
4420
5108
  \uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
4421
5109
  `));
4422
5110
  memories.forEach((m, index) => {
4423
5111
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4424
- console.log(chalk23.white(` [${index + 1}] ${m.content}`));
5112
+ console.log(chalk26.white(` [${index + 1}] ${m.content}`));
4425
5113
  if (m.tags && m.tags.length > 0) {
4426
- console.log(chalk23.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
5114
+ console.log(chalk26.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
4427
5115
  }
4428
- console.log(chalk23.gray(` \u{1F4C5} ${date}`));
5116
+ console.log(chalk26.gray(` \u{1F4C5} ${date}`));
4429
5117
  console.log("");
4430
5118
  });
4431
5119
  }
@@ -4433,26 +5121,44 @@ async function memoryRemove(indexStr) {
4433
5121
  const memories = loadMemories();
4434
5122
  const idx = parseInt(indexStr, 10) - 1;
4435
5123
  if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
4436
- 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})`));
4437
5125
  return;
4438
5126
  }
4439
5127
  const removed = memories.splice(idx, 1)[0];
4440
5128
  saveMemories(memories);
4441
- console.log(chalk23.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
4442
- 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}`));
4443
5131
  }
4444
5132
 
4445
5133
  // src/commands/brief.ts
4446
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4447
- 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";
4448
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
+ }
4449
5155
  function git2(args) {
4450
5156
  const result = safeExecFile("git", args);
4451
5157
  return result.ok ? result.out : "";
4452
5158
  }
4453
5159
  async function brief() {
4454
- console.log(chalk24.bold("\n\u{1F4CB} " + t("brief.title")));
4455
- 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)));
4456
5162
  const lines = [];
4457
5163
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
4458
5164
  lines.push("");
@@ -4460,11 +5166,12 @@ async function brief() {
4460
5166
  lines.push("");
4461
5167
  try {
4462
5168
  const pkg = readJsonFile("package.json");
5169
+ const id = readProjectIdentity();
4463
5170
  lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
4464
5171
  lines.push("");
4465
- lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
5172
+ lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
4466
5173
  lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
4467
- lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
5174
+ lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
4468
5175
  const deps = Object.keys(pkg.dependencies ?? {}).length;
4469
5176
  const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
4470
5177
  lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
@@ -4488,7 +5195,7 @@ async function brief() {
4488
5195
  `- **\uBBF8\uCEE4\uBC0B \uBCC0\uACBD**: ${uncommitted ? `${uncommitted.split("\n").length}\uAC1C \uD30C\uC77C` : "\uC5C6\uC74C \u2705"}`
4489
5196
  );
4490
5197
  lines.push("");
4491
- if (existsSync13(".vhk/memory.json")) {
5198
+ if (existsSync14(".vhk/memory.json")) {
4492
5199
  try {
4493
5200
  const memories = readJsonFile(".vhk/memory.json");
4494
5201
  if (Array.isArray(memories) && memories.length > 0) {
@@ -4505,7 +5212,7 @@ async function brief() {
4505
5212
  } catch {
4506
5213
  }
4507
5214
  }
4508
- if (existsSync13(".vhk/refs.json")) {
5215
+ if (existsSync14(".vhk/refs.json")) {
4509
5216
  try {
4510
5217
  const refs = readJsonFile(".vhk/refs.json");
4511
5218
  if (Array.isArray(refs) && refs.length > 0) {
@@ -4536,7 +5243,7 @@ async function brief() {
4536
5243
  mkdirSync9(".vhk", { recursive: true });
4537
5244
  writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
4538
5245
  console.log("\n" + lines.join("\n"));
4539
- console.log(chalk24.green(`
5246
+ console.log(chalk27.green(`
4540
5247
  \u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
4541
5248
  printNextStep({
4542
5249
  message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
@@ -4546,11 +5253,11 @@ async function brief() {
4546
5253
  }
4547
5254
 
4548
5255
  // src/commands/start.ts
4549
- import chalk25 from "chalk";
4550
- import inquirer10 from "inquirer";
5256
+ import chalk28 from "chalk";
5257
+ import inquirer12 from "inquirer";
4551
5258
  import { simpleGit as simpleGit2 } from "simple-git";
4552
- import { existsSync as existsSync14 } from "fs";
4553
- import { join as join7 } from "path";
5259
+ import { existsSync as existsSync15 } from "fs";
5260
+ import { join as join9 } from "path";
4554
5261
  var VHK_FOOTPRINT_FILES = [
4555
5262
  "CLAUDE.md",
4556
5263
  ".cursorrules",
@@ -4559,7 +5266,7 @@ var VHK_FOOTPRINT_FILES = [
4559
5266
  "docs/PRD.md"
4560
5267
  ];
4561
5268
  function detectExistingFootprint(cwd) {
4562
- return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join7(cwd, rel)));
5269
+ return VHK_FOOTPRINT_FILES.filter((rel) => existsSync15(join9(cwd, rel)));
4563
5270
  }
4564
5271
  async function runGitInit(cwd) {
4565
5272
  try {
@@ -4588,22 +5295,22 @@ async function runStep(label, fn) {
4588
5295
  }
4589
5296
  }
4590
5297
  async function start(options = {}) {
4591
- console.log(chalk25.bold(`
5298
+ console.log(chalk28.bold(`
4592
5299
  ${ko.start.title}
4593
5300
  `));
4594
- console.log(chalk25.dim(ko.start.intro));
4595
- console.log(chalk25.dim(` ${ko.start.step1}`));
4596
- console.log(chalk25.dim(` ${ko.start.step2}`));
4597
- console.log(chalk25.dim(` ${ko.start.step3}`));
4598
- 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}`));
4599
5306
  console.log();
4600
5307
  const cwd = process.cwd();
4601
5308
  const footprint = detectExistingFootprint(cwd);
4602
5309
  if (footprint.length > 0 && !options.yes) {
4603
- console.log(chalk25.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
4604
- for (const f of footprint) console.log(chalk25.dim(` - ${f}`));
4605
- 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."));
4606
- 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([{
4607
5314
  type: "confirm",
4608
5315
  name: "proceedExisting",
4609
5316
  message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
@@ -4614,7 +5321,7 @@ ${ko.start.title}
4614
5321
  return;
4615
5322
  }
4616
5323
  } else if (!options.yes) {
4617
- const { proceed } = await inquirer10.prompt([{
5324
+ const { proceed } = await inquirer12.prompt([{
4618
5325
  type: "confirm",
4619
5326
  name: "proceed",
4620
5327
  message: ko.start.confirmStart,
@@ -4640,7 +5347,7 @@ ${ko.start.title}
4640
5347
  await runStep("[3/4] vhk mcp-init", () => mcpInit());
4641
5348
  log.step(ko.start.step4Header);
4642
5349
  await runStep("[4/4] vhk context", () => context());
4643
- console.log(chalk25.bold.green(`
5350
+ console.log(chalk28.bold.green(`
4644
5351
  ${ko.start.allDone}
4645
5352
  `));
4646
5353
  printNextStep({
@@ -4650,15 +5357,15 @@ ${ko.start.allDone}
4650
5357
  }
4651
5358
 
4652
5359
  // src/commands/cloud.ts
4653
- import fs14 from "fs";
5360
+ import fs15 from "fs";
4654
5361
  import os from "os";
4655
- import path15 from "path";
4656
- import chalk26 from "chalk";
5362
+ import path16 from "path";
5363
+ import chalk29 from "chalk";
4657
5364
 
4658
5365
  // src/lib/vhk-cloud.ts
4659
5366
  var import_ignore = __toESM(require_ignore(), 1);
4660
- import fs13 from "fs";
4661
- import path14 from "path";
5367
+ import fs14 from "fs";
5368
+ import path15 from "path";
4662
5369
  var DEFAULT_CLOUD_EXCLUDES = [
4663
5370
  "memory.json",
4664
5371
  // 개인 의사결정 메모
@@ -4676,17 +5383,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
4676
5383
  function loadVhkignore(rootDir) {
4677
5384
  const ig = (0, import_ignore.default)();
4678
5385
  ig.add(DEFAULT_CLOUD_EXCLUDES);
4679
- const ignorePath = path14.join(rootDir, ".vhkignore");
4680
- if (fs13.existsSync(ignorePath)) {
4681
- 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"));
4682
5389
  }
4683
5390
  return ig;
4684
5391
  }
4685
5392
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
4686
- const vhkDir = path14.join(rootDir, VHK_DIR2);
5393
+ const vhkDir = path15.join(rootDir, VHK_DIR2);
4687
5394
  let entries;
4688
5395
  try {
4689
- entries = fs13.readdirSync(vhkDir, { withFileTypes: true });
5396
+ entries = fs14.readdirSync(vhkDir, { withFileTypes: true });
4690
5397
  } catch {
4691
5398
  return [];
4692
5399
  }
@@ -4702,10 +5409,10 @@ function partitionGistFiles(gistFiles, ig) {
4702
5409
  return { keep, excluded };
4703
5410
  }
4704
5411
  function readCloudConfig(rootDir) {
4705
- const p = path14.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4706
- if (!fs13.existsSync(p)) return null;
5412
+ const p = path15.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5413
+ if (!fs14.existsSync(p)) return null;
4707
5414
  try {
4708
- const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
5415
+ const parsed = JSON.parse(fs14.readFileSync(p, "utf-8"));
4709
5416
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
4710
5417
  return { gistId: parsed.gistId };
4711
5418
  }
@@ -4715,24 +5422,24 @@ function readCloudConfig(rootDir) {
4715
5422
  }
4716
5423
  }
4717
5424
  function writeCloudConfig(rootDir, config) {
4718
- const vhkDir = path14.join(rootDir, VHK_DIR2);
4719
- fs13.mkdirSync(vhkDir, { recursive: true });
4720
- const p = path14.join(vhkDir, CLOUD_CONFIG_FILE);
4721
- 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");
4722
5429
  }
4723
5430
 
4724
5431
  // src/commands/cloud.ts
4725
5432
  function ensureGhReady() {
4726
5433
  const ver = safeExecFile("gh", ["--version"]);
4727
5434
  if (!ver.ok) {
4728
- console.log(chalk26.red(` ${ko.cloud.noGh}`));
4729
- 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`)"));
4730
5437
  return false;
4731
5438
  }
4732
5439
  const auth = safeExecFile("gh", ["auth", "status"]);
4733
5440
  if (!auth.ok) {
4734
- console.log(chalk26.red(` ${ko.cloud.noAuth}`));
4735
- 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)"));
4736
5443
  return false;
4737
5444
  }
4738
5445
  return true;
@@ -4745,29 +5452,29 @@ function parseGistId(output) {
4745
5452
  return null;
4746
5453
  }
4747
5454
  async function cloudPush() {
4748
- console.log(chalk26.bold(`
5455
+ console.log(chalk29.bold(`
4749
5456
  ${ko.cloud.pushTitle}
4750
5457
  `));
4751
5458
  const cwd = process.cwd();
4752
- if (!fs14.existsSync(path15.join(cwd, VHK_DIR2))) {
4753
- console.log(chalk26.yellow(` ${ko.cloud.noVhkDir}`));
5459
+ if (!fs15.existsSync(path16.join(cwd, VHK_DIR2))) {
5460
+ console.log(chalk29.yellow(` ${ko.cloud.noVhkDir}`));
4754
5461
  return;
4755
5462
  }
4756
5463
  const ig = loadVhkignore(cwd);
4757
5464
  const files = collectVhkFiles(cwd, ig);
4758
5465
  if (files.length === 0) {
4759
- console.log(chalk26.yellow(` ${ko.cloud.nothingToSync}`));
5466
+ console.log(chalk29.yellow(` ${ko.cloud.nothingToSync}`));
4760
5467
  return;
4761
5468
  }
4762
5469
  if (!ensureGhReady()) {
4763
5470
  process.exitCode = 1;
4764
5471
  return;
4765
5472
  }
4766
- const filePaths = files.map((f) => path15.join(cwd, VHK_DIR2, f));
4767
- 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(", ")}
4768
5475
  `));
4769
5476
  const existing = readCloudConfig(cwd);
4770
- const desc = `vhk .vhk backup \u2014 ${path15.basename(cwd)}`;
5477
+ const desc = `vhk .vhk backup \u2014 ${path16.basename(cwd)}`;
4771
5478
  if (existing) {
4772
5479
  const gistFiles = listGistFiles(existing.gistId);
4773
5480
  for (let i = 0; i < files.length; i++) {
@@ -4776,8 +5483,8 @@ ${ko.cloud.pushTitle}
4776
5483
  const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
4777
5484
  const res2 = safeExecFile("gh", args);
4778
5485
  if (!res2.ok) {
4779
- console.log(chalk26.red(` ${ko.cloud.pushFail}: ${name}`));
4780
- console.log(chalk26.dim(` ${res2.err}`));
5486
+ console.log(chalk29.red(` ${ko.cloud.pushFail}: ${name}`));
5487
+ console.log(chalk29.dim(` ${res2.err}`));
4781
5488
  process.exitCode = 1;
4782
5489
  return;
4783
5490
  }
@@ -4792,15 +5499,15 @@ ${ko.cloud.pushTitle}
4792
5499
  if (!purgeFailed.includes(name)) purgeFailed.push(name);
4793
5500
  }
4794
5501
  }
4795
- console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4796
- 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)`));
4797
5504
  if (excluded.length > 0) {
4798
5505
  const purged = excluded.filter((n) => !purgeFailed.includes(n));
4799
5506
  if (purged.length > 0) {
4800
- 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(", ")}`));
4801
5508
  }
4802
5509
  if (purgeFailed.length > 0) {
4803
- 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)`));
4804
5511
  }
4805
5512
  }
4806
5513
  printPushNext();
@@ -4808,32 +5515,32 @@ ${ko.cloud.pushTitle}
4808
5515
  }
4809
5516
  const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
4810
5517
  if (!res.ok) {
4811
- console.log(chalk26.red(` ${ko.cloud.pushFail}`));
4812
- 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}`));
4813
5520
  process.exitCode = 1;
4814
5521
  return;
4815
5522
  }
4816
5523
  const gistId = parseGistId(res.out);
4817
5524
  if (!gistId) {
4818
- console.log(chalk26.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
4819
- 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}`));
4820
5527
  process.exitCode = 1;
4821
5528
  return;
4822
5529
  }
4823
5530
  writeCloudConfig(cwd, { gistId });
4824
- console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4825
- 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`));
4826
5533
  printPushNext();
4827
5534
  }
4828
5535
  async function cloudPull(gistIdArg) {
4829
- console.log(chalk26.bold(`
5536
+ console.log(chalk29.bold(`
4830
5537
  ${ko.cloud.pullTitle}
4831
5538
  `));
4832
5539
  const cwd = process.cwd();
4833
5540
  const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
4834
5541
  if (!gistId) {
4835
- console.log(chalk26.yellow(` ${ko.cloud.noGistId}`));
4836
- 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)"));
4837
5544
  return;
4838
5545
  }
4839
5546
  if (!ensureGhReady()) {
@@ -4842,34 +5549,34 @@ ${ko.cloud.pullTitle}
4842
5549
  }
4843
5550
  const allNames = listGistFiles(gistId);
4844
5551
  if (allNames.length === 0) {
4845
- 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}`));
4846
5553
  process.exitCode = 1;
4847
5554
  return;
4848
5555
  }
4849
5556
  const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
4850
5557
  if (skipped.length > 0) {
4851
- 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(", ")}`));
4852
5559
  }
4853
5560
  if (names.length === 0) {
4854
- 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).`));
4855
5562
  return;
4856
5563
  }
4857
- const vhkDir = path15.join(cwd, VHK_DIR2);
4858
- fs14.mkdirSync(vhkDir, { recursive: true });
5564
+ const vhkDir = path16.join(cwd, VHK_DIR2);
5565
+ fs15.mkdirSync(vhkDir, { recursive: true });
4859
5566
  let restored = 0;
4860
5567
  for (const name of names) {
4861
5568
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
4862
5569
  if (!res.ok) {
4863
- console.log(chalk26.red(` ${ko.cloud.pullFail}: ${name}`));
4864
- console.log(chalk26.dim(` ${res.err}`));
5570
+ console.log(chalk29.red(` ${ko.cloud.pullFail}: ${name}`));
5571
+ console.log(chalk29.dim(` ${res.err}`));
4865
5572
  continue;
4866
5573
  }
4867
- fs14.writeFileSync(path15.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
5574
+ fs15.writeFileSync(path16.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4868
5575
  restored++;
4869
5576
  }
4870
5577
  writeCloudConfig(cwd, { gistId });
4871
- console.log(chalk26.green.bold(` ${ko.cloud.pullDone}`));
4872
- 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})`));
4873
5580
  printNextStep({
4874
5581
  message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
4875
5582
  command: "vhk \uB9E5\uB77D",
@@ -4881,9 +5588,9 @@ function purgeExcludedFromGist(gistId, names) {
4881
5588
  const body = JSON.stringify({
4882
5589
  files: Object.fromEntries(names.map((n) => [n, null]))
4883
5590
  });
4884
- 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`);
4885
5592
  try {
4886
- fs14.writeFileSync(tmp, body, "utf-8");
5593
+ fs15.writeFileSync(tmp, body, "utf-8");
4887
5594
  for (let attempt = 0; attempt < 2; attempt++) {
4888
5595
  const res = safeExecFile(
4889
5596
  "gh",
@@ -4895,7 +5602,7 @@ function purgeExcludedFromGist(gistId, names) {
4895
5602
  return false;
4896
5603
  } finally {
4897
5604
  try {
4898
- fs14.unlinkSync(tmp);
5605
+ fs15.unlinkSync(tmp);
4899
5606
  } catch {
4900
5607
  }
4901
5608
  }
@@ -4916,6 +5623,198 @@ function printPushNext() {
4916
5623
  });
4917
5624
  }
4918
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
+
4919
5818
  // src/lib/nlp-run.ts
4920
5819
  async function dispatchNlpRoute(route, input) {
4921
5820
  switch (route.command) {
@@ -4946,6 +5845,8 @@ async function dispatchNlpRoute(route, input) {
4946
5845
  return save();
4947
5846
  case "undo":
4948
5847
  return undo();
5848
+ case "restore":
5849
+ return restore(route.args?.[0]);
4949
5850
  case "status":
4950
5851
  return status();
4951
5852
  case "diff":
@@ -4995,111 +5896,167 @@ async function dispatchNlpRoute(route, input) {
4995
5896
  if (sub === "done") return goalDone({});
4996
5897
  return goalList();
4997
5898
  }
5899
+ case "help":
5900
+ return quickActions();
5901
+ case "mode":
5902
+ return mode();
5903
+ case "verify":
5904
+ return verify();
4998
5905
  }
4999
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
+ }
5000
5914
  async function runNaturalLanguageRoute(input) {
5001
5915
  const route = routeNaturalLanguage(input);
5002
5916
  if (!route) {
5003
- console.log(chalk27.yellow(`
5917
+ console.log(chalk33.yellow(`
5004
5918
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
5005
5919
  `));
5006
5920
  return;
5007
5921
  }
5008
5922
  console.log("");
5009
- console.log(chalk27.cyan(` \u{1F4AC} "${input}"`));
5010
- console.log(chalk27.cyan(` \u2192 ${route.explanation}`));
5011
- if (route.confidence === "low") {
5012
- 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([{
5013
5927
  type: "confirm",
5014
5928
  name: "confirm",
5015
5929
  message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
5016
5930
  default: true
5017
5931
  }]);
5018
5932
  if (!confirm) {
5019
- console.log(chalk27.dim(` ${ko.nlp.menuHint}`));
5933
+ console.log(chalk33.dim(` ${ko.nlp.menuHint}`));
5020
5934
  return;
5021
5935
  }
5022
5936
  }
5023
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
+ }
5024
5947
  await dispatchNlpRoute(route, input);
5025
5948
  }
5026
5949
 
5027
5950
  // src/commands/agent.ts
5028
- import chalk28 from "chalk";
5951
+ import chalk34 from "chalk";
5029
5952
  function activeGoalId() {
5030
5953
  const goals = listGoals("goals");
5031
5954
  const id = selectActiveId(goals);
5032
5955
  return id ?? void 0;
5033
5956
  }
5034
5957
  async function blocker(description) {
5035
- console.log(chalk28.bold(`
5958
+ console.log(chalk34.bold(`
5036
5959
  ${ko.agent.blockerTitle}
5037
5960
  `));
5038
5961
  if (!description || !description.trim()) {
5039
- console.log(chalk28.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5040
- 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"'));
5041
5964
  process.exitCode = 1;
5042
5965
  return;
5043
5966
  }
5044
5967
  const goalId = activeGoalId();
5045
5968
  const r = appendBlocker(description, goalId);
5046
- 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)`));
5047
5970
  if (r.hardStopTripped) {
5048
- console.log(chalk28.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5049
- 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."));
5050
5973
  process.exitCode = 2;
5051
5974
  }
5052
5975
  }
5053
5976
  async function learn(lesson) {
5054
- console.log(chalk28.bold(`
5977
+ console.log(chalk34.bold(`
5055
5978
  ${ko.agent.learnTitle}
5056
5979
  `));
5057
5980
  if (!lesson || !lesson.trim()) {
5058
- console.log(chalk28.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5059
- 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)"'));
5060
5983
  process.exitCode = 1;
5061
5984
  return;
5062
5985
  }
5063
5986
  const goalId = activeGoalId();
5064
5987
  appendLearning(lesson, goalId);
5065
- console.log(chalk28.green(" \u2705 learnings.md append."));
5988
+ console.log(chalk34.green(" \u2705 learnings.md append."));
5066
5989
  console.log(
5067
- 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.")
5068
5991
  );
5069
5992
  }
5070
5993
  async function resume(opts = {}) {
5071
- console.log(chalk28.bold(`
5994
+ console.log(chalk34.bold(`
5072
5995
  ${ko.agent.resumeTitle}
5073
5996
  `));
5074
5997
  if (!isHardStopActive()) {
5075
- 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."));
5076
5999
  return;
5077
6000
  }
5078
6001
  const reason = readHardStopReason();
5079
6002
  if (reason) {
5080
- console.log(chalk28.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5081
- 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 ")}`));
5082
6005
  console.log("");
5083
6006
  }
5084
6007
  if (!opts.confirm) {
5085
6008
  console.log(
5086
- chalk28.red(
6009
+ chalk34.red(
5087
6010
  " \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
5088
6011
  )
5089
6012
  );
5090
- 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"));
5091
6014
  process.exitCode = 1;
5092
6015
  return;
5093
6016
  }
5094
6017
  const removed = clearHardStop();
5095
6018
  if (removed) {
5096
- 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."));
5097
6020
  } else {
5098
- 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."));
5099
6022
  }
5100
6023
  }
5101
6024
 
5102
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
+ }
5103
6060
  var program = new Command();
5104
6061
  var defaultHelp = new Help();
5105
6062
  var KO_ALIASES = {
@@ -5114,6 +6071,7 @@ var KO_ALIASES = {
5114
6071
  doctor: "\uD658\uACBD",
5115
6072
  save: "\uC800\uC7A5",
5116
6073
  undo: "\uB418\uB3CC\uB9AC\uAE30",
6074
+ restore: "\uBCF5\uC6D0",
5117
6075
  status: "\uC0C1\uD0DC",
5118
6076
  diff: "\uBCC0\uACBD",
5119
6077
  deploy: "\uBC30\uD3EC",
@@ -5165,7 +6123,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
5165
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);
5166
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);
5167
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);
5168
- 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
+ });
5169
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) => {
5170
6130
  await check(opts);
5171
6131
  });
@@ -5177,16 +6137,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
5177
6137
  cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
5178
6138
  await cloudPush();
5179
6139
  });
5180
- 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) => {
5181
- 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));
5182
6142
  });
5183
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);
5184
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);
5185
- program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
5186
- 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());
5187
6150
  });
5188
- program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
5189
- 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);
5190
6153
  });
5191
6154
  program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
5192
6155
  await status();
@@ -5198,17 +6161,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
5198
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 () => {
5199
6162
  await mcpInit();
5200
6163
  });
5201
- program.command("deploy").alias("\uBC30\uD3EC").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async () => {
5202
- 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());
5203
6166
  });
5204
- program.command("env").alias("\uD658\uACBD\uBCC0\uC218").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async () => {
5205
- 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());
5206
6169
  });
5207
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 () => {
5208
6171
  await envCheck();
5209
6172
  });
5210
- 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 () => {
5211
- 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());
5212
6175
  });
5213
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 () => {
5214
6177
  await design();
@@ -5237,14 +6200,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
5237
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) => {
5238
6201
  await audit(opts.fix);
5239
6202
  });
5240
- program.command("migrate [target]").alias("\uC804\uD658").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm)").action(async (target) => {
5241
- 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));
5242
6205
  });
5243
6206
  program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
5244
6207
  await update();
5245
6208
  });
5246
- program.command("context").alias("\uB9E5\uB77D").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async () => {
5247
- 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();
5248
6217
  });
5249
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 () => {
5250
6219
  await contextShow();
@@ -5290,7 +6259,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
5290
6259
  await learn(lesson);
5291
6260
  });
5292
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) => {
5293
- await resume(opts);
6262
+ await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
5294
6263
  });
5295
6264
  program.on("command:*", async (operands) => {
5296
6265
  const unknown = operands[0] ?? "";
@@ -5300,7 +6269,7 @@ program.on("command:*", async (operands) => {
5300
6269
  });
5301
6270
  program.action(async () => {
5302
6271
  console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
5303
- const { choice } = await inquirer12.prompt([{
6272
+ const { choice } = await inquirer14.prompt([{
5304
6273
  type: "list",
5305
6274
  name: "choice",
5306
6275
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -5331,24 +6300,40 @@ program.action(async () => {
5331
6300
  case "secure":
5332
6301
  return secure();
5333
6302
  case "sync":
5334
- return sync();
6303
+ return guardCli("sync", false, () => sync());
5335
6304
  case "doctor":
5336
6305
  return doctor();
5337
6306
  case "ship":
5338
6307
  return ship();
5339
6308
  case "save":
5340
- return save();
6309
+ return guardCli("save", false, () => save());
5341
6310
  case "undo":
5342
- return undo();
6311
+ return guardCliDefer("undo", false, () => undo());
5343
6312
  case "status":
5344
6313
  return status();
5345
6314
  case "diff":
5346
6315
  return diff();
5347
6316
  }
5348
6317
  });
5349
- var nlInput = detectNaturalLanguageInput(process.argv);
5350
- if (nlInput !== null) {
5351
- await runNaturalLanguageRoute(nlInput);
5352
- } else {
5353
- 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
+ }
5354
6336
  }
6337
+ export {
6338
+ program
6339
+ };