@byh3071/vhk 0.4.0 β†’ 0.5.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.
Files changed (3) hide show
  1. package/README.md +138 -28
  2. package/dist/index.js +589 -71
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,58 +1,168 @@
1
+ ---
2
+ id: vhk-readme
3
+ date: 2026-05-23
4
+ tags: [vhk, cli, readme, v0.4.0]
5
+ ---
6
+
1
7
  # πŸ”§ VHK β€” Vibe Harness Kit
2
8
 
3
- > AI μ½”λ”© μ—μ΄μ „νŠΈλ₯Ό λΆ€λ¦¬λŠ” μ‚¬λžŒμ„ μœ„ν•œ 풀사이클 CLI
9
+ > AI μ½”λ”© μ—μ΄μ „νŠΈλ₯Ό λΆ€λ¦¬λŠ” μ‚¬λžŒμ„ μœ„ν•œ **ν•œκ΅­μ–΄ 풀사이클 CLI** (v0.4.0)
10
+
11
+ λͺ…λ Ήμ–΄λ₯Ό μ™Έμš°μ§€ μ•Šμ•„λ„ λ©λ‹ˆλ‹€. `vhk`만 치면 메뉴가 λ‚˜μ˜€κ³ , ν•œκ΅­μ–΄λ‘œ 말해도 μ•Œμ•„λ“£μŠ΅λ‹ˆλ‹€.
4
12
 
5
13
  ## μ„€μΉ˜
6
14
 
7
15
  ```bash
8
- npm i -g vhk
16
+ npm install -g @byh3071/vhk
9
17
  ```
10
18
 
11
19
  ```bash
12
- # λ˜λŠ”
13
- npx vhk init
20
+ # ν•œ 번만 μ“Έ λ•Œ
21
+ npx @byh3071/vhk
22
+ ```
23
+
24
+ 둜컬 개발 쀑:
25
+
26
+ ```powershell
27
+ cd vhk-cli
28
+ pnpm install
29
+ pnpm build
30
+ pnpm link --global
31
+ vhk --version
32
+ ```
33
+
34
+ ## λΉ λ₯Έ μ‹œμž‘
35
+
36
+ ```bash
37
+ vhk
38
+ ```
39
+
40
+ 인자 없이 μ‹€ν–‰ν•˜λ©΄ **γ€Œλ­˜ λ„μ™€λ“œλ¦΄κΉŒμš”?」** 메뉴가 μ—΄λ¦½λ‹ˆλ‹€.
41
+
42
+ ```bash
43
+ # μžμ—°μ–΄λ‘œλ„ κ°€λŠ₯
44
+ vhk ν”„λ‘œμ νŠΈ λ§Œλ“€κ³  μ‹Άμ–΄
45
+ vhk 기획 끝났고 λ°”λ‘œ μ‹œμž‘
46
+ vhk 였늘 ν•œ 일 정리
47
+ vhk λ­”κ°€ μ•ˆ 돼
48
+ ```
49
+
50
+ ## μ›Œν¬ν”Œλ‘œμš° (ꢌμž₯ μˆœμ„œ)
51
+
52
+ ```text
53
+ vhk 검증 (gate) β†’ 아이디어 GO/닀듬기/λ‹€λ₯Έ 아이디어
54
+ vhk μ‹œμž‘ (init) β†’ ν•˜λ„€μŠ€ 파일 생성 (CLAUDE.md, PRD, ADR ν…œν”Œλ¦Ώ λ“±)
55
+ 개발 ...
56
+ vhk 정리 (recap) β†’ μ„Έμ…˜ 둜그 + ADR/νŠΈλŸ¬λΈ”μŠˆνŒ… μ œμ•ˆ
57
+ vhk 점검 (check) β†’ RULES.md κ·œμΉ™ 린트
58
+ vhk λ³΄μ•ˆ scan β†’ μ‹œν¬λ¦ΏΒ·ν‚€ 유좜 검사
59
+ vhk 배포 (ship) β†’ 배포 체크리슀트 + 회고 β†’ docs/build-log/
14
60
  ```
15
61
 
16
- ## μ»€λ§¨λ“œ
62
+ 기획이 이미 끝났닀면:
63
+
64
+ ```bash
65
+ vhk μ‹œμž‘ --skip-gate
66
+ # λ˜λŠ”
67
+ vhk 기획 끝났고 λ°”λ‘œ μ‹œμž‘
68
+ ```
17
69
 
18
- | μ»€λ§¨λ“œ | μ„€λͺ… |
70
+ ## 전체 μ»€λ§¨λ“œ
71
+
72
+ | μ˜μ–΄ | ν•œκ΅­μ–΄ 별칭 | μ„€λͺ… |
73
+ |------|-------------|------|
74
+ | `vhk` | β€” | μ‹œμž‘ 메뉴 (λͺ…λ Ή μ—†μŒ) |
75
+ | `vhk gate` | `검증`, `아이디어` | 아이디어 검증 (퀡 5λ¬Έν•­ / ν’€ 13λ¬Έν•­ / μŠ€ν‚΅) |
76
+ | `vhk init` | `μ‹œμž‘`, `λ§Œλ“€κΈ°` | ν”„λ‘œμ νŠΈ μ΄ˆκΈ°ν™” + ν•˜λ„€μŠ€ 생성 |
77
+ | `vhk recap` | `정리`, `였늘` | Git λ³€κ²½ β†’ `docs/log/` μ„Έμ…˜ 둜그 |
78
+ | `vhk sync` | `κ·œμΉ™`, `λ§žμΆ”κΈ°` | RULES.md β†’ `.cursorrules` + CLAUDE.md |
79
+ | `vhk check` | `점검`, `린트` | RULES.md κ·œμΉ™ μœ„λ°˜ 검사 |
80
+ | `vhk secure scan` | `λ³΄μ•ˆ`, `μŠ€μΊ”` | μ½”λ“œ λ‚΄ μ‹œν¬λ¦ΏΒ·ν‚€ νŒ¨ν„΄ μŠ€μΊ” |
81
+ | `vhk ship` | `배포`, `릴리즈` | 배포 체크리슀트 + 회고 + λΉŒλ“œ 둜그 |
82
+ | `vhk doctor` | `진단`, `ν™˜κ²½` | Node / npm / pnpm / Git ν™˜κ²½ 점검 |
83
+
84
+ ### init μ˜΅μ…˜
85
+
86
+ | μ˜΅μ…˜ | μ„€λͺ… |
87
+ |------|------|
88
+ | `--skip-gate` | 아이디어 검증(gate) μƒλž΅ |
89
+ | `--from-notion <url>` | Notion PRD νŽ˜μ΄μ§€μ—μ„œ import |
90
+ | `--name`, `--description`, `--type` | λΉ„λŒ€ν™”ν˜• μž…λ ₯ |
91
+ | `-y, --yes` | μŠ€νƒ 확인 μŠ€ν‚΅ |
92
+
93
+ ### recap μ˜΅μ…˜
94
+
95
+ | μ˜΅μ…˜ | μ„€λͺ… |
96
+ |------|------|
97
+ | `--since YYYY-MM-DD` | 뢄석 μ‹œμž‘μΌ (κΈ°λ³Έ: 였늘) |
98
+
99
+ ## v0.4.0 ν•˜μ΄λΌμ΄νŠΈ
100
+
101
+ | κΈ°λŠ₯ | μ„€λͺ… |
102
+ |------|------|
103
+ | **μ‹œμž‘ 메뉴** | `vhk`만 μž…λ ₯해도 λ‹€μŒ μž‘μ—… 선택 |
104
+ | **ν•œκ΅­μ–΄ 별칭** | `vhk 검증`, `vhk μ‹œμž‘`, `vhk 정리` λ“± |
105
+ | **μžμ—°μ–΄ λΌμš°νŒ…** | `vhk "ν”„λ‘œμ νŠΈ λ§Œλ“€κ³  μ‹Άμ–΄"` β†’ init μ‹€ν–‰ |
106
+ | **doctor** | Node / npm / pnpm / Git + ν”„λ‘œμ νŠΈ 파일 점검 |
107
+ | **ship** | 배포 μ „ 체크리슀트, 회고, `docs/build-log/` 생성 |
108
+ | **λ‹€μŒμ— μ΄κ²ƒλ§Œ ν•˜μ„Έμš”** | 각 λͺ…λ Ή 끝에 볡뢙 λͺ…λ Ή + Cursor 힌트 |
109
+ | **check / secure** | RULES 린트, μ‹œν¬λ¦Ώ μŠ€μΊ” (λŒ€ν˜• lockΒ·node_modules μ œμ™Έ) |
110
+
111
+ ## init이 λ§Œλ“œλŠ” 것 (μš”μ•½)
112
+
113
+ - `CLAUDE.md`, `.cursorrules`
114
+ - `docs/PRD.md`, `docs/ARCHITECTURE.md`
115
+ - `docs/adr/`, `docs/log/`, `docs/troubleshooting/`
116
+ - `COMMANDS.md`, `BACKLOG.md` (ν”„λ‘œμ νŠΈ μœ ν˜•μ— 따라)
117
+
118
+ ## μžμ—°μ–΄ μ˜ˆμ‹œ
119
+
120
+ | λ§ν•˜λ©΄ | μ‹€ν–‰ |
19
121
  |--------|------|
