@repo-toolkit/confluence 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -25
- package/cli.js +647 -14
- package/index.d.ts +150 -1
- package/index.js +622 -12
- package/package.json +2 -2
package/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ var DEFAULT_USER_AGENT = "repo-toolkit-confluence/1.0 (+node)";
|
|
|
28
28
|
var MAX_LIMIT = 250;
|
|
29
29
|
var MAX_ERROR_BODY_LENGTH = 8192;
|
|
30
30
|
var MAX_PAGES_PER_QUERY = 100;
|
|
31
|
+
var CONFLUENCE_MANAGED_LABEL = "repo-toolkit-confluence";
|
|
31
32
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
32
33
|
var DEFAULT_MAX_RETRIES = 3;
|
|
33
34
|
var DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
@@ -155,6 +156,44 @@ var ConfluenceClient = class {
|
|
|
155
156
|
body: JSON.stringify(body)
|
|
156
157
|
});
|
|
157
158
|
}
|
|
159
|
+
async getPageDescendants(pageId) {
|
|
160
|
+
const query = new URLSearchParams({ limit: String(MAX_LIMIT) });
|
|
161
|
+
return this.listAll(this.v2Url(`/pages/${encodeURIComponent(pageId)}/descendants?${query.toString()}`));
|
|
162
|
+
}
|
|
163
|
+
async getPageLabels(pageId) {
|
|
164
|
+
const query = new URLSearchParams({ limit: String(MAX_LIMIT) });
|
|
165
|
+
return this.listAll(this.v2Url(`/pages/${encodeURIComponent(pageId)}/labels?${query.toString()}`));
|
|
166
|
+
}
|
|
167
|
+
async addManagedLabel(pageId) {
|
|
168
|
+
await this.requestJson(this.v1Url(`/content/${encodeURIComponent(pageId)}/label`), {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify([{ prefix: "global", name: CONFLUENCE_MANAGED_LABEL }])
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async deletePage(pageId) {
|
|
175
|
+
await this.requestJson(this.v2Url(`/pages/${encodeURIComponent(pageId)}`), { method: "DELETE" });
|
|
176
|
+
}
|
|
177
|
+
async listAll(startUrl) {
|
|
178
|
+
const results = [];
|
|
179
|
+
const visited = /* @__PURE__ */ new Set();
|
|
180
|
+
let pageCount = 0;
|
|
181
|
+
let nextUrl = startUrl;
|
|
182
|
+
while (nextUrl) {
|
|
183
|
+
pageCount += 1;
|
|
184
|
+
if (pageCount > MAX_PAGES_PER_QUERY) {
|
|
185
|
+
throw new ConfluenceApiError(`Pagination limit (${MAX_PAGES_PER_QUERY}) exceeded`, 0, nextUrl, "");
|
|
186
|
+
}
|
|
187
|
+
if (visited.has(nextUrl)) {
|
|
188
|
+
throw new ConfluenceApiError("Confluence pagination loop detected", 0, nextUrl, "");
|
|
189
|
+
}
|
|
190
|
+
visited.add(nextUrl);
|
|
191
|
+
const data = await this.requestJson(nextUrl, { method: "GET" });
|
|
192
|
+
results.push(...data.results);
|
|
193
|
+
nextUrl = resolveNextUrl(this.baseUrl, this.baseUrlOrigin, data._links?.next);
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
158
197
|
async getAttachments(pageId) {
|
|
159
198
|
const results = [];
|
|
160
199
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -588,6 +627,64 @@ function titleFromSegment(segment) {
|
|
|
588
627
|
}
|
|
589
628
|
return segment;
|
|
590
629
|
}
|
|
630
|
+
var PAGE_TITLE_STRATEGIES = [
|
|
631
|
+
"filename-stem",
|
|
632
|
+
"filename",
|
|
633
|
+
"sentence-case-parent",
|
|
634
|
+
"sentence-case-parents",
|
|
635
|
+
"sentence-case-path"
|
|
636
|
+
];
|
|
637
|
+
var DEFAULT_PAGE_TITLE_STRATEGY = "filename-stem";
|
|
638
|
+
function resolvePageTitleStrategy(value) {
|
|
639
|
+
if (value === void 0 || value === null) {
|
|
640
|
+
return DEFAULT_PAGE_TITLE_STRATEGY;
|
|
641
|
+
}
|
|
642
|
+
if (typeof value === "string" && PAGE_TITLE_STRATEGIES.includes(value)) {
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Invalid pageTitleStrategy: expected one of ${PAGE_TITLE_STRATEGIES.join(", ")}, got ${JSON.stringify(value)}`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
function sentenceCaseStem(filename) {
|
|
650
|
+
let stem = filename;
|
|
651
|
+
if (/\.md$/i.test(stem)) {
|
|
652
|
+
stem = stem.slice(0, stem.length - MARKDOWN_EXT.length);
|
|
653
|
+
}
|
|
654
|
+
let out = stem.replace(/[-_]+/g, " ").trim().replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
655
|
+
const firstLetter = /[a-z]/.exec(out);
|
|
656
|
+
if (firstLetter) {
|
|
657
|
+
const i = firstLetter.index;
|
|
658
|
+
out = out.slice(0, i) + out.charAt(i).toUpperCase() + out.slice(i + 1);
|
|
659
|
+
}
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
function pageTitleFromSegments(segments, strategy) {
|
|
663
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
664
|
+
const parents = segments.slice(0, segments.length - 1);
|
|
665
|
+
switch (strategy) {
|
|
666
|
+
case "filename-stem":
|
|
667
|
+
return titleFromSegment(filename);
|
|
668
|
+
case "filename":
|
|
669
|
+
return filename;
|
|
670
|
+
case "sentence-case-parent": {
|
|
671
|
+
const title = sentenceCaseStem(filename);
|
|
672
|
+
if (parents.length === 0) {
|
|
673
|
+
return title;
|
|
674
|
+
}
|
|
675
|
+
return `${title} (${parents[parents.length - 1]})`;
|
|
676
|
+
}
|
|
677
|
+
case "sentence-case-parents": {
|
|
678
|
+
const title = sentenceCaseStem(filename);
|
|
679
|
+
if (parents.length === 0) {
|
|
680
|
+
return title;
|
|
681
|
+
}
|
|
682
|
+
return `${title} (${parents.join("/")})`;
|
|
683
|
+
}
|
|
684
|
+
case "sentence-case-path":
|
|
685
|
+
return `${sentenceCaseStem(filename)} (${[...parents, filename].join("/")})`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
591
688
|
|
|
592
689
|
// src/markdown.ts
|
|
593
690
|
var STORAGE_LINE_BREAK = "<br />";
|
|
@@ -1603,6 +1700,141 @@ function runMmdc(cmdPath, source, outFile, timeoutMs, maxStreamBytes) {
|
|
|
1603
1700
|
});
|
|
1604
1701
|
}
|
|
1605
1702
|
|
|
1703
|
+
// src/parent-summary.ts
|
|
1704
|
+
var PARENT_SUMMARY_START_MARKER = "<!-- repo-toolkit-confluence:parent-summary:start -->";
|
|
1705
|
+
var PARENT_SUMMARY_END_MARKER = "<!-- repo-toolkit-confluence:parent-summary:end -->";
|
|
1706
|
+
function escapeCdata(text) {
|
|
1707
|
+
return text.replace(/]]>/g, "]]]]><![CDATA[>");
|
|
1708
|
+
}
|
|
1709
|
+
function pageLink(pageId, title) {
|
|
1710
|
+
const safeTitle = escapeCdata(title);
|
|
1711
|
+
return `<ac:link><ri:page ri:content-id="${escapeXmlAttribute(pageId)}" /><ac:plain-text-link-body><![CDATA[${safeTitle}]]></ac:plain-text-link-body></ac:link>`;
|
|
1712
|
+
}
|
|
1713
|
+
function countOccurrences(haystack, needle) {
|
|
1714
|
+
let count = 0;
|
|
1715
|
+
let idx = 0;
|
|
1716
|
+
while (true) {
|
|
1717
|
+
const found = haystack.indexOf(needle, idx);
|
|
1718
|
+
if (found === -1) {
|
|
1719
|
+
break;
|
|
1720
|
+
}
|
|
1721
|
+
count += 1;
|
|
1722
|
+
idx = found + needle.length;
|
|
1723
|
+
}
|
|
1724
|
+
return count;
|
|
1725
|
+
}
|
|
1726
|
+
function mergeParentSummaryBody(currentBody, generatedRegion) {
|
|
1727
|
+
const startCount = countOccurrences(currentBody, PARENT_SUMMARY_START_MARKER);
|
|
1728
|
+
const endCount = countOccurrences(currentBody, PARENT_SUMMARY_END_MARKER);
|
|
1729
|
+
if (startCount === 0 && endCount === 0) {
|
|
1730
|
+
if (currentBody === "") {
|
|
1731
|
+
return generatedRegion;
|
|
1732
|
+
}
|
|
1733
|
+
const sep2 = currentBody.endsWith("\n") ? "" : "\n";
|
|
1734
|
+
return currentBody + sep2 + generatedRegion;
|
|
1735
|
+
}
|
|
1736
|
+
if (startCount === 1 && endCount === 1) {
|
|
1737
|
+
const startIdx = currentBody.indexOf(PARENT_SUMMARY_START_MARKER);
|
|
1738
|
+
const endIdx = currentBody.indexOf(PARENT_SUMMARY_END_MARKER);
|
|
1739
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
1740
|
+
throw new Error("malformed parent summary markers: missing marker");
|
|
1741
|
+
}
|
|
1742
|
+
if (startIdx > endIdx) {
|
|
1743
|
+
throw new Error("malformed parent summary markers: start after end");
|
|
1744
|
+
}
|
|
1745
|
+
const before = currentBody.slice(0, startIdx);
|
|
1746
|
+
const after = currentBody.slice(endIdx + PARENT_SUMMARY_END_MARKER.length);
|
|
1747
|
+
if (countOccurrences(before, PARENT_SUMMARY_START_MARKER) !== 0 || countOccurrences(before, PARENT_SUMMARY_END_MARKER) !== 0) {
|
|
1748
|
+
throw new Error("malformed parent summary markers: duplicate marker before region");
|
|
1749
|
+
}
|
|
1750
|
+
if (countOccurrences(after, PARENT_SUMMARY_START_MARKER) !== 0 || countOccurrences(after, PARENT_SUMMARY_END_MARKER) !== 0) {
|
|
1751
|
+
throw new Error("malformed parent summary markers: duplicate marker after region");
|
|
1752
|
+
}
|
|
1753
|
+
return before + generatedRegion + after;
|
|
1754
|
+
}
|
|
1755
|
+
throw new Error("malformed or duplicate parent summary markers: expected 0 or 1 managed region");
|
|
1756
|
+
}
|
|
1757
|
+
function renderParentSummary(input) {
|
|
1758
|
+
const lines = [];
|
|
1759
|
+
lines.push(PARENT_SUMMARY_START_MARKER);
|
|
1760
|
+
lines.push("<h2>Synced documentation</h2>");
|
|
1761
|
+
if (input.repositoryUrl) {
|
|
1762
|
+
const url = escapeXmlAttribute(input.repositoryUrl);
|
|
1763
|
+
const text = escapeHtml(input.repositoryUrl);
|
|
1764
|
+
lines.push(
|
|
1765
|
+
`<p><em>This documentation subtree is synced from <a href="${url}">${text}</a> and maintained by <code>repo-toolkit-confluence</code>.</em></p>`
|
|
1766
|
+
);
|
|
1767
|
+
} else {
|
|
1768
|
+
lines.push("<p><em>This documentation subtree is maintained by <code>repo-toolkit-confluence</code>.</em></p>");
|
|
1769
|
+
}
|
|
1770
|
+
lines.push("<h3>Statistics</h3>");
|
|
1771
|
+
lines.push("<ul>");
|
|
1772
|
+
lines.push(`<li>Markdown pages: ${input.stats.markdownPages}</li>`);
|
|
1773
|
+
lines.push(`<li>Directory pages: ${input.stats.directoryPages}</li>`);
|
|
1774
|
+
lines.push(`<li>Total managed pages: ${input.stats.totalPages}</li>`);
|
|
1775
|
+
lines.push(`<li>Maximum depth: ${input.stats.maxDepth}</li>`);
|
|
1776
|
+
lines.push(`<li>Attachment references: ${input.stats.attachmentReferences}</li>`);
|
|
1777
|
+
lines.push(`<li>Mermaid blocks: ${input.stats.mermaidBlocks}</li>`);
|
|
1778
|
+
lines.push("</ul>");
|
|
1779
|
+
lines.push("<h3>Pages</h3>");
|
|
1780
|
+
if (input.pages.length === 0) {
|
|
1781
|
+
lines.push("<p>No managed child pages</p>");
|
|
1782
|
+
} else {
|
|
1783
|
+
lines.push(renderTree(input.pages));
|
|
1784
|
+
}
|
|
1785
|
+
lines.push("<h3>Ownership</h3>");
|
|
1786
|
+
lines.push(
|
|
1787
|
+
`<p>All generated pages carry the <code>${CONFLUENCE_MANAGED_LABEL}</code> label. On default sync, stale labeled pages not in the local tree are pruned; unlabeled pages are preserved. Use <code>clean: true</code> to move all safely deletable page descendants to trash before recreation.</p>`
|
|
1788
|
+
);
|
|
1789
|
+
lines.push(PARENT_SUMMARY_END_MARKER);
|
|
1790
|
+
return lines.join("\n");
|
|
1791
|
+
}
|
|
1792
|
+
function renderTree(pages) {
|
|
1793
|
+
const sorted = [...pages].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1794
|
+
const root = { children: /* @__PURE__ */ new Map(), key: "" };
|
|
1795
|
+
for (const page of sorted) {
|
|
1796
|
+
const parts = page.relativePath.split("/");
|
|
1797
|
+
let node = root;
|
|
1798
|
+
let currentPath = "";
|
|
1799
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
1800
|
+
const part = parts[i] ?? "";
|
|
1801
|
+
currentPath = currentPath ? currentPath + "/" + part : part;
|
|
1802
|
+
let child = node.children.get(part);
|
|
1803
|
+
if (!child) {
|
|
1804
|
+
child = { children: /* @__PURE__ */ new Map(), key: part };
|
|
1805
|
+
node.children.set(part, child);
|
|
1806
|
+
}
|
|
1807
|
+
if (i === parts.length - 1) {
|
|
1808
|
+
child.record = page;
|
|
1809
|
+
}
|
|
1810
|
+
node = child;
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
const renderNode = (node) => {
|
|
1814
|
+
const entries = [...node.children.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1815
|
+
if (entries.length === 0) {
|
|
1816
|
+
return "";
|
|
1817
|
+
}
|
|
1818
|
+
let html = "<ul>";
|
|
1819
|
+
for (const [, child] of entries) {
|
|
1820
|
+
const rec = child.record;
|
|
1821
|
+
if (rec) {
|
|
1822
|
+
const link = pageLink(rec.pageId, rec.title);
|
|
1823
|
+
const kindLabel = rec.kind === "directory" ? "directory" : "page";
|
|
1824
|
+
const pathCode = `<code>${escapeHtml(rec.relativePath)}</code>`;
|
|
1825
|
+
const childrenHtml = renderNode(child);
|
|
1826
|
+
html += `<li>${link} \u2014 ${pathCode} <em>(${kindLabel})</em>${childrenHtml}</li>`;
|
|
1827
|
+
} else {
|
|
1828
|
+
const childrenHtml = renderNode(child);
|
|
1829
|
+
html += `<li>${escapeHtml(child.key)}${childrenHtml}</li>`;
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
html += "</ul>";
|
|
1833
|
+
return html;
|
|
1834
|
+
};
|
|
1835
|
+
return renderNode(root);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1606
1838
|
// src/index.ts
|
|
1607
1839
|
var LocalSyncValidationAggregateError = class extends Error {
|
|
1608
1840
|
constructor(defects) {
|
|
@@ -1631,6 +1863,7 @@ function validateLocalSync(entries, plan) {
|
|
|
1631
1863
|
}) : [];
|
|
1632
1864
|
plans.push({
|
|
1633
1865
|
entry,
|
|
1866
|
+
title: pageTitleFromSegments(entry.segments, plan.pageTitleStrategy),
|
|
1634
1867
|
html: body,
|
|
1635
1868
|
mermaidBlocks,
|
|
1636
1869
|
markdownDir,
|
|
@@ -1659,7 +1892,131 @@ var SyncMutationError = class extends Error {
|
|
|
1659
1892
|
this.unprocessed = input.unprocessed;
|
|
1660
1893
|
}
|
|
1661
1894
|
};
|
|
1895
|
+
var ReconciliationError = class extends Error {
|
|
1896
|
+
constructor(input) {
|
|
1897
|
+
super(input.failure.error.message);
|
|
1898
|
+
this.name = "ReconciliationError";
|
|
1899
|
+
this.phase = input.phase;
|
|
1900
|
+
this.completed = input.completed;
|
|
1901
|
+
this.failure = input.failure;
|
|
1902
|
+
this.unprocessed = input.unprocessed;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
var ParentSummaryError = class extends Error {
|
|
1906
|
+
constructor(input) {
|
|
1907
|
+
super(input.failure.error.message);
|
|
1908
|
+
this.name = "ParentSummaryError";
|
|
1909
|
+
this.phase = "parent-summary";
|
|
1910
|
+
this.changes = input.changes;
|
|
1911
|
+
this.labelsAdded = input.labelsAdded;
|
|
1912
|
+
this.cleanDeletions = input.cleanDeletions;
|
|
1913
|
+
this.pruneDeletions = input.pruneDeletions;
|
|
1914
|
+
this.blocked = input.blocked;
|
|
1915
|
+
this.failure = input.failure;
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
function planStalePruning(input) {
|
|
1919
|
+
const nodes = buildInventoryNodes(input.parentPageId, input.inventory);
|
|
1920
|
+
const retained = /* @__PURE__ */ new Set();
|
|
1921
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1922
|
+
const isStale = (entry) => entry.type === "page" && entry.labeled && !input.expectedIds.has(entry.id) && entry.id !== input.parentPageId;
|
|
1923
|
+
const visit = (node) => {
|
|
1924
|
+
if (visited.has(node.entry.id)) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
visited.add(node.entry.id);
|
|
1928
|
+
let safe = isStale(node.entry);
|
|
1929
|
+
for (const child of node.children) {
|
|
1930
|
+
visit(child);
|
|
1931
|
+
if (retained.has(child.entry.id)) {
|
|
1932
|
+
safe = false;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (!safe) {
|
|
1936
|
+
retained.add(node.entry.id);
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
for (const node of nodes.values()) {
|
|
1940
|
+
visit(node);
|
|
1941
|
+
}
|
|
1942
|
+
const deletions = deepestFirst([...nodes.values()].filter((n) => !retained.has(n.entry.id)));
|
|
1943
|
+
const blocked = [...nodes.values()].filter((n) => retained.has(n.entry.id) && isStale(n.entry)).map((n) => n.entry.id).sort();
|
|
1944
|
+
return { deletions, blocked };
|
|
1945
|
+
}
|
|
1946
|
+
function planCleanDeletions(input) {
|
|
1947
|
+
const nodes = buildInventoryNodes(input.parentPageId, input.inventory);
|
|
1948
|
+
for (const node of nodes.values()) {
|
|
1949
|
+
if (node.entry.type !== "page") {
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`clean refused: descendant ${node.entry.id} has unsupported type "${node.entry.type}"; deleting its ancestors could remove content that cannot be restored by this tool`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return { deletions: deepestFirst([...nodes.values()]), blocked: [] };
|
|
1956
|
+
}
|
|
1957
|
+
function buildInventoryNodes(parentPageId, inventory) {
|
|
1958
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1959
|
+
for (const entry of inventory) {
|
|
1960
|
+
if (entry.id === parentPageId) {
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
if (nodes.has(entry.id)) {
|
|
1964
|
+
throw new Error(`Incomplete descendant inventory: duplicate id ${entry.id}`);
|
|
1965
|
+
}
|
|
1966
|
+
nodes.set(entry.id, { entry, depth: -1, children: [] });
|
|
1967
|
+
}
|
|
1968
|
+
const depthOf = (node) => {
|
|
1969
|
+
if (node.depth >= 0) {
|
|
1970
|
+
return node.depth;
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof node.entry.depth === "number") {
|
|
1973
|
+
node.depth = node.entry.depth;
|
|
1974
|
+
return node.depth;
|
|
1975
|
+
}
|
|
1976
|
+
let depth = 0;
|
|
1977
|
+
let current = node;
|
|
1978
|
+
const seen = /* @__PURE__ */ new Set([node.entry.id]);
|
|
1979
|
+
while (true) {
|
|
1980
|
+
const pid = current.entry.parentId;
|
|
1981
|
+
if (pid === parentPageId) {
|
|
1982
|
+
depth += 1;
|
|
1983
|
+
node.depth = depth;
|
|
1984
|
+
return depth;
|
|
1985
|
+
}
|
|
1986
|
+
if (!pid) {
|
|
1987
|
+
throw new Error(`Incomplete descendant inventory: missing parent for ${node.entry.id}`);
|
|
1988
|
+
}
|
|
1989
|
+
if (seen.has(pid)) {
|
|
1990
|
+
throw new Error(`Incomplete descendant inventory: parent cycle at ${pid}`);
|
|
1991
|
+
}
|
|
1992
|
+
seen.add(pid);
|
|
1993
|
+
const parent = nodes.get(pid);
|
|
1994
|
+
if (!parent) {
|
|
1995
|
+
throw new Error(
|
|
1996
|
+
`Incomplete descendant inventory: ancestor ${pid} of ${node.entry.id} is missing from the listing`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
depth += 1;
|
|
2000
|
+
current = parent;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
for (const node of nodes.values()) {
|
|
2004
|
+
depthOf(node);
|
|
2005
|
+
const pid = node.entry.parentId;
|
|
2006
|
+
if (pid !== void 0 && pid !== parentPageId) {
|
|
2007
|
+
nodes.get(pid)?.children.push(node);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
return nodes;
|
|
2011
|
+
}
|
|
2012
|
+
function deepestFirst(nodes) {
|
|
2013
|
+
return [...nodes].sort((a, b) => b.depth - a.depth !== 0 ? b.depth - a.depth : a.entry.id.localeCompare(b.entry.id)).map((n) => n.entry.id);
|
|
2014
|
+
}
|
|
2015
|
+
function hasManagedMarker(labels) {
|
|
2016
|
+
return labels.some((label) => label.name === CONFLUENCE_MANAGED_LABEL && label.prefix === "global");
|
|
2017
|
+
}
|
|
1662
2018
|
function resolveConfluenceSyncPlan(options = {}) {
|
|
2019
|
+
const pageTitleStrategy = resolvePageTitleStrategy(options.pageTitleStrategy);
|
|
1663
2020
|
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
1664
2021
|
const folder = resolveInputPath(cwd, options.folder ?? "");
|
|
1665
2022
|
if (!options.folder) {
|
|
@@ -1706,7 +2063,10 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1706
2063
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1707
2064
|
dryRun: options.dryRun ?? false,
|
|
1708
2065
|
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1709
|
-
repositoryUrl
|
|
2066
|
+
repositoryUrl,
|
|
2067
|
+
clean: options.clean ?? false,
|
|
2068
|
+
updateParentPage: options.updateParentPage ?? true,
|
|
2069
|
+
pageTitleStrategy
|
|
1710
2070
|
};
|
|
1711
2071
|
}
|
|
1712
2072
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1715,19 +2075,44 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1715
2075
|
const tree = await readDocTree(plan.folder);
|
|
1716
2076
|
if (tree.entries.length === 0) {
|
|
1717
2077
|
log(`No markdown files found under ${plan.folder}`);
|
|
1718
|
-
return;
|
|
1719
2078
|
}
|
|
1720
|
-
validateLocalHierarchy(tree.entries);
|
|
1721
2079
|
const localPlan = validateLocalSync(tree.entries, plan);
|
|
2080
|
+
validateLocalHierarchy(localPlan.entries, plan.pageTitleStrategy);
|
|
1722
2081
|
if (plan.dryRun) {
|
|
1723
2082
|
log("[dry-run] Walking documentation tree only.");
|
|
1724
2083
|
for (const entryPlan of localPlan.entries) {
|
|
1725
2084
|
const attCount = entryPlan.attachments.length;
|
|
1726
2085
|
const mermaidCount = entryPlan.mermaidBlocks.length;
|
|
1727
2086
|
log(
|
|
1728
|
-
`[dry-run] would sync ${entryPlan.entry.segments.join("/")}` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
2087
|
+
`[dry-run] would sync ${entryPlan.entry.segments.join("/")} as "${entryPlan.title}"` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
1729
2088
|
);
|
|
1730
2089
|
}
|
|
2090
|
+
if (plan.clean) {
|
|
2091
|
+
log(
|
|
2092
|
+
"[dry-run] clean requested: a real sync would move every page descendant of the target page to trash before recreating the local hierarchy."
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
log(
|
|
2096
|
+
"[dry-run] a real sync would label every mapped page with the ownership marker and prune stale labeled descendants."
|
|
2097
|
+
);
|
|
2098
|
+
if (plan.updateParentPage) {
|
|
2099
|
+
const stats = computeDryRunStats(localPlan);
|
|
2100
|
+
log(
|
|
2101
|
+
`[dry-run] parent summary: Markdown pages: ${stats.markdownPages}, Directory pages: ${stats.directoryPages}, Total managed pages: ${stats.totalPages}, Maximum depth: ${stats.maxDepth}, Attachment references: ${stats.attachmentReferences}, Mermaid blocks: ${stats.mermaidBlocks}`
|
|
2102
|
+
);
|
|
2103
|
+
if (localPlan.entries.length === 0) {
|
|
2104
|
+
log("[dry-run] parent tree: No managed child pages");
|
|
2105
|
+
} else {
|
|
2106
|
+
for (const entryPlan of localPlan.entries) {
|
|
2107
|
+
const dirParts = entryPlan.entry.segments.slice(0, -1);
|
|
2108
|
+
for (let i = 0; i < dirParts.length; i += 1) {
|
|
2109
|
+
const dirPath = dirParts.slice(0, i + 1).join("/");
|
|
2110
|
+
log(`[dry-run] parent tree: ${dirPath} (directory) => "${dirParts[i] ?? ""}"`);
|
|
2111
|
+
}
|
|
2112
|
+
log(`[dry-run] parent tree: ${entryPlan.entry.segments.join("/")} (page) => "${entryPlan.title}"`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
1731
2116
|
return;
|
|
1732
2117
|
}
|
|
1733
2118
|
const client = options.client ?? new ConfluenceClient({
|
|
@@ -1735,13 +2120,29 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1735
2120
|
username: plan.username,
|
|
1736
2121
|
apiToken: plan.apiToken
|
|
1737
2122
|
});
|
|
2123
|
+
const labelsAdded = [];
|
|
2124
|
+
const cleanDeletions = [];
|
|
2125
|
+
const pruneDeletions = [];
|
|
2126
|
+
const blocked = [];
|
|
2127
|
+
if (plan.clean) {
|
|
2128
|
+
const descendants = await client.getPageDescendants(plan.parentPageId);
|
|
2129
|
+
const inventory = descendants.filter((d) => d.id !== plan.parentPageId).map((d) => toInventoryEntry(d, false));
|
|
2130
|
+
const cleanPlan = planCleanDeletions({ parentPageId: plan.parentPageId, inventory });
|
|
2131
|
+
await executeDeletions(cleanPlan.deletions, "clean", client, log, cleanDeletions);
|
|
2132
|
+
}
|
|
1738
2133
|
const spaceId = await client.getSpaceIdByKey(plan.spaceKey);
|
|
1739
2134
|
const cache = new PageTitleCache(spaceId, client);
|
|
2135
|
+
const syncState = {
|
|
2136
|
+
mappedIds: /* @__PURE__ */ new Set(),
|
|
2137
|
+
ensuredLabels: /* @__PURE__ */ new Set(),
|
|
2138
|
+
labelsAdded,
|
|
2139
|
+
mappedRecords: /* @__PURE__ */ new Map()
|
|
2140
|
+
};
|
|
1740
2141
|
const changes = [];
|
|
1741
2142
|
for (let i = 0; i < localPlan.entries.length; i += 1) {
|
|
1742
2143
|
const entryPlan = localPlan.entries[i];
|
|
1743
2144
|
try {
|
|
1744
|
-
await syncEntry(entryPlan, plan, client, cache, log, changes);
|
|
2145
|
+
await syncEntry(entryPlan, plan, client, cache, log, changes, syncState);
|
|
1745
2146
|
} catch (error) {
|
|
1746
2147
|
throw new SyncMutationError({
|
|
1747
2148
|
changes,
|
|
@@ -1750,20 +2151,207 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1750
2151
|
});
|
|
1751
2152
|
}
|
|
1752
2153
|
}
|
|
1753
|
-
|
|
2154
|
+
if (!plan.clean) {
|
|
2155
|
+
const descendants = await client.getPageDescendants(plan.parentPageId);
|
|
2156
|
+
const inventory = [];
|
|
2157
|
+
for (const d of descendants) {
|
|
2158
|
+
if (d.id === plan.parentPageId) {
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (d.type !== "page") {
|
|
2162
|
+
inventory.push(toInventoryEntry(d, false));
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2165
|
+
if (syncState.mappedIds.has(d.id)) {
|
|
2166
|
+
inventory.push(toInventoryEntry(d, true));
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
const labels = await client.getPageLabels(d.id);
|
|
2170
|
+
inventory.push(toInventoryEntry(d, hasManagedMarker(labels)));
|
|
2171
|
+
}
|
|
2172
|
+
const prunePlan = planStalePruning({
|
|
2173
|
+
parentPageId: plan.parentPageId,
|
|
2174
|
+
expectedIds: syncState.mappedIds,
|
|
2175
|
+
inventory
|
|
2176
|
+
});
|
|
2177
|
+
await executeDeletions(prunePlan.deletions, "prune", client, log, pruneDeletions);
|
|
2178
|
+
for (const pageId of prunePlan.blocked) {
|
|
2179
|
+
blocked.push(pageId);
|
|
2180
|
+
log(`blocked: stale page ${pageId} retained because it has unlabeled, non-page, or expected descendants`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
let parentStatus = "skipped";
|
|
2184
|
+
if (plan.updateParentPage) {
|
|
2185
|
+
try {
|
|
2186
|
+
const parentPage = await client.getPage(plan.parentPageId);
|
|
2187
|
+
const stats = computeParentStats(localPlan, syncState);
|
|
2188
|
+
const pages = [...syncState.mappedRecords.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2189
|
+
const region = renderParentSummary({ repositoryUrl: plan.repositoryUrl, stats, pages });
|
|
2190
|
+
const currentBody = parentPage.body?.storage?.value ?? "";
|
|
2191
|
+
const merged = mergeParentSummaryBody(currentBody, region);
|
|
2192
|
+
if (merged === currentBody) {
|
|
2193
|
+
log(`parent-unchanged: page ${plan.parentPageId}`);
|
|
2194
|
+
parentStatus = "unchanged";
|
|
2195
|
+
} else {
|
|
2196
|
+
await client.updatePage({
|
|
2197
|
+
id: plan.parentPageId,
|
|
2198
|
+
title: parentPage.title,
|
|
2199
|
+
body: { representation: "storage", value: merged },
|
|
2200
|
+
version: { number: (parentPage.version?.number ?? 0) + 1, message: plan.versionMessage }
|
|
2201
|
+
});
|
|
2202
|
+
log(`parent-updated: page ${plan.parentPageId}`);
|
|
2203
|
+
parentStatus = "updated";
|
|
2204
|
+
}
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
throw new ParentSummaryError({
|
|
2207
|
+
changes,
|
|
2208
|
+
labelsAdded: [...labelsAdded],
|
|
2209
|
+
cleanDeletions: [...cleanDeletions],
|
|
2210
|
+
pruneDeletions: [...pruneDeletions],
|
|
2211
|
+
blocked: [...blocked],
|
|
2212
|
+
failure: { pageId: plan.parentPageId, error: error instanceof Error ? error : new Error(String(error)) }
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return { changes, labelsAdded, cleanDeletions, pruneDeletions, blocked, parentStatus };
|
|
2217
|
+
}
|
|
2218
|
+
function computeParentStats(localPlan, state) {
|
|
2219
|
+
const dirSet = /* @__PURE__ */ new Set();
|
|
2220
|
+
let maxDepth = 0;
|
|
2221
|
+
let attachmentReferences = 0;
|
|
2222
|
+
let mermaidBlocks = 0;
|
|
2223
|
+
for (const entryPlan of localPlan.entries) {
|
|
2224
|
+
const segs = entryPlan.entry.segments;
|
|
2225
|
+
if (segs.length > maxDepth) {
|
|
2226
|
+
maxDepth = segs.length;
|
|
2227
|
+
}
|
|
2228
|
+
for (let i = 0; i < segs.length - 1; i += 1) {
|
|
2229
|
+
dirSet.add(segs.slice(0, i + 1).join("/"));
|
|
2230
|
+
}
|
|
2231
|
+
attachmentReferences += entryPlan.attachments.length;
|
|
2232
|
+
mermaidBlocks += entryPlan.mermaidBlocks.length;
|
|
2233
|
+
}
|
|
2234
|
+
const markdownPages = localPlan.entries.length;
|
|
2235
|
+
const directoryPages = dirSet.size;
|
|
2236
|
+
const totalPages = state.mappedRecords.size;
|
|
2237
|
+
const effectiveMaxDepth = localPlan.entries.length === 0 ? 0 : maxDepth;
|
|
2238
|
+
return {
|
|
2239
|
+
markdownPages,
|
|
2240
|
+
directoryPages,
|
|
2241
|
+
totalPages: totalPages > 0 ? totalPages : directoryPages + markdownPages,
|
|
2242
|
+
maxDepth: effectiveMaxDepth,
|
|
2243
|
+
attachmentReferences,
|
|
2244
|
+
mermaidBlocks
|
|
2245
|
+
};
|
|
1754
2246
|
}
|
|
1755
|
-
|
|
2247
|
+
function computeDryRunStats(localPlan) {
|
|
2248
|
+
const dirSet = /* @__PURE__ */ new Set();
|
|
2249
|
+
let maxDepth = 0;
|
|
2250
|
+
let attachmentReferences = 0;
|
|
2251
|
+
let mermaidBlocks = 0;
|
|
2252
|
+
for (const entryPlan of localPlan.entries) {
|
|
2253
|
+
const segs = entryPlan.entry.segments;
|
|
2254
|
+
if (segs.length > maxDepth) {
|
|
2255
|
+
maxDepth = segs.length;
|
|
2256
|
+
}
|
|
2257
|
+
for (let i = 0; i < segs.length - 1; i += 1) {
|
|
2258
|
+
dirSet.add(segs.slice(0, i + 1).join("/"));
|
|
2259
|
+
}
|
|
2260
|
+
attachmentReferences += entryPlan.attachments.length;
|
|
2261
|
+
mermaidBlocks += entryPlan.mermaidBlocks.length;
|
|
2262
|
+
}
|
|
2263
|
+
const markdownPages = localPlan.entries.length;
|
|
2264
|
+
const directoryPages = dirSet.size;
|
|
2265
|
+
return {
|
|
2266
|
+
markdownPages,
|
|
2267
|
+
directoryPages,
|
|
2268
|
+
totalPages: directoryPages + markdownPages,
|
|
2269
|
+
maxDepth: markdownPages === 0 ? 0 : maxDepth,
|
|
2270
|
+
attachmentReferences,
|
|
2271
|
+
mermaidBlocks
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function toInventoryEntry(d, labeled) {
|
|
2275
|
+
const entry = { id: d.id, type: d.type, labeled };
|
|
2276
|
+
if (d.parentId !== void 0) {
|
|
2277
|
+
entry.parentId = d.parentId;
|
|
2278
|
+
}
|
|
2279
|
+
if (d.depth !== void 0) {
|
|
2280
|
+
entry.depth = d.depth;
|
|
2281
|
+
}
|
|
2282
|
+
if (d.title !== void 0) {
|
|
2283
|
+
entry.title = d.title;
|
|
2284
|
+
}
|
|
2285
|
+
return entry;
|
|
2286
|
+
}
|
|
2287
|
+
async function ensureManagedLabel(pageId, client, state, log) {
|
|
2288
|
+
if (state.ensuredLabels.has(pageId)) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
const labels = await client.getPageLabels(pageId);
|
|
2292
|
+
if (!hasManagedMarker(labels)) {
|
|
2293
|
+
await client.addManagedLabel(pageId);
|
|
2294
|
+
state.labelsAdded.push(pageId);
|
|
2295
|
+
log(`labeled: page ${pageId}`);
|
|
2296
|
+
}
|
|
2297
|
+
state.ensuredLabels.add(pageId);
|
|
2298
|
+
}
|
|
2299
|
+
async function executeDeletions(ids, phase, client, log, evidence) {
|
|
2300
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
2301
|
+
const pageId = ids[i];
|
|
2302
|
+
if (pageId === void 0) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
try {
|
|
2306
|
+
await client.deletePage(pageId);
|
|
2307
|
+
evidence.push(pageId);
|
|
2308
|
+
log(`${phase === "clean" ? "clean" : "pruned"}: trashed page ${pageId}`);
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
throw new ReconciliationError({
|
|
2311
|
+
phase,
|
|
2312
|
+
completed: [...evidence],
|
|
2313
|
+
failure: { pageId, error: error instanceof Error ? error : new Error(String(error)) },
|
|
2314
|
+
unprocessed: ids.slice(i + 1)
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
async function syncEntry(entryPlan, plan, client, cache, log, changes, state) {
|
|
1756
2320
|
const { entry, html: precomputedHtml, mermaidBlocks, markdownDir, hasLocalImages, hasMermaidBlocks } = entryPlan;
|
|
1757
2321
|
const segments = entry.segments;
|
|
1758
2322
|
if (segments.length === 0) {
|
|
1759
2323
|
return;
|
|
1760
2324
|
}
|
|
1761
2325
|
let currentParentId = plan.parentPageId;
|
|
2326
|
+
const recordDirectory = (relativePath, title, pageId, depth) => {
|
|
2327
|
+
if (!state.mappedRecords.has(relativePath)) {
|
|
2328
|
+
state.mappedRecords.set(relativePath, {
|
|
2329
|
+
relativePath,
|
|
2330
|
+
kind: "directory",
|
|
2331
|
+
title,
|
|
2332
|
+
pageId,
|
|
2333
|
+
depth,
|
|
2334
|
+
attachmentCount: 0,
|
|
2335
|
+
mermaidCount: 0
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
const recordLeaf = (relativePath, title, pageId, depth) => {
|
|
2340
|
+
state.mappedRecords.set(relativePath, {
|
|
2341
|
+
relativePath,
|
|
2342
|
+
kind: "leaf",
|
|
2343
|
+
title,
|
|
2344
|
+
pageId,
|
|
2345
|
+
depth,
|
|
2346
|
+
attachmentCount: entryPlan.attachments.length,
|
|
2347
|
+
mermaidCount: entryPlan.mermaidBlocks.length
|
|
2348
|
+
});
|
|
2349
|
+
};
|
|
1762
2350
|
for (let idx = 0; idx < segments.length; idx += 1) {
|
|
1763
2351
|
const isLast = idx === segments.length - 1;
|
|
1764
2352
|
const segment = segments[idx] ?? "";
|
|
1765
2353
|
if (isLast && isMarkdownName(segment)) {
|
|
1766
|
-
const title =
|
|
2354
|
+
const title = entryPlan.title;
|
|
1767
2355
|
const leafNeedsUploads = hasLocalImages || hasMermaidBlocks;
|
|
1768
2356
|
const existing = await cache.find(title, currentParentId);
|
|
1769
2357
|
if (!existing && !leafNeedsUploads) {
|
|
@@ -1772,12 +2360,18 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1772
2360
|
parentId: currentParentId,
|
|
1773
2361
|
body: { representation: "storage", value: precomputedHtml }
|
|
1774
2362
|
});
|
|
2363
|
+
state.mappedIds.add(pageId2);
|
|
2364
|
+
await ensureManagedLabel(pageId2, client, state, log);
|
|
2365
|
+
recordLeaf(segments.join("/"), title, pageId2, segments.length);
|
|
1775
2366
|
log(`created: ${segments.join("/")} (page ${pageId2})`);
|
|
1776
2367
|
changes.push({ entry, pageId: pageId2, kind: "created" });
|
|
1777
2368
|
return;
|
|
1778
2369
|
}
|
|
1779
2370
|
const existingPage = existing ?? await cache.findOrCreate(title, currentParentId);
|
|
1780
2371
|
const pageId = existingPage.id;
|
|
2372
|
+
state.mappedIds.add(pageId);
|
|
2373
|
+
await ensureManagedLabel(pageId, client, state, log);
|
|
2374
|
+
recordLeaf(segments.join("/"), title, pageId, segments.length);
|
|
1781
2375
|
const current = await client.getPage(pageId);
|
|
1782
2376
|
const currentBody = current.body?.storage?.value ?? "";
|
|
1783
2377
|
let body = precomputedHtml;
|
|
@@ -1822,12 +2416,18 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1822
2416
|
return;
|
|
1823
2417
|
}
|
|
1824
2418
|
if (isMarkdownName(segment)) {
|
|
1825
|
-
const title =
|
|
2419
|
+
const title = pageTitleFromSegments(segments.slice(0, idx + 1), plan.pageTitleStrategy);
|
|
1826
2420
|
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
2421
|
+
state.mappedIds.add(page2.id);
|
|
2422
|
+
await ensureManagedLabel(page2.id, client, state, log);
|
|
2423
|
+
recordDirectory(segments.slice(0, idx + 1).join("/"), title, page2.id, idx + 1);
|
|
1827
2424
|
currentParentId = page2.id;
|
|
1828
2425
|
continue;
|
|
1829
2426
|
}
|
|
1830
2427
|
const page = await cache.findOrCreate(segment, currentParentId);
|
|
2428
|
+
state.mappedIds.add(page.id);
|
|
2429
|
+
await ensureManagedLabel(page.id, client, state, log);
|
|
2430
|
+
recordDirectory(segments.slice(0, idx + 1).join("/"), segment, page.id, idx + 1);
|
|
1831
2431
|
currentParentId = page.id;
|
|
1832
2432
|
}
|
|
1833
2433
|
}
|
|
@@ -1953,14 +2553,15 @@ async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
|
1953
2553
|
}
|
|
1954
2554
|
return predicted;
|
|
1955
2555
|
}
|
|
1956
|
-
function validateLocalHierarchy(entries) {
|
|
2556
|
+
function validateLocalHierarchy(entries, strategy) {
|
|
1957
2557
|
const seen = /* @__PURE__ */ new Map();
|
|
1958
|
-
for (const
|
|
2558
|
+
for (const entryPlan of entries) {
|
|
2559
|
+
const { entry } = entryPlan;
|
|
1959
2560
|
let parentKey = "";
|
|
1960
2561
|
for (let index = 0; index < entry.segments.length; index += 1) {
|
|
1961
2562
|
const segment = entry.segments[index] ?? "";
|
|
1962
2563
|
const isLast = index === entry.segments.length - 1;
|
|
1963
|
-
const title = isMarkdownName(segment) ?
|
|
2564
|
+
const title = isMarkdownName(segment) ? isLast ? entryPlan.title : pageTitleFromSegments(entry.segments.slice(0, index + 1), strategy) : segment;
|
|
1964
2565
|
const kind = isLast && isMarkdownName(segment) ? "file" : "dir";
|
|
1965
2566
|
const key = `${parentKey}::${title}`;
|
|
1966
2567
|
const existing = seen.get(key);
|
|
@@ -1986,14 +2587,17 @@ var SPECS = [
|
|
|
1986
2587
|
{ name: "parent-page-id" },
|
|
1987
2588
|
{ name: "version-message" },
|
|
1988
2589
|
{ name: "repository-url" },
|
|
2590
|
+
{ name: "page-title-strategy" },
|
|
1989
2591
|
{ name: "skip-unchanged", boolean: true, negatable: true },
|
|
1990
2592
|
{ name: "dry-run", boolean: true },
|
|
1991
2593
|
{ name: "render-html-blocks", boolean: true },
|
|
2594
|
+
{ name: "clean", boolean: true },
|
|
2595
|
+
{ name: "update-parent-page", boolean: true, negatable: true },
|
|
1992
2596
|
INTERACTIVE_FLAG
|
|
1993
2597
|
];
|
|
1994
2598
|
var ENV_TRUTHY = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
|
|
1995
2599
|
var ENV_FALSY = /* @__PURE__ */ new Set(["false", "0", "no", "off", ""]);
|
|
1996
|
-
var BOOLEAN_ENV_KEYS = /* @__PURE__ */ new Set(["skipUnchanged", "dryRun", "renderHtmlBlocks"]);
|
|
2600
|
+
var BOOLEAN_ENV_KEYS = /* @__PURE__ */ new Set(["skipUnchanged", "dryRun", "renderHtmlBlocks", "clean", "updateParentPage"]);
|
|
1997
2601
|
function isBooleanOption(key) {
|
|
1998
2602
|
return BOOLEAN_ENV_KEYS.has(key);
|
|
1999
2603
|
}
|
|
@@ -2031,9 +2635,12 @@ Environment variables (CLI form; GitHub Action INPUT_* form is also read):
|
|
|
2031
2635
|
CONFLUENCE_PARENT_PAGE_ID Numeric parent page id
|
|
2032
2636
|
CONFLUENCE_VERSION_MESSAGE Version-message suffix for every PUT
|
|
2033
2637
|
CONFLUENCE_REPOSITORY_URL Repository URL appended to synced pages
|
|
2638
|
+
CONFLUENCE_PAGE_TITLE_STRATEGY Leaf page title strategy (default: filename-stem)
|
|
2034
2639
|
CONFLUENCE_SKIP_UNCHANGED true|false (default: true)
|
|
2035
2640
|
CONFLUENCE_DRY_RUN true|false (default: false)
|
|
2036
2641
|
CONFLUENCE_RENDER_HTML_BLOCKS true|false (default: false)
|
|
2642
|
+
CONFLUENCE_CLEAN true|false (default: false)
|
|
2643
|
+
CONFLUENCE_UPDATE_PARENT_PAGE true|false (default: true)
|
|
2037
2644
|
INPUT_<UPPER-FLAG> GitHub Actions input form (lower precedence)
|
|
2038
2645
|
|
|
2039
2646
|
Note: prefer CONFLUENCE_API_TOKEN_FILE or CONFLUENCE_API_TOKEN over
|
|
@@ -2051,11 +2658,18 @@ Options:
|
|
|
2051
2658
|
--parent-page-id <id> Numeric page id under which docs will be published (required)
|
|
2052
2659
|
--version-message <text> Commit message appended to every page/attachment PUT
|
|
2053
2660
|
--repository-url <url> Repository URL appended to synced pages as an italic notice
|
|
2661
|
+
--page-title-strategy <value> Leaf page title strategy: filename-stem (default), filename,
|
|
2662
|
+
sentence-case-parent, sentence-case-parents, sentence-case-path
|
|
2054
2663
|
--skip-unchanged Skip pages whose body is unchanged (default: true)
|
|
2055
2664
|
--no-skip-unchanged Re-upload every page even when unchanged
|
|
2056
2665
|
--dry-run Walk the doc tree and print the plan without API calls
|
|
2057
2666
|
--render-html-blocks Render \`\`\`html fenced blocks as inline HTML via the
|
|
2058
2667
|
Confluence html macro (default: false; emits as code box)
|
|
2668
|
+
--clean Move all page descendants to trash before recreation (default: false).
|
|
2669
|
+
WARNING: destructive \u2014 all page descendants, including manual/unlabeled
|
|
2670
|
+
pages, are moved to trash; parentPageId is retained.
|
|
2671
|
+
--update-parent-page Update parent page summary region (default: true)
|
|
2672
|
+
--no-update-parent-page Do not update parent page summary
|
|
2059
2673
|
-i, --interactive Prompt interactively for missing non-secret required values
|
|
2060
2674
|
-h, --help Show this help message
|
|
2061
2675
|
`);
|
|
@@ -2092,9 +2706,12 @@ var ENV_BINDINGS = [
|
|
|
2092
2706
|
{ envName: "INPUT_PARENT-PAGE-ID", key: "parentPageId", kind: "string" },
|
|
2093
2707
|
{ envName: "INPUT_VERSION-MESSAGE", key: "versionMessage", kind: "string" },
|
|
2094
2708
|
{ envName: "INPUT_REPOSITORY-URL", key: "repositoryUrl", kind: "string" },
|
|
2709
|
+
{ envName: "INPUT_PAGE-TITLE-STRATEGY", key: "pageTitleStrategy", kind: "string" },
|
|
2095
2710
|
{ envName: "INPUT_DRY-RUN", key: "dryRun", kind: "boolean" },
|
|
2096
2711
|
{ envName: "INPUT_SKIP-UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
2097
2712
|
{ envName: "INPUT_RENDER-HTML-BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
|
|
2713
|
+
{ envName: "INPUT_CLEAN", key: "clean", kind: "boolean" },
|
|
2714
|
+
{ envName: "INPUT_UPDATE-PARENT-PAGE", key: "updateParentPage", kind: "boolean" },
|
|
2098
2715
|
{ envName: "CONFLUENCE_FOLDER", key: "folder", kind: "string" },
|
|
2099
2716
|
{ envName: "CONFLUENCE_USERNAME", key: "username", kind: "string" },
|
|
2100
2717
|
{ envName: "CONFLUENCE_API_TOKEN", key: "apiToken", kind: "string" },
|
|
@@ -2104,9 +2721,12 @@ var ENV_BINDINGS = [
|
|
|
2104
2721
|
{ envName: "CONFLUENCE_PARENT_PAGE_ID", key: "parentPageId", kind: "string" },
|
|
2105
2722
|
{ envName: "CONFLUENCE_VERSION_MESSAGE", key: "versionMessage", kind: "string" },
|
|
2106
2723
|
{ envName: "CONFLUENCE_REPOSITORY_URL", key: "repositoryUrl", kind: "string" },
|
|
2724
|
+
{ envName: "CONFLUENCE_PAGE_TITLE_STRATEGY", key: "pageTitleStrategy", kind: "string" },
|
|
2107
2725
|
{ envName: "CONFLUENCE_DRY_RUN", key: "dryRun", kind: "boolean" },
|
|
2108
2726
|
{ envName: "CONFLUENCE_SKIP_UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
2109
|
-
{ envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" }
|
|
2727
|
+
{ envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
|
|
2728
|
+
{ envName: "CONFLUENCE_CLEAN", key: "clean", kind: "boolean" },
|
|
2729
|
+
{ envName: "CONFLUENCE_UPDATE_PARENT_PAGE", key: "updateParentPage", kind: "boolean" }
|
|
2110
2730
|
];
|
|
2111
2731
|
function optionsFromEnv(env = process.env) {
|
|
2112
2732
|
const options = {};
|
|
@@ -2116,6 +2736,10 @@ function optionsFromEnv(env = process.env) {
|
|
|
2116
2736
|
continue;
|
|
2117
2737
|
}
|
|
2118
2738
|
if (kind === "string") {
|
|
2739
|
+
if (key === "pageTitleStrategy") {
|
|
2740
|
+
options[key] = raw;
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2119
2743
|
if (!isStringOptionKey(key)) {
|
|
2120
2744
|
continue;
|
|
2121
2745
|
}
|
|
@@ -2156,6 +2780,9 @@ function buildOptions(result) {
|
|
|
2156
2780
|
setIfString(options, "parentPageId", values["parent-page-id"]);
|
|
2157
2781
|
setIfString(options, "versionMessage", values["version-message"]);
|
|
2158
2782
|
setIfString(options, "repositoryUrl", values["repository-url"]);
|
|
2783
|
+
if (values["page-title-strategy"] !== void 0) {
|
|
2784
|
+
options.pageTitleStrategy = values["page-title-strategy"];
|
|
2785
|
+
}
|
|
2159
2786
|
if (values["skip-unchanged"] !== void 0) {
|
|
2160
2787
|
options.skipUnchanged = values["skip-unchanged"] === "true";
|
|
2161
2788
|
}
|
|
@@ -2165,6 +2792,12 @@ function buildOptions(result) {
|
|
|
2165
2792
|
if (values["render-html-blocks"] !== void 0) {
|
|
2166
2793
|
options.renderHtmlBlocks = true;
|
|
2167
2794
|
}
|
|
2795
|
+
if (values["clean"] !== void 0) {
|
|
2796
|
+
options.clean = values["clean"] === "true";
|
|
2797
|
+
}
|
|
2798
|
+
if (values["update-parent-page"] !== void 0) {
|
|
2799
|
+
options.updateParentPage = values["update-parent-page"] === "true";
|
|
2800
|
+
}
|
|
2168
2801
|
return options;
|
|
2169
2802
|
}
|
|
2170
2803
|
async function resolveConfluenceOptions(args) {
|