confluence-cli 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.8.0](https://github.com/pchuri/confluence-cli/compare/v1.7.0...v1.8.0) (2025-09-28)
2
+
3
+
4
+ ### Features
5
+
6
+ * make Confluence API path configurable ([#14](https://github.com/pchuri/confluence-cli/issues/14)) ([be000e0](https://github.com/pchuri/confluence-cli/commit/be000e0d92881d65329b84bad6555dcad0bbb455)), closes [#13](https://github.com/pchuri/confluence-cli/issues/13)
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Make the Confluence REST base path configurable to support both `/rest/api` and `/wiki/rest/api`.
12
+
13
+ # [1.7.0](https://github.com/pchuri/confluence-cli/compare/v1.6.0...v1.7.0) (2025-09-28)
14
+
15
+
16
+ ### Features
17
+
18
+ * support basic auth for Atlassian API tokens ([#12](https://github.com/pchuri/confluence-cli/issues/12)) ([e80ea9b](https://github.com/pchuri/confluence-cli/commit/e80ea9b7913d5f497b60bf72149737b6f704c6b8))
19
+
1
20
  # [1.6.0](https://github.com/pchuri/confluence-cli/compare/v1.5.0...v1.6.0) (2025-09-05)
2
21
 
3
22
 
package/README.md CHANGED
@@ -58,12 +58,20 @@ npx confluence-cli
58
58
  confluence init
59
59
  ```
60
60
 
61
+ The wizard now helps you choose the right API endpoint and authentication method. It recommends `/wiki/rest/api` for Atlassian Cloud domains (e.g., `*.atlassian.net`) and `/rest/api` for self-hosted/Data Center instances, then prompts for Basic (email + token) or Bearer authentication.
62
+
61
63
  ### Option 2: Environment Variables
62
64
  ```bash
63
65
  export CONFLUENCE_DOMAIN="your-domain.atlassian.net"
64
66
  export CONFLUENCE_API_TOKEN="your-api-token"
67
+ export CONFLUENCE_EMAIL="your.email@example.com" # required when using Atlassian Cloud
68
+ export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/api for Server/DC
69
+ # Optional: set to 'bearer' for self-hosted/Data Center instances
70
+ export CONFLUENCE_AUTH_TYPE="basic"
65
71
  ```
66
72
 
73
+ `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.
74
+
67
75
  ### Getting Your API Token
68
76
 
69
77
  1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens)
package/lib/config.js CHANGED
@@ -7,9 +7,54 @@ const chalk = require('chalk');
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.confluence-cli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
9
 
10
- /**
11
- * Initialize configuration
12
- */
10
+ const AUTH_CHOICES = [
11
+ { name: 'Basic (email + API token)', value: 'basic' },
12
+ { name: 'Bearer token', value: 'bearer' }
13
+ ];
14
+
15
+ const requiredInput = (label) => (input) => {
16
+ if (!input || !input.trim()) {
17
+ return `${label} is required`;
18
+ }
19
+ return true;
20
+ };
21
+
22
+ const normalizeAuthType = (rawValue, hasEmail) => {
23
+ const normalized = (rawValue || '').trim().toLowerCase();
24
+ if (normalized === 'basic' || normalized === 'bearer') {
25
+ return normalized;
26
+ }
27
+ return hasEmail ? 'basic' : 'bearer';
28
+ };
29
+
30
+ const inferApiPath = (domain) => {
31
+ if (!domain) {
32
+ return '/rest/api';
33
+ }
34
+
35
+ const normalizedDomain = domain.trim().toLowerCase();
36
+ if (normalizedDomain.endsWith('.atlassian.net')) {
37
+ return '/wiki/rest/api';
38
+ }
39
+
40
+ return '/rest/api';
41
+ };
42
+
43
+ const normalizeApiPath = (rawValue, domain) => {
44
+ const trimmed = (rawValue || '').trim();
45
+
46
+ if (!trimmed) {
47
+ return inferApiPath(domain);
48
+ }
49
+
50
+ if (!trimmed.startsWith('/')) {
51
+ throw new Error('Confluence API path must start with "/".');
52
+ }
53
+
54
+ const withoutTrailing = trimmed.replace(/\/+$/, '');
55
+ return withoutTrailing || inferApiPath(domain);
56
+ };
57
+
13
58
  async function initConfig() {
14
59
  console.log(chalk.blue('🚀 Confluence CLI Configuration'));
15
60
  console.log('Please provide your Confluence connection details:\n');
@@ -19,70 +64,145 @@ async function initConfig() {
19
64
  type: 'input',
20
65
  name: 'domain',
21
66
  message: 'Confluence domain (e.g., yourcompany.atlassian.net):',
22
- validate: (input) => {
23
- if (!input.trim()) {
24
- return 'Domain is required';
67
+ validate: requiredInput('Domain')
68
+ },
69
+ {
70
+ type: 'input',
71
+ name: 'apiPath',
72
+ message: 'REST API path (Cloud: /wiki/rest/api, Server: /rest/api):',
73
+ default: (responses) => inferApiPath(responses.domain),
74
+ validate: (input, responses) => {
75
+ const value = (input || '').trim();
76
+ if (!value) {
77
+ return true;
78
+ }
79
+ if (!value.startsWith('/')) {
80
+ return 'API path must start with "/"';
81
+ }
82
+ try {
83
+ normalizeApiPath(value, responses.domain);
84
+ return true;
85
+ } catch (error) {
86
+ return error.message;
25
87
  }
26
- return true;
27
88
  }
28
89
  },
90
+ {
91
+ type: 'list',
92
+ name: 'authType',
93
+ message: 'Authentication method:',
94
+ choices: AUTH_CHOICES,
95
+ default: 'basic'
96
+ },
97
+ {
98
+ type: 'input',
99
+ name: 'email',
100
+ message: 'Confluence email (used with API token):',
101
+ when: (responses) => responses.authType === 'basic',
102
+ validate: requiredInput('Email')
103
+ },
29
104
  {
30
105
  type: 'password',
31
106
  name: 'token',
32
107
  message: 'API Token:',
33
- validate: (input) => {
34
- if (!input.trim()) {
35
- return 'API Token is required';
36
- }
37
- return true;
38
- }
108
+ validate: requiredInput('API Token')
39
109
  }
40
110
  ]);
41
111
 
42
- // Create config directory if it doesn't exist
43
112
  if (!fs.existsSync(CONFIG_DIR)) {
44
113
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
45
114
  }
46
115
 
47
- // Save configuration
48
116
  const config = {
49
117
  domain: answers.domain.trim(),
50
- token: answers.token.trim()
118
+ apiPath: normalizeApiPath(answers.apiPath, answers.domain),
119
+ token: answers.token.trim(),
120
+ authType: answers.authType,
121
+ email: answers.authType === 'basic' ? answers.email.trim() : undefined
51
122
  };
52
123
 
53
124
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
54
-
125
+
55
126
  console.log(chalk.green('✅ Configuration saved successfully!'));
56
127
  console.log(`Config file location: ${chalk.gray(CONFIG_FILE)}`);
57
128
  console.log(chalk.yellow('\n💡 Tip: You can regenerate this config anytime by running "confluence init"'));
58
129
  }
59
130
 
60
- /**
61
- * Get configuration
62
- */
63
131
  function getConfig() {
64
- // First check for environment variables
65
132
  const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
66
133
  const envToken = process.env.CONFLUENCE_API_TOKEN;
134
+ const envEmail = process.env.CONFLUENCE_EMAIL;
135
+ const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
136
+ const envApiPath = process.env.CONFLUENCE_API_PATH;
67
137
 
68
138
  if (envDomain && envToken) {
139
+ const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
140
+ let apiPath;
141
+
142
+ try {
143
+ apiPath = normalizeApiPath(envApiPath, envDomain);
144
+ } catch (error) {
145
+ console.error(chalk.red(`❌ ${error.message}`));
146
+ process.exit(1);
147
+ }
148
+
149
+ if (authType === 'basic' && !envEmail) {
150
+ console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL.'));
151
+ console.log(chalk.yellow('Set CONFLUENCE_EMAIL or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
152
+ process.exit(1);
153
+ }
154
+
69
155
  return {
70
- domain: envDomain,
71
- token: envToken
156
+ domain: envDomain.trim(),
157
+ apiPath,
158
+ token: envToken.trim(),
159
+ email: envEmail ? envEmail.trim() : undefined,
160
+ authType
72
161
  };
73
162
  }
74
163
 
75
- // Check for config file
76
164
  if (!fs.existsSync(CONFIG_FILE)) {
77
165
  console.error(chalk.red('❌ No configuration found!'));
78
166
  console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
79
- console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN and CONFLUENCE_API_TOKEN'));
167
+ console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, CONFLUENCE_EMAIL, and optionally CONFLUENCE_API_PATH.'));
80
168
  process.exit(1);
81
169
  }
82
170
 
83
171
  try {
84
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
85
- return config;
172
+ const storedConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
173
+ const trimmedDomain = (storedConfig.domain || '').trim();
174
+ const trimmedToken = (storedConfig.token || '').trim();
175
+ const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
176
+ const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
177
+ let apiPath;
178
+
179
+ if (!trimmedDomain || !trimmedToken) {
180
+ console.error(chalk.red('❌ Configuration file is missing required values.'));
181
+ console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
182
+ process.exit(1);
183
+ }
184
+
185
+ if (authType === 'basic' && !trimmedEmail) {
186
+ console.error(chalk.red('❌ Basic authentication requires an email address.'));
187
+ console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email.'));
188
+ process.exit(1);
189
+ }
190
+
191
+ try {
192
+ apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
193
+ } catch (error) {
194
+ console.error(chalk.red(`❌ ${error.message}`));
195
+ console.log(chalk.yellow('Please rerun "confluence init" to update your API path.'));
196
+ process.exit(1);
197
+ }
198
+
199
+ return {
200
+ domain: trimmedDomain,
201
+ apiPath,
202
+ token: trimmedToken,
203
+ email: trimmedEmail,
204
+ authType
205
+ };
86
206
  } catch (error) {
87
207
  console.error(chalk.red('❌ Error reading configuration file:'), error.message);
88
208
  console.log(chalk.yellow('Please run "confluence init" to recreate your configuration.'));
@@ -4,21 +4,48 @@ const MarkdownIt = require('markdown-it');
4
4
 
5
5
  class ConfluenceClient {
6
6
  constructor(config) {
7
- this.baseURL = `https://${config.domain}/rest/api`;
8
- this.token = config.token;
9
7
  this.domain = config.domain;
8
+ this.token = config.token;
9
+ this.email = config.email;
10
+ this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase();
11
+ this.apiPath = this.sanitizeApiPath(config.apiPath);
12
+ this.baseURL = `https://${this.domain}${this.apiPath}`;
10
13
  this.markdown = new MarkdownIt();
11
14
  this.setupConfluenceMarkdownExtensions();
12
-
15
+
16
+ const headers = {
17
+ 'Content-Type': 'application/json',
18
+ 'Authorization': this.authType === 'basic' ? this.buildBasicAuthHeader() : `Bearer ${this.token}`
19
+ };
20
+
13
21
  this.client = axios.create({
14
22
  baseURL: this.baseURL,
15
- headers: {
16
- 'Authorization': `Bearer ${this.token}`,
17
- 'Content-Type': 'application/json'
18
- }
23
+ headers
19
24
  });
20
25
  }
21
26
 
27
+ sanitizeApiPath(rawPath) {
28
+ const fallback = '/rest/api';
29
+ const value = (rawPath || '').trim();
30
+
31
+ if (!value) {
32
+ return fallback;
33
+ }
34
+
35
+ const withoutLeading = value.replace(/^\/+/, '');
36
+ const normalized = `/${withoutLeading}`.replace(/\/+$/, '');
37
+ return normalized || fallback;
38
+ }
39
+
40
+ buildBasicAuthHeader() {
41
+ if (!this.email) {
42
+ throw new Error('Basic authentication requires an email address.');
43
+ }
44
+
45
+ const encodedCredentials = Buffer.from(`${this.email}:${this.token}`).toString('base64');
46
+ return `Basic ${encodedCredentials}`;
47
+ }
48
+
22
49
  /**
23
50
  * Extract page ID from URL or return the ID if it's already a number
24
51
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "1.6.0",
3
+ "version": "1.8.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": {
@@ -22,7 +22,7 @@
22
22
  "author": "pchuri",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "axios": "^1.6.2",
25
+ "axios": "^1.12.0",
26
26
  "chalk": "^4.1.2",
27
27
  "commander": "^11.1.0",
28
28
  "html-to-text": "^9.0.5",
@@ -10,6 +10,58 @@ describe('ConfluenceClient', () => {
10
10
  });
11
11
  });
12
12
 
13
+ describe('api path handling', () => {
14
+ test('defaults to /rest/api when path is not provided', () => {
15
+ const defaultClient = new ConfluenceClient({
16
+ domain: 'example.com',
17
+ token: 'no-path-token'
18
+ });
19
+
20
+ expect(defaultClient.baseURL).toBe('https://example.com/rest/api');
21
+ });
22
+
23
+ test('normalizes custom api paths', () => {
24
+ const customClient = new ConfluenceClient({
25
+ domain: 'cloud.example',
26
+ token: 'custom-path',
27
+ apiPath: 'wiki/rest/api/'
28
+ });
29
+
30
+ expect(customClient.baseURL).toBe('https://cloud.example/wiki/rest/api');
31
+ });
32
+ });
33
+
34
+ describe('authentication setup', () => {
35
+ test('uses bearer token headers by default', () => {
36
+ const bearerClient = new ConfluenceClient({
37
+ domain: 'test.atlassian.net',
38
+ token: 'bearer-token'
39
+ });
40
+
41
+ expect(bearerClient.client.defaults.headers.Authorization).toBe('Bearer bearer-token');
42
+ });
43
+
44
+ test('builds basic auth headers when email is provided', () => {
45
+ const basicClient = new ConfluenceClient({
46
+ domain: 'test.atlassian.net',
47
+ token: 'basic-token',
48
+ authType: 'basic',
49
+ email: 'user@example.com'
50
+ });
51
+
52
+ const encoded = Buffer.from('user@example.com:basic-token').toString('base64');
53
+ expect(basicClient.client.defaults.headers.Authorization).toBe(`Basic ${encoded}`);
54
+ });
55
+
56
+ test('throws when basic auth is missing an email', () => {
57
+ expect(() => new ConfluenceClient({
58
+ domain: 'test.atlassian.net',
59
+ token: 'missing-email',
60
+ authType: 'basic'
61
+ })).toThrow('Basic authentication requires an email address.');
62
+ });
63
+ });
64
+
13
65
  describe('extractPageId', () => {
14
66
  test('should return numeric page ID as is', () => {
15
67
  expect(client.extractPageId('123456789')).toBe('123456789');