feat: implement v3.0.1 - fix silent postinstall failures with auto-recovery

- Add auto-recovery mechanisms for missing/corrupted configuration files
- Implement comprehensive health check command (`ccs doctor`)
- Enhance error messages with context-aware diagnostics and recovery commands
- Fix silent postinstall failures - now exits with proper error codes
- Add RecoveryManager class for automatic config restoration
- Add ErrorManager class for structured, helpful error messages
- Update postinstall script to validate created files and auto-create ~/.claude/settings.json
- Add doctor command support to bash and npm implementations
- Implement atomic file operations to prevent corruption
- Add comprehensive testing scenarios and validation

Fixes critical issue where npm install succeeded but CCS failed on first run.
Enhances user experience with automatic recovery and clear error guidance.

BREAKING CHANGE: Postinstall now exits with error code 1 on critical failures
This commit is contained in:
kaitranntt committed 2025-11-10 17:39:17 -05:00
1 parent f034c25a61
commit f65b87c123
13 files changed
+1198 -53

No files matched your search

+17 -3
View File
@@ -16,9 +16,19 @@ CCS (Claude Code Switch) is a lightweight CLI wrapper enabling instant switching
**YAGNI** (You Aren't Gonna Need It): No features "just in case"
**KISS** (Keep It Simple): Simple bash/PowerShell/Node.js, no complexity
**DRY** (Don't Repeat Yourself): One source of truth (config.json)
**CLI-First UX**: The primary user experience is through the command-line interface
The tool does ONE thing: enable instant switching between Claude accounts and alternative models. Never add features that violate these principles.
### CLI Documentation Requirement (CRITICAL)
**All functionality changes MUST update `--help` output across all implementations:**
- `bin/ccs.js` - handleHelpCommand() function
- `lib/ccs` - show_help() function (bash)
- `lib/ccs.ps1` - Show-Help function (PowerShell)
This is non-negotiable. The CLI help is the primary documentation users reference. If a feature exists but isn't documented in `--help`, it effectively doesn't exist for users.
## Key Constraints
1. **NO EMOJIS in terminal output** - Use ASCII symbols ([OK], [!], [X], [i]) for compatibility
@@ -281,9 +291,11 @@ All values must be strings (not booleans/objects) to prevent PowerShell crashes.
### Adding a New Feature
1. Verify it aligns with YAGNI/KISS/DRY principles
2. Implement for both bash/PowerShell and Node.js if applicable
3. Test on all platforms (macOS, Linux, Windows)
4. Update tests in `tests/edge-cases.sh` and `tests/edge-cases.ps1`
5. Update CONTRIBUTING.md if it affects contributors
3. **Update `--help` output in ALL three implementations** (bin/ccs.js, lib/ccs, lib/ccs.ps1) - REQUIRED
4. Test on all platforms (macOS, Linux, Windows)
5. Update tests in `tests/edge-cases.sh` and `tests/edge-cases.ps1`
6. Update CONTRIBUTING.md if it affects contributors
7. Update README.md examples if user-facing
### Fixing Bugs
1. Add test case reproducing the bug
@@ -318,6 +330,8 @@ Before any PR, verify:
- [ ] Manual PATH setup instructions clear if auto fails
- [ ] v3.0 concurrent sessions work correctly
- [ ] Instance isolation works (no cross-profile contamination)
- [ ] **`--help` output updated in bin/ccs.js, lib/ccs, and lib/ccs.ps1** (if feature added/changed)
- [ ] **`--help` output consistent across all three implementations**
## Integration with Claude Code
+1 -1
View File
@@ -1 +1 @@
3.0.0
3.0.1
+47 -12
View File
@@ -8,6 +8,8 @@ const os = require('os');
const { error, colored } = require('./helpers');
const { detectClaudeCli, showClaudeNotFoundError } = require('./claude-detector');
const { getSettingsPath, getConfigPath } = require('./config-manager');
const { ErrorManager } = require('./error-manager');
const RecoveryManager = require('./recovery-manager');
// Version (sync with package.json)
const CCS_VERSION = require('../package.json').version;
@@ -97,25 +99,34 @@ function handleHelpCommand() {
// Description
console.log(colored('Description:', 'cyan'));
console.log(' Switch between Claude models instantly. Stop hitting rate limits.');
console.log(' Maps profile names to Claude settings files via ~/.ccs/config.json');
console.log(' Switch between multiple Claude accounts (work, personal, team) and');
console.log(' alternative models (GLM, Kimi) instantly. Concurrent sessions with');
console.log(' auto-recovery. Zero downtime.');
console.log('');
// Profile Switching
console.log(colored('Profile Switching:', 'cyan'));
console.log(` ${colored('ccs', 'yellow')} Use default profile`);
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM profile`);
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi profile`);
console.log(` ${colored('ccs work', 'yellow')} Use work account (saved profile)`);
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Switch to GLM and run command`);
console.log(` ${colored('ccs work', 'yellow')} "review code" Use work account and run command`);
// Model Switching
console.log(colored('Model Switching:', 'cyan'));
console.log(` ${colored('ccs', 'yellow')} Use default Claude account`);
console.log(` ${colored('ccs glm', 'yellow')} Switch to GLM 4.6 model`);
console.log(` ${colored('ccs kimi', 'yellow')} Switch to Kimi for Coding`);
console.log(` ${colored('ccs glm', 'yellow')} "debug this code" Use GLM and run command`);
console.log('');
// Account Management
console.log(colored('Account Management:', 'cyan'));
console.log(` ${colored('ccs auth create <profile>', 'yellow')} Create new profile and login`);
console.log(` ${colored('ccs auth list', 'yellow')} List all saved profiles`);
console.log(` ${colored('ccs auth --help', 'yellow')} Show account management help`);
console.log(` ${colored('ccs auth show <profile>', 'yellow')} Show profile details`);
console.log(` ${colored('ccs auth remove <profile>', 'yellow')} Remove profile (requires --force)`);
console.log(` ${colored('ccs auth default <profile>', 'yellow')} Set default profile`);
console.log(` ${colored('ccs work', 'yellow')} Switch to work account`);
console.log(` ${colored('ccs personal', 'yellow')} Switch to personal account`);
console.log(` ${colored('ccs work', 'yellow')} "review code" Run command with work account`);
console.log('');
// Diagnostics
console.log(colored('Diagnostics:', 'cyan'));
console.log(` ${colored('ccs doctor', 'yellow')} Run health check and diagnostics`);
console.log('');
// Flags
@@ -195,6 +206,16 @@ function handleUninstallCommand() {
process.exit(0);
}
async function handleDoctorCommand() {
const Doctor = require('./doctor');
const doctor = new Doctor();
await doctor.runAllChecks();
// Exit with error code if unhealthy
process.exit(doctor.results.isHealthy() ? 0 : 1);
}
// Smart profile detection
function detectProfile(args) {
if (args.length === 0 || args[0].startsWith('-')) {
@@ -234,6 +255,12 @@ async function main() {
return;
}
// Special case: doctor command
if (firstArg === 'doctor' || firstArg === '--doctor') {
await handleDoctorCommand();
return;
}
// Special case: auth command (multi-account management)
if (firstArg === 'auth') {
const AuthCommands = require('./auth-commands');
@@ -242,13 +269,21 @@ async function main() {
return;
}
// Auto-recovery for missing configuration
const recovery = new RecoveryManager();
const recovered = recovery.recoverAll();
if (recovered) {
recovery.showRecoveryHints();
}
// Detect profile
const { profile, remainingArgs } = detectProfile(args);
// Detect Claude CLI first (needed for all paths)
const claudeCli = detectClaudeCli();
if (!claudeCli) {
showClaudeNotFoundError();
ErrorManager.showClaudeNotFound();
process.exit(1);
}
+36 -7
View File
@@ -4,6 +4,7 @@ const fs = require('fs');
const path = require('path');
const os = require('os');
const { error, expandPath } = require('./helpers');
const { ErrorManager } = require('./error-manager');
// Get config file path
function getConfigPath() {
@@ -16,7 +17,15 @@ function readConfig() {
// Check config exists
if (!fs.existsSync(configPath)) {
error(`Config file not found: ${configPath}`);
// Attempt recovery
const RecoveryManager = require('./recovery-manager');
const recovery = new RecoveryManager();
recovery.ensureConfigJson();
if (!fs.existsSync(configPath)) {
ErrorManager.showInvalidConfig(configPath, 'File not found');
process.exit(1);
}
}
// Read and parse JSON
@@ -25,12 +34,14 @@ function readConfig() {
const configContent = fs.readFileSync(configPath, 'utf8');
config = JSON.parse(configContent);
} catch (e) {
error(`Invalid JSON in ${configPath}: ${e.message}`);
ErrorManager.showInvalidConfig(configPath, `Invalid JSON: ${e.message}`);
process.exit(1);
}
// Validate config has profiles object
if (!config.profiles || typeof config.profiles !== 'object') {
error(`Config must have 'profiles' object in ${configPath}`);
ErrorManager.showInvalidConfig(configPath, "Missing 'profiles' object");
process.exit(1);
}
return config;
@@ -44,8 +55,10 @@ function getSettingsPath(profile) {
const settingsPath = config.profiles[profile];
if (!settingsPath) {
const availableProfiles = Object.keys(config.profiles).join(', ');
error(`Profile '${profile}' not found. Available: ${availableProfiles}`);
const availableProfiles = Object.keys(config.profiles);
const profileList = availableProfiles.map(p => ` - ${p}`);
ErrorManager.showProfileNotFound(profile, profileList);
process.exit(1);
}
// Expand path
@@ -53,7 +66,22 @@ function getSettingsPath(profile) {
// Validate settings file exists
if (!fs.existsSync(expandedPath)) {
error(`Settings file not found: ${expandedPath}`);
// Auto-create if it's ~/.claude/settings.json
if (expandedPath.includes('.claude') && expandedPath.endsWith('settings.json')) {
const RecoveryManager = require('./recovery-manager');
const recovery = new RecoveryManager();
recovery.ensureClaudeSettings();
if (!fs.existsSync(expandedPath)) {
ErrorManager.showSettingsNotFound(expandedPath);
process.exit(1);
}
console.log('[i] Auto-created missing settings file');
} else {
ErrorManager.showSettingsNotFound(expandedPath);
process.exit(1);
}
}
// Validate settings file is valid JSON
@@ -61,7 +89,8 @@ function getSettingsPath(profile) {
const settingsContent = fs.readFileSync(expandedPath, 'utf8');
JSON.parse(settingsContent);
} catch (e) {
error(`Invalid JSON in ${expandedPath}: ${e.message}`);
ErrorManager.showInvalidConfig(expandedPath, `Invalid JSON: ${e.message}`);
process.exit(1);
}
return expandedPath;
+365
View File
@@ -0,0 +1,365 @@
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const { spawn } = require('child_process');
const { colored } = require('./helpers');
const { detectClaudeCli } = require('./claude-detector');
/**
* Health check results
*/
class HealthCheck {
constructor() {
this.checks = [];
this.warnings = [];
this.errors = [];
}
addCheck(name, status, message = '', fix = null) {
this.checks.push({ name, status, message, fix });
if (status === 'error') this.errors.push({ name, message, fix });
if (status === 'warning') this.warnings.push({ name, message, fix });
}
hasErrors() {
return this.errors.length > 0;
}
hasWarnings() {
return this.warnings.length > 0;
}
isHealthy() {
return !this.hasErrors();
}
}
/**
* CCS Health Check and Diagnostics
*/
class Doctor {
constructor() {
this.homedir = os.homedir();
this.ccsDir = path.join(this.homedir, '.ccs');
this.claudeDir = path.join(this.homedir, '.claude');
this.results = new HealthCheck();
}
/**
* Run all health checks
*/
async runAllChecks() {
console.log(colored('Running CCS Health Check...', 'cyan'));
console.log('');
await this.checkClaudeCli();
this.checkCcsDirectory();
this.checkConfigFiles();
this.checkClaudeSettings();
this.checkProfiles();
this.checkInstances();
this.checkPermissions();
this.showReport();
return this.results;
}
/**
* Check 1: Claude CLI availability
*/
async checkClaudeCli() {
process.stdout.write('[?] Checking Claude CLI... ');
const claudeCli = detectClaudeCli();
// Try to execute claude --version
try {
const result = await new Promise((resolve, reject) => {
const child = spawn(claudeCli, ['--version'], {
stdio: 'pipe',
timeout: 5000
});
let output = '';
child.stdout.on('data', data => output += data);
child.stderr.on('data', data => output += data);
child.on('close', code => {
if (code === 0) resolve(output);
else reject(new Error('Exit code ' + code));
});
child.on('error', reject);
});
console.log(colored('[OK]', 'green'));
this.results.addCheck('Claude CLI', 'success', `Found: ${claudeCli}`);
} catch (err) {
console.log(colored('[X]', 'red'));
this.results.addCheck(
'Claude CLI',
'error',
'Claude CLI not found or not working',
'Install from: https://docs.claude.com/en/docs/claude-code/installation'
);
}
}
/**
* Check 2: ~/.ccs/ directory
*/
checkCcsDirectory() {
process.stdout.write('[?] Checking ~/.ccs/ directory... ');
if (fs.existsSync(this.ccsDir)) {
console.log(colored('[OK]', 'green'));
this.results.addCheck('CCS Directory', 'success');
} else {
console.log(colored('[X]', 'red'));
this.results.addCheck(
'CCS Directory',
'error',
'~/.ccs/ directory not found',
'Run: npm install -g @kaitranntt/ccs --force'
);
}
}
/**
* Check 3: Config files
*/
checkConfigFiles() {
const files = [
{ path: path.join(this.ccsDir, 'config.json'), name: 'config.json' },
{ path: path.join(this.ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
{ path: path.join(this.ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
];
for (const file of files) {
process.stdout.write(`[?] Checking ${file.name}... `);
if (!fs.existsSync(file.path)) {
console.log(colored('[X]', 'red'));
this.results.addCheck(
file.name,
'error',
`${file.name} not found`,
'Run: npm install -g @kaitranntt/ccs --force'
);
continue;
}
// Validate JSON
try {
const content = fs.readFileSync(file.path, 'utf8');
JSON.parse(content);
console.log(colored('[OK]', 'green'));
this.results.addCheck(file.name, 'success');
} catch (e) {
console.log(colored('[X]', 'red'));
this.results.addCheck(
file.name,
'error',
`Invalid JSON: ${e.message}`,
`Backup and recreate: mv ${file.path} ${file.path}.backup && npm install -g @kaitranntt/ccs --force`
);
}
}
}
/**
* Check 4: Claude settings
*/
checkClaudeSettings() {
process.stdout.write('[?] Checking ~/.claude/settings.json... ');
const settingsPath = path.join(this.claudeDir, 'settings.json');
if (!fs.existsSync(settingsPath)) {
console.log(colored('[!]', 'yellow'));
this.results.addCheck(
'Claude Settings',
'warning',
'~/.claude/settings.json not found',
'Run: claude /login'
);
return;
}
// Validate JSON
try {
const content = fs.readFileSync(settingsPath, 'utf8');
JSON.parse(content);
console.log(colored('[OK]', 'green'));
this.results.addCheck('Claude Settings', 'success');
} catch (e) {
console.log(colored('[!]', 'yellow'));
this.results.addCheck(
'Claude Settings',
'warning',
`Invalid JSON: ${e.message}`,
'Run: claude /login'
);
}
}
/**
* Check 5: Profile configurations
*/
checkProfiles() {
process.stdout.write('[?] Checking profiles... ');
const configPath = path.join(this.ccsDir, 'config.json');
if (!fs.existsSync(configPath)) {
console.log(colored('[SKIP]', 'yellow'));
return;
}
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (!config.profiles || typeof config.profiles !== 'object') {
console.log(colored('[X]', 'red'));
this.results.addCheck(
'Profiles',
'error',
'config.json missing profiles object',
'Run: npm install -g @kaitranntt/ccs --force'
);
return;
}
const profileCount = Object.keys(config.profiles).length;
console.log(colored('[OK]', 'green'), `(${profileCount} profiles)`);
this.results.addCheck('Profiles', 'success', `${profileCount} profiles configured`);
} catch (e) {
console.log(colored('[X]', 'red'));
this.results.addCheck('Profiles', 'error', e.message);
}
}
/**
* Check 6: Instance directories (account-based profiles)
*/
checkInstances() {
process.stdout.write('[?] Checking instances... ');
const instancesDir = path.join(this.ccsDir, 'instances');
if (!fs.existsSync(instancesDir)) {
console.log(colored('[i]', 'cyan'), '(no account profiles)');
this.results.addCheck('Instances', 'success', 'No account profiles configured');
return;
}
const instances = fs.readdirSync(instancesDir).filter(name => {
return fs.statSync(path.join(instancesDir, name)).isDirectory();
});
if (instances.length === 0) {
console.log(colored('[i]', 'cyan'), '(no account profiles)');
this.results.addCheck('Instances', 'success', 'No account profiles');
return;
}
console.log(colored('[OK]', 'green'), `(${instances.length} instances)`);
this.results.addCheck('Instances', 'success', `${instances.length} account profiles`);
}
/**
* Check 7: File permissions
*/
checkPermissions() {
process.stdout.write('[?] Checking permissions... ');
const testFile = path.join(this.ccsDir, '.permission-test');
try {
fs.writeFileSync(testFile, 'test', 'utf8');
fs.unlinkSync(testFile);
console.log(colored('[OK]', 'green'));
this.results.addCheck('Permissions', 'success');
} catch (e) {
console.log(colored('[X]', 'red'));
this.results.addCheck(
'Permissions',
'error',
'Cannot write to ~/.ccs/',
'Fix: sudo chown -R $USER ~/.ccs ~/.claude && chmod 755 ~/.ccs ~/.claude'
);
}
}
/**
* Show health check report
*/
showReport() {
console.log('');
console.log(colored('═══════════════════════════════════════════', 'cyan'));
console.log(colored('Health Check Report', 'bold'));
console.log(colored('═══════════════════════════════════════════', 'cyan'));
console.log('');
if (this.results.isHealthy() && !this.results.hasWarnings()) {
console.log(colored('✓ All checks passed!', 'green'));
console.log('');
console.log('Your CCS installation is healthy.');
console.log('');
return;
}
// Show errors
if (this.results.hasErrors()) {
console.log(colored('Errors:', 'red'));
this.results.errors.forEach(err => {
console.log(` [X] ${err.name}: ${err.message}`);
if (err.fix) {
console.log(` Fix: ${err.fix}`);
}
});
console.log('');
}
// Show warnings
if (this.results.hasWarnings()) {
console.log(colored('Warnings:', 'yellow'));
this.results.warnings.forEach(warn => {
console.log(` [!] ${warn.name}: ${warn.message}`);
if (warn.fix) {
console.log(` Fix: ${warn.fix}`);
}
});
console.log('');
}
// Summary
if (this.results.hasErrors()) {
console.log(colored('Status: Installation has errors', 'red'));
console.log('Run suggested fixes above to resolve issues.');
} else {
console.log(colored('Status: Installation healthy (warnings only)', 'green'));
}
console.log('');
}
/**
* Generate JSON report
*/
generateJsonReport() {
return JSON.stringify({
timestamp: new Date().toISOString(),
platform: process.platform,
nodeVersion: process.version,
ccsVersion: require('../package.json').version,
checks: this.results.checks,
errors: this.results.errors,
warnings: this.results.warnings,
healthy: this.results.isHealthy()
}, null, 2);
}
}
module.exports = Doctor;
+159
View File
@@ -0,0 +1,159 @@
'use strict';
const { colored } = require('./helpers');
/**
* Error types with structured messages
*/
const ErrorTypes = {
NO_CLAUDE_CLI: 'NO_CLAUDE_CLI',
MISSING_SETTINGS: 'MISSING_SETTINGS',
INVALID_CONFIG: 'INVALID_CONFIG',
UNKNOWN_PROFILE: 'UNKNOWN_PROFILE',
PERMISSION_DENIED: 'PERMISSION_DENIED',
GENERIC: 'GENERIC'
};
/**
* Enhanced error manager with context-aware messages
*/
class ErrorManager {
/**
* Show Claude CLI not found error
*/
static showClaudeNotFound() {
console.error('');
console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
console.error(colored('║ ERROR: Claude CLI not found ║', 'red'));
console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
console.error('');
console.error('CCS requires Claude CLI to be installed.');
console.error('');
console.error(colored('Fix:', 'yellow'));
console.error(' 1. Install Claude CLI:');
console.error(' https://docs.claude.com/en/docs/claude-code/installation');
console.error('');
console.error(' 2. Verify installation:');
console.error(' command -v claude (Unix)');
console.error(' Get-Command claude (Windows)');
console.error('');
console.error(' 3. Custom path (if installed elsewhere):');
console.error(' export CCS_CLAUDE_PATH="/path/to/claude"');
console.error('');
console.error('Restart terminal after installation.');
console.error('');
}
/**
* Show settings file not found error
* @param {string} settingsPath - Path to missing settings file
*/
static showSettingsNotFound(settingsPath) {
const isClaudeSettings = settingsPath.includes('.claude') && settingsPath.endsWith('settings.json');
console.error('');
console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
console.error(colored('║ ERROR: Settings file not found ║', 'red'));
console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
console.error('');
console.error(`File: ${settingsPath}`);
console.error('');
if (isClaudeSettings) {
console.error('This file is auto-created when you login to Claude CLI.');
console.error('');
console.error(colored('Fix (copy-paste):', 'yellow'));
console.error(` echo '{}' > ${settingsPath}`);
console.error(' claude /login');
console.error('');
console.error('Why: Newer Claude CLI versions require explicit login.');
} else {
console.error(colored('Fix (copy-paste):', 'yellow'));
console.error(' npm install -g @kaitranntt/ccs --force');
console.error('');
console.error('This will recreate missing profile settings.');
}
console.error('');
}
/**
* Show invalid configuration error
* @param {string} configPath - Path to invalid config
* @param {string} errorDetail - JSON parse error detail
*/
static showInvalidConfig(configPath, errorDetail) {
console.error('');
console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
console.error(colored('║ ERROR: Configuration invalid ║', 'red'));
console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
console.error('');
console.error(`File: ${configPath}`);
console.error(`Issue: ${errorDetail}`);
console.error('');
console.error(colored('Fix (copy-paste):', 'yellow'));
console.error(' # Backup corrupted file');
console.error(` mv ${configPath} ${configPath}.backup`);
console.error('');
console.error(' # Reinstall CCS');
console.error(' npm install -g @kaitranntt/ccs --force');
console.error('');
console.error('Your profile settings will be preserved.');
console.error('');
}
/**
* Show profile not found error
* @param {string} profileName - Requested profile name
* @param {string[]} availableProfiles - List of available profiles
* @param {string} suggestion - Suggested profile name (fuzzy match)
*/
static showProfileNotFound(profileName, availableProfiles, suggestion = null) {
console.error('');
console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
console.error(colored(`║ ERROR: Profile '${profileName}' not found${' '.repeat(Math.max(0, 35 - profileName.length))}║`, 'red'));
console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
console.error('');
console.error(colored('Available profiles:', 'cyan'));
availableProfiles.forEach(line => console.error(` ${line}`));
console.error('');
console.error(colored('Fix:', 'yellow'));
console.error(' # Use existing profile');
console.error(' ccs <profile> "your prompt"');
console.error('');
console.error(' # Create new account profile');
console.error(' ccs auth create <name>');
console.error('');
if (suggestion) {
console.error(colored(`Did you mean: ${suggestion}`, 'yellow'));
console.error('');
}
}
/**
* Show permission denied error
* @param {string} path - Path with permission issue
*/
static showPermissionDenied(path) {
console.error('');
console.error(colored('╔══════════════════════════════════════════════════════════╗', 'red'));
console.error(colored('║ ERROR: Permission denied ║', 'red'));
console.error(colored('╚══════════════════════════════════════════════════════════╝', 'red'));
console.error('');
console.error(`Cannot write to: ${path}`);
console.error('');
console.error(colored('Fix (copy-paste):', 'yellow'));
console.error(' # Fix ownership');
console.error(' sudo chown -R $USER ~/.ccs ~/.claude');
console.error('');
console.error(' # Fix permissions');
console.error(' chmod 755 ~/.ccs ~/.claude');
console.error('');
console.error(' # Retry installation');
console.error(' npm install -g @kaitranntt/ccs --force');
console.error('');
}
}
module.exports = { ErrorManager, ErrorTypes };
+135
View File
@@ -0,0 +1,135 @@
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
/**
* Auto-recovery for missing or corrupted configuration
*/
class RecoveryManager {
constructor() {
this.homedir = os.homedir();
this.ccsDir = path.join(this.homedir, '.ccs');
this.claudeDir = path.join(this.homedir, '.claude');
this.recovered = [];
}
/**
* Ensure ~/.ccs/ directory exists
*/
ensureCcsDirectory() {
if (!fs.existsSync(this.ccsDir)) {
fs.mkdirSync(this.ccsDir, { recursive: true, mode: 0o755 });
this.recovered.push('Created ~/.ccs/ directory');
return true;
}
return false;
}
/**
* Ensure ~/.ccs/config.json exists with defaults
*/
ensureConfigJson() {
const configPath = path.join(this.ccsDir, 'config.json');
// Check if exists and valid
if (fs.existsSync(configPath)) {
try {
const content = fs.readFileSync(configPath, 'utf8');
JSON.parse(content); // Validate JSON
return false; // No recovery needed
} catch (e) {
// Corrupted - backup and recreate
const backupPath = `${configPath}.backup.${Date.now()}`;
fs.renameSync(configPath, backupPath);
this.recovered.push(`Backed up corrupted config.json to ${path.basename(backupPath)}`);
}
}
// Create default config
const defaultConfig = {
profiles: {
glm: '~/.ccs/glm.settings.json',
kimi: '~/.ccs/kimi.settings.json',
default: '~/.claude/settings.json'
}
};
const tmpPath = `${configPath}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
fs.renameSync(tmpPath, configPath);
this.recovered.push('Created ~/.ccs/config.json');
return true;
}
/**
* Ensure ~/.claude/settings.json exists
*/
ensureClaudeSettings() {
const claudeSettingsPath = path.join(this.claudeDir, 'settings.json');
// Create ~/.claude/ if missing
if (!fs.existsSync(this.claudeDir)) {
fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o755 });
this.recovered.push('Created ~/.claude/ directory');
}
// Create settings.json if missing
if (!fs.existsSync(claudeSettingsPath)) {
const tmpPath = `${claudeSettingsPath}.tmp`;
fs.writeFileSync(tmpPath, '{}\n', 'utf8');
fs.renameSync(tmpPath, claudeSettingsPath);
this.recovered.push('Created ~/.claude/settings.json');
return true;
}
return false;
}
/**
* Run all recovery operations
* @returns {boolean} True if any recovery performed
*/
recoverAll() {
this.recovered = [];
this.ensureCcsDirectory();
this.ensureConfigJson();
this.ensureClaudeSettings();
return this.recovered.length > 0;
}
/**
* Get recovery summary
* @returns {string[]} List of recovery actions performed
*/
getRecoverySummary() {
return this.recovered;
}
/**
* Show recovery hints
*/
showRecoveryHints() {
if (this.recovered.length === 0) return;
console.log('');
console.log('[i] Auto-recovery completed:');
this.recovered.forEach(msg => console.log(` - ${msg}`));
// Show login hint if created Claude settings
if (this.recovered.some(msg => msg.includes('settings.json'))) {
console.log('');
console.log('[i] Next step: Login to Claude CLI');
console.log(' Run: claude /login');
}
console.log('');
}
}
module.exports = RecoveryManager;
+1 -1
View File
@@ -31,7 +31,7 @@ $InstallMethod = if ($ScriptDir -and ((Test-Path "$ScriptDir\lib\ccs.ps1") -or (
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (irm | iex)
# For git installations, VERSION file is read if available
$CcsVersion = "3.0.0"
$CcsVersion = "3.0.1"
# Try to read VERSION file for git installations
if ($ScriptDir) {
+1 -1
View File
@@ -32,7 +32,7 @@ fi
# IMPORTANT: Update this version when releasing new versions!
# This hardcoded version is used for standalone installations (curl | bash)
# For git installations, VERSION file is read if available
CCS_VERSION="3.0.0"
CCS_VERSION="3.0.1"
# Try to read VERSION file for git installations
if [[ -f "$SCRIPT_DIR/VERSION" ]]; then
+231 -8
View File
@@ -2,7 +2,7 @@
set -euo pipefail
# Version (updated by scripts/bump-version.sh)
CCS_VERSION="3.0.0"
CCS_VERSION="3.0.1"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${CCS_CONFIG:-$HOME/.ccs/config.json}"
readonly PROFILES_JSON="$HOME/.ccs/profiles.json"
@@ -44,14 +44,15 @@ show_help() {
echo -e " ${YELLOW}ccs${RESET} [flags]"
echo ""
echo -e "${CYAN}Description:${RESET}"
echo -e " Switch between Claude accounts and models instantly."
echo -e " Supports concurrent multi-account sessions."
echo -e " Switch between multiple Claude accounts (work, personal, team) and"
echo -e " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
echo -e " auto-recovery. Zero downtime."
echo ""
echo -e "${CYAN}Profile Switching:${RESET}"
echo -e " ${YELLOW}ccs${RESET} Use default profile"
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM profile"
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
echo -e " ${YELLOW}ccs work${RESET} \"debug code\" Run command with work account"
echo -e "${CYAN}Model Switching:${RESET}"
echo -e " ${YELLOW}ccs${RESET} Use default Claude account"
echo -e " ${YELLOW}ccs glm${RESET} Switch to GLM 4.6 model"
echo -e " ${YELLOW}ccs kimi${RESET} Switch to Kimi for Coding"
echo -e " ${YELLOW}ccs glm${RESET} \"debug this code\" Use GLM and run command"
echo ""
echo -e "${CYAN}Account Management:${RESET}"
echo -e " ${YELLOW}ccs auth create <profile>${RESET} Create new account profile"
@@ -59,6 +60,12 @@ show_help() {
echo -e " ${YELLOW}ccs auth show <profile>${RESET} Show profile details"
echo -e " ${YELLOW}ccs auth remove <profile>${RESET} Remove profile (requires --force)"
echo -e " ${YELLOW}ccs auth default <profile>${RESET} Set default profile"
echo -e " ${YELLOW}ccs work${RESET} Switch to work account"
echo -e " ${YELLOW}ccs personal${RESET} Switch to personal account"
echo -e " ${YELLOW}ccs work${RESET} \"review code\" Run command with work account"
echo ""
echo -e "${CYAN}Diagnostics:${RESET}"
echo -e " ${YELLOW}ccs doctor${RESET} Run health check and diagnostics"
echo ""
echo -e "${CYAN}Flags:${RESET}"
echo -e " ${YELLOW}-h, --help${RESET} Show this help message"
@@ -94,6 +101,210 @@ command -v jq &>/dev/null || {
exit 1
}
# --- Auto-Recovery Functions ---
ensure_ccs_directory() {
[[ -d "$HOME/.ccs" ]] && return 0
mkdir -p "$HOME/.ccs" 2>/dev/null || {
msg_error "Cannot create ~/.ccs/ directory. Check permissions."
return 1
}
echo "[i] Auto-recovery: Created ~/.ccs/ directory"
return 0
}
ensure_config_json() {
local config_file="$HOME/.ccs/config.json"
# Check if exists and valid
if [[ -f "$config_file" ]]; then
jq empty "$config_file" 2>/dev/null && return 0
# Corrupted - backup and recreate
local backup_file="${config_file}.backup.$(date +%s)"
mv "$config_file" "$backup_file" 2>/dev/null
echo "[i] Auto-recovery: Backed up corrupted config.json"
fi
# Create default config
cat > "$config_file" <<'EOF'
{
"profiles": {
"glm": "~/.ccs/glm.settings.json",
"kimi": "~/.ccs/kimi.settings.json",
"default": "~/.claude/settings.json"
}
}
EOF
echo "[i] Auto-recovery: Created ~/.ccs/config.json"
return 0
}
ensure_claude_settings() {
local claude_dir="$HOME/.claude"
local settings_file="$claude_dir/settings.json"
# Create ~/.claude/ if missing
if [[ ! -d "$claude_dir" ]]; then
mkdir -p "$claude_dir" 2>/dev/null || return 1
echo "[i] Auto-recovery: Created ~/.claude/ directory"
fi
# Create settings.json if missing
if [[ ! -f "$settings_file" ]]; then
echo '{}' > "$settings_file" 2>/dev/null || return 1
echo "[i] Auto-recovery: Created ~/.claude/settings.json"
echo "[i] Next step: Run 'claude /login' to authenticate"
return 0
fi
return 0
}
# Run auto-recovery
auto_recover() {
ensure_ccs_directory || return 1
ensure_config_json || return 1
ensure_claude_settings || return 1
return 0
}
# --- Doctor Command ---
doctor_check() {
local check_name="$1"
local status="$2" # success, warning, error
local message="${3:-}"
case "$status" in
success)
echo -e "${GREEN}[OK]${RESET} $check_name"
;;
warning)
echo -e "${YELLOW}[!]${RESET} $check_name${message:+: $message}"
;;
error)
echo -e "${RED}[X]${RESET} $check_name: $message"
;;
esac
}
doctor_run() {
echo -e "${CYAN}Running CCS Health Check...${RESET}"
echo ""
local has_errors=false
# Check Claude CLI
if command -v "$(detect_claude_cli)" &>/dev/null; then
doctor_check "Claude CLI" "success"
else
doctor_check "Claude CLI" "error" "Not found in PATH"
has_errors=true
fi
# Check ~/.ccs/
if [[ -d "$HOME/.ccs" ]]; then
doctor_check "CCS Directory" "success"
else
doctor_check "CCS Directory" "error" "~/.ccs/ not found"
has_errors=true
fi
# Check config.json
if [[ -f "$CONFIG_FILE" ]]; then
if jq empty "$CONFIG_FILE" 2>/dev/null; then
doctor_check "config.json" "success"
else
doctor_check "config.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "config.json" "error" "Not found"
has_errors=true
fi
# Check glm.settings.json
local glm_file="$HOME/.ccs/glm.settings.json"
if [[ -f "$glm_file" ]]; then
if jq empty "$glm_file" 2>/dev/null; then
doctor_check "glm.settings.json" "success"
else
doctor_check "glm.settings.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "glm.settings.json" "error" "Not found"
has_errors=true
fi
# Check kimi.settings.json
local kimi_file="$HOME/.ccs/kimi.settings.json"
if [[ -f "$kimi_file" ]]; then
if jq empty "$kimi_file" 2>/dev/null; then
doctor_check "kimi.settings.json" "success"
else
doctor_check "kimi.settings.json" "error" "Invalid JSON"
has_errors=true
fi
else
doctor_check "kimi.settings.json" "error" "Not found"
has_errors=true
fi
# Check ~/.claude/settings.json
if [[ -f "$HOME/.claude/settings.json" ]]; then
if jq empty "$HOME/.claude/settings.json" 2>/dev/null; then
doctor_check "Claude Settings" "success"
else
doctor_check "Claude Settings" "warning" "Invalid JSON"
fi
else
doctor_check "Claude Settings" "warning" "Not found - run 'claude /login'"
fi
# Check profiles
if [[ -f "$CONFIG_FILE" ]]; then
local profile_count=$(jq -r '.profiles | length' "$CONFIG_FILE" 2>/dev/null || echo "0")
doctor_check "Profiles" "success" "($profile_count configured)"
fi
# Check instances
if [[ -d "$INSTANCES_DIR" ]]; then
local instance_count=$(find "$INSTANCES_DIR" -maxdepth 1 -type d 2>/dev/null | wc -l)
instance_count=$((instance_count - 1)) # Exclude parent dir
doctor_check "Instances" "success" "($instance_count account profiles)"
else
doctor_check "Instances" "success" "(no account profiles)"
fi
# Check permissions
local test_file="$HOME/.ccs/.permission-test"
if echo "test" > "$test_file" 2>/dev/null; then
rm -f "$test_file" 2>/dev/null
doctor_check "Permissions" "success"
else
doctor_check "Permissions" "error" "Cannot write to ~/.ccs/"
has_errors=true
fi
# Summary
echo ""
echo -e "${CYAN}═══════════════════════════════════════════${RESET}"
if $has_errors; then
echo -e "${RED}Status: Installation has errors${RESET}"
echo "Run: npm install -g @kaitranntt/ccs --force"
else
echo -e "${GREEN}✓ All checks passed!${RESET}"
fi
echo ""
$has_errors && exit 1 || exit 0
}
# --- Claude CLI Detection Logic ---
detect_claude_cli() {
@@ -727,6 +938,18 @@ if [[ $# -gt 0 ]] && [[ "${1}" == "auth" ]]; then
exit $?
fi
# Special case: doctor command
if [[ $# -gt 0 ]] && [[ "${1}" == "doctor" || "${1}" == "--doctor" ]]; then
doctor_run
exit $?
fi
# Run auto-recovery before main logic
auto_recover || {
msg_error "Auto-recovery failed. Check permissions."
exit 1
}
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if [[ $# -eq 0 ]] || [[ "${1}" =~ ^- ]]; then
# No args or first arg is a flag → use default profile
+94 -8
View File
@@ -12,7 +12,7 @@ param(
$ErrorActionPreference = "Stop"
# Version (updated by scripts/bump-version.sh)
$CcsVersion = "3.0.0"
$CcsVersion = "3.0.1"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$ConfigFile = if ($env:CCS_CONFIG) { $env:CCS_CONFIG } else { "$env:USERPROFILE\.ccs\config.json" }
$ProfilesJson = "$env:USERPROFILE\.ccs\profiles.json"
@@ -100,14 +100,15 @@ function Show-Help {
Write-ColorLine " ccs [flags]" "Yellow"
Write-Host ""
Write-ColorLine "Description:" "Cyan"
Write-Host " Switch between Claude accounts and models instantly."
Write-Host " Supports concurrent multi-account sessions."
Write-Host " Switch between multiple Claude accounts (work, personal, team) and"
Write-Host " alternative models (GLM, Kimi) instantly. Concurrent sessions with"
Write-Host " auto-recovery. Zero downtime."
Write-Host ""
Write-ColorLine "Profile Switching:" "Cyan"
Write-ColorLine " ccs Use default profile" "Yellow"
Write-ColorLine " ccs glm Switch to GLM profile" "Yellow"
Write-ColorLine " ccs work Switch to work account" "Yellow"
Write-ColorLine " ccs work 'debug code' Run command with work account" "Yellow"
Write-ColorLine "Model Switching:" "Cyan"
Write-ColorLine " ccs Use default Claude account" "Yellow"
Write-ColorLine " ccs glm Switch to GLM 4.6 model" "Yellow"
Write-ColorLine " ccs kimi Switch to Kimi for Coding" "Yellow"
Write-ColorLine " ccs glm 'debug this code' Use GLM and run command" "Yellow"
Write-Host ""
Write-ColorLine "Account Management:" "Cyan"
Write-ColorLine " ccs auth create <profile> Create new account profile" "Yellow"
@@ -115,6 +116,12 @@ function Show-Help {
Write-ColorLine " ccs auth show <profile> Show profile details" "Yellow"
Write-ColorLine " ccs auth remove <profile> Remove profile (requires --force)" "Yellow"
Write-ColorLine " ccs auth default <profile> Set default profile" "Yellow"
Write-ColorLine " ccs work Switch to work account" "Yellow"
Write-ColorLine " ccs personal Switch to personal account" "Yellow"
Write-ColorLine " ccs work 'review code' Run command with work account" "Yellow"
Write-Host ""
Write-ColorLine "Diagnostics:" "Cyan"
Write-ColorLine " ccs doctor Run health check and diagnostics" "Yellow"
Write-Host ""
Write-ColorLine "Flags:" "Cyan"
Write-ColorLine " -h, --help Show this help message" "Yellow"
@@ -202,6 +209,79 @@ function Show-Version {
}
}
# --- Auto-Recovery Functions ---
function Ensure-CcsDirectory {
if (Test-Path "$env:USERPROFILE\.ccs") { return $true }
try {
New-Item -ItemType Directory -Path "$env:USERPROFILE\.ccs" -Force | Out-Null
Write-Host "[i] Auto-recovery: Created ~/.ccs/ directory"
return $true
} catch {
Write-ErrorMsg "Cannot create ~/.ccs/ directory. Check permissions."
return $false
}
}
function Ensure-ConfigJson {
$ConfigFile = "$env:USERPROFILE\.ccs\config.json"
# Check if exists and valid
if (Test-Path $ConfigFile) {
try {
Get-Content $ConfigFile -Raw | ConvertFrom-Json | Out-Null
return $true
} catch {
# Corrupted - backup and recreate
$BackupFile = "$ConfigFile.backup.$(Get-Date -Format 'yyyyMMddHHmmss')"
Move-Item $ConfigFile $BackupFile -Force
Write-Host "[i] Auto-recovery: Backed up corrupted config.json"
}
}
# Create default config
$DefaultConfig = @{
profiles = @{
glm = "~/.ccs/glm.settings.json"
kimi = "~/.ccs/kimi.settings.json"
default = "~/.claude/settings.json"
}
}
$DefaultConfig | ConvertTo-Json -Depth 10 | Set-Content $ConfigFile
Write-Host "[i] Auto-recovery: Created ~/.ccs/config.json"
return $true
}
function Ensure-ClaudeSettings {
$ClaudeDir = "$env:USERPROFILE\.claude"
$SettingsFile = "$ClaudeDir\settings.json"
# Create ~/.claude/ if missing
if (-not (Test-Path $ClaudeDir)) {
New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
Write-Host "[i] Auto-recovery: Created ~/.claude/ directory"
}
# Create settings.json if missing
if (-not (Test-Path $SettingsFile)) {
'{}' | Set-Content $SettingsFile
Write-Host "[i] Auto-recovery: Created ~/.claude/settings.json"
Write-Host "[i] Next step: Run 'claude /login' to authenticate"
return $true
}
return $false
}
function Invoke-AutoRecovery {
if (-not (Ensure-CcsDirectory)) { return $false }
if (-not (Ensure-ConfigJson)) { return $false }
Ensure-ClaudeSettings | Out-Null
return $true
}
# --- Profile Registry Functions (Phase 4) ---
function Initialize-ProfilesJson {
@@ -830,6 +910,12 @@ if ($RemainingArgs.Count -gt 0 -and $RemainingArgs[0] -eq "auth") {
exit $LASTEXITCODE
}
# Run auto-recovery before main logic
if (-not (Invoke-AutoRecovery)) {
Write-ErrorMsg "Auto-recovery failed. Check permissions."
exit 1
}
# Smart profile detection: if first arg starts with '-', it's a flag not a profile
if ($RemainingArgs.Count -eq 0 -or $RemainingArgs[0] -match '^-') {
# No args or first arg is a flag → use default profile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
"version": "3.0.0",
"version": "3.0.1",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
+110 -11
View File
@@ -14,6 +14,52 @@ const os = require('os');
* Cross-platform: Works on Unix, macOS, Windows
*/
/**
* Validate created configuration files
* @returns {object} { success: boolean, errors: string[], warnings: string[] }
*/
function validateConfiguration() {
const homedir = os.homedir();
const errors = [];
const warnings = [];
// Check ~/.ccs/ directory
const ccsDir = path.join(homedir, '.ccs');
if (!fs.existsSync(ccsDir)) {
errors.push('~/.ccs/ directory not found');
}
// Check required files
const requiredFiles = [
{ path: path.join(ccsDir, 'config.json'), name: 'config.json' },
{ path: path.join(ccsDir, 'glm.settings.json'), name: 'glm.settings.json' },
{ path: path.join(ccsDir, 'kimi.settings.json'), name: 'kimi.settings.json' }
];
for (const file of requiredFiles) {
if (!fs.existsSync(file.path)) {
errors.push(`${file.name} not found`);
continue;
}
// Validate JSON syntax
try {
const content = fs.readFileSync(file.path, 'utf8');
JSON.parse(content);
} catch (e) {
errors.push(`${file.name} has invalid JSON: ${e.message}`);
}
}
// Check ~/.claude/settings.json (warning only, not critical)
const claudeSettings = path.join(homedir, '.claude', 'settings.json');
if (!fs.existsSync(claudeSettings)) {
warnings.push('~/.claude/settings.json not found - run "claude /login"');
}
return { success: errors.length === 0, errors, warnings };
}
function createConfigFiles() {
try {
// Get user home directory (cross-platform)
@@ -107,23 +153,76 @@ function createConfigFiles() {
console.log('[OK] Kimi profile exists: ~/.ccs/kimi.settings.json (preserved)');
}
// Create ~/.claude/settings.json if missing (NEW)
const claudeDir = path.join(homedir, '.claude');
const claudeSettingsPath = path.join(claudeDir, 'settings.json');
if (!fs.existsSync(claudeDir)) {
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
console.log('[OK] Created directory: ~/.claude/');
}
if (!fs.existsSync(claudeSettingsPath)) {
// Create empty settings (matches Claude CLI behavior)
const tmpPath = `${claudeSettingsPath}.tmp`;
fs.writeFileSync(tmpPath, '{}\n', 'utf8');
fs.renameSync(tmpPath, claudeSettingsPath);
console.log('[OK] Created default settings: ~/.claude/settings.json');
console.log('');
console.log(' [i] Configure Claude CLI:');
console.log(' Run: claude /login');
console.log('');
} else {
console.log('[OK] Claude settings exist: ~/.claude/settings.json (preserved)');
}
// Validate configuration
console.log('');
console.log('[i] Validating configuration...');
const validation = validateConfiguration();
if (!validation.success) {
console.error('');
console.error('[X] Configuration validation failed:');
validation.errors.forEach(err => console.error(` - ${err}`));
console.error('');
throw new Error('Configuration incomplete');
}
// Show warnings (non-critical)
if (validation.warnings.length > 0) {
console.warn('');
console.warn('[!] Warnings:');
validation.warnings.forEach(warn => console.warn(` - ${warn}`));
}
console.log('');
console.log('[OK] CCS configuration ready!');
console.log(' Run: ccs --version');
} catch (err) {
// Silent failure: don't break npm install
console.warn('');
console.warn('[!] Could not auto-create CCS configuration');
console.warn(` Error: ${err.message}`);
console.warn('');
console.warn(' Manual setup:');
console.warn(' mkdir -p ~/.ccs');
console.warn(' # See: https://github.com/kaitranntt/ccs#configuration');
console.warn('');
// Show error details
console.error('');
console.error('[X] CCS configuration failed');
console.error(` Error: ${err.message}`);
console.error('');
console.error('Recovery steps:');
console.error(' 1. Create directory manually:');
console.error(' mkdir -p ~/.ccs ~/.claude');
console.error('');
console.error(' 2. Create empty settings:');
console.error(' echo "{}" > ~/.claude/settings.json');
console.error('');
console.error(' 3. Retry installation:');
console.error(' npm install -g @kaitranntt/ccs --force');
console.error('');
console.error(' 4. If issue persists, report at:');
console.error(' https://github.com/kaitranntt/ccs/issues');
console.error('');
// Don't exit with error code - allow npm install to succeed
process.exit(0);
// Exit with error code (npm will show warning)
process.exit(1);
}
}