@byh3071/vhk 2.4.0 → 2.4.1

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.
@@ -1692,8 +1692,42 @@ ${ko.sync.done}`));
1692
1692
  });
1693
1693
  }
1694
1694
 
1695
- // src/commands/deploy.ts
1695
+ // src/lib/version.ts
1696
1696
  import { existsSync as existsSync2 } from "fs";
1697
+ import { dirname, join as join2 } from "path";
1698
+ import { fileURLToPath } from "url";
1699
+
1700
+ // src/lib/read-json.ts
1701
+ import { readFileSync as readFileSync2 } from "fs";
1702
+ function stripBom(text) {
1703
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1704
+ }
1705
+ function readJsonFile(filePath) {
1706
+ const raw = stripBom(readFileSync2(filePath, "utf-8"));
1707
+ return JSON.parse(raw);
1708
+ }
1709
+
1710
+ // src/lib/version.ts
1711
+ function getVhkVersion() {
1712
+ const dir = dirname(fileURLToPath(import.meta.url));
1713
+ for (const pkgPath of [
1714
+ join2(dir, "../../package.json"),
1715
+ join2(dir, "../package.json")
1716
+ ]) {
1717
+ try {
1718
+ if (existsSync2(pkgPath)) {
1719
+ const pkg = readJsonFile(pkgPath);
1720
+ if (pkg.version) return pkg.version;
1721
+ }
1722
+ } catch {
1723
+ continue;
1724
+ }
1725
+ }
1726
+ return "0.0.0";
1727
+ }
1728
+
1729
+ // src/commands/deploy.ts
1730
+ import { existsSync as existsSync3 } from "fs";
1697
1731
  import chalk4 from "chalk";
1698
1732
  import inquirer2 from "inquirer";
1699
1733
 
@@ -1794,7 +1828,7 @@ var PLATFORMS = {
1794
1828
  function detectPlatform() {
1795
1829
  for (const [key, config] of Object.entries(PLATFORMS)) {
1796
1830
  for (const file of config.detectFiles) {
1797
- if (existsSync2(file)) return key;
1831
+ if (existsSync3(file)) return key;
1798
1832
  }
1799
1833
  }
1800
1834
  return null;
@@ -1863,8 +1897,8 @@ ${t("deploy.deploying")}
1863
1897
  }
1864
1898
 
1865
1899
  // src/commands/env.ts
1866
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, appendFileSync, readdirSync } from "fs";
1867
- import { join as join2 } from "path";
1900
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, appendFileSync, readdirSync } from "fs";
1901
+ import { join as join3 } from "path";
1868
1902
  import chalk5 from "chalk";
1869
1903
  function parseEnvKeys(content) {
1870
1904
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
@@ -1881,7 +1915,7 @@ function loadDefinedEnvKeys(dir = ".") {
1881
1915
  if (!name.startsWith(".env")) continue;
1882
1916
  if (name.endsWith(".example") || name.endsWith(".sample")) continue;
1883
1917
  try {
1884
- for (const k of parseEnvKeys(readFileSync2(join2(dir, name), "utf-8"))) keys.add(k);
1918
+ for (const k of parseEnvKeys(readFileSync3(join3(dir, name), "utf-8"))) keys.add(k);
1885
1919
  } catch {
1886
1920
  }
1887
1921
  }
@@ -1889,8 +1923,8 @@ function loadDefinedEnvKeys(dir = ".") {
1889
1923
  }
1890
1924
  function ensureGitignore() {
1891
1925
  const gitignorePath = ".gitignore";
1892
- if (existsSync3(gitignorePath)) {
1893
- const content = readFileSync2(gitignorePath, "utf-8");
1926
+ if (existsSync4(gitignorePath)) {
1927
+ const content = readFileSync3(gitignorePath, "utf-8");
1894
1928
  if (!content.split("\n").some((l) => l.trim() === ".env")) {
1895
1929
  appendFileSync(gitignorePath, "\n.env\n");
1896
1930
  console.log(chalk5.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
@@ -1903,12 +1937,12 @@ function ensureGitignore() {
1903
1937
  async function env() {
1904
1938
  console.log(chalk5.bold("\n\u{1F510} " + t("env.title")));
1905
1939
  console.log(chalk5.gray("\u2500".repeat(40)));
1906
- if (!existsSync3(".env")) {
1940
+ if (!existsSync4(".env")) {
1907
1941
  console.log(chalk5.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
1908
1942
  console.log(chalk5.gray(" .env \uD30C\uC77C\uC744 \uBA3C\uC800 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694."));
1909
1943
  return;
1910
1944
  }
1911
- const envContent = readFileSync2(".env", "utf-8");
1945
+ const envContent = readFileSync3(".env", "utf-8");
1912
1946
  const keys = parseEnvKeys(envContent);
1913
1947
  if (keys.length === 0) {
1914
1948
  console.log(chalk5.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
@@ -1929,11 +1963,11 @@ async function env() {
1929
1963
  async function envCheck() {
1930
1964
  console.log(chalk5.bold("\n\u{1F50D} " + t("env.checkTitle")));
1931
1965
  console.log(chalk5.gray("\u2500".repeat(40)));
1932
- if (!existsSync3(".env.example")) {
1966
+ if (!existsSync4(".env.example")) {
1933
1967
  console.log(chalk5.yellow("\n\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 vhk env\uB97C \uC2E4\uD589\uD558\uC138\uC694."));
1934
1968
  return;
1935
1969
  }
1936
- const requiredKeys = parseEnvKeys(readFileSync2(".env.example", "utf-8"));
1970
+ const requiredKeys = parseEnvKeys(readFileSync3(".env.example", "utf-8"));
1937
1971
  const currentKeys = loadDefinedEnvKeys();
1938
1972
  const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
1939
1973
  const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
@@ -1956,22 +1990,10 @@ async function envCheck() {
1956
1990
  }
1957
1991
 
1958
1992
  // src/commands/publish.ts
1959
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1993
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1960
1994
  import chalk6 from "chalk";
1961
1995
  import inquirer3 from "inquirer";
1962
1996
  import ora from "ora";
1963
-
1964
- // src/lib/read-json.ts
1965
- import { readFileSync as readFileSync3 } from "fs";
1966
- function stripBom(text) {
1967
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1968
- }
1969
- function readJsonFile(filePath) {
1970
- const raw = stripBom(readFileSync3(filePath, "utf-8"));
1971
- return JSON.parse(raw);
1972
- }
1973
-
1974
- // src/commands/publish.ts
1975
1997
  function bumpVersion(current, type) {
1976
1998
  const [major, minor, patch] = current.split(".").map((n) => parseInt(n, 10) || 0);
1977
1999
  switch (type) {
@@ -2001,7 +2023,9 @@ function bumpClaudeMdVersion(content, newVersion) {
2001
2023
  return content.replace(/(\*\*버전:\*\*\s*v)\d+\.\d+\.\d+/, `$1${newVersion}`);
2002
2024
  }
2003
2025
  function gitPostRelease(newVersion) {
2004
- const filesToAdd = existsSync4("CHANGELOG.md") ? ["package.json", "CHANGELOG.md"] : ["package.json"];
2026
+ const filesToAdd = ["package.json"];
2027
+ if (existsSync5("CHANGELOG.md")) filesToAdd.push("CHANGELOG.md");
2028
+ if (existsSync5("CLAUDE.md")) filesToAdd.push("CLAUDE.md");
2005
2029
  const add = safeExecFile("git", ["add", ...filesToAdd]);
2006
2030
  if (!add.ok) {
2007
2031
  return {
@@ -2067,7 +2091,7 @@ async function publish() {
2067
2091
  \u274C ${msg}`));
