@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 +5 -4
- package/dist/{chunk-4KWZANQG.js → chunk-O3A6SO7G.js} +87 -20
- package/dist/index.js +280 -120
- package/dist/mcp/index.js +1 -1
- package/package.json +1 -1
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.
|
|
4
|
+
tags: [vhk, cli, readme, v1.6.1, ga]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# 🔧 VHK — Vibe Harness Kit
|
|
8
8
|
|
|
9
|
-
> 🎉 **v1.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
1226
|
-
if (
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1236
|
-
|
|
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-
|
|
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
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
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
|
-
|
|
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 =
|
|
2625
|
+
const dir = path11.dirname(fileURLToPath(import.meta.url));
|
|
2509
2626
|
const candidates = [
|
|
2510
|
-
|
|
2511
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
2591
|
-
if (
|
|
2592
|
-
const gitignore =
|
|
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
|
|
2625
|
-
import
|
|
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 =
|
|
2639
|
-
if (!
|
|
2640
|
-
const content =
|
|
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
|
-
|
|
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 =
|
|
2715
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
2884
|
+
fs11.writeFileSync(filePath, content, "utf-8");
|
|
2749
2885
|
console.log(chalk10.green(`
|
|
2750
|
-
${ko.ship.buildLogDone(
|
|
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
|
|
3066
|
-
import
|
|
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 =
|
|
3106
|
-
if (!
|
|
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
|
|
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(
|
|
4346
|
+
writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
|
|
4249
4347
|
console.log(chalk22.green(`
|
|
4250
|
-
\u2705 ${
|
|
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(
|
|
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(
|
|
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
|
|
4556
|
-
import
|
|
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
|
|
4562
|
-
import
|
|
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 =
|
|
4581
|
-
if (
|
|
4582
|
-
ig.add(
|
|
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 =
|
|
4686
|
+
const vhkDir = path14.join(rootDir, VHK_DIR2);
|
|
4588
4687
|
let entries;
|
|
4589
4688
|
try {
|
|
4590
|
-
entries =
|
|
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 =
|
|
4598
|
-
if (!
|
|
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(
|
|
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 =
|
|
4611
|
-
|
|
4612
|
-
const p =
|
|
4613
|
-
|
|
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 (!
|
|
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
|
|
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) =>
|
|
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 ${
|
|
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
|
|
4716
|
-
if (
|
|
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
|
|
4722
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED