@vulcn/plugin-report 0.6.2 → 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.js CHANGED
@@ -1601,6 +1601,65 @@ function getFormats(format) {
1601
1601
  if (format === "all") return ["html", "json", "yaml", "sarif"];
1602
1602
  return [format];
1603
1603
  }
1604
+ async function writeReports(report, config, logger) {
1605
+ const formats = getFormats(config.format);
1606
+ const outDir = resolve(config.outputDir);
1607
+ await mkdir(outDir, { recursive: true });
1608
+ const basePath = resolve(outDir, config.filename);
1609
+ const writtenFiles = [];
1610
+ for (const fmt of formats) {
1611
+ try {
1612
+ switch (fmt) {
1613
+ case "html": {
1614
+ const html = generateHtml(report);
1615
+ const htmlPath = `${basePath}.html`;
1616
+ await writeFile(htmlPath, html, "utf-8");
1617
+ writtenFiles.push(htmlPath);
1618
+ logger.info(`\u{1F4C4} HTML report: ${htmlPath}`);
1619
+ break;
1620
+ }
1621
+ case "json": {
1622
+ const jsonReport = generateJson(report);
1623
+ const jsonPath = `${basePath}.json`;
1624
+ await writeFile(
1625
+ jsonPath,
1626
+ JSON.stringify(jsonReport, null, 2),
1627
+ "utf-8"
1628
+ );
1629
+ writtenFiles.push(jsonPath);
1630
+ logger.info(`\u{1F4C4} JSON report: ${jsonPath}`);
1631
+ break;
1632
+ }
1633
+ case "yaml": {
1634
+ const yamlContent = generateYaml(report);
1635
+ const yamlPath = `${basePath}.yml`;
1636
+ await writeFile(yamlPath, yamlContent, "utf-8");
1637
+ writtenFiles.push(yamlPath);
1638
+ logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
1639
+ break;
1640
+ }
1641
+ case "sarif": {
1642
+ const sarifReport = generateSarif(report);
1643
+ const sarifPath = `${basePath}.sarif`;
1644
+ await writeFile(
1645
+ sarifPath,
1646
+ JSON.stringify(sarifReport, null, 2),
1647
+ "utf-8"
1648
+ );
1649
+ writtenFiles.push(sarifPath);
1650
+ logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
1651
+ break;
1652
+ }
1653
+ }
1654
+ } catch (err) {
1655
+ logger.error(
1656
+ `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`
1657
+ );
1658
+ }
1659
+ }
1660
+ return writtenFiles;
1661
+ }
1662
+ var isScanMode = false;
1604
1663
  var plugin = {
1605
1664
  name: "@vulcn/plugin-report",
1606
1665
  version: "0.1.0",
@@ -1615,76 +1674,64 @@ var plugin = {
1615
1674
  );
1616
1675
  },
1617
1676
  /**
1618
- * Generate report(s) after run completes.
1619
- *
1620
- * Architecture: RunResult + Session → buildReport() → VulcnReport
1621
- * Each output format is a pure projection of the canonical model.
1677
+ * Mark that we're in a multi-session scan.
1678
+ * onRunEnd will skip per-session reports — onScanEnd writes the aggregate.
1679
+ */
1680
+ onScanStart: async (_ctx) => {
1681
+ isScanMode = true;
1682
+ },
1683
+ /**
1684
+ * Generate report after a single-session run.
1685
+ * Skipped when inside a multi-session scan (onScanEnd handles that).
1622
1686
  */
