Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,16 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read

### JSON output (for scripting / jq)

Pass the global `--json` flag to make any read command emit raw JSON to stdout, so you can pipe it to tools like `jq`:
Pass the global `--json` flag to make a command emit raw JSON to stdout, so you can pipe it to tools like `jq`:

```bash
confluence search "release notes" --json | jq '.results[].title'
confluence spaces --json | jq -r '.spaces[].key'
confluence info 123456789 --json | jq '.version'
confluence create "Notes" ENG --content "hi" --json | jq -r '.id' # capture the new page id
```

`--json` works on `info`, `search`, `spaces`, `find`, `children`, `versions`, `comments`, `attachments`, `property-list`, `property-get`, and `property-set`. Passing `--json` to any other command is rejected with an error rather than silently ignored. Human-readable messages and warnings go to stderr, so stdout stays valid JSON.
`--json` works on read commands (`info`, `search`, `spaces`, `find`, `children`, `versions`, `comments`, `attachments`, `property-list`/`-get`/`-set`) and write commands (`create`, `create-child`, `update`, `move`, `delete`, `copy-tree`, `comment`, `comment-delete`, `property-delete`, `attachment-upload`/`-delete`, `version-delete`, `versions-purge`). For destructive commands, `--json` requires `--yes` — it will not prompt interactively. Passing `--json` to a command that doesn't support it (e.g. `init`, `convert`, `export`, `edit`) is rejected with an error rather than silently ignored. Human-readable messages and warnings go to stderr, so stdout stays valid JSON.

> **Deprecation:** the per-command `--format json` form is deprecated in favor of the global `--json` flag. It still works but prints a warning to stderr and will be removed in a future major version.

