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 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]')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.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": {