@byh3071/vhk 1.6.1 → 1.6.3

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
@@ -1,31 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CONTEXT_GIT_MARKER,
3
4
  MAX_SCAN_FILE_BYTES,
4
5
  MAX_SECRET_FINDINGS,
5
6
  NETWORK_EXEC_TIMEOUT_MS,
6
7
  __toESM,
7
8
  audit,
9
+ buildAdoptedRules,
10
+ checkContextDrift,
11
+ checkRuleDrift,
12
+ countLocalCommits,
8
13
  deploy,
14
+ detectExistingRuleFiles,
9
15
  env,
10
16
  envCheck,
11
17
  filterSevereFindings,
12
18
  filterTrackedPaths,
19
+ getExecErrorMessage,
20
+ getGitRoot,
13
21
  getVhkVersion,
22
+ gitOut,
23
+ gitRun,
24
+ hasGitRemote,
14
25
  ko,
26
+ listBackups,
27
+ localDate,
28
+ printContextResumeHint,
15
29
  printNextStep,
16
30
  printSecurityWarnings,
17
31
  publish,
18
32
  readJsonFile,
19
33
  require_ignore,
34
+ restoreBackup,
20
35
  safeExecFile,
21
36
  scanProjectForSecrets,
22
37
  startMcpServer,
38
+ sync,
23
39
  t
24
- } from "./chunk-O3A6SO7G.js";
40
+ } from "./chunk-53RJHPP6.js";
25
41
 
26
42
  // src/index.ts
27
43
  import { Command, Help } from "commander";
28
- import inquirer12 from "inquirer";
44
+ import { pathToFileURL } from "url";
45
+ import chalk34 from "chalk";
46
+ import inquirer13 from "inquirer";
29
47
 
30
48
  // src/lib/nlp-router.ts
31
49
  function normalize(input) {
@@ -43,6 +61,14 @@ function matchesKeywords(text, command) {
43
61
  return keywords.some((kw) => text.includes(kw.toLowerCase()));
44
62
  }
45
63
  var RULES = [
64
+ // restore 는 cloud-pull/undo 보다 먼저 평가 — "백업 복원/되돌려" 가 클라우드 복원이나
65
+ // 커밋 되돌리기로 새지 않도록. "백업" 한정이라 bare "복원해"(=cloud-pull)·"되돌려"(=undo)는 안 가로챔.
66
+ {
67
+ command: "restore",
68
+ explanation: "sync \uBC31\uC5C5 \uBCF5\uC6D0 (vhk restore)",
69
+ confidence: "high",
70
+ test: (t2) => /백업/.test(t2) && /(복원|복구|되돌려|되살려|롤백|restore)/.test(t2)
71
+ },
46
72
  // 영문 `vhk cloud push|pull [id]` 은 commander 가 직접 처리(가로채기 금지) — 한국어 표현만 매칭.
47
73
  {
48
74
  command: "cloud-pull",
@@ -69,6 +95,28 @@ var RULES = [
69
95
  confidence: "high",
70
96
  test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|새\s*프로젝트|^시작$|마법사|기획.*(끝|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2) && !/디자인|design|팔레트|palette|테마|theme|레퍼런스|reference|다크\s*모드|라이트\s*모드|색상\s*모드|브리핑|brief|컨텍스트|context|맥락|기억|memory|^초기화$|하네스.*만/.test(t2)
71
97
  },
98
+ // 도움말 — 초보자가 "뭐부터/도움말/명령어" 라고 물으면 읽기전용 quick actions 를 출력.
99
+ // (적대 리뷰 HIGH 수정: 이전엔 start 마법사로 라우팅돼 도움말이 scaffold 를 유발했음.
100
+ // 도움말은 절대 상태를 바꾸지 않는다.) 실제 서브커맨드는 cli-args R1 가드가 먼저 commander 로 보냄.
101
+ {
102
+ command: "help",
103
+ explanation: "\uC790\uC5F0\uC5B4\uB85C vhk \uC4F0\uB294 \uBC95 \u2014 quick actions \uCD9C\uB825(\uC0C1\uD0DC\uBCC0\uACBD \uC5C6\uC74C)",
104
+ confidence: "high",
105
+ test: (t2) => /도움말|사용법|help|^명령어$|뭐\s*(할\s*수\s*있|하면\s*(돼|되|좋)|해야)|처음\s*(뭐|어떻게|시작|할)|어떻게\s*시작|뭐부터/.test(t2)
106
+ },
107
+ // Safety Mode — 위험 작업 가드 강도 조회/변경.
108
+ {
109
+ command: "mode",
110
+ explanation: "Safety Mode \uC870\uD68C/\uBCC0\uACBD (vhk mode)",
111
+ confidence: "high",
112
+ test: (t2) => /안전\s*모드|safety\s*mode|모드\s*(바꿔|변경|설정|확인|보여|뭐)|위험\s*작업\s*(가드|모드)/.test(t2)
113
+ },
114
+ {
115
+ command: "verify",
116
+ explanation: "\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C (vhk verify)",
117
+ confidence: "high",
118
+ test: (t2) => /검증\s*묶음|사전\s*검증|저장\s*전\s*(검증|확인)|^verify$/.test(t2)
119
+ },
72
120
  {
73
121
  command: "init",
74
122
  explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
@@ -257,7 +305,8 @@ var RULES = [
257
305
  explanation: "\uBAA9\uD45C \uAC8C\uC774\uD2B8 \uAC80\uC99D (vhk goal check)",
258
306
  confidence: "high",
259
307
  args: ["check"],
260
- test: (t2) => /목표\s*(점검|검증|체크)/.test(t2)
308
+ // '스크립트' 포함 시는 sync 의도(게이트 스크립트 생성) check 가 가로채지 않게 제외.
309
+ test: (t2) => /목표\s*(점검|검증|체크)/.test(t2) && !/스크립트/.test(t2)
261
310
  },
262
311
  {
263
312
  command: "goal",
@@ -272,6 +321,13 @@ var RULES = [
272
321
  confidence: "high",
273
322
  args: ["list"],
274
323
  test: (t2) => /목표\s*(목록|리스트)/.test(t2)
324
+ },
325
+ {
326
+ command: "goal",
327
+ explanation: "\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uB3D9\uAE30\uD654 (vhk goal sync)",
328
+ confidence: "high",
329
+ args: ["sync"],
330
+ test: (t2) => /(게이트|목표).*(스크립트|동기화)|체크\s*스크립트\s*(생성|만들)/.test(t2)
275
331
  }
276
332
  ];
277
333
  function routeNaturalLanguage(input) {
@@ -294,6 +350,28 @@ function extractNotionUrl(input) {
294
350
  return m?.[0];
295
351
  }
296
352
 
353
+ // src/lib/command-registry.ts
354
+ var CONTAINER_SUBCOMMANDS = {
355
+ goal: ["list", "next", "check", "init", "done", "sync"],
356
+ ref: ["add", "list", "open"],
357
+ memory: ["add", "list", "remove"],
358
+ cloud: ["push", "pull"],
359
+ secure: ["scan"],
360
+ design: ["palette"],
361
+ env: ["check"],
362
+ mode: ["lite", "standard", "strict"]
363
+ };
364
+ var CONTAINER_ALIASES = {
365
+ \uBAA9\uD45C: "goal",
366
+ \uB808\uD37C\uB7F0\uC2A4: "ref",
367
+ \uAE30\uC5B5: "memory",
368
+ \uD074\uB77C\uC6B0\uB4DC: "cloud",
369
+ \uBCF4\uC548: "secure",
370
+ \uB514\uC790\uC778: "design",
371
+ \uD658\uACBD\uBCC0\uC218: "env",
372
+ \uBAA8\uB4DC: "mode"
373
+ };
374
+
297
375
  // src/lib/cli-args.ts
298
376
  var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
299
377
  "gate",
@@ -326,6 +404,8 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
326
404
  "\uC800\uC7A5",
327
405
  "undo",
328
406
  "\uB418\uB3CC\uB9AC\uAE30",
407
+ "restore",
408
+ "\uBCF5\uC6D0",
329
409
  "status",
330
410
  "\uC0C1\uD0DC",
331
411
  "\uD604\uD669",
@@ -378,11 +458,28 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
378
458
  "\uAD50\uD6C8",
379
459
  "resume",
380
460
  "\uC7AC\uAC1C",
461
+ "mode",
462
+ "\uBAA8\uB4DC",
463
+ "verify",
464
+ "\uC0AC\uC804\uC810\uAC80",
381
465
  "help"
382
466
  ]);
383
467
  function isOptionToken(token) {
384
468
  return token.startsWith("-");
385
469
  }
470
+ var COMMAND_SUBCOMMANDS = (() => {
471
+ const map = { ...CONTAINER_SUBCOMMANDS };
472
+ for (const [alias, canonical] of Object.entries(CONTAINER_ALIASES)) {
473
+ const subs = CONTAINER_SUBCOMMANDS[canonical];
474
+ if (subs) map[alias] = subs;
475
+ }
476
+ return map;
477
+ })();
478
+ function isRealSubcommandPath(first, second) {
479
+ if (second === void 0) return false;
480
+ const subs = COMMAND_SUBCOMMANDS[first];
481
+ return subs !== void 0 && subs.includes(second);
482
+ }
386
483
  function detectNaturalLanguageInput(argv) {
387
484
  const rest = argv.slice(2);
388
485
  if (rest.length === 0) return null;
@@ -394,6 +491,7 @@ function detectNaturalLanguageInput(argv) {
394
491
  const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
395
492
  if (firstIsKnown && rest.length === 1) return null;
396
493
  if (firstIsKnown && rest.length > 1) {
494
+ if (isRealSubcommandPath(first, rest[1])) return null;
397
495
  if (routeNaturalLanguage(input)) return input;
398
496
  return null;
399
497
  }
@@ -401,8 +499,8 @@ function detectNaturalLanguageInput(argv) {
401
499
  }
402
500
 
403
501
  // src/lib/nlp-run.ts
404
- import chalk27 from "chalk";
405
- import inquirer11 from "inquirer";
502
+ import chalk32 from "chalk";
503
+ import inquirer12 from "inquirer";
406
504
 
407
505
  // src/commands/gate.ts
408
506
  import inquirer from "inquirer";
@@ -431,7 +529,7 @@ async function gate() {
431
529
  console.log(chalk.bold(`
432
530
  ${ko.gate.title}
433
531
  `));
434
- const { mode } = await inquirer.prompt([{
532
+ const { mode: mode2 } = await inquirer.prompt([{
435
533
  type: "list",
436
534
  name: "mode",
437
535
  message: ko.gate.modePrompt,
@@ -441,7 +539,7 @@ ${ko.gate.title}
441
539
  { name: ko.gate.modeSkipLabel, value: "skip" }
442
540
  ]
443
541
  }]);
444
- if (mode === "skip") {
542
+ if (mode2 === "skip") {
445
543
  const { source } = await inquirer.prompt([{
446
544
  type: "input",
447
545
  name: "source",
@@ -452,9 +550,9 @@ ${ko.gate.skipGo}`));
452
550
  console.log(chalk.dim(ko.gate.skipSourceLabel(source)));
453
551
  return;
454
552
  }
455
- const questions = mode === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
553
+ const questions = mode2 === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
456
554
  const total = questions.length;
457
- const header = mode === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
555
+ const header = mode2 === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
458
556
  console.log(chalk.dim(`
459
557
  ${header} ${ko.gate.modeCountSuffix(total)}
460
558
  `));
@@ -542,7 +640,7 @@ import path2 from "path";
542
640
 
543
641
  // src/templates/claude-md.ts
544
642
  function CLAUDE_MD_TEMPLATE(name, _stack) {
545
- const d = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
643
+ const d = localDate();
546
644
  const slug = name.toLowerCase().replace(/\s+/g, "-");
547
645
  return [
548
646
  "---",
@@ -554,12 +652,12 @@ function CLAUDE_MD_TEMPLATE(name, _stack) {
554
652
  "# \uAE30\uB85D \uADDC\uCE59 (" + name + ")",
555
653
  "",
556
654
  "> \uC774 \uD30C\uC77C\uC740 \uAE30\uB85D/\uC6B4\uC601 \uC804\uC6A9. \uCF54\uB529/\uB514\uC790\uC778 \u2192 .cursorrules \uCC38\uC870.",
557
- "> See also: AGENTS.md",
655
+ "> See also: AGENTS.md (`vhk sync` \uB85C \uC0DD\uC131 \u2014 Codex/OpenAI \uACC4\uC5F4 \uD638\uD658).",
558
656
  "",
559
657
  "## \uD604\uC7AC \uC0C1\uD0DC",
560
658
  "- **Phase:** Phase 1 \u2014 MVP",
561
659
  "- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C",
562
- "- **\uB2E4\uC74C \uC561\uC158:** __FILL__",
660
+ "- **\uB2E4\uC74C \uC561\uC158:** **FILL**",
563
661
  "- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** " + d,
564
662
  "",
565
663
  "## ADR",
@@ -613,8 +711,39 @@ function CURSORRULES_TEMPLATE(name, desc, stack) {
613
711
  ].join("\n");
614
712
  }
615
713
 
714
+ // src/templates/rules-md.ts
715
+ function RULES_MD_TEMPLATE(name, description, stack) {
716
+ const stackList = stack.split(" + ").map((s) => "- " + s).join("\n");
717
+ return [
718
+ "# " + name + " \u2014 Rules",
719
+ "",
720
+ "> \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.",
721
+ "> `vhk sync` \uAC00 Cursor\xB7Claude\xB7Windsurf\xB7Copilot\xB7Antigravity \uADDC\uCE59\uC73C\uB85C \uC804\uD30C\uD569\uB2C8\uB2E4.",
722
+ "",
723
+ "## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131",
724
+ "- \uD55C \uC904 \uC124\uBA85: " + description,
725
+ "- \uC2A4\uD0DD: " + stack,
726
+ "",
727
+ "## \uAE30\uC220 \uC2A4\uD0DD",
728
+ stackList,
729
+ "",
730
+ "## \uCF54\uB529 \uADDC\uCE59",
731
+ "- TypeScript strict (any \uAE08\uC9C0)",
732
+ "- try-catch \uD544\uC218, \uBE48 catch \uAE08\uC9C0",
733
+ "- console.log \uD504\uB85C\uB355\uC158 \uC81C\uAC70",
734
+ "- \uD30C\uC77C\uBA85\uC740 kebab-case",
735
+ "",
736
+ "## \uCEE4\uBC0B \uCEE8\uBCA4\uC158",
737
+ "- feat: / fix: / refactor: / docs: / chore:",
738
+ "",
739
+ "## \uAE30\uB85D \uADDC\uCE59",
740
+ "- \uC138\uC158 \uC885\uB8CC \uC2DC docs/log/YYYY-MM-DD-{\uC791\uC5C5\uBA85}.md \uC0DD\uC131",
741
+ "- \uAE30\uC220 \uC120\uD0DD \uC2DC docs/adr/ADR-{\uBC88\uD638}-{\uC81C\uBAA9}.md \uC0DD\uC131"
742
+ ].join("\n");
743
+ }
744
+
616
745
  // src/templates/prd.ts
617
- var FILL = "__FILL__";
746
+ var FILL = "**FILL**";
618
747
  function fill(value, fallback = FILL) {
619
748
  return value?.trim() || fallback;
620
749
  }
@@ -683,12 +812,12 @@ function ARCHITECTURE_TEMPLATE(name, stack) {
683
812
  "## \uB370\uC774\uD130 \uBAA8\uB378",
684
813
  "| \uD14C\uC774\uBE14 | \uD575\uC2EC \uCEEC\uB7FC | \uC124\uBA85 |",
685
814
  "|--------|----------|------|",
686
- "| __FILL__ | | |",
815
+ "| **FILL** | | |",
687
816
  "",
688
817
  "## \uC678\uBD80 \uC11C\uBE44\uC2A4",
689
818
  "| \uC11C\uBE44\uC2A4 | \uC6A9\uB3C4 |",
690
819
  "|--------|------|",
691
- "| __FILL__ | |"
820
+ "| **FILL** | |"
692
821
  ].join("\n");
693
822
  }
694
823
 
@@ -705,21 +834,22 @@ function ADR_TEMPLATE() {
705
834
  "# ADR-000: \uC81C\uBAA9",
706
835
  "",
707
836
  "## \uB9E5\uB77D (Context)",
708
- "__FILL__",
837
+ "**FILL**",
709
838
  "",
710
839
  "## \uACB0\uC815 (Decision)",
711
- "__FILL__",
840
+ "**FILL**",
712
841
  "",
713
842
  "## \uB300\uC548 (Alternatives)",
714
- "__FILL__",
843
+ "**FILL**",
715
844
  "",
716
845
  "## \uACB0\uACFC (Consequences)",
717
- "__FILL__"
846
+ "**FILL**"
718
847
  ].join("\n");
719
848
  }
720
849
 
721
850
  // src/templates/commands-md.ts
722
- function COMMANDS_MD_TEMPLATE() {
851
+ function COMMANDS_MD_TEMPLATE(opts = {}) {
852
+ 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
853
  return [
724
854
  "# \u{1F4CB} \uD55C\uAD6D\uC5B4 \uBA85\uB839\uC5B4 \uAC00\uC774\uB4DC",
725
855
  "",
@@ -742,7 +872,7 @@ function COMMANDS_MD_TEMPLATE() {
742
872
  '| \uC624\uB298 \uC815\uB9AC | `vhk \uC815\uB9AC` | "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574" |',
743
873
  '| \uADDC\uCE59 \uC810\uAC80 | `vhk \uC810\uAC80` | "\uADDC\uCE59 \uC810\uAC80\uD574" |',
744
874
  '| \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" |',
875
+ buildTestRow,
746
876
  '| \uBC30\uD3EC | `vhk \uBC30\uD3EC` | "\uBC30\uD3EC\uD574" |',
747
877
  "",
748
878
  "## \uD658\uACBD \uC810\uAC80",
@@ -762,7 +892,8 @@ function VHK_README_TEMPLATE() {
762
892
  "# `.vhk/` \u2014 VHK runtime state",
763
893
  "",
764
894
  "\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.",
895
+ // VHK-006: 생성 프로젝트엔 docs/spec.md 없음 → vhk 저장소의 규격 문서를 외부 링크로 참조.
896
+ "\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
897
  "",
767
898
  "## \uD2B8\uB798\uD0B9 \uC815\uCC45",
768
899
  "",
@@ -782,10 +913,14 @@ function VHK_README_TEMPLATE() {
782
913
  }
783
914
  function VHK_GITIGNORE_TEMPLATE() {
784
915
  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)",
916
+ "# 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
917
  "memory.json",
787
918
  "refs.json",
788
919
  "HARD_STOP",
920
+ "# secret gist \uD3EC\uC778\uD130 (gistId). \uACF5\uAC1C repo \uC5D0 \uCEE4\uBC0B\uB418\uBA74 \uBC31\uC5C5 gist \uAC00 \uB178\uCD9C\uB428 (VHK-022).",
921
+ "cloud.json",
922
+ "# 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.",
923
+ "backups/",
789
924
  ""
790
925
  ].join("\n");
791
926
  }
@@ -988,6 +1123,40 @@ async function fetchPrdFromNotion(urlOrId) {
988
1123
  };
989
1124
  }
990
1125
 
1126
+ // src/lib/stack-detect.ts
1127
+ import { join } from "path";
1128
+ function detectStackFromDeps(deps) {
1129
+ const stack = [];
1130
+ if (deps.next) stack.push("Next.js");
1131
+ else if (deps.nuxt) stack.push("Nuxt");
1132
+ else if (deps.vite || deps["@vitejs/plugin-react"]) stack.push("Vite");
1133
+ if (deps.react) stack.push("React");
1134
+ else if (deps.vue) stack.push("Vue");
1135
+ else if (deps.svelte) stack.push("Svelte");
1136
+ if (deps.typescript) stack.push("TypeScript");
1137
+ if (deps.tailwindcss || deps["@tailwindcss/vite"]) stack.push("Tailwind CSS");
1138
+ if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"]) stack.push("Supabase");
1139
+ if (deps["@prisma/client"] || deps.prisma) stack.push("Prisma");
1140
+ if (deps["drizzle-orm"]) stack.push("Drizzle");
1141
+ if (deps["@anthropic-ai/sdk"]) stack.push("Anthropic SDK");
1142
+ if (deps.openai) stack.push("OpenAI SDK");
1143
+ if (deps.zod) stack.push("zod");
1144
+ if (deps["@tanstack/react-query"]) stack.push("TanStack Query");
1145
+ return stack;
1146
+ }
1147
+ function detectProjectStack(cwd = ".") {
1148
+ let pkg;
1149
+ try {
1150
+ pkg = readJsonFile(join(cwd, "package.json"));
1151
+ } catch {
1152
+ return null;
1153
+ }
1154
+ const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
1155
+ if (Object.keys(all).length === 0) return null;
1156
+ const stack = detectStackFromDeps(all);
1157
+ return stack.length ? stack : null;
1158
+ }
1159
+
991
1160
  // src/commands/init.ts
992
1161
  var PROJECT_TYPES = [
993
1162
  { name: "\u{1F310} \uC6F9 \uC571 (Next.js + Supabase + Vercel)", value: "webapp" },
@@ -1011,23 +1180,30 @@ function resolveType(type) {
1011
1180
  }
1012
1181
  return type;
1013
1182
  }
1183
+ function isNonInteractive(options) {
1184
+ return Boolean(options.yes) || !process.stdin.isTTY || !process.stdout.isTTY;
1185
+ }
1186
+ var DEFAULT_TYPE = PROJECT_TYPES[0].value;
1014
1187
  async function collectAnswers(options, defaults = {}) {
1188
+ const noninteractive = isNonInteractive(options);
1015
1189
  const prompts = [];
1016
- if (!options.name && !defaults.name) {
1017
- prompts.push({ type: "input", name: "name", message: ko.init.projectName });
1018
- }
1019
- if (!options.description && !defaults.description) {
1020
- prompts.push({ type: "input", name: "description", message: ko.init.description });
1021
- }
1022
- if (!options.type && !defaults.type) {
1023
- prompts.push({ type: "list", name: "type", message: ko.init.projectType, choices: PROJECT_TYPES });
1190
+ if (!noninteractive) {
1191
+ if (!options.name && !defaults.name) {
1192
+ prompts.push({ type: "input", name: "name", message: ko.init.projectName });
1193
+ }
1194
+ if (!options.description && !defaults.description) {
1195
+ prompts.push({ type: "input", name: "description", message: ko.init.description });
1196
+ }
1197
+ if (!options.type && !defaults.type) {
1198
+ prompts.push({ type: "list", name: "type", message: ko.init.projectType, choices: PROJECT_TYPES });
1199
+ }
1024
1200
  }
1025
1201
  const prompted = prompts.length ? await inquirer2.prompt(prompts) : {};
1026
- return {
1027
- name: options.name ?? defaults.name ?? prompted.name,
1028
- description: options.description ?? defaults.description ?? prompted.description,
1029
- type: resolveType(options.type ?? defaults.type ?? prompted.type) ?? prompted.type
1030
- };
1202
+ const fallbackName = path2.basename(process.cwd()) || "my-project";
1203
+ const name = options.name || defaults.name || prompted.name || fallbackName;
1204
+ const description = options.description || defaults.description || prompted.description || `${name} \u2014 vhk \uD504\uB85C\uC81D\uD2B8`;
1205
+ const type = resolveType(options.type || defaults.type || prompted.type) ?? prompted.type ?? DEFAULT_TYPE;
1206
+ return { name, description, type };
1031
1207
  }
1032
1208
  async function init(options = {}) {
1033
1209
  const skipGate = Boolean(options.skipGate || options.fromNotion);
@@ -1060,11 +1236,13 @@ ${ko.init.title}
1060
1236
  log.error("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984, \uC124\uBA85, \uC720\uD615\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
1061
1237
  process.exit(1);
1062
1238
  }
1063
- const stack = STACK_PRESETS[answers.type];
1239
+ const detected = detectProjectStack(process.cwd());
1240
+ const stack = detected ?? STACK_PRESETS[answers.type];
1241
+ if (detected) console.log(chalk3.dim(" \u{1F50E} package.json \uC758\uC874\uC131\uC5D0\uC11C \uC2E4\uC81C \uC2A4\uD0DD \uAC10\uC9C0"));
1064
1242
  console.log(chalk3.dim(`
1065
1243
  ${ko.init.recommendedStack} ${stack.join(" + ")}
1066
1244
  `));
1067
- if (!options.yes) {
1245
+ if (!isNonInteractive(options)) {
1068
1246
  const { confirmStack } = await inquirer2.prompt([{
1069
1247
  type: "confirm",
1070
1248
  name: "confirmStack",
@@ -1077,17 +1255,37 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1077
1255
  }
1078
1256
  }
1079
1257
  const cwd = process.cwd();
1258
+ let adoptedRules = null;
1259
+ if (!isNonInteractive(options) && !options.fromNotion) {
1260
+ const existingRules = detectExistingRuleFiles(cwd);
1261
+ if (existingRules.length > 0) {
1262
+ const { adopt } = await inquirer2.prompt([{
1263
+ type: "confirm",
1264
+ name: "adopt",
1265
+ message: ko.init.adoptPrompt(
1266
+ existingRules.length,
1267
+ existingRules.map((f) => f.path).join(", ")
1268
+ ),
1269
+ default: true
1270
+ }]);
1271
+ if (adopt) {
1272
+ adoptedRules = buildAdoptedRules(existingRules, answers.name);
1273
+ console.log(chalk3.dim(` ${ko.init.adoptPreview(existingRules.length)}`));
1274
+ }
1275
+ }
1276
+ }
1080
1277
  const files = generateFiles(answers.name, answers.description, stack, prdContent, answers.type);
1278
+ if (adoptedRules) files["RULES.md"] = adoptedRules;
1081
1279
  log.step(ko.init.filesGenerating);
1082
1280
  for (const [filePath, content] of Object.entries(files)) {
1083
1281
  const fullPath = path2.join(cwd, filePath);
1084
1282
  if (fileExists(fullPath)) {
1085
- const { overwrite } = await inquirer2.prompt([{
1283
+ const overwrite = isNonInteractive(options) ? false : (await inquirer2.prompt([{
1086
1284
  type: "confirm",
1087
1285
  name: "overwrite",
1088
1286
  message: ko.init.overwrite(filePath),
1089
1287
  default: false
1090
- }]);
1288
+ }])).overwrite;
1091
1289
  if (!overwrite) {
1092
1290
  log.warn(ko.init.skipped(filePath));
1093
1291
  continue;
@@ -1096,7 +1294,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
1096
1294
  writeFile(fullPath, content);
1097
1295
  log.success(filePath);
1098
1296
  }
1099
- await writeInitExtras(cwd);
1297
+ await writeInitExtras(cwd, isNonInteractive(options));
1100
1298
  console.log(chalk3.bold.green(`
1101
1299
  ${ko.init.done}`));
1102
1300
  console.log(chalk3.dim(`
@@ -1131,6 +1329,8 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
1131
1329
  return {
1132
1330
  "CLAUDE.md": CLAUDE_MD_TEMPLATE(name, stackStr),
1133
1331
  ".cursorrules": CURSORRULES_TEMPLATE(name, description, stackStr),
1332
+ // RULES.md — 규칙 SoT. init 이 항상 생성해 sync 와 흐름을 연결한다.
1333
+ "RULES.md": RULES_MD_TEMPLATE(name, description, stackStr),
1134
1334
  "docs/PRD.md": PRD_TEMPLATE(name, description, prd),
1135
1335
  "docs/ARCHITECTURE.md": ARCHITECTURE_TEMPLATE(name, stackStr),
1136
1336
  "docs/adr/ADR-000-template.md": ADR_TEMPLATE(),
@@ -1138,8 +1338,9 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
1138
1338
  "docs/troubleshooting/.gitkeep": "",
1139
1339
  "docs/til.md": `# TIL (Today I Learned)
1140
1340
 
1141
- - [${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
1341
+ - [${localDate()}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
1142
1342
  `,
1343
+ // VHK-019
1143
1344
  "BACKLOG.md": `# BACKLOG
1144
1345
 
1145
1346
  > v1 OUT \uAE30\uB2A5\uC740 \uC5EC\uAE30\uC5D0 \uAE30\uB85D. \uBC94\uC704 \uC218\uBE44 \uD544\uC218.
@@ -1197,19 +1398,30 @@ function enhancePackageScripts(projectDir) {
1197
1398
  fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1198
1399
  return true;
1199
1400
  }
1200
- async function writeInitExtras(projectDir) {
1401
+ function projectHasTestScript(projectDir) {
1402
+ const pkgPath = path2.join(projectDir, "package.json");
1403
+ if (!fs2.existsSync(pkgPath)) return false;
1404
+ try {
1405
+ const pkg = readJsonFile(pkgPath);
1406
+ return Boolean(pkg.scripts?.test?.trim());
1407
+ } catch {
1408
+ return false;
1409
+ }
1410
+ }
1411
+ async function writeInitExtras(projectDir, noninteractive = false) {
1201
1412
  const commandsPath = path2.join(projectDir, "COMMANDS.md");
1413
+ const hasTest = projectHasTestScript(projectDir);
1202
1414
  if (fileExists(commandsPath)) {
1203
- const { overwrite } = await inquirer2.prompt([{
1415
+ const overwrite = noninteractive ? false : (await inquirer2.prompt([{
1204
1416
  type: "confirm",
1205
1417
  name: "overwrite",
1206
1418
  message: ko.init.overwrite("COMMANDS.md"),
1207
1419
  default: false
1208
- }]);
1420
+ }])).overwrite;
1209
1421
  if (!overwrite) {
1210
1422
  log.warn(ko.init.skipped("COMMANDS.md"));
1211
1423
  } else {
1212
- writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
1424
+ writeFile(commandsPath, COMMANDS_MD_TEMPLATE({ hasTest }));
1213
1425
  log.success(ko.init.commandsMdDone);
1214
1426
  }
1215
1427
  } else {
@@ -1229,7 +1441,7 @@ async function writeInitExtras(projectDir) {
1229
1441
 
1230
1442
  // src/commands/recap.ts
1231
1443
  import inquirer3 from "inquirer";
1232
- import chalk4 from "chalk";
1444
+ import chalk6 from "chalk";
1233
1445
  import fs4 from "fs";
1234
1446
  import path5 from "path";
1235
1447
 
@@ -1273,10 +1485,13 @@ function buildSessionDiffFromSummary(diffSummary) {
1273
1485
  files
1274
1486
  };
1275
1487
  }
1488
+ var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1276
1489
  async function getSessionDiff(since) {
1277
- const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1490
+ const sinceDate = since || localDate();
1278
1491
  try {
1279
- const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
1492
+ const boundary = (await git.raw(["rev-list", "-1", `--before=${sinceDate}`, "HEAD"])).trim();
1493
+ const base = boundary || EMPTY_TREE_SHA;
1494
+ const diffSummary = await git.diffSummary([`${base}..HEAD`]);
1280
1495
  const normalized = diffSummary.files.map((f) => ({
1281
1496
  file: f.file,
1282
1497
  insertions: "insertions" in f ? f.insertions : 0,
@@ -1379,7 +1594,7 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
1379
1594
  const adrDir = path4.join(cwd, "docs", "adr");
1380
1595
  if (!fs3.existsSync(adrDir)) fs3.mkdirSync(adrDir, { recursive: true });
1381
1596
  const num = nextAdrNumber(adrDir);
1382
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1597
+ const today = localDate();
1383
1598
  const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
1384
1599
  const filePath = path4.join(adrDir, fileName);
1385
1600
  const content = [
@@ -1411,50 +1626,191 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
1411
1626
  return filePath;
1412
1627
  }
1413
1628
 
1629
+ // src/lib/interactive.ts
1630
+ import chalk4 from "chalk";
1631
+ function ensureInteractive(hint = "") {
1632
+ if (process.stdin.isTTY) return true;
1633
+ 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."));
1634
+ if (hint) console.error(chalk4.dim(` ${hint}`));
1635
+ process.exitCode = 1;
1636
+ return false;
1637
+ }
1638
+ function isPromptAbortError(err) {
1639
+ const msg = err instanceof Error ? err.message : String(err);
1640
+ return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
1641
+ }
1642
+
1643
+ // src/lib/hard-stop-guard.ts
1644
+ import chalk5 from "chalk";
1645
+
1646
+ // src/lib/state-files.ts
1647
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rmSync } from "fs";
1648
+ import { join as join2 } from "path";
1649
+ var STATE_DIR = "docs/state";
1650
+ var BLOCKERS_PATH = join2(STATE_DIR, "blockers.md");
1651
+ var LEARNINGS_PATH = join2(STATE_DIR, "learnings.md");
1652
+ var VHK_DIR = ".vhk";
1653
+ var HARD_STOP_PATH = join2(VHK_DIR, "HARD_STOP");
1654
+ var HARD_STOP_BLOCKER_THRESHOLD = 3;
1655
+ function ensureStateDir() {
1656
+ mkdirSync(STATE_DIR, { recursive: true });
1657
+ }
1658
+ function ensureVhkDir() {
1659
+ mkdirSync(VHK_DIR, { recursive: true });
1660
+ }
1661
+ function isoDate() {
1662
+ return localDate();
1663
+ }
1664
+ var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
1665
+ function countActiveBlockers(content) {
1666
+ let count = 0;
1667
+ for (const line of content.split(/\r?\n/)) {
1668
+ if (ACTIVE_BLOCKER_RE.test(line)) count++;
1669
+ }
1670
+ return count;
1671
+ }
1672
+ function appendBlocker(description, goalId) {
1673
+ ensureStateDir();
1674
+ const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
1675
+ const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
1676
+ if (!existsSync(BLOCKERS_PATH)) {
1677
+ writeFileSync(
1678
+ BLOCKERS_PATH,
1679
+ `# Blockers
1680
+
1681
+ _Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
1682
+
1683
+ ${line}
1684
+ `,
1685
+ "utf-8"
1686
+ );
1687
+ } else {
1688
+ appendFileSync(BLOCKERS_PATH, `${line}
1689
+ `, "utf-8");
1690
+ }
1691
+ const current = readFileSync(BLOCKERS_PATH, "utf-8");
1692
+ const count = countActiveBlockers(current);
1693
+ let hardStopTripped = false;
1694
+ if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync(HARD_STOP_PATH)) {
1695
+ writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
1696
+ hardStopTripped = true;
1697
+ }
1698
+ return { count, hardStopTripped };
1699
+ }
1700
+ function appendLearning(lesson, goalId) {
1701
+ ensureStateDir();
1702
+ const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
1703
+ const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
1704
+ if (!existsSync(LEARNINGS_PATH)) {
1705
+ writeFileSync(
1706
+ LEARNINGS_PATH,
1707
+ `# Learnings
1708
+
1709
+ _Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
1710
+
1711
+ ${line}
1712
+ `,
1713
+ "utf-8"
1714
+ );
1715
+ } else {
1716
+ appendFileSync(LEARNINGS_PATH, `${line}
1717
+ `, "utf-8");
1718
+ }
1719
+ }
1720
+ function getRecentLearnings(limit = 3) {
1721
+ if (!existsSync(LEARNINGS_PATH)) return [];
1722
+ const lines = readFileSync(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
1723
+ const entries = lines.filter((l) => l.startsWith("- ["));
1724
+ return entries.slice(-limit);
1725
+ }
1726
+ function getActiveBlockers(limit = 3) {
1727
+ if (!existsSync(BLOCKERS_PATH)) return [];
1728
+ const lines = readFileSync(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
1729
+ const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
1730
+ return entries.slice(-limit);
1731
+ }
1732
+ function writeHardStop(reason) {
1733
+ ensureVhkDir();
1734
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1735
+ writeFileSync(HARD_STOP_PATH, `${ts}
1736
+ ${reason}
1737
+ `, "utf-8");
1738
+ }
1739
+ function isHardStopActive() {
1740
+ return existsSync(HARD_STOP_PATH);
1741
+ }
1742
+ function readHardStopReason() {
1743
+ if (!existsSync(HARD_STOP_PATH)) return null;
1744
+ try {
1745
+ return readFileSync(HARD_STOP_PATH, "utf-8").trim();
1746
+ } catch {
1747
+ return null;
1748
+ }
1749
+ }
1750
+ function clearHardStop() {
1751
+ if (!existsSync(HARD_STOP_PATH)) return false;
1752
+ rmSync(HARD_STOP_PATH, { force: true });
1753
+ return true;
1754
+ }
1755
+
1756
+ // src/lib/hard-stop-guard.ts
1757
+ function ensureNotHardStopped(action) {
1758
+ if (!isHardStopActive()) return true;
1759
+ console.error(chalk5.red.bold(`
1760
+ \u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
1761
+ const reason = readHardStopReason();
1762
+ if (reason) console.error(chalk5.dim(` \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}`));
1763
+ console.error(chalk5.dim(" \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)"));
1764
+ process.exitCode = 1;
1765
+ return false;
1766
+ }
1767
+
1414
1768
  // src/commands/recap.ts
1415
1769
  async function recap(options = {}) {
1416
- console.log(chalk4.bold(`
1770
+ if (!ensureNotHardStopped("recap")) return;
1771
+ console.log(chalk6.bold(`
1417
1772
  ${ko.recap.title}
1418
1773
  `));
1419
1774
  if (!await isGitRepo()) {
1420
- console.log(chalk4.red(ko.recap.noRepo));
1775
+ console.log(chalk6.red(ko.recap.noRepo));
1421
1776
  return;
1422
1777
  }
1423
1778
  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."));
1779
+ console.log(chalk6.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
1780
+ 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
1781
  return;
1427
1782
  }
1428
1783
  printSecurityWarnings();
1429
- console.log(chalk4.dim(`${ko.recap.analyzing}
1784
+ console.log(chalk6.dim(`${ko.recap.analyzing}
1430
1785
  `));
1431
- const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1786
+ const since = options.since || localDate();
1432
1787
  const diff2 = await getSessionDiff(since);
1433
1788
  const commits = await getRecentCommits(10, since);
1434
1789
  if (diff2.filesChanged === 0 && commits.length === 0) {
1435
- console.log(chalk4.yellow(ko.recap.noChanges));
1790
+ console.log(chalk6.yellow(ko.recap.noChanges));
1436
1791
  return;
1437
1792
  }
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)}`);
1793
+ console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1794
+ console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1795
+ console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
1441
1796
  if (diff2.files.length > 0) {
1442
- console.log(chalk4.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1797
+ console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1443
1798
  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");
1799
+ const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
1445
1800
  console.log(` ${icon} ${f.file}`);
1446
1801
  });
1447
1802
  if (diff2.files.length > 15) {
1448
- console.log(chalk4.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1803
+ console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1449
1804
  }
1450
1805
  }
1451
1806
  if (commits.length > 0) {
1452
- console.log(chalk4.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1807
+ console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
1453
1808
  commits.slice(0, 5).forEach((c) => {
1454
- console.log(chalk4.dim(` \u2022 ${c.message}`));
1809
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1455
1810
  });
1456
1811
  }
1457
1812
  console.log("");
1813
+ if (!ensureInteractive("\uD68C\uACE0 \uC785\uB825\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
1458
1814
  const answers = await inquirer3.prompt([
1459
1815
  {
1460
1816
  type: "input",
@@ -1479,7 +1835,7 @@ ${ko.recap.title}
1479
1835
  default: "\uC5C6\uC74C"
1480
1836
  }
1481
1837
  ]);
1482
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1838
+ const today = localDate();
1483
1839
  const logDir = path5.join(process.cwd(), "docs", "log");
1484
1840
  if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true });
1485
1841
  const existing = fs4.readdirSync(logDir).filter((f) => f.startsWith(today));
@@ -1519,11 +1875,11 @@ ${ko.recap.title}
1519
1875
  fs4.writeFileSync(filePath, content, "utf-8");
1520
1876
  const adrCandidates = detectAdrCandidates(diff2);
1521
1877
  if (adrCandidates.length > 0) {
1522
- console.log(chalk4.cyan.bold(`
1878
+ console.log(chalk6.cyan.bold(`
1523
1879
  ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1524
1880
  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}`)));
1881
+ console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
1882
+ candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
1527
1883
  }
1528
1884
  const { createAdr } = await inquirer3.prompt([{
1529
1885
  type: "confirm",
@@ -1553,17 +1909,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
1553
1909
  adrAnswers.decision,
1554
1910
  adrAnswers.consequences
1555
1911
  );
1556
- console.log(chalk4.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
1912
+ console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
1557
1913
  }
1558
1914
  }
1559
1915
  }
1560
1916
  const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
1561
1917
  const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
1562
1918
  if (troubleCommits.length > 0) {
1563
- console.log(chalk4.yellow.bold(`
1919
+ console.log(chalk6.yellow.bold(`
1564
1920
  ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1565
1921
  troubleCommits.forEach((c) => {
1566
- console.log(chalk4.dim(` \u2022 ${c.message}`));
1922
+ console.log(chalk6.dim(` \u2022 ${c.message}`));
1567
1923
  });
1568
1924
  const { createTroubleshoot } = await inquirer3.prompt([{
1569
1925
  type: "confirm",
@@ -1614,12 +1970,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1614
1970
  `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1615
1971
  ].join("\n");
1616
1972
  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)}`));
1973
+ console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
1618
1974
  }
1619
1975
  }
1620
- console.log(chalk4.green.bold(`
1976
+ console.log(chalk6.green.bold(`
1621
1977
  ${ko.recap.done}`));
1622
- console.log(chalk4.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
1978
+ console.log(chalk6.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
1623
1979
  const claudeMdPath = path5.join(process.cwd(), "CLAUDE.md");
1624
1980
  if (fs4.existsSync(claudeMdPath)) {
1625
1981
  const { updateClaude } = await inquirer3.prompt([{
@@ -1639,7 +1995,7 @@ ${ko.recap.done}`));
1639
1995
  `- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
1640
1996
  );
1641
1997
  fs4.writeFileSync(claudeMdPath, claudeContent, "utf-8");
1642
- console.log(chalk4.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
1998
+ console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
1643
1999
  }
1644
2000
  }
1645
2001
  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"';
@@ -1650,237 +2006,73 @@ ${ko.recap.done}`));
1650
2006
  });
1651
2007
  }
1652
2008
 
1653
- // src/commands/sync.ts
1654
- import chalk5 from "chalk";
1655
- import fs5 from "fs";
1656
- 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
- }
1674
- }
1675
- if (currentTitle) {
1676
- sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
1677
- }
1678
- return sections;
1679
- }
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}`,
1686
- "",
1687
- "> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
1688
- "> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
1689
- "",
1690
- "## \uD544\uC218 \uCC38\uC870",
1691
- "- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md \xB7 RULES.md",
1692
- ""
1693
- ];
1694
- for (const section of codingSections) {
1695
- lines.push(`## ${section.title}`);
1696
- lines.push(section.content);
1697
- lines.push("");
1698
- }
1699
- return lines.join("\n");
1700
- }
1701
- function toCursorrules(sections, projectName) {
1702
- return buildCodingDoc("Cursor Rules", sections, projectName);
1703
- }
1704
- function toWindsurfrules(sections, projectName) {
1705
- return buildCodingDoc("Windsurf Rules", sections, projectName);
1706
- }
1707
- function toCopilotInstructions(sections, projectName) {
1708
- return buildCodingDoc("GitHub Copilot Instructions", sections, projectName);
1709
- }
1710
- var ANTIGRAVITY_CHAR_LIMIT = 12e3;
1711
- var ANTIGRAVITY_TRUNCATE_MARKER = "\n\n<!-- \u26A0\uFE0F Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428 \u2014 \uC804\uCCB4 \uADDC\uCE59\uC740 RULES.md \uCC38\uC870 -->\n";
1712
- function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
1713
- if (Buffer.byteLength(content, "utf8") <= limit) return content;
1714
- const SAFETY = 200;
1715
- const budget = limit - Buffer.byteLength(ANTIGRAVITY_TRUNCATE_MARKER, "utf8") - SAFETY;
1716
- let lo = 0;
1717
- let hi = content.length;
1718
- while (lo < hi) {
1719
- const mid = lo + hi + 1 >> 1;
1720
- if (Buffer.byteLength(content.slice(0, mid), "utf8") <= budget) lo = mid;
1721
- else hi = mid - 1;
1722
- }
1723
- const charCut = lo;
1724
- let cut = content.lastIndexOf("\n## ", charCut);
1725
- if (cut < charCut * 0.5) {
1726
- const nl = content.lastIndexOf("\n", charCut);
1727
- cut = nl > 0 ? nl : charCut;
1728
- }
1729
- return content.slice(0, cut).trimEnd() + ANTIGRAVITY_TRUNCATE_MARKER;
1730
- }
1731
- function toAntigravityRules(sections, projectName) {
1732
- return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
1733
- }
1734
- function toClaudeMd(sections, existing) {
1735
- const recordSections = sections.filter(
1736
- (s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
1737
- );
1738
- const statusMatch = existing.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
1739
- const statusSection = statusMatch ? statusMatch[0] : "";
1740
- const header = existing.split("## ")[0].trim();
1741
- const lines = [
1742
- header,
1743
- "",
1744
- statusSection,
1745
- "",
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.",
1747
- ""
1748
- ];
1749
- for (const section of recordSections) {
1750
- lines.push(`## ${section.title}`);
1751
- lines.push(section.content);
1752
- lines.push("");
1753
- }
1754
- return lines.join("\n");
1755
- }
1756
- function deriveProjectName(rulesContent) {
1757
- const firstLine = rulesContent.split("\n")[0];
1758
- return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
1759
- }
1760
- var SYNC_TARGETS = [
1761
- { path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
1762
- { path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
1763
- { path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
1764
- { path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone }
1765
- ];
1766
- async function sync() {
1767
- console.log(chalk5.bold(`
1768
- ${ko.sync.title}
1769
- `));
1770
- 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."));
1776
- 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"));
1783
- return;
1784
- }
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}`));
1797
- }
1798
- }
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(`
1810
- ${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."));
1814
- printNextStep({
1815
- message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
1816
- command: "vhk \uC810\uAC80",
1817
- cursorHint: "\uADDC\uCE59 \uC810\uAC80\uD574\uC918"
1818
- });
1819
- }
1820
-
1821
2009
  // src/commands/check.ts
1822
- import chalk7 from "chalk";
1823
- import path8 from "path";
1824
- import fs7 from "fs";
2010
+ import chalk8 from "chalk";
2011
+ import path7 from "path";
2012
+ import fs6 from "fs";
1825
2013
 
1826
2014
  // src/lib/rules-parser.ts
1827
- import fs6 from "fs";
1828
- import path7 from "path";
2015
+ import fs5 from "fs";
2016
+ import path6 from "path";
1829
2017
  function parseRules(rulesPath) {
1830
- if (!fs6.existsSync(rulesPath)) return [];
1831
- const content = fs6.readFileSync(rulesPath, "utf-8");
2018
+ if (!fs5.existsSync(rulesPath)) return [];
2019
+ const content = fs5.readFileSync(rulesPath, "utf-8");
1832
2020
  const lines = content.split("\n");
1833
2021
  const rules = [];
1834
2022
  let currentSection = "";
1835
- let ruleIndex = 0;
1836
- for (const line of lines) {
2023
+ for (let i = 0; i < lines.length; i++) {
2024
+ const line = lines[i];
2025
+ const lineNo = i + 1;
1837
2026
  if (line.startsWith("## ")) {
1838
2027
  currentSection = line.replace("## ", "").trim();
1839
2028
  continue;
1840
2029
  }
1841
2030
  const bulletMatch = line.match(/^[-*]\s+(.+)/);
1842
2031
  if (!bulletMatch) continue;
2032
+ if (isMetaSection(currentSection)) continue;
1843
2033
  const ruleText = bulletMatch[1];
1844
- ruleIndex++;
1845
2034
  if (/kebab[- ]?case/i.test(ruleText)) {
1846
- rules.push(createNamingRule(
1847
- `naming-${ruleIndex}`,
1848
- currentSection,
1849
- ruleText,
1850
- "kebab-case"
1851
- ));
2035
+ rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "kebab-case"));
1852
2036
  } else if (/camel[- ]?case/i.test(ruleText)) {
1853
- rules.push(createNamingRule(
1854
- `naming-${ruleIndex}`,
1855
- currentSection,
1856
- ruleText,
1857
- "camelCase"
1858
- ));
1859
- }
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
- ));
2037
+ rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "camelCase"));
1868
2038
  }
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
- ));
2039
+ if (isStructureSection(currentSection)) {
2040
+ const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
2041
+ if (pathMatch) {
2042
+ rules.push(createStructureRule(`structure-L${lineNo}`, currentSection, ruleText, pathMatch[1]));
1879
2043
  }
1880
2044
  }
