9router 0.2.49 → 0.2.50
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/README.md +0 -1
- package/app/.next/BUILD_ID +1 -1
- package/app/.next/app-build-manifest.json +78 -78
- package/app/.next/app-path-routes-manifest.json +18 -18
- package/app/.next/build-manifest.json +2 -2
- package/app/.next/prerender-manifest.json +36 -36
- package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/_not-found.html +1 -1
- package/app/.next/server/app/_not-found.rsc +1 -1
- package/app/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cli-tools/claude-settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cli-tools/codex-settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/auth/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/credentials/update/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/model/resolve/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/cloud/models/alias/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/combos/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/combos/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/keys/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/keys/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/models/alias/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/[provider]/[action]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/import/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/social-authorize/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/oauth/kiro/social-exchange/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/[id]/test/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/client/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/providers/validate/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/sync/cloud/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/sync/initialize/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/tags/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/load/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/save/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/send/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/translator/translate/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/usage/[connectionId]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/usage/history/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/api/chat/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/chat/completions/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/messages/count_tokens/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/messages/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/responses/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1beta/models/[...path]/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/api/v1beta/models/route_client-reference-manifest.js +1 -1
- package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/callback.html +1 -1
- package/app/.next/server/app/callback.rsc +1 -1
- package/app/.next/server/app/dashboard/cli-tools.html +1 -1
- package/app/.next/server/app/dashboard/cli-tools.rsc +1 -1
- package/app/.next/server/app/dashboard/combos.html +1 -1
- package/app/.next/server/app/dashboard/combos.rsc +1 -1
- package/app/.next/server/app/dashboard/endpoint.html +1 -1
- package/app/.next/server/app/dashboard/endpoint.rsc +1 -1
- package/app/.next/server/app/dashboard/profile.html +1 -1
- package/app/.next/server/app/dashboard/profile.rsc +1 -1
- package/app/.next/server/app/dashboard/providers/new.html +1 -1
- package/app/.next/server/app/dashboard/providers/new.rsc +1 -1
- package/app/.next/server/app/dashboard/providers.html +1 -1
- package/app/.next/server/app/dashboard/providers.rsc +1 -1
- package/app/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/dashboard/settings/pricing.html +1 -1
- package/app/.next/server/app/dashboard/settings/pricing.rsc +1 -1
- package/app/.next/server/app/dashboard/translator.html +1 -1
- package/app/.next/server/app/dashboard/translator.rsc +1 -1
- package/app/.next/server/app/dashboard/usage.html +1 -1
- package/app/.next/server/app/dashboard/usage.rsc +1 -1
- package/app/.next/server/app/dashboard.html +1 -1
- package/app/.next/server/app/dashboard.rsc +1 -1
- package/app/.next/server/app/index.html +1 -1
- package/app/.next/server/app/index.rsc +1 -1
- package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/landing.html +1 -1
- package/app/.next/server/app/landing.rsc +1 -1
- package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app/login.html +1 -1
- package/app/.next/server/app/login.rsc +1 -1
- package/app/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next/server/app-paths-manifest.json +18 -18
- package/app/.next/server/middleware-manifest.json +1 -1
- package/app/.next/server/pages/404.html +1 -1
- package/app/.next/server/pages/500.html +1 -1
- package/cli.js +49 -14
- package/package.json +2 -1
- package/src/cli/api/client.js +375 -0
- package/src/cli/menus/apiKeys.js +259 -0
- package/src/cli/menus/cliTools.js +221 -0
- package/src/cli/menus/combos.js +477 -0
- package/src/cli/menus/providers.js +448 -0
- package/src/cli/menus/settings.js +86 -0
- package/src/cli/terminalUI.js +86 -0
- package/src/cli/utils/display.js +156 -0
- package/src/cli/utils/format.js +125 -0
- package/src/cli/utils/input.js +211 -0
- package/src/cli/utils/menuHelper.js +155 -0
- package/src/cli/utils/modelSelector.js +133 -0
- package/hooks/model-websearch.cjs +0 -319
- /package/app/.next/static/{z-NGTtnYbvnsa63SSl6HV → vEruHXnDBNEM-jV9xL72D}/_buildManifest.js +0 -0
- /package/app/.next/static/{z-NGTtnYbvnsa63SSl6HV → vEruHXnDBNEM-jV9xL72D}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const { formatNumber } = require("./format");
|
|
2
|
+
|
|
3
|
+
// ANSI color codes
|
|
4
|
+
const COLORS = {
|
|
5
|
+
reset: "\x1b[0m",
|
|
6
|
+
success: "\x1b[32m",
|
|
7
|
+
error: "\x1b[31m",
|
|
8
|
+
warning: "\x1b[33m",
|
|
9
|
+
info: "\x1b[36m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
bold: "\x1b[1m",
|
|
12
|
+
bright: "\x1b[1m",
|
|
13
|
+
cyan: "\x1b[36m"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Box drawing characters
|
|
17
|
+
const BOX_CHARS = {
|
|
18
|
+
topLeft: "┌",
|
|
19
|
+
topRight: "┐",
|
|
20
|
+
bottomLeft: "└",
|
|
21
|
+
bottomRight: "┘",
|
|
22
|
+
horizontal: "─",
|
|
23
|
+
vertical: "│"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Draw a box with border around content
|
|
28
|
+
* @param {string} title - Box title
|
|
29
|
+
* @param {string} content - Content to display inside box
|
|
30
|
+
* @param {number} [width=60] - Box width
|
|
31
|
+
*/
|
|
32
|
+
function showBox(title, content, width = 60) {
|
|
33
|
+
const innerWidth = width - 4;
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
|
|
36
|
+
// Top border with title
|
|
37
|
+
const topBorder = BOX_CHARS.topLeft + BOX_CHARS.horizontal.repeat(2) +
|
|
38
|
+
` ${title} ` +
|
|
39
|
+
BOX_CHARS.horizontal.repeat(Math.max(0, innerWidth - title.length - 3)) +
|
|
40
|
+
BOX_CHARS.topRight;
|
|
41
|
+
|
|
42
|
+
console.log(topBorder);
|
|
43
|
+
|
|
44
|
+
// Content lines
|
|
45
|
+
lines.forEach(line => {
|
|
46
|
+
const paddedLine = line.padEnd(innerWidth);
|
|
47
|
+
console.log(`${BOX_CHARS.vertical} ${paddedLine} ${BOX_CHARS.vertical}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Bottom border
|
|
51
|
+
const bottomBorder = BOX_CHARS.bottomLeft +
|
|
52
|
+
BOX_CHARS.horizontal.repeat(innerWidth + 2) +
|
|
53
|
+
BOX_CHARS.bottomRight;
|
|
54
|
+
|
|
55
|
+
console.log(bottomBorder);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Display a menu with numbered items
|
|
60
|
+
* @param {string} title - Menu title
|
|
61
|
+
* @param {string[]} items - Array of menu items
|
|
62
|
+
* @param {string} [footer] - Optional footer text
|
|
63
|
+
*/
|
|
64
|
+
function showMenu(title, items, footer) {
|
|
65
|
+
console.log(`\n${COLORS.bold}${title}${COLORS.reset}`);
|
|
66
|
+
console.log(COLORS.dim + "─".repeat(title.length) + COLORS.reset);
|
|
67
|
+
|
|
68
|
+
items.forEach((item, index) => {
|
|
69
|
+
console.log(` ${COLORS.info}${index + 1}.${COLORS.reset} ${item}`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (footer) {
|
|
73
|
+
console.log(`\n${COLORS.dim}${footer}${COLORS.reset}`);
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Display data in table format
|
|
80
|
+
* @param {string[]} headers - Array of column headers
|
|
81
|
+
* @param {Array<Array<string|number>>} rows - Array of row data
|
|
82
|
+
*/
|
|
83
|
+
function showTable(headers, rows) {
|
|
84
|
+
if (!headers.length || !rows.length) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Calculate column widths
|
|
89
|
+
const colWidths = headers.map((header, i) => {
|
|
90
|
+
const maxDataWidth = Math.max(...rows.map(row => String(row[i] || "").length));
|
|
91
|
+
return Math.max(header.length, maxDataWidth);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Print header
|
|
95
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(" │ ");
|
|
96
|
+
console.log(COLORS.bold + headerRow + COLORS.reset);
|
|
97
|
+
|
|
98
|
+
// Print separator
|
|
99
|
+
const separator = colWidths.map(w => "─".repeat(w)).join("─┼─");
|
|
100
|
+
console.log(COLORS.dim + separator + COLORS.reset);
|
|
101
|
+
|
|
102
|
+
// Print rows
|
|
103
|
+
rows.forEach(row => {
|
|
104
|
+
const rowStr = row.map((cell, i) => String(cell || "").padEnd(colWidths[i])).join(" │ ");
|
|
105
|
+
console.log(rowStr);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Show colored status message
|
|
111
|
+
* @param {string} message - Message to display
|
|
112
|
+
* @param {string} [type="info"] - Status type: success, error, warning, info
|
|
113
|
+
*/
|
|
114
|
+
function showStatus(message, type = "info") {
|
|
115
|
+
const symbols = {
|
|
116
|
+
success: "✓",
|
|
117
|
+
error: "✗",
|
|
118
|
+
warning: "⚠",
|
|
119
|
+
info: "ℹ"
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const color = COLORS[type] || COLORS.info;
|
|
123
|
+
const symbol = symbols[type] || symbols.info;
|
|
124
|
+
|
|
125
|
+
console.log(`${color}${symbol} ${message}${COLORS.reset}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear the terminal screen
|
|
130
|
+
*/
|
|
131
|
+
function clearScreen() {
|
|
132
|
+
console.clear();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Show menu header with title and subtitle
|
|
137
|
+
* @param {string} title - Main title
|
|
138
|
+
* @param {string} subtitle - Optional subtitle
|
|
139
|
+
*/
|
|
140
|
+
function showHeader(title, subtitle) {
|
|
141
|
+
console.log(`\n${"=".repeat(60)}`);
|
|
142
|
+
console.log(` ${COLORS.bright}${COLORS.cyan}${title}${COLORS.reset}`);
|
|
143
|
+
if (subtitle) {
|
|
144
|
+
console.log(` ${COLORS.dim}${subtitle}${COLORS.reset}`);
|
|
145
|
+
}
|
|
146
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
showBox,
|
|
151
|
+
showMenu,
|
|
152
|
+
showTable,
|
|
153
|
+
showStatus,
|
|
154
|
+
clearScreen,
|
|
155
|
+
showHeader
|
|
156
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncate text with ellipsis
|
|
3
|
+
* @param {string} text - Text to truncate
|
|
4
|
+
* @param {number} maxLength - Maximum length
|
|
5
|
+
* @returns {string} Truncated text
|
|
6
|
+
*/
|
|
7
|
+
function truncate(text, maxLength) {
|
|
8
|
+
if (!text || text.length <= maxLength) {
|
|
9
|
+
return text;
|
|
10
|
+
}
|
|
11
|
+
return text.substring(0, maxLength - 3) + "...";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mask API key showing only first and last characters
|
|
16
|
+
* @param {string} key - API key to mask
|
|
17
|
+
* @returns {string} Masked key
|
|
18
|
+
*/
|
|
19
|
+
function maskKey(key) {
|
|
20
|
+
if (!key || key.length < 8) {
|
|
21
|
+
return "***";
|
|
22
|
+
}
|
|
23
|
+
const firstChars = key.substring(0, 4);
|
|
24
|
+
const lastChars = key.substring(key.length - 4);
|
|
25
|
+
return `${firstChars}${"*".repeat(key.length - 8)}${lastChars}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format date to readable string
|
|
30
|
+
* @param {Date|string|number} date - Date to format
|
|
31
|
+
* @returns {string} Formatted date string
|
|
32
|
+
*/
|
|
33
|
+
function formatDate(date) {
|
|
34
|
+
const d = new Date(date);
|
|
35
|
+
if (isNaN(d.getTime())) {
|
|
36
|
+
return "Invalid Date";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const year = d.getFullYear();
|
|
40
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
42
|
+
const hours = String(d.getHours()).padStart(2, "0");
|
|
43
|
+
const minutes = String(d.getMinutes()).padStart(2, "0");
|
|
44
|
+
const seconds = String(d.getSeconds()).padStart(2, "0");
|
|
45
|
+
|
|
46
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format number with commas
|
|
51
|
+
* @param {number} num - Number to format
|
|
52
|
+
* @returns {string} Formatted number
|
|
53
|
+
*/
|
|
54
|
+
function formatNumber(num) {
|
|
55
|
+
if (typeof num !== "number" || isNaN(num)) {
|
|
56
|
+
return "0";
|
|
57
|
+
}
|
|
58
|
+
return num.toLocaleString("en-US");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format bytes to human readable size
|
|
63
|
+
* @param {number} bytes - Bytes to format
|
|
64
|
+
* @returns {string} Formatted size string
|
|
65
|
+
*/
|
|
66
|
+
function formatBytes(bytes) {
|
|
67
|
+
if (typeof bytes !== "number" || isNaN(bytes) || bytes < 0) {
|
|
68
|
+
return "0 B";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
72
|
+
let size = bytes;
|
|
73
|
+
let unitIndex = 0;
|
|
74
|
+
|
|
75
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
76
|
+
size /= 1024;
|
|
77
|
+
unitIndex++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get relative time string
|
|
85
|
+
* @param {Date|string|number} date - Date to compare
|
|
86
|
+
* @returns {string} Relative time string
|
|
87
|
+
*/
|
|
88
|
+
function getRelativeTime(date) {
|
|
89
|
+
const d = new Date(date);
|
|
90
|
+
if (isNaN(d.getTime())) {
|
|
91
|
+
return "Invalid Date";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const diffMs = now - d;
|
|
96
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
97
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
98
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
99
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
100
|
+
const diffMonth = Math.floor(diffDay / 30);
|
|
101
|
+
const diffYear = Math.floor(diffDay / 365);
|
|
102
|
+
|
|
103
|
+
if (diffSec < 60) {
|
|
104
|
+
return "just now";
|
|
105
|
+
} else if (diffMin < 60) {
|
|
106
|
+
return `${diffMin} minute${diffMin > 1 ? "s" : ""} ago`;
|
|
107
|
+
} else if (diffHour < 24) {
|
|
108
|
+
return `${diffHour} hour${diffHour > 1 ? "s" : ""} ago`;
|
|
109
|
+
} else if (diffDay < 30) {
|
|
110
|
+
return `${diffDay} day${diffDay > 1 ? "s" : ""} ago`;
|
|
111
|
+
} else if (diffMonth < 12) {
|
|
112
|
+
return `${diffMonth} month${diffMonth > 1 ? "s" : ""} ago`;
|
|
113
|
+
} else {
|
|
114
|
+
return `${diffYear} year${diffYear > 1 ? "s" : ""} ago`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
truncate,
|
|
120
|
+
maskKey,
|
|
121
|
+
formatDate,
|
|
122
|
+
formatNumber,
|
|
123
|
+
formatBytes,
|
|
124
|
+
getRelativeTime
|
|
125
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const readline = require("readline");
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
bright: "\x1b[1m",
|
|
6
|
+
dim: "\x1b[2m",
|
|
7
|
+
underline: "\x1b[4m",
|
|
8
|
+
reverse: "\x1b[7m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
green: "\x1b[32m",
|
|
11
|
+
yellow: "\x1b[33m",
|
|
12
|
+
blue: "\x1b[34m",
|
|
13
|
+
white: "\x1b[37m",
|
|
14
|
+
bgGreen: "\x1b[42m",
|
|
15
|
+
bgBlue: "\x1b[44m",
|
|
16
|
+
black: "\x1b[30m",
|
|
17
|
+
// Terracotta/Earth orange - using RGB escape code
|
|
18
|
+
terracotta: "\x1b[38;2;217;119;87m", // #D97757
|
|
19
|
+
bgTerracotta: "\x1b[48;2;217;119;87m"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ask a question and return the user's answer
|
|
24
|
+
* @param {string} question - The question to ask
|
|
25
|
+
* @returns {Promise<string>} The user's answer
|
|
26
|
+
*/
|
|
27
|
+
async function prompt(question) {
|
|
28
|
+
const rl = readline.createInterface({
|
|
29
|
+
input: process.stdin,
|
|
30
|
+
output: process.stdout
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
rl.question(question, (answer) => {
|
|
35
|
+
rl.close();
|
|
36
|
+
resolve(answer.trim());
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Show a numbered menu and return the selected option number
|
|
43
|
+
* @param {string} question - The question to ask
|
|
44
|
+
* @param {string[]} options - Array of options to display
|
|
45
|
+
* @returns {Promise<number>} The selected option index (0-based)
|
|
46
|
+
*/
|
|
47
|
+
async function select(question, options) {
|
|
48
|
+
console.log(question);
|
|
49
|
+
options.forEach((option, index) => {
|
|
50
|
+
console.log(` ${index + 1}. ${option}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
while (true) {
|
|
54
|
+
const answer = await prompt("\nSelect option (number): ");
|
|
55
|
+
const num = parseInt(answer, 10);
|
|
56
|
+
|
|
57
|
+
if (!isNaN(num) && num >= 1 && num <= options.length) {
|
|
58
|
+
return num - 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(`Invalid selection. Please enter a number between 1 and ${options.length}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask a yes/no question and return boolean
|
|
67
|
+
* @param {string} question - The question to ask
|
|
68
|
+
* @returns {Promise<boolean>} True for yes, false for no
|
|
69
|
+
*/
|
|
70
|
+
async function confirm(question) {
|
|
71
|
+
while (true) {
|
|
72
|
+
const answer = await prompt(`${question} (y/n): `);
|
|
73
|
+
const lower = answer.toLowerCase();
|
|
74
|
+
|
|
75
|
+
if (lower === "y" || lower === "yes") {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (lower === "n" || lower === "no") {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("Please answer 'y' or 'n'");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Pause execution until user presses Enter
|
|
88
|
+
* @param {string} [message="Press Enter to continue..."] - Message to display
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async function pause(message = "Press Enter to continue...") {
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
rl.question(message, () => {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Show interactive menu with arrow key navigation
|
|
107
|
+
* @param {string} title - Menu title
|
|
108
|
+
* @param {Array<{label: string, icon?: string}>} items - Menu items
|
|
109
|
+
* @param {number} defaultIndex - Default selected index
|
|
110
|
+
* @param {string} headerContent - Optional content to show above menu
|
|
111
|
+
* @param {Array<string>} breadcrumb - Optional breadcrumb path
|
|
112
|
+
* @returns {Promise<number>} Selected index, or -1 if ESC pressed
|
|
113
|
+
*/
|
|
114
|
+
async function selectMenu(title, items, defaultIndex = 0, headerContent = "", breadcrumb = []) {
|
|
115
|
+
return new Promise((resolve) => {
|
|
116
|
+
let selectedIndex = defaultIndex;
|
|
117
|
+
let isActive = true;
|
|
118
|
+
|
|
119
|
+
// Remove any existing keypress listeners first
|
|
120
|
+
process.stdin.removeAllListeners("keypress");
|
|
121
|
+
|
|
122
|
+
readline.emitKeypressEvents(process.stdin);
|
|
123
|
+
if (process.stdin.isTTY) {
|
|
124
|
+
process.stdin.setRawMode(true);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const renderMenu = () => {
|
|
128
|
+
if (!isActive) return;
|
|
129
|
+
|
|
130
|
+
// Clear previous menu
|
|
131
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
132
|
+
|
|
133
|
+
// Show title with terracotta color
|
|
134
|
+
console.log(`\n${COLORS.terracotta}${"=".repeat(60)}${COLORS.reset}`);
|
|
135
|
+
console.log(` ${COLORS.bright}${COLORS.terracotta}${title}${COLORS.reset}`);
|
|
136
|
+
console.log(`${COLORS.terracotta}${"=".repeat(60)}${COLORS.reset}`);
|
|
137
|
+
|
|
138
|
+
// Show breadcrumb if provided
|
|
139
|
+
if (breadcrumb.length > 0) {
|
|
140
|
+
console.log(` ${COLORS.dim}${breadcrumb.join(" > ")}${COLORS.reset}`);
|
|
141
|
+
}
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
// Show header content if provided
|
|
145
|
+
if (headerContent) {
|
|
146
|
+
console.log(headerContent);
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Show menu items with proper alignment
|
|
151
|
+
items.forEach((item, index) => {
|
|
152
|
+
const isSelected = index === selectedIndex;
|
|
153
|
+
|
|
154
|
+
// Use ★ for selected, ☆ for normal
|
|
155
|
+
const icon = isSelected ? "★" : "☆";
|
|
156
|
+
|
|
157
|
+
if (isSelected) {
|
|
158
|
+
// Selected: reverse + bright for high visibility on any terminal
|
|
159
|
+
console.log(` ${COLORS.reverse}${COLORS.bright}${icon} ${item.label}${COLORS.reset}`);
|
|
160
|
+
} else {
|
|
161
|
+
// Not selected: plain text with empty star
|
|
162
|
+
console.log(` ${icon} ${item.label}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const cleanup = () => {
|
|
168
|
+
if (!isActive) return;
|
|
169
|
+
isActive = false;
|
|
170
|
+
|
|
171
|
+
if (process.stdin.isTTY) {
|
|
172
|
+
process.stdin.setRawMode(false);
|
|
173
|
+
}
|
|
174
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
175
|
+
process.stdin.pause();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const onKeypress = (str, key) => {
|
|
179
|
+
if (!isActive || !key) return;
|
|
180
|
+
|
|
181
|
+
if (key.name === "up") {
|
|
182
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
183
|
+
renderMenu();
|
|
184
|
+
} else if (key.name === "down") {
|
|
185
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
186
|
+
renderMenu();
|
|
187
|
+
} else if (key.name === "return") {
|
|
188
|
+
cleanup();
|
|
189
|
+
resolve(selectedIndex);
|
|
190
|
+
} else if (key.name === "escape") {
|
|
191
|
+
cleanup();
|
|
192
|
+
resolve(-1);
|
|
193
|
+
} else if (key.ctrl && key.name === "c") {
|
|
194
|
+
cleanup();
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
process.stdin.on("keypress", onKeypress);
|
|
200
|
+
process.stdin.resume();
|
|
201
|
+
renderMenu();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
prompt,
|
|
207
|
+
select,
|
|
208
|
+
confirm,
|
|
209
|
+
pause,
|
|
210
|
+
selectMenu
|
|
211
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const { selectMenu } = require("./input");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Show a menu with back button at top and handle selection
|
|
5
|
+
* @param {Object} config - Menu configuration
|
|
6
|
+
* @param {string} config.title - Menu title
|
|
7
|
+
* @param {string} config.headerContent - Optional header content
|
|
8
|
+
* @param {Array<{label: string, action: Function}>} config.items - Menu items with actions
|
|
9
|
+
* @param {string} config.backLabel - Back button label (default: "← Back")
|
|
10
|
+
* @param {number} config.defaultIndex - Default selected index (default: 0)
|
|
11
|
+
* @param {Function} config.refresh - Optional refresh function to call after each action
|
|
12
|
+
* @param {Array<string>} config.breadcrumb - Optional breadcrumb path
|
|
13
|
+
* @returns {Promise<void>}
|
|
14
|
+
*/
|
|
15
|
+
async function showMenuWithBack(config) {
|
|
16
|
+
const {
|
|
17
|
+
title,
|
|
18
|
+
headerContent = "",
|
|
19
|
+
items,
|
|
20
|
+
backLabel = "← Back",
|
|
21
|
+
defaultIndex = 0,
|
|
22
|
+
refresh = null,
|
|
23
|
+
breadcrumb = []
|
|
24
|
+
} = config;
|
|
25
|
+
|
|
26
|
+
while (true) {
|
|
27
|
+
// Call refresh if provided
|
|
28
|
+
let refreshedData = null;
|
|
29
|
+
if (refresh) {
|
|
30
|
+
refreshedData = await refresh();
|
|
31
|
+
if (refreshedData === null) {
|
|
32
|
+
// Refresh failed, exit menu
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Build menu items with back at top
|
|
38
|
+
const menuItems = [
|
|
39
|
+
{ label: backLabel, icon: "☆" },
|
|
40
|
+
...items.map(item => ({
|
|
41
|
+
label: typeof item.label === "function" ? item.label(refreshedData) : item.label,
|
|
42
|
+
icon: "☆"
|
|
43
|
+
}))
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Resolve headerContent if it's a function
|
|
47
|
+
const resolvedHeader = typeof headerContent === "function"
|
|
48
|
+
? await headerContent(refreshedData)
|
|
49
|
+
: headerContent;
|
|
50
|
+
|
|
51
|
+
const selected = await selectMenu(
|
|
52
|
+
title,
|
|
53
|
+
menuItems,
|
|
54
|
+
defaultIndex,
|
|
55
|
+
resolvedHeader,
|
|
56
|
+
breadcrumb
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Back or ESC
|
|
60
|
+
if (selected === -1 || selected === 0) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Execute action for selected item
|
|
65
|
+
const actionIndex = selected - 1;
|
|
66
|
+
const item = items[actionIndex];
|
|
67
|
+
|
|
68
|
+
if (item && item.action) {
|
|
69
|
+
const shouldContinue = await item.action(refreshedData);
|
|
70
|
+
// If action returns false, exit menu
|
|
71
|
+
if (shouldContinue === false) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Show a list menu where items are fetched dynamically
|
|
80
|
+
* @param {Object} config - Menu configuration
|
|
81
|
+
* @param {string} config.title - Menu title
|
|
82
|
+
* @param {string} config.headerContent - Optional header content
|
|
83
|
+
* @param {Function} config.fetchItems - Async function to fetch items array
|
|
84
|
+
* @param {Function} config.formatItem - Function to format each item to {label, data}
|
|
85
|
+
* @param {Function} config.onSelect - Action when item is selected
|
|
86
|
+
* @param {Object} config.createAction - Optional create action {label, action}
|
|
87
|
+
* @param {string} config.backLabel - Back button label
|
|
88
|
+
* @param {Array<string>} config.breadcrumb - Optional breadcrumb path
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
async function showListMenu(config) {
|
|
92
|
+
const {
|
|
93
|
+
title,
|
|
94
|
+
headerContent = "",
|
|
95
|
+
fetchItems,
|
|
96
|
+
formatItem,
|
|
97
|
+
onSelect,
|
|
98
|
+
createAction = null,
|
|
99
|
+
backLabel = "← Back",
|
|
100
|
+
breadcrumb = []
|
|
101
|
+
} = config;
|
|
102
|
+
|
|
103
|
+
while (true) {
|
|
104
|
+
// Fetch items
|
|
105
|
+
const result = await fetchItems();
|
|
106
|
+
if (!result) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const items = result.items || [];
|
|
111
|
+
const metadata = result.metadata || {};
|
|
112
|
+
|
|
113
|
+
// Build menu items
|
|
114
|
+
const menuItems = [{ label: backLabel, icon: "☆" }];
|
|
115
|
+
|
|
116
|
+
if (createAction) {
|
|
117
|
+
menuItems.push({ label: createAction.label, icon: "☆" });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
items.forEach(item => {
|
|
121
|
+
const formatted = formatItem(item);
|
|
122
|
+
menuItems.push({ label: formatted, icon: "☆" });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const header = typeof headerContent === "function"
|
|
126
|
+
? await headerContent(metadata)
|
|
127
|
+
: headerContent;
|
|
128
|
+
|
|
129
|
+
const selected = await selectMenu(title, menuItems, 0, header, breadcrumb);
|
|
130
|
+
|
|
131
|
+
// Back or ESC
|
|
132
|
+
if (selected === -1 || selected === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create action
|
|
137
|
+
if (createAction && selected === 1) {
|
|
138
|
+
await createAction.action();
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Select item
|
|
143
|
+
const offset = createAction ? 2 : 1;
|
|
144
|
+
const itemIndex = selected - offset;
|
|
145
|
+
|
|
146
|
+
if (itemIndex >= 0 && itemIndex < items.length) {
|
|
147
|
+
await onSelect(items[itemIndex]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
showMenuWithBack,
|
|
154
|
+
showListMenu
|
|
155
|
+
};
|