@pikecode/api-key-manager 2.0.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.
package/src/config.js ADDED
@@ -0,0 +1,250 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const chalk = require('chalk');
5
+
6
+ class ConfigManager {
7
+ constructor() {
8
+ this.configPath = path.join(os.homedir(), '.akm-config.json');
9
+ this.config = null; // 延迟加载
10
+ this.isLoaded = false;
11
+ this.lastModified = null;
12
+ this.loadPromise = null; // 防止并发加载
13
+ }
14
+
15
+ getDefaultConfig() {
16
+ return {
17
+ version: '1.0.0',
18
+ currentProvider: null,
19
+ providers: {}
20
+ };
21
+ }
22
+
23
+ async load(forceReload = false) {
24
+ // 如果正在加载,等待当前加载完成
25
+ if (this.loadPromise) {
26
+ return await this.loadPromise;
27
+ }
28
+
29
+ // 如果已经加载且不是强制重载,直接返回缓存
30
+ if (this.isLoaded && !forceReload) {
31
+ // 检查文件是否被外部修改
32
+ const needsReload = await this.checkIfModified();
33
+ if (!needsReload) {
34
+ return this.config;
35
+ }
36
+ }
37
+
38
+ // 创建加载Promise
39
+ this.loadPromise = this._performLoad();
40
+ try {
41
+ const result = await this.loadPromise;
42
+ this.loadPromise = null;
43
+ return result;
44
+ } catch (error) {
45
+ this.loadPromise = null;
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ async _performLoad() {
51
+ try {
52
+ if (await fs.pathExists(this.configPath)) {
53
+ const data = await fs.readJSON(this.configPath);
54
+
55
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
56
+ // 配置文件被写成非对象内容时,重置为默认配置
57
+ this.config = this.getDefaultConfig();
58
+ await this._performSave();
59
+ } else {
60
+ this.config = { ...this.getDefaultConfig(), ...data };
61
+ }
62
+
63
+ // 迁移旧的认证模式
64
+ this._migrateAuthModes();
65
+
66
+ const stat = await fs.stat(this.configPath);
67
+ this.lastModified = stat.mtime;
68
+ } else {
69
+ this.config = this.getDefaultConfig();
70
+ await this._performSave();
71
+ }
72
+ this.isLoaded = true;
73
+ return this.config;
74
+ } catch (error) {
75
+ if (error.message.includes('Unexpected end of JSON input')) {
76
+ // 处理空文件或损坏的JSON文件
77
+ this.config = this.getDefaultConfig();
78
+ await this._performSave();
79
+ this.isLoaded = true;
80
+ return this.config;
81
+ }
82
+ console.error(chalk.red('❌ 加载配置失败:'), error.message);
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ _migrateAuthModes() {
88
+ // 迁移旧的 api_token 模式到新的 auth_token 模式
89
+ if (this.config.providers) {
90
+ Object.keys(this.config.providers).forEach(key => {
91
+ const provider = this.config.providers[key];
92
+ if (provider.authMode === 'api_token') {
93
+ provider.authMode = 'auth_token';
94
+ }
95
+ });
96
+ }
97
+ }
98
+
99
+ async checkIfModified() {
100
+ try {
101
+ if (!this.lastModified || !await fs.pathExists(this.configPath)) {
102
+ return true;
103
+ }
104
+ const stat = await fs.stat(this.configPath);
105
+ return stat.mtime > this.lastModified;
106
+ } catch (error) {
107
+ return true; // 出错时重新加载
108
+ }
109
+ }
110
+
111
+ async save(config = this.config) {
112
+ // 确保配置已加载
113
+ await this.ensureLoaded();
114
+ if (config) {
115
+ this.config = config;
116
+ }
117
+ return await this._performSave();
118
+ }
119
+
120
+ async _performSave() {
121
+ try {
122
+ await fs.writeJSON(this.configPath, this.config, { spaces: 2 });
123
+ // 更新最后修改时间
124
+ const stat = await fs.stat(this.configPath);
125
+ this.lastModified = stat.mtime;
126
+ return true;
127
+ } catch (error) {
128
+ console.error(chalk.red('❌ 保存配置失败:'), error.message);
129
+ throw error;
130
+ }
131
+ }
132
+
133
+ // 确保配置已加载的辅助方法
134
+ async ensureLoaded() {
135
+ if (!this.isLoaded) {
136
+ await this.load();
137
+ }
138
+ }
139
+
140
+ async addProvider(name, providerConfig) {
141
+ await this.ensureLoaded();
142
+
143
+ this.config.providers[name] = {
144
+ name,
145
+ displayName: providerConfig.displayName || name,
146
+ baseUrl: providerConfig.baseUrl,
147
+ authToken: providerConfig.authToken,
148
+ authMode: providerConfig.authMode || 'api_key',
149
+ tokenType: providerConfig.tokenType || 'api_key', // 'api_key' 或 'auth_token' - 仅在 authMode 为 'api_key' 时使用
150
+ launchArgs: providerConfig.launchArgs || [],
151
+ models: {
152
+ primary: providerConfig.primaryModel || null,
153
+ smallFast: providerConfig.smallFastModel || null
154
+ },
155
+ createdAt: new Date().toISOString(),
156
+ lastUsed: new Date().toISOString(),
157
+ current: false
158
+ };
159
+
160
+ // 如果是第一个供应商或设置为默认,则设为当前供应商
161
+ if (Object.keys(this.config.providers).length === 1 || providerConfig.setAsDefault) {
162
+ // 重置所有供应商的current状态
163
+ Object.keys(this.config.providers).forEach(key => {
164
+ this.config.providers[key].current = false;
165
+ });
166
+
167
+ // 设置新的当前供应商
168
+ this.config.providers[name].current = true;
169
+ this.config.providers[name].lastUsed = new Date().toISOString();
170
+ this.config.currentProvider = name;
171
+ }
172
+
173
+ return await this.save();
174
+ }
175
+
176
+ async removeProvider(name) {
177
+ await this.ensureLoaded();
178
+
179
+ if (!this.config.providers[name]) {
180
+ throw new Error(`供应商 '${name}' 不存在`);
181
+ }
182
+
183
+ delete this.config.providers[name];
184
+
185
+ // 如果删除的是当前供应商,清空当前供应商
186
+ if (this.config.currentProvider === name) {
187
+ this.config.currentProvider = null;
188
+ }
189
+
190
+ return await this.save();
191
+ }
192
+
193
+ async setCurrentProvider(name) {
194
+ await this.ensureLoaded();
195
+
196
+ if (!this.config.providers[name]) {
197
+ throw new Error(`供应商 '${name}' 不存在`);
198
+ }
199
+
200
+ // 重置所有供应商的current状态
201
+ Object.keys(this.config.providers).forEach(key => {
202
+ this.config.providers[key].current = false;
203
+ });
204
+
205
+ // 设置新的当前供应商
206
+ this.config.providers[name].current = true;
207
+ this.config.providers[name].lastUsed = new Date().toISOString();
208
+ this.config.currentProvider = name;
209
+
210
+ return await this.save();
211
+ }
212
+
213
+ getProvider(name) {
214
+ // 同步方法,但需要先确保配置已加载
215
+ if (!this.isLoaded) {
216
+ throw new Error('配置未加载,请先调用 load() 方法');
217
+ }
218
+ return this.config.providers[name];
219
+ }
220
+
221
+ listProviders() {
222
+ // 同步方法,但需要先确保配置已加载
223
+ if (!this.isLoaded) {
224
+ throw new Error('配置未加载,请先调用 load() 方法');
225
+ }
226
+ return Object.keys(this.config.providers).map(name => ({
227
+ name,
228
+ ...this.config.providers[name]
229
+ }));
230
+ }
231
+
232
+ getCurrentProvider() {
233
+ // 同步方法,但需要先确保配置已加载
234
+ if (!this.isLoaded) {
235
+ throw new Error('配置未加载,请先调用 load() 方法');
236
+ }
237
+ if (!this.config.currentProvider) {
238
+ return null;
239
+ }
240
+ return this.getProvider(this.config.currentProvider);
241
+ }
242
+
243
+ async reset() {
244
+ this.config = this.getDefaultConfig();
245
+ this.isLoaded = true;
246
+ return await this._performSave();
247
+ }
248
+ }
249
+
250
+ module.exports = { ConfigManager };
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ require('./utils/inquirer-setup');
2
+ const { switchCommand } = require('./commands/switch');
3
+ const { Logger } = require('./utils/logger');
4
+
5
+ async function main(providerName) {
6
+ try {
7
+ if (providerName) {
8
+ // 直接切换到指定供应商
9
+ await switchCommand(providerName);
10
+ } else {
11
+ // 显示供应商选择界面
12
+ await switchCommand();
13
+ }
14
+ } catch (error) {
15
+ Logger.fatal(`程序执行失败: ${error.message}`);
16
+ }
17
+ }
18
+
19
+ module.exports = { main };
@@ -0,0 +1,213 @@
1
+ const readline = require('readline');
2
+
3
+ class EscNavigationManager {
4
+ constructor(input = process.stdin, options = {}) {
5
+ this.input = input;
6
+ this.handlers = [];
7
+ this.pendingTimeout = null;
8
+ this.escapePending = false;
9
+ this.triggerDelay = typeof options.triggerDelay === 'number' ? options.triggerDelay : 100;
10
+ this.listenerBound = false;
11
+ this.rawModeEnabled = false;
12
+ this.previousRawMode = null;
13
+ this.postCallbackDelay = typeof options.postCallbackDelay === 'number' ? options.postCallbackDelay : 50;
14
+
15
+ this.supported = Boolean(
16
+ this.input &&
17
+ typeof this.input.on === 'function' &&
18
+ typeof this.input.removeListener === 'function' &&
19
+ typeof this.input.setRawMode === 'function'
20
+ );
21
+
22
+ this.handleData = this.handleData.bind(this);
23
+ }
24
+
25
+ isSupported() {
26
+ return this.supported;
27
+ }
28
+
29
+ register(options = {}) {
30
+ if (!this.isSupported()) {
31
+ return null;
32
+ }
33
+
34
+ const handler = {
35
+ id: Symbol('esc-handler'),
36
+ onTrigger: typeof options.onTrigger === 'function' ? options.onTrigger : null,
37
+ once: options.once !== false,
38
+ postDelay: typeof options.postDelay === 'number' ? options.postDelay : this.postCallbackDelay
39
+ };
40
+
41
+ this.handlers.push(handler);
42
+ this.ensureListening();
43
+ return handler;
44
+ }
45
+
46
+ unregister(handler) {
47
+ if (!this.isSupported() || !handler) {
48
+ return;
49
+ }
50
+
51
+ const index = this.handlers.indexOf(handler);
52
+ if (index === -1) {
53
+ return;
54
+ }
55
+
56
+ this.handlers.splice(index, 1);
57
+ if (this.handlers.length === 0) {
58
+ this.teardown();
59
+ }
60
+ }
61
+
62
+ reset() {
63
+ if (!this.isSupported()) {
64
+ return;
65
+ }
66
+
67
+ this.handlers = [];
68
+ this.teardown();
69
+ }
70
+
71
+ handleData(chunk) {
72
+ if (!this.handlers.length) {
73
+ return;
74
+ }
75
+
76
+ const data = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
77
+ if (!data) {
78
+ return;
79
+ }
80
+
81
+ for (const char of data) {
82
+ if (char === '\u001b') {
83
+ this.escapePending = true;
84
+ this.scheduleTrigger();
85
+ } else if (this.escapePending) {
86
+ // 遇到其它字符说明这是组合键 (例如方向键)
87
+ this.cancelPending();
88
+ }
89
+ }
90
+ }
91
+
92
+ scheduleTrigger() {
93
+ if (this.pendingTimeout) {
94
+ clearTimeout(this.pendingTimeout);
95
+ }
96
+
97
+ this.pendingTimeout = setTimeout(() => {
98
+ this.pendingTimeout = null;
99
+ const shouldTrigger = this.escapePending;
100
+ this.escapePending = false;
101
+ if (shouldTrigger) {
102
+ this.triggerTopHandler();
103
+ }
104
+ }, this.triggerDelay);
105
+ }
106
+
107
+ cancelPending() {
108
+ if (this.pendingTimeout) {
109
+ clearTimeout(this.pendingTimeout);
110
+ this.pendingTimeout = null;
111
+ }
112
+ this.escapePending = false;
113
+ }
114
+
115
+ triggerTopHandler() {
116
+ if (!this.handlers.length) {
117
+ return;
118
+ }
119
+
120
+ const handler = this.handlers[this.handlers.length - 1];
121
+ if (!handler) {
122
+ return;
123
+ }
124
+
125
+ if (handler.once) {
126
+ this.handlers.pop();
127
+ }
128
+
129
+ if (!this.handlers.length) {
130
+ this.teardown();
131
+ }
132
+
133
+ if (handler.onTrigger) {
134
+ setTimeout(() => {
135
+ try {
136
+ handler.onTrigger();
137
+ } catch (error) {
138
+ // 在ESC回调中抛出的错误不应终止进程
139
+ // 交由调用方自行处理记录
140
+ }
141
+ }, handler.postDelay);
142
+ }
143
+ }
144
+
145
+ ensureListening() {
146
+ if (this.listenerBound || !this.isSupported()) {
147
+ return;
148
+ }
149
+
150
+ readline.emitKeypressEvents(this.input);
151
+
152
+ if (typeof this.input.setMaxListeners === 'function') {
153
+ try {
154
+ const currentMax = this.input.getMaxListeners();
155
+ if (currentMax !== 0 && currentMax < 50) {
156
+ this.input.setMaxListeners(50);
157
+ }
158
+ } catch (error) {
159
+ // 某些输入流可能不支持获取/设置监听器上限
160
+ }
161
+ }
162
+
163
+ if (this.input.isTTY && !this.rawModeEnabled) {
164
+ try {
165
+ this.previousRawMode = typeof this.input.isRaw === 'boolean' ? this.input.isRaw : null;
166
+ this.input.setRawMode(true);
167
+ this.rawModeEnabled = true;
168
+ } catch (error) {
169
+ this.supported = false;
170
+ this.handlers = [];
171
+ return;
172
+ }
173
+ }
174
+
175
+ if (typeof this.input.resume === 'function') {
176
+ try {
177
+ this.input.resume();
178
+ } catch (error) {
179
+ // 某些输入流不支持 resume,忽略即可
180
+ }
181
+ }
182
+
183
+ this.input.on('data', this.handleData);
184
+ this.listenerBound = true;
185
+ }
186
+
187
+ teardown() {
188
+ if (!this.isSupported()) {
189
+ this.cancelPending();
190
+ return;
191
+ }
192
+
193
+ if (this.listenerBound) {
194
+ this.input.removeListener('data', this.handleData);
195
+ this.listenerBound = false;
196
+ }
197
+
198
+ this.cancelPending();
199
+
200
+ if (this.rawModeEnabled && this.input.isTTY) {
201
+ try {
202
+ const restoreRaw = typeof this.previousRawMode === 'boolean' ? this.previousRawMode : false;
203
+ this.input.setRawMode(restoreRaw);
204
+ } catch (error) {
205
+ // 忽略还原失败
206
+ }
207
+
208
+ this.rawModeEnabled = false;
209
+ }
210
+ }
211
+ }
212
+
213
+ module.exports = { EscNavigationManager };
@@ -0,0 +1,150 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFLICT_ENV_KEYS = [
6
+ 'ANTHROPIC_API_KEY',
7
+ 'ANTHROPIC_AUTH_TOKEN',
8
+ 'ANTHROPIC_BASE_URL',
9
+ 'CLAUDE_CODE_OAUTH_TOKEN',
10
+ 'ANTHROPIC_MODEL',
11
+ 'ANTHROPIC_SMALL_FAST_MODEL'
12
+ ];
13
+
14
+ const SETTINGS_FILE_NAMES = ['settings.local.json', 'settings.json'];
15
+
16
+ function unique(array) {
17
+ return Array.from(new Set(array.filter(Boolean)));
18
+ }
19
+
20
+ function timestampSuffix() {
21
+ const now = new Date();
22
+ const pad = (num) => String(num).padStart(2, '0');
23
+ return [
24
+ now.getFullYear(),
25
+ pad(now.getMonth() + 1),
26
+ pad(now.getDate()),
27
+ '_',
28
+ pad(now.getHours()),
29
+ pad(now.getMinutes()),
30
+ pad(now.getSeconds())
31
+ ].join('');
32
+ }
33
+
34
+ function resolveCandidatePaths() {
35
+ const candidates = [];
36
+
37
+ if (process.env.CLAUDE_SETTINGS_PATH) {
38
+ candidates.push(process.env.CLAUDE_SETTINGS_PATH);
39
+ }
40
+
41
+ const cwd = process.cwd();
42
+ if (cwd) {
43
+ for (const fileName of SETTINGS_FILE_NAMES) {
44
+ candidates.push(path.join(cwd, '.claude', fileName));
45
+ }
46
+ }
47
+
48
+ const homeDir = os.homedir();
49
+ if (homeDir) {
50
+ for (const fileName of SETTINGS_FILE_NAMES) {
51
+ candidates.push(path.join(homeDir, '.claude', fileName));
52
+ }
53
+ }
54
+
55
+ if (process.platform === 'win32') {
56
+ const baseDirs = unique([process.env.APPDATA, process.env.LOCALAPPDATA]);
57
+ for (const baseDir of baseDirs) {
58
+ if (!baseDir) {
59
+ continue;
60
+ }
61
+ for (const fileName of SETTINGS_FILE_NAMES) {
62
+ candidates.push(path.join(baseDir, 'claude', fileName));
63
+ }
64
+ }
65
+ }
66
+
67
+ return unique(candidates);
68
+ }
69
+
70
+ async function loadSettingsFile() {
71
+ const candidates = resolveCandidatePaths();
72
+ for (const candidate of candidates) {
73
+ try {
74
+ if (await fs.pathExists(candidate)) {
75
+ const data = await fs.readJson(candidate);
76
+ return { path: candidate, data };
77
+ }
78
+ } catch (error) {
79
+ throw new Error(`读取 Claude 设置文件失败 (${candidate}): ${error.message}`);
80
+ }
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function detectConflictKeys(settings) {
86
+ if (!settings || typeof settings !== 'object') {
87
+ return [];
88
+ }
89
+ const env = settings.env;
90
+ if (!env || typeof env !== 'object') {
91
+ return [];
92
+ }
93
+
94
+ return CONFLICT_ENV_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(env, key));
95
+ }
96
+
97
+ async function backupSettingsFile(filePath) {
98
+ const dir = path.dirname(filePath);
99
+ const backupName = `settings.backup-${timestampSuffix()}.json`;
100
+ const backupPath = path.join(dir, backupName);
101
+ await fs.copy(filePath, backupPath, { overwrite: false, errorOnExist: false });
102
+ return backupPath;
103
+ }
104
+
105
+ function clearConflictKeys(settings, keys) {
106
+ if (!settings || !settings.env) {
107
+ return settings;
108
+ }
109
+
110
+ for (const key of keys) {
111
+ if (Object.prototype.hasOwnProperty.call(settings.env, key)) {
112
+ delete settings.env[key];
113
+ }
114
+ }
115
+
116
+ if (Object.keys(settings.env).length === 0) {
117
+ delete settings.env;
118
+ }
119
+
120
+ return settings;
121
+ }
122
+
123
+ async function saveSettingsFile(filePath, data) {
124
+ await fs.writeJson(filePath, data, { spaces: 2 });
125
+ }
126
+
127
+ async function findSettingsConflict() {
128
+ const fileInfo = await loadSettingsFile();
129
+ if (!fileInfo) {
130
+ return null;
131
+ }
132
+
133
+ const conflictKeys = detectConflictKeys(fileInfo.data);
134
+ if (!conflictKeys.length) {
135
+ return null;
136
+ }
137
+
138
+ return {
139
+ filePath: fileInfo.path,
140
+ settings: fileInfo.data,
141
+ keys: conflictKeys
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ findSettingsConflict,
147
+ backupSettingsFile,
148
+ clearConflictKeys,
149
+ saveSettingsFile
150
+ };
@@ -0,0 +1,44 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { spawn } = require('child_process');
5
+
6
+ const AKM_CONFIG_FILE = path.join(os.homedir(), '.akm-config.json');
7
+
8
+ function ensureConfigExists() {
9
+ return fs.pathExists(AKM_CONFIG_FILE);
10
+ }
11
+
12
+ function openFileWithDefaultApp(filePath) {
13
+ return new Promise((resolve, reject) => {
14
+ let command;
15
+ let args = [];
16
+
17
+ if (process.platform === 'win32') {
18
+ command = 'cmd';
19
+ args = ['/c', 'start', '', filePath];
20
+ } else if (process.platform === 'darwin') {
21
+ command = 'open';
22
+ args = [filePath];
23
+ } else {
24
+ command = 'xdg-open';
25
+ args = [filePath];
26
+ }
27
+
28
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
29
+ child.on('error', reject);
30
+ child.unref();
31
+ resolve();
32
+ });
33
+ }
34
+
35
+ async function openAKMConfigFile() {
36
+ if (!(await ensureConfigExists())) {
37
+ throw new Error('未找到 ~/.akm-config.json,请先运行 akm add 创建配置');
38
+ }
39
+ await openFileWithDefaultApp(AKM_CONFIG_FILE);
40
+ }
41
+
42
+ module.exports = {
43
+ openAKMConfigFile
44
+ };