confluence-cli 2.4.0 → 2.5.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/README.md +3 -0
- package/bin/confluence.js +141 -0
- package/lib/confluence-client.js +142 -0
- package/lib/html-to-storage.js +3 -2
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -707,6 +707,9 @@ confluence stats
|
|
|
707
707
|
| `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <storage\|html\|markdown>` |
|
|
708
708
|
| `move <pageId_or_url> <newParentId_or_url>` | Move a page to a new parent location | `--title <string>` |
|
|
709
709
|
| `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` |
|
|
710
|
+
| `versions <pageId_or_url>` | List historical versions of a page | `--format <text\|json>` |
|
|
711
|
+
| `version-delete <pageId_or_url> <versionNumber>` | Delete a single non-current version of a page | `--yes` |
|
|
712
|
+
| `versions-purge <pageId_or_url>` | Delete every non-current historical version of a page | `--yes`, `--throttle <seconds>` |
|
|
710
713
|
| `edit <pageId>` | Export page content for editing | `--output <file>` |
|
|
711
714
|
| `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
|
|
712
715
|
| `attachment-upload <pageId_or_url>` | Upload attachments to a page | `--file <path>`, `--comment <text>`, `--replace`, `--minor-edit` |
|
package/bin/confluence.js
CHANGED
|
@@ -453,6 +453,147 @@ program
|
|
|
453
453
|
}
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
+
// List historical versions of a page
|
|
457
|
+
program
|
|
458
|
+
.command('versions <pageId>')
|
|
459
|
+
.description('List historical versions of a Confluence page')
|
|
460
|
+
.option('--format <format>', 'Output format: text or json (default: text)', 'text')
|
|
461
|
+
.action(async (pageId, options) => {
|
|
462
|
+
const analytics = new Analytics();
|
|
463
|
+
try {
|
|
464
|
+
const config = getConfig(getProfileName());
|
|
465
|
+
const client = new ConfluenceClient(config);
|
|
466
|
+
const resolvedId = String(await client.extractPageId(pageId));
|
|
467
|
+
const versions = await client.listVersions(resolvedId);
|
|
468
|
+
|
|
469
|
+
if (options.format === 'json') {
|
|
470
|
+
console.log(JSON.stringify({ pageId: resolvedId, versions }, null, 2));
|
|
471
|
+
} else {
|
|
472
|
+
const max = versions.length ? Math.max(...versions.map(v => v.number)) : 0;
|
|
473
|
+
console.log(chalk.blue(`Versions for page ${resolvedId} (${versions.length} total):`));
|
|
474
|
+
if (versions.length === 0) {
|
|
475
|
+
console.log(chalk.yellow(' (no versions returned)'));
|
|
476
|
+
}
|
|
477
|
+
for (const v of versions) {
|
|
478
|
+
const tag = v.number === max ? chalk.green(' [current]') : '';
|
|
479
|
+
const author = v.by || 'unknown';
|
|
480
|
+
const note = v.message ? ` — ${v.message}` : '';
|
|
481
|
+
console.log(` v${v.number}${tag} ${v.when} ${author}${note}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
analytics.track('versions', true);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
handleCommandError(analytics, 'versions', error);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
// Delete a single historical version of a page
|
|
491
|
+
program
|
|
492
|
+
.command('version-delete <pageId> <versionNumber>')
|
|
493
|
+
.description('Delete a single historical version of a page (cannot delete the current version)')
|
|
494
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
495
|
+
.action(async (pageId, versionNumber, options) => {
|
|
496
|
+
const analytics = new Analytics();
|
|
497
|
+
try {
|
|
498
|
+
const config = getConfig(getProfileName());
|
|
499
|
+
assertWritable(config);
|
|
500
|
+
const client = new ConfluenceClient(config);
|
|
501
|
+
const resolvedId = String(await client.extractPageId(pageId));
|
|
502
|
+
const n = Number(versionNumber);
|
|
503
|
+
|
|
504
|
+
if (!options.yes) {
|
|
505
|
+
const { confirmed } = await inquirer.prompt([{
|
|
506
|
+
type: 'confirm',
|
|
507
|
+
name: 'confirmed',
|
|
508
|
+
default: false,
|
|
509
|
+
message: `Delete v${n} of page ${resolvedId}? This cannot be undone.`
|
|
510
|
+
}]);
|
|
511
|
+
if (!confirmed) {
|
|
512
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
513
|
+
analytics.track('version_delete_cancel', true);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const result = await client.deleteVersion(resolvedId, n);
|
|
519
|
+
const note = result.viaExperimental ? chalk.yellow(' (via experimental endpoint)') : '';
|
|
520
|
+
console.log(chalk.green(`✅ Deleted v${result.versionNumber} of page ${result.id}${note}`));
|
|
521
|
+
analytics.track('version_delete', true);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
handleCommandError(analytics, 'version_delete', error);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// Convenience: delete every non-current historical version of a page,
|
|
528
|
+
// keeping only the current one.
|
|
529
|
+
program
|
|
530
|
+
.command('versions-purge <pageId>')
|
|
531
|
+
.description('Delete every non-current historical version of a page (keeps only current)')
|
|
532
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
533
|
+
.option('--throttle <seconds>', 'Sleep between version-delete calls', '0')
|
|
534
|
+
.action(async (pageId, options) => {
|
|
535
|
+
const analytics = new Analytics();
|
|
536
|
+
try {
|
|
537
|
+
const config = getConfig(getProfileName());
|
|
538
|
+
assertWritable(config);
|
|
539
|
+
const client = new ConfluenceClient(config);
|
|
540
|
+
const resolvedId = String(await client.extractPageId(pageId));
|
|
541
|
+
const versions = await client.listVersions(resolvedId);
|
|
542
|
+
|
|
543
|
+
if (versions.length === 0) {
|
|
544
|
+
console.log(chalk.yellow(`No versions returned for page ${resolvedId}.`));
|
|
545
|
+
analytics.track('versions_purge', true);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const max = Math.max(...versions.map(v => v.number));
|
|
549
|
+
const historicalCount = versions.filter(v => v.number !== max).length;
|
|
550
|
+
if (historicalCount === 0) {
|
|
551
|
+
console.log(chalk.yellow(`Only current version v${max} exists for page ${resolvedId}; nothing to purge.`));
|
|
552
|
+
analytics.track('versions_purge', true);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!options.yes) {
|
|
557
|
+
const { confirmed } = await inquirer.prompt([{
|
|
558
|
+
type: 'confirm',
|
|
559
|
+
name: 'confirmed',
|
|
560
|
+
default: false,
|
|
561
|
+
message: `Delete ${historicalCount} historical version(s) of page ${resolvedId}? Current version (v${max}) will be kept.`
|
|
562
|
+
}]);
|
|
563
|
+
if (!confirmed) {
|
|
564
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
565
|
+
analytics.track('versions_purge_cancel', true);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const throttleMs = Math.max(0, parseFloat(options.throttle || '0')) * 1000;
|
|
571
|
+
const result = await client.purgeNonCurrentVersions(resolvedId, {
|
|
572
|
+
onProgress: async (event) => {
|
|
573
|
+
if (event.kind === 'deleted') {
|
|
574
|
+
const note = event.viaExperimental ? chalk.yellow(' (experimental)') : '';
|
|
575
|
+
console.log(chalk.green(` ✓ deleted v${event.versionNumber}${note}`));
|
|
576
|
+
} else if (event.kind === 'failed') {
|
|
577
|
+
console.log(chalk.red(` ✗ v${event.versionNumber}: ${event.message}`));
|
|
578
|
+
}
|
|
579
|
+
if (throttleMs > 0) {
|
|
580
|
+
await new Promise(r => setTimeout(r, throttleMs));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
console.log('');
|
|
586
|
+
console.log(chalk.green(`✅ Purge complete for page ${result.id}: ` +
|
|
587
|
+
`${result.deleted} deleted, ${result.failed} failed, kept v${result.kept}.`));
|
|
588
|
+
analytics.track('versions_purge', result.failed === 0);
|
|
589
|
+
if (result.failed > 0) {
|
|
590
|
+
process.exitCode = 1;
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
handleCommandError(analytics, 'versions_purge', error);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
456
597
|
// Edit command - opens page content for editing
|
|
457
598
|
program
|
|
458
599
|
.command('edit <pageId>')
|
package/lib/confluence-client.js
CHANGED
|
@@ -1443,6 +1443,148 @@ class ConfluenceClient {
|
|
|
1443
1443
|
return { id: String(pageId) };
|
|
1444
1444
|
}
|
|
1445
1445
|
|
|
1446
|
+
/**
|
|
1447
|
+
* Build the absolute URL for the experimental version endpoint.
|
|
1448
|
+
* Confluence Server/Data Center exposes content versions only at
|
|
1449
|
+
* /rest/experimental/ (the modern /rest/api/.../version path 404s
|
|
1450
|
+
* there). Cloud accepts both. We use this as a fallback when the
|
|
1451
|
+
* configured apiPath returns 404/405.
|
|
1452
|
+
*/
|
|
1453
|
+
experimentalVersionUrl(pageId, versionNumber = null) {
|
|
1454
|
+
const base = `${this.protocol}://${this.domain}${this.webUrlPrefix}/rest/experimental/content/${pageId}/version`;
|
|
1455
|
+
return versionNumber == null ? base : `${base}/${versionNumber}`;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* List historical versions of a page. Returns an array sorted by
|
|
1460
|
+
* version number ascending. Each entry has: number, when, by,
|
|
1461
|
+
* minorEdit, message.
|
|
1462
|
+
*
|
|
1463
|
+
* Pages with many versions (e.g. heavily edited or repeatedly
|
|
1464
|
+
* uploaded) may exceed a single page of results, so this paginates
|
|
1465
|
+
* via start/limit until all versions are collected.
|
|
1466
|
+
*
|
|
1467
|
+
* Path strategy: try the configured /rest/api/ path first; on 404
|
|
1468
|
+
* or 405 (typical of Server/DC where the version endpoints live
|
|
1469
|
+
* under /rest/experimental/), fall back transparently. Subsequent
|
|
1470
|
+
* pages use whichever path succeeded so we don't double the
|
|
1471
|
+
* round-trip count on long histories.
|
|
1472
|
+
*/
|
|
1473
|
+
async listVersions(pageIdOrUrl) {
|
|
1474
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
1475
|
+
const limit = 200;
|
|
1476
|
+
let start = 0;
|
|
1477
|
+
let useExperimental = false;
|
|
1478
|
+
const all = [];
|
|
1479
|
+
while (true) {
|
|
1480
|
+
let response;
|
|
1481
|
+
try {
|
|
1482
|
+
const url = useExperimental
|
|
1483
|
+
? this.experimentalVersionUrl(pageId)
|
|
1484
|
+
: `/content/${pageId}/version`;
|
|
1485
|
+
response = await this.client.get(url, { params: { start, limit } });
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
const status = error.response && error.response.status;
|
|
1488
|
+
if (!useExperimental && (status === 404 || status === 405)) {
|
|
1489
|
+
useExperimental = true;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
const results = response.data.results || [];
|
|
1495
|
+
all.push(...results);
|
|
1496
|
+
if (results.length < limit) {
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
start += limit;
|
|
1500
|
+
}
|
|
1501
|
+
return all.map(v => ({
|
|
1502
|
+
number: v.number,
|
|
1503
|
+
when: v.when,
|
|
1504
|
+
by: v.by ? (v.by.displayName || v.by.publicName || v.by.email || null) : null,
|
|
1505
|
+
minorEdit: v.minorEdit === true,
|
|
1506
|
+
message: v.message || ''
|
|
1507
|
+
})).sort((a, b) => a.number - b.number);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Delete a single historical version of a page.
|
|
1512
|
+
*
|
|
1513
|
+
* Confluence refuses to delete the current version (returns 400).
|
|
1514
|
+
* The "rolled up into the next version" wording in Atlassian's
|
|
1515
|
+
* docs refers to the diff representation; the deleted version's
|
|
1516
|
+
* snapshot is removed from page history.
|
|
1517
|
+
*
|
|
1518
|
+
* Server/DC instances expose this only at /rest/experimental/, so
|
|
1519
|
+
* a 404 or 405 on the configured /rest/api/ path triggers a
|
|
1520
|
+
* one-shot retry against the experimental URL.
|
|
1521
|
+
*/
|
|
1522
|
+
async deleteVersion(pageIdOrUrl, versionNumber) {
|
|
1523
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
1524
|
+
const n = Number(versionNumber);
|
|
1525
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1526
|
+
throw new Error(`versionNumber must be a positive integer, got: ${versionNumber}`);
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
await this.client.delete(`/content/${pageId}/version/${n}`);
|
|
1530
|
+
return { id: String(pageId), versionNumber: n, viaExperimental: false };
|
|
1531
|
+
} catch (error) {
|
|
1532
|
+
const status = error.response && error.response.status;
|
|
1533
|
+
if (status !== 404 && status !== 405) {
|
|
1534
|
+
throw error;
|
|
1535
|
+
}
|
|
1536
|
+
await this.client.delete(this.experimentalVersionUrl(pageId, n));
|
|
1537
|
+
return { id: String(pageId), versionNumber: n, viaExperimental: true };
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Delete every non-current version of a page. Used to purge the
|
|
1543
|
+
* historical content snapshots that may still hold a leaked secret
|
|
1544
|
+
* even after the current version has been overwritten with a
|
|
1545
|
+
* redacted copy.
|
|
1546
|
+
*
|
|
1547
|
+
* Returns { id, kept, deleted, failed } where `kept` is the version
|
|
1548
|
+
* number that survived (the current one) and `deleted`/`failed` are
|
|
1549
|
+
* the counts of historical versions removed/errored.
|
|
1550
|
+
*/
|
|
1551
|
+
async purgeNonCurrentVersions(pageIdOrUrl, options = {}) {
|
|
1552
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
1553
|
+
const pageId = String(await this.extractPageId(pageIdOrUrl));
|
|
1554
|
+
const versions = await this.listVersions(pageId);
|
|
1555
|
+
|
|
1556
|
+
if (versions.length === 0) {
|
|
1557
|
+
return { id: pageId, kept: null, deleted: 0, failed: 0, errors: [] };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const max = Math.max(...versions.map(v => v.number));
|
|
1561
|
+
// Delete in descending order. Some Confluence builds renumber
|
|
1562
|
+
// historical versions when an earlier one is removed; deleting
|
|
1563
|
+
// newest-first sidesteps the resulting off-by-one drift.
|
|
1564
|
+
const toDelete = versions
|
|
1565
|
+
.filter(v => v.number !== max)
|
|
1566
|
+
.map(v => v.number)
|
|
1567
|
+
.sort((a, b) => b - a);
|
|
1568
|
+
|
|
1569
|
+
let deleted = 0;
|
|
1570
|
+
const errors = [];
|
|
1571
|
+
for (const n of toDelete) {
|
|
1572
|
+
try {
|
|
1573
|
+
const res = await this.deleteVersion(pageId, n);
|
|
1574
|
+
deleted += 1;
|
|
1575
|
+
onProgress({ kind: 'deleted', versionNumber: n, viaExperimental: res.viaExperimental });
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
const detail = error.response
|
|
1578
|
+
? `HTTP ${error.response.status} ${error.response.statusText || ''}`.trim()
|
|
1579
|
+
: error.message;
|
|
1580
|
+
errors.push({ versionNumber: n, message: detail });
|
|
1581
|
+
onProgress({ kind: 'failed', versionNumber: n, message: detail });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
return { id: pageId, kept: max, deleted, failed: errors.length, errors };
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1446
1588
|
/**
|
|
1447
1589
|
* Search for a page by title and space
|
|
1448
1590
|
*/
|
package/lib/html-to-storage.js
CHANGED
|
@@ -344,10 +344,11 @@ function dispatchTag(node, ctx) {
|
|
|
344
344
|
return `<img${renderAttrs(node.attribs)}>`;
|
|
345
345
|
case 'ul':
|
|
346
346
|
case 'ol':
|
|
347
|
-
return `<${node.name}>${walkChildren(node, ctx)}</${node.name}>`;
|
|
347
|
+
return `<${node.name}${renderAttrs(node.attribs)}>${walkChildren(node, ctx)}</${node.name}>`;
|
|
348
348
|
case 'li': {
|
|
349
349
|
const inner = walkChildren(node, ctx);
|
|
350
|
-
|
|
350
|
+
const open = `<li${renderAttrs(node.attribs)}>`;
|
|
351
|
+
return shouldWrapInP(node) ? `${open}<p>${inner}</p></li>` : `${open}${inner}</li>`;
|
|
351
352
|
}
|
|
352
353
|
case 'pre':
|
|
353
354
|
return convertCodeBlock(node, ctx);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.5.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"axios": "
|
|
12
|
+
"axios": "~1.15.2",
|
|
13
13
|
"chalk": "^4.1.2",
|
|
14
14
|
"commander": "^11.1.0",
|
|
15
15
|
"entities": "^4.5.0",
|
|
@@ -129,9 +129,9 @@
|
|
|
129
129
|
"license": "MIT"
|
|
130
130
|
},
|
|
131
131
|
"node_modules/axios": {
|
|
132
|
-
"version": "1.15.
|
|
133
|
-
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.
|
|
134
|
-
"integrity": "sha512-
|
|
132
|
+
"version": "1.15.2",
|
|
133
|
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
|
134
|
+
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
|
|
135
135
|
"license": "MIT",
|
|
136
136
|
"dependencies": {
|
|
137
137
|
"follow-redirects": "^1.15.11",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "pchuri",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"axios": "
|
|
26
|
+
"axios": "~1.15.2",
|
|
27
27
|
"chalk": "^4.1.2",
|
|
28
28
|
"commander": "^11.1.0",
|
|
29
29
|
"entities": "^4.5.0",
|