@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.
- package/README.ja.md +325 -0
- package/README.md +149 -102
- package/README.vi.md +147 -94
- package/VERSION +1 -1
- package/bin/auth-commands.js +405 -0
- package/bin/ccs.js +113 -35
- package/bin/config-manager.js +36 -7
- package/bin/doctor.js +365 -0
- package/bin/error-manager.js +159 -0
- package/bin/instance-manager.js +218 -0
- package/bin/profile-detector.js +199 -0
- package/bin/profile-registry.js +226 -0
- package/bin/recovery-manager.js +135 -0
- package/lib/ccs +856 -301
- package/lib/ccs.ps1 +792 -122
- package/package.json +1 -1
- package/scripts/postinstall.js +111 -12
|
@@ -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;
|