@byh3071/vhk 1.5.1 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.1, 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.1** — **규칙은 한 벌로 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,124 @@ ${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
+ var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
2581
+ function contextSourcesChanged(generatedSha, rootDir) {
2582
+ const content = gitOut(
2583
+ ["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
2584
+ rootDir
2585
+ ).trim();
2586
+ if (content) return true;
2587
+ const structural = gitOut(
2588
+ ["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
2589
+ rootDir
2590
+ ).trim();
2591
+ return structural.length > 0;
2592
+ }
2593
+ function checkContextDrift(rootDir) {
2594
+ const ctxPath = path10.join(rootDir, CONTEXT_PATH);
2595
+ if (!fs9.existsSync(ctxPath)) return { checked: false, stale: false };
2596
+ const generatedSha = extractContextSha(fs9.readFileSync(ctxPath, "utf-8"));
2597
+ if (!generatedSha) return { checked: false, stale: false };
2598
+ let currentSha;
2599
+ try {
2600
+ currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
2601
+ } catch {
2602
+ return { checked: false, stale: false };
2603
+ }
2604
+ if (!currentSha) return { checked: false, stale: false };
2605
+ if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
2606
+ return { checked: true, stale: false, generatedSha, currentSha };
2607
+ }
2608
+ let stale;
2609
+ try {
2610
+ stale = contextSourcesChanged(generatedSha, rootDir);
2611
+ } catch {
2612
+ return { checked: false, stale: false };
2613
+ }
2614
+ return { checked: true, stale, generatedSha, currentSha };
2615
+ }
2616
+
2617
+ // src/commands/doctor.ts
2501
2618
  function checkCommand(name, command, hint) {
2502
2619
  const result = safeExecFile(command, ["--version"]);
2503
2620
  if (!result.ok) return { name, command, ok: false, hint };
@@ -2505,14 +2622,14 @@ function checkCommand(name, command, hint) {
2505
2622
  return { name, command, version, ok: true, hint };
2506
2623
  }
2507
2624
  function getVhkVersion2() {
2508
- const dir = path10.dirname(fileURLToPath(import.meta.url));
2625
+ const dir = path11.dirname(fileURLToPath(import.meta.url));
2509
2626
  const candidates = [
2510
- path10.join(dir, "../package.json"),
2511
- path10.join(dir, "../../package.json")
2627
+ path11.join(dir, "../package.json"),
2628
+ path11.join(dir, "../../package.json")
2512
2629
  ];
2513
2630
  for (const pkgPath of candidates) {
2514
2631
  try {
2515
- if (fs9.existsSync(pkgPath)) {
2632
+ if (fs10.existsSync(pkgPath)) {
2516
2633
  const pkg = readJsonFile(pkgPath);
2517
2634
  return pkg.version;
2518
2635
  }
@@ -2523,7 +2640,9 @@ function getVhkVersion2() {
2523
2640
  return void 0;
2524
2641
  }
2525
2642
  function fetchLatestNpmVersion(packageName) {
2526
- const result = safeExecFile("npm", ["view", packageName, "version"]);
2643
+ const result = safeExecFile("npm", ["view", packageName, "version"], {
2644
+ timeoutMs: NETWORK_EXEC_TIMEOUT_MS
2645
+ });
2527
2646
  if (!result.ok) return void 0;
2528
2647
  const out = result.out;
2529
2648
  if (/^\d+\.\d+\.\d+/.test(out)) return out;
@@ -2583,13 +2702,13 @@ ${ko.doctor.title}
2583
2702
  { name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
2584
2703
  ];
2585
2704
  for (const file of projectFiles) {
2586
- const exists = fs9.existsSync(path10.join(cwd, file.name));
2705
+ const exists = fs10.existsSync(path11.join(cwd, file.name));
2587
2706
  if (exists) {
2588
2707
  console.log(chalk9.green(` \u2705 ${file.name}`));
2589
2708
  if (file.name === ".env") {
2590
- const gitignorePath = path10.join(cwd, ".gitignore");
2591
- if (fs9.existsSync(gitignorePath)) {
2592
- const gitignore = fs9.readFileSync(gitignorePath, "utf-8");
2709
+ const gitignorePath = path11.join(cwd, ".gitignore");
2710
+ if (fs10.existsSync(gitignorePath)) {
2711
+ const gitignore = fs10.readFileSync(gitignorePath, "utf-8");
2593
2712
  if (!gitignore.includes(".env")) {
2594
2713
  console.log(chalk9.yellow(` ${ko.doctor.envNotIgnored}`));
2595
2714
  }
@@ -2600,6 +2719,23 @@ ${ko.doctor.title}
2600
2719
  }
2601
2720
  }
2602
2721
  console.log("");
2722
+ console.log(chalk9.bold(` ${ko.doctor.driftTitle}`));
2723
+ const ruleDrift = checkRuleDrift(cwd);
2724
+ if (!ruleDrift.checked) {
2725
+ console.log(chalk9.dim(` ${ko.doctor.driftNoRules}`));
2726
+ } else {
2727
+ const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
2728
+ if (drifted.length === 0) {
2729
+ console.log(chalk9.green(` ${ko.doctor.driftRuleClean}`));
2730
+ } else {
2731
+ console.log(chalk9.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
2732
+ }
2733
+ }
2734
+ const ctxDrift = checkContextDrift(cwd);
2735
+ if (ctxDrift.checked && ctxDrift.stale) {
2736
+ console.log(chalk9.yellow(` ${ko.doctor.driftContextWarn}`));
2737
+ }
2738
+ console.log("");
2603
2739
  if (allOk) {
2604
2740
  console.log(chalk9.green.bold(` ${ko.doctor.allOk}`));
2605
2741
  printNextStep({
@@ -2621,8 +2757,8 @@ ${ko.doctor.title}
2621
2757
  // src/commands/ship.ts
2622
2758
  import chalk10 from "chalk";
2623
2759
  import inquirer4 from "inquirer";
2624
- import fs10 from "fs";
2625
- import path11 from "path";
2760
+ import fs11 from "fs";
2761
+ import path12 from "path";
2626
2762
  var CHECKLIST = [
2627
2763
  { id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
2628
2764
  { id: "test", questionKey: "checkTest", hintKey: "hintTest" },
@@ -2635,9 +2771,9 @@ function sanitizeVersion(version) {
2635
2771
  return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
2636
2772
  }
2637
2773
  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");
2774
+ const changelogPath = path12.join(cwd, "CHANGELOG.md");
2775
+ if (!fs11.existsSync(changelogPath)) return { status: "missing" };
2776
+ const content = fs11.readFileSync(changelogPath, "utf-8");
2641
2777
  const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
2642
2778
  if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
2643
2779
  const blankUnreleased = [
@@ -2654,7 +2790,7 @@ function updateChangelogUnreleased(cwd, version, date) {
2654
2790
  `## [${version}] \u2014 ${date}`
2655
2791
  ].join("\n");
2656
2792
  const updated = content.replace(unreleasedHeading, blankUnreleased);
2657
- fs10.writeFileSync(changelogPath, updated, "utf-8");
2793
+ fs11.writeFileSync(changelogPath, updated, "utf-8");
2658
2794
  return { status: "updated", version };
2659
2795
  }
2660
2796
  async function ship() {
@@ -2711,12 +2847,12 @@ ${ko.ship.title}
2711
2847
  { type: "input", name: "learned", message: ko.ship.questionLearned },
2712
2848
  { type: "input", name: "nextVersion", message: ko.ship.questionNext }
2713
2849
  ]);
2714
- const buildLogDir = path11.join(cwd, "docs", "build-log");
2715
- if (!fs10.existsSync(buildLogDir)) fs10.mkdirSync(buildLogDir, { recursive: true });
2850
+ const buildLogDir = path12.join(cwd, "docs", "build-log");
2851
+ if (!fs11.existsSync(buildLogDir)) fs11.mkdirSync(buildLogDir, { recursive: true });
2716
2852
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2717
2853
  const versionSlug = sanitizeVersion(retro.version);
2718
2854
  const fileName = `${today}-v${versionSlug}.md`;
2719
- const filePath = path11.join(buildLogDir, fileName);
2855
+ const filePath = path12.join(buildLogDir, fileName);
2720
2856
  const content = [
2721
2857
  `# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
2722
2858
  "",
@@ -2745,9 +2881,9 @@ ${ko.ship.title}
2745
2881
  "---",
2746
2882
  `*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
2747
2883
  ].join("\n");
2748
- fs10.writeFileSync(filePath, content, "utf-8");
2884
+ fs11.writeFileSync(filePath, content, "utf-8");
2749
2885
  console.log(chalk10.green(`
2750
- ${ko.ship.buildLogDone(path11.relative(cwd, filePath))}`));
2886
+ ${ko.ship.buildLogDone(path12.relative(cwd, filePath))}`));
2751
2887
  const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
2752
2888
  if (changelogResult.status === "updated") {
2753
2889
  log.success(ko.ship.changelogUpdated(changelogResult.version));
@@ -2778,49 +2914,6 @@ function parsePorcelainLines(raw) {
2778
2914
  return normalizePorcelain(raw).split("\n").filter(Boolean);
2779
2915
  }
2780
2916
 
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
2917
  // src/commands/save.ts
2825
2918
  function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
2826
2919
  const y = date.getFullYear();
@@ -3062,8 +3155,8 @@ ${t("undo.recentHeader")}`));
3062
3155
 
3063
3156
  // src/commands/status.ts
3064
3157
  import { execFileSync as execFileSync4 } from "child_process";
3065
- import fs11 from "fs";
3066
- import path12 from "path";
3158
+ import fs12 from "fs";
3159
+ import path13 from "path";
3067
3160
  import chalk13 from "chalk";
3068
3161
  function countFileChanges(porcelain) {
3069
3162
  const lines = porcelain.split("\n").filter(Boolean);
@@ -3102,8 +3195,8 @@ function parseRecentCommitLines(logOutput) {
3102
3195
  return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
3103
3196
  }
3104
3197
  function readProjectPackage(cwd = process.cwd()) {
3105
- const pkgPath = path12.join(cwd, "package.json");
3106
- if (!fs11.existsSync(pkgPath)) return null;
3198
+ const pkgPath = path13.join(cwd, "package.json");
3199
+ if (!fs12.existsSync(pkgPath)) return null;
3107
3200
  try {
3108
3201
  const pkg = readJsonFile(pkgPath);
3109
3202
  if (!pkg.name && !pkg.version) return null;
@@ -3893,7 +3986,7 @@ function getCurrentVersion() {
3893
3986
  return "0.0.0";
3894
3987
  }
3895
3988
  function getLatestVersion() {
3896
- const r = safeExecFile("npm", ["view", PACKAGE, "version"]);
3989
+ const r = safeExecFile("npm", ["view", PACKAGE, "version"], { timeoutMs: NETWORK_EXEC_TIMEOUT_MS });
3897
3990
  return r.ok ? r.out : null;
3898
3991
  }
3899
3992
  function isUpToDate(current, latest) {
@@ -4066,7 +4159,7 @@ function clearHardStop() {
4066
4159
  }
4067
4160
 
4068
4161
  // src/commands/context.ts
4069
- var CONTEXT_PATH = ".vhk/context.md";
4162
+ var CONTEXT_PATH2 = ".vhk/context.md";
4070
4163
  var IGNORE_DIRS = /* @__PURE__ */ new Set([
4071
4164
  "node_modules",
4072
4165
  ".git",
@@ -4243,11 +4336,16 @@ async function context() {
4243
4336
  lines.push("---");
4244
4337
  lines.push("");
4245
4338
  lines.push(`_\uC0DD\uC131: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}_`);
4339
+ try {
4340
+ const sha = gitOut(["rev-parse", "HEAD"], process.cwd()).trim();
4341
+ if (sha) lines.push(`_${CONTEXT_GIT_MARKER}: ${sha}_`);
4342
+ } catch {
4343
+ }
4246
4344
  lines.push("");
4247
4345
  mkdirSync7(".vhk", { recursive: true });
4248
- writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
4346
+ writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
4249
4347
  console.log(chalk22.green(`
4250
- \u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
4348
+ \u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
4251
4349
  console.log(chalk22.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
4252
4350
  console.log(chalk22.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
4253
4351
  printNextStep({
@@ -4259,12 +4357,12 @@ async function context() {
4259
4357
  async function contextShow() {
4260
4358
  console.log(chalk22.bold("\n\u{1F4C4} " + t("context.showTitle")));
4261
4359
  console.log(chalk22.gray("\u2500".repeat(40)));
4262
- if (!existsSync11(CONTEXT_PATH)) {
4360
+ if (!existsSync11(CONTEXT_PATH2)) {
4263
4361
  console.log(chalk22.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
4264
4362
  console.log(chalk22.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
4265
4363
  return;
4266
4364
  }
4267
- const content = readFileSync4(CONTEXT_PATH, "utf-8");
4365
+ const content = readFileSync4(CONTEXT_PATH2, "utf-8");
4268
4366
  console.log("\n" + content);
4269
4367
  }
4270
4368
 
@@ -4552,14 +4650,15 @@ ${ko.start.allDone}
4552
4650
  }
4553
4651
 
4554
4652
  // src/commands/cloud.ts
4555
- import fs13 from "fs";
4556
- import path14 from "path";
4653
+ import fs14 from "fs";
4654
+ import os from "os";
4655
+ import path15 from "path";
4557
4656
  import chalk26 from "chalk";
4558
4657
 
4559
4658
  // src/lib/vhk-cloud.ts
4560
4659
  var import_ignore = __toESM(require_ignore(), 1);
4561
- import fs12 from "fs";
4562
- import path13 from "path";
4660
+ import fs13 from "fs";
4661
+ import path14 from "path";
4563
4662
  var DEFAULT_CLOUD_EXCLUDES = [
4564
4663
  "memory.json",
4565
4664
  // 개인 의사결정 메모
@@ -4577,27 +4676,36 @@ var CLOUD_CONFIG_FILE = "cloud.json";
4577
4676
  function loadVhkignore(rootDir) {
4578
4677
  const ig = (0, import_ignore.default)();
4579
4678
  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"));
4679
+ const ignorePath = path14.join(rootDir, ".vhkignore");
4680
+ if (fs13.existsSync(ignorePath)) {
4681
+ ig.add(fs13.readFileSync(ignorePath, "utf-8"));
4583
4682
  }
4584
4683
  return ig;
4585
4684
  }
4586
4685
  function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
4587
- const vhkDir = path13.join(rootDir, VHK_DIR2);
4686
+ const vhkDir = path14.join(rootDir, VHK_DIR2);
4588
4687
  let entries;
4589
4688
  try {
4590
- entries = fs12.readdirSync(vhkDir, { withFileTypes: true });
4689
+ entries = fs13.readdirSync(vhkDir, { withFileTypes: true });
4591
4690
  } catch {
4592
4691
  return [];
4593
4692
  }
4594
4693
  return entries.filter((e) => e.isFile()).map((e) => e.name).filter((name) => !ig.ignores(name)).sort();
4595
4694
  }
4695
+ function partitionGistFiles(gistFiles, ig) {
4696
+ const keep = [];
4697
+ const excluded = [];
4698
+ for (const name of gistFiles) {
4699
+ if (name && ig.ignores(name)) excluded.push(name);
4700
+ else if (name) keep.push(name);
4701
+ }
4702
+ return { keep, excluded };
4703
+ }
4596
4704
  function readCloudConfig(rootDir) {
4597
- const p = path13.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4598
- if (!fs12.existsSync(p)) return null;
4705
+ const p = path14.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
4706
+ if (!fs13.existsSync(p)) return null;
4599
4707
  try {
4600
- const parsed = JSON.parse(fs12.readFileSync(p, "utf-8"));
4708
+ const parsed = JSON.parse(fs13.readFileSync(p, "utf-8"));
4601
4709
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
4602
4710
  return { gistId: parsed.gistId };
4603
4711
  }
@@ -4607,10 +4715,10 @@ function readCloudConfig(rootDir) {
4607
4715
  }
4608
4716
  }
4609
4717
  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");
4718
+ const vhkDir = path14.join(rootDir, VHK_DIR2);
4719
+ fs13.mkdirSync(vhkDir, { recursive: true });
4720
+ const p = path14.join(vhkDir, CLOUD_CONFIG_FILE);
4721
+ fs13.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
4614
4722
  }
4615
4723
 
4616
4724
  // src/commands/cloud.ts
@@ -4641,11 +4749,12 @@ async function cloudPush() {
4641
4749
  ${ko.cloud.pushTitle}
4642
4750
  `));
4643
4751
  const cwd = process.cwd();
4644
- if (!fs13.existsSync(path14.join(cwd, VHK_DIR2))) {
4752
+ if (!fs14.existsSync(path15.join(cwd, VHK_DIR2))) {
4645
4753
  console.log(chalk26.yellow(` ${ko.cloud.noVhkDir}`));
4646
4754
  return;
4647
4755
  }
4648
- const files = collectVhkFiles(cwd);
4756
+ const ig = loadVhkignore(cwd);
4757
+ const files = collectVhkFiles(cwd, ig);
4649
4758
  if (files.length === 0) {
4650
4759
  console.log(chalk26.yellow(` ${ko.cloud.nothingToSync}`));
4651
4760
  return;
@@ -4654,11 +4763,11 @@ ${ko.cloud.pushTitle}
4654
4763
  process.exitCode = 1;
4655
4764
  return;
4656
4765
  }
4657
- const filePaths = files.map((f) => path14.join(cwd, VHK_DIR2, f));
4766
+ const filePaths = files.map((f) => path15.join(cwd, VHK_DIR2, f));
4658
4767
  console.log(chalk26.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
4659
4768
  `));
4660
4769
  const existing = readCloudConfig(cwd);
4661
- const desc = `vhk .vhk backup \u2014 ${path14.basename(cwd)}`;
4770
+ const desc = `vhk .vhk backup \u2014 ${path15.basename(cwd)}`;
4662
4771
  if (existing) {
4663
4772
  const gistFiles = listGistFiles(existing.gistId);
4664
4773
  for (let i = 0; i < files.length; i++) {
@@ -4673,8 +4782,27 @@ ${ko.cloud.pushTitle}
4673
4782
  return;
4674
4783
  }
4675
4784
  }
4785
+ const { excluded } = partitionGistFiles(gistFiles, ig);
4786
+ const purgeFailed = [];
4787
+ if (excluded.length > 0) {
4788
+ const purgeOk = purgeExcludedFromGist(existing.gistId, excluded);
4789
+ if (!purgeOk) purgeFailed.push(...excluded);
4790
+ const stillThere = partitionGistFiles(listGistFiles(existing.gistId), ig).excluded;
4791
+ for (const name of stillThere) {
4792
+ if (!purgeFailed.includes(name)) purgeFailed.push(name);
4793
+ }
4794
+ }
4676
4795
  console.log(chalk26.green.bold(` ${ko.cloud.pushDone}`));
4677
4796
  console.log(chalk26.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
4797
+ if (excluded.length > 0) {
4798
+ const purged = excluded.filter((n) => !purgeFailed.includes(n));
4799
+ if (purged.length > 0) {
4800
+ console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
4801
+ }
4802
+ if (purgeFailed.length > 0) {
4803
+ 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)`));
4804
+ }
4805
+ }
4678
4806
  printPushNext();
4679
4807
  return;
4680
4808
  }
@@ -4712,14 +4840,22 @@ ${ko.cloud.pullTitle}
4712
4840
  process.exitCode = 1;
4713
4841
  return;
4714
4842
  }
4715
- const names = listGistFiles(gistId);
4716
- if (names.length === 0) {
4843
+ const allNames = listGistFiles(gistId);
4844
+ if (allNames.length === 0) {
4717
4845
  console.log(chalk26.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
4718
4846
  process.exitCode = 1;
4719
4847
  return;
4720
4848
  }
4721
- const vhkDir = path14.join(cwd, VHK_DIR2);
4722
- fs13.mkdirSync(vhkDir, { recursive: true });
4849
+ const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
4850
+ if (skipped.length > 0) {
4851
+ console.log(chalk26.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
4852
+ }
4853
+ if (names.length === 0) {
4854
+ 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).`));
4855
+ return;
4856
+ }
4857
+ const vhkDir = path15.join(cwd, VHK_DIR2);
4858
+ fs14.mkdirSync(vhkDir, { recursive: true });
4723
4859
  let restored = 0;
4724
4860
  for (const name of names) {
4725
4861
  const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
@@ -4728,7 +4864,7 @@ ${ko.cloud.pullTitle}
4728
4864
  console.log(chalk26.dim(` ${res.err}`));
4729
4865
  continue;
4730
4866
  }
4731
- fs13.writeFileSync(path14.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4867
+ fs14.writeFileSync(path15.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
4732
4868
  restored++;
4733
4869
  }
4734
4870
  writeCloudConfig(cwd, { gistId });
@@ -4740,6 +4876,30 @@ ${ko.cloud.pullTitle}
4740
4876
  cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uBCF4\uC5EC\uC918"
4741
4877
  });
4742
4878
  }
4879
+ function purgeExcludedFromGist(gistId, names) {
4880
+ if (names.length === 0) return true;
4881
+ const body = JSON.stringify({
4882
+ files: Object.fromEntries(names.map((n) => [n, null]))
4883
+ });
4884
+ const tmp = path15.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
4885
+ try {
4886
+ fs14.writeFileSync(tmp, body, "utf-8");
4887
+ for (let attempt = 0; attempt < 2; attempt++) {
4888
+ const res = safeExecFile(
4889
+ "gh",
4890
+ ["api", "--method", "PATCH", `/gists/${gistId}`, "--input", tmp],
4891
+ { timeoutMs: NETWORK_EXEC_TIMEOUT_MS }
4892
+ );
4893
+ if (res.ok) return true;
4894
+ }
4895
+ return false;
4896
+ } finally {
4897
+ try {
4898
+ fs14.unlinkSync(tmp);
4899
+ } catch {
4900
+ }
4901
+ }
4902
+ }
4743
4903
  function listGistFiles(gistId) {
4744
4904
  const res = safeExecFile("gh", ["gist", "view", gistId, "--files"]);
4745
4905
  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.1",
4
4
  "description": "Vibe Harness Kit — AI 코딩 도구·기기를 바꿔도 규칙·맥락이 따라가는 포터빌리티 CLI (sync: Cursor·Claude·Windsurf·Copilot·Antigravity / cloud 백업)",
5
5
  "bin": {
6
6
  "vhk": "dist/index.js",