@byh3071/vhk 2.4.1 → 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,30 +1726,128 @@ ${ko.sync.done}`));
1692
1726
  });
1693
1727
  }
1694
1728
 
1729
+ // src/lib/hard-stop-guard.ts
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
+
1695
1827
  // src/lib/version.ts
1696
- import { existsSync as existsSync2 } from "fs";
1697
- import { dirname, join as join2 } from "path";
1828
+ import { existsSync as existsSync3 } from "fs";
1829
+ import { dirname as dirname2, join as join4 } from "path";
1698
1830
  import { fileURLToPath } from "url";
1699
1831
 
1700
1832
  // src/lib/read-json.ts
1701
- import { readFileSync as readFileSync2 } from "fs";
1833
+ import { readFileSync as readFileSync3 } from "fs";
1702
1834
  function stripBom(text) {
1703
1835
  return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
1704
1836
  }
1705
1837
  function readJsonFile(filePath) {
1706
- const raw = stripBom(readFileSync2(filePath, "utf-8"));
1838
+ const raw = stripBom(readFileSync3(filePath, "utf-8"));
1707
1839
  return JSON.parse(raw);
1708
1840
  }
1709
1841
 
1710
1842
  // src/lib/version.ts
1711
1843
  function getVhkVersion() {
1712
- const dir = dirname(fileURLToPath(import.meta.url));
1844
+ const dir = dirname2(fileURLToPath(import.meta.url));
1713
1845
  for (const pkgPath of [
1714
- join2(dir, "../../package.json"),
1715
- join2(dir, "../package.json")
1846
+ join4(dir, "../../package.json"),
1847
+ join4(dir, "../package.json")
1716
1848
  ]) {
1717
1849
  try {
1718
- if (existsSync2(pkgPath)) {
1850
+ if (existsSync3(pkgPath)) {
1719
1851
  const pkg = readJsonFile(pkgPath);
1720
1852
  if (pkg.version) return pkg.version;
1721
1853
  }
@@ -1727,8 +1859,8 @@ function getVhkVersion() {
1727
1859
  }
1728
1860
 
1729
1861
  // src/commands/deploy.ts
1730
- import { existsSync as existsSync3 } from "fs";
1731
- import chalk4 from "chalk";
1862
+ import { existsSync as existsSync4 } from "fs";
1863
+ import chalk5 from "chalk";
1732
1864
  import inquirer2 from "inquirer";
1733
1865
 
1734
1866
  // src/lib/exec.ts
@@ -1828,7 +1960,7 @@ var PLATFORMS = {
1828
1960
  function detectPlatform() {
1829
1961
  for (const [key, config] of Object.entries(PLATFORMS)) {
1830
1962
  for (const file of config.detectFiles) {
1831
- if (existsSync3(file)) return key;
1963
+ if (existsSync4(file)) return key;
1832
1964
  }
1833
1965
  }
1834
1966
  return null;
@@ -1837,11 +1969,11 @@ function isCLIAvailable(cmd, checkArgs) {
1837
1969
  return safeExecFile(cmd, checkArgs).ok;
1838
1970
  }
1839
1971
  async function deploy() {
1840
- console.log(chalk4.bold("\n\u{1F680} " + t("deploy.title")));
1841
- 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)));
1842
1974
  let platform = detectPlatform();
1843
1975
  if (platform) {
1844
- console.log(chalk4.cyan(`
1976
+ console.log(chalk5.cyan(`
1845
1977
  \u{1F50D} \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${PLATFORMS[platform].name}`));
