@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/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var DEFAULT_USER_AGENT = "repo-toolkit-confluence/1.0 (+node)";
|
|
|
14
14
|
var MAX_LIMIT = 250;
|
|
15
15
|
var MAX_ERROR_BODY_LENGTH = 8192;
|
|
16
16
|
var MAX_PAGES_PER_QUERY = 100;
|
|
17
|
+
var CONFLUENCE_MANAGED_LABEL = "repo-toolkit-confluence";
|
|
17
18
|
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
18
19
|
var DEFAULT_MAX_RETRIES = 3;
|
|
19
20
|
var DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
|
|
@@ -141,6 +142,44 @@ var ConfluenceClient = class {
|
|
|
141
142
|
body: JSON.stringify(body)
|
|
142
143
|
});
|
|
143
144
|
}
|
|
145
|
+
async getPageDescendants(pageId) {
|
|
146
|
+
const query = new URLSearchParams({ limit: String(MAX_LIMIT) });
|
|
147
|
+
return this.listAll(this.v2Url(`/pages/${encodeURIComponent(pageId)}/descendants?${query.toString()}`));
|
|
148
|
+
}
|
|
149
|
+
async getPageLabels(pageId) {
|
|
150
|
+
const query = new URLSearchParams({ limit: String(MAX_LIMIT) });
|
|
151
|
+
return this.listAll(this.v2Url(`/pages/${encodeURIComponent(pageId)}/labels?${query.toString()}`));
|
|
152
|
+
}
|
|
153
|
+
async addManagedLabel(pageId) {
|
|
154
|
+
await this.requestJson(this.v1Url(`/content/${encodeURIComponent(pageId)}/label`), {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify([{ prefix: "global", name: CONFLUENCE_MANAGED_LABEL }])
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async deletePage(pageId) {
|
|
161
|
+
await this.requestJson(this.v2Url(`/pages/${encodeURIComponent(pageId)}`), { method: "DELETE" });
|
|
162
|
+
}
|
|
163
|
+
async listAll(startUrl) {
|
|
164
|
+
const results = [];
|
|
165
|
+
const visited = /* @__PURE__ */ new Set();
|
|
166
|
+
let pageCount = 0;
|
|
167
|
+
let nextUrl = startUrl;
|
|
168
|
+
while (nextUrl) {
|
|
169
|
+
pageCount += 1;
|
|
170
|
+
if (pageCount > MAX_PAGES_PER_QUERY) {
|
|
171
|
+
throw new ConfluenceApiError(`Pagination limit (${MAX_PAGES_PER_QUERY}) exceeded`, 0, nextUrl, "");
|
|
172
|
+
}
|
|
173
|
+
if (visited.has(nextUrl)) {
|
|
174
|
+
throw new ConfluenceApiError("Confluence pagination loop detected", 0, nextUrl, "");
|
|
175
|
+
}
|
|
176
|
+
visited.add(nextUrl);
|
|
177
|
+
const data = await this.requestJson(nextUrl, { method: "GET" });
|
|
178
|
+
results.push(...data.results);
|
|
179
|
+
nextUrl = resolveNextUrl(this.baseUrl, this.baseUrlOrigin, data._links?.next);
|
|
180
|
+
}
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
144
183
|
async getAttachments(pageId) {
|
|
145
184
|
const results = [];
|
|
146
185
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -574,6 +613,64 @@ function titleFromSegment(segment) {
|
|
|
574
613
|
}
|
|
575
614
|
return segment;
|
|
576
615
|
}
|
|
616
|
+
var PAGE_TITLE_STRATEGIES = [
|
|
617
|
+
"filename-stem",
|
|
618
|
+
"filename",
|
|
619
|
+
"sentence-case-parent",
|
|
620
|
+
"sentence-case-parents",
|
|
621
|
+
"sentence-case-path"
|
|
622
|
+
];
|
|
623
|
+
var DEFAULT_PAGE_TITLE_STRATEGY = "filename-stem";
|
|
624
|
+
function resolvePageTitleStrategy(value) {
|
|
625
|
+
if (value === void 0 || value === null) {
|
|
626
|
+
return DEFAULT_PAGE_TITLE_STRATEGY;
|
|
627
|
+
}
|
|
628
|
+
if (typeof value === "string" && PAGE_TITLE_STRATEGIES.includes(value)) {
|
|
629
|
+
return value;
|
|
630
|
+
}
|
|
631
|
+
throw new Error(
|
|
632
|
+
`Invalid pageTitleStrategy: expected one of ${PAGE_TITLE_STRATEGIES.join(", ")}, got ${JSON.stringify(value)}`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
function sentenceCaseStem(filename) {
|
|
636
|
+
let stem = filename;
|
|
637
|
+
if (/\.md$/i.test(stem)) {
|
|
638
|
+
stem = stem.slice(0, stem.length - MARKDOWN_EXT.length);
|
|
639
|
+
}
|
|
640
|
+
let out = stem.replace(/[-_]+/g, " ").trim().replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
641
|
+
const firstLetter = /[a-z]/.exec(out);
|
|
642
|
+
if (firstLetter) {
|
|
643
|
+
const i = firstLetter.index;
|
|
644
|
+
out = out.slice(0, i) + out.charAt(i).toUpperCase() + out.slice(i + 1);
|
|
645
|
+
}
|
|
646
|
+
return out;
|
|
647
|
+
}
|
|
648
|
+
function pageTitleFromSegments(segments, strategy) {
|
|
649
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
650
|
+
const parents = segments.slice(0, segments.length - 1);
|
|
651
|
+
switch (strategy) {
|
|
652
|
+
case "filename-stem":
|
|
653
|
+
return titleFromSegment(filename);
|
|
654
|
+
case "filename":
|
|
655
|
+
return filename;
|
|
656
|
+
case "sentence-case-parent": {
|
|
657
|
+
const title = sentenceCaseStem(filename);
|
|
658
|
+
if (parents.length === 0) {
|
|
659
|
+
return title;
|
|
660
|
+
}
|
|
661
|
+
return `${title} (${parents[parents.length - 1]})`;
|
|
662
|
+
}
|
|
663
|
+
case "sentence-case-parents": {
|
|
664
|
+
const title = sentenceCaseStem(filename);
|
|
665
|
+
if (parents.length === 0) {
|
|
666
|
+
return title;
|
|
667
|
+
}
|
|
668
|
+
return `${title} (${parents.join("/")})`;
|
|
669
|
+
}
|
|
670
|
+
case "sentence-case-path":
|
|
671
|
+
return `${sentenceCaseStem(filename)} (${[...parents, filename].join("/")})`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
577
674
|
|
|
578
675
|
// src/markdown.ts
|
|
579
676
|
var STORAGE_LINE_BREAK = "<br />";
|
|
@@ -1589,6 +1686,141 @@ function runMmdc(cmdPath, source, outFile, timeoutMs, maxStreamBytes) {
|
|
|
1589
1686
|
});
|
|
1590
1687
|
}
|
|
1591
1688
|
|
|
1689
|
+
// src/parent-summary.ts
|
|
1690
|
+
var PARENT_SUMMARY_START_MARKER = "<!-- repo-toolkit-confluence:parent-summary:start -->";
|
|
1691
|
+
var PARENT_SUMMARY_END_MARKER = "<!-- repo-toolkit-confluence:parent-summary:end -->";
|
|
1692
|
+
function escapeCdata(text) {
|
|
1693
|
+
return text.replace(/]]>/g, "]]]]><![CDATA[>");
|
|
1694
|
+
}
|
|
1695
|
+
function pageLink(pageId, title) {
|
|
1696
|
+
const safeTitle = escapeCdata(title);
|
|
1697
|
+
return `<ac:link><ri:page ri:content-id="${escapeXmlAttribute(pageId)}" /><ac:plain-text-link-body><![CDATA[${safeTitle}]]></ac:plain-text-link-body></ac:link>`;
|
|
1698
|
+
}
|
|
1699
|
+
function countOccurrences(haystack, needle) {
|
|
1700
|
+
let count = 0;
|
|
1701
|
+
let idx = 0;
|
|
1702
|
+
while (true) {
|
|
1703
|
+
const found = haystack.indexOf(needle, idx);
|
|
1704
|
+
if (found === -1) {
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
count += 1;
|
|
1708
|
+
idx = found + needle.length;
|
|
1709
|
+
}
|
|
1710
|
+
return count;
|
|
1711
|
+
}
|
|
1712
|
+
function mergeParentSummaryBody(currentBody, generatedRegion) {
|
|
1713
|
+
const startCount = countOccurrences(currentBody, PARENT_SUMMARY_START_MARKER);
|
|
1714
|
+
const endCount = countOccurrences(currentBody, PARENT_SUMMARY_END_MARKER);
|
|
1715
|
+
if (startCount === 0 && endCount === 0) {
|
|
1716
|
+
if (currentBody === "") {
|
|
1717
|
+
return generatedRegion;
|
|
1718
|
+
}
|
|
1719
|
+
const sep2 = currentBody.endsWith("\n") ? "" : "\n";
|
|
1720
|
+
return currentBody + sep2 + generatedRegion;
|
|
1721
|
+
}
|
|
1722
|
+
if (startCount === 1 && endCount === 1) {
|
|
1723
|
+
const startIdx = currentBody.indexOf(PARENT_SUMMARY_START_MARKER);
|
|
1724
|
+
const endIdx = currentBody.indexOf(PARENT_SUMMARY_END_MARKER);
|
|
1725
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
1726
|
+
throw new Error("malformed parent summary markers: missing marker");
|
|
1727
|
+
}
|
|
1728
|
+
if (startIdx > endIdx) {
|
|
1729
|
+
throw new Error("malformed parent summary markers: start after end");
|
|
1730
|
+
}
|
|
1731
|
+
const before = currentBody.slice(0, startIdx);
|
|
1732
|
+
const after = currentBody.slice(endIdx + PARENT_SUMMARY_END_MARKER.length);
|
|
1733
|
+
if (countOccurrences(before, PARENT_SUMMARY_START_MARKER) !== 0 || countOccurrences(before, PARENT_SUMMARY_END_MARKER) !== 0) {
|
|
1734
|
+
throw new Error("malformed parent summary markers: duplicate marker before region");
|
|
1735
|
+
}
|
|
1736
|
+
if (countOccurrences(after, PARENT_SUMMARY_START_MARKER) !== 0 || countOccurrences(after, PARENT_SUMMARY_END_MARKER) !== 0) {
|
|
1737
|
+
throw new Error("malformed parent summary markers: duplicate marker after region");
|
|
1738
|
+
}
|
|
1739
|
+
return before + generatedRegion + after;
|
|
1740
|
+
}
|
|
1741
|
+
throw new Error("malformed or duplicate parent summary markers: expected 0 or 1 managed region");
|
|
1742
|
+
}
|
|
1743
|
+
function renderParentSummary(input) {
|
|
1744
|
+
const lines = [];
|
|
1745
|
+
lines.push(PARENT_SUMMARY_START_MARKER);
|
|
1746
|
+
lines.push("<h2>Synced documentation</h2>");
|
|
1747
|
+
if (input.repositoryUrl) {
|
|
1748
|
+
const url = escapeXmlAttribute(input.repositoryUrl);
|
|
1749
|
+
const text = escapeHtml(input.repositoryUrl);
|
|
1750
|
+
lines.push(
|
|
1751
|
+
`<p><em>This documentation subtree is synced from <a href="${url}">${text}</a> and maintained by <code>repo-toolkit-confluence</code>.</em></p>`
|
|
1752
|
+
);
|
|
1753
|
+
} else {
|
|
1754
|
+
lines.push("<p><em>This documentation subtree is maintained by <code>repo-toolkit-confluence</code>.</em></p>");
|
|
1755
|
+
}
|
|
1756
|
+
lines.push("<h3>Statistics</h3>");
|
|
1757
|
+
lines.push("<ul>");
|
|
1758
|
+
lines.push(`<li>Markdown pages: ${input.stats.markdownPages}</li>`);
|
|
1759
|
+
lines.push(`<li>Directory pages: ${input.stats.directoryPages}</li>`);
|
|
1760
|
+
lines.push(`<li>Total managed pages: ${input.stats.totalPages}</li>`);
|
|
1761
|
+
lines.push(`<li>Maximum depth: ${input.stats.maxDepth}</li>`);
|
|
1762
|
+
lines.push(`<li>Attachment references: ${input.stats.attachmentReferences}</li>`);
|
|
1763
|
+
lines.push(`<li>Mermaid blocks: ${input.stats.mermaidBlocks}</li>`);
|
|
1764
|
+
lines.push("</ul>");
|
|
1765
|
+
lines.push("<h3>Pages</h3>");
|
|
1766
|
+
if (input.pages.length === 0) {
|
|
1767
|
+
lines.push("<p>No managed child pages</p>");
|
|
1768
|
+
} else {
|
|
1769
|
+
lines.push(renderTree(input.pages));
|
|
1770
|
+
}
|
|
1771
|
+
lines.push("<h3>Ownership</h3>");
|
|
1772
|
+
lines.push(
|
|
1773
|
+
`<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>`
|
|
1774
|
+
);
|
|
1775
|
+
lines.push(PARENT_SUMMARY_END_MARKER);
|
|
1776
|
+
return lines.join("\n");
|
|
1777
|
+
}
|
|
1778
|
+
function renderTree(pages) {
|
|
1779
|
+
const sorted = [...pages].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1780
|
+
const root = { children: /* @__PURE__ */ new Map(), key: "" };
|
|
1781
|
+
for (const page of sorted) {
|
|
1782
|
+
const parts = page.relativePath.split("/");
|
|
1783
|
+
let node = root;
|
|
1784
|
+
let currentPath = "";
|
|
1785
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
1786
|
+
const part = parts[i] ?? "";
|
|
1787
|
+
currentPath = currentPath ? currentPath + "/" + part : part;
|
|
1788
|
+
let child = node.children.get(part);
|
|
1789
|
+
if (!child) {
|
|
1790
|
+
child = { children: /* @__PURE__ */ new Map(), key: part };
|
|
1791
|
+
node.children.set(part, child);
|
|
1792
|
+
}
|
|
1793
|
+
if (i === parts.length - 1) {
|
|
1794
|
+
child.record = page;
|
|
1795
|
+
}
|
|
1796
|
+
node = child;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const renderNode = (node) => {
|
|
1800
|
+
const entries = [...node.children.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1801
|
+
if (entries.length === 0) {
|
|
1802
|
+
return "";
|
|
1803
|
+
}
|
|
1804
|
+
let html = "<ul>";
|
|
1805
|
+
for (const [, child] of entries) {
|
|
1806
|
+
const rec = child.record;
|
|
1807
|
+
if (rec) {
|
|
1808
|
+
const link = pageLink(rec.pageId, rec.title);
|
|
1809
|
+
const kindLabel = rec.kind === "directory" ? "directory" : "page";
|
|
1810
|
+
const pathCode = `<code>${escapeHtml(rec.relativePath)}</code>`;
|
|
1811
|
+
const childrenHtml = renderNode(child);
|
|
1812
|
+
html += `<li>${link} \u2014 ${pathCode} <em>(${kindLabel})</em>${childrenHtml}</li>`;
|
|
1813
|
+
} else {
|
|
1814
|
+
const childrenHtml = renderNode(child);
|
|
1815
|
+
html += `<li>${escapeHtml(child.key)}${childrenHtml}</li>`;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
html += "</ul>";
|
|
1819
|
+
return html;
|
|
1820
|
+
};
|
|
1821
|
+
return renderNode(root);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1592
1824
|
// src/index.ts
|
|
1593
1825
|
var INTERACTIVE_FLAG = { name: "interactive", aliases: ["i"], boolean: true };
|
|
1594
1826
|
var LocalSyncValidationAggregateError = class extends Error {
|
|
@@ -1618,6 +1850,7 @@ function validateLocalSync(entries, plan) {
|
|
|
1618
1850
|
}) : [];
|
|
1619
1851
|
plans.push({
|
|
1620
1852
|
entry,
|
|
1853
|
+
title: pageTitleFromSegments(entry.segments, plan.pageTitleStrategy),
|
|
1621
1854
|
html: body,
|
|
1622
1855
|
mermaidBlocks,
|
|
1623
1856
|
markdownDir,
|
|
@@ -1646,7 +1879,131 @@ var SyncMutationError = class extends Error {
|
|
|
1646
1879
|
this.unprocessed = input.unprocessed;
|
|
1647
1880
|
}
|
|
1648
1881
|
};
|
|
1882
|
+
var ReconciliationError = class extends Error {
|
|
1883
|
+
constructor(input) {
|
|
1884
|
+
super(input.failure.error.message);
|
|
1885
|
+
this.name = "ReconciliationError";
|
|
1886
|
+
this.phase = input.phase;
|
|
1887
|
+
this.completed = input.completed;
|
|
1888
|
+
this.failure = input.failure;
|
|
1889
|
+
this.unprocessed = input.unprocessed;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
var ParentSummaryError = class extends Error {
|
|
1893
|
+
constructor(input) {
|
|
1894
|
+
super(input.failure.error.message);
|
|
1895
|
+
this.name = "ParentSummaryError";
|
|
1896
|
+
this.phase = "parent-summary";
|
|
1897
|
+
this.changes = input.changes;
|
|
1898
|
+
this.labelsAdded = input.labelsAdded;
|
|
1899
|
+
this.cleanDeletions = input.cleanDeletions;
|
|
1900
|
+
this.pruneDeletions = input.pruneDeletions;
|
|
1901
|
+
this.blocked = input.blocked;
|
|
1902
|
+
this.failure = input.failure;
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
function planStalePruning(input) {
|
|
1906
|
+
const nodes = buildInventoryNodes(input.parentPageId, input.inventory);
|
|
1907
|
+
const retained = /* @__PURE__ */ new Set();
|
|
1908
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1909
|
+
const isStale = (entry) => entry.type === "page" && entry.labeled && !input.expectedIds.has(entry.id) && entry.id !== input.parentPageId;
|
|
1910
|
+
const visit = (node) => {
|
|
1911
|
+
if (visited.has(node.entry.id)) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
visited.add(node.entry.id);
|
|
1915
|
+
let safe = isStale(node.entry);
|
|
1916
|
+
for (const child of node.children) {
|
|
1917
|
+
visit(child);
|
|
1918
|
+
if (retained.has(child.entry.id)) {
|
|
1919
|
+
safe = false;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (!safe) {
|
|
1923
|
+
retained.add(node.entry.id);
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
for (const node of nodes.values()) {
|
|
1927
|
+
visit(node);
|
|
1928
|
+
}
|
|
1929
|
+
const deletions = deepestFirst([...nodes.values()].filter((n) => !retained.has(n.entry.id)));
|
|
1930
|
+
const blocked = [...nodes.values()].filter((n) => retained.has(n.entry.id) && isStale(n.entry)).map((n) => n.entry.id).sort();
|
|
1931
|
+
return { deletions, blocked };
|
|
1932
|
+
}
|
|
1933
|
+
function planCleanDeletions(input) {
|
|
1934
|
+
const nodes = buildInventoryNodes(input.parentPageId, input.inventory);
|
|
1935
|
+
for (const node of nodes.values()) {
|
|
1936
|
+
if (node.entry.type !== "page") {
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
`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`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
return { deletions: deepestFirst([...nodes.values()]), blocked: [] };
|
|
1943
|
+
}
|
|
1944
|
+
function buildInventoryNodes(parentPageId, inventory) {
|
|
1945
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1946
|
+
for (const entry of inventory) {
|
|
1947
|
+
if (entry.id === parentPageId) {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (nodes.has(entry.id)) {
|
|
1951
|
+
throw new Error(`Incomplete descendant inventory: duplicate id ${entry.id}`);
|
|
1952
|
+
}
|
|
1953
|
+
nodes.set(entry.id, { entry, depth: -1, children: [] });
|
|
1954
|
+
}
|
|
1955
|
+
const depthOf = (node) => {
|
|
1956
|
+
if (node.depth >= 0) {
|
|
1957
|
+
return node.depth;
|
|
1958
|
+
}
|
|
1959
|
+
if (typeof node.entry.depth === "number") {
|
|
1960
|
+
node.depth = node.entry.depth;
|
|
1961
|
+
return node.depth;
|
|
1962
|
+
}
|
|
1963
|
+
let depth = 0;
|
|
1964
|
+
let current = node;
|
|
1965
|
+
const seen = /* @__PURE__ */ new Set([node.entry.id]);
|
|
1966
|
+
while (true) {
|
|
1967
|
+
const pid = current.entry.parentId;
|
|
1968
|
+
if (pid === parentPageId) {
|
|
1969
|
+
depth += 1;
|
|
1970
|
+
node.depth = depth;
|
|
1971
|
+
return depth;
|
|
1972
|
+
}
|
|
1973
|
+
if (!pid) {
|
|
1974
|
+
throw new Error(`Incomplete descendant inventory: missing parent for ${node.entry.id}`);
|
|
1975
|
+
}
|
|
1976
|
+
if (seen.has(pid)) {
|
|
1977
|
+
throw new Error(`Incomplete descendant inventory: parent cycle at ${pid}`);
|
|
1978
|
+
}
|
|
1979
|
+
seen.add(pid);
|
|
1980
|
+
const parent = nodes.get(pid);
|
|
1981
|
+
if (!parent) {
|
|
1982
|
+
throw new Error(
|
|
1983
|
+
`Incomplete descendant inventory: ancestor ${pid} of ${node.entry.id} is missing from the listing`
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
depth += 1;
|
|
1987
|
+
current = parent;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
for (const node of nodes.values()) {
|
|
1991
|
+
depthOf(node);
|
|
1992
|
+
const pid = node.entry.parentId;
|
|
1993
|
+
if (pid !== void 0 && pid !== parentPageId) {
|
|
1994
|
+
nodes.get(pid)?.children.push(node);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return nodes;
|
|
1998
|
+
}
|
|
1999
|
+
function deepestFirst(nodes) {
|
|
2000
|
+
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);
|
|
2001
|
+
}
|
|
2002
|
+
function hasManagedMarker(labels) {
|
|
2003
|
+
return labels.some((label) => label.name === CONFLUENCE_MANAGED_LABEL && label.prefix === "global");
|
|
2004
|
+
}
|
|
1649
2005
|
function resolveConfluenceSyncPlan(options = {}) {
|
|
2006
|
+
const pageTitleStrategy = resolvePageTitleStrategy(options.pageTitleStrategy);
|
|
1650
2007
|
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
1651
2008
|
const folder = resolveInputPath(cwd, options.folder ?? "");
|
|
1652
2009
|
if (!options.folder) {
|
|
@@ -1693,7 +2050,10 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1693
2050
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1694
2051
|
dryRun: options.dryRun ?? false,
|
|
1695
2052
|
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1696
|
-
repositoryUrl
|
|
2053
|
+
repositoryUrl,
|
|
2054
|
+
clean: options.clean ?? false,
|
|
2055
|
+
updateParentPage: options.updateParentPage ?? true,
|
|
2056
|
+
pageTitleStrategy
|
|
1697
2057
|
};
|
|
1698
2058
|
}
|
|
1699
2059
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1702,18 +2062,43 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1702
2062
|
const tree = await readDocTree(plan.folder);
|
|
1703
2063
|
if (tree.entries.length === 0) {
|
|
1704
2064
|
log(`No markdown files found under ${plan.folder}`);
|
|
1705
|
-
return;
|
|
1706
2065
|
}
|
|
1707
|
-
validateLocalHierarchy(tree.entries);
|
|
1708
2066
|
const localPlan = validateLocalSync(tree.entries, plan);
|
|
2067
|
+
validateLocalHierarchy(localPlan.entries, plan.pageTitleStrategy);
|
|
1709
2068
|
if (plan.dryRun) {
|
|
1710
2069
|
log("[dry-run] Walking documentation tree only.");
|
|
1711
2070
|
for (const entryPlan of localPlan.entries) {
|
|
1712
2071
|
const attCount = entryPlan.attachments.length;
|
|
1713
2072
|
const mermaidCount = entryPlan.mermaidBlocks.length;
|
|
1714
2073
|
log(
|
|
1715
|
-
`[dry-run] would sync ${entryPlan.entry.segments.join("/")}` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
2074
|
+
`[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"})` : "")
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
if (plan.clean) {
|
|
2078
|
+
log(
|
|
2079
|
+
"[dry-run] clean requested: a real sync would move every page descendant of the target page to trash before recreating the local hierarchy."
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
log(
|
|
2083
|
+
"[dry-run] a real sync would label every mapped page with the ownership marker and prune stale labeled descendants."
|
|
2084
|
+
);
|
|
2085
|
+
if (plan.updateParentPage) {
|
|
2086
|
+
const stats = computeDryRunStats(localPlan);
|
|
2087
|
+
log(
|
|
2088
|
+
`[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}`
|
|
1716
2089
|
);
|
|
2090
|
+
if (localPlan.entries.length === 0) {
|
|
2091
|
+
log("[dry-run] parent tree: No managed child pages");
|
|
2092
|
+
} else {
|
|
2093
|
+
for (const entryPlan of localPlan.entries) {
|
|
2094
|
+
const dirParts = entryPlan.entry.segments.slice(0, -1);
|
|
2095
|
+
for (let i = 0; i < dirParts.length; i += 1) {
|
|
2096
|
+
const dirPath = dirParts.slice(0, i + 1).join("/");
|
|
2097
|
+
log(`[dry-run] parent tree: ${dirPath} (directory) => "${dirParts[i] ?? ""}"`);
|
|
2098
|
+
}
|
|
2099
|
+
log(`[dry-run] parent tree: ${entryPlan.entry.segments.join("/")} (page) => "${entryPlan.title}"`);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
1717
2102
|
}
|
|
1718
2103
|
return;
|
|
1719
2104
|
}
|
|
@@ -1722,13 +2107,29 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1722
2107
|
username: plan.username,
|
|
1723
2108
|
apiToken: plan.apiToken
|
|
1724
2109
|
});
|
|
2110
|
+
const labelsAdded = [];
|
|
2111
|
+
const cleanDeletions = [];
|
|
2112
|
+
const pruneDeletions = [];
|
|
2113
|
+
const blocked = [];
|
|
2114
|
+
if (plan.clean) {
|
|
2115
|
+
const descendants = await client.getPageDescendants(plan.parentPageId);
|
|
2116
|
+
const inventory = descendants.filter((d) => d.id !== plan.parentPageId).map((d) => toInventoryEntry(d, false));
|
|
2117
|
+
const cleanPlan = planCleanDeletions({ parentPageId: plan.parentPageId, inventory });
|
|
2118
|
+
await executeDeletions(cleanPlan.deletions, "clean", client, log, cleanDeletions);
|
|
2119
|
+
}
|
|
1725
2120
|
const spaceId = await client.getSpaceIdByKey(plan.spaceKey);
|
|
1726
2121
|
const cache = new PageTitleCache(spaceId, client);
|
|
2122
|
+
const syncState = {
|
|
2123
|
+
mappedIds: /* @__PURE__ */ new Set(),
|
|
2124
|
+
ensuredLabels: /* @__PURE__ */ new Set(),
|
|
2125
|
+
labelsAdded,
|
|
2126
|
+
mappedRecords: /* @__PURE__ */ new Map()
|
|
2127
|
+
};
|
|
1727
2128
|
const changes = [];
|
|
1728
2129
|
for (let i = 0; i < localPlan.entries.length; i += 1) {
|
|
1729
2130
|
const entryPlan = localPlan.entries[i];
|
|
1730
2131
|
try {
|
|
1731
|
-
await syncEntry(entryPlan, plan, client, cache, log, changes);
|
|
2132
|
+
await syncEntry(entryPlan, plan, client, cache, log, changes, syncState);
|
|
1732
2133
|
} catch (error) {
|
|
1733
2134
|
throw new SyncMutationError({
|
|
1734
2135
|
changes,
|
|
@@ -1737,20 +2138,207 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1737
2138
|
});
|
|
1738
2139
|
}
|
|
1739
2140
|
}
|
|
1740
|
-
|
|
2141
|
+
if (!plan.clean) {
|
|
2142
|
+
const descendants = await client.getPageDescendants(plan.parentPageId);
|
|
2143
|
+
const inventory = [];
|
|
2144
|
+
for (const d of descendants) {
|
|
2145
|
+
if (d.id === plan.parentPageId) {
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
if (d.type !== "page") {
|
|
2149
|
+
inventory.push(toInventoryEntry(d, false));
|
|
2150
|
+
continue;
|
|
2151
|
+
}
|
|
2152
|
+
if (syncState.mappedIds.has(d.id)) {
|
|
2153
|
+
inventory.push(toInventoryEntry(d, true));
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
const labels = await client.getPageLabels(d.id);
|
|
2157
|
+
inventory.push(toInventoryEntry(d, hasManagedMarker(labels)));
|
|
2158
|
+
}
|
|
2159
|
+
const prunePlan = planStalePruning({
|
|
2160
|
+
parentPageId: plan.parentPageId,
|
|
2161
|
+
expectedIds: syncState.mappedIds,
|
|
2162
|
+
inventory
|
|
2163
|
+
});
|
|
2164
|
+
await executeDeletions(prunePlan.deletions, "prune", client, log, pruneDeletions);
|
|
2165
|
+
for (const pageId of prunePlan.blocked) {
|
|
2166
|
+
blocked.push(pageId);
|
|
2167
|
+
log(`blocked: stale page ${pageId} retained because it has unlabeled, non-page, or expected descendants`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
let parentStatus = "skipped";
|
|
2171
|
+
if (plan.updateParentPage) {
|
|
2172
|
+
try {
|
|
2173
|
+
const parentPage = await client.getPage(plan.parentPageId);
|
|
2174
|
+
const stats = computeParentStats(localPlan, syncState);
|
|
2175
|
+
const pages = [...syncState.mappedRecords.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
2176
|
+
const region = renderParentSummary({ repositoryUrl: plan.repositoryUrl, stats, pages });
|
|
2177
|
+
const currentBody = parentPage.body?.storage?.value ?? "";
|
|
2178
|
+
const merged = mergeParentSummaryBody(currentBody, region);
|
|
2179
|
+
if (merged === currentBody) {
|
|
2180
|
+
log(`parent-unchanged: page ${plan.parentPageId}`);
|
|
2181
|
+
parentStatus = "unchanged";
|
|
2182
|
+
} else {
|
|
2183
|
+
await client.updatePage({
|
|
2184
|
+
id: plan.parentPageId,
|
|
2185
|
+
title: parentPage.title,
|
|
2186
|
+
body: { representation: "storage", value: merged },
|
|
2187
|
+
version: { number: (parentPage.version?.number ?? 0) + 1, message: plan.versionMessage }
|
|
2188
|
+
});
|
|
2189
|
+
log(`parent-updated: page ${plan.parentPageId}`);
|
|
2190
|
+
parentStatus = "updated";
|
|
2191
|
+
}
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
throw new ParentSummaryError({
|
|
2194
|
+
changes,
|
|
2195
|
+
labelsAdded: [...labelsAdded],
|
|
2196
|
+
cleanDeletions: [...cleanDeletions],
|
|
2197
|
+
pruneDeletions: [...pruneDeletions],
|
|
2198
|
+
blocked: [...blocked],
|
|
2199
|
+
failure: { pageId: plan.parentPageId, error: error instanceof Error ? error : new Error(String(error)) }
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return { changes, labelsAdded, cleanDeletions, pruneDeletions, blocked, parentStatus };
|
|
2204
|
+
}
|
|
2205
|
+
function computeParentStats(localPlan, state) {
|
|
2206
|
+
const dirSet = /* @__PURE__ */ new Set();
|
|
2207
|
+
let maxDepth = 0;
|
|
2208
|
+
let attachmentReferences = 0;
|
|
2209
|
+
let mermaidBlocks = 0;
|
|
2210
|
+
for (const entryPlan of localPlan.entries) {
|
|
2211
|
+
const segs = entryPlan.entry.segments;
|
|
2212
|
+
if (segs.length > maxDepth) {
|
|
2213
|
+
maxDepth = segs.length;
|
|
2214
|
+
}
|
|
2215
|
+
for (let i = 0; i < segs.length - 1; i += 1) {
|
|
2216
|
+
dirSet.add(segs.slice(0, i + 1).join("/"));
|
|
2217
|
+
}
|
|
2218
|
+
attachmentReferences += entryPlan.attachments.length;
|
|
2219
|
+
mermaidBlocks += entryPlan.mermaidBlocks.length;
|
|
2220
|
+
}
|
|
2221
|
+
const markdownPages = localPlan.entries.length;
|
|
2222
|
+
const directoryPages = dirSet.size;
|
|
2223
|
+
const totalPages = state.mappedRecords.size;
|
|
2224
|
+
const effectiveMaxDepth = localPlan.entries.length === 0 ? 0 : maxDepth;
|
|
2225
|
+
return {
|
|
2226
|
+
markdownPages,
|
|
2227
|
+
directoryPages,
|
|
2228
|
+
totalPages: totalPages > 0 ? totalPages : directoryPages + markdownPages,
|
|
2229
|
+
maxDepth: effectiveMaxDepth,
|
|
2230
|
+
attachmentReferences,
|
|
2231
|
+
mermaidBlocks
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function computeDryRunStats(localPlan) {
|
|
2235
|
+
const dirSet = /* @__PURE__ */ new Set();
|
|
2236
|
+
let maxDepth = 0;
|
|
2237
|
+
let attachmentReferences = 0;
|
|
2238
|
+
let mermaidBlocks = 0;
|
|
2239
|
+
for (const entryPlan of localPlan.entries) {
|
|
2240
|
+
const segs = entryPlan.entry.segments;
|
|
2241
|
+
if (segs.length > maxDepth) {
|
|
2242
|
+
maxDepth = segs.length;
|
|
2243
|
+
}
|
|
2244
|
+
for (let i = 0; i < segs.length - 1; i += 1) {
|
|
2245
|
+
dirSet.add(segs.slice(0, i + 1).join("/"));
|
|
2246
|
+
}
|
|
2247
|
+
attachmentReferences += entryPlan.attachments.length;
|
|
2248
|
+
mermaidBlocks += entryPlan.mermaidBlocks.length;
|
|
2249
|
+
}
|
|
2250
|
+
const markdownPages = localPlan.entries.length;
|
|
2251
|
+
const directoryPages = dirSet.size;
|
|
2252
|
+
return {
|
|
2253
|
+
markdownPages,
|
|
2254
|
+
directoryPages,
|
|
2255
|
+
totalPages: directoryPages + markdownPages,
|
|
2256
|
+
maxDepth: markdownPages === 0 ? 0 : maxDepth,
|
|
2257
|
+
attachmentReferences,
|
|
2258
|
+
mermaidBlocks
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
function toInventoryEntry(d, labeled) {
|
|
2262
|
+
const entry = { id: d.id, type: d.type, labeled };
|
|
2263
|
+
if (d.parentId !== void 0) {
|
|
2264
|
+
entry.parentId = d.parentId;
|
|
2265
|
+
}
|
|
2266
|
+
if (d.depth !== void 0) {
|
|
2267
|
+
entry.depth = d.depth;
|
|
2268
|
+
}
|
|
2269
|
+
if (d.title !== void 0) {
|
|
2270
|
+
entry.title = d.title;
|
|
2271
|
+
}
|
|
2272
|
+
return entry;
|
|
2273
|
+
}
|
|
2274
|
+
async function ensureManagedLabel(pageId, client, state, log) {
|
|
2275
|
+
if (state.ensuredLabels.has(pageId)) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const labels = await client.getPageLabels(pageId);
|
|
2279
|
+
if (!hasManagedMarker(labels)) {
|
|
2280
|
+
await client.addManagedLabel(pageId);
|
|
2281
|
+
state.labelsAdded.push(pageId);
|
|
2282
|
+
log(`labeled: page ${pageId}`);
|
|
2283
|
+
}
|
|
2284
|
+
state.ensuredLabels.add(pageId);
|
|
2285
|
+
}
|
|
2286
|
+
async function executeDeletions(ids, phase, client, log, evidence) {
|
|
2287
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
2288
|
+
const pageId = ids[i];
|
|
2289
|
+
if (pageId === void 0) {
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
2292
|
+
try {
|
|
2293
|
+
await client.deletePage(pageId);
|
|
2294
|
+
evidence.push(pageId);
|
|
2295
|
+
log(`${phase === "clean" ? "clean" : "pruned"}: trashed page ${pageId}`);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
throw new ReconciliationError({
|
|
2298
|
+
phase,
|
|
2299
|
+
completed: [...evidence],
|
|
2300
|
+
failure: { pageId, error: error instanceof Error ? error : new Error(String(error)) },
|
|
2301
|
+
unprocessed: ids.slice(i + 1)
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
1741
2305
|
}
|
|
1742
|
-
async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
2306
|
+
async function syncEntry(entryPlan, plan, client, cache, log, changes, state) {
|
|
1743
2307
|
const { entry, html: precomputedHtml, mermaidBlocks, markdownDir, hasLocalImages, hasMermaidBlocks } = entryPlan;
|
|
1744
2308
|
const segments = entry.segments;
|
|
1745
2309
|
if (segments.length === 0) {
|
|
1746
2310
|
return;
|
|
1747
2311
|
}
|
|
1748
2312
|
let currentParentId = plan.parentPageId;
|
|
2313
|
+
const recordDirectory = (relativePath, title, pageId, depth) => {
|
|
2314
|
+
if (!state.mappedRecords.has(relativePath)) {
|
|
2315
|
+
state.mappedRecords.set(relativePath, {
|
|
2316
|
+
relativePath,
|
|
2317
|
+
kind: "directory",
|
|
2318
|
+
title,
|
|
2319
|
+
pageId,
|
|
2320
|
+
depth,
|
|
2321
|
+
attachmentCount: 0,
|
|
2322
|
+
mermaidCount: 0
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
};
|
|
2326
|
+
const recordLeaf = (relativePath, title, pageId, depth) => {
|
|
2327
|
+
state.mappedRecords.set(relativePath, {
|
|
2328
|
+
relativePath,
|
|
2329
|
+
kind: "leaf",
|
|
2330
|
+
title,
|
|
2331
|
+
pageId,
|
|
2332
|
+
depth,
|
|
2333
|
+
attachmentCount: entryPlan.attachments.length,
|
|
2334
|
+
mermaidCount: entryPlan.mermaidBlocks.length
|
|
2335
|
+
});
|
|
2336
|
+
};
|
|
1749
2337
|
for (let idx = 0; idx < segments.length; idx += 1) {
|
|
1750
2338
|
const isLast = idx === segments.length - 1;
|
|
1751
2339
|
const segment = segments[idx] ?? "";
|
|
1752
2340
|
if (isLast && isMarkdownName(segment)) {
|
|
1753
|
-
const title =
|
|
2341
|
+
const title = entryPlan.title;
|
|
1754
2342
|
const leafNeedsUploads = hasLocalImages || hasMermaidBlocks;
|
|
1755
2343
|
const existing = await cache.find(title, currentParentId);
|
|
1756
2344
|
if (!existing && !leafNeedsUploads) {
|
|
@@ -1759,12 +2347,18 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1759
2347
|
parentId: currentParentId,
|
|
1760
2348
|
body: { representation: "storage", value: precomputedHtml }
|
|
1761
2349
|
});
|
|
2350
|
+
state.mappedIds.add(pageId2);
|
|
2351
|
+
await ensureManagedLabel(pageId2, client, state, log);
|
|
2352
|
+
recordLeaf(segments.join("/"), title, pageId2, segments.length);
|
|
1762
2353
|
log(`created: ${segments.join("/")} (page ${pageId2})`);
|
|
1763
2354
|
changes.push({ entry, pageId: pageId2, kind: "created" });
|
|
1764
2355
|
return;
|
|
1765
2356
|
}
|
|
1766
2357
|
const existingPage = existing ?? await cache.findOrCreate(title, currentParentId);
|
|
1767
2358
|
const pageId = existingPage.id;
|
|
2359
|
+
state.mappedIds.add(pageId);
|
|
2360
|
+
await ensureManagedLabel(pageId, client, state, log);
|
|
2361
|
+
recordLeaf(segments.join("/"), title, pageId, segments.length);
|
|
1768
2362
|
const current = await client.getPage(pageId);
|
|
1769
2363
|
const currentBody = current.body?.storage?.value ?? "";
|
|
1770
2364
|
let body = precomputedHtml;
|
|
@@ -1809,12 +2403,18 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1809
2403
|
return;
|
|
1810
2404
|
}
|
|
1811
2405
|
if (isMarkdownName(segment)) {
|
|
1812
|
-
const title =
|
|
2406
|
+
const title = pageTitleFromSegments(segments.slice(0, idx + 1), plan.pageTitleStrategy);
|
|
1813
2407
|
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
2408
|
+
state.mappedIds.add(page2.id);
|
|
2409
|
+
await ensureManagedLabel(page2.id, client, state, log);
|
|
2410
|
+
recordDirectory(segments.slice(0, idx + 1).join("/"), title, page2.id, idx + 1);
|
|
1814
2411
|
currentParentId = page2.id;
|
|
1815
2412
|
continue;
|
|
1816
2413
|
}
|
|
1817
2414
|
const page = await cache.findOrCreate(segment, currentParentId);
|
|
2415
|
+
state.mappedIds.add(page.id);
|
|
2416
|
+
await ensureManagedLabel(page.id, client, state, log);
|
|
2417
|
+
recordDirectory(segments.slice(0, idx + 1).join("/"), segment, page.id, idx + 1);
|
|
1818
2418
|
currentParentId = page.id;
|
|
1819
2419
|
}
|
|
1820
2420
|
}
|
|
@@ -1940,14 +2540,15 @@ async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
|
1940
2540
|
}
|
|
1941
2541
|
return predicted;
|
|
1942
2542
|
}
|
|
1943
|
-
function validateLocalHierarchy(entries) {
|
|
2543
|
+
function validateLocalHierarchy(entries, strategy) {
|
|
1944
2544
|
const seen = /* @__PURE__ */ new Map();
|
|
1945
|
-
for (const
|
|
2545
|
+
for (const entryPlan of entries) {
|
|
2546
|
+
const { entry } = entryPlan;
|
|
1946
2547
|
let parentKey = "";
|
|
1947
2548
|
for (let index = 0; index < entry.segments.length; index += 1) {
|
|
1948
2549
|
const segment = entry.segments[index] ?? "";
|
|
1949
2550
|
const isLast = index === entry.segments.length - 1;
|
|
1950
|
-
const title = isMarkdownName(segment) ?
|
|
2551
|
+
const title = isMarkdownName(segment) ? isLast ? entryPlan.title : pageTitleFromSegments(entry.segments.slice(0, index + 1), strategy) : segment;
|
|
1951
2552
|
const kind = isLast && isMarkdownName(segment) ? "file" : "dir";
|
|
1952
2553
|
const key = `${parentKey}::${title}`;
|
|
1953
2554
|
const existing = seen.get(key);
|
|
@@ -1960,10 +2561,15 @@ function validateLocalHierarchy(entries) {
|
|
|
1960
2561
|
}
|
|
1961
2562
|
}
|
|
1962
2563
|
export {
|
|
2564
|
+
CONFLUENCE_MANAGED_LABEL,
|
|
1963
2565
|
ConfluenceApiError,
|
|
1964
2566
|
ConfluenceClient,
|
|
2567
|
+
DEFAULT_PAGE_TITLE_STRATEGY,
|
|
1965
2568
|
INTERACTIVE_FLAG,
|
|
1966
2569
|
LocalSyncValidationAggregateError,
|
|
2570
|
+
PAGE_TITLE_STRATEGIES,
|
|
2571
|
+
ParentSummaryError,
|
|
2572
|
+
ReconciliationError,
|
|
1967
2573
|
SyncMutationError,
|
|
1968
2574
|
escapeAttachmentFilename,
|
|
1969
2575
|
escapeXmlAttribute,
|
|
@@ -1971,10 +2577,14 @@ export {
|
|
|
1971
2577
|
isMarkdownName,
|
|
1972
2578
|
isRemoteUrl,
|
|
1973
2579
|
markdownToStorage,
|
|
2580
|
+
pageTitleFromSegments,
|
|
2581
|
+
planCleanDeletions,
|
|
2582
|
+
planStalePruning,
|
|
1974
2583
|
preflightImagesToAttachments,
|
|
1975
2584
|
preflightMermaidBlocks,
|
|
1976
2585
|
readDocTree,
|
|
1977
2586
|
resolveConfluenceSyncPlan,
|
|
2587
|
+
resolvePageTitleStrategy,
|
|
1978
2588
|
resolveConfluenceSyncPlan as resolveSyncPlan,
|
|
1979
2589
|
rewriteImagesToAttachments,
|
|
1980
2590
|
rewriteMermaidBlocks,
|