@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.
@@ -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(path3, checkUnignored, mode) {
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(path3);
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 = (path3, originalPath, doThrow) => {
357
- if (!isString(path3)) {
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 (!path3) {
363
+ if (!path7) {
364
364
  return doThrow(`path must not be empty`, TypeError);
365
365
  }
366
- if (checkPath.isNotRelative(path3)) {
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 = (path3) => REGEX_TEST_INVALID_PATH.test(path3);
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 path3 = originalPath && checkPath.convert(originalPath);
405
+ const path7 = originalPath && checkPath.convert(originalPath);
406
406
  checkPath(
407
- path3,
407
+ path7,
408
408
  originalPath,
409
409
  this._strictPathCheck ? throwError : RETURN_FALSE
410
410
  );
411
- return this._t(path3, cache, checkUnignored, slices);
411
+ return this._t(path7, cache, checkUnignored, slices);
412
412
  }
413
- checkIgnore(path3) {
414
- if (!REGEX_TEST_TRAILING_SLASH.test(path3)) {
415
- return this.test(path3);
413
+ checkIgnore(path7) {
414
+ if (!REGEX_TEST_TRAILING_SLASH.test(path7)) {
415
+ return this.test(path7);
416
416
  }
417
- const slices = path3.split(SLASH).filter(Boolean);
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(path3, false, MODE_CHECK_IGNORE);
430
+ return this._rules.test(path7, false, MODE_CHECK_IGNORE);
431
431
  }
432
- _t(path3, cache, checkUnignored, slices) {
433
- if (path3 in cache) {
434
- return cache[path3];
432
+ _t(path7, cache, checkUnignored, slices) {
433
+ if (path7 in cache) {
434
+ return cache[path7];
435
435
  }
436
436
  if (!slices) {
437
- slices = path3.split(SLASH).filter(Boolean);
437
+ slices = path7.split(SLASH).filter(Boolean);
438
438
  }
439
439
  slices.pop();
440
440
  if (!slices.length) {
441
- return cache[path3] = this._rules.test(path3, checkUnignored, MODE_IGNORE);
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[path3] = parent.ignored ? parent : this._rules.test(path3, checkUnignored, MODE_IGNORE);
449
+ return cache[path7] = parent.ignored ? parent : this._rules.test(path7, checkUnignored, MODE_IGNORE);
450
450
  }
451
- ignores(path3) {
452
- return this._test(path3, this._ignoreCache, false).ignored;
451
+ ignores(path7) {
452
+ return this._test(path7, this._ignoreCache, false).ignored;
453
453
  }
454
454
  createFilter() {
455
- return (path3) => !this.ignores(path3);
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(path3) {
462
- return this._test(path3, this._testCache, true);
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 = (path3) => checkPath(path3 && checkPath.convert(path3), path3, RETURN_FALSE);
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 = (path3) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path3) || isNotRelative(path3);
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/deploy.ts
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/exec.ts
492
- import { execFileSync } from "child_process";
493
- var SHIM_BINARIES = /* @__PURE__ */ new Set(["pnpm", "npm", "npx", "yarn"]);
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: "\uCD5C\uADFC \uCEE4\uBC0B (3):",
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. \uCEE4\uBC0B\xB7\uD478\uC2DC\uB97C \uC9C4\uD589\uD558\uC138\uC694.",
577
- nextWithChangesCursor: "\uC800\uC7A5\uD574\uC918",
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(path3) {
925
- const parts = path3.split(".");
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 (existsSync(file)) return key;
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(chalk2.bold("\n\u{1F680} " + t("deploy.title")));
1008
- console.log(chalk2.gray("\u2500".repeat(40)));
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(chalk2.cyan(`
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 inquirer.prompt([
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(chalk2.red(`
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(chalk2.yellow(` \u2192 ${config.installHint}`));
1692
+ console.log(chalk3.yellow(` \u2192 ${config.installHint}`));
1033
1693
  return;
1034
1694
  }
1035
- const { confirm } = await inquirer.prompt([
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(chalk2.gray("\uCDE8\uC18C\uB428"));
1704
+ console.log(chalk3.gray("\uCDE8\uC18C\uB428"));
1045
1705
  return;
1046
1706
  }
1047
- console.log(chalk2.cyan(`
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(chalk2.green(`
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(chalk2.red(`
1720
+ console.log(chalk3.red(`
1061
1721
  \u274C ${t("deploy.failed")}`));
1062
- console.log(chalk2.red(result.err));
1722
+ console.log(chalk3.red(result.err));
1063
1723
  }
1064
1724
  }
1065
1725
 
1066
1726
  // src/commands/env.ts
1067
- import { existsSync as existsSync2, readFileSync, writeFileSync, appendFileSync } from "fs";
1068
- import chalk3 from "chalk";
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 (existsSync2(gitignorePath)) {
1075
- const content = readFileSync(gitignorePath, "utf-8");
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(chalk3.green("\n\u{1F512} .gitignore\uC5D0 .env \uCD94\uAC00\uB428"));
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(chalk3.green("\n\u{1F512} .gitignore \uC0DD\uC131 (.env \uD3EC\uD568)"));
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(chalk3.bold("\n\u{1F510} " + t("env.title")));
1087
- console.log(chalk3.gray("\u2500".repeat(40)));
1088
- if (!existsSync2(".env")) {
1089
- console.log(chalk3.yellow("\n\u26A0\uFE0F .env \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
1090
- console.log(chalk3.gray(" .env \uD30C\uC77C\uC744 \uBA3C\uC800 \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694."));
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 = readFileSync(".env", "utf-8");
1772
+ const envContent = readFileSync2(".env", "utf-8");
1094
1773
  const keys = parseEnvKeys(envContent);
1095
1774
  if (keys.length === 0) {
1096
- console.log(chalk3.yellow("\n\u{1F4ED} .env\uC5D0 \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4."));
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(chalk3.green(`
1780
+ console.log(chalk4.green(`
1102
1781
  \u2705 .env.example \uC0DD\uC131 (${keys.length}\uAC1C \uD0A4)`));
1103
- keys.forEach((k) => console.log(chalk3.gray(` ${k}`)));
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(chalk3.bold("\n\u{1F50D} " + t("env.checkTitle")));
1113
- console.log(chalk3.gray("\u2500".repeat(40)));
1114
- if (!existsSync2(".env.example")) {
1115
- console.log(chalk3.yellow("\n\u26A0\uFE0F .env.example\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 vhk env\uB97C \uC2E4\uD589\uD558\uC138\uC694."));
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(readFileSync(".env.example", "utf-8"));
1119
- const currentKeys = existsSync2(".env") ? parseEnvKeys(readFileSync(".env", "utf-8")) : [];
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(chalk3.cyan(`
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(chalk3.green("\n\u2705 \uBAA8\uB4E0 \uD544\uC218 \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4!"));
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(chalk3.red(`
1806
+ console.log(chalk4.red(`
1128
1807
  \u274C \uB204\uB77D\uB41C \uD658\uACBD\uBCC0\uC218 (${missing.length}\uAC1C):`));
1129
- missing.forEach((k) => console.log(chalk3.red(` \u2022 ${k}`)));
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(chalk3.yellow(`
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(chalk3.yellow(` \u2022 ${k}`)));
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 existsSync3, writeFileSync as writeFileSync2 } from "fs";
1141
- import chalk4 from "chalk";
1142
- import inquirer2 from "inquirer";
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 readFileSync2 } from "fs";
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(readFileSync2(filePath, "utf-8"));
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(chalk4.bold("\n\u{1F4E6} " + t("publish.title")));
1211
- console.log(chalk4.gray("\u2500".repeat(40)));
1212
- if (!existsSync3("package.json")) {
1213
- console.log(chalk4.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
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(chalk4.red("\u274C package.json \uD30C\uC2F1 \uC2E4\uD328"));
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(chalk4.cyan(`
1904
+ console.log(chalk5.cyan(`
1225
1905
  \u{1F4CC} \uD604\uC7AC \uBC84\uC804: v${currentVersion}`));
1226
- const { bumpType } = await inquirer2.prompt([
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(chalk4.cyan(`
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(chalk4.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
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(chalk4.red(buildResult.err.slice(0, 500)));
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(chalk4.red(testResult.err.slice(0, 500)));
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 inquirer2.prompt([
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(chalk4.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
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(chalk4.cyan(`
1958
+ console.log(chalk5.cyan(`
1279
1959
  \u{1F4E4} ${t("publish.publishing")}`));
1280
- console.log(chalk4.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)"));
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(chalk4.red(`
1963
+ console.log(chalk5.red(`
1284
1964
  \u2716 ${t("publish.publishFailed")}`));
1285
- console.log(chalk4.red(pubResult.err.slice(0, 500)));
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(chalk4.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
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(chalk4.green(`
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(chalk4.yellow(`
1975
+ console.log(chalk5.yellow(`
1296
1976
  \u26A0\uFE0F ${git.warning}`));
1297
- console.log(chalk4.dim(` npm \uBC30\uD3EC\uB294 \uC774\uBBF8 \uC131\uACF5\uD588\uC2B5\uB2C8\uB2E4 (v${newVersion}).`));
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(chalk4.green(`
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(chalk4.yellow(`
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(chalk4.green.bold(`
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 existsSync4 } from "fs";
1316
- import chalk5 from "chalk";
1317
- import inquirer3 from "inquirer";
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 (existsSync4("pnpm-lock.yaml")) return "pnpm";
1321
- if (existsSync4("yarn.lock")) return "yarn";
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(chalk5.bold("\n\u{1F6E1}\uFE0F " + t("audit.title")));
1362
- console.log(chalk5.gray("\u2500".repeat(40)));
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(chalk5.cyan(`\u{1F4E6} \uD328\uD0A4\uC9C0 \uB9E4\uB2C8\uC800: ${pm}`));
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(chalk5.green.bold("\n\u{1F389} \uCDE8\uC57D\uC810\uC774 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4!"));
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(chalk5.bold("\n\u{1F4CA} \uCDE8\uC57D\uC810 \uC694\uC57D:"));
1374
- if (summary.critical > 0) console.log(chalk5.red(` \u{1F534} Critical: ${summary.critical}`));
1375
- if (summary.high > 0) console.log(chalk5.red(` \u{1F7E0} High: ${summary.high}`));
1376
- if (summary.moderate > 0) console.log(chalk5.yellow(` \u{1F7E1} Moderate: ${summary.moderate}`));
1377
- if (summary.low > 0) console.log(chalk5.gray(` \u26AA Low: ${summary.low}`));
1378
- console.log(chalk5.bold(`
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 inquirer3.prompt([
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 existsSync5 } from "fs";
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
- join(dir, "../../package.json"),
1412
- join(dir, "../package.json")
2091
+ join3(dir, "../../package.json"),
2092
+ join3(dir, "../package.json")
1413
2093
  ]) {
1414
2094
  try {
1415
- if (existsSync5(pkgPath)) {
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 existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync3, appendFileSync as appendFileSync2 } from "fs";
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 fs3 from "fs";
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 fs2 from "fs";
1500
- import path2 from "path";
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 fs from "fs";
1505
- import path from "path";
1506
- import chalk6 from "chalk";
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 = path.join(rootDir, ".gitignore");
1510
- if (fs.existsSync(gitignorePath)) {
1511
- const content = fs.readFileSync(gitignorePath, "utf-8");
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 = fs.readdirSync(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
1533
- const rel = path.relative(rootDir, fullPath).replace(/\\/g, "/");
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 = path.join(rootDir, ".gitignore");
1556
- const missingGitignore = !fs.existsSync(gitignorePath);
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(chalk6.yellow(` \u26A0\uFE0F ${w}`));
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(path2.extname(fileName).toLowerCase());
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 = fs2.readdirSync(dir, { withFileTypes: true });
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 = path2.join(dir, entry.name);
1639
- const rel = path2.relative(rootDir, fullPath).replace(/\\/g, "/");
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 = fs2.statSync(fullPath).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 = fs3.readFileSync(filePath, "utf-8");
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 result = safeExecFile("vhk", args, { env: { FORCE_COLOR: "0", NO_COLOR: "1" } });
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("undo", { description: "\uCD5C\uADFC \uCEE4\uBC0B \uB418\uB3CC\uB9AC\uAE30 (soft reset, \uBCC0\uACBD\uC0AC\uD56D\uC740 \uC720\uC9C0)" }, async () => {
1799
- if (!isGitRepo()) {
1800
- return { content: [{ type: "text", text: "\u274C git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4." }] };
1801
- }
1802
- const last = safeExecFile("git", ["log", "--oneline", "-1"]);
1803
- if (!last.ok || !last.out) {
1804
- return { content: [{ type: "text", text: "\u{1F4ED} \uB418\uB3CC\uB9B4 \uCEE4\uBC0B\uC774 \uC5C6\uC2B5\uB2C8\uB2E4." }] };
1805
- }
1806
- const reset = safeExecFile("git", ["reset", "--soft", "HEAD~1"]);
1807
- if (!reset.ok) {
1808
- return { content: [{ type: "text", text: `\u274C reset \uC2E4\uD328: ${reset.err}` }] };
1809
- }
1810
- return {
1811
- content: [
1812
- {
1813
- type: "text",
1814
- text: `\u2705 \uB418\uB3CC\uB9AC\uAE30 \uC644\uB8CC!
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 (existsSync6("package.json")) {
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 (existsSync6("package.json")) {
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(` ${existsSync6(f) ? "\u2705" : "\u274C"} ${f}`);
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(` ${existsSync6(f) ? "\u2705" : "\u26A0\uFE0F"} ${f}`);
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 (!existsSync6(".env")) {
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(readFileSync4(".env", "utf-8"));
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 (existsSync6(".gitignore")) {
1995
- const content = readFileSync4(".gitignore", "utf-8");
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 (!existsSync6(".env.example")) {
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(readFileSync4(".env.example", "utf-8"));
2019
- const currentKeys = existsSync6(".env") ? parseEnvKeys(readFileSync4(".env", "utf-8")) : [];
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
- { description: "RULES.md \u2192 .cursorrules + CLAUDE.md \uC790\uB3D9 \uB3D9\uAE30\uD654" },
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 (!existsSync6("package.json")) {
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 = existsSync6("pnpm-lock.yaml") ? "pnpm" : existsSync6("yarn.lock") ? "yarn" : existsSync6("package-lock.json") ? "npm" : null;
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
  };