@vulcn/plugin-report 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1641,6 +1641,65 @@ function getFormats(format) {
1641
1641
  if (format === "all") return ["html", "json", "yaml", "sarif"];
1642
1642
  return [format];
1643
1643
  }
1644
+ async function writeReports(report, config, logger) {
1645
+ const formats = getFormats(config.format);
1646
+ const outDir = (0, import_node_path.resolve)(config.outputDir);
1647
+ await (0, import_promises.mkdir)(outDir, { recursive: true });
1648
+ const basePath = (0, import_node_path.resolve)(outDir, config.filename);
1649
+ const writtenFiles = [];
1650
+ for (const fmt of formats) {
1651
+ try {
1652
+ switch (fmt) {
1653
+ case "html": {
1654
+ const html = generateHtml(report);
1655
+ const htmlPath = `${basePath}.html`;
1656
+ await (0, import_promises.writeFile)(htmlPath, html, "utf-8");
1657
+ writtenFiles.push(htmlPath);
1658
+ logger.info(`\u{1F4C4} HTML report: ${htmlPath}`);
1659
+ break;
1660
+ }
1661
+ case "json": {
1662
+ const jsonReport = generateJson(report);
1663
+ const jsonPath = `${basePath}.json`;
1664
+ await (0, import_promises.writeFile)(
1665
+ jsonPath,
1666
+ JSON.stringify(jsonReport, null, 2),
1667
+ "utf-8"
1668
+ );
1669
+ writtenFiles.push(jsonPath);
1670
+ logger.info(`\u{1F4C4} JSON report: ${jsonPath}`);
1671
+ break;
1672
+ }
1673
+ case "yaml": {
1674
+ const yamlContent = generateYaml(report);
1675
+ const yamlPath = `${basePath}.yml`;
1676
+ await (0, import_promises.writeFile)(yamlPath, yamlContent, "utf-8");
1677
+ writtenFiles.push(yamlPath);
1678
+ logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
1679
+ break;
1680
+ }
1681
+ case "sarif": {
1682
+ const sarifReport = generateSarif(report);
1683
+ const sarifPath = `${basePath}.sarif`;
1684
+ await (0, import_promises.writeFile)(
1685
+ sarifPath,
1686
+ JSON.stringify(sarifReport, null, 2),
1687
+ "utf-8"
1688
+ );
1689
+ writtenFiles.push(sarifPath);
1690
+ logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
1691
+ break;
1692
+ }
1693
+ }
1694
+ } catch (err) {
1695
+ logger.error(
1696
+ `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`
1697
+ );
1698
+ }
1699
+ }
1700
+ return writtenFiles;
1701
+ }
1702
+ var isScanMode = false;
1644
1703
  var plugin = {
1645
1704
  name: "@vulcn/plugin-report",
1646
1705
  version: "0.1.0",
@@ -1655,76 +1714,64 @@ var plugin = {
1655
1714
  );
1656
1715
  },
1657
1716
  /**
1658
- * Generate report(s) after run completes.
1659
- *
1660
- * Architecture: RunResult + Session → buildReport() → VulcnReport
1661
- * Each output format is a pure projection of the canonical model.
1717
+ * Mark that we're in a multi-session scan.
1718
+ * onRunEnd will skip per-session reports — onScanEnd writes the aggregate.
1719
+ */
1720
+ onScanStart: async (_ctx) => {
1721
+ isScanMode = true;
1722
+ },
1723
+ /**
1724
+ * Generate report after a single-session run.
1725
+ * Skipped when inside a multi-session scan (onScanEnd handles that).
1662
1726
  */
