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.
Files changed (119) hide show
  1. package/README.md +0 -1
  2. package/app/.next/BUILD_ID +1 -1
  3. package/app/.next/app-build-manifest.json +78 -78
  4. package/app/.next/app-path-routes-manifest.json +18 -18
  5. package/app/.next/build-manifest.json +2 -2
  6. package/app/.next/prerender-manifest.json +36 -36
  7. package/app/.next/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
  8. package/app/.next/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
  9. package/app/.next/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
  10. package/app/.next/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
  11. package/app/.next/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
  12. package/app/.next/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
  13. package/app/.next/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
  14. package/app/.next/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
  15. package/app/.next/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
  16. package/app/.next/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
  17. package/app/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/app/.next/server/app/_not-found.html +1 -1
  19. package/app/.next/server/app/_not-found.rsc +1 -1
  20. package/app/.next/server/app/api/auth/login/route_client-reference-manifest.js +1 -1
  21. package/app/.next/server/app/api/auth/logout/route_client-reference-manifest.js +1 -1
  22. package/app/.next/server/app/api/cli-tools/claude-settings/route_client-reference-manifest.js +1 -1
  23. package/app/.next/server/app/api/cli-tools/codex-settings/route_client-reference-manifest.js +1 -1
  24. package/app/.next/server/app/api/cloud/auth/route_client-reference-manifest.js +1 -1
  25. package/app/.next/server/app/api/cloud/credentials/update/route_client-reference-manifest.js +1 -1
  26. package/app/.next/server/app/api/cloud/model/resolve/route_client-reference-manifest.js +1 -1
  27. package/app/.next/server/app/api/cloud/models/alias/route_client-reference-manifest.js +1 -1
  28. package/app/.next/server/app/api/combos/[id]/route_client-reference-manifest.js +1 -1
  29. package/app/.next/server/app/api/combos/route_client-reference-manifest.js +1 -1
  30. package/app/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  31. package/app/.next/server/app/api/keys/[id]/route_client-reference-manifest.js +1 -1
  32. package/app/.next/server/app/api/keys/route_client-reference-manifest.js +1 -1
  33. package/app/.next/server/app/api/models/alias/route_client-reference-manifest.js +1 -1
  34. package/app/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  35. package/app/.next/server/app/api/oauth/[provider]/[action]/route_client-reference-manifest.js +1 -1
  36. package/app/.next/server/app/api/oauth/kiro/import/route_client-reference-manifest.js +1 -1
  37. package/app/.next/server/app/api/oauth/kiro/social-authorize/route_client-reference-manifest.js +1 -1
  38. package/app/.next/server/app/api/oauth/kiro/social-exchange/route_client-reference-manifest.js +1 -1
  39. package/app/.next/server/app/api/pricing/route_client-reference-manifest.js +1 -1
  40. package/app/.next/server/app/api/providers/[id]/models/route_client-reference-manifest.js +1 -1
  41. package/app/.next/server/app/api/providers/[id]/route_client-reference-manifest.js +1 -1
  42. package/app/.next/server/app/api/providers/[id]/test/route_client-reference-manifest.js +1 -1
  43. package/app/.next/server/app/api/providers/client/route_client-reference-manifest.js +1 -1
  44. package/app/.next/server/app/api/providers/route_client-reference-manifest.js +1 -1
  45. package/app/.next/server/app/api/providers/validate/route_client-reference-manifest.js +1 -1
  46. package/app/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  47. package/app/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  48. package/app/.next/server/app/api/sync/cloud/route_client-reference-manifest.js +1 -1
  49. package/app/.next/server/app/api/sync/initialize/route_client-reference-manifest.js +1 -1
  50. package/app/.next/server/app/api/tags/route_client-reference-manifest.js +1 -1
  51. package/app/.next/server/app/api/translator/load/route_client-reference-manifest.js +1 -1
  52. package/app/.next/server/app/api/translator/save/route_client-reference-manifest.js +1 -1
  53. package/app/.next/server/app/api/translator/send/route_client-reference-manifest.js +1 -1
  54. package/app/.next/server/app/api/translator/translate/route_client-reference-manifest.js +1 -1
  55. package/app/.next/server/app/api/usage/[connectionId]/route_client-reference-manifest.js +1 -1
  56. package/app/.next/server/app/api/usage/history/route_client-reference-manifest.js +1 -1
  57. package/app/.next/server/app/api/v1/api/chat/route_client-reference-manifest.js +1 -1
  58. package/app/.next/server/app/api/v1/chat/completions/route_client-reference-manifest.js +1 -1
  59. package/app/.next/server/app/api/v1/messages/count_tokens/route_client-reference-manifest.js +1 -1
  60. package/app/.next/server/app/api/v1/messages/route_client-reference-manifest.js +1 -1
  61. package/app/.next/server/app/api/v1/models/route_client-reference-manifest.js +1 -1
  62. package/app/.next/server/app/api/v1/responses/route_client-reference-manifest.js +1 -1
  63. package/app/.next/server/app/api/v1/route_client-reference-manifest.js +1 -1
  64. package/app/.next/server/app/api/v1beta/models/[...path]/route_client-reference-manifest.js +1 -1
  65. package/app/.next/server/app/api/v1beta/models/route_client-reference-manifest.js +1 -1
  66. package/app/.next/server/app/callback/page_client-reference-manifest.js +1 -1
  67. package/app/.next/server/app/callback.html +1 -1
  68. package/app/.next/server/app/callback.rsc +1 -1
  69. package/app/.next/server/app/dashboard/cli-tools.html +1 -1
  70. package/app/.next/server/app/dashboard/cli-tools.rsc +1 -1
  71. package/app/.next/server/app/dashboard/combos.html +1 -1
  72. package/app/.next/server/app/dashboard/combos.rsc +1 -1
  73. package/app/.next/server/app/dashboard/endpoint.html +1 -1
  74. package/app/.next/server/app/dashboard/endpoint.rsc +1 -1
  75. package/app/.next/server/app/dashboard/profile.html +1 -1
  76. package/app/.next/server/app/dashboard/profile.rsc +1 -1
  77. package/app/.next/server/app/dashboard/providers/new.html +1 -1
  78. package/app/.next/server/app/dashboard/providers/new.rsc +1 -1
  79. package/app/.next/server/app/dashboard/providers.html +1 -1
  80. package/app/.next/server/app/dashboard/providers.rsc +1 -1
  81. package/app/.next/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
  82. package/app/.next/server/app/dashboard/settings/pricing.html +1 -1
  83. package/app/.next/server/app/dashboard/settings/pricing.rsc +1 -1
  84. package/app/.next/server/app/dashboard/translator.html +1 -1
  85. package/app/.next/server/app/dashboard/translator.rsc +1 -1
  86. package/app/.next/server/app/dashboard/usage.html +1 -1
  87. package/app/.next/server/app/dashboard/usage.rsc +1 -1
  88. package/app/.next/server/app/dashboard.html +1 -1
  89. package/app/.next/server/app/dashboard.rsc +1 -1
  90. package/app/.next/server/app/index.html +1 -1
  91. package/app/.next/server/app/index.rsc +1 -1
  92. package/app/.next/server/app/landing/page_client-reference-manifest.js +1 -1
  93. package/app/.next/server/app/landing.html +1 -1
  94. package/app/.next/server/app/landing.rsc +1 -1
  95. package/app/.next/server/app/login/page_client-reference-manifest.js +1 -1
  96. package/app/.next/server/app/login.html +1 -1
  97. package/app/.next/server/app/login.rsc +1 -1
  98. package/app/.next/server/app/page_client-reference-manifest.js +1 -1
  99. package/app/.next/server/app-paths-manifest.json +18 -18
  100. package/app/.next/server/middleware-manifest.json +1 -1
  101. package/app/.next/server/pages/404.html +1 -1
  102. package/app/.next/server/pages/500.html +1 -1
  103. package/cli.js +49 -14
  104. package/package.json +2 -1
  105. package/src/cli/api/client.js +375 -0
  106. package/src/cli/menus/apiKeys.js +259 -0
  107. package/src/cli/menus/cliTools.js +221 -0
  108. package/src/cli/menus/combos.js +477 -0
  109. package/src/cli/menus/providers.js +448 -0
  110. package/src/cli/menus/settings.js +86 -0
  111. package/src/cli/terminalUI.js +86 -0
  112. package/src/cli/utils/display.js +156 -0
  113. package/src/cli/utils/format.js +125 -0
  114. package/src/cli/utils/input.js +211 -0
  115. package/src/cli/utils/menuHelper.js +155 -0
  116. package/src/cli/utils/modelSelector.js +133 -0
  117. package/hooks/model-websearch.cjs +0 -319
  118. /package/app/.next/static/{z-NGTtnYbvnsa63SSl6HV → vEruHXnDBNEM-jV9xL72D}/_buildManifest.js +0 -0
  119. /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
+ };