ccconfig 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ccconfig.js CHANGED
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const readline = require('readline');
7
+ const { spawn, execSync } = require('child_process');
7
8
 
8
9
  // Configuration file paths
9
10
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccconfig');
@@ -16,6 +17,80 @@ const MODE_FILE = path.join(CONFIG_DIR, 'mode');
16
17
  const MODE_SETTINGS = 'settings'; // Directly modify ~/.claude/settings.json
17
18
  const MODE_ENV = 'env'; // Use environment variable files
18
19
 
20
+ // Environment variable keys
21
+ const ENV_KEYS = {
22
+ BASE_URL: 'ANTHROPIC_BASE_URL',
23
+ AUTH_TOKEN: 'ANTHROPIC_AUTH_TOKEN',
24
+ API_KEY: 'ANTHROPIC_API_KEY',
25
+ MODEL: 'ANTHROPIC_MODEL',
26
+ SMALL_FAST_MODEL: 'ANTHROPIC_SMALL_FAST_MODEL'
27
+ };
28
+
29
+ // Sensitive keys that should be masked
30
+ const SENSITIVE_KEYS = [ENV_KEYS.AUTH_TOKEN, ENV_KEYS.API_KEY];
31
+
32
+ function getProfilesMap(profiles) {
33
+ return profiles && profiles.profiles ? profiles.profiles : {};
34
+ }
35
+
36
+ function isProfilesEmpty(profiles) {
37
+ return !profiles || Object.keys(getProfilesMap(profiles)).length === 0;
38
+ }
39
+
40
+ function ensureProfilesAvailable({onEmpty} = {}) {
41
+ const profiles = loadProfiles();
42
+ if (isProfilesEmpty(profiles)) {
43
+ if (typeof onEmpty === 'function') {
44
+ onEmpty();
45
+ } else {
46
+ console.error('Error: No configurations found');
47
+ console.error('Please add a configuration first: ccconfig add <name>');
48
+ }
49
+ process.exit(1);
50
+ }
51
+ return profiles;
52
+ }
53
+
54
+ function ensureProfileAvailable(
55
+ name, {allowEmptyEnv = false, onEmptyProfiles, onMissingProfile, onEmptyEnv} = {}) {
56
+ const profiles = ensureProfilesAvailable({onEmpty: onEmptyProfiles});
57
+ const profilesMap = getProfilesMap(profiles);
58
+ const profile = profilesMap[name];
59
+
60
+ if (!profile) {
61
+ if (typeof onMissingProfile === 'function') {
62
+ onMissingProfile();
63
+ } else {
64
+ console.error(`Error: Configuration '${name}' does not exist`);
65
+ console.error('');
66
+ console.error('Run ccconfig list to see available configurations');
67
+ }
68
+ process.exit(1);
69
+ }
70
+
71
+ if (!allowEmptyEnv && (!profile.env || Object.keys(profile.env).length === 0)) {
72
+ if (typeof onEmptyEnv === 'function') {
73
+ onEmptyEnv();
74
+ } else {
75
+ console.error(
76
+ `Error: Configuration '${name}' has empty environment variables`);
77
+ console.error('Please edit the configuration file to add env field');
78
+ }
79
+ process.exit(1);
80
+ }
81
+
82
+ return {profile, profiles};
83
+ }
84
+
85
+ // All supported commands
86
+ const COMMANDS = ['list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion'];
87
+
88
+ // ccconfig markers for shell config files
89
+ const SHELL_MARKERS = {
90
+ start: '# >>> ccconfig >>>',
91
+ end: '# <<< ccconfig <<<'
92
+ };
93
+
19
94
  let PACKAGE_VERSION = 'unknown';
