@byh3071/vhk 2.4.0 → 2.4.2

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.
@@ -716,6 +716,7 @@ var ko = {
716
716
  allOk: "\u{1F389} \uAC1C\uBC1C \uD658\uACBD \uC900\uBE44 \uC644\uB8CC!",
717
717
  missing: "\u26A0\uFE0F \uC77C\uBD80 \uB3C4\uAD6C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
718
718
  missingHint: "\uC704 \uC548\uB0B4\uB97C \uB530\uB77C \uC124\uCE58\uD558\uC138\uC694.",
719
+ warnSummary: (n) => `\u26A0\uFE0F \uACBD\uACE0 ${n}\uAC1C \u2014 \uC704 \uAD8C\uC7A5 \uC870\uCE58\uB97C \uD655\uC778\uD558\uC138\uC694 (\uD544\uC218\uB294 \uC544\uB2D8)`,
719
720
  projectFiles: "\u{1F4C1} \uD504\uB85C\uC81D\uD2B8 \uD30C\uC77C \uD655\uC778:",
720
721
  envNotIgnored: "\u26A0\uFE0F .env\uAC00 .gitignore\uC5D0 \uC5C6\uC74C! \uCD94\uAC00\uD558\uC138\uC694",
721
722
  nextOkMessage: "\uD658\uACBD \uC810\uAC80 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uC2DC\uC791\uD558\uC138\uC694.",
@@ -728,6 +729,21 @@ var ko = {
728
729
  driftRuleWarn: (files) => `\u26A0\uFE0F RULES.md\uC640 \uC5B4\uAE0B\uB09C \uADDC\uCE59 \uD30C\uC77C: ${files} \u2014 vhk sync \uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694`,
729
730
  driftContextWarn: "\u26A0\uFE0F .vhk/context.md \uAC00 \uD604\uC7AC \uCF54\uB4DC\uBCF4\uB2E4 \uB0A1\uC558\uC5B4\uC694 \u2014 vhk context \uB85C \uAC31\uC2E0\uD558\uC138\uC694"
730
731
  },
732
+ preflight: {
733
+ title: "\u{1F6EB} Preflight \u2014 \uCD9C\uACE0 \uC804 \uC548\uC804\uC810\uAC80",
734
+ resultBlocked: (n) => `\uACB0\uACFC: \uCC28\uB2E8 \u2014 \uCE58\uBA85 \uC2E4\uD328 ${n}\uAC1C. \uACE0\uCE5C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.`,
735
+ resultPass: (warn) => warn > 0 ? `\uACB0\uACFC: \uD1B5\uACFC (\uACBD\uACE0 ${warn})` : "\uACB0\uACFC: \uD1B5\uACFC",
736
+ nextBlocked: "\uCE58\uBA85(\u{1F534}) \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uB4A4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694.",
737
+ nextPass: "publish/PR \uC9C4\uD589 \uAC00\uB2A5"
738
+ },
739
+ standup: {
740
+ title: (d) => `\u{1F305} Standup \u2014 ${d}`,
741
+ yesterday: "\u{1F4CC} \uC5B4\uC81C \uD55C \uC77C",
742
+ noHistory: "\uC774\uC804 \uAE30\uB85D \uC5C6\uC74C \u2014 \uCCAB \uC138\uC158\uC774\uAC70\uB098 \uC2E0\uADDC repo",
743
+ todayRecommend: "\u{1F3AF} \uC624\uB298 \uCD94\uCC9C",
744
+ unresolved: "\u26A0\uFE0F \uBBF8\uD574\uACB0",
745
+ commitsLine: (n) => `\uCEE4\uBC0B ${n}\uAC1C`
746
+ },
731
747
  nlp: {
732
748
  matched: "\uC774\uAC8C \uB9DE\uB098\uC694?",
733
749
  notMatched: "\uBB34\uC2A8 \uB73B\uC778\uC9C0 \uBAA8\uB974\uACA0\uC5B4\uC694. vhk\uB97C \uC785\uB825\uD558\uBA74 \uBA54\uB274\uC5D0\uC11C \uC120\uD0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
@@ -1219,9 +1235,27 @@ function pruneBackups(keepN, rootDir) {
1219
1235
  return toDelete.map((b) => b.id);
1220
1236
  }
1221
1237
 
1238
+ // src/lib/atomic-write.ts
1239
+ import { writeFileSync, renameSync, rmSync } from "fs";
1240
+ import { join, dirname, basename } from "path";
1241
+ var writeCounter = 0;
1242
+ function atomicWriteFile(filePath, data) {
1243
+ const tmp = join(dirname(filePath), `.${basename(filePath)}.tmp-${process.pid}-${writeCounter++}`);
1244
+ try {
1245
+ writeFileSync(tmp, data, "utf-8");
1246
+ renameSync(tmp, filePath);
1247
+ } catch (err) {
1248
+ try {
1249
+ rmSync(tmp, { force: true });
1250
+ } catch {
1251
+ }
1252
+ throw err;
1253
+ }
1254
+ }
1255
+
1222
1256
  // src/lib/rules-import.ts
1223
1257
  import { existsSync, readFileSync } from "fs";
1224
- import { join } from "path";
1258
+ import { join as join2 } from "path";
1225
1259
  var ADOPT_SOURCES = [
1226
1260
  ".cursorrules",
1227
1261
  "CLAUDE.md",
@@ -1233,7 +1267,7 @@ var PREAMBLE_TITLE = "\uC11C\uBB38";
1233
1267
  function detectExistingRuleFiles(cwd) {
1234
1268
  const found = [];
1235
1269
  for (const rel of ADOPT_SOURCES) {
1236
- const full = join(cwd, rel);
1270
+ const full = join2(cwd, rel);
1237
1271
  if (existsSync(full)) {
1238
1272
  try {
1239
1273
  found.push({ path: rel, content: readFileSync(full, "utf-8") });
@@ -1591,7 +1625,7 @@ async function syncCore(rootDir, opts, confirmOverwrite) {
1591
1625
  }
1592
1626
  }
1593
1627
  fs4.mkdirSync(path4.join(rootDir, ".vhk"), { recursive: true });
1594
- fs4.writeFileSync(path4.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
1628
+ atomicWriteFile(path4.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n");
1595
1629
  ensureVhkIgnored(rootDir, ".synced");
1596
1630
  return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped, claudeMigration };
1597
1631
  }
@@ -1692,9 +1726,141 @@ ${ko.sync.done}`));
1692
1726
  });
1693
1727
  }
1694
1728
 
1695
- // src/commands/deploy.ts
1696
- import { existsSync as existsSync2 } from "fs";
1729
+ // src/lib/hard-stop-guard.ts
1697
1730
  import chalk4 from "chalk";
1731
+
1732
+ // src/lib/state-files.ts
1733
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, appendFileSync, rmSync as rmSync2 } from "fs";
1734
+ import { join as join3 } from "path";
1735
+ var STATE_DIR = "docs/state";
1736
+ var BLOCKERS_PATH = join3(STATE_DIR, "blockers.md");
1737
+ var LEARNINGS_PATH = join3(STATE_DIR, "learnings.md");
1738
+ var VHK_DIR = ".vhk";
1739
+ var HARD_STOP_PATH = join3(VHK_DIR, "HARD_STOP");
1740
+ var HARD_STOP_BLOCKER_THRESHOLD = 3;
1741
+ function ensureStateDir() {
1742
+ mkdirSync(STATE_DIR, { recursive: true });
1743
+ }
1744
+ function ensureVhkDir() {
1745
+ mkdirSync(VHK_DIR, { recursive: true });
1746
+ }
1747
+ function isoDate() {
1748
+ return localDate();
1749
+ }
1750
+ var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
1751
+ function countActiveBlockers(content) {
1752
+ let count = 0;
1753
+ for (const line of content.split(/\r?\n/)) {
1754
+ if (ACTIVE_BLOCKER_RE.test(line)) count++;
1755
+ }
1756
+ return count;
1757
+ }
1758
+ function appendBlocker(description, goalId) {
1759
+ ensureStateDir();
1760
+ const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
1761
+ const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
1762
+ if (!existsSync2(BLOCKERS_PATH)) {
1763
+ atomicWriteFile(
1764
+ BLOCKERS_PATH,
1765
+ `# Blockers
1766
+
1767
+ _Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
1768
+
1769
+ ${line}
1770
+ `
1771
+ );
1772
+ } else {
1773
+ appendFileSync(BLOCKERS_PATH, `${line}
1774
+ `, "utf-8");
1775
+ }
1776
+ const current = readFileSync2(BLOCKERS_PATH, "utf-8");
1777
+ const count = countActiveBlockers(current);
1778
+ let hardStopTripped = false;
1779
+ if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync2(HARD_STOP_PATH)) {
1780
+ writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
1781
+ hardStopTripped = true;
1782
+ }
1783
+ return { count, hardStopTripped };
1784
+ }
1785
+ function getActiveBlockers(limit = 3) {
1786
+ if (!existsSync2(BLOCKERS_PATH)) return [];
1787
+ const lines = readFileSync2(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
1788
+ const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
1789
+ return entries.slice(-limit);
1790
+ }
1791
+ function writeHardStop(reason) {
1792
+ ensureVhkDir();
1793
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1794
+ atomicWriteFile(HARD_STOP_PATH, `${ts}
1795
+ ${reason}
1796
+ `);
1797
+ }
1798
+ function isHardStopActive() {
1799
+ return existsSync2(HARD_STOP_PATH);
1800
+ }
1801
+ function readHardStopReason() {
1802
+ if (!existsSync2(HARD_STOP_PATH)) return null;
1803
+ try {
1804
+ return readFileSync2(HARD_STOP_PATH, "utf-8").trim();
1805
+ } catch {
1806
+ return null;
1807
+ }
1808
+ }
1809
+ function clearHardStop() {
1810
+ if (!existsSync2(HARD_STOP_PATH)) return false;
1811
+ rmSync2(HARD_STOP_PATH, { force: true });
1812
+ return true;
1813
+ }
1814
+
1815
+ // src/lib/hard-stop-guard.ts
1816
+ function ensureNotHardStopped(action) {
1817
+ if (!isHardStopActive()) return true;
1818
+ console.error(chalk4.red.bold(`
1819
+ \u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
1820
+ const reason = readHardStopReason();
1821
+ if (reason) console.error(chalk4.dim(` \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}`));
1822
+ console.error(chalk4.dim(" \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)"));
1823
+ process.exitCode = 1;
1824
+ return false;
1825
+ }
1826
+
1827
+ // src/lib/version.ts
1828
+ import { existsSync as existsSync3 } from "fs";
1829
+ import { dirname as dirname2, join as join4 } from "path";
1830
+ import { fileURLToPath } from "url";
1831
+
1832
+ // src/lib/read-json.ts
1833
+ import { readFileSync as readFileSync3 } from "fs";
1834
+ function stripBom(text) {
1835
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1836
+ }
1837
+ function readJsonFile(filePath) {
1838
+ const raw = stripBom(readFileSync3(filePath, "utf-8"));
1839
+ return JSON.parse(raw);
1840
+ }
1841
+
1842
+ // src/lib/version.ts
1843
+ function getVhkVersion() {
1844
+ const dir = dirname2(fileURLToPath(import.meta.url));
1845
+ for (const pkgPath of [
1846
+ join4(dir, "../../package.json"),
1847
+ join4(dir, "../package.json")
1848
+ ]) {
1849
+ try {
1850
+ if (existsSync3(pkgPath)) {
1851
+ const pkg = readJsonFile(pkgPath);
1852
+ if (pkg.version) return pkg.version;
1853
+ }
1854
+ } catch {
1855
+ continue;
1856
+ }
1857
+ }
1858
+ return "0.0.0";
1859
+ }
1860
+
1861
+ // src/commands/deploy.ts
1862
+ import { existsSync as existsSync4 } from "fs";
1863
+ import chalk5 from "chalk";
1698
1864
  import inquirer2 from "inquirer";
1699
1865
 
1700
1866
  // src/lib/exec.ts
@@ -1794,7 +1960,7 @@ var PLATFORMS = {
1794
1960
  function detectPlatform() {
1795
1961
  for (const [key, config] of Object.entries(PLATFORMS)) {
1796
1962
  for (const file of config.detectFiles) {
1797
- if (existsSync2(file)) return key;
1963
+ if (existsSync4(file)) return key;
1798
1964
  }
1799
1965
  }
1800
1966
  return null;
@@ -1803,11 +1969,11 @@ function isCLIAvailable(cmd, checkArgs) {
1803
1969
  return safeExecFile(cmd, checkArgs).ok;
1804
1970
  }
1805
1971
  async function deploy() {
1806
- console.log(chalk4.bold("\n\u{1F680} " + t("deploy.title")));
1807
- console.log(chalk4.gray("\u2500".repeat(40)));
1972
+ console.log(chalk5.bold("\n\u{1F680} " + t("deploy.title")));
1973
+ console.log(chalk5.gray("\u2500".repeat(40)));
1808
1974
  let platform = detectPlatform();
1809
1975
  if (platform) {
1810
- console.log(chalk4.cyan(`
1976
+ console.log(chalk5.cyan(`
1811
1977
  \u{1F50D} \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${PLATFORMS[platform].name}`));
1812
1978
  } else {
1813
1979
  const { selected } = await inquirer2.prompt([
@@ -1826,9 +1992,9 @@ async function deploy() {
1826
1992
  }
1827
1993
  const config = PLATFORMS[platform];
1828
1994
  if (!isCLIAvailable(config.command, config.checkArgs)) {
1829
- console.log(chalk4.red(`
1995
+ console.log(chalk5.red(`
1830
1996
  \u274C ${config.name} CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
1831
- console.log(chalk4.yellow(` \u2192 ${config.installHint}`));
1997
+ console.log(chalk5.yellow(` \u2192 ${config.installHint}`));
1832
1998
  return;
1833
1999
  }
1834
2000
  const { confirm } = await inquirer2.prompt([
@@ -1840,15 +2006,15 @@ async function deploy() {
1840
2006
  }
1841
2007
  ]);
1842
2008
  if (!confirm) {
1843
- console.log(chalk4.gray("\uCDE8\uC18C\uB428"));
2009
+ console.log(chalk5.gray("\uCDE8\uC18C\uB428"));
1844
2010
  return;
1845
2011
  }
1846
- console.log(chalk4.cyan(`
2012
+ console.log(chalk5.cyan(`
1847
2013
  ${t("deploy.deploying")}
1848
2014
  `));
1849
2015
  const result = safeExecFileStream(config.command, config.commandArgs);
1850
2016
  if (result.ok) {
1851
- console.log(chalk4.green(`
2017
+ console.log(chalk5.green(`
1852
2018
  \u2705 ${t("deploy.success")}`));
1853
2019
  printNextStep({
1854
2020
  message: "\uBC30\uD3EC \uC644\uB8CC! \uC0AC\uC774\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.",
@@ -1856,16 +2022,16 @@ ${t("deploy.deploying")}
1856
2022
  cursorHint: "\uC0C1\uD0DC \uD655\uC778\uD574\uC918"
1857
2023
  });
1858
2024
  } else {
1859
- console.log(chalk4.red(`
2025
+ console.log(chalk5.red(`
1860
2026
  \u274C ${t("deploy.failed")}`));
1861
- console.log(chalk4.red(result.err));
2027
+ console.log(chalk5.red(result.err));
1862
2028
  }
1863
2029
  }
1864
2030
 
1865
2031
  // src/commands/env.ts
1866
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, appendFileSync, readdirSync } from "fs";
1867
- import { join as join2 } from "path";
1868
- import chalk5 from "chalk";
2032
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, readdirSync } from "fs";
2033
+ import { join as join5 } from "path";
2034
+ import chalk6 from "chalk";
1869
2035
  function parseEnvKeys(content) {
1870
2036
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
1871
2037
  }
@@ -1881,7 +2047,7 @@ function loadDefinedEnvKeys(dir = ".") {
1881
2047
  if (!name.startsWith(".env")) continue;
1882
2048
  if (name.endsWith(".example") || name.endsWith(".sample")) continue;
1883
2049
  try {
1884
- for (const k of parseEnvKeys(readFileSync2(join2(dir, name), "utf-8"))) keys.add(k);
2050
+ for (const k of parseEnvKeys(readFileSync4(join5(dir, name), "utf-8"))) keys.add(k);
1885
2051
  } catch {
1886
2052
  }
1887
2053
  }
@@ -1889,36 +2055,37 @@ function loadDefinedEnvKeys(dir = ".") {
1889
2055
  }
1890
2056
  function ensureGitignore() {
1891
2057
  const gitignorePath = ".gitignore";
1892
- if (existsSync3(gitignorePath)) {
1893
- const content = readFileSync2(gitignorePath, "utf-8");
2058
+ if (existsSync5(gitignorePath)) {
2059
+ const content = readFileSync4(gitignorePath, "utf-8");
1894
2060
  if (!content.split("\n").some((l) => l.trim() === ".env")) {
1895
- appendFileSync(gitignorePath, "\n.env\n");
1896
- console.log(chalk5.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
2061
+ appendFileSync2(gitignorePath, "\n.env\n");
2062
+ console.log(chalk6.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
1897
2063
  }
1898
2064
  } else {
1899
- writeFileSync(gitignorePath, ".env\nnode_modules/\ndist/\n");
1900
- console.log(chalk5.green("\n\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)"));
2065
+ writeFileSync2(gitignorePath, ".env\nnode_modules/\ndist/\n");
2066
+ console.log(chalk6.green("\n\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)"));
1901
2067
  }
1902
2068
  }
1903
2069
  async function env() {
1904
- console.log(chalk5.bold("\n\u{1F510} " + t("env.title")));
1905
- console.log(chalk5.gray("\u2500".repeat(40)));
1906
- if (!existsSync3(".env")) {
1907
- console.log(chalk5.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
1908
- console.log(chalk5.gray(" .env \uD30C\uC77C\uC744 \uBA3C\uC800 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694."));
2070
+ if (!ensureNotHardStopped("env")) return;
2071
+ console.log(chalk6.bold("\n\u{1F510} " + t("env.title")));
2072
+ console.log(chalk6.gray("\u2500".repeat(40)));
2073
+ if (!existsSync5(".env")) {
2074
+ console.log(chalk6.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
2075
+ console.log(chalk6.gray(" .env \uD30C\uC77C\uC744 \uBA3C\uC800 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694."));
1909
2076
  return;
1910
2077
  }
1911
- const envContent = readFileSync2(".env", "utf-8");
2078
+ const envContent = readFileSync4(".env", "utf-8");
1912
2079
  const keys = parseEnvKeys(envContent);
1913
2080
  if (keys.length === 0) {
1914
- console.log(chalk5.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
2081
+ console.log(chalk6.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
1915
2082
  return;
1916
2083
  }
1917
2084
  const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
1918
- writeFileSync(".env.example", exampleContent, "utf-8");
1919
- console.log(chalk5.green(`
2085
+ writeFileSync2(".env.example", exampleContent, "utf-8");
2086
+ console.log(chalk6.green(`
1920
2087
  \u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`));
1921
- keys.forEach((k) => console.log(chalk5.gray(` ${k}`)));
2088
+ keys.forEach((k) => console.log(chalk6.gray(` ${k}`)));
1922
2089
  ensureGitignore();
1923
2090
  printNextStep({
1924
2091
  message: ".env.example \uC0DD\uC131 \uC644\uB8CC!",
@@ -1927,51 +2094,39 @@ async function env() {
1927
2094
  });
1928
2095
  }
1929
2096
  async function envCheck() {
1930
- console.log(chalk5.bold("\n\u{1F50D} " + t("env.checkTitle")));
1931
- console.log(chalk5.gray("\u2500".repeat(40)));
1932
- if (!existsSync3(".env.example")) {
1933
- console.log(chalk5.yellow("\n\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 vhk env\uB97C \uC2E4\uD589\uD558\uC138\uC694."));
2097
+ console.log(chalk6.bold("\n\u{1F50D} " + t("env.checkTitle")));
2098
+ console.log(chalk6.gray("\u2500".repeat(40)));
2099
+ if (!existsSync5(".env.example")) {
2100
+ console.log(chalk6.yellow("\n\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 vhk env\uB97C \uC2E4\uD589\uD558\uC138\uC694."));
1934
2101
  return;
1935
2102
  }
1936
- const requiredKeys = parseEnvKeys(readFileSync2(".env.example", "utf-8"));
2103
+ const requiredKeys = parseEnvKeys(readFileSync4(".env.example", "utf-8"));
1937
2104
  const currentKeys = loadDefinedEnvKeys();
1938
2105
  const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
1939
2106
  const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
1940
- console.log(chalk5.cyan(`
2107
+ console.log(chalk6.cyan(`
1941
2108
  \u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`));
1942
2109
  if (missing.length === 0) {
1943
- console.log(chalk5.green("\n\u2705 \uBAA8\uB4E0 \uD544\uC218 \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4!"));
2110
+ console.log(chalk6.green("\n\u2705 \uBAA8\uB4E0 \uD544\uC218 \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4!"));
1944
2111
  } else {
1945
- console.log(chalk5.red(`
2112
+ console.log(chalk6.red(`
1946
2113
  \u274C \uB204\uB77D\uB41C \uD658\uACBD\uBCC0\uC218 (${missing.length}\uAC1C):`));
1947
- missing.forEach((k) => console.log(chalk5.red(` \u2022 ${k}`)));
2114
+ missing.forEach((k) => console.log(chalk6.red(` \u2022 ${k}`)));
1948
2115
  process.exitCode = 1;
1949
2116
  }
1950
2117
  if (extra.length > 0) {
1951
- console.log(chalk5.yellow(`
2118
+ console.log(chalk6.yellow(`
1952
2119
  \u{1F4A1} .env.example\uC5D0 \uC5C6\uB294 \uCD94\uAC00 \uBCC0\uC218 (${extra.length}\uAC1C):`));
1953
- extra.forEach((k) => console.log(chalk5.yellow(` \u2022 ${k}`)));
2120
+ extra.forEach((k) => console.log(chalk6.yellow(` \u2022 ${k}`)));
1954
2121
  }
1955
2122
  ensureGitignore();
1956
2123
  }
1957
2124
 
1958
2125
  // src/commands/publish.ts
1959
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1960
- import chalk6 from "chalk";
2126
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2127
+ import chalk7 from "chalk";
1961
2128
  import inquirer3 from "inquirer";
1962
2129
  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
2130
  function bumpVersion(current, type) {
1976
2131
  const [major, minor, patch] = current.split(".").map((n) => parseInt(n, 10) || 0);
1977
2132
  switch (type) {
@@ -2001,7 +2156,9 @@ function bumpClaudeMdVersion(content, newVersion) {
2001
2156
  return content.replace(/(\*\*버전:\*\*\s*v)\d+\.\d+\.\d+/, `$1${newVersion}`);
2002
2157
  }
2003
2158
  function gitPostRelease(newVersion) {
2004
- const filesToAdd = existsSync4("CHANGELOG.md") ? ["package.json", "CHANGELOG.md"] : ["package.json"];
2159
+ const filesToAdd = ["package.json"];
2160
+ if (existsSync6("CHANGELOG.md")) filesToAdd.push("CHANGELOG.md");
2161
+ if (existsSync6("CLAUDE.md")) filesToAdd.push("CLAUDE.md");
2005
2162
  const add = safeExecFile("git", ["add", ...filesToAdd]);
2006
2163
  if (!add.ok) {
2007
2164
  return {
@@ -2058,28 +2215,28 @@ function publishPreflight() {
2058
2215
  return { ...evaluatePublishPreflight(branch, trackedStatus, defaultBranch), branch, defaultBranch };
2059
2216
  }
2060
2217
  async function publish() {
2061
- console.log(chalk6.bold("\n\u{1F4E6} " + t("publish.title")));
2062
- console.log(chalk6.gray("\u2500".repeat(40)));
2218
+ console.log(chalk7.bold("\n\u{1F4E6} " + t("publish.title")));
2219
+ console.log(chalk7.gray("\u2500".repeat(40)));
2063
2220
  const pre = publishPreflight();
2064
2221
  if (!pre.ok) {
2065
2222
  const msg = pre.code === "wrong-branch" ? t("publish.preflightWrongBranch", pre.branch || "(detached)", pre.defaultBranch) : t("publish.preflightDirty");
2066
- console.log(chalk6.red(`
2223
+ console.log(chalk7.red(`
2067
2224
  \u274C ${msg}`));
2068
2225
  return;
2069
2226
  }
2070
- if (!existsSync4("package.json")) {
2071
- console.log(chalk6.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2227
+ if (!existsSync6("package.json")) {
2228
+ console.log(chalk7.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
2072
2229
  return;
2073
2230
  }
2074
2231
  let pkg;
2075
2232
  try {
2076
2233
  pkg = readJsonFile("package.json");
2077
2234
  } catch {
2078
- console.log(chalk6.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
2235
+ console.log(chalk7.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
2079
2236
  return;
2080
2237
  }
2081
2238
  const currentVersion = pkg.version || "0.0.0";
2082
- console.log(chalk6.cyan(`
2239
+ console.log(chalk7.cyan(`
2083
2240
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
2084
2241
  const { bumpType } = await inquirer3.prompt([
2085
2242
  {
@@ -2094,29 +2251,29 @@ async function publish() {
2094
2251
  }
2095
2252
  ]);
2096
2253
  const newVersion = bumpVersion(currentVersion, bumpType);
2097
- console.log(chalk6.cyan(`
2254
+ console.log(chalk7.cyan(`
2098
2255
  \u{1F195} \uC0C8 \uBC84\uC804: v${newVersion}`));
2099
2256
  pkg.version = newVersion;
2100
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2101
- 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;
2257
+ writeFileSync3("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2258
+ console.log(chalk7.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
2259
+ const claudeMdOriginal = existsSync6("CLAUDE.md") ? readFileSync5("CLAUDE.md", "utf-8") : null;
2103
2260
  if (claudeMdOriginal !== null) {
2104
2261
  const bumped = bumpClaudeMdVersion(claudeMdOriginal, newVersion);
2105
2262
  if (bumped !== claudeMdOriginal) {
2106
- writeFileSync2("CLAUDE.md", bumped, "utf-8");
2107
- console.log(chalk6.green("\u2705 CLAUDE.md \uBC84\uC804\uC904 \uB3D9\uAE30\uD654"));
2263
+ writeFileSync3("CLAUDE.md", bumped, "utf-8");
2264
+ console.log(chalk7.green("\u2705 CLAUDE.md \uBC84\uC804\uC904 \uB3D9\uAE30\uD654"));
2108
2265
  }
2109
2266
  }
2110
2267
  const rollbackVersion = () => {
2111
2268
  pkg.version = currentVersion;
2112
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2113
- if (claudeMdOriginal !== null) writeFileSync2("CLAUDE.md", claudeMdOriginal, "utf-8");
2269
+ writeFileSync3("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2270
+ if (claudeMdOriginal !== null) writeFileSync3("CLAUDE.md", claudeMdOriginal, "utf-8");
2114
2271
  };
2115
2272
  const buildSpinner = ora(t("publish.building")).start();
2116
2273
  const buildResult = safeExecFile("pnpm", ["build"]);
2117
2274
  if (!buildResult.ok) {
2118
2275
  buildSpinner.fail(t("publish.buildFailed"));
2119
- console.log(chalk6.red(buildResult.err.slice(0, 500)));
2276
+ console.log(chalk7.red(buildResult.err.slice(0, 500)));
2120
2277
  rollbackVersion();
2121
2278
  return;
2122
2279
  }
@@ -2125,7 +2282,7 @@ async function publish() {
2125
2282
  const testResult = safeExecFile("pnpm", ["test", "--run"]);
2126
2283
  if (!testResult.ok) {
2127
2284
  testSpinner.fail(t("publish.testFailed"));
2128
- console.log(chalk6.red(testResult.err.slice(0, 500)));
2285
+ console.log(chalk7.red(testResult.err.slice(0, 500)));
2129
2286
  rollbackVersion();
2130
2287
  return;
2131
2288
  }
@@ -2140,45 +2297,45 @@ async function publish() {
2140
2297
  ]);
2141
2298
  if (!confirm) {
2142
2299
  rollbackVersion();
2143
- console.log(chalk6.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
2300
+ console.log(chalk7.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
2144
2301
  return;
2145
2302
  }
2146
- console.log(chalk6.cyan(`
2303
+ console.log(chalk7.cyan(`
2147
2304
  \u{1F4E4} ${t("publish.publishing")}`));
2148
- console.log(chalk6.gray(" 2FA \uD65C\uC131\uD654 \uC2DC: OTP 6\uC790\uB9AC \uC785\uB825 \uB610\uB294 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D URL \uD074\uB9AD (Windows Hello / PIN \uC9C0\uC6D0)"));
2305
+ console.log(chalk7.gray(" 2FA \uD65C\uC131\uD654 \uC2DC: OTP 6\uC790\uB9AC \uC785\uB825 \uB610\uB294 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D URL \uD074\uB9AD (Windows Hello / PIN \uC9C0\uC6D0)"));
2149
2306
  const pubResult = safeExecFileStream("npm", ["publish", "--access", "public"]);
2150
2307
  if (!pubResult.ok) {
2151
- console.log(chalk6.red(`
2308
+ console.log(chalk7.red(`
2152
2309
  \u2716 ${t("publish.publishFailed")}`));
2153
- console.log(chalk6.red(pubResult.err.slice(0, 500)));
2310
+ console.log(chalk7.red(pubResult.err.slice(0, 500)));
2154
2311
  rollbackVersion();
2155
- console.log(chalk6.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
2312
+ console.log(chalk7.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
2156
2313
  return;
2157
2314
  }
2158
- console.log(chalk6.green(`
2315
+ console.log(chalk7.green(`
2159
2316
  \u2714 ${t("publish.publishSuccess")}`));
2160
- if (existsSync4("CHANGELOG.md")) {
2161
- const cl = readFileSync4("CHANGELOG.md", "utf-8");
2162
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2317
+ if (existsSync6("CHANGELOG.md")) {
2318
+ const cl = readFileSync5("CHANGELOG.md", "utf-8");
2319
+ const date = localDate();
2163
2320
  const updated = insertChangelogStub(cl, newVersion, date);
2164
2321
  if (updated !== cl) {
2165
- writeFileSync2("CHANGELOG.md", updated, "utf-8");
2166
- console.log(chalk6.green(`\u2705 CHANGELOG.md \uC5D0 [${newVersion}] \uC2A4\uD141 \uCD94\uAC00 \u2014 \uBCF8\uBB38 \uBCF4\uAC15 \uD544\uC694`));
2322
+ writeFileSync3("CHANGELOG.md", updated, "utf-8");
2323
+ console.log(chalk7.green(`\u2705 CHANGELOG.md \uC5D0 [${newVersion}] \uC2A4\uD141 \uCD94\uAC00 \u2014 \uBCF8\uBB38 \uBCF4\uAC15 \uD544\uC694`));
2167
2324
  }
2168
2325
  }
2169
2326
  const git = gitPostRelease(newVersion);
2170
2327
  if (git.warning) {
2171
- console.log(chalk6.yellow(`
2328
+ console.log(chalk7.yellow(`
2172
2329
  \u26A0\uFE0F ${git.warning}`));
2173
- console.log(chalk6.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
2330
+ console.log(chalk7.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
2174
2331
  } else if (git.tagged && git.pushed) {
2175
- console.log(chalk6.green(`
2332
+ console.log(chalk7.green(`
2176
2333
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
2177
2334
  } else if (git.tagged) {
2178
- console.log(chalk6.yellow(`
2335
+ console.log(chalk7.yellow(`
2179
2336
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
2180
2337
  }
2181
- console.log(chalk6.green.bold(`
2338
+ console.log(chalk7.green.bold(`
2182
2339
  \u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
2183
2340
  printNextStep({
2184
2341
  message: "npm \uBC30\uD3EC \uC644\uB8CC!",
@@ -2188,13 +2345,13 @@ async function publish() {
2188
2345
  }
2189
2346
 
2190
2347
  // src/commands/audit.ts
2191
- import { existsSync as existsSync5 } from "fs";
2192
- import chalk7 from "chalk";
2348
+ import { existsSync as existsSync7 } from "fs";
2349
+ import chalk8 from "chalk";
2193
2350
  import inquirer4 from "inquirer";
2194
2351
  import ora2 from "ora";
2195
2352
  function detectCurrentPM() {
2196
- if (existsSync5("pnpm-lock.yaml")) return "pnpm";
2197
- if (existsSync5("yarn.lock")) return "yarn";
2353
+ if (existsSync7("pnpm-lock.yaml")) return "pnpm";
2354
+ if (existsSync7("yarn.lock")) return "yarn";
2198
2355
  return "npm";
2199
2356
  }
2200
2357
  function parseAuditOutput(output, pm) {
@@ -2234,24 +2391,24 @@ function runAuditFix(pm) {
2234
2391
  return result.ok ? { ok: true } : { ok: false, err: result.err };
2235
2392
  }
2236
2393
  async function audit(autoFix = false) {
2237
- console.log(chalk7.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
2238
- console.log(chalk7.gray("\u2500".repeat(40)));
2394
+ console.log(chalk8.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
2395
+ console.log(chalk8.gray("\u2500".repeat(40)));
2239
2396
  const pm = detectCurrentPM();
2240
- console.log(chalk7.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
2397
+ console.log(chalk8.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
2241
2398
  const spinner = ora2("\uBCF4\uC548 \uAC10\uC0AC \uC2E4\uD589 \uC911...").start();
2242
2399
  const output = runAuditJson(pm);
2243
2400
  spinner.stop();
2244
2401
  const summary = parseAuditOutput(output, pm);
2245
2402
  if (summary.total === 0) {
2246
- console.log(chalk7.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
2403
+ console.log(chalk8.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
2247
2404
  return;
2248
2405
  }
2249
- console.log(chalk7.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
2250
- if (summary.critical > 0) console.log(chalk7.red(` \u{1F534} Critical: ${summary.critical}`));
2251
- if (summary.high > 0) console.log(chalk7.red(` \u{1F7E0} High: ${summary.high}`));
2252
- if (summary.moderate > 0) console.log(chalk7.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
2253
- if (summary.low > 0) console.log(chalk7.gray(` \u26AA Low: ${summary.low}`));
2254
- console.log(chalk7.bold(`
2406
+ console.log(chalk8.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
2407
+ if (summary.critical > 0) console.log(chalk8.red(` \u{1F534} Critical: ${summary.critical}`));
2408
+ if (summary.high > 0) console.log(chalk8.red(` \u{1F7E0} High: ${summary.high}`));
2409
+ if (summary.moderate > 0) console.log(chalk8.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
2410
+ if (summary.low > 0) console.log(chalk8.gray(` \u26AA Low: ${summary.low}`));
2411
+ console.log(chalk8.bold(`
2255
2412
  \uCD1D ${summary.total}\uAC1C\uC758 \uCDE8\uC57D\uC810`));
2256
2413
  const shouldRunFix = autoFix ? true : summary.critical > 0 || summary.high > 0 ? (await inquirer4.prompt([
2257
2414
  {
@@ -2277,32 +2434,10 @@ async function audit(autoFix = false) {
2277
2434
  });
2278
2435
  }
2279
2436
 
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
2437
  // src/mcp/cli-path.ts
2303
- import { existsSync as existsSync7 } from "fs";
2438
+ import { existsSync as existsSync8 } from "fs";
2304
2439
  import { fileURLToPath as fileURLToPath2 } from "url";
2305
- import { dirname as dirname2, resolve } from "path";
2440
+ import { dirname as dirname3, resolve } from "path";
2306
2441
  function pickCliInvocation(globalAvailable, localCli, localExists) {
2307
2442
  if (globalAvailable) return { bin: "vhk", prefixArgs: [], fallback: false };
2308
2443
  if (localExists) return { bin: process.execPath, prefixArgs: [localCli], fallback: true };
@@ -2312,20 +2447,20 @@ function composeInvocation(cli, args) {
2312
2447
  return { bin: cli.bin, args: [...cli.prefixArgs, ...args] };
2313
2448
  }
2314
2449
  function localCliPath() {
2315
- const here = dirname2(fileURLToPath2(import.meta.url));
2450
+ const here = dirname3(fileURLToPath2(import.meta.url));
2316
2451
  return resolve(here, "..", "index.js");
2317
2452
  }
2318
2453
  function resolveVhkCliInvocation() {
2319
2454
  const globalAvailable = safeExecFile("vhk", ["--version"]).ok;
2320
2455
  const local = localCliPath();
2321
- return pickCliInvocation(globalAvailable, local, existsSync7(local));
2456
+ return pickCliInvocation(globalAvailable, local, existsSync8(local));
2322
2457
  }
2323
2458
 
2324
2459
  // src/mcp/server.ts
2325
2460
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2326
2461
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2327
2462
  import { z } from "zod";
2328
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2 } from "fs";
2463
+ import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync4, appendFileSync as appendFileSync3 } from "fs";
2329
2464
 
2330
2465
  // src/lib/scan-secrets.ts
2331
2466
  import fs7 from "fs";
@@ -2401,7 +2536,7 @@ import path6 from "path";
2401
2536
  var import_ignore = __toESM(require_ignore(), 1);
2402
2537
  import fs5 from "fs";
2403
2538
  import path5 from "path";
2404
- import chalk8 from "chalk";
2539
+ import chalk9 from "chalk";
2405
2540
  function loadGitignore(rootDir) {
2406
2541
  const ig = (0, import_ignore.default)();
2407
2542
  const gitignorePath = path5.join(rootDir, ".gitignore");
@@ -2475,7 +2610,7 @@ function printSecurityWarnings(rootDir = process.cwd()) {
2475
2610
  const result = checkProjectSecurity(rootDir);
2476
2611
  if (result.ok) return true;
2477
2612
  for (const w of result.warnings) {
2478
- console.log(chalk8.yellow(` \u26A0\uFE0F ${w}`));
2613
+ console.log(chalk9.yellow(` \u26A0\uFE0F ${w}`));
2479
2614
  }
2480
2615
  return false;
2481
2616
  }
@@ -2616,6 +2751,14 @@ var SERVER_VERSION = getVhkVersion();
2616
2751
  function isGitRepo() {
2617
2752
  return safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok;
2618
2753
  }
2754
+ function hardStopBlocked(action) {
2755
+ if (!isHardStopActive()) return null;
2756
+ const reason = readHardStopReason();
2757
+ const text = `\u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.` + (reason ? `
2758
+ \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}` : "") + `
2759
+ \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)`;
2760
+ return { content: [{ type: "text", text }] };
2761
+ }
2619
2762
  var ANSI_RE = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
2620
2763
  function stripAnsi(s) {
2621
2764
  return s.replace(ANSI_RE, "");
@@ -2649,6 +2792,8 @@ function createVhkMcpServer() {
2649
2792
  }
2650
2793
  },
2651
2794
  async ({ message }) => {
2795
+ const blocked = hardStopBlocked("save");
2796
+ if (blocked) return blocked;
2652
2797
  if (!isGitRepo()) {
2653
2798
  return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
2654
2799
  }
@@ -2710,6 +2855,8 @@ ${preview}${more}
2710
2855
  }
2711
2856
  },
2712
2857
  async ({ confirm }) => {
2858
+ const blocked = hardStopBlocked("undo");
2859
+ if (blocked) return blocked;
2713
2860
  if (!isGitRepo()) {
2714
2861
  return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
2715
2862
  }
@@ -2750,7 +2897,7 @@ ${last.out}
2750
2897
  );
2751
2898
  server.registerTool("status", { description: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC (\uBE0C\uB79C\uCE58/\uBCC0\uACBD\uC0AC\uD56D/\uCD5C\uADFC \uCEE4\uBC0B)" }, async () => {
2752
2899
  const lines = [];
2753
- if (existsSync8("package.json")) {
2900
+ if (existsSync9("package.json")) {
2754
2901
  try {
2755
2902
  const pkg = readJsonFile("package.json");
2756
2903
  lines.push(`\u{1F4E6} \uD504\uB85C\uC81D\uD2B8: ${pkg.name ?? "(\uC774\uB984 \uC5C6\uC74C)"} v${pkg.version ?? "?"}`);
@@ -2835,7 +2982,7 @@ ${last.out}
2835
2982
  checks.push(build.ok ? "\u2705 \uBE4C\uB4DC \uC131\uACF5" : "\u274C \uBE4C\uB4DC \uC2E4\uD328");
2836
2983
  const test = safeExecFile("pnpm", ["test", "--run"]);
2837
2984
  checks.push(test.ok ? "\u2705 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC" : "\u274C \uD14C\uC2A4\uD2B8 \uC2E4\uD328");
2838
- if (existsSync8("package.json")) {
2985
+ if (existsSync9("package.json")) {
2839
2986
  try {
2840
2987
  const pkg = readJsonFile("package.json");
2841
2988
  checks.push(`\u{1F4E6} \uBC84\uC804: ${pkg.version}`);
@@ -2873,11 +3020,11 @@ ${last.out}
2873
3020
  const recommended = ["CLAUDE.md", ".cursorrules", "docs/PRD.md", "docs/ARCHITECTURE.md"];
2874
3021
  const lines = ["\u{1F50D} \uD504\uB85C\uC81D\uD2B8 \uC810\uAC80", "", "\uD544\uC218:"];
2875
3022
  required.forEach((f) => {
2876
- lines.push(` ${existsSync8(f) ? "\u2705" : "\u274C"} ${f}`);
3023
+ lines.push(` ${existsSync9(f) ? "\u2705" : "\u274C"} ${f}`);
2877
3024
  });
2878
3025
  lines.push("", "\uAD8C\uC7A5 (VHK \uD558\uB124\uC2A4):");
2879
3026
  recommended.forEach((f) => {
2880
- lines.push(` ${existsSync8(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
3027
+ lines.push(` ${existsSync9(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
2881
3028
  });
2882
3029
  return { content: [{ type: "text", text: lines.join("\n") }] };
2883
3030
  });
@@ -2911,24 +3058,26 @@ ${log.out}` }] };
2911
3058
  description: ".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore\uC5D0 .env \uC790\uB3D9 \uCD94\uAC00"
2912
3059
  },
2913
3060
  async () => {
2914
- if (!existsSync8(".env")) {
3061
+ const blocked = hardStopBlocked("env");
3062
+ if (blocked) return blocked;
3063
+ if (!existsSync9(".env")) {
2915
3064
  return { content: [{ type: "text", text: "\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 .env\uB97C \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694." }] };
2916
3065
  }
2917
- const keys = parseEnvKeys(readFileSync5(".env", "utf-8"));
3066
+ const keys = parseEnvKeys(readFileSync6(".env", "utf-8"));
2918
3067
  if (keys.length === 0) {
2919
3068
  return { content: [{ type: "text", text: "\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
2920
3069
  }
2921
3070
  const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
2922
- writeFileSync3(".env.example", exampleContent, "utf-8");
3071
+ writeFileSync4(".env.example", exampleContent, "utf-8");
2923
3072
  const gitignoreLines = [];
2924
- if (existsSync8(".gitignore")) {
2925
- const content = readFileSync5(".gitignore", "utf-8");
3073
+ if (existsSync9(".gitignore")) {
3074
+ const content = readFileSync6(".gitignore", "utf-8");
2926
3075
  if (!content.split("\n").some((l) => l.trim() === ".env")) {
2927
- appendFileSync2(".gitignore", "\n.env\n");
3076
+ appendFileSync3(".gitignore", "\n.env\n");
2928
3077
  gitignoreLines.push("\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428");
2929
3078
  }
2930
3079
  } else {
2931
- writeFileSync3(".gitignore", ".env\nnode_modules/\ndist/\n");
3080
+ writeFileSync4(".gitignore", ".env\nnode_modules/\ndist/\n");
2932
3081
  gitignoreLines.push("\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)");
2933
3082
  }
2934
3083
  const lines = [`\u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`, ...keys.map((k) => ` ${k}`)];
@@ -2942,11 +3091,11 @@ ${log.out}` }] };
2942
3091
  description: "\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC (.env.example \uAE30\uC900)"
2943
3092
  },
2944
3093
  async () => {
2945
- if (!existsSync8(".env.example")) {
3094
+ if (!existsSync9(".env.example")) {
2946
3095
  return { content: [{ type: "text", text: "\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 env \uB3C4\uAD6C\uB97C \uC2E4\uD589\uD558\uC138\uC694." }] };
2947
3096
  }
2948
- const requiredKeys = parseEnvKeys(readFileSync5(".env.example", "utf-8"));
2949
- const currentKeys = existsSync8(".env") ? parseEnvKeys(readFileSync5(".env", "utf-8")) : [];
3097
+ const requiredKeys = parseEnvKeys(readFileSync6(".env.example", "utf-8"));
3098
+ const currentKeys = existsSync9(".env") ? parseEnvKeys(readFileSync6(".env", "utf-8")) : [];
2950
3099
  const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
2951
3100
  const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
2952
3101
  const lines = [`\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`];
@@ -3062,7 +3211,7 @@ ${cliStatus}
3062
3211
  description: "\uD604\uC7AC \uBC84\uC804 + bump \uD6C4\uBCF4 \uD45C\uC2DC (MCP \uBAA8\uB4DC: \uC2E4\uC81C npm publish \uBBF8\uC218\uD589 \u2014 `vhk publish` \uC548\uB0B4)"
3063
3212
  },
3064
3213
  async () => {
3065
- if (!existsSync8("package.json")) {
3214
+ if (!existsSync9("package.json")) {
3066
3215
  return { content: [{ type: "text", text: "\u274C package.json \uC5C6\uC74C." }] };
3067
3216
  }
3068
3217
  try {
@@ -3090,7 +3239,7 @@ ${cliStatus}
3090
3239
  description: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uAC10\uC9C0 + \uC804\uD658 \uD6C4\uBCF4 \uAC00\uC6A9\uC131 (MCP \uBAA8\uB4DC: \uC2E4\uC81C \uC804\uD658 \uBBF8\uC218\uD589 \u2014 `vhk migrate <target>` \uC548\uB0B4)"
3091
3240
  },
3092
3241
  async () => {
3093
- const current = existsSync8("pnpm-lock.yaml") ? "pnpm" : existsSync8("yarn.lock") ? "yarn" : existsSync8("package-lock.json") ? "npm" : null;
3242
+ const current = existsSync9("pnpm-lock.yaml") ? "pnpm" : existsSync9("yarn.lock") ? "yarn" : existsSync9("package-lock.json") ? "npm" : null;
3094
3243
  const candidates = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current);
3095
3244
  const lines = [`\uD604\uC7AC PM: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00 (lock \uD30C\uC77C \uC5C6\uC74C)"}`];
3096
3245
  for (const pm of candidates) {
@@ -3222,6 +3371,7 @@ export {
3222
3371
  ensureVhkIgnored,
3223
3372
  listBackups,
3224
3373
  restoreBackup,
3374
+ atomicWriteFile,
3225
3375
  detectExistingRuleFiles,
3226
3376
  buildAdoptedRules,
3227
3377
  isInteractive,
@@ -3245,18 +3395,24 @@ export {
3245
3395
  filterTrackedPaths,
3246
3396
  stripBom,
3247
3397
  readJsonFile,
3398
+ appendBlocker,
3399
+ getActiveBlockers,
3400
+ isHardStopActive,
3401
+ readHardStopReason,
3402
+ clearHardStop,
3403
+ ensureNotHardStopped,
3248
3404
  NETWORK_EXEC_TIMEOUT_MS,
3249
3405
  safeExecFile,
3250
3406
  MAX_SCAN_FILE_BYTES,
3251
3407
  MAX_SECRET_FINDINGS,
3252
3408
  scanProjectForSecrets,
3253
3409
  filterSevereFindings,
3410
+ getVhkVersion,
3254
3411
  deploy,
3255
3412
  env,
3256
3413
  envCheck,
3257
3414
  publish,
3258
3415
  audit,
3259
- getVhkVersion,
3260
3416
  resolveVhkCliInvocation,
3261
3417
  startMcpServer
3262
3418
  };