@supatest/cli 0.0.5 → 0.0.7

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 (69) hide show
  1. package/dist/index.js +9512 -157
  2. package/package.json +9 -6
  3. package/dist/commands/login.js +0 -392
  4. package/dist/commands/setup.js +0 -234
  5. package/dist/config.js +0 -29
  6. package/dist/core/agent.js +0 -259
  7. package/dist/modes/headless.js +0 -117
  8. package/dist/modes/interactive.js +0 -418
  9. package/dist/presenters/composite.js +0 -32
  10. package/dist/presenters/console.js +0 -163
  11. package/dist/presenters/react.js +0 -217
  12. package/dist/presenters/types.js +0 -1
  13. package/dist/presenters/web.js +0 -78
  14. package/dist/prompts/builder.js +0 -181
  15. package/dist/prompts/fixer.js +0 -148
  16. package/dist/prompts/index.js +0 -3
  17. package/dist/prompts/planner.js +0 -70
  18. package/dist/services/api-client.js +0 -244
  19. package/dist/services/event-streamer.js +0 -130
  20. package/dist/types.js +0 -1
  21. package/dist/ui/App.js +0 -322
  22. package/dist/ui/components/AuthBanner.js +0 -24
  23. package/dist/ui/components/AuthDialog.js +0 -32
  24. package/dist/ui/components/Banner.js +0 -12
  25. package/dist/ui/components/ExpandableSection.js +0 -17
  26. package/dist/ui/components/Header.js +0 -51
  27. package/dist/ui/components/HelpMenu.js +0 -89
  28. package/dist/ui/components/InputPrompt.js +0 -286
  29. package/dist/ui/components/MessageList.js +0 -42
  30. package/dist/ui/components/QueuedMessageDisplay.js +0 -31
  31. package/dist/ui/components/Scrollable.js +0 -103
  32. package/dist/ui/components/SessionSelector.js +0 -196
  33. package/dist/ui/components/StatusBar.js +0 -34
  34. package/dist/ui/components/messages/AssistantMessage.js +0 -20
  35. package/dist/ui/components/messages/ErrorMessage.js +0 -26
  36. package/dist/ui/components/messages/LoadingMessage.js +0 -28
  37. package/dist/ui/components/messages/ThinkingMessage.js +0 -17
  38. package/dist/ui/components/messages/TodoMessage.js +0 -44
  39. package/dist/ui/components/messages/ToolMessage.js +0 -218
  40. package/dist/ui/components/messages/UserMessage.js +0 -14
  41. package/dist/ui/contexts/KeypressContext.js +0 -527
  42. package/dist/ui/contexts/MouseContext.js +0 -98
  43. package/dist/ui/contexts/SessionContext.js +0 -129
  44. package/dist/ui/hooks/useAnimatedScrollbar.js +0 -83
  45. package/dist/ui/hooks/useBatchedScroll.js +0 -22
  46. package/dist/ui/hooks/useBracketedPaste.js +0 -31
  47. package/dist/ui/hooks/useFocus.js +0 -50
  48. package/dist/ui/hooks/useKeypress.js +0 -26
  49. package/dist/ui/hooks/useModeToggle.js +0 -25
  50. package/dist/ui/types/auth.js +0 -13
  51. package/dist/ui/utils/file-completion.js +0 -56
  52. package/dist/ui/utils/input.js +0 -50
  53. package/dist/ui/utils/markdown.js +0 -376
  54. package/dist/ui/utils/mouse.js +0 -189
  55. package/dist/ui/utils/theme.js +0 -59
  56. package/dist/utils/banner.js +0 -9
  57. package/dist/utils/encryption.js +0 -71
  58. package/dist/utils/events.js +0 -36
  59. package/dist/utils/keychain-storage.js +0 -120
  60. package/dist/utils/logger.js +0 -209
  61. package/dist/utils/node-version.js +0 -89
  62. package/dist/utils/plan-file.js +0 -75
  63. package/dist/utils/project-instructions.js +0 -23
  64. package/dist/utils/rich-logger.js +0 -208
  65. package/dist/utils/stdin.js +0 -25
  66. package/dist/utils/stdio.js +0 -80
  67. package/dist/utils/summary.js +0 -94
  68. package/dist/utils/token-storage.js +0 -242
  69. package/dist/version.js +0 -6
