@byh3071/vhk 1.6.2 → 1.6.4
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/dist/{chunk-ACJN723Q.js → chunk-EJTVXWUZ.js} +878 -226
- package/dist/index.js +988 -1314
- package/dist/mcp/index.js +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,33 +1,50 @@
|
|
|
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
|
+
stripBom,
|
|
39
|
+
sync,
|
|
23
40
|
t
|
|
24
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-EJTVXWUZ.js";
|
|
25
42
|
|
|
26
43
|
// src/index.ts
|
|
27
44
|
import { Command, Help } from "commander";
|
|
28
45
|
import { pathToFileURL } from "url";
|
|
29
|
-
import
|
|
30
|
-
import
|
|
46
|
+
import chalk34 from "chalk";
|
|
47
|
+
import inquirer13 from "inquirer";
|
|
31
48
|
|
|
32
49
|
// src/lib/nlp-router.ts
|
|
33
50
|
function normalize(input) {
|
|
@@ -289,7 +306,8 @@ var RULES = [
|
|
|
289
306
|
explanation: "\uBAA9\uD45C \uAC8C\uC774\uD2B8 \uAC80\uC99D (vhk goal check)",
|
|
290
307
|
confidence: "high",
|
|
291
308
|
args: ["check"],
|
|
292
|
-
|
|
309
|
+
// '스크립트' 포함 시는 sync 의도(게이트 스크립트 생성) → check 가 가로채지 않게 제외.
|
|
310
|
+
test: (t2) => /목표\s*(점검|검증|체크)/.test(t2) && !/스크립트/.test(t2)
|
|
293
311
|
},
|
|
294
312
|
{
|
|
295
313
|
command: "goal",
|
|
@@ -304,6 +322,13 @@ var RULES = [
|
|
|
304
322
|
confidence: "high",
|
|
305
323
|
args: ["list"],
|
|
306
324
|
test: (t2) => /목표\s*(목록|리스트)/.test(t2)
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
command: "goal",
|
|
328
|
+
explanation: "\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uB3D9\uAE30\uD654 (vhk goal sync)",
|
|
329
|
+
confidence: "high",
|
|
330
|
+
args: ["sync"],
|
|
331
|
+
test: (t2) => /(게이트|목표).*(스크립트|동기화)|체크\s*스크립트\s*(생성|만들)/.test(t2)
|
|
307
332
|
}
|
|
308
333
|
];
|
|
309
334
|
function routeNaturalLanguage(input) {
|
|
@@ -328,7 +353,7 @@ function extractNotionUrl(input) {
|
|
|
328
353
|
|
|
329
354
|
// src/lib/command-registry.ts
|
|
330
355
|
var CONTAINER_SUBCOMMANDS = {
|
|
331
|
-
goal: ["list", "next", "check", "init", "done"],
|
|
356
|
+
goal: ["list", "next", "check", "init", "done", "sync"],
|
|
332
357
|
ref: ["add", "list", "open"],
|
|
333
358
|
memory: ["add", "list", "remove"],
|
|
334
359
|
cloud: ["push", "pull"],
|
|
@@ -475,12 +500,42 @@ function detectNaturalLanguageInput(argv) {
|
|
|
475
500
|
}
|
|
476
501
|
|
|
477
502
|
// src/lib/nlp-run.ts
|
|
478
|
-
import
|
|
479
|
-
import
|
|
503
|
+
import chalk32 from "chalk";
|
|
504
|
+
import inquirer12 from "inquirer";
|
|
480
505
|
|
|
481
506
|
// src/commands/gate.ts
|
|
482
507
|
import inquirer from "inquirer";
|
|
508
|
+
import chalk2 from "chalk";
|
|
509
|
+
|
|
510
|
+
// src/lib/interactive.ts
|
|
483
511
|
import chalk from "chalk";
|
|
512
|
+
function isInteractive(opts) {
|
|
513
|
+
if (opts?.yes) return false;
|
|
514
|
+
if (process.env.VHK_FORCE_INTERACTIVE === "1") return true;
|
|
515
|
+
return !!process.stdin.isTTY;
|
|
516
|
+
}
|
|
517
|
+
async function promptOrDefault(ask, fallback, opts) {
|
|
518
|
+
if (!isInteractive(opts)) return fallback;
|
|
519
|
+
try {
|
|
520
|
+
return await ask();
|
|
521
|
+
} catch (err) {
|
|
522
|
+
if (isPromptAbortError(err)) return fallback;
|
|
523
|
+
throw err;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function ensureInteractive(hint = "") {
|
|
527
|
+
if (isInteractive()) return true;
|
|
528
|
+
console.error(chalk.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."));
|
|
529
|
+
if (hint) console.error(chalk.dim(` ${hint}`));
|
|
530
|
+
process.exitCode = 1;
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
function isPromptAbortError(err) {
|
|
534
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
535
|
+
return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/gate.ts
|
|
484
539
|
var GATE_QUESTIONS = [
|
|
485
540
|
{ id: 1, stage: "\uBB38\uC81C \uC815\uC758", question: "\uC774 \uC544\uC774\uB514\uC5B4\uAC00 \uD574\uACB0\uD558\uB294 \uBB38\uC81C\uB97C \uD55C \uBB38\uC7A5\uC73C\uB85C \uB9D0\uD574\uBCF4\uC138\uC694.", failIf: "\uD55C \uBB38\uC7A5 \uBD88\uAC00 \u2192 \uBBF8\uC131\uC219", quick: true },
|
|
486
541
|
{ id: 2, stage: "\uD575\uC2EC \uAE30\uB2A5", question: "\uB531 1\uAC1C \uAE30\uB2A5\uB9CC \uACE0\uB974\uBA74?", failIf: "2\uAC1C \uC774\uC0C1 \u2192 \uBC94\uC704 \uCD08\uACFC", quick: true },
|
|
@@ -502,7 +557,8 @@ function judgeGate(failCount, holdCount) {
|
|
|
502
557
|
return "DROP";
|
|
503
558
|
}
|
|
504
559
|
async function gate() {
|
|
505
|
-
|
|
560
|
+
if (!ensureInteractive("\uC544\uC774\uB514\uC5B4 \uAC80\uC99D\uC740 \uB300\uD654\uD615 \uC9C8\uBB38\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. \uD130\uBBF8\uB110(PowerShell \uB4F1)\uC5D0\uC11C \uC9C1\uC811 \uC2E4\uD589\uD558\uC138\uC694. Git Bash \uBA74 VHK_FORCE_INTERACTIVE=1.")) return;
|
|
561
|
+
console.log(chalk2.bold(`
|
|
506
562
|
${ko.gate.title}
|
|
507
563
|
`));
|
|
508
564
|
const { mode: mode2 } = await inquirer.prompt([{
|
|
@@ -521,33 +577,33 @@ ${ko.gate.title}
|
|
|
521
577
|
name: "source",
|
|
522
578
|
message: ko.gate.skipSourcePrompt
|
|
523
579
|
}]);
|
|
524
|
-
console.log(
|
|
580
|
+
console.log(chalk2.green.bold(`
|
|
525
581
|
${ko.gate.skipGo}`));
|
|
526
|
-
console.log(
|
|
582
|
+
console.log(chalk2.dim(ko.gate.skipSourceLabel(source)));
|
|
527
583
|
return;
|
|
528
584
|
}
|
|
529
585
|
const questions = mode2 === "quick" ? GATE_QUESTIONS.filter((q) => q.quick) : GATE_QUESTIONS;
|
|
530
586
|
const total = questions.length;
|
|
531
587
|
const header = mode2 === "quick" ? ko.gate.quickHeader : ko.gate.fullHeader;
|
|
532
|
-
console.log(
|
|
588
|
+
console.log(chalk2.dim(`
|
|
533
589
|
${header} ${ko.gate.modeCountSuffix(total)}
|
|
534
590
|
`));
|
|
535
|
-
console.log(
|
|
591
|
+
console.log(chalk2.dim(`
|
|
536
592
|
${ko.gate.welcome}
|
|
537
593
|
`));
|
|
538
|
-
console.log(
|
|
594
|
+
console.log(chalk2.dim(` ${ko.gate.ideaHint}`));
|
|
539
595
|
const { idea } = await inquirer.prompt([
|
|
540
596
|
{ type: "input", name: "idea", message: ko.gate.idea }
|
|
541
597
|
]);
|
|
542
|
-
console.log(
|
|
598
|
+
console.log(chalk2.dim(` ${ko.gate.painPointHint}`));
|
|
543
599
|
const { painPoint } = await inquirer.prompt([
|
|
544
600
|
{ type: "input", name: "painPoint", message: ko.gate.painPoint }
|
|
545
601
|
]);
|
|
546
|
-
console.log(
|
|
602
|
+
console.log(chalk2.dim(` ${ko.gate.edgeHint}`));
|
|
547
603
|
const { edge } = await inquirer.prompt([
|
|
548
604
|
{ type: "input", name: "edge", message: ko.gate.edge }
|
|
549
605
|
]);
|
|
550
|
-
console.log(
|
|
606
|
+
console.log(chalk2.dim(`
|
|
551
607
|
${ko.gate.checklistStart}
|
|
552
608
|
`));
|
|
553
609
|
let failCount = 0;
|
|
@@ -555,7 +611,7 @@ ${ko.gate.checklistStart}
|
|
|
555
611
|
const results = [];
|
|
556
612
|
for (let i = 0; i < questions.length; i++) {
|
|
557
613
|
const q = questions[i];
|
|
558
|
-
if (q.hint) console.log(
|
|
614
|
+
if (q.hint) console.log(chalk2.dim(`${ko.gate.hintPrefix} ${q.hint}`));
|
|
559
615
|
const { answer } = await inquirer.prompt([{
|
|
560
616
|
type: "input",
|
|
561
617
|
name: "answer",
|
|
@@ -574,22 +630,22 @@ ${ko.gate.checklistStart}
|
|
|
574
630
|
if (status2 === "fail") failCount++;
|
|
575
631
|
if (status2 === "hold") holdCount++;
|
|
576
632
|
results.push({ id: q.id, stage: q.stage, status: status2, answer });
|
|
577
|
-
const icon = status2 === "pass" ?
|
|
633
|
+
const icon = status2 === "pass" ? chalk2.green(ko.gate.statusPassLine) : status2 === "hold" ? chalk2.yellow(ko.gate.statusHoldLine) : chalk2.red(ko.gate.statusFailLine);
|
|
578
634
|
console.log(icon);
|
|
579
635
|
}
|
|
580
|
-
console.log(
|
|
636
|
+
console.log(chalk2.bold(`
|
|
581
637
|
${ko.gate.verdictTitle}
|
|
582
638
|
`));
|
|
583
|
-
console.log(`${ko.gate.ideaLabel} ${
|
|
639
|
+
console.log(`${ko.gate.ideaLabel} ${chalk2.cyan(idea)}`);
|
|
584
640
|
console.log(`${ko.gate.painPointLabel} ${painPoint}`);
|
|
585
641
|
console.log(`${ko.gate.edgeLabel} ${edge}`);
|
|
586
642
|
console.log(`${ko.gate.countLine(failCount, holdCount, total)}
|
|
587
643
|
`);
|
|
588
644
|
const verdict = judgeGate(failCount, holdCount);
|
|
589
645
|
if (verdict === "GO") {
|
|
590
|
-
console.log(
|
|
646
|
+
console.log(chalk2.green.bold(ko.gate.go));
|
|
591
647
|
if (holdCount > 0) {
|
|
592
|
-
console.log(
|
|
648
|
+
console.log(chalk2.yellow(ko.gate.holdRemainHint));
|
|
593
649
|
}
|
|
594
650
|
printNextStep({
|
|
595
651
|
message: "\uC544\uC774\uB514\uC5B4 \uD1B5\uACFC! \uC774\uC81C \uD504\uB85C\uC81D\uD2B8\uB97C \uB9CC\uB4E4\uC5B4\uBCF4\uC138\uC694.",
|
|
@@ -597,28 +653,23 @@ ${ko.gate.verdictTitle}
|
|
|
597
653
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
598
654
|
});
|
|
599
655
|
} else if (verdict === "REFINE") {
|
|
600
|
-
console.log(
|
|
656
|
+
console.log(chalk2.yellow.bold(ko.gate.refine));
|
|
601
657
|
printNextStep({
|
|
602
658
|
message: "\uC870\uAE08 \uB354 \uB2E4\uB4EC\uC740 \uD6C4 \uB2E4\uC2DC \uAC80\uC99D\uD574\uBCF4\uC138\uC694.",
|
|
603
659
|
command: "vhk \uAC80\uC99D",
|
|
604
660
|
cursorHint: "\uC544\uC774\uB514\uC5B4 \uB2E4\uC2DC \uAC80\uC99D\uD574\uC918"
|
|
605
661
|
});
|
|
606
662
|
} else {
|
|
607
|
-
console.log(
|
|
663
|
+
console.log(chalk2.red.bold(ko.gate.drop));
|
|
608
664
|
}
|
|
609
665
|
}
|
|
610
666
|
|
|
611
667
|
// src/commands/init.ts
|
|
612
668
|
import inquirer2 from "inquirer";
|
|
613
|
-
import
|
|
669
|
+
import chalk4 from "chalk";
|
|
614
670
|
import fs2 from "fs";
|
|
615
671
|
import path2 from "path";
|
|
616
672
|
|
|
617
|
-
// src/lib/date.ts
|
|
618
|
-
function localDate(d = /* @__PURE__ */ new Date()) {
|
|
619
|
-
return d.toLocaleDateString("sv-SE");
|
|
620
|
-
}
|
|
621
|
-
|
|
622
673
|
// src/templates/claude-md.ts
|
|
623
674
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
624
675
|
const d = localDate();
|
|
@@ -898,6 +949,8 @@ function VHK_GITIGNORE_TEMPLATE() {
|
|
|
898
949
|
"memory.json",
|
|
899
950
|
"refs.json",
|
|
900
951
|
"HARD_STOP",
|
|
952
|
+
"# secret gist \uD3EC\uC778\uD130 (gistId). \uACF5\uAC1C repo \uC5D0 \uCEE4\uBC0B\uB418\uBA74 \uBC31\uC5C5 gist \uAC00 \uB178\uCD9C\uB428 (VHK-022).",
|
|
953
|
+
"cloud.json",
|
|
901
954
|
"# sync \uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uBC31\uC5C5 (\uB85C\uCEEC \uBCF5\uAD6C\uC6A9 \u2014 vhk restore). \uCD94\uC801/\uD074\uB77C\uC6B0\uB4DC \uC81C\uC678.",
|
|
902
955
|
"backups/",
|
|
903
956
|
""
|
|
@@ -935,13 +988,13 @@ function VHK_CONTEXT_SEED(name, type, stack) {
|
|
|
935
988
|
}
|
|
936
989
|
|
|
937
990
|
// src/utils/logger.ts
|
|
938
|
-
import
|
|
991
|
+
import chalk3 from "chalk";
|
|
939
992
|
var log = {
|
|
940
|
-
success: (msg) => console.log(
|
|
941
|
-
error: (msg) => console.log(
|
|
942
|
-
warn: (msg) => console.log(
|
|
943
|
-
info: (msg) => console.log(
|
|
944
|
-
step: (msg) => console.log(
|
|
993
|
+
success: (msg) => console.log(chalk3.green(`\u2705 ${msg}`)),
|
|
994
|
+
error: (msg) => console.log(chalk3.red(`\u274C ${msg}`)),
|
|
995
|
+
warn: (msg) => console.log(chalk3.yellow(`\u26A0\uFE0F ${msg}`)),
|
|
996
|
+
info: (msg) => console.log(chalk3.blue(`\u2139\uFE0F ${msg}`)),
|
|
997
|
+
step: (msg) => console.log(chalk3.bold(`
|
|
945
998
|
\u25B8 ${msg}`))
|
|
946
999
|
};
|
|
947
1000
|
|
|
@@ -1102,90 +1155,8 @@ async function fetchPrdFromNotion(urlOrId) {
|
|
|
1102
1155
|
};
|
|
1103
1156
|
}
|
|
1104
1157
|
|
|
1105
|
-
// src/lib/rules-import.ts
|
|
1106
|
-
import { existsSync, readFileSync } from "fs";
|
|
1107
|
-
import { join } from "path";
|
|
1108
|
-
var ADOPT_SOURCES = [
|
|
1109
|
-
".cursorrules",
|
|
1110
|
-
"CLAUDE.md",
|
|
1111
|
-
"AGENTS.md",
|
|
1112
|
-
".windsurfrules",
|
|
1113
|
-
".github/copilot-instructions.md"
|
|
1114
|
-
];
|
|
1115
|
-
var PREAMBLE_TITLE = "\uC11C\uBB38";
|
|
1116
|
-
function detectExistingRuleFiles(cwd) {
|
|
1117
|
-
const found = [];
|
|
1118
|
-
for (const rel of ADOPT_SOURCES) {
|
|
1119
|
-
const full = join(cwd, rel);
|
|
1120
|
-
if (existsSync(full)) {
|
|
1121
|
-
try {
|
|
1122
|
-
found.push({ path: rel, content: readFileSync(full, "utf-8") });
|
|
1123
|
-
} catch {
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
return found;
|
|
1128
|
-
}
|
|
1129
|
-
function splitSections(content) {
|
|
1130
|
-
const sections = [];
|
|
1131
|
-
let title = "";
|
|
1132
|
-
let buf = [];
|
|
1133
|
-
const preamble = [];
|
|
1134
|
-
let sawHeading = false;
|
|
1135
|
-
for (const line of content.split("\n")) {
|
|
1136
|
-
if (line.startsWith("## ")) {
|
|
1137
|
-
sawHeading = true;
|
|
1138
|
-
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1139
|
-
title = line.replace("## ", "").trim();
|
|
1140
|
-
buf = [];
|
|
1141
|
-
} else if (title) {
|
|
1142
|
-
buf.push(line);
|
|
1143
|
-
} else if (!sawHeading) {
|
|
1144
|
-
preamble.push(line);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1148
|
-
const pre = preamble.join("\n").trim();
|
|
1149
|
-
if (pre) sections.unshift({ title: PREAMBLE_TITLE, content: pre });
|
|
1150
|
-
return sections;
|
|
1151
|
-
}
|
|
1152
|
-
function buildAdoptedRules(files, projectName) {
|
|
1153
|
-
const order = [];
|
|
1154
|
-
const byTitle = /* @__PURE__ */ new Map();
|
|
1155
|
-
for (const file of files) {
|
|
1156
|
-
for (const sec of splitSections(file.content)) {
|
|
1157
|
-
let merged = byTitle.get(sec.title);
|
|
1158
|
-
if (!merged) {
|
|
1159
|
-
merged = { title: sec.title, parts: [] };
|
|
1160
|
-
byTitle.set(sec.title, merged);
|
|
1161
|
-
order.push(sec.title);
|
|
1162
|
-
}
|
|
1163
|
-
merged.parts.push({ source: file.path, content: sec.content });
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
const lines = [
|
|
1167
|
-
`# ${projectName} \u2014 Rules`,
|
|
1168
|
-
"",
|
|
1169
|
-
"> \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 \uB2E8\uC77C \uC18C\uC2A4(SoT). \uAE30\uC874 \uADDC\uCE59\uC744 `vhk init` adopt \uB85C \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.",
|
|
1170
|
-
"> \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 \uC774 \uD30C\uC77C\uC5D0\uC11C\uB9CC \u2014 `vhk sync` \uB85C \uAC01 \uB3C4\uAD6C\uC5D0 \uC804\uD30C\uB429\uB2C8\uB2E4.",
|
|
1171
|
-
""
|
|
1172
|
-
];
|
|
1173
|
-
for (const title of order) {
|
|
1174
|
-
const merged = byTitle.get(title);
|
|
1175
|
-
const nonEmpty = merged.parts.filter((p) => p.content.trim());
|
|
1176
|
-
if (!nonEmpty.length) continue;
|
|
1177
|
-
lines.push(`## ${title}`);
|
|
1178
|
-
for (const part of nonEmpty) {
|
|
1179
|
-
lines.push(`<!-- \uCD9C\uCC98: ${part.source} -->`);
|
|
1180
|
-
lines.push(part.content);
|
|
1181
|
-
}
|
|
1182
|
-
lines.push("");
|
|
1183
|
-
}
|
|
1184
|
-
return lines.join("\n");
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
1158
|
// src/lib/stack-detect.ts
|
|
1188
|
-
import { join
|
|
1159
|
+
import { join } from "path";
|
|
1189
1160
|
function detectStackFromDeps(deps) {
|
|
1190
1161
|
const stack = [];
|
|
1191
1162
|
if (deps.next) stack.push("Next.js");
|
|
@@ -1208,7 +1179,7 @@ function detectStackFromDeps(deps) {
|
|
|
1208
1179
|
function detectProjectStack(cwd = ".") {
|
|
1209
1180
|
let pkg;
|
|
1210
1181
|
try {
|
|
1211
|
-
pkg = readJsonFile(
|
|
1182
|
+
pkg = readJsonFile(join(cwd, "package.json"));
|
|
1212
1183
|
} catch {
|
|
1213
1184
|
return null;
|
|
1214
1185
|
}
|
|
@@ -1241,32 +1212,36 @@ function resolveType(type) {
|
|
|
1241
1212
|
}
|
|
1242
1213
|
return type;
|
|
1243
1214
|
}
|
|
1215
|
+
var DEFAULT_TYPE = PROJECT_TYPES[0].value;
|
|
1244
1216
|
async function collectAnswers(options, defaults = {}) {
|
|
1217
|
+
const noninteractive = !isInteractive(options);
|
|
1245
1218
|
const prompts = [];
|
|
1246
|
-
if (!
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1219
|
+
if (!noninteractive) {
|
|
1220
|
+
if (!options.name && !defaults.name) {
|
|
1221
|
+
prompts.push({ type: "input", name: "name", message: ko.init.projectName });
|
|
1222
|
+
}
|
|
1223
|
+
if (!options.description && !defaults.description) {
|
|
1224
|
+
prompts.push({ type: "input", name: "description", message: ko.init.description });
|
|
1225
|
+
}
|
|
1226
|
+
if (!options.type && !defaults.type) {
|
|
1227
|
+
prompts.push({ type: "list", name: "type", message: ko.init.projectType, choices: PROJECT_TYPES });
|
|
1228
|
+
}
|
|
1254
1229
|
}
|
|
1255
1230
|
const prompted = prompts.length ? await inquirer2.prompt(prompts) : {};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
};
|
|
1231
|
+
const fallbackName = path2.basename(process.cwd()) || "my-project";
|
|
1232
|
+
const name = options.name || defaults.name || prompted.name || fallbackName;
|
|
1233
|
+
const description = options.description || defaults.description || prompted.description || `${name} \u2014 vhk \uD504\uB85C\uC81D\uD2B8`;
|
|
1234
|
+
const type = resolveType(options.type || defaults.type || prompted.type) ?? prompted.type ?? DEFAULT_TYPE;
|
|
1235
|
+
return { name, description, type };
|
|
1261
1236
|
}
|
|
1262
1237
|
async function init(options = {}) {
|
|
1263
1238
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
1264
1239
|
if (skipGate) {
|
|
1265
|
-
console.log(
|
|
1240
|
+
console.log(chalk4.dim(`
|
|
1266
1241
|
${ko.init.skipGate}
|
|
1267
1242
|
`));
|
|
1268
1243
|
}
|
|
1269
|
-
console.log(
|
|
1244
|
+
console.log(chalk4.bold(`
|
|
1270
1245
|
${ko.init.title}
|
|
1271
1246
|
`));
|
|
1272
1247
|
printSecurityWarnings();
|
|
@@ -1292,11 +1267,11 @@ ${ko.init.title}
|
|
|
1292
1267
|
}
|
|
1293
1268
|
const detected = detectProjectStack(process.cwd());
|
|
1294
1269
|
const stack = detected ?? STACK_PRESETS[answers.type];
|
|
1295
|
-
if (detected) console.log(
|
|
1296
|
-
console.log(
|
|
1270
|
+
if (detected) console.log(chalk4.dim(" \u{1F50E} package.json \uC758\uC874\uC131\uC5D0\uC11C \uC2E4\uC81C \uC2A4\uD0DD \uAC10\uC9C0"));
|
|
1271
|
+
console.log(chalk4.dim(`
|
|
1297
1272
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1298
1273
|
`));
|
|
1299
|
-
if (
|
|
1274
|
+
if (isInteractive(options)) {
|
|
1300
1275
|
const { confirmStack } = await inquirer2.prompt([{
|
|
1301
1276
|
type: "confirm",
|
|
1302
1277
|
name: "confirmStack",
|
|
@@ -1310,7 +1285,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1310
1285
|
}
|
|
1311
1286
|
const cwd = process.cwd();
|
|
1312
1287
|
let adoptedRules = null;
|
|
1313
|
-
if (
|
|
1288
|
+
if (isInteractive(options) && !options.fromNotion) {
|
|
1314
1289
|
const existingRules = detectExistingRuleFiles(cwd);
|
|
1315
1290
|
if (existingRules.length > 0) {
|
|
1316
1291
|
const { adopt } = await inquirer2.prompt([{
|
|
@@ -1324,7 +1299,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1324
1299
|
}]);
|
|
1325
1300
|
if (adopt) {
|
|
1326
1301
|
adoptedRules = buildAdoptedRules(existingRules, answers.name);
|
|
1327
|
-
console.log(
|
|
1302
|
+
console.log(chalk4.dim(` ${ko.init.adoptPreview(existingRules.length)}`));
|
|
1328
1303
|
}
|
|
1329
1304
|
}
|
|
1330
1305
|
}
|
|
@@ -1334,12 +1309,12 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1334
1309
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1335
1310
|
const fullPath = path2.join(cwd, filePath);
|
|
1336
1311
|
if (fileExists(fullPath)) {
|
|
1337
|
-
const
|
|
1312
|
+
const overwrite = !isInteractive(options) ? false : (await inquirer2.prompt([{
|
|
1338
1313
|
type: "confirm",
|
|
1339
1314
|
name: "overwrite",
|
|
1340
1315
|
message: ko.init.overwrite(filePath),
|
|
1341
1316
|
default: false
|
|
1342
|
-
}]);
|
|
1317
|
+
}])).overwrite;
|
|
1343
1318
|
if (!overwrite) {
|
|
1344
1319
|
log.warn(ko.init.skipped(filePath));
|
|
1345
1320
|
continue;
|
|
@@ -1348,22 +1323,22 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1348
1323
|
writeFile(fullPath, content);
|
|
1349
1324
|
log.success(filePath);
|
|
1350
1325
|
}
|
|
1351
|
-
await writeInitExtras(cwd);
|
|
1352
|
-
console.log(
|
|
1326
|
+
await writeInitExtras(cwd, !isInteractive(options));
|
|
1327
|
+
console.log(chalk4.bold.green(`
|
|
1353
1328
|
${ko.init.done}`));
|
|
1354
|
-
console.log(
|
|
1329
|
+
console.log(chalk4.dim(`
|
|
1355
1330
|
${ko.init.nextSteps}`));
|
|
1356
1331
|
if (options.fromNotion) {
|
|
1357
1332
|
console.log(` 1. ${ko.init.notionReviewHint}`);
|
|
1358
1333
|
console.log(` 2. ${ko.init.gitHintLabel}`);
|
|
1359
|
-
console.log(` ${
|
|
1334
|
+
console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
|
|
1360
1335
|
console.log(` 3. ${ko.init.startDev}
|
|
1361
1336
|
`);
|
|
1362
1337
|
} else {
|
|
1363
1338
|
console.log(` 1. ${ko.init.fillHint}`);
|
|
1364
1339
|
console.log(` 2. ${ko.init.prdHint}`);
|
|
1365
1340
|
console.log(` 3. ${ko.init.gitHintLabel}`);
|
|
1366
|
-
console.log(` ${
|
|
1341
|
+
console.log(` ${chalk4.cyan(ko.init.gitHintCommand)}`);
|
|
1367
1342
|
console.log(` 4. ${ko.init.startDev}
|
|
1368
1343
|
`);
|
|
1369
1344
|
}
|
|
@@ -1462,16 +1437,16 @@ function projectHasTestScript(projectDir) {
|
|
|
1462
1437
|
return false;
|
|
1463
1438
|
}
|
|
1464
1439
|
}
|
|
1465
|
-
async function writeInitExtras(projectDir) {
|
|
1440
|
+
async function writeInitExtras(projectDir, noninteractive = false) {
|
|
1466
1441
|
const commandsPath = path2.join(projectDir, "COMMANDS.md");
|
|
1467
1442
|
const hasTest = projectHasTestScript(projectDir);
|
|
1468
1443
|
if (fileExists(commandsPath)) {
|
|
1469
|
-
const
|
|
1444
|
+
const overwrite = noninteractive ? false : (await inquirer2.prompt([{
|
|
1470
1445
|
type: "confirm",
|
|
1471
1446
|
name: "overwrite",
|
|
1472
1447
|
message: ko.init.overwrite("COMMANDS.md"),
|
|
1473
1448
|
default: false
|
|
1474
|
-
}]);
|
|
1449
|
+
}])).overwrite;
|
|
1475
1450
|
if (!overwrite) {
|
|
1476
1451
|
log.warn(ko.init.skipped("COMMANDS.md"));
|
|
1477
1452
|
} else {
|
|
@@ -1680,31 +1655,17 @@ function createAdrFile(cwd, title, context2, decision, consequences) {
|
|
|
1680
1655
|
return filePath;
|
|
1681
1656
|
}
|
|
1682
1657
|
|
|
1683
|
-
// src/lib/interactive.ts
|
|
1684
|
-
import chalk4 from "chalk";
|
|
1685
|
-
function ensureInteractive(hint = "") {
|
|
1686
|
-
if (process.stdin.isTTY) return true;
|
|
1687
|
-
console.error(chalk4.yellow(" \u26A0\uFE0F \uC774 \uBA85\uB839\uC740 \uB300\uD654\uD615 \uC785\uB825\uC774 \uD544\uC694\uD569\uB2C8\uB2E4 \u2014 \uBE44-TTY/\uD30C\uC774\uD504 \uD658\uACBD\uC5D0\uC11C\uB294 \uC2E4\uD589\uD560 \uC218 \uC5C6\uC5B4\uC694."));
|
|
1688
|
-
if (hint) console.error(chalk4.dim(` ${hint}`));
|
|
1689
|
-
process.exitCode = 1;
|
|
1690
|
-
return false;
|
|
1691
|
-
}
|
|
1692
|
-
function isPromptAbortError(err) {
|
|
1693
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1694
|
-
return /ERR_USE_AFTER_CLOSE|force closed|ExitPromptError|readline was closed|User force closed/i.test(msg);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
1658
|
// src/lib/hard-stop-guard.ts
|
|
1698
1659
|
import chalk5 from "chalk";
|
|
1699
1660
|
|
|
1700
1661
|
// src/lib/state-files.ts
|
|
1701
|
-
import { existsSync
|
|
1702
|
-
import { join as
|
|
1662
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, rmSync } from "fs";
|
|
1663
|
+
import { join as join2 } from "path";
|
|
1703
1664
|
var STATE_DIR = "docs/state";
|
|
1704
|
-
var BLOCKERS_PATH =
|
|
1705
|
-
var LEARNINGS_PATH =
|
|
1665
|
+
var BLOCKERS_PATH = join2(STATE_DIR, "blockers.md");
|
|
1666
|
+
var LEARNINGS_PATH = join2(STATE_DIR, "learnings.md");
|
|
1706
1667
|
var VHK_DIR = ".vhk";
|
|
1707
|
-
var HARD_STOP_PATH =
|
|
1668
|
+
var HARD_STOP_PATH = join2(VHK_DIR, "HARD_STOP");
|
|
1708
1669
|
var HARD_STOP_BLOCKER_THRESHOLD = 3;
|
|
1709
1670
|
function ensureStateDir() {
|
|
1710
1671
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
@@ -1727,7 +1688,7 @@ function appendBlocker(description, goalId) {
|
|
|
1727
1688
|
ensureStateDir();
|
|
1728
1689
|
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1729
1690
|
const line = `- [${isoDate()} ${tag}] ${description.trim()}`;
|
|
1730
|
-
if (!
|
|
1691
|
+
if (!existsSync(BLOCKERS_PATH)) {
|
|
1731
1692
|
writeFileSync(
|
|
1732
1693
|
BLOCKERS_PATH,
|
|
1733
1694
|
`# Blockers
|
|
@@ -1742,10 +1703,10 @@ ${line}
|
|
|
1742
1703
|
appendFileSync(BLOCKERS_PATH, `${line}
|
|
1743
1704
|
`, "utf-8");
|
|
1744
1705
|
}
|
|
1745
|
-
const current =
|
|
1706
|
+
const current = readFileSync(BLOCKERS_PATH, "utf-8");
|
|
1746
1707
|
const count = countActiveBlockers(current);
|
|
1747
1708
|
let hardStopTripped = false;
|
|
1748
|
-
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !
|
|
1709
|
+
if (count >= HARD_STOP_BLOCKER_THRESHOLD && !existsSync(HARD_STOP_PATH)) {
|
|
1749
1710
|
writeHardStop(`auto: ${count} active blockers (threshold ${HARD_STOP_BLOCKER_THRESHOLD})`);
|
|
1750
1711
|
hardStopTripped = true;
|
|
1751
1712
|
}
|
|
@@ -1755,7 +1716,7 @@ function appendLearning(lesson, goalId) {
|
|
|
1755
1716
|
ensureStateDir();
|
|
1756
1717
|
const tag = goalId !== void 0 ? `goal-${goalId}` : "no-goal";
|
|
1757
1718
|
const line = `- [${isoDate()} ${tag}] ${lesson.trim()}`;
|
|
1758
|
-
if (!
|
|
1719
|
+
if (!existsSync(LEARNINGS_PATH)) {
|
|
1759
1720
|
writeFileSync(
|
|
1760
1721
|
LEARNINGS_PATH,
|
|
1761
1722
|
`# Learnings
|
|
@@ -1772,14 +1733,14 @@ ${line}
|
|
|
1772
1733
|
}
|
|
1773
1734
|
}
|
|
1774
1735
|
function getRecentLearnings(limit = 3) {
|
|
1775
|
-
if (!
|
|
1776
|
-
const lines =
|
|
1736
|
+
if (!existsSync(LEARNINGS_PATH)) return [];
|
|
1737
|
+
const lines = readFileSync(LEARNINGS_PATH, "utf-8").split(/\r?\n/);
|
|
1777
1738
|
const entries = lines.filter((l) => l.startsWith("- ["));
|
|
1778
1739
|
return entries.slice(-limit);
|
|
1779
1740
|
}
|
|
1780
1741
|
function getActiveBlockers(limit = 3) {
|
|
1781
|
-
if (!
|
|
1782
|
-
const lines =
|
|
1742
|
+
if (!existsSync(BLOCKERS_PATH)) return [];
|
|
1743
|
+
const lines = readFileSync(BLOCKERS_PATH, "utf-8").split(/\r?\n/);
|
|
1783
1744
|
const entries = lines.filter((l) => ACTIVE_BLOCKER_RE.test(l));
|
|
1784
1745
|
return entries.slice(-limit);
|
|
1785
1746
|
}
|
|
@@ -1791,18 +1752,18 @@ ${reason}
|
|
|
1791
1752
|
`, "utf-8");
|
|
1792
1753
|
}
|
|
1793
1754
|
function isHardStopActive() {
|
|
1794
|
-
return
|
|
1755
|
+
return existsSync(HARD_STOP_PATH);
|
|
1795
1756
|
}
|
|
1796
1757
|
function readHardStopReason() {
|
|
1797
|
-
if (!
|
|
1758
|
+
if (!existsSync(HARD_STOP_PATH)) return null;
|
|
1798
1759
|
try {
|
|
1799
|
-
return
|
|
1760
|
+
return readFileSync(HARD_STOP_PATH, "utf-8").trim();
|
|
1800
1761
|
} catch {
|
|
1801
1762
|
return null;
|
|
1802
1763
|
}
|
|
1803
1764
|
}
|
|
1804
1765
|
function clearHardStop() {
|
|
1805
|
-
if (!
|
|
1766
|
+
if (!existsSync(HARD_STOP_PATH)) return false;
|
|
1806
1767
|
rmSync(HARD_STOP_PATH, { force: true });
|
|
1807
1768
|
return true;
|
|
1808
1769
|
}
|
|
@@ -2060,543 +2021,17 @@ ${ko.recap.done}`));
|
|
|
2060
2021
|
});
|
|
2061
2022
|
}
|
|
2062
2023
|
|
|
2063
|
-
// src/commands/sync.ts
|
|
2064
|
-
import chalk7 from "chalk";
|
|
2065
|
-
import fs7 from "fs";
|
|
2066
|
-
import path8 from "path";
|
|
2067
|
-
import inquirer4 from "inquirer";
|
|
2068
|
-
|
|
2069
|
-
// src/lib/drift.ts
|
|
2070
|
-
import fs5 from "fs";
|
|
2071
|
-
import path6 from "path";
|
|
2072
|
-
|
|
2073
|
-
// src/lib/git-repo.ts
|
|
2074
|
-
import { execFileSync } from "child_process";
|
|
2075
|
-
function getGitRoot(cwd = process.cwd()) {
|
|
2076
|
-
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2077
|
-
encoding: "utf-8",
|
|
2078
|
-
cwd,
|
|
2079
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2080
|
-
}).trim();
|
|
2081
|
-
}
|
|
2082
|
-
function gitOut(args, cwd) {
|
|
2083
|
-
return execFileSync("git", args, {
|
|
2084
|
-
encoding: "utf-8",
|
|
2085
|
-
cwd,
|
|
2086
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2087
|
-
});
|
|
2088
|
-
}
|
|
2089
|
-
function gitRun(args, cwd) {
|
|
2090
|
-
execFileSync("git", args, { stdio: "pipe", cwd });
|
|
2091
|
-
}
|
|
2092
|
-
function getExecErrorMessage(err) {
|
|
2093
|
-
if (err && typeof err === "object" && "stderr" in err) {
|
|
2094
|
-
const stderr = err.stderr;
|
|
2095
|
-
if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
|
|
2096
|
-
if (typeof stderr === "string") return stderr.trim();
|
|
2097
|
-
}
|
|
2098
|
-
return err instanceof Error ? err.message : String(err);
|
|
2099
|
-
}
|
|
2100
|
-
function hasGitRemote(cwd) {
|
|
2101
|
-
try {
|
|
2102
|
-
return gitOut(["remote"], cwd).trim().length > 0;
|
|
2103
|
-
} catch {
|
|
2104
|
-
return false;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
function countLocalCommits(cwd) {
|
|
2108
|
-
try {
|
|
2109
|
-
const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
|
|
2110
|
-
return parseInt(out, 10) || 0;
|
|
2111
|
-
} catch {
|
|
2112
|
-
return 0;
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// src/lib/drift.ts
|
|
2117
|
-
function normalizeForCompare(s) {
|
|
2118
|
-
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
|
|
2119
|
-
}
|
|
2120
|
-
function checkRuleDrift(rootDir) {
|
|
2121
|
-
const rulesPath = path6.join(rootDir, "RULES.md");
|
|
2122
|
-
if (!fs5.existsSync(rulesPath)) return { checked: false, results: [] };
|
|
2123
|
-
const rulesContent = fs5.readFileSync(rulesPath, "utf-8");
|
|
2124
|
-
const sections = parseRulesMd(rulesContent);
|
|
2125
|
-
const projectName = deriveProjectName(rulesContent);
|
|
2126
|
-
const results = [];
|
|
2127
|
-
for (const target of SYNC_TARGETS) {
|
|
2128
|
-
const fullPath = path6.join(rootDir, target.path);
|
|
2129
|
-
if (!fs5.existsSync(fullPath)) {
|
|
2130
|
-
results.push({ path: target.path, status: "missing" });
|
|
2131
|
-
continue;
|
|
2132
|
-
}
|
|
2133
|
-
const expected = normalizeForCompare(target.generate(sections, projectName));
|
|
2134
|
-
const actual = normalizeForCompare(fs5.readFileSync(fullPath, "utf-8"));
|
|
2135
|
-
results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
|
|
2136
|
-
}
|
|
2137
|
-
return { checked: true, results };
|
|
2138
|
-
}
|
|
2139
|
-
var CONTEXT_GIT_MARKER = "vhk-context-git";
|
|
2140
|
-
var CONTEXT_PATH = ".vhk/context.md";
|
|
2141
|
-
function extractContextSha(content) {
|
|
2142
|
-
const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
|
|
2143
|
-
return m ? m[1] : null;
|
|
2144
|
-
}
|
|
2145
|
-
var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
|
|
2146
|
-
function contextSourcesChanged(generatedSha, rootDir) {
|
|
2147
|
-
const content = gitOut(
|
|
2148
|
-
["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
|
|
2149
|
-
rootDir
|
|
2150
|
-
).trim();
|
|
2151
|
-
if (content) return true;
|
|
2152
|
-
const structural = gitOut(
|
|
2153
|
-
["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
|
|
2154
|
-
rootDir
|
|
2155
|
-
).trim();
|
|
2156
|
-
return structural.length > 0;
|
|
2157
|
-
}
|
|
2158
|
-
function checkContextDrift(rootDir) {
|
|
2159
|
-
const ctxPath = path6.join(rootDir, CONTEXT_PATH);
|
|
2160
|
-
if (!fs5.existsSync(ctxPath)) return { checked: false, stale: false };
|
|
2161
|
-
const generatedSha = extractContextSha(fs5.readFileSync(ctxPath, "utf-8"));
|
|
2162
|
-
if (!generatedSha) return { checked: false, stale: false };
|
|
2163
|
-
let currentSha;
|
|
2164
|
-
try {
|
|
2165
|
-
currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
|
|
2166
|
-
} catch {
|
|
2167
|
-
return { checked: false, stale: false };
|
|
2168
|
-
}
|
|
2169
|
-
if (!currentSha) return { checked: false, stale: false };
|
|
2170
|
-
if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
|
|
2171
|
-
return { checked: true, stale: false, generatedSha, currentSha };
|
|
2172
|
-
}
|
|
2173
|
-
let stale;
|
|
2174
|
-
try {
|
|
2175
|
-
stale = contextSourcesChanged(generatedSha, rootDir);
|
|
2176
|
-
} catch {
|
|
2177
|
-
return { checked: false, stale: false };
|
|
2178
|
-
}
|
|
2179
|
-
return { checked: true, stale, generatedSha, currentSha };
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
// src/lib/backup.ts
|
|
2183
|
-
import fs6 from "fs";
|
|
2184
|
-
import path7 from "path";
|
|
2185
|
-
var BACKUPS_REL = path7.join(".vhk", "backups");
|
|
2186
|
-
var VHK_GITIGNORE_REL = path7.join(".vhk", ".gitignore");
|
|
2187
|
-
function fsSafeStamp(d) {
|
|
2188
|
-
return d.toISOString().replace(/[:.]/g, "-");
|
|
2189
|
-
}
|
|
2190
|
-
function ensureVhkIgnored(rootDir, ...entries) {
|
|
2191
|
-
const giPath = path7.join(rootDir, VHK_GITIGNORE_REL);
|
|
2192
|
-
fs6.mkdirSync(path7.dirname(giPath), { recursive: true });
|
|
2193
|
-
let content = fs6.existsSync(giPath) ? fs6.readFileSync(giPath, "utf-8") : "";
|
|
2194
|
-
const present = new Set(content.split("\n").map((l) => l.trim().replace(/\/$/, "")));
|
|
2195
|
-
const missing = entries.filter((e) => !present.has(e.trim().replace(/\/$/, "")));
|
|
2196
|
-
if (missing.length === 0) return;
|
|
2197
|
-
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
2198
|
-
content += missing.join("\n") + "\n";
|
|
2199
|
-
fs6.writeFileSync(giPath, content, "utf-8");
|
|
2200
|
-
}
|
|
2201
|
-
function walkRelFiles(baseDir, cur = baseDir) {
|
|
2202
|
-
const out = [];
|
|
2203
|
-
for (const entry of fs6.readdirSync(cur)) {
|
|
2204
|
-
const full = path7.join(cur, entry);
|
|
2205
|
-
if (fs6.statSync(full).isDirectory()) {
|
|
2206
|
-
out.push(...walkRelFiles(baseDir, full));
|
|
2207
|
-
} else {
|
|
2208
|
-
out.push(path7.relative(baseDir, full).split(path7.sep).join("/"));
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
return out;
|
|
2212
|
-
}
|
|
2213
|
-
function saveBackup(files, rootDir, stamp) {
|
|
2214
|
-
const baseId = stamp ?? fsSafeStamp(/* @__PURE__ */ new Date());
|
|
2215
|
-
let id = baseId;
|
|
2216
|
-
let n = 1;
|
|
2217
|
-
while (fs6.existsSync(path7.join(rootDir, BACKUPS_REL, id))) {
|
|
2218
|
-
id = `${baseId}-${String(n++).padStart(3, "0")}`;
|
|
2219
|
-
}
|
|
2220
|
-
const backupDir = path7.join(rootDir, BACKUPS_REL, id);
|
|
2221
|
-
const saved = [];
|
|
2222
|
-
for (const rel of files) {
|
|
2223
|
-
const src = path7.join(rootDir, rel);
|
|
2224
|
-
if (!fs6.existsSync(src)) continue;
|
|
2225
|
-
const dest = path7.join(backupDir, rel);
|
|
2226
|
-
fs6.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
2227
|
-
fs6.copyFileSync(src, dest);
|
|
2228
|
-
saved.push(rel);
|
|
2229
|
-
}
|
|
2230
|
-
ensureVhkIgnored(rootDir, "backups/");
|
|
2231
|
-
return { id, dir: backupDir, files: saved };
|
|
2232
|
-
}
|
|
2233
|
-
function backupOrderKey(id) {
|
|
2234
|
-
const m = /^(.*Z)(?:-(\d+))?$/.exec(id);
|
|
2235
|
-
return m ? [m[1], m[2] ? parseInt(m[2], 10) : 0] : [id, 0];
|
|
2236
|
-
}
|
|
2237
|
-
function listBackups(rootDir) {
|
|
2238
|
-
const root = path7.join(rootDir, BACKUPS_REL);
|
|
2239
|
-
if (!fs6.existsSync(root)) return [];
|
|
2240
|
-
return fs6.readdirSync(root).filter((e) => fs6.statSync(path7.join(root, e)).isDirectory()).sort((a, b) => {
|
|
2241
|
-
const [ba, na] = backupOrderKey(a);
|
|
2242
|
-
const [bb, nb] = backupOrderKey(b);
|
|
2243
|
-
if (ba !== bb) return ba < bb ? 1 : -1;
|
|
2244
|
-
return nb - na;
|
|
2245
|
-
}).map((id) => {
|
|
2246
|
-
const dir = path7.join(root, id);
|
|
2247
|
-
return { id, dir, files: walkRelFiles(dir) };
|
|
2248
|
-
});
|
|
2249
|
-
}
|
|
2250
|
-
function restoreBackup(id, rootDir) {
|
|
2251
|
-
const backupDir = path7.join(rootDir, BACKUPS_REL, id);
|
|
2252
|
-
if (!fs6.existsSync(backupDir)) {
|
|
2253
|
-
throw new Error(`\uBC31\uC5C5 \uC5C6\uC74C: ${id}`);
|
|
2254
|
-
}
|
|
2255
|
-
const rels = walkRelFiles(backupDir);
|
|
2256
|
-
for (const rel of rels) {
|
|
2257
|
-
const src = path7.join(backupDir, rel);
|
|
2258
|
-
const dest = path7.join(rootDir, rel);
|
|
2259
|
-
fs6.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
2260
|
-
fs6.copyFileSync(src, dest);
|
|
2261
|
-
}
|
|
2262
|
-
return rels;
|
|
2263
|
-
}
|
|
2264
|
-
function pruneBackups(keepN, rootDir) {
|
|
2265
|
-
const all = listBackups(rootDir);
|
|
2266
|
-
const toDelete = all.slice(Math.max(0, keepN));
|
|
2267
|
-
for (const b of toDelete) {
|
|
2268
|
-
fs6.rmSync(b.dir, { recursive: true, force: true });
|
|
2269
|
-
}
|
|
2270
|
-
return toDelete.map((b) => b.id);
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
// src/commands/sync.ts
|
|
2274
|
-
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
2275
|
-
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
2276
|
-
function findUnmappedSections(sections) {
|
|
2277
|
-
const allKeys = [...CURSORRULES_KEYS, ...CLAUDE_MD_KEYS];
|
|
2278
|
-
return sections.filter((s) => s.title !== PREAMBLE_TITLE && !allKeys.some((k) => s.title.includes(k))).map((s) => s.title);
|
|
2279
|
-
}
|
|
2280
|
-
function parseRulesMd(content) {
|
|
2281
|
-
const sections = [];
|
|
2282
|
-
const lines = content.split("\n");
|
|
2283
|
-
let currentTitle = "";
|
|
2284
|
-
let currentContent = [];
|
|
2285
|
-
for (const line of lines) {
|
|
2286
|
-
if (line.startsWith("## ")) {
|
|
2287
|
-
if (currentTitle) {
|
|
2288
|
-
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
2289
|
-
}
|
|
2290
|
-
currentTitle = line.replace("## ", "").trim();
|
|
2291
|
-
currentContent = [];
|
|
2292
|
-
} else {
|
|
2293
|
-
currentContent.push(line);
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
if (currentTitle) {
|
|
2297
|
-
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
2298
|
-
}
|
|
2299
|
-
return sections;
|
|
2300
|
-
}
|
|
2301
|
-
function buildCodingDoc(headerTitle, sections, projectName) {
|
|
2302
|
-
const codingSections = sections.filter(
|
|
2303
|
-
(s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
|
|
2304
|
-
);
|
|
2305
|
-
const lines = [
|
|
2306
|
-
`# ${projectName} \u2014 ${headerTitle}`,
|
|
2307
|
-
"",
|
|
2308
|
-
"> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
|
|
2309
|
-
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
2310
|
-
"",
|
|
2311
|
-
"## \uD544\uC218 \uCC38\uC870",
|
|
2312
|
-
"- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md \xB7 RULES.md",
|
|
2313
|
-
""
|
|
2314
|
-
];
|
|
2315
|
-
for (const section of codingSections) {
|
|
2316
|
-
lines.push(`## ${section.title}`);
|
|
2317
|
-
lines.push(section.content);
|
|
2318
|
-
lines.push("");
|
|
2319
|
-
}
|
|
2320
|
-
return lines.join("\n");
|
|
2321
|
-
}
|
|
2322
|
-
function toCursorrules(sections, projectName) {
|
|
2323
|
-
return buildCodingDoc("Cursor Rules", sections, projectName);
|
|
2324
|
-
}
|
|
2325
|
-
function toWindsurfrules(sections, projectName) {
|
|
2326
|
-
return buildCodingDoc("Windsurf Rules", sections, projectName);
|
|
2327
|
-
}
|
|
2328
|
-
function toCopilotInstructions(sections, projectName) {
|
|
2329
|
-
return buildCodingDoc("GitHub Copilot Instructions", sections, projectName);
|
|
2330
|
-
}
|
|
2331
|
-
var ANTIGRAVITY_CHAR_LIMIT = 12e3;
|
|
2332
|
-
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";
|
|
2333
|
-
function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
|
|
2334
|
-
if (Buffer.byteLength(content, "utf8") <= limit) return content;
|
|
2335
|
-
const SAFETY = 200;
|
|
2336
|
-
const budget = limit - Buffer.byteLength(ANTIGRAVITY_TRUNCATE_MARKER, "utf8") - SAFETY;
|
|
2337
|
-
let lo = 0;
|
|
2338
|
-
let hi = content.length;
|
|
2339
|
-
while (lo < hi) {
|
|
2340
|
-
const mid = lo + hi + 1 >> 1;
|
|
2341
|
-
if (Buffer.byteLength(content.slice(0, mid), "utf8") <= budget) lo = mid;
|
|
2342
|
-
else hi = mid - 1;
|
|
2343
|
-
}
|
|
2344
|
-
const charCut = lo;
|
|
2345
|
-
let cut = content.lastIndexOf("\n## ", charCut);
|
|
2346
|
-
if (cut < charCut * 0.5) {
|
|
2347
|
-
const nl = content.lastIndexOf("\n", charCut);
|
|
2348
|
-
cut = nl > 0 ? nl : charCut;
|
|
2349
|
-
}
|
|
2350
|
-
return content.slice(0, cut).trimEnd() + ANTIGRAVITY_TRUNCATE_MARKER;
|
|
2351
|
-
}
|
|
2352
|
-
function toAntigravityRules(sections, projectName) {
|
|
2353
|
-
return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
|
|
2354
|
-
}
|
|
2355
|
-
var CLAUDE_AUTOGEN_BANNER = "> \u26A1 \uC544\uB798 \uADDC\uCE59 \uC139\uC158\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.";
|
|
2356
|
-
function toClaudeMd(sections, existing) {
|
|
2357
|
-
const recordSections = sections.filter(
|
|
2358
|
-
(s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
|
|
2359
|
-
);
|
|
2360
|
-
const cleaned = existing.split("\n").filter((line) => line.trim() !== CLAUDE_AUTOGEN_BANNER).join("\n");
|
|
2361
|
-
const statusMatch = cleaned.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
|
|
2362
|
-
const statusSection = statusMatch ? statusMatch[0].trimEnd() : "";
|
|
2363
|
-
const header = cleaned.split("## ")[0].trim();
|
|
2364
|
-
const lines = [
|
|
2365
|
-
header,
|
|
2366
|
-
"",
|
|
2367
|
-
statusSection,
|
|
2368
|
-
"",
|
|
2369
|
-
CLAUDE_AUTOGEN_BANNER,
|
|
2370
|
-
""
|
|
2371
|
-
];
|
|
2372
|
-
for (const section of recordSections) {
|
|
2373
|
-
lines.push(`## ${section.title}`);
|
|
2374
|
-
lines.push(section.content);
|
|
2375
|
-
lines.push("");
|
|
2376
|
-
}
|
|
2377
|
-
return lines.join("\n");
|
|
2378
|
-
}
|
|
2379
|
-
function toAgentsMd(sections, projectName) {
|
|
2380
|
-
const codingSections = sections.filter((s) => CURSORRULES_KEYS.some((k) => s.title.includes(k)));
|
|
2381
|
-
const recordSections = sections.filter((s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k)));
|
|
2382
|
-
const lines = [
|
|
2383
|
-
`# ${projectName} \u2014 AGENTS.md (\uC5D0\uC774\uC804\uD2B8 \uC791\uB3D9 \uADDC\uC57D)`,
|
|
2384
|
-
"",
|
|
2385
|
-
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
2386
|
-
"> \uBE60\uB978 \uC2DC\uC791(\uD1A0\uD070 \uC808\uAC10): `docs/context/agent-compact.md` \uB97C \uBA3C\uC800 \uC77D\uC73C\uC138\uC694.",
|
|
2387
|
-
"",
|
|
2388
|
-
"## Loop Protocol",
|
|
2389
|
-
"- \uB8E8\uD504: `context \u2192 goal next \u2192 \uC791\uC5C5 \u2192 goal check \u2192 goal done`",
|
|
2390
|
-
"- \uC791\uC5C5 \uC2DC\uC791 \uC2DC `.vhk/HARD_STOP` \uD655\uC778 \u2014 \uC788\uC73C\uBA74 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC989\uC2DC \uC911\uB2E8.",
|
|
2391
|
-
"- active goal \uB9CC \uC791\uC5C5. `docs/state`(next-task/blockers/learnings)\uB294 SoT, append-only.",
|
|
2392
|
-
"- \uAC8C\uC774\uD2B8(tsc / test:run / build) \uD1B5\uACFC\uD574\uC57C\uB9CC `vhk goal done`.",
|
|
2393
|
-
""
|
|
2394
|
-
];
|
|
2395
|
-
for (const section of codingSections) {
|
|
2396
|
-
lines.push(`## ${section.title}`);
|
|
2397
|
-
lines.push(section.content);
|
|
2398
|
-
lines.push("");
|
|
2399
|
-
}
|
|
2400
|
-
for (const section of recordSections) {
|
|
2401
|
-
lines.push(`## ${section.title}`);
|
|
2402
|
-
lines.push(section.content);
|
|
2403
|
-
lines.push("");
|
|
2404
|
-
}
|
|
2405
|
-
return lines.join("\n");
|
|
2406
|
-
}
|
|
2407
|
-
function deriveProjectName(rulesContent) {
|
|
2408
|
-
const firstLine = rulesContent.split("\n")[0];
|
|
2409
|
-
return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
2410
|
-
}
|
|
2411
|
-
var SYNC_TARGETS = [
|
|
2412
|
-
{ path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
|
|
2413
|
-
{ path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
|
|
2414
|
-
{ path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
|
|
2415
|
-
{ path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone },
|
|
2416
|
-
// AGENTS.md — 6번째 타겟. 항목 1개 추가로 sync·드리프트·백업 가드가 자동 반영된다.
|
|
2417
|
-
{ path: "AGENTS.md", generate: toAgentsMd, doneMessage: ko.sync.agentsDone }
|
|
2418
|
-
];
|
|
2419
|
-
var BACKUP_KEEP = 10;
|
|
2420
|
-
var SYNCED_MARKER_REL = path8.join(".vhk", ".synced");
|
|
2421
|
-
function buildSyncPlan(rootDir, sections, projectName) {
|
|
2422
|
-
const plan = [];
|
|
2423
|
-
for (const target of SYNC_TARGETS) {
|
|
2424
|
-
const fullPath = path8.join(rootDir, target.path);
|
|
2425
|
-
const exists = fs7.existsSync(fullPath);
|
|
2426
|
-
const newContent = target.generate(sections, projectName);
|
|
2427
|
-
const drift = exists ? normalizeForCompare(fs7.readFileSync(fullPath, "utf-8")) !== normalizeForCompare(newContent) : false;
|
|
2428
|
-
plan.push({ path: target.path, newContent, doneMessage: target.doneMessage, exists, drift });
|
|
2429
|
-
}
|
|
2430
|
-
const claudePath = path8.join(rootDir, "CLAUDE.md");
|
|
2431
|
-
const claudeExists = fs7.existsSync(claudePath);
|
|
2432
|
-
const existingClaude = claudeExists ? fs7.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
2433
|
-
|
|
2434
|
-
## \uD604\uC7AC \uC0C1\uD0DC
|
|
2435
|
-
- **Phase:** **FILL**
|
|
2436
|
-
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
2437
|
-
- **\uB2E4\uC74C \uC561\uC158:** **FILL**
|
|
2438
|
-
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${localDate()}`;
|
|
2439
|
-
const claudeNew = toClaudeMd(sections, existingClaude);
|
|
2440
|
-
const claudeDrift = claudeExists ? normalizeForCompare(existingClaude) !== normalizeForCompare(claudeNew) : false;
|
|
2441
|
-
plan.push({
|
|
2442
|
-
path: "CLAUDE.md",
|
|
2443
|
-
newContent: claudeNew,
|
|
2444
|
-
doneMessage: ko.sync.claudeDone,
|
|
2445
|
-
exists: claudeExists,
|
|
2446
|
-
drift: claudeDrift
|
|
2447
|
-
});
|
|
2448
|
-
return plan;
|
|
2449
|
-
}
|
|
2450
|
-
async function syncCore(rootDir, opts, confirmOverwrite) {
|
|
2451
|
-
const rulesContent = fs7.readFileSync(path8.join(rootDir, "RULES.md"), "utf-8");
|
|
2452
|
-
const sections = parseRulesMd(rulesContent);
|
|
2453
|
-
const projectName = deriveProjectName(rulesContent);
|
|
2454
|
-
const plan = buildSyncPlan(rootDir, sections, projectName);
|
|
2455
|
-
const firstSync = !fs7.existsSync(path8.join(rootDir, SYNCED_MARKER_REL));
|
|
2456
|
-
const unmapped = findUnmappedSections(sections);
|
|
2457
|
-
if (opts.dryRun) {
|
|
2458
|
-
return {
|
|
2459
|
-
dryRun: true,
|
|
2460
|
-
firstSync,
|
|
2461
|
-
backupId: null,
|
|
2462
|
-
backedUp: [],
|
|
2463
|
-
written: [],
|
|
2464
|
-
skipped: [],
|
|
2465
|
-
truncated: [],
|
|
2466
|
-
plan,
|
|
2467
|
-
unmapped
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
const toBackup = plan.filter((p) => p.exists && (p.drift || firstSync)).map((p) => p.path);
|
|
2471
|
-
let backupId = null;
|
|
2472
|
-
let backedUp = [];
|
|
2473
|
-
if (toBackup.length) {
|
|
2474
|
-
const info = saveBackup(toBackup, rootDir);
|
|
2475
|
-
pruneBackups(BACKUP_KEEP, rootDir);
|
|
2476
|
-
backupId = info.id;
|
|
2477
|
-
backedUp = info.files;
|
|
2478
|
-
}
|
|
2479
|
-
const drifted = plan.filter((p) => p.drift);
|
|
2480
|
-
const overwriteDrift = drifted.length ? await confirmOverwrite(drifted) : true;
|
|
2481
|
-
const written = [];
|
|
2482
|
-
const skipped = [];
|
|
2483
|
-
const truncated = [];
|
|
2484
|
-
for (const item of plan) {
|
|
2485
|
-
if (item.drift && !overwriteDrift) {
|
|
2486
|
-
skipped.push(item.path);
|
|
2487
|
-
continue;
|
|
2488
|
-
}
|
|
2489
|
-
const fullPath = path8.join(rootDir, item.path);
|
|
2490
|
-
fs7.mkdirSync(path8.dirname(fullPath), { recursive: true });
|
|
2491
|
-
fs7.writeFileSync(fullPath, item.newContent, "utf-8");
|
|
2492
|
-
written.push(item.path);
|
|
2493
|
-
if (item.newContent.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
|
|
2494
|
-
truncated.push(item.path);
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
fs7.mkdirSync(path8.join(rootDir, ".vhk"), { recursive: true });
|
|
2498
|
-
fs7.writeFileSync(path8.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
2499
|
-
ensureVhkIgnored(rootDir, ".synced");
|
|
2500
|
-
return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped };
|
|
2501
|
-
}
|
|
2502
|
-
async function sync(opts = {}) {
|
|
2503
|
-
console.log(chalk7.bold(`
|
|
2504
|
-
${ko.sync.title}
|
|
2505
|
-
`));
|
|
2506
|
-
const cwd = process.cwd();
|
|
2507
|
-
const rulesPath = path8.join(cwd, "RULES.md");
|
|
2508
|
-
if (!fs7.existsSync(rulesPath)) {
|
|
2509
|
-
console.log(chalk7.yellow(ko.sync.noRules));
|
|
2510
|
-
console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
2511
|
-
console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
2512
|
-
console.log("");
|
|
2513
|
-
console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
2514
|
-
console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
2515
|
-
console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
2516
|
-
console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
2517
|
-
console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
2518
|
-
console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
const sections = parseRulesMd(fs7.readFileSync(rulesPath, "utf-8"));
|
|
2522
|
-
console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
2523
|
-
const interactive = !!process.stdout.isTTY && !opts.yes;
|
|
2524
|
-
const confirmOverwrite = async (drifted) => {
|
|
2525
|
-
if (!interactive) return true;
|
|
2526
|
-
for (const d of drifted) console.log(chalk7.yellow(` ${ko.sync.driftWarn(d.path)}`));
|
|
2527
|
-
const { confirm } = await inquirer4.prompt([
|
|
2528
|
-
{
|
|
2529
|
-
type: "confirm",
|
|
2530
|
-
name: "confirm",
|
|
2531
|
-
message: ko.sync.driftConfirm(drifted.length),
|
|
2532
|
-
default: false
|
|
2533
|
-
}
|
|
2534
|
-
]);
|
|
2535
|
-
return confirm;
|
|
2536
|
-
};
|
|
2537
|
-
const result = await syncCore(cwd, opts, confirmOverwrite);
|
|
2538
|
-
if (result.unmapped.length) {
|
|
2539
|
-
console.error(
|
|
2540
|
-
chalk7.yellow(
|
|
2541
|
-
` \u26A0\uFE0F ${result.unmapped.length}\uAC1C \uC139\uC158\uC774 \uC5B4\uB290 \uD0C0\uAE43\uC5D0\uB3C4 \uB9E4\uD551 \uC548 \uB3FC \uC0B0\uCD9C\uBB3C\uC5D0\uC11C \uC81C\uC678\uB428: ${result.unmapped.join(", ")}
|
|
2542
|
-
(\uCF54\uB529 \uADDC\uCE59/\uAE30\uC220 \uC2A4\uD0DD/\uCEE4\uBC0B/\uAE30\uB85D \uB4F1 \uD45C\uC900 \uC81C\uBAA9\uC744 \uC4F0\uAC70\uB098, \uC774 \uC139\uC158\uC740 RULES.md \uC5D0\uB9CC \uBCF4\uC874\uB429\uB2C8\uB2E4.)`
|
|
2543
|
-
)
|
|
2544
|
-
);
|
|
2545
|
-
}
|
|
2546
|
-
if (result.dryRun) {
|
|
2547
|
-
console.log(chalk7.cyan(`
|
|
2548
|
-
${ko.sync.dryRunHeader}`));
|
|
2549
|
-
for (const item of result.plan) {
|
|
2550
|
-
console.log(ko.sync.dryRunWouldWrite(item.path, item.exists && item.drift));
|
|
2551
|
-
}
|
|
2552
|
-
const wouldBackup = result.plan.filter((p) => p.exists && (p.drift || result.firstSync)).map((p) => p.path);
|
|
2553
|
-
if (wouldBackup.length) {
|
|
2554
|
-
console.log(chalk7.dim(`
|
|
2555
|
-
\uBC31\uC5C5 \uC608\uC815(${wouldBackup.length}): ${wouldBackup.join(", ")}`));
|
|
2556
|
-
}
|
|
2557
|
-
return;
|
|
2558
|
-
}
|
|
2559
|
-
if (result.backupId) {
|
|
2560
|
-
if (result.firstSync) console.log(chalk7.cyan(` ${ko.sync.firstSync}`));
|
|
2561
|
-
if (!process.stdout.isTTY) {
|
|
2562
|
-
console.log(chalk7.yellow(` ${ko.sync.nonTtyAuto(result.backedUp.length, result.backupId)}`));
|
|
2563
|
-
} else {
|
|
2564
|
-
console.log(chalk7.cyan(` ${ko.sync.backupSaved(result.backedUp.length, result.backupId)}`));
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
for (const p of result.written) {
|
|
2568
|
-
const item = result.plan.find((i) => i.path === p);
|
|
2569
|
-
if (item) console.log(chalk7.green(` ${item.doneMessage}`));
|
|
2570
|
-
}
|
|
2571
|
-
for (const _ of result.truncated) {
|
|
2572
|
-
console.log(chalk7.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
|
|
2573
|
-
}
|
|
2574
|
-
for (const p of result.skipped) {
|
|
2575
|
-
console.log(chalk7.gray(` ${ko.sync.skipped(p)}`));
|
|
2576
|
-
}
|
|
2577
|
-
console.log(chalk7.bold.green(`
|
|
2578
|
-
${ko.sync.done}`));
|
|
2579
|
-
console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
|
|
2580
|
-
console.log(chalk7.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
2581
|
-
console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
2582
|
-
printNextStep({
|
|
2583
|
-
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
2584
|
-
command: "vhk \uC810\uAC80",
|
|
2585
|
-
cursorHint: "\uADDC\uCE59 \uC810\uAC80\uD574\uC918"
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
2024
|
// src/commands/check.ts
|
|
2590
|
-
import
|
|
2591
|
-
import
|
|
2592
|
-
import
|
|
2025
|
+
import chalk8 from "chalk";
|
|
2026
|
+
import path7 from "path";
|
|
2027
|
+
import fs6 from "fs";
|
|
2593
2028
|
|
|
2594
2029
|
// src/lib/rules-parser.ts
|
|
2595
|
-
import
|
|
2596
|
-
import
|
|
2030
|
+
import fs5 from "fs";
|
|
2031
|
+
import path6 from "path";
|
|
2597
2032
|
function parseRules(rulesPath) {
|
|
2598
|
-
if (!
|
|
2599
|
-
const content =
|
|
2033
|
+
if (!fs5.existsSync(rulesPath)) return [];
|
|
2034
|
+
const content = fs5.readFileSync(rulesPath, "utf-8");
|
|
2600
2035
|
const lines = content.split("\n");
|
|
2601
2036
|
const rules = [];
|
|
2602
2037
|
let currentSection = "";
|
|
@@ -2661,17 +2096,17 @@ function createNamingRule(id, section, desc, convention) {
|
|
|
2661
2096
|
description: desc,
|
|
2662
2097
|
check: (cwd) => {
|
|
2663
2098
|
const violations = [];
|
|
2664
|
-
const srcDir =
|
|
2665
|
-
if (!
|
|
2099
|
+
const srcDir = path6.join(cwd, "src");
|
|
2100
|
+
if (!fs5.existsSync(srcDir)) return violations;
|
|
2666
2101
|
walkFiles(srcDir, (filePath) => {
|
|
2667
|
-
const name =
|
|
2102
|
+
const name = path6.basename(filePath, path6.extname(filePath));
|
|
2668
2103
|
if (convention === "kebab-case" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
|
2669
2104
|
if (!["index", "vite.config", "tsconfig"].includes(name)) {
|
|
2670
2105
|
violations.push({
|
|
2671
2106
|
ruleId: id,
|
|
2672
2107
|
severity: "warning",
|
|
2673
2108
|
message: `\uD30C\uC77C\uBA85\uC774 kebab-case\uAC00 \uC544\uB2D8: ${name}`,
|
|
2674
|
-
file:
|
|
2109
|
+
file: path6.relative(cwd, filePath)
|
|
2675
2110
|
});
|
|
2676
2111
|
}
|
|
2677
2112
|
}
|
|
@@ -2687,8 +2122,8 @@ function createStructureRule(id, section, desc, expectedPath) {
|
|
|
2687
2122
|
type: "structure",
|
|
2688
2123
|
description: desc,
|
|
2689
2124
|
check: (cwd) => {
|
|
2690
|
-
const fullPath =
|
|
2691
|
-
if (!
|
|
2125
|
+
const fullPath = path6.join(cwd, expectedPath);
|
|
2126
|
+
if (!fs5.existsSync(fullPath)) {
|
|
2692
2127
|
return [{
|
|
2693
2128
|
ruleId: id,
|
|
2694
2129
|
severity: "error",
|
|
@@ -2708,11 +2143,11 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2708
2143
|
pattern: new RegExp(escapeRegex(pattern), "i"),
|
|
2709
2144
|
check: (cwd) => {
|
|
2710
2145
|
const violations = [];
|
|
2711
|
-
const srcDir =
|
|
2712
|
-
if (!
|
|
2146
|
+
const srcDir = path6.join(cwd, "src");
|
|
2147
|
+
if (!fs5.existsSync(srcDir)) return violations;
|
|
2713
2148
|
const regex = new RegExp(escapeRegex(pattern), "i");
|
|
2714
2149
|
walkFiles(srcDir, (filePath) => {
|
|
2715
|
-
const fileContent =
|
|
2150
|
+
const fileContent = fs5.readFileSync(filePath, "utf-8");
|
|
2716
2151
|
const fileLines = fileContent.split("\n");
|
|
2717
2152
|
fileLines.forEach((line, idx) => {
|
|
2718
2153
|
if (regex.test(line)) {
|
|
@@ -2720,7 +2155,7 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2720
2155
|
ruleId: id,
|
|
2721
2156
|
severity: type === "banned" ? "error" : "warning",
|
|
2722
2157
|
message: type === "banned" ? `\uAE08\uC9C0 \uD328\uD134 \uBC1C\uACAC: \`${pattern}\`` : `\uD544\uC218 \uD328\uD134 \uB204\uB77D: \`${pattern}\``,
|
|
2723
|
-
file:
|
|
2158
|
+
file: path6.relative(cwd, filePath),
|
|
2724
2159
|
line: idx + 1
|
|
2725
2160
|
});
|
|
2726
2161
|
}
|
|
@@ -2732,9 +2167,9 @@ function createContentRule(id, section, desc, pattern, type) {
|
|
|
2732
2167
|
};
|
|
2733
2168
|
}
|
|
2734
2169
|
function walkFiles(dir, callback) {
|
|
2735
|
-
const entries =
|
|
2170
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
2736
2171
|
for (const entry of entries) {
|
|
2737
|
-
const fullPath =
|
|
2172
|
+
const fullPath = path6.join(dir, entry.name);
|
|
2738
2173
|
if (entry.isDirectory()) {
|
|
2739
2174
|
if (!["node_modules", ".git", "dist", ".next"].includes(entry.name)) {
|
|
2740
2175
|
walkFiles(fullPath, callback);
|
|
@@ -2749,17 +2184,18 @@ function escapeRegex(str) {
|
|
|
2749
2184
|
}
|
|
2750
2185
|
|
|
2751
2186
|
// src/commands/goal.ts
|
|
2752
|
-
import { existsSync as
|
|
2753
|
-
import { join as
|
|
2754
|
-
import
|
|
2187
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3 } from "fs";
|
|
2188
|
+
import { join as join4 } from "path";
|
|
2189
|
+
import chalk7 from "chalk";
|
|
2755
2190
|
|
|
2756
2191
|
// src/lib/goal-frontmatter.ts
|
|
2757
|
-
import { existsSync as
|
|
2758
|
-
import { join as
|
|
2192
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
2193
|
+
import { join as join3 } from "path";
|
|
2759
2194
|
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
2760
2195
|
function parseFrontmatter(content) {
|
|
2761
|
-
const
|
|
2762
|
-
|
|
2196
|
+
const normalized = stripBom(content);
|
|
2197
|
+
const m = normalized.match(FRONTMATTER_RE);
|
|
2198
|
+
if (!m) return { frontmatter: {}, body: normalized };
|
|
2763
2199
|
const fm = parseSimpleYaml(m[1]);
|
|
2764
2200
|
const body = (m[2] ?? "").replace(/^\r?\n+/, "");
|
|
2765
2201
|
return { frontmatter: fm, body };
|
|
@@ -2787,9 +2223,9 @@ function parseSimpleYaml(yaml) {
|
|
|
2787
2223
|
return out;
|
|
2788
2224
|
}
|
|
2789
2225
|
function parseGoalFile(filePath) {
|
|
2790
|
-
if (!
|
|
2226
|
+
if (!existsSync2(filePath)) return null;
|
|
2791
2227
|
try {
|
|
2792
|
-
const content =
|
|
2228
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2793
2229
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
2794
2230
|
return { filePath, frontmatter, body };
|
|
2795
2231
|
} catch {
|
|
@@ -2797,7 +2233,7 @@ function parseGoalFile(filePath) {
|
|
|
2797
2233
|
}
|
|
2798
2234
|
}
|
|
2799
2235
|
function listGoals(goalsDir) {
|
|
2800
|
-
if (!
|
|
2236
|
+
if (!existsSync2(goalsDir)) return [];
|
|
2801
2237
|
let entries;
|
|
2802
2238
|
try {
|
|
2803
2239
|
entries = readdirSync(goalsDir);
|
|
@@ -2808,7 +2244,7 @@ function listGoals(goalsDir) {
|
|
|
2808
2244
|
for (const name of entries) {
|
|
2809
2245
|
if (!name.endsWith(".md")) continue;
|
|
2810
2246
|
if (name === "_meta.md") continue;
|
|
2811
|
-
const fp =
|
|
2247
|
+
const fp = join3(goalsDir, name);
|
|
2812
2248
|
try {
|
|
2813
2249
|
if (!statSync(fp).isFile()) continue;
|
|
2814
2250
|
} catch {
|
|
@@ -2823,6 +2259,36 @@ function listGoals(goalsDir) {
|
|
|
2823
2259
|
parsed.sort((a, b) => a.frontmatter.id - b.frontmatter.id);
|
|
2824
2260
|
return parsed;
|
|
2825
2261
|
}
|
|
2262
|
+
function findSkippedGoalFiles(goalsDir) {
|
|
2263
|
+
if (!existsSync2(goalsDir)) return [];
|
|
2264
|
+
let entries;
|
|
2265
|
+
try {
|
|
2266
|
+
entries = readdirSync(goalsDir);
|
|
2267
|
+
} catch {
|
|
2268
|
+
return [];
|
|
2269
|
+
}
|
|
2270
|
+
const out = [];
|
|
2271
|
+
for (const name of entries) {
|
|
2272
|
+
if (!name.endsWith(".md")) continue;
|
|
2273
|
+
if (name === "_meta.md") continue;
|
|
2274
|
+
const fp = join3(goalsDir, name);
|
|
2275
|
+
try {
|
|
2276
|
+
if (!statSync(fp).isFile()) continue;
|
|
2277
|
+
} catch {
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
const g = parseGoalFile(fp);
|
|
2281
|
+
if (!g) continue;
|
|
2282
|
+
const fm = g.frontmatter;
|
|
2283
|
+
if (fm.type === "goal" && typeof fm.id === "number") continue;
|
|
2284
|
+
if (fm.type === "meta") continue;
|
|
2285
|
+
const looksLikeGoal = fm.type === "goal" || "id" in fm || "status" in fm || "priority" in fm || "title" in fm;
|
|
2286
|
+
if (!looksLikeGoal) continue;
|
|
2287
|
+
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)";
|
|
2288
|
+
out.push({ file: name, reason });
|
|
2289
|
+
}
|
|
2290
|
+
return out;
|
|
2291
|
+
}
|
|
2826
2292
|
function findDuplicateIds(goals) {
|
|
2827
2293
|
const counts = /* @__PURE__ */ new Map();
|
|
2828
2294
|
for (const g of goals) {
|
|
@@ -2900,13 +2366,15 @@ function resolveGoalId(optId, goals) {
|
|
|
2900
2366
|
return selectActiveId(goals);
|
|
2901
2367
|
}
|
|
2902
2368
|
async function goalList() {
|
|
2903
|
-
console.log(
|
|
2369
|
+
console.log(chalk7.bold(`
|
|
2904
2370
|
${ko.goal.listTitle}
|
|
2905
2371
|
`));
|
|
2906
2372
|
const goals = listGoals(GOALS_DIR);
|
|
2373
|
+
const skipped = findSkippedGoalFiles(GOALS_DIR);
|
|
2907
2374
|
if (goals.length === 0) {
|
|
2908
|
-
console.log(
|
|
2909
|
-
console.log(
|
|
2375
|
+
console.log(chalk7.yellow(" \u{1F4ED} goals/ \uB514\uB809\uD1A0\uB9AC\uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2376
|
+
console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2377
|
+
printSkippedGoalWarnings(skipped);
|
|
2910
2378
|
return;
|
|
2911
2379
|
}
|
|
2912
2380
|
for (const g of goals) {
|
|
@@ -2923,22 +2391,33 @@ ${ko.goal.listTitle}
|
|
|
2923
2391
|
const dups = findDuplicateIds(goals);
|
|
2924
2392
|
if (dups.length > 0) {
|
|
2925
2393
|
console.log("");
|
|
2926
|
-
console.log(
|
|
2394
|
+
console.log(chalk7.yellow(` ${ko.goal.duplicateId(dups.join(", "))}`));
|
|
2395
|
+
}
|
|
2396
|
+
printSkippedGoalWarnings(skipped);
|
|
2397
|
+
}
|
|
2398
|
+
function printSkippedGoalWarnings(skipped) {
|
|
2399
|
+
if (skipped.length > 0) {
|
|
2400
|
+
console.log("");
|
|
2401
|
+
console.log(chalk7.yellow(` ${ko.goal.skippedFiles(skipped.length)}`));
|
|
2402
|
+
for (const s of skipped) {
|
|
2403
|
+
console.log(chalk7.yellow(` - goals/${s.file}: ${s.reason}`));
|
|
2404
|
+
}
|
|
2405
|
+
console.log(chalk7.dim(" \uD544\uC218: type: goal + \uC22B\uC790 id. \uC2A4\uD0A4\uB9C8 \uC804\uCCB4: goals/_meta.md"));
|
|
2927
2406
|
}
|
|
2928
2407
|
}
|
|
2929
2408
|
async function goalNext() {
|
|
2930
|
-
console.log(
|
|
2409
|
+
console.log(chalk7.bold(`
|
|
2931
2410
|
${ko.goal.nextTitle}
|
|
2932
2411
|
`));
|
|
2933
2412
|
const goals = listGoals(GOALS_DIR);
|
|
2934
2413
|
if (goals.length === 0) {
|
|
2935
|
-
console.log(
|
|
2936
|
-
console.log(
|
|
2414
|
+
console.log(chalk7.yellow(" \u{1F4ED} \uC815\uC758\uB41C goal \uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2415
|
+
console.log(chalk7.dim(" vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694."));
|
|
2937
2416
|
return;
|
|
2938
2417
|
}
|
|
2939
2418
|
const activeId = selectActiveId(goals);
|
|
2940
2419
|
if (activeId === null) {
|
|
2941
|
-
console.log(
|
|
2420
|
+
console.log(chalk7.green(" \u{1F389} \uBAA8\uB4E0 goal \uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4!"));
|
|
2942
2421
|
return;
|
|
2943
2422
|
}
|
|
2944
2423
|
const active = goals.find((g) => g.frontmatter.id === activeId);
|
|
@@ -2958,9 +2437,9 @@ ${ko.goal.nextTitle}
|
|
|
2958
2437
|
""
|
|
2959
2438
|
].join("\n");
|
|
2960
2439
|
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2961
|
-
writeFileSync2(
|
|
2440
|
+
writeFileSync2(join4(STATE_DIR2, "next-task.md"), text, "utf-8");
|
|
2962
2441
|
console.log(
|
|
2963
|
-
|
|
2442
|
+
chalk7.green(
|
|
2964
2443
|
` \u2705 next-task.md \uAC31\uC2E0 \u2014 Goal ${activeId}: ${active.frontmatter.title ?? ""}`
|
|
2965
2444
|
)
|
|
2966
2445
|
);
|
|
@@ -2979,35 +2458,70 @@ version: v0.1
|
|
|
2979
2458
|
## Forbidden Actions (\uC804\uC5ED)
|
|
2980
2459
|
|
|
2981
2460
|
- (\uD574\uB2F9 \uC0AC\uD56D)
|
|
2461
|
+
|
|
2462
|
+
## Goal \uD30C\uC77C \uC2A4\uD0A4\uB9C8 (\uD544\uB3C5 \u2014 VHK-021)
|
|
2463
|
+
|
|
2464
|
+
\`vhk goal list/next/check/done\` \uB294 \`goals/*.md\`(\uC774 \`_meta.md\` \uC81C\uC678) \uC911 \uC544\uB798
|
|
2465
|
+
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
|
|
2466
|
+
\`vhk goal list\` \uAC00 \uACBD\uACE0\uB85C \uC54C\uB824\uC900\uB2E4.
|
|
2467
|
+
|
|
2468
|
+
| \uD544\uB4DC | \uD544\uC218 | \uAC12 |
|
|
2469
|
+
| --- | --- | --- |
|
|
2470
|
+
| \`type\` | \u2705 | \`goal\` (\uBB38\uC790\uC5F4 \uADF8\uB300\uB85C) |
|
|
2471
|
+
| \`id\` | \u2705 | **\uC22B\uC790\uB9CC** (\`1\`, \`2\` \u2026 \u2014 \`G1\` \uAC19\uC740 \uBB38\uC790\uC5F4 \u274C) |
|
|
2472
|
+
| \`status\` | \u2705 | \`NOT_STARTED\` | \`IN_PROGRESS\` | \`DONE\` | \`BLOCKED\` |
|
|
2473
|
+
| \`priority\` | \uAD8C\uC7A5 | \`P0\` | \`P1\` | \`P2\` |
|
|
2474
|
+
| \`title\` | \uAD8C\uC7A5 | \uD55C \uC904 \uC81C\uBAA9 |
|
|
2475
|
+
|
|
2476
|
+
\uD30C\uC77C\uBA85 \uADDC\uCE59: \`goals/<id>-<name>.md\` (\uC608: \`goals/1-login.md\`).
|
|
2477
|
+
|
|
2478
|
+
### \uC0C8 goal \uD15C\uD50C\uB9BF (\uBCF5\uBD99)
|
|
2479
|
+
|
|
2480
|
+
\`\`\`markdown
|
|
2481
|
+
---
|
|
2482
|
+
vhk_format: 1
|
|
2483
|
+
type: goal
|
|
2484
|
+
id: 1
|
|
2485
|
+
title: \uB85C\uADF8\uC778 \uAE30\uB2A5
|
|
2486
|
+
status: NOT_STARTED
|
|
2487
|
+
priority: P0
|
|
2488
|
+
---
|
|
2489
|
+
|
|
2490
|
+
# Goal 1: \uB85C\uADF8\uC778 \uAE30\uB2A5
|
|
2491
|
+
|
|
2492
|
+
## \uBC30\uACBD / \uB3D9\uC791 / Completion Check ...
|
|
2493
|
+
\`\`\`
|
|
2494
|
+
|
|
2495
|
+
\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uB294 \`vhk goal sync\` \uB85C \`scripts/check-goal-<id>.mjs\` \uB97C \uBC31\uD544\uD55C\uB2E4.
|
|
2982
2496
|
`;
|
|
2983
2497
|
var STATE_NEXT_TASK_TEMPLATE = "# Next Task\n\n```\nTASK: (vhk goal next \uB85C \uC790\uB3D9 \uAC31\uC2E0)\n```\n";
|
|
2984
2498
|
var STATE_BLOCKERS_TEMPLATE = "# Blockers\n\n_Append-only. \uD574\uACB0 \uD56D\uBAA9\uC740 ~~\uCDE8\uC18C\uC120~~\uC73C\uB85C \uD45C\uAE30._\n";
|
|
2985
2499
|
var STATE_LEARNINGS_TEMPLATE = "# Learnings\n\n_Append-only. \uD55C \uC904 = \uD55C \uAD50\uD6C8._\n";
|
|
2986
2500
|
async function goalInit() {
|
|
2987
|
-
console.log(
|
|
2501
|
+
console.log(chalk7.bold(`
|
|
2988
2502
|
${ko.goal.initTitle}
|
|
2989
2503
|
`));
|
|
2990
2504
|
const targets = [
|
|
2991
|
-
{ path:
|
|
2992
|
-
{ path:
|
|
2993
|
-
{ path:
|
|
2994
|
-
{ path:
|
|
2505
|
+
{ path: join4(GOALS_DIR, "_meta.md"), content: META_TEMPLATE },
|
|
2506
|
+
{ path: join4(STATE_DIR2, "next-task.md"), content: STATE_NEXT_TASK_TEMPLATE },
|
|
2507
|
+
{ path: join4(STATE_DIR2, "blockers.md"), content: STATE_BLOCKERS_TEMPLATE },
|
|
2508
|
+
{ path: join4(STATE_DIR2, "learnings.md"), content: STATE_LEARNINGS_TEMPLATE }
|
|
2995
2509
|
];
|
|
2996
2510
|
mkdirSync2(GOALS_DIR, { recursive: true });
|
|
2997
2511
|
mkdirSync2(STATE_DIR2, { recursive: true });
|
|
2998
2512
|
let created = 0;
|
|
2999
2513
|
let skipped = 0;
|
|
3000
2514
|
for (const t2 of targets) {
|
|
3001
|
-
if (
|
|
3002
|
-
console.log(
|
|
2515
|
+
if (existsSync3(t2.path)) {
|
|
2516
|
+
console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${t2.path}`));
|
|
3003
2517
|
skipped++;
|
|
3004
2518
|
} else {
|
|
3005
2519
|
writeFileSync2(t2.path, t2.content, "utf-8");
|
|
3006
|
-
console.log(
|
|
2520
|
+
console.log(chalk7.green(` \u2713 created: ${t2.path}`));
|
|
3007
2521
|
created++;
|
|
3008
2522
|
}
|
|
3009
2523
|
}
|
|
3010
|
-
console.log(
|
|
2524
|
+
console.log(chalk7.bold(`
|
|
3011
2525
|
\u{1F4CA} created=${created} skipped=${skipped}`));
|
|
3012
2526
|
if (created > 0) {
|
|
3013
2527
|
printNextStep({
|
|
@@ -3018,10 +2532,10 @@ ${ko.goal.initTitle}
|
|
|
3018
2532
|
}
|
|
3019
2533
|
}
|
|
3020
2534
|
function findGateScript(id) {
|
|
3021
|
-
const mjs =
|
|
3022
|
-
if (
|
|
3023
|
-
const sh =
|
|
3024
|
-
if (
|
|
2535
|
+
const mjs = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
2536
|
+
if (existsSync3(mjs)) return mjs;
|
|
2537
|
+
const sh = join4(SCRIPTS_DIR, `check-goal-${id}.sh`);
|
|
2538
|
+
if (existsSync3(sh)) return sh;
|
|
3025
2539
|
return null;
|
|
3026
2540
|
}
|
|
3027
2541
|
function runGate(scriptPath) {
|
|
@@ -3030,82 +2544,93 @@ function runGate(scriptPath) {
|
|
|
3030
2544
|
const r = safeExecFile(runner, [scriptPath]);
|
|
3031
2545
|
return { ok: r.ok, out: r.out, err: r.ok ? "" : r.err, runner };
|
|
3032
2546
|
}
|
|
2547
|
+
function warnIfBashOnWindows(scriptPath) {
|
|
2548
|
+
if (process.platform === "win32" && scriptPath.endsWith(".sh")) {
|
|
2549
|
+
console.log(
|
|
2550
|
+
chalk7.yellow(
|
|
2551
|
+
" \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"
|
|
2552
|
+
)
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
3033
2556
|
async function goalCheck(opts) {
|
|
3034
|
-
console.log(
|
|
2557
|
+
console.log(chalk7.bold(`
|
|
3035
2558
|
${ko.goal.checkTitle}
|
|
3036
2559
|
`));
|
|
3037
2560
|
const goals = listGoals(GOALS_DIR);
|
|
3038
2561
|
const id = resolveGoalId(opts.id, goals);
|
|
3039
2562
|
if (id === null) {
|
|
3040
2563
|
console.log(
|
|
3041
|
-
|
|
2564
|
+
chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
3042
2565
|
);
|
|
3043
2566
|
process.exitCode = 1;
|
|
3044
2567
|
return;
|
|
3045
2568
|
}
|
|
3046
2569
|
if (!goals.some((g) => g.frontmatter.id === id)) {
|
|
3047
|
-
console.log(
|
|
2570
|
+
console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
3048
2571
|
process.exitCode = 1;
|
|
3049
2572
|
return;
|
|
3050
2573
|
}
|
|
3051
2574
|
const scriptPath = findGateScript(id);
|
|
3052
2575
|
if (!scriptPath) {
|
|
3053
2576
|
console.log(
|
|
3054
|
-
|
|
2577
|
+
chalk7.red(` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C: scripts/check-goal-${id}.{mjs,sh}`)
|
|
3055
2578
|
);
|
|
3056
2579
|
process.exitCode = 1;
|
|
3057
2580
|
return;
|
|
3058
2581
|
}
|
|
2582
|
+
warnIfBashOnWindows(scriptPath);
|
|
3059
2583
|
const gate2 = runGate(scriptPath);
|
|
3060
|
-
console.log(
|
|
2584
|
+
console.log(chalk7.dim(` \u25B6 ${gate2.runner} ${scriptPath}
|
|
3061
2585
|
`));
|
|
3062
2586
|
if (gate2.out) console.log(gate2.out);
|
|
3063
2587
|
if (gate2.ok) {
|
|
3064
|
-
console.log(
|
|
2588
|
+
console.log(chalk7.green(`
|
|
3065
2589
|
\u2705 Goal ${id} \uAC8C\uC774\uD2B8 \uD1B5\uACFC`));
|
|
3066
2590
|
} else {
|
|
3067
|
-
console.log(
|
|
2591
|
+
console.log(chalk7.red(`
|
|
3068
2592
|
\u274C Goal ${id} \uAC8C\uC774\uD2B8 \uC2E4\uD328`));
|
|
3069
|
-
if (gate2.err && !gate2.out) console.log(
|
|
2593
|
+
if (gate2.err && !gate2.out) console.log(chalk7.dim(gate2.err.slice(0, 500)));
|
|
3070
2594
|
process.exitCode = 1;
|
|
3071
2595
|
}
|
|
3072
2596
|
}
|
|
3073
2597
|
async function goalDone(opts) {
|
|
3074
|
-
console.log(
|
|
2598
|
+
console.log(chalk7.bold(`
|
|
3075
2599
|
${ko.goal.doneTitle}
|
|
3076
2600
|
`));
|
|
3077
2601
|
const goals = listGoals(GOALS_DIR);
|
|
3078
2602
|
const id = resolveGoalId(opts.id, goals);
|
|
3079
2603
|
if (id === null) {
|
|
3080
2604
|
console.log(
|
|
3081
|
-
|
|
2605
|
+
chalk7.yellow(" \u26A0 \uB300\uC0C1 goal \uC744 \uACB0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (--id \uBA85\uC2DC \uB610\uB294 active goal \uD544\uC694).")
|
|
3082
2606
|
);
|
|
3083
2607
|
process.exitCode = 1;
|
|
3084
2608
|
return;
|
|
3085
2609
|
}
|
|
3086
2610
|
const target = goals.find((g) => g.frontmatter.id === id);
|
|
3087
2611
|
if (!target) {
|
|
3088
|
-
console.log(
|
|
2612
|
+
console.log(chalk7.red(` \u274C ${ko.goal.notFound(id)}`));
|
|
3089
2613
|
process.exitCode = 1;
|
|
3090
2614
|
return;
|
|
3091
2615
|
}
|
|
3092
2616
|
const scriptPath = findGateScript(id);
|
|
3093
2617
|
if (!scriptPath) {
|
|
3094
2618
|
console.log(
|
|
3095
|
-
|
|
2619
|
+
chalk7.red(
|
|
3096
2620
|
` \u274C \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uC5C6\uC74C \u2014 done \uCC98\uB9AC \uAC70\uBD80: scripts/check-goal-${id}.{mjs,sh}`
|
|
3097
2621
|
)
|
|
3098
2622
|
);
|
|
3099
2623
|
process.exitCode = 1;
|
|
3100
2624
|
return;
|
|
3101
2625
|
}
|
|
2626
|
+
warnIfBashOnWindows(scriptPath);
|
|
3102
2627
|
const gate2 = runGate(scriptPath);
|
|
3103
|
-
console.log(
|
|
2628
|
+
console.log(chalk7.dim(` \u25B6 \uAC8C\uC774\uD2B8 \uAC80\uC99D: ${gate2.runner} ${scriptPath}
|
|
3104
2629
|
`));
|
|
3105
2630
|
if (gate2.out) console.log(gate2.out);
|
|
3106
2631
|
if (!gate2.ok) {
|
|
3107
2632
|
console.log(
|
|
3108
|
-
|
|
2633
|
+
chalk7.red(
|
|
3109
2634
|
`
|
|
3110
2635
|
\u274C \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 frontmatter \uBCC0\uACBD \uC5C6\uC774 \uC885\uB8CC. (Forbidden: \uC2E4\uD328 = \uBCF4\uC874)`
|
|
3111
2636
|
)
|
|
@@ -3113,11 +2638,11 @@ ${ko.goal.doneTitle}
|
|
|
3113
2638
|
process.exitCode = 1;
|
|
3114
2639
|
return;
|
|
3115
2640
|
}
|
|
3116
|
-
const content =
|
|
2641
|
+
const content = readFileSync3(target.filePath, "utf-8");
|
|
3117
2642
|
const today = localDate();
|
|
3118
2643
|
const updated = updateFrontmatterStatus(content, "DONE", { completed: today });
|
|
3119
2644
|
writeFileSync2(target.filePath, updated, "utf-8");
|
|
3120
|
-
console.log(
|
|
2645
|
+
console.log(chalk7.green(`
|
|
3121
2646
|
\u2705 Goal ${id} \u2192 DONE (completed: ${today})`));
|
|
3122
2647
|
printNextStep({
|
|
3123
2648
|
message: `Goal ${id} \uC644\uB8CC! \uB2E4\uC74C goal \uB85C:`,
|
|
@@ -3125,6 +2650,112 @@ ${ko.goal.doneTitle}
|
|
|
3125
2650
|
cursorHint: "\uB2E4\uC74C goal \uC54C\uB824\uC918"
|
|
3126
2651
|
});
|
|
3127
2652
|
}
|
|
2653
|
+
function generateGateScript(id) {
|
|
2654
|
+
const ID = String(id);
|
|
2655
|
+
return [
|
|
2656
|
+
"#!/usr/bin/env node",
|
|
2657
|
+
`// scripts/check-goal-${ID}.mjs \u2014 \uC790\uB3D9 \uC0DD\uC131 (vhk goal sync).`,
|
|
2658
|
+
"// \uAE30\uBCF8 \uAC8C\uC774\uD2B8 = typecheck + (lint) + test + build. goal \uACE0\uC720 \uAC80\uC99D\uC740 \uC544\uB798 \uAD6C\uC5ED\uC5D0 \uCD94\uAC00.",
|
|
2659
|
+
"// sync \uC7AC\uC2E4\uD589\uD574\uB3C4 \uAE30\uC874 \uD30C\uC77C\uC740 \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (idempotent).",
|
|
2660
|
+
"//",
|
|
2661
|
+
"// Env: VHK_GATES_SKIP_DEEP=1 \u2192 test + build \uC2A4\uD0B5 (\uBE60\uB978 typecheck-only \uD328\uC2A4)",
|
|
2662
|
+
"",
|
|
2663
|
+
"import { execFileSync } from 'node:child_process'",
|
|
2664
|
+
"import { existsSync, readFileSync } from 'node:fs'",
|
|
2665
|
+
"",
|
|
2666
|
+
"const SHIM = new Set(['pnpm', 'npm', 'npx', 'yarn'])",
|
|
2667
|
+
"function run(cmd, args) {",
|
|
2668
|
+
" let bin = cmd, argv = args",
|
|
2669
|
+
" if (process.platform === 'win32' && SHIM.has(cmd)) {",
|
|
2670
|
+
" // Windows: .cmd shim \uC9C1\uC811 spawn \uC740 Node CVE-2024-27980 \uC73C\uB85C EINVAL \u2192 cmd.exe \uB798\uD551.",
|
|
2671
|
+
" bin = 'cmd.exe'; argv = ['/d', '/s', '/c', cmd + '.cmd', ...args]",
|
|
2672
|
+
" }",
|
|
2673
|
+
" try {",
|
|
2674
|
+
" // 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.",
|
|
2675
|
+
" execFileSync(bin, argv, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 })",
|
|
2676
|
+
" return true",
|
|
2677
|
+
" } catch (e) {",
|
|
2678
|
+
" const out = (e?.stdout?.toString() ?? '') + (e?.stderr?.toString() ?? '')",
|
|
2679
|
+
" if (out.trim()) console.log(out.split('\\n').slice(-25).join('\\n'))",
|
|
2680
|
+
" return false",
|
|
2681
|
+
" }",
|
|
2682
|
+
"}",
|
|
2683
|
+
"",
|
|
2684
|
+
"if (existsSync('.vhk/HARD_STOP')) {",
|
|
2685
|
+
` console.log('\u{1F6D1} .vhk/HARD_STOP detected \u2014 refusing to run goal ${ID} gate.')`,
|
|
2686
|
+
" process.exit(1)",
|
|
2687
|
+
"}",
|
|
2688
|
+
"",
|
|
2689
|
+
"const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf-8')) : {}",
|
|
2690
|
+
"const scripts = pkg.scripts ?? {}",
|
|
2691
|
+
"const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
|
|
2692
|
+
"const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
|
|
2693
|
+
"let pass = true",
|
|
2694
|
+
`const gate = (label, ok) => { console.log('[goal ${ID}] ' + label + ': ' + (ok ? '\u2713' : '\u2717')); if (!ok) pass = false }`,
|
|
2695
|
+
"const must = (cond, label) => { console.log((cond ? ' \u2713 ' : ' \u2717 ') + label); if (!cond) pass = false }",
|
|
2696
|
+
"",
|
|
2697
|
+
"// typecheck (\uC2A4\uD06C\uB9BD\uD2B8 \uC6B0\uC120, \uC5C6\uC73C\uBA74 tsc --noEmit)",
|
|
2698
|
+
"if (scripts.typecheck) gate('typecheck', run(pm, ['run', 'typecheck']))",
|
|
2699
|
+
"else if (existsSync('tsconfig.json')) gate('tsc --noEmit', run(pm, pm === 'npm' ? ['exec', '--', 'tsc', '--noEmit'] : ['exec', 'tsc', '--noEmit']))",
|
|
2700
|
+
"if (scripts.lint) gate('lint', run(pm, ['run', 'lint']))",
|
|
2701
|
+
"if (!skipDeep) {",
|
|
2702
|
+
" if (scripts['test:run']) gate('test', run(pm, ['run', 'test:run']))",
|
|
2703
|
+
" else if (scripts.test && /vitest/.test(scripts.test)) gate('test', run(pm, ['run', 'test', '--', '--run']))",
|
|
2704
|
+
" else if (scripts.test) gate('test', run(pm, ['run', 'test']))",
|
|
2705
|
+
" if (scripts.build) gate('build', run(pm, ['run', 'build']))",
|
|
2706
|
+
"}",
|
|
2707
|
+
"",
|
|
2708
|
+
`// \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`,
|
|
2709
|
+
"// const read = (p) => existsSync(p) ? readFileSync(p, 'utf-8') : null",
|
|
2710
|
+
"// must(read('src/foo.ts')?.includes('bar'), 'foo.ts \uC5D0 bar \uC874\uC7AC')",
|
|
2711
|
+
"",
|
|
2712
|
+
`if (pass) { console.log('\u2705 goal ${ID} gate passes'); process.exit(0) }`,
|
|
2713
|
+
`console.log('\u274C goal ${ID} gate failed'); process.exit(1)`,
|
|
2714
|
+
""
|
|
2715
|
+
].join("\n");
|
|
2716
|
+
}
|
|
2717
|
+
async function goalSync() {
|
|
2718
|
+
console.log(chalk7.bold(`
|
|
2719
|
+
${ko.goal.syncTitle}
|
|
2720
|
+
`));
|
|
2721
|
+
const goals = listGoals(GOALS_DIR);
|
|
2722
|
+
const result = { created: [], skipped: [] };
|
|
2723
|
+
if (goals.length === 0) {
|
|
2724
|
+
console.log(
|
|
2725
|
+
chalk7.yellow(" \u{1F4ED} goals/ \uC5D0 goal \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. vhk goal init \uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694.")
|
|
2726
|
+
);
|
|
2727
|
+
return result;
|
|
2728
|
+
}
|
|
2729
|
+
mkdirSync2(SCRIPTS_DIR, { recursive: true });
|
|
2730
|
+
for (const g of goals) {
|
|
2731
|
+
const id = g.frontmatter.id;
|
|
2732
|
+
if (typeof id !== "number") continue;
|
|
2733
|
+
const target = join4(SCRIPTS_DIR, `check-goal-${id}.mjs`);
|
|
2734
|
+
if (existsSync3(target)) {
|
|
2735
|
+
console.log(chalk7.gray(` \u2298 skip (\uC774\uBBF8 \uC874\uC7AC): ${target}`));
|
|
2736
|
+
result.skipped.push(id);
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
const shOnly = existsSync3(join4(SCRIPTS_DIR, `check-goal-${id}.sh`));
|
|
2740
|
+
writeFileSync2(target, generateGateScript(id), "utf-8");
|
|
2741
|
+
console.log(
|
|
2742
|
+
chalk7.green(` \u2713 created: ${target}${shOnly ? " (.sh \u2192 .mjs \uBC31\uD544, Windows 1\uAE09)" : ""}`)
|
|
2743
|
+
);
|
|
2744
|
+
result.created.push(id);
|
|
2745
|
+
}
|
|
2746
|
+
console.log(
|
|
2747
|
+
chalk7.bold(`
|
|
2748
|
+
\u{1F4CA} created=${result.created.length} skipped=${result.skipped.length}`)
|
|
2749
|
+
);
|
|
2750
|
+
if (result.created.length > 0) {
|
|
2751
|
+
printNextStep({
|
|
2752
|
+
message: `\uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 ${result.created.length}\uAC1C \uC0DD\uC131 (goal ${result.created.join(", ")}). \uAC80\uC99D\uD558\uB824\uBA74:`,
|
|
2753
|
+
command: `vhk goal check --id ${result.created[0]}`,
|
|
2754
|
+
cursorHint: `goal ${result.created[0]} \uAC8C\uC774\uD2B8 \uAC80\uC99D\uD574\uC918`
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
return result;
|
|
2758
|
+
}
|
|
3128
2759
|
|
|
3129
2760
|
// src/commands/check.ts
|
|
3130
2761
|
async function check(opts = {}) {
|
|
@@ -3134,22 +2765,22 @@ async function check(opts = {}) {
|
|
|
3134
2765
|
return checkRules();
|
|
3135
2766
|
}
|
|
3136
2767
|
async function checkRules() {
|
|
3137
|
-
console.log(
|
|
2768
|
+
console.log(chalk8.bold(`
|
|
3138
2769
|
${ko.check.title}
|
|
3139
2770
|
`));
|
|
3140
2771
|
const cwd = process.cwd();
|
|
3141
|
-
const rulesPath =
|
|
3142
|
-
if (!
|
|
3143
|
-
console.log(
|
|
3144
|
-
console.log(
|
|
2772
|
+
const rulesPath = path7.join(cwd, "RULES.md");
|
|
2773
|
+
if (!fs6.existsSync(rulesPath)) {
|
|
2774
|
+
console.log(chalk8.yellow(ko.check.noRules));
|
|
2775
|
+
console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
3145
2776
|
return;
|
|
3146
2777
|
}
|
|
3147
2778
|
const rules = parseRules(rulesPath);
|
|
3148
|
-
console.log(
|
|
2779
|
+
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)
|
|
3149
2780
|
`));
|
|
3150
2781
|
if (rules.length === 0) {
|
|
3151
|
-
console.log(
|
|
3152
|
-
console.log(
|
|
2782
|
+
console.log(chalk8.yellow(ko.check.noAutoRules));
|
|
2783
|
+
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."));
|
|
3153
2784
|
return;
|
|
3154
2785
|
}
|
|
3155
2786
|
const allViolations = [];
|
|
@@ -3157,14 +2788,14 @@ ${ko.check.title}
|
|
|
3157
2788
|
for (const rule of rules) {
|
|
3158
2789
|
const violations = rule.check(cwd);
|
|
3159
2790
|
if (violations.length === 0) {
|
|
3160
|
-
const patternHint = rule.type === "content" && rule.pattern ?
|
|
3161
|
-
console.log(
|
|
2791
|
+
const patternHint = rule.type === "content" && rule.pattern ? chalk8.dim(` [\uAC80\uC0AC: ${rule.pattern.source}]`) : "";
|
|
2792
|
+
console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`) + patternHint);
|
|
3162
2793
|
passCount++;
|
|
3163
2794
|
} else {
|
|
3164
|
-
console.log(
|
|
2795
|
+
console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
3165
2796
|
violations.forEach((v) => {
|
|
3166
|
-
const loc = v.file ?
|
|
3167
|
-
const icon = v.severity === "error" ?
|
|
2797
|
+
const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2798
|
+
const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
|
|
3168
2799
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
3169
2800
|
});
|
|
3170
2801
|
allViolations.push(...violations);
|
|
@@ -3174,18 +2805,18 @@ ${ko.check.title}
|
|
|
3174
2805
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
3175
2806
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
3176
2807
|
if (allViolations.length === 0) {
|
|
3177
|
-
console.log(
|
|
3178
|
-
console.log(
|
|
2808
|
+
console.log(chalk8.green.bold(`\u2705 \uC790\uB3D9 \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 ${passCount}\uAC1C \uD1B5\uACFC`));
|
|
2809
|
+
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.)"));
|
|
3179
2810
|
printNextStep({
|
|
3180
2811
|
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
3181
2812
|
command: "vhk \uBCF4\uC548 scan",
|
|
3182
2813
|
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
3183
2814
|
});
|
|
3184
2815
|
} else {
|
|
3185
|
-
console.log(
|
|
3186
|
-
console.log(` \uADDC\uCE59: ${
|
|
3187
|
-
if (errors > 0) console.log(` ${
|
|
3188
|
-
if (warnings > 0) console.log(` ${
|
|
2816
|
+
console.log(chalk8.bold(ko.check.summary));
|
|
2817
|
+
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`);
|
|
2818
|
+
if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
2819
|
+
if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
3189
2820
|
printNextStep({
|
|
3190
2821
|
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
3191
2822
|
command: "vhk \uC810\uAC80",
|
|
@@ -3198,36 +2829,36 @@ ${ko.check.title}
|
|
|
3198
2829
|
}
|
|
3199
2830
|
|
|
3200
2831
|
// src/commands/secure.ts
|
|
3201
|
-
import
|
|
3202
|
-
import
|
|
3203
|
-
import
|
|
2832
|
+
import chalk9 from "chalk";
|
|
2833
|
+
import fs7 from "fs";
|
|
2834
|
+
import path8 from "path";
|
|
3204
2835
|
async function secure() {
|
|
3205
|
-
console.log(
|
|
2836
|
+
console.log(chalk9.bold(`
|
|
3206
2837
|
${ko.secure.title}
|
|
3207
2838
|
`));
|
|
3208
2839
|
const cwd = process.cwd();
|
|
3209
|
-
const gitignorePath =
|
|
3210
|
-
const hasGitignore =
|
|
2840
|
+
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
2841
|
+
const hasGitignore = fs7.existsSync(gitignorePath);
|
|
3211
2842
|
if (!hasGitignore) {
|
|
3212
|
-
console.log(
|
|
3213
|
-
console.log(
|
|
2843
|
+
console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
|
|
2844
|
+
console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
3214
2845
|
} else {
|
|
3215
|
-
const gitignoreContent =
|
|
2846
|
+
const gitignoreContent = fs7.readFileSync(gitignorePath, "utf-8");
|
|
3216
2847
|
if (!gitignoreContent.includes(".env")) {
|
|
3217
|
-
console.log(
|
|
3218
|
-
console.log(
|
|
2848
|
+
console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2849
|
+
console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
3219
2850
|
}
|
|
3220
2851
|
}
|
|
3221
|
-
console.log(
|
|
2852
|
+
console.log(chalk9.dim(` ${ko.secure.scanning}
|
|
3222
2853
|
`));
|
|
3223
2854
|
const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
|
|
3224
|
-
console.log(
|
|
2855
|
+
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)`));
|
|
3225
2856
|
if (truncated) {
|
|
3226
|
-
console.log(
|
|
2857
|
+
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.`));
|
|
3227
2858
|
}
|
|
3228
2859
|
console.log("");
|
|
3229
2860
|
if (findings.length === 0) {
|
|
3230
|
-
console.log(
|
|
2861
|
+
console.log(chalk9.green.bold(` ${ko.secure.clean}`));
|
|
3231
2862
|
printNextStep({
|
|
3232
2863
|
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
3233
2864
|
command: "vhk \uC815\uB9AC",
|
|
@@ -3239,45 +2870,45 @@ ${ko.secure.title}
|
|
|
3239
2870
|
const high = findings.filter((f) => f.severity === "high");
|
|
3240
2871
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
3241
2872
|
if (critical.length > 0) {
|
|
3242
|
-
console.log(
|
|
2873
|
+
console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
3243
2874
|
critical.forEach((f) => {
|
|
3244
|
-
console.log(
|
|
3245
|
-
console.log(
|
|
2875
|
+
console.log(chalk9.red(` \u2716 ${f.patternName}`));
|
|
2876
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
3246
2877
|
});
|
|
3247
2878
|
console.log("");
|
|
3248
2879
|
}
|
|
3249
2880
|
if (high.length > 0) {
|
|
3250
|
-
console.log(
|
|
2881
|
+
console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
3251
2882
|
high.forEach((f) => {
|
|
3252
|
-
console.log(
|
|
3253
|
-
console.log(
|
|
2883
|
+
console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
|
|
2884
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
3254
2885
|
});
|
|
3255
2886
|
console.log("");
|
|
3256
2887
|
}
|
|
3257
2888
|
if (medium.length > 0) {
|
|
3258
|
-
console.log(
|
|
2889
|
+
console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
3259
2890
|
medium.forEach((f) => {
|
|
3260
|
-
console.log(
|
|
3261
|
-
console.log(
|
|
2891
|
+
console.log(chalk9.blue(` \u2139 ${f.patternName}`));
|
|
2892
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
3262
2893
|
});
|
|
3263
2894
|
console.log("");
|
|
3264
2895
|
}
|
|
3265
|
-
console.log(
|
|
3266
|
-
console.log(` \uCD1D ${
|
|
2896
|
+
console.log(chalk9.bold(` ${ko.secure.summary}`));
|
|
2897
|
+
console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
3267
2898
|
console.log("");
|
|
3268
|
-
console.log(
|
|
3269
|
-
console.log(
|
|
3270
|
-
console.log(
|
|
3271
|
-
console.log(
|
|
2899
|
+
console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
2900
|
+
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"));
|
|
2901
|
+
console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
2902
|
+
console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
3272
2903
|
if (critical.length > 0 || high.length > 0) {
|
|
3273
2904
|
process.exitCode = 1;
|
|
3274
2905
|
}
|
|
3275
2906
|
}
|
|
3276
2907
|
|
|
3277
2908
|
// src/commands/doctor.ts
|
|
3278
|
-
import
|
|
3279
|
-
import
|
|
3280
|
-
import
|
|
2909
|
+
import chalk10 from "chalk";
|
|
2910
|
+
import fs8 from "fs";
|
|
2911
|
+
import path9 from "path";
|
|
3281
2912
|
import { fileURLToPath } from "url";
|
|
3282
2913
|
function checkCommand(name, command, hint) {
|
|
3283
2914
|
const result = safeExecFile(command, ["--version"]);
|
|
@@ -3286,14 +2917,14 @@ function checkCommand(name, command, hint) {
|
|
|
3286
2917
|
return { name, command, version, ok: true, hint };
|
|
3287
2918
|
}
|
|
3288
2919
|
function getVhkVersion2() {
|
|
3289
|
-
const dir =
|
|
2920
|
+
const dir = path9.dirname(fileURLToPath(import.meta.url));
|
|
3290
2921
|
const candidates = [
|
|
3291
|
-
|
|
3292
|
-
|
|
2922
|
+
path9.join(dir, "../package.json"),
|
|
2923
|
+
path9.join(dir, "../../package.json")
|
|
3293
2924
|
];
|
|
3294
2925
|
for (const pkgPath of candidates) {
|
|
3295
2926
|
try {
|
|
3296
|
-
if (
|
|
2927
|
+
if (fs8.existsSync(pkgPath)) {
|
|
3297
2928
|
const pkg = readJsonFile(pkgPath);
|
|
3298
2929
|
return pkg.version;
|
|
3299
2930
|
}
|
|
@@ -3321,7 +2952,7 @@ function compareSemver(a, b) {
|
|
|
3321
2952
|
return a3 - b3;
|
|
3322
2953
|
}
|
|
3323
2954
|
async function doctor() {
|
|
3324
|
-
console.log(
|
|
2955
|
+
console.log(chalk10.bold(`
|
|
3325
2956
|
${ko.doctor.title}
|
|
3326
2957
|
`));
|
|
3327
2958
|
const checks = [
|
|
@@ -3333,30 +2964,30 @@ ${ko.doctor.title}
|
|
|
3333
2964
|
let allOk = true;
|
|
3334
2965
|
for (const check2 of checks) {
|
|
3335
2966
|
if (check2.ok) {
|
|
3336
|
-
console.log(
|
|
2967
|
+
console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
|
|
3337
2968
|
} else {
|
|
3338
|
-
console.log(
|
|
3339
|
-
console.log(
|
|
2969
|
+
console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
2970
|
+
console.log(chalk10.dim(` \u2192 ${check2.hint}`));
|
|
3340
2971
|
allOk = false;
|
|
3341
2972
|
}
|
|
3342
2973
|
}
|
|
3343
2974
|
console.log("");
|
|
3344
2975
|
const vhkVersion = getVhkVersion2();
|
|
3345
2976
|
if (vhkVersion) {
|
|
3346
|
-
console.log(
|
|
2977
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
|
|
3347
2978
|
} else {
|
|
3348
|
-
console.log(
|
|
2979
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
3349
2980
|
}
|
|
3350
2981
|
if (vhkVersion) {
|
|
3351
2982
|
const latest = fetchLatestNpmVersion("@byh3071/vhk");
|
|
3352
2983
|
if (latest && compareSemver(latest, vhkVersion) > 0) {
|
|
3353
|
-
console.log(
|
|
2984
|
+
console.log(chalk10.yellow(` ${ko.doctor.updateAvailable(latest)}`));
|
|
3354
2985
|
} else if (latest) {
|
|
3355
|
-
console.log(
|
|
2986
|
+
console.log(chalk10.dim(` ${ko.doctor.updateCurrent}`));
|
|
3356
2987
|
}
|
|
3357
2988
|
}
|
|
3358
2989
|
console.log("");
|
|
3359
|
-
console.log(
|
|
2990
|
+
console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
|
|
3360
2991
|
const cwd = process.cwd();
|
|
3361
2992
|
const projectFiles = [
|
|
3362
2993
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -3366,51 +2997,51 @@ ${ko.doctor.title}
|
|
|
3366
2997
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
3367
2998
|
];
|
|
3368
2999
|
for (const file of projectFiles) {
|
|
3369
|
-
const exists =
|
|
3000
|
+
const exists = fs8.existsSync(path9.join(cwd, file.name));
|
|
3370
3001
|
if (exists) {
|
|
3371
|
-
console.log(
|
|
3002
|
+
console.log(chalk10.green(` \u2705 ${file.name}`));
|
|
3372
3003
|
if (file.name === ".env") {
|
|
3373
|
-
const gitignorePath =
|
|
3374
|
-
if (
|
|
3375
|
-
const gitignore =
|
|
3004
|
+
const gitignorePath = path9.join(cwd, ".gitignore");
|
|
3005
|
+
if (fs8.existsSync(gitignorePath)) {
|
|
3006
|
+
const gitignore = fs8.readFileSync(gitignorePath, "utf-8");
|
|
3376
3007
|
if (!gitignore.includes(".env")) {
|
|
3377
|
-
console.log(
|
|
3008
|
+
console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
3378
3009
|
}
|
|
3379
3010
|
}
|
|
3380
3011
|
}
|
|
3381
|
-
} else if (file.name === ".env" &&
|
|
3382
|
-
console.log(
|
|
3012
|
+
} else if (file.name === ".env" && fs8.existsSync(path9.join(cwd, ".env.local"))) {
|
|
3013
|
+
console.log(chalk10.green(" \u2705 .env.local") + chalk10.dim(" \u2014 \uB85C\uCEEC env \uC0AC\uC6A9 \uC911 (.env \uC5C6\uC5B4\uB3C4 \uC815\uC0C1)"));
|
|
3383
3014
|
} else {
|
|
3384
|
-
console.log(
|
|
3015
|
+
console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
|
|
3385
3016
|
}
|
|
3386
3017
|
}
|
|
3387
3018
|
console.log("");
|
|
3388
|
-
console.log(
|
|
3019
|
+
console.log(chalk10.bold(` ${ko.doctor.driftTitle}`));
|
|
3389
3020
|
const ruleDrift = checkRuleDrift(cwd);
|
|
3390
3021
|
if (!ruleDrift.checked) {
|
|
3391
|
-
console.log(
|
|
3022
|
+
console.log(chalk10.dim(` ${ko.doctor.driftNoRules}`));
|
|
3392
3023
|
} else {
|
|
3393
3024
|
const drifted = ruleDrift.results.filter((r) => r.status === "drifted");
|
|
3394
3025
|
if (drifted.length === 0) {
|
|
3395
|
-
console.log(
|
|
3026
|
+
console.log(chalk10.green(` ${ko.doctor.driftRuleClean}`));
|
|
3396
3027
|
} else {
|
|
3397
|
-
console.log(
|
|
3028
|
+
console.log(chalk10.yellow(` ${ko.doctor.driftRuleWarn(drifted.map((d) => d.path).join(", "))}`));
|
|
3398
3029
|
}
|
|
3399
3030
|
}
|
|
3400
3031
|
const ctxDrift = checkContextDrift(cwd);
|
|
3401
3032
|
if (ctxDrift.checked && ctxDrift.stale) {
|
|
3402
|
-
console.log(
|
|
3033
|
+
console.log(chalk10.yellow(` ${ko.doctor.driftContextWarn}`));
|
|
3403
3034
|
}
|
|
3404
3035
|
console.log("");
|
|
3405
3036
|
if (allOk) {
|
|
3406
|
-
console.log(
|
|
3037
|
+
console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
|
|
3407
3038
|
printNextStep({
|
|
3408
3039
|
message: ko.doctor.nextOkMessage,
|
|
3409
3040
|
command: "vhk \uC2DC\uC791",
|
|
3410
3041
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
3411
3042
|
});
|
|
3412
3043
|
} else {
|
|
3413
|
-
console.log(
|
|
3044
|
+
console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
3414
3045
|
printNextStep({
|
|
3415
3046
|
message: ko.doctor.nextRetryMessage,
|
|
3416
3047
|
command: "vhk doctor",
|
|
@@ -3421,10 +3052,10 @@ ${ko.doctor.title}
|
|
|
3421
3052
|
}
|
|
3422
3053
|
|
|
3423
3054
|
// src/commands/ship.ts
|
|
3424
|
-
import
|
|
3425
|
-
import
|
|
3426
|
-
import
|
|
3427
|
-
import
|
|
3055
|
+
import chalk11 from "chalk";
|
|
3056
|
+
import inquirer4 from "inquirer";
|
|
3057
|
+
import fs9 from "fs";
|
|
3058
|
+
import path10 from "path";
|
|
3428
3059
|
var CHECKLIST = [
|
|
3429
3060
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
3430
3061
|
{ id: "test", questionKey: "checkTest", hintKey: "hintTest" },
|
|
@@ -3437,9 +3068,9 @@ function sanitizeVersion(version) {
|
|
|
3437
3068
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
3438
3069
|
}
|
|
3439
3070
|
function updateChangelogUnreleased(cwd, version, date) {
|
|
3440
|
-
const changelogPath =
|
|
3441
|
-
if (!
|
|
3442
|
-
const content =
|
|
3071
|
+
const changelogPath = path10.join(cwd, "CHANGELOG.md");
|
|
3072
|
+
if (!fs9.existsSync(changelogPath)) return { status: "missing" };
|
|
3073
|
+
const content = fs9.readFileSync(changelogPath, "utf-8");
|
|
3443
3074
|
const unreleasedHeading = /^## \[Unreleased\][^\n]*$/m;
|
|
3444
3075
|
if (!unreleasedHeading.test(content)) return { status: "no-unreleased" };
|
|
3445
3076
|
const blankUnreleased = [
|
|
@@ -3456,36 +3087,36 @@ function updateChangelogUnreleased(cwd, version, date) {
|
|
|
3456
3087
|
`## [${version}] \u2014 ${date}`
|
|
3457
3088
|
].join("\n");
|
|
3458
3089
|
const updated = content.replace(unreleasedHeading, blankUnreleased);
|
|
3459
|
-
|
|
3090
|
+
fs9.writeFileSync(changelogPath, updated, "utf-8");
|
|
3460
3091
|
return { status: "updated", version };
|
|
3461
3092
|
}
|
|
3462
3093
|
async function ship() {
|
|
3463
3094
|
if (!ensureNotHardStopped("ship")) return;
|
|
3464
|
-
console.log(
|
|
3095
|
+
console.log(chalk11.bold(`
|
|
3465
3096
|
${ko.ship.title}
|
|
3466
3097
|
`));
|
|
3467
3098
|
const cwd = process.cwd();
|
|
3468
|
-
console.log(
|
|
3099
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
|
|
3469
3100
|
`));
|
|
3470
|
-
const { passed } = await
|
|
3101
|
+
const { passed } = await inquirer4.prompt([{
|
|
3471
3102
|
type: "checkbox",
|
|
3472
3103
|
name: "passed",
|
|
3473
3104
|
message: ko.ship.checkboxPrompt,
|
|
3474
3105
|
choices: CHECKLIST.map((c) => ({
|
|
3475
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
3106
|
+
name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
3476
3107
|
value: c.id
|
|
3477
3108
|
}))
|
|
3478
3109
|
}]);
|
|
3479
3110
|
const allPassed = passed.length === CHECKLIST.length;
|
|
3480
3111
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
3481
3112
|
if (!allPassed) {
|
|
3482
|
-
console.log(
|
|
3113
|
+
console.log(chalk11.yellow(`
|
|
3483
3114
|
${ko.ship.incompleteHeader}`));
|
|
3484
3115
|
skipped.forEach((s) => {
|
|
3485
|
-
console.log(
|
|
3486
|
-
console.log(
|
|
3116
|
+
console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
3117
|
+
console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
3487
3118
|
});
|
|
3488
|
-
const { proceed } = await
|
|
3119
|
+
const { proceed } = await inquirer4.prompt([{
|
|
3489
3120
|
type: "confirm",
|
|
3490
3121
|
name: "proceed",
|
|
3491
3122
|
message: ko.ship.proceedConfirm,
|
|
@@ -3500,26 +3131,26 @@ ${ko.ship.title}
|
|
|
3500
3131
|
return;
|
|
3501
3132
|
}
|
|
3502
3133
|
} else {
|
|
3503
|
-
console.log(
|
|
3134
|
+
console.log(chalk11.green(`
|
|
3504
3135
|
${ko.ship.allPassed}
|
|
3505
3136
|
`));
|
|
3506
3137
|
}
|
|
3507
|
-
console.log(
|
|
3138
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.retro}
|
|
3508
3139
|
`));
|
|
3509
|
-
console.log(
|
|
3510
|
-
const retro = await
|
|
3140
|
+
console.log(chalk11.dim(` ${ko.ship.versionHint}`));
|
|
3141
|
+
const retro = await inquirer4.prompt([
|
|
3511
3142
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
3512
3143
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
3513
3144
|
{ type: "input", name: "whatWentWrong", message: ko.ship.questionWrong },
|
|
3514
3145
|
{ type: "input", name: "learned", message: ko.ship.questionLearned },
|
|
3515
3146
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
3516
3147
|
]);
|
|
3517
|
-
const buildLogDir =
|
|
3518
|
-
if (!
|
|
3148
|
+
const buildLogDir = path10.join(cwd, "docs", "build-log");
|
|
3149
|
+
if (!fs9.existsSync(buildLogDir)) fs9.mkdirSync(buildLogDir, { recursive: true });
|
|
3519
3150
|
const today = localDate();
|
|
3520
3151
|
const versionSlug = sanitizeVersion(retro.version);
|
|
3521
3152
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
3522
|
-
const filePath =
|
|
3153
|
+
const filePath = path10.join(buildLogDir, fileName);
|
|
3523
3154
|
const content = [
|
|
3524
3155
|
`# \uBE4C\uB4DC \uB85C\uADF8: v${versionSlug}`,
|
|
3525
3156
|
"",
|
|
@@ -3548,9 +3179,9 @@ ${ko.ship.title}
|
|
|
3548
3179
|
"---",
|
|
3549
3180
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
3550
3181
|
].join("\n");
|
|
3551
|
-
|
|
3552
|
-
console.log(
|
|
3553
|
-
${ko.ship.buildLogDone(
|
|
3182
|
+
fs9.writeFileSync(filePath, content, "utf-8");
|
|
3183
|
+
console.log(chalk11.green(`
|
|
3184
|
+
${ko.ship.buildLogDone(path10.relative(cwd, filePath))}`));
|
|
3554
3185
|
const changelogResult = updateChangelogUnreleased(cwd, versionSlug, today);
|
|
3555
3186
|
if (changelogResult.status === "updated") {
|
|
3556
3187
|
log.success(ko.ship.changelogUpdated(changelogResult.version));
|
|
@@ -3568,10 +3199,10 @@ ${ko.ship.title}
|
|
|
3568
3199
|
}
|
|
3569
3200
|
|
|
3570
3201
|
// src/commands/save.ts
|
|
3571
|
-
import { execFileSync
|
|
3572
|
-
import
|
|
3202
|
+
import { execFileSync } from "child_process";
|
|
3203
|
+
import chalk12 from "chalk";
|
|
3573
3204
|
import ora from "ora";
|
|
3574
|
-
import
|
|
3205
|
+
import inquirer5 from "inquirer";
|
|
3575
3206
|
|
|
3576
3207
|
// src/lib/git-porcelain.ts
|
|
3577
3208
|
function normalizePorcelain(raw) {
|
|
@@ -3597,59 +3228,66 @@ function statusIcon(code) {
|
|
|
3597
3228
|
return "\u{1F4C4}";
|
|
3598
3229
|
}
|
|
3599
3230
|
async function save() {
|
|
3600
|
-
console.log(
|
|
3231
|
+
console.log(chalk12.bold(`
|
|
3601
3232
|
\u{1F4BE} ${t("save.title")}`));
|
|
3602
|
-
console.log(
|
|
3233
|
+
console.log(chalk12.gray("\u2500".repeat(40)));
|
|
3603
3234
|
let gitRoot;
|
|
3604
3235
|
try {
|
|
3605
|
-
|
|
3236
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3606
3237
|
gitRoot = getGitRoot();
|
|
3607
3238
|
} catch {
|
|
3608
|
-
console.log(
|
|
3239
|
+
console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
|
|
3609
3240
|
return;
|
|
3610
3241
|
}
|
|
3611
|
-
console.log(
|
|
3242
|
+
console.log(chalk12.cyan(`
|
|
3612
3243
|
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
3613
3244
|
printSecurityWarnings(gitRoot);
|
|
3614
3245
|
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
3615
3246
|
if (severe.length > 0) {
|
|
3616
|
-
console.log(
|
|
3247
|
+
console.log(chalk12.red(`
|
|
3617
3248
|
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
3618
3249
|
severe.slice(0, 5).forEach((f) => {
|
|
3619
|
-
console.log(
|
|
3250
|
+
console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
3620
3251
|
});
|
|
3621
3252
|
if (severe.length > 5) {
|
|
3622
|
-
console.log(
|
|
3253
|
+
console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
3623
3254
|
}
|
|
3624
|
-
const
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3255
|
+
const proceed = await promptOrDefault(
|
|
3256
|
+
async () => (await inquirer5.prompt([{
|
|
3257
|
+
type: "confirm",
|
|
3258
|
+
name: "proceed",
|
|
3259
|
+
message: t("save.secretsConfirm"),
|
|
3260
|
+
default: false
|
|
3261
|
+
}])).proceed,
|
|
3262
|
+
false
|
|
3263
|
+
// 비대화형 = 시크릿 커밋 안 함 (안전)
|
|
3264
|
+
);
|
|
3630
3265
|
if (!proceed) {
|
|
3631
|
-
console.log(
|
|
3266
|
+
console.log(chalk12.gray(t("save.cancelled")));
|
|
3632
3267
|
return;
|
|
3633
3268
|
}
|
|
3634
3269
|
}
|
|
3635
3270
|
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
3636
3271
|
if (lines.length === 0) {
|
|
3637
|
-
console.log(
|
|
3272
|
+
console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
3638
3273
|
return;
|
|
3639
3274
|
}
|
|
3640
|
-
console.log(
|
|
3275
|
+
console.log(chalk12.cyan(`
|
|
3641
3276
|
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
3642
3277
|
lines.forEach((line) => {
|
|
3643
3278
|
const code = line.substring(0, 2);
|
|
3644
3279
|
const name = line.substring(3);
|
|
3645
3280
|
console.log(` ${statusIcon(code)} ${name}`);
|
|
3646
3281
|
});
|
|
3647
|
-
const
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3282
|
+
const message = await promptOrDefault(
|
|
3283
|
+
async () => (await inquirer5.prompt([{
|
|
3284
|
+
type: "input",
|
|
3285
|
+
name: "message",
|
|
3286
|
+
message: t("save.commitMessage"),
|
|
3287
|
+
default: formatDefaultCommitMessage()
|
|
3288
|
+
}])).message,
|
|
3289
|
+
"chore: vhk save"
|
|
3290
|
+
);
|
|
3653
3291
|
const spinner = ora(t("save.saving")).start();
|
|
3654
3292
|
let didAdd = false;
|
|
3655
3293
|
try {
|
|
@@ -3659,21 +3297,21 @@ async function save() {
|
|
|
3659
3297
|
spinner.text = t("save.pushing");
|
|
3660
3298
|
if (!hasGitRemote(gitRoot)) {
|
|
3661
3299
|
spinner.succeed(t("save.successLocal"));
|
|
3662
|
-
console.log(
|
|
3300
|
+
console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
3663
3301
|
} else {
|
|
3664
3302
|
try {
|
|
3665
3303
|
gitRun(["push"], gitRoot);
|
|
3666
3304
|
spinner.succeed(t("save.successWithPush"));
|
|
3667
3305
|
} catch (pushErr) {
|
|
3668
3306
|
spinner.fail(t("save.pushFailed"));
|
|
3669
|
-
console.log(
|
|
3670
|
-
console.log(
|
|
3307
|
+
console.log(chalk12.red(getExecErrorMessage(pushErr)));
|
|
3308
|
+
console.log(chalk12.yellow(`
|
|
3671
3309
|
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
3672
3310
|
process.exitCode = 1;
|
|
3673
3311
|
}
|
|
3674
3312
|
}
|
|
3675
3313
|
if (process.exitCode !== 1) {
|
|
3676
|
-
console.log(
|
|
3314
|
+
console.log(chalk12.green(`
|
|
3677
3315
|
\u2705 ${t("save.done", lines.length)}`));
|
|
3678
3316
|
printNextStep({
|
|
3679
3317
|
message: t("save.nextOkMessage"),
|
|
@@ -3681,7 +3319,7 @@ async function save() {
|
|
|
3681
3319
|
cursorHint: t("save.nextOkCursor")
|
|
3682
3320
|
});
|
|
3683
3321
|
} else {
|
|
3684
|
-
console.log(
|
|
3322
|
+
console.log(chalk12.green(`
|
|
3685
3323
|
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
3686
3324
|
printNextStep({
|
|
3687
3325
|
message: t("save.nextPushFailMessage"),
|
|
@@ -3691,12 +3329,12 @@ async function save() {
|
|
|
3691
3329
|
}
|
|
3692
3330
|
} catch (err) {
|
|
3693
3331
|
spinner.fail(t("save.failed"));
|
|
3694
|
-
console.log(
|
|
3332
|
+
console.log(chalk12.red(getExecErrorMessage(err)));
|
|
3695
3333
|
if (didAdd) {
|
|
3696
3334
|
try {
|
|
3697
3335
|
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3698
3336
|
if (staged) {
|
|
3699
|
-
console.log(
|
|
3337
|
+
console.log(chalk12.yellow(`
|
|
3700
3338
|
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3701
3339
|
}
|
|
3702
3340
|
} catch {
|
|
@@ -3707,9 +3345,9 @@ async function save() {
|
|
|
3707
3345
|
}
|
|
3708
3346
|
|
|
3709
3347
|
// src/commands/undo.ts
|
|
3710
|
-
import { execFileSync as
|
|
3711
|
-
import
|
|
3712
|
-
import
|
|
3348
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3349
|
+
import chalk13 from "chalk";
|
|
3350
|
+
import inquirer6 from "inquirer";
|
|
3713
3351
|
function parseRecentCommits(logOutput) {
|
|
3714
3352
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3715
3353
|
}
|
|
@@ -3732,36 +3370,36 @@ function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
|
3732
3370
|
return false;
|
|
3733
3371
|
}
|
|
3734
3372
|
async function undo() {
|
|
3735
|
-
console.log(
|
|
3373
|
+
console.log(chalk13.bold(`
|
|
3736
3374
|
\u23EA ${t("undo.title")}`));
|
|
3737
|
-
console.log(
|
|
3375
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
3738
3376
|
let gitRoot;
|
|
3739
3377
|
try {
|
|
3740
|
-
|
|
3378
|
+
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3741
3379
|
gitRoot = getGitRoot();
|
|
3742
3380
|
} catch {
|
|
3743
|
-
console.log(
|
|
3381
|
+
console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
3744
3382
|
return;
|
|
3745
3383
|
}
|
|
3746
3384
|
let logOutput;
|
|
3747
3385
|
try {
|
|
3748
3386
|
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
3749
3387
|
} catch {
|
|
3750
|
-
console.log(
|
|
3388
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3751
3389
|
return;
|
|
3752
3390
|
}
|
|
3753
3391
|
const commits = parseRecentCommits(logOutput);
|
|
3754
3392
|
if (commits.length === 0) {
|
|
3755
|
-
console.log(
|
|
3393
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
3756
3394
|
return;
|
|
3757
3395
|
}
|
|
3758
|
-
console.log(
|
|
3396
|
+
console.log(chalk13.cyan(`
|
|
3759
3397
|
${t("undo.recentHeader")}`));
|
|
3760
3398
|
commits.forEach((c, i) => {
|
|
3761
3399
|
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
3762
3400
|
});
|
|
3763
3401
|
const maxUndo = commits.length;
|
|
3764
|
-
const { count } = await
|
|
3402
|
+
const { count } = await inquirer6.prompt([{
|
|
3765
3403
|
type: "number",
|
|
3766
3404
|
name: "count",
|
|
3767
3405
|
message: t("undo.howMany"),
|
|
@@ -3772,7 +3410,7 @@ ${t("undo.recentHeader")}`));
|
|
|
3772
3410
|
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
3773
3411
|
const headCount = countLocalCommits(gitRoot);
|
|
3774
3412
|
if (undoCount >= headCount) {
|
|
3775
|
-
console.log(
|
|
3413
|
+
console.log(chalk13.yellow(`
|
|
3776
3414
|
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3777
3415
|
return;
|
|
3778
3416
|
}
|
|
@@ -3781,30 +3419,30 @@ ${t("undo.recentHeader")}`));
|
|
|
3781
3419
|
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3782
3420
|
if (risky) {
|
|
3783
3421
|
if (unpushed < 0) {
|
|
3784
|
-
console.log(
|
|
3422
|
+
console.log(chalk13.red(`
|
|
3785
3423
|
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3786
3424
|
} else {
|
|
3787
|
-
console.log(
|
|
3425
|
+
console.log(chalk13.red(`
|
|
3788
3426
|
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3789
3427
|
}
|
|
3790
3428
|
}
|
|
3791
|
-
const { confirm } = await
|
|
3429
|
+
const { confirm } = await inquirer6.prompt([{
|
|
3792
3430
|
type: "confirm",
|
|
3793
3431
|
name: "confirm",
|
|
3794
3432
|
message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
|
|
3795
3433
|
default: false
|
|
3796
3434
|
}]);
|
|
3797
3435
|
if (!confirm) {
|
|
3798
|
-
console.log(
|
|
3436
|
+
console.log(chalk13.gray(t("undo.cancelled")));
|
|
3799
3437
|
return;
|
|
3800
3438
|
}
|
|
3801
3439
|
try {
|
|
3802
3440
|
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3803
|
-
console.log(
|
|
3441
|
+
console.log(chalk13.green(`
|
|
3804
3442
|
\u2705 ${t("undo.success")}`));
|
|
3805
|
-
console.log(
|
|
3443
|
+
console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3806
3444
|
if (risky) {
|
|
3807
|
-
console.log(
|
|
3445
|
+
console.log(chalk13.yellow(`
|
|
3808
3446
|
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3809
3447
|
}
|
|
3810
3448
|
printNextStep({
|
|
@@ -3813,39 +3451,39 @@ ${t("undo.recentHeader")}`));
|
|
|
3813
3451
|
cursorHint: t("undo.nextCursor")
|
|
3814
3452
|
});
|
|
3815
3453
|
} catch (err) {
|
|
3816
|
-
console.log(
|
|
3454
|
+
console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
|
|
3817
3455
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3818
|
-
console.log(
|
|
3456
|
+
console.log(chalk13.red(msg));
|
|
3819
3457
|
process.exitCode = 1;
|
|
3820
3458
|
}
|
|
3821
3459
|
}
|
|
3822
3460
|
|
|
3823
3461
|
// src/commands/restore.ts
|
|
3824
|
-
import
|
|
3825
|
-
import
|
|
3462
|
+
import chalk14 from "chalk";
|
|
3463
|
+
import inquirer7 from "inquirer";
|
|
3826
3464
|
async function restore(id) {
|
|
3827
|
-
console.log(
|
|
3465
|
+
console.log(chalk14.bold(`
|
|
3828
3466
|
${ko.restore.title}`));
|
|
3829
|
-
console.log(
|
|
3830
|
-
console.log(
|
|
3467
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3468
|
+
console.log(chalk14.dim(` ${ko.restore.notGitNote}`));
|
|
3831
3469
|
const cwd = process.cwd();
|
|
3832
3470
|
const backups = listBackups(cwd);
|
|
3833
3471
|
if (backups.length === 0) {
|
|
3834
|
-
console.log(
|
|
3472
|
+
console.log(chalk14.yellow(`
|
|
3835
3473
|
${ko.restore.noBackups}`));
|
|
3836
3474
|
return;
|
|
3837
3475
|
}
|
|
3838
3476
|
let targetId = id;
|
|
3839
3477
|
if (!targetId) {
|
|
3840
3478
|
if (!process.stdout.isTTY) {
|
|
3841
|
-
console.log(
|
|
3479
|
+
console.log(chalk14.cyan(`
|
|
3842
3480
|
${ko.restore.listHeader}`));
|
|
3843
3481
|
for (const b of backups) console.log(` ${b.id} (${b.files.length}\uAC1C \uD30C\uC77C)`);
|
|
3844
|
-
console.log(
|
|
3482
|
+
console.log(chalk14.yellow(`
|
|
3845
3483
|
${ko.restore.nonTtyHint}`));
|
|
3846
3484
|
return;
|
|
3847
3485
|
}
|
|
3848
|
-
const { picked } = await
|
|
3486
|
+
const { picked } = await inquirer7.prompt([
|
|
3849
3487
|
{
|
|
3850
3488
|
type: "list",
|
|
3851
3489
|
name: "picked",
|
|
@@ -3860,26 +3498,26 @@ ${ko.restore.nonTtyHint}`));
|
|
|
3860
3498
|
}
|
|
3861
3499
|
try {
|
|
3862
3500
|
const restored = restoreBackup(targetId, cwd);
|
|
3863
|
-
console.log(
|
|
3501
|
+
console.log(chalk14.green(`
|
|
3864
3502
|
${ko.restore.restored(restored.length, targetId)}`));
|
|
3865
|
-
for (const r of restored) console.log(
|
|
3503
|
+
for (const r of restored) console.log(chalk14.gray(` ${r}`));
|
|
3866
3504
|
printNextStep({
|
|
3867
3505
|
message: "\uBC31\uC5C5 \uBCF5\uC6D0 \uC644\uB8CC! \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uC138\uC694.",
|
|
3868
3506
|
command: "vhk diff",
|
|
3869
3507
|
cursorHint: "\uBCC0\uACBD\uC0AC\uD56D \uBCF4\uC5EC\uC918"
|
|
3870
3508
|
});
|
|
3871
3509
|
} catch {
|
|
3872
|
-
console.log(
|
|
3510
|
+
console.log(chalk14.red(`
|
|
3873
3511
|
${ko.restore.notFound(targetId)}`));
|
|
3874
3512
|
process.exitCode = 1;
|
|
3875
3513
|
}
|
|
3876
3514
|
}
|
|
3877
3515
|
|
|
3878
3516
|
// src/commands/status.ts
|
|
3879
|
-
import { execFileSync as
|
|
3880
|
-
import
|
|
3881
|
-
import
|
|
3882
|
-
import
|
|
3517
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3518
|
+
import fs10 from "fs";
|
|
3519
|
+
import path11 from "path";
|
|
3520
|
+
import chalk15 from "chalk";
|
|
3883
3521
|
function countFileChanges(porcelain) {
|
|
3884
3522
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
3885
3523
|
let staged = 0;
|
|
@@ -3917,8 +3555,8 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3917
3555
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3918
3556
|
}
|
|
3919
3557
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3920
|
-
const pkgPath =
|
|
3921
|
-
if (!
|
|
3558
|
+
const pkgPath = path11.join(cwd, "package.json");
|
|
3559
|
+
if (!fs10.existsSync(pkgPath)) return null;
|
|
3922
3560
|
try {
|
|
3923
3561
|
const pkg = readJsonFile(pkgPath);
|
|
3924
3562
|
if (!pkg.name && !pkg.version) return null;
|
|
@@ -3954,15 +3592,15 @@ function getSyncCounts(gitRoot) {
|
|
|
3954
3592
|
}
|
|
3955
3593
|
}
|
|
3956
3594
|
async function status() {
|
|
3957
|
-
console.log(
|
|
3595
|
+
console.log(chalk15.bold(`
|
|
3958
3596
|
\u{1F4CA} ${t("status.title")}`));
|
|
3959
|
-
console.log(
|
|
3597
|
+
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3960
3598
|
let gitRoot;
|
|
3961
3599
|
try {
|
|
3962
|
-
|
|
3600
|
+
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3963
3601
|
gitRoot = getGitRoot();
|
|
3964
3602
|
} catch {
|
|
3965
|
-
console.log(
|
|
3603
|
+
console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3966
3604
|
return;
|
|
3967
3605
|
}
|
|
3968
3606
|
let branch;
|
|
@@ -3981,36 +3619,37 @@ async function status() {
|
|
|
3981
3619
|
commits = [];
|
|
3982
3620
|
}
|
|
3983
3621
|
const pkg = readProjectPackage();
|
|
3984
|
-
console.log(
|
|
3985
|
-
\u{1F33F} ${t("status.branch")}`) +
|
|
3622
|
+
console.log(chalk15.cyan(`
|
|
3623
|
+
\u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
|
|
3986
3624
|
console.log(
|
|
3987
|
-
|
|
3625
|
+
chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
|
|
3988
3626
|
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3989
3627
|
)
|
|
3990
3628
|
);
|
|
3991
|
-
console.log(
|
|
3629
|
+
console.log(chalk15.cyan(`
|
|
3992
3630
|
\u{1F4CB} ${t("status.recentCommits", commits.length)}`));
|
|
3993
3631
|
if (commits.length === 0) {
|
|
3994
|
-
console.log(
|
|
3632
|
+
console.log(chalk15.dim(` ${t("status.noCommits")}`));
|
|
3995
3633
|
} else {
|
|
3996
|
-
commits.forEach((c) => console.log(` ${
|
|
3634
|
+
commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
|
|
3997
3635
|
}
|
|
3998
3636
|
console.log(
|
|
3999
|
-
|
|
4000
|
-
\u{1F504} ${t("status.remote")}`) +
|
|
3637
|
+
chalk15.cyan(`
|
|
3638
|
+
\u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
|
|
4001
3639
|
);
|
|
4002
|
-
console.log(
|
|
3640
|
+
console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
|
|
4003
3641
|
if (pkg) {
|
|
4004
|
-
console.log(
|
|
3642
|
+
console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
|
|
4005
3643
|
} else {
|
|
4006
|
-
console.log(
|
|
3644
|
+
console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
4007
3645
|
}
|
|
4008
3646
|
const hasChanges = counts.staged + counts.unstaged + counts.untracked > 0;
|
|
4009
3647
|
printNextStep(selectStatusNextStep(hasChanges));
|
|
3648
|
+
printContextResumeHint();
|
|
4010
3649
|
}
|
|
4011
3650
|
|
|
4012
3651
|
// src/commands/diff.ts
|
|
4013
|
-
import
|
|
3652
|
+
import chalk16 from "chalk";
|
|
4014
3653
|
function gitOut2(args) {
|
|
4015
3654
|
const r = safeExecFile("git", args);
|
|
4016
3655
|
return r.ok ? r.out : "";
|
|
@@ -4047,67 +3686,67 @@ function summarizeNumstat(numstat) {
|
|
|
4047
3686
|
return { fileCount, totalAdd, totalDel };
|
|
4048
3687
|
}
|
|
4049
3688
|
function printFile(f) {
|
|
4050
|
-
const adds = f.additions > 0 ?
|
|
4051
|
-
const dels = f.deletions > 0 ?
|
|
3689
|
+
const adds = f.additions > 0 ? chalk16.green(`+${f.additions}`) : "";
|
|
3690
|
+
const dels = f.deletions > 0 ? chalk16.red(`-${f.deletions}`) : "";
|
|
4052
3691
|
const change = [adds, dels].filter(Boolean).join(" ");
|
|
4053
3692
|
console.log(` ${f.name} ${change}`);
|
|
4054
3693
|
}
|
|
4055
3694
|
async function diff() {
|
|
4056
|
-
console.log(
|
|
3695
|
+
console.log(chalk16.bold(`
|
|
4057
3696
|
\u{1F50D} ${t("diff.title")}`));
|
|
4058
|
-
console.log(
|
|
3697
|
+
console.log(chalk16.gray("\u2500".repeat(40)));
|
|
4059
3698
|
if (!safeExecFile("git", ["rev-parse", "--is-inside-work-tree"]).ok) {
|
|
4060
|
-
console.log(
|
|
3699
|
+
console.log(chalk16.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
4061
3700
|
return;
|
|
4062
3701
|
}
|
|
4063
3702
|
const unstaged = gitOut2(["diff", "--stat"]);
|
|
4064
3703
|
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
4065
3704
|
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
4066
3705
|
if (!unstaged && !staged && !untracked) {
|
|
4067
|
-
console.log(
|
|
3706
|
+
console.log(chalk16.green(`
|
|
4068
3707
|
\u2705 ${t("diff.noChanges")}`));
|
|
4069
3708
|
return;
|
|
4070
3709
|
}
|
|
4071
3710
|
if (staged) {
|
|
4072
|
-
console.log(
|
|
3711
|
+
console.log(chalk16.cyan(`
|
|
4073
3712
|
${t("diff.stagedHeader")}`));
|
|
4074
3713
|
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
4075
3714
|
}
|
|
4076
3715
|
if (unstaged) {
|
|
4077
|
-
console.log(
|
|
3716
|
+
console.log(chalk16.cyan(`
|
|
4078
3717
|
${t("diff.unstagedHeader")}`));
|
|
4079
3718
|
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
4080
3719
|
}
|
|
4081
3720
|
if (untracked) {
|
|
4082
3721
|
const files = untracked.split("\n").filter(Boolean);
|
|
4083
|
-
console.log(
|
|
3722
|
+
console.log(chalk16.cyan(`
|
|
4084
3723
|
${t("diff.untrackedHeader", files.length)}`));
|
|
4085
|
-
files.forEach((f) => console.log(` ${
|
|
3724
|
+
files.forEach((f) => console.log(` ${chalk16.green("+")} ${f}`));
|
|
4086
3725
|
}
|
|
4087
3726
|
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
4088
3727
|
if (numstat) {
|
|
4089
3728
|
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
4090
|
-
console.log(
|
|
3729
|
+
console.log(chalk16.cyan(`
|
|
4091
3730
|
${t("diff.summaryHeader")}`));
|
|
4092
3731
|
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
4093
|
-
console.log(` \uCD94\uAC00: ${
|
|
4094
|
-
console.log(` \uC0AD\uC81C: ${
|
|
3732
|
+
console.log(` \uCD94\uAC00: ${chalk16.green(`+${totalAdd}`)}\uC904`);
|
|
3733
|
+
console.log(` \uC0AD\uC81C: ${chalk16.red(`-${totalDel}`)}\uC904`);
|
|
4095
3734
|
}
|
|
4096
3735
|
console.log("");
|
|
4097
3736
|
}
|
|
4098
3737
|
|
|
4099
3738
|
// src/commands/mcp-init.ts
|
|
4100
|
-
import { existsSync as
|
|
4101
|
-
import { join as
|
|
3739
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3740
|
+
import { join as join5, dirname } from "path";
|
|
4102
3741
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4103
|
-
import
|
|
3742
|
+
import chalk17 from "chalk";
|
|
4104
3743
|
function resolveMcpEntryPoint() {
|
|
4105
3744
|
try {
|
|
4106
3745
|
const here = fileURLToPath2(import.meta.url);
|
|
4107
3746
|
const dir = dirname(here);
|
|
4108
3747
|
for (const rel of [["mcp", "index.js"], ["..", "mcp", "index.js"]]) {
|
|
4109
|
-
const candidate =
|
|
4110
|
-
if (
|
|
3748
|
+
const candidate = join5(dir, ...rel);
|
|
3749
|
+
if (existsSync4(candidate)) return candidate;
|
|
4111
3750
|
}
|
|
4112
3751
|
} catch {
|
|
4113
3752
|
}
|
|
@@ -4115,17 +3754,17 @@ function resolveMcpEntryPoint() {
|
|
|
4115
3754
|
const url = import.meta.resolve?.("@byh3071/vhk/dist/mcp/index.js");
|
|
4116
3755
|
if (typeof url === "string") {
|
|
4117
3756
|
const p = fileURLToPath2(url);
|
|
4118
|
-
if (
|
|
3757
|
+
if (existsSync4(p)) return p;
|
|
4119
3758
|
}
|
|
4120
3759
|
} catch {
|
|
4121
3760
|
}
|
|
4122
3761
|
try {
|
|
4123
|
-
const pkgPath =
|
|
4124
|
-
if (
|
|
3762
|
+
const pkgPath = join5(process.cwd(), "package.json");
|
|
3763
|
+
if (existsSync4(pkgPath)) {
|
|
4125
3764
|
const pkg = readJsonFile(pkgPath);
|
|
4126
3765
|
if (pkg.name === "@byh3071/vhk") {
|
|
4127
|
-
const local =
|
|
4128
|
-
if (
|
|
3766
|
+
const local = join5(process.cwd(), "dist", "mcp", "index.js");
|
|
3767
|
+
if (existsSync4(local)) return local;
|
|
4129
3768
|
}
|
|
4130
3769
|
}
|
|
4131
3770
|
} catch {
|
|
@@ -4140,31 +3779,31 @@ function resolveVhkMcpEntry() {
|
|
|
4140
3779
|
return { command: "vhk-mcp", args: [] };
|
|
4141
3780
|
}
|
|
4142
3781
|
async function mcpInit() {
|
|
4143
|
-
console.log(
|
|
4144
|
-
console.log(
|
|
4145
|
-
const cursorDir =
|
|
4146
|
-
if (!
|
|
3782
|
+
console.log(chalk17.bold("\n\u{1F50C} " + t("mcp.initTitle")));
|
|
3783
|
+
console.log(chalk17.gray("\u2500".repeat(40)));
|
|
3784
|
+
const cursorDir = join5(process.cwd(), ".cursor");
|
|
3785
|
+
if (!existsSync4(cursorDir)) {
|
|
4147
3786
|
mkdirSync3(cursorDir, { recursive: true });
|
|
4148
3787
|
}
|
|
4149
|
-
const configPath =
|
|
3788
|
+
const configPath = join5(cursorDir, "mcp.json");
|
|
4150
3789
|
const vhkEntry = resolveVhkMcpEntry();
|
|
4151
3790
|
let config;
|
|
4152
|
-
if (
|
|
3791
|
+
if (existsSync4(configPath)) {
|
|
4153
3792
|
try {
|
|
4154
3793
|
const parsed = readJsonFile(configPath);
|
|
4155
3794
|
config = {
|
|
4156
3795
|
mcpServers: { ...parsed.mcpServers ?? {}, vhk: vhkEntry }
|
|
4157
3796
|
};
|
|
4158
3797
|
} catch {
|
|
4159
|
-
console.log(
|
|
3798
|
+
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."));
|
|
4160
3799
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
4161
3800
|
}
|
|
4162
3801
|
} else {
|
|
4163
3802
|
config = { mcpServers: { vhk: vhkEntry } };
|
|
4164
3803
|
}
|
|
4165
3804
|
writeFileSync3(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
4166
|
-
console.log(
|
|
4167
|
-
console.log(
|
|
3805
|
+
console.log(chalk17.green("\n\u2705 Cursor MCP \uC124\uC815 \uC644\uB8CC!"));
|
|
3806
|
+
console.log(chalk17.cyan("\u{1F4C1} \uC0DD\uC131\uB41C \uD30C\uC77C:"));
|
|
4168
3807
|
console.log(` ${configPath}`);
|
|
4169
3808
|
printNextStep({
|
|
4170
3809
|
message: t("mcp.nextMessage"),
|
|
@@ -4174,9 +3813,9 @@ async function mcpInit() {
|
|
|
4174
3813
|
}
|
|
4175
3814
|
|
|
4176
3815
|
// src/commands/design.ts
|
|
4177
|
-
import { existsSync as
|
|
4178
|
-
import
|
|
4179
|
-
import
|
|
3816
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
3817
|
+
import chalk18 from "chalk";
|
|
3818
|
+
import inquirer8 from "inquirer";
|
|
4180
3819
|
var PALETTES = [
|
|
4181
3820
|
{
|
|
4182
3821
|
name: "Minimal",
|
|
@@ -4228,7 +3867,7 @@ var PALETTES = [
|
|
|
4228
3867
|
}
|
|
4229
3868
|
];
|
|
4230
3869
|
function hasTailwind() {
|
|
4231
|
-
return
|
|
3870
|
+
return existsSync5("tailwind.config.js") || existsSync5("tailwind.config.ts") || existsSync5("tailwind.config.mjs") || existsSync5("tailwind.config.cjs");
|
|
4232
3871
|
}
|
|
4233
3872
|
function isTailwindV4Deps(deps) {
|
|
4234
3873
|
if (deps["@tailwindcss/vite"] || deps["@tailwindcss/postcss"]) return true;
|
|
@@ -4278,10 +3917,10 @@ export default vhkColors
|
|
|
4278
3917
|
`;
|
|
4279
3918
|
}
|
|
4280
3919
|
async function design() {
|
|
4281
|
-
console.log(
|
|
4282
|
-
console.log(
|
|
3920
|
+
console.log(chalk18.bold("\n\u{1F3A8} " + t("design.title")));
|
|
3921
|
+
console.log(chalk18.gray("\u2500".repeat(40)));
|
|
4283
3922
|
if (!ensureInteractive("\uCEEC\uB7EC \uD314\uB808\uD2B8 \uC120\uD0DD\uC740 \uB300\uD654\uD615\uC73C\uB85C\uB9CC \uAC00\uB2A5\uD569\uB2C8\uB2E4.")) return;
|
|
4284
|
-
const { paletteIndex } = await
|
|
3923
|
+
const { paletteIndex } = await inquirer8.prompt([
|
|
4285
3924
|
{
|
|
4286
3925
|
type: "list",
|
|
4287
3926
|
name: "paletteIndex",
|
|
@@ -4293,37 +3932,37 @@ async function design() {
|
|
|
4293
3932
|
}
|
|
4294
3933
|
]);
|
|
4295
3934
|
const palette = PALETTES[paletteIndex];
|
|
4296
|
-
console.log(
|
|
3935
|
+
console.log(chalk18.cyan(`
|
|
4297
3936
|
\u{1F3A8} \uC120\uD0DD\uB41C \uD314\uB808\uD2B8: ${palette.name}`));
|
|
4298
3937
|
const v4 = hasTailwindV4();
|
|
4299
3938
|
const targetPath = v4 ? "src/styles/theme.css" : hasTailwind() ? "src/styles/vhk-colors.ts" : "src/styles/tokens.css";
|
|
4300
3939
|
const content = v4 ? generateTailwindV4Theme(palette) : hasTailwind() ? generateTailwindExtend(palette) : generateCSSTokens(palette);
|
|
4301
|
-
if (
|
|
4302
|
-
const { overwrite } = await
|
|
3940
|
+
if (existsSync5(targetPath)) {
|
|
3941
|
+
const { overwrite } = await inquirer8.prompt([{
|
|
4303
3942
|
type: "confirm",
|
|
4304
3943
|
name: "overwrite",
|
|
4305
3944
|
message: `${targetPath} \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?`,
|
|
4306
3945
|
default: false
|
|
4307
3946
|
}]);
|
|
4308
3947
|
if (!overwrite) {
|
|
4309
|
-
console.log(
|
|
3948
|
+
console.log(chalk18.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
4310
3949
|
return;
|
|
4311
3950
|
}
|
|
4312
3951
|
}
|
|
4313
3952
|
mkdirSync4("src/styles", { recursive: true });
|
|
4314
3953
|
writeFileSync4(targetPath, content, "utf-8");
|
|
4315
3954
|
if (v4) {
|
|
4316
|
-
console.log(
|
|
4317
|
-
console.log(
|
|
4318
|
-
console.log(
|
|
3955
|
+
console.log(chalk18.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (Tailwind v4 @theme)"));
|
|
3956
|
+
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.'));
|
|
3957
|
+
console.log(chalk18.gray(" \uB2E4\uD06C \uD1A0\uAE00: \uB8E8\uD2B8 <html>/<body> \uC5D0 `.dark` \uD074\uB798\uC2A4 on/off."));
|
|
4319
3958
|
} else if (hasTailwind()) {
|
|
4320
|
-
console.log(
|
|
4321
|
-
console.log(
|
|
3959
|
+
console.log(chalk18.green("\n\u2705 src/styles/vhk-colors.ts \uC0DD\uC131"));
|
|
3960
|
+
console.log(chalk18.gray(" tailwind.config\uC758 extend.colors\uC5D0 import \uD574\uC11C \uC0AC\uC6A9\uD558\uC138\uC694."));
|
|
4322
3961
|
} else {
|
|
4323
|
-
console.log(
|
|
4324
|
-
console.log(
|
|
3962
|
+
console.log(chalk18.green("\n\u2705 src/styles/tokens.css \uC0DD\uC131"));
|
|
3963
|
+
console.log(chalk18.gray(" HTML\uC5D0 <link>\uB85C \uCD94\uAC00\uD558\uAC70\uB098 CSS\uC5D0\uC11C @import \uD558\uC138\uC694."));
|
|
4325
3964
|
}
|
|
4326
|
-
console.log(
|
|
3965
|
+
console.log(chalk18.bold("\n\u{1F308} \uCEEC\uB7EC \uBBF8\uB9AC\uBCF4\uAE30:"));
|
|
4327
3966
|
for (const [key, value] of Object.entries(palette.colors)) {
|
|
4328
3967
|
console.log(` ${key.padEnd(12)} ${value}`);
|
|
4329
3968
|
}
|
|
@@ -4338,9 +3977,9 @@ async function designPalette() {
|
|
|
4338
3977
|
}
|
|
4339
3978
|
|
|
4340
3979
|
// src/commands/theme.ts
|
|
4341
|
-
import { existsSync as
|
|
4342
|
-
import
|
|
4343
|
-
import
|
|
3980
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
3981
|
+
import chalk19 from "chalk";
|
|
3982
|
+
import inquirer9 from "inquirer";
|
|
4344
3983
|
function generateDarkCSS() {
|
|
4345
3984
|
return `/* vhk theme \u2014 \uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC CSS \uBCC0\uC218 */
|
|
4346
3985
|
|
|
@@ -4396,13 +4035,13 @@ export function initTheme(): void {
|
|
|
4396
4035
|
`;
|
|
4397
4036
|
}
|
|
4398
4037
|
async function theme() {
|
|
4399
|
-
console.log(
|
|
4400
|
-
console.log(
|
|
4038
|
+
console.log(chalk19.bold("\n\u{1F319} " + t("theme.title")));
|
|
4039
|
+
console.log(chalk19.gray("\u2500".repeat(40)));
|
|
4401
4040
|
const cssPath = "src/styles/theme.css";
|
|
4402
4041
|
const togglePath = "src/lib/theme-toggle.ts";
|
|
4403
|
-
const conflicts = [cssPath, togglePath].filter((p) =>
|
|
4042
|
+
const conflicts = [cssPath, togglePath].filter((p) => existsSync6(p));
|
|
4404
4043
|
if (conflicts.length > 0) {
|
|
4405
|
-
const { overwrite } = await
|
|
4044
|
+
const { overwrite } = await inquirer9.prompt([{
|
|
4406
4045
|
type: "confirm",
|
|
4407
4046
|
name: "overwrite",
|
|
4408
4047
|
message: `\uB2E4\uC74C \uD30C\uC77C\uC774 \uC774\uBBF8 \uC788\uC5B4\uC694. \uB36E\uC5B4\uC4F8\uAE4C\uC694?
|
|
@@ -4410,21 +4049,21 @@ async function theme() {
|
|
|
4410
4049
|
default: false
|
|
4411
4050
|
}]);
|
|
4412
4051
|
if (!overwrite) {
|
|
4413
|
-
console.log(
|
|
4052
|
+
console.log(chalk19.yellow("\n\u23ED\uFE0F \uC0DD\uC131 \uCDE8\uC18C \u2014 \uAE30\uC874 \uD30C\uC77C \uC720\uC9C0."));
|
|
4414
4053
|
return;
|
|
4415
4054
|
}
|
|
4416
4055
|
}
|
|
4417
4056
|
mkdirSync5("src/styles", { recursive: true });
|
|
4418
4057
|
mkdirSync5("src/lib", { recursive: true });
|
|
4419
4058
|
writeFileSync5(cssPath, generateDarkCSS(), "utf-8");
|
|
4420
|
-
console.log(
|
|
4059
|
+
console.log(chalk19.green("\n\u2705 src/styles/theme.css \uC0DD\uC131 (\uB2E4\uD06C/\uB77C\uC774\uD2B8 \uBAA8\uB4DC)"));
|
|
4421
4060
|
writeFileSync5(togglePath, generateToggleUtil(), "utf-8");
|
|
4422
|
-
console.log(
|
|
4423
|
-
console.log(
|
|
4424
|
-
console.log(
|
|
4425
|
-
console.log(
|
|
4426
|
-
console.log(
|
|
4427
|
-
console.log(
|
|
4061
|
+
console.log(chalk19.green("\u2705 src/lib/theme-toggle.ts \uC0DD\uC131 (\uD1A0\uAE00 \uC720\uD2F8\uB9AC\uD2F0)"));
|
|
4062
|
+
console.log(chalk19.bold("\n\u{1F4D6} \uC0AC\uC6A9\uBC95:"));
|
|
4063
|
+
console.log(chalk19.gray(" 1. theme.css\uB97C \uAE00\uB85C\uBC8C \uC2A4\uD0C0\uC77C\uC5D0 \uCD94\uAC00"));
|
|
4064
|
+
console.log(chalk19.gray(' 2. import { initTheme, toggleTheme } from "./lib/theme-toggle"'));
|
|
4065
|
+
console.log(chalk19.gray(" 3. \uC571 \uC9C4\uC785\uC810\uC5D0\uC11C initTheme() \uD638\uCD9C"));
|
|
4066
|
+
console.log(chalk19.gray(" 4. \uD1A0\uAE00 \uBC84\uD2BC\uC5D0\uC11C toggleTheme() \uD638\uCD9C"));
|
|
4428
4067
|
printNextStep({
|
|
4429
4068
|
message: "\uD14C\uB9C8 \uC124\uC815 \uC644\uB8CC!",
|
|
4430
4069
|
command: "vhk ref list",
|
|
@@ -4433,11 +4072,11 @@ async function theme() {
|
|
|
4433
4072
|
}
|
|
4434
4073
|
|
|
4435
4074
|
// src/commands/ref.ts
|
|
4436
|
-
import { existsSync as
|
|
4437
|
-
import
|
|
4075
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
4076
|
+
import chalk20 from "chalk";
|
|
4438
4077
|
var REFS_PATH = ".vhk/refs.json";
|
|
4439
4078
|
function loadRefs() {
|
|
4440
|
-
if (!
|
|
4079
|
+
if (!existsSync7(REFS_PATH)) return [];
|
|
4441
4080
|
try {
|
|
4442
4081
|
const parsed = readJsonFile(REFS_PATH);
|
|
4443
4082
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -4450,24 +4089,24 @@ function saveRefs(refs) {
|
|
|
4450
4089
|
writeFileSync6(REFS_PATH, JSON.stringify(refs, null, 2) + "\n", "utf-8");
|
|
4451
4090
|
}
|
|
4452
4091
|
async function refAdd(url, memo = "") {
|
|
4453
|
-
console.log(
|
|
4454
|
-
console.log(
|
|
4092
|
+
console.log(chalk20.bold("\n\u{1F517} " + t("ref.addTitle")));
|
|
4093
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
4455
4094
|
if (!url) {
|
|
4456
|
-
console.log(
|
|
4457
|
-
console.log(
|
|
4095
|
+
console.log(chalk20.red("\u274C URL\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4096
|
+
console.log(chalk20.gray(' \uC608: vhk ref add https://example.com --memo "\uCC38\uACE0 \uC0AC\uC774\uD2B8"'));
|
|
4458
4097
|
return;
|
|
4459
4098
|
}
|
|
4460
4099
|
const refs = loadRefs();
|
|
4461
4100
|
if (refs.some((r) => r.url === url)) {
|
|
4462
|
-
console.log(
|
|
4101
|
+
console.log(chalk20.yellow("\u26A0\uFE0F \uC774\uBBF8 \uC800\uC7A5\uB41C URL\uC785\uB2C8\uB2E4."));
|
|
4463
4102
|
return;
|
|
4464
4103
|
}
|
|
4465
4104
|
refs.push({ url, memo, addedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
4466
4105
|
saveRefs(refs);
|
|
4467
|
-
console.log(
|
|
4106
|
+
console.log(chalk20.green(`
|
|
4468
4107
|
\u2705 \uB808\uD37C\uB7F0\uC2A4 \uCD94\uAC00\uB428 (#${refs.length})`));
|
|
4469
|
-
console.log(
|
|
4470
|
-
if (memo) console.log(
|
|
4108
|
+
console.log(chalk20.cyan(` ${url}`));
|
|
4109
|
+
if (memo) console.log(chalk20.gray(` \u{1F4DD} ${memo}`));
|
|
4471
4110
|
printNextStep({
|
|
4472
4111
|
message: "\uB808\uD37C\uB7F0\uC2A4 \uC800\uC7A5 \uC644\uB8CC!",
|
|
4473
4112
|
command: "vhk ref list",
|
|
@@ -4475,22 +4114,22 @@ async function refAdd(url, memo = "") {
|
|
|
4475
4114
|
});
|
|
4476
4115
|
}
|
|
4477
4116
|
async function refList() {
|
|
4478
|
-
console.log(
|
|
4479
|
-
console.log(
|
|
4117
|
+
console.log(chalk20.bold("\n\u{1F4DA} " + t("ref.listTitle")));
|
|
4118
|
+
console.log(chalk20.gray("\u2500".repeat(40)));
|
|
4480
4119
|
const refs = loadRefs();
|
|
4481
4120
|
if (refs.length === 0) {
|
|
4482
|
-
console.log(
|
|
4483
|
-
console.log(
|
|
4121
|
+
console.log(chalk20.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uB808\uD37C\uB7F0\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4122
|
+
console.log(chalk20.gray(' vhk ref add <url> --memo "\uBA54\uBAA8"\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
4484
4123
|
return;
|
|
4485
4124
|
}
|
|
4486
|
-
console.log(
|
|
4125
|
+
console.log(chalk20.cyan(`
|
|
4487
4126
|
\uCD1D ${refs.length}\uAC1C\uC758 \uB808\uD37C\uB7F0\uC2A4:
|
|
4488
4127
|
`));
|
|
4489
4128
|
refs.forEach((ref, index) => {
|
|
4490
4129
|
const date = new Date(ref.addedAt).toLocaleDateString("ko-KR");
|
|
4491
|
-
console.log(
|
|
4492
|
-
if (ref.memo) console.log(
|
|
4493
|
-
console.log(
|
|
4130
|
+
console.log(chalk20.white(` [${index + 1}] ${ref.url}`));
|
|
4131
|
+
if (ref.memo) console.log(chalk20.gray(` \u{1F4DD} ${ref.memo}`));
|
|
4132
|
+
console.log(chalk20.gray(` \u{1F4C5} ${date}`));
|
|
4494
4133
|
console.log("");
|
|
4495
4134
|
});
|
|
4496
4135
|
}
|
|
@@ -4498,7 +4137,7 @@ async function refOpen(indexStr) {
|
|
|
4498
4137
|
const refs = loadRefs();
|
|
4499
4138
|
const idx = parseInt(indexStr, 10) - 1;
|
|
4500
4139
|
if (Number.isNaN(idx) || idx < 0 || idx >= refs.length) {
|
|
4501
|
-
console.log(
|
|
4140
|
+
console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${refs.length || 0})`));
|
|
4502
4141
|
return;
|
|
4503
4142
|
}
|
|
4504
4143
|
const ref = refs[idx];
|
|
@@ -4506,14 +4145,14 @@ async function refOpen(indexStr) {
|
|
|
4506
4145
|
try {
|
|
4507
4146
|
parsed = new URL(ref.url);
|
|
4508
4147
|
} catch {
|
|
4509
|
-
console.log(
|
|
4148
|
+
console.log(chalk20.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 URL: ${ref.url}`));
|
|
4510
4149
|
return;
|
|
4511
4150
|
}
|
|
4512
4151
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
4513
|
-
console.log(
|
|
4152
|
+
console.log(chalk20.red(`\u274C http(s) URL\uB9CC \uC5F4 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (${parsed.protocol})`));
|
|
4514
4153
|
return;
|
|
4515
4154
|
}
|
|
4516
|
-
console.log(
|
|
4155
|
+
console.log(chalk20.cyan(`
|
|
4517
4156
|
\u{1F310} \uC5F4\uAE30: ${ref.url}`));
|
|
4518
4157
|
let result;
|
|
4519
4158
|
if (process.platform === "darwin") {
|
|
@@ -4524,19 +4163,19 @@ async function refOpen(indexStr) {
|
|
|
4524
4163
|
result = safeExecFile("xdg-open", [ref.url]);
|
|
4525
4164
|
}
|
|
4526
4165
|
if (result.ok) {
|
|
4527
|
-
console.log(
|
|
4166
|
+
console.log(chalk20.green("\u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
4528
4167
|
} else {
|
|
4529
|
-
console.log(
|
|
4168
|
+
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."));
|
|
4530
4169
|
}
|
|
4531
4170
|
}
|
|
4532
4171
|
|
|
4533
4172
|
// src/commands/harness.ts
|
|
4534
|
-
import { existsSync as
|
|
4535
|
-
import
|
|
4173
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4174
|
+
import chalk21 from "chalk";
|
|
4536
4175
|
import ora2 from "ora";
|
|
4537
4176
|
function detectPM() {
|
|
4538
|
-
if (
|
|
4539
|
-
if (
|
|
4177
|
+
if (existsSync8("pnpm-lock.yaml")) return "pnpm";
|
|
4178
|
+
if (existsSync8("yarn.lock")) return "yarn";
|
|
4540
4179
|
return "npm";
|
|
4541
4180
|
}
|
|
4542
4181
|
function pmRun(pm, script) {
|
|
@@ -4554,14 +4193,14 @@ function detectChecks() {
|
|
|
4554
4193
|
const pm = detectPM();
|
|
4555
4194
|
if (s.lint) {
|
|
4556
4195
|
checks.push({ name: "lint", bin: pm, args: pmRun(pm, "lint") });
|
|
4557
|
-
} else if (
|
|
4196
|
+
} else if (existsSync8(".eslintrc.js") || existsSync8(".eslintrc.json") || existsSync8("eslint.config.js")) {
|
|
4558
4197
|
checks.push({ name: "lint", bin: "npx", args: ["eslint", ".", "--ext", ".ts,.tsx"] });
|
|
4559
4198
|
}
|
|
4560
4199
|
if (s["type-check"]) {
|
|
4561
4200
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "type-check") });
|
|
4562
4201
|
} else if (s.typecheck) {
|
|
4563
4202
|
checks.push({ name: "type-check", bin: pm, args: pmRun(pm, "typecheck") });
|
|
4564
|
-
} else if (
|
|
4203
|
+
} else if (existsSync8("tsconfig.json")) {
|
|
4565
4204
|
checks.push({ name: "type-check", bin: "npx", args: ["tsc", "--noEmit"] });
|
|
4566
4205
|
}
|
|
4567
4206
|
if (s.test) {
|
|
@@ -4574,15 +4213,15 @@ function detectChecks() {
|
|
|
4574
4213
|
}
|
|
4575
4214
|
async function harness() {
|
|
4576
4215
|
if (!ensureNotHardStopped("harness")) return;
|
|
4577
|
-
console.log(
|
|
4578
|
-
console.log(
|
|
4216
|
+
console.log(chalk21.bold("\n\u{1F527} " + t("harness.title")));
|
|
4217
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
4579
4218
|
const checks = detectChecks();
|
|
4580
4219
|
if (checks.length === 0) {
|
|
4581
|
-
console.log(
|
|
4582
|
-
console.log(
|
|
4220
|
+
console.log(chalk21.yellow("\n\u26A0\uFE0F \uC2E4\uD589\uD560 \uC218 \uC788\uB294 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4221
|
+
console.log(chalk21.gray(" package.json\uC5D0 lint, test, build \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uCD94\uAC00\uD574\uC8FC\uC138\uC694."));
|
|
4583
4222
|
return;
|
|
4584
4223
|
}
|
|
4585
|
-
console.log(
|
|
4224
|
+
console.log(chalk21.cyan(`
|
|
4586
4225
|
\u{1F3C3} ${checks.length}\uAC1C \uC810\uAC80 \uC2DC\uC791:
|
|
4587
4226
|
`));
|
|
4588
4227
|
const results = [];
|
|
@@ -4594,10 +4233,10 @@ async function harness() {
|
|
|
4594
4233
|
const duration = Date.now() - start2;
|
|
4595
4234
|
const sec = (duration / 1e3).toFixed(1);
|
|
4596
4235
|
if (result.ok) {
|
|
4597
|
-
spinner.succeed(`${check2.name} ${
|
|
4236
|
+
spinner.succeed(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
|
|
4598
4237
|
results.push({ name: check2.name, command: display, passed: true, duration });
|
|
4599
4238
|
} else {
|
|
4600
|
-
spinner.fail(`${check2.name} ${
|
|
4239
|
+
spinner.fail(`${check2.name} ${chalk21.gray(`(${sec}s)`)}`);
|
|
4601
4240
|
results.push({
|
|
4602
4241
|
name: check2.name,
|
|
4603
4242
|
command: display,
|
|
@@ -4607,22 +4246,22 @@ async function harness() {
|
|
|
4607
4246
|
});
|
|
4608
4247
|
}
|
|
4609
4248
|
}
|
|
4610
|
-
console.log(
|
|
4611
|
-
console.log(
|
|
4249
|
+
console.log(chalk21.bold("\n\u{1F4CA} \uD1B5\uD569 \uB9AC\uD3EC\uD2B8:"));
|
|
4250
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
4612
4251
|
for (const r of results) {
|
|
4613
|
-
const icon = r.passed ?
|
|
4252
|
+
const icon = r.passed ? chalk21.green("\u2705") : chalk21.red("\u274C");
|
|
4614
4253
|
const sec = (r.duration / 1e3).toFixed(1);
|
|
4615
|
-
console.log(` ${icon} ${r.name.padEnd(15)} ${
|
|
4254
|
+
console.log(` ${icon} ${r.name.padEnd(15)} ${chalk21.gray(`${sec}s`)}`);
|
|
4616
4255
|
}
|
|
4617
4256
|
const passed = results.filter((r) => r.passed).length;
|
|
4618
4257
|
const all = passed === results.length;
|
|
4619
|
-
console.log(
|
|
4258
|
+
console.log(chalk21.gray("\u2500".repeat(40)));
|
|
4620
4259
|
if (all) {
|
|
4621
|
-
console.log(
|
|
4260
|
+
console.log(chalk21.green.bold(`
|
|
4622
4261
|
\u{1F389} \uC804\uCCB4 \uD1B5\uACFC! (${passed}/${results.length})`));
|
|
4623
4262
|
} else {
|
|
4624
4263
|
console.log(
|
|
4625
|
-
|
|
4264
|
+
chalk21.red.bold(`
|
|
4626
4265
|
\u26A0\uFE0F ${results.length - passed}\uAC1C \uC2E4\uD328 (${passed}/${results.length} \uD1B5\uACFC)`)
|
|
4627
4266
|
);
|
|
4628
4267
|
process.exitCode = 1;
|
|
@@ -4635,9 +4274,9 @@ async function harness() {
|
|
|
4635
4274
|
}
|
|
4636
4275
|
|
|
4637
4276
|
// src/commands/migrate.ts
|
|
4638
|
-
import { existsSync as
|
|
4639
|
-
import
|
|
4640
|
-
import
|
|
4277
|
+
import { existsSync as existsSync9, unlinkSync, rmSync as rmSync2 } from "fs";
|
|
4278
|
+
import chalk22 from "chalk";
|
|
4279
|
+
import inquirer10 from "inquirer";
|
|
4641
4280
|
import ora3 from "ora";
|
|
4642
4281
|
var LOCK_FILES = {
|
|
4643
4282
|
npm: "package-lock.json",
|
|
@@ -4645,26 +4284,26 @@ var LOCK_FILES = {
|
|
|
4645
4284
|
pnpm: "pnpm-lock.yaml"
|
|
4646
4285
|
};
|
|
4647
4286
|
function detectCurrentPM() {
|
|
4648
|
-
if (
|
|
4649
|
-
if (
|
|
4650
|
-
if (
|
|
4287
|
+
if (existsSync9("pnpm-lock.yaml")) return "pnpm";
|
|
4288
|
+
if (existsSync9("yarn.lock")) return "yarn";
|
|
4289
|
+
if (existsSync9("package-lock.json")) return "npm";
|
|
4651
4290
|
return null;
|
|
4652
4291
|
}
|
|
4653
4292
|
function isCLIAvailable(pm) {
|
|
4654
4293
|
return safeExecFile(pm, ["--version"]).ok;
|
|
4655
4294
|
}
|
|
4656
4295
|
async function migrate(target) {
|
|
4657
|
-
console.log(
|
|
4658
|
-
console.log(
|
|
4296
|
+
console.log(chalk22.bold("\n\u{1F504} " + t("migrate.title")));
|
|
4297
|
+
console.log(chalk22.gray("\u2500".repeat(40)));
|
|
4659
4298
|
const current = detectCurrentPM();
|
|
4660
|
-
console.log(
|
|
4299
|
+
console.log(chalk22.cyan(`
|
|
4661
4300
|
\uD604\uC7AC \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00"}`));
|
|
4662
4301
|
let targetPM;
|
|
4663
4302
|
if (target && ["npm", "yarn", "pnpm"].includes(target)) {
|
|
4664
4303
|
targetPM = target;
|
|
4665
4304
|
} else {
|
|
4666
4305
|
const choices = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current).map((pm) => ({ name: pm, value: pm }));
|
|
4667
|
-
const { selected } = await
|
|
4306
|
+
const { selected } = await inquirer10.prompt([
|
|
4668
4307
|
{
|
|
4669
4308
|
type: "list",
|
|
4670
4309
|
name: "selected",
|
|
@@ -4675,17 +4314,17 @@ async function migrate(target) {
|
|
|
4675
4314
|
targetPM = selected;
|
|
4676
4315
|
}
|
|
4677
4316
|
if (targetPM === current) {
|
|
4678
|
-
console.log(
|
|
4317
|
+
console.log(chalk22.yellow(`
|
|
4679
4318
|
\u26A0\uFE0F \uC774\uBBF8 ${targetPM}\uC744 \uC0AC\uC6A9 \uC911\uC785\uB2C8\uB2E4.`));
|
|
4680
4319
|
return;
|
|
4681
4320
|
}
|
|
4682
4321
|
if (!isCLIAvailable(targetPM)) {
|
|
4683
|
-
console.log(
|
|
4322
|
+
console.log(chalk22.red(`
|
|
4684
4323
|
\u274C ${targetPM}\uC774 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
4685
|
-
console.log(
|
|
4324
|
+
console.log(chalk22.yellow(` npm i -g ${targetPM}`));
|
|
4686
4325
|
return;
|
|
4687
4326
|
}
|
|
4688
|
-
const { confirm } = await
|
|
4327
|
+
const { confirm } = await inquirer10.prompt([
|
|
4689
4328
|
{
|
|
4690
4329
|
type: "confirm",
|
|
4691
4330
|
name: "confirm",
|
|
@@ -4694,16 +4333,16 @@ async function migrate(target) {
|
|
|
4694
4333
|
}
|
|
4695
4334
|
]);
|
|
4696
4335
|
if (!confirm) {
|
|
4697
|
-
console.log(
|
|
4336
|
+
console.log(chalk22.gray("\uCDE8\uC18C\uB428"));
|
|
4698
4337
|
return;
|
|
4699
4338
|
}
|
|
4700
4339
|
const cleanup = ora3("\uAE30\uC874 lock \uD30C\uC77C \uC815\uB9AC \uC911...").start();
|
|
4701
4340
|
for (const lockFile of Object.values(LOCK_FILES)) {
|
|
4702
|
-
if (
|
|
4341
|
+
if (existsSync9(lockFile)) {
|
|
4703
4342
|
unlinkSync(lockFile);
|
|
4704
4343
|
}
|
|
4705
4344
|
}
|
|
4706
|
-
if (
|
|
4345
|
+
if (existsSync9("node_modules")) {
|
|
4707
4346
|
cleanup.text = "node_modules \uC0AD\uC81C \uC911...";
|
|
4708
4347
|
rmSync2("node_modules", { recursive: true, force: true });
|
|
4709
4348
|
}
|
|
@@ -4714,10 +4353,10 @@ async function migrate(target) {
|
|
|
4714
4353
|
install.succeed(`${targetPM} install \uC644\uB8CC!`);
|
|
4715
4354
|
} else {
|
|
4716
4355
|
install.fail(`${targetPM} install \uC2E4\uD328`);
|
|
4717
|
-
console.log(
|
|
4356
|
+
console.log(chalk22.red(installResult.err.slice(0, 300)));
|
|
4718
4357
|
return;
|
|
4719
4358
|
}
|
|
4720
|
-
console.log(
|
|
4359
|
+
console.log(chalk22.green.bold(`
|
|
4721
4360
|
\u{1F389} ${current ?? "\uC774\uC804"} \u2192 ${targetPM} \uC804\uD658 \uC644\uB8CC!`));
|
|
4722
4361
|
printNextStep({
|
|
4723
4362
|
message: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uC804\uD658 \uC644\uB8CC!",
|
|
@@ -4727,17 +4366,17 @@ async function migrate(target) {
|
|
|
4727
4366
|
}
|
|
4728
4367
|
|
|
4729
4368
|
// src/commands/update.ts
|
|
4730
|
-
import { existsSync as
|
|
4731
|
-
import { dirname as dirname2, join as
|
|
4369
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4370
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
4732
4371
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4733
|
-
import
|
|
4372
|
+
import chalk23 from "chalk";
|
|
4734
4373
|
import ora4 from "ora";
|
|
4735
4374
|
var PACKAGE = "@byh3071/vhk";
|
|
4736
4375
|
function getCurrentVersion() {
|
|
4737
4376
|
const dir = dirname2(fileURLToPath3(import.meta.url));
|
|
4738
|
-
for (const pkgPath of [
|
|
4377
|
+
for (const pkgPath of [join6(dir, "../package.json"), join6(dir, "../../package.json")]) {
|
|
4739
4378
|
try {
|
|
4740
|
-
if (
|
|
4379
|
+
if (existsSync10(pkgPath)) {
|
|
4741
4380
|
const pkg = readJsonFile(pkgPath);
|
|
4742
4381
|
if (pkg.version) return pkg.version;
|
|
4743
4382
|
}
|
|
@@ -4760,32 +4399,32 @@ function isUpToDate(current, latest) {
|
|
|
4760
4399
|
return cc >= lc;
|
|
4761
4400
|
}
|
|
4762
4401
|
async function update() {
|
|
4763
|
-
console.log(
|
|
4764
|
-
console.log(
|
|
4402
|
+
console.log(chalk23.bold("\n\u2B06\uFE0F " + t("update.title")));
|
|
4403
|
+
console.log(chalk23.gray("\u2500".repeat(40)));
|
|
4765
4404
|
const current = getCurrentVersion();
|
|
4766
|
-
console.log(
|
|
4405
|
+
console.log(chalk23.cyan(`
|
|
4767
4406
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${current}`));
|
|
4768
4407
|
const spinner = ora4("\uCD5C\uC2E0 \uBC84\uC804 \uD655\uC778 \uC911...").start();
|
|
4769
4408
|
const latest = getLatestVersion();
|
|
4770
4409
|
if (!latest) {
|
|
4771
4410
|
spinner.fail("\uCD5C\uC2E0 \uBC84\uC804\uC744 \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
4772
|
-
console.log(
|
|
4773
|
-
console.log(
|
|
4411
|
+
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:"));
|
|
4412
|
+
console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
|
|
4774
4413
|
return;
|
|
4775
4414
|
}
|
|
4776
4415
|
spinner.stop();
|
|
4777
|
-
console.log(
|
|
4416
|
+
console.log(chalk23.cyan(`\u{1F195} \uCD5C\uC2E0 \uBC84\uC804: v${latest}`));
|
|
4778
4417
|
if (isUpToDate(current, latest)) {
|
|
4779
|
-
console.log(
|
|
4418
|
+
console.log(chalk23.green("\n\u2705 \uC774\uBBF8 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4!"));
|
|
4780
4419
|
return;
|
|
4781
4420
|
}
|
|
4782
4421
|
const updateSpinner = ora4(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC911...`).start();
|
|
4783
4422
|
const upd = safeExecFile("npm", ["update", "-g", PACKAGE]);
|
|
4784
4423
|
if (upd.ok) {
|
|
4785
4424
|
updateSpinner.succeed(`v${latest}\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`);
|
|
4786
|
-
console.log(
|
|
4425
|
+
console.log(chalk23.green.bold(`
|
|
4787
4426
|
\u{1F389} VHK CLI v${latest} \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
4788
|
-
console.log(
|
|
4427
|
+
console.log(chalk23.gray(" \uBCC0\uACBD \uC0AC\uD56D\uC740 GitHub Releases\uB97C \uD655\uC778\uD558\uC138\uC694."));
|
|
4789
4428
|
printNextStep({
|
|
4790
4429
|
message: t("update.nextOkMessage"),
|
|
4791
4430
|
command: "vhk --version",
|
|
@@ -4793,9 +4432,9 @@ async function update() {
|
|
|
4793
4432
|
});
|
|
4794
4433
|
} else {
|
|
4795
4434
|
updateSpinner.fail("\uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328");
|
|
4796
|
-
console.log(
|
|
4797
|
-
console.log(
|
|
4798
|
-
console.log(
|
|
4435
|
+
console.log(chalk23.red(upd.err.slice(0, 300)));
|
|
4436
|
+
console.log(chalk23.yellow("\n\uC218\uB3D9\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694:"));
|
|
4437
|
+
console.log(chalk23.gray(` npm update -g ${PACKAGE}`));
|
|
4799
4438
|
printNextStep({
|
|
4800
4439
|
message: t("update.nextFailMessage"),
|
|
4801
4440
|
command: "vhk doctor",
|
|
@@ -4806,16 +4445,16 @@ async function update() {
|
|
|
4806
4445
|
|
|
4807
4446
|
// src/commands/context.ts
|
|
4808
4447
|
import {
|
|
4809
|
-
existsSync as
|
|
4448
|
+
existsSync as existsSync11,
|
|
4810
4449
|
mkdirSync as mkdirSync7,
|
|
4811
|
-
readFileSync as
|
|
4450
|
+
readFileSync as readFileSync4,
|
|
4812
4451
|
readdirSync as readdirSync2,
|
|
4813
4452
|
statSync as statSync2,
|
|
4814
4453
|
writeFileSync as writeFileSync7
|
|
4815
4454
|
} from "fs";
|
|
4816
|
-
import { join as
|
|
4817
|
-
import
|
|
4818
|
-
var
|
|
4455
|
+
import { join as join7 } from "path";
|
|
4456
|
+
import chalk24 from "chalk";
|
|
4457
|
+
var CONTEXT_PATH = ".vhk/context.md";
|
|
4819
4458
|
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4820
4459
|
"node_modules",
|
|
4821
4460
|
".git",
|
|
@@ -4839,7 +4478,7 @@ function buildTree(dir, prefix = "", maxDepth = 3, depth = 0) {
|
|
|
4839
4478
|
filtered.forEach((entry, index) => {
|
|
4840
4479
|
const isLast = index === filtered.length - 1;
|
|
4841
4480
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4842
|
-
const fullPath =
|
|
4481
|
+
const fullPath = join7(dir, entry);
|
|
4843
4482
|
const stat = statSync2(fullPath);
|
|
4844
4483
|
const isDir = stat.isDirectory();
|
|
4845
4484
|
lines.push(`${prefix}${connector}${entry}${isDir ? "/" : ""}`);
|
|
@@ -4871,8 +4510,8 @@ function extractTechStack() {
|
|
|
4871
4510
|
else if (all.jest) stack["\uD14C\uC2A4\uD2B8"] = "jest";
|
|
4872
4511
|
if (all.commander) stack["CLI"] = "commander";
|
|
4873
4512
|
if (all.inquirer) stack["\uC778\uD130\uB799\uD2F0\uBE0C"] = "inquirer";
|
|
4874
|
-
if (
|
|
4875
|
-
else if (
|
|
4513
|
+
if (existsSync11("pnpm-lock.yaml")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "pnpm";
|
|
4514
|
+
else if (existsSync11("yarn.lock")) stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "yarn";
|
|
4876
4515
|
else stack["\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800"] = "npm";
|
|
4877
4516
|
if (pkg.name) stack["\uD328\uD0A4\uC9C0 \uC774\uB984"] = pkg.name;
|
|
4878
4517
|
if (pkg.version) stack["\uBC84\uC804"] = pkg.version;
|
|
@@ -4915,8 +4554,8 @@ function getVhkCommands() {
|
|
|
4915
4554
|
}
|
|
4916
4555
|
async function context(opts = {}) {
|
|
4917
4556
|
const compact = opts.compact === true;
|
|
4918
|
-
console.log(
|
|
4919
|
-
console.log(
|
|
4557
|
+
console.log(chalk24.bold("\n\u{1F9E0} " + t("context.title")));
|
|
4558
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4920
4559
|
const stack = extractTechStack();
|
|
4921
4560
|
const tree = buildTree(".", "", compact ? 2 : 3).join("\n");
|
|
4922
4561
|
const commands = getVhkCommands();
|
|
@@ -4951,7 +4590,7 @@ async function context(opts = {}) {
|
|
|
4951
4590
|
}
|
|
4952
4591
|
lines.push("");
|
|
4953
4592
|
}
|
|
4954
|
-
if (
|
|
4593
|
+
if (existsSync11(".vhk/memory.json")) {
|
|
4955
4594
|
try {
|
|
4956
4595
|
const memories = readJsonFile(
|
|
4957
4596
|
".vhk/memory.json"
|
|
@@ -5030,11 +4669,11 @@ async function context(opts = {}) {
|
|
|
5030
4669
|
}
|
|
5031
4670
|
lines.push("");
|
|
5032
4671
|
mkdirSync7(".vhk", { recursive: true });
|
|
5033
|
-
writeFileSync7(
|
|
5034
|
-
console.log(
|
|
5035
|
-
\u2705 ${
|
|
5036
|
-
console.log(
|
|
5037
|
-
console.log(
|
|
4672
|
+
writeFileSync7(CONTEXT_PATH, lines.join("\n"), "utf-8");
|
|
4673
|
+
console.log(chalk24.green(`
|
|
4674
|
+
\u2705 ${CONTEXT_PATH} \uC0DD\uC131 \uC644\uB8CC!`));
|
|
4675
|
+
console.log(chalk24.gray(` \uAE30\uC220 \uC2A4\uD0DD ${Object.keys(stack).length}\uAC1C \uAC10\uC9C0`));
|
|
4676
|
+
console.log(chalk24.gray(" AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8\uC5D0\uAC8C \uC774 \uD30C\uC77C\uC744 \uCC38\uC870\uD558\uAC8C \uD558\uC138\uC694."));
|
|
5038
4677
|
printNextStep({
|
|
5039
4678
|
message: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131 \uC644\uB8CC!",
|
|
5040
4679
|
command: "vhk context-show",
|
|
@@ -5042,23 +4681,23 @@ async function context(opts = {}) {
|
|
|
5042
4681
|
});
|
|
5043
4682
|
}
|
|
5044
4683
|
async function contextShow() {
|
|
5045
|
-
console.log(
|
|
5046
|
-
console.log(
|
|
5047
|
-
if (!
|
|
5048
|
-
console.log(
|
|
5049
|
-
console.log(
|
|
4684
|
+
console.log(chalk24.bold("\n\u{1F4C4} " + t("context.showTitle")));
|
|
4685
|
+
console.log(chalk24.gray("\u2500".repeat(40)));
|
|
4686
|
+
if (!existsSync11(CONTEXT_PATH)) {
|
|
4687
|
+
console.log(chalk24.yellow("\n\u26A0\uFE0F \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4688
|
+
console.log(chalk24.gray(" vhk context\uB97C \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694."));
|
|
5050
4689
|
return;
|
|
5051
4690
|
}
|
|
5052
|
-
const content =
|
|
4691
|
+
const content = readFileSync4(CONTEXT_PATH, "utf-8");
|
|
5053
4692
|
console.log("\n" + content);
|
|
5054
4693
|
}
|
|
5055
4694
|
|
|
5056
4695
|
// src/commands/memory.ts
|
|
5057
|
-
import { existsSync as
|
|
5058
|
-
import
|
|
4696
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
4697
|
+
import chalk25 from "chalk";
|
|
5059
4698
|
var MEMORY_PATH = ".vhk/memory.json";
|
|
5060
4699
|
function loadMemories() {
|
|
5061
|
-
if (!
|
|
4700
|
+
if (!existsSync12(MEMORY_PATH)) return [];
|
|
5062
4701
|
try {
|
|
5063
4702
|
const parsed = readJsonFile(MEMORY_PATH);
|
|
5064
4703
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -5071,11 +4710,11 @@ function saveMemories(memories) {
|
|
|
5071
4710
|
writeFileSync8(MEMORY_PATH, JSON.stringify(memories, null, 2) + "\n", "utf-8");
|
|
5072
4711
|
}
|
|
5073
4712
|
async function memoryAdd(content, tags) {
|
|
5074
|
-
console.log(
|
|
5075
|
-
console.log(
|
|
4713
|
+
console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.addTitle")));
|
|
4714
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
5076
4715
|
if (!content) {
|
|
5077
|
-
console.log(
|
|
5078
|
-
console.log(
|
|
4716
|
+
console.log(chalk25.red("\u274C \uAE30\uC5B5\uD560 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694."));
|
|
4717
|
+
console.log(chalk25.gray(' \uC608: vhk memory add "API\uB294 tRPC \uC0AC\uC6A9\uD558\uAE30\uB85C \uACB0\uC815"'));
|
|
5079
4718
|
process.exitCode = 1;
|
|
5080
4719
|
return;
|
|
5081
4720
|
}
|
|
@@ -5086,9 +4725,9 @@ async function memoryAdd(content, tags) {
|
|
|
5086
4725
|
tags: tags && tags.length > 0 ? tags : []
|
|
5087
4726
|
});
|
|
5088
4727
|
saveMemories(memories);
|
|
5089
|
-
console.log(
|
|
4728
|
+
console.log(chalk25.green(`
|
|
5090
4729
|
\u2705 \uAE30\uC5B5 \uC800\uC7A5\uB428 (#${memories.length})`));
|
|
5091
|
-
console.log(
|
|
4730
|
+
console.log(chalk25.cyan(` \u{1F4DD} ${content}`));
|
|
5092
4731
|
printNextStep({
|
|
5093
4732
|
message: "\uAE30\uC5B5 \uC800\uC7A5 \uC644\uB8CC!",
|
|
5094
4733
|
command: "vhk memory list",
|
|
@@ -5096,24 +4735,24 @@ async function memoryAdd(content, tags) {
|
|
|
5096
4735
|
});
|
|
5097
4736
|
}
|
|
5098
4737
|
async function memoryList() {
|
|
5099
|
-
console.log(
|
|
5100
|
-
console.log(
|
|
4738
|
+
console.log(chalk25.bold("\n\u{1F9E0} " + t("memory.listTitle")));
|
|
4739
|
+
console.log(chalk25.gray("\u2500".repeat(40)));
|
|
5101
4740
|
const memories = loadMemories();
|
|
5102
4741
|
if (memories.length === 0) {
|
|
5103
|
-
console.log(
|
|
5104
|
-
console.log(
|
|
4742
|
+
console.log(chalk25.yellow("\n\u{1F4ED} \uC800\uC7A5\uB41C \uAE30\uC5B5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
4743
|
+
console.log(chalk25.gray(' vhk memory add "\uB0B4\uC6A9"\uC73C\uB85C \uCD94\uAC00\uD558\uC138\uC694.'));
|
|
5105
4744
|
return;
|
|
5106
4745
|
}
|
|
5107
|
-
console.log(
|
|
4746
|
+
console.log(chalk25.cyan(`
|
|
5108
4747
|
\uCD1D ${memories.length}\uAC1C\uC758 \uAE30\uC5B5:
|
|
5109
4748
|
`));
|
|
5110
4749
|
memories.forEach((m, index) => {
|
|
5111
4750
|
const date = new Date(m.addedAt).toLocaleDateString("ko-KR");
|
|
5112
|
-
console.log(
|
|
4751
|
+
console.log(chalk25.white(` [${index + 1}] ${m.content}`));
|
|
5113
4752
|
if (m.tags && m.tags.length > 0) {
|
|
5114
|
-
console.log(
|
|
4753
|
+
console.log(chalk25.blue(` \u{1F3F7}\uFE0F ${m.tags.join(", ")}`));
|
|
5115
4754
|
}
|
|
5116
|
-
console.log(
|
|
4755
|
+
console.log(chalk25.gray(` \u{1F4C5} ${date}`));
|
|
5117
4756
|
console.log("");
|
|
5118
4757
|
});
|
|
5119
4758
|
}
|
|
@@ -5121,31 +4760,31 @@ async function memoryRemove(indexStr) {
|
|
|
5121
4760
|
const memories = loadMemories();
|
|
5122
4761
|
const idx = parseInt(indexStr, 10) - 1;
|
|
5123
4762
|
if (Number.isNaN(idx) || idx < 0 || idx >= memories.length) {
|
|
5124
|
-
console.log(
|
|
4763
|
+
console.log(chalk25.red(`\u274C \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBC88\uD638\uC785\uB2C8\uB2E4. (1~${memories.length || 0})`));
|
|
5125
4764
|
return;
|
|
5126
4765
|
}
|
|
5127
4766
|
const removed = memories.splice(idx, 1)[0];
|
|
5128
4767
|
saveMemories(memories);
|
|
5129
|
-
console.log(
|
|
5130
|
-
console.log(
|
|
4768
|
+
console.log(chalk25.green("\n\u2705 \uAE30\uC5B5 \uC0AD\uC81C\uB428:"));
|
|
4769
|
+
console.log(chalk25.gray(` ${removed.content}`));
|
|
5131
4770
|
}
|
|
5132
4771
|
|
|
5133
4772
|
// src/commands/brief.ts
|
|
5134
|
-
import { existsSync as
|
|
5135
|
-
import
|
|
4773
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync9, readFileSync as readFileSync5 } from "fs";
|
|
4774
|
+
import chalk26 from "chalk";
|
|
5136
4775
|
var BRIEF_PATH = ".vhk/brief.md";
|
|
5137
4776
|
function readProjectIdentity() {
|
|
5138
4777
|
const out = {};
|
|
5139
4778
|
try {
|
|
5140
|
-
if (
|
|
5141
|
-
const r =
|
|
4779
|
+
if (existsSync13("RULES.md")) {
|
|
4780
|
+
const r = readFileSync5("RULES.md", "utf-8");
|
|
5142
4781
|
const m = r.split("\n")[0].match(/^#\s*(.+?)(?:\s*—.*)?$/);
|
|
5143
4782
|
if (m) out.name = m[1].trim();
|
|
5144
4783
|
const d = r.match(/한 줄 설명:\s*(.+)/);
|
|
5145
4784
|
if (d) out.description = d[1].trim();
|
|
5146
4785
|
}
|
|
5147
|
-
if (!out.name &&
|
|
5148
|
-
const m =
|
|
4786
|
+
if (!out.name && existsSync13("CLAUDE.md")) {
|
|
4787
|
+
const m = readFileSync5("CLAUDE.md", "utf-8").match(/#\s*기록 규칙\s*\((.+?)\)/);
|
|
5149
4788
|
if (m) out.name = m[1].trim();
|
|
5150
4789
|
}
|
|
5151
4790
|
} catch {
|
|
@@ -5157,8 +4796,8 @@ function git2(args) {
|
|
|
5157
4796
|
return result.ok ? result.out : "";
|
|
5158
4797
|
}
|
|
5159
4798
|
async function brief() {
|
|
5160
|
-
console.log(
|
|
5161
|
-
console.log(
|
|
4799
|
+
console.log(chalk26.bold("\n\u{1F4CB} " + t("brief.title")));
|
|
4800
|
+
console.log(chalk26.gray("\u2500".repeat(40)));
|
|
5162
4801
|
const lines = [];
|
|
5163
4802
|
lines.push("# \uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551");
|
|
5164
4803
|
lines.push("");
|
|
@@ -5195,7 +4834,7 @@ async function brief() {
|
|
|
5195
4834
|
`- **\uBBF8\uCEE4\uBC0B \uBCC0\uACBD**: ${uncommitted ? `${uncommitted.split("\n").length}\uAC1C \uD30C\uC77C` : "\uC5C6\uC74C \u2705"}`
|
|
5196
4835
|
);
|
|
5197
4836
|
lines.push("");
|
|
5198
|
-
if (
|
|
4837
|
+
if (existsSync13(".vhk/memory.json")) {
|
|
5199
4838
|
try {
|
|
5200
4839
|
const memories = readJsonFile(".vhk/memory.json");
|
|
5201
4840
|
if (Array.isArray(memories) && memories.length > 0) {
|
|
@@ -5212,7 +4851,7 @@ async function brief() {
|
|
|
5212
4851
|
} catch {
|
|
5213
4852
|
}
|
|
5214
4853
|
}
|
|
5215
|
-
if (
|
|
4854
|
+
if (existsSync13(".vhk/refs.json")) {
|
|
5216
4855
|
try {
|
|
5217
4856
|
const refs = readJsonFile(".vhk/refs.json");
|
|
5218
4857
|
if (Array.isArray(refs) && refs.length > 0) {
|
|
@@ -5243,7 +4882,7 @@ async function brief() {
|
|
|
5243
4882
|
mkdirSync9(".vhk", { recursive: true });
|
|
5244
4883
|
writeFileSync9(BRIEF_PATH, lines.join("\n"), "utf-8");
|
|
5245
4884
|
console.log("\n" + lines.join("\n"));
|
|
5246
|
-
console.log(
|
|
4885
|
+
console.log(chalk26.green(`
|
|
5247
4886
|
\u2705 ${BRIEF_PATH} \uC800\uC7A5 \uC644\uB8CC`));
|
|
5248
4887
|
printNextStep({
|
|
5249
4888
|
message: "\uBE0C\uB9AC\uD551 \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -5253,11 +4892,11 @@ async function brief() {
|
|
|
5253
4892
|
}
|
|
5254
4893
|
|
|
5255
4894
|
// src/commands/start.ts
|
|
5256
|
-
import
|
|
5257
|
-
import
|
|
4895
|
+
import chalk27 from "chalk";
|
|
4896
|
+
import inquirer11 from "inquirer";
|
|
5258
4897
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
5259
|
-
import { existsSync as
|
|
5260
|
-
import { join as
|
|
4898
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4899
|
+
import { join as join8 } from "path";
|
|
5261
4900
|
var VHK_FOOTPRINT_FILES = [
|
|
5262
4901
|
"CLAUDE.md",
|
|
5263
4902
|
".cursorrules",
|
|
@@ -5266,7 +4905,7 @@ var VHK_FOOTPRINT_FILES = [
|
|
|
5266
4905
|
"docs/PRD.md"
|
|
5267
4906
|
];
|
|
5268
4907
|
function detectExistingFootprint(cwd) {
|
|
5269
|
-
return VHK_FOOTPRINT_FILES.filter((rel) =>
|
|
4908
|
+
return VHK_FOOTPRINT_FILES.filter((rel) => existsSync14(join8(cwd, rel)));
|
|
5270
4909
|
}
|
|
5271
4910
|
async function runGitInit(cwd) {
|
|
5272
4911
|
try {
|
|
@@ -5295,22 +4934,22 @@ async function runStep(label, fn) {
|
|
|
5295
4934
|
}
|
|
5296
4935
|
}
|
|
5297
4936
|
async function start(options = {}) {
|
|
5298
|
-
console.log(
|
|
4937
|
+
console.log(chalk27.bold(`
|
|
5299
4938
|
${ko.start.title}
|
|
5300
4939
|
`));
|
|
5301
|
-
console.log(
|
|
5302
|
-
console.log(
|
|
5303
|
-
console.log(
|
|
5304
|
-
console.log(
|
|
5305
|
-
console.log(
|
|
4940
|
+
console.log(chalk27.dim(ko.start.intro));
|
|
4941
|
+
console.log(chalk27.dim(` ${ko.start.step1}`));
|
|
4942
|
+
console.log(chalk27.dim(` ${ko.start.step2}`));
|
|
4943
|
+
console.log(chalk27.dim(` ${ko.start.step3}`));
|
|
4944
|
+
console.log(chalk27.dim(` ${ko.start.step4}`));
|
|
5306
4945
|
console.log();
|
|
5307
4946
|
const cwd = process.cwd();
|
|
5308
4947
|
const footprint = detectExistingFootprint(cwd);
|
|
5309
4948
|
if (footprint.length > 0 && !options.yes) {
|
|
5310
|
-
console.log(
|
|
5311
|
-
for (const f of footprint) console.log(
|
|
5312
|
-
console.log(
|
|
5313
|
-
const { proceedExisting } = await
|
|
4949
|
+
console.log(chalk27.yellow("\u26A0\uFE0F \uC774\uBBF8 VHK \uC124\uCE58 \uD754\uC801\uC774 \uAC10\uC9C0\uB410\uC5B4\uC694:"));
|
|
4950
|
+
for (const f of footprint) console.log(chalk27.dim(` - ${f}`));
|
|
4951
|
+
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."));
|
|
4952
|
+
const { proceedExisting } = await inquirer11.prompt([{
|
|
5314
4953
|
type: "confirm",
|
|
5315
4954
|
name: "proceedExisting",
|
|
5316
4955
|
message: "\uADF8\uB798\uB3C4 \uB2E4\uC2DC \uB9C8\uBC95\uC0AC\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
@@ -5321,7 +4960,7 @@ ${ko.start.title}
|
|
|
5321
4960
|
return;
|
|
5322
4961
|
}
|
|
5323
4962
|
} else if (!options.yes) {
|
|
5324
|
-
const { proceed } = await
|
|
4963
|
+
const { proceed } = await inquirer11.prompt([{
|
|
5325
4964
|
type: "confirm",
|
|
5326
4965
|
name: "proceed",
|
|
5327
4966
|
message: ko.start.confirmStart,
|
|
@@ -5347,7 +4986,7 @@ ${ko.start.title}
|
|
|
5347
4986
|
await runStep("[3/4] vhk mcp-init", () => mcpInit());
|
|
5348
4987
|
log.step(ko.start.step4Header);
|
|
5349
4988
|
await runStep("[4/4] vhk context", () => context());
|
|
5350
|
-
console.log(
|
|
4989
|
+
console.log(chalk27.bold.green(`
|
|
5351
4990
|
${ko.start.allDone}
|
|
5352
4991
|
`));
|
|
5353
4992
|
printNextStep({
|
|
@@ -5357,15 +4996,15 @@ ${ko.start.allDone}
|
|
|
5357
4996
|
}
|
|
5358
4997
|
|
|
5359
4998
|
// src/commands/cloud.ts
|
|
5360
|
-
import
|
|
4999
|
+
import fs12 from "fs";
|
|
5361
5000
|
import os from "os";
|
|
5362
|
-
import
|
|
5363
|
-
import
|
|
5001
|
+
import path13 from "path";
|
|
5002
|
+
import chalk28 from "chalk";
|
|
5364
5003
|
|
|
5365
5004
|
// src/lib/vhk-cloud.ts
|
|
5366
5005
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
5367
|
-
import
|
|
5368
|
-
import
|
|
5006
|
+
import fs11 from "fs";
|
|
5007
|
+
import path12 from "path";
|
|
5369
5008
|
var DEFAULT_CLOUD_EXCLUDES = [
|
|
5370
5009
|
"memory.json",
|
|
5371
5010
|
// 개인 의사결정 메모
|
|
@@ -5383,17 +5022,17 @@ var CLOUD_CONFIG_FILE = "cloud.json";
|
|
|
5383
5022
|
function loadVhkignore(rootDir) {
|
|
5384
5023
|
const ig = (0, import_ignore.default)();
|
|
5385
5024
|
ig.add(DEFAULT_CLOUD_EXCLUDES);
|
|
5386
|
-
const ignorePath =
|
|
5387
|
-
if (
|
|
5388
|
-
ig.add(
|
|
5025
|
+
const ignorePath = path12.join(rootDir, ".vhkignore");
|
|
5026
|
+
if (fs11.existsSync(ignorePath)) {
|
|
5027
|
+
ig.add(fs11.readFileSync(ignorePath, "utf-8"));
|
|
5389
5028
|
}
|
|
5390
5029
|
return ig;
|
|
5391
5030
|
}
|
|
5392
5031
|
function collectVhkFiles(rootDir, ig = loadVhkignore(rootDir)) {
|
|
5393
|
-
const vhkDir =
|
|
5032
|
+
const vhkDir = path12.join(rootDir, VHK_DIR2);
|
|
5394
5033
|
let entries;
|
|
5395
5034
|
try {
|
|
5396
|
-
entries =
|
|
5035
|
+
entries = fs11.readdirSync(vhkDir, { withFileTypes: true });
|
|
5397
5036
|
} catch {
|
|
5398
5037
|
return [];
|
|
5399
5038
|
}
|
|
@@ -5409,10 +5048,10 @@ function partitionGistFiles(gistFiles, ig) {
|
|
|
5409
5048
|
return { keep, excluded };
|
|
5410
5049
|
}
|
|
5411
5050
|
function readCloudConfig(rootDir) {
|
|
5412
|
-
const p =
|
|
5413
|
-
if (!
|
|
5051
|
+
const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
|
|
5052
|
+
if (!fs11.existsSync(p)) return null;
|
|
5414
5053
|
try {
|
|
5415
|
-
const parsed = JSON.parse(
|
|
5054
|
+
const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
|
|
5416
5055
|
if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
|
|
5417
5056
|
return { gistId: parsed.gistId };
|
|
5418
5057
|
}
|
|
@@ -5422,24 +5061,44 @@ function readCloudConfig(rootDir) {
|
|
|
5422
5061
|
}
|
|
5423
5062
|
}
|
|
5424
5063
|
function writeCloudConfig(rootDir, config) {
|
|
5425
|
-
const vhkDir =
|
|
5426
|
-
|
|
5427
|
-
const p =
|
|
5428
|
-
|
|
5064
|
+
const vhkDir = path12.join(rootDir, VHK_DIR2);
|
|
5065
|
+
fs11.mkdirSync(vhkDir, { recursive: true });
|
|
5066
|
+
const p = path12.join(vhkDir, CLOUD_CONFIG_FILE);
|
|
5067
|
+
fs11.writeFileSync(p, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5068
|
+
ensureCloudConfigIgnored(vhkDir);
|
|
5069
|
+
}
|
|
5070
|
+
function ensureCloudConfigIgnored(vhkDir) {
|
|
5071
|
+
const giPath = path12.join(vhkDir, ".gitignore");
|
|
5072
|
+
let content = "";
|
|
5073
|
+
try {
|
|
5074
|
+
if (fs11.existsSync(giPath)) content = fs11.readFileSync(giPath, "utf-8");
|
|
5075
|
+
} catch {
|
|
5076
|
+
return;
|
|
5077
|
+
}
|
|
5078
|
+
const already = content.split(/\r?\n/).some((l) => l.trim() === CLOUD_CONFIG_FILE);
|
|
5079
|
+
if (already) return;
|
|
5080
|
+
const block = `# secret gist \uD3EC\uC778\uD130 \u2014 \uCD94\uC801 \uAE08\uC9C0 (VHK-022)
|
|
5081
|
+
${CLOUD_CONFIG_FILE}
|
|
5082
|
+
`;
|
|
5083
|
+
const base = content.length === 0 ? "" : content.endsWith("\n") ? content : content + "\n";
|
|
5084
|
+
try {
|
|
5085
|
+
fs11.writeFileSync(giPath, base + block, "utf-8");
|
|
5086
|
+
} catch {
|
|
5087
|
+
}
|
|
5429
5088
|
}
|
|
5430
5089
|
|
|
5431
5090
|
// src/commands/cloud.ts
|
|
5432
5091
|
function ensureGhReady() {
|
|
5433
5092
|
const ver = safeExecFile("gh", ["--version"]);
|
|
5434
5093
|
if (!ver.ok) {
|
|
5435
|
-
console.log(
|
|
5436
|
-
console.log(
|
|
5094
|
+
console.log(chalk28.red(` ${ko.cloud.noGh}`));
|
|
5095
|
+
console.log(chalk28.dim(" \uC124\uCE58: https://cli.github.com/ (\uC124\uCE58 \uD6C4 `gh auth login`)"));
|
|
5437
5096
|
return false;
|
|
5438
5097
|
}
|
|
5439
5098
|
const auth = safeExecFile("gh", ["auth", "status"]);
|
|
5440
5099
|
if (!auth.ok) {
|
|
5441
|
-
console.log(
|
|
5442
|
-
console.log(
|
|
5100
|
+
console.log(chalk28.red(` ${ko.cloud.noAuth}`));
|
|
5101
|
+
console.log(chalk28.dim(" \uC2E4\uD589: gh auth login (gist \uAD8C\uD55C \uD544\uC694)"));
|
|
5443
5102
|
return false;
|
|
5444
5103
|
}
|
|
5445
5104
|
return true;
|
|
@@ -5452,29 +5111,29 @@ function parseGistId(output) {
|
|
|
5452
5111
|
return null;
|
|
5453
5112
|
}
|
|
5454
5113
|
async function cloudPush() {
|
|
5455
|
-
console.log(
|
|
5114
|
+
console.log(chalk28.bold(`
|
|
5456
5115
|
${ko.cloud.pushTitle}
|
|
5457
5116
|
`));
|
|
5458
5117
|
const cwd = process.cwd();
|
|
5459
|
-
if (!
|
|
5460
|
-
console.log(
|
|
5118
|
+
if (!fs12.existsSync(path13.join(cwd, VHK_DIR2))) {
|
|
5119
|
+
console.log(chalk28.yellow(` ${ko.cloud.noVhkDir}`));
|
|
5461
5120
|
return;
|
|
5462
5121
|
}
|
|
5463
5122
|
const ig = loadVhkignore(cwd);
|
|
5464
5123
|
const files = collectVhkFiles(cwd, ig);
|
|
5465
5124
|
if (files.length === 0) {
|
|
5466
|
-
console.log(
|
|
5125
|
+
console.log(chalk28.yellow(` ${ko.cloud.nothingToSync}`));
|
|
5467
5126
|
return;
|
|
5468
5127
|
}
|
|
5469
5128
|
if (!ensureGhReady()) {
|
|
5470
5129
|
process.exitCode = 1;
|
|
5471
5130
|
return;
|
|
5472
5131
|
}
|
|
5473
|
-
const filePaths = files.map((f) =>
|
|
5474
|
-
console.log(
|
|
5132
|
+
const filePaths = files.map((f) => path13.join(cwd, VHK_DIR2, f));
|
|
5133
|
+
console.log(chalk28.dim(` \u{1F4E6} \uBC31\uC5C5 \uB300\uC0C1 ${files.length}\uAC1C: ${files.join(", ")}
|
|
5475
5134
|
`));
|
|
5476
5135
|
const existing = readCloudConfig(cwd);
|
|
5477
|
-
const desc = `vhk .vhk backup \u2014 ${
|
|
5136
|
+
const desc = `vhk .vhk backup \u2014 ${path13.basename(cwd)}`;
|
|
5478
5137
|
if (existing) {
|
|
5479
5138
|
const gistFiles = listGistFiles(existing.gistId);
|
|
5480
5139
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -5483,8 +5142,8 @@ ${ko.cloud.pushTitle}
|
|
|
5483
5142
|
const args = gistFiles.includes(name) ? ["gist", "edit", existing.gistId, "-f", name, src] : ["gist", "edit", existing.gistId, "-a", src];
|
|
5484
5143
|
const res2 = safeExecFile("gh", args);
|
|
5485
5144
|
if (!res2.ok) {
|
|
5486
|
-
console.log(
|
|
5487
|
-
console.log(
|
|
5145
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail}: ${name}`));
|
|
5146
|
+
console.log(chalk28.dim(` ${res2.err}`));
|
|
5488
5147
|
process.exitCode = 1;
|
|
5489
5148
|
return;
|
|
5490
5149
|
}
|
|
@@ -5499,15 +5158,15 @@ ${ko.cloud.pushTitle}
|
|
|
5499
5158
|
if (!purgeFailed.includes(name)) purgeFailed.push(name);
|
|
5500
5159
|
}
|
|
5501
5160
|
}
|
|
5502
|
-
console.log(
|
|
5503
|
-
console.log(
|
|
5161
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
|
|
5162
|
+
console.log(chalk28.dim(` gist: ${existing.gistId} (\uAC31\uC2E0)`));
|
|
5504
5163
|
if (excluded.length > 0) {
|
|
5505
5164
|
const purged = excluded.filter((n) => !purgeFailed.includes(n));
|
|
5506
5165
|
if (purged.length > 0) {
|
|
5507
|
-
console.log(
|
|
5166
|
+
console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${purged.length}\uAC1C gist \uC5D0\uC11C \uC81C\uAC70: ${purged.join(", ")}`));
|
|
5508
5167
|
}
|
|
5509
5168
|
if (purgeFailed.length > 0) {
|
|
5510
|
-
console.log(
|
|
5169
|
+
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)`));
|
|
5511
5170
|
}
|
|
5512
5171
|
}
|
|
5513
5172
|
printPushNext();
|
|
@@ -5515,32 +5174,32 @@ ${ko.cloud.pushTitle}
|
|
|
5515
5174
|
}
|
|
5516
5175
|
const res = safeExecFile("gh", ["gist", "create", "--desc", desc, ...filePaths]);
|
|
5517
5176
|
if (!res.ok) {
|
|
5518
|
-
console.log(
|
|
5519
|
-
console.log(
|
|
5177
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail}`));
|
|
5178
|
+
console.log(chalk28.dim(` ${res.err || res.out}`));
|
|
5520
5179
|
process.exitCode = 1;
|
|
5521
5180
|
return;
|
|
5522
5181
|
}
|
|
5523
5182
|
const gistId = parseGistId(res.out);
|
|
5524
5183
|
if (!gistId) {
|
|
5525
|
-
console.log(
|
|
5526
|
-
console.log(
|
|
5184
|
+
console.log(chalk28.red(` ${ko.cloud.pushFail} \u2014 gist id \uD30C\uC2F1 \uC2E4\uD328`));
|
|
5185
|
+
console.log(chalk28.dim(` \uCD9C\uB825: ${res.out}`));
|
|
5527
5186
|
process.exitCode = 1;
|
|
5528
5187
|
return;
|
|
5529
5188
|
}
|
|
5530
5189
|
writeCloudConfig(cwd, { gistId });
|
|
5531
|
-
console.log(
|
|
5532
|
-
console.log(
|
|
5190
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pushDone}`));
|
|
5191
|
+
console.log(chalk28.dim(` gist: ${gistId} (\uC2E0\uADDC, secret) \u2192 .vhk/cloud.json \uC800\uC7A5`));
|
|
5533
5192
|
printPushNext();
|
|
5534
5193
|
}
|
|
5535
5194
|
async function cloudPull(gistIdArg) {
|
|
5536
|
-
console.log(
|
|
5195
|
+
console.log(chalk28.bold(`
|
|
5537
5196
|
${ko.cloud.pullTitle}
|
|
5538
5197
|
`));
|
|
5539
5198
|
const cwd = process.cwd();
|
|
5540
5199
|
const gistId = gistIdArg || readCloudConfig(cwd)?.gistId;
|
|
5541
5200
|
if (!gistId) {
|
|
5542
|
-
console.log(
|
|
5543
|
-
console.log(
|
|
5201
|
+
console.log(chalk28.yellow(` ${ko.cloud.noGistId}`));
|
|
5202
|
+
console.log(chalk28.dim(" \uC0AC\uC6A9\uBC95: vhk cloud pull <gistId> (\uB610\uB294 cloud.json \uC774 \uC788\uB294 \uACF3\uC5D0\uC11C \uC2E4\uD589)"));
|
|
5544
5203
|
return;
|
|
5545
5204
|
}
|
|
5546
5205
|
if (!ensureGhReady()) {
|
|
@@ -5549,34 +5208,34 @@ ${ko.cloud.pullTitle}
|
|
|
5549
5208
|
}
|
|
5550
5209
|
const allNames = listGistFiles(gistId);
|
|
5551
5210
|
if (allNames.length === 0) {
|
|
5552
|
-
console.log(
|
|
5211
|
+
console.log(chalk28.red(` ${ko.cloud.pullFail} \u2014 gist \uBE44\uC5C8\uAC70\uB098 \uC811\uADFC \uBD88\uAC00: ${gistId}`));
|
|
5553
5212
|
process.exitCode = 1;
|
|
5554
5213
|
return;
|
|
5555
5214
|
}
|
|
5556
5215
|
const { keep: names, excluded: skipped } = partitionGistFiles(allNames, loadVhkignore(cwd));
|
|
5557
5216
|
if (skipped.length > 0) {
|
|
5558
|
-
console.log(
|
|
5217
|
+
console.log(chalk28.dim(` \u{1F512} \uC81C\uC678 \uB300\uC0C1 ${skipped.length}\uAC1C \uBCF5\uC6D0 \uC2A4\uD0B5: ${skipped.join(", ")}`));
|
|
5559
5218
|
}
|
|
5560
5219
|
if (names.length === 0) {
|
|
5561
|
-
console.log(
|
|
5220
|
+
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).`));
|
|
5562
5221
|
return;
|
|
5563
5222
|
}
|
|
5564
|
-
const vhkDir =
|
|
5565
|
-
|
|
5223
|
+
const vhkDir = path13.join(cwd, VHK_DIR2);
|
|
5224
|
+
fs12.mkdirSync(vhkDir, { recursive: true });
|
|
5566
5225
|
let restored = 0;
|
|
5567
5226
|
for (const name of names) {
|
|
5568
5227
|
const res = safeExecFile("gh", ["gist", "view", gistId, "-f", name, "--raw"]);
|
|
5569
5228
|
if (!res.ok) {
|
|
5570
|
-
console.log(
|
|
5571
|
-
console.log(
|
|
5229
|
+
console.log(chalk28.red(` ${ko.cloud.pullFail}: ${name}`));
|
|
5230
|
+
console.log(chalk28.dim(` ${res.err}`));
|
|
5572
5231
|
continue;
|
|
5573
5232
|
}
|
|
5574
|
-
|
|
5233
|
+
fs12.writeFileSync(path13.join(vhkDir, name), ensureTrailingNewline(res.out), "utf-8");
|
|
5575
5234
|
restored++;
|
|
5576
5235
|
}
|
|
5577
5236
|
writeCloudConfig(cwd, { gistId });
|
|
5578
|
-
console.log(
|
|
5579
|
-
console.log(
|
|
5237
|
+
console.log(chalk28.green.bold(` ${ko.cloud.pullDone}`));
|
|
5238
|
+
console.log(chalk28.dim(` ${restored}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 (gist: ${gistId})`));
|
|
5580
5239
|
printNextStep({
|
|
5581
5240
|
message: "\uD074\uB77C\uC6B0\uB4DC\uC5D0\uC11C .vhk/ \uBCF5\uC6D0 \uC644\uB8CC!",
|
|
5582
5241
|
command: "vhk \uB9E5\uB77D",
|
|
@@ -5588,9 +5247,9 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
5588
5247
|
const body = JSON.stringify({
|
|
5589
5248
|
files: Object.fromEntries(names.map((n) => [n, null]))
|
|
5590
5249
|
});
|
|
5591
|
-
const tmp =
|
|
5250
|
+
const tmp = path13.join(os.tmpdir(), `vhk-gist-purge-${process.pid}.json`);
|
|
5592
5251
|
try {
|
|
5593
|
-
|
|
5252
|
+
fs12.writeFileSync(tmp, body, "utf-8");
|
|
5594
5253
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
5595
5254
|
const res = safeExecFile(
|
|
5596
5255
|
"gh",
|
|
@@ -5602,7 +5261,7 @@ function purgeExcludedFromGist(gistId, names) {
|
|
|
5602
5261
|
return false;
|
|
5603
5262
|
} finally {
|
|
5604
5263
|
try {
|
|
5605
|
-
|
|
5264
|
+
fs12.unlinkSync(tmp);
|
|
5606
5265
|
} catch {
|
|
5607
5266
|
}
|
|
5608
5267
|
}
|
|
@@ -5624,7 +5283,7 @@ function printPushNext() {
|
|
|
5624
5283
|
}
|
|
5625
5284
|
|
|
5626
5285
|
// src/commands/help.ts
|
|
5627
|
-
import
|
|
5286
|
+
import chalk29 from "chalk";
|
|
5628
5287
|
var QUICK_ACTIONS = [
|
|
5629
5288
|
{ say: "\uC0C1\uD0DC \uC54C\uB824\uC918", does: "vhk status" },
|
|
5630
5289
|
{ say: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?", does: "vhk diff" },
|
|
@@ -5638,21 +5297,21 @@ var QUICK_ACTIONS = [
|
|
|
5638
5297
|
{ say: "\uC804\uCCB4 \uBA85\uB839\uC5B4 \uBCF4\uAE30", does: "vhk --help" }
|
|
5639
5298
|
];
|
|
5640
5299
|
function quickActions() {
|
|
5641
|
-
console.log(
|
|
5642
|
-
console.log(
|
|
5300
|
+
console.log(chalk29.bold("\n\u{1F9ED} VHK \u2014 \uC774\uB807\uAC8C \uB9D0\uD558\uBA74 \uB429\uB2C8\uB2E4 (quick actions)"));
|
|
5301
|
+
console.log(chalk29.gray("\u2500".repeat(40)));
|
|
5643
5302
|
for (const a of QUICK_ACTIONS) {
|
|
5644
|
-
console.log(` "${
|
|
5303
|
+
console.log(` "${chalk29.cyan(a.say)}" \u2192 ${chalk29.dim(a.does)}`);
|
|
5645
5304
|
}
|
|
5646
|
-
console.log(
|
|
5305
|
+
console.log(chalk29.gray("\n \uC804\uCCB4 \uBA85\uB839\uC740 `vhk --help` \uB610\uB294 COMMANDS.md \uB97C \uBCF4\uC138\uC694."));
|
|
5647
5306
|
console.log("");
|
|
5648
5307
|
}
|
|
5649
5308
|
|
|
5650
5309
|
// src/commands/mode.ts
|
|
5651
|
-
import
|
|
5310
|
+
import chalk30 from "chalk";
|
|
5652
5311
|
|
|
5653
5312
|
// src/lib/config.ts
|
|
5654
|
-
import { existsSync as
|
|
5655
|
-
import { join as
|
|
5313
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
5314
|
+
import { join as join9 } from "path";
|
|
5656
5315
|
|
|
5657
5316
|
// src/lib/safety-mode.ts
|
|
5658
5317
|
var SAFETY_MODES = ["lite", "standard", "strict"];
|
|
@@ -5668,11 +5327,11 @@ function isSafetyMode(value) {
|
|
|
5668
5327
|
|
|
5669
5328
|
// src/lib/config.ts
|
|
5670
5329
|
var CONFIG_DIR = ".vhk";
|
|
5671
|
-
var CONFIG_PATH =
|
|
5330
|
+
var CONFIG_PATH = join9(CONFIG_DIR, "config.json");
|
|
5672
5331
|
var DEFAULT_CONFIG = { safetyMode: DEFAULT_SAFETY_MODE };
|
|
5673
5332
|
function readConfig(rootDir = process.cwd()) {
|
|
5674
|
-
const full =
|
|
5675
|
-
if (!
|
|
5333
|
+
const full = join9(rootDir, CONFIG_PATH);
|
|
5334
|
+
if (!existsSync15(full)) return { ...DEFAULT_CONFIG };
|
|
5676
5335
|
try {
|
|
5677
5336
|
const raw = readJsonFile(full);
|
|
5678
5337
|
return {
|
|
@@ -5683,23 +5342,23 @@ function readConfig(rootDir = process.cwd()) {
|
|
|
5683
5342
|
}
|
|
5684
5343
|
}
|
|
5685
5344
|
function writeConfig(config, rootDir = process.cwd()) {
|
|
5686
|
-
mkdirSync10(
|
|
5687
|
-
writeFileSync10(
|
|
5345
|
+
mkdirSync10(join9(rootDir, CONFIG_DIR), { recursive: true });
|
|
5346
|
+
writeFileSync10(join9(rootDir, CONFIG_PATH), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5688
5347
|
}
|
|
5689
5348
|
|
|
5690
5349
|
// src/commands/mode.ts
|
|
5691
5350
|
async function mode(target) {
|
|
5692
|
-
console.log(
|
|
5693
|
-
console.log(
|
|
5351
|
+
console.log(chalk30.bold("\n\u{1F6E1}\uFE0F Safety Mode"));
|
|
5352
|
+
console.log(chalk30.gray("\u2500".repeat(40)));
|
|
5694
5353
|
const current = readConfig().safetyMode;
|
|
5695
5354
|
if (!target) {
|
|
5696
|
-
console.log(
|
|
5697
|
-
\uD604\uC7AC \uBAA8\uB4DC: ${
|
|
5698
|
-
console.log(
|
|
5355
|
+
console.log(chalk30.cyan(`
|
|
5356
|
+
\uD604\uC7AC \uBAA8\uB4DC: ${chalk30.bold(current)}`));
|
|
5357
|
+
console.log(chalk30.dim(` ${SAFETY_MODE_DESC[current]}`));
|
|
5699
5358
|
console.log("");
|
|
5700
5359
|
for (const m of SAFETY_MODES) {
|
|
5701
5360
|
const mark = m === current ? "\u25CF" : "\u25CB";
|
|
5702
|
-
console.log(` ${mark} ${m.padEnd(9)} ${
|
|
5361
|
+
console.log(` ${mark} ${m.padEnd(9)} ${chalk30.dim(SAFETY_MODE_DESC[m])}`);
|
|
5703
5362
|
}
|
|
5704
5363
|
printNextStep({
|
|
5705
5364
|
message: "\uBAA8\uB4DC\uB97C \uBC14\uAFB8\uB824\uBA74:",
|
|
@@ -5709,20 +5368,20 @@ async function mode(target) {
|
|
|
5709
5368
|
return;
|
|
5710
5369
|
}
|
|
5711
5370
|
if (!isSafetyMode(target)) {
|
|
5712
|
-
console.log(
|
|
5371
|
+
console.log(chalk30.red(`
|
|
5713
5372
|
\u274C \uC54C \uC218 \uC5C6\uB294 \uBAA8\uB4DC: ${target}`));
|
|
5714
|
-
console.log(
|
|
5373
|
+
console.log(chalk30.dim(` \uAC00\uB2A5: ${SAFETY_MODES.join(" | ")}`));
|
|
5715
5374
|
process.exitCode = 1;
|
|
5716
5375
|
return;
|
|
5717
5376
|
}
|
|
5718
5377
|
writeConfig({ ...readConfig(), safetyMode: target });
|
|
5719
|
-
console.log(
|
|
5720
|
-
\u2705 Safety Mode \u2192 ${
|
|
5721
|
-
console.log(
|
|
5378
|
+
console.log(chalk30.green(`
|
|
5379
|
+
\u2705 Safety Mode \u2192 ${chalk30.bold(target)}`));
|
|
5380
|
+
console.log(chalk30.dim(` ${SAFETY_MODE_DESC[target]}`));
|
|
5722
5381
|
}
|
|
5723
5382
|
|
|
5724
5383
|
// src/commands/verify.ts
|
|
5725
|
-
import
|
|
5384
|
+
import chalk31 from "chalk";
|
|
5726
5385
|
function verificationChecklist() {
|
|
5727
5386
|
return [
|
|
5728
5387
|
"\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
|
|
@@ -5732,15 +5391,15 @@ function verificationChecklist() {
|
|
|
5732
5391
|
];
|
|
5733
5392
|
}
|
|
5734
5393
|
async function verify() {
|
|
5735
|
-
console.log(
|
|
5736
|
-
console.log(
|
|
5394
|
+
console.log(chalk31.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
|
|
5395
|
+
console.log(chalk31.gray("\u2500".repeat(40)));
|
|
5737
5396
|
const mode2 = readConfig().safetyMode;
|
|
5738
|
-
console.log(
|
|
5739
|
-
console.log(
|
|
5397
|
+
console.log(chalk31.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
|
|
5398
|
+
console.log(chalk31.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
|
|
5740
5399
|
for (const item of verificationChecklist()) {
|
|
5741
5400
|
console.log(` \u2610 ${item}`);
|
|
5742
5401
|
}
|
|
5743
|
-
console.log(
|
|
5402
|
+
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)."));
|
|
5744
5403
|
printNextStep({
|
|
5745
5404
|
message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
|
|
5746
5405
|
command: "vhk save",
|
|
@@ -5757,7 +5416,8 @@ var HIGH_RISK_ACTIONS = [
|
|
|
5757
5416
|
"cloud-pull",
|
|
5758
5417
|
"resume",
|
|
5759
5418
|
"env-write",
|
|
5760
|
-
"delete"
|
|
5419
|
+
"delete",
|
|
5420
|
+
"restore"
|
|
5761
5421
|
];
|
|
5762
5422
|
var STRICT_EXTRA_ACTIONS = /* @__PURE__ */ new Set(["save", "sync"]);
|
|
5763
5423
|
var NL_GUARDED_ACTIONS = {
|
|
@@ -5768,7 +5428,8 @@ var NL_GUARDED_ACTIONS = {
|
|
|
5768
5428
|
"cloud-pull": "cloud-pull",
|
|
5769
5429
|
env: "env-write",
|
|
5770
5430
|
save: "save",
|
|
5771
|
-
sync: "sync"
|
|
5431
|
+
sync: "sync",
|
|
5432
|
+
restore: "restore"
|
|
5772
5433
|
};
|
|
5773
5434
|
function isHighRisk(action) {
|
|
5774
5435
|
return HIGH_RISK_ACTIONS.includes(action);
|
|
@@ -5790,6 +5451,11 @@ async function runGuarded(action, deps, run) {
|
|
|
5790
5451
|
return { outcome: { ran: true, guard, reason: "low-risk" }, result: await run() };
|
|
5791
5452
|
}
|
|
5792
5453
|
if (guard === "warn") {
|
|
5454
|
+
const canConfirm = deps.isTTY ?? !!process.stdin.isTTY;
|
|
5455
|
+
if (!deps.approved && !canConfirm) {
|
|
5456
|
+
log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uC9C0\uB9CC \uBE44\uB300\uD654\uD615+\uBBF8\uC2B9\uC778 \u2192 \uC911\uB2E8. (--yes \uB85C \uC2B9\uC778)`);
|
|
5457
|
+
return { outcome: { ran: false, guard, reason: "lite-noninteractive-block" } };
|
|
5458
|
+
}
|
|
5793
5459
|
log2(`\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action}) \u2014 lite \uBAA8\uB4DC: \uACBD\uACE0\uB9CC \uD558\uACE0 \uC9C4\uD589\uD569\uB2C8\uB2E4.`);
|
|
5794
5460
|
return { outcome: { ran: true, guard, reason: "lite-warn" }, result: await run() };
|
|
5795
5461
|
}
|
|
@@ -5797,7 +5463,7 @@ async function runGuarded(action, deps, run) {
|
|
|
5797
5463
|
if (deps.approved === true) {
|
|
5798
5464
|
return { outcome: { ran: true, guard, reason: "approved" }, result: await run() };
|
|
5799
5465
|
}
|
|
5800
|
-
const tty = deps.isTTY ?? !!process.
|
|
5466
|
+
const tty = deps.isTTY ?? !!process.stdin.isTTY;
|
|
5801
5467
|
if (tty && deps.confirm) {
|
|
5802
5468
|
const ok = await deps.confirm();
|
|
5803
5469
|
if (ok) return { outcome: { ran: true, guard, reason: "confirmed" }, result: await run() };
|
|
@@ -5894,6 +5560,10 @@ async function dispatchNlpRoute(route, input) {
|
|
|
5894
5560
|
if (sub === "next") return goalNext();
|
|
5895
5561
|
if (sub === "check") return goalCheck({});
|
|
5896
5562
|
if (sub === "done") return goalDone({});
|
|
5563
|
+
if (sub === "sync") {
|
|
5564
|
+
await goalSync();
|
|
5565
|
+
return;
|
|
5566
|
+
}
|
|
5897
5567
|
return goalList();
|
|
5898
5568
|
}
|
|
5899
5569
|
case "help":
|
|
@@ -5909,28 +5579,29 @@ var STATE_CHANGING_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5909
5579
|
"init"
|
|
5910
5580
|
]);
|
|
5911
5581
|
function requiresConfirmation(route) {
|
|
5912
|
-
|
|
5582
|
+
const goalSync2 = route.command === "goal" && route.args?.[0] === "sync";
|
|
5583
|
+
return route.confidence === "low" || STATE_CHANGING_COMMANDS.has(route.command) || goalSync2;
|
|
5913
5584
|
}
|
|
5914
5585
|
async function runNaturalLanguageRoute(input) {
|
|
5915
5586
|
const route = routeNaturalLanguage(input);
|
|
5916
5587
|
if (!route) {
|
|
5917
|
-
console.log(
|
|
5588
|
+
console.log(chalk32.yellow(`
|
|
5918
5589
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
5919
5590
|
`));
|
|
5920
5591
|
return;
|
|
5921
5592
|
}
|
|
5922
5593
|
console.log("");
|
|
5923
|
-
console.log(
|
|
5924
|
-
console.log(
|
|
5594
|
+
console.log(chalk32.cyan(` \u{1F4AC} "${input}"`));
|
|
5595
|
+
console.log(chalk32.cyan(` \u2192 ${route.explanation}`));
|
|
5925
5596
|
if (requiresConfirmation(route)) {
|
|
5926
|
-
const { confirm } = await
|
|
5597
|
+
const { confirm } = await inquirer12.prompt([{
|
|
5927
5598
|
type: "confirm",
|
|
5928
5599
|
name: "confirm",
|
|
5929
5600
|
message: `${route.explanation} \u2014 ${ko.nlp.matched}`,
|
|
5930
5601
|
default: true
|
|
5931
5602
|
}]);
|
|
5932
5603
|
if (!confirm) {
|
|
5933
|
-
console.log(
|
|
5604
|
+
console.log(chalk32.dim(` ${ko.nlp.menuHint}`));
|
|
5934
5605
|
return;
|
|
5935
5606
|
}
|
|
5936
5607
|
}
|
|
@@ -5939,7 +5610,7 @@ async function runNaturalLanguageRoute(input) {
|
|
|
5939
5610
|
if (riskAction) {
|
|
5940
5611
|
await runGuarded(
|
|
5941
5612
|
riskAction,
|
|
5942
|
-
{ channel: "nl", approved: false, log: (m) => console.log(
|
|
5613
|
+
{ channel: "nl", approved: false, log: (m) => console.log(chalk32.yellow(` ${m}`)) },
|
|
5943
5614
|
() => dispatchNlpRoute(route, input)
|
|
5944
5615
|
);
|
|
5945
5616
|
return;
|
|
@@ -5948,77 +5619,77 @@ async function runNaturalLanguageRoute(input) {
|
|
|
5948
5619
|
}
|
|
5949
5620
|
|
|
5950
5621
|
// src/commands/agent.ts
|
|
5951
|
-
import
|
|
5622
|
+
import chalk33 from "chalk";
|
|
5952
5623
|
function activeGoalId() {
|
|
5953
5624
|
const goals = listGoals("goals");
|
|
5954
5625
|
const id = selectActiveId(goals);
|
|
5955
5626
|
return id ?? void 0;
|
|
5956
5627
|
}
|
|
5957
5628
|
async function blocker(description) {
|
|
5958
|
-
console.log(
|
|
5629
|
+
console.log(chalk33.bold(`
|
|
5959
5630
|
${ko.agent.blockerTitle}
|
|
5960
5631
|
`));
|
|
5961
5632
|
if (!description || !description.trim()) {
|
|
5962
|
-
console.log(
|
|
5963
|
-
console.log(
|
|
5633
|
+
console.log(chalk33.red(" \u274C \uBE14\uB85C\uCEE4 \uC124\uBA85\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5634
|
+
console.log(chalk33.dim(' \uC608: vhk blocker "tsc \uC5D0\uB7EC \u2014 simple-git \uD0C0\uC785 \uD638\uD658"'));
|
|
5964
5635
|
process.exitCode = 1;
|
|
5965
5636
|
return;
|
|
5966
5637
|
}
|
|
5967
5638
|
const goalId = activeGoalId();
|
|
5968
5639
|
const r = appendBlocker(description, goalId);
|
|
5969
|
-
console.log(
|
|
5640
|
+
console.log(chalk33.green(` \u2705 blocker \uAE30\uB85D (\uD604\uC7AC \uD65C\uC131 ${r.count}\uAC74)`));
|
|
5970
5641
|
if (r.hardStopTripped) {
|
|
5971
|
-
console.log(
|
|
5972
|
-
console.log(
|
|
5642
|
+
console.log(chalk33.red.bold(" \u{1F6D1} HARD_STOP \uC790\uB3D9 \uC0DD\uC131 \u2014 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC911\uB2E8."));
|
|
5643
|
+
console.log(chalk33.yellow(" \uC0AC\uB78C \uAC80\uD1A0 \uD6C4 `vhk resume --confirm` \uC73C\uB85C\uB9CC \uD574\uC81C."));
|
|
5973
5644
|
process.exitCode = 2;
|
|
5974
5645
|
}
|
|
5975
5646
|
}
|
|
5976
5647
|
async function learn(lesson) {
|
|
5977
|
-
console.log(
|
|
5648
|
+
console.log(chalk33.bold(`
|
|
5978
5649
|
${ko.agent.learnTitle}
|
|
5979
5650
|
`));
|
|
5980
5651
|
if (!lesson || !lesson.trim()) {
|
|
5981
|
-
console.log(
|
|
5982
|
-
console.log(
|
|
5652
|
+
console.log(chalk33.red(" \u274C \uAD50\uD6C8 \uB0B4\uC6A9\uC744 \uC785\uB825\uD574 \uC8FC\uC138\uC694."));
|
|
5653
|
+
console.log(chalk33.dim(' \uC608: vhk learn "PowerShell \uC5D0\uC11C\uB294 ; \uC0AC\uC6A9 (&& \uBBF8\uC9C0\uC6D0)"'));
|
|
5983
5654
|
process.exitCode = 1;
|
|
5984
5655
|
return;
|
|
5985
5656
|
}
|
|
5986
5657
|
const goalId = activeGoalId();
|
|
5987
5658
|
appendLearning(lesson, goalId);
|
|
5988
|
-
console.log(
|
|
5659
|
+
console.log(chalk33.green(" \u2705 learnings.md append."));
|
|
5989
5660
|
console.log(
|
|
5990
|
-
|
|
5661
|
+
chalk33.dim(" \uACB0\uC815\uC0AC\uD56D(decision)\uC740 `vhk memory add` \uB85C \uBCC4\uB3C4 \uAE30\uB85D \u2014 SoT \uBD84\uB9AC.")
|
|
5991
5662
|
);
|
|
5992
5663
|
}
|
|
5993
5664
|
async function resume(opts = {}) {
|
|
5994
|
-
console.log(
|
|
5665
|
+
console.log(chalk33.bold(`
|
|
5995
5666
|
${ko.agent.resumeTitle}
|
|
5996
5667
|
`));
|
|
5997
5668
|
if (!isHardStopActive()) {
|
|
5998
|
-
console.log(
|
|
5669
|
+
console.log(chalk33.dim(" HARD_STOP \uD65C\uC131 \uC544\uB2D8 \u2014 \uD560 \uC77C \uC5C6\uC74C."));
|
|
5999
5670
|
return;
|
|
6000
5671
|
}
|
|
6001
5672
|
const reason = readHardStopReason();
|
|
6002
5673
|
if (reason) {
|
|
6003
|
-
console.log(
|
|
6004
|
-
console.log(
|
|
5674
|
+
console.log(chalk33.yellow(" \u{1F4CB} HARD_STOP \uC0AC\uC720:"));
|
|
5675
|
+
console.log(chalk33.dim(` ${reason.split("\n").join("\n ")}`));
|
|
6005
5676
|
console.log("");
|
|
6006
5677
|
}
|
|
6007
5678
|
if (!opts.confirm) {
|
|
6008
5679
|
console.log(
|
|
6009
|
-
|
|
5680
|
+
chalk33.red(
|
|
6010
5681
|
" \u274C --confirm \uD50C\uB798\uADF8 \uC5C6\uC774\uB294 \uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC790\uB3D9 \uD638\uCD9C \uAE08\uC9C0)."
|
|
6011
5682
|
)
|
|
6012
5683
|
);
|
|
6013
|
-
console.log(
|
|
5684
|
+
console.log(chalk33.yellow(" \uC0AC\uC720\uB97C \uD655\uC778\uD55C \uD6C4 \uB2E4\uC2DC: vhk resume --confirm"));
|
|
6014
5685
|
process.exitCode = 1;
|
|
6015
5686
|
return;
|
|
6016
5687
|
}
|
|
6017
5688
|
const removed = clearHardStop();
|
|
6018
5689
|
if (removed) {
|
|
6019
|
-
console.log(
|
|
5690
|
+
console.log(chalk33.green(" \u2705 HARD_STOP \uD574\uC81C. \uC790\uB3D9\uD654 \uC7AC\uAC1C \uAC00\uB2A5."));
|
|
6020
5691
|
} else {
|
|
6021
|
-
console.log(
|
|
5692
|
+
console.log(chalk33.dim(" \uD30C\uC77C\uC774 \uC774\uBBF8 \uC5C6\uC74C \u2014 no-op."));
|
|
6022
5693
|
}
|
|
6023
5694
|
}
|
|
6024
5695
|
|
|
@@ -6031,7 +5702,7 @@ async function guardCli(action, approved, run) {
|
|
|
6031
5702
|
channel: "cli",
|
|
6032
5703
|
approved,
|
|
6033
5704
|
confirm: async () => {
|
|
6034
|
-
const { ok } = await
|
|
5705
|
+
const { ok } = await inquirer13.prompt([{
|
|
6035
5706
|
type: "confirm",
|
|
6036
5707
|
name: "ok",
|
|
6037
5708
|
message: `\u26A0\uFE0F \uC704\uD5D8 \uC791\uC5C5(${action})\uC744 \uC2E4\uD589\uD560\uAE4C\uC694?`,
|
|
@@ -6039,7 +5710,7 @@ async function guardCli(action, approved, run) {
|
|
|
6039
5710
|
}]);
|
|
6040
5711
|
return ok;
|
|
6041
5712
|
},
|
|
6042
|
-
log: (m) => console.log(
|
|
5713
|
+
log: (m) => console.log(chalk34.yellow(` ${m}`))
|
|
6043
5714
|
},
|
|
6044
5715
|
run
|
|
6045
5716
|
);
|
|
@@ -6052,7 +5723,7 @@ async function guardCliDefer(action, approved, run) {
|
|
|
6052
5723
|
approved,
|
|
6053
5724
|
// TTY 면 통과(명령이 자체 확인), 비대화형은 confirm 불가 → 가드가 차단.
|
|
6054
5725
|
confirm: async () => !!process.stdout.isTTY,
|
|
6055
|
-
log: (m) => console.log(
|
|
5726
|
+
log: (m) => console.log(chalk34.yellow(` ${m}`))
|
|
6056
5727
|
},
|
|
6057
5728
|
run
|
|
6058
5729
|
);
|
|
@@ -6148,8 +5819,8 @@ program.command("save").alias("\uC800\uC7A5").option("--yes", "\uD655\uC778 \uC5
|
|
|
6148
5819
|
program.command("undo").alias("\uB418\uB3CC\uB9AC\uAE30").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30").action(async (opts) => {
|
|
6149
5820
|
await guardCliDefer("undo", opts?.yes === true, () => undo());
|
|
6150
5821
|
});
|
|
6151
|
-
program.command("restore").alias("\uBCF5\uC6D0").argument("[id]", "\uBCF5\uC6D0\uD560 \uBC31\uC5C5 id (\uC0DD\uB7B5 \uC2DC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD)").description("sync \uBC31\uC5C5 \uBCF5\uC6D0 (.vhk/backups/ \u2014 \uC5B8\uCEE4\uBC0B \uB36E\uC5B4\uC4F0\uAE30 \uBCF5\uAD6C)").action(async (id) => {
|
|
6152
|
-
await restore(id);
|
|
5822
|
+
program.command("restore").alias("\uBCF5\uC6D0").argument("[id]", "\uBCF5\uC6D0\uD560 \uBC31\uC5C5 id (\uC0DD\uB7B5 \uC2DC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD)").option("--yes", "\uD655\uC778 \uC5C6\uC774 \uC2E4\uD589 (\uC704\uD5D8 \uC791\uC5C5 \uBA85\uC2DC \uC2B9\uC778)").description("sync \uBC31\uC5C5 \uBCF5\uC6D0 (.vhk/backups/ \u2014 \uC5B8\uCEE4\uBC0B \uB36E\uC5B4\uC4F0\uAE30 \uBCF5\uAD6C)").action(async (id, opts) => {
|
|
5823
|
+
await guardCli("restore", opts?.yes === true, () => restore(id));
|
|
6153
5824
|
});
|
|
6154
5825
|
program.command("status").alias("\uC0C1\uD0DC").description("\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC").action(async () => {
|
|
6155
5826
|
await status();
|
|
@@ -6234,7 +5905,7 @@ memoryCmd.command("remove <index>").alias("\uC0AD\uC81C").description("\uAE30\uC
|
|
|
6234
5905
|
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 () => {
|
|
6235
5906
|
await brief();
|
|
6236
5907
|
});
|
|
6237
|
-
var goalCmd = program.command("goal").alias("\uBAA9\uD45C").description("Goal \uB2E8\uACC4\uBCC4 \uBBF8\uC158 \uAD00\uB9AC (init / list / next / check / done)").action(async () => {
|
|
5908
|
+
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 () => {
|
|
6238
5909
|
await goalList();
|
|
6239
5910
|
});
|
|
6240
5911
|
goalCmd.command("list").alias("\uBAA9\uB85D").description("goals/*.md \uBAA9\uB85D (id, status, priority, title)").action(async () => {
|
|
@@ -6246,12 +5917,15 @@ goalCmd.command("next").alias("\uB2E4\uC74C").description("active goal \uC790\uB
|
|
|
6246
5917
|
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 () => {
|
|
6247
5918
|
await goalInit();
|
|
6248
5919
|
});
|
|
6249
|
-
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) => {
|
|
5920
|
+
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) => {
|
|
6250
5921
|
await goalCheck(opts);
|
|
6251
5922
|
});
|
|
6252
5923
|
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) => {
|
|
6253
5924
|
await goalDone(opts);
|
|
6254
5925
|
});
|
|
5926
|
+
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 () => {
|
|
5927
|
+
await goalSync();
|
|
5928
|
+
});
|
|
6255
5929
|
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) => {
|
|
6256
5930
|
await blocker(description);
|
|
6257
5931
|
});
|
|
@@ -6269,7 +5943,7 @@ program.on("command:*", async (operands) => {
|
|
|
6269
5943
|
});
|
|
6270
5944
|
program.action(async () => {
|
|
6271
5945
|
console.log("\n\u{1F3AF} VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58\n");
|
|
6272
|
-
const { choice } = await
|
|
5946
|
+
const { choice } = await inquirer13.prompt([{
|
|
6273
5947
|
type: "list",
|
|
6274
5948
|
name: "choice",
|
|
6275
5949
|
message: "\uBB58 \uB3C4\uC640\uB4DC\uB9B4\uAE4C\uC694?",
|
|
@@ -6326,9 +6000,9 @@ if (isMainModule) {
|
|
|
6326
6000
|
}
|
|
6327
6001
|
} catch (err) {
|
|
6328
6002
|
if (isPromptAbortError(err)) {
|
|
6329
|
-
console.error(
|
|
6003
|
+
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)"));
|
|
6330
6004
|
} else {
|
|
6331
|
-
console.error(
|
|
6005
|
+
console.error(chalk34.red(`
|
|
6332
6006
|
\u274C ${err instanceof Error ? err.message : String(err)}`));
|
|
6333
6007
|
}
|
|
6334
6008
|
process.exitCode = 1;
|