@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
|
@@ -323,7 +323,7 @@ var require_ignore = __commonJS({
|
|
|
323
323
|
// path matching.
|
|
324
324
|
// - check `string` either `MODE_IGNORE` or `MODE_CHECK_IGNORE`
|
|
325
325
|
// @returns {TestResult} true if a file is ignored
|
|
326
|
-
test(
|
|
326
|
+
test(path7, checkUnignored, mode) {
|
|
327
327
|
let ignored = false;
|
|
328
328
|
let unignored = false;
|
|
329
329
|
let matchedRule;
|
|
@@ -332,7 +332,7 @@ var require_ignore = __commonJS({
|
|
|
332
332
|
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) {
|
|
333
333
|
return;
|
|
334
334
|
}
|
|
335
|
-
const matched = rule[mode].test(
|
|
335
|
+
const matched = rule[mode].test(path7);
|
|
336
336
|
if (!matched) {
|
|
337
337
|
return;
|
|
338
338
|
}
|
|
@@ -353,17 +353,17 @@ var require_ignore = __commonJS({
|
|
|
353
353
|
var throwError = (message, Ctor) => {
|
|
354
354
|
throw new Ctor(message);
|
|
355
355
|
};
|
|
356
|
-
var checkPath = (
|
|
357
|
-
if (!isString(
|
|
356
|
+
var checkPath = (path7, originalPath, doThrow) => {
|
|
357
|
+
if (!isString(path7)) {
|
|
358
358
|
return doThrow(
|
|
359
359
|
`path must be a string, but got \`${originalPath}\``,
|
|
360
360
|
TypeError
|
|
361
361
|
);
|
|
362
362
|
}
|
|
363
|
-
if (!
|
|
363
|
+
if (!path7) {
|
|
364
364
|
return doThrow(`path must not be empty`, TypeError);
|
|
365
365
|
}
|
|
366
|
-
if (checkPath.isNotRelative(
|
|
366
|
+
if (checkPath.isNotRelative(path7)) {
|
|
367
367
|
const r = "`path.relative()`d";
|
|
368
368
|
return doThrow(
|
|
369
369
|
`path should be a ${r} string, but got "${originalPath}"`,
|
|
@@ -372,7 +372,7 @@ var require_ignore = __commonJS({
|
|
|
372
372
|
}
|
|
373
373
|
return true;
|
|
374
374
|
};
|
|
375
|
-
var isNotRelative = (
|
|
375
|
+
var isNotRelative = (path7) => REGEX_TEST_INVALID_PATH.test(path7);
|
|
376
376
|
checkPath.isNotRelative = isNotRelative;
|
|
377
377
|
checkPath.convert = (p) => p;
|
|
378
378
|
var Ignore = class {
|
|
@@ -402,19 +402,19 @@ var require_ignore = __commonJS({
|
|
|
402
402
|
}
|
|
403
403
|
// @returns {TestResult}
|
|
404
404
|
_test(originalPath, cache, checkUnignored, slices) {
|
|
405
|
-
const
|
|
405
|
+
const path7 = originalPath && checkPath.convert(originalPath);
|
|
406
406
|
checkPath(
|
|
407
|
-
|
|
407
|
+
path7,
|
|
408
408
|
originalPath,
|
|
409
409
|
this._strictPathCheck ? throwError : RETURN_FALSE
|
|
410
410
|
);
|
|
411
|
-
return this._t(
|
|
411
|
+
return this._t(path7, cache, checkUnignored, slices);
|
|
412
412
|
}
|
|
413
|
-
checkIgnore(
|
|
414
|
-
if (!REGEX_TEST_TRAILING_SLASH.test(
|
|
415
|
-
return this.test(
|
|
413
|
+
checkIgnore(path7) {
|
|
414
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path7)) {
|
|
415
|
+
return this.test(path7);
|
|
416
416
|
}
|
|
417
|
-
const slices =
|
|
417
|
+
const slices = path7.split(SLASH).filter(Boolean);
|
|
418
418
|
slices.pop();
|
|
419
419
|
if (slices.length) {
|
|
420
420
|
const parent = this._t(
|
|
@@ -427,18 +427,18 @@ var require_ignore = __commonJS({
|
|
|
427
427
|
return parent;
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
|
-
return this._rules.test(
|
|
430
|
+
return this._rules.test(path7, false, MODE_CHECK_IGNORE);
|
|
431
431
|
}
|
|
432
|
-
_t(
|
|
433
|
-
if (
|
|
434
|
-
return cache[
|
|
432
|
+
_t(path7, cache, checkUnignored, slices) {
|
|
433
|
+
if (path7 in cache) {
|
|
434
|
+
return cache[path7];
|
|
435
435
|
}
|
|
436
436
|
if (!slices) {
|
|
437
|
-
slices =
|
|
437
|
+
slices = path7.split(SLASH).filter(Boolean);
|
|
438
438
|
}
|
|
439
439
|
slices.pop();
|
|
440
440
|
if (!slices.length) {
|
|
441
|
-
return cache[
|
|
441
|
+
return cache[path7] = this._rules.test(path7, checkUnignored, MODE_IGNORE);
|
|
442
442
|
}
|
|
443
443
|
const parent = this._t(
|
|
444
444
|
slices.join(SLASH) + SLASH,
|
|
@@ -446,29 +446,29 @@ var require_ignore = __commonJS({
|
|
|
446
446
|
checkUnignored,
|
|
447
447
|
slices
|
|
448
448
|
);
|
|
449
|
-
return cache[
|
|
449
|
+
return cache[path7] = parent.ignored ? parent : this._rules.test(path7, checkUnignored, MODE_IGNORE);
|
|
450
450
|
}
|
|
451
|
-
ignores(
|
|
452
|
-
return this._test(
|
|
451
|
+
ignores(path7) {
|
|
452
|
+
return this._test(path7, this._ignoreCache, false).ignored;
|
|
453
453
|
}
|
|
454
454
|
createFilter() {
|
|
455
|
-
return (
|
|
455
|
+
return (path7) => !this.ignores(path7);
|
|
456
456
|
}
|
|
457
457
|
filter(paths) {
|
|
458
458
|
return makeArray(paths).filter(this.createFilter());
|
|
459
459
|
}
|
|
460
460
|
// @returns {TestResult}
|
|
461
|
-
test(
|
|
462
|
-
return this._test(
|
|
461
|
+
test(path7) {
|
|
462
|
+
return this._test(path7, this._testCache, true);
|
|
463
463
|
}
|
|
464
464
|
};
|
|
465
465
|
var factory = (options) => new Ignore(options);
|
|
466
|
-
var isPathValid = (
|
|
466
|
+
var isPathValid = (path7) => checkPath(path7 && checkPath.convert(path7), path7, RETURN_FALSE);
|
|
467
467
|
var setupWindows = () => {
|
|
468
468
|
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
469
469
|
checkPath.convert = makePosix;
|
|
470
470
|
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
471
|
-
checkPath.isNotRelative = (
|
|
471
|
+
checkPath.isNotRelative = (path7) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path7) || isNotRelative(path7);
|
|
472
472
|
};
|
|
473
473
|
if (
|
|
474
474
|
// Detect `process` so that it can run in browsers.
|
|
@@ -483,76 +483,15 @@ var require_ignore = __commonJS({
|
|
|
483
483
|
}
|
|
484
484
|
});
|
|
485
485
|
|
|
486
|
-
// src/commands/
|
|
487
|
-
import { existsSync } from "fs";
|
|
486
|
+
// src/commands/sync.ts
|
|
488
487
|
import chalk2 from "chalk";
|
|
488
|
+
import fs4 from "fs";
|
|
489
|
+
import path4 from "path";
|
|
489
490
|
import inquirer from "inquirer";
|
|
490
491
|
|
|
491
|
-
// src/lib/
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
function platformCmd(cmd) {
|
|
495
|
-
if (process.platform === "win32" && SHIM_BINARIES.has(cmd)) {
|
|
496
|
-
return `${cmd}.cmd`;
|
|
497
|
-
}
|
|
498
|
-
return cmd;
|
|
499
|
-
}
|
|
500
|
-
function resolveCmd(cmd, args) {
|
|
501
|
-
if (process.platform === "win32" && SHIM_BINARIES.has(cmd)) {
|
|
502
|
-
return { bin: "cmd.exe", argv: ["/d", "/s", "/c", `${cmd}.cmd`, ...args] };
|
|
503
|
-
}
|
|
504
|
-
return { bin: platformCmd(cmd), argv: args };
|
|
505
|
-
}
|
|
506
|
-
var DEFAULT_EXEC_TIMEOUT_MS = 6e5;
|
|
507
|
-
var NETWORK_EXEC_TIMEOUT_MS = 3e4;
|
|
508
|
-
function resolveTimeout(timeoutMs, fallback) {
|
|
509
|
-
const v = timeoutMs === void 0 ? fallback : timeoutMs;
|
|
510
|
-
return v > 0 ? v : void 0;
|
|
511
|
-
}
|
|
512
|
-
function isTimeoutError(e, timeout) {
|
|
513
|
-
if (!timeout) return false;
|
|
514
|
-
return e.code === "ETIMEDOUT";
|
|
515
|
-
}
|
|
516
|
-
function safeExecFile(cmd, args, opts = {}) {
|
|
517
|
-
const { bin, argv } = resolveCmd(cmd, args);
|
|
518
|
-
const env2 = opts.env ? { ...process.env, ...opts.env } : void 0;
|
|
519
|
-
const timeout = resolveTimeout(opts.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
|
|
520
|
-
try {
|
|
521
|
-
const out = execFileSync(bin, argv, {
|
|
522
|
-
encoding: "utf-8",
|
|
523
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
524
|
-
env: env2,
|
|
525
|
-
...timeout ? { timeout, killSignal: "SIGTERM" } : {}
|
|
526
|
-
}).toString();
|
|
527
|
-
return { ok: true, out: out.trim() };
|
|
528
|
-
} catch (err) {
|
|
529
|
-
const e = err;
|
|
530
|
-
const stdout = e.stdout ? e.stdout.toString() : "";
|
|
531
|
-
let msg = e.message ?? String(err);
|
|
532
|
-
if (isTimeoutError(e, timeout)) {
|
|
533
|
-
msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
|
|
534
|
-
}
|
|
535
|
-
return { ok: false, err: msg, out: stdout.trim() };
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
function safeExecFileStream(cmd, args, opts = {}) {
|
|
539
|
-
const { bin, argv } = resolveCmd(cmd, args);
|
|
540
|
-
const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : void 0;
|
|
541
|
-
try {
|
|
542
|
-
execFileSync(bin, argv, {
|
|
543
|
-
encoding: "utf-8",
|
|
544
|
-
stdio: "inherit",
|
|
545
|
-
...timeout ? { timeout, killSignal: "SIGTERM" } : {}
|
|
546
|
-
});
|
|
547
|
-
return { ok: true };
|
|
548
|
-
} catch (err) {
|
|
549
|
-
const e = err;
|
|
550
|
-
let msg = err instanceof Error ? err.message : String(err);
|
|
551
|
-
if (isTimeoutError(e, timeout)) {
|
|
552
|
-
msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
|
|
553
|
-
}
|
|
554
|
-
return { ok: false, err: msg };
|
|
555
|
-
}
|
|
492
|
+
// src/lib/date.ts
|
|
493
|
+
function localDate(d = /* @__PURE__ */ new Date()) {
|
|
494
|
+
return d.toLocaleDateString("sv-SE");
|
|
556
495
|
}
|
|
557
496
|
|
|
558
497
|
// src/i18n/ko.ts
|
|
@@ -926,7 +865,10 @@ var ko = {
|
|
|
926
865
|
},
|
|
927
866
|
context: {
|
|
928
867
|
title: "\uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8 \uC0DD\uC131",
|
|
929
|
-
showTitle: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C"
|
|
868
|
+
showTitle: "\uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C",
|
|
869
|
+
resumeMissing: "\u{1F9ED} AI \uC138\uC158 \uBCF5\uC6D0 \uCEE8\uD14D\uC2A4\uD2B8 \uC5C6\uC74C \u2192 \uC0DD\uC131: vhk context",
|
|
870
|
+
resumeExists: "\u{1F9ED} \uC0C8 \uC138\uC158\uC774\uBA74 AI \uCEE8\uD14D\uC2A4\uD2B8 \uBCF5\uC6D0: vhk context-show (\uAC31\uC2E0: vhk context)",
|
|
871
|
+
resumeStale: "\u{1F9ED} \uCEE8\uD14D\uC2A4\uD2B8\uAC00 \uC624\uB798\uB428(\uCF54\uB4DC \uBCC0\uACBD \uC774\uD6C4) \u2192 \uAC31\uC2E0: vhk context"
|
|
930
872
|
},
|
|
931
873
|
brief: {
|
|
932
874
|
title: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551"
|
|
@@ -937,7 +879,9 @@ var ko = {
|
|
|
937
879
|
initTitle: "\u{1F3D7}\uFE0F goals/ \uAD6C\uC870 \uC2A4\uCE90\uD3F4\uB529",
|
|
938
880
|
checkTitle: "\u2705 Goal \uAC8C\uC774\uD2B8 \uAC80\uC99D",
|
|
939
881
|
doneTitle: "\u{1F3C1} Goal \uC644\uB8CC \uCC98\uB9AC",
|
|
882
|
+
syncTitle: "\u{1F504} Goal \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uB3D9\uAE30\uD654",
|
|
940
883
|
duplicateId: (ids) => `\u26A0 \uC911\uBCF5\uB41C goal id: ${ids} \u2014 \uAC19\uC740 id \uD30C\uC77C\uC774 \uC5EC\uB7EC \uAC1C\uBA74 \uCCAB \uB9E4\uCE58\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4. id \uB97C \uC720\uC77C\uD558\uAC8C \uACE0\uCE58\uC138\uC694.`,
|
|
884
|
+
skippedFiles: (n) => `\u26A0 \uC2A4\uD0A4\uB9C8 \uBD88\uC77C\uCE58\uB85C \uBB34\uC2DC\uB41C \uD30C\uC77C ${n}\uAC1C (goal \uB85C \uC548 \uC7A1\uD798 \u2014 silent skip):`,
|
|
941
885
|
notFound: (id) => `goal id ${id} \uC5C6\uC74C \u2014 vhk goal list \uB85C \uD655\uC778\uD558\uC138\uC694.`
|
|
942
886
|
},
|
|
943
887
|
agent: {
|
|
@@ -946,8 +890,8 @@ var ko = {
|
|
|
946
890
|
resumeTitle: "\u25B6\uFE0F HARD_STOP \uD574\uC81C"
|
|
947
891
|
}
|
|
948
892
|
};
|
|
949
|
-
function lookup(
|
|
950
|
-
const parts =
|
|
893
|
+
function lookup(path7) {
|
|
894
|
+
const parts = path7.split(".");
|
|
951
895
|
let cur = ko;
|
|
952
896
|
for (const part of parts) {
|
|
953
897
|
if (cur === null || typeof cur !== "object") return void 0;
|
|
@@ -965,7 +909,124 @@ function t(key, ...args) {
|
|
|
965
909
|
}
|
|
966
910
|
|
|
967
911
|
// src/lib/next-step.ts
|
|
912
|
+
import fs2 from "fs";
|
|
913
|
+
import path2 from "path";
|
|
968
914
|
import chalk from "chalk";
|
|
915
|
+
|
|
916
|
+
// src/lib/drift.ts
|
|
917
|
+
import fs from "fs";
|
|
918
|
+
import path from "path";
|
|
919
|
+
|
|
920
|
+
// src/lib/git-repo.ts
|
|
921
|
+
import { execFileSync } from "child_process";
|
|
922
|
+
function getGitRoot(cwd = process.cwd()) {
|
|
923
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
924
|
+
encoding: "utf-8",
|
|
925
|
+
cwd,
|
|
926
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
927
|
+
}).trim();
|
|
928
|
+
}
|
|
929
|
+
function gitOut(args, cwd) {
|
|
930
|
+
return execFileSync("git", args, {
|
|
931
|
+
encoding: "utf-8",
|
|
932
|
+
cwd,
|
|
933
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function gitRun(args, cwd) {
|
|
937
|
+
execFileSync("git", args, { stdio: "pipe", cwd });
|
|
938
|
+
}
|
|
939
|
+
function getExecErrorMessage(err) {
|
|
940
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
941
|
+
const stderr = err.stderr;
|
|
942
|
+
if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
|
|
943
|
+
if (typeof stderr === "string") return stderr.trim();
|
|
944
|
+
}
|
|
945
|
+
return err instanceof Error ? err.message : String(err);
|
|
946
|
+
}
|
|
947
|
+
function hasGitRemote(cwd) {
|
|
948
|
+
try {
|
|
949
|
+
return gitOut(["remote"], cwd).trim().length > 0;
|
|
950
|
+
} catch {
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
function countLocalCommits(cwd) {
|
|
955
|
+
try {
|
|
956
|
+
const out = gitOut(["rev-list", "--count", "HEAD"], cwd).trim();
|
|
957
|
+
return parseInt(out, 10) || 0;
|
|
958
|
+
} catch {
|
|
959
|
+
return 0;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/lib/drift.ts
|
|
964
|
+
function normalizeForCompare(s) {
|
|
965
|
+
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").replace(/\n+$/, "\n");
|
|
966
|
+
}
|
|
967
|
+
function checkRuleDrift(rootDir) {
|
|
968
|
+
const rulesPath = path.join(rootDir, "RULES.md");
|
|
969
|
+
if (!fs.existsSync(rulesPath)) return { checked: false, results: [] };
|
|
970
|
+
const rulesContent = fs.readFileSync(rulesPath, "utf-8");
|
|
971
|
+
const sections = parseRulesMd(rulesContent);
|
|
972
|
+
const projectName = deriveProjectName(rulesContent);
|
|
973
|
+
const results = [];
|
|
974
|
+
for (const target of SYNC_TARGETS) {
|
|
975
|
+
const fullPath = path.join(rootDir, target.path);
|
|
976
|
+
if (!fs.existsSync(fullPath)) {
|
|
977
|
+
results.push({ path: target.path, status: "missing" });
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const expected = normalizeForCompare(target.generate(sections, projectName));
|
|
981
|
+
const actual = normalizeForCompare(fs.readFileSync(fullPath, "utf-8"));
|
|
982
|
+
results.push({ path: target.path, status: expected === actual ? "ok" : "drifted" });
|
|
983
|
+
}
|
|
984
|
+
return { checked: true, results };
|
|
985
|
+
}
|
|
986
|
+
var CONTEXT_GIT_MARKER = "vhk-context-git";
|
|
987
|
+
var CONTEXT_PATH = ".vhk/context.md";
|
|
988
|
+
function extractContextSha(content) {
|
|
989
|
+
const m = content.match(new RegExp(`${CONTEXT_GIT_MARKER}:\\s*([0-9a-f]{7,40})`));
|
|
990
|
+
return m ? m[1] : null;
|
|
991
|
+
}
|
|
992
|
+
var CONTEXT_SOURCE_PATHS = ["package.json", "goals", "docs/state/learnings.md"];
|
|
993
|
+
function contextSourcesChanged(generatedSha, rootDir) {
|
|
994
|
+
const content = gitOut(
|
|
995
|
+
["diff", "--name-only", generatedSha, "HEAD", "--", ...CONTEXT_SOURCE_PATHS],
|
|
996
|
+
rootDir
|
|
997
|
+
).trim();
|
|
998
|
+
if (content) return true;
|
|
999
|
+
const structural = gitOut(
|
|
1000
|
+
["diff", "--name-only", "--diff-filter=ADR", generatedSha, "HEAD"],
|
|
1001
|
+
rootDir
|
|
1002
|
+
).trim();
|
|
1003
|
+
return structural.length > 0;
|
|
1004
|
+
}
|
|
1005
|
+
function checkContextDrift(rootDir) {
|
|
1006
|
+
const ctxPath = path.join(rootDir, CONTEXT_PATH);
|
|
1007
|
+
if (!fs.existsSync(ctxPath)) return { checked: false, stale: false };
|
|
1008
|
+
const generatedSha = extractContextSha(fs.readFileSync(ctxPath, "utf-8"));
|
|
1009
|
+
if (!generatedSha) return { checked: false, stale: false };
|
|
1010
|
+
let currentSha;
|
|
1011
|
+
try {
|
|
1012
|
+
currentSha = gitOut(["rev-parse", "HEAD"], rootDir).trim();
|
|
1013
|
+
} catch {
|
|
1014
|
+
return { checked: false, stale: false };
|
|
1015
|
+
}
|
|
1016
|
+
if (!currentSha) return { checked: false, stale: false };
|
|
1017
|
+
if (currentSha.startsWith(generatedSha) || generatedSha.startsWith(currentSha)) {
|
|
1018
|
+
return { checked: true, stale: false, generatedSha, currentSha };
|
|
1019
|
+
}
|
|
1020
|
+
let stale;
|
|
1021
|
+
try {
|
|
1022
|
+
stale = contextSourcesChanged(generatedSha, rootDir);
|
|
1023
|
+
} catch {
|
|
1024
|
+
return { checked: false, stale: false };
|
|
1025
|
+
}
|
|
1026
|
+
return { checked: true, stale, generatedSha, currentSha };
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/lib/next-step.ts
|
|
969
1030
|
function printNextStep(step) {
|
|
970
1031
|
console.log("");
|
|
971
1032
|
console.log(chalk.cyan.bold("\u2501\u2501\u2501 \uB2E4\uC74C\uC5D0 \uC774\uAC83\uB9CC \uD558\uC138\uC694 \u2501\u2501\u2501"));
|
|
@@ -989,6 +1050,580 @@ function printNextStep(step) {
|
|
|
989
1050
|
console.log(chalk.cyan.bold("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"));
|
|
990
1051
|
console.log("");
|
|
991
1052
|
}
|
|
1053
|
+
function printContextResumeHint(cwd = process.cwd()) {
|
|
1054
|
+
const ctxPath = path2.join(cwd, ".vhk", "context.md");
|
|
1055
|
+
if (!fs2.existsSync(ctxPath)) {
|
|
1056
|
+
console.log(chalk.dim(` ${t("context.resumeMissing")}`));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const drift = checkContextDrift(cwd);
|
|
1060
|
+
if (drift.checked && drift.stale) {
|
|
1061
|
+
console.log(chalk.dim(` ${t("context.resumeStale")}`));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
console.log(chalk.dim(` ${t("context.resumeExists")}`));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/lib/backup.ts
|
|
1068
|
+
import fs3 from "fs";
|
|
1069
|
+
import path3 from "path";
|
|
1070
|
+
var BACKUPS_REL = path3.join(".vhk", "backups");
|
|
1071
|
+
var VHK_GITIGNORE_REL = path3.join(".vhk", ".gitignore");
|
|
1072
|
+
function fsSafeStamp(d) {
|
|
1073
|
+
return d.toISOString().replace(/[:.]/g, "-");
|
|
1074
|
+
}
|
|
1075
|
+
function ensureVhkIgnored(rootDir, ...entries) {
|
|
1076
|
+
const giPath = path3.join(rootDir, VHK_GITIGNORE_REL);
|
|
1077
|
+
fs3.mkdirSync(path3.dirname(giPath), { recursive: true });
|
|
1078
|
+
let content = fs3.existsSync(giPath) ? fs3.readFileSync(giPath, "utf-8") : "";
|
|
1079
|
+
const present = new Set(content.split("\n").map((l) => l.trim().replace(/\/$/, "")));
|
|
1080
|
+
const missing = entries.filter((e) => !present.has(e.trim().replace(/\/$/, "")));
|
|
1081
|
+
if (missing.length === 0) return;
|
|
1082
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
1083
|
+
content += missing.join("\n") + "\n";
|
|
1084
|
+
fs3.writeFileSync(giPath, content, "utf-8");
|
|
1085
|
+
}
|
|
1086
|
+
function walkRelFiles(baseDir, cur = baseDir) {
|
|
1087
|
+
const out = [];
|
|
1088
|
+
for (const entry of fs3.readdirSync(cur)) {
|
|
1089
|
+
const full = path3.join(cur, entry);
|
|
1090
|
+
if (fs3.statSync(full).isDirectory()) {
|
|
1091
|
+
out.push(...walkRelFiles(baseDir, full));
|
|
1092
|
+
} else {
|
|
1093
|
+
out.push(path3.relative(baseDir, full).split(path3.sep).join("/"));
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
function saveBackup(files, rootDir, stamp) {
|
|
1099
|
+
const baseId = stamp ?? fsSafeStamp(/* @__PURE__ */ new Date());
|
|
1100
|
+
let id = baseId;
|
|
1101
|
+
let n = 1;
|
|
1102
|
+
while (fs3.existsSync(path3.join(rootDir, BACKUPS_REL, id))) {
|
|
1103
|
+
id = `${baseId}-${String(n++).padStart(3, "0")}`;
|
|
1104
|
+
}
|
|
1105
|
+
const backupDir = path3.join(rootDir, BACKUPS_REL, id);
|
|
1106
|
+
const saved = [];
|
|
1107
|
+
for (const rel of files) {
|
|
1108
|
+
const src = path3.join(rootDir, rel);
|
|
1109
|
+
if (!fs3.existsSync(src)) continue;
|
|
1110
|
+
const dest = path3.join(backupDir, rel);
|
|
1111
|
+
fs3.mkdirSync(path3.dirname(dest), { recursive: true });
|
|
1112
|
+
fs3.copyFileSync(src, dest);
|
|
1113
|
+
saved.push(rel);
|
|
1114
|
+
}
|
|
1115
|
+
ensureVhkIgnored(rootDir, "backups/");
|
|
1116
|
+
return { id, dir: backupDir, files: saved };
|
|
1117
|
+
}
|
|
1118
|
+
function backupOrderKey(id) {
|
|
1119
|
+
const m = /^(.*Z)(?:-(\d+))?$/.exec(id);
|
|
1120
|
+
return m ? [m[1], m[2] ? parseInt(m[2], 10) : 0] : [id, 0];
|
|
1121
|
+
}
|
|
1122
|
+
function listBackups(rootDir) {
|
|
1123
|
+
const root = path3.join(rootDir, BACKUPS_REL);
|
|
1124
|
+
if (!fs3.existsSync(root)) return [];
|
|
1125
|
+
return fs3.readdirSync(root).filter((e) => fs3.statSync(path3.join(root, e)).isDirectory()).sort((a, b) => {
|
|
1126
|
+
const [ba, na] = backupOrderKey(a);
|
|
1127
|
+
const [bb, nb] = backupOrderKey(b);
|
|
1128
|
+
if (ba !== bb) return ba < bb ? 1 : -1;
|
|
1129
|
+
return nb - na;
|
|
1130
|
+
}).map((id) => {
|
|
1131
|
+
const dir = path3.join(root, id);
|
|
1132
|
+
return { id, dir, files: walkRelFiles(dir) };
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function restoreBackup(id, rootDir) {
|
|
1136
|
+
const backupDir = path3.join(rootDir, BACKUPS_REL, id);
|
|
1137
|
+
if (!fs3.existsSync(backupDir)) {
|
|
1138
|
+
throw new Error(`\uBC31\uC5C5 \uC5C6\uC74C: ${id}`);
|
|
1139
|
+
}
|
|
1140
|
+
const rels = walkRelFiles(backupDir);
|
|
1141
|
+
for (const rel of rels) {
|
|
1142
|
+
const src = path3.join(backupDir, rel);
|
|
1143
|
+
const dest = path3.join(rootDir, rel);
|
|
1144
|
+
fs3.mkdirSync(path3.dirname(dest), { recursive: true });
|
|
1145
|
+
fs3.copyFileSync(src, dest);
|
|
1146
|
+
}
|
|
1147
|
+
return rels;
|
|
1148
|
+
}
|
|
1149
|
+
function pruneBackups(keepN, rootDir) {
|
|
1150
|
+
const all = listBackups(rootDir);
|
|
1151
|
+
const toDelete = all.slice(Math.max(0, keepN));
|
|
1152
|
+
for (const b of toDelete) {
|
|
1153
|
+
fs3.rmSync(b.dir, { recursive: true, force: true });
|
|
1154
|
+
}
|
|
1155
|
+
return toDelete.map((b) => b.id);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/lib/rules-import.ts
|
|
1159
|
+
import { existsSync, readFileSync } from "fs";
|
|
1160
|
+
import { join } from "path";
|
|
1161
|
+
var ADOPT_SOURCES = [
|
|
1162
|
+
".cursorrules",
|
|
1163
|
+
"CLAUDE.md",
|
|
1164
|
+
"AGENTS.md",
|
|
1165
|
+
".windsurfrules",
|
|
1166
|
+
".github/copilot-instructions.md"
|
|
1167
|
+
];
|
|
1168
|
+
var PREAMBLE_TITLE = "\uC11C\uBB38";
|
|
1169
|
+
function detectExistingRuleFiles(cwd) {
|
|
1170
|
+
const found = [];
|
|
1171
|
+
for (const rel of ADOPT_SOURCES) {
|
|
1172
|
+
const full = join(cwd, rel);
|
|
1173
|
+
if (existsSync(full)) {
|
|
1174
|
+
try {
|
|
1175
|
+
found.push({ path: rel, content: readFileSync(full, "utf-8") });
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return found;
|
|
1181
|
+
}
|
|
1182
|
+
function splitSections(content) {
|
|
1183
|
+
const sections = [];
|
|
1184
|
+
let title = "";
|
|
1185
|
+
let buf = [];
|
|
1186
|
+
const preamble = [];
|
|
1187
|
+
let sawHeading = false;
|
|
1188
|
+
for (const line of content.split("\n")) {
|
|
1189
|
+
if (line.startsWith("## ")) {
|
|
1190
|
+
sawHeading = true;
|
|
1191
|
+
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1192
|
+
title = line.replace("## ", "").trim();
|
|
1193
|
+
buf = [];
|
|
1194
|
+
} else if (title) {
|
|
1195
|
+
buf.push(line);
|
|
1196
|
+
} else if (!sawHeading) {
|
|
1197
|
+
preamble.push(line);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (title) sections.push({ title, content: buf.join("\n").trim() });
|
|
1201
|
+
const pre = preamble.join("\n").trim();
|
|
1202
|
+
if (pre) sections.unshift({ title: PREAMBLE_TITLE, content: pre });
|
|
1203
|
+
return sections;
|
|
1204
|
+
}
|
|
1205
|
+
function buildAdoptedRules(files, projectName) {
|
|
1206
|
+
const order = [];
|
|
1207
|
+
const byTitle = /* @__PURE__ */ new Map();
|
|
1208
|
+
for (const file of files) {
|
|
1209
|
+
for (const sec of splitSections(file.content)) {
|
|
1210
|
+
let merged = byTitle.get(sec.title);
|
|
1211
|
+
if (!merged) {
|
|
1212
|
+
merged = { title: sec.title, parts: [] };
|
|
1213
|
+
byTitle.set(sec.title, merged);
|
|
1214
|
+
order.push(sec.title);
|
|
1215
|
+
}
|
|
1216
|
+
merged.parts.push({ source: file.path, content: sec.content });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const lines = [
|
|
1220
|
+
`# ${projectName} \u2014 Rules`,
|
|
1221
|
+
"",
|
|
1222
|
+
"> \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.",
|
|
1223
|
+
"> \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.",
|
|
1224
|
+
""
|
|
1225
|
+
];
|
|
1226
|
+
for (const title of order) {
|
|
1227
|
+
const merged = byTitle.get(title);
|
|
1228
|
+
const nonEmpty = merged.parts.filter((p) => p.content.trim());
|
|
1229
|
+
if (!nonEmpty.length) continue;
|
|
1230
|
+
lines.push(`## ${title}`);
|
|
1231
|
+
for (const part of nonEmpty) {
|
|
1232
|
+
lines.push(`<!-- \uCD9C\uCC98: ${part.source} -->`);
|
|
1233
|
+
lines.push(part.content);
|
|
1234
|
+
}
|
|
1235
|
+
lines.push("");
|
|
1236
|
+
}
|
|
1237
|
+
return lines.join("\n");
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/commands/sync.ts
|
|
1241
|
+
var CURSORRULES_KEYS = ["\uCF54\uB529 \uADDC\uCE59", "\uAE30\uC220 \uC2A4\uD0DD", "\uC544\uD0A4\uD14D\uCC98", "\uB514\uC790\uC778", "Anti-patterns", "\uCEE4\uBC0B"];
|
|
1242
|
+
var CLAUDE_MD_KEYS = ["\uAE30\uB85D", "\uB85C\uADF8", "ADR", "\uD2B8\uB7EC\uBE14\uC288\uD305", "TIL", "/done", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8"];
|
|
1243
|
+
function findUnmappedSections(sections) {
|
|
1244
|
+
const allKeys = [...CURSORRULES_KEYS, ...CLAUDE_MD_KEYS];
|
|
1245
|
+
return sections.filter((s) => s.title !== PREAMBLE_TITLE && !allKeys.some((k) => s.title.includes(k))).map((s) => s.title);
|
|
1246
|
+
}
|
|
1247
|
+
function parseRulesMd(content) {
|
|
1248
|
+
const sections = [];
|
|
1249
|
+
const lines = content.split("\n");
|
|
1250
|
+
let currentTitle = "";
|
|
1251
|
+
let currentContent = [];
|
|
1252
|
+
for (const line of lines) {
|
|
1253
|
+
if (line.startsWith("## ")) {
|
|
1254
|
+
if (currentTitle) {
|
|
1255
|
+
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
1256
|
+
}
|
|
1257
|
+
currentTitle = line.replace("## ", "").trim();
|
|
1258
|
+
currentContent = [];
|
|
1259
|
+
} else {
|
|
1260
|
+
currentContent.push(line);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (currentTitle) {
|
|
1264
|
+
sections.push({ title: currentTitle, content: currentContent.join("\n").trim() });
|
|
1265
|
+
}
|
|
1266
|
+
return sections;
|
|
1267
|
+
}
|
|
1268
|
+
function buildCodingDoc(headerTitle, sections, projectName) {
|
|
1269
|
+
const codingSections = sections.filter(
|
|
1270
|
+
(s) => CURSORRULES_KEYS.some((k) => s.title.includes(k))
|
|
1271
|
+
);
|
|
1272
|
+
const lines = [
|
|
1273
|
+
`# ${projectName} \u2014 ${headerTitle}`,
|
|
1274
|
+
"",
|
|
1275
|
+
"> \uCF54\uB529/\uB514\uC790\uC778 \uC804\uC6A9. \uAE30\uB85D/\uC6B4\uC601 \u2192 CLAUDE.md \uCC38\uC870.",
|
|
1276
|
+
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
1277
|
+
"",
|
|
1278
|
+
"## \uD544\uC218 \uCC38\uC870",
|
|
1279
|
+
"- docs/PRD.md \xB7 docs/ARCHITECTURE.md \xB7 CLAUDE.md \xB7 RULES.md",
|
|
1280
|
+
""
|
|
1281
|
+
];
|
|
1282
|
+
for (const section of codingSections) {
|
|
1283
|
+
lines.push(`## ${section.title}`);
|
|
1284
|
+
lines.push(section.content);
|
|
1285
|
+
lines.push("");
|
|
1286
|
+
}
|
|
1287
|
+
return lines.join("\n");
|
|
1288
|
+
}
|
|
1289
|
+
function toCursorrules(sections, projectName) {
|
|
1290
|
+
return buildCodingDoc("Cursor Rules", sections, projectName);
|
|
1291
|
+
}
|
|
1292
|
+
function toWindsurfrules(sections, projectName) {
|
|
1293
|
+
return buildCodingDoc("Windsurf Rules", sections, projectName);
|
|
1294
|
+
}
|
|
1295
|
+
function toCopilotInstructions(sections, projectName) {
|
|
1296
|
+
return buildCodingDoc("GitHub Copilot Instructions", sections, projectName);
|
|
1297
|
+
}
|
|
1298
|
+
var ANTIGRAVITY_CHAR_LIMIT = 12e3;
|
|
1299
|
+
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";
|
|
1300
|
+
function truncateForAntigravity(content, limit = ANTIGRAVITY_CHAR_LIMIT) {
|
|
1301
|
+
if (Buffer.byteLength(content, "utf8") <= limit) return content;
|
|
1302
|
+
const SAFETY = 200;
|
|
1303
|
+
const budget = limit - Buffer.byteLength(ANTIGRAVITY_TRUNCATE_MARKER, "utf8") - SAFETY;
|
|
1304
|
+
let lo = 0;
|
|
1305
|
+
let hi = content.length;
|
|
1306
|
+
while (lo < hi) {
|
|
1307
|
+
const mid = lo + hi + 1 >> 1;
|
|
1308
|
+
if (Buffer.byteLength(content.slice(0, mid), "utf8") <= budget) lo = mid;
|
|
1309
|
+
else hi = mid - 1;
|
|
1310
|
+
}
|
|
1311
|
+
const charCut = lo;
|
|
1312
|
+
let cut = content.lastIndexOf("\n## ", charCut);
|
|
1313
|
+
if (cut < charCut * 0.5) {
|
|
1314
|
+
const nl = content.lastIndexOf("\n", charCut);
|
|
1315
|
+
cut = nl > 0 ? nl : charCut;
|
|
1316
|
+
}
|
|
1317
|
+
return content.slice(0, cut).trimEnd() + ANTIGRAVITY_TRUNCATE_MARKER;
|
|
1318
|
+
}
|
|
1319
|
+
function toAntigravityRules(sections, projectName) {
|
|
1320
|
+
return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
|
|
1321
|
+
}
|
|
1322
|
+
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.";
|
|
1323
|
+
function toClaudeMd(sections, existing) {
|
|
1324
|
+
const recordSections = sections.filter(
|
|
1325
|
+
(s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
|
|
1326
|
+
);
|
|
1327
|
+
const cleaned = existing.split("\n").filter((line) => line.trim() !== CLAUDE_AUTOGEN_BANNER).join("\n");
|
|
1328
|
+
const statusMatch = cleaned.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
|
|
1329
|
+
const statusSection = statusMatch ? statusMatch[0].trimEnd() : "";
|
|
1330
|
+
const header = cleaned.split("## ")[0].trim();
|
|
1331
|
+
const lines = [
|
|
1332
|
+
header,
|
|
1333
|
+
"",
|
|
1334
|
+
statusSection,
|
|
1335
|
+
"",
|
|
1336
|
+
CLAUDE_AUTOGEN_BANNER,
|
|
1337
|
+
""
|
|
1338
|
+
];
|
|
1339
|
+
for (const section of recordSections) {
|
|
1340
|
+
lines.push(`## ${section.title}`);
|
|
1341
|
+
lines.push(section.content);
|
|
1342
|
+
lines.push("");
|
|
1343
|
+
}
|
|
1344
|
+
return lines.join("\n");
|
|
1345
|
+
}
|
|
1346
|
+
function toAgentsMd(sections, projectName) {
|
|
1347
|
+
const codingSections = sections.filter((s) => CURSORRULES_KEYS.some((k) => s.title.includes(k)));
|
|
1348
|
+
const recordSections = sections.filter((s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k)));
|
|
1349
|
+
const lines = [
|
|
1350
|
+
`# ${projectName} \u2014 AGENTS.md (\uC5D0\uC774\uC804\uD2B8 \uC791\uB3D9 \uADDC\uC57D)`,
|
|
1351
|
+
"",
|
|
1352
|
+
"> \u26A1 \uC774 \uD30C\uC77C\uC740 RULES.md\uC5D0\uC11C \uC790\uB3D9 \uC0DD\uC131\uB428 (vhk sync). \uC9C1\uC811 \uC218\uC815 \uAE08\uC9C0.",
|
|
1353
|
+
"> \uBE60\uB978 \uC2DC\uC791(\uD1A0\uD070 \uC808\uAC10): `docs/context/agent-compact.md` \uB97C \uBA3C\uC800 \uC77D\uC73C\uC138\uC694.",
|
|
1354
|
+
"",
|
|
1355
|
+
"## Loop Protocol",
|
|
1356
|
+
"- \uB8E8\uD504: `context \u2192 goal next \u2192 \uC791\uC5C5 \u2192 goal check \u2192 goal done`",
|
|
1357
|
+
"- \uC791\uC5C5 \uC2DC\uC791 \uC2DC `.vhk/HARD_STOP` \uD655\uC778 \u2014 \uC788\uC73C\uBA74 \uBAA8\uB4E0 \uC790\uB3D9\uD654 \uC989\uC2DC \uC911\uB2E8.",
|
|
1358
|
+
"- active goal \uB9CC \uC791\uC5C5. `docs/state`(next-task/blockers/learnings)\uB294 SoT, append-only.",
|
|
1359
|
+
"- \uAC8C\uC774\uD2B8(tsc / test:run / build) \uD1B5\uACFC\uD574\uC57C\uB9CC `vhk goal done`.",
|
|
1360
|
+
""
|
|
1361
|
+
];
|
|
1362
|
+
for (const section of codingSections) {
|
|
1363
|
+
lines.push(`## ${section.title}`);
|
|
1364
|
+
lines.push(section.content);
|
|
1365
|
+
lines.push("");
|
|
1366
|
+
}
|
|
1367
|
+
for (const section of recordSections) {
|
|
1368
|
+
lines.push(`## ${section.title}`);
|
|
1369
|
+
lines.push(section.content);
|
|
1370
|
+
lines.push("");
|
|
1371
|
+
}
|
|
1372
|
+
return lines.join("\n");
|
|
1373
|
+
}
|
|
1374
|
+
function deriveProjectName(rulesContent) {
|
|
1375
|
+
const firstLine = rulesContent.split("\n")[0];
|
|
1376
|
+
return firstLine.replace(/^#\s*/, "").replace(/\s*—.*/, "").trim() || "Project";
|
|
1377
|
+
}
|
|
1378
|
+
var SYNC_TARGETS = [
|
|
1379
|
+
{ path: ".cursorrules", generate: toCursorrules, doneMessage: ko.sync.cursorrulesDone },
|
|
1380
|
+
{ path: ".windsurfrules", generate: toWindsurfrules, doneMessage: ko.sync.windsurfDone },
|
|
1381
|
+
{ path: ".github/copilot-instructions.md", generate: toCopilotInstructions, doneMessage: ko.sync.copilotDone },
|
|
1382
|
+
{ path: ".agents/rules/vhk-rules.md", generate: toAntigravityRules, doneMessage: ko.sync.antigravityDone },
|
|
1383
|
+
// AGENTS.md — 6번째 타겟. 항목 1개 추가로 sync·드리프트·백업 가드가 자동 반영된다.
|
|
1384
|
+
{ path: "AGENTS.md", generate: toAgentsMd, doneMessage: ko.sync.agentsDone }
|
|
1385
|
+
];
|
|
1386
|
+
var BACKUP_KEEP = 10;
|
|
1387
|
+
var SYNCED_MARKER_REL = path4.join(".vhk", ".synced");
|
|
1388
|
+
function buildSyncPlan(rootDir, sections, projectName) {
|
|
1389
|
+
const plan = [];
|
|
1390
|
+
for (const target of SYNC_TARGETS) {
|
|
1391
|
+
const fullPath = path4.join(rootDir, target.path);
|
|
1392
|
+
const exists = fs4.existsSync(fullPath);
|
|
1393
|
+
const newContent = target.generate(sections, projectName);
|
|
1394
|
+
const drift = exists ? normalizeForCompare(fs4.readFileSync(fullPath, "utf-8")) !== normalizeForCompare(newContent) : false;
|
|
1395
|
+
plan.push({ path: target.path, newContent, doneMessage: target.doneMessage, exists, drift });
|
|
1396
|
+
}
|
|
1397
|
+
const claudePath = path4.join(rootDir, "CLAUDE.md");
|
|
1398
|
+
const claudeExists = fs4.existsSync(claudePath);
|
|
1399
|
+
const existingClaude = claudeExists ? fs4.readFileSync(claudePath, "utf-8") : `# \uAE30\uB85D \uADDC\uCE59 (${projectName})
|
|
1400
|
+
|
|
1401
|
+
## \uD604\uC7AC \uC0C1\uD0DC
|
|
1402
|
+
- **Phase:** **FILL**
|
|
1403
|
+
- **\uBE14\uB85C\uCEE4:** \uC5C6\uC74C
|
|
1404
|
+
- **\uB2E4\uC74C \uC561\uC158:** **FILL**
|
|
1405
|
+
- **\uB9C8\uC9C0\uB9C9 \uC5C5\uB370\uC774\uD2B8:** ${localDate()}`;
|
|
1406
|
+
const claudeNew = toClaudeMd(sections, existingClaude);
|
|
1407
|
+
const claudeDrift = claudeExists ? normalizeForCompare(existingClaude) !== normalizeForCompare(claudeNew) : false;
|
|
1408
|
+
plan.push({
|
|
1409
|
+
path: "CLAUDE.md",
|
|
1410
|
+
newContent: claudeNew,
|
|
1411
|
+
doneMessage: ko.sync.claudeDone,
|
|
1412
|
+
exists: claudeExists,
|
|
1413
|
+
drift: claudeDrift
|
|
1414
|
+
});
|
|
1415
|
+
return plan;
|
|
1416
|
+
}
|
|
1417
|
+
async function syncCore(rootDir, opts, confirmOverwrite) {
|
|
1418
|
+
const rulesContent = fs4.readFileSync(path4.join(rootDir, "RULES.md"), "utf-8");
|
|
1419
|
+
const sections = parseRulesMd(rulesContent);
|
|
1420
|
+
const projectName = deriveProjectName(rulesContent);
|
|
1421
|
+
const plan = buildSyncPlan(rootDir, sections, projectName);
|
|
1422
|
+
const firstSync = !fs4.existsSync(path4.join(rootDir, SYNCED_MARKER_REL));
|
|
1423
|
+
const unmapped = findUnmappedSections(sections);
|
|
1424
|
+
if (opts.dryRun) {
|
|
1425
|
+
return {
|
|
1426
|
+
dryRun: true,
|
|
1427
|
+
firstSync,
|
|
1428
|
+
backupId: null,
|
|
1429
|
+
backedUp: [],
|
|
1430
|
+
written: [],
|
|
1431
|
+
skipped: [],
|
|
1432
|
+
truncated: [],
|
|
1433
|
+
plan,
|
|
1434
|
+
unmapped
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
const toBackup = plan.filter((p) => p.exists && (p.drift || firstSync)).map((p) => p.path);
|
|
1438
|
+
let backupId = null;
|
|
1439
|
+
let backedUp = [];
|
|
1440
|
+
if (toBackup.length) {
|
|
1441
|
+
const info = saveBackup(toBackup, rootDir);
|
|
1442
|
+
pruneBackups(BACKUP_KEEP, rootDir);
|
|
1443
|
+
backupId = info.id;
|
|
1444
|
+
backedUp = info.files;
|
|
1445
|
+
}
|
|
1446
|
+
const drifted = plan.filter((p) => p.drift);
|
|
1447
|
+
const overwriteDrift = drifted.length ? await confirmOverwrite(drifted) : true;
|
|
1448
|
+
const written = [];
|
|
1449
|
+
const skipped = [];
|
|
1450
|
+
const truncated = [];
|
|
1451
|
+
for (const item of plan) {
|
|
1452
|
+
if (item.drift && !overwriteDrift) {
|
|
1453
|
+
skipped.push(item.path);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
const fullPath = path4.join(rootDir, item.path);
|
|
1457
|
+
fs4.mkdirSync(path4.dirname(fullPath), { recursive: true });
|
|
1458
|
+
fs4.writeFileSync(fullPath, item.newContent, "utf-8");
|
|
1459
|
+
written.push(item.path);
|
|
1460
|
+
if (item.newContent.includes("Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC808\uC0AD\uB428")) {
|
|
1461
|
+
truncated.push(item.path);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
fs4.mkdirSync(path4.join(rootDir, ".vhk"), { recursive: true });
|
|
1465
|
+
fs4.writeFileSync(path4.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
1466
|
+
ensureVhkIgnored(rootDir, ".synced");
|
|
1467
|
+
return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped };
|
|
1468
|
+
}
|
|
1469
|
+
async function sync(opts = {}) {
|
|
1470
|
+
console.log(chalk2.bold(`
|
|
1471
|
+
${ko.sync.title}
|
|
1472
|
+
`));
|
|
1473
|
+
const cwd = process.cwd();
|
|
1474
|
+
const rulesPath = path4.join(cwd, "RULES.md");
|
|
1475
|
+
if (!fs4.existsSync(rulesPath)) {
|
|
1476
|
+
console.log(chalk2.yellow(ko.sync.noRules));
|
|
1477
|
+
console.log(chalk2.dim(" RULES.md\uB294 \uD504\uB85C\uC81D\uD2B8 \uADDC\uCE59\uC758 Single Source of Truth\uC785\uB2C8\uB2E4."));
|
|
1478
|
+
console.log(chalk2.dim(" \uC0DD\uC131\uD558\uB824\uBA74: vhk init \uC2E4\uD589 \uD6C4 RULES.md\uB97C \uC791\uC131\uD558\uC138\uC694."));
|
|
1479
|
+
console.log("");
|
|
1480
|
+
console.log(chalk2.dim(" RULES.md \uAE30\uBCF8 \uAD6C\uC870:"));
|
|
1481
|
+
console.log(chalk2.dim(" ## \uD504\uB85C\uC81D\uD2B8 \uC815\uCCB4\uC131"));
|
|
1482
|
+
console.log(chalk2.dim(" ## \uAE30\uC220 \uC2A4\uD0DD"));
|
|
1483
|
+
console.log(chalk2.dim(" ## \uCF54\uB529 \uADDC\uCE59"));
|
|
1484
|
+
console.log(chalk2.dim(" ## \uAE30\uB85D \uADDC\uCE59"));
|
|
1485
|
+
console.log(chalk2.dim(" ## \uCEE4\uBC0B \uCEE8\uBCA4\uC158"));
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const sections = parseRulesMd(fs4.readFileSync(rulesPath, "utf-8"));
|
|
1489
|
+
console.log(chalk2.dim(` \u{1F4C4} RULES.md \uD30C\uC2F1 \uC644\uB8CC \u2014 ${sections.length}\uAC1C \uC139\uC158`));
|
|
1490
|
+
const interactive = !!process.stdout.isTTY && !opts.yes;
|
|
1491
|
+
const confirmOverwrite = async (drifted) => {
|
|
1492
|
+
if (!interactive) return true;
|
|
1493
|
+
for (const d of drifted) console.log(chalk2.yellow(` ${ko.sync.driftWarn(d.path)}`));
|
|
1494
|
+
const { confirm } = await inquirer.prompt([
|
|
1495
|
+
{
|
|
1496
|
+
type: "confirm",
|
|
1497
|
+
name: "confirm",
|
|
1498
|
+
message: ko.sync.driftConfirm(drifted.length),
|
|
1499
|
+
default: false
|
|
1500
|
+
}
|
|
1501
|
+
]);
|
|
1502
|
+
return confirm;
|
|
1503
|
+
};
|
|
1504
|
+
const result = await syncCore(cwd, opts, confirmOverwrite);
|
|
1505
|
+
if (result.unmapped.length) {
|
|
1506
|
+
console.error(
|
|
1507
|
+
chalk2.yellow(
|
|
1508
|
+
` \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(", ")}
|
|
1509
|
+
(\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.)`
|
|
1510
|
+
)
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
if (result.dryRun) {
|
|
1514
|
+
console.log(chalk2.cyan(`
|
|
1515
|
+
${ko.sync.dryRunHeader}`));
|
|
1516
|
+
for (const item of result.plan) {
|
|
1517
|
+
console.log(ko.sync.dryRunWouldWrite(item.path, item.exists && item.drift));
|
|
1518
|
+
}
|
|
1519
|
+
const wouldBackup = result.plan.filter((p) => p.exists && (p.drift || result.firstSync)).map((p) => p.path);
|
|
1520
|
+
if (wouldBackup.length) {
|
|
1521
|
+
console.log(chalk2.dim(`
|
|
1522
|
+
\uBC31\uC5C5 \uC608\uC815(${wouldBackup.length}): ${wouldBackup.join(", ")}`));
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (result.backupId) {
|
|
1527
|
+
if (result.firstSync) console.log(chalk2.cyan(` ${ko.sync.firstSync}`));
|
|
1528
|
+
if (!process.stdout.isTTY) {
|
|
1529
|
+
console.log(chalk2.yellow(` ${ko.sync.nonTtyAuto(result.backedUp.length, result.backupId)}`));
|
|
1530
|
+
} else {
|
|
1531
|
+
console.log(chalk2.cyan(` ${ko.sync.backupSaved(result.backedUp.length, result.backupId)}`));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const p of result.written) {
|
|
1535
|
+
const item = result.plan.find((i) => i.path === p);
|
|
1536
|
+
if (item) console.log(chalk2.green(` ${item.doneMessage}`));
|
|
1537
|
+
}
|
|
1538
|
+
for (const _ of result.truncated) {
|
|
1539
|
+
console.log(chalk2.yellow(` \u26A0\uFE0F ${ko.sync.antigravityTruncated}`));
|
|
1540
|
+
}
|
|
1541
|
+
for (const p of result.skipped) {
|
|
1542
|
+
console.log(chalk2.gray(` ${ko.sync.skipped(p)}`));
|
|
1543
|
+
}
|
|
1544
|
+
console.log(chalk2.bold.green(`
|
|
1545
|
+
${ko.sync.done}`));
|
|
1546
|
+
console.log(chalk2.dim(" RULES.md (\uC6D0\uBCF8) \u2192 .cursorrules + CLAUDE.md + .windsurfrules"));
|
|
1547
|
+
console.log(chalk2.dim(" + .github/copilot-instructions.md + .agents/rules/vhk-rules.md (\uC790\uB3D9 \uC0DD\uC131)"));
|
|
1548
|
+
console.log(chalk2.dim(" \uADDC\uCE59 \uBCC0\uACBD\uC740 \uD56D\uC0C1 RULES.md\uC5D0\uC11C\uB9CC \uD558\uC138\uC694."));
|
|
1549
|
+
printNextStep({
|
|
1550
|
+
message: "\uADDC\uCE59 \uB3D9\uAE30\uD654 \uC644\uB8CC! \uC774\uC81C Cursor\uAC00 \uC0C8 \uADDC\uCE59\uC744 \uB530\uB985\uB2C8\uB2E4.",
|
|
1551
|
+
command: "vhk \uC810\uAC80",
|
|
1552
|
+
cursorHint: "\uADDC\uCE59 \uC810\uAC80\uD574\uC918"
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/commands/deploy.ts
|
|
1557
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1558
|
+
import chalk3 from "chalk";
|
|
1559
|
+
import inquirer2 from "inquirer";
|
|
1560
|
+
|
|
1561
|
+
// src/lib/exec.ts
|
|
1562
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1563
|
+
var SHIM_BINARIES = /* @__PURE__ */ new Set(["pnpm", "npm", "npx", "yarn"]);
|
|
1564
|
+
function platformCmd(cmd) {
|
|
1565
|
+
if (process.platform === "win32" && SHIM_BINARIES.has(cmd)) {
|
|
1566
|
+
return `${cmd}.cmd`;
|
|
1567
|
+
}
|
|
1568
|
+
return cmd;
|
|
1569
|
+
}
|
|
1570
|
+
function resolveCmd(cmd, args) {
|
|
1571
|
+
if (process.platform === "win32" && SHIM_BINARIES.has(cmd)) {
|
|
1572
|
+
return { bin: "cmd.exe", argv: ["/d", "/s", "/c", `${cmd}.cmd`, ...args] };
|
|
1573
|
+
}
|
|
1574
|
+
return { bin: platformCmd(cmd), argv: args };
|
|
1575
|
+
}
|
|
1576
|
+
var DEFAULT_EXEC_TIMEOUT_MS = 6e5;
|
|
1577
|
+
var NETWORK_EXEC_TIMEOUT_MS = 3e4;
|
|
1578
|
+
function resolveTimeout(timeoutMs, fallback) {
|
|
1579
|
+
const v = timeoutMs === void 0 ? fallback : timeoutMs;
|
|
1580
|
+
return v > 0 ? v : void 0;
|
|
1581
|
+
}
|
|
1582
|
+
function isTimeoutError(e, timeout) {
|
|
1583
|
+
if (!timeout) return false;
|
|
1584
|
+
return e.code === "ETIMEDOUT";
|
|
1585
|
+
}
|
|
1586
|
+
function safeExecFile(cmd, args, opts = {}) {
|
|
1587
|
+
const { bin, argv } = resolveCmd(cmd, args);
|
|
1588
|
+
const env2 = opts.env ? { ...process.env, ...opts.env } : void 0;
|
|
1589
|
+
const timeout = resolveTimeout(opts.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
|
|
1590
|
+
try {
|
|
1591
|
+
const out = execFileSync2(bin, argv, {
|
|
1592
|
+
encoding: "utf-8",
|
|
1593
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1594
|
+
env: env2,
|
|
1595
|
+
...timeout ? { timeout, killSignal: "SIGTERM" } : {}
|
|
1596
|
+
}).toString();
|
|
1597
|
+
return { ok: true, out: out.trim() };
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
const e = err;
|
|
1600
|
+
const stdout = e.stdout ? e.stdout.toString() : "";
|
|
1601
|
+
let msg = e.message ?? String(err);
|
|
1602
|
+
if (isTimeoutError(e, timeout)) {
|
|
1603
|
+
msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
|
|
1604
|
+
}
|
|
1605
|
+
return { ok: false, err: msg, out: stdout.trim() };
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function safeExecFileStream(cmd, args, opts = {}) {
|
|
1609
|
+
const { bin, argv } = resolveCmd(cmd, args);
|
|
1610
|
+
const timeout = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : void 0;
|
|
1611
|
+
try {
|
|
1612
|
+
execFileSync2(bin, argv, {
|
|
1613
|
+
encoding: "utf-8",
|
|
1614
|
+
stdio: "inherit",
|
|
1615
|
+
...timeout ? { timeout, killSignal: "SIGTERM" } : {}
|
|
1616
|
+
});
|
|
1617
|
+
return { ok: true };
|
|
1618
|
+
} catch (err) {
|
|
1619
|
+
const e = err;
|
|
1620
|
+
let msg = err instanceof Error ? err.message : String(err);
|
|
1621
|
+
if (isTimeoutError(e, timeout)) {
|
|
1622
|
+
msg = `\uBA85\uB839 \uC2DC\uAC04 \uCD08\uACFC (timeout ${timeout}ms): ${cmd} ${args.join(" ")}`.trim();
|
|
1623
|
+
}
|
|
1624
|
+
return { ok: false, err: msg };
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
992
1627
|
|
|
993
1628
|
// src/commands/deploy.ts
|
|
994
1629
|
var PLATFORMS = {
|
|
@@ -1020,7 +1655,7 @@ var PLATFORMS = {
|
|
|
1020
1655
|
function detectPlatform() {
|
|
1021
1656
|
for (const [key, config] of Object.entries(PLATFORMS)) {
|
|
1022
1657
|
for (const file of config.detectFiles) {
|
|
1023
|
-
if (
|
|
1658
|
+
if (existsSync2(file)) return key;
|
|
1024
1659
|
}
|
|
1025
1660
|
}
|
|
1026
1661
|
return null;
|
|
@@ -1029,14 +1664,14 @@ function isCLIAvailable(cmd, checkArgs) {
|
|
|
1029
1664
|
return safeExecFile(cmd, checkArgs).ok;
|
|
1030
1665
|
}
|
|
1031
1666
|
async function deploy() {
|
|
1032
|
-
console.log(
|
|
1033
|
-
console.log(
|
|
1667
|
+
console.log(chalk3.bold("\n\u{1F680} " + t("deploy.title")));
|
|
1668
|
+
console.log(chalk3.gray("\u2500".repeat(40)));
|
|
1034
1669
|
let platform = detectPlatform();
|
|
1035
1670
|
if (platform) {
|
|
1036
|
-
console.log(
|
|
1671
|
+
console.log(chalk3.cyan(`
|
|
1037
1672
|
\u{1F50D} \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${PLATFORMS[platform].name}`));
|
|
1038
1673
|
} else {
|
|
1039
|
-
const { selected } = await
|
|
1674
|
+
const { selected } = await inquirer2.prompt([
|
|
1040
1675
|
{
|
|
1041
1676
|
type: "list",
|
|
1042
1677
|
name: "selected",
|
|
@@ -1052,12 +1687,12 @@ async function deploy() {
|
|
|
1052
1687
|
}
|
|
1053
1688
|
const config = PLATFORMS[platform];
|
|
1054
1689
|
if (!isCLIAvailable(config.command, config.checkArgs)) {
|
|
1055
|
-
console.log(
|
|
1690
|
+
console.log(chalk3.red(`
|
|
1056
1691
|
\u274C ${config.name} CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
1057
|
-
console.log(
|
|
1692
|
+
console.log(chalk3.yellow(` \u2192 ${config.installHint}`));
|
|
1058
1693
|
return;
|
|
1059
1694
|
}
|
|
1060
|
-
const { confirm } = await
|
|
1695
|
+
const { confirm } = await inquirer2.prompt([
|
|
1061
1696
|
{
|
|
1062
1697
|
type: "confirm",
|
|
1063
1698
|
name: "confirm",
|
|
@@ -1066,15 +1701,15 @@ async function deploy() {
|
|
|
1066
1701
|
}
|
|
1067
1702
|
]);
|
|
1068
1703
|
if (!confirm) {
|
|
1069
|
-
console.log(
|
|
1704
|
+
console.log(chalk3.gray("\uCDE8\uC18C\uB428"));
|
|
1070
1705
|
return;
|
|
1071
1706
|
}
|
|
1072
|
-
console.log(
|
|
1707
|
+
console.log(chalk3.cyan(`
|
|
1073
1708
|
${t("deploy.deploying")}
|
|
1074
1709
|
`));
|
|
1075
1710
|
const result = safeExecFileStream(config.command, config.commandArgs);
|
|
1076
1711
|
if (result.ok) {
|
|
1077
|
-
console.log(
|
|
1712
|
+
console.log(chalk3.green(`
|
|
1078
1713
|
\u2705 ${t("deploy.success")}`));
|
|
1079
1714
|
printNextStep({
|
|
1080
1715
|
message: "\uBC30\uD3EC \uC644\uB8CC! \uC0AC\uC774\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
@@ -1082,16 +1717,16 @@ ${t("deploy.deploying")}
|
|
|
1082
1717
|
cursorHint: "\uC0C1\uD0DC \uD655\uC778\uD574\uC918"
|
|
1083
1718
|
});
|
|
1084
1719
|
} else {
|
|
1085
|
-
console.log(
|
|
1720
|
+
console.log(chalk3.red(`
|
|
1086
1721
|
\u274C ${t("deploy.failed")}`));
|
|
1087
|
-
console.log(
|
|
1722
|
+
console.log(chalk3.red(result.err));
|
|
1088
1723
|
}
|
|
1089
1724
|
}
|
|
1090
1725
|
|
|
1091
1726
|
// src/commands/env.ts
|
|
1092
|
-
import { existsSync as
|
|
1093
|
-
import { join } from "path";
|
|
1094
|
-
import
|
|
1727
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, appendFileSync, readdirSync } from "fs";
|
|
1728
|
+
import { join as join2 } from "path";
|
|
1729
|
+
import chalk4 from "chalk";
|
|
1095
1730
|
function parseEnvKeys(content) {
|
|
1096
1731
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
|
|
1097
1732
|
}
|
|
@@ -1107,7 +1742,7 @@ function loadDefinedEnvKeys(dir = ".") {
|
|
|
1107
1742
|
if (!name.startsWith(".env")) continue;
|
|
1108
1743
|
if (name.endsWith(".example") || name.endsWith(".sample")) continue;
|
|
1109
1744
|
try {
|
|
1110
|
-
for (const k of parseEnvKeys(
|
|
1745
|
+
for (const k of parseEnvKeys(readFileSync2(join2(dir, name), "utf-8"))) keys.add(k);
|
|
1111
1746
|
} catch {
|
|
1112
1747
|
}
|
|
1113
1748
|
}
|
|
@@ -1115,36 +1750,36 @@ function loadDefinedEnvKeys(dir = ".") {
|
|
|
1115
1750
|
}
|
|
1116
1751
|
function ensureGitignore() {
|
|
1117
1752
|
const gitignorePath = ".gitignore";
|
|
1118
|
-
if (
|
|
1119
|
-
const content =
|
|
1753
|
+
if (existsSync3(gitignorePath)) {
|
|
1754
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1120
1755
|
if (!content.split("\n").some((l) => l.trim() === ".env")) {
|
|
1121
1756
|
appendFileSync(gitignorePath, "\n.env\n");
|
|
1122
|
-
console.log(
|
|
1757
|
+
console.log(chalk4.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
|
|
1123
1758
|
}
|
|
1124
1759
|
} else {
|
|
1125
1760
|
writeFileSync(gitignorePath, ".env\nnode_modules/\ndist/\n");
|
|
1126
|
-
console.log(
|
|
1761
|
+
console.log(chalk4.green("\n\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)"));
|
|
1127
1762
|
}
|
|
1128
1763
|
}
|
|
1129
1764
|
async function env() {
|
|
1130
|
-
console.log(
|
|
1131
|
-
console.log(
|
|
1132
|
-
if (!
|
|
1133
|
-
console.log(
|
|
1134
|
-
console.log(
|
|
1765
|
+
console.log(chalk4.bold("\n\u{1F510} " + t("env.title")));
|
|
1766
|
+
console.log(chalk4.gray("\u2500".repeat(40)));
|
|
1767
|
+
if (!existsSync3(".env")) {
|
|
1768
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1769
|
+
console.log(chalk4.gray(" .env \uD30C\uC77C\uC744 \uBA3C\uC800 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694."));
|
|
1135
1770
|
return;
|
|
1136
1771
|
}
|
|
1137
|
-
const envContent =
|
|
1772
|
+
const envContent = readFileSync2(".env", "utf-8");
|
|
1138
1773
|
const keys = parseEnvKeys(envContent);
|
|
1139
1774
|
if (keys.length === 0) {
|
|
1140
|
-
console.log(
|
|
1775
|
+
console.log(chalk4.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1141
1776
|
return;
|
|
1142
1777
|
}
|
|
1143
1778
|
const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
|
|
1144
1779
|
writeFileSync(".env.example", exampleContent, "utf-8");
|
|
1145
|
-
console.log(
|
|
1780
|
+
console.log(chalk4.green(`
|
|
1146
1781
|
\u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`));
|
|
1147
|
-
keys.forEach((k) => console.log(
|
|
1782
|
+
keys.forEach((k) => console.log(chalk4.gray(` ${k}`)));
|
|
1148
1783
|
ensureGitignore();
|
|
1149
1784
|
printNextStep({
|
|
1150
1785
|
message: ".env.example \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -1153,47 +1788,47 @@ async function env() {
|
|
|
1153
1788
|
});
|
|
1154
1789
|
}
|
|
1155
1790
|
async function envCheck() {
|
|
1156
|
-
console.log(
|
|
1157
|
-
console.log(
|
|
1158
|
-
if (!
|
|
1159
|
-
console.log(
|
|
1791
|
+
console.log(chalk4.bold("\n\u{1F50D} " + t("env.checkTitle")));
|
|
1792
|
+
console.log(chalk4.gray("\u2500".repeat(40)));
|
|
1793
|
+
if (!existsSync3(".env.example")) {
|
|
1794
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 vhk env\uB97C \uC2E4\uD589\uD558\uC138\uC694."));
|
|
1160
1795
|
return;
|
|
1161
1796
|
}
|
|
1162
|
-
const requiredKeys = parseEnvKeys(
|
|
1797
|
+
const requiredKeys = parseEnvKeys(readFileSync2(".env.example", "utf-8"));
|
|
1163
1798
|
const currentKeys = loadDefinedEnvKeys();
|
|
1164
1799
|
const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
|
|
1165
1800
|
const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
|
|
1166
|
-
console.log(
|
|
1801
|
+
console.log(chalk4.cyan(`
|
|
1167
1802
|
\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`));
|
|
1168
1803
|
if (missing.length === 0) {
|
|
1169
|
-
console.log(
|
|
1804
|
+
console.log(chalk4.green("\n\u2705 \uBAA8\uB4E0 \uD544\uC218 \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4!"));
|
|
1170
1805
|
} else {
|
|
1171
|
-
console.log(
|
|
1806
|
+
console.log(chalk4.red(`
|
|
1172
1807
|
\u274C \uB204\uB77D\uB41C \uD658\uACBD\uBCC0\uC218 (${missing.length}\uAC1C):`));
|
|
1173
|
-
missing.forEach((k) => console.log(
|
|
1808
|
+
missing.forEach((k) => console.log(chalk4.red(` \u2022 ${k}`)));
|
|
1174
1809
|
process.exitCode = 1;
|
|
1175
1810
|
}
|
|
1176
1811
|
if (extra.length > 0) {
|
|
1177
|
-
console.log(
|
|
1812
|
+
console.log(chalk4.yellow(`
|
|
1178
1813
|
\u{1F4A1} .env.example\uC5D0 \uC5C6\uB294 \uCD94\uAC00 \uBCC0\uC218 (${extra.length}\uAC1C):`));
|
|
1179
|
-
extra.forEach((k) => console.log(
|
|
1814
|
+
extra.forEach((k) => console.log(chalk4.yellow(` \u2022 ${k}`)));
|
|
1180
1815
|
}
|
|
1181
1816
|
ensureGitignore();
|
|
1182
1817
|
}
|
|
1183
1818
|
|
|
1184
1819
|
// src/commands/publish.ts
|
|
1185
|
-
import { existsSync as
|
|
1186
|
-
import
|
|
1187
|
-
import
|
|
1820
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1821
|
+
import chalk5 from "chalk";
|
|
1822
|
+
import inquirer3 from "inquirer";
|
|
1188
1823
|
import ora from "ora";
|
|
1189
1824
|
|
|
1190
1825
|
// src/lib/read-json.ts
|
|
1191
|
-
import { readFileSync as
|
|
1826
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1192
1827
|
function stripBom(text) {
|
|
1193
1828
|
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1194
1829
|
}
|
|
1195
1830
|
function readJsonFile(filePath) {
|
|
1196
|
-
const raw = stripBom(
|
|
1831
|
+
const raw = stripBom(readFileSync3(filePath, "utf-8"));
|
|
1197
1832
|
return JSON.parse(raw);
|
|
1198
1833
|
}
|
|
1199
1834
|
|
|
@@ -1252,23 +1887,23 @@ function gitPostRelease(newVersion) {
|
|
|
1252
1887
|
};
|
|
1253
1888
|
}
|
|
1254
1889
|
async function publish() {
|
|
1255
|
-
console.log(
|
|
1256
|
-
console.log(
|
|
1257
|
-
if (!
|
|
1258
|
-
console.log(
|
|
1890
|
+
console.log(chalk5.bold("\n\u{1F4E6} " + t("publish.title")));
|
|
1891
|
+
console.log(chalk5.gray("\u2500".repeat(40)));
|
|
1892
|
+
if (!existsSync4("package.json")) {
|
|
1893
|
+
console.log(chalk5.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1259
1894
|
return;
|
|
1260
1895
|
}
|
|
1261
1896
|
let pkg;
|
|
1262
1897
|
try {
|
|
1263
1898
|
pkg = readJsonFile("package.json");
|
|
1264
1899
|
} catch {
|
|
1265
|
-
console.log(
|
|
1900
|
+
console.log(chalk5.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
|
|
1266
1901
|
return;
|
|
1267
1902
|
}
|
|
1268
1903
|
const currentVersion = pkg.version || "0.0.0";
|
|
1269
|
-
console.log(
|
|
1904
|
+
console.log(chalk5.cyan(`
|
|
1270
1905
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
|
|
1271
|
-
const { bumpType } = await
|
|
1906
|
+
const { bumpType } = await inquirer3.prompt([
|
|
1272
1907
|
{
|
|
1273
1908
|
type: "list",
|
|
1274
1909
|
name: "bumpType",
|
|
@@ -1281,16 +1916,16 @@ async function publish() {
|
|
|
1281
1916
|
}
|
|
1282
1917
|
]);
|
|
1283
1918
|
const newVersion = bumpVersion(currentVersion, bumpType);
|
|
1284
|
-
console.log(
|
|
1919
|
+
console.log(chalk5.cyan(`
|
|
1285
1920
|
\u{1F195} \uC0C8 \uBC84\uC804: v${newVersion}`));
|
|
1286
1921
|
pkg.version = newVersion;
|
|
1287
1922
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1288
|
-
console.log(
|
|
1923
|
+
console.log(chalk5.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
|
|
1289
1924
|
const buildSpinner = ora(t("publish.building")).start();
|
|
1290
1925
|
const buildResult = safeExecFile("pnpm", ["build"]);
|
|
1291
1926
|
if (!buildResult.ok) {
|
|
1292
1927
|
buildSpinner.fail(t("publish.buildFailed"));
|
|
1293
|
-
console.log(
|
|
1928
|
+
console.log(chalk5.red(buildResult.err.slice(0, 500)));
|
|
1294
1929
|
pkg.version = currentVersion;
|
|
1295
1930
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1296
1931
|
return;
|
|
@@ -1300,13 +1935,13 @@ async function publish() {
|
|
|
1300
1935
|
const testResult = safeExecFile("pnpm", ["test", "--run"]);
|
|
1301
1936
|
if (!testResult.ok) {
|
|
1302
1937
|
testSpinner.fail(t("publish.testFailed"));
|
|
1303
|
-
console.log(
|
|
1938
|
+
console.log(chalk5.red(testResult.err.slice(0, 500)));
|
|
1304
1939
|
pkg.version = currentVersion;
|
|
1305
1940
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1306
1941
|
return;
|
|
1307
1942
|
}
|
|
1308
1943
|
testSpinner.succeed(t("publish.testSuccess"));
|
|
1309
|
-
const { confirm } = await
|
|
1944
|
+
const { confirm } = await inquirer3.prompt([
|
|
1310
1945
|
{
|
|
1311
1946
|
type: "confirm",
|
|
1312
1947
|
name: "confirm",
|
|
@@ -1317,37 +1952,37 @@ async function publish() {
|
|
|
1317
1952
|
if (!confirm) {
|
|
1318
1953
|
pkg.version = currentVersion;
|
|
1319
1954
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1320
|
-
console.log(
|
|
1955
|
+
console.log(chalk5.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
|
|
1321
1956
|
return;
|
|
1322
1957
|
}
|
|
1323
|
-
console.log(
|
|
1958
|
+
console.log(chalk5.cyan(`
|
|
1324
1959
|
\u{1F4E4} ${t("publish.publishing")}`));
|
|
1325
|
-
console.log(
|
|
1960
|
+
console.log(chalk5.gray(" 2FA \uD65C\uC131\uD654 \uC2DC: OTP 6\uC790\uB9AC \uC785\uB825 \uB610\uB294 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC99D URL \uD074\uB9AD (Windows Hello / PIN \uC9C0\uC6D0)"));
|
|
1326
1961
|
const pubResult = safeExecFileStream("npm", ["publish", "--access", "public"]);
|
|
1327
1962
|
if (!pubResult.ok) {
|
|
1328
|
-
console.log(
|
|
1963
|
+
console.log(chalk5.red(`
|
|
1329
1964
|
\u2716 ${t("publish.publishFailed")}`));
|
|
1330
|
-
console.log(
|
|
1965
|
+
console.log(chalk5.red(pubResult.err.slice(0, 500)));
|
|
1331
1966
|
pkg.version = currentVersion;
|
|
1332
1967
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1333
|
-
console.log(
|
|
1968
|
+
console.log(chalk5.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
1334
1969
|
return;
|
|
1335
1970
|
}
|
|
1336
|
-
console.log(
|
|
1971
|
+
console.log(chalk5.green(`
|
|
1337
1972
|
\u2714 ${t("publish.publishSuccess")}`));
|
|
1338
1973
|
const git = gitPostRelease(newVersion);
|
|
1339
1974
|
if (git.warning) {
|
|
1340
|
-
console.log(
|
|
1975
|
+
console.log(chalk5.yellow(`
|
|
1341
1976
|
\u26A0\uFE0F ${git.warning}`));
|
|
1342
|
-
console.log(
|
|
1977
|
+
console.log(chalk5.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
|
|
1343
1978
|
} else if (git.tagged && git.pushed) {
|
|
1344
|
-
console.log(
|
|
1979
|
+
console.log(chalk5.green(`
|
|
1345
1980
|
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
|
|
1346
1981
|
} else if (git.tagged) {
|
|
1347
|
-
console.log(
|
|
1982
|
+
console.log(chalk5.yellow(`
|
|
1348
1983
|
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
|
|
1349
1984
|
}
|
|
1350
|
-
console.log(
|
|
1985
|
+
console.log(chalk5.green.bold(`
|
|
1351
1986
|
\u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
|
|
1352
1987
|
printNextStep({
|
|
1353
1988
|
message: "npm \uBC30\uD3EC \uC644\uB8CC!",
|
|
@@ -1357,13 +1992,13 @@ async function publish() {
|
|
|
1357
1992
|
}
|
|
1358
1993
|
|
|
1359
1994
|
// src/commands/audit.ts
|
|
1360
|
-
import { existsSync as
|
|
1361
|
-
import
|
|
1362
|
-
import
|
|
1995
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1996
|
+
import chalk6 from "chalk";
|
|
1997
|
+
import inquirer4 from "inquirer";
|
|
1363
1998
|
import ora2 from "ora";
|
|
1364
1999
|
function detectCurrentPM() {
|
|
1365
|
-
if (
|
|
1366
|
-
if (
|
|
2000
|
+
if (existsSync5("pnpm-lock.yaml")) return "pnpm";
|
|
2001
|
+
if (existsSync5("yarn.lock")) return "yarn";
|
|
1367
2002
|
return "npm";
|
|
1368
2003
|
}
|
|
1369
2004
|
function parseAuditOutput(output, pm) {
|
|
@@ -1403,26 +2038,26 @@ function runAuditFix(pm) {
|
|
|
1403
2038
|
return result.ok ? { ok: true } : { ok: false, err: result.err };
|
|
1404
2039
|
}
|
|
1405
2040
|
async function audit(autoFix = false) {
|
|
1406
|
-
console.log(
|
|
1407
|
-
console.log(
|
|
2041
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
|
|
2042
|
+
console.log(chalk6.gray("\u2500".repeat(40)));
|
|
1408
2043
|
const pm = detectCurrentPM();
|
|
1409
|
-
console.log(
|
|
2044
|
+
console.log(chalk6.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
|
|
1410
2045
|
const spinner = ora2("\uBCF4\uC548 \uAC10\uC0AC \uC2E4\uD589 \uC911...").start();
|
|
1411
2046
|
const output = runAuditJson(pm);
|
|
1412
2047
|
spinner.stop();
|
|
1413
2048
|
const summary = parseAuditOutput(output, pm);
|
|
1414
2049
|
if (summary.total === 0) {
|
|
1415
|
-
console.log(
|
|
2050
|
+
console.log(chalk6.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
|
|
1416
2051
|
return;
|
|
1417
2052
|
}
|
|
1418
|
-
console.log(
|
|
1419
|
-
if (summary.critical > 0) console.log(
|
|
1420
|
-
if (summary.high > 0) console.log(
|
|
1421
|
-
if (summary.moderate > 0) console.log(
|
|
1422
|
-
if (summary.low > 0) console.log(
|
|
1423
|
-
console.log(
|
|
2053
|
+
console.log(chalk6.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
|
|
2054
|
+
if (summary.critical > 0) console.log(chalk6.red(` \u{1F534} Critical: ${summary.critical}`));
|
|
2055
|
+
if (summary.high > 0) console.log(chalk6.red(` \u{1F7E0} High: ${summary.high}`));
|
|
2056
|
+
if (summary.moderate > 0) console.log(chalk6.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
|
|
2057
|
+
if (summary.low > 0) console.log(chalk6.gray(` \u26AA Low: ${summary.low}`));
|
|
2058
|
+
console.log(chalk6.bold(`
|
|
1424
2059
|
\uCD1D ${summary.total}\uAC1C\uC758 \uCDE8\uC57D\uC810`));
|
|
1425
|
-
const shouldRunFix = autoFix ? true : summary.critical > 0 || summary.high > 0 ? (await
|
|
2060
|
+
const shouldRunFix = autoFix ? true : summary.critical > 0 || summary.high > 0 ? (await inquirer4.prompt([
|
|
1426
2061
|
{
|
|
1427
2062
|
type: "confirm",
|
|
1428
2063
|
name: "shouldFix",
|
|
@@ -1447,17 +2082,17 @@ async function audit(autoFix = false) {
|
|
|
1447
2082
|
}
|
|
1448
2083
|
|
|
1449
2084
|
// src/lib/version.ts
|
|
1450
|
-
import { existsSync as
|
|
1451
|
-
import { dirname, join as
|
|
2085
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2086
|
+
import { dirname, join as join3 } from "path";
|
|
1452
2087
|
import { fileURLToPath } from "url";
|
|
1453
2088
|
function getVhkVersion() {
|
|
1454
2089
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
1455
2090
|
for (const pkgPath of [
|
|
1456
|
-
|
|
1457
|
-
|
|
2091
|
+
join3(dir, "../../package.json"),
|
|
2092
|
+
join3(dir, "../package.json")
|
|
1458
2093
|
]) {
|
|
1459
2094
|
try {
|
|
1460
|
-
if (
|
|
2095
|
+
if (existsSync6(pkgPath)) {
|
|
1461
2096
|
const pkg = readJsonFile(pkgPath);
|
|
1462
2097
|
if (pkg.version) return pkg.version;
|
|
1463
2098
|
}
|
|
@@ -1469,7 +2104,7 @@ function getVhkVersion() {
|
|
|
1469
2104
|
}
|
|
1470
2105
|
|
|
1471
2106
|
// src/mcp/cli-path.ts
|
|
1472
|
-
import { existsSync as
|
|
2107
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1473
2108
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1474
2109
|
import { dirname as dirname2, resolve } from "path";
|
|
1475
2110
|
function pickCliInvocation(globalAvailable, localCli, localExists) {
|
|
@@ -1487,17 +2122,17 @@ function localCliPath() {
|
|
|
1487
2122
|
function resolveVhkCliInvocation() {
|
|
1488
2123
|
const globalAvailable = safeExecFile("vhk", ["--version"]).ok;
|
|
1489
2124
|
const local = localCliPath();
|
|
1490
|
-
return pickCliInvocation(globalAvailable, local,
|
|
2125
|
+
return pickCliInvocation(globalAvailable, local, existsSync7(local));
|
|
1491
2126
|
}
|
|
1492
2127
|
|
|
1493
2128
|
// src/mcp/server.ts
|
|
1494
2129
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1495
2130
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1496
2131
|
import { z } from "zod";
|
|
1497
|
-
import { existsSync as
|
|
2132
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2 } from "fs";
|
|
1498
2133
|
|
|
1499
2134
|
// src/lib/scan-secrets.ts
|
|
1500
|
-
import
|
|
2135
|
+
import fs7 from "fs";
|
|
1501
2136
|
|
|
1502
2137
|
// src/lib/secret-patterns.ts
|
|
1503
2138
|
var SECRET_PATTERNS = [
|
|
@@ -1563,19 +2198,19 @@ function maskSecret(value) {
|
|
|
1563
2198
|
}
|
|
1564
2199
|
|
|
1565
2200
|
// src/lib/scan-files.ts
|
|
1566
|
-
import
|
|
1567
|
-
import
|
|
2201
|
+
import fs6 from "fs";
|
|
2202
|
+
import path6 from "path";
|
|
1568
2203
|
|
|
1569
2204
|
// src/lib/check-secure.ts
|
|
1570
2205
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
1571
|
-
import
|
|
1572
|
-
import
|
|
1573
|
-
import
|
|
2206
|
+
import fs5 from "fs";
|
|
2207
|
+
import path5 from "path";
|
|
2208
|
+
import chalk7 from "chalk";
|
|
1574
2209
|
function loadGitignore(rootDir) {
|
|
1575
2210
|
const ig = (0, import_ignore.default)();
|
|
1576
|
-
const gitignorePath =
|
|
1577
|
-
if (
|
|
1578
|
-
const content =
|
|
2211
|
+
const gitignorePath = path5.join(rootDir, ".gitignore");
|
|
2212
|
+
if (fs5.existsSync(gitignorePath)) {
|
|
2213
|
+
const content = fs5.readFileSync(gitignorePath, "utf-8");
|
|
1579
2214
|
ig.add(content);
|
|
1580
2215
|
}
|
|
1581
2216
|
return ig;
|
|
@@ -1590,14 +2225,14 @@ function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDept
|
|
|
1590
2225
|
if (depth > maxDepth) return;
|
|
1591
2226
|
let entries;
|
|
1592
2227
|
try {
|
|
1593
|
-
entries =
|
|
2228
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1594
2229
|
} catch {
|
|
1595
2230
|
return;
|
|
1596
2231
|
}
|
|
1597
2232
|
for (const entry of entries) {
|
|
1598
2233
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1599
|
-
const fullPath =
|
|
1600
|
-
const rel =
|
|
2234
|
+
const fullPath = path5.join(dir, entry.name);
|
|
2235
|
+
const rel = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1601
2236
|
if (entry.isDirectory()) {
|
|
1602
2237
|
if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
|
|
1603
2238
|
continue;
|
|
@@ -1620,8 +2255,8 @@ function isSensitiveName(name) {
|
|
|
1620
2255
|
return false;
|
|
1621
2256
|
}
|
|
1622
2257
|
function checkProjectSecurity(rootDir = process.cwd()) {
|
|
1623
|
-
const gitignorePath =
|
|
1624
|
-
const missingGitignore = !
|
|
2258
|
+
const gitignorePath = path5.join(rootDir, ".gitignore");
|
|
2259
|
+
const missingGitignore = !fs5.existsSync(gitignorePath);
|
|
1625
2260
|
const ig = loadGitignore(rootDir);
|
|
1626
2261
|
const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
|
|
1627
2262
|
const warnings = [];
|
|
@@ -1644,7 +2279,7 @@ function printSecurityWarnings(rootDir = process.cwd()) {
|
|
|
1644
2279
|
const result = checkProjectSecurity(rootDir);
|
|
1645
2280
|
if (result.ok) return true;
|
|
1646
2281
|
for (const w of result.warnings) {
|
|
1647
|
-
console.log(
|
|
2282
|
+
console.log(chalk7.yellow(` \u26A0\uFE0F ${w}`));
|
|
1648
2283
|
}
|
|
1649
2284
|
return false;
|
|
1650
2285
|
}
|
|
@@ -1692,19 +2327,19 @@ var MAX_SCAN_FILE_BYTES = 512 * 1024;
|
|
|
1692
2327
|
function isScannableFileName(fileName) {
|
|
1693
2328
|
if (SKIP_FILE_NAMES.has(fileName)) return false;
|
|
1694
2329
|
if (fileName.startsWith(".env")) return true;
|
|
1695
|
-
return SCAN_EXTENSIONS.has(
|
|
2330
|
+
return SCAN_EXTENSIONS.has(path6.extname(fileName).toLowerCase());
|
|
1696
2331
|
}
|
|
1697
2332
|
function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
1698
2333
|
function walk(dir) {
|
|
1699
2334
|
let entries;
|
|
1700
2335
|
try {
|
|
1701
|
-
entries =
|
|
2336
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1702
2337
|
} catch {
|
|
1703
2338
|
return;
|
|
1704
2339
|
}
|
|
1705
2340
|
for (const entry of entries) {
|
|
1706
|
-
const fullPath =
|
|
1707
|
-
const rel =
|
|
2341
|
+
const fullPath = path6.join(dir, entry.name);
|
|
2342
|
+
const rel = path6.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1708
2343
|
if (entry.isDirectory()) {
|
|
1709
2344
|
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
1710
2345
|
if (isPathIgnored(ig, `${rel}/`)) continue;
|
|
@@ -1715,7 +2350,7 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
|
1715
2350
|
if (isPathIgnored(ig, rel)) continue;
|
|
1716
2351
|
let size = 0;
|
|
1717
2352
|
try {
|
|
1718
|
-
size =
|
|
2353
|
+
size = fs6.statSync(fullPath).size;
|
|
1719
2354
|
} catch {
|
|
1720
2355
|
continue;
|
|
1721
2356
|
}
|
|
@@ -1760,7 +2395,7 @@ function scanProjectForSecrets(cwd) {
|
|
|
1760
2395
|
let truncated = false;
|
|
1761
2396
|
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
1762
2397
|
scannedFiles++;
|
|
1763
|
-
const content =
|
|
2398
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1764
2399
|
const lines = content.split("\n");
|
|
1765
2400
|
lines.forEach((line, idx) => {
|
|
1766
2401
|
if (truncated) return;
|
|
@@ -1919,7 +2554,7 @@ ${last.out}
|
|
|
1919
2554
|
);
|
|
1920
2555
|
server.registerTool("status", { description: "\uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uB300\uC2DC\uBCF4\uB4DC (\uBE0C\uB79C\uCE58/\uBCC0\uACBD\uC0AC\uD56D/\uCD5C\uADFC \uCEE4\uBC0B)" }, async () => {
|
|
1921
2556
|
const lines = [];
|
|
1922
|
-
if (
|
|
2557
|
+
if (existsSync8("package.json")) {
|
|
1923
2558
|
try {
|
|
1924
2559
|
const pkg = readJsonFile("package.json");
|
|
1925
2560
|
lines.push(`\u{1F4E6} \uD504\uB85C\uC81D\uD2B8: ${pkg.name ?? "(\uC774\uB984 \uC5C6\uC74C)"} v${pkg.version ?? "?"}`);
|
|
@@ -2004,7 +2639,7 @@ ${last.out}
|
|
|
2004
2639
|
checks.push(build.ok ? "\u2705 \uBE4C\uB4DC \uC131\uACF5" : "\u274C \uBE4C\uB4DC \uC2E4\uD328");
|
|
2005
2640
|
const test = safeExecFile("pnpm", ["test", "--run"]);
|
|
2006
2641
|
checks.push(test.ok ? "\u2705 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC" : "\u274C \uD14C\uC2A4\uD2B8 \uC2E4\uD328");
|
|
2007
|
-
if (
|
|
2642
|
+
if (existsSync8("package.json")) {
|
|
2008
2643
|
try {
|
|
2009
2644
|
const pkg = readJsonFile("package.json");
|
|
2010
2645
|
checks.push(`\u{1F4E6} \uBC84\uC804: ${pkg.version}`);
|
|
@@ -2042,11 +2677,11 @@ ${last.out}
|
|
|
2042
2677
|
const recommended = ["CLAUDE.md", ".cursorrules", "docs/PRD.md", "docs/ARCHITECTURE.md"];
|
|
2043
2678
|
const lines = ["\u{1F50D} \uD504\uB85C\uC81D\uD2B8 \uC810\uAC80", "", "\uD544\uC218:"];
|
|
2044
2679
|
required.forEach((f) => {
|
|
2045
|
-
lines.push(` ${
|
|
2680
|
+
lines.push(` ${existsSync8(f) ? "\u2705" : "\u274C"} ${f}`);
|
|
2046
2681
|
});
|
|
2047
2682
|
lines.push("", "\uAD8C\uC7A5 (VHK \uD558\uB124\uC2A4):");
|
|
2048
2683
|
recommended.forEach((f) => {
|
|
2049
|
-
lines.push(` ${
|
|
2684
|
+
lines.push(` ${existsSync8(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
|
|
2050
2685
|
});
|
|
2051
2686
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2052
2687
|
});
|
|
@@ -2080,18 +2715,18 @@ ${log.out}` }] };
|
|
|
2080
2715
|
description: ".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore\uC5D0 .env \uC790\uB3D9 \uCD94\uAC00"
|
|
2081
2716
|
},
|
|
2082
2717
|
async () => {
|
|
2083
|
-
if (!
|
|
2718
|
+
if (!existsSync8(".env")) {
|
|
2084
2719
|
return { content: [{ type: "text", text: "\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 .env\uB97C \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694." }] };
|
|
2085
2720
|
}
|
|
2086
|
-
const keys = parseEnvKeys(
|
|
2721
|
+
const keys = parseEnvKeys(readFileSync5(".env", "utf-8"));
|
|
2087
2722
|
if (keys.length === 0) {
|
|
2088
2723
|
return { content: [{ type: "text", text: "\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
|
|
2089
2724
|
}
|
|
2090
2725
|
const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
|
|
2091
2726
|
writeFileSync3(".env.example", exampleContent, "utf-8");
|
|
2092
2727
|
const gitignoreLines = [];
|
|
2093
|
-
if (
|
|
2094
|
-
const content =
|
|
2728
|
+
if (existsSync8(".gitignore")) {
|
|
2729
|
+
const content = readFileSync5(".gitignore", "utf-8");
|
|
2095
2730
|
if (!content.split("\n").some((l) => l.trim() === ".env")) {
|
|
2096
2731
|
appendFileSync2(".gitignore", "\n.env\n");
|
|
2097
2732
|
gitignoreLines.push("\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428");
|
|
@@ -2111,11 +2746,11 @@ ${log.out}` }] };
|
|
|
2111
2746
|
description: "\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC (.env.example \uAE30\uC900)"
|
|
2112
2747
|
},
|
|
2113
2748
|
async () => {
|
|
2114
|
-
if (!
|
|
2749
|
+
if (!existsSync8(".env.example")) {
|
|
2115
2750
|
return { content: [{ type: "text", text: "\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 env \uB3C4\uAD6C\uB97C \uC2E4\uD589\uD558\uC138\uC694." }] };
|
|
2116
2751
|
}
|
|
2117
|
-
const requiredKeys = parseEnvKeys(
|
|
2118
|
-
const currentKeys =
|
|
2752
|
+
const requiredKeys = parseEnvKeys(readFileSync5(".env.example", "utf-8"));
|
|
2753
|
+
const currentKeys = existsSync8(".env") ? parseEnvKeys(readFileSync5(".env", "utf-8")) : [];
|
|
2119
2754
|
const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
|
|
2120
2755
|
const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
|
|
2121
2756
|
const lines = [`\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`];
|
|
@@ -2231,7 +2866,7 @@ ${cliStatus}
|
|
|
2231
2866
|
description: "\uD604\uC7AC \uBC84\uC804 + bump \uD6C4\uBCF4 \uD45C\uC2DC (MCP \uBAA8\uB4DC: \uC2E4\uC81C npm publish \uBBF8\uC218\uD589 \u2014 `vhk publish` \uC548\uB0B4)"
|
|
2232
2867
|
},
|
|
2233
2868
|
async () => {
|
|
2234
|
-
if (!
|
|
2869
|
+
if (!existsSync8("package.json")) {
|
|
2235
2870
|
return { content: [{ type: "text", text: "\u274C package.json \uC5C6\uC74C." }] };
|
|
2236
2871
|
}
|
|
2237
2872
|
try {
|
|
@@ -2259,7 +2894,7 @@ ${cliStatus}
|
|
|
2259
2894
|
description: "\uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800 \uAC10\uC9C0 + \uC804\uD658 \uD6C4\uBCF4 \uAC00\uC6A9\uC131 (MCP \uBAA8\uB4DC: \uC2E4\uC81C \uC804\uD658 \uBBF8\uC218\uD589 \u2014 `vhk migrate <target>` \uC548\uB0B4)"
|
|
2260
2895
|
},
|
|
2261
2896
|
async () => {
|
|
2262
|
-
const current =
|
|
2897
|
+
const current = existsSync8("pnpm-lock.yaml") ? "pnpm" : existsSync8("yarn.lock") ? "yarn" : existsSync8("package-lock.json") ? "npm" : null;
|
|
2263
2898
|
const candidates = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current);
|
|
2264
2899
|
const lines = [`\uD604\uC7AC PM: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00 (lock \uD30C\uC77C \uC5C6\uC74C)"}`];
|
|
2265
2900
|
for (const pm of candidates) {
|
|
@@ -2325,10 +2960,27 @@ export {
|
|
|
2325
2960
|
__toESM,
|
|
2326
2961
|
ko,
|
|
2327
2962
|
t,
|
|
2963
|
+
localDate,
|
|
2964
|
+
listBackups,
|
|
2965
|
+
restoreBackup,
|
|
2966
|
+
detectExistingRuleFiles,
|
|
2967
|
+
buildAdoptedRules,
|
|
2968
|
+
sync,
|
|
2969
|
+
getGitRoot,
|
|
2970
|
+
gitOut,
|
|
2971
|
+
gitRun,
|
|
2972
|
+
getExecErrorMessage,
|
|
2973
|
+
hasGitRemote,
|
|
2974
|
+
countLocalCommits,
|
|
2975
|
+
checkRuleDrift,
|
|
2976
|
+
CONTEXT_GIT_MARKER,
|
|
2977
|
+
checkContextDrift,
|
|
2328
2978
|
printNextStep,
|
|
2979
|
+
printContextResumeHint,
|
|
2329
2980
|
require_ignore,
|
|
2330
2981
|
printSecurityWarnings,
|
|
2331
2982
|
filterTrackedPaths,
|
|
2983
|
+
stripBom,
|
|
2332
2984
|
readJsonFile,
|
|
2333
2985
|
NETWORK_EXEC_TIMEOUT_MS,
|
|
2334
2986
|
safeExecFile,
|