@@ -1,376 +0,0 @@
1
- /**
2
- * Markdown rendering for terminal UI
3
- * Custom implementation with improved table rendering, code block syntax highlighting,
4
- * and full theme integration
5
- */
6
- import chalk from "chalk";
7
- import { Box, Text } from "ink";
8
- import { all, createLowlight } from "lowlight";
9
- import React, { useMemo } from "react";
10
- import { theme } from "./theme.js";
11
- const lowlight = createLowlight(all);
12
- /**
13
- * Parse markdown and extract tables, code blocks, and text sections
14
- */
15
- function parseMarkdownSections(text) {
16
- const sections = [];
17
- const lines = text.split(/\r?\n/);
18
- let currentTextSection = "";
19
- let inCodeBlock = false;
20
- let codeBlockLanguage = "";
21
- let codeBlockLines = [];
22
- let inTable = false;
23
- let tableRows = [];
24
- const flushTextSection = () => {
25
- if (currentTextSection.trim()) {
26
- sections.push({
27
- type: "text",
28
- content: currentTextSection.trim(),
29
- });
30
- currentTextSection = "";
31
- }
32
- };
33
- const flushCodeBlock = () => {
34
- if (codeBlockLines.length > 0) {
35
- flushTextSection();
36
- sections.push({
37
- type: "code",
38
- content: codeBlockLines.join("\n"),
39
- codeLanguage: codeBlockLanguage,
40
- });
41
- codeBlockLines = [];
42
- codeBlockLanguage = "";
43
- }
44
- };
45
- const flushTable = () => {
46
- if (tableRows.length > 0) {
47
- flushTextSection();
48
- sections.push({
49
- type: "table",
50
- content: "",
51
- tableRows: [...tableRows],
52
- });
53
- tableRows = [];
54
- }
55
- };
56
- for (const line of lines) {
57
- // Code fence detection
58
- const codeFenceMatch = line.match(/^```(\w*)/);
59
- if (codeFenceMatch) {
60
- if (!inCodeBlock) {
61
- flushTextSection();
62
- flushTable();
63
- inCodeBlock = true;
64
- codeBlockLanguage = codeFenceMatch[1] || "";
65
- }
66
- else {
67
- inCodeBlock = false;
68
- flushCodeBlock();
69
- }
70
- continue;
71
- }
72
- // Inside code block
73
- if (inCodeBlock) {
74
- codeBlockLines.push(line);
75
- continue;
76
- }
77
- // Table detection
78
- const trimmedLine = line.trim();
79
- const isTableRow = trimmedLine.match(/^\|.+\|$/);
80
- const isTableSeparator = trimmedLine.match(/^\|[\s\-:]+\|$/) && trimmedLine.includes("-");
81
- if (isTableRow || isTableSeparator) {
82
- if (!inTable) {
83
- flushTextSection();
84
- inTable = true;
85
- }
86
- // Skip separator rows
87
- if (!isTableSeparator) {
88
- tableRows.push(line);
89
- }
90
- continue;
91
- }
92
- // End of table
93
- if (inTable) {
94
- flushTable();
95
- inTable = false;
96
- }
97
- // Regular text
98
- if (currentTextSection) {
99
- currentTextSection += "\n" + line;
100
- }
101
- else {
102
- currentTextSection = line;
103
- }
104
- }
105
- // Flush any remaining sections
106
- if (inCodeBlock) {
107
- flushCodeBlock();
108
- }
109
- if (inTable) {
110
- flushTable();
111
- }
112
- flushTextSection();
113
- return sections;
114
- }
115
- /**
116
- * Parse and render markdown text for terminal display
117
- */
118
- export const MarkdownDisplay = ({ text, isPending = false, terminalWidth = 80, }) => {
119
- const sections = useMemo(() => parseMarkdownSections(text), [text]);
120
- const elements = sections.map((section, index) => {
121
- if (section.type === "table" && section.tableRows) {
122
- return (React.createElement(Table, { key: `table-${index}`, rows: section.tableRows }));
123
- }
124
- if (section.type === "code") {
125
- return (React.createElement(CodeBlock, { code: section.content, key: `code-${index}`, language: section.codeLanguage || "" }));
126
- }
127
- // Render text sections with custom markdown parser
128
- return (React.createElement(TextSection, { content: section.content, key: `text-${index}` }));
129
- });
130
- // Add cursor for pending state
131
- if (isPending) {
132
- elements.push(React.createElement(Text, { color: theme.text.accent, key: "cursor" }, "\u258A"));
133
- }
134
- return React.createElement(Box, { flexDirection: "column" }, elements);
135
- };
136
- /**
137
- * Render text section with markdown formatting
138
- */
139
- const TextSection = ({ content }) => {
140
- const lines = content.split(/\r?\n/);
141
- const elements = [];
142
- let lineIndex = 0;
143
- for (const line of lines) {
144
- lineIndex++;
145
- // Header detection (# H1, ## H2, etc.)
146
- const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
147
- if (headerMatch) {
148
- const level = headerMatch[1].length;
149
- const content = headerMatch[2];
150
- elements.push(React.createElement(Header, { content: content, key: `header-${lineIndex}`, level: level }));
151
- continue;
152
- }
153
- // List item detection (- item or * item or 1. item)
154
- const listMatch = line.match(/^(\s*)([-*]|\d+\.)\s+(.+)/);
155
- if (listMatch) {
156
- const indent = listMatch[1].length;
157
- const marker = listMatch[2];
158
- const content = listMatch[3];
159
- elements.push(React.createElement(ListItem, { content: content, indent: indent, key: `list-${lineIndex}`, marker: marker }));
160
- continue;
161
- }
162
- // Horizontal rule
163
- if (line.match(/^-{3,}$/)) {
164
- elements.push(React.createElement(HorizontalRule, { key: `hr-${lineIndex}` }));
165
- continue;
166
- }
167
- // Regular paragraph with inline formatting
168
- if (line.trim()) {
169
- elements.push(React.createElement(Paragraph, { content: line, key: `para-${lineIndex}` }));
170
- }
171
- else {
172
- // Empty line
173
- elements.push(React.createElement(Text, { key: `empty-${lineIndex}` }, "\n"));
174
- }
175
- }
176
- return React.createElement(Box, { flexDirection: "column" }, elements);
177
- };
178
- /**
179
- * Render a header (h1-h6)
180
- */
181
- const Header = ({ level, content, }) => {
182
- const colors = [
183
- theme.text.accent, // H1
184
- theme.text.accent, // H2
185
- theme.text.primary, // H3
186
- theme.text.secondary, // H4
187
- theme.text.secondary, // H5
188
- theme.text.dim, // H6
189
- ];
190
- const color = colors[level - 1] || theme.text.primary;
191
- const bold = level <= 2;
192
- return (React.createElement(Text, { bold: bold, color: color },
193
- content,
194
- "\n"));
195
- };
196
- /**
197
- * Render a list item
198
- */
199
- const ListItem = ({ indent, marker, content }) => {
200
- const formattedContent = formatInline(content);
201
- const indentStr = " ".repeat(indent);
202
- return (React.createElement(Text, null,
203
- indentStr,
204
- React.createElement(Text, { color: theme.text.accent }, marker),
205
- " ",
206
- formattedContent,
207
- "\n"));
208
- };
209
- /**
210
- * Render a horizontal rule
211
- */
212
- const HorizontalRule = () => {
213
- return (React.createElement(Text, { color: theme.border.default },
214
- "─".repeat(60),
215
- "\n"));
216
- };
217
- /**
218
- * Render a paragraph with inline formatting
219
- */
220
- const Paragraph = ({ content }) => {
221
- const formattedContent = formatInline(content);
222
- return (React.createElement(Text, null,
223
- formattedContent,
224
- "\n"));
225
- };
226
- /**
227
- * Format inline markdown (bold, italic, inline code, links)
228
- */
229
- function formatInline(text) {
230
- let result = text;
231
- // Inline code: `code`
232
- result = result.replace(/`([^`]+)`/g, (_, code) => {
233
- return chalk.cyan(code);
234
- });
235
- // Bold: **text** or __text__
236
- result = result.replace(/(\*\*|__)([^*_]+)\1/g, (_, __, text) => {
237
- return chalk.bold(text);
238
- });
239
- // Italic: *text* or _text_ (but not **, __, or inside words)
240
- result = result.replace(/(?<!\*)\*(?!\*)([^*]+)\*(?!\*)/g, (_, text) => {
241
- return chalk.italic(text);
242
- });
243
- // Links: [text](url) - show as underlined cyan text
244
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text) => {
245
- return chalk.cyan.underline(text);
246
- });
247
- return result;
248
- }
249
- /**
250
- * Render a markdown table with improved formatting
251
- */
252
- const Table = ({ rows }) => {
253
- if (rows.length === 0) {
254
- return null;
255
- }
256
- // Parse all rows to extract cells
257
- const parsedRows = rows.map((row) => {
258
- const cells = row
259
- .trim()
260
- .replace(/^\||\|$/g, "")
261
- .split("|")
262
- .map((cell) => cell.trim());
263
- return cells;
264
- });
265
- if (parsedRows.length === 0) {
266
- return null;
267
- }
268
- // Determine column widths (calculate based on actual content length)
269
- const numColumns = parsedRows[0]?.length || 0;
270
- const columnWidths = [];
271
- for (let col = 0; col < numColumns; col++) {
272
- let maxWidth = 0;
273
- for (const row of parsedRows) {
274
- const cellContent = row[col] || "";
275
- // Strip ANSI codes for width calculation
276
- const plainText = cellContent.replace(/\u001b\[[0-9;]*m/g, "");
277
- const width = plainText.length;
278
- maxWidth = Math.max(maxWidth, width);
279
- }
280
- columnWidths.push(Math.max(maxWidth, 3)); // Minimum width of 3
281
- }
282
- // Build formatted table as text with better visual separation
283
- const tableLines = [];
284
- // Header row (first row)
285
- if (parsedRows.length > 0) {
286
- const headerRow = parsedRows[0];
287
- const headerCells = headerRow.map((cell, colIndex) => {
288
- const formatted = formatInline(cell);
289
- const width = columnWidths[colIndex] || 10;
290
- return formatted.padEnd(width);
291
- });
292
- tableLines.push(chalk.hex(theme.text.accent).bold(headerCells.join(" │ ")));
293
- // Separator line with better visual design
294
- const separator = columnWidths
295
- .map((width) => "─".repeat(width))
296
- .join("─┼─");
297
- tableLines.push(chalk.hex(theme.border.default)(separator));
298
- // Data rows
299
- for (let rowIndex = 1; rowIndex < parsedRows.length; rowIndex++) {
300
- const row = parsedRows[rowIndex];
301
- const cells = row.map((cell, colIndex) => {
302
- const formatted = formatInline(cell);
303
- const width = columnWidths[colIndex] || 10;
304
- return formatted.padEnd(width);
305
- });
306
- tableLines.push(cells.join(" │ "));
307
- }
308
- }
309
- return (React.createElement(Text, null,
310
- tableLines.join("\n"),
311
- "\n"));
312
- };
313
- /**
314
- * Code block with syntax highlighting
315
- */
316
- const CodeBlock = ({ code, language, }) => {
317
- // Try to highlight if language is specified
318
- let highlighted = code;
319
- if (language) {
320
- try {
321
- const result = lowlight.highlight(language, code);
322
- highlighted = highlightTreeToString(result);
323
- }
324
- catch (error) {
325
- // Language not supported, use plain text
326
- highlighted = code;
327
- }
328
- }
329
- return (React.createElement(Box, { borderColor: theme.border.default, borderStyle: "round", flexDirection: "column", marginY: 0, paddingX: 1 },
330
- language && (React.createElement(Text, { color: theme.text.dim },
331
- language,
332
- "\n")),
333
- React.createElement(Text, null, highlighted)));
334
- };
335
- /**
336
- * Convert lowlight AST to colored string
337
- */
338
- function highlightTreeToString(node) {
339
- if (node.type === "text") {
340
- return node.value;
341
- }
342
- if (node.type === "element") {
343
- const className = node.properties?.className?.[0] || "";
344
- let content = "";
345
- if (node.children) {
346
- content = node.children.map(highlightTreeToString).join("");
347
- }
348
- // Map token types to colors
349
- if (className.includes("keyword")) {
350
- return chalk.magenta(content);
351
- }
352
- if (className.includes("string")) {
353
- return chalk.green(content);
354
- }
355
- if (className.includes("comment")) {
356
- return chalk.gray(content);
357
- }
358
- if (className.includes("function")) {
359
- return chalk.blue(content);
360
- }
361
- if (className.includes("number")) {
362
- return chalk.cyan(content);
363
- }
364
- if (className.includes("operator")) {
365
- return chalk.yellow(content);
366
- }
367
- if (className.includes("class-name") || className.includes("type")) {
368
- return chalk.yellow(content);
369
- }
370
- return content;
371
- }
372
- if (Array.isArray(node)) {
373
- return node.map(highlightTreeToString).join("");
374
- }
375
- return "";
376
- }
@@ -1,189 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright 2025 Google LLC
4
- * SPDX-License-Identifier: Apache-2.0
5
- */
6
- import { writeToStdout } from '../../utils/stdio';
7
- import { couldBeMouseSequence as inputCouldBeMouseSequence, SGR_EVENT_PREFIX, SGR_MOUSE_REGEX, X11_EVENT_PREFIX, X11_MOUSE_REGEX, } from './input';
8
- export function getMouseEventName(buttonCode, isRelease) {
9
- const isMove = (buttonCode & 32) !== 0;
10
- if (buttonCode === 66) {
11
- return 'scroll-left';
12
- }
13
- else if (buttonCode === 67) {
14
- return 'scroll-right';
15
- }
16
- else if ((buttonCode & 64) === 64) {
17
- if ((buttonCode & 1) === 0) {
18
- return 'scroll-up';
19
- }
20
- else {
21
- return 'scroll-down';
22
- }
23
- }
24
- else if (isMove) {
25
- return 'move';
26
- }
27
- else {
28
- const button = buttonCode & 3;
29
- const type = isRelease ? 'release' : 'press';
30
- switch (button) {
31
- case 0:
32
- return `left-${type}`;
33
- case 1:
34
- return `middle-${type}`;
35
- case 2:
36
- return `right-${type}`;
37
- default:
38
- return null;
39
- }
40
- }
41
- }
42
- function getButtonFromCode(code) {
43
- const button = code & 3;
44
- switch (button) {
45
- case 0:
46
- return 'left';
47
- case 1:
48
- return 'middle';
49
- case 2:
50
- return 'right';
51
- default:
52
- return 'none';
53
- }
54
- }
55
- export function parseSGRMouseEvent(buffer) {
56
- const match = buffer.match(SGR_MOUSE_REGEX);
57
- if (match) {
58
- const buttonCode = parseInt(match[1], 10);
59
- const col = parseInt(match[2], 10);
60
- const row = parseInt(match[3], 10);
61
- const action = match[4];
62
- const isRelease = action === 'm';
63
- const shift = (buttonCode & 4) !== 0;
64
- const meta = (buttonCode & 8) !== 0;
65
- const ctrl = (buttonCode & 16) !== 0;
66
- const name = getMouseEventName(buttonCode, isRelease);
67
- if (name) {
68
- return {
69
- event: {
70
- name,
71
- ctrl,
72
- meta,
73
- shift,
74
- col,
75
- row,
76
- button: getButtonFromCode(buttonCode),
77
- },
78
- length: match[0].length,
79
- };
80
- }
81
- return null;
82
- }
83
- return null;
84
- }
85
- export function parseX11MouseEvent(buffer) {
86
- const match = buffer.match(X11_MOUSE_REGEX);
87
- if (!match)
88
- return null;
89
- // The 3 bytes are in match[1]
90
- const b = match[1].charCodeAt(0) - 32;
91
- const col = match[1].charCodeAt(1) - 32;
92
- const row = match[1].charCodeAt(2) - 32;
93
- const shift = (b & 4) !== 0;
94
- const meta = (b & 8) !== 0;
95
- const ctrl = (b & 16) !== 0;
96
- const isMove = (b & 32) !== 0;
97
- const isWheel = (b & 64) !== 0;
98
- let name = null;
99
- if (isWheel) {
100
- const button = b & 3;
101
- switch (button) {
102
- case 0:
103
- name = 'scroll-up';
104
- break;
105
- case 1:
106
- name = 'scroll-down';
107
- break;
108
- default:
109
- break;
110
- }
111
- }
112
- else if (isMove) {
113
- name = 'move';
114
- }
115
- else {
116
- const button = b & 3;
117
- if (button === 3) {
118
- // X11 reports 'release' (3) for all button releases without specifying which one.
119
- // We'll default to 'left-release' as a best-effort guess if we don't track state.
120
- name = 'left-release';
121
- }
122
- else {
123
- switch (button) {
124
- case 0:
125
- name = 'left-press';
126
- break;
127
- case 1:
128
- name = 'middle-press';
129
- break;
130
- case 2:
131
- name = 'right-press';
132
- break;
133
- default:
134
- break;
135
- }
136
- }
137
- }
138
- if (name) {
139
- let button = getButtonFromCode(b);
140
- if (name === 'left-release' && button === 'none') {
141
- button = 'left';
142
- }
143
- return {
144
- event: {
145
- name,
146
- ctrl,
147
- meta,
148
- shift,
149
- col,
150
- row,
151
- button,
152
- },
153
- length: match[0].length,
154
- };
155
- }
156
- return null;
157
- }
158
- export function parseMouseEvent(buffer) {
159
- return parseSGRMouseEvent(buffer) || parseX11MouseEvent(buffer);
160
- }
161
- export function isIncompleteMouseSequence(buffer) {
162
- if (!inputCouldBeMouseSequence(buffer))
163
- return false;
164
- // If it matches a complete sequence, it's not incomplete.
165
- if (parseMouseEvent(buffer))
166
- return false;
167
- if (buffer.startsWith(X11_EVENT_PREFIX)) {
168
- // X11 needs exactly 3 bytes after prefix.
169
- return buffer.length < X11_EVENT_PREFIX.length + 3;
170
- }
171
- if (buffer.startsWith(SGR_EVENT_PREFIX)) {
172
- // SGR sequences end with 'm' or 'M'.
173
- // If it doesn't have it yet, it's incomplete.
174
- // Add a reasonable max length check to fail early on garbage.
175
- return !/[mM]/.test(buffer) && buffer.length < 50;
176
- }
177
- // It's a prefix of the prefix (e.g. "ESC" or "ESC [")
178
- return true;
179
- }
180
- export function enableMouseEvents() {
181
- // Enable mouse tracking with SGR format
182
- // ?1002h = button event tracking (clicks + drags + scroll wheel)
183
- // ?1006h = SGR extended mouse mode (better coordinate handling)
184
- writeToStdout('\u001b[?1002h\u001b[?1006h');
185
- }
186
- export function disableMouseEvents() {
187
- // Disable mouse tracking with SGR format
188
- writeToStdout('\u001b[?1006l\u001b[?1002l');
189
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * UI Theme for Supatest CLI
3
- * Defines colors and styling for the interactive terminal UI
4
- */
5
- export const theme = {
6
- // Text colors
7
- text: {
8
- primary: "#FFFFFF",
9
- secondary: "#A0AEC0",
10
- dim: "#4A5568",
11
- accent: "#38B2AC", // Cyan/teal accent
12
- success: "#48BB78",
13
- error: "#F56565",
14
- warning: "#ED8936",
15
- info: "#4299E1",
16
- },
17
- // Background colors (for boxed content)
18
- bg: {
19
- primary: "#1A202C",
20
- secondary: "#2D3748",
21
- accent: "#234E52",
22
- },
23
- // Borders
24
- border: {
25
- default: "#4A5568",
26
- accent: "#38B2AC",
27
- error: "#F56565",
28
- },
29
- // Tool/action colors
30
- tool: {
31
- read: "#4299E1", // Blue
32
- write: "#48BB78", // Green
33
- edit: "#ED8936", // Orange
34
- bash: "#38B2AC", // Cyan
35
- search: "#9F7AEA", // Purple
36
- agent: "#ED64A6", // Pink
37
- },
38
- // Status colors
39
- status: {
40
- pending: "#A0AEC0",
41
- inProgress: "#ED8936",
42
- completed: "#48BB78",
43
- failed: "#F56565",
44
- },
45
- };
46
- /**
47
- * Get color value by path (e.g., 'text.primary', 'tool.read')
48
- */
49
- export function getThemeColor(path) {
50
- const parts = path.split(".");
51
- let current = theme;
52
- for (const part of parts) {
53
- if (current[part] === undefined) {
54
- return theme.text.primary;
55
- }
56
- current = current[part];
57
- }
58
- return typeof current === "string" ? current : theme.text.primary;
59
- }
@@ -1,9 +0,0 @@
1
- export function getBanner() {
2
- const banner = `███████╗██╗ ██╗██████╗ █████╗ ████████╗███████╗███████╗████████╗
3
- ██╔════╝██║ ██║██╔══██╗██╔══██╗╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝
4
- ███████╗██║ ██║██████╔╝███████║ ██║ █████╗ ███████╗ ██║
5
- ╚════██║██║ ██║██╔═══╝ ██╔══██║ ██║ ██╔══╝ ╚════██║ ██║
6
- ███████║╚██████╔╝██║ ██║ ██║ ██║ ███████╗███████║ ██║
7
- ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝ ╚═╝`;
8
- return banner;
9
- }
@@ -1,71 +0,0 @@
1
- /**
2
- * Token encryption utilities using AES-256-GCM with machine-specific key derivation.
3
- * Inspired by Gemini CLI's file-token-storage implementation.
4
- */
5
- import crypto from "node:crypto";
6
- import { hostname, userInfo } from "node:os";
7
- const ALGORITHM = "aes-256-gcm";
8
- const KEY_LENGTH = 32;
9
- const IV_LENGTH = 16;
10
- /**
11
- * Derive an encryption key from machine-specific identifiers.
12
- * Uses scrypt for key derivation with a salt based on hostname and username.
13
- * This means tokens encrypted on one machine cannot be decrypted on another.
14
- */
15
- function deriveEncryptionKey() {
16
- const salt = `${hostname()}-${userInfo().username}-supatest-cli`;
17
- return crypto.scryptSync("supatest-cli-token", salt, KEY_LENGTH);
18
- }
19
- // Cache the derived key for performance
20
- let cachedKey = null;
21
- function getEncryptionKey() {
22
- if (!cachedKey) {
23
- cachedKey = deriveEncryptionKey();
24
- }
25
- return cachedKey;
26
- }
27
- /**
28
- * Encrypt plaintext using AES-256-GCM with a machine-derived key.
29
- * Returns a colon-separated string: iv:authTag:ciphertext (all hex encoded)
30
- */
31
- export function encrypt(plaintext) {
32
- const key = getEncryptionKey();
33
- const iv = crypto.randomBytes(IV_LENGTH);
34
- const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
35
- let encrypted = cipher.update(plaintext, "utf8", "hex");
36
- encrypted += cipher.final("hex");
37
- const authTag = cipher.getAuthTag();
38
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
39
- }
40
- /**
41
- * Decrypt ciphertext that was encrypted with the encrypt() function.
42
- * Expects format: iv:authTag:ciphertext (all hex encoded)
43
- */
44
- export function decrypt(encryptedData) {
45
- const parts = encryptedData.split(":");
46
- if (parts.length !== 3) {
47
- throw new Error("Invalid encrypted data format");
48
- }
49
- const [ivHex, authTagHex, encrypted] = parts;
50
- const iv = Buffer.from(ivHex, "hex");
51
- const authTag = Buffer.from(authTagHex, "hex");
52
- const key = getEncryptionKey();
53
- const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
54
- decipher.setAuthTag(authTag);
55
- let decrypted = decipher.update(encrypted, "hex", "utf8");
56
- decrypted += decipher.final("utf8");
57
- return decrypted;
58
- }
59
- /**
60
- * Check if a string appears to be in the encrypted format.
61
- * Encrypted format has 3 hex segments separated by colons.
62
- */
63
- export function isEncrypted(data) {
64
- const parts = data.split(":");
65
- if (parts.length !== 3) {
66
- return false;
67
- }
68
- // Check each part looks like valid hex
69
- const hexRegex = /^[0-9a-fA-F]+$/;
70
- return parts.every((p) => p.length > 0 && hexRegex.test(p));
71
- }