ccjk 1.3.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/LICENSE +21 -0
- package/README.ja.md +455 -0
- package/README.ko.md +455 -0
- package/README.md +550 -0
- package/README.zh-CN.md +488 -0
- package/bin/ccjk.mjs +2 -0
- package/dist/chunks/api-providers.mjs +89 -0
- package/dist/chunks/claude-code-config-manager.mjs +733 -0
- package/dist/chunks/claude-code-incremental-manager.mjs +603 -0
- package/dist/chunks/codex-config-switch.mjs +427 -0
- package/dist/chunks/codex-provider-manager.mjs +232 -0
- package/dist/chunks/codex-uninstaller.mjs +404 -0
- package/dist/chunks/commands.mjs +120 -0
- package/dist/chunks/features.mjs +642 -0
- package/dist/chunks/simple-config.mjs +10445 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +5972 -0
- package/dist/i18n/locales/en/api.json +63 -0
- package/dist/i18n/locales/en/ccjk.json +276 -0
- package/dist/i18n/locales/en/ccr.json +65 -0
- package/dist/i18n/locales/en/cli.json +57 -0
- package/dist/i18n/locales/en/codex.json +124 -0
- package/dist/i18n/locales/en/cometix.json +29 -0
- package/dist/i18n/locales/en/common.json +20 -0
- package/dist/i18n/locales/en/configuration.json +77 -0
- package/dist/i18n/locales/en/errors.json +26 -0
- package/dist/i18n/locales/en/installation.json +80 -0
- package/dist/i18n/locales/en/interview.json +104 -0
- package/dist/i18n/locales/en/language.json +19 -0
- package/dist/i18n/locales/en/mcp.json +38 -0
- package/dist/i18n/locales/en/menu.json +51 -0
- package/dist/i18n/locales/en/multi-config.json +79 -0
- package/dist/i18n/locales/en/shencha.json +14 -0
- package/dist/i18n/locales/en/team.json +7 -0
- package/dist/i18n/locales/en/tools.json +42 -0
- package/dist/i18n/locales/en/uninstall.json +56 -0
- package/dist/i18n/locales/en/updater.json +25 -0
- package/dist/i18n/locales/en/workflow.json +25 -0
- package/dist/i18n/locales/zh-CN/api.json +63 -0
- package/dist/i18n/locales/zh-CN/ccjk.json +276 -0
- package/dist/i18n/locales/zh-CN/ccr.json +65 -0
- package/dist/i18n/locales/zh-CN/cli.json +57 -0
- package/dist/i18n/locales/zh-CN/codex.json +124 -0
- package/dist/i18n/locales/zh-CN/cometix.json +29 -0
- package/dist/i18n/locales/zh-CN/common.json +20 -0
- package/dist/i18n/locales/zh-CN/configuration.json +77 -0
- package/dist/i18n/locales/zh-CN/errors.json +26 -0
- package/dist/i18n/locales/zh-CN/installation.json +80 -0
- package/dist/i18n/locales/zh-CN/interview.json +104 -0
- package/dist/i18n/locales/zh-CN/language.json +19 -0
- package/dist/i18n/locales/zh-CN/mcp.json +38 -0
- package/dist/i18n/locales/zh-CN/menu.json +51 -0
- package/dist/i18n/locales/zh-CN/multi-config.json +79 -0
- package/dist/i18n/locales/zh-CN/shencha.json +14 -0
- package/dist/i18n/locales/zh-CN/team.json +7 -0
- package/dist/i18n/locales/zh-CN/tools.json +42 -0
- package/dist/i18n/locales/zh-CN/uninstall.json +56 -0
- package/dist/i18n/locales/zh-CN/updater.json +25 -0
- package/dist/i18n/locales/zh-CN/workflow.json +25 -0
- package/dist/index.d.mts +2644 -0
- package/dist/index.d.ts +2644 -0
- package/dist/index.mjs +1706 -0
- package/package.json +157 -0
- package/templates/CLAUDE.md +219 -0
- package/templates/claude-code/CLAUDE.md +250 -0
- package/templates/claude-code/common/settings.json +38 -0
- package/templates/claude-code/en/workflow/bmad/commands/bmad-init.md +165 -0
- package/templates/claude-code/en/workflow/common/agents/get-current-datetime.md +29 -0
- package/templates/claude-code/en/workflow/common/agents/init-architect.md +114 -0
- package/templates/claude-code/en/workflow/common/commands/init-project.md +53 -0
- package/templates/claude-code/en/workflow/plan/agents/planner.md +116 -0
- package/templates/claude-code/en/workflow/plan/agents/ui-ux-designer.md +91 -0
- package/templates/claude-code/en/workflow/plan/commands/feat.md +105 -0
- package/templates/claude-code/zh-CN/workflow/bmad/commands/bmad-init.md +172 -0
- package/templates/claude-code/zh-CN/workflow/common/agents/get-current-datetime.md +29 -0
- package/templates/claude-code/zh-CN/workflow/common/agents/init-architect.md +114 -0
- package/templates/claude-code/zh-CN/workflow/common/commands/init-project.md +53 -0
- package/templates/claude-code/zh-CN/workflow/plan/agents/planner.md +116 -0
- package/templates/claude-code/zh-CN/workflow/plan/agents/ui-ux-designer.md +91 -0
- package/templates/claude-code/zh-CN/workflow/plan/commands/feat.md +105 -0
- package/templates/codex/common/config.toml +0 -0
- package/templates/common/output-styles/en/casual-friendly.md +97 -0
- package/templates/common/output-styles/en/engineer-professional.md +88 -0
- package/templates/common/output-styles/en/expert-concise.md +93 -0
- package/templates/common/output-styles/en/laowang-engineer.md +127 -0
- package/templates/common/output-styles/en/nekomata-engineer.md +120 -0
- package/templates/common/output-styles/en/ojousama-engineer.md +121 -0
- package/templates/common/output-styles/en/teaching-mode.md +102 -0
- package/templates/common/output-styles/en/technical-precise.md +101 -0
- package/templates/common/output-styles/zh-CN/engineer-professional.md +89 -0
- package/templates/common/output-styles/zh-CN/laowang-engineer.md +127 -0
- package/templates/common/output-styles/zh-CN/nekomata-engineer.md +120 -0
- package/templates/common/output-styles/zh-CN/ojousama-engineer.md +121 -0
- package/templates/common/workflow/git/en/git-cleanBranches.md +102 -0
- package/templates/common/workflow/git/en/git-commit.md +205 -0
- package/templates/common/workflow/git/en/git-rollback.md +90 -0
- package/templates/common/workflow/git/en/git-worktree.md +276 -0
- package/templates/common/workflow/git/zh-CN/git-cleanBranches.md +102 -0
- package/templates/common/workflow/git/zh-CN/git-commit.md +205 -0
- package/templates/common/workflow/git/zh-CN/git-rollback.md +90 -0
- package/templates/common/workflow/git/zh-CN/git-worktree.md +276 -0
- package/templates/common/workflow/interview/en/interview.md +212 -0
- package/templates/common/workflow/interview/zh-CN/interview.md +212 -0
- package/templates/common/workflow/sixStep/en/workflow.md +251 -0
- package/templates/common/workflow/sixStep/zh-CN/workflow.md +215 -0
- package/templates/industry/devops/en/ci-cd-pipeline.md +410 -0
- package/templates/industry/web-dev/en/api-design.md +299 -0
- package/templates/industry/web-dev/en/react-nextjs-setup.md +236 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import { join } from 'pathe';
|
|
3
|
+
import { N as ZCF_CONFIG_FILE, Z as ZCF_CONFIG_DIR, cR as ensureDir, cS as readDefaultTomlConfig, cT as createDefaultTomlConfig, cU as exists, cC as readJsonConfig, cV as writeTomlConfig, t as SETTINGS_FILE, cW as clearModelEnv, cX as copyFile } from './simple-config.mjs';
|
|
4
|
+
import 'node:fs';
|
|
5
|
+
import 'node:process';
|
|
6
|
+
import 'ansis';
|
|
7
|
+
import 'inquirer';
|
|
8
|
+
import 'smol-toml';
|
|
9
|
+
import 'node:child_process';
|
|
10
|
+
import 'node:os';
|
|
11
|
+
import 'node:util';
|
|
12
|
+
import 'node:url';
|
|
13
|
+
import 'inquirer-toggle';
|
|
14
|
+
import 'ora';
|
|
15
|
+
import 'tinyexec';
|
|
16
|
+
import 'semver';
|
|
17
|
+
import 'node:fs/promises';
|
|
18
|
+
import 'fs-extra';
|
|
19
|
+
import 'trash';
|
|
20
|
+
import 'i18next';
|
|
21
|
+
import 'i18next-fs-backend';
|
|
22
|
+
|
|
23
|
+
class ClaudeCodeConfigManager {
|
|
24
|
+
static CONFIG_FILE = ZCF_CONFIG_FILE;
|
|
25
|
+
static LEGACY_CONFIG_FILE = join(ZCF_CONFIG_DIR, "claude-code-configs.json");
|
|
26
|
+
/**
|
|
27
|
+
* Ensure configuration directory exists
|
|
28
|
+
*/
|
|
29
|
+
static ensureConfigDir() {
|
|
30
|
+
ensureDir(ZCF_CONFIG_DIR);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read TOML configuration
|
|
34
|
+
*/
|
|
35
|
+
static readTomlConfig() {
|
|
36
|
+
return readDefaultTomlConfig();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Load TOML configuration, falling back to default when missing
|
|
40
|
+
*/
|
|
41
|
+
static loadTomlConfig() {
|
|
42
|
+
const existingConfig = this.readTomlConfig();
|
|
43
|
+
if (existingConfig) {
|
|
44
|
+
return existingConfig;
|
|
45
|
+
}
|
|
46
|
+
return createDefaultTomlConfig();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Migrate legacy JSON-based configuration into TOML storage
|
|
50
|
+
*/
|
|
51
|
+
static migrateFromLegacyConfig() {
|
|
52
|
+
if (!exists(this.LEGACY_CONFIG_FILE)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const legacyConfig = readJsonConfig(this.LEGACY_CONFIG_FILE);
|
|
57
|
+
if (!legacyConfig) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const normalizedProfiles = {};
|
|
61
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
62
|
+
let migratedCurrentKey = "";
|
|
63
|
+
Object.entries(legacyConfig.profiles || {}).forEach(([legacyKey, profile]) => {
|
|
64
|
+
const sourceProfile = profile;
|
|
65
|
+
const name = sourceProfile.name?.trim() || legacyKey;
|
|
66
|
+
const baseKey = this.generateProfileId(name);
|
|
67
|
+
let uniqueKey = baseKey || legacyKey;
|
|
68
|
+
let suffix = 2;
|
|
69
|
+
while (existingKeys.has(uniqueKey)) {
|
|
70
|
+
uniqueKey = `${baseKey || legacyKey}-${suffix++}`;
|
|
71
|
+
}
|
|
72
|
+
existingKeys.add(uniqueKey);
|
|
73
|
+
const sanitizedProfile = this.sanitizeProfile({
|
|
74
|
+
...sourceProfile,
|
|
75
|
+
name
|
|
76
|
+
});
|
|
77
|
+
normalizedProfiles[uniqueKey] = {
|
|
78
|
+
...sanitizedProfile,
|
|
79
|
+
id: uniqueKey
|
|
80
|
+
};
|
|
81
|
+
if (legacyConfig.currentProfileId === legacyKey || legacyConfig.currentProfileId === sourceProfile.id) {
|
|
82
|
+
migratedCurrentKey = uniqueKey;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
if (!migratedCurrentKey && legacyConfig.currentProfileId) {
|
|
86
|
+
const fallbackKey = this.generateProfileId(legacyConfig.currentProfileId);
|
|
87
|
+
if (existingKeys.has(fallbackKey)) {
|
|
88
|
+
migratedCurrentKey = fallbackKey;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!migratedCurrentKey && existingKeys.size > 0) {
|
|
92
|
+
migratedCurrentKey = Array.from(existingKeys)[0];
|
|
93
|
+
}
|
|
94
|
+
const migratedConfig = {
|
|
95
|
+
currentProfileId: migratedCurrentKey,
|
|
96
|
+
profiles: normalizedProfiles
|
|
97
|
+
};
|
|
98
|
+
this.writeConfig(migratedConfig);
|
|
99
|
+
return migratedConfig;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Failed to migrate legacy Claude Code config:", error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Read configuration
|
|
107
|
+
*/
|
|
108
|
+
static readConfig() {
|
|
109
|
+
try {
|
|
110
|
+
const tomlConfig = readDefaultTomlConfig();
|
|
111
|
+
if (!tomlConfig || !tomlConfig.claudeCode) {
|
|
112
|
+
return this.migrateFromLegacyConfig();
|
|
113
|
+
}
|
|
114
|
+
const { claudeCode } = tomlConfig;
|
|
115
|
+
const rawProfiles = claudeCode.profiles || {};
|
|
116
|
+
const sanitizedProfiles = Object.fromEntries(
|
|
117
|
+
Object.entries(rawProfiles).map(([key, profile]) => {
|
|
118
|
+
const storedProfile = this.sanitizeProfile({
|
|
119
|
+
...profile,
|
|
120
|
+
name: profile.name || key
|
|
121
|
+
});
|
|
122
|
+
return [key, { ...storedProfile, id: key }];
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
const configData = {
|
|
126
|
+
currentProfileId: claudeCode.currentProfile || "",
|
|
127
|
+
profiles: sanitizedProfiles
|
|
128
|
+
};
|
|
129
|
+
if (Object.keys(configData.profiles).length === 0) {
|
|
130
|
+
const migrated = this.migrateFromLegacyConfig();
|
|
131
|
+
if (migrated) {
|
|
132
|
+
return migrated;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return configData;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error("Failed to read Claude Code config:", error);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Write configuration
|
|
143
|
+
*/
|
|
144
|
+
static writeConfig(config) {
|
|
145
|
+
try {
|
|
146
|
+
this.ensureConfigDir();
|
|
147
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
148
|
+
const sanitizedProfiles = Object.fromEntries(
|
|
149
|
+
Object.entries(config.profiles).map(([key, profile]) => {
|
|
150
|
+
const normalizedName = profile.name?.trim() || key;
|
|
151
|
+
const profileKey = this.generateProfileId(normalizedName);
|
|
152
|
+
keyMap.set(key, profileKey);
|
|
153
|
+
const sanitizedProfile = this.sanitizeProfile({
|
|
154
|
+
...profile,
|
|
155
|
+
name: normalizedName
|
|
156
|
+
});
|
|
157
|
+
return [profileKey, sanitizedProfile];
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
const tomlConfig = this.loadTomlConfig();
|
|
161
|
+
const nextTomlConfig = {
|
|
162
|
+
...tomlConfig,
|
|
163
|
+
claudeCode: {
|
|
164
|
+
...tomlConfig.claudeCode,
|
|
165
|
+
currentProfile: keyMap.get(config.currentProfileId) || config.currentProfileId,
|
|
166
|
+
profiles: sanitizedProfiles
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
writeTomlConfig(this.CONFIG_FILE, nextTomlConfig);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error("Failed to write Claude Code config:", error);
|
|
172
|
+
throw new Error(`Failed to write config: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Create empty configuration
|
|
177
|
+
*/
|
|
178
|
+
static createEmptyConfig() {
|
|
179
|
+
return {
|
|
180
|
+
currentProfileId: "",
|
|
181
|
+
profiles: {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Apply profile settings to Claude Code runtime
|
|
186
|
+
*/
|
|
187
|
+
static async applyProfileSettings(profile) {
|
|
188
|
+
const { ensureI18nInitialized, i18n } = await import('./simple-config.mjs').then(function (n) { return n.d3; });
|
|
189
|
+
ensureI18nInitialized();
|
|
190
|
+
try {
|
|
191
|
+
if (!profile) {
|
|
192
|
+
const { switchToOfficialLogin } = await import('./simple-config.mjs').then(function (n) { return n.d7; });
|
|
193
|
+
switchToOfficialLogin();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const { readJsonConfig: readJsonConfig2, writeJsonConfig } = await import('./simple-config.mjs').then(function (n) { return n.d5; });
|
|
197
|
+
const settings = readJsonConfig2(SETTINGS_FILE) || {};
|
|
198
|
+
if (!settings.env)
|
|
199
|
+
settings.env = {};
|
|
200
|
+
clearModelEnv(settings.env);
|
|
201
|
+
let shouldRestartCcr = false;
|
|
202
|
+
if (profile.authType === "api_key") {
|
|
203
|
+
settings.env.ANTHROPIC_API_KEY = profile.apiKey;
|
|
204
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
205
|
+
} else if (profile.authType === "auth_token") {
|
|
206
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = profile.apiKey;
|
|
207
|
+
delete settings.env.ANTHROPIC_API_KEY;
|
|
208
|
+
} else if (profile.authType === "ccr_proxy") {
|
|
209
|
+
const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.d8; });
|
|
210
|
+
const ccrConfig = readCcrConfig();
|
|
211
|
+
if (!ccrConfig) {
|
|
212
|
+
throw new Error(i18n.t("ccr:ccrNotConfigured") || "CCR proxy configuration not found");
|
|
213
|
+
}
|
|
214
|
+
const host = ccrConfig.HOST || "127.0.0.1";
|
|
215
|
+
const port = ccrConfig.PORT || 3456;
|
|
216
|
+
const apiKey = ccrConfig.APIKEY || "sk-ccjk-x-ccr";
|
|
217
|
+
settings.env.ANTHROPIC_BASE_URL = `http://${host}:${port}`;
|
|
218
|
+
settings.env.ANTHROPIC_API_KEY = apiKey;
|
|
219
|
+
delete settings.env.ANTHROPIC_AUTH_TOKEN;
|
|
220
|
+
shouldRestartCcr = true;
|
|
221
|
+
}
|
|
222
|
+
if (profile.authType !== "ccr_proxy") {
|
|
223
|
+
if (profile.baseUrl)
|
|
224
|
+
settings.env.ANTHROPIC_BASE_URL = profile.baseUrl;
|
|
225
|
+
else
|
|
226
|
+
delete settings.env.ANTHROPIC_BASE_URL;
|
|
227
|
+
}
|
|
228
|
+
const hasModelConfig = Boolean(
|
|
229
|
+
profile.primaryModel || profile.defaultHaikuModel || profile.defaultSonnetModel || profile.defaultOpusModel
|
|
230
|
+
);
|
|
231
|
+
if (hasModelConfig) {
|
|
232
|
+
if (profile.primaryModel)
|
|
233
|
+
settings.env.ANTHROPIC_MODEL = profile.primaryModel;
|
|
234
|
+
if (profile.defaultHaikuModel)
|
|
235
|
+
settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL = profile.defaultHaikuModel;
|
|
236
|
+
if (profile.defaultSonnetModel)
|
|
237
|
+
settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL = profile.defaultSonnetModel;
|
|
238
|
+
if (profile.defaultOpusModel)
|
|
239
|
+
settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL = profile.defaultOpusModel;
|
|
240
|
+
} else {
|
|
241
|
+
clearModelEnv(settings.env);
|
|
242
|
+
}
|
|
243
|
+
writeJsonConfig(SETTINGS_FILE, settings);
|
|
244
|
+
const { setPrimaryApiKey, addCompletedOnboarding } = await import('./simple-config.mjs').then(function (n) { return n.d6; });
|
|
245
|
+
setPrimaryApiKey();
|
|
246
|
+
addCompletedOnboarding();
|
|
247
|
+
if (shouldRestartCcr) {
|
|
248
|
+
const { runCcrRestart } = await import('./commands.mjs');
|
|
249
|
+
await runCcrRestart();
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
253
|
+
throw new Error(`${i18n.t("multi-config:failedToApplySettings")}: ${reason}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
static async applyCurrentProfile() {
|
|
257
|
+
const currentProfile = this.getCurrentProfile();
|
|
258
|
+
await this.applyProfileSettings(currentProfile);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Remove unsupported fields from profile payload
|
|
262
|
+
*/
|
|
263
|
+
static sanitizeProfile(profile) {
|
|
264
|
+
const sanitized = {
|
|
265
|
+
name: profile.name,
|
|
266
|
+
authType: profile.authType
|
|
267
|
+
};
|
|
268
|
+
if (profile.apiKey)
|
|
269
|
+
sanitized.apiKey = profile.apiKey;
|
|
270
|
+
if (profile.baseUrl)
|
|
271
|
+
sanitized.baseUrl = profile.baseUrl;
|
|
272
|
+
if (profile.primaryModel)
|
|
273
|
+
sanitized.primaryModel = profile.primaryModel;
|
|
274
|
+
if (profile.defaultHaikuModel)
|
|
275
|
+
sanitized.defaultHaikuModel = profile.defaultHaikuModel;
|
|
276
|
+
if (profile.defaultSonnetModel)
|
|
277
|
+
sanitized.defaultSonnetModel = profile.defaultSonnetModel;
|
|
278
|
+
if (profile.defaultOpusModel)
|
|
279
|
+
sanitized.defaultOpusModel = profile.defaultOpusModel;
|
|
280
|
+
return sanitized;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Backup configuration
|
|
284
|
+
*/
|
|
285
|
+
static backupConfig() {
|
|
286
|
+
try {
|
|
287
|
+
if (!exists(this.CONFIG_FILE)) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const timestamp = dayjs().format("YYYY-MM-DD_HH-mm-ss");
|
|
291
|
+
const backupPath = join(ZCF_CONFIG_DIR, `config.backup.${timestamp}.toml`);
|
|
292
|
+
copyFile(this.CONFIG_FILE, backupPath);
|
|
293
|
+
return backupPath;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error("Failed to backup Claude Code config:", error);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Add configuration
|
|
301
|
+
*/
|
|
302
|
+
static async addProfile(profile) {
|
|
303
|
+
try {
|
|
304
|
+
const validationErrors = this.validateProfile(profile);
|
|
305
|
+
if (validationErrors.length > 0) {
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
error: `Validation failed: ${validationErrors.join(", ")}`
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const backupPath = this.backupConfig();
|
|
312
|
+
let config = this.readConfig();
|
|
313
|
+
if (!config) {
|
|
314
|
+
config = this.createEmptyConfig();
|
|
315
|
+
}
|
|
316
|
+
if (profile.id && config.profiles[profile.id]) {
|
|
317
|
+
return {
|
|
318
|
+
success: false,
|
|
319
|
+
error: `Profile with ID "${profile.id}" already exists`,
|
|
320
|
+
backupPath: backupPath || void 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const normalizedName = profile.name.trim();
|
|
324
|
+
const profileKey = this.generateProfileId(normalizedName);
|
|
325
|
+
const existingNames = Object.values(config.profiles).map((p) => p.name || "");
|
|
326
|
+
if (config.profiles[profileKey] || existingNames.some((name) => name.toLowerCase() === normalizedName.toLowerCase())) {
|
|
327
|
+
return {
|
|
328
|
+
success: false,
|
|
329
|
+
error: `Profile with name "${profile.name}" already exists`,
|
|
330
|
+
backupPath: backupPath || void 0
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const sanitizedProfile = this.sanitizeProfile({
|
|
334
|
+
...profile,
|
|
335
|
+
name: normalizedName
|
|
336
|
+
});
|
|
337
|
+
const runtimeProfile = {
|
|
338
|
+
...sanitizedProfile,
|
|
339
|
+
id: profileKey
|
|
340
|
+
};
|
|
341
|
+
config.profiles[profileKey] = runtimeProfile;
|
|
342
|
+
if (!config.currentProfileId) {
|
|
343
|
+
config.currentProfileId = profileKey;
|
|
344
|
+
}
|
|
345
|
+
this.writeConfig(config);
|
|
346
|
+
return {
|
|
347
|
+
success: true,
|
|
348
|
+
backupPath: backupPath || void 0,
|
|
349
|
+
addedProfile: runtimeProfile
|
|
350
|
+
};
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
error: error instanceof Error ? error.message : String(error)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Update configuration
|
|
360
|
+
*/
|
|
361
|
+
static async updateProfile(id, data) {
|
|
362
|
+
try {
|
|
363
|
+
const validationErrors = this.validateProfile(data, true);
|
|
364
|
+
if (validationErrors.length > 0) {
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
error: `Validation failed: ${validationErrors.join(", ")}`
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const backupPath = this.backupConfig();
|
|
371
|
+
const config = this.readConfig();
|
|
372
|
+
if (!config || !config.profiles[id]) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
error: `Profile with ID "${id}" not found`,
|
|
376
|
+
backupPath: backupPath || void 0
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const existingProfile = config.profiles[id];
|
|
380
|
+
const nextName = data.name !== void 0 ? data.name.trim() : existingProfile.name;
|
|
381
|
+
const nextKey = this.generateProfileId(nextName);
|
|
382
|
+
const nameChanged = nextKey !== id;
|
|
383
|
+
if (nameChanged) {
|
|
384
|
+
const duplicateName = Object.entries(config.profiles).some(([key, profile]) => key !== id && (profile.name || "").toLowerCase() === nextName.toLowerCase());
|
|
385
|
+
if (duplicateName || config.profiles[nextKey]) {
|
|
386
|
+
return {
|
|
387
|
+
success: false,
|
|
388
|
+
error: `Profile with name "${data.name}" already exists`,
|
|
389
|
+
backupPath: backupPath || void 0
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const mergedProfile = this.sanitizeProfile({
|
|
394
|
+
...existingProfile,
|
|
395
|
+
...data,
|
|
396
|
+
name: nextName
|
|
397
|
+
});
|
|
398
|
+
if (nameChanged) {
|
|
399
|
+
delete config.profiles[id];
|
|
400
|
+
config.profiles[nextKey] = {
|
|
401
|
+
...mergedProfile,
|
|
402
|
+
id: nextKey
|
|
403
|
+
};
|
|
404
|
+
if (config.currentProfileId === id) {
|
|
405
|
+
config.currentProfileId = nextKey;
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
config.profiles[id] = {
|
|
409
|
+
...mergedProfile,
|
|
410
|
+
id
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
this.writeConfig(config);
|
|
414
|
+
return {
|
|
415
|
+
success: true,
|
|
416
|
+
backupPath: backupPath || void 0,
|
|
417
|
+
updatedProfile: {
|
|
418
|
+
...mergedProfile,
|
|
419
|
+
id: nameChanged ? nextKey : id
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
error: error instanceof Error ? error.message : String(error)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Delete configuration
|
|
431
|
+
*/
|
|
432
|
+
static async deleteProfile(id) {
|
|
433
|
+
try {
|
|
434
|
+
const backupPath = this.backupConfig();
|
|
435
|
+
const config = this.readConfig();
|
|
436
|
+
if (!config || !config.profiles[id]) {
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
error: `Profile with ID "${id}" not found`,
|
|
440
|
+
backupPath: backupPath || void 0
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const profileCount = Object.keys(config.profiles).length;
|
|
444
|
+
if (profileCount === 1) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: "Cannot delete the last profile. At least one profile must remain.",
|
|
448
|
+
backupPath: backupPath || void 0
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
delete config.profiles[id];
|
|
452
|
+
if (config.currentProfileId === id) {
|
|
453
|
+
const remainingIds = Object.keys(config.profiles);
|
|
454
|
+
config.currentProfileId = remainingIds[0];
|
|
455
|
+
}
|
|
456
|
+
this.writeConfig(config);
|
|
457
|
+
return {
|
|
458
|
+
success: true,
|
|
459
|
+
backupPath: backupPath || void 0,
|
|
460
|
+
remainingProfiles: Object.entries(config.profiles).map(([key, profile]) => ({
|
|
461
|
+
...profile,
|
|
462
|
+
id: key
|
|
463
|
+
}))
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
return {
|
|
467
|
+
success: false,
|
|
468
|
+
error: error instanceof Error ? error.message : String(error)
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Delete multiple configurations
|
|
474
|
+
*/
|
|
475
|
+
static async deleteProfiles(ids) {
|
|
476
|
+
try {
|
|
477
|
+
const backupPath = this.backupConfig();
|
|
478
|
+
const config = this.readConfig();
|
|
479
|
+
if (!config) {
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: "No configuration found",
|
|
483
|
+
backupPath: backupPath || void 0
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
const missingIds = ids.filter((id) => !config.profiles[id]);
|
|
487
|
+
if (missingIds.length > 0) {
|
|
488
|
+
return {
|
|
489
|
+
success: false,
|
|
490
|
+
error: `Profiles not found: ${missingIds.join(", ")}`,
|
|
491
|
+
backupPath: backupPath || void 0
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const remainingCount = Object.keys(config.profiles).length - ids.length;
|
|
495
|
+
if (remainingCount === 0) {
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
error: "Cannot delete all profiles. At least one profile must remain.",
|
|
499
|
+
backupPath: backupPath || void 0
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
let newCurrentProfileId;
|
|
503
|
+
ids.forEach((id) => {
|
|
504
|
+
delete config.profiles[id];
|
|
505
|
+
});
|
|
506
|
+
if (ids.includes(config.currentProfileId)) {
|
|
507
|
+
const remainingIds = Object.keys(config.profiles);
|
|
508
|
+
config.currentProfileId = remainingIds[0];
|
|
509
|
+
newCurrentProfileId = config.currentProfileId;
|
|
510
|
+
}
|
|
511
|
+
this.writeConfig(config);
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
backupPath: backupPath || void 0,
|
|
515
|
+
newCurrentProfileId,
|
|
516
|
+
deletedProfiles: ids,
|
|
517
|
+
remainingProfiles: Object.entries(config.profiles).map(([key, profile]) => ({
|
|
518
|
+
...profile,
|
|
519
|
+
id: key
|
|
520
|
+
}))
|
|
521
|
+
};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
error: error instanceof Error ? error.message : String(error)
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Generate profile ID from name
|
|
531
|
+
*/
|
|
532
|
+
static generateProfileId(name) {
|
|
533
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "profile";
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Switch configuration
|
|
537
|
+
*/
|
|
538
|
+
static async switchProfile(id) {
|
|
539
|
+
try {
|
|
540
|
+
const config = this.readConfig();
|
|
541
|
+
if (!config || !config.profiles[id]) {
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
error: "Profile not found"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (config.currentProfileId === id) {
|
|
548
|
+
return { success: true };
|
|
549
|
+
}
|
|
550
|
+
config.currentProfileId = id;
|
|
551
|
+
this.writeConfig(config);
|
|
552
|
+
return { success: true };
|
|
553
|
+
} catch (error) {
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
error: error instanceof Error ? error.message : String(error)
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* List all configurations
|
|
562
|
+
*/
|
|
563
|
+
static listProfiles() {
|
|
564
|
+
const config = this.readConfig();
|
|
565
|
+
if (!config) {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
return Object.values(config.profiles);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get current configuration
|
|
572
|
+
*/
|
|
573
|
+
static getCurrentProfile() {
|
|
574
|
+
const config = this.readConfig();
|
|
575
|
+
if (!config || !config.currentProfileId) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return config.profiles[config.currentProfileId] || null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Get configuration by ID
|
|
582
|
+
*/
|
|
583
|
+
static getProfileById(id) {
|
|
584
|
+
const config = this.readConfig();
|
|
585
|
+
if (!config) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return config.profiles[id] || null;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get configuration by name
|
|
592
|
+
*/
|
|
593
|
+
static getProfileByName(name) {
|
|
594
|
+
const config = this.readConfig();
|
|
595
|
+
if (!config) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return Object.values(config.profiles).find((p) => p.name === name) || null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Sync CCR configuration
|
|
602
|
+
*/
|
|
603
|
+
static async syncCcrProfile() {
|
|
604
|
+
try {
|
|
605
|
+
const { readCcrConfig } = await import('./simple-config.mjs').then(function (n) { return n.d8; });
|
|
606
|
+
const ccrConfig = readCcrConfig();
|
|
607
|
+
if (!ccrConfig) {
|
|
608
|
+
await this.ensureCcrProfileExists(ccrConfig);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
await this.ensureCcrProfileExists(ccrConfig);
|
|
612
|
+
} catch (error) {
|
|
613
|
+
console.error("Failed to sync CCR profile:", error);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* 确保CCR配置文件存在
|
|
618
|
+
*/
|
|
619
|
+
static async ensureCcrProfileExists(ccrConfig) {
|
|
620
|
+
const config = this.readConfig() || this.createEmptyConfig();
|
|
621
|
+
const ccrProfileId = "ccr-proxy";
|
|
622
|
+
const existingCcrProfile = config.profiles[ccrProfileId];
|
|
623
|
+
if (!ccrConfig) {
|
|
624
|
+
if (existingCcrProfile) {
|
|
625
|
+
delete config.profiles[ccrProfileId];
|
|
626
|
+
if (config.currentProfileId === ccrProfileId) {
|
|
627
|
+
const remainingIds = Object.keys(config.profiles);
|
|
628
|
+
config.currentProfileId = remainingIds[0] || "";
|
|
629
|
+
}
|
|
630
|
+
this.writeConfig(config);
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const host = ccrConfig.HOST || "127.0.0.1";
|
|
635
|
+
const port = ccrConfig.PORT || 3456;
|
|
636
|
+
const apiKey = ccrConfig.APIKEY || "sk-ccjk-x-ccr";
|
|
637
|
+
const baseUrl = `http://${host}:${port}`;
|
|
638
|
+
const ccrProfile = {
|
|
639
|
+
name: "CCR Proxy",
|
|
640
|
+
authType: "ccr_proxy",
|
|
641
|
+
baseUrl,
|
|
642
|
+
apiKey
|
|
643
|
+
};
|
|
644
|
+
config.profiles[ccrProfileId] = {
|
|
645
|
+
...ccrProfile,
|
|
646
|
+
id: ccrProfileId
|
|
647
|
+
};
|
|
648
|
+
if (!config.currentProfileId) {
|
|
649
|
+
config.currentProfileId = ccrProfileId;
|
|
650
|
+
}
|
|
651
|
+
this.writeConfig(config);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Switch to official login
|
|
655
|
+
*/
|
|
656
|
+
static async switchToOfficial() {
|
|
657
|
+
try {
|
|
658
|
+
const config = this.readConfig();
|
|
659
|
+
if (!config) {
|
|
660
|
+
return { success: true };
|
|
661
|
+
}
|
|
662
|
+
config.currentProfileId = "";
|
|
663
|
+
this.writeConfig(config);
|
|
664
|
+
return { success: true };
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return {
|
|
667
|
+
success: false,
|
|
668
|
+
error: error instanceof Error ? error.message : String(error)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Switch to CCR proxy
|
|
674
|
+
*/
|
|
675
|
+
static async switchToCcr() {
|
|
676
|
+
try {
|
|
677
|
+
await this.syncCcrProfile();
|
|
678
|
+
const config = this.readConfig();
|
|
679
|
+
if (!config || !config.profiles["ccr-proxy"]) {
|
|
680
|
+
return {
|
|
681
|
+
success: false,
|
|
682
|
+
error: "CCR proxy configuration not found. Please configure CCR first."
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return await this.switchProfile("ccr-proxy");
|
|
686
|
+
} catch (error) {
|
|
687
|
+
return {
|
|
688
|
+
success: false,
|
|
689
|
+
error: error instanceof Error ? error.message : String(error)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Validate configuration
|
|
695
|
+
*/
|
|
696
|
+
static validateProfile(profile, isUpdate = false) {
|
|
697
|
+
const errors = [];
|
|
698
|
+
if (!isUpdate && (!profile.name || typeof profile.name !== "string" || profile.name.trim() === "")) {
|
|
699
|
+
errors.push("Profile name is required");
|
|
700
|
+
}
|
|
701
|
+
if (profile.name && typeof profile.name !== "string") {
|
|
702
|
+
errors.push("Profile name must be a string");
|
|
703
|
+
}
|
|
704
|
+
if (profile.authType && !["api_key", "auth_token", "ccr_proxy"].includes(profile.authType)) {
|
|
705
|
+
errors.push("Invalid auth type. Must be one of: api_key, auth_token, ccr_proxy");
|
|
706
|
+
}
|
|
707
|
+
if (profile.authType === "api_key" || profile.authType === "auth_token") {
|
|
708
|
+
if (!profile.apiKey || typeof profile.apiKey !== "string" || profile.apiKey.trim() === "") {
|
|
709
|
+
errors.push("API key is required for api_key and auth_token types");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (profile.baseUrl) {
|
|
713
|
+
try {
|
|
714
|
+
new URL(profile.baseUrl);
|
|
715
|
+
} catch {
|
|
716
|
+
errors.push("Invalid base URL format");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return errors;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* 检查是否为最后一个配置
|
|
723
|
+
*/
|
|
724
|
+
static isLastProfile(id) {
|
|
725
|
+
const config = this.readConfig();
|
|
726
|
+
if (!config || !config.profiles[id]) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
return Object.keys(config.profiles).length === 1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export { ClaudeCodeConfigManager };
|