executable-stories-formatters 0.7.8 → 0.7.9

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
@@ -596,8 +596,8 @@ function generateRunId(startedAtMs, projectRoot) {
596
596
  const input = `${startedAtMs}::${projectRoot}`;
597
597
  return createHash("sha1").update(input).digest("hex").slice(0, 16);
598
598
  }
599
- function slugify(text) {
600
- return text.toLowerCase().replace(/[/\\]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
599
+ function slugify(text2) {
600
+ return text2.toLowerCase().replace(/[/\\]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
601
601
  }
602
602
  function generateFeatureId(uri) {
603
603
  const pathWithoutExt = uri.replace(/\.[^.]+$/, "");
@@ -13696,7 +13696,7 @@ function renderDocEntry(entry, deps) {
13696
13696
  // src/formatters/html/renderers/steps.ts
13697
13697
  var CONTINUATION_KEYWORDS = ["And", "But", "*"];
13698
13698
  function renderStep(step, stepResult, index, deps) {
13699
- const statusIcon = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
13699
+ const statusIcon2 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
13700
13700
  const statusClass = stepResult ? `status-${stepResult.status}` : "";
13701
13701
  const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
13702
13702
  const keywordTrimmed = step.keyword.trim();
@@ -13705,7 +13705,7 @@ function renderStep(step, stepResult, index, deps) {
13705
13705
  const stepDocs = deps.renderDocs(step.docs, "step-docs");
13706
13706
  const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
13707
13707
  return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
13708
- <span class="step-status ${statusClass}">${statusIcon}</span>
13708
+ <span class="step-status ${statusClass}">${statusIcon2}</span>
13709
13709
  <span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
13710
13710
  <span class="step-text">${textHtml}</span>
13711
13711
  <span class="step-duration">${duration}</span>
@@ -13721,10 +13721,10 @@ function renderSteps(args, deps) {
13721
13721
 
13722
13722
  // src/formatters/html/renderers/step-params.ts
13723
13723
  var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
13724
- function highlightStepParams(text, deps) {
13725
- const matches = Array.from(text.matchAll(STEP_PARAM_PATTERN));
13724
+ function highlightStepParams(text2, deps) {
13725
+ const matches = Array.from(text2.matchAll(STEP_PARAM_PATTERN));
13726
13726
  if (matches.length === 0) {
13727
- return deps.escapeHtml(text);
13727
+ return deps.escapeHtml(text2);
13728
13728
  }
13729
13729
  let result = "";
13730
13730
  let lastIndex = 0;
@@ -13732,13 +13732,13 @@ function highlightStepParams(text, deps) {
13732
13732
  const matchStart = match.index;
13733
13733
  const matchEnd = matchStart + match[0].length;
13734
13734
  if (matchStart > lastIndex) {
13735
- result += deps.escapeHtml(text.slice(lastIndex, matchStart));
13735
+ result += deps.escapeHtml(text2.slice(lastIndex, matchStart));
13736
13736
  }
13737
13737
  result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
13738
13738
  lastIndex = matchEnd;
13739
13739
  }
13740
- if (lastIndex < text.length) {
13741
- result += deps.escapeHtml(text.slice(lastIndex));
13740
+ if (lastIndex < text2.length) {
13741
+ result += deps.escapeHtml(text2.slice(lastIndex));
13742
13742
  }
13743
13743
  return result;
13744
13744
  }
@@ -13756,7 +13756,7 @@ function renderTicket(ticket, template, escapeHtml3) {
13756
13756
  }
13757
13757
  function renderScenario(args, deps) {
13758
13758
  const { tc } = args;
13759
- const statusIcon = deps.getStatusIcon(tc.status);
13759
+ const statusIcon2 = deps.getStatusIcon(tc.status);
13760
13760
  const statusClass = `status-${tc.status}`;
13761
13761
  const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
13762
13762
  const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
@@ -13825,7 +13825,7 @@ function renderScenario(args, deps) {
13825
13825
  <div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
13826
13826
  <div class="scenario-info">
13827
13827
  <div class="scenario-title">
13828
- <span class="status-icon ${statusClass}">${statusIcon}</span>
13828
+ <span class="status-icon ${statusClass}">${statusIcon2}</span>
13829
13829
  <span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
13830
13830
  </div>
13831
13831
  <div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
@@ -13964,11 +13964,11 @@ function buildTooltip(span, escapeHtml3) {
13964
13964
  parts.push(`${key}=${formatted}`);
13965
13965
  }
13966
13966
  }
13967
- let text = parts.join("\n");
13968
- if (text.length > TOOLTIP_MAX_LENGTH) {
13969
- text = text.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
13967
+ let text2 = parts.join("\n");
13968
+ if (text2.length > TOOLTIP_MAX_LENGTH) {
13969
+ text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
13970
13970
  }
13971
- return escapeHtml3(text);
13971
+ return escapeHtml3(text2);
13972
13972
  }
13973
13973
  function renderTraceView(args, deps) {
13974
13974
  if (!args.spans || args.spans.length === 0) return "";
@@ -14191,11 +14191,11 @@ function renderToc(args, deps) {
14191
14191
  const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
14192
14192
  const featureSlug = `feature-${slugify(file)}`;
14193
14193
  const scenarios = testCases.map((tc) => {
14194
- const statusIcon = deps.getStatusIcon(tc.status);
14194
+ const statusIcon2 = deps.getStatusIcon(tc.status);
14195
14195
  const statusClass = `status-${tc.status}`;
14196
14196
  const failedClass = tc.status === "failed" ? " toc-failed" : "";
14197
14197
  return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
14198
- <span class="toc-status ${statusClass}">${statusIcon}</span>
14198
+ <span class="toc-status ${statusClass}">${statusIcon2}</span>
14199
14199
  ${deps.escapeHtml(tc.story.scenario)}
14200
14200
  </a>`;
14201
14201
  }).join("\n");
@@ -14253,7 +14253,7 @@ function createHtmlFormatter(options = {}) {
14253
14253
  escapeHtml,
14254
14254
  getStatusIcon,
14255
14255
  renderDocs,
14256
- highlightStepParams: (text) => highlightStepParams(text, { escapeHtml })
14256
+ highlightStepParams: (text2) => highlightStepParams(text2, { escapeHtml })
14257
14257
  };
14258
14258
  const scenarioDeps = {
14259
14259
  escapeHtml,
@@ -15188,7 +15188,7 @@ function deterministicId(kind, salt, ...parts) {
15188
15188
 
15189
15189
  // src/formatters/cucumber-messages/build-gherkin-document.ts
15190
15190
  function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
15191
- const { lineMap, featureName, featureTags, text } = synthesized;
15191
+ const { lineMap, featureName, featureTags, text: text2 } = synthesized;
15192
15192
  const featureTagNodes = featureTags.map((tag, i) => ({
15193
15193
  location: {
15194
15194
  line: lineMap.featureTagLine ?? 1,
@@ -15257,7 +15257,7 @@ function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
15257
15257
  sourceEnvelope: {
15258
15258
  source: {
15259
15259
  uri,
15260
- data: text,
15260
+ data: text2,
15261
15261
  mediaType: "text/x.cucumber.gherkin+plain"
15262
15262
  }
15263
15263
  },
@@ -15268,8 +15268,8 @@ function buildStepArguments(step, stepLine) {
15268
15268
  if (!step.docs || step.docs.length === 0) return {};
15269
15269
  const tableDocs = step.docs.filter((d) => d.kind === "table");
15270
15270
  if (tableDocs.length > 0) {
15271
- const table = tableDocs[0];
15272
- return { dataTable: buildDataTable(table, stepLine + 1) };
15271
+ const table2 = tableDocs[0];
15272
+ return { dataTable: buildDataTable(table2, stepLine + 1) };
15273
15273
  }
15274
15274
  for (const doc of step.docs) {
15275
15275
  const ds = docEntryToDocString(doc, stepLine + 1);
@@ -15340,21 +15340,21 @@ function docEntryToDocString(doc, line) {
15340
15340
  return void 0;
15341
15341
  }
15342
15342
  }
15343
- function buildDataTable(table, line) {
15343
+ function buildDataTable(table2, line) {
15344
15344
  const rows = [];
15345
15345
  rows.push({
15346
15346
  location: { line },
15347
- cells: table.columns.map((col) => ({
15347
+ cells: table2.columns.map((col) => ({
15348
15348
  location: { line },
15349
15349
  value: col
15350
15350
  })),
15351
15351
  id: ""
15352
15352
  });
15353
- for (let r = 0; r < table.rows.length; r++) {
15353
+ for (let r = 0; r < table2.rows.length; r++) {
15354
15354
  const rowLine = line + 1 + r;
15355
15355
  rows.push({
15356
15356
  location: { line: rowLine },
15357
- cells: table.rows[r].map((cell) => ({
15357
+ cells: table2.rows[r].map((cell) => ({
15358
15358
  location: { line: rowLine },
15359
15359
  value: cell
15360
15360
  })),
@@ -15445,12 +15445,12 @@ function docEntryToPickleDocString(doc) {
15445
15445
  return void 0;
15446
15446
  }
15447
15447
  }
15448
- function buildPickleTable(table) {
15448
+ function buildPickleTable(table2) {
15449
15449
  const rows = [];
15450
15450
  rows.push({
15451
- cells: table.columns.map((col) => ({ value: col }))
15451
+ cells: table2.columns.map((col) => ({ value: col }))
15452
15452
  });
15453
- for (const row of table.rows) {
15453
+ for (const row of table2.rows) {
15454
15454
  rows.push({
15455
15455
  cells: row.map((cell) => ({ value: cell }))
15456
15456
  });
@@ -16896,6 +16896,414 @@ ${body}`;
16896
16896
  }
16897
16897
  };
16898
16898
 
16899
+ // src/formatters/confluence.ts
16900
+ var ConfluenceFormatter = class {
16901
+ options;
16902
+ constructor(options = {}) {
16903
+ this.options = {
16904
+ title: options.title ?? "User Stories",
16905
+ includeStatusIcons: options.includeStatusIcons ?? true,
16906
+ includeMetadata: options.includeMetadata ?? true,
16907
+ includeSummaryTable: options.includeSummaryTable ?? true,
16908
+ includeErrors: options.includeErrors ?? true,
16909
+ scenarioHeadingLevel: options.scenarioHeadingLevel ?? 3,
16910
+ groupBy: options.groupBy ?? "file",
16911
+ sortScenarios: options.sortScenarios ?? "source",
16912
+ pretty: options.pretty ?? true,
16913
+ permalinkBaseUrl: options.permalinkBaseUrl,
16914
+ ticketUrlTemplate: options.ticketUrlTemplate
16915
+ };
16916
+ }
16917
+ /** Build the ADF document tree. Returns the JS object (not stringified). */
16918
+ formatToAdf(run) {
16919
+ const content = [];
16920
+ content.push(heading(1, [text(this.options.title)]));
16921
+ if (this.options.includeMetadata) {
16922
+ const metaTable = this.renderMetadataTable(run);
16923
+ if (metaTable) content.push(metaTable);
16924
+ }
16925
+ if (this.options.includeSummaryTable) {
16926
+ content.push(this.renderSummaryTable(run));
16927
+ }
16928
+ switch (this.options.groupBy) {
16929
+ case "none":
16930
+ this.renderFlat(content, run.testCases);
16931
+ break;
16932
+ case "suite":
16933
+ this.renderBySuite(content, run.testCases);
16934
+ break;
16935
+ case "file":
16936
+ default:
16937
+ this.renderByFile(content, run.testCases);
16938
+ break;
16939
+ }
16940
+ return { version: 1, type: "doc", content };
16941
+ }
16942
+ /** Format a test run as an ADF JSON string. */
16943
+ format(run) {
16944
+ const adf = this.formatToAdf(run);
16945
+ return this.options.pretty ? JSON.stringify(adf, null, 2) : JSON.stringify(adf);
16946
+ }
16947
+ // --------------------------------------------------------------------------
16948
+ // Metadata / summary tables
16949
+ // --------------------------------------------------------------------------
16950
+ renderMetadataTable(run) {
16951
+ const rows = [];
16952
+ rows.push(["Date", new Date(run.startedAtMs).toISOString()]);
16953
+ if (run.packageVersion) rows.push(["Version", run.packageVersion]);
16954
+ if (run.gitSha) {
16955
+ const shortSha = run.gitSha.length > 7 ? run.gitSha.slice(0, 7) : run.gitSha;
16956
+ rows.push(["Git SHA", shortSha]);
16957
+ }
16958
+ if (rows.length === 0) return null;
16959
+ return table([
16960
+ tableRow([tableHeader("Key"), tableHeader("Value")]),
16961
+ ...rows.map(([k, v]) => tableRow([tableCell(k), tableCell(v)]))
16962
+ ]);
16963
+ }
16964
+ renderSummaryTable(run) {
16965
+ const total = run.testCases.length;
16966
+ const steps = run.testCases.reduce(
16967
+ (acc, tc) => acc + tc.story.steps.length,
16968
+ 0
16969
+ );
16970
+ const passed = run.testCases.filter((tc) => tc.status === "passed").length;
16971
+ const failed = run.testCases.filter((tc) => tc.status === "failed").length;
16972
+ const skipped = run.testCases.filter((tc) => tc.status === "skipped").length;
16973
+ const pending = run.testCases.filter((tc) => tc.status === "pending").length;
16974
+ return table([
16975
+ tableRow([
16976
+ tableHeader("Scenarios"),
16977
+ tableHeader("Steps"),
16978
+ tableHeader("Passed"),
16979
+ tableHeader("Failed"),
16980
+ tableHeader("Skipped"),
16981
+ tableHeader("Pending"),
16982
+ tableHeader("Duration")
16983
+ ]),
16984
+ tableRow([
16985
+ tableCell(String(total)),
16986
+ tableCell(String(steps)),
16987
+ tableCell(String(passed)),
16988
+ tableCell(String(failed)),
16989
+ tableCell(String(skipped)),
16990
+ tableCell(String(pending)),
16991
+ tableCell(formatDuration2(run.durationMs))
16992
+ ])
16993
+ ]);
16994
+ }
16995
+ // --------------------------------------------------------------------------
16996
+ // Grouping
16997
+ // --------------------------------------------------------------------------
16998
+ renderByFile(content, testCases) {
16999
+ const byFile = groupBy7(testCases, (tc) => tc.sourceFile);
17000
+ for (const [file, fileCases] of byFile) {
17001
+ content.push(heading(2, [codeInline(file)]));
17002
+ this.renderSuiteGroups(content, fileCases, 3);
17003
+ }
17004
+ }
17005
+ renderBySuite(content, testCases) {
17006
+ this.renderSuiteGroups(content, testCases, 2);
17007
+ }
17008
+ renderFlat(content, testCases) {
17009
+ const sorted = this.sortCases(testCases);
17010
+ for (const tc of sorted) this.renderScenario(content, tc);
17011
+ }
17012
+ renderSuiteGroups(content, testCases, baseLevel) {
17013
+ const bySuite = groupBy7(testCases, (tc) => tc.titlePath.join(" - "));
17014
+ const entries = this.sortSuiteGroups([...bySuite.entries()]);
17015
+ for (const [suitePath, cases] of entries) {
17016
+ if (suitePath) {
17017
+ content.push(
17018
+ heading(clampHeadingLevel(baseLevel), [text(suitePath)])
17019
+ );
17020
+ }
17021
+ for (const tc of this.sortCases(cases)) {
17022
+ this.renderScenario(content, tc);
17023
+ }
17024
+ }
17025
+ }
17026
+ // --------------------------------------------------------------------------
17027
+ // Scenario
17028
+ // --------------------------------------------------------------------------
17029
+ renderScenario(content, tc) {
17030
+ const level = clampHeadingLevel(this.options.scenarioHeadingLevel);
17031
+ const headingNodes = [];
17032
+ if (this.options.includeStatusIcons) {
17033
+ headingNodes.push(text(`${statusIcon(tc.status)} `));
17034
+ }
17035
+ headingNodes.push(text(tc.story.scenario));
17036
+ content.push(heading(level, headingNodes));
17037
+ const metaChildren = [];
17038
+ if (tc.tags.length > 0) {
17039
+ metaChildren.push(text("Tags: ", strong()));
17040
+ tc.tags.forEach((t, i) => {
17041
+ if (i > 0) metaChildren.push(text(", "));
17042
+ metaChildren.push(codeInline(t));
17043
+ });
17044
+ }
17045
+ if (tc.story.tickets && tc.story.tickets.length > 0) {
17046
+ if (metaChildren.length > 0) metaChildren.push(text(" | "));
17047
+ metaChildren.push(text("Tickets: ", strong()));
17048
+ tc.story.tickets.forEach((ticket, i) => {
17049
+ if (i > 0) metaChildren.push(text(", "));
17050
+ const url = ticket.url ?? (this.options.ticketUrlTemplate ? this.options.ticketUrlTemplate.replace("{ticket}", ticket.id) : void 0);
17051
+ metaChildren.push(
17052
+ url ? link(ticket.id, url) : codeInline(ticket.id)
17053
+ );
17054
+ });
17055
+ }
17056
+ if (this.options.permalinkBaseUrl && tc.sourceFile !== "unknown" && tc.sourceFile) {
17057
+ if (metaChildren.length > 0) metaChildren.push(text(" | "));
17058
+ metaChildren.push(text("Source: ", strong()));
17059
+ const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
17060
+ const url = `${base}/${tc.sourceFile}${tc.sourceLine > 0 ? `#L${tc.sourceLine}` : ""}`;
17061
+ metaChildren.push(link(tc.sourceFile, url));
17062
+ }
17063
+ if (metaChildren.length > 0) {
17064
+ content.push(paragraph(metaChildren));
17065
+ }
17066
+ if (tc.story.docs && tc.story.docs.length > 0) {
17067
+ for (const doc of tc.story.docs) this.renderDocEntry(content, doc);
17068
+ }
17069
+ if (tc.story.steps.length > 0) {
17070
+ content.push(this.renderStepsList(tc.story.steps));
17071
+ for (const step of tc.story.steps) {
17072
+ if (step.docs && step.docs.length > 0) {
17073
+ for (const doc of step.docs) this.renderDocEntry(content, doc);
17074
+ }
17075
+ }
17076
+ }
17077
+ if (tc.status === "failed" && tc.errorMessage && this.options.includeErrors) {
17078
+ const errorContent = (tc.errorMessage ?? "") + (tc.errorStack ? `
17079
+
17080
+ ${tc.errorStack}` : "");
17081
+ content.push(
17082
+ panel("warning", [paragraph([text("Failure", strong())])])
17083
+ );
17084
+ content.push(codeBlock(errorContent, "text"));
17085
+ }
17086
+ }
17087
+ renderStepsList(steps) {
17088
+ return {
17089
+ type: "bulletList",
17090
+ content: steps.map((step) => {
17091
+ const children = [text(`${step.keyword} `, strong()), text(step.text)];
17092
+ if (step.mode && step.mode !== "normal") {
17093
+ children.push(text(` (${step.mode})`, em()));
17094
+ }
17095
+ return {
17096
+ type: "listItem",
17097
+ content: [paragraph(children)]
17098
+ };
17099
+ })
17100
+ };
17101
+ }
17102
+ // --------------------------------------------------------------------------
17103
+ // Doc entries
17104
+ // --------------------------------------------------------------------------
17105
+ renderDocEntry(content, entry) {
17106
+ switch (entry.kind) {
17107
+ case "note":
17108
+ content.push(panel("info", [paragraph([text(entry.text)])]));
17109
+ break;
17110
+ case "tag": {
17111
+ const kids = [];
17112
+ entry.names.forEach((name, i) => {
17113
+ if (i > 0) kids.push(text(" "));
17114
+ kids.push(codeInline(name));
17115
+ });
17116
+ if (kids.length > 0) content.push(paragraph(kids));
17117
+ break;
17118
+ }
17119
+ case "kv": {
17120
+ const val = typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value);
17121
+ content.push(
17122
+ paragraph([text(`${entry.label}: `, strong()), codeInline(val)])
17123
+ );
17124
+ break;
17125
+ }
17126
+ case "code":
17127
+ if (entry.label) {
17128
+ content.push(paragraph([text(entry.label, strong())]));
17129
+ }
17130
+ content.push(codeBlock(entry.content ?? "", entry.lang));
17131
+ break;
17132
+ case "table":
17133
+ if (entry.label) {
17134
+ content.push(paragraph([text(entry.label, strong())]));
17135
+ }
17136
+ content.push(
17137
+ table([
17138
+ tableRow(entry.columns.map((c) => tableHeader(c))),
17139
+ ...entry.rows.map(
17140
+ (row) => tableRow(row.map((cell) => tableCell(cell)))
17141
+ )
17142
+ ])
17143
+ );
17144
+ break;
17145
+ case "link":
17146
+ content.push(paragraph([link(entry.label, entry.url)]));
17147
+ break;
17148
+ case "section":
17149
+ if (entry.title) {
17150
+ content.push(paragraph([text(entry.title, strong())]));
17151
+ }
17152
+ if (entry.markdown) {
17153
+ for (const para of entry.markdown.split(/\n{2,}/)) {
17154
+ const trimmed = para.trim();
17155
+ if (trimmed) content.push(paragraph([text(trimmed)]));
17156
+ }
17157
+ }
17158
+ break;
17159
+ case "mermaid":
17160
+ if (entry.title) {
17161
+ content.push(paragraph([text(entry.title, strong())]));
17162
+ }
17163
+ content.push(codeBlock(entry.code ?? "", "mermaid"));
17164
+ break;
17165
+ case "screenshot":
17166
+ content.push(
17167
+ paragraph([
17168
+ text(entry.alt ?? "Screenshot", strong()),
17169
+ text(": "),
17170
+ link(entry.path, entry.path)
17171
+ ])
17172
+ );
17173
+ break;
17174
+ case "custom":
17175
+ content.push(paragraph([text(`[${entry.type}]`, strong())]));
17176
+ content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
17177
+ break;
17178
+ }
17179
+ if (entry.children && entry.children.length > 0) {
17180
+ for (const child of entry.children) {
17181
+ this.renderDocEntry(content, child);
17182
+ }
17183
+ }
17184
+ }
17185
+ // --------------------------------------------------------------------------
17186
+ // Sorting
17187
+ // --------------------------------------------------------------------------
17188
+ sortCases(cases) {
17189
+ if (this.options.sortScenarios === "alpha") {
17190
+ return [...cases].sort(
17191
+ (a, b) => a.story.scenario.localeCompare(b.story.scenario)
17192
+ );
17193
+ }
17194
+ if (this.options.sortScenarios === "source") {
17195
+ return [...cases].sort(
17196
+ (a, b) => (a.story.sourceOrder ?? 0) - (b.story.sourceOrder ?? 0)
17197
+ );
17198
+ }
17199
+ return cases;
17200
+ }
17201
+ sortSuiteGroups(entries) {
17202
+ if (this.options.sortScenarios === "alpha") {
17203
+ return entries.sort(([a], [b]) => a.localeCompare(b));
17204
+ }
17205
+ if (this.options.sortScenarios === "source") {
17206
+ return entries.sort(([, a], [, b]) => {
17207
+ const minA = Math.min(...a.map((s) => s.story.sourceOrder ?? Infinity));
17208
+ const minB = Math.min(...b.map((s) => s.story.sourceOrder ?? Infinity));
17209
+ return minA - minB;
17210
+ });
17211
+ }
17212
+ return entries;
17213
+ }
17214
+ };
17215
+ function text(value, mark) {
17216
+ const node = { type: "text", text: value };
17217
+ if (mark) {
17218
+ node.marks = Array.isArray(mark) ? mark : [mark];
17219
+ }
17220
+ return node;
17221
+ }
17222
+ function strong() {
17223
+ return { type: "strong" };
17224
+ }
17225
+ function em() {
17226
+ return { type: "em" };
17227
+ }
17228
+ function codeMark() {
17229
+ return { type: "code" };
17230
+ }
17231
+ function codeInline(value) {
17232
+ return text(value, codeMark());
17233
+ }
17234
+ function link(label, href) {
17235
+ return text(label, { type: "link", attrs: { href } });
17236
+ }
17237
+ function paragraph(content) {
17238
+ return { type: "paragraph", content };
17239
+ }
17240
+ function heading(level, content) {
17241
+ return {
17242
+ type: "heading",
17243
+ attrs: { level: clampHeadingLevel(level) },
17244
+ content
17245
+ };
17246
+ }
17247
+ function codeBlock(content, lang) {
17248
+ return {
17249
+ type: "codeBlock",
17250
+ attrs: lang ? { language: lang } : {},
17251
+ content: content ? [{ type: "text", text: content }] : []
17252
+ };
17253
+ }
17254
+ function panel(panelType, content) {
17255
+ return { type: "panel", attrs: { panelType }, content };
17256
+ }
17257
+ function table(rows) {
17258
+ return {
17259
+ type: "table",
17260
+ attrs: { isNumberColumnEnabled: false, layout: "default" },
17261
+ content: rows
17262
+ };
17263
+ }
17264
+ function tableRow(cells) {
17265
+ return { type: "tableRow", content: cells };
17266
+ }
17267
+ function tableHeader(value) {
17268
+ return { type: "tableHeader", content: [paragraph([text(value)])] };
17269
+ }
17270
+ function tableCell(value) {
17271
+ return { type: "tableCell", content: [paragraph([text(value)])] };
17272
+ }
17273
+ function clampHeadingLevel(level) {
17274
+ if (level < 1) return 1;
17275
+ if (level > 6) return 6;
17276
+ return level;
17277
+ }
17278
+ function statusIcon(status) {
17279
+ switch (status) {
17280
+ case "passed":
17281
+ return "\u2705";
17282
+ case "failed":
17283
+ return "\u274C";
17284
+ case "skipped":
17285
+ return "\u23E9";
17286
+ case "pending":
17287
+ return "\u{1F4DD}";
17288
+ default:
17289
+ return "\u26A0\uFE0F";
17290
+ }
17291
+ }
17292
+ function formatDuration2(ms) {
17293
+ if (ms < 1e3) return `${ms}ms`;
17294
+ return `${(ms / 1e3).toFixed(2)}s`;
17295
+ }
17296
+ function groupBy7(items, keyFn) {
17297
+ const map = /* @__PURE__ */ new Map();
17298
+ for (const item of items) {
17299
+ const key = keyFn(item);
17300
+ const existing = map.get(key);
17301
+ if (existing) existing.push(item);
17302
+ else map.set(key, [item]);
17303
+ }
17304
+ return map;
17305
+ }
17306
+
16899
17307
  // src/formatters/astro-assets.ts
16900
17308
  import * as fs4 from "fs";
16901
17309
  import * as path4 from "path";
@@ -17020,6 +17428,235 @@ function copyMarkdownAssets(options) {
17020
17428
  };
17021
17429
  }
17022
17430
 
17431
+ // src/publishers/confluence.ts
17432
+ function parseAdf(adf) {
17433
+ let parsed;
17434
+ try {
17435
+ parsed = JSON.parse(adf);
17436
+ } catch (err) {
17437
+ throw new Error(
17438
+ `ADF payload is not valid JSON: ${err.message}`
17439
+ );
17440
+ }
17441
+ if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
17442
+ throw new Error(
17443
+ `ADF payload must be an object with { version, type: "doc", content: [...] }`
17444
+ );
17445
+ }
17446
+ return parsed;
17447
+ }
17448
+ function basicAuthHeader(auth) {
17449
+ const raw = `${auth.email}:${auth.token}`;
17450
+ const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
17451
+ return `Basic ${encoded}`;
17452
+ }
17453
+ async function parseErrorBody(response) {
17454
+ try {
17455
+ const body = await response.text();
17456
+ return body ? body.slice(0, 800) : "";
17457
+ } catch {
17458
+ return "";
17459
+ }
17460
+ }
17461
+ async function publishConfluencePage(args, deps) {
17462
+ parseAdf(args.adf);
17463
+ if (!args.pageId && !args.spaceId) {
17464
+ throw new Error(
17465
+ "publishConfluencePage requires either pageId (update) or spaceId (create)"
17466
+ );
17467
+ }
17468
+ if (!args.pageId && !args.title) {
17469
+ throw new Error("Creating a new page requires a title");
17470
+ }
17471
+ const base = args.baseUrl.replace(/\/$/, "");
17472
+ const fetchFn = deps.fetch ?? globalThis.fetch;
17473
+ if (!fetchFn) {
17474
+ throw new Error("No fetch implementation available (Node >= 22 expected)");
17475
+ }
17476
+ const headers = {
17477
+ Authorization: basicAuthHeader(deps.auth),
17478
+ Accept: "application/json",
17479
+ "Content-Type": "application/json"
17480
+ };
17481
+ if (args.pageId) {
17482
+ return updatePage(args, base, headers, fetchFn);
17483
+ }
17484
+ return createPage(args, base, headers, fetchFn);
17485
+ }
17486
+ async function updatePage(args, base, headers, fetchFn) {
17487
+ const getUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
17488
+ const getResp = await fetchFn(getUrl, { method: "GET", headers });
17489
+ if (!getResp.ok) {
17490
+ const body = await parseErrorBody(getResp);
17491
+ throw new Error(
17492
+ `GET ${getUrl} failed with ${getResp.status} ${getResp.statusText}${body ? `: ${body}` : ""}`
17493
+ );
17494
+ }
17495
+ const current = await getResp.json();
17496
+ const nextVersion = current.version.number + 1;
17497
+ const title = args.title ?? current.title;
17498
+ const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
17499
+ const putResp = await fetchFn(putUrl, {
17500
+ method: "PUT",
17501
+ headers,
17502
+ body: JSON.stringify({
17503
+ id: args.pageId,
17504
+ status: "current",
17505
+ title,
17506
+ body: {
17507
+ representation: "atlas_doc_format",
17508
+ value: args.adf
17509
+ },
17510
+ version: { number: nextVersion }
17511
+ })
17512
+ });
17513
+ if (!putResp.ok) {
17514
+ const body = await parseErrorBody(putResp);
17515
+ throw new Error(
17516
+ `PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
17517
+ );
17518
+ }
17519
+ const updated = await putResp.json();
17520
+ return {
17521
+ id: updated.id,
17522
+ title: updated.title,
17523
+ version: updated.version.number,
17524
+ url: buildPageUrl(base, updated._links?.webui, updated.id),
17525
+ action: "updated"
17526
+ };
17527
+ }
17528
+ async function createPage(args, base, headers, fetchFn) {
17529
+ const body = {
17530
+ spaceId: args.spaceId,
17531
+ status: "current",
17532
+ title: args.title,
17533
+ body: {
17534
+ representation: "atlas_doc_format",
17535
+ value: args.adf
17536
+ }
17537
+ };
17538
+ if (args.parentId) body.parentId = args.parentId;
17539
+ const postUrl = `${base}/api/v2/pages`;
17540
+ const resp = await fetchFn(postUrl, {
17541
+ method: "POST",
17542
+ headers,
17543
+ body: JSON.stringify(body)
17544
+ });
17545
+ if (!resp.ok) {
17546
+ const errBody = await parseErrorBody(resp);
17547
+ throw new Error(
17548
+ `POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
17549
+ );
17550
+ }
17551
+ const created = await resp.json();
17552
+ return {
17553
+ id: created.id,
17554
+ title: created.title,
17555
+ version: created.version.number,
17556
+ url: buildPageUrl(base, created._links?.webui, created.id),
17557
+ action: "created"
17558
+ };
17559
+ }
17560
+ function buildPageUrl(base, webui, id) {
17561
+ if (webui) {
17562
+ return webui.startsWith("http") ? webui : `${base}${webui}`;
17563
+ }
17564
+ return `${base}/pages/${id}`;
17565
+ }
17566
+
17567
+ // src/publishers/jira.ts
17568
+ function parseAdf2(adf) {
17569
+ let parsed;
17570
+ try {
17571
+ parsed = JSON.parse(adf);
17572
+ } catch (err) {
17573
+ throw new Error(
17574
+ `ADF payload is not valid JSON: ${err.message}`
17575
+ );
17576
+ }
17577
+ if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
17578
+ throw new Error(
17579
+ `ADF payload must be an object with { version, type: "doc", content: [...] }`
17580
+ );
17581
+ }
17582
+ return parsed;
17583
+ }
17584
+ function basicAuthHeader2(auth) {
17585
+ const raw = `${auth.email}:${auth.token}`;
17586
+ const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
17587
+ return `Basic ${encoded}`;
17588
+ }
17589
+ async function parseErrorBody2(resp) {
17590
+ try {
17591
+ const body = await resp.text();
17592
+ return body ? body.slice(0, 800) : "";
17593
+ } catch {
17594
+ return "";
17595
+ }
17596
+ }
17597
+ async function publishJiraIssue(args, deps) {
17598
+ const adfObject = parseAdf2(args.adf);
17599
+ if (!args.issueKey) {
17600
+ throw new Error("publishJiraIssue requires an issueKey, e.g. PROJ-123");
17601
+ }
17602
+ const base = args.baseUrl.replace(/\/$/, "");
17603
+ const fetchFn = deps.fetch ?? globalThis.fetch;
17604
+ if (!fetchFn) {
17605
+ throw new Error("No fetch implementation available (Node >= 22 expected)");
17606
+ }
17607
+ const headers = {
17608
+ Authorization: basicAuthHeader2(deps.auth),
17609
+ Accept: "application/json",
17610
+ "Content-Type": "application/json"
17611
+ };
17612
+ const mode = args.mode ?? "comment";
17613
+ if (mode === "description") {
17614
+ return updateDescription(args.issueKey, base, adfObject, headers, fetchFn);
17615
+ }
17616
+ return addComment(args.issueKey, base, adfObject, headers, fetchFn);
17617
+ }
17618
+ async function addComment(issueKey, base, adf, headers, fetchFn) {
17619
+ const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}/comment`;
17620
+ const resp = await fetchFn(url, {
17621
+ method: "POST",
17622
+ headers,
17623
+ body: JSON.stringify({ body: adf })
17624
+ });
17625
+ if (!resp.ok) {
17626
+ const body = await parseErrorBody2(resp);
17627
+ throw new Error(
17628
+ `POST ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
17629
+ );
17630
+ }
17631
+ const comment = await resp.json();
17632
+ const issueUrl = `${base}/browse/${encodeURIComponent(issueKey)}`;
17633
+ return {
17634
+ issueKey,
17635
+ action: "comment-added",
17636
+ url: `${issueUrl}?focusedCommentId=${encodeURIComponent(comment.id)}`,
17637
+ commentId: comment.id
17638
+ };
17639
+ }
17640
+ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
17641
+ const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}`;
17642
+ const resp = await fetchFn(url, {
17643
+ method: "PUT",
17644
+ headers,
17645
+ body: JSON.stringify({ fields: { description: adf } })
17646
+ });
17647
+ if (!resp.ok) {
17648
+ const body = await parseErrorBody2(resp);
17649
+ throw new Error(
17650
+ `PUT ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
17651
+ );
17652
+ }
17653
+ return {
17654
+ issueKey,
17655
+ action: "description-updated",
17656
+ url: `${base}/browse/${encodeURIComponent(issueKey)}`
17657
+ };
17658
+ }
17659
+
17023
17660
  // src/converters/ndjson-parser.ts
17024
17661
  function parseNdjson(ndjson) {
17025
17662
  const lines = ndjson.trim().split("\n").filter(Boolean);
@@ -17320,10 +17957,10 @@ function pickleStepArgumentToDocs(ps) {
17320
17957
  const docs = [];
17321
17958
  const phase = "static";
17322
17959
  if (ps.argument.dataTable) {
17323
- const table = ps.argument.dataTable;
17324
- if (table.rows.length > 0) {
17325
- const columns = table.rows[0].cells.map((c) => c.value);
17326
- const rows = table.rows.slice(1).map((r) => r.cells.map((c) => c.value));
17960
+ const table2 = ps.argument.dataTable;
17961
+ if (table2.rows.length > 0) {
17962
+ const columns = table2.rows[0].cells.map((c) => c.value);
17963
+ const rows = table2.rows.slice(1).map((r) => r.cells.map((c) => c.value));
17327
17964
  docs.push({
17328
17965
  kind: "table",
17329
17966
  label: "",
@@ -17386,16 +18023,16 @@ function pickleStepArgumentToDocs(ps) {
17386
18023
  }
17387
18024
 
17388
18025
  // src/notifiers/ansi-strip.ts
17389
- function stripAnsi(text) {
17390
- return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
18026
+ function stripAnsi(text2) {
18027
+ return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
17391
18028
  }
17392
18029
 
17393
18030
  // src/notifiers/slack.ts
17394
- function truncate(text, maxLen) {
17395
- if (text.length <= maxLen) return text;
17396
- return text.slice(0, maxLen - 3) + "...";
18031
+ function truncate(text2, maxLen) {
18032
+ if (text2.length <= maxLen) return text2;
18033
+ return text2.slice(0, maxLen - 3) + "...";
17397
18034
  }
17398
- function formatDuration2(ms) {
18035
+ function formatDuration3(ms) {
17399
18036
  const seconds = ms / 1e3;
17400
18037
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
17401
18038
  const minutes = Math.floor(seconds / 60);
@@ -17422,7 +18059,7 @@ function buildSlackPayload(summary, maxFailedTests) {
17422
18059
  { type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
17423
18060
  { type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
17424
18061
  { type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
17425
- { type: "mrkdwn", text: `*Duration:* ${formatDuration2(summary.durationMs)}` },
18062
+ { type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
17426
18063
  { type: "mrkdwn", text: `*Status:* ${statusText}` }
17427
18064
  ]
17428
18065
  });
@@ -17437,9 +18074,9 @@ function buildSlackPayload(summary, maxFailedTests) {
17437
18074
  }
17438
18075
  return `*${name}*`;
17439
18076
  });
17440
- let text = lines.join("\n\n");
18077
+ let text2 = lines.join("\n\n");
17441
18078
  if (summary.failedTests.length > maxFailedTests) {
17442
- text += `
18079
+ text2 += `
17443
18080
 
17444
18081
  _...and ${summary.failedTests.length - maxFailedTests} more_`;
17445
18082
  }
@@ -17447,7 +18084,7 @@ _...and ${summary.failedTests.length - maxFailedTests} more_`;
17447
18084
  type: "section",
17448
18085
  text: {
17449
18086
  type: "mrkdwn",
17450
- text
18087
+ text: text2
17451
18088
  }
17452
18089
  });
17453
18090
  }
@@ -17524,11 +18161,11 @@ async function sendSlackNotification(args, deps) {
17524
18161
  }
17525
18162
 
17526
18163
  // src/notifiers/teams.ts
17527
- function truncate2(text, maxLen) {
17528
- if (text.length <= maxLen) return text;
17529
- return text.slice(0, maxLen - 3) + "...";
18164
+ function truncate2(text2, maxLen) {
18165
+ if (text2.length <= maxLen) return text2;
18166
+ return text2.slice(0, maxLen - 3) + "...";
17530
18167
  }
17531
- function formatDuration3(ms) {
18168
+ function formatDuration4(ms) {
17532
18169
  const seconds = ms / 1e3;
17533
18170
  if (seconds < 60) return `${seconds.toFixed(1)}s`;
17534
18171
  const minutes = Math.floor(seconds / 60);
@@ -17554,7 +18191,7 @@ function buildTeamsPayload(summary, maxFailedTests) {
17554
18191
  { title: "Passed", value: String(summary.passed) },
17555
18192
  { title: "Failed", value: String(summary.failed) },
17556
18193
  { title: "Skipped", value: String(summary.skipped) },
17557
- { title: "Duration", value: formatDuration3(summary.durationMs) }
18194
+ { title: "Duration", value: formatDuration4(summary.durationMs) }
17558
18195
  ]
17559
18196
  });
17560
18197
  if (summary.failedTests.length > 0) {
@@ -18051,7 +18688,8 @@ var FORMAT_EXTENSIONS = {
18051
18688
  "cucumber-html": ".cucumber.html",
18052
18689
  junit: ".junit.xml",
18053
18690
  "cucumber-json": ".cucumber.json",
18054
- "cucumber-messages": ".ndjson"
18691
+ "cucumber-messages": ".ndjson",
18692
+ confluence: ".adf.json"
18055
18693
  };
18056
18694
  var TEST_EXTENSIONS = [
18057
18695
  ".test.ts",
@@ -18219,6 +18857,19 @@ var ReportGenerator = class {
18219
18857
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
18220
18858
  customRenderers: options.markdown?.customRenderers
18221
18859
  },
18860
+ confluence: {
18861
+ title: options.confluence?.title ?? "User Stories",
18862
+ includeStatusIcons: options.confluence?.includeStatusIcons ?? true,
18863
+ includeMetadata: options.confluence?.includeMetadata ?? true,
18864
+ includeSummaryTable: options.confluence?.includeSummaryTable ?? true,
18865
+ includeErrors: options.confluence?.includeErrors ?? true,
18866
+ scenarioHeadingLevel: options.confluence?.scenarioHeadingLevel ?? 3,
18867
+ groupBy: options.confluence?.groupBy ?? "file",
18868
+ sortScenarios: options.confluence?.sortScenarios ?? "source",
18869
+ pretty: options.confluence?.pretty ?? true,
18870
+ permalinkBaseUrl: options.confluence?.permalinkBaseUrl,
18871
+ ticketUrlTemplate: options.confluence?.ticketUrlTemplate
18872
+ },
18222
18873
  astro: {
18223
18874
  assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
18224
18875
  assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
@@ -18394,6 +19045,22 @@ var ReportGenerator = class {
18394
19045
  });
18395
19046
  return formatter.format(run);
18396
19047
  }
19048
+ case "confluence": {
19049
+ const formatter = new ConfluenceFormatter({
19050
+ title: this.options.confluence.title,
19051
+ includeStatusIcons: this.options.confluence.includeStatusIcons,
19052
+ includeMetadata: this.options.confluence.includeMetadata,
19053
+ includeSummaryTable: this.options.confluence.includeSummaryTable,
19054
+ includeErrors: this.options.confluence.includeErrors,
19055
+ scenarioHeadingLevel: this.options.confluence.scenarioHeadingLevel,
19056
+ groupBy: this.options.confluence.groupBy,
19057
+ sortScenarios: this.options.confluence.sortScenarios,
19058
+ pretty: this.options.confluence.pretty,
19059
+ permalinkBaseUrl: this.options.confluence.permalinkBaseUrl,
19060
+ ticketUrlTemplate: this.options.confluence.ticketUrlTemplate
19061
+ });
19062
+ return formatter.format(run);
19063
+ }
18397
19064
  case "markdown": {
18398
19065
  const formatter = new MarkdownFormatter({
18399
19066
  title: this.options.markdown.title,
@@ -18508,17 +19175,22 @@ USAGE
18508
19175
  executable-stories validate <file>
18509
19176
  executable-stories validate --stdin
18510
19177
  executable-stories init-astro [directory]
19178
+ executable-stories publish-confluence <file.adf.json> [options]
19179
+ executable-stories publish-jira <file.adf.json> [options]
18511
19180
 
18512
19181
  SUBCOMMANDS
18513
- format Read raw test results and generate reports
18514
- compare Compare two runs and generate a diff report
18515
- list List scenarios from a test run (text table or JSON)
18516
- validate Validate a JSON file against the schema (no output generated)
18517
- init-astro Scaffold an Astro Starlight docs site for story output
19182
+ format Read raw test results and generate reports
19183
+ compare Compare two runs and generate a diff report
19184
+ list List scenarios from a test run (text table or JSON)
19185
+ validate Validate a JSON file against the schema (no output generated)
19186
+ init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
19187
+ publish-confluence Publish an ADF JSON file to a Confluence page via REST API
19188
+ publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
18518
19189
 
18519
19190
  OPTIONS
18520
- --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, or custom names from config (default: html)
18521
- astro Starlight-compatible Markdown (for Astro docs sites)
19191
+ --format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, or custom names from config (default: html)
19192
+ astro Themed Markdown (for Astro docs sites with matching CSS)
19193
+ confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
18522
19194
  html Custom HTML report (accessible, dark mode, mermaid)
18523
19195
  cucumber-html Official Cucumber HTML report
18524
19196
  markdown Markdown documentation
@@ -18572,6 +19244,26 @@ INIT-ASTRO
18572
19244
  executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
18573
19245
  --force Overwrite existing directory
18574
19246
 
19247
+ PUBLISH-CONFLUENCE
19248
+ executable-stories publish-confluence <file.adf.json> [options]
19249
+ --page-id <id> Update an existing page (alternative to --space-id)
19250
+ --space-id <id> Create a new page (requires --title)
19251
+ --parent-id <id> Parent page ID (for new pages)
19252
+ --title <title> Page title
19253
+ --base-url <url> Confluence base URL (env: CONFLUENCE_BASE_URL)
19254
+ --email <email> Atlassian email (env: CONFLUENCE_EMAIL)
19255
+ --token <token> API token (env: CONFLUENCE_TOKEN)
19256
+ --dry-run Validate and print request plan, don't POST
19257
+
19258
+ PUBLISH-JIRA
19259
+ executable-stories publish-jira <file.adf.json> [options]
19260
+ --issue <KEY> Issue key, e.g. PROJ-123 (required)
19261
+ --mode <mode> "comment" (default) or "description"
19262
+ --base-url <url> Jira base URL (env: JIRA_BASE_URL)
19263
+ --email <email> Atlassian email (env: JIRA_EMAIL)
19264
+ --token <token> API token (env: JIRA_TOKEN)
19265
+ --dry-run Validate and print request plan, don't POST
19266
+
18575
19267
  NOTIFICATIONS
18576
19268
  --slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
18577
19269
  --teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
@@ -18606,17 +19298,38 @@ async function parseCliArgs(argv) {
18606
19298
  process.exit(EXIT_SUCCESS);
18607
19299
  }
18608
19300
  const subcommand = args[0];
18609
- if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro") {
18610
- console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", or "init-astro".`);
19301
+ if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
19302
+ console.error(
19303
+ `Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
19304
+ );
18611
19305
  process.exit(EXIT_USAGE);
18612
19306
  }
19307
+ if (subcommand === "publish-confluence") {
19308
+ await runPublishConfluence(args.slice(1));
19309
+ process.exit(EXIT_SUCCESS);
19310
+ }
19311
+ if (subcommand === "publish-jira") {
19312
+ await runPublishJira(args.slice(1));
19313
+ process.exit(EXIT_SUCCESS);
19314
+ }
18613
19315
  if (subcommand === "init-astro") {
18614
19316
  const initArgs = args.slice(1);
18615
19317
  const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
18616
19318
  const force = initArgs.includes("--force");
18617
19319
  try {
18618
19320
  const result = initAstro({ targetDir, force });
18619
- console.log(`Scaffolded Astro Starlight project at ${result.targetDir}`);
19321
+ console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
19322
+ console.log("");
19323
+ console.log("Themes available in src/styles/themes/:");
19324
+ console.log(" default.css IBM Plex Sans, cucumber green (default)");
19325
+ console.log(" corporate.css DM Sans, navy accent");
19326
+ console.log(" terminal.css JetBrains Mono, green-on-dark");
19327
+ console.log(" minimal.css DM Sans, warm teal");
19328
+ console.log(" dashboard.css DM Sans, blue accent");
19329
+ console.log(" playful.css Source Sans, coral pastels");
19330
+ console.log("");
19331
+ console.log("To change theme, edit astro.config.mjs customCss array.");
19332
+ console.log("");
18620
19333
  console.log("");
18621
19334
  console.log("Next steps:");
18622
19335
  console.log(` cd ${result.targetDir}`);
@@ -18718,7 +19431,7 @@ async function parseCliArgs(argv) {
18718
19431
  }
18719
19432
  const pluginConfig = await loadConfig(values["config"]);
18720
19433
  const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
18721
- const builtInFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
19434
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18722
19435
  const formatStr = values.format;
18723
19436
  const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
18724
19437
  const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
@@ -18726,7 +19439,7 @@ async function parseCliArgs(argv) {
18726
19439
  const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
18727
19440
  if (unknownFormats.length > 0) {
18728
19441
  const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
18729
- console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
19442
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
18730
19443
  process.exit(EXIT_USAGE);
18731
19444
  }
18732
19445
  const formats = builtInRequested;
@@ -18877,18 +19590,18 @@ function readStdin() {
18877
19590
  process.stdin.on("error", reject);
18878
19591
  });
18879
19592
  }
18880
- function parseJson(text) {
19593
+ function parseJson(text2) {
18881
19594
  try {
18882
- return JSON.parse(text);
19595
+ return JSON.parse(text2);
18883
19596
  } catch (err) {
18884
19597
  const msg = err instanceof Error ? err.message : String(err);
18885
19598
  console.error(`Error: Invalid JSON \u2014 ${msg}`);
18886
19599
  process.exit(EXIT_USAGE);
18887
19600
  }
18888
19601
  }
18889
- function tryParseJson(text) {
19602
+ function tryParseJson(text2) {
18890
19603
  try {
18891
- return JSON.parse(text);
19604
+ return JSON.parse(text2);
18892
19605
  } catch {
18893
19606
  return void 0;
18894
19607
  }
@@ -18938,17 +19651,17 @@ ${msg}`);
18938
19651
  }
18939
19652
  return { run: canonical, droppedMissingStory };
18940
19653
  }
18941
- function normalizeRunFromText(text, args) {
19654
+ function normalizeRunFromText(text2, args) {
18942
19655
  if (args.inputType === "ndjson") {
18943
19656
  try {
18944
- return { run: parseNdjson(text), droppedMissingStory: 0 };
19657
+ return { run: parseNdjson(text2), droppedMissingStory: 0 };
18945
19658
  } catch (err) {
18946
19659
  const msg = err instanceof Error ? err.message : String(err);
18947
19660
  console.error(`NDJSON parse failed: ${msg}`);
18948
19661
  process.exit(EXIT_SCHEMA_VALIDATION);
18949
19662
  }
18950
19663
  }
18951
- return normalizeRunFromJsonData(parseJson(text), args);
19664
+ return normalizeRunFromJsonData(parseJson(text2), args);
18952
19665
  }
18953
19666
  function applySelection(run, args) {
18954
19667
  const testCases = selectTestCases(
@@ -18964,15 +19677,15 @@ function applySelection(run, args) {
18964
19677
  );
18965
19678
  return { ...run, testCases };
18966
19679
  }
18967
- function tryNormalizeRunFromText(text, args) {
19680
+ function tryNormalizeRunFromText(text2, args) {
18968
19681
  if (args.inputType === "ndjson") {
18969
19682
  try {
18970
- return parseNdjson(text);
19683
+ return parseNdjson(text2);
18971
19684
  } catch {
18972
19685
  return void 0;
18973
19686
  }
18974
19687
  }
18975
- const data = tryParseJson(text);
19688
+ const data = tryParseJson(text2);
18976
19689
  if (data === void 0) return void 0;
18977
19690
  if (args.inputType === "canonical") {
18978
19691
  try {
@@ -19052,8 +19765,8 @@ async function main() {
19052
19765
  }
19053
19766
  }
19054
19767
  if (args.subcommand === "list") {
19055
- const text2 = await readInput(args);
19056
- const run = applySelection(normalizeRunFromText(text2, args).run, args);
19768
+ const text3 = await readInput(args);
19769
+ const run = applySelection(normalizeRunFromText(text3, args).run, args);
19057
19770
  const resolvedFormat = args.jsonSummary ? "json" : args.listFormat;
19058
19771
  const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
19059
19772
  if (!validListFormats.has(resolvedFormat)) {
@@ -19067,10 +19780,10 @@ async function main() {
19067
19780
  console.log(output);
19068
19781
  process.exit(EXIT_SUCCESS);
19069
19782
  }
19070
- const text = await readInput(args);
19783
+ const text2 = await readInput(args);
19071
19784
  if (args.inputType === "ndjson") {
19072
19785
  if (args.subcommand === "validate") {
19073
- const lines = text.trim().split("\n").filter(Boolean);
19786
+ const lines = text2.trim().split("\n").filter(Boolean);
19074
19787
  const validKeys = /* @__PURE__ */ new Set([
19075
19788
  "meta",
19076
19789
  "source",
@@ -19103,7 +19816,7 @@ async function main() {
19103
19816
  }
19104
19817
  let run;
19105
19818
  try {
19106
- run = parseNdjson(text);
19819
+ run = parseNdjson(text2);
19107
19820
  } catch (err) {
19108
19821
  const msg = err instanceof Error ? err.message : String(err);
19109
19822
  console.error(`NDJSON parse failed: ${msg}`);
@@ -19127,7 +19840,7 @@ async function main() {
19127
19840
  process.exit(EXIT_GENERATION);
19128
19841
  }
19129
19842
  }
19130
- const data = parseJson(text);
19843
+ const data = parseJson(text2);
19131
19844
  if (args.subcommand === "validate") {
19132
19845
  if (args.inputType === "canonical") {
19133
19846
  try {
@@ -19456,6 +20169,225 @@ function printCompareResult(result, args, startMs) {
19456
20169
  console.log(result.prSummary);
19457
20170
  }
19458
20171
  }
20172
+ async function runPublishConfluence(rawArgs) {
20173
+ const { values, positionals } = parseArgs({
20174
+ args: rawArgs,
20175
+ options: {
20176
+ "page-id": { type: "string" },
20177
+ "space-id": { type: "string" },
20178
+ "parent-id": { type: "string" },
20179
+ title: { type: "string" },
20180
+ "base-url": { type: "string" },
20181
+ email: { type: "string" },
20182
+ token: { type: "string" },
20183
+ "dry-run": { type: "boolean", default: false },
20184
+ help: { type: "boolean", default: false }
20185
+ },
20186
+ allowPositionals: true,
20187
+ strict: true
20188
+ });
20189
+ if (values.help) {
20190
+ console.log(`Usage: executable-stories publish-confluence <file.adf.json> [options]
20191
+
20192
+ Publishes an ADF JSON document to a Confluence Cloud page.
20193
+
20194
+ Required (one of):
20195
+ --page-id <id> Update an existing page in place
20196
+ --space-id <id> Create a new page in a space (also requires --title)
20197
+
20198
+ Optional:
20199
+ --parent-id <id> Parent page ID (new pages only)
20200
+ --title <title> Page title (required for create; overrides current title on update)
20201
+ --base-url <url> Confluence base URL, e.g. https://acme.atlassian.net/wiki
20202
+ (env: CONFLUENCE_BASE_URL)
20203
+ --email <email> Atlassian account email (env: CONFLUENCE_EMAIL)
20204
+ --token <token> Atlassian API token (env: CONFLUENCE_TOKEN)
20205
+ --dry-run Validate inputs and print the request plan, don't POST
20206
+ --help Show this help
20207
+
20208
+ Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
20209
+ process.exit(EXIT_SUCCESS);
20210
+ }
20211
+ const inputFile = positionals[0];
20212
+ if (!inputFile) {
20213
+ console.error("Error: missing ADF file argument. Run with --help for usage.");
20214
+ process.exit(EXIT_USAGE);
20215
+ }
20216
+ if (!fs7.existsSync(inputFile)) {
20217
+ console.error(`Error: file not found: ${inputFile}`);
20218
+ process.exit(EXIT_USAGE);
20219
+ }
20220
+ const baseUrl = values["base-url"] ?? process.env.CONFLUENCE_BASE_URL;
20221
+ const email = values.email ?? process.env.CONFLUENCE_EMAIL;
20222
+ const token = values.token ?? process.env.CONFLUENCE_TOKEN;
20223
+ const pageId = values["page-id"];
20224
+ const spaceId = values["space-id"];
20225
+ const parentId = values["parent-id"];
20226
+ const title = values.title;
20227
+ const dryRun = values["dry-run"];
20228
+ if (!baseUrl) {
20229
+ console.error(
20230
+ "Error: --base-url or CONFLUENCE_BASE_URL is required (e.g. https://acme.atlassian.net/wiki)"
20231
+ );
20232
+ process.exit(EXIT_USAGE);
20233
+ }
20234
+ if (!pageId && !spaceId) {
20235
+ console.error(
20236
+ "Error: specify either --page-id (to update) or --space-id (to create)"
20237
+ );
20238
+ process.exit(EXIT_USAGE);
20239
+ }
20240
+ if (!pageId && !title) {
20241
+ console.error("Error: --title is required when creating a new page");
20242
+ process.exit(EXIT_USAGE);
20243
+ }
20244
+ const adf = fs7.readFileSync(path7.resolve(inputFile), "utf8");
20245
+ if (dryRun) {
20246
+ console.log(
20247
+ JSON.stringify(
20248
+ {
20249
+ action: pageId ? "update" : "create",
20250
+ baseUrl,
20251
+ pageId,
20252
+ spaceId,
20253
+ parentId,
20254
+ title,
20255
+ adfBytes: adf.length
20256
+ },
20257
+ null,
20258
+ 2
20259
+ )
20260
+ );
20261
+ process.exit(EXIT_SUCCESS);
20262
+ }
20263
+ if (!email || !token) {
20264
+ console.error(
20265
+ "Error: --email/CONFLUENCE_EMAIL and --token/CONFLUENCE_TOKEN are required unless --dry-run is set"
20266
+ );
20267
+ process.exit(EXIT_USAGE);
20268
+ }
20269
+ try {
20270
+ const result = await publishConfluencePage(
20271
+ { adf, pageId, spaceId, parentId, title, baseUrl },
20272
+ { auth: { email, token } }
20273
+ );
20274
+ console.log(
20275
+ `${result.action === "created" ? "Created" : "Updated"} "${result.title}" (v${result.version}) \u2192 ${result.url}`
20276
+ );
20277
+ process.exit(EXIT_SUCCESS);
20278
+ } catch (err) {
20279
+ console.error(`Error: ${err.message}`);
20280
+ process.exit(EXIT_GENERATION);
20281
+ }
20282
+ }
20283
+ async function runPublishJira(rawArgs) {
20284
+ const { values, positionals } = parseArgs({
20285
+ args: rawArgs,
20286
+ options: {
20287
+ issue: { type: "string" },
20288
+ mode: { type: "string", default: "comment" },
20289
+ "base-url": { type: "string" },
20290
+ email: { type: "string" },
20291
+ token: { type: "string" },
20292
+ "dry-run": { type: "boolean", default: false },
20293
+ help: { type: "boolean", default: false }
20294
+ },
20295
+ allowPositionals: true,
20296
+ strict: true
20297
+ });
20298
+ if (values.help) {
20299
+ console.log(`Usage: executable-stories publish-jira <file.adf.json> [options]
20300
+
20301
+ Publishes an ADF JSON document to a Jira Cloud issue.
20302
+
20303
+ Required:
20304
+ --issue <KEY> Issue key, e.g. PROJ-123
20305
+
20306
+ Optional:
20307
+ --mode <mode> "comment" (default, append-only) or "description" (replaces field)
20308
+ --base-url <url> Jira base URL, e.g. https://acme.atlassian.net
20309
+ (env: JIRA_BASE_URL)
20310
+ --email <email> Atlassian account email (env: JIRA_EMAIL)
20311
+ --token <token> Atlassian API token (env: JIRA_TOKEN)
20312
+ --dry-run Validate inputs and print the request plan, don't POST
20313
+ --help Show this help
20314
+
20315
+ Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
20316
+ process.exit(EXIT_SUCCESS);
20317
+ }
20318
+ const inputFile = positionals[0];
20319
+ if (!inputFile) {
20320
+ console.error("Error: missing ADF file argument. Run with --help for usage.");
20321
+ process.exit(EXIT_USAGE);
20322
+ }
20323
+ if (!fs7.existsSync(inputFile)) {
20324
+ console.error(`Error: file not found: ${inputFile}`);
20325
+ process.exit(EXIT_USAGE);
20326
+ }
20327
+ const baseUrl = values["base-url"] ?? process.env.JIRA_BASE_URL;
20328
+ const email = values.email ?? process.env.JIRA_EMAIL;
20329
+ const token = values.token ?? process.env.JIRA_TOKEN;
20330
+ const issueKey = values.issue;
20331
+ const modeRaw = values.mode;
20332
+ const dryRun = values["dry-run"];
20333
+ if (!baseUrl) {
20334
+ console.error(
20335
+ "Error: --base-url or JIRA_BASE_URL is required (e.g. https://acme.atlassian.net)"
20336
+ );
20337
+ process.exit(EXIT_USAGE);
20338
+ }
20339
+ if (!issueKey) {
20340
+ console.error("Error: --issue <KEY> is required (e.g. --issue PROJ-123)");
20341
+ process.exit(EXIT_USAGE);
20342
+ }
20343
+ if (modeRaw !== "comment" && modeRaw !== "description") {
20344
+ console.error(
20345
+ `Error: --mode must be "comment" or "description" (got "${modeRaw}")`
20346
+ );
20347
+ process.exit(EXIT_USAGE);
20348
+ }
20349
+ const mode = modeRaw;
20350
+ const adf = fs7.readFileSync(path7.resolve(inputFile), "utf8");
20351
+ if (dryRun) {
20352
+ console.log(
20353
+ JSON.stringify(
20354
+ {
20355
+ action: mode === "description" ? "description-updated" : "comment-added",
20356
+ baseUrl,
20357
+ issueKey,
20358
+ mode,
20359
+ adfBytes: adf.length
20360
+ },
20361
+ null,
20362
+ 2
20363
+ )
20364
+ );
20365
+ process.exit(EXIT_SUCCESS);
20366
+ }
20367
+ if (!email || !token) {
20368
+ console.error(
20369
+ "Error: --email/JIRA_EMAIL and --token/JIRA_TOKEN are required unless --dry-run is set"
20370
+ );
20371
+ process.exit(EXIT_USAGE);
20372
+ }
20373
+ try {
20374
+ const result = await publishJiraIssue(
20375
+ { adf, issueKey, baseUrl, mode },
20376
+ { auth: { email, token } }
20377
+ );
20378
+ if (result.action === "comment-added") {
20379
+ console.log(
20380
+ `Added comment to ${result.issueKey} (comment ${result.commentId}) \u2192 ${result.url}`
20381
+ );
20382
+ } else {
20383
+ console.log(`Updated description for ${result.issueKey} \u2192 ${result.url}`);
20384
+ }
20385
+ process.exit(EXIT_SUCCESS);
20386
+ } catch (err) {
20387
+ console.error(`Error: ${err.message}`);
20388
+ process.exit(EXIT_GENERATION);
20389
+ }
20390
+ }
19459
20391
  main().catch((err) => {
19460
20392
  console.error(err);
19461
20393
  process.exit(EXIT_USAGE);