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 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
 
@@ -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 - would need to search by space and title
65
+ // Handle display URLs - search by space and title
66
66
  const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
67
67
  if (displayMatch) {
68
- throw new Error('Display URLs not yet supported. Please use page ID or viewpage URL with pageId parameter.');
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.10.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 throw error for display URLs', () => {
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(() => client.extractPageId(displayUrl)).toThrow('Display URLs not yet supported');
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