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
42 changes: 28 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,20 @@ For **read-only** usage, select at minimum: `read:confluence-content.all`, `read

## Usage

### 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`:

```bash
confluence search "release notes" --json | jq '.results[].title'
confluence spaces --json | jq -r '.spaces[].key'
confluence info 123456789 --json | jq '.version'
```

`--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.

> **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.

### Read a Page
```bash
# Read by page ID
Expand All @@ -363,7 +377,7 @@ Use `--format storage` when you need Confluence's native storage representation,
confluence info 123456789

# Emit machine-readable metadata
confluence info 123456789 --format json
confluence info 123456789 --json
```

Example JSON shape:
Expand Down Expand Up @@ -515,10 +529,10 @@ confluence children 123456789 --show-id --show-url
confluence children 123456789 --recursive --max-depth 3

# Output as JSON for scripting
confluence children 123456789 --recursive --format json > children.json
confluence children 123456789 --recursive --json > children.json
```

`children --format json` returns structured metadata for each page, including `id`, `title`, `type`, `status`, `spaceKey`, `parentId`, `version`, and `url`. Recursive output also includes `depth`, and when available, `ancestors`.
`children --json` returns structured metadata for each page, including `id`, `title`, `type`, `status`, `spaceKey`, `parentId`, `version`, and `url`. Recursive output also includes `depth`, and when available, `ancestors`.

