ccconfig 1.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.
Files changed (3) hide show
  1. package/README.md +368 -0
  2. package/ccconfig.js +942 -0
  3. package/package.json +23 -0
package/README.md ADDED
@@ -0,0 +1,368 @@
1
+ # Claude Code Configuration Manager
2
+
3
+ Quickly switch between different claude-code providers
4
+
5
+
6
+ ```bash
7
+ # Switch to work configuration during work hours
8
+ ccconfig use company
9
+
10
+ # Switch back to personal configuration after work
11
+ ccconfig use personal
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ # Install from npm (recommended)
20
+ npm install -g ccconfig
21
+ ```
22
+
23
+ ### ENV Mode (Recommended, Default)
24
+
25
+ ```bash
26
+ # 1. Configure Shell auto-loading (see below)
27
+
28
+ # 2. Add configuration (interactive mode)
29
+ ccconfig add
30
+ # Follow the prompts to enter:
31
+ # - Name
32
+ # - ANTHROPIC_BASE_URL
33
+ # - ANTHROPIC_AUTH_TOKEN
34
+ # - ANTHROPIC_API_KEY
35
+ # - Description
36
+
37
+ # 3. Switch configuration
38
+ ccconfig use work
39
+
40
+ # 4. Restart Shell or apply immediately
41
+ eval $(ccconfig env bash) # or use the detected command from output
42
+ ```
43
+
44
+ ### Settings Mode
45
+
46
+ ```bash
47
+ # 1. Switch to settings mode
48
+ ccconfig mode settings
49
+
50
+ # 2. Add configuration (interactive mode)
51
+ ccconfig add
52
+ # Follow the prompts to configure
53
+
54
+ # 3. Switch configuration
55
+ ccconfig use work
56
+
57
+ # 4. Restart Claude Code
58
+ # Configuration is now active!
59
+ ```
60
+
61
+ #### ENV Mode Shell Configuration
62
+
63
+ Configure once by adding to your Shell startup files:
64
+
65
+ **Fish** (`~/.config/fish/config.fish`):
66
+ ```fish
67
+ # Load Claude Code environment variables
68
+ set -l claude_env ~/.config/claude-code/current.env
69
+ if test -f $claude_env
70
+ for line in (cat $claude_env)
71
+ set -l parts (string split '=' $line)
72
+ set -gx $parts[1] $parts[2]
73
+ end
74
+ end
75
+ ```
76
+
77
+ **Bash** (`~/.bashrc`):
78
+ ```bash
79
+ # Load Claude Code environment variables
80
+ if [ -f ~/.config/claude-code/current.env ]; then
81
+ export $(grep -v '^#' ~/.config/claude-code/current.env | xargs)
82
+ fi
83
+ ```
84
+
85
+ **Zsh** (`~/.zshrc`):
86
+ ```zsh
87
+ # Load Claude Code environment variables
88
+ if [ -f ~/.config/claude-code/current.env ]; then
89
+ export $(grep -v '^#' ~/.config/claude-code/current.env | xargs)
90
+ fi
91
+ ```
92
+
93
+ **PowerShell** (`$PROFILE`):
94
+ ```powershell
95
+ # Load Claude Code environment variables
96
+ $claudeEnv = "$env:USERPROFILE\.config\claude-code\current.env"
97
+ if (Test-Path $claudeEnv) {
98
+ Get-Content $claudeEnv | ForEach-Object {
99
+ if ($_ -match '^([^=]+)=(.*)$') {
100
+ [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Command Reference
107
+
108
+ ### Basic Commands
109
+
110
+ ```bash
111
+ # Run without command (defaults to list)
112
+ ccconfig
113
+
114
+ # List all configurations
115
+ ccconfig list
116
+
117
+ # Add new configuration (interactive mode only, auto-creates config file on first use)
118
+ ccconfig add
119
+
120
+ # Switch configuration
121
+ ccconfig use <name>
122
+
123
+ # Remove configuration
124
+ ccconfig remove <name>
125
+
126
+ # View current status (shows all configuration sources)
127
+ ccconfig current
128
+ ccconfig current --show-secret # Show full token
129
+
130
+ # Show configuration file path
131
+ ccconfig edit
132
+
133
+ # View version
134
+ ccconfig --version # or -V
135
+ ```
136
+
137
+ ### Mode Management
138
+
139
+ ```bash
140
+ # View current mode
141
+ ccconfig mode
142
+
143
+ # Switch to settings mode
144
+ ccconfig mode settings
145
+
146
+ # Switch to env mode
147
+ ccconfig mode env
148
+ ```
149
+
150
+ ### ENV Mode Specific
151
+
152
+ ```bash
153
+ # Apply immediately in current Shell (env mode)
154
+ eval $(ccconfig env bash) # Bash/Zsh
155
+ ccconfig env fish | source # Fish
156
+ ccconfig env pwsh | iex # PowerShell
157
+
158
+ # Output .env format
159
+ ccconfig env dotenv > .env
160
+ ```
161
+
162
+ ## Configuration File Locations
163
+
164
+ - **Configuration List**: `~/.config/claude-code/profiles.json`
165
+ - **Claude Settings**: `~/.claude/settings.json`
166
+ - **Environment Variables File**: `~/.config/claude-code/current.env`
167
+ - **Mode Settings**: `~/.config/claude-code/mode`
168
+
169
+ ## Configuration Example
170
+
171
+ `~/.config/claude-code/profiles.json`:
172
+
173
+ ```json
174
+ {
175
+ "profiles": {
176
+ "work": {
177
+ "env": {
178
+ "ANTHROPIC_BASE_URL": "https://api-proxy.company.com",
179
+ "ANTHROPIC_AUTH_TOKEN": "sk-auth-work-xxxxx",
180
+ "ANTHROPIC_API_KEY": "sk-key-work-xxxxx"
181
+ },
182
+ "description": "Work account"
183
+ },
184
+ "personal": {
185
+ "env": {
186
+ "ANTHROPIC_BASE_URL": "https://api.anthropic.com",
187
+ "ANTHROPIC_AUTH_TOKEN": "sk-ant-personal-xxxxx",
188
+ "ANTHROPIC_API_KEY": "sk-ant-personal-xxxxx"
189
+ },
190
+ "description": "Personal account"
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ ## Advanced Usage
197
+
198
+ ### Quick Aliases
199
+
200
+ ```bash
201
+ # Add to ~/.bashrc or ~/.zshrc
202
+ alias ccs='ccconfig'
203
+ alias ccs-use='ccconfig use'
204
+ alias ccs-list='ccconfig list'
205
+ alias ccs-current='ccconfig current'
206
+
207
+ # Fish (~/.config/fish/config.fish)
208
+ abbr ccs 'ccconfig'
209
+ abbr ccs-use 'ccconfig use'
210
+ abbr ccs-list 'ccconfig list'
211
+ ```
212
+
213
+ ### Project-Level Configuration
214
+
215
+ For specific projects, you can export .env files:
216
+
217
+ ```bash
218
+ # Export to project directory
219
+ cd my-project
220
+ ccconfig use project-config
221
+ ccconfig env dotenv > .env
222
+
223
+ # Use project configuration
224
+ source .env
225
+ ```
226
+
227
+ ### Backup and Synchronization
228
+
229
+ ```bash
230
+ # Backup configuration
231
+ cp ~/.config/claude-code/profiles.json ~/backup/claude-profiles.json
232
+
233
+ # Sync to new machine
234
+ scp ~/backup/claude-profiles.json new-machine:~/.config/claude-code/
235
+
236
+ # Or use version control (be careful with security!)
237
+ cd ~/.config/claude-code
238
+ git init
239
+ echo "*.env" >> .gitignore
240
+ git add profiles.json
241
+ git commit -m "Claude Code profiles"
242
+ ```
243
+
244
+ ## Troubleshooting
245
+
246
+ ### Configuration Not Taking Effect
247
+
248
+ **Settings Mode**:
249
+ 1. Check if configuration is written correctly: `ccconfig current`
250
+ 2. Confirm Claude Code has been restarted
251
+ 3. Check the `env` field in `~/.claude/settings.json`
252
+
253
+ **ENV Mode**:
254
+ 1. Check environment variables file: `cat ~/.config/claude-code/current.env`
255
+ 2. Confirm Shell configuration is correct: `cat ~/.bashrc | grep claude`
256
+ 3. Restart Shell or use `eval $(ccconfig env bash)`
257
+ 4. Check process environment variables: `ccconfig current`
258
+
259
+ ### Configuration Lost After Mode Switch
260
+
261
+ Switching modes does not affect saved configurations, only changes how configurations are applied. After switching, you need to `use` once more:
262
+
263
+ ```bash
264
+ ccconfig mode env # Switch to env mode
265
+ ccconfig use work # Reapply configuration
266
+ ```
267
+
268
+ ### File Permission Issues
269
+
270
+ ```bash
271
+ # Fix configuration file permissions
272
+ chmod 600 ~/.config/claude-code/profiles.json
273
+ chmod 600 ~/.claude/settings.json
274
+ chmod 600 ~/.config/claude-code/current.env
275
+ ```
276
+
277
+ ## Security Considerations
278
+
279
+ 1. **File Permissions**: The tool automatically sets configuration files to 600 permissions (owner read/write only)
280
+
281
+ 2. **Sensitive Information**:
282
+ - API keys are hidden by default, use `--show-secret` to view full values
283
+ - Do not commit configuration files to public repositories
284
+ - Use `.gitignore` to exclude sensitive files
285
+
286
+ 3. **Environment Variables**: ENV mode environment variables are inherited by child processes, be mindful of security
287
+
288
+ 4. **Version Control**: If version controlling configurations, use encryption or private repositories
289
+
290
+ ## Frequently Asked Questions
291
+
292
+ **Q: Which is better, Settings mode or ENV mode?**
293
+
294
+ A:
295
+ - **ENV mode** is recommended (default, better cross-shell support, instant apply)
296
+ - If you prefer not to configure shell startup files, **Settings mode** can be simpler (only needs Claude Code restart)
297
+
298
+ **Q: Can I use both modes simultaneously?**
299
+
300
+ A: Not recommended. Claude Code reads configuration based on priority:
301
+ - Settings mode: Reads directly from `settings.json`
302
+ - ENV mode: Reads from environment variables
303
+
304
+ Using both simultaneously may cause confusion.
305
+
306
+ **Q: How to use on Windows?**
307
+
308
+ A: Fully supported on Windows:
309
+ - Configuration file location: `%USERPROFILE%\.config\claude-code\`
310
+ - Settings mode requires no additional configuration
311
+ - ENV mode uses PowerShell configuration
312
+
313
+ **Q: Do I need to restart after switching configurations?**
314
+
315
+ A:
316
+ - **Settings mode**: Need to restart Claude Code
317
+ - **ENV mode**: Need to restart Shell (or use `env` command for immediate effect)
318
+
319
+ **Q: Can I export configurations for team use?**
320
+
321
+ A: Yes, but be careful:
322
+ ```bash
323
+ # Export configuration structure (excluding API keys)
324
+ cat ~/.config/claude-code/profiles.json | \
325
+ jq '.profiles | map_values({baseUrl, description})' > team-config.json
326
+
327
+ # Team members manually add their own API keys after importing
328
+ ```
329
+
330
+ ## Development
331
+
332
+ ### Project Structure
333
+
334
+ ```
335
+ .
336
+ ├── ccconfig.js # Core script
337
+ ├── package.json # npm configuration
338
+ ├── README.md # This document
339
+ └── .gitignore # Git ignore file
340
+ ```
341
+
342
+ ### Testing
343
+
344
+ ```bash
345
+ # Test version output
346
+ node ccconfig.js --version
347
+
348
+ # Test adding configuration (interactive only)
349
+ node ccconfig.js add
350
+
351
+ # Test listing
352
+ node ccconfig.js list
353
+
354
+ # Test switching
355
+ node ccconfig.js use test
356
+
357
+ # Test status viewing
358
+ node ccconfig.js current
359
+ node ccconfig.js current --show-secret
360
+
361
+ # Test mode switching
362
+ node ccconfig.js mode
363
+ node ccconfig.js mode env
364
+ ```
365
+
366
+ ## License
367
+
368
+ MIT
package/ccconfig.js ADDED
@@ -0,0 +1,942 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+
8
+ // Configuration file paths
9
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'claude-code');
10
+ const PROFILES_FILE = path.join(CONFIG_DIR, 'profiles.json');
11
+ const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
12
+ const ENV_FILE = path.join(CONFIG_DIR, 'current.env');
13
+ const MODE_FILE = path.join(CONFIG_DIR, 'mode');
14
+
15
+ // Default modes
16
+ const MODE_SETTINGS = 'settings'; // Directly modify ~/.claude/settings.json
17
+ const MODE_ENV = 'env'; // Use environment variable files
18
+
19
+ let PACKAGE_VERSION = 'unknown';
20
+ try {
21
+ const packageJson = require('./package.json');
22
+ if (packageJson && typeof packageJson.version === 'string') {
23
+ PACKAGE_VERSION = packageJson.version;
24
+ }
25
+ } catch (_) {
26
+ // Keep default 'unknown' when package.json is unavailable
27
+ }
28
+
29
+ /**
30
+ * Ensure directory exists
31
+ */
32
+ function ensureDir(dir) {
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, {recursive: true});
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Load configuration file
40
+ */
41
+ function loadProfiles() {
42
+ try {
43
+ if (!fs.existsSync(PROFILES_FILE)) {
44
+ return null;
45
+ }
46
+ const content = fs.readFileSync(PROFILES_FILE, 'utf-8');
47
+ return JSON.parse(content);
48
+ } catch (error) {
49
+ console.error(`Error: Unable to read configuration file: ${error.message}`);
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save configuration file
56
+ */
57
+ function saveProfiles(profiles) {
58
+ try {
59
+ ensureDir(CONFIG_DIR);
60
+ fs.writeFileSync(PROFILES_FILE, JSON.stringify(profiles, null, 2), 'utf-8');
61
+
62
+ // Set file permissions to owner read/write only (600)
63
+ if (os.platform() !== 'win32') {
64
+ fs.chmodSync(PROFILES_FILE, 0o600);
65
+ }
66
+ } catch (error) {
67
+ console.error(`Error: Unable to save configuration file: ${error.message}`);
68
+ process.exit(1);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Load Claude Code settings.json
74
+ */
75
+ function loadClaudeSettings() {
76
+ try {
77
+ if (!fs.existsSync(CLAUDE_SETTINGS)) {
78
+ return {};
79
+ }
80
+ const content = fs.readFileSync(CLAUDE_SETTINGS, 'utf-8');
81
+ return JSON.parse(content);
82
+ } catch (error) {
83
+ console.error(
84
+ `Warning: Unable to read Claude settings.json: ${error.message}`);
85
+ return {};
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Save Claude Code settings.json
91
+ */
92
+ function saveClaudeSettings(settings) {
93
+ try {
94
+ ensureDir(path.dirname(CLAUDE_SETTINGS));
95
+ fs.writeFileSync(
96
+ CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf-8');
97
+
98
+ if (os.platform() !== 'win32') {
99
+ fs.chmodSync(CLAUDE_SETTINGS, 0o600);
100
+ }
101
+ } catch (error) {
102
+ console.error(
103
+ `Error: Unable to save Claude settings.json: ${error.message}`);
104
+ process.exit(1);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get current mode
110
+ */
111
+ function getMode() {
112
+ try {
113
+ if (fs.existsSync(MODE_FILE)) {
114
+ const mode = fs.readFileSync(MODE_FILE, 'utf-8').trim();
115
+ return mode === MODE_SETTINGS ? MODE_SETTINGS : MODE_ENV;
116
+ }
117
+ } catch (error) {
118
+ // Ignore error, use default mode
119
+ }
120
+ return MODE_ENV;
121
+ }
122
+
123
+ /**
124
+ * Set mode
125
+ */
126
+ function setMode(mode) {
127
+ try {
128
+ ensureDir(CONFIG_DIR);
129
+ fs.writeFileSync(MODE_FILE, mode, 'utf-8');
130
+ } catch (error) {
131
+ console.error(`Error: Unable to save mode settings: ${error.message}`);
132
+ process.exit(1);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Update Claude Code environment variable configuration (settings mode)
138
+ */
139
+ function updateClaudeSettings(envVars) {
140
+ const settings = loadClaudeSettings();
141
+
142
+ if (!settings.env) {
143
+ settings.env = {};
144
+ }
145
+
146
+ // Clear old related environment variables
147
+ delete settings.env.ANTHROPIC_BASE_URL;
148
+ delete settings.env.ANTHROPIC_AUTH_TOKEN;
149
+ delete settings.env.ANTHROPIC_API_KEY;
150
+
151
+ // Set new environment variables
152
+ Object.assign(settings.env, envVars);
153
+
154
+ saveClaudeSettings(settings);
155
+ }
156
+
157
+ /**
158
+ * Write environment variable file (env mode)
159
+ */
160
+ function writeEnvFile(envVars) {
161
+ try {
162
+ ensureDir(CONFIG_DIR);
163
+ const lines =
164
+ Object.entries(envVars).map(([key, value]) => `${key}=${value}`);
165
+ const content = lines.join('\n') + '\n';
166
+ fs.writeFileSync(ENV_FILE, content, 'utf-8');
167
+
168
+ if (os.platform() !== 'win32') {
169
+ fs.chmodSync(ENV_FILE, 0o600);
170
+ }
171
+ } catch (error) {
172
+ console.error(
173
+ `Error: Unable to write environment variable file: ${error.message}`);
174
+ process.exit(1);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Read environment variable file
180
+ */
181
+ function readEnvFile() {
182
+ try {
183
+ if (!fs.existsSync(ENV_FILE)) {
184
+ return null;
185
+ }
186
+ const content = fs.readFileSync(ENV_FILE, 'utf-8');
187
+ const env = {};
188
+ content.split('\n').forEach(line => {
189
+ const match = line.match(/^([^=]+)=(.*)$/);
190
+ if (match) {
191
+ env[match[1]] = match[2];
192
+ }
193
+ });
194
+ return env;
195
+ } catch (error) {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get currently available environment variables (automatically select source
202
+ * based on mode)
203
+ */
204
+ function getActiveEnvVars() {
205
+ const mode = getMode();
206
+
207
+ if (mode === MODE_ENV) {
208
+ return readEnvFile();
209
+ }
210
+
211
+ const settings = loadClaudeSettings();
212
+ if (settings && settings.env && Object.keys(settings.env).length > 0) {
213
+ return settings.env;
214
+ }
215
+
216
+ const envVars = readEnvFile();
217
+ if (envVars && Object.keys(envVars).length > 0) {
218
+ return envVars;
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Get currently active configuration
226
+ */
227
+ function getCurrentProfile() {
228
+ const mode = getMode();
229
+ const profiles = loadProfiles();
230
+
231
+ if (!profiles) {
232
+ return null;
233
+ }
234
+
235
+ let currentEnv;
236
+
237
+ if (mode === MODE_SETTINGS) {
238
+ const settings = loadClaudeSettings();
239
+ if (!settings.env) return null;
240
+ currentEnv = settings.env;
241
+ } else {
242
+ const env = readEnvFile();
243
+ if (!env) return null;
244
+ currentEnv = env;
245
+ }
246
+
247
+ // Compare environment variables for matches
248
+ for (const [name, profile] of Object.entries(profiles.profiles)) {
249
+ if (!profile.env) continue;
250
+
251
+ const profileEnv = profile.env;
252
+ let matched = true;
253
+
254
+ // Check if ANTHROPIC_BASE_URL matches
255
+ if (profileEnv.ANTHROPIC_BASE_URL !== currentEnv.ANTHROPIC_BASE_URL) {
256
+ matched = false;
257
+ continue;
258
+ }
259
+
260
+ // Check if authentication token matches (supports both fields)
261
+ const profileAuth =
262
+ profileEnv.ANTHROPIC_AUTH_TOKEN || profileEnv.ANTHROPIC_API_KEY;
263
+ const currentAuth =
264
+ currentEnv.ANTHROPIC_AUTH_TOKEN || currentEnv.ANTHROPIC_API_KEY;
265
+
266
+ if (profileAuth !== currentAuth) {
267
+ matched = false;
268
+ continue;
269
+ }
270
+
271
+ if (matched) {
272
+ return name;
273
+ }
274
+ }
275
+
276
+ return null;
277
+ }
278
+
279
+ /**
280
+ * Initialize configuration file (auto-called when needed)
281
+ */
282
+ function initIfNeeded() {
283
+ if (!fs.existsSync(PROFILES_FILE)) {
284
+ const emptyProfiles = {profiles: {}};
285
+ try {
286
+ ensureDir(CONFIG_DIR);
287
+ saveProfiles(emptyProfiles);
288
+ console.log(`✓ Configuration file created: ${PROFILES_FILE}`);
289
+ console.log('');
290
+ } catch (error) {
291
+ console.error(
292
+ `Error: Unable to create configuration file: ${error.message}`);
293
+ process.exit(1);
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * List all configurations
300
+ */
301
+ function list() {
302
+ const profiles = loadProfiles();
303
+
304
+ if (!profiles || !profiles.profiles ||
305
+ Object.keys(profiles.profiles).length === 0) {
306
+ console.log('No configurations found.');
307
+ console.log('');
308
+ console.log('Add your first configuration:');
309
+ console.log(' ccconfig add work');
310
+ console.log('');
311
+ console.log(
312
+ 'The command will guide you through configuration step by step.');
313
+ return;
314
+ }
315
+
316
+ const currentProfile = getCurrentProfile();
317
+
318
+ console.log('Available configurations:\n');
319
+
320
+ for (const [name, profile] of Object.entries(profiles.profiles)) {
321
+ const isCurrent = name === currentProfile ? ' ← current' : '';
322
+ console.log(` ${name}${isCurrent}`);
323
+ if (profile.env && profile.env.ANTHROPIC_BASE_URL) {
324
+ console.log(` URL: ${profile.env.ANTHROPIC_BASE_URL}`);
325
+ }
326
+ if (profile.description) {
327
+ console.log(` Description: ${profile.description}`);
328
+ }
329
+ console.log('');
330
+ }
331
+
332
+ if (currentProfile) {
333
+ console.log(`Currently active: ${currentProfile}`);
334
+ } else {
335
+ const settings = loadClaudeSettings();
336
+ if (settings.env && settings.env.ANTHROPIC_BASE_URL) {
337
+ console.log(
338
+ 'Currently using custom configuration (not in configuration list)');
339
+ console.log(` URL: ${settings.env.ANTHROPIC_BASE_URL}`);
340
+ } else {
341
+ console.log('Claude Code environment variables not configured yet');
342
+ }
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Add new configuration
348
+ */
349
+ async function add(name) {
350
+ // Auto-initialize if needed
351
+ initIfNeeded();
352
+
353
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
354
+
355
+ if (!isInteractive) {
356
+ console.error('Error: Interactive mode required for adding configurations');
357
+ console.error('This command must be run in an interactive terminal');
358
+ process.exit(1);
359
+ }
360
+
361
+ let rl = null;
362
+
363
+ const askQuestion = (question, defaultValue = '') => {
364
+ if (!rl) {
365
+ rl = readline.createInterface(
366
+ {input: process.stdin, output: process.stdout});
367
+ }
368
+ return new Promise(resolve => {
369
+ const suffix = defaultValue ? ` (${defaultValue})` : '';
370
+ rl.question(`${question}${suffix}: `, answer => {
371
+ const trimmed = answer.trim();
372
+ resolve(trimmed ? trimmed : defaultValue.trim());
373
+ });
374
+ });
375
+ };
376
+
377
+ let baseUrl, authToken, apiKey, description;
378
+
379
+ try {
380
+ if (!name) {
381
+ name = await askQuestion('Please enter configuration name (e.g., work)');
382
+ }
383
+
384
+ if (!name) {
385
+ console.error('Error: Configuration name cannot be empty');
386
+ process.exit(1);
387
+ }
388
+
389
+ baseUrl = await askQuestion(
390
+ 'Please enter ANTHROPIC_BASE_URL (can be empty, default https://api.anthropic.com)',
391
+ 'https://api.anthropic.com');
392
+
393
+ authToken =
394
+ await askQuestion('Please enter ANTHROPIC_AUTH_TOKEN (can be empty)');
395
+
396
+ apiKey = await askQuestion('Please enter ANTHROPIC_API_KEY (can be empty)');
397
+
398
+ description = await askQuestion(
399
+ 'Please enter configuration description (can be empty)');
400
+ } finally {
401
+ if (rl) {
402
+ rl.close();
403
+ }
404
+ }
405
+
406
+ const profiles = loadProfiles() || {profiles: {}};
407
+
408
+ if (profiles.profiles[name]) {
409
+ console.error(`Error: Configuration '${name}' already exists`);
410
+ console.error('To update, please edit the configuration file directly');
411
+ process.exit(1);
412
+ }
413
+
414
+ const envVars = {
415
+ ANTHROPIC_BASE_URL: baseUrl || '',
416
+ ANTHROPIC_AUTH_TOKEN: authToken || '',
417
+ ANTHROPIC_API_KEY: apiKey || ''
418
+ };
419
+
420
+ profiles.profiles[name] = {env: envVars, description};
421
+
422
+ saveProfiles(profiles);
423
+ console.log(`✓ Configuration '${name}' added`);
424
+ console.log('');
425
+ console.log('Run the following command to activate:');
426
+ console.log(` ccconfig use ${name}`);
427
+ console.log('');
428
+ console.log('Saved environment variables:');
429
+ const safePrint = (key, value, mask = true) => {
430
+ if (!value) {
431
+ console.log(` ${key}: (not set)`);
432
+ return;
433
+ }
434
+ if (!mask) {
435
+ console.log(` ${key}: ${value}`);
436
+ return;
437
+ }
438
+ const masked = value.length > 20 ? value.substring(0, 20) + '...' : value;
439
+ console.log(` ${key}: ${masked}`);
440
+ };
441
+ safePrint('ANTHROPIC_BASE_URL', envVars.ANTHROPIC_BASE_URL, false);
442
+ safePrint('ANTHROPIC_AUTH_TOKEN', envVars.ANTHROPIC_AUTH_TOKEN);
443
+ safePrint('ANTHROPIC_API_KEY', envVars.ANTHROPIC_API_KEY);
444
+ console.log('');
445
+ console.log('This information has been saved to:');
446
+ console.log(` ${PROFILES_FILE}`);
447
+ console.log(
448
+ 'You can edit this file directly to further customize the profile:');
449
+ console.log(` vim ${PROFILES_FILE}`);
450
+ console.log('Or run ccconfig edit to open it with your preferred editor');
451
+ }
452
+
453
+ /**
454
+ * Remove configuration
455
+ */
456
+ function remove(name) {
457
+ if (!name) {
458
+ console.error('Error: Missing configuration name');
459
+ console.error('Usage: ccconfig remove <name>');
460
+ process.exit(1);
461
+ }
462
+
463
+ const profiles = loadProfiles();
464
+
465
+ if (!profiles) {
466
+ console.error('Error: Configuration file does not exist');
467
+ process.exit(1);
468
+ }
469
+
470
+ if (!profiles.profiles[name]) {
471
+ console.error(`Error: Configuration '${name}' does not exist`);
472
+ process.exit(1);
473
+ }
474
+
475
+ delete profiles.profiles[name];
476
+ saveProfiles(profiles);
477
+ console.log(`✓ Configuration '${name}' removed`);
478
+ }
479
+
480
+ /**
481
+ * Detect current shell and return recommended activation command
482
+ */
483
+ function detectShellCommand() {
484
+ const shellPath = (process.env.SHELL || '').toLowerCase();
485
+
486
+ if (process.env.FISH_VERSION || shellPath.includes('fish')) {
487
+ return {shell: 'fish', command: 'ccconfig env fish | source'};
488
+ }
489
+
490
+ if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
491
+ shellPath.includes('zsh')) {
492
+ return {shell: 'zsh', command: 'eval $(ccconfig env bash)'};
493
+ }
494
+
495
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
496
+ shellPath.includes('pwsh') || shellPath.includes('powershell')) {
497
+ return {shell: 'PowerShell', command: 'ccconfig env pwsh | iex'};
498
+ }
499
+
500
+ if (shellPath.includes('bash')) {
501
+ return {shell: 'bash', command: 'eval $(ccconfig env bash)'};
502
+ }
503
+
504
+ if (process.platform === 'win32') {
505
+ const comSpec = (process.env.ComSpec || '').toLowerCase();
506
+ if (comSpec.includes('powershell')) {
507
+ return {shell: 'PowerShell', command: 'ccconfig env pwsh | iex'};
508
+ }
509
+ }
510
+
511
+ return {shell: null, command: null};
512
+ }
513
+
514
+ /**
515
+ * Switch configuration
516
+ */
517
+ function use(name) {
518
+ const profiles = loadProfiles();
519
+
520
+ if (!profiles || !profiles.profiles ||
521
+ Object.keys(profiles.profiles).length === 0) {
522
+ console.error('Error: No configurations found');
523
+ console.error('Please add a configuration first: ccconfig add <name>');
524
+ process.exit(1);
525
+ }
526
+
527
+ if (!profiles.profiles[name]) {
528
+ console.error(`Error: Configuration '${name}' does not exist`);
529
+ console.error('Run ccconfig list to see available configurations');
530
+ process.exit(1);
531
+ }
532
+
533
+ const profile = profiles.profiles[name];
534
+
535
+ if (!profile.env || Object.keys(profile.env).length === 0) {
536
+ console.error(
537
+ `Error: Configuration '${name}' has empty environment variables`);
538
+ console.error('Please edit the configuration file to add env field');
539
+ process.exit(1);
540
+ }
541
+
542
+ const mode = getMode();
543
+
544
+ if (mode === MODE_SETTINGS) {
545
+ // Settings mode: directly modify ~/.claude/settings.json
546
+ updateClaudeSettings(profile.env);
547
+
548
+ console.log(`✓ Switched to configuration: ${name} (settings mode)`);
549
+ console.log(` Environment variables:`);
550
+ for (const [key, value] of Object.entries(profile.env)) {
551
+ const displayValue =
552
+ value.length > 20 ? value.substring(0, 20) + '...' : value;
553
+ console.log(` ${key}: ${displayValue}`);
554
+ }
555
+ console.log('');
556
+ console.log('Configuration written to ~/.claude/settings.json');
557
+ console.log('Restart Claude Code to make configuration take effect');
558
+ } else {
559
+ // Env mode: write to environment variable file
560
+ writeEnvFile(profile.env);
561
+
562
+ console.log(`✓ Switched to configuration: ${name} (env mode)`);
563
+ console.log(` Environment variables:`);
564
+ for (const [key, value] of Object.entries(profile.env)) {
565
+ const displayValue =
566
+ value.length > 20 ? value.substring(0, 20) + '...' : value;
567
+ console.log(` ${key}: ${displayValue}`);
568
+ }
569
+ console.log('');
570
+ console.log(`Environment variable file updated: ${ENV_FILE}`);
571
+ console.log('');
572
+ const shellSuggestion = detectShellCommand();
573
+ const applyCommands = [
574
+ {command: 'eval $(ccconfig env bash)', note: '# Bash/Zsh'},
575
+ {command: 'ccconfig env fish | source', note: '# Fish'},
576
+ {command: 'ccconfig env pwsh | iex', note: '# PowerShell'}
577
+ ];
578
+
579
+ console.log('Apply immediately in current Shell (optional):');
580
+
581
+ if (shellSuggestion.command) {
582
+ console.log(
583
+ ` ${shellSuggestion.command} # Detected ${shellSuggestion.shell}`);
584
+
585
+ const normalizedSuggestion =
586
+ shellSuggestion.command.replace(/\s+/g, ' ').trim();
587
+ for (const item of applyCommands) {
588
+ const normalizedCommand = item.command.replace(/\s+/g, ' ').trim();
589
+ if (normalizedCommand === normalizedSuggestion) {
590
+ item.skip = true;
591
+ }
592
+ }
593
+ }
594
+
595
+ for (const item of applyCommands) {
596
+ if (item.skip) continue;
597
+ console.log(` ${item.command} ${item.note}`);
598
+ }
599
+ console.log('');
600
+ console.log('Or restart Shell to auto-load');
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Display current configuration
606
+ */
607
+ function current(showSecret = false) {
608
+ const currentMode = getMode();
609
+ const settings = loadClaudeSettings();
610
+ const envFile = readEnvFile();
611
+ const processEnv = {
612
+ ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
613
+ ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
614
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY
615
+ };
616
+ const currentProfile = getCurrentProfile();
617
+
618
+ console.log('═══════════════════════════════════════════');
619
+ console.log('Claude Code Configuration Status');
620
+ console.log('═══════════════════════════════════════════');
621
+ console.log('');
622
+
623
+ // Display current mode
624
+ console.log(`Current Mode: ${currentMode}`);
625
+ if (currentProfile) {
626
+ console.log(`Active Configuration: ${currentProfile}`);
627
+ } else {
628
+ console.log('Active Configuration: (no matching configuration)');
629
+ }
630
+ console.log('');
631
+
632
+ // Display settings.json configuration
633
+ console.log('【1】~/.claude/settings.json:');
634
+ if (settings.env &&
635
+ (settings.env.ANTHROPIC_BASE_URL || settings.env.ANTHROPIC_AUTH_TOKEN)) {
636
+ const baseUrl = settings.env.ANTHROPIC_BASE_URL || '(not set)';
637
+ const authToken = settings.env.ANTHROPIC_AUTH_TOKEN || '(not set)';
638
+ const maskedToken = (authToken === '(not set)' || showSecret) ?
639
+ authToken :
640
+ authToken.substring(0, 20) + '...';
641
+
642
+ console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
643
+ console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
644
+ } else {
645
+ console.log(' (not configured)');
646
+ }
647
+ console.log('');
648
+
649
+ // Display environment variable file configuration
650
+ console.log(`【2】Environment Variables File (${ENV_FILE}):`);
651
+ if (envFile &&
652
+ (envFile.ANTHROPIC_BASE_URL || envFile.ANTHROPIC_AUTH_TOKEN ||
653
+ envFile.ANTHROPIC_API_KEY)) {
654
+ const baseUrl = envFile.ANTHROPIC_BASE_URL || '(not set)';
655
+ const authToken = envFile.ANTHROPIC_AUTH_TOKEN ||
656
+ envFile.ANTHROPIC_API_KEY || '(not set)';
657
+ const maskedToken = (authToken === '(not set)' || showSecret) ?
658
+ authToken :
659
+ authToken.substring(0, 20) + '...';
660
+
661
+ console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
662
+ console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
663
+ } else {
664
+ console.log(' (not configured)');
665
+ }
666
+ console.log('');
667
+
668
+ // Display current process environment variables
669
+ console.log('【3】Current Process Environment Variables:');
670
+ if (processEnv.ANTHROPIC_BASE_URL || processEnv.ANTHROPIC_AUTH_TOKEN ||
671
+ processEnv.ANTHROPIC_API_KEY) {
672
+ const baseUrl = processEnv.ANTHROPIC_BASE_URL || '(not set)';
673
+ const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ||
674
+ processEnv.ANTHROPIC_API_KEY || '(not set)';
675
+ const maskedToken = (authToken === '(not set)' || showSecret) ?
676
+ authToken :
677
+ authToken.substring(0, 20) + '...';
678
+
679
+ console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
680
+ console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
681
+ } else {
682
+ console.log(' (not set)');
683
+ }
684
+ console.log('');
685
+
686
+ // Display notes
687
+ console.log('───────────────────────────────────────────');
688
+ console.log('Notes:');
689
+ console.log(' • Settings mode: Claude Code reads from 【1】');
690
+ console.log(' • ENV mode: Claude Code reads from 【3】(loaded from 【2】)');
691
+ if (!showSecret) {
692
+ console.log('');
693
+ console.log('Use --show-secret to display full token');
694
+ }
695
+ console.log('═══════════════════════════════════════════');
696
+ }
697
+
698
+ /**
699
+ * Show configuration file path
700
+ */
701
+ function edit() {
702
+ if (!fs.existsSync(PROFILES_FILE)) {
703
+ console.error('Error: Configuration file does not exist');
704
+ console.error('Please add a configuration first: ccconfig add <name>');
705
+ process.exit(1);
706
+ }
707
+
708
+ const editor = process.env.EDITOR || process.env.VISUAL || 'vim';
709
+
710
+ console.log('Configuration file path:');
711
+ console.log(` ${PROFILES_FILE}`);
712
+ console.log('');
713
+ console.log('Open it with your preferred editor, for example:');
714
+ console.log(` ${editor} ${PROFILES_FILE}`);
715
+ }
716
+
717
+ /**
718
+ * Switch/view mode
719
+ */
720
+ function mode(newMode) {
721
+ if (!newMode) {
722
+ // Display current mode
723
+ const currentMode = getMode();
724
+ console.log(`Current mode: ${currentMode}`);
725
+ console.log('');
726
+ if (currentMode === MODE_SETTINGS) {
727
+ console.log('SETTINGS mode:');
728
+ console.log(' - Directly modify ~/.claude/settings.json');
729
+ console.log(' - No Shell configuration needed');
730
+ console.log(' - Restart Claude Code to take effect');
731
+ } else {
732
+ console.log('ENV mode:');
733
+ console.log(' - Use environment variable files');
734
+ console.log(' - Need to configure Shell loading script');
735
+ console.log(' - Cross-Shell configuration sharing');
736
+ }
737
+ console.log('');
738
+ console.log('Switch modes:');
739
+ console.log(' ccconfig mode settings');
740
+ console.log(' ccconfig mode env');
741
+ return;
742
+ }
743
+
744
+ if (newMode !== MODE_SETTINGS && newMode !== MODE_ENV) {
745
+ console.error(`Error: Invalid mode '${newMode}'`);
746
+ console.error(`Available modes: ${MODE_SETTINGS}, ${MODE_ENV}`);
747
+ process.exit(1);
748
+ }
749
+
750
+ const oldMode = getMode();
751
+ setMode(newMode);
752
+
753
+ console.log(`✓ Mode switched: ${oldMode} -> ${newMode}`);
754
+ console.log('');
755
+
756
+ if (newMode === MODE_SETTINGS) {
757
+ console.log('SETTINGS mode enabled');
758
+ console.log(
759
+ ' Next use command will directly modify ~/.claude/settings.json');
760
+ } else {
761
+ console.log('ENV mode enabled');
762
+ console.log(' Next use command will write to environment variable file');
763
+ console.log(' Please ensure Shell loading script is configured');
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Output environment variables (for source)
769
+ */
770
+ function env(format = 'bash') {
771
+ const envVars = getActiveEnvVars();
772
+
773
+ if (!envVars || Object.keys(envVars).length === 0) {
774
+ console.error(
775
+ 'Error: No available environment variable configuration found');
776
+ console.error(
777
+ 'Please run ccconfig use <name> to select a configuration first');
778
+ process.exit(1);
779
+ }
780
+
781
+ // Output all environment variables
782
+ switch (format) {
783
+ case 'fish':
784
+ for (const [key, value] of Object.entries(envVars)) {
785
+ const renderedValue = value == null ? '' : String(value);
786
+ console.log(`set -gx ${key} "${renderedValue}"`);
787
+ }
788
+ break;
789
+ case 'bash':
790
+ case 'zsh':
791
+ case 'sh':
792
+ for (const [key, value] of Object.entries(envVars)) {
793
+ const renderedValue = value == null ? '' : String(value);
794
+ console.log(`export ${key}="${renderedValue}"`);
795
+ }
796
+ break;
797
+ case 'powershell':
798
+ case 'pwsh':
799
+ for (const [key, value] of Object.entries(envVars)) {
800
+ const renderedValue = value == null ? '' : String(value);
801
+ console.log(`$env:${key}="${renderedValue}"`);
802
+ }
803
+ break;
804
+ case 'dotenv':
805
+ for (const [key, value] of Object.entries(envVars)) {
806
+ const renderedValue = value == null ? '' : String(value);
807
+ console.log(`${key}=${renderedValue}`);
808
+ }
809
+ break;
810
+ default:
811
+ console.error(`Error: Unsupported format: ${format}`);
812
+ console.error(
813
+ 'Supported formats: fish, bash, zsh, sh, powershell, pwsh, dotenv');
814
+ process.exit(1);
815
+ }
816
+ }
817
+
818
+ /**
819
+ * Display help information
820
+ */
821
+ function help() {
822
+ console.log('Claude Code Configuration Manager');
823
+ console.log('');
824
+ console.log(`Profiles are stored in: ${PROFILES_FILE}`);
825
+ console.log('');
826
+ console.log('Supports two modes:');
827
+ console.log(
828
+ ' env - Use environment variable files (default, cross-Shell, instant apply)');
829
+ console.log(
830
+ ' settings - Directly modify ~/.claude/settings.json (no Shell config needed)');
831
+ console.log('');
832
+ console.log('Usage:');
833
+ console.log(' ccconfig [command] [options]');
834
+ console.log('');
835
+ console.log('Global Options:');
836
+ console.log(
837
+ ' --help, -h Display this help information');
838
+ console.log(
839
+ ' --version, -V Display version information');
840
+ console.log('');
841
+ console.log('Commands:');
842
+ console.log(
843
+ ' list|ls List all configurations (default)');
844
+ console.log(
845
+ ' add [name] Add new configuration (interactive)');
846
+ console.log(
847
+ ' use <name> Switch to specified configuration');
848
+ console.log(
849
+ ' remove|rm <name> Remove configuration');
850
+ console.log(
851
+ ' current [--show-secret] Display current configuration');
852
+ console.log(
853
+ ' mode [settings|env] View or switch mode');
854
+ console.log(
855
+ ' env [format] Output environment variables (env mode)');
856
+ console.log(
857
+ ' edit Show configuration file location');
858
+ console.log('');
859
+ console.log('Configuration file locations:');
860
+ console.log(` Configuration list: ${PROFILES_FILE}`);
861
+ console.log(` Claude settings: ${CLAUDE_SETTINGS}`);
862
+ console.log(` Environment variables file: ${ENV_FILE}`);
863
+ }
864
+
865
+ // Main program
866
+ async function main() {
867
+ const args = process.argv.slice(2);
868
+
869
+ // Handle global flags first (standardized behavior)
870
+ if (args.includes('--version') || args.includes('-V')) {
871
+ showVersion();
872
+ return;
873
+ }
874
+
875
+ if (args.includes('--help') || args.includes('-h')) {
876
+ help();
877
+ return;
878
+ }
879
+
880
+ // Extract flags
881
+ const showSecret = args.includes('--show-secret');
882
+ const filteredArgs = args.filter(
883
+ arg => arg !== '--show-secret' && arg !== '--version' && arg !== '-V' &&
884
+ arg !== '--help' && arg !== '-h');
885
+
886
+ const command = filteredArgs[0];
887
+
888
+ switch (command) {
889
+ case 'list':
890
+ case 'ls':
891
+ list();
892
+ break;
893
+ case 'use':
894
+ if (!filteredArgs[1]) {
895
+ console.error('Error: Missing configuration name');
896
+ console.error('Usage: ccconfig use <name>');
897
+ process.exit(1);
898
+ }
899
+ use(filteredArgs[1]);
900
+ break;
901
+ case 'add':
902
+ await add(filteredArgs[1]);
903
+ break;
904
+ case 'remove':
905
+ case 'rm':
906
+ remove(filteredArgs[1]);
907
+ break;
908
+ case 'current':
909
+ current(showSecret);
910
+ break;
911
+ case 'mode':
912
+ mode(filteredArgs[1]);
913
+ break;
914
+ case 'env':
915
+ env(filteredArgs[1] || 'bash');
916
+ break;
917
+ case 'edit':
918
+ edit();
919
+ break;
920
+ default:
921
+ if (!command) {
922
+ list();
923
+ } else {
924
+ console.error(`Error: Unknown command '${command}'`);
925
+ console.error('Run ccconfig --help to see help');
926
+ process.exit(1);
927
+ }
928
+ }
929
+ }
930
+
931
+ function showVersion() {
932
+ console.log(`ccconfig version ${PACKAGE_VERSION}`);
933
+ }
934
+
935
+ (async () => {
936
+ try {
937
+ await main();
938
+ } catch (error) {
939
+ console.error(`Error: ${error.message}`);
940
+ process.exit(1);
941
+ }
942
+ })();
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "ccconfig",
3
+ "version": "1.0.0",
4
+ "description": "Cross-platform Claude Code configuration switching tool",
5
+ "main": "ccconfig.js",
6
+ "bin": {
7
+ "ccconfig": "ccconfig.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node ccconfig.js --version"
11
+ },
12
+ "keywords": [
13
+ "claude",
14
+ "cli",
15
+ "config",
16
+ "environment"
17
+ ],
18
+ "author": "",
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ }
23
+ }