@byh3071/vhk 2.3.1 → 2.4.0

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.
@@ -762,7 +762,12 @@ var ko = {
762
762
  skipped: (p) => `\u23ED\uFE0F \uAC74\uB108\uB700: ${p} (\uB36E\uC5B4\uC4F0\uAE30 \uAC70\uBD80 \u2014 \uBC31\uC5C5\uB9CC \uBCF4\uAD00)`,
763
763
  dryRunHeader: "\u{1F50E} \uBBF8\uB9AC\uBCF4\uAE30 (--dry-run) \u2014 \uC2E4\uC81C \uD30C\uC77C \uBCC0\uACBD \uC5C6\uC74C",
764
764
  dryRunWouldWrite: (p, drift) => ` ${drift ? "\u270F\uFE0F \uBCC0\uACBD\uB428" : "\xB7 \uB3D9\uC77C"} : ${p}`,
765
- 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}`
765
+ 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}`,
766
+ // 배치1 — CLAUDE.md 를 vhk 마커(<!-- vhk:rules:start/end -->) 형식으로 1회 정리할 때의 안내.
767
+ // 사용자 섹션은 보존, RULES.md 기준 옛 자동생성 섹션만 재생성 교체(조용한 드롭 방지).
768
+ claudeMigrated: (preserved, removed) => `\u2139\uFE0F CLAUDE.md \uB97C vhk \uB9C8\uCEE4 \uD615\uC2DD\uC73C\uB85C \uC815\uB9AC\uD588\uC5B4\uC694 \u2014 \uB9C8\uCEE4 \uBC16 \uC0AC\uC6A9\uC790 \uC139\uC158\uC740 \uBCF4\uC874\uB429\uB2C8\uB2E4.` + (preserved.length ? `
769
+ \uBCF4\uC874\uB41C \uC0AC\uC6A9\uC790 \uC139\uC158 ${preserved.length}\uAC1C: ${preserved.join(", ")}` : "") + (removed.length ? `
770
+ RULES.md \uAE30\uC900\uC73C\uB85C \uC7AC\uC0DD\uC131\xB7\uAD50\uCCB4\uB41C \uC61B \uC790\uB3D9\uC0DD\uC131 \uC139\uC158 ${removed.length}\uAC1C: ${removed.join(", ")} (\uD544\uC694 \uC2DC .vhk/backups \uC5D0\uC11C \uBCF5\uAD6C)` : "")
766
771
  },
