confluence-cli 1.32.1 → 1.33.1
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/README.md +24 -0
- package/bin/index.js +1 -1
- package/lib/config.js +25 -2
- package/lib/confluence-client.js +46 -0
- package/lib/macro-converter.js +15 -3
- package/npm-shrinkwrap.json +4 -5
- package/package.json +3 -4
- package/plugins/confluence/skills/confluence/SKILL.md +1 -0
package/README.md
CHANGED
|
@@ -249,6 +249,30 @@ Or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`:
|
|
|
249
249
|
}
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
**Link rendering on Cloud (`linkStyle`):**
|
|
253
|
+
|
|
254
|
+
Some Cloud instances — particularly custom-domain Cloud setups — fail to render smart links (`<a data-card-appearance="inline">`) and show "Cannot handle: DefaultLink" errors instead. If you hit this, set `linkStyle` to `plain` to emit simple `<a href>` tags, which render reliably everywhere:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
export CONFLUENCE_LINK_STYLE=plain
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Or per-profile:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"profiles": {
|
|
265
|
+
"default": {
|
|
266
|
+
"domain": "wiki.example.org",
|
|
267
|
+
"forceCloud": true,
|
|
268
|
+
"linkStyle": "plain"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Valid values: `smart` (Cloud smart links), `plain` (simple `<a href>`), `wiki` (Server/DC `ac:link`). When unset, the CLI picks `smart` for Cloud and `wiki` for Server/DC — existing behavior is unchanged.
|
|
275
|
+
|
|
252
276
|
**Read-only mode** (recommended for AI agents):
|
|
253
277
|
```bash
|
|
254
278
|
export CONFLUENCE_READ_ONLY=true
|
package/bin/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
// Make sure we're using a supported Node.js version
|
|
14
14
|
const nodeVersion = process.version;
|
|
15
|
-
const requiredVersion = '
|
|
15
|
+
const requiredVersion = '18.0.0';
|
|
16
16
|
|
|
17
17
|
if (!nodeVersion.startsWith('v') ||
|
|
18
18
|
parseInt(nodeVersion.slice(1).split('.')[0]) < parseInt(requiredVersion.split('.')[0])) {
|
package/lib/config.js
CHANGED
|
@@ -17,6 +17,23 @@ const AUTH_CHOICES = [
|
|
|
17
17
|
|
|
18
18
|
const AUTH_TYPES = ['basic', 'bearer', 'mtls', 'cookie'];
|
|
19
19
|
|
|
20
|
+
const { VALID_LINK_STYLES } = require('./macro-converter');
|
|
21
|
+
|
|
22
|
+
const normalizeLinkStyle = (rawValue, source) => {
|
|
23
|
+
if (rawValue === undefined || rawValue === null || rawValue === '') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const value = String(rawValue).trim().toLowerCase();
|
|
27
|
+
if (VALID_LINK_STYLES.includes(value)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
const label = source ? `${source} ` : '';
|
|
31
|
+
console.error(chalk.yellow(
|
|
32
|
+
`⚠ Invalid linkStyle ${label}"${rawValue}"; valid values: ${VALID_LINK_STYLES.join(', ')}. Falling back to auto-detection.`
|
|
33
|
+
));
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
20
37
|
const isValidProfileName = (name) => /^[a-zA-Z0-9_-]+$/.test(name);
|
|
21
38
|
|
|
22
39
|
const requiredInput = (label) => (input) => {
|
|
@@ -719,6 +736,7 @@ function getConfig(profileName) {
|
|
|
719
736
|
const envProtocol = process.env.CONFLUENCE_PROTOCOL;
|
|
720
737
|
const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
|
|
721
738
|
const envForceCloud = process.env.CONFLUENCE_FORCE_CLOUD;
|
|
739
|
+
const envLinkStyle = normalizeLinkStyle(process.env.CONFLUENCE_LINK_STYLE, 'from CONFLUENCE_LINK_STYLE');
|
|
722
740
|
const envCookie = process.env.CONFLUENCE_COOKIE;
|
|
723
741
|
const envMtls = normalizeMtlsConfig({
|
|
724
742
|
caCert: process.env.CONFLUENCE_TLS_CA_CERT,
|
|
@@ -772,7 +790,8 @@ function getConfig(profileName) {
|
|
|
772
790
|
authType,
|
|
773
791
|
mtls: envMtls,
|
|
774
792
|
readOnly: envReadOnly === 'true',
|
|
775
|
-
forceCloud: envForceCloud === 'true'
|
|
793
|
+
forceCloud: envForceCloud === 'true',
|
|
794
|
+
linkStyle: envLinkStyle
|
|
776
795
|
};
|
|
777
796
|
}
|
|
778
797
|
|
|
@@ -844,6 +863,9 @@ function getConfig(profileName) {
|
|
|
844
863
|
? envForceCloud === 'true'
|
|
845
864
|
: Boolean(storedConfig.forceCloud);
|
|
846
865
|
|
|
866
|
+
const linkStyle = envLinkStyle
|
|
867
|
+
?? normalizeLinkStyle(storedConfig.linkStyle, `in profile "${targetProfile}"`);
|
|
868
|
+
|
|
847
869
|
return {
|
|
848
870
|
domain: trimmedDomain,
|
|
849
871
|
protocol: normalizeProtocol(storedConfig.protocol),
|
|
@@ -854,7 +876,8 @@ function getConfig(profileName) {
|
|
|
854
876
|
authType,
|
|
855
877
|
mtls,
|
|
856
878
|
readOnly,
|
|
857
|
-
forceCloud
|
|
879
|
+
forceCloud,
|
|
880
|
+
linkStyle
|
|
858
881
|
};
|
|
859
882
|
} catch (error) {
|
|
860
883
|
console.error(chalk.red('❌ Error reading configuration file:'), error.message);
|
package/lib/confluence-client.js
CHANGED
|
@@ -47,6 +47,7 @@ class ConfluenceClient {
|
|
|
47
47
|
isCloud: this.isCloud(),
|
|
48
48
|
webUrlPrefix: this.webUrlPrefix,
|
|
49
49
|
buildUrl: (pathOrUrl) => this.buildUrl(pathOrUrl),
|
|
50
|
+
linkStyle: config.linkStyle,
|
|
50
51
|
});
|
|
51
52
|
this.markdown = this.converter.markdown;
|
|
52
53
|
|
|
@@ -995,6 +996,12 @@ class ConfluenceClient {
|
|
|
995
996
|
throw new Error('Unable to determine download URL for attachment');
|
|
996
997
|
}
|
|
997
998
|
|
|
999
|
+
// Refuse to send credentials to an unexpected origin. The download URL is
|
|
1000
|
+
// derived from a server-supplied _links.download value, so a tampered or
|
|
1001
|
+
// misconfigured response could otherwise exfiltrate the bearer/basic token,
|
|
1002
|
+
// including via an http:// downgrade against an https-configured client.
|
|
1003
|
+
this.assertSameOrigin(downloadUrl);
|
|
1004
|
+
|
|
998
1005
|
// Download directly using axios with the same auth headers
|
|
999
1006
|
const downloadRequestConfig = {
|
|
1000
1007
|
responseType: options.responseType || 'stream',
|
|
@@ -1779,6 +1786,45 @@ class ConfluenceClient {
|
|
|
1779
1786
|
return `${this.protocol}://${this.domain}${normalized}`;
|
|
1780
1787
|
}
|
|
1781
1788
|
|
|
1789
|
+
configuredOrigin() {
|
|
1790
|
+
if (!this.domain) {
|
|
1791
|
+
return null;
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
return new URL(`${this.protocol}://${this.domain}`).origin;
|
|
1795
|
+
} catch {
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
isSameOriginAsConfigured(url) {
|
|
1801
|
+
const expected = this.configuredOrigin();
|
|
1802
|
+
if (!url || !expected) {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
try {
|
|
1806
|
+
return new URL(url).origin === expected;
|
|
1807
|
+
} catch {
|
|
1808
|
+
return false;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
assertSameOrigin(url) {
|
|
1813
|
+
if (this.isSameOriginAsConfigured(url)) {
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
let actualOrigin;
|
|
1817
|
+
try {
|
|
1818
|
+
actualOrigin = new URL(url).origin;
|
|
1819
|
+
} catch {
|
|
1820
|
+
actualOrigin = String(url);
|
|
1821
|
+
}
|
|
1822
|
+
const expectedOrigin = this.configuredOrigin() ?? `${this.protocol}://${this.domain}`;
|
|
1823
|
+
throw new Error(
|
|
1824
|
+
`Refusing to send credentials to "${actualOrigin}": origin does not match the configured Confluence origin "${expectedOrigin}". This may indicate a tampered or misconfigured API response, or an http downgrade against an https-configured client.`
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1782
1828
|
joinBaseUrl(baseUrl, path) {
|
|
1783
1829
|
if (!baseUrl) {
|
|
1784
1830
|
return null;
|
package/lib/macro-converter.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const MarkdownIt = require('markdown-it');
|
|
2
2
|
const { htmlToMarkdown } = require('./html-to-markdown');
|
|
3
3
|
|
|
4
|
+
const VALID_LINK_STYLES = ['smart', 'plain', 'wiki'];
|
|
5
|
+
|
|
4
6
|
class MacroConverter {
|
|
5
|
-
constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null } = {}) {
|
|
7
|
+
constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null, linkStyle = null } = {}) {
|
|
6
8
|
this._isCloud = isCloud;
|
|
7
9
|
this.webUrlPrefix = webUrlPrefix;
|
|
8
10
|
this.buildUrl = buildUrl || ((pathOrUrl) => pathOrUrl);
|
|
11
|
+
this.linkStyle = VALID_LINK_STYLES.includes(linkStyle)
|
|
12
|
+
? linkStyle
|
|
13
|
+
: (isCloud ? 'smart' : 'wiki');
|
|
9
14
|
this.markdown = new MarkdownIt();
|
|
10
15
|
this.setupConfluenceMarkdownExtensions();
|
|
11
16
|
}
|
|
@@ -107,11 +112,17 @@ class MacroConverter {
|
|
|
107
112
|
storage = storage.replace(/<th>(.*?)<\/th>/g, '<th><p>$1</p></th>');
|
|
108
113
|
storage = storage.replace(/<td>(.*?)<\/td>/g, '<td><p>$1</p></td>');
|
|
109
114
|
|
|
110
|
-
|
|
115
|
+
// Convert links based on linkStyle:
|
|
116
|
+
// "smart" — Cloud smart links (<a data-card-appearance="inline">)
|
|
117
|
+
// "plain" — simple <a href>; workaround for "Cannot handle: DefaultLink"
|
|
118
|
+
// errors on custom-domain Cloud instances
|
|
119
|
+
// "wiki" — Server/DC ac:link + ri:url storage format
|
|
120
|
+
if (this.linkStyle === 'smart') {
|
|
111
121
|
storage = storage.replace(/<a href="(.*?)">(.*?)<\/a>/g, '<a href="$1" data-card-appearance="inline">$2</a>');
|
|
112
|
-
} else {
|
|
122
|
+
} else if (this.linkStyle === 'wiki') {
|
|
113
123
|
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>');
|
|
114
124
|
}
|
|
125
|
+
// "plain" — leave <a href> tags as-is
|
|
115
126
|
|
|
116
127
|
storage = storage.replace(/<hr\s*\/?>/g, '<hr />');
|
|
117
128
|
|
|
@@ -296,3 +307,4 @@ class MacroConverter {
|
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
module.exports = MacroConverter;
|
|
310
|
+
module.exports.VALID_LINK_STYLES = VALID_LINK_STYLES;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.33.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "^1.15.0",
|
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
"form-data": "^4.0.5",
|
|
16
16
|
"html-to-text": "^9.0.5",
|
|
17
17
|
"inquirer": "^8.2.6",
|
|
18
|
-
"markdown-it": "^14.1.0"
|
|
19
|
-
"ora": "^5.4.1"
|
|
18
|
+
"markdown-it": "^14.1.0"
|
|
20
19
|
},
|
|
21
20
|
"bin": {
|
|
22
21
|
"confluence": "bin/index.js",
|
|
23
22
|
"confluence-cli": "bin/index.js"
|
|
24
23
|
},
|
|
25
24
|
"engines": {
|
|
26
|
-
"node": ">=
|
|
25
|
+
"node": ">=18.0.0"
|
|
27
26
|
}
|
|
28
27
|
},
|
|
29
28
|
"node_modules/@inquirer/external-editor": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.1",
|
|
4
4
|
"description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
"form-data": "^4.0.5",
|
|
30
30
|
"html-to-text": "^9.0.5",
|
|
31
31
|
"inquirer": "^8.2.6",
|
|
32
|
-
"markdown-it": "^14.1.0"
|
|
33
|
-
"ora": "^5.4.1"
|
|
32
|
+
"markdown-it": "^14.1.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/node": "^20.10.0",
|
|
@@ -45,7 +44,7 @@
|
|
|
45
44
|
}
|
|
46
45
|
},
|
|
47
46
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
47
|
+
"node": ">=18.0.0"
|
|
49
48
|
},
|
|
50
49
|
"repository": {
|
|
51
50
|
"type": "git",
|
|
@@ -30,6 +30,7 @@ confluence --version # verify install
|
|
|
30
30
|
| `CONFLUENCE_PROFILE` | Named profile to use (optional) | `staging` |
|
|
31
31
|
| `CONFLUENCE_READ_ONLY` | Block all write operations when `true` | `true` |
|
|
32
32
|
| `CONFLUENCE_FORCE_CLOUD` | Force Cloud link format for custom domains | `true` |
|
|
33
|
+
| `CONFLUENCE_LINK_STYLE` | Override link rendering: `smart`, `plain`, or `wiki` | `plain` |
|
|
33
34
|
|
|
34
35
|
**Global `--profile` flag (use a named profile for any command):**
|
|
35
36
|
|