@byh3071/vhk 0.3.0

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 ADDED
@@ -0,0 +1,1636 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/gate.ts
7
+ import inquirer from "inquirer";
8
+ import chalk from "chalk";
9
+
10
+ // src/i18n/ko.ts
11
+ var ko = {
12
+ gate: {
13
+ title: "\u{1F50D} VHK IDEA GATE \u2014 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D",
14
+ modePrompt: "\uC5B4\uB5A4 \uAC80\uC99D\uC744 \uD560\uAE4C\uC694?",
15
+ modeQuickLabel: "\u26A1 \uD035 \uCCB4\uD06C (\uD575\uC2EC 5\uBB38\uD56D) \u2014 \uC544\uC774\uB514\uC5B4 \uB9C9 \uB5A0\uC62C\uB790\uC744 \uB54C",
16
+ modeFullLabel: "\u{1F50D} \uD480 \uAC80\uC99D (13\uB2E8\uACC4) \u2014 \uAE30\uD68D\uC774 \uC5B4\uB290 \uC815\uB3C4 \uC7A1\uD614\uC744 \uB54C",
17
+ modeSkipLabel: "\u23ED\uFE0F \uC2A4\uD0B5 \u2014 \uC774\uBBF8 \uB178\uC158/\uBB38\uC11C\uC5D0\uC11C \uAE30\uD68D \uC644\uB8CC",
18
+ skipSourcePrompt: "\u{1F4C4} \uAE30\uD68D \uBB38\uC11C \uC704\uCE58 (\uB178\uC158 URL, \uD30C\uC77C \uACBD\uB85C \uB4F1):",
19
+ skipGo: "\u{1F7E2} GO \u2014 \uAE30\uD68D \uC644\uB8CC \uD655\uC778. vhk init\uC73C\uB85C \uC9C4\uD589\uD558\uC138\uC694!",
20
+ skipSourceLabel: (source) => `\uAE30\uD68D \uBB38\uC11C: ${source}`,
21
+ quickHeader: "\u26A1 \uD035 \uCCB4\uD06C",
22
+ fullHeader: "\u{1F50D} \uD480 \uAC80\uC99D",
23
+ modeCountSuffix: (total) => `\u2014 ${total}\uBB38\uD56D`,
24
+ idea: "\u{1F4A1} \uBB50 \uB9CC\uB4E4 \uAC70\uC57C? (\uD55C \uC904)",
25
+ painPoint: "\u{1F624} Pain Point\uB294?",
26
+ edge: "\u{1F4AA} \uB098\uC758 Edge\uB294?",
27
+ checklistStart: "\u2500\u2500\u2500 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC2DC\uC791 \u2500\u2500\u2500",
28
+ hintPrefix: " \u{1F4A1}",
29
+ verdictPrompt: (failIf) => ` \u2192 \uD310\uC815? (\u{1F534} \uC2E4\uD328 \uAE30\uC900: ${failIf})`,
30
+ statusPassChoice: "\u2705 \uD1B5\uACFC",
31
+ statusHoldChoice: "\u{1F7E1} \uC544\uC9C1 \uBAA8\uB974\uACA0\uC74C / \uBCF4\uB958",
32
+ statusFailChoice: "\u{1F534} \uC2E4\uD328",
33
+ statusPassLine: " \u2705 \uD1B5\uACFC",
34
+ statusHoldLine: " \u{1F7E1} \uBCF4\uB958",
35
+ statusFailLine: " \u{1F534} \uC2E4\uD328",
36
+ verdictTitle: "\u2550\u2550\u2550 \uD310\uC815 \uACB0\uACFC \u2550\u2550\u2550",
37
+ ideaLabel: "\uC544\uC774\uB514\uC5B4:",
38
+ painPointLabel: "Pain Point:",
39
+ edgeLabel: "Edge:",
40
+ countLine: (failCount, holdCount, total) => `\u{1F534} \uC2E4\uD328: ${failCount}\uAC1C \xB7 \u{1F7E1} \uBCF4\uB958: ${holdCount}\uAC1C / ${total}\uAC1C`,
41
+ go: "\u{1F7E2} GO \u2014 Phase 2\uB85C \uC989\uC2DC \uC9C4\uD589!",
42
+ refine: "\u{1F7E1} REFINE \u2014 \u{1F534} \uD56D\uBAA9 \uC218\uC815 \uD6C4 \uC7AC\uAC80\uC99D (1\uD68C \uD55C\uC815)",
43
+ drop: "\u{1F534} DROP \u2014 \uC544\uC774\uB514\uC5B4 \uD5C8\uBE0C\uB85C \uBCF5\uADC0. \uB2E4\uB978 \uC544\uC774\uB514\uC5B4\uB97C \uC2DC\uB3C4\uD558\uC138\uC694.",
44
+ nextCommand: "\uB2E4\uC74C \uBA85\uB839\uC5B4: vhk init",
45
+ holdRemainHint: "\u{1F4A1} \uBCF4\uB958 \uD56D\uBAA9\uC740 \uAC1C\uBC1C\uD558\uBA74\uC11C \uCC44\uC6CC\uB098\uAC00\uC138\uC694."
46
+ },
47
+ init: {
48
+ title: "\u{1F6E0}\uFE0F VHK INIT \u2014 \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 + \uD558\uB124\uC2A4 \uC0DD\uC131",
49
+ skipGate: "\u23ED\uFE0F Phase 0 (gate) \uC2A4\uD0B5 \u2014 \uAE30\uC874 \uAE30\uD68D/\uC124\uACC4\uB85C \uC9C4\uD589",
50
+ projectName: "\u{1F4E6} \uD504\uB85C\uC81D\uD2B8 \uC774\uB984:",
51
+ description: "\u{1F4DD} \uD55C \uC904 \uC124\uBA85:",
52
+ projectType: "\u{1F3D7}\uFE0F \uD504\uB85C\uC81D\uD2B8 \uC720\uD615:",
53
+ confirmStack: "\uC774 \uC2A4\uD0DD\uC73C\uB85C \uC9C4\uD589\uD560\uAE4C\uC694?",
54
+ canceled: "\uCDE8\uC18C\uB428. \uC2A4\uD0DD\uC744 \uC218\uC815\uD558\uB824\uBA74 \uB2E4\uC2DC vhk init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
55
+ recommendedStack: "\uCD94\uCC9C \uC2A4\uD0DD:",
56
+ filesGenerating: "\u{1F4C2} \uD30C\uC77C \uC0DD\uC131 \uC911...",
57
+ overwrite: (filePath) => ` \u26A0\uFE0F ${filePath} \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
58
+ skipped: (filePath) => `${filePath} \uAC74\uB108\uB700`,
59
+ done: "\u{1F389} \uD558\uB124\uC2A4 \uC124\uCE58 \uC644\uB8CC!",
60
+ nextSteps: "\uB2E4\uC74C \uB2E8\uACC4:",
61
+ fillHint: "CLAUDE.md \xB7 .cursorrules\uC758 __FILL__ \uC601\uC5ED\uC744 \uCC44\uC6B0\uC138\uC694",
62
+ prdHint: "docs/PRD.md\uC5D0 v1 IN/OUT \uAE30\uB2A5\uC744 \uC815\uC758\uD558\uC138\uC694",
63
+ notionFetching: "\u{1F4E1} Notion PRD \uD398\uC774\uC9C0 \uBD88\uB7EC\uC624\uB294 \uC911...",
64
+ notionDone: (name) => `Notion PRD import \uC644\uB8CC: ${name}`,
65
+ notionReviewHint: "docs/PRD.md \uB0B4\uC6A9\uC744 \uAC80\uD1A0\uD558\uACE0 __FILL__ \uC794\uC5EC \uD56D\uBAA9\uC744 \uCC44\uC6B0\uC138\uC694",
66
+ gitHint: 'git init && git add . && git commit -m "feat: vhk init"',
67
+ startDev: "\uAC1C\uBC1C \uC2DC\uC791! \u{1F680}"
68
+ },
69
+ recap: {
70
+ title: "\u{1F4DD} VHK RECAP \u2014 \uC138\uC158 \uAE30\uB85D \uC790\uB3D9 \uC0DD\uC131",
71
+ analyzing: "\u{1F4CA} Git \uBCC0\uACBD\uC0AC\uD56D \uBD84\uC11D \uC911...",
72
+ noRepo: "\u274C Git \uB808\uD3EC\uAC00 \uC544\uB2D9\uB2C8\uB2E4. git init \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.",
73
+ noChanges: "\u26A0\uFE0F \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
74
+ summary: "\u{1F4DD} \uC774\uBC88 \uC138\uC158\uC5D0\uC11C \uBB58 \uD588\uB098\uC694? (1~3\uC904)",
75
+ decisions: "\u{1F9ED} \uB0B4\uB9B0 \uACB0\uC815\uC774 \uC788\uB098\uC694? (\uC5C6\uC73C\uBA74 Enter)",
76
+ nextTodo: "\u23ED\uFE0F \uB2E4\uC74C \uC138\uC158\uC5D0\uC11C \uD560 \uC77C\uC740?",
77
+ blockers: "\u{1F6A7} \uBE14\uB85C\uCEE4\uAC00 \uC788\uB098\uC694? (\uC5C6\uC73C\uBA74 Enter)",
78
+ done: "\u2705 \uC138\uC158 \uB85C\uADF8 \uC0DD\uC131 \uC644\uB8CC!",
79
+ updateClaude: 'CLAUDE.md "\uD604\uC7AC \uC0C1\uD0DC" \uC139\uC158\uB3C4 \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694?',
80
+ adrDetected: "\u{1F4D0} \uAE30\uC220 \uC2A4\uD0DD \uBCC0\uACBD \uAC10\uC9C0!",
81
+ createAdr: "ADR\uC744 \uC0DD\uC131\uD560\uAE4C\uC694?",
82
+ troubleDetected: "\u{1F527} \uD2B8\uB7EC\uBE14\uC288\uD305 \uCEE4\uBC0B \uAC10\uC9C0!",
83
+ createTroubleshoot: "\uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C\uB97C \uC0DD\uC131\uD560\uAE4C\uC694?"
84
+ },
85
+ check: {
86
+ title: "\u{1F50D} VHK CHECK \u2014 RULES.md \uADDC\uCE59 \uB9B0\uD2B8",
87
+ noRules: "\u26A0\uFE0F RULES.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
88
+ noAutoRules: "\u26A0\uFE0F \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
89
+ allPassed: "\u{1F389} \uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC!",
90
+ summary: "\u{1F4CA} \uB9B0\uD2B8 \uACB0\uACFC:"
91
+ },
92
+ secure: {
93
+ title: "\u{1F512} VHK SECURE SCAN \u2014 \uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uAC10\uC9C0",
94
+ noGitignore: "\u26A0\uFE0F .gitignore\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4!",
95
+ noEnvInGitignore: "\u26A0\uFE0F .gitignore\uC5D0 .env\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4!",
96
+ scanning: "\u{1F50D} \uD30C\uC77C \uC2A4\uCE94 \uC911...",
97
+ clean: "\u{1F389} \uC2DC\uD06C\uB9BF\uC774 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!",
98
+ summary: "\u{1F4CA} \uC2A4\uCE94 \uC694\uC57D:"
99
+ },
100
+ sync: {
101
+ title: "\u{1F504} VHK SYNC \u2014 RULES.md \u2192 \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654",
102
+ noRules: "\u26A0\uFE0F RULES.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
103
+ cursorrulesDone: "\u2705 .cursorrules \uB3D9\uAE30\uD654 \uC644\uB8CC",
104
+ claudeDone: "\u2705 CLAUDE.md \uB3D9\uAE30\uD654 \uC644\uB8CC",
105
+ done: "\u{1F504} \uB3D9\uAE30\uD654 \uC644\uB8CC!"
106
+ }
107
+ };
108
+
109
+ // src/commands/gate.ts
110
+ var GATE_QUESTIONS = [
111
+ { id: 1, stage: "\uBB38\uC81C \uC815\uC758", question: "\uC774 \uC544\uC774\uB514\uC5B4\uAC00 \uD574\uACB0\uD558\uB294 \uBB38\uC81C\uB97C \uD55C \uBB38\uC7A5\uC73C\uB85C \uB9D0\uD574\uBCF4\uC138\uC694.", failIf: "\uD55C \uBB38\uC7A5 \uBD88\uAC00 \u2192 \uBBF8\uC131\uC219", quick: true },
112
+ { id: 2, stage: "\uD575\uC2EC \uAE30\uB2A5", question: "\uB531 1\uAC1C \uAE30\uB2A5\uB9CC \uACE0\uB974\uBA74?", failIf: "2\uAC1C \uC774\uC0C1 \u2192 \uBC94\uC704 \uCD08\uACFC", quick: true },
113
+ { id: 3, stage: "\uC218\uC694 \uAC80\uC99D", question: "\uD0C0\uAC9F\uC774 \uBAA8\uC774\uB294 \uCEE4\uBBA4\uB2C8\uD2F0 3\uACF3\uC740?", failIf: "0\uACF3 \u2192 \uC2DC\uC7A5 \uC811\uC810 \uBD80\uC7AC", quick: true, hint: "\uC608: \uC778\uB514\uD574\uCEE4\uC2A4, \uD2B8\uC704\uD130 #buildinpublic, \uB514\uC2A4\uCF54\uB4DC" },
114
+ { id: 4, stage: "\uAE30\uD68D", question: "v1 \uAE30\uB2A5 \uBAA9\uB85D 5\uAC1C \uC774\uB0B4\uB85C \uC815\uB9AC\uD574\uBCF4\uC138\uC694.", failIf: "5\uAC1C \uCD08\uACFC \u2192 \uC624\uBC84\uC5D4\uC9C0\uB2C8\uC5B4\uB9C1" },
115
+ { id: 5, stage: "\uC124\uACC4", question: "\uD575\uC2EC API/DB \uC2A4\uD0A4\uB9C8 3\uAC1C \uC774\uB0B4\uB85C?", failIf: "\uBCF5\uC7A1\uB3C4 \uCD08\uACFC" },
116
+ { id: 6, stage: "\uAC1C\uBC1C", question: "3\uC77C \uC548\uC5D0 \uCF54\uC5B4 \uC644\uC131 \uAC00\uB2A5\uD55C\uAC00?", failIf: "\uBD88\uAC00\uB2A5 \u2192 \uBC94\uC704 \uCD95\uC18C \uD544\uC694", quick: true },
117
+ { id: 7, stage: "\uB514\uC790\uC778", question: "\uB808\uD37C\uB7F0\uC2A4 UI 1\uAC1C \uC9C0\uC815\uD560 \uC218 \uC788\uB098?", failIf: "\uC5C6\uC73C\uBA74 \u2192 \uBC29\uD5A5 \uBBF8\uC815" },
118
+ { id: 8, stage: "\uBC30\uD3EC", question: "\uBC30\uD3EC \uD50C\uB7AB\uD3FC + \uB3C4\uBA54\uC778 \uD655\uC815\uD588\uB098?", failIf: "\uBBF8\uC815 \u2192 \uBC30\uD3EC \uC9C0\uC5F0 \uC608\uACE0" },
119
+ { id: 9, stage: "\uAE30\uB85D", question: "\uBC30\uC6B4 \uAC83\uC744 \uC5B4\uB514\uC5D0 \uAE30\uB85D\uD560 \uAC74\uAC00?", failIf: "\uAE30\uB85D \uC5C6\uC73C\uBA74 \u2192 \uC790\uC0B0\uD654 \uC2E4\uD328" },
120
+ { id: 10, stage: "\uCF58\uD150\uCE20\uD654", question: "\uD2B8\uC717 1\uAC1C\uB85C \uC124\uBA85\uD574\uBCF4\uC138\uC694.", failIf: "\uBABB \uD558\uBA74 \u2192 \uD3EC\uC9C0\uC154\uB2DD \uBBF8\uD761", quick: true, hint: '\uC608: "AI \uCF54\uB529\uC744 \uBD80\uB9AC\uB294 \uC0AC\uB78C\uC744 \uC704\uD55C \uD480\uC0AC\uC774\uD074 CLI"' },
121
+ { id: 11, stage: "\uB9C8\uCF00\uD305", question: "\uCCAB \uC8FC\uC5D0 \uC62C\uB9B4 \uACF3 3\uACF3\uC740?", failIf: "0\uACF3 \u2192 \uBC30\uD3EC\uB9CC \uD558\uACE0 \uB05D" },
122
+ { id: 12, stage: "\uD310\uB9E4", question: "\uAC00\uACA9 + \uACB0\uC81C \uC218\uB2E8 \uD655\uC815\uD588\uB098?", failIf: "\uBB34\uB8CC\uB9CC \u2192 \uC218\uC775\uD654 \uBBF8\uC2E4\uD5D8" },
123
+ { id: 13, stage: "\uD53C\uB4DC\uBC31", question: "\uC0AC\uC6A9\uC790 \uD53C\uB4DC\uBC31 \uC218\uC9D1 \uCC44\uB110\uC740?", failIf: "\uC5C6\uC73C\uBA74 \u2192 \uB8E8\uD504 \uB04A\uAE40" }
124
+ ];
125
+ function judgeGate(failCount, holdCount) {
126
+ if (failCount <= 2 && holdCount <= 3) return "GO";
127
+ if (failCount <= 4) return "REFINE";
128
+ return "DROP";
129
+ }
130
+ async function gate() {
131
+ console.log(chalk.bold(`
132
+ ${ko.gate.title}
133
+ `));
134
+ const { mode } = await inquirer.prompt([{
135
+ type: "list",
136
+ name: "mode",
137
+ message: ko.gate.modePrompt,
138
+ choices: [
139
+ { name: ko.gate.modeQuickLabel, value: "quick" },
140
+ { name: ko.gate.modeFullLabel, value: "full" },
141
+ { name: ko.gate.modeSkipLabel, value: "skip" }
142
+ ]
143
+ }]);
144
+ if (mode === "skip") {
145
+ const { source } = await inquirer.prompt([{
146
+ type: "input",
147
+ name: "source",
148
+ message: ko.gate.skipSourcePrompt
149
+ }]);
150
+ console.log(chalk.green.bold(`
151
+ ${ko.gate.skipGo}`));
152
+ console.log(chalk.dim(ko.gate.skipSourceLabel(source)));
153
+ return;
154
+ }
155
+ const questions = mode === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
156
+ const total = questions.length;
157
+ const header = mode === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
158
+ console.log(chalk.dim(`
159
+ ${header} ${ko.gate.modeCountSuffix(total)}
160
+ `));
161
+ const { idea, painPoint, edge } = await inquirer.prompt([
162
+ { type: "input", name: "idea", message: ko.gate.idea },
163
+ { type: "input", name: "painPoint", message: ko.gate.painPoint },
164
+ { type: "input", name: "edge", message: ko.gate.edge }
165
+ ]);
166
+ console.log(chalk.dim(`
167
+ ${ko.gate.checklistStart}
168
+ `));
169
+ let failCount = 0;
170
+ let holdCount = 0;
171
+ const results = [];
172
+ for (let i = 0; i < questions.length; i++) {
173
+ const q = questions[i];
174
+ if (q.hint) console.log(chalk.dim(`${ko.gate.hintPrefix} ${q.hint}`));
175
+ const { answer } = await inquirer.prompt([{
176
+ type: "input",
177
+ name: "answer",
178
+ message: `[${i + 1}/${total}] ${q.stage}: ${q.question}`
179
+ }]);
180
+ const { status } = await inquirer.prompt([{
181
+ type: "list",
182
+ name: "status",
183
+ message: ko.gate.verdictPrompt(q.failIf),
184
+ choices: [
185
+ { name: ko.gate.statusPassChoice, value: "pass" },
186
+ { name: ko.gate.statusHoldChoice, value: "hold" },
187
+ { name: ko.gate.statusFailChoice, value: "fail" }
188
+ ]
189
+ }]);
190
+ if (status === "fail") failCount++;
191
+ if (status === "hold") holdCount++;
192
+ results.push({ id: q.id, stage: q.stage, status, answer });
193
+ const icon = status === "pass" ? chalk.green(ko.gate.statusPassLine) : status === "hold" ? chalk.yellow(ko.gate.statusHoldLine) : chalk.red(ko.gate.statusFailLine);
194
+ console.log(icon);
195
+ }
196
+ console.log(chalk.bold(`
197
+ ${ko.gate.verdictTitle}
198
+ `));
199
+ console.log(`${ko.gate.ideaLabel} ${chalk.cyan(idea)}`);
200
+ console.log(`${ko.gate.painPointLabel} ${painPoint}`);
201
+ console.log(`${ko.gate.edgeLabel} ${edge}`);
202
+ console.log(`${ko.gate.countLine(failCount, holdCount, total)}
203
+ `);
204
+ const verdict = judgeGate(failCount, holdCount);
205
+ if (verdict === "GO") {
206
+ console.log(chalk.green.bold(ko.gate.go));
207
+ console.log(chalk.green(ko.gate.nextCommand));
208
+ if (holdCount > 0) {
209
+ console.log(chalk.yellow(ko.gate.holdRemainHint));
210
+ }
211
+ } else if (verdict === "REFINE") {
212
+ console.log(chalk.yellow.bold(ko.gate.refine));
213
+ } else {
214
+ console.log(chalk.red.bold(ko.gate.drop));
215
+ }
216
+ }
217
+
218
+ // src/commands/init.ts
219
+ import inquirer2 from "inquirer";
220
+ import chalk3 from "chalk";
221
+ import path2 from "path";
222
+
223
+ // src/templates/claude-md.ts
224
+ function CLAUDE_MD_TEMPLATE(name, _stack) {
225
+ const d = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
226
+ const slug = name.toLowerCase().replace(/\s+/g, "-");
227
+ return [
228
+ "---",
229
+ "id: claude-md-" + slug,
230
+ "date: " + d,
231
+ "tags: [process, documentation]",
232
+ "---",
233
+ "",
234
+ "# \uAE30\uB85D \uADDC\uCE59 (" + name + ")",
235
+ "",
236
+ "> \uC774 \uD30C\uC77C\uC740 \uAE30\uB85D/\uC6B4\uC601 \uC804\uC6A9. \uCF54\uB529/\uB514\uC790\uC778 \u2192 .cursorrules \uCC38\uC870.",
237
+ "> See also: AGENTS.md",
238
+ "",
239
+ "## \uD604\uC7AC \uC0C1\uD0DC",
240
+ "- **Phase:** Phase 1 \u2014 MVP",
241
+ "- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C",
242
+ "- **\uB2E4\uC74C \uC561\uC158:** __FILL__",
243
+ "- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** " + d,
244
+ "",
245
+ "## ADR",
246
+ "\uAE30\uC220/\uB77C\uC774\uBE0C\uB7EC\uB9AC \uC120\uD0DD \uC2DC docs/adr/ADR-{\uBC88\uD638}-{\uC81C\uBAA9}.md \uC0DD\uC131.",
247
+ "",
248
+ "## \uC791\uC5C5 \uB85C\uADF8",
249
+ "\uC138\uC158 \uC885\uB8CC \uC2DC docs/log/YYYY-MM-DD-{\uC791\uC5C5\uBA85}.md \uC0DD\uC131.",
250
+ "",
251
+ "## \uD2B8\uB7EC\uBE14\uC288\uD305",
252
+ "\uC5D0\uB7EC \uD574\uACB0 \uC2DC docs/troubleshooting/TS-{\uBC88\uD638}-{\uC99D\uC0C1}.md",
253
+ "",
254
+ "## TIL",
255
+ "\uC0C8\uB85C \uBC30\uC6B4 \uAC1C\uB150 \u2192 docs/til.md \uD55C \uC904 \uCD94\uAC00",
256
+ "",
257
+ "## /done \uCEE4\uB9E8\uB4DC",
258
+ "\uC138\uC158 \uC885\uB8CC \u2192 /done \u2192 \uC694\uC57D \uC790\uB3D9 \uC0DD\uC131 \u2192 docs/log/ \uC800\uC7A5",
259
+ "",
260
+ "## \uC885\uB8CC \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8",
261
+ "1. ADR 2. \uC791\uC5C5 \uB85C\uADF8 3. \uD2B8\uB7EC\uBE14\uC288\uD305 4. TIL 5. /done"
262
+ ].join("\n");
263
+ }
264
+
265
+ // src/templates/cursorrules.ts
266
+ function CURSORRULES_TEMPLATE(name, desc, stack) {
267
+ const stackList = stack.split(" + ").map((s) => "- " + s).join("\n");
268
+ return [
269
+ "# " + name + " \u2014 Cursor Rules",
270
+ "",
271
+ "> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
272
+ "",
273
+ "## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131",
274
+ "- \uD55C \uC904 \uC124\uBA85: " + desc,
275
+ "- \uC2A4\uD0DD: " + stack,
276
+ "",
277
+ "## \uD544\uC218 \uCC38\uC870",
278
+ "- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md",
279
+ "",
280
+ "## \uAE30\uC220 \uC2A4\uD0DD (\uBCC0\uACBD \uC2DC ADR \uD544\uC218)",
281
+ stackList,
282
+ "",
283
+ "## \uCF54\uB529 \uADDC\uCE59",
284
+ "- TypeScript strict (any \uAE08\uC9C0)",
285
+ "- try-catch \uD544\uC218, \uBE48 catch \uAE08\uC9C0",
286
+ "- console.log \uD504\uB85C\uB355\uC158 \uC81C\uAC70",
287
+ "- \uCEE4\uBC0B: feat: / fix: / refactor: / docs: / chore:",
288
+ "",
289
+ "## \uB514\uC790\uC778 Anti-patterns",
290
+ "- \uBCF4\uB77C-\uD30C\uB791 \uAE30\uBCF8 \uADF8\uB77C\uB514\uC5B8\uD2B8 \uAE08\uC9C0",
291
+ "- \uACFC\uB3C4\uD55C \uB465\uADFC \uBAA8\uC11C\uB9AC (>16px) \uAE08\uC9C0",
292
+ "- \uADF8\uB9BC\uC790 \uC911\uCCA9 \xB7 \uC7A5\uC2DD SVG \uB0A8\uBC1C \uAE08\uC9C0"
293
+ ].join("\n");
294
+ }
295
+
296
+ // src/templates/prd.ts
297
+ var FILL = "__FILL__";
298
+ function fill(value, fallback = FILL) {
299
+ return value?.trim() || fallback;
300
+ }
301
+ function v1InRows(items) {
302
+ const defaults = [
303
+ { feature: FILL, description: "", priority: "P0" },
304
+ { feature: FILL, description: "", priority: "P0" },
305
+ { feature: FILL, description: "", priority: "P1" }
306
+ ];
307
+ const rows = items?.length ? items : defaults;
308
+ return rows.map(
309
+ (item, i) => `| ${i + 1} | ${fill(item.feature)} | ${item.description ?? ""} | ${item.priority || "P0"} |`
310
+ );
311
+ }
312
+ function bulletList(items, fallback = FILL) {
313
+ if (!items?.length) return [`- ${fallback}`];
314
+ return items.map((item) => `- ${item}`);
315
+ }
316
+ function screenRows(items) {
317
+ if (!items?.length) return [`| ${FILL} | |`];
318
+ return items.map((item) => `| ${fill(item.screen)} | ${item.elements ?? ""} |`);
319
+ }
320
+ function PRD_TEMPLATE(name, desc, content = {}) {
321
+ const tagline = fill(content.tagline ?? desc);
322
+ return [
323
+ "# PRD \u2014 " + name,
324
+ "",
325
+ "## \uD55C \uC904 \uC815\uC758",
326
+ tagline,
327
+ "",
328
+ "## \uBB38\uC81C (Problem)",
329
+ fill(content.problem),
330
+ "",
331
+ "## \uD574\uACB0 (Solution)",
332
+ fill(content.solution),
333
+ "",
334
+ "## v1 IN (\uD544\uC218 \uAE30\uB2A5)",
335
+ "| # | \uAE30\uB2A5 | \uC124\uBA85 | \uC6B0\uC120\uC21C\uC704 |",
336
+ "|---|------|------|----------|",
337
+ ...v1InRows(content.v1In),
338
+ "",
339
+ "## v1 OUT (\uBA85\uC2DC\uC801 \uC81C\uC678)",
340
+ ...bulletList(content.v1Out),
341
+ "",
342
+ "## \uD654\uBA74 \uC778\uBCA4\uD1A0\uB9AC",
343
+ "| \uD654\uBA74 | \uD575\uC2EC \uC694\uC18C |",
344
+ "|------|----------|",
345
+ ...screenRows(content.screens),
346
+ "",
347
+ "## \uC131\uACF5 \uC9C0\uD45C",
348
+ ...bulletList(content.metrics)
349
+ ].join("\n");
350
+ }
351
+
352
+ // src/templates/architecture.ts
353
+ function ARCHITECTURE_TEMPLATE(name, stack) {
354
+ return [
355
+ "# Architecture \u2014 " + name,
356
+ "",
357
+ "## \uAE30\uC220 \uC2A4\uD0DD",
358
+ stack,
359
+ "",
360
+ "## \uD3F4\uB354 \uAD6C\uC870",
361
+ "(\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870\uB97C \uC5EC\uAE30\uC5D0 \uC791\uC131)",
362
+ "",
363
+ "## \uB370\uC774\uD130 \uBAA8\uB378",
364
+ "| \uD14C\uC774\uBE14 | \uD575\uC2EC \uCEEC\uB7FC | \uC124\uBA85 |",
365
+ "|--------|----------|------|",
366
+ "| __FILL__ | | |",
367
+ "",
368
+ "## \uC678\uBD80 \uC11C\uBE44\uC2A4",
369
+ "| \uC11C\uBE44\uC2A4 | \uC6A9\uB3C4 |",
370
+ "|--------|------|",
371
+ "| __FILL__ | |"
372
+ ].join("\n");
373
+ }
374
+
375
+ // src/templates/adr-template.ts
376
+ function ADR_TEMPLATE() {
377
+ return [
378
+ "---",
379
+ "id: ADR-000",
380
+ "date: YYYY-MM-DD",
381
+ "status: proposed",
382
+ "tags: []",
383
+ "---",
384
+ "",
385
+ "# ADR-000: \uC81C\uBAA9",
386
+ "",
387
+ "## \uB9E5\uB77D (Context)",
388
+ "__FILL__",
389
+ "",
390
+ "## \uACB0\uC815 (Decision)",
391
+ "__FILL__",
392
+ "",
393
+ "## \uB300\uC548 (Alternatives)",
394
+ "__FILL__",
395
+ "",
396
+ "## \uACB0\uACFC (Consequences)",
397
+ "__FILL__"
398
+ ].join("\n");
399
+ }
400
+
401
+ // src/utils/logger.ts
402
+ import chalk2 from "chalk";
403
+ var log = {
404
+ success: (msg) => console.log(chalk2.green(`\u2705 ${msg}`)),
405
+ error: (msg) => console.log(chalk2.red(`\u274C ${msg}`)),
406
+ warn: (msg) => console.log(chalk2.yellow(`\u26A0\uFE0F ${msg}`)),
407
+ info: (msg) => console.log(chalk2.blue(`\u2139\uFE0F ${msg}`)),
408
+ step: (msg) => console.log(chalk2.bold(`
409
+ \u25B8 ${msg}`))
410
+ };
411
+
412
+ // src/utils/file.ts
413
+ import fs from "fs";
414
+ import path from "path";
415
+ function writeFile(filePath, content) {
416
+ const dir = path.dirname(filePath);
417
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
418
+ fs.writeFileSync(filePath, content, "utf-8");
419
+ }
420
+ function fileExists(filePath) {
421
+ return fs.existsSync(filePath);
422
+ }
423
+
424
+ // src/lib/notion-import.ts
425
+ import { Client } from "@notionhq/client";
426
+ var SECTION_MAP = {
427
+ "\uD55C \uC904 \uC815\uC758": "## \uD55C \uC904 \uC815\uC758",
428
+ "\uBB38\uC81C": "## \uBB38\uC81C (Problem)",
429
+ "Problem": "## \uBB38\uC81C (Problem)",
430
+ "\uD574\uACB0": "## \uD574\uACB0 (Solution)",
431
+ "Solution": "## \uD574\uACB0 (Solution)",
432
+ "v1 IN": "## v1 IN (\uD544\uC218 \uAE30\uB2A5)",
433
+ "\uD544\uC218 \uAE30\uB2A5": "## v1 IN (\uD544\uC218 \uAE30\uB2A5)",
434
+ "v1 OUT": "## v1 OUT (\uBA85\uC2DC\uC801 \uC81C\uC678)",
435
+ "\uC81C\uC678": "## v1 OUT (\uBA85\uC2DC\uC801 \uC81C\uC678)",
436
+ "\uD654\uBA74 \uC778\uBCA4\uD1A0\uB9AC": "## \uD654\uBA74 \uC778\uBCA4\uD1A0\uB9AC",
437
+ "\uC131\uACF5 \uC9C0\uD45C": "## \uC131\uACF5 \uC9C0\uD45C"
438
+ };
439
+ function extractPageId(url) {
440
+ const trimmed = url.trim();
441
+ const uuidMatch = trimmed.match(
442
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
443
+ );
444
+ if (uuidMatch) return uuidMatch[1];
445
+ const hex32 = trimmed.replace(/-/g, "").match(/([0-9a-f]{32})$/i);
446
+ if (hex32) {
447
+ const id = hex32[1];
448
+ return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
449
+ }
450
+ throw new Error("\uC720\uD6A8\uD55C Notion \uD398\uC774\uC9C0 URL\uC774 \uC544\uB2D9\uB2C8\uB2E4.");
451
+ }
452
+ function getPageTitle(page) {
453
+ for (const prop of Object.values(page.properties)) {
454
+ if (prop.type === "title") {
455
+ return prop.title.map((t) => t.plain_text).join("");
456
+ }
457
+ }
458
+ return "Untitled";
459
+ }
460
+ async function fetchAllBlocks(client, blockId) {
461
+ const blocks = [];
462
+ let cursor;
463
+ do {
464
+ const response = await client.blocks.children.list({
465
+ block_id: blockId,
466
+ start_cursor: cursor,
467
+ page_size: 100
468
+ });
469
+ for (const block of response.results) {
470
+ if (!("type" in block)) continue;
471
+ const b = block;
472
+ blocks.push(b);
473
+ if (b.has_children && b.type !== "child_page" && b.type !== "child_database") {
474
+ blocks.push(...await fetchAllBlocks(client, b.id));
475
+ }
476
+ }
477
+ cursor = response.has_more ? response.next_cursor ?? void 0 : void 0;
478
+ } while (cursor);
479
+ return blocks;
480
+ }
481
+ function extractText(block) {
482
+ const type = block.type;
483
+ const data = block[type];
484
+ if (!data?.rich_text) return "";
485
+ return data.rich_text.map((t) => t.plain_text).join("");
486
+ }
487
+ function parseBlocks(blocks) {
488
+ const sections = {};
489
+ let currentSection = "";
490
+ let currentContent = [];
491
+ for (const block of blocks) {
492
+ if (block.type === "heading_2" || block.type === "heading_1") {
493
+ if (currentSection) {
494
+ sections[currentSection] = currentContent.join("\n").trim();
495
+ }
496
+ const text = extractText(block);
497
+ const mapped = Object.entries(SECTION_MAP).find(([k]) => text.includes(k));
498
+ currentSection = mapped ? mapped[1] : `## ${text}`;
499
+ currentContent = [];
500
+ } else {
501
+ const text = extractText(block);
502
+ if (text) currentContent.push(text);
503
+ }
504
+ }
505
+ if (currentSection) {
506
+ sections[currentSection] = currentContent.join("\n").trim();
507
+ }
508
+ return sections;
509
+ }
510
+ async function importNotionPrd(pageUrl) {
511
+ const token = process.env.NOTION_TOKEN;
512
+ if (!token) {
513
+ throw new Error(
514
+ 'NOTION_TOKEN \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.\n 1. notion.so/my-integrations \uC5D0\uC11C Integration \uC0DD\uC131\n 2. PRD \uD398\uC774\uC9C0 \u2192 ... \u2192 Connections \u2192 Integration \uC5F0\uACB0\n 3. $env:NOTION_TOKEN = "secret_xxx" \uC124\uC815'
515
+ );
516
+ }
517
+ const pageId = extractPageId(pageUrl);
518
+ const notion = new Client({ auth: token });
519
+ const page = await notion.pages.retrieve({ page_id: pageId });
520
+ const title = getPageTitle(page);
521
+ const blocks = await fetchAllBlocks(notion, pageId);
522
+ const sections = parseBlocks(blocks);
523
+ return { title, sections };
524
+ }
525
+
526
+ // src/notion/parse-blocks.ts
527
+ function extractProjectNameFromTitle(title) {
528
+ return title.replace(/^PRD\s*[—\-–|:]\s*/i, "").trim() || title;
529
+ }
530
+
531
+ // src/notion/fetch-prd.ts
532
+ function sectionsToPrdContent(sections) {
533
+ const prd = {};
534
+ for (const [heading, content] of Object.entries(sections)) {
535
+ if (heading.includes("\uD55C \uC904")) prd.tagline = content;
536
+ else if (heading.includes("\uBB38\uC81C")) prd.problem = content;
537
+ else if (heading.includes("\uD574\uACB0")) prd.solution = content;
538
+ else if (heading.includes("v1 IN")) {
539
+ prd.v1In = content.split("\n").map((l) => l.replace(/^[-*]\s*/, "").trim()).filter(Boolean).map((feature, i) => ({
540
+ feature,
541
+ description: "",
542
+ priority: i < 2 ? "P0" : "P1"
543
+ }));
544
+ } else if (heading.includes("v1 OUT")) {
545
+ prd.v1Out = content.split("\n").map((l) => l.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
546
+ } else if (heading.includes("\uD654\uBA74")) {
547
+ prd.screens = content.split("\n").map((l) => l.trim()).filter(Boolean).map((line) => {
548
+ const [screen, ...rest] = line.split(/\s*[|—]\s*/);
549
+ return { screen: screen?.trim() ?? line, elements: rest.join(" ").trim() };
550
+ });
551
+ } else if (heading.includes("\uC131\uACF5")) {
552
+ prd.metrics = content.split("\n").map((l) => l.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
553
+ }
554
+ }
555
+ return prd;
556
+ }
557
+ async function fetchPrdFromNotion(urlOrId) {
558
+ const { title, sections } = await importNotionPrd(urlOrId);
559
+ const prd = sectionsToPrdContent(sections);
560
+ if (!prd.tagline && sections["## \uD55C \uC904 \uC815\uC758"]) {
561
+ prd.tagline = sections["## \uD55C \uC904 \uC815\uC758"];
562
+ }
563
+ return {
564
+ projectName: extractProjectNameFromTitle(title),
565
+ prd
566
+ };
567
+ }
568
+
569
+ // src/commands/init.ts
570
+ var PROJECT_TYPES = [
571
+ { name: "\u{1F310} \uC6F9 \uC571 (Next.js + Supabase + Vercel)", value: "webapp" },
572
+ { name: "\u{1F50C} Chrome \uD655\uC7A5 \uD504\uB85C\uADF8\uB7A8", value: "extension" },
573
+ { name: "\u2699\uFE0F \uC790\uB3D9\uD654/CLI \uB3C4\uAD6C", value: "cli" },
574
+ { name: "\u{1F916} \uB178\uC158 \uD1B5\uD569/MCP \uC11C\uBC84", value: "notion" },
575
+ { name: "\u{1F4F1} \uBAA8\uBC14\uC77C \uC571 (Flutter)", value: "mobile" }
576
+ ];
577
+ var VALID_TYPES = PROJECT_TYPES.map((t) => t.value);
578
+ var STACK_PRESETS = {
579
+ webapp: ["Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "Supabase", "Vercel"],
580
+ extension: ["Vite", "TypeScript", "@crxjs/vite-plugin", "Chrome Extension Manifest V3"],
581
+ cli: ["Node.js", "TypeScript", "commander", "inquirer", "chalk"],
582
+ notion: ["TypeScript", "Notion API", "MCP SDK"],
583
+ mobile: ["Flutter", "Dart", "Supabase"]
584
+ };
585
+ function resolveType(type) {
586
+ if (!type) return void 0;
587
+ if (!VALID_TYPES.includes(type)) {
588
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 type: ${type} (${VALID_TYPES.join(", ")})`);
589
+ }
590
+ return type;
591
+ }
592
+ async function collectAnswers(options, defaults = {}) {
593
+ const prompts = [];
594
+ if (!options.name && !defaults.name) {
595
+ prompts.push({ type: "input", name: "name", message: ko.init.projectName });
596
+ }
597
+ if (!options.description && !defaults.description) {
598
+ prompts.push({ type: "input", name: "description", message: ko.init.description });
599
+ }
600
+ if (!options.type && !defaults.type) {
601
+ prompts.push({ type: "list", name: "type", message: ko.init.projectType, choices: PROJECT_TYPES });
602
+ }
603
+ const prompted = prompts.length ? await inquirer2.prompt(prompts) : {};
604
+ return {
605
+ name: options.name ?? defaults.name ?? prompted.name,
606
+ description: options.description ?? defaults.description ?? prompted.description,
607
+ type: resolveType(options.type ?? defaults.type ?? prompted.type) ?? prompted.type
608
+ };
609
+ }
610
+ async function init(options = {}) {
611
+ const skipGate = Boolean(options.skipGate || options.fromNotion);
612
+ if (skipGate) {
613
+ console.log(chalk3.dim(`
614
+ ${ko.init.skipGate}
615
+ `));
616
+ }
617
+ console.log(chalk3.bold(`
618
+ ${ko.init.title}
619
+ `));
620
+ let prdContent = {};
621
+ const defaults = {};
622
+ if (options.fromNotion) {
623
+ log.step(ko.init.notionFetching);
624
+ try {
625
+ const notion = await fetchPrdFromNotion(options.fromNotion);
626
+ prdContent = notion.prd;
627
+ defaults.name = notion.projectName;
628
+ defaults.description = notion.prd.tagline;
629
+ log.success(ko.init.notionDone(notion.projectName));
630
+ } catch (err) {
631
+ log.error(err instanceof Error ? err.message : String(err));
632
+ process.exit(1);
633
+ }
634
+ }
635
+ const answers = await collectAnswers(options, defaults);
636
+ if (!answers.name || !answers.description || !answers.type) {
637
+ log.error("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984, \uC124\uBA85, \uC720\uD615\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
638
+ process.exit(1);
639
+ }
640
+ const stack = STACK_PRESETS[answers.type];
641
+ console.log(chalk3.dim(`
642
+ ${ko.init.recommendedStack} ${stack.join(" + ")}
643
+ `));
644
+ if (!options.yes) {
645
+ const { confirmStack } = await inquirer2.prompt([{
646
+ type: "confirm",
647
+ name: "confirmStack",
648
+ message: ko.init.confirmStack,
649
+ default: true
650
+ }]);
651
+ if (!confirmStack) {
652
+ log.warn(ko.init.canceled);
653
+ return;
654
+ }
655
+ }
656
+ const cwd = process.cwd();
657
+ const files = generateFiles(answers.name, answers.description, stack, prdContent);
658
+ log.step(ko.init.filesGenerating);
659
+ for (const [filePath, content] of Object.entries(files)) {
660
+ const fullPath = path2.join(cwd, filePath);
661
+ if (fileExists(fullPath)) {
662
+ const { overwrite } = await inquirer2.prompt([{
663
+ type: "confirm",
664
+ name: "overwrite",
665
+ message: ko.init.overwrite(filePath),
666
+ default: false
667
+ }]);
668
+ if (!overwrite) {
669
+ log.warn(ko.init.skipped(filePath));
670
+ continue;
671
+ }
672
+ }
673
+ writeFile(fullPath, content);
674
+ log.success(filePath);
675
+ }
676
+ console.log(chalk3.bold.green(`
677
+ ${ko.init.done}`));
678
+ console.log(chalk3.dim(`
679
+ ${ko.init.nextSteps}`));
680
+ if (options.fromNotion) {
681
+ console.log(` 1. ${ko.init.notionReviewHint}`);
682
+ console.log(` 2. ${chalk3.cyan(ko.init.gitHint)}`);
683
+ console.log(` 3. ${ko.init.startDev}
684
+ `);
685
+ } else {
686
+ console.log(` 1. ${ko.init.fillHint}`);
687
+ console.log(` 2. ${ko.init.prdHint}`);
688
+ console.log(` 3. ${chalk3.cyan(ko.init.gitHint)}`);
689
+ console.log(` 4. ${ko.init.startDev}
690
+ `);
691
+ }
692
+ }
693
+ function generateFiles(name, description, stack, prdContent = {}) {
694
+ const stackStr = stack.join(" + ");
695
+ const prd = {
696
+ tagline: description,
697
+ ...prdContent
698
+ };
699
+ return {
700
+ "CLAUDE.md": CLAUDE_MD_TEMPLATE(name, stackStr),
701
+ ".cursorrules": CURSORRULES_TEMPLATE(name, description, stackStr),
702
+ "docs/PRD.md": PRD_TEMPLATE(name, description, prd),
703
+ "docs/ARCHITECTURE.md": ARCHITECTURE_TEMPLATE(name, stackStr),
704
+ "docs/adr/ADR-000-template.md": ADR_TEMPLATE(),
705
+ "docs/log/.gitkeep": "",
706
+ "docs/troubleshooting/.gitkeep": "",
707
+ "docs/til.md": `# TIL (Today I Learned)
708
+
709
+ - [${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
710
+ `,
711
+ "BACKLOG.md": `# BACKLOG
712
+
713
+ > v1 OUT \uAE30\uB2A5\uC740 \uC5EC\uAE30\uC5D0 \uAE30\uB85D. \uBC94\uC704 \uC218\uBE44 \uD544\uC218.
714
+
715
+ ## v1.1 \uD6C4\uBCF4
716
+
717
+ -
718
+ `
719
+ };
720
+ }
721
+
722
+ // src/commands/recap.ts
723
+ import inquirer3 from "inquirer";
724
+ import chalk4 from "chalk";
725
+ import fs3 from "fs";
726
+ import path4 from "path";
727
+
728
+ // src/lib/git.ts
729
+ import simpleGit from "simple-git";
730
+ var git = simpleGit();
731
+ function fileStatus(workingDir) {
732
+ if (workingDir === "?") return "new";
733
+ if (workingDir === "D") return "deleted";
734
+ if (workingDir === "R") return "renamed";
735
+ return "modified";
736
+ }
737
+ async function getSessionDiff(since) {
738
+ const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
739
+ const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
740
+ const statusResult = await git.status();
741
+ const statByFile = new Map(
742
+ diffSummary.files.map((f) => [f.file, f])
743
+ );
744
+ const files = statusResult.files.map((f) => {
745
+ const stat = statByFile.get(f.path);
746
+ return {
747
+ file: f.path,
748
+ insertions: stat?.insertions ?? 0,
749
+ deletions: stat?.deletions ?? 0,
750
+ status: fileStatus(f.working_dir)
751
+ };
752
+ });
753
+ return {
754
+ filesChanged: statusResult.files.length,
755
+ insertions: diffSummary.insertions,
756
+ deletions: diffSummary.deletions,
757
+ files
758
+ };
759
+ }
760
+ async function getRecentCommits(count = 10, since) {
761
+ const options = { maxCount: count };
762
+ if (since) options["--since"] = since;
763
+ const log2 = await git.log(options);
764
+ return log2.all.map((entry) => ({
765
+ hash: entry.hash,
766
+ message: entry.message,
767
+ date: entry.date,
768
+ author: entry.author_name
769
+ }));
770
+ }
771
+ async function isGitRepo() {
772
+ try {
773
+ await git.revparse(["--is-inside-work-tree"]);
774
+ return true;
775
+ } catch {
776
+ return false;
777
+ }
778
+ }
779
+
780
+ // src/lib/adr.ts
781
+ import fs2 from "fs";
782
+ import path3 from "path";
783
+ var ADR_RULES = [
784
+ {
785
+ title: "\uC758\uC874\uC131 \uBCC0\uACBD",
786
+ context: "package.json \uB610\uB294 lockfile \uBCC0\uACBD \u2014 \uB77C\uC774\uBE0C\uB7EC\uB9AC \uCD94\uAC00/\uAD50\uCCB4 \uAC80\uD1A0 \uD544\uC694",
787
+ test: (f) => /package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(f)
788
+ },
789
+ {
790
+ title: "\uBE4C\uB4DC/\uB3C4\uAD6C \uC124\uC815",
791
+ context: "\uBE4C\uB4DC\xB7\uB9B0\uD2B8\xB7\uD14C\uC2A4\uD2B8 \uC124\uC815 \uD30C\uC77C \uBCC0\uACBD",
792
+ test: (f) => /\.(config|rc)\.(ts|js|mjs|cjs)$|tsconfig.*\.json$|vitest\.config|vite\.config|eslint|prettier/.test(f)
793
+ },
794
+ {
795
+ title: "\uB370\uC774\uD130\uBCA0\uC774\uC2A4/\uC2A4\uD0A4\uB9C8",
796
+ context: "DB \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uB610\uB294 \uC2A4\uD0A4\uB9C8 \uAD00\uB828 \uD30C\uC77C \uBCC0\uACBD",
797
+ test: (f) => /prisma\/|migrations\/|supabase\/|schema\.sql|drizzle/.test(f)
798
+ },
799
+ {
800
+ title: "\uC778\uD504\uB77C/\uBC30\uD3EC",
801
+ context: "Docker, CI, \uBC30\uD3EC \uC124\uC815 \uBCC0\uACBD",
802
+ test: (f) => /Dockerfile|docker-compose|\.github\/workflows|vercel\.json|fly\.toml/.test(f)
803
+ },
804
+ {
805
+ title: "\uC778\uC99D/\uBCF4\uC548",
806
+ context: "\uC778\uC99D\xB7\uAD8C\uD55C\xB7\uD658\uACBD\uBCC0\uC218 \uD15C\uD50C\uB9BF \uBCC0\uACBD",
807
+ test: (f) => /\.env\.example$|auth\/|middleware\.(ts|js)$/.test(f)
808
+ }
809
+ ];
810
+ function detectAdrCandidates(diff) {
811
+ const candidates = [];
812
+ for (const rule of ADR_RULES) {
813
+ const matched = diff.files.map((f) => f.file).filter(rule.test);
814
+ if (matched.length > 0) {
815
+ candidates.push({
816
+ title: rule.title,
817
+ context: rule.context,
818
+ files: matched
819
+ });
820
+ }
821
+ }
822
+ return candidates;
823
+ }
824
+ function nextAdrNumber(adrDir) {
825
+ if (!fs2.existsSync(adrDir)) return 1;
826
+ const nums = fs2.readdirSync(adrDir).map((name) => name.match(/^ADR-(\d+)/i)?.[1]).filter((n) => Boolean(n)).map((n) => parseInt(n, 10));
827
+ return nums.length ? Math.max(...nums) + 1 : 1;
828
+ }
829
+ function slugify(title) {
830
+ return title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9가-힣-]/g, "").slice(0, 40) || "decision";
831
+ }
832
+ function createAdrFile(cwd, title, context, decision, consequences) {
833
+ const adrDir = path3.join(cwd, "docs", "adr");
834
+ if (!fs2.existsSync(adrDir)) fs2.mkdirSync(adrDir, { recursive: true });
835
+ const num = nextAdrNumber(adrDir);
836
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
837
+ const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
838
+ const filePath = path3.join(adrDir, fileName);
839
+ const content = [
840
+ "---",
841
+ `id: ADR-${String(num).padStart(3, "0")}`,
842
+ `date: ${today}`,
843
+ "status: accepted",
844
+ `tags: [${slugify(title)}]`,
845
+ "---",
846
+ "",
847
+ `# ADR-${String(num).padStart(3, "0")}: ${title}`,
848
+ "",
849
+ "## \uB9E5\uB77D (Context)",
850
+ context,
851
+ "",
852
+ "## \uACB0\uC815 (Decision)",
853
+ decision,
854
+ "",
855
+ "## \uB300\uC548 (Alternatives)",
856
+ "_\uAC80\uD1A0\uD55C \uB300\uC548\uC744 \uAE30\uB85D\uD558\uC138\uC694._",
857
+ "",
858
+ "## \uACB0\uACFC (Consequences)",
859
+ consequences,
860
+ "",
861
+ "---",
862
+ `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
863
+ ].join("\n");
864
+ fs2.writeFileSync(filePath, content, "utf-8");
865
+ return filePath;
866
+ }
867
+
868
+ // src/commands/recap.ts
869
+ async function recap(options = {}) {
870
+ console.log(chalk4.bold(`
871
+ ${ko.recap.title}
872
+ `));
873
+ if (!await isGitRepo()) {
874
+ console.log(chalk4.red(ko.recap.noRepo));
875
+ return;
876
+ }
877
+ console.log(chalk4.dim(`${ko.recap.analyzing}
878
+ `));
879
+ const diff = await getSessionDiff(options.since);
880
+ const commits = await getRecentCommits(10, options.since);
881
+ if (diff.filesChanged === 0 && commits.length === 0) {
882
+ console.log(chalk4.yellow(ko.recap.noChanges));
883
+ return;
884
+ }
885
+ console.log(chalk4.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
886
+ console.log(` \uD30C\uC77C: ${chalk4.cyan(String(diff.filesChanged))}\uAC1C \uBCC0\uACBD`);
887
+ console.log(` \uCD94\uAC00: ${chalk4.green("+" + diff.insertions)} / \uC0AD\uC81C: ${chalk4.red("-" + diff.deletions)}`);
888
+ if (diff.files.length > 0) {
889
+ console.log(chalk4.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
890
+ diff.files.slice(0, 15).forEach((f) => {
891
+ const icon = f.status === "new" ? chalk4.green("\u{1F195}") : f.status === "deleted" ? chalk4.red("\u{1F5D1}\uFE0F") : chalk4.yellow("\u270F\uFE0F");
892
+ console.log(` ${icon} ${f.file}`);
893
+ });
894
+ if (diff.files.length > 15) {
895
+ console.log(chalk4.dim(` ... \uC678 ${diff.files.length - 15}\uAC1C`));
896
+ }
897
+ }
898
+ if (commits.length > 0) {
899
+ console.log(chalk4.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
900
+ commits.slice(0, 5).forEach((c) => {
901
+ console.log(chalk4.dim(` \u2022 ${c.message}`));
902
+ });
903
+ }
904
+ console.log("");
905
+ const answers = await inquirer3.prompt([
906
+ {
907
+ type: "input",
908
+ name: "summary",
909
+ message: ko.recap.summary
910
+ },
911
+ {
912
+ type: "input",
913
+ name: "decisions",
914
+ message: ko.recap.decisions,
915
+ default: "\uC5C6\uC74C"
916
+ },
917
+ {
918
+ type: "input",
919
+ name: "nextTodo",
920
+ message: ko.recap.nextTodo
921
+ },
922
+ {
923
+ type: "input",
924
+ name: "blockers",
925
+ message: ko.recap.blockers,
926
+ default: "\uC5C6\uC74C"
927
+ }
928
+ ]);
929
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
930
+ const logDir = path4.join(process.cwd(), "docs", "log");
931
+ if (!fs3.existsSync(logDir)) fs3.mkdirSync(logDir, { recursive: true });
932
+ const existing = fs3.readdirSync(logDir).filter((f) => f.startsWith(today));
933
+ const sessionNum = existing.length + 1;
934
+ const fileName = `${today}-session-${sessionNum}.md`;
935
+ const filePath = path4.join(logDir, fileName);
936
+ const fileList = diff.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
937
+ const commitList = commits.slice(0, 10).map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
938
+ const content = [
939
+ `# \uC138\uC158 \uB85C\uADF8 \u2014 ${today} #${sessionNum}`,
940
+ "",
941
+ "## \uC791\uC5C5 \uC694\uC57D",
942
+ answers.summary,
943
+ "",
944
+ "## \uACB0\uC815 \uC0AC\uD56D",
945
+ answers.decisions,
946
+ "",
947
+ "## \uB2E4\uC74C \uD560 \uC77C",
948
+ answers.nextTodo,
949
+ "",
950
+ "## \uBE14\uB85C\uCEE4",
951
+ answers.blockers,
952
+ "",
953
+ "## \uBCC0\uACBD \uD30C\uC77C",
954
+ `\uCD1D ${diff.filesChanged}\uAC1C \uD30C\uC77C (+${diff.insertions} -${diff.deletions})`,
955
+ "",
956
+ "| \uD30C\uC77C | \uC0C1\uD0DC |",
957
+ "|------|------|",
958
+ fileList,
959
+ "",
960
+ "## \uCEE4\uBC0B \uB85C\uADF8",
961
+ commitList || "(\uCEE4\uBC0B \uC5C6\uC74C)",
962
+ "",
963
+ "---",
964
+ `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
965
+ ].join("\n");
966
+ fs3.writeFileSync(filePath, content, "utf-8");
967
+ const adrCandidates = detectAdrCandidates(diff);
968
+ if (adrCandidates.length > 0) {
969
+ console.log(chalk4.cyan.bold(`
970
+ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
971
+ for (const candidate of adrCandidates) {
972
+ console.log(chalk4.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
973
+ candidate.files.forEach((f) => console.log(chalk4.dim(` ${f}`)));
974
+ }
975
+ const { createAdr } = await inquirer3.prompt([{
976
+ type: "confirm",
977
+ name: "createAdr",
978
+ message: ko.recap.createAdr,
979
+ default: true
980
+ }]);
981
+ if (createAdr) {
982
+ for (const candidate of adrCandidates) {
983
+ const adrAnswers = await inquirer3.prompt([
984
+ {
985
+ type: "input",
986
+ name: "decision",
987
+ message: `\u{1F9ED} [${candidate.title}] \uC5B4\uB5A4 \uACB0\uC815\uC744 \uB0B4\uB838\uB098\uC694?`
988
+ },
989
+ {
990
+ type: "input",
991
+ name: "consequences",
992
+ message: "\u{1F4DD} \uC774 \uACB0\uC815\uC758 \uACB0\uACFC/\uC601\uD5A5\uC740?",
993
+ default: "\uCD94\uD6C4 \uD655\uC778"
994
+ }
995
+ ]);
996
+ const adrPath = createAdrFile(
997
+ process.cwd(),
998
+ candidate.title,
999
+ candidate.context,
1000
+ adrAnswers.decision,
1001
+ adrAnswers.consequences
1002
+ );
1003
+ console.log(chalk4.green(` \u2705 ADR \uC0DD\uC131: ${path4.relative(process.cwd(), adrPath)}`));
1004
+ }
1005
+ }
1006
+ }
1007
+ const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
1008
+ const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
1009
+ if (troubleCommits.length > 0) {
1010
+ console.log(chalk4.yellow.bold(`
1011
+ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
1012
+ troubleCommits.forEach((c) => {
1013
+ console.log(chalk4.dim(` \u2022 ${c.message}`));
1014
+ });
1015
+ const { createTroubleshoot } = await inquirer3.prompt([{
1016
+ type: "confirm",
1017
+ name: "createTroubleshoot",
1018
+ message: ko.recap.createTroubleshoot,
1019
+ default: true
1020
+ }]);
1021
+ if (createTroubleshoot) {
1022
+ const tsDir = path4.join(process.cwd(), "docs", "troubleshooting");
1023
+ if (!fs3.existsSync(tsDir)) fs3.mkdirSync(tsDir, { recursive: true });
1024
+ const tsAnswers = await inquirer3.prompt([
1025
+ {
1026
+ type: "input",
1027
+ name: "problem",
1028
+ message: "\u{1F41B} \uBB34\uC2A8 \uBB38\uC81C\uC600\uB098\uC694? (\uC99D\uC0C1)"
1029
+ },
1030
+ {
1031
+ type: "input",
1032
+ name: "cause",
1033
+ message: "\u{1F50D} \uC6D0\uC778\uC740?"
1034
+ },
1035
+ {
1036
+ type: "input",
1037
+ name: "solution",
1038
+ message: "\u2705 \uC5B4\uB5BB\uAC8C \uD574\uACB0\uD588\uB098\uC694?"
1039
+ }
1040
+ ]);
1041
+ const tsFileName = `${today}-${tsAnswers.problem.slice(0, 30).replace(/[^a-zA-Z0-9가-힣]/g, "-")}.md`;
1042
+ const tsFilePath = path4.join(tsDir, tsFileName);
1043
+ const tsContent = [
1044
+ `# \uD2B8\uB7EC\uBE14\uC288\uD305: ${tsAnswers.problem}`,
1045
+ "",
1046
+ `**\uB0A0\uC9DC:** ${today}`,
1047
+ "",
1048
+ "## \uC99D\uC0C1",
1049
+ tsAnswers.problem,
1050
+ "",
1051
+ "## \uC6D0\uC778",
1052
+ tsAnswers.cause,
1053
+ "",
1054
+ "## \uD574\uACB0",
1055
+ tsAnswers.solution,
1056
+ "",
1057
+ "## \uAD00\uB828 \uCEE4\uBC0B",
1058
+ troubleCommits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n"),
1059
+ "",
1060
+ "---",
1061
+ `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1062
+ ].join("\n");
1063
+ fs3.writeFileSync(tsFilePath, tsContent, "utf-8");
1064
+ console.log(chalk4.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path4.relative(process.cwd(), tsFilePath)}`));
1065
+ }
1066
+ }
1067
+ console.log(chalk4.green.bold(`
1068
+ ${ko.recap.done}`));
1069
+ console.log(chalk4.dim(` \u{1F4C4} ${path4.relative(process.cwd(), filePath)}`));
1070
+ const claudeMdPath = path4.join(process.cwd(), "CLAUDE.md");
1071
+ if (fs3.existsSync(claudeMdPath)) {
1072
+ const { updateClaude } = await inquirer3.prompt([{
1073
+ type: "confirm",
1074
+ name: "updateClaude",
1075
+ message: ko.recap.updateClaude,
1076
+ default: true
1077
+ }]);
1078
+ if (updateClaude) {
1079
+ let claudeContent = fs3.readFileSync(claudeMdPath, "utf-8");
1080
+ claudeContent = claudeContent.replace(
1081
+ /- \*\*마지막 업데이트:\*\*.*/,
1082
+ `- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${today}`
1083
+ );
1084
+ claudeContent = claudeContent.replace(
1085
+ /- \*\*다음 액션:\*\*.*/,
1086
+ `- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
1087
+ );
1088
+ fs3.writeFileSync(claudeMdPath, claudeContent, "utf-8");
1089
+ console.log(chalk4.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
1090
+ }
1091
+ }
1092
+ console.log(chalk4.dim(`
1093
+ \u{1F4A1} \uD301: git add docs/log/ && git commit -m "docs: session recap ${today}"
1094
+ `));
1095
+ }
1096
+
1097
+ // src/commands/sync.ts
1098
+ import chalk5 from "chalk";
1099
+ import fs4 from "fs";
1100
+ import path5 from "path";
1101
+ var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
1102
+ var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
1103
+ function parseRulesMd(content) {
1104
+ const sections = [];
1105
+ const lines = content.split("\n");
1106
+ let currentTitle = "";
1107
+ let currentContent = [];
1108
+ for (const line of lines) {
1109
+ if (line.startsWith("## ")) {
1110
+ if (currentTitle) {
1111
+ sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
1112
+ }
1113
+ currentTitle = line.replace("## ", "").trim();
1114
+ currentContent = [];
1115
+ } else {
1116
+ currentContent.push(line);
1117
+ }
1118
+ }
1119
+ if (currentTitle) {
1120
+ sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
1121
+ }
1122
+ return sections;
1123
+ }
1124
+ function toCursorrules(sections, projectName) {
1125
+ const codingSections = sections.filter(
1126
+ (s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
1127
+ );
1128
+ const lines = [
1129
+ `# ${projectName} \u2014 Cursor Rules`,
1130
+ "",
1131
+ "> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
1132
+ "> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
1133
+ "",
1134
+ "## \uD544\uC218 \uCC38\uC870",
1135
+ "- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md \xB7 RULES.md",
1136
+ ""
1137
+ ];
1138
+ for (const section of codingSections) {
1139
+ lines.push(`## ${section.title}`);
1140
+ lines.push(section.content);
1141
+ lines.push("");
1142
+ }
1143
+ return lines.join("\n");
1144
+ }
1145
+ function toClaudeMd(sections, existing) {
1146
+ const recordSections = sections.filter(
1147
+ (s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
1148
+ );
1149
+ const statusMatch = existing.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
1150
+ const statusSection = statusMatch ? statusMatch[0] : "";
1151
+ const header = existing.split("## ")[0].trim();
1152
+ const lines = [
1153
+ header,
1154
+ "",
1155
+ statusSection,
1156
+ "",
1157
+ "> \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.",
1158
+ ""
1159
+ ];
1160
+ for (const section of recordSections) {
1161
+ lines.push(`## ${section.title}`);
1162
+ lines.push(section.content);
1163
+ lines.push("");
1164
+ }
1165
+ return lines.join("\n");
1166
+ }
1167
+ async function sync() {
1168
+ console.log(chalk5.bold(`
1169
+ ${ko.sync.title}
1170
+ `));
1171
+ const cwd = process.cwd();
1172
+ const rulesPath = path5.join(cwd, "RULES.md");
1173
+ if (!fs4.existsSync(rulesPath)) {
1174
+ console.log(chalk5.yellow(ko.sync.noRules));
1175
+ console.log(chalk5.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
1176
+ console.log(chalk5.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
1177
+ console.log("");
1178
+ console.log(chalk5.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
1179
+ console.log(chalk5.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
1180
+ console.log(chalk5.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
1181
+ console.log(chalk5.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
1182
+ console.log(chalk5.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
1183
+ console.log(chalk5.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
1184
+ return;
1185
+ }
1186
+ const rulesContent = fs4.readFileSync(rulesPath, "utf-8");
1187
+ const sections = parseRulesMd(rulesContent);
1188
+ console.log(chalk5.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
1189
+ const firstLine = rulesContent.split("\n")[0];
1190
+ const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
1191
+ const cursorrulesPath = path5.join(cwd, ".cursorrules");
1192
+ fs4.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
1193
+ console.log(chalk5.green(` ${ko.sync.cursorrulesDone}`));
1194
+ const claudePath = path5.join(cwd, "CLAUDE.md");
1195
+ const existingClaude = fs4.existsSync(claudePath) ? fs4.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
1196
+
1197
+ ## \uD604\uC7AC \uC0C1\uD0DC
1198
+ - **Phase:** __FILL__
1199
+ - **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
1200
+ - **\uB2E4\uC74C \uC561\uC158:** __FILL__
1201
+ - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
1202
+ fs4.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
1203
+ console.log(chalk5.green(` ${ko.sync.claudeDone}`));
1204
+ console.log(chalk5.bold.green(`
1205
+ ${ko.sync.done}`));
1206
+ console.log(chalk5.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
1207
+ console.log(chalk5.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694.\n"));
1208
+ }
1209
+
1210
+ // src/commands/check.ts
1211
+ import chalk6 from "chalk";
1212
+ import path7 from "path";
1213
+ import fs6 from "fs";
1214
+
1215
+ // src/lib/rules-parser.ts
1216
+ import fs5 from "fs";
1217
+ import path6 from "path";
1218
+ function parseRules(rulesPath) {
1219
+ if (!fs5.existsSync(rulesPath)) return [];
1220
+ const content = fs5.readFileSync(rulesPath, "utf-8");
1221
+ const lines = content.split("\n");
1222
+ const rules = [];
1223
+ let currentSection = "";
1224
+ let ruleIndex = 0;
1225
+ for (const line of lines) {
1226
+ if (line.startsWith("## ")) {
1227
+ currentSection = line.replace("## ", "").trim();
1228
+ continue;
1229
+ }
1230
+ const bulletMatch = line.match(/^[-*]\s+(.+)/);
1231
+ if (!bulletMatch) continue;
1232
+ const ruleText = bulletMatch[1];
1233
+ ruleIndex++;
1234
+ if (/kebab[- ]?case/i.test(ruleText)) {
1235
+ rules.push(createNamingRule(
1236
+ `naming-${ruleIndex}`,
1237
+ currentSection,
1238
+ ruleText,
1239
+ "kebab-case"
1240
+ ));
1241
+ } else if (/camel[- ]?case/i.test(ruleText)) {
1242
+ rules.push(createNamingRule(
1243
+ `naming-${ruleIndex}`,
1244
+ currentSection,
1245
+ ruleText,
1246
+ "camelCase"
1247
+ ));
1248
+ }
1249
+ const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
1250
+ if (pathMatch) {
1251
+ rules.push(createStructureRule(
1252
+ `structure-${ruleIndex}`,
1253
+ currentSection,
1254
+ ruleText,
1255
+ pathMatch[1]
1256
+ ));
1257
+ }
1258
+ if (/금지|사용하지|쓰지 마|하지 않는다|never use|do not use/i.test(ruleText)) {
1259
+ const backtickContent = ruleText.match(/`([^`]+)`/);
1260
+ if (backtickContent) {
1261
+ rules.push(createContentRule(
1262
+ `ban-${ruleIndex}`,
1263
+ currentSection,
1264
+ ruleText,
1265
+ backtickContent[1],
1266
+ "banned"
1267
+ ));
1268
+ }
1269
+ }
1270
+ if (/반드시|필수|항상|must|always|required/i.test(ruleText)) {
1271
+ rules.push({
1272
+ id: `required-${ruleIndex}`,
1273
+ section: currentSection,
1274
+ type: "custom",
1275
+ description: ruleText,
1276
+ check: () => []
1277
+ });
1278
+ }
1279
+ }
1280
+ return rules;
1281
+ }
1282
+ function createNamingRule(id, section, desc, convention) {
1283
+ return {
1284
+ id,
1285
+ section,
1286
+ type: "naming",
1287
+ description: desc,
1288
+ check: (cwd) => {
1289
+ const violations = [];
1290
+ const srcDir = path6.join(cwd, "src");
1291
+ if (!fs5.existsSync(srcDir)) return violations;
1292
+ walkFiles(srcDir, (filePath) => {
1293
+ const name = path6.basename(filePath, path6.extname(filePath));
1294
+ if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
1295
+ if (!["index", "vite.config", "tsconfig"].includes(name)) {
1296
+ violations.push({
1297
+ ruleId: id,
1298
+ severity: "warning",
1299
+ message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
1300
+ file: path6.relative(cwd, filePath)
1301
+ });
1302
+ }
1303
+ }
1304
+ });
1305
+ return violations;
1306
+ }
1307
+ };
1308
+ }
1309
+ function createStructureRule(id, section, desc, expectedPath) {
1310
+ return {
1311
+ id,
1312
+ section,
1313
+ type: "structure",
1314
+ description: desc,
1315
+ check: (cwd) => {
1316
+ const fullPath = path6.join(cwd, expectedPath);
1317
+ if (!fs5.existsSync(fullPath)) {
1318
+ return [{
1319
+ ruleId: id,
1320
+ severity: "error",
1321
+ message: `\uD544\uC218 \uB514\uB809\uD1A0\uB9AC/\uD30C\uC77C \uB204\uB77D: ${expectedPath}`
1322
+ }];
1323
+ }
1324
+ return [];
1325
+ }
1326
+ };
1327
+ }
1328
+ function createContentRule(id, section, desc, pattern, type) {
1329
+ return {
1330
+ id,
1331
+ section,
1332
+ type: "content",
1333
+ description: desc,
1334
+ pattern: new RegExp(escapeRegex(pattern), "i"),
1335
+ check: (cwd) => {
1336
+ const violations = [];
1337
+ const srcDir = path6.join(cwd, "src");
1338
+ if (!fs5.existsSync(srcDir)) return violations;
1339
+ const regex = new RegExp(escapeRegex(pattern), "i");
1340
+ walkFiles(srcDir, (filePath) => {
1341
+ const fileContent = fs5.readFileSync(filePath, "utf-8");
1342
+ const fileLines = fileContent.split("\n");
1343
+ fileLines.forEach((line, idx) => {
1344
+ if (regex.test(line)) {
1345
+ violations.push({
1346
+ ruleId: id,
1347
+ severity: type === "banned" ? "error" : "warning",
1348
+ message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
1349
+ file: path6.relative(cwd, filePath),
1350
+ line: idx + 1
1351
+ });
1352
+ }
1353
+ regex.lastIndex = 0;
1354
+ });
1355
+ });
1356
+ return violations;
1357
+ }
1358
+ };
1359
+ }
1360
+ function walkFiles(dir, callback) {
1361
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1362
+ for (const entry of entries) {
1363
+ const fullPath = path6.join(dir, entry.name);
1364
+ if (entry.isDirectory()) {
1365
+ if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
1366
+ walkFiles(fullPath, callback);
1367
+ }
1368
+ } else if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name)) {
1369
+ callback(fullPath);
1370
+ }
1371
+ }
1372
+ }
1373
+ function escapeRegex(str) {
1374
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1375
+ }
1376
+
1377
+ // src/commands/check.ts
1378
+ async function check() {
1379
+ console.log(chalk6.bold(`
1380
+ ${ko.check.title}
1381
+ `));
1382
+ const cwd = process.cwd();
1383
+ const rulesPath = path7.join(cwd, "RULES.md");
1384
+ if (!fs6.existsSync(rulesPath)) {
1385
+ console.log(chalk6.yellow(ko.check.noRules));
1386
+ console.log(chalk6.dim(" vhk init\uC73C\uB85C \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD558\uAC70\uB098 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
1387
+ return;
1388
+ }
1389
+ const rules = parseRules(rulesPath);
1390
+ console.log(chalk6.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
1391
+ `));
1392
+ if (rules.length === 0) {
1393
+ console.log(chalk6.yellow(ko.check.noAutoRules));
1394
+ console.log(chalk6.dim(" RULES.md\uC5D0 \uCF54\uB529 \uADDC\uCE59/\uB124\uC774\uBC0D/\uAD6C\uC870 \uADDC\uCE59\uC744 \uCD94\uAC00\uD558\uBA74 \uC790\uB3D9 \uB9B0\uD2B8\uB429\uB2C8\uB2E4."));
1395
+ return;
1396
+ }
1397
+ const allViolations = [];
1398
+ let passCount = 0;
1399
+ for (const rule of rules) {
1400
+ const violations = rule.check(cwd);
1401
+ if (violations.length === 0) {
1402
+ console.log(chalk6.green(` \u2705 ${rule.id}`) + chalk6.dim(` \u2014 ${rule.description.slice(0, 60)}`));
1403
+ passCount++;
1404
+ } else {
1405
+ console.log(chalk6.red(` \u274C ${rule.id}`) + chalk6.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
1406
+ violations.forEach((v) => {
1407
+ const loc = v.file ? chalk6.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
1408
+ const icon = v.severity === "error" ? chalk6.red("\u2716") : v.severity === "warning" ? chalk6.yellow("\u26A0") : chalk6.blue("\u2139");
1409
+ console.log(` ${icon} ${v.message}${loc}`);
1410
+ });
1411
+ allViolations.push(...violations);
1412
+ }
1413
+ }
1414
+ console.log("");
1415
+ const errors = allViolations.filter((v) => v.severity === "error").length;
1416
+ const warnings = allViolations.filter((v) => v.severity === "warning").length;
1417
+ if (allViolations.length === 0) {
1418
+ console.log(chalk6.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
1419
+ } else {
1420
+ console.log(chalk6.bold(ko.check.summary));
1421
+ console.log(` \uADDC\uCE59: ${chalk6.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk6.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk6.red(String(allViolations.length))}\uAC74`);
1422
+ if (errors > 0) console.log(` ${chalk6.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
1423
+ if (warnings > 0) console.log(` ${chalk6.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
1424
+ console.log("");
1425
+ console.log(chalk6.dim(" \u{1F4A1} \uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 vhk check\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694."));
1426
+ }
1427
+ if (errors > 0) {
1428
+ process.exitCode = 1;
1429
+ }
1430
+ }
1431
+
1432
+ // src/commands/secure.ts
1433
+ import chalk7 from "chalk";
1434
+ import fs7 from "fs";
1435
+ import path8 from "path";
1436
+
1437
+ // src/lib/secret-patterns.ts
1438
+ var SECRET_PATTERNS = [
1439
+ {
1440
+ id: "aws-access-key",
1441
+ name: "AWS Access Key",
1442
+ severity: "critical",
1443
+ pattern: /AKIA[0-9A-Z]{16}/
1444
+ },
1445
+ {
1446
+ id: "aws-secret-key",
1447
+ name: "AWS Secret Key",
1448
+ severity: "critical",
1449
+ pattern: /aws_secret_access_key\s*=\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i
1450
+ },
1451
+ {
1452
+ id: "private-key",
1453
+ name: "Private Key",
1454
+ severity: "critical",
1455
+ pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/
1456
+ },
1457
+ {
1458
+ id: "notion-token",
1459
+ name: "Notion Integration Token",
1460
+ severity: "critical",
1461
+ pattern: /secret_[A-Za-z0-9]{24,}/
1462
+ },
1463
+ {
1464
+ id: "github-token",
1465
+ name: "GitHub Token",
1466
+ severity: "critical",
1467
+ pattern: /ghp_[A-Za-z0-9]{36,}/
1468
+ },
1469
+ {
1470
+ id: "openai-key",
1471
+ name: "OpenAI API Key",
1472
+ severity: "critical",
1473
+ pattern: /sk-[A-Za-z0-9]{20,}/
1474
+ },
1475
+ {
1476
+ id: "generic-api-key",
1477
+ name: "Generic API Key",
1478
+ severity: "high",
1479
+ pattern: /(?:api[_-]?key|apikey|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9_\-]{16,}['"]?/i
1480
+ },
1481
+ {
1482
+ id: "password-inline",
1483
+ name: "Inline Password",
1484
+ severity: "high",
1485
+ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/i
1486
+ },
1487
+ {
1488
+ id: "jwt",
1489
+ name: "JWT Token",
1490
+ severity: "medium",
1491
+ pattern: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
1492
+ }
1493
+ ];
1494
+ function maskSecret(value) {
1495
+ if (value.length <= 8) return "****";
1496
+ const visible = Math.min(8, value.length - 4);
1497
+ return value.slice(0, visible) + "****";
1498
+ }
1499
+
1500
+ // src/commands/secure.ts
1501
+ var IGNORE_DIRS = ["node_modules", ".git", "dist", ".next", ".nuxt", "build", "coverage"];
1502
+ var SCAN_EXTENSIONS = [
1503
+ ".ts",
1504
+ ".tsx",
1505
+ ".js",
1506
+ ".jsx",
1507
+ ".mjs",
1508
+ ".cjs",
1509
+ ".json",
1510
+ ".yaml",
1511
+ ".yml",
1512
+ ".toml",
1513
+ ".env",
1514
+ ".local",
1515
+ ".production",
1516
+ ".development"
1517
+ ];
1518
+ async function secure() {
1519
+ console.log(chalk7.bold(`
1520
+ ${ko.secure.title}
1521
+ `));
1522
+ const cwd = process.cwd();
1523
+ const findings = [];
1524
+ let scannedFiles = 0;
1525
+ const gitignorePath = path8.join(cwd, ".gitignore");
1526
+ const hasGitignore = fs7.existsSync(gitignorePath);
1527
+ if (!hasGitignore) {
1528
+ console.log(chalk7.yellow(` ${ko.secure.noGitignore}`));
1529
+ console.log(chalk7.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
1530
+ } else {
1531
+ const gitignoreContent = fs7.readFileSync(gitignorePath, "utf-8");
1532
+ if (!gitignoreContent.includes(".env")) {
1533
+ console.log(chalk7.yellow(` ${ko.secure.noEnvInGitignore}`));
1534
+ console.log(chalk7.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
1535
+ }
1536
+ }
1537
+ console.log(chalk7.dim(` ${ko.secure.scanning}
1538
+ `));
1539
+ walkFiles2(cwd, (filePath) => {
1540
+ scannedFiles++;
1541
+ const content = fs7.readFileSync(filePath, "utf-8");
1542
+ const lines = content.split("\n");
1543
+ const relPath = path8.relative(cwd, filePath);
1544
+ for (const pattern of SECRET_PATTERNS) {
1545
+ lines.forEach((line, idx) => {
1546
+ const trimmed = line.trim();
1547
+ if (trimmed.startsWith("//") && trimmed.includes("example")) return;
1548
+ if (trimmed.startsWith("#") && trimmed.includes("example")) return;
1549
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
1550
+ let match;
1551
+ while ((match = regex.exec(line)) !== null) {
1552
+ findings.push({
1553
+ patternId: pattern.id,
1554
+ patternName: pattern.name,
1555
+ severity: pattern.severity,
1556
+ file: relPath,
1557
+ line: idx + 1,
1558
+ match: maskSecret(match[0])
1559
+ });
1560
+ }
1561
+ });
1562
+ }
1563
+ });
1564
+ console.log(chalk7.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC
1565
+ `));
1566
+ if (findings.length === 0) {
1567
+ console.log(chalk7.green.bold(` ${ko.secure.clean}`));
1568
+ console.log(chalk7.dim(" \uC815\uAE30\uC801\uC73C\uB85C vhk secure scan\uC744 \uC2E4\uD589\uD558\uC138\uC694.\n"));
1569
+ return;
1570
+ }
1571
+ const critical = findings.filter((f) => f.severity === "critical");
1572
+ const high = findings.filter((f) => f.severity === "high");
1573
+ const medium = findings.filter((f) => f.severity === "medium");
1574
+ if (critical.length > 0) {
1575
+ console.log(chalk7.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
1576
+ critical.forEach((f) => {
1577
+ console.log(chalk7.red(` \u2716 ${f.patternName}`));
1578
+ console.log(chalk7.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
1579
+ });
1580
+ console.log("");
1581
+ }
1582
+ if (high.length > 0) {
1583
+ console.log(chalk7.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
1584
+ high.forEach((f) => {
1585
+ console.log(chalk7.yellow(` \u26A0 ${f.patternName}`));
1586
+ console.log(chalk7.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
1587
+ });
1588
+ console.log("");
1589
+ }
1590
+ if (medium.length > 0) {
1591
+ console.log(chalk7.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
1592
+ medium.forEach((f) => {
1593
+ console.log(chalk7.blue(` \u2139 ${f.patternName}`));
1594
+ console.log(chalk7.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
1595
+ });
1596
+ console.log("");
1597
+ }
1598
+ console.log(chalk7.bold(` ${ko.secure.summary}`));
1599
+ console.log(` \uCD1D ${chalk7.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
1600
+ console.log("");
1601
+ console.log(chalk7.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
1602
+ console.log(chalk7.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
1603
+ console.log(chalk7.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
1604
+ console.log(chalk7.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
1605
+ if (critical.length > 0) {
1606
+ process.exitCode = 1;
1607
+ }
1608
+ }
1609
+ function walkFiles2(dir, callback) {
1610
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1611
+ for (const entry of entries) {
1612
+ const fullPath = path8.join(dir, entry.name);
1613
+ if (entry.isDirectory()) {
1614
+ if (!IGNORE_DIRS.includes(entry.name) && !entry.name.startsWith(".")) {
1615
+ walkFiles2(fullPath, callback);
1616
+ }
1617
+ } else {
1618
+ const ext = path8.extname(entry.name);
1619
+ if (SCAN_EXTENSIONS.includes(ext) || entry.name.startsWith(".env")) {
1620
+ callback(fullPath);
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1625
+
1626
+ // src/index.ts
1627
+ var program = new Command();
1628
+ program.name("vhk").description("Vibe Harness Kit \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD480\uC0AC\uC774\uD074 CLI").version("0.3.0");
1629
+ program.command("gate").description("Phase 0: \uC544\uC774\uB514\uC5B4 \uAC80\uC99D \u2192 GO/REFINE/DROP").action(gate);
1630
+ program.command("init").description("Phase 1-2: \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 + \uD558\uB124\uC2A4 \uD30C\uC77C \uC790\uB3D9 \uC0DD\uC131").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);
1631
+ program.command("recap").description("Phase 3: \uC138\uC158 \uAE30\uB85D + ADR/\uD2B8\uB7EC\uBE14\uC288\uD305 \uC790\uB3D9 \uBD84\uB9AC").option("--since <date>", "\uBD84\uC11D \uC2DC\uC791\uC77C (YYYY-MM-DD)").action(recap);
1632
+ program.command("sync").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
1633
+ program.command("check").description("RULES.md \uADDC\uCE59 \uB9B0\uD2B8 \u2014 \uCF54\uB4DC\uBCA0\uC774\uC2A4 \uC704\uBC18 \uAC80\uC0AC").action(check);
1634
+ var secureCmd = program.command("secure").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C");
1635
+ secureCmd.command("scan").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
1636
+ program.parse();