1663
1727
  onRunEnd: async (result, ctx) => {
1728
+ if (isScanMode) {
1729
+ return result;
1730
+ }
1664
1731
  const config = configSchema.parse(ctx.config);
1665
- const formats = getFormats(config.format);
1666
1732
  const report = buildReport(
1667
1733
  ctx.session,
1668
1734
  result,
1669
1735
  (/* @__PURE__ */ new Date()).toISOString(),
1670
1736
  ctx.engine.version
1671
1737
  );
1672
- const outDir = (0, import_node_path.resolve)(config.outputDir);
1673
- await (0, import_promises.mkdir)(outDir, { recursive: true });
1674
- const basePath = (0, import_node_path.resolve)(outDir, config.filename);
1675
- const writtenFiles = [];
1676
- for (const fmt of formats) {
1738
+ const writtenFiles = await writeReports(report, config, ctx.logger);
1739
+ if (config.open && writtenFiles.some((f) => f.endsWith(".html"))) {
1740
+ const htmlPath = writtenFiles.find((f) => f.endsWith(".html"));
1677
1741
  try {
1678
- switch (fmt) {
1679
- case "html": {
1680
- const html = generateHtml(report);
1681
- const htmlPath = `${basePath}.html`;
1682
- await (0, import_promises.writeFile)(htmlPath, html, "utf-8");
1683
- writtenFiles.push(htmlPath);
1684
- ctx.logger.info(`\u{1F4C4} HTML report: ${htmlPath}`);
1685
- break;
1686
- }
1687
- case "json": {
1688
- const jsonReport = generateJson(report);
1689
- const jsonPath = `${basePath}.json`;
1690
- await (0, import_promises.writeFile)(
1691
- jsonPath,
1692
- JSON.stringify(jsonReport, null, 2),
1693
- "utf-8"
1694
- );
1695
- writtenFiles.push(jsonPath);
1696
- ctx.logger.info(`\u{1F4C4} JSON report: ${jsonPath}`);
1697
- break;
1698
- }
1699
- case "yaml": {
1700
- const yamlContent = generateYaml(report);
1701
- const yamlPath = `${basePath}.yml`;
1702
- await (0, import_promises.writeFile)(yamlPath, yamlContent, "utf-8");
1703
- writtenFiles.push(yamlPath);
1704
- ctx.logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
1705
- break;
1706
- }
1707
- case "sarif": {
1708
- const sarifReport = generateSarif(report);
1709
- const sarifPath = `${basePath}.sarif`;
1710
- await (0, import_promises.writeFile)(
1711
- sarifPath,
1712
- JSON.stringify(sarifReport, null, 2),
1713
- "utf-8"
1714
- );
1715
- writtenFiles.push(sarifPath);
1716
- ctx.logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
1717
- break;
1718
- }
1719
- }
1720
- } catch (err) {
1721
- ctx.logger.error(
1722
- `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`
1723
- );
1742
+ const { exec } = await import("child_process");
1743
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1744
+ exec(`${openCmd} "${htmlPath}"`);
1745
+ } catch {
1724
1746
  }
1725
1747
  }
1726
- if (config.open && formats.includes("html")) {
1727
- const htmlPath = `${basePath}.html`;
1748
+ return result;
1749
+ },
1750
+ /**
1751
+ * Generate aggregate report after all sessions in a scan complete.
1752
+ * This is the single report for vulcn run <session-dir>.
1753
+ */
1754
+ onScanEnd: async (result, ctx) => {
1755
+ isScanMode = false;
1756
+ const config = configSchema.parse(ctx.config);
1757
+ const syntheticSession = {
1758
+ name: `Scan (${ctx.sessionCount} session${ctx.sessionCount !== 1 ? "s" : ""})`,
1759
+ driver: ctx.sessions[0]?.driver ?? "browser",
1760
+ driverConfig: ctx.sessions[0]?.driverConfig ?? {},
1761
+ steps: [],
1762
+ metadata: {
1763
+ sessionCount: ctx.sessionCount
1764
+ }
1765
+ };
1766
+ const report = buildReport(
1767
+ syntheticSession,
1768
+ result,
1769
+ (/* @__PURE__ */ new Date()).toISOString(),
1770
+ ctx.engine.version
1771
+ );
1772
+ const writtenFiles = await writeReports(report, config, ctx.logger);
1773
+ if (config.open && writtenFiles.some((f) => f.endsWith(".html"))) {
1774
+ const htmlPath = writtenFiles.find((f) => f.endsWith(".html"));
1728
1775
  try {
1729
1776
  const { exec } = await import("child_process");
1730
1777
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";