@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/bin/index.js CHANGED
@@ -21,4 +21,9 @@ if (!nodeVersion.startsWith('v') ||
21
21
  }
22
22
 
23
23
  // Load the main CLI application
24
- require('./confluence.js');
24
+ const { program } = require('./confluence.js');
25
+
26
+ if (process.argv.length <= 2) {
27
+ program.help({ error: false });
28
+ }
29
+ program.parse(process.argv);
package/lib/config.js CHANGED
@@ -6,12 +6,15 @@ const chalk = require('chalk');
6
6
 
7
7
  const CONFIG_DIR = path.join(os.homedir(), '.confluence-cli');
8
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ const DEFAULT_PROFILE = 'default';
9
10
 
10
11
  const AUTH_CHOICES = [
11
- { name: 'Basic (email + API token)', value: 'basic' },
12
+ { name: 'Basic (credentials)', value: 'basic' },
12
13
  { name: 'Bearer token', value: 'bearer' }
13
14
  ];
14
15
 
16
+ const isValidProfileName = (name) => /^[a-zA-Z0-9_-]+$/.test(name);
17
+
15
18
  const requiredInput = (label) => (input) => {
16
19
  if (!input || !input.trim()) {
17
20
  return `${label} is required`;
@@ -19,6 +22,19 @@ const requiredInput = (label) => (input) => {
19
22
  return true;
20
23
  };
21
24
 
25
+ const PROTOCOL_CHOICES = [
26
+ { name: 'HTTPS (recommended)', value: 'https' },
27
+ { name: 'HTTP', value: 'http' }
28
+ ];
29
+
30
+ const normalizeProtocol = (rawValue) => {
31
+ const normalized = (rawValue || '').trim().toLowerCase();
32
+ if (normalized === 'http' || normalized === 'https') {
33
+ return normalized;
34
+ }
35
+ return 'https';
36
+ };
37
+
22
38
  const normalizeAuthType = (rawValue, hasEmail) => {
23
39
  const normalized = (rawValue || '').trim().toLowerCase();
24
40
  if (normalized === 'basic' || normalized === 'bearer') {
@@ -55,6 +71,50 @@ const normalizeApiPath = (rawValue, domain) => {
55
71
  return withoutTrailing || inferApiPath(domain);
56
72
  };
57
73
 
74
+ // Read config file with backward compatibility for old flat format
75
+ function readConfigFile() {
76
+ if (!fs.existsSync(CONFIG_FILE)) {
77
+ return null;
78
+ }
79
+
80
+ try {
81
+ const raw = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
82
+
83
+ // Detect old flat format (has domain at top level, no profiles key)
84
+ if (raw.domain && !raw.profiles) {
85
+ const profile = {
86
+ domain: raw.domain,
87
+ protocol: raw.protocol,
88
+ apiPath: raw.apiPath,
89
+ token: raw.token,
90
+ authType: raw.authType
91
+ };
92
+ if (raw.email) {
93
+ profile.email = raw.email;
94
+ }
95
+ return {
96
+ activeProfile: DEFAULT_PROFILE,
97
+ profiles: { [DEFAULT_PROFILE]: profile }
98
+ };
99
+ }
100
+
101
+ return raw;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // Write the full multi-profile config structure
108
+ function saveConfigFile(data) {
109
+ if (!fs.existsSync(CONFIG_DIR)) {
110
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
111
+ } else {
112
+ fs.chmodSync(CONFIG_DIR, 0o700);
113
+ }
114
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
115
+ fs.chmodSync(CONFIG_FILE, 0o600);
116
+ }
117
+
58
118
  // Helper function to validate CLI-provided options
59
119
  const validateCliOptions = (options) => {
60
120
  const errors = [];
@@ -84,6 +144,10 @@ const validateCliOptions = (options) => {
84
144
  }
85
145
  }
86
146
 
147
+ if (options.protocol && !['http', 'https'].includes(options.protocol.toLowerCase())) {
148
+ errors.push('--protocol must be "http" or "https"');
149
+ }
150
+
87
151
  if (options.authType && !['basic', 'bearer'].includes(options.authType.toLowerCase())) {
88
152
  errors.push('--auth-type must be "basic" or "bearer"');
89
153
  }
@@ -91,29 +155,47 @@ const validateCliOptions = (options) => {
91
155
  // Check if basic auth is provided with email
92
156
  const normAuthType = options.authType ? normalizeAuthType(options.authType, Boolean(options.email)) : null;
93
157
  if (normAuthType === 'basic' && !options.email) {
94
- errors.push('--email is required when using basic authentication');
158
+ errors.push('--email is required when using basic authentication (use your username for on-premise)');
95
159
  }
96
160
 
97
161
  return errors;
98
162
  };
99
163
 
100
164
  // 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
-
165
+ const saveConfig = (configData, profileName) => {
106
166
  const config = {
107
167
  domain: configData.domain.trim(),
168
+ protocol: normalizeProtocol(configData.protocol),
108
169
  apiPath: normalizeApiPath(configData.apiPath, configData.domain),
109
170
  token: configData.token.trim(),
110
- authType: configData.authType,
111
- email: configData.authType === 'basic' && configData.email ? configData.email.trim() : undefined
171
+ authType: configData.authType
112
172
  };
113
173
 
114
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
174
+ if (configData.authType === 'basic' && configData.email) {
175
+ config.email = configData.email.trim();
176
+ }
177
+
178
+ if (configData.readOnly) {
179
+ config.readOnly = true;
180
+ }
181
+
182
+ // Read existing config file (or create new structure)
183
+ const fileData = readConfigFile() || { activeProfile: DEFAULT_PROFILE, profiles: {} };
184
+
185
+ const targetProfile = profileName || fileData.activeProfile || DEFAULT_PROFILE;
186
+ fileData.profiles[targetProfile] = config;
187
+
188
+ // If this is the first profile, make it active
189
+ if (!fileData.activeProfile || !fileData.profiles[fileData.activeProfile]) {
190
+ fileData.activeProfile = targetProfile;
191
+ }
192
+
193
+ saveConfigFile(fileData);
115
194
 
116
195
  console.log(chalk.green('✅ Configuration saved successfully!'));
196
+ if (profileName) {
197
+ console.log(`Profile: ${chalk.cyan(targetProfile)}`);
198
+ }
117
199
  console.log(`Config file location: ${chalk.gray(CONFIG_FILE)}`);
118
200
  console.log(chalk.yellow('\n💡 Tip: You can regenerate this config anytime by running "confluence init"'));
119
201
  };
@@ -122,6 +204,17 @@ const saveConfig = (configData) => {
122
204
  const promptForMissingValues = async (providedValues) => {
123
205
  const questions = [];
124
206
 
207
+ // Protocol question
208
+ if (!providedValues.protocol) {
209
+ questions.push({
210
+ type: 'list',
211
+ name: 'protocol',
212
+ message: 'Protocol:',
213
+ choices: PROTOCOL_CHOICES,
214
+ default: 'https'
215
+ });
216
+ }
217
+
125
218
  // Domain question
126
219
  if (!providedValues.domain) {
127
220
  questions.push({
@@ -175,12 +268,12 @@ const promptForMissingValues = async (providedValues) => {
175
268
  questions.push({
176
269
  type: 'input',
177
270
  name: 'email',
178
- message: 'Confluence email (used with API token):',
271
+ message: 'Email / username:',
179
272
  when: (responses) => {
180
273
  const authType = providedValues.authType || responses.authType;
181
274
  return authType === 'basic';
182
275
  },
183
- validate: requiredInput('Email')
276
+ validate: requiredInput('Email / username')
184
277
  });
185
278
  }
186
279
 
@@ -189,8 +282,8 @@ const promptForMissingValues = async (providedValues) => {
189
282
  questions.push({
190
283
  type: 'password',
191
284
  name: 'token',
192
- message: 'API Token:',
193
- validate: requiredInput('API Token')
285
+ message: 'API token / password:',
286
+ validate: requiredInput('API token / password')
194
287
  });
195
288
  }
196
289
 
@@ -203,8 +296,19 @@ const promptForMissingValues = async (providedValues) => {
203
296
  };
204
297
 
205
298
  async function initConfig(cliOptions = {}) {
299
+ const profileName = cliOptions.profile;
300
+
301
+ // Validate profile name if provided
302
+ if (profileName && !isValidProfileName(profileName)) {
303
+ console.error(chalk.red('❌ Invalid profile name. Use only letters, numbers, hyphens, and underscores.'));
304
+ process.exit(1);
305
+ }
306
+
307
+ const readOnly = cliOptions.readOnly || false;
308
+
206
309
  // Extract provided values from CLI options
207
310
  const providedValues = {
311
+ protocol: cliOptions.protocol,
208
312
  domain: cliOptions.domain,
209
313
  apiPath: cliOptions.apiPath,
210
314
  authType: cliOptions.authType,
@@ -218,9 +322,19 @@ async function initConfig(cliOptions = {}) {
218
322
  if (!hasCliOptions) {
219
323
  // Interactive mode: no CLI options provided
220
324
  console.log(chalk.blue('🚀 Confluence CLI Configuration'));
325
+ if (profileName) {
326
+ console.log(`Profile: ${chalk.cyan(profileName)}`);
327
+ }
221
328
  console.log('Please provide your Confluence connection details:\n');
222
329
 
223
330
  const answers = await inquirer.prompt([
331
+ {
332
+ type: 'list',
333
+ name: 'protocol',
334
+ message: 'Protocol:',
335
+ choices: PROTOCOL_CHOICES,
336
+ default: 'https'
337
+ },
224
338
  {
225
339
  type: 'input',
226
340
  name: 'domain',
@@ -258,19 +372,19 @@ async function initConfig(cliOptions = {}) {
258
372
  {
259
373
  type: 'input',
260
374
  name: 'email',
261
- message: 'Confluence email (used with API token):',
375
+ message: 'Email / username:',
262
376
  when: (responses) => responses.authType === 'basic',
263
- validate: requiredInput('Email')
377
+ validate: requiredInput('Email / username')
264
378
  },
265
379
  {
266
380
  type: 'password',
267
381
  name: 'token',
268
- message: 'API Token:',
269
- validate: requiredInput('API Token')
382
+ message: 'API token / password:',
383
+ validate: requiredInput('API token / password')
270
384
  }
271
385
  ]);
272
386
 
273
- saveConfig(answers);
387
+ saveConfig({ ...answers, readOnly }, profileName);
274
388
  return;
275
389
  }
276
390
 
@@ -288,8 +402,8 @@ async function initConfig(cliOptions = {}) {
288
402
  // Check if all required values are provided for non-interactive mode
289
403
  // Non-interactive requires: domain, token, and either authType or email (for inference)
290
404
  const hasRequiredValues = Boolean(
291
- providedValues.domain &&
292
- providedValues.token &&
405
+ providedValues.domain &&
406
+ providedValues.token &&
293
407
  (providedValues.authType || providedValues.email)
294
408
  );
295
409
 
@@ -304,7 +418,7 @@ async function initConfig(cliOptions = {}) {
304
418
 
305
419
  const normalizedAuthType = normalizeAuthType(inferredAuthType, Boolean(providedValues.email));
306
420
  const normalizedDomain = providedValues.domain.trim();
307
-
421
+
308
422
  // Verify basic auth has email
309
423
  if (normalizedAuthType === 'basic' && !providedValues.email) {
310
424
  console.error(chalk.red('❌ Email is required for basic authentication'));
@@ -318,13 +432,15 @@ async function initConfig(cliOptions = {}) {
318
432
 
319
433
  const configData = {
320
434
  domain: normalizedDomain,
435
+ protocol: normalizeProtocol(providedValues.protocol),
321
436
  apiPath: providedValues.apiPath || inferApiPath(normalizedDomain),
322
437
  token: providedValues.token,
323
438
  authType: normalizedAuthType,
324
- email: providedValues.email
439
+ email: providedValues.email,
440
+ readOnly
325
441
  };
326
442
 
327
- saveConfig(configData);
443
+ saveConfig(configData, profileName);
328
444
  } catch (error) {
329
445
  console.error(chalk.red(`❌ ${error.message}`));
330
446
  process.exit(1);
@@ -335,26 +451,31 @@ async function initConfig(cliOptions = {}) {
335
451
  // Hybrid mode: some values provided, prompt for the rest
336
452
  try {
337
453
  console.log(chalk.blue('🚀 Confluence CLI Configuration'));
454
+ if (profileName) {
455
+ console.log(`Profile: ${chalk.cyan(profileName)}`);
456
+ }
338
457
  console.log('Completing configuration with interactive prompts:\n');
339
458
 
340
459
  const mergedValues = await promptForMissingValues(providedValues);
341
-
460
+
342
461
  // Normalize auth type
343
462
  mergedValues.authType = normalizeAuthType(mergedValues.authType, Boolean(mergedValues.email));
344
-
345
- saveConfig(mergedValues);
463
+
464
+ saveConfig({ ...mergedValues, readOnly }, profileName);
346
465
  } catch (error) {
347
466
  console.error(chalk.red(`❌ ${error.message}`));
348
467
  process.exit(1);
349
468
  }
350
469
  }
351
470
 
352
- function getConfig() {
471
+ function getConfig(profileName) {
353
472
  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;
473
+ const envToken = process.env.CONFLUENCE_API_TOKEN || process.env.CONFLUENCE_PASSWORD;
474
+ const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
356
475
  const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
357
476
  const envApiPath = process.env.CONFLUENCE_API_PATH;
477
+ const envProtocol = process.env.CONFLUENCE_PROTOCOL;
478
+ const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
358
479
 
359
480
  if (envDomain && envToken) {
360
481
  const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
@@ -368,29 +489,50 @@ function getConfig() {
368
489
  }
369
490
 
370
491
  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.'));
492
+ console.error(chalk.red('❌ Basic authentication requires CONFLUENCE_EMAIL or CONFLUENCE_USERNAME.'));
493
+ console.log(chalk.yellow('Set CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME for on-premise) or switch to bearer auth by setting CONFLUENCE_AUTH_TYPE=bearer.'));
373
494
  process.exit(1);
374
495
  }
375
496
 
376
497
  return {
377
498
  domain: envDomain.trim(),
499
+ protocol: normalizeProtocol(envProtocol),
378
500
  apiPath,
379
501
  token: envToken.trim(),
380
502
  email: envEmail ? envEmail.trim() : undefined,
381
- authType
503
+ authType,
504
+ readOnly: envReadOnly === 'true'
382
505
  };
383
506
  }
384
507
 
385
- if (!fs.existsSync(CONFIG_FILE)) {
508
+ // Resolve profile: explicit param > CONFLUENCE_PROFILE env var > activeProfile > default
509
+ const resolvedProfileName = profileName
510
+ || process.env.CONFLUENCE_PROFILE
511
+ || null;
512
+
513
+ const fileData = readConfigFile();
514
+
515
+ if (!fileData) {
386
516
  console.error(chalk.red('❌ No configuration found!'));
387
517
  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.'));
518
+ console.log(chalk.gray('Or set environment variables: CONFLUENCE_DOMAIN, CONFLUENCE_API_TOKEN (or CONFLUENCE_PASSWORD), CONFLUENCE_EMAIL (or CONFLUENCE_USERNAME), and optionally CONFLUENCE_API_PATH, CONFLUENCE_PROTOCOL.'));
519
+ process.exit(1);
520
+ }
521
+
522
+ const targetProfile = resolvedProfileName || fileData.activeProfile || DEFAULT_PROFILE;
523
+ const storedConfig = fileData.profiles && fileData.profiles[targetProfile];
524
+
525
+ if (!storedConfig) {
526
+ console.error(chalk.red(`❌ Profile "${targetProfile}" not found!`));
527
+ const available = fileData.profiles ? Object.keys(fileData.profiles) : [];
528
+ if (available.length > 0) {
529
+ console.log(chalk.yellow(`Available profiles: ${available.join(', ')}`));
530
+ }
531
+ console.log(chalk.yellow('Run "confluence init --profile <name>" to create it, or "confluence profile list" to see available profiles.'));
389
532
  process.exit(1);
390
533
  }
391
534
 
392
535
  try {
393
- const storedConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
394
536
  const trimmedDomain = (storedConfig.domain || '').trim();
395
537
  const trimmedToken = (storedConfig.token || '').trim();
396
538
  const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
@@ -404,8 +546,8 @@ function getConfig() {
404
546
  }
405
547
 
406
548
  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.'));
549
+ console.error(chalk.red('❌ Basic authentication requires an email address or username.'));
550
+ console.log(chalk.yellow('Please rerun "confluence init" to add your Confluence email or username.'));
409
551
  process.exit(1);
410
552
  }
411
553
 
@@ -417,12 +559,18 @@ function getConfig() {
417
559
  process.exit(1);
418
560
  }
419
561
 
562
+ const readOnly = envReadOnly !== undefined
563
+ ? envReadOnly === 'true'
564
+ : Boolean(storedConfig.readOnly);
565
+
420
566
  return {
421
567
  domain: trimmedDomain,
568
+ protocol: normalizeProtocol(storedConfig.protocol),
422
569
  apiPath,
423
570
  token: trimmedToken,
424
571
  email: trimmedEmail,
425
- authType
572
+ authType,
573
+ readOnly
426
574
  };
427
575
  } catch (error) {
428
576
  console.error(chalk.red('❌ Error reading configuration file:'), error.message);
@@ -431,7 +579,61 @@ function getConfig() {
431
579
  }
432
580
  }
433
581
 
582
+ function listProfiles() {
583
+ const fileData = readConfigFile();
584
+ if (!fileData || !fileData.profiles || Object.keys(fileData.profiles).length === 0) {
585
+ return { activeProfile: null, profiles: [] };
586
+ }
587
+ return {
588
+ activeProfile: fileData.activeProfile,
589
+ profiles: Object.keys(fileData.profiles).map(name => ({
590
+ name,
591
+ active: name === fileData.activeProfile,
592
+ domain: fileData.profiles[name].domain,
593
+ readOnly: Boolean(fileData.profiles[name].readOnly)
594
+ }))
595
+ };
596
+ }
597
+
598
+ function setActiveProfile(profileName) {
599
+ const fileData = readConfigFile();
600
+ if (!fileData) {
601
+ throw new Error('No configuration file found. Run "confluence init" first.');
602
+ }
603
+ if (!fileData.profiles || !fileData.profiles[profileName]) {
604
+ const available = fileData.profiles ? Object.keys(fileData.profiles) : [];
605
+ throw new Error(`Profile "${profileName}" not found. Available: ${available.join(', ')}`);
606
+ }
607
+ fileData.activeProfile = profileName;
608
+ saveConfigFile(fileData);
609
+ }
610
+
611
+ function deleteProfile(profileName) {
612
+ const fileData = readConfigFile();
613
+ if (!fileData) {
614
+ throw new Error('No configuration file found. Run "confluence init" first.');
615
+ }
616
+ if (!fileData.profiles || !fileData.profiles[profileName]) {
617
+ throw new Error(`Profile "${profileName}" not found.`);
618
+ }
619
+ if (Object.keys(fileData.profiles).length === 1) {
620
+ throw new Error('Cannot delete the only remaining profile.');
621
+ }
622
+ delete fileData.profiles[profileName];
623
+ if (fileData.activeProfile === profileName) {
624
+ fileData.activeProfile = Object.keys(fileData.profiles)[0];
625
+ }
626
+ saveConfigFile(fileData);
627
+ }
628
+
434
629
  module.exports = {
435
630
  initConfig,
436
- getConfig
631
+ getConfig,
632
+ listProfiles,
633
+ setActiveProfile,
634
+ deleteProfile,
635
+ isValidProfileName,
636
+ CONFIG_DIR,
637
+ CONFIG_FILE,
638
+ DEFAULT_PROFILE
437
639
  };