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 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 HTML format
82
- confluence read 123456789 --format html
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/spaces/SPACE/pages/123456789/Page+Title"
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
- ### Create a New Page
88
+ ### Get Page Information
89
89
  ```bash
90
- # Create with inline content
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
- ### Create a Child Page
93
+ ### Search Pages
104
94
  ```bash
105
- # Find parent page first
106
- confluence find "Project Documentation" --space MYTEAM
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
- # Create child page from file
112
- confluence create-child "Technical Specifications" 123456789 --file ./content.md --format markdown
98
+ # Limit results
99
+ confluence search "search term" --limit 5
100
+ ```
113
101
 
114
- # Create child page with HTML content
115
- confluence create-child "Report Summary" 123456789 --file ./content.html --format html
102
+ ### List Spaces
103
+ ```bash
104
+ confluence spaces
116
105
  ```
117
106
 
118
- ### Find Pages
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
- ### Update an Existing Page
116
+ ### Create a New Page
128
117
  ```bash
129
- # Update content only
130
- confluence update 123456789 --content "Updated content"
118
+ # Create with inline content and markdown format
119
+ confluence create "My New Page" SPACEKEY --content "**Hello** World!" --format markdown
131
120
 
132
- # Update content from file
133
- confluence update 123456789 --file ./updated-content.md --format markdown
121
+ # Create from a file
122
+ confluence create "Documentation" SPACEKEY --file ./content.md --format markdown
123
+ ```
134
124
 
135
- # Update both title and content
136
- confluence update 123456789 --title "New Title" --content "New content"
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
- # Update from HTML file
139
- confluence update 123456789 --file ./content.html --format html
130
+ # Create child page from a file
131
+ confluence create-child "Tech Specs" 123456789 --file ./specs.md --format markdown
140
132
  ```
141
133
 
142
- ### Edit Workflow
134
+ ### Copy Page Tree
143
135
  ```bash
144
- # Export page content for editing
145
- confluence edit 123456789 --output ./page-content.xml
136
+ # Copy a page and all its children to a new location
137
+ confluence copy-tree 123456789 987654321 "Project Docs (Copy)"
146
138
 
147
- # Edit the file with your preferred editor
148
- vim ./page-content.xml
139
+ # Copy with maximum depth limit (only 3 levels deep)
140
+ confluence copy-tree 123456789 987654321 --max-depth 3
149
141
 
150
- # Update the page with your changes
151
- confluence update 123456789 --file ./page-content.xml --format storage
152
- ```
142
+ # Exclude pages by title (supports wildcards * and ?; case-insensitive)
143
+ confluence copy-tree 123456789 987654321 --exclude "temp*,test*,*draft*"
153
144
 
154
- # Read with HTML format
155
- confluence read 123456789 --format html
145
+ # Control pacing and naming
146
+ confluence copy-tree 123456789 987654321 --delay-ms 150 --copy-suffix " (Backup)"
156
147
 
157
- # Read by URL (with pageId parameter)
158
- confluence read "https://yourcompany.atlassian.net/wiki/spaces/SPACE/pages/123456789/Page+Title"
159
- ```
148
+ # Dry run (preview only)
149
+ confluence copy-tree 123456789 987654321 --dry-run
160
150
 
161
- ### Get Page Information
162
- ```bash
163
- confluence info 123456789
151
+ # Quiet mode (suppress progress output)
152
+ confluence copy-tree 123456789 987654321 --quiet
164
153
  ```
165
154
 
166
- ### Search Pages
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
- # Basic search
169
- confluence search "search term"
165
+ # Update title only
166
+ confluence update 123456789 --title "A Newer Title for the Page"
170
167
 
171
- # Limit results
172
- confluence search "search term" --limit 5
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
- ### List Spaces
178
+ ### Edit Workflow
179
+ The `edit` and `update` commands work together to create a seamless editing workflow.
176
180
  ```bash
177
- confluence spaces
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 <pageId>` | Read page content | `--format <html\|text>` |
191
- | `info <pageId>` | Get page information | - |
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
- | `stats` | View your usage statistics | - |
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
- # 실제 Project Documentation 페이지 하위에 테스트 페이지 생성 예제
4
- # 이 스크립트는 일반적인 Confluence 설정에서 작동합니다.
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 "🔍 Project Documentation 페이지 하위에 테스트 페이지 생성"
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 "실행: confluence find \"Project Documentation\" --space MYTEAM"
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 "📝 명령어 결과에서 페이지 ID 확인하세요 (예: 123456789)"
20
+ echo "📝 Note the page ID from the output (e.g., 123456789)"
19
21
  echo ""
20
22
 
21
- # 2단계: 페이지 정보 확인
22
- echo "2️⃣ 페이지 정보 확인..."
23
- echo "실행: confluence info [페이지ID]"
24
- echo "예시: confluence info 123456789"
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 "실행: confluence read [페이지ID] | head -20"
30
- echo "예시: confluence read 123456789 | head -20"
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 "📄 방법 1: 간단한 텍스트 콘텐츠로 생성"
39
- echo 'confluence create-child "Test Page - $(date +%Y%m%d)" [부모페이지ID] --content "이것은 CLI로 생성된 테스트 페이지입니다. 생성 시간: $(date)"'
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 "📄 방법 2: 마크다운 파일에서 생성"
44
- echo "confluence create-child \"Test Documentation - $(date +%Y%m%d)\" [부모페이지ID] --file ./sample-page.md --format markdown"
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 "📄 방법 3: HTML 콘텐츠로 생성"
49
- echo 'confluence create-child "Test HTML Page" [부모페이지ID] --content "<h1>테스트 페이지</h1><p>이것은 <strong>HTML</strong>로 작성된 테스트 페이지입니다.</p>" --format html'
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. 부모 페이지 ID 찾기"
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 "테스트 페이지 - $(date +%Y%m%d_%H%M)" $PARENT_ID --content "CLI 테스트용 페이지입니다."'
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 올바르게 설정되어 있어야 합니다 (confluence init)"
63
- echo "- 해당 Confluence 인스턴스에 대한 적절한 권한이 있어야 합니다"
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"
@@ -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 = content;
560
-
561
- if (format === 'markdown') {
562
- storageContent = this.markdownToStorage(content);
563
- } else if (format === 'html') {
564
- // Convert HTML directly to storage format (no macro wrapper)
565
- storageContent = content;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
  });