ccconfig 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
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);
35
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,9 +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;
435
+ for (const key of Object.values(ENV_KEYS)) {
436
+ delete settings.env[key];
437
+ }
150
438
 
151
439
  // Set new environment variables
152
440
  Object.assign(settings.env, envVars);
@@ -160,8 +448,15 @@ function updateClaudeSettings(envVars) {
160
448
  function writeEnvFile(envVars) {
161
449
  try {
162
450
  ensureDir(CONFIG_DIR);
163
- const lines =
164
- 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
+ });
165
460
  const content = lines.join('\n') + '\n';
166
461
  fs.writeFileSync(ENV_FILE, content, 'utf-8');
167
462
 
@@ -186,9 +481,18 @@ function readEnvFile() {
186
481
  const content = fs.readFileSync(ENV_FILE, 'utf-8');
187
482
  const env = {};
188
483
  content.split('\n').forEach(line => {
189
- 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_]*)=(.*)$/);
190
487
  if (match) {
191
- 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;
192
496
  }
193
497
  });
194
498
  return env;
@@ -323,8 +627,11 @@ function list() {
323
627
  if (profile.env && profile.env.ANTHROPIC_BASE_URL) {
324
628
  console.log(` URL: ${profile.env.ANTHROPIC_BASE_URL}`);
325
629
  }
326
- if (profile.description) {
327
- console.log(` Description: ${profile.description}`);
630
+ if (profile.env && profile.env.ANTHROPIC_MODEL) {
631
+ console.log(` Model: ${profile.env.ANTHROPIC_MODEL}`);
632
+ }
633
+ if (profile.env && profile.env.ANTHROPIC_SMALL_FAST_MODEL) {
634
+ console.log(` Small Fast Model: ${profile.env.ANTHROPIC_SMALL_FAST_MODEL}`);
328
635
  }
329
636
  console.log('');
330
637
  }
@@ -347,107 +654,109 @@ function list() {
347
654
  * Add new configuration
348
655
  */
349
656
  async function add(name) {
350
- // Auto-initialize if needed
351
657
  initIfNeeded();
658
+ requireInteractive('adding configurations');
352
659
 
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;
660
+ const helper = new ReadlineHelper();
378
661
 
379
662
  try {
380
663
  if (!name) {
381
- name = await askQuestion('Please enter configuration name (e.g., work)');
664
+ name = await helper.ask('Please enter configuration name (e.g., work)');
382
665
  }
383
666
 
384
- if (!name) {
385
- console.error('Error: Configuration name cannot be empty');
667
+ validateConfigName(name);
668
+
669
+ const profiles = loadProfiles() || {profiles: {}};
670
+ const profilesMap = getProfilesMap(profiles);
671
+
672
+ if (profilesMap[name]) {
673
+ console.error(`Error: Configuration '${name}' already exists`);
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`);
386
678
  process.exit(1);
387
679
  }
388
680
 
389
- baseUrl = await askQuestion(
390
- 'Please enter ANTHROPIC_BASE_URL (can be empty, default https://api.anthropic.com)',
391
- 'https://api.anthropic.com');
681
+ console.log('Please enter the following information:');
682
+ console.log('');
392
683
 
393
- authToken =
394
- await askQuestion('Please enter ANTHROPIC_AUTH_TOKEN (can be empty)');
684
+ const envVars = await helper.askEnvVars();
395
685
 
396
- apiKey = await askQuestion('Please enter ANTHROPIC_API_KEY (can be empty)');
686
+ profiles.profiles[name] = {env: envVars};
687
+ saveProfiles(profiles);
397
688
 
398
- description = await askQuestion(
399
- 'Please enter configuration description (can be empty)');
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');
400
702
  } finally {
401
- if (rl) {
402
- rl.close();
403
- }
703
+ helper.close();
404
704
  }
705
+ }
405
706
 
406
- const profiles = loadProfiles() || {profiles: {}};
707
+ /**
708
+ * Update existing configuration
709
+ */
710
+ async function update(name) {
711
+ initIfNeeded();
712
+ requireInteractive('updating configurations');
407
713
 
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
- }
714
+ const helper = new ReadlineHelper();
413
715
 
414
- const envVars = {
415
- ANTHROPIC_BASE_URL: baseUrl || '',
416
- ANTHROPIC_AUTH_TOKEN: authToken || '',
417
- ANTHROPIC_API_KEY: apiKey || ''
418
- };
716
+ try {
717
+ if (!name) {
718
+ name = await helper.ask('Please enter configuration name to update');
719
+ }
419
720
 
420
- profiles.profiles[name] = {env: envVars, description};
721
+ validateConfigName(name);
421
722
 
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');
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
+ });
737
+
738
+ const existingEnv = profile.env || {};
739
+
740
+ console.log(`Updating configuration '${name}'`);
741
+ console.log('Press Enter to keep current value/default, or enter new value to update');
742
+ console.log('');
743
+
744
+ const envVars = await helper.askEnvVars(existingEnv);
745
+
746
+ const profilesMap = getProfilesMap(profiles);
747
+ profilesMap[name] = {env: envVars};
748
+ saveProfiles(profiles);
749
+
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}`);
757
+ } finally {
758
+ helper.close();
759
+ }
451
760
  }
452
761
 
453
762
  /**
@@ -460,19 +769,20 @@ function remove(name) {
460
769
  process.exit(1);
461
770
  }
462
771
 
463
- const profiles = loadProfiles();
464
-
465
- if (!profiles) {
466
- console.error('Error: Configuration file does not exist');
467
- process.exit(1);
468
- }
772
+ // Validate configuration name
773
+ validateConfigName(name);
469
774
 
470
- if (!profiles.profiles[name]) {
471
- console.error(`Error: Configuration '${name}' does not exist`);
472
- process.exit(1);
473
- }
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
+ });
474
784
 
475
- delete profiles.profiles[name];
785
+ delete getProfilesMap(profiles)[name];
476
786
  saveProfiles(profiles);
477
787
  console.log(`✓ Configuration '${name}' removed`);
478
788
  }
@@ -481,94 +791,136 @@ function remove(name) {
481
791
  * Detect current shell and return recommended activation command
482
792
  */
483
793
  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)'};
794
+ const shellType = ShellUtils.detectType();
795
+ if (!shellType) {
796
+ return {shell: null, command: null};
493
797
  }
798
+ const command = ShellUtils.getActivationCommand(shellType);
799
+ const shellName = shellType === 'powershell' ? 'PowerShell' : shellType;
800
+ return {shell: shellName, command};
801
+ }
494
802
 
495
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
496
- shellPath.includes('pwsh') || shellPath.includes('powershell')) {
497
- return {shell: 'PowerShell', command: 'ccconfig env pwsh | iex'};
498
- }
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
+ },
499
822
 
