create-confluence-sync 1.0.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 +111 -0
- package/package.json +25 -0
- package/src/agents-md.js +273 -0
- package/src/api.js +207 -0
- package/src/cli.js +496 -0
- package/src/git.js +137 -0
- package/src/hook.js +118 -0
- package/src/sync.js +278 -0
- package/src/tree.js +171 -0
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# create-confluence-sync
|
|
2
|
+
|
|
3
|
+
Bidirectional documentation sync between local files and Confluence Server via Git.
|
|
4
|
+
|
|
5
|
+
Edit XHTML files locally, commit — changes sync to Confluence automatically. Someone edits in Confluence — changes sync back on your next commit.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-confluence-sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The wizard will ask for:
|
|
14
|
+
- Confluence URL
|
|
15
|
+
- Personal Access Token
|
|
16
|
+
- Space to sync
|
|
17
|
+
|
|
18
|
+
It downloads all pages, sets up Git with auto-sync hooks, and you're ready.
|
|
19
|
+
|
|
20
|
+
## How It Works
|
|
21
|
+
|
|
22
|
+
1. You edit `.xhtml` files in `docs/` folder
|
|
23
|
+
2. You `git commit`
|
|
24
|
+
3. Post-commit hook automatically:
|
|
25
|
+
- Pulls changes from Confluence
|
|
26
|
+
- Merges with your changes
|
|
27
|
+
- Pushes your changes to Confluence
|
|
28
|
+
- Updates the local tree map
|
|
29
|
+
|
|
30
|
+
## File Structure
|
|
31
|
+
|
|
32
|
+
After setup:
|
|
33
|
+
```
|
|
34
|
+
your-project/
|
|
35
|
+
├── docs/
|
|
36
|
+
│ └── LLS/ # your Confluence space
|
|
37
|
+
│ ├── Page Title/
|
|
38
|
+
│ │ ├── Page Title.xhtml
|
|
39
|
+
│ │ └── Child Page/
|
|
40
|
+
│ │ └── Child Page.xhtml
|
|
41
|
+
├── .confluence/
|
|
42
|
+
│ ├── config.json # connection settings (gitignored)
|
|
43
|
+
│ └── tree.json # page map (tracked in git)
|
|
44
|
+
├── AGENTS.md # instructions for AI agents
|
|
45
|
+
└── .gitignore
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Setup (first time)
|
|
52
|
+
npx create-confluence-sync
|
|
53
|
+
|
|
54
|
+
# Manual sync
|
|
55
|
+
npx create-confluence-sync sync
|
|
56
|
+
|
|
57
|
+
# Push specific file
|
|
58
|
+
npx create-confluence-sync sync "docs/LLS/Page/Page.xhtml"
|
|
59
|
+
|
|
60
|
+
# Restore hidden pages (interactive)
|
|
61
|
+
npx create-confluence-sync restore
|
|
62
|
+
|
|
63
|
+
# Restore by name
|
|
64
|
+
npx create-confluence-sync restore "Page Title"
|
|
65
|
+
|
|
66
|
+
# List spaces
|
|
67
|
+
npx create-confluence-sync spaces
|
|
68
|
+
|
|
69
|
+
# Get page content
|
|
70
|
+
npx create-confluence-sync page <pageId>
|
|
71
|
+
|
|
72
|
+
# View remote tree
|
|
73
|
+
npx create-confluence-sync tree
|
|
74
|
+
|
|
75
|
+
# View local tree
|
|
76
|
+
npx create-confluence-sync local-tree
|
|
77
|
+
|
|
78
|
+
# Delete page from Confluence
|
|
79
|
+
npx create-confluence-sync delete <pageId>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Hiding Pages (Virtual Delete)
|
|
83
|
+
|
|
84
|
+
Delete a file or folder locally and commit. The page stays in Confluence but stops syncing locally. Use `restore` to bring it back.
|
|
85
|
+
|
|
86
|
+
## Format
|
|
87
|
+
|
|
88
|
+
Files use Confluence Storage Format (XHTML):
|
|
89
|
+
```html
|
|
90
|
+
<h2>Title</h2>
|
|
91
|
+
<p>Text with <strong>bold</strong> and <em>italic</em>.</p>
|
|
92
|
+
<ul>
|
|
93
|
+
<li>Item 1</li>
|
|
94
|
+
<li>Item 2</li>
|
|
95
|
+
</ul>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## AI Agent Compatible
|
|
99
|
+
|
|
100
|
+
The generated `AGENTS.md` file contains full instructions for AI agents (Claude, GPT, etc.) to create, edit, and manage documentation through the same Git workflow.
|
|
101
|
+
|
|
102
|
+
## Requirements
|
|
103
|
+
|
|
104
|
+
- Node.js 20+
|
|
105
|
+
- Git
|
|
106
|
+
- Confluence Server with REST API access
|
|
107
|
+
- Personal Access Token
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-confluence-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Bidirectional Confluence Server documentation sync via Git. Edit XHTML locally, commit, auto-sync with Confluence.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-confluence-sync": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/cli.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/cli.js",
|
|
12
|
+
"test": "node --test tests/e2e.test.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"confluence",
|
|
16
|
+
"sync",
|
|
17
|
+
"documentation",
|
|
18
|
+
"git"
|
|
19
|
+
],
|
|
20
|
+
"author": "damix96",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@inquirer/prompts": "^7.10.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/agents-md.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a tree structure from flat pages map.
|
|
6
|
+
* Returns array of root nodes, each with nested `children`.
|
|
7
|
+
*/
|
|
8
|
+
function buildPageTree(pages) {
|
|
9
|
+
const nodes = {};
|
|
10
|
+
const roots = [];
|
|
11
|
+
|
|
12
|
+
for (const [id, page] of Object.entries(pages)) {
|
|
13
|
+
nodes[id] = { id, ...page, children: [] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
17
|
+
if (node.parentId && nodes[node.parentId]) {
|
|
18
|
+
nodes[node.parentId].children.push(node);
|
|
19
|
+
} else {
|
|
20
|
+
roots.push(node);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Sort children alphabetically by title at each level
|
|
25
|
+
const sortChildren = (nodeList) => {
|
|
26
|
+
nodeList.sort((a, b) => a.title.localeCompare(b.title));
|
|
27
|
+
for (const node of nodeList) {
|
|
28
|
+
sortChildren(node.children);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
sortChildren(roots);
|
|
32
|
+
|
|
33
|
+
return roots;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Render a tree of pages as an indented directory listing.
|
|
38
|
+
* Only visible (non-hidden) pages are shown.
|
|
39
|
+
*/
|
|
40
|
+
function renderTree(roots, space, indent = '') {
|
|
41
|
+
const lines = [];
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < roots.length; i++) {
|
|
44
|
+
const node = roots[i];
|
|
45
|
+
if (node.hidden) continue;
|
|
46
|
+
|
|
47
|
+
const isLast = i === roots.length - 1;
|
|
48
|
+
const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251c\u2500\u2500 ';
|
|
49
|
+
const childIndent = indent + (isLast ? ' ' : '\u2502 ');
|
|
50
|
+
|
|
51
|
+
lines.push(`${indent}${connector}${node.title}/`);
|
|
52
|
+
lines.push(`${childIndent}\u251c\u2500\u2500 ${node.title}.xhtml`);
|
|
53
|
+
|
|
54
|
+
const visibleChildren = node.children.filter((c) => !c.hidden);
|
|
55
|
+
const childLines = renderTree(visibleChildren, space, childIndent);
|
|
56
|
+
if (childLines.length > 0) {
|
|
57
|
+
lines.push(...childLines);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return lines;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate the full AGENTS.md content.
|
|
66
|
+
*/
|
|
67
|
+
function buildContent(config, tree) {
|
|
68
|
+
const space = tree.space || config.space || 'SPACE';
|
|
69
|
+
const baseUrl = tree.baseUrl || config.baseUrl || config.url || '';
|
|
70
|
+
const lastSync = tree.lastSync || 'never';
|
|
71
|
+
|
|
72
|
+
const roots = buildPageTree(tree.pages || {});
|
|
73
|
+
const visibleRoots = roots.filter((r) => !r.hidden);
|
|
74
|
+
const treeLines = renderTree(visibleRoots, space);
|
|
75
|
+
const treeBlock =
|
|
76
|
+
treeLines.length > 0
|
|
77
|
+
? `docs/${space}/\n${treeLines.join('\n')}`
|
|
78
|
+
: `docs/${space}/ (empty)`;
|
|
79
|
+
|
|
80
|
+
return `# AGENTS.md
|
|
81
|
+
|
|
82
|
+
> Auto-generated by confluence-sync. Do not edit manually.
|
|
83
|
+
> Re-generated on every sync.
|
|
84
|
+
|
|
85
|
+
## What is this project
|
|
86
|
+
|
|
87
|
+
This repository is a **bidirectional documentation sync** between a local file system and **Confluence**.
|
|
88
|
+
All interaction happens through files and git commits — no direct Confluence API calls needed.
|
|
89
|
+
|
|
90
|
+
| Parameter | Value |
|
|
91
|
+
|-----------|-------|
|
|
92
|
+
| Confluence URL | ${baseUrl} |
|
|
93
|
+
| Space | ${space} |
|
|
94
|
+
| Last synced | ${lastSync} |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## File structure
|
|
99
|
+
|
|
100
|
+
\`\`\`
|
|
101
|
+
project-root/
|
|
102
|
+
\u251c\u2500\u2500 docs/${space}/ <- documentation mirror of Confluence space
|
|
103
|
+
\u251c\u2500\u2500 .confluence/ <- internal data, DO NOT TOUCH
|
|
104
|
+
\u2502 \u251c\u2500\u2500 config.json <- connection config (in .gitignore)
|
|
105
|
+
\u2502 \u2514\u2500\u2500 tree.json <- structure map (page IDs, versions, hidden flags)
|
|
106
|
+
\u251c\u2500\u2500 AGENTS.md <- this file
|
|
107
|
+
\u2514\u2500\u2500 .gitignore
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
### docs/${space}/
|
|
111
|
+
|
|
112
|
+
This folder mirrors the Confluence space hierarchy:
|
|
113
|
+
|
|
114
|
+
- **Folder** = Confluence page
|
|
115
|
+
- **Nesting** = parent-child relationship
|
|
116
|
+
- **File \`{title}.xhtml\`** inside the folder = page content
|
|
117
|
+
|
|
118
|
+
### .confluence/
|
|
119
|
+
|
|
120
|
+
Internal sync data. **Never modify these files manually.**
|
|
121
|
+
|
|
122
|
+
- \`tree.json\` — map of all pages: their IDs, versions, paths, hidden flags
|
|
123
|
+
- \`config.json\` — connection credentials (excluded from git)
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## File format
|
|
128
|
+
|
|
129
|
+
Pages are stored in **Confluence Storage Format** (XHTML with Confluence-specific tags).
|
|
130
|
+
|
|
131
|
+
### Basic markup
|
|
132
|
+
|
|
133
|
+
\`\`\`xhtml
|
|
134
|
+
<h2>Heading</h2>
|
|
135
|
+
<p>Text with <strong>bold</strong> and <em>italic</em>.</p>
|
|
136
|
+
|
|
137
|
+
<ul>
|
|
138
|
+
<li>Item 1</li>
|
|
139
|
+
<li>Item 2</li>
|
|
140
|
+
</ul>
|
|
141
|
+
|
|
142
|
+
<ol>
|
|
143
|
+
<li>First</li>
|
|
144
|
+
<li>Second</li>
|
|
145
|
+
</ol>
|
|
146
|
+
\`\`\`
|
|
147
|
+
|
|
148
|
+
### Tables
|
|
149
|
+
|
|
150
|
+
\`\`\`xhtml
|
|
151
|
+
<table>
|
|
152
|
+
<tbody>
|
|
153
|
+
<tr>
|
|
154
|
+
<th>Column A</th>
|
|
155
|
+
<th>Column B</th>
|
|
156
|
+
</tr>
|
|
157
|
+
<tr>
|
|
158
|
+
<td>Value 1</td>
|
|
159
|
+
<td>Value 2</td>
|
|
160
|
+
</tr>
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
### Code blocks (Confluence macro)
|
|
166
|
+
|
|
167
|
+
\`\`\`xhtml
|
|
168
|
+
<ac:structured-macro ac:name="code">
|
|
169
|
+
<ac:parameter ac:name="language">java</ac:parameter>
|
|
170
|
+
<ac:plain-text-body><![CDATA[public class Example {
|
|
171
|
+
public static void main(String[] args) {
|
|
172
|
+
System.out.println("Hello");
|
|
173
|
+
}
|
|
174
|
+
}]]></ac:plain-text-body>
|
|
175
|
+
</ac:structured-macro>
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
### Info/warning panels
|
|
179
|
+
|
|
180
|
+
\`\`\`xhtml
|
|
181
|
+
<ac:structured-macro ac:name="info">
|
|
182
|
+
<ac:rich-text-body>
|
|
183
|
+
<p>This is an informational note.</p>
|
|
184
|
+
</ac:rich-text-body>
|
|
185
|
+
</ac:structured-macro>
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Rules
|
|
191
|
+
|
|
192
|
+
### MANDATORY
|
|
193
|
+
|
|
194
|
+
- Work **only through files and git** — edit \`.xhtml\` files, then \`git commit\`
|
|
195
|
+
- Create/edit \`.xhtml\` files in the correct folder under \`docs/${space}/\`
|
|
196
|
+
- Commit your changes — synchronization is automatic (post-commit hook)
|
|
197
|
+
- Follow valid XHTML format (Confluence Storage Format)
|
|
198
|
+
|
|
199
|
+
### FORBIDDEN
|
|
200
|
+
|
|
201
|
+
- **DO NOT** touch the \`.confluence/\` folder or any files inside it
|
|
202
|
+
- **DO NOT** call the Confluence API directly
|
|
203
|
+
- **DO NOT** bypass the sync flow
|
|
204
|
+
- **DO NOT** modify git hooks (\`.git/hooks/\`)
|
|
205
|
+
- **DO NOT** commit \`.confluence/config.json\` (it contains credentials)
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## How to create a new page
|
|
210
|
+
|
|
211
|
+
1. Identify the parent folder where the page should appear in the Confluence hierarchy
|
|
212
|
+
2. Create a folder with the page title: \`docs/${space}/{parent}/New Page/\`
|
|
213
|
+
3. Create a file with the same name: \`docs/${space}/{parent}/New Page/New Page.xhtml\`
|
|
214
|
+
4. Write content in XHTML format (see examples above)
|
|
215
|
+
5. Stage and commit:
|
|
216
|
+
\`\`\`
|
|
217
|
+
git add "docs/${space}/{parent}/New Page/"
|
|
218
|
+
git commit -m "Add page: New Page"
|
|
219
|
+
\`\`\`
|
|
220
|
+
6. The page will be automatically created in Confluence
|
|
221
|
+
|
|
222
|
+
## How to edit a page
|
|
223
|
+
|
|
224
|
+
1. Find the \`.xhtml\` file for the page you want to edit
|
|
225
|
+
2. Modify the content (keep valid XHTML)
|
|
226
|
+
3. Commit:
|
|
227
|
+
\`\`\`
|
|
228
|
+
git add "docs/${space}/path/to/Page.xhtml"
|
|
229
|
+
git commit -m "Update page: Page"
|
|
230
|
+
\`\`\`
|
|
231
|
+
4. Changes will be automatically pushed to Confluence
|
|
232
|
+
|
|
233
|
+
## How to hide (virtually delete) a page
|
|
234
|
+
|
|
235
|
+
Hiding removes the page from your local tree without deleting it from Confluence.
|
|
236
|
+
|
|
237
|
+
1. Delete the folder/file from the file system
|
|
238
|
+
2. Commit:
|
|
239
|
+
\`\`\`
|
|
240
|
+
git add -A
|
|
241
|
+
git commit -m "Hide page: Page Name"
|
|
242
|
+
\`\`\`
|
|
243
|
+
3. The file disappears locally, but the Confluence page remains intact
|
|
244
|
+
4. The page will not be pulled back on future syncs
|
|
245
|
+
5. To restore: set \`hidden: false\` for the page in \`.confluence/tree.json\` — this is the **only** allowed manual edit of \`.confluence/\` files — the page will reappear on next sync
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Current page tree
|
|
250
|
+
|
|
251
|
+
\`\`\`
|
|
252
|
+
${treeBlock}
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
*Generated by confluence-sync*
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate and write AGENTS.md to the project root.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} projectRoot - absolute path to project root
|
|
265
|
+
* @param {object} config - parsed config (baseUrl/url, space, token)
|
|
266
|
+
* @param {object} tree - parsed tree.json (space, pages, lastSync, etc.)
|
|
267
|
+
*/
|
|
268
|
+
export function generateAgentsMd(projectRoot, config, tree) {
|
|
269
|
+
const content = buildContent(config, tree);
|
|
270
|
+
const filePath = path.join(projectRoot, 'AGENTS.md');
|
|
271
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
272
|
+
return filePath;
|
|
273
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
|
|
4
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
5
|
+
|
|
6
|
+
function request(baseUrl, token, method, path, body = null) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const url = new URL(path, baseUrl);
|
|
9
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
method,
|
|
13
|
+
hostname: url.hostname,
|
|
14
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
15
|
+
path: url.pathname + url.search,
|
|
16
|
+
headers: {
|
|
17
|
+
'Authorization': `Bearer ${token}`,
|
|
18
|
+
'Accept': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const payload = body !== null ? JSON.stringify(body) : null;
|
|
23
|
+
|
|
24
|
+
if (payload !== null) {
|
|
25
|
+
options.headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
26
|
+
options.headers['Content-Length'] = Buffer.byteLength(payload);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const req = transport.request(options, (res) => {
|
|
30
|
+
const chunks = [];
|
|
31
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
const buffer = Buffer.concat(chunks);
|
|
34
|
+
|
|
35
|
+
if (res.statusCode >= 400) {
|
|
36
|
+
const text = buffer.toString('utf-8');
|
|
37
|
+
let message;
|
|
38
|
+
try {
|
|
39
|
+
const json = JSON.parse(text);
|
|
40
|
+
message = json.message || json.errorMessage || text;
|
|
41
|
+
} catch {
|
|
42
|
+
message = text;
|
|
43
|
+
}
|
|
44
|
+
reject(new Error(`Confluence API error ${res.statusCode}: ${message}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resolve({ statusCode: res.statusCode, buffer, headers: res.headers });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
|
|
54
|
+
if (payload !== null) {
|
|
55
|
+
req.write(payload);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
req.end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function jsonRequest(baseUrl, token, method, path, body = null) {
|
|
63
|
+
const { buffer } = await request(baseUrl, token, method, path, body);
|
|
64
|
+
const text = buffer.toString('utf-8');
|
|
65
|
+
if (!text) return null;
|
|
66
|
+
return JSON.parse(text);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createApiClient(config) {
|
|
70
|
+
const { baseUrl, token } = config;
|
|
71
|
+
|
|
72
|
+
async function getSpaces() {
|
|
73
|
+
const data = await jsonRequest(baseUrl, token, 'GET', '/rest/api/space');
|
|
74
|
+
return data.results;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getPageTree(spaceKey) {
|
|
78
|
+
const pages = {};
|
|
79
|
+
let start = 0;
|
|
80
|
+
const limit = 500;
|
|
81
|
+
|
|
82
|
+
while (true) {
|
|
83
|
+
const data = await jsonRequest(
|
|
84
|
+
baseUrl, token, 'GET',
|
|
85
|
+
`/rest/api/content?spaceKey=${encodeURIComponent(spaceKey)}&expand=ancestors,version&limit=${limit}&start=${start}`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (!data.results || data.results.length === 0) break;
|
|
89
|
+
|
|
90
|
+
for (const page of data.results) {
|
|
91
|
+
const ancestors = page.ancestors || [];
|
|
92
|
+
const parentId = ancestors.length > 0
|
|
93
|
+
? String(ancestors[ancestors.length - 1].id)
|
|
94
|
+
: null;
|
|
95
|
+
|
|
96
|
+
pages[String(page.id)] = {
|
|
97
|
+
title: page.title,
|
|
98
|
+
parentId,
|
|
99
|
+
version: page.version.number,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (data.size + data.start >= data.totalSize) break;
|
|
104
|
+
start += data.results.length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return pages;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getPageContent(pageId) {
|
|
111
|
+
const data = await jsonRequest(
|
|
112
|
+
baseUrl, token, 'GET',
|
|
113
|
+
`/rest/api/content/${pageId}?expand=body.storage,version`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
title: data.title,
|
|
118
|
+
body: data.body?.storage?.value || '',
|
|
119
|
+
version: data.version?.number || 0,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getPageAttachments(pageId) {
|
|
124
|
+
const data = await jsonRequest(
|
|
125
|
+
baseUrl, token, 'GET',
|
|
126
|
+
`/rest/api/content/${pageId}/child/attachment`
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return (data.results || []).map((att) => ({
|
|
130
|
+
id: String(att.id),
|
|
131
|
+
title: att.title,
|
|
132
|
+
version: att.version?.number || 0,
|
|
133
|
+
downloadPath: att._links?.download || null,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function createPage(spaceKey, parentId, title, body) {
|
|
138
|
+
const payload = {
|
|
139
|
+
type: 'page',
|
|
140
|
+
title,
|
|
141
|
+
space: { key: spaceKey },
|
|
142
|
+
body: {
|
|
143
|
+
storage: {
|
|
144
|
+
value: body,
|
|
145
|
+
representation: 'storage',
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (parentId) {
|
|
151
|
+
payload.ancestors = [{ id: String(parentId) }];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const data = await jsonRequest(baseUrl, token, 'POST', '/rest/api/content', payload);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id: String(data.id),
|
|
158
|
+
version: data.version.number,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function updatePage(pageId, title, body, currentVersion) {
|
|
163
|
+
const payload = {
|
|
164
|
+
type: 'page',
|
|
165
|
+
title,
|
|
166
|
+
version: { number: currentVersion + 1 },
|
|
167
|
+
body: {
|
|
168
|
+
storage: {
|
|
169
|
+
value: body,
|
|
170
|
+
representation: 'storage',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const data = await jsonRequest(
|
|
176
|
+
baseUrl, token, 'PUT',
|
|
177
|
+
`/rest/api/content/${pageId}`,
|
|
178
|
+
payload
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: String(data.id),
|
|
183
|
+
version: data.version.number,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function downloadAttachment(downloadPath) {
|
|
188
|
+
const { buffer } = await request(baseUrl, token, 'GET', downloadPath);
|
|
189
|
+
return buffer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function deletePage(pageId) {
|
|
193
|
+
await request(baseUrl, token, 'DELETE', `/rest/api/content/${pageId}`);
|
|
194
|
+
return { deleted: true, pageId: String(pageId) };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
getSpaces,
|
|
199
|
+
getPageTree,
|
|
200
|
+
getPageContent,
|
|
201
|
+
getPageAttachments,
|
|
202
|
+
createPage,
|
|
203
|
+
updatePage,
|
|
204
|
+
deletePage,
|
|
205
|
+
downloadAttachment,
|
|
206
|
+
};
|
|
207
|
+
}
|