@pikecode/api-key-manager 1.0.26 → 1.0.27
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/bin/akm.js +46 -0
- package/package.json +1 -1
- package/src/CommandRegistry.js +15 -0
- package/src/commands/backup.js +327 -0
- package/src/commands/switch.js +29 -0
package/bin/akm.js
CHANGED
|
@@ -120,5 +120,51 @@ program
|
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
// Export command
|
|
124
|
+
program
|
|
125
|
+
.command('export')
|
|
126
|
+
.argument('[file]', '导出文件路径 (默认: akm-config-{timestamp}.json)')
|
|
127
|
+
.description('导出配置到文件')
|
|
128
|
+
.option('--mask', '脱敏 Token (导入后需重新设置)')
|
|
129
|
+
.action(async (file, options) => {
|
|
130
|
+
try {
|
|
131
|
+
await registry.executeCommand('export', file, options);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(chalk.red('❌ 导出失败:'), error.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Import command
|
|
139
|
+
program
|
|
140
|
+
.command('import')
|
|
141
|
+
.argument('<file>', '要导入的配置文件路径')
|
|
142
|
+
.description('从文件导入配置')
|
|
143
|
+
.option('--overwrite', '覆盖已存在的供应商')
|
|
144
|
+
.action(async (file, options) => {
|
|
145
|
+
try {
|
|
146
|
+
await registry.executeCommand('import', file, options);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(chalk.red('❌ 导入失败:'), error.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Backup command
|
|
154
|
+
program
|
|
155
|
+
.command('backup')
|
|
156
|
+
.description('备份和恢复配置')
|
|
157
|
+
.option('-l, --list', '列出所有备份')
|
|
158
|
+
.option('-r, --restore <file>', '从备份恢复')
|
|
159
|
+
.option('-d, --dir <path>', '指定备份目录')
|
|
160
|
+
.action(async (options) => {
|
|
161
|
+
try {
|
|
162
|
+
await registry.executeCommand('backup', options);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(chalk.red('❌ 备份操作失败:'), error.message);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
123
169
|
// Parse arguments
|
|
124
170
|
program.parse();
|
package/package.json
CHANGED
package/src/CommandRegistry.js
CHANGED
|
@@ -71,4 +71,19 @@ registry.registerLazy('edit', async () => {
|
|
|
71
71
|
return editCommand;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
registry.registerLazy('export', async () => {
|
|
75
|
+
const { exportCommand } = require('./commands/backup');
|
|
76
|
+
return exportCommand;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
registry.registerLazy('import', async () => {
|
|
80
|
+
const { importCommand } = require('./commands/backup');
|
|
81
|
+
return importCommand;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
registry.registerLazy('backup', async () => {
|
|
85
|
+
const { backupCommand } = require('./commands/backup');
|
|
86
|
+
return backupCommand;
|
|
87
|
+
});
|
|
88
|
+
|
|
74
89
|
module.exports = { CommandRegistry, registry };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { ConfigManager } = require('../config');
|
|
5
|
+
const { Logger } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
class BackupManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.configManager = new ConfigManager();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 导出配置到指定文件
|
|
14
|
+
* @param {string} outputPath - 输出文件路径
|
|
15
|
+
* @param {object} options - 选项
|
|
16
|
+
*/
|
|
17
|
+
async export(outputPath, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
await this.configManager.ensureLoaded();
|
|
20
|
+
const config = this.configManager.config;
|
|
21
|
+
|
|
22
|
+
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
23
|
+
Logger.warning('没有供应商配置可导出');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 准备导出数据
|
|
28
|
+
const exportData = {
|
|
29
|
+
version: '1.0',
|
|
30
|
+
exportedAt: new Date().toISOString(),
|
|
31
|
+
providers: config.providers,
|
|
32
|
+
currentProvider: config.currentProvider
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 如果需要脱敏
|
|
36
|
+
if (options.mask) {
|
|
37
|
+
exportData.providers = this.maskTokens(exportData.providers);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 确定输出路径
|
|
41
|
+
const finalPath = outputPath || `akm-config-${this.getTimestamp()}.json`;
|
|
42
|
+
const absolutePath = path.isAbsolute(finalPath) ? finalPath : path.join(process.cwd(), finalPath);
|
|
43
|
+
|
|
44
|
+
// 写入文件
|
|
45
|
+
await fs.writeJson(absolutePath, exportData, { spaces: 2 });
|
|
46
|
+
|
|
47
|
+
Logger.success(`配置已导出到: ${absolutePath}`);
|
|
48
|
+
console.log(chalk.gray(` 供应商数量: ${Object.keys(config.providers).length}`));
|
|
49
|
+
if (options.mask) {
|
|
50
|
+
console.log(chalk.yellow(' 注意: Token 已脱敏,导入后需要重新设置'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
Logger.error(`导出配置失败: ${error.message}`);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从文件导入配置
|
|
61
|
+
* @param {string} inputPath - 输入文件路径
|
|
62
|
+
* @param {object} options - 选项
|
|
63
|
+
*/
|
|
64
|
+
async import(inputPath, options = {}) {
|
|
65
|
+
try {
|
|
66
|
+
if (!inputPath) {
|
|
67
|
+
Logger.error('请指定要导入的配置文件路径');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.join(process.cwd(), inputPath);
|
|
72
|
+
|
|
73
|
+
if (!await fs.pathExists(absolutePath)) {
|
|
74
|
+
Logger.error(`文件不存在: ${absolutePath}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const importData = await fs.readJson(absolutePath);
|
|
79
|
+
|
|
80
|
+
// 验证导入数据
|
|
81
|
+
if (!importData.providers || typeof importData.providers !== 'object') {
|
|
82
|
+
Logger.error('无效的配置文件格式');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.configManager.ensureLoaded();
|
|
87
|
+
|
|
88
|
+
const importedCount = Object.keys(importData.providers).length;
|
|
89
|
+
let addedCount = 0;
|
|
90
|
+
let skippedCount = 0;
|
|
91
|
+
|
|
92
|
+
for (const [name, provider] of Object.entries(importData.providers)) {
|
|
93
|
+
const exists = this.configManager.getProvider(name);
|
|
94
|
+
|
|
95
|
+
if (exists && !options.overwrite) {
|
|
96
|
+
Logger.warning(`供应商 "${name}" 已存在,跳过 (使用 --overwrite 覆盖)`);
|
|
97
|
+
skippedCount++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 添加或更新供应商
|
|
102
|
+
this.configManager.config.providers[name] = {
|
|
103
|
+
...provider,
|
|
104
|
+
name,
|
|
105
|
+
importedAt: new Date().toISOString()
|
|
106
|
+
};
|
|
107
|
+
addedCount++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 如果设置了 currentProvider 且该供应商存在
|
|
111
|
+
if (importData.currentProvider && this.configManager.config.providers[importData.currentProvider]) {
|
|
112
|
+
if (!this.configManager.config.currentProvider || options.overwrite) {
|
|
113
|
+
this.configManager.config.currentProvider = importData.currentProvider;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await this.configManager.save();
|
|
118
|
+
|
|
119
|
+
Logger.success(`配置导入完成`);
|
|
120
|
+
console.log(chalk.gray(` 导入文件: ${absolutePath}`));
|
|
121
|
+
console.log(chalk.gray(` 成功导入: ${addedCount} 个供应商`));
|
|
122
|
+
if (skippedCount > 0) {
|
|
123
|
+
console.log(chalk.yellow(` 跳过: ${skippedCount} 个已存在的供应商`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
Logger.error(`导入配置失败: ${error.message}`);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 备份当前配置
|
|
134
|
+
* @param {string} backupDir - 备份目录
|
|
135
|
+
*/
|
|
136
|
+
async backup(backupDir) {
|
|
137
|
+
try {
|
|
138
|
+
await this.configManager.ensureLoaded();
|
|
139
|
+
|
|
140
|
+
// 确定备份目录
|
|
141
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
142
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
143
|
+
const finalDir = backupDir || defaultBackupDir;
|
|
144
|
+
|
|
145
|
+
// 确保备份目录存在
|
|
146
|
+
await fs.ensureDir(finalDir);
|
|
147
|
+
|
|
148
|
+
// 生成备份文件名
|
|
149
|
+
const backupFileName = `akm-backup-${this.getTimestamp()}.json`;
|
|
150
|
+
const backupPath = path.join(finalDir, backupFileName);
|
|
151
|
+
|
|
152
|
+
// 读取原始配置文件
|
|
153
|
+
const configPath = this.configManager.configPath;
|
|
154
|
+
if (!await fs.pathExists(configPath)) {
|
|
155
|
+
Logger.warning('配置文件不存在,无需备份');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 复制配置文件
|
|
160
|
+
await fs.copy(configPath, backupPath);
|
|
161
|
+
|
|
162
|
+
Logger.success(`配置已备份到: ${backupPath}`);
|
|
163
|
+
|
|
164
|
+
// 清理旧备份(保留最近 10 个)
|
|
165
|
+
await this.cleanOldBackups(finalDir, 10);
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
Logger.error(`备份配置失败: ${error.message}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 列出所有备份
|
|
175
|
+
* @param {string} backupDir - 备份目录
|
|
176
|
+
*/
|
|
177
|
+
async listBackups(backupDir) {
|
|
178
|
+
try {
|
|
179
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
180
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
181
|
+
const finalDir = backupDir || defaultBackupDir;
|
|
182
|
+
|
|
183
|
+
if (!await fs.pathExists(finalDir)) {
|
|
184
|
+
Logger.warning('备份目录不存在');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const files = await fs.readdir(finalDir);
|
|
189
|
+
const backups = files
|
|
190
|
+
.filter(f => f.startsWith('akm-backup-') && f.endsWith('.json'))
|
|
191
|
+
.sort()
|
|
192
|
+
.reverse();
|
|
193
|
+
|
|
194
|
+
if (backups.length === 0) {
|
|
195
|
+
Logger.warning('没有找到备份文件');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.blue('\n备份列表:'));
|
|
200
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
201
|
+
|
|
202
|
+
for (const backup of backups) {
|
|
203
|
+
const backupPath = path.join(finalDir, backup);
|
|
204
|
+
const stat = await fs.stat(backupPath);
|
|
205
|
+
const size = (stat.size / 1024).toFixed(2);
|
|
206
|
+
console.log(chalk.white(` ${backup}`));
|
|
207
|
+
console.log(chalk.gray(` 大小: ${size} KB | 时间: ${stat.mtime.toLocaleString()}`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
211
|
+
console.log(chalk.blue(`总计: ${backups.length} 个备份`));
|
|
212
|
+
console.log(chalk.gray(`备份目录: ${finalDir}`));
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
Logger.error(`列出备份失败: ${error.message}`);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 从备份恢复
|
|
222
|
+
* @param {string} backupFile - 备份文件路径或名称
|
|
223
|
+
*/
|
|
224
|
+
async restore(backupFile) {
|
|
225
|
+
try {
|
|
226
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
227
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
228
|
+
|
|
229
|
+
let backupPath;
|
|
230
|
+
if (path.isAbsolute(backupFile)) {
|
|
231
|
+
backupPath = backupFile;
|
|
232
|
+
} else if (backupFile.includes('/') || backupFile.includes('\\')) {
|
|
233
|
+
backupPath = path.join(process.cwd(), backupFile);
|
|
234
|
+
} else {
|
|
235
|
+
// 假设是备份目录中的文件名
|
|
236
|
+
backupPath = path.join(defaultBackupDir, backupFile);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!await fs.pathExists(backupPath)) {
|
|
240
|
+
Logger.error(`备份文件不存在: ${backupPath}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 先备份当前配置
|
|
245
|
+
await this.backup();
|
|
246
|
+
|
|
247
|
+
// 恢复备份
|
|
248
|
+
const configPath = this.configManager.configPath;
|
|
249
|
+
await fs.copy(backupPath, configPath);
|
|
250
|
+
|
|
251
|
+
Logger.success(`配置已从备份恢复: ${backupPath}`);
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
Logger.error(`恢复备份失败: ${error.message}`);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 清理旧备份
|
|
261
|
+
*/
|
|
262
|
+
async cleanOldBackups(backupDir, keepCount) {
|
|
263
|
+
const files = await fs.readdir(backupDir);
|
|
264
|
+
const backups = files
|
|
265
|
+
.filter(f => f.startsWith('akm-backup-') && f.endsWith('.json'))
|
|
266
|
+
.sort()
|
|
267
|
+
.reverse();
|
|
268
|
+
|
|
269
|
+
if (backups.length > keepCount) {
|
|
270
|
+
const toDelete = backups.slice(keepCount);
|
|
271
|
+
for (const file of toDelete) {
|
|
272
|
+
await fs.remove(path.join(backupDir, file));
|
|
273
|
+
}
|
|
274
|
+
if (toDelete.length > 0) {
|
|
275
|
+
Logger.info(`已清理 ${toDelete.length} 个旧备份`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Token 脱敏处理
|
|
282
|
+
*/
|
|
283
|
+
maskTokens(providers) {
|
|
284
|
+
const masked = {};
|
|
285
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
286
|
+
masked[name] = {
|
|
287
|
+
...provider,
|
|
288
|
+
authToken: provider.authToken ? this.maskToken(provider.authToken) : null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return masked;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
maskToken(token) {
|
|
295
|
+
if (!token || token.length < 10) return '***';
|
|
296
|
+
return token.substring(0, 8) + '***' + token.substring(token.length - 4);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
getTimestamp() {
|
|
300
|
+
const now = new Date();
|
|
301
|
+
return now.toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function exportCommand(outputPath, options) {
|
|
306
|
+
const manager = new BackupManager();
|
|
307
|
+
await manager.export(outputPath, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function importCommand(inputPath, options) {
|
|
311
|
+
const manager = new BackupManager();
|
|
312
|
+
await manager.import(inputPath, options);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function backupCommand(options) {
|
|
316
|
+
const manager = new BackupManager();
|
|
317
|
+
|
|
318
|
+
if (options.list) {
|
|
319
|
+
await manager.listBackups(options.dir);
|
|
320
|
+
} else if (options.restore) {
|
|
321
|
+
await manager.restore(options.restore);
|
|
322
|
+
} else {
|
|
323
|
+
await manager.backup(options.dir);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { exportCommand, importCommand, backupCommand, BackupManager };
|
package/src/commands/switch.js
CHANGED
|
@@ -93,6 +93,13 @@ class EnvSwitcher extends BaseCommand {
|
|
|
93
93
|
|
|
94
94
|
this.removeESCListener(escListener);
|
|
95
95
|
|
|
96
|
+
// 检查互斥参数
|
|
97
|
+
const conflictError = this.checkExclusiveArgs(answers.selectedArgs, availableArgs);
|
|
98
|
+
if (conflictError) {
|
|
99
|
+
Logger.warning(conflictError);
|
|
100
|
+
return await this.showLaunchArgsSelection(providerName);
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
// 选择参数后直接启动
|
|
97
104
|
await this.launchProvider(provider, answers.selectedArgs);
|
|
98
105
|
|
|
@@ -274,6 +281,28 @@ class EnvSwitcher extends BaseCommand {
|
|
|
274
281
|
];
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
checkExclusiveArgs(selectedArgs, availableArgs) {
|
|
285
|
+
if (!selectedArgs || selectedArgs.length < 2) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const argDef of availableArgs) {
|
|
290
|
+
if (!argDef.exclusive || !selectedArgs.includes(argDef.name)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const exclusiveArg of argDef.exclusive) {
|
|
295
|
+
if (selectedArgs.includes(exclusiveArg)) {
|
|
296
|
+
const arg1 = availableArgs.find(a => a.name === argDef.name);
|
|
297
|
+
const arg2 = availableArgs.find(a => a.name === exclusiveArg);
|
|
298
|
+
return `"${arg1?.label || argDef.name}" 和 "${arg2?.label || exclusiveArg}" 不能同时选择`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
277
306
|
getCodexLaunchArgs() {
|
|
278
307
|
return [
|
|
279
308
|
{
|