@byh3071/vhk 1.6.1 → 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 +15 -1
- package/dist/{chunk-O3A6SO7G.js → chunk-ACJN723Q.js} +146 -44
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1957 -972
- 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,19 +2352,21 @@ 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,
|
|
1747
2370
|
""
|
|
1748
2371
|
];
|
|
1749
2372
|
for (const section of recordSections) {
|
|
@@ -1753,6 +2376,34 @@ function toClaudeMd(sections, existing) {
|
|
|
1753
2376
|
}
|
|
1754
2377
|
return lines.join("\n");
|
|
1755
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`.",
|
|
2393
|
+
""
|
|
2394
|
+
];
|
|
2395
|
+
for (const section of codingSections) {
|
|
2396
|
+
lines.push(`## ${section.title}`);
|
|
2397
|
+
lines.push(section.content);
|
|
2398
|
+
lines.push("");
|
|
2399
|
+
}
|
|
2400
|
+
for (const section of recordSections) {
|
|
2401
|
+
lines.push(`## ${section.title}`);
|
|
2402
|
+
lines.push(section.content);
|
|
2403
|
+
lines.push("");
|
|
2404
|
+
}
|
|
2405
|
+
return lines.join("\n");
|
|
2406
|
+
}
|
|
1756
2407
|
function deriveProjectName(rulesContent) {
|
|
1757
2408
|
const firstLine = rulesContent.split("\n")[0];
|
|
1758
2409
|
return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
@@ -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));
|
|
1797
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(", ")}`));
|
|
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,161 +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
|
-
var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
|
|
2581
|
-
function contextSourcesChanged(generatedSha, rootDir) {
|
|
2582
|
-
const content = gitOut(
|
|
2583
|
-
["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
|
|
2584
|
-
rootDir
|
|
2585
|
-
).trim();
|
|
2586
|
-
if (content) return true;
|
|
2587
|
-
const structural = gitOut(
|
|
2588
|
-
["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
|
|
2589
|
-
rootDir
|
|
2590
|
-
).trim();
|
|
2591
|
-
return structural.length > 0;
|
|
2592
|
-
}
|
|
2593
|
-
function checkContextDrift(rootDir) {
|
|
2594
|
-
const ctxPath = path10.join(rootDir, CONTEXT_PATH);
|
|
2595
|
-
if (!fs9.existsSync(ctxPath)) return { checked: false, stale: false };
|
|
2596
|
-
const generatedSha = extractContextSha(fs9.readFileSync(ctxPath, "utf-8"));
|
|
2597
|
-
if (!generatedSha) return { checked: false, stale: false };
|
|
2598
|
-
let currentSha;
|
|
2599
|
-
try {
|
|
2600
|
-
currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
|
|
2601
|
-
} catch {
|
|
2602
|
-
return { checked: false, stale: false };
|
|
2603
|
-
}
|
|
2604
|
-
if (!currentSha) return { checked: false, stale: false };
|
|
2605
|
-
if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
|
|
2606
|
-
return { checked: true, stale: false, generatedSha, currentSha };
|
|
2607
|
-
}
|
|
2608
|
-
let stale;
|
|
2609
|
-
try {
|
|
2610
|
-
stale = contextSourcesChanged(generatedSha, rootDir);
|
|
2611
|
-
} catch {
|
|
2612
|
-
return { checked: false, stale: false };
|
|
2613
|
-
}
|
|
2614
|
-
return { checked: true, stale, generatedSha, currentSha };
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
// src/commands/doctor.ts
|
|
2618
3282
|
function checkCommand(name, command, hint) {
|
|
2619
3283
|
const result = safeExecFile(command, ["--version"]);
|
|
2620
3284
|
if (!result.ok) return { name, command, ok: false, hint };
|
|
@@ -2622,14 +3286,14 @@ function checkCommand(name, command, hint) {
|
|
|
2622
3286
|
return { name, command, version, ok: true, hint };
|
|
2623
3287
|
}
|
|
2624
3288
|
function getVhkVersion2() {
|
|
2625
|
-
const dir =
|
|
3289
|
+
const dir = path12.dirname(fileURLToPath(import.meta.url));
|
|
2626
3290
|
const candidates = [
|
|
2627
|
-
|
|
2628
|
-
|
|
3291
|
+
path12.join(dir, "../package.json"),
|
|
3292
|
+
path12.join(dir, "../../package.json")
|
|
2629
3293
|
];
|
|
2630
3294
|
for (const pkgPath of candidates) {
|
|
2631
3295
|
try {
|
|
2632
|
-
if (
|
|
3296
|
+
if (fs11.existsSync(pkgPath)) {
|
|
2633
3297
|
const pkg = readJsonFile(pkgPath);
|
|
2634
3298
|
return pkg.version;
|
|
2635
3299
|
}
|
|
@@ -2657,7 +3321,7 @@ function compareSemver(a, b) {
|
|
|
2657
3321
|
return a3 - b3;
|
|
2658
3322
|
}
|
|
2659
3323
|
async function doctor() {
|
|
2660
|
-
console.log(
|
|
3324
|
+
console.log(chalk11.bold(`
|
|
2661
3325
|
${ko.doctor.title}
|
|
2662
3326
|
`));
|
|
2663
3327
|
const checks = [
|
|
@@ -2669,30 +3333,30 @@ ${ko.doctor.title}
|
|
|
2669
3333
|
let allOk = true;
|
|
2670
3334
|
for (const check2 of checks) {
|
|
2671
3335
|
if (check2.ok) {
|
|
2672
|
-
console.log(
|
|
3336
|
+
console.log(chalk11.green(` \u2705 ${check2.name}`) + chalk11.dim(` \u2014 ${check2.version}`));
|
|
2673
3337
|
} else {
|
|
2674
|
-
console.log(
|
|
2675
|
-
console.log(
|
|
3338
|
+
console.log(chalk11.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
3339
|
+
console.log(chalk11.dim(` \u2192 ${check2.hint}`));
|
|
2676
3340
|
allOk = false;
|
|
2677
3341
|
}
|
|
2678
3342
|
}
|
|
2679
3343
|
console.log("");
|
|
2680
3344
|
const vhkVersion = getVhkVersion2();
|
|
2681
3345
|
if (vhkVersion) {
|
|
2682
|
-
console.log(
|
|
3346
|
+
console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(` \u2014 v${vhkVersion}`));
|
|
2683
3347
|
} else {
|
|
2684
|
-
console.log(
|
|
3348
|
+
console.log(chalk11.green(" \u2705 VHK") + chalk11.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2685
3349
|
}
|
|
2686
3350
|
if (vhkVersion) {
|
|
2687
3351
|
const latest = fetchLatestNpmVersion("@byh3071/vhk");
|
|
2688
3352
|
if (latest && compareSemver(latest, vhkVersion) > 0) {
|
|
2689
|
-
console.log(
|
|
3353
|
+
console.log(chalk11.yellow(` ${ko.doctor.updateAvailable(latest)}`));
|
|
2690
3354
|
} else if (latest) {
|
|
2691
|
-
console.log(
|
|
3355
|
+
console.log(chalk11.dim(` ${ko.doctor.updateCurrent}`));
|
|
2692
3356
|
}
|
|
2693
3357
|
}
|
|
2694
3358
|
console.log("");
|
|
2695
|
-
console.log(
|
|
3359
|
+
console.log(chalk11.bold(` ${ko.doctor.projectFiles}`));
|
|
2696
3360
|
const cwd = process.cwd();
|
|
2697
3361
|
const projectFiles = [
|
|
2698
3362
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -2702,49 +3366,51 @@ ${ko.doctor.title}
|
|
|
2702
3366
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2703
3367
|
];
|
|
2704
3368
|
for (const file of projectFiles) {
|
|
2705
|
-
const exists =
|
|
3369
|
+
const exists = fs11.existsSync(path12.join(cwd, file.name));
|
|
2706
3370
|
if (exists) {
|
|
2707
|
-
console.log(
|
|
3371
|
+
console.log(chalk11.green(` \u2705 ${file.name}`));
|
|
2708
3372
|
if (file.name === ".env") {
|
|
2709
|
-
const gitignorePath =
|
|
2710
|
-
if (
|
|
2711
|
-
const gitignore =
|
|
3373
|
+
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
3374
|
+
if (fs11.existsSync(gitignorePath)) {
|
|
3375
|
+
const gitignore = fs11.readFileSync(gitignorePath, "utf-8");
|
|
2712
3376
|
if (!gitignore.includes(".env")) {
|
|
2713
|
-
console.log(
|
|
3377
|
+
console.log(chalk11.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2714
3378
|
}
|
|
2715
3379
|
}
|
|
2716
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)"));
|
|
2717
3383
|
} else {
|
|
2718
|
-
console.log(
|
|
3384
|
+
console.log(chalk11.dim(` \u2B1A ${file.name}`) + chalk11.dim(` \u2014 ${file.hint}`));
|
|
2719
3385
|
}
|
|
2720
3386
|
}
|
|
2721
3387
|
console.log("");
|
|
2722
|
-
console.log(
|
|
3388
|
+
console.log(chalk11.bold(` ${ko.doctor.driftTitle}`));
|
|
2723
3389
|
const ruleDrift = checkRuleDrift(cwd);
|
|
2724
3390
|
if (!ruleDrift.checked) {
|
|
2725
|
-
console.log(
|
|
3391
|
+
console.log(chalk11.dim(` ${ko.doctor.driftNoRules}`));
|
|
2726
3392
|
} else {
|
|
2727
3393
|
const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
|
|
2728
3394
|
if (drifted.length === 0) {
|
|
2729
|
-
console.log(
|
|
3395
|
+
console.log(chalk11.green(` ${ko.doctor.driftRuleClean}`));
|
|
2730
3396
|
} else {
|
|
2731
|
-
console.log(
|
|
3397
|
+
console.log(chalk11.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
|
|
2732
3398
|
}
|
|
2733
3399
|
}
|
|
2734
3400
|
const ctxDrift = checkContextDrift(cwd);
|
|
2735
3401
|
if (ctxDrift.checked && ctxDrift.stale) {
|
|
2736
|
-
console.log(
|
|
3402
|
+
console.log(chalk11.yellow(` ${ko.doctor.driftContextWarn}`));
|
|
2737
3403
|
}
|
|
2738
3404
|
console.log("");
|
|
2739
3405
|
if (allOk) {
|
|
2740
|
-
console.log(
|
|
3406
|
+
console.log(chalk11.green.bold(` ${ko.doctor.allOk}`));
|
|
2741
3407
|
printNextStep({
|
|
2742
3408
|
message: ko.doctor.nextOkMessage,
|
|
2743
3409
|
command: "vhk \uC2DC\uC791",
|
|
2744
3410
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2745
3411
|
});
|
|
2746
3412
|
} else {
|
|
2747
|
-
console.log(
|
|
3413
|
+
console.log(chalk11.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2748
3414
|
printNextStep({
|
|
2749
3415
|
message: ko.doctor.nextRetryMessage,
|
|
2750
3416
|
command: "vhk doctor",
|
|
@@ -2755,10 +3421,10 @@ ${ko.doctor.title}
|
|
|
2755
3421
|
}
|
|
2756
3422
|
|
|
2757
3423
|
// src/commands/ship.ts
|
|
2758
|
-
import
|
|
2759
|
-
import
|
|
2760
|
-
import
|
|
2761
|
-
import
|
|
3424
|
+
import chalk12 from "chalk";
|
|
3425
|
+
import inquirer5 from "inquirer";
|
|
3426
|
+
import fs12 from "fs";
|
|
3427
|
+
import path13 from "path";
|
|
2762
3428
|
var CHECKLIST = [
|
|
2763
3429
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
2764
3430
|
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
@@ -2771,9 +3437,9 @@ function sanitizeVersion(version) {
|
|
|
2771
3437
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2772
3438
|
}
|
|
2773
3439
|
function updateChangelogUnreleased(cwd, version, date) {
|
|
2774
|
-
const changelogPath =
|
|
2775
|
-
if (!
|
|
2776
|
-
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");
|
|
2777
3443
|
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
|
2778
3444
|
if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
|
|
2779
3445
|
const blankUnreleased = [
|
|
@@ -2790,35 +3456,36 @@ function updateChangelogUnreleased(cwd, version, date) {
|
|
|
2790
3456
|
`## [${version}] \u2014 ${date}`
|
|
2791
3457
|
].join("\n");
|
|
2792
3458
|
const updated = content.replace(unreleasedHeading, blankUnreleased);
|
|
2793
|
-
|
|
3459
|
+
fs12.writeFileSync(changelogPath, updated, "utf-8");
|
|
2794
3460
|
return { status: "updated", version };
|
|
2795
3461
|
}
|
|
2796
3462
|
async function ship() {
|
|
2797
|
-
|
|
3463
|
+
if (!ensureNotHardStopped("ship")) return;
|
|
3464
|
+
console.log(chalk12.bold(`
|
|
2798
3465
|
${ko.ship.title}
|
|
2799
3466
|
`));
|
|
2800
3467
|
const cwd = process.cwd();
|
|
2801
|
-
console.log(
|
|
3468
|
+
console.log(chalk12.cyan.bold(` ${ko.ship.checklist}
|
|
2802
3469
|
`));
|
|
2803
|
-
const { passed } = await
|
|
3470
|
+
const { passed } = await inquirer5.prompt([{
|
|
2804
3471
|
type: "checkbox",
|
|
2805
3472
|
name: "passed",
|
|
2806
3473
|
message: ko.ship.checkboxPrompt,
|
|
2807
3474
|
choices: CHECKLIST.map((c) => ({
|
|
2808
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
3475
|
+
name: `${ko.ship[c.questionKey]} ${chalk12.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2809
3476
|
value: c.id
|
|
2810
3477
|
}))
|
|
2811
3478
|
}]);
|
|
2812
3479
|
const allPassed = passed.length === CHECKLIST.length;
|
|
2813
3480
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2814
3481
|
if (!allPassed) {
|
|
2815
|
-
console.log(
|
|
3482
|
+
console.log(chalk12.yellow(`
|
|
2816
3483
|
${ko.ship.incompleteHeader}`));
|
|
2817
3484
|
skipped.forEach((s) => {
|
|
2818
|
-
console.log(
|
|
2819
|
-
console.log(
|
|
3485
|
+
console.log(chalk12.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
3486
|
+
console.log(chalk12.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2820
3487
|
});
|
|
2821
|
-
const { proceed } = await
|
|
3488
|
+
const { proceed } = await inquirer5.prompt([{
|
|
2822
3489
|
type: "confirm",
|
|
2823
3490
|
name: "proceed",
|
|
2824
3491
|
message: ko.ship.proceedConfirm,
|
|
@@ -2833,26 +3500,26 @@ ${ko.ship.title}
|
|
|
2833
3500
|
return;
|
|
2834
3501
|
}
|
|
2835
3502
|
} else {
|
|
2836
|
-
console.log(
|
|
3503
|
+
console.log(chalk12.green(`
|
|
2837
3504
|
${ko.ship.allPassed}
|
|
2838
3505
|
`));
|
|
2839
3506
|
}
|
|
2840
|
-
console.log(
|
|
3507
|
+
console.log(chalk12.cyan.bold(` ${ko.ship.retro}
|
|
2841
3508
|
`));
|
|
2842
|
-
console.log(
|
|
2843
|
-
const retro = await
|
|
3509
|
+
console.log(chalk12.dim(` ${ko.ship.versionHint}`));
|
|
3510
|
+
const retro = await inquirer5.prompt([
|
|
2844
3511
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2845
3512
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
2846
3513
|
{ type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
|
|
2847
3514
|
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
2848
3515
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2849
3516
|
]);
|
|
2850
|
-
const buildLogDir =
|
|
2851
|
-
if (!
|
|
2852
|
-
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();
|
|
2853
3520
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2854
3521
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
2855
|
-
const filePath =
|
|
3522
|
+
const filePath = path13.join(buildLogDir, fileName);
|
|
2856
3523
|
const content = [
|
|
2857
3524
|
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
2858
3525
|
"",
|
|
@@ -2881,9 +3548,9 @@ ${ko.ship.title}
|
|
|
2881
3548
|
"---",
|
|
2882
3549
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2883
3550
|
].join("\n");
|
|
2884
|
-
|
|
2885
|
-
console.log(
|
|
2886
|
-
${ko.ship.buildLogDone(
|
|
3551
|
+
fs12.writeFileSync(filePath, content, "utf-8");
|
|
3552
|
+
console.log(chalk12.green(`
|
|
3553
|
+
${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
|
|
2887
3554
|
const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
|
|
2888
3555
|
if (changelogResult.status === "updated") {
|
|
2889
3556
|
log.success(ko.ship.changelogUpdated(changelogResult.version));
|
|
@@ -2902,9 +3569,9 @@ ${ko.ship.title}
|
|
|
2902
3569
|
|
|
2903
3570
|
// src/commands/save.ts
|
|
2904
3571
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2905
|
-
import
|
|
3572
|
+
import chalk13 from "chalk";
|
|
2906
3573
|
import ora from "ora";
|
|
2907
|
-
import
|
|
3574
|
+
import inquirer6 from "inquirer";
|
|
2908
3575
|
|
|
2909
3576
|
// src/lib/git-porcelain.ts
|
|
2910
3577
|
function normalizePorcelain(raw) {
|
|
@@ -2930,54 +3597,54 @@ function statusIcon(code) {
|
|
|
2930
3597
|
return "\u{1F4C4}";
|
|
2931
3598
|
}
|
|
2932
3599
|
async function save() {
|
|
2933
|
-
console.log(
|
|
3600
|
+
console.log(chalk13.bold(`
|
|
2934
3601
|
\u{1F4BE} ${t("save.title")}`));
|
|
2935
|
-
console.log(
|
|
3602
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
2936
3603
|
let gitRoot;
|
|
2937
3604
|
try {
|
|
2938
3605
|
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
2939
3606
|
gitRoot = getGitRoot();
|
|
2940
3607
|
} catch {
|
|
2941
|
-
console.log(
|
|
3608
|
+
console.log(chalk13.red(`\u274C ${t("save.notGitRepo")}`));
|
|
2942
3609
|
return;
|
|
2943
3610
|
}
|
|
2944
|
-
console.log(
|
|
3611
|
+
console.log(chalk13.cyan(`
|
|
2945
3612
|
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
2946
3613
|
printSecurityWarnings(gitRoot);
|
|
2947
3614
|
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
2948
3615
|
if (severe.length > 0) {
|
|
2949
|
-
console.log(
|
|
3616
|
+
console.log(chalk13.red(`
|
|
2950
3617
|
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
2951
3618
|
severe.slice(0, 5).forEach((f) => {
|
|
2952
|
-
console.log(
|
|
3619
|
+
console.log(chalk13.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
2953
3620
|
});
|
|
2954
3621
|
if (severe.length > 5) {
|
|
2955
|
-
console.log(
|
|
3622
|
+
console.log(chalk13.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
2956
3623
|
}
|
|
2957
|
-
const { proceed } = await
|
|
3624
|
+
const { proceed } = await inquirer6.prompt([{
|
|
2958
3625
|
type: "confirm",
|
|
2959
3626
|
name: "proceed",
|
|
2960
3627
|
message: t("save.secretsConfirm"),
|
|
2961
3628
|
default: false
|
|
2962
3629
|
}]);
|
|
2963
3630
|
if (!proceed) {
|
|
2964
|
-
console.log(
|
|
3631
|
+
console.log(chalk13.gray(t("save.cancelled")));
|
|
2965
3632
|
return;
|
|
2966
3633
|
}
|
|
2967
3634
|
}
|
|
2968
3635
|
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
2969
3636
|
if (lines.length === 0) {
|
|
2970
|
-
console.log(
|
|
3637
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
2971
3638
|
return;
|
|
2972
3639
|
}
|
|
2973
|
-
console.log(
|
|
3640
|
+
console.log(chalk13.cyan(`
|
|
2974
3641
|
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
2975
3642
|
lines.forEach((line) => {
|
|
2976
3643
|
const code = line.substring(0, 2);
|
|
2977
3644
|
const name = line.substring(3);
|
|
2978
3645
|
console.log(` ${statusIcon(code)} ${name}`);
|
|
2979
3646
|
});
|
|
2980
|
-
const { message } = await
|
|
3647
|
+
const { message } = await inquirer6.prompt([{
|
|
2981
3648
|
type: "input",
|
|
2982
3649
|
name: "message",
|
|
2983
3650
|
message: t("save.commitMessage"),
|
|
@@ -2992,21 +3659,21 @@ async function save() {
|
|
|
2992
3659
|
spinner.text = t("save.pushing");
|
|
2993
3660
|
if (!hasGitRemote(gitRoot)) {
|
|
2994
3661
|
spinner.succeed(t("save.successLocal"));
|
|
2995
|
-
console.log(
|
|
3662
|
+
console.log(chalk13.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
2996
3663
|
} else {
|
|
2997
3664
|
try {
|
|
2998
3665
|
gitRun(["push"], gitRoot);
|
|
2999
3666
|
spinner.succeed(t("save.successWithPush"));
|
|
3000
3667
|
} catch (pushErr) {
|
|
3001
3668
|
spinner.fail(t("save.pushFailed"));
|
|
3002
|
-
console.log(
|
|
3003
|
-
console.log(
|
|
3669
|
+
console.log(chalk13.red(getExecErrorMessage(pushErr)));
|
|
3670
|
+
console.log(chalk13.yellow(`
|
|
3004
3671
|
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
3005
3672
|
process.exitCode = 1;
|
|
3006
3673
|
}
|
|
3007
3674
|
}
|
|
3008
3675
|
if (process.exitCode !== 1) {
|
|
3009
|
-
console.log(
|
|
3676
|
+
console.log(chalk13.green(`
|
|
3010
3677
|
\u2705 ${t("save.done", lines.length)}`));
|
|
3011
3678
|
printNextStep({
|
|
3012
3679
|
message: t("save.nextOkMessage"),
|
|
@@ -3014,7 +3681,7 @@ async function save() {
|
|
|
3014
3681
|
cursorHint: t("save.nextOkCursor")
|
|
3015
3682
|
});
|
|
3016
3683
|
} else {
|
|
3017
|
-
console.log(
|
|
3684
|
+
console.log(chalk13.green(`
|
|
3018
3685
|
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
3019
3686
|
printNextStep({
|
|
3020
3687
|
message: t("save.nextPushFailMessage"),
|
|
@@ -3024,12 +3691,12 @@ async function save() {
|
|
|
3024
3691
|
}
|
|
3025
3692
|
} catch (err) {
|
|
3026
3693
|
spinner.fail(t("save.failed"));
|
|
3027
|
-
console.log(
|
|
3694
|
+
console.log(chalk13.red(getExecErrorMessage(err)));
|
|
3028
3695
|
if (didAdd) {
|
|
3029
3696
|
try {
|
|
3030
3697
|
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3031
3698
|
if (staged) {
|
|
3032
|
-
console.log(
|
|
3699
|
+
console.log(chalk13.yellow(`
|
|
3033
3700
|
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3034
3701
|
}
|
|
3035
3702
|
} catch {
|
|
@@ -3041,8 +3708,8 @@ async function save() {
|
|
|
3041
3708
|
|
|
3042
3709
|
// src/commands/undo.ts
|
|
3043
3710
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3044
|
-
import
|
|
3045
|
-
import
|
|
3711
|
+
import chalk14 from "chalk";
|
|
3712
|
+
import inquirer7 from "inquirer";
|
|
3046
3713
|
function parseRecentCommits(logOutput) {
|
|
3047
3714
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3048
3715
|
}
|
|
@@ -3065,36 +3732,36 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
|
3065
3732
|
return false;
|
|
3066
3733
|
}
|
|
3067
3734
|
async function undo() {
|
|
3068
|
-
console.log(
|
|
3735
|
+
console.log(chalk14.bold(`
|
|
3069
3736
|
\u23EA ${t("undo.title")}`));
|
|
3070
|
-
console.log(
|
|
3737
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3071
3738
|
let gitRoot;
|
|
3072
3739
|
try {
|
|
3073
3740
|
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3074
3741
|
gitRoot = getGitRoot();
|
|
3075
3742
|
} catch {
|
|
3076
|
-
console.log(
|
|
3743
|
+
console.log(chalk14.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
3077
3744
|
return;
|
|
3078
3745
|
}
|
|
3079
3746
|
let logOutput;
|
|
3080
3747
|
try {
|
|
3081
3748
|
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
3082
3749
|
} catch {
|
|
3083
|
-
console.log(
|
|
3750
|
+
console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3084
3751
|
return;
|
|
3085
3752
|
}
|
|
3086
3753
|
const commits = parseRecentCommits(logOutput);
|
|
3087
3754
|
if (commits.length === 0) {
|
|
3088
|
-
console.log(
|
|
3755
|
+
console.log(chalk14.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3089
3756
|
return;
|
|
3090
3757
|
}
|
|
3091
|
-
console.log(
|
|
3758
|
+
console.log(chalk14.cyan(`
|
|
3092
3759
|
${t("undo.recentHeader")}`));
|
|
3093
3760
|
commits.forEach((c, i) => {
|
|
3094
3761
|
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
3095
3762
|
});
|
|
3096
3763
|
const maxUndo = commits.length;
|
|
3097
|
-
const { count } = await
|
|
3764
|
+
const { count } = await inquirer7.prompt([{
|
|
3098
3765
|
type: "number",
|
|
3099
3766
|
name: "count",
|
|
3100
3767
|
message: t("undo.howMany"),
|
|
@@ -3105,7 +3772,7 @@ ${t("undo.recentHeader")}`));
|
|
|
3105
3772
|
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
3106
3773
|
const headCount = countLocalCommits(gitRoot);
|
|
3107
3774
|
if (undoCount >= headCount) {
|
|
3108
|
-
console.log(
|
|
3775
|
+
console.log(chalk14.yellow(`
|
|
3109
3776
|
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3110
3777
|
return;
|
|
3111
3778
|
}
|
|
@@ -3114,30 +3781,30 @@ ${t("undo.recentHeader")}`));
|
|
|
3114
3781
|
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3115
3782
|
if (risky) {
|
|
3116
3783
|
if (unpushed < 0) {
|
|
3117
|
-
console.log(
|
|
3784
|
+
console.log(chalk14.red(`
|
|
3118
3785
|
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3119
3786
|
} else {
|
|
3120
|
-
console.log(
|
|
3787
|
+
console.log(chalk14.red(`
|
|
3121
3788
|
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3122
3789
|
}
|
|
3123
3790
|
}
|
|
3124
|
-
const { confirm } = await
|
|
3791
|
+
const { confirm } = await inquirer7.prompt([{
|
|
3125
3792
|
type: "confirm",
|
|
3126
3793
|
name: "confirm",
|
|
3127
3794
|
message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
|
|
3128
3795
|
default: false
|
|
3129
3796
|
}]);
|
|
3130
3797
|
if (!confirm) {
|
|
3131
|
-
console.log(
|
|
3798
|
+
console.log(chalk14.gray(t("undo.cancelled")));
|
|
3132
3799
|
return;
|
|
3133
3800
|
}
|
|
3134
3801
|
try {
|
|
3135
3802
|
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3136
|
-
console.log(
|
|
3803
|
+
console.log(chalk14.green(`
|
|
3137
3804
|
\u2705 ${t("undo.success")}`));
|
|
3138
|
-
console.log(
|
|
3805
|
+
console.log(chalk14.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3139
3806
|
if (risky) {
|
|
3140
|
-
console.log(
|
|
3807
|
+
console.log(chalk14.yellow(`
|
|
3141
3808
|
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3142
3809
|
}
|
|
3143
3810
|
printNextStep({
|
|
@@ -3146,18 +3813,73 @@ ${t("undo.recentHeader")}`));
|
|
|
3146
3813
|
cursorHint: t("undo.nextCursor")
|
|
3147
3814
|
});
|
|
3148
3815
|
} catch (err) {
|
|
3149
|
-
console.log(
|
|
3816
|
+
console.log(chalk14.red(`\u274C ${t("undo.failed")}`));
|
|
3150
3817
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3151
|
-
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)}`));
|
|
3152
3874
|
process.exitCode = 1;
|
|
3153
3875
|
}
|
|
3154
3876
|
}
|
|
3155
3877
|
|
|
3156
3878
|
// src/commands/status.ts
|
|
3157
3879
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
3158
|
-
import
|
|
3159
|
-
import
|
|
3160
|
-
import
|
|
3880
|
+
import fs13 from "fs";
|
|
3881
|
+
import path14 from "path";
|
|
3882
|
+
import chalk16 from "chalk";
|
|
3161
3883
|
function countFileChanges(porcelain) {
|
|
3162
3884
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
3163
3885
|
let staged = 0;
|
|
@@ -3195,8 +3917,8 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3195
3917
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3196
3918
|
}
|
|
3197
3919
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3198
|
-
const pkgPath =
|
|
3199
|
-
if (!
|
|
3920
|
+
const pkgPath = path14.join(cwd, "package.json");
|
|
3921
|
+
if (!fs13.existsSync(pkgPath)) return null;
|
|
3200
3922
|
try {
|
|
3201
3923
|
const pkg = readJsonFile(pkgPath);
|
|
3202
3924
|
if (!pkg.name && !pkg.version) return null;
|
|
@@ -3208,6 +3930,21 @@ function readProjectPackage(cwd = process.cwd()) {
|
|
|
3208
3930
|
return null;
|
|
3209
3931
|
}
|
|
3210
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
|
+
}
|
|
3211
3948
|
function getSyncCounts(gitRoot) {
|
|
3212
3949
|
try {
|
|
3213
3950
|
const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
|
|
@@ -3217,15 +3954,15 @@ function getSyncCounts(gitRoot) {
|
|
|
3217
3954
|
}
|
|
3218
3955
|
}
|
|
3219
3956
|
async function status() {
|
|
3220
|
-
console.log(
|
|
3957
|
+
console.log(chalk16.bold(`
|
|
3221
3958
|
\u{1F4CA} ${t("status.title")}`));
|
|
3222
|
-
console.log(
|
|
3959
|
+
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
3223
3960
|
let gitRoot;
|
|
3224
3961
|
try {
|
|
3225
3962
|
execFileSync4("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3226
3963
|
gitRoot = getGitRoot();
|
|
3227
3964
|
} catch {
|
|
3228
|
-
console.log(
|
|
3965
|
+
console.log(chalk16.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3229
3966
|
return;
|
|
3230
3967
|
}
|
|
3231
3968
|
let branch;
|
|
@@ -3244,48 +3981,36 @@ async function status() {
|
|
|
3244
3981
|
commits = [];
|
|
3245
3982
|
}
|
|
3246
3983
|
const pkg = readProjectPackage();
|
|
3247
|
-
console.log(
|
|
3248
|
-
\u{1F33F} ${t("status.branch")}`) +
|
|
3984
|
+
console.log(chalk16.cyan(`
|
|
3985
|
+
\u{1F33F} ${t("status.branch")}`) + chalk16.white(` ${branch}`));
|
|
3249
3986
|
console.log(
|
|
3250
|
-
|
|
3987
|
+
chalk16.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk16.white(
|
|
3251
3988
|
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3252
3989
|
)
|
|
3253
3990
|
);
|
|
3254
|
-
console.log(
|
|
3255
|
-
\u{1F4CB} ${t("status.recentCommits")}`));
|
|
3991
|
+
console.log(chalk16.cyan(`
|
|
3992
|
+
\u{1F4CB} ${t("status.recentCommits", commits.length)}`));
|
|
3256
3993
|
if (commits.length === 0) {
|
|
3257
|
-
console.log(
|
|
3994
|
+
console.log(chalk16.dim(` ${t("status.noCommits")}`));
|
|
3258
3995
|
} else {
|
|
3259
|
-
commits.forEach((c) => console.log(` ${
|
|
3996
|
+
commits.forEach((c) => console.log(` ${chalk16.dim("\u2022")} ${c}`));
|
|
3260
3997
|
}
|
|
3261
3998
|
console.log(
|
|
3262
|
-
|
|
3263
|
-
\u{1F504} ${t("status.remote")}`) +
|
|
3999
|
+
chalk16.cyan(`
|
|
4000
|
+
\u{1F504} ${t("status.remote")}`) + chalk16.white(` ${formatSyncLabel(sync2)}`)
|
|
3264
4001
|
);
|
|
3265
|
-
console.log(
|
|
4002
|
+
console.log(chalk16.gray("\n" + "\u2500".repeat(40)));
|
|
3266
4003
|
if (pkg) {
|
|
3267
|
-
console.log(
|
|
4004
|
+
console.log(chalk16.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk16.white(` ${pkg.name} v${pkg.version}`));
|
|
3268
4005
|
} else {
|
|
3269
|
-
console.log(
|
|
4006
|
+
console.log(chalk16.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3270
4007
|
}
|
|
3271
4008
|
const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
|
|
3272
|
-
|
|
3273
|
-
printNextStep({
|
|
3274
|
-
message: t("status.nextWithChangesMessage"),
|
|
3275
|
-
command: "vhk save",
|
|
3276
|
-
cursorHint: t("status.nextWithChangesCursor")
|
|
3277
|
-
});
|
|
3278
|
-
} else {
|
|
3279
|
-
printNextStep({
|
|
3280
|
-
message: t("status.nextCleanMessage"),
|
|
3281
|
-
command: "vhk goal next",
|
|
3282
|
-
cursorHint: t("status.nextCleanCursor")
|
|
3283
|
-
});
|
|
3284
|
-
}
|
|
4009
|
+
printNextStep(selectStatusNextStep(hasChanges));
|
|
3285
4010
|
}
|
|
3286
4011
|
|
|
3287
4012
|
// src/commands/diff.ts
|
|
3288
|
-
import
|
|
4013
|
+
import chalk17 from "chalk";
|
|
3289
4014
|
function gitOut2(args) {
|
|
3290
4015
|
const r = safeExecFile("git", args);
|
|
3291
4016
|
return r.ok ? r.out : "";
|
|
@@ -3322,67 +4047,67 @@ function summarizeNumstat(numstat) {
|
|
|
3322
4047
|
return { fileCount, totalAdd, totalDel };
|
|
3323
4048
|
}
|
|
3324
4049
|
function printFile(f) {
|
|
3325
|
-
const adds = f.additions > 0 ?
|
|
3326
|
-
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}`) : "";
|
|
3327
4052
|
const change = [adds, dels].filter(Boolean).join(" ");
|
|
3328
4053
|
console.log(` ${f.name} ${change}`);
|
|
3329
4054
|
}
|
|
3330
4055
|
async function diff() {
|
|
3331
|
-
console.log(
|
|
4056
|
+
console.log(chalk17.bold(`
|
|
3332
4057
|
\u{1F50D} ${t("diff.title")}`));
|
|
3333
|
-
console.log(
|
|
4058
|
+
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3334
4059
|
if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
|
|
3335
|
-
console.log(
|
|
4060
|
+
console.log(chalk17.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3336
4061
|
return;
|
|
3337
4062
|
}
|
|
3338
4063
|
const unstaged = gitOut2(["diff", "--stat"]);
|
|
3339
4064
|
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
3340
4065
|
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
3341
4066
|
if (!unstaged && !staged && !untracked) {
|
|
3342
|
-
console.log(
|
|
4067
|
+
console.log(chalk17.green(`
|
|
3343
4068
|
\u2705 ${t("diff.noChanges")}`));
|
|
3344
4069
|
return;
|
|
3345
4070
|
}
|
|
3346
4071
|
if (staged) {
|
|
3347
|
-
console.log(
|
|
4072
|
+
console.log(chalk17.cyan(`
|
|
3348
4073
|
${t("diff.stagedHeader")}`));
|
|
3349
4074
|
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
3350
4075
|
}
|
|
3351
4076
|
if (unstaged) {
|
|
3352
|
-
console.log(
|
|
4077
|
+
console.log(chalk17.cyan(`
|
|
3353
4078
|
${t("diff.unstagedHeader")}`));
|
|
3354
4079
|
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
3355
4080
|
}
|
|
3356
4081
|
if (untracked) {
|
|
3357
4082
|
const files = untracked.split("\n").filter(Boolean);
|
|
3358
|
-
console.log(
|
|
4083
|
+
console.log(chalk17.cyan(`
|
|
3359
4084
|
${t("diff.untrackedHeader", files.length)}`));
|
|
3360
|
-
files.forEach((f) => console.log(` ${
|
|
4085
|
+
files.forEach((f) => console.log(` ${chalk17.green("+")} ${f}`));
|
|
3361
4086
|
}
|
|
3362
4087
|
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
3363
4088
|
if (numstat) {
|
|
3364
4089
|
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
3365
|
-
console.log(
|
|
4090
|
+
console.log(chalk17.cyan(`
|
|
3366
4091
|
${t("diff.summaryHeader")}`));
|
|
3367
4092
|
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
3368
|
-
console.log(` \uCD94\uAC00: ${
|
|
3369
|
-
console.log(` \uC0AD\uC81C: ${
|
|
4093
|
+
console.log(` \uCD94\uAC00: ${chalk17.green(`+${totalAdd}`)}\uC904`);
|
|
4094
|
+
console.log(` \uC0AD\uC81C: ${chalk17.red(`-${totalDel}`)}\uC904`);
|
|
3370
4095
|
}
|
|
3371
4096
|
console.log("");
|
|
3372
4097
|
}
|
|
3373
4098
|
|
|
3374
4099
|
// src/commands/mcp-init.ts
|
|
3375
|
-
import { existsSync as
|
|
3376
|
-
import { join as
|
|
4100
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
4101
|
+
import { join as join6, dirname } from "path";
|
|
3377
4102
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3378
|
-
import
|
|
4103
|
+
import chalk18 from "chalk";
|
|
3379
4104
|
function resolveMcpEntryPoint() {
|
|
3380
4105
|
try {
|
|
3381
4106
|
const here = fileURLToPath2(import.meta.url);
|
|
3382
4107
|
const dir = dirname(here);
|
|
3383
4108
|
for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
|
|
3384
|
-
const candidate =
|
|
3385
|
-
if (
|
|
4109
|
+
const candidate = join6(dir, ...rel);
|
|
4110
|
+
if (existsSync5(candidate)) return candidate;
|
|
3386
4111
|
}
|
|
3387
4112
|
} catch {
|
|
3388
4113
|
}
|
|
@@ -3390,17 +4115,17 @@ function resolveMcpEntryPoint() {
|
|
|
3390
4115
|
const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
|
|
3391
4116
|
if (typeof url === "string") {
|
|
3392
4117
|
const p = fileURLToPath2(url);
|
|
3393
|
-
if (
|
|
4118
|
+
if (existsSync5(p)) return p;
|
|
3394
4119
|
}
|
|
3395
4120
|
} catch {
|
|
3396
4121
|
}
|
|
3397
4122
|
try {
|
|
3398
|
-
const pkgPath =
|
|
3399
|
-
if (
|
|
4123
|
+
const pkgPath = join6(process.cwd(), "package.json");
|
|
4124
|
+
if (existsSync5(pkgPath)) {
|
|
3400
4125
|
const pkg = readJsonFile(pkgPath);
|
|
3401
4126
|
if (pkg.name === "@byh3071/vhk") {
|
|
3402
|
-
const local =
|
|
3403
|
-
if (
|
|
4127
|
+
const local = join6(process.cwd(), "dist", "mcp", "index.js");
|
|
4128
|
+
if (existsSync5(local)) return local;
|
|
3404
4129
|
}
|
|
3405
4130
|
}
|
|
3406
4131
|
} catch {
|
|
@@ -3415,31 +4140,31 @@ function resolveVhkMcpEntry() {
|
|
|
3415
4140
|
return { command: "vhk-mcp", args: [] };
|
|
3416
4141
|
}
|
|
3417
4142
|
async function mcpInit() {
|
|
3418
|
-
console.log(
|
|
3419
|
-
console.log(
|
|
3420
|
-
const cursorDir =
|
|
3421
|
-
if (!
|
|
3422
|
-
|
|
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 });
|
|
3423
4148
|
}
|
|
3424
|
-
const configPath =
|
|
4149
|
+
const configPath = join6(cursorDir, "mcp.json");
|
|
3425
4150
|
const vhkEntry = resolveVhkMcpEntry();
|
|
3426
4151
|
let config;
|
|
3427
|
-
if (
|
|
4152
|
+
if (existsSync5(configPath)) {
|
|
3428
4153
|
try {
|
|
3429
4154
|
const parsed = readJsonFile(configPath);
|
|
3430
4155
|
config = {
|
|
3431
4156
|
mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
|
|
3432
4157
|
};
|
|
3433
4158
|
} catch {
|
|
3434
|
-
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."));
|
|
3435
4160
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3436
4161
|
}
|
|
3437
4162
|
} else {
|
|
3438
4163
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3439
4164
|
}
|
|
3440
|
-
|
|
3441
|
-
console.log(
|
|
3442
|
-
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:"));
|
|
3443
4168
|
console.log(` ${configPath}`);
|
|
3444
4169
|
printNextStep({
|
|
3445
4170
|
message: t("mcp.nextMessage"),
|
|
@@ -3449,9 +4174,9 @@ async function mcpInit() {
|
|
|
3449
4174
|
}
|
|
3450
4175
|
|
|
3451
4176
|
// src/commands/design.ts
|
|
3452
|
-
import { existsSync as
|
|
3453
|
-
import
|
|
3454
|
-
import
|
|
4177
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4178
|
+
import chalk19 from "chalk";
|
|
4179
|
+
import inquirer9 from "inquirer";
|
|
3455
4180
|
var PALETTES = [
|
|
3456
4181
|
{
|
|
3457
4182
|
name: "Minimal",
|
|
@@ -3503,7 +4228,36 @@ var PALETTES = [
|
|
|
3503
4228
|
}
|
|
3504
4229
|
];
|
|
3505
4230
|
function hasTailwind() {
|
|
3506
|
-
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");
|
|
3507
4261
|
}
|
|
3508
4262
|
function generateCSSTokens(palette) {
|
|
3509
4263
|
const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
|
|
@@ -3524,9 +4278,10 @@ export default vhkColors
|
|
|
3524
4278
|
`;
|
|
3525
4279
|
}
|
|
3526
4280
|
async function design() {
|
|
3527
|
-
console.log(
|
|
3528
|
-
console.log(
|
|
3529
|
-
|
|
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([
|
|
3530
4285
|
{
|
|
3531
4286
|
type: "list",
|
|
3532
4287
|
name: "paletteIndex",
|
|
@@ -3538,32 +4293,37 @@ async function design() {
|
|
|
3538
4293
|
}
|
|
3539
4294
|
]);
|
|
3540
4295
|
const palette = PALETTES[paletteIndex];
|
|
3541
|
-
console.log(
|
|
4296
|
+
console.log(chalk19.cyan(`
|
|
3542
4297
|
\u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
|
|
3543
|
-
const
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
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([{
|
|
3547
4303
|
type: "confirm",
|
|
3548
4304
|
name: "overwrite",
|
|
3549
4305
|
message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
3550
4306
|
default: false
|
|
3551
4307
|
}]);
|
|
3552
4308
|
if (!overwrite) {
|
|
3553
|
-
console.log(
|
|
4309
|
+
console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3554
4310
|
return;
|
|
3555
4311
|
}
|
|
3556
4312
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
if (
|
|
3560
|
-
console.log(
|
|
3561
|
-
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."));
|
|
3562
4322
|
} else {
|
|
3563
|
-
console.log(
|
|
3564
|
-
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."));
|
|
3565
4325
|
}
|
|
3566
|
-
console.log(
|
|
4326
|
+
console.log(chalk19.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
|
|
3567
4327
|
for (const [key, value] of Object.entries(palette.colors)) {
|
|
3568
4328
|
console.log(` ${key.padEnd(12)} ${value}`);
|
|
3569
4329
|
}
|
|
@@ -3578,9 +4338,9 @@ async function designPalette() {
|
|
|
3578
4338
|
}
|
|
3579
4339
|
|
|
3580
4340
|
// src/commands/theme.ts
|
|
3581
|
-
import { existsSync as
|
|
3582
|
-
import
|
|
3583
|
-
import
|
|
4341
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
4342
|
+
import chalk20 from "chalk";
|
|
4343
|
+
import inquirer10 from "inquirer";
|
|
3584
4344
|
function generateDarkCSS() {
|
|
3585
4345
|
return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
|
|
3586
4346
|
|
|
@@ -3636,13 +4396,13 @@ export function initTheme(): void {
|
|
|
3636
4396
|
`;
|
|
3637
4397
|
}
|
|
3638
4398
|
async function theme() {
|
|
3639
|
-
console.log(
|
|
3640
|
-
console.log(
|
|
4399
|
+
console.log(chalk20.bold("\n\u{1F319} " + t("theme.title")));
|
|
4400
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
3641
4401
|
const cssPath = "src/styles/theme.css";
|
|
3642
4402
|
const togglePath = "src/lib/theme-toggle.ts";
|
|
3643
|
-
const conflicts = [cssPath, togglePath].filter((p) =>
|
|
4403
|
+
const conflicts = [cssPath, togglePath].filter((p) => existsSync7(p));
|
|
3644
4404
|
if (conflicts.length > 0) {
|
|
3645
|
-
const { overwrite } = await
|
|
4405
|
+
const { overwrite } = await inquirer10.prompt([{
|
|
3646
4406
|
type: "confirm",
|
|
3647
4407
|
name: "overwrite",
|
|
3648
4408
|
message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
|
|
@@ -3650,21 +4410,21 @@ async function theme() {
|
|
|
3650
4410
|
default: false
|
|
3651
4411
|
}]);
|
|
3652
4412
|
if (!overwrite) {
|
|
3653
|
-
console.log(
|
|
4413
|
+
console.log(chalk20.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3654
4414
|
return;
|
|
3655
4415
|
}
|
|
3656
4416
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
console.log(
|
|
3661
|
-
|
|
3662
|
-
console.log(
|
|
3663
|
-
console.log(
|
|
3664
|
-
console.log(
|
|
3665
|
-
console.log(
|
|
3666
|
-
console.log(
|
|
3667
|
-
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"));
|
|
3668
4428
|
printNextStep({
|
|
3669
4429
|
message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
|
|
3670
4430
|
command: "vhk ref list",
|
|
@@ -3673,11 +4433,11 @@ async function theme() {
|
|
|
3673
4433
|
}
|
|
3674
4434
|
|
|
3675
4435
|
// src/commands/ref.ts
|
|
3676
|
-
import { existsSync as
|
|
3677
|
-
import
|
|
4436
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4437
|
+
import chalk21 from "chalk";
|
|
3678
4438
|
var REFS_PATH = ".vhk/refs.json";
|
|
3679
4439
|
function loadRefs() {
|
|
3680
|
-
if (!
|
|
4440
|
+
if (!existsSync8(REFS_PATH)) return [];
|
|
3681
4441
|
try {
|
|
3682
4442
|
const parsed = readJsonFile(REFS_PATH);
|
|
3683
4443
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -3686,28 +4446,28 @@ function loadRefs() {
|
|
|
3686
4446
|
}
|
|
3687
4447
|
}
|
|
3688
4448
|
function saveRefs(refs) {
|
|
3689
|
-
|
|
3690
|
-
|
|
4449
|
+
mkdirSync6(".vhk", { recursive: true });
|
|
4450
|
+
writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
|
|
3691
4451
|
}
|
|
3692
4452
|
async function refAdd(url, memo = "") {
|
|
3693
|
-
console.log(
|
|
3694
|
-
console.log(
|
|
4453
|
+
console.log(chalk21.bold("\n\u{1F517} " + t("ref.addTitle")));
|
|
4454
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3695
4455
|
if (!url) {
|
|
3696
|
-
console.log(
|
|
3697
|
-
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"'));
|
|
3698
4458
|
return;
|
|
3699
4459
|
}
|
|
3700
4460
|
const refs = loadRefs();
|
|
3701
4461
|
if (refs.some((r) => r.url === url)) {
|
|
3702
|
-
console.log(
|
|
4462
|
+
console.log(chalk21.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
|
|
3703
4463
|
return;
|
|
3704
4464
|
}
|
|
3705
4465
|
refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3706
4466
|
saveRefs(refs);
|
|
3707
|
-
console.log(
|
|
4467
|
+
console.log(chalk21.green(`
|
|
3708
4468
|
\u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
|
|
3709
|
-
console.log(
|
|
3710
|
-
if (memo) console.log(
|
|
4469
|
+
console.log(chalk21.cyan(` ${url}`));
|
|
4470
|
+
if (memo) console.log(chalk21.gray(` \u{1F4DD} ${memo}`));
|
|
3711
4471
|
printNextStep({
|
|
3712
4472
|
message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
|
|
3713
4473
|
command: "vhk ref list",
|
|
@@ -3715,22 +4475,22 @@ async function refAdd(url, memo = "") {
|
|
|
3715
4475
|
});
|
|
3716
4476
|
}
|
|
3717
4477
|
async function refList() {
|
|
3718
|
-
console.log(
|
|
3719
|
-
console.log(
|
|
4478
|
+
console.log(chalk21.bold("\n\u{1F4DA} " + t("ref.listTitle")));
|
|
4479
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3720
4480
|
const refs = loadRefs();
|
|
3721
4481
|
if (refs.length === 0) {
|
|
3722
|
-
console.log(
|
|
3723
|
-
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.'));
|
|
3724
4484
|
return;
|
|
3725
4485
|
}
|
|
3726
|
-
console.log(
|
|
4486
|
+
console.log(chalk21.cyan(`
|
|
3727
4487
|
\uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
|
|
3728
4488
|
`));
|
|
3729
4489
|
refs.forEach((ref, index) => {
|
|
3730
4490
|
const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
|
|
3731
|
-
console.log(
|
|
3732
|
-
if (ref.memo) console.log(
|
|
3733
|
-
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}`));
|
|
3734
4494
|
console.log("");
|
|
3735
4495
|
});
|
|
3736
4496
|
}
|
|
@@ -3738,7 +4498,7 @@ async function refOpen(indexStr) {
|
|
|
3738
4498
|
const refs = loadRefs();
|
|
3739
4499
|
const idx = parseInt(indexStr, 10) - 1;
|
|
3740
4500
|
if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
|
|
3741
|
-
console.log(
|
|
4501
|
+
console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
|
|
3742
4502
|
return;
|
|
3743
4503
|
}
|
|
3744
4504
|
const ref = refs[idx];
|
|
@@ -3746,14 +4506,14 @@ async function refOpen(indexStr) {
|
|
|
3746
4506
|
try {
|
|
3747
4507
|
parsed = new URL(ref.url);
|
|
3748
4508
|
} catch {
|
|
3749
|
-
console.log(
|
|
4509
|
+
console.log(chalk21.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
|
|
3750
4510
|
return;
|
|
3751
4511
|
}
|
|
3752
4512
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
3753
|
-
console.log(
|
|
4513
|
+
console.log(chalk21.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
|
|
3754
4514
|
return;
|
|
3755
4515
|
}
|
|
3756
|
-
console.log(
|
|
4516
|
+
console.log(chalk21.cyan(`
|
|
3757
4517
|
\u{1F310} \uC5F4\uAE30: ${ref.url}`));
|
|
3758
4518
|
let result;
|
|
3759
4519
|
if (process.platform === "darwin") {
|
|
@@ -3764,19 +4524,19 @@ async function refOpen(indexStr) {
|
|
|
3764
4524
|
result = safeExecFile("xdg-open", [ref.url]);
|
|
3765
4525
|
}
|
|
3766
4526
|
if (result.ok) {
|
|
3767
|
-
console.log(
|
|
4527
|
+
console.log(chalk21.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
3768
4528
|
} else {
|
|
3769
|
-
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."));
|
|
3770
4530
|
}
|
|
3771
4531
|
}
|
|
3772
4532
|
|
|
3773
4533
|
// src/commands/harness.ts
|
|
3774
|
-
import { existsSync as
|
|
3775
|
-
import
|
|
4534
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4535
|
+
import chalk22 from "chalk";
|
|
3776
4536
|
import ora2 from "ora";
|
|
3777
4537
|
function detectPM() {
|
|
3778
|
-
if (
|
|
3779
|
-
if (
|
|
4538
|
+
if (existsSync9("pnpm-lock.yaml")) return "pnpm";
|
|
4539
|
+
if (existsSync9("yarn.lock")) return "yarn";
|
|
3780
4540
|
return "npm";
|
|
3781
4541
|
}
|
|
3782
4542
|
function pmRun(pm, script) {
|
|
@@ -3794,14 +4554,14 @@ function detectChecks() {
|
|
|
3794
4554
|
const pm = detectPM();
|
|
3795
4555
|
if (s.lint) {
|
|
3796
4556
|
checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
|
|
3797
|
-
} else if (
|
|
4557
|
+
} else if (existsSync9(".eslintrc.js") || existsSync9(".eslintrc.json") || existsSync9("eslint.config.js")) {
|
|
3798
4558
|
checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
|
|
3799
4559
|
}
|
|
3800
4560
|
if (s["type-check"]) {
|
|
3801
4561
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
|
|
3802
4562
|
} else if (s.typecheck) {
|
|
3803
4563
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
|
|
3804
|
-
} else if (
|
|
4564
|
+
} else if (existsSync9("tsconfig.json")) {
|
|
3805
4565
|
checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
|
|
3806
4566
|
}
|
|
3807
4567
|
if (s.test) {
|
|
@@ -3813,15 +4573,16 @@ function detectChecks() {
|
|
|
3813
4573
|
return checks;
|
|
3814
4574
|
}
|
|
3815
4575
|
async function harness() {
|
|
3816
|
-
|
|
3817
|
-
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)));
|
|
3818
4579
|
const checks = detectChecks();
|
|
3819
4580
|
if (checks.length === 0) {
|
|
3820
|
-
console.log(
|
|
3821
|
-
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."));
|
|
3822
4583
|
return;
|
|
3823
4584
|
}
|
|
3824
|
-
console.log(
|
|
4585
|
+
console.log(chalk22.cyan(`
|
|
3825
4586
|
\u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
|
|
3826
4587
|
`));
|
|
3827
4588
|
const results = [];
|
|
@@ -3833,10 +4594,10 @@ async function harness() {
|
|
|
3833
4594
|
const duration = Date.now() - start2;
|
|
3834
4595
|
const sec = (duration / 1e3).toFixed(1);
|
|
3835
4596
|
if (result.ok) {
|
|
3836
|
-
spinner.succeed(`${check2.name} ${
|
|
4597
|
+
spinner.succeed(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
|
|
3837
4598
|
results.push({ name: check2.name, command: display, passed: true, duration });
|
|
3838
4599
|
} else {
|
|
3839
|
-
spinner.fail(`${check2.name} ${
|
|
4600
|
+
spinner.fail(`${check2.name} ${chalk22.gray(`(${sec}s)`)}`);
|
|
3840
4601
|
results.push({
|
|
3841
4602
|
name: check2.name,
|
|
3842
4603
|
command: display,
|
|
@@ -3846,24 +4607,25 @@ async function harness() {
|
|
|
3846
4607
|
});
|
|
3847
4608
|
}
|
|
3848
4609
|
}
|
|
3849
|
-
console.log(
|
|
3850
|
-
console.log(
|
|
4610
|
+
console.log(chalk22.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
|
|
4611
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3851
4612
|
for (const r of results) {
|
|
3852
|
-
const icon = r.passed ?
|
|
4613
|
+
const icon = r.passed ? chalk22.green("\u2705") : chalk22.red("\u274C");
|
|
3853
4614
|
const sec = (r.duration / 1e3).toFixed(1);
|
|
3854
|
-
console.log(` ${icon} ${r.name.padEnd(15)} ${
|
|
4615
|
+
console.log(` ${icon} ${r.name.padEnd(15)} ${chalk22.gray(`${sec}s`)}`);
|
|
3855
4616
|
}
|
|
3856
4617
|
const passed = results.filter((r) => r.passed).length;
|
|
3857
4618
|
const all = passed === results.length;
|
|
3858
|
-
console.log(
|
|
4619
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3859
4620
|
if (all) {
|
|
3860
|
-
console.log(
|
|
4621
|
+
console.log(chalk22.green.bold(`
|
|
3861
4622
|
\u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
|
|
3862
4623
|
} else {
|
|
3863
4624
|
console.log(
|
|
3864
|
-
|
|
4625
|
+
chalk22.red.bold(`
|
|
3865
4626
|
\u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
|
|
3866
4627
|
);
|
|
4628
|
+
process.exitCode = 1;
|
|
3867
4629
|
}
|
|
3868
4630
|
printNextStep({
|
|
3869
4631
|
message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
|
|
@@ -3873,9 +4635,9 @@ async function harness() {
|
|
|
3873
4635
|
}
|
|
3874
4636
|
|
|
3875
4637
|
// src/commands/migrate.ts
|
|
3876
|
-
import { existsSync as
|
|
3877
|
-
import
|
|
3878
|
-
import
|
|
4638
|
+
import { existsSync as existsSync10, unlinkSync, rmSync as rmSync2 } from "fs";
|
|
4639
|
+
import chalk23 from "chalk";
|
|
4640
|
+
import inquirer11 from "inquirer";
|
|
3879
4641
|
import ora3 from "ora";
|
|
3880
4642
|
var LOCK_FILES = {
|
|
3881
4643
|
npm: "package-lock.json",
|
|
@@ -3883,26 +4645,26 @@ var LOCK_FILES = {
|
|
|
3883
4645
|
pnpm: "pnpm-lock.yaml"
|
|
3884
4646
|
};
|
|
3885
4647
|
function detectCurrentPM() {
|
|
3886
|
-
if (
|
|
3887
|
-
if (
|
|
3888
|
-
if (
|
|
4648
|
+
if (existsSync10("pnpm-lock.yaml")) return "pnpm";
|
|
4649
|
+
if (existsSync10("yarn.lock")) return "yarn";
|
|
4650
|
+
if (existsSync10("package-lock.json")) return "npm";
|
|
3889
4651
|
return null;
|
|
3890
4652
|
}
|
|
3891
4653
|
function isCLIAvailable(pm) {
|
|
3892
4654
|
return safeExecFile(pm, ["--version"]).ok;
|
|
3893
4655
|
}
|
|
3894
4656
|
async function migrate(target) {
|
|
3895
|
-
console.log(
|
|
3896
|
-
console.log(
|
|
4657
|
+
console.log(chalk23.bold("\n\u{1F504} " + t("migrate.title")));
|
|
4658
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
3897
4659
|
const current = detectCurrentPM();
|
|
3898
|
-
console.log(
|
|
4660
|
+
console.log(chalk23.cyan(`
|
|
3899
4661
|
\uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
|
|
3900
4662
|
let targetPM;
|
|
3901
4663
|
if (target && ["npm", "yarn", "pnpm"].includes(target)) {
|
|
3902
4664
|
targetPM = target;
|
|
3903
4665
|
} else {
|
|
3904
4666
|
const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
|
|
3905
|
-
const { selected } = await
|
|
4667
|
+
const { selected } = await inquirer11.prompt([
|
|
3906
4668
|
{
|
|
3907
4669
|
type: "list",
|
|
3908
4670
|
name: "selected",
|
|
@@ -3913,17 +4675,17 @@ async function migrate(target) {
|
|
|
3913
4675
|
targetPM = selected;
|
|
3914
4676
|
}
|
|
3915
4677
|
if (targetPM === current) {
|
|
3916
|
-
console.log(
|
|
4678
|
+
console.log(chalk23.yellow(`
|
|
3917
4679
|
\u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
|
|
3918
4680
|
return;
|
|
3919
4681
|
}
|
|
3920
4682
|
if (!isCLIAvailable(targetPM)) {
|
|
3921
|
-
console.log(
|
|
4683
|
+
console.log(chalk23.red(`
|
|
3922
4684
|
\u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
3923
|
-
console.log(
|
|
4685
|
+
console.log(chalk23.yellow(` npm i -g ${targetPM}`));
|
|
3924
4686
|
return;
|
|
3925
4687
|
}
|
|
3926
|
-
const { confirm } = await
|
|
4688
|
+
const { confirm } = await inquirer11.prompt([
|
|
3927
4689
|
{
|
|
3928
4690
|
type: "confirm",
|
|
3929
4691
|
name: "confirm",
|
|
@@ -3932,18 +4694,18 @@ async function migrate(target) {
|
|
|
3932
4694
|
}
|
|
3933
4695
|
]);
|
|
3934
4696
|
if (!confirm) {
|
|
3935
|
-
console.log(
|
|
4697
|
+
console.log(chalk23.gray("\uCDE8\uC18C\uB428"));
|
|
3936
4698
|
return;
|
|
3937
4699
|
}
|
|
3938
4700
|
const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
|
|
3939
4701
|
for (const lockFile of Object.values(LOCK_FILES)) {
|
|
3940
|
-
if (
|
|
4702
|
+
if (existsSync10(lockFile)) {
|
|
3941
4703
|
unlinkSync(lockFile);
|
|
3942
4704
|
}
|
|
3943
4705
|
}
|
|
3944
|
-
if (
|
|
4706
|
+
if (existsSync10("node_modules")) {
|
|
3945
4707
|
cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
|
|
3946
|
-
|
|
4708
|
+
rmSync2("node_modules", { recursive: true, force: true });
|
|
3947
4709
|
}
|
|
3948
4710
|
cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
|
|
3949
4711
|
const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
|
|
@@ -3952,10 +4714,10 @@ async function migrate(target) {
|
|
|
3952
4714
|
install.succeed(`${targetPM} install \uC644\uB8CC!`);
|
|
3953
4715
|
} else {
|
|
3954
4716
|
install.fail(`${targetPM} install \uC2E4\uD328`);
|
|
3955
|
-
console.log(
|
|
4717
|
+
console.log(chalk23.red(installResult.err.slice(0, 300)));
|
|
3956
4718
|
return;
|
|
3957
4719
|
}
|
|
3958
|
-
console.log(
|
|
4720
|
+
console.log(chalk23.green.bold(`
|
|
3959
4721
|
\u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
|
|
3960
4722
|
printNextStep({
|
|
3961
4723
|
message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
|
|
@@ -3965,17 +4727,17 @@ async function migrate(target) {
|
|
|
3965
4727
|
}
|
|
3966
4728
|
|
|
3967
4729
|
// src/commands/update.ts
|
|
3968
|
-
import { existsSync as
|
|
3969
|
-
import { dirname as dirname2, join as
|
|
4730
|
+
import { existsSync as existsSync11 } from "fs";
|
|
4731
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
3970
4732
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3971
|
-
import
|
|
4733
|
+
import chalk24 from "chalk";
|
|
3972
4734
|
import ora4 from "ora";
|
|
3973
4735
|
var PACKAGE = "@byh3071/vhk";
|
|
3974
4736
|
function getCurrentVersion() {
|
|
3975
4737
|
const dir = dirname2(fileURLToPath3(import.meta.url));
|
|
3976
|
-
for (const pkgPath of [
|
|
4738
|
+
for (const pkgPath of [join7(dir, "../package.json"), join7(dir, "../../package.json")]) {
|
|
3977
4739
|
try {
|
|
3978
|
-
if (
|
|
4740
|
+
if (existsSync11(pkgPath)) {
|
|
3979
4741
|
const pkg = readJsonFile(pkgPath);
|
|
3980
4742
|
if (pkg.version) return pkg.version;
|
|
3981
4743
|
}
|
|
@@ -3998,32 +4760,32 @@ function isUpToDate(current, latest) {
|
|
|
3998
4760
|
return cc >= lc;
|
|
3999
4761
|
}
|
|
4000
4762
|
async function update() {
|
|
4001
|
-
console.log(
|
|
4002
|
-
console.log(
|
|
4763
|
+
console.log(chalk24.bold("\n\u2B06\uFE0F " + t("update.title")));
|
|
4764
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4003
4765
|
const current = getCurrentVersion();
|
|
4004
|
-
console.log(
|
|
4766
|
+
console.log(chalk24.cyan(`
|
|
4005
4767
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
|
|
4006
4768
|
const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
|
|
4007
4769
|
const latest = getLatestVersion();
|
|
4008
4770
|
if (!latest) {
|
|
4009
4771
|
spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
4010
|
-
console.log(
|
|
4011
|
-
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}`));
|
|
4012
4774
|
return;
|
|
4013
4775
|
}
|
|
4014
4776
|
spinner.stop();
|
|
4015
|
-
console.log(
|
|
4777
|
+
console.log(chalk24.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
|
|
4016
4778
|
if (isUpToDate(current, latest)) {
|
|
4017
|
-
console.log(
|
|
4779
|
+
console.log(chalk24.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
|
|
4018
4780
|
return;
|
|
4019
4781
|
}
|
|
4020
4782
|
const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
|
|
4021
4783
|
const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
|
|
4022
4784
|
if (upd.ok) {
|
|
4023
4785
|
updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
|
|
4024
|
-
console.log(
|
|
4786
|
+
console.log(chalk24.green.bold(`
|
|
4025
4787
|
\u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
4026
|
-
console.log(
|
|
4788
|
+
console.log(chalk24.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
4027
4789
|
printNextStep({
|
|
4028
4790
|
message: t("update.nextOkMessage"),
|
|
4029
4791
|
command: "vhk --version",
|
|
@@ -4031,9 +4793,9 @@ async function update() {
|
|
|
4031
4793
|
});
|
|
4032
4794
|
} else {
|
|
4033
4795
|
updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
|
|
4034
|
-
console.log(
|
|
4035
|
-
console.log(
|
|
4036
|
-
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}`));
|
|
4037
4799
|
printNextStep({
|
|
4038
4800
|
message: t("update.nextFailMessage"),
|
|
4039
4801
|
command: "vhk doctor",
|
|
@@ -4044,121 +4806,15 @@ async function update() {
|
|
|
4044
4806
|
|
|
4045
4807
|
// src/commands/context.ts
|
|
4046
4808
|
import {
|
|
4047
|
-
existsSync as
|
|
4809
|
+
existsSync as existsSync12,
|
|
4048
4810
|
mkdirSync as mkdirSync7,
|
|
4049
|
-
readFileSync as
|
|
4811
|
+
readFileSync as readFileSync5,
|
|
4050
4812
|
readdirSync as readdirSync2,
|
|
4051
4813
|
statSync as statSync2,
|
|
4052
4814
|
writeFileSync as writeFileSync7
|
|
4053
4815
|
} from "fs";
|
|
4054
|
-
import { join as
|
|
4055
|
-
import
|
|
4056
|
-
|
|
4057
|
-
// src/lib/state-files.ts
|
|
4058
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync6, appendFileSync, rmSync as rmSync2 } from "fs";
|
|
4059
|
-
import { join as join5 } from "path";
|
|
4060
|
-
var STATE_DIR2 = "docs/state";
|
|
4061
|
-
var BLOCKERS_PATH = join5(STATE_DIR2, "blockers.md");
|
|
4062
|
-
var LEARNINGS_PATH = join5(STATE_DIR2, "learnings.md");
|
|
4063
|
-
var VHK_DIR = ".vhk";
|
|
4064
|
-
var HARD_STOP_PATH = join5(VHK_DIR, "HARD_STOP");
|
|
4065
|
-
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
4066
|
-
function ensureStateDir() {
|
|
4067
|
-
mkdirSync6(STATE_DIR2, { recursive: true });
|
|
4068
|
-
}
|
|
4069
|
-
function ensureVhkDir() {
|
|
4070
|
-
mkdirSync6(VHK_DIR, { recursive: true });
|
|
4071
|
-
}
|
|
4072
|
-
function isoDate() {
|
|
4073
|
-
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4074
|
-
}
|
|
4075
|
-
var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
|
|
4076
|
-
function countActiveBlockers(content) {
|
|
4077
|
-
let count = 0;
|
|
4078
|
-
for (const line of content.split(/\r?\n/)) {
|
|
4079
|
-
if (ACTIVE_BLOCKER_RE.test(line)) count++;
|
|
4080
|
-
}
|
|
4081
|
-
return count;
|
|
4082
|
-
}
|
|
4083
|
-
function appendBlocker(description, goalId) {
|
|
4084
|
-
ensureStateDir();
|
|
4085
|
-
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
4086
|
-
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
4087
|
-
if (!existsSync10(BLOCKERS_PATH)) {
|
|
4088
|
-
writeFileSync6(
|
|
4089
|
-
BLOCKERS_PATH,
|
|
4090
|
-
`# Blockers
|
|
4091
|
-
|
|
4092
|
-
_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
|
|
4093
|
-
|
|
4094
|
-
${line}
|
|
4095
|
-
`,
|
|
4096
|
-
"utf-8"
|
|
4097
|
-
);
|
|
4098
|
-
} else {
|
|
4099
|
-
appendFileSync(BLOCKERS_PATH, `${line}
|
|
4100
|
-
`, "utf-8");
|
|
4101
|
-
}
|
|
4102
|
-
const current = readFileSync3(BLOCKERS_PATH, "utf-8");
|
|
4103
|
-
const count = countActiveBlockers(current);
|
|
4104
|
-
let hardStopTripped = false;
|
|
4105
|
-
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync10(HARD_STOP_PATH)) {
|
|
4106
|
-
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
4107
|
-
hardStopTripped = true;
|
|
4108
|
-
}
|
|
4109
|
-
return { count, hardStopTripped };
|
|
4110
|
-
}
|
|
4111
|
-
function appendLearning(lesson, goalId) {
|
|
4112
|
-
ensureStateDir();
|
|
4113
|
-
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
4114
|
-
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
4115
|
-
if (!existsSync10(LEARNINGS_PATH)) {
|
|
4116
|
-
writeFileSync6(
|
|
4117
|
-
LEARNINGS_PATH,
|
|
4118
|
-
`# Learnings
|
|
4119
|
-
|
|
4120
|
-
_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
|
|
4121
|
-
|
|
4122
|
-
${line}
|
|
4123
|
-
`,
|
|
4124
|
-
"utf-8"
|
|
4125
|
-
);
|
|
4126
|
-
} else {
|
|
4127
|
-
appendFileSync(LEARNINGS_PATH, `${line}
|
|
4128
|
-
`, "utf-8");
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4131
|
-
function getRecentLearnings(limit = 3) {
|
|
4132
|
-
if (!existsSync10(LEARNINGS_PATH)) return [];
|
|
4133
|
-
const lines = readFileSync3(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
4134
|
-
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
4135
|
-
return entries.slice(-limit);
|
|
4136
|
-
}
|
|
4137
|
-
function writeHardStop(reason) {
|
|
4138
|
-
ensureVhkDir();
|
|
4139
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
4140
|
-
writeFileSync6(HARD_STOP_PATH, `${ts}
|
|
4141
|
-
${reason}
|
|
4142
|
-
`, "utf-8");
|
|
4143
|
-
}
|
|
4144
|
-
function isHardStopActive() {
|
|
4145
|
-
return existsSync10(HARD_STOP_PATH);
|
|
4146
|
-
}
|
|
4147
|
-
function readHardStopReason() {
|
|
4148
|
-
if (!existsSync10(HARD_STOP_PATH)) return null;
|
|
4149
|
-
try {
|
|
4150
|
-
return readFileSync3(HARD_STOP_PATH, "utf-8").trim();
|
|
4151
|
-
} catch {
|
|
4152
|
-
return null;
|
|
4153
|
-
}
|
|
4154
|
-
}
|
|
4155
|
-
function clearHardStop() {
|
|
4156
|
-
if (!existsSync10(HARD_STOP_PATH)) return false;
|
|
4157
|
-
rmSync2(HARD_STOP_PATH, { force: true });
|
|
4158
|
-
return true;
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
// src/commands/context.ts
|
|
4816
|
+
import { join as join8 } from "path";
|
|
4817
|
+
import chalk25 from "chalk";
|
|
4162
4818
|
var CONTEXT_PATH2 = ".vhk/context.md";
|
|
4163
4819
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4164
4820
|
"node_modules",
|
|
@@ -4183,7 +4839,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
|
|
|
4183
4839
|
filtered.forEach((entry, index) => {
|
|
4184
4840
|
const isLast = index === filtered.length - 1;
|
|
4185
4841
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4186
|
-
const fullPath =
|
|
4842
|
+
const fullPath = join8(dir, entry);
|
|
4187
4843
|
const stat = statSync2(fullPath);
|
|
4188
4844
|
const isDir = stat.isDirectory();
|
|
4189
4845
|
lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
@@ -4215,8 +4871,8 @@ function extractTechStack() {
|
|
|
4215
4871
|
else if (all.jest) stack["\uD14C\uC2A4\uD2B8"] = "jest";
|
|
4216
4872
|
if (all.commander) stack["CLI"] = "commander";
|
|
4217
4873
|
if (all.inquirer) stack["\uC778\uD130\uB799\uD2F0\uBE0C"] = "inquirer";
|
|
4218
|
-
if (
|
|
4219
|
-
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";
|
|
4220
4876
|
else stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "npm";
|
|
4221
4877
|
if (pkg.name) stack["\uD328\uD0A4\uC9C0 \uC774\uB984"] = pkg.name;
|
|
4222
4878
|
if (pkg.version) stack["\uBC84\uC804"] = pkg.version;
|
|
@@ -4257,11 +4913,12 @@ function getVhkCommands() {
|
|
|
4257
4913
|
"mcp-init \u2014 Cursor MCP \uC124\uC815"
|
|
4258
4914
|
];
|
|
4259
4915
|
}
|
|
4260
|
-
async function context() {
|
|
4261
|
-
|
|
4262
|
-
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)));
|
|
4263
4920
|
const stack = extractTechStack();
|
|
4264
|
-
const tree = buildTree(".").join("\n");
|
|
4921
|
+
const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
|
|
4265
4922
|
const commands = getVhkCommands();
|
|
4266
4923
|
const lines = [];
|
|
4267
4924
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
|
|
@@ -4277,25 +4934,37 @@ async function context() {
|
|
|
4277
4934
|
lines.push("");
|
|
4278
4935
|
lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
|
|
4279
4936
|
lines.push("");
|
|
4280
|
-
lines.push("```");
|
|
4937
|
+
lines.push("```text");
|
|
4281
4938
|
lines.push(tree);
|
|
4282
4939
|
lines.push("```");
|
|
4283
4940
|
lines.push("");
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
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("");
|
|
4288
4953
|
}
|
|
4289
|
-
|
|
4290
|
-
if (existsSync11(".vhk/memory.json")) {
|
|
4954
|
+
if (existsSync12(".vhk/memory.json")) {
|
|
4291
4955
|
try {
|
|
4292
4956
|
const memories = readJsonFile(
|
|
4293
4957
|
".vhk/memory.json"
|
|
4294
4958
|
);
|
|
4295
4959
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
4960
|
+
const recentMemories = memories.slice(-5);
|
|
4296
4961
|
lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
|
|
4297
4962
|
lines.push("");
|
|
4298
|
-
|
|
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) {
|
|
4299
4968
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4300
4969
|
lines.push(`- ${m.content} _(${date})_`);
|
|
4301
4970
|
}
|
|
@@ -4326,6 +4995,24 @@ async function context() {
|
|
|
4326
4995
|
for (const r of recent) lines.push(r);
|
|
4327
4996
|
lines.push("");
|
|
4328
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
|
+
}
|
|
4329
5016
|
if (isHardStopActive()) {
|
|
4330
5017
|
lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
|
|
4331
5018
|
lines.push("");
|
|
@@ -4344,10 +5031,10 @@ async function context() {
|
|
|
4344
5031
|
lines.push("");
|
|
4345
5032
|
mkdirSync7(".vhk", { recursive: true });
|
|
4346
5033
|
writeFileSync7(CONTEXT_PATH2, lines.join("\n"), "utf-8");
|
|
4347
|
-
console.log(
|
|
5034
|
+
console.log(chalk25.green(`
|
|
4348
5035
|
\u2705 ${CONTEXT_PATH2} \uC0DD\uC131 \uC644\uB8CC!`));
|
|
4349
|
-
console.log(
|
|
4350
|
-
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."));
|
|
4351
5038
|
printNextStep({
|
|
4352
5039
|
message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
|
|
4353
5040
|
command: "vhk context-show",
|
|
@@ -4355,23 +5042,23 @@ async function context() {
|
|
|
4355
5042
|
});
|
|
4356
5043
|
}
|
|
4357
5044
|
async function contextShow() {
|
|
4358
|
-
console.log(
|
|
4359
|
-
console.log(
|
|
4360
|
-
if (!
|
|
4361
|
-
console.log(
|
|
4362
|
-
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."));
|
|
4363
5050
|
return;
|
|
4364
5051
|
}
|
|
4365
|
-
const content =
|
|
5052
|
+
const content = readFileSync5(CONTEXT_PATH2, "utf-8");
|
|
4366
5053
|
console.log("\n" + content);
|
|
4367
5054
|
}
|
|
4368
5055
|
|
|
4369
5056
|
// src/commands/memory.ts
|
|
4370
|
-
import { existsSync as
|
|
4371
|
-
import
|
|
5057
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
5058
|
+
import chalk26 from "chalk";
|
|
4372
5059
|
var MEMORY_PATH = ".vhk/memory.json";
|
|
4373
5060
|
function loadMemories() {
|
|
4374
|
-
if (!
|
|
5061
|
+
if (!existsSync13(MEMORY_PATH)) return [];
|
|
4375
5062
|
try {
|
|
4376
5063
|
const parsed = readJsonFile(MEMORY_PATH);
|
|
4377
5064
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -4384,11 +5071,12 @@ function saveMemories(memories) {
|
|
|
4384
5071
|
writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
4385
5072
|
}
|
|
4386
5073
|
async function memoryAdd(content, tags) {
|
|
4387
|
-
console.log(
|
|
4388
|
-
console.log(
|
|
5074
|
+
console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.addTitle")));
|
|
5075
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
4389
5076
|
if (!content) {
|
|
4390
|
-
console.log(
|
|
4391
|
-
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;
|
|
4392
5080
|
return;
|
|
4393
5081
|
}
|
|
4394
5082
|
const memories = loadMemories();
|
|
@@ -4398,9 +5086,9 @@ async function memoryAdd(content, tags) {
|
|
|
4398
5086
|
tags: tags && tags.length > 0 ? tags : []
|
|
4399
5087
|
});
|
|
4400
5088
|
saveMemories(memories);
|
|
4401
|
-
console.log(
|
|
5089
|
+
console.log(chalk26.green(`
|
|
4402
5090
|
\u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
|
|
4403
|
-
console.log(
|
|
5091
|
+
console.log(chalk26.cyan(` \u{1F4DD} ${content}`));
|
|
4404
5092
|
printNextStep({
|
|
4405
5093
|
message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
|
|
4406
5094
|
command: "vhk memory list",
|
|
@@ -4408,24 +5096,24 @@ async function memoryAdd(content, tags) {
|
|
|
4408
5096
|
});
|
|
4409
5097
|
}
|
|
4410
5098
|
async function memoryList() {
|
|
4411
|
-
console.log(
|
|
4412
|
-
console.log(
|
|
5099
|
+
console.log(chalk26.bold("\n\u{1F9E0} " + t("memory.listTitle")));
|
|
5100
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
4413
5101
|
const memories = loadMemories();
|
|
4414
5102
|
if (memories.length === 0) {
|
|
4415
|
-
console.log(
|
|
4416
|
-
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.'));
|
|
4417
5105
|
return;
|
|
4418
5106
|
}
|
|
4419
|
-
console.log(
|
|
5107
|
+
console.log(chalk26.cyan(`
|
|
4420
5108
|
\uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
|
|
4421
5109
|
`));
|
|
4422
5110
|
memories.forEach((m, index) => {
|
|
4423
5111
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4424
|
-
console.log(
|
|
5112
|
+
console.log(chalk26.white(` [${index + 1}] ${m.content}`));
|
|
4425
5113
|
if (m.tags && m.tags.length > 0) {
|
|
4426
|
-
console.log(
|
|
5114
|
+
console.log(chalk26.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
|
|
4427
5115
|
}
|
|
4428
|
-
console.log(
|
|
5116
|
+
console.log(chalk26.gray(` \u{1F4C5} ${date}`));
|
|
4429
5117
|
console.log("");
|
|
4430
5118
|
});
|
|
4431
5119
|
}
|
|
@@ -4433,26 +5121,44 @@ async function memoryRemove(indexStr) {
|
|
|
4433
5121
|
const memories = loadMemories();
|
|
4434
5122
|
const idx = parseInt(indexStr, 10) - 1;
|
|
4435
5123
|
if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
|
|
4436
|
-
console.log(
|
|
5124
|
+
console.log(chalk26.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
|
|
4437
5125
|
return;
|
|
4438
5126
|
}
|
|
4439
5127
|
const removed = memories.splice(idx, 1)[0];
|
|
4440
5128
|
saveMemories(memories);
|
|
4441
|
-
console.log(
|
|
4442
|
-
console.log(
|
|
5129
|
+
console.log(chalk26.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
|
|
5130
|
+
console.log(chalk26.gray(` ${removed.content}`));
|
|
4443
5131
|
}
|
|
4444
5132
|
|
|
4445
5133
|
// src/commands/brief.ts
|
|
4446
|
-
import { existsSync as
|
|
4447
|
-
import
|
|
5134
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync6 } from "fs";
|
|
5135
|
+
import chalk27 from "chalk";
|
|
4448
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
|
+
}
|
|
4449
5155
|
function git2(args) {
|
|
4450
5156
|
const result = safeExecFile("git", args);
|
|
4451
5157
|
return result.ok ? result.out : "";
|
|
4452
5158
|
}
|
|
4453
5159
|
async function brief() {
|
|
4454
|
-
console.log(
|
|
4455
|
-
console.log(
|
|
5160
|
+
console.log(chalk27.bold("\n\u{1F4CB} " + t("brief.title")));
|
|
5161
|
+
console.log(chalk27.gray("\u2500".repeat(40)));
|
|
4456
5162
|
const lines = [];
|
|
4457
5163
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
|
|
4458
5164
|
lines.push("");
|
|
@@ -4460,11 +5166,12 @@ async function brief() {
|
|
|
4460
5166
|
lines.push("");
|
|
4461
5167
|
try {
|
|
4462
5168
|
const pkg = readJsonFile("package.json");
|
|
5169
|
+
const id = readProjectIdentity();
|
|
4463
5170
|
lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
|
|
4464
5171
|
lines.push("");
|
|
4465
|
-
lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
|
|
5172
|
+
lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
|
|
4466
5173
|
lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
|
|
4467
|
-
lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
|
|
5174
|
+
lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
|
|
4468
5175
|
const deps = Object.keys(pkg.dependencies ?? {}).length;
|
|
4469
5176
|
const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
|
|
4470
5177
|
lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
|
|
@@ -4488,7 +5195,7 @@ async function brief() {
|
|
|
4488
5195
|
`- **\uBBF8\uCEE4\uBC0B \uBCC0\uACBD**: ${uncommitted ? `${uncommitted.split("\n").length}\uAC1C \uD30C\uC77C` : "\uC5C6\uC74C \u2705"}`
|
|
4489
5196
|
);
|
|
4490
5197
|
lines.push("");
|
|
4491
|
-
if (
|
|
5198
|
+
if (existsSync14(".vhk/memory.json")) {
|
|
4492
5199
|
try {
|
|
4493
5200
|
const memories = readJsonFile(".vhk/memory.json");
|
|
4494
5201
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
@@ -4505,7 +5212,7 @@ async function brief() {
|
|
|
4505
5212
|
} catch {
|
|
4506
5213
|
}
|
|
4507
5214
|
}
|
|
4508
|
-
if (
|
|
5215
|
+
if (existsSync14(".vhk/refs.json")) {
|
|
4509
5216
|
try {
|
|
4510
5217
|
const refs = readJsonFile(".vhk/refs.json");
|
|
4511
5218
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
@@ -4536,7 +5243,7 @@ async function brief() {
|
|
|
4536
5243
|
mkdirSync9(".vhk", { recursive: true });
|
|
4537
5244
|
writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
|
|
4538
5245
|
console.log("\n" + lines.join("\n"));
|
|
4539
|
-
console.log(
|
|
5246
|
+
console.log(chalk27.green(`
|
|
4540
5247
|
\u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
|
|
4541
5248
|
printNextStep({
|
|
4542
5249
|
message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -4546,11 +5253,11 @@ async function brief() {
|
|
|
4546
5253
|
}
|
|
4547
5254
|
|
|
4548
5255
|
// src/commands/start.ts
|
|
4549
|
-
import
|
|
4550
|
-
import
|
|
5256
|
+
import chalk28 from "chalk";
|
|
5257
|
+
import inquirer12 from "inquirer";
|
|
4551
5258
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4552
|
-
import { existsSync as
|
|
4553
|
-
import { join as
|
|
5259
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5260
|
+
import { join as join9 } from "path";
|
|
4554
5261
|
var VHK_FOOTPRINT_FILES = [
|
|
4555
5262
|
"CLAUDE.md",
|
|
4556
5263
|
".cursorrules",
|
|
@@ -4559,7 +5266,7 @@ var VHK_FOOTPRINT_FILES = [
|
|
|
4559
5266
|
"docs/PRD.md"
|
|
4560
5267
|
];
|
|
4561
5268
|
function detectExistingFootprint(cwd) {
|
|
4562
|
-
return VHK_FOOTPRINT_FILES.filter((rel) =>
|
|
5269
|
+
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync15(join9(cwd, rel)));
|
|
4563
5270
|
}
|
|
4564
5271
|
async function runGitInit(cwd) {
|
|
4565
5272
|
try {
|
|
@@ -4588,22 +5295,22 @@ async function runStep(label, fn) {
|
|
|
4588
5295
|
}
|
|
4589
5296
|
}
|
|
4590
5297
|
async function start(options = {}) {
|
|
4591
|
-
console.log(
|
|
5298
|
+
console.log(chalk28.bold(`
|
|
4592
5299
|
${ko.start.title}
|
|
4593
5300
|
`));
|
|
4594
|
-
console.log(
|
|
4595
|
-
console.log(
|
|
4596
|
-
console.log(
|
|
4597
|
-
console.log(
|
|
4598
|
-
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}`));
|
|
4599
5306
|
console.log();
|
|
4600
5307
|
const cwd = process.cwd();
|
|
4601
5308
|
const footprint = detectExistingFootprint(cwd);
|
|
4602
5309
|
if (footprint.length > 0 && !options.yes) {
|
|
4603
|
-
console.log(
|
|
4604
|
-
for (const f of footprint) console.log(
|
|
4605
|
-
console.log(
|
|
4606
|
-
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([{
|
|
4607
5314
|
type: "confirm",
|
|
4608
5315
|
name: "proceedExisting",
|
|
4609
5316
|
message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
@@ -4614,7 +5321,7 @@ ${ko.start.title}
|
|
|
4614
5321
|
return;
|
|
4615
5322
|
}
|
|
4616
5323
|
} else if (!options.yes) {
|
|
4617
|
-
const { proceed } = await
|
|
5324
|
+
const { proceed } = await inquirer12.prompt([{
|
|
4618
5325
|
type: "confirm",
|
|
4619
5326
|
name: "proceed",
|
|
4620
5327
|
message: ko.start.confirmStart,
|
|
@@ -4640,7 +5347,7 @@ ${ko.start.title}
|
|
|
4640
5347
|
await runStep("[3/4] vhk mcp-init", () => mcpInit());
|
|
4641
5348
|
log.step(ko.start.step4Header);
|
|
4642
5349
|
await runStep("[4/4] vhk context", () => context());
|
|
4643
|
-
console.log(
|
|
5350
|
+
console.log(chalk28.bold.green(`
|
|
4644
5351
|
${ko.start.allDone}
|
|
4645
5352
|
`));
|
|
4646
5353
|
printNextStep({
|
|
@@ -4650,15 +5357,15 @@ ${ko.start.allDone}
|
|
|
4650
5357
|
}
|
|
4651
5358
|
|
|
4652
5359
|
// src/commands/cloud.ts
|
|
4653
|
-
import
|
|
5360
|
+
import fs15 from "fs";
|
|
4654
5361
|
import os from "os";
|
|
4655
|
-
import
|
|
4656
|
-
import
|
|
5362
|
+
import path16 from "path";
|
|
5363
|
+
import chalk29 from "chalk";
|
|
4657
5364
|
|
|
4658
5365
|
// src/lib/vhk-cloud.ts
|
|
4659
5366
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
4660
|
-
import
|
|
4661
|
-
import
|
|
5367
|
+
import fs14 from "fs";
|
|
5368
|
+
import path15 from "path";
|
|
4662
5369
|
var DEFAULT_CLOUD_EXCLUDES = [
|
|
4663
5370
|
"memory.json",
|
|
4664
5371
|
// 개인 의사결정 메모
|
|
@@ -4676,17 +5383,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
|
|
|
4676
5383
|
function loadVhkignore(rootDir) {
|
|
4677
5384
|
const ig = (0, import_ignore.default)();
|
|
4678
5385
|
ig.add(DEFAULT_CLOUD_EXCLUDES);
|
|
4679
|
-
const ignorePath =
|
|
4680
|
-
if (
|
|
4681
|
-
ig.add(
|
|
5386
|
+
const ignorePath = path15.join(rootDir, ".vhkignore");
|
|
5387
|
+
if (fs14.existsSync(ignorePath)) {
|
|
5388
|
+
ig.add(fs14.readFileSync(ignorePath, "utf-8"));
|
|
4682
5389
|
}
|
|
4683
5390
|
return ig;
|
|
4684
5391
|
}
|
|
4685
5392
|
function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
|
|
4686
|
-
const vhkDir =
|
|
5393
|
+
const vhkDir = path15.join(rootDir, VHK_DIR2);
|
|
4687
5394
|
let entries;
|
|
4688
5395
|
try {
|
|
4689
|
-
entries =
|
|
5396
|
+
entries = fs14.readdirSync(vhkDir, { withFileTypes: true });
|
|
4690
5397
|
} catch {
|
|
4691
5398
|
return [];
|
|
4692
5399
|
}
|
|
@@ -4702,10 +5409,10 @@ function partitionGistFiles(gistFiles, ig) {
|
|
|
4702
5409
|
return { keep, excluded };
|
|
4703
5410
|
}
|
|
4704
5411
|
function readCloudConfig(rootDir) {
|
|
4705
|
-
const p =
|
|
4706
|
-
if (!
|
|
5412
|
+
const p = path15.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
|
|
5413
|
+
if (!fs14.existsSync(p)) return null;
|
|
4707
5414
|
try {
|
|
4708
|
-
const parsed = JSON.parse(
|
|
5415
|
+
const parsed = JSON.parse(fs14.readFileSync(p, "utf-8"));
|
|
4709
5416
|
if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
|
|
4710
5417
|
return { gistId: parsed.gistId };
|
|
4711
5418
|
}
|
|
@@ -4715,24 +5422,24 @@ function readCloudConfig(rootDir) {
|
|
|
4715
5422
|
}
|
|
4716
5423
|
}
|
|
4717
5424
|
function writeCloudConfig(rootDir, config) {
|
|
4718
|
-
const vhkDir =
|
|
4719
|
-
|
|
4720
|
-
const p =
|
|
4721
|
-
|
|
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");
|
|
4722
5429
|
}
|
|
4723
5430
|
|
|
4724
5431
|
// src/commands/cloud.ts
|
|
4725
5432
|
function ensureGhReady() {
|
|
4726
5433
|
const ver = safeExecFile("gh", ["--version"]);
|
|
4727
5434
|
if (!ver.ok) {
|
|
4728
|
-
console.log(
|
|
4729
|
-
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`)"));
|
|
4730
5437
|
return false;
|
|
4731
5438
|
}
|
|
4732
5439
|
const auth = safeExecFile("gh", ["auth", "status"]);
|
|
4733
5440
|
if (!auth.ok) {
|
|
4734
|
-
console.log(
|
|
4735
|
-
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)"));
|
|
4736
5443
|
return false;
|
|
4737
5444
|
}
|
|
4738
5445
|
return true;
|
|
@@ -4745,29 +5452,29 @@ function parseGistId(output) {
|
|
|
4745
5452
|
return null;
|
|
4746
5453
|
}
|
|
4747
5454
|
async function cloudPush() {
|
|
4748
|
-
console.log(
|
|
5455
|
+
console.log(chalk29.bold(`
|
|
4749
5456
|
${ko.cloud.pushTitle}
|
|
4750
5457
|
`));
|
|
4751
5458
|
const cwd = process.cwd();
|
|
4752
|
-
if (!
|
|
4753
|
-
console.log(
|
|
5459
|
+
if (!fs15.existsSync(path16.join(cwd, VHK_DIR2))) {
|
|
5460
|
+
console.log(chalk29.yellow(` ${ko.cloud.noVhkDir}`));
|
|
4754
5461
|
return;
|
|
4755
5462
|
}
|
|
4756
5463
|
const ig = loadVhkignore(cwd);
|
|
4757
5464
|
const files = collectVhkFiles(cwd, ig);
|
|
4758
5465
|
if (files.length === 0) {
|
|
4759
|
-
console.log(
|
|
5466
|
+
console.log(chalk29.yellow(` ${ko.cloud.nothingToSync}`));
|
|
4760
5467
|
return;
|
|
4761
5468
|
}
|
|
4762
5469
|
if (!ensureGhReady()) {
|
|
4763
5470
|
process.exitCode = 1;
|
|
4764
5471
|
return;
|
|
4765
5472
|
}
|
|
4766
|
-
const filePaths = files.map((f) =>
|
|
4767
|
-
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(", ")}
|
|
4768
5475
|
`));
|
|
4769
5476
|
const existing = readCloudConfig(cwd);
|
|
4770
|
-
const desc = `vhk .vhk backup \u2014 ${
|
|
5477
|
+
const desc = `vhk .vhk backup \u2014 ${path16.basename(cwd)}`;
|
|
4771
5478
|
if (existing) {
|
|
4772
5479
|
const gistFiles = listGistFiles(existing.gistId);
|
|
4773
5480
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -4776,8 +5483,8 @@ ${ko.cloud.pushTitle}
|
|
|
4776
5483
|
const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
|
|
4777
5484
|
const res2 = safeExecFile("gh", args);
|
|
4778
5485
|
if (!res2.ok) {
|
|
4779
|
-
console.log(
|
|
4780
|
-
console.log(
|
|
5486
|
+
console.log(chalk29.red(` ${ko.cloud.pushFail}: ${name}`));
|
|
5487
|
+
console.log(chalk29.dim(` ${res2.err}`));
|
|
4781
5488
|
process.exitCode = 1;
|
|
4782
5489
|
return;
|
|
4783
5490
|
}
|
|
@@ -4792,15 +5499,15 @@ ${ko.cloud.pushTitle}
|
|
|
4792
5499
|
if (!purgeFailed.includes(name)) purgeFailed.push(name);
|
|
4793
5500
|
}
|
|
4794
5501
|
}
|
|
4795
|
-
console.log(
|
|
4796
|
-
console.log(
|
|
5502
|
+
console.log(chalk29.green.bold(` ${ko.cloud.pushDone}`));
|
|
5503
|
+
console.log(chalk29.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
|
|
4797
5504
|
if (excluded.length > 0) {
|
|
4798
5505
|
const purged = excluded.filter((n) => !purgeFailed.includes(n));
|
|
4799
5506
|
if (purged.length > 0) {
|
|
4800
|
-
console.log(
|
|
5507
|
+
console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
|
|
4801
5508
|
}
|
|
4802
5509
|
if (purgeFailed.length > 0) {
|
|
4803
|
-
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)`));
|
|
4804
5511
|
}
|
|
4805
5512
|
}
|
|
4806
5513
|
printPushNext();
|
|
@@ -4808,32 +5515,32 @@ ${ko.cloud.pushTitle}
|
|
|
4808
5515
|
}
|
|
4809
5516
|
const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
|
|
4810
5517
|
if (!res.ok) {
|
|
4811
|
-
console.log(
|
|
4812
|
-
console.log(
|
|
5518
|
+
console.log(chalk29.red(` ${ko.cloud.pushFail}`));
|
|
5519
|
+
console.log(chalk29.dim(` ${res.err || res.out}`));
|
|
4813
5520
|
process.exitCode = 1;
|
|
4814
5521
|
return;
|
|
4815
5522
|
}
|
|
4816
5523
|
const gistId = parseGistId(res.out);
|
|
4817
5524
|
if (!gistId) {
|
|
4818
|
-
console.log(
|
|
4819
|
-
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}`));
|
|
4820
5527
|
process.exitCode = 1;
|
|
4821
5528
|
return;
|
|
4822
5529
|
}
|
|
4823
5530
|
writeCloudConfig(cwd, { gistId });
|
|
4824
|
-
console.log(
|
|
4825
|
-
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`));
|
|
4826
5533
|
printPushNext();
|
|
4827
5534
|
}
|
|
4828
5535
|
async function cloudPull(gistIdArg) {
|
|
4829
|
-
console.log(
|
|
5536
|
+
console.log(chalk29.bold(`
|
|
4830
5537
|
${ko.cloud.pullTitle}
|
|
4831
5538
|
`));
|
|
4832
5539
|
const cwd = process.cwd();
|
|
4833
5540
|
const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
|
|
4834
5541
|
if (!gistId) {
|
|
4835
|
-
console.log(
|
|
4836
|
-
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)"));
|
|
4837
5544
|
return;
|
|
4838
5545
|
}
|
|
4839
5546
|
if (!ensureGhReady()) {
|
|
@@ -4842,34 +5549,34 @@ ${ko.cloud.pullTitle}
|
|
|
4842
5549
|
}
|
|
4843
5550
|
const allNames = listGistFiles(gistId);
|
|
4844
5551
|
if (allNames.length === 0) {
|
|
4845
|
-
console.log(
|
|
5552
|
+
console.log(chalk29.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
|
|
4846
5553
|
process.exitCode = 1;
|
|
4847
5554
|
return;
|
|
4848
5555
|
}
|
|
4849
5556
|
const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
|
|
4850
5557
|
if (skipped.length > 0) {
|
|
4851
|
-
console.log(
|
|
5558
|
+
console.log(chalk29.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
|
|
4852
5559
|
}
|
|
4853
5560
|
if (names.length === 0) {
|
|
4854
|
-
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).`));
|
|
4855
5562
|
return;
|
|
4856
5563
|
}
|
|
4857
|
-
const vhkDir =
|
|
4858
|
-
|
|
5564
|
+
const vhkDir = path16.join(cwd, VHK_DIR2);
|
|
5565
|
+
fs15.mkdirSync(vhkDir, { recursive: true });
|
|
4859
5566
|
let restored = 0;
|
|
4860
5567
|
for (const name of names) {
|
|
4861
5568
|
const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
|
|
4862
5569
|
if (!res.ok) {
|
|
4863
|
-
console.log(
|
|
4864
|
-
console.log(
|
|
5570
|
+
console.log(chalk29.red(` ${ko.cloud.pullFail}: ${name}`));
|
|
5571
|
+
console.log(chalk29.dim(` ${res.err}`));
|
|
4865
5572
|
continue;
|
|
4866
5573
|
}
|
|
4867
|
-
|
|
5574
|
+
fs15.writeFileSync(path16.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
|
|
4868
5575
|
restored++;
|
|
4869
5576
|
}
|
|
4870
5577
|
writeCloudConfig(cwd, { gistId });
|
|
4871
|
-
console.log(
|
|
4872
|
-
console.log(
|
|
5578
|
+
console.log(chalk29.green.bold(` ${ko.cloud.pullDone}`));
|
|
5579
|
+
console.log(chalk29.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
|
|
4873
5580
|
printNextStep({
|
|
4874
5581
|
message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
|
|
4875
5582
|
command: "vhk \uB9E5\uB77D",
|
|
@@ -4881,9 +5588,9 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4881
5588
|
const body = JSON.stringify({
|
|
4882
5589
|
files: Object.fromEntries(names.map((n) => [n, null]))
|
|
4883
5590
|
});
|
|
4884
|
-
const tmp =
|
|
5591
|
+
const tmp = path16.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
|
|
4885
5592
|
try {
|
|
4886
|
-
|
|
5593
|
+
fs15.writeFileSync(tmp, body, "utf-8");
|
|
4887
5594
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4888
5595
|
const res = safeExecFile(
|
|
4889
5596
|
"gh",
|
|
@@ -4895,7 +5602,7 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4895
5602
|
return false;
|
|
4896
5603
|
} finally {
|
|
4897
5604
|
try {
|
|
4898
|
-
|
|
5605
|
+
fs15.unlinkSync(tmp);
|
|
4899
5606
|
} catch {
|
|
4900
5607
|
}
|
|
4901
5608
|
}
|
|
@@ -4916,6 +5623,198 @@ function printPushNext() {
|
|
|
4916
5623
|
});
|
|
4917
5624
|
}
|
|
4918
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
|
+
|
|
4919
5818
|
// src/lib/nlp-run.ts
|
|
4920
5819
|
async function dispatchNlpRoute(route, input) {
|
|
4921
5820
|
switch (route.command) {
|
|
@@ -4946,6 +5845,8 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4946
5845
|
return save();
|
|
4947
5846
|
case "undo":
|
|
4948
5847
|
return undo();
|
|
5848
|
+
case "restore":
|
|
5849
|
+
return restore(route.args?.[0]);
|
|
4949
5850
|
case "status":
|
|
4950
5851
|
return status();
|
|
4951
5852
|
case "diff":
|
|
@@ -4995,111 +5896,167 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4995
5896
|
if (sub === "done") return goalDone({});
|
|
4996
5897
|
return goalList();
|
|
4997
5898
|
}
|
|
5899
|
+
case "help":
|
|
5900
|
+
return quickActions();
|
|
5901
|
+
case "mode":
|
|
5902
|
+
return mode();
|
|
5903
|
+
case "verify":
|
|
5904
|
+
return verify();
|
|
4998
5905
|
}
|
|
4999
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
|
+
}
|
|
5000
5914
|
async function runNaturalLanguageRoute(input) {
|
|
5001
5915
|
const route = routeNaturalLanguage(input);
|
|
5002
5916
|
if (!route) {
|
|
5003
|
-
console.log(
|
|
5917
|
+
console.log(chalk33.yellow(`
|
|
5004
5918
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
5005
5919
|
`));
|
|
5006
5920
|
return;
|
|
5007
5921
|
}
|
|
5008
5922
|
console.log("");
|
|
5009
|
-
console.log(
|
|
5010
|
-
console.log(
|
|
5011
|
-
if (route
|
|
5012
|
-
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([{
|
|
5013
5927
|
type: "confirm",
|
|
5014
5928
|
name: "confirm",
|
|
5015
5929
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
5016
5930
|
default: true
|
|
5017
5931
|
}]);
|
|
5018
5932
|
if (!confirm) {
|
|
5019
|
-
console.log(
|
|
5933
|
+
console.log(chalk33.dim(` ${ko.nlp.menuHint}`));
|
|
5020
5934
|
return;
|
|
5021
5935
|
}
|
|
5022
5936
|
}
|
|
5023
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
|
+
}
|
|
5024
5947
|
await dispatchNlpRoute(route, input);
|
|
5025
5948
|
}
|
|
5026
5949
|
|
|
5027
5950
|
// src/commands/agent.ts
|
|
5028
|
-
import
|
|
5951
|
+
import chalk34 from "chalk";
|
|
5029
5952
|
function activeGoalId() {
|
|
5030
5953
|
const goals = listGoals("goals");
|
|
5031
5954
|
const id = selectActiveId(goals);
|
|
5032
5955
|
return id ?? void 0;
|
|
5033
5956
|
}
|
|
5034
5957
|
async function blocker(description) {
|
|
5035
|
-
console.log(
|
|
5958
|
+
console.log(chalk34.bold(`
|
|
5036
5959
|
${ko.agent.blockerTitle}
|
|
5037
5960
|
`));
|
|
5038
5961
|
if (!description || !description.trim()) {
|
|
5039
|
-
console.log(
|
|
5040
|
-
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"'));
|
|
5041
5964
|
process.exitCode = 1;
|
|
5042
5965
|
return;
|
|
5043
5966
|
}
|
|
5044
5967
|
const goalId = activeGoalId();
|
|
5045
5968
|
const r = appendBlocker(description, goalId);
|
|
5046
|
-
console.log(
|
|
5969
|
+
console.log(chalk34.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
5047
5970
|
if (r.hardStopTripped) {
|
|
5048
|
-
console.log(
|
|
5049
|
-
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."));
|
|
5050
5973
|
process.exitCode = 2;
|
|
5051
5974
|
}
|
|
5052
5975
|
}
|
|
5053
5976
|
async function learn(lesson) {
|
|
5054
|
-
console.log(
|
|
5977
|
+
console.log(chalk34.bold(`
|
|
5055
5978
|
${ko.agent.learnTitle}
|
|
5056
5979
|
`));
|
|
5057
5980
|
if (!lesson || !lesson.trim()) {
|
|
5058
|
-
console.log(
|
|
5059
|
-
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)"'));
|
|
5060
5983
|
process.exitCode = 1;
|
|
5061
5984
|
return;
|
|
5062
5985
|
}
|
|
5063
5986
|
const goalId = activeGoalId();
|
|
5064
5987
|
appendLearning(lesson, goalId);
|
|
5065
|
-
console.log(
|
|
5988
|
+
console.log(chalk34.green(" \u2705 learnings.md append."));
|
|
5066
5989
|
console.log(
|
|
5067
|
-
|
|
5990
|
+
chalk34.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
|
|
5068
5991
|
);
|
|
5069
5992
|
}
|
|
5070
5993
|
async function resume(opts = {}) {
|
|
5071
|
-
console.log(
|
|
5994
|
+
console.log(chalk34.bold(`
|
|
5072
5995
|
${ko.agent.resumeTitle}
|
|
5073
5996
|
`));
|
|
5074
5997
|
if (!isHardStopActive()) {
|
|
5075
|
-
console.log(
|
|
5998
|
+
console.log(chalk34.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
|
|
5076
5999
|
return;
|
|
5077
6000
|
}
|
|
5078
6001
|
const reason = readHardStopReason();
|
|
5079
6002
|
if (reason) {
|
|
5080
|
-
console.log(
|
|
5081
|
-
console.log(
|
|
6003
|
+
console.log(chalk34.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
|
|
6004
|
+
console.log(chalk34.dim(` ${reason.split("\n").join("\n ")}`));
|
|
5082
6005
|
console.log("");
|
|
5083
6006
|
}
|
|
5084
6007
|
if (!opts.confirm) {
|
|
5085
6008
|
console.log(
|
|
5086
|
-
|
|
6009
|
+
chalk34.red(
|
|
5087
6010
|
" \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
|
|
5088
6011
|
)
|
|
5089
6012
|
);
|
|
5090
|
-
console.log(
|
|
6013
|
+
console.log(chalk34.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
|
|
5091
6014
|
process.exitCode = 1;
|
|
5092
6015
|
return;
|
|
5093
6016
|
}
|
|
5094
6017
|
const removed = clearHardStop();
|
|
5095
6018
|
if (removed) {
|
|
5096
|
-
console.log(
|
|
6019
|
+
console.log(chalk34.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
5097
6020
|
} else {
|
|
5098
|
-
console.log(
|
|
6021
|
+
console.log(chalk34.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
|
|
5099
6022
|
}
|
|
5100
6023
|
}
|
|
5101
6024
|
|
|
5102
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
|
+
}
|
|
5103
6060
|
var program = new Command();
|
|
5104
6061
|
var defaultHelp = new Help();
|
|
5105
6062
|
var KO_ALIASES = {
|
|
@@ -5114,6 +6071,7 @@ var KO_ALIASES = {
|
|
|
5114
6071
|
doctor: "\uD658\uACBD",
|
|
5115
6072
|
save: "\uC800\uC7A5",
|
|
5116
6073
|
undo: "\uB418\uB3CC\uB9AC\uAE30",
|
|
6074
|
+
restore: "\uBCF5\uC6D0",
|
|
5117
6075
|
status: "\uC0C1\uD0DC",
|
|
5118
6076
|
diff: "\uBCC0\uACBD",
|
|
5119
6077
|
deploy: "\uBC30\uD3EC",
|
|
@@ -5165,7 +6123,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
|
|
|
5165
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);
|
|
5166
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);
|
|
5167
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);
|
|
5168
|
-
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
|
+
});
|
|
5169
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) => {
|
|
5170
6130
|
await check(opts);
|
|
5171
6131
|
});
|
|
@@ -5177,16 +6137,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
|
|
|
5177
6137
|
cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
|
|
5178
6138
|
await cloudPush();
|
|
5179
6139
|
});
|
|
5180
|
-
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) => {
|
|
5181
|
-
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));
|
|
5182
6142
|
});
|
|
5183
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);
|
|
5184
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);
|
|
5185
|
-
program.command("save").alias("\uC800\uC7A5").description("\uBCC0\uACBD\uC0AC\uD56D \uC800\uC7A5 (git add \u2192 commit \u2192 push)").action(async () => {
|
|
5186
|
-
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());
|
|
5187
6150
|
});
|
|
5188
|
-
program.command("
|
|
5189
|
-
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);
|
|
5190
6153
|
});
|
|
5191
6154
|
program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
|
|
5192
6155
|
await status();
|
|
@@ -5198,17 +6161,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
|
|
|
5198
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 () => {
|
|
5199
6162
|
await mcpInit();
|
|
5200
6163
|
});
|
|
5201
|
-
program.command("deploy").alias("\uBC30\uD3EC").description("\uD504\uB85C\uB355\uC158 \uBC30\uD3EC (Vercel/Netlify/Cloudflare \uC790\uB3D9 \uAC10\uC9C0)").action(async () => {
|
|
5202
|
-
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());
|
|
5203
6166
|
});
|
|
5204
|
-
program.command("env").alias("\uD658\uACBD\uBCC0\uC218").description(".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore \uC790\uB3D9 \uCD94\uAC00").action(async () => {
|
|
5205
|
-
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());
|
|
5206
6169
|
});
|
|
5207
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 () => {
|
|
5208
6171
|
await envCheck();
|
|
5209
6172
|
});
|
|
5210
|
-
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 () => {
|
|
5211
|
-
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());
|
|
5212
6175
|
});
|
|
5213
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 () => {
|
|
5214
6177
|
await design();
|
|
@@ -5237,14 +6200,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
|
|
|
5237
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) => {
|
|
5238
6201
|
await audit(opts.fix);
|
|
5239
6202
|
});
|
|
5240
|
-
program.command("migrate [target]").alias("\uC804\uD658").description("\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 (npm/yarn/pnpm)").action(async (target) => {
|
|
5241
|
-
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));
|
|
5242
6205
|
});
|
|
5243
6206
|
program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
|
|
5244
6207
|
await update();
|
|
5245
6208
|
});
|
|
5246
|
-
program.command("context").alias("\uB9E5\uB77D").description("\uD504\uB85C\uC81D\uD2B8 \uB9E5\uB77D \uD30C\uC77C \uC0DD\uC131 (.vhk/context.md)").action(async () => {
|
|
5247
|
-
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();
|
|
5248
6217
|
});
|
|
5249
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 () => {
|
|
5250
6219
|
await contextShow();
|
|
@@ -5290,7 +6259,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
|
|
|
5290
6259
|
await learn(lesson);
|
|
5291
6260
|
});
|
|
5292
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) => {
|
|
5293
|
-
await resume(opts);
|
|
6262
|
+
await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
|
|
5294
6263
|
});
|
|
5295
6264
|
program.on("command:*", async (operands) => {
|
|
5296
6265
|
const unknown = operands[0] ?? "";
|
|
@@ -5300,7 +6269,7 @@ program.on("command:*", async (operands) => {
|
|
|
5300
6269
|
});
|
|
5301
6270
|
program.action(async () => {
|
|
5302
6271
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
5303
|
-
const { choice } = await
|
|
6272
|
+
const { choice } = await inquirer14.prompt([{
|
|
5304
6273
|
type: "list",
|
|
5305
6274
|
name: "choice",
|
|
5306
6275
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
@@ -5331,24 +6300,40 @@ program.action(async () => {
|
|
|
5331
6300
|
case "secure":
|
|
5332
6301
|
return secure();
|
|
5333
6302
|
case "sync":
|
|
5334
|
-
return sync();
|
|
6303
|
+
return guardCli("sync", false, () => sync());
|
|
5335
6304
|
case "doctor":
|
|
5336
6305
|
return doctor();
|
|
5337
6306
|
case "ship":
|
|
5338
6307
|
return ship();
|
|
5339
6308
|
case "save":
|
|
5340
|
-
return save();
|
|
6309
|
+
return guardCli("save", false, () => save());
|
|
5341
6310
|
case "undo":
|
|
5342
|
-
return undo();
|
|
6311
|
+
return guardCliDefer("undo", false, () => undo());
|
|
5343
6312
|
case "status":
|
|
5344
6313
|
return status();
|
|
5345
6314
|
case "diff":
|
|
5346
6315
|
return diff();
|
|
5347
6316
|
}
|
|
5348
6317
|
});
|
|
5349
|
-
var
|
|
5350
|
-
if (
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
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
|
+
}
|
|
5354
6336
|
}
|
|
6337
|
+
export {
|
|
6338
|
+
program
|
|
6339
|
+
};
|