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.
- package/ccconfig.js +251 -152
- 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 {
|
|
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,
|
|
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 &&
|
|
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 = [
|
|
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 = [
|
|
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 &&
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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 ||
|
|
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] ||
|
|
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(
|
|
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}: ${
|
|
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 ${
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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(
|
|
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
|
|
676
|
-
console.error(` ccconfig update ${name}
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
console.log(
|
|
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, '\\\\')
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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) {
|
|
911
|
-
|
|
912
|
-
|
|
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 =
|
|
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(
|
|
1323
|
-
|
|
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 =
|
|
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 {
|
|
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 =
|
|
1377
|
-
|
|
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 =
|
|
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(
|
|
1404
|
-
|
|
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(
|
|
1408
|
-
|
|
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' &&
|
|
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', {
|
|
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', {
|
|
1563
|
+
execSync('stty echo icanon', {stdio: 'inherit'});
|
|
1460
1564
|
} catch (e2) {
|
|
1461
1565
|
try {
|
|
1462
|
-
execSync('stty echo', {
|
|
1463
|
-
execSync('stty icanon', {
|
|
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
|
|
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, {
|
|
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
|
|
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, {
|
|
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(
|
|
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', '
|
|
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') ||
|
|
1920
|
-
|
|
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(
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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');
|