ccconfig 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/ccconfig.js +251 -152
  2. package/package.json +1 -1
package/ccconfig.js CHANGED
@@ -4,7 +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
+ const {spawn, execSync} = require('child_process');
8
8
 
9
9
  // Configuration file paths
10
10
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'ccconfig');
@@ -52,7 +52,11 @@ function ensureProfilesAvailable({onEmpty} = {}) {
52
52
  }
53
53
 
54
54
  function ensureProfileAvailable(
55
- name, {allowEmptyEnv = false, onEmptyProfiles, onMissingProfile, onEmptyEnv} = {}) {
55
+ name,
56
+ {allowEmptyEnv = false,
57
+ onEmptyProfiles,
58
+ onMissingProfile,
59
+ onEmptyEnv} = {}) {
56
60
  const profiles = ensureProfilesAvailable({onEmpty: onEmptyProfiles});
57
61
  const profilesMap = getProfilesMap(profiles);
58
62
  const profile = profilesMap[name];
@@ -68,7 +72,8 @@ function ensureProfileAvailable(
68
72
  process.exit(1);
69
73
  }
70
74
 
71
- if (!allowEmptyEnv && (!profile.env || Object.keys(profile.env).length === 0)) {
75
+ if (!allowEmptyEnv &&
76
+ (!profile.env || Object.keys(profile.env).length === 0)) {
72
77
  if (typeof onEmptyEnv === 'function') {
73
78
  onEmptyEnv();
74
79
  } else {
@@ -83,7 +88,10 @@ function ensureProfileAvailable(
83
88
  }
84
89
 
85
90
  // All supported commands
86
- const COMMANDS = ['list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion'];
91
+ const COMMANDS = [
92
+ 'list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm',
93
+ 'current', 'mode', 'env', 'completion'
94
+ ];
87
95
 
88
96
  // ccconfig markers for shell config files
89
97
  const SHELL_MARKERS = {
@@ -140,11 +148,16 @@ function printEnvVar(key, value, mask = true) {
140
148
  * Utility: Display environment variables with consistent formatting
141
149
  */
142
150
  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];
151
+ const keys = [
152
+ ENV_KEYS.BASE_URL, ENV_KEYS.AUTH_TOKEN, ENV_KEYS.API_KEY, ENV_KEYS.MODEL,
153
+ ENV_KEYS.SMALL_FAST_MODEL
154
+ ];
144
155
  for (const key of keys) {
145
156
  if (!(key in envVars)) continue;
146
157
  const value = envVars[key];
147
- if (!value && key !== ENV_KEYS.BASE_URL && key !== ENV_KEYS.AUTH_TOKEN && key !== ENV_KEYS.API_KEY) continue;
158
+ if (!value && key !== ENV_KEYS.BASE_URL && key !== ENV_KEYS.AUTH_TOKEN &&
159
+ key !== ENV_KEYS.API_KEY)
160
+ continue;
148
161
  const displayValue = maskValue(key, value, mask);
149
162
  console.log(`${indent}${key}: ${displayValue || '(not set)'}`);
150
163
  }
@@ -160,16 +173,14 @@ class ReadlineHelper {
160
173
 
161
174
  ensureInterface() {
162
175
  if (!this.rl) {
163
- this.rl = readline.createInterface({
164
- input: process.stdin,
165
- output: process.stdout
166
- });
176
+ this.rl = readline.createInterface(
177
+ {input: process.stdin, output: process.stdout});
167
178
  }
168
179
  }
169
180
 
170
181
  async ask(question, defaultValue = '', options = {}) {
171
182
  this.ensureInterface();
172
- const { brackets = 'parentheses' } = options;
183
+ const {brackets = 'parentheses'} = options;
173
184
  const left = brackets === 'square' ? '[' : '(';
174
185
  const right = brackets === 'square' ? ']' : ')';
175
186
  const suffix = defaultValue ? ` ${left}${defaultValue}${right}` : '';
@@ -184,34 +195,32 @@ class ReadlineHelper {
184
195
 
185
196
  async askEnvVars(existingEnv = {}) {
186
197
  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
- );
198
+ 'ANTHROPIC_BASE_URL (press Enter to keep current/default)',
199
+ existingEnv.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
200
+ {brackets: existingEnv.ANTHROPIC_BASE_URL ? 'square' : 'parentheses'});
191
201
 
192
202
  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
- );
203
+ 'ANTHROPIC_AUTH_TOKEN (press Enter to keep current/set empty)',
204
+ existingEnv.ANTHROPIC_AUTH_TOKEN || '', {
205
+ brackets: existingEnv.ANTHROPIC_AUTH_TOKEN ? 'square' : 'parentheses'
206
+ });
197
207
 
198
208
  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
- );
209
+ 'ANTHROPIC_API_KEY (press Enter to keep current/set empty)',
210
+ existingEnv.ANTHROPIC_API_KEY || '',
211
+ {brackets: existingEnv.ANTHROPIC_API_KEY ? 'square' : 'parentheses'});
203
212
 
204
213
  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
- );
214
+ 'ANTHROPIC_MODEL (press Enter to skip/keep current)',
215
+ existingEnv.ANTHROPIC_MODEL || '',
216
+ {brackets: existingEnv.ANTHROPIC_MODEL ? 'square' : 'parentheses'});
209
217
 
210
218
  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
- );
219
+ 'ANTHROPIC_SMALL_FAST_MODEL (press Enter to skip/keep current)',
220
+ existingEnv.ANTHROPIC_SMALL_FAST_MODEL || '', {
221
+ brackets: existingEnv.ANTHROPIC_SMALL_FAST_MODEL ? 'square' :
222
+ 'parentheses'
223
+ });
215
224
 