2045
+ const banToken = extractBanToken(ruleText);
2046
+ if (banToken) {
2047
+ rules.push(createContentRule(`ban-L${lineNo}`, currentSection, ruleText, banToken, "banned"));
2048
+ }
1881
2049
  }
1882
2050
  return rules;
1883
2051
  }
2052
+ function isMetaSection(section) {
2053
+ const s = section.toLowerCase();
2054
+ 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(
2055
+ (k) => s.includes(k)
2056
+ );
2057
+ }
2058
+ function isStructureSection(section) {
2059
+ const s = section.toLowerCase();
2060
+ return ["\uC544\uD0A4\uD14D\uCC98", "\uAD6C\uC870", "\uB514\uB809\uD130\uB9AC", "\uD3F4\uB354", "architecture", "structure"].some((k) => s.includes(k));
2061
+ }
2062
+ function extractBanToken(ruleText) {
2063
+ const banKw = ruleText.search(/금지|사용하지|쓰지\s*마|하지\s*않는다|never use|do not use/i);
2064
+ if (banKw < 0) return null;
2065
+ let token = null;
2066
+ const before = [...ruleText.slice(0, banKw).matchAll(/`([^`]+)`/g)].pop();
2067
+ if (before) token = before[1];
2068
+ else {
2069
+ const after = ruleText.slice(banKw).match(/^(?:금지|사용하지|쓰지\s*마|do not use|never use)\s*[::]?\s*`([^`]+)`/i);
2070
+ if (after) token = after[1];
2071
+ }
2072
+ if (!token) return null;
2073
+ if (/:\/\/|www\.|\.(com|io|dev|net|org|app)\b|\//.test(token)) return null;
2074
+ return token;
2075
+ }
1884
2076
  function createNamingRule(id, section, desc, convention) {
1885
2077
  return {
1886
2078
  id,
@@ -1889,17 +2081,17 @@ function createNamingRule(id, section, desc, convention) {
1889
2081
  description: desc,
1890
2082
  check: (cwd) => {
1891
2083
  const violations = [];
1892
- const srcDir = path7.join(cwd, "src");
1893
- if (!fs6.existsSync(srcDir)) return violations;
2084
+ const srcDir = path6.join(cwd, "src");
2085
+ if (!fs5.existsSync(srcDir)) return violations;
1894
2086
  walkFiles(srcDir, (filePath) => {
1895
- const name = path7.basename(filePath, path7.extname(filePath));
2087
+ const name = path6.basename(filePath, path6.extname(filePath));
1896
2088
  if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
1897
2089
  if (!["index", "vite.config", "tsconfig"].includes(name)) {
1898
2090
  violations.push({
1899
2091
  ruleId: id,
1900
2092
  severity: "warning",
1901
2093
  message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
1902
- file: path7.relative(cwd, filePath)
2094
+ file: path6.relative(cwd, filePath)
1903
2095
  });
1904
2096
  }
1905
2097
  }
@@ -1915,8 +2107,8 @@ function createStructureRule(id, section, desc, expectedPath) {
1915
2107
  type: "structure",
1916
2108
  description: desc,
1917
2109
  check: (cwd) => {
1918
- const fullPath = path7.join(cwd, expectedPath);
1919
- if (!fs6.existsSync(fullPath)) {
2110
+ const fullPath = path6.join(cwd, expectedPath);
2111
+ if (!fs5.existsSync(fullPath)) {
1920
2112
  return [{
1921
2113
  ruleId: id,
1922
2114
  severity: "error",
@@ -1936,11 +2128,11 @@ function createContentRule(id, section, desc, pattern, type) {
1936
2128
  pattern: new RegExp(escapeRegex(pattern), "i"),
1937
2129
  check: (cwd) => {
1938
2130
  const violations = [];
1939
- const srcDir = path7.join(cwd, "src");
1940
- if (!fs6.existsSync(srcDir)) return violations;
2131
+ const srcDir = path6.join(cwd, "src");
2132
+ if (!fs5.existsSync(srcDir)) return violations;
1941
2133
  const regex = new RegExp(escapeRegex(pattern), "i");
1942
2134
  walkFiles(srcDir, (filePath) => {
1943
- const fileContent = fs6.readFileSync(filePath, "utf-8");
2135
+ const fileContent = fs5.readFileSync(filePath, "utf-8");
1944
2136
  const fileLines = fileContent.split("\n");
1945
2137
  fileLines.forEach((line, idx) => {
1946
2138
  if (regex.test(line)) {
@@ -1948,7 +2140,7 @@ function createContentRule(id, section, desc, pattern, type) {
1948
2140
  ruleId: id,
1949
2141
  severity: type === "banned" ? "error" : "warning",
1950
2142
  message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
1951
- file: path7.relative(cwd, filePath),
2143
+ file: path6.relative(cwd, filePath),
1952
2144
  line: idx + 1
1953
2145
  });
1954
2146
  }
@@ -1960,9 +2152,9 @@ function createContentRule(id, section, desc, pattern, type) {
1960
2152
  };
1961
2153
  }
1962
2154
  function walkFiles(dir, callback) {
1963
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
2155
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1964
2156
  for (const entry of entries) {
1965
- const fullPath = path7.join(dir, entry.name);
2157
+ const fullPath = path6.join(dir, entry.name);
1966
2158
  if (entry.isDirectory()) {
1967
2159
  if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
1968
2160
  walkFiles(fullPath, callback);
@@ -1977,13 +2169,13 @@ function escapeRegex(str) {
1977
2169
  }
1978
2170
 
1979
2171
  // 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";
2172
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
2173
+ import { join as join4 } from "path";
2174
+ import chalk7 from "chalk";
1983
2175
 
1984
2176
  // src/lib/goal-frontmatter.ts
1985
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
1986
- import { join } from "path";
2177
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
2178
+ import { join as join3 } from "path";
1987
2179
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1988
2180
  function parseFrontmatter(content) {
1989
2181
  const m = content.match(FRONTMATTER_RE);
@@ -2015,9 +2207,9 @@ function parseSimpleYaml(yaml) {
2015
2207
  return out;
2016
2208
  }
2017
2209
  function parseGoalFile(filePath) {
2018
- if (!existsSync(filePath)) return null;
2210
+ if (!existsSync2(filePath)) return null;
2019
2211
  try {
2020
- const content = readFileSync(filePath, "utf-8");
2212
+ const content = readFileSync2(filePath, "utf-8");
2021
2213
  const { frontmatter, body } = parseFrontmatter(content);
2022
2214
  return { filePath, frontmatter, body };
2023
2215
  } catch {
@@ -2025,7 +2217,7 @@ function parseGoalFile(filePath) {
2025
2217
  }
2026
2218
  }
2027
2219
  function listGoals(goalsDir) {
2028
- if (!existsSync(goalsDir)) return [];
2220
+ if (!existsSync2(goalsDir)) return [];
2029
2221
  let entries;
2030
2222
  try {
2031
2223
  entries = readdirSync(goalsDir);
@@ -2036,7 +2228,7 @@ function listGoals(goalsDir) {
2036
2228
  for (const name of entries) {
2037
2229
  if (!name.endsWith(".md")) continue;
2038
2230
  if (name === "_meta.md") continue;
2039
- const fp = join(goalsDir, name);
2231
+ const fp = join3(goalsDir, name);
2040
2232
  try {
2041
2233
  if (!statSync(fp).isFile()) continue;
2042
2234
  } catch {
@@ -2051,10 +2243,40 @@ function listGoals(goalsDir) {
2051
2243
  parsed.sort((a, b) => a.frontmatter.id - b.frontmatter.id);
2052
2244
  return parsed;
2053
2245
  }
2054
- function findDuplicateIds(goals) {
2055
- const counts = /* @__PURE__ */ new Map();
2056
- for (const g of goals) {
2057
- const id = g.frontmatter.id;
2246
+ function findSkippedGoalFiles(goalsDir) {
2247
+ if (!existsSync2(goalsDir)) return [];
2248
+ let entries;
2249
+ try {
2250
+ entries = readdirSync(goalsDir);
2251
+ } catch {
2252
+ return [];
2253
+ }
2254
+ const out = [];
2255
+ for (const name of entries) {
2256
+ if (!name.endsWith(".md")) continue;
2257
+ if (name === "_meta.md") continue;
2258
+ const fp = join3(goalsDir, name);
2259
+ try {
2260
+ if (!statSync(fp).isFile()) continue;
2261
+ } catch {
2262
+ continue;
2263
+ }
2264
+ const g = parseGoalFile(fp);
2265
+ if (!g) continue;
2266
+ const fm = g.frontmatter;
2267
+ if (fm.type === "goal" && typeof fm.id === "number") continue;
2268
+ if (fm.type === "meta") continue;
2269
+ const looksLikeGoal = fm.type === "goal" || "id" in fm || "status" in fm || "priority" in fm || "title" in fm;
2270
+ if (!looksLikeGoal) continue;
2271
+ const reason = fm.type !== "goal" ? "type: goal \uB204\uB77D (frontmatter \uC5D0 'type: goal' \uD544\uC694)" : "id \uAC00 \uC22B\uC790\uAC00 \uC544\uB2D8 ('id: 1' \uCC98\uB7FC \uC22B\uC790\uB9CC)";
2272
+ out.push({ file: name, reason });
2273
+ }
2274
+ return out;
2275
+ }
2276
+ function findDuplicateIds(goals) {
2277
+ const counts = /* @__PURE__ */ new Map();
2278
+ for (const g of goals) {
2279
+ const id = g.frontmatter.id;
2058
2280
  if (typeof id !== "number") continue;
2059
2281
  counts.set(id, (counts.get(id) ?? 0) + 1);
2060
2282
  }
@@ -2102,7 +2324,7 @@ ${body}`;
2102
2324
 
2103
2325
  // src/commands/goal.ts
2104
2326
  var GOALS_DIR = "goals";
2105
- var STATE_DIR = "docs/state";
2327
+ var STATE_DIR2 = "docs/state";
2106
2328
  var SCRIPTS_DIR = "scripts";
2107
2329
  var STATUS_ICON = {
2108
2330
  NOT_STARTED: "\u26AA",
@@ -2128,13 +2350,15 @@ function resolveGoalId(optId, goals) {
2128
2350
  return selectActiveId(goals);
2129
2351
  }
2130
2352
  async function goalList() {
2131
- console.log(chalk6.bold(`
2353
+ console.log(chalk7.bold(`
2132
2354
  ${ko.goal.listTitle}
2133
2355
  `));
2134
2356
  const goals = listGoals(GOALS_DIR);
2357
+ const skipped = findSkippedGoalFiles(GOALS_DIR);
2135
2358
  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."));
2359
+ console.log(chalk7.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2360
+ console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
2361
+ printSkippedGoalWarnings(skipped);
2138
2362
  return;
2139
2363
  }
2140
2364
  for (const g of goals) {
@@ -2151,17 +2375,33 @@ ${ko.goal.listTitle}
2151
2375
  const dups = findDuplicateIds(goals);
2152
2376
  if (dups.length > 0) {
2153
2377
  console.log("");
2154
- console.log(chalk6.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
2378
+ console.log(chalk7.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
2379
+ }
2380
+ printSkippedGoalWarnings(skipped);
2381
+ }
2382
+ function printSkippedGoalWarnings(skipped) {
2383
+ if (skipped.length > 0) {
2384
+ console.log("");
2385
+ console.log(chalk7.yellow(` ${ko.goal.skippedFiles(skipped.length)}`));
2386
+ for (const s of skipped) {
2387
+ console.log(chalk7.yellow(` - goals/${s.file}: ${s.reason}`));
2388
+ }
2389
+ console.log(chalk7.dim(" \uD544\uC218: type: goal + \uC22B\uC790 id. \uC2A4\uD0A4\uB9C8 \uC804\uCCB4: goals/_meta.md"));
2155
2390
  }
2156
2391
  }
2157
2392
  async function goalNext() {
2158
- console.log(chalk6.bold(`
2393
+ console.log(chalk7.bold(`
2159
2394
  ${ko.goal.nextTitle}
2160
2395
  `));
2161
2396
  const goals = listGoals(GOALS_DIR);
2397
+ if (goals.length === 0) {
2398
+ console.log(chalk7.yellow(" \u{1F4ED} \uC815\uC758\uB41C goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2399
+ console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
2400
+ return;
2401
+ }
2162
2402
  const activeId = selectActiveId(goals);
2163
2403
  if (activeId === null) {
2164
- console.log(chalk6.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
2404
+ console.log(chalk7.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
2165
2405
  return;
2166
2406
  }
2167
2407
  const active = goals.find((g) => g.frontmatter.id === activeId);
@@ -2180,10 +2420,10 @@ ${ko.goal.nextTitle}
2180
2420
  "```",
2181
2421
  ""
2182
2422
  ].join("\n");
2183
- mkdirSync(STATE_DIR, { recursive: true });
2184
- writeFileSync(join2(STATE_DIR, "next-task.md"), text, "utf-8");
2423
+ mkdirSync2(STATE_DIR2, { recursive: true });
2424
+ writeFileSync2(join4(STATE_DIR2, "next-task.md"), text, "utf-8");
2185
2425
  console.log(
2186
- chalk6.green(
2426
+ chalk7.green(
2187
2427
  ` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
2188
2428
  )
2189
2429
  );
@@ -2202,35 +2442,70 @@ version: v0.1
2202
2442
  ## Forbidden Actions (\uC804\uC5ED)
2203
2443
 
2204
2444
  - (\uD574\uB2F9 \uC0AC\uD56D)
2445
+
2446
+ ## Goal \uD30C\uC77C \uC2A4\uD0A4\uB9C8 (\uD544\uB3C5 \u2014 VHK-021)
2447
+
2448
+ \`vhk goal list/next/check/done\` \uB294 \`goals/*.md\`(\uC774 \`_meta.md\` \uC81C\uC678) \uC911 \uC544\uB798
2449
+ frontmatter \uB97C \uB9CC\uC871\uD558\uB294 \uD30C\uC77C\uB9CC goal \uB85C \uC778\uC2DD\uD55C\uB2E4. **\uD558\uB098\uB77C\uB3C4 \uC5B4\uAE0B\uB098\uBA74 \uC870\uC6A9\uD788 \uBB34\uC2DC**\uB418\uBA70
2450
+ \`vhk goal list\` \uAC00 \uACBD\uACE0\uB85C \uC54C\uB824\uC900\uB2E4.
2451
+
2452
+ | \uD544\uB4DC | \uD544\uC218 | \uAC12 |
2453
+ | --- | --- | --- |
2454
+ | \`type\` | \u2705 | \`goal\` (\uBB38\uC790\uC5F4 \uADF8\uB300\uB85C) |
2455
+ | \`id\` | \u2705 | **\uC22B\uC790\uB9CC** (\`1\`, \`2\` \u2026 \u2014 \`G1\` \uAC19\uC740 \uBB38\uC790\uC5F4 \u274C) |
2456
+ | \`status\` | \u2705 | \`NOT_STARTED\` | \`IN_PROGRESS\` | \`DONE\` | \`BLOCKED\` |
2457
+ | \`priority\` | \uAD8C\uC7A5 | \`P0\` | \`P1\` | \`P2\` |
2458
+ | \`title\` | \uAD8C\uC7A5 | \uD55C \uC904 \uC81C\uBAA9 |
2459
+
2460
+ \uD30C\uC77C\uBA85 \uADDC\uCE59: \`goals/<id>-<name>.md\` (\uC608: \`goals/1-login.md\`).
2461
+
2462
+ ### \uC0C8 goal \uD15C\uD50C\uB9BF (\uBCF5\uBD99)
2463
+
2464
+ \`\`\`markdown
2465
+ ---
2466
+ vhk_format: 1
2467
+ type: goal
2468
+ id: 1
2469
+ title: \uB85C\uADF8\uC778 \uAE30\uB2A5
2470
+ status: NOT_STARTED
2471
+ priority: P0
2472
+ ---
2473
+
2474
+ # Goal 1: \uB85C\uADF8\uC778 \uAE30\uB2A5
2475
+
2476
+ ## \uBC30\uACBD / \uB3D9\uC791 / Completion Check ...
2477
+ \`\`\`
2478
+
2479
+ \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uB294 \`vhk goal sync\` \uB85C \`scripts/check-goal-<id>.mjs\` \uB97C \uBC31\uD544\uD55C\uB2E4.
2205
2480
  `;
2206
2481
  var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C \uC790\uB3D9 \uAC31\uC2E0)\n```\n";
2207
2482
  var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
2208
2483
  var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
2209
2484
  async function goalInit() {
2210
- console.log(chalk6.bold(`
2485
+ console.log(chalk7.bold(`
2211
2486
  ${ko.goal.initTitle}
2212
2487
  `));
2213
2488
  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 }
2489
+ { path: join4(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
2490
+ { path: join4(STATE_DIR2, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
2491
+ { path: join4(STATE_DIR2, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
2492
+ { path: join4(STATE_DIR2, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
2218
2493
  ];
2219
- mkdirSync(GOALS_DIR, { recursive: true });
2220
- mkdirSync(STATE_DIR, { recursive: true });
2494
+ mkdirSync2(GOALS_DIR, { recursive: true });
2495
+ mkdirSync2(STATE_DIR2, { recursive: true });
2221
2496
  let created = 0;
2222
2497
  let skipped = 0;
2223
2498
  for (const t2 of targets) {
2224
- if (existsSync2(t2.path)) {
2225
- console.log(chalk6.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
2499
+ if (existsSync3(t2.path)) {
2500
+ console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
2226
2501
  skipped++;
2227
2502
  } else {
2228
- writeFileSync(t2.path, t2.content, "utf-8");
2229
- console.log(chalk6.green(` \u2713 created: ${t2.path}`));
2503
+ writeFileSync2(t2.path, t2.content, "utf-8");
2504
+ console.log(chalk7.green(` \u2713 created: ${t2.path}`));
2230
2505
  created++;
2231
2506
  }
2232
2507
  }
2233
- console.log(chalk6.bold(`
2508
+ console.log(chalk7.bold(`
2234
2509
  \u{1F4CA} created=${created} skipped=${skipped}`));
2235
2510
  if (created > 0) {
2236
2511
  printNextStep({
@@ -2241,10 +2516,10 @@ ${ko.goal.initTitle}
2241
2516
  }
2242
2517
  }
2243
2518
  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;
2519
+ const mjs = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
2520
+ if (existsSync3(mjs)) return mjs;
2521
+ const sh = join4(SCRIPTS_DIR, `check-goal-${id}.sh`);
2522
+ if (existsSync3(sh)) return sh;
2248
2523
  return null;
2249
2524
  }
2250
2525
  function runGate(scriptPath) {
@@ -2253,82 +2528,93 @@ function runGate(scriptPath) {
2253
2528
  const r = safeExecFile(runner, [scriptPath]);
2254
2529
  return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
2255
2530
  }
2531
+ function warnIfBashOnWindows(scriptPath) {
2532
+ if (process.platform === "win32" && scriptPath.endsWith(".sh")) {
2533
+ console.log(
2534
+ chalk7.yellow(
2535
+ " \u26A0 Windows: .sh \uAC8C\uC774\uD2B8\uB294 bash \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. cross-platform .mjs \uB85C \uBC31\uD544\uD558\uC138\uC694 \u2192 vhk goal sync"
2536
+ )
2537
+ );
2538
+ }
2539
+ }
2256
2540
  async function goalCheck(opts) {
2257
- console.log(chalk6.bold(`
2541
+ console.log(chalk7.bold(`
2258
2542
  ${ko.goal.checkTitle}
2259
2543
  `));
2260
2544
  const goals = listGoals(GOALS_DIR);
2261
2545
  const id = resolveGoalId(opts.id, goals);
2262
2546
  if (id === null) {
2263
2547
  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).")
2548
+ chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
2265
2549
  );
2266
2550
  process.exitCode = 1;
2267
2551
  return;
2268
2552
  }
2269
2553
  if (!goals.some((g) => g.frontmatter.id === id)) {
2270
- console.log(chalk6.red(` \u274C ${ko.goal.notFound(id)}`));
2554
+ console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
2271
2555
  process.exitCode = 1;
2272
2556
  return;
2273
2557
  }
2274
2558
  const scriptPath = findGateScript(id);
2275
2559
  if (!scriptPath) {
2276
2560
  console.log(
2277
- chalk6.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
2561
+ chalk7.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
2278
2562
  );
2279
2563
  process.exitCode = 1;
2280
2564
  return;
2281
2565
  }
2566
+ warnIfBashOnWindows(scriptPath);
2282
2567
  const gate2 = runGate(scriptPath);
2283
- console.log(chalk6.dim(` \u25B6 ${gate2.runner} ${scriptPath}
2568
+ console.log(chalk7.dim(` \u25B6 ${gate2.runner} ${scriptPath}
2284
2569
  `));
2285
2570
  if (gate2.out) console.log(gate2.out);
2286
2571
  if (gate2.ok) {
2287
- console.log(chalk6.green(`
2572
+ console.log(chalk7.green(`
2288
2573
  \u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
2289
2574
  } else {
2290
- console.log(chalk6.red(`
2575
+ console.log(chalk7.red(`
2291
2576
  \u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
2292
- if (gate2.err && !gate2.out) console.log(chalk6.dim(gate2.err.slice(0, 500)));
2577
+ if (gate2.err && !gate2.out) console.log(chalk7.dim(gate2.err.slice(0, 500)));
2293
2578
  process.exitCode = 1;
2294
2579
  }
2295
2580
  }
2296
2581
  async function goalDone(opts) {
2297
- console.log(chalk6.bold(`
2582
+ console.log(chalk7.bold(`
2298
2583
  ${ko.goal.doneTitle}
2299
2584
  `));
2300
2585
  const goals = listGoals(GOALS_DIR);
2301
2586
  const id = resolveGoalId(opts.id, goals);
2302
2587
  if (id === null) {
2303
2588
  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).")
2589
+ chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
2305
2590
  );
2306
2591
  process.exitCode = 1;
2307
2592
  return;
2308
2593
  }
2309
2594
  const target = goals.find((g) => g.frontmatter.id === id);
2310
2595
  if (!target) {
2311
- console.log(chalk6.red(` \u274C ${ko.goal.notFound(id)}`));
2596
+ console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
2312
2597
  process.exitCode = 1;
2313
2598
  return;
2314
2599
  }
2315
2600
  const scriptPath = findGateScript(id);
2316
2601
  if (!scriptPath) {
2317
2602
  console.log(
2318
- chalk6.red(
2603
+ chalk7.red(
2319
2604
  ` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
2320
2605
  )
2321
2606
  );
2322
2607
  process.exitCode = 1;
2323
2608
  return;
2324
2609
  }
2610
+ warnIfBashOnWindows(scriptPath);
2325
2611
  const gate2 = runGate(scriptPath);
2326
- console.log(chalk6.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
2612
+ console.log(chalk7.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
2327
2613
  `));
2328
2614
  if (gate2.out) console.log(gate2.out);
2329
2615
  if (!gate2.ok) {
2330
2616
  console.log(
2331
- chalk6.red(
2617
+ chalk7.red(
2332
2618
  `
2333
2619
  \u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
2334
2620
  )
@@ -2336,11 +2622,11 @@ ${ko.goal.doneTitle}
2336
2622
  process.exitCode = 1;
2337
2623
  return;
2338
2624
  }
2339
- const content = readFileSync2(target.filePath, "utf-8");
2340
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2625
+ const content = readFileSync3(target.filePath, "utf-8");
2626
+ const today = localDate();
2341
2627
  const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
2342
- writeFileSync(target.filePath, updated, "utf-8");
2343
- console.log(chalk6.green(`
2628
+ writeFileSync2(target.filePath, updated, "utf-8");
2629
+ console.log(chalk7.green(`
2344
2630
  \u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
2345
2631
  printNextStep({
2346
2632
  message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
@@ -2348,6 +2634,112 @@ ${ko.goal.doneTitle}
2348
2634
  cursorHint: "\uB2E4\uC74C goal \uC54C\uB824\uC918"
2349
2635
  });
2350
2636
  }
2637
+ function generateGateScript(id) {
2638
+ const ID = String(id);
2639
+ return [
2640
+ "#!/usr/bin/env node",
2641
+ `// scripts/check-goal-${ID}.mjs \u2014 \uC790\uB3D9 \uC0DD\uC131 (vhk goal sync).`,
2642
+ "// \uAE30\uBCF8 \uAC8C\uC774\uD2B8 = typecheck + (lint) + test + build. goal \uACE0\uC720 \uAC80\uC99D\uC740 \uC544\uB798 \uAD6C\uC5ED\uC5D0 \uCD94\uAC00.",
2643
+ "// sync \uC7AC\uC2E4\uD589\uD574\uB3C4 \uAE30\uC874 \uD30C\uC77C\uC740 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (idempotent).",
2644
+ "//",
2645
+ "// Env: VHK_GATES_SKIP_DEEP=1 \u2192 test + build \uC2A4\uD0B5 (\uBE60\uB978 typecheck-only \uD328\uC2A4)",
2646
+ "",
2647
+ "import { execFileSync } from 'node:child_process'",
2648
+ "import { existsSync, readFileSync } from 'node:fs'",
2649
+ "",
2650
+ "const SHIM = new Set(['pnpm', 'npm', 'npx', 'yarn'])",
2651
+ "function run(cmd, args) {",
2652
+ " let bin = cmd, argv = args",
2653
+ " if (process.platform === 'win32' && SHIM.has(cmd)) {",
2654
+ " // Windows: .cmd shim \uC9C1\uC811 spawn \uC740 Node CVE-2024-27980 \uC73C\uB85C EINVAL \u2192 cmd.exe \uB798\uD551.",
2655
+ " bin = 'cmd.exe'; argv = ['/d', '/s', '/c', cmd + '.cmd', ...args]",
2656
+ " }",
2657
+ " try {",
2658
+ " // maxBuffer \uC0C1\uD5A5: \uD070 \uBE4C\uB4DC/\uD14C\uC2A4\uD2B8 \uB85C\uADF8(>1MB)\uC5D0\uC11C \uC131\uACF5\uD574\uB3C4 ENOBUFS \uAC70\uC9D3\uC2E4\uD328 \uBC29\uC9C0.",
2659
+ " execFileSync(bin, argv, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 })",
2660
+ " return true",
2661
+ " } catch (e) {",
2662
+ " const out = (e?.stdout?.toString() ?? '') + (e?.stderr?.toString() ?? '')",
2663
+ " if (out.trim()) console.log(out.split('\\n').slice(-25).join('\\n'))",
2664
+ " return false",
2665
+ " }",
2666
+ "}",
2667
+ "",
2668
+ "if (existsSync('.vhk/HARD_STOP')) {",
2669
+ ` console.log('\u{1F6D1} .vhk/HARD_STOP detected \u2014 refusing to run goal ${ID} gate.')`,
2670
+ " process.exit(1)",
2671
+ "}",
2672
+ "",
2673
+ "const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf-8')) : {}",
2674
+ "const scripts = pkg.scripts ?? {}",
2675
+ "const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
2676
+ "const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
2677
+ "let pass = true",
2678
+ `const gate = (label, ok) => { console.log('[goal ${ID}] ' + label + ': ' + (ok ? '\u2713' : '\u2717')); if (!ok) pass = false }`,
2679
+ "const must = (cond, label) => { console.log((cond ? ' \u2713 ' : ' \u2717 ') + label); if (!cond) pass = false }",
2680
+ "",
2681
+ "// typecheck (\uC2A4\uD06C\uB9BD\uD2B8 \uC6B0\uC120, \uC5C6\uC73C\uBA74 tsc --noEmit)",
2682
+ "if (scripts.typecheck) gate('typecheck', run(pm, ['run', 'typecheck']))",
2683
+ "else if (existsSync('tsconfig.json')) gate('tsc --noEmit', run(pm, pm === 'npm' ? ['exec', '--', 'tsc', '--noEmit'] : ['exec', 'tsc', '--noEmit']))",
2684
+ "if (scripts.lint) gate('lint', run(pm, ['run', 'lint']))",
2685
+ "if (!skipDeep) {",
2686
+ " if (scripts['test:run']) gate('test', run(pm, ['run', 'test:run']))",
2687
+ " else if (scripts.test && /vitest/.test(scripts.test)) gate('test', run(pm, ['run', 'test', '--', '--run']))",
2688
+ " else if (scripts.test) gate('test', run(pm, ['run', 'test']))",
2689
+ " if (scripts.build) gate('build', run(pm, ['run', 'build']))",
2690
+ "}",
2691
+ "",
2692
+ `// \u2500\u2500\u2500 goal ${ID} \uACE0\uC720 \uAC80\uC99D (\uC9C1\uC811 \uCD94\uAC00) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
2693
+ "// const read = (p) => existsSync(p) ? readFileSync(p, 'utf-8') : null",
2694
+ "// must(read('src/foo.ts')?.includes('bar'), 'foo.ts \uC5D0 bar \uC874\uC7AC')",
2695
+ "",
2696
+ `if (pass) { console.log('\u2705 goal ${ID} gate passes'); process.exit(0) }`,
2697
+ `console.log('\u274C goal ${ID} gate failed'); process.exit(1)`,
2698
+ ""
2699
+ ].join("\n");
2700
+ }
2701
+ async function goalSync() {
2702
+ console.log(chalk7.bold(`
2703
+ ${ko.goal.syncTitle}
2704
+ `));
2705
+ const goals = listGoals(GOALS_DIR);
2706
+ const result = { created: [], skipped: [] };
2707
+ if (goals.length === 0) {
2708
+ console.log(
2709
+ chalk7.yellow(" \u{1F4ED} goals/ \uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694.")
2710
+ );
2711
+ return result;
2712
+ }
2713
+ mkdirSync2(SCRIPTS_DIR, { recursive: true });
2714
+ for (const g of goals) {
2715
+ const id = g.frontmatter.id;
2716
+ if (typeof id !== "number") continue;
2717
+ const target = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
2718
+ if (existsSync3(target)) {
2719
+ console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${target}`));
2720
+ result.skipped.push(id);
2721
+ continue;
2722
+ }
2723
+ const shOnly = existsSync3(join4(SCRIPTS_DIR, `check-goal-${id}.sh`));
2724
+ writeFileSync2(target, generateGateScript(id), "utf-8");
2725
+ console.log(
2726
+ chalk7.green(` \u2713 created: ${target}${shOnly ? " (.sh \u2192 .mjs \uBC31\uD544, Windows 1\uAE09)" : ""}`)
2727
+ );
2728
+ result.created.push(id);
2729
+ }
2730
+ console.log(
2731
+ chalk7.bold(`
2732
+ \u{1F4CA} created=${result.created.length} skipped=${result.skipped.length}`)
2733
+ );
2734
+ if (result.created.length > 0) {
2735
+ printNextStep({
2736
+ message: `\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 ${result.created.length}\uAC1C \uC0DD\uC131 (goal ${result.created.join(", ")}). \uAC80\uC99D\uD558\uB824\uBA74:`,
2737
+ command: `vhk goal check --id ${result.created[0]}`,
2738
+ cursorHint: `goal ${result.created[0]} \uAC8C\uC774\uD2B8 \uAC80\uC99D\uD574\uC918`
2739
+ });
2740
+ }
2741
+ return result;
2742
+ }
2351
2743
 
2352
2744
  // src/commands/check.ts
2353
2745
  async function check(opts = {}) {
@@ -2357,22 +2749,22 @@ async function check(opts = {}) {
2357
2749
  return checkRules();
2358
2750
  }
2359
2751
  async function checkRules() {
2360
- console.log(chalk7.bold(`
2752
+ console.log(chalk8.bold(`
2361
2753
  ${ko.check.title}
2362
2754
  `));
2363
2755
  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."));
2756
+ const rulesPath = path7.join(cwd, "RULES.md");
2757
+ if (!fs6.existsSync(rulesPath)) {
2758
+ console.log(chalk8.yellow(ko.check.noRules));
2759
+ console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
2368
2760
  return;
2369
2761
  }
2370
2762
  const rules = parseRules(rulesPath);
2371
- console.log(chalk7.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
2763
+ console.log(chalk8.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
2764
  `));
2373
2765
  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."));
2766
+ console.log(chalk8.yellow(ko.check.noAutoRules));
2767
+ console.log(chalk8.dim(" RULES.md\uC5D0 \uD30C\uC77C \uC774\uB984\xB7\uD3F4\uB354 \uADDC\uCE59\uC744 \uC801\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C \uC810\uAC80\uD574\uC694."));
2376
2768
  return;
2377
2769
  }
2378
2770
  const allViolations = [];
@@ -2380,13 +2772,14 @@ ${ko.check.title}
2380
2772
  for (const rule of rules) {
2381
2773
  const violations = rule.check(cwd);
2382
2774
  if (violations.length === 0) {
2383
- console.log(chalk7.green(` \u2705 ${rule.id}`) + chalk7.dim(` \u2014 ${rule.description.slice(0, 60)}`));
2775
+ const patternHint = rule.type === "content" && rule.pattern ? chalk8.dim(` [\uAC80\uC0AC: ${rule.pattern.source}]`) : "";
2776
+ console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`) + patternHint);
2384
2777
  passCount++;
2385
2778
  } else {
2386
- console.log(chalk7.red(` \u274C ${rule.id}`) + chalk7.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2779
+ console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
2387
2780
  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");
2781
+ const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
2782
+ const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
2390
2783
  console.log(` ${icon} ${v.message}${loc}`);
2391
2784
  });
2392
2785
  allViolations.push(...violations);
@@ -2396,17 +2789,18 @@ ${ko.check.title}
2396
2789
  const errors = allViolations.filter((v) => v.severity === "error").length;
2397
2790
  const warnings = allViolations.filter((v) => v.severity === "warning").length;
2398
2791
  if (allViolations.length === 0) {
2399
- console.log(chalk7.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
2792
+ console.log(chalk8.green.bold(`\u2705 \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${passCount}\uAC1C \uD1B5\uACFC`));
2793
+ console.log(chalk8.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
2794
  printNextStep({
2401
2795
  message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
2402
2796
  command: "vhk \uBCF4\uC548 scan",
2403
2797
  cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
2404
2798
  });
2405
2799
  } 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`)}`);
2800
+ console.log(chalk8.bold(ko.check.summary));
2801
+ console.log(` \uADDC\uCE59: ${chalk8.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk8.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk8.red(String(allViolations.length))}\uAC74`);
2802
+ if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
2803
+ if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
2410
2804
  printNextStep({
2411
2805
  message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
2412
2806
  command: "vhk \uC810\uAC80",
@@ -2419,36 +2813,36 @@ ${ko.check.title}
2419
2813
  }
2420
2814
 
2421
2815
  // src/commands/secure.ts
2422
- import chalk8 from "chalk";
2423
- import fs8 from "fs";
2424
- import path9 from "path";
2816
+ import chalk9 from "chalk";
2817
+ import fs7 from "fs";
2818
+ import path8 from "path";
2425
2819
  async function secure() {
2426
- console.log(chalk8.bold(`
2820
+ console.log(chalk9.bold(`
2427
2821
  ${ko.secure.title}
2428
2822
  `));
2429
2823
  const cwd = process.cwd();
2430
- const gitignorePath = path9.join(cwd, ".gitignore");
2431
- const hasGitignore = fs8.existsSync(gitignorePath);
2824
+ const gitignorePath = path8.join(cwd, ".gitignore");
2825
+ const hasGitignore = fs7.existsSync(gitignorePath);
2432
2826
  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"));
2827
+ console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
2828
+ console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
2435
2829
  } else {
2436
- const gitignoreContent = fs8.readFileSync(gitignorePath, "utf-8");
2830
+ const gitignoreContent = fs7.readFileSync(gitignorePath, "utf-8");
2437
2831
  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"));
2832
+ console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
2833
+ console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
2440
2834
  }
2441
2835
  }
2442
- console.log(chalk8.dim(` ${ko.secure.scanning}
2836
+ console.log(chalk9.dim(` ${ko.secure.scanning}
2443
2837
  `));
2444
2838
  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)`));
2839
+ console.log(chalk9.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
2446
2840
  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.`));
2841
+ console.log(chalk9.yellow(` \u26A0\uFE0F \uACB0\uACFC ${MAX_SECRET_FINDINGS}\uAC74\uC5D0\uC11C \uCD9C\uB825\uC744 \uC81C\uD55C\uD588\uC2B5\uB2C8\uB2E4. lock \uD30C\uC77C \uB4F1\uC740 \uC790\uB3D9 \uC81C\uC678\uB429\uB2C8\uB2E4.`));
2448
2842
  }
2449
2843
  console.log("");
2450
2844
  if (findings.length === 0) {
2451
- console.log(chalk8.green.bold(` ${ko.secure.clean}`));
2845
+ console.log(chalk9.green.bold(` ${ko.secure.clean}`));
2452
2846
  printNextStep({
2453
2847
  message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
2454
2848
  command: "vhk \uC815\uB9AC",
@@ -2460,161 +2854,46 @@ ${ko.secure.title}
2460
2854
  const high = findings.filter((f) => f.severity === "high");
2461
2855
  const medium = findings.filter((f) => f.severity === "medium");
2462
2856
  if (critical.length > 0) {
2463
- console.log(chalk8.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2857
+ console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
2464
2858
  critical.forEach((f) => {
2465
- console.log(chalk8.red(` \u2716 ${f.patternName}`));
2466
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2859
+ console.log(chalk9.red(` \u2716 ${f.patternName}`));
2860
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2467
2861
  });
2468
2862
  console.log("");
2469
2863
  }
2470
2864
  if (high.length > 0) {
2471
- console.log(chalk8.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2865
+ console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
2472
2866
  high.forEach((f) => {
2473
- console.log(chalk8.yellow(` \u26A0 ${f.patternName}`));
2474
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2867
+ console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
2868
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2475
2869
  });
2476
2870
  console.log("");
2477
2871
  }
2478
2872
  if (medium.length > 0) {
2479
- console.log(chalk8.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2873
+ console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
2480
2874
  medium.forEach((f) => {
2481
- console.log(chalk8.blue(` \u2139 ${f.patternName}`));
2482
- console.log(chalk8.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2875
+ console.log(chalk9.blue(` \u2139 ${f.patternName}`));
2876
+ console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
2483
2877
  });
2484
2878
  console.log("");
2485
2879
  }
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}`);
2880
+ console.log(chalk9.bold(` ${ko.secure.summary}`));
2881
+ console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
2488
2882
  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"));
2883
+ console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
2884
+ console.log(chalk9.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
2885
+ console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
2886
+ console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
2493
2887
  if (critical.length > 0 || high.length > 0) {
2494
2888
  process.exitCode = 1;
2495
2889
  }
2496
2890
  }
2497
2891
 
2498
2892
  // src/commands/doctor.ts
2499
- import chalk9 from "chalk";
2500
- import fs10 from "fs";
2501
- import path11 from "path";
2893
+ import chalk10 from "chalk";
2894
+ import fs8 from "fs";
2895
+ import path9 from "path";
2502
2896
  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
2897
  function checkCommand(name, command, hint) {
2619
2898
  const result = safeExecFile(command, ["--version"]);
2620
2899
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2622,14 +2901,14 @@ function checkCommand(name, command, hint) {
2622
2901
  return { name, command, version, ok: true, hint };
2623
2902
  }
2624
2903
  function getVhkVersion2() {
2625
- const dir = path11.dirname(fileURLToPath(import.meta.url));
2904
+ const dir = path9.dirname(fileURLToPath(import.meta.url));
2626
2905
  const candidates = [
2627
- path11.join(dir, "../package.json"),
2628
- path11.join(dir, "../../package.json")
2906
+ path9.join(dir, "../package.json"),
2907
+ path9.join(dir, "../../package.json")
2629
2908
  ];
2630
2909
  for (const pkgPath of candidates) {
2631
2910
  try {
2632
- if (fs10.existsSync(pkgPath)) {
2911
+ if (fs8.existsSync(pkgPath)) {
2633
2912
  const pkg = readJsonFile(pkgPath);
2634
2913
  return pkg.version;
2635
2914
  }
@@ -2657,7 +2936,7 @@ function compareSemver(a, b) {
2657
2936
  return a3 - b3;
2658
2937
  }
2659
2938
  async function doctor() {
2660
- console.log(chalk9.bold(`
2939
+ console.log(chalk10.bold(`
2661
2940
  ${ko.doctor.title}
2662
2941
  `));
2663
2942
  const checks = [
@@ -2669,30 +2948,30 @@ ${ko.doctor.title}
2669
2948
  let allOk = true;
2670
2949
  for (const check2 of checks) {
2671
2950
  if (check2.ok) {
2672
- console.log(chalk9.green(` \u2705 ${check2.name}`) + chalk9.dim(` \u2014 ${check2.version}`));
2951
+ console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
2673
2952
  } else {
2674
- console.log(chalk9.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2675
- console.log(chalk9.dim(` \u2192 ${check2.hint}`));
2953
+ console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
2954
+ console.log(chalk10.dim(` \u2192 ${check2.hint}`));
2676
2955
  allOk = false;
2677
2956
  }
2678
2957
  }
2679
2958
  console.log("");
2680
2959
  const vhkVersion = getVhkVersion2();
2681
2960
  if (vhkVersion) {
2682
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(` \u2014 v${vhkVersion}`));
2961
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
2683
2962
  } else {
2684
- console.log(chalk9.green(" \u2705 VHK") + chalk9.dim(" \u2014 \uC124\uCE58\uB428"));
2963
+ console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
2685
2964
  }
2686
2965
  if (vhkVersion) {
2687
2966
  const latest = fetchLatestNpmVersion("@byh3071/vhk");
2688
2967
  if (latest && compareSemver(latest, vhkVersion) > 0) {
2689
- console.log(chalk9.yellow(` ${ko.doctor.updateAvailable(latest)}`));
2968
+ console.log(chalk10.yellow(` ${ko.doctor.updateAvailable(latest)}`));
2690
2969
  } else if (latest) {
2691
- console.log(chalk9.dim(` ${ko.doctor.updateCurrent}`));
2970
+ console.log(chalk10.dim(` ${ko.doctor.updateCurrent}`));
2692
2971
  }
2693
2972
  }
2694
2973
  console.log("");
2695
- console.log(chalk9.bold(` ${ko.doctor.projectFiles}`));
2974
+ console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
2696
2975
  const cwd = process.cwd();
2697
2976
  const projectFiles = [
2698
2977
  { name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
@@ -2702,49 +2981,51 @@ ${ko.doctor.title}
2702
2981
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2703
2982
  ];
2704
2983
  for (const file of projectFiles) {
2705
- const exists = fs10.existsSync(path11.join(cwd, file.name));
2984
+ const exists = fs8.existsSync(path9.join(cwd, file.name));
2706
2985
  if (exists) {
2707
- console.log(chalk9.green(` \u2705 ${file.name}`));
2986
+ console.log(chalk10.green(` \u2705 ${file.name}`));
2708
2987
  if (file.name === ".env") {
2709
- const gitignorePath = path11.join(cwd, ".gitignore");
2710
- if (fs10.existsSync(gitignorePath)) {
2711
- const gitignore = fs10.readFileSync(gitignorePath, "utf-8");
2988
+ const gitignorePath = path9.join(cwd, ".gitignore");
2989
+ if (fs8.existsSync(gitignorePath)) {
2990
+ const gitignore = fs8.readFileSync(gitignorePath, "utf-8");
2712
2991
  if (!gitignore.includes(".env")) {
2713
- console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
2992
+ console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
2714
2993
  }
2715
2994
  }
2716
2995
  }
2996
+ } else if (file.name === ".env" && fs8.existsSync(path9.join(cwd, ".env.local"))) {
2997
+ console.log(chalk10.green(" \u2705 .env.local") + chalk10.dim(" \u2014 \uB85C\uCEEC env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
2717
2998
  } else {
2718
- console.log(chalk9.dim(` \u2B1A ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
2999
+ console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
2719
3000
  }
2720
3001
  }
2721
3002
  console.log("");
2722
- console.log(chalk9.bold(` ${ko.doctor.driftTitle}`));
3003
+ console.log(chalk10.bold(` ${ko.doctor.driftTitle}`));
2723
3004
  const ruleDrift = checkRuleDrift(cwd);
2724
3005
  if (!ruleDrift.checked) {
2725
- console.log(chalk9.dim(` ${ko.doctor.driftNoRules}`));
3006
+ console.log(chalk10.dim(` ${ko.doctor.driftNoRules}`));
2726
3007
  } else {
2727
3008
  const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
2728
3009
  if (drifted.length === 0) {
2729
- console.log(chalk9.green(` ${ko.doctor.driftRuleClean}`));
3010
+ console.log(chalk10.green(` ${ko.doctor.driftRuleClean}`));
2730
3011
  } else {
2731
- console.log(chalk9.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
3012
+ console.log(chalk10.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
2732
3013
  }
2733
3014
  }
2734
3015
  const ctxDrift = checkContextDrift(cwd);
2735
3016
  if (ctxDrift.checked && ctxDrift.stale) {
2736
- console.log(chalk9.yellow(` ${ko.doctor.driftContextWarn}`));
3017
+ console.log(chalk10.yellow(` ${ko.doctor.driftContextWarn}`));
2737
3018
  }
2738
3019
  console.log("");
2739
3020
  if (allOk) {
2740
- console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
3021
+ console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
2741
3022
  printNextStep({
2742
3023
  message: ko.doctor.nextOkMessage,
2743
3024
  command: "vhk \uC2DC\uC791",
2744
3025
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
2745
3026
  });
2746
3027
  } else {
2747
- console.log(chalk9.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
3028
+ console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
2748
3029
  printNextStep({
2749
3030
  message: ko.doctor.nextRetryMessage,
2750
3031
  command: "vhk doctor",
@@ -2755,10 +3036,10 @@ ${ko.doctor.title}
2755
3036
  }
2756
3037
 
2757
3038
  // src/commands/ship.ts
2758
- import chalk10 from "chalk";
3039
+ import chalk11 from "chalk";
2759
3040
  import inquirer4 from "inquirer";
2760
- import fs11 from "fs";
2761
- import path12 from "path";
3041
+ import fs9 from "fs";
3042
+ import path10 from "path";
2762
3043
  var CHECKLIST = [
2763
3044
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
2764
3045
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -2771,9 +3052,9 @@ function sanitizeVersion(version) {
2771
3052
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2772
3053
  }
2773
3054
  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");
3055
+ const changelogPath = path10.join(cwd, "CHANGELOG.md");
3056
+ if (!fs9.existsSync(changelogPath)) return { status: "missing" };
3057
+ const content = fs9.readFileSync(changelogPath, "utf-8");
2777
3058
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2778
3059
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2779
3060
  const blankUnreleased = [
@@ -2790,33 +3071,34 @@ function updateChangelogUnreleased(cwd, version, date) {
2790
3071
  `## [${version}] \u2014 ${date}`
2791
3072
  ].join("\n");
2792
3073
  const updated = content.replace(unreleasedHeading, blankUnreleased);
2793
- fs11.writeFileSync(changelogPath, updated, "utf-8");
3074
+ fs9.writeFileSync(changelogPath, updated, "utf-8");
2794
3075
  return { status: "updated", version };
2795
3076
  }
2796
3077
  async function ship() {
2797
- console.log(chalk10.bold(`
3078
+ if (!ensureNotHardStopped("ship")) return;
3079
+ console.log(chalk11.bold(`
2798
3080
  ${ko.ship.title}
2799
3081
  `));
2800
3082
  const cwd = process.cwd();
2801
- console.log(chalk10.cyan.bold(` ${ko.ship.checklist}
3083
+ console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
2802
3084
  `));
2803
3085
  const { passed } = await inquirer4.prompt([{
2804
3086
  type: "checkbox",
2805
3087
  name: "passed",
2806
3088
  message: ko.ship.checkboxPrompt,
2807
3089
  choices: CHECKLIST.map((c) => ({
2808
- name: `${ko.ship[c.questionKey]} ${chalk10.dim(`(${ko.ship[c.hintKey]})`)}`,
3090
+ name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
2809
3091
  value: c.id
2810
3092
  }))
2811
3093
  }]);
2812
3094
  const allPassed = passed.length === CHECKLIST.length;
2813
3095
  const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
2814
3096
  if (!allPassed) {
2815
- console.log(chalk10.yellow(`
3097
+ console.log(chalk11.yellow(`
2816
3098
  ${ko.ship.incompleteHeader}`));
2817
3099
  skipped.forEach((s) => {
2818
- console.log(chalk10.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
2819
- console.log(chalk10.dim(` \u2192 ${ko.ship[s.hintKey]}`));
3100
+ console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
3101
+ console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
2820
3102
  });
2821
3103
  const { proceed } = await inquirer4.prompt([{
2822
3104
  type: "confirm",
@@ -2833,13 +3115,13 @@ ${ko.ship.title}
2833
3115
  return;
2834
3116
  }
2835
3117
  } else {
2836
- console.log(chalk10.green(`
3118
+ console.log(chalk11.green(`
2837
3119
  ${ko.ship.allPassed}
2838
3120
  `));
2839
3121
  }
2840
- console.log(chalk10.cyan.bold(` ${ko.ship.retro}
3122
+ console.log(chalk11.cyan.bold(` ${ko.ship.retro}
2841
3123
  `));
2842
- console.log(chalk10.dim(` ${ko.ship.versionHint}`));
3124
+ console.log(chalk11.dim(` ${ko.ship.versionHint}`));
2843
3125
  const retro = await inquirer4.prompt([
2844
3126
  { type: "input", name: "version", message: ko.ship.versionPrompt },
2845
3127
  { type: "input", name: "whatWentWell", message: ko.ship.questionWell },
@@ -2847,12 +3129,12 @@ ${ko.ship.title}
2847
3129
  { type: "input", name: "learned", message: ko.ship.questionLearned },
2848
3130
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2849
3131
  ]);
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];
3132
+ const buildLogDir = path10.join(cwd, "docs", "build-log");
3133
+ if (!fs9.existsSync(buildLogDir)) fs9.mkdirSync(buildLogDir, { recursive: true });
3134
+ const today = localDate();
2853
3135
  const versionSlug = sanitizeVersion(retro.version);
2854
3136
  const fileName = `${today}-v${versionSlug}.md`;
2855
- const filePath = path12.join(buildLogDir, fileName);
3137
+ const filePath = path10.join(buildLogDir, fileName);
2856
3138
  const content = [
2857
3139
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
2858
3140
  "",
@@ -2881,9 +3163,9 @@ ${ko.ship.title}
2881
3163
  "---",
2882
3164
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2883
3165
  ].join("\n");
2884
- fs11.writeFileSync(filePath, content, "utf-8");
2885
- console.log(chalk10.green(`
2886
- ${ko.ship.buildLogDone(path12.relative(cwd, filePath))}`));
3166
+ fs9.writeFileSync(filePath, content, "utf-8");
3167
+ console.log(chalk11.green(`
3168
+ ${ko.ship.buildLogDone(path10.relative(cwd, filePath))}`));
2887
3169
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
2888
3170
  if (changelogResult.status === "updated") {
2889
3171
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -2901,8 +3183,8 @@ ${ko.ship.title}
2901
3183
  }
2902
3184
 
2903
3185
  // src/commands/save.ts
2904
- import { execFileSync as execFileSync2 } from "child_process";
2905
- import chalk11 from "chalk";
3186
+ import { execFileSync } from "child_process";
3187
+ import chalk12 from "chalk";
2906
3188
  import ora from "ora";
2907
3189
  import inquirer5 from "inquirer";
2908
3190
 
@@ -2930,29 +3212,29 @@ function statusIcon(code) {
2930
3212
  return "\u{1F4C4}";
2931
3213
  }
2932
3214
  async function save() {
2933
- console.log(chalk11.bold(`
3215
+ console.log(chalk12.bold(`
2934
3216
  \u{1F4BE} ${t("save.title")}`));
2935
- console.log(chalk11.gray("\u2500".repeat(40)));
3217
+ console.log(chalk12.gray("\u2500".repeat(40)));
2936
3218
  let gitRoot;
2937
3219
  try {
2938
- execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3220
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
2939
3221
  gitRoot = getGitRoot();
2940
3222
  } catch {
2941
- console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
3223
+ console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
2942
3224
  return;
2943
3225
  }
2944
- console.log(chalk11.cyan(`
3226
+ console.log(chalk12.cyan(`
2945
3227
  \u{1F512} ${t("save.securityWarnHeader")}`));
2946
3228
  printSecurityWarnings(gitRoot);
2947
3229
  const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
2948
3230
  if (severe.length > 0) {
2949
- console.log(chalk11.red(`
3231
+ console.log(chalk12.red(`
2950
3232
  \u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
2951
3233
  severe.slice(0, 5).forEach((f) => {
2952
- console.log(chalk11.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
3234
+ console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
2953
3235
  });
2954
3236
  if (severe.length > 5) {
2955
- console.log(chalk11.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
3237
+ console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
2956
3238
  }
2957
3239
  const { proceed } = await inquirer5.prompt([{
2958
3240
  type: "confirm",
@@ -2961,16 +3243,16 @@ async function save() {
2961
3243
  default: false
2962
3244
  }]);
2963
3245
  if (!proceed) {
2964
- console.log(chalk11.gray(t("save.cancelled")));
3246
+ console.log(chalk12.gray(t("save.cancelled")));
2965
3247
  return;
2966
3248
  }
2967
3249
  }
2968
3250
  const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
2969
3251
  if (lines.length === 0) {
2970
- console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
3252
+ console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
2971
3253
  return;
2972
3254
  }
2973
- console.log(chalk11.cyan(`
3255
+ console.log(chalk12.cyan(`
2974
3256
  \u{1F4CB} ${t("save.filesHeader", lines.length)}`));
2975
3257
  lines.forEach((line) => {
2976
3258
  const code = line.substring(0, 2);
@@ -2992,21 +3274,21 @@ async function save() {
2992
3274
  spinner.text = t("save.pushing");
2993
3275
  if (!hasGitRemote(gitRoot)) {
2994
3276
  spinner.succeed(t("save.successLocal"));
2995
- console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
3277
+ console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
2996
3278
  } else {
2997
3279
  try {
2998
3280
  gitRun(["push"], gitRoot);
2999
3281
  spinner.succeed(t("save.successWithPush"));
3000
3282
  } catch (pushErr) {
3001
3283
  spinner.fail(t("save.pushFailed"));
3002
- console.log(chalk11.red(getExecErrorMessage(pushErr)));
3003
- console.log(chalk11.yellow(`
3284
+ console.log(chalk12.red(getExecErrorMessage(pushErr)));
3285
+ console.log(chalk12.yellow(`
3004
3286
  \u{1F4A1} ${t("save.commitOkPushFailed")}`));
3005
3287
  process.exitCode = 1;
3006
3288
  }
3007
3289
  }
3008
3290
  if (process.exitCode !== 1) {
3009
- console.log(chalk11.green(`
3291
+ console.log(chalk12.green(`
3010
3292
  \u2705 ${t("save.done", lines.length)}`));
3011
3293
  printNextStep({
3012
3294
  message: t("save.nextOkMessage"),
@@ -3014,7 +3296,7 @@ async function save() {
3014
3296
  cursorHint: t("save.nextOkCursor")
3015
3297
  });
3016
3298
  } else {
3017
- console.log(chalk11.green(`
3299
+ console.log(chalk12.green(`
3018
3300
  \u2705 ${t("save.doneLocalOnly", lines.length)}`));
3019
3301
  printNextStep({
3020
3302
  message: t("save.nextPushFailMessage"),
@@ -3024,12 +3306,12 @@ async function save() {
3024
3306
  }
3025
3307
  } catch (err) {
3026
3308
  spinner.fail(t("save.failed"));
3027
- console.log(chalk11.red(getExecErrorMessage(err)));
3309
+ console.log(chalk12.red(getExecErrorMessage(err)));
3028
3310
  if (didAdd) {
3029
3311
  try {
3030
3312
  const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
3031
3313
  if (staged) {
3032
- console.log(chalk11.yellow(`
3314
+ console.log(chalk12.yellow(`
3033
3315
  \u{1F4A1} ${t("save.stagedAfterFail")}`));
3034
3316
  }
3035
3317
  } catch {
@@ -3040,8 +3322,8 @@ async function save() {
3040
3322
  }
3041
3323
 
3042
3324
  // src/commands/undo.ts
3043
- import { execFileSync as execFileSync3 } from "child_process";
3044
- import chalk12 from "chalk";
3325
+ import { execFileSync as execFileSync2 } from "child_process";
3326
+ import chalk13 from "chalk";
3045
3327
  import inquirer6 from "inquirer";
3046
3328
  function parseRecentCommits(logOutput) {
3047
3329
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
@@ -3065,30 +3347,30 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
3065
3347
  return false;
3066
3348
  }
3067
3349
  async function undo() {
3068
- console.log(chalk12.bold(`
3350
+ console.log(chalk13.bold(`
3069
3351
  \u23EA ${t("undo.title")}`));
3070
- console.log(chalk12.gray("\u2500".repeat(40)));
3352
+ console.log(chalk13.gray("\u2500".repeat(40)));
3071
3353
  let gitRoot;
3072
3354
  try {
3073
- execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3355
+ execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3074
3356
  gitRoot = getGitRoot();
3075
3357
  } catch {
3076
- console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
3358
+ console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
3077
3359
  return;
3078
3360
  }
3079
3361
  let logOutput;
3080
3362
  try {
3081
3363
  logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
3082
3364
  } catch {
3083
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3365
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3084
3366
  return;
3085
3367
  }
3086
3368
  const commits = parseRecentCommits(logOutput);
3087
3369
  if (commits.length === 0) {
3088
- console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3370
+ console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
3089
3371
  return;
3090
3372
  }
3091
- console.log(chalk12.cyan(`
3373
+ console.log(chalk13.cyan(`
3092
3374
  ${t("undo.recentHeader")}`));
3093
3375
  commits.forEach((c, i) => {
3094
3376
  console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
@@ -3105,7 +3387,7 @@ ${t("undo.recentHeader")}`));
3105
3387
  const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
3106
3388
  const headCount = countLocalCommits(gitRoot);
3107
3389
  if (undoCount >= headCount) {
3108
- console.log(chalk12.yellow(`
3390
+ console.log(chalk13.yellow(`
3109
3391
  \u{1F4ED} ${t("undo.rootCommit")}`));
3110
3392
  return;
3111
3393
  }
@@ -3114,10 +3396,10 @@ ${t("undo.recentHeader")}`));
3114
3396
  const risky = isUndoRisky(undoCount, unpushed, remote);
3115
3397
  if (risky) {
3116
3398
  if (unpushed < 0) {
3117
- console.log(chalk12.red(`
3399
+ console.log(chalk13.red(`
3118
3400
  \u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
3119
3401
  } else {
3120
- console.log(chalk12.red(`
3402
+ console.log(chalk13.red(`
3121
3403
  \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
3122
3404
  }
3123
3405
  }
@@ -3128,16 +3410,16 @@ ${t("undo.recentHeader")}`));
3128
3410
  default: false
3129
3411
  }]);
3130
3412
  if (!confirm) {
3131
- console.log(chalk12.gray(t("undo.cancelled")));
3413
+ console.log(chalk13.gray(t("undo.cancelled")));
3132
3414
  return;
3133
3415
  }
3134
3416
  try {
3135
3417
  gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
3136
- console.log(chalk12.green(`
3418
+ console.log(chalk13.green(`
3137
3419
  \u2705 ${t("undo.success")}`));
3138
- console.log(chalk12.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3420
+ console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
3139
3421
  if (risky) {
3140
- console.log(chalk12.yellow(`
3422
+ console.log(chalk13.yellow(`
3141
3423
  \u{1F4A1} ${t("undo.forcePushHint")}`));
3142
3424
  }
3143
3425
  printNextStep({
@@ -3146,18 +3428,73 @@ ${t("undo.recentHeader")}`));
3146
3428
  cursorHint: t("undo.nextCursor")
3147
3429
  });
3148
3430
  } catch (err) {
3149
- console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3431
+ console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
3150
3432
  const msg = err instanceof Error ? err.message : String(err);
3151
- console.log(chalk12.red(msg));
3433
+ console.log(chalk13.red(msg));
3434
+ process.exitCode = 1;
3435
+ }
3436
+ }
3437
+
3438
+ // src/commands/restore.ts
3439
+ import chalk14 from "chalk";
3440
+ import inquirer7 from "inquirer";
3441
+ async function restore(id) {
3442
+ console.log(chalk14.bold(`
3443
+ ${ko.restore.title}`));
3444
+ console.log(chalk14.gray("\u2500".repeat(40)));
3445
+ console.log(chalk14.dim(` ${ko.restore.notGitNote}`));
3446
+ const cwd = process.cwd();
3447
+ const backups = listBackups(cwd);
3448
+ if (backups.length === 0) {
3449
+ console.log(chalk14.yellow(`
3450
+ ${ko.restore.noBackups}`));
3451
+ return;
3452
+ }
3453
+ let targetId = id;
3454
+ if (!targetId) {
3455
+ if (!process.stdout.isTTY) {
3456
+ console.log(chalk14.cyan(`
3457
+ ${ko.restore.listHeader}`));
3458
+ for (const b of backups) console.log(` ${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`);
3459
+ console.log(chalk14.yellow(`
3460
+ ${ko.restore.nonTtyHint}`));
3461
+ return;
3462
+ }
3463
+ const { picked } = await inquirer7.prompt([
3464
+ {
3465
+ type: "list",
3466
+ name: "picked",
3467
+ message: ko.restore.selectPrompt,
3468
+ choices: backups.map((b) => ({
3469
+ name: `${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`,
3470
+ value: b.id
3471
+ }))
3472
+ }
3473
+ ]);
3474
+ targetId = picked;
3475
+ }
3476
+ try {
3477
+ const restored = restoreBackup(targetId, cwd);
3478
+ console.log(chalk14.green(`
3479
+ ${ko.restore.restored(restored.length, targetId)}`));
3480
+ for (const r of restored) console.log(chalk14.gray(` ${r}`));
3481
+ printNextStep({
3482
+ message: "\uBC31\uC5C5 \uBCF5\uC6D0 \uC644\uB8CC! \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.",
3483
+ command: "vhk diff",
3484
+ cursorHint: "\uBCC0\uACBD\uC0AC\uD56D \uBCF4\uC5EC\uC918"
3485
+ });
3486
+ } catch {
3487
+ console.log(chalk14.red(`
3488
+ ${ko.restore.notFound(targetId)}`));
3152
3489
  process.exitCode = 1;
3153
3490
  }
3154
3491
  }
3155
3492
 
3156
3493
  // src/commands/status.ts
3157
- import { execFileSync as execFileSync4 } from "child_process";
3158
- import fs12 from "fs";
3159
- import path13 from "path";
3160
- import chalk13 from "chalk";
3494
+ import { execFileSync as execFileSync3 } from "child_process";
3495
+ import fs10 from "fs";
3496
+ import path11 from "path";
3497
+ import chalk15 from "chalk";
3161
3498
  function countFileChanges(porcelain) {
3162
3499
  const lines = porcelain.split("\n").filter(Boolean);
3163
3500
  let staged = 0;
@@ -3195,8 +3532,8 @@ function parseRecentCommitLines(logOutput) {
3195
3532
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3196
3533
  }
3197
3534
  function readProjectPackage(cwd = process.cwd()) {
3198
- const pkgPath = path13.join(cwd, "package.json");
3199
- if (!fs12.existsSync(pkgPath)) return null;
3535
+ const pkgPath = path11.join(cwd, "package.json");
3536
+ if (!fs10.existsSync(pkgPath)) return null;
3200
3537
  try {
3201
3538
  const pkg = readJsonFile(pkgPath);
3202
3539
  if (!pkg.name && !pkg.version) return null;
@@ -3208,6 +3545,21 @@ function readProjectPackage(cwd = process.cwd()) {
3208
3545
  return null;
3209
3546
  }
3210
3547
  }
3548
+ function selectStatusNextStep(hasChanges) {
3549
+ if (hasChanges) {
3550
+ return {
3551
+ message: t("status.nextWithChangesMessage"),
3552
+ command: "vhk diff",
3553
+ cursorHint: t("status.nextWithChangesCursor"),
3554
+ alternative: t("status.nextWithChangesAlt")
3555
+ };
3556
+ }
3557
+ return {
3558
+ message: t("status.nextCleanMessage"),
3559
+ command: "vhk goal next",
3560
+ cursorHint: t("status.nextCleanCursor")
3561
+ };
3562
+ }
3211
3563
  function getSyncCounts(gitRoot) {
3212
3564
  try {
3213
3565
  const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
@@ -3217,15 +3569,15 @@ function getSyncCounts(gitRoot) {
3217
3569
  }
3218
3570
  }
3219
3571
  async function status() {
3220
- console.log(chalk13.bold(`
3572
+ console.log(chalk15.bold(`
3221
3573
  \u{1F4CA} ${t("status.title")}`));
3222
- console.log(chalk13.gray("\u2500".repeat(40)));
3574
+ console.log(chalk15.gray("\u2500".repeat(40)));
3223
3575
  let gitRoot;
3224
3576
  try {
3225
- execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3577
+ execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3226
3578
  gitRoot = getGitRoot();
3227
3579
  } catch {
3228
- console.log(chalk13.red(`\u274C ${t("status.notGitRepo")}`));
3580
+ console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
3229
3581
  return;
3230
3582
  }
3231
3583
  let branch;
@@ -3244,48 +3596,37 @@ async function status() {
3244
3596
  commits = [];
3245
3597
  }
3246
3598
  const pkg = readProjectPackage();
3247
- console.log(chalk13.cyan(`
3248
- \u{1F33F} ${t("status.branch")}`) + chalk13.white(` ${branch}`));
3599
+ console.log(chalk15.cyan(`
3600
+ \u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
3249
3601
  console.log(
3250
- chalk13.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk13.white(
3602
+ chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
3251
3603
  ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3252
3604
  )
3253
3605
  );
3254
- console.log(chalk13.cyan(`
3255
- \u{1F4CB} ${t("status.recentCommits")}`));
3606
+ console.log(chalk15.cyan(`
3607
+ \u{1F4CB} ${t("status.recentCommits", commits.length)}`));
3256
3608
  if (commits.length === 0) {
3257
- console.log(chalk13.dim(` ${t("status.noCommits")}`));
3609
+ console.log(chalk15.dim(` ${t("status.noCommits")}`));
3258
3610
  } else {
3259
- commits.forEach((c) => console.log(` ${chalk13.dim("\u2022")} ${c}`));
3611
+ commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
3260
3612
  }
3261
3613
  console.log(
3262
- chalk13.cyan(`
3263
- \u{1F504} ${t("status.remote")}`) + chalk13.white(` ${formatSyncLabel(sync2)}`)
3614
+ chalk15.cyan(`
3615
+ \u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
3264
3616
  );
3265
- console.log(chalk13.gray("\n" + "\u2500".repeat(40)));
3617
+ console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
3266
3618
  if (pkg) {
3267
- console.log(chalk13.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk13.white(` ${pkg.name} v${pkg.version}`));
3619
+ console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
3268
3620
  } else {
3269
- console.log(chalk13.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3621
+ console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3270
3622
  }
3271
3623
  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
- }
3624
+ printNextStep(selectStatusNextStep(hasChanges));
3625
+ printContextResumeHint();
3285
3626
  }
3286
3627
 
3287
3628
  // src/commands/diff.ts
3288
- import chalk14 from "chalk";
3629
+ import chalk16 from "chalk";
3289
3630
  function gitOut2(args) {
3290
3631
  const r = safeExecFile("git", args);
3291
3632
  return r.ok ? r.out : "";
@@ -3322,67 +3663,67 @@ function summarizeNumstat(numstat) {
3322
3663
  return { fileCount, totalAdd, totalDel };
3323
3664
  }
3324
3665
  function printFile(f) {
3325
- const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
3326
- const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
3666
+ const adds = f.additions > 0 ? chalk16.green(`+${f.additions}`) : "";
3667
+ const dels = f.deletions > 0 ? chalk16.red(`-${f.deletions}`) : "";
3327
3668
  const change = [adds, dels].filter(Boolean).join(" ");
3328
3669
  console.log(` ${f.name} ${change}`);
3329
3670
  }
3330
3671
  async function diff() {
3331
- console.log(chalk14.bold(`
3672
+ console.log(chalk16.bold(`
3332
3673
  \u{1F50D} ${t("diff.title")}`));
3333
- console.log(chalk14.gray("\u2500".repeat(40)));
3674
+ console.log(chalk16.gray("\u2500".repeat(40)));
3334
3675
  if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
3335
- console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
3676
+ console.log(chalk16.red(`\u274C ${t("diff.notGitRepo")}`));
3336
3677
  return;
3337
3678
  }
3338
3679
  const unstaged = gitOut2(["diff", "--stat"]);
3339
3680
  const staged = gitOut2(["diff", "--cached", "--stat"]);
3340
3681
  const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3341
3682
  if (!unstaged && !staged && !untracked) {
3342
- console.log(chalk14.green(`
3683
+ console.log(chalk16.green(`
3343
3684
  \u2705 ${t("diff.noChanges")}`));
3344
3685
  return;
3345
3686
  }
3346
3687
  if (staged) {
3347
- console.log(chalk14.cyan(`
3688
+ console.log(chalk16.cyan(`
3348
3689
  ${t("diff.stagedHeader")}`));
3349
3690
  parseDiffStat(staged).forEach((f) => printFile(f));
3350
3691
  }
3351
3692
  if (unstaged) {
3352
- console.log(chalk14.cyan(`
3693
+ console.log(chalk16.cyan(`
3353
3694
  ${t("diff.unstagedHeader")}`));
3354
3695
  parseDiffStat(unstaged).forEach((f) => printFile(f));
3355
3696
  }
3356
3697
  if (untracked) {
3357
3698
  const files = untracked.split("\n").filter(Boolean);
3358
- console.log(chalk14.cyan(`
3699
+ console.log(chalk16.cyan(`
3359
3700
  ${t("diff.untrackedHeader", files.length)}`));
3360
- files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
3701
+ files.forEach((f) => console.log(` ${chalk16.green("+")} ${f}`));
3361
3702
  }
3362
3703
  const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3363
3704
  if (numstat) {
3364
3705
  const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3365
- console.log(chalk14.cyan(`
3706
+ console.log(chalk16.cyan(`
3366
3707
  ${t("diff.summaryHeader")}`));
3367
3708
  console.log(` ${t("diff.filesLine", fileCount)}`);
3368
- console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
3369
- console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
3709
+ console.log(` \uCD94\uAC00: ${chalk16.green(`+${totalAdd}`)}\uC904`);
3710
+ console.log(` \uC0AD\uC81C: ${chalk16.red(`-${totalDel}`)}\uC904`);
3370
3711
  }
3371
3712
  console.log("");
3372
3713
  }
3373
3714
 
3374
3715
  // 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";
3716
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
3717
+ import { join as join5, dirname } from "path";
3377
3718
  import { fileURLToPath as fileURLToPath2 } from "url";
3378
- import chalk15 from "chalk";
3719
+ import chalk17 from "chalk";
3379
3720
  function resolveMcpEntryPoint() {
3380
3721
  try {
3381
3722
  const here = fileURLToPath2(import.meta.url);
3382
3723
  const dir = dirname(here);
3383
3724
  for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
3384
- const candidate = join3(dir, ...rel);
3385
- if (existsSync3(candidate)) return candidate;
3725
+ const candidate = join5(dir, ...rel);
3726
+ if (existsSync4(candidate)) return candidate;
3386
3727
  }
3387
3728
  } catch {
3388
3729
  }
@@ -3390,17 +3731,17 @@ function resolveMcpEntryPoint() {
3390
3731
  const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
3391
3732
  if (typeof url === "string") {
3392
3733
  const p = fileURLToPath2(url);
3393
- if (existsSync3(p)) return p;
3734
+ if (existsSync4(p)) return p;
3394
3735
  }
3395
3736
  } catch {
3396
3737
  }
3397
3738
  try {
3398
- const pkgPath = join3(process.cwd(), "package.json");
3399
- if (existsSync3(pkgPath)) {
3739
+ const pkgPath = join5(process.cwd(), "package.json");
3740
+ if (existsSync4(pkgPath)) {
3400
3741
  const pkg = readJsonFile(pkgPath);
3401
3742
  if (pkg.name === "@byh3071/vhk") {
3402
- const local = join3(process.cwd(), "dist", "mcp", "index.js");
3403
- if (existsSync3(local)) return local;
3743
+ const local = join5(process.cwd(), "dist", "mcp", "index.js");
3744
+ if (existsSync4(local)) return local;
3404
3745
  }
3405
3746
  }
3406
3747
  } catch {
@@ -3415,31 +3756,31 @@ function resolveVhkMcpEntry() {
3415
3756
  return { command: "vhk-mcp", args: [] };
3416
3757
  }
3417
3758
  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 });
3759
+ console.log(chalk17.bold("\n\u{1F50C} " + t("mcp.initTitle")));
3760
+ console.log(chalk17.gray("\u2500".repeat(40)));
3761
+ const cursorDir = join5(process.cwd(), ".cursor");
3762
+ if (!existsSync4(cursorDir)) {
3763
+ mkdirSync3(cursorDir, { recursive: true });
3423
3764
  }
3424
- const configPath = join3(cursorDir, "mcp.json");
3765
+ const configPath = join5(cursorDir, "mcp.json");
3425
3766
  const vhkEntry = resolveVhkMcpEntry();
3426
3767
  let config;
3427
- if (existsSync3(configPath)) {
3768
+ if (existsSync4(configPath)) {
3428
3769
  try {
3429
3770
  const parsed = readJsonFile(configPath);
3430
3771
  config = {
3431
3772
  mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
3432
3773
  };
3433
3774
  } 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."));
3775
+ console.log(chalk17.yellow("\u26A0\uFE0F \uAE30\uC874 .cursor/mcp.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uC0C8 \uD30C\uC77C\uB85C \uB36E\uC5B4\uC501\uB2C8\uB2E4."));
3435
3776
  config = { mcpServers: { vhk: vhkEntry } };
3436
3777
  }
3437
3778
  } else {
3438
3779
  config = { mcpServers: { vhk: vhkEntry } };
3439
3780
  }
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:"));
3781
+ writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
3782
+ console.log(chalk17.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
3783
+ console.log(chalk17.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
3443
3784
  console.log(` ${configPath}`);
3444
3785
  printNextStep({
3445
3786
  message: t("mcp.nextMessage"),
@@ -3449,9 +3790,9 @@ async function mcpInit() {
3449
3790
  }
3450
3791
 
3451
3792
  // 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";
3793
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3794
+ import chalk18 from "chalk";
3795
+ import inquirer8 from "inquirer";
3455
3796
  var PALETTES = [
3456
3797
  {
3457
3798
  name: "Minimal",
@@ -3503,7 +3844,36 @@ var PALETTES = [
3503
3844
  }
3504
3845
  ];
3505
3846
  function hasTailwind() {
3506
- return existsSync4("tailwind.config.js") || existsSync4("tailwind.config.ts") || existsSync4("tailwind.config.mjs") || existsSync4("tailwind.config.cjs");
3847
+ return existsSync5("tailwind.config.js") || existsSync5("tailwind.config.ts") || existsSync5("tailwind.config.mjs") || existsSync5("tailwind.config.cjs");
3848
+ }
3849
+ function isTailwindV4Deps(deps) {
3850
+ if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return true;
3851
+ const tw = deps.tailwindcss;
3852
+ return typeof tw === "string" && /^\D*4(\.|$)/.test(tw);
3853
+ }
3854
+ function hasTailwindV4() {
3855
+ try {
3856
+ const pkg = readJsonFile(
3857
+ "package.json"
3858
+ );
3859
+ return isTailwindV4Deps({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
3860
+ } catch {
3861
+ return false;
3862
+ }
3863
+ }
3864
+ function generateTailwindV4Theme(palette) {
3865
+ return [
3866
+ "/* vhk design \u2014 Tailwind v4 @theme \uD1A0\uD070 (CSS-first). \uC9C4\uC785 CSS \uC5D0 @import \uD558\uC138\uC694. */",
3867
+ '@import "tailwindcss";',
3868
+ "",
3869
+ "@theme {",
3870
+ ...Object.entries(palette.colors).map(([k, v]) => ` --color-${k}: ${v};`),
3871
+ "}",
3872
+ "",
3873
+ "/* \uB2E4\uD06C \uBAA8\uB4DC \u2014 .dark \uD074\uB798\uC2A4 \uAE30\uBC18 variant (bg-background \uB4F1\uC774 .dark \uC5D0\uC11C \uC804\uD658) */",
3874
+ "@custom-variant dark (&:where(.dark, .dark *));",
3875
+ ""
3876
+ ].join("\n");
3507
3877
  }
3508
3878
  function generateCSSTokens(palette) {
3509
3879
  const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
@@ -3524,9 +3894,10 @@ export default vhkColors
3524
3894
  `;
3525
3895
  }
3526
3896
  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([
3897
+ console.log(chalk18.bold("\n\u{1F3A8} " + t("design.title")));
3898
+ console.log(chalk18.gray("\u2500".repeat(40)));
3899
+ if (!ensureInteractive("\uCEEC\uB7EC \uD314\uB808\uD2B8 \uC120\uD0DD\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
3900
+ const { paletteIndex } = await inquirer8.prompt([
3530
3901
  {
3531
3902
  type: "list",
3532
3903
  name: "paletteIndex",
@@ -3538,32 +3909,37 @@ async function design() {
3538
3909
  }
3539
3910
  ]);
3540
3911
  const palette = PALETTES[paletteIndex];
3541
- console.log(chalk16.cyan(`
3912
+ console.log(chalk18.cyan(`
3542
3913
  \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([{
3914
+ const v4 = hasTailwindV4();
3915
+ const targetPath = v4 ? "src/styles/theme.css" : hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
3916
+ const content = v4 ? generateTailwindV4Theme(palette) : hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
3917
+ if (existsSync5(targetPath)) {
3918
+ const { overwrite } = await inquirer8.prompt([{
3547
3919
  type: "confirm",
3548
3920
  name: "overwrite",
3549
3921
  message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
3550
3922
  default: false
3551
3923
  }]);
3552
3924
  if (!overwrite) {
3553
- console.log(chalk16.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
3925
+ console.log(chalk18.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
3554
3926
  return;
3555
3927
  }
3556
3928
  }
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."));
3929
+ mkdirSync4("src/styles", { recursive: true });
3930
+ writeFileSync4(targetPath, content, "utf-8");
3931
+ if (v4) {
3932
+ console.log(chalk18.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (Tailwind v4 @theme)"));
3933
+ console.log(chalk18.gray(' \uC9C4\uC785 CSS(\uC608: src/index.css)\uC5D0 `@import "./styles/theme.css";` \uCD94\uAC00 \u2192 bg-primary \uB4F1 \uC720\uD2F8 \uC0AC\uC6A9.'));
3934
+ console.log(chalk18.gray(" \uB2E4\uD06C \uD1A0\uAE00: \uB8E8\uD2B8 <html>/<body> \uC5D0 `.dark` \uD074\uB798\uC2A4 on/off."));
3935
+ } else if (hasTailwind()) {
3936
+ console.log(chalk18.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
3937
+ console.log(chalk18.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
3562
3938
  } 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."));
3939
+ console.log(chalk18.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
3940
+ console.log(chalk18.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
3565
3941
  }
3566
- console.log(chalk16.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
3942
+ console.log(chalk18.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
3567
3943
  for (const [key, value] of Object.entries(palette.colors)) {
3568
3944
  console.log(` ${key.padEnd(12)} ${value}`);
3569
3945
  }
@@ -3578,9 +3954,9 @@ async function designPalette() {
3578
3954
  }
3579
3955
 
3580
3956
  // 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";
3957
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3958
+ import chalk19 from "chalk";
3959
+ import inquirer9 from "inquirer";
3584
3960
  function generateDarkCSS() {
3585
3961
  return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
3586
3962
 
@@ -3636,13 +4012,13 @@ export function initTheme(): void {
3636
4012
  `;
3637
4013
  }
3638
4014
  async function theme() {
3639
- console.log(chalk17.bold("\n\u{1F319} " + t("theme.title")));
3640
- console.log(chalk17.gray("\u2500".repeat(40)));
4015
+ console.log(chalk19.bold("\n\u{1F319} " + t("theme.title")));
4016
+ console.log(chalk19.gray("\u2500".repeat(40)));
3641
4017
  const cssPath = "src/styles/theme.css";
3642
4018
  const togglePath = "src/lib/theme-toggle.ts";
3643
- const conflicts = [cssPath, togglePath].filter((p) => existsSync5(p));
4019
+ const conflicts = [cssPath, togglePath].filter((p) => existsSync6(p));
3644
4020
  if (conflicts.length > 0) {
3645
- const { overwrite } = await inquirer8.prompt([{
4021
+ const { overwrite } = await inquirer9.prompt([{
3646
4022
  type: "confirm",
3647
4023
  name: "overwrite",
3648
4024
  message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
@@ -3650,21 +4026,21 @@ async function theme() {
3650
4026
  default: false
3651
4027
  }]);
3652
4028
  if (!overwrite) {
3653
- console.log(chalk17.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
4029
+ console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
3654
4030
  return;
3655
4031
  }
3656
4032
  }
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"));
4033
+ mkdirSync5("src/styles", { recursive: true });
4034
+ mkdirSync5("src/lib", { recursive: true });
4035
+ writeFileSync5(cssPath, generateDarkCSS(), "utf-8");
4036
+ console.log(chalk19.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
4037
+ writeFileSync5(togglePath, generateToggleUtil(), "utf-8");
4038
+ console.log(chalk19.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
4039
+ console.log(chalk19.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
4040
+ console.log(chalk19.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
4041
+ console.log(chalk19.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
4042
+ console.log(chalk19.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
4043
+ console.log(chalk19.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
3668
4044
  printNextStep({
3669
4045
  message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
3670
4046
  command: "vhk ref list",
@@ -3673,11 +4049,11 @@ async function theme() {
3673
4049
  }
3674
4050
 
3675
4051
  // src/commands/ref.ts
3676
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
3677
- import chalk18 from "chalk";
4052
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
4053
+ import chalk20 from "chalk";
3678
4054
  var REFS_PATH = ".vhk/refs.json";
3679
4055
  function loadRefs() {
3680
- if (!existsSync6(REFS_PATH)) return [];
4056
+ if (!existsSync7(REFS_PATH)) return [];
3681
4057
  try {
3682
4058
  const parsed = readJsonFile(REFS_PATH);
3683
4059
  return Array.isArray(parsed) ? parsed : [];
@@ -3686,28 +4062,28 @@ function loadRefs() {
3686
4062
  }
3687
4063
  }
3688
4064
  function saveRefs(refs) {
3689
- mkdirSync5(".vhk", { recursive: true });
3690
- writeFileSync5(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
4065
+ mkdirSync6(".vhk", { recursive: true });
4066
+ writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
3691
4067
  }
3692
4068
  async function refAdd(url, memo = "") {
3693
- console.log(chalk18.bold("\n\u{1F517} " + t("ref.addTitle")));
3694
- console.log(chalk18.gray("\u2500".repeat(40)));
4069
+ console.log(chalk20.bold("\n\u{1F517} " + t("ref.addTitle")));
4070
+ console.log(chalk20.gray("\u2500".repeat(40)));
3695
4071
  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"'));
4072
+ console.log(chalk20.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
4073
+ console.log(chalk20.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
3698
4074
  return;
3699
4075
  }
3700
4076
  const refs = loadRefs();
3701
4077
  if (refs.some((r) => r.url === url)) {
3702
- console.log(chalk18.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
4078
+ console.log(chalk20.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
3703
4079
  return;
3704
4080
  }
3705
4081
  refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
3706
4082
  saveRefs(refs);
3707
- console.log(chalk18.green(`
4083
+ console.log(chalk20.green(`
3708
4084
  \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}`));
4085
+ console.log(chalk20.cyan(` ${url}`));
4086
+ if (memo) console.log(chalk20.gray(` \u{1F4DD} ${memo}`));
3711
4087
  printNextStep({
3712
4088
  message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
3713
4089
  command: "vhk ref list",
@@ -3715,22 +4091,22 @@ async function refAdd(url, memo = "") {
3715
4091
  });
3716
4092
  }
3717
4093
  async function refList() {
3718
- console.log(chalk18.bold("\n\u{1F4DA} " + t("ref.listTitle")));
3719
- console.log(chalk18.gray("\u2500".repeat(40)));
4094
+ console.log(chalk20.bold("\n\u{1F4DA} " + t("ref.listTitle")));
4095
+ console.log(chalk20.gray("\u2500".repeat(40)));
3720
4096
  const refs = loadRefs();
3721
4097
  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.'));
4098
+ console.log(chalk20.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
4099
+ console.log(chalk20.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
3724
4100
  return;
3725
4101
  }
3726
- console.log(chalk18.cyan(`
4102
+ console.log(chalk20.cyan(`
3727
4103
  \uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
3728
4104
  `));
3729
4105
  refs.forEach((ref, index) => {
3730
4106
  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}`));
4107
+ console.log(chalk20.white(` [${index + 1}] ${ref.url}`));
4108
+ if (ref.memo) console.log(chalk20.gray(` \u{1F4DD} ${ref.memo}`));
4109
+ console.log(chalk20.gray(` \u{1F4C5} ${date}`));
3734
4110
  console.log("");
3735
4111
  });
3736
4112
  }
@@ -3738,7 +4114,7 @@ async function refOpen(indexStr) {
3738
4114
  const refs = loadRefs();
3739
4115
  const idx = parseInt(indexStr, 10) - 1;
3740
4116
  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})`));
4117
+ console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
3742
4118
  return;
3743
4119
  }
3744
4120
  const ref = refs[idx];
@@ -3746,14 +4122,14 @@ async function refOpen(indexStr) {
3746
4122
  try {
3747
4123
  parsed = new URL(ref.url);
3748
4124
  } catch {
3749
- console.log(chalk18.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
4125
+ console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
3750
4126
  return;
3751
4127
  }
3752
4128
  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})`));
4129
+ console.log(chalk20.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
3754
4130
  return;
3755
4131
  }
3756
- console.log(chalk18.cyan(`
4132
+ console.log(chalk20.cyan(`
3757
4133
  \u{1F310} \uC5F4\uAE30: ${ref.url}`));
3758
4134
  let result;
3759
4135
  if (process.platform === "darwin") {
@@ -3764,19 +4140,19 @@ async function refOpen(indexStr) {
3764
4140
  result = safeExecFile("xdg-open", [ref.url]);
3765
4141
  }
3766
4142
  if (result.ok) {
3767
- console.log(chalk18.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
4143
+ console.log(chalk20.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
3768
4144
  } 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."));
4145
+ console.log(chalk20.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
4146
  }
3771
4147
  }
3772
4148
 
3773
4149
  // src/commands/harness.ts
3774
- import { existsSync as existsSync7 } from "fs";
3775
- import chalk19 from "chalk";
4150
+ import { existsSync as existsSync8 } from "fs";
4151
+ import chalk21 from "chalk";
3776
4152
  import ora2 from "ora";
3777
4153
  function detectPM() {
3778
- if (existsSync7("pnpm-lock.yaml")) return "pnpm";
3779
- if (existsSync7("yarn.lock")) return "yarn";
4154
+ if (existsSync8("pnpm-lock.yaml")) return "pnpm";
4155
+ if (existsSync8("yarn.lock")) return "yarn";
3780
4156
  return "npm";
3781
4157
  }
3782
4158
  function pmRun(pm, script) {
@@ -3794,14 +4170,14 @@ function detectChecks() {
3794
4170
  const pm = detectPM();
3795
4171
  if (s.lint) {
3796
4172
  checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
3797
- } else if (existsSync7(".eslintrc.js") || existsSync7(".eslintrc.json") || existsSync7("eslint.config.js")) {
4173
+ } else if (existsSync8(".eslintrc.js") || existsSync8(".eslintrc.json") || existsSync8("eslint.config.js")) {
3798
4174
  checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
3799
4175
  }
3800
4176
  if (s["type-check"]) {
3801
4177
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
3802
4178
  } else if (s.typecheck) {
3803
4179
  checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
3804
- } else if (existsSync7("tsconfig.json")) {
4180
+ } else if (existsSync8("tsconfig.json")) {
3805
4181
  checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
3806
4182
  }
3807
4183
  if (s.test) {
@@ -3813,15 +4189,16 @@ function detectChecks() {
3813
4189
  return checks;
3814
4190
  }
3815
4191
  async function harness() {
3816
- console.log(chalk19.bold("\n\u{1F527} " + t("harness.title")));
3817
- console.log(chalk19.gray("\u2500".repeat(40)));
4192
+ if (!ensureNotHardStopped("harness")) return;
4193
+ console.log(chalk21.bold("\n\u{1F527} " + t("harness.title")));
4194
+ console.log(chalk21.gray("\u2500".repeat(40)));
3818
4195
  const checks = detectChecks();
3819
4196
  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."));
4197
+ console.log(chalk21.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
4198
+ console.log(chalk21.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
3822
4199
  return;
3823
4200
  }
3824
- console.log(chalk19.cyan(`
4201
+ console.log(chalk21.cyan(`
3825
4202
  \u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
3826
4203
  `));
3827
4204
  const results = [];
@@ -3833,10 +4210,10 @@ async function harness() {
3833
4210
  const duration = Date.now() - start2;
3834
4211
  const sec = (duration / 1e3).toFixed(1);
3835
4212
  if (result.ok) {
3836
- spinner.succeed(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4213
+ spinner.succeed(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
3837
4214
  results.push({ name: check2.name, command: display, passed: true, duration });
3838
4215
  } else {
3839
- spinner.fail(`${check2.name} ${chalk19.gray(`(${sec}s)`)}`);
4216
+ spinner.fail(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
3840
4217
  results.push({
3841
4218
  name: check2.name,
3842
4219
  command: display,
@@ -3846,24 +4223,25 @@ async function harness() {
3846
4223
  });
3847
4224
  }
3848
4225
  }
3849
- console.log(chalk19.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
3850
- console.log(chalk19.gray("\u2500".repeat(40)));
4226
+ console.log(chalk21.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
4227
+ console.log(chalk21.gray("\u2500".repeat(40)));
3851
4228
  for (const r of results) {
3852
- const icon = r.passed ? chalk19.green("\u2705") : chalk19.red("\u274C");
4229
+ const icon = r.passed ? chalk21.green("\u2705") : chalk21.red("\u274C");
3853
4230
  const sec = (r.duration / 1e3).toFixed(1);
3854
- console.log(` ${icon} ${r.name.padEnd(15)} ${chalk19.gray(`${sec}s`)}`);
4231
+ console.log(` ${icon} ${r.name.padEnd(15)} ${chalk21.gray(`${sec}s`)}`);
3855
4232
  }
3856
4233
  const passed = results.filter((r) => r.passed).length;
3857
4234
  const all = passed === results.length;
3858
- console.log(chalk19.gray("\u2500".repeat(40)));
4235
+ console.log(chalk21.gray("\u2500".repeat(40)));
3859
4236
  if (all) {
3860
- console.log(chalk19.green.bold(`
4237
+ console.log(chalk21.green.bold(`
3861
4238
  \u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
3862
4239
  } else {
3863
4240
  console.log(
3864
- chalk19.red.bold(`
4241
+ chalk21.red.bold(`
3865
4242
  \u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
3866
4243
  );
4244
+ process.exitCode = 1;
3867
4245
  }
3868
4246
  printNextStep({
3869
4247
  message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
@@ -3873,9 +4251,9 @@ async function harness() {
3873
4251
  }
3874
4252
 
3875
4253
  // src/commands/migrate.ts
3876
- import { existsSync as existsSync8, unlinkSync, rmSync } from "fs";
3877
- import chalk20 from "chalk";
3878
- import inquirer9 from "inquirer";
4254
+ import { existsSync as existsSync9, unlinkSync, rmSync as rmSync2 } from "fs";
4255
+ import chalk22 from "chalk";
4256
+ import inquirer10 from "inquirer";
3879
4257
  import ora3 from "ora";
3880
4258
  var LOCK_FILES = {
3881
4259
  npm: "package-lock.json",
@@ -3883,26 +4261,26 @@ var LOCK_FILES = {
3883
4261
  pnpm: "pnpm-lock.yaml"
3884
4262
  };
3885
4263
  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";
4264
+ if (existsSync9("pnpm-lock.yaml")) return "pnpm";
4265
+ if (existsSync9("yarn.lock")) return "yarn";
4266
+ if (existsSync9("package-lock.json")) return "npm";
3889
4267
  return null;
3890
4268
  }
3891
4269
  function isCLIAvailable(pm) {
3892
4270
  return safeExecFile(pm, ["--version"]).ok;
3893
4271
  }
3894
4272
  async function migrate(target) {
3895
- console.log(chalk20.bold("\n\u{1F504} " + t("migrate.title")));
3896
- console.log(chalk20.gray("\u2500".repeat(40)));
4273
+ console.log(chalk22.bold("\n\u{1F504} " + t("migrate.title")));
4274
+ console.log(chalk22.gray("\u2500".repeat(40)));
3897
4275
  const current = detectCurrentPM();
3898
- console.log(chalk20.cyan(`
4276
+ console.log(chalk22.cyan(`
3899
4277
  \uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
3900
4278
  let targetPM;
3901
4279
  if (target && ["npm", "yarn", "pnpm"].includes(target)) {
3902
4280
  targetPM = target;
3903
4281
  } else {
3904
4282
  const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
3905
- const { selected } = await inquirer9.prompt([
4283
+ const { selected } = await inquirer10.prompt([
3906
4284
  {
3907
4285
  type: "list",
3908
4286
  name: "selected",
@@ -3913,17 +4291,17 @@ async function migrate(target) {
3913
4291
  targetPM = selected;
3914
4292
  }
3915
4293
  if (targetPM === current) {
3916
- console.log(chalk20.yellow(`
4294
+ console.log(chalk22.yellow(`
3917
4295
  \u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
3918
4296
  return;
3919
4297
  }
3920
4298
  if (!isCLIAvailable(targetPM)) {
3921
- console.log(chalk20.red(`
4299
+ console.log(chalk22.red(`
3922
4300
  \u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
3923
- console.log(chalk20.yellow(` npm i -g ${targetPM}`));
4301
+ console.log(chalk22.yellow(` npm i -g ${targetPM}`));
3924
4302
  return;
3925
4303
  }
3926
- const { confirm } = await inquirer9.prompt([
4304
+ const { confirm } = await inquirer10.prompt([
3927
4305
  {
3928
4306
  type: "confirm",
3929
4307
  name: "confirm",
@@ -3932,18 +4310,18 @@ async function migrate(target) {
3932
4310
  }
3933
4311
  ]);
3934
4312
  if (!confirm) {
3935
- console.log(chalk20.gray("\uCDE8\uC18C\uB428"));
4313
+ console.log(chalk22.gray("\uCDE8\uC18C\uB428"));
3936
4314
  return;
3937
4315
  }
3938
4316
  const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
3939
4317
  for (const lockFile of Object.values(LOCK_FILES)) {
3940
- if (existsSync8(lockFile)) {
4318
+ if (existsSync9(lockFile)) {
3941
4319
  unlinkSync(lockFile);
3942
4320
  }
3943
4321
  }
3944
- if (existsSync8("node_modules")) {
4322
+ if (existsSync9("node_modules")) {
3945
4323
  cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
3946
- rmSync("node_modules", { recursive: true, force: true });
4324
+ rmSync2("node_modules", { recursive: true, force: true });
3947
4325
  }
3948
4326
  cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
3949
4327
  const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
@@ -3952,10 +4330,10 @@ async function migrate(target) {
3952
4330
  install.succeed(`${targetPM} install \uC644\uB8CC!`);
3953
4331
  } else {
3954
4332
  install.fail(`${targetPM} install \uC2E4\uD328`);
3955
- console.log(chalk20.red(installResult.err.slice(0, 300)));
4333
+ console.log(chalk22.red(installResult.err.slice(0, 300)));
3956
4334
  return;
3957
4335
  }
3958
- console.log(chalk20.green.bold(`
4336
+ console.log(chalk22.green.bold(`
3959
4337
  \u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
3960
4338
  printNextStep({
3961
4339
  message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
@@ -3965,17 +4343,17 @@ async function migrate(target) {
3965
4343
  }
3966
4344
 
3967
4345
  // src/commands/update.ts
3968
- import { existsSync as existsSync9 } from "fs";
3969
- import { dirname as dirname2, join as join4 } from "path";
4346
+ import { existsSync as existsSync10 } from "fs";
4347
+ import { dirname as dirname2, join as join6 } from "path";
3970
4348
  import { fileURLToPath as fileURLToPath3 } from "url";
3971
- import chalk21 from "chalk";
4349
+ import chalk23 from "chalk";
3972
4350
  import ora4 from "ora";
3973
4351
  var PACKAGE = "@byh3071/vhk";
3974
4352
  function getCurrentVersion() {
3975
4353
  const dir = dirname2(fileURLToPath3(import.meta.url));
3976
- for (const pkgPath of [join4(dir, "../package.json"), join4(dir, "../../package.json")]) {
4354
+ for (const pkgPath of [join6(dir, "../package.json"), join6(dir, "../../package.json")]) {
3977
4355
  try {
3978
- if (existsSync9(pkgPath)) {
4356
+ if (existsSync10(pkgPath)) {
3979
4357
  const pkg = readJsonFile(pkgPath);
3980
4358
  if (pkg.version) return pkg.version;
3981
4359
  }
@@ -3998,32 +4376,32 @@ function isUpToDate(current, latest) {
3998
4376
  return cc >= lc;
3999
4377
  }
4000
4378
  async function update() {
4001
- console.log(chalk21.bold("\n\u2B06\uFE0F " + t("update.title")));
4002
- console.log(chalk21.gray("\u2500".repeat(40)));
4379
+ console.log(chalk23.bold("\n\u2B06\uFE0F " + t("update.title")));
4380
+ console.log(chalk23.gray("\u2500".repeat(40)));
4003
4381
  const current = getCurrentVersion();
4004
- console.log(chalk21.cyan(`
4382
+ console.log(chalk23.cyan(`
4005
4383
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
4006
4384
  const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
4007
4385
  const latest = getLatestVersion();
4008
4386
  if (!latest) {
4009
4387
  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}`));
4388
+ console.log(chalk23.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4389
+ console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
4012
4390
  return;
4013
4391
  }
4014
4392
  spinner.stop();
4015
- console.log(chalk21.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
4393
+ console.log(chalk23.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
4016
4394
  if (isUpToDate(current, latest)) {
4017
- console.log(chalk21.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
4395
+ console.log(chalk23.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
4018
4396
  return;
4019
4397
  }
4020
4398
  const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
4021
4399
  const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
4022
4400
  if (upd.ok) {
4023
4401
  updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
4024
- console.log(chalk21.green.bold(`
4402
+ console.log(chalk23.green.bold(`
4025
4403
  \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."));
4404
+ console.log(chalk23.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
4027
4405
  printNextStep({
4028
4406
  message: t("update.nextOkMessage"),
4029
4407
  command: "vhk --version",
@@ -4031,9 +4409,9 @@ async function update() {
4031
4409
  });
4032
4410
  } else {
4033
4411
  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}`));
4412
+ console.log(chalk23.red(upd.err.slice(0, 300)));
4413
+ console.log(chalk23.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
4414
+ console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
4037
4415
  printNextStep({
4038
4416
  message: t("update.nextFailMessage"),
4039
4417
  command: "vhk doctor",
@@ -4051,115 +4429,9 @@ import {
4051
4429
  statSync as statSync2,
4052
4430
  writeFileSync as writeFileSync7
4053
4431
  } 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
4162
- var CONTEXT_PATH2 = ".vhk/context.md";
4432
+ import { join as join7 } from "path";
4433
+ import chalk24 from "chalk";
4434
+ var CONTEXT_PATH = ".vhk/context.md";
4163
4435
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
4164
4436
  "node_modules",
4165
4437
  ".git",
@@ -4183,7 +4455,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
4183
4455
  filtered.forEach((entry, index) => {
4184
4456
  const isLast = index === filtered.length - 1;
4185
4457
  const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
4186
- const fullPath = join6(dir, entry);
4458
+ const fullPath = join7(dir, entry);
4187
4459
  const stat = statSync2(fullPath);
4188
4460
  const isDir = stat.isDirectory();
4189
4461
  lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
@@ -4257,11 +4529,12 @@ function getVhkCommands() {
4257
4529
  "mcp-init \u2014 Cursor MCP \uC124\uC815"
4258
4530
  ];
4259
4531
  }
4260
- async function context() {
4261
- console.log(chalk22.bold("\n\u{1F9E0} " + t("context.title")));
4262
- console.log(chalk22.gray("\u2500".repeat(40)));
4532
+ async function context(opts = {}) {
4533
+ const compact = opts.compact === true;
4534
+ console.log(chalk24.bold("\n\u{1F9E0} " + t("context.title")));
4535
+ console.log(chalk24.gray("\u2500".repeat(40)));
4263
4536
  const stack = extractTechStack();
4264
- const tree = buildTree(".").join("\n");
4537
+ const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
4265
4538
  const commands = getVhkCommands();
4266
4539
  const lines = [];
4267
4540
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
@@ -4277,25 +4550,37 @@ async function context() {
4277
4550
  lines.push("");
4278
4551
  lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
4279
4552
  lines.push("");
4280
- lines.push("```");
4553
+ lines.push("```text");
4281
4554
  lines.push(tree);
4282
4555
  lines.push("```");
4283
4556
  lines.push("");
4284
- lines.push("## VHK CLI \uBA85\uB839\uC5B4");
4285
- lines.push("");
4286
- for (const cmd of commands) {
4287
- lines.push(`- \`vhk ${cmd}\``);
4557
+ if (compact) {
4558
+ lines.push("## \uBA85\uB839\uC5B4 (\uC694\uC57D)");
4559
+ lines.push("");
4560
+ lines.push("- \uC804\uCCB4 \uBAA9\uB85D\uC740 `vhk help` \uB610\uB294 `COMMANDS.md` \uCC38\uC870 (compact \uBAA8\uB4DC\uB294 \uC0DD\uB7B5)");
4561
+ lines.push("");
4562
+ } else {
4563
+ lines.push("## VHK CLI \uBA85\uB839\uC5B4");
4564
+ lines.push("");
4565
+ for (const cmd of commands) {
4566
+ lines.push(`- \`vhk ${cmd}\``);
4567
+ }
4568
+ lines.push("");
4288
4569
  }
4289
- lines.push("");
4290
4570
  if (existsSync11(".vhk/memory.json")) {
4291
4571
  try {
4292
4572
  const memories = readJsonFile(
4293
4573
  ".vhk/memory.json"
4294
4574
  );
4295
4575
  if (Array.isArray(memories) && memories.length > 0) {
4576
+ const recentMemories = memories.slice(-5);
4296
4577
  lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
4297
4578
  lines.push("");
4298
- for (const m of memories) {
4579
+ if (memories.length > recentMemories.length) {
4580
+ lines.push(`_\uCD5C\uADFC ${recentMemories.length}\uAC1C\uB9CC \uD45C\uC2DC (\uC804\uCCB4 ${memories.length}\uAC1C)_`);
4581
+ lines.push("");
4582
+ }
4583
+ for (const m of recentMemories) {
4299
4584
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4300
4585
  lines.push(`- ${m.content} _(${date})_`);
4301
4586
  }
@@ -4326,6 +4611,24 @@ async function context() {
4326
4611
  for (const r of recent) lines.push(r);
4327
4612
  lines.push("");
4328
4613
  }
4614
+ const activeBlockers = getActiveBlockers(3);
4615
+ if (activeBlockers.length > 0) {
4616
+ lines.push("## Active Blockers");
4617
+ lines.push("");
4618
+ for (const b of activeBlockers) lines.push(b);
4619
+ lines.push("");
4620
+ }
4621
+ if (compact) {
4622
+ lines.push("## \uCC38\uC870 \uBB38\uC11C (\uD544\uC694\uC2DC \uC5F4\uB78C)");
4623
+ lines.push("");
4624
+ lines.push("- \uC791\uB3D9 \uADDC\uC57D(\uC694\uC57D): `docs/context/agent-compact.md`");
4625
+ lines.push("- \uADDC\uC57D \uC0C1\uC138: `AGENTS.md`");
4626
+ lines.push("- \uAE30\uB85D \uADDC\uCE59: `CLAUDE.md`");
4627
+ lines.push("- \uBA85\uB839 \uC0C1\uC138: `COMMANDS.md`");
4628
+ lines.push("- \uAD6C\uC870 \uC0C1\uC138: `docs/ARCHITECTURE.md`");
4629
+ lines.push("- \uD604\uC7AC \uC0C1\uD0DC: `docs/state/next-task.md`");
4630
+ lines.push("");
4631
+ }
4329
4632
  if (isHardStopActive()) {
4330
4633
  lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
4331
4634
  lines.push("");
@@ -4343,11 +4646,11 @@ async function context() {
4343
4646
  }
4344
4647
  lines.push("");
4345
4648
  mkdirSync7(".vhk", { recursive: true });
4346
- writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
4347
- console.log(chalk22.green(`
4348
- \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."));
4649
+ writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
4650
+ console.log(chalk24.green(`
4651
+ \u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
4652
+ console.log(chalk24.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
4653
+ console.log(chalk24.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
4351
4654
  printNextStep({
4352
4655
  message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
4353
4656
  command: "vhk context-show",
@@ -4355,20 +4658,20 @@ async function context() {
4355
4658
  });
4356
4659
  }
4357
4660
  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."));
4661
+ console.log(chalk24.bold("\n\u{1F4C4} " + t("context.showTitle")));
4662
+ console.log(chalk24.gray("\u2500".repeat(40)));
4663
+ if (!existsSync11(CONTEXT_PATH)) {
4664
+ console.log(chalk24.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4665
+ console.log(chalk24.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
4363
4666
  return;
4364
4667
  }
4365
- const content = readFileSync4(CONTEXT_PATH2, "utf-8");
4668
+ const content = readFileSync4(CONTEXT_PATH, "utf-8");
4366
4669
  console.log("\n" + content);
4367
4670
  }
4368
4671
 
4369
4672
  // src/commands/memory.ts
4370
4673
  import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
4371
- import chalk23 from "chalk";
4674
+ import chalk25 from "chalk";
4372
4675
  var MEMORY_PATH = ".vhk/memory.json";
4373
4676
  function loadMemories() {
4374
4677
  if (!existsSync12(MEMORY_PATH)) return [];
@@ -4384,11 +4687,12 @@ function saveMemories(memories) {
4384
4687
  writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
4385
4688
  }
4386
4689
  async function memoryAdd(content, tags) {
4387
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.addTitle")));
4388
- console.log(chalk23.gray("\u2500".repeat(40)));
4690
+ console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.addTitle")));
4691
+ console.log(chalk25.gray("\u2500".repeat(40)));
4389
4692
  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"'));
4693
+ console.log(chalk25.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
4694
+ console.log(chalk25.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
4695
+ process.exitCode = 1;
4392
4696
  return;
4393
4697
  }
4394
4698
  const memories = loadMemories();
@@ -4398,9 +4702,9 @@ async function memoryAdd(content, tags) {
4398
4702
  tags: tags && tags.length > 0 ? tags : []
4399
4703
  });
4400
4704
  saveMemories(memories);
4401
- console.log(chalk23.green(`
4705
+ console.log(chalk25.green(`
4402
4706
  \u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
4403
- console.log(chalk23.cyan(` \u{1F4DD} ${content}`));
4707
+ console.log(chalk25.cyan(` \u{1F4DD} ${content}`));
4404
4708
  printNextStep({
4405
4709
  message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
4406
4710
  command: "vhk memory list",
@@ -4408,24 +4712,24 @@ async function memoryAdd(content, tags) {
4408
4712
  });
4409
4713
  }
4410
4714
  async function memoryList() {
4411
- console.log(chalk23.bold("\n\u{1F9E0} " + t("memory.listTitle")));
4412
- console.log(chalk23.gray("\u2500".repeat(40)));
4715
+ console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.listTitle")));
4716
+ console.log(chalk25.gray("\u2500".repeat(40)));
4413
4717
  const memories = loadMemories();
4414
4718
  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.'));
4719
+ console.log(chalk25.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4720
+ console.log(chalk25.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
4417
4721
  return;
4418
4722
  }
4419
- console.log(chalk23.cyan(`
4723
+ console.log(chalk25.cyan(`
4420
4724
  \uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
4421
4725
  `));
4422
4726
  memories.forEach((m, index) => {
4423
4727
  const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
4424
- console.log(chalk23.white(` [${index + 1}] ${m.content}`));
4728
+ console.log(chalk25.white(` [${index + 1}] ${m.content}`));
4425
4729
  if (m.tags && m.tags.length > 0) {
4426
- console.log(chalk23.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
4730
+ console.log(chalk25.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
4427
4731
  }
4428
- console.log(chalk23.gray(` \u{1F4C5} ${date}`));
4732
+ console.log(chalk25.gray(` \u{1F4C5} ${date}`));
4429
4733
  console.log("");
4430
4734
  });
4431
4735
  }
@@ -4433,26 +4737,44 @@ async function memoryRemove(indexStr) {
4433
4737
  const memories = loadMemories();
4434
4738
  const idx = parseInt(indexStr, 10) - 1;
4435
4739
  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})`));
4740
+ console.log(chalk25.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
4437
4741
  return;
4438
4742
  }
4439
4743
  const removed = memories.splice(idx, 1)[0];
4440
4744
  saveMemories(memories);
4441
- console.log(chalk23.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
4442
- console.log(chalk23.gray(` ${removed.content}`));
4745
+ console.log(chalk25.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
4746
+ console.log(chalk25.gray(` ${removed.content}`));
4443
4747
  }
4444
4748
 
4445
4749
  // src/commands/brief.ts
4446
- import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
4447
- import chalk24 from "chalk";
4750
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync5 } from "fs";
4751
+ import chalk26 from "chalk";
4448
4752
  var BRIEF_PATH = ".vhk/brief.md";
4753
+ function readProjectIdentity() {
4754
+ const out = {};
4755
+ try {
4756
+ if (existsSync13("RULES.md")) {
4757
+ const r = readFileSync5("RULES.md", "utf-8");
4758
+ const m = r.split("\n")[0].match(/^#\s*(.+?)(?:\s*—.*)?$/);
4759
+ if (m) out.name = m[1].trim();
4760
+ const d = r.match(/한 줄 설명:\s*(.+)/);
4761
+ if (d) out.description = d[1].trim();
4762
+ }
4763
+ if (!out.name && existsSync13("CLAUDE.md")) {
4764
+ const m = readFileSync5("CLAUDE.md", "utf-8").match(/#\s*기록 규칙\s*\((.+?)\)/);
4765
+ if (m) out.name = m[1].trim();
4766
+ }
4767
+ } catch {
4768
+ }
4769
+ return out;
4770
+ }
4449
4771
  function git2(args) {
4450
4772
  const result = safeExecFile("git", args);
4451
4773
  return result.ok ? result.out : "";
4452
4774
  }
4453
4775
  async function brief() {
4454
- console.log(chalk24.bold("\n\u{1F4CB} " + t("brief.title")));
4455
- console.log(chalk24.gray("\u2500".repeat(40)));
4776
+ console.log(chalk26.bold("\n\u{1F4CB} " + t("brief.title")));
4777
+ console.log(chalk26.gray("\u2500".repeat(40)));
4456
4778
  const lines = [];
4457
4779
  lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
4458
4780
  lines.push("");
@@ -4460,11 +4782,12 @@ async function brief() {
4460
4782
  lines.push("");
4461
4783
  try {
4462
4784
  const pkg = readJsonFile("package.json");
4785
+ const id = readProjectIdentity();
4463
4786
  lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
4464
4787
  lines.push("");
4465
- lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
4788
+ lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
4466
4789
  lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
4467
- lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
4790
+ lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
4468
4791
  const deps = Object.keys(pkg.dependencies ?? {}).length;
4469
4792
  const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
4470
4793
  lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
@@ -4536,7 +4859,7 @@ async function brief() {
4536
4859
  mkdirSync9(".vhk", { recursive: true });
4537
4860
  writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
4538
4861
  console.log("\n" + lines.join("\n"));
4539
- console.log(chalk24.green(`
4862
+ console.log(chalk26.green(`
4540
4863
  \u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
4541
4864
  printNextStep({
4542
4865
  message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
@@ -4546,11 +4869,11 @@ async function brief() {
4546
4869
  }
4547
4870
 
4548
4871
  // src/commands/start.ts
4549
- import chalk25 from "chalk";
4550
- import inquirer10 from "inquirer";
4872
+ import chalk27 from "chalk";
4873
+ import inquirer11 from "inquirer";
4551
4874
  import { simpleGit as simpleGit2 } from "simple-git";
4552
4875
  import { existsSync as existsSync14 } from "fs";
4553
- import { join as join7 } from "path";
4876
+ import { join as join8 } from "path";
4554
4877
  var VHK_FOOTPRINT_FILES = [
4555
4878
  "CLAUDE.md",
4556
4879
  ".cursorrules",
@@ -4559,7 +4882,7 @@ var VHK_FOOTPRINT_FILES = [
4559
4882
  "docs/PRD.md"
4560
4883
  ];
4561
4884
  function detectExistingFootprint(cwd) {
4562
- return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join7(cwd, rel)));
4885
+ return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join8(cwd, rel)));
4563
4886
  }
4564
4887
  async function runGitInit(cwd) {
4565
4888
  try {
@@ -4588,22 +4911,22 @@ async function runStep(label, fn) {
4588
4911
  }
4589
4912
  }
4590
4913
  async function start(options = {}) {
4591
- console.log(chalk25.bold(`
4914
+ console.log(chalk27.bold(`
4592
4915
  ${ko.start.title}
4593
4916
  `));
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}`));
4917
+ console.log(chalk27.dim(ko.start.intro));
4918
+ console.log(chalk27.dim(` ${ko.start.step1}`));
4919
+ console.log(chalk27.dim(` ${ko.start.step2}`));
4920
+ console.log(chalk27.dim(` ${ko.start.step3}`));
4921
+ console.log(chalk27.dim(` ${ko.start.step4}`));
4599
4922
  console.log();
4600
4923
  const cwd = process.cwd();
4601
4924
  const footprint = detectExistingFootprint(cwd);
4602
4925
  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([{
4926
+ console.log(chalk27.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
4927
+ for (const f of footprint) console.log(chalk27.dim(` - ${f}`));
4928
+ console.log(chalk27.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."));
4929
+ const { proceedExisting } = await inquirer11.prompt([{
4607
4930
  type: "confirm",
4608
4931
  name: "proceedExisting",
4609
4932
  message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
@@ -4614,7 +4937,7 @@ ${ko.start.title}
4614
4937
  return;
4615
4938
  }
4616
4939
  } else if (!options.yes) {
4617
- const { proceed } = await inquirer10.prompt([{
4940
+ const { proceed } = await inquirer11.prompt([{
4618
4941
  type: "confirm",
4619
4942
  name: "proceed",
4620
4943
  message: ko.start.confirmStart,
@@ -4640,7 +4963,7 @@ ${ko.start.title}
4640
4963
  await runStep("[3/4] vhk mcp-init", () => mcpInit());
4641
4964
  log.step(ko.start.step4Header);
4642
4965
  await runStep("[4/4] vhk context", () => context());
4643
- console.log(chalk25.bold.green(`
4966
+ console.log(chalk27.bold.green(`
4644
4967
  ${ko.start.allDone}
4645
4968
  `));
4646
4969
  printNextStep({
@@ -4650,15 +4973,15 @@ ${ko.start.allDone}
4650
4973
  }
4651
4974
 
4652
4975
  // src/commands/cloud.ts
4653
- import fs14 from "fs";
4976
+ import fs12 from "fs";
4654
4977
  import os from "os";
4655
- import path15 from "path";
4656
- import chalk26 from "chalk";
4978
+ import path13 from "path";
4979
+ import chalk28 from "chalk";
4657
4980
 
4658
4981
  // src/lib/vhk-cloud.ts
4659
4982
  var import_ignore = __toESM(require_ignore(), 1);
4660
- import fs13 from "fs";
4661
- import path14 from "path";
4983
+ import fs11 from "fs";
4984
+ import path12 from "path";
4662
4985
  var DEFAULT_CLOUD_EXCLUDES = [
4663
4986
  "memory.json",
4664
4987
  // 개인 의사결정 메모
@@ -4676,17 +4999,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
4676
4999
  function loadVhkignore(rootDir) {
4677
5000
  const ig = (0, import_ignore.default)();
4678
5001
  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"));
5002
+ const ignorePath = path12.join(rootDir, ".vhkignore");
5003
+ if (fs11.existsSync(ignorePath)) {
5004
+ ig.add(fs11.readFileSync(ignorePath, "utf-8"));
4682
5005
  }
4683
5006
  return ig;
4684
5007
  }
4685
5008
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
4686
- const vhkDir = path14.join(rootDir, VHK_DIR2);
5009
+ const vhkDir = path12.join(rootDir, VHK_DIR2);
4687
5010
  let entries;
4688
5011
  try {
4689
- entries = fs13.readdirSync(vhkDir, { withFileTypes: true });
5012
+ entries = fs11.readdirSync(vhkDir, { withFileTypes: true });
4690
5013
  } catch {
4691
5014
  return [];
4692
5015
  }
@@ -4702,10 +5025,10 @@ function partitionGistFiles(gistFiles, ig) {
4702
5025
  return { keep, excluded };
4703
5026
  }
4704
5027
  function readCloudConfig(rootDir) {
4705
- const p = path14.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4706
- if (!fs13.existsSync(p)) return null;
5028
+ const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5029
+ if (!fs11.existsSync(p)) return null;
4707
5030
  try {
4708
- const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
5031
+ const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
4709
5032
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
4710
5033
  return { gistId: parsed.gistId };
4711
5034
  }
@@ -4715,24 +5038,44 @@ function readCloudConfig(rootDir) {
4715
5038
  }
4716
5039
  }
4717
5040
  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");
5041
+ const vhkDir = path12.join(rootDir, VHK_DIR2);
5042
+ fs11.mkdirSync(vhkDir, { recursive: true });
5043
+ const p = path12.join(vhkDir, CLOUD_CONFIG_FILE);
5044
+ fs11.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
5045
+ ensureCloudConfigIgnored(vhkDir);
5046
+ }
5047
+ function ensureCloudConfigIgnored(vhkDir) {
5048
+ const giPath = path12.join(vhkDir, ".gitignore");
5049
+ let content = "";
5050
+ try {
5051
+ if (fs11.existsSync(giPath)) content = fs11.readFileSync(giPath, "utf-8");
5052
+ } catch {
5053
+ return;
5054
+ }
5055
+ const already = content.split(/\r?\n/).some((l) => l.trim() === CLOUD_CONFIG_FILE);
5056
+ if (already) return;
5057
+ const block = `# secret gist \uD3EC\uC778\uD130 \u2014 \uCD94\uC801 \uAE08\uC9C0 (VHK-022)
5058
+ ${CLOUD_CONFIG_FILE}
5059
+ `;
5060
+ const base = content.length === 0 ? "" : content.endsWith("\n") ? content : content + "\n";
5061
+ try {
5062
+ fs11.writeFileSync(giPath, base + block, "utf-8");
5063
+ } catch {
5064
+ }
4722
5065
  }
4723
5066
 
4724
5067
  // src/commands/cloud.ts
4725
5068
  function ensureGhReady() {
4726
5069
  const ver = safeExecFile("gh", ["--version"]);
4727
5070
  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`)"));
5071
+ console.log(chalk28.red(` ${ko.cloud.noGh}`));
5072
+ console.log(chalk28.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
4730
5073
  return false;
4731
5074
  }
4732
5075
  const auth = safeExecFile("gh", ["auth", "status"]);
4733
5076
  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)"));
5077
+ console.log(chalk28.red(` ${ko.cloud.noAuth}`));
5078
+ console.log(chalk28.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
4736
5079
  return false;
4737
5080
  }
4738
5081
  return true;
@@ -4745,29 +5088,29 @@ function parseGistId(output) {
4745
5088
  return null;
4746
5089
  }
4747
5090
  async function cloudPush() {
4748
- console.log(chalk26.bold(`
5091
+ console.log(chalk28.bold(`
4749
5092
  ${ko.cloud.pushTitle}
4750
5093
  `));
4751
5094
  const cwd = process.cwd();
4752
- if (!fs14.existsSync(path15.join(cwd, VHK_DIR2))) {
4753
- console.log(chalk26.yellow(` ${ko.cloud.noVhkDir}`));
5095
+ if (!fs12.existsSync(path13.join(cwd, VHK_DIR2))) {
5096
+ console.log(chalk28.yellow(` ${ko.cloud.noVhkDir}`));
4754
5097
  return;
4755
5098
  }
4756
5099
  const ig = loadVhkignore(cwd);
4757
5100
  const files = collectVhkFiles(cwd, ig);
4758
5101
  if (files.length === 0) {
4759
- console.log(chalk26.yellow(` ${ko.cloud.nothingToSync}`));
5102
+ console.log(chalk28.yellow(` ${ko.cloud.nothingToSync}`));
4760
5103
  return;
4761
5104
  }
4762
5105
  if (!ensureGhReady()) {
4763
5106
  process.exitCode = 1;
4764
5107
  return;
4765
5108
  }
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(", ")}
5109
+ const filePaths = files.map((f) => path13.join(cwd, VHK_DIR2, f));
5110
+ console.log(chalk28.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
4768
5111
  `));
4769
5112
  const existing = readCloudConfig(cwd);
4770
- const desc = `vhk .vhk backup \u2014 ${path15.basename(cwd)}`;
5113
+ const desc = `vhk .vhk backup \u2014 ${path13.basename(cwd)}`;
4771
5114
  if (existing) {
4772
5115
  const gistFiles = listGistFiles(existing.gistId);
4773
5116
  for (let i = 0; i < files.length; i++) {
@@ -4776,8 +5119,8 @@ ${ko.cloud.pushTitle}
4776
5119
  const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
4777
5120
  const res2 = safeExecFile("gh", args);
4778
5121
  if (!res2.ok) {
4779
- console.log(chalk26.red(` ${ko.cloud.pushFail}: ${name}`));
4780
- console.log(chalk26.dim(` ${res2.err}`));
5122
+ console.log(chalk28.red(` ${ko.cloud.pushFail}: ${name}`));
5123
+ console.log(chalk28.dim(` ${res2.err}`));
4781
5124
  process.exitCode = 1;
4782
5125
  return;
4783
5126
  }
@@ -4792,15 +5135,15 @@ ${ko.cloud.pushTitle}
4792
5135
  if (!purgeFailed.includes(name)) purgeFailed.push(name);
4793
5136
  }
4794
5137
  }
4795
- console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4796
- console.log(chalk26.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
5138
+ console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
5139
+ console.log(chalk28.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
4797
5140
  if (excluded.length > 0) {
4798
5141
  const purged = excluded.filter((n) => !purgeFailed.includes(n));
4799
5142
  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(", ")}`));
5143
+ console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
4801
5144
  }
4802
5145
  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)`));
5146
+ console.log(chalk28.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
5147
  }
4805
5148
  }
4806
5149
  printPushNext();
@@ -4808,32 +5151,32 @@ ${ko.cloud.pushTitle}
4808
5151
  }
4809
5152
  const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
4810
5153
  if (!res.ok) {
4811
- console.log(chalk26.red(` ${ko.cloud.pushFail}`));
4812
- console.log(chalk26.dim(` ${res.err || res.out}`));
5154
+ console.log(chalk28.red(` ${ko.cloud.pushFail}`));
5155
+ console.log(chalk28.dim(` ${res.err || res.out}`));
4813
5156
  process.exitCode = 1;
4814
5157
  return;
4815
5158
  }
4816
5159
  const gistId = parseGistId(res.out);
4817
5160
  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}`));
5161
+ console.log(chalk28.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
5162
+ console.log(chalk28.dim(` \uCD9C\uB825: ${res.out}`));
4820
5163
  process.exitCode = 1;
4821
5164
  return;
4822
5165
  }
4823
5166
  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`));
5167
+ console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
5168
+ console.log(chalk28.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
4826
5169
  printPushNext();
4827
5170
  }
4828
5171
  async function cloudPull(gistIdArg) {
4829
- console.log(chalk26.bold(`
5172
+ console.log(chalk28.bold(`
4830
5173
  ${ko.cloud.pullTitle}
4831
5174
  `));
4832
5175
  const cwd = process.cwd();
4833
5176
  const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
4834
5177
  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)"));
5178
+ console.log(chalk28.yellow(` ${ko.cloud.noGistId}`));
5179
+ console.log(chalk28.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
4837
5180
  return;
4838
5181
  }
4839
5182
  if (!ensureGhReady()) {
@@ -4842,34 +5185,34 @@ ${ko.cloud.pullTitle}
4842
5185
  }
4843
5186
  const allNames = listGistFiles(gistId);
4844
5187
  if (allNames.length === 0) {
4845
- console.log(chalk26.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
5188
+ console.log(chalk28.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
4846
5189
  process.exitCode = 1;
4847
5190
  return;
4848
5191
  }
4849
5192
  const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
4850
5193
  if (skipped.length > 0) {
4851
- console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
5194
+ console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
4852
5195
  }
4853
5196
  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).`));
5197
+ console.log(chalk28.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
4855
5198
  return;
4856
5199
  }
4857
- const vhkDir = path15.join(cwd, VHK_DIR2);
4858
- fs14.mkdirSync(vhkDir, { recursive: true });
5200
+ const vhkDir = path13.join(cwd, VHK_DIR2);
5201
+ fs12.mkdirSync(vhkDir, { recursive: true });
4859
5202
  let restored = 0;
4860
5203
  for (const name of names) {
4861
5204
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
4862
5205
  if (!res.ok) {
4863
- console.log(chalk26.red(` ${ko.cloud.pullFail}: ${name}`));
4864
- console.log(chalk26.dim(` ${res.err}`));
5206
+ console.log(chalk28.red(` ${ko.cloud.pullFail}: ${name}`));
5207
+ console.log(chalk28.dim(` ${res.err}`));
4865
5208
  continue;
4866
5209
  }
4867
- fs14.writeFileSync(path15.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
5210
+ fs12.writeFileSync(path13.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4868
5211
  restored++;
4869
5212
  }
4870
5213
  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})`));
5214
+ console.log(chalk28.green.bold(` ${ko.cloud.pullDone}`));
5215
+ console.log(chalk28.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
4873
5216
  printNextStep({
4874
5217
  message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
4875
5218
  command: "vhk \uB9E5\uB77D",
@@ -4881,9 +5224,9 @@ function purgeExcludedFromGist(gistId, names) {
4881
5224
  const body = JSON.stringify({
4882
5225
  files: Object.fromEntries(names.map((n) => [n, null]))
4883
5226
  });
4884
- const tmp = path15.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
5227
+ const tmp = path13.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
4885
5228
  try {
4886
- fs14.writeFileSync(tmp, body, "utf-8");
5229
+ fs12.writeFileSync(tmp, body, "utf-8");
4887
5230
  for (let attempt = 0; attempt < 2; attempt++) {
4888
5231
  const res = safeExecFile(
4889
5232
  "gh",
@@ -4895,7 +5238,7 @@ function purgeExcludedFromGist(gistId, names) {
4895
5238
  return false;
4896
5239
  } finally {
4897
5240
  try {
4898
- fs14.unlinkSync(tmp);
5241
+ fs12.unlinkSync(tmp);
4899
5242
  } catch {
4900
5243
  }
4901
5244
  }
@@ -4916,6 +5259,198 @@ function printPushNext() {
4916
5259
  });
4917
5260
  }
4918
5261
 
5262
+ // src/commands/help.ts
5263
+ import chalk29 from "chalk";
5264
+ var QUICK_ACTIONS = [
5265
+ { say: "\uC0C1\uD0DC \uC54C\uB824\uC918", does: "vhk status" },
5266
+ { say: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?", does: "vhk diff" },
5267
+ { say: "\uC800\uC7A5\uD574\uC918", does: "vhk save" },
5268
+ { say: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574\uC918", does: "vhk recap" },
5269
+ { say: "\uB2E4\uC74C\uC5D0 \uBB50 \uD558\uBA74 \uB3FC?", does: "vhk goal next" },
5270
+ { say: "\uADDC\uCE59 \uB3D9\uAE30\uD654\uD574\uC918", does: "vhk sync" },
5271
+ { say: "\uBC31\uC5C5 \uBCF5\uC6D0\uD574\uC918", does: "vhk restore" },
5272
+ { say: "\uBCF4\uC548 \uC810\uAC80\uD574\uC918", does: "vhk secure scan" },
5273
+ { say: "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791", does: "vhk start" },
5274
+ { say: "\uC804\uCCB4 \uBA85\uB839\uC5B4 \uBCF4\uAE30", does: "vhk --help" }
5275
+ ];
5276
+ function quickActions() {
5277
+ console.log(chalk29.bold("\n\u{1F9ED} VHK \u2014 \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4 (quick actions)"));
5278
+ console.log(chalk29.gray("\u2500".repeat(40)));
5279
+ for (const a of QUICK_ACTIONS) {
5280
+ console.log(` "${chalk29.cyan(a.say)}" \u2192 ${chalk29.dim(a.does)}`);
5281
+ }
5282
+ console.log(chalk29.gray("\n \uC804\uCCB4 \uBA85\uB839\uC740 `vhk --help` \uB610\uB294 COMMANDS.md \uB97C \uBCF4\uC138\uC694."));
5283
+ console.log("");
5284
+ }
5285
+
5286
+ // src/commands/mode.ts
5287
+ import chalk30 from "chalk";
5288
+
5289
+ // src/lib/config.ts
5290
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
5291
+ import { join as join9 } from "path";
5292
+
5293
+ // src/lib/safety-mode.ts
5294
+ var SAFETY_MODES = ["lite", "standard", "strict"];
5295
+ var DEFAULT_SAFETY_MODE = "standard";
5296
+ var SAFETY_MODE_DESC = {
5297
+ lite: "\uACBD\uACE0\uB9CC \u2014 \uC704\uD5D8 \uC791\uC5C5\uB3C4 \uB9C9\uC9C0 \uC54A\uACE0 \uACBD\uACE0\uB9CC \uD45C\uC2DC (\uBE60\uB978 \uBC18\uBCF5\uC6A9)",
5298
+ standard: "\uAE30\uBCF8 \u2014 \uC704\uD5D8 \uC791\uC5C5\uC740 CLI \uD655\uC778\xB7MCP/\uC790\uC5F0\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30",
5299
+ strict: "\uC5C4\uACA9 \u2014 \uB354 \uB9CE\uC740 \uC791\uC5C5(\uC800\uC7A5/\uB3D9\uAE30\uD654 \uB4F1)\uC5D0\uB3C4 \uD655\uC778 \uC694\uAD6C"
5300
+ };
5301
+ function isSafetyMode(value) {
5302
+ return typeof value === "string" && SAFETY_MODES.includes(value);
5303
+ }
5304
+
5305
+ // src/lib/config.ts
5306
+ var CONFIG_DIR = ".vhk";
5307
+ var CONFIG_PATH = join9(CONFIG_DIR, "config.json");
5308
+ var DEFAULT_CONFIG = { safetyMode: DEFAULT_SAFETY_MODE };
5309
+ function readConfig(rootDir = process.cwd()) {
5310
+ const full = join9(rootDir, CONFIG_PATH);
5311
+ if (!existsSync15(full)) return { ...DEFAULT_CONFIG };
5312
+ try {
5313
+ const raw = readJsonFile(full);
5314
+ return {
5315
+ safetyMode: isSafetyMode(raw.safetyMode) ? raw.safetyMode : DEFAULT_CONFIG.safetyMode
5316
+ };
5317
+ } catch {
5318
+ return { ...DEFAULT_CONFIG };
5319
+ }
5320
+ }
5321
+ function writeConfig(config, rootDir = process.cwd()) {
5322
+ mkdirSync10(join9(rootDir, CONFIG_DIR), { recursive: true });
5323
+ writeFileSync10(join9(rootDir, CONFIG_PATH), JSON.stringify(config, null, 2) + "\n", "utf-8");
5324
+ }
5325
+
5326
+ // src/commands/mode.ts
5327
+ async function mode(target) {
5328
+ console.log(chalk30.bold("\n\u{1F6E1}\uFE0F Safety Mode"));
5329
+ console.log(chalk30.gray("\u2500".repeat(40)));
5330
+ const current = readConfig().safetyMode;
5331
+ if (!target) {
5332
+ console.log(chalk30.cyan(`
5333
+ \uD604\uC7AC \uBAA8\uB4DC: ${chalk30.bold(current)}`));
5334
+ console.log(chalk30.dim(` ${SAFETY_MODE_DESC[current]}`));
5335
+ console.log("");
5336
+ for (const m of SAFETY_MODES) {
5337
+ const mark = m === current ? "\u25CF" : "\u25CB";
5338
+ console.log(` ${mark} ${m.padEnd(9)} ${chalk30.dim(SAFETY_MODE_DESC[m])}`);
5339
+ }
5340
+ printNextStep({
5341
+ message: "\uBAA8\uB4DC\uB97C \uBC14\uAFB8\uB824\uBA74:",
5342
+ command: "vhk mode strict",
5343
+ cursorHint: "\uC548\uC804 \uBAA8\uB4DC strict\uB85C \uBC14\uAFD4\uC918"
5344
+ });
5345
+ return;
5346
+ }
5347
+ if (!isSafetyMode(target)) {
5348
+ console.log(chalk30.red(`
5349
+ \u274C \uC54C \uC218 \uC5C6\uB294 \uBAA8\uB4DC: ${target}`));
5350
+ console.log(chalk30.dim(` \uAC00\uB2A5: ${SAFETY_MODES.join(" | ")}`));
5351
+ process.exitCode = 1;
5352
+ return;
5353
+ }
5354
+ writeConfig({ ...readConfig(), safetyMode: target });
5355
+ console.log(chalk30.green(`
5356
+ \u2705 Safety Mode \u2192 ${chalk30.bold(target)}`));
5357
+ console.log(chalk30.dim(` ${SAFETY_MODE_DESC[target]}`));
5358
+ }
5359
+
5360
+ // src/commands/verify.ts
5361
+ import chalk31 from "chalk";
5362
+ function verificationChecklist() {
5363
+ return [
5364
+ "\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
5365
+ "\uD14C\uC2A4\uD2B8 \u2014 pnpm run test:run",
5366
+ "\uBE4C\uB4DC \u2014 pnpm run build",
5367
+ "\uBCF4\uC548 \uC2A4\uCE94 \u2014 vhk secure scan"
5368
+ ];
5369
+ }
5370
+ async function verify() {
5371
+ console.log(chalk31.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
5372
+ console.log(chalk31.gray("\u2500".repeat(40)));
5373
+ const mode2 = readConfig().safetyMode;
5374
+ console.log(chalk31.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
5375
+ console.log(chalk31.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
5376
+ for (const item of verificationChecklist()) {
5377
+ console.log(` \u2610 ${item}`);
5378
+ }
5379
+ console.log(chalk31.dim("\n \u203B \uBA54\uD0C0\uB7EC\uB108(\uC790\uB3D9 \uC2E4\uD589) \uC790\uB9AC \u2014 \uD604\uC7AC\uB294 \uBB36\uC74C \uC548\uB0B4\uB9CC(lite)."));
5380
+ printNextStep({
5381
+ message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
5382
+ command: "vhk save",
5383
+ cursorHint: "\uC800\uC7A5\uD574\uC918"
5384
+ });
5385
+ }
5386
+
5387
+ // src/lib/risk-policy.ts
5388
+ var HIGH_RISK_ACTIONS = [
5389
+ "undo",
5390
+ "deploy",
5391
+ "publish",
5392
+ "migrate",
5393
+ "cloud-pull",
5394
+ "resume",
5395
+ "env-write",
5396
+ "delete"
5397
+ ];
5398
+ var STRICT_EXTRA_ACTIONS = /* @__PURE__ */ new Set(["save", "sync"]);
5399
+ var NL_GUARDED_ACTIONS = {
5400
+ undo: "undo",
5401
+ deploy: "deploy",
5402
+ publish: "publish",
5403
+ migrate: "migrate",
5404
+ "cloud-pull": "cloud-pull",
5405
+ env: "env-write",
5406
+ save: "save",
5407
+ sync: "sync"
5408
+ };
5409
+ function isHighRisk(action) {
5410
+ return HIGH_RISK_ACTIONS.includes(action);
5411
+ }
5412
+ function resolveGuard(action, mode2, channel) {
5413
+ const guarded = isHighRisk(action) || mode2 === "strict" && STRICT_EXTRA_ACTIONS.has(action);
5414
+ if (!guarded) return "allow";
5415
+ if (mode2 === "lite") return "warn";
5416
+ return channel === "cli" ? "confirm" : "preview";
5417
+ }
5418
+
5419
+ // src/lib/safety-guard.ts
5420
+ async function runGuarded(action, deps, run) {
5421
+ const mode2 = deps.mode ?? readConfig().safetyMode;
5422
+ const log2 = deps.log ?? (() => {
5423
+ });
5424
+ const guard = resolveGuard(action, mode2, deps.channel);
5425
+ if (guard === "allow") {
5426
+ return { outcome: { ran: true, guard, reason: "low-risk" }, result: await run() };
5427
+ }
5428
+ if (guard === "warn") {
5429
+ log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uBAA8\uB4DC: \uACBD\uACE0\uB9CC \uD558\uACE0 \uC9C4\uD589\uD569\uB2C8\uB2E4.`);
5430
+ return { outcome: { ran: true, guard, reason: "lite-warn" }, result: await run() };
5431
+ }
5432
+ if (guard === "confirm") {
5433
+ if (deps.approved === true) {
5434
+ return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
5435
+ }
5436
+ const tty = deps.isTTY ?? !!process.stdout.isTTY;
5437
+ if (tty && deps.confirm) {
5438
+ const ok = await deps.confirm();
5439
+ if (ok) return { outcome: { ran: true, guard, reason: "confirmed" }, result: await run() };
5440
+ log2(`\uCDE8\uC18C\uB428 \u2014 ${action} \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
5441
+ return { outcome: { ran: false, guard, reason: "declined" } };
5442
+ }
5443
+ 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)`);
5444
+ return { outcome: { ran: false, guard, reason: "no-confirm" } };
5445
+ }
5446
+ 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})`);
5447
+ if (deps.approved === true) {
5448
+ return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
5449
+ }
5450
+ 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.`);
5451
+ return { outcome: { ran: false, guard, reason: "preview-no-approve" } };
5452
+ }
5453
+
4919
5454
  // src/lib/nlp-run.ts
4920
5455
  async function dispatchNlpRoute(route, input) {
4921
5456
  switch (route.command) {
@@ -4946,6 +5481,8 @@ async function dispatchNlpRoute(route, input) {
4946
5481
  return save();
4947
5482
  case "undo":
4948
5483
  return undo();
5484
+ case "restore":
5485
+ return restore(route.args?.[0]);
4949
5486
  case "status":
4950
5487
  return status();
4951
5488
  case "diff":
@@ -4993,113 +5530,174 @@ async function dispatchNlpRoute(route, input) {
4993
5530
  if (sub === "next") return goalNext();
4994
5531
  if (sub === "check") return goalCheck({});
4995
5532
  if (sub === "done") return goalDone({});
5533
+ if (sub === "sync") {
5534
+ await goalSync();
5535
+ return;
5536
+ }
4996
5537
  return goalList();
4997
5538
  }
5539
+ case "help":
5540
+ return quickActions();
5541
+ case "mode":
5542
+ return mode();
5543
+ case "verify":
5544
+ return verify();
4998
5545
  }
4999
5546
  }
5547
+ var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
5548
+ "start",
5549
+ "init"
5550
+ ]);
5551
+ function requiresConfirmation(route) {
5552
+ const goalSync2 = route.command === "goal" && route.args?.[0] === "sync";
5553
+ return route.confidence === "low" || STATE_CHANGING_COMMANDS.has(route.command) || goalSync2;
5554
+ }
5000
5555
  async function runNaturalLanguageRoute(input) {
5001
5556
  const route = routeNaturalLanguage(input);
5002
5557
  if (!route) {
5003
- console.log(chalk27.yellow(`
5558
+ console.log(chalk32.yellow(`
5004
5559
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
5005
5560
  `));
5006
5561
  return;
5007
5562
  }
5008
5563
  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([{
5564
+ console.log(chalk32.cyan(` \u{1F4AC} "${input}"`));
5565
+ console.log(chalk32.cyan(` \u2192 ${route.explanation}`));
5566
+ if (requiresConfirmation(route)) {
5567
+ const { confirm } = await inquirer12.prompt([{
5013
5568
  type: "confirm",
5014
5569
  name: "confirm",
5015
5570
  message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
5016
5571
  default: true
5017
5572
  }]);
5018
5573
  if (!confirm) {
5019
- console.log(chalk27.dim(` ${ko.nlp.menuHint}`));
5574
+ console.log(chalk32.dim(` ${ko.nlp.menuHint}`));
5020
5575
  return;
5021
5576
  }
5022
5577
  }
5023
5578
  console.log("");
5579
+ const riskAction = NL_GUARDED_ACTIONS[route.command];
5580
+ if (riskAction) {
5581
+ await runGuarded(
5582
+ riskAction,
5583
+ { channel: "nl", approved: false, log: (m) => console.log(chalk32.yellow(` ${m}`)) },
5584
+ () => dispatchNlpRoute(route, input)
5585
+ );
5586
+ return;
5587
+ }
5024
5588
  await dispatchNlpRoute(route, input);
5025
5589
  }
5026
5590
 
5027
5591
  // src/commands/agent.ts
5028
- import chalk28 from "chalk";
5592
+ import chalk33 from "chalk";
5029
5593
  function activeGoalId() {
5030
5594
  const goals = listGoals("goals");
5031
5595
  const id = selectActiveId(goals);
5032
5596
  return id ?? void 0;
5033
5597
  }
5034
5598
  async function blocker(description) {
5035
- console.log(chalk28.bold(`
5599
+ console.log(chalk33.bold(`
5036
5600
  ${ko.agent.blockerTitle}
5037
5601
  `));
5038
5602
  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"'));
5603
+ console.log(chalk33.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5604
+ console.log(chalk33.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
5041
5605
  process.exitCode = 1;
5042
5606
  return;
5043
5607
  }
5044
5608
  const goalId = activeGoalId();
5045
5609
  const r = appendBlocker(description, goalId);
5046
- console.log(chalk28.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
5610
+ console.log(chalk33.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
5047
5611
  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."));
5612
+ console.log(chalk33.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
5613
+ console.log(chalk33.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
5050
5614
  process.exitCode = 2;
5051
5615
  }
5052
5616
  }
5053
5617
  async function learn(lesson) {
5054
- console.log(chalk28.bold(`
5618
+ console.log(chalk33.bold(`
5055
5619
  ${ko.agent.learnTitle}
5056
5620
  `));
5057
5621
  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)"'));
5622
+ console.log(chalk33.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
5623
+ console.log(chalk33.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
5060
5624
  process.exitCode = 1;
5061
5625
  return;
5062
5626
  }
5063
5627
  const goalId = activeGoalId();
5064
5628
  appendLearning(lesson, goalId);
5065
- console.log(chalk28.green(" \u2705 learnings.md append."));
5629
+ console.log(chalk33.green(" \u2705 learnings.md append."));
5066
5630
  console.log(
5067
- chalk28.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
5631
+ chalk33.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
5068
5632
  );
5069
5633
  }
5070
5634
  async function resume(opts = {}) {
5071
- console.log(chalk28.bold(`
5635
+ console.log(chalk33.bold(`
5072
5636
  ${ko.agent.resumeTitle}
5073
5637
  `));
5074
5638
  if (!isHardStopActive()) {
5075
- console.log(chalk28.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
5639
+ console.log(chalk33.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
5076
5640
  return;
5077
5641
  }
5078
5642
  const reason = readHardStopReason();
5079
5643
  if (reason) {
5080
- console.log(chalk28.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5081
- console.log(chalk28.dim(` ${reason.split("\n").join("\n ")}`));
5644
+ console.log(chalk33.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
5645
+ console.log(chalk33.dim(` ${reason.split("\n").join("\n ")}`));
5082
5646
  console.log("");
5083
5647
  }
5084
5648
  if (!opts.confirm) {
5085
5649
  console.log(
5086
- chalk28.red(
5650
+ chalk33.red(
5087
5651
  " \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
5088
5652
  )
5089
5653
  );
5090
- console.log(chalk28.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
5654
+ console.log(chalk33.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
5091
5655
  process.exitCode = 1;
5092
5656
  return;
5093
5657
  }
5094
5658
  const removed = clearHardStop();
5095
5659
  if (removed) {
5096
- console.log(chalk28.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
5660
+ console.log(chalk33.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
5097
5661
  } else {
5098
- console.log(chalk28.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
5662
+ console.log(chalk33.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
5099
5663
  }
5100
5664
  }
5101
5665
 
5102
5666
  // src/index.ts
5667
+ async function guardCli(action, approved, run) {
5668
+ if (!ensureNotHardStopped(action)) return;
5669
+ await runGuarded(
5670
+ action,
5671
+ {
5672
+ channel: "cli",
5673
+ approved,
5674
+ confirm: async () => {
5675
+ const { ok } = await inquirer13.prompt([{
5676
+ type: "confirm",
5677
+ name: "ok",
5678
+ message: `\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action})\uC744 \uC2E4\uD589\uD560\uAE4C\uC694?`,
5679
+ default: false
5680
+ }]);
5681
+ return ok;
5682
+ },
5683
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
5684
+ },
5685
+ run
5686
+ );
5687
+ }
5688
+ async function guardCliDefer(action, approved, run) {
5689
+ await runGuarded(
5690
+ action,
5691
+ {
5692
+ channel: "cli",
5693
+ approved,
5694
+ // TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
5695
+ confirm: async () => !!process.stdout.isTTY,
5696
+ log: (m) => console.log(chalk34.yellow(` ${m}`))
5697
+ },
5698
+ run
5699
+ );
5700
+ }
5103
5701
  var program = new Command();
5104
5702
  var defaultHelp = new Help();
5105
5703
  var KO_ALIASES = {
@@ -5114,6 +5712,7 @@ var KO_ALIASES = {
5114
5712
  doctor: "\uD658\uACBD",
5115
5713
  save: "\uC800\uC7A5",
5116
5714
  undo: "\uB418\uB3CC\uB9AC\uAE30",
5715
+ restore: "\uBCF5\uC6D0",
5117
5716
  status: "\uC0C1\uD0DC",
5118
5717
  diff: "\uBCC0\uACBD",
5119
5718
  deploy: "\uBC30\uD3EC",
@@ -5165,7 +5764,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
5165
5764
  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
5765
  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
5766
  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);
5767
+ 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) => {
5768
+ await guardCli("sync", opts?.yes === true, () => sync(opts));
5769
+ });
5169
5770
  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
5771
  await check(opts);
5171
5772
  });
@@ -5177,16 +5778,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
5177
5778
  cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
5178
5779
  await cloudPush();
5179
5780
  });
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);
5781
+ 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) => {
5782
+ await guardCli("cloud-pull", opts?.yes === true, () => cloudPull(gistId));
5182
5783
  });
5183
5784
  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
5785
  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();
5786
+ 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) => {
5787
+ await guardCli("save", opts?.yes === true, () => save());
5187
5788
  });
5188
- program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
5189
- await undo();
5789
+ 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) => {
5790
+ await guardCliDefer("undo", opts?.yes === true, () => undo());
5791
+ });
5792
+ 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) => {
5793
+ await restore(id);
5190
5794
  });
5191
5795
  program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
5192
5796
  await status();
@@ -5198,17 +5802,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
5198
5802
  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
5803
  await mcpInit();
5200
5804
  });
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();
5805
+ 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) => {
5806
+ await guardCli("deploy", opts?.yes === true, () => deploy());
5203
5807
  });
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();
5808
+ 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) => {
5809
+ await guardCli("env-write", opts?.yes === true, () => env());
5206
5810
  });
5207
5811
  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
5812
  await envCheck();
5209
5813
  });
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();
5814
+ 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) => {
5815
+ await guardCli("publish", opts?.yes === true, () => publish());
5212
5816
  });
5213
5817
  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
5818
  await design();
@@ -5237,14 +5841,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
5237
5841
  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
5842
  await audit(opts.fix);
5239
5843
  });
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);
5844
+ 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) => {
5845
+ await guardCli("migrate", opts?.yes === true, () => migrate(target));
5242
5846
  });
5243
5847
  program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
5244
5848
  await update();
5245
5849
  });
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();
5850
+ 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) => {
5851
+ await context({ compact: opts.compact });
5852
+ });
5853
+ 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) => {
5854
+ await mode(target);
5855
+ });
5856
+ 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 () => {
5857
+ await verify();
5248
5858
  });
5249
5859
  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
5860
  await contextShow();
@@ -5265,7 +5875,7 @@ memoryCmd.command("remove <index>").alias("\uC0AD\uC81C").description("\uAE30\uC
5265
5875
  program.command("brief").alias("\uBE0C\uB9AC\uD551").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC694\uC57D \uBCF4\uACE0\uC11C \uC0DD\uC131 (.vhk/brief.md)").action(async () => {
5266
5876
  await brief();
5267
5877
  });
5268
- var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done)").action(async () => {
5878
+ var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done / sync)").action(async () => {
5269
5879
  await goalList();
5270
5880
  });
5271
5881
  goalCmd.command("list").alias("\uBAA9\uB85D").description("goals/*.md \uBAA9\uB85D (id, status, priority, title)").action(async () => {
@@ -5277,12 +5887,15 @@ goalCmd.command("next").alias("\uB2E4\uC74C").description("active goal \uC790\uB
5277
5887
  goalCmd.command("init").alias("\uCD08\uAE30\uD654").description("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8\uC5D0 goals/ + docs/state/ \uC2A4\uCE90\uD3F4\uB529 (\uAE30\uC874 \uD30C\uC77C \uBCF4\uC874)").action(async () => {
5278
5888
  await goalInit();
5279
5889
  });
5280
- goalCmd.command("check").alias("\uAC80\uC99D").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("scripts/check-goal-<id>.sh \uC2E4\uD589 + exit code \uC804\uB2EC").action(async (opts) => {
5890
+ goalCmd.command("check").alias("\uAC80\uC99D").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("scripts/check-goal-<id>.{mjs,sh} \uC2E4\uD589 + exit code \uC804\uB2EC (.mjs \uC6B0\uC120)").action(async (opts) => {
5281
5891
  await goalCheck(opts);
5282
5892
  });
5283
5893
  goalCmd.command("done").alias("\uC644\uB8CC").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("\uAC8C\uC774\uD2B8 \uC7AC\uAC80\uC99D \u2192 \uD1B5\uACFC \uC2DC frontmatter status=DONE \uC73C\uB85C \uC804\uC774").action(async (opts) => {
5284
5894
  await goalDone(opts);
5285
5895
  });
5896
+ goalCmd.command("sync").alias("\uB3D9\uAE30\uD654").description("goals/*.md \uC2A4\uCE94 \u2192 \uB204\uB77D\uB41C check-goal-<id>.mjs \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uBC31\uD544 (idempotent)").action(async () => {
5897
+ await goalSync();
5898
+ });
5286
5899
  program.command("blocker <description>").alias("\uBE14\uB85C\uCEE4").description("\uBE14\uB85C\uCEE4 \uAE30\uB85D \u2192 docs/state/blockers.md append (3\uAC74 \uB204\uC801 \uC2DC HARD_STOP \uC790\uB3D9 \uC0DD\uC131)").action(async (description) => {
5287
5900
  await blocker(description);
5288
5901
  });
@@ -5290,7 +5903,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
5290
5903
  await learn(lesson);
5291
5904
  });
5292
5905
  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);
5906
+ await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
5294
5907
  });
5295
5908
  program.on("command:*", async (operands) => {
5296
5909
  const unknown = operands[0] ?? "";
@@ -5300,7 +5913,7 @@ program.on("command:*", async (operands) => {
5300
5913
  });
5301
5914
  program.action(async () => {
5302
5915
  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([{
5916
+ const { choice } = await inquirer13.prompt([{
5304
5917
  type: "list",
5305
5918
  name: "choice",
5306
5919
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -5331,24 +5944,40 @@ program.action(async () => {
5331
5944
  case "secure":
5332
5945
  return secure();
5333
5946
  case "sync":
5334
- return sync();
5947
+ return guardCli("sync", false, () => sync());
5335
5948
  case "doctor":
5336
5949
  return doctor();
5337
5950
  case "ship":
5338
5951
  return ship();
5339
5952
  case "save":
5340
- return save();
5953
+ return guardCli("save", false, () => save());
5341
5954
  case "undo":
5342
- return undo();
5955
+ return guardCliDefer("undo", false, () => undo());
5343
5956
  case "status":
5344
5957
  return status();
5345
5958
  case "diff":
5346
5959
  return diff();
5347
5960
  }
5348
5961
  });
5349
- var nlInput = detectNaturalLanguageInput(process.argv);
5350
- if (nlInput !== null) {
5351
- await runNaturalLanguageRoute(nlInput);
5352
- } else {
5353
- await program.parseAsync(process.argv);
5962
+ var isMainModule = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
5963
+ if (isMainModule) {
5964
+ try {
5965
+ const nlInput = detectNaturalLanguageInput(process.argv);
5966
+ if (nlInput !== null) {
5967
+ await runNaturalLanguageRoute(nlInput);
5968
+ } else {
5969
+ await program.parseAsync(process.argv);
5970
+ }
5971
+ } catch (err) {
5972
+ if (isPromptAbortError(err)) {
5973
+ console.error(chalk34.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)"));
5974
+ } else {
5975
+ console.error(chalk34.red(`
5976
+ \u274C ${err instanceof Error ? err.message : String(err)}`));
5977
+ }
5978
+ process.exitCode = 1;
5979
+ }
5354
5980
  }
5981
+ export {
5982
+ program
5983
+ };