20
- | `vhk gate` | 아이디어 13단계 검증 β†’ GO/REFINE/DROP |
21
- | `vhk init` | ν”„λ‘œμ νŠΈ μ΄ˆκΈ°ν™” + ν•˜λ„€μŠ€ 파일 μžλ™ 생성 |
22
- | `vhk recap` | Git diff β†’ μ„Έμ…˜ 둜그 μžλ™ 생성 |
23
- | `vhk sync` | RULES.md β†’ `.cursorrules` + `CLAUDE.md` 동기화 |
122
+ | ν”„λ‘œμ νŠΈ λ§Œλ“€κ³  μ‹Άμ–΄ | `vhk μ‹œμž‘` |
123
+ | 기획 끝났고 λ°”λ‘œ μ‹œμž‘ | `vhk μ‹œμž‘ --skip-gate` |
124
+ | 였늘 ν•œ 일 정리 | `vhk 정리` |
125
+ | λ³΄μ•ˆ μŠ€μΊ” 돌렀 | `vhk λ³΄μ•ˆ scan` |
126
+ | λ°°ν¬ν•˜κ³  μ‹Άμ–΄ | `vhk 배포` |
127
+ | λ­”κ°€ μ•ˆ 돼 | `vhk doctor` |
24
128
 
25
129
  ## νŠΉμ§•
26
130
 
27
- - πŸ‡°πŸ‡· **ν•œκ΅­μ–΄ 퍼슀트** β€” 터미널 전체가 ν•œκ΅­μ–΄λ‘œ λ™μž‘
28
- - ⚑ **제둜 μ„€μ •** β€” `npx vhk init` ν•œ 방이면 끝
29
- - πŸ“ **둜컬 퍼슀트** β€” λ°μ΄ν„°λŠ” ν”„λ‘œμ νŠΈ 폴더에 μ €μž₯
30
- - πŸ• **독푸딩** β€” VHK μžμ²΄κ°€ VHK둜 λ§Œλ“€μ–΄μ§
131
+ - πŸ‡°πŸ‡· **ν•œκ΅­μ–΄ 퍼슀트** β€” μ§ˆλ¬ΈΒ·νŒμ •Β·λ‹€μŒ 단계 μ•ˆλ‚΄κ°€ ν•œκ΅­μ–΄
132
+ - πŸ—£οΈ **μžμ—°μ–΄ μΉœν™”** β€” λͺ…λ Ήμ–΄ λͺ°λΌλ„ λ¬Έμž₯으둜 μ‹œμž‘
133
+ - πŸ“ **둜컬 퍼슀트** β€” 둜그·ADRΒ·λΉŒλ“œ λ‘œκ·ΈλŠ” ν”„λ‘œμ νŠΈ 폴더에 μ €μž₯
134
+ - πŸ”’ **λ³΄μ•ˆ κΈ°λ³Έ** β€” `.gitignore`Β·μ‹œν¬λ¦Ώ μŠ€μΊ”Β·λ―Όκ° 파일 κ²½κ³ 
31
135
 
32
136
  ## μš”κ΅¬ 사항
33
137
 
34
138
  - Node.js >= 20
139
+ - Git (recapΒ·ship ꢌμž₯)
140
+
141
+ ## 개발
142
+
143
+ ```powershell
144
+ pnpm install
145
+ pnpm build
146
+ pnpm test --run
147
+ pnpm dev
148
+ pnpm dev 검증
149
+ ```
150
+
151
+ > Windows PowerShell 5.xμ—μ„œλŠ” `&&` λŒ€μ‹  `;` μ‚¬μš©: `pnpm build; pnpm test --run`
35
152
 
36
153
  ## λΌμ΄μ„ μŠ€
37
154
 
38
- MIT
155
+ MIT β€” [LICENSE](LICENSE)
39
156
 
40
157
  ## 배포 (maintainers)
41
158
 
42
159
  ```bash
43
- # 1. npm 둜그인 (졜초 1회)
44
160
  npm login
45
-
46
- # 2. λΉŒλ“œ + ν…ŒμŠ€νŠΈ
47
- pnpm build
48
- pnpm test
49
-
50
- # 3. 배포
51
- npm publish
52
-
53
- # 4. 확인
54
- npm info vhk
55
- npx vhk --help
161
+ pnpm run prepublishOnly
162
+ npm publish --access public
163
+ npm info @byh3071/vhk
56
164
  ```
57
165
 
