confluence-cli 1.15.1 → 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 +7 -0
- package/README.md +23 -0
- package/bin/confluence.js +27 -0
- package/lib/confluence-client.js +49 -0
- package/package.json +1 -1
- package/tests/confluence-client.test.js +154 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
## [1.15.1](https://github.com/pchuri/confluence-cli/compare/v1.15.0...v1.15.1) (2026-02-12)
|
|
2
9
|
|
|
3
10
|
|
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>')
|
package/lib/confluence-client.js
CHANGED
|
@@ -1449,6 +1449,55 @@ class ConfluenceClient {
|
|
|
1449
1449
|
return response.data;
|
|
1450
1450
|
}
|
|
1451
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
|
+
|
|
1452
1501
|
/**
|
|
1453
1502
|
* Get page content for editing
|
|
1454
1503
|
*/
|
package/package.json
CHANGED
|
@@ -298,6 +298,160 @@ describe('ConfluenceClient', () => {
|
|
|
298
298
|
});
|
|
299
299
|
});
|
|
300
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
|
+
|
|
301
455
|
describe('page tree operations', () => {
|
|
302
456
|
test('should have required methods for tree operations', () => {
|
|
303
457
|
expect(typeof client.getChildPages).toBe('function');
|