confluence-cli 1.12.1 → 1.14.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 +62 -3
- package/bin/confluence.js +513 -6
- package/eslint.config.js +33 -0
- package/lib/confluence-client.js +257 -0
- package/package.json +2 -2
- package/tests/confluence-client.test.js +74 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.14.0](https://github.com/pchuri/confluence-cli/compare/v1.13.0...v1.14.0) (2026-02-03)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add comments support to CLI ([d40de55](https://github.com/pchuri/confluence-cli/commit/d40de55573aa71409b3aa2743531f2a4cb5a4eda)), closes [#28](https://github.com/pchuri/confluence-cli/issues/28)
|
|
7
|
+
|
|
8
|
+
# [1.13.0](https://github.com/pchuri/confluence-cli/compare/v1.12.1...v1.13.0) (2026-01-08)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add children command to list child pages ([#27](https://github.com/pchuri/confluence-cli/issues/27)) ([7e8b4ed](https://github.com/pchuri/confluence-cli/commit/7e8b4ed1b0ed69a7e1de52dfaf0c1ff36973f78b))
|
|
14
|
+
|
|
1
15
|
## [1.12.1](https://github.com/pchuri/confluence-cli/compare/v1.12.0...v1.12.1) (2025-12-31)
|
|
2
16
|
|
|
3
17
|
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
12
12
|
- 📝 **Update pages** - Update existing page content and titles
|
|
13
13
|
- 🗑️ **Delete pages** - Delete (or move to trash) pages by ID or URL
|
|
14
14
|
- 📎 **Attachments** - List or download page attachments
|
|
15
|
+
- 💬 **Comments** - List, create, and delete page comments (footer or inline)
|
|
15
16
|
- 📦 **Export** - Save a page and its attachments to a local folder
|
|
16
17
|
- 🛠️ **Edit workflow** - Export page content for editing and re-import
|
|
17
18
|
- 🔧 **Easy setup** - Simple configuration with environment variables or interactive setup
|
|
@@ -44,12 +45,17 @@ npx confluence-cli
|
|
|
44
45
|
confluence search "my search term"
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
4. **
|
|
48
|
+
4. **List child pages:**
|
|
49
|
+
```bash
|
|
50
|
+
confluence children 123456789
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
5. **Create a new page:**
|
|
48
54
|
```bash
|
|
49
55
|
confluence create "My New Page" SPACEKEY --content "Hello World!"
|
|
50
56
|
```
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
6. **Update a page:**
|
|
53
59
|
```bash
|
|
54
60
|
confluence update 123456789 --content "Updated content"
|
|
55
61
|
```
|
|
@@ -122,6 +128,33 @@ confluence attachments 123456789 --pattern "*.png" --limit 5
|
|
|
122
128
|
confluence attachments 123456789 --pattern "*.png" --download --dest ./downloads
|
|
123
129
|
```
|
|
124
130
|
|
|
131
|
+
### Comments
|
|
132
|
+
```bash
|
|
133
|
+
# List all comments (footer + inline)
|
|
134
|
+
confluence comments 123456789
|
|
135
|
+
|
|
136
|
+
# List inline comments as markdown
|
|
137
|
+
confluence comments 123456789 --location inline --format markdown
|
|
138
|
+
|
|
139
|
+
# Create a footer comment
|
|
140
|
+
confluence comment 123456789 --content "Looks good to me!"
|
|
141
|
+
|
|
142
|
+
# Create an inline comment
|
|
143
|
+
confluence comment 123456789 \
|
|
144
|
+
--location inline \
|
|
145
|
+
--content "Consider renaming this" \
|
|
146
|
+
--inline-selection "foo" \
|
|
147
|
+
--inline-original-selection "foo"
|
|
148
|
+
|
|
149
|
+
# Reply to a comment
|
|
150
|
+
confluence comment 123456789 --parent 998877 --content "Agree with this"
|
|
151
|
+
|
|
152
|
+
# Delete a comment
|
|
153
|
+
confluence comment-delete 998877
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Inline comment creation note (Confluence Cloud): Creating inline comments requires editor-generated highlight metadata (`matchIndex`, `lastFetchTime`, `serializedHighlights`, plus the selection text). The public REST API does not provide these fields, so inline creation and inline replies can fail with a 400 unless you supply the full `--inline-properties` payload captured from the editor. Footer comments and replies are fully supported.
|
|
157
|
+
|
|
125
158
|
### Export a Page with Attachments
|
|
126
159
|
```bash
|
|
127
160
|
# Export page content (markdown by default) and all attachments
|
|
@@ -139,6 +172,27 @@ confluence export 123456789 --skip-attachments
|
|
|
139
172
|
confluence spaces
|
|
140
173
|
```
|
|
141
174
|
|
|
175
|
+
### List Child Pages
|
|
176
|
+
```bash
|
|
177
|
+
# List direct child pages
|
|
178
|
+
confluence children 123456789
|
|
179
|
+
|
|
180
|
+
# List all descendants recursively
|
|
181
|
+
confluence children 123456789 --recursive
|
|
182
|
+
|
|
183
|
+
# Display as tree structure
|
|
184
|
+
confluence children 123456789 --recursive --format tree
|
|
185
|
+
|
|
186
|
+
# Show page IDs and URLs
|
|
187
|
+
confluence children 123456789 --show-id --show-url
|
|
188
|
+
|
|
189
|
+
# Limit recursion depth
|
|
190
|
+
confluence children 123456789 --recursive --max-depth 3
|
|
191
|
+
|
|
192
|
+
# Output as JSON for scripting
|
|
193
|
+
confluence children 123456789 --recursive --format json > children.json
|
|
194
|
+
```
|
|
195
|
+
|
|
142
196
|
### Find a Page by Title
|
|
143
197
|
```bash
|
|
144
198
|
# Find page by title
|
|
@@ -250,6 +304,7 @@ confluence stats
|
|
|
250
304
|
| `search <query>` | Search for pages | `--limit <number>` |
|
|
251
305
|
| `spaces` | List all available spaces | |
|
|
252
306
|
| `find <title>` | Find a page by its title | `--space <spaceKey>` |
|
|
307
|
+
| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree\|json>`, `--show-url`, `--show-id` |
|
|
253
308
|
| `create <title> <spaceKey>` | Create a new page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>`|
|
|
254
309
|
| `create-child <title> <parentId>` | Create a child page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
|
|
255
310
|
| `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` |
|
|
@@ -257,6 +312,9 @@ confluence stats
|
|
|
257
312
|
| `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` |
|
|
258
313
|
| `edit <pageId>` | Export page content for editing | `--output <file>` |
|
|
259
314
|
| `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
|
|
315
|
+
| `comments <pageId_or_url>` | List comments for a page | `--format <text\|markdown\|json>`, `--limit <number>`, `--start <number>`, `--location <inline\|footer\|resolved>`, `--depth <root\|all>`, `--all` |
|
|
316
|
+
| `comment <pageId_or_url>` | Create a comment on a page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>`, `--parent <commentId>`, `--location <inline\|footer>`, `--inline-selection <text>`, `--inline-original-selection <text>`, `--inline-marker-ref <ref>`, `--inline-properties <json>` |
|
|
317
|
+
| `comment-delete <commentId>` | Delete a comment by ID | `--yes` |
|
|
260
318
|
| `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` |
|
|
261
319
|
| `stats` | View your usage statistics | |
|
|
262
320
|
|
|
@@ -325,7 +383,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
|
|
325
383
|
- [ ] Export pages to different formats
|
|
326
384
|
- [ ] Integration with other Atlassian tools (Jira)
|
|
327
385
|
- [ ] Page attachments management
|
|
328
|
-
- [
|
|
386
|
+
- [x] Comments
|
|
387
|
+
- [ ] Reviews
|
|
329
388
|
|
|
330
389
|
## Support & Feedback
|
|
331
390
|
|
package/bin/confluence.js
CHANGED
|
@@ -29,8 +29,7 @@ program
|
|
|
29
29
|
.action(async (pageId, options) => {
|
|
30
30
|
const analytics = new Analytics();
|
|
31
31
|
try {
|
|
32
|
-
const
|
|
33
|
-
const client = new ConfluenceClient(config);
|
|
32
|
+
const client = new ConfluenceClient(getConfig());
|
|
34
33
|
const content = await client.readPage(pageId, options.format);
|
|
35
34
|
console.log(content);
|
|
36
35
|
analytics.track('read', true);
|
|
@@ -48,8 +47,7 @@ program
|
|
|
48
47
|
.action(async (pageId) => {
|
|
49
48
|
const analytics = new Analytics();
|
|
50
49
|
try {
|
|
51
|
-
const
|
|
52
|
-
const client = new ConfluenceClient(config);
|
|
50
|
+
const client = new ConfluenceClient(getConfig());
|
|
53
51
|
const info = await client.getPageInfo(pageId);
|
|
54
52
|
console.log(chalk.blue('Page Information:'));
|
|
55
53
|
console.log(`Title: ${chalk.green(info.title)}`);
|
|
@@ -75,8 +73,7 @@ program
|
|
|
75
73
|
.action(async (query, options) => {
|
|
76
74
|
const analytics = new Analytics();
|
|
77
75
|
try {
|
|
78
|
-
const
|
|
79
|
-
const client = new ConfluenceClient(config);
|
|
76
|
+
const client = new ConfluenceClient(getConfig());
|
|
80
77
|
const results = await client.search(query, parseInt(options.limit));
|
|
81
78
|
|
|
82
79
|
if (results.length === 0) {
|
|
@@ -466,6 +463,320 @@ program
|
|
|
466
463
|
}
|
|
467
464
|
});
|
|
468
465
|
|
|
466
|
+
// Comments command
|
|
467
|
+
program
|
|
468
|
+
.command('comments <pageId>')
|
|
469
|
+
.description('List comments for a page by ID or URL')
|
|
470
|
+
.option('-f, --format <format>', 'Output format (text, markdown, json)', 'text')
|
|
471
|
+
.option('-l, --limit <limit>', 'Maximum number of comments to fetch (default: 25)')
|
|
472
|
+
.option('--start <start>', 'Start index for results (default: 0)', '0')
|
|
473
|
+
.option('--location <location>', 'Filter by location (inline, footer, resolved). Comma-separated')
|
|
474
|
+
.option('--depth <depth>', 'Comment depth ("" for root only, "all")')
|
|
475
|
+
.option('--all', 'Fetch all comments (ignores pagination)')
|
|
476
|
+
.action(async (pageId, options) => {
|
|
477
|
+
const analytics = new Analytics();
|
|
478
|
+
try {
|
|
479
|
+
const config = getConfig();
|
|
480
|
+
const client = new ConfluenceClient(config);
|
|
481
|
+
|
|
482
|
+
const format = (options.format || 'text').toLowerCase();
|
|
483
|
+
if (!['text', 'markdown', 'json'].includes(format)) {
|
|
484
|
+
throw new Error('Format must be one of: text, markdown, json');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const limit = options.limit ? parseInt(options.limit, 10) : null;
|
|
488
|
+
if (options.limit && (Number.isNaN(limit) || limit <= 0)) {
|
|
489
|
+
throw new Error('Limit must be a positive number.');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const start = options.start ? parseInt(options.start, 10) : 0;
|
|
493
|
+
if (options.start && (Number.isNaN(start) || start < 0)) {
|
|
494
|
+
throw new Error('Start must be a non-negative number.');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const locationValues = parseLocationOptions(options.location);
|
|
498
|
+
const invalidLocations = locationValues.filter(value => !['inline', 'footer', 'resolved'].includes(value));
|
|
499
|
+
if (invalidLocations.length > 0) {
|
|
500
|
+
throw new Error(`Invalid location value(s): ${invalidLocations.join(', ')}`);
|
|
501
|
+
}
|
|
502
|
+
const locationParam = locationValues.length === 0
|
|
503
|
+
? null
|
|
504
|
+
: (locationValues.length === 1 ? locationValues[0] : locationValues);
|
|
505
|
+
|
|
506
|
+
let comments = [];
|
|
507
|
+
let nextStart = null;
|
|
508
|
+
|
|
509
|
+
if (options.all) {
|
|
510
|
+
comments = await client.getAllComments(pageId, {
|
|
511
|
+
maxResults: limit || null,
|
|
512
|
+
start,
|
|
513
|
+
location: locationParam,
|
|
514
|
+
depth: options.depth
|
|
515
|
+
});
|
|
516
|
+
} else {
|
|
517
|
+
const response = await client.listComments(pageId, {
|
|
518
|
+
limit: limit || undefined,
|
|
519
|
+
start,
|
|
520
|
+
location: locationParam,
|
|
521
|
+
depth: options.depth
|
|
522
|
+
});
|
|
523
|
+
comments = response.results;
|
|
524
|
+
nextStart = response.nextStart;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (comments.length === 0) {
|
|
528
|
+
console.log(chalk.yellow('No comments found.'));
|
|
529
|
+
analytics.track('comments', true);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (format === 'json') {
|
|
534
|
+
const resolvedPageId = await client.extractPageId(pageId);
|
|
535
|
+
const output = {
|
|
536
|
+
pageId: resolvedPageId,
|
|
537
|
+
commentCount: comments.length,
|
|
538
|
+
comments: comments.map(comment => ({
|
|
539
|
+
...comment,
|
|
540
|
+
bodyStorage: comment.body,
|
|
541
|
+
bodyText: client.formatCommentBody(comment.body, 'text')
|
|
542
|
+
}))
|
|
543
|
+
};
|
|
544
|
+
if (!options.all) {
|
|
545
|
+
output.nextStart = nextStart;
|
|
546
|
+
}
|
|
547
|
+
console.log(JSON.stringify(output, null, 2));
|
|
548
|
+
analytics.track('comments', true);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const commentTree = buildCommentTree(comments);
|
|
553
|
+
console.log(chalk.blue(`Found ${comments.length} comment${comments.length === 1 ? '' : 's'}:`));
|
|
554
|
+
|
|
555
|
+
const renderComments = (nodes, path = []) => {
|
|
556
|
+
nodes.forEach((comment, index) => {
|
|
557
|
+
const currentPath = [...path, index + 1];
|
|
558
|
+
const level = currentPath.length - 1;
|
|
559
|
+
const indent = ' '.repeat(level);
|
|
560
|
+
const branchGlyph = level > 0 ? (index === nodes.length - 1 ? '└─ ' : '├─ ') : '';
|
|
561
|
+
const headerPrefix = `${indent}${chalk.dim(branchGlyph)}`;
|
|
562
|
+
const bodyIndent = level === 0
|
|
563
|
+
? ' '
|
|
564
|
+
: `${indent}${' '.repeat(branchGlyph.length)}`;
|
|
565
|
+
|
|
566
|
+
const isReply = Boolean(comment.parentId);
|
|
567
|
+
const location = comment.location || 'unknown';
|
|
568
|
+
const author = comment.author?.displayName || 'Unknown';
|
|
569
|
+
const createdAt = comment.createdAt || 'unknown date';
|
|
570
|
+
const metaParts = [`Created: ${createdAt}`];
|
|
571
|
+
if (comment.status) metaParts.push(`Status: ${comment.status}`);
|
|
572
|
+
if (comment.version) metaParts.push(`Version: ${comment.version}`);
|
|
573
|
+
if (!isReply && comment.resolution) metaParts.push(`Resolution: ${comment.resolution}`);
|
|
574
|
+
|
|
575
|
+
const label = isReply ? chalk.gray('[reply]') : chalk.cyan(`[${location}]`);
|
|
576
|
+
console.log(`${headerPrefix}${currentPath.join('.')}. ${chalk.green(author)} ${chalk.gray(`(ID: ${comment.id})`)} ${label}`);
|
|
577
|
+
console.log(chalk.dim(`${bodyIndent}${metaParts.join(' • ')}`));
|
|
578
|
+
|
|
579
|
+
if (!isReply) {
|
|
580
|
+
const inlineProps = comment.inlineProperties || {};
|
|
581
|
+
const selectionText = inlineProps.selection || inlineProps.originalSelection;
|
|
582
|
+
if (selectionText) {
|
|
583
|
+
const selectionLabel = inlineProps.selection ? 'Highlight' : 'Highlight (original)';
|
|
584
|
+
console.log(chalk.dim(`${bodyIndent}${selectionLabel}: ${selectionText}`));
|
|
585
|
+
}
|
|
586
|
+
if (inlineProps.markerRef) {
|
|
587
|
+
console.log(chalk.dim(`${bodyIndent}Marker ref: ${inlineProps.markerRef}`));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const body = client.formatCommentBody(comment.body, format);
|
|
592
|
+
if (body) {
|
|
593
|
+
console.log(`${bodyIndent}${chalk.yellowBright('Body:')}`);
|
|
594
|
+
console.log(formatBodyBlock(body, `${bodyIndent} `));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (comment.children && comment.children.length > 0) {
|
|
598
|
+
renderComments(comment.children, currentPath);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
renderComments(commentTree);
|
|
604
|
+
|
|
605
|
+
if (!options.all && nextStart !== null && nextStart !== undefined) {
|
|
606
|
+
console.log(chalk.gray(`Next start: ${nextStart}`));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
analytics.track('comments', true);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
analytics.track('comments', false);
|
|
612
|
+
console.error(chalk.red('Error:'), error.message);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Comment creation command
|
|
618
|
+
program
|
|
619
|
+
.command('comment <pageId>')
|
|
620
|
+
.description('Create a comment on a page by ID or URL (footer or inline)')
|
|
621
|
+
.option('-f, --file <file>', 'Read content from file')
|
|
622
|
+
.option('-c, --content <content>', 'Comment content as string')
|
|
623
|
+
.option('--format <format>', 'Content format (storage, html, markdown)', 'storage')
|
|
624
|
+
.option('--parent <commentId>', 'Reply to a comment by ID')
|
|
625
|
+
.option('--location <location>', 'Comment location (inline or footer)', 'footer')
|
|
626
|
+
.option('--inline-selection <text>', 'Inline selection text')
|
|
627
|
+
.option('--inline-original-selection <text>', 'Original inline selection text')
|
|
628
|
+
.option('--inline-marker-ref <ref>', 'Inline marker reference (optional)')
|
|
629
|
+
.option('--inline-properties <json>', 'Inline properties JSON (advanced)')
|
|
630
|
+
.action(async (pageId, options) => {
|
|
631
|
+
const analytics = new Analytics();
|
|
632
|
+
let location = null;
|
|
633
|
+
try {
|
|
634
|
+
const config = getConfig();
|
|
635
|
+
const client = new ConfluenceClient(config);
|
|
636
|
+
|
|
637
|
+
let content = '';
|
|
638
|
+
|
|
639
|
+
if (options.file) {
|
|
640
|
+
const fs = require('fs');
|
|
641
|
+
if (!fs.existsSync(options.file)) {
|
|
642
|
+
throw new Error(`File not found: ${options.file}`);
|
|
643
|
+
}
|
|
644
|
+
content = fs.readFileSync(options.file, 'utf8');
|
|
645
|
+
} else if (options.content) {
|
|
646
|
+
content = options.content;
|
|
647
|
+
} else {
|
|
648
|
+
throw new Error('Either --file or --content option is required');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
location = (options.location || 'footer').toLowerCase();
|
|
652
|
+
if (!['inline', 'footer'].includes(location)) {
|
|
653
|
+
throw new Error('Location must be either "inline" or "footer".');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
let inlineProperties = {};
|
|
657
|
+
if (options.inlineProperties) {
|
|
658
|
+
try {
|
|
659
|
+
const parsed = JSON.parse(options.inlineProperties);
|
|
660
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
661
|
+
throw new Error('Inline properties must be a JSON object.');
|
|
662
|
+
}
|
|
663
|
+
inlineProperties = { ...parsed };
|
|
664
|
+
} catch (error) {
|
|
665
|
+
throw new Error(`Invalid --inline-properties JSON: ${error.message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (options.inlineSelection) {
|
|
670
|
+
inlineProperties.selection = options.inlineSelection;
|
|
671
|
+
}
|
|
672
|
+
if (options.inlineOriginalSelection) {
|
|
673
|
+
inlineProperties.originalSelection = options.inlineOriginalSelection;
|
|
674
|
+
}
|
|
675
|
+
if (options.inlineMarkerRef) {
|
|
676
|
+
inlineProperties.markerRef = options.inlineMarkerRef;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (Object.keys(inlineProperties).length > 0 && location !== 'inline') {
|
|
680
|
+
throw new Error('Inline properties can only be used with --location inline.');
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const parentId = options.parent;
|
|
684
|
+
|
|
685
|
+
if (location === 'inline') {
|
|
686
|
+
const hasSelection = inlineProperties.selection || inlineProperties.originalSelection;
|
|
687
|
+
if (!hasSelection && !parentId) {
|
|
688
|
+
throw new Error('Inline comments require --inline-selection or --inline-original-selection when starting a new inline thread.');
|
|
689
|
+
}
|
|
690
|
+
if (hasSelection) {
|
|
691
|
+
if (!inlineProperties.originalSelection && inlineProperties.selection) {
|
|
692
|
+
inlineProperties.originalSelection = inlineProperties.selection;
|
|
693
|
+
}
|
|
694
|
+
if (!inlineProperties.markerRef) {
|
|
695
|
+
inlineProperties.markerRef = `comment-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const result = await client.createComment(pageId, content, options.format, {
|
|
701
|
+
parentId,
|
|
702
|
+
location,
|
|
703
|
+
inlineProperties: location === 'inline' ? inlineProperties : null
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
console.log(chalk.green('✅ Comment created successfully!'));
|
|
707
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
708
|
+
if (result.container?.id) {
|
|
709
|
+
console.log(`Page ID: ${chalk.blue(result.container.id)}`);
|
|
710
|
+
}
|
|
711
|
+
if (result._links?.webui) {
|
|
712
|
+
const url = client.toAbsoluteUrl(result._links.webui);
|
|
713
|
+
console.log(`URL: ${chalk.gray(url)}`);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
analytics.track('comment_create', true);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
analytics.track('comment_create', false);
|
|
719
|
+
console.error(chalk.red('Error:'), error.message);
|
|
720
|
+
if (error.response?.data) {
|
|
721
|
+
const detail = typeof error.response.data === 'string'
|
|
722
|
+
? error.response.data
|
|
723
|
+
: JSON.stringify(error.response.data, null, 2);
|
|
724
|
+
console.error(chalk.red('API response:'), detail);
|
|
725
|
+
}
|
|
726
|
+
const apiErrors = error.response?.data?.data?.errors || error.response?.data?.errors || [];
|
|
727
|
+
const errorKeys = apiErrors
|
|
728
|
+
.map((entry) => entry?.message?.key || entry?.message || entry?.key)
|
|
729
|
+
.filter(Boolean);
|
|
730
|
+
const needsInlineMeta = ['matchIndex', 'lastFetchTime', 'serializedHighlights']
|
|
731
|
+
.every((key) => errorKeys.includes(key));
|
|
732
|
+
if (location === 'inline' && needsInlineMeta) {
|
|
733
|
+
console.error(chalk.yellow('Inline comment creation requires editor highlight metadata (matchIndex, lastFetchTime, serializedHighlights).'));
|
|
734
|
+
console.error(chalk.yellow('Try replying to an existing inline comment or use footer comments instead.'));
|
|
735
|
+
}
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
// Comment delete command
|
|
741
|
+
program
|
|
742
|
+
.command('comment-delete <commentId>')
|
|
743
|
+
.description('Delete a comment by ID')
|
|
744
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
745
|
+
.action(async (commentId, options) => {
|
|
746
|
+
const analytics = new Analytics();
|
|
747
|
+
try {
|
|
748
|
+
const config = getConfig();
|
|
749
|
+
const client = new ConfluenceClient(config);
|
|
750
|
+
|
|
751
|
+
if (!options.yes) {
|
|
752
|
+
const { confirmed } = await inquirer.prompt([
|
|
753
|
+
{
|
|
754
|
+
type: 'confirm',
|
|
755
|
+
name: 'confirmed',
|
|
756
|
+
default: false,
|
|
757
|
+
message: `Delete comment ${commentId}?`
|
|
758
|
+
}
|
|
759
|
+
]);
|
|
760
|
+
|
|
761
|
+
if (!confirmed) {
|
|
762
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
763
|
+
analytics.track('comment_delete_cancel', true);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const result = await client.deleteComment(commentId);
|
|
769
|
+
|
|
770
|
+
console.log(chalk.green('✅ Comment deleted successfully!'));
|
|
771
|
+
console.log(`ID: ${chalk.blue(result.id)}`);
|
|
772
|
+
analytics.track('comment_delete', true);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
analytics.track('comment_delete', false);
|
|
775
|
+
console.error(chalk.red('Error:'), error.message);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
469
780
|
// Export page content with attachments
|
|
470
781
|
program
|
|
471
782
|
.command('export <pageId>')
|
|
@@ -584,6 +895,49 @@ function sanitizeTitle(value) {
|
|
|
584
895
|
return cleaned || fallback;
|
|
585
896
|
}
|
|
586
897
|
|
|
898
|
+
function parseLocationOptions(raw) {
|
|
899
|
+
if (!raw) {
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
if (Array.isArray(raw)) {
|
|
903
|
+
return raw.flatMap(item => String(item).split(','))
|
|
904
|
+
.map(value => value.trim().toLowerCase())
|
|
905
|
+
.filter(Boolean);
|
|
906
|
+
}
|
|
907
|
+
return String(raw).split(',').map(value => value.trim().toLowerCase()).filter(Boolean);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function formatBodyBlock(text, indent = '') {
|
|
911
|
+
return text.split('\n').map(line => `${indent}${chalk.white(line)}`).join('\n');
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function buildCommentTree(comments) {
|
|
915
|
+
const nodes = comments.map((comment, index) => ({
|
|
916
|
+
...comment,
|
|
917
|
+
_order: index,
|
|
918
|
+
children: []
|
|
919
|
+
}));
|
|
920
|
+
const byId = new Map(nodes.map(node => [String(node.id), node]));
|
|
921
|
+
const roots = [];
|
|
922
|
+
|
|
923
|
+
nodes.forEach((node) => {
|
|
924
|
+
const parentId = node.parentId ? String(node.parentId) : null;
|
|
925
|
+
if (parentId && byId.has(parentId)) {
|
|
926
|
+
byId.get(parentId).children.push(node);
|
|
927
|
+
} else {
|
|
928
|
+
roots.push(node);
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
const sortNodes = (list) => {
|
|
933
|
+
list.sort((a, b) => a._order - b._order);
|
|
934
|
+
list.forEach((child) => sortNodes(child.children));
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
sortNodes(roots);
|
|
938
|
+
return roots;
|
|
939
|
+
}
|
|
940
|
+
|
|
587
941
|
// Copy page tree command
|
|
588
942
|
program
|
|
589
943
|
.command('copy-tree <sourcePageId> <targetParentId> [newTitle]')
|
|
@@ -706,6 +1060,159 @@ program
|
|
|
706
1060
|
}
|
|
707
1061
|
});
|
|
708
1062
|
|
|
1063
|
+
// List children command
|
|
1064
|
+
program
|
|
1065
|
+
.command('children <pageId>')
|
|
1066
|
+
.description('List child pages of a Confluence page')
|
|
1067
|
+
.option('-r, --recursive', 'List all descendants recursively', false)
|
|
1068
|
+
.option('--max-depth <number>', 'Maximum depth for recursive listing', '10')
|
|
1069
|
+
.option('--format <format>', 'Output format (list, tree, json)', 'list')
|
|
1070
|
+
.option('--show-url', 'Show page URLs', false)
|
|
1071
|
+
.option('--show-id', 'Show page IDs', false)
|
|
1072
|
+
.action(async (pageId, options) => {
|
|
1073
|
+
const analytics = new Analytics();
|
|
1074
|
+
try {
|
|
1075
|
+
const config = getConfig();
|
|
1076
|
+
const client = new ConfluenceClient(config);
|
|
1077
|
+
|
|
1078
|
+
// Extract page ID from URL if needed
|
|
1079
|
+
const resolvedPageId = await client.extractPageId(pageId);
|
|
1080
|
+
|
|
1081
|
+
// Get children
|
|
1082
|
+
let children;
|
|
1083
|
+
if (options.recursive) {
|
|
1084
|
+
const maxDepth = parseInt(options.maxDepth) || 10;
|
|
1085
|
+
children = await client.getAllDescendantPages(resolvedPageId, maxDepth);
|
|
1086
|
+
} else {
|
|
1087
|
+
children = await client.getChildPages(resolvedPageId);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (children.length === 0) {
|
|
1091
|
+
console.log(chalk.yellow('No child pages found.'));
|
|
1092
|
+
analytics.track('children', true);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Format output
|
|
1097
|
+
const format = options.format.toLowerCase();
|
|
1098
|
+
|
|
1099
|
+
if (format === 'json') {
|
|
1100
|
+
// JSON output
|
|
1101
|
+
const output = {
|
|
1102
|
+
pageId: resolvedPageId,
|
|
1103
|
+
childCount: children.length,
|
|
1104
|
+
children: children.map(page => ({
|
|
1105
|
+
id: page.id,
|
|
1106
|
+
title: page.title,
|
|
1107
|
+
type: page.type,
|
|
1108
|
+
status: page.status,
|
|
1109
|
+
spaceKey: page.space?.key,
|
|
1110
|
+
url: `https://${config.domain}/wiki/spaces/${page.space?.key}/pages/${page.id}`,
|
|
1111
|
+
parentId: page.parentId || resolvedPageId
|
|
1112
|
+
}))
|
|
1113
|
+
};
|
|
1114
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1115
|
+
} else if (format === 'tree' && options.recursive) {
|
|
1116
|
+
// Tree format (only for recursive mode)
|
|
1117
|
+
const pageInfo = await client.getPageInfo(resolvedPageId);
|
|
1118
|
+
console.log(chalk.blue(`📁 ${pageInfo.title}`));
|
|
1119
|
+
|
|
1120
|
+
// Build tree structure
|
|
1121
|
+
const tree = buildTree(children, resolvedPageId);
|
|
1122
|
+
printTree(tree, config, options, 1);
|
|
1123
|
+
|
|
1124
|
+
console.log('');
|
|
1125
|
+
console.log(chalk.gray(`Total: ${children.length} child page${children.length === 1 ? '' : 's'}`));
|
|
1126
|
+
} else {
|
|
1127
|
+
// List format (default)
|
|
1128
|
+
console.log(chalk.blue('Child pages:'));
|
|
1129
|
+
console.log('');
|
|
1130
|
+
|
|
1131
|
+
children.forEach((page, index) => {
|
|
1132
|
+
let output = `${index + 1}. ${chalk.green(page.title)}`;
|
|
1133
|
+
|
|
1134
|
+
if (options.showId) {
|
|
1135
|
+
output += ` ${chalk.gray(`(ID: ${page.id})`)}`;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (options.showUrl) {
|
|
1139
|
+
const url = `https://${config.domain}/wiki/spaces/${page.space?.key}/pages/${page.id}`;
|
|
1140
|
+
output += `\n ${chalk.gray(url)}`;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (options.recursive && page.parentId && page.parentId !== resolvedPageId) {
|
|
1144
|
+
output += ` ${chalk.dim('(nested)')}`;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
console.log(output);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
console.log('');
|
|
1151
|
+
console.log(chalk.gray(`Total: ${children.length} child page${children.length === 1 ? '' : 's'}`));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
analytics.track('children', true);
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
analytics.track('children', false);
|
|
1157
|
+
console.error(chalk.red('Error:'), error.message);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// Helper function to build tree structure
|
|
1163
|
+
function buildTree(pages, rootId) {
|
|
1164
|
+
const tree = [];
|
|
1165
|
+
const pageMap = new Map();
|
|
1166
|
+
|
|
1167
|
+
// Create a map of all pages
|
|
1168
|
+
pages.forEach(page => {
|
|
1169
|
+
pageMap.set(page.id, { ...page, children: [] });
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Build tree structure
|
|
1173
|
+
pages.forEach(page => {
|
|
1174
|
+
const node = pageMap.get(page.id);
|
|
1175
|
+
const parentId = page.parentId || rootId;
|
|
1176
|
+
|
|
1177
|
+
if (parentId === rootId) {
|
|
1178
|
+
tree.push(node);
|
|
1179
|
+
} else {
|
|
1180
|
+
const parent = pageMap.get(parentId);
|
|
1181
|
+
if (parent) {
|
|
1182
|
+
parent.children.push(node);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
return tree;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Helper function to print tree
|
|
1191
|
+
function printTree(nodes, config, options, depth = 1) {
|
|
1192
|
+
nodes.forEach((node, index) => {
|
|
1193
|
+
const isLast = index === nodes.length - 1;
|
|
1194
|
+
const indent = ' '.repeat(depth - 1);
|
|
1195
|
+
const prefix = isLast ? '└── ' : '├── ';
|
|
1196
|
+
|
|
1197
|
+
let output = `${indent}${prefix}📄 ${chalk.green(node.title)}`;
|
|
1198
|
+
|
|
1199
|
+
if (options.showId) {
|
|
1200
|
+
output += ` ${chalk.gray(`(ID: ${node.id})`)}`;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (options.showUrl) {
|
|
1204
|
+
const url = `https://${config.domain}/wiki/spaces/${node.space?.key}/pages/${node.id}`;
|
|
1205
|
+
output += `\n${indent}${isLast ? ' ' : '│ '}${chalk.gray(url)}`;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
console.log(output);
|
|
1209
|
+
|
|
1210
|
+
if (node.children && node.children.length > 0) {
|
|
1211
|
+
printTree(node.children, config, options, depth + 1);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
709
1216
|
if (process.argv.length <= 2) {
|
|
710
1217
|
program.help({ error: false });
|
|
711
1218
|
}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const util = require('util');
|
|
2
|
+
const js = require('@eslint/js');
|
|
3
|
+
const globals = require('globals');
|
|
4
|
+
|
|
5
|
+
if (typeof global.structuredClone !== 'function') {
|
|
6
|
+
global.structuredClone = typeof util.structuredClone === 'function'
|
|
7
|
+
? util.structuredClone
|
|
8
|
+
: (value) => JSON.parse(JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = [
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
{
|
|
14
|
+
files: ['**/*.js'],
|
|
15
|
+
languageOptions: {
|
|
16
|
+
ecmaVersion: 2021,
|
|
17
|
+
sourceType: 'module',
|
|
18
|
+
globals: {
|
|
19
|
+
...globals.node,
|
|
20
|
+
...globals.jest
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
rules: {
|
|
24
|
+
indent: ['error', 2],
|
|
25
|
+
'linebreak-style': ['error', 'unix'],
|
|
26
|
+
quotes: ['error', 'single'],
|
|
27
|
+
semi: ['error', 'always'],
|
|
28
|
+
'no-unused-vars': ['error', { argsIgnorePattern: '^_', caughtErrors: 'none' }],
|
|
29
|
+
'no-console': 'off',
|
|
30
|
+
'no-process-exit': 'off'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
];
|
package/lib/confluence-client.js
CHANGED
|
@@ -449,6 +449,263 @@ class ConfluenceClient {
|
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
/**
|
|
453
|
+
* List comments for a page with pagination support
|
|
454
|
+
*/
|
|
455
|
+
async listComments(pageIdOrUrl, options = {}) {
|
|
456
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
457
|
+
const limit = this.parsePositiveInt(options.limit, 25);
|
|
458
|
+
const start = this.parsePositiveInt(options.start, 0);
|
|
459
|
+
const params = {
|
|
460
|
+
limit,
|
|
461
|
+
start
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const expand = options.expand || 'body.storage,history,version,extensions.inlineProperties,extensions.resolution,ancestors';
|
|
465
|
+
if (expand) {
|
|
466
|
+
params.expand = expand;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (options.parentVersion !== undefined && options.parentVersion !== null) {
|
|
470
|
+
params.parentVersion = options.parentVersion;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (options.location) {
|
|
474
|
+
params.location = options.location;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (options.depth) {
|
|
478
|
+
params.depth = options.depth;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const paramsSerializer = (input) => {
|
|
482
|
+
const searchParams = new URLSearchParams();
|
|
483
|
+
Object.entries(input || {}).forEach(([key, value]) => {
|
|
484
|
+
if (value === undefined || value === null || value === '') {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (Array.isArray(value)) {
|
|
488
|
+
value.forEach((item) => {
|
|
489
|
+
if (item !== undefined && item !== null && item !== '') {
|
|
490
|
+
searchParams.append(key, item);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
searchParams.append(key, value);
|
|
496
|
+
});
|
|
497
|
+
return searchParams.toString();
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const response = await this.client.get(`/content/${pageId}/child/comment`, {
|
|
501
|
+
params,
|
|
502
|
+
paramsSerializer
|
|
503
|
+
});
|
|
504
|
+
const results = Array.isArray(response.data?.results)
|
|
505
|
+
? response.data.results.map((item) => this.normalizeComment(item))
|
|
506
|
+
: [];
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
results,
|
|
510
|
+
nextStart: this.parseNextStart(response.data?._links?.next)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Fetch all comments for a page, honoring an optional maxResults cap
|
|
516
|
+
*/
|
|
517
|
+
async getAllComments(pageIdOrUrl, options = {}) {
|
|
518
|
+
const pageSize = this.parsePositiveInt(options.pageSize || options.limit, 25);
|
|
519
|
+
const maxResults = this.parsePositiveInt(options.maxResults, null);
|
|
520
|
+
let start = this.parsePositiveInt(options.start, 0);
|
|
521
|
+
const comments = [];
|
|
522
|
+
|
|
523
|
+
let hasNext = true;
|
|
524
|
+
while (hasNext) {
|
|
525
|
+
const page = await this.listComments(pageIdOrUrl, {
|
|
526
|
+
limit: pageSize,
|
|
527
|
+
start,
|
|
528
|
+
expand: options.expand,
|
|
529
|
+
location: options.location,
|
|
530
|
+
depth: options.depth,
|
|
531
|
+
parentVersion: options.parentVersion
|
|
532
|
+
});
|
|
533
|
+
comments.push(...page.results);
|
|
534
|
+
|
|
535
|
+
if (maxResults && comments.length >= maxResults) {
|
|
536
|
+
return comments.slice(0, maxResults);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
hasNext = page.nextStart !== null && page.nextStart !== undefined;
|
|
540
|
+
if (hasNext) {
|
|
541
|
+
start = page.nextStart;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return comments;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
normalizeComment(raw) {
|
|
549
|
+
const history = raw?.history || {};
|
|
550
|
+
const author = history.createdBy || {};
|
|
551
|
+
const extensions = raw?.extensions || {};
|
|
552
|
+
const ancestors = Array.isArray(raw?.ancestors)
|
|
553
|
+
? raw.ancestors.map((ancestor) => {
|
|
554
|
+
const id = ancestor?.id ?? ancestor;
|
|
555
|
+
return {
|
|
556
|
+
id: id !== undefined && id !== null ? String(id) : null,
|
|
557
|
+
type: ancestor?.type || null,
|
|
558
|
+
title: ancestor?.title || null
|
|
559
|
+
};
|
|
560
|
+
}).filter((ancestor) => ancestor.id)
|
|
561
|
+
: [];
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
id: raw?.id,
|
|
565
|
+
title: raw?.title,
|
|
566
|
+
status: raw?.status,
|
|
567
|
+
body: raw?.body?.storage?.value || '',
|
|
568
|
+
author: {
|
|
569
|
+
displayName: author.displayName || author.publicName || author.username || author.userKey || author.accountId || 'Unknown',
|
|
570
|
+
accountId: author.accountId,
|
|
571
|
+
userKey: author.userKey,
|
|
572
|
+
username: author.username,
|
|
573
|
+
email: author.email
|
|
574
|
+
},
|
|
575
|
+
createdAt: history.createdDate || null,
|
|
576
|
+
version: raw?.version?.number || null,
|
|
577
|
+
location: this.getCommentLocation(extensions),
|
|
578
|
+
inlineProperties: extensions.inlineProperties || null,
|
|
579
|
+
resolution: this.getCommentResolution(extensions),
|
|
580
|
+
parentId: this.getCommentParentId(ancestors),
|
|
581
|
+
ancestors,
|
|
582
|
+
extensions
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
getCommentParentId(ancestors = []) {
|
|
587
|
+
if (!Array.isArray(ancestors) || ancestors.length === 0) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
const commentAncestors = ancestors.filter((ancestor) => {
|
|
591
|
+
const type = ancestor?.type ? String(ancestor.type).toLowerCase() : '';
|
|
592
|
+
return type === 'comment';
|
|
593
|
+
});
|
|
594
|
+
if (commentAncestors.length === 0) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return commentAncestors[commentAncestors.length - 1].id || null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
getCommentLocation(extensions = {}) {
|
|
601
|
+
const location = extensions.location;
|
|
602
|
+
if (!location) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
if (typeof location === 'string') {
|
|
606
|
+
return location;
|
|
607
|
+
}
|
|
608
|
+
if (typeof location.value === 'string') {
|
|
609
|
+
return location.value;
|
|
610
|
+
}
|
|
611
|
+
if (typeof location.name === 'string') {
|
|
612
|
+
return location.name;
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
getCommentResolution(extensions = {}) {
|
|
618
|
+
const resolution = extensions.resolution;
|
|
619
|
+
if (!resolution) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
if (typeof resolution === 'string') {
|
|
623
|
+
return resolution;
|
|
624
|
+
}
|
|
625
|
+
if (typeof resolution.status === 'string') {
|
|
626
|
+
return resolution.status;
|
|
627
|
+
}
|
|
628
|
+
if (typeof resolution.value === 'string') {
|
|
629
|
+
return resolution.value;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
formatCommentBody(storageValue, format = 'text') {
|
|
635
|
+
const value = storageValue || '';
|
|
636
|
+
if (format === 'storage' || format === 'html') {
|
|
637
|
+
return value;
|
|
638
|
+
}
|
|
639
|
+
if (format === 'markdown') {
|
|
640
|
+
return this.storageToMarkdown(value);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return convert(value, {
|
|
644
|
+
wordwrap: 80,
|
|
645
|
+
selectors: [
|
|
646
|
+
{ selector: 'h1', options: { uppercase: false } },
|
|
647
|
+
{ selector: 'h2', options: { uppercase: false } },
|
|
648
|
+
{ selector: 'h3', options: { uppercase: false } },
|
|
649
|
+
{ selector: 'table', options: { uppercaseHeaderCells: false } }
|
|
650
|
+
]
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Create a comment on a page
|
|
656
|
+
*/
|
|
657
|
+
async createComment(pageIdOrUrl, content, format = 'storage', options = {}) {
|
|
658
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
659
|
+
let storageContent = content;
|
|
660
|
+
|
|
661
|
+
if (format === 'markdown') {
|
|
662
|
+
storageContent = this.markdownToStorage(content);
|
|
663
|
+
} else if (format === 'html') {
|
|
664
|
+
storageContent = this.htmlToConfluenceStorage(content);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const commentData = {
|
|
668
|
+
type: 'comment',
|
|
669
|
+
container: {
|
|
670
|
+
id: pageId,
|
|
671
|
+
type: 'page'
|
|
672
|
+
},
|
|
673
|
+
body: {
|
|
674
|
+
storage: {
|
|
675
|
+
value: storageContent,
|
|
676
|
+
representation: 'storage'
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
if (options.parentId) {
|
|
682
|
+
commentData.ancestors = [{ id: options.parentId }];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const extensions = {};
|
|
686
|
+
const location = options.location || (options.inlineProperties ? 'inline' : null);
|
|
687
|
+
if (location) {
|
|
688
|
+
extensions.location = location;
|
|
689
|
+
}
|
|
690
|
+
if (options.inlineProperties) {
|
|
691
|
+
extensions.inlineProperties = options.inlineProperties;
|
|
692
|
+
}
|
|
693
|
+
if (Object.keys(extensions).length > 0) {
|
|
694
|
+
commentData.extensions = extensions;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const response = await this.client.post('/content', commentData);
|
|
698
|
+
return response.data;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Delete a comment by ID
|
|
703
|
+
*/
|
|
704
|
+
async deleteComment(commentId) {
|
|
705
|
+
await this.client.delete(`/content/${commentId}`);
|
|
706
|
+
return { id: String(commentId) };
|
|
707
|
+
}
|
|
708
|
+
|
|
452
709
|
/**
|
|
453
710
|
* List attachments for a page with pagination support
|
|
454
711
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^20.10.0",
|
|
36
36
|
"axios-mock-adapter": "^2.1.0",
|
|
37
|
-
"eslint": "^
|
|
37
|
+
"eslint": "^9.39.2",
|
|
38
38
|
"jest": "^29.7.0"
|
|
39
39
|
},
|
|
40
40
|
"overrides": {
|
|
@@ -361,6 +361,80 @@ describe('ConfluenceClient', () => {
|
|
|
361
361
|
});
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
describe('comments', () => {
|
|
365
|
+
test('should list comments with location filter', async () => {
|
|
366
|
+
const mock = new MockAdapter(client.client);
|
|
367
|
+
mock.onGet('/content/123/child/comment').reply(config => {
|
|
368
|
+
expect(config.params.location).toBe('inline');
|
|
369
|
+
expect(config.params.expand).toContain('body.storage');
|
|
370
|
+
expect(config.params.expand).toContain('ancestors');
|
|
371
|
+
return [200, {
|
|
372
|
+
results: [
|
|
373
|
+
{
|
|
374
|
+
id: 'c1',
|
|
375
|
+
status: 'current',
|
|
376
|
+
body: { storage: { value: '<p>Hello</p>' } },
|
|
377
|
+
history: { createdBy: { displayName: 'Ada' }, createdDate: '2025-01-01' },
|
|
378
|
+
version: { number: 1 },
|
|
379
|
+
ancestors: [{ id: 'c0', type: 'comment' }],
|
|
380
|
+
extensions: {
|
|
381
|
+
location: 'inline',
|
|
382
|
+
inlineProperties: { selection: 'Hello', originalSelection: 'Hello' },
|
|
383
|
+
resolution: { status: 'open' }
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
_links: { next: '/rest/api/content/123/child/comment?start=2' }
|
|
388
|
+
}];
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const page = await client.listComments('123', { location: 'inline' });
|
|
392
|
+
expect(page.results).toHaveLength(1);
|
|
393
|
+
expect(page.results[0].location).toBe('inline');
|
|
394
|
+
expect(page.results[0].resolution).toBe('open');
|
|
395
|
+
expect(page.results[0].parentId).toBe('c0');
|
|
396
|
+
expect(page.nextStart).toBe(2);
|
|
397
|
+
|
|
398
|
+
mock.restore();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('should create inline comment with inline properties', async () => {
|
|
402
|
+
const mock = new MockAdapter(client.client);
|
|
403
|
+
mock.onPost('/content').reply(config => {
|
|
404
|
+
const payload = JSON.parse(config.data);
|
|
405
|
+
expect(payload.type).toBe('comment');
|
|
406
|
+
expect(payload.container.id).toBe('123');
|
|
407
|
+
expect(payload.body.storage.value).toBe('<p>Hi</p>');
|
|
408
|
+
expect(payload.ancestors[0].id).toBe('c0');
|
|
409
|
+
expect(payload.extensions.location).toBe('inline');
|
|
410
|
+
expect(payload.extensions.inlineProperties.originalSelection).toBe('Hi');
|
|
411
|
+
expect(payload.extensions.inlineProperties.markerRef).toBe('comment-1');
|
|
412
|
+
return [200, { id: 'c1', type: 'comment' }];
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
await client.createComment('123', '<p>Hi</p>', 'storage', {
|
|
416
|
+
parentId: 'c0',
|
|
417
|
+
location: 'inline',
|
|
418
|
+
inlineProperties: {
|
|
419
|
+
selection: 'Hi',
|
|
420
|
+
originalSelection: 'Hi',
|
|
421
|
+
markerRef: 'comment-1'
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
mock.restore();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('should delete a comment by ID', async () => {
|
|
429
|
+
const mock = new MockAdapter(client.client);
|
|
430
|
+
mock.onDelete('/content/456').reply(204);
|
|
431
|
+
|
|
432
|
+
await expect(client.deleteComment('456')).resolves.toEqual({ id: '456' });
|
|
433
|
+
|
|
434
|
+
mock.restore();
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
364
438
|
describe('attachments', () => {
|
|
365
439
|
test('should have required methods for attachment handling', () => {
|
|
366
440
|
expect(typeof client.listAttachments).toBe('function');
|