confluence-cli 1.15.0 → 1.16.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,17 @@
1
+ # [1.16.0](https://github.com/pchuri/confluence-cli/compare/v1.15.1...v1.16.0) (2026-02-13)
2
+
3
+
4
+ ### Features
5
+
6
+ * Add move command to relocate pages ([#32](https://github.com/pchuri/confluence-cli/issues/32)) ([a37f9b8](https://github.com/pchuri/confluence-cli/commit/a37f9b83174ef08a6517fe279dcce1b39fc1fb1a))
7
+
8
+ ## [1.15.1](https://github.com/pchuri/confluence-cli/compare/v1.15.0...v1.15.1) (2026-02-12)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * parse page ID from pretty URLs ([#34](https://github.com/pchuri/confluence-cli/issues/34)) ([6f22cd5](https://github.com/pchuri/confluence-cli/commit/6f22cd5424aec11a330a3cc7faf9cbeccb943168))
14
+
1
15
  # [1.15.0](https://github.com/pchuri/confluence-cli/compare/v1.14.0...v1.15.0) (2026-02-06)
2
16
 
3
17
 
package/README.md CHANGED
@@ -296,6 +296,22 @@ confluence update 123456789 --file ./updated-content.md --format markdown
296
296
  confluence update 123456789 --title "New Title" --content "And new content"
297
297
  ```
298
298
 
299
+ ### Move a Page to New Parent
300
+
301
+ ```bash
302
+ # Move page by ID
303
+ confluence move 123456789 987654321
304
+
305
+ # Move page and rename it
306
+ confluence move 123456789 987654321 --title "Relocated Page"
307
+
308
+ # Move using URLs (for convenience)
309
+ confluence move "https://domain.atlassian.net/wiki/viewpage.action?pageId=123456789" \
310
+ "https://domain.atlassian.net/wiki/viewpage.action?pageId=987654321"
311
+ ```
312
+
313
+ **Note:** Pages can only be moved within the same Confluence space. Cross-space moves are not supported.
314
+
299
315
  ### Delete a Page
300
316
  ```bash
301
317
  # Delete by page ID (prompts for confirmation)
@@ -341,6 +357,7 @@ confluence stats
341
357
  | `create-child <title> <parentId>` | Create a child page | `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
342
358
  | `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` |
343
359
  | `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
360
+ | `move <pageId_or_url> <newParentId_or_url>` | Move a page to a new parent location | `--title <string>` |
344
361
  | `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` |
345
362
  | `edit <pageId>` | Export page content for editing | `--output <file>` |
346
363
  | `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
@@ -371,6 +388,12 @@ confluence search "API documentation" --limit 3
371
388
  # List all spaces
372
389
  confluence spaces
373
390
 
391
+ # Move a page to a new parent
392
+ confluence move 123456789 987654321
393
+
394
+ # Move and rename
395
+ confluence move 123456789 987654321 --title "New Title"
396
+
374
397
  # View usage statistics
375
398
  confluence stats
376
399
  ```
package/bin/confluence.js CHANGED
@@ -277,6 +277,33 @@ program
277
277
  }
278
278
  });
279
279
 
