executable-stories-formatters 0.7.7 → 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
  });
@@ -15813,11 +15813,11 @@ var CucumberHtmlFormatter = class {
15813
15813
  for (const envelope of envelopes) {
15814
15814
  const accepted = htmlStream.write(envelope);
15815
15815
  if (!accepted) {
15816
- await new Promise((resolve7) => htmlStream.once("drain", resolve7));
15816
+ await new Promise((resolve8) => htmlStream.once("drain", resolve8));
15817
15817
  }
15818
15818
  }
15819
- await new Promise((resolve7, reject) => {
15820
- collector.on("finish", resolve7);
15819
+ await new Promise((resolve8, reject) => {
15820
+ collector.on("finish", resolve8);
15821
15821
  collector.on("error", reject);
15822
15822
  htmlStream.end();
15823
15823
  });
@@ -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) {
@@ -17973,6 +18610,37 @@ function listScenarios(args, _deps) {
17973
18610
  }));
17974
18611
  return JSON.stringify(items, null, 2);
17975
18612
  }
18613
+ if (format === "csv") {
18614
+ const header = "id,scenario,status,sourceFile,sourceLine,tags";
18615
+ const rows = testCases.map((tc) => {
18616
+ const fields = [
18617
+ tc.id,
18618
+ tc.story.scenario,
18619
+ tc.status,
18620
+ tc.sourceFile,
18621
+ String(tc.sourceLine),
18622
+ tc.tags.join(" ")
18623
+ ];
18624
+ return fields.map((f) => {
18625
+ if (f.includes(",") || f.includes('"') || f.includes("\n")) {
18626
+ return `"${f.replace(/"/g, '""')}"`;
18627
+ }
18628
+ return f;
18629
+ }).join(",");
18630
+ });
18631
+ return [header, ...rows].join("\n");
18632
+ }
18633
+ if (format === "markdown-table") {
18634
+ const header = "| Status | Scenario | Location | Tags |";
18635
+ const divider = "|--------|----------|----------|------|";
18636
+ const rows = testCases.map((tc) => {
18637
+ const icon = STATUS_ICONS[tc.status] ?? "?";
18638
+ const location = `${tc.sourceFile}:${tc.sourceLine}`;
18639
+ const tags = tc.tags.map((t) => `@${t}`).join(" ");
18640
+ return `| ${icon} | ${tc.story.scenario} | ${location} | ${tags} |`;
18641
+ });
18642
+ return [header, divider, ...rows].join("\n");
18643
+ }
17976
18644
  if (testCases.length === 0) {
17977
18645
  return "No scenarios found.";
17978
18646
  }
@@ -18020,7 +18688,8 @@ var FORMAT_EXTENSIONS = {
18020
18688
  "cucumber-html": ".cucumber.html",
18021
18689
  junit: ".junit.xml",
18022
18690
  "cucumber-json": ".cucumber.json",
18023
- "cucumber-messages": ".ndjson"
18691
+ "cucumber-messages": ".ndjson",
18692
+ confluence: ".adf.json"
18024
18693
  };
18025
18694
  var TEST_EXTENSIONS = [
18026
18695
  ".test.ts",
@@ -18188,6 +18857,19 @@ var ReportGenerator = class {
18188
18857
  includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
18189
18858
  customRenderers: options.markdown?.customRenderers
18190
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
+ },
18191
18873
  astro: {
18192
18874
  assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
18193
18875
  assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
@@ -18363,6 +19045,22 @@ var ReportGenerator = class {
18363
19045
  });
18364
19046
  return formatter.format(run);
18365
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
+ }
18366
19064
  case "markdown": {
18367
19065
  const formatter = new MarkdownFormatter({
18368
19066
  title: this.options.markdown.title,
@@ -18444,6 +19142,22 @@ function copyDirRecursive(src, dest) {
18444
19142
  }
18445
19143
  }
18446
19144
 
19145
+ // src/config.ts
19146
+ import { existsSync as existsSync6 } from "fs";
19147
+ import { resolve as resolve6 } from "path";
19148
+ async function loadConfig(configPath) {
19149
+ const resolved = configPath ? resolve6(configPath) : resolve6(process.cwd(), "executable-stories.config.js");
19150
+ if (!existsSync6(resolved)) return {};
19151
+ const mod = await import(resolved);
19152
+ const config = mod.default;
19153
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
19154
+ throw new Error(
19155
+ `Config file at ${resolved} must export a default object. Got: ${typeof config}`
19156
+ );
19157
+ }
19158
+ return config;
19159
+ }
19160
+
18447
19161
  // src/cli.ts
18448
19162
  var EXIT_SUCCESS = 0;
18449
19163
  var EXIT_SCHEMA_VALIDATION = 1;
@@ -18461,23 +19175,29 @@ USAGE
18461
19175
  executable-stories validate <file>
18462
19176
  executable-stories validate --stdin
18463
19177
  executable-stories init-astro [directory]
19178
+ executable-stories publish-confluence <file.adf.json> [options]
19179
+ executable-stories publish-jira <file.adf.json> [options]
18464
19180
 
18465
19181
  SUBCOMMANDS
18466
- format Read raw test results and generate reports
18467
- compare Compare two runs and generate a diff report
18468
- list List scenarios from a test run (text table or JSON)
18469
- validate Validate a JSON file against the schema (no output generated)
18470
- 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)
18471
19189
 
