executable-stories-formatters 0.7.5 → 0.7.7

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/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { parseArgs } from "util";
5
- import * as fs5 from "fs";
6
- import * as path5 from "path";
5
+ import * as fs7 from "fs";
6
+ import * as path7 from "path";
7
7
 
8
8
  // src/validation/schema-validator.ts
9
9
  import Ajv from "ajv/dist/2020.js";
@@ -492,17 +492,17 @@ function validateRawRun(data) {
492
492
  return { valid: true, errors: [] };
493
493
  }
494
494
  const errors = (validate.errors ?? []).map((err) => {
495
- const path6 = err.instancePath || "/";
495
+ const path8 = err.instancePath || "/";
496
496
  const message = err.message ?? "unknown error";
497
497
  if (err.keyword === "additionalProperties") {
498
498
  const extra = err.params.additionalProperty;
499
- return `${path6}: ${message} \u2014 '${extra}'`;
499
+ return `${path8}: ${message} \u2014 '${extra}'`;
500
500
  }
501
501
  if (err.keyword === "enum") {
502
502
  const allowed = err.params.allowedValues;
503
- return `${path6}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
503
+ return `${path8}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
504
504
  }
505
- return `${path6}: ${message}`;
505
+ return `${path8}: ${message}`;
506
506
  });
507
507
  return { valid: false, errors };
508
508
  }
@@ -966,7 +966,7 @@ ${result.errors.join("\n")}`);
966
966
 
967
967
  // src/index.ts
968
968
  import "fs";
969
- import * as path4 from "path";
969
+ import * as path5 from "path";
970
970
  import * as fsPromises from "fs/promises";
971
971
 
972
972
  // src/converters/acl/lines.ts
@@ -4028,6 +4028,12 @@ body {
4028
4028
  font-weight: 600;
4029
4029
  font-size: 0.875rem;
4030
4030
  color: var(--foreground);
4031
+ text-decoration: none;
4032
+ cursor: pointer;
4033
+ }
4034
+
4035
+ a.toc-title:hover {
4036
+ color: var(--primary);
4031
4037
  }
4032
4038
 
4033
4039
  .toc-feature {
@@ -4205,7 +4211,7 @@ function corporateBuildBody(args, deps) {
4205
4211
  const sidebar = `
4206
4212
  <nav class="toc">
4207
4213
  <div class="toc-header">
4208
- <div class="toc-title">Test Report</div>
4214
+ <a href="#" class="toc-title" onclick="window.scrollTo({top:0,behavior:'smooth'});return false;">Test Report</a>
4209
4215
  <div class="toc-stats">
4210
4216
  <div class="toc-stat-row">
4211
4217
  <span class="toc-stat-label">Total</span>
