ai-account-switch 1.9.0 → 1.12.0

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,86 @@
1
+ /**
2
+ * Constants for AI Account Switch (AIS)
3
+ * Centralized constants definition for the entire application
4
+ */
5
+
6
+ // Wire API modes for Codex accounts
7
+ const WIRE_API_MODES = {
8
+ CHAT: 'chat',
9
+ RESPONSES: 'responses',
10
+ ENV: 'env'
11
+ };
12
+
13
+ const DEFAULT_WIRE_API = WIRE_API_MODES.CHAT;
14
+
15
+ // Account types
16
+ const ACCOUNT_TYPES = {
17
+ CLAUDE: 'Claude',
18
+ CODEX: 'Codex',
19
+ CCR: 'CCR',
20
+ DROIDS: 'Droids'
21
+ };
22
+
23
+ // Account type values as array (for iteration)
24
+ const ACCOUNT_TYPE_VALUES = ['Claude', 'Codex', 'CCR', 'Droids', 'Other'];
25
+
26
+ // MCP server scopes
27
+ const MCP_SCOPES = {
28
+ LOCAL: 'local', // Only available in current project
29
+ PROJECT: 'project', // Shared with project members via .mcp.json
30
+ USER: 'user' // Available to all projects for current user (global)
31
+ };
32
+
33
+ const DEFAULT_MCP_SCOPE = MCP_SCOPES.LOCAL;
34
+
35
+ // Model-related environment variable keys
36
+ const MODEL_KEYS = [
37
+ 'DEFAULT_MODEL',
38
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
39
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
40
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
41
+ 'CLAUDE_CODE_SUBAGENT_MODEL',
42
+ 'ANTHROPIC_MODEL'
43
+ ];
44
+
45
+ // Configuration file names
46
+ const CONFIG_FILES = {
47
+ GLOBAL_DIR: '.ai-account-switch',
48
+ GLOBAL_CONFIG: 'config.json',
49
+ PROJECT_CONFIG: '.ais-project-config',
50
+ CLAUDE_DIR: '.claude',
51
+ CLAUDE_LOCAL_CONFIG: 'settings.local.json',
52
+ CLAUDE_USER_CONFIG: 'settings.json',
53
+ CODEX_DIR: '.codex',
54
+ CODEX_CONFIG: 'config.toml',
55
+ CODEX_AUTH: 'auth.json',
56
+ CODEX_PROFILE: '.codex-profile',
57
+ CCR_DIR: '.claude-code-router',
58
+ CCR_CONFIG: 'config.json',
59
+ DROIDS_DIR: '.droids',
60
+ DROIDS_CONFIG: 'config.json',
61
+ MCP_CONFIG: '.mcp.json'
62
+ };
63
+
64
+ // Default CCR port
65
+ const DEFAULT_CCR_PORT = 3456;
66
+
67
+ // Gitignore entries for AIS
68
+ const GITIGNORE_ENTRIES = [
69
+ CONFIG_FILES.PROJECT_CONFIG,
70
+ `${CONFIG_FILES.CLAUDE_DIR}/${CONFIG_FILES.CLAUDE_LOCAL_CONFIG}`,
71
+ CONFIG_FILES.CODEX_PROFILE,
72
+ `${CONFIG_FILES.DROIDS_DIR}/${CONFIG_FILES.DROIDS_CONFIG}`
73
+ ];
74
+
75
+ module.exports = {
76
+ WIRE_API_MODES,
77
+ DEFAULT_WIRE_API,
78
+ ACCOUNT_TYPES,
79
+ ACCOUNT_TYPE_VALUES,
80
+ MCP_SCOPES,
81
+ DEFAULT_MCP_SCOPE,
82
+ MODEL_KEYS,
83
+ CONFIG_FILES,
84
+ DEFAULT_CCR_PORT,
85
+ GITIGNORE_ENTRIES
86
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Base Generator Class
3
+ * Provides common functionality for all configuration generators
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { CONFIG_FILES, DEFAULT_CCR_PORT } = require('../constants');
8
+
9
+ class BaseGenerator {
10
+ constructor(projectRoot) {
11
+ this.projectRoot = projectRoot;
12
+ }
13
+
14
+ /**
15
+ * Ensure a directory exists, create if not
16
+ */
17
+ ensureDir(dir) {
18
+ if (!fs.existsSync(dir)) {
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Read and parse a JSON file
25
+ */
26
+ readJsonFile(filePath, defaultValue = {}) {
27
+ if (fs.existsSync(filePath)) {
28
+ try {
29
+ const data = fs.readFileSync(filePath, 'utf8');
30
+ return JSON.parse(data);
31
+ } catch (error) {
32
+ return defaultValue;
33
+ }
34
+ }
35
+ return defaultValue;
36
+ }
37
+
38
+ /**
39
+ * Write data to a JSON file
40
+ */
41
+ writeJsonFile(filePath, data) {
42
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
43
+ }
44
+
45
+ /**
46
+ * Read auth.json file
47
+ */
48
+ readAuthJson(authJsonFile) {
49
+ if (!fs.existsSync(authJsonFile)) {
50
+ return {};
51
+ }
52
+
53
+ try {
54
+ const content = fs.readFileSync(authJsonFile, 'utf8');
55
+ return JSON.parse(content);
56
+ } catch (parseError) {
57
+ const chalk = require('chalk');
58
+ console.warn(
59
+ chalk.yellow(
60
+ `⚠ Warning: Could not parse existing auth.json, will create new file (警告: 无法解析现有 auth.json,将创建新文件)`
61
+ )
62
+ );
63
+ return {};
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Write auth.json file atomically with proper permissions
69
+ */
70
+ writeAuthJson(authJsonFile, authData) {
71
+ const tempFile = `${authJsonFile}.tmp.${process.pid}`;
72
+
73
+ try {
74
+ // Write to temporary file first (atomic operation)
75
+ fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), 'utf8');
76
+
77
+ // Set file permissions to 600 (owner read/write only) for security
78
+ if (process.platform !== 'win32') {
79
+ fs.chmodSync(tempFile, 0o600);
80
+ }
81
+
82
+ // Atomically rename temp file to actual file
83
+ fs.renameSync(tempFile, authJsonFile);
84
+ } catch (error) {
85
+ // Clean up temp file if it exists
86
+ if (fs.existsSync(tempFile)) {
87
+ try {
88
+ fs.unlinkSync(tempFile);
89
+ } catch (cleanupError) {
90
+ // Ignore cleanup errors
91
+ }
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get CCR port from config file
99
+ */
100
+ getCcrPort() {
101
+ const ccrConfigFile = path.join(require('os').homedir(), CONFIG_FILES.CCR_DIR, CONFIG_FILES.CCR_CONFIG);
102
+ let port = DEFAULT_CCR_PORT;
103
+ if (fs.existsSync(ccrConfigFile)) {
104
+ try {
105
+ const ccrConfig = JSON.parse(fs.readFileSync(ccrConfigFile, 'utf8'));
106
+ if (ccrConfig.PORT) {
107
+ port = ccrConfig.PORT;
108
+ }
109
+ } catch (e) {
110
+ // Use default port if reading fails
111
+ }
112
+ }
113
+ return port;
114
+ }
115
+
116
+ /**
117
+ * Generate configuration - to be implemented by subclasses
118
+ */
119
+ generate(account, options = {}) {
120
+ throw new Error('generate() must be implemented by subclass');
121
+ }
122
+ }
123
+
124
+ module.exports = BaseGenerator;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * CCR Configuration Generator
3
+ * Generates ~/.claude-code-router/config.json and Claude config for CCR
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const BaseGenerator = require('./base-generator');
9
+ const ClaudeGenerator = require('./claude-generator');
10
+ const { CONFIG_FILES } = require('../constants');
11
+
12
+ class CCRGenerator extends BaseGenerator {
13
+ constructor(projectRoot) {
14
+ super(projectRoot);
15
+ this.ccrConfigDir = path.join(os.homedir(), CONFIG_FILES.CCR_DIR);
16
+ this.ccrConfigFile = path.join(this.ccrConfigDir, CONFIG_FILES.CCR_CONFIG);
17
+ this.claudeGenerator = new ClaudeGenerator(projectRoot);
18
+ }
19
+
20
+ /**
21
+ * Generate CCR configuration in ~/.claude-code-router/config.json
22
+ */
23
+ generateCCRConfig(account) {
24
+ // Read existing config
25
+ let config = {};
26
+ if (fs.existsSync(this.ccrConfigFile)) {
27
+ const data = fs.readFileSync(this.ccrConfigFile, 'utf8');
28
+ config = JSON.parse(data);
29
+ }
30
+
31
+ if (!account.ccrConfig) return;
32
+
33
+ const { providerName, models, defaultModel, backgroundModel, thinkModel } = account.ccrConfig;
34
+
35
+ // Check if provider exists
36
+ const providerIndex = config.Providers?.findIndex(p => p.name === providerName);
37
+
38
+ const provider = {
39
+ api_base_url: account.apiUrl || '',
40
+ api_key: account.apiKey,
41
+ models: models,
42
+ name: providerName
43
+ };
44
+
45
+ if (providerIndex >= 0) {
46
+ config.Providers[providerIndex] = provider;
47
+ } else {
48
+ if (!config.Providers) config.Providers = [];
49
+ config.Providers.push(provider);
50
+ }
51
+
52
+ // Update Router configuration
53
+ if (!config.Router) config.Router = {};
54
+ config.Router.default = `${providerName},${defaultModel}`;
55
+ config.Router.background = `${providerName},${backgroundModel}`;
56
+ config.Router.think = `${providerName},${thinkModel}`;
57
+
58
+ fs.writeFileSync(this.ccrConfigFile, JSON.stringify(config, null, 2), 'utf8');
59
+ }
60
+
61
+ /**
62
+ * Generate Claude configuration for CCR type accounts
63
+ */
64
+ generateClaudeConfigForCCR(account) {
65
+ const port = this.getCcrPort();
66
+
67
+ // Create .claude directory if it doesn't exist
68
+ const claudeDir = path.join(this.projectRoot, CONFIG_FILES.CLAUDE_DIR);
69
+ const claudeConfigFile = path.join(claudeDir, CONFIG_FILES.CLAUDE_LOCAL_CONFIG);
70
+ this.ensureDir(claudeDir);
71
+
72
+ // Read existing config if it exists
73
+ const existingConfig = this.readJsonFile(claudeConfigFile, {});
74
+
75
+ const claudeConfig = {
76
+ ...existingConfig,
77
+ env: {
78
+ ...(existingConfig.env || {}),
79
+ ANTHROPIC_AUTH_TOKEN: account.apiKey,
80
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`
81
+ }
82
+ };
83
+
84
+ // Add custom environment variables if specified
85
+ if (account.customEnv && typeof account.customEnv === 'object') {
86
+ Object.keys(account.customEnv).forEach(key => {
87
+ claudeConfig.env[key] = account.customEnv[key];
88
+ });
89
+ }
90
+
91
+ // Preserve existing permissions if any
92
+ if (!claudeConfig.permissions) {
93
+ claudeConfig.permissions = existingConfig.permissions || {
94
+ allow: [],
95
+ deny: [],
96
+ ask: []
97
+ };
98
+ }
99
+
100
+ // Write Claude configuration
101
+ this.writeJsonFile(claudeConfigFile, claudeConfig);
102
+ }
103
+
104
+ /**
105
+ * Generate both CCR and Claude configurations
106
+ */
107
+ generate(account, options = {}) {
108
+ this.generateCCRConfig(account);
109
+ this.generateClaudeConfigForCCR(account);
110
+ }
111
+ }
112
+
113
+ module.exports = CCRGenerator;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Claude Configuration Generator
3
+ * Generates .claude/settings.local.json for Claude Code
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const BaseGenerator = require('./base-generator');
8
+ const { MODEL_KEYS, CONFIG_FILES } = require('../constants');
9
+
10
+ class ClaudeGenerator extends BaseGenerator {
11
+ constructor(projectRoot) {
12
+ super(projectRoot);
13
+ this.claudeDir = path.join(this.projectRoot, CONFIG_FILES.CLAUDE_DIR);
14
+ this.claudeConfigFile = path.join(this.claudeDir, CONFIG_FILES.CLAUDE_LOCAL_CONFIG);
15
+ }
16
+
17
+ /**
18
+ * Generate Claude Code .claude/settings.local.json configuration
19
+ */
20
+ generate(account, options = {}) {
21
+ this.ensureDir(this.claudeDir);
22
+
23
+ // Read existing config if it exists
24
+ const existingConfig = this.readJsonFile(this.claudeConfigFile, {});
25
+
26
+ // Build Claude configuration - preserve existing env but clear model configs
27
+ const existingEnv = existingConfig.env || {};
28
+ const cleanedEnv = {};
29
+
30
+ // Copy all existing env vars except model-related ones
31
+ Object.keys(existingEnv).forEach(key => {
32
+ if (!MODEL_KEYS.includes(key)) {
33
+ cleanedEnv[key] = existingEnv[key];
34
+ }
35
+ });
36
+
37
+ const claudeConfig = {
38
+ ...existingConfig,
39
+ env: {
40
+ ...cleanedEnv,
41
+ ANTHROPIC_AUTH_TOKEN: account.apiKey
42
+ }
43
+ };
44
+
45
+ // Add API URL if specified
46
+ if (account.apiUrl) {
47
+ claudeConfig.env.ANTHROPIC_BASE_URL = account.apiUrl;
48
+ }
49
+
50
+ // Add custom environment variables if specified
51
+ if (account.customEnv && typeof account.customEnv === 'object') {
52
+ Object.keys(account.customEnv).forEach(key => {
53
+ claudeConfig.env[key] = account.customEnv[key];
54
+ });
55
+ }
56
+
57
+ // Add model configuration from active model group
58
+ this._applyModelConfig(claudeConfig, account);
59
+
60
+ // Preserve existing permissions if any
61
+ if (!claudeConfig.permissions) {
62
+ claudeConfig.permissions = existingConfig.permissions || {
63
+ allow: [],
64
+ deny: [],
65
+ ask: []
66
+ };
67
+ }
68
+
69
+ // Add MCP servers if provided
70
+ if (options.mcpServers) {
71
+ claudeConfig.mcpServers = options.mcpServers;
72
+ }
73
+
74
+ // Write Claude configuration
75
+ this.writeJsonFile(this.claudeConfigFile, claudeConfig);
76
+ }
77
+
78
+ /**
79
+ * Apply model configuration from account
80
+ * @private
81
+ */
82
+ _applyModelConfig(claudeConfig, account) {
83
+ // Add model configuration from active model group
84
+ if (account.modelGroups && account.activeModelGroup) {
85
+ const activeGroup = account.modelGroups[account.activeModelGroup];
86
+
87
+ if (activeGroup && typeof activeGroup === 'object') {
88
+ const defaultModel = activeGroup.DEFAULT_MODEL;
89
+
90
+ // Set DEFAULT_MODEL if specified
91
+ if (defaultModel) {
92
+ claudeConfig.env.DEFAULT_MODEL = defaultModel;
93
+ }
94
+
95
+ // Set other model configs, using DEFAULT_MODEL as fallback if they're not specified
96
+ MODEL_KEYS.slice(1).forEach(key => {
97
+ if (activeGroup[key]) {
98
+ claudeConfig.env[key] = activeGroup[key];
99
+ } else if (defaultModel) {
100
+ claudeConfig.env[key] = defaultModel;
101
+ }
102
+ });
103
+ }
104
+ }
105
+ // Backward compatibility: support old modelConfig structure
106
+ else if (account.modelConfig && typeof account.modelConfig === 'object') {
107
+ const defaultModel = account.modelConfig.DEFAULT_MODEL;
108
+
109
+ if (defaultModel) {
110
+ claudeConfig.env.DEFAULT_MODEL = defaultModel;
111
+ }
112
+
113
+ MODEL_KEYS.slice(1).forEach(key => {
114
+ if (account.modelConfig[key]) {
115
+ claudeConfig.env[key] = account.modelConfig[key];
116
+ } else if (defaultModel) {
117
+ claudeConfig.env[key] = defaultModel;
118
+ }
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ module.exports = ClaudeGenerator;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Codex Configuration Generator
3
+ * Generates ~/.codex/config.toml and handles auth.json
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const chalk = require('chalk');
9
+ const BaseGenerator = require('./base-generator');
10
+ const { CONFIG_FILES } = require('../constants');
11
+
12
+ class CodexGenerator extends BaseGenerator {
13
+ constructor(projectRoot) {
14
+ super(projectRoot);
15
+ this.codexConfigDir = path.join(os.homedir(), CONFIG_FILES.CODEX_DIR);
16
+ this.codexConfigFile = path.join(this.codexConfigDir, CONFIG_FILES.CODEX_CONFIG);
17
+ this.authJsonFile = path.join(this.codexConfigDir, CONFIG_FILES.CODEX_AUTH);
18
+ }
19
+
20
+ /**
21
+ * Generate Codex profile in global ~/.codex/config.toml
22
+ */
23
+ generate(account, options = {}) {
24
+ const { WIRE_API_MODES, DEFAULT_WIRE_API } = require('../constants');
25
+
26
+ // Create .codex directory if it doesn't exist
27
+ this.ensureDir(this.codexConfigDir);
28
+
29
+ // Read existing config if it exists
30
+ let existingConfig = '';
31
+ if (fs.existsSync(this.codexConfigFile)) {
32
+ existingConfig = fs.readFileSync(this.codexConfigFile, 'utf8');
33
+ }
34
+
35
+ // Generate profile name based on project path
36
+ const projectName = path.basename(this.projectRoot);
37
+ const profileName = `ais_${projectName}`;
38
+
39
+ // Build profile configuration
40
+ let profileConfig = `\n# AIS Profile for project: ${this.projectRoot}\n`;
41
+ profileConfig += `[profiles.${profileName}]\n`;
42
+
43
+ // For Codex type accounts, use custom provider configuration
44
+ const providerName = `ais_${account.name || 'provider'}`;
45
+ const escapedProviderName = providerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
46
+
47
+ profileConfig += `model_provider = "${providerName}"\n`;
48
+
49
+ // Add model configuration
50
+ if (account.model) {
51
+ profileConfig += `model = "${account.model}"\n`;
52
+ }
53
+
54
+ // Smart /v1 path handling
55
+ let baseUrl = account.apiUrl || '';
56
+ if (baseUrl) {
57
+ baseUrl = baseUrl.replace(/\/+$/, '');
58
+ const isDomainOnly = baseUrl.match(/^https?:\/\/[^\/]+$/);
59
+ if (isDomainOnly) {
60
+ baseUrl += '/v1';
61
+ }
62
+ }
63
+
64
+ // Remove existing provider if it exists
65
+ const providerPattern = new RegExp(`\\[model_providers\\.${escapedProviderName}\\][\\s\\S]*?(?=\\n\\[|$)`, 'g');
66
+ existingConfig = existingConfig.replace(providerPattern, '');
67
+
68
+ // Add new provider details
69
+ profileConfig += `\n[model_providers.${providerName}]\n`;
70
+ profileConfig += `name = "${providerName}"\n`;
71
+
72
+ if (baseUrl) {
73
+ profileConfig += `base_url = "${baseUrl}"\n`;
74
+ }
75
+
76
+ // Determine wire_api based on account configuration
77
+ const wireApi = account.wireApi || DEFAULT_WIRE_API;
78
+
79
+ if (wireApi === WIRE_API_MODES.CHAT) {
80
+ profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
81
+ profileConfig += `http_headers = { "Authorization" = "Bearer ${account.apiKey}" }\n`;
82
+ } else if (wireApi === WIRE_API_MODES.RESPONSES) {
83
+ profileConfig += `wire_api = "${WIRE_API_MODES.RESPONSES}"\n`;
84
+ profileConfig += `requires_openai_auth = true\n`;
85
+ this.updateCodexAuthJson(account.apiKey);
86
+ } else if (wireApi === WIRE_API_MODES.ENV) {
87
+ profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
88
+ const envKey = account.envKey || 'AIS_USER_API_KEY';
89
+ profileConfig += `env_key = "${envKey}"\n`;
90
+ this.clearCodexAuthJson();
91
+ }
92
+
93
+ // Remove all old profiles with the same name
94
+ existingConfig = this._removeProfileFromConfig(existingConfig, profileName);
95
+
96
+ // Append new profile
97
+ const newConfig = existingConfig.trimEnd() + '\n' + profileConfig;
98
+
99
+ // Write Codex configuration
100
+ fs.writeFileSync(this.codexConfigFile, newConfig, 'utf8');
101
+
102
+ // Create a helper script in project directory
103
+ const helperScript = path.join(this.projectRoot, '.codex-profile');
104
+ fs.writeFileSync(helperScript, profileName, 'utf8');
105
+
106
+ return { profileName };
107
+ }
108
+
109
+ /**
110
+ * Remove a profile from TOML config string
111
+ * @private
112
+ */
113
+ _removeProfileFromConfig(configContent, profileName) {
114
+ const lines = configContent.split('\n');
115
+ const cleanedLines = [];
116
+ let skipUntilNextSection = false;
117
+ const profileSectionHeader = `[profiles.${profileName}]`;
118
+
119
+ for (let i = 0; i < lines.length; i++) {
120
+ const line = lines[i];
121
+ const trimmedLine = line.trim();
122
+
123
+ if (trimmedLine === profileSectionHeader) {
124
+ skipUntilNextSection = true;
125
+
126
+ if (cleanedLines.length > 0) {
127
+ const lastLine = cleanedLines[cleanedLines.length - 1].trim();
128
+ if (lastLine.startsWith('# AIS Profile for project:')) {
129
+ cleanedLines.pop();
130
+ }
131
+ }
132
+
133
+ while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1].trim() === '') {
134
+ cleanedLines.pop();
135
+ }
136
+
137
+ continue;
138
+ }
139
+
140
+ if (skipUntilNextSection) {
141
+ if (trimmedLine.startsWith('[')) {
142
+ skipUntilNextSection = false;
143
+ } else {
144
+ continue;
145
+ }
146
+ }
147
+
148
+ cleanedLines.push(line);
149
+ }
150
+
151
+ let result = cleanedLines.join('\n');
152
+ result = result.replace(/\n{3,}/g, '\n\n');
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Clear OPENAI_API_KEY in ~/.codex/auth.json for chat mode
159
+ */
160
+ clearCodexAuthJson() {
161
+ try {
162
+ this.ensureDir(this.codexConfigDir);
163
+ const authData = this.readAuthJson(this.authJsonFile);
164
+ authData.OPENAI_API_KEY = "";
165
+ this.writeAuthJson(this.authJsonFile, authData);
166
+
167
+ console.log(
168
+ chalk.cyan(
169
+ `✓ Cleared OPENAI_API_KEY in auth.json (chat mode) (已清空 auth.json 中的 OPENAI_API_KEY)`
170
+ )
171
+ );
172
+ } catch (error) {
173
+ console.error(
174
+ chalk.yellow(
175
+ `⚠ Warning: Failed to clear auth.json: ${error.message} (警告: 清空 auth.json 失败)`
176
+ )
177
+ );
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Update ~/.codex/auth.json with API key for responses mode
183
+ */
184
+ updateCodexAuthJson(apiKey) {
185
+ try {
186
+ this.ensureDir(this.codexConfigDir);
187
+ const authData = this.readAuthJson(this.authJsonFile);
188
+ authData.OPENAI_API_KEY = apiKey;
189
+ this.writeAuthJson(this.authJsonFile, authData);
190
+
191
+ console.log(
192
+ chalk.green(
193
+ `✓ Updated auth.json at: ${this.authJsonFile} (已更新 auth.json)`
194
+ )
195
+ );
196
+ } catch (error) {
197
+ console.error(
198
+ chalk.red(
199
+ `✗ Failed to update auth.json: ${error.message} (更新 auth.json 失败)`
200
+ )
201
+ );
202
+ throw error;
203
+ }
204
+ }
205
+ }
206
+
207
+ module.exports = CodexGenerator;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Droids Configuration Generator
3
+ * Generates .droids/config.json
4
+ */
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const BaseGenerator = require('./base-generator');
8
+ const { CONFIG_FILES } = require('../constants');
9
+
10
+ class DroidsGenerator extends BaseGenerator {
11
+ constructor(projectRoot) {
12
+ super(projectRoot);
13
+ this.droidsDir = path.join(this.projectRoot, CONFIG_FILES.DROIDS_DIR);
14
+ this.droidsConfigFile = path.join(this.droidsDir, CONFIG_FILES.DROIDS_CONFIG);
15
+ }
16
+
17
+ /**
18
+ * Generate Droids configuration in .droids/config.json
19
+ */
20
+ generate(account, options = {}) {
21
+ // Create .droids directory if it doesn't exist
22
+ this.ensureDir(this.droidsDir);
23
+
24
+ // Build Droids configuration
25
+ const droidsConfig = {
26
+ apiKey: account.apiKey
27
+ };
28
+
29
+ // Add API URL if specified
30
+ if (account.apiUrl) {
31
+ droidsConfig.baseUrl = account.apiUrl;
32
+ }
33
+
34
+ // Add model configuration
35
+ if (account.model) {
36
+ droidsConfig.model = account.model;
37
+ }
38
+
39
+ // Add custom environment variables as customSettings
40
+ if (account.customEnv && typeof account.customEnv === 'object') {
41
+ droidsConfig.customSettings = account.customEnv;
42
+ }
43
+
44
+ // Write Droids configuration
45
+ this.writeJsonFile(this.droidsConfigFile, droidsConfig);
46
+ }
47
+ }
48
+
49
+ module.exports = DroidsGenerator;