confluence-cli 1.25.1 → 1.27.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/.claude/skills/confluence/SKILL.md +98 -2
- package/README.md +78 -1
- package/bin/confluence.js +137 -31
- package/lib/config.js +181 -23
- package/package.json +1 -1
|
@@ -25,6 +25,18 @@ confluence --version # verify install
|
|
|
25
25
|
| `CONFLUENCE_AUTH_TYPE` | `basic` or `bearer` | `basic` |
|
|
26
26
|
| `CONFLUENCE_EMAIL` | Email address (basic auth only) | `user@company.com` |
|
|
27
27
|
| `CONFLUENCE_API_TOKEN` | API token or personal access token | `ATATT3x...` |
|
|
28
|
+
| `CONFLUENCE_PROFILE` | Named profile to use (optional) | `staging` |
|
|
29
|
+
| `CONFLUENCE_READ_ONLY` | Block all write operations when `true` | `true` |
|
|
30
|
+
|
|
31
|
+
**Global `--profile` flag (use a named profile for any command):**
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
confluence --profile <name> <command>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Config resolution works in two stages:
|
|
38
|
+
- **Direct env config:** If both `CONFLUENCE_DOMAIN` and `CONFLUENCE_API_TOKEN` are set, they are used directly and the config file / profiles are not consulted.
|
|
39
|
+
- **Profile-based config:** Otherwise, a profile is selected in this order: `--profile` flag > `CONFLUENCE_PROFILE` env > `activeProfile` in config > `default`.
|
|
28
40
|
|
|
29
41
|
**Non-interactive init (good for CI/CD scripts):**
|
|
30
42
|
|
|
@@ -52,6 +64,27 @@ export CONFLUENCE_EMAIL="user@company.com"
|
|
|
52
64
|
export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
53
65
|
```
|
|
54
66
|
|
|
67
|
+
**Read-only mode (recommended for AI agents):**
|
|
68
|
+
|
|
69
|
+
Prevents all write operations (create, update, delete, move, etc.) at the profile level. Useful when giving an AI agent access to Confluence for reading only.
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
# Via profile flag
|
|
73
|
+
confluence profile add agent --domain "company.atlassian.net" --token "xxx" --read-only
|
|
74
|
+
|
|
75
|
+
# Via environment variable (overrides config file)
|
|
76
|
+
export CONFLUENCE_READ_ONLY=true
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When read-only mode is active, any write command exits with an error:
|
|
80
|
+
```
|
|
81
|
+
Error: This profile is in read-only mode. Write operations are not allowed.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`profile list` shows read-only profiles with a `[read-only]` badge.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
55
88
|
## Page ID Resolution
|
|
56
89
|
|
|
57
90
|
Most commands accept `<pageId>` — a numeric ID or any of the supported URL formats below.
|
|
@@ -91,10 +124,14 @@ confluence read "https://company.atlassian.net/wiki/spaces/MYSPACE/pages/1234567
|
|
|
91
124
|
Initialize configuration. Saves credentials to `~/.confluence-cli/config.json`.
|
|
92
125
|
|
|
93
126
|
```sh
|
|
94
|
-
confluence init [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>]
|
|
127
|
+
confluence init [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>] [--read-only]
|
|
95
128
|
```
|
|
96
129
|
|
|
97
|
-
All flags are optional; omitting any flag triggers an interactive prompt for that field. Provide all flags to run fully non-interactive.
|
|
130
|
+
All flags are optional; omitting any flag triggers an interactive prompt for that field. Provide all flags to run fully non-interactive. Use the global `--profile` flag to save to a named profile:
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
confluence --profile staging init --domain "staging.example.com" --auth-type bearer --token "your-token"
|
|
134
|
+
```
|
|
98
135
|
|
|
99
136
|
---
|
|
100
137
|
|
|
@@ -520,6 +557,60 @@ confluence copy-tree 123456789 987654321 --exclude "Draft*,Archive*"
|
|
|
520
557
|
|
|
521
558
|
---
|
|
522
559
|
|
|
560
|
+
### `profile list`
|
|
561
|
+
|
|
562
|
+
List all configuration profiles with the active profile marked.
|
|
563
|
+
|
|
564
|
+
```sh
|
|
565
|
+
confluence profile list
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### `profile use <name>`
|
|
571
|
+
|
|
572
|
+
Switch the active configuration profile.
|
|
573
|
+
|
|
574
|
+
```sh
|
|
575
|
+
confluence profile use <name>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
```sh
|
|
579
|
+
confluence profile use staging
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
### `profile add <name>`
|
|
585
|
+
|
|
586
|
+
Add a new configuration profile. Supports the same options as `init` (interactive, non-interactive, or hybrid).
|
|
587
|
+
|
|
588
|
+
```sh
|
|
589
|
+
confluence profile add <name> [--domain <domain>] [--api-path <path>] [--auth-type basic|bearer] [--email <email>] [--token <token>] [--protocol http|https] [--read-only]
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Profile names may contain letters, numbers, hyphens, and underscores only.
|
|
593
|
+
|
|
594
|
+
```sh
|
|
595
|
+
confluence profile add staging --domain "staging.example.com" --auth-type bearer --token "xyz"
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
### `profile remove <name>`
|
|
601
|
+
|
|
602
|
+
Remove a configuration profile (prompts for confirmation). Cannot remove the only remaining profile.
|
|
603
|
+
|
|
604
|
+
```sh
|
|
605
|
+
confluence profile remove <name>
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
```sh
|
|
609
|
+
confluence profile remove staging
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
523
614
|
### `stats`
|
|
524
615
|
|
|
525
616
|
Show local usage statistics.
|
|
@@ -614,6 +705,8 @@ confluence search "release notes" --limit 20
|
|
|
614
705
|
- **ANSI color codes**: stdout may contain ANSI escape sequences. Pipe through `| cat` or use `NO_COLOR=1` if your downstream tool doesn't handle them.
|
|
615
706
|
- **Page ID vs URL**: when you have a Confluence URL, extract `?pageId=<number>` and pass the number. Do not pass pretty/display URLs — they are not supported.
|
|
616
707
|
- **Cross-space moves**: `confluence move` only works within the same space. Moving across spaces is not supported.
|
|
708
|
+
- **Multiple instances**: Use `--profile <name>` or `CONFLUENCE_PROFILE` env var to target different Confluence instances without reconfiguring.
|
|
709
|
+
- **Read-only mode**: Set `CONFLUENCE_READ_ONLY=true` or use `--read-only` when creating profiles to prevent accidental writes. This is enforced at the CLI level — all write commands will be blocked.
|
|
617
710
|
|
|
618
711
|
## Error Patterns
|
|
619
712
|
|
|
@@ -624,3 +717,6 @@ confluence search "release notes" --limit 20
|
|
|
624
717
|
| 400 on inline comment creation | Editor metadata required | Use `--location footer` or reply to existing inline comment with `--parent` |
|
|
625
718
|
| `File not found: <path>` | `--file` path doesn't exist | Check the path before calling the command |
|
|
626
719
|
| `At least one of --title, --file, or --content must be provided` | `update` called with no content options | Provide at least one of the required options |
|
|
720
|
+
| `Profile "<name>" not found!` | Specified profile doesn't exist | Run `confluence profile list` to see available profiles |
|
|
721
|
+
| `Cannot delete the only remaining profile.` | Tried to remove the last profile | Add another profile before removing |
|
|
722
|
+
| `This profile is in read-only mode` | Write command used with a read-only profile | Use a writable profile or remove `readOnly` from config |
|
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ A powerful command-line interface for Atlassian Confluence that allows you to re
|
|
|
16
16
|
- 💬 **Comments** - List, create, and delete page comments (footer or inline)
|
|
17
17
|
- 📦 **Export** - Save a page and its attachments to a local folder
|
|
18
18
|
- 🛠️ **Edit workflow** - Export page content for editing and re-import
|
|
19
|
+
- 🔀 **Profiles** - Manage multiple Confluence instances with named configuration profiles
|
|
20
|
+
- 🔒 **Read-only mode** - Profile-level write protection for safe AI agent usage
|
|
19
21
|
- 🔧 **Easy setup** - Simple configuration with environment variables or interactive setup
|
|
20
22
|
|
|
21
23
|
## Installation
|
|
@@ -115,6 +117,15 @@ confluence init \
|
|
|
115
117
|
--token "your-scoped-token"
|
|
116
118
|
```
|
|
117
119
|
|
|
120
|
+
**Named profile** (save to a specific profile):
|
|
121
|
+
```bash
|
|
122
|
+
confluence --profile staging init \
|
|
123
|
+
--domain "staging.example.com" \
|
|
124
|
+
--api-path "/rest/api" \
|
|
125
|
+
--auth-type "bearer" \
|
|
126
|
+
--token "your-personal-access-token"
|
|
127
|
+
```
|
|
128
|
+
|
|
118
129
|
**Hybrid mode** (some fields provided, rest via prompts):
|
|
119
130
|
```bash
|
|
120
131
|
# Domain and token provided, will prompt for auth method and email
|
|
@@ -130,6 +141,7 @@ confluence init --email "user@example.com" --token "your-api-token"
|
|
|
130
141
|
- `-a, --auth-type <type>` - Authentication type: `basic` or `bearer`
|
|
131
142
|
- `-e, --email <email>` - Email or username for basic authentication
|
|
132
143
|
- `-t, --token <token>` - API token or password
|
|
144
|
+
- `--read-only` - Enable read-only mode (blocks all write operations)
|
|
133
145
|
|
|
134
146
|
⚠️ **Security note:** While flags work, storing tokens in shell history is risky. Prefer environment variables (Option 3) for production environments.
|
|
135
147
|
|
|
@@ -141,6 +153,8 @@ export CONFLUENCE_EMAIL="your.email@example.com" # required for basic auth (ali
|
|
|
141
153
|
export CONFLUENCE_API_PATH="/wiki/rest/api" # Cloud default; use /rest/api for Server/DC
|
|
142
154
|
# Optional: set to 'bearer' for self-hosted/Data Center instances
|
|
143
155
|
export CONFLUENCE_AUTH_TYPE="basic"
|
|
156
|
+
# Optional: select a named profile (overridden by --profile flag)
|
|
157
|
+
export CONFLUENCE_PROFILE="default"
|
|
144
158
|
```
|
|
145
159
|
|
|
146
160
|
**Scoped API token** (recommended for agents):
|
|
@@ -154,6 +168,12 @@ export CONFLUENCE_API_TOKEN="your-scoped-token"
|
|
|
154
168
|
|
|
155
169
|
`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.
|
|
156
170
|
|
|
171
|
+
**Read-only mode** (recommended for AI agents):
|
|
172
|
+
```bash
|
|
173
|
+
export CONFLUENCE_READ_ONLY=true
|
|
174
|
+
```
|
|
175
|
+
When set, all write operations (`create`, `update`, `delete`, etc.) are blocked at the CLI level. The environment variable overrides the profile's `readOnly` setting.
|
|
176
|
+
|
|
157
177
|
### Getting Your API Token
|
|
158
178
|
|
|
159
179
|
**Atlassian Cloud:**
|
|
@@ -434,6 +454,52 @@ vim ./page-to-edit.xml
|
|
|
434
454
|
confluence update 123456789 --file ./page-to-edit.xml --format storage
|
|
435
455
|
```
|
|
436
456
|
|
|
457
|
+
### Profile Management
|
|
458
|
+
```bash
|
|
459
|
+
# List all profiles and see which is active
|
|
460
|
+
confluence profile list
|
|
461
|
+
|
|
462
|
+
# Switch the active profile
|
|
463
|
+
confluence profile use staging
|
|
464
|
+
|
|
465
|
+
# Add a new profile interactively
|
|
466
|
+
confluence profile add staging
|
|
467
|
+
|
|
468
|
+
# Add a new profile non-interactively
|
|
469
|
+
confluence profile add staging --domain "staging.example.com" --auth-type bearer --token "xyz"
|
|
470
|
+
|
|
471
|
+
# Add a read-only profile (blocks all write operations)
|
|
472
|
+
confluence profile add agent --domain "company.atlassian.net" --auth-type basic --email "bot@example.com" --token "xyz" --read-only
|
|
473
|
+
|
|
474
|
+
# Remove a profile
|
|
475
|
+
confluence profile remove staging
|
|
476
|
+
|
|
477
|
+
# Use a specific profile for a single command
|
|
478
|
+
confluence --profile staging spaces
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Read-Only Mode
|
|
482
|
+
|
|
483
|
+
Read-only mode blocks all write operations at the CLI level, making it safe to hand the tool to AI agents (Claude Code, Copilot, etc.) without risking accidental edits.
|
|
484
|
+
|
|
485
|
+
**Enable via profile:**
|
|
486
|
+
```bash
|
|
487
|
+
# During init
|
|
488
|
+
confluence init --read-only
|
|
489
|
+
|
|
490
|
+
# When adding a profile
|
|
491
|
+
confluence profile add agent --domain "company.atlassian.net" --token "xyz" --read-only
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Enable via environment variable:**
|
|
495
|
+
```bash
|
|
496
|
+
export CONFLUENCE_READ_ONLY=true # overrides profile setting
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
When read-only mode is active, any write command (`create`, `create-child`, `update`, `delete`, `move`, `edit`, `comment`, `attachment-upload`, `attachment-delete`, `property-set`, `property-delete`, `comment-delete`, `copy-tree`) exits with code 1 and prints an error message.
|
|
500
|
+
|
|
501
|
+
`confluence profile list` shows a `[read-only]` badge next to protected profiles.
|
|
502
|
+
|
|
437
503
|
### View Usage Statistics
|
|
438
504
|
```bash
|
|
439
505
|
confluence stats
|
|
@@ -443,7 +509,7 @@ confluence stats
|
|
|
443
509
|
|
|
444
510
|
| Command | Description | Options |
|
|
445
511
|
|---|---|---|
|
|
446
|
-
| `init` | Initialize CLI configuration | |
|
|
512
|
+
| `init` | Initialize CLI configuration | `--read-only` |
|
|
447
513
|
| `read <pageId_or_url>` | Read page content | `--format <html\|text\|markdown>` |
|
|
448
514
|
| `info <pageId_or_url>` | Get page information | |
|
|
449
515
|
| `search <query>` | Search for pages | `--limit <number>` |
|
|
@@ -468,8 +534,14 @@ confluence stats
|
|
|
468
534
|
| `property-set <pageId_or_url> <key>` | Set a content property (create or update) | `--value <json>`, `--file <path>`, `--format <text\|json>` |
|
|
469
535
|
| `property-delete <pageId_or_url> <key>` | Delete a content property by key | `--yes` |
|
|
470
536
|
| `export <pageId_or_url>` | Export a page to a directory with its attachments | `--format <html\|text\|markdown>`, `--dest <directory>`, `--file <filename>`, `--attachments-dir <name>`, `--pattern <glob>`, `--referenced-only`, `--skip-attachments` |
|
|
537
|
+
| `profile list` | List all configuration profiles | |
|
|
538
|
+
| `profile use <name>` | Set the active configuration profile | |
|
|
539
|
+
| `profile add <name>` | Add a new configuration profile | `-d, --domain`, `-p, --api-path`, `-a, --auth-type`, `-e, --email`, `-t, --token`, `--protocol`, `--read-only` |
|
|
540
|
+
| `profile remove <name>` | Remove a configuration profile | |
|
|
471
541
|
| `stats` | View your usage statistics | |
|
|
472
542
|
|
|
543
|
+
**Global option:** `--profile <name>` — Use a specific profile for any command (overrides `CONFLUENCE_PROFILE` env var and active profile).
|
|
544
|
+
|
|
473
545
|
## Examples
|
|
474
546
|
|
|
475
547
|
```bash
|
|
@@ -503,6 +575,11 @@ confluence attachment-delete 123456789 998877 --yes
|
|
|
503
575
|
|
|
504
576
|
# View usage statistics
|
|
505
577
|
confluence stats
|
|
578
|
+
|
|
579
|
+
# Profile management
|
|
580
|
+
confluence profile list
|
|
581
|
+
confluence profile use staging
|
|
582
|
+
confluence --profile staging spaces
|
|
506
583
|
```
|
|
507
584
|
|
|
508
585
|
## Development
|
package/bin/confluence.js
CHANGED
|
@@ -4,7 +4,7 @@ const { program } = require('commander');
|
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
const inquirer = require('inquirer');
|
|
6
6
|
const ConfluenceClient = require('../lib/confluence-client');
|
|
7
|
-
const { getConfig, initConfig } = require('../lib/config');
|
|
7
|
+
const { getConfig, initConfig, listProfiles, setActiveProfile, deleteProfile, isValidProfileName } = require('../lib/config');
|
|
8
8
|
const Analytics = require('../lib/analytics');
|
|
9
9
|
const pkg = require('../package.json');
|
|
10
10
|
|
|
@@ -13,10 +13,24 @@ function buildPageUrl(config, path) {
|
|
|
13
13
|
return `${protocol}://${config.domain}${path}`;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function assertWritable(config) {
|
|
17
|
+
if (config.readOnly) {
|
|
18
|
+
console.error(chalk.red('Error: This profile is in read-only mode. Write operations are not allowed.'));
|
|
19
|
+
console.error(chalk.yellow('Tip: Use "confluence profile add <name>" without --read-only, or set readOnly to false in config.'));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
program
|
|
17
25
|
.name('confluence')
|
|
18
26
|
.description('CLI tool for Atlassian Confluence')
|
|
19
|
-
.version(pkg.version)
|
|
27
|
+
.version(pkg.version)
|
|
28
|
+
.option('--profile <name>', 'Use a specific configuration profile');
|
|
29
|
+
|
|
30
|
+
// Helper: resolve profile name from global --profile flag
|
|
31
|
+
function getProfileName() {
|
|
32
|
+
return program.opts().profile || undefined;
|
|
33
|
+
}
|
|
20
34
|
|
|
21
35
|
// Init command
|
|
22
36
|
program
|
|
@@ -28,8 +42,10 @@ program
|
|
|
28
42
|
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
|
|
29
43
|
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
30
44
|
.option('-t, --token <token>', 'API token')
|
|
45
|
+
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
31
46
|
.action(async (options) => {
|
|
32
|
-
|
|
47
|
+
const profile = getProfileName();
|
|
48
|
+
await initConfig({ ...options, profile });
|
|
33
49
|
});
|
|
34
50
|
|
|
35
51
|
// Read command
|
|
@@ -40,7 +56,7 @@ program
|
|
|
40
56
|
.action(async (pageId, options) => {
|
|
41
57
|
const analytics = new Analytics();
|
|
42
58
|
try {
|
|
43
|
-
const client = new ConfluenceClient(getConfig());
|
|
59
|
+
const client = new ConfluenceClient(getConfig(getProfileName()));
|
|
44
60
|
const content = await client.readPage(pageId, options.format);
|
|
45
61
|
console.log(content);
|
|
46
62
|
analytics.track('read', true);
|
|
@@ -58,7 +74,7 @@ program
|
|
|
58
74
|
.action(async (pageId) => {
|
|
59
75
|
const analytics = new Analytics();
|
|
60
76
|
try {
|
|
61
|
-
const client = new ConfluenceClient(getConfig());
|
|
77
|
+
const client = new ConfluenceClient(getConfig(getProfileName()));
|
|
62
78
|
const info = await client.getPageInfo(pageId);
|
|
63
79
|
console.log(chalk.blue('Page Information:'));
|
|
64
80
|
console.log(`Title: ${chalk.green(info.title)}`);
|
|
@@ -85,7 +101,7 @@ program
|
|
|
85
101
|
.action(async (query, options) => {
|
|
86
102
|
const analytics = new Analytics();
|
|
87
103
|
try {
|
|
88
|
-
const client = new ConfluenceClient(getConfig());
|
|
104
|
+
const client = new ConfluenceClient(getConfig(getProfileName()));
|
|
89
105
|
const results = await client.search(query, parseInt(options.limit), options.cql);
|
|
90
106
|
|
|
91
107
|
if (results.length === 0) {
|
|
@@ -116,7 +132,7 @@ program
|
|
|
116
132
|
.action(async () => {
|
|
117
133
|
const analytics = new Analytics();
|
|
118
134
|
try {
|
|
119
|
-
const config = getConfig();
|
|
135
|
+
const config = getConfig(getProfileName());
|
|
120
136
|
const client = new ConfluenceClient(config);
|
|
121
137
|
const spaces = await client.getSpaces();
|
|
122
138
|
|
|
@@ -200,9 +216,10 @@ program
|
|
|
200
216
|
.action(async (title, spaceKey, options) => {
|
|
201
217
|
const analytics = new Analytics();
|
|
202
218
|
try {
|
|
203
|
-
const config = getConfig();
|
|
219
|
+
const config = getConfig(getProfileName());
|
|
220
|
+
assertWritable(config);
|
|
204
221
|
const client = new ConfluenceClient(config);
|
|
205
|
-
|
|
222
|
+
|
|
206
223
|
let content = '';
|
|
207
224
|
|
|
208
225
|
if (options.file) {
|
|
@@ -243,9 +260,10 @@ program
|
|
|
243
260
|
.action(async (title, parentId, options) => {
|
|
244
261
|
const analytics = new Analytics();
|
|
245
262
|
try {
|
|
246
|
-
const config = getConfig();
|
|
263
|
+
const config = getConfig(getProfileName());
|
|
264
|
+
assertWritable(config);
|
|
247
265
|
const client = new ConfluenceClient(config);
|
|
248
|
-
|
|
266
|
+
|
|
249
267
|
// Get parent page info to get space key
|
|
250
268
|
const parentInfo = await client.getPageInfo(parentId);
|
|
251
269
|
const spaceKey = parentInfo.space.key;
|
|
@@ -297,9 +315,10 @@ program
|
|
|
297
315
|
throw new Error('At least one of --title, --file, or --content must be provided.');
|
|
298
316
|
}
|
|
299
317
|
|
|
300
|
-
const config = getConfig();
|
|
318
|
+
const config = getConfig(getProfileName());
|
|
319
|
+
assertWritable(config);
|
|
301
320
|
const client = new ConfluenceClient(config);
|
|
302
|
-
|
|
321
|
+
|
|
303
322
|
let content = null; // Use null to indicate no content change
|
|
304
323
|
|
|
305
324
|
if (options.file) {
|
|
@@ -336,7 +355,8 @@ program
|
|
|
336
355
|
.action(async (pageId, newParentId, options) => {
|
|
337
356
|
const analytics = new Analytics();
|
|
338
357
|
try {
|
|
339
|
-
const config = getConfig();
|
|
358
|
+
const config = getConfig(getProfileName());
|
|
359
|
+
assertWritable(config);
|
|
340
360
|
const client = new ConfluenceClient(config);
|
|
341
361
|
const result = await client.movePage(pageId, newParentId, options.title);
|
|
342
362
|
|
|
@@ -363,7 +383,8 @@ program
|
|
|
363
383
|
.action(async (pageIdOrUrl, options) => {
|
|
364
384
|
const analytics = new Analytics();
|
|
365
385
|
try {
|
|
366
|
-
const config = getConfig();
|
|
386
|
+
const config = getConfig(getProfileName());
|
|
387
|
+
assertWritable(config);
|
|
367
388
|
const client = new ConfluenceClient(config);
|
|
368
389
|
const pageInfo = await client.getPageInfo(pageIdOrUrl);
|
|
369
390
|
|
|
@@ -406,7 +427,8 @@ program
|
|
|
406
427
|
.action(async (pageId, options) => {
|
|
407
428
|
const analytics = new Analytics();
|
|
408
429
|
try {
|
|
409
|
-
const config = getConfig();
|
|
430
|
+
const config = getConfig(getProfileName());
|
|
431
|
+
assertWritable(config);
|
|
410
432
|
const client = new ConfluenceClient(config);
|
|
411
433
|
const pageData = await client.getPageForEdit(pageId);
|
|
412
434
|
|
|
@@ -443,7 +465,7 @@ program
|
|
|
443
465
|
.action(async (title, options) => {
|
|
444
466
|
const analytics = new Analytics();
|
|
445
467
|
try {
|
|
446
|
-
const config = getConfig();
|
|
468
|
+
const config = getConfig(getProfileName());
|
|
447
469
|
const client = new ConfluenceClient(config);
|
|
448
470
|
const pageInfo = await client.findPageByTitle(title, options.space);
|
|
449
471
|
|
|
@@ -473,7 +495,7 @@ program
|
|
|
473
495
|
.action(async (pageId, options) => {
|
|
474
496
|
const analytics = new Analytics();
|
|
475
497
|
try {
|
|
476
|
-
const config = getConfig();
|
|
498
|
+
const config = getConfig(getProfileName());
|
|
477
499
|
const client = new ConfluenceClient(config);
|
|
478
500
|
const maxResults = options.limit ? parseInt(options.limit, 10) : null;
|
|
479
501
|
const pattern = options.pattern ? options.pattern.trim() : null;
|
|
@@ -605,7 +627,8 @@ program
|
|
|
605
627
|
|
|
606
628
|
const fs = require('fs');
|
|
607
629
|
const path = require('path');
|
|
608
|
-
const config = getConfig();
|
|
630
|
+
const config = getConfig(getProfileName());
|
|
631
|
+
assertWritable(config);
|
|
609
632
|
const client = new ConfluenceClient(config);
|
|
610
633
|
|
|
611
634
|
const resolvedFiles = files.map((filePath) => ({
|
|
@@ -652,7 +675,8 @@ program
|
|
|
652
675
|
.action(async (pageId, attachmentId, options) => {
|
|
653
676
|
const analytics = new Analytics();
|
|
654
677
|
try {
|
|
655
|
-
const config = getConfig();
|
|
678
|
+
const config = getConfig(getProfileName());
|
|
679
|
+
assertWritable(config);
|
|
656
680
|
const client = new ConfluenceClient(config);
|
|
657
681
|
|
|
658
682
|
if (!options.yes) {
|
|
@@ -696,7 +720,7 @@ program
|
|
|
696
720
|
.action(async (pageId, options) => {
|
|
697
721
|
const analytics = new Analytics();
|
|
698
722
|
try {
|
|
699
|
-
const config = getConfig();
|
|
723
|
+
const config = getConfig(getProfileName());
|
|
700
724
|
const client = new ConfluenceClient(config);
|
|
701
725
|
|
|
702
726
|
const format = (options.format || 'text').toLowerCase();
|
|
@@ -766,7 +790,7 @@ program
|
|
|
766
790
|
.action(async (pageId, key, options) => {
|
|
767
791
|
const analytics = new Analytics();
|
|
768
792
|
try {
|
|
769
|
-
const config = getConfig();
|
|
793
|
+
const config = getConfig(getProfileName());
|
|
770
794
|
const client = new ConfluenceClient(config);
|
|
771
795
|
|
|
772
796
|
const format = (options.format || 'text').toLowerCase();
|
|
@@ -802,7 +826,8 @@ program
|
|
|
802
826
|
.action(async (pageId, key, options) => {
|
|
803
827
|
const analytics = new Analytics();
|
|
804
828
|
try {
|
|
805
|
-
const config = getConfig();
|
|
829
|
+
const config = getConfig(getProfileName());
|
|
830
|
+
assertWritable(config);
|
|
806
831
|
const client = new ConfluenceClient(config);
|
|
807
832
|
|
|
808
833
|
if (!options.value && !options.file) {
|
|
@@ -858,7 +883,8 @@ program
|
|
|
858
883
|
.action(async (pageId, key, options) => {
|
|
859
884
|
const analytics = new Analytics();
|
|
860
885
|
try {
|
|
861
|
-
const config = getConfig();
|
|
886
|
+
const config = getConfig(getProfileName());
|
|
887
|
+
assertWritable(config);
|
|
862
888
|
const client = new ConfluenceClient(config);
|
|
863
889
|
|
|
864
890
|
if (!options.yes) {
|
|
@@ -904,7 +930,7 @@ program
|
|
|
904
930
|
.action(async (pageId, options) => {
|
|
905
931
|
const analytics = new Analytics();
|
|
906
932
|
try {
|
|
907
|
-
const config = getConfig();
|
|
933
|
+
const config = getConfig(getProfileName());
|
|
908
934
|
const client = new ConfluenceClient(config);
|
|
909
935
|
|
|
910
936
|
const format = (options.format || 'text').toLowerCase();
|
|
@@ -1059,7 +1085,8 @@ program
|
|
|
1059
1085
|
const analytics = new Analytics();
|
|
1060
1086
|
let location = null;
|
|
1061
1087
|
try {
|
|
1062
|
-
const config = getConfig();
|
|
1088
|
+
const config = getConfig(getProfileName());
|
|
1089
|
+
assertWritable(config);
|
|
1063
1090
|
const client = new ConfluenceClient(config);
|
|
1064
1091
|
|
|
1065
1092
|
let content = '';
|
|
@@ -1173,7 +1200,8 @@ program
|
|
|
1173
1200
|
.action(async (commentId, options) => {
|
|
1174
1201
|
const analytics = new Analytics();
|
|
1175
1202
|
try {
|
|
1176
|
-
const config = getConfig();
|
|
1203
|
+
const config = getConfig(getProfileName());
|
|
1204
|
+
assertWritable(config);
|
|
1177
1205
|
const client = new ConfluenceClient(config);
|
|
1178
1206
|
|
|
1179
1207
|
if (!options.yes) {
|
|
@@ -1225,7 +1253,7 @@ program
|
|
|
1225
1253
|
.action(async (pageId, options) => {
|
|
1226
1254
|
const analytics = new Analytics();
|
|
1227
1255
|
try {
|
|
1228
|
-
const config = getConfig();
|
|
1256
|
+
const config = getConfig(getProfileName());
|
|
1229
1257
|
const client = new ConfluenceClient(config);
|
|
1230
1258
|
const fs = require('fs');
|
|
1231
1259
|
const path = require('path');
|
|
@@ -1591,9 +1619,10 @@ program
|
|
|
1591
1619
|
.action(async (sourcePageId, targetParentId, newTitle, options) => {
|
|
1592
1620
|
const analytics = new Analytics();
|
|
1593
1621
|
try {
|
|
1594
|
-
const config = getConfig();
|
|
1622
|
+
const config = getConfig(getProfileName());
|
|
1623
|
+
assertWritable(config);
|
|
1595
1624
|
const client = new ConfluenceClient(config);
|
|
1596
|
-
|
|
1625
|
+
|
|
1597
1626
|
// Parse numeric flags with safe fallbacks
|
|
1598
1627
|
const parsedDepth = parseInt(options.maxDepth, 10);
|
|
1599
1628
|
const maxDepth = Number.isNaN(parsedDepth) ? 10 : parsedDepth;
|
|
@@ -1711,7 +1740,7 @@ program
|
|
|
1711
1740
|
.action(async (pageId, options) => {
|
|
1712
1741
|
const analytics = new Analytics();
|
|
1713
1742
|
try {
|
|
1714
|
-
const config = getConfig();
|
|
1743
|
+
const config = getConfig(getProfileName());
|
|
1715
1744
|
const client = new ConfluenceClient(config);
|
|
1716
1745
|
|
|
1717
1746
|
// Extract page ID from URL if needed
|
|
@@ -1852,6 +1881,82 @@ function printTree(nodes, config, options, depth = 1) {
|
|
|
1852
1881
|
});
|
|
1853
1882
|
}
|
|
1854
1883
|
|
|
1884
|
+
// Profile management commands
|
|
1885
|
+
const profileCmd = program
|
|
1886
|
+
.command('profile')
|
|
1887
|
+
.description('Manage configuration profiles');
|
|
1888
|
+
|
|
1889
|
+
profileCmd
|
|
1890
|
+
.command('list')
|
|
1891
|
+
.description('List all configuration profiles')
|
|
1892
|
+
.action(() => {
|
|
1893
|
+
const { profiles } = listProfiles();
|
|
1894
|
+
if (profiles.length === 0) {
|
|
1895
|
+
console.log(chalk.yellow('No profiles configured. Run "confluence init" to create one.'));
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
console.log(chalk.blue('Configuration profiles:\n'));
|
|
1899
|
+
profiles.forEach(p => {
|
|
1900
|
+
const marker = p.active ? chalk.green(' (active)') : '';
|
|
1901
|
+
const readOnlyBadge = p.readOnly ? chalk.red(' [read-only]') : '';
|
|
1902
|
+
console.log(` ${p.active ? chalk.green('*') : ' '} ${chalk.cyan(p.name)}${marker}${readOnlyBadge} - ${chalk.gray(p.domain)}`);
|
|
1903
|
+
});
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
profileCmd
|
|
1907
|
+
.command('use <name>')
|
|
1908
|
+
.description('Set the active configuration profile')
|
|
1909
|
+
.action((name) => {
|
|
1910
|
+
try {
|
|
1911
|
+
setActiveProfile(name);
|
|
1912
|
+
console.log(chalk.green(`Switched to profile "${name}"`));
|
|
1913
|
+
} catch (error) {
|
|
1914
|
+
console.error(chalk.red('Error:'), error.message);
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
profileCmd
|
|
1920
|
+
.command('add <name>')
|
|
1921
|
+
.description('Add a new configuration profile interactively')
|
|
1922
|
+
.option('-d, --domain <domain>', 'Confluence domain')
|
|
1923
|
+
.option('--protocol <protocol>', 'Protocol (http or https)')
|
|
1924
|
+
.option('-p, --api-path <path>', 'REST API path')
|
|
1925
|
+
.option('-a, --auth-type <type>', 'Authentication type (basic or bearer)')
|
|
1926
|
+
.option('-e, --email <email>', 'Email or username for basic auth')
|
|
1927
|
+
.option('-t, --token <token>', 'API token')
|
|
1928
|
+
.option('--read-only', 'Set profile to read-only mode (blocks write operations)')
|
|
1929
|
+
.action(async (name, options) => {
|
|
1930
|
+
if (!isValidProfileName(name)) {
|
|
1931
|
+
console.error(chalk.red('Invalid profile name. Use only letters, numbers, hyphens, and underscores.'));
|
|
1932
|
+
process.exit(1);
|
|
1933
|
+
}
|
|
1934
|
+
await initConfig({ ...options, profile: name });
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
profileCmd
|
|
1938
|
+
.command('remove <name>')
|
|
1939
|
+
.description('Remove a configuration profile')
|
|
1940
|
+
.action(async (name) => {
|
|
1941
|
+
try {
|
|
1942
|
+
const { confirmed } = await inquirer.prompt([{
|
|
1943
|
+
type: 'confirm',
|
|
1944
|
+
name: 'confirmed',
|
|
1945
|
+
message: `Delete profile "${name}"?`,
|
|
1946
|
+
default: false
|
|
1947
|
+
}]);
|
|
1948
|
+
if (!confirmed) {
|
|
1949
|
+
console.log(chalk.yellow('Cancelled.'));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
deleteProfile(name);
|
|
1953
|
+
console.log(chalk.green(`Profile "${name}" removed.`));
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
console.error(chalk.red('Error:'), error.message);
|
|
1956
|
+
process.exit(1);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1855
1960
|
// Exported for testing
|
|
1856
1961
|
module.exports = {
|
|
1857
1962
|
program,
|
|
@@ -1862,6 +1967,7 @@ module.exports = {
|
|
|
1862
1967
|
uniquePathFor,
|
|
1863
1968
|
exportRecursive,
|
|
1864
1969
|
sanitizeTitle,
|
|
1970
|
+
assertWritable,
|
|
1865
1971
|
},
|
|
1866
1972
|
};
|
|
1867
1973
|
|
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
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`;
|
|
@@ -68,6 +71,47 @@ const normalizeApiPath = (rawValue, domain) => {
|
|
|
68
71
|
return withoutTrailing || inferApiPath(domain);
|
|
69
72
|
};
|
|
70
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 });
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
71
115
|
// Helper function to validate CLI-provided options
|
|
72
116
|
const validateCliOptions = (options) => {
|
|
73
117
|
const errors = [];
|
|
@@ -115,23 +159,40 @@ const validateCliOptions = (options) => {
|
|
|
115
159
|
};
|
|
116
160
|
|
|
117
161
|
// Helper function to save configuration with validation
|
|
118
|
-
const saveConfig = (configData) => {
|
|
119
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
120
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
121
|
-
}
|
|
122
|
-
|
|
162
|
+
const saveConfig = (configData, profileName) => {
|
|
123
163
|
const config = {
|
|
124
164
|
domain: configData.domain.trim(),
|
|
125
165
|
protocol: normalizeProtocol(configData.protocol),
|
|
126
166
|
apiPath: normalizeApiPath(configData.apiPath, configData.domain),
|
|
127
167
|
token: configData.token.trim(),
|
|
128
|
-
authType: configData.authType
|
|
129
|
-
email: configData.authType === 'basic' && configData.email ? configData.email.trim() : undefined
|
|
168
|
+
authType: configData.authType
|
|
130
169
|
};
|
|
131
170
|
|
|
132
|
-
|
|
171
|
+
if (configData.authType === 'basic' && configData.email) {
|
|
172
|
+
config.email = configData.email.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (configData.readOnly) {
|
|
176
|
+
config.readOnly = true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Read existing config file (or create new structure)
|
|
180
|
+
const fileData = readConfigFile() || { activeProfile: DEFAULT_PROFILE, profiles: {} };
|
|
181
|
+
|
|
182
|
+
const targetProfile = profileName || fileData.activeProfile || DEFAULT_PROFILE;
|
|
183
|
+
fileData.profiles[targetProfile] = config;
|
|
184
|
+
|
|
185
|
+
// If this is the first profile, make it active
|
|
186
|
+
if (!fileData.activeProfile || !fileData.profiles[fileData.activeProfile]) {
|
|
187
|
+
fileData.activeProfile = targetProfile;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
saveConfigFile(fileData);
|
|
133
191
|
|
|
134
192
|
console.log(chalk.green('✅ Configuration saved successfully!'));
|
|
193
|
+
if (profileName) {
|
|
194
|
+
console.log(`Profile: ${chalk.cyan(targetProfile)}`);
|
|
195
|
+
}
|
|
135
196
|
console.log(`Config file location: ${chalk.gray(CONFIG_FILE)}`);
|
|
136
197
|
console.log(chalk.yellow('\n💡 Tip: You can regenerate this config anytime by running "confluence init"'));
|
|
137
198
|
};
|
|
@@ -232,6 +293,16 @@ const promptForMissingValues = async (providedValues) => {
|
|
|
232
293
|
};
|
|
233
294
|
|
|
234
295
|
async function initConfig(cliOptions = {}) {
|
|
296
|
+
const profileName = cliOptions.profile;
|
|
297
|
+
|
|
298
|
+
// Validate profile name if provided
|
|
299
|
+
if (profileName && !isValidProfileName(profileName)) {
|
|
300
|
+
console.error(chalk.red('❌ Invalid profile name. Use only letters, numbers, hyphens, and underscores.'));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const readOnly = cliOptions.readOnly || false;
|
|
305
|
+
|
|
235
306
|
// Extract provided values from CLI options
|
|
236
307
|
const providedValues = {
|
|
237
308
|
protocol: cliOptions.protocol,
|
|
@@ -248,6 +319,9 @@ async function initConfig(cliOptions = {}) {
|
|
|
248
319
|
if (!hasCliOptions) {
|
|
249
320
|
// Interactive mode: no CLI options provided
|
|
250
321
|
console.log(chalk.blue('🚀 Confluence CLI Configuration'));
|
|
322
|
+
if (profileName) {
|
|
323
|
+
console.log(`Profile: ${chalk.cyan(profileName)}`);
|
|
324
|
+
}
|
|
251
325
|
console.log('Please provide your Confluence connection details:\n');
|
|
252
326
|
|
|
253
327
|
const answers = await inquirer.prompt([
|
|
@@ -307,7 +381,7 @@ async function initConfig(cliOptions = {}) {
|
|
|
307
381
|
}
|
|
308
382
|
]);
|
|
309
383
|
|
|
310
|
-
saveConfig(answers);
|
|
384
|
+
saveConfig({ ...answers, readOnly }, profileName);
|
|
311
385
|
return;
|
|
312
386
|
}
|
|
313
387
|
|
|
@@ -325,8 +399,8 @@ async function initConfig(cliOptions = {}) {
|
|
|
325
399
|
// Check if all required values are provided for non-interactive mode
|
|
326
400
|
// Non-interactive requires: domain, token, and either authType or email (for inference)
|
|
327
401
|
const hasRequiredValues = Boolean(
|
|
328
|
-
providedValues.domain &&
|
|
329
|
-
providedValues.token &&
|
|
402
|
+
providedValues.domain &&
|
|
403
|
+
providedValues.token &&
|
|
330
404
|
(providedValues.authType || providedValues.email)
|
|
331
405
|
);
|
|
332
406
|
|
|
@@ -341,7 +415,7 @@ async function initConfig(cliOptions = {}) {
|
|
|
341
415
|
|
|
342
416
|
const normalizedAuthType = normalizeAuthType(inferredAuthType, Boolean(providedValues.email));
|
|
343
417
|
const normalizedDomain = providedValues.domain.trim();
|
|
344
|
-
|
|
418
|
+
|
|
345
419
|
// Verify basic auth has email
|
|
346
420
|
if (normalizedAuthType === 'basic' && !providedValues.email) {
|
|
347
421
|
console.error(chalk.red('❌ Email is required for basic authentication'));
|
|
@@ -359,10 +433,11 @@ async function initConfig(cliOptions = {}) {
|
|
|
359
433
|
apiPath: providedValues.apiPath || inferApiPath(normalizedDomain),
|
|
360
434
|
token: providedValues.token,
|
|
361
435
|
authType: normalizedAuthType,
|
|
362
|
-
email: providedValues.email
|
|
436
|
+
email: providedValues.email,
|
|
437
|
+
readOnly
|
|
363
438
|
};
|
|
364
439
|
|
|
365
|
-
saveConfig(configData);
|
|
440
|
+
saveConfig(configData, profileName);
|
|
366
441
|
} catch (error) {
|
|
367
442
|
console.error(chalk.red(`❌ ${error.message}`));
|
|
368
443
|
process.exit(1);
|
|
@@ -373,27 +448,31 @@ async function initConfig(cliOptions = {}) {
|
|
|
373
448
|
// Hybrid mode: some values provided, prompt for the rest
|
|
374
449
|
try {
|
|
375
450
|
console.log(chalk.blue('🚀 Confluence CLI Configuration'));
|
|
451
|
+
if (profileName) {
|
|
452
|
+
console.log(`Profile: ${chalk.cyan(profileName)}`);
|
|
453
|
+
}
|
|
376
454
|
console.log('Completing configuration with interactive prompts:\n');
|
|
377
455
|
|
|
378
456
|
const mergedValues = await promptForMissingValues(providedValues);
|
|
379
|
-
|
|
457
|
+
|
|
380
458
|
// Normalize auth type
|
|
381
459
|
mergedValues.authType = normalizeAuthType(mergedValues.authType, Boolean(mergedValues.email));
|
|
382
|
-
|
|
383
|
-
saveConfig(mergedValues);
|
|
460
|
+
|
|
461
|
+
saveConfig({ ...mergedValues, readOnly }, profileName);
|
|
384
462
|
} catch (error) {
|
|
385
463
|
console.error(chalk.red(`❌ ${error.message}`));
|
|
386
464
|
process.exit(1);
|
|
387
465
|
}
|
|
388
466
|
}
|
|
389
467
|
|
|
390
|
-
function getConfig() {
|
|
468
|
+
function getConfig(profileName) {
|
|
391
469
|
const envDomain = process.env.CONFLUENCE_DOMAIN || process.env.CONFLUENCE_HOST;
|
|
392
470
|
const envToken = process.env.CONFLUENCE_API_TOKEN || process.env.CONFLUENCE_PASSWORD;
|
|
393
471
|
const envEmail = process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
|
|
394
472
|
const envAuthType = process.env.CONFLUENCE_AUTH_TYPE;
|
|
395
473
|
const envApiPath = process.env.CONFLUENCE_API_PATH;
|
|
396
474
|
const envProtocol = process.env.CONFLUENCE_PROTOCOL;
|
|
475
|
+
const envReadOnly = process.env.CONFLUENCE_READ_ONLY;
|
|
397
476
|
|
|
398
477
|
if (envDomain && envToken) {
|
|
399
478
|
const authType = normalizeAuthType(envAuthType, Boolean(envEmail));
|
|
@@ -418,19 +497,39 @@ function getConfig() {
|
|
|
418
497
|
apiPath,
|
|
419
498
|
token: envToken.trim(),
|
|
420
499
|
email: envEmail ? envEmail.trim() : undefined,
|
|
421
|
-
authType
|
|
500
|
+
authType,
|
|
501
|
+
readOnly: envReadOnly === 'true'
|
|
422
502
|
};
|
|
423
503
|
}
|
|
424
504
|
|
|
425
|
-
|
|
505
|
+
// Resolve profile: explicit param > CONFLUENCE_PROFILE env var > activeProfile > default
|
|
506
|
+
const resolvedProfileName = profileName
|
|
507
|
+
|| process.env.CONFLUENCE_PROFILE
|
|
508
|
+
|| null;
|
|
509
|
+
|
|
510
|
+
const fileData = readConfigFile();
|
|
511
|
+
|
|
512
|
+
if (!fileData) {
|
|
426
513
|
console.error(chalk.red('❌ No configuration found!'));
|
|
427
514
|
console.log(chalk.yellow('Please run "confluence init" to set up your configuration.'));
|
|
428
515
|
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.'));
|
|
429
516
|
process.exit(1);
|
|
430
517
|
}
|
|
431
518
|
|
|
519
|
+
const targetProfile = resolvedProfileName || fileData.activeProfile || DEFAULT_PROFILE;
|
|
520
|
+
const storedConfig = fileData.profiles && fileData.profiles[targetProfile];
|
|
521
|
+
|
|
522
|
+
if (!storedConfig) {
|
|
523
|
+
console.error(chalk.red(`❌ Profile "${targetProfile}" not found!`));
|
|
524
|
+
const available = fileData.profiles ? Object.keys(fileData.profiles) : [];
|
|
525
|
+
if (available.length > 0) {
|
|
526
|
+
console.log(chalk.yellow(`Available profiles: ${available.join(', ')}`));
|
|
527
|
+
}
|
|
528
|
+
console.log(chalk.yellow('Run "confluence init --profile <name>" to create it, or "confluence profile list" to see available profiles.'));
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
432
532
|
try {
|
|
433
|
-
const storedConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
434
533
|
const trimmedDomain = (storedConfig.domain || '').trim();
|
|
435
534
|
const trimmedToken = (storedConfig.token || '').trim();
|
|
436
535
|
const trimmedEmail = storedConfig.email ? storedConfig.email.trim() : undefined;
|
|
@@ -457,13 +556,18 @@ function getConfig() {
|
|
|
457
556
|
process.exit(1);
|
|
458
557
|
}
|
|
459
558
|
|
|
559
|
+
const readOnly = envReadOnly !== undefined
|
|
560
|
+
? envReadOnly === 'true'
|
|
561
|
+
: Boolean(storedConfig.readOnly);
|
|
562
|
+
|
|
460
563
|
return {
|
|
461
564
|
domain: trimmedDomain,
|
|
462
565
|
protocol: normalizeProtocol(storedConfig.protocol),
|
|
463
566
|
apiPath,
|
|
464
567
|
token: trimmedToken,
|
|
465
568
|
email: trimmedEmail,
|
|
466
|
-
authType
|
|
569
|
+
authType,
|
|
570
|
+
readOnly
|
|
467
571
|
};
|
|
468
572
|
} catch (error) {
|
|
469
573
|
console.error(chalk.red('❌ Error reading configuration file:'), error.message);
|
|
@@ -472,7 +576,61 @@ function getConfig() {
|
|
|
472
576
|
}
|
|
473
577
|
}
|
|
474
578
|
|
|
579
|
+
function listProfiles() {
|
|
580
|
+
const fileData = readConfigFile();
|
|
581
|
+
if (!fileData || !fileData.profiles || Object.keys(fileData.profiles).length === 0) {
|
|
582
|
+
return { activeProfile: null, profiles: [] };
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
activeProfile: fileData.activeProfile,
|
|
586
|
+
profiles: Object.keys(fileData.profiles).map(name => ({
|
|
587
|
+
name,
|
|
588
|
+
active: name === fileData.activeProfile,
|
|
589
|
+
domain: fileData.profiles[name].domain,
|
|
590
|
+
readOnly: Boolean(fileData.profiles[name].readOnly)
|
|
591
|
+
}))
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function setActiveProfile(profileName) {
|
|
596
|
+
const fileData = readConfigFile();
|
|
597
|
+
if (!fileData) {
|
|
598
|
+
throw new Error('No configuration file found. Run "confluence init" first.');
|
|
599
|
+
}
|
|
600
|
+
if (!fileData.profiles || !fileData.profiles[profileName]) {
|
|
601
|
+
const available = fileData.profiles ? Object.keys(fileData.profiles) : [];
|
|
602
|
+
throw new Error(`Profile "${profileName}" not found. Available: ${available.join(', ')}`);
|
|
603
|
+
}
|
|
604
|
+
fileData.activeProfile = profileName;
|
|
605
|
+
saveConfigFile(fileData);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function deleteProfile(profileName) {
|
|
609
|
+
const fileData = readConfigFile();
|
|
610
|
+
if (!fileData) {
|
|
611
|
+
throw new Error('No configuration file found. Run "confluence init" first.');
|
|
612
|
+
}
|
|
613
|
+
if (!fileData.profiles || !fileData.profiles[profileName]) {
|
|
614
|
+
throw new Error(`Profile "${profileName}" not found.`);
|
|
615
|
+
}
|
|
616
|
+
if (Object.keys(fileData.profiles).length === 1) {
|
|
617
|
+
throw new Error('Cannot delete the only remaining profile.');
|
|
618
|
+
}
|
|
619
|
+
delete fileData.profiles[profileName];
|
|
620
|
+
if (fileData.activeProfile === profileName) {
|
|
621
|
+
fileData.activeProfile = Object.keys(fileData.profiles)[0];
|
|
622
|
+
}
|
|
623
|
+
saveConfigFile(fileData);
|
|
624
|
+
}
|
|
625
|
+
|
|
475
626
|
module.exports = {
|
|
476
627
|
initConfig,
|
|
477
|
-
getConfig
|
|
628
|
+
getConfig,
|
|
629
|
+
listProfiles,
|
|
630
|
+
setActiveProfile,
|
|
631
|
+
deleteProfile,
|
|
632
|
+
isValidProfileName,
|
|
633
|
+
CONFIG_DIR,
|
|
634
|
+
CONFIG_FILE,
|
|
635
|
+
DEFAULT_PROFILE
|
|
478
636
|
};
|