18472
19190
  OPTIONS
18473
- --format <formats> Comma-separated formats (default: html)
18474
- 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
18475
19194
  html Custom HTML report (accessible, dark mode, mermaid)
18476
19195
  cucumber-html Official Cucumber HTML report
18477
19196
  markdown Markdown documentation
18478
19197
  junit JUnit XML
18479
19198
  cucumber-json Cucumber JSON
18480
19199
  cucumber-messages Raw NDJSON (Cucumber Messages)
19200
+ --config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
18481
19201
  --input-type <type> Input type: raw, canonical, or ndjson (default: raw)
18482
19202
  --output-dir <dir> Output directory (default: reports)
18483
19203
  --output-name <name> Base filename (default: index)
@@ -18501,7 +19221,8 @@ OPTIONS
18501
19221
  --asset-mode <mode> Asset bundling: "none" (default) or "copy"
18502
19222
  --allow-missing-assets Warn on missing assets instead of failing
18503
19223
  --stdin Read JSON from stdin instead of file
18504
- --json-summary Print machine-parsable JSON summary
19224
+ --list-format <format> list output format: text (default), json, csv, markdown-table
19225
+ --json-summary Deprecated alias for --list-format json
18505
19226
  --baseline <path|auto> Compare baseline file, or auto-pick a prior run for compare
18506
19227
  --baseline-dir <dir> Directory to scan when --baseline auto is used
18507
19228
  --pr-summary Print a PR-friendly markdown summary after compare
@@ -18510,7 +19231,8 @@ OPTIONS
18510
19231
  --help Show this help message
18511
19232
 
18512
19233
  LIST
18513
- list prints one scenario per line (text by default, JSON with --json-summary)
19234
+ list prints one scenario per line (--list-format text by default)
19235
+ list --list-format json outputs machine-parsable JSON (--json-summary is a deprecated alias)
18514
19236
  list supports --include-tags, --exclude-tags for filtering
18515
19237
  list supports --input-type and --stdin
18516
19238
 
@@ -18522,6 +19244,26 @@ INIT-ASTRO
18522
19244
  executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
18523
19245
  --force Overwrite existing directory
18524
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
+
18525
19267
  NOTIFICATIONS
18526
19268
  --slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
18527
19269
  --teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
@@ -18549,24 +19291,45 @@ EXIT CODES
18549
19291
  3 Formatter/generation failure
18550
19292
  4 Bad arguments / usage error
18551
19293
  `.trim();
18552
- function parseCliArgs(argv) {
19294
+ async function parseCliArgs(argv) {
18553
19295
  const args = argv.slice(2);
18554
19296
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
18555
19297
  console.log(HELP_TEXT);
18556
19298
  process.exit(EXIT_SUCCESS);
18557
19299
  }
18558
19300
  const subcommand = args[0];
18559
- if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro") {
18560
- console.error(`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", or "init-astro".`);
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
+ );
18561
19305
  process.exit(EXIT_USAGE);