500
- if (shellPath.includes('bash')) {
501
- return {shell: 'bash', command: 'eval $(ccconfig env bash)'};
502
- }
823
+ // Detect current shell type
824
+ detectType: () => {
825
+ const shellPath = (process.env.SHELL || '').toLowerCase();
503
826
 
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'};
827
+ if (process.env.FISH_VERSION || shellPath.includes('fish')) {
828
+ return 'fish';
829
+ }
830
+ if (process.env.ZSH_NAME || process.env.ZSH_VERSION || shellPath.includes('zsh')) {
831
+ return 'zsh';
508
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;
509
906
  }
907
+ };
510
908
 
511
- return {shell: null, command: null};
512
- }
513
-
514
- function escapePosix(value) {
515
- const str = value == null ? '' : String(value);
516
- return `'${str.replace(/'/g, `'"'"'`)}'`;
517
- }
518
-
519
- function escapeFish(value) {
520
- const str = value == null ? '' : String(value);
521
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
522
- }
523
-
524
- function escapePwsh(value) {
525
- const str = value == null ? '' : String(value);
526
- return `'${str.replace(/'/g, `''`)}'`;
527
- }
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); }
528
913
 
529
914
  /**
530
915
  * Detect shell type and config file path
531
916
  */
532
917
  function detectShellConfig() {
533
- const shellPath = (process.env.SHELL || '').toLowerCase();
534
- const homeDir = os.homedir();
535
-
536
- if (process.env.FISH_VERSION || shellPath.includes('fish')) {
537
- const configPath = path.join(homeDir, '.config', 'fish', 'config.fish');
538
- return {shell: 'fish', configPath, detected: true};
539
- }
540
-
541
- if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
542
- shellPath.includes('zsh')) {
543
- const configPath = path.join(homeDir, '.zshrc');
544
- return {shell: 'zsh', configPath, detected: true};
545
- }
546
-
547
- if (shellPath.includes('bash')) {
548
- if (process.platform === 'darwin') {
549
- const bashProfile = path.join(homeDir, '.bash_profile');
550
- const bashrc = path.join(homeDir, '.bashrc');
551
- const configPath = fs.existsSync(bashProfile) || !fs.existsSync(bashrc) ?
552
- bashProfile :
553
- bashrc;
554
- return {shell: 'bash', configPath, detected: true};
555
- }
556
- const configPath = path.join(homeDir, '.bashrc');
557
- return {shell: 'bash', configPath, detected: true};
558
- }
559
-
560
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
561
- shellPath.includes('pwsh') || shellPath.includes('powershell')) {
562
- // PowerShell profile path varies by OS
563
- const configPath = process.platform === 'win32' ?
564
- path.join(
565
- process.env.USERPROFILE || homeDir, 'Documents', 'PowerShell',
566
- 'Microsoft.PowerShell_profile.ps1') :
567
- path.join(homeDir, '.config', 'powershell', 'profile.ps1');
568
- return {shell: 'powershell', configPath, detected: true};
918
+ const shellType = ShellUtils.detectType();
919
+ if (!shellType) {
920
+ return {shell: null, configPath: null, detected: false};
569
921
  }
570
-
571
- return {shell: null, configPath: null, detected: false};
922
+ const configPath = ShellUtils.getConfigPath(shellType);
923
+ return {shell: shellType, configPath, detected: true};
572
924
  }