@@ -13825,7 +13831,7 @@ function renderScenario(args, deps) {
13825
13831
  <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
13826
13832
  </div>
13827
13833
  <div class="scenario-actions">
13828
- <button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown">&#x2398;</button>
13834
+ <button class="copy-scenario-btn" onclick="copyScenarioAsMarkdown('scenario-${tc.id}')" aria-label="Copy scenario as markdown" title="Copy as Markdown"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>
13829
13835
  <button class="permalink-anchor" onclick="copyPermalink('scenario-${tc.id}')" aria-label="Copy link to scenario" title="Copy link">#</button>
13830
13836
  <span class="scenario-duration">${duration}</span>
13831
13837
  </div>
@@ -14204,7 +14210,7 @@ function renderToc(args, deps) {
14204
14210
  }
14205
14211
  return `<nav class="toc-sidebar" aria-label="Table of contents">
14206
14212
  <div class="toc-header">
14207
- <span class="toc-title">Contents</span>
14213
+ <a href="#" class="toc-title" onclick="window.scrollTo({top:0,behavior:'smooth'});return false;">Contents</a>
14208
14214
  </div>
14209
14215
  <div class="toc-body">
14210
14216
  ${features.join("\n")}
@@ -15665,8 +15671,8 @@ function extractDocAttachments(step) {
15665
15671
  }
15666
15672
  return attachments;
15667
15673
  }
15668
- function guessMediaType(path6) {
15669
- const lower = path6.toLowerCase();
15674
+ function guessMediaType(path8) {
15675
+ const lower = path8.toLowerCase();
15670
15676
  if (lower.endsWith(".png")) return "image/png";
15671
15677
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
15672
15678
  if (lower.endsWith(".gif")) return "image/gif";
@@ -15807,11 +15813,11 @@ var CucumberHtmlFormatter = class {
15807
15813
  for (const envelope of envelopes) {
15808
15814
  const accepted = htmlStream.write(envelope);
15809
15815
  if (!accepted) {
15810
- await new Promise((resolve4) => htmlStream.once("drain", resolve4));
15816
+ await new Promise((resolve7) => htmlStream.once("drain", resolve7));
15811
15817
  }
15812
15818
  }
15813
- await new Promise((resolve4, reject) => {
15814
- collector.on("finish", resolve4);
15819
+ await new Promise((resolve7, reject) => {
15820
+ collector.on("finish", resolve7);
15815
15821
  collector.on("error", reject);
15816
15822
  htmlStream.end();
15817
15823
  });
@@ -16843,6 +16849,177 @@ function replaceAssetRef(html, original, replacement) {
16843
16849
  return html;
16844
16850
  }
16845
16851
 
16852
+ // src/formatters/astro.ts
16853
+ var AstroFormatter = class _AstroFormatter {
16854
+ markdownFormatter;
16855
+ title;
16856
+ constructor(options = {}) {
16857
+ this.title = options.markdown?.title ?? "User Stories";
16858
+ this.markdownFormatter = new MarkdownFormatter({
16859
+ ...options.markdown,
16860
+ title: this.title,
16861
+ stepStyle: "gherkin",
16862
+ includeFrontMatter: false,
16863
+ includeSummaryTable: false,
16864
+ includeMetadata: false
16865
+ });
16866
+ }
16867
+ format(run) {
16868
+ const markdown = this.markdownFormatter.format(run);
16869
+ const body = markdown.replace(/^# .+\n\n?/, "");
16870
+ const frontmatter = this.buildFrontmatter(run);
16871
+ return `${frontmatter}
16872
+ ${body}`;
16873
+ }
16874
+ buildFrontmatter(run) {
16875
+ const badge = _AstroFormatter.computeBadge(run.testCases);
16876
+ const count = run.testCases.length;
16877
+ const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
16878
+ const lines = [
16879
+ "---",
16880
+ `title: ${this.title}`,
16881
+ `description: ${description}`,
16882
+ "sidebar:",
16883
+ " badge:",
16884
+ ` text: ${badge.text}`,
16885
+ ` variant: ${badge.variant}`,
16886
+ "---"
16887
+ ];
16888
+ return lines.join("\n");
16889
+ }
16890
+ static computeBadge(testCases) {
16891
+ const statuses = new Set(testCases.map((tc) => tc.status));
16892
+ if (statuses.has("failed")) return { text: "Failed", variant: "danger" };
16893
+ if (statuses.has("pending")) return { text: "Pending", variant: "caution" };
16894
+ if (statuses.has("skipped") && !statuses.has("passed")) return { text: "Skipped", variant: "caution" };
16895
+ return { text: "Passed", variant: "success" };
16896
+ }
16897
+ };
16898
+
16899
+ // src/formatters/astro-assets.ts
16900
+ import * as fs4 from "fs";
16901
+ import * as path4 from "path";
16902
+ var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
16903
+ function isLocalPath(src) {
16904
+ const trimmed = src.trim();
16905
+ if (SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
16906
+ return false;
16907
+ }
16908
+ return !path4.posix.isAbsolute(trimmed) && !path4.win32.isAbsolute(trimmed);
16909
+ }
16910
+ function stripCodeContent(markdown) {
16911
+ let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
16912
+ result = result.replace(/(`+)(?:(?!\1).)+\1/g, "");
16913
+ result = result.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, "");
16914
+ result = result.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, "");
16915
+ return result;
16916
+ }
16917
+ function scanMarkdownAssets(markdown) {
16918
+ const found = /* @__PURE__ */ new Set();
16919
+ const stripped = stripCodeContent(markdown);
16920
+ const mdImageRe = /!\[[^\]]*\]\(([^)"'\s]+)(?:\s+["'][^"']*["'])?\s*\)/g;
16921
+ let match;
16922
+ while ((match = mdImageRe.exec(stripped)) !== null) {
16923
+ const src = match[1].trim();
16924
+ if (isLocalPath(src)) {
16925
+ found.add(src);
16926
+ }
16927
+ }
16928
+ const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
16929
+ while ((match = htmlSrcRe.exec(stripped)) !== null) {
16930
+ const src = match[1].trim();
16931
+ if (isLocalPath(src)) {
16932
+ found.add(src);
16933
+ }
16934
+ }
16935
+ return Array.from(found);
16936
+ }
16937
+ function splitByCode(markdown) {
16938
+ const codeRe = /^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$|<pre\b[^>]*>[\s\S]*?<\/pre>|<code\b[^>]*>[\s\S]*?<\/code>|(`+)(?:(?!\2).)+\2/gim;
16939
+ const segments = [];
16940
+ let lastIndex = 0;
16941
+ for (const match of markdown.matchAll(codeRe)) {
16942
+ if (match.index > lastIndex) {
16943
+ segments.push(markdown.slice(lastIndex, match.index));
16944
+ }
16945
+ segments.push(match[0]);
16946
+ lastIndex = match.index + match[0].length;
16947
+ }
16948
+ if (lastIndex < markdown.length) {
16949
+ segments.push(markdown.slice(lastIndex));
16950
+ }
16951
+ return segments;
16952
+ }
16953
+ function isCode(segment) {
16954
+ const trimmed = segment.trimStart();
16955
+ return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
16956
+ }
16957
+ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
16958
+ let result = prose;
16959
+ result = result.replace(
16960
+ /(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g,
16961
+ (full, pre, src, post) => {
16962
+ const trimmed = src.trim();
16963
+ if (!isLocalPath(trimmed)) return full;
16964
+ if (pathMap) {
16965
+ const mapped = pathMap.get(trimmed);
16966
+ if (mapped === void 0) return full;
16967
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
16968
+ }
16969
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
16970
+ }
16971
+ );
16972
+ result = result.replace(
16973
+ /(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
16974
+ (full, pre, src, post) => {
16975
+ const trimmed = src.trim();
16976
+ if (!isLocalPath(trimmed)) return full;
16977
+ if (pathMap) {
16978
+ const mapped = pathMap.get(trimmed);
16979
+ if (mapped === void 0) return full;
16980
+ return `${pre}${assetsBaseUrl}/${mapped}${post}`;
16981
+ }
16982
+ return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
16983
+ }
16984
+ );
16985
+ return result;
16986
+ }
16987
+ function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
16988
+ return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
16989
+ }
16990
+ function copyMarkdownAssets(options) {
16991
+ const {
16992
+ markdown,
16993
+ markdownDir,
16994
+ assetsDir,
16995
+ assetsBaseUrl,
16996
+ allowMissing = false
16997
+ } = options;
16998
+ const refs = scanMarkdownAssets(markdown);
16999
+ const pathMap = /* @__PURE__ */ new Map();
17000
+ const missing = [];
17001
+ for (const ref of refs) {
17002
+ const absPath = path4.resolve(markdownDir, ref);
17003
+ if (!fs4.existsSync(absPath)) {
17004
+ if (!allowMissing) {
17005
+ throw new Error(`Asset not found: ${absPath}`);
17006
+ }
17007
+ missing.push(ref);
17008
+ continue;
17009
+ }
17010
+ const relativeCopied = copyAsset(absPath, assetsDir);
17011
+ const fileName = relativeCopied.replace(/^assets\//, "");
17012
+ pathMap.set(ref, fileName);
17013
+ }
17014
+ const rewritten = rewriteAssetPaths(markdown, assetsBaseUrl, pathMap);
17015
+ return {
17016
+ markdown: rewritten,
17017
+ copiedCount: pathMap.size,
17018
+ missingCount: missing.length,
17019
+ missing
17020
+ };
17021
+ }
17022
+
16846
17023
  // src/converters/ndjson-parser.ts
16847
17024
  function parseNdjson(ndjson) {
16848
17025
  const lines = ndjson.trim().split("\n").filter(Boolean);
@@ -17837,6 +18014,7 @@ function listScenarios(args, _deps) {
17837
18014
 
17838
18015
  // src/index.ts
17839
18016
  var FORMAT_EXTENSIONS = {
18017
+ astro: ".md",
17840
18018
  markdown: ".md",
17841
18019
  html: ".html",
17842
18020
  "cucumber-html": ".cucumber.html",
@@ -17869,11 +18047,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17869
18047
  const ext = FORMAT_EXTENSIONS[format];
17870
18048
  const effectiveName = outputName + (outputNameSuffix ?? "");
17871
18049
  if (mode === "aggregated") {
17872
- return toPosix(path4.join(baseOutputDir, `${effectiveName}${ext}`));
18050
+ return toPosix(path5.join(baseOutputDir, `${effectiveName}${ext}`));
17873
18051
  }
17874
18052
  const normalizedSource = toPosix(sourceFile);
17875
- const dirOfSource = path4.posix.dirname(normalizedSource);
17876
- let baseName = path4.posix.basename(normalizedSource);
18053
+ const dirOfSource = path5.posix.dirname(normalizedSource);
18054
+ let baseName = path5.posix.basename(normalizedSource);
17877
18055
  for (const testExt of TEST_EXTENSIONS) {
17878
18056
  if (baseName.endsWith(testExt)) {
17879
18057
  baseName = baseName.slice(0, -testExt.length);
@@ -17882,9 +18060,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
17882
18060
  }
17883
18061
  const fileName = `${baseName}.${effectiveName}${ext}`;
17884
18062
  if (colocatedStyle === "adjacent") {
17885
- return toPosix(path4.posix.join(dirOfSource, fileName));
18063
+ return toPosix(path5.posix.join(dirOfSource, fileName));
17886
18064
  }
17887
- return toPosix(path4.posix.join(baseOutputDir, dirOfSource, fileName));
18065
+ return toPosix(path5.posix.join(baseOutputDir, dirOfSource, fileName));
17888
18066
  }
17889
18067
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
17890
18068
  const groups = /* @__PURE__ */ new Map();
@@ -17955,7 +18133,7 @@ var ReportGenerator = class {
17955
18133
  excludeTags: options.excludeTags ?? [],
17956
18134
  formats: options.formats ?? ["cucumber-json"],
17957
18135
  outputDir: options.outputDir ?? "reports",
17958
- outputName: options.outputName ?? "test-results",
18136
+ outputName: options.outputName ?? "index",
17959
18137
  outputNameTimestamp: options.outputNameTimestamp ?? false,
17960
18138
  sortTestCases: options.sortTestCases ?? "none",
17961
18139
  output: {
@@ -18010,6 +18188,24 @@ var ReportGenerator = class {
18010
18188
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
18011
18189
  customRenderers: options.markdown?.customRenderers
18012
18190
  },
18191
+ astro: {
18192
+ assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
18193
+ assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
18194
+ markdown: {
18195
+ title: options.astro?.markdown?.title ?? "User Stories",
18196
+ includeStatusIcons: options.astro?.markdown?.includeStatusIcons ?? true,
18197
+ includeErrors: options.astro?.markdown?.includeErrors ?? true,
18198
+ scenarioHeadingLevel: options.astro?.markdown?.scenarioHeadingLevel ?? 3,
18199
+ groupBy: options.astro?.markdown?.groupBy ?? "file",
18200
+ sortScenarios: options.astro?.markdown?.sortScenarios ?? "source",
18201
+ suiteSeparator: options.astro?.markdown?.suiteSeparator ?? " - ",
18202
+ includeSourceLinks: options.astro?.markdown?.includeSourceLinks ?? true,
18203
+ permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
18204
+ ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
18205
+ traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
18206
+ customRenderers: options.astro?.markdown?.customRenderers
18207
+ }
18208
+ },
18013
18209
  assetMode: options.assetMode ?? "none",
18014
18210
  allowMissingAssets: options.allowMissingAssets ?? false
18015
18211
  };
@@ -18047,6 +18243,24 @@ var ReportGenerator = class {
18047
18243
  });
18048
18244
  }
18049
18245
  }
18246
+ const astroPaths = results.get("astro");
18247
+ if (astroPaths) {
18248
+ for (const mdPath of astroPaths) {
18249
+ const content = await fsPromises.readFile(mdPath, "utf8");
18250
+ const mdDir = path5.dirname(mdPath);
18251
+ const assetsDir = path5.resolve(this.options.astro.assetsDir);
18252
+ const result = copyMarkdownAssets({
18253
+ markdown: content,
18254
+ markdownDir: mdDir,
18255
+ assetsDir,
18256
+ assetsBaseUrl: this.options.astro.assetsBaseUrl,
18257
+ allowMissing: this.options.allowMissingAssets
18258
+ });
18259
+ if (result.copiedCount > 0 || result.missingCount > 0) {
18260
+ await this.deps.writeFile(mdPath, result.markdown);
18261
+ }
18262
+ }
18263
+ }
18050
18264
  }
18051
18265
  return results;
18052
18266
  }
@@ -18065,9 +18279,9 @@ var ReportGenerator = class {
18065
18279
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
18066
18280
  const ext = FORMAT_EXTENSIONS[format];
18067
18281
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
18068
- const outputPath = toPosix(path4.join(this.options.outputDir, `${effectiveName}${ext}`));
18282
+ const outputPath = toPosix(path5.join(this.options.outputDir, `${effectiveName}${ext}`));
18069
18283
  const content = await this.formatContent(run, format);
18070
- const dir = path4.dirname(outputPath);
18284
+ const dir = path5.dirname(outputPath);
18071
18285
  await fsPromises.mkdir(dir, { recursive: true });
18072
18286
  await this.deps.writeFile(outputPath, content);
18073
18287
  return [outputPath];
@@ -18079,7 +18293,7 @@ var ReportGenerator = class {
18079
18293
  testCases
18080
18294
  };
18081
18295
  const content = await this.formatContent(groupRun, format);
18082
- const dir = path4.dirname(outputPath);
18296
+ const dir = path5.dirname(outputPath);
18083
18297
  await fsPromises.mkdir(dir, { recursive: true });
18084
18298
  await this.deps.writeFile(outputPath, content);
18085
18299
  writtenPaths.push(outputPath);
@@ -18142,6 +18356,13 @@ var ReportGenerator = class {
18142
18356
  });
18143
18357
  return formatter.formatToString(run);
18144
18358
  }
18359
+ case "astro": {
18360
+ const formatter = new AstroFormatter({
18361
+ assetsBaseUrl: this.options.astro.assetsBaseUrl,
18362
+ markdown: this.options.astro.markdown
18363
+ });
18364
+ return formatter.format(run);
18365
+ }
18145
18366
  case "markdown": {
18146
18367
  const formatter = new MarkdownFormatter({
18147
18368
  title: this.options.markdown.title,
@@ -18176,7 +18397,7 @@ async function generateRunComparison(args) {
18176
18397
  await fsPromises.mkdir(outputDir, { recursive: true });
18177
18398
  for (const format of args.formats) {
18178
18399
  const ext = format === "html" ? ".html" : ".md";
18179
- const outputPath = toPosix(path4.join(outputDir, `${outputName}${ext}`));
18400
+ const outputPath = toPosix(path5.join(outputDir, `${outputName}${ext}`));
18180
18401
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
18181
18402
  await fsPromises.writeFile(outputPath, content, "utf8");
18182
18403
  files.push(outputPath);
@@ -18184,6 +18405,45 @@ async function generateRunComparison(args) {
18184
18405
  return { files, diff };
18185
18406
  }
18186
18407
 
18408
+ // src/init-astro.ts
18409
+ import * as fs6 from "fs";
18410
+ import * as path6 from "path";
18411
+ import { fileURLToPath } from "url";
18412
+ var __dirname = path6.dirname(fileURLToPath(import.meta.url));
18413
+ function initAstro(options = {}) {
18414
+ const targetDir = options.targetDir ?? "./story-docs";
18415
+ const force = options.force ?? false;
18416
+ if (fs6.existsSync(targetDir)) {
18417
+ const entries = fs6.readdirSync(targetDir);
18418
+ if (entries.length > 0 && !force) {
18419
+ throw new Error(
18420
+ `Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
18421
+ );
18422
+ }
18423
+ }
18424
+ const templateDir = path6.resolve(__dirname, "..", "templates", "astro-starlight");
18425
+ if (!fs6.existsSync(templateDir)) {
18426
+ throw new Error(
18427
+ `Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
18428
+ );
18429
+ }
18430
+ copyDirRecursive(templateDir, targetDir);
18431
+ return { targetDir };
18432
+ }
18433
+ function copyDirRecursive(src, dest) {
18434
+ fs6.mkdirSync(dest, { recursive: true });
18435
+ const entries = fs6.readdirSync(src, { withFileTypes: true });
18436
+ for (const entry of entries) {
18437
+ const srcPath = path6.join(src, entry.name);
18438
+ const destPath = path6.join(dest, entry.name);
18439
+ if (entry.isDirectory()) {
18440
+ copyDirRecursive(srcPath, destPath);
18441
+ } else {
18442
+ fs6.copyFileSync(srcPath, destPath);
18443
+ }
18444
+ }
18445
+ }
18446
+
18187
18447
  // src/cli.ts
18188
18448
  var EXIT_SUCCESS = 0;
18189
18449
  var EXIT_SCHEMA_VALIDATION = 1;
@@ -18200,15 +18460,18 @@ USAGE
18200
18460
  executable-stories list <file> [options]
18201
18461
  executable-stories validate <file>
18202
18462
  executable-stories validate --stdin
18463
+ executable-stories init-astro [directory]
18203
18464
 
18204
18465
  SUBCOMMANDS
18205
18466
  format Read raw test results and generate reports
18206
18467
  compare Compare two runs and generate a diff report
18207
18468
  list List scenarios from a test run (text table or JSON)
18208
18469
  validate Validate a JSON file against the schema (no output generated)
18470
+ init-astro Scaffold an Astro Starlight docs site for story output
18209
18471
 
18210
18472
  OPTIONS
18211
18473
  --format <formats> Comma-separated formats (default: html)
18474
+ astro Starlight-compatible Markdown (for Astro docs sites)
18212
18475
  html Custom HTML report (accessible, dark mode, mermaid)
18213
18476
  cucumber-html Official Cucumber HTML report
18214
18477
  markdown Markdown documentation
@@ -18217,7 +18480,7 @@ OPTIONS
18217
18480
  cucumber-messages Raw NDJSON (Cucumber Messages)
18218
18481
  --input-type <type> Input type: raw, canonical, or ndjson (default: raw)
18219
18482
  --output-dir <dir> Output directory (default: reports)
18220
- --output-name <name> Base filename (default: test-results)
18483
+ --output-name <name> Base filename (default: index)
18221
18484
  --output-name-timestamp Append run timestamp (UTC seconds) to output filename for before/after diffs
18222
18485
  --sort-test-cases <mode> Sort scenarios deterministically: id, source, none (default: none)
18223
18486
  --include <globs> Comma-separated globs to include test cases by sourceFile (e.g. "**/*.Story*.cs")
@@ -18255,6 +18518,10 @@ COMPARE
18255
18518
  compare supports --format html,markdown
18256
18519
  compare uses the same --input-type for both baseline and current files
18257
18520
 
18521
+ INIT-ASTRO
18522
+ executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
18523
+ --force Overwrite existing directory
18524
+
18258
18525
  NOTIFICATIONS
18259
18526
  --slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
18260
18527
  --teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
@@ -18289,10 +18556,31 @@ function parseCliArgs(argv) {
18289
18556
  process.exit(EXIT_SUCCESS);
18290
18557
  }
18291
18558
  const subcommand = args[0];
18292
- if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate") {
18293
- console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", or "validate".`);
18559
+ if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro") {
18560
+ console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", or "init-astro".`);
18294
18561
  process.exit(EXIT_USAGE);
18295
18562
  }
