@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
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Editor Module — Edit API field-by-field with validation
|
|
3
|
+
* Flow: Select API → Field Menu → Edit Single Field → Save
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const colors = require('./colors');
|
|
7
|
+
const Menu = require('./menu');
|
|
8
|
+
const screen = require('./screen');
|
|
9
|
+
const { showApiSelectionTable } = require('./interactive-table');
|
|
10
|
+
const { simpleInput, waitForKey, selectProvider } = require('./prompts');
|
|
11
|
+
const { getProvider, detectProvider, getSuggestedModels } = require('../presets/providers');
|
|
12
|
+
const { truncateStringToWidth, getStringWidth, padStringToWidth } = require('../utils/string-width');
|
|
13
|
+
const i18n = require('../i18n');
|
|
14
|
+
|
|
15
|
+
const FIELD_VALUE_MAX_WIDTH = 30;
|
|
16
|
+
const HINT_PROVIDER_NAME_MAX_WIDTH = 20;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve provider id to display name
|
|
20
|
+
* @param {string} providerId
|
|
21
|
+
* @returns {string} Display name or raw id as fallback
|
|
22
|
+
*/
|
|
23
|
+
function resolveProviderName(providerId) {
|
|
24
|
+
const provider = getProvider(providerId);
|
|
25
|
+
return provider ? provider.name : providerId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build field menu options with current values
|
|
30
|
+
*/
|
|
31
|
+
function buildFieldMenuOptions(api) {
|
|
32
|
+
const fields = [
|
|
33
|
+
{ key: 'name', label: i18n.tSync('api.edit.field_name'), value: api.name || '' },
|
|
34
|
+
{ key: 'provider', label: i18n.tSync('api.edit.field_provider'), value: resolveProviderName(api.provider) },
|
|
35
|
+
{ key: 'baseUrl', label: i18n.tSync('api.edit.field_base_url'), value: api.baseUrl || '' },
|
|
36
|
+
{ key: 'model', label: i18n.tSync('api.edit.field_model'), value: api.model || '' }
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const maxLabelWidth = Math.max(...fields.map(f => getStringWidth(f.label)));
|
|
40
|
+
const options = fields.map(f => {
|
|
41
|
+
const paddedLabel = padStringToWidth(f.label, maxLabelWidth);
|
|
42
|
+
const truncatedValue = truncateStringToWidth(f.value, FIELD_VALUE_MAX_WIDTH);
|
|
43
|
+
return `${paddedLabel} ${truncatedValue}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
options.push(i18n.tSync('api.edit.back'));
|
|
47
|
+
return options;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build hintCallback for field menu — shows mismatch warning or blank
|
|
52
|
+
*/
|
|
53
|
+
function buildFieldMenuHintCallback(api) {
|
|
54
|
+
return function (_selectedIndex) {
|
|
55
|
+
const detected = detectProvider(api.baseUrl);
|
|
56
|
+
if (detected !== api.provider) {
|
|
57
|
+
const currentName = truncateStringToWidth(resolveProviderName(api.provider), HINT_PROVIDER_NAME_MAX_WIDTH);
|
|
58
|
+
const detectedName = truncateStringToWidth(resolveProviderName(detected), HINT_PROVIDER_NAME_MAX_WIDTH);
|
|
59
|
+
const line1 = i18n.tSync('api.edit.provider_url_mismatch');
|
|
60
|
+
const line2 = i18n.tSync('api.edit.provider_url_mismatch_detail', currentName, detectedName);
|
|
61
|
+
return `${line1}\n${line2}\n\n`;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Edit a single text field (name, baseUrl, model)
|
|
69
|
+
*/
|
|
70
|
+
async function editTextField(apiManager, api, fieldKey, fieldLabel) {
|
|
71
|
+
const currentValue = api[fieldKey] || '';
|
|
72
|
+
|
|
73
|
+
const headerLines = [
|
|
74
|
+
'',
|
|
75
|
+
colors.cyan + i18n.tSync('api.edit.current_value', currentValue) + colors.reset,
|
|
76
|
+
'',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// Show suggested models for model field
|
|
80
|
+
if (fieldKey === 'model') {
|
|
81
|
+
const suggested = getSuggestedModels(api.provider);
|
|
82
|
+
if (suggested.length > 0) {
|
|
83
|
+
headerLines.push(colors.gray + ' Suggested models: ' + suggested.join(', ') + colors.reset);
|
|
84
|
+
headerLines.push('');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
screen.render(headerLines);
|
|
89
|
+
|
|
90
|
+
const input = await simpleInput(colors.green + i18n.tSync('api.edit.new_value') + colors.reset);
|
|
91
|
+
|
|
92
|
+
// Cancel check
|
|
93
|
+
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
94
|
+
screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
|
|
95
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Attempt save
|
|
100
|
+
try {
|
|
101
|
+
const updated = apiManager.updateApiField(api.id, fieldKey, input);
|
|
102
|
+
|
|
103
|
+
// Success message
|
|
104
|
+
screen.write(colors.green + i18n.tSync('api.edit.success', fieldLabel) + colors.reset + '\n');
|
|
105
|
+
|
|
106
|
+
// baseUrl mismatch warning on same screen
|
|
107
|
+
if (fieldKey === 'baseUrl') {
|
|
108
|
+
const detected = detectProvider(input);
|
|
109
|
+
if (detected !== updated.provider) {
|
|
110
|
+
const detectedName = resolveProviderName(detected);
|
|
111
|
+
const currentName = resolveProviderName(updated.provider);
|
|
112
|
+
screen.write(colors.yellow + i18n.tSync('api.edit.url_provider_hint', detectedName, currentName) + colors.reset + '\n');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
117
|
+
return updated;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
|
|
120
|
+
// Re-prompt — stay in field edit
|
|
121
|
+
return await editTextField(apiManager, api, fieldKey, fieldLabel);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Edit provider field via selectProvider() helper
|
|
127
|
+
*/
|
|
128
|
+
async function editProviderField(apiManager, api) {
|
|
129
|
+
screen.render([
|
|
130
|
+
'',
|
|
131
|
+
colors.cyan + i18n.tSync('api.edit.current_value', resolveProviderName(api.provider)) + colors.reset,
|
|
132
|
+
'',
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const result = await selectProvider({ title: null, showNote: false });
|
|
136
|
+
|
|
137
|
+
if (!result) {
|
|
138
|
+
screen.write(colors.yellow + i18n.tSync('api.edit.cancelled') + colors.reset + '\n');
|
|
139
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const updated = apiManager.updateApiField(api.id, 'provider', result.id);
|
|
145
|
+
screen.write(colors.green + i18n.tSync('api.edit.success', i18n.tSync('api.edit.field_provider')) + colors.reset + '\n');
|
|
146
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
147
|
+
return updated;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
screen.write(colors.red + '❌ ' + error.message + colors.reset + '\n');
|
|
150
|
+
await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Main edit API flow
|
|
157
|
+
* @param {Object} apiManager - ApiManager instance
|
|
158
|
+
*/
|
|
159
|
+
async function editApi(apiManager) {
|
|
160
|
+
// Step 1: Select API
|
|
161
|
+
const apis = apiManager.getApis();
|
|
162
|
+
const selectedApi = await showApiSelectionTable(
|
|
163
|
+
apis,
|
|
164
|
+
i18n.tSync('api.edit.select_api'),
|
|
165
|
+
'edit'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
if (!selectedApi) return;
|
|
169
|
+
|
|
170
|
+
// Step 2: Field menu loop
|
|
171
|
+
let currentApi = selectedApi;
|
|
172
|
+
const fieldMenu = new Menu();
|
|
173
|
+
|
|
174
|
+
while (true) {
|
|
175
|
+
fieldMenu.setOptions(buildFieldMenuOptions(currentApi));
|
|
176
|
+
const hintCallback = buildFieldMenuHintCallback(currentApi);
|
|
177
|
+
const choice = await fieldMenu.navigate(null, hintCallback);
|
|
178
|
+
|
|
179
|
+
// Esc or Back
|
|
180
|
+
if (choice === -1 || choice === 4) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 3: Edit selected field
|
|
185
|
+
const fieldKeys = ['name', 'provider', 'baseUrl', 'model'];
|
|
186
|
+
const fieldLabels = [
|
|
187
|
+
i18n.tSync('api.edit.field_name'),
|
|
188
|
+
i18n.tSync('api.edit.field_provider'),
|
|
189
|
+
i18n.tSync('api.edit.field_base_url'),
|
|
190
|
+
i18n.tSync('api.edit.field_model')
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
let updated = null;
|
|
194
|
+
if (choice === 1) {
|
|
195
|
+
// Provider — uses selectProvider() not simpleInput()
|
|
196
|
+
updated = await editProviderField(apiManager, currentApi);
|
|
197
|
+
} else if (choice >= 0 && choice < 4) {
|
|
198
|
+
updated = await editTextField(apiManager, currentApi, fieldKeys[choice], fieldLabels[choice]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (updated) {
|
|
202
|
+
currentApi = updated;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
editApi,
|
|
209
|
+
resolveProviderName
|
|
210
|
+
};
|
|
@@ -3,70 +3,156 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
const colors = require('./colors');
|
|
6
|
+
const screen = require('./screen');
|
|
6
7
|
const { maskApiToken } = require('../validators');
|
|
7
8
|
const { decrypt } = require('../crypto');
|
|
8
9
|
const i18n = require('../i18n');
|
|
9
10
|
const { padStringToWidth } = require('../utils/string-width');
|
|
10
11
|
const stdinManager = require('../utils/stdin-manager');
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Calculate pagination parameters from terminal dimensions and API count.
|
|
15
|
+
* Pure function — no side effects, no I/O.
|
|
16
|
+
*/
|
|
17
|
+
function calculatePagination(apiCount, terminalRows, isSwitchWithActive, isLegacyOverflow = false) {
|
|
18
|
+
const warningLine = isLegacyOverflow ? 1 : 0;
|
|
19
|
+
const fixedOverhead = (isSwitchWithActive ? 16 : 10) + warningLine;
|
|
20
|
+
const linesPerItem = 7;
|
|
21
|
+
const itemsPerPage = Math.max(1, Math.floor((terminalRows - fixedOverhead) / linesPerItem));
|
|
22
|
+
const totalPages = Math.ceil(apiCount / itemsPerPage);
|
|
23
|
+
return { itemsPerPage, totalPages };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize pagination state: current page and per-page selection memory.
|
|
28
|
+
* For switch mode, starts on the page containing activeIndex.
|
|
29
|
+
*/
|
|
30
|
+
function initPaginationState(itemsPerPage, totalPages, activeIndex, actionType, apiCount) {
|
|
31
|
+
const pageSelections = new Array(totalPages).fill(0);
|
|
32
|
+
let currentPage = 0;
|
|
33
|
+
|
|
34
|
+
if (actionType === 'switch' && activeIndex >= 0 && activeIndex < apiCount) {
|
|
35
|
+
currentPage = Math.floor(activeIndex / itemsPerPage);
|
|
36
|
+
pageSelections[currentPage] = activeIndex - currentPage * itemsPerPage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { currentPage, pageSelections };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Pure state transition for page key presses.
|
|
44
|
+
* Returns updated state for navigation keys, or action object for enter/escape.
|
|
45
|
+
*/
|
|
46
|
+
function handlePageKeyPress(key, state) {
|
|
47
|
+
const { currentPage, pageSelections, itemsPerPage, totalPages, apiCount } = state;
|
|
48
|
+
const pageItemCount = Math.min(itemsPerPage, apiCount - currentPage * itemsPerPage);
|
|
49
|
+
const newSelections = [...pageSelections];
|
|
50
|
+
|
|
51
|
+
switch (key) {
|
|
52
|
+
case 'right':
|
|
53
|
+
if (totalPages <= 1) return state;
|
|
54
|
+
return { ...state, currentPage: (currentPage + 1) % totalPages, pageSelections: newSelections };
|
|
55
|
+
case 'left':
|
|
56
|
+
if (totalPages <= 1) return state;
|
|
57
|
+
return { ...state, currentPage: (currentPage - 1 + totalPages) % totalPages, pageSelections: newSelections };
|
|
58
|
+
case 'up':
|
|
59
|
+
newSelections[currentPage] = (newSelections[currentPage] - 1 + pageItemCount) % pageItemCount;
|
|
60
|
+
return { ...state, pageSelections: newSelections };
|
|
61
|
+
case 'down':
|
|
62
|
+
newSelections[currentPage] = (newSelections[currentPage] + 1) % pageItemCount;
|
|
63
|
+
return { ...state, pageSelections: newSelections };
|
|
64
|
+
case 'enter':
|
|
65
|
+
return { action: 'select', globalIndex: currentPage * itemsPerPage + newSelections[currentPage] };
|
|
66
|
+
case 'escape':
|
|
67
|
+
return { action: 'cancel' };
|
|
68
|
+
default:
|
|
69
|
+
return state;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
12
73
|
/**
|
|
13
74
|
* Display simple interactive table for API selection
|
|
14
75
|
*/
|
|
15
76
|
async function showApiSelectionTable(apis, title, actionType = 'select', activeIndex = -1, apiManager = null) {
|
|
16
77
|
if (apis.length === 0) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
78
|
+
screen.render([
|
|
79
|
+
'',
|
|
80
|
+
colors.yellow + 'ℹ️ ' + i18n.tSync('messages.info.no_apis_info_title') + colors.reset,
|
|
81
|
+
colors.gray + ' ' + i18n.tSync('messages.info.apis_removed_or_none') + colors.reset,
|
|
82
|
+
'',
|
|
83
|
+
colors.gray + i18n.tSync('ui.general.press_any_key_continue') + colors.reset,
|
|
84
|
+
]);
|
|
23
85
|
|
|
24
86
|
await waitForKeyPress();
|
|
25
87
|
return null;
|
|
26
88
|
}
|
|
27
89
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
90
|
+
// Legacy >99 guard: truncate display set, keep original refs
|
|
91
|
+
const isLegacyOverflow = apis.length > 99;
|
|
92
|
+
const displayApis = isLegacyOverflow ? apis.slice(0, 99) : apis;
|
|
93
|
+
|
|
94
|
+
// Recalculate activeIndex against displayApis
|
|
95
|
+
let effectiveActiveIndex = activeIndex;
|
|
96
|
+
if (isLegacyOverflow && activeIndex >= 99) effectiveActiveIndex = -1;
|
|
97
|
+
|
|
98
|
+
// Snapshot terminal height once — no resize handling during interaction
|
|
99
|
+
const rows = process.stdout.rows || 30;
|
|
100
|
+
const isSwitchWithActive = actionType === 'switch' && effectiveActiveIndex >= 0 && effectiveActiveIndex < displayApis.length;
|
|
101
|
+
const { itemsPerPage, totalPages } = calculatePagination(displayApis.length, rows, isSwitchWithActive, isLegacyOverflow);
|
|
102
|
+
let paginationState = initPaginationState(itemsPerPage, totalPages, effectiveActiveIndex, actionType, displayApis.length);
|
|
103
|
+
let currentPage = paginationState.currentPage;
|
|
104
|
+
let pageSelections = paginationState.pageSelections;
|
|
32
105
|
|
|
33
106
|
function displaySimpleTable() {
|
|
107
|
+
const lines = [];
|
|
108
|
+
|
|
34
109
|
// Header info
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
110
|
+
lines.push('');
|
|
111
|
+
lines.push(colors.cyan + title + colors.reset);
|
|
112
|
+
lines.push('');
|
|
113
|
+
|
|
114
|
+
// Legacy overflow warning
|
|
115
|
+
if (isLegacyOverflow) {
|
|
116
|
+
lines.push(colors.yellow + ` ⚠ Showing first 99 of ${apis.length} APIs` + colors.reset);
|
|
117
|
+
}
|
|
38
118
|
|
|
39
119
|
// Show current active API for switch mode
|
|
40
|
-
if (
|
|
41
|
-
const activeApi =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
120
|
+
if (isSwitchWithActive) {
|
|
121
|
+
const activeApi = displayApis[effectiveActiveIndex];
|
|
122
|
+
lines.push(colors.gray + i18n.tSync('ui.general.currently_active_api') + colors.reset);
|
|
123
|
+
lines.push(colors.gray + ` Name: ${activeApi.name}` + colors.reset);
|
|
124
|
+
lines.push(colors.gray + ` Provider: ${activeApi.provider}` + colors.reset);
|
|
125
|
+
lines.push(colors.gray + ` Usage Count: ${activeApi.usageCount || 0}` + colors.reset);
|
|
126
|
+
lines.push('');
|
|
47
127
|
}
|
|
48
128
|
|
|
129
|
+
// Current page slice
|
|
130
|
+
const startIdx = currentPage * itemsPerPage;
|
|
131
|
+
const endIdx = Math.min(startIdx + itemsPerPage, displayApis.length);
|
|
132
|
+
const pageApis = displayApis.slice(startIdx, endIdx);
|
|
133
|
+
|
|
49
134
|
// Table header with 3-column layout
|
|
50
|
-
|
|
135
|
+
lines.push(colors.bright + colors.orange +
|
|
51
136
|
'┌────┬─────────────────────────┬────────────────────────────────────────────────────────────────────────┐' + colors.reset);
|
|
52
|
-
|
|
137
|
+
lines.push(colors.bright + colors.orange +
|
|
53
138
|
'│ No.│ Name │ Detail │' + colors.reset);
|
|
54
|
-
|
|
139
|
+
lines.push(colors.bright + colors.orange +
|
|
55
140
|
'├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
|
|
56
141
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
const
|
|
142
|
+
// Multi-row display loop — iterate page slice
|
|
143
|
+
pageApis.forEach((api, localIndex) => {
|
|
144
|
+
const globalIndex = startIdx + localIndex;
|
|
145
|
+
const num = (globalIndex + 1).toString().padStart(2, ' ');
|
|
60
146
|
|
|
61
|
-
//
|
|
62
|
-
const isActiveApi =
|
|
147
|
+
// Active marker uses global index
|
|
148
|
+
const isActiveApi = effectiveActiveIndex === globalIndex;
|
|
63
149
|
const activeMarker = isActiveApi ? '●' : ' ';
|
|
64
150
|
|
|
65
151
|
// Format name with active marker
|
|
66
152
|
const nameWithMarker = `${activeMarker} ${api.name}`;
|
|
67
153
|
const displayName = nameWithMarker.padEnd(23, ' ');
|
|
68
154
|
|
|
69
|
-
//
|
|
155
|
+
// Decrypt and mask token for display
|
|
70
156
|
const decryptedToken = decrypt(api.authToken);
|
|
71
157
|
const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
|
|
72
158
|
|
|
@@ -83,48 +169,55 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
83
169
|
// Pad each detail line to exactly 70 characters
|
|
84
170
|
const paddedDetails = details.map(detail => padStringToWidth(detail, 70));
|
|
85
171
|
|
|
86
|
-
//
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
172
|
+
// Selection highlight uses local index
|
|
173
|
+
const isSelected = localIndex === pageSelections[currentPage];
|
|
174
|
+
const nameColor = isActiveApi ? colors.green : (isSelected ? colors.white : colors.gray);
|
|
175
|
+
const detailColor = isActiveApi ? colors.green : (isSelected ? colors.white : colors.gray);
|
|
176
|
+
const bgColor = isSelected ? colors.bgAmber : '';
|
|
177
|
+
const textBg = isSelected ? colors.black : '';
|
|
91
178
|
|
|
92
179
|
// Display 6 rows for each API, with No. and Name centered on row 3 (index 2)
|
|
93
180
|
for (let i = 0; i < paddedDetails.length; i++) {
|
|
94
181
|
if (i === 2) {
|
|
95
182
|
// Middle row (3rd row) - show No. and Name for vertical centering
|
|
96
|
-
|
|
183
|
+
lines.push(colors.orange + '│' + textBg + bgColor + nameColor +
|
|
97
184
|
` ${num} ` + colors.reset + colors.orange + '│' + textBg + bgColor + nameColor +
|
|
98
185
|
` ${displayName} ` + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
|
|
99
186
|
` ${paddedDetails[i]} ` + colors.reset + colors.orange + '│' + colors.reset);
|
|
100
187
|
} else {
|
|
101
188
|
// Other rows - empty No. and Name columns
|
|
102
|
-
|
|
189
|
+
lines.push(colors.orange + '│' + textBg + bgColor + colors.gray +
|
|
103
190
|
' ' + colors.reset + colors.orange + '│' + textBg + bgColor + colors.gray +
|
|
104
191
|
' ' + colors.reset + colors.orange + '│' + textBg + bgColor + detailColor +
|
|
105
192
|
' ' + paddedDetails[i] + ' ' + colors.reset + colors.orange + '│' + colors.reset);
|
|
106
193
|
}
|
|
107
194
|
}
|
|
108
195
|
|
|
109
|
-
// Add separator line after each API except the last one
|
|
110
|
-
if (
|
|
111
|
-
|
|
196
|
+
// Add separator line after each API except the last one on this page
|
|
197
|
+
if (localIndex < pageApis.length - 1) {
|
|
198
|
+
lines.push(colors.bright + colors.orange +
|
|
112
199
|
'├────┼─────────────────────────┼────────────────────────────────────────────────────────────────────────┤' + colors.reset);
|
|
113
200
|
}
|
|
114
201
|
});
|
|
115
202
|
|
|
116
|
-
|
|
203
|
+
lines.push(colors.bright + colors.orange +
|
|
117
204
|
'└────┴─────────────────────────┴────────────────────────────────────────────────────────────────────────┘' + colors.reset);
|
|
118
|
-
|
|
205
|
+
lines.push('');
|
|
119
206
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
207
|
+
if (isSwitchWithActive) {
|
|
208
|
+
lines.push(colors.green + ' ● = ' + i18n.tSync('ui.general.currently_active_api') + colors.reset);
|
|
122
209
|
}
|
|
123
210
|
|
|
124
|
-
//
|
|
125
|
-
const actionText =
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
// Navigation hint — pagination-aware
|
|
212
|
+
const actionText = i18n.tSync(`navigation.action.${actionType}`);
|
|
213
|
+
if (totalPages > 1) {
|
|
214
|
+
lines.push(colors.amber + ' ' + i18n.tSync('navigation.use_arrows_page_esc', (currentPage + 1).toString(), totalPages.toString(), actionText) + colors.reset);
|
|
215
|
+
} else {
|
|
216
|
+
lines.push(colors.amber + ' ' + i18n.tSync('navigation.use_arrows_esc', actionText) + colors.reset);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
screen.render(lines);
|
|
128
221
|
}
|
|
129
222
|
|
|
130
223
|
function handleKeyPress(key) {
|
|
@@ -140,33 +233,48 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
140
233
|
// Continue to process this key normally
|
|
141
234
|
}
|
|
142
235
|
|
|
236
|
+
// Map raw key codes to logical key names
|
|
237
|
+
let keyName;
|
|
143
238
|
switch (key) {
|
|
144
|
-
case '\u001b[
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case '\u001b[B': // Down arrow
|
|
151
|
-
selectedIndex = (selectedIndex + 1) % apis.length;
|
|
152
|
-
console.clear(); // Force clear screen
|
|
153
|
-
displaySimpleTable();
|
|
154
|
-
break;
|
|
155
|
-
|
|
156
|
-
case '\r': // Enter
|
|
157
|
-
return apis[selectedIndex];
|
|
158
|
-
|
|
159
|
-
case '\u001b': // Escape
|
|
239
|
+
case '\u001b[C': keyName = 'right'; break;
|
|
240
|
+
case '\u001b[D': keyName = 'left'; break;
|
|
241
|
+
case '\u001b[A': keyName = 'up'; break;
|
|
242
|
+
case '\u001b[B': keyName = 'down'; break;
|
|
243
|
+
case '\r': keyName = 'enter'; break;
|
|
244
|
+
case '\u001b':
|
|
160
245
|
case 'q':
|
|
161
246
|
case 'Q':
|
|
162
|
-
|
|
247
|
+
keyName = 'escape'; break;
|
|
248
|
+
default:
|
|
249
|
+
return undefined;
|
|
163
250
|
}
|
|
251
|
+
|
|
252
|
+
const state = {
|
|
253
|
+
currentPage,
|
|
254
|
+
pageSelections,
|
|
255
|
+
itemsPerPage,
|
|
256
|
+
totalPages,
|
|
257
|
+
apiCount: displayApis.length,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = handlePageKeyPress(keyName, state);
|
|
261
|
+
|
|
262
|
+
if (result.action === 'select') {
|
|
263
|
+
return displayApis[result.globalIndex];
|
|
264
|
+
}
|
|
265
|
+
if (result.action === 'cancel') {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// State update — navigation key
|
|
270
|
+
currentPage = result.currentPage;
|
|
271
|
+
pageSelections = result.pageSelections;
|
|
272
|
+
displaySimpleTable();
|
|
164
273
|
return undefined;
|
|
165
274
|
}
|
|
166
275
|
|
|
167
276
|
return new Promise((resolve) => {
|
|
168
277
|
// Initial display
|
|
169
|
-
console.clear();
|
|
170
278
|
displaySimpleTable();
|
|
171
279
|
|
|
172
280
|
if (process.stdin.isTTY) {
|
|
@@ -187,23 +295,23 @@ async function showApiSelectionTable(apis, title, actionType = 'select', activeI
|
|
|
187
295
|
const selectedIndex = apis.findIndex(api => api.id === result.id);
|
|
188
296
|
const switchedApi = apiManager.setActiveApi(selectedIndex);
|
|
189
297
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(colors.gray + i18n.tSync('messages.prompts.press_any_key') + colors.reset);
|
|
298
|
+
screen.render([
|
|
299
|
+
'',
|
|
300
|
+
colors.bright + colors.green + `✓ ${i18n.tSync('messages.success.api_switched')}` + colors.reset,
|
|
301
|
+
colors.gray + ` ${i18n.tSync('api.actions.switch_success', switchedApi.name)}` + colors.reset,
|
|
302
|
+
colors.gray + ` ${i18n.tSync('api.details.provider')}: ${switchedApi.provider}` + colors.reset,
|
|
303
|
+
colors.gray + ` ${i18n.tSync('api.details.url')}: ${switchedApi.baseUrl}` + colors.reset,
|
|
304
|
+
colors.gray + ` ${i18n.tSync('api.details.model')}: ${switchedApi.model}` + colors.reset,
|
|
305
|
+
'',
|
|
306
|
+
colors.gray + i18n.tSync('messages.prompts.press_any_key') + colors.reset,
|
|
307
|
+
]);
|
|
201
308
|
await waitForKeyPress();
|
|
202
|
-
} else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
309
|
+
} else if (actionType !== 'edit') {
|
|
310
|
+
screen.render([
|
|
311
|
+
'',
|
|
312
|
+
colors.green + '✓ Selection completed: ' + (result ? result.name : 'Cancelled') + colors.reset,
|
|
313
|
+
'',
|
|
314
|
+
]);
|
|
207
315
|
}
|
|
208
316
|
|
|
209
317
|
resolve(result);
|
|
@@ -250,22 +358,24 @@ function waitForKeyPress() {
|
|
|
250
358
|
}
|
|
251
359
|
|
|
252
360
|
async function confirmDeletion(api) {
|
|
253
|
-
console.clear();
|
|
254
|
-
console.log('');
|
|
255
|
-
console.log(colors.red + colors.bright + '[!] ' + i18n.tSync('messages.prompts.confirm_deletion') + colors.reset);
|
|
256
|
-
console.log('');
|
|
257
|
-
console.log(colors.yellow + i18n.tSync('ui.general.confirm_delete_api') + colors.reset);
|
|
258
|
-
console.log('');
|
|
259
|
-
console.log(colors.gray + `Name: ${api.name}` + colors.reset);
|
|
260
|
-
console.log(colors.gray + `Provider: ${api.provider}` + colors.reset);
|
|
261
|
-
console.log(colors.gray + `Base URL: ${api.baseUrl}` + colors.reset);
|
|
262
|
-
console.log(colors.gray + `Model: ${api.model}` + colors.reset);
|
|
263
361
|
const decryptedToken = decrypt(api.authToken);
|
|
264
362
|
const displayToken = decryptedToken.success ? maskApiToken(decryptedToken.value) : '***ERROR***';
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
363
|
+
|
|
364
|
+
screen.render([
|
|
365
|
+
'',
|
|
366
|
+
colors.red + colors.bright + '[!] ' + i18n.tSync('messages.prompts.confirm_deletion') + colors.reset,
|
|
367
|
+
'',
|
|
368
|
+
colors.yellow + i18n.tSync('ui.general.confirm_delete_api') + colors.reset,
|
|
369
|
+
'',
|
|
370
|
+
colors.gray + `Name: ${api.name}` + colors.reset,
|
|
371
|
+
colors.gray + `Provider: ${api.provider}` + colors.reset,
|
|
372
|
+
colors.gray + `Base URL: ${api.baseUrl}` + colors.reset,
|
|
373
|
+
colors.gray + `Model: ${api.model}` + colors.reset,
|
|
374
|
+
colors.gray + `Token: ${displayToken}` + colors.reset,
|
|
375
|
+
'',
|
|
376
|
+
colors.red + i18n.tSync('ui.general.action_cannot_undone') + colors.reset,
|
|
377
|
+
'',
|
|
378
|
+
]);
|
|
269
379
|
|
|
270
380
|
// Use StdinManager for proper state management
|
|
271
381
|
const scope = stdinManager.acquire('line', {
|
|
@@ -277,7 +387,7 @@ async function confirmDeletion(api) {
|
|
|
277
387
|
try {
|
|
278
388
|
rl = scope.createReadline();
|
|
279
389
|
} catch (error) {
|
|
280
|
-
|
|
390
|
+
screen.debug('[ERROR] Failed to create readline interface: ' + error.message);
|
|
281
391
|
scope.release();
|
|
282
392
|
return false; // Default to not deleting if we can't get user confirmation
|
|
283
393
|
}
|
|
@@ -314,18 +424,22 @@ async function confirmDeletion(api) {
|
|
|
314
424
|
|
|
315
425
|
// Set a timeout to prevent infinite waiting
|
|
316
426
|
timeoutId = setTimeout(() => {
|
|
317
|
-
|
|
427
|
+
screen.write('\n' + colors.yellow + '[!] Confirmation timeout - operation cancelled' + colors.reset + '\n');
|
|
318
428
|
cleanup(false); // Timeout means no deletion
|
|
319
429
|
}, 60000); // 60 second timeout
|
|
320
430
|
|
|
431
|
+
screen.showCursor();
|
|
432
|
+
screen.setReadlineActive(true);
|
|
321
433
|
rl.question(colors.red + i18n.tSync('ui.general.confirm_deletion_prompt') + colors.reset, (answer) => {
|
|
434
|
+
screen.setReadlineActive(false);
|
|
435
|
+
screen.hideCursor();
|
|
322
436
|
const confirmed = answer.trim().toLowerCase() === 'y';
|
|
323
437
|
cleanup(confirmed);
|
|
324
438
|
});
|
|
325
439
|
|
|
326
440
|
// Handle readline errors
|
|
327
441
|
rl.on('error', (error) => {
|
|
328
|
-
|
|
442
|
+
screen.debug('[ERROR] Readline error: ' + error.message);
|
|
329
443
|
cleanup(false); // Error means no deletion
|
|
330
444
|
});
|
|
331
445
|
});
|
|
@@ -334,5 +448,8 @@ async function confirmDeletion(api) {
|
|
|
334
448
|
module.exports = {
|
|
335
449
|
showApiSelectionTable,
|
|
336
450
|
waitForKeyPress,
|
|
337
|
-
confirmDeletion
|
|
451
|
+
confirmDeletion,
|
|
452
|
+
calculatePagination,
|
|
453
|
+
initPaginationState,
|
|
454
|
+
handlePageKeyPress,
|
|
338
455
|
};
|