@bestend/confluence-cli 1.16.1 → 2.0.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/.claude/skills/confluence/SKILL.md +722 -0
- package/README.md +251 -21
- package/bin/confluence.js +848 -128
- package/bin/index.js +6 -1
- package/lib/config.js +242 -40
- package/lib/confluence-client.js +299 -61
- package/package.json +10 -4
- package/.eslintrc.js +0 -23
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -34
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -26
- package/.github/ISSUE_TEMPLATE/feedback.md +0 -37
- package/.github/pull_request_template.md +0 -31
- package/.github/workflows/ci.yml +0 -28
- package/.github/workflows/publish.yml +0 -38
- package/.releaserc +0 -17
- package/AGENTS.md +0 -105
- package/CHANGELOG.md +0 -232
- package/CONTRIBUTING.md +0 -246
- package/docs/PROMOTION.md +0 -63
- package/eslint.config.js +0 -33
- package/examples/copy-tree-example.sh +0 -117
- package/examples/create-child-page-example.sh +0 -67
- package/examples/demo-page-management.sh +0 -68
- package/examples/demo.sh +0 -43
- package/examples/sample-page.md +0 -30
- package/jest.config.js +0 -13
- package/llms.txt +0 -46
- package/tests/confluence-client.test.js +0 -458
package/lib/confluence-client.js
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const FormData = require('form-data');
|
|
2
5
|
const { convert } = require('html-to-text');
|
|
3
6
|
const MarkdownIt = require('markdown-it');
|
|
4
7
|
const { XMLValidator } = require('fast-xml-parser');
|
|
5
8
|
|
|
9
|
+
const NAMED_ENTITIES = {
|
|
10
|
+
// uppercase variants
|
|
11
|
+
aring: 'å', auml: 'ä', ouml: 'ö',
|
|
12
|
+
eacute: 'é', egrave: 'è', ecirc: 'ê', euml: 'ë',
|
|
13
|
+
aacute: 'á', agrave: 'à', acirc: 'â', atilde: 'ã',
|
|
14
|
+
oacute: 'ó', ograve: 'ò', ocirc: 'ô', otilde: 'õ',
|
|
15
|
+
uacute: 'ú', ugrave: 'ù', ucirc: 'û', uuml: 'ü',
|
|
16
|
+
iacute: 'í', igrave: 'ì', icirc: 'î', iuml: 'ï',
|
|
17
|
+
ntilde: 'ñ', ccedil: 'ç', szlig: 'ß', yuml: 'ÿ',
|
|
18
|
+
eth: 'ð', thorn: 'þ',
|
|
19
|
+
// uppercase variants
|
|
20
|
+
Aring: 'Å', Auml: 'Ä', Ouml: 'Ö',
|
|
21
|
+
Eacute: 'É', Egrave: 'È', Ecirc: 'Ê', Euml: 'Ë',
|
|
22
|
+
Aacute: 'Á', Agrave: 'À', Acirc: 'Â', Atilde: 'Ã',
|
|
23
|
+
Oacute: 'Ó', Ograve: 'Ò', Ocirc: 'Ô', Otilde: 'Õ',
|
|
24
|
+
Uacute: 'Ú', Ugrave: 'Ù', Ucirc: 'Û', Uuml: 'Ü',
|
|
25
|
+
Iacute: 'Í', Igrave: 'Ì', Icirc: 'Î', Iuml: 'Ï',
|
|
26
|
+
Ntilde: 'Ñ', Ccedil: 'Ç', Szlig: 'SS', Yuml: 'Ÿ',
|
|
27
|
+
Eth: 'Ð', Thorn: 'Þ'
|
|
28
|
+
};
|
|
29
|
+
|
|
6
30
|
class ConfluenceClient {
|
|
7
31
|
constructor(config) {
|
|
8
32
|
this.domain = config.domain;
|
|
33
|
+
const rawProtocol = (config.protocol || 'https').trim().toLowerCase();
|
|
34
|
+
this.protocol = (rawProtocol === 'http' || rawProtocol === 'https') ? rawProtocol : 'https';
|
|
9
35
|
this.token = config.token;
|
|
10
36
|
this.email = config.email;
|
|
11
37
|
this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
|
|
12
38
|
this.apiPath = this.sanitizeApiPath(config.apiPath);
|
|
13
|
-
this.baseURL =
|
|
39
|
+
this.baseURL = `${this.protocol}://${this.domain}${this.apiPath}`;
|
|
14
40
|
this.markdown = new MarkdownIt();
|
|
15
41
|
this.setupConfluenceMarkdownExtensions();
|
|
16
42
|
|
|
@@ -34,14 +60,17 @@ class ConfluenceClient {
|
|
|
34
60
|
});
|
|
35
61
|
}
|
|
36
62
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
sanitizeApiPath(rawPath) {
|
|
64
|
+
const fallback = '/rest/api';
|
|
65
|
+
const value = (rawPath || '').trim();
|
|
66
|
+
|
|
67
|
+
if (!value) {
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const withoutLeading = value.replace(/^\/+/, '');
|
|
72
|
+
const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
|
|
73
|
+
return normalized || fallback;
|
|
45
74
|
}
|
|
46
75
|
|
|
47
76
|
sanitizeStorageContent(content, options = {}) {
|
|
@@ -72,22 +101,9 @@ class ConfluenceClient {
|
|
|
72
101
|
return result;
|
|
73
102
|
}
|
|
74
103
|
|
|
75
|
-
sanitizeApiPath(rawPath) {
|
|
76
|
-
const fallback = '/rest/api';
|
|
77
|
-
const value = (rawPath || '').trim();
|
|
78
|
-
|
|
79
|
-
if (!value) {
|
|
80
|
-
return fallback;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const withoutLeading = value.replace(/^\/+/, '');
|
|
84
|
-
const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
|
|
85
|
-
return normalized || fallback;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
104
|
buildBasicAuthHeader() {
|
|
89
105
|
if (!this.email) {
|
|
90
|
-
throw new Error('Basic authentication requires an email address.');
|
|
106
|
+
throw new Error('Basic authentication requires an email address or username.');
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
const encodedCredentials = Buffer.from(`${this.email}:${this.token}`).toString('base64');
|
|
@@ -110,12 +126,11 @@ class ConfluenceClient {
|
|
|
110
126
|
return pageIdMatch[1];
|
|
111
127
|
}
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return spacesMatch[1];
|
|
129
|
+
const prettyMatch = pageIdOrUrl.match(/\/pages\/(\d+)(?:[/?#]|$)/);
|
|
130
|
+
if (prettyMatch) {
|
|
131
|
+
return prettyMatch[1];
|
|
117
132
|
}
|
|
118
|
-
|
|
133
|
+
|
|
119
134
|
// Handle display URLs - search by space and title
|
|
120
135
|
const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
|
|
121
136
|
if (displayMatch) {
|
|
@@ -267,10 +282,11 @@ class ConfluenceClient {
|
|
|
267
282
|
/**
|
|
268
283
|
* Search for pages
|
|
269
284
|
*/
|
|
270
|
-
async search(query, limit = 10) {
|
|
285
|
+
async search(query, limit = 10, rawCql = false) {
|
|
286
|
+
const cql = rawCql ? query : `text ~ "${String(query).replace(/"/g, '\\"')}"`;
|
|
271
287
|
const response = await this.client.get('/search', {
|
|
272
288
|
params: {
|
|
273
|
-
cql
|
|
289
|
+
cql,
|
|
274
290
|
limit: limit
|
|
275
291
|
}
|
|
276
292
|
});
|
|
@@ -392,7 +408,7 @@ class ConfluenceClient {
|
|
|
392
408
|
const webui = page._links?.webui || '';
|
|
393
409
|
return {
|
|
394
410
|
title: page.title,
|
|
395
|
-
url: this.
|
|
411
|
+
url: webui ? this.buildUrl(webui) : ''
|
|
396
412
|
};
|
|
397
413
|
}
|
|
398
414
|
return null;
|
|
@@ -486,7 +502,7 @@ class ConfluenceClient {
|
|
|
486
502
|
// Format: - [Page Title](URL)
|
|
487
503
|
const childPagesList = childPages.map(page => {
|
|
488
504
|
const webui = page._links?.webui || '';
|
|
489
|
-
const url = this.
|
|
505
|
+
const url = webui ? this.buildUrl(webui) : '';
|
|
490
506
|
if (url) {
|
|
491
507
|
return `- [${page.title}](${url})`;
|
|
492
508
|
} else {
|
|
@@ -857,6 +873,158 @@ class ConfluenceClient {
|
|
|
857
873
|
return downloadResponse.data;
|
|
858
874
|
}
|
|
859
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Upload an attachment to a page
|
|
878
|
+
*/
|
|
879
|
+
async uploadAttachment(pageIdOrUrl, filePath, options = {}) {
|
|
880
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
881
|
+
throw new Error('File path is required for attachment upload.');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const resolvedPath = path.resolve(filePath);
|
|
885
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
886
|
+
throw new Error(`File not found: ${filePath}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
890
|
+
const form = new FormData();
|
|
891
|
+
form.append('file', fs.createReadStream(resolvedPath), { filename: path.basename(resolvedPath) });
|
|
892
|
+
|
|
893
|
+
if (options.comment !== undefined && options.comment !== null) {
|
|
894
|
+
form.append('comment', options.comment, { contentType: 'text/plain; charset=utf-8' });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (typeof options.minorEdit === 'boolean') {
|
|
898
|
+
form.append('minorEdit', options.minorEdit ? 'true' : 'false');
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const method = options.replace ? 'put' : 'post';
|
|
902
|
+
const response = await this.client.request({
|
|
903
|
+
url: `/content/${pageId}/child/attachment`,
|
|
904
|
+
method,
|
|
905
|
+
headers: {
|
|
906
|
+
...form.getHeaders(),
|
|
907
|
+
'X-Atlassian-Token': 'nocheck'
|
|
908
|
+
},
|
|
909
|
+
data: form,
|
|
910
|
+
maxBodyLength: Infinity,
|
|
911
|
+
maxContentLength: Infinity
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
const results = Array.isArray(response.data?.results)
|
|
915
|
+
? response.data.results.map((item) => this.normalizeAttachment(item))
|
|
916
|
+
: [];
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
results,
|
|
920
|
+
raw: response.data
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Delete an attachment by ID
|
|
926
|
+
*/
|
|
927
|
+
async deleteAttachment(pageIdOrUrl, attachmentId) {
|
|
928
|
+
if (!attachmentId) {
|
|
929
|
+
throw new Error('Attachment ID is required.');
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
933
|
+
await this.client.delete(`/content/${pageId}/child/attachment/${attachmentId}`);
|
|
934
|
+
return { id: String(attachmentId), pageId: String(pageId) };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* List content properties for a page with pagination support
|
|
939
|
+
*/
|
|
940
|
+
async listProperties(pageIdOrUrl, options = {}) {
|
|
941
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
942
|
+
const limit = this.parsePositiveInt(options.limit, 25);
|
|
943
|
+
const start = this.parsePositiveInt(options.start, 0);
|
|
944
|
+
const params = { limit, start };
|
|
945
|
+
|
|
946
|
+
const response = await this.client.get(`/content/${pageId}/property`, { params });
|
|
947
|
+
const results = Array.isArray(response.data.results) ? response.data.results : [];
|
|
948
|
+
|
|
949
|
+
return {
|
|
950
|
+
results,
|
|
951
|
+
nextStart: this.parseNextStart(response.data?._links?.next)
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Fetch all content properties for a page, honoring an optional maxResults cap
|
|
957
|
+
*/
|
|
958
|
+
async getAllProperties(pageIdOrUrl, options = {}) {
|
|
959
|
+
const pageSize = this.parsePositiveInt(options.pageSize || options.limit, 25);
|
|
960
|
+
const maxResults = this.parsePositiveInt(options.maxResults, null);
|
|
961
|
+
let start = this.parsePositiveInt(options.start, 0);
|
|
962
|
+
const properties = [];
|
|
963
|
+
|
|
964
|
+
let hasNext = true;
|
|
965
|
+
while (hasNext) {
|
|
966
|
+
const page = await this.listProperties(pageIdOrUrl, {
|
|
967
|
+
limit: pageSize,
|
|
968
|
+
start
|
|
969
|
+
});
|
|
970
|
+
properties.push(...page.results);
|
|
971
|
+
|
|
972
|
+
if (maxResults && properties.length >= maxResults) {
|
|
973
|
+
return properties.slice(0, maxResults);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
hasNext = page.nextStart !== null && page.nextStart !== undefined;
|
|
977
|
+
if (hasNext) {
|
|
978
|
+
start = page.nextStart;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return properties;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Get a single content property by key
|
|
987
|
+
*/
|
|
988
|
+
async getProperty(pageIdOrUrl, key) {
|
|
989
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
990
|
+
const response = await this.client.get(`/content/${pageId}/property/${encodeURIComponent(key)}`);
|
|
991
|
+
return response.data;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Set (create or update) a content property
|
|
996
|
+
*/
|
|
997
|
+
async setProperty(pageIdOrUrl, key, value) {
|
|
998
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
999
|
+
const encodedKey = encodeURIComponent(key);
|
|
1000
|
+
|
|
1001
|
+
let version = 1;
|
|
1002
|
+
try {
|
|
1003
|
+
const existing = await this.client.get(`/content/${pageId}/property/${encodedKey}`);
|
|
1004
|
+
version = existing.data.version.number + 1;
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
if (!err.response || err.response.status !== 404) {
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const response = await this.client.put(`/content/${pageId}/property/${encodedKey}`, {
|
|
1012
|
+
key,
|
|
1013
|
+
value,
|
|
1014
|
+
version: { number: version }
|
|
1015
|
+
});
|
|
1016
|
+
return response.data;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Delete a content property by key
|
|
1021
|
+
*/
|
|
1022
|
+
async deleteProperty(pageIdOrUrl, key) {
|
|
1023
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
1024
|
+
await this.client.delete(`/content/${pageId}/property/${encodeURIComponent(key)}`);
|
|
1025
|
+
return { pageId: String(pageId), key };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
860
1028
|
/**
|
|
861
1029
|
* Convert markdown to Confluence storage format
|
|
862
1030
|
*/
|
|
@@ -872,36 +1040,39 @@ class ConfluenceClient {
|
|
|
872
1040
|
* Convert HTML to native Confluence storage format
|
|
873
1041
|
*/
|
|
874
1042
|
htmlToConfluenceStorage(html) {
|
|
875
|
-
// MarkdownIt already produces valid HTML tags (h1-h6, p, strong, em, ul, ol, table, etc.)
|
|
876
|
-
// This function only transforms elements that need Confluence-specific handling:
|
|
877
|
-
// - li content wrapped in p (Confluence requirement)
|
|
878
|
-
// - pre/code → ac:structured-macro code blocks with CDATA
|
|
879
|
-
// - blockquote → info/warning/note macros
|
|
880
|
-
// - th/td content wrapped in p
|
|
881
|
-
// - hr → self-closing hr /
|
|
882
1043
|
let storage = html;
|
|
883
1044
|
|
|
884
|
-
//
|
|
1045
|
+
// Convert headings to native Confluence format
|
|
1046
|
+
storage = storage.replace(/<h([1-6])>(.*?)<\/h[1-6]>/g, '<h$1>$2</h$1>');
|
|
1047
|
+
|
|
1048
|
+
// Convert paragraphs
|
|
1049
|
+
storage = storage.replace(/<p>(.*?)<\/p>/g, '<p>$1</p>');
|
|
1050
|
+
|
|
1051
|
+
// Convert strong/bold text
|
|
1052
|
+
storage = storage.replace(/<strong>(.*?)<\/strong>/g, '<strong>$1</strong>');
|
|
1053
|
+
|
|
1054
|
+
// Convert emphasis/italic text
|
|
1055
|
+
storage = storage.replace(/<em>(.*?)<\/em>/g, '<em>$1</em>');
|
|
1056
|
+
|
|
1057
|
+
// Convert unordered lists
|
|
1058
|
+
storage = storage.replace(/<ul>(.*?)<\/ul>/gs, '<ul>$1</ul>');
|
|
885
1059
|
storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
|
|
886
1060
|
|
|
1061
|
+
// Convert ordered lists
|
|
1062
|
+
storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
|
|
1063
|
+
|
|
887
1064
|
// Convert code blocks to Confluence code macro
|
|
888
1065
|
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
889
1066
|
const language = lang || 'text';
|
|
890
|
-
// Decode HTML entities inside code blocks before wrapping in CDATA
|
|
891
|
-
let decoded = code
|
|
892
|
-
.replace(/&/g, '&')
|
|
893
|
-
.replace(/</g, '<')
|
|
894
|
-
.replace(/>/g, '>')
|
|
895
|
-
.replace(/"/g, '"')
|
|
896
|
-
.replace(/'/g, '\'');
|
|
897
|
-
decoded = decoded.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
898
|
-
|
|
899
1067
|
return `<ac:structured-macro ac:name="code">
|
|
900
1068
|
<ac:parameter ac:name="language">${language}</ac:parameter>
|
|
901
|
-
<ac:plain-text-body><![CDATA[${
|
|
1069
|
+
<ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
|
|
902
1070
|
</ac:structured-macro>`;
|
|
903
1071
|
});
|
|
904
1072
|
|
|
1073
|
+
// Convert inline code
|
|
1074
|
+
storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
|
|
1075
|
+
|
|
905
1076
|
// Convert blockquotes to appropriate macros based on content
|
|
906
1077
|
storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
|
|
907
1078
|
// Check for admonition patterns
|
|
@@ -928,14 +1099,22 @@ class ConfluenceClient {
|
|
|
928
1099
|
}
|
|
929
1100
|
});
|
|
930
1101
|
|
|
931
|
-
//
|
|
1102
|
+
// Convert tables
|
|
1103
|
+
storage = storage.replace(/<table>(.*?)<\/table>/gs, '<table>$1</table>');
|
|
1104
|
+
storage = storage.replace(/<thead>(.*?)<\/thead>/gs, '<thead>$1</thead>');
|
|
1105
|
+
storage = storage.replace(/<tbody>(.*?)<\/tbody>/gs, '<tbody>$1</tbody>');
|
|
1106
|
+
storage = storage.replace(/<tr>(.*?)<\/tr>/gs, '<tr>$1</tr>');
|
|
932
1107
|
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
933
1108
|
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
934
1109
|
|
|
935
|
-
//
|
|
1110
|
+
// Convert links
|
|
1111
|
+
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<ac:link><ri:url ri:value="$1" /><ac:plain-text-link-body><![CDATA[$2]]></ac:plain-text-link-body></ac:link>');
|
|
936
1112
|
|
|
937
|
-
//
|
|
938
|
-
storage = storage.replace(/<hr\s*\/?>/g, '
|
|
1113
|
+
// Remove horizontal rules (not needed in Confluence storage format)
|
|
1114
|
+
storage = storage.replace(/<hr\s*\/?>/g, '');
|
|
1115
|
+
|
|
1116
|
+
// Clean up any remaining HTML entities and normalize whitespace
|
|
1117
|
+
storage = storage.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
939
1118
|
|
|
940
1119
|
return storage;
|
|
941
1120
|
}
|
|
@@ -1154,12 +1333,12 @@ class ConfluenceClient {
|
|
|
1154
1333
|
markdown = markdown.replace(/<ac:structured-macro ac:name="include"[^>]*>[\s\S]*?<ac:parameter ac:name="">[\s\S]*?<ac:link>[\s\S]*?<ri:page\s+ri:space-key="([^"]+)"\s+ri:content-title="([^"]+)"[^>]*\/>[\s\S]*?<\/ac:link>[\s\S]*?<\/ac:parameter>[\s\S]*?<\/ac:structured-macro>/g, (_, spaceKey, title) => {
|
|
1155
1334
|
// Try to build a proper URL - if spaceKey starts with ~, it's a user space
|
|
1156
1335
|
if (spaceKey.startsWith('~')) {
|
|
1157
|
-
const spacePath =
|
|
1158
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](${this.
|
|
1336
|
+
const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
1337
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/display/${spacePath}`)})\n`;
|
|
1159
1338
|
} else {
|
|
1160
1339
|
// For non-user spaces, we cannot construct a valid link without the page ID.
|
|
1161
1340
|
// Document that manual correction is required.
|
|
1162
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](${this.
|
|
1341
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
|
|
1163
1342
|
}
|
|
1164
1343
|
});
|
|
1165
1344
|
|
|
@@ -1354,6 +1533,9 @@ class ConfluenceClient {
|
|
|
1354
1533
|
// Numeric HTML entities
|
|
1355
1534
|
markdown = markdown.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
1356
1535
|
markdown = markdown.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCharCode(parseInt(code, 16)));
|
|
1536
|
+
|
|
1537
|
+
// Clean up nordic alphabets and other named entities
|
|
1538
|
+
markdown = markdown.replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name] || match);
|
|
1357
1539
|
|
|
1358
1540
|
// Clean up extra whitespace for standard Markdown format
|
|
1359
1541
|
// Remove trailing spaces from each line
|
|
@@ -1377,7 +1559,7 @@ class ConfluenceClient {
|
|
|
1377
1559
|
*/
|
|
1378
1560
|
async createPage(title, spaceKey, content, format = 'storage', options = {}) {
|
|
1379
1561
|
let storageContent = content;
|
|
1380
|
-
|
|
1562
|
+
|
|
1381
1563
|
if (format === 'markdown') {
|
|
1382
1564
|
storageContent = this.markdownToStorage(content);
|
|
1383
1565
|
} else if (format === 'html') {
|
|
@@ -1412,7 +1594,7 @@ class ConfluenceClient {
|
|
|
1412
1594
|
*/
|
|
1413
1595
|
async createChildPage(title, spaceKey, parentId, content, format = 'storage', options = {}) {
|
|
1414
1596
|
let storageContent = content;
|
|
1415
|
-
|
|
1597
|
+
|
|
1416
1598
|
if (format === 'markdown') {
|
|
1417
1599
|
storageContent = this.markdownToStorage(content);
|
|
1418
1600
|
} else if (format === 'html') {
|
|
@@ -1499,6 +1681,55 @@ class ConfluenceClient {
|
|
|
1499
1681
|
return response.data;
|
|
1500
1682
|
}
|
|
1501
1683
|
|
|
1684
|
+
/**
|
|
1685
|
+
* Move a page to a new parent location
|
|
1686
|
+
*/
|
|
1687
|
+
async movePage(pageIdOrUrl, newParentIdOrUrl, newTitle = null) {
|
|
1688
|
+
// Resolve both IDs from URLs if needed
|
|
1689
|
+
const pageId = await this.extractPageId(pageIdOrUrl);
|
|
1690
|
+
const newParentId = await this.extractPageId(newParentIdOrUrl);
|
|
1691
|
+
|
|
1692
|
+
// Fetch current page
|
|
1693
|
+
const response = await this.client.get(`/content/${pageId}`, {
|
|
1694
|
+
params: { expand: 'body.storage,version,space' }
|
|
1695
|
+
});
|
|
1696
|
+
const { version, title, body, space } = response.data;
|
|
1697
|
+
|
|
1698
|
+
// Fetch new parent to get its space (for validation)
|
|
1699
|
+
const parentResponse = await this.client.get(`/content/${newParentId}`, {
|
|
1700
|
+
params: { expand: 'space' }
|
|
1701
|
+
});
|
|
1702
|
+
const parentSpace = parentResponse.data.space;
|
|
1703
|
+
|
|
1704
|
+
// Validate same space
|
|
1705
|
+
if (parentSpace.key !== space.key) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
`Cannot move page across spaces. Page is in space "${space.key}" ` +
|
|
1708
|
+
`but new parent is in space "${parentSpace.key}". ` +
|
|
1709
|
+
'Pages can only be moved within the same space.'
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Proceed with move
|
|
1714
|
+
const pageData = {
|
|
1715
|
+
id: pageId,
|
|
1716
|
+
type: 'page',
|
|
1717
|
+
title: newTitle || title,
|
|
1718
|
+
space: { key: space.key },
|
|
1719
|
+
body: {
|
|
1720
|
+
storage: {
|
|
1721
|
+
value: body.storage.value,
|
|
1722
|
+
representation: 'storage'
|
|
1723
|
+
}
|
|
1724
|
+
},
|
|
1725
|
+
version: { number: version.number + 1 },
|
|
1726
|
+
ancestors: [{ id: newParentId }]
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
const updateResponse = await this.client.put(`/content/${pageId}`, pageData);
|
|
1730
|
+
return updateResponse.data;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1502
1733
|
/**
|
|
1503
1734
|
* Get page content for editing
|
|
1504
1735
|
*/
|
|
@@ -1826,6 +2057,13 @@ class ConfluenceClient {
|
|
|
1826
2057
|
};
|
|
1827
2058
|
}
|
|
1828
2059
|
|
|
2060
|
+
buildUrl(path) {
|
|
2061
|
+
if (!path) return '';
|
|
2062
|
+
const context = this._contextPath || '';
|
|
2063
|
+
const normalized = path.startsWith('/') ? path : `/${path}`;
|
|
2064
|
+
return `${this.protocol}://${this.domain}${context}${normalized}`;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1829
2067
|
toAbsoluteUrl(pathOrUrl) {
|
|
1830
2068
|
if (!pathOrUrl) {
|
|
1831
2069
|
return null;
|
|
@@ -1835,8 +2073,7 @@ class ConfluenceClient {
|
|
|
1835
2073
|
return pathOrUrl;
|
|
1836
2074
|
}
|
|
1837
2075
|
|
|
1838
|
-
|
|
1839
|
-
return `https://${this.domain}${normalized}`;
|
|
2076
|
+
return this.buildUrl(pathOrUrl);
|
|
1840
2077
|
}
|
|
1841
2078
|
|
|
1842
2079
|
parseNextStart(nextLink) {
|
|
@@ -1863,3 +2100,4 @@ class ConfluenceClient {
|
|
|
1863
2100
|
}
|
|
1864
2101
|
|
|
1865
2102
|
module.exports = ConfluenceClient;
|
|
2103
|
+
module.exports.NAMED_ENTITIES = NAMED_ENTITIES;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bestend/confluence-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": {
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
"wiki",
|
|
21
21
|
"documentation"
|
|
22
22
|
],
|
|
23
|
-
"author": "
|
|
23
|
+
"author": "bestend",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"axios": "^1.12.0",
|
|
27
27
|
"chalk": "^4.1.2",
|
|
28
28
|
"commander": "^11.1.0",
|
|
29
|
-
"fast-xml-parser": "^
|
|
29
|
+
"fast-xml-parser": "^5.5.4",
|
|
30
|
+
"form-data": "^4.0.5",
|
|
30
31
|
"html-to-text": "^9.0.5",
|
|
31
32
|
"inquirer": "^8.2.6",
|
|
32
33
|
"markdown-it": "^14.1.0",
|
|
@@ -54,5 +55,10 @@
|
|
|
54
55
|
"bugs": {
|
|
55
56
|
"url": "https://github.com/bestend/confluence-cli/issues"
|
|
56
57
|
},
|
|
57
|
-
"homepage": "https://github.com/bestend/confluence-cli#readme"
|
|
58
|
+
"homepage": "https://github.com/bestend/confluence-cli#readme",
|
|
59
|
+
"files": [
|
|
60
|
+
"bin/",
|
|
61
|
+
"lib/",
|
|
62
|
+
".claude/skills/"
|
|
63
|
+
]
|
|
58
64
|
}
|
package/.eslintrc.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
env: {
|
|
3
|
-
es2021: true,
|
|
4
|
-
node: true,
|
|
5
|
-
jest: true
|
|
6
|
-
},
|
|
7
|
-
extends: [
|
|
8
|
-
'eslint:recommended'
|
|
9
|
-
],
|
|
10
|
-
parserOptions: {
|
|
11
|
-
ecmaVersion: 12,
|
|
12
|
-
sourceType: 'module'
|
|
13
|
-
},
|
|
14
|
-
rules: {
|
|
15
|
-
'indent': ['error', 2],
|
|
16
|
-
'linebreak-style': ['error', 'unix'],
|
|
17
|
-
'quotes': ['error', 'single'],
|
|
18
|
-
'semi': ['error', 'always'],
|
|
19
|
-
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
20
|
-
'no-console': 'off',
|
|
21
|
-
'no-process-exit': 'off'
|
|
22
|
-
}
|
|
23
|
-
};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Bug report
|
|
3
|
-
about: Create a report to help us improve
|
|
4
|
-
title: '[BUG] '
|
|
5
|
-
labels: bug
|
|
6
|
-
assignees: ''
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
**Describe the bug**
|
|
11
|
-
A clear and concise description of what the bug is.
|
|
12
|
-
|
|
13
|
-
**To Reproduce**
|
|
14
|
-
Steps to reproduce the behavior:
|
|
15
|
-
1. Run command '...'
|
|
16
|
-
2. See error
|
|
17
|
-
|
|
18
|
-
**Expected behavior**
|
|
19
|
-
A clear and concise description of what you expected to happen.
|
|
20
|
-
|
|
21
|
-
**Screenshots**
|
|
22
|
-
If applicable, add screenshots to help explain your problem.
|
|
23
|
-
|
|
24
|
-
**Environment (please complete the following information):**
|
|
25
|
-
- OS: [e.g. macOS, Windows, Linux]
|
|
26
|
-
- Node.js version: [e.g. 18.17.0]
|
|
27
|
-
- confluence-cli version: [e.g. 1.0.0]
|
|
28
|
-
- Confluence version: [e.g. Cloud, Server 8.5]
|
|
29
|
-
|
|
30
|
-
**Additional context**
|
|
31
|
-
Add any other context about the problem here.
|
|
32
|
-
|
|
33
|
-
**Error logs**
|
|
34
|
-
If applicable, add error logs or stack traces.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: Feature request
|
|
3
|
-
about: Suggest an idea for this project
|
|
4
|
-
title: '[FEATURE] '
|
|
5
|
-
labels: enhancement
|
|
6
|
-
assignees: ''
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
**Is your feature request related to a problem? Please describe.**
|
|
11
|
-
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
12
|
-
|
|
13
|
-
**Describe the solution you'd like**
|
|
14
|
-
A clear and concise description of what you want to happen.
|
|
15
|
-
|
|
16
|
-
**Describe alternatives you've considered**
|
|
17
|
-
A clear and concise description of any alternative solutions or features you've considered.
|
|
18
|
-
|
|
19
|
-
**Use case**
|
|
20
|
-
Describe how this feature would be used and who would benefit from it.
|
|
21
|
-
|
|
22
|
-
**Additional context**
|
|
23
|
-
Add any other context or screenshots about the feature request here.
|
|
24
|
-
|
|
25
|
-
**Implementation suggestions**
|
|
26
|
-
If you have ideas about how this could be implemented, please share them here.
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: General Feedback
|
|
3
|
-
about: Share your thoughts, suggestions, or general feedback about confluence-cli
|
|
4
|
-
title: '[FEEDBACK] '
|
|
5
|
-
labels: feedback, enhancement
|
|
6
|
-
assignees: ''
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 📝 Your Feedback
|
|
11
|
-
|
|
12
|
-
Thank you for taking the time to share your thoughts about confluence-cli!
|
|
13
|
-
|
|
14
|
-
### What did you try to accomplish?
|
|
15
|
-
A clear description of what you were trying to do with confluence-cli.
|
|
16
|
-
|
|
17
|
-
### How was your experience?
|
|
18
|
-
Tell us about your experience using the tool:
|
|
19
|
-
- What worked well?
|
|
20
|
-
- What was confusing or difficult?
|
|
21
|
-
- What features are you missing?
|
|
22
|
-
|
|
23
|
-
### Your environment
|
|
24
|
-
- OS: [e.g. macOS, Windows, Linux]
|
|
25
|
-
- Node.js version: [e.g. 18.17.0]
|
|
26
|
-
- confluence-cli version: [e.g. 1.0.1]
|
|
27
|
-
- Confluence instance: [e.g. Cloud, Server, Data Center]
|
|
28
|
-
|
|
29
|
-
### Feature requests or improvements
|
|
30
|
-
What would make confluence-cli more useful for you?
|
|
31
|
-
|
|
32
|
-
### Additional context
|
|
33
|
-
Anything else you'd like to share about your experience with confluence-cli?
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
💡 **Tip**: You can also join our [Discussions](https://github.com/pchuri/confluence-cli/discussions) for general questions and community chat!
|