confluence-cli 1.4.1 → 1.6.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/CHANGELOG.md +14 -0
- package/README.md +83 -66
- package/bin/confluence.js +128 -3
- package/examples/copy-tree-example.sh +117 -0
- package/examples/create-child-page-example.sh +39 -37
- package/lib/confluence-client.js +260 -9
- package/llms.txt +46 -0
- package/package.json +1 -1
- package/tests/confluence-client.test.js +68 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.6.0](https://github.com/pchuri/confluence-cli/compare/v1.5.0...v1.6.0) (2025-09-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Add copy-tree command for recursive page copying with children ([#9](https://github.com/pchuri/confluence-cli/issues/9)) ([29efa5b](https://github.com/pchuri/confluence-cli/commit/29efa5b2f8edeee1c5072ad8d7077f38f860c2ba))
|
|
7
|
+
|
|
8
|
+
# [1.5.0](https://github.com/pchuri/confluence-cli/compare/v1.4.1...v1.5.0) (2025-08-13)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* Align README with implementation and fix update command ([#7](https://github.com/pchuri/confluence-cli/issues/7)) ([87f48e0](https://github.com/pchuri/confluence-cli/commit/87f48e03c6310bb9bfc7fda2930247c0d61414ec))
|
|
14
|
+
|
|
1
15
|
## [1.4.1](https://github.com/pchuri/confluence-cli/compare/v1.4.0...v1.4.1) (2025-06-30)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -78,103 +78,114 @@ export CONFLUENCE_API_TOKEN="your-api-token"
|
|
|
78
78
|
# Read by page ID
|
|
79
79
|
confluence read 123456789
|
|
80
80
|
|
|
81
|
-
# Read in
|
|
82
|
-
confluence read 123456789 --format
|
|
81
|
+
# Read in markdown format
|
|
82
|
+
confluence read 123456789 --format markdown
|
|
83
83
|
|
|
84
|
-
# Read by URL
|
|
85
|
-
confluence read "https://your-domain.atlassian.net/wiki/
|
|
84
|
+
# Read by URL (must contain pageId parameter)
|
|
85
|
+
confluence read "https://your-domain.atlassian.net/wiki/viewpage.action?pageId=123456789"
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### Get Page Information
|
|
89
89
|
```bash
|
|
90
|
-
|
|
91
|
-
confluence create "My New Page" SPACEKEY --content "This is my page content"
|
|
92
|
-
|
|
93
|
-
# Create from a file
|
|
94
|
-
confluence create "Documentation" SPACEKEY --file ./content.md --format markdown
|
|
95
|
-
|
|
96
|
-
# Create with HTML content
|
|
97
|
-
confluence create "Rich Content" SPACEKEY --file ./content.html --format html
|
|
98
|
-
|
|
99
|
-
# Create with Storage format (Confluence native)
|
|
100
|
-
confluence create "Advanced Page" SPACEKEY --file ./content.xml --format storage
|
|
90
|
+
confluence info 123456789
|
|
101
91
|
```
|
|
102
92
|
|
|
103
|
-
###
|
|
93
|
+
### Search Pages
|
|
104
94
|
```bash
|
|
105
|
-
#
|
|
106
|
-
confluence
|
|
107
|
-
|
|
108
|
-
# Create child page with inline content
|
|
109
|
-
confluence create-child "Meeting Notes" 123456789 --content "This is a child page"
|
|
95
|
+
# Basic search
|
|
96
|
+
confluence search "search term"
|
|
110
97
|
|
|
111
|
-
#
|
|
112
|
-
confluence
|
|
98
|
+
# Limit results
|
|
99
|
+
confluence search "search term" --limit 5
|
|
100
|
+
```
|
|
113
101
|
|
|
114
|
-
|
|
115
|
-
|
|
102
|
+
### List Spaces
|
|
103
|
+
```bash
|
|
104
|
+
confluence spaces
|
|
116
105
|
```
|
|
117
106
|
|
|
118
|
-
### Find
|
|
107
|
+
### Find a Page by Title
|
|
119
108
|
```bash
|
|
120
109
|
# Find page by title
|
|
121
110
|
confluence find "Project Documentation"
|
|
122
111
|
|
|
123
|
-
# Find page by title in specific space
|
|
112
|
+
# Find page by title in a specific space
|
|
124
113
|
confluence find "Project Documentation" --space MYTEAM
|
|
125
114
|
```
|
|
126
115
|
|
|
127
|
-
###
|
|
116
|
+
### Create a New Page
|
|
128
117
|
```bash
|
|
129
|
-
#
|
|
130
|
-
confluence
|
|
118
|
+
# Create with inline content and markdown format
|
|
119
|
+
confluence create "My New Page" SPACEKEY --content "**Hello** World!" --format markdown
|
|
131
120
|
|
|
132
|
-
#
|
|
133
|
-
confluence
|
|
121
|
+
# Create from a file
|
|
122
|
+
confluence create "Documentation" SPACEKEY --file ./content.md --format markdown
|
|
123
|
+
```
|
|
134
124
|
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
### Create a Child Page
|
|
126
|
+
```bash
|
|
127
|
+
# Create child page with inline content
|
|
128
|
+
confluence create-child "Meeting Notes" 123456789 --content "This is a child page"
|
|
137
129
|
|
|
138
|
-
#
|
|
139
|
-
confluence
|
|
130
|
+
# Create child page from a file
|
|
131
|
+
confluence create-child "Tech Specs" 123456789 --file ./specs.md --format markdown
|
|
140
132
|
```
|
|
141
133
|
|
|
142
|
-
###
|
|
134
|
+
### Copy Page Tree
|
|
143
135
|
```bash
|
|
144
|
-
#
|
|
145
|
-
confluence
|
|
136
|
+
# Copy a page and all its children to a new location
|
|
137
|
+
confluence copy-tree 123456789 987654321 "Project Docs (Copy)"
|
|
146
138
|
|
|
147
|
-
#
|
|
148
|
-
|
|
139
|
+
# Copy with maximum depth limit (only 3 levels deep)
|
|
140
|
+
confluence copy-tree 123456789 987654321 --max-depth 3
|
|
149
141
|
|
|
150
|
-
#
|
|
151
|
-
confluence
|
|
152
|
-
```
|
|
142
|
+
# Exclude pages by title (supports wildcards * and ?; case-insensitive)
|
|
143
|
+
confluence copy-tree 123456789 987654321 --exclude "temp*,test*,*draft*"
|
|
153
144
|
|
|
154
|
-
#
|
|
155
|
-
confluence
|
|
145
|
+
# Control pacing and naming
|
|
146
|
+
confluence copy-tree 123456789 987654321 --delay-ms 150 --copy-suffix " (Backup)"
|
|
156
147
|
|
|
157
|
-
#
|
|
158
|
-
confluence
|
|
159
|
-
```
|
|
148
|
+
# Dry run (preview only)
|
|
149
|
+
confluence copy-tree 123456789 987654321 --dry-run
|
|
160
150
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
confluence info 123456789
|
|
151
|
+
# Quiet mode (suppress progress output)
|
|
152
|
+
confluence copy-tree 123456789 987654321 --quiet
|
|
164
153
|
```
|
|
165
154
|
|
|
166
|
-
|
|
155
|
+
Notes:
|
|
156
|
+
- Preserves the original parent-child hierarchy when copying.
|
|
157
|
+
- Continues on errors: failed pages are logged and the copy proceeds.
|
|
158
|
+
- Exclude patterns use simple globbing: `*` matches any sequence, `?` matches any single character, and special regex characters are treated literally.
|
|
159
|
+
- Large trees may take time; the CLI applies a small delay between sibling page creations to avoid rate limits (configurable via `--delay-ms`).
|
|
160
|
+
- Root title suffix defaults to ` (Copy)`; override with `--copy-suffix`. Child pages keep their original titles.
|
|
161
|
+
- Use `--fail-on-error` to exit non-zero if any page fails to copy.
|
|
162
|
+
|
|
163
|
+
### Update an Existing Page
|
|
167
164
|
```bash
|
|
168
|
-
#
|
|
169
|
-
confluence
|
|
165
|
+
# Update title only
|
|
166
|
+
confluence update 123456789 --title "A Newer Title for the Page"
|
|
170
167
|
|
|
171
|
-
#
|
|
172
|
-
confluence
|
|
168
|
+
# Update content only from a string
|
|
169
|
+
confluence update 123456789 --content "Updated page content."
|
|
170
|
+
|
|
171
|
+
# Update content from a file
|
|
172
|
+
confluence update 123456789 --file ./updated-content.md --format markdown
|
|
173
|
+
|
|
174
|
+
# Update both title and content
|
|
175
|
+
confluence update 123456789 --title "New Title" --content "And new content"
|
|
173
176
|
```
|
|
174
177
|
|
|
175
|
-
###
|
|
178
|
+
### Edit Workflow
|
|
179
|
+
The `edit` and `update` commands work together to create a seamless editing workflow.
|
|
176
180
|
```bash
|
|
177
|
-
|
|
181
|
+
# 1. Export page content to a file (in Confluence storage format)
|
|
182
|
+
confluence edit 123456789 --output ./page-to-edit.xml
|
|
183
|
+
|
|
184
|
+
# 2. Edit the file with your preferred editor
|
|
185
|
+
vim ./page-to-edit.xml
|
|
186
|
+
|
|
187
|
+
# 3. Update the page with your changes
|
|
188
|
+
confluence update 123456789 --file ./page-to-edit.xml --format storage
|
|
178
189
|
```
|
|
179
190
|
|
|
180
191
|
### View Usage Statistics
|
|
@@ -185,13 +196,19 @@ confluence stats
|
|
|
185
196
|
## Commands
|
|
186
197
|
|
|
187
198
|
| Command | Description | Options |
|
|
188
|
-
|
|
189
|
-
| `init` | Initialize CLI configuration |
|
|
190
|
-
| `read <
|
|
191
|
-
| `info <
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `init` | Initialize CLI configuration | |
|
|
201
|
+
| `read <pageId_or_url>` | Read page content | `--format <html\|text\|markdown>` |
|
|
202
|
+
| `info <pageId_or_url>` | Get page information | |
|
|
192
203
|
| `search <query>` | Search for pages | `--limit <number>` |
|
|
193
|
-
| `spaces` | List all spaces |
|
|
194
|
-
| `
|
|
204
|
+
| `spaces` | List all available spaces | |
|
|
205
|
+
| `find <title>` | Find a page by its title | `--space <spaceKey>` |
|
|
206
|
+
| `create <title> <spaceKey>` | Create a new page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>`|
|
|
207
|
+
| `create-child <title> <parentId>` | Create a child page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
|
|
208
|
+
| `copy-tree <sourcePageId> <targetParentId> [newTitle]` | Copy page tree with all children | `--max-depth <number>`, `--exclude <patterns>`, `--delay-ms <ms>`, `--copy-suffix <text>`, `--dry-run`, `--fail-on-error`, `--quiet` |
|
|
209
|
+
| `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
|
|
210
|
+
| `edit <pageId>` | Export page content for editing | `--output <file>` |
|
|
211
|
+
| `stats` | View your usage statistics | |
|
|
195
212
|
|
|
196
213
|
## Examples
|
|
197
214
|
|
package/bin/confluence.js
CHANGED
|
@@ -238,10 +238,15 @@ program
|
|
|
238
238
|
.action(async (pageId, options) => {
|
|
239
239
|
const analytics = new Analytics();
|
|
240
240
|
try {
|
|
241
|
+
// Check if at least one option is provided
|
|
242
|
+
if (!options.title && !options.file && !options.content) {
|
|
243
|
+
throw new Error('At least one of --title, --file, or --content must be provided.');
|
|
244
|
+
}
|
|
245
|
+
|
|
241
246
|
const config = getConfig();
|
|
242
247
|
const client = new ConfluenceClient(config);
|
|
243
248
|
|
|
244
|
-
let content =
|
|
249
|
+
let content = null; // Use null to indicate no content change
|
|
245
250
|
|
|
246
251
|
if (options.file) {
|
|
247
252
|
const fs = require('fs');
|
|
@@ -251,8 +256,6 @@ program
|
|
|
251
256
|
content = fs.readFileSync(options.file, 'utf8');
|
|
252
257
|
} else if (options.content) {
|
|
253
258
|
content = options.content;
|
|
254
|
-
} else {
|
|
255
|
-
throw new Error('Either --file or --content option is required');
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
const result = await client.updatePage(pageId, options.title, content, options.format);
|
|
@@ -334,4 +337,126 @@ program
|
|
|
334
337
|
}
|
|
335
338
|
});
|
|
336
339
|
|
|
340
|
+
// Copy page tree command
|
|
341
|
+
program
|
|
342
|
+
.command('copy-tree <sourcePageId> <targetParentId> [newTitle]')
|
|
343
|
+
.description('Copy a page and all its children to a new location')
|
|
344
|
+
.option('--max-depth <depth>', 'Maximum depth to copy (default: 10)', '10')
|
|
345
|
+
.option('--exclude <patterns>', 'Comma-separated patterns to exclude (supports wildcards)')
|
|
346
|
+
.option('--delay-ms <ms>', 'Delay between sibling creations in ms (default: 100)', '100')
|
|
347
|
+
.option('--copy-suffix <suffix>', 'Suffix for new root title (default: " (Copy)")', ' (Copy)')
|
|
348
|
+
.option('-n, --dry-run', 'Preview operations without creating pages')
|
|
349
|
+
.option('--fail-on-error', 'Exit with non-zero code if any page fails')
|
|
350
|
+
.option('-q, --quiet', 'Suppress progress output')
|
|
351
|
+
.action(async (sourcePageId, targetParentId, newTitle, options) => {
|
|
352
|
+
const analytics = new Analytics();
|
|
353
|
+
try {
|
|
354
|
+
const config = getConfig();
|
|
355
|
+
const client = new ConfluenceClient(config);
|
|
356
|
+
|
|
357
|
+
// Parse numeric flags with safe fallbacks
|
|
358
|
+
const parsedDepth = parseInt(options.maxDepth, 10);
|
|
359
|
+
const maxDepth = Number.isNaN(parsedDepth) ? 10 : parsedDepth;
|
|
360
|
+
const parsedDelay = parseInt(options.delayMs, 10);
|
|
361
|
+
const delayMs = Number.isNaN(parsedDelay) ? 100 : parsedDelay;
|
|
362
|
+
const copySuffix = options.copySuffix ?? ' (Copy)';
|
|
363
|
+
|
|
364
|
+
console.log(chalk.blue('🚀 Starting page tree copy...'));
|
|
365
|
+
console.log(`Source: ${sourcePageId}`);
|
|
366
|
+
console.log(`Target parent: ${targetParentId}`);
|
|
367
|
+
if (newTitle) console.log(`New root title: ${newTitle}`);
|
|
368
|
+
console.log(`Max depth: ${maxDepth}`);
|
|
369
|
+
console.log(`Delay: ${delayMs} ms`);
|
|
370
|
+
if (copySuffix) console.log(`Root suffix: ${copySuffix}`);
|
|
371
|
+
console.log('');
|
|
372
|
+
|
|
373
|
+
// Parse exclude patterns
|
|
374
|
+
let excludePatterns = [];
|
|
375
|
+
if (options.exclude) {
|
|
376
|
+
excludePatterns = options.exclude.split(',').map(p => p.trim()).filter(Boolean);
|
|
377
|
+
if (excludePatterns.length > 0) {
|
|
378
|
+
console.log(chalk.yellow(`Exclude patterns: ${excludePatterns.join(', ')}`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Progress callback
|
|
383
|
+
const onProgress = (message) => {
|
|
384
|
+
console.log(message);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Dry-run: compute plan without creating anything
|
|
388
|
+
if (options.dryRun) {
|
|
389
|
+
const info = await client.getPageInfo(sourcePageId);
|
|
390
|
+
const rootTitle = newTitle || `${info.title}${copySuffix}`;
|
|
391
|
+
const descendants = await client.getAllDescendantPages(sourcePageId, maxDepth);
|
|
392
|
+
const filtered = descendants.filter(p => !client.shouldExcludePage(p.title, excludePatterns));
|
|
393
|
+
console.log(chalk.yellow('Dry run: no changes will be made.'));
|
|
394
|
+
console.log(`Would create root: ${chalk.blue(rootTitle)} (under parent ${targetParentId})`);
|
|
395
|
+
console.log(`Would create ${filtered.length} child page(s)`);
|
|
396
|
+
// Show a preview list (first 50)
|
|
397
|
+
const tree = client.buildPageTree(filtered, sourcePageId);
|
|
398
|
+
const lines = [];
|
|
399
|
+
const walk = (nodes, depth = 0) => {
|
|
400
|
+
for (const n of nodes) {
|
|
401
|
+
if (lines.length >= 50) return; // limit output
|
|
402
|
+
lines.push(`${' '.repeat(depth)}- ${n.title}`);
|
|
403
|
+
if (n.children && n.children.length) walk(n.children, depth + 1);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
walk(tree);
|
|
407
|
+
if (lines.length) {
|
|
408
|
+
console.log('Planned children:');
|
|
409
|
+
lines.forEach(l => console.log(l));
|
|
410
|
+
if (filtered.length > lines.length) {
|
|
411
|
+
console.log(`...and ${filtered.length - lines.length} more`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
analytics.track('copy_tree_dry_run', true);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Copy the page tree
|
|
419
|
+
const result = await client.copyPageTree(
|
|
420
|
+
sourcePageId,
|
|
421
|
+
targetParentId,
|
|
422
|
+
newTitle,
|
|
423
|
+
{
|
|
424
|
+
maxDepth,
|
|
425
|
+
excludePatterns,
|
|
426
|
+
onProgress: options.quiet ? null : onProgress,
|
|
427
|
+
quiet: options.quiet,
|
|
428
|
+
delayMs,
|
|
429
|
+
copySuffix
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
console.log('');
|
|
434
|
+
console.log(chalk.green('✅ Page tree copy completed'));
|
|
435
|
+
console.log(`Root page: ${chalk.blue(result.rootPage.title)} (ID: ${result.rootPage.id})`);
|
|
436
|
+
console.log(`Total copied pages: ${chalk.blue(result.totalCopied)}`);
|
|
437
|
+
if (result.failures?.length) {
|
|
438
|
+
console.log(chalk.yellow(`Failures: ${result.failures.length}`));
|
|
439
|
+
result.failures.slice(0, 10).forEach(f => {
|
|
440
|
+
const reason = f.status ? `${f.status}` : '';
|
|
441
|
+
console.log(` - ${f.title} (ID: ${f.id})${reason ? `: ${reason}` : ''}`);
|
|
442
|
+
});
|
|
443
|
+
if (result.failures.length > 10) {
|
|
444
|
+
console.log(` - ...and ${result.failures.length - 10} more`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
console.log(`URL: ${chalk.gray(`https://${config.domain}/wiki${result.rootPage._links.webui}`)}`);
|
|
448
|
+
if (options.failOnError && result.failures?.length) {
|
|
449
|
+
analytics.track('copy_tree', false);
|
|
450
|
+
console.error(chalk.red('Completed with failures and --fail-on-error is set.'));
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
analytics.track('copy_tree', true);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
analytics.track('copy_tree', false);
|
|
457
|
+
console.error(chalk.red('Error:'), error.message);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
337
462
|
program.parse();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Confluence CLI - Copy Page Tree Example
|
|
4
|
+
# This script shows how to copy a page and all its descendants to a new location.
|
|
5
|
+
|
|
6
|
+
echo "📋 Confluence CLI - Copy Page Tree Example"
|
|
7
|
+
echo "=================================================="
|
|
8
|
+
|
|
9
|
+
# Prerequisites
|
|
10
|
+
echo ""
|
|
11
|
+
echo "📝 Prerequisites:"
|
|
12
|
+
echo "- confluence CLI is set up (confluence init)"
|
|
13
|
+
echo "- You have access to source and target locations"
|
|
14
|
+
echo "- You have permissions to create pages"
|
|
15
|
+
echo ""
|
|
16
|
+
|
|
17
|
+
# Step 1: Find the source page
|
|
18
|
+
echo "1️⃣ Find the source page"
|
|
19
|
+
echo "=============================="
|
|
20
|
+
echo ""
|
|
21
|
+
echo "Method 1: Find by title"
|
|
22
|
+
echo "confluence find \"Project Docs\" --space MYTEAM"
|
|
23
|
+
echo ""
|
|
24
|
+
echo "Method 2: Search"
|
|
25
|
+
echo "confluence search \"Project\""
|
|
26
|
+
echo ""
|
|
27
|
+
echo "📝 Note the source page ID from the output (e.g., 123456789)"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
# Step 2: Find the target parent page
|
|
31
|
+
echo "2️⃣ Find the target parent page"
|
|
32
|
+
echo "========================="
|
|
33
|
+
echo ""
|
|
34
|
+
echo "confluence find \"Backup\" --space BACKUP"
|
|
35
|
+
echo "or"
|
|
36
|
+
echo "confluence find \"Archive\" --space ARCHIVE"
|
|
37
|
+
echo ""
|
|
38
|
+
echo "📝 Note the target parent page ID (e.g., 987654321)"
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
# Step 3: Run the copy
|
|
42
|
+
echo "3️⃣ Run copy"
|
|
43
|
+
echo "========================"
|
|
44
|
+
echo ""
|
|
45
|
+
|
|
46
|
+
echo "📄 Basic: copy with all children"
|
|
47
|
+
echo 'confluence copy-tree 123456789 987654321 "Project Docs (Backup)"'
|
|
48
|
+
echo ""
|
|
49
|
+
|
|
50
|
+
echo "📄 Depth-limited (3 levels)"
|
|
51
|
+
echo 'confluence copy-tree 123456789 987654321 "Project Docs (Summary)" --max-depth 3'
|
|
52
|
+
echo ""
|
|
53
|
+
|
|
54
|
+
echo "📄 Exclude patterns"
|
|
55
|
+
echo 'confluence copy-tree 123456789 987654321 "Project Docs (Clean)" --exclude "temp*,test*,*draft*"'
|
|
56
|
+
echo ""
|
|
57
|
+
|
|
58
|
+
echo "📄 Quiet mode"
|
|
59
|
+
echo 'confluence copy-tree 123456789 987654321 --quiet'
|
|
60
|
+
echo ""
|
|
61
|
+
|
|
62
|
+
echo "📄 Control pacing and naming"
|
|
63
|
+
echo 'confluence copy-tree 123456789 987654321 --delay-ms 150 --copy-suffix " (Backup)"'
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
# Practical example
|
|
67
|
+
echo "💡 Practical example"
|
|
68
|
+
echo "================="
|
|
69
|
+
echo ""
|
|
70
|
+
echo "# 1. Capture source page ID"
|
|
71
|
+
echo 'SOURCE_ID=$(confluence find "Project Docs" --space MYTEAM | grep "ID:" | awk "{print \$2}")'
|
|
72
|
+
echo ""
|
|
73
|
+
echo "# 2. Capture target parent ID"
|
|
74
|
+
echo 'TARGET_ID=$(confluence find "Backup Folder" --space BACKUP | grep "ID:" | awk "{print \$2}")'
|
|
75
|
+
echo ""
|
|
76
|
+
echo "# 3. Run backup with date suffix"
|
|
77
|
+
echo 'confluence copy-tree $SOURCE_ID $TARGET_ID "Project Docs Backup - $(date +%Y%m%d)"'
|
|
78
|
+
echo ""
|
|
79
|
+
|
|
80
|
+
# Advanced usage
|
|
81
|
+
echo "🚀 Advanced"
|
|
82
|
+
echo "============="
|
|
83
|
+
echo ""
|
|
84
|
+
echo "1. Large trees with progress"
|
|
85
|
+
echo " confluence copy-tree 123456789 987654321 | tee copy-log.txt"
|
|
86
|
+
echo ""
|
|
87
|
+
echo "2. Multiple exclude patterns"
|
|
88
|
+
echo " confluence copy-tree 123456789 987654321 --exclude \"temp*,test*,*draft*,*temp*\""
|
|
89
|
+
echo ""
|
|
90
|
+
echo "3. Shallow copy (only direct children)"
|
|
91
|
+
echo " confluence copy-tree 123456789 987654321 --max-depth 1"
|
|
92
|
+
echo ""
|
|
93
|
+
|
|
94
|
+
# Notes and tips
|
|
95
|
+
echo "⚠️ Notes and tips"
|
|
96
|
+
echo "=================="
|
|
97
|
+
echo "- Large trees may take time to copy"
|
|
98
|
+
echo "- A short delay between siblings helps avoid rate limits (tune with --delay-ms)"
|
|
99
|
+
echo "- Partial copies can remain if errors occur"
|
|
100
|
+
echo "- Pages without permission are skipped; run with --fail-on-error to fail the run"
|
|
101
|
+
echo "- Validate links and references after copying"
|
|
102
|
+
echo "- Try with a small tree first"
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
echo "📊 Verify results"
|
|
106
|
+
echo "================"
|
|
107
|
+
echo "After completion, you can check the results:"
|
|
108
|
+
echo ""
|
|
109
|
+
echo "# Root page info"
|
|
110
|
+
echo "confluence info [NEW_PAGE_ID]"
|
|
111
|
+
echo ""
|
|
112
|
+
echo "# Find copied pages"
|
|
113
|
+
echo "confluence search \"Copy\" --limit 20"
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
echo "✅ Example complete!"
|
|
117
|
+
echo "Replace example IDs with real ones when running."
|
|
@@ -1,65 +1,67 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
#!/bin/bash
|
|
4
|
+
|
|
5
|
+
# Create a test page under the Project Documentation page
|
|
6
|
+
# This script demonstrates typical Confluence CLI usage.
|
|
5
7
|
|
|
6
|
-
echo "🔍
|
|
7
|
-
echo "
|
|
8
|
+
echo "🔍 Create a test page under Project Documentation"
|
|
9
|
+
echo "================================================"
|
|
8
10
|
|
|
9
|
-
# 1
|
|
11
|
+
# Step 1: Find the parent page
|
|
10
12
|
echo ""
|
|
11
|
-
echo "1️⃣
|
|
12
|
-
echo "
|
|
13
|
+
echo "1️⃣ Find the parent page..."
|
|
14
|
+
echo "Run: confluence find \"Project Documentation\" --space MYTEAM"
|
|
13
15
|
echo ""
|
|
14
16
|
|
|
15
|
-
#
|
|
17
|
+
# For real execution, uncomment below
|
|
16
18
|
# confluence find "Project Documentation" --space MYTEAM
|
|
17
19
|
|
|
18
|
-
echo "📝
|
|
20
|
+
echo "📝 Note the page ID from the output (e.g., 123456789)"
|
|
19
21
|
echo ""
|
|
20
22
|
|
|
21
|
-
# 2
|
|
22
|
-
echo "2️⃣
|
|
23
|
-
echo "
|
|
24
|
-
echo "
|
|
23
|
+
# Step 2: Inspect page info
|
|
24
|
+
echo "2️⃣ Inspect page info..."
|
|
25
|
+
echo "Run: confluence info [PAGE_ID]"
|
|
26
|
+
echo "Example: confluence info 123456789"
|
|
25
27
|
echo ""
|
|
26
28
|
|
|
27
|
-
# 3
|
|
28
|
-
echo "3️⃣
|
|
29
|
-
echo "
|
|
30
|
-
echo "
|
|
29
|
+
# Step 3: Read page content (optional)
|
|
30
|
+
echo "3️⃣ Read content (optional)..."
|
|
31
|
+
echo "Run: confluence read [PAGE_ID] | head -20"
|
|
32
|
+
echo "Example: confluence read 123456789 | head -20"
|
|
31
33
|
echo ""
|
|
32
34
|
|
|
33
|
-
# 4
|
|
34
|
-
echo "4️⃣
|
|
35
|
+
# Step 4: Create a child test page
|
|
36
|
+
echo "4️⃣ Create child test page..."
|
|
35
37
|
echo ""
|
|
36
38
|
|
|
37
|
-
#
|
|
38
|
-
echo "📄
|
|
39
|
-
echo 'confluence create-child "Test Page - $(date +%Y%m%d)" [
|
|
39
|
+
# Simple text content
|
|
40
|
+
echo "📄 Option 1: Simple text content"
|
|
41
|
+
echo 'confluence create-child "Test Page - $(date +%Y%m%d)" [PARENT_PAGE_ID] --content "This is a test page created via CLI. Created at: $(date)"'
|
|
40
42
|
echo ""
|
|
41
43
|
|
|
42
|
-
#
|
|
43
|
-
echo "📄
|
|
44
|
-
echo "confluence create-child \"Test Documentation - $(date +%Y%m%d)\" [
|
|
44
|
+
# From Markdown file
|
|
45
|
+
echo "📄 Option 2: From Markdown file"
|
|
46
|
+
echo "confluence create-child \"Test Documentation - $(date +%Y%m%d)\" [PARENT_PAGE_ID] --file ./sample-page.md --format markdown"
|
|
45
47
|
echo ""
|
|
46
48
|
|
|
47
|
-
# HTML
|
|
48
|
-
echo "📄
|
|
49
|
-
echo 'confluence create-child "Test HTML Page" [
|
|
49
|
+
# From HTML content
|
|
50
|
+
echo "📄 Option 3: From HTML content"
|
|
51
|
+
echo 'confluence create-child "Test HTML Page" [PARENT_PAGE_ID] --content "<h1>Test Page</h1><p>This is a <strong>HTML</strong> example page.</p>" --format html'
|
|
50
52
|
echo ""
|
|
51
53
|
|
|
52
|
-
echo "💡
|
|
54
|
+
echo "💡 Practical example:"
|
|
53
55
|
echo "=============================="
|
|
54
|
-
echo "# 1.
|
|
56
|
+
echo "# 1. Get parent page ID"
|
|
55
57
|
echo 'PARENT_ID=$(confluence find "Project Documentation" --space MYTEAM | grep "ID:" | cut -d" " -f2)'
|
|
56
58
|
echo ""
|
|
57
|
-
echo "# 2.
|
|
58
|
-
echo 'confluence create-child "
|
|
59
|
+
echo "# 2. Create test page"
|
|
60
|
+
echo 'confluence create-child "Test Page - $(date +%Y%m%d_%H%M)" $PARENT_ID --content "Page for CLI testing."'
|
|
59
61
|
echo ""
|
|
60
62
|
|
|
61
|
-
echo "⚠️
|
|
62
|
-
echo "- confluence CLI
|
|
63
|
-
echo "-
|
|
64
|
-
echo "-
|
|
65
|
-
echo "-
|
|
63
|
+
echo "⚠️ Notes:"
|
|
64
|
+
echo "- confluence CLI must be set up (confluence init)"
|
|
65
|
+
echo "- You need appropriate permissions on the Confluence instance"
|
|
66
|
+
echo "- Ensure you have page creation permission"
|
|
67
|
+
echo "- Clean up test pages afterward"
|
package/lib/confluence-client.js
CHANGED
|
@@ -552,17 +552,28 @@ class ConfluenceClient {
|
|
|
552
552
|
* Update an existing Confluence page
|
|
553
553
|
*/
|
|
554
554
|
async updatePage(pageId, title, content, format = 'storage') {
|
|
555
|
-
// First, get the current page to get the version number
|
|
556
|
-
const currentPage = await this.client.get(`/content/${pageId}
|
|
555
|
+
// First, get the current page to get the version number and existing content
|
|
556
|
+
const currentPage = await this.client.get(`/content/${pageId}`, {
|
|
557
|
+
params: {
|
|
558
|
+
expand: 'body.storage,version,space'
|
|
559
|
+
}
|
|
560
|
+
});
|
|
557
561
|
const currentVersion = currentPage.data.version.number;
|
|
558
562
|
|
|
559
|
-
let storageContent
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
563
|
+
let storageContent;
|
|
564
|
+
|
|
565
|
+
if (content !== undefined && content !== null) {
|
|
566
|
+
// If new content is provided, convert it to storage format
|
|
567
|
+
if (format === 'markdown') {
|
|
568
|
+
storageContent = this.markdownToStorage(content);
|
|
569
|
+
} else if (format === 'html') {
|
|
570
|
+
storageContent = this.htmlToConfluenceStorage(content); // Using the conversion function for robustness
|
|
571
|
+
} else { // 'storage' format
|
|
572
|
+
storageContent = content;
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
// If no new content, use the existing content
|
|
576
|
+
storageContent = currentPage.data.body.storage.value;
|
|
566
577
|
}
|
|
567
578
|
|
|
568
579
|
const pageData = {
|
|
@@ -637,6 +648,246 @@ class ConfluenceClient {
|
|
|
637
648
|
url: content._links?.webui || ''
|
|
638
649
|
};
|
|
639
650
|
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Get child pages of a given page
|
|
654
|
+
*/
|
|
655
|
+
async getChildPages(pageId, limit = 500) {
|
|
656
|
+
const response = await this.client.get(`/content/${pageId}/child/page`, {
|
|
657
|
+
params: {
|
|
658
|
+
limit: limit,
|
|
659
|
+
// Fetch lightweight payload; content fetched on-demand when copying
|
|
660
|
+
expand: 'space,version'
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return response.data.results.map(page => ({
|
|
665
|
+
id: page.id,
|
|
666
|
+
title: page.title,
|
|
667
|
+
type: page.type,
|
|
668
|
+
status: page.status,
|
|
669
|
+
space: page.space,
|
|
670
|
+
version: page.version?.number || 1
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get all descendant pages recursively
|
|
676
|
+
*/
|
|
677
|
+
async getAllDescendantPages(pageId, maxDepth = 10, currentDepth = 0) {
|
|
678
|
+
if (currentDepth >= maxDepth) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const children = await this.getChildPages(pageId);
|
|
683
|
+
// Attach parentId so we can later reconstruct hierarchy if needed
|
|
684
|
+
const childrenWithParent = children.map(child => ({ ...child, parentId: pageId }));
|
|
685
|
+
let allDescendants = [...childrenWithParent];
|
|
686
|
+
|
|
687
|
+
for (const child of children) {
|
|
688
|
+
const grandChildren = await this.getAllDescendantPages(
|
|
689
|
+
child.id,
|
|
690
|
+
maxDepth,
|
|
691
|
+
currentDepth + 1
|
|
692
|
+
);
|
|
693
|
+
allDescendants = allDescendants.concat(grandChildren);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return allDescendants;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Copy a page tree (page and all its descendants) to a new location
|
|
701
|
+
*/
|
|
702
|
+
async copyPageTree(sourcePageId, targetParentId, newTitle = null, options = {}) {
|
|
703
|
+
const {
|
|
704
|
+
maxDepth = 10,
|
|
705
|
+
excludePatterns = [],
|
|
706
|
+
onProgress = null,
|
|
707
|
+
quiet = false,
|
|
708
|
+
delayMs = 100,
|
|
709
|
+
copySuffix = ' (Copy)'
|
|
710
|
+
} = options;
|
|
711
|
+
|
|
712
|
+
// Get source page information
|
|
713
|
+
const sourcePage = await this.getPageForEdit(sourcePageId);
|
|
714
|
+
const sourceInfo = await this.getPageInfo(sourcePageId);
|
|
715
|
+
|
|
716
|
+
// Determine new title
|
|
717
|
+
const finalTitle = newTitle || `${sourcePage.title}${copySuffix}`;
|
|
718
|
+
|
|
719
|
+
if (!quiet && onProgress) {
|
|
720
|
+
onProgress(`Copying root: ${sourcePage.title} -> ${finalTitle}`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Create the root copied page
|
|
724
|
+
const newRootPage = await this.createChildPage(
|
|
725
|
+
finalTitle,
|
|
726
|
+
sourceInfo.space.key,
|
|
727
|
+
targetParentId,
|
|
728
|
+
sourcePage.content,
|
|
729
|
+
'storage'
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
if (!quiet && onProgress) {
|
|
733
|
+
onProgress(`Root page created: ${newRootPage.title} (ID: ${newRootPage.id})`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const result = {
|
|
737
|
+
rootPage: newRootPage,
|
|
738
|
+
copiedPages: [newRootPage],
|
|
739
|
+
failures: [],
|
|
740
|
+
totalCopied: 1,
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// Precompile exclude patterns once for efficiency
|
|
744
|
+
const compiledExclude = Array.isArray(excludePatterns)
|
|
745
|
+
? excludePatterns.filter(Boolean).map(p => this.globToRegExp(p))
|
|
746
|
+
: [];
|
|
747
|
+
|
|
748
|
+
await this.copyChildrenRecursive(
|
|
749
|
+
sourcePageId,
|
|
750
|
+
newRootPage.id,
|
|
751
|
+
0,
|
|
752
|
+
{
|
|
753
|
+
spaceKey: sourceInfo.space.key,
|
|
754
|
+
maxDepth,
|
|
755
|
+
excludePatterns,
|
|
756
|
+
compiledExclude,
|
|
757
|
+
onProgress,
|
|
758
|
+
quiet,
|
|
759
|
+
delayMs,
|
|
760
|
+
},
|
|
761
|
+
result
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
result.totalCopied = result.copiedPages.length;
|
|
765
|
+
return result;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Build a tree structure from flat array of pages
|
|
770
|
+
*/
|
|
771
|
+
buildPageTree(pages, rootPageId) {
|
|
772
|
+
const pageMap = new Map();
|
|
773
|
+
const tree = [];
|
|
774
|
+
|
|
775
|
+
// Create nodes
|
|
776
|
+
pages.forEach(page => {
|
|
777
|
+
pageMap.set(page.id, { ...page, children: [] });
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Link by parentId if available; otherwise attach to root
|
|
781
|
+
pages.forEach(page => {
|
|
782
|
+
const node = pageMap.get(page.id);
|
|
783
|
+
const parentId = page.parentId;
|
|
784
|
+
if (parentId && pageMap.has(parentId)) {
|
|
785
|
+
pageMap.get(parentId).children.push(node);
|
|
786
|
+
} else if (parentId === rootPageId || !parentId) {
|
|
787
|
+
tree.push(node);
|
|
788
|
+
} else {
|
|
789
|
+
// Parent not present in the list; treat as top-level under root
|
|
790
|
+
tree.push(node);
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
return tree;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Recursively copy pages maintaining hierarchy
|
|
799
|
+
*/
|
|
800
|
+
async copyChildrenRecursive(sourceParentId, targetParentId, currentDepth, opts, result) {
|
|
801
|
+
const { spaceKey, maxDepth, excludePatterns, compiledExclude = [], onProgress, quiet, delayMs = 100 } = opts || {};
|
|
802
|
+
|
|
803
|
+
if (currentDepth >= maxDepth) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const children = await this.getChildPages(sourceParentId);
|
|
808
|
+
for (let i = 0; i < children.length; i++) {
|
|
809
|
+
const child = children[i];
|
|
810
|
+
const patterns = (compiledExclude && compiledExclude.length) ? compiledExclude : excludePatterns;
|
|
811
|
+
if (this.shouldExcludePage(child.title, patterns)) {
|
|
812
|
+
if (!quiet && onProgress) {
|
|
813
|
+
onProgress(`Skipped: ${child.title}`);
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (!quiet && onProgress) {
|
|
819
|
+
onProgress(`Copying: ${child.title}`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
// Fetch full content to ensure complete copy
|
|
824
|
+
const fullChild = await this.getPageForEdit(child.id);
|
|
825
|
+
const newPage = await this.createChildPage(
|
|
826
|
+
fullChild.title,
|
|
827
|
+
spaceKey,
|
|
828
|
+
targetParentId,
|
|
829
|
+
fullChild.content,
|
|
830
|
+
'storage'
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
result.copiedPages.push(newPage);
|
|
834
|
+
if (!quiet && onProgress) {
|
|
835
|
+
onProgress(`Created: ${newPage.title} (ID: ${newPage.id})`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Rate limiting safety: only pause between siblings
|
|
839
|
+
if (delayMs > 0 && i < children.length - 1) {
|
|
840
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Recurse into this child's subtree
|
|
844
|
+
await this.copyChildrenRecursive(child.id, newPage.id, currentDepth + 1, opts, result);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (!quiet && onProgress) {
|
|
847
|
+
const status = error?.response?.status;
|
|
848
|
+
const statusText = error?.response?.statusText;
|
|
849
|
+
const msg = status ? `${status} ${statusText || ''}`.trim() : error.message;
|
|
850
|
+
onProgress(`Failed: ${child.title} - ${msg}`);
|
|
851
|
+
}
|
|
852
|
+
result.failures.push({
|
|
853
|
+
id: child.id,
|
|
854
|
+
title: child.title,
|
|
855
|
+
error: error.message,
|
|
856
|
+
status: error?.response?.status || null
|
|
857
|
+
});
|
|
858
|
+
// Continue with other pages (do not throw)
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Convert a simple glob pattern to a safe RegExp
|
|
866
|
+
* Supports '*' → '.*' and '?' → '.', escapes other regex metacharacters.
|
|
867
|
+
*/
|
|
868
|
+
globToRegExp(pattern, flags = 'i') {
|
|
869
|
+
// Escape regex special characters: . + ^ $ { } ( ) | [ ] \
|
|
870
|
+
// Note: backslash must be escaped properly in string and class contexts
|
|
871
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
872
|
+
const regexPattern = escaped
|
|
873
|
+
.replace(/\*/g, '.*')
|
|
874
|
+
.replace(/\?/g, '.');
|
|
875
|
+
return new RegExp(`^${regexPattern}$`, flags);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Check if a page should be excluded based on patterns
|
|
880
|
+
*/
|
|
881
|
+
shouldExcludePage(title, excludePatterns) {
|
|
882
|
+
if (!excludePatterns || excludePatterns.length === 0) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return excludePatterns.some(pattern => {
|
|
887
|
+
if (pattern instanceof RegExp) return pattern.test(title);
|
|
888
|
+
return this.globToRegExp(pattern).test(title);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
640
891
|
}
|
|
641
892
|
|
|
642
893
|
module.exports = ConfluenceClient;
|
package/llms.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Confluence CLI
|
|
2
|
+
|
|
3
|
+
> A powerful command-line interface for Atlassian Confluence that allows you to read, search, and manage your Confluence content from the terminal.
|
|
4
|
+
|
|
5
|
+
This CLI tool provides a comprehensive set of commands for interacting with Confluence. Key features include creating, reading, updating, and searching for pages. It supports various content formats like markdown and HTML.
|
|
6
|
+
|
|
7
|
+
To get started, install the tool via npm and initialize the configuration:
|
|
8
|
+
```sh
|
|
9
|
+
npm install -g confluence-cli
|
|
10
|
+
confluence init
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
- [README.md](./README.md): Main documentation with installation, usage, and command reference.
|
|
16
|
+
|
|
17
|
+
## Core Source Code
|
|
18
|
+
|
|
19
|
+
- [bin/confluence.js](./bin/confluence.js): The main entry point for the CLI, defines commands and argument parsing.
|
|
20
|
+
- [lib/confluence-client.js](./lib/confluence-client.js): The client library for interacting with the Confluence REST API.
|
|
21
|
+
- [package.json](./package.json): Project metadata, dependencies, and scripts.
|
|
22
|
+
|
|
23
|
+
## Recent Changes & Fixes
|
|
24
|
+
|
|
25
|
+
This section summarizes recent improvements to align the codebase with its documentation and fix bugs.
|
|
26
|
+
|
|
27
|
+
### `update` Command Logic and Documentation:
|
|
28
|
+
- **Inconsistency:** The `README.md` suggested that updating a page's title without changing its content was possible. However, the implementation in `bin/confluence.js` incorrectly threw an error if the `--content` or `--file` flags were not provided, making title-only updates impossible.
|
|
29
|
+
- **Fix:**
|
|
30
|
+
- Modified `updatePage` in `lib/confluence-client.js` to fetch and re-use existing page content if no new content is provided.
|
|
31
|
+
- Adjusted validation in `bin/confluence.js` for the `update` command to only require one of `--title`, `--content`, or `--file`.
|
|
32
|
+
- Updated `README.md` with an example of a title-only update.
|
|
33
|
+
|
|
34
|
+
### Incomplete `README.md` Command Reference:
|
|
35
|
+
- **Inconsistency:** The main command table in `README.md` was missing several commands (`create`, `create-child`, `update`, `edit`, `find`).
|
|
36
|
+
- **Fix:** Expanded the command table in `README.md` to include all available commands.
|
|
37
|
+
|
|
38
|
+
### Misleading `read` Command URL Examples:
|
|
39
|
+
- **Inconsistency:** The documentation for the `read` command used "display" or "pretty" URLs, which are not supported by the `extractPageId` function.
|
|
40
|
+
- **Fix:** Removed incorrect URL examples and clarified that only URLs with a `pageId` query parameter are supported.
|
|
41
|
+
|
|
42
|
+
## Next Steps
|
|
43
|
+
|
|
44
|
+
- Refer to [README.md](./README.md) for detailed usage instructions and advanced configuration.
|
|
45
|
+
- For troubleshooting or to contribute, visit the project's GitHub repository at https://github.com/pchuri/confluence-cli.
|
|
46
|
+
- If you encounter issues, open an issue or check the FAQ section in the documentation.
|
package/package.json
CHANGED
|
@@ -154,4 +154,72 @@ describe('ConfluenceClient', () => {
|
|
|
154
154
|
expect(typeof client.findPageByTitle).toBe('function');
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
describe('page tree operations', () => {
|
|
159
|
+
test('should have required methods for tree operations', () => {
|
|
160
|
+
expect(typeof client.getChildPages).toBe('function');
|
|
161
|
+
expect(typeof client.getAllDescendantPages).toBe('function');
|
|
162
|
+
expect(typeof client.copyPageTree).toBe('function');
|
|
163
|
+
expect(typeof client.buildPageTree).toBe('function');
|
|
164
|
+
expect(typeof client.shouldExcludePage).toBe('function');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should correctly exclude pages based on patterns', () => {
|
|
168
|
+
const patterns = ['temp*', 'test*', '*draft*'];
|
|
169
|
+
|
|
170
|
+
expect(client.shouldExcludePage('temporary document', patterns)).toBe(true);
|
|
171
|
+
expect(client.shouldExcludePage('test page', patterns)).toBe(true);
|
|
172
|
+
expect(client.shouldExcludePage('my draft page', patterns)).toBe(true);
|
|
173
|
+
expect(client.shouldExcludePage('normal document', patterns)).toBe(false);
|
|
174
|
+
expect(client.shouldExcludePage('production page', patterns)).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should handle empty exclude patterns', () => {
|
|
178
|
+
expect(client.shouldExcludePage('any page', [])).toBe(false);
|
|
179
|
+
expect(client.shouldExcludePage('any page', null)).toBe(false);
|
|
180
|
+
expect(client.shouldExcludePage('any page', undefined)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('globToRegExp should escape regex metacharacters and match case-insensitively', () => {
|
|
184
|
+
const patterns = [
|
|
185
|
+
'file.name*', // dot should be literal
|
|
186
|
+
'[draft]?', // brackets should be literal
|
|
187
|
+
'Plan (Q1)?', // parentheses literal, ? wildcard
|
|
188
|
+
'DATA*SET', // case-insensitive
|
|
189
|
+
];
|
|
190
|
+
const rx = patterns.map(p => client.globToRegExp(p));
|
|
191
|
+
expect('file.name.v1').toMatch(rx[0]);
|
|
192
|
+
expect('filexname').not.toMatch(rx[0]);
|
|
193
|
+
expect('[draft]1').toMatch(rx[1]);
|
|
194
|
+
expect('[draft]AB').not.toMatch(rx[1]);
|
|
195
|
+
expect('Plan (Q1)A').toMatch(rx[2]);
|
|
196
|
+
expect('Plan Q1A').not.toMatch(rx[2]);
|
|
197
|
+
expect('data big set').toMatch(rx[3]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('buildPageTree should link children by parentId and collect orphans at root', () => {
|
|
201
|
+
const rootId = 'root';
|
|
202
|
+
const pages = [
|
|
203
|
+
{ id: 'a', title: 'A', parentId: rootId },
|
|
204
|
+
{ id: 'b', title: 'B', parentId: 'a' },
|
|
205
|
+
{ id: 'c', title: 'C', parentId: 'missing' }, // orphan
|
|
206
|
+
];
|
|
207
|
+
const tree = client.buildPageTree(pages, rootId);
|
|
208
|
+
// tree should contain A and C at top-level (B is child of A)
|
|
209
|
+
const topTitles = tree.map(n => n.title).sort();
|
|
210
|
+
expect(topTitles).toEqual(['A', 'C']);
|
|
211
|
+
const a = tree.find(n => n.title === 'A');
|
|
212
|
+
expect(a.children.map(n => n.title)).toEqual(['B']);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('exclude parser should tolerate spaces and empty items', () => {
|
|
216
|
+
const raw = ' temp* , , *draft* ,,test? ';
|
|
217
|
+
const patterns = raw.split(',').map(p => p.trim()).filter(Boolean);
|
|
218
|
+
expect(patterns).toEqual(['temp*', '*draft*', 'test?']);
|
|
219
|
+
expect(client.shouldExcludePage('temp file', patterns)).toBe(true);
|
|
220
|
+
expect(client.shouldExcludePage('my draft page', patterns)).toBe(true);
|
|
221
|
+
expect(client.shouldExcludePage('test1', patterns)).toBe(true);
|
|
222
|
+
expect(client.shouldExcludePage('production', patterns)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
157
225
|
});
|