@byh3071/vhk 1.6.1 → 1.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/{chunk-O3A6SO7G.js → chunk-53RJHPP6.js} +1004 -251
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1714 -1085
- package/dist/mcp/index.js +6 -1
- package/package.json +72 -71
|
@@ -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
|
|
@@ -562,7 +501,7 @@ var ko = {
|
|
|
562
501
|
notGitRepo: "Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2C8\uC5D0\uC694. \uBA3C\uC800 git init\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
563
502
|
branch: "\uBE0C\uB79C\uCE58:",
|
|
564
503
|
changes: "\uBCC0\uACBD:",
|
|
565
|
-
recentCommits:
|
|
504
|
+
recentCommits: (n) => `\uCD5C\uADFC \uCEE4\uBC0B (${n}):`,
|
|
566
505
|
noCommits: "\uCEE4\uBC0B \uC5C6\uC74C",
|
|
567
506
|
remote: "\uC6D0\uACA9:",
|
|
568
507
|
noUpstream: "upstream \uC5C6\uC74C",
|
|
@@ -573,8 +512,9 @@ var ko = {
|
|
|
573
512
|
noPackage: "package.json \uC5C6\uC74C",
|
|
574
513
|
detached: "(detached HEAD)",
|
|
575
514
|
unknownBranch: "(\uC54C \uC218 \uC5C6\uC74C)",
|
|
576
|
-
nextWithChangesMessage: "\uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC5B4\uC694. \
|
|
577
|
-
nextWithChangesCursor: "\
|
|
515
|
+
nextWithChangesMessage: "\uBCC0\uACBD\uC0AC\uD56D\uC774 \uC788\uC5B4\uC694. \uBA3C\uC800 \uBB34\uC5C7\uC774 \uBC14\uB00C\uC5C8\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
|
|
516
|
+
nextWithChangesCursor: "\uBB50 \uBC14\uB00C\uC5C8\uC5B4?",
|
|
517
|
+
nextWithChangesAlt: "\uD655\uC778\uD588\uC73C\uBA74 vhk save \uB85C \uC800\uC7A5\uD558\uC138\uC694",
|
|
578
518
|
nextCleanMessage: "\uD074\uB9B0 \uC0C1\uD0DC! \uB2E4\uC74C \uBBF8\uC158\uC73C\uB85C \uB118\uC5B4\uAC00\uC138\uC694.",
|
|
579
519
|
nextCleanCursor: "\uB2E4\uC74C \uBAA9\uD45C \uC54C\uB824\uC918"
|
|
580
520
|
},
|
|
@@ -719,7 +659,10 @@ var ko = {
|
|
|
719
659
|
commandsMdDone: "\u{1F4CB} COMMANDS.md \uC0DD\uC131",
|
|
720
660
|
scriptsDone: "\u{1F4E6} package.json scripts \uCD94\uAC00",
|
|
721
661
|
gitignoreCreated: "\u{1F512} .gitignore \uC0DD\uC131 (.env\xB7node_modules\xB7dist \uC81C\uC678)",
|
|
722
|
-
gitignoreUpdated: "\u{1F512} .gitignore \uBCF4\uAC15 (\uB204\uB77D \uD56D\uBAA9 \uCD94\uAC00)"
|
|
662
|
+
gitignoreUpdated: "\u{1F512} .gitignore \uBCF4\uAC15 (\uB204\uB77D \uD56D\uBAA9 \uCD94\uAC00)",
|
|
663
|
+
adoptPrompt: (n, list) => `\u{1F4E5} \uAE30\uC874 \uADDC\uCE59 \uD30C\uC77C ${n}\uAC1C \uBC1C\uACAC (${list}). RULES.md\uB85C \uAC00\uC838\uC62C\uAE4C\uC694?`,
|
|
664
|
+
adoptPreview: (n) => `\uAE30\uC874 \uADDC\uCE59 ${n}\uAC1C\uB97C RULES.md \uD45C\uC900 \uC139\uC158\uC73C\uB85C \uBCD1\uD569\uD588\uC5B4\uC694 (\uCD9C\uCC98 \uC8FC\uC11D \uD3EC\uD568).`,
|
|
665
|
+
adoptDone: "\u{1F4E5} RULES.md \u2014 \uAE30\uC874 \uADDC\uCE59 adopt \uC644\uB8CC"
|
|
723
666
|
},
|
|
724
667
|
recap: {
|
|
725
668
|
title: "\u{1F4DD} \uC624\uB298 \uD55C \uC77C \uC815\uB9AC",
|
|
@@ -783,8 +726,29 @@ var ko = {
|
|
|
783
726
|
windsurfDone: "\u2705 .windsurfrules \uB9DE\uCDA4 \uC644\uB8CC",
|
|
784
727
|
copilotDone: "\u2705 .github/copilot-instructions.md \uB9DE\uCDA4 \uC644\uB8CC",
|
|
785
728
|
antigravityDone: "\u2705 .agents/rules/vhk-rules.md \uB9DE\uCDA4 \uC644\uB8CC",
|
|
729
|
+
agentsDone: "\u2705 AGENTS.md \uB9DE\uCDA4 \uC644\uB8CC",
|
|
786
730
|
antigravityTruncated: "Antigravity 12,000\uC790 \uC81C\uD55C\uC73C\uB85C \uC77C\uBD80 \uC808\uC0AD\uB428 \u2014 \uC804\uCCB4\uB294 RULES.md \uCC38\uC870",
|
|
787
|
-
done: "\u{1F504} \uB9DE\uCD94\uAE30 \uC644\uB8CC!"
|
|
731
|
+
done: "\u{1F504} \uB9DE\uCD94\uAE30 \uC644\uB8CC!",
|
|
732
|
+
// 안전 가드 (배치 0) — 덮어쓰기 전 백업·드리프트 확인·미리보기
|
|
733
|
+
backupSaved: (n, id) => `\u{1F6DF} \uB36E\uC5B4\uC4F0\uAE30 \uC804 ${n}\uAC1C \uD30C\uC77C \uBC31\uC5C5\uD568 \u2192 .vhk/backups/${id} (\uBCF5\uC6D0: vhk restore)`,
|
|
734
|
+
firstSync: "\u{1F6DF} \uCCAB sync \u2014 \uAE30\uC874 \uD30C\uC77C\uC744 \uBC31\uC5C5\uD55C \uB4A4 \uC0DD\uC131\uD569\uB2C8\uB2E4.",
|
|
735
|
+
driftWarn: (p) => `\u26A0\uFE0F ${p} \uAC00 RULES.md \uC0DD\uC131\uBCF8\uACFC \uB2E4\uB985\uB2C8\uB2E4 (\uC9C1\uC811 \uC218\uC815\uD588\uC744 \uC218 \uC788\uC5B4\uC694).`,
|
|
736
|
+
driftConfirm: (n) => `\uC704 ${n}\uAC1C \uD30C\uC77C\uC758 \uAE30\uC874 \uB0B4\uC6A9\uC744 \uB36E\uC5B4\uC4F8\uAE4C\uC694? (\uBC31\uC5C5\uC740 \uC774\uBBF8 \uC800\uC7A5\uB428)`,
|
|
737
|
+
skipped: (p) => `\u23ED\uFE0F \uAC74\uB108\uB700: ${p} (\uB36E\uC5B4\uC4F0\uAE30 \uAC70\uBD80 \u2014 \uBC31\uC5C5\uB9CC \uBCF4\uAD00)`,
|
|
738
|
+
dryRunHeader: "\u{1F50E} \uBBF8\uB9AC\uBCF4\uAE30 (--dry-run) \u2014 \uC2E4\uC81C \uD30C\uC77C \uBCC0\uACBD \uC5C6\uC74C",
|
|
739
|
+
dryRunWouldWrite: (p, drift) => ` ${drift ? "\u270F\uFE0F \uBCC0\uACBD\uB428" : "\xB7 \uB3D9\uC77C"} : ${p}`,
|
|
740
|
+
nonTtyAuto: (n, id) => `\u{1F916} \uBE44\uB300\uD654\uD615(CI/\uC5D0\uC774\uC804\uD2B8) \u2014 ${n}\uAC1C \uBC31\uC5C5 \uD6C4 \uC9C4\uD589. \uBCF5\uC6D0: vhk restore ${id}`
|
|
741
|
+
},
|
|
742
|
+
restore: {
|
|
743
|
+
title: "\u{1F6DF} \uBC31\uC5C5 \uBCF5\uC6D0",
|
|
744
|
+
notGitNote: "\uBC31\uC5C5\uC740 .vhk/backups/ \uC758 \uB85C\uCEEC \uBCF5\uC0AC\uBCF8\uC5D0\uC11C \uBCF5\uC6D0\uB429\uB2C8\uB2E4 (git \uBB34\uAD00).",
|
|
745
|
+
noBackups: "\uBCF5\uC6D0\uD560 \uBC31\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. (vhk sync \uAC00 \uB36E\uC5B4\uC4F0\uAE30 \uC804 \uC790\uB3D9 \uC0DD\uC131)",
|
|
746
|
+
selectPrompt: "\uBCF5\uC6D0\uD560 \uBC31\uC5C5\uC744 \uC120\uD0DD\uD558\uC138\uC694:",
|
|
747
|
+
listHeader: "\u{1F4CB} \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBC31\uC5C5 (\uCD5C\uC2E0\uC21C):",
|
|
748
|
+
restored: (n, id) => `\u2705 ${n}\uAC1C \uD30C\uC77C \uBCF5\uC6D0 \uC644\uB8CC (\uBC31\uC5C5 ${id})`,
|
|
749
|
+
notFound: (id) => `\u274C \uBC31\uC5C5\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`,
|
|
750
|
+
nonTtyHint: "\uBE44\uB300\uD654\uD615 \uBAA8\uB4DC \u2014 \uBCF5\uC6D0\uD560 \uBC31\uC5C5 id \uB97C \uC778\uC790\uB85C \uC9C0\uC815\uD558\uC138\uC694: vhk restore <id>",
|
|
751
|
+
cancelled: "\uBCF5\uC6D0 \uCDE8\uC18C\uB428"
|
|
788
752
|
},
|
|
789
753
|
cloud: {
|
|
790
754
|
pushTitle: "\u2601\uFE0F .vhk \uD074\uB77C\uC6B0\uB4DC \uBC31\uC5C5 (gist \uC62C\uB9AC\uAE30)",
|
|
@@ -901,7 +865,10 @@ var ko = {
|
|
|
901
865
|
},
|
|
902
866
|
context: {
|
|
903
867
|
title: "\uD504\uB85C\uC81D\uD2B8 \uCEE8\uD14D\uC2A4\uD2B8 \uC0DD\uC131",
|
|
904
|
-
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"
|
|
905
872
|
},
|
|
906
873
|
brief: {
|
|
907
874
|
title: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB9AC\uD551"
|
|
@@ -912,7 +879,9 @@ var ko = {
|
|
|
912
879
|
initTitle: "\u{1F3D7}\uFE0F goals/ \uAD6C\uC870 \uC2A4\uCE90\uD3F4\uB529",
|
|
913
880
|
checkTitle: "\u2705 Goal \uAC8C\uC774\uD2B8 \uAC80\uC99D",
|
|
914
881
|
doneTitle: "\u{1F3C1} Goal \uC644\uB8CC \uCC98\uB9AC",
|
|
882
|
+
syncTitle: "\u{1F504} Goal \uAC8C\uC774\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8 \uB3D9\uAE30\uD654",
|
|
915
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):`,
|
|
916
885
|
notFound: (id) => `goal id ${id} \uC5C6\uC74C \u2014 vhk goal list \uB85C \uD655\uC778\uD558\uC138\uC694.`
|
|
917
886
|
},
|
|
918
887
|
agent: {
|
|
@@ -921,8 +890,8 @@ var ko = {
|
|
|
921
890
|
resumeTitle: "\u25B6\uFE0F HARD_STOP \uD574\uC81C"
|
|
922
891
|
}
|
|
923
892
|
};
|
|
924
|
-
function lookup(
|
|
925
|
-
const parts =
|
|
893
|
+
function lookup(path7) {
|
|
894
|
+
const parts = path7.split(".");
|
|
926
895
|
let cur = ko;
|
|
927
896
|
for (const part of parts) {
|
|
928
897
|
if (cur === null || typeof cur !== "object") return void 0;
|
|
@@ -940,7 +909,124 @@ function t(key, ...args) {
|
|
|
940
909
|
}
|
|
941
910
|
|
|
942
911
|
// src/lib/next-step.ts
|
|
912
|
+
import fs2 from "fs";
|
|
913
|
+
import path2 from "path";
|
|
943
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
|
|
944
1030
|
function printNextStep(step) {
|
|
945
1031
|
console.log("");
|
|
946
1032
|
console.log(chalk.cyan.bold("\u2501\u2501\u2501 \uB2E4\uC74C\uC5D0 \uC774\uAC83\uB9CC \uD558\uC138\uC694 \u2501\u2501\u2501"));
|
|
@@ -964,6 +1050,580 @@ function printNextStep(step) {
|
|
|
964
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"));
|
|
965
1051
|
console.log("");
|
|
966
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
|
+
}
|
|
967
1627
|
|
|
968
1628
|
// src/commands/deploy.ts
|
|
969
1629
|
var PLATFORMS = {
|
|
@@ -995,7 +1655,7 @@ var PLATFORMS = {
|
|
|
995
1655
|
function detectPlatform() {
|
|
996
1656
|
for (const [key, config] of Object.entries(PLATFORMS)) {
|
|
997
1657
|
for (const file of config.detectFiles) {
|
|
998
|
-
if (
|
|
1658
|
+
if (existsSync2(file)) return key;
|
|
999
1659
|
}
|
|
1000
1660
|
}
|
|
1001
1661
|
return null;
|
|
@@ -1004,14 +1664,14 @@ function isCLIAvailable(cmd, checkArgs) {
|
|
|
1004
1664
|
return safeExecFile(cmd, checkArgs).ok;
|
|
1005
1665
|
}
|
|
1006
1666
|
async function deploy() {
|
|
1007
|
-
console.log(
|
|
1008
|
-
console.log(
|
|
1667
|
+
console.log(chalk3.bold("\n\u{1F680} " + t("deploy.title")));
|
|
1668
|
+
console.log(chalk3.gray("\u2500".repeat(40)));
|
|
1009
1669
|
let platform = detectPlatform();
|
|
1010
1670
|
if (platform) {
|
|
1011
|
-
console.log(
|
|
1671
|
+
console.log(chalk3.cyan(`
|
|
1012
1672
|
\u{1F50D} \uAC10\uC9C0\uB41C \uD50C\uB7AB\uD3FC: ${PLATFORMS[platform].name}`));
|
|
1013
1673
|
} else {
|
|
1014
|
-
const { selected } = await
|
|
1674
|
+
const { selected } = await inquirer2.prompt([
|
|
1015
1675
|
{
|
|
1016
1676
|
type: "list",
|
|
1017
1677
|
name: "selected",
|
|
@@ -1027,12 +1687,12 @@ async function deploy() {
|
|
|
1027
1687
|
}
|
|
1028
1688
|
const config = PLATFORMS[platform];
|
|
1029
1689
|
if (!isCLIAvailable(config.command, config.checkArgs)) {
|
|
1030
|
-
console.log(
|
|
1690
|
+
console.log(chalk3.red(`
|
|
1031
1691
|
\u274C ${config.name} CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.`));
|
|
1032
|
-
console.log(
|
|
1692
|
+
console.log(chalk3.yellow(` \u2192 ${config.installHint}`));
|
|
1033
1693
|
return;
|
|
1034
1694
|
}
|
|
1035
|
-
const { confirm } = await
|
|
1695
|
+
const { confirm } = await inquirer2.prompt([
|
|
1036
1696
|
{
|
|
1037
1697
|
type: "confirm",
|
|
1038
1698
|
name: "confirm",
|
|
@@ -1041,15 +1701,15 @@ async function deploy() {
|
|
|
1041
1701
|
}
|
|
1042
1702
|
]);
|
|
1043
1703
|
if (!confirm) {
|
|
1044
|
-
console.log(
|
|
1704
|
+
console.log(chalk3.gray("\uCDE8\uC18C\uB428"));
|
|
1045
1705
|
return;
|
|
1046
1706
|
}
|
|
1047
|
-
console.log(
|
|
1707
|
+
console.log(chalk3.cyan(`
|
|
1048
1708
|
${t("deploy.deploying")}
|
|
1049
1709
|
`));
|
|
1050
1710
|
const result = safeExecFileStream(config.command, config.commandArgs);
|
|
1051
1711
|
if (result.ok) {
|
|
1052
|
-
console.log(
|
|
1712
|
+
console.log(chalk3.green(`
|
|
1053
1713
|
\u2705 ${t("deploy.success")}`));
|
|
1054
1714
|
printNextStep({
|
|
1055
1715
|
message: "\uBC30\uD3EC \uC644\uB8CC! \uC0AC\uC774\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.",
|
|
@@ -1057,50 +1717,69 @@ ${t("deploy.deploying")}
|
|
|
1057
1717
|
cursorHint: "\uC0C1\uD0DC \uD655\uC778\uD574\uC918"
|
|
1058
1718
|
});
|
|
1059
1719
|
} else {
|
|
1060
|
-
console.log(
|
|
1720
|
+
console.log(chalk3.red(`
|
|
1061
1721
|
\u274C ${t("deploy.failed")}`));
|
|
1062
|
-
console.log(
|
|
1722
|
+
console.log(chalk3.red(result.err));
|
|
1063
1723
|
}
|
|
1064
1724
|
}
|
|
1065
1725
|
|
|
1066
1726
|
// src/commands/env.ts
|
|
1067
|
-
import { existsSync as
|
|
1068
|
-
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";
|
|
1069
1730
|
function parseEnvKeys(content) {
|
|
1070
1731
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
|
|
1071
1732
|
}
|
|
1733
|
+
function loadDefinedEnvKeys(dir = ".") {
|
|
1734
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1735
|
+
let entries;
|
|
1736
|
+
try {
|
|
1737
|
+
entries = readdirSync(dir);
|
|
1738
|
+
} catch {
|
|
1739
|
+
return [];
|
|
1740
|
+
}
|
|
1741
|
+
for (const name of entries) {
|
|
1742
|
+
if (!name.startsWith(".env")) continue;
|
|
1743
|
+
if (name.endsWith(".example") || name.endsWith(".sample")) continue;
|
|
1744
|
+
try {
|
|
1745
|
+
for (const k of parseEnvKeys(readFileSync2(join2(dir, name), "utf-8"))) keys.add(k);
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return [...keys];
|
|
1750
|
+
}
|
|
1072
1751
|
function ensureGitignore() {
|
|
1073
1752
|
const gitignorePath = ".gitignore";
|
|
1074
|
-
if (
|
|
1075
|
-
const content =
|
|
1753
|
+
if (existsSync3(gitignorePath)) {
|
|
1754
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1076
1755
|
if (!content.split("\n").some((l) => l.trim() === ".env")) {
|
|
1077
1756
|
appendFileSync(gitignorePath, "\n.env\n");
|
|
1078
|
-
console.log(
|
|
1757
|
+
console.log(chalk4.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
|
|
1079
1758
|
}
|
|
1080
1759
|
} else {
|
|
1081
1760
|
writeFileSync(gitignorePath, ".env\nnode_modules/\ndist/\n");
|
|
1082
|
-
console.log(
|
|
1761
|
+
console.log(chalk4.green("\n\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)"));
|
|
1083
1762
|
}
|
|
1084
1763
|
}
|
|
1085
1764
|
async function env() {
|
|
1086
|
-
console.log(
|
|
1087
|
-
console.log(
|
|
1088
|
-
if (!
|
|
1089
|
-
console.log(
|
|
1090
|
-
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."));
|
|
1091
1770
|
return;
|
|
1092
1771
|
}
|
|
1093
|
-
const envContent =
|
|
1772
|
+
const envContent = readFileSync2(".env", "utf-8");
|
|
1094
1773
|
const keys = parseEnvKeys(envContent);
|
|
1095
1774
|
if (keys.length === 0) {
|
|
1096
|
-
console.log(
|
|
1775
|
+
console.log(chalk4.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1097
1776
|
return;
|
|
1098
1777
|
}
|
|
1099
1778
|
const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
|
|
1100
1779
|
writeFileSync(".env.example", exampleContent, "utf-8");
|
|
1101
|
-
console.log(
|
|
1780
|
+
console.log(chalk4.green(`
|
|
1102
1781
|
\u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`));
|
|
1103
|
-
keys.forEach((k) => console.log(
|
|
1782
|
+
keys.forEach((k) => console.log(chalk4.gray(` ${k}`)));
|
|
1104
1783
|
ensureGitignore();
|
|
1105
1784
|
printNextStep({
|
|
1106
1785
|
message: ".env.example \uC0DD\uC131 \uC644\uB8CC!",
|
|
@@ -1109,46 +1788,47 @@ async function env() {
|
|
|
1109
1788
|
});
|
|
1110
1789
|
}
|
|
1111
1790
|
async function envCheck() {
|
|
1112
|
-
console.log(
|
|
1113
|
-
console.log(
|
|
1114
|
-
if (!
|
|
1115
|
-
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."));
|
|
1116
1795
|
return;
|
|
1117
1796
|
}
|
|
1118
|
-
const requiredKeys = parseEnvKeys(
|
|
1119
|
-
const currentKeys =
|
|
1797
|
+
const requiredKeys = parseEnvKeys(readFileSync2(".env.example", "utf-8"));
|
|
1798
|
+
const currentKeys = loadDefinedEnvKeys();
|
|
1120
1799
|
const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
|
|
1121
1800
|
const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
|
|
1122
|
-
console.log(
|
|
1801
|
+
console.log(chalk4.cyan(`
|
|
1123
1802
|
\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`));
|
|
1124
1803
|
if (missing.length === 0) {
|
|
1125
|
-
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!"));
|
|
1126
1805
|
} else {
|
|
1127
|
-
console.log(
|
|
1806
|
+
console.log(chalk4.red(`
|
|
1128
1807
|
\u274C \uB204\uB77D\uB41C \uD658\uACBD\uBCC0\uC218 (${missing.length}\uAC1C):`));
|
|
1129
|
-
missing.forEach((k) => console.log(
|
|
1808
|
+
missing.forEach((k) => console.log(chalk4.red(` \u2022 ${k}`)));
|
|
1809
|
+
process.exitCode = 1;
|
|
1130
1810
|
}
|
|
1131
1811
|
if (extra.length > 0) {
|
|
1132
|
-
console.log(
|
|
1812
|
+
console.log(chalk4.yellow(`
|
|
1133
1813
|
\u{1F4A1} .env.example\uC5D0 \uC5C6\uB294 \uCD94\uAC00 \uBCC0\uC218 (${extra.length}\uAC1C):`));
|
|
1134
|
-
extra.forEach((k) => console.log(
|
|
1814
|
+
extra.forEach((k) => console.log(chalk4.yellow(` \u2022 ${k}`)));
|
|
1135
1815
|
}
|
|
1136
1816
|
ensureGitignore();
|
|
1137
1817
|
}
|
|
1138
1818
|
|
|
1139
1819
|
// src/commands/publish.ts
|
|
1140
|
-
import { existsSync as
|
|
1141
|
-
import
|
|
1142
|
-
import
|
|
1820
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
1821
|
+
import chalk5 from "chalk";
|
|
1822
|
+
import inquirer3 from "inquirer";
|
|
1143
1823
|
import ora from "ora";
|
|
1144
1824
|
|
|
1145
1825
|
// src/lib/read-json.ts
|
|
1146
|
-
import { readFileSync as
|
|
1826
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1147
1827
|
function stripBom(text) {
|
|
1148
1828
|
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1149
1829
|
}
|
|
1150
1830
|
function readJsonFile(filePath) {
|
|
1151
|
-
const raw = stripBom(
|
|
1831
|
+
const raw = stripBom(readFileSync3(filePath, "utf-8"));
|
|
1152
1832
|
return JSON.parse(raw);
|
|
1153
1833
|
}
|
|
1154
1834
|
|
|
@@ -1207,23 +1887,23 @@ function gitPostRelease(newVersion) {
|
|
|
1207
1887
|
};
|
|
1208
1888
|
}
|
|
1209
1889
|
async function publish() {
|
|
1210
|
-
console.log(
|
|
1211
|
-
console.log(
|
|
1212
|
-
if (!
|
|
1213
|
-
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."));
|
|
1214
1894
|
return;
|
|
1215
1895
|
}
|
|
1216
1896
|
let pkg;
|
|
1217
1897
|
try {
|
|
1218
1898
|
pkg = readJsonFile("package.json");
|
|
1219
1899
|
} catch {
|
|
1220
|
-
console.log(
|
|
1900
|
+
console.log(chalk5.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
|
|
1221
1901
|
return;
|
|
1222
1902
|
}
|
|
1223
1903
|
const currentVersion = pkg.version || "0.0.0";
|
|
1224
|
-
console.log(
|
|
1904
|
+
console.log(chalk5.cyan(`
|
|
1225
1905
|
\u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
|
|
1226
|
-
const { bumpType } = await
|
|
1906
|
+
const { bumpType } = await inquirer3.prompt([
|
|
1227
1907
|
{
|
|
1228
1908
|
type: "list",
|
|
1229
1909
|
name: "bumpType",
|
|
@@ -1236,16 +1916,16 @@ async function publish() {
|
|
|
1236
1916
|
}
|
|
1237
1917
|
]);
|
|
1238
1918
|
const newVersion = bumpVersion(currentVersion, bumpType);
|
|
1239
|
-
console.log(
|
|
1919
|
+
console.log(chalk5.cyan(`
|
|
1240
1920
|
\u{1F195} \uC0C8 \uBC84\uC804: v${newVersion}`));
|
|
1241
1921
|
pkg.version = newVersion;
|
|
1242
1922
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1243
|
-
console.log(
|
|
1923
|
+
console.log(chalk5.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
|
|
1244
1924
|
const buildSpinner = ora(t("publish.building")).start();
|
|
1245
1925
|
const buildResult = safeExecFile("pnpm", ["build"]);
|
|
1246
1926
|
if (!buildResult.ok) {
|
|
1247
1927
|
buildSpinner.fail(t("publish.buildFailed"));
|
|
1248
|
-
console.log(
|
|
1928
|
+
console.log(chalk5.red(buildResult.err.slice(0, 500)));
|
|
1249
1929
|
pkg.version = currentVersion;
|
|
1250
1930
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1251
1931
|
return;
|
|
@@ -1255,13 +1935,13 @@ async function publish() {
|
|
|
1255
1935
|
const testResult = safeExecFile("pnpm", ["test", "--run"]);
|
|
1256
1936
|
if (!testResult.ok) {
|
|
1257
1937
|
testSpinner.fail(t("publish.testFailed"));
|
|
1258
|
-
console.log(
|
|
1938
|
+
console.log(chalk5.red(testResult.err.slice(0, 500)));
|
|
1259
1939
|
pkg.version = currentVersion;
|
|
1260
1940
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1261
1941
|
return;
|
|
1262
1942
|
}
|
|
1263
1943
|
testSpinner.succeed(t("publish.testSuccess"));
|
|
1264
|
-
const { confirm } = await
|
|
1944
|
+
const { confirm } = await inquirer3.prompt([
|
|
1265
1945
|
{
|
|
1266
1946
|
type: "confirm",
|
|
1267
1947
|
name: "confirm",
|
|
@@ -1272,37 +1952,37 @@ async function publish() {
|
|
|
1272
1952
|
if (!confirm) {
|
|
1273
1953
|
pkg.version = currentVersion;
|
|
1274
1954
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1275
|
-
console.log(
|
|
1955
|
+
console.log(chalk5.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
|
|
1276
1956
|
return;
|
|
1277
1957
|
}
|
|
1278
|
-
console.log(
|
|
1958
|
+
console.log(chalk5.cyan(`
|
|
1279
1959
|
\u{1F4E4} ${t("publish.publishing")}`));
|
|
1280
|
-
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)"));
|
|
1281
1961
|
const pubResult = safeExecFileStream("npm", ["publish", "--access", "public"]);
|
|
1282
1962
|
if (!pubResult.ok) {
|
|
1283
|
-
console.log(
|
|
1963
|
+
console.log(chalk5.red(`
|
|
1284
1964
|
\u2716 ${t("publish.publishFailed")}`));
|
|
1285
|
-
console.log(
|
|
1965
|
+
console.log(chalk5.red(pubResult.err.slice(0, 500)));
|
|
1286
1966
|
pkg.version = currentVersion;
|
|
1287
1967
|
writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
1288
|
-
console.log(
|
|
1968
|
+
console.log(chalk5.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
|
|
1289
1969
|
return;
|
|
1290
1970
|
}
|
|
1291
|
-
console.log(
|
|
1971
|
+
console.log(chalk5.green(`
|
|
1292
1972
|
\u2714 ${t("publish.publishSuccess")}`));
|
|
1293
1973
|
const git = gitPostRelease(newVersion);
|
|
1294
1974
|
if (git.warning) {
|
|
1295
|
-
console.log(
|
|
1975
|
+
console.log(chalk5.yellow(`
|
|
1296
1976
|
\u26A0\uFE0F ${git.warning}`));
|
|
1297
|
-
console.log(
|
|
1977
|
+
console.log(chalk5.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
|
|
1298
1978
|
} else if (git.tagged && git.pushed) {
|
|
1299
|
-
console.log(
|
|
1979
|
+
console.log(chalk5.green(`
|
|
1300
1980
|
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131 + push \uC644\uB8CC`));
|
|
1301
1981
|
} else if (git.tagged) {
|
|
1302
|
-
console.log(
|
|
1982
|
+
console.log(chalk5.yellow(`
|
|
1303
1983
|
\u{1F3F7}\uFE0F git tag v${newVersion} \uC0DD\uC131\uB428 (push\uB294 \uC218\uB3D9\uC73C\uB85C)`));
|
|
1304
1984
|
}
|
|
1305
|
-
console.log(
|
|
1985
|
+
console.log(chalk5.green.bold(`
|
|
1306
1986
|
\u{1F389} v${newVersion} \uBC30\uD3EC \uC644\uB8CC!`));
|
|
1307
1987
|
printNextStep({
|
|
1308
1988
|
message: "npm \uBC30\uD3EC \uC644\uB8CC!",
|
|
@@ -1312,13 +1992,13 @@ async function publish() {
|
|
|
1312
1992
|
}
|
|
1313
1993
|
|
|
1314
1994
|
// src/commands/audit.ts
|
|
1315
|
-
import { existsSync as
|
|
1316
|
-
import
|
|
1317
|
-
import
|
|
1995
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1996
|
+
import chalk6 from "chalk";
|
|
1997
|
+
import inquirer4 from "inquirer";
|
|
1318
1998
|
import ora2 from "ora";
|
|
1319
1999
|
function detectCurrentPM() {
|
|
1320
|
-
if (
|
|
1321
|
-
if (
|
|
2000
|
+
if (existsSync5("pnpm-lock.yaml")) return "pnpm";
|
|
2001
|
+
if (existsSync5("yarn.lock")) return "yarn";
|
|
1322
2002
|
return "npm";
|
|
1323
2003
|
}
|
|
1324
2004
|
function parseAuditOutput(output, pm) {
|
|
@@ -1358,26 +2038,26 @@ function runAuditFix(pm) {
|
|
|
1358
2038
|
return result.ok ? { ok: true } : { ok: false, err: result.err };
|
|
1359
2039
|
}
|
|
1360
2040
|
async function audit(autoFix = false) {
|
|
1361
|
-
console.log(
|
|
1362
|
-
console.log(
|
|
2041
|
+
console.log(chalk6.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
|
|
2042
|
+
console.log(chalk6.gray("\u2500".repeat(40)));
|
|
1363
2043
|
const pm = detectCurrentPM();
|
|
1364
|
-
console.log(
|
|
2044
|
+
console.log(chalk6.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
|
|
1365
2045
|
const spinner = ora2("\uBCF4\uC548 \uAC10\uC0AC \uC2E4\uD589 \uC911...").start();
|
|
1366
2046
|
const output = runAuditJson(pm);
|
|
1367
2047
|
spinner.stop();
|
|
1368
2048
|
const summary = parseAuditOutput(output, pm);
|
|
1369
2049
|
if (summary.total === 0) {
|
|
1370
|
-
console.log(
|
|
2050
|
+
console.log(chalk6.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
|
|
1371
2051
|
return;
|
|
1372
2052
|
}
|
|
1373
|
-
console.log(
|
|
1374
|
-
if (summary.critical > 0) console.log(
|
|
1375
|
-
if (summary.high > 0) console.log(
|
|
1376
|
-
if (summary.moderate > 0) console.log(
|
|
1377
|
-
if (summary.low > 0) console.log(
|
|
1378
|
-
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(`
|
|
1379
2059
|
\uCD1D ${summary.total}\uAC1C\uC758 \uCDE8\uC57D\uC810`));
|
|
1380
|
-
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([
|
|
1381
2061
|
{
|
|
1382
2062
|
type: "confirm",
|
|
1383
2063
|
name: "shouldFix",
|
|
@@ -1402,17 +2082,17 @@ async function audit(autoFix = false) {
|
|
|
1402
2082
|
}
|
|
1403
2083
|
|
|
1404
2084
|
// src/lib/version.ts
|
|
1405
|
-
import { existsSync as
|
|
1406
|
-
import { dirname, join } from "path";
|
|
2085
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2086
|
+
import { dirname, join as join3 } from "path";
|
|
1407
2087
|
import { fileURLToPath } from "url";
|
|
1408
2088
|
function getVhkVersion() {
|
|
1409
2089
|
const dir = dirname(fileURLToPath(import.meta.url));
|
|
1410
2090
|
for (const pkgPath of [
|
|
1411
|
-
|
|
1412
|
-
|
|
2091
|
+
join3(dir, "../../package.json"),
|
|
2092
|
+
join3(dir, "../package.json")
|
|
1413
2093
|
]) {
|
|
1414
2094
|
try {
|
|
1415
|
-
if (
|
|
2095
|
+
if (existsSync6(pkgPath)) {
|
|
1416
2096
|
const pkg = readJsonFile(pkgPath);
|
|
1417
2097
|
if (pkg.version) return pkg.version;
|
|
1418
2098
|
}
|
|
@@ -1423,14 +2103,36 @@ function getVhkVersion() {
|
|
|
1423
2103
|
return "0.0.0";
|
|
1424
2104
|
}
|
|
1425
2105
|
|
|
2106
|
+
// src/mcp/cli-path.ts
|
|
2107
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2108
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2109
|
+
import { dirname as dirname2, resolve } from "path";
|
|
2110
|
+
function pickCliInvocation(globalAvailable, localCli, localExists) {
|
|
2111
|
+
if (globalAvailable) return { bin: "vhk", prefixArgs: [], fallback: false };
|
|
2112
|
+
if (localExists) return { bin: process.execPath, prefixArgs: [localCli], fallback: true };
|
|
2113
|
+
return { bin: "vhk", prefixArgs: [], fallback: false };
|
|
2114
|
+
}
|
|
2115
|
+
function composeInvocation(cli, args) {
|
|
2116
|
+
return { bin: cli.bin, args: [...cli.prefixArgs, ...args] };
|
|
2117
|
+
}
|
|
2118
|
+
function localCliPath() {
|
|
2119
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
2120
|
+
return resolve(here, "..", "index.js");
|
|
2121
|
+
}
|
|
2122
|
+
function resolveVhkCliInvocation() {
|
|
2123
|
+
const globalAvailable = safeExecFile("vhk", ["--version"]).ok;
|
|
2124
|
+
const local = localCliPath();
|
|
2125
|
+
return pickCliInvocation(globalAvailable, local, existsSync7(local));
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1426
2128
|
// src/mcp/server.ts
|
|
1427
2129
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1428
2130
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1429
2131
|
import { z } from "zod";
|
|
1430
|
-
import { existsSync as
|
|
2132
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2 } from "fs";
|
|
1431
2133
|
|
|
1432
2134
|
// src/lib/scan-secrets.ts
|
|
1433
|
-
import
|
|
2135
|
+
import fs7 from "fs";
|
|
1434
2136
|
|
|
1435
2137
|
// src/lib/secret-patterns.ts
|
|
1436
2138
|
var SECRET_PATTERNS = [
|
|
@@ -1496,19 +2198,19 @@ function maskSecret(value) {
|
|
|
1496
2198
|
}
|
|
1497
2199
|
|
|
1498
2200
|
// src/lib/scan-files.ts
|
|
1499
|
-
import
|
|
1500
|
-
import
|
|
2201
|
+
import fs6 from "fs";
|
|
2202
|
+
import path6 from "path";
|
|
1501
2203
|
|
|
1502
2204
|
// src/lib/check-secure.ts
|
|
1503
2205
|
var import_ignore = __toESM(require_ignore(), 1);
|
|
1504
|
-
import
|
|
1505
|
-
import
|
|
1506
|
-
import
|
|
2206
|
+
import fs5 from "fs";
|
|
2207
|
+
import path5 from "path";
|
|
2208
|
+
import chalk7 from "chalk";
|
|
1507
2209
|
function loadGitignore(rootDir) {
|
|
1508
2210
|
const ig = (0, import_ignore.default)();
|
|
1509
|
-
const gitignorePath =
|
|
1510
|
-
if (
|
|
1511
|
-
const content =
|
|
2211
|
+
const gitignorePath = path5.join(rootDir, ".gitignore");
|
|
2212
|
+
if (fs5.existsSync(gitignorePath)) {
|
|
2213
|
+
const content = fs5.readFileSync(gitignorePath, "utf-8");
|
|
1512
2214
|
ig.add(content);
|
|
1513
2215
|
}
|
|
1514
2216
|
return ig;
|
|
@@ -1523,14 +2225,14 @@ function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDept
|
|
|
1523
2225
|
if (depth > maxDepth) return;
|
|
1524
2226
|
let entries;
|
|
1525
2227
|
try {
|
|
1526
|
-
entries =
|
|
2228
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1527
2229
|
} catch {
|
|
1528
2230
|
return;
|
|
1529
2231
|
}
|
|
1530
2232
|
for (const entry of entries) {
|
|
1531
2233
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
1532
|
-
const fullPath =
|
|
1533
|
-
const rel =
|
|
2234
|
+
const fullPath = path5.join(dir, entry.name);
|
|
2235
|
+
const rel = path5.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1534
2236
|
if (entry.isDirectory()) {
|
|
1535
2237
|
if (!isPathIgnored(ig, rel + "/")) walk(fullPath, depth + 1);
|
|
1536
2238
|
continue;
|
|
@@ -1545,6 +2247,7 @@ function findExposedSensitiveFiles(rootDir, ig = loadGitignore(rootDir), maxDept
|
|
|
1545
2247
|
}
|
|
1546
2248
|
function isSensitiveName(name) {
|
|
1547
2249
|
const lower = name.toLowerCase();
|
|
2250
|
+
if (lower.endsWith(".example") || lower.endsWith(".sample")) return false;
|
|
1548
2251
|
if (lower === ".env" || lower.startsWith(".env.")) return true;
|
|
1549
2252
|
if (lower.endsWith(".pem") || lower.endsWith(".key")) return true;
|
|
1550
2253
|
if (lower === "credentials.json" || lower === "secrets.json") return true;
|
|
@@ -1552,8 +2255,8 @@ function isSensitiveName(name) {
|
|
|
1552
2255
|
return false;
|
|
1553
2256
|
}
|
|
1554
2257
|
function checkProjectSecurity(rootDir = process.cwd()) {
|
|
1555
|
-
const gitignorePath =
|
|
1556
|
-
const missingGitignore = !
|
|
2258
|
+
const gitignorePath = path5.join(rootDir, ".gitignore");
|
|
2259
|
+
const missingGitignore = !fs5.existsSync(gitignorePath);
|
|
1557
2260
|
const ig = loadGitignore(rootDir);
|
|
1558
2261
|
const exposedPaths = findExposedSensitiveFiles(rootDir, ig);
|
|
1559
2262
|
const warnings = [];
|
|
@@ -1576,7 +2279,7 @@ function printSecurityWarnings(rootDir = process.cwd()) {
|
|
|
1576
2279
|
const result = checkProjectSecurity(rootDir);
|
|
1577
2280
|
if (result.ok) return true;
|
|
1578
2281
|
for (const w of result.warnings) {
|
|
1579
|
-
console.log(
|
|
2282
|
+
console.log(chalk7.yellow(` \u26A0\uFE0F ${w}`));
|
|
1580
2283
|
}
|
|
1581
2284
|
return false;
|
|
1582
2285
|
}
|
|
@@ -1624,19 +2327,19 @@ var MAX_SCAN_FILE_BYTES = 512 * 1024;
|
|
|
1624
2327
|
function isScannableFileName(fileName) {
|
|
1625
2328
|
if (SKIP_FILE_NAMES.has(fileName)) return false;
|
|
1626
2329
|
if (fileName.startsWith(".env")) return true;
|
|
1627
|
-
return SCAN_EXTENSIONS.has(
|
|
2330
|
+
return SCAN_EXTENSIONS.has(path6.extname(fileName).toLowerCase());
|
|
1628
2331
|
}
|
|
1629
2332
|
function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
1630
2333
|
function walk(dir) {
|
|
1631
2334
|
let entries;
|
|
1632
2335
|
try {
|
|
1633
|
-
entries =
|
|
2336
|
+
entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1634
2337
|
} catch {
|
|
1635
2338
|
return;
|
|
1636
2339
|
}
|
|
1637
2340
|
for (const entry of entries) {
|
|
1638
|
-
const fullPath =
|
|
1639
|
-
const rel =
|
|
2341
|
+
const fullPath = path6.join(dir, entry.name);
|
|
2342
|
+
const rel = path6.relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
1640
2343
|
if (entry.isDirectory()) {
|
|
1641
2344
|
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
1642
2345
|
if (isPathIgnored(ig, `${rel}/`)) continue;
|
|
@@ -1647,7 +2350,7 @@ function walkProjectFiles(rootDir, onFile, ig = loadGitignore(rootDir)) {
|
|
|
1647
2350
|
if (isPathIgnored(ig, rel)) continue;
|
|
1648
2351
|
let size = 0;
|
|
1649
2352
|
try {
|
|
1650
|
-
size =
|
|
2353
|
+
size = fs6.statSync(fullPath).size;
|
|
1651
2354
|
} catch {
|
|
1652
2355
|
continue;
|
|
1653
2356
|
}
|
|
@@ -1692,7 +2395,7 @@ function scanProjectForSecrets(cwd) {
|
|
|
1692
2395
|
let truncated = false;
|
|
1693
2396
|
walkProjectFiles(cwd, (filePath, relPath) => {
|
|
1694
2397
|
scannedFiles++;
|
|
1695
|
-
const content =
|
|
2398
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
1696
2399
|
const lines = content.split("\n");
|
|
1697
2400
|
lines.forEach((line, idx) => {
|
|
1698
2401
|
if (truncated) return;
|
|
@@ -1721,8 +2424,15 @@ var ANSI_RE = /\x1B\[[0-9;?]*[ -/]*[@-~]/g;
|
|
|
1721
2424
|
function stripAnsi(s) {
|
|
1722
2425
|
return s.replace(ANSI_RE, "");
|
|
1723
2426
|
}
|
|
2427
|
+
var cachedCli = null;
|
|
2428
|
+
function getVhkCli() {
|
|
2429
|
+
return cachedCli ??= resolveVhkCliInvocation();
|
|
2430
|
+
}
|
|
1724
2431
|
function runVhkCli(args, headline) {
|
|
1725
|
-
const
|
|
2432
|
+
const { bin, args: fullArgs } = composeInvocation(getVhkCli(), args);
|
|
2433
|
+
const result = safeExecFile(bin, fullArgs, {
|
|
2434
|
+
env: { FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
2435
|
+
});
|
|
1726
2436
|
const body = stripAnsi(result.out || (result.ok ? "" : `(stdout \uC5C6\uC74C)
|
|
1727
2437
|
${result.err}`));
|
|
1728
2438
|
const prefix = result.ok ? `\u2705 ${headline}` : `\u274C ${headline} \uC2E4\uD328`;
|
|
@@ -1795,32 +2505,56 @@ ${preview}${more}
|
|
|
1795
2505
|
};
|
|
1796
2506
|
}
|
|
1797
2507
|
);
|
|
1798
|
-
server.registerTool(
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
{
|
|
1813
|
-
|
|
1814
|
-
|
|
2508
|
+
server.registerTool(
|
|
2509
|
+
"undo",
|
|
2510
|
+
{
|
|
2511
|
+
description: "\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30 (soft reset). \uAE30\uBCF8\uC740 \uBBF8\uB9AC\uBCF4\uAE30 \u2014 confirm:true \uC77C \uB54C\uB9CC \uC2E4\uC81C \uC2E4\uD589.",
|
|
2512
|
+
inputSchema: {
|
|
2513
|
+
confirm: z.boolean().optional().describe("true \uC77C \uB54C\uB9CC \uC2E4\uC81C git reset \uC2E4\uD589 (\uAE30\uBCF8 false = \uBBF8\uB9AC\uBCF4\uAE30\uB9CC)")
|
|
2514
|
+
}
|
|
2515
|
+
},
|
|
2516
|
+
async ({ confirm }) => {
|
|
2517
|
+
if (!isGitRepo()) {
|
|
2518
|
+
return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
|
|
2519
|
+
}
|
|
2520
|
+
const last = safeExecFile("git", ["log", "--oneline", "-1"]);
|
|
2521
|
+
if (!last.ok || !last.out) {
|
|
2522
|
+
return { content: [{ type: "text", text: "\u{1F4ED} \uB418\uB3CC\uB9B4 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
|
|
2523
|
+
}
|
|
2524
|
+
if (!confirm) {
|
|
2525
|
+
return {
|
|
2526
|
+
content: [
|
|
2527
|
+
{
|
|
2528
|
+
type: "text",
|
|
2529
|
+
text: `\u{1F50E} \uBBF8\uB9AC\uBCF4\uAE30 \u2014 \uB418\uB3CC\uB9B4 \uCEE4\uBC0B:
|
|
2530
|
+
${last.out}
|
|
2531
|
+
|
|
2532
|
+
\uC2E4\uC81C\uB85C \uB418\uB3CC\uB9AC\uB824\uBA74 confirm: true \uB85C \uB2E4\uC2DC \uD638\uCD9C\uD558\uC138\uC694.
|
|
2533
|
+
(soft reset \u2014 \uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uBCF4\uC874\uB429\uB2C8\uB2E4.)
|
|
2534
|
+
\uB610\uB294 \uD130\uBBF8\uB110\uC5D0\uC11C \`vhk undo\` (\uB300\uD654\uD615 \uD655\uC778).`
|
|
2535
|
+
}
|
|
2536
|
+
]
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
const reset = safeExecFile("git", ["reset", "--soft", "HEAD~1"]);
|
|
2540
|
+
if (!reset.ok) {
|
|
2541
|
+
return { content: [{ type: "text", text: `\u274C reset \uC2E4\uD328: ${reset.err}` }] };
|
|
2542
|
+
}
|
|
2543
|
+
return {
|
|
2544
|
+
content: [
|
|
2545
|
+
{
|
|
2546
|
+
type: "text",
|
|
2547
|
+
text: `\u2705 \uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC!
|
|
1815
2548
|
\uCDE8\uC18C\uB41C \uCEE4\uBC0B: ${last.out}
|
|
1816
2549
|
\u{1F4A1} \uBCC0\uACBD\uC0AC\uD56D\uC740 \uC2A4\uD14C\uC774\uC9D5 \uC601\uC5ED\uC5D0 \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4.`
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2550
|
+
}
|
|
2551
|
+
]
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
);
|
|
1821
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 () => {
|
|
1822
2556
|
const lines = [];
|
|
1823
|
-
if (
|
|
2557
|
+
if (existsSync8("package.json")) {
|
|
1824
2558
|
try {
|
|
1825
2559
|
const pkg = readJsonFile("package.json");
|
|
1826
2560
|
lines.push(`\u{1F4E6} \uD504\uB85C\uC81D\uD2B8: ${pkg.name ?? "(\uC774\uB984 \uC5C6\uC74C)"} v${pkg.version ?? "?"}`);
|
|
@@ -1905,7 +2639,7 @@ ${preview}${more}
|
|
|
1905
2639
|
checks.push(build.ok ? "\u2705 \uBE4C\uB4DC \uC131\uACF5" : "\u274C \uBE4C\uB4DC \uC2E4\uD328");
|
|
1906
2640
|
const test = safeExecFile("pnpm", ["test", "--run"]);
|
|
1907
2641
|
checks.push(test.ok ? "\u2705 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC" : "\u274C \uD14C\uC2A4\uD2B8 \uC2E4\uD328");
|
|
1908
|
-
if (
|
|
2642
|
+
if (existsSync8("package.json")) {
|
|
1909
2643
|
try {
|
|
1910
2644
|
const pkg = readJsonFile("package.json");
|
|
1911
2645
|
checks.push(`\u{1F4E6} \uBC84\uC804: ${pkg.version}`);
|
|
@@ -1943,11 +2677,11 @@ ${preview}${more}
|
|
|
1943
2677
|
const recommended = ["CLAUDE.md", ".cursorrules", "docs/PRD.md", "docs/ARCHITECTURE.md"];
|
|
1944
2678
|
const lines = ["\u{1F50D} \uD504\uB85C\uC81D\uD2B8 \uC810\uAC80", "", "\uD544\uC218:"];
|
|
1945
2679
|
required.forEach((f) => {
|
|
1946
|
-
lines.push(` ${
|
|
2680
|
+
lines.push(` ${existsSync8(f) ? "\u2705" : "\u274C"} ${f}`);
|
|
1947
2681
|
});
|
|
1948
2682
|
lines.push("", "\uAD8C\uC7A5 (VHK \uD558\uB124\uC2A4):");
|
|
1949
2683
|
recommended.forEach((f) => {
|
|
1950
|
-
lines.push(` ${
|
|
2684
|
+
lines.push(` ${existsSync8(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
|
|
1951
2685
|
});
|
|
1952
2686
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
1953
2687
|
});
|
|
@@ -1981,18 +2715,18 @@ ${log.out}` }] };
|
|
|
1981
2715
|
description: ".env \u2192 .env.example \uB3D9\uAE30\uD654 + .gitignore\uC5D0 .env \uC790\uB3D9 \uCD94\uAC00"
|
|
1982
2716
|
},
|
|
1983
2717
|
async () => {
|
|
1984
|
-
if (!
|
|
2718
|
+
if (!existsSync8(".env")) {
|
|
1985
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." }] };
|
|
1986
2720
|
}
|
|
1987
|
-
const keys = parseEnvKeys(
|
|
2721
|
+
const keys = parseEnvKeys(readFileSync5(".env", "utf-8"));
|
|
1988
2722
|
if (keys.length === 0) {
|
|
1989
2723
|
return { content: [{ type: "text", text: "\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
|
|
1990
2724
|
}
|
|
1991
2725
|
const exampleContent = keys.map((k) => `${k}=`).join("\n") + "\n";
|
|
1992
2726
|
writeFileSync3(".env.example", exampleContent, "utf-8");
|
|
1993
2727
|
const gitignoreLines = [];
|
|
1994
|
-
if (
|
|
1995
|
-
const content =
|
|
2728
|
+
if (existsSync8(".gitignore")) {
|
|
2729
|
+
const content = readFileSync5(".gitignore", "utf-8");
|
|
1996
2730
|
if (!content.split("\n").some((l) => l.trim() === ".env")) {
|
|
1997
2731
|
appendFileSync2(".gitignore", "\n.env\n");
|
|
1998
2732
|
gitignoreLines.push("\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428");
|
|
@@ -2012,11 +2746,11 @@ ${log.out}` }] };
|
|
|
2012
2746
|
description: "\uD544\uC218 \uD658\uACBD\uBCC0\uC218 \uB204\uB77D \uAC80\uC0AC (.env.example \uAE30\uC900)"
|
|
2013
2747
|
},
|
|
2014
2748
|
async () => {
|
|
2015
|
-
if (!
|
|
2749
|
+
if (!existsSync8(".env.example")) {
|
|
2016
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." }] };
|
|
2017
2751
|
}
|
|
2018
|
-
const requiredKeys = parseEnvKeys(
|
|
2019
|
-
const currentKeys =
|
|
2752
|
+
const requiredKeys = parseEnvKeys(readFileSync5(".env.example", "utf-8"));
|
|
2753
|
+
const currentKeys = existsSync8(".env") ? parseEnvKeys(readFileSync5(".env", "utf-8")) : [];
|
|
2020
2754
|
const missing = requiredKeys.filter((k) => !currentKeys.includes(k));
|
|
2021
2755
|
const extra = currentKeys.filter((k) => !requiredKeys.includes(k));
|
|
2022
2756
|
const lines = [`\u{1F4CB} \uD544\uC218 \uD658\uACBD\uBCC0\uC218: ${requiredKeys.length}\uAC1C`];
|
|
@@ -2035,7 +2769,9 @@ ${log.out}` }] };
|
|
|
2035
2769
|
);
|
|
2036
2770
|
server.registerTool(
|
|
2037
2771
|
"sync",
|
|
2038
|
-
{
|
|
2772
|
+
{
|
|
2773
|
+
description: "RULES.md \u2192 Cursor\xB7Claude\xB7Windsurf\xB7Copilot\xB7Antigravity\xB7AGENTS.md \uADDC\uCE59 \uB3D9\uAE30\uD654. \uB36E\uC5B4\uC4F0\uAE30 \uC804 \uAE30\uC874 \uD30C\uC77C\uC744 \uC790\uB3D9 \uBC31\uC5C5\uD558\uBBC0\uB85C \uC548\uC804\uD558\uBA70, \uB418\uB3CC\uB9AC\uB824\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C `vhk restore` \uB97C \uC548\uB0B4\uD558\uC138\uC694."
|
|
2774
|
+
},
|
|
2039
2775
|
async () => runVhkCli(["sync"], "sync")
|
|
2040
2776
|
);
|
|
2041
2777
|
server.registerTool(
|
|
@@ -2130,7 +2866,7 @@ ${cliStatus}
|
|
|
2130
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)"
|
|
2131
2867
|
},
|
|
2132
2868
|
async () => {
|
|
2133
|
-
if (!
|
|
2869
|
+
if (!existsSync8("package.json")) {
|
|
2134
2870
|
return { content: [{ type: "text", text: "\u274C package.json \uC5C6\uC74C." }] };
|
|
2135
2871
|
}
|
|
2136
2872
|
try {
|
|
@@ -2158,7 +2894,7 @@ ${cliStatus}
|
|
|
2158
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)"
|
|
2159
2895
|
},
|
|
2160
2896
|
async () => {
|
|
2161
|
-
const current =
|
|
2897
|
+
const current = existsSync8("pnpm-lock.yaml") ? "pnpm" : existsSync8("yarn.lock") ? "yarn" : existsSync8("package-lock.json") ? "npm" : null;
|
|
2162
2898
|
const candidates = ["npm", "yarn", "pnpm"].filter((pm) => pm !== current);
|
|
2163
2899
|
const lines = [`\uD604\uC7AC PM: ${current ?? "\uAC10\uC9C0 \uBD88\uAC00 (lock \uD30C\uC77C \uC5C6\uC74C)"}`];
|
|
2164
2900
|
for (const pm of candidates) {
|
|
@@ -2224,7 +2960,23 @@ export {
|
|
|
2224
2960
|
__toESM,
|
|
2225
2961
|
ko,
|
|
2226
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,
|
|
2227
2978
|
printNextStep,
|
|
2979
|
+
printContextResumeHint,
|
|
2228
2980
|
require_ignore,
|
|
2229
2981
|
printSecurityWarnings,
|
|
2230
2982
|
filterTrackedPaths,
|
|
@@ -2241,5 +2993,6 @@ export {
|
|
|
2241
2993
|
publish,
|
|
2242
2994
|
audit,
|
|
2243
2995
|
getVhkVersion,
|
|
2996
|
+
resolveVhkCliInvocation,
|
|
2244
2997
|
startMcpServer
|
|
2245
2998
|
};
|