58
- `package.json`의 `prepublishOnly` μŠ€ν¬λ¦½νŠΈκ°€ publish 전에 λΉŒλ“œΒ·ν…ŒμŠ€νŠΈλ₯Ό μžλ™ μ‹€ν–‰ν•©λ‹ˆλ‹€.
166
+ `prepublishOnly`κ°€ publish 전에 `pnpm build && pnpm test:run`을 μ‹€ν–‰ν•©λ‹ˆλ‹€.
167
+
168
+ Repository: https://github.com/byh3071-cpu/vhk
package/dist/index.js CHANGED
@@ -323,7 +323,7 @@ var require_ignore = __commonJS({
323
323
  // path matching.
324
324
  // - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
325
325
  // @returns {TestResult} true if a file is ignored
326
- test(path14, checkUnignored, mode) {
326
+ test(path15, checkUnignored, mode) {
327
327
  let ignored = false;
328
328
  let unignored = false;
329
329
  let matchedRule;
@@ -332,7 +332,7 @@ var require_ignore = __commonJS({
332
332
  if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
333
333
  return;
334
334
  }
335
- const matched = rule[mode].test(path14);
335
+ const matched = rule[mode].test(path15);
336
336
  if (!matched) {
337
337
  return;
338
338
  }
@@ -353,17 +353,17 @@ var require_ignore = __commonJS({
353
353
  var throwError = (message, Ctor) => {
354
354
  throw new Ctor(message);
355
355
  };
356
- var checkPath = (path14, originalPath, doThrow) => {
357
- if (!isString(path14)) {
356
+ var checkPath = (path15, originalPath, doThrow) => {
357
+ if (!isString(path15)) {
358
358
  return doThrow(
359
359
  `path must be a string, but got \`${originalPath}\``,
360
360
  TypeError
361
361
  );
362
362
  }
363
- if (!path14) {
363
+ if (!path15) {
364
364
  return doThrow(`path must not be empty`, TypeError);
365
365
  }
366
- if (checkPath.isNotRelative(path14)) {
366
+ if (checkPath.isNotRelative(path15)) {
367
367
  const r = "`path.relative()`d";
368
368
  return doThrow(
369
369
  `path should be a ${r} string, but got "${originalPath}"`,
@@ -372,7 +372,7 @@ var require_ignore = __commonJS({
372
372
  }
373
373
  return true;
374
374
  };
375
- var isNotRelative = (path14) => REGEX_TEST_INVALID_PATH.test(path14);
375
+ var isNotRelative = (path15) => REGEX_TEST_INVALID_PATH.test(path15);
376
376
  checkPath.isNotRelative = isNotRelative;
377
377
  checkPath.convert = (p) => p;
378
378
  var Ignore = class {
@@ -402,19 +402,19 @@ var require_ignore = __commonJS({
402
402
  }
403
403
  // @returns {TestResult}
404
404
  _test(originalPath, cache, checkUnignored, slices) {
405
- const path14 = originalPath && checkPath.convert(originalPath);
405
+ const path15 = originalPath && checkPath.convert(originalPath);
406
406
  checkPath(
407
- path14,
407
+ path15,
408
408
  originalPath,
409
409
  this._strictPathCheck ? throwError : RETURN_FALSE
410
410
  );
411
- return this._t(path14, cache, checkUnignored, slices);
411
+ return this._t(path15, cache, checkUnignored, slices);
412
412
  }
413
- checkIgnore(path14) {
414
- if (!REGEX_TEST_TRAILING_SLASH.test(path14)) {
415
- return this.test(path14);
413
+ checkIgnore(path15) {
414
+ if (!REGEX_TEST_TRAILING_SLASH.test(path15)) {
415
+ return this.test(path15);
416
416
  }
417
- const slices = path14.split(SLASH).filter(Boolean);
417
+ const slices = path15.split(SLASH).filter(Boolean);
418
418
  slices.pop();
419
419
  if (slices.length) {
420
420
  const parent = this._t(
@@ -427,18 +427,18 @@ var require_ignore = __commonJS({
427
427
  return parent;
428
428
  }
429
429
  }
430
- return this._rules.test(path14, false, MODE_CHECK_IGNORE);
430
+ return this._rules.test(path15, false, MODE_CHECK_IGNORE);
431
431
  }
432
- _t(path14, cache, checkUnignored, slices) {
433
- if (path14 in cache) {
434
- return cache[path14];
432
+ _t(path15, cache, checkUnignored, slices) {
433
+ if (path15 in cache) {
434
+ return cache[path15];
435
435
  }
436
436
  if (!slices) {
437
- slices = path14.split(SLASH).filter(Boolean);
437
+ slices = path15.split(SLASH).filter(Boolean);
438
438
  }
439
439
  slices.pop();
440
440
  if (!slices.length) {
441
- return cache[path14] = this._rules.test(path14, checkUnignored, MODE_IGNORE);
441
+ return cache[path15] = this._rules.test(path15, checkUnignored, MODE_IGNORE);
442
442
  }
443
443
  const parent = this._t(
444
444
  slices.join(SLASH) + SLASH,
@@ -446,29 +446,29 @@ var require_ignore = __commonJS({
446
446
  checkUnignored,
447
447
  slices
448
448
  );
449
- return cache[path14] = parent.ignored ? parent : this._rules.test(path14, checkUnignored, MODE_IGNORE);
449
+ return cache[path15] = parent.ignored ? parent : this._rules.test(path15, checkUnignored, MODE_IGNORE);
450
450
  }
451
- ignores(path14) {
452
- return this._test(path14, this._ignoreCache, false).ignored;
451
+ ignores(path15) {
452
+ return this._test(path15, this._ignoreCache, false).ignored;
453
453
  }
454
454
  createFilter() {
455
- return (path14) => !this.ignores(path14);
455
+ return (path15) => !this.ignores(path15);
456
456
  }
457
457
  filter(paths) {
458
458
  return makeArray(paths).filter(this.createFilter());
459
459
  }
460
460
  // @returns {TestResult}
461
- test(path14) {
462
- return this._test(path14, this._testCache, true);
461
+ test(path15) {
462
+ return this._test(path15, this._testCache, true);
463
463
  }
464
464
  };
465
465
  var factory = (options) => new Ignore(options);
466
- var isPathValid = (path14) => checkPath(path14 && checkPath.convert(path14), path14, RETURN_FALSE);
466
+ var isPathValid = (path15) => checkPath(path15 && checkPath.convert(path15), path15, RETURN_FALSE);
467
467
  var setupWindows = () => {
468
468
  const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
469
469
  checkPath.convert = makePosix;
470
470
  const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
471
- checkPath.isNotRelative = (path14) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path14) || isNotRelative(path14);
471
+ checkPath.isNotRelative = (path15) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path15) || isNotRelative(path15);
472
472
  };
473
473
  if (
474
474
  // Detect `process` so that it can run in browsers.
@@ -485,75 +485,110 @@ var require_ignore = __commonJS({
485
485
 
486
486
  // src/index.ts
487
487
  import { Command, Help } from "commander";
488
- import chalk11 from "chalk";
489
- import inquirer5 from "inquirer";
488
+ import chalk15 from "chalk";
489
+ import inquirer7 from "inquirer";
490
490
 
491
491
  // src/lib/nlp-router.ts
492
492
  function normalize(input) {
493
493
  return input.trim().toLowerCase().replace(/\s+/g, " ");
494
494
  }
495
+ var NLP_KEYWORDS = {
496
+ save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
497
+ undo: ["\uB418\uB3CC\uB824", "\uB418\uB3CC\uB9AC\uAE30", "\uCDE8\uC18C", "\uC6D0\uB798\uB300\uB85C", "\uB864\uBC31", "\uB9AC\uC14B", "reset", "rollback"],
498
+ status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08", "\uD655\uC778"],
499
+ diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
500
+ };
501
+ function matchesKeywords(text, command) {
502
+ const keywords = NLP_KEYWORDS[command];
503
+ if (!keywords) return false;
504
+ return keywords.some((kw) => text.includes(kw.toLowerCase()));
505
+ }
495
506
  var RULES = [
496
507
  {
497
508
  command: "init",
498
509
  explanation: "\uAC80\uC99D \uC2A4\uD0B5\uD558\uACE0 \uBC14\uB85C \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --skip-gate)",
499
510
  confidence: "high",
500
511
  args: ["--skip-gate"],
501
- test: (t) => /기획.*(끝|μ™„λ£Œ)|λ…Έμ…˜.*(기획|μ™„λ£Œ)|검증.*(μŠ€ν‚΅|κ±΄λ„ˆ)|gate.*(μŠ€ν‚΅|κ±΄λ„ˆ)|λ°”λ‘œ.*μ‹œμž‘/.test(t)
512
+ test: (t2) => /기획.*(끝|μ™„λ£Œ)|λ…Έμ…˜.*(기획|μ™„λ£Œ)|검증.*(μŠ€ν‚΅|κ±΄λ„ˆ)|gate.*(μŠ€ν‚΅|κ±΄λ„ˆ)|λ°”λ‘œ.*μ‹œμž‘/.test(t2)
502
513
  },
503
514
  {
504
515
  command: "init",
505
516
  explanation: "Notion\uC5D0\uC11C \uAC00\uC838\uC640 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791 --from-notion)",
506
517
  confidence: "low",
507
518
  args: ["--from-notion"],
508
- test: (t) => /λ…Έμ…˜|notion/.test(t) && /(μ‹œμž‘|λ§Œλ“€|import|κ°€μ Έ)/.test(t)
519
+ test: (t2) => /λ…Έμ…˜|notion/.test(t2) && /(μ‹œμž‘|λ§Œλ“€|import|κ°€μ Έ)/.test(t2)
509
520
  },
510
521
  {
511
522
  command: "init",
512
523
  explanation: "\uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 (vhk \uC2DC\uC791)",
513
524
  confidence: "high",
514
- test: (t) => /ν”„λ‘œμ νŠΈ.*(λ§Œλ“€|μ‹œμž‘)|폴더.*λ§Œλ“€|λ§Œλ“€κ³ \s*μ‹Ά|ν•˜λ„€μŠ€|μ΄ˆκΈ°ν™”/.test(t) || /^μ‹œμž‘$/.test(t)
525
+ test: (t2) => /ν”„λ‘œμ νŠΈ.*(λ§Œλ“€|μ‹œμž‘)|폴더.*λ§Œλ“€|λ§Œλ“€κ³ \s*μ‹Ά|ν•˜λ„€μŠ€|μ΄ˆκΈ°ν™”/.test(t2) || /^μ‹œμž‘$/.test(t2)
526
+ },
527
+ {
528
+ command: "diff",
529
+ explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
530
+ confidence: "high",
531
+ test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /변경사항|μˆ˜μ •\s*λ‚΄μ—­|차이\s*보/.test(t2)) && !/μ €μž₯|컀밋|push|ν‘Έμ‹œ|μƒνƒœ|ν˜„ν™©|μ„Έμ΄λΈŒ|commit/.test(t2)
532
+ },
533
+ {
534
+ command: "undo",
535
+ explanation: "\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30 (vhk \uB418\uB3CC\uB9AC\uAE30)",
536
+ confidence: "high",
537
+ test: (t2) => matchesKeywords(t2, "undo") || /undo|컀밋\s*μ·¨/.test(t2)
538
+ },
539
+ {
540
+ command: "status",
541
+ explanation: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778 (vhk \uC0C1\uD0DC)",
542
+ confidence: "high",
543
+ test: (t2) => matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|μ–΄λ””)|git\s*μƒνƒœ|동기화\s*μƒνƒœ/.test(t2)
544
+ },
545
+ {
546
+ command: "save",
547
+ explanation: "Git\uC5D0 \uC800\uC7A5 (vhk \uC800\uC7A5)",
548
+ confidence: "high",
549
+ test: (t2) => (matchesKeywords(t2, "save") || /κΉƒν—ˆλΈŒ|github/.test(t2)) && !/정리|recap|되돌|μ·¨μ†Œ|rollback|reset|리셋|λ‘€λ°±|μ›λž˜λŒ€λ‘œ/.test(t2)
515
550
  },
516
551
  {
517
552
  command: "recap",
518
553
  explanation: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC (vhk \uC815\uB9AC)",
519
554
  confidence: "high",
520
- test: (t) => /였늘.*(정리|기둝)|ν•œ\s*일|μ„Έμ…˜|회고|recap|정리해/.test(t)
555
+ test: (t2) => /였늘.*(정리|기둝)|ν•œ\s*일|μ„Έμ…˜|회고|recap|정리해/.test(t2)
521
556
  },
522
557
  {
523
558
  command: "doctor",
524
559
  explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
525
560
  confidence: "high",
526
- test: (t) => /λ­”κ°€\s*μ•ˆ|μ•ˆ\s*돼|μ•ˆλΌ|ν™˜κ²½\s*(점검|진단)|진단|doctor|μ„€μΉ˜.*확인|μ™œ\s*μ•ˆ/.test(t)
561
+ test: (t2) => /λ­”κ°€\s*μ•ˆ|μ•ˆ\s*돼|μ•ˆλΌ|ν™˜κ²½\s*(점검|진단)|진단|doctor|μ„€μΉ˜.*확인|μ™œ\s*μ•ˆ/.test(t2)
527
562
  },
528
563
  {
529
564
  command: "gate",
530
565
  explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
531
566
  confidence: "high",
532
- test: (t) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t)
567
+ test: (t2) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t2)
533
568
  },
534
569
  {
535
570
  command: "secure",
536
571
  explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548 scan)",
537
572
  confidence: "high",
538
- test: (t) => /λ³΄μ•ˆ|μ‹œν¬λ¦Ώ|λΉ„λ°€|ν‚€\s*유좜|secure|scan/.test(t)
573
+ test: (t2) => /λ³΄μ•ˆ|μ‹œν¬λ¦Ώ|λΉ„λ°€|ν‚€\s*유좜|secure|scan/.test(t2)
539
574
  },
540
575
  {
541
576
  command: "check",
542
577
  explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
543
578
  confidence: "high",
544
- test: (t) => /κ·œμΉ™.*(점검|μœ„λ°˜)|린트|check|μœ„λ°˜/.test(t)
579
+ test: (t2) => /κ·œμΉ™.*(점검|μœ„λ°˜)|린트|check|μœ„λ°˜/.test(t2)
545
580
  },
546
581
  {
547
582
  command: "sync",
548
583
  explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
549
584
  confidence: "high",
550
- test: (t) => /κ·œμΉ™.*(맞|동기)|sync|cursorrules|claude\.md.*맞/.test(t)
585
+ test: (t2) => /κ·œμΉ™.*(맞|동기)|sync|cursorrules|claude\.md.*맞/.test(t2)
551
586
  },
552
587
  {
553
588
  command: "ship",
554
589
  explanation: "\uBC30\uD3EC \uCCB4\uD06C + \uD68C\uACE0 (vhk ship)",
555
590
  confidence: "high",
556
- test: (t) => /배포|μΆœμ‹œ|릴리슀|ship|λΉŒλ“œ\s*μ „/.test(t)
591
+ test: (t2) => /배포|μΆœμ‹œ|릴리슀|ship|λΉŒλ“œ\s*μ „/.test(t2)
557
592
  }
558
593
  ];
559
594
  function routeNaturalLanguage(input) {
@@ -578,6 +613,60 @@ function extractNotionUrl(input) {
578
613
 
579
614
  // src/i18n/ko.ts
580
615
  var ko = {
616
+ status: {
617
+ title: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC",
618
+ notGitRepo: "Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2C8\uC5D0\uC694. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
619
+ branch: "\uBE0C\uB79C\uCE58:",
620
+ changes: "\uBCC0\uACBD:",
621
+ recentCommits: "\uCD5C\uADFC \uCEE4\uBC0B (3):",
622
+ noCommits: "\uCEE4\uBC0B \uC5C6\uC74C",
623
+ remote: "\uC6D0\uACA9:",
624
+ noUpstream: "upstream \uC5C6\uC74C",
625
+ inSync: "\uB3D9\uAE30\uD654\uB428",
626
+ ahead: (n) => `\u2191${n} ahead`,
627
+ behind: (n) => `\u2193${n} behind`,
628
+ package: "package.json:",
629
+ noPackage: "package.json \uC5C6\uC74C",
630
+ detached: "(detached HEAD)",
631
+ unknownBranch: "(\uC54C \uC218 \uC5C6\uC74C)"
632
+ },
633
+ save: {
634
+ title: "\uC800\uC7A5\uD558\uAE30",
635
+ notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
636
+ noChanges: "\uC800\uC7A5\uD560 \uBCC0\uACBD\uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
637
+ filesHeader: (n) => `\uBCC0\uACBD\uB41C \uD30C\uC77C (${n}\uAC1C):`,
638
+ commitMessage: "\uCEE4\uBC0B \uBA54\uC2DC\uC9C0 (Enter\uB85C \uAE30\uBCF8\uAC12 \uC0AC\uC6A9):",
639
+ saving: "\uC800\uC7A5 \uC911...",
640
+ pushing: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uC5D0 \uC62C\uB9AC\uB294 \uC911...",
641
+ successWithPush: "\uC800\uC7A5 + \uC6D0\uACA9 \uC5C5\uB85C\uB4DC \uC644\uB8CC!",
642
+ successLocal: "\uB85C\uCEEC \uC800\uC7A5 \uC644\uB8CC!",
643
+ noRemote: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 push\uB97C \uAC74\uB108\uB6F0\uC5C8\uC2B5\uB2C8\uB2E4.",
644
+ failed: "\uC800\uC7A5 \uC2E4\uD328",
645
+ done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`
646
+ },
647
+ undo: {
648
+ title: "\uB418\uB3CC\uB9AC\uAE30",
649
+ notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
650
+ noCommits: "\uB418\uB3CC\uB9B4 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.",
651
+ recentHeader: "\u{1F4CB} \uCD5C\uADFC \uCEE4\uBC0B:",
652
+ howMany: "\uBA87 \uAC1C\uC758 \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9B4\uAE4C\uC694?",
653
+ alreadyPushed: "\uC774 \uCEE4\uBC0B\uC740 \uC774\uBBF8 \uC6D0\uACA9\uC5D0 \uC62C\uB77C\uAC14\uC2B5\uB2C8\uB2E4. \uB418\uB3CC\uB9AC\uBA74 \uCDA9\uB3CC\uC774 \uC0DD\uAE38 \uC218 \uC788\uC5B4\uC694.",
654
+ confirmMessage: "\uCD5C\uADFC \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9AC\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
655
+ cancelled: "\uCDE8\uC18C\uB428",
656
+ success: "\uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC! \uBCC0\uACBD\uC0AC\uD56D\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.",
657
+ stagedHint: "\uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uB0A8\uC544 \uC788\uC5B4\uC694.",
658
+ failed: "\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328"
659
+ },
660
+ diff: {
661
+ title: "\uBCC0\uACBD\uC0AC\uD56D \uD655\uC778",
662
+ notGitRepo: "git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4.",
663
+ noChanges: "\uBCC0\uACBD\uC0AC\uD56D \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
664
+ stagedHeader: "\u{1F4E6} \uCEE4\uBC0B \uB300\uAE30 (staged):",
665
+ unstagedHeader: "\u270F\uFE0F \uC218\uC815\uB428 (unstaged):",
666
+ untrackedHeader: (n) => `\u2795 \uC0C8 \uD30C\uC77C (${n}\uAC1C):`,
667
+ summaryHeader: "\u{1F4CA} \uCD1D \uBCC0\uACBD \uC694\uC57D (\uC791\uC5C5 \uD2B8\uB9AC vs HEAD)",
668
+ filesLine: (n) => `\uD30C\uC77C: ${n}\uAC1C`
669
+ },
581
670
  start: {
582
671
  title: "\u{1F527} VHK \u2014 \uBB34\uC5C7\uC744 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
583
672
  subtitle: "\uBC88\uD638\uB9CC \uACE0\uB974\uBA74 \uB429\uB2C8\uB2E4. \uBA85\uB839\uC5B4\uB97C \uC678\uC6B8 \uD544\uC694 \uC5C6\uC5B4\uC694.",
@@ -749,6 +838,23 @@ var ko = {
749
838
  hintCommit: "git status \uD655\uC778"
750
839
  }
751
840
  };
841
+ function lookup(path15) {
842
+ const parts = path15.split(".");
843
+ let cur = ko;
844
+ for (const part of parts) {
845
+ if (cur === null || typeof cur !== "object") return void 0;
846
+ cur = cur[part];
847
+ }
848
+ return cur;
849
+ }
850
+ function t(key, ...args) {
851
+ const value = lookup(key);
852
+ if (typeof value === "function") {
853
+ return value(...args);
854
+ }
855
+ if (typeof value === "string") return value;
856
+ return key;
857
+ }
752
858
 
753
859
  // src/commands/gate.ts
754
860
  import inquirer from "inquirer";
@@ -861,7 +967,7 @@ ${ko.gate.checklistStart}
861
967
  name: "answer",
862
968
  message: `[${i + 1}/${total}] ${q.stage}: ${q.question}`
863
969
  }]);
864
- const { status } = await inquirer.prompt([{
970
+ const { status: status2 } = await inquirer.prompt([{
865
971
  type: "list",
866
972
  name: "status",
867
973
  message: ko.gate.verdictPrompt(q.failIf),
@@ -871,10 +977,10 @@ ${ko.gate.checklistStart}
871
977
  { name: ko.gate.statusFailChoice, value: "fail" }
872
978
  ]
873
979
  }]);
874
- if (status === "fail") failCount++;
875
- if (status === "hold") holdCount++;
876
- results.push({ id: q.id, stage: q.stage, status, answer });
877
- const icon = status === "pass" ? chalk2.green(ko.gate.statusPassLine) : status === "hold" ? chalk2.yellow(ko.gate.statusHoldLine) : chalk2.red(ko.gate.statusFailLine);
980
+ if (status2 === "fail") failCount++;
981
+ if (status2 === "hold") holdCount++;
982
+ results.push({ id: q.id, stage: q.stage, status: status2, answer });
983
+ const icon = status2 === "pass" ? chalk2.green(ko.gate.statusPassLine) : status2 === "hold" ? chalk2.yellow(ko.gate.statusHoldLine) : chalk2.red(ko.gate.statusFailLine);
878
984
  console.log(icon);
879
985
  }
880
986
  console.log(chalk2.bold(`
@@ -1176,7 +1282,7 @@ function extractPageId(url) {
1176
1282
  function getPageTitle(page) {
1177
1283
  for (const prop of Object.values(page.properties)) {
1178
1284
  if (prop.type === "title") {
1179
- return prop.title.map((t) => t.plain_text).join("");
1285
+ return prop.title.map((t2) => t2.plain_text).join("");
1180
1286
  }
1181
1287
  }
1182
1288
  return "Untitled";
@@ -1206,7 +1312,7 @@ function extractText(block) {
1206
1312
  const type = block.type;
1207
1313
  const data = block[type];
1208
1314
  if (!data?.rich_text) return "";
1209
- return data.rich_text.map((t) => t.plain_text).join("");
1315
+ return data.rich_text.map((t2) => t2.plain_text).join("");
1210
1316
  }
1211
1317
  function parseBlocks(blocks) {
1212
1318
  const sections = {};
@@ -1298,7 +1404,7 @@ var PROJECT_TYPES = [
1298
1404
  { name: "\u{1F916} \uB178\uC158 \uD1B5\uD569/MCP \uC11C\uBC84", value: "notion" },
1299
1405
  { name: "\u{1F4F1} \uBAA8\uBC14\uC77C \uC571 (Flutter)", value: "mobile" }
1300
1406
  ];
1301
- var VALID_TYPES = PROJECT_TYPES.map((t) => t.value);
1407
+ var VALID_TYPES = PROJECT_TYPES.map((t2) => t2.value);
1302
1408
  var STACK_PRESETS = {
1303
1409
  webapp: ["Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "Supabase", "Vercel"],
1304
1410
  extension: ["Vite", "TypeScript", "@crxjs/vite-plugin", "Chrome Extension Manifest V3"],
@@ -1621,10 +1727,10 @@ var ADR_RULES = [
1621
1727
  test: (f) => /\.env\.example$|auth\/|middleware\.(ts|js)$/.test(f)
1622
1728
  }
1623
1729
  ];
1624
- function detectAdrCandidates(diff) {
1730
+ function detectAdrCandidates(diff2) {
1625
1731
  const candidates = [];
1626
1732
  for (const rule of ADR_RULES) {
1627
- const matched = diff.files.map((f) => f.file).filter(rule.test);
1733
+ const matched = diff2.files.map((f) => f.file).filter(rule.test);
1628
1734
  if (matched.length > 0) {
1629
1735
  candidates.push({
1630
1736
  title: rule.title,
@@ -1691,23 +1797,23 @@ ${ko.recap.title}
1691
1797
  console.log(chalk5.dim(`${ko.recap.analyzing}
1692
1798
  `));
1693
1799
  const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1694
- const diff = await getSessionDiff(since);
1800
+ const diff2 = await getSessionDiff(since);
1695
1801
  const commits = await getRecentCommits(10, since);
1696
- if (diff.filesChanged === 0 && commits.length === 0) {
1802
+ if (diff2.filesChanged === 0 && commits.length === 0) {
1697
1803
  console.log(chalk5.yellow(ko.recap.noChanges));
1698
1804
  return;
1699
1805
  }
1700
1806
  console.log(chalk5.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
1701
- console.log(` \uD30C\uC77C: ${chalk5.cyan(String(diff.filesChanged))}\uAC1C \uBCC0\uACBD`);
1702
- console.log(` \uCD94\uAC00: ${chalk5.green("+" + diff.insertions)} / \uC0AD\uC81C: ${chalk5.red("-" + diff.deletions)}`);
1703
- if (diff.files.length > 0) {
1807
+ console.log(` \uD30C\uC77C: ${chalk5.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
1808
+ console.log(` \uCD94\uAC00: ${chalk5.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk5.red("-" + diff2.deletions)}`);
1809
+ if (diff2.files.length > 0) {
1704
1810
  console.log(chalk5.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
1705
- diff.files.slice(0, 15).forEach((f) => {
1811
+ diff2.files.slice(0, 15).forEach((f) => {
1706
1812
  const icon = f.status === "new" ? chalk5.green("\u{1F195}") : f.status === "deleted" ? chalk5.red("\u{1F5D1}\uFE0F") : chalk5.yellow("\u270F\uFE0F");
1707
1813
  console.log(` ${icon} ${f.file}`);
1708
1814
  });
1709
- if (diff.files.length > 15) {
1710
- console.log(chalk5.dim(` ... \uC678 ${diff.files.length - 15}\uAC1C`));
1815
+ if (diff2.files.length > 15) {
1816
+ console.log(chalk5.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
1711
1817
  }
1712
1818
  }
1713
1819
  if (commits.length > 0) {
@@ -1748,7 +1854,7 @@ ${ko.recap.title}
1748
1854
  const sessionNum = existing.length + 1;
1749
1855
  const fileName = `${today}-session-${sessionNum}.md`;
1750
1856
  const filePath = path6.join(logDir, fileName);
1751
- const fileList = diff.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
1857
+ const fileList = diff2.files.map((f) => `| ${f.file} | ${f.status} |`).join("\n");
1752
1858
  const commitList = commits.slice(0, 10).map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
1753
1859
  const content = [
1754
1860
  `# \uC138\uC158 \uB85C\uADF8 \u2014 ${today} #${sessionNum}`,
@@ -1766,7 +1872,7 @@ ${ko.recap.title}
1766
1872
  answers.blockers,
1767
1873
  "",
1768
1874
  "## \uBCC0\uACBD \uD30C\uC77C",
1769
- `\uCD1D ${diff.filesChanged}\uAC1C \uD30C\uC77C (+${diff.insertions} -${diff.deletions})`,
1875
+ `\uCD1D ${diff2.filesChanged}\uAC1C \uD30C\uC77C (+${diff2.insertions} -${diff2.deletions})`,
1770
1876
  "",
1771
1877
  "| \uD30C\uC77C | \uC0C1\uD0DC |",
1772
1878
  "|------|------|",
@@ -1779,7 +1885,7 @@ ${ko.recap.title}
1779
1885
  `*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
1780
1886
  ].join("\n");
1781
1887
  fs5.writeFileSync(filePath, content, "utf-8");
1782
- const adrCandidates = detectAdrCandidates(diff);
1888
+ const adrCandidates = detectAdrCandidates(diff2);
1783
1889
  if (adrCandidates.length > 0) {
1784
1890
  console.log(chalk5.cyan.bold(`
1785
1891
  ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
@@ -2732,6 +2838,384 @@ ${ko.ship.title}
2732
2838
  });
2733
2839
  }
2734
2840
 
2841
+ // src/commands/save.ts
2842
+ import { execFileSync, execSync as execSync2 } from "child_process";
2843
+ import chalk11 from "chalk";
2844
+ import ora from "ora";
2845
+ import inquirer5 from "inquirer";
2846
+ function gitOut(args) {
2847
+ return execFileSync("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2848
+ }
2849
+ function gitRun(args) {
2850
+ execFileSync("git", args, { stdio: "pipe" });
2851
+ }
2852
+ function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
2853
+ const y = date.getFullYear();
2854
+ const m = String(date.getMonth() + 1).padStart(2, "0");
2855
+ const d = String(date.getDate()).padStart(2, "0");
2856
+ const h = String(date.getHours()).padStart(2, "0");
2857
+ const min = String(date.getMinutes()).padStart(2, "0");
2858
+ return `\u2728 vhk save: ${y}-${m}-${d} ${h}:${min}`;
2859
+ }
2860
+ function statusIcon(code) {
2861
+ if (code.includes("M")) return "\u270F\uFE0F";
2862
+ if (code.includes("A") || code.includes("?")) return "\u2795";
2863
+ if (code.includes("D")) return "\u{1F5D1}\uFE0F";
2864
+ return "\u{1F4C4}";
2865
+ }
2866
+ async function save() {
2867
+ console.log(chalk11.bold(`
2868
+ \u{1F4BE} ${t("save.title")}`));
2869
+ console.log(chalk11.gray("\u2500".repeat(40)));
2870
+ try {
2871
+ execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
2872
+ } catch {
2873
+ console.log(chalk11.red(`\u274C ${t("save.notGitRepo")}`));
2874
+ return;
2875
+ }
2876
+ const status2 = gitOut(["status", "--porcelain"]).trim();
2877
+ if (!status2) {
2878
+ console.log(chalk11.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
2879
+ return;
2880
+ }
2881
+ const files = status2.split("\n").filter(Boolean);
2882
+ console.log(chalk11.cyan(`
2883
+ \u{1F4CB} ${t("save.filesHeader", files.length)}`));
2884
+ files.forEach((line) => {
2885
+ const code = line.substring(0, 2);
2886
+ const name = line.substring(3);
2887
+ console.log(` ${statusIcon(code)} ${name}`);
2888
+ });
2889
+ const { message } = await inquirer5.prompt([{
2890
+ type: "input",
2891
+ name: "message",
2892
+ message: t("save.commitMessage"),
2893
+ default: formatDefaultCommitMessage()
2894
+ }]);
2895
+ const spinner = ora(t("save.saving")).start();
2896
+ try {
2897
+ gitRun(["add", "."]);
2898
+ gitRun(["commit", "-m", message]);
2899
+ spinner.text = t("save.pushing");
2900
+ try {
2901
+ gitRun(["push"]);
2902
+ spinner.succeed(t("save.successWithPush"));
2903
+ } catch {
2904
+ spinner.succeed(t("save.successLocal"));
2905
+ console.log(chalk11.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
2906
+ }
2907
+ console.log(chalk11.green(`
2908
+ \u2705 ${t("save.done", files.length)}`));
2909
+ } catch (err) {
2910
+ spinner.fail(t("save.failed"));
2911
+ const msg = err instanceof Error ? err.message : String(err);
2912
+ console.log(chalk11.red(msg));
2913
+ process.exitCode = 1;
2914
+ }
2915
+ }
2916
+
2917
+ // src/commands/undo.ts
2918
+ import { execFileSync as execFileSync2, execSync as execSync3 } from "child_process";
2919
+ import chalk12 from "chalk";
2920
+ import inquirer6 from "inquirer";
2921
+ function gitOut2(args) {
2922
+ return execFileSync2("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2923
+ }
2924
+ function gitRun2(args) {
2925
+ execFileSync2("git", args, { stdio: "pipe" });
2926
+ }
2927
+ function parseRecentCommits(logOutput) {
2928
+ return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
2929
+ }
2930
+ function countUnpushedCommits() {
2931
+ try {
2932
+ const out = gitOut2(["rev-list", "--count", "@{u}..HEAD"]).trim();
2933
+ return parseInt(out, 10) || 0;
2934
+ } catch {
2935
+ return -1;
2936
+ }
2937
+ }
2938
+ function willUndoPushedCommits(undoCount, unpushedCount) {
2939
+ if (unpushedCount < 0) return false;
2940
+ return undoCount > unpushedCount;
2941
+ }
2942
+ async function undo() {
2943
+ console.log(chalk12.bold(`
2944
+ \u23EA ${t("undo.title")}`));
2945
+ console.log(chalk12.gray("\u2500".repeat(40)));
2946
+ try {
2947
+ execSync3("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
2948
+ } catch {
2949
+ console.log(chalk12.red(`\u274C ${t("undo.notGitRepo")}`));
2950
+ return;
2951
+ }
2952
+ let logOutput;
2953
+ try {
2954
+ logOutput = gitOut2(["log", "--oneline", "-5"]).trim();
2955
+ } catch {
2956
+ console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2957
+ return;
2958
+ }
2959
+ const commits = parseRecentCommits(logOutput);
2960
+ if (commits.length === 0) {
2961
+ console.log(chalk12.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
2962
+ return;
2963
+ }
2964
+ console.log(chalk12.cyan(`
2965
+ ${t("undo.recentHeader")}`));
2966
+ commits.forEach((c, i) => {
2967
+ console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
2968
+ });
2969
+ const maxUndo = commits.length;
2970
+ const { count } = await inquirer6.prompt([{
2971
+ type: "number",
2972
+ name: "count",
2973
+ message: t("undo.howMany"),
2974
+ default: 1,
2975
+ min: 1,
2976
+ max: maxUndo
2977
+ }]);
2978
+ const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
2979
+ const unpushed = countUnpushedCommits();
2980
+ if (willUndoPushedCommits(undoCount, unpushed)) {
2981
+ console.log(chalk12.red(`
2982
+ \u26A0\uFE0F ${t("undo.alreadyPushed")}`));
2983
+ }
2984
+ const { confirm } = await inquirer6.prompt([{
2985
+ type: "confirm",
2986
+ name: "confirm",
2987
+ message: t("undo.confirmMessage", undoCount),
2988
+ default: false
2989
+ }]);
2990
+ if (!confirm) {
2991
+ console.log(chalk12.gray(t("undo.cancelled")));
2992
+ return;
2993
+ }
2994
+ try {
2995
+ gitRun2(["reset", "--soft", `HEAD~${undoCount}`]);
2996
+ console.log(chalk12.green(`
2997
+ \u2705 ${t("undo.success", undoCount)}`));
2998
+ console.log(chalk12.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
2999
+ } catch (err) {
3000
+ console.log(chalk12.red(`\u274C ${t("undo.failed")}`));
3001
+ const msg = err instanceof Error ? err.message : String(err);
3002
+ console.log(chalk12.red(msg));
3003
+ process.exitCode = 1;
3004
+ }
3005
+ }
3006
+
3007
+ // src/commands/diff.ts
3008
+ import { execFileSync as execFileSync3, execSync as execSync4 } from "child_process";
3009
+ import chalk13 from "chalk";
3010
+ function gitOut3(args) {
3011
+ try {
3012
+ return execFileSync3("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3013
+ } catch {
3014
+ return "";
3015
+ }
3016
+ }
3017
+ function parseDiffStat(stat) {
3018
+ const files = [];
3019
+ const lines = stat.split("\n");
3020
+ for (const line of lines) {
3021
+ const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
3022
+ if (!match) continue;
3023
+ const name = match[1].trim();
3024
+ if (name.includes("changed") || name.includes("file")) continue;
3025
+ const plusMatch = line.match(/(\++)/);
3026
+ const minusMatch = line.match(/(\-+)/);
3027
+ files.push({
3028
+ name,
3029
+ additions: plusMatch ? plusMatch[1].length : 0,
3030
+ deletions: minusMatch ? minusMatch[1].length : 0
3031
+ });
3032
+ }
3033
+ return files;
3034
+ }
3035
+ function summarizeNumstat(numstat) {
3036
+ let totalAdd = 0;
3037
+ let totalDel = 0;
3038
+ let fileCount = 0;
3039
+ for (const line of numstat.split("\n").filter(Boolean)) {
3040
+ const [add, del] = line.split(" ");
3041
+ if (add === void 0 || del === void 0) continue;
3042
+ totalAdd += parseInt(add, 10) || 0;
3043
+ totalDel += parseInt(del, 10) || 0;
3044
+ fileCount++;
3045
+ }
3046
+ return { fileCount, totalAdd, totalDel };
3047
+ }
3048
+ function printFile(f) {
3049
+ const adds = f.additions > 0 ? chalk13.green(`+${f.additions}`) : "";
3050
+ const dels = f.deletions > 0 ? chalk13.red(`-${f.deletions}`) : "";
3051
+ const change = [adds, dels].filter(Boolean).join(" ");
3052
+ console.log(` ${f.name} ${change}`);
3053
+ }
3054
+ async function diff() {
3055
+ console.log(chalk13.bold(`
3056
+ \u{1F50D} ${t("diff.title")}`));
3057
+ console.log(chalk13.gray("\u2500".repeat(40)));
3058
+ try {
3059
+ execSync4("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3060
+ } catch {
3061
+ console.log(chalk13.red(`\u274C ${t("diff.notGitRepo")}`));
3062
+ return;
3063
+ }
3064
+ const unstaged = gitOut3(["diff", "--stat"]);
3065
+ const staged = gitOut3(["diff", "--cached", "--stat"]);
3066
+ const untracked = gitOut3(["ls-files", "--others", "--exclude-standard"]);
3067
+ if (!unstaged && !staged && !untracked) {
3068
+ console.log(chalk13.green(`
3069
+ \u2705 ${t("diff.noChanges")}`));
3070
+ return;
3071
+ }
3072
+ if (staged) {
3073
+ console.log(chalk13.cyan(`
3074
+ ${t("diff.stagedHeader")}`));
3075
+ parseDiffStat(staged).forEach((f) => printFile(f));
3076
+ }
3077
+ if (unstaged) {
3078
+ console.log(chalk13.cyan(`
3079
+ ${t("diff.unstagedHeader")}`));
3080
+ parseDiffStat(unstaged).forEach((f) => printFile(f));
3081
+ }
3082
+ if (untracked) {
3083
+ const files = untracked.split("\n").filter(Boolean);
3084
+ console.log(chalk13.cyan(`
3085
+ ${t("diff.untrackedHeader", files.length)}`));
3086
+ files.forEach((f) => console.log(` ${chalk13.green("+")} ${f}`));
3087
+ }
3088
+ const numstat = gitOut3(["diff", "--numstat", "HEAD"]);
3089
+ if (numstat) {
3090
+ const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3091
+ console.log(chalk13.cyan(`
3092
+ ${t("diff.summaryHeader")}`));
3093
+ console.log(` ${t("diff.filesLine", fileCount)}`);
3094
+ console.log(` \uCD94\uAC00: ${chalk13.green(`+${totalAdd}`)}\uC904`);
3095
+ console.log(` \uC0AD\uC81C: ${chalk13.red(`-${totalDel}`)}\uC904`);
3096
+ }
3097
+ console.log("");
3098
+ }
3099
+
3100
+ // src/commands/status.ts
3101
+ import { execFileSync as execFileSync4, execSync as execSync5 } from "child_process";
3102
+ import fs13 from "fs";
3103
+ import path14 from "path";
3104
+ import chalk14 from "chalk";
3105
+ function gitOut4(args) {
3106
+ return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3107
+ }
3108
+ function countFileChanges(porcelain) {
3109
+ const lines = porcelain.split("\n").filter(Boolean);
3110
+ let staged = 0;
3111
+ let unstaged = 0;
3112
+ let untracked = 0;
3113
+ for (const line of lines) {
3114
+ const x = line[0];
3115
+ const y = line[1];
3116
+ if (x === "?" && y === "?") {
3117
+ untracked++;
3118
+ continue;
3119
+ }
3120
+ if (x !== " ") staged++;
3121
+ if (y !== " ") unstaged++;
3122
+ }
3123
+ return { staged, unstaged, untracked };
3124
+ }
3125
+ function parseSyncCounts(revListOutput) {
3126
+ const parts = revListOutput.trim().split(/\s+/);
3127
+ return {
3128
+ ahead: parseInt(parts[0] ?? "0", 10) || 0,
3129
+ behind: parseInt(parts[1] ?? "0", 10) || 0,
3130
+ hasUpstream: true
3131
+ };
3132
+ }
3133
+ function formatSyncLabel(sync2) {
3134
+ if (!sync2.hasUpstream) return t("status.noUpstream");
3135
+ if (sync2.ahead === 0 && sync2.behind === 0) return t("status.inSync");
3136
+ const parts = [];
3137
+ if (sync2.ahead > 0) parts.push(t("status.ahead", sync2.ahead));
3138
+ if (sync2.behind > 0) parts.push(t("status.behind", sync2.behind));
3139
+ return parts.join(" \xB7 ");
3140
+ }
3141
+ function parseRecentCommitLines(logOutput) {
3142
+ return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3143
+ }
3144
+ function readProjectPackage(cwd = process.cwd()) {
3145
+ const pkgPath = path14.join(cwd, "package.json");
3146
+ if (!fs13.existsSync(pkgPath)) return null;
3147
+ try {
3148
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
3149
+ if (!pkg.name && !pkg.version) return null;
3150
+ return {
3151
+ name: pkg.name ?? "(no name)",
3152
+ version: pkg.version ?? "(no version)"
3153
+ };
3154
+ } catch {
3155
+ return null;
3156
+ }
3157
+ }
3158
+ function getSyncCounts() {
3159
+ try {
3160
+ const out = gitOut4(["rev-list", "--left-right", "--count", "HEAD...@{u}"]);
3161
+ return parseSyncCounts(out);
3162
+ } catch {
3163
+ return { ahead: 0, behind: 0, hasUpstream: false };
3164
+ }
3165
+ }
3166
+ async function status() {
3167
+ console.log(chalk14.bold(`
3168
+ \u{1F4CA} ${t("status.title")}`));
3169
+ console.log(chalk14.gray("\u2500".repeat(40)));
3170
+ try {
3171
+ execSync5("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3172
+ } catch {
3173
+ console.log(chalk14.red(`\u274C ${t("status.notGitRepo")}`));
3174
+ return;
3175
+ }
3176
+ let branch;
3177
+ try {
3178
+ branch = gitOut4(["branch", "--show-current"]).trim() || t("status.detached");
3179
+ } catch {
3180
+ branch = t("status.unknownBranch");
3181
+ }
3182
+ const porcelain = gitOut4(["status", "--porcelain"]).trim();
3183
+ const counts = countFileChanges(porcelain);
3184
+ const sync2 = getSyncCounts();
3185
+ let commits = [];
3186
+ try {
3187
+ commits = parseRecentCommitLines(gitOut4(["log", "--oneline", "-3"]).trim());
3188
+ } catch {
3189
+ commits = [];
3190
+ }
3191
+ const pkg = readProjectPackage();
3192
+ console.log(chalk14.cyan(`
3193
+ \u{1F33F} ${t("status.branch")}`) + chalk14.white(` ${branch}`));
3194
+ console.log(
3195
+ chalk14.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk14.white(
3196
+ ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3197
+ )
3198
+ );
3199
+ console.log(chalk14.cyan(`
3200
+ \u{1F4CB} ${t("status.recentCommits")}`));
3201
+ if (commits.length === 0) {
3202
+ console.log(chalk14.dim(` ${t("status.noCommits")}`));
3203
+ } else {
3204
+ commits.forEach((c) => console.log(` ${chalk14.dim("\u2022")} ${c}`));
3205
+ }
3206
+ console.log(
3207
+ chalk14.cyan(`
3208
+ \u{1F504} ${t("status.remote")}`) + chalk14.white(` ${formatSyncLabel(sync2)}`)
3209
+ );
3210
+ console.log(chalk14.gray("\n" + "\u2500".repeat(40)));
3211
+ if (pkg) {
3212
+ console.log(chalk14.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk14.white(` ${pkg.name} v${pkg.version}`));
3213
+ } else {
3214
+ console.log(chalk14.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3215
+ }
3216
+ console.log("");
3217
+ }
3218
+
2735
3219
  // src/index.ts
2736
3220
  var program = new Command();
2737
3221
  var defaultHelp = new Help();
@@ -2743,9 +3227,13 @@ var KO_ALIASES = {
2743
3227
  check: "\uC810\uAC80",
2744
3228
  secure: "\uBCF4\uC548",
2745
3229
  ship: "\uBC30\uD3EC",
2746
- doctor: "\uD658\uACBD"
3230
+ doctor: "\uD658\uACBD",
3231
+ save: "\uC800\uC7A5",
3232
+ undo: "\uB418\uB3CC\uB9AC\uAE30",
3233
+ status: "\uC0C1\uD0DC",
3234
+ diff: "\uBCC0\uACBD"
2747
3235
  };
2748
- program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.4.0");
3236
+ program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.5.0");
2749
3237
  program.configureHelp({
2750
3238
  formatHelp(cmd, helper) {
2751
3239
  if (cmd.parent) {
@@ -2753,7 +3241,7 @@ program.configureHelp({
2753
3241
  }
2754
3242
  const subs = helper.visibleCommands(cmd).filter((c) => c.name() !== "help");
2755
3243
  const terms = subs.map((c) => `${c.name()} (${KO_ALIASES[c.name()]})`);
2756
- const termWidth = Math.max(...terms.map((t) => t.length), 0);
3244
+ const termWidth = Math.max(...terms.map((t2) => t2.length), 0);
2757
3245
  const lines = [
2758
3246
  helper.commandDescription(cmd),
2759
3247
  "",
@@ -2775,22 +3263,32 @@ var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uB
2775
3263
  secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
2776
3264
  program.command("ship").alias("\uBC30\uD3EC").alias("\uB9B4\uB9AC\uC988").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
2777
3265
  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);
3266
+ program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
3267
+ await save();
3268
+ });
3269
+ program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
3270
+ await undo();
3271
+ });
3272
+ program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
3273
+ await status();
3274
+ });
3275
+ program.command("diff").alias("\uBCC0\uACBD").alias("\uCC28\uC774").description("Git \uBCC0\uACBD\uC0AC\uD56D \uD55C\uAD6D\uC5B4 \uC694\uC57D (staged / unstaged / \uC0C8 \uD30C\uC77C)").action(diff);
2778
3276
  program.on("command:*", async (operands) => {
2779
3277
  const input = operands.join(" ");
2780
3278
  const route = routeNaturalLanguage(input);
2781
3279
  if (route) {
2782
3280
  console.log("");
2783
- console.log(chalk11.cyan(` \u{1F4AC} "${input}"`));
2784
- console.log(chalk11.cyan(` \u2192 ${route.explanation}`));
3281
+ console.log(chalk15.cyan(` \u{1F4AC} "${input}"`));
3282
+ console.log(chalk15.cyan(` \u2192 ${route.explanation}`));
2785
3283
  if (route.confidence === "low") {
2786
- const { confirm } = await inquirer5.prompt([{
3284
+ const { confirm } = await inquirer7.prompt([{
2787
3285
  type: "confirm",
2788
3286
  name: "confirm",
2789
3287
  message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
2790
3288
  default: true
2791
3289
  }]);
2792
3290
  if (!confirm) {
2793
- console.log(chalk11.dim(` ${ko.nlp.menuHint}`));
3291
+ console.log(chalk15.dim(` ${ko.nlp.menuHint}`));
2794
3292
  return;
2795
3293
  }
2796
3294
  }
@@ -2815,15 +3313,23 @@ program.on("command:*", async (operands) => {
2815
3313
  return ship();
2816
3314
  case "doctor":
2817
3315
  return doctor();
3316
+ case "save":
3317
+ return save();
3318
+ case "undo":
3319
+ return undo();
3320
+ case "status":
3321
+ return status();
3322
+ case "diff":
3323
+ return diff();
2818
3324
  }
2819
3325
  }
2820
- console.log(chalk11.yellow(`
3326
+ console.log(chalk15.yellow(`
2821
3327
  \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
2822
3328
  `));
2823
3329
  });
2824
3330
  program.action(async () => {
2825
3331
  console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
2826
- const { choice } = await inquirer5.prompt([{
3332
+ const { choice } = await inquirer7.prompt([{
2827
3333
  type: "list",
2828
3334
  name: "choice",
2829
3335
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -2835,7 +3341,11 @@ program.action(async () => {
2835
3341
  { name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
2836
3342
  { name: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654", value: "sync" },
2837
3343
  { name: "\u{1F680} \uBC30\uD3EC\uD558\uAE30", value: "ship" },
2838
- { name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" }
3344
+ { name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" },
3345
+ { name: "\u{1F4BE} Git\uC5D0 \uC800\uC7A5\uD558\uAE30", value: "save" },
3346
+ { name: "\u23EA \uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30", value: "undo" },
3347
+ { name: "\u{1F50D} \uBCC0\uACBD\uC0AC\uD56D \uBCF4\uAE30", value: "diff" },
3348
+ { name: "\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uBCF4\uAE30", value: "status" }
2839
3349
  ]
2840
3350
  }]);
2841
3351
  switch (choice) {
@@ -2855,6 +3365,14 @@ program.action(async () => {
2855
3365
  return doctor();
2856
3366
  case "ship":
2857
3367
  return ship();
3368
+ case "save":
3369
+ return save();
3370
+ case "undo":
3371
+ return undo();
3372
+ case "status":
3373
+ return status();
3374
+ case "diff":
3375
+ return diff();
2858
3376
  }
2859
3377
  });
2860
3378
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Vibe Harness Kit β€” λ°”μ΄λΈŒμ½”λ”© 풀사이클 CLI",
5
5
  "bin": {
6
6
  "vhk": "./dist/index.js"
@@ -41,6 +41,7 @@
41
41
  "commander": "^14.0.3",
42
42
  "handlebars": "^4.7.9",
43
43
  "inquirer": "^9.3.8",
44
+ "ora": "^9.4.0",
44
45
  "simple-git": "^3.36.0"
45
46
  },
46
47
  "devDependencies": {