1846
1978
  } else {
1847
1979
  const { selected } = await inquirer2.prompt([
@@ -1860,9 +1992,9 @@ async function deploy() {
1860
1992
  }
1861
1993
  const config = PLATFORMS[platform];
1862
1994
  if (!isCLIAvailable(config.command, config.checkArgs)) {
1863
- console.log(chalk4.red(`
1995
+ console.log(chalk5.red(`
1864
1996
  \u274C ${config.name} CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
1865
- console.log(chalk4.yellow(` \u2192 ${config.installHint}`));
1997
+ console.log(chalk5.yellow(` \u2192 ${config.installHint}`));
1866
1998
  return;
1867
1999
  }
1868
2000
  const { confirm } = await inquirer2.prompt([
@@ -1874,15 +2006,15 @@ async function deploy() {
1874
2006
  }
1875
2007
  ]);
1876
2008
  if (!confirm) {
1877
- console.log(chalk4.gray("\uCDE8\uC18C\uB428"));
2009
+ console.log(chalk5.gray("\uCDE8\uC18C\uB428"));
1878
2010
  return;
1879
2011
  }
1880
- console.log(chalk4.cyan(`
2012
+ console.log(chalk5.cyan(`
1881
2013
  ${t("deploy.deploying")}
1882
2014
  `));
1883
2015
  const result = safeExecFileStream(config.command, config.commandArgs);
1884
2016
  if (result.ok) {
1885
- console.log(chalk4.green(`
2017
+ console.log(chalk5.green(`
1886
2018
  \u2705 ${t("deploy.success")}`));
1887
2019
  printNextStep({
1888
2020
  message: "\uBC30\uD3EC \uC644\uB8CC! \uC0AC\uC774\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.",
@@ -1890,16 +2022,16 @@ ${t("deploy.deploying")}
1890
2022
  cursorHint: "\uC0C1\uD0DC \uD655\uC778\uD574\uC918"
1891
2023
  });
1892
2024
  } else {
1893
- console.log(chalk4.red(`
2025
+ console.log(chalk5.red(`
1894
2026
  \u274C ${t("deploy.failed")}`));
1895
- console.log(chalk4.red(result.err));
2027
+ console.log(chalk5.red(result.err));
1896
2028
  }
1897
2029
  }
1898
2030
 
1899
2031
  // src/commands/env.ts
1900
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, appendFileSync, readdirSync } from "fs";
1901
- import { join as join3 } from "path";
1902
- 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";
1903
2035
  function parseEnvKeys(content) {
1904
2036
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
1905
2037
  }
@@ -1915,7 +2047,7 @@ function loadDefinedEnvKeys(dir = ".") {
1915
2047
  if (!name.startsWith(".env")) continue;
1916
2048
  if (name.endsWith(".example") || name.endsWith(".sample")) continue;
1917
2049
  try {
1918
- for (const k of parseEnvKeys(readFileSync3(join3(dir, name), "utf-8"))) keys.add(k);
2050
+ for (const k of parseEnvKeys(readFileSync4(join5(dir, name), "utf-8"))) keys.add(k);
1919
2051
  } catch {
1920
2052
  }
1921
2053
  }
@@ -1923,36 +2055,37 @@ function loadDefinedEnvKeys(dir = ".") {
1923
2055
  }
1924
2056
  function ensureGitignore() {
1925
2057
  const gitignorePath = ".gitignore";
1926
- if (existsSync4(gitignorePath)) {
1927
- const content = readFileSync3(gitignorePath, "utf-8");
2058
+ if (existsSync5(gitignorePath)) {
2059
+ const content = readFileSync4(gitignorePath, "utf-8");
1928
2060
  if (!content.split("\n").some((l) => l.trim() === ".env")) {
1929
- appendFileSync(gitignorePath, "\n.env\n");
1930
- 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"));
1931
2063
  }
1932
2064
  } else {
1933
- writeFileSync(gitignorePath, ".env\nnode_modules/\ndist/\n");
1934
- 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)"));
1935
2067
  }
1936
2068
  }
1937
2069
  async function env() {
1938
- console.log(chalk5.bold("\n\u{1F510} " + t("env.title")));
1939
- console.log(chalk5.gray("\u2500".repeat(40)));
1940
- if (!existsSync4(".env")) {
1941
- console.log(chalk5.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
1942
- 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."));
1943
2076
  return;
1944
2077
  }
1945
- const envContent = readFileSync3(".env", "utf-8");
2078
+ const envContent = readFileSync4(".env", "utf-8");
1946
2079
  const keys = parseEnvKeys(envContent);
1947
2080
  if (keys.length === 0) {
1948
- 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."));
1949
2082
  return;
1950
2083
  }
1951
2084
  const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
1952
- writeFileSync(".env.example", exampleContent, "utf-8");
1953
- console.log(chalk5.green(`
2085
+ writeFileSync2(".env.example", exampleContent, "utf-8");
2086
+ console.log(chalk6.green(`
1954
2087
  \u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`));
1955
- keys.forEach((k) => console.log(chalk5.gray(` ${k}`)));
2088
+ keys.forEach((k) => console.log(chalk6.gray(` ${k}`)));
1956
2089
  ensureGitignore();
1957
2090
  printNextStep({
1958
2091
  message: ".env.example \uC0DD\uC131 \uC644\uB8CC!",
@@ -1961,37 +2094,37 @@ async function env() {
1961
2094
  });
1962
2095
  }
1963
2096
  async function envCheck() {
1964
- console.log(chalk5.bold("\n\u{1F50D} " + t("env.checkTitle")));
1965
- console.log(chalk5.gray("\u2500".repeat(40)));
1966
- if (!existsSync4(".env.example")) {
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."));
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."));
1968
2101
  return;
1969
2102
  }
1970
- const requiredKeys = parseEnvKeys(readFileSync3(".env.example", "utf-8"));
2103
+ const requiredKeys = parseEnvKeys(readFileSync4(".env.example", "utf-8"));
1971
2104
  const currentKeys = loadDefinedEnvKeys();
1972
2105
  const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
1973
2106
  const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
1974
- console.log(chalk5.cyan(`
2107
+ console.log(chalk6.cyan(`
1975
2108
  \u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`));
1976
2109
  if (missing.length === 0) {
1977
- 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!"));
1978
2111
  } else {
1979
- console.log(chalk5.red(`
2112
+ console.log(chalk6.red(`
1980
2113
  \u274C \uB204\uB77D\uB41C \uD658\uACBD\uBCC0\uC218 (${missing.length}\uAC1C):`));
1981
- missing.forEach((k) => console.log(chalk5.red(` \u2022 ${k}`)));
2114
+ missing.forEach((k) => console.log(chalk6.red(` \u2022 ${k}`)));
1982
2115
  process.exitCode = 1;
1983
2116
  }
1984
2117
  if (extra.length > 0) {
1985
- console.log(chalk5.yellow(`
2118
+ console.log(chalk6.yellow(`
1986
2119
  \u{1F4A1} .env.example\uC5D0 \uC5C6\uB294 \uCD94\uAC00 \uBCC0\uC218 (${extra.length}\uAC1C):`));
1987
- extra.forEach((k) => console.log(chalk5.yellow(` \u2022 ${k}`)));
2120
+ extra.forEach((k) => console.log(chalk6.yellow(` \u2022 ${k}`)));
1988
2121
  }
1989
2122
  ensureGitignore();
1990
2123
  }
1991
2124
 
1992
2125
  // src/commands/publish.ts
1993
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1994
- import chalk6 from "chalk";
2126
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
2127
+ import chalk7 from "chalk";
1995
2128
  import inquirer3 from "inquirer";
1996
2129
  import ora from "ora";
1997
2130
  function bumpVersion(current, type) {
@@ -2024,8 +2157,8 @@ function bumpClaudeMdVersion(content, newVersion) {
2024
2157
  }
2025
2158
  function gitPostRelease(newVersion) {
2026
2159
  const filesToAdd = ["package.json"];
2027
- if (existsSync5("CHANGELOG.md")) filesToAdd.push("CHANGELOG.md");
2028
- if (existsSync5("CLAUDE.md")) filesToAdd.push("CLAUDE.md");
2160
+ if (existsSync6("CHANGELOG.md")) filesToAdd.push("CHANGELOG.md");
2161
+ if (existsSync6("CLAUDE.md")) filesToAdd.push("CLAUDE.md");
2029
2162
  const add = safeExecFile("git", ["add", ...filesToAdd]);
2030
2163
  if (!add.ok) {
2031
2164
  return {
@@ -2082,28 +2215,28 @@ function publishPreflight() {
2082
2215
  return { ...evaluatePublishPreflight(branch, trackedStatus, defaultBranch), branch, defaultBranch };
2083
2216
  }
2084
2217
  async function publish() {
2085
- console.log(chalk6.bold("\n\u{1F4E6} " + t("publish.title")));
2086
- 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)));
2087
2220
  const pre = publishPreflight();
2088
2221
  if (!pre.ok) {
2089
2222
  const msg = pre.code === "wrong-branch" ? t("publish.preflightWrongBranch", pre.branch || "(detached)", pre.defaultBranch) : t("publish.preflightDirty");
2090
- console.log(chalk6.red(`
2223
+ console.log(chalk7.red(`
2091
2224
  \u274C ${msg}`));
2092
2225
  return;
2093
2226
  }
2094
- if (!existsSync5("package.json")) {
2095
- 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."));
2096
2229
  return;
2097
2230
  }
2098
2231
  let pkg;
2099
2232
  try {
2100
2233
  pkg = readJsonFile("package.json");
2101
2234
  } catch {
2102
- console.log(chalk6.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
2235
+ console.log(chalk7.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
2103
2236
  return;
2104
2237
  }
2105
2238
  const currentVersion = pkg.version || "0.0.0";
2106
- console.log(chalk6.cyan(`
2239
+ console.log(chalk7.cyan(`
2107
2240
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
2108
2241
  const { bumpType } = await inquirer3.prompt([
2109
2242
  {
@@ -2118,29 +2251,29 @@ async function publish() {
2118
2251
  }
2119
2252
  ]);
2120
2253
  const newVersion = bumpVersion(currentVersion, bumpType);
2121
- console.log(chalk6.cyan(`
2254
+ console.log(chalk7.cyan(`
2122
2255
  \u{1F195} \uC0C8 \uBC84\uC804: v${newVersion}`));
2123
2256
  pkg.version = newVersion;
2124
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2125
- console.log(chalk6.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
2126
- const claudeMdOriginal = existsSync5("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;
2127
2260
  if (claudeMdOriginal !== null) {
2128
2261
  const bumped = bumpClaudeMdVersion(claudeMdOriginal, newVersion);
2129
2262
  if (bumped !== claudeMdOriginal) {
2130
- writeFileSync2("CLAUDE.md", bumped, "utf-8");
2131
- 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"));
2132
2265
  }
2133
2266
  }
2134
2267
  const rollbackVersion = () => {
2135
2268
  pkg.version = currentVersion;
2136
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2137
- 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");
2138
2271
  };
2139
2272
  const buildSpinner = ora(t("publish.building")).start();
2140
2273
  const buildResult = safeExecFile("pnpm", ["build"]);
2141
2274
  if (!buildResult.ok) {
2142
2275
  buildSpinner.fail(t("publish.buildFailed"));
2143
- console.log(chalk6.red(buildResult.err.slice(0, 500)));
2276
+ console.log(chalk7.red(buildResult.err.slice(0, 500)));
2144
2277
  rollbackVersion();
2145
2278
  return;
2146
2279
  }
@@ -2149,7 +2282,7 @@ async function publish() {
2149
2282
  const testResult = safeExecFile("pnpm", ["test", "--run"]);
2150
2283
  if (!testResult.ok) {
2151
2284
  testSpinner.fail(t("publish.testFailed"));
2152
- console.log(chalk6.red(testResult.err.slice(0, 500)));
2285
+ console.log(chalk7.red(testResult.err.slice(0, 500)));
2153
2286
  rollbackVersion();
2154
2287
  return;
2155
2288
  }
@@ -2164,45 +2297,45 @@ async function publish() {
2164
2297
  ]);
2165
2298
  if (!confirm) {
2166
2299
  rollbackVersion();
2167
- 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."));
2168
2301
  return;
2169
2302
  }
2170
- console.log(chalk6.cyan(`
2303
+ console.log(chalk7.cyan(`
2171
2304
  \u{1F4E4} ${t("publish.publishing")}`));
2172
- 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)"));
2173
2306
  const pubResult = safeExecFileStream("npm", ["publish", "--access", "public"]);
2174
2307
  if (!pubResult.ok) {
2175
- console.log(chalk6.red(`
2308
+ console.log(chalk7.red(`
2176
2309
  \u2716 ${t("publish.publishFailed")}`));
2177
- console.log(chalk6.red(pubResult.err.slice(0, 500)));
2310
+ console.log(chalk7.red(pubResult.err.slice(0, 500)));
2178
2311
  rollbackVersion();
2179
- 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.`));
2180
2313
  return;
2181
2314
  }
2182
- console.log(chalk6.green(`
2315
+ console.log(chalk7.green(`
2183
2316
  \u2714 ${t("publish.publishSuccess")}`));
2184
- if (existsSync5("CHANGELOG.md")) {
2185
- const cl = readFileSync4("CHANGELOG.md", "utf-8");
2186
- 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();
2187
2320
  const updated = insertChangelogStub(cl, newVersion, date);
2188
2321
  if (updated !== cl) {
2189
- writeFileSync2("CHANGELOG.md", updated, "utf-8");
2190
- 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`));
2191
2324
  }
2192
2325
  }
2193
2326
  const git = gitPostRelease(newVersion);
2194
2327
  if (git.warning) {
2195
- console.log(chalk6.yellow(`
2328
+ console.log(chalk7.yellow(`
2196
2329
  \u26A0\uFE0F ${git.warning}`));
2197
- 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}).`));
2198
2331
  } else if (git.tagged && git.pushed) {
2199
- console.log(chalk6.green(`
2332
+ console.log(chalk7.green(`
2200
2333
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
2201
2334
  } else if (git.tagged) {
2202
- console.log(chalk6.yellow(`
2335
+ console.log(chalk7.yellow(`
2203
2336
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
2204
2337
  }
2205
- console.log(chalk6.green.bold(`
2338
+ console.log(chalk7.green.bold(`
2206
2339
  \u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
2207
2340
  printNextStep({
2208
2341
  message: "npm \uBC30\uD3EC \uC644\uB8CC!",
@@ -2212,13 +2345,13 @@ async function publish() {
2212
2345
  }
2213
2346
 
2214
2347
  // src/commands/audit.ts
2215
- import { existsSync as existsSync6 } from "fs";
2216
- import chalk7 from "chalk";
2348
+ import { existsSync as existsSync7 } from "fs";
2349
+ import chalk8 from "chalk";
2217
2350
  import inquirer4 from "inquirer";
2218
2351
  import ora2 from "ora";
2219
2352
  function detectCurrentPM() {
2220
- if (existsSync6("pnpm-lock.yaml")) return "pnpm";
2221
- if (existsSync6("yarn.lock")) return "yarn";
2353
+ if (existsSync7("pnpm-lock.yaml")) return "pnpm";
2354
+ if (existsSync7("yarn.lock")) return "yarn";
2222
2355
  return "npm";
2223
2356
  }
2224
2357
  function parseAuditOutput(output, pm) {
@@ -2258,24 +2391,24 @@ function runAuditFix(pm) {
2258
2391
  return result.ok ? { ok: true } : { ok: false, err: result.err };
2259
2392
  }
2260
2393
  async function audit(autoFix = false) {
2261
- console.log(chalk7.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
2262
- 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)));
2263
2396
  const pm = detectCurrentPM();
2264
- 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}`));
2265
2398
  const spinner = ora2("\uBCF4\uC548 \uAC10\uC0AC \uC2E4\uD589 \uC911...").start();
2266
2399
  const output = runAuditJson(pm);
2267
2400
  spinner.stop();
2268
2401
  const summary = parseAuditOutput(output, pm);
2269
2402
  if (summary.total === 0) {
2270
- 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!"));
2271
2404
  return;
2272
2405
  }
2273
- console.log(chalk7.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
2274
- if (summary.critical > 0) console.log(chalk7.red(` \u{1F534} Critical: ${summary.critical}`));
2275
- if (summary.high > 0) console.log(chalk7.red(` \u{1F7E0} High: ${summary.high}`));
2276
- if (summary.moderate > 0) console.log(chalk7.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
2277
- if (summary.low > 0) console.log(chalk7.gray(` \u26AA Low: ${summary.low}`));
2278
- 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(`
2279
2412
  \uCD1D ${summary.total}\uAC1C\uC758 \uCDE8\uC57D\uC810`));
2280
2413
  const shouldRunFix = autoFix ? true : summary.critical > 0 || summary.high > 0 ? (await inquirer4.prompt([
2281
2414
  {
@@ -2302,9 +2435,9 @@ async function audit(autoFix = false) {
2302
2435
  }
2303
2436
 
2304
2437
  // src/mcp/cli-path.ts
2305
- import { existsSync as existsSync7 } from "fs";
2438
+ import { existsSync as existsSync8 } from "fs";
2306
2439
  import { fileURLToPath as fileURLToPath2 } from "url";
2307
- import { dirname as dirname2, resolve } from "path";
2440
+ import { dirname as dirname3, resolve } from "path";
2308
2441
  function pickCliInvocation(globalAvailable, localCli, localExists) {
2309
2442
  if (globalAvailable) return { bin: "vhk", prefixArgs: [], fallback: false };
2310
2443
  if (localExists) return { bin: process.execPath, prefixArgs: [localCli], fallback: true };
@@ -2314,20 +2447,20 @@ function composeInvocation(cli, args) {
2314
2447
  return { bin: cli.bin, args: [...cli.prefixArgs, ...args] };
2315
2448
  }
2316
2449
  function localCliPath() {
2317
- const here = dirname2(fileURLToPath2(import.meta.url));
2450
+ const here = dirname3(fileURLToPath2(import.meta.url));
2318
2451
  return resolve(here, "..", "index.js");
2319
2452
  }
2320
2453
  function resolveVhkCliInvocation() {
2321
2454
  const globalAvailable = safeExecFile("vhk", ["--version"]).ok;
2322
2455
  const local = localCliPath();
2323
- return pickCliInvocation(globalAvailable, local, existsSync7(local));
2456
+ return pickCliInvocation(globalAvailable, local, existsSync8(local));
2324
2457
  }
2325
2458
 
2326
2459
  // src/mcp/server.ts
2327
2460
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2328
2461
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2329
2462
  import { z } from "zod";
2330
- 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";
2331
2464
 
2332
2465
  // src/lib/scan-secrets.ts
2333
2466
  import fs7 from "fs";
@@ -2403,7 +2536,7 @@ import path6 from "path";
2403
2536
  var import_ignore = __toESM(require_ignore(), 1);
2404
2537
  import fs5 from "fs";
2405
2538
  import path5 from "path";
2406
- import chalk8 from "chalk";
2539
+ import chalk9 from "chalk";
2407
2540
  function loadGitignore(rootDir) {
2408
2541
  const ig = (0, import_ignore.default)();
2409
2542
  const gitignorePath = path5.join(rootDir, ".gitignore");
@@ -2477,7 +2610,7 @@ function printSecurityWarnings(rootDir = process.cwd()) {
2477
2610
  const result = checkProjectSecurity(rootDir);
2478
2611
  if (result.ok) return true;
2479
2612
  for (const w of result.warnings) {
2480
- console.log(chalk8.yellow(` \u26A0\uFE0F ${w}`));
2613
+ console.log(chalk9.yellow(` \u26A0\uFE0F ${w}`));
2481
2614
  }
2482
2615
  return false;
2483
2616
  }
@@ -2618,6 +2751,14 @@ var SERVER_VERSION = getVhkVersion();
2618
2751
  function isGitRepo() {
2619
2752
  return safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok;
2620
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
+ }
2621
2762
  var ANSI_RE = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
2622
2763
  function stripAnsi(s) {
2623
2764
  return s.replace(ANSI_RE, "");
@@ -2651,6 +2792,8 @@ function createVhkMcpServer() {
2651
2792
  }
2652
2793
  },
2653
2794
  async ({ message }) => {
2795
+ const blocked = hardStopBlocked("save");
2796
+ if (blocked) return blocked;
2654
2797
  if (!isGitRepo()) {
2655
2798
  return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
2656
2799
  }
@@ -2712,6 +2855,8 @@ ${preview}${more}
2712
2855
  }
2713
2856
  },
2714
2857
  async ({ confirm }) => {
2858
+ const blocked = hardStopBlocked("undo");
2859
+ if (blocked) return blocked;
2715
2860
  if (!isGitRepo()) {
2716
2861
  return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
2717
2862
  }
@@ -2752,7 +2897,7 @@ ${last.out}
2752
2897
  );
2753
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 () => {
2754
2899
  const lines = [];
2755
- if (existsSync8("package.json")) {
2900
+ if (existsSync9("package.json")) {
2756
2901
  try {
2757
2902
  const pkg = readJsonFile("package.json");
2758
2903
  lines.push(`\u{1F4E6} \uD504\uB85C\uC81D\uD2B8: ${pkg.name ?? "(\uC774\uB984 \uC5C6\uC74C)"} v${pkg.version ?? "?"}`);
@@ -2837,7 +2982,7 @@ ${last.out}
2837
2982
  checks.push(build.ok ? "\u2705 \uBE4C\uB4DC \uC131\uACF5" : "\u274C \uBE4C\uB4DC \uC2E4\uD328");
2838
2983
  const test = safeExecFile("pnpm", ["test", "--run"]);
2839
2984
  checks.push(test.ok ? "\u2705 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC" : "\u274C \uD14C\uC2A4\uD2B8 \uC2E4\uD328");
2840
- if (existsSync8("package.json")) {
2985
+ if (existsSync9("package.json")) {
2841
2986
  try {
2842
2987
  const pkg = readJsonFile("package.json");
2843
2988
  checks.push(`\u{1F4E6} \uBC84\uC804: ${pkg.version}`);
@@ -2875,11 +3020,11 @@ ${last.out}
2875
3020
  const recommended = ["CLAUDE.md", ".cursorrules", "docs/PRD.md", "docs/ARCHITECTURE.md"];
2876
3021
  const lines = ["\u{1F50D} \uD504\uB85C\uC81D\uD2B8 \uC810\uAC80", "", "\uD544\uC218:"];
2877
3022
  required.forEach((f) => {
2878
- lines.push(` ${existsSync8(f) ? "\u2705" : "\u274C"} ${f}`);
3023
+ lines.push(` ${existsSync9(f) ? "\u2705" : "\u274C"} ${f}`);
2879
3024
  });
2880
3025
  lines.push("", "\uAD8C\uC7A5 (VHK \uD558\uB124\uC2A4):");
2881
3026
  recommended.forEach((f) => {
2882
- lines.push(` ${existsSync8(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
3027
+ lines.push(` ${existsSync9(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
2883
3028
  });
2884
3029
  return { content: [{ type: "text", text: lines.join("\n") }] };
2885
3030
  });
@@ -2913,24 +3058,26 @@ ${log.out}` }] };
2913
3058
  description: ".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore\uC5D0 .env \uC790\uB3D9 \uCD94\uAC00"
2914
3059
  },
2915
3060
  async () => {
2916
- if (!existsSync8(".env")) {
3061
+ const blocked = hardStopBlocked("env");
3062
+ if (blocked) return blocked;
3063
+ if (!existsSync9(".env")) {
2917
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." }] };
2918
3065
  }
2919
- const keys = parseEnvKeys(readFileSync5(".env", "utf-8"));
3066
+ const keys = parseEnvKeys(readFileSync6(".env", "utf-8"));
2920
3067
  if (keys.length === 0) {
2921
3068
  return { content: [{ type: "text", text: "\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
2922
3069
  }
2923
3070
  const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
2924
- writeFileSync3(".env.example", exampleContent, "utf-8");
3071
+ writeFileSync4(".env.example", exampleContent, "utf-8");
2925
3072
  const gitignoreLines = [];
2926
- if (existsSync8(".gitignore")) {
2927
- const content = readFileSync5(".gitignore", "utf-8");
3073
+ if (existsSync9(".gitignore")) {
3074
+ const content = readFileSync6(".gitignore", "utf-8");
2928
3075
  if (!content.split("\n").some((l) => l.trim() === ".env")) {
2929
- appendFileSync2(".gitignore", "\n.env\n");
3076
+ appendFileSync3(".gitignore", "\n.env\n");
2930
3077
  gitignoreLines.push("\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428");
2931
3078
  }
2932
3079
  } else {
2933
- writeFileSync3(".gitignore", ".env\nnode_modules/\ndist/\n");
3080
+ writeFileSync4(".gitignore", ".env\nnode_modules/\ndist/\n");
2934
3081
  gitignoreLines.push("\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)");
2935
3082
  }
2936
3083
  const lines = [`\u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`, ...keys.map((k) => ` ${k}`)];
@@ -2944,11 +3091,11 @@ ${log.out}` }] };
2944
3091
  description: "\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC (.env.example \uAE30\uC900)"
2945
3092
  },
2946
3093
  async () => {
2947
- if (!existsSync8(".env.example")) {
3094
+ if (!existsSync9(".env.example")) {
2948
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." }] };
2949
3096
  }
2950
- const requiredKeys = parseEnvKeys(readFileSync5(".env.example", "utf-8"));
2951
- 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")) : [];
2952
3099
  const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
2953
3100
  const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
2954
3101
  const lines = [`\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`];
@@ -3064,7 +3211,7 @@ ${cliStatus}
3064
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)"
3065
3212
  },
3066
3213
  async () => {
3067
- if (!existsSync8("package.json")) {
3214
+ if (!existsSync9("package.json")) {
3068
3215
  return { content: [{ type: "text", text: "\u274C package.json \uC5C6\uC74C." }] };
3069
3216
  }
3070
3217
  try {
@@ -3092,7 +3239,7 @@ ${cliStatus}
3092
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)"
3093
3240
  },
3094
3241
  async () => {
3095
- 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;
3096
3243
  const candidates = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current);
3097
3244
  const lines = [`\uD604\uC7AC PM: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00 (lock \uD30C\uC77C \uC5C6\uC74C)"}`];
3098
3245
  for (const pm of candidates) {
@@ -3224,6 +3371,7 @@ export {
3224
3371
  ensureVhkIgnored,
3225
3372
  listBackups,
3226
3373
  restoreBackup,
3374
+ atomicWriteFile,
3227
3375
  detectExistingRuleFiles,
3228
3376
  buildAdoptedRules,
3229
3377
  isInteractive,
@@ -3247,6 +3395,12 @@ export {
3247
3395
  filterTrackedPaths,
3248
3396
  stripBom,
3249
3397
  readJsonFile,
3398
+ appendBlocker,
3399
+ getActiveBlockers,
3400
+ isHardStopActive,
3401
+ readHardStopReason,
3402
+ clearHardStop,
3403
+ ensureNotHardStopped,
3250
3404
  NETWORK_EXEC_TIMEOUT_MS,
3251
3405
  safeExecFile,
3252
3406
  MAX_SCAN_FILE_BYTES,