573
925
 
574
926
  /**
@@ -585,37 +937,20 @@ async function writePermanentEnv(envVars) {
585
937
  }
586
938
 
587
939
  const {shell, configPath} = shellConfig;
588
- const marker = '# >>> ccconfig >>>';
589
- const markerEnd = '# <<< ccconfig <<<';
940
+ const marker = SHELL_MARKERS.start;
941
+ const markerEnd = SHELL_MARKERS.end;
590
942
 
591
- // Generate environment variable lines
592
- let envBlock = '';
593
- switch (shell) {
594
- case 'fish':
595
- envBlock = `${marker}\n`;
596
- for (const [key, value] of Object.entries(envVars)) {
597
- envBlock += `set -gx ${key} "${escapeFish(value)}"\n`;
598
- }
599
- envBlock += `${markerEnd}\n`;
600
- 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
+ }
601
948
 
602
- case 'bash':
603
- case 'zsh':
604
- envBlock = `${marker}\n`;
605
- for (const [key, value] of Object.entries(envVars)) {
606
- envBlock += `export ${key}=${escapePosix(value)}\n`;
607
- }
608
- envBlock += `${markerEnd}\n`;
609
- break;
949
+ const envLines = ShellUtils.formatEnvVars(envVars, shell);
950
+ const maskedEnvLines = ShellUtils.formatEnvVars(maskedEnvVars, shell);
610
951
 
611
- case 'powershell':
612
- envBlock = `${marker}\n`;
613
- for (const [key, value] of Object.entries(envVars)) {
614
- envBlock += `$env:${key}=${escapePwsh(value)}\n`;
615
- }
616
- envBlock += `${markerEnd}\n`;
617
- break;
618
- }
952
+ const envBlock = `${marker}\n${envLines.join('\n')}\n${markerEnd}\n`;
953
+ const maskedEnvBlock = `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
619
954
 
620
955
  // Display warning and confirmation
621
956
  console.log('');
@@ -626,7 +961,7 @@ async function writePermanentEnv(envVars) {
626
961
  console.log('');
627
962
  console.log('The following block will be added/updated:');
628
963
  console.log('───────────────────────────────────────────');
629
- console.log(envBlock.trim());
964
+ console.log(maskedEnvBlock.trim());
630
965
  console.log('───────────────────────────────────────────');
631
966
  console.log('');
632
967
  console.log('What this does:');
@@ -744,29 +1079,25 @@ async function writePermanentEnv(envVars) {
744
1079
  * Switch configuration
745
1080
  */