20
95
  try {
21
96
  const packageJson = require('./package.json');
@@ -27,12 +102,225 @@ try {
27
102
  }
28
103
 
29
104
  /**
30
- * Ensure directory exists
105
+ * Ensure directory exists with secure permissions
31
106
  */
32
107
  function ensureDir(dir) {
33
108
  if (!fs.existsSync(dir)) {
34
- fs.mkdirSync(dir, {recursive: true});
109
+ fs.mkdirSync(dir, {recursive: true, mode: 0o700});
110
+ } else if (os.platform() !== 'win32') {
111
+ // Ensure existing directory has correct permissions
112
+ try {
113
+ fs.chmodSync(dir, 0o700);
114
+ } catch (e) {
115
+ // Ignore permission errors - may not own the directory
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Utility: Mask sensitive value for display
122
+ */
123
+ function maskValue(key, value, shouldMask = true) {
124
+ const v = String(value ?? '');
125
+ if (!v || v === '(not set)') return v;
126
+ if (!shouldMask || !SENSITIVE_KEYS.includes(key)) return v;
127
+ return v.length > 20 ? v.substring(0, 20) + '...' : v;
128
+ }
129
+
130
+ /**
131
+ * Utility: Print environment variable value (with optional masking)
132
+ */
133
+ function printEnvVar(key, value, mask = true) {
134
+ const v = String(value ?? '');
135
+ const displayValue = v ? maskValue(key, v, mask) : '(not set)';
136
+ console.log(` ${key}: ${displayValue}`);
137
+ }
138
+
139
+ /**
140
+ * Utility: Display environment variables with consistent formatting
141
+ */
142
+ function displayEnvVars(envVars, mask = true, indent = ' ') {
143
+ const keys = [ENV_KEYS.BASE_URL, ENV_KEYS.AUTH_TOKEN, ENV_KEYS.API_KEY, ENV_KEYS.MODEL, ENV_KEYS.SMALL_FAST_MODEL];
144
+ for (const key of keys) {
145
+ if (!(key in envVars)) continue;
146
+ const value = envVars[key];
147
+ if (!value && key !== ENV_KEYS.BASE_URL && key !== ENV_KEYS.AUTH_TOKEN && key !== ENV_KEYS.API_KEY) continue;
148
+ const displayValue = maskValue(key, value, mask);
149
+ console.log(`${indent}${key}: ${displayValue || '(not set)'}`);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Utility: Interactive readline helper
155
+ */
156
+ class ReadlineHelper {
157
+ constructor() {
158
+ this.rl = null;
159
+ }
160
+
161
+ ensureInterface() {
162
+ if (!this.rl) {
163
+ this.rl = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout
166
+ });
167
+ }
168
+ }
169
+
170
+ async ask(question, defaultValue = '', options = {}) {
171
+ this.ensureInterface();
172
+ const { brackets = 'parentheses' } = options;
173
+ const left = brackets === 'square' ? '[' : '(';
174
+ const right = brackets === 'square' ? ']' : ')';
175
+ const suffix = defaultValue ? ` ${left}${defaultValue}${right}` : '';
176
+
177
+ return new Promise(resolve => {
178
+ this.rl.question(`${question}${suffix}: `, answer => {
179
+ const trimmed = answer.trim();
180
+ resolve(trimmed || defaultValue);
181
+ });
182
+ });
183
+ }
184
+
185
+ async askEnvVars(existingEnv = {}) {
186
+ const baseUrl = await this.ask(
187
+ 'ANTHROPIC_BASE_URL (press Enter to keep current/default)',
188
+ existingEnv.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
189
+ { brackets: existingEnv.ANTHROPIC_BASE_URL ? 'square' : 'parentheses' }
190
+ );
191
+
192
+ const authToken = await this.ask(
193
+ 'ANTHROPIC_AUTH_TOKEN (press Enter to keep current/set empty)',
194
+ existingEnv.ANTHROPIC_AUTH_TOKEN || '',
195
+ { brackets: existingEnv.ANTHROPIC_AUTH_TOKEN ? 'square' : 'parentheses' }
196
+ );
197
+
198
+ const apiKey = await this.ask(
199
+ 'ANTHROPIC_API_KEY (press Enter to keep current/set empty)',
200
+ existingEnv.ANTHROPIC_API_KEY || '',
201
+ { brackets: existingEnv.ANTHROPIC_API_KEY ? 'square' : 'parentheses' }
202
+ );
203
+
204
+ const model = await this.ask(
205
+ 'ANTHROPIC_MODEL (press Enter to skip/keep current)',
206
+ existingEnv.ANTHROPIC_MODEL || '',
207
+ { brackets: existingEnv.ANTHROPIC_MODEL ? 'square' : 'parentheses' }
208
+ );
209
+
210
+ const smallFastModel = await this.ask(
211
+ 'ANTHROPIC_SMALL_FAST_MODEL (press Enter to skip/keep current)',
212
+ existingEnv.ANTHROPIC_SMALL_FAST_MODEL || '',
213
+ { brackets: existingEnv.ANTHROPIC_SMALL_FAST_MODEL ? 'square' : 'parentheses' }
214
+ );
215
+
216
+ const envVars = {
217
+ [ENV_KEYS.BASE_URL]: baseUrl || '',
218
+ [ENV_KEYS.AUTH_TOKEN]: authToken || '',
219
+ [ENV_KEYS.API_KEY]: apiKey || ''
220
+ };
221
+
222
+ if (model) envVars[ENV_KEYS.MODEL] = model;
223
+ if (smallFastModel) envVars[ENV_KEYS.SMALL_FAST_MODEL] = smallFastModel;
224
+
225
+ return envVars;
226
+ }
227
+
228
+ close() {
229
+ if (this.rl) {
230
+ this.rl.close();
231
+ this.rl = null;
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Utility: Check if terminal is interactive
238
+ */
239
+ function requireInteractive(commandName) {
240
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
241
+ if (!isInteractive) {
242
+ console.error(`Error: Interactive mode required for ${commandName}`);
243
+ console.error('This command must be run in an interactive terminal');
244
+ process.exit(1);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Utility: Display environment variables section for current command
250
+ */
251
+ function displayEnvSection(envVars, showSecret) {
252
+ if (!envVars || (!envVars[ENV_KEYS.BASE_URL] && !envVars[ENV_KEYS.AUTH_TOKEN] && !envVars[ENV_KEYS.API_KEY])) {
253
+ console.log(' (not configured)');
254
+ return;
255
+ }
256
+
257
+ const normalizedEnv = {
258
+ [ENV_KEYS.BASE_URL]: envVars[ENV_KEYS.BASE_URL] || '(not set)',
259
+ [ENV_KEYS.AUTH_TOKEN]: envVars[ENV_KEYS.AUTH_TOKEN] || envVars[ENV_KEYS.API_KEY] || '(not set)',
260
+ [ENV_KEYS.MODEL]: envVars[ENV_KEYS.MODEL],
261
+ [ENV_KEYS.SMALL_FAST_MODEL]: envVars[ENV_KEYS.SMALL_FAST_MODEL]
262
+ };
263
+
264
+ // Mask token if needed
265
+ if (normalizedEnv[ENV_KEYS.AUTH_TOKEN] !== '(not set)' && !showSecret) {
266
+ const token = normalizedEnv[ENV_KEYS.AUTH_TOKEN];
267
+ normalizedEnv[ENV_KEYS.AUTH_TOKEN] = token.substring(0, 20) + '...';
268
+ }
269
+
270
+ // Display with aligned columns
271
+ console.log(` ${ENV_KEYS.BASE_URL}: ${normalizedEnv[ENV_KEYS.BASE_URL]}`);
272
+ console.log(` ${ENV_KEYS.AUTH_TOKEN}: ${normalizedEnv[ENV_KEYS.AUTH_TOKEN]}`);
273
+ if (normalizedEnv[ENV_KEYS.MODEL]) {
274
+ console.log(` ${ENV_KEYS.MODEL}: ${normalizedEnv[ENV_KEYS.MODEL]}`);
275
+ }
276
+ if (normalizedEnv[ENV_KEYS.SMALL_FAST_MODEL]) {
277
+ console.log(` ${ENV_KEYS.SMALL_FAST_MODEL}: ${normalizedEnv[ENV_KEYS.SMALL_FAST_MODEL]}`);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Validate configuration name
283
+ * @param {string} name - Configuration name to validate
284
+ * @param {boolean} allowEmpty - Whether to allow empty names (default: false)
285
+ * @returns {boolean} - Returns true if valid, exits process if invalid
286
+ */
287
+ function validateConfigName(name, allowEmpty = false) {
288
+ if (!name || name.trim() === '') {
289
+ if (allowEmpty) {
290
+ return true;
291
+ }
292
+ console.error('Error: Configuration name cannot be empty');
293
+ process.exit(1);
35
294
  }
295
+
296
+ // Allow only alphanumeric characters, hyphens, and underscores
297
+ const CONFIG_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
298
+ if (!CONFIG_NAME_REGEX.test(name)) {
299
+ console.error(`Error: Invalid configuration name '${name}'`);
300
+ console.error('');
301
+ console.error('Configuration names can only contain:');
302
+ console.error(' • Letters (a-z, A-Z)');
303
+ console.error(' • Numbers (0-9)');
304
+ console.error(' • Hyphens (-)');
305
+ console.error(' • Underscores (_)');
306
+ console.error('');
307
+ console.error('Examples of valid names:');
308
+ console.error(' • work');
309
+ console.error(' • personal');
310
+ console.error(' • project-1');
311
+ console.error(' • staging_env');
312
+ process.exit(1);
313
+ }
314
+
315
+ // Limit length to prevent issues
316
+ const MAX_NAME_LENGTH = 50;
317
+ if (name.length > MAX_NAME_LENGTH) {
318
+ console.error(`Error: Configuration name too long (max ${MAX_NAME_LENGTH} characters)`);
319
+ console.error(`Current length: ${name.length}`);
320
+ process.exit(1);
321
+ }
322
+
323
+ return true;
36
324
  }
37
325
 
38
326
  /**
@@ -144,11 +432,9 @@ function updateClaudeSettings(envVars) {
144
432
  }
145
433
 
146
434
  // 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
- delete settings.env.ANTHROPIC_MODEL;
151
- delete settings.env.ANTHROPIC_SMALL_FAST_MODEL;
435
+ for (const key of Object.values(ENV_KEYS)) {
436
+ delete settings.env[key];
437
+ }
152
438
 
153
439
  // Set new environment variables
154
440
  Object.assign(settings.env, envVars);
@@ -162,8 +448,15 @@ function updateClaudeSettings(envVars) {
162
448
  function writeEnvFile(envVars) {
163
449
  try {
164
450
  ensureDir(CONFIG_DIR);
165
- const lines =
166
- Object.entries(envVars).map(([key, value]) => `${key}=${value}`);
451
+ const lines = Object.entries(envVars).map(([key, value]) => {
452
+ // Escape special characters to prevent injection
453
+ const escapedValue = String(value ?? '')
454
+ .replace(/\\/g, '\\\\')
455
+ .replace(/\n/g, '\\n')
456
+ .replace(/\r/g, '\\r')
457
+ .replace(/\t/g, '\\t');
458
+ return `${key}=${escapedValue}`;
459
+ });
167
460
  const content = lines.join('\n') + '\n';
168
461
  fs.writeFileSync(ENV_FILE, content, 'utf-8');
169
462
 
@@ -188,9 +481,18 @@ function readEnvFile() {
188
481
  const content = fs.readFileSync(ENV_FILE, 'utf-8');
189
482
  const env = {};
190
483
  content.split('\n').forEach(line => {
191
- const match = line.match(/^([^=]+)=(.*)$/);
484
+ // Only accept valid environment variable names: starts with letter or underscore,
485
+ // followed by letters, numbers, or underscores
486
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
192
487
  if (match) {
193
- env[match[1]] = match[2];
488
+ // Unescape special characters
489
+ // IMPORTANT: Must unescape \\\\ first to avoid double-unescaping
490
+ const unescapedValue = match[2]
491
+ .replace(/\\\\/g, '\\')
492
+ .replace(/\\n/g, '\n')
493
+ .replace(/\\r/g, '\r')
494
+ .replace(/\\t/g, '\t');
495
+ env[match[1]] = unescapedValue;
194
496
  }
195
497
  });
196
498
  return env;
@@ -352,249 +654,109 @@ function list() {
352
654
  * Add new configuration
353
655
  */
354
656
  async function add(name) {
355
- // Auto-initialize if needed
356
657
  initIfNeeded();
658
+ requireInteractive('adding configurations');
357
659
 
358
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
359
-
360
- if (!isInteractive) {
361
- console.error('Error: Interactive mode required for adding configurations');
362
- console.error('This command must be run in an interactive terminal');
363
- process.exit(1);
364
- }
365
-
366
- let rl = null;
367
-
368
- const askQuestion = (question, defaultValue = '') => {
369
- if (!rl) {
370
- rl = readline.createInterface(
371
- {input: process.stdin, output: process.stdout});
372
- }
373
- return new Promise(resolve => {
374
- const suffix = defaultValue ? ` (${defaultValue})` : '';
375
- rl.question(`${question}${suffix}: `, answer => {
376
- const trimmed = answer.trim();
377
- resolve(trimmed ? trimmed : defaultValue.trim());
378
- });
379
- });
380
- };
381
-
382
- let baseUrl, authToken, apiKey, model, smallFastModel;
383
- let profiles;
660
+ const helper = new ReadlineHelper();
384
661
 
385
662
  try {
386
663
  if (!name) {
387
- name = await askQuestion('Please enter configuration name (e.g., work)');
664
+ name = await helper.ask('Please enter configuration name (e.g., work)');
388
665
  }
389
666
 
390
- if (!name) {
391
- console.error('Error: Configuration name cannot be empty');
392
- process.exit(1);
393
- }
667
+ validateConfigName(name);
394
668
 
395
- // Check if configuration already exists before asking for details
396
- profiles = loadProfiles() || {profiles: {}};
669
+ const profiles = loadProfiles() || {profiles: {}};
670
+ const profilesMap = getProfilesMap(profiles);
397
671
 
398
- if (profiles.profiles[name]) {
672
+ if (profilesMap[name]) {
399
673
  console.error(`Error: Configuration '${name}' already exists`);
400
- console.error('To update, please edit the configuration file directly');
674
+ console.error('');
675
+ console.error('To modify this configuration, use one of:');
676
+ console.error(` ccconfig update ${name} # Interactive update`);
677
+ console.error(` ccconfig edit # Manual edit`);
401
678
  process.exit(1);
402
679
  }
403
680
 
404
- baseUrl = await askQuestion(
405
- 'Please enter ANTHROPIC_BASE_URL (press Enter for default)',
406
- 'https://api.anthropic.com');
407
-
408
- authToken =
409
- await askQuestion('Please enter ANTHROPIC_AUTH_TOKEN (press Enter to set as empty)');
681
+ console.log('Please enter the following information:');
682
+ console.log('');
410
683
 
411
- apiKey = await askQuestion('Please enter ANTHROPIC_API_KEY (press Enter to set as empty)');
684
+ const envVars = await helper.askEnvVars();
412
685
 
413
- model = await askQuestion('Please enter ANTHROPIC_MODEL (press Enter to skip)');
686
+ profiles.profiles[name] = {env: envVars};
687
+ saveProfiles(profiles);
414
688
 
415
- smallFastModel = await askQuestion('Please enter ANTHROPIC_SMALL_FAST_MODEL (press Enter to skip)');
689
+ console.log(`✓ Configuration '${name}' added`);
690
+ console.log('');
691
+ console.log('Run the following command to activate:');
692
+ console.log(` ccconfig use ${name}`);
693
+ console.log('');
694
+ console.log('Saved environment variables:');
695
+ displayEnvVars(envVars);
696
+ console.log('');
697
+ console.log('This information has been saved to:');
698
+ console.log(` ${PROFILES_FILE}`);
699
+ console.log('You can edit this file directly to further customize the profile:');
700
+ console.log(` vim ${PROFILES_FILE}`);
701
+ console.log('Or run ccconfig edit to open it with your preferred editor');
416
702
  } finally {
417
- if (rl) {
418
- rl.close();
419
- }
420
- }
421
-
422
- const envVars = {
423
- ANTHROPIC_BASE_URL: baseUrl || '',
424
- ANTHROPIC_AUTH_TOKEN: authToken || '',
425
- ANTHROPIC_API_KEY: apiKey || ''
426
- };
427
-
428
- // Add optional model variables if provided
429
- if (model) {
430
- envVars.ANTHROPIC_MODEL = model;
431
- }
432
- if (smallFastModel) {
433
- envVars.ANTHROPIC_SMALL_FAST_MODEL = smallFastModel;
434
- }
435
-
436
- profiles.profiles[name] = {env: envVars};
437
-
438
- saveProfiles(profiles);
439
- console.log(`✓ Configuration '${name}' added`);
440
- console.log('');
441
- console.log('Run the following command to activate:');
442
- console.log(` ccconfig use ${name}`);
443
- console.log('');
444
- console.log('Saved environment variables:');
445
- const safePrint = (key, value, mask = true) => {
446
- if (!value) {
447
- console.log(` ${key}: (not set)`);
448
- return;
449
- }
450
- if (!mask) {
451
- console.log(` ${key}: ${value}`);
452
- return;
453
- }
454
- const masked = value.length > 20 ? value.substring(0, 20) + '...' : value;
455
- console.log(` ${key}: ${masked}`);
456
- };
457
- safePrint('ANTHROPIC_BASE_URL', envVars.ANTHROPIC_BASE_URL, false);
458
- safePrint('ANTHROPIC_AUTH_TOKEN', envVars.ANTHROPIC_AUTH_TOKEN);
459
- safePrint('ANTHROPIC_API_KEY', envVars.ANTHROPIC_API_KEY);
460
- if (envVars.ANTHROPIC_MODEL) {
461
- safePrint('ANTHROPIC_MODEL', envVars.ANTHROPIC_MODEL, false);
703
+ helper.close();
462
704
  }
463
- if (envVars.ANTHROPIC_SMALL_FAST_MODEL) {
464
- safePrint('ANTHROPIC_SMALL_FAST_MODEL', envVars.ANTHROPIC_SMALL_FAST_MODEL, false);
465
- }
466
- console.log('');
467
- console.log('This information has been saved to:');
468
- console.log(` ${PROFILES_FILE}`);
469
- console.log(
470
- 'You can edit this file directly to further customize the profile:');
471
- console.log(` vim ${PROFILES_FILE}`);
472
- console.log('Or run ccconfig edit to open it with your preferred editor');
473
705
  }
474
706
 
475
707
  /**
476
708
  * Update existing configuration
477
709
  */
478
710
  async function update(name) {
479
- // Auto-initialize if needed
480
711
  initIfNeeded();
712
+ requireInteractive('updating configurations');
481
713
 
482
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
483
-
484
- if (!isInteractive) {
485
- console.error('Error: Interactive mode required for updating configurations');
486
- console.error('This command must be run in an interactive terminal');
487
- process.exit(1);
488
- }
489
-
490
- let rl = null;
491
-
492
- const askQuestion = (question, defaultValue = '') => {
493
- if (!rl) {
494
- rl = readline.createInterface(
495
- {input: process.stdin, output: process.stdout});
496
- }
497
- return new Promise(resolve => {
498
- const suffix = defaultValue ? ` [${defaultValue}]` : '';
499
- rl.question(`${question}${suffix}: `, answer => {
500
- const trimmed = answer.trim();
501
- resolve(trimmed ? trimmed : defaultValue);
502
- });
503
- });
504
- };
505
-
506
- let baseUrl, authToken, apiKey, model, smallFastModel;
507
- let profiles;
714
+ const helper = new ReadlineHelper();
508
715
 
509
716
  try {
510
717
  if (!name) {
511
- name = await askQuestion('Please enter configuration name to update');
512
- }
513
-
514
- if (!name) {
515
- console.error('Error: Configuration name cannot be empty');
516
- process.exit(1);
718
+ name = await helper.ask('Please enter configuration name to update');
517
719
  }
518
720
 
519
- // Check if configuration exists
520
- profiles = loadProfiles() || {profiles: {}};
721
+ validateConfigName(name);
521
722
 
522
- if (!profiles.profiles[name]) {
523
- console.error(`Error: Configuration '${name}' does not exist`);
524
- console.error('Run ccconfig list to see available configurations');
525
- console.error(`Or use 'ccconfig add ${name}' to create a new configuration`);
526
- process.exit(1);
527
- }
723
+ const {profile, profiles} = ensureProfileAvailable(name, {
724
+ allowEmptyEnv: true,
725
+ onEmptyProfiles: () => {
726
+ console.error('Error: Configuration file does not exist');
727
+ process.exit(1);
728
+ },
729
+ onMissingProfile: () => {
730
+ console.error(`Error: Configuration '${name}' does not exist`);
731
+ console.error('');
732
+ console.error('Run ccconfig list to see available configurations');
733
+ console.error(`Or use 'ccconfig add ${name}' to create a new configuration`);
734
+ process.exit(1);
735
+ }
736
+ });
528
737
 
529
- const existingProfile = profiles.profiles[name];
530
- const existingEnv = existingProfile.env || {};
738
+ const existingEnv = profile.env || {};
531
739
 
532
740
  console.log(`Updating configuration '${name}'`);
533
741
  console.log('Press Enter to keep current value/default, or enter new value to update');
534
742
  console.log('');
535
743
 
536
- baseUrl = await askQuestion(
537
- 'ANTHROPIC_BASE_URL (press Enter to keep current/default)',
538
- existingEnv.ANTHROPIC_BASE_URL || 'https://api.anthropic.com');
539
-
540
- authToken =
541
- await askQuestion('ANTHROPIC_AUTH_TOKEN (press Enter to keep current/set empty)', existingEnv.ANTHROPIC_AUTH_TOKEN || '');
744
+ const envVars = await helper.askEnvVars(existingEnv);
542
745
 
543
- apiKey = await askQuestion('ANTHROPIC_API_KEY (press Enter to keep current/set empty)', existingEnv.ANTHROPIC_API_KEY || '');
746
+ const profilesMap = getProfilesMap(profiles);
747
+ profilesMap[name] = {env: envVars};
748
+ saveProfiles(profiles);
544
749
 
545
- model = await askQuestion('ANTHROPIC_MODEL (press Enter to skip/keep current)', existingEnv.ANTHROPIC_MODEL || '');
546
-
547
- smallFastModel = await askQuestion('ANTHROPIC_SMALL_FAST_MODEL (press Enter to skip/keep current)', existingEnv.ANTHROPIC_SMALL_FAST_MODEL || '');
750
+ console.log(`✓ Configuration '${name}' updated`);
751
+ console.log('');
752
+ console.log('Updated environment variables:');
753
+ displayEnvVars(envVars);
754
+ console.log('');
755
+ console.log('Run the following command to activate:');
756
+ console.log(` ccconfig use ${name}`);
548
757
  } finally {
549
- if (rl) {
550
- rl.close();
551
- }
552
- }
553
-
554
- const envVars = {
555
- ANTHROPIC_BASE_URL: baseUrl || '',
556
- ANTHROPIC_AUTH_TOKEN: authToken || '',
557
- ANTHROPIC_API_KEY: apiKey || ''
558
- };
559
-
560
- // Add optional model variables if provided
561
- if (model) {
562
- envVars.ANTHROPIC_MODEL = model;
758
+ helper.close();
563
759
  }
564
- if (smallFastModel) {
565
- envVars.ANTHROPIC_SMALL_FAST_MODEL = smallFastModel;
566
- }
567
-
568
- profiles.profiles[name] = {env: envVars};
569
-
570
- saveProfiles(profiles);
571
- console.log(`✓ Configuration '${name}' updated`);
572
- console.log('');
573
- console.log('Updated environment variables:');
574
- const safePrint = (key, value, mask = true) => {
575
- if (!value) {
576
- console.log(` ${key}: (not set)`);
577
- return;
578
- }
579
- if (!mask) {
580
- console.log(` ${key}: ${value}`);
581
- return;
582
- }
583
- const masked = value.length > 20 ? value.substring(0, 20) + '...' : value;
584
- console.log(` ${key}: ${masked}`);
585
- };
586
- safePrint('ANTHROPIC_BASE_URL', envVars.ANTHROPIC_BASE_URL, false);
587
- safePrint('ANTHROPIC_AUTH_TOKEN', envVars.ANTHROPIC_AUTH_TOKEN);
588
- safePrint('ANTHROPIC_API_KEY', envVars.ANTHROPIC_API_KEY);
589
- if (envVars.ANTHROPIC_MODEL) {
590
- safePrint('ANTHROPIC_MODEL', envVars.ANTHROPIC_MODEL, false);
591
- }
592
- if (envVars.ANTHROPIC_SMALL_FAST_MODEL) {
593
- safePrint('ANTHROPIC_SMALL_FAST_MODEL', envVars.ANTHROPIC_SMALL_FAST_MODEL, false);
594
- }
595
- console.log('');
596
- console.log('Run the following command to activate:');
597
- console.log(` ccconfig use ${name}`);
598
760
  }
599
761
 
600
762
  /**
@@ -607,19 +769,20 @@ function remove(name) {
607
769
  process.exit(1);
608
770
  }
609
771
 
610
- const profiles = loadProfiles();
772
+ // Validate configuration name
773
+ validateConfigName(name);
611
774
 
612
- if (!profiles) {
613
- console.error('Error: Configuration file does not exist');
614
- process.exit(1);
615
- }
616
-
617
- if (!profiles.profiles[name]) {
618
- console.error(`Error: Configuration '${name}' does not exist`);
619
- process.exit(1);
620
- }
775
+ const {profiles} = ensureProfileAvailable(name, {
776
+ allowEmptyEnv: true,
777
+ onEmptyProfiles: () => {
778
+ console.error('Error: Configuration file does not exist');
779
+ },
780
+ onMissingProfile: () => {
781
+ console.error(`Error: Configuration '${name}' does not exist`);
782
+ }
783
+ });
621
784
 
622
- delete profiles.profiles[name];
785
+ delete getProfilesMap(profiles)[name];
623
786
  saveProfiles(profiles);
624
787
  console.log(`✓ Configuration '${name}' removed`);
625
788
  }
@@ -628,94 +791,136 @@ function remove(name) {
628
791
  * Detect current shell and return recommended activation command
629
792
  */
630
793
  function detectShellCommand() {
631
- const shellPath = (process.env.SHELL || '').toLowerCase();
632
-
633
- if (process.env.FISH_VERSION || shellPath.includes('fish')) {
634
- return {shell: 'fish', command: 'ccconfig env fish | source'};
635
- }
636
-
637
- if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
638
- shellPath.includes('zsh')) {
639
- return {shell: 'zsh', command: 'eval $(ccconfig env bash)'};
794
+ const shellType = ShellUtils.detectType();
795
+ if (!shellType) {
796
+ return {shell: null, command: null};
640
797
  }
798
+ const command = ShellUtils.getActivationCommand(shellType);
799
+ const shellName = shellType === 'powershell' ? 'PowerShell' : shellType;
800
+ return {shell: shellName, command};
801
+ }
641
802
 
642
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
643
- shellPath.includes('pwsh') || shellPath.includes('powershell')) {
644
- return {shell: 'PowerShell', command: 'ccconfig env pwsh | iex'};
645
- }
803
+ /**
804
+ * Shell utilities - unified shell detection, escaping, and formatting
805
+ */
806
+ const ShellUtils = {
807
+ // Escape functions for different shells
808
+ escape: {
809
+ posix: (value) => {
810
+ const str = value == null ? '' : String(value);
811
+ return `'${str.replace(/'/g, `'"'"'`)}'`;
812
+ },
813
+ fish: (value) => {
814
+ const str = value == null ? '' : String(value);
815
+ return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
816
+ },
817
+ pwsh: (value) => {
818
+ const str = value == null ? '' : String(value);
819
+ return `'${str.replace(/'/g, `''`)}'`;
820
+ }
821
+ },
646
822
 
647
- if (shellPath.includes('bash')) {
648
- return {shell: 'bash', command: 'eval $(ccconfig env bash)'};
649
- }
823
+ // Detect current shell type
824
+ detectType: () => {
825
+ const shellPath = (process.env.SHELL || '').toLowerCase();
650
826
 
651
- if (process.platform === 'win32') {
652
- const comSpec = (process.env.ComSpec || '').toLowerCase();
653
- if (comSpec.includes('powershell')) {
654
- return {shell: 'PowerShell', command: 'ccconfig env pwsh | iex'};
827
+ if (process.env.FISH_VERSION || shellPath.includes('fish')) {
828
+ return 'fish';
655
829
  }
830
+ if (process.env.ZSH_NAME || process.env.ZSH_VERSION || shellPath.includes('zsh')) {
831
+ return 'zsh';
832
+ }
833
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL || shellPath.includes('pwsh') || shellPath.includes('powershell')) {
834
+ return 'powershell';
835
+ }
836
+ if (shellPath.includes('bash')) {
837
+ return 'bash';
838
+ }
839
+ if (process.platform === 'win32') {
840
+ const comSpec = (process.env.ComSpec || '').toLowerCase();
841
+ if (comSpec.includes('powershell')) {
842
+ return 'powershell';
843
+ }
844
+ }
845
+ return null;
846
+ },
847
+
848
+ // Get shell config file path
849
+ getConfigPath: (shellType) => {
850
+ const homeDir = os.homedir();
851
+ const configs = {
852
+ fish: path.join(homeDir, '.config', 'fish', 'config.fish'),
853
+ zsh: path.join(homeDir, '.zshrc'),
854
+ bash: process.platform === 'darwin'
855
+ ? (fs.existsSync(path.join(homeDir, '.bash_profile')) || !fs.existsSync(path.join(homeDir, '.bashrc'))
856
+ ? path.join(homeDir, '.bash_profile')
857
+ : path.join(homeDir, '.bashrc'))
858
+ : path.join(homeDir, '.bashrc'),
859
+ powershell: process.platform === 'win32'
860
+ ? path.join(process.env.USERPROFILE || homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
861
+ : path.join(homeDir, '.config', 'powershell', 'profile.ps1')
862
+ };
863
+ return configs[shellType];
864
+ },
865
+
866
+ // Get activation command for specific shell
867
+ getActivationCommand: (shellType) => {
868
+ const commands = {
869
+ fish: 'ccconfig env fish | source',
870
+ zsh: 'eval $(ccconfig env bash)',
871
+ bash: 'eval $(ccconfig env bash)',
872
+ powershell: 'ccconfig env pwsh | iex'
873
+ };
874
+ return commands[shellType];
875
+ },
876
+
877
+ // Format environment variables for specific shell
878
+ formatEnvVars: (envVars, format) => {
879
+ const lines = [];
880
+ for (const [key, value] of Object.entries(envVars)) {
881
+ switch (format) {
882
+ case 'fish':
883
+ lines.push(`set -gx ${key} "${ShellUtils.escape.fish(value)}"`);
884
+ break;
885
+ case 'bash':
886
+ case 'zsh':
887
+ case 'sh':
888
+ lines.push(`export ${key}=${ShellUtils.escape.posix(value)}`);
889
+ break;
890
+ case 'powershell':
891
+ case 'pwsh':
892
+ lines.push(`$env:${key}=${ShellUtils.escape.pwsh(value)}`);
893
+ break;
894
+ case 'dotenv':
895
+ const renderedValue = value == null ? '' : String(value);
896
+ const escapedValue = renderedValue
897
+ .replace(/\\/g, '\\\\')
898
+ .replace(/\n/g, '\\n')
899
+ .replace(/\r/g, '\\r')
900
+ .replace(/\t/g, '\\t');
901
+ lines.push(`${key}=${escapedValue}`);
902
+ break;
903
+ }
904
+ }
905
+ return lines;
656
906
  }
907
+ };
657
908
 
658
- return {shell: null, command: null};
659
- }
660
-
661
- function escapePosix(value) {
662
- const str = value == null ? '' : String(value);
663
- return `'${str.replace(/'/g, `'"'"'`)}'`;
664
- }
665
-
666
- function escapeFish(value) {
667
- const str = value == null ? '' : String(value);
668
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
669
- }
670
-
671
- function escapePwsh(value) {
672
- const str = value == null ? '' : String(value);
673
- return `'${str.replace(/'/g, `''`)}'`;
674
- }
909
+ // Legacy function wrappers for backward compatibility
910
+ function escapePosix(value) { return ShellUtils.escape.posix(value); }
911
+ function escapeFish(value) { return ShellUtils.escape.fish(value); }
912
+ function escapePwsh(value) { return ShellUtils.escape.pwsh(value); }
675
913
 
676
914
  /**
677
915
  * Detect shell type and config file path
678
916
  */
679
917
  function detectShellConfig() {
680
- const shellPath = (process.env.SHELL || '').toLowerCase();
681
- const homeDir = os.homedir();
682
-
683
- if (process.env.FISH_VERSION || shellPath.includes('fish')) {
684
- const configPath = path.join(homeDir, '.config', 'fish', 'config.fish');
685
- return {shell: 'fish', configPath, detected: true};
686
- }
687
-
688
- if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
689
- shellPath.includes('zsh')) {
690
- const configPath = path.join(homeDir, '.zshrc');
691
- return {shell: 'zsh', configPath, detected: true};
918
+ const shellType = ShellUtils.detectType();
919
+ if (!shellType) {
920
+ return {shell: null, configPath: null, detected: false};
692
921
  }
693
-
694
- if (shellPath.includes('bash')) {
695
- if (process.platform === 'darwin') {
696
- const bashProfile = path.join(homeDir, '.bash_profile');
697
- const bashrc = path.join(homeDir, '.bashrc');
698
- const configPath = fs.existsSync(bashProfile) || !fs.existsSync(bashrc) ?
699
- bashProfile :
700
- bashrc;
701
- return {shell: 'bash', configPath, detected: true};
702
- }
703
- const configPath = path.join(homeDir, '.bashrc');
704
- return {shell: 'bash', configPath, detected: true};
705
- }
706
-
707
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
708
- shellPath.includes('pwsh') || shellPath.includes('powershell')) {
709
- // PowerShell profile path varies by OS
710
- const configPath = process.platform === 'win32' ?
711
- path.join(
712
- process.env.USERPROFILE || homeDir, 'Documents', 'PowerShell',
713
- 'Microsoft.PowerShell_profile.ps1') :
714
- path.join(homeDir, '.config', 'powershell', 'profile.ps1');
715
- return {shell: 'powershell', configPath, detected: true};
716
- }
717
-
718
- return {shell: null, configPath: null, detected: false};
922
+ const configPath = ShellUtils.getConfigPath(shellType);
923
+ return {shell: shellType, configPath, detected: true};
719
924
  }
720
925
 
721
926
  /**
@@ -732,37 +937,20 @@ async function writePermanentEnv(envVars) {
732
937
  }
733
938
 
734
939
  const {shell, configPath} = shellConfig;
735
- const marker = '# >>> ccconfig >>>';
736
- const markerEnd = '# <<< ccconfig <<<';
940
+ const marker = SHELL_MARKERS.start;
941
+ const markerEnd = SHELL_MARKERS.end;
737
942
 
738
- // Generate environment variable lines
739
- let envBlock = '';
740
- switch (shell) {
741
- case 'fish':
742
- envBlock = `${marker}\n`;
743
- for (const [key, value] of Object.entries(envVars)) {
744
- envBlock += `set -gx ${key} "${escapeFish(value)}"\n`;
745
- }
746
- envBlock += `${markerEnd}\n`;
747
- break;
943
+ // Generate environment variable lines (real and masked)
944
+ const maskedEnvVars = {};
945
+ for (const [key, value] of Object.entries(envVars)) {
946
+ maskedEnvVars[key] = maskValue(key, value, true);
947
+ }
748
948
 
749
- case 'bash':
750
- case 'zsh':
751
- envBlock = `${marker}\n`;
752
- for (const [key, value] of Object.entries(envVars)) {
753
- envBlock += `export ${key}=${escapePosix(value)}\n`;
754
- }
755
- envBlock += `${markerEnd}\n`;
756
- break;
949
+ const envLines = ShellUtils.formatEnvVars(envVars, shell);
950
+ const maskedEnvLines = ShellUtils.formatEnvVars(maskedEnvVars, shell);
757
951
 
758
- case 'powershell':
759
- envBlock = `${marker}\n`;
760
- for (const [key, value] of Object.entries(envVars)) {
761
- envBlock += `$env:${key}=${escapePwsh(value)}\n`;
762
- }
763
- envBlock += `${markerEnd}\n`;
764
- break;
765
- }
952
+ const envBlock = `${marker}\n${envLines.join('\n')}\n${markerEnd}\n`;
953
+ const maskedEnvBlock = `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
766
954
 
767
955
  // Display warning and confirmation
768
956
  console.log('');
@@ -773,7 +961,7 @@ async function writePermanentEnv(envVars) {
773
961
  console.log('');
774
962
  console.log('The following block will be added/updated:');
775
963
  console.log('───────────────────────────────────────────');
776
- console.log(envBlock.trim());
964
+ console.log(maskedEnvBlock.trim());
777
965
  console.log('───────────────────────────────────────────');
778
966
  console.log('');
779
967
  console.log('What this does:');
@@ -891,29 +1079,25 @@ async function writePermanentEnv(envVars) {
891
1079
  * Switch configuration
892
1080
  */
893
1081
  async function use(name, options = {}) {
894
- const profiles = loadProfiles();
895
-
896
- if (!profiles || !profiles.profiles ||
897
- Object.keys(profiles.profiles).length === 0) {
898
- console.error('Error: No configurations found');
899
- console.error('Please add a configuration first: ccconfig add <name>');
900
- process.exit(1);
901
- }
902
-
903
- if (!profiles.profiles[name]) {
904
- console.error(`Error: Configuration '${name}' does not exist`);
905
- console.error('Run ccconfig list to see available configurations');
906
- process.exit(1);
907
- }
908
-
909
- const profile = profiles.profiles[name];
910
-
911
- if (!profile.env || Object.keys(profile.env).length === 0) {
912
- console.error(
913
- `Error: Configuration '${name}' has empty environment variables`);
914
- console.error('Please edit the configuration file to add env field');
915
- process.exit(1);
916
- }
1082
+ // Validate configuration name
1083
+ validateConfigName(name);
1084
+
1085
+ const {profile} = ensureProfileAvailable(name, {
1086
+ onEmptyProfiles: () => {
1087
+ console.error('Error: No configurations found');
1088
+ console.error('Please add a configuration first: ccconfig add <name>');
1089
+ },
1090
+ onMissingProfile: () => {
1091
+ console.error(`Error: Configuration '${name}' does not exist`);
1092
+ console.error('');
1093
+ console.error('Run ccconfig list to see available configurations');
1094
+ },
1095
+ onEmptyEnv: () => {
1096
+ console.error(
1097
+ `Error: Configuration '${name}' has empty environment variables`);
1098
+ console.error('Please edit the configuration file to add env field');
1099
+ }
1100
+ });
917
1101
 
918
1102
  const mode = getMode();
919
1103
  const permanent = options.permanent || false;
@@ -924,11 +1108,7 @@ async function use(name, options = {}) {
924
1108
 
925
1109
  console.log(`✓ Switched to configuration: ${name} (settings mode)`);
926
1110
  console.log(` Environment variables:`);
927
- for (const [key, value] of Object.entries(profile.env)) {
928
- const displayValue =
929
- value.length > 20 ? value.substring(0, 20) + '...' : value;
930
- console.log(` ${key}: ${displayValue}`);
931
- }
1111
+ displayEnvVars(profile.env, true, ' ');
932
1112
  console.log('');
933
1113
  console.log('Configuration written to ~/.claude/settings.json');
934
1114
  console.log('Restart Claude Code to make configuration take effect');
@@ -944,11 +1124,7 @@ async function use(name, options = {}) {
944
1124
 
945
1125
  console.log(`✓ Switched to configuration: ${name} (env mode)`);
946
1126
  console.log(` Environment variables:`);
947
- for (const [key, value] of Object.entries(profile.env)) {
948
- const displayValue =
949
- value.length > 20 ? value.substring(0, 20) + '...' : value;
950
- console.log(` ${key}: ${displayValue}`);
951
- }
1127
+ displayEnvVars(profile.env, true, ' ');
952
1128
  console.log('');
953
1129
  console.log(`Environment variable file updated: ${ENV_FILE}`);
954
1130
 
@@ -1029,74 +1205,17 @@ function current(showSecret = false) {
1029
1205
 
1030
1206
  // Display settings.json configuration
1031
1207
  console.log('【1】~/.claude/settings.json:');
1032
- if (settings.env &&
1033
- (settings.env.ANTHROPIC_BASE_URL || settings.env.ANTHROPIC_AUTH_TOKEN)) {
1034
- const baseUrl = settings.env.ANTHROPIC_BASE_URL || '(not set)';
1035
- const authToken = settings.env.ANTHROPIC_AUTH_TOKEN || '(not set)';
1036
- const maskedToken = (authToken === '(not set)' || showSecret) ?
1037
- authToken :
1038
- authToken.substring(0, 20) + '...';
1039
-
1040
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
1041
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
1042
- if (settings.env.ANTHROPIC_MODEL) {
1043
- console.log(` ANTHROPIC_MODEL: ${settings.env.ANTHROPIC_MODEL}`);
1044
- }
1045
- if (settings.env.ANTHROPIC_SMALL_FAST_MODEL) {
1046
- console.log(` ANTHROPIC_SMALL_FAST_MODEL: ${settings.env.ANTHROPIC_SMALL_FAST_MODEL}`);
1047
- }
1048
- } else {
1049
- console.log(' (not configured)');
1050
- }
1208
+ displayEnvSection(settings.env, showSecret);
1051
1209
  console.log('');
1052
1210
 
1053
1211
  // Display environment variable file configuration
1054
1212
  console.log(`【2】Environment Variables File (${ENV_FILE}):`);
1055
- if (envFile &&
1056
- (envFile.ANTHROPIC_BASE_URL || envFile.ANTHROPIC_AUTH_TOKEN ||
1057
- envFile.ANTHROPIC_API_KEY)) {
1058
- const baseUrl = envFile.ANTHROPIC_BASE_URL || '(not set)';
1059
- const authToken = envFile.ANTHROPIC_AUTH_TOKEN ||
1060
- envFile.ANTHROPIC_API_KEY || '(not set)';
1061
- const maskedToken = (authToken === '(not set)' || showSecret) ?
1062
- authToken :
1063
- authToken.substring(0, 20) + '...';
1064
-
1065
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
1066
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
1067
- if (envFile.ANTHROPIC_MODEL) {
1068
- console.log(` ANTHROPIC_MODEL: ${envFile.ANTHROPIC_MODEL}`);
1069
- }
1070
- if (envFile.ANTHROPIC_SMALL_FAST_MODEL) {
1071
- console.log(` ANTHROPIC_SMALL_FAST_MODEL: ${envFile.ANTHROPIC_SMALL_FAST_MODEL}`);
1072
- }
1073
- } else {
1074
- console.log(' (not configured)');
1075
- }
1213
+ displayEnvSection(envFile, showSecret);
1076
1214
  console.log('');
1077
1215
 
1078
1216
  // Display current process environment variables
1079
1217
  console.log('【3】Current Process Environment Variables:');
1080
- if (processEnv.ANTHROPIC_BASE_URL || processEnv.ANTHROPIC_AUTH_TOKEN ||
1081
- processEnv.ANTHROPIC_API_KEY) {
1082
- const baseUrl = processEnv.ANTHROPIC_BASE_URL || '(not set)';
1083
- const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ||
1084
- processEnv.ANTHROPIC_API_KEY || '(not set)';
1085
- const maskedToken = (authToken === '(not set)' || showSecret) ?
1086
- authToken :
1087
- authToken.substring(0, 20) + '...';
1088
-
1089
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
1090
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
1091
- if (processEnv.ANTHROPIC_MODEL) {
1092
- console.log(` ANTHROPIC_MODEL: ${processEnv.ANTHROPIC_MODEL}`);
1093
- }
1094
- if (processEnv.ANTHROPIC_SMALL_FAST_MODEL) {
1095
- console.log(` ANTHROPIC_SMALL_FAST_MODEL: ${processEnv.ANTHROPIC_SMALL_FAST_MODEL}`);
1096
- }
1097
- } else {
1098
- console.log(' (not set)');
1099
- }
1218
+ displayEnvSection(processEnv, showSecret);
1100
1219
  console.log('');
1101
1220
 
1102
1221
  // Display notes
@@ -1200,45 +1319,147 @@ function env(format = 'bash') {
1200
1319
  const envVars = getActiveEnvVars();
1201
1320
 
1202
1321
  if (!envVars || Object.keys(envVars).length === 0) {
1203
- console.error(
1204
- 'Error: No available environment variable configuration found');
1205
- console.error(
1206
- 'Please run ccconfig use <name> to select a configuration first');
1322
+ console.error('Error: No available environment variable configuration found');
1323
+ console.error('Please run ccconfig use <name> to select a configuration first');
1207
1324
  process.exit(1);
1208
1325
  }
1209
1326
 
1210
- // Output all environment variables
1211
- switch (format) {
1212
- case 'fish':
1213
- for (const [key, value] of Object.entries(envVars)) {
1214
- console.log(`set -gx ${key} "${escapeFish(value)}"`);
1215
- }
1216
- break;
1217
- case 'bash':
1218
- case 'zsh':
1219
- case 'sh':
1220
- for (const [key, value] of Object.entries(envVars)) {
1221
- console.log(`export ${key}=${escapePosix(value)}`);
1222
- }
1223
- break;
1224
- case 'powershell':
1225
- case 'pwsh':
1226
- for (const [key, value] of Object.entries(envVars)) {
1227
- console.log(`$env:${key}=${escapePwsh(value)}`);
1228
- }
1229
- break;
1230
- case 'dotenv':
1231
- for (const [key, value] of Object.entries(envVars)) {
1232
- const renderedValue = value == null ? '' : String(value);
1233
- console.log(`${key}=${renderedValue}`);
1234
- }
1235
- break;
1236
- default:
1237
- console.error(`Error: Unsupported format: ${format}`);
1327
+ const supportedFormats = ['fish', 'bash', 'zsh', 'sh', 'powershell', 'pwsh', 'dotenv'];
1328
+ if (!supportedFormats.includes(format)) {
1329
+ console.error(`Error: Unsupported format: ${format}`);
1330
+ console.error(`Supported formats: ${supportedFormats.join(', ')}`);
1331
+ process.exit(1);
1332
+ }
1333
+
1334
+ const lines = ShellUtils.formatEnvVars(envVars, format);
1335
+ lines.forEach(line => console.log(line));
1336
+ }
1337
+
1338
+ /**
1339
+ * Start Claude Code with specified profile (internal implementation)
1340
+ * @param {string} name - Profile name
1341
+ * @param {Array} extraArgs - Additional arguments to pass to Claude
1342
+ * @param {Object} options - Options object
1343
+ * @param {boolean} options.safe - Whether to run in safe mode (default: false)
1344
+ */
1345
+ function startClaude(name, extraArgs = [], options = {}) {
1346
+ const { safe = false } = options;
1347
+ const commandName = safe ? 'safe-start' : 'start';
1348
+
1349
+ if (!name) {
1350
+ console.error('Error: Missing configuration name');
1351
+ console.error(`Usage: ccconfig ${commandName} <name> [claude-args...]`);
1352
+ process.exit(1);
1353
+ }
1354
+
1355
+ // Validate configuration name
1356
+ validateConfigName(name);
1357
+
1358
+ const {profile} = ensureProfileAvailable(name, {
1359
+ onEmptyProfiles: () => {
1360
+ console.error('Error: No configurations found');
1361
+ console.error('Please add a configuration first: ccconfig add <name>');
1362
+ },
1363
+ onMissingProfile: () => {
1364
+ console.error(`Error: Configuration '${name}' does not exist`);
1365
+ console.error('Run ccconfig list to see available configurations');
1366
+ },
1367
+ onEmptyEnv: () => {
1238
1368
  console.error(
1239
- 'Supported formats: fish, bash, zsh, sh, powershell, pwsh, dotenv');
1240
- process.exit(1);
1369
+ `Error: Configuration '${name}' has empty environment variables`);
1370
+ console.error('Please edit the configuration file to add env field');
1371
+ }
1372
+ });
1373
+
1374
+ // Check if claude binary exists before proceeding
1375
+ try {
1376
+ const command = process.platform === 'win32' ? 'where claude' : 'which claude';
1377
+ execSync(command, { stdio: 'pipe' });
1378
+ } catch (err) {
1379
+ console.error('Error: Claude Code CLI not found');
1380
+ console.error('');
1381
+ console.error('Please make sure Claude Code CLI is installed:');
1382
+ console.error(' npm install -g claude-code');
1383
+ process.exit(1);
1384
+ }
1385
+
1386
+ // Display startup message
1387
+ const modeLabel = safe ? ' (safe mode)' : '';
1388
+ console.log(`Starting Claude Code with profile: ${name}${modeLabel}`);
1389
+ console.log('Environment variables:');
1390
+ for (const [key, value] of Object.entries(profile.env)) {
1391
+ const strValue = String(value ?? '');
1392
+ const displayValue =
1393
+ strValue.length > 20 ? strValue.substring(0, 20) + '...' : strValue;
1394
+ console.log(` ${key}: ${displayValue}`);
1395
+ }
1396
+
1397
+ // Build Claude arguments based on mode
1398
+ const claudeArgs = safe ? extraArgs : ['--dangerously-skip-permissions', ...extraArgs];
1399
+
1400
+ // Display mode-specific notes
1401
+ console.log('');
1402
+ if (safe) {
1403
+ console.log('Note: Running in safe mode (permission confirmation required)');
1404
+ console.log(' Claude Code will ask for confirmation before executing commands');
1405
+ console.log(' For automatic execution, use "ccconfig start" instead');
1406
+ } else {
1407
+ console.log('Note: Starting with --dangerously-skip-permissions flag enabled');
1408
+ console.log(' This allows Claude Code to execute commands without confirmation prompts');
1409
+ console.log(' Only use this with profiles you trust');
1241
1410
  }
1411
+ console.log('');
1412
+
1413
+ if (extraArgs.length > 0) {
1414
+ const argsLabel = safe ? 'Arguments' : 'Additional arguments';
1415
+ console.log(`${argsLabel}: ${extraArgs.join(' ')}`);
1416
+ console.log('');
1417
+ }
1418
+
1419
+ // Merge profile env vars with current process env
1420
+ // Normalize all profile env values to strings (spawn requires string values)
1421
+ const normalizedEnv = {};
1422
+ for (const [key, value] of Object.entries(profile.env)) {
1423
+ normalizedEnv[key] = String(value ?? '');
1424
+ }
1425
+ const envVars = {...process.env, ...normalizedEnv};
1426
+
1427
+ // Spawn claude process
1428
+ const claude = spawn('claude', claudeArgs, {
1429
+ env: envVars,
1430
+ stdio: 'inherit' // Inherit stdin, stdout, stderr from parent process
1431
+ });
1432
+
1433
+ // Handle process exit
1434
+ claude.on('close', (code) => {
1435
+ if (code !== 0 && code !== null) {
1436
+ console.error(`Claude Code exited with code ${code}`);
1437
+ process.exit(code);
1438
+ }
1439
+ process.exit(0);
1440
+ });
1441
+
1442
+ claude.on('error', (err) => {
1443
+ console.error(`Error starting Claude Code: ${err.message}`);
1444
+ console.error('');
1445
+ console.error('Please make sure Claude Code CLI is installed:');
1446
+ console.error(' npm install -g claude-code');
1447
+ process.exit(1);
1448
+ });
1449
+ }
1450
+
1451
+ /**
1452
+ * Start Claude Code with specified profile (auto-approve mode)
1453
+ */
1454
+ function start(name, extraArgs = []) {
1455
+ return startClaude(name, extraArgs, { safe: false });
1456
+ }
1457
+
1458
+ /**
1459
+ * Start Claude Code with specified profile (safe mode - requires permission confirmation)
1460
+ */
1461
+ function safeStart(name, extraArgs = []) {
1462
+ return startClaude(name, extraArgs, { safe: true });
1242
1463
  }
1243
1464
 
1244
1465
  /**
@@ -1257,7 +1478,7 @@ function completion(shell) {
1257
1478
  process.exit(1);
1258
1479
  }
1259
1480
 
1260
- const commands = 'list ls add update use remove rm current mode env edit';
1481
+ const commands = COMMANDS.join(' ');
1261
1482
 
1262
1483
  switch (shell) {
1263
1484
  case 'bash':
@@ -1280,7 +1501,7 @@ _ccconfig_completions() {
1280
1501
  ;;
1281
1502
  2)
1282
1503
  case "\${prev}" in
1283
- use|update|remove|rm)
1504
+ use|start|safe-start|update|remove|rm)
1284
1505
  COMPREPLY=( $(compgen -W "\${profiles}" -- \${cur}) )
1285
1506
  ;;
1286
1507
  mode)
@@ -1318,6 +1539,8 @@ _ccconfig() {
1318
1539
  'add:Add new configuration'
1319
1540
  'update:Update existing configuration'
1320
1541
  'use:Switch to specified configuration'
1542
+ 'start:Start Claude Code (auto-approve mode)'
1543
+ 'safe-start:Start Claude Code (safe mode, requires confirmation)'
1321
1544
  'remove:Remove configuration'
1322
1545
  'rm:Remove configuration'
1323
1546
  'current:Display current configuration'
@@ -1340,7 +1563,7 @@ _ccconfig() {
1340
1563
  ;;
1341
1564
  3)
1342
1565
  case $words[2] in
1343
- use|update|remove|rm)
1566
+ use|start|safe-start|update|remove|rm)
1344
1567
  _describe 'profile' profiles
1345
1568
  ;;
1346
1569
  mode)
@@ -1377,6 +1600,8 @@ complete -c ccconfig -f -n "__fish_use_subcommand" -a "ls" -d "List all configur
1377
1600
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "add" -d "Add new configuration"
1378
1601
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "update" -d "Update existing configuration"
1379
1602
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "use" -d "Switch to specified configuration"
1603
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "start" -d "Start Claude Code (auto-approve mode)"
1604
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "safe-start" -d "Start Claude Code (safe mode, requires confirmation)"
1380
1605
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "remove" -d "Remove configuration"
1381
1606
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "rm" -d "Remove configuration"
1382
1607
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "current" -d "Display current configuration"
@@ -1391,8 +1616,8 @@ function __ccconfig_profiles
1391
1616
  end
1392
1617
  end
1393
1618
 
1394
- # Profile name completion for use, update, remove
1395
- complete -c ccconfig -f -n "__fish_seen_subcommand_from use update remove rm" -a "(__ccconfig_profiles)"
1619
+ # Profile name completion for use, start, safe-start, update, remove
1620
+ complete -c ccconfig -f -n "__fish_seen_subcommand_from use start safe-start update remove rm" -a "(__ccconfig_profiles)"
1396
1621
 
1397
1622
  # Mode options
1398
1623
  complete -c ccconfig -f -n "__fish_seen_subcommand_from mode" -a "settings env"
@@ -1434,7 +1659,7 @@ function Get-CconfigProfiles {
1434
1659
  Register-ArgumentCompleter -Native -CommandName ccconfig -ScriptBlock {
1435
1660
  param($wordToComplete, $commandAst, $cursorPosition)
1436
1661
 
1437
- $commands = @('list', 'ls', 'add', 'update', 'use', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion')
1662
+ $commands = @('list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion')
1438
1663
  $modes = @('settings', 'env')
1439
1664
  $formats = @('bash', 'zsh', 'fish', 'sh', 'powershell', 'pwsh', 'dotenv')
1440
1665
 
@@ -1456,7 +1681,7 @@ Register-ArgumentCompleter -Native -CommandName ccconfig -ScriptBlock {
1456
1681
  # Second argument completions based on command
1457
1682
  if ($position -eq 2 -or ($position -eq 3 -and $wordToComplete)) {
1458
1683
  switch ($command) {
1459
- { $_ -in 'use', 'update', 'remove', 'rm' } {
1684
+ { $_ -in 'use', 'start', 'safe-start', 'update', 'remove', 'rm' } {
1460
1685
  Get-CconfigProfiles | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1461
1686
  [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1462
1687
  }
@@ -1535,6 +1760,10 @@ function help() {
1535
1760
  ' update [name] Update existing configuration (interactive)');
1536
1761
  console.log(
1537
1762
  ' use <name> [-p|--permanent] Switch to specified configuration');
1763
+ console.log(
1764
+ ' start <name> [claude-args...] Start Claude Code (auto-approve mode)');
1765
+ console.log(
1766
+ ' safe-start <name> [claude-args...] Start Claude Code (safe mode, requires confirmation)');
1538
1767
  console.log(
1539
1768
  ' remove|rm <name> Remove configuration');
1540
1769
  console.log(
@@ -1556,6 +1785,18 @@ function help() {
1556
1785
  console.log(
1557
1786
  ' -s, --show-secret Show full token in current command');
1558
1787
  console.log('');
1788
+ console.log('Notes:');
1789
+ console.log(
1790
+ ' • Two ways to start Claude Code:');
1791
+ console.log(
1792
+ ' - start: Auto-approve mode (adds --dangerously-skip-permissions)');
1793
+ console.log(
1794
+ ' Fast and convenient, but use only with profiles you trust');
1795
+ console.log(
1796
+ ' - safe-start: Safe mode (requires manual confirmation for each command)');
1797
+ console.log(
1798
+ ' Recommended for production or untrusted environments');
1799
+ console.log('');
1559
1800
  console.log('Configuration file locations:');
1560
1801
  console.log(` Configuration list: ${PROFILES_FILE}`);
1561
1802
  console.log(` Claude settings: ${CLAUDE_SETTINGS}`);
@@ -1566,7 +1807,7 @@ function help() {
1566
1807
  async function main() {
1567
1808
  const args = process.argv.slice(2);
1568
1809
 
1569
- // Handle global flags first (standardized behavior)
1810
+ // Handle global flags first (can appear anywhere)
1570
1811
  if (args.includes('--version') || args.includes('-V')) {
1571
1812
  showVersion();
1572
1813
  return;
@@ -1577,15 +1818,55 @@ async function main() {
1577
1818
  return;
1578
1819
  }
1579
1820
 
1580
- // Extract flags
1581
- const showSecret = args.includes('--show-secret') || args.includes('-s');
1582
- const permanent = args.includes('--permanent') || args.includes('-p');
1583
- const filteredArgs = args.filter(
1584
- arg => arg !== '--show-secret' && arg !== '-s' && arg !== '--permanent' &&
1585
- arg !== '-p' && arg !== '--version' && arg !== '-V' &&
1586
- arg !== '--help' && arg !== '-h');
1821
+ // Find the command (first non-flag argument)
1822
+ let commandIndex = -1;
1823
+ for (let i = 0; i < args.length; i++) {
1824
+ if (!args[i].startsWith('-')) {
1825
+ commandIndex = i;
1826
+ break;
1827
+ }
1828
+ }
1829
+
1830
+ // If no command found, default to 'list'
1831
+ const command = commandIndex >= 0 ? args[commandIndex] : null;
1832
+
1833
+ // Extract flags and arguments based on command type
1834
+ let showSecret = false;
1835
+ let permanent = false;
1836
+ let filteredArgs = [];
1837
+
1838
+ // Commands that pass through arguments to Claude Code
1839
+ const passThruCommands = ['start', 'safe-start'];
1840
+
1841
+ if (passThruCommands.includes(command)) {
1842
+ // For pass-through commands:
1843
+ // - Extract flags that appear BEFORE the command
1844
+ // - Keep command and all arguments after it unchanged (for Claude)
1845
+ const preCommandArgs = commandIndex >= 0 ? args.slice(0, commandIndex) : [];
1846
+ showSecret = preCommandArgs.includes('--show-secret') || preCommandArgs.includes('-s');
1847
+ permanent = preCommandArgs.includes('--permanent') || preCommandArgs.includes('-p');
1587
1848
 
1588
- const command = filteredArgs[0];
1849
+ // Keep command and all arguments after it (these go to Claude)
1850
+ filteredArgs = commandIndex >= 0 ? args.slice(commandIndex) : [];
1851
+ } else {
1852
+ // For normal commands:
1853
+ // - Extract flags from anywhere in the arguments
1854
+ // - Remove all recognized flags from arguments
1855
+ showSecret = args.includes('--show-secret') || args.includes('-s');
1856
+ permanent = args.includes('--permanent') || args.includes('-p');
1857
+
1858
+ // Filter out all recognized flags
1859
+ filteredArgs = args.filter(arg =>
1860
+ arg !== '--show-secret' &&
1861
+ arg !== '-s' &&
1862
+ arg !== '--permanent' &&
1863
+ arg !== '-p' &&
1864
+ arg !== '--version' &&
1865
+ arg !== '-V' &&
1866
+ arg !== '--help' &&
1867
+ arg !== '-h'
1868
+ );
1869
+ }
1589
1870
 
1590
1871
  switch (command) {
1591
1872
  case 'list':
@@ -1622,6 +1903,24 @@ async function main() {
1622
1903
  case 'edit':
1623
1904
  edit();
1624
1905
  break;
1906
+ case 'start':
1907
+ if (!filteredArgs[1]) {
1908
+ console.error('Error: Missing configuration name');
1909
+ console.error('Usage: ccconfig start <name> [claude-args...]');
1910
+ process.exit(1);
1911
+ }
1912
+ // Pass all arguments after the profile name to Claude
1913
+ start(filteredArgs[1], filteredArgs.slice(2));
1914
+ break;
1915
+ case 'safe-start':
1916
+ if (!filteredArgs[1]) {
1917
+ console.error('Error: Missing configuration name');
1918
+ console.error('Usage: ccconfig safe-start <name> [claude-args...]');
1919
+ process.exit(1);
1920
+ }
1921
+ // Pass all arguments after the profile name to Claude
1922
+ safeStart(filteredArgs[1], filteredArgs.slice(2));
1923
+ break;
1625
1924
  case 'completion':
1626
1925
  completion(filteredArgs[1]);
1627
1926
  break;