krims-code-cli 1.5.2__py3-none-any.whl
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.
- krims_code_cli-1.5.2.dist-info/METADATA +336 -0
- krims_code_cli-1.5.2.dist-info/RECORD +43 -0
- krims_code_cli-1.5.2.dist-info/WHEEL +5 -0
- krims_code_cli-1.5.2.dist-info/entry_points.txt +2 -0
- krims_code_cli-1.5.2.dist-info/licenses/LICENSE +21 -0
- krims_code_cli-1.5.2.dist-info/top_level.txt +1 -0
- krims_pip/__init__.py +1 -0
- krims_pip/cli.py +50 -0
- krims_pip/node_project/bin/krims-code.js +10 -0
- krims_pip/node_project/package-lock.json +793 -0
- krims_pip/node_project/package.json +46 -0
- krims_pip/node_project/src/agent.js +415 -0
- krims_pip/node_project/src/ai/fallback.js +239 -0
- krims_pip/node_project/src/ai/google.js +87 -0
- krims_pip/node_project/src/ai/providers.js +203 -0
- krims_pip/node_project/src/ai/router.js +146 -0
- krims_pip/node_project/src/ai/search.js +94 -0
- krims_pip/node_project/src/ai/telemetry.js +125 -0
- krims_pip/node_project/src/ai/tokens.js +101 -0
- krims_pip/node_project/src/ai/universal.js +507 -0
- krims_pip/node_project/src/ai/xai.js +50 -0
- krims_pip/node_project/src/chat.js +2543 -0
- krims_pip/node_project/src/cli.js +632 -0
- krims_pip/node_project/src/commands/commit.js +157 -0
- krims_pip/node_project/src/commands/dashboard.js +59 -0
- krims_pip/node_project/src/commands/github.js +349 -0
- krims_pip/node_project/src/commands/index.js +67 -0
- krims_pip/node_project/src/commands/status.js +66 -0
- krims_pip/node_project/src/commands/teamwork-preview.js +326 -0
- krims_pip/node_project/src/commands/theme.js +42 -0
- krims_pip/node_project/src/commands/themes.js +30 -0
- krims_pip/node_project/src/config.js +400 -0
- krims_pip/node_project/src/dashboard.js +112 -0
- krims_pip/node_project/src/file-parser.js +164 -0
- krims_pip/node_project/src/git.js +41 -0
- krims_pip/node_project/src/mic.js +220 -0
- krims_pip/node_project/src/modes.js +92 -0
- krims_pip/node_project/src/telemetry-server.js +855 -0
- krims_pip/node_project/src/ui/banner.js +204 -0
- krims_pip/node_project/src/ui/dashboard.html +834 -0
- krims_pip/node_project/src/ui/spinner.js +43 -0
- krims_pip/node_project/src/ui/theme.js +624 -0
- krims_pip/node_project/src/updater.js +196 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Spinner Helpers
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a styled spinner with the Krims Code theme.
|
|
9
|
+
* @param {string} text - Spinner label text
|
|
10
|
+
* @returns {object} An ora spinner instance
|
|
11
|
+
*/
|
|
12
|
+
export function createSpinner(text) {
|
|
13
|
+
return ora({
|
|
14
|
+
text,
|
|
15
|
+
spinner: {
|
|
16
|
+
interval: 80,
|
|
17
|
+
frames: ["▖", "▘", "▝", "▗"],
|
|
18
|
+
},
|
|
19
|
+
color: "cyan",
|
|
20
|
+
discardStdin: false,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wraps an async function with a loading spinner.
|
|
26
|
+
* Shows the spinner while the function runs and reports success/failure.
|
|
27
|
+
* @param {string} text - The loading message
|
|
28
|
+
* @param {Function} asyncFn - The async function to execute
|
|
29
|
+
* @returns {Promise<*>} The result of the async function
|
|
30
|
+
*/
|
|
31
|
+
export async function withSpinner(text, asyncFn) {
|
|
32
|
+
const spinner = createSpinner(text);
|
|
33
|
+
spinner.start();
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await asyncFn();
|
|
37
|
+
spinner.succeed();
|
|
38
|
+
return result;
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail(err.message || "Operation failed");
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════
|
|
2
|
+
// Krims Code AI CLI — Dynamic Theme-Aware Formatting Utilities
|
|
3
|
+
// ═══════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
|
|
7
|
+
// ── Theme Definitions ─────────────────────────────────────
|
|
8
|
+
export const THEMES = {
|
|
9
|
+
cyberpunk: {
|
|
10
|
+
accent: "#00f0ff", // Neon cyan
|
|
11
|
+
accent2: "#bd93f9", // Neon purple/magenta
|
|
12
|
+
accent3: "#50fa7b", // Neon green
|
|
13
|
+
danger: "#ff5555", // Neon red
|
|
14
|
+
warning: "#ffb86c", // Neon orange
|
|
15
|
+
muted: "#6272a4", // Comment gray
|
|
16
|
+
text: "#f8f8f2", // Crisp white-yellow
|
|
17
|
+
dim: "#44475a", // Border gray
|
|
18
|
+
brand: "#00f0ff", // Neon cyan
|
|
19
|
+
success: "#50fa7b", // Neon green
|
|
20
|
+
error: "#ff5555",
|
|
21
|
+
magenta: "#ff79c6",
|
|
22
|
+
orange: "#ffb86c",
|
|
23
|
+
},
|
|
24
|
+
matrix: {
|
|
25
|
+
accent: "#50fa7b", // Lime green
|
|
26
|
+
accent2: "#00ff00", // Matrix green
|
|
27
|
+
accent3: "#8efb50", // Light lime
|
|
28
|
+
danger: "#ff5555",
|
|
29
|
+
warning: "#ffb86c",
|
|
30
|
+
muted: "#1f5f1f", // Dark forest green
|
|
31
|
+
text: "#aaffaa", // Soft green text
|
|
32
|
+
dim: "#103f10", // Dark green border
|
|
33
|
+
brand: "#50fa7b",
|
|
34
|
+
success: "#50fa7b",
|
|
35
|
+
error: "#ff5555",
|
|
36
|
+
magenta: "#50fa7b",
|
|
37
|
+
orange: "#8efb50",
|
|
38
|
+
},
|
|
39
|
+
synthwave: {
|
|
40
|
+
accent: "#ff79c6", // Neon pink
|
|
41
|
+
accent2: "#ffb86c", // Neon orange
|
|
42
|
+
accent3: "#bd93f9", // Neon purple
|
|
43
|
+
danger: "#ff5555",
|
|
44
|
+
warning: "#ffb86c",
|
|
45
|
+
muted: "#6f2a8a", // Dark purple
|
|
46
|
+
text: "#ffe8f5", // Soft pinkish white
|
|
47
|
+
dim: "#3e104f", // Deep purple border
|
|
48
|
+
brand: "#ff79c6",
|
|
49
|
+
success: "#bd93f9",
|
|
50
|
+
error: "#ff5555",
|
|
51
|
+
magenta: "#ff79c6",
|
|
52
|
+
orange: "#ffb86c",
|
|
53
|
+
},
|
|
54
|
+
crimson: {
|
|
55
|
+
accent: "#ff5555", // Neon red
|
|
56
|
+
accent2: "#ffb86c", // Gold/orange
|
|
57
|
+
accent3: "#f1fa8c", // Neon yellow
|
|
58
|
+
danger: "#ff5555",
|
|
59
|
+
warning: "#ffb86c",
|
|
60
|
+
muted: "#662222", // Muted red-gray
|
|
61
|
+
text: "#ffffff", // White text
|
|
62
|
+
dim: "#331111", // Dark red border
|
|
63
|
+
brand: "#ff5555",
|
|
64
|
+
success: "#f1fa8c",
|
|
65
|
+
error: "#ff5555",
|
|
66
|
+
magenta: "#ff5555",
|
|
67
|
+
orange: "#ffb86c",
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let activeThemeName = "cyberpunk";
|
|
72
|
+
|
|
73
|
+
// ── Dynamic Colors Resolver ───────────────────────────────
|
|
74
|
+
export const colors = new Proxy({}, {
|
|
75
|
+
get(target, prop) {
|
|
76
|
+
const theme = THEMES[activeThemeName] || THEMES.cyberpunk;
|
|
77
|
+
const colorHex = theme[prop] || "#ffffff";
|
|
78
|
+
const isBold = prop === "brand" || prop === "success" || prop === "error";
|
|
79
|
+
|
|
80
|
+
let fn = chalk.hex(colorHex);
|
|
81
|
+
if (isBold) {
|
|
82
|
+
fn = fn.bold;
|
|
83
|
+
}
|
|
84
|
+
return fn;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── Labels ────────────────────────────────────────────────
|
|
89
|
+
export const label = {
|
|
90
|
+
get system() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" SYSTEM "); },
|
|
91
|
+
get user() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent3).bold(" YOU "); },
|
|
92
|
+
get krims() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" KRIMS "); },
|
|
93
|
+
get error() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#2a0a14" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].danger).bold(" ERROR "); },
|
|
94
|
+
get info() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent2).bold(" INFO "); },
|
|
95
|
+
get config() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].warning).bold(" CONFIG "); },
|
|
96
|
+
get math() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent3).bold(" MATH "); },
|
|
97
|
+
get krylo() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" KRYLO "); },
|
|
98
|
+
get mode() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent2).bold(" MODE "); },
|
|
99
|
+
get mesh() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].accent).bold(" MESH "); },
|
|
100
|
+
get file() { return chalk.bgHex(activeThemeName === "cyberpunk" ? "#0c1825" : THEMES[activeThemeName].dim).hex(THEMES[activeThemeName].warning).bold(" FILE "); },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ── Formatting Helpers ───────────────────────────────────
|
|
104
|
+
export function separator(char = "─", length) {
|
|
105
|
+
const width = process.stdout.columns || 80;
|
|
106
|
+
const targetLength = length !== undefined ? length : Math.max(10, width - 4);
|
|
107
|
+
return colors.dim(char.repeat(targetLength));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function heading(text) {
|
|
111
|
+
return colors.brand(`\n ${text}\n`) + separator();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function keyValue(key, value) {
|
|
115
|
+
return ` ${colors.muted(key + ":")} ${colors.text(value)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function bullet(text) {
|
|
119
|
+
return ` ${colors.accent("›")} ${colors.text(text)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function modeBadge(mode) {
|
|
123
|
+
const theme = THEMES[activeThemeName] || THEMES.cyberpunk;
|
|
124
|
+
const badges = {
|
|
125
|
+
synthesis: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a3a2a" : theme.dim).hex(theme.accent3).bold(" SYNTHESIS "),
|
|
126
|
+
research: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a2a3a" : theme.dim).hex(theme.accent2).bold(" RESEARCH "),
|
|
127
|
+
architect: chalk.bgHex(activeThemeName === "cyberpunk" ? "#2a1a3a" : theme.dim).hex(theme.magenta).bold(" ARCHITECT "),
|
|
128
|
+
titan: chalk.bgHex(activeThemeName === "cyberpunk" ? "#1a2a3a" : theme.dim).hex(theme.accent).bold(" TITAN FUSION "),
|
|
129
|
+
};
|
|
130
|
+
return badges[mode?.toLowerCase()] || badges.titan;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Backs up the cursor and clears terminal lines printed during real-time streaming
|
|
135
|
+
* so they can be replaced by the final formatted response.
|
|
136
|
+
* @param {string} text - The raw streamed text that was printed
|
|
137
|
+
*/
|
|
138
|
+
export function clearStreamedText(text) {
|
|
139
|
+
if (!process.stdout.isTTY) return;
|
|
140
|
+
const width = process.stdout.columns || 80;
|
|
141
|
+
const lines = text.split("\n");
|
|
142
|
+
let lineCount = 0;
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
lineCount += Math.max(1, Math.ceil(line.length / width));
|
|
145
|
+
}
|
|
146
|
+
if (lineCount > 0) {
|
|
147
|
+
process.stdout.write(`\x1b[${lineCount}A\x1b[J`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filter stream tokens on the fly to suppress file write blocks
|
|
153
|
+
* and print a cleaner placeholder badge in real-time instead of raw code.
|
|
154
|
+
*/
|
|
155
|
+
export class StreamFilter {
|
|
156
|
+
constructor(writeFn) {
|
|
157
|
+
this.writeFn = writeFn;
|
|
158
|
+
this.buffer = "";
|
|
159
|
+
this.cursor = 0;
|
|
160
|
+
this.state = "NORMAL"; // "NORMAL", "COLLECTING_FILENAME", "SUPPRESSING"
|
|
161
|
+
this.filenameBuffer = "";
|
|
162
|
+
this.filteredText = "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
_write(text) {
|
|
166
|
+
this.writeFn(text);
|
|
167
|
+
this.filteredText += text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
write(token) {
|
|
171
|
+
this.buffer += token;
|
|
172
|
+
this.process();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
process() {
|
|
176
|
+
const writeTag = "[WRITE_FILE:";
|
|
177
|
+
const endTag = "[END_WRITE]";
|
|
178
|
+
|
|
179
|
+
while (this.cursor < this.buffer.length) {
|
|
180
|
+
if (this.state === "NORMAL") {
|
|
181
|
+
const nextIndex = this.buffer.indexOf(writeTag, this.cursor);
|
|
182
|
+
if (nextIndex !== -1) {
|
|
183
|
+
if (nextIndex > this.cursor) {
|
|
184
|
+
this._write(this.buffer.slice(this.cursor, nextIndex));
|
|
185
|
+
}
|
|
186
|
+
this.cursor = nextIndex + writeTag.length;
|
|
187
|
+
this.state = "COLLECTING_FILENAME";
|
|
188
|
+
this.filenameBuffer = "";
|
|
189
|
+
} else {
|
|
190
|
+
// Check for partial match of writeTag at the end of the buffer
|
|
191
|
+
let partialMatchLength = 0;
|
|
192
|
+
for (let i = 1; i < writeTag.length; i++) {
|
|
193
|
+
const part = writeTag.slice(0, i);
|
|
194
|
+
if (this.buffer.endsWith(part)) {
|
|
195
|
+
partialMatchLength = i;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const safeEnd = this.buffer.length - partialMatchLength;
|
|
200
|
+
if (safeEnd > this.cursor) {
|
|
201
|
+
this._write(this.buffer.slice(this.cursor, safeEnd));
|
|
202
|
+
this.cursor = safeEnd;
|
|
203
|
+
}
|
|
204
|
+
break; // Wait for more tokens
|
|
205
|
+
}
|
|
206
|
+
} else if (this.state === "COLLECTING_FILENAME") {
|
|
207
|
+
const closeIndex = this.buffer.indexOf("]", this.cursor);
|
|
208
|
+
if (closeIndex !== -1) {
|
|
209
|
+
this.filenameBuffer += this.buffer.slice(this.cursor, closeIndex);
|
|
210
|
+
const filename = this.filenameBuffer.trim();
|
|
211
|
+
this._write(`\n\n${colors.brand("⚡ [File creation request: " + filename + "]")}\n\n`);
|
|
212
|
+
this.cursor = closeIndex + 1;
|
|
213
|
+
this.state = "SUPPRESSING";
|
|
214
|
+
} else {
|
|
215
|
+
this.filenameBuffer += this.buffer.slice(this.cursor);
|
|
216
|
+
this.cursor = this.buffer.length;
|
|
217
|
+
break; // Wait for more tokens
|
|
218
|
+
}
|
|
219
|
+
} else if (this.state === "SUPPRESSING") {
|
|
220
|
+
const nextIndex = this.buffer.indexOf(endTag, this.cursor);
|
|
221
|
+
if (nextIndex !== -1) {
|
|
222
|
+
this.cursor = nextIndex + endTag.length;
|
|
223
|
+
this.state = "NORMAL";
|
|
224
|
+
} else {
|
|
225
|
+
// Check for partial match of endTag at the end of the buffer
|
|
226
|
+
let partialMatchLength = 0;
|
|
227
|
+
for (let i = 1; i < endTag.length; i++) {
|
|
228
|
+
const part = endTag.slice(0, i);
|
|
229
|
+
if (this.buffer.endsWith(part)) {
|
|
230
|
+
partialMatchLength = i;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const safeEnd = this.buffer.length - partialMatchLength;
|
|
235
|
+
if (safeEnd > this.cursor) {
|
|
236
|
+
this.cursor = safeEnd;
|
|
237
|
+
}
|
|
238
|
+
break; // Wait for more tokens
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
flush() {
|
|
245
|
+
if (this.state === "NORMAL" && this.cursor < this.buffer.length) {
|
|
246
|
+
this._write(this.buffer.slice(this.cursor));
|
|
247
|
+
this.cursor = this.buffer.length;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Theme State Management ────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export function getActiveTheme() {
|
|
255
|
+
return activeThemeName;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function setTheme(themeName) {
|
|
259
|
+
if (!themeName) return false;
|
|
260
|
+
const name = themeName.toLowerCase().trim();
|
|
261
|
+
if (THEMES[name]) {
|
|
262
|
+
activeThemeName = name;
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function getThemesList() {
|
|
269
|
+
return Object.keys(THEMES);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function stripCodeFences(content) {
|
|
273
|
+
let cleaned = content.trim();
|
|
274
|
+
if (cleaned.startsWith("```")) {
|
|
275
|
+
const firstNewline = cleaned.indexOf("\n");
|
|
276
|
+
if (firstNewline !== -1) {
|
|
277
|
+
cleaned = cleaned.slice(firstNewline + 1);
|
|
278
|
+
} else {
|
|
279
|
+
cleaned = cleaned.slice(3);
|
|
280
|
+
}
|
|
281
|
+
if (cleaned.endsWith("```")) {
|
|
282
|
+
cleaned = cleaned.slice(0, -3);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return cleaned.trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Interactive checkbox selector inside terminal using raw stdin. Renders scrollable pagination.
|
|
290
|
+
* Arrow Up/Down to navigate, Space to toggle, Enter to confirm, Esc/q to abort.
|
|
291
|
+
*/
|
|
292
|
+
export async function interactiveCheckbox(headerText, items, preselected = []) {
|
|
293
|
+
if (items.length === 0) return [];
|
|
294
|
+
|
|
295
|
+
const stdin = process.stdin;
|
|
296
|
+
const stdout = process.stdout;
|
|
297
|
+
|
|
298
|
+
const wasRaw = stdin.isRaw;
|
|
299
|
+
stdin.setRawMode(true);
|
|
300
|
+
stdin.resume();
|
|
301
|
+
stdin.setEncoding("utf8");
|
|
302
|
+
|
|
303
|
+
stdout.write("\x1b[?25l"); // Hide cursor
|
|
304
|
+
|
|
305
|
+
let activeIndex = 0;
|
|
306
|
+
const selected = new Set(preselected.map(item => items.indexOf(item)).filter(i => i !== -1));
|
|
307
|
+
|
|
308
|
+
const PAGE_SIZE = 10;
|
|
309
|
+
let startRow = 0;
|
|
310
|
+
let renderedLines = 0;
|
|
311
|
+
|
|
312
|
+
function render() {
|
|
313
|
+
if (renderedLines > 0) {
|
|
314
|
+
stdout.write(`\x1b[${renderedLines}A\x1b[J`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let lines = [];
|
|
318
|
+
lines.push(colors.brand(headerText));
|
|
319
|
+
|
|
320
|
+
if (activeIndex < startRow) {
|
|
321
|
+
startRow = activeIndex;
|
|
322
|
+
} else if (activeIndex >= startRow + PAGE_SIZE) {
|
|
323
|
+
startRow = activeIndex - PAGE_SIZE + 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const visibleEnd = Math.min(items.length, startRow + PAGE_SIZE);
|
|
327
|
+
for (let i = startRow; i < visibleEnd; i++) {
|
|
328
|
+
const isActive = i === activeIndex;
|
|
329
|
+
const isSelected = selected.has(i);
|
|
330
|
+
|
|
331
|
+
const pointer = isActive ? colors.accent("❯ ") : " ";
|
|
332
|
+
const checkbox = isSelected
|
|
333
|
+
? colors.success("[⬢] ")
|
|
334
|
+
: colors.muted("[⬡] ");
|
|
335
|
+
|
|
336
|
+
const itemText = isActive
|
|
337
|
+
? colors.brand(items[i])
|
|
338
|
+
: colors.text(items[i]);
|
|
339
|
+
|
|
340
|
+
lines.push(pointer + checkbox + itemText);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (items.length > PAGE_SIZE) {
|
|
344
|
+
lines.push(colors.dim(` (Arrow Keys, Page ${Math.floor(startRow/PAGE_SIZE) + 1}/${Math.ceil(items.length/PAGE_SIZE)})`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const outputStr = lines.join("\n") + "\n";
|
|
348
|
+
stdout.write(outputStr);
|
|
349
|
+
renderedLines = lines.length;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
render();
|
|
353
|
+
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
function handleKey(key) {
|
|
356
|
+
if (key === "\u0003" || key === "\u001b" || key === "q") { // Ctrl+C, Esc, q
|
|
357
|
+
cleanup();
|
|
358
|
+
resolve(null);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (key === "\r" || key === "\n") { // Enter
|
|
363
|
+
cleanup();
|
|
364
|
+
resolve([...selected].map(i => items[i]));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (key === " ") { // Spacebar
|
|
369
|
+
if (selected.has(activeIndex)) {
|
|
370
|
+
selected.delete(activeIndex);
|
|
371
|
+
} else {
|
|
372
|
+
selected.add(activeIndex);
|
|
373
|
+
}
|
|
374
|
+
render();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (key === "\u001b[A") { // Up Arrow
|
|
379
|
+
activeIndex = (activeIndex - 1 + items.length) % items.length;
|
|
380
|
+
render();
|
|
381
|
+
} else if (key === "\u001b[B") { // Down Arrow
|
|
382
|
+
activeIndex = (activeIndex + 1) % items.length;
|
|
383
|
+
render();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function cleanup() {
|
|
388
|
+
stdin.removeListener("data", handleKey);
|
|
389
|
+
stdin.setRawMode(wasRaw);
|
|
390
|
+
stdin.pause();
|
|
391
|
+
stdout.write("\x1b[?25h"); // Show cursor
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
stdin.on("data", handleKey);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Interactive single-select menu selector inside terminal. Renders scrollable pagination.
|
|
400
|
+
* Arrow Up/Down to navigate, Enter to select, Esc/q to abort.
|
|
401
|
+
*/
|
|
402
|
+
export async function interactiveMenu(headerText, items) {
|
|
403
|
+
if (items.length === 0) return null;
|
|
404
|
+
|
|
405
|
+
const stdin = process.stdin;
|
|
406
|
+
const stdout = process.stdout;
|
|
407
|
+
|
|
408
|
+
const wasRaw = stdin.isRaw;
|
|
409
|
+
stdin.setRawMode(true);
|
|
410
|
+
stdin.resume();
|
|
411
|
+
stdin.setEncoding("utf8");
|
|
412
|
+
|
|
413
|
+
stdout.write("\x1b[?25l"); // Hide cursor
|
|
414
|
+
|
|
415
|
+
let activeIndex = 0;
|
|
416
|
+
const PAGE_SIZE = 10;
|
|
417
|
+
let startRow = 0;
|
|
418
|
+
let renderedLines = 0;
|
|
419
|
+
|
|
420
|
+
function render() {
|
|
421
|
+
if (renderedLines > 0) {
|
|
422
|
+
stdout.write(`\x1b[${renderedLines}A\x1b[J`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let lines = [];
|
|
426
|
+
lines.push(colors.brand(headerText));
|
|
427
|
+
|
|
428
|
+
if (activeIndex < startRow) {
|
|
429
|
+
startRow = activeIndex;
|
|
430
|
+
} else if (activeIndex >= startRow + PAGE_SIZE) {
|
|
431
|
+
startRow = activeIndex - PAGE_SIZE + 1;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const visibleEnd = Math.min(items.length, startRow + PAGE_SIZE);
|
|
435
|
+
for (let i = startRow; i < visibleEnd; i++) {
|
|
436
|
+
const isActive = i === activeIndex;
|
|
437
|
+
const pointer = isActive ? colors.accent("❯ ") : " ";
|
|
438
|
+
const itemText = isActive
|
|
439
|
+
? colors.success(items[i])
|
|
440
|
+
: colors.text(items[i]);
|
|
441
|
+
|
|
442
|
+
lines.push(pointer + itemText);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (items.length > PAGE_SIZE) {
|
|
446
|
+
lines.push(colors.dim(` (Page ${Math.floor(startRow/PAGE_SIZE) + 1}/${Math.ceil(items.length/PAGE_SIZE)})`));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const outputStr = lines.join("\n") + "\n";
|
|
450
|
+
stdout.write(outputStr);
|
|
451
|
+
renderedLines = lines.length;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
render();
|
|
455
|
+
|
|
456
|
+
return new Promise((resolve) => {
|
|
457
|
+
function handleKey(key) {
|
|
458
|
+
if (key === "\u0003" || key === "\u001b" || key === "q") { // Ctrl+C, Esc, q
|
|
459
|
+
cleanup();
|
|
460
|
+
resolve(null);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (key === "\r" || key === "\n") { // Enter
|
|
465
|
+
cleanup();
|
|
466
|
+
resolve(activeIndex);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (key === "\u001b[A") { // Up Arrow
|
|
471
|
+
activeIndex = (activeIndex - 1 + items.length) % items.length;
|
|
472
|
+
render();
|
|
473
|
+
} else if (key === "\u001b[B") { // Down Arrow
|
|
474
|
+
activeIndex = (activeIndex + 1) % items.length;
|
|
475
|
+
render();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function cleanup() {
|
|
480
|
+
stdin.removeListener("data", handleKey);
|
|
481
|
+
stdin.setRawMode(wasRaw);
|
|
482
|
+
stdin.pause();
|
|
483
|
+
stdout.write("\x1b[?25h"); // Show cursor
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
stdin.on("data", handleKey);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Returns either a premium Nerd Font glyph or a standard Unicode emoji fallback
|
|
492
|
+
* depending on whether NERD_FONTS is enabled in the configuration.
|
|
493
|
+
* @param {string} name - Icon name
|
|
494
|
+
* @param {object} config - Active configuration object
|
|
495
|
+
* @returns {string}
|
|
496
|
+
*/
|
|
497
|
+
export function getIcon(name, config) {
|
|
498
|
+
const useNerd = config?.NERD_FONTS === true || config?.NERD_FONTS === "true";
|
|
499
|
+
|
|
500
|
+
const icons = {
|
|
501
|
+
workspace: useNerd ? "\uf07c " : "📂 ",
|
|
502
|
+
mode: useNerd ? "\uf0e0 " : "🧠 ", // brain / envelope-like modes icon
|
|
503
|
+
network: useNerd ? "\uf6ff " : "🟢 ", // network icon
|
|
504
|
+
engine: useNerd ? "\uf013 " : "⚙️ ", // gear icon
|
|
505
|
+
package: useNerd ? "\uf1b2 " : "📦 ", // package icon
|
|
506
|
+
mic: useNerd ? "\uf130 " : "🎤 ", // microphone icon
|
|
507
|
+
git: useNerd ? "\uf113 " : "🌿 ", // git/leaf icon
|
|
508
|
+
dashboard: useNerd ? "\uf201 " : "📊 ", // chart icon
|
|
509
|
+
bolt: useNerd ? "\uf0e7 " : "⚡ ", // lightning bolt icon
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
return icons[name] || "";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Zero-dependency dynamic theme-aware code block syntax highlighting.
|
|
517
|
+
* @param {string} code - The raw code to highlight
|
|
518
|
+
* @param {string} lang - The code block language (e.g. javascript, python, etc.)
|
|
519
|
+
* @returns {string} Colored code for terminal output
|
|
520
|
+
*/
|
|
521
|
+
export function highlightCode(code, lang) {
|
|
522
|
+
if (!lang) return colors.orange(code);
|
|
523
|
+
const l = lang.toLowerCase().trim();
|
|
524
|
+
|
|
525
|
+
// Theme-aware colors from the active theme proxy
|
|
526
|
+
const kwColor = colors.magenta; // keywords
|
|
527
|
+
const strColor = colors.success; // strings
|
|
528
|
+
const commentColor = colors.muted; // comments
|
|
529
|
+
const numColor = colors.accent2; // numbers
|
|
530
|
+
const fnColor = colors.accent; // function calls
|
|
531
|
+
const opColor = colors.warning; // operators
|
|
532
|
+
|
|
533
|
+
if (l === "javascript" || l === "js" || l === "typescript" || l === "ts" || l === "json") {
|
|
534
|
+
let result = code;
|
|
535
|
+
|
|
536
|
+
// Comments
|
|
537
|
+
const comments = [];
|
|
538
|
+
result = result.replace(/(\/\*[\s\S]*?\*\/|\/\/.*)/g, (match) => {
|
|
539
|
+
comments.push(commentColor(match));
|
|
540
|
+
return `__COMMENT_PLACEHOLDER_${comments.length - 1}__`;
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Strings
|
|
544
|
+
const strings = [];
|
|
545
|
+
result = result.replace(/(["'`])(?:\\.|[^\\])*?\1/g, (match) => {
|
|
546
|
+
strings.push(strColor(match));
|
|
547
|
+
return `__STRING_PLACEHOLDER_${strings.length - 1}__`;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Keywords
|
|
551
|
+
const keywords = /\b(const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|default|import|export|from|class|extends|new|this|async|await|try|catch|finally|throw|typeof|instanceof|in|of|null|undefined|true|false|void|delete|debugger)\b/g;
|
|
552
|
+
result = result.replace(keywords, (m) => kwColor(m));
|
|
553
|
+
|
|
554
|
+
// Numbers
|
|
555
|
+
result = result.replace(/\b(\d+(?:\.\d+)?)\b/g, (m) => numColor(m));
|
|
556
|
+
|
|
557
|
+
// Function calls
|
|
558
|
+
result = result.replace(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)(?=\s*\()/g, (m) => fnColor(m));
|
|
559
|
+
|
|
560
|
+
// Operators
|
|
561
|
+
result = result.replace(/([+\-*/%=<>!&|^~]+)/g, (m) => opColor(m));
|
|
562
|
+
|
|
563
|
+
// Restore strings and comments
|
|
564
|
+
result = result.replace(/__STRING_PLACEHOLDER_(\d+)__/g, (_, idx) => strings[parseInt(idx, 10)]);
|
|
565
|
+
result = result.replace(/__COMMENT_PLACEHOLDER_(\d+)__/g, (_, idx) => comments[parseInt(idx, 10)]);
|
|
566
|
+
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (l === "python" || l === "py") {
|
|
571
|
+
let result = code;
|
|
572
|
+
|
|
573
|
+
const comments = [];
|
|
574
|
+
result = result.replace(/(#.*)/g, (match) => {
|
|
575
|
+
comments.push(commentColor(match));
|
|
576
|
+
return `__COMMENT_PLACEHOLDER_${comments.length - 1}__`;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const strings = [];
|
|
580
|
+
result = result.replace(/(["'])(?:\\.|[^\\])*?\1/g, (match) => {
|
|
581
|
+
strings.push(strColor(match));
|
|
582
|
+
return `__STRING_PLACEHOLDER_${strings.length - 1}__`;
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const keywords = /\b(def|class|return|if|elif|else|for|while|break|continue|import|from|as|in|is|not|and|or|try|except|finally|raise|assert|with|lambda|pass|global|nonlocal|None|True|False)\b/g;
|
|
586
|
+
result = result.replace(keywords, (m) => kwColor(m));
|
|
587
|
+
|
|
588
|
+
result = result.replace(/\b(\d+(?:\.\d+)?)\b/g, (m) => numColor(m));
|
|
589
|
+
result = result.replace(/\b([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*\()/g, (m) => fnColor(m));
|
|
590
|
+
|
|
591
|
+
result = result.replace(/__STRING_PLACEHOLDER_(\d+)__/g, (_, idx) => strings[parseInt(idx, 10)]);
|
|
592
|
+
result = result.replace(/__COMMENT_PLACEHOLDER_(\d+)__/g, (_, idx) => comments[parseInt(idx, 10)]);
|
|
593
|
+
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (l === "html" || l === "xml" || l === "svg") {
|
|
598
|
+
let result = code;
|
|
599
|
+
result = result.replace(/(<\/?[a-zA-Z0-9:-]+)/g, (m) => kwColor(m));
|
|
600
|
+
result = result.replace(/(\/?>)/g, (m) => kwColor(m));
|
|
601
|
+
result = result.replace(/(="[^"]*")/g, (match) => "=" + strColor(match.slice(1)));
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (l === "css") {
|
|
606
|
+
let result = code;
|
|
607
|
+
result = result.replace(/([a-zA-Z0-9_#-]+)\s*:/g, (m, p1) => fnColor(p1) + ":");
|
|
608
|
+
result = result.replace(/:\s*([^;]+);/g, (match, p1) => ": " + numColor(p1) + ";");
|
|
609
|
+
result = result.replace(/(\.[a-zA-Z0-9_-]+)/g, (m) => kwColor(m));
|
|
610
|
+
result = result.replace(/(#[a-zA-Z0-9_-]+)/g, (m) => opColor(m));
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (l === "bash" || l === "sh" || l === "cmd" || l === "powershell" || l === "ps1") {
|
|
615
|
+
let result = code;
|
|
616
|
+
result = result.replace(/(#.*)/g, (m) => commentColor(m));
|
|
617
|
+
const keywords = /\b(echo|cd|ls|dir|pwd|git|npm|node|pip|python|mkdir|rm|cp|mv|cat|grep|tail|head)\b/g;
|
|
618
|
+
result = result.replace(keywords, (m) => kwColor(m));
|
|
619
|
+
return result;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return colors.orange(code);
|
|
623
|
+
}
|
|
624
|
+
|