767
772
  restore: {
768
773
  title: "\u{1F6DF} \uBC31\uC5C5 \uBCF5\uC6D0",
@@ -869,7 +874,10 @@ var ko = {
869
874
  testFailed: "\uD14C\uC2A4\uD2B8 \uC2E4\uD328",
870
875
  publishing: "npm \uBC30\uD3EC \uC911...",
871
876
  publishSuccess: "npm \uBC30\uD3EC \uC131\uACF5!",
872
- publishFailed: "npm \uBC30\uD3EC \uC2E4\uD328"
877
+ publishFailed: "npm \uBC30\uD3EC \uC2E4\uD328",
878
+ // 발행 전 안전 가드 — feature 브랜치/미커밋 발행로 픽스 누락본이 latest 로 나가는 사고 방지(v2.3.1 사례)
879
+ preflightWrongBranch: (branch, def) => `\uBC1C\uD589 \uC911\uB2E8 \u2014 \uD604\uC7AC '${branch}' \uBE0C\uB79C\uCE58\uC785\uB2C8\uB2E4. \uBC1C\uD589\uC740 '${def}' \uC5D0\uC11C\uB9CC \uD558\uC138\uC694 (feature \uBE0C\uB79C\uCE58 \uBC1C\uD589 \u2192 \uD53D\uC2A4 \uB204\uB77D\uBCF8\uC774 npm latest \uB85C \uB098\uAC00\uB294 \uC0AC\uACE0 \uBC29\uC9C0). git checkout ${def} && git pull \uD6C4 \uC7AC\uC2DC\uB3C4.`,
880
+ preflightDirty: "\uBC1C\uD589 \uC911\uB2E8 \u2014 \uCEE4\uBC0B \uC548 \uB41C \uBCC0\uACBD\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uBC1C\uD589 \uC804 \uCEE4\uBC0B/\uC815\uB9AC\uD558\uC138\uC694 (untracked \uD30C\uC77C\uC740 \uBB34\uC2DC)."
873
881
  },
874
882
  harness: {
875
883
  title: "\uD1B5\uD569 \uD488\uC9C8 \uC810\uAC80"
@@ -914,6 +922,10 @@ var ko = {
914
922
  learnTitle: "\u{1F9E0} Learning \uAE30\uB85D",
915
923
  resumeTitle: "\u25B6\uFE0F HARD_STOP \uD574\uC81C"
916
924
  },
925
+ work: {
926
+ workTitle: "\u{1F680} vhk work \u2014 \uC791\uC5C5 \uC2DC\uC791/\uC774\uC5B4\uD558\uAE30",
927
+ handoffTitle: "\u23F8\uFE0F vhk work handoff \u2014 \uC911\uB2E8 \uC815\uB9AC"
928
+ },
917
929
  pattern: {
918
930
  detectTitle: "\uD328\uD134 \uAC10\uC9C0",
919
931
  listTitle: "\uD328\uD134 \uBAA9\uB85D",
@@ -1378,29 +1390,80 @@ function toAntigravityRules(sections, projectName) {
1378
1390
  return truncateForAntigravity(buildCodingDoc("Antigravity Rules", sections, projectName));
1379
1391
  }
1380
1392
  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.";
1381
- function toClaudeMd(sections, existing) {
1382
- const recordSections = sections.filter(
1383
- (s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
1384
- );
1385
- const cleaned = existing.split("\n").filter((line) => line.trim() !== CLAUDE_AUTOGEN_BANNER).join("\n");
1386
- const statusMatch = cleaned.match(/## 현재 상태[\s\S]*?(?=\n## |$)/);
1387
- const statusSection = statusMatch ? statusMatch[0].trimEnd() : "";
1388
- const header = cleaned.split("## ")[0].trim();
1389
- const lines = [
1390
- header,
1391
- "",
1392
- statusSection,
1393
- "",
1394
- CLAUDE_AUTOGEN_BANNER,
1395
- ""
1396
- ];
1393
+ var VHK_BLOCK_START = "<!-- vhk:rules:start -->";
1394
+ var VHK_BLOCK_END = "<!-- vhk:rules:end -->";
1395
+ function buildVhkBlock(recordSections) {
1396
+ const lines = [VHK_BLOCK_START, CLAUDE_AUTOGEN_BANNER, ""];
1397
1397
  for (const section of recordSections) {
1398
1398
  lines.push(`## ${section.title}`);
1399
1399
  lines.push(section.content);
1400
1400
  lines.push("");
1401
1401
  }
1402
+ lines.push(VHK_BLOCK_END);
1402
1403
  return lines.join("\n");
1403
1404
  }
1405
+ function splitVhkBlock(existing) {
1406
+ const start = existing.indexOf(VHK_BLOCK_START);
1407
+ const end = existing.indexOf(VHK_BLOCK_END);
1408
+ if (start === -1 || end === -1 || end < start) return null;
1409
+ return {
1410
+ before: existing.slice(0, start),
1411
+ after: existing.slice(end + VHK_BLOCK_END.length)
1412
+ };
1413
+ }
1414
+ function stripLegacyAutogen(existing) {
1415
+ const lines = existing.split("\n").filter((line) => {
1416
+ const t2 = line.trim();
1417
+ return t2 !== CLAUDE_AUTOGEN_BANNER && t2 !== VHK_BLOCK_START && t2 !== VHK_BLOCK_END;
1418
+ });
1419
+ const headerLines = [];
1420
+ const blocks = [];
1421
+ let cur = null;
1422
+ for (const line of lines) {
1423
+ if (line.startsWith("## ")) {
1424
+ if (cur) blocks.push(cur);
1425
+ cur = { title: line.slice(3).trim(), body: [line] };
1426
+ } else if (cur) {
1427
+ cur.body.push(line);
1428
+ } else {
1429
+ headerLines.push(line);
1430
+ }
1431
+ }
1432
+ if (cur) blocks.push(cur);
1433
+ const removed = [];
1434
+ const preserved = [];
1435
+ const keptBodies = [];
1436
+ for (const b of blocks) {
1437
+ if (CLAUDE_MD_KEYS.some((k) => b.title.includes(k))) {
1438
+ removed.push(b.title);
1439
+ } else {
1440
+ preserved.push(b.title);
1441
+ keptBodies.push(b.body.join("\n").trimEnd());
1442
+ }
1443
+ }
1444
+ const header = headerLines.join("\n").trim();
1445
+ const cleaned = [header, ...keptBodies].filter(Boolean).join("\n\n");
1446
+ return { cleaned, removed, preserved };
1447
+ }
1448
+ function claudeMdMigration(existing) {
1449
+ if (splitVhkBlock(existing)) return { migrated: false, removed: [], preserved: [] };
1450
+ const { removed, preserved } = stripLegacyAutogen(existing);
1451
+ return { migrated: true, removed, preserved };
1452
+ }
1453
+ function toClaudeMd(sections, existing) {
1454
+ const recordSections = sections.filter(
1455
+ (s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k))
1456
+ );
1457
+ const vhkBlock = buildVhkBlock(recordSections);
1458
+ const split = splitVhkBlock(existing);
1459
+ if (split) {
1460
+ const before = split.before.replace(/\s+$/, "");
1461
+ const after = split.after.replace(/^\s+/, "").replace(/\s+$/, "");
1462
+ return [before, vhkBlock, after].filter((s) => s.length > 0).join("\n\n") + "\n";
1463
+ }
1464
+ const { cleaned } = stripLegacyAutogen(existing);
1465
+ return [cleaned, vhkBlock].filter((s) => s.length > 0).join("\n\n") + "\n";
1466
+ }
1404
1467
  function toAgentsMd(sections, projectName) {
1405
1468
  const codingSections = sections.filter((s) => CURSORRULES_KEYS.some((k) => s.title.includes(k)));
1406
1469
  const recordSections = sections.filter((s) => CLAUDE_MD_KEYS.some((k) => s.title.includes(k)));
@@ -1472,7 +1535,9 @@ function buildSyncPlan(rootDir, sections, projectName) {
1472
1535
  newContent: claudeNew,
1473
1536
  doneMessage: ko.sync.claudeDone,
1474
1537
  exists: claudeExists,
1475
- drift: claudeDrift
1538
+ drift: claudeDrift,
1539
+ // 기존 CLAUDE.md 가 있을 때만 마이그레이션 집계(첫 생성은 마이그레이션 아님). 추가 I/O 0 — 이미 읽은 existingClaude 재사용.
1540
+ migration: claudeExists ? claudeMdMigration(existingClaude) : void 0
1476
1541
  });
1477
1542
  return plan;
1478
1543
  }
@@ -1483,6 +1548,7 @@ async function syncCore(rootDir, opts, confirmOverwrite) {
1483
1548
  const plan = buildSyncPlan(rootDir, sections, projectName);
1484
1549
  const firstSync = !fs4.existsSync(path4.join(rootDir, SYNCED_MARKER_REL));
1485
1550
  const unmapped = findUnmappedSections(sections);
1551
+ const claudeMigration = plan.find((p) => p.path === "CLAUDE.md")?.migration;
1486
1552
  if (opts.dryRun) {
1487
1553
  return {
1488
1554
  dryRun: true,
@@ -1493,7 +1559,8 @@ async function syncCore(rootDir, opts, confirmOverwrite) {
1493
1559
  skipped: [],
1494
1560
  truncated: [],
1495
1561
  plan,
1496
- unmapped
1562
+ unmapped,
1563
+ claudeMigration
1497
1564
  };
1498
1565
  }
1499
1566
  const toBackup = plan.filter((p) => p.exists && (p.drift || firstSync)).map((p) => p.path);
@@ -1526,7 +1593,7 @@ async function syncCore(rootDir, opts, confirmOverwrite) {
1526
1593
  fs4.mkdirSync(path4.join(rootDir, ".vhk"), { recursive: true });
1527
1594
  fs4.writeFileSync(path4.join(rootDir, SYNCED_MARKER_REL), (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
1528
1595
  ensureVhkIgnored(rootDir, ".synced");
1529
- return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped };
1596
+ return { dryRun: false, firstSync, backupId, backedUp, written, skipped, truncated, plan, unmapped, claudeMigration };
1530
1597
  }
1531
1598
  async function sync(opts = {}) {
1532
1599
  console.log(chalk3.bold(`
@@ -1575,6 +1642,13 @@ ${ko.sync.title}
1575
1642
  )
1576
1643
  );
1577
1644
  }
1645
+ if (result.claudeMigration?.migrated) {
1646
+ console.log(
1647
+ chalk3.cyan(
1648
+ ` ${ko.sync.claudeMigrated(result.claudeMigration.preserved, result.claudeMigration.removed)}`
1649
+ )
1650
+ );
1651
+ }
1578
1652
  if (result.dryRun) {
1579
1653
  console.log(chalk3.cyan(`
1580
1654
  ${ko.sync.dryRunHeader}`));
@@ -1882,7 +1956,7 @@ async function envCheck() {
1882
1956
  }
1883
1957
 
1884
1958
  // src/commands/publish.ts
1885
- import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
1959
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1886
1960
  import chalk6 from "chalk";
1887
1961
  import inquirer3 from "inquirer";
1888
1962
  import ora from "ora";
@@ -1909,8 +1983,26 @@ function bumpVersion(current, type) {
1909
1983
  return `${major}.${minor}.${patch + 1}`;
1910
1984
  }
1911
1985
  }
1986
+ function insertChangelogStub(content, version, date) {
1987
+ const escaped = version.replace(/\./g, "\\.");
1988
+ if (new RegExp(`^## \\[${escaped}\\]`, "m").test(content)) return content;
1989
+ const stub = `## [${version}] - ${date}
1990
+
1991
+ _\uBCC0\uACBD \uB0B4\uC5ED \uC791\uC131 \uD544\uC694._
1992
+
1993
+ `;
1994
+ const firstEntry = content.match(/^## \[\d+\.\d+\.\d+\]/m);
1995
+ if (firstEntry && firstEntry.index !== void 0) {
1996
+ return content.slice(0, firstEntry.index) + stub + content.slice(firstEntry.index);
1997
+ }
1998
+ return content.trimEnd() + "\n\n" + stub;
1999
+ }
2000
+ function bumpClaudeMdVersion(content, newVersion) {
2001
+ return content.replace(/(\*\*버전:\*\*\s*v)\d+\.\d+\.\d+/, `$1${newVersion}`);
2002
+ }
1912
2003
  function gitPostRelease(newVersion) {
1913
- const add = safeExecFile("git", ["add", "package.json"]);
2004
+ const filesToAdd = existsSync4("CHANGELOG.md") ? ["package.json", "CHANGELOG.md"] : ["package.json"];
2005
+ const add = safeExecFile("git", ["add", ...filesToAdd]);
1914
2006
  if (!add.ok) {
1915
2007
  return {
1916
2008
  added: false,
@@ -1951,9 +2043,30 @@ function gitPostRelease(newVersion) {
1951
2043
  pushed: push.ok && pushTags.ok
1952
2044
  };
1953
2045
  }
2046
+ function evaluatePublishPreflight(branch, trackedStatus, defaultBranch) {
2047
+ if (branch !== defaultBranch) return { ok: false, code: "wrong-branch" };
2048
+ if (trackedStatus.trim()) return { ok: false, code: "dirty" };
2049
+ return { ok: true };
2050
+ }
2051
+ function publishPreflight() {
2052
+ const br = safeExecFile("git", ["branch", "--show-current"]);
2053
+ const branch = br.ok ? br.out.trim() : "";
2054
+ const head = safeExecFile("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
2055
+ const defaultBranch = head.ok ? head.out.trim().split("/").pop() || "main" : "main";
2056
+ const st = safeExecFile("git", ["status", "--porcelain", "--untracked-files=no"]);
2057
+ const trackedStatus = st.ok ? st.out : "";
2058
+ return { ...evaluatePublishPreflight(branch, trackedStatus, defaultBranch), branch, defaultBranch };
2059
+ }
1954
2060
  async function publish() {
1955
2061
  console.log(chalk6.bold("\n\u{1F4E6} " + t("publish.title")));
1956
2062
  console.log(chalk6.gray("\u2500".repeat(40)));
2063
+ const pre = publishPreflight();
2064
+ if (!pre.ok) {
2065
+ const msg = pre.code === "wrong-branch" ? t("publish.preflightWrongBranch", pre.branch || "(detached)", pre.defaultBranch) : t("publish.preflightDirty");
2066
+ console.log(chalk6.red(`
2067
+ \u274C ${msg}`));
2068
+ return;
2069
+ }
1957
2070
  if (!existsSync4("package.json")) {
1958
2071
  console.log(chalk6.red("\u274C package.json\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
1959
2072
  return;
@@ -1986,13 +2099,25 @@ async function publish() {
1986
2099
  pkg.version = newVersion;
1987
2100
  writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1988
2101
  console.log(chalk6.green("\u2705 package.json \uBC84\uC804 \uC5C5\uB370\uC774\uD2B8"));
2102
+ const claudeMdOriginal = existsSync4("CLAUDE.md") ? readFileSync4("CLAUDE.md", "utf-8") : null;
2103
+ if (claudeMdOriginal !== null) {
2104
+ const bumped = bumpClaudeMdVersion(claudeMdOriginal, newVersion);
2105
+ if (bumped !== claudeMdOriginal) {
2106
+ writeFileSync2("CLAUDE.md", bumped, "utf-8");
2107
+ console.log(chalk6.green("\u2705 CLAUDE.md \uBC84\uC804\uC904 \uB3D9\uAE30\uD654"));
2108
+ }
2109
+ }
2110
+ const rollbackVersion = () => {
2111
+ pkg.version = currentVersion;
2112
+ writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2113
+ if (claudeMdOriginal !== null) writeFileSync2("CLAUDE.md", claudeMdOriginal, "utf-8");
2114
+ };
1989
2115
  const buildSpinner = ora(t("publish.building")).start();
1990
2116
  const buildResult = safeExecFile("pnpm", ["build"]);
1991
2117
  if (!buildResult.ok) {
1992
2118
  buildSpinner.fail(t("publish.buildFailed"));
1993
2119
  console.log(chalk6.red(buildResult.err.slice(0, 500)));
1994
- pkg.version = currentVersion;
1995
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2120
+ rollbackVersion();
1996
2121
  return;
1997
2122
  }
1998
2123
  buildSpinner.succeed(t("publish.buildSuccess"));
@@ -2001,8 +2126,7 @@ async function publish() {
2001
2126
  if (!testResult.ok) {
2002
2127
  testSpinner.fail(t("publish.testFailed"));
2003
2128
  console.log(chalk6.red(testResult.err.slice(0, 500)));
2004
- pkg.version = currentVersion;
2005
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2129
+ rollbackVersion();
2006
2130
  return;
2007
2131
  }
2008
2132
  testSpinner.succeed(t("publish.testSuccess"));
@@ -2015,8 +2139,7 @@ async function publish() {
2015
2139
  }
2016
2140
  ]);
2017
2141
  if (!confirm) {
2018
- pkg.version = currentVersion;
2019
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2142
+ rollbackVersion();
2020
2143
  console.log(chalk6.gray("\uCDE8\uC18C\uB428. \uBC84\uC804\uC774 \uC6D0\uB798\uB300\uB85C \uBCF5\uAD6C\uB429\uB2C8\uB2E4."));
2021
2144
  return;
2022
2145
  }
@@ -2028,13 +2151,21 @@ async function publish() {
2028
2151
  console.log(chalk6.red(`
2029
2152
  \u2716 ${t("publish.publishFailed")}`));
2030
2153
  console.log(chalk6.red(pubResult.err.slice(0, 500)));
2031
- pkg.version = currentVersion;
2032
- writeFileSync2("package.json", JSON.stringify(pkg, null, 2) + "\n", "utf-8");
2154
+ rollbackVersion();
2033
2155
  console.log(chalk6.gray(`\u{1F4E6} package.json \uBC84\uC804\uC744 v${currentVersion}\uB85C \uBCF5\uAD6C\uD588\uC2B5\uB2C8\uB2E4.`));
2034
2156
  return;
2035
2157
  }
2036
2158
  console.log(chalk6.green(`
2037
2159
  \u2714 ${t("publish.publishSuccess")}`));
2160
+ if (existsSync4("CHANGELOG.md")) {
2161
+ const cl = readFileSync4("CHANGELOG.md", "utf-8");
2162
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2163
+ const updated = insertChangelogStub(cl, newVersion, date);
2164
+ if (updated !== cl) {
2165
+ writeFileSync2("CHANGELOG.md", updated, "utf-8");
2166
+ console.log(chalk6.green(`\u2705 CHANGELOG.md \uC5D0 [${newVersion}] \uC2A4\uD141 \uCD94\uAC00 \u2014 \uBCF8\uBB38 \uBCF4\uAC15 \uD544\uC694`));
2167
+ }
2168
+ }
2038
2169
  const git = gitPostRelease(newVersion);
2039
2170
  if (git.warning) {
2040
2171
  console.log(chalk6.yellow(`