@byh3071/vhk 0.5.1 โ†’ 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -2
  2. package/dist/index.js +327 -172
  3. package/package.json +62 -56
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  ---
2
2
  id: vhk-readme
3
3
  date: 2026-05-23
4
- tags: [vhk, cli, readme, v0.5.1]
4
+ tags: [vhk, cli, readme, v0.5.3]
5
5
  ---
6
6
 
7
7
  # ๐Ÿ”ง VHK โ€” Vibe Harness Kit
8
8
 
9
- > AI ์ฝ”๋”ฉ ์—์ด์ „ํŠธ๋ฅผ ๋ถ€๋ฆฌ๋Š” ์‚ฌ๋žŒ์„ ์œ„ํ•œ **ํ•œ๊ตญ์–ด ํ’€์‚ฌ์ดํด CLI** (v0.5.1)
9
+ > AI ์ฝ”๋”ฉ ์—์ด์ „ํŠธ๋ฅผ ๋ถ€๋ฆฌ๋Š” ์‚ฌ๋žŒ์„ ์œ„ํ•œ **ํ•œ๊ตญ์–ด ํ’€์‚ฌ์ดํด CLI** (v0.5.3)
10
+ >
11
+ > ๐Ÿฝ๏ธ **VHK๋Š” VHK๋กœ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ๋จ** โ€” ์ด ๋ ˆํฌ์˜ `docs/`, `CLAUDE.md`, `.cursorrules`๋„ `vhk init`์ด ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.
10
12
 
11
13
  ๋ช…๋ น์–ด๋ฅผ ์™ธ์šฐ์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค. `vhk`๋งŒ ์น˜๋ฉด ๋ฉ”๋‰ด๊ฐ€ ๋‚˜์˜ค๊ณ , ํ•œ๊ตญ์–ด๋กœ ๋งํ•ด๋„ ์•Œ์•„๋“ฃ์Šต๋‹ˆ๋‹ค.
12
14
 
@@ -103,6 +105,15 @@ vhk ๊ธฐํš ๋๋‚ฌ๊ณ  ๋ฐ”๋กœ ์‹œ์ž‘
103
105
  |------|------|
104
106
  | `--since YYYY-MM-DD` | ๋ถ„์„ ์‹œ์ž‘์ผ (๊ธฐ๋ณธ: ์˜ค๋Š˜) |
105
107
 
108
+ ## v0.5.3 ํ•˜์ด๋ผ์ดํŠธ
109
+
110
+ | ๊ธฐ๋Šฅ | ์„ค๋ช… |
111
+ |------|------|
112
+ | **์…€ํ”„ํ˜ธ์ŠคํŒ…** | `vhk init`์ด vhk-cli ๋ ˆํฌ ์ž์ฒด๋ฅผ ๋ถ€ํŠธ์ŠคํŠธ๋žฉ โ€” ์ž๊ธฐ ๋„๊ตฌ๋กœ ์ž๊ธฐ ๋ ˆํฌ ๋งŒ๋“ค๊ธฐ |
113
+ | **CHANGELOG.md** | ๋ณ€๊ฒฝ ์ด๋ ฅ ํ‘œ์ค€ ํŒŒ์ผ ์‹ ์„ค. `vhk ship`์ด `[Unreleased]` โ†’ `[๋ฒ„์ „]` ์ž๋™ ์ด๋™ |
114
+ | **doctor ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ** | `vhk doctor`๊ฐ€ npm ์ตœ์‹  ๋ฒ„์ „ ๋น„๊ต ํ›„ `๐Ÿ†• v0.X.X ์‚ฌ์šฉ ๊ฐ€๋Šฅ` ํ•œ ์ค„ ํ‘œ์‹œ |
115
+ | **init ์•ˆ์ „์„ฑ** | ์˜ต์…˜๊ฐ’ ํฌํ•จ ๋ช…๋ น ๋ผ์šฐํŒ… ๋ฒ„๊ทธ ํ”ฝ์Šค, ์‚ฌ์šฉ์ž ์ •์˜ `package.json` scripts ๋ณด์กด |
116
+
106
117
  ## v0.5.0 ํ•˜์ด๋ผ์ดํŠธ
107
118
 
108
119
  | ๊ธฐ๋Šฅ | ์„ค๋ช… |
