@byh3071/vhk 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +170 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2661,7 +2661,9 @@ function generateGateScript(id) {
2661
2661
  " process.exit(1)",
2662
2662
  "}",
2663
2663
  "",
2664
- "const pkg = existsSync('package.json') ? JSON.parse(readFileSync('package.json', 'utf-8')) : {}",
2664
+ "// BOM-safe \uC77D\uAE30: PowerShell Set-Content -Encoding utf8 \uC758 UTF-8 BOM \uC81C\uAC70(\uC5C6\uC73C\uBA74 throw).",
2665
+ "const readJson = (p) => { const t = readFileSync(p, 'utf-8'); return JSON.parse(t.charCodeAt(0) === 0xfeff ? t.slice(1) : t) }",
2666
+ "const pkg = existsSync('package.json') ? readJson('package.json') : {}",
2665
2667
  "const scripts = pkg.scripts ?? {}",
2666
2668
  "const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
2667
2669
  "const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
@@ -5030,7 +5032,7 @@ function readCloudConfig(rootDir) {
5030
5032
  const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5031
5033
  if (!fs11.existsSync(p)) return null;
5032
5034
  try {
5033
- const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
5035
+ const parsed = readJsonFile(p);
5034
5036
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
5035
5037
  return { gistId: parsed.gistId };
5036
5038
  }
@@ -5364,9 +5366,116 @@ import { execFileSync as execFileSync4 } from "child_process";
5364
5366
  import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
5365
5367
  import { join as join10 } from "path";
5366
5368
  import chalk30 from "chalk";
5369
+
5370
+ // src/commands/verify-report.ts
5371
+ function escapeHtml(text) {
5372
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5373
+ }
5374
+ var STATUS_COLOR = {
5375
+ PASS: { bg: "#16a34a", fg: "#ffffff", label: "PASS" },
5376
+ WARN: { bg: "#d97706", fg: "#ffffff", label: "WARN" },
5377
+ FAIL: { bg: "#dc2626", fg: "#ffffff", label: "FAIL" }
5378
+ };
5379
+ var GATE_STATUS = {
5380
+ pass: { color: "#16a34a", label: "\uD1B5\uACFC" },
5381
+ fail: { color: "#dc2626", label: "\uC2E4\uD328" },
5382
+ skip: { color: "#d97706", label: "\uAC74\uB108\uB700" }
5383
+ };
5384
+ function renderGateRow(g) {
5385
+ const s = GATE_STATUS[g.status];
5386
+ const exit = g.exitCode === null ? "\u2014" : String(g.exitCode);
5387
+ const detail = g.detail ? escapeHtml(g.detail) : "";
5388
+ return ` <tr>
5389
+ <td class="gate-label">${escapeHtml(g.label)}</td>
5390
+ <td><span class="gate-status" style="color:${s.color}">${s.label}</span></td>
5391
+ <td class="gate-exit">${escapeHtml(exit)}</td>
5392
+ <td class="gate-detail">${detail}</td>
5393
+ </tr>`;
5394
+ }
5395
+ function renderReportHtml(report) {
5396
+ const c = STATUS_COLOR[report.status];
5397
+ const s = report.summary;
5398
+ const rows = report.gates.map(renderGateRow).join("\n");
5399
+ const actions = report.nextActions.map((a) => ` <li>${escapeHtml(a)}</li>`).join("\n");
5400
+ return `<!DOCTYPE html>
5401
+ <html lang="ko">
5402
+ <head>
5403
+ <meta charset="utf-8">
5404
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5405
+ <title>vhk verify \uB9AC\uD3EC\uD2B8 \u2014 ${c.label}</title>
5406
+ <style>
5407
+ :root { color-scheme: light dark; }
5408
+ * { box-sizing: border-box; }
5409
+ body {
5410
+ margin: 0; padding: 2rem 1rem;
5411
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;
5412
+ background: #f8fafc; color: #0f172a; line-height: 1.55;
5413
+ }
5414
+ .wrap { max-width: 760px; margin: 0 auto; }
5415
+ header { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
5416
+ h1 { font-size: 1.25rem; margin: 0; font-weight: 700; }
5417
+ .badge {
5418
+ display: inline-block; padding: 0.35rem 1rem; border-radius: 9999px;
5419
+ font-weight: 800; font-size: 1rem; letter-spacing: 0.05em;
5420
+ background: ${c.bg}; color: ${c.fg};
5421
+ }
5422
+ .summary { color: #475569; font-size: 0.95rem; margin: 0.25rem 0 1.5rem; }
5423
+ .summary strong { color: #0f172a; }
5424
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1.75rem; font-size: 0.95rem; }
5425
+ th, td { text-align: left; padding: 0.6rem 0.75rem; border-bottom: 1px solid #e2e8f0; }
5426
+ th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; }
5427
+ .gate-label { font-weight: 600; }
5428
+ .gate-status { font-weight: 700; }
5429
+ .gate-exit { font-variant-numeric: tabular-nums; color: #475569; }
5430
+ .gate-detail { color: #64748b; font-size: 0.88rem; }
5431
+ h2 { font-size: 1rem; margin: 0 0 0.5rem; }
5432
+ ul { margin: 0 0 1.75rem; padding-left: 1.25rem; }
5433
+ li { margin: 0.2rem 0; }
5434
+ footer { color: #94a3b8; font-size: 0.8rem; border-top: 1px solid #e2e8f0; padding-top: 0.75rem; }
5435
+ @media (prefers-color-scheme: dark) {
5436
+ body { background: #0f172a; color: #e2e8f0; }
5437
+ .summary { color: #94a3b8; } .summary strong { color: #e2e8f0; }
5438
+ th { color: #94a3b8; } th, td { border-color: #1e293b; }
5439
+ .gate-exit { color: #94a3b8; } .gate-detail { color: #94a3b8; }
5440
+ footer { color: #64748b; border-color: #1e293b; }
5441
+ }
5442
+ </style>
5443
+ </head>
5444
+ <body>
5445
+ <div class="wrap">
5446
+ <header>
5447
+ <h1>\u{1F50E} vhk verify \uB9AC\uD3EC\uD2B8</h1>
5448
+ <span class="badge">${c.label}</span>
5449
+ </header>
5450
+ <p class="summary">
5451
+ \uAC8C\uC774\uD2B8 <strong>${s.total}</strong>\uAC1C \u2014 \uD1B5\uACFC ${s.pass} / \uC2E4\uD328 ${s.fail} / \uAC74\uB108\uB700 ${s.skip}
5452
+ </p>
5453
+ <table>
5454
+ <thead>
5455
+ <tr><th>\uAC8C\uC774\uD2B8</th><th>\uC0C1\uD0DC</th><th>\uC885\uB8CC\uCF54\uB4DC</th><th>\uBE44\uACE0</th></tr>
5456
+ </thead>
5457
+ <tbody>
5458
+ ${rows}
5459
+ </tbody>
5460
+ </table>
5461
+ <h2>\uB2E4\uC74C \uD589\uB3D9</h2>
5462
+ <ul>
5463
+ ${actions}
5464
+ </ul>
5465
+ <footer>
5466
+ \uC0DD\uC131: ${escapeHtml(report.generatedAt)} \xB7 \uB0A0\uC9DC: ${escapeHtml(report.date)} \xB7 schema v${report.schemaVersion}
5467
+ </footer>
5468
+ </div>
5469
+ </body>
5470
+ </html>
5471
+ `;
5472
+ }
5473
+
5474
+ // src/commands/verify.ts
5367
5475
  var REPORT_SCHEMA_VERSION = 1;
5368
5476
  var REPORT_DIR_REL = join10(".vhk", "reports");
5369
5477
  var REPORT_PATH_REL = join10(REPORT_DIR_REL, "latest.json");
5478
+ var REPORT_HTML_PATH_REL = join10(REPORT_DIR_REL, "latest.html");
5370
5479
  var SHIM = /* @__PURE__ */ new Set(["pnpm", "npm", "npx", "yarn"]);
5371
5480
  function detectPm(cwd) {
5372
5481
  if (existsSync16(join10(cwd, "pnpm-lock.yaml"))) return "pnpm";
@@ -5523,9 +5632,67 @@ var STATUS_BADGE = {
5523
5632
  WARN: chalk30.yellow.bold("WARN"),
5524
5633
  FAIL: chalk30.red.bold("FAIL")
5525
5634
  };
5635
+ async function renderVerifyReport(cwd, opts) {
5636
+ const jsonPath = join10(cwd, REPORT_PATH_REL);
5637
+ let report;
5638
+ if (existsSync16(jsonPath)) {
5639
+ try {
5640
+ report = readJsonFile(jsonPath);
5641
+ } catch {
5642
+ console.log(chalk30.yellow(" \u26A0\uFE0F \uAE30\uC874 latest.json \uC190\uC0C1 \u2014 verify \uC7AC\uC2E4\uD589\uC73C\uB85C \uC99D\uAC70\uB97C \uB2E4\uC2DC \uB9CC\uB4ED\uB2C8\uB2E4."));
5643
+ report = verifyEvidence(cwd).report;
5644
+ }
5645
+ } else {
5646
+ console.log(chalk30.dim(" latest.json \uC5C6\uC74C \u2014 verify 1\uD68C \uC120\uC2E4\uD589\uC73C\uB85C \uC99D\uAC70\uB97C \uB9CC\uB4ED\uB2C8\uB2E4."));
5647
+ report = verifyEvidence(cwd).report;
5648
+ }
5649
+ const html = renderReportHtml(report);
5650
+ const htmlPath = join10(cwd, REPORT_HTML_PATH_REL);
5651
+ try {
5652
+ mkdirSync11(join10(cwd, REPORT_DIR_REL), { recursive: true });
5653
+ writeFileSync11(htmlPath, html, "utf-8");
5654
+ } catch (e) {
5655
+ console.error(
5656
+ chalk30.red(` \u274C \uB9AC\uD3EC\uD2B8 HTML \uC744 \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (${REPORT_HTML_PATH_REL}): ${e instanceof Error ? e.message : String(e)}`)
5657
+ );
5658
+ console.error(chalk30.dim(" \uD574\uB2F9 \uACBD\uB85C\uC758 \uC4F0\uAE30 \uAD8C\uD55C\uC744 \uD655\uC778\uD558\uC138\uC694."));
5659
+ process.exitCode = 1;
5660
+ return;
5661
+ }
5662
+ console.log(chalk30.bold("\n\u{1F50E} \uAC80\uC99D \uB9AC\uD3EC\uD2B8 (verify --report)"));
5663
+ console.log(` \uACB0\uACFC: ${STATUS_BADGE[report.status]}`);
5664
+ console.log(chalk30.dim(` \u{1F4C4} HTML: ${REPORT_HTML_PATH_REL}`));
5665
+ process.exitCode = report.status === "FAIL" ? 1 : 0;
5666
+ if (opts.open) {
5667
+ if (isInteractive()) openReportInBrowser(htmlPath);
5668
+ else console.log(chalk30.dim(" (\uBE44\uB300\uD654\uD615/CI/MCP \u2014 --open \uC790\uB3D9 \uC2A4\uD0B5)"));
5669
+ return;
5670
+ }
5671
+ printNextStep({
5672
+ message: "\uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uC644\uB8CC. \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uB824\uBA74:",
5673
+ command: "vhk verify --report --open",
5674
+ cursorHint: "\uB9AC\uD3EC\uD2B8 \uC5F4\uC5B4\uC918"
5675
+ });
5676
+ }
5677
+ function openReportInBrowser(filePath) {
5678
+ let result;
5679
+ if (process.platform === "darwin") {
5680
+ result = safeExecFile("open", [filePath]);
5681
+ } else if (process.platform === "win32") {
5682
+ result = safeExecFile("rundll32.exe", ["url.dll,FileProtocolHandler", filePath]);
5683
+ } else {
5684
+ result = safeExecFile("xdg-open", [filePath]);
5685
+ }
5686
+ if (result.ok) console.log(chalk30.green(" \u2705 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5C8\uC2B5\uB2C8\uB2E4."));
5687
+ else console.log(chalk30.yellow(" \u26A0\uFE0F \uBE0C\uB77C\uC6B0\uC800\uB97C \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC704 \uD30C\uC77C\uC744 \uC9C1\uC811 \uC5EC\uC138\uC694."));
5688
+ }
5526
5689
  async function verify(opts = {}) {
5527
5690
  if (!ensureNotHardStopped("verify")) return;
5528
5691
  const cwd = process.cwd();
5692
+ if (opts.report) {
5693
+ await renderVerifyReport(cwd, opts);
5694
+ return;
5695
+ }
5529
5696
  const { report, path: path14 } = verifyEvidence(cwd);
5530
5697
  if (opts.json) {
5531
5698
  console.log(JSON.stringify(report, null, 2));
@@ -6040,7 +6207,7 @@ program.command("context").alias("\uB9E5\uB77D").option("--compact", "\uD1A0\uD0
6040
6207
  program.command("mode [target]").alias("\uBAA8\uB4DC").description("Safety Mode \uC870\uD68C/\uBCC0\uACBD (lite|standard|strict) \u2014 \uC704\uD5D8 \uC791\uC5C5 \uAC00\uB4DC \uAC15\uB3C4").action(async (target) => {
6041
6208
  await mode(target);
6042
6209
  });
6043
- program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").option("--json", "\uB9AC\uD3EC\uD2B8 JSON \uC744 stdout \uC73C\uB85C \uCD9C\uB825 (CI\uC6A9 \u2014 \uACBD\uB85C \uB300\uC2E0)").description("\uAC80\uC99D \uAC8C\uC774\uD2B8(tsc/test/build/secure) \uC2E4\uC81C \uC2E4\uD589 + \uC99D\uAC70 \uAE30\uB85D (.vhk/reports/latest.json)").action(async (opts) => {
6210
+ program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").option("--json", "\uB9AC\uD3EC\uD2B8 JSON \uC744 stdout \uC73C\uB85C \uCD9C\uB825 (CI\uC6A9 \u2014 \uACBD\uB85C \uB300\uC2E0)").option("--report", "latest.json \uC744 \uC0AC\uB78C\uC6A9 \uC815\uC801 HTML(.vhk/reports/latest.html) \uB85C \uB80C\uB354 (\uC678\uBD80 \uC758\uC874 0)").option("--open", "\uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uD6C4 \uAE30\uBCF8 \uBE0C\uB77C\uC6B0\uC800\uB85C \uC5F4\uAE30 (\uBE44\uB300\uD654\uD615/CI/MCP \uC790\uB3D9 \uC2A4\uD0B5)").description("\uAC80\uC99D \uAC8C\uC774\uD2B8(tsc/test/build/secure) \uC2E4\uC81C \uC2E4\uD589 + \uC99D\uAC70 \uAE30\uB85D (.vhk/reports/latest.json)").action(async (opts) => {
6044
6211
  await verify(opts);
6045
6212
  });
6046
6213
  program.command("context-show").alias("\uB9E5\uB77D\uBCF4\uAE30").description("\uD604\uC7AC \uCEE8\uD14D\uC2A4\uD2B8 \uD30C\uC77C \uB0B4\uC6A9 \uCD9C\uB825").action(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Vibe Harness Kit — AI 코딩 도구·기기를 바꿔도 규칙·맥락이 따라가는 포터빌리티 CLI (sync: Cursor·Claude·Windsurf·Copilot·Antigravity / cloud 백업)",
5
5
  "bin": {
6
6
  "vhk": "dist/index.js",