confluence-cli 1.7.0 → 1.9.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 +19 -0
- package/README.md +16 -2
- package/bin/confluence.js +84 -0
- package/lib/config.js +71 -1
- package/lib/confluence-client.js +144 -1
- package/package.json +7 -1
- package/tests/confluence-client.test.js +43 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [1.9.0](https://github.com/pchuri/confluence-cli/compare/v1.8.0...v1.9.0) (2025-12-04)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add attachments list and download command ([#17](https://github.com/pchuri/confluence-cli/issues/17)) ([fb3d4f8](https://github.com/pchuri/confluence-cli/commit/fb3d4f81a3926fec832a39c78f4eda3b4a22130a))
|
|
7
|
+
|
|
8
|
+
# [1.8.0](https://github.com/pchuri/confluence-cli/compare/v1.7.0...v1.8.0) (2025-09-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* make Confluence API path configurable ([#14](https://github.com/pchuri/confluence-cli/issues/14)) ([be000e0](https://github.com/pchuri/confluence-cli/commit/be000e0d92881d65329b84bad6555dcad0bbb455)), closes [#13](https://github.com/pchuri/confluence-cli/issues/13)
|
|
14
|
+
|
|
15
|
+
## [Unreleased]
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Make the Confluence REST base path configurable to support both `/rest/api` and `/wiki/rest/api`.
|
|
19
|
+
|
|
1
20
|
# [1.7.0](https://github.com/pchuri/confluence-cli/compare/v1.6.0...v1.7.0) (2025-09-28)
|
|
2
21
|
|
|
3
22
|
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
10
10
|
- 🏠 **List spaces** - View all available Confluence spaces
|
|
11
11
|
- ✏️ **Create pages** - Create new pages with support for Markdown, HTML, or Storage format
|
|
12
12
|
- 📝 **Update pages** - Update existing page content and titles
|
|
13
|
+
- 📎 **Attachments** - List or download page attachments
|
|
13
14
|
- 🛠️ **Edit workflow** - Export page content for editing and re-import
|
|
14
15
|
- 🔧 **Easy setup** - Simple configuration with environment variables or interactive setup
|
|
15
16
|
|
|
@@ -58,18 +59,19 @@ npx confluence-cli
|
|
|
58
59
|
confluence init
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
The wizard now
|
|
62
|
+
The wizard now helps you choose the right API endpoint and authentication method. It recommends `/wiki/rest/api` for Atlassian Cloud domains (e.g., `*.atlassian.net`) and `/rest/api` for self-hosted/Data Center instances, then prompts for Basic (email + token) or Bearer authentication.
|
|
62
63
|
|
|
63
64
|
### Option 2: Environment Variables
|
|
64
65
|
```bash
|
|
65
66
|
export CONFLUENCE_DOMAIN="your-domain.atlassian.net"
|
|
66
67
|
export CONFLUENCE_API_TOKEN="your-api-token"
|
|
67
68
|
export CONFLUENCE_EMAIL="your.email@example.com" # required when using Atlassian Cloud
|
|
69
|
+
export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/api for Server/DC
|
|
68
70
|
# Optional: set to 'bearer' for self-hosted/Data Center instances
|
|
69
71
|
export CONFLUENCE_AUTH_TYPE="basic"
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
`
|
|
74
|
+
`CONFLUENCE_API_PATH` defaults to `/wiki/rest/api` for Atlassian Cloud domains and `/rest/api` otherwise. Override it when your site lives under a custom reverse proxy or on-premises path. `CONFLUENCE_AUTH_TYPE` defaults to `basic` when an email is present and falls back to `bearer` otherwise.
|
|
73
75
|
|
|
74
76
|
### Getting Your API Token
|
|
75
77
|
|
|
@@ -106,6 +108,18 @@ confluence search "search term"
|
|
|
106
108
|
confluence search "search term" --limit 5
|
|
107
109
|
```
|
|
108
110
|
|
|
111
|
+
### List or Download Attachments
|
|
112
|
+
```bash
|
|
113
|
+
# List all attachments on a page
|
|
114
|
+
confluence attachments 123456789
|
|
115
|
+
|
|
116
|
+
# Filter by filename and limit the number returned
|
|
117
|
+
confluence attachments 123456789 --pattern "*.png" --limit 5
|
|
118
|
+
|
|
119
|
+
# Download matching attachments to a directory
|
|
120
|
+
confluence attachments 123456789 --pattern "*.png" --download --dest ./downloads
|
|
121
|
+
```
|
|
122
|
+
|
|
109
123
|
### List Spaces
|
|
110
124
|
```bash
|
|
111
125
|
confluence spaces
|
package/bin/confluence.js
CHANGED
|
@@ -337,6 +337,90 @@ program
|
|
|
337
337
|
}
|
|
338
338
|
});
|
|
339
339
|
|
|
340
|
+
// Attachments command
|
|
341
|
+
program
|
|
342
|
+
.command('attachments <pageId>')
|
|
343
|
+
.description('List or download attachments for a page')
|
|
344
|
+
.option('-l, --limit <limit>', 'Maximum number of attachments to fetch (default: all)')
|
|
345
|
+
.option('-p, --pattern <glob>', 'Filter attachments by filename (e.g., "*.png")')
|
|
346
|
+
.option('-d, --download', 'Download matching attachments')
|
|
347
|
+
.option('--dest <directory>', 'Directory to save downloads (default: current directory)', '.')
|
|
348
|
+
.action(async (pageId, options) => {
|
|
349
|
+
const analytics = new Analytics();
|
|
350
|
+
try {
|
|
351
|
+
const config = getConfig();
|
|
352
|
+
const client = new ConfluenceClient(config);
|
|
353
|
+
const maxResults = options.limit ? parseInt(options.limit, 10) : null;
|
|
354
|
+
const pattern = options.pattern ? options.pattern.trim() : null;
|
|
355
|
+
|
|
356
|
+
if (options.limit && (Number.isNaN(maxResults) || maxResults <= 0)) {
|
|
357
|
+
throw new Error('Limit must be a positive number.');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const attachments = await client.getAllAttachments(pageId, { maxResults });
|
|
361
|
+
const filtered = pattern ? attachments.filter(att => client.matchesPattern(att.title, pattern)) : attachments;
|
|
362
|
+
|
|
363
|
+
if (filtered.length === 0) {
|
|
364
|
+
console.log(chalk.yellow('No attachments found.'));
|
|
365
|
+
analytics.track('attachments', true);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(chalk.blue(`Found ${filtered.length} attachment${filtered.length === 1 ? '' : 's'}:`));
|
|
370
|
+
filtered.forEach((att, index) => {
|
|
371
|
+
const sizeKb = att.fileSize ? `${Math.max(1, Math.round(att.fileSize / 1024))} KB` : 'unknown size';
|
|
372
|
+
const typeLabel = att.mediaType || 'unknown';
|
|
373
|
+
console.log(`${index + 1}. ${chalk.green(att.title)} (ID: ${att.id})`);
|
|
374
|
+
console.log(` Type: ${chalk.gray(typeLabel)} • Size: ${chalk.gray(sizeKb)} • Version: ${chalk.gray(att.version)}`);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (options.download) {
|
|
378
|
+
const fs = require('fs');
|
|
379
|
+
const path = require('path');
|
|
380
|
+
const destDir = path.resolve(options.dest || '.');
|
|
381
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
382
|
+
|
|
383
|
+
const uniquePathFor = (dir, filename) => {
|
|
384
|
+
const parsed = path.parse(filename);
|
|
385
|
+
let attempt = path.join(dir, filename);
|
|
386
|
+
let counter = 1;
|
|
387
|
+
while (fs.existsSync(attempt)) {
|
|
388
|
+
const suffix = ` (${counter})`;
|
|
389
|
+
const nextName = `${parsed.name}${suffix}${parsed.ext}`;
|
|
390
|
+
attempt = path.join(dir, nextName);
|
|
391
|
+
counter += 1;
|
|
392
|
+
}
|
|
393
|
+
return attempt;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const writeStream = (stream, targetPath) => new Promise((resolve, reject) => {
|
|
397
|
+
const writer = fs.createWriteStream(targetPath);
|
|
398
|
+
stream.pipe(writer);
|
|
399
|
+
stream.on('error', reject);
|
|
400
|
+
writer.on('error', reject);
|
|
401
|
+
writer.on('finish', resolve);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
let downloaded = 0;
|
|
405
|
+
for (const attachment of filtered) {
|
|
406
|
+
const targetPath = uniquePathFor(destDir, attachment.title);
|
|
407
|
+
const dataStream = await client.downloadAttachment(pageId, attachment.id);
|
|
408
|
+
await writeStream(dataStream, targetPath);
|
|
409
|
+
downloaded += 1;
|
|
410
|
+
console.log(`⬇️ ${chalk.green(attachment.title)} -> ${chalk.gray(targetPath)}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log(chalk.green(`Downloaded ${downloaded} attachment${downloaded === 1 ? '' : 's'} to ${destDir}`));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
analytics.track('attachments', true);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
analytics.track('attachments', false);
|
|
419
|
+
console.error(chalk.red('Error:'), error.message);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
340
424
|
// Copy page tree command
|
|
341
425
|
program
|
|
342
426
|
.command('copy-tree <sourcePageId> <targetParentId> [newTitle]')
|
package/lib/config.js
CHANGED
|
@@ -27,6 +27,34 @@ const normalizeAuthType = (rawValue, hasEmail) => {
|
|
|
27
27
|
return hasEmail ? 'basic' : 'bearer';
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
const inferApiPath = (domain) => {
|
|
31
|
+
if (!domain) {
|
|
32
|
+
return '/rest/api';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const normalizedDomain = domain.trim().toLowerCase();
|
|
36
|
+
if (normalizedDomain.endsWith('.atlassian.net')) {
|
|
37
|
+
return '/wiki/rest/api';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return '/rest/api';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const normalizeApiPath = (rawValue, domain) => {
|
|
44
|
+
const trimmed = (rawValue || '').trim();
|
|
45
|
+
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return inferApiPath(domain);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!trimmed.startsWith('/')) {
|
|
51
|
+
throw new Error('Confluence API path must start with "/".');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const withoutTrailing = trimmed.replace(/\/+$/, '');
|
|
55
|
+
return withoutTrailing || inferApiPath(domain);
|
|
56
|
+
};
|
|
57
|
+
|
|
30
58
|
async function initConfig() {
|
|
31
59
|
console.log(chalk.blue('🚀 Confluence CLI Configuration'));
|
|
32
60
|
console.log('Please provide your Confluence connection details:\n');
|
|
@@ -38,6 +66,27 @@ async function initConfig() {
|
|
|
38
66
|
message: 'Confluence domain (e.g., yourcompany.atlassian.net):',
|
|
39
67
|
validate: requiredInput('Domain')
|
|
40
68
|
},
|
|
69
|
+
{
|
|
70
|
+
type: 'input',
|
|
71
|
+
name: 'apiPath',
|
|
72
|
+
message: 'REST API path (Cloud: /wiki/rest/api, Server: /rest/api):',
|
|
73
|
+
default: (responses) => inferApiPath(responses.domain),
|
|
74
|
+
validate: (input, responses) => {
|
|
75
|
+
const value = (input || '').trim();
|
|
76
|
+
if (!value) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (!value.startsWith('/')) {
|
|
80
|
+
return 'API path must start with "/"';
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
normalizeApiPath(value, responses.domain);
|
|
84
|
+
return true;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return error.message;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
41
90
|
{
|
|
42
91
|
type: 'list',
|
|
43
92
|
name: 'authType',
|
|
@@ -66,6 +115,7 @@ async function initConfig() {
|
|
|
66
115
|
|
|
67
116
|
const config = {
|
|
68
117
|
domain: answers.domain.trim(),
|
|
118
|
+
apiPath: normalizeApiPath(answers.apiPath, answers.domain),
|
|
69
119
|
token: answers.token.trim(),
|
|
70
120
|
authType: answers.authType,
|
|
71
121
|
email: answers.authType === 'basic' ? answers.email.trim() : undefined
|
|
@@ -83,9 +133,18 @@ function getConfig() {
|
|
|
83
133
|
const envToken = process.env.CONFLUENCE_API_TOKEN;
|
|
84
134
|
const envEmail = process.env.CONFLUENCE_EMAIL;
|
|
85
135
|
const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
|
|
136
|
+
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
86
137
|
|
|
87
138
|
if (envDomain && envToken) {
|
|
88
139
|
const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
|
|
140
|
+
let apiPath;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
apiPath = normalizeApiPath(envApiPath, envDomain);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(chalk.red(`❌ ${error.message}`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
89
148
|
|
|
90
149
|
if (authType === 'basic' && !envEmail) {
|
|
91
150
|
console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL.'));
|
|
@@ -95,6 +154,7 @@ function getConfig() {
|
|
|
95
154
|
|
|
96
155
|
return {
|
|
97
156
|
domain: envDomain.trim(),
|
|
157
|
+
apiPath,
|
|
98
158
|
token: envToken.trim(),
|
|
99
159
|
email: envEmail ? envEmail.trim() : undefined,
|
|
100
160
|
authType
|
|
@@ -104,7 +164,7 @@ function getConfig() {
|
|
|
104
164
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
105
165
|
console.error(chalk.red('❌ No configuration found!'));
|
|
106
166
|
console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
|
|
107
|
-
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, and
|
|
167
|
+
console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, CONFLUENCE_EMAIL, and optionally CONFLUENCE_API_PATH.'));
|
|
108
168
|
process.exit(1);
|
|
109
169
|
}
|
|
110
170
|
|
|
@@ -114,6 +174,7 @@ function getConfig() {
|
|
|
114
174
|
const trimmedToken = (storedConfig.token || '').trim();
|
|
115
175
|
const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
|
|
116
176
|
const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
|
|
177
|
+
let apiPath;
|
|
117
178
|
|
|
118
179
|
if (!trimmedDomain || !trimmedToken) {
|
|
119
180
|
console.error(chalk.red('❌ Configuration file is missing required values.'));
|
|
@@ -127,8 +188,17 @@ function getConfig() {
|
|
|
127
188
|
process.exit(1);
|
|
128
189
|
}
|
|
129
190
|
|
|
191
|
+
try {
|
|
192
|
+
apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error(chalk.red(`❌ ${error.message}`));
|
|
195
|
+
console.log(chalk.yellow('Please rerun "confluence init" to update your API path.'));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
return {
|
|
131
200
|
domain: trimmedDomain,
|
|
201
|
+
apiPath,
|
|
132
202
|
token: trimmedToken,
|
|
133
203
|
email: trimmedEmail,
|
|
134
204
|
authType
|
package/lib/confluence-client.js
CHANGED
|
@@ -8,7 +8,8 @@ class ConfluenceClient {
|
|
|
8
8
|
this.token = config.token;
|
|
9
9
|
this.email = config.email;
|
|
10
10
|
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
|
|
11
|
-
this.
|
|
11
|
+
this.apiPath = this.sanitizeApiPath(config.apiPath);
|
|
12
|
+
this.baseURL = `https://${this.domain}${this.apiPath}`;
|
|
12
13
|
this.markdown = new MarkdownIt();
|
|
13
14
|
this.setupConfluenceMarkdownExtensions();
|
|
14
15
|
|
|
@@ -23,6 +24,19 @@ class ConfluenceClient {
|
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
sanitizeApiPath(rawPath) {
|
|
28
|
+
const fallback = '/rest/api';
|
|
29
|
+
const value = (rawPath || '').trim();
|
|
30
|
+
|
|
31
|
+
if (!value) {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const withoutLeading = value.replace(/^\/+/, '');
|
|
36
|
+
const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
|
|
37
|
+
return normalized || fallback;
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
buildBasicAuthHeader() {
|
|
27
41
|
if (!this.email) {
|
|
28
42
|
throw new Error('Basic authentication requires an email address.');
|
|
@@ -153,6 +167,76 @@ class ConfluenceClient {
|
|
|
153
167
|
}));
|
|
154
168
|
}
|
|
155
169
|
|
|
170
|
+
/**
|
|
171
|
+
* List attachments for a page with pagination support
|
|
172
|
+
*/
|
|
173
|
+
async listAttachments(pageIdOrUrl, options = {}) {
|
|
174
|
+
const pageId = this.extractPageId(pageIdOrUrl);
|
|
175
|
+
const limit = this.parsePositiveInt(options.limit, 50);
|
|
176
|
+
const start = this.parsePositiveInt(options.start, 0);
|
|
177
|
+
const params = {
|
|
178
|
+
limit,
|
|
179
|
+
start
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (options.filename) {
|
|
183
|
+
params.filename = options.filename;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const response = await this.client.get(`/content/${pageId}/child/attachment`, { params });
|
|
187
|
+
const results = Array.isArray(response.data.results)
|
|
188
|
+
? response.data.results.map((item) => this.normalizeAttachment(item))
|
|
189
|
+
: [];
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
results,
|
|
193
|
+
nextStart: this.parseNextStart(response.data?._links?.next)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Fetch all attachments for a page, honoring an optional maxResults cap
|
|
199
|
+
*/
|
|
200
|
+
async getAllAttachments(pageIdOrUrl, options = {}) {
|
|
201
|
+
const pageSize = this.parsePositiveInt(options.pageSize || options.limit, 50);
|
|
202
|
+
const maxResults = this.parsePositiveInt(options.maxResults, null);
|
|
203
|
+
const filename = options.filename;
|
|
204
|
+
let start = this.parsePositiveInt(options.start, 0);
|
|
205
|
+
const attachments = [];
|
|
206
|
+
|
|
207
|
+
let hasNext = true;
|
|
208
|
+
while (hasNext) {
|
|
209
|
+
const page = await this.listAttachments(pageIdOrUrl, {
|
|
210
|
+
limit: pageSize,
|
|
211
|
+
start,
|
|
212
|
+
filename
|
|
213
|
+
});
|
|
214
|
+
attachments.push(...page.results);
|
|
215
|
+
|
|
216
|
+
if (maxResults && attachments.length >= maxResults) {
|
|
217
|
+
return attachments.slice(0, maxResults);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
hasNext = page.nextStart !== null && page.nextStart !== undefined;
|
|
221
|
+
if (hasNext) {
|
|
222
|
+
start = page.nextStart;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return attachments;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Download an attachment's data stream
|
|
231
|
+
*/
|
|
232
|
+
async downloadAttachment(pageIdOrUrl, attachmentId, options = {}) {
|
|
233
|
+
const pageId = this.extractPageId(pageIdOrUrl);
|
|
234
|
+
const response = await this.client.get(`/content/${pageId}/child/attachment/${attachmentId}/data`, {
|
|
235
|
+
responseType: options.responseType || 'stream'
|
|
236
|
+
});
|
|
237
|
+
return response.data;
|
|
238
|
+
}
|
|
239
|
+
|
|
156
240
|
/**
|
|
157
241
|
* Convert markdown to Confluence storage format
|
|
158
242
|
*/
|
|
@@ -901,6 +985,65 @@ class ConfluenceClient {
|
|
|
901
985
|
return this.globToRegExp(pattern).test(title);
|
|
902
986
|
});
|
|
903
987
|
}
|
|
988
|
+
|
|
989
|
+
matchesPattern(value, patterns) {
|
|
990
|
+
if (!patterns) {
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const list = Array.isArray(patterns) ? patterns.filter(Boolean) : [patterns];
|
|
995
|
+
if (list.length === 0) {
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return list.some((pattern) => this.globToRegExp(pattern).test(value));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
normalizeAttachment(raw) {
|
|
1003
|
+
return {
|
|
1004
|
+
id: raw.id,
|
|
1005
|
+
title: raw.title,
|
|
1006
|
+
mediaType: raw.metadata?.mediaType || raw.type || '',
|
|
1007
|
+
fileSize: raw.extensions?.fileSize || 0,
|
|
1008
|
+
version: raw.version?.number || 1,
|
|
1009
|
+
downloadLink: this.toAbsoluteUrl(raw._links?.download)
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
toAbsoluteUrl(pathOrUrl) {
|
|
1014
|
+
if (!pathOrUrl) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (pathOrUrl.startsWith('http://') || pathOrUrl.startsWith('https://')) {
|
|
1019
|
+
return pathOrUrl;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const normalized = pathOrUrl.startsWith('/') ? pathOrUrl : `/${pathOrUrl}`;
|
|
1023
|
+
return `https://${this.domain}${normalized}`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
parseNextStart(nextLink) {
|
|
1027
|
+
if (!nextLink) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const match = nextLink.match(/[?&]start=(\d+)/);
|
|
1032
|
+
if (!match) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const value = parseInt(match[1], 10);
|
|
1037
|
+
return Number.isNaN(value) ? null : value;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
parsePositiveInt(value, fallback) {
|
|
1041
|
+
const parsed = parseInt(value, 10);
|
|
1042
|
+
if (Number.isNaN(parsed) || parsed < 0) {
|
|
1043
|
+
return fallback;
|
|
1044
|
+
}
|
|
1045
|
+
return parsed;
|
|
1046
|
+
}
|
|
904
1047
|
}
|
|
905
1048
|
|
|
906
1049
|
module.exports = ConfluenceClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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": {
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"eslint": "^8.55.0",
|
|
36
36
|
"jest": "^29.7.0"
|
|
37
37
|
},
|
|
38
|
+
"overrides": {
|
|
39
|
+
"js-yaml": "^4.1.1",
|
|
40
|
+
"@istanbuljs/load-nyc-config": {
|
|
41
|
+
"js-yaml": "^3.14.2"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
38
44
|
"engines": {
|
|
39
45
|
"node": ">=14.0.0"
|
|
40
46
|
},
|
|
@@ -10,6 +10,27 @@ describe('ConfluenceClient', () => {
|
|
|
10
10
|
});
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
describe('api path handling', () => {
|
|
14
|
+
test('defaults to /rest/api when path is not provided', () => {
|
|
15
|
+
const defaultClient = new ConfluenceClient({
|
|
16
|
+
domain: 'example.com',
|
|
17
|
+
token: 'no-path-token'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(defaultClient.baseURL).toBe('https://example.com/rest/api');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('normalizes custom api paths', () => {
|
|
24
|
+
const customClient = new ConfluenceClient({
|
|
25
|
+
domain: 'cloud.example',
|
|
26
|
+
token: 'custom-path',
|
|
27
|
+
apiPath: 'wiki/rest/api/'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(customClient.baseURL).toBe('https://cloud.example/wiki/rest/api');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
13
34
|
describe('authentication setup', () => {
|
|
14
35
|
test('uses bearer token headers by default', () => {
|
|
15
36
|
const bearerClient = new ConfluenceClient({
|
|
@@ -253,4 +274,26 @@ describe('ConfluenceClient', () => {
|
|
|
253
274
|
expect(client.shouldExcludePage('production', patterns)).toBe(false);
|
|
254
275
|
});
|
|
255
276
|
});
|
|
277
|
+
|
|
278
|
+
describe('attachments', () => {
|
|
279
|
+
test('should have required methods for attachment handling', () => {
|
|
280
|
+
expect(typeof client.listAttachments).toBe('function');
|
|
281
|
+
expect(typeof client.getAllAttachments).toBe('function');
|
|
282
|
+
expect(typeof client.downloadAttachment).toBe('function');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('matchesPattern should respect glob patterns', () => {
|
|
286
|
+
expect(client.matchesPattern('report.png', '*.png')).toBe(true);
|
|
287
|
+
expect(client.matchesPattern('report.png', '*.jpg')).toBe(false);
|
|
288
|
+
expect(client.matchesPattern('report.png', ['*.jpg', 'report.*'])).toBe(true);
|
|
289
|
+
expect(client.matchesPattern('report.png', null)).toBe(true);
|
|
290
|
+
expect(client.matchesPattern('report.png', [])).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('parseNextStart should read start query param when present', () => {
|
|
294
|
+
expect(client.parseNextStart('/rest/api/content/1/child/attachment?start=25')).toBe(25);
|
|
295
|
+
expect(client.parseNextStart('/rest/api/content/1/child/attachment?limit=50')).toBeNull();
|
|
296
|
+
expect(client.parseNextStart(null)).toBeNull();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
256
299
|
});
|