@kaitranntt/ccs 2.5.1 → 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 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 glm]
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 ~/.ccs/config.json]
169
- LOOKUP[Lookup profile settings file]
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
- EXEC[claude --settings file_path]
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 Sub or GLM API]
186
+ API[Claude/GLM/Kimi API]
179
187
  end
180
188
 
181
- CMD --> CONFIG
182
- CONFIG --> LOOKUP
183
- LOOKUP --> VALIDATE
184
- VALIDATE --> EXEC
185
- EXEC --> API
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
- 2.5.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 kimi', 'yellow')} "write tests" Switch to Kimi and run command`);
105
- console.log(` ${colored('ccs glm', 'yellow')} --verbose Switch to GLM with Claude flags`);
106
- console.log(` ${colored('ccs kimi', 'yellow')} --verbose Switch to Kimi with Claude flags`);
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
- // Detect profile
226
- const { profile, remainingArgs } = detectProfile(args);
227
-
228
- // Special case: "default" profile just runs claude directly
229
- if (profile === 'default') {
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
- // Get settings path for profile
241
- const settingsPath = getSettingsPath(profile);
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
- // Execute claude with --settings
253
- execClaude(claudeCli, ['--settings', settingsPath, ...remainingArgs]);
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;
package/lib/ccs CHANGED
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # Version (updated by scripts/bump-version.sh)
5
- CCS_VERSION="2.5.1"
5
+ CCS_VERSION="3.0.0"
6
6
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
7
  readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
8
8
 
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 = "2.5.1"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaitranntt/ccs",
3
- "version": "2.5.1",
3
+ "version": "3.0.0",
4
4
  "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
5
5
  "keywords": [
6
6
  "cli",