@byh3071/vhk 1.6.0 → 1.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/dist/{chunk-O3A6SO7G.js → chunk-ACJN723Q.js} +146 -44
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1957 -951
- package/dist/mcp/index.js +6 -1
- package/package.json +72 -71
package/dist/index.js
CHANGED
|
@@ -21,11 +21,13 @@ import {
|
|
|
21
21
|
scanProjectForSecrets,
|
|
22
22
|
startMcpServer,
|
|
23
23
|
t
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-ACJN723Q.js";
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
import { Command, Help } from "commander";
|
|
28
|
-
import
|
|
28
|
+
import { pathToFileURL } from "url";
|
|
29
|
+
import chalk35 from "chalk";
|
|
30
|
+
import inquirer14 from "inquirer";
|
|
29
31
|
|
|
30
32
|
// src/lib/nlp-router.ts
|
|
31
33
|
function normalize(input) {
|
|
@@ -43,6 +45,14 @@ function matchesKeywords(text, command) {
|
|
|
43
45
|
return keywords.some((kw) => text.includes(kw.toLowerCase()));
|
|
44
46
|
}
|
|
45
47
|
var RULES = [
|
|
48
|
+
// restore 는 cloud-pull/undo 보다 먼저 평가 — "백업 복원/되돌려" 가 클라우드 복원이나
|
|
49
|
+
// 커밋 되돌리기로 새지 않도록. "백업" 한정이라 bare "복원해"(=cloud-pull)·"되돌려"(=undo)는 안 가로챔.
|
|
50
|
+
{
|
|
51
|
+
command: "restore",
|
|
52
|
+
explanation: "sync \uBC31\uC5C5 \uBCF5\uC6D0 (vhk restore)",
|
|
53
|
+
confidence: "high",
|
|
54
|
+
test: (t2) => /백업/.test(t2) && /(복원|복구|되돌려|되살려|롤백|restore)/.test(t2)
|
|
55
|
+
},
|
|
46
56
|
// 영문 `vhk cloud push|pull [id]` 은 commander 가 직접 처리(가로채기 금지) — 한국어 표현만 매칭.
|
|
47
57
|
{
|
|
48
58
|
command: "cloud-pull",
|
|
@@ -69,6 +79,28 @@ var RULES = [
|
|
|
69
79
|
confidence: "high",
|
|
70
80
|
test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|새\s*프로젝트|^시작$|마법사|기획.*(끝|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2) && !/디자인|design|팔레트|palette|테마|theme|레퍼런스|reference|다크\s*모드|라이트\s*모드|색상\s*모드|브리핑|brief|컨텍스트|context|맥락|기억|memory|^초기화$|하네스.*만/.test(t2)
|
|
71
81
|
},
|
|
82
|
+
// 도움말 — 초보자가 "뭐부터/도움말/명령어" 라고 물으면 읽기전용 quick actions 를 출력.
|
|
83
|
+
// (적대 리뷰 HIGH 수정: 이전엔 start 마법사로 라우팅돼 도움말이 scaffold 를 유발했음.
|
|
84
|
+
// 도움말은 절대 상태를 바꾸지 않는다.) 실제 서브커맨드는 cli-args R1 가드가 먼저 commander 로 보냄.
|
|
85
|
+
{
|
|
86
|
+
command: "help",
|
|
87
|
+
explanation: "\uC790\uC5F0\uC5B4\uB85C vhk \uC4F0\uB294 \uBC95 \u2014 quick actions \uCD9C\uB825(\uC0C1\uD0DC\uBCC0\uACBD \uC5C6\uC74C)",
|
|
88
|
+
confidence: "high",
|
|
89
|
+
test: (t2) => /도움말|사용법|help|^명령어$|뭐\s*(할\s*수\s*있|하면\s*(돼|되|좋)|해야)|처음\s*(뭐|어떻게|시작|할)|어떻게\s*시작|뭐부터/.test(t2)
|
|
90
|
+
},
|
|
91
|
+
// Safety Mode — 위험 작업 가드 강도 조회/변경.
|
|
92
|
+
{
|
|
93
|
+
command: "mode",
|
|
94
|
+
explanation: "Safety Mode \uC870\uD68C/\uBCC0\uACBD (vhk mode)",
|
|
95
|
+
confidence: "high",
|
|
96
|
+
test: (t2) => /안전\s*모드|safety\s*mode|모드\s*(바꿔|변경|설정|확인|보여|뭐)|위험\s*작업\s*(가드|모드)/.test(t2)
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
command: "verify",
|
|
100
|
+
explanation: "\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C (vhk verify)",
|
|
101
|
+
confidence: "high",
|
|
102
|
+
test: (t2) => /검증\s*묶음|사전\s*검증|저장\s*전\s*(검증|확인)|^verify$/.test(t2)
|
|
103
|
+
},
|
|
72
104
|
{
|
|
73
105
|
command: "init",
|
|
74
106
|
explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
|
|
@@ -294,6 +326,28 @@ function extractNotionUrl(input) {
|
|
|
294
326
|
return m?.[0];
|
|
295
327
|
}
|
|
296
328
|
|
|
329
|
+
// src/lib/command-registry.ts
|
|
330
|
+
var CONTAINER_SUBCOMMANDS = {
|
|
331
|
+
goal: ["list", "next", "check", "init", "done"],
|
|
332
|
+
ref: ["add", "list", "open"],
|
|
333
|
+
memory: ["add", "list", "remove"],
|
|
334
|
+
cloud: ["push", "pull"],
|
|
335
|
+
secure: ["scan"],
|
|
336
|
+
design: ["palette"],
|
|
337
|
+
env: ["check"],
|
|
338
|
+
mode: ["lite", "standard", "strict"]
|
|
339
|
+
};
|
|
340
|
+
var CONTAINER_ALIASES = {
|
|
341
|
+
\uBAA9\uD45C: "goal",
|
|
342
|
+
\uB808\uD37C\uB7F0\uC2A4: "ref",
|
|
343
|
+
\uAE30\uC5B5: "memory",
|
|
344
|
+
\uD074\uB77C\uC6B0\uB4DC: "cloud",
|
|
345
|
+
\uBCF4\uC548: "secure",
|
|
346
|
+
\uB514\uC790\uC778: "design",
|
|
347
|
+
\uD658\uACBD\uBCC0\uC218: "env",
|
|
348
|
+
\uBAA8\uB4DC: "mode"
|
|
349
|
+
};
|
|
350
|
+
|
|
297
351
|
// src/lib/cli-args.ts
|
|
298
352
|
var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
299
353
|
"gate",
|
|
@@ -326,6 +380,8 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
326
380
|
"\uC800\uC7A5",
|
|
327
381
|
"undo",
|
|
328
382
|
"\uB418\uB3CC\uB9AC\uAE30",
|
|
383
|
+
"restore",
|
|
384
|
+
"\uBCF5\uC6D0",
|
|
329
385
|
"status",
|
|
330
386
|
"\uC0C1\uD0DC",
|
|
331
387
|
"\uD604\uD669",
|
|
@@ -378,11 +434,28 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
378
434
|
"\uAD50\uD6C8",
|
|
379
435
|
"resume",
|
|
380
436
|
"\uC7AC\uAC1C",
|
|
437
|
+
"mode",
|
|
438
|
+
"\uBAA8\uB4DC",
|
|
439
|
+
"verify",
|
|
440
|
+
"\uC0AC\uC804\uC810\uAC80",
|
|
381
441
|
"help"
|
|
382
442
|
]);
|
|
383
443
|
function isOptionToken(token) {
|
|
384
444
|
return token.startsWith("-");
|
|
385
445
|
}
|
|
446
|
+
var COMMAND_SUBCOMMANDS = (() => {
|
|
447
|
+
const map = { ...CONTAINER_SUBCOMMANDS };
|
|
448
|
+
for (const [alias, canonical] of Object.entries(CONTAINER_ALIASES)) {
|
|
449
|
+
const subs = CONTAINER_SUBCOMMANDS[canonical];
|
|
450
|
+
if (subs) map[alias] = subs;
|
|
451
|
+
}
|
|
452
|
+
return map;
|
|
453
|
+
})();
|
|
454
|
+
function isRealSubcommandPath(first, second) {
|
|
455
|
+
if (second === void 0) return false;
|
|
456
|
+
const subs = COMMAND_SUBCOMMANDS[first];
|
|
457
|
+
return subs !== void 0 && subs.includes(second);
|
|
458
|
+
}
|
|
386
459
|
function detectNaturalLanguageInput(argv) {
|
|
387
460
|
const rest = argv.slice(2);
|
|
388
461
|
if (rest.length === 0) return null;
|
|
@@ -394,6 +467,7 @@ function detectNaturalLanguageInput(argv) {
|
|
|
394
467
|
const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
|
|
395
468
|
if (firstIsKnown && rest.length === 1) return null;
|
|
396
469
|
if (firstIsKnown && rest.length > 1) {
|
|
470
|
+
if (isRealSubcommandPath(first, rest[1])) return null;
|
|
397
471
|
if (routeNaturalLanguage(input)) return input;
|
|
398
472
|
return null;
|
|
399
473
|
}
|
|
@@ -401,8 +475,8 @@ function detectNaturalLanguageInput(argv) {
|
|
|
401
475
|
}
|
|
402
476
|
|
|
403
477
|
// src/lib/nlp-run.ts
|
|
404
|
-
import
|
|
405
|
-
import
|
|
478
|
+
import chalk33 from "chalk";
|
|
479
|
+
import inquirer13 from "inquirer";
|
|
406
480
|
|
|
407
481
|
// src/commands/gate.ts
|
|
408
482
|
import inquirer from "inquirer";
|
|
@@ -431,7 +505,7 @@ async function gate() {
|
|
|
431
505
|
console.log(chalk.bold(`
|
|
432
506
|
${ko.gate.title}
|
|
433
507
|
`));
|
|
434
|
-
const { mode } = await inquirer.prompt([{
|
|
508
|
+
const { mode: mode2 } = await inquirer.prompt([{
|
|
435
509
|
type: "list",
|
|
436
510
|
name: "mode",
|
|
437
511
|
message: ko.gate.modePrompt,
|
|
@@ -441,7 +515,7 @@ ${ko.gate.title}
|
|
|
441
515
|
{ name: ko.gate.modeSkipLabel, value: "skip" }
|
|
442
516
|
]
|
|
443
517
|
}]);
|
|
444
|
-
if (
|
|
518
|
+
if (mode2 === "skip") {
|
|
445
519
|
const { source } = await inquirer.prompt([{
|
|
446
520
|
type: "input",
|
|
447
521
|
name: "source",
|
|
@@ -452,9 +526,9 @@ ${ko.gate.skipGo}`));
|
|
|
452
526
|
console.log(chalk.dim(ko.gate.skipSourceLabel(source)));
|
|
453
527
|
return;
|
|
454
528
|
}
|
|
455
|
-
const questions =
|
|
529
|
+
const questions = mode2 === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
|
|
456
530
|
const total = questions.length;
|
|
457
|
-
const header =
|
|
531
|
+
const header = mode2 === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
|
|
458
532
|
console.log(chalk.dim(`
|
|
459
533
|
${header} ${ko.gate.modeCountSuffix(total)}
|
|
460
534
|
`));
|
|
@@ -540,9 +614,14 @@ import chalk3 from "chalk";
|
|
|
540
614
|
import fs2 from "fs";
|
|
541
615
|
import path2 from "path";
|
|
542
616
|
|
|
617
|
+
// src/lib/date.ts
|
|
618
|
+
function localDate(d = /* @__PURE__ */ new Date()) {
|
|
619
|
+
return d.toLocaleDateString("sv-SE");
|
|
620
|
+
}
|
|
621
|
+
|
|
543
622
|
// src/templates/claude-md.ts
|
|
544
623
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
545
|
-
const d = (
|
|
624
|
+
const d = localDate();
|
|
546
625
|
const slug = name.toLowerCase().replace(/\s+/g, "-");
|
|
547
626
|
return [
|
|
548
627
|
"---",
|
|
@@ -554,12 +633,12 @@ function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
|
554
633
|
"# \uAE30\uB85D \uADDC\uCE59 (" + name + ")",
|
|
555
634
|
"",
|
|
556
635
|
"> \uC774 \uD30C\uC77C\uC740 \uAE30\uB85D/\uC6B4\uC601 \uC804\uC6A9. \uCF54\uB529/\uB514\uC790\uC778 \u2192 .cursorrules \uCC38\uC870.",
|
|
557
|
-
"> See also: AGENTS.md",
|
|
636
|
+
"> See also: AGENTS.md (`vhk sync` \uB85C \uC0DD\uC131 \u2014 Codex/OpenAI \uACC4\uC5F4 \uD638\uD658).",
|
|
558
637
|
"",
|
|
559
638
|
"## \uD604\uC7AC \uC0C1\uD0DC",
|
|
560
639
|
"- **Phase:** Phase 1 \u2014 MVP",
|
|
561
640
|
"- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C",
|
|
562
|
-
"- **\uB2E4\uC74C \uC561\uC158:**
|
|
641
|
+
"- **\uB2E4\uC74C \uC561\uC158:** **FILL**",
|
|
563
642
|
"- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** " + d,
|
|
564
643
|
"",
|
|
565
644
|
"## ADR",
|
|
@@ -613,8 +692,39 @@ function CURSORRULES_TEMPLATE(name, desc, stack) {
|
|
|
613
692
|
].join("\n");
|
|
614
693
|
}
|
|
615
694
|
|
|
695
|
+
// src/templates/rules-md.ts
|
|
696
|
+
function RULES_MD_TEMPLATE(name, description, stack) {
|
|
697
|
+
const stackList = stack.split(" + ").map((s) => "- " + s).join("\n");
|
|
698
|
+
return [
|
|
699
|
+
"# " + name + " \u2014 Rules",
|
|
700
|
+
"",
|
|
701
|
+
"> \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 \uB2E8\uC77C \uC18C\uC2A4(SoT). \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 \uC774 \uD30C\uC77C\uC5D0\uC11C\uB9CC.",
|
|
702
|
+
"> `vhk sync` \uAC00 Cursor\xB7Claude\xB7Windsurf\xB7Copilot\xB7Antigravity \uADDC\uCE59\uC73C\uB85C \uC804\uD30C\uD569\uB2C8\uB2E4.",
|
|
703
|
+
"",
|
|
704
|
+
"## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131",
|
|
705
|
+
"- \uD55C \uC904 \uC124\uBA85: " + description,
|
|
706
|
+
"- \uC2A4\uD0DD: " + stack,
|
|
707
|
+
"",
|
|
708
|
+
"## \uAE30\uC220 \uC2A4\uD0DD",
|
|
709
|
+
stackList,
|
|
710
|
+
"",
|
|
711
|
+
"## \uCF54\uB529 \uADDC\uCE59",
|
|
712
|
+
"- TypeScript strict (any \uAE08\uC9C0)",
|
|
713
|
+
"- try-catch \uD544\uC218, \uBE48 catch \uAE08\uC9C0",
|
|
714
|
+
"- console.log \uD504\uB85C\uB355\uC158 \uC81C\uAC70",
|
|
715
|
+
"- \uD30C\uC77C\uBA85\uC740 kebab-case",
|
|
716
|
+
"",
|
|
717
|
+
"## \uCEE4\uBC0B \uCEE8\uBCA4\uC158",
|
|
718
|
+
"- feat: / fix: / refactor: / docs: / chore:",
|
|
719
|
+
"",
|
|
720
|
+
"## \uAE30\uB85D \uADDC\uCE59",
|
|
721
|
+
"- \uC138\uC158 \uC885\uB8CC \uC2DC docs/log/YYYY-MM-DD-{\uC791\uC5C5\uBA85}.md \uC0DD\uC131",
|
|
722
|
+
"- \uAE30\uC220 \uC120\uD0DD \uC2DC docs/adr/ADR-{\uBC88\uD638}-{\uC81C\uBAA9}.md \uC0DD\uC131"
|
|
723
|
+
].join("\n");
|
|
724
|
+
}
|
|
725
|
+
|
|
616
726
|
// src/templates/prd.ts
|
|
617
|
-
var FILL = "
|
|
727
|
+
var FILL = "**FILL**";
|
|
618
728
|
function fill(value, fallback = FILL) {
|
|
619
729
|
return value?.trim() || fallback;
|
|
620
730
|
}
|
|
@@ -683,12 +793,12 @@ function ARCHITECTURE_TEMPLATE(name, stack) {
|
|
|
683
793
|
"## \uB370\uC774\uD130 \uBAA8\uB378",
|
|
684
794
|
"| \uD14C\uC774\uBE14 | \uD575\uC2EC \uCEEC\uB7FC | \uC124\uBA85 |",
|
|
685
795
|
"|--------|----------|------|",
|
|
686
|
-
"|
|
|
796
|
+
"| **FILL** | | |",
|
|
687
797
|
"",
|
|
688
798
|
"## \uC678\uBD80 \uC11C\uBE44\uC2A4",
|
|
689
799
|
"| \uC11C\uBE44\uC2A4 | \uC6A9\uB3C4 |",
|
|
690
800
|
"|--------|------|",
|
|
691
|
-
"|
|
|
801
|
+
"| **FILL** | |"
|
|
692
802
|
].join("\n");
|
|
693
803
|
}
|
|
694
804
|
|
|
@@ -705,21 +815,22 @@ function ADR_TEMPLATE() {
|
|
|
705
815
|
"# ADR-000: \uC81C\uBAA9",
|
|
706
816
|
"",
|
|
707
817
|
"## \uB9E5\uB77D (Context)",
|
|
708
|
-
"
|
|
818
|
+
"**FILL**",
|
|
709
819
|
"",
|
|
710
820
|
"## \uACB0\uC815 (Decision)",
|
|
711
|
-
"
|
|
821
|
+
"**FILL**",
|
|
712
822
|
"",
|
|
713
823
|
"## \uB300\uC548 (Alternatives)",
|
|
714
|
-
"
|
|
824
|
+
"**FILL**",
|
|
715
825
|
"",
|
|
716
826
|
"## \uACB0\uACFC (Consequences)",
|
|
717
|
-
"
|
|
827
|
+
"**FILL**"
|
|
718
828
|
].join("\n");
|
|
719
829
|
}
|
|
720
830
|
|
|
721
831
|
// src/templates/commands-md.ts
|
|
722
|
-
function COMMANDS_MD_TEMPLATE() {
|
|
832
|
+
function COMMANDS_MD_TEMPLATE(opts = {}) {
|
|
833
|
+
const buildTestRow = opts.hasTest ? '| \uBE4C\uB4DC+\uD14C\uC2A4\uD2B8 | `pnpm build; pnpm test --run` | "\uBE4C\uB4DC\uD558\uACE0 \uD14C\uC2A4\uD2B8 \uB3CC\uB824" |' : '| \uBE4C\uB4DC | `pnpm build` | "\uBE4C\uB4DC\uD574" |';
|
|
723
834
|
return [
|
|
724
835
|
"# \u{1F4CB} \uD55C\uAD6D\uC5B4 \uBA85\uB839\uC5B4 \uAC00\uC774\uB4DC",
|
|
725
836
|
"",
|
|
@@ -742,7 +853,7 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
742
853
|
'| \uC624\uB298 \uC815\uB9AC | `vhk \uC815\uB9AC` | "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574" |',
|
|
743
854
|
'| \uADDC\uCE59 \uC810\uAC80 | `vhk \uC810\uAC80` | "\uADDC\uCE59 \uC810\uAC80\uD574" |',
|
|
744
855
|
'| \uBCF4\uC548 \uC2A4\uCE94 | `vhk \uBCF4\uC548 scan` | "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824" |',
|
|
745
|
-
|
|
856
|
+
buildTestRow,
|
|
746
857
|
'| \uBC30\uD3EC | `vhk \uBC30\uD3EC` | "\uBC30\uD3EC\uD574" |',
|
|
747
858
|
"",
|
|
748
859
|
"## \uD658\uACBD \uC810\uAC80",
|
|
@@ -762,7 +873,8 @@ function VHK_README_TEMPLATE() {
|
|
|
762
873
|
"# `.vhk/` \u2014 VHK runtime state",
|
|
763
874
|
"",
|
|
764
875
|
"\uC774 \uB514\uB809\uD1A0\uB9AC\uB294 VHK\uAC00 \uD504\uB85C\uC81D\uD2B8\uBCC4 \uC0C1\uD0DC\uB97C \uC800\uC7A5\uD558\uB294 \uACF3\uC785\uB2C8\uB2E4.",
|
|
765
|
-
|
|
876
|
+
// VHK-006: 생성 프로젝트엔 docs/spec.md 가 없음 → vhk 저장소의 규격 문서를 외부 링크로 참조.
|
|
877
|
+
"\uC804\uCCB4 \uADDC\uACA9\uC740 [vhk \uADDC\uACA9 \uBB38\uC11C](https://github.com/byh3071-cpu/vhk/blob/main/docs/spec.md) (spec_version 1.0) \uCC38\uC870.",
|
|
766
878
|
"",
|
|
767
879
|
"## \uD2B8\uB798\uD0B9 \uC815\uCC45",
|
|
768
880
|
"",
|
|
@@ -782,10 +894,12 @@ function VHK_README_TEMPLATE() {
|
|
|
782
894
|
}
|
|
783
895
|
function VHK_GITIGNORE_TEMPLATE() {
|
|
784
896
|
return [
|
|
785
|
-
"# VHK \uB85C\uCEEC \uC804\uC6A9 \u2014 \uAC1C\uC778 \uBA54\uBAA8/\uCC38\uACE0\uB9C1\uD06C/\uC548\uC804\uC2E0\uD638 (
|
|
897
|
+
"# VHK \uB85C\uCEEC \uC804\uC6A9 \u2014 \uAC1C\uC778 \uBA54\uBAA8/\uCC38\uACE0\uB9C1\uD06C/\uC548\uC804\uC2E0\uD638 (.vhk/README.md \uD2B8\uB798\uD0B9 \uC815\uCC45)",
|
|
786
898
|
"memory.json",
|
|
787
899
|
"refs.json",
|
|
788
900
|
"HARD_STOP",
|
|
901
|
+
"# sync \uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uBC31\uC5C5 (\uB85C\uCEEC \uBCF5\uAD6C\uC6A9 \u2014 vhk restore). \uCD94\uC801/\uD074\uB77C\uC6B0\uB4DC \uC81C\uC678.",
|
|
902
|
+
"backups/",
|
|
789
903
|
""
|
|
790
904
|
].join("\n");
|
|
791
905
|
}
|
|
@@ -988,6 +1102,122 @@ async function fetchPrdFromNotion(urlOrId) {
|
|
|
988
1102
|
};
|
|
989
1103
|
}
|
|
990
1104
|
|
|
1105
|
+
// src/lib/rules-import.ts
|
|
1106
|
+
import { existsSync, readFileSync } from "fs";
|
|
1107
|
+
import { join } from "path";
|
|
1108
|
+
var ADOPT_SOURCES = [
|
|
1109
|
+
".cursorrules",
|
|
1110
|
+
"CLAUDE.md",
|
|
1111
|
+
"AGENTS.md",
|
|
1112
|
+
".windsurfrules",
|
|
1113
|
+
".github/copilot-instructions.md"
|
|
1114
|
+
];
|
|
1115
|
+
var PREAMBLE_TITLE = "\uC11C\uBB38";
|
|
1116
|
+
function detectExistingRuleFiles(cwd) {
|
|
1117
|
+
const found = [];
|
|
1118
|
+
for (const rel of ADOPT_SOURCES) {
|
|
1119
|
+
const full = join(cwd, rel);
|
|
1120
|
+
if (existsSync(full)) {
|
|
1121
|
+
try {
|
|
1122
|
+
found.push({ path: rel, content: readFileSync(full, "utf-8") });
|
|
1123
|
+
} catch {
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return found;
|
|
1128
|
+
}
|
|
1129
|
+
function splitSections(content) {
|
|
1130
|
+
const sections = [];
|
|
1131
|
+
let title = "";
|
|
1132
|
+
let buf = [];
|
|
1133
|
+
const preamble = [];
|
|
1134
|
+
let sawHeading = false;
|
|
1135
|
+
for (const line of content.split("\n")) {
|
|
1136
|
+
if (line.startsWith("## ")) {
|
|
1137
|
+
sawHeading = true;
|
|
1138
|
+
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1139
|
+
title = line.replace("## ", "").trim();
|
|
1140
|
+
buf = [];
|
|
1141
|
+
} else if (title) {
|
|
1142
|
+
buf.push(line);
|
|
1143
|
+
} else if (!sawHeading) {
|
|
1144
|
+
preamble.push(line);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1148
|
+
const pre = preamble.join("\n").trim();
|
|
1149
|
+
if (pre) sections.unshift({ title: PREAMBLE_TITLE, content: pre });
|
|
1150
|
+
return sections;
|
|
1151
|
+
}
|
|
1152
|
+
function buildAdoptedRules(files, projectName) {
|
|
1153
|
+
const order = [];
|
|
1154
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const file of files) {
|
|
1156
|
+
for (const sec of splitSections(file.content)) {
|
|
1157
|
+
let merged = byTitle.get(sec.title);
|
|
1158
|
+
if (!merged) {
|
|
1159
|
+
merged = { title: sec.title, parts: [] };
|
|
1160
|
+
byTitle.set(sec.title, merged);
|
|
1161
|
+
order.push(sec.title);
|
|
1162
|
+
}
|
|
1163
|
+
merged.parts.push({ source: file.path, content: sec.content });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const lines = [
|
|
1167
|
+
`# ${projectName} \u2014 Rules`,
|
|
1168
|
+
"",
|
|
1169
|
+
"> \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 \uB2E8\uC77C \uC18C\uC2A4(SoT). \uAE30\uC874 \uADDC\uCE59\uC744 `vhk init` adopt \uB85C \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.",
|
|
1170
|
+
"> \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 \uC774 \uD30C\uC77C\uC5D0\uC11C\uB9CC \u2014 `vhk sync` \uB85C \uAC01 \uB3C4\uAD6C\uC5D0 \uC804\uD30C\uB429\uB2C8\uB2E4.",
|
|
1171
|
+
""
|
|
1172
|
+
];
|
|
1173
|
+
for (const title of order) {
|
|
1174
|
+
const merged = byTitle.get(title);
|
|
1175
|
+
const nonEmpty = merged.parts.filter((p) => p.content.trim());
|
|
1176
|
+
if (!nonEmpty.length) continue;
|
|
1177
|
+
lines.push(`## ${title}`);
|
|
1178
|
+
for (const part of nonEmpty) {
|
|
1179
|
+
lines.push(`<!-- \uCD9C\uCC98: ${part.source} -->`);
|
|
1180
|
+
lines.push(part.content);
|
|
1181
|
+
}
|
|
1182
|
+
lines.push("");
|
|
1183
|
+
}
|
|
1184
|
+
return lines.join("\n");
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/lib/stack-detect.ts
|
|
1188
|
+
import { join as join2 } from "path";
|
|
1189
|
+
function detectStackFromDeps(deps) {
|
|
1190
|
+
const stack = [];
|
|
1191
|
+
if (deps.next) stack.push("Next.js");
|
|
1192
|
+
else if (deps.nuxt) stack.push("Nuxt");
|
|
1193
|
+
else if (deps.vite || deps["@vitejs/plugin-react"]) stack.push("Vite");
|
|
1194
|
+
if (deps.react) stack.push("React");
|
|
1195
|
+
else if (deps.vue) stack.push("Vue");
|
|
1196
|
+
else if (deps.svelte) stack.push("Svelte");
|
|
1197
|
+
if (deps.typescript) stack.push("TypeScript");
|
|
1198
|
+
if (deps.tailwindcss || deps["@tailwindcss/vite"]) stack.push("Tailwind CSS");
|
|
1199
|
+
if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"]) stack.push("Supabase");
|
|
1200
|
+
if (deps["@prisma/client"] || deps.prisma) stack.push("Prisma");
|
|
1201
|
+
if (deps["drizzle-orm"]) stack.push("Drizzle");
|
|
1202
|
+
if (deps["@anthropic-ai/sdk"]) stack.push("Anthropic SDK");
|
|
1203
|
+
if (deps.openai) stack.push("OpenAI SDK");
|
|
1204
|
+
if (deps.zod) stack.push("zod");
|
|
1205
|
+
if (deps["@tanstack/react-query"]) stack.push("TanStack Query");
|
|
1206
|
+
return stack;
|
|
1207
|
+
}
|
|
1208
|
+
function detectProjectStack(cwd = ".") {
|
|
1209
|
+
let pkg;
|
|
1210
|
+
try {
|
|
1211
|
+
pkg = readJsonFile(join2(cwd, "package.json"));
|
|
1212
|
+
} catch {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1216
|
+
if (Object.keys(all).length === 0) return null;
|
|
1217
|
+
const stack = detectStackFromDeps(all);
|
|
1218
|
+
return stack.length ? stack : null;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
991
1221
|
// src/commands/init.ts
|
|
992
1222
|
var PROJECT_TYPES = [
|
|
993
1223
|
{ name: "\u{1F310} \uC6F9 \uC571 (Next.js + Supabase + Vercel)", value: "webapp" },
|
|
@@ -1060,7 +1290,9 @@ ${ko.init.title}
|
|
|
1060
1290
|
log.error("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984, \uC124\uBA85, \uC720\uD615\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
1061
1291
|
process.exit(1);
|
|
1062
1292
|
}
|
|
1063
|
-
const
|
|
1293
|
+
const detected = detectProjectStack(process.cwd());
|
|
1294
|
+
const stack = detected ?? STACK_PRESETS[answers.type];
|
|
1295
|
+
if (detected) console.log(chalk3.dim(" \u{1F50E} package.json \uC758\uC874\uC131\uC5D0\uC11C \uC2E4\uC81C \uC2A4\uD0DD \uAC10\uC9C0"));
|
|
1064
1296
|
console.log(chalk3.dim(`
|
|
1065
1297
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1066
1298
|
`));
|
|
@@ -1077,7 +1309,27 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1077
1309
|
}
|
|
1078
1310
|
}
|
|
1079
1311
|
const cwd = process.cwd();
|
|
1312
|
+
let adoptedRules = null;
|
|
1313
|
+
if (!options.yes && !options.fromNotion) {
|
|
1314
|
+
const existingRules = detectExistingRuleFiles(cwd);
|
|
1315
|
+
if (existingRules.length > 0) {
|
|
1316
|
+
const { adopt } = await inquirer2.prompt([{
|
|
1317
|
+
type: "confirm",
|
|
1318
|
+
name: "adopt",
|
|
1319
|
+
message: ko.init.adoptPrompt(
|
|
1320
|
+
existingRules.length,
|
|
1321
|
+
existingRules.map((f) => f.path).join(", ")
|
|
1322
|
+
),
|
|
1323
|
+
default: true
|
|
1324
|
+
}]);
|
|
1325
|
+
if (adopt) {
|
|
1326
|
+
adoptedRules = buildAdoptedRules(existingRules, answers.name);
|
|
1327
|
+
console.log(chalk3.dim(` ${ko.init.adoptPreview(existingRules.length)}`));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1080
1331
|
const files = generateFiles(answers.name, answers.description, stack, prdContent, answers.type);
|
|
1332
|
+
if (adoptedRules) files["RULES.md"] = adoptedRules;
|
|
1081
1333
|
log.step(ko.init.filesGenerating);
|
|
1082
1334
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1083
1335
|
const fullPath = path2.join(cwd, filePath);
|
|
@@ -1131,6 +1383,8 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
|
|
|
1131
1383
|
return {
|
|
1132
1384
|
"CLAUDE.md": CLAUDE_MD_TEMPLATE(name, stackStr),
|
|
1133
1385
|
".cursorrules": CURSORRULES_TEMPLATE(name, description, stackStr),
|
|
1386
|
+
// RULES.md — 규칙 SoT. init 이 항상 생성해 sync 와 흐름을 연결한다.
|
|
1387
|
+
"RULES.md": RULES_MD_TEMPLATE(name, description, stackStr),
|
|
1134
1388
|
"docs/PRD.md": PRD_TEMPLATE(name, description, prd),
|
|
1135
1389
|
"docs/ARCHITECTURE.md": ARCHITECTURE_TEMPLATE(name, stackStr),
|
|
1136
1390
|
"docs/adr/ADR-000-template.md": ADR_TEMPLATE(),
|
|
@@ -1138,8 +1392,9 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
|
|
|
1138
1392
|
"docs/troubleshooting/.gitkeep": "",
|
|
1139
1393
|
"docs/til.md": `# TIL (Today I Learned)
|
|
1140
1394
|
|
|
1141
|
-
- [${(
|
|
1395
|
+
- [${localDate()}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
|
|
1142
1396
|
`,
|
|
1397
|
+
// VHK-019
|
|
1143
1398
|
"BACKLOG.md": `# BACKLOG
|
|
1144
1399
|
|
|
1145
1400
|
> v1 OUT \uAE30\uB2A5\uC740 \uC5EC\uAE30\uC5D0 \uAE30\uB85D. \uBC94\uC704 \uC218\uBE44 \uD544\uC218.
|
|
@@ -1197,8 +1452,19 @@ function enhancePackageScripts(projectDir) {
|
|
|
1197
1452
|
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1198
1453
|
return true;
|
|
1199
1454
|
}
|
|
1455
|
+
function projectHasTestScript(projectDir) {
|
|
1456
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
1457
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
1458
|
+
try {
|
|
1459
|
+
const pkg = readJsonFile(pkgPath);
|
|
1460
|
+
return Boolean(pkg.scripts?.test?.trim());
|
|
1461
|
+
} catch {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1200
1465
|
async function writeInitExtras(projectDir) {
|
|
1201
1466
|
const commandsPath = path2.join(projectDir, "COMMANDS.md");
|
|
1467
|
+
const hasTest = projectHasTestScript(projectDir);
|
|
1202
1468
|
if (fileExists(commandsPath)) {
|
|
1203
1469
|
const { overwrite } = await inquirer2.prompt([{
|
|
1204
1470
|
type: "confirm",
|
|
@@ -1209,7 +1475,7 @@ async function writeInitExtras(projectDir) {
|
|
|
1209
1475
|
if (!overwrite) {
|
|
1210
1476
|
log.warn(ko.init.skipped("COMMANDS.md"));
|
|
1211
1477
|
} else {
|
|
1212
|
-
writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
|
|
1478
|
+
writeFile(commandsPath, COMMANDS_MD_TEMPLATE({ hasTest }));
|
|
1213
1479
|
log.success(ko.init.commandsMdDone);
|
|
1214
1480
|
}
|
|
1215
1481
|
} else {
|
|
@@ -1229,7 +1495,7 @@ async function writeInitExtras(projectDir) {
|
|
|
1229
1495
|
|
|
1230
1496
|
// src/commands/recap.ts
|
|
1231
1497
|
import inquirer3 from "inquirer";
|
|
1232
|
-
import
|
|
1498
|
+
import chalk6 from "chalk";
|
|
1233
1499
|
import fs4 from "fs";
|
|
1234
1500
|
import path5 from "path";
|
|
1235
1501
|
|
|
@@ -1273,10 +1539,13 @@ function buildSessionDiffFromSummary(diffSummary) {
|
|
|
1273
1539
|
files
|
|
1274
1540
|
};
|
|
1275
1541
|
}
|
|
1542
|
+
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1276
1543
|
async function getSessionDiff(since) {
|
|
1277
|
-
const sinceDate = since || (
|
|
1544
|
+
const sinceDate = since || localDate();
|
|
1278
1545
|
try {
|
|
1279
|
-
const
|
|
1546
|
+
const boundary = (await git.raw(["rev-list", "-1", `--before=${sinceDate}`, "HEAD"])).trim();
|
|
1547
|
+
const base = boundary || EMPTY_TREE_SHA;
|
|
1548
|
+
const diffSummary = await git.diffSummary([`${base}..HEAD`]);
|
|
1280
1549
|
const normalized = diffSummary.files.map((f) => ({
|
|
1281
1550
|
file: f.file,
|
|
1282
1551
|
insertions: "insertions" in f ? f.insertions : 0,
|
|
@@ -1379,7 +1648,7 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1379
1648
|
const adrDir = path4.join(cwd, "docs", "adr");
|
|
1380
1649
|
if (!fs3.existsSync(adrDir)) fs3.mkdirSync(adrDir, { recursive: true });
|
|
1381
1650
|
const num = nextAdrNumber(adrDir);
|
|
1382
|
-
const today = (
|
|
1651
|
+
const today = localDate();
|
|
1383
1652
|
const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
|
|
1384
1653
|
const filePath = path4.join(adrDir, fileName);
|
|
1385
1654
|
const content = [
|
|
@@ -1411,50 +1680,191 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1411
1680
|
return filePath;
|
|
1412
1681
|
}
|
|
1413
1682
|
|
|
1683
|
+
// src/lib/interactive.ts
|
|
1684
|
+
import chalk4 from "chalk";
|
|
1685
|
+
function ensureInteractive(hint = "") {
|
|
1686
|
+
if (process.stdin.isTTY) return true;
|
|
1687
|
+
console.error(chalk4.yellow(" \u26A0\uFE0F \uC774 \uBA85\uB839\uC740 \uB300\uD654\uD615 \uC785\uB825\uC774 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uBE44-TTY/\uD30C\uC774\uD504 \uD658\uACBD\uC5D0\uC11C\uB294 \uC2E4\uD589\uD560 \uC218 \uC5C6\uC5B4\uC694."));
|
|
1688
|
+
if (hint) console.error(chalk4.dim(` ${hint}`));
|
|
1689
|
+
process.exitCode = 1;
|
|
1690
|
+
return false;
|
|
1691
|
+
}
|
|
1692
|
+
function isPromptAbortError(err) {
|
|
1693
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1694
|
+
return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/lib/hard-stop-guard.ts
|
|
1698
|
+
import chalk5 from "chalk";
|
|
1699
|
+
|
|
1700
|
+
// src/lib/state-files.ts
|
|
1701
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync, rmSync } from "fs";
|
|
1702
|
+
import { join as join3 } from "path";
|
|
1703
|
+
var STATE_DIR = "docs/state";
|
|
1704
|
+
var BLOCKERS_PATH = join3(STATE_DIR, "blockers.md");
|
|
1705
|
+
var LEARNINGS_PATH = join3(STATE_DIR, "learnings.md");
|
|
1706
|
+
var VHK_DIR = ".vhk";
|
|
1707
|
+
var HARD_STOP_PATH = join3(VHK_DIR, "HARD_STOP");
|
|
1708
|
+
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
1709
|
+
function ensureStateDir() {
|
|
1710
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
1711
|
+
}
|
|
1712
|
+
function ensureVhkDir() {
|
|
1713
|
+
mkdirSync(VHK_DIR, { recursive: true });
|
|
1714
|
+
}
|
|
1715
|
+
function isoDate() {
|
|
1716
|
+
return localDate();
|
|
1717
|
+
}
|
|
1718
|
+
var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
|
|
1719
|
+
function countActiveBlockers(content) {
|
|
1720
|
+
let count = 0;
|
|
1721
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1722
|
+
if (ACTIVE_BLOCKER_RE.test(line)) count++;
|
|
1723
|
+
}
|
|
1724
|
+
return count;
|
|
1725
|
+
}
|
|
1726
|
+
function appendBlocker(description, goalId) {
|
|
1727
|
+
ensureStateDir();
|
|
1728
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1729
|
+
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
1730
|
+
if (!existsSync2(BLOCKERS_PATH)) {
|
|
1731
|
+
writeFileSync(
|
|
1732
|
+
BLOCKERS_PATH,
|
|
1733
|
+
`# Blockers
|
|
1734
|
+
|
|
1735
|
+
_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
|
|
1736
|
+
|
|
1737
|
+
${line}
|
|
1738
|
+
`,
|
|
1739
|
+
"utf-8"
|
|
1740
|
+
);
|
|
1741
|
+
} else {
|
|
1742
|
+
appendFileSync(BLOCKERS_PATH, `${line}
|
|
1743
|
+
`, "utf-8");
|
|
1744
|
+
}
|
|
1745
|
+
const current = readFileSync2(BLOCKERS_PATH, "utf-8");
|
|
1746
|
+
const count = countActiveBlockers(current);
|
|
1747
|
+
let hardStopTripped = false;
|
|
1748
|
+
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync2(HARD_STOP_PATH)) {
|
|
1749
|
+
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
1750
|
+
hardStopTripped = true;
|
|
1751
|
+
}
|
|
1752
|
+
return { count, hardStopTripped };
|
|
1753
|
+
}
|
|
1754
|
+
function appendLearning(lesson, goalId) {
|
|
1755
|
+
ensureStateDir();
|
|
1756
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1757
|
+
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
1758
|
+
if (!existsSync2(LEARNINGS_PATH)) {
|
|
1759
|
+
writeFileSync(
|
|
1760
|
+
LEARNINGS_PATH,
|
|
1761
|
+
`# Learnings
|
|
1762
|
+
|
|
1763
|
+
_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
|
|
1764
|
+
|
|
1765
|
+
${line}
|
|
1766
|
+
`,
|
|
1767
|
+
"utf-8"
|
|
1768
|
+
);
|
|
1769
|
+
} else {
|
|
1770
|
+
appendFileSync(LEARNINGS_PATH, `${line}
|
|
1771
|
+
`, "utf-8");
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function getRecentLearnings(limit = 3) {
|
|
1775
|
+
if (!existsSync2(LEARNINGS_PATH)) return [];
|
|
1776
|
+
const lines = readFileSync2(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
1777
|
+
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
1778
|
+
return entries.slice(-limit);
|
|
1779
|
+
}
|
|
1780
|
+
function getActiveBlockers(limit = 3) {
|
|
1781
|
+
if (!existsSync2(BLOCKERS_PATH)) return [];
|
|
1782
|
+
const lines = readFileSync2(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
|
|
1783
|
+
const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
|
|
1784
|
+
return entries.slice(-limit);
|
|
1785
|
+
}
|
|
1786
|
+
function writeHardStop(reason) {
|
|
1787
|
+
ensureVhkDir();
|
|
1788
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1789
|
+
writeFileSync(HARD_STOP_PATH, `${ts}
|
|
1790
|
+
${reason}
|
|
1791
|
+
`, "utf-8");
|
|
1792
|
+
}
|
|
1793
|
+
function isHardStopActive() {
|
|
1794
|
+
return existsSync2(HARD_STOP_PATH);
|
|
1795
|
+
}
|
|
1796
|
+
function readHardStopReason() {
|
|
1797
|
+
if (!existsSync2(HARD_STOP_PATH)) return null;
|
|
1798
|
+
try {
|
|
1799
|
+
return readFileSync2(HARD_STOP_PATH, "utf-8").trim();
|
|
1800
|
+
} catch {
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function clearHardStop() {
|
|
1805
|
+
if (!existsSync2(HARD_STOP_PATH)) return false;
|
|
1806
|
+
rmSync(HARD_STOP_PATH, { force: true });
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/lib/hard-stop-guard.ts
|
|
1811
|
+
function ensureNotHardStopped(action) {
|
|
1812
|
+
if (!isHardStopActive()) return true;
|
|
1813
|
+
console.error(chalk5.red.bold(`
|
|
1814
|
+
\u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
|
|
1815
|
+
const reason = readHardStopReason();
|
|
1816
|
+
if (reason) console.error(chalk5.dim(` \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}`));
|
|
1817
|
+
console.error(chalk5.dim(" \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)"));
|
|
1818
|
+
process.exitCode = 1;
|
|
1819
|
+
return false;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1414
1822
|
// src/commands/recap.ts
|
|
1415
1823
|
async function recap(options = {}) {
|
|
1416
|
-
|
|
1824
|
+
if (!ensureNotHardStopped("recap")) return;
|
|
1825
|
+
console.log(chalk6.bold(`
|
|
1417
1826
|
${ko.recap.title}
|
|
1418
1827
|
`));
|
|
1419
1828
|
if (!await isGitRepo()) {
|
|
1420
|
-
console.log(
|
|
1829
|
+
console.log(chalk6.red(ko.recap.noRepo));
|
|
1421
1830
|
return;
|
|
1422
1831
|
}
|
|
1423
1832
|
if (!await hasAnyCommits()) {
|
|
1424
|
-
console.log(
|
|
1425
|
-
console.log(
|
|
1833
|
+
console.log(chalk6.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
|
|
1834
|
+
console.log(chalk6.gray(" \uD30C\uC77C\uC744 \uCD94\uAC00\uD558\uACE0 `vhk save` \uB610\uB294 `git commit`\uC73C\uB85C \uCCAB \uCEE4\uBC0B\uC744 \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
1426
1835
|
return;
|
|
1427
1836
|
}
|
|
1428
1837
|
printSecurityWarnings();
|
|
1429
|
-
console.log(
|
|
1838
|
+
console.log(chalk6.dim(`${ko.recap.analyzing}
|
|
1430
1839
|
`));
|
|
1431
|
-
const since = options.since || (
|
|
1840
|
+
const since = options.since || localDate();
|
|
1432
1841
|
const diff2 = await getSessionDiff(since);
|
|
1433
1842
|
const commits = await getRecentCommits(10, since);
|
|
1434
1843
|
if (diff2.filesChanged === 0 && commits.length === 0) {
|
|
1435
|
-
console.log(
|
|
1844
|
+
console.log(chalk6.yellow(ko.recap.noChanges));
|
|
1436
1845
|
return;
|
|
1437
1846
|
}
|
|
1438
|
-
console.log(
|
|
1439
|
-
console.log(` \uD30C\uC77C: ${
|
|
1440
|
-
console.log(` \uCD94\uAC00: ${
|
|
1847
|
+
console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1848
|
+
console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1849
|
+
console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
|
|
1441
1850
|
if (diff2.files.length > 0) {
|
|
1442
|
-
console.log(
|
|
1851
|
+
console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
1443
1852
|
diff2.files.slice(0, 15).forEach((f) => {
|
|
1444
|
-
const icon = f.status === "new" ?
|
|
1853
|
+
const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
|
|
1445
1854
|
console.log(` ${icon} ${f.file}`);
|
|
1446
1855
|
});
|
|
1447
1856
|
if (diff2.files.length > 15) {
|
|
1448
|
-
console.log(
|
|
1857
|
+
console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
|
|
1449
1858
|
}
|
|
1450
1859
|
}
|
|
1451
1860
|
if (commits.length > 0) {
|
|
1452
|
-
console.log(
|
|
1861
|
+
console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
1453
1862
|
commits.slice(0, 5).forEach((c) => {
|
|
1454
|
-
console.log(
|
|
1863
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1455
1864
|
});
|
|
1456
1865
|
}
|
|
1457
1866
|
console.log("");
|
|
1867
|
+
if (!ensureInteractive("\uD68C\uACE0 \uC785\uB825\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
|
|
1458
1868
|
const answers = await inquirer3.prompt([
|
|
1459
1869
|
{
|
|
1460
1870
|
type: "input",
|
|
@@ -1479,7 +1889,7 @@ ${ko.recap.title}
|
|
|
1479
1889
|
default: "\uC5C6\uC74C"
|
|
1480
1890
|
}
|
|
1481
1891
|
]);
|
|
1482
|
-
const today = (
|
|
1892
|
+
const today = localDate();
|
|
1483
1893
|
const logDir = path5.join(process.cwd(), "docs", "log");
|
|
1484
1894
|
if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true });
|
|
1485
1895
|
const existing = fs4.readdirSync(logDir).filter((f) => f.startsWith(today));
|
|
@@ -1519,11 +1929,11 @@ ${ko.recap.title}
|
|
|
1519
1929
|
fs4.writeFileSync(filePath, content, "utf-8");
|
|
1520
1930
|
const adrCandidates = detectAdrCandidates(diff2);
|
|
1521
1931
|
if (adrCandidates.length > 0) {
|
|
1522
|
-
console.log(
|
|
1932
|
+
console.log(chalk6.cyan.bold(`
|
|
1523
1933
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
1524
1934
|
for (const candidate of adrCandidates) {
|
|
1525
|
-
console.log(
|
|
1526
|
-
candidate.files.forEach((f) => console.log(
|
|
1935
|
+
console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1936
|
+
candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
|
|
1527
1937
|
}
|
|
1528
1938
|
const { createAdr } = await inquirer3.prompt([{
|
|
1529
1939
|
type: "confirm",
|
|
@@ -1553,17 +1963,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1553
1963
|
adrAnswers.decision,
|
|
1554
1964
|
adrAnswers.consequences
|
|
1555
1965
|
);
|
|
1556
|
-
console.log(
|
|
1966
|
+
console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
|
|
1557
1967
|
}
|
|
1558
1968
|
}
|
|
1559
1969
|
}
|
|
1560
1970
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1561
1971
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1562
1972
|
if (troubleCommits.length > 0) {
|
|
1563
|
-
console.log(
|
|
1973
|
+
console.log(chalk6.yellow.bold(`
|
|
1564
1974
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1565
1975
|
troubleCommits.forEach((c) => {
|
|
1566
|
-
console.log(
|
|
1976
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1567
1977
|
});
|
|
1568
1978
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1569
1979
|
type: "confirm",
|
|
@@ -1614,12 +2024,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1614
2024
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1615
2025
|
].join("\n");
|
|
1616
2026
|
fs4.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1617
|
-
console.log(
|
|
2027
|
+
console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
|
|
1618
2028
|
}
|
|
1619
2029
|
}
|
|
1620
|
-
console.log(
|
|
2030
|
+
console.log(chalk6.green.bold(`
|
|
1621
2031
|
${ko.recap.done}`));
|
|
1622
|
-
console.log(
|
|
2032
|
+
console.log(chalk6.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
|
|
1623
2033
|
const claudeMdPath = path5.join(process.cwd(), "CLAUDE.md");
|
|
1624
2034
|
if (fs4.existsSync(claudeMdPath)) {
|
|
1625
2035
|
const { updateClaude } = await inquirer3.prompt([{
|
|
@@ -1639,7 +2049,7 @@ ${ko.recap.done}`));
|
|
|
1639
2049
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
1640
2050
|
);
|
|
1641
2051
|
fs4.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
1642
|
-
console.log(
|
|
2052
|
+
console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
1643
2053
|
}
|
|
1644
2054
|
}
|
|
1645
2055
|
const gitSaveCmd = process.platform === "win32" ? 'git add .; git commit -m "recap: \uC138\uC158 \uAE30\uB85D"' : 'git add . && git commit -m "recap: \uC138\uC158 \uAE30\uB85D"';
|
|
@@ -1651,38 +2061,249 @@ ${ko.recap.done}`));
|
|
|
1651
2061
|
}
|
|
1652
2062
|
|
|
1653
2063
|
// src/commands/sync.ts
|
|
1654
|
-
import
|
|
2064
|
+
import chalk7 from "chalk";
|
|
2065
|
+
import fs7 from "fs";
|
|
2066
|
+
import path8 from "path";
|
|
2067
|
+
import inquirer4 from "inquirer";
|
|
2068
|
+
|
|
2069
|
+
// src/lib/drift.ts
|
|
1655
2070
|
import fs5 from "fs";
|
|
1656
2071
|
import path6 from "path";
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2072
|
+
|
|
2073
|
+
// src/lib/git-repo.ts
|
|
2074
|
+
import { execFileSync } from "child_process";
|
|
2075
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
2076
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2077
|
+
encoding: "utf-8",
|
|
2078
|
+
cwd,
|
|
2079
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2080
|
+
}).trim();
|
|
2081
|
+
}
|
|
2082
|
+
function gitOut(args, cwd) {
|
|
2083
|
+
return execFileSync("git", args, {
|
|
2084
|
+
encoding: "utf-8",
|
|
2085
|
+
cwd,
|
|
2086
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
function gitRun(args, cwd) {
|
|
2090
|
+
execFileSync("git", args, { stdio: "pipe", cwd });
|
|
2091
|
+
}
|
|
2092
|
+
function getExecErrorMessage(err) {
|
|
2093
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
2094
|
+
const stderr = err.stderr;
|
|
2095
|
+
if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
|
|
2096
|
+
if (typeof stderr === "string") return stderr.trim();
|
|
1674
2097
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
2098
|
+
return err instanceof Error ? err.message : String(err);
|
|
2099
|
+
}
|
|
2100
|
+
function hasGitRemote(cwd) {
|
|
2101
|
+
try {
|
|
2102
|
+
return gitOut(["remote"], cwd).trim().length > 0;
|
|
2103
|
+
} catch {
|
|
2104
|
+
return false;
|
|
1677
2105
|
}
|
|
1678
|
-
return sections;
|
|
1679
2106
|
}
|
|
1680
|
-
function
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2107
|
+
function countLocalCommits(cwd) {
|
|
2108
|
+
try {
|
|
2109
|
+
const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
|
|
2110
|
+
return parseInt(out, 10) || 0;
|
|
2111
|
+
} catch {
|
|
2112
|
+
return 0;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// src/lib/drift.ts
|
|
2117
|
+
function normalizeForCompare(s) {
|
|
2118
|
+
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
|
|
2119
|
+
}
|
|
2120
|
+
function checkRuleDrift(rootDir) {
|
|
2121
|
+
const rulesPath = path6.join(rootDir, "RULES.md");
|
|
2122
|
+
if (!fs5.existsSync(rulesPath)) return { checked: false, results: [] };
|
|
2123
|
+
const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
|
|
2124
|
+
const sections = parseRulesMd(rulesContent);
|
|
2125
|
+
const projectName = deriveProjectName(rulesContent);
|
|
2126
|
+
const results = [];
|
|
2127
|
+
for (const target of SYNC_TARGETS) {
|
|
2128
|
+
const fullPath = path6.join(rootDir, target.path);
|
|
2129
|
+
if (!fs5.existsSync(fullPath)) {
|
|
2130
|
+
results.push({ path: target.path, status: "missing" });
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
const expected = normalizeForCompare(target.generate(sections, projectName));
|
|
2134
|
+
const actual = normalizeForCompare(fs5.readFileSync(fullPath, "utf-8"));
|
|
2135
|
+
results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
|
|
2136
|
+
}
|
|
2137
|
+
return { checked: true, results };
|
|
2138
|
+
}
|
|
2139
|
+
var CONTEXT_GIT_MARKER = "vhk-context-git";
|
|
2140
|
+
var CONTEXT_PATH = ".vhk/context.md";
|
|
2141
|
+
function extractContextSha(content) {
|
|
2142
|
+
const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
|
|
2143
|
+
return m ? m[1] : null;
|
|
2144
|
+
}
|
|
2145
|
+
var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
|
|
2146
|
+
function contextSourcesChanged(generatedSha, rootDir) {
|
|
2147
|
+
const content = gitOut(
|
|
2148
|
+
["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
|
|
2149
|
+
rootDir
|
|
2150
|
+
).trim();
|
|
2151
|
+
if (content) return true;
|
|
2152
|
+
const structural = gitOut(
|
|
2153
|
+
["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
|
|
2154
|
+
rootDir
|
|
2155
|
+
).trim();
|
|
2156
|
+
return structural.length > 0;
|
|
2157
|
+
}
|
|
2158
|
+
function checkContextDrift(rootDir) {
|
|
2159
|
+
const ctxPath = path6.join(rootDir, CONTEXT_PATH);
|
|
2160
|
+
if (!fs5.existsSync(ctxPath)) return { checked: false, stale: false };
|
|
2161
|
+
const generatedSha = extractContextSha(fs5.readFileSync(ctxPath, "utf-8"));
|
|
2162
|
+
if (!generatedSha) return { checked: false, stale: false };
|
|
2163
|
+
let currentSha;
|
|
2164
|
+
try {
|
|
2165
|
+
currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
|
|
2166
|
+
} catch {
|
|
2167
|
+
return { checked: false, stale: false };
|
|
2168
|
+
}
|
|
2169
|
+
if (!currentSha) return { checked: false, stale: false };
|
|
2170
|
+
if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
|
|
2171
|
+
return { checked: true, stale: false, generatedSha, currentSha };
|
|
2172
|
+
}
|
|
2173
|
+
let stale;
|
|
2174
|
+
try {
|
|
2175
|
+
stale = contextSourcesChanged(generatedSha, rootDir);
|
|
2176
|
+
} catch {
|
|
2177
|
+
return { checked: false, stale: false };
|
|
2178
|
+
}
|
|
2179
|
+
return { checked: true, stale, generatedSha, currentSha };
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/lib/backup.ts
|
|
2183
|
+
import fs6 from "fs";
|
|
2184
|
+
import path7 from "path";
|
|
2185
|
+
var BACKUPS_REL = path7.join(".vhk", "backups");
|
|
2186
|
+
var VHK_GITIGNORE_REL = path7.join(".vhk", ".gitignore");
|
|
2187
|
+
function fsSafeStamp(d) {
|
|
2188
|
+
return d.toISOString().replace(/[:.]/g, "-");
|
|
2189
|
+
}
|
|
2190
|
+
function ensureVhkIgnored(rootDir, ...entries) {
|
|
2191
|
+
const giPath = path7.join(rootDir, VHK_GITIGNORE_REL);
|
|
2192
|
+
fs6.mkdirSync(path7.dirname(giPath), { recursive: true });
|
|
2193
|
+
let content = fs6.existsSync(giPath) ? fs6.readFileSync(giPath, "utf-8") : "";
|
|
2194
|
+
const present = new Set(content.split("\n").map((l) => l.trim().replace(/\/$/, "")));
|
|
2195
|
+
const missing = entries.filter((e) => !present.has(e.trim().replace(/\/$/, "")));
|
|
2196
|
+
if (missing.length === 0) return;
|
|
2197
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
2198
|
+
content += missing.join("\n") + "\n";
|
|
2199
|
+
fs6.writeFileSync(giPath, content, "utf-8");
|
|
2200
|
+
}
|
|
2201
|
+
function walkRelFiles(baseDir, cur = baseDir) {
|
|
2202
|
+
const out = [];
|
|
2203
|
+
for (const entry of fs6.readdirSync(cur)) {
|
|
2204
|
+
const full = path7.join(cur, entry);
|
|
2205
|
+
if (fs6.statSync(full).isDirectory()) {
|
|
2206
|
+
out.push(...walkRelFiles(baseDir, full));
|
|
2207
|
+
} else {
|
|
2208
|
+
out.push(path7.relative(baseDir, full).split(path7.sep).join("/"));
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return out;
|
|
2212
|
+
}
|
|
2213
|
+
function saveBackup(files, rootDir, stamp) {
|
|
2214
|
+
const baseId = stamp ?? fsSafeStamp(/* @__PURE__ */ new Date());
|
|
2215
|
+
let id = baseId;
|
|
2216
|
+
let n = 1;
|
|
2217
|
+
while (fs6.existsSync(path7.join(rootDir, BACKUPS_REL, id))) {
|
|
2218
|
+
id = `${baseId}-${String(n++).padStart(3, "0")}`;
|
|
2219
|
+
}
|
|
2220
|
+
const backupDir = path7.join(rootDir, BACKUPS_REL, id);
|
|
2221
|
+
const saved = [];
|
|
2222
|
+
for (const rel of files) {
|
|
2223
|
+
const src = path7.join(rootDir, rel);
|
|
2224
|
+
if (!fs6.existsSync(src)) continue;
|
|
2225
|
+
const dest = path7.join(backupDir, rel);
|
|
2226
|
+
fs6.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
2227
|
+
fs6.copyFileSync(src, dest);
|
|
2228
|
+
saved.push(rel);
|
|
2229
|
+
}
|
|
2230
|
+
ensureVhkIgnored(rootDir, "backups/");
|
|
2231
|
+
return { id, dir: backupDir, files: saved };
|
|
2232
|
+
}
|
|
2233
|
+
function backupOrderKey(id) {
|
|
2234
|
+
const m = /^(.*Z)(?:-(\d+))?$/.exec(id);
|
|
2235
|
+
return m ? [m[1], m[2] ? parseInt(m[2], 10) : 0] : [id, 0];
|
|
2236
|
+
}
|
|
2237
|
+
function listBackups(rootDir) {
|
|
2238
|
+
const root = path7.join(rootDir, BACKUPS_REL);
|
|
2239
|
+
if (!fs6.existsSync(root)) return [];
|
|
2240
|
+
return fs6.readdirSync(root).filter((e) => fs6.statSync(path7.join(root, e)).isDirectory()).sort((a, b) => {
|
|
2241
|
+
const [ba, na] = backupOrderKey(a);
|
|
2242
|
+
const [bb, nb] = backupOrderKey(b);
|
|
2243
|
+
if (ba !== bb) return ba < bb ? 1 : -1;
|
|
2244
|
+
return nb - na;
|
|
2245
|
+
}).map((id) => {
|
|
2246
|
+
const dir = path7.join(root, id);
|
|
2247
|
+
return { id, dir, files: walkRelFiles(dir) };
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
function restoreBackup(id, rootDir) {
|
|
2251
|
+
const backupDir = path7.join(rootDir, BACKUPS_REL, id);
|
|
2252
|
+
if (!fs6.existsSync(backupDir)) {
|
|
2253
|
+
throw new Error(`\uBC31\uC5C5 \uC5C6\uC74C: ${id}`);
|
|
2254
|
+
}
|
|
2255
|
+
const rels = walkRelFiles(backupDir);
|
|
2256
|
+
for (const rel of rels) {
|
|
2257
|
+
const src = path7.join(backupDir, rel);
|
|
2258
|
+
const dest = path7.join(rootDir, rel);
|
|
2259
|
+
fs6.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
2260
|
+
fs6.copyFileSync(src, dest);
|
|
2261
|
+
}
|
|
2262
|
+
return rels;
|
|
2263
|
+
}
|
|
2264
|
+
function pruneBackups(keepN, rootDir) {
|
|
2265
|
+
const all = listBackups(rootDir);
|
|
2266
|
+
const toDelete = all.slice(Math.max(0, keepN));
|
|
2267
|
+
for (const b of toDelete) {
|
|
2268
|
+
fs6.rmSync(b.dir, { recursive: true, force: true });
|
|
2269
|
+
}
|
|
2270
|
+
return toDelete.map((b) => b.id);
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/commands/sync.ts
|
|
2274
|
+
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
2275
|
+
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
2276
|
+
function findUnmappedSections(sections) {
|
|
2277
|
+
const allKeys = [...CURSORRULES_KEYS, ...CLAUDE_MD_KEYS];
|
|
2278
|
+
return sections.filter((s) => s.title !== PREAMBLE_TITLE && !allKeys.some((k) => s.title.includes(k))).map((s) => s.title);
|
|
2279
|
+
}
|
|
2280
|
+
function parseRulesMd(content) {
|
|
2281
|
+
const sections = [];
|
|
2282
|
+
const lines = content.split("\n");
|
|
2283
|
+
let currentTitle = "";
|
|
2284
|
+
let currentContent = [];
|
|
2285
|
+
for (const line of lines) {
|
|
2286
|
+
if (line.startsWith("## ")) {
|
|
2287
|
+
if (currentTitle) {
|
|
2288
|
+
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
2289
|
+
}
|
|
2290
|
+
currentTitle = line.replace("## ", "").trim();
|
|
2291
|
+
currentContent = [];
|
|
2292
|
+
} else {
|
|
2293
|
+
currentContent.push(line);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
if (currentTitle) {
|
|
2297
|
+
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
2298
|
+
}
|
|
2299
|
+
return sections;
|
|
2300
|
+
}
|
|
2301
|
+
function buildCodingDoc(headerTitle, sections, projectName) {
|
|
2302
|
+
const codingSections = sections.filter(
|
|
2303
|
+
(s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
|
|
2304
|
+
);
|
|
2305
|
+
const lines = [
|
|
2306
|
+
`# ${projectName} \u2014 ${headerTitle}`,
|
|
1686
2307
|
"",
|
|
1687
2308
|
"> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
|
|
1688
2309
|
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
@@ -1731,21 +2352,51 @@ function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
|
|
|
1731
2352
|
function toAntigravityRules(sections, projectName) {
|
|
1732
2353
|
return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
|
|
1733
2354
|
}
|
|
2355
|
+
var CLAUDE_AUTOGEN_BANNER = "> \u26A1 \uC544\uB798 \uADDC\uCE59 \uC139\uC158\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.";
|
|
1734
2356
|
function toClaudeMd(sections, existing) {
|
|
1735
2357
|
const recordSections = sections.filter(
|
|
1736
2358
|
(s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
|
|
1737
2359
|
);
|
|
1738
|
-
const
|
|
1739
|
-
const
|
|
1740
|
-
const
|
|
2360
|
+
const cleaned = existing.split("\n").filter((line) => line.trim() !== CLAUDE_AUTOGEN_BANNER).join("\n");
|
|
2361
|
+
const statusMatch = cleaned.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
|
|
2362
|
+
const statusSection = statusMatch ? statusMatch[0].trimEnd() : "";
|
|
2363
|
+
const header = cleaned.split("## ")[0].trim();
|
|
1741
2364
|
const lines = [
|
|
1742
2365
|
header,
|
|
1743
2366
|
"",
|
|
1744
2367
|
statusSection,
|
|
1745
2368
|
"",
|
|
1746
|
-
|
|
2369
|
+
CLAUDE_AUTOGEN_BANNER,
|
|
2370
|
+
""
|
|
2371
|
+
];
|
|
2372
|
+
for (const section of recordSections) {
|
|
2373
|
+
lines.push(`## ${section.title}`);
|
|
2374
|
+
lines.push(section.content);
|
|
2375
|
+
lines.push("");
|
|
2376
|
+
}
|
|
2377
|
+
return lines.join("\n");
|
|
2378
|
+
}
|
|
2379
|
+
function toAgentsMd(sections, projectName) {
|
|
2380
|
+
const codingSections = sections.filter((s) => CURSORRULES_KEYS.some((k) => s.title.includes(k)));
|
|
2381
|
+
const recordSections = sections.filter((s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k)));
|
|
2382
|
+
const lines = [
|
|
2383
|
+
`# ${projectName} \u2014 AGENTS.md (\uC5D0\uC774\uC804\uD2B8 \uC791\uB3D9 \uADDC\uC57D)`,
|
|
2384
|
+
"",
|
|
2385
|
+
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
2386
|
+
"> \uBE60\uB978 \uC2DC\uC791(\uD1A0\uD070 \uC808\uAC10): `docs/context/agent-compact.md` \uB97C \uBA3C\uC800 \uC77D\uC73C\uC138\uC694.",
|
|
2387
|
+
"",
|
|
2388
|
+
"## Loop Protocol",
|
|
2389
|
+
"- \uB8E8\uD504: `context \u2192 goal next \u2192 \uC791\uC5C5 \u2192 goal check \u2192 goal done`",
|
|
2390
|
+
"- \uC791\uC5C5 \uC2DC\uC791 \uC2DC `.vhk/HARD_STOP` \uD655\uC778 \u2014 \uC788\uC73C\uBA74 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC989\uC2DC \uC911\uB2E8.",
|
|
2391
|
+
"- active goal \uB9CC \uC791\uC5C5. `docs/state`(next-task/blockers/learnings)\uB294 SoT, append-only.",
|
|
2392
|
+
"- \uAC8C\uC774\uD2B8(tsc / test:run / build) \uD1B5\uACFC\uD574\uC57C\uB9CC `vhk goal done`.",
|
|
1747
2393
|
""
|
|
1748
2394
|
];
|
|
2395
|
+
for (const section of codingSections) {
|
|
2396
|
+
lines.push(`## ${section.title}`);
|
|
2397
|
+
lines.push(section.content);
|
|
2398
|
+
lines.push("");
|
|
2399
|
+
}
|
|
1749
2400
|
for (const section of recordSections) {
|
|
1750
2401
|
lines.push(`## ${section.title}`);
|
|
1751
2402
|
lines.push(section.content);
|
|
@@ -1761,56 +2412,173 @@ var SYNC_TARGETS = [
|
|
|
1761
2412
|
{ path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
|
|
1762
2413
|
{ path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
|
|
1763
2414
|
{ path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
|
|
1764
|
-
{ path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone }
|
|
2415
|
+
{ path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone },
|
|
2416
|
+
// AGENTS.md — 6번째 타겟. 항목 1개 추가로 sync·드리프트·백업 가드가 자동 반영된다.
|
|
2417
|
+
{ path: "AGENTS.md", generate: toAgentsMd, doneMessage: ko.sync.agentsDone }
|
|
1765
2418
|
];
|
|
1766
|
-
|
|
1767
|
-
|
|
2419
|
+
var BACKUP_KEEP = 10;
|
|
2420
|
+
var SYNCED_MARKER_REL = path8.join(".vhk", ".synced");
|
|
2421
|
+
function buildSyncPlan(rootDir, sections, projectName) {
|
|
2422
|
+
const plan = [];
|
|
2423
|
+
for (const target of SYNC_TARGETS) {
|
|
2424
|
+
const fullPath = path8.join(rootDir, target.path);
|
|
2425
|
+
const exists = fs7.existsSync(fullPath);
|
|
2426
|
+
const newContent = target.generate(sections, projectName);
|
|
2427
|
+
const drift = exists ? normalizeForCompare(fs7.readFileSync(fullPath, "utf-8")) !== normalizeForCompare(newContent) : false;
|
|
2428
|
+
plan.push({ path: target.path, newContent, doneMessage: target.doneMessage, exists, drift });
|
|
2429
|
+
}
|
|
2430
|
+
const claudePath = path8.join(rootDir, "CLAUDE.md");
|
|
2431
|
+
const claudeExists = fs7.existsSync(claudePath);
|
|
2432
|
+
const existingClaude = claudeExists ? fs7.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
2433
|
+
|
|
2434
|
+
## \uD604\uC7AC \uC0C1\uD0DC
|
|
2435
|
+
- **Phase:** **FILL**
|
|
2436
|
+
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
2437
|
+
- **\uB2E4\uC74C \uC561\uC158:** **FILL**
|
|
2438
|
+
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${localDate()}`;
|
|
2439
|
+
const claudeNew = toClaudeMd(sections, existingClaude);
|
|
2440
|
+
const claudeDrift = claudeExists ? normalizeForCompare(existingClaude) !== normalizeForCompare(claudeNew) : false;
|
|
2441
|
+
plan.push({
|
|
2442
|
+
path: "CLAUDE.md",
|
|
2443
|
+
newContent: claudeNew,
|
|
2444
|
+
doneMessage: ko.sync.claudeDone,
|
|
2445
|
+
exists: claudeExists,
|
|
2446
|
+
drift: claudeDrift
|
|
2447
|
+
});
|
|
2448
|
+
return plan;
|
|
2449
|
+
}
|
|
2450
|
+
async function syncCore(rootDir, opts, confirmOverwrite) {
|
|
2451
|
+
const rulesContent = fs7.readFileSync(path8.join(rootDir, "RULES.md"), "utf-8");
|
|
2452
|
+
const sections = parseRulesMd(rulesContent);
|
|
2453
|
+
const projectName = deriveProjectName(rulesContent);
|
|
2454
|
+
const plan = buildSyncPlan(rootDir, sections, projectName);
|
|
2455
|
+
const firstSync = !fs7.existsSync(path8.join(rootDir, SYNCED_MARKER_REL));
|
|
2456
|
+
const unmapped = findUnmappedSections(sections);
|
|
2457
|
+
if (opts.dryRun) {
|
|
2458
|
+
return {
|
|
2459
|
+
dryRun: true,
|
|
2460
|
+
firstSync,
|
|
2461
|
+
backupId: null,
|
|
2462
|
+
backedUp: [],
|
|
2463
|
+
written: [],
|
|
2464
|
+
skipped: [],
|
|
2465
|
+
truncated: [],
|
|
2466
|
+
plan,
|
|
2467
|
+
unmapped
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
const toBackup = plan.filter((p) => p.exists && (p.drift || firstSync)).map((p) => p.path);
|
|
2471
|
+
let backupId = null;
|
|
2472
|
+
let backedUp = [];
|
|
2473
|
+
if (toBackup.length) {
|
|
2474
|
+
const info = saveBackup(toBackup, rootDir);
|
|
2475
|
+
pruneBackups(BACKUP_KEEP, rootDir);
|
|
2476
|
+
backupId = info.id;
|
|
2477
|
+
backedUp = info.files;
|
|
2478
|
+
}
|
|
2479
|
+
const drifted = plan.filter((p) => p.drift);
|
|
2480
|
+
const overwriteDrift = drifted.length ? await confirmOverwrite(drifted) : true;
|
|
2481
|
+
const written = [];
|
|
2482
|
+
const skipped = [];
|
|
2483
|
+
const truncated = [];
|
|
2484
|
+
for (const item of plan) {
|
|
2485
|
+
if (item.drift && !overwriteDrift) {
|
|
2486
|
+
skipped.push(item.path);
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const fullPath = path8.join(rootDir, item.path);
|
|
2490
|
+
fs7.mkdirSync(path8.dirname(fullPath), { recursive: true });
|
|
2491
|
+
fs7.writeFileSync(fullPath, item.newContent, "utf-8");
|
|
2492
|
+
written.push(item.path);
|
|
2493
|
+
if (item.newContent.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
|
|
2494
|
+
truncated.push(item.path);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
fs7.mkdirSync(path8.join(rootDir, ".vhk"), { recursive: true });
|
|
2498
|
+
fs7.writeFileSync(path8.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2499
|
+
ensureVhkIgnored(rootDir, ".synced");
|
|
2500
|
+
return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped };
|
|
2501
|
+
}
|
|
2502
|
+
async function sync(opts = {}) {
|
|
2503
|
+
console.log(chalk7.bold(`
|
|
1768
2504
|
${ko.sync.title}
|
|
1769
2505
|
`));
|
|
1770
2506
|
const cwd = process.cwd();
|
|
1771
|
-
const rulesPath =
|
|
1772
|
-
if (!
|
|
1773
|
-
console.log(
|
|
1774
|
-
console.log(
|
|
1775
|
-
console.log(
|
|
2507
|
+
const rulesPath = path8.join(cwd, "RULES.md");
|
|
2508
|
+
if (!fs7.existsSync(rulesPath)) {
|
|
2509
|
+
console.log(chalk7.yellow(ko.sync.noRules));
|
|
2510
|
+
console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
2511
|
+
console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
1776
2512
|
console.log("");
|
|
1777
|
-
console.log(
|
|
1778
|
-
console.log(
|
|
1779
|
-
console.log(
|
|
1780
|
-
console.log(
|
|
1781
|
-
console.log(
|
|
1782
|
-
console.log(
|
|
2513
|
+
console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
2514
|
+
console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
2515
|
+
console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
2516
|
+
console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
2517
|
+
console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
2518
|
+
console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
1783
2519
|
return;
|
|
1784
2520
|
}
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
const
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2521
|
+
const sections = parseRulesMd(fs7.readFileSync(rulesPath, "utf-8"));
|
|
2522
|
+
console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
2523
|
+
const interactive = !!process.stdout.isTTY && !opts.yes;
|
|
2524
|
+
const confirmOverwrite = async (drifted) => {
|
|
2525
|
+
if (!interactive) return true;
|
|
2526
|
+
for (const d of drifted) console.log(chalk7.yellow(` ${ko.sync.driftWarn(d.path)}`));
|
|
2527
|
+
const { confirm } = await inquirer4.prompt([
|
|
2528
|
+
{
|
|
2529
|
+
type: "confirm",
|
|
2530
|
+
name: "confirm",
|
|
2531
|
+
message: ko.sync.driftConfirm(drifted.length),
|
|
2532
|
+
default: false
|
|
2533
|
+
}
|
|
2534
|
+
]);
|
|
2535
|
+
return confirm;
|
|
2536
|
+
};
|
|
2537
|
+
const result = await syncCore(cwd, opts, confirmOverwrite);
|
|
2538
|
+
if (result.unmapped.length) {
|
|
2539
|
+
console.error(
|
|
2540
|
+
chalk7.yellow(
|
|
2541
|
+
` \u26A0\uFE0F ${result.unmapped.length}\uAC1C \uC139\uC158\uC774 \uC5B4\uB290 \uD0C0\uAE43\uC5D0\uB3C4 \uB9E4\uD551 \uC548 \uB3FC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC81C\uC678\uB428: ${result.unmapped.join(", ")}
|
|
2542
|
+
(\uCF54\uB529 \uADDC\uCE59/\uAE30\uC220 \uC2A4\uD0DD/\uCEE4\uBC0B/\uAE30\uB85D \uB4F1 \uD45C\uC900 \uC81C\uBAA9\uC744 \uC4F0\uAC70\uB098, \uC774 \uC139\uC158\uC740 RULES.md \uC5D0\uB9CC \uBCF4\uC874\uB429\uB2C8\uB2E4.)`
|
|
2543
|
+
)
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
if (result.dryRun) {
|
|
2547
|
+
console.log(chalk7.cyan(`
|
|
2548
|
+
${ko.sync.dryRunHeader}`));
|
|
2549
|
+
for (const item of result.plan) {
|
|
2550
|
+
console.log(ko.sync.dryRunWouldWrite(item.path, item.exists && item.drift));
|
|
2551
|
+
}
|
|
2552
|
+
const wouldBackup = result.plan.filter((p) => p.exists && (p.drift || result.firstSync)).map((p) => p.path);
|
|
2553
|
+
if (wouldBackup.length) {
|
|
2554
|
+
console.log(chalk7.dim(`
|
|
2555
|
+
\uBC31\uC5C5 \uC608\uC815(${wouldBackup.length}): ${wouldBackup.join(", ")}`));
|
|
1797
2556
|
}
|
|
2557
|
+
return;
|
|
1798
2558
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
2559
|
+
if (result.backupId) {
|
|
2560
|
+
if (result.firstSync) console.log(chalk7.cyan(` ${ko.sync.firstSync}`));
|
|
2561
|
+
if (!process.stdout.isTTY) {
|
|
2562
|
+
console.log(chalk7.yellow(` ${ko.sync.nonTtyAuto(result.backedUp.length, result.backupId)}`));
|
|
2563
|
+
} else {
|
|
2564
|
+
console.log(chalk7.cyan(` ${ko.sync.backupSaved(result.backedUp.length, result.backupId)}`));
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
for (const p of result.written) {
|
|
2568
|
+
const item = result.plan.find((i) => i.path === p);
|
|
2569
|
+
if (item) console.log(chalk7.green(` ${item.doneMessage}`));
|
|
2570
|
+
}
|
|
2571
|
+
for (const _ of result.truncated) {
|
|
2572
|
+
console.log(chalk7.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
|
|
2573
|
+
}
|
|
2574
|
+
for (const p of result.skipped) {
|
|
2575
|
+
console.log(chalk7.gray(` ${ko.sync.skipped(p)}`));
|
|
2576
|
+
}
|
|
2577
|
+
console.log(chalk7.bold.green(`
|
|
1810
2578
|
${ko.sync.done}`));
|
|
1811
|
-
console.log(
|
|
1812
|
-
console.log(
|
|
1813
|
-
console.log(
|
|
2579
|
+
console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
|
|
2580
|
+
console.log(chalk7.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
2581
|
+
console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
1814
2582
|
printNextStep({
|
|
1815
2583
|
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
1816
2584
|
command: "vhk \uC810\uAC80",
|
|
@@ -1819,68 +2587,72 @@ ${ko.sync.done}`));
|
|
|
1819
2587
|
}
|
|
1820
2588
|
|
|
1821
2589
|
// src/commands/check.ts
|
|
1822
|
-
import
|
|
1823
|
-
import
|
|
1824
|
-
import
|
|
2590
|
+
import chalk9 from "chalk";
|
|
2591
|
+
import path10 from "path";
|
|
2592
|
+
import fs9 from "fs";
|
|
1825
2593
|
|
|
1826
2594
|
// src/lib/rules-parser.ts
|
|
1827
|
-
import
|
|
1828
|
-
import
|
|
2595
|
+
import fs8 from "fs";
|
|
2596
|
+
import path9 from "path";
|
|
1829
2597
|
function parseRules(rulesPath) {
|
|
1830
|
-
if (!
|
|
1831
|
-
const content =
|
|
2598
|
+
if (!fs8.existsSync(rulesPath)) return [];
|
|
2599
|
+
const content = fs8.readFileSync(rulesPath, "utf-8");
|
|
1832
2600
|
const lines = content.split("\n");
|
|
1833
2601
|
const rules = [];
|
|
1834
2602
|
let currentSection = "";
|
|
1835
|
-
let
|
|
1836
|
-
|
|
2603
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2604
|
+
const line = lines[i];
|
|
2605
|
+
const lineNo = i + 1;
|
|
1837
2606
|
if (line.startsWith("## ")) {
|
|
1838
2607
|
currentSection = line.replace("## ", "").trim();
|
|
1839
2608
|
continue;
|
|
1840
2609
|
}
|
|
1841
2610
|
const bulletMatch = line.match(/^[-*]\s+(.+)/);
|
|
1842
2611
|
if (!bulletMatch) continue;
|
|
2612
|
+
if (isMetaSection(currentSection)) continue;
|
|
1843
2613
|
const ruleText = bulletMatch[1];
|
|
1844
|
-
ruleIndex++;
|
|
1845
2614
|
if (/kebab[- ]?case/i.test(ruleText)) {
|
|
1846
|
-
rules.push(createNamingRule(
|
|
1847
|
-
`naming-${ruleIndex}`,
|
|
1848
|
-
currentSection,
|
|
1849
|
-
ruleText,
|
|
1850
|
-
"kebab-case"
|
|
1851
|
-
));
|
|
2615
|
+
rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "kebab-case"));
|
|
1852
2616
|
} else if (/camel[- ]?case/i.test(ruleText)) {
|
|
1853
|
-
rules.push(createNamingRule(
|
|
1854
|
-
`naming-${ruleIndex}`,
|
|
1855
|
-
currentSection,
|
|
1856
|
-
ruleText,
|
|
1857
|
-
"camelCase"
|
|
1858
|
-
));
|
|
2617
|
+
rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "camelCase"));
|
|
1859
2618
|
}
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
`structure
|
|
1864
|
-
currentSection,
|
|
1865
|
-
ruleText,
|
|
1866
|
-
pathMatch[1]
|
|
1867
|
-
));
|
|
1868
|
-
}
|
|
1869
|
-
if (/금지|사용하지|쓰지 마|하지 않는다|never use|do not use/i.test(ruleText)) {
|
|
1870
|
-
const backtickContent = ruleText.match(/`([^`]+)`/);
|
|
1871
|
-
if (backtickContent) {
|
|
1872
|
-
rules.push(createContentRule(
|
|
1873
|
-
`ban-${ruleIndex}`,
|
|
1874
|
-
currentSection,
|
|
1875
|
-
ruleText,
|
|
1876
|
-
backtickContent[1],
|
|
1877
|
-
"banned"
|
|
1878
|
-
));
|
|
2619
|
+
if (isStructureSection(currentSection)) {
|
|
2620
|
+
const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
|
|
2621
|
+
if (pathMatch) {
|
|
2622
|
+
rules.push(createStructureRule(`structure-L${lineNo}`, currentSection, ruleText, pathMatch[1]));
|
|
1879
2623
|
}
|
|
1880
2624
|
}
|
|
2625
|
+
const banToken = extractBanToken(ruleText);
|
|
2626
|
+
if (banToken) {
|
|
2627
|
+
rules.push(createContentRule(`ban-L${lineNo}`, currentSection, ruleText, banToken, "banned"));
|
|
2628
|
+
}
|
|
1881
2629
|
}
|
|
1882
2630
|
return rules;
|
|
1883
2631
|
}
|
|
2632
|
+
function isMetaSection(section) {
|
|
2633
|
+
const s = section.toLowerCase();
|
|
2634
|
+
return ["\uAE30\uB85D", "\uB85C\uADF8", "adr", "\uD2B8\uB7EC\uBE14", "til", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8", "\uC9C0\uCE68", "\uC774\uC288", "\uC6B4\uC601", "\uCEE4\uBC0B"].some(
|
|
2635
|
+
(k) => s.includes(k)
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
function isStructureSection(section) {
|
|
2639
|
+
const s = section.toLowerCase();
|
|
2640
|
+
return ["\uC544\uD0A4\uD14D\uCC98", "\uAD6C\uC870", "\uB514\uB809\uD130\uB9AC", "\uD3F4\uB354", "architecture", "structure"].some((k) => s.includes(k));
|
|
2641
|
+
}
|
|
2642
|
+
function extractBanToken(ruleText) {
|
|
2643
|
+
const banKw = ruleText.search(/금지|사용하지|쓰지\s*마|하지\s*않는다|never use|do not use/i);
|
|
2644
|
+
if (banKw < 0) return null;
|
|
2645
|
+
let token = null;
|
|
2646
|
+
const before = [...ruleText.slice(0, banKw).matchAll(/`([^`]+)`/g)].pop();
|
|
2647
|
+
if (before) token = before[1];
|
|
2648
|
+
else {
|
|
2649
|
+
const after = ruleText.slice(banKw).match(/^(?:금지|사용하지|쓰지\s*마|do not use|never use)\s*[::]?\s*`([^`]+)`/i);
|
|
2650
|
+
if (after) token = after[1];
|
|
2651
|
+
}
|
|
2652
|
+
if (!token) return null;
|
|
2653
|
+
if (/:\/\/|www\.|\.(com|io|dev|net|org|app)\b|\//.test(token)) return null;
|
|
2654
|
+
return token;
|
|
2655
|
+
}
|
|
1884
2656
|
function createNamingRule(id, section, desc, convention) {
|
|
1885
2657
|
return {
|
|
1886
2658
|
id,
|
|
@@ -1889,17 +2661,17 @@ function createNamingRule(id, section, desc, convention) {
|
|
|
1889
2661
|
description: desc,
|
|
1890
2662
|
check: (cwd) => {
|
|
1891
2663
|
const violations = [];
|
|
1892
|
-
const srcDir =
|
|
1893
|
-
if (!
|
|
2664
|
+
const srcDir = path9.join(cwd, "src");
|
|
2665
|
+
if (!fs8.existsSync(srcDir)) return violations;
|
|
1894
2666
|
walkFiles(srcDir, (filePath) => {
|
|
1895
|
-
const name =
|
|
2667
|
+
const name = path9.basename(filePath, path9.extname(filePath));
|
|
1896
2668
|
if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
1897
2669
|
if (!["index", "vite.config", "tsconfig"].includes(name)) {
|
|
1898
2670
|
violations.push({
|
|
1899
2671
|
ruleId: id,
|
|
1900
2672
|
severity: "warning",
|
|
1901
2673
|
message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
|
|
1902
|
-
file:
|
|
2674
|
+
file: path9.relative(cwd, filePath)
|
|
1903
2675
|
});
|
|
1904
2676
|
}
|
|
1905
2677
|
}
|
|
@@ -1915,8 +2687,8 @@ function createStructureRule(id, section, desc, expectedPath) {
|
|
|
1915
2687
|
type: "structure",
|
|
1916
2688
|
description: desc,
|
|
1917
2689
|
check: (cwd) => {
|
|
1918
|
-
const fullPath =
|
|
1919
|
-
if (!
|
|
2690
|
+
const fullPath = path9.join(cwd, expectedPath);
|
|
2691
|
+
if (!fs8.existsSync(fullPath)) {
|
|
1920
2692
|
return [{
|
|
1921
2693
|
ruleId: id,
|
|
1922
2694
|
severity: "error",
|
|
@@ -1936,11 +2708,11 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1936
2708
|
pattern: new RegExp(escapeRegex(pattern), "i"),
|
|
1937
2709
|
check: (cwd) => {
|
|
1938
2710
|
const violations = [];
|
|
1939
|
-
const srcDir =
|
|
1940
|
-
if (!
|
|
2711
|
+
const srcDir = path9.join(cwd, "src");
|
|
2712
|
+
if (!fs8.existsSync(srcDir)) return violations;
|
|
1941
2713
|
const regex = new RegExp(escapeRegex(pattern), "i");
|
|
1942
2714
|
walkFiles(srcDir, (filePath) => {
|
|
1943
|
-
const fileContent =
|
|
2715
|
+
const fileContent = fs8.readFileSync(filePath, "utf-8");
|
|
1944
2716
|
const fileLines = fileContent.split("\n");
|
|
1945
2717
|
fileLines.forEach((line, idx) => {
|
|
1946
2718
|
if (regex.test(line)) {
|
|
@@ -1948,7 +2720,7 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1948
2720
|
ruleId: id,
|
|
1949
2721
|
severity: type === "banned" ? "error" : "warning",
|
|
1950
2722
|
message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
|
|
1951
|
-
file:
|
|
2723
|
+
file: path9.relative(cwd, filePath),
|
|
1952
2724
|
line: idx + 1
|
|
1953
2725
|
});
|
|
1954
2726
|
}
|
|
@@ -1960,9 +2732,9 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1960
2732
|
};
|
|
1961
2733
|
}
|
|
1962
2734
|
function walkFiles(dir, callback) {
|
|
1963
|
-
const entries =
|
|
2735
|
+
const entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1964
2736
|
for (const entry of entries) {
|
|
1965
|
-
const fullPath =
|
|
2737
|
+
const fullPath = path9.join(dir, entry.name);
|
|
1966
2738
|
if (entry.isDirectory()) {
|
|
1967
2739
|
if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
|
|
1968
2740
|
walkFiles(fullPath, callback);
|
|
@@ -1977,13 +2749,13 @@ function escapeRegex(str) {
|
|
|
1977
2749
|
}
|
|
1978
2750
|
|
|
1979
2751
|
// src/commands/goal.ts
|
|
1980
|
-
import { existsSync as
|
|
1981
|
-
import { join as
|
|
1982
|
-
import
|
|
2752
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync4 } from "fs";
|
|
2753
|
+
import { join as join5 } from "path";
|
|
2754
|
+
import chalk8 from "chalk";
|
|
1983
2755
|
|
|
1984
2756
|
// src/lib/goal-frontmatter.ts
|
|
1985
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
1986
|
-
import { join } from "path";
|
|
2757
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
|
|
2758
|
+
import { join as join4 } from "path";
|
|
1987
2759
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1988
2760
|
function parseFrontmatter(content) {
|
|
1989
2761
|
const m = content.match(FRONTMATTER_RE);
|
|
@@ -2015,9 +2787,9 @@ function parseSimpleYaml(yaml) {
|
|
|
2015
2787
|
return out;
|
|
2016
2788
|
}
|
|
2017
2789
|
function parseGoalFile(filePath) {
|
|
2018
|
-
if (!
|
|
2790
|
+
if (!existsSync3(filePath)) return null;
|
|
2019
2791
|
try {
|
|
2020
|
-
const content =
|
|
2792
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
2021
2793
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
2022
2794
|
return { filePath, frontmatter, body };
|
|
2023
2795
|
} catch {
|
|
@@ -2025,7 +2797,7 @@ function parseGoalFile(filePath) {
|
|
|
2025
2797
|
}
|
|
2026
2798
|
}
|
|
2027
2799
|
function listGoals(goalsDir) {
|
|
2028
|
-
if (!
|
|
2800
|
+
if (!existsSync3(goalsDir)) return [];
|
|
2029
2801
|
let entries;
|
|
2030
2802
|
try {
|
|
2031
2803
|
entries = readdirSync(goalsDir);
|
|
@@ -2036,7 +2808,7 @@ function listGoals(goalsDir) {
|
|
|
2036
2808
|
for (const name of entries) {
|
|
2037
2809
|
if (!name.endsWith(".md")) continue;
|
|
2038
2810
|
if (name === "_meta.md") continue;
|
|
2039
|
-
const fp =
|
|
2811
|
+
const fp = join4(goalsDir, name);
|
|
2040
2812
|
try {
|
|
2041
2813
|
if (!statSync(fp).isFile()) continue;
|
|
2042
2814
|
} catch {
|
|
@@ -2102,7 +2874,7 @@ ${body}`;
|
|
|
2102
2874
|
|
|
2103
2875
|
// src/commands/goal.ts
|
|
2104
2876
|
var GOALS_DIR = "goals";
|
|
2105
|
-
var
|
|
2877
|
+
var STATE_DIR2 = "docs/state";
|
|
2106
2878
|
var SCRIPTS_DIR = "scripts";
|
|
2107
2879
|
var STATUS_ICON = {
|
|
2108
2880
|
NOT_STARTED: "\u26AA",
|
|
@@ -2128,13 +2900,13 @@ function resolveGoalId(optId, goals) {
|
|
|
2128
2900
|
return selectActiveId(goals);
|
|
2129
2901
|
}
|
|
2130
2902
|
async function goalList() {
|
|
2131
|
-
console.log(
|
|
2903
|
+
console.log(chalk8.bold(`
|
|
2132
2904
|
${ko.goal.listTitle}
|
|
2133
2905
|
`));
|
|
2134
2906
|
const goals = listGoals(GOALS_DIR);
|
|
2135
2907
|
if (goals.length === 0) {
|
|
2136
|
-
console.log(
|
|
2137
|
-
console.log(
|
|
2908
|
+
console.log(chalk8.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2909
|
+
console.log(chalk8.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2138
2910
|
return;
|
|
2139
2911
|
}
|
|
2140
2912
|
for (const g of goals) {
|
|
@@ -2151,17 +2923,22 @@ ${ko.goal.listTitle}
|
|
|
2151
2923
|
const dups = findDuplicateIds(goals);
|
|
2152
2924
|
if (dups.length > 0) {
|
|
2153
2925
|
console.log("");
|
|
2154
|
-
console.log(
|
|
2926
|
+
console.log(chalk8.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
|
|
2155
2927
|
}
|
|
2156
2928
|
}
|
|
2157
2929
|
async function goalNext() {
|
|
2158
|
-
console.log(
|
|
2930
|
+
console.log(chalk8.bold(`
|
|
2159
2931
|
${ko.goal.nextTitle}
|
|
2160
2932
|
`));
|
|
2161
2933
|
const goals = listGoals(GOALS_DIR);
|
|
2934
|
+
if (goals.length === 0) {
|
|
2935
|
+
console.log(chalk8.yellow(" \u{1F4ED} \uC815\uC758\uB41C goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2936
|
+
console.log(chalk8.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2162
2939
|
const activeId = selectActiveId(goals);
|
|
2163
2940
|
if (activeId === null) {
|
|
2164
|
-
console.log(
|
|
2941
|
+
console.log(chalk8.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
|
|
2165
2942
|
return;
|
|
2166
2943
|
}
|
|
2167
2944
|
const active = goals.find((g) => g.frontmatter.id === activeId);
|
|
@@ -2180,10 +2957,10 @@ ${ko.goal.nextTitle}
|
|
|
2180
2957
|
"```",
|
|
2181
2958
|
""
|
|
2182
2959
|
].join("\n");
|
|
2183
|
-
|
|
2184
|
-
|
|
2960
|
+
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2961
|
+
writeFileSync2(join5(STATE_DIR2, "next-task.md"), text, "utf-8");
|
|
2185
2962
|
console.log(
|
|
2186
|
-
|
|
2963
|
+
chalk8.green(
|
|
2187
2964
|
` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
|
|
2188
2965
|
)
|
|
2189
2966
|
);
|
|
@@ -2207,30 +2984,30 @@ var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C
|
|
|
2207
2984
|
var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
|
|
2208
2985
|
var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
|
|
2209
2986
|
async function goalInit() {
|
|
2210
|
-
console.log(
|
|
2987
|
+
console.log(chalk8.bold(`
|
|
2211
2988
|
${ko.goal.initTitle}
|
|
2212
2989
|
`));
|
|
2213
2990
|
const targets = [
|
|
2214
|
-
{ path:
|
|
2215
|
-
{ path:
|
|
2216
|
-
{ path:
|
|
2217
|
-
{ path:
|
|
2991
|
+
{ path: join5(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
|
|
2992
|
+
{ path: join5(STATE_DIR2, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
|
|
2993
|
+
{ path: join5(STATE_DIR2, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
|
|
2994
|
+
{ path: join5(STATE_DIR2, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
|
|
2218
2995
|
];
|
|
2219
|
-
|
|
2220
|
-
|
|
2996
|
+
mkdirSync2(GOALS_DIR, { recursive: true });
|
|
2997
|
+
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2221
2998
|
let created = 0;
|
|
2222
2999
|
let skipped = 0;
|
|
2223
3000
|
for (const t2 of targets) {
|
|
2224
|
-
if (
|
|
2225
|
-
console.log(
|
|
3001
|
+
if (existsSync4(t2.path)) {
|
|
3002
|
+
console.log(chalk8.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
|
|
2226
3003
|
skipped++;
|
|
2227
3004
|
} else {
|
|
2228
|
-
|
|
2229
|
-
console.log(
|
|
3005
|
+
writeFileSync2(t2.path, t2.content, "utf-8");
|
|
3006
|
+
console.log(chalk8.green(` \u2713 created: ${t2.path}`));
|
|
2230
3007
|
created++;
|
|
2231
3008
|
}
|
|
2232
3009
|
}
|
|
2233
|
-
console.log(
|
|
3010
|
+
console.log(chalk8.bold(`
|
|
2234
3011
|
\u{1F4CA} created=${created} skipped=${skipped}`));
|
|
2235
3012
|
if (created > 0) {
|
|
2236
3013
|
printNextStep({
|
|
@@ -2241,10 +3018,10 @@ ${ko.goal.initTitle}
|
|
|
2241
3018
|
}
|
|
2242
3019
|
}
|
|
2243
3020
|
function findGateScript(id) {
|
|
2244
|
-
const mjs =
|
|
2245
|
-
if (
|
|
2246
|
-
const sh =
|
|
2247
|
-
if (
|
|
3021
|
+
const mjs = join5(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
3022
|
+
if (existsSync4(mjs)) return mjs;
|
|
3023
|
+
const sh = join5(SCRIPTS_DIR, `check-goal-${id}.sh`);
|
|
3024
|
+
if (existsSync4(sh)) return sh;
|
|
2248
3025
|
return null;
|
|
2249
3026
|
}
|
|
2250
3027
|
function runGate(scriptPath) {
|
|
@@ -2254,68 +3031,68 @@ function runGate(scriptPath) {
|
|
|
2254
3031
|
return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
|
|
2255
3032
|
}
|
|
2256
3033
|
async function goalCheck(opts) {
|
|
2257
|
-
console.log(
|
|
3034
|
+
console.log(chalk8.bold(`
|
|
2258
3035
|
${ko.goal.checkTitle}
|
|
2259
3036
|
`));
|
|
2260
3037
|
const goals = listGoals(GOALS_DIR);
|
|
2261
3038
|
const id = resolveGoalId(opts.id, goals);
|
|
2262
3039
|
if (id === null) {
|
|
2263
3040
|
console.log(
|
|
2264
|
-
|
|
3041
|
+
chalk8.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2265
3042
|
);
|
|
2266
3043
|
process.exitCode = 1;
|
|
2267
3044
|
return;
|
|
2268
3045
|
}
|
|
2269
3046
|
if (!goals.some((g) => g.frontmatter.id === id)) {
|
|
2270
|
-
console.log(
|
|
3047
|
+
console.log(chalk8.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
2271
3048
|
process.exitCode = 1;
|
|
2272
3049
|
return;
|
|
2273
3050
|
}
|
|
2274
3051
|
const scriptPath = findGateScript(id);
|
|
2275
3052
|
if (!scriptPath) {
|
|
2276
3053
|
console.log(
|
|
2277
|
-
|
|
3054
|
+
chalk8.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
|
|
2278
3055
|
);
|
|
2279
3056
|
process.exitCode = 1;
|
|
2280
3057
|
return;
|
|
2281
3058
|
}
|
|
2282
3059
|
const gate2 = runGate(scriptPath);
|
|
2283
|
-
console.log(
|
|
3060
|
+
console.log(chalk8.dim(` \u25B6 ${gate2.runner} ${scriptPath}
|
|
2284
3061
|
`));
|
|
2285
3062
|
if (gate2.out) console.log(gate2.out);
|
|
2286
3063
|
if (gate2.ok) {
|
|
2287
|
-
console.log(
|
|
3064
|
+
console.log(chalk8.green(`
|
|
2288
3065
|
\u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
|
|
2289
3066
|
} else {
|
|
2290
|
-
console.log(
|
|
3067
|
+
console.log(chalk8.red(`
|
|
2291
3068
|
\u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
|
|
2292
|
-
if (gate2.err && !gate2.out) console.log(
|
|
3069
|
+
if (gate2.err && !gate2.out) console.log(chalk8.dim(gate2.err.slice(0, 500)));
|
|
2293
3070
|
process.exitCode = 1;
|
|
2294
3071
|
}
|
|
2295
3072
|
}
|
|
2296
3073
|
async function goalDone(opts) {
|
|
2297
|
-
console.log(
|
|
3074
|
+
console.log(chalk8.bold(`
|
|
2298
3075
|
${ko.goal.doneTitle}
|
|
2299
3076
|
`));
|
|
2300
3077
|
const goals = listGoals(GOALS_DIR);
|
|
2301
3078
|
const id = resolveGoalId(opts.id, goals);
|
|
2302
3079
|
if (id === null) {
|
|
2303
3080
|
console.log(
|
|
2304
|
-
|
|
3081
|
+
chalk8.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2305
3082
|
);
|
|
2306
3083
|
process.exitCode = 1;
|
|
2307
3084
|
return;
|
|
2308
3085
|
}
|
|
2309
3086
|
const target = goals.find((g) => g.frontmatter.id === id);
|
|
2310
3087
|
if (!target) {
|
|
2311
|
-
console.log(
|
|
3088
|
+
console.log(chalk8.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
2312
3089
|
process.exitCode = 1;
|
|
2313
3090
|
return;
|
|
2314
3091
|
}
|
|
2315
3092
|
const scriptPath = findGateScript(id);
|
|
2316
3093
|
if (!scriptPath) {
|
|
2317
3094
|
console.log(
|
|
2318
|
-
|
|
3095
|
+
chalk8.red(
|
|
2319
3096
|
` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
|
|
2320
3097
|
)
|
|
2321
3098
|
);
|
|
@@ -2323,12 +3100,12 @@ ${ko.goal.doneTitle}
|
|
|
2323
3100
|
return;
|
|
2324
3101
|
}
|
|
2325
3102
|
const gate2 = runGate(scriptPath);
|
|
2326
|
-
console.log(
|
|
3103
|
+
console.log(chalk8.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
|
|
2327
3104
|
`));
|
|
2328
3105
|
if (gate2.out) console.log(gate2.out);
|
|
2329
3106
|
if (!gate2.ok) {
|
|
2330
3107
|
console.log(
|
|
2331
|
-
|
|
3108
|
+
chalk8.red(
|
|
2332
3109
|
`
|
|
2333
3110
|
\u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
|
|
2334
3111
|
)
|
|
@@ -2336,11 +3113,11 @@ ${ko.goal.doneTitle}
|
|
|
2336
3113
|
process.exitCode = 1;
|
|
2337
3114
|
return;
|
|
2338
3115
|
}
|
|
2339
|
-
const content =
|
|
2340
|
-
const today = (
|
|
3116
|
+
const content = readFileSync4(target.filePath, "utf-8");
|
|
3117
|
+
const today = localDate();
|
|
2341
3118
|
const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
|
|
2342
|
-
|
|
2343
|
-
console.log(
|
|
3119
|
+
writeFileSync2(target.filePath, updated, "utf-8");
|
|
3120
|
+
console.log(chalk8.green(`
|
|
2344
3121
|
\u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
|
|
2345
3122
|
printNextStep({
|
|
2346
3123
|
message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
|
|
@@ -2357,22 +3134,22 @@ async function check(opts = {}) {
|
|
|
2357
3134
|
return checkRules();
|
|
2358
3135
|
}
|
|
2359
3136
|
async function checkRules() {
|
|
2360
|
-
console.log(
|
|
3137
|
+
console.log(chalk9.bold(`
|
|
2361
3138
|
${ko.check.title}
|
|
2362
3139
|
`));
|
|
2363
3140
|
const cwd = process.cwd();
|
|
2364
|
-
const rulesPath =
|
|
2365
|
-
if (!
|
|
2366
|
-
console.log(
|
|
2367
|
-
console.log(
|
|
3141
|
+
const rulesPath = path10.join(cwd, "RULES.md");
|
|
3142
|
+
if (!fs9.existsSync(rulesPath)) {
|
|
3143
|
+
console.log(chalk9.yellow(ko.check.noRules));
|
|
3144
|
+
console.log(chalk9.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
2368
3145
|
return;
|
|
2369
3146
|
}
|
|
2370
3147
|
const rules = parseRules(rulesPath);
|
|
2371
|
-
console.log(
|
|
3148
|
+
console.log(chalk9.dim(` \u{1F4CF} \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${rules.length}\uAC1C \uAC10\uC9C0 (\uB098\uBA38\uC9C0 \uADDC\uCE59\uC740 \uC218\uB3D9/\uB3C4\uAD6C \uD655\uC778)
|
|
2372
3149
|
`));
|
|
2373
3150
|
if (rules.length === 0) {
|
|
2374
|
-
console.log(
|
|
2375
|
-
console.log(
|
|
3151
|
+
console.log(chalk9.yellow(ko.check.noAutoRules));
|
|
3152
|
+
console.log(chalk9.dim(" RULES.md\uC5D0 \uD30C\uC77C \uC774\uB984\xB7\uD3F4\uB354 \uADDC\uCE59\uC744 \uC801\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C \uC810\uAC80\uD574\uC694."));
|
|
2376
3153
|
return;
|
|
2377
3154
|
}
|
|
2378
3155
|
const allViolations = [];
|
|
@@ -2380,13 +3157,14 @@ ${ko.check.title}
|
|
|
2380
3157
|
for (const rule of rules) {
|
|
2381
3158
|
const violations = rule.check(cwd);
|
|
2382
3159
|
if (violations.length === 0) {
|
|
2383
|
-
|
|
3160
|
+
const patternHint = rule.type === "content" && rule.pattern ? chalk9.dim(` [\uAC80\uC0AC: ${rule.pattern.source}]`) : "";
|
|
3161
|
+
console.log(chalk9.green(` \u2705 ${rule.id}`) + chalk9.dim(` \u2014 ${rule.description.slice(0, 60)}`) + patternHint);
|
|
2384
3162
|
passCount++;
|
|
2385
3163
|
} else {
|
|
2386
|
-
console.log(
|
|
3164
|
+
console.log(chalk9.red(` \u274C ${rule.id}`) + chalk9.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
2387
3165
|
violations.forEach((v) => {
|
|
2388
|
-
const loc = v.file ?
|
|
2389
|
-
const icon = v.severity === "error" ?
|
|
3166
|
+
const loc = v.file ? chalk9.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
3167
|
+
const icon = v.severity === "error" ? chalk9.red("\u2716") : v.severity === "warning" ? chalk9.yellow("\u26A0") : chalk9.blue("\u2139");
|
|
2390
3168
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
2391
3169
|
});
|
|
2392
3170
|
allViolations.push(...violations);
|
|
@@ -2396,17 +3174,18 @@ ${ko.check.title}
|
|
|
2396
3174
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
2397
3175
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
2398
3176
|
if (allViolations.length === 0) {
|
|
2399
|
-
console.log(
|
|
3177
|
+
console.log(chalk9.green.bold(`\u2705 \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${passCount}\uAC1C \uD1B5\uACFC`));
|
|
3178
|
+
console.log(chalk9.dim(" (RULES.md \uC758 \uB098\uBA38\uC9C0 \uADDC\uCE59\uC740 \uCF54\uB4DC \uC790\uB3D9 \uAC80\uC0AC \uBD88\uAC00 \u2014 \uC9C1\uC811/\uB3C4\uAD6C\uB85C \uD655\uC778\uD558\uC138\uC694.)"));
|
|
2400
3179
|
printNextStep({
|
|
2401
3180
|
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
2402
3181
|
command: "vhk \uBCF4\uC548 scan",
|
|
2403
3182
|
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
2404
3183
|
});
|
|
2405
3184
|
} else {
|
|
2406
|
-
console.log(
|
|
2407
|
-
console.log(` \uADDC\uCE59: ${
|
|
2408
|
-
if (errors > 0) console.log(` ${
|
|
2409
|
-
if (warnings > 0) console.log(` ${
|
|
3185
|
+
console.log(chalk9.bold(ko.check.summary));
|
|
3186
|
+
console.log(` \uADDC\uCE59: ${chalk9.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk9.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk9.red(String(allViolations.length))}\uAC74`);
|
|
3187
|
+
if (errors > 0) console.log(` ${chalk9.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
3188
|
+
if (warnings > 0) console.log(` ${chalk9.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
2410
3189
|
printNextStep({
|
|
2411
3190
|
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
2412
3191
|
command: "vhk \uC810\uAC80",
|
|
@@ -2419,36 +3198,36 @@ ${ko.check.title}
|
|
|
2419
3198
|
}
|
|
2420
3199
|
|
|
2421
3200
|
// src/commands/secure.ts
|
|
2422
|
-
import
|
|
2423
|
-
import
|
|
2424
|
-
import
|
|
3201
|
+
import chalk10 from "chalk";
|
|
3202
|
+
import fs10 from "fs";
|
|
3203
|
+
import path11 from "path";
|
|
2425
3204
|
async function secure() {
|
|
2426
|
-
console.log(
|
|
3205
|
+
console.log(chalk10.bold(`
|
|
2427
3206
|
${ko.secure.title}
|
|
2428
3207
|
`));
|
|
2429
3208
|
const cwd = process.cwd();
|
|
2430
|
-
const gitignorePath =
|
|
2431
|
-
const hasGitignore =
|
|
3209
|
+
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3210
|
+
const hasGitignore = fs10.existsSync(gitignorePath);
|
|
2432
3211
|
if (!hasGitignore) {
|
|
2433
|
-
console.log(
|
|
2434
|
-
console.log(
|
|
3212
|
+
console.log(chalk10.yellow(` ${ko.secure.noGitignore}`));
|
|
3213
|
+
console.log(chalk10.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
2435
3214
|
} else {
|
|
2436
|
-
const gitignoreContent =
|
|
3215
|
+
const gitignoreContent = fs10.readFileSync(gitignorePath, "utf-8");
|
|
2437
3216
|
if (!gitignoreContent.includes(".env")) {
|
|
2438
|
-
console.log(
|
|
2439
|
-
console.log(
|
|
3217
|
+
console.log(chalk10.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
3218
|
+
console.log(chalk10.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
2440
3219
|
}
|
|
2441
3220
|
}
|
|
2442
|
-
console.log(
|
|
3221
|
+
console.log(chalk10.dim(` ${ko.secure.scanning}
|
|
2443
3222
|
`));
|
|
2444
3223
|
const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
|
|
2445
|
-
console.log(
|
|
3224
|
+
console.log(chalk10.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2446
3225
|
if (truncated) {
|
|
2447
|
-
console.log(
|
|
3226
|
+
console.log(chalk10.yellow(` \u26A0\uFE0F \uACB0\uACFC ${MAX_SECRET_FINDINGS}\uAC74\uC5D0\uC11C \uCD9C\uB825\uC744 \uC81C\uD55C\uD588\uC2B5\uB2C8\uB2E4. lock \uD30C\uC77C \uB4F1\uC740 \uC790\uB3D9 \uC81C\uC678\uB429\uB2C8\uB2E4.`));
|
|
2448
3227
|
}
|
|
2449
3228
|
console.log("");
|
|
2450
3229
|
if (findings.length === 0) {
|
|
2451
|
-
console.log(
|
|
3230
|
+
console.log(chalk10.green.bold(` ${ko.secure.clean}`));
|
|
2452
3231
|
printNextStep({
|
|
2453
3232
|
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
2454
3233
|
command: "vhk \uC815\uB9AC",
|
|
@@ -2460,140 +3239,46 @@ ${ko.secure.title}
|
|
|
2460
3239
|
const high = findings.filter((f) => f.severity === "high");
|
|
2461
3240
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
2462
3241
|
if (critical.length > 0) {
|
|
2463
|
-
console.log(
|
|
3242
|
+
console.log(chalk10.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
2464
3243
|
critical.forEach((f) => {
|
|
2465
|
-
console.log(
|
|
2466
|
-
console.log(
|
|
3244
|
+
console.log(chalk10.red(` \u2716 ${f.patternName}`));
|
|
3245
|
+
console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2467
3246
|
});
|
|
2468
3247
|
console.log("");
|
|
2469
3248
|
}
|
|
2470
3249
|
if (high.length > 0) {
|
|
2471
|
-
console.log(
|
|
3250
|
+
console.log(chalk10.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
2472
3251
|
high.forEach((f) => {
|
|
2473
|
-
console.log(
|
|
2474
|
-
console.log(
|
|
3252
|
+
console.log(chalk10.yellow(` \u26A0 ${f.patternName}`));
|
|
3253
|
+
console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2475
3254
|
});
|
|
2476
3255
|
console.log("");
|
|
2477
3256
|
}
|
|
2478
3257
|
if (medium.length > 0) {
|
|
2479
|
-
console.log(
|
|
3258
|
+
console.log(chalk10.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
2480
3259
|
medium.forEach((f) => {
|
|
2481
|
-
console.log(
|
|
2482
|
-
console.log(
|
|
3260
|
+
console.log(chalk10.blue(` \u2139 ${f.patternName}`));
|
|
3261
|
+
console.log(chalk10.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2483
3262
|
});
|
|
2484
3263
|
console.log("");
|
|
2485
3264
|
}
|
|
2486
|
-
console.log(
|
|
2487
|
-
console.log(` \uCD1D ${
|
|
3265
|
+
console.log(chalk10.bold(` ${ko.secure.summary}`));
|
|
3266
|
+
console.log(` \uCD1D ${chalk10.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
2488
3267
|
console.log("");
|
|
2489
|
-
console.log(
|
|
2490
|
-
console.log(
|
|
2491
|
-
console.log(
|
|
2492
|
-
console.log(
|
|
3268
|
+
console.log(chalk10.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
3269
|
+
console.log(chalk10.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
|
|
3270
|
+
console.log(chalk10.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
3271
|
+
console.log(chalk10.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
2493
3272
|
if (critical.length > 0 || high.length > 0) {
|
|
2494
3273
|
process.exitCode = 1;
|
|
2495
3274
|
}
|
|
2496
3275
|
}
|
|
2497
3276
|
|
|
2498
3277
|
// src/commands/doctor.ts
|
|
2499
|
-
import
|
|
2500
|
-
import
|
|
2501
|
-
import
|
|
3278
|
+
import chalk11 from "chalk";
|
|
3279
|
+
import fs11 from "fs";
|
|
3280
|
+
import path12 from "path";
|
|
2502
3281
|
import { fileURLToPath } from "url";
|
|
2503
|
-
|
|
2504
|
-
// src/lib/drift.ts
|
|
2505
|
-
import fs9 from "fs";
|
|
2506
|
-
import path10 from "path";
|
|
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
|
|
2597
3282
|
function checkCommand(name, command, hint) {
|
|
2598
3283
|
const result = safeExecFile(command, ["--version"]);
|
|
2599
3284
|
if (!result.ok) return { name, command, ok: false, hint };
|
|
@@ -2601,14 +3286,14 @@ function checkCommand(name, command, hint) {
|
|
|
2601
3286
|
return { name, command, version, ok: true, hint };
|
|
2602
3287
|
}
|
|
2603
3288
|
function getVhkVersion2() {
|
|
2604
|
-
const dir =
|
|
3289
|
+
const dir = path12.dirname(fileURLToPath(import.meta.url));
|
|
2605
3290
|
const candidates = [
|
|
2606
|
-
|
|
2607
|
-
|
|
3291
|
+
path12.join(dir, "../package.json"),
|
|
3292
|
+
path12.join(dir, "../../package.json")
|
|
2608
3293
|
];
|
|
2609
3294
|
for (const pkgPath of candidates) {
|
|
2610
3295
|
try {
|
|
2611
|
-
if (
|
|
3296
|
+
if (fs11.existsSync(pkgPath)) {
|
|
2612
3297
|
const pkg = readJsonFile(pkgPath);
|
|
2613
3298
|
return pkg.version;
|
|
2614
3299
|
}
|
|
@@ -2636,7 +3321,7 @@ function compareSemver(a, b) {
|
|
|
2636
3321
|
return a3 - b3;
|
|
2637
3322
|
}
|
|
2638
3323
|
async function doctor() {
|
|
2639
|
-
console.log(
|
|
3324
|
+
console.log(chalk11.bold(`
|
|
2640
3325
|
${ko.doctor.title}
|
|
2641
3326
|
`));
|
|
2642
3327
|
const checks = [
|
|
@@ -2648,30 +3333,30 @@ ${ko.doctor.title}
|
|
|
2648
3333
|
let allOk = true;
|
|
2649
3334
|
for (const check2 of checks) {
|
|
2650
3335
|
if (check2.ok) {
|
|
2651
|
-
console.log(
|
|
3336
|
+
console.log(chalk11.green(` \u2705 ${check2.name}`) + chalk11.dim(` \u2014 ${check2.version}`));
|
|
2652
3337
|
} else {
|
|
2653
|
-
console.log(
|
|
2654
|
-
console.log(
|
|
3338
|
+
console.log(chalk11.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
3339
|
+
console.log(chalk11.dim(` \u2192 ${check2.hint}`));
|
|
2655
3340
|
allOk = false;
|
|
2656
3341
|
}
|
|
2657
3342
|
}
|
|
2658
3343
|
console.log("");
|
|
2659
3344
|
const vhkVersion = getVhkVersion2();
|
|
2660
3345
|
if (vhkVersion) {
|
|
2661
|
-
console.log(
|
|
3346
|
+
console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(` \u2014 v${vhkVersion}`));
|
|
2662
3347
|
} else {
|
|
2663
|
-
console.log(
|
|
3348
|
+
console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2664
3349
|
}
|
|
2665
3350
|
if (vhkVersion) {
|
|
2666
3351
|
const latest = fetchLatestNpmVersion("@byh3071/vhk");
|
|
2667
3352
|
if (latest && compareSemver(latest, vhkVersion) > 0) {
|
|
2668
|
-
console.log(
|
|
3353
|
+
console.log(chalk11.yellow(` ${ko.doctor.updateAvailable(latest)}`));
|
|
2669
3354
|
} else if (latest) {
|
|
2670
|
-
console.log(
|
|
3355
|
+
console.log(chalk11.dim(` ${ko.doctor.updateCurrent}`));
|
|
2671
3356
|
}
|
|
2672
3357
|
}
|
|
2673
3358
|
console.log("");
|
|
2674
|
-
console.log(
|
|
3359
|
+
console.log(chalk11.bold(` ${ko.doctor.projectFiles}`));
|
|
2675
3360
|
const cwd = process.cwd();
|
|
2676
3361
|
const projectFiles = [
|
|
2677
3362
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -2681,49 +3366,51 @@ ${ko.doctor.title}
|
|
|
2681
3366
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2682
3367
|
];
|
|
2683
3368
|
for (const file of projectFiles) {
|
|
2684
|
-
const exists =
|
|
3369
|
+
const exists = fs11.existsSync(path12.join(cwd, file.name));
|
|
2685
3370
|
if (exists) {
|
|
2686
|
-
console.log(
|
|
3371
|
+
console.log(chalk11.green(` \u2705 ${file.name}`));
|
|
2687
3372
|
if (file.name === ".env") {
|
|
2688
|
-
const gitignorePath =
|
|
2689
|
-
if (
|
|
2690
|
-
const gitignore =
|
|
3373
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
3374
|
+
if (fs11.existsSync(gitignorePath)) {
|
|
3375
|
+
const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
|
|
2691
3376
|
if (!gitignore.includes(".env")) {
|
|
2692
|
-
console.log(
|
|
3377
|
+
console.log(chalk11.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2693
3378
|
}
|
|
2694
3379
|
}
|
|
2695
3380
|
}
|
|
3381
|
+
} else if (file.name === ".env" && fs11.existsSync(path12.join(cwd, ".env.local"))) {
|
|
3382
|
+
console.log(chalk11.green(" \u2705 .env.local") + chalk11.dim(" \u2014 \uB85C\uCEEC env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
|
|
2696
3383
|
} else {
|
|
2697
|
-
console.log(
|
|
3384
|
+
console.log(chalk11.dim(` \u2B1A ${file.name}`) + chalk11.dim(` \u2014 ${file.hint}`));
|
|
2698
3385
|
}
|
|
2699
3386
|
}
|
|
2700
3387
|
console.log("");
|
|
2701
|
-
console.log(
|
|
3388
|
+
console.log(chalk11.bold(` ${ko.doctor.driftTitle}`));
|
|
2702
3389
|
const ruleDrift = checkRuleDrift(cwd);
|
|
2703
3390
|
if (!ruleDrift.checked) {
|
|
2704
|
-
console.log(
|
|
3391
|
+
console.log(chalk11.dim(` ${ko.doctor.driftNoRules}`));
|
|
2705
3392
|
} else {
|
|
2706
3393
|
const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
|
|
2707
3394
|
if (drifted.length === 0) {
|
|
2708
|
-
console.log(
|
|
3395
|
+
console.log(chalk11.green(` ${ko.doctor.driftRuleClean}`));
|
|
2709
3396
|
} else {
|
|
2710
|
-
console.log(
|
|
3397
|
+
console.log(chalk11.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
|
|
2711
3398
|
}
|
|
2712
3399
|
}
|
|
2713
3400
|
const ctxDrift = checkContextDrift(cwd);
|
|
2714
3401
|
if (ctxDrift.checked && ctxDrift.stale) {
|
|
2715
|
-
console.log(
|
|
3402
|
+
console.log(chalk11.yellow(` ${ko.doctor.driftContextWarn}`));
|
|
2716
3403
|
}
|
|
2717
3404
|
console.log("");
|
|
2718
3405
|
if (allOk) {
|
|
2719
|
-
console.log(
|
|
3406
|
+
console.log(chalk11.green.bold(` ${ko.doctor.allOk}`));
|
|
2720
3407
|
printNextStep({
|
|
2721
3408
|
message: ko.doctor.nextOkMessage,
|
|
2722
3409
|
command: "vhk \uC2DC\uC791",
|
|
2723
3410
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2724
3411
|
});
|
|
2725
3412
|
} else {
|
|
2726
|
-
console.log(
|
|
3413
|
+
console.log(chalk11.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2727
3414
|
printNextStep({
|
|
2728
3415
|
message: ko.doctor.nextRetryMessage,
|
|
2729
3416
|
command: "vhk doctor",
|
|
@@ -2734,10 +3421,10 @@ ${ko.doctor.title}
|
|
|
2734
3421
|
}
|
|
2735
3422
|
|
|
2736
3423
|
// src/commands/ship.ts
|
|
2737
|
-
import
|
|
2738
|
-
import
|
|
2739
|
-
import
|
|
2740
|
-
import
|
|
3424
|
+
import chalk12 from "chalk";
|
|
3425
|
+
import inquirer5 from "inquirer";
|
|
3426
|
+
import fs12 from "fs";
|
|
3427
|
+
import path13 from "path";
|
|
2741
3428
|
var CHECKLIST = [
|
|
2742
3429
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
2743
3430
|
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
@@ -2750,9 +3437,9 @@ function sanitizeVersion(version) {
|
|
|
2750
3437
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2751
3438
|
}
|
|
2752
3439
|
function updateChangelogUnreleased(cwd, version, date) {
|
|
2753
|
-
const changelogPath =
|
|
2754
|
-
if (!
|
|
2755
|
-
const content =
|
|
3440
|
+
const changelogPath = path13.join(cwd, "CHANGELOG.md");
|
|
3441
|
+
if (!fs12.existsSync(changelogPath)) return { status: "missing" };
|
|
3442
|
+
const content = fs12.readFileSync(changelogPath, "utf-8");
|
|
2756
3443
|
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
|
2757
3444
|
if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
|
|
2758
3445
|
const blankUnreleased = [
|
|
@@ -2769,35 +3456,36 @@ function updateChangelogUnreleased(cwd, version, date) {
|
|
|
2769
3456
|
`## [${version}] \u2014 ${date}`
|
|
2770
3457
|
].join("\n");
|
|
2771
3458
|
const updated = content.replace(unreleasedHeading, blankUnreleased);
|
|
2772
|
-
|
|
3459
|
+
fs12.writeFileSync(changelogPath, updated, "utf-8");
|
|
2773
3460
|
return { status: "updated", version };
|
|
2774
3461
|
}
|
|
2775
3462
|
async function ship() {
|
|
2776
|
-
|
|
3463
|
+
if (!ensureNotHardStopped("ship")) return;
|
|
3464
|
+
console.log(chalk12.bold(`
|
|
2777
3465
|
${ko.ship.title}
|
|
2778
3466
|
`));
|
|
2779
3467
|
const cwd = process.cwd();
|
|
2780
|
-
console.log(
|
|
3468
|
+
console.log(chalk12.cyan.bold(` ${ko.ship.checklist}
|
|
2781
3469
|
`));
|
|
2782
|
-
const { passed } = await
|
|
3470
|
+
const { passed } = await inquirer5.prompt([{
|
|
2783
3471
|
type: "checkbox",
|
|
2784
3472
|
name: "passed",
|
|
2785
3473
|
message: ko.ship.checkboxPrompt,
|
|
2786
3474
|
choices: CHECKLIST.map((c) => ({
|
|
2787
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
3475
|
+
name: `${ko.ship[c.questionKey]} ${chalk12.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2788
3476
|
value: c.id
|
|
2789
3477
|
}))
|
|
2790
3478
|
}]);
|
|
2791
3479
|
const allPassed = passed.length === CHECKLIST.length;
|
|
2792
3480
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2793
3481
|
if (!allPassed) {
|
|
2794
|
-
console.log(
|
|
3482
|
+
console.log(chalk12.yellow(`
|
|
2795
3483
|
${ko.ship.incompleteHeader}`));
|
|
2796
3484
|
skipped.forEach((s) => {
|
|
2797
|
-
console.log(
|
|
2798
|
-
console.log(
|
|
3485
|
+
console.log(chalk12.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
3486
|
+
console.log(chalk12.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2799
3487
|
});
|
|
2800
|
-
const { proceed } = await
|
|
3488
|
+
const { proceed } = await inquirer5.prompt([{
|
|
2801
3489
|
type: "confirm",
|
|
2802
3490
|
name: "proceed",
|
|
2803
3491
|
message: ko.ship.proceedConfirm,
|
|
@@ -2812,26 +3500,26 @@ ${ko.ship.title}
|
|
|
2812
3500
|
return;
|
|
2813
3501
|
}
|
|
2814
3502
|
} else {
|
|
2815
|
-
console.log(
|
|
3503
|
+
console.log(chalk12.green(`
|
|
2816
3504
|
${ko.ship.allPassed}
|
|
2817
3505
|
`));
|
|
2818
3506
|
}
|
|
2819
|
-
console.log(
|
|
3507
|
+
console.log(chalk12.cyan.bold(` ${ko.ship.retro}
|
|
2820
3508
|
`));
|
|
2821
|
-
console.log(
|
|
2822
|
-
const retro = await
|
|
3509
|
+
console.log(chalk12.dim(` ${ko.ship.versionHint}`));
|
|
3510
|
+
const retro = await inquirer5.prompt([
|
|
2823
3511
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2824
3512
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
2825
3513
|
{ type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
|
|
2826
3514
|
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
2827
3515
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2828
3516
|
]);
|
|
2829
|
-
const buildLogDir =
|
|
2830
|
-
if (!
|
|
2831
|
-
const today = (
|
|
3517
|
+
const buildLogDir = path13.join(cwd, "docs", "build-log");
|
|
3518
|
+
if (!fs12.existsSync(buildLogDir)) fs12.mkdirSync(buildLogDir, { recursive: true });
|
|
3519
|
+
const today = localDate();
|
|
2832
3520
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2833
3521
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
2834
|
-
const filePath =
|
|
3522
|
+
const filePath = path13.join(buildLogDir, fileName);
|
|
2835
3523
|
const content = [
|
|
2836
3524
|
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
2837
3525
|
"",
|
|
@@ -2860,9 +3548,9 @@ ${ko.ship.title}
|
|
|
2860
3548
|
"---",
|
|
2861
3549
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2862
3550
|
].join("\n");
|
|
2863
|
-
|
|
2864
|
-
console.log(
|
|
2865
|
-
${ko.ship.buildLogDone(
|
|
3551
|
+
fs12.writeFileSync(filePath, content, "utf-8");
|
|
3552
|
+
console.log(chalk12.green(`
|
|
3553
|
+
${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
|
|
2866
3554
|
const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
|
|
2867
3555
|
if (changelogResult.status === "updated") {
|
|
2868
3556
|
log.success(ko.ship.changelogUpdated(changelogResult.version));
|
|
@@ -2881,9 +3569,9 @@ ${ko.ship.title}
|
|
|
2881
3569
|
|
|
2882
3570
|
// src/commands/save.ts
|
|
2883
3571
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2884
|
-
import
|
|
3572
|
+
import chalk13 from "chalk";
|
|
2885
3573
|
import ora from "ora";
|
|
2886
|
-
import
|
|
3574
|
+
import inquirer6 from "inquirer";
|
|
2887
3575
|
|
|
2888
3576
|
// src/lib/git-porcelain.ts
|
|
2889
3577
|
function normalizePorcelain(raw) {
|
|
@@ -2909,54 +3597,54 @@ function statusIcon(code) {
|
|
|
2909
3597
|
return "\u{1F4C4}";
|
|
2910
3598
|
}
|
|
2911
3599
|
async function save() {
|
|
2912
|
-
console.log(
|
|
3600
|
+
console.log(chalk13.bold(`
|
|
2913
3601
|
\u{1F4BE} ${t("save.title")}`));
|
|
2914
|
-
console.log(
|
|
3602
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
2915
3603
|
let gitRoot;
|
|
2916
3604
|
try {
|
|
2917
3605
|
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
2918
3606
|
gitRoot = getGitRoot();
|
|
2919
3607
|
} catch {
|
|
2920
|
-
console.log(
|
|
3608
|
+
console.log(chalk13.red(`\u274C ${t("save.notGitRepo")}`));
|
|
2921
3609
|
return;
|
|
2922
3610
|
}
|
|
2923
|
-
console.log(
|
|
3611
|
+
console.log(chalk13.cyan(`
|
|
2924
3612
|
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
2925
3613
|
printSecurityWarnings(gitRoot);
|
|
2926
3614
|
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
2927
3615
|
if (severe.length > 0) {
|
|
2928
|
-
console.log(
|
|
3616
|
+
console.log(chalk13.red(`
|
|
2929
3617
|
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
2930
3618
|
severe.slice(0, 5).forEach((f) => {
|
|
2931
|
-
console.log(
|
|
3619
|
+
console.log(chalk13.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
2932
3620
|
});
|
|
2933
3621
|
if (severe.length > 5) {
|
|
2934
|
-
console.log(
|
|
3622
|
+
console.log(chalk13.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
2935
3623
|
}
|
|
2936
|
-
const { proceed } = await
|
|
3624
|
+
const { proceed } = await inquirer6.prompt([{
|
|
2937
3625
|
type: "confirm",
|
|
2938
3626
|
name: "proceed",
|
|
2939
3627
|
message: t("save.secretsConfirm"),
|
|
2940
3628
|
default: false
|
|
2941
3629
|
}]);
|
|
2942
3630
|
if (!proceed) {
|
|
2943
|
-
console.log(
|
|
3631
|
+
console.log(chalk13.gray(t("save.cancelled")));
|
|
2944
3632
|
return;
|
|
2945
3633
|
}
|
|
2946
3634
|
}
|
|
2947
3635
|
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
2948
3636
|
if (lines.length === 0) {
|
|
2949
|
-
console.log(
|
|
3637
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
2950
3638
|
return;
|
|
2951
3639
|
}
|
|
2952
|
-
console.log(
|
|
3640
|
+
console.log(chalk13.cyan(`
|
|
2953
3641
|
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
2954
3642
|
lines.forEach((line) => {
|
|
2955
3643
|
const code = line.substring(0, 2);
|
|
2956
3644
|
const name = line.substring(3);
|
|
2957
3645
|
console.log(` ${statusIcon(code)} ${name}`);
|
|
2958
3646
|
});
|
|
2959
|
-
const { message } = await
|
|
3647
|
+
const { message } = await inquirer6.prompt([{
|
|
2960
3648
|
type: "input",
|
|
2961
3649
|
name: "message",
|
|
2962
3650
|
message: t("save.commitMessage"),
|
|
@@ -2971,21 +3659,21 @@ async function save() {
|
|
|
2971
3659
|
spinner.text = t("save.pushing");
|
|
2972
3660
|
if (!hasGitRemote(gitRoot)) {
|
|
2973
3661
|
spinner.succeed(t("save.successLocal"));
|
|
2974
|
-
console.log(
|
|
3662
|
+
console.log(chalk13.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
2975
3663
|
} else {
|
|
2976
3664
|
try {
|
|
2977
3665
|
gitRun(["push"], gitRoot);
|
|
2978
3666
|
spinner.succeed(t("save.successWithPush"));
|
|
2979
3667
|
} catch (pushErr) {
|
|
2980
3668
|
spinner.fail(t("save.pushFailed"));
|
|
2981
|
-
console.log(
|
|
2982
|
-
console.log(
|
|
3669
|
+
console.log(chalk13.red(getExecErrorMessage(pushErr)));
|
|
3670
|
+
console.log(chalk13.yellow(`
|
|
2983
3671
|
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
2984
3672
|
process.exitCode = 1;
|
|
2985
3673
|
}
|
|
2986
3674
|
}
|
|
2987
3675
|
if (process.exitCode !== 1) {
|
|
2988
|
-
console.log(
|
|
3676
|
+
console.log(chalk13.green(`
|
|
2989
3677
|
\u2705 ${t("save.done", lines.length)}`));
|
|
2990
3678
|
printNextStep({
|
|
2991
3679
|
message: t("save.nextOkMessage"),
|
|
@@ -2993,7 +3681,7 @@ async function save() {
|
|
|
2993
3681
|
cursorHint: t("save.nextOkCursor")
|
|
2994
3682
|
});
|
|
2995
3683
|
} else {
|
|
2996
|
-
console.log(
|
|
3684
|
+
console.log(chalk13.green(`
|
|
2997
3685
|
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
2998
3686
|
printNextStep({
|
|
2999
3687
|
message: t("save.nextPushFailMessage"),
|
|
@@ -3003,12 +3691,12 @@ async function save() {
|
|
|
3003
3691
|
}
|
|
3004
3692
|
} catch (err) {
|
|
3005
3693
|
spinner.fail(t("save.failed"));
|
|
3006
|
-
console.log(
|
|
3694
|
+
console.log(chalk13.red(getExecErrorMessage(err)));
|
|
3007
3695
|
if (didAdd) {
|
|
3008
3696
|
try {
|
|
3009
3697
|
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3010
3698
|
if (staged) {
|
|
3011
|
-
console.log(
|
|
3699
|
+
console.log(chalk13.yellow(`
|
|
3012
3700
|
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3013
3701
|
}
|
|
3014
3702
|
} catch {
|
|
@@ -3020,8 +3708,8 @@ async function save() {
|
|
|
3020
3708
|
|
|
3021
3709
|
// src/commands/undo.ts
|
|
3022
3710
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3023
|
-
import
|
|
3024
|
-
import
|
|
3711
|
+
import chalk14 from "chalk";
|
|
3712
|
+
import inquirer7 from "inquirer";
|
|
3025
3713
|
function parseRecentCommits(logOutput) {
|
|
3026
3714
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3027
3715
|
}
|
|
@@ -3044,36 +3732,36 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
|
3044
3732
|
return false;
|
|
3045
3733
|
}
|
|
3046
3734
|
async function undo() {
|
|
3047
|
-
console.log(
|
|
3735
|
+
console.log(chalk14.bold(`
|
|
3048
3736
|
\u23EA ${t("undo.title")}`));
|
|
3049
|
-
console.log(
|
|
3737
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3050
3738
|
let gitRoot;
|
|
3051
3739
|
try {
|
|
3052
3740
|
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3053
3741
|
gitRoot = getGitRoot();
|
|
3054
3742
|
} catch {
|
|
3055
|
-
console.log(
|
|
3743
|
+
console.log(chalk14.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
3056
3744
|
return;
|
|
3057
3745
|
}
|
|
3058
3746
|
let logOutput;
|
|
3059
3747
|
try {
|
|
3060
3748
|
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
3061
3749
|
} catch {
|
|
3062
|
-
console.log(
|
|
3750
|
+
console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3063
3751
|
return;
|
|
3064
3752
|
}
|
|
3065
3753
|
const commits = parseRecentCommits(logOutput);
|
|
3066
3754
|
if (commits.length === 0) {
|
|
3067
|
-
console.log(
|
|
3755
|
+
console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3068
3756
|
return;
|
|
3069
3757
|
}
|
|
3070
|
-
console.log(
|
|
3758
|
+
console.log(chalk14.cyan(`
|
|
3071
3759
|
${t("undo.recentHeader")}`));
|
|
3072
3760
|
commits.forEach((c, i) => {
|
|
3073
3761
|
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
3074
3762
|
});
|
|
3075
3763
|
const maxUndo = commits.length;
|
|
3076
|
-
const { count } = await
|
|
3764
|
+
const { count } = await inquirer7.prompt([{
|
|
3077
3765
|
type: "number",
|
|
3078
3766
|
name: "count",
|
|
3079
3767
|
message: t("undo.howMany"),
|
|
@@ -3084,7 +3772,7 @@ ${t("undo.recentHeader")}`));
|
|
|
3084
3772
|
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
3085
3773
|
const headCount = countLocalCommits(gitRoot);
|
|
3086
3774
|
if (undoCount >= headCount) {
|
|
3087
|
-
console.log(
|
|
3775
|
+
console.log(chalk14.yellow(`
|
|
3088
3776
|
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3089
3777
|
return;
|
|
3090
3778
|
}
|
|
@@ -3093,30 +3781,30 @@ ${t("undo.recentHeader")}`));
|
|
|
3093
3781
|
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3094
3782
|
if (risky) {
|
|
3095
3783
|
if (unpushed < 0) {
|
|
3096
|
-
console.log(
|
|
3784
|
+
console.log(chalk14.red(`
|
|
3097
3785
|
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3098
3786
|
} else {
|
|
3099
|
-
console.log(
|
|
3787
|
+
console.log(chalk14.red(`
|
|
3100
3788
|
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3101
3789
|
}
|
|
3102
3790
|
}
|
|
3103
|
-
const { confirm } = await
|
|
3791
|
+
const { confirm } = await inquirer7.prompt([{
|
|
3104
3792
|
type: "confirm",
|
|
3105
3793
|
name: "confirm",
|
|
3106
3794
|
message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
|
|
3107
3795
|
default: false
|
|
3108
3796
|
}]);
|
|
3109
3797
|
if (!confirm) {
|
|
3110
|
-
console.log(
|
|
3798
|
+
console.log(chalk14.gray(t("undo.cancelled")));
|
|
3111
3799
|
return;
|
|
3112
3800
|
}
|
|
3113
3801
|
try {
|
|
3114
3802
|
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3115
|
-
console.log(
|
|
3803
|
+
console.log(chalk14.green(`
|
|
3116
3804
|
\u2705 ${t("undo.success")}`));
|
|
3117
|
-
console.log(
|
|
3805
|
+
console.log(chalk14.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3118
3806
|
if (risky) {
|
|
3119
|
-
console.log(
|
|
3807
|
+
console.log(chalk14.yellow(`
|
|
3120
3808
|
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3121
3809
|
}
|
|
3122
3810
|
printNextStep({
|
|
@@ -3125,18 +3813,73 @@ ${t("undo.recentHeader")}`));
|
|
|
3125
3813
|
cursorHint: t("undo.nextCursor")
|
|
3126
3814
|
});
|
|
3127
3815
|
} catch (err) {
|
|
3128
|
-
console.log(
|
|
3816
|
+
console.log(chalk14.red(`\u274C ${t("undo.failed")}`));
|
|
3129
3817
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3130
|
-
console.log(
|
|
3818
|
+
console.log(chalk14.red(msg));
|
|
3819
|
+
process.exitCode = 1;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// src/commands/restore.ts
|
|
3824
|
+
import chalk15 from "chalk";
|
|
3825
|
+
import inquirer8 from "inquirer";
|
|
3826
|
+
async function restore(id) {
|
|
3827
|
+
console.log(chalk15.bold(`
|
|
3828
|
+
${ko.restore.title}`));
|
|
3829
|
+
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3830
|
+
console.log(chalk15.dim(` ${ko.restore.notGitNote}`));
|
|
3831
|
+
const cwd = process.cwd();
|
|
3832
|
+
const backups = listBackups(cwd);
|
|
3833
|
+
if (backups.length === 0) {
|
|
3834
|
+
console.log(chalk15.yellow(`
|
|
3835
|
+
${ko.restore.noBackups}`));
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
let targetId = id;
|
|
3839
|
+
if (!targetId) {
|
|
3840
|
+
if (!process.stdout.isTTY) {
|
|
3841
|
+
console.log(chalk15.cyan(`
|
|
3842
|
+
${ko.restore.listHeader}`));
|
|
3843
|
+
for (const b of backups) console.log(` ${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`);
|
|
3844
|
+
console.log(chalk15.yellow(`
|
|
3845
|
+
${ko.restore.nonTtyHint}`));
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const { picked } = await inquirer8.prompt([
|
|
3849
|
+
{
|
|
3850
|
+
type: "list",
|
|
3851
|
+
name: "picked",
|
|
3852
|
+
message: ko.restore.selectPrompt,
|
|
3853
|
+
choices: backups.map((b) => ({
|
|
3854
|
+
name: `${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`,
|
|
3855
|
+
value: b.id
|
|
3856
|
+
}))
|
|
3857
|
+
}
|
|
3858
|
+
]);
|
|
3859
|
+
targetId = picked;
|
|
3860
|
+
}
|
|
3861
|
+
try {
|
|
3862
|
+
const restored = restoreBackup(targetId, cwd);
|
|
3863
|
+
console.log(chalk15.green(`
|
|
3864
|
+
${ko.restore.restored(restored.length, targetId)}`));
|
|
3865
|
+
for (const r of restored) console.log(chalk15.gray(` ${r}`));
|
|
3866
|
+
printNextStep({
|
|
3867
|
+
message: "\uBC31\uC5C5 \uBCF5\uC6D0 \uC644\uB8CC! \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
3868
|
+
command: "vhk diff",
|
|
3869
|
+
cursorHint: "\uBCC0\uACBD\uC0AC\uD56D \uBCF4\uC5EC\uC918"
|
|
3870
|
+
});
|
|
3871
|
+
} catch {
|
|
3872
|
+
console.log(chalk15.red(`
|
|
3873
|
+
${ko.restore.notFound(targetId)}`));
|
|
3131
3874
|
process.exitCode = 1;
|
|
3132
3875
|
}
|
|
3133
3876
|
}
|
|
3134
3877
|
|
|
3135
3878
|
// src/commands/status.ts
|
|
3136
3879
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3137
|
-
import
|
|
3138
|
-
import
|
|
3139
|
-
import
|
|
3880
|
+
import fs13 from "fs";
|
|
3881
|
+
import path14 from "path";
|
|
3882
|
+
import chalk16 from "chalk";
|
|
3140
3883
|
function countFileChanges(porcelain) {
|
|
3141
3884
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
3142
3885
|
let staged = 0;
|
|
@@ -3174,8 +3917,8 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3174
3917
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3175
3918
|
}
|
|
3176
3919
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3177
|
-
const pkgPath =
|
|
3178
|
-
if (!
|
|
3920
|
+
const pkgPath = path14.join(cwd, "package.json");
|
|
3921
|
+
if (!fs13.existsSync(pkgPath)) return null;
|
|
3179
3922
|
try {
|
|
3180
3923
|
const pkg = readJsonFile(pkgPath);
|
|
3181
3924
|
if (!pkg.name && !pkg.version) return null;
|
|
@@ -3187,6 +3930,21 @@ function readProjectPackage(cwd = process.cwd()) {
|
|
|
3187
3930
|
return null;
|
|
3188
3931
|
}
|
|
3189
3932
|
}
|
|
3933
|
+
function selectStatusNextStep(hasChanges) {
|
|
3934
|
+
if (hasChanges) {
|
|
3935
|
+
return {
|
|
3936
|
+
message: t("status.nextWithChangesMessage"),
|
|
3937
|
+
command: "vhk diff",
|
|
3938
|
+
cursorHint: t("status.nextWithChangesCursor"),
|
|
3939
|
+
alternative: t("status.nextWithChangesAlt")
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3942
|
+
return {
|
|
3943
|
+
message: t("status.nextCleanMessage"),
|
|
3944
|
+
command: "vhk goal next",
|
|
3945
|
+
cursorHint: t("status.nextCleanCursor")
|
|
3946
|
+
};
|
|
3947
|
+
}
|
|
3190
3948
|
function getSyncCounts(gitRoot) {
|
|
3191
3949
|
try {
|
|
3192
3950
|
const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
|
|
@@ -3196,15 +3954,15 @@ function getSyncCounts(gitRoot) {
|
|
|
3196
3954
|
}
|
|
3197
3955
|
}
|
|
3198
3956
|
async function status() {
|
|
3199
|
-
console.log(
|
|
3957
|
+
console.log(chalk16.bold(`
|
|
3200
3958
|
\u{1F4CA} ${t("status.title")}`));
|
|
3201
|
-
console.log(
|
|
3959
|
+
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
3202
3960
|
let gitRoot;
|
|
3203
3961
|
try {
|
|
3204
3962
|
execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3205
3963
|
gitRoot = getGitRoot();
|
|
3206
3964
|
} catch {
|
|
3207
|
-
console.log(
|
|
3965
|
+
console.log(chalk16.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3208
3966
|
return;
|
|
3209
3967
|
}
|
|
3210
3968
|
let branch;
|
|
@@ -3223,48 +3981,36 @@ async function status() {
|
|
|
3223
3981
|
commits = [];
|
|
3224
3982
|
}
|
|
3225
3983
|
const pkg = readProjectPackage();
|
|
3226
|
-
console.log(
|
|
3227
|
-
\u{1F33F} ${t("status.branch")}`) +
|
|
3984
|
+
console.log(chalk16.cyan(`
|
|
3985
|
+
\u{1F33F} ${t("status.branch")}`) + chalk16.white(` ${branch}`));
|
|
3228
3986
|
console.log(
|
|
3229
|
-
|
|
3987
|
+
chalk16.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk16.white(
|
|
3230
3988
|
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3231
3989
|
)
|
|
3232
3990
|
);
|
|
3233
|
-
console.log(
|
|
3234
|
-
\u{1F4CB} ${t("status.recentCommits")}`));
|
|
3991
|
+
console.log(chalk16.cyan(`
|
|
3992
|
+
\u{1F4CB} ${t("status.recentCommits", commits.length)}`));
|
|
3235
3993
|
if (commits.length === 0) {
|
|
3236
|
-
console.log(
|
|
3994
|
+
console.log(chalk16.dim(` ${t("status.noCommits")}`));
|
|
3237
3995
|
} else {
|
|
3238
|
-
commits.forEach((c) => console.log(` ${
|
|
3996
|
+
commits.forEach((c) => console.log(` ${chalk16.dim("\u2022")} ${c}`));
|
|
3239
3997
|
}
|
|
3240
3998
|
console.log(
|
|
3241
|
-
|
|
3242
|
-
\u{1F504} ${t("status.remote")}`) +
|
|
3999
|
+
chalk16.cyan(`
|
|
4000
|
+
\u{1F504} ${t("status.remote")}`) + chalk16.white(` ${formatSyncLabel(sync2)}`)
|
|
3243
4001
|
);
|
|
3244
|
-
console.log(
|
|
4002
|
+
console.log(chalk16.gray("\n" + "\u2500".repeat(40)));
|
|
3245
4003
|
if (pkg) {
|
|
3246
|
-
console.log(
|
|
4004
|
+
console.log(chalk16.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk16.white(` ${pkg.name} v${pkg.version}`));
|
|
3247
4005
|
} else {
|
|
3248
|
-
console.log(
|
|
4006
|
+
console.log(chalk16.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3249
4007
|
}
|
|
3250
4008
|
const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
|
|
3251
|
-
|
|
3252
|
-
printNextStep({
|
|
3253
|
-
message: t("status.nextWithChangesMessage"),
|
|
3254
|
-
command: "vhk save",
|
|
3255
|
-
cursorHint: t("status.nextWithChangesCursor")
|
|
3256
|
-
});
|
|
3257
|
-
} else {
|
|
3258
|
-
printNextStep({
|
|
3259
|
-
message: t("status.nextCleanMessage"),
|
|
3260
|
-
command: "vhk goal next",
|
|
3261
|
-
cursorHint: t("status.nextCleanCursor")
|
|
3262
|
-
});
|
|
3263
|
-
}
|
|
4009
|
+
printNextStep(selectStatusNextStep(hasChanges));
|
|
3264
4010
|
}
|
|
3265
4011
|
|
|
3266
4012
|
// src/commands/diff.ts
|
|
3267
|
-
import
|
|
4013
|
+
import chalk17 from "chalk";
|
|
3268
4014
|
function gitOut2(args) {
|
|
3269
4015
|
const r = safeExecFile("git", args);
|
|
3270
4016
|
return r.ok ? r.out : "";
|
|
@@ -3301,67 +4047,67 @@ function summarizeNumstat(numstat) {
|
|
|
3301
4047
|
return { fileCount, totalAdd, totalDel };
|
|
3302
4048
|
}
|
|
3303
4049
|
function printFile(f) {
|
|
3304
|
-
const adds = f.additions > 0 ?
|
|
3305
|
-
const dels = f.deletions > 0 ?
|
|
4050
|
+
const adds = f.additions > 0 ? chalk17.green(`+${f.additions}`) : "";
|
|
4051
|
+
const dels = f.deletions > 0 ? chalk17.red(`-${f.deletions}`) : "";
|
|
3306
4052
|
const change = [adds, dels].filter(Boolean).join(" ");
|
|
3307
4053
|
console.log(` ${f.name} ${change}`);
|
|
3308
4054
|
}
|
|
3309
4055
|
async function diff() {
|
|
3310
|
-
console.log(
|
|
4056
|
+
console.log(chalk17.bold(`
|
|
3311
4057
|
\u{1F50D} ${t("diff.title")}`));
|
|
3312
|
-
console.log(
|
|
4058
|
+
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3313
4059
|
if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
|
|
3314
|
-
console.log(
|
|
4060
|
+
console.log(chalk17.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3315
4061
|
return;
|
|
3316
4062
|
}
|
|
3317
4063
|
const unstaged = gitOut2(["diff", "--stat"]);
|
|
3318
4064
|
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
3319
4065
|
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
3320
4066
|
if (!unstaged && !staged && !untracked) {
|
|
3321
|
-
console.log(
|
|
4067
|
+
console.log(chalk17.green(`
|
|
3322
4068
|
\u2705 ${t("diff.noChanges")}`));
|
|
3323
4069
|
return;
|
|
3324
4070
|
}
|
|
3325
4071
|
if (staged) {
|
|
3326
|
-
console.log(
|
|
4072
|
+
console.log(chalk17.cyan(`
|
|
3327
4073
|
${t("diff.stagedHeader")}`));
|
|
3328
4074
|
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
3329
4075
|
}
|
|
3330
4076
|
if (unstaged) {
|
|
3331
|
-
console.log(
|
|
4077
|
+
console.log(chalk17.cyan(`
|
|
3332
4078
|
${t("diff.unstagedHeader")}`));
|
|
3333
4079
|
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
3334
4080
|
}
|
|
3335
4081
|
if (untracked) {
|
|
3336
4082
|
const files = untracked.split("\n").filter(Boolean);
|
|
3337
|
-
console.log(
|
|
4083
|
+
console.log(chalk17.cyan(`
|
|
3338
4084
|
${t("diff.untrackedHeader", files.length)}`));
|
|
3339
|
-
files.forEach((f) => console.log(` ${
|
|
4085
|
+
files.forEach((f) => console.log(` ${chalk17.green("+")} ${f}`));
|
|
3340
4086
|
}
|
|
3341
4087
|
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
3342
4088
|
if (numstat) {
|
|
3343
4089
|
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
3344
|
-
console.log(
|
|
4090
|
+
console.log(chalk17.cyan(`
|
|
3345
4091
|
${t("diff.summaryHeader")}`));
|
|
3346
4092
|
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
3347
|
-
console.log(` \uCD94\uAC00: ${
|
|
3348
|
-
console.log(` \uC0AD\uC81C: ${
|
|
4093
|
+
console.log(` \uCD94\uAC00: ${chalk17.green(`+${totalAdd}`)}\uC904`);
|
|
4094
|
+
console.log(` \uC0AD\uC81C: ${chalk17.red(`-${totalDel}`)}\uC904`);
|
|
3349
4095
|
}
|
|
3350
4096
|
console.log("");
|
|
3351
4097
|
}
|
|
3352
4098
|
|
|
3353
4099
|
// src/commands/mcp-init.ts
|
|
3354
|
-
import { existsSync as
|
|
3355
|
-
import { join as
|
|
4100
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4101
|
+
import { join as join6, dirname } from "path";
|
|
3356
4102
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3357
|
-
import
|
|
4103
|
+
import chalk18 from "chalk";
|
|
3358
4104
|
function resolveMcpEntryPoint() {
|
|
3359
4105
|
try {
|
|
3360
4106
|
const here = fileURLToPath2(import.meta.url);
|
|
3361
4107
|
const dir = dirname(here);
|
|
3362
4108
|
for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
|
|
3363
|
-
const candidate =
|
|
3364
|
-
if (
|
|
4109
|
+
const candidate = join6(dir, ...rel);
|
|
4110
|
+
if (existsSync5(candidate)) return candidate;
|
|
3365
4111
|
}
|
|
3366
4112
|
} catch {
|
|
3367
4113
|
}
|
|
@@ -3369,17 +4115,17 @@ function resolveMcpEntryPoint() {
|
|
|
3369
4115
|
const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
|
|
3370
4116
|
if (typeof url === "string") {
|
|
3371
4117
|
const p = fileURLToPath2(url);
|
|
3372
|
-
if (
|
|
4118
|
+
if (existsSync5(p)) return p;
|
|
3373
4119
|
}
|
|
3374
4120
|
} catch {
|
|
3375
4121
|
}
|
|
3376
4122
|
try {
|
|
3377
|
-
const pkgPath =
|
|
3378
|
-
if (
|
|
4123
|
+
const pkgPath = join6(process.cwd(), "package.json");
|
|
4124
|
+
if (existsSync5(pkgPath)) {
|
|
3379
4125
|
const pkg = readJsonFile(pkgPath);
|
|
3380
4126
|
if (pkg.name === "@byh3071/vhk") {
|
|
3381
|
-
const local =
|
|
3382
|
-
if (
|
|
4127
|
+
const local = join6(process.cwd(), "dist", "mcp", "index.js");
|
|
4128
|
+
if (existsSync5(local)) return local;
|
|
3383
4129
|
}
|
|
3384
4130
|
}
|
|
3385
4131
|
} catch {
|
|
@@ -3394,31 +4140,31 @@ function resolveVhkMcpEntry() {
|
|
|
3394
4140
|
return { command: "vhk-mcp", args: [] };
|
|
3395
4141
|
}
|
|
3396
4142
|
async function mcpInit() {
|
|
3397
|
-
console.log(
|
|
3398
|
-
console.log(
|
|
3399
|
-
const cursorDir =
|
|
3400
|
-
if (!
|
|
3401
|
-
|
|
4143
|
+
console.log(chalk18.bold("\n\u{1F50C} " + t("mcp.initTitle")));
|
|
4144
|
+
console.log(chalk18.gray("\u2500".repeat(40)));
|
|
4145
|
+
const cursorDir = join6(process.cwd(), ".cursor");
|
|
4146
|
+
if (!existsSync5(cursorDir)) {
|
|
4147
|
+
mkdirSync3(cursorDir, { recursive: true });
|
|
3402
4148
|
}
|
|
3403
|
-
const configPath =
|
|
4149
|
+
const configPath = join6(cursorDir, "mcp.json");
|
|
3404
4150
|
const vhkEntry = resolveVhkMcpEntry();
|
|
3405
4151
|
let config;
|
|
3406
|
-
if (
|
|
4152
|
+
if (existsSync5(configPath)) {
|
|
3407
4153
|
try {
|
|
3408
4154
|
const parsed = readJsonFile(configPath);
|
|
3409
4155
|
config = {
|
|
3410
4156
|
mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
|
|
3411
4157
|
};
|
|
3412
4158
|
} catch {
|
|
3413
|
-
console.log(
|
|
4159
|
+
console.log(chalk18.yellow("\u26A0\uFE0F \uAE30\uC874 .cursor/mcp.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uC0C8 \uD30C\uC77C\uB85C \uB36E\uC5B4\uC501\uB2C8\uB2E4."));
|
|
3414
4160
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3415
4161
|
}
|
|
3416
4162
|
} else {
|
|
3417
4163
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3418
4164
|
}
|
|
3419
|
-
|
|
3420
|
-
console.log(
|
|
3421
|
-
console.log(
|
|
4165
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4166
|
+
console.log(chalk18.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
|
|
4167
|
+
console.log(chalk18.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
|
|
3422
4168
|
console.log(` ${configPath}`);
|
|
3423
4169
|
printNextStep({
|
|
3424
4170
|
message: t("mcp.nextMessage"),
|
|
@@ -3428,9 +4174,9 @@ async function mcpInit() {
|
|
|
3428
4174
|
}
|
|
3429
4175
|
|
|
3430
4176
|
// src/commands/design.ts
|
|
3431
|
-
import { existsSync as
|
|
3432
|
-
import
|
|
3433
|
-
import
|
|
4177
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4178
|
+
import chalk19 from "chalk";
|
|
4179
|
+
import inquirer9 from "inquirer";
|
|
3434
4180
|
var PALETTES = [
|
|
3435
4181
|
{
|
|
3436
4182
|
name: "Minimal",
|
|
@@ -3482,7 +4228,36 @@ var PALETTES = [
|
|
|
3482
4228
|
}
|
|
3483
4229
|
];
|
|
3484
4230
|
function hasTailwind() {
|
|
3485
|
-
return
|
|
4231
|
+
return existsSync6("tailwind.config.js") || existsSync6("tailwind.config.ts") || existsSync6("tailwind.config.mjs") || existsSync6("tailwind.config.cjs");
|
|
4232
|
+
}
|
|
4233
|
+
function isTailwindV4Deps(deps) {
|
|
4234
|
+
if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return true;
|
|
4235
|
+
const tw = deps.tailwindcss;
|
|
4236
|
+
return typeof tw === "string" && /^\D*4(\.|$)/.test(tw);
|
|
4237
|
+
}
|
|
4238
|
+
function hasTailwindV4() {
|
|
4239
|
+
try {
|
|
4240
|
+
const pkg = readJsonFile(
|
|
4241
|
+
"package.json"
|
|
4242
|
+
);
|
|
4243
|
+
return isTailwindV4Deps({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
4244
|
+
} catch {
|
|
4245
|
+
return false;
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
function generateTailwindV4Theme(palette) {
|
|
4249
|
+
return [
|
|
4250
|
+
"/* vhk design \u2014 Tailwind v4 @theme \uD1A0\uD070 (CSS-first). \uC9C4\uC785 CSS \uC5D0 @import \uD558\uC138\uC694. */",
|
|
4251
|
+
'@import "tailwindcss";',
|
|
4252
|
+
"",
|
|
4253
|
+
"@theme {",
|
|
4254
|
+
...Object.entries(palette.colors).map(([k, v]) => ` --color-${k}: ${v};`),
|
|
4255
|
+
"}",
|
|
4256
|
+
"",
|
|
4257
|
+
"/* \uB2E4\uD06C \uBAA8\uB4DC \u2014 .dark \uD074\uB798\uC2A4 \uAE30\uBC18 variant (bg-background \uB4F1\uC774 .dark \uC5D0\uC11C \uC804\uD658) */",
|
|
4258
|
+
"@custom-variant dark (&:where(.dark, .dark *));",
|
|
4259
|
+
""
|
|
4260
|
+
].join("\n");
|
|
3486
4261
|
}
|
|
3487
4262
|
function generateCSSTokens(palette) {
|
|
3488
4263
|
const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
|
|
@@ -3503,9 +4278,10 @@ export default vhkColors
|
|
|
3503
4278
|
`;
|
|
3504
4279
|
}
|
|
3505
4280
|
async function design() {
|
|
3506
|
-
console.log(
|
|
3507
|
-
console.log(
|
|
3508
|
-
|
|
4281
|
+
console.log(chalk19.bold("\n\u{1F3A8} " + t("design.title")));
|
|
4282
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
4283
|
+
if (!ensureInteractive("\uCEEC\uB7EC \uD314\uB808\uD2B8 \uC120\uD0DD\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
|
|
4284
|
+
const { paletteIndex } = await inquirer9.prompt([
|
|
3509
4285
|
{
|
|
3510
4286
|
type: "list",
|
|
3511
4287
|
name: "paletteIndex",
|
|
@@ -3517,32 +4293,37 @@ async function design() {
|
|
|
3517
4293
|
}
|
|
3518
4294
|
]);
|
|
3519
4295
|
const palette = PALETTES[paletteIndex];
|
|
3520
|
-
console.log(
|
|
4296
|
+
console.log(chalk19.cyan(`
|
|
3521
4297
|
\u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
|
|
3522
|
-
const
|
|
3523
|
-
const
|
|
3524
|
-
|
|
3525
|
-
|
|
4298
|
+
const v4 = hasTailwindV4();
|
|
4299
|
+
const targetPath = v4 ? "src/styles/theme.css" : hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
|
|
4300
|
+
const content = v4 ? generateTailwindV4Theme(palette) : hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
|
|
4301
|
+
if (existsSync6(targetPath)) {
|
|
4302
|
+
const { overwrite } = await inquirer9.prompt([{
|
|
3526
4303
|
type: "confirm",
|
|
3527
4304
|
name: "overwrite",
|
|
3528
4305
|
message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
3529
4306
|
default: false
|
|
3530
4307
|
}]);
|
|
3531
4308
|
if (!overwrite) {
|
|
3532
|
-
console.log(
|
|
4309
|
+
console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3533
4310
|
return;
|
|
3534
4311
|
}
|
|
3535
4312
|
}
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
if (
|
|
3539
|
-
console.log(
|
|
3540
|
-
console.log(
|
|
4313
|
+
mkdirSync4("src/styles", { recursive: true });
|
|
4314
|
+
writeFileSync4(targetPath, content, "utf-8");
|
|
4315
|
+
if (v4) {
|
|
4316
|
+
console.log(chalk19.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (Tailwind v4 @theme)"));
|
|
4317
|
+
console.log(chalk19.gray(' \uC9C4\uC785 CSS(\uC608: src/index.css)\uC5D0 `@import "./styles/theme.css";` \uCD94\uAC00 \u2192 bg-primary \uB4F1 \uC720\uD2F8 \uC0AC\uC6A9.'));
|
|
4318
|
+
console.log(chalk19.gray(" \uB2E4\uD06C \uD1A0\uAE00: \uB8E8\uD2B8 <html>/<body> \uC5D0 `.dark` \uD074\uB798\uC2A4 on/off."));
|
|
4319
|
+
} else if (hasTailwind()) {
|
|
4320
|
+
console.log(chalk19.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
|
|
4321
|
+
console.log(chalk19.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
|
|
3541
4322
|
} else {
|
|
3542
|
-
console.log(
|
|
3543
|
-
console.log(
|
|
4323
|
+
console.log(chalk19.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
|
|
4324
|
+
console.log(chalk19.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
|
|
3544
4325
|
}
|
|
3545
|
-
console.log(
|
|
4326
|
+
console.log(chalk19.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
|
|
3546
4327
|
for (const [key, value] of Object.entries(palette.colors)) {
|
|
3547
4328
|
console.log(` ${key.padEnd(12)} ${value}`);
|
|
3548
4329
|
}
|
|
@@ -3557,9 +4338,9 @@ async function designPalette() {
|
|
|
3557
4338
|
}
|
|
3558
4339
|
|
|
3559
4340
|
// src/commands/theme.ts
|
|
3560
|
-
import { existsSync as
|
|
3561
|
-
import
|
|
3562
|
-
import
|
|
4341
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
4342
|
+
import chalk20 from "chalk";
|
|
4343
|
+
import inquirer10 from "inquirer";
|
|
3563
4344
|
function generateDarkCSS() {
|
|
3564
4345
|
return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
|
|
3565
4346
|
|
|
@@ -3615,13 +4396,13 @@ export function initTheme(): void {
|
|
|
3615
4396
|
`;
|
|
3616
4397
|
}
|
|
3617
4398
|
async function theme() {
|
|
3618
|
-
console.log(
|
|
3619
|
-
console.log(
|
|
4399
|
+
console.log(chalk20.bold("\n\u{1F319} " + t("theme.title")));
|
|
4400
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
3620
4401
|
const cssPath = "src/styles/theme.css";
|
|
3621
4402
|
const togglePath = "src/lib/theme-toggle.ts";
|
|
3622
|
-
const conflicts = [cssPath, togglePath].filter((p) =>
|
|
4403
|
+
const conflicts = [cssPath, togglePath].filter((p) => existsSync7(p));
|
|
3623
4404
|
if (conflicts.length > 0) {
|
|
3624
|
-
const { overwrite } = await
|
|
4405
|
+
const { overwrite } = await inquirer10.prompt([{
|
|
3625
4406
|
type: "confirm",
|
|
3626
4407
|
name: "overwrite",
|
|
3627
4408
|
message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
|
|
@@ -3629,21 +4410,21 @@ async function theme() {
|
|
|
3629
4410
|
default: false
|
|
3630
4411
|
}]);
|
|
3631
4412
|
if (!overwrite) {
|
|
3632
|
-
console.log(
|
|
4413
|
+
console.log(chalk20.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3633
4414
|
return;
|
|
3634
4415
|
}
|
|
3635
4416
|
}
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
console.log(
|
|
3640
|
-
|
|
3641
|
-
console.log(
|
|
3642
|
-
console.log(
|
|
3643
|
-
console.log(
|
|
3644
|
-
console.log(
|
|
3645
|
-
console.log(
|
|
3646
|
-
console.log(
|
|
4417
|
+
mkdirSync5("src/styles", { recursive: true });
|
|
4418
|
+
mkdirSync5("src/lib", { recursive: true });
|
|
4419
|
+
writeFileSync5(cssPath, generateDarkCSS(), "utf-8");
|
|
4420
|
+
console.log(chalk20.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
|
|
4421
|
+
writeFileSync5(togglePath, generateToggleUtil(), "utf-8");
|
|
4422
|
+
console.log(chalk20.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
|
|
4423
|
+
console.log(chalk20.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
|
|
4424
|
+
console.log(chalk20.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
|
|
4425
|
+
console.log(chalk20.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
|
|
4426
|
+
console.log(chalk20.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
|
|
4427
|
+
console.log(chalk20.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
|
|
3647
4428
|
printNextStep({
|
|
3648
4429
|
message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
|
|
3649
4430
|
command: "vhk ref list",
|
|
@@ -3652,11 +4433,11 @@ async function theme() {
|
|
|
3652
4433
|
}
|
|
3653
4434
|
|
|
3654
4435
|
// src/commands/ref.ts
|
|
3655
|
-
import { existsSync as
|
|
3656
|
-
import
|
|
4436
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4437
|
+
import chalk21 from "chalk";
|
|
3657
4438
|
var REFS_PATH = ".vhk/refs.json";
|
|
3658
4439
|
function loadRefs() {
|
|
3659
|
-
if (!
|
|
4440
|
+
if (!existsSync8(REFS_PATH)) return [];
|
|
3660
4441
|
try {
|
|
3661
4442
|
const parsed = readJsonFile(REFS_PATH);
|
|
3662
4443
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -3665,28 +4446,28 @@ function loadRefs() {
|
|
|
3665
4446
|
}
|
|
3666
4447
|
}
|
|
3667
4448
|
function saveRefs(refs) {
|
|
3668
|
-
|
|
3669
|
-
|
|
4449
|
+
mkdirSync6(".vhk", { recursive: true });
|
|
4450
|
+
writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
|
|
3670
4451
|
}
|
|
3671
4452
|
async function refAdd(url, memo = "") {
|
|
3672
|
-
console.log(
|
|
3673
|
-
console.log(
|
|
4453
|
+
console.log(chalk21.bold("\n\u{1F517} " + t("ref.addTitle")));
|
|
4454
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3674
4455
|
if (!url) {
|
|
3675
|
-
console.log(
|
|
3676
|
-
console.log(
|
|
4456
|
+
console.log(chalk21.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4457
|
+
console.log(chalk21.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
|
|
3677
4458
|
return;
|
|
3678
4459
|
}
|
|
3679
4460
|
const refs = loadRefs();
|
|
3680
4461
|
if (refs.some((r) => r.url === url)) {
|
|
3681
|
-
console.log(
|
|
4462
|
+
console.log(chalk21.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
|
|
3682
4463
|
return;
|
|
3683
4464
|
}
|
|
3684
4465
|
refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3685
4466
|
saveRefs(refs);
|
|
3686
|
-
console.log(
|
|
4467
|
+
console.log(chalk21.green(`
|
|
3687
4468
|
\u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
|
|
3688
|
-
console.log(
|
|
3689
|
-
if (memo) console.log(
|
|
4469
|
+
console.log(chalk21.cyan(` ${url}`));
|
|
4470
|
+
if (memo) console.log(chalk21.gray(` \u{1F4DD} ${memo}`));
|
|
3690
4471
|
printNextStep({
|
|
3691
4472
|
message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
|
|
3692
4473
|
command: "vhk ref list",
|
|
@@ -3694,22 +4475,22 @@ async function refAdd(url, memo = "") {
|
|
|
3694
4475
|
});
|
|
3695
4476
|
}
|
|
3696
4477
|
async function refList() {
|
|
3697
|
-
console.log(
|
|
3698
|
-
console.log(
|
|
4478
|
+
console.log(chalk21.bold("\n\u{1F4DA} " + t("ref.listTitle")));
|
|
4479
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3699
4480
|
const refs = loadRefs();
|
|
3700
4481
|
if (refs.length === 0) {
|
|
3701
|
-
console.log(
|
|
3702
|
-
console.log(
|
|
4482
|
+
console.log(chalk21.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4483
|
+
console.log(chalk21.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
3703
4484
|
return;
|
|
3704
4485
|
}
|
|
3705
|
-
console.log(
|
|
4486
|
+
console.log(chalk21.cyan(`
|
|
3706
4487
|
\uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
|
|
3707
4488
|
`));
|
|
3708
4489
|
refs.forEach((ref, index) => {
|
|
3709
4490
|
const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
|
|
3710
|
-
console.log(
|
|
3711
|
-
if (ref.memo) console.log(
|
|
3712
|
-
console.log(
|
|
4491
|
+
console.log(chalk21.white(` [${index + 1}] ${ref.url}`));
|
|
4492
|
+
if (ref.memo) console.log(chalk21.gray(` \u{1F4DD} ${ref.memo}`));
|
|
4493
|
+
console.log(chalk21.gray(` \u{1F4C5} ${date}`));
|
|
3713
4494
|
console.log("");
|
|
3714
4495
|
});
|
|
3715
4496
|
}
|
|
@@ -3717,7 +4498,7 @@ async function refOpen(indexStr) {
|
|
|
3717
4498
|
const refs = loadRefs();
|
|
3718
4499
|
const idx = parseInt(indexStr, 10) - 1;
|
|
3719
4500
|
if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
|
|
3720
|
-
console.log(
|
|
4501
|
+
console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
|
|
3721
4502
|
return;
|
|
3722
4503
|
}
|
|
3723
4504
|
const ref = refs[idx];
|
|
@@ -3725,14 +4506,14 @@ async function refOpen(indexStr) {
|
|
|
3725
4506
|
try {
|
|
3726
4507
|
parsed = new URL(ref.url);
|
|
3727
4508
|
} catch {
|
|
3728
|
-
console.log(
|
|
4509
|
+
console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
|
|
3729
4510
|
return;
|
|
3730
4511
|
}
|
|
3731
4512
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
3732
|
-
console.log(
|
|
4513
|
+
console.log(chalk21.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
|
|
3733
4514
|
return;
|
|
3734
4515
|
}
|
|
3735
|
-
console.log(
|
|
4516
|
+
console.log(chalk21.cyan(`
|
|
3736
4517
|
\u{1F310} \uC5F4\uAE30: ${ref.url}`));
|
|
3737
4518
|
let result;
|
|
3738
4519
|
if (process.platform === "darwin") {
|
|
@@ -3743,19 +4524,19 @@ async function refOpen(indexStr) {
|
|
|
3743
4524
|
result = safeExecFile("xdg-open", [ref.url]);
|
|
3744
4525
|
}
|
|
3745
4526
|
if (result.ok) {
|
|
3746
|
-
console.log(
|
|
4527
|
+
console.log(chalk21.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
3747
4528
|
} else {
|
|
3748
|
-
console.log(
|
|
4529
|
+
console.log(chalk21.yellow("\u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. URL\uC744 \uC9C1\uC811 \uBC29\uBB38\uD574\uC8FC\uC138\uC694."));
|
|
3749
4530
|
}
|
|
3750
4531
|
}
|
|
3751
4532
|
|
|
3752
4533
|
// src/commands/harness.ts
|
|
3753
|
-
import { existsSync as
|
|
3754
|
-
import
|
|
4534
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4535
|
+
import chalk22 from "chalk";
|
|
3755
4536
|
import ora2 from "ora";
|
|
3756
4537
|
function detectPM() {
|
|
3757
|
-
if (
|
|
3758
|
-
if (
|
|
4538
|
+
if (existsSync9("pnpm-lock.yaml")) return "pnpm";
|
|
4539
|
+
if (existsSync9("yarn.lock")) return "yarn";
|
|
3759
4540
|
return "npm";
|
|
3760
4541
|
}
|
|
3761
4542
|
function pmRun(pm, script) {
|
|
@@ -3773,14 +4554,14 @@ function detectChecks() {
|
|
|
3773
4554
|
const pm = detectPM();
|
|
3774
4555
|
if (s.lint) {
|
|
3775
4556
|
checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
|
|
3776
|
-
} else if (
|
|
4557
|
+
} else if (existsSync9(".eslintrc.js") || existsSync9(".eslintrc.json") || existsSync9("eslint.config.js")) {
|
|
3777
4558
|
checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
|
|
3778
4559
|
}
|
|
3779
4560
|
if (s["type-check"]) {
|
|
3780
4561
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
|
|
3781
4562
|
} else if (s.typecheck) {
|
|
3782
4563
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
|
|
3783
|
-
} else if (
|
|
4564
|
+
} else if (existsSync9("tsconfig.json")) {
|
|
3784
4565
|
checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
|
|
3785
4566
|
}
|
|
3786
4567
|
if (s.test) {
|
|
@@ -3792,15 +4573,16 @@ function detectChecks() {
|
|
|
3792
4573
|
return checks;
|
|
3793
4574
|
}
|
|
3794
4575
|
async function harness() {
|
|
3795
|
-
|
|
3796
|
-
console.log(
|
|
4576
|
+
if (!ensureNotHardStopped("harness")) return;
|
|
4577
|
+
console.log(chalk22.bold("\n\u{1F527} " + t("harness.title")));
|
|
4578
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3797
4579
|
const checks = detectChecks();
|
|
3798
4580
|
if (checks.length === 0) {
|
|
3799
|
-
console.log(
|
|
3800
|
-
console.log(
|
|
4581
|
+
console.log(chalk22.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4582
|
+
console.log(chalk22.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
|
|
3801
4583
|
return;
|
|
3802
4584
|
}
|
|
3803
|
-
console.log(
|
|
4585
|
+
console.log(chalk22.cyan(`
|
|
3804
4586
|
\u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
|
|
3805
4587
|
`));
|
|
3806
4588
|
const results = [];
|
|
@@ -3812,10 +4594,10 @@ async function harness() {
|
|
|
3812
4594
|
const duration = Date.now() - start2;
|
|
3813
4595
|
const sec = (duration / 1e3).toFixed(1);
|
|
3814
4596
|
if (result.ok) {
|
|
3815
|
-
spinner.succeed(`${check2.name} ${
|
|
4597
|
+
spinner.succeed(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
|
|
3816
4598
|
results.push({ name: check2.name, command: display, passed: true, duration });
|
|
3817
4599
|
} else {
|
|
3818
|
-
spinner.fail(`${check2.name} ${
|
|
4600
|
+
spinner.fail(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
|
|
3819
4601
|
results.push({
|
|
3820
4602
|
name: check2.name,
|
|
3821
4603
|
command: display,
|
|
@@ -3825,24 +4607,25 @@ async function harness() {
|
|
|
3825
4607
|
});
|
|
3826
4608
|
}
|
|
3827
4609
|
}
|
|
3828
|
-
console.log(
|
|
3829
|
-
console.log(
|
|
4610
|
+
console.log(chalk22.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
|
|
4611
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3830
4612
|
for (const r of results) {
|
|
3831
|
-
const icon = r.passed ?
|
|
4613
|
+
const icon = r.passed ? chalk22.green("\u2705") : chalk22.red("\u274C");
|
|
3832
4614
|
const sec = (r.duration / 1e3).toFixed(1);
|
|
3833
|
-
console.log(` ${icon} ${r.name.padEnd(15)} ${
|
|
4615
|
+
console.log(` ${icon} ${r.name.padEnd(15)} ${chalk22.gray(`${sec}s`)}`);
|
|
3834
4616
|
}
|
|
3835
4617
|
const passed = results.filter((r) => r.passed).length;
|
|
3836
4618
|
const all = passed === results.length;
|
|
3837
|
-
console.log(
|
|
4619
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3838
4620
|
if (all) {
|
|
3839
|
-
console.log(
|
|
4621
|
+
console.log(chalk22.green.bold(`
|
|
3840
4622
|
\u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
|
|
3841
4623
|
} else {
|
|
3842
4624
|
console.log(
|
|
3843
|
-
|
|
4625
|
+
chalk22.red.bold(`
|
|
3844
4626
|
\u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
|
|
3845
4627
|
);
|
|
4628
|
+
process.exitCode = 1;
|
|
3846
4629
|
}
|
|
3847
4630
|
printNextStep({
|
|
3848
4631
|
message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
|
|
@@ -3852,9 +4635,9 @@ async function harness() {
|
|
|
3852
4635
|
}
|
|
3853
4636
|
|
|
3854
4637
|
// src/commands/migrate.ts
|
|
3855
|
-
import { existsSync as
|
|
3856
|
-
import
|
|
3857
|
-
import
|
|
4638
|
+
import { existsSync as existsSync10, unlinkSync, rmSync as rmSync2 } from "fs";
|
|
4639
|
+
import chalk23 from "chalk";
|
|
4640
|
+
import inquirer11 from "inquirer";
|
|
3858
4641
|
import ora3 from "ora";
|
|
3859
4642
|
var LOCK_FILES = {
|
|
3860
4643
|
npm: "package-lock.json",
|
|
@@ -3862,26 +4645,26 @@ var LOCK_FILES = {
|
|
|
3862
4645
|
pnpm: "pnpm-lock.yaml"
|
|
3863
4646
|
};
|
|
3864
4647
|
function detectCurrentPM() {
|
|
3865
|
-
if (
|
|
3866
|
-
if (
|
|
3867
|
-
if (
|
|
4648
|
+
if (existsSync10("pnpm-lock.yaml")) return "pnpm";
|
|
4649
|
+
if (existsSync10("yarn.lock")) return "yarn";
|
|
4650
|
+
if (existsSync10("package-lock.json")) return "npm";
|
|
3868
4651
|
return null;
|
|
3869
4652
|
}
|
|
3870
4653
|
function isCLIAvailable(pm) {
|
|
3871
4654
|
return safeExecFile(pm, ["--version"]).ok;
|
|
3872
4655
|
}
|
|
3873
4656
|
async function migrate(target) {
|
|
3874
|
-
console.log(
|
|
3875
|
-
console.log(
|
|
4657
|
+
console.log(chalk23.bold("\n\u{1F504} " + t("migrate.title")));
|
|
4658
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
3876
4659
|
const current = detectCurrentPM();
|
|
3877
|
-
console.log(
|
|
4660
|
+
console.log(chalk23.cyan(`
|
|
3878
4661
|
\uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
|
|
3879
4662
|
let targetPM;
|
|
3880
4663
|
if (target && ["npm", "yarn", "pnpm"].includes(target)) {
|
|
3881
4664
|
targetPM = target;
|
|
3882
4665
|
} else {
|
|
3883
4666
|
const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
|
|
3884
|
-
const { selected } = await
|
|
4667
|
+
const { selected } = await inquirer11.prompt([
|
|
3885
4668
|
{
|
|
3886
4669
|
type: "list",
|
|
3887
4670
|
name: "selected",
|
|
@@ -3892,17 +4675,17 @@ async function migrate(target) {
|
|
|
3892
4675
|
targetPM = selected;
|
|
3893
4676
|
}
|
|
3894
4677
|
if (targetPM === current) {
|
|
3895
|
-
console.log(
|
|
4678
|
+
console.log(chalk23.yellow(`
|
|
3896
4679
|
\u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
|
|
3897
4680
|
return;
|
|
3898
4681
|
}
|
|
3899
4682
|
if (!isCLIAvailable(targetPM)) {
|
|
3900
|
-
console.log(
|
|
4683
|
+
console.log(chalk23.red(`
|
|
3901
4684
|
\u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
3902
|
-
console.log(
|
|
4685
|
+
console.log(chalk23.yellow(` npm i -g ${targetPM}`));
|
|
3903
4686
|
return;
|
|
3904
4687
|
}
|
|
3905
|
-
const { confirm } = await
|
|
4688
|
+
const { confirm } = await inquirer11.prompt([
|
|
3906
4689
|
{
|
|
3907
4690
|
type: "confirm",
|
|
3908
4691
|
name: "confirm",
|
|
@@ -3911,18 +4694,18 @@ async function migrate(target) {
|
|
|
3911
4694
|
}
|
|
3912
4695
|
]);
|
|
3913
4696
|
if (!confirm) {
|
|
3914
|
-
console.log(
|
|
4697
|
+
console.log(chalk23.gray("\uCDE8\uC18C\uB428"));
|
|
3915
4698
|
return;
|
|
3916
4699
|
}
|
|
3917
4700
|
const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
|
|
3918
4701
|
for (const lockFile of Object.values(LOCK_FILES)) {
|
|
3919
|
-
if (
|
|
4702
|
+
if (existsSync10(lockFile)) {
|
|
3920
4703
|
unlinkSync(lockFile);
|
|
3921
4704
|
}
|
|
3922
4705
|
}
|
|
3923
|
-
if (
|
|
4706
|
+
if (existsSync10("node_modules")) {
|
|
3924
4707
|
cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
|
|
3925
|
-
|
|
4708
|
+
rmSync2("node_modules", { recursive: true, force: true });
|
|
3926
4709
|
}
|
|
3927
4710
|
cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
|
|
3928
4711
|
const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
|
|
@@ -3931,10 +4714,10 @@ async function migrate(target) {
|
|
|
3931
4714
|
install.succeed(`${targetPM} install \uC644\uB8CC!`);
|
|
3932
4715
|
} else {
|
|
3933
4716
|
install.fail(`${targetPM} install \uC2E4\uD328`);
|
|
3934
|
-
console.log(
|
|
4717
|
+
console.log(chalk23.red(installResult.err.slice(0, 300)));
|
|
3935
4718
|
return;
|
|
3936
4719
|
}
|
|
3937
|
-
console.log(
|
|
4720
|
+
console.log(chalk23.green.bold(`
|
|
3938
4721
|
\u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
|
|
3939
4722
|
printNextStep({
|
|
3940
4723
|
message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
|
|
@@ -3944,17 +4727,17 @@ async function migrate(target) {
|
|
|
3944
4727
|
}
|
|
3945
4728
|
|
|
3946
4729
|
// src/commands/update.ts
|
|
3947
|
-
import { existsSync as
|
|
3948
|
-
import { dirname as dirname2, join as
|
|
4730
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4731
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
3949
4732
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3950
|
-
import
|
|
4733
|
+
import chalk24 from "chalk";
|
|
3951
4734
|
import ora4 from "ora";
|
|
3952
4735
|
var PACKAGE = "@byh3071/vhk";
|
|
3953
4736
|
function getCurrentVersion() {
|
|
3954
4737
|
const dir = dirname2(fileURLToPath3(import.meta.url));
|
|
3955
|
-
for (const pkgPath of [
|
|
4738
|
+
for (const pkgPath of [join7(dir, "../package.json"), join7(dir, "../../package.json")]) {
|
|
3956
4739
|
try {
|
|
3957
|
-
if (
|
|
4740
|
+
if (existsSync11(pkgPath)) {
|
|
3958
4741
|
const pkg = readJsonFile(pkgPath);
|
|
3959
4742
|
if (pkg.version) return pkg.version;
|
|
3960
4743
|
}
|
|
@@ -3977,32 +4760,32 @@ function isUpToDate(current, latest) {
|
|
|
3977
4760
|
return cc >= lc;
|
|
3978
4761
|
}
|
|
3979
4762
|
async function update() {
|
|
3980
|
-
console.log(
|
|
3981
|
-
console.log(
|
|
4763
|
+
console.log(chalk24.bold("\n\u2B06\uFE0F " + t("update.title")));
|
|
4764
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
3982
4765
|
const current = getCurrentVersion();
|
|
3983
|
-
console.log(
|
|
4766
|
+
console.log(chalk24.cyan(`
|
|
3984
4767
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
|
|
3985
4768
|
const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
|
|
3986
4769
|
const latest = getLatestVersion();
|
|
3987
4770
|
if (!latest) {
|
|
3988
4771
|
spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3989
|
-
console.log(
|
|
3990
|
-
console.log(
|
|
4772
|
+
console.log(chalk24.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
4773
|
+
console.log(chalk24.gray(` npm update -g ${PACKAGE}`));
|
|
3991
4774
|
return;
|
|
3992
4775
|
}
|
|
3993
4776
|
spinner.stop();
|
|
3994
|
-
console.log(
|
|
4777
|
+
console.log(chalk24.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
|
|
3995
4778
|
if (isUpToDate(current, latest)) {
|
|
3996
|
-
console.log(
|
|
4779
|
+
console.log(chalk24.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
|
|
3997
4780
|
return;
|
|
3998
4781
|
}
|
|
3999
4782
|
const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
|
|
4000
4783
|
const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
|
|
4001
4784
|
if (upd.ok) {
|
|
4002
4785
|
updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
|
|
4003
|
-
console.log(
|
|
4786
|
+
console.log(chalk24.green.bold(`
|
|
4004
4787
|
\u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
4005
|
-
console.log(
|
|
4788
|
+
console.log(chalk24.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
4006
4789
|
printNextStep({
|
|
4007
4790
|
message: t("update.nextOkMessage"),
|
|
4008
4791
|
command: "vhk --version",
|
|
@@ -4010,9 +4793,9 @@ async function update() {
|
|
|
4010
4793
|
});
|
|
4011
4794
|
} else {
|
|
4012
4795
|
updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
|
|
4013
|
-
console.log(
|
|
4014
|
-
console.log(
|
|
4015
|
-
console.log(
|
|
4796
|
+
console.log(chalk24.red(upd.err.slice(0, 300)));
|
|
4797
|
+
console.log(chalk24.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
4798
|
+
console.log(chalk24.gray(` npm update -g ${PACKAGE}`));
|
|
4016
4799
|
printNextStep({
|
|
4017
4800
|
message: t("update.nextFailMessage"),
|
|
4018
4801
|
command: "vhk doctor",
|
|
@@ -4023,121 +4806,15 @@ async function update() {
|
|
|
4023
4806
|
|
|
4024
4807
|
// src/commands/context.ts
|
|
4025
4808
|
import {
|
|
4026
|
-
existsSync as
|
|
4809
|
+
existsSync as existsSync12,
|
|
4027
4810
|
mkdirSync as mkdirSync7,
|
|
4028
|
-
readFileSync as
|
|
4811
|
+
readFileSync as readFileSync5,
|
|
4029
4812
|
readdirSync as readdirSync2,
|
|
4030
4813
|
statSync as statSync2,
|
|
4031
4814
|
writeFileSync as writeFileSync7
|
|
4032
4815
|
} from "fs";
|
|
4033
|
-
import { join as
|
|
4034
|
-
import
|
|
4035
|
-
|
|
4036
|
-
// src/lib/state-files.ts
|
|
4037
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6, appendFileSync, rmSync as rmSync2 } from "fs";
|
|
4038
|
-
import { join as join5 } from "path";
|
|
4039
|
-
var STATE_DIR2 = "docs/state";
|
|
4040
|
-
var BLOCKERS_PATH = join5(STATE_DIR2, "blockers.md");
|
|
4041
|
-
var LEARNINGS_PATH = join5(STATE_DIR2, "learnings.md");
|
|
4042
|
-
var VHK_DIR = ".vhk";
|
|
4043
|
-
var HARD_STOP_PATH = join5(VHK_DIR, "HARD_STOP");
|
|
4044
|
-
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
4045
|
-
function ensureStateDir() {
|
|
4046
|
-
mkdirSync6(STATE_DIR2, { recursive: true });
|
|
4047
|
-
}
|
|
4048
|
-
function ensureVhkDir() {
|
|
4049
|
-
mkdirSync6(VHK_DIR, { recursive: true });
|
|
4050
|
-
}
|
|
4051
|
-
function isoDate() {
|
|
4052
|
-
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4053
|
-
}
|
|
4054
|
-
var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
|
|
4055
|
-
function countActiveBlockers(content) {
|
|
4056
|
-
let count = 0;
|
|
4057
|
-
for (const line of content.split(/\r?\n/)) {
|
|
4058
|
-
if (ACTIVE_BLOCKER_RE.test(line)) count++;
|
|
4059
|
-
}
|
|
4060
|
-
return count;
|
|
4061
|
-
}
|
|
4062
|
-
function appendBlocker(description, goalId) {
|
|
4063
|
-
ensureStateDir();
|
|
4064
|
-
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
4065
|
-
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
4066
|
-
if (!existsSync10(BLOCKERS_PATH)) {
|
|
4067
|
-
writeFileSync6(
|
|
4068
|
-
BLOCKERS_PATH,
|
|
4069
|
-
`# Blockers
|
|
4070
|
-
|
|
4071
|
-
_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
|
|
4072
|
-
|
|
4073
|
-
${line}
|
|
4074
|
-
`,
|
|
4075
|
-
"utf-8"
|
|
4076
|
-
);
|
|
4077
|
-
} else {
|
|
4078
|
-
appendFileSync(BLOCKERS_PATH, `${line}
|
|
4079
|
-
`, "utf-8");
|
|
4080
|
-
}
|
|
4081
|
-
const current = readFileSync3(BLOCKERS_PATH, "utf-8");
|
|
4082
|
-
const count = countActiveBlockers(current);
|
|
4083
|
-
let hardStopTripped = false;
|
|
4084
|
-
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync10(HARD_STOP_PATH)) {
|
|
4085
|
-
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
4086
|
-
hardStopTripped = true;
|
|
4087
|
-
}
|
|
4088
|
-
return { count, hardStopTripped };
|
|
4089
|
-
}
|
|
4090
|
-
function appendLearning(lesson, goalId) {
|
|
4091
|
-
ensureStateDir();
|
|
4092
|
-
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
4093
|
-
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
4094
|
-
if (!existsSync10(LEARNINGS_PATH)) {
|
|
4095
|
-
writeFileSync6(
|
|
4096
|
-
LEARNINGS_PATH,
|
|
4097
|
-
`# Learnings
|
|
4098
|
-
|
|
4099
|
-
_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
|
|
4100
|
-
|
|
4101
|
-
${line}
|
|
4102
|
-
`,
|
|
4103
|
-
"utf-8"
|
|
4104
|
-
);
|
|
4105
|
-
} else {
|
|
4106
|
-
appendFileSync(LEARNINGS_PATH, `${line}
|
|
4107
|
-
`, "utf-8");
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
function getRecentLearnings(limit = 3) {
|
|
4111
|
-
if (!existsSync10(LEARNINGS_PATH)) return [];
|
|
4112
|
-
const lines = readFileSync3(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
4113
|
-
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
4114
|
-
return entries.slice(-limit);
|
|
4115
|
-
}
|
|
4116
|
-
function writeHardStop(reason) {
|
|
4117
|
-
ensureVhkDir();
|
|
4118
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4119
|
-
writeFileSync6(HARD_STOP_PATH, `${ts}
|
|
4120
|
-
${reason}
|
|
4121
|
-
`, "utf-8");
|
|
4122
|
-
}
|
|
4123
|
-
function isHardStopActive() {
|
|
4124
|
-
return existsSync10(HARD_STOP_PATH);
|
|
4125
|
-
}
|
|
4126
|
-
function readHardStopReason() {
|
|
4127
|
-
if (!existsSync10(HARD_STOP_PATH)) return null;
|
|
4128
|
-
try {
|
|
4129
|
-
return readFileSync3(HARD_STOP_PATH, "utf-8").trim();
|
|
4130
|
-
} catch {
|
|
4131
|
-
return null;
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
function clearHardStop() {
|
|
4135
|
-
if (!existsSync10(HARD_STOP_PATH)) return false;
|
|
4136
|
-
rmSync2(HARD_STOP_PATH, { force: true });
|
|
4137
|
-
return true;
|
|
4138
|
-
}
|
|
4139
|
-
|
|
4140
|
-
// src/commands/context.ts
|
|
4816
|
+
import { join as join8 } from "path";
|
|
4817
|
+
import chalk25 from "chalk";
|
|
4141
4818
|
var CONTEXT_PATH2 = ".vhk/context.md";
|
|
4142
4819
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4143
4820
|
"node_modules",
|
|
@@ -4162,7 +4839,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
|
|
|
4162
4839
|
filtered.forEach((entry, index) => {
|
|
4163
4840
|
const isLast = index === filtered.length - 1;
|
|
4164
4841
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4165
|
-
const fullPath =
|
|
4842
|
+
const fullPath = join8(dir, entry);
|
|
4166
4843
|
const stat = statSync2(fullPath);
|
|
4167
4844
|
const isDir = stat.isDirectory();
|
|
4168
4845
|
lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
@@ -4194,8 +4871,8 @@ function extractTechStack() {
|
|
|
4194
4871
|
else if (all.jest) stack["\uD14C\uC2A4\uD2B8"] = "jest";
|
|
4195
4872
|
if (all.commander) stack["CLI"] = "commander";
|
|
4196
4873
|
if (all.inquirer) stack["\uC778\uD130\uB799\uD2F0\uBE0C"] = "inquirer";
|
|
4197
|
-
if (
|
|
4198
|
-
else if (
|
|
4874
|
+
if (existsSync12("pnpm-lock.yaml")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "pnpm";
|
|
4875
|
+
else if (existsSync12("yarn.lock")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "yarn";
|
|
4199
4876
|
else stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "npm";
|
|
4200
4877
|
if (pkg.name) stack["\uD328\uD0A4\uC9C0 \uC774\uB984"] = pkg.name;
|
|
4201
4878
|
if (pkg.version) stack["\uBC84\uC804"] = pkg.version;
|
|
@@ -4236,11 +4913,12 @@ function getVhkCommands() {
|
|
|
4236
4913
|
"mcp-init \u2014 Cursor MCP \uC124\uC815"
|
|
4237
4914
|
];
|
|
4238
4915
|
}
|
|
4239
|
-
async function context() {
|
|
4240
|
-
|
|
4241
|
-
console.log(
|
|
4916
|
+
async function context(opts = {}) {
|
|
4917
|
+
const compact = opts.compact === true;
|
|
4918
|
+
console.log(chalk25.bold("\n\u{1F9E0} " + t("context.title")));
|
|
4919
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
4242
4920
|
const stack = extractTechStack();
|
|
4243
|
-
const tree = buildTree(".").join("\n");
|
|
4921
|
+
const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
|
|
4244
4922
|
const commands = getVhkCommands();
|
|
4245
4923
|
const lines = [];
|
|
4246
4924
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
|
|
@@ -4256,25 +4934,37 @@ async function context() {
|
|
|
4256
4934
|
lines.push("");
|
|
4257
4935
|
lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
|
|
4258
4936
|
lines.push("");
|
|
4259
|
-
lines.push("```");
|
|
4937
|
+
lines.push("```text");
|
|
4260
4938
|
lines.push(tree);
|
|
4261
4939
|
lines.push("```");
|
|
4262
4940
|
lines.push("");
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
lines.push(
|
|
4941
|
+
if (compact) {
|
|
4942
|
+
lines.push("## \uBA85\uB839\uC5B4 (\uC694\uC57D)");
|
|
4943
|
+
lines.push("");
|
|
4944
|
+
lines.push("- \uC804\uCCB4 \uBAA9\uB85D\uC740 `vhk help` \uB610\uB294 `COMMANDS.md` \uCC38\uC870 (compact \uBAA8\uB4DC\uB294 \uC0DD\uB7B5)");
|
|
4945
|
+
lines.push("");
|
|
4946
|
+
} else {
|
|
4947
|
+
lines.push("## VHK CLI \uBA85\uB839\uC5B4");
|
|
4948
|
+
lines.push("");
|
|
4949
|
+
for (const cmd of commands) {
|
|
4950
|
+
lines.push(`- \`vhk ${cmd}\``);
|
|
4951
|
+
}
|
|
4952
|
+
lines.push("");
|
|
4267
4953
|
}
|
|
4268
|
-
|
|
4269
|
-
if (existsSync11(".vhk/memory.json")) {
|
|
4954
|
+
if (existsSync12(".vhk/memory.json")) {
|
|
4270
4955
|
try {
|
|
4271
4956
|
const memories = readJsonFile(
|
|
4272
4957
|
".vhk/memory.json"
|
|
4273
4958
|
);
|
|
4274
4959
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
4960
|
+
const recentMemories = memories.slice(-5);
|
|
4275
4961
|
lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
|
|
4276
4962
|
lines.push("");
|
|
4277
|
-
|
|
4963
|
+
if (memories.length > recentMemories.length) {
|
|
4964
|
+
lines.push(`_\uCD5C\uADFC ${recentMemories.length}\uAC1C\uB9CC \uD45C\uC2DC (\uC804\uCCB4 ${memories.length}\uAC1C)_`);
|
|
4965
|
+
lines.push("");
|
|
4966
|
+
}
|
|
4967
|
+
for (const m of recentMemories) {
|
|
4278
4968
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4279
4969
|
lines.push(`- ${m.content} _(${date})_`);
|
|
4280
4970
|
}
|
|
@@ -4305,6 +4995,24 @@ async function context() {
|
|
|
4305
4995
|
for (const r of recent) lines.push(r);
|
|
4306
4996
|
lines.push("");
|
|
4307
4997
|
}
|
|
4998
|
+
const activeBlockers = getActiveBlockers(3);
|
|
4999
|
+
if (activeBlockers.length > 0) {
|
|
5000
|
+
lines.push("## Active Blockers");
|
|
5001
|
+
lines.push("");
|
|
5002
|
+
for (const b of activeBlockers) lines.push(b);
|
|
5003
|
+
lines.push("");
|
|
5004
|
+
}
|
|
5005
|
+
if (compact) {
|
|
5006
|
+
lines.push("## \uCC38\uC870 \uBB38\uC11C (\uD544\uC694\uC2DC \uC5F4\uB78C)");
|
|
5007
|
+
lines.push("");
|
|
5008
|
+
lines.push("- \uC791\uB3D9 \uADDC\uC57D(\uC694\uC57D): `docs/context/agent-compact.md`");
|
|
5009
|
+
lines.push("- \uADDC\uC57D \uC0C1\uC138: `AGENTS.md`");
|
|
5010
|
+
lines.push("- \uAE30\uB85D \uADDC\uCE59: `CLAUDE.md`");
|
|
5011
|
+
lines.push("- \uBA85\uB839 \uC0C1\uC138: `COMMANDS.md`");
|
|
5012
|
+
lines.push("- \uAD6C\uC870 \uC0C1\uC138: `docs/ARCHITECTURE.md`");
|
|
5013
|
+
lines.push("- \uD604\uC7AC \uC0C1\uD0DC: `docs/state/next-task.md`");
|
|
5014
|
+
lines.push("");
|
|
5015
|
+
}
|
|
4308
5016
|
if (isHardStopActive()) {
|
|
4309
5017
|
lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
|
|
4310
5018
|
lines.push("");
|
|
@@ -4323,10 +5031,10 @@ async function context() {
|
|
|
4323
5031
|
lines.push("");
|
|
4324
5032
|
mkdirSync7(".vhk", { recursive: true });
|
|
4325
5033
|
writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
|
|
4326
|
-
console.log(
|
|
5034
|
+
console.log(chalk25.green(`
|
|
4327
5035
|
\u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
|
|
4328
|
-
console.log(
|
|
4329
|
-
console.log(
|
|
5036
|
+
console.log(chalk25.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
|
|
5037
|
+
console.log(chalk25.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
|
|
4330
5038
|
printNextStep({
|
|
4331
5039
|
message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
|
|
4332
5040
|
command: "vhk context-show",
|
|
@@ -4334,23 +5042,23 @@ async function context() {
|
|
|
4334
5042
|
});
|
|
4335
5043
|
}
|
|
4336
5044
|
async function contextShow() {
|
|
4337
|
-
console.log(
|
|
4338
|
-
console.log(
|
|
4339
|
-
if (!
|
|
4340
|
-
console.log(
|
|
4341
|
-
console.log(
|
|
5045
|
+
console.log(chalk25.bold("\n\u{1F4C4} " + t("context.showTitle")));
|
|
5046
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
5047
|
+
if (!existsSync12(CONTEXT_PATH2)) {
|
|
5048
|
+
console.log(chalk25.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
5049
|
+
console.log(chalk25.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
|
|
4342
5050
|
return;
|
|
4343
5051
|
}
|
|
4344
|
-
const content =
|
|
5052
|
+
const content = readFileSync5(CONTEXT_PATH2, "utf-8");
|
|
4345
5053
|
console.log("\n" + content);
|
|
4346
5054
|
}
|
|
4347
5055
|
|
|
4348
5056
|
// src/commands/memory.ts
|
|
4349
|
-
import { existsSync as
|
|
4350
|
-
import
|
|
5057
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
5058
|
+
import chalk26 from "chalk";
|
|
4351
5059
|
var MEMORY_PATH = ".vhk/memory.json";
|
|
4352
5060
|
function loadMemories() {
|
|
4353
|
-
if (!
|
|
5061
|
+
if (!existsSync13(MEMORY_PATH)) return [];
|
|
4354
5062
|
try {
|
|
4355
5063
|
const parsed = readJsonFile(MEMORY_PATH);
|
|
4356
5064
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -4363,11 +5071,12 @@ function saveMemories(memories) {
|
|
|
4363
5071
|
writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
4364
5072
|
}
|
|
4365
5073
|
async function memoryAdd(content, tags) {
|
|
4366
|
-
console.log(
|
|
4367
|
-
console.log(
|
|
5074
|
+
console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.addTitle")));
|
|
5075
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
4368
5076
|
if (!content) {
|
|
4369
|
-
console.log(
|
|
4370
|
-
console.log(
|
|
5077
|
+
console.log(chalk26.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
5078
|
+
console.log(chalk26.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
|
|
5079
|
+
process.exitCode = 1;
|
|
4371
5080
|
return;
|
|
4372
5081
|
}
|
|
4373
5082
|
const memories = loadMemories();
|
|
@@ -4377,9 +5086,9 @@ async function memoryAdd(content, tags) {
|
|
|
4377
5086
|
tags: tags && tags.length > 0 ? tags : []
|
|
4378
5087
|
});
|
|
4379
5088
|
saveMemories(memories);
|
|
4380
|
-
console.log(
|
|
5089
|
+
console.log(chalk26.green(`
|
|
4381
5090
|
\u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
|
|
4382
|
-
console.log(
|
|
5091
|
+
console.log(chalk26.cyan(` \u{1F4DD} ${content}`));
|
|
4383
5092
|
printNextStep({
|
|
4384
5093
|
message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
|
|
4385
5094
|
command: "vhk memory list",
|
|
@@ -4387,24 +5096,24 @@ async function memoryAdd(content, tags) {
|
|
|
4387
5096
|
});
|
|
4388
5097
|
}
|
|
4389
5098
|
async function memoryList() {
|
|
4390
|
-
console.log(
|
|
4391
|
-
console.log(
|
|
5099
|
+
console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.listTitle")));
|
|
5100
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
4392
5101
|
const memories = loadMemories();
|
|
4393
5102
|
if (memories.length === 0) {
|
|
4394
|
-
console.log(
|
|
4395
|
-
console.log(
|
|
5103
|
+
console.log(chalk26.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
5104
|
+
console.log(chalk26.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
4396
5105
|
return;
|
|
4397
5106
|
}
|
|
4398
|
-
console.log(
|
|
5107
|
+
console.log(chalk26.cyan(`
|
|
4399
5108
|
\uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
|
|
4400
5109
|
`));
|
|
4401
5110
|
memories.forEach((m, index) => {
|
|
4402
5111
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4403
|
-
console.log(
|
|
5112
|
+
console.log(chalk26.white(` [${index + 1}] ${m.content}`));
|
|
4404
5113
|
if (m.tags && m.tags.length > 0) {
|
|
4405
|
-
console.log(
|
|
5114
|
+
console.log(chalk26.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
|
|
4406
5115
|
}
|
|
4407
|
-
console.log(
|
|
5116
|
+
console.log(chalk26.gray(` \u{1F4C5} ${date}`));
|
|
4408
5117
|
console.log("");
|
|
4409
5118
|
});
|
|
4410
5119
|
}
|
|
@@ -4412,26 +5121,44 @@ async function memoryRemove(indexStr) {
|
|
|
4412
5121
|
const memories = loadMemories();
|
|
4413
5122
|
const idx = parseInt(indexStr, 10) - 1;
|
|
4414
5123
|
if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
|
|
4415
|
-
console.log(
|
|
5124
|
+
console.log(chalk26.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
|
|
4416
5125
|
return;
|
|
4417
5126
|
}
|
|
4418
5127
|
const removed = memories.splice(idx, 1)[0];
|
|
4419
5128
|
saveMemories(memories);
|
|
4420
|
-
console.log(
|
|
4421
|
-
console.log(
|
|
5129
|
+
console.log(chalk26.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
|
|
5130
|
+
console.log(chalk26.gray(` ${removed.content}`));
|
|
4422
5131
|
}
|
|
4423
5132
|
|
|
4424
5133
|
// src/commands/brief.ts
|
|
4425
|
-
import { existsSync as
|
|
4426
|
-
import
|
|
5134
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync6 } from "fs";
|
|
5135
|
+
import chalk27 from "chalk";
|
|
4427
5136
|
var BRIEF_PATH = ".vhk/brief.md";
|
|
5137
|
+
function readProjectIdentity() {
|
|
5138
|
+
const out = {};
|
|
5139
|
+
try {
|
|
5140
|
+
if (existsSync14("RULES.md")) {
|
|
5141
|
+
const r = readFileSync6("RULES.md", "utf-8");
|
|
5142
|
+
const m = r.split("\n")[0].match(/^#\s*(.+?)(?:\s*—.*)?$/);
|
|
5143
|
+
if (m) out.name = m[1].trim();
|
|
5144
|
+
const d = r.match(/한 줄 설명:\s*(.+)/);
|
|
5145
|
+
if (d) out.description = d[1].trim();
|
|
5146
|
+
}
|
|
5147
|
+
if (!out.name && existsSync14("CLAUDE.md")) {
|
|
5148
|
+
const m = readFileSync6("CLAUDE.md", "utf-8").match(/#\s*기록 규칙\s*\((.+?)\)/);
|
|
5149
|
+
if (m) out.name = m[1].trim();
|
|
5150
|
+
}
|
|
5151
|
+
} catch {
|
|
5152
|
+
}
|
|
5153
|
+
return out;
|
|
5154
|
+
}
|
|
4428
5155
|
function git2(args) {
|
|
4429
5156
|
const result = safeExecFile("git", args);
|
|
4430
5157
|
return result.ok ? result.out : "";
|
|
4431
5158
|
}
|
|
4432
5159
|
async function brief() {
|
|
4433
|
-
console.log(
|
|
4434
|
-
console.log(
|
|
5160
|
+
console.log(chalk27.bold("\n\u{1F4CB} " + t("brief.title")));
|
|
5161
|
+
console.log(chalk27.gray("\u2500".repeat(40)));
|
|
4435
5162
|
const lines = [];
|
|
4436
5163
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
|
|
4437
5164
|
lines.push("");
|
|
@@ -4439,11 +5166,12 @@ async function brief() {
|
|
|
4439
5166
|
lines.push("");
|
|
4440
5167
|
try {
|
|
4441
5168
|
const pkg = readJsonFile("package.json");
|
|
5169
|
+
const id = readProjectIdentity();
|
|
4442
5170
|
lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
|
|
4443
5171
|
lines.push("");
|
|
4444
|
-
lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
|
|
5172
|
+
lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
|
|
4445
5173
|
lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
|
|
4446
|
-
lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
|
|
5174
|
+
lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
|
|
4447
5175
|
const deps = Object.keys(pkg.dependencies ?? {}).length;
|
|
4448
5176
|
const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
|
|
4449
5177
|
lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
|
|
@@ -4467,7 +5195,7 @@ async function brief() {
|
|
|
4467
5195
|
`- **\uBBF8\uCEE4\uBC0B \uBCC0\uACBD**: ${uncommitted ? `${uncommitted.split("\n").length}\uAC1C \uD30C\uC77C` : "\uC5C6\uC74C \u2705"}`
|
|
4468
5196
|
);
|
|
4469
5197
|
lines.push("");
|
|
4470
|
-
if (
|
|
5198
|
+
if (existsSync14(".vhk/memory.json")) {
|
|
4471
5199
|
try {
|
|
4472
5200
|
const memories = readJsonFile(".vhk/memory.json");
|
|
4473
5201
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
@@ -4484,7 +5212,7 @@ async function brief() {
|
|
|
4484
5212
|
} catch {
|
|
4485
5213
|
}
|
|
4486
5214
|
}
|
|
4487
|
-
if (
|
|
5215
|
+
if (existsSync14(".vhk/refs.json")) {
|
|
4488
5216
|
try {
|
|
4489
5217
|
const refs = readJsonFile(".vhk/refs.json");
|
|
4490
5218
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
@@ -4515,7 +5243,7 @@ async function brief() {
|
|
|
4515
5243
|
mkdirSync9(".vhk", { recursive: true });
|
|
4516
5244
|
writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
|
|
4517
5245
|
console.log("\n" + lines.join("\n"));
|
|
4518
|
-
console.log(
|
|
5246
|
+
console.log(chalk27.green(`
|
|
4519
5247
|
\u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
|
|
4520
5248
|
printNextStep({
|
|
4521
5249
|
message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -4525,11 +5253,11 @@ async function brief() {
|
|
|
4525
5253
|
}
|
|
4526
5254
|
|
|
4527
5255
|
// src/commands/start.ts
|
|
4528
|
-
import
|
|
4529
|
-
import
|
|
5256
|
+
import chalk28 from "chalk";
|
|
5257
|
+
import inquirer12 from "inquirer";
|
|
4530
5258
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4531
|
-
import { existsSync as
|
|
4532
|
-
import { join as
|
|
5259
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5260
|
+
import { join as join9 } from "path";
|
|
4533
5261
|
var VHK_FOOTPRINT_FILES = [
|
|
4534
5262
|
"CLAUDE.md",
|
|
4535
5263
|
".cursorrules",
|
|
@@ -4538,7 +5266,7 @@ var VHK_FOOTPRINT_FILES = [
|
|
|
4538
5266
|
"docs/PRD.md"
|
|
4539
5267
|
];
|
|
4540
5268
|
function detectExistingFootprint(cwd) {
|
|
4541
|
-
return VHK_FOOTPRINT_FILES.filter((rel) =>
|
|
5269
|
+
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync15(join9(cwd, rel)));
|
|
4542
5270
|
}
|
|
4543
5271
|
async function runGitInit(cwd) {
|
|
4544
5272
|
try {
|
|
@@ -4567,22 +5295,22 @@ async function runStep(label, fn) {
|
|
|
4567
5295
|
}
|
|
4568
5296
|
}
|
|
4569
5297
|
async function start(options = {}) {
|
|
4570
|
-
console.log(
|
|
5298
|
+
console.log(chalk28.bold(`
|
|
4571
5299
|
${ko.start.title}
|
|
4572
5300
|
`));
|
|
4573
|
-
console.log(
|
|
4574
|
-
console.log(
|
|
4575
|
-
console.log(
|
|
4576
|
-
console.log(
|
|
4577
|
-
console.log(
|
|
5301
|
+
console.log(chalk28.dim(ko.start.intro));
|
|
5302
|
+
console.log(chalk28.dim(` ${ko.start.step1}`));
|
|
5303
|
+
console.log(chalk28.dim(` ${ko.start.step2}`));
|
|
5304
|
+
console.log(chalk28.dim(` ${ko.start.step3}`));
|
|
5305
|
+
console.log(chalk28.dim(` ${ko.start.step4}`));
|
|
4578
5306
|
console.log();
|
|
4579
5307
|
const cwd = process.cwd();
|
|
4580
5308
|
const footprint = detectExistingFootprint(cwd);
|
|
4581
5309
|
if (footprint.length > 0 && !options.yes) {
|
|
4582
|
-
console.log(
|
|
4583
|
-
for (const f of footprint) console.log(
|
|
4584
|
-
console.log(
|
|
4585
|
-
const { proceedExisting } = await
|
|
5310
|
+
console.log(chalk28.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
|
|
5311
|
+
for (const f of footprint) console.log(chalk28.dim(` - ${f}`));
|
|
5312
|
+
console.log(chalk28.dim(" \uACC4\uC18D \uC9C4\uD589\uD558\uBA74 \uC77C\uBD80 \uD30C\uC77C(`.cursor/mcp.json`, `.vhk/context.md`)\uC740 \uAC31\uC2E0\xB7\uB36E\uC5B4\uC4F0\uAE30\uB429\uB2C8\uB2E4."));
|
|
5313
|
+
const { proceedExisting } = await inquirer12.prompt([{
|
|
4586
5314
|
type: "confirm",
|
|
4587
5315
|
name: "proceedExisting",
|
|
4588
5316
|
message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
@@ -4593,7 +5321,7 @@ ${ko.start.title}
|
|
|
4593
5321
|
return;
|
|
4594
5322
|
}
|
|
4595
5323
|
} else if (!options.yes) {
|
|
4596
|
-
const { proceed } = await
|
|
5324
|
+
const { proceed } = await inquirer12.prompt([{
|
|
4597
5325
|
type: "confirm",
|
|
4598
5326
|
name: "proceed",
|
|
4599
5327
|
message: ko.start.confirmStart,
|
|
@@ -4619,7 +5347,7 @@ ${ko.start.title}
|
|
|
4619
5347
|
await runStep("[3/4] vhk mcp-init", () => mcpInit());
|
|
4620
5348
|
log.step(ko.start.step4Header);
|
|
4621
5349
|
await runStep("[4/4] vhk context", () => context());
|
|
4622
|
-
console.log(
|
|
5350
|
+
console.log(chalk28.bold.green(`
|
|
4623
5351
|
${ko.start.allDone}
|
|
4624
5352
|
`));
|
|
4625
5353
|
printNextStep({
|
|
@@ -4629,15 +5357,15 @@ ${ko.start.allDone}
|
|
|
4629
5357
|
}
|
|
4630
5358
|
|
|
4631
5359
|
// src/commands/cloud.ts
|
|
4632
|
-
import
|
|
5360
|
+
import fs15 from "fs";
|
|
4633
5361
|
import os from "os";
|
|
4634
|
-
import
|
|
4635
|
-
import
|
|
5362
|
+
import path16 from "path";
|
|
5363
|
+
import chalk29 from "chalk";
|
|
4636
5364
|
|
|
4637
5365
|
// src/lib/vhk-cloud.ts
|
|
4638
5366
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
4639
|
-
import
|
|
4640
|
-
import
|
|
5367
|
+
import fs14 from "fs";
|
|
5368
|
+
import path15 from "path";
|
|
4641
5369
|
var DEFAULT_CLOUD_EXCLUDES = [
|
|
4642
5370
|
"memory.json",
|
|
4643
5371
|
// 개인 의사결정 메모
|
|
@@ -4655,17 +5383,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
|
|
|
4655
5383
|
function loadVhkignore(rootDir) {
|
|
4656
5384
|
const ig = (0, import_ignore.default)();
|
|
4657
5385
|
ig.add(DEFAULT_CLOUD_EXCLUDES);
|
|
4658
|
-
const ignorePath =
|
|
4659
|
-
if (
|
|
4660
|
-
ig.add(
|
|
5386
|
+
const ignorePath = path15.join(rootDir, ".vhkignore");
|
|
5387
|
+
if (fs14.existsSync(ignorePath)) {
|
|
5388
|
+
ig.add(fs14.readFileSync(ignorePath, "utf-8"));
|
|
4661
5389
|
}
|
|
4662
5390
|
return ig;
|
|
4663
5391
|
}
|
|
4664
5392
|
function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
|
|
4665
|
-
const vhkDir =
|
|
5393
|
+
const vhkDir = path15.join(rootDir, VHK_DIR2);
|
|
4666
5394
|
let entries;
|
|
4667
5395
|
try {
|
|
4668
|
-
entries =
|
|
5396
|
+
entries = fs14.readdirSync(vhkDir, { withFileTypes: true });
|
|
4669
5397
|
} catch {
|
|
4670
5398
|
return [];
|
|
4671
5399
|
}
|
|
@@ -4681,10 +5409,10 @@ function partitionGistFiles(gistFiles, ig) {
|
|
|
4681
5409
|
return { keep, excluded };
|
|
4682
5410
|
}
|
|
4683
5411
|
function readCloudConfig(rootDir) {
|
|
4684
|
-
const p =
|
|
4685
|
-
if (!
|
|
5412
|
+
const p = path15.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
|
|
5413
|
+
if (!fs14.existsSync(p)) return null;
|
|
4686
5414
|
try {
|
|
4687
|
-
const parsed = JSON.parse(
|
|
5415
|
+
const parsed = JSON.parse(fs14.readFileSync(p, "utf-8"));
|
|
4688
5416
|
if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
|
|
4689
5417
|
return { gistId: parsed.gistId };
|
|
4690
5418
|
}
|
|
@@ -4694,24 +5422,24 @@ function readCloudConfig(rootDir) {
|
|
|
4694
5422
|
}
|
|
4695
5423
|
}
|
|
4696
5424
|
function writeCloudConfig(rootDir, config) {
|
|
4697
|
-
const vhkDir =
|
|
4698
|
-
|
|
4699
|
-
const p =
|
|
4700
|
-
|
|
5425
|
+
const vhkDir = path15.join(rootDir, VHK_DIR2);
|
|
5426
|
+
fs14.mkdirSync(vhkDir, { recursive: true });
|
|
5427
|
+
const p = path15.join(vhkDir, CLOUD_CONFIG_FILE);
|
|
5428
|
+
fs14.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4701
5429
|
}
|
|
4702
5430
|
|
|
4703
5431
|
// src/commands/cloud.ts
|
|
4704
5432
|
function ensureGhReady() {
|
|
4705
5433
|
const ver = safeExecFile("gh", ["--version"]);
|
|
4706
5434
|
if (!ver.ok) {
|
|
4707
|
-
console.log(
|
|
4708
|
-
console.log(
|
|
5435
|
+
console.log(chalk29.red(` ${ko.cloud.noGh}`));
|
|
5436
|
+
console.log(chalk29.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
|
|
4709
5437
|
return false;
|
|
4710
5438
|
}
|
|
4711
5439
|
const auth = safeExecFile("gh", ["auth", "status"]);
|
|
4712
5440
|
if (!auth.ok) {
|
|
4713
|
-
console.log(
|
|
4714
|
-
console.log(
|
|
5441
|
+
console.log(chalk29.red(` ${ko.cloud.noAuth}`));
|
|
5442
|
+
console.log(chalk29.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
|
|
4715
5443
|
return false;
|
|
4716
5444
|
}
|
|
4717
5445
|
return true;
|
|
@@ -4724,29 +5452,29 @@ function parseGistId(output) {
|
|
|
4724
5452
|
return null;
|
|
4725
5453
|
}
|
|
4726
5454
|
async function cloudPush() {
|
|
4727
|
-
console.log(
|
|
5455
|
+
console.log(chalk29.bold(`
|
|
4728
5456
|
${ko.cloud.pushTitle}
|
|
4729
5457
|
`));
|
|
4730
5458
|
const cwd = process.cwd();
|
|
4731
|
-
if (!
|
|
4732
|
-
console.log(
|
|
5459
|
+
if (!fs15.existsSync(path16.join(cwd, VHK_DIR2))) {
|
|
5460
|
+
console.log(chalk29.yellow(` ${ko.cloud.noVhkDir}`));
|
|
4733
5461
|
return;
|
|
4734
5462
|
}
|
|
4735
5463
|
const ig = loadVhkignore(cwd);
|
|
4736
5464
|
const files = collectVhkFiles(cwd, ig);
|
|
4737
5465
|
if (files.length === 0) {
|
|
4738
|
-
console.log(
|
|
5466
|
+
console.log(chalk29.yellow(` ${ko.cloud.nothingToSync}`));
|
|
4739
5467
|
return;
|
|
4740
5468
|
}
|
|
4741
5469
|
if (!ensureGhReady()) {
|
|
4742
5470
|
process.exitCode = 1;
|
|
4743
5471
|
return;
|
|
4744
5472
|
}
|
|
4745
|
-
const filePaths = files.map((f) =>
|
|
4746
|
-
console.log(
|
|
5473
|
+
const filePaths = files.map((f) => path16.join(cwd, VHK_DIR2, f));
|
|
5474
|
+
console.log(chalk29.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
|
|
4747
5475
|
`));
|
|
4748
5476
|
const existing = readCloudConfig(cwd);
|
|
4749
|
-
const desc = `vhk .vhk backup \u2014 ${
|
|
5477
|
+
const desc = `vhk .vhk backup \u2014 ${path16.basename(cwd)}`;
|
|
4750
5478
|
if (existing) {
|
|
4751
5479
|
const gistFiles = listGistFiles(existing.gistId);
|
|
4752
5480
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -4755,8 +5483,8 @@ ${ko.cloud.pushTitle}
|
|
|
4755
5483
|
const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
|
|
4756
5484
|
const res2 = safeExecFile("gh", args);
|
|
4757
5485
|
if (!res2.ok) {
|
|
4758
|
-
console.log(
|
|
4759
|
-
console.log(
|
|
5486
|
+
console.log(chalk29.red(` ${ko.cloud.pushFail}: ${name}`));
|
|
5487
|
+
console.log(chalk29.dim(` ${res2.err}`));
|
|
4760
5488
|
process.exitCode = 1;
|
|
4761
5489
|
return;
|
|
4762
5490
|
}
|
|
@@ -4771,15 +5499,15 @@ ${ko.cloud.pushTitle}
|
|
|
4771
5499
|
if (!purgeFailed.includes(name)) purgeFailed.push(name);
|
|
4772
5500
|
}
|
|
4773
5501
|
}
|
|
4774
|
-
console.log(
|
|
4775
|
-
console.log(
|
|
5502
|
+
console.log(chalk29.green.bold(` ${ko.cloud.pushDone}`));
|
|
5503
|
+
console.log(chalk29.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
|
|
4776
5504
|
if (excluded.length > 0) {
|
|
4777
5505
|
const purged = excluded.filter((n) => !purgeFailed.includes(n));
|
|
4778
5506
|
if (purged.length > 0) {
|
|
4779
|
-
console.log(
|
|
5507
|
+
console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
|
|
4780
5508
|
}
|
|
4781
5509
|
if (purgeFailed.length > 0) {
|
|
4782
|
-
console.log(
|
|
5510
|
+
console.log(chalk29.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
5511
|
}
|
|
4784
5512
|
}
|
|
4785
5513
|
printPushNext();
|
|
@@ -4787,32 +5515,32 @@ ${ko.cloud.pushTitle}
|
|
|
4787
5515
|
}
|
|
4788
5516
|
const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
|
|
4789
5517
|
if (!res.ok) {
|
|
4790
|
-
console.log(
|
|
4791
|
-
console.log(
|
|
5518
|
+
console.log(chalk29.red(` ${ko.cloud.pushFail}`));
|
|
5519
|
+
console.log(chalk29.dim(` ${res.err || res.out}`));
|
|
4792
5520
|
process.exitCode = 1;
|
|
4793
5521
|
return;
|
|
4794
5522
|
}
|
|
4795
5523
|
const gistId = parseGistId(res.out);
|
|
4796
5524
|
if (!gistId) {
|
|
4797
|
-
console.log(
|
|
4798
|
-
console.log(
|
|
5525
|
+
console.log(chalk29.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
|
|
5526
|
+
console.log(chalk29.dim(` \uCD9C\uB825: ${res.out}`));
|
|
4799
5527
|
process.exitCode = 1;
|
|
4800
5528
|
return;
|
|
4801
5529
|
}
|
|
4802
5530
|
writeCloudConfig(cwd, { gistId });
|
|
4803
|
-
console.log(
|
|
4804
|
-
console.log(
|
|
5531
|
+
console.log(chalk29.green.bold(` ${ko.cloud.pushDone}`));
|
|
5532
|
+
console.log(chalk29.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
|
|
4805
5533
|
printPushNext();
|
|
4806
5534
|
}
|
|
4807
5535
|
async function cloudPull(gistIdArg) {
|
|
4808
|
-
console.log(
|
|
5536
|
+
console.log(chalk29.bold(`
|
|
4809
5537
|
${ko.cloud.pullTitle}
|
|
4810
5538
|
`));
|
|
4811
5539
|
const cwd = process.cwd();
|
|
4812
5540
|
const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
|
|
4813
5541
|
if (!gistId) {
|
|
4814
|
-
console.log(
|
|
4815
|
-
console.log(
|
|
5542
|
+
console.log(chalk29.yellow(` ${ko.cloud.noGistId}`));
|
|
5543
|
+
console.log(chalk29.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
|
|
4816
5544
|
return;
|
|
4817
5545
|
}
|
|
4818
5546
|
if (!ensureGhReady()) {
|
|
@@ -4821,34 +5549,34 @@ ${ko.cloud.pullTitle}
|
|
|
4821
5549
|
}
|
|
4822
5550
|
const allNames = listGistFiles(gistId);
|
|
4823
5551
|
if (allNames.length === 0) {
|
|
4824
|
-
console.log(
|
|
5552
|
+
console.log(chalk29.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
|
|
4825
5553
|
process.exitCode = 1;
|
|
4826
5554
|
return;
|
|
4827
5555
|
}
|
|
4828
5556
|
const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
|
|
4829
5557
|
if (skipped.length > 0) {
|
|
4830
|
-
console.log(
|
|
5558
|
+
console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
|
|
4831
5559
|
}
|
|
4832
5560
|
if (names.length === 0) {
|
|
4833
|
-
console.log(
|
|
5561
|
+
console.log(chalk29.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
|
|
4834
5562
|
return;
|
|
4835
5563
|
}
|
|
4836
|
-
const vhkDir =
|
|
4837
|
-
|
|
5564
|
+
const vhkDir = path16.join(cwd, VHK_DIR2);
|
|
5565
|
+
fs15.mkdirSync(vhkDir, { recursive: true });
|
|
4838
5566
|
let restored = 0;
|
|
4839
5567
|
for (const name of names) {
|
|
4840
5568
|
const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
|
|
4841
5569
|
if (!res.ok) {
|
|
4842
|
-
console.log(
|
|
4843
|
-
console.log(
|
|
5570
|
+
console.log(chalk29.red(` ${ko.cloud.pullFail}: ${name}`));
|
|
5571
|
+
console.log(chalk29.dim(` ${res.err}`));
|
|
4844
5572
|
continue;
|
|
4845
5573
|
}
|
|
4846
|
-
|
|
5574
|
+
fs15.writeFileSync(path16.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
|
|
4847
5575
|
restored++;
|
|
4848
5576
|
}
|
|
4849
5577
|
writeCloudConfig(cwd, { gistId });
|
|
4850
|
-
console.log(
|
|
4851
|
-
console.log(
|
|
5578
|
+
console.log(chalk29.green.bold(` ${ko.cloud.pullDone}`));
|
|
5579
|
+
console.log(chalk29.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
|
|
4852
5580
|
printNextStep({
|
|
4853
5581
|
message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
|
|
4854
5582
|
command: "vhk \uB9E5\uB77D",
|
|
@@ -4860,9 +5588,9 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4860
5588
|
const body = JSON.stringify({
|
|
4861
5589
|
files: Object.fromEntries(names.map((n) => [n, null]))
|
|
4862
5590
|
});
|
|
4863
|
-
const tmp =
|
|
5591
|
+
const tmp = path16.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
|
|
4864
5592
|
try {
|
|
4865
|
-
|
|
5593
|
+
fs15.writeFileSync(tmp, body, "utf-8");
|
|
4866
5594
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4867
5595
|
const res = safeExecFile(
|
|
4868
5596
|
"gh",
|
|
@@ -4874,7 +5602,7 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4874
5602
|
return false;
|
|
4875
5603
|
} finally {
|
|
4876
5604
|
try {
|
|
4877
|
-
|
|
5605
|
+
fs15.unlinkSync(tmp);
|
|
4878
5606
|
} catch {
|
|
4879
5607
|
}
|
|
4880
5608
|
}
|
|
@@ -4895,6 +5623,198 @@ function printPushNext() {
|
|
|
4895
5623
|
});
|
|
4896
5624
|
}
|
|
4897
5625
|
|
|
5626
|
+
// src/commands/help.ts
|
|
5627
|
+
import chalk30 from "chalk";
|
|
5628
|
+
var QUICK_ACTIONS = [
|
|
5629
|
+
{ say: "\uC0C1\uD0DC \uC54C\uB824\uC918", does: "vhk status" },
|
|
5630
|
+
{ say: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?", does: "vhk diff" },
|
|
5631
|
+
{ say: "\uC800\uC7A5\uD574\uC918", does: "vhk save" },
|
|
5632
|
+
{ say: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574\uC918", does: "vhk recap" },
|
|
5633
|
+
{ say: "\uB2E4\uC74C\uC5D0 \uBB50 \uD558\uBA74 \uB3FC?", does: "vhk goal next" },
|
|
5634
|
+
{ say: "\uADDC\uCE59 \uB3D9\uAE30\uD654\uD574\uC918", does: "vhk sync" },
|
|
5635
|
+
{ say: "\uBC31\uC5C5 \uBCF5\uC6D0\uD574\uC918", does: "vhk restore" },
|
|
5636
|
+
{ say: "\uBCF4\uC548 \uC810\uAC80\uD574\uC918", does: "vhk secure scan" },
|
|
5637
|
+
{ say: "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791", does: "vhk start" },
|
|
5638
|
+
{ say: "\uC804\uCCB4 \uBA85\uB839\uC5B4 \uBCF4\uAE30", does: "vhk --help" }
|
|
5639
|
+
];
|
|
5640
|
+
function quickActions() {
|
|
5641
|
+
console.log(chalk30.bold("\n\u{1F9ED} VHK \u2014 \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4 (quick actions)"));
|
|
5642
|
+
console.log(chalk30.gray("\u2500".repeat(40)));
|
|
5643
|
+
for (const a of QUICK_ACTIONS) {
|
|
5644
|
+
console.log(` "${chalk30.cyan(a.say)}" \u2192 ${chalk30.dim(a.does)}`);
|
|
5645
|
+
}
|
|
5646
|
+
console.log(chalk30.gray("\n \uC804\uCCB4 \uBA85\uB839\uC740 `vhk --help` \uB610\uB294 COMMANDS.md \uB97C \uBCF4\uC138\uC694."));
|
|
5647
|
+
console.log("");
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
// src/commands/mode.ts
|
|
5651
|
+
import chalk31 from "chalk";
|
|
5652
|
+
|
|
5653
|
+
// src/lib/config.ts
|
|
5654
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
5655
|
+
import { join as join10 } from "path";
|
|
5656
|
+
|
|
5657
|
+
// src/lib/safety-mode.ts
|
|
5658
|
+
var SAFETY_MODES = ["lite", "standard", "strict"];
|
|
5659
|
+
var DEFAULT_SAFETY_MODE = "standard";
|
|
5660
|
+
var SAFETY_MODE_DESC = {
|
|
5661
|
+
lite: "\uACBD\uACE0\uB9CC \u2014 \uC704\uD5D8 \uC791\uC5C5\uB3C4 \uB9C9\uC9C0 \uC54A\uACE0 \uACBD\uACE0\uB9CC \uD45C\uC2DC (\uBE60\uB978 \uBC18\uBCF5\uC6A9)",
|
|
5662
|
+
standard: "\uAE30\uBCF8 \u2014 \uC704\uD5D8 \uC791\uC5C5\uC740 CLI \uD655\uC778\xB7MCP/\uC790\uC5F0\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30",
|
|
5663
|
+
strict: "\uC5C4\uACA9 \u2014 \uB354 \uB9CE\uC740 \uC791\uC5C5(\uC800\uC7A5/\uB3D9\uAE30\uD654 \uB4F1)\uC5D0\uB3C4 \uD655\uC778 \uC694\uAD6C"
|
|
5664
|
+
};
|
|
5665
|
+
function isSafetyMode(value) {
|
|
5666
|
+
return typeof value === "string" && SAFETY_MODES.includes(value);
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
// src/lib/config.ts
|
|
5670
|
+
var CONFIG_DIR = ".vhk";
|
|
5671
|
+
var CONFIG_PATH = join10(CONFIG_DIR, "config.json");
|
|
5672
|
+
var DEFAULT_CONFIG = { safetyMode: DEFAULT_SAFETY_MODE };
|
|
5673
|
+
function readConfig(rootDir = process.cwd()) {
|
|
5674
|
+
const full = join10(rootDir, CONFIG_PATH);
|
|
5675
|
+
if (!existsSync16(full)) return { ...DEFAULT_CONFIG };
|
|
5676
|
+
try {
|
|
5677
|
+
const raw = readJsonFile(full);
|
|
5678
|
+
return {
|
|
5679
|
+
safetyMode: isSafetyMode(raw.safetyMode) ? raw.safetyMode : DEFAULT_CONFIG.safetyMode
|
|
5680
|
+
};
|
|
5681
|
+
} catch {
|
|
5682
|
+
return { ...DEFAULT_CONFIG };
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5685
|
+
function writeConfig(config, rootDir = process.cwd()) {
|
|
5686
|
+
mkdirSync10(join10(rootDir, CONFIG_DIR), { recursive: true });
|
|
5687
|
+
writeFileSync10(join10(rootDir, CONFIG_PATH), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
// src/commands/mode.ts
|
|
5691
|
+
async function mode(target) {
|
|
5692
|
+
console.log(chalk31.bold("\n\u{1F6E1}\uFE0F Safety Mode"));
|
|
5693
|
+
console.log(chalk31.gray("\u2500".repeat(40)));
|
|
5694
|
+
const current = readConfig().safetyMode;
|
|
5695
|
+
if (!target) {
|
|
5696
|
+
console.log(chalk31.cyan(`
|
|
5697
|
+
\uD604\uC7AC \uBAA8\uB4DC: ${chalk31.bold(current)}`));
|
|
5698
|
+
console.log(chalk31.dim(` ${SAFETY_MODE_DESC[current]}`));
|
|
5699
|
+
console.log("");
|
|
5700
|
+
for (const m of SAFETY_MODES) {
|
|
5701
|
+
const mark = m === current ? "\u25CF" : "\u25CB";
|
|
5702
|
+
console.log(` ${mark} ${m.padEnd(9)} ${chalk31.dim(SAFETY_MODE_DESC[m])}`);
|
|
5703
|
+
}
|
|
5704
|
+
printNextStep({
|
|
5705
|
+
message: "\uBAA8\uB4DC\uB97C \uBC14\uAFB8\uB824\uBA74:",
|
|
5706
|
+
command: "vhk mode strict",
|
|
5707
|
+
cursorHint: "\uC548\uC804 \uBAA8\uB4DC strict\uB85C \uBC14\uAFD4\uC918"
|
|
5708
|
+
});
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
if (!isSafetyMode(target)) {
|
|
5712
|
+
console.log(chalk31.red(`
|
|
5713
|
+
\u274C \uC54C \uC218 \uC5C6\uB294 \uBAA8\uB4DC: ${target}`));
|
|
5714
|
+
console.log(chalk31.dim(` \uAC00\uB2A5: ${SAFETY_MODES.join(" | ")}`));
|
|
5715
|
+
process.exitCode = 1;
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
writeConfig({ ...readConfig(), safetyMode: target });
|
|
5719
|
+
console.log(chalk31.green(`
|
|
5720
|
+
\u2705 Safety Mode \u2192 ${chalk31.bold(target)}`));
|
|
5721
|
+
console.log(chalk31.dim(` ${SAFETY_MODE_DESC[target]}`));
|
|
5722
|
+
}
|
|
5723
|
+
|
|
5724
|
+
// src/commands/verify.ts
|
|
5725
|
+
import chalk32 from "chalk";
|
|
5726
|
+
function verificationChecklist() {
|
|
5727
|
+
return [
|
|
5728
|
+
"\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
|
|
5729
|
+
"\uD14C\uC2A4\uD2B8 \u2014 pnpm run test:run",
|
|
5730
|
+
"\uBE4C\uB4DC \u2014 pnpm run build",
|
|
5731
|
+
"\uBCF4\uC548 \uC2A4\uCE94 \u2014 vhk secure scan"
|
|
5732
|
+
];
|
|
5733
|
+
}
|
|
5734
|
+
async function verify() {
|
|
5735
|
+
console.log(chalk32.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
|
|
5736
|
+
console.log(chalk32.gray("\u2500".repeat(40)));
|
|
5737
|
+
const mode2 = readConfig().safetyMode;
|
|
5738
|
+
console.log(chalk32.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
|
|
5739
|
+
console.log(chalk32.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
|
|
5740
|
+
for (const item of verificationChecklist()) {
|
|
5741
|
+
console.log(` \u2610 ${item}`);
|
|
5742
|
+
}
|
|
5743
|
+
console.log(chalk32.dim("\n \u203B \uBA54\uD0C0\uB7EC\uB108(\uC790\uB3D9 \uC2E4\uD589) \uC790\uB9AC \u2014 \uD604\uC7AC\uB294 \uBB36\uC74C \uC548\uB0B4\uB9CC(lite)."));
|
|
5744
|
+
printNextStep({
|
|
5745
|
+
message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
|
|
5746
|
+
command: "vhk save",
|
|
5747
|
+
cursorHint: "\uC800\uC7A5\uD574\uC918"
|
|
5748
|
+
});
|
|
5749
|
+
}
|
|
5750
|
+
|
|
5751
|
+
// src/lib/risk-policy.ts
|
|
5752
|
+
var HIGH_RISK_ACTIONS = [
|
|
5753
|
+
"undo",
|
|
5754
|
+
"deploy",
|
|
5755
|
+
"publish",
|
|
5756
|
+
"migrate",
|
|
5757
|
+
"cloud-pull",
|
|
5758
|
+
"resume",
|
|
5759
|
+
"env-write",
|
|
5760
|
+
"delete"
|
|
5761
|
+
];
|
|
5762
|
+
var STRICT_EXTRA_ACTIONS = /* @__PURE__ */ new Set(["save", "sync"]);
|
|
5763
|
+
var NL_GUARDED_ACTIONS = {
|
|
5764
|
+
undo: "undo",
|
|
5765
|
+
deploy: "deploy",
|
|
5766
|
+
publish: "publish",
|
|
5767
|
+
migrate: "migrate",
|
|
5768
|
+
"cloud-pull": "cloud-pull",
|
|
5769
|
+
env: "env-write",
|
|
5770
|
+
save: "save",
|
|
5771
|
+
sync: "sync"
|
|
5772
|
+
};
|
|
5773
|
+
function isHighRisk(action) {
|
|
5774
|
+
return HIGH_RISK_ACTIONS.includes(action);
|
|
5775
|
+
}
|
|
5776
|
+
function resolveGuard(action, mode2, channel) {
|
|
5777
|
+
const guarded = isHighRisk(action) || mode2 === "strict" && STRICT_EXTRA_ACTIONS.has(action);
|
|
5778
|
+
if (!guarded) return "allow";
|
|
5779
|
+
if (mode2 === "lite") return "warn";
|
|
5780
|
+
return channel === "cli" ? "confirm" : "preview";
|
|
5781
|
+
}
|
|
5782
|
+
|
|
5783
|
+
// src/lib/safety-guard.ts
|
|
5784
|
+
async function runGuarded(action, deps, run) {
|
|
5785
|
+
const mode2 = deps.mode ?? readConfig().safetyMode;
|
|
5786
|
+
const log2 = deps.log ?? (() => {
|
|
5787
|
+
});
|
|
5788
|
+
const guard = resolveGuard(action, mode2, deps.channel);
|
|
5789
|
+
if (guard === "allow") {
|
|
5790
|
+
return { outcome: { ran: true, guard, reason: "low-risk" }, result: await run() };
|
|
5791
|
+
}
|
|
5792
|
+
if (guard === "warn") {
|
|
5793
|
+
log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uBAA8\uB4DC: \uACBD\uACE0\uB9CC \uD558\uACE0 \uC9C4\uD589\uD569\uB2C8\uB2E4.`);
|
|
5794
|
+
return { outcome: { ran: true, guard, reason: "lite-warn" }, result: await run() };
|
|
5795
|
+
}
|
|
5796
|
+
if (guard === "confirm") {
|
|
5797
|
+
if (deps.approved === true) {
|
|
5798
|
+
return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
|
|
5799
|
+
}
|
|
5800
|
+
const tty = deps.isTTY ?? !!process.stdout.isTTY;
|
|
5801
|
+
if (tty && deps.confirm) {
|
|
5802
|
+
const ok = await deps.confirm();
|
|
5803
|
+
if (ok) return { outcome: { ran: true, guard, reason: "confirmed" }, result: await run() };
|
|
5804
|
+
log2(`\uCDE8\uC18C\uB428 \u2014 ${action} \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
5805
|
+
return { outcome: { ran: false, guard, reason: "declined" } };
|
|
5806
|
+
}
|
|
5807
|
+
log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 \uD655\uC778 \uBD88\uAC00(\uBE44\uB300\uD654\uD615). \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (--yes \uB85C \uBA85\uC2DC \uC2B9\uC778)`);
|
|
5808
|
+
return { outcome: { ran: false, guard, reason: "no-confirm" } };
|
|
5809
|
+
}
|
|
5810
|
+
log2(`\u{1F50E} \uC704\uD5D8 \uC791\uC5C5(${action}) \uBBF8\uB9AC\uBCF4\uAE30 \u2014 \uC2E4\uD589 \uC804 \uD655\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (Safety Mode: ${mode2})`);
|
|
5811
|
+
if (deps.approved === true) {
|
|
5812
|
+
return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
|
|
5813
|
+
}
|
|
5814
|
+
log2(`\uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 \u2014 \uBA85\uC2DC\uC801 \uD655\uC778(\uC2B9\uC778 \uD50C\uB798\uADF8) \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`);
|
|
5815
|
+
return { outcome: { ran: false, guard, reason: "preview-no-approve" } };
|
|
5816
|
+
}
|
|
5817
|
+
|
|
4898
5818
|
// src/lib/nlp-run.ts
|
|
4899
5819
|
async function dispatchNlpRoute(route, input) {
|
|
4900
5820
|
switch (route.command) {
|
|
@@ -4925,6 +5845,8 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4925
5845
|
return save();
|
|
4926
5846
|
case "undo":
|
|
4927
5847
|
return undo();
|
|
5848
|
+
case "restore":
|
|
5849
|
+
return restore(route.args?.[0]);
|
|
4928
5850
|
case "status":
|
|
4929
5851
|
return status();
|
|
4930
5852
|
case "diff":
|
|
@@ -4974,111 +5896,167 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4974
5896
|
if (sub === "done") return goalDone({});
|
|
4975
5897
|
return goalList();
|
|
4976
5898
|
}
|
|
5899
|
+
case "help":
|
|
5900
|
+
return quickActions();
|
|
5901
|
+
case "mode":
|
|
5902
|
+
return mode();
|
|
5903
|
+
case "verify":
|
|
5904
|
+
return verify();
|
|
4977
5905
|
}
|
|
4978
5906
|
}
|
|
5907
|
+
var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
|
|
5908
|
+
"start",
|
|
5909
|
+
"init"
|
|
5910
|
+
]);
|
|
5911
|
+
function requiresConfirmation(route) {
|
|
5912
|
+
return route.confidence === "low" || STATE_CHANGING_COMMANDS.has(route.command);
|
|
5913
|
+
}
|
|
4979
5914
|
async function runNaturalLanguageRoute(input) {
|
|
4980
5915
|
const route = routeNaturalLanguage(input);
|
|
4981
5916
|
if (!route) {
|
|
4982
|
-
console.log(
|
|
5917
|
+
console.log(chalk33.yellow(`
|
|
4983
5918
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
4984
5919
|
`));
|
|
4985
5920
|
return;
|
|
4986
5921
|
}
|
|
4987
5922
|
console.log("");
|
|
4988
|
-
console.log(
|
|
4989
|
-
console.log(
|
|
4990
|
-
if (route
|
|
4991
|
-
const { confirm } = await
|
|
5923
|
+
console.log(chalk33.cyan(` \u{1F4AC} "${input}"`));
|
|
5924
|
+
console.log(chalk33.cyan(` \u2192 ${route.explanation}`));
|
|
5925
|
+
if (requiresConfirmation(route)) {
|
|
5926
|
+
const { confirm } = await inquirer13.prompt([{
|
|
4992
5927
|
type: "confirm",
|
|
4993
5928
|
name: "confirm",
|
|
4994
5929
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
4995
5930
|
default: true
|
|
4996
5931
|
}]);
|
|
4997
5932
|
if (!confirm) {
|
|
4998
|
-
console.log(
|
|
5933
|
+
console.log(chalk33.dim(` ${ko.nlp.menuHint}`));
|
|
4999
5934
|
return;
|
|
5000
5935
|
}
|
|
5001
5936
|
}
|
|
5002
5937
|
console.log("");
|
|
5938
|
+
const riskAction = NL_GUARDED_ACTIONS[route.command];
|
|
5939
|
+
if (riskAction) {
|
|
5940
|
+
await runGuarded(
|
|
5941
|
+
riskAction,
|
|
5942
|
+
{ channel: "nl", approved: false, log: (m) => console.log(chalk33.yellow(` ${m}`)) },
|
|
5943
|
+
() => dispatchNlpRoute(route, input)
|
|
5944
|
+
);
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5003
5947
|
await dispatchNlpRoute(route, input);
|
|
5004
5948
|
}
|
|
5005
5949
|
|
|
5006
5950
|
// src/commands/agent.ts
|
|
5007
|
-
import
|
|
5951
|
+
import chalk34 from "chalk";
|
|
5008
5952
|
function activeGoalId() {
|
|
5009
5953
|
const goals = listGoals("goals");
|
|
5010
5954
|
const id = selectActiveId(goals);
|
|
5011
5955
|
return id ?? void 0;
|
|
5012
5956
|
}
|
|
5013
5957
|
async function blocker(description) {
|
|
5014
|
-
console.log(
|
|
5958
|
+
console.log(chalk34.bold(`
|
|
5015
5959
|
${ko.agent.blockerTitle}
|
|
5016
5960
|
`));
|
|
5017
5961
|
if (!description || !description.trim()) {
|
|
5018
|
-
console.log(
|
|
5019
|
-
console.log(
|
|
5962
|
+
console.log(chalk34.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5963
|
+
console.log(chalk34.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
|
|
5020
5964
|
process.exitCode = 1;
|
|
5021
5965
|
return;
|
|
5022
5966
|
}
|
|
5023
5967
|
const goalId = activeGoalId();
|
|
5024
5968
|
const r = appendBlocker(description, goalId);
|
|
5025
|
-
console.log(
|
|
5969
|
+
console.log(chalk34.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
5026
5970
|
if (r.hardStopTripped) {
|
|
5027
|
-
console.log(
|
|
5028
|
-
console.log(
|
|
5971
|
+
console.log(chalk34.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
|
|
5972
|
+
console.log(chalk34.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
|
|
5029
5973
|
process.exitCode = 2;
|
|
5030
5974
|
}
|
|
5031
5975
|
}
|
|
5032
5976
|
async function learn(lesson) {
|
|
5033
|
-
console.log(
|
|
5977
|
+
console.log(chalk34.bold(`
|
|
5034
5978
|
${ko.agent.learnTitle}
|
|
5035
5979
|
`));
|
|
5036
5980
|
if (!lesson || !lesson.trim()) {
|
|
5037
|
-
console.log(
|
|
5038
|
-
console.log(
|
|
5981
|
+
console.log(chalk34.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5982
|
+
console.log(chalk34.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
|
|
5039
5983
|
process.exitCode = 1;
|
|
5040
5984
|
return;
|
|
5041
5985
|
}
|
|
5042
5986
|
const goalId = activeGoalId();
|
|
5043
5987
|
appendLearning(lesson, goalId);
|
|
5044
|
-
console.log(
|
|
5988
|
+
console.log(chalk34.green(" \u2705 learnings.md append."));
|
|
5045
5989
|
console.log(
|
|
5046
|
-
|
|
5990
|
+
chalk34.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
|
|
5047
5991
|
);
|
|
5048
5992
|
}
|
|
5049
5993
|
async function resume(opts = {}) {
|
|
5050
|
-
console.log(
|
|
5994
|
+
console.log(chalk34.bold(`
|
|
5051
5995
|
${ko.agent.resumeTitle}
|
|
5052
5996
|
`));
|
|
5053
5997
|
if (!isHardStopActive()) {
|
|
5054
|
-
console.log(
|
|
5998
|
+
console.log(chalk34.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
|
|
5055
5999
|
return;
|
|
5056
6000
|
}
|
|
5057
6001
|
const reason = readHardStopReason();
|
|
5058
6002
|
if (reason) {
|
|
5059
|
-
console.log(
|
|
5060
|
-
console.log(
|
|
6003
|
+
console.log(chalk34.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
|
|
6004
|
+
console.log(chalk34.dim(` ${reason.split("\n").join("\n ")}`));
|
|
5061
6005
|
console.log("");
|
|
5062
6006
|
}
|
|
5063
6007
|
if (!opts.confirm) {
|
|
5064
6008
|
console.log(
|
|
5065
|
-
|
|
6009
|
+
chalk34.red(
|
|
5066
6010
|
" \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
|
|
5067
6011
|
)
|
|
5068
6012
|
);
|
|
5069
|
-
console.log(
|
|
6013
|
+
console.log(chalk34.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
|
|
5070
6014
|
process.exitCode = 1;
|
|
5071
6015
|
return;
|
|
5072
6016
|
}
|
|
5073
6017
|
const removed = clearHardStop();
|
|
5074
6018
|
if (removed) {
|
|
5075
|
-
console.log(
|
|
6019
|
+
console.log(chalk34.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
5076
6020
|
} else {
|
|
5077
|
-
console.log(
|
|
6021
|
+
console.log(chalk34.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
|
|
5078
6022
|
}
|
|
5079
6023
|
}
|
|
5080
6024
|
|
|
5081
6025
|
// src/index.ts
|
|
6026
|
+
async function guardCli(action, approved, run) {
|
|
6027
|
+
if (!ensureNotHardStopped(action)) return;
|
|
6028
|
+
await runGuarded(
|
|
6029
|
+
action,
|
|
6030
|
+
{
|
|
6031
|
+
channel: "cli",
|
|
6032
|
+
approved,
|
|
6033
|
+
confirm: async () => {
|
|
6034
|
+
const { ok } = await inquirer14.prompt([{
|
|
6035
|
+
type: "confirm",
|
|
6036
|
+
name: "ok",
|
|
6037
|
+
message: `\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action})\uC744 \uC2E4\uD589\uD560\uAE4C\uC694?`,
|
|
6038
|
+
default: false
|
|
6039
|
+
}]);
|
|
6040
|
+
return ok;
|
|
6041
|
+
},
|
|
6042
|
+
log: (m) => console.log(chalk35.yellow(` ${m}`))
|
|
6043
|
+
},
|
|
6044
|
+
run
|
|
6045
|
+
);
|
|
6046
|
+
}
|
|
6047
|
+
async function guardCliDefer(action, approved, run) {
|
|
6048
|
+
await runGuarded(
|
|
6049
|
+
action,
|
|
6050
|
+
{
|
|
6051
|
+
channel: "cli",
|
|
6052
|
+
approved,
|
|
6053
|
+
// TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
|
|
6054
|
+
confirm: async () => !!process.stdout.isTTY,
|
|
6055
|
+
log: (m) => console.log(chalk35.yellow(` ${m}`))
|
|
6056
|
+
},
|
|
6057
|
+
run
|
|
6058
|
+
);
|
|
6059
|
+
}
|
|
5082
6060
|
var program = new Command();
|
|
5083
6061
|
var defaultHelp = new Help();
|
|
5084
6062
|
var KO_ALIASES = {
|
|
@@ -5093,6 +6071,7 @@ var KO_ALIASES = {
|
|
|
5093
6071
|
doctor: "\uD658\uACBD",
|
|
5094
6072
|
save: "\uC800\uC7A5",
|
|
5095
6073
|
undo: "\uB418\uB3CC\uB9AC\uAE30",
|
|
6074
|
+
restore: "\uBCF5\uC6D0",
|
|
5096
6075
|
status: "\uC0C1\uD0DC",
|
|
5097
6076
|
diff: "\uBCC0\uACBD",
|
|
5098
6077
|
deploy: "\uBC30\uD3EC",
|
|
@@ -5144,7 +6123,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
|
|
|
5144
6123
|
program.command("start").alias("\uC2DC\uC791").alias("\uC0C8\uD504\uB85C\uC81D\uD2B8").description("\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791 \uB9C8\uBC95\uC0AC \u2014 git init + \uBB38\uC11C + MCP + \uCEE8\uD14D\uC2A4\uD2B8 \uD55C \uBC88\uC5D0").option("--from-notion <url>", "Notion PRD \uD398\uC774\uC9C0\uC5D0\uC11C import").option("--name <name>", "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984").option("--description <desc>", "\uD55C \uC904 \uC124\uBA85").option("--type <type>", "\uD504\uB85C\uC81D\uD2B8 \uC720\uD615 (webapp|extension|cli|notion|mobile)").option("-y, --yes", "\uBAA8\uB4E0 \uD655\uC778 \uC2A4\uD0B5 (\uC790\uB3D9 yes)").action(start);
|
|
5145
6124
|
program.command("init").alias("\uCD08\uAE30\uD654").alias("\uB9CC\uB4E4\uAE30").description("\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (git/MCP/context\uB294 \uC81C\uC678) \u2014 \uBCF4\uD1B5 vhk start \uAD8C\uC7A5").option("--skip-gate", "gate \uAC80\uC99D \uC2A4\uD0B5").option("--from-notion <url>", "Notion PRD \uD398\uC774\uC9C0\uC5D0\uC11C import").option("--name <name>", "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984").option("--description <desc>", "\uD55C \uC904 \uC124\uBA85").option("--type <type>", "\uD504\uB85C\uC81D\uD2B8 \uC720\uD615 (webapp|extension|cli|notion|mobile)").option("-y, --yes", "\uC2A4\uD0DD \uD655\uC778 \uC2A4\uD0B5").action(init);
|
|
5146
6125
|
program.command("recap").alias("\uC815\uB9AC").alias("\uC624\uB298").description("\uC624\uB298 \uD55C \uC77C \uC815\uB9AC + ADR/\uD2B8\uB7EC\uBE14\uC288\uD305 \uC790\uB3D9 \uBD84\uB9AC").option("--since <date>", "\uBD84\uC11D \uC2DC\uC791\uC77C (YYYY-MM-DD)").action(recap);
|
|
5147
|
-
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(
|
|
6126
|
+
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").option("--dry-run", "\uBBF8\uB9AC\uBCF4\uAE30\uB9CC \u2014 \uD30C\uC77C \uBCC0\uACBD \uC5C6\uC74C").option("-y, --yes", "drift \uD655\uC778 \uD504\uB86C\uD504\uD2B8 \uC0DD\uB7B5(\uB36E\uC5B4\uC4F0\uAE30 \uB3D9\uC758)").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654 (\uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uBC31\uC5C5)").action(async (opts) => {
|
|
6127
|
+
await guardCli("sync", opts?.yes === true, () => sync(opts));
|
|
6128
|
+
});
|
|
5148
6129
|
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").option("--goal <id>", "goal id \uC9C0\uC815 \uC2DC scripts/check-goal-<id>.sh \uAC8C\uC774\uD2B8 \uC2E4\uD589").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC (\uB610\uB294 --goal <id> \uB85C goal \uAC8C\uC774\uD2B8)").action(async (opts) => {
|
|
5149
6130
|
await check(opts);
|
|
5150
6131
|
});
|
|
@@ -5156,16 +6137,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
|
|
|
5156
6137
|
cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
|
|
5157
6138
|
await cloudPush();
|
|
5158
6139
|
});
|
|
5159
|
-
cloudCmd.command("pull").alias("\uB0B4\uB9AC\uAE30").argument("[gistId]", "\uBCF5\uC6D0\uD560 gist id (\uC0DD\uB7B5 \uC2DC .vhk/cloud.json \uC0AC\uC6A9)").description("gist \uC5D0\uC11C .vhk/ \uBCF5\uC6D0").action(async (gistId) => {
|
|
5160
|
-
await cloudPull(gistId);
|
|
6140
|
+
cloudCmd.command("pull").alias("\uB0B4\uB9AC\uAE30").argument("[gistId]", "\uBCF5\uC6D0\uD560 gist id (\uC0DD\uB7B5 \uC2DC .vhk/cloud.json \uC0AC\uC6A9)").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("gist \uC5D0\uC11C .vhk/ \uBCF5\uC6D0").action(async (gistId, opts) => {
|
|
6141
|
+
await guardCli("cloud-pull", opts?.yes === true, () => cloudPull(gistId));
|
|
5161
6142
|
});
|
|
5162
6143
|
program.command("ship").alias("\uCD9C\uD558").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
|
|
5163
6144
|
program.command("doctor").alias("\uD658\uACBD").alias("\uC9C4\uB2E8").description("\uAC1C\uBC1C \uD658\uACBD \uC810\uAC80 \u2014 Node/Git/npm \uC0C1\uD0DC \uD655\uC778").action(doctor);
|
|
5164
|
-
program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
|
|
5165
|
-
await save();
|
|
6145
|
+
program.command("save").alias("\uC800\uC7A5").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (strict \uBAA8\uB4DC \uAC00\uB4DC \uBA85\uC2DC \uC2B9\uC778)").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async (opts) => {
|
|
6146
|
+
await guardCli("save", opts?.yes === true, () => save());
|
|
6147
|
+
});
|
|
6148
|
+
program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async (opts) => {
|
|
6149
|
+
await guardCliDefer("undo", opts?.yes === true, () => undo());
|
|
5166
6150
|
});
|
|
5167
|
-
program.command("
|
|
5168
|
-
await
|
|
6151
|
+
program.command("restore").alias("\uBCF5\uC6D0").argument("[id]", "\uBCF5\uC6D0\uD560 \uBC31\uC5C5 id (\uC0DD\uB7B5 \uC2DC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD)").description("sync \uBC31\uC5C5 \uBCF5\uC6D0 (.vhk/backups/ \u2014 \uC5B8\uCEE4\uBC0B \uB36E\uC5B4\uC4F0\uAE30 \uBCF5\uAD6C)").action(async (id) => {
|
|
6152
|
+
await restore(id);
|
|
5169
6153
|
});
|
|
5170
6154
|
program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
|
|
5171
6155
|
await status();
|
|
@@ -5177,17 +6161,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
|
|
|
5177
6161
|
program.command("mcp-init").alias("mcp\uC124\uC815").description("Cursor\xB7Claude Desktop MCP \uC5F0\uB3D9 \uC124\uC815 \uC790\uB3D9 \uC0DD\uC131 (.cursor/mcp.json)").action(async () => {
|
|
5178
6162
|
await mcpInit();
|
|
5179
6163
|
});
|
|
5180
|
-
program.command("deploy").alias("\uBC30\uD3EC").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async () => {
|
|
5181
|
-
await deploy();
|
|
6164
|
+
program.command("deploy").alias("\uBC30\uD3EC").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async (opts) => {
|
|
6165
|
+
await guardCli("deploy", opts?.yes === true, () => deploy());
|
|
5182
6166
|
});
|
|
5183
|
-
program.command("env").alias("\uD658\uACBD\uBCC0\uC218").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async () => {
|
|
5184
|
-
await env();
|
|
6167
|
+
program.command("env").alias("\uD658\uACBD\uBCC0\uC218").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async (opts) => {
|
|
6168
|
+
await guardCli("env-write", opts?.yes === true, () => env());
|
|
5185
6169
|
});
|
|
5186
6170
|
program.command("env-check").alias("\uD658\uACBD\uBCC0\uC218\uC810\uAC80").description("\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC").action(async () => {
|
|
5187
6171
|
await envCheck();
|
|
5188
6172
|
});
|
|
5189
|
-
program.command("publish").alias("\uCD9C\uC2DC").description("npm \uBC30\uD3EC (\uBC84\uC804 \uBC94\uD504 \u2192 \uBE4C\uB4DC \u2192 \uD14C\uC2A4\uD2B8 \u2192 publish)").action(async () => {
|
|
5190
|
-
await publish();
|
|
6173
|
+
program.command("publish").alias("\uCD9C\uC2DC").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("npm \uBC30\uD3EC (\uBC84\uC804 \uBC94\uD504 \u2192 \uBE4C\uB4DC \u2192 \uD14C\uC2A4\uD2B8 \u2192 publish)").action(async (opts) => {
|
|
6174
|
+
await guardCli("publish", opts?.yes === true, () => publish());
|
|
5191
6175
|
});
|
|
5192
6176
|
program.command("design").alias("\uB514\uC790\uC778").description("\uB514\uC790\uC778 \uD1A0\uD070 \uC0DD\uC131 (Tailwind config \uB610\uB294 CSS \uBCC0\uC218)").action(async () => {
|
|
5193
6177
|
await design();
|
|
@@ -5216,14 +6200,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
|
|
|
5216
6200
|
program.command("audit").alias("\uAC10\uC0AC").option("--fix", "\uC790\uB3D9 \uC218\uC815 \uC2DC\uB3C4").description("\uBCF4\uC548 \uCDE8\uC57D\uC810 \uAC10\uC0AC (npm audit \uB798\uD551)").action(async (opts) => {
|
|
5217
6201
|
await audit(opts.fix);
|
|
5218
6202
|
});
|
|
5219
|
-
program.command("migrate [target]").alias("\uC804\uD658").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm)").action(async (target) => {
|
|
5220
|
-
await migrate(target);
|
|
6203
|
+
program.command("migrate [target]").alias("\uC804\uD658").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm) \u2014 \uD328\uD0A4\uC9C0\uB9E4\uB2C8\uC800\uB9CC \uBC14\uAFC8, \uC124\uC815 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC544\uB2D8").action(async (target, opts) => {
|
|
6204
|
+
await guardCli("migrate", opts?.yes === true, () => migrate(target));
|
|
5221
6205
|
});
|
|
5222
6206
|
program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
|
|
5223
6207
|
await update();
|
|
5224
6208
|
});
|
|
5225
|
-
program.command("context").alias("\uB9E5\uB77D").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async () => {
|
|
5226
|
-
await context();
|
|
6209
|
+
program.command("context").alias("\uB9E5\uB77D").option("--compact", "\uD1A0\uD070 \uC808\uAC10\uD615 \u2014 \uC804\uCCB4 \uBA85\uB839 \uBAA9\uB85D/\uAE4A\uC740 \uD2B8\uB9AC \uC0DD\uB7B5, \uCC38\uC870 \uB9C1\uD06C \uC911\uC2EC").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async (opts) => {
|
|
6210
|
+
await context({ compact: opts.compact });
|
|
6211
|
+
});
|
|
6212
|
+
program.command("mode [target]").alias("\uBAA8\uB4DC").description("Safety Mode \uC870\uD68C/\uBCC0\uACBD (lite|standard|strict) \u2014 \uC704\uD5D8 \uC791\uC5C5 \uAC00\uB4DC \uAC15\uB3C4").action(async (target) => {
|
|
6213
|
+
await mode(target);
|
|
6214
|
+
});
|
|
6215
|
+
program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").description("\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C \uC548\uB0B4 (lite)").action(async () => {
|
|
6216
|
+
await verify();
|
|
5227
6217
|
});
|
|
5228
6218
|
program.command("context-show").alias("\uB9E5\uB77D\uBCF4\uAE30").description("\uD604\uC7AC \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uB0B4\uC6A9 \uCD9C\uB825").action(async () => {
|
|
5229
6219
|
await contextShow();
|
|
@@ -5269,7 +6259,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
|
|
|
5269
6259
|
await learn(lesson);
|
|
5270
6260
|
});
|
|
5271
6261
|
program.command("resume").alias("\uC7AC\uAC1C").option("--confirm", "\uC0AC\uB78C \uD655\uC778 \u2014 \uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0 (Forbidden \uC704\uBC18)").description(".vhk/HARD_STOP \uD574\uC81C (\uC0AC\uC6A9\uC790\uAC00 \uC0AC\uC720 \uD655\uC778 \uD6C4 --confirm \uD544\uC694)").action(async (opts) => {
|
|
5272
|
-
await resume(opts);
|
|
6262
|
+
await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
|
|
5273
6263
|
});
|
|
5274
6264
|
program.on("command:*", async (operands) => {
|
|
5275
6265
|
const unknown = operands[0] ?? "";
|
|
@@ -5279,7 +6269,7 @@ program.on("command:*", async (operands) => {
|
|
|
5279
6269
|
});
|
|
5280
6270
|
program.action(async () => {
|
|
5281
6271
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
5282
|
-
const { choice } = await
|
|
6272
|
+
const { choice } = await inquirer14.prompt([{
|
|
5283
6273
|
type: "list",
|
|
5284
6274
|
name: "choice",
|
|
5285
6275
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
@@ -5310,24 +6300,40 @@ program.action(async () => {
|
|
|
5310
6300
|
case "secure":
|
|
5311
6301
|
return secure();
|
|
5312
6302
|
case "sync":
|
|
5313
|
-
return sync();
|
|
6303
|
+
return guardCli("sync", false, () => sync());
|
|
5314
6304
|
case "doctor":
|
|
5315
6305
|
return doctor();
|
|
5316
6306
|
case "ship":
|
|
5317
6307
|
return ship();
|
|
5318
6308
|
case "save":
|
|
5319
|
-
return save();
|
|
6309
|
+
return guardCli("save", false, () => save());
|
|
5320
6310
|
case "undo":
|
|
5321
|
-
return undo();
|
|
6311
|
+
return guardCliDefer("undo", false, () => undo());
|
|
5322
6312
|
case "status":
|
|
5323
6313
|
return status();
|
|
5324
6314
|
case "diff":
|
|
5325
6315
|
return diff();
|
|
5326
6316
|
}
|
|
5327
6317
|
});
|
|
5328
|
-
var
|
|
5329
|
-
if (
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
6318
|
+
var isMainModule = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
6319
|
+
if (isMainModule) {
|
|
6320
|
+
try {
|
|
6321
|
+
const nlInput = detectNaturalLanguageInput(process.argv);
|
|
6322
|
+
if (nlInput !== null) {
|
|
6323
|
+
await runNaturalLanguageRoute(nlInput);
|
|
6324
|
+
} else {
|
|
6325
|
+
await program.parseAsync(process.argv);
|
|
6326
|
+
}
|
|
6327
|
+
} catch (err) {
|
|
6328
|
+
if (isPromptAbortError(err)) {
|
|
6329
|
+
console.error(chalk35.yellow("\n \u26A0\uFE0F \uB300\uD654\uD615 \uC785\uB825\uC774 \uCDE8\uC18C/\uC885\uB8CC\uB410\uC2B5\uB2C8\uB2E4. (\uBE44\uB300\uD654\uD615 \uD658\uACBD\uC5D0\uC11C\uB294 \uD574\uB2F9 \uBA85\uB839\uC744 \uC4F8 \uC218 \uC5C6\uC5B4\uC694)"));
|
|
6330
|
+
} else {
|
|
6331
|
+
console.error(chalk35.red(`
|
|
6332
|
+
\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
6333
|
+
}
|
|
6334
|
+
process.exitCode = 1;
|
|
6335
|
+
}
|
|
5333
6336
|
}
|
|
6337
|
+
export {
|
|
6338
|
+
program
|
|
6339
|
+
};
|