mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 22:16:41 +00:00
* feat: add -sc short flag for --shell-completion Add -sc as a short flag alias for --shell-completion, matching the pattern of -h for --help and -v for --version. Changes: - bin/ccs.js: Add -sc support in help text and flag detection - lib/ccs: Add -sc support in help text and flag detection - lib/ccs.ps1: Add -sc support in help text and flag detection * chore: bump version to 4.1.4 * chore: bump version to 4.1.5 * feat: emphasize concurrent account usage in auth help text Update all auth-related help messages to emphasize the ability to run multiple Claude accounts concurrently. Changes: - bin/ccs.js: "Run multiple Claude accounts concurrently" - bin/auth/auth-commands.js: "CCS Concurrent Account Management" - lib/ccs: Updated both main help and auth_help function - lib/ccs.ps1: Updated both main help and Show-AuthHelp function * feat: implement ccs update command across all platforms Add cross-platform ccs update command to sync delegation commands and skills from ~/.ccs/.claude/ to ~/.claude/. Replaces vague "CCS items" wording with specific "delegation commands and skills". Changes: - bin/ccs.js: Add update command handler and update help text - bin/utils/claude-symlink-manager.js: Update user-facing messages - lib/ccs: Implement update_run() function with symlink logic - lib/ccs.ps1: Implement Update-Run function with Junction/SymbolicLink support Features: - Automatically backs up existing files before symlinking - Skips items that are already correctly symlinked - Reports installed vs up-to-date counts - Handles Windows permissions gracefully (suggests Admin/Developer Mode) Cross-platform parity: bash, PowerShell, and Node.js now all support ccs update * refactor: rename ccs update to ccs sync for clarity Rename the update command to sync across all platforms to avoid confusion with updating the CCS tool itself. The sync command clearly communicates syncing delegation features from the CCS package to ~/.claude/. Changes: - bin/ccs.js: Rename handleUpdateCommand → handleSyncCommand - bin/utils/claude-symlink-manager.js: Rename update() → sync() - lib/ccs: Rename update_run() → sync_run() - lib/ccs.ps1: Rename Update-Run → Sync-Run - All: Update help text "update" → "sync" - All: Update messages "Updating" → "Syncing" Command usage: ccs sync or ccs --sync * fix: update GitHub documentation links to stable permalink Update broken #usage anchor links to stable /blob/main/README.md permalink format. This matches the format already used in PowerShell version and ensures links always work. Changes: - bin/ccs.js: Update link from #usage to /blob/main/README.md - lib/ccs: Update link from #usage to /blob/main/README.md - Now consistent with lib/ccs.ps1 which already used this format Old: https://github.com/kaitranntt/ccs#usage New: https://github.com/kaitranntt/ccs/blob/main/README.md * feat: add sync command and -sc flag to shell completions Update all shell completion scripts to include the newly added sync command and -sc short flag for --shell-completion. Changes across all completion scripts (bash, zsh, fish, PowerShell): - Add 'sync' command to completion suggestions - Add '-sc' as short flag for '--shell-completion' - Update fish to handle both -sc and --shell-completion for subflags - Update PowerShell to recognize -sc for shell completion flags - Add sync command description: "Sync delegation commands and skills" This ensures tab completion discovers the sync command and users can use both -sc and --shell-completion interchangeably. * fix: standardize help text across all implementations Fix inconsistencies in help text that appeared after merge from main. Ensures all three implementations (bash, PowerShell, Node.js) display identical help messages. Changes: - lib/ccs: Update "Delegation (Token Optimization)" → "Delegation (inside Claude Code CLI)" - lib/ccs: Remove redundant /ccs:create line, simplify description - lib/ccs.ps1: Add missing Delegation section - All: Now use consistent messaging about delegation features This ensures users see the same information regardless of which platform they're using (Linux/macOS bash, Windows PowerShell, or npm). * fix: update description text to emphasize concurrent sessions Update outdated description in lib/ccs and lib/ccs.ps1 to match the improved wording already in bin/ccs.js. The new description better emphasizes running concurrent Claude CLI sessions. Changes: - lib/ccs: Update description to emphasize "Run different Claude CLI sessions concurrently" - lib/ccs.ps1: Update description to match bash and Node.js versions - Remove "(work, personal, team)" examples to keep description cleaner - Emphasize "Run different Claude CLI sessions concurrently" over vague "Concurrent sessions" Old: "Switch between multiple Claude accounts (work, personal, team) and alternative models (GLM, Kimi) instantly. Concurrent sessions with auto-recovery. Zero downtime." New: "Switch between multiple Claude accounts and alternative models (GLM, Kimi) instantly. Run different Claude CLI sessions concurrently with auto-recovery. Zero downtime." All three implementations now show identical, clearer description text. * feat(cli): enhance version display formatting and delegation status ---------
500 lines
16 KiB
JavaScript
500 lines
16 KiB
JavaScript
'use strict';
|
|
|
|
const { spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const ProfileRegistry = require('./profile-registry');
|
|
const InstanceManager = require('../management/instance-manager');
|
|
const { colored } = require('../utils/helpers');
|
|
const { detectClaudeCli } = require('../utils/claude-detector');
|
|
const { InteractivePrompt } = require('../utils/prompt');
|
|
const CCS_VERSION = require('../../package.json').version;
|
|
|
|
/**
|
|
* Auth Commands (Simplified)
|
|
*
|
|
* CLI interface for CCS multi-account management.
|
|
* Commands: create, list, show, remove, default
|
|
*
|
|
* Login-per-profile model: Each profile is an isolated Claude instance.
|
|
* Users login directly in each instance (no credential copying).
|
|
*/
|
|
class AuthCommands {
|
|
constructor() {
|
|
this.registry = new ProfileRegistry();
|
|
this.instanceMgr = new InstanceManager();
|
|
}
|
|
|
|
/**
|
|
* Show help for auth commands
|
|
*/
|
|
showHelp() {
|
|
console.log(colored('CCS Concurrent Account Management', 'bold'));
|
|
console.log('');
|
|
console.log(colored('Usage:', 'cyan'));
|
|
console.log(` ${colored('ccs auth', 'yellow')} <command> [options]`);
|
|
console.log('');
|
|
console.log(colored('Commands:', 'cyan'));
|
|
console.log(` ${colored('create <profile>', 'yellow')} Create new profile and login`);
|
|
console.log(` ${colored('list', 'yellow')} List all saved profiles`);
|
|
console.log(` ${colored('show <profile>', 'yellow')} Show profile details`);
|
|
console.log(` ${colored('remove <profile>', 'yellow')} Remove saved profile`);
|
|
console.log(` ${colored('default <profile>', 'yellow')} Set default profile`);
|
|
console.log('');
|
|
console.log(colored('Examples:', 'cyan'));
|
|
console.log(` ${colored('ccs auth create work', 'yellow')} # Create & login to work profile`);
|
|
console.log(` ${colored('ccs auth default work', 'yellow')} # Set work as default`);
|
|
console.log(` ${colored('ccs auth list', 'yellow')} # List all profiles`);
|
|
console.log(` ${colored('ccs work "review code"', 'yellow')} # Use work profile`);
|
|
console.log(` ${colored('ccs "review code"', 'yellow')} # Use default profile`);
|
|
console.log('');
|
|
console.log(colored('Options:', 'cyan'));
|
|
console.log(` ${colored('--force', 'yellow')} Allow overwriting existing profile (create)`);
|
|
console.log(` ${colored('--yes, -y', 'yellow')} Skip confirmation prompts (remove)`);
|
|
console.log(` ${colored('--json', 'yellow')} Output in JSON format (list, show)`);
|
|
console.log(` ${colored('--verbose', 'yellow')} Show additional details (list)`);
|
|
console.log('');
|
|
console.log(colored('Note:', 'cyan'));
|
|
console.log(` By default, ${colored('ccs', 'yellow')} uses Claude CLI defaults from ~/.claude/`);
|
|
console.log(` Use ${colored('ccs auth default <profile>', 'yellow')} to change the default profile.`);
|
|
console.log('');
|
|
}
|
|
|
|
/**
|
|
* Create new profile and prompt for login
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async handleCreate(args) {
|
|
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
const force = args.includes('--force');
|
|
|
|
if (!profileName) {
|
|
console.error('[X] Profile name is required');
|
|
console.log('');
|
|
console.log(`Usage: ${colored('ccs auth create <profile> [--force]', 'yellow')}`);
|
|
console.log('');
|
|
console.log('Example:');
|
|
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Check if profile already exists
|
|
if (!force && this.registry.hasProfile(profileName)) {
|
|
console.error(`[X] Profile already exists: ${profileName}`);
|
|
console.log(` Use ${colored('--force', 'yellow')} to overwrite`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Create instance directory
|
|
console.log(`[i] Creating profile: ${profileName}`);
|
|
const instancePath = this.instanceMgr.ensureInstance(profileName);
|
|
|
|
// Create/update profile entry
|
|
if (this.registry.hasProfile(profileName)) {
|
|
this.registry.updateProfile(profileName, {
|
|
type: 'account'
|
|
});
|
|
} else {
|
|
this.registry.createProfile(profileName, {
|
|
type: 'account'
|
|
});
|
|
}
|
|
|
|
console.log(`[i] Instance directory: ${instancePath}`);
|
|
console.log('');
|
|
console.log(colored('[i] Starting Claude in isolated instance...', 'yellow'));
|
|
console.log(colored('[i] You will be prompted to login with your account.', 'yellow'));
|
|
console.log('');
|
|
|
|
// Detect Claude CLI
|
|
const claudeCli = detectClaudeCli();
|
|
if (!claudeCli) {
|
|
console.error('[X] Claude CLI not found');
|
|
console.log('');
|
|
console.log('Please install Claude CLI first:');
|
|
console.log(' https://claude.ai/download');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Execute Claude in isolated instance (will auto-prompt for login if no credentials)
|
|
const child = spawn(claudeCli, [], {
|
|
stdio: 'inherit',
|
|
env: { ...process.env, CLAUDE_CONFIG_DIR: instancePath }
|
|
});
|
|
|
|
child.on('exit', (code) => {
|
|
if (code === 0) {
|
|
console.log('');
|
|
console.log(colored('[OK] Profile created successfully', 'green'));
|
|
console.log('');
|
|
console.log(` Profile: ${profileName}`);
|
|
console.log(` Instance: ${instancePath}`);
|
|
console.log('');
|
|
console.log('Usage:');
|
|
console.log(` ${colored(`ccs ${profileName} "your prompt here"`, 'yellow')} # Use this specific profile`);
|
|
console.log('');
|
|
console.log('To set as default (so you can use just "ccs"):');
|
|
console.log(` ${colored(`ccs auth default ${profileName}`, 'yellow')}`);
|
|
console.log('');
|
|
process.exit(0);
|
|
} else {
|
|
console.log('');
|
|
console.error('[X] Login failed or cancelled');
|
|
console.log('');
|
|
console.log('To retry:');
|
|
console.log(` ${colored(`ccs auth create ${profileName} --force`, 'yellow')}`);
|
|
console.log('');
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
console.error(`[X] Failed to execute Claude CLI: ${err.message}`);
|
|
process.exit(1);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(`[X] Failed to create profile: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all saved profiles
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async handleList(args) {
|
|
const verbose = args.includes('--verbose');
|
|
const json = args.includes('--json');
|
|
|
|
try {
|
|
const profiles = this.registry.getAllProfiles();
|
|
const defaultProfile = this.registry.getDefaultProfile();
|
|
const profileNames = Object.keys(profiles);
|
|
|
|
// JSON output mode
|
|
if (json) {
|
|
const output = {
|
|
version: CCS_VERSION,
|
|
profiles: profileNames.map(name => {
|
|
const profile = profiles[name];
|
|
const isDefault = name === defaultProfile;
|
|
const instancePath = this.instanceMgr.getInstancePath(name);
|
|
|
|
return {
|
|
name: name,
|
|
type: profile.type || 'account',
|
|
is_default: isDefault,
|
|
created: profile.created,
|
|
last_used: profile.last_used || null,
|
|
instance_path: instancePath
|
|
};
|
|
})
|
|
};
|
|
console.log(JSON.stringify(output, null, 2));
|
|
return;
|
|
}
|
|
|
|
// Human-readable output
|
|
if (profileNames.length === 0) {
|
|
console.log(colored('No account profiles found', 'yellow'));
|
|
console.log('');
|
|
console.log('To create your first profile:');
|
|
console.log(` ${colored('ccs auth create <profile>', 'yellow')} # Create and login to profile`);
|
|
console.log('');
|
|
console.log('Example:');
|
|
console.log(` ${colored('ccs auth create work', 'yellow')}`);
|
|
console.log('');
|
|
return;
|
|
}
|
|
|
|
console.log(colored('Saved Account Profiles:', 'bold'));
|
|
console.log('');
|
|
|
|
// Sort by last_used (descending), then alphabetically
|
|
const sorted = profileNames.sort((a, b) => {
|
|
const aProfile = profiles[a];
|
|
const bProfile = profiles[b];
|
|
|
|
// Default first
|
|
if (a === defaultProfile) return -1;
|
|
if (b === defaultProfile) return 1;
|
|
|
|
// Then by last_used
|
|
if (aProfile.last_used && bProfile.last_used) {
|
|
return new Date(bProfile.last_used) - new Date(aProfile.last_used);
|
|
}
|
|
if (aProfile.last_used) return -1;
|
|
if (bProfile.last_used) return 1;
|
|
|
|
// Then alphabetically
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
sorted.forEach(name => {
|
|
const profile = profiles[name];
|
|
const isDefault = name === defaultProfile;
|
|
const indicator = isDefault ? colored('[*]', 'green') : '[ ]';
|
|
|
|
console.log(`${indicator} ${colored(name, 'cyan')}${isDefault ? colored(' (default)', 'green') : ''}`);
|
|
|
|
console.log(` Type: ${profile.type || 'account'}`);
|
|
|
|
if (verbose) {
|
|
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
|
if (profile.last_used) {
|
|
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
|
}
|
|
}
|
|
|
|
console.log('');
|
|
});
|
|
|
|
console.log(`Total profiles: ${profileNames.length}`);
|
|
console.log('');
|
|
|
|
} catch (error) {
|
|
console.error(`[X] Failed to list profiles: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show details for a specific profile
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async handleShow(args) {
|
|
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
const json = args.includes('--json');
|
|
|
|
if (!profileName) {
|
|
console.error('[X] Profile name is required');
|
|
console.log('');
|
|
console.log(`Usage: ${colored('ccs auth show <profile> [--json]', 'yellow')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
const profile = this.registry.getProfile(profileName);
|
|
const defaultProfile = this.registry.getDefaultProfile();
|
|
const isDefault = profileName === defaultProfile;
|
|
const instancePath = this.instanceMgr.getInstancePath(profileName);
|
|
|
|
// Count sessions
|
|
let sessionCount = 0;
|
|
try {
|
|
const sessionsDir = path.join(instancePath, 'session-env');
|
|
if (fs.existsSync(sessionsDir)) {
|
|
const files = fs.readdirSync(sessionsDir);
|
|
sessionCount = files.filter(f => f.endsWith('.json')).length;
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors counting sessions
|
|
}
|
|
|
|
// JSON output mode
|
|
if (json) {
|
|
const output = {
|
|
name: profileName,
|
|
type: profile.type || 'account',
|
|
is_default: isDefault,
|
|
created: profile.created,
|
|
last_used: profile.last_used || null,
|
|
instance_path: instancePath,
|
|
session_count: sessionCount
|
|
};
|
|
console.log(JSON.stringify(output, null, 2));
|
|
return;
|
|
}
|
|
|
|
// Human-readable output
|
|
console.log(colored(`Profile: ${profileName}`, 'bold'));
|
|
console.log('');
|
|
console.log(` Type: ${profile.type || 'account'}`);
|
|
console.log(` Default: ${isDefault ? 'Yes' : 'No'}`);
|
|
console.log(` Instance: ${instancePath}`);
|
|
console.log(` Created: ${new Date(profile.created).toLocaleString()}`);
|
|
|
|
if (profile.last_used) {
|
|
console.log(` Last used: ${new Date(profile.last_used).toLocaleString()}`);
|
|
} else {
|
|
console.log(` Last used: Never`);
|
|
}
|
|
|
|
console.log('');
|
|
|
|
} catch (error) {
|
|
console.error(`[X] ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a saved profile
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async handleRemove(args) {
|
|
const profileName = args.find(arg => !arg.startsWith('--') && !arg.startsWith('-'));
|
|
|
|
if (!profileName) {
|
|
console.error('[X] Profile name is required');
|
|
console.log('');
|
|
console.log(`Usage: ${colored('ccs auth remove <profile> [--yes]', 'yellow')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!this.registry.hasProfile(profileName)) {
|
|
console.error(`[X] Profile not found: ${profileName}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
// Get instance path and session count for impact display
|
|
const instancePath = this.instanceMgr.getInstancePath(profileName);
|
|
let sessionCount = 0;
|
|
|
|
try {
|
|
const sessionsDir = path.join(instancePath, 'session-env');
|
|
if (fs.existsSync(sessionsDir)) {
|
|
const files = fs.readdirSync(sessionsDir);
|
|
sessionCount = files.filter(f => f.endsWith('.json')).length;
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors counting sessions
|
|
}
|
|
|
|
// Display impact
|
|
console.log('');
|
|
console.log(`Profile '${colored(profileName, 'cyan')}' will be permanently deleted.`);
|
|
console.log(` Instance path: ${instancePath}`);
|
|
console.log(` Sessions: ${sessionCount} conversation${sessionCount !== 1 ? 's' : ''}`);
|
|
console.log('');
|
|
|
|
// Interactive confirmation (or --yes flag)
|
|
const confirmed = await InteractivePrompt.confirm(
|
|
'Delete this profile?',
|
|
{ default: false } // Default to NO (safe)
|
|
);
|
|
|
|
if (!confirmed) {
|
|
console.log('[i] Cancelled');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Delete instance
|
|
this.instanceMgr.deleteInstance(profileName);
|
|
|
|
// Delete profile
|
|
this.registry.deleteProfile(profileName);
|
|
|
|
console.log(colored('[OK] Profile removed successfully', 'green'));
|
|
console.log(` Profile: ${profileName}`);
|
|
console.log('');
|
|
|
|
} catch (error) {
|
|
console.error(`[X] Failed to remove profile: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set default profile
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async handleDefault(args) {
|
|
const profileName = args.find(arg => !arg.startsWith('--'));
|
|
|
|
if (!profileName) {
|
|
console.error('[X] Profile name is required');
|
|
console.log('');
|
|
console.log(`Usage: ${colored('ccs auth default <profile>', 'yellow')}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
try {
|
|
this.registry.setDefaultProfile(profileName);
|
|
|
|
console.log(colored('[OK] Default profile set', 'green'));
|
|
console.log(` Profile: ${profileName}`);
|
|
console.log('');
|
|
console.log('Now you can use:');
|
|
console.log(` ${colored('ccs "your prompt"', 'yellow')} # Uses ${profileName} profile`);
|
|
console.log('');
|
|
|
|
} catch (error) {
|
|
console.error(`[X] ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Route auth command to appropriate handler
|
|
* @param {Array} args - Command arguments
|
|
*/
|
|
async route(args) {
|
|
if (args.length === 0 || args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
this.showHelp();
|
|
return;
|
|
}
|
|
|
|
const command = args[0];
|
|
const commandArgs = args.slice(1);
|
|
|
|
switch (command) {
|
|
case 'create':
|
|
await this.handleCreate(commandArgs);
|
|
break;
|
|
|
|
case 'save':
|
|
// Deprecated - redirect to create
|
|
console.log(colored('[!] Command "save" is deprecated', 'yellow'));
|
|
console.log(` Use: ${colored('ccs auth create <profile>', 'yellow')} instead`);
|
|
console.log('');
|
|
await this.handleCreate(commandArgs);
|
|
break;
|
|
|
|
case 'list':
|
|
await this.handleList(commandArgs);
|
|
break;
|
|
|
|
case 'show':
|
|
await this.handleShow(commandArgs);
|
|
break;
|
|
|
|
case 'remove':
|
|
await this.handleRemove(commandArgs);
|
|
break;
|
|
|
|
case 'default':
|
|
await this.handleDefault(commandArgs);
|
|
break;
|
|
|
|
case 'current':
|
|
console.log(colored('[!] Command "current" has been removed', 'yellow'));
|
|
console.log('');
|
|
console.log('Each profile has its own login in an isolated instance.');
|
|
console.log('Use "ccs auth list" to see all profiles.');
|
|
console.log('');
|
|
break;
|
|
|
|
case 'cleanup':
|
|
console.log(colored('[!] Command "cleanup" has been removed', 'yellow'));
|
|
console.log('');
|
|
console.log('No cleanup needed - no separate vault files.');
|
|
console.log('Use "ccs auth list" to see all profiles.');
|
|
console.log('');
|
|
break;
|
|
|
|
default:
|
|
console.error(`[X] Unknown command: ${command}`);
|
|
console.log('');
|
|
console.log('Run for help:');
|
|
console.log(` ${colored('ccs auth --help', 'yellow')}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = AuthCommands;
|