@kikkimo/claude-launcher 1.0.0 → 2.1.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/CHANGELOG.md +146 -0
- package/README.md +103 -41
- package/claude-launcher +1071 -576
- package/docs/README-zh.md +107 -45
- package/lib/api-manager.js +449 -0
- package/lib/auth/password-input.js +158 -0
- package/lib/auth/password-strength.js +154 -0
- package/lib/auth/password-validator.js +255 -0
- package/lib/crypto.js +85 -0
- package/lib/i18n/formatter.js +62 -0
- package/lib/i18n/index.js +218 -0
- package/lib/i18n/language-manager.js +160 -0
- package/lib/i18n/locales/de.js +538 -0
- package/lib/i18n/locales/en.js +539 -0
- package/lib/i18n/locales/es.js +538 -0
- package/lib/i18n/locales/fr.js +538 -0
- package/lib/i18n/locales/it.js +539 -0
- package/lib/i18n/locales/ja.js +538 -0
- package/lib/i18n/locales/ko.js +538 -0
- package/lib/i18n/locales/pt.js +539 -0
- package/lib/i18n/locales/ru.js +539 -0
- package/lib/i18n/locales/zh-TW.js +538 -0
- package/lib/i18n/locales/zh.js +538 -0
- package/lib/launcher.js +359 -0
- package/lib/presets/providers.js +148 -0
- package/lib/ui/colors.js +32 -0
- package/lib/ui/interactive-table.js +338 -0
- package/lib/ui/menu.js +383 -0
- package/lib/ui/prompts.js +571 -0
- package/lib/utils/stdin-manager.js +715 -0
- package/lib/utils/string-width.js +180 -0
- package/lib/utils/version-checker.js +240 -0
- package/lib/validators.js +130 -0
- package/package.json +2 -2
package/docs/README-zh.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Claude Launcher
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@kikkimo/claude-launcher) [](https://opensource.org/licenses/MIT) [](https://nodejs.org/) [](https://www.npmjs.com/package/@kikkimo/claude-launcher) [](https://deepwiki.com/kikkimo/claude-launcher)
|
|
4
|
+
|
|
5
|
+
一个优雅的 Claude Code 交互式启动器,具有美观的 Claude 风格界面和全面的第三方 API 管理功能。通过直观的多语言命令行菜单使用各种配置启动 Claude Code。
|
|
4
6
|
|
|
5
7
|
## 📖 文档
|
|
6
8
|
|
|
@@ -9,13 +11,33 @@
|
|
|
9
11
|
|
|
10
12
|
## ✨ 特性
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
### 🎨 **精美界面**
|
|
15
|
+
- Claude 风格界面,采用正宗的橙色/琥珀色配色方案
|
|
16
|
+
- 方向键导航,流畅的菜单切换
|
|
17
|
+
- API 选择和管理的交互式表格
|
|
18
|
+
- 多语言支持(简体中文、繁体中文、英文、德文、法文、西班牙文、意大利文、葡萄牙文、日文、韩文、俄文)
|
|
19
|
+
|
|
20
|
+
### 🔐 **高级安全**
|
|
21
|
+
- 所有敏感数据使用 AES-256-CBC 加密
|
|
22
|
+
- 机器特定的加密密钥增强安全性
|
|
23
|
+
- 密码保护的配置导入/导出
|
|
24
|
+
- 安全的 API 令牌存储,带掩码显示
|
|
25
|
+
- 强密码要求和验证
|
|
26
|
+
|
|
27
|
+
### 🚀 **第三方 API 管理**
|
|
28
|
+
- 全面支持多个第三方 API 提供商(OpenAI、Anthropic、DeepSeek、Kimi、GLM/智谱AI 和自定义 API)
|
|
29
|
+
- 带验证的交互式 API 配置
|
|
30
|
+
- API 使用统计和跟踪
|
|
31
|
+
- 安全的配置备份和恢复
|
|
32
|
+
- 简单的 API 切换和删除
|
|
33
|
+
|
|
34
|
+
### 🌍 **企业级功能**
|
|
35
|
+
- 全局安装 - 在任何地方都可以使用 `claude-launcher`
|
|
36
|
+
- 模块化架构,包含 28+ 个专门模块
|
|
37
|
+
- 全面的错误处理和恢复
|
|
38
|
+
- 版本更新检查,自动通知
|
|
39
|
+
- 跨平台支持(Windows、macOS、Linux)
|
|
40
|
+
- 新用户首次设置向导
|
|
19
41
|
|
|
20
42
|
## 🚀 快速开始
|
|
21
43
|
|
|
@@ -29,9 +51,12 @@
|
|
|
29
51
|
claude-launcher
|
|
30
52
|
```
|
|
31
53
|
|
|
32
|
-
3.
|
|
54
|
+
3. **首次设置:** 启动器将引导您完成:
|
|
55
|
+
- 语言选择(提供11种语言)
|
|
56
|
+
- 安全设置(配置导入/导出的密码)
|
|
57
|
+
- 第三方 API 配置(如果需要)
|
|
33
58
|
|
|
34
|
-
|
|
59
|
+
就这么简单!直观的界面会引导您了解所有可用选项。
|
|
35
60
|
|
|
36
61
|
## 📦 安装
|
|
37
62
|
|
|
@@ -54,19 +79,23 @@ node claude-launcher
|
|
|
54
79
|
|
|
55
80
|
## 🎮 使用方法
|
|
56
81
|
|
|
57
|
-
###
|
|
82
|
+
### 可用的选项
|
|
58
83
|
|
|
59
84
|
1. **启动 Claude Code** - 标准 Claude Code 启动
|
|
60
85
|
2. **启动 Claude Code(跳过权限)** - 使用 `--dangerously-skip-permissions` 启动
|
|
61
|
-
3.
|
|
62
|
-
4.
|
|
63
|
-
5.
|
|
86
|
+
3. **使用第三方 API 启动 Claude Code** - 使用配置的第三方 API
|
|
87
|
+
4. **使用第三方 API 启动 Claude Code(跳过权限)** - 结合第三方 API 和跳过权限
|
|
88
|
+
5. **第三方 API 管理** - 配置、切换、删除 API,查看统计信息
|
|
89
|
+
6. **语言设置** - 在11种支持的语言之间切换
|
|
90
|
+
7. **版本更新检查** - 检查启动器更新
|
|
91
|
+
8. **退出** - 关闭启动器
|
|
64
92
|
|
|
65
93
|
### 交互式导航
|
|
66
94
|
|
|
67
|
-
- **方向键**:使用 ↑↓ 导航,Enter
|
|
68
|
-
-
|
|
69
|
-
-
|
|
95
|
+
- **方向键**:使用 ↑↓ 导航,Enter 选择
|
|
96
|
+
- **Escape 键**:按 Esc 返回或退出
|
|
97
|
+
- **多语言**:所有界面文本适应您选择的语言
|
|
98
|
+
- **智能表格**:API 管理的交互式表格,具有清晰的视觉反馈
|
|
70
99
|
|
|
71
100
|
### 示例会话
|
|
72
101
|
|
|
@@ -77,48 +106,81 @@ $ claude-launcher
|
|
|
77
106
|
│ Claude Code Launcher │
|
|
78
107
|
└────────────────────────────────────────┘
|
|
79
108
|
|
|
80
|
-
|
|
109
|
+
使用 ↑↓ 方向键导航,Enter 选择
|
|
81
110
|
|
|
82
|
-
→
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
→ 启动 Claude Code
|
|
112
|
+
启动 Claude Code(跳过权限)
|
|
113
|
+
使用第三方 API 启动 Claude Code
|
|
114
|
+
使用第三方 API 启动 Claude Code(跳过权限)
|
|
115
|
+
第三方 API 管理
|
|
116
|
+
语言设置
|
|
117
|
+
版本更新检查
|
|
118
|
+
退出
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 第三方 API 管理
|
|
122
|
+
|
|
123
|
+
通过专门的菜单访问全面的 API 管理:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
📋 第三方 API 管理
|
|
127
|
+
|
|
128
|
+
→ 添加新 API
|
|
129
|
+
删除 API
|
|
130
|
+
切换活动 API
|
|
131
|
+
查看统计信息
|
|
132
|
+
导出配置
|
|
133
|
+
导入配置
|
|
134
|
+
更改密码
|
|
135
|
+
返回主菜单
|
|
87
136
|
```
|
|
88
137
|
|
|
89
138
|
## ⚙️ 配置
|
|
90
139
|
|
|
91
|
-
###
|
|
140
|
+
### 现代配置系统
|
|
92
141
|
|
|
93
|
-
|
|
142
|
+
Claude Launcher 2.0 使用先进的配置系统:
|
|
94
143
|
|
|
95
|
-
1.
|
|
96
|
-
2.
|
|
97
|
-
3.
|
|
144
|
+
1. **加密 JSON 存储**:配置存储在 `~/.claude-launcher-apis.json`
|
|
145
|
+
2. **交互式设置**:首次设置向导引导您完成所有选项
|
|
146
|
+
3. **多语言支持**:界面适应您的首选语言
|
|
147
|
+
4. **安全第一**:所有敏感数据使用 AES-256-CBC 加密
|
|
98
148
|
|
|
99
|
-
###
|
|
149
|
+
### 首次设置流程
|
|
100
150
|
|
|
101
|
-
|
|
151
|
+
1. **语言选择**:从11种支持的语言中选择
|
|
152
|
+
2. **安全设置**:
|
|
153
|
+
- 设置导入/导出的密码保护(推荐)
|
|
154
|
+
- 或跳过基本使用(功能有限)
|
|
155
|
+
3. **API 配置**:根据需要添加第三方 API
|
|
102
156
|
|
|
103
|
-
|
|
104
|
-
2. 用户主目录中的 `.claude-launcher.env`
|
|
105
|
-
3. 安装目录中的 `.claude-launcher.env`
|
|
157
|
+
### 第三方 API 配置
|
|
106
158
|
|
|
107
|
-
|
|
159
|
+
通过交互界面配置任何第三方 API 提供商:
|
|
108
160
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
161
|
+
- **支持的提供商**:OpenAI、Anthropic、DeepSeek、Kimi、GLM/智谱AI 和自定义 API
|
|
162
|
+
- **安全存储**:所有 API 令牌在存储前加密
|
|
163
|
+
- **验证**:URL、令牌和模型的实时验证
|
|
164
|
+
- **使用跟踪**:监控 API 使用统计
|
|
165
|
+
- **提供商特定功能**:为每个提供商优化配置,提供有用的注释和建议
|
|
166
|
+
|
|
167
|
+
### 配置导入/导出
|
|
168
|
+
|
|
169
|
+
启用密码保护后:
|
|
113
170
|
|
|
114
|
-
|
|
171
|
+
- **导出**:所有配置的安全备份
|
|
172
|
+
- **导入**:在新机器上恢复配置
|
|
173
|
+
- **密码保护**:所有导出都使用您的密码加密
|
|
174
|
+
- **自动验证**:导入验证确保数据完整性
|
|
115
175
|
|
|
116
|
-
###
|
|
176
|
+
### 增强的安全功能
|
|
117
177
|
|
|
118
|
-
- **AES-256-CBC
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
178
|
+
- **AES-256-CBC 加密**:所有敏感数据使用行业标准算法加密
|
|
179
|
+
- **机器特定密钥**:从独特机器特征派生的加密密钥
|
|
180
|
+
- **密码保护**:配置导入/导出的可选密码层
|
|
181
|
+
- **安全令牌显示**:所有界面显示中的 API 令牌都经过掩码处理
|
|
182
|
+
- **强密码要求**:强制执行密码复杂性以确保最大安全性
|
|
183
|
+
- **仅本地存储**:所有数据保留在您的机器上,无法在其他地方解密
|
|
122
184
|
|
|
123
185
|
## 📋 系统要求
|
|
124
186
|
|
|
@@ -174,4 +236,4 @@ node claude-launcher
|
|
|
174
236
|
|
|
175
237
|
---
|
|
176
238
|
|
|
177
|
-
**注意**: 此启动器设计用于 Claude Code
|
|
239
|
+
**注意**: 此启动器设计用于 Claude Code 和各种第三方 API。使用此工具前请确保已安装 Claude Code。使用第三方 API 时,请确保您拥有首选提供商的有效 API 凭据。
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Manager Module - Manages third-party API configurations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { encrypt, decrypt } = require('./crypto');
|
|
9
|
+
const { validateBaseUrl, validateAuthToken, validateModel } = require('./validators');
|
|
10
|
+
|
|
11
|
+
class ApiManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.configFile = path.join(os.homedir(), '.claude-launcher-apis.json');
|
|
14
|
+
this.config = this.loadConfig();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load configuration from encrypted file
|
|
19
|
+
*/
|
|
20
|
+
loadConfig() {
|
|
21
|
+
try {
|
|
22
|
+
if (fs.existsSync(this.configFile)) {
|
|
23
|
+
const encryptedData = fs.readFileSync(this.configFile, 'utf8');
|
|
24
|
+
const decrypted = decrypt(encryptedData);
|
|
25
|
+
if (decrypted.success) {
|
|
26
|
+
const config = JSON.parse(decrypted.value);
|
|
27
|
+
// Ensure required fields exist
|
|
28
|
+
if (!config.hasOwnProperty('exportPassword')) {
|
|
29
|
+
config.exportPassword = null;
|
|
30
|
+
}
|
|
31
|
+
if (!config.hasOwnProperty('passwordSkipped')) {
|
|
32
|
+
config.passwordSkipped = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[!] Could not load API config: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
apis: [],
|
|
44
|
+
activeIndex: -1,
|
|
45
|
+
version: '2.0.0',
|
|
46
|
+
createdAt: new Date().toISOString(),
|
|
47
|
+
exportPassword: null, // Hashed export password for validation
|
|
48
|
+
passwordSkipped: false // Whether user permanently skipped password setup
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Save configuration to encrypted file
|
|
54
|
+
*/
|
|
55
|
+
saveConfig() {
|
|
56
|
+
try {
|
|
57
|
+
const configJson = JSON.stringify(this.config, null, 2);
|
|
58
|
+
const encrypted = encrypt(configJson);
|
|
59
|
+
if (encrypted.success) {
|
|
60
|
+
fs.writeFileSync(this.configFile, encrypted.value);
|
|
61
|
+
return true;
|
|
62
|
+
} else {
|
|
63
|
+
console.error(`[!] Failed to save API config: ${encrypted.error}`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`[!] Error saving API config: ${error.message}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check for duplicate API configurations - URL + authToken + model must be unique
|
|
74
|
+
*/
|
|
75
|
+
checkDuplicate(baseUrl, authToken, model) {
|
|
76
|
+
const existing = this.config.apis.find(api => {
|
|
77
|
+
const decryptedToken = decrypt(api.authToken);
|
|
78
|
+
const existingToken = decryptedToken.success ? decryptedToken.value : '';
|
|
79
|
+
return api.baseUrl === baseUrl &&
|
|
80
|
+
existingToken === authToken &&
|
|
81
|
+
api.model === model;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (existing) {
|
|
85
|
+
return {
|
|
86
|
+
isDuplicate: true,
|
|
87
|
+
type: 'Complete Configuration (URL + Token + Model)',
|
|
88
|
+
existing
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { isDuplicate: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Add a new API configuration
|
|
97
|
+
*/
|
|
98
|
+
addApi(baseUrl, authToken, model, name, provider = 'custom') {
|
|
99
|
+
// Validate inputs
|
|
100
|
+
const urlValidation = validateBaseUrl(baseUrl);
|
|
101
|
+
if (!urlValidation.valid) {
|
|
102
|
+
throw new Error(`Invalid Base URL: ${urlValidation.error}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tokenValidation = validateAuthToken(authToken);
|
|
106
|
+
if (!tokenValidation.valid) {
|
|
107
|
+
throw new Error(`Invalid Auth Token: ${tokenValidation.error}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const modelValidation = validateModel(model);
|
|
111
|
+
if (!modelValidation.valid) {
|
|
112
|
+
throw new Error(`Invalid Model: ${modelValidation.error}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check for duplicates
|
|
116
|
+
const duplicate = this.checkDuplicate(baseUrl, authToken, model);
|
|
117
|
+
if (duplicate.isDuplicate) {
|
|
118
|
+
throw new Error(`${duplicate.type} already exists for API: ${duplicate.existing.name}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Encrypt the auth token before storing
|
|
122
|
+
const encryptedToken = encrypt(tokenValidation.value);
|
|
123
|
+
if (!encryptedToken.success) {
|
|
124
|
+
throw new Error(`Failed to encrypt auth token: ${encryptedToken.error}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const newApi = {
|
|
128
|
+
id: Date.now().toString(),
|
|
129
|
+
name: name || `API-${this.config.apis.length + 1}`,
|
|
130
|
+
provider: provider,
|
|
131
|
+
baseUrl: urlValidation.value,
|
|
132
|
+
authToken: encryptedToken.value,
|
|
133
|
+
model: modelValidation.value,
|
|
134
|
+
smallFastModel: modelValidation.value, // Same as model as requested
|
|
135
|
+
createdAt: new Date().toISOString(),
|
|
136
|
+
lastUsed: null,
|
|
137
|
+
usageCount: 0
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
this.config.apis.push(newApi);
|
|
141
|
+
|
|
142
|
+
// Set as active if it's the first API
|
|
143
|
+
if (this.config.apis.length === 1) {
|
|
144
|
+
this.config.activeIndex = 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.saveConfig();
|
|
148
|
+
return newApi;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Remove an API configuration
|
|
153
|
+
*/
|
|
154
|
+
removeApi(index) {
|
|
155
|
+
if (index < 0 || index >= this.config.apis.length) {
|
|
156
|
+
throw new Error('Invalid API index');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const removedApi = this.config.apis[index];
|
|
160
|
+
this.config.apis.splice(index, 1);
|
|
161
|
+
|
|
162
|
+
// Adjust active index
|
|
163
|
+
if (this.config.activeIndex >= index) {
|
|
164
|
+
this.config.activeIndex = this.config.activeIndex > 0 ? this.config.activeIndex - 1 : -1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.config.apis.length === 0) {
|
|
168
|
+
this.config.activeIndex = -1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.saveConfig();
|
|
172
|
+
return removedApi;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all API configurations
|
|
177
|
+
*/
|
|
178
|
+
getApis() {
|
|
179
|
+
return this.config.apis;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Set the active API
|
|
184
|
+
*/
|
|
185
|
+
setActiveApi(index) {
|
|
186
|
+
if (index < 0 || index >= this.config.apis.length) {
|
|
187
|
+
throw new Error('Invalid API index');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.config.activeIndex = index;
|
|
191
|
+
this.saveConfig();
|
|
192
|
+
return this.config.apis[index];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get the currently active API
|
|
197
|
+
*/
|
|
198
|
+
getActiveApi() {
|
|
199
|
+
if (this.config.activeIndex >= 0 && this.config.activeIndex < this.config.apis.length) {
|
|
200
|
+
return this.config.apis[this.config.activeIndex];
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Increment usage count for the active API when actually used
|
|
207
|
+
*/
|
|
208
|
+
incrementActiveApiUsage() {
|
|
209
|
+
const activeApi = this.getActiveApi();
|
|
210
|
+
if (activeApi) {
|
|
211
|
+
const index = this.config.activeIndex;
|
|
212
|
+
this.config.apis[index].lastUsed = new Date().toISOString();
|
|
213
|
+
this.config.apis[index].usageCount = (this.config.apis[index].usageCount || 0) + 1;
|
|
214
|
+
this.saveConfig();
|
|
215
|
+
return this.config.apis[index];
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get statistics about API usage
|
|
224
|
+
*/
|
|
225
|
+
getStatistics() {
|
|
226
|
+
const totalApis = this.config.apis.length;
|
|
227
|
+
const activeApi = this.getActiveApi();
|
|
228
|
+
const mostUsed = this.config.apis.reduce((prev, current) =>
|
|
229
|
+
(current.usageCount > (prev?.usageCount || 0)) ? current : prev, null);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
totalApis,
|
|
233
|
+
activeApiName: activeApi?.name || 'None',
|
|
234
|
+
mostUsedApi: mostUsed?.name || 'None',
|
|
235
|
+
totalUsage: this.config.apis.reduce((sum, api) => sum + api.usageCount, 0)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Check if this is first time usage (no password set AND no APIs configured)
|
|
241
|
+
*/
|
|
242
|
+
isFirstTimeUsage() {
|
|
243
|
+
return this.config.exportPassword === null &&
|
|
244
|
+
this.config.apis.length === 0 &&
|
|
245
|
+
!this.config.passwordSkipped;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if export password is set
|
|
250
|
+
*/
|
|
251
|
+
hasExportPassword() {
|
|
252
|
+
return this.config.exportPassword !== null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if password was permanently skipped
|
|
257
|
+
*/
|
|
258
|
+
isPasswordSkipped() {
|
|
259
|
+
return this.config.passwordSkipped === true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check if import/export features should be available
|
|
264
|
+
*/
|
|
265
|
+
canUseImportExport() {
|
|
266
|
+
return this.hasExportPassword();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Set export password (hashed)
|
|
271
|
+
*/
|
|
272
|
+
setExportPassword(password) {
|
|
273
|
+
const crypto = require('crypto');
|
|
274
|
+
this.config.exportPassword = crypto.createHash('sha256').update(password).digest('hex');
|
|
275
|
+
this.saveConfig();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Permanently skip password setup (one-time only, can't be undone)
|
|
280
|
+
*/
|
|
281
|
+
skipPasswordSetup() {
|
|
282
|
+
if (!this.isFirstTimeUsage()) {
|
|
283
|
+
throw new Error('Password setup can only be skipped during first time usage');
|
|
284
|
+
}
|
|
285
|
+
this.config.passwordSkipped = true;
|
|
286
|
+
this.saveConfig();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Verify export password
|
|
291
|
+
*/
|
|
292
|
+
verifyExportPassword(password) {
|
|
293
|
+
if (!this.hasExportPassword()) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const crypto = require('crypto');
|
|
297
|
+
const hashedInput = crypto.createHash('sha256').update(password).digest('hex');
|
|
298
|
+
return hashedInput === this.config.exportPassword;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Remove export password
|
|
303
|
+
*/
|
|
304
|
+
removeExportPassword() {
|
|
305
|
+
this.config.exportPassword = null;
|
|
306
|
+
this.saveConfig();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Export configuration as plaintext JSON (password verification required)
|
|
311
|
+
*/
|
|
312
|
+
exportConfig(password) {
|
|
313
|
+
// Verify password before export
|
|
314
|
+
if (!this.verifyExportPassword(password)) {
|
|
315
|
+
throw new Error('Invalid password for export operation');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return this.exportConfigAuthenticated();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Export configuration as plaintext JSON (already authenticated)
|
|
323
|
+
*/
|
|
324
|
+
exportConfigAuthenticated() {
|
|
325
|
+
// Create export data with plaintext API keys
|
|
326
|
+
const exportData = {
|
|
327
|
+
version: this.config.version,
|
|
328
|
+
exportedAt: new Date().toISOString(),
|
|
329
|
+
apis: this.config.apis.map(api => {
|
|
330
|
+
const decrypted = decrypt(api.authToken);
|
|
331
|
+
return {
|
|
332
|
+
...api,
|
|
333
|
+
authToken: decrypted.success ? decrypted.value : '***DECRYPTION_FAILED***'
|
|
334
|
+
};
|
|
335
|
+
}),
|
|
336
|
+
activeIndex: this.config.activeIndex
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return JSON.stringify(exportData, null, 2);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Import configuration from plaintext JSON (password verification required)
|
|
344
|
+
*/
|
|
345
|
+
importConfig(plaintextData, password) {
|
|
346
|
+
// Verify password before import
|
|
347
|
+
if (!this.verifyExportPassword(password)) {
|
|
348
|
+
throw new Error('Invalid password for import operation');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return this.importConfigAuthenticated(plaintextData);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Import configuration from plaintext JSON (already authenticated)
|
|
356
|
+
*/
|
|
357
|
+
importConfigAuthenticated(plaintextData) {
|
|
358
|
+
const configData = JSON.parse(plaintextData);
|
|
359
|
+
return this.processImportData(configData);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Process import data (merge with existing)
|
|
365
|
+
*/
|
|
366
|
+
processImportData(configData) {
|
|
367
|
+
let imported = 0;
|
|
368
|
+
let skipped = 0;
|
|
369
|
+
|
|
370
|
+
if (!configData.apis || !Array.isArray(configData.apis)) {
|
|
371
|
+
throw new Error('Invalid configuration format - no APIs found');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
configData.apis.forEach(importApi => {
|
|
375
|
+
// Validate the API configuration before importing
|
|
376
|
+
try {
|
|
377
|
+
// Validate Base URL
|
|
378
|
+
const urlValidation = validateBaseUrl(importApi.baseUrl);
|
|
379
|
+
if (!urlValidation.valid) {
|
|
380
|
+
console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
|
|
381
|
+
skipped++;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Validate Auth Token (skip validation for placeholder tokens)
|
|
386
|
+
if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
|
|
387
|
+
const tokenValidation = validateAuthToken(importApi.authToken);
|
|
388
|
+
if (!tokenValidation.valid) {
|
|
389
|
+
console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
|
|
390
|
+
skipped++;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Validate Model
|
|
396
|
+
const modelValidation = validateModel(importApi.model);
|
|
397
|
+
if (!modelValidation.valid) {
|
|
398
|
+
console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
|
|
399
|
+
skipped++;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check for duplicates using the same logic as addApi
|
|
404
|
+
const importToken = importApi.authToken === '***REQUIRES_MANUAL_INPUT***' ? '' : importApi.authToken;
|
|
405
|
+
const duplicate = this.checkDuplicate(importApi.baseUrl, importToken, importApi.model);
|
|
406
|
+
|
|
407
|
+
if (duplicate.isDuplicate) {
|
|
408
|
+
skipped++;
|
|
409
|
+
} else {
|
|
410
|
+
// Encrypt the auth token if it's not already encrypted or masked
|
|
411
|
+
let encryptedToken;
|
|
412
|
+
if (importApi.authToken === '***REQUIRES_MANUAL_INPUT***') {
|
|
413
|
+
encryptedToken = encrypt('').value; // Empty encrypted token
|
|
414
|
+
} else {
|
|
415
|
+
encryptedToken = encrypt(importApi.authToken).value;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const newApi = {
|
|
419
|
+
id: Date.now() + Math.random(),
|
|
420
|
+
name: importApi.name || `Imported API ${this.config.apis.length + 1}`,
|
|
421
|
+
baseUrl: urlValidation.value,
|
|
422
|
+
authToken: encryptedToken,
|
|
423
|
+
model: modelValidation.value,
|
|
424
|
+
provider: importApi.provider || 'custom',
|
|
425
|
+
createdAt: new Date().toISOString(),
|
|
426
|
+
lastUsed: null,
|
|
427
|
+
usageCount: 0
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
this.config.apis.push(newApi);
|
|
431
|
+
imported++;
|
|
432
|
+
|
|
433
|
+
// Set as active if this is the first API
|
|
434
|
+
if (this.config.apis.length === 1) {
|
|
435
|
+
this.config.activeIndex = 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.warn(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
|
|
440
|
+
skipped++;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
this.saveConfig();
|
|
445
|
+
return { imported, skipped };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
module.exports = ApiManager;
|