18562
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
+ }
18563
19315
  if (subcommand === "init-astro") {
18564
19316
  const initArgs = args.slice(1);
18565
19317
  const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
18566
19318
  const force = initArgs.includes("--force");
18567
19319
  try {
18568
19320
  const result = initAstro({ targetDir, force });
18569
- 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("");
18570
19333
  console.log("");
18571
19334
  console.log("Next steps:");
18572
19335
  console.log(` cd ${result.targetDir}`);
@@ -18609,6 +19372,7 @@ function parseCliArgs(argv) {
18609
19372
  "html-theme-picker": { type: "boolean", default: false },
18610
19373
  stdin: { type: "boolean", default: false },
18611
19374
  "json-summary": { type: "boolean", default: false },
19375
+ "list-format": { type: "string", default: "text" },
18612
19376
  "emit-canonical": { type: "string" },
18613
19377
  "slack-webhook": { type: "string" },
18614
19378
  "teams-webhook": { type: "string" },
@@ -18627,6 +19391,7 @@ function parseCliArgs(argv) {
18627
19391
  "allow-missing-assets": { type: "boolean", default: false },
18628
19392
  "pr-summary": { type: "boolean", default: false },
18629
19393
  "pr-summary-file": { type: "string" },
19394
+ "config": { type: "string" },
18630
19395
  help: { type: "boolean", default: false }
18631
19396
  },
18632
19397
  allowPositionals: true,
@@ -18664,15 +19429,20 @@ function parseCliArgs(argv) {
18664
19429
  console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
18665
19430
  process.exit(EXIT_USAGE);
18666
19431
  }
18667
- const validFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
19432
+ const pluginConfig = await loadConfig(values["config"]);
19433
+ const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
19434
+ const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
18668
19435
  const formatStr = values.format;
18669
- const formats = formatStr.split(",").map((f) => f.trim());
18670
- for (const f of formats) {
18671
- if (!validFormats.has(f)) {
18672
- console.error(`Error: Unknown format "${f}". Valid: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html.`);
18673
- process.exit(EXIT_USAGE);
18674
- }
19436
+ const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
19437
+ const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
19438
+ const customRequested = allRequestedFormats.filter((f) => customFormatterNames.has(f));
19439
+ const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
19440
+ if (unknownFormats.length > 0) {
19441
+ const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
19442
+ console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
19443
+ process.exit(EXIT_USAGE);
18675
19444
  }
19445
+ const formats = builtInRequested;
18676
19446
  const htmlTheme = values["html-theme"];
18677
19447
  const validThemes = /* @__PURE__ */ new Set(["default", "corporate", "terminal", "minimal", "dashboard", "playful"]);
18678
19448
  if (!validThemes.has(htmlTheme)) {
@@ -18740,7 +19510,7 @@ function parseCliArgs(argv) {
18740
19510
  console.error(`Error: --asset-mode must be "none" or "copy", got "${assetModeRaw}".`);
18741
19511
  process.exit(EXIT_USAGE);
18742
19512
  }
18743
- return {
19513
+ const cliArgs = {
18744
19514
  subcommand,
18745
19515
  inputFile,
18746
19516
  baselineFile,
@@ -18769,6 +19539,7 @@ function parseCliArgs(argv) {
18769
19539
  htmlNoToc: values["html-no-toc"],
18770
19540
  htmlThemePicker: values["html-theme-picker"],
18771
19541
  jsonSummary: values["json-summary"],
19542
+ listFormat: values["list-format"],
18772
19543
  emitCanonical: values["emit-canonical"],
18773
19544
  slackWebhook,
18774
19545
  teamsWebhook,
@@ -18786,8 +19557,10 @@ function parseCliArgs(argv) {
18786
19557
  assetMode: assetModeRaw,
18787
19558
  allowMissingAssets: values["allow-missing-assets"],
18788
19559
  prSummary: values["pr-summary"],
18789
- prSummaryFile: values["pr-summary-file"]
19560
+ prSummaryFile: values["pr-summary-file"],
19561
+ config: values["config"]
18790
19562
  };
19563
+ return { args: cliArgs, pluginConfig, customRequested };
18791
19564
  }
18792
19565
  async function readInput(args) {
18793
19566
  if (args.stdin) {
@@ -18809,26 +19582,26 @@ function readFileInput(filePath) {
18809
19582
  return fs7.readFileSync(resolved, "utf8");
18810
19583
  }
18811
19584
  function readStdin() {
18812
- return new Promise((resolve7, reject) => {
19585
+ return new Promise((resolve8, reject) => {
18813
19586
  const chunks = [];
18814
19587
  process.stdin.setEncoding("utf8");
18815
19588
  process.stdin.on("data", (chunk) => chunks.push(chunk));
18816
- process.stdin.on("end", () => resolve7(chunks.join("")));
19589
+ process.stdin.on("end", () => resolve8(chunks.join("")));
18817
19590
  process.stdin.on("error", reject);
18818
19591
  });
18819
19592
  }
18820
- function parseJson(text) {
19593
+ function parseJson(text2) {
18821
19594
  try {
18822
- return JSON.parse(text);
19595
+ return JSON.parse(text2);
18823
19596
  } catch (err) {
18824
19597
  const msg = err instanceof Error ? err.message : String(err);
18825
19598
  console.error(`Error: Invalid JSON \u2014 ${msg}`);
18826
19599
  process.exit(EXIT_USAGE);
18827
19600
  }
18828
19601
  }
18829
- function tryParseJson(text) {
19602
+ function tryParseJson(text2) {
18830
19603
  try {
18831
- return JSON.parse(text);
19604
+ return JSON.parse(text2);
18832
19605
  } catch {
18833
19606
  return void 0;
18834
19607
  }
@@ -18878,17 +19651,17 @@ ${msg}`);
18878
19651
  }
18879
19652
  return { run: canonical, droppedMissingStory };
18880
19653
  }
18881
- function normalizeRunFromText(text, args) {
19654
+ function normalizeRunFromText(text2, args) {
18882
19655
  if (args.inputType === "ndjson") {
18883
19656
  try {
18884
- return { run: parseNdjson(text), droppedMissingStory: 0 };
19657
+ return { run: parseNdjson(text2), droppedMissingStory: 0 };
18885
19658
  } catch (err) {
18886
19659
  const msg = err instanceof Error ? err.message : String(err);
18887
19660
  console.error(`NDJSON parse failed: ${msg}`);
18888
19661
  process.exit(EXIT_SCHEMA_VALIDATION);
18889
19662
  }
18890
19663
  }
18891
- return normalizeRunFromJsonData(parseJson(text), args);
19664
+ return normalizeRunFromJsonData(parseJson(text2), args);
18892
19665
  }
18893
19666
  function applySelection(run, args) {
18894
19667
  const testCases = selectTestCases(
@@ -18904,15 +19677,15 @@ function applySelection(run, args) {
18904
19677
  );
18905
19678
  return { ...run, testCases };
18906
19679
  }
18907
- function tryNormalizeRunFromText(text, args) {
19680
+ function tryNormalizeRunFromText(text2, args) {
18908
19681
  if (args.inputType === "ndjson") {
18909
19682
  try {
18910
- return parseNdjson(text);
19683
+ return parseNdjson(text2);
18911
19684
  } catch {
18912
19685
  return void 0;
18913
19686
  }
18914
19687
  }
18915
- const data = tryParseJson(text);
19688
+ const data = tryParseJson(text2);
18916
19689
  if (data === void 0) return void 0;
18917
19690
  if (args.inputType === "canonical") {
18918
19691
  try {
@@ -18973,7 +19746,7 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
18973
19746
  return picked.file;
18974
19747
  }
18975
19748
  async function main() {
18976
- const args = parseCliArgs(process.argv);
19749
+ const { args, pluginConfig, customRequested } = await parseCliArgs(process.argv);
18977
19750
  const startMs = Date.now();
18978
19751
  if (args.subcommand === "compare") {
18979
19752
  const currentText = readFileInput(args.currentFile);
@@ -18992,17 +19765,25 @@ async function main() {
18992
19765
  }
18993
19766
  }
18994
19767
  if (args.subcommand === "list") {
18995
- const text2 = await readInput(args);
18996
- const run = applySelection(normalizeRunFromText(text2, args).run, args);
18997
- const outputFormat = args.jsonSummary ? "json" : "text";
18998
- const output = listScenarios({ testCases: run.testCases, format: outputFormat }, {});
19768
+ const text3 = await readInput(args);
19769
+ const run = applySelection(normalizeRunFromText(text3, args).run, args);
19770
+ const resolvedFormat = args.jsonSummary ? "json" : args.listFormat;
19771
+ const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
19772
+ if (!validListFormats.has(resolvedFormat)) {
19773
+ console.error(`Error: Unknown list format "${resolvedFormat}". Valid: text, json, csv, markdown-table.`);
19774
+ process.exit(EXIT_USAGE);
19775
+ }
19776
+ const output = listScenarios(
19777
+ { testCases: run.testCases, format: resolvedFormat },
19778
+ {}
19779
+ );
18999
19780
  console.log(output);
19000
19781
  process.exit(EXIT_SUCCESS);
19001
19782
  }
19002
- const text = await readInput(args);
19783
+ const text2 = await readInput(args);
19003
19784
  if (args.inputType === "ndjson") {
19004
19785
  if (args.subcommand === "validate") {
19005
- const lines = text.trim().split("\n").filter(Boolean);
19786
+ const lines = text2.trim().split("\n").filter(Boolean);
19006
19787
  const validKeys = /* @__PURE__ */ new Set([
19007
19788
  "meta",
19008
19789
  "source",
@@ -19035,7 +19816,7 @@ async function main() {
19035
19816
  }
19036
19817
  let run;
19037
19818
  try {
19038
- run = parseNdjson(text);
19819
+ run = parseNdjson(text2);
19039
19820
  } catch (err) {
19040
19821
  const msg = err instanceof Error ? err.message : String(err);
19041
19822
  console.error(`NDJSON parse failed: ${msg}`);
@@ -19048,6 +19829,7 @@ async function main() {
19048
19829
  }
19049
19830
  try {
19050
19831
  const result = await generateReports(run, args);
19832
+ runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
19051
19833
  await dispatchNotifications(run, args);
19052
19834
  runHistoryPipeline(run, args);
19053
19835
  printResult(result, args, startMs);
@@ -19058,7 +19840,7 @@ async function main() {
19058
19840
  process.exit(EXIT_GENERATION);
19059
19841
  }
19060
19842
  }
19061
- const data = parseJson(text);
19843
+ const data = parseJson(text2);
19062
19844
  if (args.subcommand === "validate") {
19063
19845
  if (args.inputType === "canonical") {
19064
19846
  try {
@@ -19106,6 +19888,7 @@ ${msg}`);
19106
19888
  }
19107
19889
  try {
19108
19890
  const result = await generateReports(run, args);
19891
+ runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
19109
19892
  await dispatchNotifications(run, args);
19110
19893
  runHistoryPipeline(run, args);
19111
19894
  printResult(result, args, startMs);
@@ -19163,6 +19946,7 @@ ${msg}`);
19163
19946
  }
19164
19947
  try {
19165
19948
  const result = await generateReports(canonical, args, droppedMissingStory);
19949
+ runCustomFormatters(canonical, customRequested, pluginConfig.formatters ?? {}, args);
19166
19950
  await dispatchNotifications(canonical, args);
19167
19951
  runHistoryPipeline(canonical, args);
19168
19952
  printResult(result, args, startMs, droppedMissingStory);
@@ -19173,6 +19957,25 @@ ${msg}`);
19173
19957
  process.exit(EXIT_GENERATION);
19174
19958
  }
19175
19959
  }
19960
+ function runCustomFormatters(run, customRequested, formatters, args) {
19961
+ if (customRequested.length === 0) return;
19962
+ const outputDir = args.outputDir ?? ".";
19963
+ for (const formatName of customRequested) {
19964
+ const formatter = formatters[formatName];
19965
+ try {
19966
+ const content = formatter.format(run);
19967
+ const ext = formatter.fileExtension ?? formatName;
19968
+ const baseName = args.outputName ?? "report";
19969
+ const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
19970
+ const filepath = path7.join(outputDir, filename);
19971
+ fs7.mkdirSync(outputDir, { recursive: true });
19972
+ fs7.writeFileSync(filepath, content, "utf8");
19973
+ console.log(`Generated: ${filepath}`);
19974
+ } catch (err) {
19975
+ console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
19976
+ }
19977
+ }
19978
+ }
19176
19979
  async function dispatchNotifications(run, args) {
19177
19980
  const webhooks = args.webhookUrls.map((url) => {
19178
19981
  const opts = { url };
@@ -19366,6 +20169,225 @@ function printCompareResult(result, args, startMs) {
19366
20169
  console.log(result.prSummary);
19367
20170
  }
19368
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
+ }
19369
20391
  main().catch((err) => {
19370
20392
  console.error(err);
19371
20393
  process.exit(EXIT_USAGE);