746
1081
  async function use(name, options = {}) {
747
- const profiles = loadProfiles();
748
-
749
- if (!profiles || !profiles.profiles ||
750
- Object.keys(profiles.profiles).length === 0) {
751
- console.error('Error: No configurations found');
752
- console.error('Please add a configuration first: ccconfig add <name>');
753
- process.exit(1);
754
- }
755
-
756
- if (!profiles.profiles[name]) {
757
- console.error(`Error: Configuration '${name}' does not exist`);
758
- console.error('Run ccconfig list to see available configurations');
759
- process.exit(1);
760
- }
761
-
762
- const profile = profiles.profiles[name];
763
-
764
- if (!profile.env || Object.keys(profile.env).length === 0) {
765
- console.error(
766
- `Error: Configuration '${name}' has empty environment variables`);
767
- console.error('Please edit the configuration file to add env field');
768
- process.exit(1);
769
- }
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
+ });
770
1101
 
771
1102
  const mode = getMode();
772
1103
  const permanent = options.permanent || false;
@@ -777,11 +1108,7 @@ async function use(name, options = {}) {
777
1108
 
778
1109
  console.log(`✓ Switched to configuration: ${name} (settings mode)`);
779
1110
  console.log(` Environment variables:`);
780
- for (const [key, value] of Object.entries(profile.env)) {
781
- const displayValue =
782
- value.length > 20 ? value.substring(0, 20) + '...' : value;
783
- console.log(` ${key}: ${displayValue}`);
784
- }
1111
+ displayEnvVars(profile.env, true, ' ');
785
1112
  console.log('');
786
1113
  console.log('Configuration written to ~/.claude/settings.json');
787
1114
  console.log('Restart Claude Code to make configuration take effect');
@@ -797,11 +1124,7 @@ async function use(name, options = {}) {
797
1124
 
798
1125
  console.log(`✓ Switched to configuration: ${name} (env mode)`);
799
1126
  console.log(` Environment variables:`);
800
- for (const [key, value] of Object.entries(profile.env)) {
801
- const displayValue =
802
- value.length > 20 ? value.substring(0, 20) + '...' : value;
803
- console.log(` ${key}: ${displayValue}`);
804
- }
1127
+ displayEnvVars(profile.env, true, ' ');
805
1128
  console.log('');
806
1129
  console.log(`Environment variable file updated: ${ENV_FILE}`);
807
1130
 
@@ -860,7 +1183,9 @@ function current(showSecret = false) {
860
1183
  const processEnv = {
861
1184
  ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
862
1185
  ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
863
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY
1186
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
1187
+ ANTHROPIC_MODEL: process.env.ANTHROPIC_MODEL,
1188
+ ANTHROPIC_SMALL_FAST_MODEL: process.env.ANTHROPIC_SMALL_FAST_MODEL
864
1189
  };
865
1190
  const currentProfile = getCurrentProfile();
866
1191
 
@@ -880,56 +1205,17 @@ function current(showSecret = false) {
880
1205
 
881
1206
  // Display settings.json configuration
882
1207
  console.log('【1】~/.claude/settings.json:');
883
- if (settings.env &&
884
- (settings.env.ANTHROPIC_BASE_URL || settings.env.ANTHROPIC_AUTH_TOKEN)) {
885
- const baseUrl = settings.env.ANTHROPIC_BASE_URL || '(not set)';
886
- const authToken = settings.env.ANTHROPIC_AUTH_TOKEN || '(not set)';
887
- const maskedToken = (authToken === '(not set)' || showSecret) ?
888
- authToken :
889
- authToken.substring(0, 20) + '...';
890
-
891
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
892
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
893
- } else {
894
- console.log(' (not configured)');
895
- }
1208
+ displayEnvSection(settings.env, showSecret);
896
1209
  console.log('');
897
1210
 
898
1211
  // Display environment variable file configuration
899
1212
  console.log(`【2】Environment Variables File (${ENV_FILE}):`);
900
- if (envFile &&
901
- (envFile.ANTHROPIC_BASE_URL || envFile.ANTHROPIC_AUTH_TOKEN ||
902
- envFile.ANTHROPIC_API_KEY)) {
903
- const baseUrl = envFile.ANTHROPIC_BASE_URL || '(not set)';
904
- const authToken = envFile.ANTHROPIC_AUTH_TOKEN ||
905
- envFile.ANTHROPIC_API_KEY || '(not set)';
906
- const maskedToken = (authToken === '(not set)' || showSecret) ?
907
- authToken :
908
- authToken.substring(0, 20) + '...';
909
-
910
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
911
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
912
- } else {
913
- console.log(' (not configured)');
914
- }
1213
+ displayEnvSection(envFile, showSecret);
915
1214
  console.log('');
916
1215
 
917
1216
  // Display current process environment variables
918
1217
  console.log('【3】Current Process Environment Variables:');
919
- if (processEnv.ANTHROPIC_BASE_URL || processEnv.ANTHROPIC_AUTH_TOKEN ||
920
- processEnv.ANTHROPIC_API_KEY) {
921
- const baseUrl = processEnv.ANTHROPIC_BASE_URL || '(not set)';
922
- const authToken = processEnv.ANTHROPIC_AUTH_TOKEN ||
923
- processEnv.ANTHROPIC_API_KEY || '(not set)';
924
- const maskedToken = (authToken === '(not set)' || showSecret) ?
925
- authToken :
926
- authToken.substring(0, 20) + '...';
927
-
928
- console.log(` ANTHROPIC_BASE_URL: ${baseUrl}`);
929
- console.log(` ANTHROPIC_AUTH_TOKEN: ${maskedToken}`);
930
- } else {
931
- console.log(' (not set)');
932
- }
1218
+ displayEnvSection(processEnv, showSecret);
933
1219
  console.log('');
934
1220
 
935
1221
  // Display notes
@@ -1033,43 +1319,411 @@ function env(format = 'bash') {
1033
1319
  const envVars = getActiveEnvVars();
1034
1320
 
1035
1321
  if (!envVars || Object.keys(envVars).length === 0) {
1036
- console.error(
1037
- 'Error: No available environment variable configuration found');
1038
- console.error(
1039
- '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');
1040
1324
  process.exit(1);
1041
1325
  }
1042
1326
 
1043
- // Output all environment variables
1044
- switch (format) {
1045
- case 'fish':
1046
- for (const [key, value] of Object.entries(envVars)) {
1047
- console.log(`set -gx ${key} "${escapeFish(value)}"`);
1048
- }
1049
- break;
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: () => {
1368
+ console.error(
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');
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 });
1463
+ }
1464
+
1465
+ /**
1466
+ * Generate shell completion script
1467
+ */
1468
+ function completion(shell) {
1469
+ if (!shell) {
1470
+ console.error('Error: Missing shell type');
1471
+ console.error('Usage: ccconfig completion <bash|zsh|fish|powershell|pwsh>');
1472
+ console.error('');
1473
+ console.error('To install:');
1474
+ console.error(' Bash: ccconfig completion bash >> ~/.bashrc');
1475
+ console.error(' Zsh: ccconfig completion zsh >> ~/.zshrc');
1476
+ console.error(' Fish: ccconfig completion fish > ~/.config/fish/completions/ccconfig.fish');
1477
+ console.error(' PowerShell: ccconfig completion pwsh >> $PROFILE');
1478
+ process.exit(1);
1479
+ }
1480
+
1481
+ const commands = COMMANDS.join(' ');
1482
+
1483
+ switch (shell) {
1050
1484
  case 'bash':
1485
+ console.log(`# ccconfig bash completion
1486
+ _ccconfig_completions() {
1487
+ local cur prev commands profiles
1488
+ COMPREPLY=()
1489
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1490
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1491
+ commands="${commands}"
1492
+
1493
+ # Get available profiles
1494
+ if [ -f ~/.config/ccconfig/profiles.json ]; then
1495
+ profiles=$(node -e "try { const data = require(process.env.HOME + '/.config/ccconfig/profiles.json'); console.log(Object.keys(data.profiles || {}).join(' ')); } catch(e) { }" 2>/dev/null)
1496
+ fi
1497
+
1498
+ case "\${COMP_CWORD}" in
1499
+ 1)
1500
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
1501
+ ;;
1502
+ 2)
1503
+ case "\${prev}" in
1504
+ use|start|safe-start|update|remove|rm)
1505
+ COMPREPLY=( $(compgen -W "\${profiles}" -- \${cur}) )
1506
+ ;;
1507
+ mode)
1508
+ COMPREPLY=( $(compgen -W "settings env" -- \${cur}) )
1509
+ ;;
1510
+ env)
1511
+ COMPREPLY=( $(compgen -W "bash zsh fish sh powershell pwsh dotenv" -- \${cur}) )
1512
+ ;;
1513
+ esac
1514
+ ;;
1515
+ 3)
1516
+ case "\${COMP_WORDS[1]}" in
1517
+ use)
1518
+ COMPREPLY=( $(compgen -W "--permanent -p" -- \${cur}) )
1519
+ ;;
1520
+ current)
1521
+ COMPREPLY=( $(compgen -W "--show-secret -s" -- \${cur}) )
1522
+ ;;
1523
+ esac
1524
+ ;;
1525
+ esac
1526
+ }
1527
+
1528
+ complete -F _ccconfig_completions ccconfig
1529
+ `);
1530
+ break;
1531
+
1051
1532
  case 'zsh':
1052
- case 'sh':
1053
- for (const [key, value] of Object.entries(envVars)) {
1054
- console.log(`export ${key}=${escapePosix(value)}`);
1055
- }
1533
+ console.log(`# ccconfig zsh completion
1534
+ _ccconfig() {
1535
+ local -a commands profiles modes formats
1536
+ commands=(
1537
+ 'list:List all configurations'
1538
+ 'ls:List all configurations'
1539
+ 'add:Add new configuration'
1540
+ 'update:Update existing configuration'
1541
+ 'use:Switch to specified configuration'
1542
+ 'start:Start Claude Code (auto-approve mode)'
1543
+ 'safe-start:Start Claude Code (safe mode, requires confirmation)'
1544
+ 'remove:Remove configuration'
1545
+ 'rm:Remove configuration'
1546
+ 'current:Display current configuration'
1547
+ 'mode:View or switch mode'
1548
+ 'env:Output environment variables'
1549
+ 'edit:Show configuration file location'
1550
+ )
1551
+
1552
+ modes=('settings' 'env')
1553
+ formats=('bash' 'zsh' 'fish' 'sh' 'powershell' 'pwsh' 'dotenv')
1554
+
1555
+ # Get available profiles
1556
+ if [ -f ~/.config/ccconfig/profiles.json ]; then
1557
+ profiles=($(node -e "try { const data = require(process.env.HOME + '/.config/ccconfig/profiles.json'); console.log(Object.keys(data.profiles || {}).join(' ')); } catch(e) { }" 2>/dev/null))
1558
+ fi
1559
+
1560
+ case $CURRENT in
1561
+ 2)
1562
+ _describe 'command' commands
1563
+ ;;
1564
+ 3)
1565
+ case $words[2] in
1566
+ use|start|safe-start|update|remove|rm)
1567
+ _describe 'profile' profiles
1568
+ ;;
1569
+ mode)
1570
+ _describe 'mode' modes
1571
+ ;;
1572
+ env)
1573
+ _describe 'format' formats
1574
+ ;;
1575
+ esac
1576
+ ;;
1577
+ 4)
1578
+ case $words[2] in
1579
+ use)
1580
+ _arguments '-p[Write permanently to shell config]' '--permanent[Write permanently to shell config]'
1581
+ ;;
1582
+ current)
1583
+ _arguments '-s[Show full token]' '--show-secret[Show full token]'
1584
+ ;;
1585
+ esac
1586
+ ;;
1587
+ esac
1588
+ }
1589
+
1590
+ compdef _ccconfig ccconfig
1591
+ `);
1592
+ break;
1593
+
1594
+ case 'fish':
1595
+ console.log(`# ccconfig fish completion
1596
+
1597
+ # Commands
1598
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "list" -d "List all configurations"
1599
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "ls" -d "List all configurations"
1600
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "add" -d "Add new configuration"
1601
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "update" -d "Update existing configuration"
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)"
1605
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "remove" -d "Remove configuration"
1606
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "rm" -d "Remove configuration"
1607
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "current" -d "Display current configuration"
1608
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "mode" -d "View or switch mode"
1609
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "env" -d "Output environment variables"
1610
+ complete -c ccconfig -f -n "__fish_use_subcommand" -a "edit" -d "Show configuration file location"
1611
+
1612
+ # Get profile names dynamically
1613
+ function __ccconfig_profiles
1614
+ if test -f ~/.config/ccconfig/profiles.json
1615
+ node -e "try { const data = require(process.env.HOME + '/.config/ccconfig/profiles.json'); Object.keys(data.profiles || {}).forEach(k => console.log(k)); } catch(e) { }" 2>/dev/null
1616
+ end
1617
+ end
1618
+
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)"
1621
+
1622
+ # Mode options
1623
+ complete -c ccconfig -f -n "__fish_seen_subcommand_from mode" -a "settings env"
1624
+
1625
+ # Env format options
1626
+ complete -c ccconfig -f -n "__fish_seen_subcommand_from env" -a "bash zsh fish sh powershell pwsh dotenv"
1627
+
1628
+ # Flags for use command
1629
+ complete -c ccconfig -f -n "__fish_seen_subcommand_from use" -s p -l permanent -d "Write permanently to shell config"
1630
+
1631
+ # Flags for current command
1632
+ complete -c ccconfig -f -n "__fish_seen_subcommand_from current" -s s -l show-secret -d "Show full token"
1633
+
1634
+ # Global flags
1635
+ complete -c ccconfig -f -s h -l help -d "Display help information"
1636
+ complete -c ccconfig -f -s V -l version -d "Display version information"
1637
+ `);
1056
1638
  break;
1639
+
1057
1640
  case 'powershell':
1058
1641
  case 'pwsh':
1059
- for (const [key, value] of Object.entries(envVars)) {
1060
- console.log(`$env:${key}=${escapePwsh(value)}`);
1061
- }
1062
- break;
1063
- case 'dotenv':
1064
- for (const [key, value] of Object.entries(envVars)) {
1065
- const renderedValue = value == null ? '' : String(value);
1066
- console.log(`${key}=${renderedValue}`);
1067
- }
1642
+ console.log(`# ccconfig PowerShell completion
1643
+
1644
+ # Get available profiles
1645
+ function Get-CconfigProfiles {
1646
+ $profilesPath = Join-Path $env:USERPROFILE ".config\\ccconfig\\profiles.json"
1647
+ if (Test-Path $profilesPath) {
1648
+ try {
1649
+ $profiles = Get-Content $profilesPath | ConvertFrom-Json
1650
+ return $profiles.profiles.PSObject.Properties.Name
1651
+ } catch {
1652
+ return @()
1653
+ }
1654
+ }
1655
+ return @()
1656
+ }
1657
+
1658
+ # Register argument completer for ccconfig
1659
+ Register-ArgumentCompleter -Native -CommandName ccconfig -ScriptBlock {
1660
+ param($wordToComplete, $commandAst, $cursorPosition)
1661
+
1662
+ $commands = @('list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion')
1663
+ $modes = @('settings', 'env')
1664
+ $formats = @('bash', 'zsh', 'fish', 'sh', 'powershell', 'pwsh', 'dotenv')
1665
+
1666
+ # Parse the command line
1667
+ $tokens = $commandAst.ToString() -split '\\s+'
1668
+ $position = $tokens.Count - 1
1669
+
1670
+ # If we're completing the first argument (command)
1671
+ if ($position -eq 1 -or ($position -eq 2 -and $wordToComplete)) {
1672
+ $commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1673
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1674
+ }
1675
+ return
1676
+ }
1677
+
1678
+ # Get the command (first argument)
1679
+ $command = if ($tokens.Count -gt 1) { $tokens[1] } else { '' }
1680
+
1681
+ # Second argument completions based on command
1682
+ if ($position -eq 2 -or ($position -eq 3 -and $wordToComplete)) {
1683
+ switch ($command) {
1684
+ { $_ -in 'use', 'start', 'safe-start', 'update', 'remove', 'rm' } {
1685
+ Get-CconfigProfiles | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1686
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1687
+ }
1688
+ }
1689
+ 'mode' {
1690
+ $modes | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1691
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1692
+ }
1693
+ }
1694
+ 'env' {
1695
+ $formats | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1696
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1697
+ }
1698
+ }
1699
+ 'completion' {
1700
+ @('bash', 'zsh', 'fish', 'powershell', 'pwsh') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1701
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
1702
+ }
1703
+ }
1704
+ }
1705
+ return
1706
+ }
1707
+
1708
+ # Flag completions
1709
+ if ($position -ge 3 -and $command -eq 'use') {
1710
+ @('-p', '--permanent') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1711
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', 'Write permanently to shell config')
1712
+ }
1713
+ }
1714
+
1715
+ if ($position -ge 2 -and $command -eq 'current') {
1716
+ @('-s', '--show-secret') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
1717
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', 'Show full token')
1718
+ }
1719
+ }
1720
+ }
1721
+ `);
1068
1722
  break;
1723
+
1069
1724
  default:
1070
- console.error(`Error: Unsupported format: ${format}`);
1071
- console.error(
1072
- 'Supported formats: fish, bash, zsh, sh, powershell, pwsh, dotenv');
1725
+ console.error(`Error: Unsupported shell: ${shell}`);
1726
+ console.error('Supported shells: bash, zsh, fish, powershell, pwsh');
1073
1727
  process.exit(1);
1074
1728
  }
1075
1729
  }
@@ -1102,8 +1756,14 @@ function help() {
1102
1756
  ' list|ls List all configurations (default)');
1103
1757
  console.log(
1104
1758
  ' add [name] Add new configuration (interactive)');
1759
+ console.log(
1760
+ ' update [name] Update existing configuration (interactive)');
1105
1761
  console.log(
1106
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)');
1107
1767
  console.log(
1108
1768
  ' remove|rm <name> Remove configuration');
1109
1769
  console.log(
@@ -1114,6 +1774,8 @@ function help() {
1114
1774
  ' env [format] Output environment variables (env mode)');
1115
1775
  console.log(
1116
1776
  ' edit Show configuration file location');
1777
+ console.log(
1778
+ ' completion <bash|zsh|fish|pwsh> Generate shell completion script');
1117
1779
  console.log('');
1118
1780
  console.log('Flags:');
1119
1781
  console.log(
@@ -1123,6 +1785,18 @@ function help() {
1123
1785
  console.log(
1124
1786
  ' -s, --show-secret Show full token in current command');
1125
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('');
1126
1800
  console.log('Configuration file locations:');
1127
1801
  console.log(` Configuration list: ${PROFILES_FILE}`);
1128
1802
  console.log(` Claude settings: ${CLAUDE_SETTINGS}`);
@@ -1133,7 +1807,7 @@ function help() {
1133
1807
  async function main() {
1134
1808
  const args = process.argv.slice(2);
1135
1809
 
1136
- // Handle global flags first (standardized behavior)
1810
+ // Handle global flags first (can appear anywhere)
1137
1811
  if (args.includes('--version') || args.includes('-V')) {
1138
1812
  showVersion();
1139
1813
  return;
@@ -1144,15 +1818,55 @@ async function main() {
1144
1818
  return;
1145
1819
  }
1146
1820
 
1147
- // Extract flags
1148
- const showSecret = args.includes('--show-secret') || args.includes('-s');
1149
- const permanent = args.includes('--permanent') || args.includes('-p');
1150
- const filteredArgs = args.filter(
1151
- arg => arg !== '--show-secret' && arg !== '-s' && arg !== '--permanent' &&
1152
- arg !== '-p' && arg !== '--version' && arg !== '-V' &&
1153
- 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;
1154
1832
 
1155
- const command = filteredArgs[0];
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');
1848
+
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
+ }
1156
1870
 
1157
1871
  switch (command) {
1158
1872
  case 'list':
@@ -1170,6 +1884,9 @@ async function main() {
1170
1884
  case 'add':
1171
1885
  await add(filteredArgs[1]);
1172
1886
  break;
1887
+ case 'update':
1888
+ await update(filteredArgs[1]);
1889
+ break;
1173
1890
  case 'remove':
1174
1891
  case 'rm':
1175
1892
  remove(filteredArgs[1]);
@@ -1186,6 +1903,27 @@ async function main() {
1186
1903
  case 'edit':
1187
1904
  edit();
1188
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;
1924
+ case 'completion':
1925
+ completion(filteredArgs[1]);
1926
+ break;
1189
1927
  default:
1190
1928
  if (!command) {
1191
1929
  list();