ccconfig 1.4.1 → 1.4.3

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 (4) hide show
  1. package/README.md +13 -13
  2. package/README_zh.md +13 -13
  3. package/ccconfig.js +215 -103
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -38,7 +38,7 @@ ccconfig use personal --permanent # or use -p for short
38
38
  npm install -g ccconfig
39
39
  ```
40
40
 
41
- ### Method 1: Direct Start Mode (Recommended 🚀)
41
+ ### Method 1: Direct Start Mode (Recommended)
42
42
 
43
43
  The easiest way to use ccconfig - directly start Claude Code with a specific profile:
44
44
 
@@ -64,21 +64,21 @@ ccconfig safe-start work # Safe mode (requires confirmation for each com
64
64
  - **`ccconfig start`** - Auto-approve mode
65
65
  - Automatically adds `--dangerously-skip-permissions` flag
66
66
  - Commands execute without confirmation prompts
67
- - ⚠️ **Only use with profiles you trust**
67
+ - **Only use with profiles you trust**
68
68
  - Perfect for: personal projects, trusted company profiles, rapid development
69
69
 
70
70
  - **`ccconfig safe-start`** - Safe mode
71
71
  - Does NOT add `--dangerously-skip-permissions`
72
72
  - Requires manual confirmation before executing each command
73
- - **Recommended for production or untrusted environments**
73
+ - **Recommended for production or untrusted environments**
74
74
  - Perfect for: production systems, new profiles, sensitive data
75
75
 
76
76
  **Advantages:**
77
- - No shell configuration needed
78
- - No manual switching required
79
- - Environment variables automatically injected
80
- - Works across all shells
81
- - Pass additional arguments: `ccconfig start work /path/to/project --verbose`
77
+ - No shell configuration needed
78
+ - No manual switching required
79
+ - Environment variables automatically injected
80
+ - Works across all shells
81
+ - Pass additional arguments: `ccconfig start work /path/to/project --verbose`
82
82
 
83
83
  ### Method 2: Manual Switch Mode
84
84
 
@@ -269,11 +269,11 @@ Do you want to set ANTHROPIC_SMALL_FAST_MODEL? (y/N) [n]:
269
269
  ccconfig supports shell completion for commands, profile names, and options. This makes it easier to discover and use commands.
270
270
 
271
271
  **Features:**
272
- - Command completion (list, add, update, use, remove, etc.)
273
- - Profile name completion (dynamically reads from your configurations)
274
- - Option completion (--permanent, --show-secret, etc.)
275
- - Mode completion (settings, env)
276
- - Format completion (bash, zsh, fish, etc.)
272
+ - Command completion (list, add, update, use, remove, etc.)
273
+ - Profile name completion (dynamically reads from your configurations)
274
+ - Option completion (--permanent, --show-secret, etc.)
275
+ - Mode completion (settings, env)
276
+ - Format completion (bash, zsh, fish, etc.)
277
277
 
278
278
  **Installation:**
279
279
 
package/README_zh.md CHANGED
@@ -38,7 +38,7 @@ ccconfig use personal --permanent # 或使用 -p 简写
38
38
  npm install -g ccconfig
39
39
  ```
40
40
 
41
- ### 方式 1:直接启动模式(推荐 🚀)
41
+ ### 方式 1:直接启动模式(推荐)
42
42
 
43
43
  最简单的使用方式 - 直接使用指定配置启动 Claude Code:
44
44
 
@@ -64,21 +64,21 @@ ccconfig safe-start work # 安全模式(每个命令需要确认)
64
64
  - **`ccconfig start`** - 自动批准模式
65
65
  - 自动添加 `--dangerously-skip-permissions` 标志
66
66
  - 命令无需确认直接执行
67
- - ⚠️ **仅在您信任的配置中使用**
67
+ - **仅在您信任的配置中使用**
68
68
  - 适用场景:个人项目、可信的公司配置、快速开发
69
69
 
70
70
  - **`ccconfig safe-start`** - 安全模式
71
71
  - 不添加 `--dangerously-skip-permissions`
72
72
  - 执行每个命令前需要手动确认
73
- - **推荐用于生产环境或不可信环境**
73
+ - **推荐用于生产环境或不可信环境**
74
74
  - 适用场景:生产系统、新配置、敏感数据
75
75
 
76
76
  **优势:**
