@kaitranntt/ccs 2.5.0 → 3.0.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/README.md +49 -13
- package/VERSION +1 -1
- package/bin/auth-commands.js +405 -0
- package/bin/ccs.js +70 -27
- package/bin/instance-manager.js +218 -0
- package/bin/profile-detector.js +199 -0
- package/bin/profile-registry.js +226 -0
- package/config/base-kimi.settings.json +1 -0
- package/lib/ccs +1 -1
- package/lib/ccs.ps1 +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -0
package/README.md
CHANGED
|
@@ -156,33 +156,45 @@ One command. Zero downtime. No file editing. Right model, right task.
|
|
|
156
156
|
|
|
157
157
|
---
|
|
158
158
|
|
|
159
|
-
## 🏗️ Architecture Overview
|
|
159
|
+
## 🏗️ Architecture Overview (v3.0 Simplified)
|
|
160
|
+
|
|
161
|
+
**v3.0 Login-Per-Profile Model**: Each profile is an isolated Claude instance where users login directly. No credential copying or vault encryption.
|
|
160
162
|
|
|
161
163
|
```mermaid
|
|
162
164
|
graph LR
|
|
163
165
|
subgraph "User Command"
|
|
164
|
-
CMD[ccs
|
|
166
|
+
CMD[ccs <profile>]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
subgraph "Profile Detection"
|
|
170
|
+
DETECT[ProfileDetector]
|
|
171
|
+
SETTINGS[Settings-based: glm, kimi]
|
|
172
|
+
ACCOUNT[Account-based: work, personal]
|
|
165
173
|
end
|
|
166
174
|
|
|
167
175
|
subgraph "CCS Processing"
|
|
168
|
-
CONFIG[Read
|
|
169
|
-
|
|
170
|
-
VALIDATE[Validate file exists]
|
|
176
|
+
CONFIG[Read config.json/profiles.json]
|
|
177
|
+
INSTANCE[InstanceManager: lazy directory init]
|
|
171
178
|
end
|
|
172
179
|
|
|
173
|
-
subgraph "Claude CLI"
|
|
174
|
-
|
|
180
|
+
subgraph "Claude CLI Execution"
|
|
181
|
+
SETTINGS_EXEC[claude --settings <path>]
|
|
182
|
+
INSTANCE_EXEC[CLAUDE_CONFIG_DIR=<instance> claude]
|
|
175
183
|
end
|
|
176
184
|
|
|
177
185
|
subgraph "API Response"
|
|
178
|
-
API[Claude
|
|
186
|
+
API[Claude/GLM/Kimi API]
|
|
179
187
|
end
|
|
180
188
|
|
|
181
|
-
CMD -->
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
CMD --> DETECT
|
|
190
|
+
DETECT --> SETTINGS
|
|
191
|
+
DETECT --> ACCOUNT
|
|
192
|
+
SETTINGS --> CONFIG
|
|
193
|
+
ACCOUNT --> INSTANCE
|
|
194
|
+
SETTINGS --> SETTINGS_EXEC
|
|
195
|
+
ACCOUNT --> INSTANCE_EXEC
|
|
196
|
+
SETTINGS_EXEC --> API
|
|
197
|
+
INSTANCE_EXEC --> API
|
|
186
198
|
```
|
|
187
199
|
|
|
188
200
|
---
|
|
@@ -194,6 +206,13 @@ graph LR
|
|
|
194
206
|
- **Smart Detection**: Automatically uses right model for each task
|
|
195
207
|
- **Persistent**: Switch stays active until changed again
|
|
196
208
|
|
|
209
|
+
### Concurrent Sessions (All Platforms)
|
|
210
|
+
- **Multiple Profiles Simultaneously**: Run `ccs work` and `ccs personal` in different terminals concurrently
|
|
211
|
+
- **Isolated Instances**: Each profile gets own config directory (`~/.ccs/instances/<profile>/`)
|
|
212
|
+
- **Independent Sessions**: Separate login, chat sessions, todos, logs per profile
|
|
213
|
+
- **Platform Parity**: Works identically on macOS, Linux, and Windows via `CLAUDE_CONFIG_DIR`
|
|
214
|
+
- **Backward Compatible**: Existing settings profiles (glm, kimi) work unchanged
|
|
215
|
+
|
|
197
216
|
### Zero Workflow Interruption
|
|
198
217
|
- **No Downtime**: Switching happens instantly between commands
|
|
199
218
|
- **Context Preservation**: Your workflow remains uninterrupted
|
|
@@ -204,6 +223,7 @@ graph LR
|
|
|
204
223
|
|
|
205
224
|
## 💻 Usage Examples
|
|
206
225
|
|
|
226
|
+
### Basic Profile Switching
|
|
207
227
|
```bash
|
|
208
228
|
ccs # Use Claude subscription (default)
|
|
209
229
|
ccs glm # Use GLM fallback
|
|
@@ -211,6 +231,22 @@ ccs kimi # Use Kimi for Coding
|
|
|
211
231
|
ccs --version # Show CCS version and install location
|
|
212
232
|
```
|
|
213
233
|
|
|
234
|
+
### Concurrent Sessions (Multi-Account)
|
|
235
|
+
```bash
|
|
236
|
+
# First time: Create profile and login
|
|
237
|
+
ccs auth create work # Opens Claude, prompts for login
|
|
238
|
+
ccs auth create personal # Opens Claude, prompts for login
|
|
239
|
+
|
|
240
|
+
# Terminal 1 - Work account
|
|
241
|
+
ccs work "implement feature"
|
|
242
|
+
|
|
243
|
+
# Terminal 2 - Personal account (concurrent)
|
|
244
|
+
ccs personal "review code"
|
|
245
|
+
|
|
246
|
+
# Both run simultaneously with isolated logins/sessions
|
|
247
|
+
# Works on all platforms: macOS, Linux, Windows
|
|
248
|
+
```
|
|
249
|
+
|
|
214
250
|
---
|
|
215
251
|
|
|
216
252
|
### 🗑️ Official Uninstall
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.0
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const ProfileRegistry = require('./profile-registry');
|
|
5
|
+
const InstanceManager = require('./instance-manager');
|
|
6
|
+
const { colored } = require('./helpers');
|
|
7
|
+
const { detectClaudeCli } = require('./claude-detector');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Auth Commands (Simplified)
|
|
11
|
+
*
|
|
12
|
+
* CLI interface for CCS multi-account management.
|
|
13
|
+
* Commands: create, list, show, remove, default
|
|
14
|
+
*
|
|
15
|
+
* Login-per-profile model: Each profile is an isolated Claude instance.
|
|
16
|
+
* Users login directly in each instance (no credential copying).
|
|
17
|
+
*/
|
|
18
|
+
class AuthCommands {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.registry = new ProfileRegistry();
|
|
21
|
+
this.instanceMgr = new InstanceManager();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Show help for auth commands
|
|
26
|
+
*/
|
|
27
|
+
showHelp() {
|
|
28
|
+
console.log(colored('CCS Account Management', 'bold'));
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(colored('Usage:', 'cyan'));
|
|
31
|
+
console.log(` ${colored('ccs auth', 'yellow')} <command> [options]`);
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(colored('Commands:', 'cyan'));
|
|
34
|
+
console.log(` ${colored('create <profile>', 'yellow')} Create new profile and login`);
|
|
35
|
+
console.log(` ${colored('list', 'yellow')} List all saved profiles`);
|
|
36
|
+
console.log(` ${colored('show <profile>', 'yellow')} Show profile details`);
|
|
37
|
+
console.log(` ${colored('remove <profile>', 'yellow')} Remove saved profile`);
|
|
38
|
+
console.log(` ${colored('default <profile>', 'yellow')} Set default profile`);
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(colored('Examples:', 'cyan'));
|
|
41
|
+
console.log(` ${colored('ccs auth create work', 'yellow')} # Create & login to work profile`);
|
|
42
|
+
console.log(` ${colored('ccs auth list', 'yellow')} # List all profiles`);
|
|
43
|
+
console.log(` ${colored('ccs work "review code"', 'yellow')} # Use work profile`);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log(colored('Options:', 'cyan'));
|
|
46
|
+
console.log(` ${colored('--force', 'yellow')} Allow overwriting existing profile`);
|
|
47
|
+
console.log('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create new profile and prompt for login
|
|
52
|
+
* @param {Array} args - Command arguments
|
|
53
|
+
*/
|
|
54
|
+
async handleCreate(args) {
|
|
55
|
+
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
56
|
+
const force = args.includes('--force');
|
|
57
|
+
|
|
58
|
+
if (!profileName) {
|
|
59
|
+
console.error('[X] Profile name is required');
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(`Usage: ${colored('ccs auth create <profile> [--force]', 'yellow')}`);
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log('Example:');
|
|
64
|
+
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if profile already exists
|
|
69
|
+
if (!force && this.registry.hasProfile(profileName)) {
|
|
70
|
+
console.error(`[X] Profile already exists: ${profileName}`);
|
|
71
|
+
console.log(` Use ${colored('--force', 'yellow')} to overwrite`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Create instance directory
|
|
77
|
+
console.log(`[i] Creating profile: ${profileName}`);
|
|
78
|
+
const instancePath = this.instanceMgr.ensureInstance(profileName);
|
|
79
|
+
|
|
80
|
+
// Create/update profile entry
|
|
81
|
+
if (this.registry.hasProfile(profileName)) {
|
|
82
|
+
this.registry.updateProfile(profileName, {
|
|
83
|
+
type: 'account'
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
this.registry.createProfile(profileName, {
|
|
87
|
+
type: 'account'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`[i] Instance directory: ${instancePath}`);
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(colored('[i] Starting Claude in isolated instance...', 'yellow'));
|
|
94
|
+
console.log(colored('[i] You will be prompted to login with your account.', 'yellow'));
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// Detect Claude CLI
|
|
98
|
+
const claudeCli = detectClaudeCli();
|
|
99
|
+
if (!claudeCli) {
|
|
100
|
+
console.error('[X] Claude CLI not found');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Please install Claude CLI first:');
|
|
103
|
+
console.log(' https://claude.ai/download');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Execute Claude in isolated instance (will auto-prompt for login if no credentials)
|
|
108
|
+
const child = spawn(claudeCli, [], {
|
|
109
|
+
stdio: 'inherit',
|
|
110
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
child.on('exit', (code) => {
|
|
114
|
+
if (code === 0) {
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(colored('[OK] Profile created successfully', 'green'));
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(` Profile: ${profileName}`);
|
|
119
|
+
console.log(` Instance: ${instancePath}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('Usage:');
|
|
122
|
+
console.log(` ${colored(`ccs ${profileName} "your prompt here"`, 'yellow')}`);
|
|
123
|
+
console.log('');
|
|
124
|
+
process.exit(0);
|
|
125
|
+
} else {
|
|
126
|
+
console.log('');
|
|
127
|
+
console.error('[X] Login failed or cancelled');
|
|
128
|
+
console.log('');
|
|
129
|
+
console.log('To retry:');
|
|
130
|
+
console.log(` ${colored(`ccs auth create ${profileName} --force`, 'yellow')}`);
|
|
131
|
+
console.log('');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
child.on('error', (err) => {
|
|
137
|
+
console.error(`[X] Failed to execute Claude CLI: ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`[X] Failed to create profile: ${error.message}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* List all saved profiles
|
|
149
|
+
* @param {Array} args - Command arguments
|
|
150
|
+
*/
|
|
151
|
+
async handleList(args) {
|
|
152
|
+
const verbose = args.includes('--verbose');
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const profiles = this.registry.getAllProfiles();
|
|
156
|
+
const defaultProfile = this.registry.getDefaultProfile();
|
|
157
|
+
const profileNames = Object.keys(profiles);
|
|
158
|
+
|
|
159
|
+
if (profileNames.length === 0) {
|
|
160
|
+
console.log(colored('No account profiles found', 'yellow'));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log('To create your first profile:');
|
|
163
|
+
console.log(` ${colored('ccs auth create <profile>', 'yellow')} # Create and login to profile`);
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log('Example:');
|
|
166
|
+
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
|
167
|
+
console.log('');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(colored('Saved Account Profiles:', 'bold'));
|
|
172
|
+
console.log('');
|
|
173
|
+
|
|
174
|
+
// Sort by last_used (descending), then alphabetically
|
|
175
|
+
const sorted = profileNames.sort((a, b) => {
|
|
176
|
+
const aProfile = profiles[a];
|
|
177
|
+
const bProfile = profiles[b];
|
|
178
|
+
|
|
179
|
+
// Default first
|
|
180
|
+
if (a === defaultProfile) return -1;
|
|
181
|
+
if (b === defaultProfile) return 1;
|
|
182
|
+
|
|
183
|
+
// Then by last_used
|
|
184
|
+
if (aProfile.last_used && bProfile.last_used) {
|
|
185
|
+
return new Date(bProfile.last_used) - new Date(aProfile.last_used);
|
|
186
|
+
}
|
|
187
|
+
if (aProfile.last_used) return -1;
|
|
188
|
+
if (bProfile.last_used) return 1;
|
|
189
|
+
|
|
190
|
+
// Then alphabetically
|
|
191
|
+
return a.localeCompare(b);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
sorted.forEach(name => {
|
|
195
|
+
const profile = profiles[name];
|
|
196
|
+
const isDefault = name === defaultProfile;
|
|
197
|
+
const indicator = isDefault ? colored('[*]', 'green') : '[ ]';
|
|
198
|
+
|
|
199
|
+
console.log(`${indicator} ${colored(name, 'cyan')}${isDefault ? colored(' (default)', 'green') : ''}`);
|
|
200
|
+
|
|
201
|
+
console.log(` Type: ${profile.type || 'account'}`);
|
|
202
|
+
|
|
203
|
+
if (verbose) {
|
|
204
|
+
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
|
205
|
+
if (profile.last_used) {
|
|
206
|
+
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
console.log(`Total profiles: ${profileNames.length}`);
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`[X] Failed to list profiles: ${error.message}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Show details for a specific profile
|
|
224
|
+
* @param {Array} args - Command arguments
|
|
225
|
+
*/
|
|
226
|
+
async handleShow(args) {
|
|
227
|
+
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
228
|
+
|
|
229
|
+
if (!profileName) {
|
|
230
|
+
console.error('[X] Profile name is required');
|
|
231
|
+
console.log('');
|
|
232
|
+
console.log(`Usage: ${colored('ccs auth show <profile>', 'yellow')}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const profile = this.registry.getProfile(profileName);
|
|
238
|
+
const defaultProfile = this.registry.getDefaultProfile();
|
|
239
|
+
const isDefault = profileName === defaultProfile;
|
|
240
|
+
|
|
241
|
+
console.log(colored(`Profile: ${profileName}`, 'bold'));
|
|
242
|
+
console.log('');
|
|
243
|
+
console.log(` Type: ${profile.type || 'account'}`);
|
|
244
|
+
console.log(` Default: ${isDefault ? 'Yes' : 'No'}`);
|
|
245
|
+
console.log(` Instance: ${this.instanceMgr.getInstancePath(profileName)}`);
|
|
246
|
+
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
|
247
|
+
|
|
248
|
+
if (profile.last_used) {
|
|
249
|
+
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
|
250
|
+
} else {
|
|
251
|
+
console.log(` Last used: Never`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log('');
|
|
255
|
+
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(`[X] ${error.message}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Remove a saved profile
|
|
264
|
+
* @param {Array} args - Command arguments
|
|
265
|
+
*/
|
|
266
|
+
async handleRemove(args) {
|
|
267
|
+
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
268
|
+
const force = args.includes('--force');
|
|
269
|
+
|
|
270
|
+
if (!profileName) {
|
|
271
|
+
console.error('[X] Profile name is required');
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(`Usage: ${colored('ccs auth remove <profile> [--force]', 'yellow')}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!this.registry.hasProfile(profileName)) {
|
|
278
|
+
console.error(`[X] Profile not found: ${profileName}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Require --force for safety
|
|
283
|
+
if (!force) {
|
|
284
|
+
console.error('[X] Removal requires --force flag for safety');
|
|
285
|
+
console.log('');
|
|
286
|
+
console.log(`Run: ${colored(`ccs auth remove ${profileName} --force`, 'yellow')}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Delete instance
|
|
292
|
+
this.instanceMgr.deleteInstance(profileName);
|
|
293
|
+
|
|
294
|
+
// Delete profile
|
|
295
|
+
this.registry.deleteProfile(profileName);
|
|
296
|
+
|
|
297
|
+
console.log(colored('[OK] Profile removed successfully', 'green'));
|
|
298
|
+
console.log(` Profile: ${profileName}`);
|
|
299
|
+
console.log('');
|
|
300
|
+
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error(`[X] Failed to remove profile: ${error.message}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Set default profile
|
|
309
|
+
* @param {Array} args - Command arguments
|
|
310
|
+
*/
|
|
311
|
+
async handleDefault(args) {
|
|
312
|
+
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
313
|
+
|
|
314
|
+
if (!profileName) {
|
|
315
|
+
console.error('[X] Profile name is required');
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(`Usage: ${colored('ccs auth default <profile>', 'yellow')}`);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
this.registry.setDefaultProfile(profileName);
|
|
323
|
+
|
|
324
|
+
console.log(colored('[OK] Default profile set', 'green'));
|
|
325
|
+
console.log(` Profile: ${profileName}`);
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log('Now you can use:');
|
|
328
|
+
console.log(` ${colored('ccs "your prompt"', 'yellow')} # Uses ${profileName} profile`);
|
|
329
|
+
console.log('');
|
|
330
|
+
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`[X] ${error.message}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Route auth command to appropriate handler
|
|
339
|
+
* @param {Array} args - Command arguments
|
|
340
|
+
*/
|
|
341
|
+
async route(args) {
|
|
342
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
343
|
+
this.showHelp();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const command = args[0];
|
|
348
|
+
const commandArgs = args.slice(1);
|
|
349
|
+
|
|
350
|
+
switch (command) {
|
|
351
|
+
case 'create':
|
|
352
|
+
await this.handleCreate(commandArgs);
|
|
353
|
+
break;
|
|
354
|
+
|
|
355
|
+
case 'save':
|
|
356
|
+
// Deprecated - redirect to create
|
|
357
|
+
console.log(colored('[!] Command "save" is deprecated', 'yellow'));
|
|
358
|
+
console.log(` Use: ${colored('ccs auth create <profile>', 'yellow')} instead`);
|
|
359
|
+
console.log('');
|
|
360
|
+
await this.handleCreate(commandArgs);
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case 'list':
|
|
364
|
+
await this.handleList(commandArgs);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'show':
|
|
368
|
+
await this.handleShow(commandArgs);
|
|
369
|
+
break;
|
|
370
|
+
|
|
371
|
+
case 'remove':
|
|
372
|
+
await this.handleRemove(commandArgs);
|
|
373
|
+
break;
|
|
374
|
+
|
|
375
|
+
case 'default':
|
|
376
|
+
await this.handleDefault(commandArgs);
|
|
377
|
+
break;
|
|
378
|
+
|
|
379
|
+
case 'current':
|
|
380
|
+
console.log(colored('[!] Command "current" has been removed', 'yellow'));
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log('Each profile has its own login in an isolated instance.');
|
|
383
|
+
console.log('Use "ccs auth list" to see all profiles.');
|
|
384
|
+
console.log('');
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
case 'cleanup':
|
|
388
|
+
console.log(colored('[!] Command "cleanup" has been removed', 'yellow'));
|
|
389
|
+
console.log('');
|
|
390
|
+
console.log('No cleanup needed - no separate vault files.');
|
|
391
|
+
console.log('Use "ccs auth list" to see all profiles.');
|
|
392
|
+
console.log('');
|
|
393
|
+
break;
|
|
394
|
+
|
|
395
|
+
default:
|
|
396
|
+
console.error(`[X] Unknown command: ${command}`);
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log('Run for help:');
|
|
399
|
+
console.log(` ${colored('ccs auth --help', 'yellow')}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = AuthCommands;
|
package/bin/ccs.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const { spawn } = require('child_process');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
7
8
|
const { error, colored } = require('./helpers');
|
|
8
9
|
const { detectClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
|
|
9
10
|
const { getSettingsPath, getConfigPath } = require('./config-manager');
|
|
@@ -18,10 +19,13 @@ function escapeShellArg(arg) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// Execute Claude CLI with unified spawn logic
|
|
21
|
-
function execClaude(claudeCli, args) {
|
|
22
|
+
function execClaude(claudeCli, args, envVars = null) {
|
|
22
23
|
const isWindows = process.platform === 'win32';
|
|
23
24
|
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
|
24
25
|
|
|
26
|
+
// Prepare environment (merge with process.env if envVars provided)
|
|
27
|
+
const env = envVars ? { ...process.env, ...envVars } : process.env;
|
|
28
|
+
|
|
25
29
|
let child;
|
|
26
30
|
if (needsShell) {
|
|
27
31
|
// When shell needed: concatenate into string to avoid DEP0190 warning
|
|
@@ -29,13 +33,15 @@ function execClaude(claudeCli, args) {
|
|
|
29
33
|
child = spawn(cmdString, {
|
|
30
34
|
stdio: 'inherit',
|
|
31
35
|
windowsHide: true,
|
|
32
|
-
shell: true
|
|
36
|
+
shell: true,
|
|
37
|
+
env
|
|
33
38
|
});
|
|
34
39
|
} else {
|
|
35
40
|
// When no shell needed: use array form (faster, no shell overhead)
|
|
36
41
|
child = spawn(claudeCli, args, {
|
|
37
42
|
stdio: 'inherit',
|
|
38
|
-
windowsHide: true
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
env
|
|
39
45
|
});
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -100,10 +106,16 @@ function handleHelpCommand() {
|
|
|
100
106
|
console.log(` ${colored('ccs', 'yellow')} Use default profile`);
|
|
101
107
|
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM profile`);
|
|
102
108
|
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi profile`);
|
|
109
|
+
console.log(` ${colored('ccs work', 'yellow')} Use work account (saved profile)`);
|
|
103
110
|
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Switch to GLM and run command`);
|
|
104
|
-
console.log(` ${colored('ccs
|
|
105
|
-
console.log(
|
|
106
|
-
|
|
111
|
+
console.log(` ${colored('ccs work', 'yellow')} "review code" Use work account and run command`);
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
// Account Management
|
|
115
|
+
console.log(colored('Account Management:', 'cyan'));
|
|
116
|
+
console.log(` ${colored('ccs auth create <profile>', 'yellow')} Create new profile and login`);
|
|
117
|
+
console.log(` ${colored('ccs auth list', 'yellow')} List all saved profiles`);
|
|
118
|
+
console.log(` ${colored('ccs auth --help', 'yellow')} Show account management help`);
|
|
107
119
|
console.log('');
|
|
108
120
|
|
|
109
121
|
// Flags
|
|
@@ -195,7 +207,7 @@ function detectProfile(args) {
|
|
|
195
207
|
}
|
|
196
208
|
|
|
197
209
|
// Main execution
|
|
198
|
-
function main() {
|
|
210
|
+
async function main() {
|
|
199
211
|
const args = process.argv.slice(2);
|
|
200
212
|
|
|
201
213
|
// Special case: version command (check BEFORE profile detection)
|
|
@@ -222,36 +234,67 @@ function main() {
|
|
|
222
234
|
return;
|
|
223
235
|
}
|
|
224
236
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const claudeCli = detectClaudeCli();
|
|
231
|
-
if (!claudeCli) {
|
|
232
|
-
showClaudeNotFoundError();
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
execClaude(claudeCli, remainingArgs);
|
|
237
|
+
// Special case: auth command (multi-account management)
|
|
238
|
+
if (firstArg === 'auth') {
|
|
239
|
+
const AuthCommands = require('./auth-commands');
|
|
240
|
+
const authCommands = new AuthCommands();
|
|
241
|
+
await authCommands.route(args.slice(1));
|
|
237
242
|
return;
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
//
|
|
241
|
-
const
|
|
245
|
+
// Detect profile
|
|
246
|
+
const { profile, remainingArgs } = detectProfile(args);
|
|
242
247
|
|
|
243
|
-
// Detect Claude CLI
|
|
248
|
+
// Detect Claude CLI first (needed for all paths)
|
|
244
249
|
const claudeCli = detectClaudeCli();
|
|
245
|
-
|
|
246
|
-
// Check if claude was found
|
|
247
250
|
if (!claudeCli) {
|
|
248
251
|
showClaudeNotFoundError();
|
|
249
252
|
process.exit(1);
|
|
250
253
|
}
|
|
251
254
|
|
|
252
|
-
//
|
|
253
|
-
|
|
255
|
+
// Use ProfileDetector to determine profile type
|
|
256
|
+
const ProfileDetector = require('./profile-detector');
|
|
257
|
+
const InstanceManager = require('./instance-manager');
|
|
258
|
+
const ProfileRegistry = require('./profile-registry');
|
|
259
|
+
const { getSettingsPath } = require('./config-manager');
|
|
260
|
+
|
|
261
|
+
const detector = new ProfileDetector();
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const profileInfo = detector.detectProfileType(profile);
|
|
265
|
+
|
|
266
|
+
if (profileInfo.type === 'settings') {
|
|
267
|
+
// EXISTING FLOW: Settings-based profile (glm, kimi)
|
|
268
|
+
// Use --settings flag (backward compatible)
|
|
269
|
+
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
|
270
|
+
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]);
|
|
271
|
+
} else if (profileInfo.type === 'account') {
|
|
272
|
+
// NEW FLOW: Account-based profile (work, personal)
|
|
273
|
+
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
|
274
|
+
const registry = new ProfileRegistry();
|
|
275
|
+
const instanceMgr = new InstanceManager();
|
|
276
|
+
|
|
277
|
+
// Ensure instance exists (lazy init if needed)
|
|
278
|
+
const instancePath = instanceMgr.ensureInstance(profileInfo.name);
|
|
279
|
+
|
|
280
|
+
// Update last_used timestamp
|
|
281
|
+
registry.touchProfile(profileInfo.name);
|
|
282
|
+
|
|
283
|
+
// Execute Claude with instance isolation
|
|
284
|
+
const envVars = { CLAUDE_CONFIG_DIR: instancePath };
|
|
285
|
+
execClaude(claudeCli, remainingArgs, envVars);
|
|
286
|
+
} else {
|
|
287
|
+
// DEFAULT: No profile configured, use Claude's own defaults
|
|
288
|
+
execClaude(claudeCli, remainingArgs);
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error(`[X] ${error.message}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
254
294
|
}
|
|
255
295
|
|
|
256
296
|
// Run main
|
|
257
|
-
main()
|
|
297
|
+
main().catch(error => {
|
|
298
|
+
console.error('Fatal error:', error.message);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Instance Manager (Simplified)
|
|
9
|
+
*
|
|
10
|
+
* Manages isolated Claude CLI instances per profile for concurrent sessions.
|
|
11
|
+
* Each instance is an isolated CLAUDE_CONFIG_DIR where users login directly.
|
|
12
|
+
* No credential copying/encryption - Claude manages credentials per instance.
|
|
13
|
+
*/
|
|
14
|
+
class InstanceManager {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.instancesDir = path.join(os.homedir(), '.ccs', 'instances');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensure instance exists for profile (lazy init only)
|
|
21
|
+
* @param {string} profileName - Profile name
|
|
22
|
+
* @returns {string} Instance path
|
|
23
|
+
*/
|
|
24
|
+
ensureInstance(profileName) {
|
|
25
|
+
const instancePath = this.getInstancePath(profileName);
|
|
26
|
+
|
|
27
|
+
// Lazy initialization
|
|
28
|
+
if (!fs.existsSync(instancePath)) {
|
|
29
|
+
this.initializeInstance(profileName, instancePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate structure (auto-fix missing dirs)
|
|
33
|
+
this.validateInstance(instancePath);
|
|
34
|
+
|
|
35
|
+
return instancePath;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get instance path for profile
|
|
40
|
+
* @param {string} profileName - Profile name
|
|
41
|
+
* @returns {string} Instance directory path
|
|
42
|
+
*/
|
|
43
|
+
getInstancePath(profileName) {
|
|
44
|
+
const safeName = this._sanitizeName(profileName);
|
|
45
|
+
return path.join(this.instancesDir, safeName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize new instance directory
|
|
50
|
+
* @param {string} profileName - Profile name
|
|
51
|
+
* @param {string} instancePath - Instance directory path
|
|
52
|
+
* @throws {Error} If initialization fails
|
|
53
|
+
*/
|
|
54
|
+
initializeInstance(profileName, instancePath) {
|
|
55
|
+
try {
|
|
56
|
+
// Create base directory
|
|
57
|
+
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
|
|
58
|
+
|
|
59
|
+
// Create Claude-expected subdirectories
|
|
60
|
+
const subdirs = [
|
|
61
|
+
'session-env',
|
|
62
|
+
'todos',
|
|
63
|
+
'logs',
|
|
64
|
+
'file-history',
|
|
65
|
+
'shell-snapshots',
|
|
66
|
+
'debug',
|
|
67
|
+
'.anthropic',
|
|
68
|
+
'commands',
|
|
69
|
+
'skills'
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
subdirs.forEach(dir => {
|
|
73
|
+
const dirPath = path.join(instancePath, dir);
|
|
74
|
+
if (!fs.existsSync(dirPath)) {
|
|
75
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Copy global configs if exist
|
|
80
|
+
this._copyGlobalConfigs(instancePath);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new Error(`Failed to initialize instance for ${profileName}: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate instance directory structure (auto-fix missing directories)
|
|
88
|
+
* @param {string} instancePath - Instance path
|
|
89
|
+
*/
|
|
90
|
+
validateInstance(instancePath) {
|
|
91
|
+
// Check required directories (auto-create if missing for migration)
|
|
92
|
+
const requiredDirs = [
|
|
93
|
+
'session-env',
|
|
94
|
+
'todos',
|
|
95
|
+
'logs',
|
|
96
|
+
'file-history',
|
|
97
|
+
'shell-snapshots',
|
|
98
|
+
'debug',
|
|
99
|
+
'.anthropic'
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
for (const dir of requiredDirs) {
|
|
103
|
+
const dirPath = path.join(instancePath, dir);
|
|
104
|
+
if (!fs.existsSync(dirPath)) {
|
|
105
|
+
// Auto-create missing directory (migration from older versions)
|
|
106
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Note: Credentials managed by Claude CLI in instance (no validation needed)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Delete instance for profile
|
|
115
|
+
* @param {string} profileName - Profile name
|
|
116
|
+
*/
|
|
117
|
+
deleteInstance(profileName) {
|
|
118
|
+
const instancePath = this.getInstancePath(profileName);
|
|
119
|
+
|
|
120
|
+
if (!fs.existsSync(instancePath)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Recursive delete
|
|
125
|
+
fs.rmSync(instancePath, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* List all instance names
|
|
130
|
+
* @returns {Array<string>} Instance names
|
|
131
|
+
*/
|
|
132
|
+
listInstances() {
|
|
133
|
+
if (!fs.existsSync(this.instancesDir)) {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return fs.readdirSync(this.instancesDir)
|
|
138
|
+
.filter(name => {
|
|
139
|
+
const instancePath = path.join(this.instancesDir, name);
|
|
140
|
+
return fs.statSync(instancePath).isDirectory();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if instance exists for profile
|
|
146
|
+
* @param {string} profileName - Profile name
|
|
147
|
+
* @returns {boolean} True if exists
|
|
148
|
+
*/
|
|
149
|
+
hasInstance(profileName) {
|
|
150
|
+
const instancePath = this.getInstancePath(profileName);
|
|
151
|
+
return fs.existsSync(instancePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Copy global configs to instance (optional)
|
|
156
|
+
* @param {string} instancePath - Instance path
|
|
157
|
+
*/
|
|
158
|
+
_copyGlobalConfigs(instancePath) {
|
|
159
|
+
const globalConfigDir = path.join(os.homedir(), '.claude');
|
|
160
|
+
|
|
161
|
+
// Copy settings.json if exists
|
|
162
|
+
const globalSettings = path.join(globalConfigDir, 'settings.json');
|
|
163
|
+
if (fs.existsSync(globalSettings)) {
|
|
164
|
+
const instanceSettings = path.join(instancePath, 'settings.json');
|
|
165
|
+
fs.copyFileSync(globalSettings, instanceSettings);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Copy commands directory if exists
|
|
169
|
+
const globalCommands = path.join(globalConfigDir, 'commands');
|
|
170
|
+
if (fs.existsSync(globalCommands)) {
|
|
171
|
+
const instanceCommands = path.join(instancePath, 'commands');
|
|
172
|
+
this._copyDirectory(globalCommands, instanceCommands);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Copy skills directory if exists
|
|
176
|
+
const globalSkills = path.join(globalConfigDir, 'skills');
|
|
177
|
+
if (fs.existsSync(globalSkills)) {
|
|
178
|
+
const instanceSkills = path.join(instancePath, 'skills');
|
|
179
|
+
this._copyDirectory(globalSkills, instanceSkills);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Copy directory recursively
|
|
185
|
+
* @param {string} src - Source directory
|
|
186
|
+
* @param {string} dest - Destination directory
|
|
187
|
+
*/
|
|
188
|
+
_copyDirectory(src, dest) {
|
|
189
|
+
if (!fs.existsSync(dest)) {
|
|
190
|
+
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
194
|
+
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
const srcPath = path.join(src, entry.name);
|
|
197
|
+
const destPath = path.join(dest, entry.name);
|
|
198
|
+
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
this._copyDirectory(srcPath, destPath);
|
|
201
|
+
} else {
|
|
202
|
+
fs.copyFileSync(srcPath, destPath);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Sanitize profile name for filesystem
|
|
209
|
+
* @param {string} name - Profile name
|
|
210
|
+
* @returns {string} Safe name
|
|
211
|
+
*/
|
|
212
|
+
_sanitizeName(name) {
|
|
213
|
+
// Replace unsafe characters with dash
|
|
214
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = InstanceManager;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Profile Detector
|
|
9
|
+
*
|
|
10
|
+
* Determines profile type (settings-based vs account-based) for routing.
|
|
11
|
+
* Priority: settings-based profiles (glm/kimi) checked FIRST for backward compatibility.
|
|
12
|
+
*/
|
|
13
|
+
class ProfileDetector {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.configPath = path.join(os.homedir(), '.ccs', 'config.json');
|
|
16
|
+
this.profilesPath = path.join(os.homedir(), '.ccs', 'profiles.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read settings-based config (config.json)
|
|
21
|
+
* @returns {Object} Config data
|
|
22
|
+
*/
|
|
23
|
+
_readConfig() {
|
|
24
|
+
if (!fs.existsSync(this.configPath)) {
|
|
25
|
+
return { profiles: {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.warn(`[!] Warning: Could not read config.json: ${error.message}`);
|
|
33
|
+
return { profiles: {} };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read account-based profiles (profiles.json)
|
|
39
|
+
* @returns {Object} Profiles data
|
|
40
|
+
*/
|
|
41
|
+
_readProfiles() {
|
|
42
|
+
if (!fs.existsSync(this.profilesPath)) {
|
|
43
|
+
return { profiles: {}, default: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const data = fs.readFileSync(this.profilesPath, 'utf8');
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.warn(`[!] Warning: Could not read profiles.json: ${error.message}`);
|
|
51
|
+
return { profiles: {}, default: null };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect profile type and return routing information
|
|
57
|
+
* @param {string} profileName - Profile name to detect
|
|
58
|
+
* @returns {Object} {type: 'settings'|'account'|'default', ...info}
|
|
59
|
+
*/
|
|
60
|
+
detectProfileType(profileName) {
|
|
61
|
+
// Special case: 'default' means use default profile
|
|
62
|
+
if (profileName === 'default' || profileName === null || profileName === undefined) {
|
|
63
|
+
return this._resolveDefaultProfile();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Priority 1: Check settings-based profiles (glm, kimi) - BACKWARD COMPATIBILITY
|
|
67
|
+
const config = this._readConfig();
|
|
68
|
+
|
|
69
|
+
if (config.profiles && config.profiles[profileName]) {
|
|
70
|
+
return {
|
|
71
|
+
type: 'settings',
|
|
72
|
+
name: profileName,
|
|
73
|
+
settingsPath: config.profiles[profileName]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Priority 2: Check account-based profiles (work, personal)
|
|
78
|
+
const profiles = this._readProfiles();
|
|
79
|
+
|
|
80
|
+
if (profiles.profiles && profiles.profiles[profileName]) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'account',
|
|
83
|
+
name: profileName,
|
|
84
|
+
profile: profiles.profiles[profileName]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Not found
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Profile not found: ${profileName}\n` +
|
|
91
|
+
`Available profiles:\n` +
|
|
92
|
+
this._listAvailableProfiles()
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve default profile
|
|
98
|
+
* @returns {Object} Default profile info
|
|
99
|
+
*/
|
|
100
|
+
_resolveDefaultProfile() {
|
|
101
|
+
// Check if account-based default exists
|
|
102
|
+
const profiles = this._readProfiles();
|
|
103
|
+
|
|
104
|
+
if (profiles.default && profiles.profiles[profiles.default]) {
|
|
105
|
+
return {
|
|
106
|
+
type: 'account',
|
|
107
|
+
name: profiles.default,
|
|
108
|
+
profile: profiles.profiles[profiles.default]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if settings-based default exists
|
|
113
|
+
const config = this._readConfig();
|
|
114
|
+
|
|
115
|
+
if (config.profiles && config.profiles['default']) {
|
|
116
|
+
return {
|
|
117
|
+
type: 'settings',
|
|
118
|
+
name: 'default',
|
|
119
|
+
settingsPath: config.profiles['default']
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// No default profile configured, use Claude's own defaults
|
|
124
|
+
return {
|
|
125
|
+
type: 'default',
|
|
126
|
+
name: 'default',
|
|
127
|
+
message: 'No profile configured. Using Claude CLI defaults from ~/.claude/'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List available profiles (for error messages)
|
|
133
|
+
* @returns {string} Formatted list
|
|
134
|
+
*/
|
|
135
|
+
_listAvailableProfiles() {
|
|
136
|
+
const lines = [];
|
|
137
|
+
|
|
138
|
+
// Settings-based profiles
|
|
139
|
+
const config = this._readConfig();
|
|
140
|
+
const settingsProfiles = Object.keys(config.profiles || {});
|
|
141
|
+
|
|
142
|
+
if (settingsProfiles.length > 0) {
|
|
143
|
+
lines.push('Settings-based profiles (GLM, Kimi, etc.):');
|
|
144
|
+
settingsProfiles.forEach(name => {
|
|
145
|
+
lines.push(` - ${name}`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Account-based profiles
|
|
150
|
+
const profiles = this._readProfiles();
|
|
151
|
+
const accountProfiles = Object.keys(profiles.profiles || {});
|
|
152
|
+
|
|
153
|
+
if (accountProfiles.length > 0) {
|
|
154
|
+
lines.push('Account-based profiles:');
|
|
155
|
+
accountProfiles.forEach(name => {
|
|
156
|
+
const isDefault = name === profiles.default;
|
|
157
|
+
lines.push(` - ${name}${isDefault ? ' [DEFAULT]' : ''}`);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (lines.length === 0) {
|
|
162
|
+
return ' (no profiles configured)\n' +
|
|
163
|
+
' Run "ccs auth save <profile>" to create your first account profile.';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return lines.join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if profile exists (any type)
|
|
171
|
+
* @param {string} profileName - Profile name
|
|
172
|
+
* @returns {boolean} True if exists
|
|
173
|
+
*/
|
|
174
|
+
hasProfile(profileName) {
|
|
175
|
+
try {
|
|
176
|
+
this.detectProfileType(profileName);
|
|
177
|
+
return true;
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get all available profile names
|
|
185
|
+
* @returns {Object} {settings: [...], accounts: [...]}
|
|
186
|
+
*/
|
|
187
|
+
getAllProfiles() {
|
|
188
|
+
const config = this._readConfig();
|
|
189
|
+
const profiles = this._readProfiles();
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
settings: Object.keys(config.profiles || {}),
|
|
193
|
+
accounts: Object.keys(profiles.profiles || {}),
|
|
194
|
+
default: profiles.default
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = ProfileDetector;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Profile Registry (Simplified)
|
|
9
|
+
*
|
|
10
|
+
* Manages account profile metadata in ~/.ccs/profiles.json
|
|
11
|
+
* Each profile represents an isolated Claude instance with login credentials.
|
|
12
|
+
*
|
|
13
|
+
* Profile Schema (v3.0 - Minimal):
|
|
14
|
+
* {
|
|
15
|
+
* type: 'account', // Profile type
|
|
16
|
+
* created: <ISO timestamp>, // Creation time
|
|
17
|
+
* last_used: <ISO timestamp or null> // Last usage time
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Removed fields from v2.x:
|
|
21
|
+
* - vault: No encrypted vault (credentials in instance)
|
|
22
|
+
* - subscription: No credential reading
|
|
23
|
+
* - email: No credential reading
|
|
24
|
+
*/
|
|
25
|
+
class ProfileRegistry {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.profilesPath = path.join(os.homedir(), '.ccs', 'profiles.json');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read profiles from disk
|
|
32
|
+
* @returns {Object} Profiles data
|
|
33
|
+
*/
|
|
34
|
+
_read() {
|
|
35
|
+
if (!fs.existsSync(this.profilesPath)) {
|
|
36
|
+
return {
|
|
37
|
+
version: '2.0.0',
|
|
38
|
+
profiles: {},
|
|
39
|
+
default: null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const data = fs.readFileSync(this.profilesPath, 'utf8');
|
|
45
|
+
return JSON.parse(data);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Failed to read profiles: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write profiles to disk atomically
|
|
53
|
+
* @param {Object} data - Profiles data
|
|
54
|
+
*/
|
|
55
|
+
_write(data) {
|
|
56
|
+
const dir = path.dirname(this.profilesPath);
|
|
57
|
+
|
|
58
|
+
// Ensure directory exists
|
|
59
|
+
if (!fs.existsSync(dir)) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Atomic write: temp file + rename
|
|
64
|
+
const tempPath = `${this.profilesPath}.tmp`;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(tempPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
68
|
+
fs.renameSync(tempPath, this.profilesPath);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Cleanup temp file on error
|
|
71
|
+
if (fs.existsSync(tempPath)) {
|
|
72
|
+
fs.unlinkSync(tempPath);
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Failed to write profiles: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new profile
|
|
80
|
+
* @param {string} name - Profile name
|
|
81
|
+
* @param {Object} metadata - Profile metadata (type, created, last_used)
|
|
82
|
+
*/
|
|
83
|
+
createProfile(name, metadata = {}) {
|
|
84
|
+
const data = this._read();
|
|
85
|
+
|
|
86
|
+
if (data.profiles[name]) {
|
|
87
|
+
throw new Error(`Profile already exists: ${name}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// v3.0 minimal schema: only essential fields
|
|
91
|
+
data.profiles[name] = {
|
|
92
|
+
type: metadata.type || 'account',
|
|
93
|
+
created: new Date().toISOString(),
|
|
94
|
+
last_used: null
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Set as default if no default exists
|
|
98
|
+
if (!data.default) {
|
|
99
|
+
data.default = name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._write(data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get profile metadata
|
|
107
|
+
* @param {string} name - Profile name
|
|
108
|
+
* @returns {Object} Profile metadata
|
|
109
|
+
*/
|
|
110
|
+
getProfile(name) {
|
|
111
|
+
const data = this._read();
|
|
112
|
+
|
|
113
|
+
if (!data.profiles[name]) {
|
|
114
|
+
throw new Error(`Profile not found: ${name}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return data.profiles[name];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Update profile metadata
|
|
122
|
+
* @param {string} name - Profile name
|
|
123
|
+
* @param {Object} updates - Fields to update
|
|
124
|
+
*/
|
|
125
|
+
updateProfile(name, updates) {
|
|
126
|
+
const data = this._read();
|
|
127
|
+
|
|
128
|
+
if (!data.profiles[name]) {
|
|
129
|
+
throw new Error(`Profile not found: ${name}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
data.profiles[name] = {
|
|
133
|
+
...data.profiles[name],
|
|
134
|
+
...updates
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this._write(data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Delete a profile
|
|
142
|
+
* @param {string} name - Profile name
|
|
143
|
+
*/
|
|
144
|
+
deleteProfile(name) {
|
|
145
|
+
const data = this._read();
|
|
146
|
+
|
|
147
|
+
if (!data.profiles[name]) {
|
|
148
|
+
throw new Error(`Profile not found: ${name}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
delete data.profiles[name];
|
|
152
|
+
|
|
153
|
+
// Clear default if it was the deleted profile
|
|
154
|
+
if (data.default === name) {
|
|
155
|
+
// Set to first remaining profile or null
|
|
156
|
+
const remaining = Object.keys(data.profiles);
|
|
157
|
+
data.default = remaining.length > 0 ? remaining[0] : null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._write(data);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* List all profiles
|
|
165
|
+
* @returns {Array} Array of profile names
|
|
166
|
+
*/
|
|
167
|
+
listProfiles() {
|
|
168
|
+
const data = this._read();
|
|
169
|
+
return Object.keys(data.profiles);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get all profiles with metadata
|
|
174
|
+
* @returns {Object} All profiles
|
|
175
|
+
*/
|
|
176
|
+
getAllProfiles() {
|
|
177
|
+
const data = this._read();
|
|
178
|
+
return data.profiles;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get default profile name
|
|
183
|
+
* @returns {string|null} Default profile name
|
|
184
|
+
*/
|
|
185
|
+
getDefaultProfile() {
|
|
186
|
+
const data = this._read();
|
|
187
|
+
return data.default;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set default profile
|
|
192
|
+
* @param {string} name - Profile name
|
|
193
|
+
*/
|
|
194
|
+
setDefaultProfile(name) {
|
|
195
|
+
const data = this._read();
|
|
196
|
+
|
|
197
|
+
if (!data.profiles[name]) {
|
|
198
|
+
throw new Error(`Profile not found: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
data.default = name;
|
|
202
|
+
this._write(data);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Check if profile exists
|
|
207
|
+
* @param {string} name - Profile name
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
hasProfile(name) {
|
|
211
|
+
const data = this._read();
|
|
212
|
+
return !!data.profiles[name];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Update last used timestamp
|
|
217
|
+
* @param {string} name - Profile name
|
|
218
|
+
*/
|
|
219
|
+
touchProfile(name) {
|
|
220
|
+
this.updateProfile(name, {
|
|
221
|
+
last_used: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = ProfileRegistry;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"ANTHROPIC_BASE_URL": "https://api.kimi.com/coding/",
|
|
4
4
|
"ANTHROPIC_AUTH_TOKEN": "YOUR_KIMI_API_KEY_HERE",
|
|
5
5
|
"ANTHROPIC_MODEL": "kimi-for-coding",
|
|
6
|
+
"ANTHROPIC_SMALL_FAST_MODEL": "kimi-for-coding",
|
|
6
7
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "kimi-for-coding",
|
|
7
8
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "kimi-for-coding",
|
|
8
9
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "kimi-for-coding"
|
package/lib/ccs
CHANGED
package/lib/ccs.ps1
CHANGED
|
@@ -141,7 +141,7 @@ function Show-Help {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
# Version (updated by scripts/bump-version.sh)
|
|
144
|
-
$CcsVersion = "
|
|
144
|
+
$CcsVersion = "3.0.0"
|
|
145
145
|
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
146
146
|
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
|
|
147
147
|
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -84,6 +84,7 @@ function createConfigFiles() {
|
|
|
84
84
|
ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/',
|
|
85
85
|
ANTHROPIC_AUTH_TOKEN: 'YOUR_KIMI_API_KEY_HERE',
|
|
86
86
|
ANTHROPIC_MODEL: 'kimi-for-coding',
|
|
87
|
+
ANTHROPIC_SMALL_FAST_MODEL: 'kimi-for-coding',
|
|
87
88
|
ANTHROPIC_DEFAULT_OPUS_MODEL: 'kimi-for-coding',
|
|
88
89
|
ANTHROPIC_DEFAULT_SONNET_MODEL: 'kimi-for-coding',
|
|
89
90
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'kimi-for-coding'
|