@bestend/confluence-cli 1.16.0 → 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 +836 -116
- package/bin/index.js +6 -1
- package/lib/config.js +242 -40
- package/lib/confluence-client.js +309 -52
- 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
|
|
|
@@ -23,6 +49,28 @@ class ConfluenceClient {
|
|
|
23
49
|
baseURL: this.baseURL,
|
|
24
50
|
headers
|
|
25
51
|
});
|
|
52
|
+
|
|
53
|
+
// Auto-detect context path (e.g., "/wiki" or "") from first API response
|
|
54
|
+
this._contextPath = null;
|
|
55
|
+
this.client.interceptors.response.use((response) => {
|
|
56
|
+
if (this._contextPath === null && response.data?._links?.context !== undefined) {
|
|
57
|
+
this._contextPath = response.data._links.context;
|
|
58
|
+
}
|
|
59
|
+
return response;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
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;
|
|
26
74
|
}
|
|
27
75
|
|
|
28
76
|
sanitizeStorageContent(content, options = {}) {
|
|
@@ -53,22 +101,9 @@ class ConfluenceClient {
|
|
|
53
101
|
return result;
|
|
54
102
|
}
|
|
55
103
|
|
|
56
|
-
sanitizeApiPath(rawPath) {
|
|
57
|
-
const fallback = '/rest/api';
|
|
58
|
-
const value = (rawPath || '').trim();
|
|
59
|
-
|
|
60
|
-
if (!value) {
|
|
61
|
-
return fallback;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const withoutLeading = value.replace(/^\/+/, '');
|
|
65
|
-
const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
|
|
66
|
-
return normalized || fallback;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
104
|
buildBasicAuthHeader() {
|
|
70
105
|
if (!this.email) {
|
|
71
|
-
throw new Error('Basic authentication requires an email address.');
|
|
106
|
+
throw new Error('Basic authentication requires an email address or username.');
|
|
72
107
|
}
|
|
73
108
|
|
|
74
109
|
const encodedCredentials = Buffer.from(`${this.email}:${this.token}`).toString('base64');
|
|
@@ -91,12 +126,11 @@ class ConfluenceClient {
|
|
|
91
126
|
return pageIdMatch[1];
|
|
92
127
|
}
|
|
93
128
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return spacesMatch[1];
|
|
129
|
+
const prettyMatch = pageIdOrUrl.match(/\/pages\/(\d+)(?:[/?#]|$)/);
|
|
130
|
+
if (prettyMatch) {
|
|
131
|
+
return prettyMatch[1];
|
|
98
132
|
}
|
|
99
|
-
|
|
133
|
+
|
|
100
134
|
// Handle display URLs - search by space and title
|
|
101
135
|
const displayMatch = pageIdOrUrl.match(/\/display\/([^/]+)\/(.+)/);
|
|
102
136
|
if (displayMatch) {
|
|
@@ -248,10 +282,11 @@ class ConfluenceClient {
|
|
|
248
282
|
/**
|
|
249
283
|
* Search for pages
|
|
250
284
|
*/
|
|
251
|
-
async search(query, limit = 10) {
|
|
285
|
+
async search(query, limit = 10, rawCql = false) {
|
|
286
|
+
const cql = rawCql ? query : `text ~ "${String(query).replace(/"/g, '\\"')}"`;
|
|
252
287
|
const response = await this.client.get('/search', {
|
|
253
288
|
params: {
|
|
254
|
-
cql
|
|
289
|
+
cql,
|
|
255
290
|
limit: limit
|
|
256
291
|
}
|
|
257
292
|
});
|
|
@@ -373,7 +408,7 @@ class ConfluenceClient {
|
|
|
373
408
|
const webui = page._links?.webui || '';
|
|
374
409
|
return {
|
|
375
410
|
title: page.title,
|
|
376
|
-
url: webui ?
|
|
411
|
+
url: webui ? this.buildUrl(webui) : ''
|
|
377
412
|
};
|
|
378
413
|
}
|
|
379
414
|
return null;
|
|
@@ -467,7 +502,7 @@ class ConfluenceClient {
|
|
|
467
502
|
// Format: - [Page Title](URL)
|
|
468
503
|
const childPagesList = childPages.map(page => {
|
|
469
504
|
const webui = page._links?.webui || '';
|
|
470
|
-
const url = webui ?
|
|
505
|
+
const url = webui ? this.buildUrl(webui) : '';
|
|
471
506
|
if (url) {
|
|
472
507
|
return `- [${page.title}](${url})`;
|
|
473
508
|
} else {
|
|
@@ -838,6 +873,158 @@ class ConfluenceClient {
|
|
|
838
873
|
return downloadResponse.data;
|
|
839
874
|
}
|
|
840
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
|
+
|
|
841
1028
|
/**
|
|
842
1029
|
* Convert markdown to Confluence storage format
|
|
843
1030
|
*/
|
|
@@ -853,36 +1040,39 @@ class ConfluenceClient {
|
|
|
853
1040
|
* Convert HTML to native Confluence storage format
|
|
854
1041
|
*/
|
|
855
1042
|
htmlToConfluenceStorage(html) {
|
|
856
|
-
// MarkdownIt already produces valid HTML tags (h1-h6, p, strong, em, ul, ol, table, etc.)
|
|
857
|
-
// This function only transforms elements that need Confluence-specific handling:
|
|
858
|
-
// - li content wrapped in p (Confluence requirement)
|
|
859
|
-
// - pre/code → ac:structured-macro code blocks with CDATA
|
|
860
|
-
// - blockquote → info/warning/note macros
|
|
861
|
-
// - th/td content wrapped in p
|
|
862
|
-
// - hr → self-closing hr /
|
|
863
1043
|
let storage = html;
|
|
864
1044
|
|
|
865
|
-
//
|
|
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>');
|
|
866
1059
|
storage = storage.replace(/<li>(.*?)<\/li>/g, '<li><p>$1</p></li>');
|
|
867
1060
|
|
|
1061
|
+
// Convert ordered lists
|
|
1062
|
+
storage = storage.replace(/<ol>(.*?)<\/ol>/gs, '<ol>$1</ol>');
|
|
1063
|
+
|
|
868
1064
|
// Convert code blocks to Confluence code macro
|
|
869
1065
|
storage = storage.replace(/<pre><code(?:\s+class="language-(\w+)")?>(.*?)<\/code><\/pre>/gs, (_, lang, code) => {
|
|
870
1066
|
const language = lang || 'text';
|
|
871
|
-
// Decode HTML entities inside code blocks before wrapping in CDATA
|
|
872
|
-
let decoded = code
|
|
873
|
-
.replace(/&/g, '&')
|
|
874
|
-
.replace(/</g, '<')
|
|
875
|
-
.replace(/>/g, '>')
|
|
876
|
-
.replace(/"/g, '"')
|
|
877
|
-
.replace(/'/g, '\'');
|
|
878
|
-
decoded = decoded.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
879
|
-
|
|
880
1067
|
return `<ac:structured-macro ac:name="code">
|
|
881
1068
|
<ac:parameter ac:name="language">${language}</ac:parameter>
|
|
882
|
-
<ac:plain-text-body><![CDATA[${
|
|
1069
|
+
<ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>
|
|
883
1070
|
</ac:structured-macro>`;
|
|
884
1071
|
});
|
|
885
1072
|
|
|
1073
|
+
// Convert inline code
|
|
1074
|
+
storage = storage.replace(/<code>(.*?)<\/code>/g, '<code>$1</code>');
|
|
1075
|
+
|
|
886
1076
|
// Convert blockquotes to appropriate macros based on content
|
|
887
1077
|
storage = storage.replace(/<blockquote>(.*?)<\/blockquote>/gs, (_, content) => {
|
|
888
1078
|
// Check for admonition patterns
|
|
@@ -909,14 +1099,22 @@ class ConfluenceClient {
|
|
|
909
1099
|
}
|
|
910
1100
|
});
|
|
911
1101
|
|
|
912
|
-
//
|
|
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>');
|
|
913
1107
|
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
914
1108
|
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
915
1109
|
|
|
916
|
-
//
|
|
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>');
|
|
917
1112
|
|
|
918
|
-
//
|
|
919
|
-
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, '&');
|
|
920
1118
|
|
|
921
1119
|
return storage;
|
|
922
1120
|
}
|
|
@@ -1136,11 +1334,11 @@ class ConfluenceClient {
|
|
|
1136
1334
|
// Try to build a proper URL - if spaceKey starts with ~, it's a user space
|
|
1137
1335
|
if (spaceKey.startsWith('~')) {
|
|
1138
1336
|
const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`;
|
|
1139
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1337
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/display/${spacePath}`)})\n`;
|
|
1140
1338
|
} else {
|
|
1141
1339
|
// For non-user spaces, we cannot construct a valid link without the page ID.
|
|
1142
1340
|
// Document that manual correction is required.
|
|
1143
|
-
return `\n> 📄 **${labels.includePage}**: [${title}](
|
|
1341
|
+
return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`;
|
|
1144
1342
|
}
|
|
1145
1343
|
});
|
|
1146
1344
|
|
|
@@ -1335,6 +1533,9 @@ class ConfluenceClient {
|
|
|
1335
1533
|
// Numeric HTML entities
|
|
1336
1534
|
markdown = markdown.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
|
|
1337
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);
|
|
1338
1539
|
|
|
1339
1540
|
// Clean up extra whitespace for standard Markdown format
|
|
1340
1541
|
// Remove trailing spaces from each line
|
|
@@ -1358,7 +1559,7 @@ class ConfluenceClient {
|
|
|
1358
1559
|
*/
|
|
1359
1560
|
async createPage(title, spaceKey, content, format = 'storage', options = {}) {
|
|
1360
1561
|
let storageContent = content;
|
|
1361
|
-
|
|
1562
|
+
|
|
1362
1563
|
if (format === 'markdown') {
|
|
1363
1564
|
storageContent = this.markdownToStorage(content);
|
|
1364
1565
|
} else if (format === 'html') {
|
|
@@ -1393,7 +1594,7 @@ class ConfluenceClient {
|
|
|
1393
1594
|
*/
|
|
1394
1595
|
async createChildPage(title, spaceKey, parentId, content, format = 'storage', options = {}) {
|
|
1395
1596
|
let storageContent = content;
|
|
1396
|
-
|
|
1597
|
+
|
|
1397
1598
|
if (format === 'markdown') {
|
|
1398
1599
|
storageContent = this.markdownToStorage(content);
|
|
1399
1600
|
} else if (format === 'html') {
|
|
@@ -1480,6 +1681,55 @@ class ConfluenceClient {
|
|
|
1480
1681
|
return response.data;
|
|
1481
1682
|
}
|
|
1482
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
|
+
|
|
1483
1733
|
/**
|
|
1484
1734
|
* Get page content for editing
|
|
1485
1735
|
*/
|
|
@@ -1807,6 +2057,13 @@ class ConfluenceClient {
|
|
|
1807
2057
|
};
|
|
1808
2058
|
}
|
|
1809
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
|
+
|
|
1810
2067
|
toAbsoluteUrl(pathOrUrl) {
|
|
1811
2068
|
if (!pathOrUrl) {
|
|
1812
2069
|
return null;
|
|
@@ -1816,8 +2073,7 @@ class ConfluenceClient {
|
|
|
1816
2073
|
return pathOrUrl;
|
|
1817
2074
|
}
|
|
1818
2075
|
|
|
1819
|
-
|
|
1820
|
-
return `https://${this.domain}${normalized}`;
|
|
2076
|
+
return this.buildUrl(pathOrUrl);
|
|
1821
2077
|
}
|
|
1822
2078
|
|
|
1823
2079
|
parseNextStart(nextLink) {
|
|
@@ -1844,3 +2100,4 @@ class ConfluenceClient {
|
|
|
1844
2100
|
}
|
|
1845
2101
|
|
|
1846
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!
|