package/dist/index.js CHANGED
@@ -485,8 +485,7 @@ var require_ignore = __commonJS({
485
485
 
486
486
  // src/index.ts
487
487
  import { Command, Help } from "commander";
488
- import chalk16 from "chalk";
489
- import inquirer7 from "inquirer";
488
+ import inquirer8 from "inquirer";
490
489
 
491
490
  // src/lib/nlp-router.ts
492
491
  function normalize(input) {
@@ -496,7 +495,7 @@ var NLP_KEYWORDS = {
496
495
  save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
497
496
  undo: ["\uB418\uB3CC\uB824", "\uB418\uB3CC\uB9AC\uAE30", "\uCDE8\uC18C", "\uC6D0\uB798\uB300\uB85C", "\uB864\uBC31", "\uB9AC\uC14B", "reset", "rollback"],
498
497
  status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"],
499
- diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
498
+ diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uBC14\uB00C\uC5C8", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
500
499
  };
501
500
  function matchesKeywords(text, command) {
502
501
  const keywords = NLP_KEYWORDS[command];
@@ -546,7 +545,7 @@ var RULES = [
546
545
  command: "diff",
547
546
  explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
548
547
  confidence: "high",
549
- test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /๋ณ€๊ฒฝ์‚ฌํ•ญ|์ˆ˜์ •\s*๋‚ด์—ญ|์ฐจ์ด\s*๋ณด/.test(t2)) && !/์ €์žฅ|์ปค๋ฐ‹|push|ํ‘ธ์‹œ|์ƒํƒœ|ํ˜„ํ™ฉ|์„ธ์ด๋ธŒ|commit/.test(t2)
548
+ test: (t2) => (matchesKeywords(t2, "diff") || /^diff$/.test(t2) || /๋ณ€๊ฒฝ์‚ฌํ•ญ|์ˆ˜์ •\s*๋‚ด์—ญ|์ฐจ์ด\s*๋ณด|๋ญ\s*๋ฐ”๋€Œ/.test(t2)) && !/์ €์žฅ|์ปค๋ฐ‹|push|ํ‘ธ์‹œ|์ƒํƒœ|ํ˜„ํ™ฉ|์„ธ์ด๋ธŒ|commit/.test(t2)
550
549
  },
551
550
  {
552
551
  command: "undo",
@@ -611,6 +610,69 @@ function extractNotionUrl(input) {
611
610
  return m?.[0];
612
611
  }
613
612
 
613
+ // src/lib/cli-args.ts
614
+ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
615
+ "gate",
616
+ "\uAC80\uC99D",
617
+ "\uC544\uC774\uB514\uC5B4",
618
+ "init",
619
+ "\uC2DC\uC791",
620
+ "\uB9CC\uB4E4\uAE30",
621
+ "recap",
622
+ "\uC815\uB9AC",
623
+ "\uC624\uB298",
624
+ "sync",
625
+ "\uB9DE\uCD94\uAE30",
626
+ "\uADDC\uCE59",
627
+ "check",
628
+ "\uC810\uAC80",
629
+ "\uB9B0\uD2B8",
630
+ "secure",
631
+ "\uBCF4\uC548",
632
+ "scan",
633
+ "\uC2A4\uCE94",
634
+ "ship",
635
+ "\uBC30\uD3EC",
636
+ "\uB9B4\uB9AC\uC988",
637
+ "doctor",
638
+ "\uD658\uACBD",
639
+ "\uC9C4\uB2E8",
640
+ "save",
641
+ "\uC800\uC7A5",
642
+ "undo",
643
+ "\uB418\uB3CC\uB9AC\uAE30",
644
+ "status",
645
+ "\uC0C1\uD0DC",
646
+ "\uD604\uD669",
647
+ "diff",
648
+ "\uBCC0\uACBD",
649
+ "\uCC28\uC774",
650
+ "help"
651
+ ]);
652
+ function isOptionToken(token) {
653
+ return token.startsWith("-");
654
+ }
655
+ function detectNaturalLanguageInput(argv) {
656
+ const rest = argv.slice(2);
657
+ if (rest.length === 0) return null;
658
+ const first = rest[0];
659
+ if (isOptionToken(first)) return null;
660
+ if (rest.some(isOptionToken)) return null;
661
+ const input = rest.join(" ").trim();
662
+ if (!input) return null;
663
+ const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
664
+ if (firstIsKnown && rest.length === 1) return null;
665
+ if (firstIsKnown && rest.length > 1) {
666
+ if (routeNaturalLanguage(input)) return input;
667
+ return null;
668
+ }
669
+ return input;
670
+ }
671
+
672
+ // src/lib/nlp-run.ts
673
+ import chalk16 from "chalk";
674
+ import inquirer7 from "inquirer";
675
+
614
676
  // src/i18n/ko.ts
615
677
  var ko = {
616
678
  status: {
@@ -792,7 +854,9 @@ var ko = {
792
854
  projectFiles: "\u{1F4C1} \uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uD655\uC778:",
793
855
  envNotIgnored: "\u26A0\uFE0F .env\uAC00 .gitignore\uC5D0 \uC5C6\uC74C! \uCD94\uAC00\uD558\uC138\uC694",
794
856
  nextOkMessage: "\uD658\uACBD \uC810\uAC80 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uC2DC\uC791\uD558\uC138\uC694.",
795
- nextRetryMessage: "\uC704 \uB3C4\uAD6C\uB97C \uC124\uCE58\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694."
857
+ nextRetryMessage: "\uC704 \uB3C4\uAD6C\uB97C \uC124\uCE58\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
858
+ updateAvailable: (latest) => `\u{1F195} v${latest} \uC0AC\uC6A9 \uAC00\uB2A5 \u2014 npm i -g @byh3071/vhk`,
859
+ updateCurrent: "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC4F0\uACE0 \uC788\uC5B4\uC694"
796
860
  },
797
861
  nlp: {
798
862
  matched: "\uC774\uAC8C \uB9DE\uB098\uC694?",
@@ -847,7 +911,10 @@ var ko = {
847
911
  checkSecurity: "\uBCF4\uC548 \uC2A4\uCE94\uC744 \uB3CC\uB838\uB098\uC694?",
848
912
  hintSecurity: "vhk \uBCF4\uC548 scan",
849
913
  checkCommit: "\uBAA8\uB4E0 \uBCC0\uACBD\uC774 \uCEE4\uBC0B\uB418\uC5C8\uB098\uC694?",
850
- hintCommit: "git status \uD655\uC778"
914
+ hintCommit: "git status \uD655\uC778",
915
+ changelogUpdated: (version) => `CHANGELOG.md \uAC31\uC2E0\uB428 \u2014 [Unreleased] \u2192 [${version}] \uC139\uC158\uC73C\uB85C \uC774\uB3D9`,
916
+ changelogNoUnreleased: "CHANGELOG.md\uC5D0 [Unreleased] \uC139\uC158\uC774 \uC5C6\uC5B4 \uC790\uB3D9 \uAC31\uC2E0\uC744 \uC2A4\uD0B5\uD588\uC5B4\uC694",
917
+ changelogMissing: "CHANGELOG.md\uAC00 \uC5C6\uC5B4\uC694. \uB9CC\uB4E4\uBA74 ship\uC774 \uC790\uB3D9\uC73C\uB85C [Unreleased] \u2192 \uBC84\uC804 \uC139\uC158\uC73C\uB85C \uC62E\uACA8\uC90D\uB2C8\uB2E4."
851
918
  }
852
919
  };
853
920
  function lookup(path15) {
@@ -1668,7 +1735,7 @@ function enhancePackageScripts(projectDir) {
1668
1735
  const pkgPath = path3.join(projectDir, "package.json");
1669
1736
  if (!fs3.existsSync(pkgPath)) return false;
1670
1737
  const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1671
- pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
1738
+ pkg.scripts = { ...VHK_PACKAGE_SCRIPTS, ...pkg.scripts };
1672
1739
  fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1673
1740
  return true;
1674
1741
  }
@@ -2734,6 +2801,27 @@ function getVhkVersion() {
2734
2801
  }
2735
2802
  return void 0;
2736
2803
  }
2804
+ function fetchLatestNpmVersion(packageName) {
2805
+ try {
2806
+ const result = execSync(`npm view ${packageName} version`, {
2807
+ encoding: "utf-8",
2808
+ timeout: 5e3,
2809
+ stdio: ["ignore", "pipe", "ignore"]
2810
+ }).trim();
2811
+ if (/^\d+\.\d+\.\d+/.test(result)) return result;
2812
+ return void 0;
2813
+ } catch {
2814
+ return void 0;
2815
+ }
2816
+ }
2817
+ function compareSemver(a, b) {
2818
+ const parse = (v) => v.replace(/^v/i, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
2819
+ const [a1 = 0, a2 = 0, a3 = 0] = parse(a);
2820
+ const [b1 = 0, b2 = 0, b3 = 0] = parse(b);
2821
+ if (a1 !== b1) return a1 - b1;
2822
+ if (a2 !== b2) return a2 - b2;
2823
+ return a3 - b3;
2824
+ }
2737
2825
  async function doctor() {
2738
2826
  console.log(chalk10.bold(`
2739
2827
  ${ko.doctor.title}
@@ -2761,6 +2849,14 @@ ${ko.doctor.title}
2761
2849
  } else {
2762
2850
  console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
2763
2851
  }
2852
+ if (vhkVersion) {
2853
+ const latest = fetchLatestNpmVersion("@byh3071/vhk");
2854
+ if (latest && compareSemver(latest, vhkVersion) > 0) {
2855
+ console.log(chalk10.yellow(` ${ko.doctor.updateAvailable(latest)}`));
2856
+ } else if (latest) {
2857
+ console.log(chalk10.dim(` ${ko.doctor.updateCurrent}`));
2858
+ }
2859
+ }
2764
2860
  console.log("");
2765
2861
  console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
2766
2862
  const cwd = process.cwd();
@@ -2823,6 +2919,29 @@ var CHECKLIST = [
2823
2919
  function sanitizeVersion(version) {
2824
2920
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2825
2921
  }
2922
+ function updateChangelogUnreleased(cwd, version, date) {
2923
+ const changelogPath = path13.join(cwd, "CHANGELOG.md");
2924
+ if (!fs13.existsSync(changelogPath)) return { status: "missing" };
2925
+ const content = fs13.readFileSync(changelogPath, "utf-8");
2926
+ const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2927
+ if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2928
+ const blankUnreleased = [
2929
+ "## [Unreleased]",
2930
+ "",
2931
+ "### Added",
2932
+ "- ",
2933
+ "",
2934
+ "### Fixed",
2935
+ "- ",
2936
+ "",
2937
+ "---",
2938
+ "",
2939
+ `## [${version}] \u2014 ${date}`
2940
+ ].join("\n");
2941
+ const updated = content.replace(unreleasedHeading, blankUnreleased);
2942
+ fs13.writeFileSync(changelogPath, updated, "utf-8");
2943
+ return { status: "updated", version };
2944
+ }
2826
2945
  async function ship() {
2827
2946
  console.log(chalk11.bold(`
2828
2947
  ${ko.ship.title}
@@ -2914,6 +3033,14 @@ ${ko.ship.title}
2914
3033
  fs13.writeFileSync(filePath, content, "utf-8");
2915
3034
  console.log(chalk11.green(`
2916
3035
  ${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
3036
+ const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
3037
+ if (changelogResult.status === "updated") {
3038
+ log.success(ko.ship.changelogUpdated(changelogResult.version));
3039
+ } else if (changelogResult.status === "no-unreleased") {
3040
+ log.warn(ko.ship.changelogNoUnreleased);
3041
+ } else {
3042
+ log.info(ko.ship.changelogMissing);
3043
+ }
2917
3044
  printNextStep({
2918
3045
  message: ko.ship.deployMessage,
2919
3046
  command: "npm publish --access=public",
@@ -3203,104 +3330,23 @@ ${t("undo.recentHeader")}`));
3203
3330
  }
3204
3331
  }
3205
3332
 
3206
- // src/commands/diff.ts
3207
- import { execFileSync as execFileSync4, execSync as execSync2 } from "child_process";
3333
+ // src/commands/status.ts
3334
+ import { execFileSync as execFileSync4 } from "child_process";
3335
+ import fs15 from "fs";
3336
+ import path14 from "path";
3208
3337
  import chalk14 from "chalk";
3209
- function gitOut2(args) {
3210
- try {
3211
- return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3212
- } catch {
3213
- return "";
3214
- }
3215
- }
3216
- function parseDiffStat(stat) {
3217
- const files = [];
3218
- const lines = stat.split("\n");
3219
- for (const line of lines) {
3220
- const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
3221
- if (!match) continue;
3222
- const name = match[1].trim();
3223
- if (name.includes("changed") || name.includes("file")) continue;
3224
- const plusMatch = line.match(/(\++)/);
3225
- const minusMatch = line.match(/(\-+)/);
3226
- files.push({
3227
- name,
3228
- additions: plusMatch ? plusMatch[1].length : 0,
3229
- deletions: minusMatch ? minusMatch[1].length : 0
3230
- });
3231
- }
3232
- return files;
3233
- }
3234
- function summarizeNumstat(numstat) {
3235
- let totalAdd = 0;
3236
- let totalDel = 0;
3237
- let fileCount = 0;
3238
- for (const line of numstat.split("\n").filter(Boolean)) {
3239
- const [add, del] = line.split(" ");
3240
- if (add === void 0 || del === void 0) continue;
3241
- totalAdd += parseInt(add, 10) || 0;
3242
- totalDel += parseInt(del, 10) || 0;
3243
- fileCount++;
3244
- }
3245
- return { fileCount, totalAdd, totalDel };
3246
- }
3247
- function printFile(f) {
3248
- const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
3249
- const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
3250
- const change = [adds, dels].filter(Boolean).join(" ");
3251
- console.log(` ${f.name} ${change}`);
3338
+
3339
+ // src/lib/read-json.ts
3340
+ import fs14 from "fs";
3341
+ function stripBom(text) {
3342
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
3252
3343
  }
3253
- async function diff() {
3254
- console.log(chalk14.bold(`
3255
- \u{1F50D} ${t("diff.title")}`));
3256
- console.log(chalk14.gray("\u2500".repeat(40)));
3257
- try {
3258
- execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3259
- } catch {
3260
- console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
3261
- return;
3262
- }
3263
- const unstaged = gitOut2(["diff", "--stat"]);
3264
- const staged = gitOut2(["diff", "--cached", "--stat"]);
3265
- const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3266
- if (!unstaged && !staged && !untracked) {
3267
- console.log(chalk14.green(`
3268
- \u2705 ${t("diff.noChanges")}`));
3269
- return;
3270
- }
3271
- if (staged) {
3272
- console.log(chalk14.cyan(`
3273
- ${t("diff.stagedHeader")}`));
3274
- parseDiffStat(staged).forEach((f) => printFile(f));
3275
- }
3276
- if (unstaged) {
3277
- console.log(chalk14.cyan(`
3278
- ${t("diff.unstagedHeader")}`));
3279
- parseDiffStat(unstaged).forEach((f) => printFile(f));
3280
- }
3281
- if (untracked) {
3282
- const files = untracked.split("\n").filter(Boolean);
3283
- console.log(chalk14.cyan(`
3284
- ${t("diff.untrackedHeader", files.length)}`));
3285
- files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
3286
- }
3287
- const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3288
- if (numstat) {
3289
- const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3290
- console.log(chalk14.cyan(`
3291
- ${t("diff.summaryHeader")}`));
3292
- console.log(` ${t("diff.filesLine", fileCount)}`);
3293
- console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
3294
- console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
3295
- }
3296
- console.log("");
3344
+ function readJsonFile(filePath) {
3345
+ const raw = stripBom(fs14.readFileSync(filePath, "utf-8"));
3346
+ return JSON.parse(raw);
3297
3347
  }
3298
3348
 
3299
3349
  // src/commands/status.ts
3300
- import { execFileSync as execFileSync5 } from "child_process";
3301
- import fs14 from "fs";
3302
- import path14 from "path";
3303
- import chalk15 from "chalk";
3304
3350
  function countFileChanges(porcelain) {
3305
3351
  const lines = porcelain.split("\n").filter(Boolean);
3306
3352
  let staged = 0;
@@ -3339,9 +3385,9 @@ function parseRecentCommitLines(logOutput) {
3339
3385
  }
3340
3386
  function readProjectPackage(cwd = process.cwd()) {
3341
3387
  const pkgPath = path14.join(cwd, "package.json");
3342
- if (!fs14.existsSync(pkgPath)) return null;
3388
+ if (!fs15.existsSync(pkgPath)) return null;
3343
3389
  try {
3344
- const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
3390
+ const pkg = readJsonFile(pkgPath);
3345
3391
  if (!pkg.name && !pkg.version) return null;
3346
3392
  return {
3347
3393
  name: pkg.name ?? "(no name)",
@@ -3360,15 +3406,15 @@ function getSyncCounts(gitRoot) {
3360
3406
  }
3361
3407
  }
3362
3408
  async function status() {
3363
- console.log(chalk15.bold(`
3409
+ console.log(chalk14.bold(`
3364
3410
  \u{1F4CA} ${t("status.title")}`));
3365
- console.log(chalk15.gray("\u2500".repeat(40)));
3411
+ console.log(chalk14.gray("\u2500".repeat(40)));
3366
3412
  let gitRoot;
3367
3413
  try {
3368
- execFileSync5("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3414
+ execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
3369
3415
  gitRoot = getGitRoot();
3370
3416
  } catch {
3371
- console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
3417
+ console.log(chalk14.red(`\u274C ${t("status.notGitRepo")}`));
3372
3418
  return;
3373
3419
  }
3374
3420
  let branch;
@@ -3387,31 +3433,183 @@ async function status() {
3387
3433
  commits = [];
3388
3434
  }
3389
3435
  const pkg = readProjectPackage();
3390
- console.log(chalk15.cyan(`
3391
- \u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
3436
+ console.log(chalk14.cyan(`
3437
+ \u{1F33F} ${t("status.branch")}`) + chalk14.white(` ${branch}`));
3392
3438
  console.log(
3393
- chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
3439
+ chalk14.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk14.white(
3394
3440
  ` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
3395
3441
  )
3396
3442
  );
3397
- console.log(chalk15.cyan(`
3443
+ console.log(chalk14.cyan(`
3398
3444
  \u{1F4CB} ${t("status.recentCommits")}`));
3399
3445
  if (commits.length === 0) {
3400
- console.log(chalk15.dim(` ${t("status.noCommits")}`));
3446
+ console.log(chalk14.dim(` ${t("status.noCommits")}`));
3401
3447
  } else {
3402
- commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
3448
+ commits.forEach((c) => console.log(` ${chalk14.dim("\u2022")} ${c}`));
3403
3449
  }
3404
3450
  console.log(
3405
- chalk15.cyan(`
3406
- \u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
3451
+ chalk14.cyan(`
3452
+ \u{1F504} ${t("status.remote")}`) + chalk14.white(` ${formatSyncLabel(sync2)}`)
3407
3453
  );
3408
- console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
3454
+ console.log(chalk14.gray("\n" + "\u2500".repeat(40)));
3409
3455
  if (pkg) {
3410
- console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
3456
+ console.log(chalk14.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk14.white(` ${pkg.name} v${pkg.version}`));
3411
3457
  } else {
3412
- console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3458
+ console.log(chalk14.dim(`\u{1F4E6} ${t("status.noPackage")}`));
3459
+ }
3460
+ console.log("");
3461
+ }
3462
+
3463
+ // src/commands/diff.ts
3464
+ import { execFileSync as execFileSync5, execSync as execSync2 } from "child_process";
3465
+ import chalk15 from "chalk";
3466
+ function gitOut2(args) {
3467
+ try {
3468
+ return execFileSync5("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3469
+ } catch {
3470
+ return "";
3471
+ }
3472
+ }
3473
+ function parseDiffStat(stat) {
3474
+ const files = [];
3475
+ const lines = stat.split("\n");
3476
+ for (const line of lines) {
3477
+ const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)/);
3478
+ if (!match) continue;
3479
+ const name = match[1].trim();
3480
+ if (name.includes("changed") || name.includes("file")) continue;
3481
+ const plusMatch = line.match(/(\++)/);
3482
+ const minusMatch = line.match(/(\-+)/);
3483
+ files.push({
3484
+ name,
3485
+ additions: plusMatch ? plusMatch[1].length : 0,
3486
+ deletions: minusMatch ? minusMatch[1].length : 0
3487
+ });
3488
+ }
3489
+ return files;
3490
+ }
3491
+ function summarizeNumstat(numstat) {
3492
+ let totalAdd = 0;
3493
+ let totalDel = 0;
3494
+ let fileCount = 0;
3495
+ for (const line of numstat.split("\n").filter(Boolean)) {
3496
+ const [add, del] = line.split(" ");
3497
+ if (add === void 0 || del === void 0) continue;
3498
+ totalAdd += parseInt(add, 10) || 0;
3499
+ totalDel += parseInt(del, 10) || 0;
3500
+ fileCount++;
3501
+ }
3502
+ return { fileCount, totalAdd, totalDel };
3503
+ }
3504
+ function printFile(f) {
3505
+ const adds = f.additions > 0 ? chalk15.green(`+${f.additions}`) : "";
3506
+ const dels = f.deletions > 0 ? chalk15.red(`-${f.deletions}`) : "";
3507
+ const change = [adds, dels].filter(Boolean).join(" ");
3508
+ console.log(` ${f.name} ${change}`);
3509
+ }
3510
+ async function diff() {
3511
+ console.log(chalk15.bold(`
3512
+ \u{1F50D} ${t("diff.title")}`));
3513
+ console.log(chalk15.gray("\u2500".repeat(40)));
3514
+ try {
3515
+ execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
3516
+ } catch {
3517
+ console.log(chalk15.red(`\u274C ${t("diff.notGitRepo")}`));
3518
+ return;
3519
+ }
3520
+ const unstaged = gitOut2(["diff", "--stat"]);
3521
+ const staged = gitOut2(["diff", "--cached", "--stat"]);
3522
+ const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
3523
+ if (!unstaged && !staged && !untracked) {
3524
+ console.log(chalk15.green(`
3525
+ \u2705 ${t("diff.noChanges")}`));
3526
+ return;
3527
+ }
3528
+ if (staged) {
3529
+ console.log(chalk15.cyan(`
3530
+ ${t("diff.stagedHeader")}`));
3531
+ parseDiffStat(staged).forEach((f) => printFile(f));
3532
+ }
3533
+ if (unstaged) {
3534
+ console.log(chalk15.cyan(`
3535
+ ${t("diff.unstagedHeader")}`));
3536
+ parseDiffStat(unstaged).forEach((f) => printFile(f));
3537
+ }
3538
+ if (untracked) {
3539
+ const files = untracked.split("\n").filter(Boolean);
3540
+ console.log(chalk15.cyan(`
3541
+ ${t("diff.untrackedHeader", files.length)}`));
3542
+ files.forEach((f) => console.log(` ${chalk15.green("+")} ${f}`));
3543
+ }
3544
+ const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
3545
+ if (numstat) {
3546
+ const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
3547
+ console.log(chalk15.cyan(`
3548
+ ${t("diff.summaryHeader")}`));
3549
+ console.log(` ${t("diff.filesLine", fileCount)}`);
3550
+ console.log(` \uCD94\uAC00: ${chalk15.green(`+${totalAdd}`)}\uC904`);
3551
+ console.log(` \uC0AD\uC81C: ${chalk15.red(`-${totalDel}`)}\uC904`);
3552
+ }
3553
+ console.log("");
3554
+ }
3555
+
3556
+ // src/lib/nlp-run.ts
3557
+ async function dispatchNlpRoute(route, input) {
3558
+ switch (route.command) {
3559
+ case "gate":
3560
+ return gate();
3561
+ case "init":
3562
+ return init({
3563
+ skipGate: route.args?.includes("--skip-gate"),
3564
+ fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
3565
+ });
3566
+ case "recap":
3567
+ return recap({});
3568
+ case "sync":
3569
+ return sync();
3570
+ case "check":
3571
+ return check();
3572
+ case "secure":
3573
+ return secure();
3574
+ case "ship":
3575
+ return ship();
3576
+ case "doctor":
3577
+ return doctor();
3578
+ case "save":
3579
+ return save();
3580
+ case "undo":
3581
+ return undo();
3582
+ case "status":
3583
+ return status();
3584
+ case "diff":
3585
+ return diff();
3586
+ }
3587
+ }
3588
+ async function runNaturalLanguageRoute(input) {
3589
+ const route = routeNaturalLanguage(input);
3590
+ if (!route) {
3591
+ console.log(chalk16.yellow(`
3592
+ \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
3593
+ `));
3594
+ return;
3413
3595
  }
3414
3596
  console.log("");
3597
+ console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
3598
+ console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
3599
+ if (route.confidence === "low") {
3600
+ const { confirm } = await inquirer7.prompt([{
3601
+ type: "confirm",
3602
+ name: "confirm",
3603
+ message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
3604
+ default: true
3605
+ }]);
3606
+ if (!confirm) {
3607
+ console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
3608
+ return;
3609
+ }
3610
+ }
3611
+ console.log("");
3612
+ await dispatchNlpRoute(route, input);
3415
3613
  }
3416
3614
 
3417
3615
  // src/index.ts
@@ -3431,7 +3629,7 @@ var KO_ALIASES = {
3431
3629
  status: "\uC0C1\uD0DC",
3432
3630
  diff: "\uBCC0\uACBD"
3433
3631
  };
3434
- 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.1");
3632
+ 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.3");
3435
3633
  program.configureHelp({
3436
3634
  formatHelp(cmd, helper) {
3437
3635
  if (cmd.parent) {
@@ -3472,62 +3670,14 @@ program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\
3472
3670
  });
3473
3671
  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);
3474
3672
  program.on("command:*", async (operands) => {
3475
- const input = operands.join(" ");
3476
- const route = routeNaturalLanguage(input);
3477
- if (route) {
3478
- console.log("");
3479
- console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
3480
- console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
3481
- if (route.confidence === "low") {
3482
- const { confirm } = await inquirer7.prompt([{
3483
- type: "confirm",
3484
- name: "confirm",
3485
- message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
3486
- default: true
3487
- }]);
3488
- if (!confirm) {
3489
- console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
3490
- return;
3491
- }
3492
- }
3493
- console.log("");
3494
- switch (route.command) {
3495
- case "gate":
3496
- return gate();
3497
- case "init":
3498
- return init({
3499
- skipGate: route.args?.includes("--skip-gate"),
3500
- fromNotion: route.args?.includes("--from-notion") ? extractNotionUrl(input) : void 0
3501
- });
3502
- case "recap":
3503
- return recap({});
3504
- case "sync":
3505
- return sync();
3506
- case "check":
3507
- return check();
3508
- case "secure":
3509
- return secure();
3510
- case "ship":
3511
- return ship();
3512
- case "doctor":
3513
- return doctor();
3514
- case "save":
3515
- return save();
3516
- case "undo":
3517
- return undo();
3518
- case "status":
3519
- return status();
3520
- case "diff":
3521
- return diff();
3522
- }
3523
- }
3524
- console.log(chalk16.yellow(`
3525
- \u2753 "${input}" \u2014 ${ko.nlp.notMatched}
3526
- `));
3673
+ const unknown = operands[0] ?? "";
3674
+ const rest = operands.slice(1);
3675
+ const input = [unknown, ...rest].join(" ").trim();
3676
+ await runNaturalLanguageRoute(input);
3527
3677
  });
3528
3678
  program.action(async () => {
3529
3679
  console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
3530
- const { choice } = await inquirer7.prompt([{
3680
+ const { choice } = await inquirer8.prompt([{
3531
3681
  type: "list",
3532
3682
  name: "choice",
3533
3683
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
@@ -3573,4 +3723,9 @@ program.action(async () => {
3573
3723
  return diff();
3574
3724
  }
3575
3725
  });
3576
- await program.parseAsync(process.argv);
3726
+ var nlInput = detectNaturalLanguageInput(process.argv);
3727
+ if (nlInput !== null) {
3728
+ await runNaturalLanguageRoute(nlInput);
3729
+ } else {
3730
+ await program.parseAsync(process.argv);
3731
+ }
package/package.json CHANGED
@@ -1,56 +1,62 @@
1
- {
2
- "name": "@byh3071/vhk",
3
- "version": "0.5.1",
4
- "description": "Vibe Harness Kit โ€” ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ํ’€์‚ฌ์ดํด CLI",
5
- "bin": {
6
- "vhk": "./dist/index.js"
7
- },
8
- "type": "module",
9
- "scripts": {
10
- "dev": "tsx src/index.ts",
11
- "build": "tsup",
12
- "test": "vitest",
13
- "test:run": "vitest --run",
14
- "prepublishOnly": "pnpm build && pnpm test:run"
15
- },
16
- "files": [
17
- "dist",
18
- "README.md",
19
- "LICENSE"
20
- ],
21
- "keywords": [
22
- "vibe-coding",
23
- "harness",
24
- "cli",
25
- "scaffold",
26
- "session-log",
27
- "rules-sync"
28
- ],
29
- "author": "byh3071 <byh3071@gmail.com>",
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/byh3071-cpu/vhk.git"
34
- },
35
- "engines": {
36
- "node": ">=20"
37
- },
38
- "dependencies": {
39
- "@notionhq/client": "^5.22.0",
40
- "chalk": "^5.6.2",
41
- "commander": "^14.0.3",
42
- "handlebars": "^4.7.9",
43
- "inquirer": "^9.3.8",
44
- "ora": "^9.4.0",
45
- "simple-git": "^3.36.0"
46
- },
47
- "devDependencies": {
48
- "@types/inquirer": "^9.0.9",
49
- "@types/node": "^25.9.1",
50
- "ignore": "^7.0.5",
51
- "tsup": "^8.5.1",
52
- "tsx": "^4.22.3",
53
- "typescript": "^6.0.3",
54
- "vitest": "^4.1.7"
55
- }
56
- }
1
+ {
2
+ "name": "@byh3071/vhk",
3
+ "version": "0.5.3",
4
+ "description": "Vibe Harness Kit โ€” ๋ฐ”์ด๋ธŒ์ฝ”๋”ฉ ํ’€์‚ฌ์ดํด CLI",
5
+ "bin": {
6
+ "vhk": "dist/index.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev": "tsx src/index.ts",
11
+ "build": "tsup",
12
+ "test": "vitest",
13
+ "test:run": "vitest --run",
14
+ "prepublishOnly": "pnpm build && pnpm test:run",
15
+ "save": "vhk save",
16
+ "check": "vhk check",
17
+ "scan": "vhk secure scan",
18
+ "recap": "vhk recap",
19
+ "ship": "vhk ship",
20
+ "doctor": "vhk doctor"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "keywords": [
28
+ "vibe-coding",
29
+ "harness",
30
+ "cli",
31
+ "scaffold",
32
+ "session-log",
33
+ "rules-sync"
34
+ ],
35
+ "author": "byh3071 <byh3071@gmail.com>",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/byh3071-cpu/vhk.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "dependencies": {
45
+ "@notionhq/client": "^5.22.0",
46
+ "chalk": "^5.6.2",
47
+ "commander": "^14.0.3",
48
+ "handlebars": "^4.7.9",
49
+ "inquirer": "^9.3.8",
50
+ "ora": "^9.4.0",
51
+ "simple-git": "^3.36.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/inquirer": "^9.0.9",
55
+ "@types/node": "^25.9.1",
56
+ "ignore": "^7.0.5",
57
+ "tsup": "^8.5.1",
58
+ "tsx": "^4.22.3",
59
+ "typescript": "^6.0.3",
60
+ "vitest": "^4.1.7"
61
+ }
62
+ }