@byh3071/vhk 1.6.1 → 1.6.3
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-53RJHPP6.js} +1004 -251
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1714 -1085
- package/dist/mcp/index.js +6 -1
- package/package.json +72 -71
package/dist/index.js
CHANGED
|
@@ -1,31 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CONTEXT_GIT_MARKER,
|
|
3
4
|
MAX_SCAN_FILE_BYTES,
|
|
4
5
|
MAX_SECRET_FINDINGS,
|
|
5
6
|
NETWORK_EXEC_TIMEOUT_MS,
|
|
6
7
|
__toESM,
|
|
7
8
|
audit,
|
|
9
|
+
buildAdoptedRules,
|
|
10
|
+
checkContextDrift,
|
|
11
|
+
checkRuleDrift,
|
|
12
|
+
countLocalCommits,
|
|
8
13
|
deploy,
|
|
14
|
+
detectExistingRuleFiles,
|
|
9
15
|
env,
|
|
10
16
|
envCheck,
|
|
11
17
|
filterSevereFindings,
|
|
12
18
|
filterTrackedPaths,
|
|
19
|
+
getExecErrorMessage,
|
|
20
|
+
getGitRoot,
|
|
13
21
|
getVhkVersion,
|
|
22
|
+
gitOut,
|
|
23
|
+
gitRun,
|
|
24
|
+
hasGitRemote,
|
|
14
25
|
ko,
|
|
26
|
+
listBackups,
|
|
27
|
+
localDate,
|
|
28
|
+
printContextResumeHint,
|
|
15
29
|
printNextStep,
|
|
16
30
|
printSecurityWarnings,
|
|
17
31
|
publish,
|
|
18
32
|
readJsonFile,
|
|
19
33
|
require_ignore,
|
|
34
|
+
restoreBackup,
|
|
20
35
|
safeExecFile,
|
|
21
36
|
scanProjectForSecrets,
|
|
22
37
|
startMcpServer,
|
|
38
|
+
sync,
|
|
23
39
|
t
|
|
24
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-53RJHPP6.js";
|
|
25
41
|
|
|
26
42
|
// src/index.ts
|
|
27
43
|
import { Command, Help } from "commander";
|
|
28
|
-
import
|
|
44
|
+
import { pathToFileURL } from "url";
|
|
45
|
+
import chalk34 from "chalk";
|
|
46
|
+
import inquirer13 from "inquirer";
|
|
29
47
|
|
|
30
48
|
// src/lib/nlp-router.ts
|
|
31
49
|
function normalize(input) {
|
|
@@ -43,6 +61,14 @@ function matchesKeywords(text, command) {
|
|
|
43
61
|
return keywords.some((kw) => text.includes(kw.toLowerCase()));
|
|
44
62
|
}
|
|
45
63
|
var RULES = [
|
|
64
|
+
// restore 는 cloud-pull/undo 보다 먼저 평가 — "백업 복원/되돌려" 가 클라우드 복원이나
|
|
65
|
+
// 커밋 되돌리기로 새지 않도록. "백업" 한정이라 bare "복원해"(=cloud-pull)·"되돌려"(=undo)는 안 가로챔.
|
|
66
|
+
{
|
|
67
|
+
command: "restore",
|
|
68
|
+
explanation: "sync \uBC31\uC5C5 \uBCF5\uC6D0 (vhk restore)",
|
|
69
|
+
confidence: "high",
|
|
70
|
+
test: (t2) => /백업/.test(t2) && /(복원|복구|되돌려|되살려|롤백|restore)/.test(t2)
|
|
71
|
+
},
|
|
46
72
|
// 영문 `vhk cloud push|pull [id]` 은 commander 가 직접 처리(가로채기 금지) — 한국어 표현만 매칭.
|
|
47
73
|
{
|
|
48
74
|
command: "cloud-pull",
|
|
@@ -69,6 +95,28 @@ var RULES = [
|
|
|
69
95
|
confidence: "high",
|
|
70
96
|
test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|새\s*프로젝트|^시작$|마법사|기획.*(끝|완료)|검증.*(스킵|건너)|gate.*(스킵|건너)|바로.*시작/.test(t2) && !/디자인|design|팔레트|palette|테마|theme|레퍼런스|reference|다크\s*모드|라이트\s*모드|색상\s*모드|브리핑|brief|컨텍스트|context|맥락|기억|memory|^초기화$|하네스.*만/.test(t2)
|
|
71
97
|
},
|
|
98
|
+
// 도움말 — 초보자가 "뭐부터/도움말/명령어" 라고 물으면 읽기전용 quick actions 를 출력.
|
|
99
|
+
// (적대 리뷰 HIGH 수정: 이전엔 start 마법사로 라우팅돼 도움말이 scaffold 를 유발했음.
|
|
100
|
+
// 도움말은 절대 상태를 바꾸지 않는다.) 실제 서브커맨드는 cli-args R1 가드가 먼저 commander 로 보냄.
|
|
101
|
+
{
|
|
102
|
+
command: "help",
|
|
103
|
+
explanation: "\uC790\uC5F0\uC5B4\uB85C vhk \uC4F0\uB294 \uBC95 \u2014 quick actions \uCD9C\uB825(\uC0C1\uD0DC\uBCC0\uACBD \uC5C6\uC74C)",
|
|
104
|
+
confidence: "high",
|
|
105
|
+
test: (t2) => /도움말|사용법|help|^명령어$|뭐\s*(할\s*수\s*있|하면\s*(돼|되|좋)|해야)|처음\s*(뭐|어떻게|시작|할)|어떻게\s*시작|뭐부터/.test(t2)
|
|
106
|
+
},
|
|
107
|
+
// Safety Mode — 위험 작업 가드 강도 조회/변경.
|
|
108
|
+
{
|
|
109
|
+
command: "mode",
|
|
110
|
+
explanation: "Safety Mode \uC870\uD68C/\uBCC0\uACBD (vhk mode)",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
test: (t2) => /안전\s*모드|safety\s*mode|모드\s*(바꿔|변경|설정|확인|보여|뭐)|위험\s*작업\s*(가드|모드)/.test(t2)
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
command: "verify",
|
|
116
|
+
explanation: "\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C (vhk verify)",
|
|
117
|
+
confidence: "high",
|
|
118
|
+
test: (t2) => /검증\s*묶음|사전\s*검증|저장\s*전\s*(검증|확인)|^verify$/.test(t2)
|
|
119
|
+
},
|
|
72
120
|
{
|
|
73
121
|
command: "init",
|
|
74
122
|
explanation: "\uBB38\uC11C/\uD558\uB124\uC2A4 \uD30C\uC77C\uB9CC \uC0DD\uC131 (vhk init) \u2014 git/MCP/context\uB294 \uC81C\uC678",
|
|
@@ -257,7 +305,8 @@ var RULES = [
|
|
|
257
305
|
explanation: "\uBAA9\uD45C \uAC8C\uC774\uD2B8 \uAC80\uC99D (vhk goal check)",
|
|
258
306
|
confidence: "high",
|
|
259
307
|
args: ["check"],
|
|
260
|
-
|
|
308
|
+
// '스크립트' 포함 시는 sync 의도(게이트 스크립트 생성) → check 가 가로채지 않게 제외.
|
|
309
|
+
test: (t2) => /목표\s*(점검|검증|체크)/.test(t2) && !/스크립트/.test(t2)
|
|
261
310
|
},
|
|
262
311
|
{
|
|
263
312
|
command: "goal",
|
|
@@ -272,6 +321,13 @@ var RULES = [
|
|
|
272
321
|
confidence: "high",
|
|
273
322
|
args: ["list"],
|
|
274
323
|
test: (t2) => /목표\s*(목록|리스트)/.test(t2)
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
command: "goal",
|
|
327
|
+
explanation: "\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uB3D9\uAE30\uD654 (vhk goal sync)",
|
|
328
|
+
confidence: "high",
|
|
329
|
+
args: ["sync"],
|
|
330
|
+
test: (t2) => /(게이트|목표).*(스크립트|동기화)|체크\s*스크립트\s*(생성|만들)/.test(t2)
|
|
275
331
|
}
|
|
276
332
|
];
|
|
277
333
|
function routeNaturalLanguage(input) {
|
|
@@ -294,6 +350,28 @@ function extractNotionUrl(input) {
|
|
|
294
350
|
return m?.[0];
|
|
295
351
|
}
|
|
296
352
|
|
|
353
|
+
// src/lib/command-registry.ts
|
|
354
|
+
var CONTAINER_SUBCOMMANDS = {
|
|
355
|
+
goal: ["list", "next", "check", "init", "done", "sync"],
|
|
356
|
+
ref: ["add", "list", "open"],
|
|
357
|
+
memory: ["add", "list", "remove"],
|
|
358
|
+
cloud: ["push", "pull"],
|
|
359
|
+
secure: ["scan"],
|
|
360
|
+
design: ["palette"],
|
|
361
|
+
env: ["check"],
|
|
362
|
+
mode: ["lite", "standard", "strict"]
|
|
363
|
+
};
|
|
364
|
+
var CONTAINER_ALIASES = {
|
|
365
|
+
\uBAA9\uD45C: "goal",
|
|
366
|
+
\uB808\uD37C\uB7F0\uC2A4: "ref",
|
|
367
|
+
\uAE30\uC5B5: "memory",
|
|
368
|
+
\uD074\uB77C\uC6B0\uB4DC: "cloud",
|
|
369
|
+
\uBCF4\uC548: "secure",
|
|
370
|
+
\uB514\uC790\uC778: "design",
|
|
371
|
+
\uD658\uACBD\uBCC0\uC218: "env",
|
|
372
|
+
\uBAA8\uB4DC: "mode"
|
|
373
|
+
};
|
|
374
|
+
|
|
297
375
|
// src/lib/cli-args.ts
|
|
298
376
|
var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
299
377
|
"gate",
|
|
@@ -326,6 +404,8 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
326
404
|
"\uC800\uC7A5",
|
|
327
405
|
"undo",
|
|
328
406
|
"\uB418\uB3CC\uB9AC\uAE30",
|
|
407
|
+
"restore",
|
|
408
|
+
"\uBCF5\uC6D0",
|
|
329
409
|
"status",
|
|
330
410
|
"\uC0C1\uD0DC",
|
|
331
411
|
"\uD604\uD669",
|
|
@@ -378,11 +458,28 @@ var KNOWN_COMMAND_TOKENS = /* @__PURE__ */ new Set([
|
|
|
378
458
|
"\uAD50\uD6C8",
|
|
379
459
|
"resume",
|
|
380
460
|
"\uC7AC\uAC1C",
|
|
461
|
+
"mode",
|
|
462
|
+
"\uBAA8\uB4DC",
|
|
463
|
+
"verify",
|
|
464
|
+
"\uC0AC\uC804\uC810\uAC80",
|
|
381
465
|
"help"
|
|
382
466
|
]);
|
|
383
467
|
function isOptionToken(token) {
|
|
384
468
|
return token.startsWith("-");
|
|
385
469
|
}
|
|
470
|
+
var COMMAND_SUBCOMMANDS = (() => {
|
|
471
|
+
const map = { ...CONTAINER_SUBCOMMANDS };
|
|
472
|
+
for (const [alias, canonical] of Object.entries(CONTAINER_ALIASES)) {
|
|
473
|
+
const subs = CONTAINER_SUBCOMMANDS[canonical];
|
|
474
|
+
if (subs) map[alias] = subs;
|
|
475
|
+
}
|
|
476
|
+
return map;
|
|
477
|
+
})();
|
|
478
|
+
function isRealSubcommandPath(first, second) {
|
|
479
|
+
if (second === void 0) return false;
|
|
480
|
+
const subs = COMMAND_SUBCOMMANDS[first];
|
|
481
|
+
return subs !== void 0 && subs.includes(second);
|
|
482
|
+
}
|
|
386
483
|
function detectNaturalLanguageInput(argv) {
|
|
387
484
|
const rest = argv.slice(2);
|
|
388
485
|
if (rest.length === 0) return null;
|
|
@@ -394,6 +491,7 @@ function detectNaturalLanguageInput(argv) {
|
|
|
394
491
|
const firstIsKnown = KNOWN_COMMAND_TOKENS.has(first);
|
|
395
492
|
if (firstIsKnown && rest.length === 1) return null;
|
|
396
493
|
if (firstIsKnown && rest.length > 1) {
|
|
494
|
+
if (isRealSubcommandPath(first, rest[1])) return null;
|
|
397
495
|
if (routeNaturalLanguage(input)) return input;
|
|
398
496
|
return null;
|
|
399
497
|
}
|
|
@@ -401,8 +499,8 @@ function detectNaturalLanguageInput(argv) {
|
|
|
401
499
|
}
|
|
402
500
|
|
|
403
501
|
// src/lib/nlp-run.ts
|
|
404
|
-
import
|
|
405
|
-
import
|
|
502
|
+
import chalk32 from "chalk";
|
|
503
|
+
import inquirer12 from "inquirer";
|
|
406
504
|
|
|
407
505
|
// src/commands/gate.ts
|
|
408
506
|
import inquirer from "inquirer";
|
|
@@ -431,7 +529,7 @@ async function gate() {
|
|
|
431
529
|
console.log(chalk.bold(`
|
|
432
530
|
${ko.gate.title}
|
|
433
531
|
`));
|
|
434
|
-
const { mode } = await inquirer.prompt([{
|
|
532
|
+
const { mode: mode2 } = await inquirer.prompt([{
|
|
435
533
|
type: "list",
|
|
436
534
|
name: "mode",
|
|
437
535
|
message: ko.gate.modePrompt,
|
|
@@ -441,7 +539,7 @@ ${ko.gate.title}
|
|
|
441
539
|
{ name: ko.gate.modeSkipLabel, value: "skip" }
|
|
442
540
|
]
|
|
443
541
|
}]);
|
|
444
|
-
if (
|
|
542
|
+
if (mode2 === "skip") {
|
|
445
543
|
const { source } = await inquirer.prompt([{
|
|
446
544
|
type: "input",
|
|
447
545
|
name: "source",
|
|
@@ -452,9 +550,9 @@ ${ko.gate.skipGo}`));
|
|
|
452
550
|
console.log(chalk.dim(ko.gate.skipSourceLabel(source)));
|
|
453
551
|
return;
|
|
454
552
|
}
|
|
455
|
-
const questions =
|
|
553
|
+
const questions = mode2 === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
|
|
456
554
|
const total = questions.length;
|
|
457
|
-
const header =
|
|
555
|
+
const header = mode2 === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
|
|
458
556
|
console.log(chalk.dim(`
|
|
459
557
|
${header} ${ko.gate.modeCountSuffix(total)}
|
|
460
558
|
`));
|
|
@@ -542,7 +640,7 @@ import path2 from "path";
|
|
|
542
640
|
|
|
543
641
|
// src/templates/claude-md.ts
|
|
544
642
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
545
|
-
const d = (
|
|
643
|
+
const d = localDate();
|
|
546
644
|
const slug = name.toLowerCase().replace(/\s+/g, "-");
|
|
547
645
|
return [
|
|
548
646
|
"---",
|
|
@@ -554,12 +652,12 @@ function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
|
554
652
|
"# \uAE30\uB85D \uADDC\uCE59 (" + name + ")",
|
|
555
653
|
"",
|
|
556
654
|
"> \uC774 \uD30C\uC77C\uC740 \uAE30\uB85D/\uC6B4\uC601 \uC804\uC6A9. \uCF54\uB529/\uB514\uC790\uC778 \u2192 .cursorrules \uCC38\uC870.",
|
|
557
|
-
"> See also: AGENTS.md",
|
|
655
|
+
"> See also: AGENTS.md (`vhk sync` \uB85C \uC0DD\uC131 \u2014 Codex/OpenAI \uACC4\uC5F4 \uD638\uD658).",
|
|
558
656
|
"",
|
|
559
657
|
"## \uD604\uC7AC \uC0C1\uD0DC",
|
|
560
658
|
"- **Phase:** Phase 1 \u2014 MVP",
|
|
561
659
|
"- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C",
|
|
562
|
-
"- **\uB2E4\uC74C \uC561\uC158:**
|
|
660
|
+
"- **\uB2E4\uC74C \uC561\uC158:** **FILL**",
|
|
563
661
|
"- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** " + d,
|
|
564
662
|
"",
|
|
565
663
|
"## ADR",
|
|
@@ -613,8 +711,39 @@ function CURSORRULES_TEMPLATE(name, desc, stack) {
|
|
|
613
711
|
].join("\n");
|
|
614
712
|
}
|
|
615
713
|
|
|
714
|
+
// src/templates/rules-md.ts
|
|
715
|
+
function RULES_MD_TEMPLATE(name, description, stack) {
|
|
716
|
+
const stackList = stack.split(" + ").map((s) => "- " + s).join("\n");
|
|
717
|
+
return [
|
|
718
|
+
"# " + name + " \u2014 Rules",
|
|
719
|
+
"",
|
|
720
|
+
"> \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.",
|
|
721
|
+
"> `vhk sync` \uAC00 Cursor\xB7Claude\xB7Windsurf\xB7Copilot\xB7Antigravity \uADDC\uCE59\uC73C\uB85C \uC804\uD30C\uD569\uB2C8\uB2E4.",
|
|
722
|
+
"",
|
|
723
|
+
"## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131",
|
|
724
|
+
"- \uD55C \uC904 \uC124\uBA85: " + description,
|
|
725
|
+
"- \uC2A4\uD0DD: " + stack,
|
|
726
|
+
"",
|
|
727
|
+
"## \uAE30\uC220 \uC2A4\uD0DD",
|
|
728
|
+
stackList,
|
|
729
|
+
"",
|
|
730
|
+
"## \uCF54\uB529 \uADDC\uCE59",
|
|
731
|
+
"- TypeScript strict (any \uAE08\uC9C0)",
|
|
732
|
+
"- try-catch \uD544\uC218, \uBE48 catch \uAE08\uC9C0",
|
|
733
|
+
"- console.log \uD504\uB85C\uB355\uC158 \uC81C\uAC70",
|
|
734
|
+
"- \uD30C\uC77C\uBA85\uC740 kebab-case",
|
|
735
|
+
"",
|
|
736
|
+
"## \uCEE4\uBC0B \uCEE8\uBCA4\uC158",
|
|
737
|
+
"- feat: / fix: / refactor: / docs: / chore:",
|
|
738
|
+
"",
|
|
739
|
+
"## \uAE30\uB85D \uADDC\uCE59",
|
|
740
|
+
"- \uC138\uC158 \uC885\uB8CC \uC2DC docs/log/YYYY-MM-DD-{\uC791\uC5C5\uBA85}.md \uC0DD\uC131",
|
|
741
|
+
"- \uAE30\uC220 \uC120\uD0DD \uC2DC docs/adr/ADR-{\uBC88\uD638}-{\uC81C\uBAA9}.md \uC0DD\uC131"
|
|
742
|
+
].join("\n");
|
|
743
|
+
}
|
|
744
|
+
|
|
616
745
|
// src/templates/prd.ts
|
|
617
|
-
var FILL = "
|
|
746
|
+
var FILL = "**FILL**";
|
|
618
747
|
function fill(value, fallback = FILL) {
|
|
619
748
|
return value?.trim() || fallback;
|
|
620
749
|
}
|
|
@@ -683,12 +812,12 @@ function ARCHITECTURE_TEMPLATE(name, stack) {
|
|
|
683
812
|
"## \uB370\uC774\uD130 \uBAA8\uB378",
|
|
684
813
|
"| \uD14C\uC774\uBE14 | \uD575\uC2EC \uCEEC\uB7FC | \uC124\uBA85 |",
|
|
685
814
|
"|--------|----------|------|",
|
|
686
|
-
"|
|
|
815
|
+
"| **FILL** | | |",
|
|
687
816
|
"",
|
|
688
817
|
"## \uC678\uBD80 \uC11C\uBE44\uC2A4",
|
|
689
818
|
"| \uC11C\uBE44\uC2A4 | \uC6A9\uB3C4 |",
|
|
690
819
|
"|--------|------|",
|
|
691
|
-
"|
|
|
820
|
+
"| **FILL** | |"
|
|
692
821
|
].join("\n");
|
|
693
822
|
}
|
|
694
823
|
|
|
@@ -705,21 +834,22 @@ function ADR_TEMPLATE() {
|
|
|
705
834
|
"# ADR-000: \uC81C\uBAA9",
|
|
706
835
|
"",
|
|
707
836
|
"## \uB9E5\uB77D (Context)",
|
|
708
|
-
"
|
|
837
|
+
"**FILL**",
|
|
709
838
|
"",
|
|
710
839
|
"## \uACB0\uC815 (Decision)",
|
|
711
|
-
"
|
|
840
|
+
"**FILL**",
|
|
712
841
|
"",
|
|
713
842
|
"## \uB300\uC548 (Alternatives)",
|
|
714
|
-
"
|
|
843
|
+
"**FILL**",
|
|
715
844
|
"",
|
|
716
845
|
"## \uACB0\uACFC (Consequences)",
|
|
717
|
-
"
|
|
846
|
+
"**FILL**"
|
|
718
847
|
].join("\n");
|
|
719
848
|
}
|
|
720
849
|
|
|
721
850
|
// src/templates/commands-md.ts
|
|
722
|
-
function COMMANDS_MD_TEMPLATE() {
|
|
851
|
+
function COMMANDS_MD_TEMPLATE(opts = {}) {
|
|
852
|
+
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
853
|
return [
|
|
724
854
|
"# \u{1F4CB} \uD55C\uAD6D\uC5B4 \uBA85\uB839\uC5B4 \uAC00\uC774\uB4DC",
|
|
725
855
|
"",
|
|
@@ -742,7 +872,7 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
742
872
|
'| \uC624\uB298 \uC815\uB9AC | `vhk \uC815\uB9AC` | "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574" |',
|
|
743
873
|
'| \uADDC\uCE59 \uC810\uAC80 | `vhk \uC810\uAC80` | "\uADDC\uCE59 \uC810\uAC80\uD574" |',
|
|
744
874
|
'| \uBCF4\uC548 \uC2A4\uCE94 | `vhk \uBCF4\uC548 scan` | "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824" |',
|
|
745
|
-
|
|
875
|
+
buildTestRow,
|
|
746
876
|
'| \uBC30\uD3EC | `vhk \uBC30\uD3EC` | "\uBC30\uD3EC\uD574" |',
|
|
747
877
|
"",
|
|
748
878
|
"## \uD658\uACBD \uC810\uAC80",
|
|
@@ -762,7 +892,8 @@ function VHK_README_TEMPLATE() {
|
|
|
762
892
|
"# `.vhk/` \u2014 VHK runtime state",
|
|
763
893
|
"",
|
|
764
894
|
"\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
|
-
|
|
895
|
+
// VHK-006: 생성 프로젝트엔 docs/spec.md 가 없음 → vhk 저장소의 규격 문서를 외부 링크로 참조.
|
|
896
|
+
"\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
897
|
"",
|
|
767
898
|
"## \uD2B8\uB798\uD0B9 \uC815\uCC45",
|
|
768
899
|
"",
|
|
@@ -782,10 +913,14 @@ function VHK_README_TEMPLATE() {
|
|
|
782
913
|
}
|
|
783
914
|
function VHK_GITIGNORE_TEMPLATE() {
|
|
784
915
|
return [
|
|
785
|
-
"# VHK \uB85C\uCEEC \uC804\uC6A9 \u2014 \uAC1C\uC778 \uBA54\uBAA8/\uCC38\uACE0\uB9C1\uD06C/\uC548\uC804\uC2E0\uD638 (
|
|
916
|
+
"# 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
917
|
"memory.json",
|
|
787
918
|
"refs.json",
|
|
788
919
|
"HARD_STOP",
|
|
920
|
+
"# secret gist \uD3EC\uC778\uD130 (gistId). \uACF5\uAC1C repo \uC5D0 \uCEE4\uBC0B\uB418\uBA74 \uBC31\uC5C5 gist \uAC00 \uB178\uCD9C\uB428 (VHK-022).",
|
|
921
|
+
"cloud.json",
|
|
922
|
+
"# 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.",
|
|
923
|
+
"backups/",
|
|
789
924
|
""
|
|
790
925
|
].join("\n");
|
|
791
926
|
}
|
|
@@ -988,6 +1123,40 @@ async function fetchPrdFromNotion(urlOrId) {
|
|
|
988
1123
|
};
|
|
989
1124
|
}
|
|
990
1125
|
|
|
1126
|
+
// src/lib/stack-detect.ts
|
|
1127
|
+
import { join } from "path";
|
|
1128
|
+
function detectStackFromDeps(deps) {
|
|
1129
|
+
const stack = [];
|
|
1130
|
+
if (deps.next) stack.push("Next.js");
|
|
1131
|
+
else if (deps.nuxt) stack.push("Nuxt");
|
|
1132
|
+
else if (deps.vite || deps["@vitejs/plugin-react"]) stack.push("Vite");
|
|
1133
|
+
if (deps.react) stack.push("React");
|
|
1134
|
+
else if (deps.vue) stack.push("Vue");
|
|
1135
|
+
else if (deps.svelte) stack.push("Svelte");
|
|
1136
|
+
if (deps.typescript) stack.push("TypeScript");
|
|
1137
|
+
if (deps.tailwindcss || deps["@tailwindcss/vite"]) stack.push("Tailwind CSS");
|
|
1138
|
+
if (deps["@supabase/supabase-js"] || deps["@supabase/ssr"]) stack.push("Supabase");
|
|
1139
|
+
if (deps["@prisma/client"] || deps.prisma) stack.push("Prisma");
|
|
1140
|
+
if (deps["drizzle-orm"]) stack.push("Drizzle");
|
|
1141
|
+
if (deps["@anthropic-ai/sdk"]) stack.push("Anthropic SDK");
|
|
1142
|
+
if (deps.openai) stack.push("OpenAI SDK");
|
|
1143
|
+
if (deps.zod) stack.push("zod");
|
|
1144
|
+
if (deps["@tanstack/react-query"]) stack.push("TanStack Query");
|
|
1145
|
+
return stack;
|
|
1146
|
+
}
|
|
1147
|
+
function detectProjectStack(cwd = ".") {
|
|
1148
|
+
let pkg;
|
|
1149
|
+
try {
|
|
1150
|
+
pkg = readJsonFile(join(cwd, "package.json"));
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
const all = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
1155
|
+
if (Object.keys(all).length === 0) return null;
|
|
1156
|
+
const stack = detectStackFromDeps(all);
|
|
1157
|
+
return stack.length ? stack : null;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
991
1160
|
// src/commands/init.ts
|
|
992
1161
|
var PROJECT_TYPES = [
|
|
993
1162
|
{ name: "\u{1F310} \uC6F9 \uC571 (Next.js + Supabase + Vercel)", value: "webapp" },
|
|
@@ -1011,23 +1180,30 @@ function resolveType(type) {
|
|
|
1011
1180
|
}
|
|
1012
1181
|
return type;
|
|
1013
1182
|
}
|
|
1183
|
+
function isNonInteractive(options) {
|
|
1184
|
+
return Boolean(options.yes) || !process.stdin.isTTY || !process.stdout.isTTY;
|
|
1185
|
+
}
|
|
1186
|
+
var DEFAULT_TYPE = PROJECT_TYPES[0].value;
|
|
1014
1187
|
async function collectAnswers(options, defaults = {}) {
|
|
1188
|
+
const noninteractive = isNonInteractive(options);
|
|
1015
1189
|
const prompts = [];
|
|
1016
|
-
if (!
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1190
|
+
if (!noninteractive) {
|
|
1191
|
+
if (!options.name && !defaults.name) {
|
|
1192
|
+
prompts.push({ type: "input", name: "name", message: ko.init.projectName });
|
|
1193
|
+
}
|
|
1194
|
+
if (!options.description && !defaults.description) {
|
|
1195
|
+
prompts.push({ type: "input", name: "description", message: ko.init.description });
|
|
1196
|
+
}
|
|
1197
|
+
if (!options.type && !defaults.type) {
|
|
1198
|
+
prompts.push({ type: "list", name: "type", message: ko.init.projectType, choices: PROJECT_TYPES });
|
|
1199
|
+
}
|
|
1024
1200
|
}
|
|
1025
1201
|
const prompted = prompts.length ? await inquirer2.prompt(prompts) : {};
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
};
|
|
1202
|
+
const fallbackName = path2.basename(process.cwd()) || "my-project";
|
|
1203
|
+
const name = options.name || defaults.name || prompted.name || fallbackName;
|
|
1204
|
+
const description = options.description || defaults.description || prompted.description || `${name} \u2014 vhk \uD504\uB85C\uC81D\uD2B8`;
|
|
1205
|
+
const type = resolveType(options.type || defaults.type || prompted.type) ?? prompted.type ?? DEFAULT_TYPE;
|
|
1206
|
+
return { name, description, type };
|
|
1031
1207
|
}
|
|
1032
1208
|
async function init(options = {}) {
|
|
1033
1209
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
@@ -1060,11 +1236,13 @@ ${ko.init.title}
|
|
|
1060
1236
|
log.error("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984, \uC124\uBA85, \uC720\uD615\uC774 \uBAA8\uB450 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
1061
1237
|
process.exit(1);
|
|
1062
1238
|
}
|
|
1063
|
-
const
|
|
1239
|
+
const detected = detectProjectStack(process.cwd());
|
|
1240
|
+
const stack = detected ?? STACK_PRESETS[answers.type];
|
|
1241
|
+
if (detected) console.log(chalk3.dim(" \u{1F50E} package.json \uC758\uC874\uC131\uC5D0\uC11C \uC2E4\uC81C \uC2A4\uD0DD \uAC10\uC9C0"));
|
|
1064
1242
|
console.log(chalk3.dim(`
|
|
1065
1243
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1066
1244
|
`));
|
|
1067
|
-
if (!options
|
|
1245
|
+
if (!isNonInteractive(options)) {
|
|
1068
1246
|
const { confirmStack } = await inquirer2.prompt([{
|
|
1069
1247
|
type: "confirm",
|
|
1070
1248
|
name: "confirmStack",
|
|
@@ -1077,17 +1255,37 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1077
1255
|
}
|
|
1078
1256
|
}
|
|
1079
1257
|
const cwd = process.cwd();
|
|
1258
|
+
let adoptedRules = null;
|
|
1259
|
+
if (!isNonInteractive(options) && !options.fromNotion) {
|
|
1260
|
+
const existingRules = detectExistingRuleFiles(cwd);
|
|
1261
|
+
if (existingRules.length > 0) {
|
|
1262
|
+
const { adopt } = await inquirer2.prompt([{
|
|
1263
|
+
type: "confirm",
|
|
1264
|
+
name: "adopt",
|
|
1265
|
+
message: ko.init.adoptPrompt(
|
|
1266
|
+
existingRules.length,
|
|
1267
|
+
existingRules.map((f) => f.path).join(", ")
|
|
1268
|
+
),
|
|
1269
|
+
default: true
|
|
1270
|
+
}]);
|
|
1271
|
+
if (adopt) {
|
|
1272
|
+
adoptedRules = buildAdoptedRules(existingRules, answers.name);
|
|
1273
|
+
console.log(chalk3.dim(` ${ko.init.adoptPreview(existingRules.length)}`));
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1080
1277
|
const files = generateFiles(answers.name, answers.description, stack, prdContent, answers.type);
|
|
1278
|
+
if (adoptedRules) files["RULES.md"] = adoptedRules;
|
|
1081
1279
|
log.step(ko.init.filesGenerating);
|
|
1082
1280
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1083
1281
|
const fullPath = path2.join(cwd, filePath);
|
|
1084
1282
|
if (fileExists(fullPath)) {
|
|
1085
|
-
const
|
|
1283
|
+
const overwrite = isNonInteractive(options) ? false : (await inquirer2.prompt([{
|
|
1086
1284
|
type: "confirm",
|
|
1087
1285
|
name: "overwrite",
|
|
1088
1286
|
message: ko.init.overwrite(filePath),
|
|
1089
1287
|
default: false
|
|
1090
|
-
}]);
|
|
1288
|
+
}])).overwrite;
|
|
1091
1289
|
if (!overwrite) {
|
|
1092
1290
|
log.warn(ko.init.skipped(filePath));
|
|
1093
1291
|
continue;
|
|
@@ -1096,7 +1294,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1096
1294
|
writeFile(fullPath, content);
|
|
1097
1295
|
log.success(filePath);
|
|
1098
1296
|
}
|
|
1099
|
-
await writeInitExtras(cwd);
|
|
1297
|
+
await writeInitExtras(cwd, isNonInteractive(options));
|
|
1100
1298
|
console.log(chalk3.bold.green(`
|
|
1101
1299
|
${ko.init.done}`));
|
|
1102
1300
|
console.log(chalk3.dim(`
|
|
@@ -1131,6 +1329,8 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
|
|
|
1131
1329
|
return {
|
|
1132
1330
|
"CLAUDE.md": CLAUDE_MD_TEMPLATE(name, stackStr),
|
|
1133
1331
|
".cursorrules": CURSORRULES_TEMPLATE(name, description, stackStr),
|
|
1332
|
+
// RULES.md — 규칙 SoT. init 이 항상 생성해 sync 와 흐름을 연결한다.
|
|
1333
|
+
"RULES.md": RULES_MD_TEMPLATE(name, description, stackStr),
|
|
1134
1334
|
"docs/PRD.md": PRD_TEMPLATE(name, description, prd),
|
|
1135
1335
|
"docs/ARCHITECTURE.md": ARCHITECTURE_TEMPLATE(name, stackStr),
|
|
1136
1336
|
"docs/adr/ADR-000-template.md": ADR_TEMPLATE(),
|
|
@@ -1138,8 +1338,9 @@ function generateFiles(name, description, stack, prdContent = {}, type = "") {
|
|
|
1138
1338
|
"docs/troubleshooting/.gitkeep": "",
|
|
1139
1339
|
"docs/til.md": `# TIL (Today I Learned)
|
|
1140
1340
|
|
|
1141
|
-
- [${(
|
|
1341
|
+
- [${localDate()}] \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791
|
|
1142
1342
|
`,
|
|
1343
|
+
// VHK-019
|
|
1143
1344
|
"BACKLOG.md": `# BACKLOG
|
|
1144
1345
|
|
|
1145
1346
|
> v1 OUT \uAE30\uB2A5\uC740 \uC5EC\uAE30\uC5D0 \uAE30\uB85D. \uBC94\uC704 \uC218\uBE44 \uD544\uC218.
|
|
@@ -1197,19 +1398,30 @@ function enhancePackageScripts(projectDir) {
|
|
|
1197
1398
|
fs2.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1198
1399
|
return true;
|
|
1199
1400
|
}
|
|
1200
|
-
|
|
1401
|
+
function projectHasTestScript(projectDir) {
|
|
1402
|
+
const pkgPath = path2.join(projectDir, "package.json");
|
|
1403
|
+
if (!fs2.existsSync(pkgPath)) return false;
|
|
1404
|
+
try {
|
|
1405
|
+
const pkg = readJsonFile(pkgPath);
|
|
1406
|
+
return Boolean(pkg.scripts?.test?.trim());
|
|
1407
|
+
} catch {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async function writeInitExtras(projectDir, noninteractive = false) {
|
|
1201
1412
|
const commandsPath = path2.join(projectDir, "COMMANDS.md");
|
|
1413
|
+
const hasTest = projectHasTestScript(projectDir);
|
|
1202
1414
|
if (fileExists(commandsPath)) {
|
|
1203
|
-
const
|
|
1415
|
+
const overwrite = noninteractive ? false : (await inquirer2.prompt([{
|
|
1204
1416
|
type: "confirm",
|
|
1205
1417
|
name: "overwrite",
|
|
1206
1418
|
message: ko.init.overwrite("COMMANDS.md"),
|
|
1207
1419
|
default: false
|
|
1208
|
-
}]);
|
|
1420
|
+
}])).overwrite;
|
|
1209
1421
|
if (!overwrite) {
|
|
1210
1422
|
log.warn(ko.init.skipped("COMMANDS.md"));
|
|
1211
1423
|
} else {
|
|
1212
|
-
writeFile(commandsPath, COMMANDS_MD_TEMPLATE());
|
|
1424
|
+
writeFile(commandsPath, COMMANDS_MD_TEMPLATE({ hasTest }));
|
|
1213
1425
|
log.success(ko.init.commandsMdDone);
|
|
1214
1426
|
}
|
|
1215
1427
|
} else {
|
|
@@ -1229,7 +1441,7 @@ async function writeInitExtras(projectDir) {
|
|
|
1229
1441
|
|
|
1230
1442
|
// src/commands/recap.ts
|
|
1231
1443
|
import inquirer3 from "inquirer";
|
|
1232
|
-
import
|
|
1444
|
+
import chalk6 from "chalk";
|
|
1233
1445
|
import fs4 from "fs";
|
|
1234
1446
|
import path5 from "path";
|
|
1235
1447
|
|
|
@@ -1273,10 +1485,13 @@ function buildSessionDiffFromSummary(diffSummary) {
|
|
|
1273
1485
|
files
|
|
1274
1486
|
};
|
|
1275
1487
|
}
|
|
1488
|
+
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1276
1489
|
async function getSessionDiff(since) {
|
|
1277
|
-
const sinceDate = since || (
|
|
1490
|
+
const sinceDate = since || localDate();
|
|
1278
1491
|
try {
|
|
1279
|
-
const
|
|
1492
|
+
const boundary = (await git.raw(["rev-list", "-1", `--before=${sinceDate}`, "HEAD"])).trim();
|
|
1493
|
+
const base = boundary || EMPTY_TREE_SHA;
|
|
1494
|
+
const diffSummary = await git.diffSummary([`${base}..HEAD`]);
|
|
1280
1495
|
const normalized = diffSummary.files.map((f) => ({
|
|
1281
1496
|
file: f.file,
|
|
1282
1497
|
insertions: "insertions" in f ? f.insertions : 0,
|
|
@@ -1379,7 +1594,7 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1379
1594
|
const adrDir = path4.join(cwd, "docs", "adr");
|
|
1380
1595
|
if (!fs3.existsSync(adrDir)) fs3.mkdirSync(adrDir, { recursive: true });
|
|
1381
1596
|
const num = nextAdrNumber(adrDir);
|
|
1382
|
-
const today = (
|
|
1597
|
+
const today = localDate();
|
|
1383
1598
|
const fileName = `ADR-${String(num).padStart(3, "0")}-${slugify(title)}.md`;
|
|
1384
1599
|
const filePath = path4.join(adrDir, fileName);
|
|
1385
1600
|
const content = [
|
|
@@ -1411,50 +1626,191 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1411
1626
|
return filePath;
|
|
1412
1627
|
}
|
|
1413
1628
|
|
|
1629
|
+
// src/lib/interactive.ts
|
|
1630
|
+
import chalk4 from "chalk";
|
|
1631
|
+
function ensureInteractive(hint = "") {
|
|
1632
|
+
if (process.stdin.isTTY) return true;
|
|
1633
|
+
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."));
|
|
1634
|
+
if (hint) console.error(chalk4.dim(` ${hint}`));
|
|
1635
|
+
process.exitCode = 1;
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
function isPromptAbortError(err) {
|
|
1639
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1640
|
+
return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// src/lib/hard-stop-guard.ts
|
|
1644
|
+
import chalk5 from "chalk";
|
|
1645
|
+
|
|
1646
|
+
// src/lib/state-files.ts
|
|
1647
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rmSync } from "fs";
|
|
1648
|
+
import { join as join2 } from "path";
|
|
1649
|
+
var STATE_DIR = "docs/state";
|
|
1650
|
+
var BLOCKERS_PATH = join2(STATE_DIR, "blockers.md");
|
|
1651
|
+
var LEARNINGS_PATH = join2(STATE_DIR, "learnings.md");
|
|
1652
|
+
var VHK_DIR = ".vhk";
|
|
1653
|
+
var HARD_STOP_PATH = join2(VHK_DIR, "HARD_STOP");
|
|
1654
|
+
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
1655
|
+
function ensureStateDir() {
|
|
1656
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
1657
|
+
}
|
|
1658
|
+
function ensureVhkDir() {
|
|
1659
|
+
mkdirSync(VHK_DIR, { recursive: true });
|
|
1660
|
+
}
|
|
1661
|
+
function isoDate() {
|
|
1662
|
+
return localDate();
|
|
1663
|
+
}
|
|
1664
|
+
var ACTIVE_BLOCKER_RE = /^- (?!~~)\[/;
|
|
1665
|
+
function countActiveBlockers(content) {
|
|
1666
|
+
let count = 0;
|
|
1667
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1668
|
+
if (ACTIVE_BLOCKER_RE.test(line)) count++;
|
|
1669
|
+
}
|
|
1670
|
+
return count;
|
|
1671
|
+
}
|
|
1672
|
+
function appendBlocker(description, goalId) {
|
|
1673
|
+
ensureStateDir();
|
|
1674
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1675
|
+
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
1676
|
+
if (!existsSync(BLOCKERS_PATH)) {
|
|
1677
|
+
writeFileSync(
|
|
1678
|
+
BLOCKERS_PATH,
|
|
1679
|
+
`# Blockers
|
|
1680
|
+
|
|
1681
|
+
_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._
|
|
1682
|
+
|
|
1683
|
+
${line}
|
|
1684
|
+
`,
|
|
1685
|
+
"utf-8"
|
|
1686
|
+
);
|
|
1687
|
+
} else {
|
|
1688
|
+
appendFileSync(BLOCKERS_PATH, `${line}
|
|
1689
|
+
`, "utf-8");
|
|
1690
|
+
}
|
|
1691
|
+
const current = readFileSync(BLOCKERS_PATH, "utf-8");
|
|
1692
|
+
const count = countActiveBlockers(current);
|
|
1693
|
+
let hardStopTripped = false;
|
|
1694
|
+
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync(HARD_STOP_PATH)) {
|
|
1695
|
+
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
1696
|
+
hardStopTripped = true;
|
|
1697
|
+
}
|
|
1698
|
+
return { count, hardStopTripped };
|
|
1699
|
+
}
|
|
1700
|
+
function appendLearning(lesson, goalId) {
|
|
1701
|
+
ensureStateDir();
|
|
1702
|
+
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1703
|
+
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
1704
|
+
if (!existsSync(LEARNINGS_PATH)) {
|
|
1705
|
+
writeFileSync(
|
|
1706
|
+
LEARNINGS_PATH,
|
|
1707
|
+
`# Learnings
|
|
1708
|
+
|
|
1709
|
+
_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._
|
|
1710
|
+
|
|
1711
|
+
${line}
|
|
1712
|
+
`,
|
|
1713
|
+
"utf-8"
|
|
1714
|
+
);
|
|
1715
|
+
} else {
|
|
1716
|
+
appendFileSync(LEARNINGS_PATH, `${line}
|
|
1717
|
+
`, "utf-8");
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
function getRecentLearnings(limit = 3) {
|
|
1721
|
+
if (!existsSync(LEARNINGS_PATH)) return [];
|
|
1722
|
+
const lines = readFileSync(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
1723
|
+
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
1724
|
+
return entries.slice(-limit);
|
|
1725
|
+
}
|
|
1726
|
+
function getActiveBlockers(limit = 3) {
|
|
1727
|
+
if (!existsSync(BLOCKERS_PATH)) return [];
|
|
1728
|
+
const lines = readFileSync(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
|
|
1729
|
+
const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
|
|
1730
|
+
return entries.slice(-limit);
|
|
1731
|
+
}
|
|
1732
|
+
function writeHardStop(reason) {
|
|
1733
|
+
ensureVhkDir();
|
|
1734
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1735
|
+
writeFileSync(HARD_STOP_PATH, `${ts}
|
|
1736
|
+
${reason}
|
|
1737
|
+
`, "utf-8");
|
|
1738
|
+
}
|
|
1739
|
+
function isHardStopActive() {
|
|
1740
|
+
return existsSync(HARD_STOP_PATH);
|
|
1741
|
+
}
|
|
1742
|
+
function readHardStopReason() {
|
|
1743
|
+
if (!existsSync(HARD_STOP_PATH)) return null;
|
|
1744
|
+
try {
|
|
1745
|
+
return readFileSync(HARD_STOP_PATH, "utf-8").trim();
|
|
1746
|
+
} catch {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function clearHardStop() {
|
|
1751
|
+
if (!existsSync(HARD_STOP_PATH)) return false;
|
|
1752
|
+
rmSync(HARD_STOP_PATH, { force: true });
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/lib/hard-stop-guard.ts
|
|
1757
|
+
function ensureNotHardStopped(action) {
|
|
1758
|
+
if (!isHardStopActive()) return true;
|
|
1759
|
+
console.error(chalk5.red.bold(`
|
|
1760
|
+
\u{1F6D1} HARD STOP \uD65C\uC131 \u2014 '${action}' \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`));
|
|
1761
|
+
const reason = readHardStopReason();
|
|
1762
|
+
if (reason) console.error(chalk5.dim(` \uC0AC\uC720: ${reason.replace(/\s*\n\s*/g, " ")}`));
|
|
1763
|
+
console.error(chalk5.dim(" \uD574\uC81C: vhk resume --confirm (\uC0AC\uB78C\uC774 \uC9C1\uC811 \uC2E4\uD589)"));
|
|
1764
|
+
process.exitCode = 1;
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1414
1768
|
// src/commands/recap.ts
|
|
1415
1769
|
async function recap(options = {}) {
|
|
1416
|
-
|
|
1770
|
+
if (!ensureNotHardStopped("recap")) return;
|
|
1771
|
+
console.log(chalk6.bold(`
|
|
1417
1772
|
${ko.recap.title}
|
|
1418
1773
|
`));
|
|
1419
1774
|
if (!await isGitRepo()) {
|
|
1420
|
-
console.log(
|
|
1775
|
+
console.log(chalk6.red(ko.recap.noRepo));
|
|
1421
1776
|
return;
|
|
1422
1777
|
}
|
|
1423
1778
|
if (!await hasAnyCommits()) {
|
|
1424
|
-
console.log(
|
|
1425
|
-
console.log(
|
|
1779
|
+
console.log(chalk6.yellow("\u26A0\uFE0F \uC544\uC9C1 \uCEE4\uBC0B\uC774 \uC5C6\uC5B4\uC694."));
|
|
1780
|
+
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
1781
|
return;
|
|
1427
1782
|
}
|
|
1428
1783
|
printSecurityWarnings();
|
|
1429
|
-
console.log(
|
|
1784
|
+
console.log(chalk6.dim(`${ko.recap.analyzing}
|
|
1430
1785
|
`));
|
|
1431
|
-
const since = options.since || (
|
|
1786
|
+
const since = options.since || localDate();
|
|
1432
1787
|
const diff2 = await getSessionDiff(since);
|
|
1433
1788
|
const commits = await getRecentCommits(10, since);
|
|
1434
1789
|
if (diff2.filesChanged === 0 && commits.length === 0) {
|
|
1435
|
-
console.log(
|
|
1790
|
+
console.log(chalk6.yellow(ko.recap.noChanges));
|
|
1436
1791
|
return;
|
|
1437
1792
|
}
|
|
1438
|
-
console.log(
|
|
1439
|
-
console.log(` \uD30C\uC77C: ${
|
|
1440
|
-
console.log(` \uCD94\uAC00: ${
|
|
1793
|
+
console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1794
|
+
console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1795
|
+
console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
|
|
1441
1796
|
if (diff2.files.length > 0) {
|
|
1442
|
-
console.log(
|
|
1797
|
+
console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
1443
1798
|
diff2.files.slice(0, 15).forEach((f) => {
|
|
1444
|
-
const icon = f.status === "new" ?
|
|
1799
|
+
const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
|
|
1445
1800
|
console.log(` ${icon} ${f.file}`);
|
|
1446
1801
|
});
|
|
1447
1802
|
if (diff2.files.length > 15) {
|
|
1448
|
-
console.log(
|
|
1803
|
+
console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
|
|
1449
1804
|
}
|
|
1450
1805
|
}
|
|
1451
1806
|
if (commits.length > 0) {
|
|
1452
|
-
console.log(
|
|
1807
|
+
console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
1453
1808
|
commits.slice(0, 5).forEach((c) => {
|
|
1454
|
-
console.log(
|
|
1809
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1455
1810
|
});
|
|
1456
1811
|
}
|
|
1457
1812
|
console.log("");
|
|
1813
|
+
if (!ensureInteractive("\uD68C\uACE0 \uC785\uB825\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
|
|
1458
1814
|
const answers = await inquirer3.prompt([
|
|
1459
1815
|
{
|
|
1460
1816
|
type: "input",
|
|
@@ -1479,7 +1835,7 @@ ${ko.recap.title}
|
|
|
1479
1835
|
default: "\uC5C6\uC74C"
|
|
1480
1836
|
}
|
|
1481
1837
|
]);
|
|
1482
|
-
const today = (
|
|
1838
|
+
const today = localDate();
|
|
1483
1839
|
const logDir = path5.join(process.cwd(), "docs", "log");
|
|
1484
1840
|
if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true });
|
|
1485
1841
|
const existing = fs4.readdirSync(logDir).filter((f) => f.startsWith(today));
|
|
@@ -1519,11 +1875,11 @@ ${ko.recap.title}
|
|
|
1519
1875
|
fs4.writeFileSync(filePath, content, "utf-8");
|
|
1520
1876
|
const adrCandidates = detectAdrCandidates(diff2);
|
|
1521
1877
|
if (adrCandidates.length > 0) {
|
|
1522
|
-
console.log(
|
|
1878
|
+
console.log(chalk6.cyan.bold(`
|
|
1523
1879
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
1524
1880
|
for (const candidate of adrCandidates) {
|
|
1525
|
-
console.log(
|
|
1526
|
-
candidate.files.forEach((f) => console.log(
|
|
1881
|
+
console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1882
|
+
candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
|
|
1527
1883
|
}
|
|
1528
1884
|
const { createAdr } = await inquirer3.prompt([{
|
|
1529
1885
|
type: "confirm",
|
|
@@ -1553,17 +1909,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1553
1909
|
adrAnswers.decision,
|
|
1554
1910
|
adrAnswers.consequences
|
|
1555
1911
|
);
|
|
1556
|
-
console.log(
|
|
1912
|
+
console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path5.relative(process.cwd(), adrPath)}`));
|
|
1557
1913
|
}
|
|
1558
1914
|
}
|
|
1559
1915
|
}
|
|
1560
1916
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1561
1917
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1562
1918
|
if (troubleCommits.length > 0) {
|
|
1563
|
-
console.log(
|
|
1919
|
+
console.log(chalk6.yellow.bold(`
|
|
1564
1920
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1565
1921
|
troubleCommits.forEach((c) => {
|
|
1566
|
-
console.log(
|
|
1922
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1567
1923
|
});
|
|
1568
1924
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1569
1925
|
type: "confirm",
|
|
@@ -1614,12 +1970,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1614
1970
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1615
1971
|
].join("\n");
|
|
1616
1972
|
fs4.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1617
|
-
console.log(
|
|
1973
|
+
console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path5.relative(process.cwd(), tsFilePath)}`));
|
|
1618
1974
|
}
|
|
1619
1975
|
}
|
|
1620
|
-
console.log(
|
|
1976
|
+
console.log(chalk6.green.bold(`
|
|
1621
1977
|
${ko.recap.done}`));
|
|
1622
|
-
console.log(
|
|
1978
|
+
console.log(chalk6.dim(` \u{1F4C4} ${path5.relative(process.cwd(), filePath)}`));
|
|
1623
1979
|
const claudeMdPath = path5.join(process.cwd(), "CLAUDE.md");
|
|
1624
1980
|
if (fs4.existsSync(claudeMdPath)) {
|
|
1625
1981
|
const { updateClaude } = await inquirer3.prompt([{
|
|
@@ -1639,7 +1995,7 @@ ${ko.recap.done}`));
|
|
|
1639
1995
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
1640
1996
|
);
|
|
1641
1997
|
fs4.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
1642
|
-
console.log(
|
|
1998
|
+
console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
1643
1999
|
}
|
|
1644
2000
|
}
|
|
1645
2001
|
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"';
|
|
@@ -1650,237 +2006,73 @@ ${ko.recap.done}`));
|
|
|
1650
2006
|
});
|
|
1651
2007
|
}
|
|
1652
2008
|
|
|
1653
|
-
// src/commands/sync.ts
|
|
1654
|
-
import chalk5 from "chalk";
|
|
1655
|
-
import fs5 from "fs";
|
|
1656
|
-
import path6 from "path";
|
|
1657
|
-
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
1658
|
-
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
1659
|
-
function parseRulesMd(content) {
|
|
1660
|
-
const sections = [];
|
|
1661
|
-
const lines = content.split("\n");
|
|
1662
|
-
let currentTitle = "";
|
|
1663
|
-
let currentContent = [];
|
|
1664
|
-
for (const line of lines) {
|
|
1665
|
-
if (line.startsWith("## ")) {
|
|
1666
|
-
if (currentTitle) {
|
|
1667
|
-
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
1668
|
-
}
|
|
1669
|
-
currentTitle = line.replace("## ", "").trim();
|
|
1670
|
-
currentContent = [];
|
|
1671
|
-
} else {
|
|
1672
|
-
currentContent.push(line);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
if (currentTitle) {
|
|
1676
|
-
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
1677
|
-
}
|
|
1678
|
-
return sections;
|
|
1679
|
-
}
|
|
1680
|
-
function buildCodingDoc(headerTitle, sections, projectName) {
|
|
1681
|
-
const codingSections = sections.filter(
|
|
1682
|
-
(s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
|
|
1683
|
-
);
|
|
1684
|
-
const lines = [
|
|
1685
|
-
`# ${projectName} \u2014 ${headerTitle}`,
|
|
1686
|
-
"",
|
|
1687
|
-
"> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
|
|
1688
|
-
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
1689
|
-
"",
|
|
1690
|
-
"## \uD544\uC218 \uCC38\uC870",
|
|
1691
|
-
"- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md \xB7 RULES.md",
|
|
1692
|
-
""
|
|
1693
|
-
];
|
|
1694
|
-
for (const section of codingSections) {
|
|
1695
|
-
lines.push(`## ${section.title}`);
|
|
1696
|
-
lines.push(section.content);
|
|
1697
|
-
lines.push("");
|
|
1698
|
-
}
|
|
1699
|
-
return lines.join("\n");
|
|
1700
|
-
}
|
|
1701
|
-
function toCursorrules(sections, projectName) {
|
|
1702
|
-
return buildCodingDoc("Cursor Rules", sections, projectName);
|
|
1703
|
-
}
|
|
1704
|
-
function toWindsurfrules(sections, projectName) {
|
|
1705
|
-
return buildCodingDoc("Windsurf Rules", sections, projectName);
|
|
1706
|
-
}
|
|
1707
|
-
function toCopilotInstructions(sections, projectName) {
|
|
1708
|
-
return buildCodingDoc("GitHub Copilot Instructions", sections, projectName);
|
|
1709
|
-
}
|
|
1710
|
-
var ANTIGRAVITY_CHAR_LIMIT = 12e3;
|
|
1711
|
-
var ANTIGRAVITY_TRUNCATE_MARKER = "\n\n<!-- \u26A0\uFE0F Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428 \u2014 \uC804\uCCB4 \uADDC\uCE59\uC740 RULES.md \uCC38\uC870 -->\n";
|
|
1712
|
-
function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
|
|
1713
|
-
if (Buffer.byteLength(content, "utf8") <= limit) return content;
|
|
1714
|
-
const SAFETY = 200;
|
|
1715
|
-
const budget = limit - Buffer.byteLength(ANTIGRAVITY_TRUNCATE_MARKER, "utf8") - SAFETY;
|
|
1716
|
-
let lo = 0;
|
|
1717
|
-
let hi = content.length;
|
|
1718
|
-
while (lo < hi) {
|
|
1719
|
-
const mid = lo + hi + 1 >> 1;
|
|
1720
|
-
if (Buffer.byteLength(content.slice(0, mid), "utf8") <= budget) lo = mid;
|
|
1721
|
-
else hi = mid - 1;
|
|
1722
|
-
}
|
|
1723
|
-
const charCut = lo;
|
|
1724
|
-
let cut = content.lastIndexOf("\n## ", charCut);
|
|
1725
|
-
if (cut < charCut * 0.5) {
|
|
1726
|
-
const nl = content.lastIndexOf("\n", charCut);
|
|
1727
|
-
cut = nl > 0 ? nl : charCut;
|
|
1728
|
-
}
|
|
1729
|
-
return content.slice(0, cut).trimEnd() + ANTIGRAVITY_TRUNCATE_MARKER;
|
|
1730
|
-
}
|
|
1731
|
-
function toAntigravityRules(sections, projectName) {
|
|
1732
|
-
return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
|
|
1733
|
-
}
|
|
1734
|
-
function toClaudeMd(sections, existing) {
|
|
1735
|
-
const recordSections = sections.filter(
|
|
1736
|
-
(s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
|
|
1737
|
-
);
|
|
1738
|
-
const statusMatch = existing.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
|
|
1739
|
-
const statusSection = statusMatch ? statusMatch[0] : "";
|
|
1740
|
-
const header = existing.split("## ")[0].trim();
|
|
1741
|
-
const lines = [
|
|
1742
|
-
header,
|
|
1743
|
-
"",
|
|
1744
|
-
statusSection,
|
|
1745
|
-
"",
|
|
1746
|
-
"> \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.",
|
|
1747
|
-
""
|
|
1748
|
-
];
|
|
1749
|
-
for (const section of recordSections) {
|
|
1750
|
-
lines.push(`## ${section.title}`);
|
|
1751
|
-
lines.push(section.content);
|
|
1752
|
-
lines.push("");
|
|
1753
|
-
}
|
|
1754
|
-
return lines.join("\n");
|
|
1755
|
-
}
|
|
1756
|
-
function deriveProjectName(rulesContent) {
|
|
1757
|
-
const firstLine = rulesContent.split("\n")[0];
|
|
1758
|
-
return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
1759
|
-
}
|
|
1760
|
-
var SYNC_TARGETS = [
|
|
1761
|
-
{ path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
|
|
1762
|
-
{ path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
|
|
1763
|
-
{ path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
|
|
1764
|
-
{ path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone }
|
|
1765
|
-
];
|
|
1766
|
-
async function sync() {
|
|
1767
|
-
console.log(chalk5.bold(`
|
|
1768
|
-
${ko.sync.title}
|
|
1769
|
-
`));
|
|
1770
|
-
const cwd = process.cwd();
|
|
1771
|
-
const rulesPath = path6.join(cwd, "RULES.md");
|
|
1772
|
-
if (!fs5.existsSync(rulesPath)) {
|
|
1773
|
-
console.log(chalk5.yellow(ko.sync.noRules));
|
|
1774
|
-
console.log(chalk5.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
1775
|
-
console.log(chalk5.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
1776
|
-
console.log("");
|
|
1777
|
-
console.log(chalk5.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
1778
|
-
console.log(chalk5.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
1779
|
-
console.log(chalk5.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
1780
|
-
console.log(chalk5.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
1781
|
-
console.log(chalk5.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
1782
|
-
console.log(chalk5.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
|
|
1786
|
-
const sections = parseRulesMd(rulesContent);
|
|
1787
|
-
console.log(chalk5.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
1788
|
-
const projectName = deriveProjectName(rulesContent);
|
|
1789
|
-
for (const target of SYNC_TARGETS) {
|
|
1790
|
-
const fullPath = path6.join(cwd, target.path);
|
|
1791
|
-
fs5.mkdirSync(path6.dirname(fullPath), { recursive: true });
|
|
1792
|
-
const content = target.generate(sections, projectName);
|
|
1793
|
-
fs5.writeFileSync(fullPath, content, "utf-8");
|
|
1794
|
-
console.log(chalk5.green(` ${target.doneMessage}`));
|
|
1795
|
-
if (content.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
|
|
1796
|
-
console.log(chalk5.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
const claudePath = path6.join(cwd, "CLAUDE.md");
|
|
1800
|
-
const existingClaude = fs5.existsSync(claudePath) ? fs5.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
1801
|
-
|
|
1802
|
-
## \uD604\uC7AC \uC0C1\uD0DC
|
|
1803
|
-
- **Phase:** __FILL__
|
|
1804
|
-
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
1805
|
-
- **\uB2E4\uC74C \uC561\uC158:** __FILL__
|
|
1806
|
-
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
1807
|
-
fs5.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
|
|
1808
|
-
console.log(chalk5.green(` ${ko.sync.claudeDone}`));
|
|
1809
|
-
console.log(chalk5.bold.green(`
|
|
1810
|
-
${ko.sync.done}`));
|
|
1811
|
-
console.log(chalk5.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
|
|
1812
|
-
console.log(chalk5.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
1813
|
-
console.log(chalk5.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
1814
|
-
printNextStep({
|
|
1815
|
-
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
1816
|
-
command: "vhk \uC810\uAC80",
|
|
1817
|
-
cursorHint: "\uADDC\uCE59 \uC810\uAC80\uD574\uC918"
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
2009
|
// src/commands/check.ts
|
|
1822
|
-
import
|
|
1823
|
-
import
|
|
1824
|
-
import
|
|
2010
|
+
import chalk8 from "chalk";
|
|
2011
|
+
import path7 from "path";
|
|
2012
|
+
import fs6 from "fs";
|
|
1825
2013
|
|
|
1826
2014
|
// src/lib/rules-parser.ts
|
|
1827
|
-
import
|
|
1828
|
-
import
|
|
2015
|
+
import fs5 from "fs";
|
|
2016
|
+
import path6 from "path";
|
|
1829
2017
|
function parseRules(rulesPath) {
|
|
1830
|
-
if (!
|
|
1831
|
-
const content =
|
|
2018
|
+
if (!fs5.existsSync(rulesPath)) return [];
|
|
2019
|
+
const content = fs5.readFileSync(rulesPath, "utf-8");
|
|
1832
2020
|
const lines = content.split("\n");
|
|
1833
2021
|
const rules = [];
|
|
1834
2022
|
let currentSection = "";
|
|
1835
|
-
let
|
|
1836
|
-
|
|
2023
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2024
|
+
const line = lines[i];
|
|
2025
|
+
const lineNo = i + 1;
|
|
1837
2026
|
if (line.startsWith("## ")) {
|
|
1838
2027
|
currentSection = line.replace("## ", "").trim();
|
|
1839
2028
|
continue;
|
|
1840
2029
|
}
|
|
1841
2030
|
const bulletMatch = line.match(/^[-*]\s+(.+)/);
|
|
1842
2031
|
if (!bulletMatch) continue;
|
|
2032
|
+
if (isMetaSection(currentSection)) continue;
|
|
1843
2033
|
const ruleText = bulletMatch[1];
|
|
1844
|
-
ruleIndex++;
|
|
1845
2034
|
if (/kebab[- ]?case/i.test(ruleText)) {
|
|
1846
|
-
rules.push(createNamingRule(
|
|
1847
|
-
`naming-${ruleIndex}`,
|
|
1848
|
-
currentSection,
|
|
1849
|
-
ruleText,
|
|
1850
|
-
"kebab-case"
|
|
1851
|
-
));
|
|
2035
|
+
rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "kebab-case"));
|
|
1852
2036
|
} else if (/camel[- ]?case/i.test(ruleText)) {
|
|
1853
|
-
rules.push(createNamingRule(
|
|
1854
|
-
`naming-${ruleIndex}`,
|
|
1855
|
-
currentSection,
|
|
1856
|
-
ruleText,
|
|
1857
|
-
"camelCase"
|
|
1858
|
-
));
|
|
1859
|
-
}
|
|
1860
|
-
const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
|
|
1861
|
-
if (pathMatch) {
|
|
1862
|
-
rules.push(createStructureRule(
|
|
1863
|
-
`structure-${ruleIndex}`,
|
|
1864
|
-
currentSection,
|
|
1865
|
-
ruleText,
|
|
1866
|
-
pathMatch[1]
|
|
1867
|
-
));
|
|
2037
|
+
rules.push(createNamingRule(`naming-L${lineNo}`, currentSection, ruleText, "camelCase"));
|
|
1868
2038
|
}
|
|
1869
|
-
if (
|
|
1870
|
-
const
|
|
1871
|
-
if (
|
|
1872
|
-
rules.push(
|
|
1873
|
-
`ban-${ruleIndex}`,
|
|
1874
|
-
currentSection,
|
|
1875
|
-
ruleText,
|
|
1876
|
-
backtickContent[1],
|
|
1877
|
-
"banned"
|
|
1878
|
-
));
|
|
2039
|
+
if (isStructureSection(currentSection)) {
|
|
2040
|
+
const pathMatch = ruleText.match(/`([a-zA-Z0-9_/.-]+\/)`/);
|
|
2041
|
+
if (pathMatch) {
|
|
2042
|
+
rules.push(createStructureRule(`structure-L${lineNo}`, currentSection, ruleText, pathMatch[1]));
|
|
1879
2043
|
}
|
|
1880
2044
|
}
|
|
2045
|
+
const banToken = extractBanToken(ruleText);
|
|
2046
|
+
if (banToken) {
|
|
2047
|
+
rules.push(createContentRule(`ban-L${lineNo}`, currentSection, ruleText, banToken, "banned"));
|
|
2048
|
+
}
|
|
1881
2049
|
}
|
|
1882
2050
|
return rules;
|
|
1883
2051
|
}
|
|
2052
|
+
function isMetaSection(section) {
|
|
2053
|
+
const s = section.toLowerCase();
|
|
2054
|
+
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(
|
|
2055
|
+
(k) => s.includes(k)
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
function isStructureSection(section) {
|
|
2059
|
+
const s = section.toLowerCase();
|
|
2060
|
+
return ["\uC544\uD0A4\uD14D\uCC98", "\uAD6C\uC870", "\uB514\uB809\uD130\uB9AC", "\uD3F4\uB354", "architecture", "structure"].some((k) => s.includes(k));
|
|
2061
|
+
}
|
|
2062
|
+
function extractBanToken(ruleText) {
|
|
2063
|
+
const banKw = ruleText.search(/금지|사용하지|쓰지\s*마|하지\s*않는다|never use|do not use/i);
|
|
2064
|
+
if (banKw < 0) return null;
|
|
2065
|
+
let token = null;
|
|
2066
|
+
const before = [...ruleText.slice(0, banKw).matchAll(/`([^`]+)`/g)].pop();
|
|
2067
|
+
if (before) token = before[1];
|
|
2068
|
+
else {
|
|
2069
|
+
const after = ruleText.slice(banKw).match(/^(?:금지|사용하지|쓰지\s*마|do not use|never use)\s*[::]?\s*`([^`]+)`/i);
|
|
2070
|
+
if (after) token = after[1];
|
|
2071
|
+
}
|
|
2072
|
+
if (!token) return null;
|
|
2073
|
+
if (/:\/\/|www\.|\.(com|io|dev|net|org|app)\b|\//.test(token)) return null;
|
|
2074
|
+
return token;
|
|
2075
|
+
}
|
|
1884
2076
|
function createNamingRule(id, section, desc, convention) {
|
|
1885
2077
|
return {
|
|
1886
2078
|
id,
|
|
@@ -1889,17 +2081,17 @@ function createNamingRule(id, section, desc, convention) {
|
|
|
1889
2081
|
description: desc,
|
|
1890
2082
|
check: (cwd) => {
|
|
1891
2083
|
const violations = [];
|
|
1892
|
-
const srcDir =
|
|
1893
|
-
if (!
|
|
2084
|
+
const srcDir = path6.join(cwd, "src");
|
|
2085
|
+
if (!fs5.existsSync(srcDir)) return violations;
|
|
1894
2086
|
walkFiles(srcDir, (filePath) => {
|
|
1895
|
-
const name =
|
|
2087
|
+
const name = path6.basename(filePath, path6.extname(filePath));
|
|
1896
2088
|
if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
1897
2089
|
if (!["index", "vite.config", "tsconfig"].includes(name)) {
|
|
1898
2090
|
violations.push({
|
|
1899
2091
|
ruleId: id,
|
|
1900
2092
|
severity: "warning",
|
|
1901
2093
|
message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
|
|
1902
|
-
file:
|
|
2094
|
+
file: path6.relative(cwd, filePath)
|
|
1903
2095
|
});
|
|
1904
2096
|
}
|
|
1905
2097
|
}
|
|
@@ -1915,8 +2107,8 @@ function createStructureRule(id, section, desc, expectedPath) {
|
|
|
1915
2107
|
type: "structure",
|
|
1916
2108
|
description: desc,
|
|
1917
2109
|
check: (cwd) => {
|
|
1918
|
-
const fullPath =
|
|
1919
|
-
if (!
|
|
2110
|
+
const fullPath = path6.join(cwd, expectedPath);
|
|
2111
|
+
if (!fs5.existsSync(fullPath)) {
|
|
1920
2112
|
return [{
|
|
1921
2113
|
ruleId: id,
|
|
1922
2114
|
severity: "error",
|
|
@@ -1936,11 +2128,11 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1936
2128
|
pattern: new RegExp(escapeRegex(pattern), "i"),
|
|
1937
2129
|
check: (cwd) => {
|
|
1938
2130
|
const violations = [];
|
|
1939
|
-
const srcDir =
|
|
1940
|
-
if (!
|
|
2131
|
+
const srcDir = path6.join(cwd, "src");
|
|
2132
|
+
if (!fs5.existsSync(srcDir)) return violations;
|
|
1941
2133
|
const regex = new RegExp(escapeRegex(pattern), "i");
|
|
1942
2134
|
walkFiles(srcDir, (filePath) => {
|
|
1943
|
-
const fileContent =
|
|
2135
|
+
const fileContent = fs5.readFileSync(filePath, "utf-8");
|
|
1944
2136
|
const fileLines = fileContent.split("\n");
|
|
1945
2137
|
fileLines.forEach((line, idx) => {
|
|
1946
2138
|
if (regex.test(line)) {
|
|
@@ -1948,7 +2140,7 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1948
2140
|
ruleId: id,
|
|
1949
2141
|
severity: type === "banned" ? "error" : "warning",
|
|
1950
2142
|
message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
|
|
1951
|
-
file:
|
|
2143
|
+
file: path6.relative(cwd, filePath),
|
|
1952
2144
|
line: idx + 1
|
|
1953
2145
|
});
|
|
1954
2146
|
}
|
|
@@ -1960,9 +2152,9 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
1960
2152
|
};
|
|
1961
2153
|
}
|
|
1962
2154
|
function walkFiles(dir, callback) {
|
|
1963
|
-
const entries =
|
|
2155
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1964
2156
|
for (const entry of entries) {
|
|
1965
|
-
const fullPath =
|
|
2157
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1966
2158
|
if (entry.isDirectory()) {
|
|
1967
2159
|
if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
|
|
1968
2160
|
walkFiles(fullPath, callback);
|
|
@@ -1977,13 +2169,13 @@ function escapeRegex(str) {
|
|
|
1977
2169
|
}
|
|
1978
2170
|
|
|
1979
2171
|
// src/commands/goal.ts
|
|
1980
|
-
import { existsSync as
|
|
1981
|
-
import { join as
|
|
1982
|
-
import
|
|
2172
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
|
|
2173
|
+
import { join as join4 } from "path";
|
|
2174
|
+
import chalk7 from "chalk";
|
|
1983
2175
|
|
|
1984
2176
|
// src/lib/goal-frontmatter.ts
|
|
1985
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
1986
|
-
import { join } from "path";
|
|
2177
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
2178
|
+
import { join as join3 } from "path";
|
|
1987
2179
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1988
2180
|
function parseFrontmatter(content) {
|
|
1989
2181
|
const m = content.match(FRONTMATTER_RE);
|
|
@@ -2015,9 +2207,9 @@ function parseSimpleYaml(yaml) {
|
|
|
2015
2207
|
return out;
|
|
2016
2208
|
}
|
|
2017
2209
|
function parseGoalFile(filePath) {
|
|
2018
|
-
if (!
|
|
2210
|
+
if (!existsSync2(filePath)) return null;
|
|
2019
2211
|
try {
|
|
2020
|
-
const content =
|
|
2212
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2021
2213
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
2022
2214
|
return { filePath, frontmatter, body };
|
|
2023
2215
|
} catch {
|
|
@@ -2025,7 +2217,7 @@ function parseGoalFile(filePath) {
|
|
|
2025
2217
|
}
|
|
2026
2218
|
}
|
|
2027
2219
|
function listGoals(goalsDir) {
|
|
2028
|
-
if (!
|
|
2220
|
+
if (!existsSync2(goalsDir)) return [];
|
|
2029
2221
|
let entries;
|
|
2030
2222
|
try {
|
|
2031
2223
|
entries = readdirSync(goalsDir);
|
|
@@ -2036,7 +2228,7 @@ function listGoals(goalsDir) {
|
|
|
2036
2228
|
for (const name of entries) {
|
|
2037
2229
|
if (!name.endsWith(".md")) continue;
|
|
2038
2230
|
if (name === "_meta.md") continue;
|
|
2039
|
-
const fp =
|
|
2231
|
+
const fp = join3(goalsDir, name);
|
|
2040
2232
|
try {
|
|
2041
2233
|
if (!statSync(fp).isFile()) continue;
|
|
2042
2234
|
} catch {
|
|
@@ -2051,10 +2243,40 @@ function listGoals(goalsDir) {
|
|
|
2051
2243
|
parsed.sort((a, b) => a.frontmatter.id - b.frontmatter.id);
|
|
2052
2244
|
return parsed;
|
|
2053
2245
|
}
|
|
2054
|
-
function
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2246
|
+
function findSkippedGoalFiles(goalsDir) {
|
|
2247
|
+
if (!existsSync2(goalsDir)) return [];
|
|
2248
|
+
let entries;
|
|
2249
|
+
try {
|
|
2250
|
+
entries = readdirSync(goalsDir);
|
|
2251
|
+
} catch {
|
|
2252
|
+
return [];
|
|
2253
|
+
}
|
|
2254
|
+
const out = [];
|
|
2255
|
+
for (const name of entries) {
|
|
2256
|
+
if (!name.endsWith(".md")) continue;
|
|
2257
|
+
if (name === "_meta.md") continue;
|
|
2258
|
+
const fp = join3(goalsDir, name);
|
|
2259
|
+
try {
|
|
2260
|
+
if (!statSync(fp).isFile()) continue;
|
|
2261
|
+
} catch {
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
const g = parseGoalFile(fp);
|
|
2265
|
+
if (!g) continue;
|
|
2266
|
+
const fm = g.frontmatter;
|
|
2267
|
+
if (fm.type === "goal" && typeof fm.id === "number") continue;
|
|
2268
|
+
if (fm.type === "meta") continue;
|
|
2269
|
+
const looksLikeGoal = fm.type === "goal" || "id" in fm || "status" in fm || "priority" in fm || "title" in fm;
|
|
2270
|
+
if (!looksLikeGoal) continue;
|
|
2271
|
+
const reason = fm.type !== "goal" ? "type: goal \uB204\uB77D (frontmatter \uC5D0 'type: goal' \uD544\uC694)" : "id \uAC00 \uC22B\uC790\uAC00 \uC544\uB2D8 ('id: 1' \uCC98\uB7FC \uC22B\uC790\uB9CC)";
|
|
2272
|
+
out.push({ file: name, reason });
|
|
2273
|
+
}
|
|
2274
|
+
return out;
|
|
2275
|
+
}
|
|
2276
|
+
function findDuplicateIds(goals) {
|
|
2277
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2278
|
+
for (const g of goals) {
|
|
2279
|
+
const id = g.frontmatter.id;
|
|
2058
2280
|
if (typeof id !== "number") continue;
|
|
2059
2281
|
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
2060
2282
|
}
|
|
@@ -2102,7 +2324,7 @@ ${body}`;
|
|
|
2102
2324
|
|
|
2103
2325
|
// src/commands/goal.ts
|
|
2104
2326
|
var GOALS_DIR = "goals";
|
|
2105
|
-
var
|
|
2327
|
+
var STATE_DIR2 = "docs/state";
|
|
2106
2328
|
var SCRIPTS_DIR = "scripts";
|
|
2107
2329
|
var STATUS_ICON = {
|
|
2108
2330
|
NOT_STARTED: "\u26AA",
|
|
@@ -2128,13 +2350,15 @@ function resolveGoalId(optId, goals) {
|
|
|
2128
2350
|
return selectActiveId(goals);
|
|
2129
2351
|
}
|
|
2130
2352
|
async function goalList() {
|
|
2131
|
-
console.log(
|
|
2353
|
+
console.log(chalk7.bold(`
|
|
2132
2354
|
${ko.goal.listTitle}
|
|
2133
2355
|
`));
|
|
2134
2356
|
const goals = listGoals(GOALS_DIR);
|
|
2357
|
+
const skipped = findSkippedGoalFiles(GOALS_DIR);
|
|
2135
2358
|
if (goals.length === 0) {
|
|
2136
|
-
console.log(
|
|
2137
|
-
console.log(
|
|
2359
|
+
console.log(chalk7.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2360
|
+
console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2361
|
+
printSkippedGoalWarnings(skipped);
|
|
2138
2362
|
return;
|
|
2139
2363
|
}
|
|
2140
2364
|
for (const g of goals) {
|
|
@@ -2151,17 +2375,33 @@ ${ko.goal.listTitle}
|
|
|
2151
2375
|
const dups = findDuplicateIds(goals);
|
|
2152
2376
|
if (dups.length > 0) {
|
|
2153
2377
|
console.log("");
|
|
2154
|
-
console.log(
|
|
2378
|
+
console.log(chalk7.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
|
|
2379
|
+
}
|
|
2380
|
+
printSkippedGoalWarnings(skipped);
|
|
2381
|
+
}
|
|
2382
|
+
function printSkippedGoalWarnings(skipped) {
|
|
2383
|
+
if (skipped.length > 0) {
|
|
2384
|
+
console.log("");
|
|
2385
|
+
console.log(chalk7.yellow(` ${ko.goal.skippedFiles(skipped.length)}`));
|
|
2386
|
+
for (const s of skipped) {
|
|
2387
|
+
console.log(chalk7.yellow(` - goals/${s.file}: ${s.reason}`));
|
|
2388
|
+
}
|
|
2389
|
+
console.log(chalk7.dim(" \uD544\uC218: type: goal + \uC22B\uC790 id. \uC2A4\uD0A4\uB9C8 \uC804\uCCB4: goals/_meta.md"));
|
|
2155
2390
|
}
|
|
2156
2391
|
}
|
|
2157
2392
|
async function goalNext() {
|
|
2158
|
-
console.log(
|
|
2393
|
+
console.log(chalk7.bold(`
|
|
2159
2394
|
${ko.goal.nextTitle}
|
|
2160
2395
|
`));
|
|
2161
2396
|
const goals = listGoals(GOALS_DIR);
|
|
2397
|
+
if (goals.length === 0) {
|
|
2398
|
+
console.log(chalk7.yellow(" \u{1F4ED} \uC815\uC758\uB41C goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2399
|
+
console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2162
2402
|
const activeId = selectActiveId(goals);
|
|
2163
2403
|
if (activeId === null) {
|
|
2164
|
-
console.log(
|
|
2404
|
+
console.log(chalk7.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
|
|
2165
2405
|
return;
|
|
2166
2406
|
}
|
|
2167
2407
|
const active = goals.find((g) => g.frontmatter.id === activeId);
|
|
@@ -2180,10 +2420,10 @@ ${ko.goal.nextTitle}
|
|
|
2180
2420
|
"```",
|
|
2181
2421
|
""
|
|
2182
2422
|
].join("\n");
|
|
2183
|
-
|
|
2184
|
-
|
|
2423
|
+
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2424
|
+
writeFileSync2(join4(STATE_DIR2, "next-task.md"), text, "utf-8");
|
|
2185
2425
|
console.log(
|
|
2186
|
-
|
|
2426
|
+
chalk7.green(
|
|
2187
2427
|
` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
|
|
2188
2428
|
)
|
|
2189
2429
|
);
|
|
@@ -2202,35 +2442,70 @@ version: v0.1
|
|
|
2202
2442
|
## Forbidden Actions (\uC804\uC5ED)
|
|
2203
2443
|
|
|
2204
2444
|
- (\uD574\uB2F9 \uC0AC\uD56D)
|
|
2445
|
+
|
|
2446
|
+
## Goal \uD30C\uC77C \uC2A4\uD0A4\uB9C8 (\uD544\uB3C5 \u2014 VHK-021)
|
|
2447
|
+
|
|
2448
|
+
\`vhk goal list/next/check/done\` \uB294 \`goals/*.md\`(\uC774 \`_meta.md\` \uC81C\uC678) \uC911 \uC544\uB798
|
|
2449
|
+
frontmatter \uB97C \uB9CC\uC871\uD558\uB294 \uD30C\uC77C\uB9CC goal \uB85C \uC778\uC2DD\uD55C\uB2E4. **\uD558\uB098\uB77C\uB3C4 \uC5B4\uAE0B\uB098\uBA74 \uC870\uC6A9\uD788 \uBB34\uC2DC**\uB418\uBA70
|
|
2450
|
+
\`vhk goal list\` \uAC00 \uACBD\uACE0\uB85C \uC54C\uB824\uC900\uB2E4.
|
|
2451
|
+
|
|
2452
|
+
| \uD544\uB4DC | \uD544\uC218 | \uAC12 |
|
|
2453
|
+
| --- | --- | --- |
|
|
2454
|
+
| \`type\` | \u2705 | \`goal\` (\uBB38\uC790\uC5F4 \uADF8\uB300\uB85C) |
|
|
2455
|
+
| \`id\` | \u2705 | **\uC22B\uC790\uB9CC** (\`1\`, \`2\` \u2026 \u2014 \`G1\` \uAC19\uC740 \uBB38\uC790\uC5F4 \u274C) |
|
|
2456
|
+
| \`status\` | \u2705 | \`NOT_STARTED\` | \`IN_PROGRESS\` | \`DONE\` | \`BLOCKED\` |
|
|
2457
|
+
| \`priority\` | \uAD8C\uC7A5 | \`P0\` | \`P1\` | \`P2\` |
|
|
2458
|
+
| \`title\` | \uAD8C\uC7A5 | \uD55C \uC904 \uC81C\uBAA9 |
|
|
2459
|
+
|
|
2460
|
+
\uD30C\uC77C\uBA85 \uADDC\uCE59: \`goals/<id>-<name>.md\` (\uC608: \`goals/1-login.md\`).
|
|
2461
|
+
|
|
2462
|
+
### \uC0C8 goal \uD15C\uD50C\uB9BF (\uBCF5\uBD99)
|
|
2463
|
+
|
|
2464
|
+
\`\`\`markdown
|
|
2465
|
+
---
|
|
2466
|
+
vhk_format: 1
|
|
2467
|
+
type: goal
|
|
2468
|
+
id: 1
|
|
2469
|
+
title: \uB85C\uADF8\uC778 \uAE30\uB2A5
|
|
2470
|
+
status: NOT_STARTED
|
|
2471
|
+
priority: P0
|
|
2472
|
+
---
|
|
2473
|
+
|
|
2474
|
+
# Goal 1: \uB85C\uADF8\uC778 \uAE30\uB2A5
|
|
2475
|
+
|
|
2476
|
+
## \uBC30\uACBD / \uB3D9\uC791 / Completion Check ...
|
|
2477
|
+
\`\`\`
|
|
2478
|
+
|
|
2479
|
+
\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uB294 \`vhk goal sync\` \uB85C \`scripts/check-goal-<id>.mjs\` \uB97C \uBC31\uD544\uD55C\uB2E4.
|
|
2205
2480
|
`;
|
|
2206
2481
|
var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C \uC790\uB3D9 \uAC31\uC2E0)\n```\n";
|
|
2207
2482
|
var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
|
|
2208
2483
|
var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
|
|
2209
2484
|
async function goalInit() {
|
|
2210
|
-
console.log(
|
|
2485
|
+
console.log(chalk7.bold(`
|
|
2211
2486
|
${ko.goal.initTitle}
|
|
2212
2487
|
`));
|
|
2213
2488
|
const targets = [
|
|
2214
|
-
{ path:
|
|
2215
|
-
{ path:
|
|
2216
|
-
{ path:
|
|
2217
|
-
{ path:
|
|
2489
|
+
{ path: join4(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
|
|
2490
|
+
{ path: join4(STATE_DIR2, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
|
|
2491
|
+
{ path: join4(STATE_DIR2, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
|
|
2492
|
+
{ path: join4(STATE_DIR2, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
|
|
2218
2493
|
];
|
|
2219
|
-
|
|
2220
|
-
|
|
2494
|
+
mkdirSync2(GOALS_DIR, { recursive: true });
|
|
2495
|
+
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2221
2496
|
let created = 0;
|
|
2222
2497
|
let skipped = 0;
|
|
2223
2498
|
for (const t2 of targets) {
|
|
2224
|
-
if (
|
|
2225
|
-
console.log(
|
|
2499
|
+
if (existsSync3(t2.path)) {
|
|
2500
|
+
console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
|
|
2226
2501
|
skipped++;
|
|
2227
2502
|
} else {
|
|
2228
|
-
|
|
2229
|
-
console.log(
|
|
2503
|
+
writeFileSync2(t2.path, t2.content, "utf-8");
|
|
2504
|
+
console.log(chalk7.green(` \u2713 created: ${t2.path}`));
|
|
2230
2505
|
created++;
|
|
2231
2506
|
}
|
|
2232
2507
|
}
|
|
2233
|
-
console.log(
|
|
2508
|
+
console.log(chalk7.bold(`
|
|
2234
2509
|
\u{1F4CA} created=${created} skipped=${skipped}`));
|
|
2235
2510
|
if (created > 0) {
|
|
2236
2511
|
printNextStep({
|
|
@@ -2241,10 +2516,10 @@ ${ko.goal.initTitle}
|
|
|
2241
2516
|
}
|
|
2242
2517
|
}
|
|
2243
2518
|
function findGateScript(id) {
|
|
2244
|
-
const mjs =
|
|
2245
|
-
if (
|
|
2246
|
-
const sh =
|
|
2247
|
-
if (
|
|
2519
|
+
const mjs = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
2520
|
+
if (existsSync3(mjs)) return mjs;
|
|
2521
|
+
const sh = join4(SCRIPTS_DIR, `check-goal-${id}.sh`);
|
|
2522
|
+
if (existsSync3(sh)) return sh;
|
|
2248
2523
|
return null;
|
|
2249
2524
|
}
|
|
2250
2525
|
function runGate(scriptPath) {
|
|
@@ -2253,82 +2528,93 @@ function runGate(scriptPath) {
|
|
|
2253
2528
|
const r = safeExecFile(runner, [scriptPath]);
|
|
2254
2529
|
return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
|
|
2255
2530
|
}
|
|
2531
|
+
function warnIfBashOnWindows(scriptPath) {
|
|
2532
|
+
if (process.platform === "win32" && scriptPath.endsWith(".sh")) {
|
|
2533
|
+
console.log(
|
|
2534
|
+
chalk7.yellow(
|
|
2535
|
+
" \u26A0 Windows: .sh \uAC8C\uC774\uD2B8\uB294 bash \uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. cross-platform .mjs \uB85C \uBC31\uD544\uD558\uC138\uC694 \u2192 vhk goal sync"
|
|
2536
|
+
)
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2256
2540
|
async function goalCheck(opts) {
|
|
2257
|
-
console.log(
|
|
2541
|
+
console.log(chalk7.bold(`
|
|
2258
2542
|
${ko.goal.checkTitle}
|
|
2259
2543
|
`));
|
|
2260
2544
|
const goals = listGoals(GOALS_DIR);
|
|
2261
2545
|
const id = resolveGoalId(opts.id, goals);
|
|
2262
2546
|
if (id === null) {
|
|
2263
2547
|
console.log(
|
|
2264
|
-
|
|
2548
|
+
chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2265
2549
|
);
|
|
2266
2550
|
process.exitCode = 1;
|
|
2267
2551
|
return;
|
|
2268
2552
|
}
|
|
2269
2553
|
if (!goals.some((g) => g.frontmatter.id === id)) {
|
|
2270
|
-
console.log(
|
|
2554
|
+
console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
2271
2555
|
process.exitCode = 1;
|
|
2272
2556
|
return;
|
|
2273
2557
|
}
|
|
2274
2558
|
const scriptPath = findGateScript(id);
|
|
2275
2559
|
if (!scriptPath) {
|
|
2276
2560
|
console.log(
|
|
2277
|
-
|
|
2561
|
+
chalk7.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
|
|
2278
2562
|
);
|
|
2279
2563
|
process.exitCode = 1;
|
|
2280
2564
|
return;
|
|
2281
2565
|
}
|
|
2566
|
+
warnIfBashOnWindows(scriptPath);
|
|
2282
2567
|
const gate2 = runGate(scriptPath);
|
|
2283
|
-
console.log(
|
|
2568
|
+
console.log(chalk7.dim(` \u25B6 ${gate2.runner} ${scriptPath}
|
|
2284
2569
|
`));
|
|
2285
2570
|
if (gate2.out) console.log(gate2.out);
|
|
2286
2571
|
if (gate2.ok) {
|
|
2287
|
-
console.log(
|
|
2572
|
+
console.log(chalk7.green(`
|
|
2288
2573
|
\u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
|
|
2289
2574
|
} else {
|
|
2290
|
-
console.log(
|
|
2575
|
+
console.log(chalk7.red(`
|
|
2291
2576
|
\u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
|
|
2292
|
-
if (gate2.err && !gate2.out) console.log(
|
|
2577
|
+
if (gate2.err && !gate2.out) console.log(chalk7.dim(gate2.err.slice(0, 500)));
|
|
2293
2578
|
process.exitCode = 1;
|
|
2294
2579
|
}
|
|
2295
2580
|
}
|
|
2296
2581
|
async function goalDone(opts) {
|
|
2297
|
-
console.log(
|
|
2582
|
+
console.log(chalk7.bold(`
|
|
2298
2583
|
${ko.goal.doneTitle}
|
|
2299
2584
|
`));
|
|
2300
2585
|
const goals = listGoals(GOALS_DIR);
|
|
2301
2586
|
const id = resolveGoalId(opts.id, goals);
|
|
2302
2587
|
if (id === null) {
|
|
2303
2588
|
console.log(
|
|
2304
|
-
|
|
2589
|
+
chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
2305
2590
|
);
|
|
2306
2591
|
process.exitCode = 1;
|
|
2307
2592
|
return;
|
|
2308
2593
|
}
|
|
2309
2594
|
const target = goals.find((g) => g.frontmatter.id === id);
|
|
2310
2595
|
if (!target) {
|
|
2311
|
-
console.log(
|
|
2596
|
+
console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
2312
2597
|
process.exitCode = 1;
|
|
2313
2598
|
return;
|
|
2314
2599
|
}
|
|
2315
2600
|
const scriptPath = findGateScript(id);
|
|
2316
2601
|
if (!scriptPath) {
|
|
2317
2602
|
console.log(
|
|
2318
|
-
|
|
2603
|
+
chalk7.red(
|
|
2319
2604
|
` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
|
|
2320
2605
|
)
|
|
2321
2606
|
);
|
|
2322
2607
|
process.exitCode = 1;
|
|
2323
2608
|
return;
|
|
2324
2609
|
}
|
|
2610
|
+
warnIfBashOnWindows(scriptPath);
|
|
2325
2611
|
const gate2 = runGate(scriptPath);
|
|
2326
|
-
console.log(
|
|
2612
|
+
console.log(chalk7.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
|
|
2327
2613
|
`));
|
|
2328
2614
|
if (gate2.out) console.log(gate2.out);
|
|
2329
2615
|
if (!gate2.ok) {
|
|
2330
2616
|
console.log(
|
|
2331
|
-
|
|
2617
|
+
chalk7.red(
|
|
2332
2618
|
`
|
|
2333
2619
|
\u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
|
|
2334
2620
|
)
|
|
@@ -2336,11 +2622,11 @@ ${ko.goal.doneTitle}
|
|
|
2336
2622
|
process.exitCode = 1;
|
|
2337
2623
|
return;
|
|
2338
2624
|
}
|
|
2339
|
-
const content =
|
|
2340
|
-
const today = (
|
|
2625
|
+
const content = readFileSync3(target.filePath, "utf-8");
|
|
2626
|
+
const today = localDate();
|
|
2341
2627
|
const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
|
|
2342
|
-
|
|
2343
|
-
console.log(
|
|
2628
|
+
writeFileSync2(target.filePath, updated, "utf-8");
|
|
2629
|
+
console.log(chalk7.green(`
|
|
2344
2630
|
\u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
|
|
2345
2631
|
printNextStep({
|
|
2346
2632
|
message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
|
|
@@ -2348,6 +2634,112 @@ ${ko.goal.doneTitle}
|
|
|
2348
2634
|
cursorHint: "\uB2E4\uC74C goal \uC54C\uB824\uC918"
|
|
2349
2635
|
});
|
|
2350
2636
|
}
|
|
2637
|
+
function generateGateScript(id) {
|
|
2638
|
+
const ID = String(id);
|
|
2639
|
+
return [
|
|
2640
|
+
"#!/usr/bin/env node",
|
|
2641
|
+
`// scripts/check-goal-${ID}.mjs \u2014 \uC790\uB3D9 \uC0DD\uC131 (vhk goal sync).`,
|
|
2642
|
+
"// \uAE30\uBCF8 \uAC8C\uC774\uD2B8 = typecheck + (lint) + test + build. goal \uACE0\uC720 \uAC80\uC99D\uC740 \uC544\uB798 \uAD6C\uC5ED\uC5D0 \uCD94\uAC00.",
|
|
2643
|
+
"// sync \uC7AC\uC2E4\uD589\uD574\uB3C4 \uAE30\uC874 \uD30C\uC77C\uC740 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (idempotent).",
|
|
2644
|
+
"//",
|
|
2645
|
+
"// Env: VHK_GATES_SKIP_DEEP=1 \u2192 test + build \uC2A4\uD0B5 (\uBE60\uB978 typecheck-only \uD328\uC2A4)",
|
|
2646
|
+
"",
|
|
2647
|
+
"import { execFileSync } from 'node:child_process'",
|
|
2648
|
+
"import { existsSync, readFileSync } from 'node:fs'",
|
|
2649
|
+
"",
|
|
2650
|
+
"const SHIM = new Set(['pnpm', 'npm', 'npx', 'yarn'])",
|
|
2651
|
+
"function run(cmd, args) {",
|
|
2652
|
+
" let bin = cmd, argv = args",
|
|
2653
|
+
" if (process.platform === 'win32' && SHIM.has(cmd)) {",
|
|
2654
|
+
" // Windows: .cmd shim \uC9C1\uC811 spawn \uC740 Node CVE-2024-27980 \uC73C\uB85C EINVAL \u2192 cmd.exe \uB798\uD551.",
|
|
2655
|
+
" bin = 'cmd.exe'; argv = ['/d', '/s', '/c', cmd + '.cmd', ...args]",
|
|
2656
|
+
" }",
|
|
2657
|
+
" try {",
|
|
2658
|
+
" // maxBuffer \uC0C1\uD5A5: \uD070 \uBE4C\uB4DC/\uD14C\uC2A4\uD2B8 \uB85C\uADF8(>1MB)\uC5D0\uC11C \uC131\uACF5\uD574\uB3C4 ENOBUFS \uAC70\uC9D3\uC2E4\uD328 \uBC29\uC9C0.",
|
|
2659
|
+
" execFileSync(bin, argv, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 })",
|
|
2660
|
+
" return true",
|
|
2661
|
+
" } catch (e) {",
|
|
2662
|
+
" const out = (e?.stdout?.toString() ?? '') + (e?.stderr?.toString() ?? '')",
|
|
2663
|
+
" if (out.trim()) console.log(out.split('\\n').slice(-25).join('\\n'))",
|
|
2664
|
+
" return false",
|
|
2665
|
+
" }",
|
|
2666
|
+
"}",
|
|
2667
|
+
"",
|
|
2668
|
+
"if (existsSync('.vhk/HARD_STOP')) {",
|
|
2669
|
+
` console.log('\u{1F6D1} .vhk/HARD_STOP detected \u2014 refusing to run goal ${ID} gate.')`,
|
|
2670
|
+
" process.exit(1)",
|
|
2671
|
+
"}",
|
|
2672
|
+
"",
|
|
2673
|
+
"const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf-8')) : {}",
|
|
2674
|
+
"const scripts = pkg.scripts ?? {}",
|
|
2675
|
+
"const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
|
|
2676
|
+
"const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
|
|
2677
|
+
"let pass = true",
|
|
2678
|
+
`const gate = (label, ok) => { console.log('[goal ${ID}] ' + label + ': ' + (ok ? '\u2713' : '\u2717')); if (!ok) pass = false }`,
|
|
2679
|
+
"const must = (cond, label) => { console.log((cond ? ' \u2713 ' : ' \u2717 ') + label); if (!cond) pass = false }",
|
|
2680
|
+
"",
|
|
2681
|
+
"// typecheck (\uC2A4\uD06C\uB9BD\uD2B8 \uC6B0\uC120, \uC5C6\uC73C\uBA74 tsc --noEmit)",
|
|
2682
|
+
"if (scripts.typecheck) gate('typecheck', run(pm, ['run', 'typecheck']))",
|
|
2683
|
+
"else if (existsSync('tsconfig.json')) gate('tsc --noEmit', run(pm, pm === 'npm' ? ['exec', '--', 'tsc', '--noEmit'] : ['exec', 'tsc', '--noEmit']))",
|
|
2684
|
+
"if (scripts.lint) gate('lint', run(pm, ['run', 'lint']))",
|
|
2685
|
+
"if (!skipDeep) {",
|
|
2686
|
+
" if (scripts['test:run']) gate('test', run(pm, ['run', 'test:run']))",
|
|
2687
|
+
" else if (scripts.test && /vitest/.test(scripts.test)) gate('test', run(pm, ['run', 'test', '--', '--run']))",
|
|
2688
|
+
" else if (scripts.test) gate('test', run(pm, ['run', 'test']))",
|
|
2689
|
+
" if (scripts.build) gate('build', run(pm, ['run', 'build']))",
|
|
2690
|
+
"}",
|
|
2691
|
+
"",
|
|
2692
|
+
`// \u2500\u2500\u2500 goal ${ID} \uACE0\uC720 \uAC80\uC99D (\uC9C1\uC811 \uCD94\uAC00) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
|
|
2693
|
+
"// const read = (p) => existsSync(p) ? readFileSync(p, 'utf-8') : null",
|
|
2694
|
+
"// must(read('src/foo.ts')?.includes('bar'), 'foo.ts \uC5D0 bar \uC874\uC7AC')",
|
|
2695
|
+
"",
|
|
2696
|
+
`if (pass) { console.log('\u2705 goal ${ID} gate passes'); process.exit(0) }`,
|
|
2697
|
+
`console.log('\u274C goal ${ID} gate failed'); process.exit(1)`,
|
|
2698
|
+
""
|
|
2699
|
+
].join("\n");
|
|
2700
|
+
}
|
|
2701
|
+
async function goalSync() {
|
|
2702
|
+
console.log(chalk7.bold(`
|
|
2703
|
+
${ko.goal.syncTitle}
|
|
2704
|
+
`));
|
|
2705
|
+
const goals = listGoals(GOALS_DIR);
|
|
2706
|
+
const result = { created: [], skipped: [] };
|
|
2707
|
+
if (goals.length === 0) {
|
|
2708
|
+
console.log(
|
|
2709
|
+
chalk7.yellow(" \u{1F4ED} goals/ \uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694.")
|
|
2710
|
+
);
|
|
2711
|
+
return result;
|
|
2712
|
+
}
|
|
2713
|
+
mkdirSync2(SCRIPTS_DIR, { recursive: true });
|
|
2714
|
+
for (const g of goals) {
|
|
2715
|
+
const id = g.frontmatter.id;
|
|
2716
|
+
if (typeof id !== "number") continue;
|
|
2717
|
+
const target = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
2718
|
+
if (existsSync3(target)) {
|
|
2719
|
+
console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${target}`));
|
|
2720
|
+
result.skipped.push(id);
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
const shOnly = existsSync3(join4(SCRIPTS_DIR, `check-goal-${id}.sh`));
|
|
2724
|
+
writeFileSync2(target, generateGateScript(id), "utf-8");
|
|
2725
|
+
console.log(
|
|
2726
|
+
chalk7.green(` \u2713 created: ${target}${shOnly ? " (.sh \u2192 .mjs \uBC31\uD544, Windows 1\uAE09)" : ""}`)
|
|
2727
|
+
);
|
|
2728
|
+
result.created.push(id);
|
|
2729
|
+
}
|
|
2730
|
+
console.log(
|
|
2731
|
+
chalk7.bold(`
|
|
2732
|
+
\u{1F4CA} created=${result.created.length} skipped=${result.skipped.length}`)
|
|
2733
|
+
);
|
|
2734
|
+
if (result.created.length > 0) {
|
|
2735
|
+
printNextStep({
|
|
2736
|
+
message: `\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 ${result.created.length}\uAC1C \uC0DD\uC131 (goal ${result.created.join(", ")}). \uAC80\uC99D\uD558\uB824\uBA74:`,
|
|
2737
|
+
command: `vhk goal check --id ${result.created[0]}`,
|
|
2738
|
+
cursorHint: `goal ${result.created[0]} \uAC8C\uC774\uD2B8 \uAC80\uC99D\uD574\uC918`
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
return result;
|
|
2742
|
+
}
|
|
2351
2743
|
|
|
2352
2744
|
// src/commands/check.ts
|
|
2353
2745
|
async function check(opts = {}) {
|
|
@@ -2357,22 +2749,22 @@ async function check(opts = {}) {
|
|
|
2357
2749
|
return checkRules();
|
|
2358
2750
|
}
|
|
2359
2751
|
async function checkRules() {
|
|
2360
|
-
console.log(
|
|
2752
|
+
console.log(chalk8.bold(`
|
|
2361
2753
|
${ko.check.title}
|
|
2362
2754
|
`));
|
|
2363
2755
|
const cwd = process.cwd();
|
|
2364
|
-
const rulesPath =
|
|
2365
|
-
if (!
|
|
2366
|
-
console.log(
|
|
2367
|
-
console.log(
|
|
2756
|
+
const rulesPath = path7.join(cwd, "RULES.md");
|
|
2757
|
+
if (!fs6.existsSync(rulesPath)) {
|
|
2758
|
+
console.log(chalk8.yellow(ko.check.noRules));
|
|
2759
|
+
console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
2368
2760
|
return;
|
|
2369
2761
|
}
|
|
2370
2762
|
const rules = parseRules(rulesPath);
|
|
2371
|
-
console.log(
|
|
2763
|
+
console.log(chalk8.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
2764
|
`));
|
|
2373
2765
|
if (rules.length === 0) {
|
|
2374
|
-
console.log(
|
|
2375
|
-
console.log(
|
|
2766
|
+
console.log(chalk8.yellow(ko.check.noAutoRules));
|
|
2767
|
+
console.log(chalk8.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
2768
|
return;
|
|
2377
2769
|
}
|
|
2378
2770
|
const allViolations = [];
|
|
@@ -2380,13 +2772,14 @@ ${ko.check.title}
|
|
|
2380
2772
|
for (const rule of rules) {
|
|
2381
2773
|
const violations = rule.check(cwd);
|
|
2382
2774
|
if (violations.length === 0) {
|
|
2383
|
-
|
|
2775
|
+
const patternHint = rule.type === "content" && rule.pattern ? chalk8.dim(` [\uAC80\uC0AC: ${rule.pattern.source}]`) : "";
|
|
2776
|
+
console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`) + patternHint);
|
|
2384
2777
|
passCount++;
|
|
2385
2778
|
} else {
|
|
2386
|
-
console.log(
|
|
2779
|
+
console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
2387
2780
|
violations.forEach((v) => {
|
|
2388
|
-
const loc = v.file ?
|
|
2389
|
-
const icon = v.severity === "error" ?
|
|
2781
|
+
const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2782
|
+
const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
|
|
2390
2783
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
2391
2784
|
});
|
|
2392
2785
|
allViolations.push(...violations);
|
|
@@ -2396,17 +2789,18 @@ ${ko.check.title}
|
|
|
2396
2789
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
2397
2790
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
2398
2791
|
if (allViolations.length === 0) {
|
|
2399
|
-
console.log(
|
|
2792
|
+
console.log(chalk8.green.bold(`\u2705 \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${passCount}\uAC1C \uD1B5\uACFC`));
|
|
2793
|
+
console.log(chalk8.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
2794
|
printNextStep({
|
|
2401
2795
|
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
2402
2796
|
command: "vhk \uBCF4\uC548 scan",
|
|
2403
2797
|
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
2404
2798
|
});
|
|
2405
2799
|
} else {
|
|
2406
|
-
console.log(
|
|
2407
|
-
console.log(` \uADDC\uCE59: ${
|
|
2408
|
-
if (errors > 0) console.log(` ${
|
|
2409
|
-
if (warnings > 0) console.log(` ${
|
|
2800
|
+
console.log(chalk8.bold(ko.check.summary));
|
|
2801
|
+
console.log(` \uADDC\uCE59: ${chalk8.cyan(String(rules.length))}\uAC1C | \uD1B5\uACFC: ${chalk8.green(String(passCount))}\uAC1C | \uC704\uBC18: ${chalk8.red(String(allViolations.length))}\uAC74`);
|
|
2802
|
+
if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
2803
|
+
if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
2410
2804
|
printNextStep({
|
|
2411
2805
|
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
2412
2806
|
command: "vhk \uC810\uAC80",
|
|
@@ -2419,36 +2813,36 @@ ${ko.check.title}
|
|
|
2419
2813
|
}
|
|
2420
2814
|
|
|
2421
2815
|
// src/commands/secure.ts
|
|
2422
|
-
import
|
|
2423
|
-
import
|
|
2424
|
-
import
|
|
2816
|
+
import chalk9 from "chalk";
|
|
2817
|
+
import fs7 from "fs";
|
|
2818
|
+
import path8 from "path";
|
|
2425
2819
|
async function secure() {
|
|
2426
|
-
console.log(
|
|
2820
|
+
console.log(chalk9.bold(`
|
|
2427
2821
|
${ko.secure.title}
|
|
2428
2822
|
`));
|
|
2429
2823
|
const cwd = process.cwd();
|
|
2430
|
-
const gitignorePath =
|
|
2431
|
-
const hasGitignore =
|
|
2824
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2825
|
+
const hasGitignore = fs7.existsSync(gitignorePath);
|
|
2432
2826
|
if (!hasGitignore) {
|
|
2433
|
-
console.log(
|
|
2434
|
-
console.log(
|
|
2827
|
+
console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
|
|
2828
|
+
console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
2435
2829
|
} else {
|
|
2436
|
-
const gitignoreContent =
|
|
2830
|
+
const gitignoreContent = fs7.readFileSync(gitignorePath, "utf-8");
|
|
2437
2831
|
if (!gitignoreContent.includes(".env")) {
|
|
2438
|
-
console.log(
|
|
2439
|
-
console.log(
|
|
2832
|
+
console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2833
|
+
console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
2440
2834
|
}
|
|
2441
2835
|
}
|
|
2442
|
-
console.log(
|
|
2836
|
+
console.log(chalk9.dim(` ${ko.secure.scanning}
|
|
2443
2837
|
`));
|
|
2444
2838
|
const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
|
|
2445
|
-
console.log(
|
|
2839
|
+
console.log(chalk9.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2446
2840
|
if (truncated) {
|
|
2447
|
-
console.log(
|
|
2841
|
+
console.log(chalk9.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
2842
|
}
|
|
2449
2843
|
console.log("");
|
|
2450
2844
|
if (findings.length === 0) {
|
|
2451
|
-
console.log(
|
|
2845
|
+
console.log(chalk9.green.bold(` ${ko.secure.clean}`));
|
|
2452
2846
|
printNextStep({
|
|
2453
2847
|
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
2454
2848
|
command: "vhk \uC815\uB9AC",
|
|
@@ -2460,161 +2854,46 @@ ${ko.secure.title}
|
|
|
2460
2854
|
const high = findings.filter((f) => f.severity === "high");
|
|
2461
2855
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
2462
2856
|
if (critical.length > 0) {
|
|
2463
|
-
console.log(
|
|
2857
|
+
console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
2464
2858
|
critical.forEach((f) => {
|
|
2465
|
-
console.log(
|
|
2466
|
-
console.log(
|
|
2859
|
+
console.log(chalk9.red(` \u2716 ${f.patternName}`));
|
|
2860
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2467
2861
|
});
|
|
2468
2862
|
console.log("");
|
|
2469
2863
|
}
|
|
2470
2864
|
if (high.length > 0) {
|
|
2471
|
-
console.log(
|
|
2865
|
+
console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
2472
2866
|
high.forEach((f) => {
|
|
2473
|
-
console.log(
|
|
2474
|
-
console.log(
|
|
2867
|
+
console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
|
|
2868
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2475
2869
|
});
|
|
2476
2870
|
console.log("");
|
|
2477
2871
|
}
|
|
2478
2872
|
if (medium.length > 0) {
|
|
2479
|
-
console.log(
|
|
2873
|
+
console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
2480
2874
|
medium.forEach((f) => {
|
|
2481
|
-
console.log(
|
|
2482
|
-
console.log(
|
|
2875
|
+
console.log(chalk9.blue(` \u2139 ${f.patternName}`));
|
|
2876
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2483
2877
|
});
|
|
2484
2878
|
console.log("");
|
|
2485
2879
|
}
|
|
2486
|
-
console.log(
|
|
2487
|
-
console.log(` \uCD1D ${
|
|
2880
|
+
console.log(chalk9.bold(` ${ko.secure.summary}`));
|
|
2881
|
+
console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
2488
2882
|
console.log("");
|
|
2489
|
-
console.log(
|
|
2490
|
-
console.log(
|
|
2491
|
-
console.log(
|
|
2492
|
-
console.log(
|
|
2883
|
+
console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
2884
|
+
console.log(chalk9.dim(" 1. \uD574\uB2F9 \uD30C\uC77C\uC5D0\uC11C \uC2DC\uD06C\uB9BF\uC744 \uC81C\uAC70\uD558\uACE0 \uD658\uACBD\uBCC0\uC218\uB85C \uC774\uB3D9"));
|
|
2885
|
+
console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
2886
|
+
console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
2493
2887
|
if (critical.length > 0 || high.length > 0) {
|
|
2494
2888
|
process.exitCode = 1;
|
|
2495
2889
|
}
|
|
2496
2890
|
}
|
|
2497
2891
|
|
|
2498
2892
|
// src/commands/doctor.ts
|
|
2499
|
-
import
|
|
2500
|
-
import
|
|
2501
|
-
import
|
|
2893
|
+
import chalk10 from "chalk";
|
|
2894
|
+
import fs8 from "fs";
|
|
2895
|
+
import path9 from "path";
|
|
2502
2896
|
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
2897
|
function checkCommand(name, command, hint) {
|
|
2619
2898
|
const result = safeExecFile(command, ["--version"]);
|
|
2620
2899
|
if (!result.ok) return { name, command, ok: false, hint };
|
|
@@ -2622,14 +2901,14 @@ function checkCommand(name, command, hint) {
|
|
|
2622
2901
|
return { name, command, version, ok: true, hint };
|
|
2623
2902
|
}
|
|
2624
2903
|
function getVhkVersion2() {
|
|
2625
|
-
const dir =
|
|
2904
|
+
const dir = path9.dirname(fileURLToPath(import.meta.url));
|
|
2626
2905
|
const candidates = [
|
|
2627
|
-
|
|
2628
|
-
|
|
2906
|
+
path9.join(dir, "../package.json"),
|
|
2907
|
+
path9.join(dir, "../../package.json")
|
|
2629
2908
|
];
|
|
2630
2909
|
for (const pkgPath of candidates) {
|
|
2631
2910
|
try {
|
|
2632
|
-
if (
|
|
2911
|
+
if (fs8.existsSync(pkgPath)) {
|
|
2633
2912
|
const pkg = readJsonFile(pkgPath);
|
|
2634
2913
|
return pkg.version;
|
|
2635
2914
|
}
|
|
@@ -2657,7 +2936,7 @@ function compareSemver(a, b) {
|
|
|
2657
2936
|
return a3 - b3;
|
|
2658
2937
|
}
|
|
2659
2938
|
async function doctor() {
|
|
2660
|
-
console.log(
|
|
2939
|
+
console.log(chalk10.bold(`
|
|
2661
2940
|
${ko.doctor.title}
|
|
2662
2941
|
`));
|
|
2663
2942
|
const checks = [
|
|
@@ -2669,30 +2948,30 @@ ${ko.doctor.title}
|
|
|
2669
2948
|
let allOk = true;
|
|
2670
2949
|
for (const check2 of checks) {
|
|
2671
2950
|
if (check2.ok) {
|
|
2672
|
-
console.log(
|
|
2951
|
+
console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
|
|
2673
2952
|
} else {
|
|
2674
|
-
console.log(
|
|
2675
|
-
console.log(
|
|
2953
|
+
console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
2954
|
+
console.log(chalk10.dim(` \u2192 ${check2.hint}`));
|
|
2676
2955
|
allOk = false;
|
|
2677
2956
|
}
|
|
2678
2957
|
}
|
|
2679
2958
|
console.log("");
|
|
2680
2959
|
const vhkVersion = getVhkVersion2();
|
|
2681
2960
|
if (vhkVersion) {
|
|
2682
|
-
console.log(
|
|
2961
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
|
|
2683
2962
|
} else {
|
|
2684
|
-
console.log(
|
|
2963
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2685
2964
|
}
|
|
2686
2965
|
if (vhkVersion) {
|
|
2687
2966
|
const latest = fetchLatestNpmVersion("@byh3071/vhk");
|
|
2688
2967
|
if (latest && compareSemver(latest, vhkVersion) > 0) {
|
|
2689
|
-
console.log(
|
|
2968
|
+
console.log(chalk10.yellow(` ${ko.doctor.updateAvailable(latest)}`));
|
|
2690
2969
|
} else if (latest) {
|
|
2691
|
-
console.log(
|
|
2970
|
+
console.log(chalk10.dim(` ${ko.doctor.updateCurrent}`));
|
|
2692
2971
|
}
|
|
2693
2972
|
}
|
|
2694
2973
|
console.log("");
|
|
2695
|
-
console.log(
|
|
2974
|
+
console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
|
|
2696
2975
|
const cwd = process.cwd();
|
|
2697
2976
|
const projectFiles = [
|
|
2698
2977
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -2702,49 +2981,51 @@ ${ko.doctor.title}
|
|
|
2702
2981
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2703
2982
|
];
|
|
2704
2983
|
for (const file of projectFiles) {
|
|
2705
|
-
const exists =
|
|
2984
|
+
const exists = fs8.existsSync(path9.join(cwd, file.name));
|
|
2706
2985
|
if (exists) {
|
|
2707
|
-
console.log(
|
|
2986
|
+
console.log(chalk10.green(` \u2705 ${file.name}`));
|
|
2708
2987
|
if (file.name === ".env") {
|
|
2709
|
-
const gitignorePath =
|
|
2710
|
-
if (
|
|
2711
|
-
const gitignore =
|
|
2988
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
2989
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
2990
|
+
const gitignore = fs8.readFileSync(gitignorePath, "utf-8");
|
|
2712
2991
|
if (!gitignore.includes(".env")) {
|
|
2713
|
-
console.log(
|
|
2992
|
+
console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2714
2993
|
}
|
|
2715
2994
|
}
|
|
2716
2995
|
}
|
|
2996
|
+
} else if (file.name === ".env" && fs8.existsSync(path9.join(cwd, ".env.local"))) {
|
|
2997
|
+
console.log(chalk10.green(" \u2705 .env.local") + chalk10.dim(" \u2014 \uB85C\uCEEC env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
|
|
2717
2998
|
} else {
|
|
2718
|
-
console.log(
|
|
2999
|
+
console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
|
|
2719
3000
|
}
|
|
2720
3001
|
}
|
|
2721
3002
|
console.log("");
|
|
2722
|
-
console.log(
|
|
3003
|
+
console.log(chalk10.bold(` ${ko.doctor.driftTitle}`));
|
|
2723
3004
|
const ruleDrift = checkRuleDrift(cwd);
|
|
2724
3005
|
if (!ruleDrift.checked) {
|
|
2725
|
-
console.log(
|
|
3006
|
+
console.log(chalk10.dim(` ${ko.doctor.driftNoRules}`));
|
|
2726
3007
|
} else {
|
|
2727
3008
|
const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
|
|
2728
3009
|
if (drifted.length === 0) {
|
|
2729
|
-
console.log(
|
|
3010
|
+
console.log(chalk10.green(` ${ko.doctor.driftRuleClean}`));
|
|
2730
3011
|
} else {
|
|
2731
|
-
console.log(
|
|
3012
|
+
console.log(chalk10.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
|
|
2732
3013
|
}
|
|
2733
3014
|
}
|
|
2734
3015
|
const ctxDrift = checkContextDrift(cwd);
|
|
2735
3016
|
if (ctxDrift.checked && ctxDrift.stale) {
|
|
2736
|
-
console.log(
|
|
3017
|
+
console.log(chalk10.yellow(` ${ko.doctor.driftContextWarn}`));
|
|
2737
3018
|
}
|
|
2738
3019
|
console.log("");
|
|
2739
3020
|
if (allOk) {
|
|
2740
|
-
console.log(
|
|
3021
|
+
console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
|
|
2741
3022
|
printNextStep({
|
|
2742
3023
|
message: ko.doctor.nextOkMessage,
|
|
2743
3024
|
command: "vhk \uC2DC\uC791",
|
|
2744
3025
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2745
3026
|
});
|
|
2746
3027
|
} else {
|
|
2747
|
-
console.log(
|
|
3028
|
+
console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2748
3029
|
printNextStep({
|
|
2749
3030
|
message: ko.doctor.nextRetryMessage,
|
|
2750
3031
|
command: "vhk doctor",
|
|
@@ -2755,10 +3036,10 @@ ${ko.doctor.title}
|
|
|
2755
3036
|
}
|
|
2756
3037
|
|
|
2757
3038
|
// src/commands/ship.ts
|
|
2758
|
-
import
|
|
3039
|
+
import chalk11 from "chalk";
|
|
2759
3040
|
import inquirer4 from "inquirer";
|
|
2760
|
-
import
|
|
2761
|
-
import
|
|
3041
|
+
import fs9 from "fs";
|
|
3042
|
+
import path10 from "path";
|
|
2762
3043
|
var CHECKLIST = [
|
|
2763
3044
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
2764
3045
|
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
@@ -2771,9 +3052,9 @@ function sanitizeVersion(version) {
|
|
|
2771
3052
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2772
3053
|
}
|
|
2773
3054
|
function updateChangelogUnreleased(cwd, version, date) {
|
|
2774
|
-
const changelogPath =
|
|
2775
|
-
if (!
|
|
2776
|
-
const content =
|
|
3055
|
+
const changelogPath = path10.join(cwd, "CHANGELOG.md");
|
|
3056
|
+
if (!fs9.existsSync(changelogPath)) return { status: "missing" };
|
|
3057
|
+
const content = fs9.readFileSync(changelogPath, "utf-8");
|
|
2777
3058
|
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
|
2778
3059
|
if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
|
|
2779
3060
|
const blankUnreleased = [
|
|
@@ -2790,33 +3071,34 @@ function updateChangelogUnreleased(cwd, version, date) {
|
|
|
2790
3071
|
`## [${version}] \u2014 ${date}`
|
|
2791
3072
|
].join("\n");
|
|
2792
3073
|
const updated = content.replace(unreleasedHeading, blankUnreleased);
|
|
2793
|
-
|
|
3074
|
+
fs9.writeFileSync(changelogPath, updated, "utf-8");
|
|
2794
3075
|
return { status: "updated", version };
|
|
2795
3076
|
}
|
|
2796
3077
|
async function ship() {
|
|
2797
|
-
|
|
3078
|
+
if (!ensureNotHardStopped("ship")) return;
|
|
3079
|
+
console.log(chalk11.bold(`
|
|
2798
3080
|
${ko.ship.title}
|
|
2799
3081
|
`));
|
|
2800
3082
|
const cwd = process.cwd();
|
|
2801
|
-
console.log(
|
|
3083
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
|
|
2802
3084
|
`));
|
|
2803
3085
|
const { passed } = await inquirer4.prompt([{
|
|
2804
3086
|
type: "checkbox",
|
|
2805
3087
|
name: "passed",
|
|
2806
3088
|
message: ko.ship.checkboxPrompt,
|
|
2807
3089
|
choices: CHECKLIST.map((c) => ({
|
|
2808
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
3090
|
+
name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2809
3091
|
value: c.id
|
|
2810
3092
|
}))
|
|
2811
3093
|
}]);
|
|
2812
3094
|
const allPassed = passed.length === CHECKLIST.length;
|
|
2813
3095
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2814
3096
|
if (!allPassed) {
|
|
2815
|
-
console.log(
|
|
3097
|
+
console.log(chalk11.yellow(`
|
|
2816
3098
|
${ko.ship.incompleteHeader}`));
|
|
2817
3099
|
skipped.forEach((s) => {
|
|
2818
|
-
console.log(
|
|
2819
|
-
console.log(
|
|
3100
|
+
console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
3101
|
+
console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2820
3102
|
});
|
|
2821
3103
|
const { proceed } = await inquirer4.prompt([{
|
|
2822
3104
|
type: "confirm",
|
|
@@ -2833,13 +3115,13 @@ ${ko.ship.title}
|
|
|
2833
3115
|
return;
|
|
2834
3116
|
}
|
|
2835
3117
|
} else {
|
|
2836
|
-
console.log(
|
|
3118
|
+
console.log(chalk11.green(`
|
|
2837
3119
|
${ko.ship.allPassed}
|
|
2838
3120
|
`));
|
|
2839
3121
|
}
|
|
2840
|
-
console.log(
|
|
3122
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.retro}
|
|
2841
3123
|
`));
|
|
2842
|
-
console.log(
|
|
3124
|
+
console.log(chalk11.dim(` ${ko.ship.versionHint}`));
|
|
2843
3125
|
const retro = await inquirer4.prompt([
|
|
2844
3126
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2845
3127
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
@@ -2847,12 +3129,12 @@ ${ko.ship.title}
|
|
|
2847
3129
|
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
2848
3130
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2849
3131
|
]);
|
|
2850
|
-
const buildLogDir =
|
|
2851
|
-
if (!
|
|
2852
|
-
const today = (
|
|
3132
|
+
const buildLogDir = path10.join(cwd, "docs", "build-log");
|
|
3133
|
+
if (!fs9.existsSync(buildLogDir)) fs9.mkdirSync(buildLogDir, { recursive: true });
|
|
3134
|
+
const today = localDate();
|
|
2853
3135
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2854
3136
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
2855
|
-
const filePath =
|
|
3137
|
+
const filePath = path10.join(buildLogDir, fileName);
|
|
2856
3138
|
const content = [
|
|
2857
3139
|
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
2858
3140
|
"",
|
|
@@ -2881,9 +3163,9 @@ ${ko.ship.title}
|
|
|
2881
3163
|
"---",
|
|
2882
3164
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2883
3165
|
].join("\n");
|
|
2884
|
-
|
|
2885
|
-
console.log(
|
|
2886
|
-
${ko.ship.buildLogDone(
|
|
3166
|
+
fs9.writeFileSync(filePath, content, "utf-8");
|
|
3167
|
+
console.log(chalk11.green(`
|
|
3168
|
+
${ko.ship.buildLogDone(path10.relative(cwd, filePath))}`));
|
|
2887
3169
|
const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
|
|
2888
3170
|
if (changelogResult.status === "updated") {
|
|
2889
3171
|
log.success(ko.ship.changelogUpdated(changelogResult.version));
|
|
@@ -2901,8 +3183,8 @@ ${ko.ship.title}
|
|
|
2901
3183
|
}
|
|
2902
3184
|
|
|
2903
3185
|
// src/commands/save.ts
|
|
2904
|
-
import { execFileSync
|
|
2905
|
-
import
|
|
3186
|
+
import { execFileSync } from "child_process";
|
|
3187
|
+
import chalk12 from "chalk";
|
|
2906
3188
|
import ora from "ora";
|
|
2907
3189
|
import inquirer5 from "inquirer";
|
|
2908
3190
|
|
|
@@ -2930,29 +3212,29 @@ function statusIcon(code) {
|
|
|
2930
3212
|
return "\u{1F4C4}";
|
|
2931
3213
|
}
|
|
2932
3214
|
async function save() {
|
|
2933
|
-
console.log(
|
|
3215
|
+
console.log(chalk12.bold(`
|
|
2934
3216
|
\u{1F4BE} ${t("save.title")}`));
|
|
2935
|
-
console.log(
|
|
3217
|
+
console.log(chalk12.gray("\u2500".repeat(40)));
|
|
2936
3218
|
let gitRoot;
|
|
2937
3219
|
try {
|
|
2938
|
-
|
|
3220
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
2939
3221
|
gitRoot = getGitRoot();
|
|
2940
3222
|
} catch {
|
|
2941
|
-
console.log(
|
|
3223
|
+
console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
|
|
2942
3224
|
return;
|
|
2943
3225
|
}
|
|
2944
|
-
console.log(
|
|
3226
|
+
console.log(chalk12.cyan(`
|
|
2945
3227
|
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
2946
3228
|
printSecurityWarnings(gitRoot);
|
|
2947
3229
|
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
2948
3230
|
if (severe.length > 0) {
|
|
2949
|
-
console.log(
|
|
3231
|
+
console.log(chalk12.red(`
|
|
2950
3232
|
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
2951
3233
|
severe.slice(0, 5).forEach((f) => {
|
|
2952
|
-
console.log(
|
|
3234
|
+
console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
2953
3235
|
});
|
|
2954
3236
|
if (severe.length > 5) {
|
|
2955
|
-
console.log(
|
|
3237
|
+
console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
2956
3238
|
}
|
|
2957
3239
|
const { proceed } = await inquirer5.prompt([{
|
|
2958
3240
|
type: "confirm",
|
|
@@ -2961,16 +3243,16 @@ async function save() {
|
|
|
2961
3243
|
default: false
|
|
2962
3244
|
}]);
|
|
2963
3245
|
if (!proceed) {
|
|
2964
|
-
console.log(
|
|
3246
|
+
console.log(chalk12.gray(t("save.cancelled")));
|
|
2965
3247
|
return;
|
|
2966
3248
|
}
|
|
2967
3249
|
}
|
|
2968
3250
|
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
2969
3251
|
if (lines.length === 0) {
|
|
2970
|
-
console.log(
|
|
3252
|
+
console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
2971
3253
|
return;
|
|
2972
3254
|
}
|
|
2973
|
-
console.log(
|
|
3255
|
+
console.log(chalk12.cyan(`
|
|
2974
3256
|
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
2975
3257
|
lines.forEach((line) => {
|
|
2976
3258
|
const code = line.substring(0, 2);
|
|
@@ -2992,21 +3274,21 @@ async function save() {
|
|
|
2992
3274
|
spinner.text = t("save.pushing");
|
|
2993
3275
|
if (!hasGitRemote(gitRoot)) {
|
|
2994
3276
|
spinner.succeed(t("save.successLocal"));
|
|
2995
|
-
console.log(
|
|
3277
|
+
console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
2996
3278
|
} else {
|
|
2997
3279
|
try {
|
|
2998
3280
|
gitRun(["push"], gitRoot);
|
|
2999
3281
|
spinner.succeed(t("save.successWithPush"));
|
|
3000
3282
|
} catch (pushErr) {
|
|
3001
3283
|
spinner.fail(t("save.pushFailed"));
|
|
3002
|
-
console.log(
|
|
3003
|
-
console.log(
|
|
3284
|
+
console.log(chalk12.red(getExecErrorMessage(pushErr)));
|
|
3285
|
+
console.log(chalk12.yellow(`
|
|
3004
3286
|
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
3005
3287
|
process.exitCode = 1;
|
|
3006
3288
|
}
|
|
3007
3289
|
}
|
|
3008
3290
|
if (process.exitCode !== 1) {
|
|
3009
|
-
console.log(
|
|
3291
|
+
console.log(chalk12.green(`
|
|
3010
3292
|
\u2705 ${t("save.done", lines.length)}`));
|
|
3011
3293
|
printNextStep({
|
|
3012
3294
|
message: t("save.nextOkMessage"),
|
|
@@ -3014,7 +3296,7 @@ async function save() {
|
|
|
3014
3296
|
cursorHint: t("save.nextOkCursor")
|
|
3015
3297
|
});
|
|
3016
3298
|
} else {
|
|
3017
|
-
console.log(
|
|
3299
|
+
console.log(chalk12.green(`
|
|
3018
3300
|
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
3019
3301
|
printNextStep({
|
|
3020
3302
|
message: t("save.nextPushFailMessage"),
|
|
@@ -3024,12 +3306,12 @@ async function save() {
|
|
|
3024
3306
|
}
|
|
3025
3307
|
} catch (err) {
|
|
3026
3308
|
spinner.fail(t("save.failed"));
|
|
3027
|
-
console.log(
|
|
3309
|
+
console.log(chalk12.red(getExecErrorMessage(err)));
|
|
3028
3310
|
if (didAdd) {
|
|
3029
3311
|
try {
|
|
3030
3312
|
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3031
3313
|
if (staged) {
|
|
3032
|
-
console.log(
|
|
3314
|
+
console.log(chalk12.yellow(`
|
|
3033
3315
|
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3034
3316
|
}
|
|
3035
3317
|
} catch {
|
|
@@ -3040,8 +3322,8 @@ async function save() {
|
|
|
3040
3322
|
}
|
|
3041
3323
|
|
|
3042
3324
|
// src/commands/undo.ts
|
|
3043
|
-
import { execFileSync as
|
|
3044
|
-
import
|
|
3325
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3326
|
+
import chalk13 from "chalk";
|
|
3045
3327
|
import inquirer6 from "inquirer";
|
|
3046
3328
|
function parseRecentCommits(logOutput) {
|
|
3047
3329
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
@@ -3065,30 +3347,30 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
|
3065
3347
|
return false;
|
|
3066
3348
|
}
|
|
3067
3349
|
async function undo() {
|
|
3068
|
-
console.log(
|
|
3350
|
+
console.log(chalk13.bold(`
|
|
3069
3351
|
\u23EA ${t("undo.title")}`));
|
|
3070
|
-
console.log(
|
|
3352
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
3071
3353
|
let gitRoot;
|
|
3072
3354
|
try {
|
|
3073
|
-
|
|
3355
|
+
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3074
3356
|
gitRoot = getGitRoot();
|
|
3075
3357
|
} catch {
|
|
3076
|
-
console.log(
|
|
3358
|
+
console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
3077
3359
|
return;
|
|
3078
3360
|
}
|
|
3079
3361
|
let logOutput;
|
|
3080
3362
|
try {
|
|
3081
3363
|
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
3082
3364
|
} catch {
|
|
3083
|
-
console.log(
|
|
3365
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3084
3366
|
return;
|
|
3085
3367
|
}
|
|
3086
3368
|
const commits = parseRecentCommits(logOutput);
|
|
3087
3369
|
if (commits.length === 0) {
|
|
3088
|
-
console.log(
|
|
3370
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3089
3371
|
return;
|
|
3090
3372
|
}
|
|
3091
|
-
console.log(
|
|
3373
|
+
console.log(chalk13.cyan(`
|
|
3092
3374
|
${t("undo.recentHeader")}`));
|
|
3093
3375
|
commits.forEach((c, i) => {
|
|
3094
3376
|
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
@@ -3105,7 +3387,7 @@ ${t("undo.recentHeader")}`));
|
|
|
3105
3387
|
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
3106
3388
|
const headCount = countLocalCommits(gitRoot);
|
|
3107
3389
|
if (undoCount >= headCount) {
|
|
3108
|
-
console.log(
|
|
3390
|
+
console.log(chalk13.yellow(`
|
|
3109
3391
|
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3110
3392
|
return;
|
|
3111
3393
|
}
|
|
@@ -3114,10 +3396,10 @@ ${t("undo.recentHeader")}`));
|
|
|
3114
3396
|
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3115
3397
|
if (risky) {
|
|
3116
3398
|
if (unpushed < 0) {
|
|
3117
|
-
console.log(
|
|
3399
|
+
console.log(chalk13.red(`
|
|
3118
3400
|
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3119
3401
|
} else {
|
|
3120
|
-
console.log(
|
|
3402
|
+
console.log(chalk13.red(`
|
|
3121
3403
|
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3122
3404
|
}
|
|
3123
3405
|
}
|
|
@@ -3128,16 +3410,16 @@ ${t("undo.recentHeader")}`));
|
|
|
3128
3410
|
default: false
|
|
3129
3411
|
}]);
|
|
3130
3412
|
if (!confirm) {
|
|
3131
|
-
console.log(
|
|
3413
|
+
console.log(chalk13.gray(t("undo.cancelled")));
|
|
3132
3414
|
return;
|
|
3133
3415
|
}
|
|
3134
3416
|
try {
|
|
3135
3417
|
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3136
|
-
console.log(
|
|
3418
|
+
console.log(chalk13.green(`
|
|
3137
3419
|
\u2705 ${t("undo.success")}`));
|
|
3138
|
-
console.log(
|
|
3420
|
+
console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3139
3421
|
if (risky) {
|
|
3140
|
-
console.log(
|
|
3422
|
+
console.log(chalk13.yellow(`
|
|
3141
3423
|
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3142
3424
|
}
|
|
3143
3425
|
printNextStep({
|
|
@@ -3146,18 +3428,73 @@ ${t("undo.recentHeader")}`));
|
|
|
3146
3428
|
cursorHint: t("undo.nextCursor")
|
|
3147
3429
|
});
|
|
3148
3430
|
} catch (err) {
|
|
3149
|
-
console.log(
|
|
3431
|
+
console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
|
|
3150
3432
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3151
|
-
console.log(
|
|
3433
|
+
console.log(chalk13.red(msg));
|
|
3434
|
+
process.exitCode = 1;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// src/commands/restore.ts
|
|
3439
|
+
import chalk14 from "chalk";
|
|
3440
|
+
import inquirer7 from "inquirer";
|
|
3441
|
+
async function restore(id) {
|
|
3442
|
+
console.log(chalk14.bold(`
|
|
3443
|
+
${ko.restore.title}`));
|
|
3444
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3445
|
+
console.log(chalk14.dim(` ${ko.restore.notGitNote}`));
|
|
3446
|
+
const cwd = process.cwd();
|
|
3447
|
+
const backups = listBackups(cwd);
|
|
3448
|
+
if (backups.length === 0) {
|
|
3449
|
+
console.log(chalk14.yellow(`
|
|
3450
|
+
${ko.restore.noBackups}`));
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
let targetId = id;
|
|
3454
|
+
if (!targetId) {
|
|
3455
|
+
if (!process.stdout.isTTY) {
|
|
3456
|
+
console.log(chalk14.cyan(`
|
|
3457
|
+
${ko.restore.listHeader}`));
|
|
3458
|
+
for (const b of backups) console.log(` ${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`);
|
|
3459
|
+
console.log(chalk14.yellow(`
|
|
3460
|
+
${ko.restore.nonTtyHint}`));
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
const { picked } = await inquirer7.prompt([
|
|
3464
|
+
{
|
|
3465
|
+
type: "list",
|
|
3466
|
+
name: "picked",
|
|
3467
|
+
message: ko.restore.selectPrompt,
|
|
3468
|
+
choices: backups.map((b) => ({
|
|
3469
|
+
name: `${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`,
|
|
3470
|
+
value: b.id
|
|
3471
|
+
}))
|
|
3472
|
+
}
|
|
3473
|
+
]);
|
|
3474
|
+
targetId = picked;
|
|
3475
|
+
}
|
|
3476
|
+
try {
|
|
3477
|
+
const restored = restoreBackup(targetId, cwd);
|
|
3478
|
+
console.log(chalk14.green(`
|
|
3479
|
+
${ko.restore.restored(restored.length, targetId)}`));
|
|
3480
|
+
for (const r of restored) console.log(chalk14.gray(` ${r}`));
|
|
3481
|
+
printNextStep({
|
|
3482
|
+
message: "\uBC31\uC5C5 \uBCF5\uC6D0 \uC644\uB8CC! \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
3483
|
+
command: "vhk diff",
|
|
3484
|
+
cursorHint: "\uBCC0\uACBD\uC0AC\uD56D \uBCF4\uC5EC\uC918"
|
|
3485
|
+
});
|
|
3486
|
+
} catch {
|
|
3487
|
+
console.log(chalk14.red(`
|
|
3488
|
+
${ko.restore.notFound(targetId)}`));
|
|
3152
3489
|
process.exitCode = 1;
|
|
3153
3490
|
}
|
|
3154
3491
|
}
|
|
3155
3492
|
|
|
3156
3493
|
// src/commands/status.ts
|
|
3157
|
-
import { execFileSync as
|
|
3158
|
-
import
|
|
3159
|
-
import
|
|
3160
|
-
import
|
|
3494
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3495
|
+
import fs10 from "fs";
|
|
3496
|
+
import path11 from "path";
|
|
3497
|
+
import chalk15 from "chalk";
|
|
3161
3498
|
function countFileChanges(porcelain) {
|
|
3162
3499
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
3163
3500
|
let staged = 0;
|
|
@@ -3195,8 +3532,8 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3195
3532
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3196
3533
|
}
|
|
3197
3534
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3198
|
-
const pkgPath =
|
|
3199
|
-
if (!
|
|
3535
|
+
const pkgPath = path11.join(cwd, "package.json");
|
|
3536
|
+
if (!fs10.existsSync(pkgPath)) return null;
|
|
3200
3537
|
try {
|
|
3201
3538
|
const pkg = readJsonFile(pkgPath);
|
|
3202
3539
|
if (!pkg.name && !pkg.version) return null;
|
|
@@ -3208,6 +3545,21 @@ function readProjectPackage(cwd = process.cwd()) {
|
|
|
3208
3545
|
return null;
|
|
3209
3546
|
}
|
|
3210
3547
|
}
|
|
3548
|
+
function selectStatusNextStep(hasChanges) {
|
|
3549
|
+
if (hasChanges) {
|
|
3550
|
+
return {
|
|
3551
|
+
message: t("status.nextWithChangesMessage"),
|
|
3552
|
+
command: "vhk diff",
|
|
3553
|
+
cursorHint: t("status.nextWithChangesCursor"),
|
|
3554
|
+
alternative: t("status.nextWithChangesAlt")
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
return {
|
|
3558
|
+
message: t("status.nextCleanMessage"),
|
|
3559
|
+
command: "vhk goal next",
|
|
3560
|
+
cursorHint: t("status.nextCleanCursor")
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3211
3563
|
function getSyncCounts(gitRoot) {
|
|
3212
3564
|
try {
|
|
3213
3565
|
const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
|
|
@@ -3217,15 +3569,15 @@ function getSyncCounts(gitRoot) {
|
|
|
3217
3569
|
}
|
|
3218
3570
|
}
|
|
3219
3571
|
async function status() {
|
|
3220
|
-
console.log(
|
|
3572
|
+
console.log(chalk15.bold(`
|
|
3221
3573
|
\u{1F4CA} ${t("status.title")}`));
|
|
3222
|
-
console.log(
|
|
3574
|
+
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3223
3575
|
let gitRoot;
|
|
3224
3576
|
try {
|
|
3225
|
-
|
|
3577
|
+
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3226
3578
|
gitRoot = getGitRoot();
|
|
3227
3579
|
} catch {
|
|
3228
|
-
console.log(
|
|
3580
|
+
console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3229
3581
|
return;
|
|
3230
3582
|
}
|
|
3231
3583
|
let branch;
|
|
@@ -3244,48 +3596,37 @@ async function status() {
|
|
|
3244
3596
|
commits = [];
|
|
3245
3597
|
}
|
|
3246
3598
|
const pkg = readProjectPackage();
|
|
3247
|
-
console.log(
|
|
3248
|
-
\u{1F33F} ${t("status.branch")}`) +
|
|
3599
|
+
console.log(chalk15.cyan(`
|
|
3600
|
+
\u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
|
|
3249
3601
|
console.log(
|
|
3250
|
-
|
|
3602
|
+
chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
|
|
3251
3603
|
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3252
3604
|
)
|
|
3253
3605
|
);
|
|
3254
|
-
console.log(
|
|
3255
|
-
\u{1F4CB} ${t("status.recentCommits")}`));
|
|
3606
|
+
console.log(chalk15.cyan(`
|
|
3607
|
+
\u{1F4CB} ${t("status.recentCommits", commits.length)}`));
|
|
3256
3608
|
if (commits.length === 0) {
|
|
3257
|
-
console.log(
|
|
3609
|
+
console.log(chalk15.dim(` ${t("status.noCommits")}`));
|
|
3258
3610
|
} else {
|
|
3259
|
-
commits.forEach((c) => console.log(` ${
|
|
3611
|
+
commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
|
|
3260
3612
|
}
|
|
3261
3613
|
console.log(
|
|
3262
|
-
|
|
3263
|
-
\u{1F504} ${t("status.remote")}`) +
|
|
3614
|
+
chalk15.cyan(`
|
|
3615
|
+
\u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
|
|
3264
3616
|
);
|
|
3265
|
-
console.log(
|
|
3617
|
+
console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
|
|
3266
3618
|
if (pkg) {
|
|
3267
|
-
console.log(
|
|
3619
|
+
console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
|
|
3268
3620
|
} else {
|
|
3269
|
-
console.log(
|
|
3621
|
+
console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3270
3622
|
}
|
|
3271
3623
|
const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
|
|
3272
|
-
|
|
3273
|
-
|
|
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
|
-
}
|
|
3624
|
+
printNextStep(selectStatusNextStep(hasChanges));
|
|
3625
|
+
printContextResumeHint();
|
|
3285
3626
|
}
|
|
3286
3627
|
|
|
3287
3628
|
// src/commands/diff.ts
|
|
3288
|
-
import
|
|
3629
|
+
import chalk16 from "chalk";
|
|
3289
3630
|
function gitOut2(args) {
|
|
3290
3631
|
const r = safeExecFile("git", args);
|
|
3291
3632
|
return r.ok ? r.out : "";
|
|
@@ -3322,67 +3663,67 @@ function summarizeNumstat(numstat) {
|
|
|
3322
3663
|
return { fileCount, totalAdd, totalDel };
|
|
3323
3664
|
}
|
|
3324
3665
|
function printFile(f) {
|
|
3325
|
-
const adds = f.additions > 0 ?
|
|
3326
|
-
const dels = f.deletions > 0 ?
|
|
3666
|
+
const adds = f.additions > 0 ? chalk16.green(`+${f.additions}`) : "";
|
|
3667
|
+
const dels = f.deletions > 0 ? chalk16.red(`-${f.deletions}`) : "";
|
|
3327
3668
|
const change = [adds, dels].filter(Boolean).join(" ");
|
|
3328
3669
|
console.log(` ${f.name} ${change}`);
|
|
3329
3670
|
}
|
|
3330
3671
|
async function diff() {
|
|
3331
|
-
console.log(
|
|
3672
|
+
console.log(chalk16.bold(`
|
|
3332
3673
|
\u{1F50D} ${t("diff.title")}`));
|
|
3333
|
-
console.log(
|
|
3674
|
+
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
3334
3675
|
if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
|
|
3335
|
-
console.log(
|
|
3676
|
+
console.log(chalk16.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3336
3677
|
return;
|
|
3337
3678
|
}
|
|
3338
3679
|
const unstaged = gitOut2(["diff", "--stat"]);
|
|
3339
3680
|
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
3340
3681
|
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
3341
3682
|
if (!unstaged && !staged && !untracked) {
|
|
3342
|
-
console.log(
|
|
3683
|
+
console.log(chalk16.green(`
|
|
3343
3684
|
\u2705 ${t("diff.noChanges")}`));
|
|
3344
3685
|
return;
|
|
3345
3686
|
}
|
|
3346
3687
|
if (staged) {
|
|
3347
|
-
console.log(
|
|
3688
|
+
console.log(chalk16.cyan(`
|
|
3348
3689
|
${t("diff.stagedHeader")}`));
|
|
3349
3690
|
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
3350
3691
|
}
|
|
3351
3692
|
if (unstaged) {
|
|
3352
|
-
console.log(
|
|
3693
|
+
console.log(chalk16.cyan(`
|
|
3353
3694
|
${t("diff.unstagedHeader")}`));
|
|
3354
3695
|
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
3355
3696
|
}
|
|
3356
3697
|
if (untracked) {
|
|
3357
3698
|
const files = untracked.split("\n").filter(Boolean);
|
|
3358
|
-
console.log(
|
|
3699
|
+
console.log(chalk16.cyan(`
|
|
3359
3700
|
${t("diff.untrackedHeader", files.length)}`));
|
|
3360
|
-
files.forEach((f) => console.log(` ${
|
|
3701
|
+
files.forEach((f) => console.log(` ${chalk16.green("+")} ${f}`));
|
|
3361
3702
|
}
|
|
3362
3703
|
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
3363
3704
|
if (numstat) {
|
|
3364
3705
|
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
3365
|
-
console.log(
|
|
3706
|
+
console.log(chalk16.cyan(`
|
|
3366
3707
|
${t("diff.summaryHeader")}`));
|
|
3367
3708
|
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
3368
|
-
console.log(` \uCD94\uAC00: ${
|
|
3369
|
-
console.log(` \uC0AD\uC81C: ${
|
|
3709
|
+
console.log(` \uCD94\uAC00: ${chalk16.green(`+${totalAdd}`)}\uC904`);
|
|
3710
|
+
console.log(` \uC0AD\uC81C: ${chalk16.red(`-${totalDel}`)}\uC904`);
|
|
3370
3711
|
}
|
|
3371
3712
|
console.log("");
|
|
3372
3713
|
}
|
|
3373
3714
|
|
|
3374
3715
|
// src/commands/mcp-init.ts
|
|
3375
|
-
import { existsSync as
|
|
3376
|
-
import { join as
|
|
3716
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3717
|
+
import { join as join5, dirname } from "path";
|
|
3377
3718
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3378
|
-
import
|
|
3719
|
+
import chalk17 from "chalk";
|
|
3379
3720
|
function resolveMcpEntryPoint() {
|
|
3380
3721
|
try {
|
|
3381
3722
|
const here = fileURLToPath2(import.meta.url);
|
|
3382
3723
|
const dir = dirname(here);
|
|
3383
3724
|
for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
|
|
3384
|
-
const candidate =
|
|
3385
|
-
if (
|
|
3725
|
+
const candidate = join5(dir, ...rel);
|
|
3726
|
+
if (existsSync4(candidate)) return candidate;
|
|
3386
3727
|
}
|
|
3387
3728
|
} catch {
|
|
3388
3729
|
}
|
|
@@ -3390,17 +3731,17 @@ function resolveMcpEntryPoint() {
|
|
|
3390
3731
|
const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
|
|
3391
3732
|
if (typeof url === "string") {
|
|
3392
3733
|
const p = fileURLToPath2(url);
|
|
3393
|
-
if (
|
|
3734
|
+
if (existsSync4(p)) return p;
|
|
3394
3735
|
}
|
|
3395
3736
|
} catch {
|
|
3396
3737
|
}
|
|
3397
3738
|
try {
|
|
3398
|
-
const pkgPath =
|
|
3399
|
-
if (
|
|
3739
|
+
const pkgPath = join5(process.cwd(), "package.json");
|
|
3740
|
+
if (existsSync4(pkgPath)) {
|
|
3400
3741
|
const pkg = readJsonFile(pkgPath);
|
|
3401
3742
|
if (pkg.name === "@byh3071/vhk") {
|
|
3402
|
-
const local =
|
|
3403
|
-
if (
|
|
3743
|
+
const local = join5(process.cwd(), "dist", "mcp", "index.js");
|
|
3744
|
+
if (existsSync4(local)) return local;
|
|
3404
3745
|
}
|
|
3405
3746
|
}
|
|
3406
3747
|
} catch {
|
|
@@ -3415,31 +3756,31 @@ function resolveVhkMcpEntry() {
|
|
|
3415
3756
|
return { command: "vhk-mcp", args: [] };
|
|
3416
3757
|
}
|
|
3417
3758
|
async function mcpInit() {
|
|
3418
|
-
console.log(
|
|
3419
|
-
console.log(
|
|
3420
|
-
const cursorDir =
|
|
3421
|
-
if (!
|
|
3422
|
-
|
|
3759
|
+
console.log(chalk17.bold("\n\u{1F50C} " + t("mcp.initTitle")));
|
|
3760
|
+
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3761
|
+
const cursorDir = join5(process.cwd(), ".cursor");
|
|
3762
|
+
if (!existsSync4(cursorDir)) {
|
|
3763
|
+
mkdirSync3(cursorDir, { recursive: true });
|
|
3423
3764
|
}
|
|
3424
|
-
const configPath =
|
|
3765
|
+
const configPath = join5(cursorDir, "mcp.json");
|
|
3425
3766
|
const vhkEntry = resolveVhkMcpEntry();
|
|
3426
3767
|
let config;
|
|
3427
|
-
if (
|
|
3768
|
+
if (existsSync4(configPath)) {
|
|
3428
3769
|
try {
|
|
3429
3770
|
const parsed = readJsonFile(configPath);
|
|
3430
3771
|
config = {
|
|
3431
3772
|
mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
|
|
3432
3773
|
};
|
|
3433
3774
|
} catch {
|
|
3434
|
-
console.log(
|
|
3775
|
+
console.log(chalk17.yellow("\u26A0\uFE0F \uAE30\uC874 .cursor/mcp.json \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uC0C8 \uD30C\uC77C\uB85C \uB36E\uC5B4\uC501\uB2C8\uB2E4."));
|
|
3435
3776
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3436
3777
|
}
|
|
3437
3778
|
} else {
|
|
3438
3779
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
3439
3780
|
}
|
|
3440
|
-
|
|
3441
|
-
console.log(
|
|
3442
|
-
console.log(
|
|
3781
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3782
|
+
console.log(chalk17.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
|
|
3783
|
+
console.log(chalk17.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
|
|
3443
3784
|
console.log(` ${configPath}`);
|
|
3444
3785
|
printNextStep({
|
|
3445
3786
|
message: t("mcp.nextMessage"),
|
|
@@ -3449,9 +3790,9 @@ async function mcpInit() {
|
|
|
3449
3790
|
}
|
|
3450
3791
|
|
|
3451
3792
|
// src/commands/design.ts
|
|
3452
|
-
import { existsSync as
|
|
3453
|
-
import
|
|
3454
|
-
import
|
|
3793
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3794
|
+
import chalk18 from "chalk";
|
|
3795
|
+
import inquirer8 from "inquirer";
|
|
3455
3796
|
var PALETTES = [
|
|
3456
3797
|
{
|
|
3457
3798
|
name: "Minimal",
|
|
@@ -3503,7 +3844,36 @@ var PALETTES = [
|
|
|
3503
3844
|
}
|
|
3504
3845
|
];
|
|
3505
3846
|
function hasTailwind() {
|
|
3506
|
-
return
|
|
3847
|
+
return existsSync5("tailwind.config.js") || existsSync5("tailwind.config.ts") || existsSync5("tailwind.config.mjs") || existsSync5("tailwind.config.cjs");
|
|
3848
|
+
}
|
|
3849
|
+
function isTailwindV4Deps(deps) {
|
|
3850
|
+
if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return true;
|
|
3851
|
+
const tw = deps.tailwindcss;
|
|
3852
|
+
return typeof tw === "string" && /^\D*4(\.|$)/.test(tw);
|
|
3853
|
+
}
|
|
3854
|
+
function hasTailwindV4() {
|
|
3855
|
+
try {
|
|
3856
|
+
const pkg = readJsonFile(
|
|
3857
|
+
"package.json"
|
|
3858
|
+
);
|
|
3859
|
+
return isTailwindV4Deps({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
3860
|
+
} catch {
|
|
3861
|
+
return false;
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
function generateTailwindV4Theme(palette) {
|
|
3865
|
+
return [
|
|
3866
|
+
"/* vhk design \u2014 Tailwind v4 @theme \uD1A0\uD070 (CSS-first). \uC9C4\uC785 CSS \uC5D0 @import \uD558\uC138\uC694. */",
|
|
3867
|
+
'@import "tailwindcss";',
|
|
3868
|
+
"",
|
|
3869
|
+
"@theme {",
|
|
3870
|
+
...Object.entries(palette.colors).map(([k, v]) => ` --color-${k}: ${v};`),
|
|
3871
|
+
"}",
|
|
3872
|
+
"",
|
|
3873
|
+
"/* \uB2E4\uD06C \uBAA8\uB4DC \u2014 .dark \uD074\uB798\uC2A4 \uAE30\uBC18 variant (bg-background \uB4F1\uC774 .dark \uC5D0\uC11C \uC804\uD658) */",
|
|
3874
|
+
"@custom-variant dark (&:where(.dark, .dark *));",
|
|
3875
|
+
""
|
|
3876
|
+
].join("\n");
|
|
3507
3877
|
}
|
|
3508
3878
|
function generateCSSTokens(palette) {
|
|
3509
3879
|
const lines = Object.entries(palette.colors).map(([key, value]) => ` --color-${key}: ${value};`).join("\n");
|
|
@@ -3524,9 +3894,10 @@ export default vhkColors
|
|
|
3524
3894
|
`;
|
|
3525
3895
|
}
|
|
3526
3896
|
async function design() {
|
|
3527
|
-
console.log(
|
|
3528
|
-
console.log(
|
|
3529
|
-
|
|
3897
|
+
console.log(chalk18.bold("\n\u{1F3A8} " + t("design.title")));
|
|
3898
|
+
console.log(chalk18.gray("\u2500".repeat(40)));
|
|
3899
|
+
if (!ensureInteractive("\uCEEC\uB7EC \uD314\uB808\uD2B8 \uC120\uD0DD\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
|
|
3900
|
+
const { paletteIndex } = await inquirer8.prompt([
|
|
3530
3901
|
{
|
|
3531
3902
|
type: "list",
|
|
3532
3903
|
name: "paletteIndex",
|
|
@@ -3538,32 +3909,37 @@ async function design() {
|
|
|
3538
3909
|
}
|
|
3539
3910
|
]);
|
|
3540
3911
|
const palette = PALETTES[paletteIndex];
|
|
3541
|
-
console.log(
|
|
3912
|
+
console.log(chalk18.cyan(`
|
|
3542
3913
|
\u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
|
|
3543
|
-
const
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3546
|
-
|
|
3914
|
+
const v4 = hasTailwindV4();
|
|
3915
|
+
const targetPath = v4 ? "src/styles/theme.css" : hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
|
|
3916
|
+
const content = v4 ? generateTailwindV4Theme(palette) : hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
|
|
3917
|
+
if (existsSync5(targetPath)) {
|
|
3918
|
+
const { overwrite } = await inquirer8.prompt([{
|
|
3547
3919
|
type: "confirm",
|
|
3548
3920
|
name: "overwrite",
|
|
3549
3921
|
message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
3550
3922
|
default: false
|
|
3551
3923
|
}]);
|
|
3552
3924
|
if (!overwrite) {
|
|
3553
|
-
console.log(
|
|
3925
|
+
console.log(chalk18.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3554
3926
|
return;
|
|
3555
3927
|
}
|
|
3556
3928
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
if (
|
|
3560
|
-
console.log(
|
|
3561
|
-
console.log(
|
|
3929
|
+
mkdirSync4("src/styles", { recursive: true });
|
|
3930
|
+
writeFileSync4(targetPath, content, "utf-8");
|
|
3931
|
+
if (v4) {
|
|
3932
|
+
console.log(chalk18.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (Tailwind v4 @theme)"));
|
|
3933
|
+
console.log(chalk18.gray(' \uC9C4\uC785 CSS(\uC608: src/index.css)\uC5D0 `@import "./styles/theme.css";` \uCD94\uAC00 \u2192 bg-primary \uB4F1 \uC720\uD2F8 \uC0AC\uC6A9.'));
|
|
3934
|
+
console.log(chalk18.gray(" \uB2E4\uD06C \uD1A0\uAE00: \uB8E8\uD2B8 <html>/<body> \uC5D0 `.dark` \uD074\uB798\uC2A4 on/off."));
|
|
3935
|
+
} else if (hasTailwind()) {
|
|
3936
|
+
console.log(chalk18.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
|
|
3937
|
+
console.log(chalk18.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
|
|
3562
3938
|
} else {
|
|
3563
|
-
console.log(
|
|
3564
|
-
console.log(
|
|
3939
|
+
console.log(chalk18.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
|
|
3940
|
+
console.log(chalk18.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
|
|
3565
3941
|
}
|
|
3566
|
-
console.log(
|
|
3942
|
+
console.log(chalk18.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
|
|
3567
3943
|
for (const [key, value] of Object.entries(palette.colors)) {
|
|
3568
3944
|
console.log(` ${key.padEnd(12)} ${value}`);
|
|
3569
3945
|
}
|
|
@@ -3578,9 +3954,9 @@ async function designPalette() {
|
|
|
3578
3954
|
}
|
|
3579
3955
|
|
|
3580
3956
|
// src/commands/theme.ts
|
|
3581
|
-
import { existsSync as
|
|
3582
|
-
import
|
|
3583
|
-
import
|
|
3957
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3958
|
+
import chalk19 from "chalk";
|
|
3959
|
+
import inquirer9 from "inquirer";
|
|
3584
3960
|
function generateDarkCSS() {
|
|
3585
3961
|
return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
|
|
3586
3962
|
|
|
@@ -3636,13 +4012,13 @@ export function initTheme(): void {
|
|
|
3636
4012
|
`;
|
|
3637
4013
|
}
|
|
3638
4014
|
async function theme() {
|
|
3639
|
-
console.log(
|
|
3640
|
-
console.log(
|
|
4015
|
+
console.log(chalk19.bold("\n\u{1F319} " + t("theme.title")));
|
|
4016
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
3641
4017
|
const cssPath = "src/styles/theme.css";
|
|
3642
4018
|
const togglePath = "src/lib/theme-toggle.ts";
|
|
3643
|
-
const conflicts = [cssPath, togglePath].filter((p) =>
|
|
4019
|
+
const conflicts = [cssPath, togglePath].filter((p) => existsSync6(p));
|
|
3644
4020
|
if (conflicts.length > 0) {
|
|
3645
|
-
const { overwrite } = await
|
|
4021
|
+
const { overwrite } = await inquirer9.prompt([{
|
|
3646
4022
|
type: "confirm",
|
|
3647
4023
|
name: "overwrite",
|
|
3648
4024
|
message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
|
|
@@ -3650,21 +4026,21 @@ async function theme() {
|
|
|
3650
4026
|
default: false
|
|
3651
4027
|
}]);
|
|
3652
4028
|
if (!overwrite) {
|
|
3653
|
-
console.log(
|
|
4029
|
+
console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
3654
4030
|
return;
|
|
3655
4031
|
}
|
|
3656
4032
|
}
|
|
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(
|
|
4033
|
+
mkdirSync5("src/styles", { recursive: true });
|
|
4034
|
+
mkdirSync5("src/lib", { recursive: true });
|
|
4035
|
+
writeFileSync5(cssPath, generateDarkCSS(), "utf-8");
|
|
4036
|
+
console.log(chalk19.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
|
|
4037
|
+
writeFileSync5(togglePath, generateToggleUtil(), "utf-8");
|
|
4038
|
+
console.log(chalk19.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
|
|
4039
|
+
console.log(chalk19.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
|
|
4040
|
+
console.log(chalk19.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
|
|
4041
|
+
console.log(chalk19.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
|
|
4042
|
+
console.log(chalk19.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
|
|
4043
|
+
console.log(chalk19.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
|
|
3668
4044
|
printNextStep({
|
|
3669
4045
|
message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
|
|
3670
4046
|
command: "vhk ref list",
|
|
@@ -3673,11 +4049,11 @@ async function theme() {
|
|
|
3673
4049
|
}
|
|
3674
4050
|
|
|
3675
4051
|
// src/commands/ref.ts
|
|
3676
|
-
import { existsSync as
|
|
3677
|
-
import
|
|
4052
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4053
|
+
import chalk20 from "chalk";
|
|
3678
4054
|
var REFS_PATH = ".vhk/refs.json";
|
|
3679
4055
|
function loadRefs() {
|
|
3680
|
-
if (!
|
|
4056
|
+
if (!existsSync7(REFS_PATH)) return [];
|
|
3681
4057
|
try {
|
|
3682
4058
|
const parsed = readJsonFile(REFS_PATH);
|
|
3683
4059
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -3686,28 +4062,28 @@ function loadRefs() {
|
|
|
3686
4062
|
}
|
|
3687
4063
|
}
|
|
3688
4064
|
function saveRefs(refs) {
|
|
3689
|
-
|
|
3690
|
-
|
|
4065
|
+
mkdirSync6(".vhk", { recursive: true });
|
|
4066
|
+
writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
|
|
3691
4067
|
}
|
|
3692
4068
|
async function refAdd(url, memo = "") {
|
|
3693
|
-
console.log(
|
|
3694
|
-
console.log(
|
|
4069
|
+
console.log(chalk20.bold("\n\u{1F517} " + t("ref.addTitle")));
|
|
4070
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
3695
4071
|
if (!url) {
|
|
3696
|
-
console.log(
|
|
3697
|
-
console.log(
|
|
4072
|
+
console.log(chalk20.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4073
|
+
console.log(chalk20.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
|
|
3698
4074
|
return;
|
|
3699
4075
|
}
|
|
3700
4076
|
const refs = loadRefs();
|
|
3701
4077
|
if (refs.some((r) => r.url === url)) {
|
|
3702
|
-
console.log(
|
|
4078
|
+
console.log(chalk20.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
|
|
3703
4079
|
return;
|
|
3704
4080
|
}
|
|
3705
4081
|
refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3706
4082
|
saveRefs(refs);
|
|
3707
|
-
console.log(
|
|
4083
|
+
console.log(chalk20.green(`
|
|
3708
4084
|
\u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
|
|
3709
|
-
console.log(
|
|
3710
|
-
if (memo) console.log(
|
|
4085
|
+
console.log(chalk20.cyan(` ${url}`));
|
|
4086
|
+
if (memo) console.log(chalk20.gray(` \u{1F4DD} ${memo}`));
|
|
3711
4087
|
printNextStep({
|
|
3712
4088
|
message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
|
|
3713
4089
|
command: "vhk ref list",
|
|
@@ -3715,22 +4091,22 @@ async function refAdd(url, memo = "") {
|
|
|
3715
4091
|
});
|
|
3716
4092
|
}
|
|
3717
4093
|
async function refList() {
|
|
3718
|
-
console.log(
|
|
3719
|
-
console.log(
|
|
4094
|
+
console.log(chalk20.bold("\n\u{1F4DA} " + t("ref.listTitle")));
|
|
4095
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
3720
4096
|
const refs = loadRefs();
|
|
3721
4097
|
if (refs.length === 0) {
|
|
3722
|
-
console.log(
|
|
3723
|
-
console.log(
|
|
4098
|
+
console.log(chalk20.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4099
|
+
console.log(chalk20.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
3724
4100
|
return;
|
|
3725
4101
|
}
|
|
3726
|
-
console.log(
|
|
4102
|
+
console.log(chalk20.cyan(`
|
|
3727
4103
|
\uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
|
|
3728
4104
|
`));
|
|
3729
4105
|
refs.forEach((ref, index) => {
|
|
3730
4106
|
const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
|
|
3731
|
-
console.log(
|
|
3732
|
-
if (ref.memo) console.log(
|
|
3733
|
-
console.log(
|
|
4107
|
+
console.log(chalk20.white(` [${index + 1}] ${ref.url}`));
|
|
4108
|
+
if (ref.memo) console.log(chalk20.gray(` \u{1F4DD} ${ref.memo}`));
|
|
4109
|
+
console.log(chalk20.gray(` \u{1F4C5} ${date}`));
|
|
3734
4110
|
console.log("");
|
|
3735
4111
|
});
|
|
3736
4112
|
}
|
|
@@ -3738,7 +4114,7 @@ async function refOpen(indexStr) {
|
|
|
3738
4114
|
const refs = loadRefs();
|
|
3739
4115
|
const idx = parseInt(indexStr, 10) - 1;
|
|
3740
4116
|
if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
|
|
3741
|
-
console.log(
|
|
4117
|
+
console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
|
|
3742
4118
|
return;
|
|
3743
4119
|
}
|
|
3744
4120
|
const ref = refs[idx];
|
|
@@ -3746,14 +4122,14 @@ async function refOpen(indexStr) {
|
|
|
3746
4122
|
try {
|
|
3747
4123
|
parsed = new URL(ref.url);
|
|
3748
4124
|
} catch {
|
|
3749
|
-
console.log(
|
|
4125
|
+
console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
|
|
3750
4126
|
return;
|
|
3751
4127
|
}
|
|
3752
4128
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
3753
|
-
console.log(
|
|
4129
|
+
console.log(chalk20.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
|
|
3754
4130
|
return;
|
|
3755
4131
|
}
|
|
3756
|
-
console.log(
|
|
4132
|
+
console.log(chalk20.cyan(`
|
|
3757
4133
|
\u{1F310} \uC5F4\uAE30: ${ref.url}`));
|
|
3758
4134
|
let result;
|
|
3759
4135
|
if (process.platform === "darwin") {
|
|
@@ -3764,19 +4140,19 @@ async function refOpen(indexStr) {
|
|
|
3764
4140
|
result = safeExecFile("xdg-open", [ref.url]);
|
|
3765
4141
|
}
|
|
3766
4142
|
if (result.ok) {
|
|
3767
|
-
console.log(
|
|
4143
|
+
console.log(chalk20.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
3768
4144
|
} else {
|
|
3769
|
-
console.log(
|
|
4145
|
+
console.log(chalk20.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
4146
|
}
|
|
3771
4147
|
}
|
|
3772
4148
|
|
|
3773
4149
|
// src/commands/harness.ts
|
|
3774
|
-
import { existsSync as
|
|
3775
|
-
import
|
|
4150
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4151
|
+
import chalk21 from "chalk";
|
|
3776
4152
|
import ora2 from "ora";
|
|
3777
4153
|
function detectPM() {
|
|
3778
|
-
if (
|
|
3779
|
-
if (
|
|
4154
|
+
if (existsSync8("pnpm-lock.yaml")) return "pnpm";
|
|
4155
|
+
if (existsSync8("yarn.lock")) return "yarn";
|
|
3780
4156
|
return "npm";
|
|
3781
4157
|
}
|
|
3782
4158
|
function pmRun(pm, script) {
|
|
@@ -3794,14 +4170,14 @@ function detectChecks() {
|
|
|
3794
4170
|
const pm = detectPM();
|
|
3795
4171
|
if (s.lint) {
|
|
3796
4172
|
checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
|
|
3797
|
-
} else if (
|
|
4173
|
+
} else if (existsSync8(".eslintrc.js") || existsSync8(".eslintrc.json") || existsSync8("eslint.config.js")) {
|
|
3798
4174
|
checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
|
|
3799
4175
|
}
|
|
3800
4176
|
if (s["type-check"]) {
|
|
3801
4177
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
|
|
3802
4178
|
} else if (s.typecheck) {
|
|
3803
4179
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
|
|
3804
|
-
} else if (
|
|
4180
|
+
} else if (existsSync8("tsconfig.json")) {
|
|
3805
4181
|
checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
|
|
3806
4182
|
}
|
|
3807
4183
|
if (s.test) {
|
|
@@ -3813,15 +4189,16 @@ function detectChecks() {
|
|
|
3813
4189
|
return checks;
|
|
3814
4190
|
}
|
|
3815
4191
|
async function harness() {
|
|
3816
|
-
|
|
3817
|
-
console.log(
|
|
4192
|
+
if (!ensureNotHardStopped("harness")) return;
|
|
4193
|
+
console.log(chalk21.bold("\n\u{1F527} " + t("harness.title")));
|
|
4194
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3818
4195
|
const checks = detectChecks();
|
|
3819
4196
|
if (checks.length === 0) {
|
|
3820
|
-
console.log(
|
|
3821
|
-
console.log(
|
|
4197
|
+
console.log(chalk21.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4198
|
+
console.log(chalk21.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
|
|
3822
4199
|
return;
|
|
3823
4200
|
}
|
|
3824
|
-
console.log(
|
|
4201
|
+
console.log(chalk21.cyan(`
|
|
3825
4202
|
\u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
|
|
3826
4203
|
`));
|
|
3827
4204
|
const results = [];
|
|
@@ -3833,10 +4210,10 @@ async function harness() {
|
|
|
3833
4210
|
const duration = Date.now() - start2;
|
|
3834
4211
|
const sec = (duration / 1e3).toFixed(1);
|
|
3835
4212
|
if (result.ok) {
|
|
3836
|
-
spinner.succeed(`${check2.name} ${
|
|
4213
|
+
spinner.succeed(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
|
|
3837
4214
|
results.push({ name: check2.name, command: display, passed: true, duration });
|
|
3838
4215
|
} else {
|
|
3839
|
-
spinner.fail(`${check2.name} ${
|
|
4216
|
+
spinner.fail(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
|
|
3840
4217
|
results.push({
|
|
3841
4218
|
name: check2.name,
|
|
3842
4219
|
command: display,
|
|
@@ -3846,24 +4223,25 @@ async function harness() {
|
|
|
3846
4223
|
});
|
|
3847
4224
|
}
|
|
3848
4225
|
}
|
|
3849
|
-
console.log(
|
|
3850
|
-
console.log(
|
|
4226
|
+
console.log(chalk21.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
|
|
4227
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3851
4228
|
for (const r of results) {
|
|
3852
|
-
const icon = r.passed ?
|
|
4229
|
+
const icon = r.passed ? chalk21.green("\u2705") : chalk21.red("\u274C");
|
|
3853
4230
|
const sec = (r.duration / 1e3).toFixed(1);
|
|
3854
|
-
console.log(` ${icon} ${r.name.padEnd(15)} ${
|
|
4231
|
+
console.log(` ${icon} ${r.name.padEnd(15)} ${chalk21.gray(`${sec}s`)}`);
|
|
3855
4232
|
}
|
|
3856
4233
|
const passed = results.filter((r) => r.passed).length;
|
|
3857
4234
|
const all = passed === results.length;
|
|
3858
|
-
console.log(
|
|
4235
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
3859
4236
|
if (all) {
|
|
3860
|
-
console.log(
|
|
4237
|
+
console.log(chalk21.green.bold(`
|
|
3861
4238
|
\u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
|
|
3862
4239
|
} else {
|
|
3863
4240
|
console.log(
|
|
3864
|
-
|
|
4241
|
+
chalk21.red.bold(`
|
|
3865
4242
|
\u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
|
|
3866
4243
|
);
|
|
4244
|
+
process.exitCode = 1;
|
|
3867
4245
|
}
|
|
3868
4246
|
printNextStep({
|
|
3869
4247
|
message: all ? "\uD488\uC9C8 \uC810\uAC80 \uD1B5\uACFC!" : "\uC2E4\uD328 \uD56D\uBAA9\uC744 \uC218\uC815\uD558\uC138\uC694.",
|
|
@@ -3873,9 +4251,9 @@ async function harness() {
|
|
|
3873
4251
|
}
|
|
3874
4252
|
|
|
3875
4253
|
// src/commands/migrate.ts
|
|
3876
|
-
import { existsSync as
|
|
3877
|
-
import
|
|
3878
|
-
import
|
|
4254
|
+
import { existsSync as existsSync9, unlinkSync, rmSync as rmSync2 } from "fs";
|
|
4255
|
+
import chalk22 from "chalk";
|
|
4256
|
+
import inquirer10 from "inquirer";
|
|
3879
4257
|
import ora3 from "ora";
|
|
3880
4258
|
var LOCK_FILES = {
|
|
3881
4259
|
npm: "package-lock.json",
|
|
@@ -3883,26 +4261,26 @@ var LOCK_FILES = {
|
|
|
3883
4261
|
pnpm: "pnpm-lock.yaml"
|
|
3884
4262
|
};
|
|
3885
4263
|
function detectCurrentPM() {
|
|
3886
|
-
if (
|
|
3887
|
-
if (
|
|
3888
|
-
if (
|
|
4264
|
+
if (existsSync9("pnpm-lock.yaml")) return "pnpm";
|
|
4265
|
+
if (existsSync9("yarn.lock")) return "yarn";
|
|
4266
|
+
if (existsSync9("package-lock.json")) return "npm";
|
|
3889
4267
|
return null;
|
|
3890
4268
|
}
|
|
3891
4269
|
function isCLIAvailable(pm) {
|
|
3892
4270
|
return safeExecFile(pm, ["--version"]).ok;
|
|
3893
4271
|
}
|
|
3894
4272
|
async function migrate(target) {
|
|
3895
|
-
console.log(
|
|
3896
|
-
console.log(
|
|
4273
|
+
console.log(chalk22.bold("\n\u{1F504} " + t("migrate.title")));
|
|
4274
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
3897
4275
|
const current = detectCurrentPM();
|
|
3898
|
-
console.log(
|
|
4276
|
+
console.log(chalk22.cyan(`
|
|
3899
4277
|
\uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
|
|
3900
4278
|
let targetPM;
|
|
3901
4279
|
if (target && ["npm", "yarn", "pnpm"].includes(target)) {
|
|
3902
4280
|
targetPM = target;
|
|
3903
4281
|
} else {
|
|
3904
4282
|
const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
|
|
3905
|
-
const { selected } = await
|
|
4283
|
+
const { selected } = await inquirer10.prompt([
|
|
3906
4284
|
{
|
|
3907
4285
|
type: "list",
|
|
3908
4286
|
name: "selected",
|
|
@@ -3913,17 +4291,17 @@ async function migrate(target) {
|
|
|
3913
4291
|
targetPM = selected;
|
|
3914
4292
|
}
|
|
3915
4293
|
if (targetPM === current) {
|
|
3916
|
-
console.log(
|
|
4294
|
+
console.log(chalk22.yellow(`
|
|
3917
4295
|
\u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
|
|
3918
4296
|
return;
|
|
3919
4297
|
}
|
|
3920
4298
|
if (!isCLIAvailable(targetPM)) {
|
|
3921
|
-
console.log(
|
|
4299
|
+
console.log(chalk22.red(`
|
|
3922
4300
|
\u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
3923
|
-
console.log(
|
|
4301
|
+
console.log(chalk22.yellow(` npm i -g ${targetPM}`));
|
|
3924
4302
|
return;
|
|
3925
4303
|
}
|
|
3926
|
-
const { confirm } = await
|
|
4304
|
+
const { confirm } = await inquirer10.prompt([
|
|
3927
4305
|
{
|
|
3928
4306
|
type: "confirm",
|
|
3929
4307
|
name: "confirm",
|
|
@@ -3932,18 +4310,18 @@ async function migrate(target) {
|
|
|
3932
4310
|
}
|
|
3933
4311
|
]);
|
|
3934
4312
|
if (!confirm) {
|
|
3935
|
-
console.log(
|
|
4313
|
+
console.log(chalk22.gray("\uCDE8\uC18C\uB428"));
|
|
3936
4314
|
return;
|
|
3937
4315
|
}
|
|
3938
4316
|
const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
|
|
3939
4317
|
for (const lockFile of Object.values(LOCK_FILES)) {
|
|
3940
|
-
if (
|
|
4318
|
+
if (existsSync9(lockFile)) {
|
|
3941
4319
|
unlinkSync(lockFile);
|
|
3942
4320
|
}
|
|
3943
4321
|
}
|
|
3944
|
-
if (
|
|
4322
|
+
if (existsSync9("node_modules")) {
|
|
3945
4323
|
cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
|
|
3946
|
-
|
|
4324
|
+
rmSync2("node_modules", { recursive: true, force: true });
|
|
3947
4325
|
}
|
|
3948
4326
|
cleanup.succeed("\uAE30\uC874 \uD30C\uC77C \uC815\uB9AC \uC644\uB8CC");
|
|
3949
4327
|
const install = ora3(`${targetPM} install \uC2E4\uD589 \uC911...`).start();
|
|
@@ -3952,10 +4330,10 @@ async function migrate(target) {
|
|
|
3952
4330
|
install.succeed(`${targetPM} install \uC644\uB8CC!`);
|
|
3953
4331
|
} else {
|
|
3954
4332
|
install.fail(`${targetPM} install \uC2E4\uD328`);
|
|
3955
|
-
console.log(
|
|
4333
|
+
console.log(chalk22.red(installResult.err.slice(0, 300)));
|
|
3956
4334
|
return;
|
|
3957
4335
|
}
|
|
3958
|
-
console.log(
|
|
4336
|
+
console.log(chalk22.green.bold(`
|
|
3959
4337
|
\u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
|
|
3960
4338
|
printNextStep({
|
|
3961
4339
|
message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
|
|
@@ -3965,17 +4343,17 @@ async function migrate(target) {
|
|
|
3965
4343
|
}
|
|
3966
4344
|
|
|
3967
4345
|
// src/commands/update.ts
|
|
3968
|
-
import { existsSync as
|
|
3969
|
-
import { dirname as dirname2, join as
|
|
4346
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4347
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
3970
4348
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
3971
|
-
import
|
|
4349
|
+
import chalk23 from "chalk";
|
|
3972
4350
|
import ora4 from "ora";
|
|
3973
4351
|
var PACKAGE = "@byh3071/vhk";
|
|
3974
4352
|
function getCurrentVersion() {
|
|
3975
4353
|
const dir = dirname2(fileURLToPath3(import.meta.url));
|
|
3976
|
-
for (const pkgPath of [
|
|
4354
|
+
for (const pkgPath of [join6(dir, "../package.json"), join6(dir, "../../package.json")]) {
|
|
3977
4355
|
try {
|
|
3978
|
-
if (
|
|
4356
|
+
if (existsSync10(pkgPath)) {
|
|
3979
4357
|
const pkg = readJsonFile(pkgPath);
|
|
3980
4358
|
if (pkg.version) return pkg.version;
|
|
3981
4359
|
}
|
|
@@ -3998,32 +4376,32 @@ function isUpToDate(current, latest) {
|
|
|
3998
4376
|
return cc >= lc;
|
|
3999
4377
|
}
|
|
4000
4378
|
async function update() {
|
|
4001
|
-
console.log(
|
|
4002
|
-
console.log(
|
|
4379
|
+
console.log(chalk23.bold("\n\u2B06\uFE0F " + t("update.title")));
|
|
4380
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
4003
4381
|
const current = getCurrentVersion();
|
|
4004
|
-
console.log(
|
|
4382
|
+
console.log(chalk23.cyan(`
|
|
4005
4383
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
|
|
4006
4384
|
const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
|
|
4007
4385
|
const latest = getLatestVersion();
|
|
4008
4386
|
if (!latest) {
|
|
4009
4387
|
spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
4010
|
-
console.log(
|
|
4011
|
-
console.log(
|
|
4388
|
+
console.log(chalk23.yellow(" \uB124\uD2B8\uC6CC\uD06C\uB97C \uD655\uC778\uD558\uAC70\uB098 \uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
4389
|
+
console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
|
|
4012
4390
|
return;
|
|
4013
4391
|
}
|
|
4014
4392
|
spinner.stop();
|
|
4015
|
-
console.log(
|
|
4393
|
+
console.log(chalk23.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
|
|
4016
4394
|
if (isUpToDate(current, latest)) {
|
|
4017
|
-
console.log(
|
|
4395
|
+
console.log(chalk23.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
|
|
4018
4396
|
return;
|
|
4019
4397
|
}
|
|
4020
4398
|
const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
|
|
4021
4399
|
const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
|
|
4022
4400
|
if (upd.ok) {
|
|
4023
4401
|
updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
|
|
4024
|
-
console.log(
|
|
4402
|
+
console.log(chalk23.green.bold(`
|
|
4025
4403
|
\u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
4026
|
-
console.log(
|
|
4404
|
+
console.log(chalk23.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
4027
4405
|
printNextStep({
|
|
4028
4406
|
message: t("update.nextOkMessage"),
|
|
4029
4407
|
command: "vhk --version",
|
|
@@ -4031,9 +4409,9 @@ async function update() {
|
|
|
4031
4409
|
});
|
|
4032
4410
|
} else {
|
|
4033
4411
|
updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
|
|
4034
|
-
console.log(
|
|
4035
|
-
console.log(
|
|
4036
|
-
console.log(
|
|
4412
|
+
console.log(chalk23.red(upd.err.slice(0, 300)));
|
|
4413
|
+
console.log(chalk23.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
4414
|
+
console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
|
|
4037
4415
|
printNextStep({
|
|
4038
4416
|
message: t("update.nextFailMessage"),
|
|
4039
4417
|
command: "vhk doctor",
|
|
@@ -4051,115 +4429,9 @@ import {
|
|
|
4051
4429
|
statSync as statSync2,
|
|
4052
4430
|
writeFileSync as writeFileSync7
|
|
4053
4431
|
} 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
|
|
4162
|
-
var CONTEXT_PATH2 = ".vhk/context.md";
|
|
4432
|
+
import { join as join7 } from "path";
|
|
4433
|
+
import chalk24 from "chalk";
|
|
4434
|
+
var CONTEXT_PATH = ".vhk/context.md";
|
|
4163
4435
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4164
4436
|
"node_modules",
|
|
4165
4437
|
".git",
|
|
@@ -4183,7 +4455,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
|
|
|
4183
4455
|
filtered.forEach((entry, index) => {
|
|
4184
4456
|
const isLast = index === filtered.length - 1;
|
|
4185
4457
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4186
|
-
const fullPath =
|
|
4458
|
+
const fullPath = join7(dir, entry);
|
|
4187
4459
|
const stat = statSync2(fullPath);
|
|
4188
4460
|
const isDir = stat.isDirectory();
|
|
4189
4461
|
lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
@@ -4257,11 +4529,12 @@ function getVhkCommands() {
|
|
|
4257
4529
|
"mcp-init \u2014 Cursor MCP \uC124\uC815"
|
|
4258
4530
|
];
|
|
4259
4531
|
}
|
|
4260
|
-
async function context() {
|
|
4261
|
-
|
|
4262
|
-
console.log(
|
|
4532
|
+
async function context(opts = {}) {
|
|
4533
|
+
const compact = opts.compact === true;
|
|
4534
|
+
console.log(chalk24.bold("\n\u{1F9E0} " + t("context.title")));
|
|
4535
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4263
4536
|
const stack = extractTechStack();
|
|
4264
|
-
const tree = buildTree(".").join("\n");
|
|
4537
|
+
const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
|
|
4265
4538
|
const commands = getVhkCommands();
|
|
4266
4539
|
const lines = [];
|
|
4267
4540
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8");
|
|
@@ -4277,25 +4550,37 @@ async function context() {
|
|
|
4277
4550
|
lines.push("");
|
|
4278
4551
|
lines.push("## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870");
|
|
4279
4552
|
lines.push("");
|
|
4280
|
-
lines.push("```");
|
|
4553
|
+
lines.push("```text");
|
|
4281
4554
|
lines.push(tree);
|
|
4282
4555
|
lines.push("```");
|
|
4283
4556
|
lines.push("");
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
lines.push(
|
|
4557
|
+
if (compact) {
|
|
4558
|
+
lines.push("## \uBA85\uB839\uC5B4 (\uC694\uC57D)");
|
|
4559
|
+
lines.push("");
|
|
4560
|
+
lines.push("- \uC804\uCCB4 \uBAA9\uB85D\uC740 `vhk help` \uB610\uB294 `COMMANDS.md` \uCC38\uC870 (compact \uBAA8\uB4DC\uB294 \uC0DD\uB7B5)");
|
|
4561
|
+
lines.push("");
|
|
4562
|
+
} else {
|
|
4563
|
+
lines.push("## VHK CLI \uBA85\uB839\uC5B4");
|
|
4564
|
+
lines.push("");
|
|
4565
|
+
for (const cmd of commands) {
|
|
4566
|
+
lines.push(`- \`vhk ${cmd}\``);
|
|
4567
|
+
}
|
|
4568
|
+
lines.push("");
|
|
4288
4569
|
}
|
|
4289
|
-
lines.push("");
|
|
4290
4570
|
if (existsSync11(".vhk/memory.json")) {
|
|
4291
4571
|
try {
|
|
4292
4572
|
const memories = readJsonFile(
|
|
4293
4573
|
".vhk/memory.json"
|
|
4294
4574
|
);
|
|
4295
4575
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
4576
|
+
const recentMemories = memories.slice(-5);
|
|
4296
4577
|
lines.push("## \uC800\uC7A5\uB41C \uACB0\uC815\uC0AC\uD56D");
|
|
4297
4578
|
lines.push("");
|
|
4298
|
-
|
|
4579
|
+
if (memories.length > recentMemories.length) {
|
|
4580
|
+
lines.push(`_\uCD5C\uADFC ${recentMemories.length}\uAC1C\uB9CC \uD45C\uC2DC (\uC804\uCCB4 ${memories.length}\uAC1C)_`);
|
|
4581
|
+
lines.push("");
|
|
4582
|
+
}
|
|
4583
|
+
for (const m of recentMemories) {
|
|
4299
4584
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4300
4585
|
lines.push(`- ${m.content} _(${date})_`);
|
|
4301
4586
|
}
|
|
@@ -4326,6 +4611,24 @@ async function context() {
|
|
|
4326
4611
|
for (const r of recent) lines.push(r);
|
|
4327
4612
|
lines.push("");
|
|
4328
4613
|
}
|
|
4614
|
+
const activeBlockers = getActiveBlockers(3);
|
|
4615
|
+
if (activeBlockers.length > 0) {
|
|
4616
|
+
lines.push("## Active Blockers");
|
|
4617
|
+
lines.push("");
|
|
4618
|
+
for (const b of activeBlockers) lines.push(b);
|
|
4619
|
+
lines.push("");
|
|
4620
|
+
}
|
|
4621
|
+
if (compact) {
|
|
4622
|
+
lines.push("## \uCC38\uC870 \uBB38\uC11C (\uD544\uC694\uC2DC \uC5F4\uB78C)");
|
|
4623
|
+
lines.push("");
|
|
4624
|
+
lines.push("- \uC791\uB3D9 \uADDC\uC57D(\uC694\uC57D): `docs/context/agent-compact.md`");
|
|
4625
|
+
lines.push("- \uADDC\uC57D \uC0C1\uC138: `AGENTS.md`");
|
|
4626
|
+
lines.push("- \uAE30\uB85D \uADDC\uCE59: `CLAUDE.md`");
|
|
4627
|
+
lines.push("- \uBA85\uB839 \uC0C1\uC138: `COMMANDS.md`");
|
|
4628
|
+
lines.push("- \uAD6C\uC870 \uC0C1\uC138: `docs/ARCHITECTURE.md`");
|
|
4629
|
+
lines.push("- \uD604\uC7AC \uC0C1\uD0DC: `docs/state/next-task.md`");
|
|
4630
|
+
lines.push("");
|
|
4631
|
+
}
|
|
4329
4632
|
if (isHardStopActive()) {
|
|
4330
4633
|
lines.push("## \u26A0\uFE0F HARD_STOP \uD65C\uC131");
|
|
4331
4634
|
lines.push("");
|
|
@@ -4343,11 +4646,11 @@ async function context() {
|
|
|
4343
4646
|
}
|
|
4344
4647
|
lines.push("");
|
|
4345
4648
|
mkdirSync7(".vhk", { recursive: true });
|
|
4346
|
-
writeFileSync7(
|
|
4347
|
-
console.log(
|
|
4348
|
-
\u2705 ${
|
|
4349
|
-
console.log(
|
|
4350
|
-
console.log(
|
|
4649
|
+
writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
|
|
4650
|
+
console.log(chalk24.green(`
|
|
4651
|
+
\u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
|
|
4652
|
+
console.log(chalk24.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
|
|
4653
|
+
console.log(chalk24.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
|
|
4351
4654
|
printNextStep({
|
|
4352
4655
|
message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
|
|
4353
4656
|
command: "vhk context-show",
|
|
@@ -4355,20 +4658,20 @@ async function context() {
|
|
|
4355
4658
|
});
|
|
4356
4659
|
}
|
|
4357
4660
|
async function contextShow() {
|
|
4358
|
-
console.log(
|
|
4359
|
-
console.log(
|
|
4360
|
-
if (!existsSync11(
|
|
4361
|
-
console.log(
|
|
4362
|
-
console.log(
|
|
4661
|
+
console.log(chalk24.bold("\n\u{1F4C4} " + t("context.showTitle")));
|
|
4662
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4663
|
+
if (!existsSync11(CONTEXT_PATH)) {
|
|
4664
|
+
console.log(chalk24.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4665
|
+
console.log(chalk24.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
|
|
4363
4666
|
return;
|
|
4364
4667
|
}
|
|
4365
|
-
const content = readFileSync4(
|
|
4668
|
+
const content = readFileSync4(CONTEXT_PATH, "utf-8");
|
|
4366
4669
|
console.log("\n" + content);
|
|
4367
4670
|
}
|
|
4368
4671
|
|
|
4369
4672
|
// src/commands/memory.ts
|
|
4370
4673
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4371
|
-
import
|
|
4674
|
+
import chalk25 from "chalk";
|
|
4372
4675
|
var MEMORY_PATH = ".vhk/memory.json";
|
|
4373
4676
|
function loadMemories() {
|
|
4374
4677
|
if (!existsSync12(MEMORY_PATH)) return [];
|
|
@@ -4384,11 +4687,12 @@ function saveMemories(memories) {
|
|
|
4384
4687
|
writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
4385
4688
|
}
|
|
4386
4689
|
async function memoryAdd(content, tags) {
|
|
4387
|
-
console.log(
|
|
4388
|
-
console.log(
|
|
4690
|
+
console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.addTitle")));
|
|
4691
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
4389
4692
|
if (!content) {
|
|
4390
|
-
console.log(
|
|
4391
|
-
console.log(
|
|
4693
|
+
console.log(chalk25.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4694
|
+
console.log(chalk25.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
|
|
4695
|
+
process.exitCode = 1;
|
|
4392
4696
|
return;
|
|
4393
4697
|
}
|
|
4394
4698
|
const memories = loadMemories();
|
|
@@ -4398,9 +4702,9 @@ async function memoryAdd(content, tags) {
|
|
|
4398
4702
|
tags: tags && tags.length > 0 ? tags : []
|
|
4399
4703
|
});
|
|
4400
4704
|
saveMemories(memories);
|
|
4401
|
-
console.log(
|
|
4705
|
+
console.log(chalk25.green(`
|
|
4402
4706
|
\u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
|
|
4403
|
-
console.log(
|
|
4707
|
+
console.log(chalk25.cyan(` \u{1F4DD} ${content}`));
|
|
4404
4708
|
printNextStep({
|
|
4405
4709
|
message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
|
|
4406
4710
|
command: "vhk memory list",
|
|
@@ -4408,24 +4712,24 @@ async function memoryAdd(content, tags) {
|
|
|
4408
4712
|
});
|
|
4409
4713
|
}
|
|
4410
4714
|
async function memoryList() {
|
|
4411
|
-
console.log(
|
|
4412
|
-
console.log(
|
|
4715
|
+
console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.listTitle")));
|
|
4716
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
4413
4717
|
const memories = loadMemories();
|
|
4414
4718
|
if (memories.length === 0) {
|
|
4415
|
-
console.log(
|
|
4416
|
-
console.log(
|
|
4719
|
+
console.log(chalk25.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4720
|
+
console.log(chalk25.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
4417
4721
|
return;
|
|
4418
4722
|
}
|
|
4419
|
-
console.log(
|
|
4723
|
+
console.log(chalk25.cyan(`
|
|
4420
4724
|
\uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
|
|
4421
4725
|
`));
|
|
4422
4726
|
memories.forEach((m, index) => {
|
|
4423
4727
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
4424
|
-
console.log(
|
|
4728
|
+
console.log(chalk25.white(` [${index + 1}] ${m.content}`));
|
|
4425
4729
|
if (m.tags && m.tags.length > 0) {
|
|
4426
|
-
console.log(
|
|
4730
|
+
console.log(chalk25.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
|
|
4427
4731
|
}
|
|
4428
|
-
console.log(
|
|
4732
|
+
console.log(chalk25.gray(` \u{1F4C5} ${date}`));
|
|
4429
4733
|
console.log("");
|
|
4430
4734
|
});
|
|
4431
4735
|
}
|
|
@@ -4433,26 +4737,44 @@ async function memoryRemove(indexStr) {
|
|
|
4433
4737
|
const memories = loadMemories();
|
|
4434
4738
|
const idx = parseInt(indexStr, 10) - 1;
|
|
4435
4739
|
if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
|
|
4436
|
-
console.log(
|
|
4740
|
+
console.log(chalk25.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
|
|
4437
4741
|
return;
|
|
4438
4742
|
}
|
|
4439
4743
|
const removed = memories.splice(idx, 1)[0];
|
|
4440
4744
|
saveMemories(memories);
|
|
4441
|
-
console.log(
|
|
4442
|
-
console.log(
|
|
4745
|
+
console.log(chalk25.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
|
|
4746
|
+
console.log(chalk25.gray(` ${removed.content}`));
|
|
4443
4747
|
}
|
|
4444
4748
|
|
|
4445
4749
|
// src/commands/brief.ts
|
|
4446
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
4447
|
-
import
|
|
4750
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync5 } from "fs";
|
|
4751
|
+
import chalk26 from "chalk";
|
|
4448
4752
|
var BRIEF_PATH = ".vhk/brief.md";
|
|
4753
|
+
function readProjectIdentity() {
|
|
4754
|
+
const out = {};
|
|
4755
|
+
try {
|
|
4756
|
+
if (existsSync13("RULES.md")) {
|
|
4757
|
+
const r = readFileSync5("RULES.md", "utf-8");
|
|
4758
|
+
const m = r.split("\n")[0].match(/^#\s*(.+?)(?:\s*—.*)?$/);
|
|
4759
|
+
if (m) out.name = m[1].trim();
|
|
4760
|
+
const d = r.match(/한 줄 설명:\s*(.+)/);
|
|
4761
|
+
if (d) out.description = d[1].trim();
|
|
4762
|
+
}
|
|
4763
|
+
if (!out.name && existsSync13("CLAUDE.md")) {
|
|
4764
|
+
const m = readFileSync5("CLAUDE.md", "utf-8").match(/#\s*기록 규칙\s*\((.+?)\)/);
|
|
4765
|
+
if (m) out.name = m[1].trim();
|
|
4766
|
+
}
|
|
4767
|
+
} catch {
|
|
4768
|
+
}
|
|
4769
|
+
return out;
|
|
4770
|
+
}
|
|
4449
4771
|
function git2(args) {
|
|
4450
4772
|
const result = safeExecFile("git", args);
|
|
4451
4773
|
return result.ok ? result.out : "";
|
|
4452
4774
|
}
|
|
4453
4775
|
async function brief() {
|
|
4454
|
-
console.log(
|
|
4455
|
-
console.log(
|
|
4776
|
+
console.log(chalk26.bold("\n\u{1F4CB} " + t("brief.title")));
|
|
4777
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
4456
4778
|
const lines = [];
|
|
4457
4779
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
|
|
4458
4780
|
lines.push("");
|
|
@@ -4460,11 +4782,12 @@ async function brief() {
|
|
|
4460
4782
|
lines.push("");
|
|
4461
4783
|
try {
|
|
4462
4784
|
const pkg = readJsonFile("package.json");
|
|
4785
|
+
const id = readProjectIdentity();
|
|
4463
4786
|
lines.push("## \uD504\uB85C\uC81D\uD2B8 \uC815\uBCF4");
|
|
4464
4787
|
lines.push("");
|
|
4465
|
-
lines.push(`- **\uC774\uB984**: ${pkg.name ?? "\uBBF8\uC815"}`);
|
|
4788
|
+
lines.push(`- **\uC774\uB984**: ${id.name ?? pkg.name ?? "\uBBF8\uC815"}`);
|
|
4466
4789
|
lines.push(`- **\uBC84\uC804**: ${pkg.version ?? "\uBBF8\uC815"}`);
|
|
4467
|
-
lines.push(`- **\uC124\uBA85**: ${pkg.description ?? "\uC5C6\uC74C"}`);
|
|
4790
|
+
lines.push(`- **\uC124\uBA85**: ${id.description ?? pkg.description ?? "\uC5C6\uC74C"}`);
|
|
4468
4791
|
const deps = Object.keys(pkg.dependencies ?? {}).length;
|
|
4469
4792
|
const devDeps = Object.keys(pkg.devDependencies ?? {}).length;
|
|
4470
4793
|
lines.push(`- **\uC758\uC874\uC131**: ${deps}\uAC1C (dev: ${devDeps}\uAC1C)`);
|
|
@@ -4536,7 +4859,7 @@ async function brief() {
|
|
|
4536
4859
|
mkdirSync9(".vhk", { recursive: true });
|
|
4537
4860
|
writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
|
|
4538
4861
|
console.log("\n" + lines.join("\n"));
|
|
4539
|
-
console.log(
|
|
4862
|
+
console.log(chalk26.green(`
|
|
4540
4863
|
\u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
|
|
4541
4864
|
printNextStep({
|
|
4542
4865
|
message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -4546,11 +4869,11 @@ async function brief() {
|
|
|
4546
4869
|
}
|
|
4547
4870
|
|
|
4548
4871
|
// src/commands/start.ts
|
|
4549
|
-
import
|
|
4550
|
-
import
|
|
4872
|
+
import chalk27 from "chalk";
|
|
4873
|
+
import inquirer11 from "inquirer";
|
|
4551
4874
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
4552
4875
|
import { existsSync as existsSync14 } from "fs";
|
|
4553
|
-
import { join as
|
|
4876
|
+
import { join as join8 } from "path";
|
|
4554
4877
|
var VHK_FOOTPRINT_FILES = [
|
|
4555
4878
|
"CLAUDE.md",
|
|
4556
4879
|
".cursorrules",
|
|
@@ -4559,7 +4882,7 @@ var VHK_FOOTPRINT_FILES = [
|
|
|
4559
4882
|
"docs/PRD.md"
|
|
4560
4883
|
];
|
|
4561
4884
|
function detectExistingFootprint(cwd) {
|
|
4562
|
-
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(
|
|
4885
|
+
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join8(cwd, rel)));
|
|
4563
4886
|
}
|
|
4564
4887
|
async function runGitInit(cwd) {
|
|
4565
4888
|
try {
|
|
@@ -4588,22 +4911,22 @@ async function runStep(label, fn) {
|
|
|
4588
4911
|
}
|
|
4589
4912
|
}
|
|
4590
4913
|
async function start(options = {}) {
|
|
4591
|
-
console.log(
|
|
4914
|
+
console.log(chalk27.bold(`
|
|
4592
4915
|
${ko.start.title}
|
|
4593
4916
|
`));
|
|
4594
|
-
console.log(
|
|
4595
|
-
console.log(
|
|
4596
|
-
console.log(
|
|
4597
|
-
console.log(
|
|
4598
|
-
console.log(
|
|
4917
|
+
console.log(chalk27.dim(ko.start.intro));
|
|
4918
|
+
console.log(chalk27.dim(` ${ko.start.step1}`));
|
|
4919
|
+
console.log(chalk27.dim(` ${ko.start.step2}`));
|
|
4920
|
+
console.log(chalk27.dim(` ${ko.start.step3}`));
|
|
4921
|
+
console.log(chalk27.dim(` ${ko.start.step4}`));
|
|
4599
4922
|
console.log();
|
|
4600
4923
|
const cwd = process.cwd();
|
|
4601
4924
|
const footprint = detectExistingFootprint(cwd);
|
|
4602
4925
|
if (footprint.length > 0 && !options.yes) {
|
|
4603
|
-
console.log(
|
|
4604
|
-
for (const f of footprint) console.log(
|
|
4605
|
-
console.log(
|
|
4606
|
-
const { proceedExisting } = await
|
|
4926
|
+
console.log(chalk27.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
|
|
4927
|
+
for (const f of footprint) console.log(chalk27.dim(` - ${f}`));
|
|
4928
|
+
console.log(chalk27.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."));
|
|
4929
|
+
const { proceedExisting } = await inquirer11.prompt([{
|
|
4607
4930
|
type: "confirm",
|
|
4608
4931
|
name: "proceedExisting",
|
|
4609
4932
|
message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
@@ -4614,7 +4937,7 @@ ${ko.start.title}
|
|
|
4614
4937
|
return;
|
|
4615
4938
|
}
|
|
4616
4939
|
} else if (!options.yes) {
|
|
4617
|
-
const { proceed } = await
|
|
4940
|
+
const { proceed } = await inquirer11.prompt([{
|
|
4618
4941
|
type: "confirm",
|
|
4619
4942
|
name: "proceed",
|
|
4620
4943
|
message: ko.start.confirmStart,
|
|
@@ -4640,7 +4963,7 @@ ${ko.start.title}
|
|
|
4640
4963
|
await runStep("[3/4] vhk mcp-init", () => mcpInit());
|
|
4641
4964
|
log.step(ko.start.step4Header);
|
|
4642
4965
|
await runStep("[4/4] vhk context", () => context());
|
|
4643
|
-
console.log(
|
|
4966
|
+
console.log(chalk27.bold.green(`
|
|
4644
4967
|
${ko.start.allDone}
|
|
4645
4968
|
`));
|
|
4646
4969
|
printNextStep({
|
|
@@ -4650,15 +4973,15 @@ ${ko.start.allDone}
|
|
|
4650
4973
|
}
|
|
4651
4974
|
|
|
4652
4975
|
// src/commands/cloud.ts
|
|
4653
|
-
import
|
|
4976
|
+
import fs12 from "fs";
|
|
4654
4977
|
import os from "os";
|
|
4655
|
-
import
|
|
4656
|
-
import
|
|
4978
|
+
import path13 from "path";
|
|
4979
|
+
import chalk28 from "chalk";
|
|
4657
4980
|
|
|
4658
4981
|
// src/lib/vhk-cloud.ts
|
|
4659
4982
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
4660
|
-
import
|
|
4661
|
-
import
|
|
4983
|
+
import fs11 from "fs";
|
|
4984
|
+
import path12 from "path";
|
|
4662
4985
|
var DEFAULT_CLOUD_EXCLUDES = [
|
|
4663
4986
|
"memory.json",
|
|
4664
4987
|
// 개인 의사결정 메모
|
|
@@ -4676,17 +4999,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
|
|
|
4676
4999
|
function loadVhkignore(rootDir) {
|
|
4677
5000
|
const ig = (0, import_ignore.default)();
|
|
4678
5001
|
ig.add(DEFAULT_CLOUD_EXCLUDES);
|
|
4679
|
-
const ignorePath =
|
|
4680
|
-
if (
|
|
4681
|
-
ig.add(
|
|
5002
|
+
const ignorePath = path12.join(rootDir, ".vhkignore");
|
|
5003
|
+
if (fs11.existsSync(ignorePath)) {
|
|
5004
|
+
ig.add(fs11.readFileSync(ignorePath, "utf-8"));
|
|
4682
5005
|
}
|
|
4683
5006
|
return ig;
|
|
4684
5007
|
}
|
|
4685
5008
|
function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
|
|
4686
|
-
const vhkDir =
|
|
5009
|
+
const vhkDir = path12.join(rootDir, VHK_DIR2);
|
|
4687
5010
|
let entries;
|
|
4688
5011
|
try {
|
|
4689
|
-
entries =
|
|
5012
|
+
entries = fs11.readdirSync(vhkDir, { withFileTypes: true });
|
|
4690
5013
|
} catch {
|
|
4691
5014
|
return [];
|
|
4692
5015
|
}
|
|
@@ -4702,10 +5025,10 @@ function partitionGistFiles(gistFiles, ig) {
|
|
|
4702
5025
|
return { keep, excluded };
|
|
4703
5026
|
}
|
|
4704
5027
|
function readCloudConfig(rootDir) {
|
|
4705
|
-
const p =
|
|
4706
|
-
if (!
|
|
5028
|
+
const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
|
|
5029
|
+
if (!fs11.existsSync(p)) return null;
|
|
4707
5030
|
try {
|
|
4708
|
-
const parsed = JSON.parse(
|
|
5031
|
+
const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
|
|
4709
5032
|
if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
|
|
4710
5033
|
return { gistId: parsed.gistId };
|
|
4711
5034
|
}
|
|
@@ -4715,24 +5038,44 @@ function readCloudConfig(rootDir) {
|
|
|
4715
5038
|
}
|
|
4716
5039
|
}
|
|
4717
5040
|
function writeCloudConfig(rootDir, config) {
|
|
4718
|
-
const vhkDir =
|
|
4719
|
-
|
|
4720
|
-
const p =
|
|
4721
|
-
|
|
5041
|
+
const vhkDir = path12.join(rootDir, VHK_DIR2);
|
|
5042
|
+
fs11.mkdirSync(vhkDir, { recursive: true });
|
|
5043
|
+
const p = path12.join(vhkDir, CLOUD_CONFIG_FILE);
|
|
5044
|
+
fs11.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5045
|
+
ensureCloudConfigIgnored(vhkDir);
|
|
5046
|
+
}
|
|
5047
|
+
function ensureCloudConfigIgnored(vhkDir) {
|
|
5048
|
+
const giPath = path12.join(vhkDir, ".gitignore");
|
|
5049
|
+
let content = "";
|
|
5050
|
+
try {
|
|
5051
|
+
if (fs11.existsSync(giPath)) content = fs11.readFileSync(giPath, "utf-8");
|
|
5052
|
+
} catch {
|
|
5053
|
+
return;
|
|
5054
|
+
}
|
|
5055
|
+
const already = content.split(/\r?\n/).some((l) => l.trim() === CLOUD_CONFIG_FILE);
|
|
5056
|
+
if (already) return;
|
|
5057
|
+
const block = `# secret gist \uD3EC\uC778\uD130 \u2014 \uCD94\uC801 \uAE08\uC9C0 (VHK-022)
|
|
5058
|
+
${CLOUD_CONFIG_FILE}
|
|
5059
|
+
`;
|
|
5060
|
+
const base = content.length === 0 ? "" : content.endsWith("\n") ? content : content + "\n";
|
|
5061
|
+
try {
|
|
5062
|
+
fs11.writeFileSync(giPath, base + block, "utf-8");
|
|
5063
|
+
} catch {
|
|
5064
|
+
}
|
|
4722
5065
|
}
|
|
4723
5066
|
|
|
4724
5067
|
// src/commands/cloud.ts
|
|
4725
5068
|
function ensureGhReady() {
|
|
4726
5069
|
const ver = safeExecFile("gh", ["--version"]);
|
|
4727
5070
|
if (!ver.ok) {
|
|
4728
|
-
console.log(
|
|
4729
|
-
console.log(
|
|
5071
|
+
console.log(chalk28.red(` ${ko.cloud.noGh}`));
|
|
5072
|
+
console.log(chalk28.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
|
|
4730
5073
|
return false;
|
|
4731
5074
|
}
|
|
4732
5075
|
const auth = safeExecFile("gh", ["auth", "status"]);
|
|
4733
5076
|
if (!auth.ok) {
|
|
4734
|
-
console.log(
|
|
4735
|
-
console.log(
|
|
5077
|
+
console.log(chalk28.red(` ${ko.cloud.noAuth}`));
|
|
5078
|
+
console.log(chalk28.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
|
|
4736
5079
|
return false;
|
|
4737
5080
|
}
|
|
4738
5081
|
return true;
|
|
@@ -4745,29 +5088,29 @@ function parseGistId(output) {
|
|
|
4745
5088
|
return null;
|
|
4746
5089
|
}
|
|
4747
5090
|
async function cloudPush() {
|
|
4748
|
-
console.log(
|
|
5091
|
+
console.log(chalk28.bold(`
|
|
4749
5092
|
${ko.cloud.pushTitle}
|
|
4750
5093
|
`));
|
|
4751
5094
|
const cwd = process.cwd();
|
|
4752
|
-
if (!
|
|
4753
|
-
console.log(
|
|
5095
|
+
if (!fs12.existsSync(path13.join(cwd, VHK_DIR2))) {
|
|
5096
|
+
console.log(chalk28.yellow(` ${ko.cloud.noVhkDir}`));
|
|
4754
5097
|
return;
|
|
4755
5098
|
}
|
|
4756
5099
|
const ig = loadVhkignore(cwd);
|
|
4757
5100
|
const files = collectVhkFiles(cwd, ig);
|
|
4758
5101
|
if (files.length === 0) {
|
|
4759
|
-
console.log(
|
|
5102
|
+
console.log(chalk28.yellow(` ${ko.cloud.nothingToSync}`));
|
|
4760
5103
|
return;
|
|
4761
5104
|
}
|
|
4762
5105
|
if (!ensureGhReady()) {
|
|
4763
5106
|
process.exitCode = 1;
|
|
4764
5107
|
return;
|
|
4765
5108
|
}
|
|
4766
|
-
const filePaths = files.map((f) =>
|
|
4767
|
-
console.log(
|
|
5109
|
+
const filePaths = files.map((f) => path13.join(cwd, VHK_DIR2, f));
|
|
5110
|
+
console.log(chalk28.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
|
|
4768
5111
|
`));
|
|
4769
5112
|
const existing = readCloudConfig(cwd);
|
|
4770
|
-
const desc = `vhk .vhk backup \u2014 ${
|
|
5113
|
+
const desc = `vhk .vhk backup \u2014 ${path13.basename(cwd)}`;
|
|
4771
5114
|
if (existing) {
|
|
4772
5115
|
const gistFiles = listGistFiles(existing.gistId);
|
|
4773
5116
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -4776,8 +5119,8 @@ ${ko.cloud.pushTitle}
|
|
|
4776
5119
|
const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
|
|
4777
5120
|
const res2 = safeExecFile("gh", args);
|
|
4778
5121
|
if (!res2.ok) {
|
|
4779
|
-
console.log(
|
|
4780
|
-
console.log(
|
|
5122
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail}: ${name}`));
|
|
5123
|
+
console.log(chalk28.dim(` ${res2.err}`));
|
|
4781
5124
|
process.exitCode = 1;
|
|
4782
5125
|
return;
|
|
4783
5126
|
}
|
|
@@ -4792,15 +5135,15 @@ ${ko.cloud.pushTitle}
|
|
|
4792
5135
|
if (!purgeFailed.includes(name)) purgeFailed.push(name);
|
|
4793
5136
|
}
|
|
4794
5137
|
}
|
|
4795
|
-
console.log(
|
|
4796
|
-
console.log(
|
|
5138
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
|
|
5139
|
+
console.log(chalk28.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
|
|
4797
5140
|
if (excluded.length > 0) {
|
|
4798
5141
|
const purged = excluded.filter((n) => !purgeFailed.includes(n));
|
|
4799
5142
|
if (purged.length > 0) {
|
|
4800
|
-
console.log(
|
|
5143
|
+
console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
|
|
4801
5144
|
}
|
|
4802
5145
|
if (purgeFailed.length > 0) {
|
|
4803
|
-
console.log(
|
|
5146
|
+
console.log(chalk28.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
5147
|
}
|
|
4805
5148
|
}
|
|
4806
5149
|
printPushNext();
|
|
@@ -4808,32 +5151,32 @@ ${ko.cloud.pushTitle}
|
|
|
4808
5151
|
}
|
|
4809
5152
|
const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
|
|
4810
5153
|
if (!res.ok) {
|
|
4811
|
-
console.log(
|
|
4812
|
-
console.log(
|
|
5154
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail}`));
|
|
5155
|
+
console.log(chalk28.dim(` ${res.err || res.out}`));
|
|
4813
5156
|
process.exitCode = 1;
|
|
4814
5157
|
return;
|
|
4815
5158
|
}
|
|
4816
5159
|
const gistId = parseGistId(res.out);
|
|
4817
5160
|
if (!gistId) {
|
|
4818
|
-
console.log(
|
|
4819
|
-
console.log(
|
|
5161
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
|
|
5162
|
+
console.log(chalk28.dim(` \uCD9C\uB825: ${res.out}`));
|
|
4820
5163
|
process.exitCode = 1;
|
|
4821
5164
|
return;
|
|
4822
5165
|
}
|
|
4823
5166
|
writeCloudConfig(cwd, { gistId });
|
|
4824
|
-
console.log(
|
|
4825
|
-
console.log(
|
|
5167
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
|
|
5168
|
+
console.log(chalk28.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
|
|
4826
5169
|
printPushNext();
|
|
4827
5170
|
}
|
|
4828
5171
|
async function cloudPull(gistIdArg) {
|
|
4829
|
-
console.log(
|
|
5172
|
+
console.log(chalk28.bold(`
|
|
4830
5173
|
${ko.cloud.pullTitle}
|
|
4831
5174
|
`));
|
|
4832
5175
|
const cwd = process.cwd();
|
|
4833
5176
|
const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
|
|
4834
5177
|
if (!gistId) {
|
|
4835
|
-
console.log(
|
|
4836
|
-
console.log(
|
|
5178
|
+
console.log(chalk28.yellow(` ${ko.cloud.noGistId}`));
|
|
5179
|
+
console.log(chalk28.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
|
|
4837
5180
|
return;
|
|
4838
5181
|
}
|
|
4839
5182
|
if (!ensureGhReady()) {
|
|
@@ -4842,34 +5185,34 @@ ${ko.cloud.pullTitle}
|
|
|
4842
5185
|
}
|
|
4843
5186
|
const allNames = listGistFiles(gistId);
|
|
4844
5187
|
if (allNames.length === 0) {
|
|
4845
|
-
console.log(
|
|
5188
|
+
console.log(chalk28.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
|
|
4846
5189
|
process.exitCode = 1;
|
|
4847
5190
|
return;
|
|
4848
5191
|
}
|
|
4849
5192
|
const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
|
|
4850
5193
|
if (skipped.length > 0) {
|
|
4851
|
-
console.log(
|
|
5194
|
+
console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
|
|
4852
5195
|
}
|
|
4853
5196
|
if (names.length === 0) {
|
|
4854
|
-
console.log(
|
|
5197
|
+
console.log(chalk28.yellow(` \uBCF5\uC6D0 \uB300\uC0C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4 (gist \uD30C\uC77C\uC774 \uBAA8\uB450 \uC81C\uC678 \uADDC\uCE59\uC5D0 \uD574\uB2F9).`));
|
|
4855
5198
|
return;
|
|
4856
5199
|
}
|
|
4857
|
-
const vhkDir =
|
|
4858
|
-
|
|
5200
|
+
const vhkDir = path13.join(cwd, VHK_DIR2);
|
|
5201
|
+
fs12.mkdirSync(vhkDir, { recursive: true });
|
|
4859
5202
|
let restored = 0;
|
|
4860
5203
|
for (const name of names) {
|
|
4861
5204
|
const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
|
|
4862
5205
|
if (!res.ok) {
|
|
4863
|
-
console.log(
|
|
4864
|
-
console.log(
|
|
5206
|
+
console.log(chalk28.red(` ${ko.cloud.pullFail}: ${name}`));
|
|
5207
|
+
console.log(chalk28.dim(` ${res.err}`));
|
|
4865
5208
|
continue;
|
|
4866
5209
|
}
|
|
4867
|
-
|
|
5210
|
+
fs12.writeFileSync(path13.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
|
|
4868
5211
|
restored++;
|
|
4869
5212
|
}
|
|
4870
5213
|
writeCloudConfig(cwd, { gistId });
|
|
4871
|
-
console.log(
|
|
4872
|
-
console.log(
|
|
5214
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pullDone}`));
|
|
5215
|
+
console.log(chalk28.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
|
|
4873
5216
|
printNextStep({
|
|
4874
5217
|
message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
|
|
4875
5218
|
command: "vhk \uB9E5\uB77D",
|
|
@@ -4881,9 +5224,9 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4881
5224
|
const body = JSON.stringify({
|
|
4882
5225
|
files: Object.fromEntries(names.map((n) => [n, null]))
|
|
4883
5226
|
});
|
|
4884
|
-
const tmp =
|
|
5227
|
+
const tmp = path13.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
|
|
4885
5228
|
try {
|
|
4886
|
-
|
|
5229
|
+
fs12.writeFileSync(tmp, body, "utf-8");
|
|
4887
5230
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
4888
5231
|
const res = safeExecFile(
|
|
4889
5232
|
"gh",
|
|
@@ -4895,7 +5238,7 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
4895
5238
|
return false;
|
|
4896
5239
|
} finally {
|
|
4897
5240
|
try {
|
|
4898
|
-
|
|
5241
|
+
fs12.unlinkSync(tmp);
|
|
4899
5242
|
} catch {
|
|
4900
5243
|
}
|
|
4901
5244
|
}
|
|
@@ -4916,6 +5259,198 @@ function printPushNext() {
|
|
|
4916
5259
|
});
|
|
4917
5260
|
}
|
|
4918
5261
|
|
|
5262
|
+
// src/commands/help.ts
|
|
5263
|
+
import chalk29 from "chalk";
|
|
5264
|
+
var QUICK_ACTIONS = [
|
|
5265
|
+
{ say: "\uC0C1\uD0DC \uC54C\uB824\uC918", does: "vhk status" },
|
|
5266
|
+
{ say: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?", does: "vhk diff" },
|
|
5267
|
+
{ say: "\uC800\uC7A5\uD574\uC918", does: "vhk save" },
|
|
5268
|
+
{ say: "\uC624\uB298 \uD55C \uC77C \uC815\uB9AC\uD574\uC918", does: "vhk recap" },
|
|
5269
|
+
{ say: "\uB2E4\uC74C\uC5D0 \uBB50 \uD558\uBA74 \uB3FC?", does: "vhk goal next" },
|
|
5270
|
+
{ say: "\uADDC\uCE59 \uB3D9\uAE30\uD654\uD574\uC918", does: "vhk sync" },
|
|
5271
|
+
{ say: "\uBC31\uC5C5 \uBCF5\uC6D0\uD574\uC918", does: "vhk restore" },
|
|
5272
|
+
{ say: "\uBCF4\uC548 \uC810\uAC80\uD574\uC918", does: "vhk secure scan" },
|
|
5273
|
+
{ say: "\uC0C8 \uD504\uB85C\uC81D\uD2B8 \uC2DC\uC791", does: "vhk start" },
|
|
5274
|
+
{ say: "\uC804\uCCB4 \uBA85\uB839\uC5B4 \uBCF4\uAE30", does: "vhk --help" }
|
|
5275
|
+
];
|
|
5276
|
+
function quickActions() {
|
|
5277
|
+
console.log(chalk29.bold("\n\u{1F9ED} VHK \u2014 \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4 (quick actions)"));
|
|
5278
|
+
console.log(chalk29.gray("\u2500".repeat(40)));
|
|
5279
|
+
for (const a of QUICK_ACTIONS) {
|
|
5280
|
+
console.log(` "${chalk29.cyan(a.say)}" \u2192 ${chalk29.dim(a.does)}`);
|
|
5281
|
+
}
|
|
5282
|
+
console.log(chalk29.gray("\n \uC804\uCCB4 \uBA85\uB839\uC740 `vhk --help` \uB610\uB294 COMMANDS.md \uB97C \uBCF4\uC138\uC694."));
|
|
5283
|
+
console.log("");
|
|
5284
|
+
}
|
|
5285
|
+
|
|
5286
|
+
// src/commands/mode.ts
|
|
5287
|
+
import chalk30 from "chalk";
|
|
5288
|
+
|
|
5289
|
+
// src/lib/config.ts
|
|
5290
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
5291
|
+
import { join as join9 } from "path";
|
|
5292
|
+
|
|
5293
|
+
// src/lib/safety-mode.ts
|
|
5294
|
+
var SAFETY_MODES = ["lite", "standard", "strict"];
|
|
5295
|
+
var DEFAULT_SAFETY_MODE = "standard";
|
|
5296
|
+
var SAFETY_MODE_DESC = {
|
|
5297
|
+
lite: "\uACBD\uACE0\uB9CC \u2014 \uC704\uD5D8 \uC791\uC5C5\uB3C4 \uB9C9\uC9C0 \uC54A\uACE0 \uACBD\uACE0\uB9CC \uD45C\uC2DC (\uBE60\uB978 \uBC18\uBCF5\uC6A9)",
|
|
5298
|
+
standard: "\uAE30\uBCF8 \u2014 \uC704\uD5D8 \uC791\uC5C5\uC740 CLI \uD655\uC778\xB7MCP/\uC790\uC5F0\uC5B4 \uBBF8\uB9AC\uBCF4\uAE30",
|
|
5299
|
+
strict: "\uC5C4\uACA9 \u2014 \uB354 \uB9CE\uC740 \uC791\uC5C5(\uC800\uC7A5/\uB3D9\uAE30\uD654 \uB4F1)\uC5D0\uB3C4 \uD655\uC778 \uC694\uAD6C"
|
|
5300
|
+
};
|
|
5301
|
+
function isSafetyMode(value) {
|
|
5302
|
+
return typeof value === "string" && SAFETY_MODES.includes(value);
|
|
5303
|
+
}
|
|
5304
|
+
|
|
5305
|
+
// src/lib/config.ts
|
|
5306
|
+
var CONFIG_DIR = ".vhk";
|
|
5307
|
+
var CONFIG_PATH = join9(CONFIG_DIR, "config.json");
|
|
5308
|
+
var DEFAULT_CONFIG = { safetyMode: DEFAULT_SAFETY_MODE };
|
|
5309
|
+
function readConfig(rootDir = process.cwd()) {
|
|
5310
|
+
const full = join9(rootDir, CONFIG_PATH);
|
|
5311
|
+
if (!existsSync15(full)) return { ...DEFAULT_CONFIG };
|
|
5312
|
+
try {
|
|
5313
|
+
const raw = readJsonFile(full);
|
|
5314
|
+
return {
|
|
5315
|
+
safetyMode: isSafetyMode(raw.safetyMode) ? raw.safetyMode : DEFAULT_CONFIG.safetyMode
|
|
5316
|
+
};
|
|
5317
|
+
} catch {
|
|
5318
|
+
return { ...DEFAULT_CONFIG };
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
function writeConfig(config, rootDir = process.cwd()) {
|
|
5322
|
+
mkdirSync10(join9(rootDir, CONFIG_DIR), { recursive: true });
|
|
5323
|
+
writeFileSync10(join9(rootDir, CONFIG_PATH), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
// src/commands/mode.ts
|
|
5327
|
+
async function mode(target) {
|
|
5328
|
+
console.log(chalk30.bold("\n\u{1F6E1}\uFE0F Safety Mode"));
|
|
5329
|
+
console.log(chalk30.gray("\u2500".repeat(40)));
|
|
5330
|
+
const current = readConfig().safetyMode;
|
|
5331
|
+
if (!target) {
|
|
5332
|
+
console.log(chalk30.cyan(`
|
|
5333
|
+
\uD604\uC7AC \uBAA8\uB4DC: ${chalk30.bold(current)}`));
|
|
5334
|
+
console.log(chalk30.dim(` ${SAFETY_MODE_DESC[current]}`));
|
|
5335
|
+
console.log("");
|
|
5336
|
+
for (const m of SAFETY_MODES) {
|
|
5337
|
+
const mark = m === current ? "\u25CF" : "\u25CB";
|
|
5338
|
+
console.log(` ${mark} ${m.padEnd(9)} ${chalk30.dim(SAFETY_MODE_DESC[m])}`);
|
|
5339
|
+
}
|
|
5340
|
+
printNextStep({
|
|
5341
|
+
message: "\uBAA8\uB4DC\uB97C \uBC14\uAFB8\uB824\uBA74:",
|
|
5342
|
+
command: "vhk mode strict",
|
|
5343
|
+
cursorHint: "\uC548\uC804 \uBAA8\uB4DC strict\uB85C \uBC14\uAFD4\uC918"
|
|
5344
|
+
});
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (!isSafetyMode(target)) {
|
|
5348
|
+
console.log(chalk30.red(`
|
|
5349
|
+
\u274C \uC54C \uC218 \uC5C6\uB294 \uBAA8\uB4DC: ${target}`));
|
|
5350
|
+
console.log(chalk30.dim(` \uAC00\uB2A5: ${SAFETY_MODES.join(" | ")}`));
|
|
5351
|
+
process.exitCode = 1;
|
|
5352
|
+
return;
|
|
5353
|
+
}
|
|
5354
|
+
writeConfig({ ...readConfig(), safetyMode: target });
|
|
5355
|
+
console.log(chalk30.green(`
|
|
5356
|
+
\u2705 Safety Mode \u2192 ${chalk30.bold(target)}`));
|
|
5357
|
+
console.log(chalk30.dim(` ${SAFETY_MODE_DESC[target]}`));
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
// src/commands/verify.ts
|
|
5361
|
+
import chalk31 from "chalk";
|
|
5362
|
+
function verificationChecklist() {
|
|
5363
|
+
return [
|
|
5364
|
+
"\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
|
|
5365
|
+
"\uD14C\uC2A4\uD2B8 \u2014 pnpm run test:run",
|
|
5366
|
+
"\uBE4C\uB4DC \u2014 pnpm run build",
|
|
5367
|
+
"\uBCF4\uC548 \uC2A4\uCE94 \u2014 vhk secure scan"
|
|
5368
|
+
];
|
|
5369
|
+
}
|
|
5370
|
+
async function verify() {
|
|
5371
|
+
console.log(chalk31.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
|
|
5372
|
+
console.log(chalk31.gray("\u2500".repeat(40)));
|
|
5373
|
+
const mode2 = readConfig().safetyMode;
|
|
5374
|
+
console.log(chalk31.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
|
|
5375
|
+
console.log(chalk31.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
|
|
5376
|
+
for (const item of verificationChecklist()) {
|
|
5377
|
+
console.log(` \u2610 ${item}`);
|
|
5378
|
+
}
|
|
5379
|
+
console.log(chalk31.dim("\n \u203B \uBA54\uD0C0\uB7EC\uB108(\uC790\uB3D9 \uC2E4\uD589) \uC790\uB9AC \u2014 \uD604\uC7AC\uB294 \uBB36\uC74C \uC548\uB0B4\uB9CC(lite)."));
|
|
5380
|
+
printNextStep({
|
|
5381
|
+
message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
|
|
5382
|
+
command: "vhk save",
|
|
5383
|
+
cursorHint: "\uC800\uC7A5\uD574\uC918"
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
// src/lib/risk-policy.ts
|
|
5388
|
+
var HIGH_RISK_ACTIONS = [
|
|
5389
|
+
"undo",
|
|
5390
|
+
"deploy",
|
|
5391
|
+
"publish",
|
|
5392
|
+
"migrate",
|
|
5393
|
+
"cloud-pull",
|
|
5394
|
+
"resume",
|
|
5395
|
+
"env-write",
|
|
5396
|
+
"delete"
|
|
5397
|
+
];
|
|
5398
|
+
var STRICT_EXTRA_ACTIONS = /* @__PURE__ */ new Set(["save", "sync"]);
|
|
5399
|
+
var NL_GUARDED_ACTIONS = {
|
|
5400
|
+
undo: "undo",
|
|
5401
|
+
deploy: "deploy",
|
|
5402
|
+
publish: "publish",
|
|
5403
|
+
migrate: "migrate",
|
|
5404
|
+
"cloud-pull": "cloud-pull",
|
|
5405
|
+
env: "env-write",
|
|
5406
|
+
save: "save",
|
|
5407
|
+
sync: "sync"
|
|
5408
|
+
};
|
|
5409
|
+
function isHighRisk(action) {
|
|
5410
|
+
return HIGH_RISK_ACTIONS.includes(action);
|
|
5411
|
+
}
|
|
5412
|
+
function resolveGuard(action, mode2, channel) {
|
|
5413
|
+
const guarded = isHighRisk(action) || mode2 === "strict" && STRICT_EXTRA_ACTIONS.has(action);
|
|
5414
|
+
if (!guarded) return "allow";
|
|
5415
|
+
if (mode2 === "lite") return "warn";
|
|
5416
|
+
return channel === "cli" ? "confirm" : "preview";
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
// src/lib/safety-guard.ts
|
|
5420
|
+
async function runGuarded(action, deps, run) {
|
|
5421
|
+
const mode2 = deps.mode ?? readConfig().safetyMode;
|
|
5422
|
+
const log2 = deps.log ?? (() => {
|
|
5423
|
+
});
|
|
5424
|
+
const guard = resolveGuard(action, mode2, deps.channel);
|
|
5425
|
+
if (guard === "allow") {
|
|
5426
|
+
return { outcome: { ran: true, guard, reason: "low-risk" }, result: await run() };
|
|
5427
|
+
}
|
|
5428
|
+
if (guard === "warn") {
|
|
5429
|
+
log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uBAA8\uB4DC: \uACBD\uACE0\uB9CC \uD558\uACE0 \uC9C4\uD589\uD569\uB2C8\uB2E4.`);
|
|
5430
|
+
return { outcome: { ran: true, guard, reason: "lite-warn" }, result: await run() };
|
|
5431
|
+
}
|
|
5432
|
+
if (guard === "confirm") {
|
|
5433
|
+
if (deps.approved === true) {
|
|
5434
|
+
return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
|
|
5435
|
+
}
|
|
5436
|
+
const tty = deps.isTTY ?? !!process.stdout.isTTY;
|
|
5437
|
+
if (tty && deps.confirm) {
|
|
5438
|
+
const ok = await deps.confirm();
|
|
5439
|
+
if (ok) return { outcome: { ran: true, guard, reason: "confirmed" }, result: await run() };
|
|
5440
|
+
log2(`\uCDE8\uC18C\uB428 \u2014 ${action} \uC744(\uB97C) \uC2E4\uD589\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.`);
|
|
5441
|
+
return { outcome: { ran: false, guard, reason: "declined" } };
|
|
5442
|
+
}
|
|
5443
|
+
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)`);
|
|
5444
|
+
return { outcome: { ran: false, guard, reason: "no-confirm" } };
|
|
5445
|
+
}
|
|
5446
|
+
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})`);
|
|
5447
|
+
if (deps.approved === true) {
|
|
5448
|
+
return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
|
|
5449
|
+
}
|
|
5450
|
+
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.`);
|
|
5451
|
+
return { outcome: { ran: false, guard, reason: "preview-no-approve" } };
|
|
5452
|
+
}
|
|
5453
|
+
|
|
4919
5454
|
// src/lib/nlp-run.ts
|
|
4920
5455
|
async function dispatchNlpRoute(route, input) {
|
|
4921
5456
|
switch (route.command) {
|
|
@@ -4946,6 +5481,8 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4946
5481
|
return save();
|
|
4947
5482
|
case "undo":
|
|
4948
5483
|
return undo();
|
|
5484
|
+
case "restore":
|
|
5485
|
+
return restore(route.args?.[0]);
|
|
4949
5486
|
case "status":
|
|
4950
5487
|
return status();
|
|
4951
5488
|
case "diff":
|
|
@@ -4993,113 +5530,174 @@ async function dispatchNlpRoute(route, input) {
|
|
|
4993
5530
|
if (sub === "next") return goalNext();
|
|
4994
5531
|
if (sub === "check") return goalCheck({});
|
|
4995
5532
|
if (sub === "done") return goalDone({});
|
|
5533
|
+
if (sub === "sync") {
|
|
5534
|
+
await goalSync();
|
|
5535
|
+
return;
|
|
5536
|
+
}
|
|
4996
5537
|
return goalList();
|
|
4997
5538
|
}
|
|
5539
|
+
case "help":
|
|
5540
|
+
return quickActions();
|
|
5541
|
+
case "mode":
|
|
5542
|
+
return mode();
|
|
5543
|
+
case "verify":
|
|
5544
|
+
return verify();
|
|
4998
5545
|
}
|
|
4999
5546
|
}
|
|
5547
|
+
var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
|
|
5548
|
+
"start",
|
|
5549
|
+
"init"
|
|
5550
|
+
]);
|
|
5551
|
+
function requiresConfirmation(route) {
|
|
5552
|
+
const goalSync2 = route.command === "goal" && route.args?.[0] === "sync";
|
|
5553
|
+
return route.confidence === "low" || STATE_CHANGING_COMMANDS.has(route.command) || goalSync2;
|
|
5554
|
+
}
|
|
5000
5555
|
async function runNaturalLanguageRoute(input) {
|
|
5001
5556
|
const route = routeNaturalLanguage(input);
|
|
5002
5557
|
if (!route) {
|
|
5003
|
-
console.log(
|
|
5558
|
+
console.log(chalk32.yellow(`
|
|
5004
5559
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
5005
5560
|
`));
|
|
5006
5561
|
return;
|
|
5007
5562
|
}
|
|
5008
5563
|
console.log("");
|
|
5009
|
-
console.log(
|
|
5010
|
-
console.log(
|
|
5011
|
-
if (route
|
|
5012
|
-
const { confirm } = await
|
|
5564
|
+
console.log(chalk32.cyan(` \u{1F4AC} "${input}"`));
|
|
5565
|
+
console.log(chalk32.cyan(` \u2192 ${route.explanation}`));
|
|
5566
|
+
if (requiresConfirmation(route)) {
|
|
5567
|
+
const { confirm } = await inquirer12.prompt([{
|
|
5013
5568
|
type: "confirm",
|
|
5014
5569
|
name: "confirm",
|
|
5015
5570
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
5016
5571
|
default: true
|
|
5017
5572
|
}]);
|
|
5018
5573
|
if (!confirm) {
|
|
5019
|
-
console.log(
|
|
5574
|
+
console.log(chalk32.dim(` ${ko.nlp.menuHint}`));
|
|
5020
5575
|
return;
|
|
5021
5576
|
}
|
|
5022
5577
|
}
|
|
5023
5578
|
console.log("");
|
|
5579
|
+
const riskAction = NL_GUARDED_ACTIONS[route.command];
|
|
5580
|
+
if (riskAction) {
|
|
5581
|
+
await runGuarded(
|
|
5582
|
+
riskAction,
|
|
5583
|
+
{ channel: "nl", approved: false, log: (m) => console.log(chalk32.yellow(` ${m}`)) },
|
|
5584
|
+
() => dispatchNlpRoute(route, input)
|
|
5585
|
+
);
|
|
5586
|
+
return;
|
|
5587
|
+
}
|
|
5024
5588
|
await dispatchNlpRoute(route, input);
|
|
5025
5589
|
}
|
|
5026
5590
|
|
|
5027
5591
|
// src/commands/agent.ts
|
|
5028
|
-
import
|
|
5592
|
+
import chalk33 from "chalk";
|
|
5029
5593
|
function activeGoalId() {
|
|
5030
5594
|
const goals = listGoals("goals");
|
|
5031
5595
|
const id = selectActiveId(goals);
|
|
5032
5596
|
return id ?? void 0;
|
|
5033
5597
|
}
|
|
5034
5598
|
async function blocker(description) {
|
|
5035
|
-
console.log(
|
|
5599
|
+
console.log(chalk33.bold(`
|
|
5036
5600
|
${ko.agent.blockerTitle}
|
|
5037
5601
|
`));
|
|
5038
5602
|
if (!description || !description.trim()) {
|
|
5039
|
-
console.log(
|
|
5040
|
-
console.log(
|
|
5603
|
+
console.log(chalk33.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5604
|
+
console.log(chalk33.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
|
|
5041
5605
|
process.exitCode = 1;
|
|
5042
5606
|
return;
|
|
5043
5607
|
}
|
|
5044
5608
|
const goalId = activeGoalId();
|
|
5045
5609
|
const r = appendBlocker(description, goalId);
|
|
5046
|
-
console.log(
|
|
5610
|
+
console.log(chalk33.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
5047
5611
|
if (r.hardStopTripped) {
|
|
5048
|
-
console.log(
|
|
5049
|
-
console.log(
|
|
5612
|
+
console.log(chalk33.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
|
|
5613
|
+
console.log(chalk33.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
|
|
5050
5614
|
process.exitCode = 2;
|
|
5051
5615
|
}
|
|
5052
5616
|
}
|
|
5053
5617
|
async function learn(lesson) {
|
|
5054
|
-
console.log(
|
|
5618
|
+
console.log(chalk33.bold(`
|
|
5055
5619
|
${ko.agent.learnTitle}
|
|
5056
5620
|
`));
|
|
5057
5621
|
if (!lesson || !lesson.trim()) {
|
|
5058
|
-
console.log(
|
|
5059
|
-
console.log(
|
|
5622
|
+
console.log(chalk33.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5623
|
+
console.log(chalk33.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
|
|
5060
5624
|
process.exitCode = 1;
|
|
5061
5625
|
return;
|
|
5062
5626
|
}
|
|
5063
5627
|
const goalId = activeGoalId();
|
|
5064
5628
|
appendLearning(lesson, goalId);
|
|
5065
|
-
console.log(
|
|
5629
|
+
console.log(chalk33.green(" \u2705 learnings.md append."));
|
|
5066
5630
|
console.log(
|
|
5067
|
-
|
|
5631
|
+
chalk33.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
|
|
5068
5632
|
);
|
|
5069
5633
|
}
|
|
5070
5634
|
async function resume(opts = {}) {
|
|
5071
|
-
console.log(
|
|
5635
|
+
console.log(chalk33.bold(`
|
|
5072
5636
|
${ko.agent.resumeTitle}
|
|
5073
5637
|
`));
|
|
5074
5638
|
if (!isHardStopActive()) {
|
|
5075
|
-
console.log(
|
|
5639
|
+
console.log(chalk33.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
|
|
5076
5640
|
return;
|
|
5077
5641
|
}
|
|
5078
5642
|
const reason = readHardStopReason();
|
|
5079
5643
|
if (reason) {
|
|
5080
|
-
console.log(
|
|
5081
|
-
console.log(
|
|
5644
|
+
console.log(chalk33.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
|
|
5645
|
+
console.log(chalk33.dim(` ${reason.split("\n").join("\n ")}`));
|
|
5082
5646
|
console.log("");
|
|
5083
5647
|
}
|
|
5084
5648
|
if (!opts.confirm) {
|
|
5085
5649
|
console.log(
|
|
5086
|
-
|
|
5650
|
+
chalk33.red(
|
|
5087
5651
|
" \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
|
|
5088
5652
|
)
|
|
5089
5653
|
);
|
|
5090
|
-
console.log(
|
|
5654
|
+
console.log(chalk33.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
|
|
5091
5655
|
process.exitCode = 1;
|
|
5092
5656
|
return;
|
|
5093
5657
|
}
|
|
5094
5658
|
const removed = clearHardStop();
|
|
5095
5659
|
if (removed) {
|
|
5096
|
-
console.log(
|
|
5660
|
+
console.log(chalk33.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
5097
5661
|
} else {
|
|
5098
|
-
console.log(
|
|
5662
|
+
console.log(chalk33.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
|
|
5099
5663
|
}
|
|
5100
5664
|
}
|
|
5101
5665
|
|
|
5102
5666
|
// src/index.ts
|
|
5667
|
+
async function guardCli(action, approved, run) {
|
|
5668
|
+
if (!ensureNotHardStopped(action)) return;
|
|
5669
|
+
await runGuarded(
|
|
5670
|
+
action,
|
|
5671
|
+
{
|
|
5672
|
+
channel: "cli",
|
|
5673
|
+
approved,
|
|
5674
|
+
confirm: async () => {
|
|
5675
|
+
const { ok } = await inquirer13.prompt([{
|
|
5676
|
+
type: "confirm",
|
|
5677
|
+
name: "ok",
|
|
5678
|
+
message: `\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action})\uC744 \uC2E4\uD589\uD560\uAE4C\uC694?`,
|
|
5679
|
+
default: false
|
|
5680
|
+
}]);
|
|
5681
|
+
return ok;
|
|
5682
|
+
},
|
|
5683
|
+
log: (m) => console.log(chalk34.yellow(` ${m}`))
|
|
5684
|
+
},
|
|
5685
|
+
run
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
async function guardCliDefer(action, approved, run) {
|
|
5689
|
+
await runGuarded(
|
|
5690
|
+
action,
|
|
5691
|
+
{
|
|
5692
|
+
channel: "cli",
|
|
5693
|
+
approved,
|
|
5694
|
+
// TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
|
|
5695
|
+
confirm: async () => !!process.stdout.isTTY,
|
|
5696
|
+
log: (m) => console.log(chalk34.yellow(` ${m}`))
|
|
5697
|
+
},
|
|
5698
|
+
run
|
|
5699
|
+
);
|
|
5700
|
+
}
|
|
5103
5701
|
var program = new Command();
|
|
5104
5702
|
var defaultHelp = new Help();
|
|
5105
5703
|
var KO_ALIASES = {
|
|
@@ -5114,6 +5712,7 @@ var KO_ALIASES = {
|
|
|
5114
5712
|
doctor: "\uD658\uACBD",
|
|
5115
5713
|
save: "\uC800\uC7A5",
|
|
5116
5714
|
undo: "\uB418\uB3CC\uB9AC\uAE30",
|
|
5715
|
+
restore: "\uBCF5\uC6D0",
|
|
5117
5716
|
status: "\uC0C1\uD0DC",
|
|
5118
5717
|
diff: "\uBCC0\uACBD",
|
|
5119
5718
|
deploy: "\uBC30\uD3EC",
|
|
@@ -5165,7 +5764,9 @@ program.command("gate").alias("\uAC80\uC99D").alias("\uC544\uC774\uB514\uC5B4").
|
|
|
5165
5764
|
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
5765
|
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
5766
|
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(
|
|
5767
|
+
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) => {
|
|
5768
|
+
await guardCli("sync", opts?.yes === true, () => sync(opts));
|
|
5769
|
+
});
|
|
5169
5770
|
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
5771
|
await check(opts);
|
|
5171
5772
|
});
|
|
@@ -5177,16 +5778,19 @@ var cloudCmd = program.command("cloud").alias("\uD074\uB77C\uC6B0\uB4DC").descri
|
|
|
5177
5778
|
cloudCmd.command("push").alias("\uC62C\uB9AC\uAE30").description(".vhk/ \uB97C secret gist \uB85C \uBC31\uC5C5").action(async () => {
|
|
5178
5779
|
await cloudPush();
|
|
5179
5780
|
});
|
|
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);
|
|
5781
|
+
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) => {
|
|
5782
|
+
await guardCli("cloud-pull", opts?.yes === true, () => cloudPull(gistId));
|
|
5182
5783
|
});
|
|
5183
5784
|
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
5785
|
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();
|
|
5786
|
+
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) => {
|
|
5787
|
+
await guardCli("save", opts?.yes === true, () => save());
|
|
5187
5788
|
});
|
|
5188
|
-
program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async () => {
|
|
5189
|
-
await undo();
|
|
5789
|
+
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) => {
|
|
5790
|
+
await guardCliDefer("undo", opts?.yes === true, () => undo());
|
|
5791
|
+
});
|
|
5792
|
+
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) => {
|
|
5793
|
+
await restore(id);
|
|
5190
5794
|
});
|
|
5191
5795
|
program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
|
|
5192
5796
|
await status();
|
|
@@ -5198,17 +5802,17 @@ program.command("mcp").description("MCP \uC11C\uBC84 \uC2DC\uC791 (24 tool stdio
|
|
|
5198
5802
|
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
5803
|
await mcpInit();
|
|
5200
5804
|
});
|
|
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();
|
|
5805
|
+
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) => {
|
|
5806
|
+
await guardCli("deploy", opts?.yes === true, () => deploy());
|
|
5203
5807
|
});
|
|
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();
|
|
5808
|
+
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) => {
|
|
5809
|
+
await guardCli("env-write", opts?.yes === true, () => env());
|
|
5206
5810
|
});
|
|
5207
5811
|
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
5812
|
await envCheck();
|
|
5209
5813
|
});
|
|
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();
|
|
5814
|
+
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) => {
|
|
5815
|
+
await guardCli("publish", opts?.yes === true, () => publish());
|
|
5212
5816
|
});
|
|
5213
5817
|
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
5818
|
await design();
|
|
@@ -5237,14 +5841,20 @@ program.command("harness").alias("\uD558\uB124\uC2A4").description("\uD1B5\uD569
|
|
|
5237
5841
|
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
5842
|
await audit(opts.fix);
|
|
5239
5843
|
});
|
|
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);
|
|
5844
|
+
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) => {
|
|
5845
|
+
await guardCli("migrate", opts?.yes === true, () => migrate(target));
|
|
5242
5846
|
});
|
|
5243
5847
|
program.command("update").alias("\uC5C5\uB370\uC774\uD2B8").description("VHK CLI \uCD5C\uC2E0 \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8").action(async () => {
|
|
5244
5848
|
await update();
|
|
5245
5849
|
});
|
|
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();
|
|
5850
|
+
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) => {
|
|
5851
|
+
await context({ compact: opts.compact });
|
|
5852
|
+
});
|
|
5853
|
+
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) => {
|
|
5854
|
+
await mode(target);
|
|
5855
|
+
});
|
|
5856
|
+
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 () => {
|
|
5857
|
+
await verify();
|
|
5248
5858
|
});
|
|
5249
5859
|
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
5860
|
await contextShow();
|
|
@@ -5265,7 +5875,7 @@ memoryCmd.command("remove <index>").alias("\uC0AD\uC81C").description("\uAE30\uC
|
|
|
5265
5875
|
program.command("brief").alias("\uBE0C\uB9AC\uD551").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC694\uC57D \uBCF4\uACE0\uC11C \uC0DD\uC131 (.vhk/brief.md)").action(async () => {
|
|
5266
5876
|
await brief();
|
|
5267
5877
|
});
|
|
5268
|
-
var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done)").action(async () => {
|
|
5878
|
+
var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done / sync)").action(async () => {
|
|
5269
5879
|
await goalList();
|
|
5270
5880
|
});
|
|
5271
5881
|
goalCmd.command("list").alias("\uBAA9\uB85D").description("goals/*.md \uBAA9\uB85D (id, status, priority, title)").action(async () => {
|
|
@@ -5277,12 +5887,15 @@ goalCmd.command("next").alias("\uB2E4\uC74C").description("active goal \uC790\uB
|
|
|
5277
5887
|
goalCmd.command("init").alias("\uCD08\uAE30\uD654").description("\uD604\uC7AC \uD504\uB85C\uC81D\uD2B8\uC5D0 goals/ + docs/state/ \uC2A4\uCE90\uD3F4\uB529 (\uAE30\uC874 \uD30C\uC77C \uBCF4\uC874)").action(async () => {
|
|
5278
5888
|
await goalInit();
|
|
5279
5889
|
});
|
|
5280
|
-
goalCmd.command("check").alias("\uAC80\uC99D").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("scripts/check-goal-<id>.sh \uC2E4\uD589 + exit code \uC804\uB2EC").action(async (opts) => {
|
|
5890
|
+
goalCmd.command("check").alias("\uAC80\uC99D").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("scripts/check-goal-<id>.{mjs,sh} \uC2E4\uD589 + exit code \uC804\uB2EC (.mjs \uC6B0\uC120)").action(async (opts) => {
|
|
5281
5891
|
await goalCheck(opts);
|
|
5282
5892
|
});
|
|
5283
5893
|
goalCmd.command("done").alias("\uC644\uB8CC").option("--id <id>", "goal id \uC9C0\uC815 (\uC0DD\uB7B5 \uC2DC active goal)").description("\uAC8C\uC774\uD2B8 \uC7AC\uAC80\uC99D \u2192 \uD1B5\uACFC \uC2DC frontmatter status=DONE \uC73C\uB85C \uC804\uC774").action(async (opts) => {
|
|
5284
5894
|
await goalDone(opts);
|
|
5285
5895
|
});
|
|
5896
|
+
goalCmd.command("sync").alias("\uB3D9\uAE30\uD654").description("goals/*.md \uC2A4\uCE94 \u2192 \uB204\uB77D\uB41C check-goal-<id>.mjs \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uBC31\uD544 (idempotent)").action(async () => {
|
|
5897
|
+
await goalSync();
|
|
5898
|
+
});
|
|
5286
5899
|
program.command("blocker <description>").alias("\uBE14\uB85C\uCEE4").description("\uBE14\uB85C\uCEE4 \uAE30\uB85D \u2192 docs/state/blockers.md append (3\uAC74 \uB204\uC801 \uC2DC HARD_STOP \uC790\uB3D9 \uC0DD\uC131)").action(async (description) => {
|
|
5287
5900
|
await blocker(description);
|
|
5288
5901
|
});
|
|
@@ -5290,7 +5903,7 @@ program.command("learn <lesson>").alias("\uAD50\uD6C8").description("\uAD50\uD6C
|
|
|
5290
5903
|
await learn(lesson);
|
|
5291
5904
|
});
|
|
5292
5905
|
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);
|
|
5906
|
+
await guardCliDefer("resume", opts?.confirm === true, () => resume(opts));
|
|
5294
5907
|
});
|
|
5295
5908
|
program.on("command:*", async (operands) => {
|
|
5296
5909
|
const unknown = operands[0] ?? "";
|
|
@@ -5300,7 +5913,7 @@ program.on("command:*", async (operands) => {
|
|
|
5300
5913
|
});
|
|
5301
5914
|
program.action(async () => {
|
|
5302
5915
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
5303
|
-
const { choice } = await
|
|
5916
|
+
const { choice } = await inquirer13.prompt([{
|
|
5304
5917
|
type: "list",
|
|
5305
5918
|
name: "choice",
|
|
5306
5919
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
@@ -5331,24 +5944,40 @@ program.action(async () => {
|
|
|
5331
5944
|
case "secure":
|
|
5332
5945
|
return secure();
|
|
5333
5946
|
case "sync":
|
|
5334
|
-
return sync();
|
|
5947
|
+
return guardCli("sync", false, () => sync());
|
|
5335
5948
|
case "doctor":
|
|
5336
5949
|
return doctor();
|
|
5337
5950
|
case "ship":
|
|
5338
5951
|
return ship();
|
|
5339
5952
|
case "save":
|
|
5340
|
-
return save();
|
|
5953
|
+
return guardCli("save", false, () => save());
|
|
5341
5954
|
case "undo":
|
|
5342
|
-
return undo();
|
|
5955
|
+
return guardCliDefer("undo", false, () => undo());
|
|
5343
5956
|
case "status":
|
|
5344
5957
|
return status();
|
|
5345
5958
|
case "diff":
|
|
5346
5959
|
return diff();
|
|
5347
5960
|
}
|
|
5348
5961
|
});
|
|
5349
|
-
var
|
|
5350
|
-
if (
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5962
|
+
var isMainModule = !!process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
5963
|
+
if (isMainModule) {
|
|
5964
|
+
try {
|
|
5965
|
+
const nlInput = detectNaturalLanguageInput(process.argv);
|
|
5966
|
+
if (nlInput !== null) {
|
|
5967
|
+
await runNaturalLanguageRoute(nlInput);
|
|
5968
|
+
} else {
|
|
5969
|
+
await program.parseAsync(process.argv);
|
|
5970
|
+
}
|
|
5971
|
+
} catch (err) {
|
|
5972
|
+
if (isPromptAbortError(err)) {
|
|
5973
|
+
console.error(chalk34.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)"));
|
|
5974
|
+
} else {
|
|
5975
|
+
console.error(chalk34.red(`
|
|
5976
|
+
\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
5977
|
+
}
|
|
5978
|
+
process.exitCode = 1;
|
|
5979
|
+
}
|
|
5354
5980
|
}
|
|
5981
|
+
export {
|
|
5982
|
+
program
|
|
5983
|
+
};
|