280
+ // Move command
281
+ program
282
+ .command('move <pageId_or_url> <newParentId_or_url>')
283
+ .description('Move a page to a new parent location (within same space)')
284
+ .option('-t, --title <title>', 'New page title (optional)')
285
+ .action(async (pageId, newParentId, options) => {
286
+ const analytics = new Analytics();
287
+ try {
288
+ const config = getConfig();
289
+ const client = new ConfluenceClient(config);
290
+ const result = await client.movePage(pageId, newParentId, options.title);
291
+
292
+ console.log(chalk.green('✅ Page moved successfully!'));
293
+ console.log(`Title: ${chalk.blue(result.title)}`);
294
+ console.log(`ID: ${chalk.blue(result.id)}`);
295
+ console.log(`New Parent: ${chalk.blue(newParentId)}`);
296
+ console.log(`Version: ${chalk.blue(result.version.number)}`);
297
+ console.log(`URL: ${chalk.gray(`https://${config.domain}/wiki${result._links.webui}`)}`);
298
+
299
+ analytics.track('move', true);
300
+ } catch (error) {
301
+ analytics.track('move', false);
302
+ console.error(chalk.red('Error:'), error.message);
303
+ process.exit(1);
304
+ }
305
+ });
306
+
280
307
  // Delete command
281
308
  program
282
309
  .command('delete <pageIdOrUrl>')
@@ -61,7 +61,12 @@ class ConfluenceClient {
61
61
  if (pageIdMatch) {
62
62
  return pageIdMatch[1];
63
63
  }
64
-
64
+
65
+ const prettyMatch = pageIdOrUrl.match(/\/pages\/(\d+)(?:[/?#]|$)/);
66
+ if (prettyMatch) {
67
+ return prettyMatch[1];
68
+ }
69
+
65
70
  // Handle display URLs - search by space and title
66
71
  const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
67
72
  if (displayMatch) {
@@ -1444,6 +1449,55 @@ class ConfluenceClient {
1444
1449
  return response.data;
1445
1450
  }
1446
1451
 
1452
+ /**
1453
+ * Move a page to a new parent location
1454
+ */
1455
+ async movePage(pageIdOrUrl, newParentIdOrUrl, newTitle = null) {
1456
+ // Resolve both IDs from URLs if needed
1457
+ const pageId = await this.extractPageId(pageIdOrUrl);
1458
+ const newParentId = await this.extractPageId(newParentIdOrUrl);
1459
+
1460
+ // Fetch current page
1461
+ const response = await this.client.get(`/content/${pageId}`, {
1462
+ params: { expand: 'body.storage,version,space' }
1463
+ });
1464
+ const { version, title, body, space } = response.data;
1465
+
1466
+ // Fetch new parent to get its space (for validation)
1467
+ const parentResponse = await this.client.get(`/content/${newParentId}`, {
1468
+ params: { expand: 'space' }
1469
+ });
1470
+ const parentSpace = parentResponse.data.space;
1471
+
1472
+ // Validate same space
1473
+ if (parentSpace.key !== space.key) {
1474
+ throw new Error(
1475
+ `Cannot move page across spaces. Page is in space "${space.key}" ` +
1476
+ `but new parent is in space "${parentSpace.key}". ` +
1477
+ 'Pages can only be moved within the same space.'
1478
+ );
1479
+ }
1480
+
1481
+ // Proceed with move
1482
+ const pageData = {
1483
+ id: pageId,
1484
+ type: 'page',
1485
+ title: newTitle || title,
1486
+ space: { key: space.key },
1487
+ body: {
1488
+ storage: {
1489
+ value: body.storage.value,
1490
+ representation: 'storage'
1491
+ }
1492
+ },
1493
+ version: { number: version.number + 1 },
1494
+ ancestors: [{ id: newParentId }]
1495
+ };
1496
+
1497
+ const updateResponse = await this.client.put(`/content/${pageId}`, pageData);
1498
+ return updateResponse.data;
1499
+ }
1500
+
1447
1501
  /**
1448
1502
  * Get page content for editing
1449
1503
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.15.0",
3
+ "version": "1.16.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": {
@@ -74,6 +74,11 @@ describe('ConfluenceClient', () => {
74
74
  expect(await client.extractPageId(url + '?pageId=987654321')).toBe('987654321');
75
75
  });
76
76
 
77
+ test('should extract page ID from pretty URL path', async () => {
78
+ const url = 'https://test.atlassian.net/wiki/spaces/TEST/pages/123456789/Page+Title';
79
+ expect(await client.extractPageId(url)).toBe('123456789');
80
+ });
81
+
77
82
  test('should resolve display URLs', async () => {
78
83
  // Mock the API response for display URL resolution
79
84
  const mock = new MockAdapter(client.client);
@@ -293,6 +298,160 @@ describe('ConfluenceClient', () => {
293
298
  });
294
299
  });
295
300
 
301
+ describe('movePage', () => {
302
+ test('should move a page by ID', async () => {
303
+ const mock = new MockAdapter(client.client);
304
+
305
+ mock.onGet('/content/123456789').reply(200, {
306
+ id: '123456789',
307
+ title: 'Original Title',
308
+ version: { number: 5 },
309
+ body: { storage: { value: '<p>Original content</p>' } },
310
+ space: { key: 'TEST' }
311
+ });
312
+
313
+ mock.onGet('/content/987654321').reply(200, {
314
+ id: '987654321',
315
+ space: { key: 'TEST' }
316
+ });
317
+
318
+ mock.onPut('/content/123456789').reply(200, {
319
+ id: '123456789',
320
+ title: 'Original Title',
321
+ version: { number: 6 },
322
+ ancestors: [{ id: '987654321' }]
323
+ });
324
+
325
+ const result = await client.movePage('123456789', '987654321');
326
+
327
+ expect(result.id).toBe('123456789');
328
+ expect(result.version.number).toBe(6);
329
+ expect(result.ancestors).toEqual([{ id: '987654321' }]);
330
+
331
+ mock.restore();
332
+ });
333
+
334
+ test('should move a page with new title', async () => {
335
+ const mock = new MockAdapter(client.client);
336
+
337
+ mock.onGet('/content/555666777').reply(200, {
338
+ id: '555666777',
339
+ title: 'Old Title',
340
+ version: { number: 2 },
341
+ body: { storage: { value: '<p>Page content</p>' } },
342
+ space: { key: 'DOCS' }
343
+ });
344
+
345
+ mock.onGet('/content/888999000').reply(200, {
346
+ id: '888999000',
347
+ space: { key: 'DOCS' }
348
+ });
349
+
350
+ mock.onPut('/content/555666777').reply(200, {
351
+ id: '555666777',
352
+ title: 'New Title',
353
+ version: { number: 3 },
354
+ ancestors: [{ id: '888999000' }]
355
+ });
356
+
357
+ const result = await client.movePage('555666777', '888999000', 'New Title');
358
+
359
+ expect(result.title).toBe('New Title');
360
+ expect(result.version.number).toBe(3);
361
+ expect(result.ancestors).toEqual([{ id: '888999000' }]);
362
+
363
+ mock.restore();
364
+ });
365
+
366
+ test('should move a page using URL for pageId', async () => {
367
+ const mock = new MockAdapter(client.client);
368
+ const pageUrl = 'https://test.atlassian.net/wiki/viewpage.action?pageId=111222333';
369
+
370
+ mock.onGet('/content/111222333').reply(200, {
371
+ id: '111222333',
372
+ title: 'Test Page',
373
+ version: { number: 1 },
374
+ body: { storage: { value: '<p>Content</p>' } },
375
+ space: { key: 'TEST' }
376
+ });
377
+
378
+ mock.onGet('/content/444555666').reply(200, {
379
+ id: '444555666',
380
+ space: { key: 'TEST' }
381
+ });
382
+
383
+ mock.onPut('/content/111222333').reply(200, {
384
+ id: '111222333',
385
+ title: 'Test Page',
386
+ version: { number: 2 },
387
+ ancestors: [{ id: '444555666' }]
388
+ });
389
+
390
+ const result = await client.movePage(pageUrl, '444555666');
391
+
392
+ expect(result.id).toBe('111222333');
393
+ expect(result.version.number).toBe(2);
394
+
395
+ mock.restore();
396
+ });
397
+
398
+ test('should move a page using URLs for both parameters', async () => {
399
+ const mock = new MockAdapter(client.client);
400
+ const pageUrl = 'https://test.atlassian.net/wiki/viewpage.action?pageId=777888999';
401
+ const parentUrl = 'https://test.atlassian.net/wiki/viewpage.action?pageId=111000111';
402
+
403
+ mock.onGet('/content/777888999').reply(200, {
404
+ id: '777888999',
405
+ title: 'Source Page',
406
+ version: { number: 3 },
407
+ body: { storage: { value: '<p>Page content</p>' } },
408
+ space: { key: 'DOCS' }
409
+ });
410
+
411
+ mock.onGet('/content/111000111').reply(200, {
412
+ id: '111000111',
413
+ space: { key: 'DOCS' }
414
+ });
415
+
416
+ mock.onPut('/content/777888999').reply(200, {
417
+ id: '777888999',
418
+ title: 'Source Page',
419
+ version: { number: 4 },
420
+ ancestors: [{ id: '111000111' }]
421
+ });
422
+
423
+ const result = await client.movePage(pageUrl, parentUrl);
424
+
425
+ expect(result.id).toBe('777888999');
426
+ expect(result.version.number).toBe(4);
427
+
428
+ mock.restore();
429
+ });
430
+
431
+ test('should throw error when moving page across spaces', async () => {
432
+ const mock = new MockAdapter(client.client);
433
+
434
+ mock.onGet('/content/123456789').reply(200, {
435
+ id: '123456789',
436
+ title: 'Page in Space A',
437
+ version: { number: 1 },
438
+ body: { storage: { value: '<p>Content</p>' } },
439
+ space: { key: 'SPACEA' }
440
+ });
441
+
442
+ mock.onGet('/content/987654321').reply(200, {
443
+ id: '987654321',
444
+ space: { key: 'SPACEB' }
445
+ });
446
+
447
+ await expect(
448
+ client.movePage('123456789', '987654321')
449
+ ).rejects.toThrow('Cannot move page across spaces');
450
+
451
+ mock.restore();
452
+ });
453
+ });
454
+
296
455
  describe('page tree operations', () => {
297
456
  test('should have required methods for tree operations', () => {
298
457
  expect(typeof client.getChildPages).toBe('function');