216
225
  const envVars = {
217
226
  [ENV_KEYS.BASE_URL]: baseUrl || '',
@@ -249,14 +258,17 @@ function requireInteractive(commandName) {
249
258
  * Utility: Display environment variables section for current command
250
259
  */
251
260
  function displayEnvSection(envVars, showSecret) {
252
- if (!envVars || (!envVars[ENV_KEYS.BASE_URL] && !envVars[ENV_KEYS.AUTH_TOKEN] && !envVars[ENV_KEYS.API_KEY])) {
261
+ if (!envVars ||
262
+ (!envVars[ENV_KEYS.BASE_URL] && !envVars[ENV_KEYS.AUTH_TOKEN] &&
263
+ !envVars[ENV_KEYS.API_KEY])) {
253
264
  console.log(' (not configured)');
254
265
  return;
255
266
  }
256
267
 
257
268
  const normalizedEnv = {
258
269
  [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)',
270
+ [ENV_KEYS.AUTH_TOKEN]: envVars[ENV_KEYS.AUTH_TOKEN] ||
271
+ envVars[ENV_KEYS.API_KEY] || '(not set)',
260
272
  [ENV_KEYS.MODEL]: envVars[ENV_KEYS.MODEL],
261
273
  [ENV_KEYS.SMALL_FAST_MODEL]: envVars[ENV_KEYS.SMALL_FAST_MODEL]
262
274
  };
@@ -269,12 +281,14 @@ function displayEnvSection(envVars, showSecret) {
269
281
 
270
282
  // Display with aligned columns
271
283
  console.log(` ${ENV_KEYS.BASE_URL}: ${normalizedEnv[ENV_KEYS.BASE_URL]}`);
272
- console.log(` ${ENV_KEYS.AUTH_TOKEN}: ${normalizedEnv[ENV_KEYS.AUTH_TOKEN]}`);
284
+ console.log(
285
+ ` ${ENV_KEYS.AUTH_TOKEN}: ${normalizedEnv[ENV_KEYS.AUTH_TOKEN]}`);
273
286
  if (normalizedEnv[ENV_KEYS.MODEL]) {
274
287
  console.log(` ${ENV_KEYS.MODEL}: ${normalizedEnv[ENV_KEYS.MODEL]}`);
275
288
  }
276
289
  if (normalizedEnv[ENV_KEYS.SMALL_FAST_MODEL]) {
277
- console.log(` ${ENV_KEYS.SMALL_FAST_MODEL}: ${normalizedEnv[ENV_KEYS.SMALL_FAST_MODEL]}`);
290
+ console.log(` ${ENV_KEYS.SMALL_FAST_MODEL}: ${
291
+ normalizedEnv[ENV_KEYS.SMALL_FAST_MODEL]}`);
278
292
  }
279
293
  }
280
294
 
@@ -315,7 +329,8 @@ function validateConfigName(name, allowEmpty = false) {
315
329
  // Limit length to prevent issues
316
330
  const MAX_NAME_LENGTH = 50;
317
331
  if (name.length > MAX_NAME_LENGTH) {
318
- console.error(`Error: Configuration name too long (max ${MAX_NAME_LENGTH} characters)`);
332
+ console.error(`Error: Configuration name too long (max ${
333
+ MAX_NAME_LENGTH} characters)`);
319
334
  console.error(`Current length: ${name.length}`);
320
335
  process.exit(1);
321
336
  }
@@ -451,10 +466,10 @@ function writeEnvFile(envVars) {
451
466
  const lines = Object.entries(envVars).map(([key, value]) => {
452
467
  // Escape special characters to prevent injection
453
468
  const escapedValue = String(value ?? '')
454
- .replace(/\\/g, '\\\\')
455
- .replace(/\n/g, '\\n')
456
- .replace(/\r/g, '\\r')
457
- .replace(/\t/g, '\\t');
469
+ .replace(/\\/g, '\\\\')
470
+ .replace(/\n/g, '\\n')
471
+ .replace(/\r/g, '\\r')
472
+ .replace(/\t/g, '\\t');
458
473
  return `${key}=${escapedValue}`;
459
474
  });
460
475
  const content = lines.join('\n') + '\n';
@@ -481,17 +496,17 @@ function readEnvFile() {
481
496
  const content = fs.readFileSync(ENV_FILE, 'utf-8');
482
497
  const env = {};
483
498
  content.split('\n').forEach(line => {
484
- // Only accept valid environment variable names: starts with letter or underscore,
485
- // followed by letters, numbers, or underscores
499
+ // Only accept valid environment variable names: starts with letter or
500
+ // underscore, followed by letters, numbers, or underscores
486
501
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
487
502
  if (match) {
488
503
  // Unescape special characters
489
504
  // IMPORTANT: Must unescape \\\\ first to avoid double-unescaping
490
505
  const unescapedValue = match[2]
491
- .replace(/\\\\/g, '\\')
492
- .replace(/\\n/g, '\n')
493
- .replace(/\\r/g, '\r')
494
- .replace(/\\t/g, '\t');
506
+ .replace(/\\\\/g, '\\')
507
+ .replace(/\\n/g, '\n')
508
+ .replace(/\\r/g, '\r')
509
+ .replace(/\\t/g, '\t');
495
510
  env[match[1]] = unescapedValue;
496
511
  }
497
512
  });
@@ -631,7 +646,8 @@ function list() {
631
646
  console.log(` Model: ${profile.env.ANTHROPIC_MODEL}`);
632
647
  }
633
648
  if (profile.env && profile.env.ANTHROPIC_SMALL_FAST_MODEL) {
634
- console.log(` Small Fast Model: ${profile.env.ANTHROPIC_SMALL_FAST_MODEL}`);
649
+ console.log(
650
+ ` Small Fast Model: ${profile.env.ANTHROPIC_SMALL_FAST_MODEL}`);
635
651
  }
636
652
  console.log('');
637
653
  }
@@ -672,9 +688,8 @@ async function add(name) {
672
688
  if (profilesMap[name]) {
673
689
  console.error(`Error: Configuration '${name}' already exists`);
674
690
  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`);
691
+ console.error('To modify this configuration, use:');
692
+ console.error(` ccconfig update ${name}`);
678
693
  process.exit(1);
679
694
  }
680
695
 
@@ -688,17 +703,60 @@ async function add(name) {
688
703
 
689
704
  console.log(`✓ Configuration '${name}' added`);
690
705
  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);
706
+
707
+ // Check if claude binary exists before offering to start
708
+ let claudeAvailable = false;
709
+ try {
710
+ const command =
711
+ process.platform === 'win32' ? 'where claude' : 'which claude';
712
+ execSync(command, {stdio: 'pipe'});
713
+ claudeAvailable = true;
714
+ } catch (err) {
715
+ // Claude not found, don't offer to start
716
+ }
717
+
718
+ // Ask if user wants to start Claude Code with this profile immediately
719
+ const shouldStart = claudeAvailable ?
720
+ await helper.ask(`Start Claude Code with ${name} now? (yes/no)`, 'no') :
721
+ 'no';
722
+ const normalized = shouldStart.trim().toLowerCase();
723
+
696
724
  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');
725
+
726
+ // Create auto-execute output manager (RAII-style)
727
+ const output = (() => {
728
+ let executed = false;
729
+ return {
730
+ execute: () => {
731
+ if (!executed) {
732
+ console.log('');
733
+ console.log(`Configuration '${name}' summary:`);
734
+ console.log('');
735
+ console.log('Saved environment variables:');
736
+ displayEnvVars(envVars);
737
+ console.log('');
738
+ console.log(`To start Claude Code with this configuration:`);
739
+ console.log(` ccconfig start ${name}`);
740
+ console.log('');
741
+ console.log('To update this configuration:');
742
+ console.log(` ccconfig update ${name}`);
743
+ console.log('');
744
+ console.log('Configuration saved to:');
745
+ console.log(` ${PROFILES_FILE}`);
746
+ executed = true;
747
+ }
748
+ }
749
+ };
750
+ })();
751
+
752
+ // Attempt to start Claude (if user chose yes), deferring output to exit
753
+ const shouldStartClaude = normalized === 'yes' || normalized === 'y';
754
+ if (shouldStartClaude) {
755
+ start(name, [], {onExit: output.execute});
756
+ } else {
757
+ // If not starting Claude, show output immediately
758
+ output.execute();
759
+ }
702
760
  } finally {
703
761
  helper.close();
704
762
  }
@@ -730,7 +788,8 @@ async function update(name) {
730
788
  console.error(`Error: Configuration '${name}' does not exist`);
731
789
  console.error('');
732
790
  console.error('Run ccconfig list to see available configurations');
733
- console.error(`Or use 'ccconfig add ${name}' to create a new configuration`);
791
+ console.error(
792
+ `Or use 'ccconfig add ${name}' to create a new configuration`);
734
793
  process.exit(1);
735
794
  }
736
795
  });
@@ -738,7 +797,8 @@ async function update(name) {
738
797
  const existingEnv = profile.env || {};
739
798
 
740
799
  console.log(`Updating configuration '${name}'`);
741
- console.log('Press Enter to keep current value/default, or enter new value to update');
800
+ console.log(
801
+ 'Press Enter to keep current value/default, or enter new value to update');
742
802
  console.log('');
743
803
 
744
804
  const envVars = await helper.askEnvVars(existingEnv);
@@ -762,17 +822,20 @@ async function update(name) {
762
822
  /**
763
823
  * Remove configuration
764
824
  */
765
- function remove(name) {
825
+ async function remove(name) {
766
826
  if (!name) {
767
827
  console.error('Error: Missing configuration name');
768
828
  console.error('Usage: ccconfig remove <name>');
769
829
  process.exit(1);
770
830
  }
771
831
 
832
+ // Check if terminal is interactive before proceeding
833
+ requireInteractive('removing configurations');
834
+
772
835
  // Validate configuration name
773
836
  validateConfigName(name);
774
837
 
775
- const {profiles} = ensureProfileAvailable(name, {
838
+ const {profile, profiles} = ensureProfileAvailable(name, {
776
839
  allowEmptyEnv: true,
777
840
  onEmptyProfiles: () => {
778
841
  console.error('Error: Configuration file does not exist');
@@ -782,9 +845,45 @@ function remove(name) {
782
845
  }
783
846
  });
784
847
 
785
- delete getProfilesMap(profiles)[name];
786
- saveProfiles(profiles);
787
- console.log(`✓ Configuration '${name}' removed`);
848
+ // Display profile information before removal
849
+ console.log('');
850
+ console.log('WARNING: PERMANENT DELETION');
851
+ console.log('═══════════════════════════════════════════════════════════');
852
+ console.log('');
853
+ console.log(`Configuration to be removed: ${name}`);
854
+ console.log('');
855
+ console.log('Profile details:');
856
+ if (profile.env && Object.keys(profile.env).length > 0) {
857
+ displayEnvVars(profile.env, true, ' ');
858
+ } else {
859
+ console.log(' (no environment variables configured)');
860
+ }
861
+ console.log('');
862
+ console.log('This action CANNOT be undone!');
863
+ console.log('All configuration data for this profile will be \x1b[1mpermanently deleted\x1b[0m.');
864
+ console.log('');
865
+
866
+ // Ask for confirmation
867
+ const helper = new ReadlineHelper();
868
+ try {
869
+ const confirmation = await helper.ask(
870
+ `Are you sure you want to remove '${name}'? (yes/no)`, 'no');
871
+ const normalized = confirmation.trim().toLowerCase();
872
+
873
+ if (normalized !== 'yes' && normalized !== 'y') {
874
+ console.log('');
875
+ console.log('Operation cancelled.');
876
+ return;
877
+ }
878
+
879
+ // Proceed with removal
880
+ delete getProfilesMap(profiles)[name];
881
+ saveProfiles(profiles);
882
+ console.log('');
883
+ console.log(`✓ Configuration '${name}' removed`);
884
+ } finally {
885
+ helper.close();
886
+ }
788
887
  }
789
888
 
790
889
  /**
@@ -812,7 +911,9 @@ const ShellUtils = {
812
911
  },
813
912
  fish: (value) => {
814
913
  const str = value == null ? '' : String(value);
815
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
914
+ return str.replace(/\\/g, '\\\\')
915
+ .replace(/"/g, '\\"')
916
+ .replace(/\$/g, '\\$');
816
917
  },
817
918
  pwsh: (value) => {
818
919
  const str = value == null ? '' : String(value);
@@ -827,10 +928,12 @@ const ShellUtils = {
827
928
  if (process.env.FISH_VERSION || shellPath.includes('fish')) {
828
929
  return 'fish';
829
930
  }
830
- if (process.env.ZSH_NAME || process.env.ZSH_VERSION || shellPath.includes('zsh')) {
931
+ if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
932
+ shellPath.includes('zsh')) {
831
933
  return 'zsh';
832
934
  }
833
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL || shellPath.includes('pwsh') || shellPath.includes('powershell')) {
935
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
936
+ shellPath.includes('pwsh') || shellPath.includes('powershell')) {
834
937
  return 'powershell';
835
938
  }
836
939
  if (shellPath.includes('bash')) {
@@ -851,14 +954,17 @@ const ShellUtils = {
851
954
  const configs = {
852
955
  fish: path.join(homeDir, '.config', 'fish', 'config.fish'),
853
956
  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')
957
+ bash: process.platform === 'darwin' ?
958
+ (fs.existsSync(path.join(homeDir, '.bash_profile')) ||
959
+ !fs.existsSync(path.join(homeDir, '.bashrc')) ?
960
+ path.join(homeDir, '.bash_profile') :
961
+ path.join(homeDir, '.bashrc')) :
962
+ path.join(homeDir, '.bashrc'),
963
+ powershell: process.platform === 'win32' ?
964
+ path.join(
965
+ process.env.USERPROFILE || homeDir, 'Documents', 'PowerShell',
966
+ 'Microsoft.PowerShell_profile.ps1') :
967
+ path.join(homeDir, '.config', 'powershell', 'profile.ps1')
862
968
  };
863
969
  return configs[shellType];
864
970
  },
@@ -893,11 +999,10 @@ const ShellUtils = {
893
999
  break;
894
1000
  case 'dotenv':
895
1001
  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');
1002
+ const escapedValue = renderedValue.replace(/\\/g, '\\\\')
1003
+ .replace(/\n/g, '\\n')
1004
+ .replace(/\r/g, '\\r')
1005
+ .replace(/\t/g, '\\t');
901
1006
  lines.push(`${key}=${escapedValue}`);
902
1007
  break;
903
1008
  }
@@ -907,9 +1012,15 @@ const ShellUtils = {
907
1012
  };
908
1013
 
909
1014
  // 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); }
1015
+ function escapePosix(value) {
1016
+ return ShellUtils.escape.posix(value);
1017
+ }
1018
+ function escapeFish(value) {
1019
+ return ShellUtils.escape.fish(value);
1020
+ }
1021
+ function escapePwsh(value) {
1022
+ return ShellUtils.escape.pwsh(value);
1023
+ }
913
1024
 
914
1025
  /**
915
1026
  * Detect shell type and config file path
@@ -950,7 +1061,8 @@ async function writePermanentEnv(envVars) {
950
1061
  const maskedEnvLines = ShellUtils.formatEnvVars(maskedEnvVars, shell);
951
1062
 
952
1063
  const envBlock = `${marker}\n${envLines.join('\n')}\n${markerEnd}\n`;
953
- const maskedEnvBlock = `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
1064
+ const maskedEnvBlock =
1065
+ `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
954
1066
 
955
1067
  // Display warning and confirmation
956
1068
  console.log('');
@@ -1230,25 +1342,6 @@ function current(showSecret = false) {
1230
1342
  console.log('═══════════════════════════════════════════');
1231
1343
  }
1232
1344
 
1233
- /**
1234
- * Show configuration file path
1235
- */
1236
- function edit() {
1237
- if (!fs.existsSync(PROFILES_FILE)) {
1238
- console.error('Error: Configuration file does not exist');
1239
- console.error('Please add a configuration first: ccconfig add <name>');
1240
- process.exit(1);
1241
- }
1242
-
1243
- const editor = process.env.EDITOR || process.env.VISUAL || 'vim';
1244
-
1245
- console.log('Configuration file path:');
1246
- console.log(` ${PROFILES_FILE}`);
1247
- console.log('');
1248
- console.log('Open it with your preferred editor, for example:');
1249
- console.log(` ${editor} ${PROFILES_FILE}`);
1250
- }
1251
-
1252
1345
  /**
1253
1346
  * Switch/view mode
1254
1347
  */
@@ -1319,12 +1412,15 @@ function env(format = 'bash') {
1319
1412
  const envVars = getActiveEnvVars();
1320
1413
 
1321
1414
  if (!envVars || Object.keys(envVars).length === 0) {
1322
- console.error('Error: No available environment variable configuration found');
1323
- console.error('Please run ccconfig use <name> to select a configuration first');
1415
+ console.error(
1416
+ 'Error: No available environment variable configuration found');
1417
+ console.error(
1418
+ 'Please run ccconfig use <name> to select a configuration first');
1324
1419
  process.exit(1);
1325
1420
  }
1326
1421
 
1327
- const supportedFormats = ['fish', 'bash', 'zsh', 'sh', 'powershell', 'pwsh', 'dotenv'];
1422
+ const supportedFormats =
1423
+ ['fish', 'bash', 'zsh', 'sh', 'powershell', 'pwsh', 'dotenv'];
1328
1424
  if (!supportedFormats.includes(format)) {
1329
1425
  console.error(`Error: Unsupported format: ${format}`);
1330
1426
  console.error(`Supported formats: ${supportedFormats.join(', ')}`);
@@ -1341,9 +1437,10 @@ function env(format = 'bash') {
1341
1437
  * @param {Array} extraArgs - Additional arguments to pass to Claude
1342
1438
  * @param {Object} options - Options object
1343
1439
  * @param {boolean} options.safe - Whether to run in safe mode (default: false)
1440
+ * @param {Function} options.onExit - Callback to execute before process exits
1344
1441
  */
1345
1442
  function startClaude(name, extraArgs = [], options = {}) {
1346
- const { safe = false } = options;
1443
+ const {safe = false, onExit = null} = options;
1347
1444
  const commandName = safe ? 'safe-start' : 'start';
1348
1445
 
1349
1446
  if (!name) {
@@ -1373,8 +1470,9 @@ function startClaude(name, extraArgs = [], options = {}) {
1373
1470
 
1374
1471
  // Check if claude binary exists before proceeding
1375
1472
  try {
1376
- const command = process.platform === 'win32' ? 'where claude' : 'which claude';
1377
- execSync(command, { stdio: 'pipe' });
1473
+ const command =
1474
+ process.platform === 'win32' ? 'where claude' : 'which claude';
1475
+ execSync(command, {stdio: 'pipe'});
1378
1476
  } catch (err) {
1379
1477
  console.error('Error: Claude Code CLI not found');
1380
1478
  console.error('');
@@ -1395,17 +1493,22 @@ function startClaude(name, extraArgs = [], options = {}) {
1395
1493
  }
1396
1494
 
1397
1495
  // Build Claude arguments based on mode
1398
- const claudeArgs = safe ? extraArgs : ['--dangerously-skip-permissions', ...extraArgs];
1496
+ const claudeArgs =
1497
+ safe ? extraArgs : ['--dangerously-skip-permissions', ...extraArgs];
1399
1498
 
1400
1499
  // Display mode-specific notes
1401
1500
  console.log('');
1402
1501
  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');
1502
+ console.log(
1503
+ 'Note: Running in safe mode (permission confirmation required)');
1504
+ console.log(
1505
+ ' Claude Code will ask for confirmation before executing commands');
1405
1506
  console.log(' For automatic execution, use "ccconfig start" instead');
1406
1507
  } 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');
1508
+ console.log(
1509
+ 'Note: Starting with --dangerously-skip-permissions flag enabled');
1510
+ console.log(
1511
+ ' This allows Claude Code to execute commands without confirmation prompts');
1409
1512
  console.log(' Only use this with profiles you trust');
1410
1513
  }
1411
1514
  console.log('');
@@ -1444,7 +1547,8 @@ function startClaude(name, extraArgs = [], options = {}) {
1444
1547
  }
1445
1548
  }
1446
1549
 
1447
- const canResetTerminal = process.platform !== 'win32' && process.stdin.isTTY && process.stdout.isTTY;
1550
+ const canResetTerminal = process.platform !== 'win32' &&
1551
+ process.stdin.isTTY && process.stdout.isTTY;
1448
1552
 
1449
1553
  // Use stty to restore terminal settings on Unix-like systems
1450
1554
  // This is more comprehensive than just setRawMode
@@ -1452,15 +1556,15 @@ function startClaude(name, extraArgs = [], options = {}) {
1452
1556
  try {
1453
1557
  // 'stty sane' restores terminal to sensible settings
1454
1558
  // Use stdio: 'inherit' to ensure it operates on the same terminal
1455
- execSync('stty sane', { stdio: 'inherit' });
1559
+ execSync('stty sane', {stdio: 'inherit'});
1456
1560
  } catch (e) {
1457
1561
  // If stty fails, try basic echo and icanon reset
1458
1562
  try {
1459
- execSync('stty echo icanon', { stdio: 'inherit' });
1563
+ execSync('stty echo icanon', {stdio: 'inherit'});
1460
1564
  } catch (e2) {
1461
1565
  try {
1462
- execSync('stty echo', { stdio: 'inherit' });
1463
- execSync('stty icanon', { stdio: 'inherit' });
1566
+ execSync('stty echo', {stdio: 'inherit'});
1567
+ execSync('stty icanon', {stdio: 'inherit'});
1464
1568
  } catch (e3) {
1465
1569
  // Ignore - best effort
1466
1570
  }
@@ -1489,10 +1593,15 @@ function startClaude(name, extraArgs = [], options = {}) {
1489
1593
  process.removeListener('SIGINT', signalHandler);
1490
1594
  process.removeListener('SIGTERM', signalHandler);
1491
1595
 
1596
+ // Execute onExit callback before showing promotion message
1597
+ if (typeof onExit === 'function') {
1598
+ onExit();
1599
+ }
1600
+
1492
1601
  // Show project promotion message on exit
1493
1602
  console.log('');
1494
1603
  console.log('──────────────────────────────────────────');
1495
- console.log('Thanks for using ccconfig!');
1604
+ console.log('Thanks for using \x1b[1mccconfig\x1b[0m!');
1496
1605
  console.log('');
1497
1606
  console.log('If you find this tool helpful, please consider:');
1498
1607
  console.log(' ⭐ Star us on GitHub: https://github.com/Danielmelody/ccconfig');
@@ -1524,15 +1633,16 @@ function startClaude(name, extraArgs = [], options = {}) {
1524
1633
  /**
1525
1634
  * Start Claude Code with specified profile (auto-approve mode)
1526
1635
  */
1527
- function start(name, extraArgs = []) {
1528
- return startClaude(name, extraArgs, { safe: false });
1636
+ function start(name, extraArgs = [], options = {}) {
1637
+ return startClaude(name, extraArgs, {safe: false, ...options});
1529
1638
  }
1530
1639
 
1531
1640
  /**
1532
- * Start Claude Code with specified profile (safe mode - requires permission confirmation)
1641
+ * Start Claude Code with specified profile (safe mode - requires permission
1642
+ * confirmation)
1533
1643
  */
1534
- function safeStart(name, extraArgs = []) {
1535
- return startClaude(name, extraArgs, { safe: true });
1644
+ function safeStart(name, extraArgs = [], options = {}) {
1645
+ return startClaude(name, extraArgs, {safe: true, ...options});
1536
1646
  }
1537
1647
 
1538
1648
  /**
@@ -1546,7 +1656,8 @@ function completion(shell) {
1546
1656
  console.error('To install:');
1547
1657
  console.error(' Bash: ccconfig completion bash >> ~/.bashrc');
1548
1658
  console.error(' Zsh: ccconfig completion zsh >> ~/.zshrc');
1549
- console.error(' Fish: ccconfig completion fish > ~/.config/fish/completions/ccconfig.fish');
1659
+ console.error(
1660
+ ' Fish: ccconfig completion fish > ~/.config/fish/completions/ccconfig.fish');
1550
1661
  console.error(' PowerShell: ccconfig completion pwsh >> $PROFILE');
1551
1662
  process.exit(1);
1552
1663
  }
@@ -1619,7 +1730,6 @@ _ccconfig() {
1619
1730
  'current:Display current configuration'
1620
1731
  'mode:View or switch mode'
1621
1732
  'env:Output environment variables'
1622
- 'edit:Show configuration file location'
1623
1733
  )
1624
1734
 
1625
1735
  modes=('settings' 'env')
@@ -1680,7 +1790,6 @@ complete -c ccconfig -f -n "__fish_use_subcommand" -a "rm" -d "Remove configurat
1680
1790
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "current" -d "Display current configuration"
1681
1791
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "mode" -d "View or switch mode"
1682
1792
  complete -c ccconfig -f -n "__fish_use_subcommand" -a "env" -d "Output environment variables"
1683
- complete -c ccconfig -f -n "__fish_use_subcommand" -a "edit" -d "Show configuration file location"
1684
1793
 
1685
1794
  # Get profile names dynamically
1686
1795
  function __ccconfig_profiles
@@ -1732,7 +1841,7 @@ function Get-CconfigProfiles {
1732
1841
  Register-ArgumentCompleter -Native -CommandName ccconfig -ScriptBlock {
1733
1842
  param($wordToComplete, $commandAst, $cursorPosition)
1734
1843
 
1735
- $commands = @('list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'edit', 'completion')
1844
+ $commands = @('list', 'ls', 'add', 'update', 'use', 'start', 'safe-start', 'remove', 'rm', 'current', 'mode', 'env', 'completion')
1736
1845
  $modes = @('settings', 'env')
1737
1846
  $formats = @('bash', 'zsh', 'fish', 'sh', 'powershell', 'pwsh', 'dotenv')
1738
1847
 
@@ -1845,8 +1954,6 @@ function help() {
1845
1954
  ' mode [settings|env] View or switch mode');
1846
1955
  console.log(
1847
1956
  ' env [format] Output environment variables (env mode)');
1848
- console.log(
1849
- ' edit Show configuration file location');
1850
1957
  console.log(
1851
1958
  ' completion <bash|zsh|fish|pwsh> Generate shell completion script');
1852
1959
  console.log('');
@@ -1859,8 +1966,7 @@ function help() {
1859
1966
  ' -s, --show-secret Show full token in current command');
1860
1967
  console.log('');
1861
1968
  console.log('Notes:');
1862
- console.log(
1863
- ' • Two ways to start Claude Code:');
1969
+ console.log(' • Two ways to start Claude Code:');
1864
1970
  console.log(
1865
1971
  ' - start: Auto-approve mode (adds --dangerously-skip-permissions)');
1866
1972
  console.log(
@@ -1916,8 +2022,10 @@ async function main() {
1916
2022
  // - Extract flags that appear BEFORE the command
1917
2023
  // - Keep command and all arguments after it unchanged (for Claude)
1918
2024
  const preCommandArgs = commandIndex >= 0 ? args.slice(0, commandIndex) : [];
1919
- showSecret = preCommandArgs.includes('--show-secret') || preCommandArgs.includes('-s');
1920
- permanent = preCommandArgs.includes('--permanent') || preCommandArgs.includes('-p');
2025
+ showSecret = preCommandArgs.includes('--show-secret') ||
2026
+ preCommandArgs.includes('-s');
2027
+ permanent =
2028
+ preCommandArgs.includes('--permanent') || preCommandArgs.includes('-p');
1921
2029
 
1922
2030
  // Keep command and all arguments after it (these go to Claude)
1923
2031
  filteredArgs = commandIndex >= 0 ? args.slice(commandIndex) : [];
@@ -1929,16 +2037,10 @@ async function main() {
1929
2037
  permanent = args.includes('--permanent') || args.includes('-p');
1930
2038
 
1931
2039
  // Filter out all recognized flags
1932
- filteredArgs = args.filter(arg =>
1933
- arg !== '--show-secret' &&
1934
- arg !== '-s' &&
1935
- arg !== '--permanent' &&
1936
- arg !== '-p' &&
1937
- arg !== '--version' &&
1938
- arg !== '-V' &&
1939
- arg !== '--help' &&
1940
- arg !== '-h'
1941
- );
2040
+ filteredArgs = args.filter(
2041
+ arg => arg !== '--show-secret' && arg !== '-s' &&
2042
+ arg !== '--permanent' && arg !== '-p' && arg !== '--version' &&
2043
+ arg !== '-V' && arg !== '--help' && arg !== '-h');
1942
2044
  }
1943
2045
 
1944
2046
  switch (command) {
@@ -1962,7 +2064,7 @@ async function main() {
1962
2064
  break;
1963
2065
  case 'remove':
1964
2066
  case 'rm':
1965
- remove(filteredArgs[1]);
2067
+ await remove(filteredArgs[1]);
1966
2068
  break;
1967
2069
  case 'current':
1968
2070
  current(showSecret);
@@ -1973,9 +2075,6 @@ async function main() {
1973
2075
  case 'env':
1974
2076
  env(filteredArgs[1] || 'bash');
1975
2077
  break;
1976
- case 'edit':
1977
- edit();
1978
- break;
1979
2078
  case 'start':
1980
2079
  if (!filteredArgs[1]) {
1981
2080
  console.error('Error: Missing configuration name');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccconfig",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Cross-platform Claude Code configuration switching tool",
5
5
  "main": "ccconfig.js",
6
6
  "bin": {