@byh3071/vhk 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  id: vhk-readme
3
3
  date: 2026-05-28
4
- tags: [vhk, cli, readme, v1.5.1, ga]
4
+ tags: [vhk, cli, readme, v1.6.0, ga]
5
5
  ---
6
6
 
7
7
  # 🔧 VHK — Vibe Harness Kit
8
8
 
9
- > 🎉 **v1.5.1** — **규칙은 한 벌로 Cursor·Claude·Windsurf·Copilot·Antigravity에, 맥락은 클라우드로.**
9
+ > 🎉 **v1.6.0** — **규칙은 한 벌로 Cursor·Claude·Windsurf·Copilot·Antigravity에, 맥락은 클라우드로.**
10
10
  > 도구·기기를 옮겨도 `vhk` 명령으로 그대로 불러옵니다. (포터빌리티)
11
11
  >
12
12
  > AI 코딩 에이전트를 부리는 사람을 위한 **한국어 풀사이클 CLI**.
@@ -55,7 +55,7 @@ vhk start
55
55
  vhk gate # 퀵 5문항 — GO / 다듬기 / 다른 아이디어
56
56
  ```
57
57
 
58
- ### 3. 그 외 기능 한눈에 (v1.5)
58
+ ### 3. 그 외 기능 한눈에 (v1.6)
59
59
 
60
60
  | 기능 | 한 줄 요약 | 진입 명령 |
61
61
  |------|-----------|-----------|
@@ -64,6 +64,7 @@ vhk gate # 퀵 5문항 — GO / 다듬기 / 다른 아이디어
64
64
  | 🚧 **HARD_STOP 안전장치** | 블로커 3건 누적 → `.vhk/HARD_STOP` 트립와이어. `vhk resume --confirm` 만 해제 | `vhk blocker "<증상>"` |
65
65
  | 🔌 **MCP 24 tool** | Cursor·Claude Desktop 등에서 vhk를 채팅으로 호출 | `vhk mcp-init` |
66
66
  | 📋 **컨텍스트 영속화** | `.vhk/context.md` + `memory.json` + `brief.md` 로 세션 간 맥락 유지 | `vhk context` |
67
+ | 🔀 **드리프트 감지** | 규칙 파일이 RULES.md와 어긋나거나 context가 코드보다 낡으면 자동 경고 (읽기전용) | `vhk doctor` |
67
68
 
68
69
  ### 4. 권장 일일 사이클
69
70
 
@@ -315,7 +316,7 @@ vhk ref open 1 # 1번 레퍼런스를 브라우저로 열기
315
316
 
316
317
  | 기능 | 설명 |
317
318
  |------|------|
318
- | **MCP 서버** | `vhk mcp` — stdio MCP 서버 첫 도입 (v0.6.0 당시 8개 도구 — save/undo/status/diff/ship/doctor/check/recap). 현재 v1.5 기준 **24개** 로 확장 — 위 "Cursor와 MCP로 연동하기" 섹션 참조 |
319
+ | **MCP 서버** | `vhk mcp` — stdio MCP 서버 첫 도입 (v0.6.0 당시 8개 도구 — save/undo/status/diff/ship/doctor/check/recap). 현재 v1.6 기준 **24개** 로 확장 — 위 "Cursor와 MCP로 연동하기" 섹션 참조 |
319
320
  | **mcp-init** | `vhk mcp-init` — Cursor `.cursor/mcp.json` 자동 생성. 재시작 한 번으로 연동 완료 |
320
321
  | **자연어 라우팅 확장** | `vhk mcp설정` → `vhk mcp-init` 별칭 |
321
322
  | **보안** | MCP save 도구의 shell injection 차단 — 모든 git 호출에 shell 미경유 `safeExecFile` 사용 |
@@ -503,33 +503,54 @@ function resolveCmd(cmd, args) {
503
503
  }
504
504
  return { bin: platformCmd(cmd), argv: args };
505
505
  }
506
+ var DEFAULT_EXEC_TIMEOUT_MS = 6e5;
507
+ var NETWORK_EXEC_TIMEOUT_MS = 3e4;
508
+ function resolveTimeout(timeoutMs, fallback) {
509
+ const v = timeoutMs === void 0 ? fallback : timeoutMs;
510
+ return v > 0 ? v : void 0;
511
+ }
512
+ function isTimeoutError(e, timeout) {
513
+ if (!timeout) return false;
514
+ return e.code === "ETIMEDOUT";
515
+ }
506
516
  function safeExecFile(cmd, args, opts = {}) {
507
517
  const { bin, argv } = resolveCmd(cmd, args);
508
518
  const env2 = opts.env ? { ...process.env, ...opts.env } : void 0;
519
+ const timeout = resolveTimeout(opts.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
509
520
  try {
510
521
  const out = execFileSync(bin, argv, {
511
522
  encoding: "utf-8",
512
523
  stdio: ["pipe", "pipe", "pipe"],
513
- env: env2
524
+ env: env2,
525
+ ...timeout ? { timeout, killSignal: "SIGTERM" } : {}
514
526
  }).toString();
515
527
  return { ok: true, out: out.trim() };
516
528
  } catch (err) {
517
529
  const e = err;
518
530
  const stdout = e.stdout ? e.stdout.toString() : "";
519
- const msg = e.message ?? String(err);
531
+ let msg = e.message ?? String(err);
532
+ if (isTimeoutError(e, timeout)) {
533
+ msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
534
+ }
520
535
  return { ok: false, err: msg, out: stdout.trim() };
521
536
  }
522
537
  }
523
- function safeExecFileStream(cmd, args) {
538
+ function safeExecFileStream(cmd, args, opts = {}) {
524
539
  const { bin, argv } = resolveCmd(cmd, args);
540
+ const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : void 0;
525
541
  try {
526
542
  execFileSync(bin, argv, {
527
543
  encoding: "utf-8",
528
- stdio: "inherit"
544
+ stdio: "inherit",
545
+ ...timeout ? { timeout, killSignal: "SIGTERM" } : {}
529
546
  });
530
547
  return { ok: true };
531
548
  } catch (err) {
532
- const msg = err instanceof Error ? err.message : String(err);
549
+ const e = err;
550
+ let msg = err instanceof Error ? err.message : String(err);
551
+ if (isTimeoutError(e, timeout)) {
552
+ msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
553
+ }
533
554
  return { ok: false, err: msg };
534
555
  }
535
556
  }
@@ -734,7 +755,12 @@ var ko = {
734
755
  nextOkMessage: "\uD658\uACBD \uC810\uAC80 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uC2DC\uC791\uD558\uC138\uC694.",
735
756
  nextRetryMessage: "\uC704 \uB3C4\uAD6C\uB97C \uC124\uCE58\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
736
757
  updateAvailable: (latest) => `\u{1F195} v${latest} \uC0AC\uC6A9 \uAC00\uB2A5 \u2014 npm i -g @byh3071/vhk`,
737
- updateCurrent: "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC4F0\uACE0 \uC788\uC5B4\uC694"
758
+ updateCurrent: "\uCD5C\uC2E0 \uBC84\uC804\uC744 \uC4F0\uACE0 \uC788\uC5B4\uC694",
759
+ driftTitle: "\u{1F500} \uB4DC\uB9AC\uD504\uD2B8 \uC810\uAC80 (\uADDC\uCE59\xB7\uB9E5\uB77D \uC5B4\uAE0B\uB0A8):",
760
+ driftNoRules: "\u2B1A RULES.md \uC5C6\uC74C \u2014 \uADDC\uCE59 \uB4DC\uB9AC\uD504\uD2B8 \uC810\uAC80 \uC0DD\uB7B5",
761
+ driftRuleClean: "\u2705 \uADDC\uCE59 \uD30C\uC77C\uC774 RULES.md\uC640 \uC77C\uCE58",
762
+ 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`,
763
+ 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"
738
764
  },