1623
1687
  onRunEnd: async (result, ctx) => {
1688
+ if (isScanMode) {
1689
+ return result;
1690
+ }
1624
1691
  const config = configSchema.parse(ctx.config);
1625
- const formats = getFormats(config.format);
1626
1692
  const report = buildReport(
1627
1693
  ctx.session,
1628
1694
  result,
1629
1695
  (/* @__PURE__ */ new Date()).toISOString(),
1630
1696
  ctx.engine.version
1631
1697
  );
1632
- const outDir = resolve(config.outputDir);
1633
- await mkdir(outDir, { recursive: true });
1634
- const basePath = resolve(outDir, config.filename);
1635
- const writtenFiles = [];
1636
- for (const fmt of formats) {
1698
+ const writtenFiles = await writeReports(report, config, ctx.logger);
1699
+ if (config.open && writtenFiles.some((f) => f.endsWith(".html"))) {
1700
+ const htmlPath = writtenFiles.find((f) => f.endsWith(".html"));
1637
1701
  try {
1638
- switch (fmt) {
1639
- case "html": {
1640
- const html = generateHtml(report);
1641
- const htmlPath = `${basePath}.html`;
1642
- await writeFile(htmlPath, html, "utf-8");
1643
- writtenFiles.push(htmlPath);
1644
- ctx.logger.info(`\u{1F4C4} HTML report: ${htmlPath}`);
1645
- break;
1646
- }
1647
- case "json": {
1648
- const jsonReport = generateJson(report);
1649
- const jsonPath = `${basePath}.json`;
1650
- await writeFile(
1651
- jsonPath,
1652
- JSON.stringify(jsonReport, null, 2),
1653
- "utf-8"
1654
- );
1655
- writtenFiles.push(jsonPath);
1656
- ctx.logger.info(`\u{1F4C4} JSON report: ${jsonPath}`);
1657
- break;
1658
- }
1659
- case "yaml": {
1660
- const yamlContent = generateYaml(report);
1661
- const yamlPath = `${basePath}.yml`;
1662
- await writeFile(yamlPath, yamlContent, "utf-8");
1663
- writtenFiles.push(yamlPath);
1664
- ctx.logger.info(`\u{1F4C4} YAML report: ${yamlPath}`);
1665
- break;
1666
- }
1667
- case "sarif": {
1668
- const sarifReport = generateSarif(report);
1669
- const sarifPath = `${basePath}.sarif`;
1670
- await writeFile(
1671
- sarifPath,
1672
- JSON.stringify(sarifReport, null, 2),
1673
- "utf-8"
1674
- );
1675
- writtenFiles.push(sarifPath);
1676
- ctx.logger.info(`\u{1F4C4} SARIF report: ${sarifPath}`);
1677
- break;
1678
- }
1679
- }
1680
- } catch (err) {
1681
- ctx.logger.error(
1682
- `Failed to generate ${fmt} report: ${err instanceof Error ? err.message : String(err)}`
1683
- );
1702
+ const { exec } = await import("child_process");
1703
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1704
+ exec(`${openCmd} "${htmlPath}"`);
1705
+ } catch {
1684
1706
  }
1685
1707
  }
1686
- if (config.open && formats.includes("html")) {
1687
- const htmlPath = `${basePath}.html`;
1708
+ return result;
1709
+ },
1710
+ /**
1711
+ * Generate aggregate report after all sessions in a scan complete.
1712
+ * This is the single report for vulcn run <session-dir>.
1713
+ */
1714
+ onScanEnd: async (result, ctx) => {
1715
+ isScanMode = false;
1716
+ const config = configSchema.parse(ctx.config);
1717
+ const syntheticSession = {
1718
+ name: `Scan (${ctx.sessionCount} session${ctx.sessionCount !== 1 ? "s" : ""})`,
1719
+ driver: ctx.sessions[0]?.driver ?? "browser",
1720
+ driverConfig: ctx.sessions[0]?.driverConfig ?? {},
1721
+ steps: [],
1722
+ metadata: {
1723
+ sessionCount: ctx.sessionCount
1724
+ }
1725
+ };
1726
+ const report = buildReport(
1727
+ syntheticSession,
1728
+ result,
1729
+ (/* @__PURE__ */ new Date()).toISOString(),
1730
+ ctx.engine.version
1731
+ );
1732
+ const writtenFiles = await writeReports(report, config, ctx.logger);
1733
+ if (config.open && writtenFiles.some((f) => f.endsWith(".html"))) {
1734
+ const htmlPath = writtenFiles.find((f) => f.endsWith(".html"));
1688
1735
  try {
1689
1736
  const { exec } = await import("child_process");
1690
1737
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";