executable-stories-formatters 0.10.0 → 0.11.0
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/README.md +35 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +1093 -104
- package/dist/cli.js.map +1 -1
- package/dist/{index-CbWFyoTx.d.cts → index-DF16Xl5i.d.cts} +7 -0
- package/dist/{index-CbWFyoTx.d.ts → index-DF16Xl5i.d.ts} +7 -0
- package/dist/index.cjs +130 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -6
- package/dist/index.d.ts +42 -6
- package/dist/index.js +129 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/schemas/raw-run.schema.json +19 -0
- package/schemas/story-report-v1.json +17 -0
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
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs13 from "fs";
|
|
6
|
+
import * as path14 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -209,6 +209,11 @@ var raw_run_schema_default = {
|
|
|
209
209
|
type: "integer",
|
|
210
210
|
minimum: 0,
|
|
211
211
|
description: "Order in which the story was defined in source (for stable sorting)."
|
|
212
|
+
},
|
|
213
|
+
otelSpans: {
|
|
214
|
+
type: "array",
|
|
215
|
+
items: { type: "object" },
|
|
216
|
+
description: "OpenTelemetry spans captured for this scenario (from autotel), used to render a trace waterfall."
|
|
212
217
|
}
|
|
213
218
|
},
|
|
214
219
|
required: ["scenario"],
|
|
@@ -397,6 +402,20 @@ var raw_run_schema_default = {
|
|
|
397
402
|
required: ["kind", "path", "phase"],
|
|
398
403
|
additionalProperties: false
|
|
399
404
|
},
|
|
405
|
+
{
|
|
406
|
+
type: "object",
|
|
407
|
+
description: "Video reference with optional caption and poster image.",
|
|
408
|
+
properties: {
|
|
409
|
+
kind: { const: "video" },
|
|
410
|
+
path: { type: "string" },
|
|
411
|
+
caption: { type: "string" },
|
|
412
|
+
poster: { type: "string" },
|
|
413
|
+
phase: { $ref: "#/$defs/DocPhase" },
|
|
414
|
+
children: { type: "array", items: { $ref: "#/$defs/DocEntry" }, description: "Nested child doc entries for grouping." }
|
|
415
|
+
},
|
|
416
|
+
required: ["kind", "path", "phase"],
|
|
417
|
+
additionalProperties: false
|
|
418
|
+
},
|
|
400
419
|
{
|
|
401
420
|
type: "object",
|
|
402
421
|
description: "Custom documentation entry with arbitrary data.",
|
|
@@ -539,17 +558,17 @@ function validateRawRun(data) {
|
|
|
539
558
|
return { valid: true, errors: [] };
|
|
540
559
|
}
|
|
541
560
|
const errors = (validate.errors ?? []).map((err) => {
|
|
542
|
-
const
|
|
561
|
+
const path15 = err.instancePath || "/";
|
|
543
562
|
const message = err.message ?? "unknown error";
|
|
544
563
|
if (err.keyword === "additionalProperties") {
|
|
545
564
|
const extra = err.params.additionalProperty;
|
|
546
|
-
return `${
|
|
565
|
+
return `${path15}: ${message} \u2014 '${extra}'`;
|
|
547
566
|
}
|
|
548
567
|
if (err.keyword === "enum") {
|
|
549
568
|
const allowed = err.params.allowedValues;
|
|
550
|
-
return `${
|
|
569
|
+
return `${path15}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
551
570
|
}
|
|
552
|
-
return `${
|
|
571
|
+
return `${path15}: ${message}`;
|
|
553
572
|
});
|
|
554
573
|
return { valid: false, errors };
|
|
555
574
|
}
|
|
@@ -1486,6 +1505,15 @@ function copyDocEntry(entry) {
|
|
|
1486
1505
|
phase: entry.phase,
|
|
1487
1506
|
...children
|
|
1488
1507
|
};
|
|
1508
|
+
case "video":
|
|
1509
|
+
return {
|
|
1510
|
+
kind: "video",
|
|
1511
|
+
path: entry.path,
|
|
1512
|
+
...entry.caption ? { caption: entry.caption } : {},
|
|
1513
|
+
...entry.poster ? { poster: entry.poster } : {},
|
|
1514
|
+
phase: entry.phase,
|
|
1515
|
+
...children
|
|
1516
|
+
};
|
|
1489
1517
|
case "custom":
|
|
1490
1518
|
return {
|
|
1491
1519
|
kind: "custom",
|
|
@@ -14600,10 +14628,10 @@ function renderDocMermaid(entry, deps) {
|
|
|
14600
14628
|
function renderDocScreenshot(entry, deps) {
|
|
14601
14629
|
const alt = entry.alt ?? "Screenshot";
|
|
14602
14630
|
const embedEnabled = deps.embedScreenshots ?? true;
|
|
14603
|
-
const
|
|
14604
|
-
const embedAttempted = !
|
|
14631
|
+
const isRemote2 = /^(?:https?:|data:)/i.test(entry.path);
|
|
14632
|
+
const embedAttempted = !isRemote2 && embedEnabled && !!deps.readScreenshot;
|
|
14605
14633
|
const inlined = embedAttempted ? deps.readScreenshot(entry.path) : void 0;
|
|
14606
|
-
const isAbsoluteFsPath = !
|
|
14634
|
+
const isAbsoluteFsPath = !isRemote2 && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
14607
14635
|
if (embedAttempted && inlined === void 0 && isAbsoluteFsPath) {
|
|
14608
14636
|
const captionHtml = entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : "";
|
|
14609
14637
|
return `<div class="doc-screenshot doc-screenshot-missing">
|
|
@@ -14618,6 +14646,23 @@ function renderDocScreenshot(entry, deps) {
|
|
|
14618
14646
|
${entry.alt ? `<div class="doc-screenshot-caption">${deps.escapeHtml(entry.alt)}</div>` : ""}
|
|
14619
14647
|
</div>`;
|
|
14620
14648
|
}
|
|
14649
|
+
function renderDocVideo(entry, deps) {
|
|
14650
|
+
const isRemote2 = /^(?:https?:|data:)/i.test(entry.path);
|
|
14651
|
+
const isAbsoluteFsPath = !isRemote2 && /^(?:[/\\]|[A-Za-z]:[/\\])/.test(entry.path);
|
|
14652
|
+
const captionHtml = entry.caption ? `<div class="doc-video-caption">${deps.escapeHtml(entry.caption)}</div>` : "";
|
|
14653
|
+
if ((deps.embedScreenshots ?? true) && isAbsoluteFsPath) {
|
|
14654
|
+
return `<div class="doc-video doc-video-missing">
|
|
14655
|
+
<div class="doc-video-missing-label">Video unavailable</div>
|
|
14656
|
+
<div class="doc-video-missing-path">${deps.escapeHtml(entry.path)}</div>
|
|
14657
|
+
${captionHtml}
|
|
14658
|
+
</div>`;
|
|
14659
|
+
}
|
|
14660
|
+
const poster = entry.poster ? ` poster="${deps.escapeHtml(entry.poster)}"` : "";
|
|
14661
|
+
return `<div class="doc-video">
|
|
14662
|
+
<video class="doc-video-player" controls preload="metadata"${poster} src="${deps.escapeHtml(entry.path)}"></video>
|
|
14663
|
+
${captionHtml}
|
|
14664
|
+
</div>`;
|
|
14665
|
+
}
|
|
14621
14666
|
function renderDocCustom(entry, deps) {
|
|
14622
14667
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
14623
14668
|
const data = entry.data;
|
|
@@ -14671,6 +14716,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14671
14716
|
case "screenshot":
|
|
14672
14717
|
html = renderDocScreenshot(entry, deps);
|
|
14673
14718
|
break;
|
|
14719
|
+
case "video":
|
|
14720
|
+
html = renderDocVideo(entry, deps);
|
|
14721
|
+
break;
|
|
14674
14722
|
case "custom":
|
|
14675
14723
|
html = renderDocCustom(entry, deps);
|
|
14676
14724
|
break;
|
|
@@ -15992,6 +16040,19 @@ var MarkdownFormatter = class {
|
|
|
15992
16040
|
case "screenshot":
|
|
15993
16041
|
lines.push(`${indent}`);
|
|
15994
16042
|
break;
|
|
16043
|
+
case "video": {
|
|
16044
|
+
const poster = entry.poster ? ` poster="${entry.poster}"` : "";
|
|
16045
|
+
lines.push(`${indent}`);
|
|
16046
|
+
lines.push(`${indent}<video controls preload="metadata"${poster} class="doc-video">`);
|
|
16047
|
+
lines.push(`${indent} <source src="${entry.path}" />`);
|
|
16048
|
+
lines.push(`${indent}</video>`);
|
|
16049
|
+
if (entry.caption) {
|
|
16050
|
+
lines.push(`${indent}`);
|
|
16051
|
+
lines.push(`${indent}*${entry.caption}*`);
|
|
16052
|
+
}
|
|
16053
|
+
lines.push(`${indent}`);
|
|
16054
|
+
break;
|
|
16055
|
+
}
|
|
15995
16056
|
case "custom":
|
|
15996
16057
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
15997
16058
|
const data = entry.data;
|
|
@@ -16091,8 +16152,8 @@ function extractFeatureName(testCases, uri) {
|
|
|
16091
16152
|
return tc.titlePath[0];
|
|
16092
16153
|
}
|
|
16093
16154
|
}
|
|
16094
|
-
const
|
|
16095
|
-
return
|
|
16155
|
+
const basename4 = uri.replace(/^.*[\\/]/, "").replace(/\.[^.]+$/, "");
|
|
16156
|
+
return basename4.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
16096
16157
|
}
|
|
16097
16158
|
function synthesizeFeature(uri, testCases) {
|
|
16098
16159
|
const featureName = extractFeatureName(testCases, uri);
|
|
@@ -16704,8 +16765,8 @@ function extractDocAttachments(step) {
|
|
|
16704
16765
|
}
|
|
16705
16766
|
return attachments;
|
|
16706
16767
|
}
|
|
16707
|
-
function guessMediaType(
|
|
16708
|
-
const lower =
|
|
16768
|
+
function guessMediaType(path15) {
|
|
16769
|
+
const lower = path15.toLowerCase();
|
|
16709
16770
|
if (lower.endsWith(".png")) return "image/png";
|
|
16710
16771
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16711
16772
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16846,11 +16907,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16846
16907
|
for (const envelope of envelopes) {
|
|
16847
16908
|
const accepted = htmlStream.write(envelope);
|
|
16848
16909
|
if (!accepted) {
|
|
16849
|
-
await new Promise((
|
|
16910
|
+
await new Promise((resolve11) => htmlStream.once("drain", resolve11));
|
|
16850
16911
|
}
|
|
16851
16912
|
}
|
|
16852
|
-
await new Promise((
|
|
16853
|
-
collector.on("finish",
|
|
16913
|
+
await new Promise((resolve11, reject) => {
|
|
16914
|
+
collector.on("finish", resolve11);
|
|
16854
16915
|
collector.on("error", reject);
|
|
16855
16916
|
htmlStream.end();
|
|
16856
16917
|
});
|
|
@@ -17181,6 +17242,8 @@ function formatDocEntry(doc) {
|
|
|
17181
17242
|
return `${escapeHtml2(doc.title ?? "mermaid diagram")}: <code>${escapeHtml2(doc.code)}</code>`;
|
|
17182
17243
|
case "screenshot":
|
|
17183
17244
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17245
|
+
case "video":
|
|
17246
|
+
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17184
17247
|
case "custom":
|
|
17185
17248
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
17186
17249
|
}
|
|
@@ -17637,6 +17700,8 @@ function formatDocEntry2(doc) {
|
|
|
17637
17700
|
return `${doc.title ?? "mermaid diagram"}: \`${doc.code}\``;
|
|
17638
17701
|
case "screenshot":
|
|
17639
17702
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17703
|
+
case "video":
|
|
17704
|
+
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
17640
17705
|
case "custom":
|
|
17641
17706
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17642
17707
|
}
|
|
@@ -17882,19 +17947,35 @@ function replaceAssetRef(html, original, replacement) {
|
|
|
17882
17947
|
return html;
|
|
17883
17948
|
}
|
|
17884
17949
|
|
|
17950
|
+
// src/utils/source-file.ts
|
|
17951
|
+
function cleanTestStem(fileName) {
|
|
17952
|
+
const base = fileName.split(/[\\/]/).pop() ?? fileName;
|
|
17953
|
+
const stripped = base.replace(/\.(story\.)?(test|spec|cy)\.[cm]?[jt]sx?$/i, "");
|
|
17954
|
+
if (stripped !== base) return stripped;
|
|
17955
|
+
return base.replace(/\.[^.]+$/, "");
|
|
17956
|
+
}
|
|
17957
|
+
function humanizeSourceFile(fileName) {
|
|
17958
|
+
return cleanTestStem(fileName).split(/[-_.\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
17959
|
+
}
|
|
17960
|
+
|
|
17885
17961
|
// src/formatters/astro.ts
|
|
17886
17962
|
var AstroFormatter = class _AstroFormatter {
|
|
17887
17963
|
markdownFormatter;
|
|
17888
17964
|
title;
|
|
17965
|
+
perFileTitle;
|
|
17889
17966
|
constructor(options = {}) {
|
|
17890
17967
|
this.title = options.markdown?.title ?? "User Stories";
|
|
17968
|
+
this.perFileTitle = options.perFileTitle ?? false;
|
|
17891
17969
|
this.markdownFormatter = new MarkdownFormatter({
|
|
17892
17970
|
...options.markdown,
|
|
17893
17971
|
title: this.title,
|
|
17894
17972
|
stepStyle: "gherkin",
|
|
17895
17973
|
includeFrontMatter: false,
|
|
17896
17974
|
includeSummaryTable: false,
|
|
17897
|
-
includeMetadata: false
|
|
17975
|
+
includeMetadata: false,
|
|
17976
|
+
// A per-file page is one file already — group by suite/describe so the
|
|
17977
|
+
// body shows clean section headings, not the redundant source path.
|
|
17978
|
+
groupBy: this.perFileTitle ? "suite" : options.markdown?.groupBy ?? "file"
|
|
17898
17979
|
});
|
|
17899
17980
|
}
|
|
17900
17981
|
format(run) {
|
|
@@ -17904,13 +17985,31 @@ var AstroFormatter = class _AstroFormatter {
|
|
|
17904
17985
|
return `${frontmatter}
|
|
17905
17986
|
${body}`;
|
|
17906
17987
|
}
|
|
17988
|
+
/**
|
|
17989
|
+
* Title for the page. A per-file page (one source file — i.e. colocated mode)
|
|
17990
|
+
* is titled by its suite/describe name, falling back to a humanized filename,
|
|
17991
|
+
* so the docs nav reads "Convert Currency" not "User Stories" six times over.
|
|
17992
|
+
* Multi-file (aggregated) pages keep the configured title.
|
|
17993
|
+
*/
|
|
17994
|
+
deriveTitle(run) {
|
|
17995
|
+
if (!this.perFileTitle) return this.title;
|
|
17996
|
+
const sourceFiles = new Set(
|
|
17997
|
+
run.testCases.map((tc) => tc.sourceFile).filter((f) => f && f !== "unknown")
|
|
17998
|
+
);
|
|
17999
|
+
if (sourceFiles.size !== 1) return this.title;
|
|
18000
|
+
const suites = new Set(
|
|
18001
|
+
run.testCases.map((tc) => tc.titlePath?.[0]).filter((s) => Boolean(s))
|
|
18002
|
+
);
|
|
18003
|
+
if (suites.size === 1) return [...suites][0];
|
|
18004
|
+
return humanizeSourceFile([...sourceFiles][0]) || this.title;
|
|
18005
|
+
}
|
|
17907
18006
|
buildFrontmatter(run) {
|
|
17908
18007
|
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
17909
18008
|
const count = run.testCases.length;
|
|
17910
18009
|
const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
17911
18010
|
const lines = [
|
|
17912
18011
|
"---",
|
|
17913
|
-
`title: ${this.
|
|
18012
|
+
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
17914
18013
|
`description: ${description}`,
|
|
17915
18014
|
"sidebar:",
|
|
17916
18015
|
" badge:",
|
|
@@ -17928,6 +18027,12 @@ ${body}`;
|
|
|
17928
18027
|
return { text: "Passed", variant: "success" };
|
|
17929
18028
|
}
|
|
17930
18029
|
};
|
|
18030
|
+
function yamlScalar(value) {
|
|
18031
|
+
if (/[:#[\]{}&*!|>'"%@`]|^[\s-]|\s$/.test(value)) {
|
|
18032
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
18033
|
+
}
|
|
18034
|
+
return value;
|
|
18035
|
+
}
|
|
17931
18036
|
|
|
17932
18037
|
// src/formatters/confluence.ts
|
|
17933
18038
|
var ConfluenceFormatter = class {
|
|
@@ -18204,6 +18309,15 @@ ${tc.errorStack}` : "");
|
|
|
18204
18309
|
])
|
|
18205
18310
|
);
|
|
18206
18311
|
break;
|
|
18312
|
+
case "video":
|
|
18313
|
+
content.push(
|
|
18314
|
+
paragraph([
|
|
18315
|
+
text(entry.caption ?? "Video", strong()),
|
|
18316
|
+
text(": "),
|
|
18317
|
+
link(entry.path, entry.path)
|
|
18318
|
+
])
|
|
18319
|
+
);
|
|
18320
|
+
break;
|
|
18207
18321
|
case "custom":
|
|
18208
18322
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
18209
18323
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -18373,6 +18487,13 @@ function scanMarkdownAssets(markdown) {
|
|
|
18373
18487
|
found.add(src);
|
|
18374
18488
|
}
|
|
18375
18489
|
}
|
|
18490
|
+
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
18491
|
+
while ((match = posterRe.exec(stripped)) !== null) {
|
|
18492
|
+
const src = match[1].trim();
|
|
18493
|
+
if (isLocalPath(src)) {
|
|
18494
|
+
found.add(src);
|
|
18495
|
+
}
|
|
18496
|
+
}
|
|
18376
18497
|
return Array.from(found);
|
|
18377
18498
|
}
|
|
18378
18499
|
function splitByCode(markdown) {
|
|
@@ -18423,6 +18544,19 @@ function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
|
18423
18544
|
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18424
18545
|
}
|
|
18425
18546
|
);
|
|
18547
|
+
result = result.replace(
|
|
18548
|
+
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18549
|
+
(full, pre, src, post) => {
|
|
18550
|
+
const trimmed = src.trim();
|
|
18551
|
+
if (!isLocalPath(trimmed)) return full;
|
|
18552
|
+
if (pathMap) {
|
|
18553
|
+
const mapped = pathMap.get(trimmed);
|
|
18554
|
+
if (mapped === void 0) return full;
|
|
18555
|
+
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18556
|
+
}
|
|
18557
|
+
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18558
|
+
}
|
|
18559
|
+
);
|
|
18426
18560
|
return result;
|
|
18427
18561
|
}
|
|
18428
18562
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
@@ -19220,10 +19354,10 @@ _...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
|
19220
19354
|
}
|
|
19221
19355
|
async function sendSlackNotification(args, deps) {
|
|
19222
19356
|
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
19223
|
-
const { fetch, logger } = deps;
|
|
19357
|
+
const { fetch: fetch2, logger } = deps;
|
|
19224
19358
|
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
19225
19359
|
try {
|
|
19226
|
-
const response = await
|
|
19360
|
+
const response = await fetch2(webhookUrl, {
|
|
19227
19361
|
method: "POST",
|
|
19228
19362
|
headers: { "Content-Type": "application/json" },
|
|
19229
19363
|
body: JSON.stringify(payload)
|
|
@@ -19374,10 +19508,10 @@ function buildTeamsPayload(summary, maxFailedTests) {
|
|
|
19374
19508
|
}
|
|
19375
19509
|
async function sendTeamsNotification(args, deps) {
|
|
19376
19510
|
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
19377
|
-
const { fetch, logger } = deps;
|
|
19511
|
+
const { fetch: fetch2, logger } = deps;
|
|
19378
19512
|
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
19379
19513
|
try {
|
|
19380
|
-
const response = await
|
|
19514
|
+
const response = await fetch2(webhookUrl, {
|
|
19381
19515
|
method: "POST",
|
|
19382
19516
|
headers: { "Content-Type": "application/json" },
|
|
19383
19517
|
body: JSON.stringify(payload)
|
|
@@ -19425,7 +19559,7 @@ function signBody(args) {
|
|
|
19425
19559
|
// src/notifiers/webhook.ts
|
|
19426
19560
|
async function sendWebhookNotification(args, deps) {
|
|
19427
19561
|
const { summary, options } = args;
|
|
19428
|
-
const { fetch, logger } = deps;
|
|
19562
|
+
const { fetch: fetch2, logger } = deps;
|
|
19429
19563
|
const payload = {
|
|
19430
19564
|
schemaVersion: 1,
|
|
19431
19565
|
event: "test_run_finished",
|
|
@@ -19447,7 +19581,7 @@ async function sendWebhookNotification(args, deps) {
|
|
|
19447
19581
|
}
|
|
19448
19582
|
}
|
|
19449
19583
|
try {
|
|
19450
|
-
const response = await
|
|
19584
|
+
const response = await fetch2(options.url, {
|
|
19451
19585
|
method: options.method ?? "POST",
|
|
19452
19586
|
headers,
|
|
19453
19587
|
body
|
|
@@ -19526,7 +19660,7 @@ async function sendNotifications(args, deps) {
|
|
|
19526
19660
|
logger.warn("notifications: skipped (fetch unavailable)");
|
|
19527
19661
|
return;
|
|
19528
19662
|
}
|
|
19529
|
-
const
|
|
19663
|
+
const fetch2 = deps.fetch;
|
|
19530
19664
|
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
19531
19665
|
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
19532
19666
|
const globalCondition = notification?.condition ?? "on-failure";
|
|
@@ -19542,7 +19676,7 @@ async function sendNotifications(args, deps) {
|
|
|
19542
19676
|
promises.push(
|
|
19543
19677
|
sendSlackNotification(
|
|
19544
19678
|
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
19545
|
-
{ fetch, logger }
|
|
19679
|
+
{ fetch: fetch2, logger }
|
|
19546
19680
|
).then(() => void 0)
|
|
19547
19681
|
);
|
|
19548
19682
|
}
|
|
@@ -19550,7 +19684,7 @@ async function sendNotifications(args, deps) {
|
|
|
19550
19684
|
promises.push(
|
|
19551
19685
|
sendTeamsNotification(
|
|
19552
19686
|
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
19553
|
-
{ fetch, logger }
|
|
19687
|
+
{ fetch: fetch2, logger }
|
|
19554
19688
|
).then(() => void 0)
|
|
19555
19689
|
);
|
|
19556
19690
|
}
|
|
@@ -19560,7 +19694,7 @@ async function sendNotifications(args, deps) {
|
|
|
19560
19694
|
promises.push(
|
|
19561
19695
|
sendWebhookNotification(
|
|
19562
19696
|
{ summary, options: webhook, maxFailedTests },
|
|
19563
|
-
{ fetch, logger }
|
|
19697
|
+
{ fetch: fetch2, logger }
|
|
19564
19698
|
).then(() => void 0)
|
|
19565
19699
|
);
|
|
19566
19700
|
}
|
|
@@ -19854,18 +19988,18 @@ function deriveChangeType(tags) {
|
|
|
19854
19988
|
}
|
|
19855
19989
|
return "unknown";
|
|
19856
19990
|
}
|
|
19857
|
-
function extensionOf(
|
|
19858
|
-
const base =
|
|
19991
|
+
function extensionOf(path15) {
|
|
19992
|
+
const base = path15.split("/").pop() ?? path15;
|
|
19859
19993
|
const dot = base.lastIndexOf(".");
|
|
19860
19994
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19861
19995
|
}
|
|
19862
|
-
function isTestFile(
|
|
19863
|
-
return TEST_INFIX.test(
|
|
19996
|
+
function isTestFile(path15) {
|
|
19997
|
+
return TEST_INFIX.test(path15);
|
|
19864
19998
|
}
|
|
19865
|
-
function isReviewableSource(
|
|
19866
|
-
if (isTestFile(
|
|
19867
|
-
if (
|
|
19868
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
19999
|
+
function isReviewableSource(path15) {
|
|
20000
|
+
if (isTestFile(path15)) return false;
|
|
20001
|
+
if (path15.endsWith(".d.ts")) return false;
|
|
20002
|
+
return CODE_EXTENSIONS.has(extensionOf(path15));
|
|
19869
20003
|
}
|
|
19870
20004
|
function testBaseKey(testFile) {
|
|
19871
20005
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -19969,7 +20103,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
19969
20103
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
19970
20104
|
const key = testBaseKey(testCase.sourceFile);
|
|
19971
20105
|
const coversFiles = changedSourcePaths.filter(
|
|
19972
|
-
(
|
|
20106
|
+
(path15) => sourceBaseKey(path15) === key
|
|
19973
20107
|
);
|
|
19974
20108
|
return {
|
|
19975
20109
|
id: testCase.id,
|
|
@@ -20513,6 +20647,10 @@ var FORMAT_EXTENSIONS = {
|
|
|
20513
20647
|
"scenario-index-json": ".scenarios-index.json",
|
|
20514
20648
|
"story-report-json": ".story-report.json"
|
|
20515
20649
|
};
|
|
20650
|
+
function joinNameAndExt(name, ext) {
|
|
20651
|
+
const stutter = `.${name}.`;
|
|
20652
|
+
return ext.startsWith(stutter) ? `${name}.${ext.slice(stutter.length)}` : `${name}${ext}`;
|
|
20653
|
+
}
|
|
20516
20654
|
var TEST_EXTENSIONS = [
|
|
20517
20655
|
".test.ts",
|
|
20518
20656
|
".test.tsx",
|
|
@@ -20538,7 +20676,7 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20538
20676
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20539
20677
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
20540
20678
|
if (mode === "aggregated") {
|
|
20541
|
-
return toPosix(path8.join(baseOutputDir,
|
|
20679
|
+
return toPosix(path8.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
20542
20680
|
}
|
|
20543
20681
|
const normalizedSource = toPosix(sourceFile);
|
|
20544
20682
|
const dirOfSource = path8.posix.dirname(normalizedSource);
|
|
@@ -20553,6 +20691,9 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20553
20691
|
if (colocatedStyle === "adjacent") {
|
|
20554
20692
|
return toPosix(path8.posix.join(dirOfSource, fileName));
|
|
20555
20693
|
}
|
|
20694
|
+
if (colocatedStyle === "flat") {
|
|
20695
|
+
return toPosix(path8.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
20696
|
+
}
|
|
20556
20697
|
return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
20557
20698
|
}
|
|
20558
20699
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
@@ -20792,7 +20933,7 @@ var ReportGenerator = class {
|
|
|
20792
20933
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
20793
20934
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20794
20935
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
20795
|
-
const outputPath = toPosix(path8.join(this.options.outputDir,
|
|
20936
|
+
const outputPath = toPosix(path8.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
20796
20937
|
const content = await this.formatContent(run, format);
|
|
20797
20938
|
const dir = path8.dirname(outputPath);
|
|
20798
20939
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
@@ -20872,6 +21013,8 @@ var ReportGenerator = class {
|
|
|
20872
21013
|
case "astro": {
|
|
20873
21014
|
const formatter = new AstroFormatter({
|
|
20874
21015
|
assetsBaseUrl: this.options.astro.assetsBaseUrl,
|
|
21016
|
+
// Colocated = one page per file, so title each by its own suite/file.
|
|
21017
|
+
perFileTitle: this.options.output.mode === "colocated",
|
|
20875
21018
|
markdown: this.options.astro.markdown
|
|
20876
21019
|
});
|
|
20877
21020
|
return formatter.format(run);
|
|
@@ -20957,46 +21100,741 @@ import * as fs8 from "fs";
|
|
|
20957
21100
|
import * as path9 from "path";
|
|
20958
21101
|
import { fileURLToPath } from "url";
|
|
20959
21102
|
var __dirname = path9.dirname(fileURLToPath(import.meta.url));
|
|
21103
|
+
var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
|
|
21104
|
+
var FRAMEWORK_FILES = ["tsconfig.json"];
|
|
20960
21105
|
function initAstro(options = {}) {
|
|
20961
21106
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
20962
21107
|
const force = options.force ?? false;
|
|
21108
|
+
const update = options.update ?? false;
|
|
21109
|
+
const templateDir = path9.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
21110
|
+
if (!fs8.existsSync(templateDir)) {
|
|
21111
|
+
throw new Error(
|
|
21112
|
+
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
21113
|
+
);
|
|
21114
|
+
}
|
|
21115
|
+
if (update) {
|
|
21116
|
+
return updateFrameworkFiles(templateDir, targetDir);
|
|
21117
|
+
}
|
|
20963
21118
|
if (fs8.existsSync(targetDir)) {
|
|
20964
21119
|
const entries = fs8.readdirSync(targetDir);
|
|
20965
21120
|
if (entries.length > 0 && !force) {
|
|
20966
21121
|
throw new Error(
|
|
20967
|
-
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite.`
|
|
21122
|
+
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
|
|
20968
21123
|
);
|
|
20969
21124
|
}
|
|
20970
21125
|
}
|
|
20971
|
-
const templateDir = path9.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
20972
|
-
if (!fs8.existsSync(templateDir)) {
|
|
20973
|
-
throw new Error(
|
|
20974
|
-
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
20975
|
-
);
|
|
20976
|
-
}
|
|
20977
21126
|
copyDirRecursive(templateDir, targetDir);
|
|
20978
21127
|
return { targetDir };
|
|
20979
21128
|
}
|
|
20980
|
-
function
|
|
21129
|
+
function updateFrameworkFiles(templateDir, targetDir) {
|
|
21130
|
+
if (!fs8.existsSync(targetDir) || !fs8.existsSync(path9.join(targetDir, "astro.config.mjs"))) {
|
|
21131
|
+
throw new Error(
|
|
21132
|
+
`"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
|
|
21133
|
+
);
|
|
21134
|
+
}
|
|
21135
|
+
const updated = [];
|
|
21136
|
+
for (const dir of FRAMEWORK_DIRS) {
|
|
21137
|
+
const src = path9.join(templateDir, dir);
|
|
21138
|
+
if (!fs8.existsSync(src)) continue;
|
|
21139
|
+
copyDirRecursive(src, path9.join(targetDir, dir), (rel) => updated.push(path9.join(dir, rel)));
|
|
21140
|
+
}
|
|
21141
|
+
for (const file of FRAMEWORK_FILES) {
|
|
21142
|
+
const src = path9.join(templateDir, file);
|
|
21143
|
+
if (!fs8.existsSync(src)) continue;
|
|
21144
|
+
fs8.copyFileSync(src, path9.join(targetDir, file));
|
|
21145
|
+
updated.push(file);
|
|
21146
|
+
}
|
|
21147
|
+
if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
|
|
21148
|
+
return { targetDir, updatedFiles: updated };
|
|
21149
|
+
}
|
|
21150
|
+
function mergeDependencies(templateDir, targetDir) {
|
|
21151
|
+
const tmplPkgPath = path9.join(templateDir, "package.json");
|
|
21152
|
+
const userPkgPath = path9.join(targetDir, "package.json");
|
|
21153
|
+
if (!fs8.existsSync(tmplPkgPath) || !fs8.existsSync(userPkgPath)) return false;
|
|
21154
|
+
const tmpl = JSON.parse(fs8.readFileSync(tmplPkgPath, "utf8"));
|
|
21155
|
+
const user = JSON.parse(fs8.readFileSync(userPkgPath, "utf8"));
|
|
21156
|
+
user.dependencies = user.dependencies ?? {};
|
|
21157
|
+
let changed = false;
|
|
21158
|
+
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
21159
|
+
if (!(name in user.dependencies)) {
|
|
21160
|
+
user.dependencies[name] = version;
|
|
21161
|
+
changed = true;
|
|
21162
|
+
}
|
|
21163
|
+
}
|
|
21164
|
+
if (changed) {
|
|
21165
|
+
fs8.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
21166
|
+
`, "utf8");
|
|
21167
|
+
}
|
|
21168
|
+
return changed;
|
|
21169
|
+
}
|
|
21170
|
+
function copyDirRecursive(src, dest, onFile, baseSrc = src) {
|
|
20981
21171
|
fs8.mkdirSync(dest, { recursive: true });
|
|
20982
21172
|
const entries = fs8.readdirSync(src, { withFileTypes: true });
|
|
20983
21173
|
for (const entry of entries) {
|
|
20984
21174
|
const srcPath = path9.join(src, entry.name);
|
|
20985
21175
|
const destPath = path9.join(dest, entry.name);
|
|
20986
21176
|
if (entry.isDirectory()) {
|
|
20987
|
-
copyDirRecursive(srcPath, destPath);
|
|
21177
|
+
copyDirRecursive(srcPath, destPath, onFile, baseSrc);
|
|
20988
21178
|
} else {
|
|
20989
21179
|
fs8.copyFileSync(srcPath, destPath);
|
|
21180
|
+
onFile?.(path9.relative(baseSrc, srcPath));
|
|
21181
|
+
}
|
|
21182
|
+
}
|
|
21183
|
+
}
|
|
21184
|
+
|
|
21185
|
+
// src/scaffold-doc.ts
|
|
21186
|
+
import * as fs9 from "fs";
|
|
21187
|
+
import * as path10 from "path";
|
|
21188
|
+
var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
|
|
21189
|
+
function slugify3(input) {
|
|
21190
|
+
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
21191
|
+
}
|
|
21192
|
+
function isoDate(today) {
|
|
21193
|
+
return today.toISOString().slice(0, 10);
|
|
21194
|
+
}
|
|
21195
|
+
function nextSeq(dir) {
|
|
21196
|
+
let max = 0;
|
|
21197
|
+
try {
|
|
21198
|
+
for (const entry of fs9.readdirSync(dir)) {
|
|
21199
|
+
const match = /^(\d{1,4})-/.exec(entry);
|
|
21200
|
+
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
21201
|
+
}
|
|
21202
|
+
} catch {
|
|
21203
|
+
}
|
|
21204
|
+
return String(max + 1).padStart(4, "0");
|
|
21205
|
+
}
|
|
21206
|
+
var COMPONENTS = "../../../components";
|
|
21207
|
+
var TEMPLATE_SPECS = {
|
|
21208
|
+
adr: {
|
|
21209
|
+
subdir: "adr",
|
|
21210
|
+
filename: (slug2, ctx) => `${ctx.seq}-${slug2}`,
|
|
21211
|
+
content: (ctx) => `---
|
|
21212
|
+
title: 'ADR ${ctx.seq} \u2014 ${ctx.name}'
|
|
21213
|
+
description: '${ctx.name}'
|
|
21214
|
+
# Link the stories that prove this decision. The badge under the title turns
|
|
21215
|
+
# red the moment any of them fail, so this record can't drift from the code.
|
|
21216
|
+
verifiedBy: []
|
|
21217
|
+
---
|
|
21218
|
+
|
|
21219
|
+
## Status
|
|
21220
|
+
|
|
21221
|
+
**Proposed** \u2014 proposed \xB7 accepted \xB7 superseded \xB7 deprecated
|
|
21222
|
+
|
|
21223
|
+
## Context
|
|
21224
|
+
|
|
21225
|
+
_What problem are we solving? What constraints and forces apply?_
|
|
21226
|
+
|
|
21227
|
+
## Decision
|
|
21228
|
+
|
|
21229
|
+
_What did we decide to do?_
|
|
21230
|
+
|
|
21231
|
+
## Consequences
|
|
21232
|
+
|
|
21233
|
+
_What becomes easier, and what becomes harder, as a result?_
|
|
21234
|
+
|
|
21235
|
+
## Verified by
|
|
21236
|
+
|
|
21237
|
+
Add the story ids or tags that exercise this decision to \`verifiedBy\` in the
|
|
21238
|
+
frontmatter above. Until you do, the badge reads **Unverified** \u2014 by design.
|
|
21239
|
+
`
|
|
21240
|
+
},
|
|
21241
|
+
runbook: {
|
|
21242
|
+
subdir: "runbooks",
|
|
21243
|
+
filename: (slug2) => slug2,
|
|
21244
|
+
content: (ctx) => `---
|
|
21245
|
+
title: 'Runbook \u2014 ${ctx.name}'
|
|
21246
|
+
description: 'Operational runbook for ${ctx.name}'
|
|
21247
|
+
---
|
|
21248
|
+
|
|
21249
|
+
import Checklist from '${COMPONENTS}/Checklist.astro';
|
|
21250
|
+
import VerifiedStep from '${COMPONENTS}/VerifiedStep.astro';
|
|
21251
|
+
|
|
21252
|
+
_When to use this runbook, prerequisites, and who to contact._
|
|
21253
|
+
|
|
21254
|
+
## Steps
|
|
21255
|
+
|
|
21256
|
+
Each step linked with \`story=\` shows a live green check when its test passed in
|
|
21257
|
+
the last run \u2014 so this runbook is trustworthy, not aspirational.
|
|
21258
|
+
|
|
21259
|
+
<Checklist>
|
|
21260
|
+
<VerifiedStep story="">Describe the first action, and link the story that verifies it.</VerifiedStep>
|
|
21261
|
+
<VerifiedStep>A manual step with no automated check.</VerifiedStep>
|
|
21262
|
+
</Checklist>
|
|
21263
|
+
|
|
21264
|
+
## Rollback
|
|
21265
|
+
|
|
21266
|
+
_How to safely undo if something goes wrong._
|
|
21267
|
+
`
|
|
21268
|
+
},
|
|
21269
|
+
"decision-log": {
|
|
21270
|
+
subdir: "decisions",
|
|
21271
|
+
filename: (slug2) => slug2,
|
|
21272
|
+
content: (ctx) => `---
|
|
21273
|
+
title: 'Decision log \u2014 ${ctx.name}'
|
|
21274
|
+
description: 'Running log of decisions for ${ctx.name}'
|
|
21275
|
+
---
|
|
21276
|
+
|
|
21277
|
+
A lightweight running log. For weightier decisions, scaffold a full ADR with
|
|
21278
|
+
\`executable-stories new adr\`.
|
|
21279
|
+
|
|
21280
|
+
| Date | Decision | Owner | Verified by |
|
|
21281
|
+
| ---- | -------- | ----- | ----------- |
|
|
21282
|
+
| ${ctx.isoDate} | _What was decided_ | _Who_ | _story id or tag_ |
|
|
21283
|
+
`
|
|
21284
|
+
},
|
|
21285
|
+
incident: {
|
|
21286
|
+
subdir: "incidents",
|
|
21287
|
+
filename: (slug2, ctx) => `${ctx.isoDate}-${slug2}`,
|
|
21288
|
+
content: (ctx) => `---
|
|
21289
|
+
title: 'Incident \u2014 ${ctx.name}'
|
|
21290
|
+
description: 'Post-mortem for ${ctx.name}'
|
|
21291
|
+
# Link the regression story added to stop this recurring.
|
|
21292
|
+
verifiedBy: []
|
|
21293
|
+
---
|
|
21294
|
+
|
|
21295
|
+
## Summary
|
|
21296
|
+
|
|
21297
|
+
_What happened, who was affected, and for how long._
|
|
21298
|
+
|
|
21299
|
+
## Timeline
|
|
21300
|
+
|
|
21301
|
+
| Time | Event |
|
|
21302
|
+
| ---- | ----- |
|
|
21303
|
+
| ${ctx.isoDate} | Detected |
|
|
21304
|
+
|
|
21305
|
+
## Root cause
|
|
21306
|
+
|
|
21307
|
+
_The underlying cause, not just the trigger._
|
|
21308
|
+
|
|
21309
|
+
## Resolution
|
|
21310
|
+
|
|
21311
|
+
_How it was fixed._
|
|
21312
|
+
|
|
21313
|
+
## Action items
|
|
21314
|
+
|
|
21315
|
+
- [ ] Add a regression story and link it in \`verifiedBy\` so a silent recurrence
|
|
21316
|
+
becomes a failing badge.
|
|
21317
|
+
`
|
|
21318
|
+
}
|
|
21319
|
+
};
|
|
21320
|
+
function isTemplateName(value) {
|
|
21321
|
+
return TEMPLATES.includes(value);
|
|
21322
|
+
}
|
|
21323
|
+
function scaffoldDoc(options) {
|
|
21324
|
+
const { template } = options;
|
|
21325
|
+
if (!isTemplateName(template)) {
|
|
21326
|
+
throw new Error(
|
|
21327
|
+
`Unknown template "${template}". Available: ${TEMPLATES.join(", ")}.`
|
|
21328
|
+
);
|
|
21329
|
+
}
|
|
21330
|
+
const spec = TEMPLATE_SPECS[template];
|
|
21331
|
+
const baseDir = options.baseDir ?? path10.join("src", "content", "docs");
|
|
21332
|
+
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
21333
|
+
const name = (options.name ?? "").trim() || defaultName(template);
|
|
21334
|
+
const slug2 = slugify3(name);
|
|
21335
|
+
const dir = path10.join(baseDir, spec.subdir);
|
|
21336
|
+
const ctx = {
|
|
21337
|
+
name,
|
|
21338
|
+
slug: slug2,
|
|
21339
|
+
isoDate: isoDate(today),
|
|
21340
|
+
seq: nextSeq(dir)
|
|
21341
|
+
};
|
|
21342
|
+
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
21343
|
+
const filePath = path10.join(dir, filename);
|
|
21344
|
+
if (fs9.existsSync(filePath) && !options.force) {
|
|
21345
|
+
throw new Error(
|
|
21346
|
+
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
21347
|
+
);
|
|
21348
|
+
}
|
|
21349
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
21350
|
+
fs9.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
21351
|
+
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
21352
|
+
}
|
|
21353
|
+
function defaultName(template) {
|
|
21354
|
+
switch (template) {
|
|
21355
|
+
case "adr":
|
|
21356
|
+
return "Untitled decision";
|
|
21357
|
+
case "runbook":
|
|
21358
|
+
return "Untitled runbook";
|
|
21359
|
+
case "decision-log":
|
|
21360
|
+
return "Decisions";
|
|
21361
|
+
case "incident":
|
|
21362
|
+
return "Untitled incident";
|
|
21363
|
+
}
|
|
21364
|
+
}
|
|
21365
|
+
function titleFor2(template, ctx) {
|
|
21366
|
+
switch (template) {
|
|
21367
|
+
case "adr":
|
|
21368
|
+
return `ADR ${ctx.seq} \u2014 ${ctx.name}`;
|
|
21369
|
+
case "runbook":
|
|
21370
|
+
return `Runbook \u2014 ${ctx.name}`;
|
|
21371
|
+
case "decision-log":
|
|
21372
|
+
return `Decision log \u2014 ${ctx.name}`;
|
|
21373
|
+
case "incident":
|
|
21374
|
+
return `Incident \u2014 ${ctx.name}`;
|
|
21375
|
+
}
|
|
21376
|
+
}
|
|
21377
|
+
|
|
21378
|
+
// src/check-links.ts
|
|
21379
|
+
import * as fs10 from "fs";
|
|
21380
|
+
import * as path11 from "path";
|
|
21381
|
+
function stripCode(markdown) {
|
|
21382
|
+
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
21383
|
+
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
21384
|
+
return out;
|
|
21385
|
+
}
|
|
21386
|
+
function extractLinks(markdown) {
|
|
21387
|
+
const stripped = stripCode(markdown);
|
|
21388
|
+
const found = [];
|
|
21389
|
+
const mdRe = /!?\[[^\]]*\]\(\s*<?([^)\s"'<>]+)>?(?:\s+["'][^"']*["'])?\s*\)/g;
|
|
21390
|
+
let match;
|
|
21391
|
+
while ((match = mdRe.exec(stripped)) !== null) {
|
|
21392
|
+
found.push(match[1].trim());
|
|
21393
|
+
}
|
|
21394
|
+
const htmlRe = /<[a-z][^>]*\b(?:href|src)=["']([^"']+)["']/gi;
|
|
21395
|
+
while ((match = htmlRe.exec(stripped)) !== null) {
|
|
21396
|
+
found.push(match[1].trim());
|
|
21397
|
+
}
|
|
21398
|
+
return found.filter(Boolean);
|
|
21399
|
+
}
|
|
21400
|
+
function classifyLink(link2) {
|
|
21401
|
+
if (/^(?:https?:)?\/\//i.test(link2)) return "external";
|
|
21402
|
+
if (/^mailto:/i.test(link2)) return "mail";
|
|
21403
|
+
if (link2.startsWith("#")) return "anchor";
|
|
21404
|
+
if (link2.startsWith("/")) return "root";
|
|
21405
|
+
return "internal";
|
|
21406
|
+
}
|
|
21407
|
+
function resolutionCandidates(fromFile, link2) {
|
|
21408
|
+
const withoutAnchor = link2.split("#")[0];
|
|
21409
|
+
if (!withoutAnchor) return [];
|
|
21410
|
+
const base = path11.resolve(path11.dirname(fromFile), withoutAnchor);
|
|
21411
|
+
const candidates = [base];
|
|
21412
|
+
if (!path11.extname(base)) {
|
|
21413
|
+
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
21414
|
+
candidates.push(path11.join(base, "index.md"), path11.join(base, "index.mdx"));
|
|
21415
|
+
}
|
|
21416
|
+
return candidates;
|
|
21417
|
+
}
|
|
21418
|
+
function resolvesOnDisk(fromFile, link2) {
|
|
21419
|
+
return resolutionCandidates(fromFile, link2).some(
|
|
21420
|
+
(candidate) => fs10.existsSync(candidate) && fs10.statSync(candidate).isFile()
|
|
21421
|
+
);
|
|
21422
|
+
}
|
|
21423
|
+
function collectDocFiles(target) {
|
|
21424
|
+
const stat = fs10.statSync(target);
|
|
21425
|
+
if (stat.isFile()) return [target];
|
|
21426
|
+
const out = [];
|
|
21427
|
+
const walk = (dir) => {
|
|
21428
|
+
for (const entry of fs10.readdirSync(dir, { withFileTypes: true })) {
|
|
21429
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
21430
|
+
const full = path11.join(dir, entry.name);
|
|
21431
|
+
if (entry.isDirectory()) walk(full);
|
|
21432
|
+
else if (/\.mdx?$/.test(entry.name)) out.push(full);
|
|
21433
|
+
}
|
|
21434
|
+
};
|
|
21435
|
+
walk(target);
|
|
21436
|
+
return out;
|
|
21437
|
+
}
|
|
21438
|
+
async function isExternalAlive(url, timeoutMs) {
|
|
21439
|
+
const attempt = async (method) => {
|
|
21440
|
+
const controller = new AbortController();
|
|
21441
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21442
|
+
try {
|
|
21443
|
+
const res = await fetch(url, { method, signal: controller.signal, redirect: "follow" });
|
|
21444
|
+
return res.status < 400;
|
|
21445
|
+
} finally {
|
|
21446
|
+
clearTimeout(timer);
|
|
21447
|
+
}
|
|
21448
|
+
};
|
|
21449
|
+
try {
|
|
21450
|
+
return await attempt("HEAD");
|
|
21451
|
+
} catch {
|
|
21452
|
+
try {
|
|
21453
|
+
return await attempt("GET");
|
|
21454
|
+
} catch {
|
|
21455
|
+
return false;
|
|
21456
|
+
}
|
|
21457
|
+
}
|
|
21458
|
+
}
|
|
21459
|
+
async function checkLinks(options) {
|
|
21460
|
+
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
21461
|
+
if (!fs10.existsSync(target)) {
|
|
21462
|
+
throw new Error(`Path not found: ${target}`);
|
|
21463
|
+
}
|
|
21464
|
+
const files = collectDocFiles(target);
|
|
21465
|
+
const broken = [];
|
|
21466
|
+
let linksChecked = 0;
|
|
21467
|
+
let externalChecked = 0;
|
|
21468
|
+
let skipped = 0;
|
|
21469
|
+
const externalCache = /* @__PURE__ */ new Map();
|
|
21470
|
+
for (const file of files) {
|
|
21471
|
+
const content = fs10.readFileSync(file, "utf8");
|
|
21472
|
+
for (const link2 of extractLinks(content)) {
|
|
21473
|
+
const kind = classifyLink(link2);
|
|
21474
|
+
if (kind === "anchor" || kind === "mail" || kind === "root") {
|
|
21475
|
+
skipped += 1;
|
|
21476
|
+
continue;
|
|
21477
|
+
}
|
|
21478
|
+
if (kind === "external") {
|
|
21479
|
+
if (!checkExternal) {
|
|
21480
|
+
skipped += 1;
|
|
21481
|
+
continue;
|
|
21482
|
+
}
|
|
21483
|
+
externalChecked += 1;
|
|
21484
|
+
linksChecked += 1;
|
|
21485
|
+
let pending = externalCache.get(link2);
|
|
21486
|
+
if (!pending) {
|
|
21487
|
+
pending = isExternalAlive(link2, externalTimeoutMs);
|
|
21488
|
+
externalCache.set(link2, pending);
|
|
21489
|
+
}
|
|
21490
|
+
if (!await pending) {
|
|
21491
|
+
broken.push({ file, link: link2, reason: "external URL unreachable" });
|
|
21492
|
+
}
|
|
21493
|
+
continue;
|
|
21494
|
+
}
|
|
21495
|
+
linksChecked += 1;
|
|
21496
|
+
if (!resolvesOnDisk(file, link2)) {
|
|
21497
|
+
broken.push({ file, link: link2, reason: "target file not found" });
|
|
21498
|
+
}
|
|
21499
|
+
}
|
|
21500
|
+
}
|
|
21501
|
+
return {
|
|
21502
|
+
filesScanned: files.length,
|
|
21503
|
+
linksChecked,
|
|
21504
|
+
brokenCount: broken.length,
|
|
21505
|
+
broken,
|
|
21506
|
+
externalChecked,
|
|
21507
|
+
skipped
|
|
21508
|
+
};
|
|
21509
|
+
}
|
|
21510
|
+
function formatLinkReport(report) {
|
|
21511
|
+
const lines = [];
|
|
21512
|
+
lines.push(
|
|
21513
|
+
`Scanned ${report.filesScanned} file(s), checked ${report.linksChecked} link(s) (${report.skipped} skipped).`
|
|
21514
|
+
);
|
|
21515
|
+
if (report.brokenCount === 0) {
|
|
21516
|
+
lines.push("\u2713 No broken links.");
|
|
21517
|
+
} else {
|
|
21518
|
+
lines.push(`\u2715 ${report.brokenCount} broken link(s):`);
|
|
21519
|
+
for (const b of report.broken) {
|
|
21520
|
+
lines.push(` ${b.file}: ${b.link} \u2014 ${b.reason}`);
|
|
20990
21521
|
}
|
|
20991
21522
|
}
|
|
21523
|
+
return lines.join("\n");
|
|
21524
|
+
}
|
|
21525
|
+
|
|
21526
|
+
// src/import-openapi.ts
|
|
21527
|
+
import * as fs11 from "fs";
|
|
21528
|
+
import * as path12 from "path";
|
|
21529
|
+
import { parse as parseYamlString } from "yaml";
|
|
21530
|
+
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
21531
|
+
function parseYaml(raw, specPath) {
|
|
21532
|
+
try {
|
|
21533
|
+
return parseYamlString(raw);
|
|
21534
|
+
} catch (err) {
|
|
21535
|
+
throw new Error(`Could not parse YAML spec ${specPath}: ${err.message}`, {
|
|
21536
|
+
cause: err
|
|
21537
|
+
});
|
|
21538
|
+
}
|
|
21539
|
+
}
|
|
21540
|
+
function parseSpec(specPath) {
|
|
21541
|
+
if (!fs11.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
21542
|
+
const raw = fs11.readFileSync(specPath, "utf8");
|
|
21543
|
+
const ext = path12.extname(specPath).toLowerCase();
|
|
21544
|
+
if (ext === ".json") return JSON.parse(raw);
|
|
21545
|
+
if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
|
|
21546
|
+
try {
|
|
21547
|
+
return JSON.parse(raw);
|
|
21548
|
+
} catch {
|
|
21549
|
+
return parseYaml(raw, specPath);
|
|
21550
|
+
}
|
|
21551
|
+
}
|
|
21552
|
+
function extractEndpoints(spec) {
|
|
21553
|
+
const paths = spec.paths ?? {};
|
|
21554
|
+
const endpoints = [];
|
|
21555
|
+
for (const [route, item] of Object.entries(paths)) {
|
|
21556
|
+
if (!item || typeof item !== "object") continue;
|
|
21557
|
+
for (const method of HTTP_METHODS) {
|
|
21558
|
+
const op = item[method];
|
|
21559
|
+
if (!op || typeof op !== "object") continue;
|
|
21560
|
+
const tags = Array.isArray(op.tags) && op.tags.length > 0 ? op.tags : ["API"];
|
|
21561
|
+
endpoints.push({
|
|
21562
|
+
method: method.toUpperCase(),
|
|
21563
|
+
path: route,
|
|
21564
|
+
operationId: typeof op.operationId === "string" ? op.operationId : void 0,
|
|
21565
|
+
summary: typeof op.summary === "string" && op.summary || typeof op.description === "string" && op.description || "",
|
|
21566
|
+
tag: String(tags[0])
|
|
21567
|
+
});
|
|
21568
|
+
}
|
|
21569
|
+
}
|
|
21570
|
+
return endpoints;
|
|
21571
|
+
}
|
|
21572
|
+
function loadScenarios(runFile) {
|
|
21573
|
+
if (!runFile) return [];
|
|
21574
|
+
if (!fs11.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
21575
|
+
const report = JSON.parse(fs11.readFileSync(runFile, "utf8"));
|
|
21576
|
+
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
21577
|
+
}
|
|
21578
|
+
function endpointRefs(endpoint) {
|
|
21579
|
+
const refs = [
|
|
21580
|
+
endpoint.operationId,
|
|
21581
|
+
`${endpoint.method} ${endpoint.path}`,
|
|
21582
|
+
endpoint.path
|
|
21583
|
+
].filter((r) => Boolean(r));
|
|
21584
|
+
return refs;
|
|
21585
|
+
}
|
|
21586
|
+
function scenarioMatchesEndpoint(scenario, refs) {
|
|
21587
|
+
const tags = scenario.tags ?? [];
|
|
21588
|
+
return refs.some(
|
|
21589
|
+
(ref) => scenario.id === ref || scenario.title === ref || tags.includes(ref)
|
|
21590
|
+
);
|
|
21591
|
+
}
|
|
21592
|
+
function computeCoverage(endpoints, scenarios) {
|
|
21593
|
+
return endpoints.map((endpoint) => {
|
|
21594
|
+
const refs = endpointRefs(endpoint);
|
|
21595
|
+
const matched = scenarios.filter((s) => scenarioMatchesEndpoint(s, refs));
|
|
21596
|
+
let status;
|
|
21597
|
+
if (matched.length === 0) status = "uncovered";
|
|
21598
|
+
else if (matched.some((s) => s.status === "failed")) status = "failing";
|
|
21599
|
+
else status = "covered";
|
|
21600
|
+
return {
|
|
21601
|
+
endpoint,
|
|
21602
|
+
status,
|
|
21603
|
+
stories: matched.map((s) => ({
|
|
21604
|
+
id: s.id ?? s.title ?? "",
|
|
21605
|
+
title: s.title ?? s.id ?? "story",
|
|
21606
|
+
status: s.status ?? "passed"
|
|
21607
|
+
}))
|
|
21608
|
+
};
|
|
21609
|
+
});
|
|
21610
|
+
}
|
|
21611
|
+
function slug(input) {
|
|
21612
|
+
return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "api";
|
|
21613
|
+
}
|
|
21614
|
+
function yamlQuote(value) {
|
|
21615
|
+
return value.replace(/'/g, "''");
|
|
21616
|
+
}
|
|
21617
|
+
function coverageSummary(rows) {
|
|
21618
|
+
return {
|
|
21619
|
+
total: rows.length,
|
|
21620
|
+
covered: rows.filter((r) => r.status === "covered").length,
|
|
21621
|
+
failing: rows.filter((r) => r.status === "failing").length,
|
|
21622
|
+
uncovered: rows.filter((r) => r.status === "uncovered").length
|
|
21623
|
+
};
|
|
21624
|
+
}
|
|
21625
|
+
function renderTagPage(tag, rows, hasRun) {
|
|
21626
|
+
const endpoints = rows.map((r) => ({
|
|
21627
|
+
method: r.endpoint.method,
|
|
21628
|
+
path: r.endpoint.path,
|
|
21629
|
+
summary: r.endpoint.summary || "",
|
|
21630
|
+
status: r.status,
|
|
21631
|
+
stories: r.stories
|
|
21632
|
+
}));
|
|
21633
|
+
const summary = coverageSummary(rows);
|
|
21634
|
+
return `---
|
|
21635
|
+
title: 'API \u2014 ${yamlQuote(tag)}'
|
|
21636
|
+
description: 'Endpoints for ${yamlQuote(tag)}, linked to the stories that exercise them.'
|
|
21637
|
+
---
|
|
21638
|
+
|
|
21639
|
+
import ApiOperations from '@components/ApiOperations.astro';
|
|
21640
|
+
|
|
21641
|
+
<ApiOperations
|
|
21642
|
+
tag={${JSON.stringify(tag)}}
|
|
21643
|
+
hasRun={${hasRun}}
|
|
21644
|
+
summary={${JSON.stringify(summary)}}
|
|
21645
|
+
endpoints={${JSON.stringify(endpoints)}}
|
|
21646
|
+
/>
|
|
21647
|
+
`;
|
|
21648
|
+
}
|
|
21649
|
+
function renderIndex(groups, hasRun, totals) {
|
|
21650
|
+
const rows = [...groups.entries()].map(([tag, eps]) => {
|
|
21651
|
+
const covered = eps.filter((e) => e.status === "covered").length;
|
|
21652
|
+
const cov = hasRun ? ` | ${covered}/${eps.length} covered` : "";
|
|
21653
|
+
return `- [${tag}](./${slug(tag)}/) \u2014 ${eps.length} endpoint(s)${cov}`;
|
|
21654
|
+
}).join("\n");
|
|
21655
|
+
const coverageNote = hasRun ? `
|
|
21656
|
+
**${totals.coveredCount} of ${totals.endpointCount} endpoints** are covered by a passing story.` + (totals.uncoveredCount > 0 ? ` \u26A0 ${totals.uncoveredCount} endpoint(s) have no verifying test.` : "") : "\nRe-run with `--run <story-report.json>` to show per-endpoint test coverage.";
|
|
21657
|
+
return `---
|
|
21658
|
+
title: 'API reference'
|
|
21659
|
+
description: 'API endpoints generated from OpenAPI, linked to verifying stories.'
|
|
21660
|
+
---
|
|
21661
|
+
|
|
21662
|
+
${coverageNote}
|
|
21663
|
+
|
|
21664
|
+
${rows}
|
|
21665
|
+
`;
|
|
21666
|
+
}
|
|
21667
|
+
async function importOpenApi(options) {
|
|
21668
|
+
const spec = parseSpec(options.specPath);
|
|
21669
|
+
const endpoints = extractEndpoints(spec);
|
|
21670
|
+
if (endpoints.length === 0) {
|
|
21671
|
+
throw new Error(`No endpoints found in ${options.specPath} (expected an OpenAPI "paths" object).`);
|
|
21672
|
+
}
|
|
21673
|
+
const scenarios = loadScenarios(options.runFile);
|
|
21674
|
+
const hasRun = Boolean(options.runFile);
|
|
21675
|
+
const coverage = computeCoverage(endpoints, scenarios);
|
|
21676
|
+
const groups = /* @__PURE__ */ new Map();
|
|
21677
|
+
for (const item of coverage) {
|
|
21678
|
+
const list = groups.get(item.endpoint.tag) ?? [];
|
|
21679
|
+
list.push(item);
|
|
21680
|
+
groups.set(item.endpoint.tag, list);
|
|
21681
|
+
}
|
|
21682
|
+
const outputDir = options.outputDir ?? path12.join("src", "content", "docs", "api");
|
|
21683
|
+
if (fs11.existsSync(outputDir) && !options.force) {
|
|
21684
|
+
const entries = fs11.readdirSync(outputDir);
|
|
21685
|
+
if (entries.length > 0) {
|
|
21686
|
+
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
21687
|
+
}
|
|
21688
|
+
}
|
|
21689
|
+
fs11.mkdirSync(outputDir, { recursive: true });
|
|
21690
|
+
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
21691
|
+
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
21692
|
+
fs11.writeFileSync(
|
|
21693
|
+
path12.join(outputDir, "index.mdx"),
|
|
21694
|
+
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
21695
|
+
"utf8"
|
|
21696
|
+
);
|
|
21697
|
+
for (const [tag, rows] of groups) {
|
|
21698
|
+
const dir = path12.join(outputDir, slug(tag));
|
|
21699
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
21700
|
+
fs11.writeFileSync(path12.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
21701
|
+
}
|
|
21702
|
+
return {
|
|
21703
|
+
outputDir,
|
|
21704
|
+
pageCount: groups.size + 1,
|
|
21705
|
+
endpointCount: endpoints.length,
|
|
21706
|
+
coveredCount,
|
|
21707
|
+
uncoveredCount
|
|
21708
|
+
};
|
|
21709
|
+
}
|
|
21710
|
+
|
|
21711
|
+
// src/build-docs.ts
|
|
21712
|
+
import * as fs12 from "fs";
|
|
21713
|
+
import * as path13 from "path";
|
|
21714
|
+
var BuildDocsError = class extends Error {
|
|
21715
|
+
constructor(message, kind) {
|
|
21716
|
+
super(message);
|
|
21717
|
+
this.kind = kind;
|
|
21718
|
+
this.name = "BuildDocsError";
|
|
21719
|
+
}
|
|
21720
|
+
};
|
|
21721
|
+
var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
|
|
21722
|
+
function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
|
|
21723
|
+
if (!fs12.existsSync(reportPath)) return 0;
|
|
21724
|
+
const report = JSON.parse(fs12.readFileSync(reportPath, "utf8"));
|
|
21725
|
+
let copied = 0;
|
|
21726
|
+
const bundle = (value) => {
|
|
21727
|
+
const rel = copyAsset(path13.resolve(value), assetsDir);
|
|
21728
|
+
copied++;
|
|
21729
|
+
return `${baseUrl}/${path13.basename(rel)}`;
|
|
21730
|
+
};
|
|
21731
|
+
const visit = (entries) => {
|
|
21732
|
+
for (const entry of entries ?? []) {
|
|
21733
|
+
const e = entry;
|
|
21734
|
+
if (e.kind === "screenshot" || e.kind === "video") {
|
|
21735
|
+
if (typeof e.path === "string" && !isRemote(e.path) && fs12.existsSync(e.path)) {
|
|
21736
|
+
e.path = bundle(e.path);
|
|
21737
|
+
}
|
|
21738
|
+
if (typeof e.poster === "string" && !isRemote(e.poster) && fs12.existsSync(e.poster)) {
|
|
21739
|
+
e.poster = bundle(e.poster);
|
|
21740
|
+
}
|
|
21741
|
+
}
|
|
21742
|
+
if (Array.isArray(e.children)) visit(e.children);
|
|
21743
|
+
}
|
|
21744
|
+
};
|
|
21745
|
+
for (const feature of report.features ?? []) {
|
|
21746
|
+
for (const scenario of feature.scenarios ?? []) {
|
|
21747
|
+
visit(scenario.docEntries);
|
|
21748
|
+
}
|
|
21749
|
+
}
|
|
21750
|
+
if (copied > 0) {
|
|
21751
|
+
fs12.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
21752
|
+
}
|
|
21753
|
+
return copied;
|
|
21754
|
+
}
|
|
21755
|
+
function clearGeneratedPages(dir) {
|
|
21756
|
+
if (!fs12.existsSync(dir)) return;
|
|
21757
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
21758
|
+
const full = path13.join(dir, entry.name);
|
|
21759
|
+
if (entry.isDirectory()) {
|
|
21760
|
+
clearGeneratedPages(full);
|
|
21761
|
+
if (fs12.readdirSync(full).length === 0) fs12.rmdirSync(full);
|
|
21762
|
+
} else if (/\.mdx?$/.test(entry.name)) {
|
|
21763
|
+
fs12.rmSync(full);
|
|
21764
|
+
}
|
|
21765
|
+
}
|
|
21766
|
+
}
|
|
21767
|
+
function loadCanonicalRun(rawRunPath, synthesize) {
|
|
21768
|
+
try {
|
|
21769
|
+
const data = JSON.parse(fs12.readFileSync(path13.resolve(rawRunPath), "utf8"));
|
|
21770
|
+
if (data.schemaVersion !== 1) {
|
|
21771
|
+
throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
|
|
21772
|
+
}
|
|
21773
|
+
const schemaResult = validateRawRun(data);
|
|
21774
|
+
if (!schemaResult.valid) {
|
|
21775
|
+
throw new BuildDocsError(
|
|
21776
|
+
`Schema validation failed:
|
|
21777
|
+
${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
|
|
21778
|
+
"schema"
|
|
21779
|
+
);
|
|
21780
|
+
}
|
|
21781
|
+
let raw = data;
|
|
21782
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
21783
|
+
const canonical = canonicalizeRun(raw);
|
|
21784
|
+
assertValidRun(canonical);
|
|
21785
|
+
return canonical;
|
|
21786
|
+
} catch (err) {
|
|
21787
|
+
if (err instanceof BuildDocsError) throw err;
|
|
21788
|
+
throw new BuildDocsError(`Could not read raw run "${rawRunPath}": ${err.message}`, "input");
|
|
21789
|
+
}
|
|
21790
|
+
}
|
|
21791
|
+
async function buildDocs(options) {
|
|
21792
|
+
const siteDir = path13.resolve(options.siteDir);
|
|
21793
|
+
const storiesPublicDir = path13.join(siteDir, "public", "stories");
|
|
21794
|
+
const assetsDir = path13.join(storiesPublicDir, "assets");
|
|
21795
|
+
const storyPagesDir = path13.join(siteDir, "src", "content", "docs", "stories");
|
|
21796
|
+
const apiDir = path13.join(siteDir, "src", "content", "docs", "api");
|
|
21797
|
+
const reportPath = path13.join(storiesPublicDir, "story-report.json");
|
|
21798
|
+
const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
|
|
21799
|
+
try {
|
|
21800
|
+
await new ReportGenerator({
|
|
21801
|
+
formats: ["story-report-json"],
|
|
21802
|
+
outputDir: storiesPublicDir,
|
|
21803
|
+
outputName: "story-report"
|
|
21804
|
+
}).generate(canonical);
|
|
21805
|
+
clearGeneratedPages(storyPagesDir);
|
|
21806
|
+
await new ReportGenerator({
|
|
21807
|
+
formats: ["astro"],
|
|
21808
|
+
outputDir: storyPagesDir,
|
|
21809
|
+
outputName: "index",
|
|
21810
|
+
output: { mode: "colocated", colocatedStyle: "flat" },
|
|
21811
|
+
assetMode: "copy",
|
|
21812
|
+
astro: { assetsDir, assetsBaseUrl: "/stories/assets" }
|
|
21813
|
+
}).generate(canonical);
|
|
21814
|
+
const bundledAssets = bundleExplorerAssets(reportPath, assetsDir);
|
|
21815
|
+
let apiPages = 0;
|
|
21816
|
+
if (options.openapiPath) {
|
|
21817
|
+
const res = await importOpenApi({
|
|
21818
|
+
specPath: path13.resolve(options.openapiPath),
|
|
21819
|
+
outputDir: apiDir,
|
|
21820
|
+
runFile: reportPath,
|
|
21821
|
+
force: true
|
|
21822
|
+
});
|
|
21823
|
+
apiPages = res.pageCount;
|
|
21824
|
+
}
|
|
21825
|
+
return { siteDir, bundledAssets, apiPages };
|
|
21826
|
+
} catch (err) {
|
|
21827
|
+
if (err instanceof BuildDocsError) throw err;
|
|
21828
|
+
throw new BuildDocsError(`Generation failed: ${err.message}`, "generation");
|
|
21829
|
+
}
|
|
20992
21830
|
}
|
|
20993
21831
|
|
|
20994
21832
|
// src/config.ts
|
|
20995
|
-
import { existsSync as
|
|
20996
|
-
import { resolve as
|
|
21833
|
+
import { existsSync as existsSync11 } from "fs";
|
|
21834
|
+
import { resolve as resolve9 } from "path";
|
|
20997
21835
|
async function loadConfig(configPath) {
|
|
20998
|
-
const resolved = configPath ?
|
|
20999
|
-
if (!
|
|
21836
|
+
const resolved = configPath ? resolve9(configPath) : resolve9(process.cwd(), "executable-stories.config.js");
|
|
21837
|
+
if (!existsSync11(resolved)) return {};
|
|
21000
21838
|
const mod = await import(resolved);
|
|
21001
21839
|
const config = mod.default;
|
|
21002
21840
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -21027,6 +21865,9 @@ USAGE
|
|
|
21027
21865
|
executable-stories validate <file>
|
|
21028
21866
|
executable-stories validate --stdin
|
|
21029
21867
|
executable-stories init-astro [directory]
|
|
21868
|
+
executable-stories new <template> "<name>" [options]
|
|
21869
|
+
executable-stories check-links <dir> [options]
|
|
21870
|
+
executable-stories import-openapi <spec> [options]
|
|
21030
21871
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
21031
21872
|
executable-stories publish-jira <file.adf.json> [options]
|
|
21032
21873
|
|
|
@@ -21038,6 +21879,9 @@ SUBCOMMANDS
|
|
|
21038
21879
|
list List scenarios from a test run (text table or JSON)
|
|
21039
21880
|
validate Validate a JSON file against the schema (no output generated)
|
|
21040
21881
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
21882
|
+
new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
|
|
21883
|
+
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
21884
|
+
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
21041
21885
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
21042
21886
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
21043
21887
|
|
|
@@ -21164,9 +22008,9 @@ async function parseCliArgs(argv) {
|
|
|
21164
22008
|
process.exit(EXIT_SUCCESS);
|
|
21165
22009
|
}
|
|
21166
22010
|
const subcommand = args[0];
|
|
21167
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22011
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
21168
22012
|
console.error(
|
|
21169
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
|
|
22013
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
21170
22014
|
);
|
|
21171
22015
|
process.exit(EXIT_USAGE);
|
|
21172
22016
|
}
|
|
@@ -21182,8 +22026,14 @@ async function parseCliArgs(argv) {
|
|
|
21182
22026
|
const initArgs = args.slice(1);
|
|
21183
22027
|
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
21184
22028
|
const force = initArgs.includes("--force");
|
|
22029
|
+
const update = initArgs.includes("--update");
|
|
21185
22030
|
try {
|
|
21186
|
-
const result = initAstro({ targetDir, force });
|
|
22031
|
+
const result = initAstro({ targetDir, force, update });
|
|
22032
|
+
if (update) {
|
|
22033
|
+
console.log(`Updated framework files in ${result.targetDir} (content left untouched)`);
|
|
22034
|
+
console.log(` Refreshed: ${result.updatedFiles?.length ?? 0} file(s)`);
|
|
22035
|
+
process.exit(EXIT_SUCCESS);
|
|
22036
|
+
}
|
|
21187
22037
|
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
21188
22038
|
console.log("");
|
|
21189
22039
|
console.log("Themes available in src/styles/themes/:");
|
|
@@ -21196,23 +22046,26 @@ async function parseCliArgs(argv) {
|
|
|
21196
22046
|
console.log("");
|
|
21197
22047
|
console.log("To change theme, edit astro.config.mjs customCss array.");
|
|
21198
22048
|
console.log("");
|
|
21199
|
-
console.log("");
|
|
21200
22049
|
console.log("Next steps:");
|
|
21201
22050
|
console.log(` cd ${result.targetDir}`);
|
|
21202
22051
|
console.log(" pnpm install # or npm install");
|
|
21203
22052
|
console.log(" pnpm dev # start the dev server");
|
|
21204
22053
|
console.log("");
|
|
21205
|
-
console.log("Generate story
|
|
21206
|
-
console.log(` executable-stories
|
|
22054
|
+
console.log("Generate everything (story pages, explorer data, API pages) in one step:");
|
|
22055
|
+
console.log(` executable-stories build-docs run.json --site-dir ${result.targetDir} [--openapi spec.json]`);
|
|
21207
22056
|
console.log("");
|
|
21208
|
-
console.log("
|
|
21209
|
-
console.log(` executable-stories
|
|
22057
|
+
console.log("Later, pull template/design improvements without losing your content:");
|
|
22058
|
+
console.log(` executable-stories init-astro ${result.targetDir} --update`);
|
|
21210
22059
|
process.exit(EXIT_SUCCESS);
|
|
21211
22060
|
} catch (err) {
|
|
21212
22061
|
console.error(`Error: ${err.message}`);
|
|
21213
22062
|
process.exit(EXIT_USAGE);
|
|
21214
22063
|
}
|
|
21215
22064
|
}
|
|
22065
|
+
if (subcommand === "new") process.exit(runNew(args.slice(1)));
|
|
22066
|
+
if (subcommand === "check-links") process.exit(await runCheckLinks(args.slice(1)));
|
|
22067
|
+
if (subcommand === "import-openapi") process.exit(await runImportOpenApi(args.slice(1)));
|
|
22068
|
+
if (subcommand === "build-docs") process.exit(await runBuildDocs(args.slice(1)));
|
|
21216
22069
|
const { values, positionals } = parseArgs({
|
|
21217
22070
|
args: args.slice(1),
|
|
21218
22071
|
options: {
|
|
@@ -21468,27 +22321,27 @@ async function readInput(args) {
|
|
|
21468
22321
|
if (args.stdin) {
|
|
21469
22322
|
return readStdin();
|
|
21470
22323
|
}
|
|
21471
|
-
const filePath =
|
|
21472
|
-
if (!
|
|
22324
|
+
const filePath = path14.resolve(args.inputFile);
|
|
22325
|
+
if (!fs13.existsSync(filePath)) {
|
|
21473
22326
|
console.error(`Error: File not found: ${filePath}`);
|
|
21474
22327
|
process.exit(EXIT_USAGE);
|
|
21475
22328
|
}
|
|
21476
|
-
return
|
|
22329
|
+
return fs13.readFileSync(filePath, "utf8");
|
|
21477
22330
|
}
|
|
21478
22331
|
function readFileInput(filePath) {
|
|
21479
|
-
const resolved =
|
|
21480
|
-
if (!
|
|
22332
|
+
const resolved = path14.resolve(filePath);
|
|
22333
|
+
if (!fs13.existsSync(resolved)) {
|
|
21481
22334
|
console.error(`Error: File not found: ${resolved}`);
|
|
21482
22335
|
process.exit(EXIT_USAGE);
|
|
21483
22336
|
}
|
|
21484
|
-
return
|
|
22337
|
+
return fs13.readFileSync(resolved, "utf8");
|
|
21485
22338
|
}
|
|
21486
22339
|
function readStdin() {
|
|
21487
|
-
return new Promise((
|
|
22340
|
+
return new Promise((resolve11, reject) => {
|
|
21488
22341
|
const chunks = [];
|
|
21489
22342
|
process.stdin.setEncoding("utf8");
|
|
21490
22343
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
21491
|
-
process.stdin.on("end", () =>
|
|
22344
|
+
process.stdin.on("end", () => resolve11(chunks.join("")));
|
|
21492
22345
|
process.stdin.on("error", reject);
|
|
21493
22346
|
});
|
|
21494
22347
|
}
|
|
@@ -21614,14 +22467,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
21614
22467
|
}
|
|
21615
22468
|
}
|
|
21616
22469
|
function listBaselineCandidates(currentFile, args) {
|
|
21617
|
-
const baselineDir =
|
|
21618
|
-
const currentResolved =
|
|
21619
|
-
if (!
|
|
22470
|
+
const baselineDir = path14.resolve(args.baselineDir ?? path14.dirname(currentFile));
|
|
22471
|
+
const currentResolved = path14.resolve(currentFile);
|
|
22472
|
+
if (!fs13.existsSync(baselineDir)) {
|
|
21620
22473
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
21621
22474
|
process.exit(EXIT_USAGE);
|
|
21622
22475
|
}
|
|
21623
|
-
const entries =
|
|
21624
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
22476
|
+
const entries = fs13.readdirSync(baselineDir, { withFileTypes: true });
|
|
22477
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path14.join(baselineDir, entry.name)).filter((candidate) => path14.resolve(candidate) !== currentResolved).filter(
|
|
21625
22478
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
21626
22479
|
);
|
|
21627
22480
|
}
|
|
@@ -21629,14 +22482,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
21629
22482
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
21630
22483
|
const comparable = [];
|
|
21631
22484
|
for (const candidate of candidates) {
|
|
21632
|
-
const run = tryNormalizeRunFromText(
|
|
22485
|
+
const run = tryNormalizeRunFromText(fs13.readFileSync(candidate, "utf8"), args);
|
|
21633
22486
|
if (run) {
|
|
21634
22487
|
comparable.push({ file: candidate, run });
|
|
21635
22488
|
}
|
|
21636
22489
|
}
|
|
21637
22490
|
if (comparable.length === 0) {
|
|
21638
22491
|
console.error(
|
|
21639
|
-
`Error: no compatible baseline files found in ${
|
|
22492
|
+
`Error: no compatible baseline files found in ${path14.resolve(args.baselineDir ?? path14.dirname(currentFile))}.`
|
|
21640
22493
|
);
|
|
21641
22494
|
process.exit(EXIT_USAGE);
|
|
21642
22495
|
}
|
|
@@ -21774,9 +22627,9 @@ async function main() {
|
|
|
21774
22627
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
21775
22628
|
}
|
|
21776
22629
|
if (args.emitCanonical) {
|
|
21777
|
-
const outPath =
|
|
21778
|
-
|
|
21779
|
-
|
|
22630
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22631
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22632
|
+
fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
21780
22633
|
}
|
|
21781
22634
|
try {
|
|
21782
22635
|
const result = await generateReports(run, args);
|
|
@@ -21833,9 +22686,9 @@ ${msg}`);
|
|
|
21833
22686
|
}
|
|
21834
22687
|
const run = data;
|
|
21835
22688
|
if (args.emitCanonical) {
|
|
21836
|
-
const outPath =
|
|
21837
|
-
|
|
21838
|
-
|
|
22689
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22690
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22691
|
+
fs13.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
21839
22692
|
}
|
|
21840
22693
|
try {
|
|
21841
22694
|
const result = await generateReports(run, args);
|
|
@@ -21891,9 +22744,9 @@ ${msg}`);
|
|
|
21891
22744
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
21892
22745
|
}
|
|
21893
22746
|
if (args.emitCanonical) {
|
|
21894
|
-
const outPath =
|
|
21895
|
-
|
|
21896
|
-
|
|
22747
|
+
const outPath = path14.resolve(args.emitCanonical);
|
|
22748
|
+
fs13.mkdirSync(path14.dirname(outPath), { recursive: true });
|
|
22749
|
+
fs13.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
21897
22750
|
}
|
|
21898
22751
|
try {
|
|
21899
22752
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -21918,9 +22771,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
21918
22771
|
const ext = formatter.fileExtension ?? formatName;
|
|
21919
22772
|
const baseName = args.outputName ?? "report";
|
|
21920
22773
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
21921
|
-
const filepath =
|
|
21922
|
-
|
|
21923
|
-
|
|
22774
|
+
const filepath = path14.join(outputDir, filename);
|
|
22775
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
22776
|
+
fs13.writeFileSync(filepath, content, "utf8");
|
|
21924
22777
|
console.log(`Generated: ${filepath}`);
|
|
21925
22778
|
} catch (err) {
|
|
21926
22779
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -21970,13 +22823,13 @@ async function dispatchNotifications(run, args) {
|
|
|
21970
22823
|
}
|
|
21971
22824
|
function runHistoryPipeline(run, args) {
|
|
21972
22825
|
if (!args.historyFile) return;
|
|
21973
|
-
const historyPath =
|
|
22826
|
+
const historyPath = path14.resolve(args.historyFile);
|
|
21974
22827
|
const store = loadHistory(
|
|
21975
22828
|
{ filePath: historyPath },
|
|
21976
22829
|
{
|
|
21977
22830
|
readFile: (p) => {
|
|
21978
22831
|
try {
|
|
21979
|
-
return
|
|
22832
|
+
return fs13.readFileSync(p, "utf8");
|
|
21980
22833
|
} catch {
|
|
21981
22834
|
return void 0;
|
|
21982
22835
|
}
|
|
@@ -21989,11 +22842,11 @@ function runHistoryPipeline(run, args) {
|
|
|
21989
22842
|
run,
|
|
21990
22843
|
maxRuns: args.maxHistoryRuns
|
|
21991
22844
|
});
|
|
21992
|
-
const dir =
|
|
21993
|
-
|
|
22845
|
+
const dir = path14.dirname(historyPath);
|
|
22846
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
21994
22847
|
saveHistory(
|
|
21995
22848
|
{ filePath: historyPath, store: updated },
|
|
21996
|
-
{ writeFile: (p, content) =>
|
|
22849
|
+
{ writeFile: (p, content) => fs13.writeFileSync(p, content, "utf8") }
|
|
21997
22850
|
);
|
|
21998
22851
|
let metricsCount = 0;
|
|
21999
22852
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -22140,11 +22993,11 @@ function writeReviewReport(review, args) {
|
|
|
22140
22993
|
const outputDir = args.outputDir ?? "reports";
|
|
22141
22994
|
const baseName = args.outputName ?? "evidence-review";
|
|
22142
22995
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
22143
|
-
|
|
22144
|
-
const mdPath =
|
|
22145
|
-
const htmlPath =
|
|
22146
|
-
|
|
22147
|
-
|
|
22996
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
22997
|
+
const mdPath = path14.join(outputDir, `${baseName}${suffix}.md`);
|
|
22998
|
+
const htmlPath = path14.join(outputDir, `${baseName}${suffix}.html`);
|
|
22999
|
+
fs13.writeFileSync(mdPath, markdown, "utf8");
|
|
23000
|
+
fs13.writeFileSync(htmlPath, html, "utf8");
|
|
22148
23001
|
return [mdPath, htmlPath];
|
|
22149
23002
|
}
|
|
22150
23003
|
function evaluateReviewGate(review, args) {
|
|
@@ -22190,9 +23043,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
22190
23043
|
function printCompareResult(result, args, startMs) {
|
|
22191
23044
|
const durationMs = Date.now() - startMs;
|
|
22192
23045
|
if (result.prSummary && args.prSummaryFile) {
|
|
22193
|
-
const outputPath =
|
|
22194
|
-
|
|
22195
|
-
|
|
23046
|
+
const outputPath = path14.resolve(args.prSummaryFile);
|
|
23047
|
+
fs13.mkdirSync(path14.dirname(outputPath), { recursive: true });
|
|
23048
|
+
fs13.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
22196
23049
|
}
|
|
22197
23050
|
if (args.jsonSummary) {
|
|
22198
23051
|
console.log(
|
|
@@ -22283,7 +23136,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22283
23136
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
22284
23137
|
process.exit(EXIT_USAGE);
|
|
22285
23138
|
}
|
|
22286
|
-
if (!
|
|
23139
|
+
if (!fs13.existsSync(inputFile)) {
|
|
22287
23140
|
console.error(`Error: file not found: ${inputFile}`);
|
|
22288
23141
|
process.exit(EXIT_USAGE);
|
|
22289
23142
|
}
|
|
@@ -22311,7 +23164,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22311
23164
|
console.error("Error: --title is required when creating a new page");
|
|
22312
23165
|
process.exit(EXIT_USAGE);
|
|
22313
23166
|
}
|
|
22314
|
-
const adf =
|
|
23167
|
+
const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
|
|
22315
23168
|
if (dryRun) {
|
|
22316
23169
|
console.log(
|
|
22317
23170
|
JSON.stringify(
|
|
@@ -22390,7 +23243,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22390
23243
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
22391
23244
|
process.exit(EXIT_USAGE);
|
|
22392
23245
|
}
|
|
22393
|
-
if (!
|
|
23246
|
+
if (!fs13.existsSync(inputFile)) {
|
|
22394
23247
|
console.error(`Error: file not found: ${inputFile}`);
|
|
22395
23248
|
process.exit(EXIT_USAGE);
|
|
22396
23249
|
}
|
|
@@ -22417,7 +23270,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22417
23270
|
process.exit(EXIT_USAGE);
|
|
22418
23271
|
}
|
|
22419
23272
|
const mode = modeRaw;
|
|
22420
|
-
const adf =
|
|
23273
|
+
const adf = fs13.readFileSync(path14.resolve(inputFile), "utf8");
|
|
22421
23274
|
if (dryRun) {
|
|
22422
23275
|
console.log(
|
|
22423
23276
|
JSON.stringify(
|
|
@@ -22458,6 +23311,142 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
22458
23311
|
process.exit(EXIT_GENERATION);
|
|
22459
23312
|
}
|
|
22460
23313
|
}
|
|
23314
|
+
function runNew(rawArgs) {
|
|
23315
|
+
const { values, positionals } = parseArgs({
|
|
23316
|
+
args: rawArgs,
|
|
23317
|
+
options: { dir: { type: "string" }, force: { type: "boolean", default: false } },
|
|
23318
|
+
allowPositionals: true,
|
|
23319
|
+
strict: true
|
|
23320
|
+
});
|
|
23321
|
+
const template = positionals[0];
|
|
23322
|
+
const name = positionals.slice(1).join(" ");
|
|
23323
|
+
if (!template) {
|
|
23324
|
+
console.error(`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--force]`);
|
|
23325
|
+
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
23326
|
+
return EXIT_USAGE;
|
|
23327
|
+
}
|
|
23328
|
+
try {
|
|
23329
|
+
const result = scaffoldDoc({
|
|
23330
|
+
template,
|
|
23331
|
+
name,
|
|
23332
|
+
baseDir: values.dir,
|
|
23333
|
+
force: values.force
|
|
23334
|
+
});
|
|
23335
|
+
console.log(`Created ${result.template}: ${result.path}`);
|
|
23336
|
+
console.log(` Title: ${result.title}`);
|
|
23337
|
+
console.log("");
|
|
23338
|
+
console.log("Next: fill in the content and link verifying stories in `verifiedBy`.");
|
|
23339
|
+
return EXIT_SUCCESS;
|
|
23340
|
+
} catch (err) {
|
|
23341
|
+
console.error(`Error: ${err.message}`);
|
|
23342
|
+
return EXIT_USAGE;
|
|
23343
|
+
}
|
|
23344
|
+
}
|
|
23345
|
+
async function runCheckLinks(rawArgs) {
|
|
23346
|
+
const { values, positionals } = parseArgs({
|
|
23347
|
+
args: rawArgs,
|
|
23348
|
+
options: {
|
|
23349
|
+
external: { type: "boolean", default: false },
|
|
23350
|
+
json: { type: "boolean", default: false }
|
|
23351
|
+
},
|
|
23352
|
+
allowPositionals: true,
|
|
23353
|
+
strict: true
|
|
23354
|
+
});
|
|
23355
|
+
try {
|
|
23356
|
+
const report = await checkLinks({
|
|
23357
|
+
target: positionals[0] ?? ".",
|
|
23358
|
+
checkExternal: values.external
|
|
23359
|
+
});
|
|
23360
|
+
console.log(values.json ? JSON.stringify(report, null, 2) : formatLinkReport(report));
|
|
23361
|
+
return report.brokenCount > 0 ? EXIT_GENERATION : EXIT_SUCCESS;
|
|
23362
|
+
} catch (err) {
|
|
23363
|
+
console.error(`Error: ${err.message}`);
|
|
23364
|
+
return EXIT_USAGE;
|
|
23365
|
+
}
|
|
23366
|
+
}
|
|
23367
|
+
async function runImportOpenApi(rawArgs) {
|
|
23368
|
+
const { values, positionals } = parseArgs({
|
|
23369
|
+
args: rawArgs,
|
|
23370
|
+
options: {
|
|
23371
|
+
"output-dir": { type: "string" },
|
|
23372
|
+
run: { type: "string" },
|
|
23373
|
+
force: { type: "boolean", default: false }
|
|
23374
|
+
},
|
|
23375
|
+
allowPositionals: true,
|
|
23376
|
+
strict: true
|
|
23377
|
+
});
|
|
23378
|
+
const spec = positionals[0];
|
|
23379
|
+
if (!spec) {
|
|
23380
|
+
console.error(`Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`);
|
|
23381
|
+
return EXIT_USAGE;
|
|
23382
|
+
}
|
|
23383
|
+
try {
|
|
23384
|
+
const result = await importOpenApi({
|
|
23385
|
+
specPath: spec,
|
|
23386
|
+
outputDir: values["output-dir"],
|
|
23387
|
+
runFile: values.run,
|
|
23388
|
+
force: values.force
|
|
23389
|
+
});
|
|
23390
|
+
console.log(`Generated ${result.pageCount} API page(s) at ${result.outputDir}`);
|
|
23391
|
+
console.log(` Covered endpoints: ${result.coveredCount} / ${result.endpointCount}`);
|
|
23392
|
+
if (result.uncoveredCount > 0) {
|
|
23393
|
+
console.log(` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`);
|
|
23394
|
+
}
|
|
23395
|
+
return EXIT_SUCCESS;
|
|
23396
|
+
} catch (err) {
|
|
23397
|
+
console.error(`Error: ${err.message}`);
|
|
23398
|
+
return EXIT_USAGE;
|
|
23399
|
+
}
|
|
23400
|
+
}
|
|
23401
|
+
async function runBuildDocs(rawArgs) {
|
|
23402
|
+
const { values, positionals } = parseArgs({
|
|
23403
|
+
args: rawArgs,
|
|
23404
|
+
options: {
|
|
23405
|
+
"site-dir": { type: "string" },
|
|
23406
|
+
openapi: { type: "string" },
|
|
23407
|
+
"no-synthesize-stories": { type: "boolean", default: false }
|
|
23408
|
+
},
|
|
23409
|
+
allowPositionals: true,
|
|
23410
|
+
strict: true
|
|
23411
|
+
});
|
|
23412
|
+
const rawRunPath = positionals[0];
|
|
23413
|
+
if (!rawRunPath) {
|
|
23414
|
+
console.error(
|
|
23415
|
+
`Usage: executable-stories build-docs <raw-run.json> [--site-dir <dir>] [--openapi <spec>]`
|
|
23416
|
+
);
|
|
23417
|
+
return EXIT_USAGE;
|
|
23418
|
+
}
|
|
23419
|
+
try {
|
|
23420
|
+
const result = await buildDocs({
|
|
23421
|
+
rawRunPath,
|
|
23422
|
+
siteDir: values["site-dir"] ?? ".",
|
|
23423
|
+
openapiPath: values.openapi,
|
|
23424
|
+
synthesizeStories: !values["no-synthesize-stories"]
|
|
23425
|
+
});
|
|
23426
|
+
console.log(`\u2713 Living docs generated in ${result.siteDir}`);
|
|
23427
|
+
console.log(` \u2022 Explorer data \u2192 public/stories/story-report.json`);
|
|
23428
|
+
console.log(` \u2022 Story pages \u2192 src/content/docs/stories`);
|
|
23429
|
+
if (result.bundledAssets > 0) {
|
|
23430
|
+
console.log(` \u2022 Bundled assets \u2192 public/stories/assets (${result.bundledAssets})`);
|
|
23431
|
+
}
|
|
23432
|
+
if (result.apiPages > 0) {
|
|
23433
|
+
console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
|
|
23434
|
+
}
|
|
23435
|
+
const rel = path14.relative(process.cwd(), result.siteDir) || ".";
|
|
23436
|
+
console.log(`
|
|
23437
|
+
Preview: cd ${rel} && npm run dev`);
|
|
23438
|
+
return EXIT_SUCCESS;
|
|
23439
|
+
} catch (err) {
|
|
23440
|
+
if (err instanceof BuildDocsError) {
|
|
23441
|
+
console.error(err.message);
|
|
23442
|
+
if (err.kind === "schema") return EXIT_SCHEMA_VALIDATION;
|
|
23443
|
+
if (err.kind === "generation") return EXIT_GENERATION;
|
|
23444
|
+
return EXIT_USAGE;
|
|
23445
|
+
}
|
|
23446
|
+
console.error(`Error: ${err.message}`);
|
|
23447
|
+
return EXIT_USAGE;
|
|
23448
|
+
}
|
|
23449
|
+
}
|
|
22461
23450
|
main().catch((err) => {
|
|
22462
23451
|
console.error(err);
|
|
22463
23452
|
process.exit(EXIT_USAGE);
|