739
765
  nlp: {
740
766
  matched: "\uC774\uAC8C \uB9DE\uB098\uC694?",
@@ -1138,6 +1164,48 @@ function bumpVersion(current, type) {
1138
1164
  return `${major}.${minor}.${patch + 1}`;
1139
1165
  }
1140
1166
  }
1167
+ function gitPostRelease(newVersion) {
1168
+ const add = safeExecFile("git", ["add", "package.json"]);
1169
+ if (!add.ok) {
1170
+ return {
1171
+ added: false,
1172
+ committed: false,
1173
+ tagged: false,
1174
+ pushed: false,
1175
+ warning: `git add \uC2E4\uD328 \u2014 \uCEE4\uBC0B/\uD0DC\uADF8/\uD478\uC2DC\uB97C \uAC74\uB108\uB701\uB2C8\uB2E4. \uC218\uB3D9\uC73C\uB85C \uCC98\uB9AC\uD558\uC138\uC694.`
1176
+ };
1177
+ }
1178
+ const commit = safeExecFile("git", ["commit", "-m", `chore: release v${newVersion}`]);
1179
+ if (!commit.ok) {
1180
+ return {
1181
+ added: true,
1182
+ committed: false,
1183
+ tagged: false,
1184
+ pushed: false,
1185
+ warning: `git commit \uC2E4\uD328 \u2014 git tag \uB97C \uAC74\uB108\uB701\uB2C8\uB2E4 (\uC798\uBABB\uB41C HEAD \uC5D0 \uD0DC\uADF8 \uBC29\uC9C0).
1186
+ ${commit.err.slice(0, 300)}
1187
+ \uC218\uB3D9 \uCC98\uB9AC: git commit && git tag v${newVersion} && git push --tags`
1188
+ };
1189
+ }
1190
+ const tag = safeExecFile("git", ["tag", `v${newVersion}`]);
1191
+ if (!tag.ok) {
1192
+ return {
1193
+ added: true,
1194
+ committed: true,
1195
+ tagged: false,
1196
+ pushed: false,
1197
+ warning: `git tag \uC0DD\uC131 \uC2E4\uD328 (\uC218\uB3D9: git tag v${newVersion}). ${tag.err.slice(0, 200)}`
1198
+ };
1199
+ }
1200
+ const push = safeExecFile("git", ["push"]);
1201
+ const pushTags = safeExecFile("git", ["push", "--tags"]);
1202
+ return {
1203
+ added: true,
1204
+ committed: true,
1205
+ tagged: true,
1206
+ pushed: push.ok && pushTags.ok
1207
+ };
1208
+ }
1141
1209
  async function publish() {
1142
1210
  console.log(chalk4.bold("\n\u{1F4E6} " + t("publish.title")));
1143
1211
  console.log(chalk4.gray("\u2500".repeat(40)));
@@ -1222,21 +1290,17 @@ async function publish() {
1222
1290
  }
1223
1291
  console.log(chalk4.green(`
1224
1292
  \u2714 ${t("publish.publishSuccess")}`));
1225
- const addResult = safeExecFile("git", ["add", "package.json"]);
1226
- if (addResult.ok) {
1227
- safeExecFile("git", ["commit", "-m", `chore: release v${newVersion}`]);
1228
- const tagResult = safeExecFile("git", ["tag", `v${newVersion}`]);
1229
- if (tagResult.ok) {
1230
- const pushResult = safeExecFile("git", ["push"]);
1231
- const pushTagsResult = safeExecFile("git", ["push", "--tags"]);
1232
- if (pushResult.ok && pushTagsResult.ok) {
1233
- console.log(chalk4.green(`
1293
+ const git = gitPostRelease(newVersion);
1294
+ if (git.warning) {
1295
+ console.log(chalk4.yellow(`
1296
+ \u26A0\uFE0F ${git.warning}`));
1297
+ console.log(chalk4.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
1298
+ } else if (git.tagged && git.pushed) {
1299
+ console.log(chalk4.green(`
1234
1300
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
1235
- } else {
1236
- console.log(chalk4.yellow(`
1301
+ } else if (git.tagged) {
1302
+ console.log(chalk4.yellow(`
1237
1303
  \u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
1238
- }
1239
- }
1240
1304
  }
1241
1305
  console.log(chalk4.green.bold(`
1242
1306
  \u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
@@ -2112,7 +2176,9 @@ ${cliStatus}
2112
2176
  },
2113
2177
  async () => {
2114
2178
  const cur = safeExecFile("vhk", ["--version"]);
2115
- const latest = safeExecFile("npm", ["view", "@byh3071/vhk", "version"]);
2179
+ const latest = safeExecFile("npm", ["view", "@byh3071/vhk", "version"], {
2180
+ timeoutMs: NETWORK_EXEC_TIMEOUT_MS
2181
+ });
2116
2182
  const lines = [
2117
2183
  `\uD604\uC7AC: ${cur.ok ? `v${cur.out.replace(/^v/, "")}` : "\uD655\uC778 \uC2E4\uD328"}`,
2118
2184
  `\uCD5C\uC2E0: ${latest.ok ? `v${latest.out.replace(/^v/, "")}` : "\uD655\uC778 \uC2E4\uD328 (\uB124\uD2B8\uC6CC\uD06C \uB610\uB294 npm registry)"}`
@@ -2163,6 +2229,7 @@ export {
2163
2229
  printSecurityWarnings,
2164
2230
  filterTrackedPaths,
2165
2231
  readJsonFile,
2232
+ NETWORK_EXEC_TIMEOUT_MS,
2166
2233
  safeExecFile,
2167
2234
  MAX_SCAN_FILE_BYTES,
2168
2235
  MAX_SECRET_FINDINGS,
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  MAX_SCAN_FILE_BYTES,
4
4
  MAX_SECRET_FINDINGS,
5
+ NETWORK_EXEC_TIMEOUT_MS,
5
6
  __toESM,
6
7
  audit,
7
8
  deploy,
@@ -20,7 +21,7 @@ import {
20
21
  scanProjectForSecrets,
21
22
  startMcpServer,
22
23
  t
23
- } from "./chunk-4KWZANQG.js";
24
+ } from "./chunk-O3A6SO7G.js";
24
25
 
25
26
  // src/index.ts
26
27
  import { Command, Help } from "commander";
@@ -1752,6 +1753,16 @@ function toClaudeMd(sections, existing) {
1752
1753
  }
1753
1754
  return lines.join("\n");
1754
1755
  }
1756
+ function deriveProjectName(rulesContent) {
1757
+ const firstLine = rulesContent.split("\n")[0];
1758
+ return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
1759
+ }
1760
+ var SYNC_TARGETS = [
1761
+ { path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
1762
+ { path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
1763
+ { path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
1764
+ { path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone }
1765
+ ];
1755
1766
  async function sync() {
1756
1767
  console.log(chalk5.bold(`
1757
1768
  ${ko.sync.title}
@@ -1774,11 +1785,17 @@ ${ko.sync.title}
1774
1785
  const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
1775
1786
  const sections = parseRulesMd(rulesContent);
1776
1787
  console.log(chalk5.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
1777
- const firstLine = rulesContent.split("\n")[0];
1778
- const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
1779
- const cursorrulesPath = path6.join(cwd, ".cursorrules");
1780
- fs5.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
1781
- console.log(chalk5.green(` ${ko.sync.cursorrulesDone}`));
1788
+ const projectName = deriveProjectName(rulesContent);
1789
+ for (const target of SYNC_TARGETS) {
1790
+ const fullPath = path6.join(cwd, target.path);
1791
+ fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
1792
+ const content = target.generate(sections, projectName);
1793
+ fs5.writeFileSync(fullPath, content, "utf-8");
1794
+ console.log(chalk5.green(` ${target.doneMessage}`));
1795
+ if (content.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
1796
+ console.log(chalk5.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
1797
+ }
1798
+ }
1782
1799
  const claudePath = path6.join(cwd, "CLAUDE.md");
1783
1800
  const existingClaude = fs5.existsSync(claudePath) ? fs5.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
1784
1801
 
@@ -1789,21 +1806,6 @@ ${ko.sync.title}
1789
1806
  - **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
1790
1807
  fs5.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
1791
1808
  console.log(chalk5.green(` ${ko.sync.claudeDone}`));
1792
- const windsurfPath = path6.join(cwd, ".windsurfrules");
1793
- fs5.writeFileSync(windsurfPath, toWindsurfrules(sections, projectName), "utf-8");
1794
- console.log(chalk5.green(` ${ko.sync.windsurfDone}`));
1795
- const copilotPath = path6.join(cwd, ".github", "copilot-instructions.md");
1796
- fs5.mkdirSync(path6.dirname(copilotPath), { recursive: true });
1797
- fs5.writeFileSync(copilotPath, toCopilotInstructions(sections, projectName), "utf-8");
1798
- console.log(chalk5.green(` ${ko.sync.copilotDone}`));
1799
- const antigravityPath = path6.join(cwd, ".agents", "rules", "vhk-rules.md");
1800
- fs5.mkdirSync(path6.dirname(antigravityPath), { recursive: true });
1801
- const antigravityDoc = toAntigravityRules(sections, projectName);
1802
- fs5.writeFileSync(antigravityPath, antigravityDoc, "utf-8");
1803
- console.log(chalk5.green(` ${ko.sync.antigravityDone}`));
1804
- if (antigravityDoc.includes("\uC808\uC0AD\uB428")) {
1805
- console.log(chalk5.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
1806
- }
1807
1809
  console.log(chalk5.bold.green(`
1808
1810
  ${ko.sync.done}`));
1809
1811
  console.log(chalk5.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
@@ -2495,9 +2497,103 @@ ${ko.secure.title}
2495
2497
 
2496
2498
  // src/commands/doctor.ts
2497
2499
  import chalk9 from "chalk";
2500
+ import fs10 from "fs";
2501
+ import path11 from "path";
2502
+ import { fileURLToPath } from "url";
2503
+
2504
+ // src/lib/drift.ts
2498
2505
  import fs9 from "fs";
2499
2506
  import path10 from "path";
2500
- import { fileURLToPath } from "url";
2507
+
2508
+ // src/lib/git-repo.ts
2509
+ import { execFileSync } from "child_process";
2510
+ function getGitRoot(cwd = process.cwd()) {
2511
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], {
2512
+ encoding: "utf-8",
2513
+ cwd,
2514
+ stdio: ["pipe", "pipe", "pipe"]
2515
+ }).trim();
2516
+ }
2517
+ function gitOut(args, cwd) {
2518
+ return execFileSync("git", args, {
2519
+ encoding: "utf-8",
2520
+ cwd,
2521
+ stdio: ["pipe", "pipe", "pipe"]
2522
+ });
2523
+ }
2524
+ function gitRun(args, cwd) {
2525
+ execFileSync("git", args, { stdio: "pipe", cwd });
2526
+ }
2527
+ function getExecErrorMessage(err) {
2528
+ if (err && typeof err === "object" && "stderr" in err) {
2529
+ const stderr = err.stderr;
2530
+ if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
2531
+ if (typeof stderr === "string") return stderr.trim();
2532
+ }
2533
+ return err instanceof Error ? err.message : String(err);
2534
+ }
2535
+ function hasGitRemote(cwd) {
2536
+ try {
2537
+ return gitOut(["remote"], cwd).trim().length > 0;
2538
+ } catch {
2539
+ return false;
2540
+ }
2541
+ }
2542
+ function countLocalCommits(cwd) {
2543
+ try {
2544
+ const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
2545
+ return parseInt(out, 10) || 0;
2546
+ } catch {
2547
+ return 0;
2548
+ }
2549
+ }
2550
+
2551
+ // src/lib/drift.ts
2552
+ function normalizeForCompare(s) {
2553
+ return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
2554
+ }
2555
+ function checkRuleDrift(rootDir) {
2556
+ const rulesPath = path10.join(rootDir, "RULES.md");
2557
+ if (!fs9.existsSync(rulesPath)) return { checked: false, results: [] };
2558
+ const rulesContent = fs9.readFileSync(rulesPath, "utf-8");
2559
+ const sections = parseRulesMd(rulesContent);
2560
+ const projectName = deriveProjectName(rulesContent);
2561
+ const results = [];
2562
+ for (const target of SYNC_TARGETS) {
2563
+ const fullPath = path10.join(rootDir, target.path);
2564
+ if (!fs9.existsSync(fullPath)) {
2565
+ results.push({ path: target.path, status: "missing" });
2566
+ continue;
2567
+ }
2568
+ const expected = normalizeForCompare(target.generate(sections, projectName));
2569
+ const actual = normalizeForCompare(fs9.readFileSync(fullPath, "utf-8"));
2570
+ results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
2571
+ }
2572
+ return { checked: true, results };
2573
+ }
2574
+ var CONTEXT_GIT_MARKER = "vhk-context-git";
2575
+ var CONTEXT_PATH = ".vhk/context.md";
2576
+ function extractContextSha(content) {
2577
+ const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
2578
+ return m ? m[1] : null;
2579
+ }
2580
+ function checkContextDrift(rootDir) {
2581
+ const ctxPath = path10.join(rootDir, CONTEXT_PATH);
2582
+ if (!fs9.existsSync(ctxPath)) return { checked: false, stale: false };
2583
+ const generatedSha = extractContextSha(fs9.readFileSync(ctxPath, "utf-8"));
2584
+ if (!generatedSha) return { checked: false, stale: false };
2585
+ let currentSha;
2586
+ try {
2587
+ currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
2588
+ } catch {
2589
+ return { checked: false, stale: false };
2590
+ }
2591
+ if (!currentSha) return { checked: false, stale: false };
2592
+ const stale = !(currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha));
2593
+ return { checked: true, stale, generatedSha, currentSha };
2594
+ }
2595
+
2596
+ // src/commands/doctor.ts
2501
2597
  function checkCommand(name, command, hint) {
2502
2598
  const result = safeExecFile(command, ["--version"]);
2503
2599
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2505,14 +2601,14 @@ function checkCommand(name, command, hint) {
2505
2601
  return { name, command, version, ok: true, hint };
2506
2602
  }
2507
2603
  function getVhkVersion2() {
2508
- const dir = path10.dirname(fileURLToPath(import.meta.url));
2604
+ const dir = path11.dirname(fileURLToPath(import.meta.url));
2509
2605
  const candidates = [
2510
- path10.join(dir, "../package.json"),
2511
- path10.join(dir, "../../package.json")
2606
+ path11.join(dir, "../package.json"),
2607
+ path11.join(dir, "../../package.json")
2512
2608
  ];
2513
2609
  for (const pkgPath of candidates) {
2514
2610
  try {
2515
- if (fs9.existsSync(pkgPath)) {
2611
+ if (fs10.existsSync(pkgPath)) {
2516
2612
  const pkg = readJsonFile(pkgPath);
2517
2613
  return pkg.version;
2518
2614
  }
@@ -2523,7 +2619,9 @@ function getVhkVersion2() {
2523
2619
  return void 0;
2524
2620
  }
2525
2621
  function fetchLatestNpmVersion(packageName) {
2526
- const result = safeExecFile("npm", ["view", packageName, "version"]);
2622
+ const result = safeExecFile("npm", ["view", packageName, "version"], {
2623
+ timeoutMs: NETWORK_EXEC_TIMEOUT_MS
2624
+ });
2527
2625
  if (!result.ok) return void 0;
2528
2626
  const out = result.out;
2529
2627
  if (/^\d+\.\d+\.\d+/.test(out)) return out;
@@ -2583,13 +2681,13 @@ ${ko.doctor.title}
2583
2681
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2584
2682
  ];
2585
2683
  for (const file of projectFiles) {
2586
- const exists = fs9.existsSync(path10.join(cwd, file.name));
2684
+ const exists = fs10.existsSync(path11.join(cwd, file.name));
2587
2685
  if (exists) {
2588
2686
  console.log(chalk9.green(` \u2705 ${file.name}`));
2589
2687
  if (file.name === ".env") {
2590
- const gitignorePath = path10.join(cwd, ".gitignore");
2591
- if (fs9.existsSync(gitignorePath)) {
2592
- const gitignore = fs9.readFileSync(gitignorePath, "utf-8");
2688
+ const gitignorePath = path11.join(cwd, ".gitignore");
2689
+ if (fs10.existsSync(gitignorePath)) {
2690
+ const gitignore = fs10.readFileSync(gitignorePath, "utf-8");
2593
2691
  if (!gitignore.includes(".env")) {
2594
2692
  console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
2595
2693
  }
@@ -2600,6 +2698,23 @@ ${ko.doctor.title}
2600
2698
  }
2601
2699
  }
2602
2700
  console.log("");
2701
+ console.log(chalk9.bold(` ${ko.doctor.driftTitle}`));
2702
+ const ruleDrift = checkRuleDrift(cwd);
2703
+ if (!ruleDrift.checked) {
2704
+ console.log(chalk9.dim(` ${ko.doctor.driftNoRules}`));
2705
+ } else {
2706
+ const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
2707
+ if (drifted.length === 0) {
2708
+ console.log(chalk9.green(` ${ko.doctor.driftRuleClean}`));
2709
+ } else {
2710
+ console.log(chalk9.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
2711
+ }
2712
+ }
2713
+ const ctxDrift = checkContextDrift(cwd);
2714
+ if (ctxDrift.checked && ctxDrift.stale) {
2715
+ console.log(chalk9.yellow(` ${ko.doctor.driftContextWarn}`));
2716
+ }
2717
+ console.log("");
2603
2718
  if (allOk) {
2604
2719
  console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
2605
2720
  printNextStep({
@@ -2621,8 +2736,8 @@ ${ko.doctor.title}
2621
2736
  // src/commands/ship.ts
2622
2737
  import chalk10 from "chalk";
2623
2738
  import inquirer4 from "inquirer";
2624
- import fs10 from "fs";
2625
- import path11 from "path";
2739
+ import fs11 from "fs";
2740
+ import path12 from "path";
2626
2741
  var CHECKLIST = [
2627
2742
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
2628
2743
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -2635,9 +2750,9 @@ function sanitizeVersion(version) {
2635
2750
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2636
2751
  }
2637
2752
  function updateChangelogUnreleased(cwd, version, date) {
2638
- const changelogPath = path11.join(cwd, "CHANGELOG.md");
2639
- if (!fs10.existsSync(changelogPath)) return { status: "missing" };
2640
- const content = fs10.readFileSync(changelogPath, "utf-8");
2753
+ const changelogPath = path12.join(cwd, "CHANGELOG.md");
2754
+ if (!fs11.existsSync(changelogPath)) return { status: "missing" };
2755
+ const content = fs11.readFileSync(changelogPath, "utf-8");
2641
2756
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2642
2757
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2643
2758
  const blankUnreleased = [
@@ -2654,7 +2769,7 @@ function updateChangelogUnreleased(cwd, version, date) {
2654
2769
  `## [${version}] \u2014 ${date}`
2655
2770
  ].join("\n");
2656
2771
  const updated = content.replace(unreleasedHeading, blankUnreleased);
2657
- fs10.writeFileSync(changelogPath, updated, "utf-8");
2772
+ fs11.writeFileSync(changelogPath, updated, "utf-8");
2658
2773
  return { status: "updated", version };
2659
2774
  }
2660
2775
  async function ship() {
@@ -2711,12 +2826,12 @@ ${ko.ship.title}
2711
2826
  { type: "input", name: "learned", message: ko.ship.questionLearned },
2712
2827
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2713
2828
  ]);
2714
- const buildLogDir = path11.join(cwd, "docs", "build-log");
2715
- if (!fs10.existsSync(buildLogDir)) fs10.mkdirSync(buildLogDir, { recursive: true });
2829
+ const buildLogDir = path12.join(cwd, "docs", "build-log");
2830
+ if (!fs11.existsSync(buildLogDir)) fs11.mkdirSync(buildLogDir, { recursive: true });
2716
2831
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2717
2832
  const versionSlug = sanitizeVersion(retro.version);
2718
2833
  const fileName = `${today}-v${versionSlug}.md`;
2719
- const filePath = path11.join(buildLogDir, fileName);
2834
+ const filePath = path12.join(buildLogDir, fileName);
2720
2835
  const content = [
2721
2836
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
2722
2837
  "",
@@ -2745,9 +2860,9 @@ ${ko.ship.title}
2745
2860
  "---",
2746
2861
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2747
2862
  ].join("\n");
2748
- fs10.writeFileSync(filePath, content, "utf-8");
2863
+ fs11.writeFileSync(filePath, content, "utf-8");
2749
2864
  console.log(chalk10.green(`
2750
- ${ko.ship.buildLogDone(path11.relative(cwd, filePath))}`));
2865
+ ${ko.ship.buildLogDone(path12.relative(cwd, filePath))}`));
2751
2866
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
2752
2867
  if (changelogResult.status === "updated") {
2753
2868
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -2778,49 +2893,6 @@ function parsePorcelainLines(raw) {
2778
2893
  return normalizePorcelain(raw).split("\n").filter(Boolean);
2779
2894
  }
2780
2895
 
2781
- // src/lib/git-repo.ts
2782
- import { execFileSync } from "child_process";
2783
- function getGitRoot(cwd = process.cwd()) {
2784
- return execFileSync("git", ["rev-parse", "--show-toplevel"], {
2785
- encoding: "utf-8",
2786
- cwd,
2787
- stdio: ["pipe", "pipe", "pipe"]
2788
- }).trim();
2789
- }
2790
- function gitOut(args, cwd) {
2791
- return execFileSync("git", args, {
2792
- encoding: "utf-8",
2793
- cwd,
2794
- stdio: ["pipe", "pipe", "pipe"]
2795
- });
2796
- }
2797
- function gitRun(args, cwd) {
2798
- execFileSync("git", args, { stdio: "pipe", cwd });
2799
- }
2800
- function getExecErrorMessage(err) {
2801
- if (err && typeof err === "object" && "stderr" in err) {
2802
- const stderr = err.stderr;
2803
- if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
2804
- if (typeof stderr === "string") return stderr.trim();
2805
- }
2806
- return err instanceof Error ? err.message : String(err);
2807
- }
2808
- function hasGitRemote(cwd) {
2809
- try {
2810
- return gitOut(["remote"], cwd).trim().length > 0;
2811
- } catch {
2812
- return false;
2813
- }
2814
- }
2815
- function countLocalCommits(cwd) {
2816
- try {
2817
- const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
2818
- return parseInt(out, 10) || 0;
2819
- } catch {
2820
- return 0;
2821
- }
2822
- }
2823
-
2824
2896
  // src/commands/save.ts
2825
2897
  function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
2826
2898
  const y = date.getFullYear();
@@ -3062,8 +3134,8 @@ ${t("undo.recentHeader")}`));
3062
3134
 
3063
3135
  // src/commands/status.ts
3064
3136
  import { execFileSync as execFileSync4 } from "child_process";
3065
- import fs11 from "fs";
3066
- import path12 from "path";
3137
+ import fs12 from "fs";
3138
+ import path13 from "path";
3067
3139
  import chalk13 from "chalk";
3068
3140
  function countFileChanges(porcelain) {
3069
3141
  const lines = porcelain.split("\n").filter(Boolean);
@@ -3102,8 +3174,8 @@ function parseRecentCommitLines(logOutput) {
3102
3174
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3103
3175
  }
3104
3176
  function readProjectPackage(cwd = process.cwd()) {
3105
- const pkgPath = path12.join(cwd, "package.json");
3106
- if (!fs11.existsSync(pkgPath)) return null;
3177
+ const pkgPath = path13.join(cwd, "package.json");
3178
+ if (!fs12.existsSync(pkgPath)) return null;
3107
3179
  try {
3108
3180
  const pkg = readJsonFile(pkgPath);
3109
3181
  if (!pkg.name && !pkg.version) return null;
@@ -3893,7 +3965,7 @@ function getCurrentVersion() {
3893
3965
  return "0.0.0";
3894
3966
  }
3895
3967
  function getLatestVersion() {
3896
- const r = safeExecFile("npm", ["view", PACKAGE, "version"]);
3968
+ const r = safeExecFile("npm", ["view", PACKAGE, "version"], { timeoutMs: NETWORK_EXEC_TIMEOUT_MS });
3897
3969
  return r.ok ? r.out : null;
3898
3970
  }
3899
3971
  function isUpToDate(current, latest) {
@@ -4066,7 +4138,7 @@ function clearHardStop() {
4066
4138
  }
4067
4139
 
4068
4140
  // src/commands/context.ts
4069
- var CONTEXT_PATH = ".vhk/context.md";
4141
+ var CONTEXT_PATH2 = ".vhk/context.md";
4070
4142
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
4071
4143
  "node_modules",
4072
4144
  ".git",
@@ -4243,11 +4315,16 @@ async function context() {
4243
4315
  lines.push("---");
4244
4316
  lines.push("");
4245
4317
  lines.push(`_\uC0DD\uC131: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}_`);
4318
+ try {
4319
+ const sha = gitOut(["rev-parse", "HEAD"], process.cwd()).trim();
4320
+ if (sha) lines.push(`_${CONTEXT_GIT_MARKER}: ${sha}_`);
4321
+ } catch {
4322
+ }
4246
4323
  lines.push("");
4247
4324
  mkdirSync7(".vhk", { recursive: true });
4248
- writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
4325
+ writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
4249
4326
  console.log(chalk22.green(`
4250
- \u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
4327
+ \u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
4251
4328
  console.log(chalk22.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
4252
4329
  console.log(chalk22.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
4253
4330
  printNextStep({
@@ -4259,12 +4336,12 @@ async function context() {
4259
4336
  async function contextShow() {
4260
4337
  console.log(chalk22.bold("\n\u{1F4C4} " + t("context.showTitle")));
4261
4338
  console.log(chalk22.gray("\u2500".repeat(40)));
4262
- if (!existsSync11(CONTEXT_PATH)) {
4339
+ if (!existsSync11(CONTEXT_PATH2)) {
4263
4340
  console.log(chalk22.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4264
4341
  console.log(chalk22.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
4265
4342
  return;
4266
4343
  }
4267
- const content = readFileSync4(CONTEXT_PATH, "utf-8");
4344
+ const content = readFileSync4(CONTEXT_PATH2, "utf-8");
4268
4345
  console.log("\n" + content);
4269
4346
  }
4270
4347
 
@@ -4552,14 +4629,15 @@ ${ko.start.allDone}
4552
4629
  }
4553
4630
 
4554
4631
  // src/commands/cloud.ts
4555
- import fs13 from "fs";
4556
- import path14 from "path";
4632
+ import fs14 from "fs";
4633
+ import os from "os";
4634
+ import path15 from "path";
4557
4635
  import chalk26 from "chalk";
4558
4636
 
4559
4637
  // src/lib/vhk-cloud.ts
4560
4638
  var import_ignore = __toESM(require_ignore(), 1);
4561
- import fs12 from "fs";
4562
- import path13 from "path";
4639
+ import fs13 from "fs";
4640
+ import path14 from "path";
4563
4641
  var DEFAULT_CLOUD_EXCLUDES = [
4564
4642
  "memory.json",
4565
4643
  // 개인 의사결정 메모
@@ -4577,27 +4655,36 @@ var CLOUD_CONFIG_FILE = "cloud.json";
4577
4655
  function loadVhkignore(rootDir) {
4578
4656
  const ig = (0, import_ignore.default)();
4579
4657
  ig.add(DEFAULT_CLOUD_EXCLUDES);
4580
- const ignorePath = path13.join(rootDir, ".vhkignore");
4581
- if (fs12.existsSync(ignorePath)) {
4582
- ig.add(fs12.readFileSync(ignorePath, "utf-8"));
4658
+ const ignorePath = path14.join(rootDir, ".vhkignore");
4659
+ if (fs13.existsSync(ignorePath)) {
4660
+ ig.add(fs13.readFileSync(ignorePath, "utf-8"));
4583
4661
  }
4584
4662
  return ig;
4585
4663
  }
4586
4664
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
4587
- const vhkDir = path13.join(rootDir, VHK_DIR2);
4665
+ const vhkDir = path14.join(rootDir, VHK_DIR2);
4588
4666
  let entries;
4589
4667
  try {
4590
- entries = fs12.readdirSync(vhkDir, { withFileTypes: true });
4668
+ entries = fs13.readdirSync(vhkDir, { withFileTypes: true });
4591
4669
  } catch {
4592
4670
  return [];
4593
4671
  }
4594
4672
  return entries.filter((e) => e.isFile()).map((e) => e.name).filter((name) => !ig.ignores(name)).sort();
4595
4673
  }
4674
+ function partitionGistFiles(gistFiles, ig) {
4675
+ const keep = [];
4676
+ const excluded = [];
4677
+ for (const name of gistFiles) {
4678
+ if (name && ig.ignores(name)) excluded.push(name);
4679
+ else if (name) keep.push(name);
4680
+ }
4681
+ return { keep, excluded };
4682
+ }
4596
4683
  function readCloudConfig(rootDir) {
4597
- const p = path13.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4598
- if (!fs12.existsSync(p)) return null;
4684
+ const p = path14.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4685
+ if (!fs13.existsSync(p)) return null;
4599
4686
  try {
4600
- const parsed = JSON.parse(fs12.readFileSync(p, "utf-8"));
4687
+ const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
4601
4688
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
4602
4689
  return { gistId: parsed.gistId };
4603
4690
  }
@@ -4607,10 +4694,10 @@ function readCloudConfig(rootDir) {
4607
4694
  }
4608
4695
  }
4609
4696
  function writeCloudConfig(rootDir, config) {
4610
- const vhkDir = path13.join(rootDir, VHK_DIR2);
4611
- fs12.mkdirSync(vhkDir, { recursive: true });
4612
- const p = path13.join(vhkDir, CLOUD_CONFIG_FILE);
4613
- fs12.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
4697
+ const vhkDir = path14.join(rootDir, VHK_DIR2);
4698
+ fs13.mkdirSync(vhkDir, { recursive: true });
4699
+ const p = path14.join(vhkDir, CLOUD_CONFIG_FILE);
4700
+ fs13.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
4614
4701
  }
4615
4702
 
4616
4703
  // src/commands/cloud.ts
@@ -4641,11 +4728,12 @@ async function cloudPush() {
4641
4728
  ${ko.cloud.pushTitle}
4642
4729
  `));
4643
4730
  const cwd = process.cwd();
4644
- if (!fs13.existsSync(path14.join(cwd, VHK_DIR2))) {
4731
+ if (!fs14.existsSync(path15.join(cwd, VHK_DIR2))) {
4645
4732
  console.log(chalk26.yellow(` ${ko.cloud.noVhkDir}`));
4646
4733
  return;
4647
4734
  }
4648
- const files = collectVhkFiles(cwd);
4735
+ const ig = loadVhkignore(cwd);
4736
+ const files = collectVhkFiles(cwd, ig);
4649
4737
  if (files.length === 0) {
4650
4738
  console.log(chalk26.yellow(` ${ko.cloud.nothingToSync}`));
4651
4739
  return;
@@ -4654,11 +4742,11 @@ ${ko.cloud.pushTitle}
4654
4742
  process.exitCode = 1;
4655
4743
  return;
4656
4744
  }
4657
- const filePaths = files.map((f) => path14.join(cwd, VHK_DIR2, f));
4745
+ const filePaths = files.map((f) => path15.join(cwd, VHK_DIR2, f));
4658
4746
  console.log(chalk26.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
4659
4747
  `));
4660
4748
  const existing = readCloudConfig(cwd);
4661
- const desc = `vhk .vhk backup \u2014 ${path14.basename(cwd)}`;
4749
+ const desc = `vhk .vhk backup \u2014 ${path15.basename(cwd)}`;
4662
4750
  if (existing) {
4663
4751
  const gistFiles = listGistFiles(existing.gistId);
4664
4752
  for (let i = 0; i < files.length; i++) {
@@ -4673,8 +4761,27 @@ ${ko.cloud.pushTitle}
4673
4761
  return;
4674
4762
  }
4675
4763
  }
4764
+ const { excluded } = partitionGistFiles(gistFiles, ig);
4765
+ const purgeFailed = [];
4766
+ if (excluded.length > 0) {
4767
+ const purgeOk = purgeExcludedFromGist(existing.gistId, excluded);
4768
+ if (!purgeOk) purgeFailed.push(...excluded);
4769
+ const stillThere = partitionGistFiles(listGistFiles(existing.gistId), ig).excluded;
4770
+ for (const name of stillThere) {
4771
+ if (!purgeFailed.includes(name)) purgeFailed.push(name);
4772
+ }
4773
+ }
4676
4774
  console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4677
4775
  console.log(chalk26.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
4776
+ if (excluded.length > 0) {
4777
+ const purged = excluded.filter((n) => !purgeFailed.includes(n));
4778
+ if (purged.length > 0) {
4779
+ console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
4780
+ }
4781
+ if (purgeFailed.length > 0) {
4782
+ console.log(chalk26.yellow(` \u26A0\uFE0F \uC81C\uC678 \uB300\uC0C1 \uC81C\uAC70 \uC2E4\uD328: ${purgeFailed.join(", ")} (\uC218\uB3D9 \uC81C\uAC70 \uAD8C\uC7A5 \u2014 pull \uC2DC\uC5D4 \uBCF5\uC6D0 \uC548 \uB428)`));
4783
+ }
4784
+ }
4678
4785
  printPushNext();
4679
4786
  return;
4680
4787
  }
@@ -4712,14 +4819,22 @@ ${ko.cloud.pullTitle}
4712
4819
  process.exitCode = 1;
4713
4820
  return;
4714
4821
  }
4715
- const names = listGistFiles(gistId);
4716
- if (names.length === 0) {
4822
+ const allNames = listGistFiles(gistId);
4823
+ if (allNames.length === 0) {
4717
4824
  console.log(chalk26.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
4718
4825
  process.exitCode = 1;
4719
4826
  return;
4720
4827
  }
4721
- const vhkDir = path14.join(cwd, VHK_DIR2);
4722
- fs13.mkdirSync(vhkDir, { recursive: true });
4828
+ const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
4829
+ if (skipped.length > 0) {
4830
+ console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
4831
+ }
4832
+ if (names.length === 0) {
4833
+ console.log(chalk26.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
4834
+ return;
4835
+ }
4836
+ const vhkDir = path15.join(cwd, VHK_DIR2);
4837
+ fs14.mkdirSync(vhkDir, { recursive: true });
4723
4838
  let restored = 0;
4724
4839
  for (const name of names) {
4725
4840
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
@@ -4728,7 +4843,7 @@ ${ko.cloud.pullTitle}
4728
4843
  console.log(chalk26.dim(` ${res.err}`));
4729
4844
  continue;
4730
4845
  }
4731
- fs13.writeFileSync(path14.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4846
+ fs14.writeFileSync(path15.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4732
4847
  restored++;
4733
4848
  }
4734
4849
  writeCloudConfig(cwd, { gistId });
@@ -4740,6 +4855,30 @@ ${ko.cloud.pullTitle}
4740
4855
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uBCF4\uC5EC\uC918"
4741
4856
  });
4742
4857
  }
4858
+ function purgeExcludedFromGist(gistId, names) {
4859
+ if (names.length === 0) return true;
4860
+ const body = JSON.stringify({
4861
+ files: Object.fromEntries(names.map((n) => [n, null]))
4862
+ });
4863
+ const tmp = path15.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
4864
+ try {
4865
+ fs14.writeFileSync(tmp, body, "utf-8");
4866
+ for (let attempt = 0; attempt < 2; attempt++) {
4867
+ const res = safeExecFile(
4868
+ "gh",
4869
+ ["api", "--method", "PATCH", `/gists/${gistId}`, "--input", tmp],
4870
+ { timeoutMs: NETWORK_EXEC_TIMEOUT_MS }
4871
+ );
4872
+ if (res.ok) return true;
4873
+ }
4874
+ return false;
4875
+ } finally {
4876
+ try {
4877
+ fs14.unlinkSync(tmp);
4878
+ } catch {
4879
+ }
4880
+ }
4881
+ }
4743
4882
  function listGistFiles(gistId) {
4744
4883
  const res = safeExecFile("gh", ["gist", "view", gistId, "--files"]);
4745
4884
  if (!res.ok) return [];
package/dist/mcp/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startMcpServer
4
- } from "../chunk-4KWZANQG.js";
4
+ } from "../chunk-O3A6SO7G.js";
5
5
 
6
6
  // src/mcp/index.ts
7
7
  startMcpServer().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Vibe Harness Kit — AI 코딩 도구·기기를 바꿔도 규칙·맥락이 따라가는 포터빌리티 CLI (sync: Cursor·Claude·Windsurf·Copilot·Antigravity / cloud 백업)",
5
5
  "bin": {
6
6
  "vhk": "dist/index.js",