Example recursive JSON item:
```json
Expand Down Expand Up @@ -773,30 +787,30 @@ confluence stats
|---|---|---|
| `init` | Initialize CLI configuration | `--read-only` |
| `read <pageId_or_url>` | Read page content | `--format <html\|text\|storage\|markdown>` |
| `info <pageId_or_url>` | Get page information | `--format <text\|json>` |
| `search <query>` | Search for pages | `--limit <number>`, `--start <number>` |
| `spaces` | List available spaces | `--limit <number>`, `--all` |
| `find <title>` | Find a page by its title | `--space <spaceKey>` |
| `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree\|json>`, `--show-url`, `--show-id` |
| `info <pageId_or_url>` | Get page information | `--json` |
| `search <query>` | Search for pages | `--json`, `--limit <number>`, `--start <number>` |
| `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` |
| `versions <pageId_or_url>` | List historical versions of a page | `--format <text\|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>` |
| `edit <pageId>` | Export page content for editing | `--output <file>` |
| `attachments <pageId_or_url>` | List or download attachments for a page | `--limit <number>`, `--pattern <glob>`, `--download`, `--dest <directory>` |
| `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` |
| `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` |
| `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` |
| `property-list <pageId_or_url>` | List all content properties for a page | `--format <text\|json>`, `--limit <number>`, `--start <number>`, `--all` |
| `property-get <pageId_or_url> <key>` | Get a content property by key | `--format <text\|json>` |
| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--format <text\|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` |
| `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 | |
Expand Down
19 changes: 10 additions & 9 deletions bin/commands/attachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ function registerAttachmentCommands(program, { withClient }) {
.option('-p, --pattern <glob>', 'Filter attachments by filename (e.g., "*.png")')
.option('-d, --download', 'Download matching attachments')
.option('--dest <directory>', 'Directory to save downloads (default: current directory)', '.')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(withClient('attachments', async ({ client, analytics }, pageId, options) => {
.option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text')
.action(withClient('attachments', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const maxResults = options.limit ? parseInt(options.limit, 10) : null;
const pattern = options.pattern ? options.pattern.trim() : null;

Expand All @@ -25,21 +25,22 @@ function registerAttachmentCommands(program, { withClient }) {
if (!['text', 'json'].includes(format)) {
throw new Error('Format must be one of: text, json');
}
const jsonMode = wantsJson(options);

const attachments = await client.getAllAttachments(pageId, { maxResults });
const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments;

if (filtered.length === 0) {
if (format === 'json') {
console.log(JSON.stringify({ attachmentCount: 0, attachments: [] }, null, 2));
if (jsonMode) {
emitJson({ attachmentCount: 0, attachments: [] });
} else {
console.log(chalk.yellow('No attachments found.'));
}
analytics.track('attachments', true);
return;
}

if (format === 'json' && !options.download) {
if (jsonMode && !options.download) {
const output = {
attachmentCount: filtered.length,
attachments: filtered.map(att => ({
Expand All @@ -52,7 +53,7 @@ function registerAttachmentCommands(program, { withClient }) {
downloadLink: att.downloadLink,
})),
};
console.log(JSON.stringify(output, null, 2));
emitJson(output);
} else if (!options.download) {
console.log(chalk.blue(`Found ${filtered.length} attachment${filtered.length === 1 ? '' : 's'}:`));
filtered.forEach((att, index) => {
Expand Down Expand Up @@ -95,19 +96,19 @@ function registerAttachmentCommands(program, { withClient }) {
const dataStream = await client.downloadAttachment(pageId, attachment);
await writeStream(dataStream, targetPath);
downloadResults.push({ title: attachment.title, id: attachment.id, savedTo: targetPath });
if (format !== 'json') {
if (!jsonMode) {
console.log(`⬇️ ${chalk.green(attachment.title)} -> ${chalk.gray(targetPath)}`);
}
}

if (format === 'json') {
if (jsonMode) {
const output = {
attachmentCount: filtered.length,
downloaded: downloadResults.length,
destination: destDir,
attachments: downloadResults,
};
console.log(JSON.stringify(output, null, 2));
emitJson(output);
} else {
console.log(chalk.green(`Downloaded ${downloadResults.length} attachment${downloadResults.length === 1 ? '' : 's'} to ${destDir}`));
}
Expand Down
20 changes: 15 additions & 5 deletions bin/commands/comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,18 @@ function registerCommentCommands(program, { withClient }) {
program
.command('comments <pageId>')
.description('List comments for a page by ID or URL')
.option('-f, --format <format>', 'Output format (text, markdown, json)', 'text')
.option('-f, --format <format>', 'Output format (text, markdown). "json" is deprecated — use --json', 'text')
.option('-l, --limit <limit>', 'Maximum number of comments to fetch (default: 25)')
.option('--start <start>', 'Start index for results (default: 0)', '0')
.option('--location <location>', 'Filter by location (inline, footer, resolved). Comma-separated')
.option('--depth <depth>', 'Comment depth ("" for root only, "all")')
.option('--all', 'Fetch all comments (ignores pagination)')
.action(withClient('comments', async ({ client, analytics }, pageId, options) => {
.action(withClient('comments', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const format = (options.format || 'text').toLowerCase();
if (!['text', 'markdown', 'json'].includes(format)) {
throw new Error('Format must be one of: text, markdown, json');
}
const jsonMode = wantsJson(options);

const limit = options.limit ? parseInt(options.limit, 10) : null;
if (options.limit && (Number.isNaN(limit) || limit <= 0)) {
Expand Down Expand Up @@ -102,12 +103,21 @@ function registerCommentCommands(program, { withClient }) {
}

if (comments.length === 0) {
console.log(chalk.yellow('No comments found.'));
if (jsonMode) {
const resolvedPageId = await client.extractPageId(pageId);
const output = { pageId: resolvedPageId, commentCount: 0, comments: [] };
if (!options.all) {
output.nextStart = nextStart;
}
emitJson(output);
} else {
console.log(chalk.yellow('No comments found.'));
}
analytics.track('comments', true);
return;
}

if (format === 'json') {
if (jsonMode) {
const resolvedPageId = await client.extractPageId(pageId);
const output = {
pageId: resolvedPageId,
Expand All @@ -121,7 +131,7 @@ function registerCommentCommands(program, { withClient }) {
if (!options.all) {
output.nextStart = nextStart;
}
console.log(JSON.stringify(output, null, 2));
emitJson(output);
analytics.track('comments', true);
return;
}
Expand Down
24 changes: 12 additions & 12 deletions bin/commands/properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ function registerPropertyCommands(program, { withClient }) {
program
.command('property-list <pageId>')
.description('List all content properties for a page')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text')
.option('-l, --limit <limit>', 'Maximum number of properties to fetch (default: 25)')
.option('--start <start>', 'Start index for results (default: 0)', '0')
.option('--all', 'Fetch all properties (ignores pagination)')
.action(withClient('property_list', async ({ client, analytics }, pageId, options) => {
.action(withClient('property_list', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const format = (options.format || 'text').toLowerCase();
if (!['text', 'json'].includes(format)) {
throw new Error('Format must be one of: text, json');
Expand Down Expand Up @@ -43,12 +43,12 @@ function registerPropertyCommands(program, { withClient }) {
nextStart = response.nextStart;
}

if (format === 'json') {
if (wantsJson(options)) {
const output = { properties };
if (!options.all) {
output.nextStart = nextStart;
}
console.log(JSON.stringify(output, null, 2));
emitJson(output);
} else if (properties.length === 0) {
console.log(chalk.yellow('No properties found.'));
} else {
Expand All @@ -68,17 +68,17 @@ function registerPropertyCommands(program, { withClient }) {
program
.command('property-get <pageId> <key>')
.description('Get a content property by key')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(withClient('property_get', async ({ client, analytics }, pageId, key, options) => {
.option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text')
.action(withClient('property_get', async ({ client, analytics, wantsJson, emitJson }, pageId, key, options) => {
const format = (options.format || 'text').toLowerCase();
if (!['text', 'json'].includes(format)) {
throw new Error('Format must be one of: text, json');
}

const property = await client.getProperty(pageId, key);

if (format === 'json') {
console.log(JSON.stringify(property, null, 2));
if (wantsJson(options)) {
emitJson(property);
} else {
console.log(`${chalk.green('Key:')} ${property.key}`);
console.log(`${chalk.green('Version:')} ${property.version.number}`);
Expand All @@ -93,8 +93,8 @@ function registerPropertyCommands(program, { withClient }) {
.description('Set a content property (create or update)')
.option('-v, --value <json>', 'Property value as JSON')
.option('--file <file>', 'Read property value from a JSON file')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.action(withClient('property_set', async ({ client, analytics }, pageId, key, options) => {
.option('-f, --format <format>', 'Output format (text). "json" is deprecated — use --json', 'text')
.action(withClient('property_set', async ({ client, analytics, wantsJson, emitJson }, pageId, key, options) => {
if (!options.value && !options.file) {
throw new Error('Provide a value with --value or --file.');
}
Expand Down Expand Up @@ -122,8 +122,8 @@ function registerPropertyCommands(program, { withClient }) {

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

if (format === 'json') {
console.log(JSON.stringify(result, null, 2));
if (wantsJson(options)) {
emitJson(result);
} else {
console.log(chalk.green('✅ Property set successfully!'));
console.log(`${chalk.green('Key:')} ${result.key}`);
Expand Down
8 changes: 4 additions & 4 deletions bin/commands/versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ function registerVersionCommands(program, { withClient }) {
program
.command('versions <pageId>')
.description('List historical versions of a Confluence page')
.option('--format <format>', 'Output format: text or json (default: text)', 'text')
.action(withClient('versions', async ({ client, analytics }, pageId, options) => {
.option('--format <format>', 'Output format: text (default). "json" is deprecated — use --json', 'text')
.action(withClient('versions', async ({ client, analytics, wantsJson, emitJson }, pageId, options) => {
const resolvedId = String(await client.extractPageId(pageId));
const versions = await client.listVersions(resolvedId);

if (options.format === 'json') {
console.log(JSON.stringify({ pageId: resolvedId, versions }, null, 2));
if (wantsJson(options)) {
emitJson({ pageId: resolvedId, versions });
} else {
const max = versions.length ? Math.max(...versions.map(v => v.number)) : 0;
console.log(chalk.blue(`Versions for page ${resolvedId} (${versions.length} total):`));
Expand Down
Loading