18563
+ if (subcommand === "init-astro") {
18564
+ const initArgs = args.slice(1);
18565
+ const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
18566
+ const force = initArgs.includes("--force");
18567
+ try {
18568
+ const result = initAstro({ targetDir, force });
18569
+ console.log(`Scaffolded Astro Starlight project at ${result.targetDir}`);
18570
+ console.log("");
18571
+ console.log("Next steps:");
18572
+ console.log(` cd ${result.targetDir}`);
18573
+ console.log(" pnpm install # or npm install");
18574
+ console.log(" pnpm dev # start the dev server");
18575
+ console.log("");
18576
+ console.log("Generate story docs with:");
18577
+ console.log(` executable-stories format run.json --format astro --output-dir ${result.targetDir}/src/content/docs/stories --asset-mode copy`);
18578
+ process.exit(EXIT_SUCCESS);
18579
+ } catch (err) {
18580
+ console.error(`Error: ${err.message}`);
18581
+ process.exit(EXIT_USAGE);
18582
+ }
18583
+ }
18296
18584
  const { values, positionals } = parseArgs({
18297
18585
  args: args.slice(1),
18298
18586
  options: {
@@ -18301,7 +18589,7 @@ function parseCliArgs(argv) {
18301
18589
  "baseline-dir": { type: "string" },
18302
18590
  "input-type": { type: "string", default: "raw" },
18303
18591
  "output-dir": { type: "string", default: "reports" },
18304
- "output-name": { type: "string", default: "test-results" },
18592
+ "output-name": { type: "string", default: "index" },
18305
18593
  "output-name-timestamp": { type: "boolean", default: false },
18306
18594
  "sort-test-cases": { type: "string", default: "none" },
18307
18595
  include: { type: "string" },
@@ -18376,12 +18664,12 @@ function parseCliArgs(argv) {
18376
18664
  console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
18377
18665
  process.exit(EXIT_USAGE);
18378
18666
  }
18379
- const validFormats = /* @__PURE__ */ new Set(["html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18667
+ const validFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18380
18668
  const formatStr = values.format;
18381
18669
  const formats = formatStr.split(",").map((f) => f.trim());
18382
18670
  for (const f of formats) {
18383
18671
  if (!validFormats.has(f)) {
18384
- console.error(`Error: Unknown format "${f}". Valid: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
18672
+ console.error(`Error: Unknown format "${f}". Valid: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
18385
18673
  process.exit(EXIT_USAGE);
18386
18674
  }
18387
18675
  }
@@ -18505,27 +18793,27 @@ async function readInput(args) {
18505
18793
  if (args.stdin) {
18506
18794
  return readStdin();
18507
18795
  }
18508
- const filePath = path5.resolve(args.inputFile);
18509
- if (!fs5.existsSync(filePath)) {
18796
+ const filePath = path7.resolve(args.inputFile);
18797
+ if (!fs7.existsSync(filePath)) {
18510
18798
  console.error(`Error: File not found: ${filePath}`);
18511
18799
  process.exit(EXIT_USAGE);
18512
18800
  }
18513
- return fs5.readFileSync(filePath, "utf8");
18801
+ return fs7.readFileSync(filePath, "utf8");
18514
18802
  }
18515
18803
  function readFileInput(filePath) {
18516
- const resolved = path5.resolve(filePath);
18517
- if (!fs5.existsSync(resolved)) {
18804
+ const resolved = path7.resolve(filePath);
18805
+ if (!fs7.existsSync(resolved)) {
18518
18806
  console.error(`Error: File not found: ${resolved}`);
18519
18807
  process.exit(EXIT_USAGE);
18520
18808
  }
18521
- return fs5.readFileSync(resolved, "utf8");
18809
+ return fs7.readFileSync(resolved, "utf8");
18522
18810
  }
18523
18811
  function readStdin() {
18524
- return new Promise((resolve4, reject) => {
18812
+ return new Promise((resolve7, reject) => {
18525
18813
  const chunks = [];
18526
18814
  process.stdin.setEncoding("utf8");
18527
18815
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18528
- process.stdin.on("end", () => resolve4(chunks.join("")));
18816
+ process.stdin.on("end", () => resolve7(chunks.join("")));
18529
18817
  process.stdin.on("error", reject);
18530
18818
  });
18531
18819
  }
@@ -18651,14 +18939,14 @@ function tryNormalizeRunFromText(text, args) {
18651
18939
  }
18652
18940
  }
18653
18941
  function listBaselineCandidates(currentFile, args) {
18654
- const baselineDir = path5.resolve(args.baselineDir ?? path5.dirname(currentFile));
18655
- const currentResolved = path5.resolve(currentFile);
18656
- if (!fs5.existsSync(baselineDir)) {
18942
+ const baselineDir = path7.resolve(args.baselineDir ?? path7.dirname(currentFile));
18943
+ const currentResolved = path7.resolve(currentFile);
18944
+ if (!fs7.existsSync(baselineDir)) {
18657
18945
  console.error(`Error: baseline directory not found: ${baselineDir}`);
18658
18946
  process.exit(EXIT_USAGE);
18659
18947
  }
18660
- const entries = fs5.readdirSync(baselineDir, { withFileTypes: true });
18661
- return entries.filter((entry) => entry.isFile()).map((entry) => path5.join(baselineDir, entry.name)).filter((candidate) => path5.resolve(candidate) !== currentResolved).filter(
18948
+ const entries = fs7.readdirSync(baselineDir, { withFileTypes: true });
18949
+ return entries.filter((entry) => entry.isFile()).map((entry) => path7.join(baselineDir, entry.name)).filter((candidate) => path7.resolve(candidate) !== currentResolved).filter(
18662
18950
  (candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
18663
18951
  );
18664
18952
  }
@@ -18666,14 +18954,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
18666
18954
  const candidates = listBaselineCandidates(currentFile, args);
18667
18955
  const comparable = [];
18668
18956
  for (const candidate of candidates) {
18669
- const run = tryNormalizeRunFromText(fs5.readFileSync(candidate, "utf8"), args);
18957
+ const run = tryNormalizeRunFromText(fs7.readFileSync(candidate, "utf8"), args);
18670
18958
  if (run) {
18671
18959
  comparable.push({ file: candidate, run });
18672
18960
  }
18673
18961
  }
18674
18962
  if (comparable.length === 0) {
18675
18963
  console.error(
18676
- `Error: no compatible baseline files found in ${path5.resolve(args.baselineDir ?? path5.dirname(currentFile))}.`
18964
+ `Error: no compatible baseline files found in ${path7.resolve(args.baselineDir ?? path7.dirname(currentFile))}.`
18677
18965
  );
18678
18966
  process.exit(EXIT_USAGE);
18679
18967
  }
@@ -18754,9 +19042,9 @@ async function main() {
18754
19042
  process.exit(EXIT_SCHEMA_VALIDATION);
18755
19043
  }
18756
19044
  if (args.emitCanonical) {
18757
- const outPath = path5.resolve(args.emitCanonical);
18758
- fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
18759
- fs5.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
19045
+ const outPath = path7.resolve(args.emitCanonical);
19046
+ fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
19047
+ fs7.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
18760
19048
  }
18761
19049
  try {
18762
19050
  const result = await generateReports(run, args);
@@ -18812,9 +19100,9 @@ ${msg}`);
18812
19100
  }
18813
19101
  const run = data;
18814
19102
  if (args.emitCanonical) {
18815
- const outPath = path5.resolve(args.emitCanonical);
18816
- fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
18817
- fs5.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
19103
+ const outPath = path7.resolve(args.emitCanonical);
19104
+ fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
19105
+ fs7.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
18818
19106
  }
18819
19107
  try {
18820
19108
  const result = await generateReports(run, args);
@@ -18869,9 +19157,9 @@ ${msg}`);
18869
19157
  process.exit(EXIT_CANONICAL_VALIDATION);
18870
19158
  }
18871
19159
  if (args.emitCanonical) {
18872
- const outPath = path5.resolve(args.emitCanonical);
18873
- fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
18874
- fs5.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
19160
+ const outPath = path7.resolve(args.emitCanonical);
19161
+ fs7.mkdirSync(path7.dirname(outPath), { recursive: true });
19162
+ fs7.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
18875
19163
  }
18876
19164
  try {
18877
19165
  const result = await generateReports(canonical, args, droppedMissingStory);
@@ -18928,13 +19216,13 @@ async function dispatchNotifications(run, args) {
18928
19216
  }
18929
19217
  function runHistoryPipeline(run, args) {
18930
19218
  if (!args.historyFile) return;
18931
- const historyPath = path5.resolve(args.historyFile);
19219
+ const historyPath = path7.resolve(args.historyFile);
18932
19220
  const store = loadHistory(
18933
19221
  { filePath: historyPath },
18934
19222
  {
18935
19223
  readFile: (p) => {
18936
19224
  try {
18937
- return fs5.readFileSync(p, "utf8");
19225
+ return fs7.readFileSync(p, "utf8");
18938
19226
  } catch {
18939
19227
  return void 0;
18940
19228
  }
@@ -18947,11 +19235,11 @@ function runHistoryPipeline(run, args) {
18947
19235
  run,
18948
19236
  maxRuns: args.maxHistoryRuns
18949
19237
  });
18950
- const dir = path5.dirname(historyPath);
18951
- fs5.mkdirSync(dir, { recursive: true });
19238
+ const dir = path7.dirname(historyPath);
19239
+ fs7.mkdirSync(dir, { recursive: true });
18952
19240
  saveHistory(
18953
19241
  { filePath: historyPath, store: updated },
18954
- { writeFile: (p, content) => fs5.writeFileSync(p, content, "utf8") }
19242
+ { writeFile: (p, content) => fs7.writeFileSync(p, content, "utf8") }
18955
19243
  );
18956
19244
  let metricsCount = 0;
18957
19245
  for (const testId of Object.keys(updated.tests)) {
@@ -19049,9 +19337,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
19049
19337
  function printCompareResult(result, args, startMs) {
19050
19338
  const durationMs = Date.now() - startMs;
19051
19339
  if (result.prSummary && args.prSummaryFile) {
19052
- const outputPath = path5.resolve(args.prSummaryFile);
19053
- fs5.mkdirSync(path5.dirname(outputPath), { recursive: true });
19054
- fs5.writeFileSync(outputPath, result.prSummary, "utf8");
19340
+ const outputPath = path7.resolve(args.prSummaryFile);
19341
+ fs7.mkdirSync(path7.dirname(outputPath), { recursive: true });
19342
+ fs7.writeFileSync(outputPath, result.prSummary, "utf8");
19055
19343
  }
19056
19344
  if (args.jsonSummary) {
19057
19345
  console.log(