@byh3071/vhk 1.6.6 → 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.
@@ -2987,6 +2987,7 @@ export {
2987
2987
  ko,
2988
2988
  t,
2989
2989
  localDate,
2990
+ ensureVhkIgnored,
2990
2991
  listBackups,
2991
2992
  restoreBackup,
2992
2993
  detectExistingRuleFiles,
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  deploy,
14
14
  detectExistingRuleFiles,
15
15
  ensureInteractive,
16
+ ensureVhkIgnored,
16
17
  env,
17
18
  envCheck,
18
19
  filterSevereFindings,
@@ -42,7 +43,7 @@ import {
42
43
  stripBom,
43
44
  sync,
44
45
  t
45
- } from "./chunk-HLUFOT2T.js";
46
+ } from "./chunk-GXFZ7JXX.js";
46
47
 
47
48
  // src/index.ts
48
49
  import { Command, Help } from "commander";
@@ -2660,7 +2661,9 @@ function generateGateScript(id) {
2660
2661
  " process.exit(1)",
2661
2662
  "}",
2662
2663
  "",
2663
- "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') : {}",
2664
2667
  "const scripts = pkg.scripts ?? {}",
2665
2668
  "const pm = existsSync('pnpm-lock.yaml') ? 'pnpm' : existsSync('yarn.lock') ? 'yarn' : 'npm'",
2666
2669
  "const skipDeep = process.env.VHK_GATES_SKIP_DEEP === '1'",
@@ -5029,7 +5032,7 @@ function readCloudConfig(rootDir) {
5029
5032
  const p = path12.join(rootDir, VHK_DIR2, CLOUD_CONFIG_FILE);
5030
5033
  if (!fs11.existsSync(p)) return null;
5031
5034
  try {
5032
- const parsed = JSON.parse(fs11.readFileSync(p, "utf-8"));
5035
+ const parsed = readJsonFile(p);
5033
5036
  if (parsed && typeof parsed.gistId === "string" && parsed.gistId) {
5034
5037
  return { gistId: parsed.gistId };
5035
5038
  }
@@ -5359,30 +5362,373 @@ async function mode(target) {
5359
5362
  }
5360
5363
 
5361
5364
  // src/commands/verify.ts
5365
+ import { execFileSync as execFileSync4 } from "child_process";
5366
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "fs";
5367
+ import { join as join10 } from "path";
5362
5368
  import chalk30 from "chalk";
5363
- function verificationChecklist() {
5364
- return [
5365
- "\uD0C0\uC785 \uCCB4\uD06C \u2014 pnpm exec tsc --noEmit",
5366
- "\uD14C\uC2A4\uD2B8 \u2014 pnpm run test:run",
5367
- "\uBE4C\uB4DC \u2014 pnpm run build",
5368
- "\uBCF4\uC548 \uC2A4\uCE94 \u2014 vhk secure scan"
5369
- ];
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
5475
+ var REPORT_SCHEMA_VERSION = 1;
5476
+ var REPORT_DIR_REL = join10(".vhk", "reports");
5477
+ var REPORT_PATH_REL = join10(REPORT_DIR_REL, "latest.json");
5478
+ var REPORT_HTML_PATH_REL = join10(REPORT_DIR_REL, "latest.html");
5479
+ var SHIM = /* @__PURE__ */ new Set(["pnpm", "npm", "npx", "yarn"]);
5480
+ function detectPm(cwd) {
5481
+ if (existsSync16(join10(cwd, "pnpm-lock.yaml"))) return "pnpm";
5482
+ if (existsSync16(join10(cwd, "yarn.lock"))) return "yarn";
5483
+ return "npm";
5484
+ }
5485
+ function execGate(cmd, args, cwd) {
5486
+ let bin = cmd;
5487
+ let argv = args;
5488
+ if (process.platform === "win32" && SHIM.has(cmd)) {
5489
+ bin = "cmd.exe";
5490
+ argv = ["/d", "/s", "/c", `${cmd}.cmd`, ...args];
5491
+ }
5492
+ try {
5493
+ execFileSync4(bin, argv, {
5494
+ cwd,
5495
+ stdio: ["pipe", "pipe", "pipe"],
5496
+ encoding: "utf-8",
5497
+ maxBuffer: 64 * 1024 * 1024,
5498
+ timeout: 6e5,
5499
+ killSignal: "SIGTERM"
5500
+ });
5501
+ return { exitCode: 0, out: "" };
5502
+ } catch (e) {
5503
+ const err = e;
5504
+ const exitCode = typeof err.status === "number" ? err.status : 1;
5505
+ const out = ((err.stdout?.toString?.() ?? "") + (err.stderr?.toString?.() ?? "")).trim();
5506
+ return { exitCode, out };
5507
+ }
5370
5508
  }
5371
- async function verify() {
5372
- console.log(chalk30.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify \u2014 lite)"));
5509
+ function runScriptGate(id, label, cwd, pm, argvFor) {
5510
+ const argv = argvFor(pm);
5511
+ if (!argv) {
5512
+ return { id, label, status: "skip", exitCode: null, skipped: true, detail: "\uD574\uB2F9 \uC2A4\uD06C\uB9BD\uD2B8/\uC124\uC815 \uC5C6\uC74C \u2014 skip(WARN)" };
5513
+ }
5514
+ const { exitCode } = execGate(pm, argv, cwd);
5515
+ return {
5516
+ id,
5517
+ label,
5518
+ status: exitCode === 0 ? "pass" : "fail",
5519
+ exitCode,
5520
+ skipped: false,
5521
+ detail: exitCode === 0 ? void 0 : `\uC885\uB8CC\uCF54\uB4DC ${exitCode}`
5522
+ };
5523
+ }
5524
+ function readPackageScripts(cwd) {
5525
+ const pkgPath = join10(cwd, "package.json");
5526
+ if (!existsSync16(pkgPath)) return {};
5527
+ try {
5528
+ const pkg = readJsonFile(pkgPath);
5529
+ return pkg.scripts ?? {};
5530
+ } catch {
5531
+ return {};
5532
+ }
5533
+ }
5534
+ function runGates(cwd) {
5535
+ const scripts = readPackageScripts(cwd);
5536
+ const pm = detectPm(cwd);
5537
+ const gates = [];
5538
+ gates.push(
5539
+ runScriptGate("typecheck", "tsc --noEmit", cwd, pm, () => {
5540
+ if (scripts.typecheck) return ["run", "typecheck"];
5541
+ if (existsSync16(join10(cwd, "tsconfig.json"))) return pm === "npm" ? ["exec", "--", "tsc", "--noEmit"] : ["exec", "tsc", "--noEmit"];
5542
+ return null;
5543
+ })
5544
+ );
5545
+ gates.push(
5546
+ runScriptGate("test", "test:run", cwd, pm, () => {
5547
+ if (scripts["test:run"]) return ["run", "test:run"];
5548
+ if (scripts.test && /vitest/.test(scripts.test)) return ["run", "test", "--", "--run"];
5549
+ if (scripts.test) return ["run", "test"];
5550
+ return null;
5551
+ })
5552
+ );
5553
+ gates.push(
5554
+ runScriptGate("build", "build", cwd, pm, () => scripts.build ? ["run", "build"] : null)
5555
+ );
5556
+ gates.push(runSecureGate(cwd));
5557
+ return gates;
5558
+ }
5559
+ function runSecureGate(cwd) {
5560
+ try {
5561
+ const severe = filterSevereFindings(scanProjectForSecrets(cwd).findings);
5562
+ const n = severe.length;
5563
+ return {
5564
+ id: "secure",
5565
+ label: "secure scan",
5566
+ status: n === 0 ? "pass" : "fail",
5567
+ exitCode: n === 0 ? 0 : 1,
5568
+ skipped: false,
5569
+ detail: n === 0 ? void 0 : `severe \uC2DC\uD06C\uB9BF ${n}\uAC74 (\uAC12 \uBBF8\uAE30\uB85D \u2014 vhk secure scan \uC73C\uB85C \uD655\uC778)`
5570
+ };
5571
+ } catch (e) {
5572
+ return {
5573
+ id: "secure",
5574
+ label: "secure scan",
5575
+ status: "fail",
5576
+ exitCode: 1,
5577
+ skipped: false,
5578
+ detail: `\uC2A4\uCE94 \uC2E4\uD589 \uC2E4\uD328: ${e instanceof Error ? e.message : String(e)}`
5579
+ };
5580
+ }
5581
+ }
5582
+ function aggregateStatus(gates) {
5583
+ if (gates.some((g) => g.status === "fail")) return "FAIL";
5584
+ if (gates.some((g) => g.status === "skip")) return "WARN";
5585
+ return "PASS";
5586
+ }
5587
+ function buildNextActions(gates) {
5588
+ const actions = [];
5589
+ for (const g of gates) {
5590
+ if (g.status === "fail") {
5591
+ if (g.id === "secure") actions.push("\uC2DC\uD06C\uB9BF \uC81C\uAC70 \uD6C4 \uC7AC\uAC80\uC99D \u2014 vhk secure scan \uC73C\uB85C \uC704\uCE58 \uD655\uC778");
5592
+ else actions.push(`${g.label} \uC2E4\uD328(\uC885\uB8CC\uCF54\uB4DC ${g.exitCode}) \u2014 \uB85C\uADF8 \uD655\uC778 \uD6C4 \uC218\uC815`);
5593
+ } else if (g.status === "skip") {
5594
+ actions.push(`${g.label} \uAC8C\uC774\uD2B8 \uC5C6\uC74C \u2014 package.json scripts \uC5D0 \uCD94\uAC00\uD558\uBA74 \uAC80\uC99D \uCEE4\uBC84\uB9AC\uC9C0 \u2191`);
5595
+ }
5596
+ }
5597
+ if (actions.length === 0) actions.push("\uAC80\uC99D \uD1B5\uACFC \u2014 vhk save \uB85C \uC800\uC7A5\uD558\uC138\uC694.");
5598
+ return actions;
5599
+ }
5600
+ function buildReport(gates, generatedAt, date) {
5601
+ const summary = {
5602
+ total: gates.length,
5603
+ pass: gates.filter((g) => g.status === "pass").length,
5604
+ fail: gates.filter((g) => g.status === "fail").length,
5605
+ skip: gates.filter((g) => g.status === "skip").length
5606
+ };
5607
+ return {
5608
+ schemaVersion: REPORT_SCHEMA_VERSION,
5609
+ generatedAt,
5610
+ date,
5611
+ status: aggregateStatus(gates),
5612
+ summary,
5613
+ gates,
5614
+ nextActions: buildNextActions(gates)
5615
+ };
5616
+ }
5617
+ function verifyEvidence(cwd = process.cwd()) {
5618
+ const gates = runGates(cwd);
5619
+ const report = buildReport(gates, (/* @__PURE__ */ new Date()).toISOString(), localDate());
5620
+ const dir = join10(cwd, REPORT_DIR_REL);
5621
+ mkdirSync11(dir, { recursive: true });
5622
+ const path14 = join10(cwd, REPORT_PATH_REL);
5623
+ writeFileSync11(path14, JSON.stringify(report, null, 2) + "\n", "utf-8");
5624
+ try {
5625
+ ensureVhkIgnored(cwd, "reports/");
5626
+ } catch {
5627
+ }
5628
+ return { report, path: REPORT_PATH_REL };
5629
+ }
5630
+ var STATUS_BADGE = {
5631
+ PASS: chalk30.green.bold("PASS"),
5632
+ WARN: chalk30.yellow.bold("WARN"),
5633
+ FAIL: chalk30.red.bold("FAIL")
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
+ }
5689
+ async function verify(opts = {}) {
5690
+ if (!ensureNotHardStopped("verify")) return;
5691
+ const cwd = process.cwd();
5692
+ if (opts.report) {
5693
+ await renderVerifyReport(cwd, opts);
5694
+ return;
5695
+ }
5696
+ const { report, path: path14 } = verifyEvidence(cwd);
5697
+ if (opts.json) {
5698
+ console.log(JSON.stringify(report, null, 2));
5699
+ process.exitCode = report.status === "FAIL" ? 1 : 0;
5700
+ return;
5701
+ }
5702
+ console.log(chalk30.bold("\n\u{1F50E} \uAC80\uC99D \uBB36\uC74C (verify)"));
5373
5703
  console.log(chalk30.gray("\u2500".repeat(40)));
5374
5704
  const mode2 = readConfig().safetyMode;
5375
5705
  console.log(chalk30.dim(` \uD604\uC7AC Safety Mode: ${mode2} \u2014 ${SAFETY_MODE_DESC[mode2]}`));
5376
- console.log(chalk30.cyan("\n \uC704\uD5D8 \uC791\uC5C5/\uC800\uC7A5 \uC804 \uAD8C\uC7A5 \uAC80\uC99D:"));
5377
- for (const item of verificationChecklist()) {
5378
- console.log(` \u2610 ${item}`);
5706
+ const icon = (s2) => s2 === "pass" ? chalk30.green("\u2713") : s2 === "fail" ? chalk30.red("\u2717") : chalk30.yellow("\u2298");
5707
+ for (const g of report.gates) {
5708
+ const tail = g.detail ? chalk30.dim(` \u2014 ${g.detail}`) : "";
5709
+ console.log(` ${icon(g.status)} ${g.label}${tail}`);
5710
+ }
5711
+ const s = report.summary;
5712
+ console.log(
5713
+ `
5714
+ \uACB0\uACFC: ${STATUS_BADGE[report.status]} ` + chalk30.dim(`(pass ${s.pass} / fail ${s.fail} / skip ${s.skip}, \uCD1D ${s.total})`)
5715
+ );
5716
+ console.log(chalk30.dim(` \u{1F4C4} \uC99D\uAC70: ${path14}`));
5717
+ process.exitCode = report.status === "FAIL" ? 1 : 0;
5718
+ if (report.status === "FAIL") {
5719
+ printNextStep({
5720
+ message: "\uAC80\uC99D \uC2E4\uD328 \u2014 \uC544\uB798\uB97C \uBA3C\uC800 \uACE0\uCE58\uC138\uC694:",
5721
+ command: "vhk verify",
5722
+ cursorHint: "\uAC80\uC99D \uB2E4\uC2DC \uB3CC\uB824\uC918",
5723
+ alternative: report.nextActions[0]
5724
+ });
5725
+ } else {
5726
+ printNextStep({
5727
+ message: report.status === "WARN" ? "\uAC80\uC99D \uD1B5\uACFC(\uC77C\uBD80 \uAC8C\uC774\uD2B8 skip). \uC800\uC7A5\uD558\uB824\uBA74:" : "\uAC80\uC99D \uD1B5\uACFC! \uC800\uC7A5\uD558\uB824\uBA74:",
5728
+ command: "vhk save",
5729
+ cursorHint: "\uC800\uC7A5\uD574\uC918"
5730
+ });
5379
5731
  }
5380
- console.log(chalk30.dim("\n \u203B \uBA54\uD0C0\uB7EC\uB108(\uC790\uB3D9 \uC2E4\uD589) \uC790\uB9AC \u2014 \uD604\uC7AC\uB294 \uBB36\uC74C \uC548\uB0B4\uB9CC(lite)."));
5381
- printNextStep({
5382
- message: "\uAC80\uC99D \uD1B5\uACFC \uD6C4 \uC800\uC7A5\uD558\uC138\uC694:",
5383
- command: "vhk save",
5384
- cursorHint: "\uC800\uC7A5\uD574\uC918"
5385
- });
5386
5732
  }
5387
5733
 
5388
5734
  // src/lib/risk-policy.ts
@@ -5861,8 +6207,8 @@ program.command("context").alias("\uB9E5\uB77D").option("--compact", "\uD1A0\uD0
5861
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) => {
5862
6208
  await mode(target);
5863
6209
  });
5864
- program.command("verify").alias("\uC0AC\uC804\uC810\uAC80").description("\uC800\uC7A5/\uC704\uD5D8 \uC791\uC5C5 \uC804 \uAC80\uC99D \uBB36\uC74C \uC548\uB0B4 (lite)").action(async () => {
5865
- await verify();
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) => {
6211
+ await verify(opts);
5866
6212
  });
5867
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 () => {
5868
6214
  await contextShow();
package/dist/mcp/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  resolveVhkCliInvocation,
4
4
  startMcpServer
5
- } from "../chunk-HLUFOT2T.js";
5
+ } from "../chunk-GXFZ7JXX.js";
6
6
 
7
7
  // src/mcp/index.ts
8
8
  var cli = resolveVhkCliInvocation();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byh3071/vhk",
3
- "version": "1.6.6",
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",