confluence-cli 1.9.0 → 1.10.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 +7 -0
- package/README.md +13 -0
- package/bin/confluence.js +101 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.10.0](https://github.com/pchuri/confluence-cli/compare/v1.9.0...v1.10.0) (2025-12-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* export page with attachments ([#18](https://github.com/pchuri/confluence-cli/issues/18)) ([bdd9da4](https://github.com/pchuri/confluence-cli/commit/bdd9da474f13a8b6f96e64836443f65f846257a2))
|
|
7
|
+
|
|
1
8
|
# [1.9.0](https://github.com/pchuri/confluence-cli/compare/v1.8.0...v1.9.0) (2025-12-04)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
11
11
|
- ✏️ **Create pages** - Create new pages with support for Markdown, HTML, or Storage format
|
|
12
12
|
- 📝 **Update pages** - Update existing page content and titles
|
|
13
13
|
- 📎 **Attachments** - List or download page attachments
|
|
14
|
+
- 📦 **Export** - Save a page and its attachments to a local folder
|
|
14
15
|
- 🛠️ **Edit workflow** - Export page content for editing and re-import
|
|
15
16
|
- 🔧 **Easy setup** - Simple configuration with environment variables or interactive setup
|
|
16
17
|
|
|
@@ -120,6 +121,18 @@ confluence attachments 123456789 --pattern "*.png" --limit 5
|
|
|
120
121
|
confluence attachments 123456789 --pattern "*.png" --download --dest ./downloads
|
|
121
122
|
```
|
|
122
123
|
|
|
124
|
+
### Export a Page with Attachments
|
|
125
|
+
```bash
|
|
126
|
+
# Export page content (markdown by default) and all attachments
|
|
127
|
+
confluence export 123456789 --dest ./exports
|
|
128
|
+
|
|
129
|
+
# Custom content format/filename and attachment filtering
|
|
130
|
+
confluence export 123456789 --format html --file content.html --pattern "*.png"
|
|
131
|
+
|
|
132
|
+
# Skip attachments if you only need the content file
|
|
133
|
+
confluence export 123456789 --skip-attachments
|
|
134
|
+
```
|
|
135
|
+
|
|
123
136
|
### List Spaces
|
|
124
137
|
```bash
|
|
125
138
|
confluence spaces
|
package/bin/confluence.js
CHANGED
|
@@ -421,6 +421,107 @@ program
|
|
|
421
421
|
}
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
+
// Export page content with attachments
|
|
425
|
+
program
|
|
426
|
+
.command('export <pageId>')
|
|
427
|
+
.description('Export a page to a directory with its attachments')
|
|
428
|
+
.option('--format <format>', 'Content format (html, text, markdown)', 'markdown')
|
|
429
|
+
.option('--dest <directory>', 'Base directory to export into', '.')
|
|
430
|
+
.option('--file <filename>', 'Content filename (default: page.<ext>)')
|
|
431
|
+
.option('--attachments-dir <name>', 'Subdirectory for attachments', 'attachments')
|
|
432
|
+
.option('--pattern <glob>', 'Filter attachments by filename (e.g., "*.png")')
|
|
433
|
+
.option('--skip-attachments', 'Do not download attachments')
|
|
434
|
+
.action(async (pageId, options) => {
|
|
435
|
+
const analytics = new Analytics();
|
|
436
|
+
try {
|
|
437
|
+
const config = getConfig();
|
|
438
|
+
const client = new ConfluenceClient(config);
|
|
439
|
+
const fs = require('fs');
|
|
440
|
+
const path = require('path');
|
|
441
|
+
|
|
442
|
+
const format = (options.format || 'markdown').toLowerCase();
|
|
443
|
+
const formatExt = { markdown: 'md', html: 'html', text: 'txt' };
|
|
444
|
+
const contentExt = formatExt[format] || 'txt';
|
|
445
|
+
|
|
446
|
+
const pageInfo = await client.getPageInfo(pageId);
|
|
447
|
+
const content = await client.readPage(pageId, format);
|
|
448
|
+
|
|
449
|
+
const baseDir = path.resolve(options.dest || '.');
|
|
450
|
+
const folderName = sanitizeTitle(pageInfo.title || 'page');
|
|
451
|
+
const exportDir = path.join(baseDir, folderName);
|
|
452
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
453
|
+
|
|
454
|
+
const contentFile = options.file || `page.${contentExt}`;
|
|
455
|
+
const contentPath = path.join(exportDir, contentFile);
|
|
456
|
+
fs.writeFileSync(contentPath, content);
|
|
457
|
+
|
|
458
|
+
console.log(chalk.green('✅ Page exported'));
|
|
459
|
+
console.log(`Title: ${chalk.blue(pageInfo.title)}`);
|
|
460
|
+
console.log(`Content: ${chalk.gray(contentPath)}`);
|
|
461
|
+
|
|
462
|
+
if (!options.skipAttachments) {
|
|
463
|
+
const pattern = options.pattern ? options.pattern.trim() : null;
|
|
464
|
+
const attachments = await client.getAllAttachments(pageId);
|
|
465
|
+
const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments;
|
|
466
|
+
|
|
467
|
+
if (filtered.length === 0) {
|
|
468
|
+
console.log(chalk.yellow('No attachments to download.'));
|
|
469
|
+
} else {
|
|
470
|
+
const attachmentsDirName = options.attachmentsDir || 'attachments';
|
|
471
|
+
const attachmentsDir = path.join(exportDir, attachmentsDirName);
|
|
472
|
+
fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
473
|
+
|
|
474
|
+
const uniquePathFor = (dir, filename) => {
|
|
475
|
+
const parsed = path.parse(filename);
|
|
476
|
+
let attempt = path.join(dir, filename);
|
|
477
|
+
let counter = 1;
|
|
478
|
+
while (fs.existsSync(attempt)) {
|
|
479
|
+
const suffix = ` (${counter})`;
|
|
480
|
+
const nextName = `${parsed.name}${suffix}${parsed.ext}`;
|
|
481
|
+
attempt = path.join(dir, nextName);
|
|
482
|
+
counter += 1;
|
|
483
|
+
}
|
|
484
|
+
return attempt;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const writeStream = (stream, targetPath) => new Promise((resolve, reject) => {
|
|
488
|
+
const writer = fs.createWriteStream(targetPath);
|
|
489
|
+
stream.pipe(writer);
|
|
490
|
+
stream.on('error', reject);
|
|
491
|
+
writer.on('error', reject);
|
|
492
|
+
writer.on('finish', resolve);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
let downloaded = 0;
|
|
496
|
+
for (const attachment of filtered) {
|
|
497
|
+
const targetPath = uniquePathFor(attachmentsDir, attachment.title);
|
|
498
|
+
const dataStream = await client.downloadAttachment(pageId, attachment.id);
|
|
499
|
+
await writeStream(dataStream, targetPath);
|
|
500
|
+
downloaded += 1;
|
|
501
|
+
console.log(`⬇️ ${chalk.green(attachment.title)} -> ${chalk.gray(targetPath)}`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(chalk.green(`Downloaded ${downloaded} attachment${downloaded === 1 ? '' : 's'} to ${attachmentsDir}`));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
analytics.track('export', true);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
analytics.track('export', false);
|
|
511
|
+
console.error(chalk.red('Error:'), error.message);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
function sanitizeTitle(value) {
|
|
517
|
+
const fallback = 'page';
|
|
518
|
+
if (!value || typeof value !== 'string') {
|
|
519
|
+
return fallback;
|
|
520
|
+
}
|
|
521
|
+
const cleaned = value.replace(/[\\/:*?"<>|]/g, ' ').trim();
|
|
522
|
+
return cleaned || fallback;
|
|
523
|
+
}
|
|
524
|
+
|
|
424
525
|
// Copy page tree command
|
|
425
526
|
program
|
|
426
527
|
.command('copy-tree <sourcePageId> <targetParentId> [newTitle]')
|