confluence-cli 1.30.1 → 1.30.2

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 CHANGED
@@ -204,6 +204,27 @@ export CONFLUENCE_API_TOKEN="your-scoped-token"
204
204
 
205
205
  `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. For `mtls`, set `CONFLUENCE_TLS_CLIENT_CERT` and `CONFLUENCE_TLS_CLIENT_KEY`; `CONFLUENCE_TLS_CA_CERT` is optional.
206
206
 
207
+ **Custom domains on Confluence Cloud:**
208
+
209
+ If your Confluence Cloud instance uses a custom domain (e.g., `wiki.example.org` instead of `*.atlassian.net`), the CLI may misidentify it as a Server/Data Center instance and produce broken link formats. Set `CONFLUENCE_FORCE_CLOUD=true` to override the automatic detection:
210
+
211
+ ```bash
212
+ export CONFLUENCE_FORCE_CLOUD=true
213
+ ```
214
+
215
+ Or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`:
216
+
217
+ ```json
218
+ {
219
+ "profiles": {
220
+ "default": {
221
+ "domain": "wiki.example.org",
222
+ "forceCloud": true
223
+ }
224
+ }
225
+ }
226
+ ```
227
+
207
228
  **Read-only mode** (recommended for AI agents):
208
229
  ```bash
209
230
  export CONFLUENCE_READ_ONLY=true
package/bin/confluence.js CHANGED
@@ -16,6 +16,18 @@ function assertWritable(config) {
16
16
  }
17
17
  }
18
18
 
19
+ function assertNonEmpty(value, label) {
20
+ if (typeof value !== 'string' || !value.trim()) {
21
+ throw new Error(`${label} is required and cannot be empty.`);
22
+ }
23
+ }
24
+
25
+ function handleCommandError(analytics, commandName, error) {
26
+ analytics.track(commandName, false);
27
+ console.error(chalk.red('Error:'), error.message);
28
+ process.exit(1);
29
+ }
30
+
19
31
  program
20
32
  .name('confluence')
21
33
  .description('CLI tool for Atlassian Confluence')
@@ -59,9 +71,7 @@ program
59
71
  console.log(content);
60
72
  analytics.track('read', true);
61
73
  } catch (error) {
62
- analytics.track('read', false);
63
- console.error(chalk.red('Error:'), error.message);
64
- process.exit(1);
74
+ handleCommandError(analytics, 'read', error);
65
75
  }
66
76
  });
67
77
 
@@ -84,9 +94,7 @@ program
84
94
  }
85
95
  analytics.track('info', true);
86
96
  } catch (error) {
87
- analytics.track('info', false);
88
- console.error(chalk.red('Error:'), error.message);
89
- process.exit(1);
97
+ handleCommandError(analytics, 'info', error);
90
98
  }
91
99
  });
92
100
 
@@ -117,9 +125,7 @@ program
117
125
  });
118
126
  analytics.track('search', true);
119
127
  } catch (error) {
120
- analytics.track('search', false);
121
- console.error(chalk.red('Error:'), error.message);
122
- process.exit(1);
128
+ handleCommandError(analytics, 'search', error);
123
129
  }
124
130
  });
125
131
 
@@ -140,9 +146,7 @@ program
140
146
  });
141
147
  analytics.track('spaces', true);
142
148
  } catch (error) {
143
- analytics.track('spaces', false);
144
- console.error(chalk.red('Error:'), error.message);
145
- process.exit(1);
149
+ handleCommandError(analytics, 'spaces', error);
146
150
  }
147
151
  });
148
152
 
@@ -214,12 +218,15 @@ program
214
218
  .action(async (title, spaceKey, options) => {
215
219
  const analytics = new Analytics();
216
220
  try {
221
+ assertNonEmpty(title, 'title');
222
+ assertNonEmpty(spaceKey, 'spaceKey');
223
+
217
224
  const config = getConfig(getProfileName());
218
225
  assertWritable(config);
219
226
  const client = new ConfluenceClient(config);
220
227
 
221
228
  let content = '';
222
-
229
+
223
230
  if (options.file) {
224
231
  const fs = require('fs');
225
232
  if (!fs.existsSync(options.file)) {
@@ -231,7 +238,7 @@ program
231
238
  } else {
232
239
  throw new Error('Either --file or --content option is required');
233
240
  }
234
-
241
+
235
242
  const result = await client.createPage(title, spaceKey, content, options.format);
236
243
 
237
244
  console.log(chalk.green('✅ Page created successfully!'));
@@ -242,9 +249,7 @@ program
242
249
 
243
250
  analytics.track('create', true);
244
251
  } catch (error) {
245
- analytics.track('create', false);
246
- console.error(chalk.red('Error:'), error.message);
247
- process.exit(1);
252
+ handleCommandError(analytics, 'create', error);
248
253
  }
249
254
  });
250
255
 
@@ -258,6 +263,9 @@ program
258
263
  .action(async (title, parentId, options) => {
259
264
  const analytics = new Analytics();
260
265
  try {
266
+ assertNonEmpty(title, 'title');
267
+ assertNonEmpty(parentId, 'parentId');
268
+
261
269
  const config = getConfig(getProfileName());
262
270
  assertWritable(config);
263
271
  const client = new ConfluenceClient(config);
@@ -291,9 +299,7 @@ program
291
299
 
292
300
  analytics.track('create_child', true);
293
301
  } catch (error) {
294
- analytics.track('create_child', false);
295
- console.error(chalk.red('Error:'), error.message);
296
- process.exit(1);
302
+ handleCommandError(analytics, 'create_child', error);
297
303
  }
298
304
  });
299
305
 
@@ -313,6 +319,10 @@ program
313
319
  throw new Error('At least one of --title, --file, or --content must be provided.');
314
320
  }
315
321
 
322
+ if (options.title !== undefined) {
323
+ assertNonEmpty(options.title, '--title');
324
+ }
325
+
316
326
  const config = getConfig(getProfileName());
317
327
  assertWritable(config);
318
328
  const client = new ConfluenceClient(config);
@@ -339,9 +349,7 @@ program
339
349
 
340
350
  analytics.track('update', true);
341
351
  } catch (error) {
342
- analytics.track('update', false);
343
- console.error(chalk.red('Error:'), error.message);
344
- process.exit(1);
352
+ handleCommandError(analytics, 'update', error);
345
353
  }
346
354
  });
347
355
 
@@ -367,9 +375,7 @@ program
367
375
 
368
376
  analytics.track('move', true);
369
377
  } catch (error) {
370
- analytics.track('move', false);
371
- console.error(chalk.red('Error:'), error.message);
372
- process.exit(1);
378
+ handleCommandError(analytics, 'move', error);
373
379
  }
374
380
  });
375
381
 
@@ -411,9 +417,7 @@ program
411
417
  console.log(`ID: ${chalk.blue(result.id)}`);
412
418
  analytics.track('delete', true);
413
419
  } catch (error) {
414
- analytics.track('delete', false);
415
- console.error(chalk.red('Error:'), error.message);
416
- process.exit(1);
420
+ handleCommandError(analytics, 'delete', error);
417
421
  }
418
422
  });
419
423
 
@@ -449,9 +453,7 @@ program
449
453
 
450
454
  analytics.track('edit', true);
451
455
  } catch (error) {
452
- analytics.track('edit', false);
453
- console.error(chalk.red('Error:'), error.message);
454
- process.exit(1);
456
+ handleCommandError(analytics, 'edit', error);
455
457
  }
456
458
  });
457
459
 
@@ -475,9 +477,7 @@ program
475
477
 
476
478
  analytics.track('find', true);
477
479
  } catch (error) {
478
- analytics.track('find', false);
479
- console.error(chalk.red('Error:'), error.message);
480
- process.exit(1);
480
+ handleCommandError(analytics, 'find', error);
481
481
  }
482
482
  });
483
483
 
@@ -597,9 +597,7 @@ program
597
597
 
598
598
  analytics.track('attachments', true);
599
599
  } catch (error) {
600
- analytics.track('attachments', false);
601
- console.error(chalk.red('Error:'), error.message);
602
- process.exit(1);
600
+ handleCommandError(analytics, 'attachments', error);
603
601
  }
604
602
  });
605
603
 
@@ -659,9 +657,7 @@ program
659
657
  console.log(chalk.green(`Uploaded ${uploaded} attachment${uploaded === 1 ? '' : 's'} to page ${pageId}`));
660
658
  analytics.track('attachment_upload', true);
661
659
  } catch (error) {
662
- analytics.track('attachment_upload', false);
663
- console.error(chalk.red('Error:'), error.message);
664
- process.exit(1);
660
+ handleCommandError(analytics, 'attachment_upload', error);
665
661
  }
666
662
  });
667
663
 
@@ -701,9 +697,7 @@ program
701
697
  console.log(`Page ID: ${chalk.blue(result.pageId)}`);
702
698
  analytics.track('attachment_delete', true);
703
699
  } catch (error) {
704
- analytics.track('attachment_delete', false);
705
- console.error(chalk.red('Error:'), error.message);
706
- process.exit(1);
700
+ handleCommandError(analytics, 'attachment_delete', error);
707
701
  }
708
702
  });
709
703
 
@@ -774,9 +768,7 @@ program
774
768
  }
775
769
  analytics.track('property_list', true);
776
770
  } catch (error) {
777
- analytics.track('property_list', false);
778
- console.error(chalk.red('Error:'), error.message);
779
- process.exit(1);
771
+ handleCommandError(analytics, 'property_list', error);
780
772
  }
781
773
  });
782
774
 
@@ -808,9 +800,7 @@ program
808
800
  }
809
801
  analytics.track('property_get', true);
810
802
  } catch (error) {
811
- analytics.track('property_get', false);
812
- console.error(chalk.red('Error:'), error.message);
813
- process.exit(1);
803
+ handleCommandError(analytics, 'property_get', error);
814
804
  }
815
805
  });
816
806
 
@@ -867,9 +857,7 @@ program
867
857
  }
868
858
  analytics.track('property_set', true);
869
859
  } catch (error) {
870
- analytics.track('property_set', false);
871
- console.error(chalk.red('Error:'), error.message);
872
- process.exit(1);
860
+ handleCommandError(analytics, 'property_set', error);
873
861
  }
874
862
  });
875
863
 
@@ -909,9 +897,7 @@ program
909
897
  console.log(`${chalk.green('Page ID:')} ${chalk.blue(result.pageId)}`);
910
898
  analytics.track('property_delete', true);
911
899
  } catch (error) {
912
- analytics.track('property_delete', false);
913
- console.error(chalk.red('Error:'), error.message);
914
- process.exit(1);
900
+ handleCommandError(analytics, 'property_delete', error);
915
901
  }
916
902
  });
917
903
 
@@ -1060,9 +1046,7 @@ program
1060
1046
 
1061
1047
  analytics.track('comments', true);
1062
1048
  } catch (error) {
1063
- analytics.track('comments', false);
1064
- console.error(chalk.red('Error:'), error.message);
1065
- process.exit(1);
1049
+ handleCommandError(analytics, 'comments', error);
1066
1050
  }
1067
1051
  });
1068
1052
 
@@ -1225,9 +1209,7 @@ program
1225
1209
  console.log(`ID: ${chalk.blue(result.id)}`);
1226
1210
  analytics.track('comment_delete', true);
1227
1211
  } catch (error) {
1228
- analytics.track('comment_delete', false);
1229
- console.error(chalk.red('Error:'), error.message);
1230
- process.exit(1);
1212
+ handleCommandError(analytics, 'comment_delete', error);
1231
1213
  }
1232
1214
  });
1233
1215
 
@@ -1332,9 +1314,7 @@ program
1332
1314
 
1333
1315
  analytics.track('export', true);
1334
1316
  } catch (error) {
1335
- analytics.track('export', false);
1336
- console.error(chalk.red('Error:'), error.message);
1337
- process.exit(1);
1317
+ handleCommandError(analytics, 'export', error);
1338
1318
  }
1339
1319
  });
1340
1320
 
@@ -1720,9 +1700,7 @@ program
1720
1700
 
1721
1701
  analytics.track('copy_tree', true);
1722
1702
  } catch (error) {
1723
- analytics.track('copy_tree', false);
1724
- console.error(chalk.red('Error:'), error.message);
1725
- process.exit(1);
1703
+ handleCommandError(analytics, 'copy_tree', error);
1726
1704
  }
1727
1705
  });
1728
1706
 
@@ -1819,9 +1797,7 @@ program
1819
1797
 
1820
1798
  analytics.track('children', true);
1821
1799
  } catch (error) {
1822
- analytics.track('children', false);
1823
- console.error(chalk.red('Error:'), error.message);
1824
- process.exit(1);
1800
+ handleCommandError(analytics, 'children', error);
1825
1801
  }
1826
1802
  });
1827
1803
 
@@ -2039,9 +2015,7 @@ program
2039
2015
  }
2040
2016
  analytics.track('convert', true);
2041
2017
  } catch (error) {
2042
- analytics.track('convert', false);
2043
- console.error(chalk.red('Error:'), error.message);
2044
- process.exit(1);
2018
+ handleCommandError(analytics, 'convert', error);
2045
2019
  }
2046
2020
  });
2047
2021
 
@@ -2056,6 +2030,8 @@ module.exports = {
2056
2030
  exportRecursive,
2057
2031
  sanitizeTitle,
2058
2032
  assertWritable,
2033
+ assertNonEmpty,
2034
+ handleCommandError,
2059
2035
  },
2060
2036
  };
2061
2037
 
package/lib/config.js CHANGED
@@ -100,6 +100,30 @@ const validateMtlsProtocol = (protocol) => {
100
100
  return null;
101
101
  };
102
102
 
103
+ // Validate a resolved auth configuration (post-normalization).
104
+ // Returns error messages only; callers format source-specific hints.
105
+ const validateAuthConfig = (auth, mtlsSourceLabel) => {
106
+ const errors = [];
107
+
108
+ if (auth.authType === 'basic' && !auth.email) {
109
+ errors.push('Basic authentication requires an email address or username.');
110
+ }
111
+
112
+ if (auth.authType !== 'mtls' && !auth.token) {
113
+ errors.push('Bearer or basic authentication requires a token.');
114
+ }
115
+
116
+ if (auth.authType === 'mtls') {
117
+ errors.push(...validateMtlsConfig(auth.mtls, mtlsSourceLabel));
118
+ const protocolError = validateMtlsProtocol(auth.protocol);
119
+ if (protocolError) {
120
+ errors.push(protocolError);
121
+ }
122
+ }
123
+
124
+ return errors;
125
+ };
126
+
103
127
  /**
104
128
  * Build an inquirer question for an mTLS file path prompt.
105
129
  * @param {string} name - answer key (e.g. 'tlsClientCert')
@@ -637,6 +661,7 @@ function getConfig(profileName) {
637
661
  const envApiPath = process.env.CONFLUENCE_API_PATH;
638
662
  const envProtocol = process.env.CONFLUENCE_PROTOCOL;
639
663
  const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
664
+ const envForceCloud = process.env.CONFLUENCE_FORCE_CLOUD;
640
665
  const envMtls = normalizeMtlsConfig({
641
666
  caCert: process.env.CONFLUENCE_TLS_CA_CERT,
642
667
  clientCert: process.env.CONFLUENCE_TLS_CLIENT_CERT,
@@ -655,29 +680,19 @@ function getConfig(profileName) {
655
680
  process.exit(1);
656
681
  }
657
682
 
658
- if (authType === 'basic' && !envEmail) {
659
- console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL or CONFLUENCE_USERNAME.'));
660
- console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
661
- process.exit(1);
662
- }
663
-
664
- if (authType !== 'mtls' && !envToken) {
665
- console.error(chalk.red(' Bearer/basic authentication requires CONFLUENCE_API_TOKEN or CONFLUENCE_PASSWORD.'));
666
- process.exit(1);
667
- }
668
-
669
- if (authType === 'mtls') {
670
- const mtlsErrors = validateMtlsConfig(envMtls, 'CONFLUENCE_AUTH_TYPE=mtls');
671
- if (mtlsErrors.length > 0) {
672
- console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
673
- console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
674
- process.exit(1);
683
+ const authErrors = validateAuthConfig(
684
+ { authType, token: envToken, email: envEmail, mtls: envMtls, protocol: envProtocol },
685
+ 'CONFLUENCE_AUTH_TYPE=mtls'
686
+ );
687
+ if (authErrors.length > 0) {
688
+ console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
689
+ if (authType === 'basic' && !envEmail) {
690
+ console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
675
691
  }
676
- if (normalizeProtocol(envProtocol) === 'http') {
677
- const protocolError = validateMtlsProtocol(envProtocol);
678
- console.error(chalk.red(`❌ ${protocolError}`));
679
- process.exit(1);
692
+ if (authType === 'mtls' && !envMtls) {
693
+ console.log(chalk.yellow('Set CONFLUENCE_TLS_CLIENT_CERT and CONFLUENCE_TLS_CLIENT_KEY. Optionally set CONFLUENCE_TLS_CA_CERT.'));
680
694
  }
695
+ process.exit(1);
681
696
  }
682
697
 
683
698
  return {
@@ -688,7 +703,8 @@ function getConfig(profileName) {
688
703
  email: envEmail ? envEmail.trim() : undefined,
689
704
  authType,
690
705
  mtls: envMtls,
691
- readOnly: envReadOnly === 'true'
706
+ readOnly: envReadOnly === 'true',
707
+ forceCloud: envForceCloud === 'true'
692
708
  };
693
709
  }
694
710
 
@@ -727,33 +743,22 @@ function getConfig(profileName) {
727
743
  const mtls = normalizeMtlsConfig(storedConfig.mtls);
728
744
  let apiPath;
729
745
 
730
- if (!trimmedDomain || (authType !== 'mtls' && !trimmedToken)) {
746
+ if (!trimmedDomain) {
731
747
  console.error(chalk.red('❌ Configuration file is missing required values.'));
732
748
  console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
733
749
  process.exit(1);
734
750
  }
735
751
 
736
- if (authType === 'basic' && !trimmedEmail) {
737
- console.error(chalk.red('❌ Basic authentication requires an email address or username.'));
738
- console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email or username.'));
752
+ const authErrors = validateAuthConfig(
753
+ { authType, token: trimmedToken, email: trimmedEmail, mtls, protocol: storedConfig.protocol },
754
+ 'mTLS authentication'
755
+ );
756
+ if (authErrors.length > 0) {
757
+ console.error(chalk.red(`❌ ${authErrors.join(' ')}`));
758
+ console.log(chalk.yellow('Please rerun "confluence init" to refresh your settings.'));
739
759
  process.exit(1);
740
760
  }
741
761
 
742
- if (authType === 'mtls') {
743
- const mtlsErrors = validateMtlsConfig(mtls, 'mTLS authentication');
744
- if (mtlsErrors.length > 0) {
745
- console.error(chalk.red(`❌ ${mtlsErrors.join(' ')}`));
746
- console.log(chalk.yellow('Please rerun "confluence init" to add your mTLS certificate paths.'));
747
- process.exit(1);
748
- }
749
- if (normalizeProtocol(storedConfig.protocol) === 'http') {
750
- const protocolError = validateMtlsProtocol(storedConfig.protocol);
751
- console.error(chalk.red(`❌ ${protocolError}`));
752
- console.log(chalk.yellow('Please rerun "confluence init" to update your protocol to HTTPS.'));
753
- process.exit(1);
754
- }
755
- }
756
-
757
762
  try {
758
763
  apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
759
764
  } catch (error) {
@@ -766,6 +771,10 @@ function getConfig(profileName) {
766
771
  ? envReadOnly === 'true'
767
772
  : Boolean(storedConfig.readOnly);
768
773
 
774
+ const forceCloud = envForceCloud !== undefined
775
+ ? envForceCloud === 'true'
776
+ : Boolean(storedConfig.forceCloud);
777
+
769
778
  return {
770
779
  domain: trimmedDomain,
771
780
  protocol: normalizeProtocol(storedConfig.protocol),
@@ -774,7 +783,8 @@ function getConfig(profileName) {
774
783
  email: trimmedEmail,
775
784
  authType,
776
785
  mtls,
777
- readOnly
786
+ readOnly,
787
+ forceCloud
778
788
  };
779
789
  } catch (error) {
780
790
  console.error(chalk.red('❌ Error reading configuration file:'), error.message);
@@ -35,6 +35,7 @@ class ConfluenceClient {
35
35
  this.token = config.token;
36
36
  this.email = config.email;
37
37
  this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
38
+ this.forceCloud = !!config.forceCloud;
38
39
  this.mtls = config.mtls;
39
40
  this.apiPath = this.sanitizeApiPath(config.apiPath);
40
41
  this.webUrlPrefix = this.apiPath.startsWith('/wiki/') ? '/wiki' : '';
@@ -100,7 +101,7 @@ class ConfluenceClient {
100
101
  }
101
102
 
102
103
  isCloud() {
103
- return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net'));
104
+ return this.isScopedToken() || (this.domain && this.domain.trim().toLowerCase().endsWith('.atlassian.net')) || this.forceCloud;
104
105
  }
105
106
 
106
107
  isScopedToken() {
@@ -477,12 +478,12 @@ class ConfluenceClient {
477
478
  // Replace userkey references with display names in HTML
478
479
  let resolvedHtml = html;
479
480
  userMap.forEach((displayName, userKey) => {
480
- // Replace <ac:link><ri:user ri:userkey="xxx" /></ac:link> with @displayName
481
+ const escapedKey = userKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
481
482
  const userLinkRegex = new RegExp(
482
- `<ac:link>\\s*<ri:user\\s+ri:userkey="${userKey}"\\s*/>\\s*</ac:link>`,
483
+ `<ac:link>\\s*<ri:user\\s+ri:userkey="${escapedKey}"\\s*/>\\s*</ac:link>`,
483
484
  'g'
484
485
  );
485
- resolvedHtml = resolvedHtml.replace(userLinkRegex, `@${displayName}`);
486
+ resolvedHtml = resolvedHtml.replace(userLinkRegex, () => `@${displayName}`);
486
487
  });
487
488
 
488
489
  return { html: resolvedHtml, userMap };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.1",
3
+ "version": "1.30.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "1.30.1",
9
+ "version": "1.30.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.30.1",
3
+ "version": "1.30.2",
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,6 +29,7 @@ confluence --version # verify install
29
29
  | `CONFLUENCE_API_TOKEN` | API token or personal access token | `ATATT3x...` |
30
30
  | `CONFLUENCE_PROFILE` | Named profile to use (optional) | `staging` |
31
31
  | `CONFLUENCE_READ_ONLY` | Block all write operations when `true` | `true` |
32
+ | `CONFLUENCE_FORCE_CLOUD` | Force Cloud link format for custom domains | `true` |
32
33
 
33
34
  **Global `--profile` flag (use a named profile for any command):**
34
35
 
@@ -53,6 +54,7 @@ confluence init \
53
54
 
54
55
  **Cloud vs Server/DC:**
55
56
  - Atlassian Cloud (`*.atlassian.net`): use `--api-path "/wiki/rest/api"`, auth type `basic` with email + API token
57
+ - Atlassian Cloud (custom domain): if your Cloud instance uses a custom domain (e.g., `wiki.example.org`), set `CONFLUENCE_FORCE_CLOUD=true` or add `"forceCloud": true` to your profile in `~/.confluence-cli/config.json`. Without this, links will render incorrectly.
56
58
  - Atlassian Cloud (scoped token): use `--domain "api.atlassian.com"`, `--api-path "/ex/confluence/<your-cloud-id>/wiki/rest/api"`, auth type `basic` with email + scoped token. Get your Cloud ID from `https://<your-site>.atlassian.net/_edge/tenant_info`. Recommended for agents (least privilege).
57
59
  - Self-hosted / Data Center: use `--api-path "/rest/api"`, auth type `bearer` with a personal access token (no email needed)
58
60