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