@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 +5 -4
- package/dist/{chunk-4KWZANQG.js → chunk-O3A6SO7G.js} +87 -20
- package/dist/index.js +259 -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.0, ga]
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# 🔧 VHK — Vibe Harness Kit
|
|
8
8
|
|
|
9
|
-
> 🎉 **v1.
|
|
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.
|
|
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,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
|
-
|
|
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 =
|
|
2604
|
+
const dir = path11.dirname(fileURLToPath(import.meta.url));
|
|
2509
2605
|
const candidates = [
|
|
2510
|
-
|
|
2511
|
-
|
|
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 (
|
|
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 =
|
|
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 =
|
|
2591
|
-
if (
|
|
2592
|
-
const gitignore =
|
|
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
|
|
2625
|
-
import
|
|
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 =
|
|
2639
|
-
if (!
|
|
2640
|
-
const content =
|
|
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
|
-
|
|
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 =
|
|
2715
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
2863
|
+
fs11.writeFileSync(filePath, content, "utf-8");
|
|
2749
2864
|
console.log(chalk10.green(`
|
|
2750
|
-
${ko.ship.buildLogDone(
|
|
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
|
|
3066
|
-
import
|
|
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 =
|
|
3106
|
-
if (!
|
|
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
|
|
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(
|
|
4325
|
+
writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
|
|
4249
4326
|
console.log(chalk22.green(`
|
|
4250
|
-
\u2705 ${
|
|
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(
|
|
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(
|
|
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
|
|
4556
|
-
import
|
|
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
|
|
4562
|
-
import
|
|
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 =
|
|
4581
|
-
if (
|
|
4582
|
-
ig.add(
|
|
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 =
|
|
4665
|
+
const vhkDir = path14.join(rootDir, VHK_DIR2);
|
|
4588
4666
|
let entries;
|
|
4589
4667
|
try {
|
|
4590
|
-
entries =
|
|
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 =
|
|
4598
|
-
if (!
|
|
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(
|
|
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 =
|
|
4611
|
-
|
|
4612
|
-
const p =
|
|
4613
|
-
|
|
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 (!
|
|
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
|
|
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) =>
|
|
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 ${
|
|
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
|
|
4716
|
-
if (
|
|
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
|
|
4722
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED