@byh3071/vhk 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/dist/index.js +573 -375
- package/package.json +56 -56
package/dist/index.js
CHANGED
|
@@ -485,7 +485,7 @@ var require_ignore = __commonJS({
|
|
|
485
485
|
|
|
486
486
|
// src/index.ts
|
|
487
487
|
import { Command, Help } from "commander";
|
|
488
|
-
import
|
|
488
|
+
import chalk16 from "chalk";
|
|
489
489
|
import inquirer7 from "inquirer";
|
|
490
490
|
|
|
491
491
|
// src/lib/nlp-router.ts
|
|
@@ -495,7 +495,7 @@ function normalize(input) {
|
|
|
495
495
|
var NLP_KEYWORDS = {
|
|
496
496
|
save: ["\uC800\uC7A5", "\uC138\uC774\uBE0C", "\uCEE4\uBC0B", "\uC62C\uB824", "\uC62C\uB9AC\uAE30", "\uD478\uC2DC", "push", "commit"],
|
|
497
497
|
undo: ["\uB418\uB3CC\uB824", "\uB418\uB3CC\uB9AC\uAE30", "\uCDE8\uC18C", "\uC6D0\uB798\uB300\uB85C", "\uB864\uBC31", "\uB9AC\uC14B", "reset", "rollback"],
|
|
498
|
-
status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"
|
|
498
|
+
status: ["\uC0C1\uD0DC", "\uD604\uD669", "\uC5B4\uB5BB\uAC8C", "\uC5B4\uB54C", "\uC9C0\uAE08"],
|
|
499
499
|
diff: ["\uBCC0\uACBD", "\uBC14\uB010", "\uBB50\uBC14\uB01C", "\uCC28\uC774", "\uB2EC\uB77C\uC9C4", "\uC218\uC815\uB41C"]
|
|
500
500
|
};
|
|
501
501
|
function matchesKeywords(text, command) {
|
|
@@ -524,6 +524,24 @@ var RULES = [
|
|
|
524
524
|
confidence: "high",
|
|
525
525
|
test: (t2) => /프로젝트.*(만들|시작)|폴더.*만들|만들고\s*싶|하네스|초기화/.test(t2) || /^시작$/.test(t2)
|
|
526
526
|
},
|
|
527
|
+
{
|
|
528
|
+
command: "secure",
|
|
529
|
+
explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548)",
|
|
530
|
+
confidence: "high",
|
|
531
|
+
test: (t2) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t2)
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
command: "check",
|
|
535
|
+
explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
|
|
536
|
+
confidence: "high",
|
|
537
|
+
test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
command: "doctor",
|
|
541
|
+
explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
|
|
542
|
+
confidence: "high",
|
|
543
|
+
test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단|확인)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
|
|
544
|
+
},
|
|
527
545
|
{
|
|
528
546
|
command: "diff",
|
|
529
547
|
explanation: "\uBCC0\uACBD\uC0AC\uD56D \uC694\uC57D (vhk diff)",
|
|
@@ -540,7 +558,7 @@ var RULES = [
|
|
|
540
558
|
command: "status",
|
|
541
559
|
explanation: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778 (vhk \uC0C1\uD0DC)",
|
|
542
560
|
confidence: "high",
|
|
543
|
-
test: (t2) => matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태/.test(t2)
|
|
561
|
+
test: (t2) => (matchesKeywords(t2, "status") || /^status$/.test(t2) || /브랜치.*(뭐|어디)|git\s*상태|동기화\s*상태|프로젝트\s*상태/.test(t2)) && !/보안|시크릿|규칙|점검|린트|환경|진단|doctor|secure|check|스캔|설치/.test(t2)
|
|
544
562
|
},
|
|
545
563
|
{
|
|
546
564
|
command: "save",
|
|
@@ -554,30 +572,12 @@ var RULES = [
|
|
|
554
572
|
confidence: "high",
|
|
555
573
|
test: (t2) => /오늘.*(정리|기록)|한\s*일|세션|회고|recap|정리해/.test(t2)
|
|
556
574
|
},
|
|
557
|
-
{
|
|
558
|
-
command: "doctor",
|
|
559
|
-
explanation: "\uD658\uACBD \uC810\uAC80 (vhk doctor)",
|
|
560
|
-
confidence: "high",
|
|
561
|
-
test: (t2) => /뭔가\s*안|안\s*돼|안돼|환경\s*(점검|진단)|진단|doctor|설치.*확인|왜\s*안/.test(t2)
|
|
562
|
-
},
|
|
563
575
|
{
|
|
564
576
|
command: "gate",
|
|
565
577
|
explanation: "\uC544\uC774\uB514\uC5B4 \uAC80\uC99D (vhk \uAC80\uC99D)",
|
|
566
578
|
confidence: "high",
|
|
567
579
|
test: (t2) => /아이디어|검증|gate|go\/refine|pain\s*point/.test(t2)
|
|
568
580
|
},
|
|
569
|
-
{
|
|
570
|
-
command: "secure",
|
|
571
|
-
explanation: "\uBCF4\uC548 \uC2A4\uCE94 (vhk \uBCF4\uC548 scan)",
|
|
572
|
-
confidence: "high",
|
|
573
|
-
test: (t2) => /보안|시크릿|비밀|키\s*유출|secure|scan/.test(t2)
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
command: "check",
|
|
577
|
-
explanation: "\uADDC\uCE59 \uC810\uAC80 (vhk \uC810\uAC80)",
|
|
578
|
-
confidence: "high",
|
|
579
|
-
test: (t2) => /규칙.*(점검|위반)|린트|check|위반/.test(t2)
|
|
580
|
-
},
|
|
581
581
|
{
|
|
582
582
|
command: "sync",
|
|
583
583
|
explanation: "\uADDC\uCE59 \uD30C\uC77C \uB3D9\uAE30\uD654 (vhk \uADDC\uCE59)",
|
|
@@ -642,7 +642,15 @@ var ko = {
|
|
|
642
642
|
successLocal: "\uB85C\uCEEC \uC800\uC7A5 \uC644\uB8CC!",
|
|
643
643
|
noRemote: "\uC6D0\uACA9 \uC800\uC7A5\uC18C\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC544 push\uB97C \uAC74\uB108\uB6F0\uC5C8\uC2B5\uB2C8\uB2E4.",
|
|
644
644
|
failed: "\uC800\uC7A5 \uC2E4\uD328",
|
|
645
|
-
|
|
645
|
+
stagedAfterFail: "\uCEE4\uBC0B\uC740 \uC2E4\uD328\uD588\uC9C0\uB9CC \uD30C\uC77C\uC740 \uC2A4\uD14C\uC774\uC9D5\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uD655\uC778: git status / \uCDE8\uC18C: git reset HEAD",
|
|
646
|
+
securityWarnHeader: "\uC800\uC7A5 \uC804 \uBCF4\uC548 \uD655\uC778:",
|
|
647
|
+
secretsFound: (n) => `\uCF54\uB4DC\uC5D0\uC11C CRITICAL/HIGH \uC2DC\uD06C\uB9BF \uD328\uD134 ${n}\uAC74 \uAC10\uC9C0`,
|
|
648
|
+
secretsConfirm: "\uADF8\uB798\uB3C4 \uCEE4\uBC0B\xB7push\uB97C \uC9C4\uD589\uD560\uAE4C\uC694?",
|
|
649
|
+
cancelled: "\uC800\uC7A5\uC744 \uCDE8\uC18C\uD588\uC2B5\uB2C8\uB2E4.",
|
|
650
|
+
pushFailed: "push \uC2E4\uD328 (\uB85C\uCEEC \uCEE4\uBC0B\uC740 \uC644\uB8CC\uB428)",
|
|
651
|
+
commitOkPushFailed: "\uB85C\uCEEC \uCEE4\uBC0B\uC740 \uB410\uC9C0\uB9CC \uC6D0\uACA9 push\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. git push\uB97C \uC9C1\uC811 \uD655\uC778\uD558\uC138\uC694.",
|
|
652
|
+
done: (n) => `${n}\uAC1C \uD30C\uC77C \uC800\uC7A5 \uC644\uB8CC!`,
|
|
653
|
+
doneLocalOnly: (n) => `${n}\uAC1C \uD30C\uC77C \uB85C\uCEEC \uC800\uC7A5\uB428 (push\uB294 \uC2E4\uD328)`
|
|
646
654
|
},
|
|
647
655
|
undo: {
|
|
648
656
|
title: "\uB418\uB3CC\uB9AC\uAE30",
|
|
@@ -651,10 +659,14 @@ var ko = {
|
|
|
651
659
|
recentHeader: "\u{1F4CB} \uCD5C\uADFC \uCEE4\uBC0B:",
|
|
652
660
|
howMany: "\uBA87 \uAC1C\uC758 \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9B4\uAE4C\uC694?",
|
|
653
661
|
alreadyPushed: "\uC774 \uCEE4\uBC0B\uC740 \uC774\uBBF8 \uC6D0\uACA9\uC5D0 \uC62C\uB77C\uAC14\uC2B5\uB2C8\uB2E4. \uB418\uB3CC\uB9AC\uBA74 \uCDA9\uB3CC\uC774 \uC0DD\uAE38 \uC218 \uC788\uC5B4\uC694.",
|
|
662
|
+
noUpstreamWarning: "upstream \uBE0C\uB79C\uCE58\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBBF8 push\uD55C \uCEE4\uBC0B\uC77C \uC218 \uC788\uC5B4\uC694. \uB418\uB3CC\uB9B0 \uB4A4 force push\uAC00 \uD544\uC694\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
654
663
|
confirmMessage: "\uCD5C\uADFC \uCEE4\uBC0B\uC744 \uB418\uB3CC\uB9AC\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?",
|
|
664
|
+
confirmRisky: (n) => `\u26A0\uFE0F \uC704\uD5D8: \uCD5C\uADFC ${n}\uAC1C \uCEE4\uBC0B\uC744 soft reset\uD569\uB2C8\uB2E4. \uC6D0\uACA9\uACFC \uC5B4\uAE0B\uB0A0 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uACC4\uC18D\uD560\uAE4C\uC694?`,
|
|
655
665
|
cancelled: "\uCDE8\uC18C\uB428",
|
|
656
666
|
success: "\uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC! \uBCC0\uACBD\uC0AC\uD56D\uC740 \uADF8\uB300\uB85C \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.",
|
|
657
667
|
stagedHint: "\uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uB0A8\uC544 \uC788\uC5B4\uC694.",
|
|
668
|
+
rootCommit: "\uCCAB \uCEE4\uBC0B\uB9CC \uC788\uC5B4\uC11C \uB354 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
669
|
+
forcePushHint: "\uC6D0\uACA9\uACFC \uB9DE\uCD94\uB824\uBA74: git push --force-with-lease (\uD63C\uC790 \uC791\uC5C5\uD55C \uBE0C\uB79C\uCE58\uC5D0\uC11C\uB9CC, \uD300\uACFC \uD569\uC758 \uD6C4)",
|
|
658
670
|
failed: "\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328"
|
|
659
671
|
},
|
|
660
672
|
diff: {
|
|
@@ -1016,9 +1028,9 @@ ${ko.gate.verdictTitle}
|
|
|
1016
1028
|
|
|
1017
1029
|
// src/commands/init.ts
|
|
1018
1030
|
import inquirer2 from "inquirer";
|
|
1019
|
-
import
|
|
1020
|
-
import
|
|
1021
|
-
import
|
|
1031
|
+
import chalk5 from "chalk";
|
|
1032
|
+
import fs3 from "fs";
|
|
1033
|
+
import path3 from "path";
|
|
1022
1034
|
|
|
1023
1035
|
// src/templates/claude-md.ts
|
|
1024
1036
|
function CLAUDE_MD_TEMPLATE(name, _stack) {
|
|
@@ -1228,27 +1240,113 @@ function COMMANDS_MD_TEMPLATE() {
|
|
|
1228
1240
|
].join("\n");
|
|
1229
1241
|
}
|
|
1230
1242
|
|
|
1231
|
-
// src/
|
|
1243
|
+
// src/lib/check-secure.ts
|
|
1244
|
+
var import_ignore = __toESM(require_ignore(), 1);
|
|
1245
|
+
import fs from "fs";
|
|
1246
|
+
import path from "path";
|
|
1232
1247
|
import chalk3 from "chalk";
|
|
1248
|
+
function loadGitignore(rootDir) {
|
|
1249
|
+
const ig = (0, import_ignore.default)();
|
|
1250
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1251
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1252
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
1253
|
+
ig.add(content);
|
|
1254
|
+
}
|
|
1255
|
+
return ig;
|
|
1256
|
+
}
|
|
1257
|
+
function isPathIgnored(ig, relativePath) {
|
|
1258
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
1259
|
+
return ig.ignores(normalized);
|
|
1260
|
+
}
|
|
1261
|
+
function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDepth = 8) {
|
|
1262
|
+
const exposed = [];
|
|
1263
|
+
function walk(dir, depth) {
|
|
1264
|
+
if (depth > maxDepth) return;
|
|
1265
|
+
let entries;
|
|
1266
|
+
try {
|
|
1267
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1268
|
+
} catch {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
for (const entry of entries) {
|
|
1272
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1273
|
+
const fullPath = path.join(dir, entry.name);
|
|
1274
|
+
const rel = path.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1275
|
+
if (entry.isDirectory()) {
|
|
1276
|
+
if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
if (isSensitiveName(entry.name) && !isPathIgnored(ig, rel)) {
|
|
1280
|
+
exposed.push(rel);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
walk(rootDir, 0);
|
|
1285
|
+
return exposed;
|
|
1286
|
+
}
|
|
1287
|
+
function isSensitiveName(name) {
|
|
1288
|
+
const lower = name.toLowerCase();
|
|
1289
|
+
if (lower === ".env" || lower.startsWith(".env.")) return true;
|
|
1290
|
+
if (lower.endsWith(".pem") || lower.endsWith(".key")) return true;
|
|
1291
|
+
if (lower === "credentials.json" || lower === "secrets.json") return true;
|
|
1292
|
+
if (lower.startsWith("id_rsa")) return true;
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
function checkProjectSecurity(rootDir = process.cwd()) {
|
|
1296
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
1297
|
+
const missingGitignore = !fs.existsSync(gitignorePath);
|
|
1298
|
+
const ig = loadGitignore(rootDir);
|
|
1299
|
+
const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
|
|
1300
|
+
const warnings = [];
|
|
1301
|
+
if (missingGitignore) {
|
|
1302
|
+
warnings.push(".gitignore \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBBFC\uAC10\uD55C \uD30C\uC77C\uC774 \uC2E4\uC218\uB85C \uC62C\uB77C\uAC08 \uC218 \uC788\uC5B4\uC694.");
|
|
1303
|
+
}
|
|
1304
|
+
if (exposedPaths.length > 0) {
|
|
1305
|
+
warnings.push(
|
|
1306
|
+
`ignore\uB418\uC9C0 \uC54A\uC740 \uBBFC\uAC10 \uD30C\uC77C ${exposedPaths.length}\uAC1C: ${exposedPaths.join(", ")}`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
return {
|
|
1310
|
+
ok: !missingGitignore && exposedPaths.length === 0,
|
|
1311
|
+
missingGitignore,
|
|
1312
|
+
exposedPaths,
|
|
1313
|
+
warnings
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function printSecurityWarnings(rootDir = process.cwd()) {
|
|
1317
|
+
const result = checkProjectSecurity(rootDir);
|
|
1318
|
+
if (result.ok) return true;
|
|
1319
|
+
for (const w of result.warnings) {
|
|
1320
|
+
console.log(chalk3.yellow(` \u26A0\uFE0F ${w}`));
|
|
1321
|
+
}
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1325
|
+
const ig = loadGitignore(rootDir);
|
|
1326
|
+
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/utils/logger.ts
|
|
1330
|
+
import chalk4 from "chalk";
|
|
1233
1331
|
var log = {
|
|
1234
|
-
success: (msg) => console.log(
|
|
1235
|
-
error: (msg) => console.log(
|
|
1236
|
-
warn: (msg) => console.log(
|
|
1237
|
-
info: (msg) => console.log(
|
|
1238
|
-
step: (msg) => console.log(
|
|
1332
|
+
success: (msg) => console.log(chalk4.green(`\u2705 ${msg}`)),
|
|
1333
|
+
error: (msg) => console.log(chalk4.red(`\u274C ${msg}`)),
|
|
1334
|
+
warn: (msg) => console.log(chalk4.yellow(`\u26A0\uFE0F ${msg}`)),
|
|
1335
|
+
info: (msg) => console.log(chalk4.blue(`\u2139\uFE0F ${msg}`)),
|
|
1336
|
+
step: (msg) => console.log(chalk4.bold(`
|
|
1239
1337
|
\u25B8 ${msg}`))
|
|
1240
1338
|
};
|
|
1241
1339
|
|
|
1242
1340
|
// src/utils/file.ts
|
|
1243
|
-
import
|
|
1244
|
-
import
|
|
1341
|
+
import fs2 from "fs";
|
|
1342
|
+
import path2 from "path";
|
|
1245
1343
|
function writeFile(filePath, content) {
|
|
1246
|
-
const dir =
|
|
1247
|
-
if (!
|
|
1248
|
-
|
|
1344
|
+
const dir = path2.dirname(filePath);
|
|
1345
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
1346
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
1249
1347
|
}
|
|
1250
1348
|
function fileExists(filePath) {
|
|
1251
|
-
return
|
|
1349
|
+
return fs2.existsSync(filePath);
|
|
1252
1350
|
}
|
|
1253
1351
|
|
|
1254
1352
|
// src/lib/notion-import.ts
|
|
@@ -1440,13 +1538,14 @@ async function collectAnswers(options, defaults = {}) {
|
|
|
1440
1538
|
async function init(options = {}) {
|
|
1441
1539
|
const skipGate = Boolean(options.skipGate || options.fromNotion);
|
|
1442
1540
|
if (skipGate) {
|
|
1443
|
-
console.log(
|
|
1541
|
+
console.log(chalk5.dim(`
|
|
1444
1542
|
${ko.init.skipGate}
|
|
1445
1543
|
`));
|
|
1446
1544
|
}
|
|
1447
|
-
console.log(
|
|
1545
|
+
console.log(chalk5.bold(`
|
|
1448
1546
|
${ko.init.title}
|
|
1449
1547
|
`));
|
|
1548
|
+
printSecurityWarnings();
|
|
1450
1549
|
let prdContent = {};
|
|
1451
1550
|
const defaults = {};
|
|
1452
1551
|
if (options.fromNotion) {
|
|
@@ -1468,7 +1567,7 @@ ${ko.init.title}
|
|
|
1468
1567
|
process.exit(1);
|
|
1469
1568
|
}
|
|
1470
1569
|
const stack = STACK_PRESETS[answers.type];
|
|
1471
|
-
console.log(
|
|
1570
|
+
console.log(chalk5.dim(`
|
|
1472
1571
|
${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
1473
1572
|
`));
|
|
1474
1573
|
if (!options.yes) {
|
|
@@ -1487,7 +1586,7 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1487
1586
|
const files = generateFiles(answers.name, answers.description, stack, prdContent);
|
|
1488
1587
|
log.step(ko.init.filesGenerating);
|
|
1489
1588
|
for (const [filePath, content] of Object.entries(files)) {
|
|
1490
|
-
const fullPath =
|
|
1589
|
+
const fullPath = path3.join(cwd, filePath);
|
|
1491
1590
|
if (fileExists(fullPath)) {
|
|
1492
1591
|
const { overwrite } = await inquirer2.prompt([{
|
|
1493
1592
|
type: "confirm",
|
|
@@ -1504,21 +1603,21 @@ ${ko.init.recommendedStack} ${stack.join(" + ")}
|
|
|
1504
1603
|
log.success(filePath);
|
|
1505
1604
|
}
|
|
1506
1605
|
await writeInitExtras(cwd);
|
|
1507
|
-
console.log(
|
|
1606
|
+
console.log(chalk5.bold.green(`
|
|
1508
1607
|
${ko.init.done}`));
|
|
1509
|
-
console.log(
|
|
1608
|
+
console.log(chalk5.dim(`
|
|
1510
1609
|
${ko.init.nextSteps}`));
|
|
1511
1610
|
if (options.fromNotion) {
|
|
1512
1611
|
console.log(` 1. ${ko.init.notionReviewHint}`);
|
|
1513
1612
|
console.log(` 2. ${ko.init.gitHintLabel}`);
|
|
1514
|
-
console.log(` ${
|
|
1613
|
+
console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
|
|
1515
1614
|
console.log(` 3. ${ko.init.startDev}
|
|
1516
1615
|
`);
|
|
1517
1616
|
} else {
|
|
1518
1617
|
console.log(` 1. ${ko.init.fillHint}`);
|
|
1519
1618
|
console.log(` 2. ${ko.init.prdHint}`);
|
|
1520
1619
|
console.log(` 3. ${ko.init.gitHintLabel}`);
|
|
1521
|
-
console.log(` ${
|
|
1620
|
+
console.log(` ${chalk5.cyan(ko.init.gitHintCommand)}`);
|
|
1522
1621
|
console.log(` 4. ${ko.init.startDev}
|
|
1523
1622
|
`);
|
|
1524
1623
|
}
|
|
@@ -1558,7 +1657,7 @@ function generateFiles(name, description, stack, prdContent = {}) {
|
|
|
1558
1657
|
};
|
|
1559
1658
|
}
|
|
1560
1659
|
var VHK_PACKAGE_SCRIPTS = {
|
|
1561
|
-
save: "
|
|
1660
|
+
save: "vhk save",
|
|
1562
1661
|
check: "vhk check",
|
|
1563
1662
|
scan: "vhk secure scan",
|
|
1564
1663
|
recap: "vhk recap",
|
|
@@ -1566,15 +1665,15 @@ var VHK_PACKAGE_SCRIPTS = {
|
|
|
1566
1665
|
doctor: "vhk doctor"
|
|
1567
1666
|
};
|
|
1568
1667
|
function enhancePackageScripts(projectDir) {
|
|
1569
|
-
const pkgPath =
|
|
1570
|
-
if (!
|
|
1571
|
-
const pkg = JSON.parse(
|
|
1668
|
+
const pkgPath = path3.join(projectDir, "package.json");
|
|
1669
|
+
if (!fs3.existsSync(pkgPath)) return false;
|
|
1670
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1572
1671
|
pkg.scripts = { ...pkg.scripts, ...VHK_PACKAGE_SCRIPTS };
|
|
1573
|
-
|
|
1672
|
+
fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1574
1673
|
return true;
|
|
1575
1674
|
}
|
|
1576
1675
|
async function writeInitExtras(projectDir) {
|
|
1577
|
-
const commandsPath =
|
|
1676
|
+
const commandsPath = path3.join(projectDir, "COMMANDS.md");
|
|
1578
1677
|
if (fileExists(commandsPath)) {
|
|
1579
1678
|
const { overwrite } = await inquirer2.prompt([{
|
|
1580
1679
|
type: "confirm",
|
|
@@ -1599,37 +1698,13 @@ async function writeInitExtras(projectDir) {
|
|
|
1599
1698
|
|
|
1600
1699
|
// src/commands/recap.ts
|
|
1601
1700
|
import inquirer3 from "inquirer";
|
|
1602
|
-
import
|
|
1701
|
+
import chalk6 from "chalk";
|
|
1603
1702
|
import fs5 from "fs";
|
|
1604
1703
|
import path6 from "path";
|
|
1605
1704
|
|
|
1606
1705
|
// src/lib/git.ts
|
|
1607
1706
|
import path4 from "path";
|
|
1608
1707
|
import simpleGit from "simple-git";
|
|
1609
|
-
|
|
1610
|
-
// src/lib/check-secure.ts
|
|
1611
|
-
var import_ignore = __toESM(require_ignore(), 1);
|
|
1612
|
-
import fs3 from "fs";
|
|
1613
|
-
import path3 from "path";
|
|
1614
|
-
function loadGitignore(rootDir) {
|
|
1615
|
-
const ig = (0, import_ignore.default)();
|
|
1616
|
-
const gitignorePath = path3.join(rootDir, ".gitignore");
|
|
1617
|
-
if (fs3.existsSync(gitignorePath)) {
|
|
1618
|
-
const content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
1619
|
-
ig.add(content);
|
|
1620
|
-
}
|
|
1621
|
-
return ig;
|
|
1622
|
-
}
|
|
1623
|
-
function isPathIgnored(ig, relativePath) {
|
|
1624
|
-
const normalized = relativePath.replace(/\\/g, "/");
|
|
1625
|
-
return ig.ignores(normalized);
|
|
1626
|
-
}
|
|
1627
|
-
function filterTrackedPaths(paths, rootDir = process.cwd()) {
|
|
1628
|
-
const ig = loadGitignore(rootDir);
|
|
1629
|
-
return paths.filter((p) => !isPathIgnored(ig, p.replace(/\\/g, "/")));
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// src/lib/git.ts
|
|
1633
1708
|
var git = simpleGit();
|
|
1634
1709
|
function isNoiseRecapPath(filePath) {
|
|
1635
1710
|
const base = path4.basename(filePath);
|
|
@@ -1646,29 +1721,19 @@ function filterRecapFiles(files) {
|
|
|
1646
1721
|
const tracked = new Set(filterTrackedPaths(paths));
|
|
1647
1722
|
return files.filter((f) => tracked.has(f.file) && !isNoiseRecapPath(f.file));
|
|
1648
1723
|
}
|
|
1649
|
-
function
|
|
1650
|
-
if (
|
|
1651
|
-
if (
|
|
1652
|
-
if (workingDir === "R") return "renamed";
|
|
1724
|
+
function inferFileStatusFromDiff(insertions, deletions) {
|
|
1725
|
+
if (deletions > 0 && insertions === 0) return "deleted";
|
|
1726
|
+
if (insertions > 0 && deletions === 0) return "new";
|
|
1653
1727
|
return "modified";
|
|
1654
1728
|
}
|
|
1655
|
-
|
|
1656
|
-
const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1657
|
-
const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
|
|
1658
|
-
const statusResult = await git.status();
|
|
1659
|
-
const statByFile = new Map(
|
|
1660
|
-
diffSummary.files.map((f) => [f.file, f])
|
|
1661
|
-
);
|
|
1729
|
+
function buildSessionDiffFromSummary(diffSummary) {
|
|
1662
1730
|
const files = filterRecapFiles(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
status: fileStatus(f.working_dir)
|
|
1670
|
-
};
|
|
1671
|
-
})
|
|
1731
|
+
diffSummary.files.map((f) => ({
|
|
1732
|
+
file: f.file,
|
|
1733
|
+
insertions: f.insertions,
|
|
1734
|
+
deletions: f.deletions,
|
|
1735
|
+
status: inferFileStatusFromDiff(f.insertions, f.deletions)
|
|
1736
|
+
}))
|
|
1672
1737
|
);
|
|
1673
1738
|
return {
|
|
1674
1739
|
filesChanged: files.length,
|
|
@@ -1677,6 +1742,11 @@ async function getSessionDiff(since) {
|
|
|
1677
1742
|
files
|
|
1678
1743
|
};
|
|
1679
1744
|
}
|
|
1745
|
+
async function getSessionDiff(since) {
|
|
1746
|
+
const sinceDate = since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1747
|
+
const diffSummary = await git.diffSummary([`--since=${sinceDate}`]);
|
|
1748
|
+
return buildSessionDiffFromSummary(diffSummary);
|
|
1749
|
+
}
|
|
1680
1750
|
async function getRecentCommits(count = 10, since) {
|
|
1681
1751
|
const options = { maxCount: count };
|
|
1682
1752
|
if (since) options["--since"] = since;
|
|
@@ -1787,39 +1857,40 @@ function createAdrFile(cwd, title, context, decision, consequences) {
|
|
|
1787
1857
|
|
|
1788
1858
|
// src/commands/recap.ts
|
|
1789
1859
|
async function recap(options = {}) {
|
|
1790
|
-
console.log(
|
|
1860
|
+
console.log(chalk6.bold(`
|
|
1791
1861
|
${ko.recap.title}
|
|
1792
1862
|
`));
|
|
1793
1863
|
if (!await isGitRepo()) {
|
|
1794
|
-
console.log(
|
|
1864
|
+
console.log(chalk6.red(ko.recap.noRepo));
|
|
1795
1865
|
return;
|
|
1796
1866
|
}
|
|
1797
|
-
|
|
1867
|
+
printSecurityWarnings();
|
|
1868
|
+
console.log(chalk6.dim(`${ko.recap.analyzing}
|
|
1798
1869
|
`));
|
|
1799
1870
|
const since = options.since || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1800
1871
|
const diff2 = await getSessionDiff(since);
|
|
1801
1872
|
const commits = await getRecentCommits(10, since);
|
|
1802
1873
|
if (diff2.filesChanged === 0 && commits.length === 0) {
|
|
1803
|
-
console.log(
|
|
1874
|
+
console.log(chalk6.yellow(ko.recap.noChanges));
|
|
1804
1875
|
return;
|
|
1805
1876
|
}
|
|
1806
|
-
console.log(
|
|
1807
|
-
console.log(` \uD30C\uC77C: ${
|
|
1808
|
-
console.log(` \uCD94\uAC00: ${
|
|
1877
|
+
console.log(chalk6.bold("\u{1F4CA} \uBCC0\uACBD \uC694\uC57D:"));
|
|
1878
|
+
console.log(` \uD30C\uC77C: ${chalk6.cyan(String(diff2.filesChanged))}\uAC1C \uBCC0\uACBD`);
|
|
1879
|
+
console.log(` \uCD94\uAC00: ${chalk6.green("+" + diff2.insertions)} / \uC0AD\uC81C: ${chalk6.red("-" + diff2.deletions)}`);
|
|
1809
1880
|
if (diff2.files.length > 0) {
|
|
1810
|
-
console.log(
|
|
1881
|
+
console.log(chalk6.dim("\n \uBCC0\uACBD \uD30C\uC77C:"));
|
|
1811
1882
|
diff2.files.slice(0, 15).forEach((f) => {
|
|
1812
|
-
const icon = f.status === "new" ?
|
|
1883
|
+
const icon = f.status === "new" ? chalk6.green("\u{1F195}") : f.status === "deleted" ? chalk6.red("\u{1F5D1}\uFE0F") : chalk6.yellow("\u270F\uFE0F");
|
|
1813
1884
|
console.log(` ${icon} ${f.file}`);
|
|
1814
1885
|
});
|
|
1815
1886
|
if (diff2.files.length > 15) {
|
|
1816
|
-
console.log(
|
|
1887
|
+
console.log(chalk6.dim(` ... \uC678 ${diff2.files.length - 15}\uAC1C`));
|
|
1817
1888
|
}
|
|
1818
1889
|
}
|
|
1819
1890
|
if (commits.length > 0) {
|
|
1820
|
-
console.log(
|
|
1891
|
+
console.log(chalk6.dim("\n \uCD5C\uADFC \uCEE4\uBC0B:"));
|
|
1821
1892
|
commits.slice(0, 5).forEach((c) => {
|
|
1822
|
-
console.log(
|
|
1893
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1823
1894
|
});
|
|
1824
1895
|
}
|
|
1825
1896
|
console.log("");
|
|
@@ -1887,11 +1958,11 @@ ${ko.recap.title}
|
|
|
1887
1958
|
fs5.writeFileSync(filePath, content, "utf-8");
|
|
1888
1959
|
const adrCandidates = detectAdrCandidates(diff2);
|
|
1889
1960
|
if (adrCandidates.length > 0) {
|
|
1890
|
-
console.log(
|
|
1961
|
+
console.log(chalk6.cyan.bold(`
|
|
1891
1962
|
${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
1892
1963
|
for (const candidate of adrCandidates) {
|
|
1893
|
-
console.log(
|
|
1894
|
-
candidate.files.forEach((f) => console.log(
|
|
1964
|
+
console.log(chalk6.cyan(` \u2022 ${candidate.title}: ${candidate.context}`));
|
|
1965
|
+
candidate.files.forEach((f) => console.log(chalk6.dim(` ${f}`)));
|
|
1895
1966
|
}
|
|
1896
1967
|
const { createAdr } = await inquirer3.prompt([{
|
|
1897
1968
|
type: "confirm",
|
|
@@ -1921,17 +1992,17 @@ ${ko.recap.adrDetected} (${adrCandidates.length}\uAC74)`));
|
|
|
1921
1992
|
adrAnswers.decision,
|
|
1922
1993
|
adrAnswers.consequences
|
|
1923
1994
|
);
|
|
1924
|
-
console.log(
|
|
1995
|
+
console.log(chalk6.green(` \u2705 ADR \uC0DD\uC131: ${path6.relative(process.cwd(), adrPath)}`));
|
|
1925
1996
|
}
|
|
1926
1997
|
}
|
|
1927
1998
|
}
|
|
1928
1999
|
const troubleshootingKeywords = /fix|bug|error|crash|hotfix|patch|revert|트러블|에러|버그|수정|핫픽스/i;
|
|
1929
2000
|
const troubleCommits = commits.filter((c) => troubleshootingKeywords.test(c.message));
|
|
1930
2001
|
if (troubleCommits.length > 0) {
|
|
1931
|
-
console.log(
|
|
2002
|
+
console.log(chalk6.yellow.bold(`
|
|
1932
2003
|
${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
1933
2004
|
troubleCommits.forEach((c) => {
|
|
1934
|
-
console.log(
|
|
2005
|
+
console.log(chalk6.dim(` \u2022 ${c.message}`));
|
|
1935
2006
|
});
|
|
1936
2007
|
const { createTroubleshoot } = await inquirer3.prompt([{
|
|
1937
2008
|
type: "confirm",
|
|
@@ -1982,12 +2053,12 @@ ${ko.recap.troubleDetected} (${troubleCommits.length}\uAC74)`));
|
|
|
1982
2053
|
`*Generated by \`vhk recap\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
1983
2054
|
].join("\n");
|
|
1984
2055
|
fs5.writeFileSync(tsFilePath, tsContent, "utf-8");
|
|
1985
|
-
console.log(
|
|
2056
|
+
console.log(chalk6.green(` \u2705 \uD2B8\uB7EC\uBE14\uC288\uD305 \uBB38\uC11C \uC0DD\uC131: ${path6.relative(process.cwd(), tsFilePath)}`));
|
|
1986
2057
|
}
|
|
1987
2058
|
}
|
|
1988
|
-
console.log(
|
|
2059
|
+
console.log(chalk6.green.bold(`
|
|
1989
2060
|
${ko.recap.done}`));
|
|
1990
|
-
console.log(
|
|
2061
|
+
console.log(chalk6.dim(` \u{1F4C4} ${path6.relative(process.cwd(), filePath)}`));
|
|
1991
2062
|
const claudeMdPath = path6.join(process.cwd(), "CLAUDE.md");
|
|
1992
2063
|
if (fs5.existsSync(claudeMdPath)) {
|
|
1993
2064
|
const { updateClaude } = await inquirer3.prompt([{
|
|
@@ -2007,7 +2078,7 @@ ${ko.recap.done}`));
|
|
|
2007
2078
|
`- **\uB2E4\uC74C \uC561\uC158:** ${answers.nextTodo}`
|
|
2008
2079
|
);
|
|
2009
2080
|
fs5.writeFileSync(claudeMdPath, claudeContent, "utf-8");
|
|
2010
|
-
console.log(
|
|
2081
|
+
console.log(chalk6.green(" \u2705 CLAUDE.md \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC"));
|
|
2011
2082
|
}
|
|
2012
2083
|
}
|
|
2013
2084
|
const gitSaveCmd = process.platform === "win32" ? 'git add .; git commit -m "recap: \uC138\uC158 \uAE30\uB85D"' : 'git add . && git commit -m "recap: \uC138\uC158 \uAE30\uB85D"';
|
|
@@ -2019,7 +2090,7 @@ ${ko.recap.done}`));
|
|
|
2019
2090
|
}
|
|
2020
2091
|
|
|
2021
2092
|
// src/commands/sync.ts
|
|
2022
|
-
import
|
|
2093
|
+
import chalk7 from "chalk";
|
|
2023
2094
|
import fs6 from "fs";
|
|
2024
2095
|
import path7 from "path";
|
|
2025
2096
|
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
@@ -2089,32 +2160,32 @@ function toClaudeMd(sections, existing) {
|
|
|
2089
2160
|
return lines.join("\n");
|
|
2090
2161
|
}
|
|
2091
2162
|
async function sync() {
|
|
2092
|
-
console.log(
|
|
2163
|
+
console.log(chalk7.bold(`
|
|
2093
2164
|
${ko.sync.title}
|
|
2094
2165
|
`));
|
|
2095
2166
|
const cwd = process.cwd();
|
|
2096
2167
|
const rulesPath = path7.join(cwd, "RULES.md");
|
|
2097
2168
|
if (!fs6.existsSync(rulesPath)) {
|
|
2098
|
-
console.log(
|
|
2099
|
-
console.log(
|
|
2100
|
-
console.log(
|
|
2169
|
+
console.log(chalk7.yellow(ko.sync.noRules));
|
|
2170
|
+
console.log(chalk7.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
2171
|
+
console.log(chalk7.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
2101
2172
|
console.log("");
|
|
2102
|
-
console.log(
|
|
2103
|
-
console.log(
|
|
2104
|
-
console.log(
|
|
2105
|
-
console.log(
|
|
2106
|
-
console.log(
|
|
2107
|
-
console.log(
|
|
2173
|
+
console.log(chalk7.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
2174
|
+
console.log(chalk7.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
2175
|
+
console.log(chalk7.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
2176
|
+
console.log(chalk7.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
2177
|
+
console.log(chalk7.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
2178
|
+
console.log(chalk7.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
2108
2179
|
return;
|
|
2109
2180
|
}
|
|
2110
2181
|
const rulesContent = fs6.readFileSync(rulesPath, "utf-8");
|
|
2111
2182
|
const sections = parseRulesMd(rulesContent);
|
|
2112
|
-
console.log(
|
|
2183
|
+
console.log(chalk7.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
2113
2184
|
const firstLine = rulesContent.split("\n")[0];
|
|
2114
2185
|
const projectName = firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
2115
2186
|
const cursorrulesPath = path7.join(cwd, ".cursorrules");
|
|
2116
2187
|
fs6.writeFileSync(cursorrulesPath, toCursorrules(sections, projectName), "utf-8");
|
|
2117
|
-
console.log(
|
|
2188
|
+
console.log(chalk7.green(` ${ko.sync.cursorrulesDone}`));
|
|
2118
2189
|
const claudePath = path7.join(cwd, "CLAUDE.md");
|
|
2119
2190
|
const existingClaude = fs6.existsSync(claudePath) ? fs6.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
2120
2191
|
|
|
@@ -2124,11 +2195,11 @@ ${ko.sync.title}
|
|
|
2124
2195
|
- **\uB2E4\uC74C \uC561\uC158:** __FILL__
|
|
2125
2196
|
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`;
|
|
2126
2197
|
fs6.writeFileSync(claudePath, toClaudeMd(sections, existingClaude), "utf-8");
|
|
2127
|
-
console.log(
|
|
2128
|
-
console.log(
|
|
2198
|
+
console.log(chalk7.green(` ${ko.sync.claudeDone}`));
|
|
2199
|
+
console.log(chalk7.bold.green(`
|
|
2129
2200
|
${ko.sync.done}`));
|
|
2130
|
-
console.log(
|
|
2131
|
-
console.log(
|
|
2201
|
+
console.log(chalk7.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
2202
|
+
console.log(chalk7.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
2132
2203
|
printNextStep({
|
|
2133
2204
|
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
2134
2205
|
command: "vhk \uC810\uAC80",
|
|
@@ -2137,7 +2208,7 @@ ${ko.sync.done}`));
|
|
|
2137
2208
|
}
|
|
2138
2209
|
|
|
2139
2210
|
// src/commands/check.ts
|
|
2140
|
-
import
|
|
2211
|
+
import chalk8 from "chalk";
|
|
2141
2212
|
import path9 from "path";
|
|
2142
2213
|
import fs8 from "fs";
|
|
2143
2214
|
|
|
@@ -2196,15 +2267,6 @@ function parseRules(rulesPath) {
|
|
|
2196
2267
|
));
|
|
2197
2268
|
}
|
|
2198
2269
|
}
|
|
2199
|
-
if (/반드시|필수|항상|must|always|required/i.test(ruleText)) {
|
|
2200
|
-
rules.push({
|
|
2201
|
-
id: `required-${ruleIndex}`,
|
|
2202
|
-
section: currentSection,
|
|
2203
|
-
type: "custom",
|
|
2204
|
-
description: ruleText,
|
|
2205
|
-
check: () => []
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
2270
|
}
|
|
2209
2271
|
return rules;
|
|
2210
2272
|
}
|
|
@@ -2305,22 +2367,22 @@ function escapeRegex(str) {
|
|
|
2305
2367
|
|
|
2306
2368
|
// src/commands/check.ts
|
|
2307
2369
|
async function check() {
|
|
2308
|
-
console.log(
|
|
2370
|
+
console.log(chalk8.bold(`
|
|
2309
2371
|
${ko.check.title}
|
|
2310
2372
|
`));
|
|
2311
2373
|
const cwd = process.cwd();
|
|
2312
2374
|
const rulesPath = path9.join(cwd, "RULES.md");
|
|
2313
2375
|
if (!fs8.existsSync(rulesPath)) {
|
|
2314
|
-
console.log(
|
|
2315
|
-
console.log(
|
|
2376
|
+
console.log(chalk8.yellow(ko.check.noRules));
|
|
2377
|
+
console.log(chalk8.dim(" vhk init\uC73C\uB85C \uC2DC\uC791\uD558\uAC70\uB098 RULES.md\uB97C \uB9CC\uB4E4\uC5B4 \uBCF4\uC138\uC694."));
|
|
2316
2378
|
return;
|
|
2317
2379
|
}
|
|
2318
2380
|
const rules = parseRules(rulesPath);
|
|
2319
|
-
console.log(
|
|
2381
|
+
console.log(chalk8.dim(` \u{1F4CF} ${rules.length}\uAC1C \uAC80\uC99D \uAC00\uB2A5\uD55C \uADDC\uCE59 \uAC10\uC9C0
|
|
2320
2382
|
`));
|
|
2321
2383
|
if (rules.length === 0) {
|
|
2322
|
-
console.log(
|
|
2323
|
-
console.log(
|
|
2384
|
+
console.log(chalk8.yellow(ko.check.noAutoRules));
|
|
2385
|
+
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."));
|
|
2324
2386
|
return;
|
|
2325
2387
|
}
|
|
2326
2388
|
const allViolations = [];
|
|
@@ -2328,13 +2390,13 @@ ${ko.check.title}
|
|
|
2328
2390
|
for (const rule of rules) {
|
|
2329
2391
|
const violations = rule.check(cwd);
|
|
2330
2392
|
if (violations.length === 0) {
|
|
2331
|
-
console.log(
|
|
2393
|
+
console.log(chalk8.green(` \u2705 ${rule.id}`) + chalk8.dim(` \u2014 ${rule.description.slice(0, 60)}`));
|
|
2332
2394
|
passCount++;
|
|
2333
2395
|
} else {
|
|
2334
|
-
console.log(
|
|
2396
|
+
console.log(chalk8.red(` \u274C ${rule.id}`) + chalk8.dim(` \u2014 ${violations.length}\uAC74 \uC704\uBC18`));
|
|
2335
2397
|
violations.forEach((v) => {
|
|
2336
|
-
const loc = v.file ?
|
|
2337
|
-
const icon = v.severity === "error" ?
|
|
2398
|
+
const loc = v.file ? chalk8.dim(` (${v.file}${v.line ? ":" + v.line : ""})`) : "";
|
|
2399
|
+
const icon = v.severity === "error" ? chalk8.red("\u2716") : v.severity === "warning" ? chalk8.yellow("\u26A0") : chalk8.blue("\u2139");
|
|
2338
2400
|
console.log(` ${icon} ${v.message}${loc}`);
|
|
2339
2401
|
});
|
|
2340
2402
|
allViolations.push(...violations);
|
|
@@ -2344,17 +2406,17 @@ ${ko.check.title}
|
|
|
2344
2406
|
const errors = allViolations.filter((v) => v.severity === "error").length;
|
|
2345
2407
|
const warnings = allViolations.filter((v) => v.severity === "warning").length;
|
|
2346
2408
|
if (allViolations.length === 0) {
|
|
2347
|
-
console.log(
|
|
2409
|
+
console.log(chalk8.green.bold(`${ko.check.allPassed} (${passCount}/${rules.length})`));
|
|
2348
2410
|
printNextStep({
|
|
2349
2411
|
message: "\uBAA8\uB4E0 \uADDC\uCE59 \uD1B5\uACFC! \uBCF4\uC548 \uC2A4\uCE94\uB3C4 \uD574\uBCFC\uAE4C\uC694?",
|
|
2350
2412
|
command: "vhk \uBCF4\uC548 scan",
|
|
2351
2413
|
cursorHint: "\uBCF4\uC548 \uC2A4\uCE94 \uB3CC\uB824\uC918"
|
|
2352
2414
|
});
|
|
2353
2415
|
} else {
|
|
2354
|
-
console.log(
|
|
2355
|
-
console.log(` \uADDC\uCE59: ${
|
|
2356
|
-
if (errors > 0) console.log(` ${
|
|
2357
|
-
if (warnings > 0) console.log(` ${
|
|
2416
|
+
console.log(chalk8.bold(ko.check.summary));
|
|
2417
|
+
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`);
|
|
2418
|
+
if (errors > 0) console.log(` ${chalk8.red(`\u2716 ${errors}\uAC1C \uC5D0\uB7EC`)}`);
|
|
2419
|
+
if (warnings > 0) console.log(` ${chalk8.yellow(`\u26A0 ${warnings}\uAC1C \uACBD\uACE0`)}`);
|
|
2358
2420
|
printNextStep({
|
|
2359
2421
|
message: "\uC704\uBC18 \uD56D\uBAA9\uC744 \uC218\uC815\uD55C \uD6C4 \uB2E4\uC2DC \uC810\uAC80\uD558\uC138\uC694.",
|
|
2360
2422
|
command: "vhk \uC810\uAC80",
|
|
@@ -2367,10 +2429,13 @@ ${ko.check.title}
|
|
|
2367
2429
|
}
|
|
2368
2430
|
|
|
2369
2431
|
// src/commands/secure.ts
|
|
2370
|
-
import
|
|
2371
|
-
import
|
|
2432
|
+
import chalk9 from "chalk";
|
|
2433
|
+
import fs11 from "fs";
|
|
2372
2434
|
import path11 from "path";
|
|
2373
2435
|
|
|
2436
|
+
// src/lib/scan-secrets.ts
|
|
2437
|
+
import fs10 from "fs";
|
|
2438
|
+
|
|
2374
2439
|
// src/lib/secret-patterns.ts
|
|
2375
2440
|
var SECRET_PATTERNS = [
|
|
2376
2441
|
{
|
|
@@ -2395,7 +2460,7 @@ var SECRET_PATTERNS = [
|
|
|
2395
2460
|
id: "notion-token",
|
|
2396
2461
|
name: "Notion Integration Token",
|
|
2397
2462
|
severity: "critical",
|
|
2398
|
-
pattern: /secret_[A-Za-z0-9]{
|
|
2463
|
+
pattern: /secret_[A-Za-z0-9]{40,50}/
|
|
2399
2464
|
},
|
|
2400
2465
|
{
|
|
2401
2466
|
id: "github-token",
|
|
@@ -2407,7 +2472,7 @@ var SECRET_PATTERNS = [
|
|
|
2407
2472
|
id: "openai-key",
|
|
2408
2473
|
name: "OpenAI API Key",
|
|
2409
2474
|
severity: "critical",
|
|
2410
|
-
pattern:
|
|
2475
|
+
pattern: /\bsk-(?:proj-|ant-api03-|live-)[A-Za-z0-9_-]{16,}\b/
|
|
2411
2476
|
},
|
|
2412
2477
|
{
|
|
2413
2478
|
id: "generic-api-key",
|
|
@@ -2509,69 +2574,88 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
|
2509
2574
|
walk(rootDir);
|
|
2510
2575
|
}
|
|
2511
2576
|
|
|
2512
|
-
// src/
|
|
2513
|
-
var
|
|
2577
|
+
// src/lib/scan-secrets.ts
|
|
2578
|
+
var MAX_SECRET_FINDINGS = 200;
|
|
2514
2579
|
var MAX_LINE_CHARS = 4e3;
|
|
2580
|
+
function globalPattern(pattern) {
|
|
2581
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
2582
|
+
return new RegExp(pattern.source, flags);
|
|
2583
|
+
}
|
|
2584
|
+
function findSecretsInLine(line, relPath, lineNum) {
|
|
2585
|
+
const found = [];
|
|
2586
|
+
const trimmed = line.trim();
|
|
2587
|
+
if (trimmed.startsWith("//") && trimmed.includes("example")) return found;
|
|
2588
|
+
if (trimmed.startsWith("#") && trimmed.includes("example")) return found;
|
|
2589
|
+
if (line.length > MAX_LINE_CHARS) return found;
|
|
2590
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
2591
|
+
const regex = globalPattern(pattern.pattern);
|
|
2592
|
+
for (const match of line.matchAll(regex)) {
|
|
2593
|
+
found.push({
|
|
2594
|
+
patternId: pattern.id,
|
|
2595
|
+
patternName: pattern.name,
|
|
2596
|
+
severity: pattern.severity,
|
|
2597
|
+
file: relPath,
|
|
2598
|
+
line: lineNum,
|
|
2599
|
+
match: maskSecret(match[0])
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
return found;
|
|
2604
|
+
}
|
|
2605
|
+
function scanProjectForSecrets(cwd) {
|
|
2606
|
+
const findings = [];
|
|
2607
|
+
let scannedFiles = 0;
|
|
2608
|
+
let truncated = false;
|
|
2609
|
+
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
2610
|
+
scannedFiles++;
|
|
2611
|
+
const content = fs10.readFileSync(filePath, "utf-8");
|
|
2612
|
+
const lines = content.split("\n");
|
|
2613
|
+
lines.forEach((line, idx) => {
|
|
2614
|
+
if (truncated) return;
|
|
2615
|
+
const lineFindings = findSecretsInLine(line, relPath, idx + 1);
|
|
2616
|
+
for (const f of lineFindings) {
|
|
2617
|
+
findings.push(f);
|
|
2618
|
+
if (findings.length >= MAX_SECRET_FINDINGS) {
|
|
2619
|
+
truncated = true;
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
});
|
|
2625
|
+
return { findings, scannedFiles, truncated };
|
|
2626
|
+
}
|
|
2627
|
+
function filterSevereFindings(findings) {
|
|
2628
|
+
return findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
// src/commands/secure.ts
|
|
2515
2632
|
async function secure() {
|
|
2516
|
-
console.log(
|
|
2633
|
+
console.log(chalk9.bold(`
|
|
2517
2634
|
${ko.secure.title}
|
|
2518
2635
|
`));
|
|
2519
2636
|
const cwd = process.cwd();
|
|
2520
|
-
const findings = [];
|
|
2521
|
-
let scannedFiles = 0;
|
|
2522
|
-
let truncated = false;
|
|
2523
2637
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
2524
|
-
const hasGitignore =
|
|
2638
|
+
const hasGitignore = fs11.existsSync(gitignorePath);
|
|
2525
2639
|
if (!hasGitignore) {
|
|
2526
|
-
console.log(
|
|
2527
|
-
console.log(
|
|
2640
|
+
console.log(chalk9.yellow(` ${ko.secure.noGitignore}`));
|
|
2641
|
+
console.log(chalk9.dim(" .env \uD30C\uC77C\uC774 \uCEE4\uBC0B\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.\n"));
|
|
2528
2642
|
} else {
|
|
2529
|
-
const gitignoreContent =
|
|
2643
|
+
const gitignoreContent = fs11.readFileSync(gitignorePath, "utf-8");
|
|
2530
2644
|
if (!gitignoreContent.includes(".env")) {
|
|
2531
|
-
console.log(
|
|
2532
|
-
console.log(
|
|
2645
|
+
console.log(chalk9.yellow(` ${ko.secure.noEnvInGitignore}`));
|
|
2646
|
+
console.log(chalk9.dim(" \uCD94\uAC00\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.\n"));
|
|
2533
2647
|
}
|
|
2534
2648
|
}
|
|
2535
|
-
console.log(
|
|
2649
|
+
console.log(chalk9.dim(` ${ko.secure.scanning}
|
|
2536
2650
|
`));
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
const content = fs10.readFileSync(filePath, "utf-8");
|
|
2540
|
-
const lines = content.split("\n");
|
|
2541
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
2542
|
-
if (truncated) break;
|
|
2543
|
-
lines.forEach((line, idx) => {
|
|
2544
|
-
if (truncated) return;
|
|
2545
|
-
if (line.length > MAX_LINE_CHARS) return;
|
|
2546
|
-
const trimmed = line.trim();
|
|
2547
|
-
if (trimmed.startsWith("//") && trimmed.includes("example")) return;
|
|
2548
|
-
if (trimmed.startsWith("#") && trimmed.includes("example")) return;
|
|
2549
|
-
const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags);
|
|
2550
|
-
let match;
|
|
2551
|
-
while ((match = regex.exec(line)) !== null) {
|
|
2552
|
-
findings.push({
|
|
2553
|
-
patternId: pattern.id,
|
|
2554
|
-
patternName: pattern.name,
|
|
2555
|
-
severity: pattern.severity,
|
|
2556
|
-
file: relPath,
|
|
2557
|
-
line: idx + 1,
|
|
2558
|
-
match: maskSecret(match[0])
|
|
2559
|
-
});
|
|
2560
|
-
if (findings.length >= MAX_FINDINGS) {
|
|
2561
|
-
truncated = true;
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
});
|
|
2566
|
-
}
|
|
2567
|
-
});
|
|
2568
|
-
console.log(chalk8.dim(` \u{1F4C2} ${scannedFiles}\uAC1C \uD30C\uC77C \uC2A4\uCE94 \uC644\uB8CC (lock\xB7node_modules\xB7>${MAX_SCAN_FILE_BYTES / 1024}KB \uC81C\uC678)`));
|
|
2651
|
+
const { findings, scannedFiles, truncated } = scanProjectForSecrets(cwd);
|
|
2652
|
+
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)`));
|
|
2569
2653
|
if (truncated) {
|
|
2570
|
-
console.log(
|
|
2654
|
+
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.`));
|
|
2571
2655
|
}
|
|
2572
2656
|
console.log("");
|
|
2573
2657
|
if (findings.length === 0) {
|
|
2574
|
-
console.log(
|
|
2658
|
+
console.log(chalk9.green.bold(` ${ko.secure.clean}`));
|
|
2575
2659
|
printNextStep({
|
|
2576
2660
|
message: "\uBCF4\uC548 \uC774\uC0C1 \uC5C6\uC74C! \uAE68\uB057\uD569\uB2C8\uB2E4.",
|
|
2577
2661
|
command: "vhk \uC815\uB9AC",
|
|
@@ -2583,45 +2667,45 @@ ${ko.secure.title}
|
|
|
2583
2667
|
const high = findings.filter((f) => f.severity === "high");
|
|
2584
2668
|
const medium = findings.filter((f) => f.severity === "medium");
|
|
2585
2669
|
if (critical.length > 0) {
|
|
2586
|
-
console.log(
|
|
2670
|
+
console.log(chalk9.red.bold(` \u{1F6A8} CRITICAL \u2014 ${critical.length}\uAC74`));
|
|
2587
2671
|
critical.forEach((f) => {
|
|
2588
|
-
console.log(
|
|
2589
|
-
console.log(
|
|
2672
|
+
console.log(chalk9.red(` \u2716 ${f.patternName}`));
|
|
2673
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2590
2674
|
});
|
|
2591
2675
|
console.log("");
|
|
2592
2676
|
}
|
|
2593
2677
|
if (high.length > 0) {
|
|
2594
|
-
console.log(
|
|
2678
|
+
console.log(chalk9.yellow.bold(` \u26A0\uFE0F HIGH \u2014 ${high.length}\uAC74`));
|
|
2595
2679
|
high.forEach((f) => {
|
|
2596
|
-
console.log(
|
|
2597
|
-
console.log(
|
|
2680
|
+
console.log(chalk9.yellow(` \u26A0 ${f.patternName}`));
|
|
2681
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2598
2682
|
});
|
|
2599
2683
|
console.log("");
|
|
2600
2684
|
}
|
|
2601
2685
|
if (medium.length > 0) {
|
|
2602
|
-
console.log(
|
|
2686
|
+
console.log(chalk9.blue.bold(` \u2139 MEDIUM \u2014 ${medium.length}\uAC74`));
|
|
2603
2687
|
medium.forEach((f) => {
|
|
2604
|
-
console.log(
|
|
2605
|
-
console.log(
|
|
2688
|
+
console.log(chalk9.blue(` \u2139 ${f.patternName}`));
|
|
2689
|
+
console.log(chalk9.dim(` ${f.file}:${f.line} \u2192 ${f.match}`));
|
|
2606
2690
|
});
|
|
2607
2691
|
console.log("");
|
|
2608
2692
|
}
|
|
2609
|
-
console.log(
|
|
2610
|
-
console.log(` \uCD1D ${
|
|
2693
|
+
console.log(chalk9.bold(` ${ko.secure.summary}`));
|
|
2694
|
+
console.log(` \uCD1D ${chalk9.red(String(findings.length))}\uAC74 \uAC10\uC9C0 | CRITICAL: ${critical.length} | HIGH: ${high.length} | MEDIUM: ${medium.length}`);
|
|
2611
2695
|
console.log("");
|
|
2612
|
-
console.log(
|
|
2613
|
-
console.log(
|
|
2614
|
-
console.log(
|
|
2615
|
-
console.log(
|
|
2616
|
-
if (critical.length > 0) {
|
|
2696
|
+
console.log(chalk9.dim(" \u{1F4A1} \uC870\uCE58 \uBC29\uBC95:"));
|
|
2697
|
+
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"));
|
|
2698
|
+
console.log(chalk9.dim(" 2. git history\uC5D0\uC11C\uB3C4 \uC81C\uAC70: git filter-branch \uB610\uB294 BFG Repo-Cleaner"));
|
|
2699
|
+
console.log(chalk9.dim(" 3. \uC720\uCD9C\uB41C \uD0A4\uB294 \uC989\uC2DC \uD3D0\uAE30\uD558\uACE0 \uC7AC\uBC1C\uAE09\n"));
|
|
2700
|
+
if (critical.length > 0 || high.length > 0) {
|
|
2617
2701
|
process.exitCode = 1;
|
|
2618
2702
|
}
|
|
2619
2703
|
}
|
|
2620
2704
|
|
|
2621
2705
|
// src/commands/doctor.ts
|
|
2622
|
-
import
|
|
2706
|
+
import chalk10 from "chalk";
|
|
2623
2707
|
import { execSync } from "child_process";
|
|
2624
|
-
import
|
|
2708
|
+
import fs12 from "fs";
|
|
2625
2709
|
import path12 from "path";
|
|
2626
2710
|
import { fileURLToPath } from "url";
|
|
2627
2711
|
function checkCommand(name, command, hint) {
|
|
@@ -2640,8 +2724,8 @@ function getVhkVersion() {
|
|
|
2640
2724
|
];
|
|
2641
2725
|
for (const pkgPath of candidates) {
|
|
2642
2726
|
try {
|
|
2643
|
-
if (
|
|
2644
|
-
const pkg = JSON.parse(
|
|
2727
|
+
if (fs12.existsSync(pkgPath)) {
|
|
2728
|
+
const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
|
|
2645
2729
|
return pkg.version;
|
|
2646
2730
|
}
|
|
2647
2731
|
} catch {
|
|
@@ -2651,7 +2735,7 @@ function getVhkVersion() {
|
|
|
2651
2735
|
return void 0;
|
|
2652
2736
|
}
|
|
2653
2737
|
async function doctor() {
|
|
2654
|
-
console.log(
|
|
2738
|
+
console.log(chalk10.bold(`
|
|
2655
2739
|
${ko.doctor.title}
|
|
2656
2740
|
`));
|
|
2657
2741
|
const checks = [
|
|
@@ -2663,22 +2747,22 @@ ${ko.doctor.title}
|
|
|
2663
2747
|
let allOk = true;
|
|
2664
2748
|
for (const check2 of checks) {
|
|
2665
2749
|
if (check2.ok) {
|
|
2666
|
-
console.log(
|
|
2750
|
+
console.log(chalk10.green(` \u2705 ${check2.name}`) + chalk10.dim(` \u2014 ${check2.version}`));
|
|
2667
2751
|
} else {
|
|
2668
|
-
console.log(
|
|
2669
|
-
console.log(
|
|
2752
|
+
console.log(chalk10.red(` \u274C ${check2.name} \uC5C6\uC74C`));
|
|
2753
|
+
console.log(chalk10.dim(` \u2192 ${check2.hint}`));
|
|
2670
2754
|
allOk = false;
|
|
2671
2755
|
}
|
|
2672
2756
|
}
|
|
2673
2757
|
console.log("");
|
|
2674
2758
|
const vhkVersion = getVhkVersion();
|
|
2675
2759
|
if (vhkVersion) {
|
|
2676
|
-
console.log(
|
|
2760
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(` \u2014 v${vhkVersion}`));
|
|
2677
2761
|
} else {
|
|
2678
|
-
console.log(
|
|
2762
|
+
console.log(chalk10.green(" \u2705 VHK") + chalk10.dim(" \u2014 \uC124\uCE58\uB428"));
|
|
2679
2763
|
}
|
|
2680
2764
|
console.log("");
|
|
2681
|
-
console.log(
|
|
2765
|
+
console.log(chalk10.bold(` ${ko.doctor.projectFiles}`));
|
|
2682
2766
|
const cwd = process.cwd();
|
|
2683
2767
|
const projectFiles = [
|
|
2684
2768
|
{ name: "RULES.md", hint: "vhk init\uC73C\uB85C \uC0DD\uC131 \uAC00\uB2A5" },
|
|
@@ -2688,32 +2772,32 @@ ${ko.doctor.title}
|
|
|
2688
2772
|
{ name: ".env", hint: ".gitignore\uC5D0 \uD3EC\uD568\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778" }
|
|
2689
2773
|
];
|
|
2690
2774
|
for (const file of projectFiles) {
|
|
2691
|
-
const exists =
|
|
2775
|
+
const exists = fs12.existsSync(path12.join(cwd, file.name));
|
|
2692
2776
|
if (exists) {
|
|
2693
|
-
console.log(
|
|
2777
|
+
console.log(chalk10.green(` \u2705 ${file.name}`));
|
|
2694
2778
|
if (file.name === ".env") {
|
|
2695
2779
|
const gitignorePath = path12.join(cwd, ".gitignore");
|
|
2696
|
-
if (
|
|
2697
|
-
const gitignore =
|
|
2780
|
+
if (fs12.existsSync(gitignorePath)) {
|
|
2781
|
+
const gitignore = fs12.readFileSync(gitignorePath, "utf-8");
|
|
2698
2782
|
if (!gitignore.includes(".env")) {
|
|
2699
|
-
console.log(
|
|
2783
|
+
console.log(chalk10.yellow(` ${ko.doctor.envNotIgnored}`));
|
|
2700
2784
|
}
|
|
2701
2785
|
}
|
|
2702
2786
|
}
|
|
2703
2787
|
} else {
|
|
2704
|
-
console.log(
|
|
2788
|
+
console.log(chalk10.dim(` \u2B1A ${file.name}`) + chalk10.dim(` \u2014 ${file.hint}`));
|
|
2705
2789
|
}
|
|
2706
2790
|
}
|
|
2707
2791
|
console.log("");
|
|
2708
2792
|
if (allOk) {
|
|
2709
|
-
console.log(
|
|
2793
|
+
console.log(chalk10.green.bold(` ${ko.doctor.allOk}`));
|
|
2710
2794
|
printNextStep({
|
|
2711
2795
|
message: ko.doctor.nextOkMessage,
|
|
2712
2796
|
command: "vhk \uC2DC\uC791",
|
|
2713
2797
|
cursorHint: "\uD504\uB85C\uC81D\uD2B8 \uB9CC\uB4E4\uC5B4\uC918"
|
|
2714
2798
|
});
|
|
2715
2799
|
} else {
|
|
2716
|
-
console.log(
|
|
2800
|
+
console.log(chalk10.yellow.bold(` ${ko.doctor.missing} ${ko.doctor.missingHint}`));
|
|
2717
2801
|
printNextStep({
|
|
2718
2802
|
message: ko.doctor.nextRetryMessage,
|
|
2719
2803
|
command: "vhk doctor",
|
|
@@ -2724,9 +2808,9 @@ ${ko.doctor.title}
|
|
|
2724
2808
|
}
|
|
2725
2809
|
|
|
2726
2810
|
// src/commands/ship.ts
|
|
2727
|
-
import
|
|
2811
|
+
import chalk11 from "chalk";
|
|
2728
2812
|
import inquirer4 from "inquirer";
|
|
2729
|
-
import
|
|
2813
|
+
import fs13 from "fs";
|
|
2730
2814
|
import path13 from "path";
|
|
2731
2815
|
var CHECKLIST = [
|
|
2732
2816
|
{ id: "build", questionKey: "checkBuild", hintKey: "hintBuild" },
|
|
@@ -2740,29 +2824,29 @@ function sanitizeVersion(version) {
|
|
|
2740
2824
|
return version.trim().replace(/^v/i, "").replace(/[^a-zA-Z0-9._-]/g, "-") || "0.0.0";
|
|
2741
2825
|
}
|
|
2742
2826
|
async function ship() {
|
|
2743
|
-
console.log(
|
|
2827
|
+
console.log(chalk11.bold(`
|
|
2744
2828
|
${ko.ship.title}
|
|
2745
2829
|
`));
|
|
2746
2830
|
const cwd = process.cwd();
|
|
2747
|
-
console.log(
|
|
2831
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.checklist}
|
|
2748
2832
|
`));
|
|
2749
2833
|
const { passed } = await inquirer4.prompt([{
|
|
2750
2834
|
type: "checkbox",
|
|
2751
2835
|
name: "passed",
|
|
2752
2836
|
message: ko.ship.checkboxPrompt,
|
|
2753
2837
|
choices: CHECKLIST.map((c) => ({
|
|
2754
|
-
name: `${ko.ship[c.questionKey]} ${
|
|
2838
|
+
name: `${ko.ship[c.questionKey]} ${chalk11.dim(`(${ko.ship[c.hintKey]})`)}`,
|
|
2755
2839
|
value: c.id
|
|
2756
2840
|
}))
|
|
2757
2841
|
}]);
|
|
2758
2842
|
const allPassed = passed.length === CHECKLIST.length;
|
|
2759
2843
|
const skipped = CHECKLIST.filter((c) => !passed.includes(c.id));
|
|
2760
2844
|
if (!allPassed) {
|
|
2761
|
-
console.log(
|
|
2845
|
+
console.log(chalk11.yellow(`
|
|
2762
2846
|
${ko.ship.incompleteHeader}`));
|
|
2763
2847
|
skipped.forEach((s) => {
|
|
2764
|
-
console.log(
|
|
2765
|
-
console.log(
|
|
2848
|
+
console.log(chalk11.yellow(` \u2022 ${ko.ship[s.questionKey]}`));
|
|
2849
|
+
console.log(chalk11.dim(` \u2192 ${ko.ship[s.hintKey]}`));
|
|
2766
2850
|
});
|
|
2767
2851
|
const { proceed } = await inquirer4.prompt([{
|
|
2768
2852
|
type: "confirm",
|
|
@@ -2779,13 +2863,13 @@ ${ko.ship.title}
|
|
|
2779
2863
|
return;
|
|
2780
2864
|
}
|
|
2781
2865
|
} else {
|
|
2782
|
-
console.log(
|
|
2866
|
+
console.log(chalk11.green(`
|
|
2783
2867
|
${ko.ship.allPassed}
|
|
2784
2868
|
`));
|
|
2785
2869
|
}
|
|
2786
|
-
console.log(
|
|
2870
|
+
console.log(chalk11.cyan.bold(` ${ko.ship.retro}
|
|
2787
2871
|
`));
|
|
2788
|
-
console.log(
|
|
2872
|
+
console.log(chalk11.dim(` ${ko.ship.versionHint}`));
|
|
2789
2873
|
const retro = await inquirer4.prompt([
|
|
2790
2874
|
{ type: "input", name: "version", message: ko.ship.versionPrompt },
|
|
2791
2875
|
{ type: "input", name: "whatWentWell", message: ko.ship.questionWell },
|
|
@@ -2794,7 +2878,7 @@ ${ko.ship.title}
|
|
|
2794
2878
|
{ type: "input", name: "nextVersion", message: ko.ship.questionNext }
|
|
2795
2879
|
]);
|
|
2796
2880
|
const buildLogDir = path13.join(cwd, "docs", "build-log");
|
|
2797
|
-
if (!
|
|
2881
|
+
if (!fs13.existsSync(buildLogDir)) fs13.mkdirSync(buildLogDir, { recursive: true });
|
|
2798
2882
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2799
2883
|
const versionSlug = sanitizeVersion(retro.version);
|
|
2800
2884
|
const fileName = `${today}-v${versionSlug}.md`;
|
|
@@ -2827,8 +2911,8 @@ ${ko.ship.title}
|
|
|
2827
2911
|
"---",
|
|
2828
2912
|
`*Generated by \`vhk ship\` at ${(/* @__PURE__ */ new Date()).toISOString()}*`
|
|
2829
2913
|
].join("\n");
|
|
2830
|
-
|
|
2831
|
-
console.log(
|
|
2914
|
+
fs13.writeFileSync(filePath, content, "utf-8");
|
|
2915
|
+
console.log(chalk11.green(`
|
|
2832
2916
|
${ko.ship.buildLogDone(path13.relative(cwd, filePath))}`));
|
|
2833
2917
|
printNextStep({
|
|
2834
2918
|
message: ko.ship.deployMessage,
|
|
@@ -2839,16 +2923,63 @@ ${ko.ship.title}
|
|
|
2839
2923
|
}
|
|
2840
2924
|
|
|
2841
2925
|
// src/commands/save.ts
|
|
2842
|
-
import { execFileSync
|
|
2843
|
-
import
|
|
2926
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2927
|
+
import chalk12 from "chalk";
|
|
2844
2928
|
import ora from "ora";
|
|
2845
2929
|
import inquirer5 from "inquirer";
|
|
2846
|
-
|
|
2847
|
-
|
|
2930
|
+
|
|
2931
|
+
// src/lib/git-porcelain.ts
|
|
2932
|
+
function normalizePorcelain(raw) {
|
|
2933
|
+
return raw.replace(/\r\n/g, "\n").trimEnd();
|
|
2848
2934
|
}
|
|
2849
|
-
function
|
|
2850
|
-
|
|
2935
|
+
function parsePorcelainLines(raw) {
|
|
2936
|
+
return normalizePorcelain(raw).split("\n").filter(Boolean);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
// src/lib/git-repo.ts
|
|
2940
|
+
import { execFileSync } from "child_process";
|
|
2941
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
2942
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
2943
|
+
encoding: "utf-8",
|
|
2944
|
+
cwd,
|
|
2945
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2946
|
+
}).trim();
|
|
2947
|
+
}
|
|
2948
|
+
function gitOut(args, cwd) {
|
|
2949
|
+
return execFileSync("git", args, {
|
|
2950
|
+
encoding: "utf-8",
|
|
2951
|
+
cwd,
|
|
2952
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
function gitRun(args, cwd) {
|
|
2956
|
+
execFileSync("git", args, { stdio: "pipe", cwd });
|
|
2957
|
+
}
|
|
2958
|
+
function getExecErrorMessage(err) {
|
|
2959
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
2960
|
+
const stderr = err.stderr;
|
|
2961
|
+
if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
|
|
2962
|
+
if (typeof stderr === "string") return stderr.trim();
|
|
2963
|
+
}
|
|
2964
|
+
return err instanceof Error ? err.message : String(err);
|
|
2851
2965
|
}
|
|
2966
|
+
function hasGitRemote(cwd) {
|
|
2967
|
+
try {
|
|
2968
|
+
return gitOut(["remote"], cwd).trim().length > 0;
|
|
2969
|
+
} catch {
|
|
2970
|
+
return false;
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
function countLocalCommits(cwd) {
|
|
2974
|
+
try {
|
|
2975
|
+
const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
|
|
2976
|
+
return parseInt(out, 10) || 0;
|
|
2977
|
+
} catch {
|
|
2978
|
+
return 0;
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/commands/save.ts
|
|
2852
2983
|
function formatDefaultCommitMessage(date = /* @__PURE__ */ new Date()) {
|
|
2853
2984
|
const y = date.getFullYear();
|
|
2854
2985
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
@@ -2864,24 +2995,49 @@ function statusIcon(code) {
|
|
|
2864
2995
|
return "\u{1F4C4}";
|
|
2865
2996
|
}
|
|
2866
2997
|
async function save() {
|
|
2867
|
-
console.log(
|
|
2998
|
+
console.log(chalk12.bold(`
|
|
2868
2999
|
\u{1F4BE} ${t("save.title")}`));
|
|
2869
|
-
console.log(
|
|
3000
|
+
console.log(chalk12.gray("\u2500".repeat(40)));
|
|
3001
|
+
let gitRoot;
|
|
2870
3002
|
try {
|
|
2871
|
-
|
|
3003
|
+
execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3004
|
+
gitRoot = getGitRoot();
|
|
2872
3005
|
} catch {
|
|
2873
|
-
console.log(
|
|
3006
|
+
console.log(chalk12.red(`\u274C ${t("save.notGitRepo")}`));
|
|
2874
3007
|
return;
|
|
2875
3008
|
}
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3009
|
+
console.log(chalk12.cyan(`
|
|
3010
|
+
\u{1F512} ${t("save.securityWarnHeader")}`));
|
|
3011
|
+
printSecurityWarnings(gitRoot);
|
|
3012
|
+
const severe = filterSevereFindings(scanProjectForSecrets(gitRoot).findings);
|
|
3013
|
+
if (severe.length > 0) {
|
|
3014
|
+
console.log(chalk12.red(`
|
|
3015
|
+
\u26A0\uFE0F ${t("save.secretsFound", severe.length)}`));
|
|
3016
|
+
severe.slice(0, 5).forEach((f) => {
|
|
3017
|
+
console.log(chalk12.dim(` ${f.file}:${f.line} \u2014 ${f.patternName}`));
|
|
3018
|
+
});
|
|
3019
|
+
if (severe.length > 5) {
|
|
3020
|
+
console.log(chalk12.dim(` ... \uC678 ${severe.length - 5}\uAC74 (vhk \uBCF4\uC548 scan)`));
|
|
3021
|
+
}
|
|
3022
|
+
const { proceed } = await inquirer5.prompt([{
|
|
3023
|
+
type: "confirm",
|
|
3024
|
+
name: "proceed",
|
|
3025
|
+
message: t("save.secretsConfirm"),
|
|
3026
|
+
default: false
|
|
3027
|
+
}]);
|
|
3028
|
+
if (!proceed) {
|
|
3029
|
+
console.log(chalk12.gray(t("save.cancelled")));
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const lines = parsePorcelainLines(gitOut(["status", "--porcelain"], gitRoot));
|
|
3034
|
+
if (lines.length === 0) {
|
|
3035
|
+
console.log(chalk12.yellow(`\u{1F4ED} ${t("save.noChanges")}`));
|
|
2879
3036
|
return;
|
|
2880
3037
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
files.forEach((line) => {
|
|
3038
|
+
console.log(chalk12.cyan(`
|
|
3039
|
+
\u{1F4CB} ${t("save.filesHeader", lines.length)}`));
|
|
3040
|
+
lines.forEach((line) => {
|
|
2885
3041
|
const code = line.substring(0, 2);
|
|
2886
3042
|
const name = line.substring(3);
|
|
2887
3043
|
console.log(` ${statusIcon(code)} ${name}`);
|
|
@@ -2893,43 +3049,62 @@ async function save() {
|
|
|
2893
3049
|
default: formatDefaultCommitMessage()
|
|
2894
3050
|
}]);
|
|
2895
3051
|
const spinner = ora(t("save.saving")).start();
|
|
3052
|
+
let didAdd = false;
|
|
2896
3053
|
try {
|
|
2897
|
-
gitRun(["add", "."]);
|
|
2898
|
-
|
|
3054
|
+
gitRun(["add", "."], gitRoot);
|
|
3055
|
+
didAdd = true;
|
|
3056
|
+
gitRun(["commit", "-m", message], gitRoot);
|
|
2899
3057
|
spinner.text = t("save.pushing");
|
|
2900
|
-
|
|
2901
|
-
gitRun(["push"]);
|
|
2902
|
-
spinner.succeed(t("save.successWithPush"));
|
|
2903
|
-
} catch {
|
|
3058
|
+
if (!hasGitRemote(gitRoot)) {
|
|
2904
3059
|
spinner.succeed(t("save.successLocal"));
|
|
2905
|
-
console.log(
|
|
3060
|
+
console.log(chalk12.yellow(` \u{1F4A1} ${t("save.noRemote")}`));
|
|
3061
|
+
} else {
|
|
3062
|
+
try {
|
|
3063
|
+
gitRun(["push"], gitRoot);
|
|
3064
|
+
spinner.succeed(t("save.successWithPush"));
|
|
3065
|
+
} catch (pushErr) {
|
|
3066
|
+
spinner.fail(t("save.pushFailed"));
|
|
3067
|
+
console.log(chalk12.red(getExecErrorMessage(pushErr)));
|
|
3068
|
+
console.log(chalk12.yellow(`
|
|
3069
|
+
\u{1F4A1} ${t("save.commitOkPushFailed")}`));
|
|
3070
|
+
process.exitCode = 1;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
if (process.exitCode !== 1) {
|
|
3074
|
+
console.log(chalk12.green(`
|
|
3075
|
+
\u2705 ${t("save.done", lines.length)}`));
|
|
3076
|
+
} else {
|
|
3077
|
+
console.log(chalk12.green(`
|
|
3078
|
+
\u2705 ${t("save.doneLocalOnly", lines.length)}`));
|
|
2906
3079
|
}
|
|
2907
|
-
console.log(chalk11.green(`
|
|
2908
|
-
\u2705 ${t("save.done", files.length)}`));
|
|
2909
3080
|
} catch (err) {
|
|
2910
3081
|
spinner.fail(t("save.failed"));
|
|
2911
|
-
|
|
2912
|
-
|
|
3082
|
+
console.log(chalk12.red(getExecErrorMessage(err)));
|
|
3083
|
+
if (didAdd) {
|
|
3084
|
+
try {
|
|
3085
|
+
const staged = gitOut(["diff", "--cached", "--stat"], gitRoot).trim();
|
|
3086
|
+
if (staged) {
|
|
3087
|
+
console.log(chalk12.yellow(`
|
|
3088
|
+
\u{1F4A1} ${t("save.stagedAfterFail")}`));
|
|
3089
|
+
}
|
|
3090
|
+
} catch {
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
2913
3093
|
process.exitCode = 1;
|
|
2914
3094
|
}
|
|
2915
3095
|
}
|
|
2916
3096
|
|
|
2917
3097
|
// src/commands/undo.ts
|
|
2918
|
-
import { execFileSync as
|
|
2919
|
-
import
|
|
3098
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3099
|
+
import chalk13 from "chalk";
|
|
2920
3100
|
import inquirer6 from "inquirer";
|
|
2921
|
-
function gitOut2(args) {
|
|
2922
|
-
return execFileSync2("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
2923
|
-
}
|
|
2924
|
-
function gitRun2(args) {
|
|
2925
|
-
execFileSync2("git", args, { stdio: "pipe" });
|
|
2926
|
-
}
|
|
2927
3101
|
function parseRecentCommits(logOutput) {
|
|
2928
3102
|
return logOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2929
3103
|
}
|
|
2930
|
-
function countUnpushedCommits() {
|
|
3104
|
+
function countUnpushedCommits(gitRoot) {
|
|
3105
|
+
const cwd = gitRoot ?? process.cwd();
|
|
2931
3106
|
try {
|
|
2932
|
-
const out =
|
|
3107
|
+
const out = gitOut(["rev-list", "--count", "@{u}..HEAD"], cwd).trim();
|
|
2933
3108
|
return parseInt(out, 10) || 0;
|
|
2934
3109
|
} catch {
|
|
2935
3110
|
return -1;
|
|
@@ -2939,29 +3114,36 @@ function willUndoPushedCommits(undoCount, unpushedCount) {
|
|
|
2939
3114
|
if (unpushedCount < 0) return false;
|
|
2940
3115
|
return undoCount > unpushedCount;
|
|
2941
3116
|
}
|
|
3117
|
+
function isUndoRisky(undoCount, unpushedCount, hasRemote) {
|
|
3118
|
+
if (willUndoPushedCommits(undoCount, unpushedCount)) return true;
|
|
3119
|
+
if (unpushedCount < 0 && hasRemote) return true;
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
2942
3122
|
async function undo() {
|
|
2943
|
-
console.log(
|
|
3123
|
+
console.log(chalk13.bold(`
|
|
2944
3124
|
\u23EA ${t("undo.title")}`));
|
|
2945
|
-
console.log(
|
|
3125
|
+
console.log(chalk13.gray("\u2500".repeat(40)));
|
|
3126
|
+
let gitRoot;
|
|
2946
3127
|
try {
|
|
2947
|
-
|
|
3128
|
+
execFileSync3("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3129
|
+
gitRoot = getGitRoot();
|
|
2948
3130
|
} catch {
|
|
2949
|
-
console.log(
|
|
3131
|
+
console.log(chalk13.red(`\u274C ${t("undo.notGitRepo")}`));
|
|
2950
3132
|
return;
|
|
2951
3133
|
}
|
|
2952
3134
|
let logOutput;
|
|
2953
3135
|
try {
|
|
2954
|
-
logOutput =
|
|
3136
|
+
logOutput = gitOut(["log", "--oneline", "-5"], gitRoot).trim();
|
|
2955
3137
|
} catch {
|
|
2956
|
-
console.log(
|
|
3138
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
2957
3139
|
return;
|
|
2958
3140
|
}
|
|
2959
3141
|
const commits = parseRecentCommits(logOutput);
|
|
2960
3142
|
if (commits.length === 0) {
|
|
2961
|
-
console.log(
|
|
3143
|
+
console.log(chalk13.yellow(`\u{1F4ED} ${t("undo.noCommits")}`));
|
|
2962
3144
|
return;
|
|
2963
3145
|
}
|
|
2964
|
-
console.log(
|
|
3146
|
+
console.log(chalk13.cyan(`
|
|
2965
3147
|
${t("undo.recentHeader")}`));
|
|
2966
3148
|
commits.forEach((c, i) => {
|
|
2967
3149
|
console.log(` ${i === 0 ? "\u{1F449}" : " "} ${c}`);
|
|
@@ -2976,40 +3158,57 @@ ${t("undo.recentHeader")}`));
|
|
|
2976
3158
|
max: maxUndo
|
|
2977
3159
|
}]);
|
|
2978
3160
|
const undoCount = Math.min(Math.max(1, count || 1), maxUndo);
|
|
2979
|
-
const
|
|
2980
|
-
if (
|
|
2981
|
-
console.log(
|
|
3161
|
+
const headCount = countLocalCommits(gitRoot);
|
|
3162
|
+
if (undoCount >= headCount) {
|
|
3163
|
+
console.log(chalk13.yellow(`
|
|
3164
|
+
\u{1F4ED} ${t("undo.rootCommit")}`));
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const unpushed = countUnpushedCommits(gitRoot);
|
|
3168
|
+
const remote = hasGitRemote(gitRoot);
|
|
3169
|
+
const risky = isUndoRisky(undoCount, unpushed, remote);
|
|
3170
|
+
if (risky) {
|
|
3171
|
+
if (unpushed < 0) {
|
|
3172
|
+
console.log(chalk13.red(`
|
|
3173
|
+
\u26A0\uFE0F ${t("undo.noUpstreamWarning")}`));
|
|
3174
|
+
} else {
|
|
3175
|
+
console.log(chalk13.red(`
|
|
2982
3176
|
\u26A0\uFE0F ${t("undo.alreadyPushed")}`));
|
|
3177
|
+
}
|
|
2983
3178
|
}
|
|
2984
3179
|
const { confirm } = await inquirer6.prompt([{
|
|
2985
3180
|
type: "confirm",
|
|
2986
3181
|
name: "confirm",
|
|
2987
|
-
message: t("undo.
|
|
3182
|
+
message: risky ? t("undo.confirmRisky", undoCount) : t("undo.confirmMessage"),
|
|
2988
3183
|
default: false
|
|
2989
3184
|
}]);
|
|
2990
3185
|
if (!confirm) {
|
|
2991
|
-
console.log(
|
|
3186
|
+
console.log(chalk13.gray(t("undo.cancelled")));
|
|
2992
3187
|
return;
|
|
2993
3188
|
}
|
|
2994
3189
|
try {
|
|
2995
|
-
|
|
2996
|
-
console.log(
|
|
2997
|
-
\u2705 ${t("undo.success"
|
|
2998
|
-
console.log(
|
|
3190
|
+
gitRun(["reset", "--soft", `HEAD~${undoCount}`], gitRoot);
|
|
3191
|
+
console.log(chalk13.green(`
|
|
3192
|
+
\u2705 ${t("undo.success")}`));
|
|
3193
|
+
console.log(chalk13.gray(` \u{1F4A1} ${t("undo.stagedHint")}`));
|
|
3194
|
+
if (risky) {
|
|
3195
|
+
console.log(chalk13.yellow(`
|
|
3196
|
+
\u{1F4A1} ${t("undo.forcePushHint")}`));
|
|
3197
|
+
}
|
|
2999
3198
|
} catch (err) {
|
|
3000
|
-
console.log(
|
|
3199
|
+
console.log(chalk13.red(`\u274C ${t("undo.failed")}`));
|
|
3001
3200
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3002
|
-
console.log(
|
|
3201
|
+
console.log(chalk13.red(msg));
|
|
3003
3202
|
process.exitCode = 1;
|
|
3004
3203
|
}
|
|
3005
3204
|
}
|
|
3006
3205
|
|
|
3007
3206
|
// src/commands/diff.ts
|
|
3008
|
-
import { execFileSync as
|
|
3009
|
-
import
|
|
3010
|
-
function
|
|
3207
|
+
import { execFileSync as execFileSync4, execSync as execSync2 } from "child_process";
|
|
3208
|
+
import chalk14 from "chalk";
|
|
3209
|
+
function gitOut2(args) {
|
|
3011
3210
|
try {
|
|
3012
|
-
return
|
|
3211
|
+
return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3013
3212
|
} catch {
|
|
3014
3213
|
return "";
|
|
3015
3214
|
}
|
|
@@ -3046,65 +3245,62 @@ function summarizeNumstat(numstat) {
|
|
|
3046
3245
|
return { fileCount, totalAdd, totalDel };
|
|
3047
3246
|
}
|
|
3048
3247
|
function printFile(f) {
|
|
3049
|
-
const adds = f.additions > 0 ?
|
|
3050
|
-
const dels = f.deletions > 0 ?
|
|
3248
|
+
const adds = f.additions > 0 ? chalk14.green(`+${f.additions}`) : "";
|
|
3249
|
+
const dels = f.deletions > 0 ? chalk14.red(`-${f.deletions}`) : "";
|
|
3051
3250
|
const change = [adds, dels].filter(Boolean).join(" ");
|
|
3052
3251
|
console.log(` ${f.name} ${change}`);
|
|
3053
3252
|
}
|
|
3054
3253
|
async function diff() {
|
|
3055
|
-
console.log(
|
|
3254
|
+
console.log(chalk14.bold(`
|
|
3056
3255
|
\u{1F50D} ${t("diff.title")}`));
|
|
3057
|
-
console.log(
|
|
3256
|
+
console.log(chalk14.gray("\u2500".repeat(40)));
|
|
3058
3257
|
try {
|
|
3059
|
-
|
|
3258
|
+
execSync2("git rev-parse --is-inside-work-tree", { stdio: "pipe" });
|
|
3060
3259
|
} catch {
|
|
3061
|
-
console.log(
|
|
3260
|
+
console.log(chalk14.red(`\u274C ${t("diff.notGitRepo")}`));
|
|
3062
3261
|
return;
|
|
3063
3262
|
}
|
|
3064
|
-
const unstaged =
|
|
3065
|
-
const staged =
|
|
3066
|
-
const untracked =
|
|
3263
|
+
const unstaged = gitOut2(["diff", "--stat"]);
|
|
3264
|
+
const staged = gitOut2(["diff", "--cached", "--stat"]);
|
|
3265
|
+
const untracked = gitOut2(["ls-files", "--others", "--exclude-standard"]);
|
|
3067
3266
|
if (!unstaged && !staged && !untracked) {
|
|
3068
|
-
console.log(
|
|
3267
|
+
console.log(chalk14.green(`
|
|
3069
3268
|
\u2705 ${t("diff.noChanges")}`));
|
|
3070
3269
|
return;
|
|
3071
3270
|
}
|
|
3072
3271
|
if (staged) {
|
|
3073
|
-
console.log(
|
|
3272
|
+
console.log(chalk14.cyan(`
|
|
3074
3273
|
${t("diff.stagedHeader")}`));
|
|
3075
3274
|
parseDiffStat(staged).forEach((f) => printFile(f));
|
|
3076
3275
|
}
|
|
3077
3276
|
if (unstaged) {
|
|
3078
|
-
console.log(
|
|
3277
|
+
console.log(chalk14.cyan(`
|
|
3079
3278
|
${t("diff.unstagedHeader")}`));
|
|
3080
3279
|
parseDiffStat(unstaged).forEach((f) => printFile(f));
|
|
3081
3280
|
}
|
|
3082
3281
|
if (untracked) {
|
|
3083
3282
|
const files = untracked.split("\n").filter(Boolean);
|
|
3084
|
-
console.log(
|
|
3283
|
+
console.log(chalk14.cyan(`
|
|
3085
3284
|
${t("diff.untrackedHeader", files.length)}`));
|
|
3086
|
-
files.forEach((f) => console.log(` ${
|
|
3285
|
+
files.forEach((f) => console.log(` ${chalk14.green("+")} ${f}`));
|
|
3087
3286
|
}
|
|
3088
|
-
const numstat =
|
|
3287
|
+
const numstat = gitOut2(["diff", "--numstat", "HEAD"]);
|
|
3089
3288
|
if (numstat) {
|
|
3090
3289
|
const { fileCount, totalAdd, totalDel } = summarizeNumstat(numstat);
|
|
3091
|
-
console.log(
|
|
3290
|
+
console.log(chalk14.cyan(`
|
|
3092
3291
|
${t("diff.summaryHeader")}`));
|
|
3093
3292
|
console.log(` ${t("diff.filesLine", fileCount)}`);
|
|
3094
|
-
console.log(` \uCD94\uAC00: ${
|
|
3095
|
-
console.log(` \uC0AD\uC81C: ${
|
|
3293
|
+
console.log(` \uCD94\uAC00: ${chalk14.green(`+${totalAdd}`)}\uC904`);
|
|
3294
|
+
console.log(` \uC0AD\uC81C: ${chalk14.red(`-${totalDel}`)}\uC904`);
|
|
3096
3295
|
}
|
|
3097
3296
|
console.log("");
|
|
3098
3297
|
}
|
|
3099
3298
|
|
|
3100
3299
|
// src/commands/status.ts
|
|
3101
|
-
import { execFileSync as
|
|
3102
|
-
import
|
|
3300
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
3301
|
+
import fs14 from "fs";
|
|
3103
3302
|
import path14 from "path";
|
|
3104
|
-
import
|
|
3105
|
-
function gitOut4(args) {
|
|
3106
|
-
return execFileSync4("git", args, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
3107
|
-
}
|
|
3303
|
+
import chalk15 from "chalk";
|
|
3108
3304
|
function countFileChanges(porcelain) {
|
|
3109
3305
|
const lines = porcelain.split("\n").filter(Boolean);
|
|
3110
3306
|
let staged = 0;
|
|
@@ -3143,9 +3339,9 @@ function parseRecentCommitLines(logOutput) {
|
|
|
3143
3339
|
}
|
|
3144
3340
|
function readProjectPackage(cwd = process.cwd()) {
|
|
3145
3341
|
const pkgPath = path14.join(cwd, "package.json");
|
|
3146
|
-
if (!
|
|
3342
|
+
if (!fs14.existsSync(pkgPath)) return null;
|
|
3147
3343
|
try {
|
|
3148
|
-
const pkg = JSON.parse(
|
|
3344
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf-8"));
|
|
3149
3345
|
if (!pkg.name && !pkg.version) return null;
|
|
3150
3346
|
return {
|
|
3151
3347
|
name: pkg.name ?? "(no name)",
|
|
@@ -3155,63 +3351,65 @@ function readProjectPackage(cwd = process.cwd()) {
|
|
|
3155
3351
|
return null;
|
|
3156
3352
|
}
|
|
3157
3353
|
}
|
|
3158
|
-
function getSyncCounts() {
|
|
3354
|
+
function getSyncCounts(gitRoot) {
|
|
3159
3355
|
try {
|
|
3160
|
-
const out =
|
|
3356
|
+
const out = gitOut(["rev-list", "--left-right", "--count", "HEAD...@{u}"], gitRoot);
|
|
3161
3357
|
return parseSyncCounts(out);
|
|
3162
3358
|
} catch {
|
|
3163
3359
|
return { ahead: 0, behind: 0, hasUpstream: false };
|
|
3164
3360
|
}
|
|
3165
3361
|
}
|
|
3166
3362
|
async function status() {
|
|
3167
|
-
console.log(
|
|
3363
|
+
console.log(chalk15.bold(`
|
|
3168
3364
|
\u{1F4CA} ${t("status.title")}`));
|
|
3169
|
-
console.log(
|
|
3365
|
+
console.log(chalk15.gray("\u2500".repeat(40)));
|
|
3366
|
+
let gitRoot;
|
|
3170
3367
|
try {
|
|
3171
|
-
|
|
3368
|
+
execFileSync5("git", ["rev-parse", "--is-inside-work-tree"], { stdio: "pipe" });
|
|
3369
|
+
gitRoot = getGitRoot();
|
|
3172
3370
|
} catch {
|
|
3173
|
-
console.log(
|
|
3371
|
+
console.log(chalk15.red(`\u274C ${t("status.notGitRepo")}`));
|
|
3174
3372
|
return;
|
|
3175
3373
|
}
|
|
3176
3374
|
let branch;
|
|
3177
3375
|
try {
|
|
3178
|
-
branch =
|
|
3376
|
+
branch = gitOut(["branch", "--show-current"], gitRoot).trim() || t("status.detached");
|
|
3179
3377
|
} catch {
|
|
3180
3378
|
branch = t("status.unknownBranch");
|
|
3181
3379
|
}
|
|
3182
|
-
const porcelain =
|
|
3380
|
+
const porcelain = normalizePorcelain(gitOut(["status", "--porcelain"], gitRoot));
|
|
3183
3381
|
const counts = countFileChanges(porcelain);
|
|
3184
|
-
const sync2 = getSyncCounts();
|
|
3382
|
+
const sync2 = getSyncCounts(gitRoot);
|
|
3185
3383
|
let commits = [];
|
|
3186
3384
|
try {
|
|
3187
|
-
commits = parseRecentCommitLines(
|
|
3385
|
+
commits = parseRecentCommitLines(gitOut(["log", "--oneline", "-3"], gitRoot).trim());
|
|
3188
3386
|
} catch {
|
|
3189
3387
|
commits = [];
|
|
3190
3388
|
}
|
|
3191
3389
|
const pkg = readProjectPackage();
|
|
3192
|
-
console.log(
|
|
3193
|
-
\u{1F33F} ${t("status.branch")}`) +
|
|
3390
|
+
console.log(chalk15.cyan(`
|
|
3391
|
+
\u{1F33F} ${t("status.branch")}`) + chalk15.white(` ${branch}`));
|
|
3194
3392
|
console.log(
|
|
3195
|
-
|
|
3393
|
+
chalk15.cyan(`\u{1F4C1} ${t("status.changes")}`) + chalk15.white(
|
|
3196
3394
|
` staged ${counts.staged} \xB7 unstaged ${counts.unstaged} \xB7 untracked ${counts.untracked}`
|
|
3197
3395
|
)
|
|
3198
3396
|
);
|
|
3199
|
-
console.log(
|
|
3397
|
+
console.log(chalk15.cyan(`
|
|
3200
3398
|
\u{1F4CB} ${t("status.recentCommits")}`));
|
|
3201
3399
|
if (commits.length === 0) {
|
|
3202
|
-
console.log(
|
|
3400
|
+
console.log(chalk15.dim(` ${t("status.noCommits")}`));
|
|
3203
3401
|
} else {
|
|
3204
|
-
commits.forEach((c) => console.log(` ${
|
|
3402
|
+
commits.forEach((c) => console.log(` ${chalk15.dim("\u2022")} ${c}`));
|
|
3205
3403
|
}
|
|
3206
3404
|
console.log(
|
|
3207
|
-
|
|
3208
|
-
\u{1F504} ${t("status.remote")}`) +
|
|
3405
|
+
chalk15.cyan(`
|
|
3406
|
+
\u{1F504} ${t("status.remote")}`) + chalk15.white(` ${formatSyncLabel(sync2)}`)
|
|
3209
3407
|
);
|
|
3210
|
-
console.log(
|
|
3408
|
+
console.log(chalk15.gray("\n" + "\u2500".repeat(40)));
|
|
3211
3409
|
if (pkg) {
|
|
3212
|
-
console.log(
|
|
3410
|
+
console.log(chalk15.cyan(`\u{1F4E6} ${t("status.package")}`) + chalk15.white(` ${pkg.name} v${pkg.version}`));
|
|
3213
3411
|
} else {
|
|
3214
|
-
console.log(
|
|
3412
|
+
console.log(chalk15.dim(`\u{1F4E6} ${t("status.noPackage")}`));
|
|
3215
3413
|
}
|
|
3216
3414
|
console.log("");
|
|
3217
3415
|
}
|
|
@@ -3233,7 +3431,7 @@ var KO_ALIASES = {
|
|
|
3233
3431
|
status: "\uC0C1\uD0DC",
|
|
3234
3432
|
diff: "\uBCC0\uACBD"
|
|
3235
3433
|
};
|
|
3236
|
-
program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.5.
|
|
3434
|
+
program.name("vhk").description("VHK \u2014 \uBC14\uC774\uBE0C\uCF54\uB529 \uD504\uB85C\uC81D\uD2B8 \uCF54\uCE58 (\uD55C\uAD6D\uC5B4\uB85C \uC548\uB0B4\uD569\uB2C8\uB2E4)").version("0.5.1");
|
|
3237
3435
|
program.configureHelp({
|
|
3238
3436
|
formatHelp(cmd, helper) {
|
|
3239
3437
|
if (cmd.parent) {
|
|
@@ -3259,7 +3457,7 @@ program.command("init").alias("\uC2DC\uC791").alias("\uB9CC\uB4E4\uAE30").descri
|
|
|
3259
3457
|
program.command("recap").alias("\uC815\uB9AC").alias("\uC624\uB298").description("\uC624\uB298 \uD55C \uC77C \uC815\uB9AC + ADR/\uD2B8\uB7EC\uBE14\uC288\uD305 \uC790\uB3D9 \uBD84\uB9AC").option("--since <date>", "\uBD84\uC11D \uC2DC\uC791\uC77C (YYYY-MM-DD)").action(recap);
|
|
3260
3458
|
program.command("sync").alias("\uB9DE\uCD94\uAE30").alias("\uADDC\uCE59").description("RULES.md \u2192 .cursorrules + CLAUDE.md \uB3D9\uAE30\uD654").action(sync);
|
|
3261
3459
|
program.command("check").alias("\uC810\uAC80").alias("\uB9B0\uD2B8").description("RULES.md \uADDC\uCE59 \uC810\uAC80 \u2014 \uCF54\uB4DC \uC704\uBC18 \uAC80\uC0AC").action(check);
|
|
3262
|
-
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C");
|
|
3460
|
+
var secureCmd = program.command("secure").alias("\uBCF4\uC548").description("\uBCF4\uC548 \uB3C4\uAD6C \uBAA8\uC74C \u2014 scan: \uC2DC\uD06C\uB9BF\xB7\uD0A4 \uC720\uCD9C \uAC80\uC0AC").action(secure);
|
|
3263
3461
|
secureCmd.command("scan").alias("\uC2A4\uCE94").description("\uC2DC\uD06C\uB9BF/\uD0A4 \uC720\uCD9C \uC2A4\uCE94").action(secure);
|
|
3264
3462
|
program.command("ship").alias("\uBC30\uD3EC").alias("\uB9B4\uB9AC\uC988").description("\uBC30\uD3EC \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 + \uD68C\uACE0 + \uBE4C\uB4DC \uB85C\uADF8 \uC0DD\uC131").action(ship);
|
|
3265
3463
|
program.command("doctor").alias("\uD658\uACBD").alias("\uC9C4\uB2E8").description("\uAC1C\uBC1C \uD658\uACBD \uC810\uAC80 \u2014 Node/Git/npm \uC0C1\uD0DC \uD655\uC778").action(doctor);
|
|
@@ -3278,8 +3476,8 @@ program.on("command:*", async (operands) => {
|
|
|
3278
3476
|
const route = routeNaturalLanguage(input);
|
|
3279
3477
|
if (route) {
|
|
3280
3478
|
console.log("");
|
|
3281
|
-
console.log(
|
|
3282
|
-
console.log(
|
|
3479
|
+
console.log(chalk16.cyan(` \u{1F4AC} "${input}"`));
|
|
3480
|
+
console.log(chalk16.cyan(` \u2192 ${route.explanation}`));
|
|
3283
3481
|
if (route.confidence === "low") {
|
|
3284
3482
|
const { confirm } = await inquirer7.prompt([{
|
|
3285
3483
|
type: "confirm",
|
|
@@ -3288,7 +3486,7 @@ program.on("command:*", async (operands) => {
|
|
|
3288
3486
|
default: true
|
|
3289
3487
|
}]);
|
|
3290
3488
|
if (!confirm) {
|
|
3291
|
-
console.log(
|
|
3489
|
+
console.log(chalk16.dim(` ${ko.nlp.menuHint}`));
|
|
3292
3490
|
return;
|
|
3293
3491
|
}
|
|
3294
3492
|
}
|
|
@@ -3323,7 +3521,7 @@ program.on("command:*", async (operands) => {
|
|
|
3323
3521
|
return diff();
|
|
3324
3522
|
}
|
|
3325
3523
|
}
|
|
3326
|
-
console.log(
|
|
3524
|
+
console.log(chalk16.yellow(`
|
|
3327
3525
|
\u2753 "${input}" \u2014 ${ko.nlp.notMatched}
|
|
3328
3526
|
`));
|
|
3329
3527
|
});
|
|
@@ -3375,4 +3573,4 @@ program.action(async () => {
|
|
|
3375
3573
|
return diff();
|
|
3376
3574
|
}
|
|
3377
3575
|
});
|
|
3378
|
-
program.
|
|
3576
|
+
await program.parseAsync(process.argv);
|