@haiyangj/ccs 1.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/CHANGELOG.md +47 -0
- package/DESIGN_SPEC.md +530 -0
- package/IMPLEMENTATION.md +340 -0
- package/README.md +365 -0
- package/UPDATE_NOTES.md +126 -0
- package/bin/ccm.js +96 -0
- package/package.json +28 -0
- package/src/commands/add.js +98 -0
- package/src/commands/delete.js +50 -0
- package/src/commands/export.js +46 -0
- package/src/commands/import.js +99 -0
- package/src/commands/init.js +55 -0
- package/src/commands/list.js +55 -0
- package/src/commands/rename.js +40 -0
- package/src/commands/set.js +90 -0
- package/src/commands/show.js +33 -0
- package/src/commands/use.js +61 -0
- package/src/core/config.js +123 -0
- package/src/core/profiles.js +221 -0
- package/src/core/validator.js +96 -0
- package/src/utils/logger.js +66 -0
- package/src/utils/paths.js +83 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { getProfile, updateProfile, getCurrentProfileName } from '../core/profiles.js';
|
|
3
|
+
import { updateApiConfig } from '../core/config.js';
|
|
4
|
+
import { validateProfile } from '../core/validator.js';
|
|
5
|
+
import { success, error, warning, maskApiKey, info } from '../utils/logger.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update an existing profile
|
|
9
|
+
*/
|
|
10
|
+
export async function setCommand(name, url, key) {
|
|
11
|
+
try {
|
|
12
|
+
// Interactive mode if arguments not provided
|
|
13
|
+
if (!name || !url || !key) {
|
|
14
|
+
const answers = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: 'input',
|
|
17
|
+
name: 'name',
|
|
18
|
+
message: 'Profile name to update:',
|
|
19
|
+
default: name,
|
|
20
|
+
validate: (input) => input ? true : 'Profile name is required'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'url',
|
|
25
|
+
message: 'New API URL:',
|
|
26
|
+
default: url,
|
|
27
|
+
validate: (input) => {
|
|
28
|
+
const result = validateProfile('default', input, 'sk-ant-12345678901234567890');
|
|
29
|
+
if (!result.valid && result.error.includes('URL')) {
|
|
30
|
+
return result.error;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'password',
|
|
37
|
+
name: 'key',
|
|
38
|
+
message: 'New API Key:',
|
|
39
|
+
mask: '*',
|
|
40
|
+
default: key,
|
|
41
|
+
validate: (input) => {
|
|
42
|
+
const result = validateProfile('default', 'https://api.anthropic.com', input);
|
|
43
|
+
if (!result.valid && result.error.includes('key')) {
|
|
44
|
+
return result.error;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
name = answers.name;
|
|
52
|
+
url = answers.url;
|
|
53
|
+
key = answers.key;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if profile exists
|
|
57
|
+
const existing = getProfile(name);
|
|
58
|
+
if (!existing) {
|
|
59
|
+
error(`Profile '${name}' does not exist`);
|
|
60
|
+
info('Use "ccm add" to create a new profile');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Validate inputs
|
|
65
|
+
const validation = validateProfile(name, url, key);
|
|
66
|
+
if (!validation.valid) {
|
|
67
|
+
error(validation.error);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Update profile
|
|
72
|
+
updateProfile(name, url, key);
|
|
73
|
+
|
|
74
|
+
success(`Profile '${name}' updated successfully`);
|
|
75
|
+
console.log(` API URL: ${url}`);
|
|
76
|
+
console.log(` API Key: ${maskApiKey(key)}`);
|
|
77
|
+
console.log();
|
|
78
|
+
|
|
79
|
+
// If this is the current profile, update settings.json
|
|
80
|
+
const currentProfile = getCurrentProfileName();
|
|
81
|
+
if (currentProfile === name) {
|
|
82
|
+
updateApiConfig(url, key);
|
|
83
|
+
warning('This profile is currently active. Changes applied to settings.json');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
} catch (err) {
|
|
87
|
+
error(`Failed to update profile: ${err.message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getCurrentProfile, getCurrentProfileName } from '../core/profiles.js';
|
|
2
|
+
import { section, error, info, maskApiKey } from '../utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Show current active profile
|
|
6
|
+
*/
|
|
7
|
+
export async function showCommand() {
|
|
8
|
+
try {
|
|
9
|
+
const currentName = getCurrentProfileName();
|
|
10
|
+
const currentProfile = getCurrentProfile();
|
|
11
|
+
|
|
12
|
+
if (!currentProfile) {
|
|
13
|
+
info('No active profile. Use "ccm use <name>" to activate a profile.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
section(`Current profile: ${currentName}`);
|
|
18
|
+
|
|
19
|
+
console.log(` API URL: ${currentProfile.apiUrl}`);
|
|
20
|
+
console.log(` API Key: ${maskApiKey(currentProfile.apiKey)}`);
|
|
21
|
+
console.log(` Created: ${new Date(currentProfile.createdAt).toLocaleString()}`);
|
|
22
|
+
|
|
23
|
+
if (currentProfile.lastUsed) {
|
|
24
|
+
console.log(` Last used: ${new Date(currentProfile.lastUsed).toLocaleString()}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log();
|
|
28
|
+
|
|
29
|
+
} catch (err) {
|
|
30
|
+
error(`Failed to show profile: ${err.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getProfile, setCurrentProfile, getAllProfiles } from '../core/profiles.js';
|
|
2
|
+
import { updateApiConfig } from '../core/config.js';
|
|
3
|
+
import { success, error, maskApiKey, info } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find similar profile names for suggestions
|
|
7
|
+
*/
|
|
8
|
+
function findSimilarProfiles(name, profiles) {
|
|
9
|
+
const profileNames = Object.keys(profiles);
|
|
10
|
+
return profileNames.filter(p =>
|
|
11
|
+
p.toLowerCase().includes(name.toLowerCase()) ||
|
|
12
|
+
name.toLowerCase().includes(p.toLowerCase())
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Switch to a different profile
|
|
18
|
+
*/
|
|
19
|
+
export async function useCommand(name) {
|
|
20
|
+
try {
|
|
21
|
+
if (!name) {
|
|
22
|
+
error('Profile name is required');
|
|
23
|
+
info('Usage: ccm use <name>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get profile
|
|
28
|
+
const profile = getProfile(name);
|
|
29
|
+
|
|
30
|
+
if (!profile) {
|
|
31
|
+
error(`Profile '${name}' does not exist`);
|
|
32
|
+
|
|
33
|
+
// Suggest similar profiles
|
|
34
|
+
const allProfiles = getAllProfiles();
|
|
35
|
+
const similar = findSimilarProfiles(name, allProfiles);
|
|
36
|
+
|
|
37
|
+
if (similar.length > 0) {
|
|
38
|
+
info(`Did you mean: ${similar.join(', ')}?`);
|
|
39
|
+
} else {
|
|
40
|
+
info('Use "ccm list" to see available profiles');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Update settings.json
|
|
47
|
+
updateApiConfig(profile.apiUrl, profile.apiKey);
|
|
48
|
+
|
|
49
|
+
// Update current profile in profiles.json
|
|
50
|
+
setCurrentProfile(name);
|
|
51
|
+
|
|
52
|
+
success(`Switched to profile '${name}'`);
|
|
53
|
+
console.log(` API URL: ${profile.apiUrl}`);
|
|
54
|
+
console.log(` API Key: ${maskApiKey(profile.apiKey)}`);
|
|
55
|
+
console.log();
|
|
56
|
+
|
|
57
|
+
} catch (err) {
|
|
58
|
+
error(`Failed to switch profile: ${err.message}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { getSettingsPath, resolveClaudeDir } from '../utils/paths.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Config file (settings.json) operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read settings.json
|
|
11
|
+
*/
|
|
12
|
+
export function readSettings() {
|
|
13
|
+
const settingsPath = getSettingsPath();
|
|
14
|
+
|
|
15
|
+
if (!existsSync(settingsPath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const data = readFileSync(settingsPath, 'utf-8');
|
|
21
|
+
return JSON.parse(data);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(`Failed to read settings: ${error.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write settings.json atomically
|
|
29
|
+
*/
|
|
30
|
+
export function writeSettings(settings) {
|
|
31
|
+
const settingsPath = getSettingsPath();
|
|
32
|
+
const claudeDir = dirname(settingsPath);
|
|
33
|
+
|
|
34
|
+
// Ensure .claude directory exists
|
|
35
|
+
if (!existsSync(claudeDir)) {
|
|
36
|
+
mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Write to temp file first
|
|
41
|
+
const tempPath = settingsPath + '.tmp';
|
|
42
|
+
writeFileSync(tempPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
43
|
+
|
|
44
|
+
// Backup existing file
|
|
45
|
+
if (existsSync(settingsPath)) {
|
|
46
|
+
const backupPath = settingsPath + '.bak';
|
|
47
|
+
writeFileSync(backupPath, readFileSync(settingsPath));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Rename temp to actual
|
|
51
|
+
writeFileSync(settingsPath, readFileSync(tempPath), 'utf-8');
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Failed to write settings: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Update specific fields in settings.json
|
|
59
|
+
*/
|
|
60
|
+
export function updateSettings(updates) {
|
|
61
|
+
const settings = readSettings() || {};
|
|
62
|
+
const merged = { ...settings, ...updates };
|
|
63
|
+
writeSettings(merged);
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get API configuration from settings
|
|
69
|
+
*/
|
|
70
|
+
export function getApiConfig() {
|
|
71
|
+
const settings = readSettings();
|
|
72
|
+
if (!settings) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
apiUrl: settings.env?.ANTHROPIC_BASE_URL || '',
|
|
78
|
+
apiKey: settings.env?.ANTHROPIC_AUTH_TOKEN || ''
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Update API configuration in settings
|
|
84
|
+
*/
|
|
85
|
+
export function updateApiConfig(apiUrl, apiKey) {
|
|
86
|
+
const settings = readSettings() || {};
|
|
87
|
+
|
|
88
|
+
// Ensure env object exists
|
|
89
|
+
if (!settings.env) {
|
|
90
|
+
settings.env = {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update env fields
|
|
94
|
+
settings.env.ANTHROPIC_BASE_URL = apiUrl;
|
|
95
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
96
|
+
|
|
97
|
+
writeSettings(settings);
|
|
98
|
+
return settings;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if settings.json exists
|
|
103
|
+
*/
|
|
104
|
+
export function settingsFileExists() {
|
|
105
|
+
return existsSync(getSettingsPath());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create initial settings.json with profile data
|
|
110
|
+
*/
|
|
111
|
+
export function createSettings(apiUrl, apiKey) {
|
|
112
|
+
const settings = {
|
|
113
|
+
env: {
|
|
114
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
115
|
+
ANTHROPIC_BASE_URL: apiUrl
|
|
116
|
+
},
|
|
117
|
+
enabledPlugins: {},
|
|
118
|
+
apiUrl: "",
|
|
119
|
+
apiKey: ""
|
|
120
|
+
};
|
|
121
|
+
writeSettings(settings);
|
|
122
|
+
return settings;
|
|
123
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
import { getProfilesPath, resolveClaudeDir } from '../utils/paths.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Profile management operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PROFILES_VERSION = '1.0.0';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize empty profiles structure
|
|
13
|
+
*/
|
|
14
|
+
function createEmptyProfiles() {
|
|
15
|
+
return {
|
|
16
|
+
version: PROFILES_VERSION,
|
|
17
|
+
currentProfile: null,
|
|
18
|
+
profiles: {}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read profiles.json
|
|
24
|
+
*/
|
|
25
|
+
export function readProfiles() {
|
|
26
|
+
const profilesPath = getProfilesPath();
|
|
27
|
+
|
|
28
|
+
if (!existsSync(profilesPath)) {
|
|
29
|
+
return createEmptyProfiles();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const data = readFileSync(profilesPath, 'utf-8');
|
|
34
|
+
return JSON.parse(data);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Failed to read profiles: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write profiles.json atomically
|
|
42
|
+
*/
|
|
43
|
+
export function writeProfiles(profiles) {
|
|
44
|
+
const profilesPath = getProfilesPath();
|
|
45
|
+
const claudeDir = dirname(profilesPath);
|
|
46
|
+
|
|
47
|
+
// Ensure .claude directory exists
|
|
48
|
+
if (!existsSync(claudeDir)) {
|
|
49
|
+
mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Write to temp file first
|
|
54
|
+
const tempPath = profilesPath + '.tmp';
|
|
55
|
+
writeFileSync(tempPath, JSON.stringify(profiles, null, 2), { mode: 0o600 });
|
|
56
|
+
|
|
57
|
+
// Backup existing file
|
|
58
|
+
if (existsSync(profilesPath)) {
|
|
59
|
+
const backupPath = profilesPath + '.bak';
|
|
60
|
+
writeFileSync(backupPath, readFileSync(profilesPath));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Rename temp to actual
|
|
64
|
+
writeFileSync(profilesPath, readFileSync(tempPath), { mode: 0o600 });
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to write profiles: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get a specific profile by name
|
|
72
|
+
*/
|
|
73
|
+
export function getProfile(name) {
|
|
74
|
+
const profiles = readProfiles();
|
|
75
|
+
return profiles.profiles[name] || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get all profiles
|
|
80
|
+
*/
|
|
81
|
+
export function getAllProfiles() {
|
|
82
|
+
const profiles = readProfiles();
|
|
83
|
+
return profiles.profiles;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get current active profile name
|
|
88
|
+
*/
|
|
89
|
+
export function getCurrentProfileName() {
|
|
90
|
+
const profiles = readProfiles();
|
|
91
|
+
return profiles.currentProfile;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get current active profile
|
|
96
|
+
*/
|
|
97
|
+
export function getCurrentProfile() {
|
|
98
|
+
const profiles = readProfiles();
|
|
99
|
+
const currentName = profiles.currentProfile;
|
|
100
|
+
return currentName ? profiles.profiles[currentName] : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add a new profile
|
|
105
|
+
*/
|
|
106
|
+
export function addProfile(name, apiUrl, apiKey) {
|
|
107
|
+
const profiles = readProfiles();
|
|
108
|
+
|
|
109
|
+
if (profiles.profiles[name]) {
|
|
110
|
+
throw new Error(`Profile '${name}' already exists`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
profiles.profiles[name] = {
|
|
114
|
+
name,
|
|
115
|
+
apiUrl,
|
|
116
|
+
apiKey,
|
|
117
|
+
createdAt: new Date().toISOString(),
|
|
118
|
+
lastUsed: null
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Set as current if it's the first profile
|
|
122
|
+
if (Object.keys(profiles.profiles).length === 1) {
|
|
123
|
+
profiles.currentProfile = name;
|
|
124
|
+
profiles.profiles[name].lastUsed = new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
writeProfiles(profiles);
|
|
128
|
+
return profiles.profiles[name];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Update an existing profile
|
|
133
|
+
*/
|
|
134
|
+
export function updateProfile(name, apiUrl, apiKey) {
|
|
135
|
+
const profiles = readProfiles();
|
|
136
|
+
|
|
137
|
+
if (!profiles.profiles[name]) {
|
|
138
|
+
throw new Error(`Profile '${name}' does not exist`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const existing = profiles.profiles[name];
|
|
142
|
+
profiles.profiles[name] = {
|
|
143
|
+
...existing,
|
|
144
|
+
apiUrl,
|
|
145
|
+
apiKey
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
writeProfiles(profiles);
|
|
149
|
+
return profiles.profiles[name];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Delete a profile
|
|
154
|
+
*/
|
|
155
|
+
export function deleteProfile(name) {
|
|
156
|
+
const profiles = readProfiles();
|
|
157
|
+
|
|
158
|
+
if (!profiles.profiles[name]) {
|
|
159
|
+
throw new Error(`Profile '${name}' does not exist`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (profiles.currentProfile === name) {
|
|
163
|
+
throw new Error(`Cannot delete active profile '${name}'. Switch to another profile first.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
delete profiles.profiles[name];
|
|
167
|
+
writeProfiles(profiles);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Rename a profile
|
|
172
|
+
*/
|
|
173
|
+
export function renameProfile(oldName, newName) {
|
|
174
|
+
const profiles = readProfiles();
|
|
175
|
+
|
|
176
|
+
if (!profiles.profiles[oldName]) {
|
|
177
|
+
throw new Error(`Profile '${oldName}' does not exist`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (profiles.profiles[newName]) {
|
|
181
|
+
throw new Error(`Profile '${newName}' already exists`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
profiles.profiles[newName] = {
|
|
185
|
+
...profiles.profiles[oldName],
|
|
186
|
+
name: newName
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
delete profiles.profiles[oldName];
|
|
190
|
+
|
|
191
|
+
// Update current profile reference if needed
|
|
192
|
+
if (profiles.currentProfile === oldName) {
|
|
193
|
+
profiles.currentProfile = newName;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
writeProfiles(profiles);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set current active profile
|
|
201
|
+
*/
|
|
202
|
+
export function setCurrentProfile(name) {
|
|
203
|
+
const profiles = readProfiles();
|
|
204
|
+
|
|
205
|
+
if (!profiles.profiles[name]) {
|
|
206
|
+
throw new Error(`Profile '${name}' does not exist`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
profiles.currentProfile = name;
|
|
210
|
+
profiles.profiles[name].lastUsed = new Date().toISOString();
|
|
211
|
+
|
|
212
|
+
writeProfiles(profiles);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if profiles.json exists and has profiles
|
|
217
|
+
*/
|
|
218
|
+
export function hasProfiles() {
|
|
219
|
+
const profiles = readProfiles();
|
|
220
|
+
return Object.keys(profiles.profiles).length > 0;
|
|
221
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate profile name
|
|
7
|
+
* Rules: 1-50 chars, alphanumeric, dash, underscore only
|
|
8
|
+
*/
|
|
9
|
+
export function validateProfileName(name) {
|
|
10
|
+
if (!name) {
|
|
11
|
+
return { valid: false, error: 'Profile name is required' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof name !== 'string') {
|
|
15
|
+
return { valid: false, error: 'Profile name must be a string' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (name.length < 1 || name.length > 50) {
|
|
19
|
+
return { valid: false, error: 'Profile name must be 1-50 characters' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/;
|
|
23
|
+
if (!validPattern.test(name)) {
|
|
24
|
+
return { valid: false, error: 'Profile name can only contain letters, numbers, dashes, and underscores' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { valid: true };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate API URL
|
|
32
|
+
* Rules: Valid HTTPS URL format
|
|
33
|
+
*/
|
|
34
|
+
export function validateApiUrl(url) {
|
|
35
|
+
if (!url) {
|
|
36
|
+
return { valid: false, error: 'API URL is required' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof url !== 'string') {
|
|
40
|
+
return { valid: false, error: 'API URL must be a string' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
|
|
46
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
47
|
+
return { valid: false, error: 'API URL must use HTTPS or HTTP protocol' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { valid: true };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate API key
|
|
58
|
+
* Rules: Non-empty string, minimum 10 chars
|
|
59
|
+
*/
|
|
60
|
+
export function validateApiKey(key) {
|
|
61
|
+
if (!key) {
|
|
62
|
+
return { valid: false, error: 'API key is required' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof key !== 'string') {
|
|
66
|
+
return { valid: false, error: 'API key must be a string' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (key.length < 10) {
|
|
70
|
+
return { valid: false, error: 'API key must be at least 10 characters' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate all profile inputs at once
|
|
78
|
+
*/
|
|
79
|
+
export function validateProfile(name, url, key) {
|
|
80
|
+
const nameValidation = validateProfileName(name);
|
|
81
|
+
if (!nameValidation.valid) {
|
|
82
|
+
return nameValidation;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const urlValidation = validateApiUrl(url);
|
|
86
|
+
if (!urlValidation.valid) {
|
|
87
|
+
return urlValidation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const keyValidation = validateApiKey(key);
|
|
91
|
+
if (!keyValidation.valid) {
|
|
92
|
+
return keyValidation;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { valid: true };
|
|
96
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Logger utility with colored output
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function success(message) {
|
|
8
|
+
console.log(chalk.green('✓'), message);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function error(message) {
|
|
12
|
+
console.log(chalk.red('✗'), message);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function warning(message) {
|
|
16
|
+
console.log(chalk.yellow('⚠'), message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function info(message) {
|
|
20
|
+
console.log(chalk.blue('ℹ'), message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function log(message) {
|
|
24
|
+
console.log(message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function header(message) {
|
|
28
|
+
console.log(chalk.bold.blue(message));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function dim(message) {
|
|
32
|
+
return chalk.dim(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function bold(message) {
|
|
36
|
+
return chalk.bold(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mask API key to show only last 4 characters
|
|
41
|
+
*/
|
|
42
|
+
export function maskApiKey(key) {
|
|
43
|
+
if (!key || key.length <= 4) {
|
|
44
|
+
return '****';
|
|
45
|
+
}
|
|
46
|
+
return '****' + key.slice(-4);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format profile name with active indicator
|
|
51
|
+
*/
|
|
52
|
+
export function formatProfileName(name, isActive = false) {
|
|
53
|
+
if (isActive) {
|
|
54
|
+
return chalk.green('✓ ' + name);
|
|
55
|
+
}
|
|
56
|
+
return ' ' + name;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Print a section header with padding
|
|
61
|
+
*/
|
|
62
|
+
export function section(title) {
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(chalk.bold.blue(title));
|
|
65
|
+
console.log();
|
|
66
|
+
}
|