2068
2092
  return;
2069
2093
  }
2070
- if (!existsSync4("package.json")) {
2094
+ if (!existsSync5("package.json")) {
2071
2095
  console.log(chalk6.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2072
2096
  return;
2073
2097
  }
@@ -2099,7 +2123,7 @@ async function publish() {
2099
2123
  pkg.version = newVersion;
2100
2124
  writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2101
2125
  console.log(chalk6.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
2102
- const claudeMdOriginal = existsSync4("CLAUDE.md") ? readFileSync4("CLAUDE.md", "utf-8") : null;
2126
+ const claudeMdOriginal = existsSync5("CLAUDE.md") ? readFileSync4("CLAUDE.md", "utf-8") : null;
2103
2127
  if (claudeMdOriginal !== null) {
2104
2128
  const bumped = bumpClaudeMdVersion(claudeMdOriginal, newVersion);
2105
2129
  if (bumped !== claudeMdOriginal) {
@@ -2157,7 +2181,7 @@ async function publish() {
2157
2181
  }
2158
2182
  console.log(chalk6.green(`
2159
2183
  \u2714 ${t("publish.publishSuccess")}`));
2160
- if (existsSync4("CHANGELOG.md")) {
2184
+ if (existsSync5("CHANGELOG.md")) {
2161
2185
  const cl = readFileSync4("CHANGELOG.md", "utf-8");
2162
2186
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2163
2187
  const updated = insertChangelogStub(cl, newVersion, date);
@@ -2188,13 +2212,13 @@ async function publish() {
2188
2212
  }
2189
2213
 
2190
2214
  // src/commands/audit.ts
2191
- import { existsSync as existsSync5 } from "fs";
2215
+ import { existsSync as existsSync6 } from "fs";
2192
2216
  import chalk7 from "chalk";
2193
2217
  import inquirer4 from "inquirer";
2194
2218
  import ora2 from "ora";
2195
2219
  function detectCurrentPM() {
2196
- if (existsSync5("pnpm-lock.yaml")) return "pnpm";
2197
- if (existsSync5("yarn.lock")) return "yarn";
2220
+ if (existsSync6("pnpm-lock.yaml")) return "pnpm";
2221
+ if (existsSync6("yarn.lock")) return "yarn";
2198
2222
  return "npm";
2199
2223
  }
2200
2224
  function parseAuditOutput(output, pm) {
@@ -2277,28 +2301,6 @@ async function audit(autoFix = false) {
2277
2301
  });
2278
2302
  }
2279
2303
 
2280
- // src/lib/version.ts
2281
- import { existsSync as existsSync6 } from "fs";
2282
- import { dirname, join as join3 } from "path";
2283
- import { fileURLToPath } from "url";
2284
- function getVhkVersion() {
2285
- const dir = dirname(fileURLToPath(import.meta.url));
2286
- for (const pkgPath of [
2287
- join3(dir, "../../package.json"),
2288
- join3(dir, "../package.json")
2289
- ]) {
2290
- try {
2291
- if (existsSync6(pkgPath)) {
2292
- const pkg = readJsonFile(pkgPath);
2293
- if (pkg.version) return pkg.version;
2294
- }
2295
- } catch {
2296
- continue;
2297
- }
2298
- }
2299
- return "0.0.0";
2300
- }
2301
-
2302
2304
  // src/mcp/cli-path.ts
2303
2305
  import { existsSync as existsSync7 } from "fs";
2304
2306
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -3251,12 +3253,12 @@ export {
3251
3253
  MAX_SECRET_FINDINGS,
3252
3254
  scanProjectForSecrets,
3253
3255
  filterSevereFindings,
3256
+ getVhkVersion,
3254
3257
  deploy,
3255
3258
  env,
3256
3259
  envCheck,
3257
3260
  publish,
3258
3261
  audit,
3259
- getVhkVersion,
3260
3262
  resolveVhkCliInvocation,
3261
3263
  startMcpServer
3262
3264
  };
package/dist/index.js CHANGED
@@ -43,12 +43,12 @@ import {
43
43
  stripBom,
44
44
  sync,
45
45
  t
46
- } from "./chunk-MN6LSPN6.js";
46
+ } from "./chunk-Y7SJVHGS.js";
47
47
 
48
48
  // src/index.ts
49
49
  import { Command, Help } from "commander";
50
50
  import { fileURLToPath as fileURLToPath4 } from "url";
51
- import fs13 from "fs";
51
+ import fs14 from "fs";
52
52
  import chalk38 from "chalk";
53
53
  import inquirer15 from "inquirer";
54
54
 
@@ -2978,9 +2978,92 @@ ${ko.secure.title}
2978
2978
 
2979
2979
  // src/commands/doctor.ts
2980
2980
  import chalk9 from "chalk";
2981
+ import fs9 from "fs";
2982
+ import path10 from "path";
2983
+ import { fileURLToPath } from "url";
2984
+
2985
+ // src/lib/version-check.ts
2981
2986
  import fs8 from "fs";
2987
+ import os from "os";
2982
2988
  import path9 from "path";
2983
- import { fileURLToPath } from "url";
2989
+ var PACKAGE_NAME = "@byh3071/vhk";
2990
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
2991
+ var COOLDOWN_MS = 60 * 60 * 1e3;
2992
+ var MENU_FETCH_TIMEOUT_MS = 1500;
2993
+ function fetchLatestNpmVersion(packageName, timeoutMs = NETWORK_EXEC_TIMEOUT_MS) {
2994
+ const result = safeExecFile("npm", ["view", packageName, "version"], { timeoutMs });
2995
+ if (!result.ok) return void 0;
2996
+ const out = result.out;
2997
+ if (/^\d+\.\d+\.\d+/.test(out)) return out;
2998
+ return void 0;
2999
+ }
3000
+ function compareSemver(a, b) {
3001
+ const parse = (v) => v.replace(/^v/i, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
3002
+ const [a1 = 0, a2 = 0, a3 = 0] = parse(a);
3003
+ const [b1 = 0, b2 = 0, b3 = 0] = parse(b);
3004
+ if (a1 !== b1) return a1 - b1;
3005
+ if (a2 !== b2) return a2 - b2;
3006
+ return a3 - b3;
3007
+ }
3008
+ function getCacheDir() {
3009
+ return path9.join(os.homedir(), ".vhk");
3010
+ }
3011
+ function getCachePath() {
3012
+ return path9.join(getCacheDir(), "version-check.json");
3013
+ }
3014
+ function readCache() {
3015
+ try {
3016
+ const p = getCachePath();
3017
+ if (!fs8.existsSync(p)) return null;
3018
+ const data = readJsonFile(p);
3019
+ if (!data || typeof data.checkedAt !== "number" || data.latest !== void 0 && typeof data.latest !== "string") {
3020
+ return null;
3021
+ }
3022
+ return { latest: data.latest, checkedAt: data.checkedAt, lastTriedAt: data.lastTriedAt };
3023
+ } catch {
3024
+ return null;
3025
+ }
3026
+ }
3027
+ function writeCache(cache) {
3028
+ try {
3029
+ fs8.mkdirSync(getCacheDir(), { recursive: true });
3030
+ fs8.writeFileSync(getCachePath(), JSON.stringify(cache, null, 2), "utf-8");
3031
+ } catch {
3032
+ }
3033
+ }
3034
+ function isStale(cache, now = Date.now()) {
3035
+ return now - cache.checkedAt > CACHE_TTL_MS;
3036
+ }
3037
+ function recordLatest(latest, now = Date.now()) {
3038
+ writeCache({ latest, checkedAt: now, lastTriedAt: now });
3039
+ }
3040
+ function getUpdateInfo(now = Date.now()) {
3041
+ const current = getVhkVersion();
3042
+ const cache = readCache();
3043
+ let latest;
3044
+ if (cache && !isStale(cache, now)) {
3045
+ latest = cache.latest;
3046
+ } else {
3047
+ const lastTried = cache?.lastTriedAt ?? 0;
3048
+ const cooledDown = now - lastTried > COOLDOWN_MS;
3049
+ if (cooledDown) {
3050
+ const fetched = fetchLatestNpmVersion(PACKAGE_NAME, MENU_FETCH_TIMEOUT_MS);
3051
+ if (fetched) {
3052
+ writeCache({ latest: fetched, checkedAt: now, lastTriedAt: now });
3053
+ latest = fetched;
3054
+ } else {
3055
+ writeCache({ latest: cache?.latest, checkedAt: cache?.checkedAt ?? 0, lastTriedAt: now });
3056
+ latest = cache?.latest;
3057
+ }
3058
+ } else {
3059
+ latest = cache?.latest;
3060
+ }
3061
+ }
3062
+ const updateAvailable = latest ? compareSemver(latest, current) > 0 : false;
3063
+ return { current, latest, updateAvailable };
3064
+ }
3065
+
3066
+ // src/commands/doctor.ts
2984
3067
  function checkCommand(name, command, hint) {
2985
3068
  const result = safeExecFile(command, ["--version"]);
2986
3069
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2988,14 +3071,14 @@ function checkCommand(name, command, hint) {
2988
3071
  return { name, command, version, ok: true, hint };
2989
3072
  }
2990
3073
  function getVhkVersion2() {
2991
- const dir = path9.dirname(fileURLToPath(import.meta.url));
3074
+ const dir = path10.dirname(fileURLToPath(import.meta.url));
2992
3075
  const candidates = [
2993
- path9.join(dir, "../package.json"),
2994
- path9.join(dir, "../../package.json")
3076
+ path10.join(dir, "../package.json"),
3077
+ path10.join(dir, "../../package.json")
2995
3078
  ];
2996
3079
  for (const pkgPath of candidates) {
2997
3080
  try {
2998
- if (fs8.existsSync(pkgPath)) {
3081
+ if (fs9.existsSync(pkgPath)) {
2999
3082
  const pkg = readJsonFile(pkgPath);
3000
3083
  return pkg.version;
3001
3084
  }
@@ -3005,23 +3088,6 @@ function getVhkVersion2() {
3005
3088
  }
3006
3089
  return void 0;
3007
3090
  }
3008
- function fetchLatestNpmVersion(packageName) {
3009
- const result = safeExecFile("npm", ["view", packageName, "version"], {
3010
- timeoutMs: NETWORK_EXEC_TIMEOUT_MS
3011
- });
3012
- if (!result.ok) return void 0;
3013
- const out = result.out;
3014
- if (/^\d+\.\d+\.\d+/.test(out)) return out;
3015
- return void 0;
3016
- }
3017
- function compareSemver(a, b) {
3018
- const parse = (v) => v.replace(/^v/i, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
3019
- const [a1 = 0, a2 = 0, a3 = 0] = parse(a);
3020
- const [b1 = 0, b2 = 0, b3 = 0] = parse(b);
3021
- if (a1 !== b1) return a1 - b1;
3022
- if (a2 !== b2) return a2 - b2;
3023
- return a3 - b3;
3024
- }
3025
3091
  async function doctor(opts = {}) {
3026
3092
  console.log(chalk9.bold(`
3027
3093
  ${ko.doctor.title}
@@ -3051,6 +3117,7 @@ ${ko.doctor.title}
3051
3117
  }
3052
3118
  if (vhkVersion) {
3053
3119
  const latest = fetchLatestNpmVersion("@byh3071/vhk");
3120
+ if (latest) recordLatest(latest);
3054
3121
  if (latest && compareSemver(latest, vhkVersion) > 0) {
3055
3122
  console.log(chalk9.yellow(` ${ko.doctor.updateAvailable(latest)}`));
3056
3123
  } else if (latest) {
@@ -3068,19 +3135,19 @@ ${ko.doctor.title}
3068
3135
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
3069
3136
  ];
3070
3137
  for (const file of projectFiles) {
3071
- const exists = fs8.existsSync(path9.join(cwd, file.name));
3138
+ const exists = fs9.existsSync(path10.join(cwd, file.name));
3072
3139
  if (exists) {
3073
3140
  console.log(chalk9.green(` \u2705 ${file.name}`));
3074
3141
  if (file.name === ".env") {
3075
- const gitignorePath = path9.join(cwd, ".gitignore");
3076
- if (fs8.existsSync(gitignorePath)) {
3077
- const gitignore = fs8.readFileSync(gitignorePath, "utf-8");
3142
+ const gitignorePath = path10.join(cwd, ".gitignore");
3143
+ if (fs9.existsSync(gitignorePath)) {
3144
+ const gitignore = fs9.readFileSync(gitignorePath, "utf-8");
3078
3145
  if (!gitignore.includes(".env")) {
3079
3146
  console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
3080
3147
  }
3081
3148
  }
3082
3149
  }
3083
- } else if (file.name === ".env" && fs8.existsSync(path9.join(cwd, ".env.local"))) {
3150
+ } else if (file.name === ".env" && fs9.existsSync(path10.join(cwd, ".env.local"))) {
3084
3151
  console.log(chalk9.green(" \u2705 .env.local") + chalk9.dim(" \u2014 \uB85C\uCEF4 env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
3085
3152
  } else {
3086
3153
  console.log(chalk9.dim(` \u26AB ${file.name}`) + chalk9.dim(` \u2014 ${file.hint}`));
@@ -3132,8 +3199,8 @@ ${ko.doctor.title}
3132
3199
  // src/commands/ship.ts
3133
3200
  import chalk10 from "chalk";
3134
3201
  import inquirer4 from "inquirer";
3135
- import fs9 from "fs";
3136
- import path10 from "path";
3202
+ import fs10 from "fs";
3203
+ import path11 from "path";
3137
3204
  var CHECKLIST = [
3138
3205
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
3139
3206
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -3146,9 +3213,9 @@ function sanitizeVersion(version) {
3146
3213
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
3147
3214
  }
3148
3215
  function updateChangelogUnreleased(cwd, version, date) {
3149
- const changelogPath = path10.join(cwd, "CHANGELOG.md");
3150
- if (!fs9.existsSync(changelogPath)) return { status: "missing" };
3151
- const content = fs9.readFileSync(changelogPath, "utf-8");
3216
+ const changelogPath = path11.join(cwd, "CHANGELOG.md");
3217
+ if (!fs10.existsSync(changelogPath)) return { status: "missing" };
3218
+ const content = fs10.readFileSync(changelogPath, "utf-8");
3152
3219
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
3153
3220
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
3154
3221
  const blankUnreleased = [
@@ -3165,7 +3232,7 @@ function updateChangelogUnreleased(cwd, version, date) {
3165
3232
  `## [${version}] \u2014 ${date}`
3166
3233
  ].join("\n");
3167
3234
  const updated = content.replace(unreleasedHeading, blankUnreleased);
3168
- fs9.writeFileSync(changelogPath, updated, "utf-8");
3235
+ fs10.writeFileSync(changelogPath, updated, "utf-8");
3169
3236
  return { status: "updated", version };
3170
3237
  }
3171
3238
  async function ship() {
@@ -3224,12 +3291,12 @@ ${ko.ship.title}
3224
3291
  { type: "input", name: "learned", message: ko.ship.questionLearned },
3225
3292
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
3226
3293
  ]);
3227
- const buildLogDir = path10.join(cwd, "docs", "build-log");
3228
- if (!fs9.existsSync(buildLogDir)) fs9.mkdirSync(buildLogDir, { recursive: true });
3294
+ const buildLogDir = path11.join(cwd, "docs", "build-log");
3295
+ if (!fs10.existsSync(buildLogDir)) fs10.mkdirSync(buildLogDir, { recursive: true });
3229
3296
  const today = localDate();
3230
3297
  const versionSlug = sanitizeVersion(retro.version);
3231
3298
  const fileName = `${today}-v${versionSlug}.md`;
3232
- const filePath = path10.join(buildLogDir, fileName);
3299
+ const filePath = path11.join(buildLogDir, fileName);
3233
3300
  const content = [
3234
3301
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
3235
3302
  "",
@@ -3258,9 +3325,9 @@ ${ko.ship.title}
3258
3325
  "---",
3259
3326
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
3260
3327
  ].join("\n");
3261
- fs9.writeFileSync(filePath, content, "utf-8");
3328
+ fs10.writeFileSync(filePath, content, "utf-8");
3262
3329
  console.log(chalk10.green(`
3263
- ${ko.ship.buildLogDone(path10.relative(cwd, filePath))}`));
3330
+ ${ko.ship.buildLogDone(path11.relative(cwd, filePath))}`));
3264
3331
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
3265
3332
  if (changelogResult.status === "updated") {
3266
3333
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -3594,8 +3661,8 @@ ${ko.restore.notFound(targetId)}`));
3594
3661
 
3595
3662
  // src/commands/status.ts
3596
3663
  import { execFileSync as execFileSync3 } from "child_process";
3597
- import fs10 from "fs";
3598
- import path11 from "path";
3664
+ import fs11 from "fs";
3665
+ import path12 from "path";
3599
3666
  import chalk14 from "chalk";
3600
3667
  function countFileChanges(porcelain) {
3601
3668
  const lines = porcelain.split("\n").filter(Boolean);
@@ -3634,8 +3701,8 @@ function parseRecentCommitLines(logOutput) {
3634
3701
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3635
3702
  }
3636
3703
  function readProjectPackage(cwd = process.cwd()) {
3637
- const pkgPath = path11.join(cwd, "package.json");
3638
- if (!fs10.existsSync(pkgPath)) return null;
3704
+ const pkgPath = path12.join(cwd, "package.json");
3705
+ if (!fs11.existsSync(pkgPath)) return null;
3639
3706
  try {
3640
3707
  const pkg = readJsonFile(pkgPath);
3641
3708
  if (!pkg.name && !pkg.version) return null;
@@ -4488,6 +4555,7 @@ async function update() {
4488
4555
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
4489
4556
  const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
4490
4557
  const latest = getLatestVersion();
4558
+ if (latest) recordLatest(latest);
4491
4559
  if (!latest) {
4492
4560
  spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
4493
4561
  console.log(chalk22.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
@@ -5586,15 +5654,15 @@ ${ko.start.allDone}
5586
5654
  }
5587
5655
 
5588
5656
  // src/commands/cloud.ts
5589
- import fs12 from "fs";
5590
- import os from "os";
5591
- import path13 from "path";
5657
+ import fs13 from "fs";
5658
+ import os2 from "os";
5659
+ import path14 from "path";
5592
5660
  import chalk28 from "chalk";
5593
5661
 
5594
5662
  // src/lib/vhk-cloud.ts
5595
5663
  var import_ignore = __toESM(require_ignore(), 1);
5596
- import fs11 from "fs";
5597
- import path12 from "path";
5664
+ import fs12 from "fs";
5665
+ import path13 from "path";
5598
5666
  var DEFAULT_CLOUD_EXCLUDES = [
5599
5667
  "memory.json",
5600
5668
  // 개인 의사결정 메모
@@ -5612,17 +5680,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
5612
5680
  function loadVhkignore(rootDir) {
5613
5681
  const ig = (0, import_ignore.default)();
5614
5682
  ig.add(DEFAULT_CLOUD_EXCLUDES);
5615
- const ignorePath = path12.join(rootDir, ".vhkignore");
5616
- if (fs11.existsSync(ignorePath)) {
5617
- ig.add(fs11.readFileSync(ignorePath, "utf-8"));
5683
+ const ignorePath = path13.join(rootDir, ".vhkignore");
5684
+ if (fs12.existsSync(ignorePath)) {
5685
+ ig.add(fs12.readFileSync(ignorePath, "utf-8"));
5618
5686
  }
5619
5687
  return ig;
5620
5688
  }
5621
5689
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
5622
- const vhkDir = path12.join(rootDir, VHK_DIR3);
5690
+ const vhkDir = path13.join(rootDir, VHK_DIR3);
5623
5691
  let entries;
5624
5692
  try {
5625
- entries = fs11.readdirSync(vhkDir, { withFileTypes: true });
5693
+ entries = fs12.readdirSync(vhkDir, { withFileTypes: true });
5626
5694
  } catch {
5627
5695
  return [];
5628
5696
  }
@@ -5638,8 +5706,8 @@ function partitionGistFiles(gistFiles, ig) {
5638
5706
  return { keep, excluded };
5639
5707
  }
5640
5708
  function readCloudConfig(rootDir) {
5641
- const p = path12.join(rootDir, VHK_DIR3, CLOUD_CONFIG_FILE);
5642
- if (!fs11.existsSync(p)) return null;
5709
+ const p = path13.join(rootDir, VHK_DIR3, CLOUD_CONFIG_FILE);
5710
+ if (!fs12.existsSync(p)) return null;
5643
5711
  try {
5644
5712
  const parsed = readJsonFile(p);
5645
5713
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
@@ -5651,17 +5719,17 @@ function readCloudConfig(rootDir) {
5651
5719
  }
5652
5720
  }
5653
5721
  function writeCloudConfig(rootDir, config) {
5654
- const vhkDir = path12.join(rootDir, VHK_DIR3);
5655
- fs11.mkdirSync(vhkDir, { recursive: true });
5656
- const p = path12.join(vhkDir, CLOUD_CONFIG_FILE);
5657
- fs11.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
5722
+ const vhkDir = path13.join(rootDir, VHK_DIR3);
5723
+ fs12.mkdirSync(vhkDir, { recursive: true });
5724
+ const p = path13.join(vhkDir, CLOUD_CONFIG_FILE);
5725
+ fs12.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
5658
5726
  ensureCloudConfigIgnored(vhkDir);
5659
5727
  }
5660
5728
  function ensureCloudConfigIgnored(vhkDir) {
5661
- const giPath = path12.join(vhkDir, ".gitignore");
5729
+ const giPath = path13.join(vhkDir, ".gitignore");
5662
5730
  let content = "";
5663
5731
  try {
5664
- if (fs11.existsSync(giPath)) content = fs11.readFileSync(giPath, "utf-8");
5732
+ if (fs12.existsSync(giPath)) content = fs12.readFileSync(giPath, "utf-8");
5665
5733
  } catch {
5666
5734
  return;
5667
5735
  }
@@ -5672,7 +5740,7 @@ ${CLOUD_CONFIG_FILE}
5672
5740
  `;
5673
5741
  const base = content.length === 0 ? "" : content.endsWith("\n") ? content : content + "\n";
5674
5742
  try {
5675
- fs11.writeFileSync(giPath, base + block, "utf-8");
5743
+ fs12.writeFileSync(giPath, base + block, "utf-8");
5676
5744
  } catch {
5677
5745
  }
5678
5746
  }
@@ -5705,7 +5773,7 @@ async function cloudPush() {
5705
5773
  ${ko.cloud.pushTitle}
5706
5774
  `));
5707
5775
  const cwd = process.cwd();
5708
- if (!fs12.existsSync(path13.join(cwd, VHK_DIR3))) {
5776
+ if (!fs13.existsSync(path14.join(cwd, VHK_DIR3))) {
5709
5777
  console.log(chalk28.yellow(` ${ko.cloud.noVhkDir}`));
5710
5778
  return;
5711
5779
  }
@@ -5719,11 +5787,11 @@ ${ko.cloud.pushTitle}
5719
5787
  process.exitCode = 1;
5720
5788
  return;
5721
5789
  }
5722
- const filePaths = files.map((f) => path13.join(cwd, VHK_DIR3, f));
5790
+ const filePaths = files.map((f) => path14.join(cwd, VHK_DIR3, f));
5723
5791
  console.log(chalk28.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
5724
5792
  `));
5725
5793
  const existing = readCloudConfig(cwd);
5726
- const desc = `vhk .vhk backup \u2014 ${path13.basename(cwd)}`;
5794
+ const desc = `vhk .vhk backup \u2014 ${path14.basename(cwd)}`;
5727
5795
  if (existing) {
5728
5796
  const gistFiles = listGistFiles(existing.gistId);
5729
5797
  for (let i = 0; i < files.length; i++) {
@@ -5810,8 +5878,8 @@ ${ko.cloud.pullTitle}
5810
5878
  console.log(chalk28.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
5811
5879
  return;
5812
5880
  }
5813
- const vhkDir = path13.join(cwd, VHK_DIR3);
5814
- fs12.mkdirSync(vhkDir, { recursive: true });
5881
+ const vhkDir = path14.join(cwd, VHK_DIR3);
5882
+ fs13.mkdirSync(vhkDir, { recursive: true });
5815
5883
  let restored = 0;
5816
5884
  for (const name of names) {
5817
5885
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
@@ -5820,7 +5888,7 @@ ${ko.cloud.pullTitle}
5820
5888
  console.log(chalk28.dim(` ${res.err}`));
5821
5889
  continue;
5822
5890
  }
5823
- fs12.writeFileSync(path13.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
5891
+ fs13.writeFileSync(path14.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
5824
5892
  restored++;
5825
5893
  }
5826
5894
  writeCloudConfig(cwd, { gistId });
@@ -5837,9 +5905,9 @@ function purgeExcludedFromGist(gistId, names) {
5837
5905
  const body = JSON.stringify({
5838
5906
  files: Object.fromEntries(names.map((n) => [n, null]))
5839
5907
  });
5840
- const tmp = path13.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
5908
+ const tmp = path14.join(os2.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
5841
5909
  try {
5842
- fs12.writeFileSync(tmp, body, "utf-8");
5910
+ fs13.writeFileSync(tmp, body, "utf-8");
5843
5911
  for (let attempt = 0; attempt < 2; attempt++) {
5844
5912
  const res = safeExecFile(
5845
5913
  "gh",
@@ -5851,7 +5919,7 @@ function purgeExcludedFromGist(gistId, names) {
5851
5919
  return false;
5852
5920
  } finally {
5853
5921
  try {
5854
- fs12.unlinkSync(tmp);
5922
+ fs13.unlinkSync(tmp);
5855
5923
  } catch {
5856
5924
  }
5857
5925
  }
@@ -6228,8 +6296,8 @@ function verifyEvidence(cwd = process.cwd()) {
6228
6296
  const report = buildReport(gates, (/* @__PURE__ */ new Date()).toISOString(), localDate());
6229
6297
  const dir = join12(cwd, REPORT_DIR_REL);
6230
6298
  mkdirSync12(dir, { recursive: true });
6231
- const path14 = join12(cwd, REPORT_PATH_REL);
6232
- writeFileSync12(path14, JSON.stringify(report, null, 2) + "\n", "utf-8");
6299
+ const path15 = join12(cwd, REPORT_PATH_REL);
6300
+ writeFileSync12(path15, JSON.stringify(report, null, 2) + "\n", "utf-8");
6233
6301
  try {
6234
6302
  ensureVhkIgnored(cwd, "reports/");
6235
6303
  } catch {
@@ -6302,7 +6370,7 @@ async function verify(opts = {}) {
6302
6370
  await renderVerifyReport(cwd, opts);
6303
6371
  return;
6304
6372
  }
6305
- const { report, path: path14 } = verifyEvidence(cwd);
6373
+ const { report, path: path15 } = verifyEvidence(cwd);
6306
6374
  if (opts.json) {
6307
6375
  console.log(JSON.stringify(report, null, 2));
6308
6376
  process.exitCode = report.status === "FAIL" ? 1 : 0;
@@ -6322,7 +6390,7 @@ async function verify(opts = {}) {
6322
6390
  `
6323
6391
  \uACB0\uACFC: ${STATUS_BADGE[report.status]} ` + chalk31.dim(`(pass ${s.pass} / fail ${s.fail} / skip ${s.skip}, \uCD1D ${s.total})`)
6324
6392
  );
6325
- console.log(chalk31.dim(` \u{1F4C4} \uC99D\uAC70: ${path14}`));
6393
+ console.log(chalk31.dim(` \u{1F4C4} \uC99D\uAC70: ${path15}`));
6326
6394
  process.exitCode = report.status === "FAIL" ? 1 : 0;
6327
6395
  if (report.status === "FAIL") {
6328
6396
  printNextStep({
@@ -8016,27 +8084,49 @@ program.on("command:*", async (operands) => {
8016
8084
  await runNaturalLanguageRoute(input);
8017
8085
  });
8018
8086
  program.action(async () => {
8019
- console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
8087
+ const info = getUpdateInfo();
8088
+ console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 " + chalk38.dim(`v${info.current}`));
8089
+ if (info.updateAvailable && info.latest) {
8090
+ console.log(chalk38.yellow(`\u{1F195} \uC5C5\uB370\uC774\uD2B8 \uAC00\uB2A5: v${info.latest}`) + chalk38.dim(" \u2192 vhk update"));
8091
+ }
8092
+ const sample = QUICK_ACTIONS[0]?.say ?? "\uC0C1\uD0DC \uC54C\uB824\uC918";
8093
+ console.log(
8094
+ chalk38.dim("\u{1F4AC} \uBA85\uB839 \uC9C1\uC811 \uC785\uB825\uB3C4 \uB3FC\uC694 \u2014 \uC608: ") + chalk38.cyan("vhk status") + chalk38.dim(" \xB7 \uC790\uC5F0\uC5B4 OK: ") + chalk38.cyan(`"${sample}"`)
8095
+ );
8096
+ console.log("");
8097
+ const choices = [
8098
+ { name: "\u{1F680} \uC791\uC5C5 \uC2DC\uC791/\uC774\uC5B4\uD558\uAE30 (work)", value: "work" },
8099
+ { name: "\u{1F4A1} \uC0C8 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uD558\uAE30", value: "gate" },
8100
+ { name: "\u{1F195} \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC (start)", value: "start" },
8101
+ { name: "\u{1F3AF} \uB2E4\uC74C \uBAA9\uD45C \uBCF4\uAE30 (goal)", value: "goal-next" },
8102
+ { name: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD558\uAE30", value: "recap" },
8103
+ { name: "\u{1F50D} \uADDC\uCE59 \uD30C\uC77C \uC810\uAC80\uD558\uAE30", value: "check" },
8104
+ { name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
8105
+ { name: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654", value: "sync" },
8106
+ { name: "\u{1F680} \uBC30\uD3EC\uD558\uAE30", value: "ship" },
8107
+ { name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" },
8108
+ { name: "\u{1F4BE} Git\uC5D0 \uC800\uC7A5\uD558\uAE30", value: "save" },
8109
+ { name: "\u23EA \uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30", value: "undo" },
8110
+ { name: "\u{1F50D} \uBCC0\uACBD\uC0AC\uD56D \uBCF4\uAE30", value: "diff" },
8111
+ { name: "\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uBCF4\uAE30", value: "status" },
8112
+ { name: "\u23F8\uFE0F \uC791\uC5C5 \uC911\uB2E8 \uC815\uB9AC (handoff)", value: "work-handoff" }
8113
+ ];
8020
8114
  const { choice } = await inquirer15.prompt([{
8021
8115
  type: "list",
8022
8116
  name: "choice",
8023
8117
  message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
8024
- choices: [
8025
- { name: "\u{1F4A1} \uC0C8 \uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uD558\uAE30", value: "gate" },
8026
- { name: "\u{1F680} \uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC (start)", value: "start" },
8027
- { name: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD558\uAE30", value: "recap" },
8028
- { name: "\u{1F50D} \uADDC\uCE59 \uD30C\uC77C \uC810\uAC80\uD558\uAE30", value: "check" },
8029
- { name: "\u{1F512} \uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB9AC\uAE30", value: "secure" },
8030
- { name: "\u{1F504} \uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654", value: "sync" },
8031
- { name: "\u{1F680} \uBC30\uD3EC\uD558\uAE30", value: "ship" },
8032
- { name: "\u{1FA7A} \uD658\uACBD \uC810\uAC80\uD558\uAE30", value: "doctor" },
8033
- { name: "\u{1F4BE} Git\uC5D0 \uC800\uC7A5\uD558\uAE30", value: "save" },
8034
- { name: "\u23EA \uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30", value: "undo" },
8035
- { name: "\u{1F50D} \uBCC0\uACBD\uC0AC\uD56D \uBCF4\uAE30", value: "diff" },
8036
- { name: "\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uBCF4\uAE30", value: "status" }
8037
- ]
8118
+ pageSize: choices.length,
8119
+ // 스크롤 잔상/잘림 방지(Windows conhost): 화면에 전부
8120
+ loop: false,
8121
+ choices
8038
8122
  }]);
8039
8123
  switch (choice) {
8124
+ case "work":
8125
+ return work();
8126
+ case "work-handoff":
8127
+ return workHandoff();
8128
+ case "goal-next":
8129
+ return goalNext();
8040
8130
  case "gate":
8041
8131
  return gate();
8042
8132
  case "start":
@@ -8065,7 +8155,7 @@ program.action(async () => {
8065
8155
  });
8066
8156
  var getRealPath = (p) => {
8067
8157
  try {
8068
- return fs13.realpathSync(p);
8158
+ return fs14.realpathSync(p);
8069
8159
  } catch {
8070
8160
  return p;
8071
8161
  }
package/dist/mcp/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  resolveVhkCliInvocation,
4
4
  startMcpServer
5
- } from "../chunk-MN6LSPN6.js";
5
+ } from "../chunk-Y7SJVHGS.js";
6
6
 
7
7
  // src/mcp/index.ts
8
8
  var cli = resolveVhkCliInvocation();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "packageManager": "pnpm@11.2.2",
5
5
  "description": "Vibe Harness Kit — AI 코딩 도구·기기를 바꿔도 규칙·맥락이 따라가는 포터빌리티 CLI (sync: Cursor·Claude·Windsurf·Copilot·Antigravity / cloud 백업)",
6
6
  "bin": {