77
- - 无需配置 shell
78
- - 无需手动切换
79
- - 自动注入环境变量
80
- - 支持所有 shell
81
- - 可传递额外参数:`ccconfig start work /path/to/project --verbose`
77
+ - 无需配置 shell
78
+ - 无需手动切换
79
+ - 自动注入环境变量
80
+ - 支持所有 shell
81
+ - 可传递额外参数:`ccconfig start work /path/to/project --verbose`
82
82
 
83
83
  ### 方式 2:手动切换模式
84
84
 
@@ -269,11 +269,11 @@ Do you want to set ANTHROPIC_SMALL_FAST_MODEL? (y/N) [n]:
269
269
  ccconfig 支持命令、配置名称和选项的 shell 自动补全,让您更容易发现和使用命令。
270
270
 
271
271
  **功能:**
272
- - 命令补全 (list, add, update, use, remove 等)
273
- - 配置名称补全(动态读取您的配置)
274
- - 选项补全 (--permanent, --show-secret 等)
275
- - 模式补全 (settings, env)
276
- - 格式补全 (bash, zsh, fish 等)
272
+ - 命令补全 (list, add, update, use, remove 等)
273
+ - 配置名称补全(动态读取您的配置)
274
+ - 选项补全 (--permanent, --show-secret 等)
275
+ - 模式补全 (settings, env)
276
+ - 格式补全 (bash, zsh, fish 等)
277
277
 
278
278
  **安装:**
279
279
 
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', 'edit', '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
  }
@@ -696,7 +712,8 @@ async function add(name) {
696
712
  console.log('');
697
713
  console.log('This information has been saved to:');
698
714
  console.log(` ${PROFILES_FILE}`);
699
- console.log('You can edit this file directly to further customize the profile:');
715
+ console.log(
716
+ 'You can edit this file directly to further customize the profile:');
700
717
  console.log(` vim ${PROFILES_FILE}`);
701
718
  console.log('Or run ccconfig edit to open it with your preferred editor');
702
719
  } finally {
@@ -730,7 +747,8 @@ async function update(name) {
730
747
  console.error(`Error: Configuration '${name}' does not exist`);
731
748
  console.error('');
732
749
  console.error('Run ccconfig list to see available configurations');
733
- console.error(`Or use 'ccconfig add ${name}' to create a new configuration`);
750
+ console.error(
751
+ `Or use 'ccconfig add ${name}' to create a new configuration`);
734
752
  process.exit(1);
735
753
  }
736
754
  });
