mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-15 16:19:12 +00:00
203 lines
5.5 KiB
JavaScript
203 lines
5.5 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const SharedManager = require('./shared-manager');
|
|
|
|
/**
|
|
* Instance Manager (Simplified)
|
|
*
|
|
* Manages isolated Claude CLI instances per profile for concurrent sessions.
|
|
* Each instance is an isolated CLAUDE_CONFIG_DIR where users login directly.
|
|
* No credential copying/encryption - Claude manages credentials per instance.
|
|
*/
|
|
class InstanceManager {
|
|
constructor() {
|
|
this.instancesDir = path.join(os.homedir(), '.ccs', 'instances');
|
|
this.sharedManager = new SharedManager();
|
|
}
|
|
|
|
/**
|
|
* Ensure instance exists for profile (lazy init only)
|
|
* @param {string} profileName - Profile name
|
|
* @returns {string} Instance path
|
|
*/
|
|
ensureInstance(profileName) {
|
|
const instancePath = this.getInstancePath(profileName);
|
|
|
|
// Lazy initialization
|
|
if (!fs.existsSync(instancePath)) {
|
|
this.initializeInstance(profileName, instancePath);
|
|
}
|
|
|
|
// Validate structure (auto-fix missing dirs)
|
|
this.validateInstance(instancePath);
|
|
|
|
return instancePath;
|
|
}
|
|
|
|
/**
|
|
* Get instance path for profile
|
|
* @param {string} profileName - Profile name
|
|
* @returns {string} Instance directory path
|
|
*/
|
|
getInstancePath(profileName) {
|
|
const safeName = this._sanitizeName(profileName);
|
|
return path.join(this.instancesDir, safeName);
|
|
}
|
|
|
|
/**
|
|
* Initialize new instance directory
|
|
* @param {string} profileName - Profile name
|
|
* @param {string} instancePath - Instance directory path
|
|
* @throws {Error} If initialization fails
|
|
*/
|
|
initializeInstance(profileName, instancePath) {
|
|
try {
|
|
// Create base directory
|
|
fs.mkdirSync(instancePath, { recursive: true, mode: 0o700 });
|
|
|
|
// Create Claude-expected subdirectories (profile-specific only)
|
|
const subdirs = [
|
|
'session-env',
|
|
'todos',
|
|
'logs',
|
|
'file-history',
|
|
'shell-snapshots',
|
|
'debug',
|
|
'.anthropic'
|
|
];
|
|
|
|
subdirs.forEach(dir => {
|
|
const dirPath = path.join(instancePath, dir);
|
|
if (!fs.existsSync(dirPath)) {
|
|
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
}
|
|
});
|
|
|
|
// Symlink shared directories (Phase 1: commands, skills)
|
|
this.sharedManager.linkSharedDirectories(instancePath);
|
|
|
|
// Copy global configs if exist (settings.json only)
|
|
this._copyGlobalConfigs(instancePath);
|
|
} catch (error) {
|
|
throw new Error(`Failed to initialize instance for ${profileName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate instance directory structure (auto-fix missing directories)
|
|
* @param {string} instancePath - Instance path
|
|
*/
|
|
validateInstance(instancePath) {
|
|
// Check required directories (auto-create if missing for migration)
|
|
const requiredDirs = [
|
|
'session-env',
|
|
'todos',
|
|
'logs',
|
|
'file-history',
|
|
'shell-snapshots',
|
|
'debug',
|
|
'.anthropic'
|
|
];
|
|
|
|
for (const dir of requiredDirs) {
|
|
const dirPath = path.join(instancePath, dir);
|
|
if (!fs.existsSync(dirPath)) {
|
|
// Auto-create missing directory (migration from older versions)
|
|
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
}
|
|
}
|
|
|
|
// Note: Credentials managed by Claude CLI in instance (no validation needed)
|
|
}
|
|
|
|
/**
|
|
* Delete instance for profile
|
|
* @param {string} profileName - Profile name
|
|
*/
|
|
deleteInstance(profileName) {
|
|
const instancePath = this.getInstancePath(profileName);
|
|
|
|
if (!fs.existsSync(instancePath)) {
|
|
return;
|
|
}
|
|
|
|
// Recursive delete
|
|
fs.rmSync(instancePath, { recursive: true, force: true });
|
|
}
|
|
|
|
/**
|
|
* List all instance names
|
|
* @returns {Array<string>} Instance names
|
|
*/
|
|
listInstances() {
|
|
if (!fs.existsSync(this.instancesDir)) {
|
|
return [];
|
|
}
|
|
|
|
return fs.readdirSync(this.instancesDir)
|
|
.filter(name => {
|
|
const instancePath = path.join(this.instancesDir, name);
|
|
return fs.statSync(instancePath).isDirectory();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if instance exists for profile
|
|
* @param {string} profileName - Profile name
|
|
* @returns {boolean} True if exists
|
|
*/
|
|
hasInstance(profileName) {
|
|
const instancePath = this.getInstancePath(profileName);
|
|
return fs.existsSync(instancePath);
|
|
}
|
|
|
|
/**
|
|
* Copy global configs to instance (optional)
|
|
* @param {string} instancePath - Instance path
|
|
*/
|
|
_copyGlobalConfigs(instancePath) {
|
|
// No longer needed - settings.json now symlinked via SharedManager
|
|
// Keeping method for backward compatibility (empty implementation)
|
|
// Can be removed in future major version
|
|
}
|
|
|
|
/**
|
|
* Copy directory recursively
|
|
* @param {string} src - Source directory
|
|
* @param {string} dest - Destination directory
|
|
*/
|
|
_copyDirectory(src, dest) {
|
|
if (!fs.existsSync(dest)) {
|
|
fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
}
|
|
|
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const srcPath = path.join(src, entry.name);
|
|
const destPath = path.join(dest, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
this._copyDirectory(srcPath, destPath);
|
|
} else {
|
|
fs.copyFileSync(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize profile name for filesystem
|
|
* @param {string} name - Profile name
|
|
* @returns {string} Safe name
|
|
*/
|
|
_sanitizeName(name) {
|
|
// Replace unsafe characters with dash
|
|
return name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
}
|
|
}
|
|
|
|
module.exports = InstanceManager;
|