@pikecode/api-key-manager 1.0.25 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 };
@@ -622,6 +622,12 @@ class ProviderAdder extends BaseCommand {
622
622
 
623
623
  try {
624
624
  const codexArgs = [
625
+ {
626
+ name: 'resume',
627
+ label: '继续上次对话',
628
+ description: '恢复之前的会话',
629
+ checked: false
630
+ },
625
631
  {
626
632
  name: '--full-auto',
627
633
  label: '全自动模式',
@@ -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 };
@@ -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,8 +281,37 @@ 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 [
308
+ {
309
+ name: 'resume',
310
+ label: '继续上次对话',
311
+ description: '恢复之前的会话',
312
+ checked: false,
313
+ isSubcommand: true
314
+ },
279
315
  {
280
316
  name: '--full-auto',
281
317
  label: '全自动模式',
@@ -87,7 +87,12 @@ async function executeCodexWithEnv(config, launchArgs = []) {
87
87
  }
88
88
 
89
89
  const env = buildCodexEnvVariables(config);
90
- const args = Array.isArray(launchArgs) ? [...launchArgs] : [];
90
+
91
+ // 处理参数:子命令放前面,选项放后面
92
+ const rawArgs = Array.isArray(launchArgs) ? [...launchArgs] : [];
93
+ const subcommands = rawArgs.filter(arg => !arg.startsWith('-'));
94
+ const options = rawArgs.filter(arg => arg.startsWith('-'));
95
+ const args = [...subcommands, ...options];
91
96
 
92
97
  clearTerminal();
93
98