confluence-cli 1.10.1 → 1.11.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/lib/confluence-client.js +35 -8
- package/package.json +2 -1
- package/tests/confluence-client.test.js +53 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.11.0](https://github.com/pchuri/confluence-cli/compare/v1.10.1...v1.11.0) (2025-12-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Support for Confluence display URLs ([#20](https://github.com/pchuri/confluence-cli/issues/20)) ([3bda7c2](https://github.com/pchuri/confluence-cli/commit/3bda7c2aad8ec02dac60f3b7c34c31b549a31cce))
|
|
7
|
+
|
|
1
8
|
## [1.10.1](https://github.com/pchuri/confluence-cli/compare/v1.10.0...v1.10.1) (2025-12-08)
|
|
2
9
|
|
|
3
10
|
|
package/lib/confluence-client.js
CHANGED
|
@@ -49,7 +49,7 @@ class ConfluenceClient {
|
|
|
49
49
|
/**
|
|
50
50
|
* Extract page ID from URL or return the ID if it's already a number
|
|
51
51
|
*/
|
|
52
|
-
extractPageId(pageIdOrUrl) {
|
|
52
|
+
async extractPageId(pageIdOrUrl) {
|
|
53
53
|
if (typeof pageIdOrUrl === 'number' || /^\d+$/.test(pageIdOrUrl)) {
|
|
54
54
|
return pageIdOrUrl;
|
|
55
55
|
}
|
|
@@ -62,10 +62,37 @@ class ConfluenceClient {
|
|
|
62
62
|
return pageIdMatch[1];
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// Handle display URLs -
|
|
65
|
+
// Handle display URLs - search by space and title
|
|
66
66
|
const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
|
|
67
67
|
if (displayMatch) {
|
|
68
|
-
|
|
68
|
+
const spaceKey = displayMatch[1];
|
|
69
|
+
// Confluence friendly URLs for child pages might look like /display/SPACE/Parent/Child
|
|
70
|
+
// We only want the last part as the title
|
|
71
|
+
const urlPath = displayMatch[2];
|
|
72
|
+
const lastSegment = urlPath.split('/').pop();
|
|
73
|
+
|
|
74
|
+
// Confluence uses + for spaces in URL titles, but decodeURIComponent doesn't convert + to space
|
|
75
|
+
const rawTitle = lastSegment.replace(/\+/g, '%20');
|
|
76
|
+
const title = decodeURIComponent(rawTitle);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await this.client.get('/content', {
|
|
80
|
+
params: {
|
|
81
|
+
spaceKey: spaceKey,
|
|
82
|
+
title: title,
|
|
83
|
+
limit: 1
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (response.data.results && response.data.results.length > 0) {
|
|
88
|
+
return response.data.results[0].id;
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Ignore error and fall through
|
|
92
|
+
console.error('Error resolving page ID from display URL:', error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error(`Could not resolve page ID from display URL: ${pageIdOrUrl}`);
|
|
69
96
|
}
|
|
70
97
|
}
|
|
71
98
|
|
|
@@ -80,7 +107,7 @@ class ConfluenceClient {
|
|
|
80
107
|
* @param {boolean} options.resolveUsers - Whether to resolve userkeys to display names (default: true for markdown)
|
|
81
108
|
*/
|
|
82
109
|
async readPage(pageIdOrUrl, format = 'text', options = {}) {
|
|
83
|
-
const pageId = this.extractPageId(pageIdOrUrl);
|
|
110
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
84
111
|
|
|
85
112
|
const response = await this.client.get(`/content/${pageId}`, {
|
|
86
113
|
params: {
|
|
@@ -127,7 +154,7 @@ class ConfluenceClient {
|
|
|
127
154
|
* Get page information
|
|
128
155
|
*/
|
|
129
156
|
async getPageInfo(pageIdOrUrl) {
|
|
130
|
-
const pageId = this.extractPageId(pageIdOrUrl);
|
|
157
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
131
158
|
|
|
132
159
|
const response = await this.client.get(`/content/${pageId}`, {
|
|
133
160
|
params: {
|
|
@@ -335,7 +362,7 @@ class ConfluenceClient {
|
|
|
335
362
|
* List attachments for a page with pagination support
|
|
336
363
|
*/
|
|
337
364
|
async listAttachments(pageIdOrUrl, options = {}) {
|
|
338
|
-
const pageId = this.extractPageId(pageIdOrUrl);
|
|
365
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
339
366
|
const limit = this.parsePositiveInt(options.limit, 50);
|
|
340
367
|
const start = this.parsePositiveInt(options.start, 0);
|
|
341
368
|
const params = {
|
|
@@ -402,7 +429,7 @@ class ConfluenceClient {
|
|
|
402
429
|
downloadUrl = attachmentIdOrAttachment.downloadLink;
|
|
403
430
|
} else {
|
|
404
431
|
// Otherwise, fetch attachment info to get the download link
|
|
405
|
-
const pageId = this.extractPageId(pageIdOrUrl);
|
|
432
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
406
433
|
const attachmentId = attachmentIdOrAttachment;
|
|
407
434
|
const response = await this.client.get(`/content/${pageId}/child/attachment`, {
|
|
408
435
|
params: { limit: 500 }
|
|
@@ -985,7 +1012,7 @@ class ConfluenceClient {
|
|
|
985
1012
|
* Get page content for editing
|
|
986
1013
|
*/
|
|
987
1014
|
async getPageForEdit(pageIdOrUrl) {
|
|
988
|
-
const pageId = this.extractPageId(pageIdOrUrl);
|
|
1015
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
989
1016
|
|
|
990
1017
|
const response = await this.client.get(`/content/${pageId}`, {
|
|
991
1018
|
params: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^20.10.0",
|
|
35
|
+
"axios-mock-adapter": "^2.1.0",
|
|
35
36
|
"eslint": "^8.55.0",
|
|
36
37
|
"jest": "^29.7.0"
|
|
37
38
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ConfluenceClient = require('../lib/confluence-client');
|
|
2
|
+
const MockAdapter = require('axios-mock-adapter');
|
|
2
3
|
|
|
3
4
|
describe('ConfluenceClient', () => {
|
|
4
5
|
let client;
|
|
@@ -63,19 +64,64 @@ describe('ConfluenceClient', () => {
|
|
|
63
64
|
});
|
|
64
65
|
|
|
65
66
|
describe('extractPageId', () => {
|
|
66
|
-
test('should return numeric page ID as is', () => {
|
|
67
|
-
expect(client.extractPageId('123456789')).toBe('123456789');
|
|
68
|
-
expect(client.extractPageId(123456789)).toBe(123456789);
|
|
67
|
+
test('should return numeric page ID as is', async () => {
|
|
68
|
+
expect(await client.extractPageId('123456789')).toBe('123456789');
|
|
69
|
+
expect(await client.extractPageId(123456789)).toBe(123456789);
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
test('should extract page ID from URL with pageId parameter', () => {
|
|
72
|
+
test('should extract page ID from URL with pageId parameter', async () => {
|
|
72
73
|
const url = 'https://test.atlassian.net/wiki/spaces/TEST/pages/123456789/Page+Title';
|
|
73
|
-
expect(client.extractPageId(url + '?pageId=987654321')).toBe('987654321');
|
|
74
|
+
expect(await client.extractPageId(url + '?pageId=987654321')).toBe('987654321');
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
test('should
|
|
77
|
+
test('should resolve display URLs', async () => {
|
|
78
|
+
// Mock the API response for display URL resolution
|
|
79
|
+
const mock = new MockAdapter(client.client);
|
|
80
|
+
|
|
81
|
+
mock.onGet('/content').reply(200, {
|
|
82
|
+
results: [{
|
|
83
|
+
id: '12345',
|
|
84
|
+
title: 'Page Title',
|
|
85
|
+
_links: { webui: '/display/TEST/Page+Title' }
|
|
86
|
+
}]
|
|
87
|
+
});
|
|
88
|
+
|
|
77
89
|
const displayUrl = 'https://test.atlassian.net/display/TEST/Page+Title';
|
|
78
|
-
expect(
|
|
90
|
+
expect(await client.extractPageId(displayUrl)).toBe('12345');
|
|
91
|
+
|
|
92
|
+
mock.restore();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('should resolve nested display URLs', async () => {
|
|
96
|
+
// Mock the API response for display URL resolution
|
|
97
|
+
const mock = new MockAdapter(client.client);
|
|
98
|
+
|
|
99
|
+
mock.onGet('/content').reply(200, {
|
|
100
|
+
results: [{
|
|
101
|
+
id: '67890',
|
|
102
|
+
title: 'Child Page',
|
|
103
|
+
_links: { webui: '/display/TEST/Parent/Child+Page' }
|
|
104
|
+
}]
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const displayUrl = 'https://test.atlassian.net/display/TEST/Parent/Child+Page';
|
|
108
|
+
expect(await client.extractPageId(displayUrl)).toBe('67890');
|
|
109
|
+
|
|
110
|
+
mock.restore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should throw error when display URL cannot be resolved', async () => {
|
|
114
|
+
const mock = new MockAdapter(client.client);
|
|
115
|
+
|
|
116
|
+
// Mock empty result
|
|
117
|
+
mock.onGet('/content').reply(200, {
|
|
118
|
+
results: []
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const displayUrl = 'https://test.atlassian.net/display/TEST/NonExistentPage';
|
|
122
|
+
await expect(client.extractPageId(displayUrl)).rejects.toThrow(/Could not resolve page ID/);
|
|
123
|
+
|
|
124
|
+
mock.restore();
|
|
79
125
|
});
|
|
80
126
|
});
|
|
81
127
|
|