Expand Down Expand Up @@ -792,26 +793,26 @@ confluence stats
| `spaces` | List available spaces | `--json`, `--limit <number>`, `--all` |
| `find <title>` | Find a page by its title | `--space <spaceKey>`, `--json` |
| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree>`, `--json`, `--show-url`, `--show-id` |
| `create <title> <spaceKey>` | Create a new page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>` |
| `create-child <title> <parentId>` | Create a child page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>` |
| `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` |
| `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>` |
| `move <pageId_or_url> <newParentId_or_url>` | Move a page to a new parent location | `--title <string>` |
| `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes` |
| `create <title> <spaceKey>` | Create a new page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>`, `--json` |
| `create-child <title> <parentId>` | Create a child page or folder | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--type <page\|folder>`, `--json` |
| `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`, `--json` |
| `update <pageId>` | Update a page's title or content | `--title <string>`, `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--json` |
| `move <pageId_or_url> <newParentId_or_url>` | Move a page to a new parent location | `--title <string>`, `--json` |
| `delete <pageId_or_url>` | Delete a page by ID or URL | `--yes`, `--json` |
| `versions <pageId_or_url>` | List historical versions of a page | `--json` |
| `version-delete <pageId_or_url> <versionNumber>` | Delete a single non-current version of a page | `--yes` |
| `versions-purge <pageId_or_url>` | Delete every non-current historical version of a page | `--yes`, `--throttle <seconds>` |
| `version-delete <pageId_or_url> <versionNumber>` | Delete a single non-current version of a page | `--yes`, `--json` |
| `versions-purge <pageId_or_url>` | Delete every non-current historical version of a page | `--yes`, `--throttle <seconds>`, `--json` |
| `edit <pageId>` | Export page content for editing | `--output <file>` |
| `attachments <pageId_or_url>` | List or download attachments for a page | `--json`, `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
| `attachment-upload <pageId_or_url>` | Upload attachments to a page | `--file <path>`, `--comment <text>`, `--replace`, `--minor-edit` |
| `attachment-delete <pageId_or_url> <attachmentId>` | Delete an attachment from a page | `--yes` |
| `attachment-upload <pageId_or_url>` | Upload attachments to a page | `--file <path>`, `--comment <text>`, `--replace`, `--minor-edit`, `--json` |
| `attachment-delete <pageId_or_url> <attachmentId>` | Delete an attachment from a page | `--yes`, `--json` |
| `comments <pageId_or_url>` | List comments for a page | `--format <text\|markdown>`, `--json`, `--limit <number>`, `--start <number>`, `--location <inline\|footer\|resolved>`, `--depth <root\|all>`, `--all` |
| `comment <pageId_or_url>` | Create a comment on a page | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--parent <commentId>`, `--location <inline\|footer>`, `--inline-selection <text>`, `--inline-original-selection <text>`, `--inline-marker-ref <ref>`, `--inline-properties <json>` |
| `comment-delete <commentId>` | Delete a comment by ID | `--yes` |
| `comment <pageId_or_url>` | Create a comment on a page | `--content <string>`, `--file <path>`, `--format <auto\|storage\|html\|markdown>`, `--parent <commentId>`, `--location <inline\|footer>`, `--inline-selection <text>`, `--inline-original-selection <text>`, `--inline-marker-ref <ref>`, `--inline-properties <json>`, `--json` |
| `comment-delete <commentId>` | Delete a comment by ID | `--yes`, `--json` |
| `property-list <pageId_or_url>` | List all content properties for a page | `--json`, `--limit <number>`, `--start <number>`, `--all` |
| `property-get <pageId_or_url> <key>` | Get a content property by key | `--json` |
| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--json` |
| `property-delete <pageId_or_url> <key>` | Delete a content property by key | `--yes` |
| `property-delete <pageId_or_url> <key>` | Delete a content property by key | `--yes`, `--json` |
| `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` |
| `profile list` | List all configuration profiles | |
| `profile use <name>` | Set the active configuration profile | |
Expand Down
39 changes: 30 additions & 9 deletions bin/commands/attachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ function registerAttachmentCommands(program, { withClient }) {
.option('--comment <comment>', 'Comment for the attachment(s)')
.option('--replace', 'Replace an existing attachment with the same filename')
.option('--minor-edit', 'Mark the upload as a minor edit')
.action(withClient('attachment_upload', async ({ client, analytics }, pageId, options) => {
.action(withClient('attachment_upload', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const jsonMode = wantsJson();
const files = Array.isArray(options.file) ? options.file.filter(Boolean) : [];
if (files.length === 0) {
throw new Error('At least one --file option is required.');
Expand All @@ -145,32 +146,46 @@ function registerAttachmentCommands(program, { withClient }) {
}
});

let uploaded = 0;
const uploadedItems = [];
for (const file of resolvedFiles) {
const result = await client.uploadAttachment(pageId, file.resolved, {
comment: options.comment,
replace: options.replace,
minorEdit: options.minorEdit === true ? true : undefined,
});
const attachment = result.results[0];
if (attachment) {
console.log(`⬆️ ${chalk.green(attachment.title)} (ID: ${attachment.id}, Version: ${attachment.version})`);
} else {
console.log(`⬆️ ${chalk.green(path.basename(file.resolved))}`);
uploadedItems.push(attachment
? { id: attachment.id, title: attachment.title, version: attachment.version }
: { title: path.basename(file.resolved) });
if (!jsonMode) {
if (attachment) {
console.log(`⬆️ ${chalk.green(attachment.title)} (ID: ${attachment.id}, Version: ${attachment.version})`);
} else {
console.log(`⬆️ ${chalk.green(path.basename(file.resolved))}`);
}
}
uploaded += 1;
}

console.log(chalk.green(`Uploaded ${uploaded} attachment${uploaded === 1 ? '' : 's'} to page ${pageId}`));
if (jsonMode) {
emitJson({ pageId, uploaded: uploadedItems });
analytics.track('attachment_upload', true);
return;
}

console.log(chalk.green(`Uploaded ${uploadedItems.length} attachment${uploadedItems.length === 1 ? '' : 's'} to page ${pageId}`));
analytics.track('attachment_upload', true);
}, { writable: true }));

program
.command('attachment-delete <pageId> <attachmentId>')
.description('Delete an attachment by ID from a page')
.option('-y, --yes', 'Skip confirmation prompt')
.action(withClient('attachment_delete', async ({ client, analytics }, pageId, attachmentId, options) => {
.action(withClient('attachment_delete', async ({ client, analytics, wantsJson, emitJson }, pageId, attachmentId, options) => {
const jsonMode = wantsJson();
if (!options.yes) {
if (jsonMode) {
throw new Error('Refusing to delete without confirmation in --json mode. Pass --yes to proceed.');
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
Expand All @@ -189,6 +204,12 @@ function registerAttachmentCommands(program, { withClient }) {

const result = await client.deleteAttachment(pageId, attachmentId);

if (jsonMode) {
emitJson({ id: result.id, pageId: result.pageId, deleted: true });
analytics.track('attachment_delete', true);
return;
}

console.log(chalk.green('✅ Attachment deleted successfully!'));
console.log(`ID: ${chalk.blue(result.id)}`);
console.log(`Page ID: ${chalk.blue(result.pageId)}`);
Expand Down
24 changes: 22 additions & 2 deletions bin/commands/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function registerCommentCommands(program, { withClient }) {
.option('--inline-original-selection <text>', 'Original inline selection text')
.option('--inline-marker-ref <ref>', 'Inline marker reference (optional)')
.option('--inline-properties <json>', 'Inline properties JSON (advanced)')
.action(withClient('comment_create', async ({ client, analytics }, pageId, options) => {
.action(withClient('comment_create', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
let content = '';

if (options.file) {
Expand Down Expand Up @@ -277,6 +277,16 @@ function registerCommentCommands(program, { withClient }) {
inlineProperties: location === 'inline' ? inlineProperties : null,
});

if (wantsJson()) {
emitJson({
id: result.id,
pageId: result.container?.id ?? null,
url: result._links?.webui ? client.toAbsoluteUrl(result._links.webui) : null,
});
analytics.track('comment_create', true);
return;
}

console.log(chalk.green('✅ Comment created successfully!'));
console.log(`ID: ${chalk.blue(result.id)}`);
if (result.container?.id) {
Expand Down Expand Up @@ -315,8 +325,12 @@ function registerCommentCommands(program, { withClient }) {
.command('comment-delete <commentId>')
.description('Delete a comment by ID')
.option('-y, --yes', 'Skip confirmation prompt')
.action(withClient('comment_delete', async ({ client, analytics }, commentId, options) => {
.action(withClient('comment_delete', async ({ client, analytics, wantsJson, emitJson }, commentId, options) => {
const jsonMode = wantsJson();
if (!options.yes) {
if (jsonMode) {
throw new Error('Refusing to delete without confirmation in --json mode. Pass --yes to proceed.');
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
Expand All @@ -335,6 +349,12 @@ function registerCommentCommands(program, { withClient }) {

const result = await client.deleteComment(commentId);

if (jsonMode) {
emitJson({ id: result.id, deleted: true });
analytics.track('comment_delete', true);
return;
}

console.log(chalk.green('✅ Comment deleted successfully!'));
console.log(`ID: ${chalk.blue(result.id)}`);
analytics.track('comment_delete', true);
Expand Down
12 changes: 11 additions & 1 deletion bin/commands/properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,12 @@ function registerPropertyCommands(program, { withClient }) {
.command('property-delete <pageId> <key>')
.description('Delete a content property by key')
.option('-y, --yes', 'Skip confirmation prompt')
.action(withClient('property_delete', async ({ client, analytics }, pageId, key, options) => {
.action(withClient('property_delete', async ({ client, analytics, wantsJson, emitJson }, pageId, key, options) => {
const jsonMode = wantsJson();
if (!options.yes) {
if (jsonMode) {
throw new Error('Refusing to delete without confirmation in --json mode. Pass --yes to proceed.');
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
Expand All @@ -158,6 +162,12 @@ function registerPropertyCommands(program, { withClient }) {

const result = await client.deleteProperty(pageId, key);

if (jsonMode) {
emitJson({ key: result.key, pageId: result.pageId, deleted: true });
analytics.track('property_delete', true);
return;
}

console.log(chalk.green('✅ Property deleted successfully!'));
console.log(`${chalk.green('Key:')} ${chalk.blue(result.key)}`);
console.log(`${chalk.green('Page ID:')} ${chalk.blue(result.pageId)}`);
Expand Down
50 changes: 43 additions & 7 deletions bin/commands/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ function registerVersionCommands(program, { withClient }) {
.command('version-delete <pageId> <versionNumber>')
.description('Delete a single historical version of a page (cannot delete the current version)')
.option('-y, --yes', 'Skip confirmation prompt')
.action(withClient('version_delete', async ({ client, analytics }, pageId, versionNumber, options) => {
.action(withClient('version_delete', async ({ client, analytics, wantsJson, emitJson }, pageId, versionNumber, options) => {
const jsonMode = wantsJson();
const resolvedId = String(await client.extractPageId(pageId));
const n = Number(versionNumber);

if (!options.yes) {
if (jsonMode) {
throw new Error('Refusing to delete without confirmation in --json mode. Pass --yes to proceed.');
}
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
Expand All @@ -51,6 +55,13 @@ function registerVersionCommands(program, { withClient }) {
}

const result = await client.deleteVersion(resolvedId, n);

if (jsonMode) {
emitJson({ id: result.id, versionNumber: result.versionNumber, viaExperimental: !!result.viaExperimental, deleted: true });
analytics.track('version_delete', true);
return;
}

const note = result.viaExperimental ? chalk.yellow(' (via experimental endpoint)') : '';
console.log(chalk.green(`✅ Deleted v${result.versionNumber} of page ${result.id}${note}`));
analytics.track('version_delete', true);
Expand All @@ -61,24 +72,38 @@ function registerVersionCommands(program, { withClient }) {
.description('Delete every non-current historical version of a page (keeps only current)')
.option('-y, --yes', 'Skip confirmation prompt')
.option('--throttle <seconds>', 'Sleep between version-delete calls', '0')
.action(withClient('versions_purge', async ({ client, analytics }, pageId, options) => {
.action(withClient('versions_purge', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const jsonMode = wantsJson();
const resolvedId = String(await client.extractPageId(pageId));
const versions = await client.listVersions(resolvedId);

if (versions.length === 0) {
if (jsonMode) {
emitJson({ id: resolvedId, deleted: 0, failed: 0, kept: null });
analytics.track('versions_purge', true);
return;
}
console.log(chalk.yellow(`No versions returned for page ${resolvedId}.`));
analytics.track('versions_purge', true);
return;
}
const max = Math.max(...versions.map(v => v.number));
const historicalCount = versions.filter(v => v.number !== max).length;
if (historicalCount === 0) {
if (jsonMode) {
emitJson({ id: resolvedId, deleted: 0, failed: 0, kept: max });
analytics.track('versions_purge', true);
return;
}
console.log(chalk.yellow(`Only current version v${max} exists for page ${resolvedId}; nothing to purge.`));
analytics.track('versions_purge', true);
return;
}

if (!options.yes) {
if (jsonMode) {
throw new Error('Refusing to purge versions without confirmation in --json mode. Pass --yes to proceed.');
}
const { confirmed } = await inquirer.prompt([{
type: 'confirm',
name: 'confirmed',
Expand All @@ -95,18 +120,29 @@ function registerVersionCommands(program, { withClient }) {
const throttleMs = Math.max(0, parseFloat(options.throttle || '0')) * 1000;
const result = await client.purgeNonCurrentVersions(resolvedId, {
onProgress: async (event) => {
if (event.kind === 'deleted') {
const note = event.viaExperimental ? chalk.yellow(' (experimental)') : '';
console.log(chalk.green(` ✓ deleted v${event.versionNumber}${note}`));
} else if (event.kind === 'failed') {
console.log(chalk.red(` ✗ v${event.versionNumber}: ${event.message}`));
if (!jsonMode) {
if (event.kind === 'deleted') {
const note = event.viaExperimental ? chalk.yellow(' (experimental)') : '';
console.log(chalk.green(` ✓ deleted v${event.versionNumber}${note}`));
} else if (event.kind === 'failed') {
console.log(chalk.red(` ✗ v${event.versionNumber}: ${event.message}`));
}
}
if (throttleMs > 0) {
await new Promise(r => setTimeout(r, throttleMs));
}
},
});

if (jsonMode) {
emitJson({ id: result.id, deleted: result.deleted, failed: result.failed, kept: result.kept });
analytics.track('versions_purge', result.failed === 0);
if (result.failed > 0) {
process.exitCode = 1;
}
return;
}

console.log('');
console.log(chalk.green(`✅ Purge complete for page ${result.id}: ` +
`${result.deleted} deleted, ${result.failed} failed, kept v${result.kept}.`));
Expand Down
Loading