@bestend/confluence-cli 1.15.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/lib/config.js ADDED
@@ -0,0 +1,437 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const inquirer = require('inquirer');
5
+ const chalk = require('chalk');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.confluence-cli');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+
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
+
58
+ // Helper function to validate CLI-provided options
59
+ const validateCliOptions = (options) => {
60
+ const errors = [];
61
+
62
+ if (options.domain && !options.domain.trim()) {
63
+ errors.push('--domain cannot be empty');
64
+ }
65
+
66
+ if (options.token && !options.token.trim()) {
67
+ errors.push('--token cannot be empty');
68
+ }
69
+
70
+ if (options.email && !options.email.trim()) {
71
+ errors.push('--email cannot be empty');
72
+ }
73
+
74
+ if (options.apiPath) {
75
+ if (!options.apiPath.startsWith('/')) {
76
+ errors.push('--api-path must start with "/"');
77
+ } else {
78
+ // Validate API path format
79
+ try {
80
+ normalizeApiPath(options.apiPath, options.domain || 'example.com');
81
+ } catch (error) {
82
+ errors.push(`--api-path is invalid: ${error.message}`);
83
+ }
84
+ }
85
+ }
86
+
87
+ if (options.authType && !['basic', 'bearer'].includes(options.authType.toLowerCase())) {
88
+ errors.push('--auth-type must be "basic" or "bearer"');
89
+ }
90
+
91
+ // Check if basic auth is provided with email
92
+ const normAuthType = options.authType ? normalizeAuthType(options.authType, Boolean(options.email)) : null;
93
+ if (normAuthType === 'basic' && !options.email) {
94
+ errors.push('--email is required when using basic authentication');
95
+ }
96
+
97
+ return errors;
98
+ };
99
+
100
+ // Helper function to save configuration with validation
101
+ const saveConfig = (configData) => {
102
+ if (!fs.existsSync(CONFIG_DIR)) {
103
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
104
+ }
105
+
106
+ const config = {
107
+ domain: configData.domain.trim(),
108
+ apiPath: normalizeApiPath(configData.apiPath, configData.domain),
109
+ token: configData.token.trim(),
110
+ authType: configData.authType,
111
+ email: configData.authType === 'basic' && configData.email ? configData.email.trim() : undefined
112
+ };
113
+
114
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
115
+
116
+ console.log(chalk.green('✅ Configuration saved successfully!'));
117
+ console.log(`Config file location: ${chalk.gray(CONFIG_FILE)}`);
118
+ console.log(chalk.yellow('\n💡 Tip: You can regenerate this config anytime by running "confluence init"'));
119
+ };
120
+
121
+ // Helper function to prompt for missing values
122
+ const promptForMissingValues = async (providedValues) => {
123
+ const questions = [];
124
+
125
+ // Domain question
126
+ if (!providedValues.domain) {
127
+ questions.push({
128
+ type: 'input',
129
+ name: 'domain',
130
+ message: 'Confluence domain (e.g., yourcompany.atlassian.net):',
131
+ validate: requiredInput('Domain')
132
+ });
133
+ }
134
+
135
+ // API Path question
136
+ if (!providedValues.apiPath) {
137
+ questions.push({
138
+ type: 'input',
139
+ name: 'apiPath',
140
+ message: 'REST API path (Cloud: /wiki/rest/api, Server: /rest/api):',
141
+ default: (responses) => inferApiPath(providedValues.domain || responses.domain),
142
+ validate: (input, responses) => {
143
+ const value = (input || '').trim();
144
+ if (!value) {
145
+ return true;
146
+ }
147
+ if (!value.startsWith('/')) {
148
+ return 'API path must start with "/"';
149
+ }
150
+ try {
151
+ const domain = providedValues.domain || responses.domain;
152
+ normalizeApiPath(value, domain);
153
+ return true;
154
+ } catch (error) {
155
+ return error.message;
156
+ }
157
+ }
158
+ });
159
+ }
160
+
161
+ // Auth Type question
162
+ const hasEmail = Boolean(providedValues.email);
163
+ if (!providedValues.authType) {
164
+ questions.push({
165
+ type: 'list',
166
+ name: 'authType',
167
+ message: 'Authentication method:',
168
+ choices: AUTH_CHOICES,
169
+ default: hasEmail ? 'basic' : 'bearer'
170
+ });
171
+ }
172
+
173
+ // Email question (conditional on authType)
174
+ if (!providedValues.email) {
175
+ questions.push({
176
+ type: 'input',
177
+ name: 'email',
178
+ message: 'Confluence email (used with API token):',
179
+ when: (responses) => {
180
+ const authType = providedValues.authType || responses.authType;
181
+ return authType === 'basic';
182
+ },
183
+ validate: requiredInput('Email')
184
+ });
185
+ }
186
+
187
+ // Token question
188
+ if (!providedValues.token) {
189
+ questions.push({
190
+ type: 'password',
191
+ name: 'token',
192
+ message: 'API Token:',
193
+ validate: requiredInput('API Token')
194
+ });
195
+ }
196
+
197
+ if (questions.length === 0) {
198
+ return providedValues;
199
+ }
200
+
201
+ const answers = await inquirer.prompt(questions);
202
+ return { ...providedValues, ...answers };
203
+ };
204
+
205
+ async function initConfig(cliOptions = {}) {
206
+ // Extract provided values from CLI options
207
+ const providedValues = {
208
+ domain: cliOptions.domain,
209
+ apiPath: cliOptions.apiPath,
210
+ authType: cliOptions.authType,
211
+ email: cliOptions.email,
212
+ token: cliOptions.token
213
+ };
214
+
215
+ // Check if any CLI options were provided
216
+ const hasCliOptions = Object.values(providedValues).some(v => v);
217
+
218
+ if (!hasCliOptions) {
219
+ // Interactive mode: no CLI options provided
220
+ console.log(chalk.blue('🚀 Confluence CLI Configuration'));
221
+ console.log('Please provide your Confluence connection details:\n');
222
+
223
+ const answers = await inquirer.prompt([
224
+ {
225
+ type: 'input',
226
+ name: 'domain',
227
+ message: 'Confluence domain (e.g., yourcompany.atlassian.net):',
228
+ validate: requiredInput('Domain')
229
+ },
230
+ {
231
+ type: 'input',
232
+ name: 'apiPath',
233
+ message: 'REST API path (Cloud: /wiki/rest/api, Server: /rest/api):',
234
+ default: (responses) => inferApiPath(responses.domain),
235
+ validate: (input, responses) => {
236
+ const value = (input || '').trim();
237
+ if (!value) {
238
+ return true;
239
+ }
240
+ if (!value.startsWith('/')) {
241
+ return 'API path must start with "/"';
242
+ }
243
+ try {
244
+ normalizeApiPath(value, responses.domain);
245
+ return true;
246
+ } catch (error) {
247
+ return error.message;
248
+ }
249
+ }
250
+ },
251
+ {
252
+ type: 'list',
253
+ name: 'authType',
254
+ message: 'Authentication method:',
255
+ choices: AUTH_CHOICES,
256
+ default: 'basic'
257
+ },
258
+ {
259
+ type: 'input',
260
+ name: 'email',
261
+ message: 'Confluence email (used with API token):',
262
+ when: (responses) => responses.authType === 'basic',
263
+ validate: requiredInput('Email')
264
+ },
265
+ {
266
+ type: 'password',
267
+ name: 'token',
268
+ message: 'API Token:',
269
+ validate: requiredInput('API Token')
270
+ }
271
+ ]);
272
+
273
+ saveConfig(answers);
274
+ return;
275
+ }
276
+
277
+ // Non-interactive or hybrid mode: CLI options provided
278
+ // Validate provided options
279
+ const validationErrors = validateCliOptions(providedValues);
280
+ if (validationErrors.length > 0) {
281
+ console.error(chalk.red('❌ Configuration Error:'));
282
+ validationErrors.forEach(error => {
283
+ console.error(chalk.red(` • ${error}`));
284
+ });
285
+ process.exit(1);
286
+ }
287
+
288
+ // Check if all required values are provided for non-interactive mode
289
+ // Non-interactive requires: domain, token, and either authType or email (for inference)
290
+ const hasRequiredValues = Boolean(
291
+ providedValues.domain &&
292
+ providedValues.token &&
293
+ (providedValues.authType || providedValues.email)
294
+ );
295
+
296
+ if (hasRequiredValues) {
297
+ // Non-interactive mode: all required values provided
298
+ try {
299
+ // Infer authType if not provided
300
+ let inferredAuthType = providedValues.authType;
301
+ if (!inferredAuthType) {
302
+ inferredAuthType = providedValues.email ? 'basic' : 'bearer';
303
+ }
304
+
305
+ const normalizedAuthType = normalizeAuthType(inferredAuthType, Boolean(providedValues.email));
306
+ const normalizedDomain = providedValues.domain.trim();
307
+
308
+ // Verify basic auth has email
309
+ if (normalizedAuthType === 'basic' && !providedValues.email) {
310
+ console.error(chalk.red('❌ Email is required for basic authentication'));
311
+ process.exit(1);
312
+ }
313
+
314
+ // Verify API path format if provided
315
+ if (providedValues.apiPath) {
316
+ normalizeApiPath(providedValues.apiPath, normalizedDomain);
317
+ }
318
+
319
+ const configData = {
320
+ domain: normalizedDomain,
321
+ apiPath: providedValues.apiPath || inferApiPath(normalizedDomain),
322
+ token: providedValues.token,
323
+ authType: normalizedAuthType,
324
+ email: providedValues.email
325
+ };
326
+
327
+ saveConfig(configData);
328
+ } catch (error) {
329
+ console.error(chalk.red(`❌ ${error.message}`));
330
+ process.exit(1);
331
+ }
332
+ return;
333
+ }
334
+
335
+ // Hybrid mode: some values provided, prompt for the rest
336
+ try {
337
+ console.log(chalk.blue('🚀 Confluence CLI Configuration'));
338
+ console.log('Completing configuration with interactive prompts:\n');
339
+
340
+ const mergedValues = await promptForMissingValues(providedValues);
341
+
342
+ // Normalize auth type
343
+ mergedValues.authType = normalizeAuthType(mergedValues.authType, Boolean(mergedValues.email));
344
+
345
+ saveConfig(mergedValues);
346
+ } catch (error) {
347
+ console.error(chalk.red(`❌ ${error.message}`));
348
+ process.exit(1);
349
+ }
350
+ }
351
+
352
+ function getConfig() {
353
+ const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
354
+ const envToken = process.env.CONFLUENCE_API_TOKEN;
355
+ const envEmail = process.env.CONFLUENCE_EMAIL;
356
+ const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
357
+ const envApiPath = process.env.CONFLUENCE_API_PATH;
358
+
359
+ if (envDomain && envToken) {
360
+ const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
361
+ let apiPath;
362
+
363
+ try {
364
+ apiPath = normalizeApiPath(envApiPath, envDomain);
365
+ } catch (error) {
366
+ console.error(chalk.red(`❌ ${error.message}`));
367
+ process.exit(1);
368
+ }
369
+
370
+ if (authType === 'basic' && !envEmail) {
371
+ console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL.'));
372
+ console.log(chalk.yellow('Set CONFLUENCE_EMAIL or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
373
+ process.exit(1);
374
+ }
375
+
376
+ return {
377
+ domain: envDomain.trim(),
378
+ apiPath,
379
+ token: envToken.trim(),
380
+ email: envEmail ? envEmail.trim() : undefined,
381
+ authType
382
+ };
383
+ }
384
+
385
+ if (!fs.existsSync(CONFIG_FILE)) {
386
+ console.error(chalk.red('❌ No configuration found!'));
387
+ console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
388
+ console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN, CONFLUENCE_EMAIL, and optionally CONFLUENCE_API_PATH.'));
389
+ process.exit(1);
390
+ }
391
+
392
+ try {
393
+ const storedConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
394
+ const trimmedDomain = (storedConfig.domain || '').trim();
395
+ const trimmedToken = (storedConfig.token || '').trim();
396
+ const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
397
+ const authType = normalizeAuthType(storedConfig.authType, Boolean(trimmedEmail));
398
+ let apiPath;
399
+
400
+ if (!trimmedDomain || !trimmedToken) {
401
+ console.error(chalk.red('❌ Configuration file is missing required values.'));
402
+ console.log(chalk.yellow('Run "confluence init" to refresh your settings.'));
403
+ process.exit(1);
404
+ }
405
+
406
+ if (authType === 'basic' && !trimmedEmail) {
407
+ console.error(chalk.red('❌ Basic authentication requires an email address.'));
408
+ console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email.'));
409
+ process.exit(1);
410
+ }
411
+
412
+ try {
413
+ apiPath = normalizeApiPath(storedConfig.apiPath, trimmedDomain);
414
+ } catch (error) {
415
+ console.error(chalk.red(`❌ ${error.message}`));
416
+ console.log(chalk.yellow('Please rerun "confluence init" to update your API path.'));
417
+ process.exit(1);
418
+ }
419
+
420
+ return {
421
+ domain: trimmedDomain,
422
+ apiPath,
423
+ token: trimmedToken,
424
+ email: trimmedEmail,
425
+ authType
426
+ };
427
+ } catch (error) {
428
+ console.error(chalk.red('❌ Error reading configuration file:'), error.message);
429
+ console.log(chalk.yellow('Please run "confluence init" to recreate your configuration.'));
430
+ process.exit(1);
431
+ }
432
+ }
433
+
434
+ module.exports = {
435
+ initConfig,
436
+ getConfig
437
+ };