@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.
@@ -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
+ }