@@ -738,7 +756,8 @@ async function update(name) {
738
756
  const existingEnv = profile.env || {};
739
757
 
740
758
  console.log(`Updating configuration '${name}'`);
741
- console.log('Press Enter to keep current value/default, or enter new value to update');
759
+ console.log(
760
+ 'Press Enter to keep current value/default, or enter new value to update');
742
761
  console.log('');
743
762
 
744
763
  const envVars = await helper.askEnvVars(existingEnv);
@@ -812,7 +831,9 @@ const ShellUtils = {
812
831
  },
813
832
  fish: (value) => {
814
833
  const str = value == null ? '' : String(value);
815
- return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$');
834
+ return str.replace(/\\/g, '\\\\')
835
+ .replace(/"/g, '\\"')
836
+ .replace(/\$/g, '\\$');
816
837
  },
817
838
  pwsh: (value) => {
818
839
  const str = value == null ? '' : String(value);
@@ -827,10 +848,12 @@ const ShellUtils = {
827
848
  if (process.env.FISH_VERSION || shellPath.includes('fish')) {
828
849
  return 'fish';
829
850
  }
830
- if (process.env.ZSH_NAME || process.env.ZSH_VERSION || shellPath.includes('zsh')) {
851
+ if (process.env.ZSH_NAME || process.env.ZSH_VERSION ||
852
+ shellPath.includes('zsh')) {
831
853
  return 'zsh';
832
854
  }
833
- if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL || shellPath.includes('pwsh') || shellPath.includes('powershell')) {
855
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL ||
856
+ shellPath.includes('pwsh') || shellPath.includes('powershell')) {
834
857
  return 'powershell';
835
858
  }
836
859
  if (shellPath.includes('bash')) {
@@ -851,14 +874,17 @@ const ShellUtils = {
851
874
  const configs = {
852
875
  fish: path.join(homeDir, '.config', 'fish', 'config.fish'),
853
876
  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')
877
+ bash: process.platform === 'darwin' ?
878
+ (fs.existsSync(path.join(homeDir, '.bash_profile')) ||
879
+ !fs.existsSync(path.join(homeDir, '.bashrc')) ?
880
+ path.join(homeDir, '.bash_profile') :
881
+ path.join(homeDir, '.bashrc')) :
882
+ path.join(homeDir, '.bashrc'),
883
+ powershell: process.platform === 'win32' ?
884
+ path.join(
885
+ process.env.USERPROFILE || homeDir, 'Documents', 'PowerShell',
886
+ 'Microsoft.PowerShell_profile.ps1') :
887
+ path.join(homeDir, '.config', 'powershell', 'profile.ps1')
862
888
  };
863
889
  return configs[shellType];
864
890
  },
@@ -893,11 +919,10 @@ const ShellUtils = {
893
919
  break;
894
920
  case 'dotenv':
895
921
  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');
922
+ const escapedValue = renderedValue.replace(/\\/g, '\\\\')
923
+ .replace(/\n/g, '\\n')
924
+ .replace(/\r/g, '\\r')
925
+ .replace(/\t/g, '\\t');
901
926
  lines.push(`${key}=${escapedValue}`);
902
927
  break;
903
928
  }
@@ -907,9 +932,15 @@ const ShellUtils = {
907
932
  };
908
933
 
909
934
  // 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); }
935
+ function escapePosix(value) {
936
+ return ShellUtils.escape.posix(value);
937
+ }
938
+ function escapeFish(value) {
939
+ return ShellUtils.escape.fish(value);
940
+ }
941
+ function escapePwsh(value) {
942
+ return ShellUtils.escape.pwsh(value);
943
+ }
913
944
 
914
945
  /**
915
946
  * Detect shell type and config file path
@@ -950,7 +981,8 @@ async function writePermanentEnv(envVars) {
950
981
  const maskedEnvLines = ShellUtils.formatEnvVars(maskedEnvVars, shell);
951
982
 
952
983
  const envBlock = `${marker}\n${envLines.join('\n')}\n${markerEnd}\n`;
953
- const maskedEnvBlock = `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
984
+ const maskedEnvBlock =
985
+ `${marker}\n${maskedEnvLines.join('\n')}\n${markerEnd}\n`;
954
986
 
955
987
  // Display warning and confirmation
956
988
  console.log('');
@@ -1319,12 +1351,15 @@ function env(format = 'bash') {
1319
1351
  const envVars = getActiveEnvVars();
1320
1352
 
1321
1353
  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');
1354
+ console.error(
1355
+ 'Error: No available environment variable configuration found');
1356
+ console.error(
1357
+ 'Please run ccconfig use <name> to select a configuration first');
1324
1358
  process.exit(1);
1325
1359
  }
1326
1360
 
1327
- const supportedFormats = ['fish', 'bash', 'zsh', 'sh', 'powershell', 'pwsh', 'dotenv'];
1361
+ const supportedFormats =
1362
+ ['fish', 'bash', 'zsh', 'sh', 'powershell', 'pwsh', 'dotenv'];
1328
1363
  if (!supportedFormats.includes(format)) {
1329
1364
  console.error(`Error: Unsupported format: ${format}`);
1330
1365
  console.error(`Supported formats: ${supportedFormats.join(', ')}`);
@@ -1343,7 +1378,7 @@ function env(format = 'bash') {
1343
1378
  * @param {boolean} options.safe - Whether to run in safe mode (default: false)
1344
1379
  */
1345
1380
  function startClaude(name, extraArgs = [], options = {}) {
1346
- const { safe = false } = options;
1381
+ const {safe = false} = options;
1347
1382
  const commandName = safe ? 'safe-start' : 'start';
1348
1383
 
1349
1384
  if (!name) {
@@ -1373,8 +1408,9 @@ function startClaude(name, extraArgs = [], options = {}) {
1373
1408
 
1374
1409
  // Check if claude binary exists before proceeding
1375
1410
  try {
1376
- const command = process.platform === 'win32' ? 'where claude' : 'which claude';
1377
- execSync(command, { stdio: 'pipe' });
1411
+ const command =
1412
+ process.platform === 'win32' ? 'where claude' : 'which claude';
1413
+ execSync(command, {stdio: 'pipe'});
1378
1414
  } catch (err) {
1379
1415
  console.error('Error: Claude Code CLI not found');
1380
1416
  console.error('');
@@ -1395,17 +1431,22 @@ function startClaude(name, extraArgs = [], options = {}) {
1395
1431
  }
1396
1432
 
1397
1433
  // Build Claude arguments based on mode
1398
- const claudeArgs = safe ? extraArgs : ['--dangerously-skip-permissions', ...extraArgs];
1434
+ const claudeArgs =
1435
+ safe ? extraArgs : ['--dangerously-skip-permissions', ...extraArgs];
1399
1436
 
1400
1437
  // Display mode-specific notes
1401
1438
  console.log('');
1402
1439
  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');
1440
+ console.log(
1441
+ 'Note: Running in safe mode (permission confirmation required)');
1442
+ console.log(
1443
+ ' Claude Code will ask for confirmation before executing commands');
1405
1444
  console.log(' For automatic execution, use "ccconfig start" instead');
1406
1445
  } 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');
1446
+ console.log(
1447
+ 'Note: Starting with --dangerously-skip-permissions flag enabled');
1448
+ console.log(
1449
+ ' This allows Claude Code to execute commands without confirmation prompts');
1409
1450
  console.log(' Only use this with profiles you trust');
1410
1451
  }
1411
1452
  console.log('');
@@ -1420,7 +1461,10 @@ function startClaude(name, extraArgs = [], options = {}) {
1420
1461
  // Normalize all profile env values to strings (spawn requires string values)
1421
1462
  const normalizedEnv = {};
1422
1463
  for (const [key, value] of Object.entries(profile.env)) {
1423
- normalizedEnv[key] = String(value ?? '');
1464
+ if (value === undefined || value === null) {
1465
+ continue;
1466
+ }
1467
+ normalizedEnv[key] = typeof value === 'string' ? value : String(value);
1424
1468
  }
1425
1469
  const envVars = {...process.env, ...normalizedEnv};
1426
1470
 
@@ -1430,21 +1474,92 @@ function startClaude(name, extraArgs = [], options = {}) {
1430
1474
  stdio: 'inherit' // Inherit stdin, stdout, stderr from parent process
1431
1475
  });
1432
1476
 
1477
+ // Function to restore terminal state and exit
1478
+ const exitGracefully = (code) => {
1479
+ // Reset terminal to normal mode (in case it was left in raw mode)
1480
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
1481
+ try {
1482
+ process.stdin.setRawMode(false);
1483
+ } catch (e) {
1484
+ // Ignore errors during cleanup
1485
+ }
1486
+ }
1487
+
1488
+ const canResetTerminal = process.platform !== 'win32' &&
1489
+ process.stdin.isTTY && process.stdout.isTTY;
1490
+
1491
+ // Use stty to restore terminal settings on Unix-like systems
1492
+ // This is more comprehensive than just setRawMode
1493
+ if (canResetTerminal) {
1494
+ try {
1495
+ // 'stty sane' restores terminal to sensible settings
1496
+ // Use stdio: 'inherit' to ensure it operates on the same terminal
1497
+ execSync('stty sane', {stdio: 'inherit'});
1498
+ } catch (e) {
1499
+ // If stty fails, try basic echo and icanon reset
1500
+ try {
1501
+ execSync('stty echo icanon', {stdio: 'inherit'});
1502
+ } catch (e2) {
1503
+ try {
1504
+ execSync('stty echo', {stdio: 'inherit'});
1505
+ execSync('stty icanon', {stdio: 'inherit'});
1506
+ } catch (e3) {
1507
+ // Ignore - best effort
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ process.exit(code || 0);
1514
+ };
1515
+
1516
+ // Handle SIGINT (Ctrl+C) and SIGTERM
1517
+ const signalHandler = (signal) => {
1518
+ // Forward signal to child process
1519
+ if (claude && !claude.killed) {
1520
+ claude.kill(signal);
1521
+ }
1522
+ // Don't exit immediately - wait for child to exit
1523
+ };
1524
+
1525
+ process.on('SIGINT', signalHandler);
1526
+ process.on('SIGTERM', signalHandler);
1527
+
1433
1528
  // Handle process exit
1434
1529
  claude.on('close', (code) => {
1530
+ // Remove signal handlers to avoid duplicate handling
1531
+ process.removeListener('SIGINT', signalHandler);
1532
+ process.removeListener('SIGTERM', signalHandler);
1533
+
1534
+ // Show project promotion message on exit
1535
+ console.log('');
1536
+ console.log('──────────────────────────────────────────');
1537
+ console.log('Thanks for using \x1b[1mccconfig\x1b[0m!');
1538
+ console.log('');
1539
+ console.log('If you find this tool helpful, please consider:');
1540
+ console.log(' ⭐ Star us on GitHub: https://github.com/Danielmelody/ccconfig');
1541
+ console.log(' 📢 Share with others who use Claude Code');
1542
+ console.log('──────────────────────────────────────────');
1543
+ console.log('');
1544
+
1435
1545
  if (code !== 0 && code !== null) {
1436
1546
  console.error(`Claude Code exited with code ${code}`);
1437
- process.exit(code);
1547
+ exitGracefully(code);
1548
+ } else {
1549
+ exitGracefully(0);
1438
1550
  }
1439
- process.exit(0);
1440
1551
  });
1441
1552
 
1442
1553
  claude.on('error', (err) => {
1554
+ // Remove signal handlers
1555
+ process.removeListener('SIGINT', signalHandler);
1556
+ process.removeListener('SIGTERM', signalHandler);
1557
+
1443
1558
  console.error(`Error starting Claude Code: ${err.message}`);
1444
1559
  console.error('');
1445
1560
  console.error('Please make sure Claude Code CLI is installed:');
1446
1561
  console.error(' npm install -g claude-code');
1447
- process.exit(1);
1562
+ exitGracefully(1);
1448
1563
  });
1449
1564
  }
1450
1565
 
@@ -1452,14 +1567,15 @@ function startClaude(name, extraArgs = [], options = {}) {
1452
1567
  * Start Claude Code with specified profile (auto-approve mode)
1453
1568
  */
1454
1569
  function start(name, extraArgs = []) {
1455
- return startClaude(name, extraArgs, { safe: false });
1570
+ return startClaude(name, extraArgs, {safe: false});
1456
1571
  }
1457
1572
 
1458
1573
  /**
1459
- * Start Claude Code with specified profile (safe mode - requires permission confirmation)
1574
+ * Start Claude Code with specified profile (safe mode - requires permission
1575
+ * confirmation)
1460
1576
  */
1461
1577
  function safeStart(name, extraArgs = []) {
1462
- return startClaude(name, extraArgs, { safe: true });
1578
+ return startClaude(name, extraArgs, {safe: true});
1463
1579
  }
1464
1580
 
1465
1581
  /**
@@ -1473,7 +1589,8 @@ function completion(shell) {
1473
1589
  console.error('To install:');
1474
1590
  console.error(' Bash: ccconfig completion bash >> ~/.bashrc');
1475
1591
  console.error(' Zsh: ccconfig completion zsh >> ~/.zshrc');
1476
- console.error(' Fish: ccconfig completion fish > ~/.config/fish/completions/ccconfig.fish');
1592
+ console.error(
1593
+ ' Fish: ccconfig completion fish > ~/.config/fish/completions/ccconfig.fish');
1477
1594
  console.error(' PowerShell: ccconfig completion pwsh >> $PROFILE');
1478
1595
  process.exit(1);
1479
1596
  }
@@ -1786,8 +1903,7 @@ function help() {
1786
1903
  ' -s, --show-secret Show full token in current command');
1787
1904
  console.log('');
1788
1905
  console.log('Notes:');
1789
- console.log(
1790
- ' • Two ways to start Claude Code:');
1906
+ console.log(' • Two ways to start Claude Code:');
1791
1907
  console.log(
1792
1908
  ' - start: Auto-approve mode (adds --dangerously-skip-permissions)');
1793
1909
  console.log(
@@ -1843,8 +1959,10 @@ async function main() {
1843
1959
  // - Extract flags that appear BEFORE the command
1844
1960
  // - Keep command and all arguments after it unchanged (for Claude)
1845
1961
  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');
1962
+ showSecret = preCommandArgs.includes('--show-secret') ||
1963
+ preCommandArgs.includes('-s');
1964
+ permanent =
1965
+ preCommandArgs.includes('--permanent') || preCommandArgs.includes('-p');
1848
1966
 
1849
1967
  // Keep command and all arguments after it (these go to Claude)
1850
1968
  filteredArgs = commandIndex >= 0 ? args.slice(commandIndex) : [];
@@ -1856,16 +1974,10 @@ async function main() {
1856
1974
  permanent = args.includes('--permanent') || args.includes('-p');
1857
1975
 
1858
1976
  // 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
- );
1977
+ filteredArgs = args.filter(
1978
+ arg => arg !== '--show-secret' && arg !== '-s' &&
1979
+ arg !== '--permanent' && arg !== '-p' && arg !== '--version' &&
1980
+ arg !== '-V' && arg !== '--help' && arg !== '-h');
1869
1981
  }
1870
1982
 
1871
1983
  switch (command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccconfig",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Cross-platform Claude Code configuration switching tool",
5
5
  "main": "ccconfig.js",
6
6
  "bin": {