@kikkimo/claude-launcher 2.5.0 → 3.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/CHANGELOG.md +42 -0
- package/README.md +17 -10
- package/claude-launcher +614 -398
- package/docs/README-zh.md +17 -10
- package/lib/api-manager.js +136 -11
- package/lib/auth/password-input.js +8 -4
- package/lib/auth/password-validator.js +83 -48
- package/lib/i18n/index.js +4 -3
- package/lib/i18n/language-manager.js +4 -3
- package/lib/i18n/locales/de.js +89 -11
- package/lib/i18n/locales/en.js +89 -11
- package/lib/i18n/locales/es.js +89 -11
- package/lib/i18n/locales/fr.js +89 -11
- package/lib/i18n/locales/it.js +89 -11
- package/lib/i18n/locales/ja.js +89 -11
- package/lib/i18n/locales/ko.js +89 -11
- package/lib/i18n/locales/pt.js +89 -11
- package/lib/i18n/locales/ru.js +89 -11
- package/lib/i18n/locales/zh-TW.js +89 -11
- package/lib/i18n/locales/zh.js +89 -11
- package/lib/launcher.js +121 -93
- package/lib/ui/api-editor.js +210 -0
- package/lib/ui/interactive-table.js +216 -99
- package/lib/ui/menu.js +73 -62
- package/lib/ui/prompts.js +168 -139
- package/lib/ui/screen.js +125 -0
- package/lib/utils/stdin-manager.js +11 -9
- package/lib/utils/version-checker.js +63 -3
- package/package.json +2 -2
- package/docs/superpowers/plans/2026-03-31-update-models-and-auto-mode.md +0 -1414
- package/docs/superpowers/specs/2026-03-31-update-models-and-auto-mode-design.md +0 -187
package/docs/README-zh.md
CHANGED
|
@@ -13,13 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
### 🎨 **精美界面**
|
|
15
15
|
- Claude 风格界面,采用正宗的橙色/琥珀色配色方案
|
|
16
|
+
- ANSI 备用屏幕缓冲区,无漂移渲染(类似 vim/htop)
|
|
16
17
|
- 方向键导航,流畅的菜单切换
|
|
18
|
+
- 分页 API 表格,支持 ←→ 翻页浏览大量 API
|
|
17
19
|
- API 选择和管理的交互式表格
|
|
18
20
|
- 多语言支持(简体中文、繁体中文、英文、德文、法文、西班牙文、意大利文、葡萄牙文、日文、韩文、俄文)
|
|
19
21
|
|
|
20
22
|
### 🔐 **高级安全**
|
|
21
23
|
- 所有敏感数据使用 AES-256-CBC 加密
|
|
22
24
|
- 机器特定的加密密钥增强安全性
|
|
25
|
+
- 统一密码守卫保护高风险操作(编辑、删除、导入、导出)
|
|
23
26
|
- 密码保护的配置导入/导出
|
|
24
27
|
- 安全的 API 令牌存储,带掩码显示
|
|
25
28
|
- 强密码要求和验证
|
|
@@ -27,10 +30,12 @@
|
|
|
27
30
|
### 🚀 **第三方 API 管理**
|
|
28
31
|
- 全面支持多个第三方 API 提供商(Anthropic、DeepSeek、Kimi K2.5、MiniMax M2.7、GLM-5.1/智谱AI 和自定义 API)
|
|
29
32
|
- 带验证的交互式 API 配置
|
|
33
|
+
- **编辑 API**:修改已有 API 的名称、供应商、Base URL 和模型
|
|
30
34
|
- API 使用统计,支持成功/失败率追踪
|
|
31
35
|
- 模型升级通知和自动升级支持
|
|
32
36
|
- 安全的配置备份和恢复
|
|
33
37
|
- 简单的 API 切换、删除和批量清空
|
|
38
|
+
- 每个配置最多支持 99 个 API
|
|
34
39
|
|
|
35
40
|
### 🌍 **企业级功能**
|
|
36
41
|
- 全局安装 - 在任何地方都可以使用 `claude-launcher`
|
|
@@ -88,20 +93,21 @@ node claude-launcher
|
|
|
88
93
|
4. **使用第三方 API 启动 Claude Code** - 使用配置的第三方 API
|
|
89
94
|
5. **使用第三方 API 启动 Claude Code(跳过权限)** - 结合第三方 API 和跳过权限
|
|
90
95
|
6. **第三方 API 管理** - 完整的 API 生命周期管理:
|
|
91
|
-
-
|
|
96
|
+
- 添加、编辑、切换和删除 API
|
|
92
97
|
- 查看使用统计(含成功/失败率)
|
|
93
98
|
- 模型升级设置(自动/手动升级)
|
|
94
|
-
-
|
|
95
|
-
7.
|
|
99
|
+
- 导入/导出配置(密码保护)
|
|
100
|
+
7. **配置管理** - 语言、遥测、启动模式、模型升级设置
|
|
96
101
|
8. **版本更新检查** - 检查启动器更新
|
|
97
102
|
9. **退出** - 关闭启动器
|
|
98
103
|
|
|
99
104
|
### 交互式导航
|
|
100
105
|
|
|
101
|
-
- **方向键**:使用 ↑↓
|
|
102
|
-
- **Escape 键**:按 Esc
|
|
106
|
+
- **方向键**:使用 ↑↓ 导航,←→ 翻页(分页表格中),Enter 选择
|
|
107
|
+
- **Escape 键**:按 Esc 返回或取消
|
|
108
|
+
- **Ctrl+C**:第一次按下显示警告,第二次按下干净退出
|
|
103
109
|
- **多语言**:所有界面文本适应您选择的语言
|
|
104
|
-
-
|
|
110
|
+
- **智能表格**:分页交互式表格,支持每页选择记忆
|
|
105
111
|
|
|
106
112
|
### 示例会话
|
|
107
113
|
|
|
@@ -120,7 +126,7 @@ $ claude-launcher
|
|
|
120
126
|
使用第三方 API 启动 Claude Code
|
|
121
127
|
使用第三方 API 启动 Claude Code(跳过权限)
|
|
122
128
|
第三方 API 管理
|
|
123
|
-
|
|
129
|
+
配置管理
|
|
124
130
|
版本更新检查
|
|
125
131
|
退出
|
|
126
132
|
```
|
|
@@ -133,12 +139,13 @@ $ claude-launcher
|
|
|
133
139
|
📋 第三方 API 管理
|
|
134
140
|
|
|
135
141
|
→ 添加新 API
|
|
142
|
+
编辑 API → 选择 API → 编辑名称/供应商/URL/模型
|
|
136
143
|
删除 API → 删除单个 API / 清空所有 API
|
|
137
144
|
切换活动 API
|
|
138
145
|
查看统计信息 → 查看详情 / 重置统计
|
|
139
146
|
模型升级设置 → 自动升级 [开/关] / 手动升级
|
|
140
|
-
导出配置
|
|
141
|
-
导入配置
|
|
147
|
+
导出配置 🔒(需要密码)
|
|
148
|
+
导入配置 🔒(需要密码)
|
|
142
149
|
更改密码
|
|
143
150
|
返回主菜单
|
|
144
151
|
```
|
|
@@ -154,7 +161,7 @@ $ claude-launcher
|
|
|
154
161
|
|
|
155
162
|
### 现代配置系统
|
|
156
163
|
|
|
157
|
-
Claude Launcher
|
|
164
|
+
Claude Launcher 使用先进的配置系统:
|
|
158
165
|
|
|
159
166
|
1. **加密 JSON 存储**:配置存储在 `~/.claude-launcher-apis.json`
|
|
160
167
|
2. **交互式设置**:首次设置向导引导您完成所有选项
|
package/lib/api-manager.js
CHANGED
|
@@ -6,7 +6,8 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const { encrypt, decrypt } = require('./crypto');
|
|
9
|
-
const { validateBaseUrl, validateAuthToken, validateModel } = require('./validators');
|
|
9
|
+
const { validateBaseUrl, validateAuthToken, validateModel, validateApiName } = require('./validators');
|
|
10
|
+
const screen = require('./ui/screen');
|
|
10
11
|
|
|
11
12
|
class ApiManager {
|
|
12
13
|
constructor() {
|
|
@@ -36,7 +37,7 @@ class ApiManager {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
} catch (error) {
|
|
39
|
-
|
|
40
|
+
screen.debug(`[!] Could not load API config: ${error.message}`);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
return {
|
|
@@ -60,11 +61,11 @@ class ApiManager {
|
|
|
60
61
|
fs.writeFileSync(this.configFile, encrypted.value);
|
|
61
62
|
return true;
|
|
62
63
|
} else {
|
|
63
|
-
|
|
64
|
+
screen.debug(`[!] Failed to save API config: ${encrypted.error}`);
|
|
64
65
|
return false;
|
|
65
66
|
}
|
|
66
67
|
} catch (error) {
|
|
67
|
-
|
|
68
|
+
screen.debug(`[!] Error saving API config: ${error.message}`);
|
|
68
69
|
return false;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -96,6 +97,10 @@ class ApiManager {
|
|
|
96
97
|
* Add a new API configuration
|
|
97
98
|
*/
|
|
98
99
|
addApi(baseUrl, authToken, model, name, provider = 'custom') {
|
|
100
|
+
if (this.config.apis.length >= 99) {
|
|
101
|
+
throw new Error('Maximum 99 APIs supported. Remove unused APIs before adding new ones.');
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
// Validate inputs
|
|
100
105
|
const urlValidation = validateBaseUrl(baseUrl);
|
|
101
106
|
if (!urlValidation.valid) {
|
|
@@ -241,15 +246,95 @@ class ApiManager {
|
|
|
241
246
|
* @returns {Object} The updated API object
|
|
242
247
|
*/
|
|
243
248
|
updateApiModel(apiId, newModel) {
|
|
249
|
+
return this.updateApiField(apiId, 'model', newModel);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Update a single field of an API configuration with validation
|
|
254
|
+
* @param {string} apiId - The API id
|
|
255
|
+
* @param {string} field - Field name: 'name', 'provider', 'baseUrl', 'model'
|
|
256
|
+
* @param {string} value - New value
|
|
257
|
+
* @returns {Object} The updated API object
|
|
258
|
+
*/
|
|
259
|
+
updateApiField(apiId, field, value) {
|
|
260
|
+
const allowedFields = ['name', 'provider', 'baseUrl', 'model'];
|
|
261
|
+
if (!allowedFields.includes(field)) {
|
|
262
|
+
throw new Error(`Field '${field}' is not allowed. Allowed: ${allowedFields.join(', ')}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
244
265
|
const index = this.config.apis.findIndex(api => api.id === apiId);
|
|
245
266
|
if (index === -1) {
|
|
246
267
|
throw new Error(`API not found: ${apiId}`);
|
|
247
268
|
}
|
|
248
269
|
|
|
249
|
-
this.config.apis[index]
|
|
250
|
-
|
|
270
|
+
const api = this.config.apis[index];
|
|
271
|
+
|
|
272
|
+
// Manager-level validation
|
|
273
|
+
switch (field) {
|
|
274
|
+
case 'name': {
|
|
275
|
+
if (!value || value.trim() === '') {
|
|
276
|
+
throw new Error('Name cannot be empty when editing');
|
|
277
|
+
}
|
|
278
|
+
const nameValidation = validateApiName(value);
|
|
279
|
+
if (!nameValidation.valid) {
|
|
280
|
+
throw new Error(`Invalid name: ${nameValidation.error}`);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case 'provider': {
|
|
285
|
+
const { getAllProviders } = require('./presets/providers');
|
|
286
|
+
const validIds = getAllProviders().map(p => p.id);
|
|
287
|
+
if (!validIds.includes(value)) {
|
|
288
|
+
throw new Error(`Unknown provider: ${value}. Valid: ${validIds.join(', ')}`);
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'baseUrl': {
|
|
293
|
+
const urlValidation = validateBaseUrl(value);
|
|
294
|
+
if (!urlValidation.valid) {
|
|
295
|
+
throw new Error(`Invalid URL: ${urlValidation.error}`);
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'model': {
|
|
300
|
+
const modelValidation = validateModel(value);
|
|
301
|
+
if (!modelValidation.valid) {
|
|
302
|
+
throw new Error(`Invalid model: ${modelValidation.error}`);
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Duplicate check for uniqueness-affecting fields
|
|
309
|
+
if (field === 'baseUrl' || field === 'model') {
|
|
310
|
+
const checkUrl = field === 'baseUrl' ? value : api.baseUrl;
|
|
311
|
+
const checkModel = field === 'model' ? value : api.model;
|
|
312
|
+
const decryptedToken = decrypt(api.authToken);
|
|
313
|
+
const tokenValue = decryptedToken.success ? decryptedToken.value : '';
|
|
314
|
+
|
|
315
|
+
// Check against all OTHER apis (exclude self)
|
|
316
|
+
const duplicate = this.config.apis.find((other, idx) => {
|
|
317
|
+
if (idx === index) return false;
|
|
318
|
+
const otherToken = decrypt(other.authToken);
|
|
319
|
+
const otherTokenValue = otherToken.success ? otherToken.value : '';
|
|
320
|
+
return other.baseUrl === checkUrl &&
|
|
321
|
+
otherTokenValue === tokenValue &&
|
|
322
|
+
other.model === checkModel;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
if (duplicate) {
|
|
326
|
+
throw new Error(`Duplicate configuration: URL + Token + Model already exists for API '${duplicate.name}'`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Apply update
|
|
331
|
+
api[field] = value.trim();
|
|
332
|
+
if (field === 'model') {
|
|
333
|
+
api.smallFastModel = value.trim();
|
|
334
|
+
}
|
|
335
|
+
|
|
251
336
|
this.saveConfig();
|
|
252
|
-
return
|
|
337
|
+
return api;
|
|
253
338
|
}
|
|
254
339
|
|
|
255
340
|
/**
|
|
@@ -289,6 +374,40 @@ class ApiManager {
|
|
|
289
374
|
return null;
|
|
290
375
|
}
|
|
291
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Record a launch attempt (optimistic success)
|
|
379
|
+
* Call rollbackLaunchAttempt() if a pre-launch sync error occurs
|
|
380
|
+
* @returns {Object|null} The updated API object or null
|
|
381
|
+
*/
|
|
382
|
+
recordLaunchAttempt() {
|
|
383
|
+
const activeApi = this.getActiveApi();
|
|
384
|
+
if (activeApi) {
|
|
385
|
+
const index = this.config.activeIndex;
|
|
386
|
+
this.config.apis[index].lastUsed = new Date().toISOString();
|
|
387
|
+
this.config.apis[index].usageCount = (this.config.apis[index].usageCount || 0) + 1;
|
|
388
|
+
this.config.apis[index].successCount = (this.config.apis[index].successCount || 0) + 1;
|
|
389
|
+
this.config.apis[index].lastError = null;
|
|
390
|
+
this.saveConfig();
|
|
391
|
+
return this.config.apis[index];
|
|
392
|
+
}
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Rollback an optimistic launch attempt on pre-launch sync error
|
|
398
|
+
* @param {string} errorMessage - The error message
|
|
399
|
+
*/
|
|
400
|
+
rollbackLaunchAttempt(errorMessage) {
|
|
401
|
+
const activeApi = this.getActiveApi();
|
|
402
|
+
if (activeApi) {
|
|
403
|
+
const index = this.config.activeIndex;
|
|
404
|
+
this.config.apis[index].successCount = Math.max(0, (this.config.apis[index].successCount || 0) - 1);
|
|
405
|
+
this.config.apis[index].failCount = (this.config.apis[index].failCount || 0) + 1;
|
|
406
|
+
this.config.apis[index].lastError = errorMessage;
|
|
407
|
+
this.saveConfig();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
292
411
|
/**
|
|
293
412
|
* Get statistics about API usage
|
|
294
413
|
*/
|
|
@@ -506,12 +625,18 @@ class ApiManager {
|
|
|
506
625
|
}
|
|
507
626
|
|
|
508
627
|
configData.apis.forEach(importApi => {
|
|
628
|
+
if (this.config.apis.length >= 99) {
|
|
629
|
+
screen.debug('Import skipped: maximum 99 APIs reached');
|
|
630
|
+
skipped++;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
509
634
|
// Validate the API configuration before importing
|
|
510
635
|
try {
|
|
511
636
|
// Validate Base URL
|
|
512
637
|
const urlValidation = validateBaseUrl(importApi.baseUrl);
|
|
513
638
|
if (!urlValidation.valid) {
|
|
514
|
-
|
|
639
|
+
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Base URL: ${urlValidation.error}`);
|
|
515
640
|
skipped++;
|
|
516
641
|
return;
|
|
517
642
|
}
|
|
@@ -520,7 +645,7 @@ class ApiManager {
|
|
|
520
645
|
if (importApi.authToken !== '***REQUIRES_MANUAL_INPUT***') {
|
|
521
646
|
const tokenValidation = validateAuthToken(importApi.authToken);
|
|
522
647
|
if (!tokenValidation.valid) {
|
|
523
|
-
|
|
648
|
+
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Auth Token: ${tokenValidation.error}`);
|
|
524
649
|
skipped++;
|
|
525
650
|
return;
|
|
526
651
|
}
|
|
@@ -529,7 +654,7 @@ class ApiManager {
|
|
|
529
654
|
// Validate Model
|
|
530
655
|
const modelValidation = validateModel(importApi.model);
|
|
531
656
|
if (!modelValidation.valid) {
|
|
532
|
-
|
|
657
|
+
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Invalid Model: ${modelValidation.error}`);
|
|
533
658
|
skipped++;
|
|
534
659
|
return;
|
|
535
660
|
}
|
|
@@ -570,7 +695,7 @@ class ApiManager {
|
|
|
570
695
|
}
|
|
571
696
|
}
|
|
572
697
|
} catch (error) {
|
|
573
|
-
|
|
698
|
+
screen.debug(`Skipping API "${importApi.name || 'Unknown'}" - Validation error: ${error.message}`);
|
|
574
699
|
skipped++;
|
|
575
700
|
}
|
|
576
701
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const stdinManager = require('../utils/stdin-manager');
|
|
8
|
+
const screen = require('../ui/screen');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get password input with proper masking (no plaintext display)
|
|
@@ -18,7 +19,7 @@ function getPasswordInput(prompt) {
|
|
|
18
19
|
let cleanedUp = false;
|
|
19
20
|
|
|
20
21
|
// Display prompt - this is necessary for password input
|
|
21
|
-
|
|
22
|
+
screen.write(prompt);
|
|
22
23
|
|
|
23
24
|
if (!process.stdin.isTTY) {
|
|
24
25
|
// Non-TTY environment fallback - use scope's createReadline
|
|
@@ -52,11 +53,14 @@ function getPasswordInput(prompt) {
|
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
screen.showCursor();
|
|
57
|
+
|
|
55
58
|
const cleanup = () => {
|
|
56
59
|
if (cleanedUp) return;
|
|
57
60
|
cleanedUp = true;
|
|
58
61
|
|
|
59
62
|
try {
|
|
63
|
+
screen.hideCursor();
|
|
60
64
|
scope.removeAllListeners('data');
|
|
61
65
|
scope.release();
|
|
62
66
|
} catch (error) {
|
|
@@ -85,7 +89,7 @@ function getPasswordInput(prompt) {
|
|
|
85
89
|
case '\r': // Enter (CR)
|
|
86
90
|
case '\n': // Line Feed (LF)
|
|
87
91
|
case '\r\n': // CRLF
|
|
88
|
-
|
|
92
|
+
screen.write('\n');
|
|
89
93
|
cleanup();
|
|
90
94
|
resolve(password);
|
|
91
95
|
return;
|
|
@@ -95,7 +99,7 @@ function getPasswordInput(prompt) {
|
|
|
95
99
|
if (password.length > 0) {
|
|
96
100
|
password = password.slice(0, -1);
|
|
97
101
|
// Clear the last asterisk
|
|
98
|
-
|
|
102
|
+
screen.write('\b \b');
|
|
99
103
|
}
|
|
100
104
|
return;
|
|
101
105
|
|
|
@@ -114,7 +118,7 @@ function getPasswordInput(prompt) {
|
|
|
114
118
|
// Filter out control characters (only accept ASCII printable)
|
|
115
119
|
if (charCode >= 32 && charCode < 127) {
|
|
116
120
|
password += char;
|
|
117
|
-
|
|
121
|
+
screen.write('*');
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
return;
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
const { getPasswordInput } = require('./password-input');
|
|
7
7
|
const colors = require('../ui/colors');
|
|
8
8
|
const { validatePasswordStrength, getPasswordRequirements, generatePasswordExample } = require('./password-strength');
|
|
9
|
+
const { waitForKey } = require('../ui/prompts');
|
|
9
10
|
const i18n = require('../i18n');
|
|
11
|
+
const screen = require('../ui/screen');
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Force cleanup stdin state to prevent navigation issues
|
|
@@ -43,36 +45,70 @@ function getTranslatedStrength(strength) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* @
|
|
48
|
+
* Unified password guard for protected operations
|
|
49
|
+
* Mode A (delete/edit): clears screen, shows header, prompts password
|
|
50
|
+
* Mode B (export/import): no clear, no header, just prompts password
|
|
51
|
+
* @param {Object} apiManager - ApiManager instance
|
|
52
|
+
* @param {string} operation - 'delete' | 'edit' | 'export' | 'import'
|
|
53
|
+
* @returns {Promise<boolean>} true if authorized, false if denied/cancelled
|
|
50
54
|
*/
|
|
51
|
-
async function
|
|
55
|
+
async function passwordGuard(apiManager, operation) {
|
|
56
|
+
const hasPassword = apiManager.hasExportPassword();
|
|
57
|
+
|
|
58
|
+
// Mode A: delete/edit — no password means allow freely
|
|
59
|
+
if ((operation === 'delete' || operation === 'edit') && !hasPassword) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Mode B: export/import — no password means block (defense-in-depth)
|
|
64
|
+
if ((operation === 'export' || operation === 'import') && !hasPassword) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Mode A: clear screen and show header
|
|
69
|
+
if (operation === 'delete' || operation === 'edit') {
|
|
70
|
+
const headerKey = `password.guard.${operation}.header`;
|
|
71
|
+
screen.render([
|
|
72
|
+
'',
|
|
73
|
+
colors.bright + colors.orange + i18n.tSync(headerKey) + colors.reset,
|
|
74
|
+
'',
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
try {
|
|
53
79
|
const password = await getPasswordInput(i18n.tSync('messages.prompts.enter_password'));
|
|
54
80
|
|
|
81
|
+
// Empty password check
|
|
55
82
|
if (!password) {
|
|
56
83
|
forceStdinCleanup();
|
|
57
|
-
|
|
84
|
+
screen.write(colors.red + i18n.tSync('errors.password.empty') + colors.reset + '\n');
|
|
85
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
58
86
|
return false;
|
|
59
87
|
}
|
|
60
88
|
|
|
89
|
+
// Verify password
|
|
61
90
|
if (!apiManager.verifyExportPassword(password)) {
|
|
62
91
|
forceStdinCleanup();
|
|
63
|
-
|
|
64
|
-
|
|
92
|
+
screen.write(colors.red + '❌ ' + i18n.tSync('errors.password.verification_failed') + colors.reset + '\n');
|
|
93
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
65
94
|
return false;
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
return true;
|
|
69
98
|
} catch (error) {
|
|
70
99
|
forceStdinCleanup();
|
|
71
|
-
if (error.message
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
if (error.message === 'Password input cancelled') {
|
|
101
|
+
// Esc — silent return
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (error.message.includes('Ctrl+C')) {
|
|
105
|
+
// Delegate to stdinManager double-tap exit
|
|
106
|
+
const stdinManager = require('../utils/stdin-manager');
|
|
107
|
+
stdinManager.handleCtrlC();
|
|
108
|
+
return false;
|
|
75
109
|
}
|
|
110
|
+
// Unexpected error — treat as failure
|
|
111
|
+
screen.write(colors.red + `❌ ${error.message}` + colors.reset + '\n');
|
|
76
112
|
return false;
|
|
77
113
|
}
|
|
78
114
|
}
|
|
@@ -88,13 +124,13 @@ async function verifyCurrentPassword(apiManager) {
|
|
|
88
124
|
|
|
89
125
|
if (!currentPassword) {
|
|
90
126
|
forceStdinCleanup();
|
|
91
|
-
|
|
127
|
+
screen.write(colors.red + i18n.tSync('errors.password.empty') + colors.reset + '\n');
|
|
92
128
|
return false;
|
|
93
129
|
}
|
|
94
130
|
|
|
95
131
|
if (!apiManager.verifyExportPassword(currentPassword)) {
|
|
96
132
|
forceStdinCleanup();
|
|
97
|
-
|
|
133
|
+
screen.write(colors.red + '❌ ' + i18n.tSync('errors.password.current_incorrect') + colors.reset + '\n');
|
|
98
134
|
return false;
|
|
99
135
|
}
|
|
100
136
|
|
|
@@ -102,9 +138,9 @@ async function verifyCurrentPassword(apiManager) {
|
|
|
102
138
|
} catch (error) {
|
|
103
139
|
forceStdinCleanup();
|
|
104
140
|
if (error.message.includes('cancelled')) {
|
|
105
|
-
|
|
141
|
+
screen.write(colors.yellow + '\n' + i18n.tSync('errors.password.verification_cancelled') + colors.reset + '\n');
|
|
106
142
|
} else {
|
|
107
|
-
|
|
143
|
+
screen.write(colors.red + `❌ Password verification error: ${error.message}` + colors.reset + '\n');
|
|
108
144
|
}
|
|
109
145
|
return false;
|
|
110
146
|
}
|
|
@@ -118,24 +154,23 @@ async function verifyCurrentPassword(apiManager) {
|
|
|
118
154
|
*/
|
|
119
155
|
async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
120
156
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.log('');
|
|
157
|
+
const titleLine = colors.cyan + (isFirstTime ? i18n.tSync('password.setup.title') : i18n.tSync('password.setup.change_title')) + colors.reset;
|
|
158
|
+
const requirements = getPasswordRequirements();
|
|
124
159
|
|
|
160
|
+
const headerLines = ['', titleLine, ''];
|
|
125
161
|
if (!isFirstTime) {
|
|
126
|
-
|
|
127
|
-
|
|
162
|
+
headerLines.push(colors.yellow + '⚠️ ' + i18n.tSync('password.setup.warning') + colors.reset);
|
|
163
|
+
headerLines.push('');
|
|
128
164
|
}
|
|
129
|
-
|
|
130
|
-
// Display password requirements
|
|
131
|
-
console.log(colors.cyan + i18n.tSync('ui.general.password_requirements_title') + colors.reset);
|
|
132
|
-
const requirements = getPasswordRequirements();
|
|
165
|
+
headerLines.push(colors.cyan + i18n.tSync('ui.general.password_requirements_title') + colors.reset);
|
|
133
166
|
requirements.forEach(req => {
|
|
134
|
-
|
|
167
|
+
headerLines.push(colors.gray + ' ' + req + colors.reset);
|
|
135
168
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
headerLines.push('');
|
|
170
|
+
headerLines.push(colors.gray + i18n.tSync('ui.general.example_strong_password', generatePasswordExample()) + colors.reset);
|
|
171
|
+
headerLines.push('');
|
|
172
|
+
|
|
173
|
+
screen.render(headerLines);
|
|
139
174
|
|
|
140
175
|
let attempts = 0;
|
|
141
176
|
const maxAttempts = 3;
|
|
@@ -149,9 +184,9 @@ async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
|
149
184
|
|
|
150
185
|
if (!newPassword) {
|
|
151
186
|
forceStdinCleanup();
|
|
152
|
-
|
|
187
|
+
screen.write(colors.red + i18n.tSync('errors.password.empty') + colors.reset + '\n');
|
|
153
188
|
if (attempts < maxAttempts) {
|
|
154
|
-
|
|
189
|
+
screen.write('\n');
|
|
155
190
|
continue;
|
|
156
191
|
} else {
|
|
157
192
|
return false;
|
|
@@ -162,27 +197,27 @@ async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
|
162
197
|
const validation = validatePasswordStrength(newPassword);
|
|
163
198
|
|
|
164
199
|
if (!validation.valid) {
|
|
165
|
-
|
|
200
|
+
screen.write(colors.red + '❌ ' + i18n.tSync('errors.password.requirements_not_met') + colors.reset + '\n');
|
|
166
201
|
validation.errors.forEach(error => {
|
|
167
|
-
|
|
202
|
+
screen.write(colors.red + ' • ' + error + colors.reset + '\n');
|
|
168
203
|
});
|
|
169
204
|
|
|
170
205
|
if (validation.suggestions.length > 0) {
|
|
171
|
-
|
|
206
|
+
screen.write(colors.yellow + '💡 ' + i18n.tSync('ui.general.suggestions') + colors.reset + '\n');
|
|
172
207
|
validation.suggestions.forEach(suggestion => {
|
|
173
|
-
|
|
208
|
+
screen.write(colors.yellow + ' • ' + suggestion + colors.reset + '\n');
|
|
174
209
|
});
|
|
175
210
|
}
|
|
176
211
|
// If password is invalid, force display strength as "Weak" regardless of technical score
|
|
177
212
|
const strengthKey = validation.valid ? validation.strength : 'Weak';
|
|
178
213
|
const translatedStrength = getTranslatedStrength(strengthKey);
|
|
179
|
-
|
|
214
|
+
screen.write(colors.gray + i18n.tSync('ui.general.current_password_strength', translatedStrength) + colors.reset + '\n');
|
|
180
215
|
|
|
181
216
|
if (attempts < maxAttempts) {
|
|
182
|
-
|
|
217
|
+
screen.write('\n');
|
|
183
218
|
continue;
|
|
184
219
|
} else {
|
|
185
|
-
|
|
220
|
+
screen.write(colors.red + i18n.tSync('ui.general.max_attempts_password_failed') + colors.reset + '\n');
|
|
186
221
|
return false;
|
|
187
222
|
}
|
|
188
223
|
}
|
|
@@ -192,9 +227,9 @@ async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
|
192
227
|
|
|
193
228
|
if (newPassword !== confirmPassword) {
|
|
194
229
|
forceStdinCleanup();
|
|
195
|
-
|
|
230
|
+
screen.write(colors.red + i18n.tSync('ui.general.passwords_mismatch') + colors.reset + '\n');
|
|
196
231
|
if (attempts < maxAttempts) {
|
|
197
|
-
|
|
232
|
+
screen.write('\n');
|
|
198
233
|
continue;
|
|
199
234
|
} else {
|
|
200
235
|
return false;
|
|
@@ -203,8 +238,8 @@ async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
|
203
238
|
|
|
204
239
|
// Success - set the password
|
|
205
240
|
apiManager.setExportPassword(newPassword);
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
screen.write('\n');
|
|
242
|
+
screen.write(colors.green + '✓ ' + i18n.tSync('password.setup.password_success', getTranslatedStrength(validation.strength)) + colors.reset + '\n');
|
|
208
243
|
return true;
|
|
209
244
|
}
|
|
210
245
|
|
|
@@ -213,9 +248,9 @@ async function setupNewPassword(apiManager, isFirstTime = false) {
|
|
|
213
248
|
} catch (error) {
|
|
214
249
|
forceStdinCleanup();
|
|
215
250
|
if (error.message.includes('cancelled')) {
|
|
216
|
-
|
|
251
|
+
screen.write(colors.yellow + '\n' + i18n.tSync('errors.password.setup_cancelled') + colors.reset + '\n');
|
|
217
252
|
} else {
|
|
218
|
-
|
|
253
|
+
screen.write(colors.red + `❌ Failed to set password: ${error.message}` + colors.reset + '\n');
|
|
219
254
|
}
|
|
220
255
|
return false;
|
|
221
256
|
}
|
|
@@ -234,21 +269,21 @@ async function changePassword(apiManager) {
|
|
|
234
269
|
return false;
|
|
235
270
|
}
|
|
236
271
|
|
|
237
|
-
|
|
238
|
-
|
|
272
|
+
screen.write(colors.green + i18n.tSync('errors.password.current_password_verified') + colors.reset + '\n');
|
|
273
|
+
screen.write('\n');
|
|
239
274
|
|
|
240
275
|
// Set new password
|
|
241
276
|
return await setupNewPassword(apiManager, false);
|
|
242
277
|
|
|
243
278
|
} catch (error) {
|
|
244
279
|
forceStdinCleanup();
|
|
245
|
-
|
|
280
|
+
screen.write(colors.red + `❌ Password change failed: ${error.message}` + colors.reset + '\n');
|
|
246
281
|
return false;
|
|
247
282
|
}
|
|
248
283
|
}
|
|
249
284
|
|
|
250
285
|
module.exports = {
|
|
251
|
-
|
|
286
|
+
passwordGuard,
|
|
252
287
|
verifyCurrentPassword,
|
|
253
288
|
setupNewPassword,
|
|
254
289
|
changePassword
|