@kaitranntt/ccs 2.5.1 → 3.0.1

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.
@@ -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;