@runloop/rl-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +54 -0
  2. package/dist/cli.js +73 -60
  3. package/dist/commands/auth.js +0 -1
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +215 -213
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/blueprint/preview.js +42 -38
  9. package/dist/commands/config.js +117 -0
  10. package/dist/commands/devbox/create.js +120 -40
  11. package/dist/commands/devbox/delete.js +17 -33
  12. package/dist/commands/devbox/download.js +29 -43
  13. package/dist/commands/devbox/exec.js +22 -39
  14. package/dist/commands/devbox/execAsync.js +20 -37
  15. package/dist/commands/devbox/get.js +13 -35
  16. package/dist/commands/devbox/getAsync.js +12 -34
  17. package/dist/commands/devbox/list.js +241 -402
  18. package/dist/commands/devbox/logs.js +20 -38
  19. package/dist/commands/devbox/read.js +29 -43
  20. package/dist/commands/devbox/resume.js +13 -35
  21. package/dist/commands/devbox/rsync.js +26 -78
  22. package/dist/commands/devbox/scp.js +25 -79
  23. package/dist/commands/devbox/sendStdin.js +41 -0
  24. package/dist/commands/devbox/shutdown.js +13 -35
  25. package/dist/commands/devbox/ssh.js +45 -78
  26. package/dist/commands/devbox/suspend.js +13 -35
  27. package/dist/commands/devbox/tunnel.js +36 -88
  28. package/dist/commands/devbox/upload.js +28 -36
  29. package/dist/commands/devbox/write.js +29 -44
  30. package/dist/commands/mcp-install.js +4 -3
  31. package/dist/commands/menu.js +24 -66
  32. package/dist/commands/object/delete.js +12 -34
  33. package/dist/commands/object/download.js +26 -74
  34. package/dist/commands/object/get.js +12 -34
  35. package/dist/commands/object/list.js +15 -93
  36. package/dist/commands/object/upload.js +35 -96
  37. package/dist/commands/snapshot/create.js +23 -39
  38. package/dist/commands/snapshot/delete.js +17 -33
  39. package/dist/commands/snapshot/get.js +16 -0
  40. package/dist/commands/snapshot/list.js +309 -80
  41. package/dist/commands/snapshot/status.js +12 -34
  42. package/dist/components/ActionsPopup.js +63 -39
  43. package/dist/components/Breadcrumb.js +10 -52
  44. package/dist/components/DevboxActionsMenu.js +182 -110
  45. package/dist/components/DevboxCreatePage.js +12 -7
  46. package/dist/components/DevboxDetailPage.js +76 -28
  47. package/dist/components/ErrorBoundary.js +29 -0
  48. package/dist/components/ErrorMessage.js +10 -2
  49. package/dist/components/Header.js +12 -4
  50. package/dist/components/InteractiveSpawn.js +94 -0
  51. package/dist/components/MainMenu.js +36 -32
  52. package/dist/components/MetadataDisplay.js +4 -4
  53. package/dist/components/OperationsMenu.js +1 -1
  54. package/dist/components/ResourceActionsMenu.js +4 -4
  55. package/dist/components/ResourceListView.js +46 -34
  56. package/dist/components/Spinner.js +7 -2
  57. package/dist/components/StatusBadge.js +1 -1
  58. package/dist/components/SuccessMessage.js +12 -2
  59. package/dist/components/Table.js +16 -6
  60. package/dist/hooks/useCursorPagination.js +125 -85
  61. package/dist/hooks/useExitOnCtrlC.js +14 -0
  62. package/dist/hooks/useViewportHeight.js +47 -0
  63. package/dist/mcp/server.js +65 -6
  64. package/dist/router/Router.js +68 -0
  65. package/dist/router/types.js +1 -0
  66. package/dist/screens/BlueprintListScreen.js +7 -0
  67. package/dist/screens/DevboxActionsScreen.js +25 -0
  68. package/dist/screens/DevboxCreateScreen.js +11 -0
  69. package/dist/screens/DevboxDetailScreen.js +60 -0
  70. package/dist/screens/DevboxListScreen.js +23 -0
  71. package/dist/screens/LogsSessionScreen.js +49 -0
  72. package/dist/screens/MenuScreen.js +23 -0
  73. package/dist/screens/SSHSessionScreen.js +55 -0
  74. package/dist/screens/SnapshotListScreen.js +7 -0
  75. package/dist/services/blueprintService.js +105 -0
  76. package/dist/services/devboxService.js +215 -0
  77. package/dist/services/snapshotService.js +81 -0
  78. package/dist/store/blueprintStore.js +89 -0
  79. package/dist/store/devboxStore.js +105 -0
  80. package/dist/store/index.js +7 -0
  81. package/dist/store/navigationStore.js +101 -0
  82. package/dist/store/snapshotStore.js +87 -0
  83. package/dist/utils/CommandExecutor.js +53 -24
  84. package/dist/utils/client.js +0 -2
  85. package/dist/utils/config.js +20 -90
  86. package/dist/utils/interactiveCommand.js +3 -2
  87. package/dist/utils/logFormatter.js +162 -0
  88. package/dist/utils/memoryMonitor.js +85 -0
  89. package/dist/utils/output.js +150 -59
  90. package/dist/utils/screen.js +23 -0
  91. package/dist/utils/ssh.js +3 -1
  92. package/dist/utils/sshSession.js +5 -29
  93. package/dist/utils/terminalDetection.js +97 -0
  94. package/dist/utils/terminalSync.js +39 -0
  95. package/dist/utils/theme.js +147 -13
  96. package/package.json +16 -13
@@ -1,17 +1,98 @@
1
1
  /**
2
2
  * Utility for handling different output formats across CLI commands
3
+ *
4
+ * Simple API:
5
+ * - output(data, options) - outputs data in specified format
6
+ * - outputError(message, error) - outputs error and exits
3
7
  */
4
8
  import YAML from "yaml";
5
9
  /**
6
- * Check if the command should use non-interactive output
10
+ * Resolve the output format from options
7
11
  */
8
- export function shouldUseNonInteractiveOutput(options) {
9
- return !!options.output && options.output !== "interactive";
12
+ function resolveFormat(options) {
13
+ const format = options.format || options.defaultFormat || "json";
14
+ if (format === "json" || format === "yaml" || format === "text") {
15
+ return format;
16
+ }
17
+ console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
18
+ process.exit(1);
10
19
  }
11
20
  /**
12
- * Output data in the specified format
21
+ * Format a value for text output (key-value pairs)
13
22
  */
14
- export function outputData(data, format = "json") {
23
+ function formatKeyValue(data, indent = 0) {
24
+ const prefix = " ".repeat(indent);
25
+ if (data === null || data === undefined) {
26
+ return `${prefix}(none)`;
27
+ }
28
+ if (typeof data === "string" ||
29
+ typeof data === "number" ||
30
+ typeof data === "boolean") {
31
+ return String(data);
32
+ }
33
+ if (Array.isArray(data)) {
34
+ if (data.length === 0) {
35
+ return `${prefix}(empty)`;
36
+ }
37
+ // For arrays of primitives, join them
38
+ if (data.every((item) => typeof item !== "object" || item === null)) {
39
+ return data.join(", ");
40
+ }
41
+ // For arrays of objects, format each with separator
42
+ return data
43
+ .map((item) => {
44
+ if (typeof item === "object" && item !== null) {
45
+ const lines = [];
46
+ for (const [key, value] of Object.entries(item)) {
47
+ if (value !== null && value !== undefined) {
48
+ const formattedValue = typeof value === "object"
49
+ ? formatKeyValue(value, indent + 1)
50
+ : String(value);
51
+ lines.push(`${prefix}${key}: ${formattedValue}`);
52
+ }
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+ return `${prefix}${item}`;
57
+ })
58
+ .join(`\n${prefix}---\n`);
59
+ }
60
+ if (typeof data === "object") {
61
+ const lines = [];
62
+ for (const [key, value] of Object.entries(data)) {
63
+ if (value !== null && value !== undefined) {
64
+ if (typeof value === "object" && !Array.isArray(value)) {
65
+ lines.push(`${prefix}${key}:`);
66
+ lines.push(formatKeyValue(value, indent + 1));
67
+ }
68
+ else if (Array.isArray(value)) {
69
+ lines.push(`${prefix}${key}: ${formatKeyValue(value, 0)}`);
70
+ }
71
+ else {
72
+ lines.push(`${prefix}${key}: ${value}`);
73
+ }
74
+ }
75
+ }
76
+ return lines.join("\n");
77
+ }
78
+ return String(data);
79
+ }
80
+ /**
81
+ * Main output function - outputs data in the specified format
82
+ *
83
+ * @param data - The data to output
84
+ * @param options - Output options (format, defaultFormat)
85
+ *
86
+ * @example
87
+ * // Output a devbox as text (default for single items)
88
+ * output(devbox, { format: options.output, defaultFormat: 'text' });
89
+ *
90
+ * @example
91
+ * // Output a list as JSON (default for lists)
92
+ * output(devboxes, { format: options.output, defaultFormat: 'json' });
93
+ */
94
+ export function output(data, options = {}) {
95
+ const format = resolveFormat(options);
15
96
  if (format === "json") {
16
97
  console.log(JSON.stringify(data, null, 2));
17
98
  return;
@@ -20,49 +101,78 @@ export function outputData(data, format = "json") {
20
101
  console.log(YAML.stringify(data));
21
102
  return;
22
103
  }
23
- if (format === "text") {
24
- // Simple text output
25
- if (Array.isArray(data)) {
26
- // For lists of complex objects, just output IDs
27
- data.forEach((item) => {
28
- if (typeof item === "object" && item !== null && "id" in item) {
29
- console.log(item.id);
30
- }
31
- else {
32
- console.log(formatTextOutput(item));
33
- }
34
- });
35
- }
36
- else {
37
- console.log(formatTextOutput(data));
38
- }
39
- return;
104
+ // Text format - key-value pairs
105
+ console.log(formatKeyValue(data));
106
+ }
107
+ /**
108
+ * Output an error message and exit
109
+ *
110
+ * @param message - Human-readable error message
111
+ * @param error - Optional Error object with details
112
+ *
113
+ * @example
114
+ * outputError('Failed to get devbox', error);
115
+ */
116
+ export function outputError(message, error) {
117
+ const errorMessage = error instanceof Error ? error.message : String(error || message);
118
+ console.error(`Error: ${message}`);
119
+ if (error && errorMessage !== message) {
120
+ console.error(` ${errorMessage}`);
40
121
  }
41
- console.error(`Unknown output format: ${format}`);
42
122
  process.exit(1);
43
123
  }
44
124
  /**
45
- * Format a single item as text output
125
+ * Output a success message for action commands
126
+ *
127
+ * @param message - Success message
128
+ * @param data - Optional data to include
129
+ * @param options - Output options
46
130
  */
47
- function formatTextOutput(item) {
48
- if (typeof item === "string") {
49
- return item;
50
- }
51
- // For objects, create a simple key: value format
52
- const lines = [];
53
- for (const [key, value] of Object.entries(item)) {
54
- if (value !== null && value !== undefined) {
55
- lines.push(`${key}: ${value}`);
56
- }
131
+ export function outputSuccess(message, data, options = {}) {
132
+ const format = resolveFormat(options);
133
+ if (format === "json") {
134
+ console.log(JSON.stringify({
135
+ success: true,
136
+ message,
137
+ ...(data && typeof data === "object" ? data : { data }),
138
+ }, null, 2));
139
+ return;
140
+ }
141
+ if (format === "yaml") {
142
+ console.log(YAML.stringify({
143
+ success: true,
144
+ message,
145
+ ...(data && typeof data === "object" ? data : { data }),
146
+ }));
147
+ return;
148
+ }
149
+ // Text format
150
+ console.log(`✓ ${message}`);
151
+ if (data) {
152
+ console.log(formatKeyValue(data));
57
153
  }
58
- return lines.join("\n");
59
154
  }
155
+ // ============================================================================
156
+ // Legacy API (for backward compatibility during migration)
157
+ // ============================================================================
60
158
  /**
61
- * Output a single result (for create, delete, etc)
159
+ * @deprecated Use output() instead
160
+ */
161
+ export function shouldUseNonInteractiveOutput(options) {
162
+ return !!options.output && options.output !== "interactive";
163
+ }
164
+ /**
165
+ * @deprecated Use output() instead
166
+ */
167
+ export function outputData(data, format = "json") {
168
+ output(data, { format, defaultFormat: format });
169
+ }
170
+ /**
171
+ * @deprecated Use output() instead
62
172
  */
63
173
  export function outputResult(result, options, successMessage) {
64
174
  if (shouldUseNonInteractiveOutput(options)) {
65
- outputData(result, options.output);
175
+ output(result, { format: options.output, defaultFormat: "text" });
66
176
  return;
67
177
  }
68
178
  // Interactive mode - print success message
@@ -71,34 +181,15 @@ export function outputResult(result, options, successMessage) {
71
181
  }
72
182
  }
73
183
  /**
74
- * Output a list of items (for list commands)
184
+ * @deprecated Use output() instead
75
185
  */
76
186
  export function outputList(items, options) {
77
187
  if (shouldUseNonInteractiveOutput(options)) {
78
- outputData(items, options.output);
79
- }
80
- }
81
- /**
82
- * Handle errors in both interactive and non-interactive modes
83
- */
84
- export function outputError(error, options) {
85
- if (shouldUseNonInteractiveOutput(options)) {
86
- if (options.output === "json") {
87
- console.error(JSON.stringify({ error: error.message }, null, 2));
88
- }
89
- else if (options.output === "yaml") {
90
- console.error(YAML.stringify({ error: error.message }));
91
- }
92
- else {
93
- console.error(`Error: ${error.message}`);
94
- }
95
- process.exit(1);
188
+ output(items, { format: options.output, defaultFormat: "json" });
96
189
  }
97
- // Let interactive UI handle the error
98
- throw error;
99
190
  }
100
191
  /**
101
- * Validate output format option
192
+ * @deprecated Use validateOutputFormat with the new output() function
102
193
  */
103
194
  export function validateOutputFormat(format) {
104
195
  if (!format || format === "text") {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Terminal screen buffer utilities.
3
+ *
4
+ * The alternate screen buffer provides a fullscreen experience similar to
5
+ * applications like vim, top, or htop. When enabled, the terminal saves
6
+ * the current screen content and switches to a clean buffer. Upon exit,
7
+ * the original screen content is restored.
8
+ */
9
+ /**
10
+ * Enter the alternate screen buffer.
11
+ * This provides a fullscreen experience where content won't mix with
12
+ * previous terminal output. Like vim or top.
13
+ */
14
+ export function enterAlternateScreenBuffer() {
15
+ process.stdout.write("\x1b[?1049h");
16
+ }
17
+ /**
18
+ * Exit the alternate screen buffer and restore the previous screen content.
19
+ * This returns the terminal to its original state before enterAlternateScreen() was called.
20
+ */
21
+ export function exitAlternateScreenBuffer() {
22
+ process.stdout.write("\x1b[?1049l");
23
+ }
package/dist/utils/ssh.js CHANGED
@@ -99,7 +99,9 @@ export function getSSHUrl() {
99
99
  */
100
100
  export function getProxyCommand() {
101
101
  const sshUrl = getSSHUrl();
102
- return `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshUrl} 2>/dev/null`;
102
+ // macOS openssl doesn't support -verify_quiet, use compatible flags
103
+ // servername should be %h (target hostname) - SSH will replace %h with the actual hostname from the SSH command
104
+ return `openssl s_client -quiet -servername %h -connect ${sshUrl} 2>/dev/null`;
103
105
  }
104
106
  /**
105
107
  * Execute SSH command
@@ -1,29 +1,5 @@
1
- import { spawnSync } from "child_process";
2
- export async function runSSHSession(config) {
3
- // Reset terminal to fix input visibility issues
4
- // This ensures the terminal is in a proper state after exiting Ink
5
- spawnSync("reset", [], { stdio: "inherit" });
6
- console.clear();
7
- console.log(`\nConnecting to devbox ${config.devboxName}...\n`);
8
- // Spawn SSH in foreground with proper terminal settings
9
- const result = spawnSync("ssh", [
10
- "-t", // Force pseudo-terminal allocation for proper input handling
11
- "-i",
12
- config.keyPath,
13
- "-o",
14
- `ProxyCommand=${config.proxyCommand}`,
15
- "-o",
16
- "StrictHostKeyChecking=no",
17
- "-o",
18
- "UserKnownHostsFile=/dev/null",
19
- `${config.sshUser}@${config.url}`,
20
- ], {
21
- stdio: "inherit",
22
- shell: false,
23
- });
24
- return {
25
- exitCode: result.status || 0,
26
- shouldRestart: true,
27
- returnToDevboxId: config.devboxId,
28
- };
29
- }
1
+ /**
2
+ * SSH Session types - kept for compatibility and type references
3
+ * Actual SSH session handling is now done via ink-spawn in SSHSessionScreen
4
+ */
5
+ export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Terminal background color detection utility
3
+ * Uses ANSI escape sequences to query the terminal's background color
4
+ */
5
+ import { stdin, stdout } from "process";
6
+ /**
7
+ * Calculate luminance from RGB values to determine if background is light or dark
8
+ * Using relative luminance formula: https://www.w3.org/TR/WCAG20/#relativeluminancedef
9
+ */
10
+ function getLuminance(r, g, b) {
11
+ const [rs, gs, bs] = [r, g, b].map((c) => {
12
+ const normalized = c / 255;
13
+ return normalized <= 0.03928
14
+ ? normalized / 12.92
15
+ : Math.pow((normalized + 0.055) / 1.055, 2.4);
16
+ });
17
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
18
+ }
19
+ /**
20
+ * Parse RGB color from terminal response
21
+ * Expected format: rgb:RRRR/GGGG/BBBB or similar variations
22
+ */
23
+ function parseRGBResponse(response) {
24
+ // Match patterns like: rgb:RRRR/GGGG/BBBB or rgba:RRRR/GGGG/BBBB/AAAA
25
+ const rgbMatch = response.match(/rgba?:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
26
+ if (!rgbMatch) {
27
+ return null;
28
+ }
29
+ // Parse hex values and normalize to 0-255 range
30
+ const r = parseInt(rgbMatch[1].substring(0, 2), 16);
31
+ const g = parseInt(rgbMatch[2].substring(0, 2), 16);
32
+ const b = parseInt(rgbMatch[3].substring(0, 2), 16);
33
+ return { r, g, b };
34
+ }
35
+ /**
36
+ * Detect terminal theme by querying background color
37
+ * Returns 'light' or 'dark' based on background luminance, or null if detection fails
38
+ *
39
+ * NOTE: This is disabled by default to prevent flashing. Theme detection writes
40
+ * escape sequences to stdout which can cause visible flashing on the terminal.
41
+ * Users can explicitly enable it with RUNLOOP_ENABLE_THEME_DETECTION=1
42
+ */
43
+ export async function detectTerminalTheme() {
44
+ // Skip detection in non-TTY environments
45
+ if (!stdin.isTTY || !stdout.isTTY) {
46
+ return null;
47
+ }
48
+ // Theme detection is now OPT-IN instead of OPT-OUT to prevent flashing
49
+ // Users need to explicitly enable it
50
+ if (process.env.RUNLOOP_ENABLE_THEME_DETECTION !== "1") {
51
+ return null;
52
+ }
53
+ return new Promise((resolve) => {
54
+ let response = "";
55
+ let timeout;
56
+ const cleanup = () => {
57
+ stdin.setRawMode(false);
58
+ stdin.pause();
59
+ stdin.removeListener("data", onData);
60
+ clearTimeout(timeout);
61
+ };
62
+ const onData = (chunk) => {
63
+ response += chunk.toString();
64
+ // Check if we have a complete response (ends with ESC \ or BEL)
65
+ if (response.includes("\x1b\\") || response.includes("\x07")) {
66
+ cleanup();
67
+ const rgb = parseRGBResponse(response);
68
+ if (rgb) {
69
+ const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
70
+ // Threshold: luminance > 0.5 is considered light background
71
+ resolve(luminance > 0.5 ? "light" : "dark");
72
+ }
73
+ else {
74
+ resolve(null);
75
+ }
76
+ }
77
+ };
78
+ // Set timeout for terminals that don't support the query
79
+ timeout = setTimeout(() => {
80
+ cleanup();
81
+ resolve(null);
82
+ }, 50); // 50ms timeout - quick to minimize any visual flashing
83
+ try {
84
+ // Enable raw mode to capture escape sequences
85
+ stdin.setRawMode(true);
86
+ stdin.resume();
87
+ stdin.on("data", onData);
88
+ // Query background color using OSC 11 sequence
89
+ // Format: ESC ] 11 ; ? ESC \
90
+ stdout.write("\x1b]11;?\x1b\\");
91
+ }
92
+ catch {
93
+ cleanup();
94
+ resolve(null);
95
+ }
96
+ });
97
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Terminal synchronous update mode utilities
3
+ *
4
+ * Uses ANSI escape sequences to prevent screen flicker by batching terminal updates.
5
+ * This tells the terminal to buffer all output between BEGIN and END markers
6
+ * and only display it atomically, preventing the visible flashing during redraws.
7
+ *
8
+ * Supported by most modern terminals (iTerm2, Terminal.app, Alacritty, etc.)
9
+ * When not supported, these sequences are simply ignored.
10
+ */
11
+ // Begin Synchronous Update (BSU) - tells terminal to start buffering
12
+ export const BEGIN_SYNC = "\x1b[?2026h";
13
+ // End Synchronous Update (ESU) - tells terminal to flush buffer atomically
14
+ export const END_SYNC = "\x1b[?2026l";
15
+ /**
16
+ * Enable synchronous updates for the terminal
17
+ * Call this once at application startup
18
+ */
19
+ export function enableSynchronousUpdates() {
20
+ return;
21
+ //process.stdout.write(BEGIN_SYNC);
22
+ }
23
+ /**
24
+ * Disable synchronous updates for the terminal
25
+ * Call this at application shutdown
26
+ */
27
+ export function disableSynchronousUpdates() {
28
+ return;
29
+ //process.stdout.write(END_SYNC);
30
+ }
31
+ /**
32
+ * Wrap terminal output with synchronous update markers
33
+ * This ensures the output is displayed atomically without flicker
34
+ */
35
+ export function withSynchronousUpdate(fn) {
36
+ //process.stdout.write(BEGIN_SYNC);
37
+ fn();
38
+ //process.stdout.write(END_SYNC);
39
+ }
@@ -2,21 +2,155 @@
2
2
  * Color theme constants for the CLI application
3
3
  * Centralized color definitions for easy theme customization
4
4
  */
5
- export const colors = {
5
+ import { detectTerminalTheme } from "./terminalDetection.js";
6
+ import { getThemePreference, getDetectedTheme, setDetectedTheme, } from "./config.js";
7
+ // Dark mode color palette (default)
8
+ const darkColors = {
6
9
  // Primary brand colors
7
- primary: "cyan",
8
- secondary: "magenta",
10
+ primary: "#00D9FF", // Bright cyan
11
+ secondary: "#FF6EC7", // Vibrant magenta
9
12
  // Status colors
10
- success: "green",
11
- warning: "yellow",
12
- error: "red",
13
- info: "blue",
13
+ success: "#10B981", // Emerald green
14
+ warning: "#F59E0B", // Amber
15
+ error: "#EF4444", // Red
16
+ info: "#3B82F6", // Blue
14
17
  // UI colors
15
- text: "white",
16
- textDim: "gray",
17
- border: "gray",
18
+ text: "#FFFFFF", // White
19
+ textDim: "#9CA3AF", // Gray
20
+ border: "#6B7280", // Medium gray
21
+ background: "#000000", // Black
18
22
  // Accent colors for menu items and highlights
19
- accent1: "cyan",
20
- accent2: "magenta",
21
- accent3: "green",
23
+ accent1: "#00D9FF", // Same as primary
24
+ accent2: "#FF6EC7", // Same as secondary
25
+ accent3: "#10B981", // Same as success
26
+ // ID color for displaying resource IDs
27
+ idColor: "#60A5FA", // Muted blue for IDs
22
28
  };
29
+ // Light mode color palette
30
+ const lightColors = {
31
+ // Primary brand colors (brighter/darker for visibility on light backgrounds)
32
+ primary: "#2563EB", // Deep blue
33
+ secondary: "#C026D3", // Deep magenta
34
+ // Status colors
35
+ success: "#059669", // Deep green
36
+ warning: "#D97706", // Deep amber
37
+ error: "#DC2626", // Deep red
38
+ info: "#2563EB", // Deep blue
39
+ // UI colors
40
+ text: "#000000", // Black
41
+ textDim: "#4B5563", // Dark gray for better contrast on light backgrounds
42
+ border: "#9CA3AF", // Medium gray
43
+ background: "#FFFFFF", // White
44
+ // Accent colors for menu items and highlights
45
+ accent1: "#2563EB", // Same as primary
46
+ accent2: "#C026D3", // Same as secondary
47
+ accent3: "#059669", // Same as success
48
+ // ID color for displaying resource IDs
49
+ idColor: "#0284C7", // Deeper blue for IDs on light backgrounds
50
+ };
51
+ // Current active color palette (initialized by initializeTheme)
52
+ let activeColors = darkColors;
53
+ let currentTheme = "dark";
54
+ /**
55
+ * Get the current color palette
56
+ * This is the main export that components should use
57
+ */
58
+ export const colors = new Proxy({}, {
59
+ get(_target, prop) {
60
+ return activeColors[prop];
61
+ },
62
+ });
63
+ /**
64
+ * Initialize the theme system
65
+ * Must be called at CLI startup before rendering any UI
66
+ */
67
+ export async function initializeTheme() {
68
+ const preference = getThemePreference();
69
+ let detectedTheme = null;
70
+ // Auto-detect if preference is 'auto'
71
+ if (preference === "auto") {
72
+ // Check cache first - only detect if we haven't cached a result
73
+ const cachedTheme = getDetectedTheme();
74
+ if (cachedTheme) {
75
+ // Use cached detection result (no flashing!)
76
+ detectedTheme = cachedTheme;
77
+ }
78
+ else {
79
+ // First time detection - run it and cache the result
80
+ try {
81
+ detectedTheme = await detectTerminalTheme();
82
+ if (detectedTheme) {
83
+ // Cache the result so we don't detect again
84
+ setDetectedTheme(detectedTheme);
85
+ }
86
+ }
87
+ catch {
88
+ // Detection failed, fall back to dark mode
89
+ detectedTheme = null;
90
+ }
91
+ }
92
+ }
93
+ // Determine final theme
94
+ if (preference === "light") {
95
+ currentTheme = "light";
96
+ activeColors = lightColors;
97
+ }
98
+ else if (preference === "dark") {
99
+ currentTheme = "dark";
100
+ activeColors = darkColors;
101
+ }
102
+ else if (detectedTheme) {
103
+ // Auto mode with successful detection
104
+ currentTheme = detectedTheme;
105
+ activeColors = detectedTheme === "light" ? lightColors : darkColors;
106
+ }
107
+ else {
108
+ // Auto mode with failed detection - default to dark
109
+ currentTheme = "dark";
110
+ activeColors = darkColors;
111
+ }
112
+ }
113
+ /**
114
+ * Get the current theme mode
115
+ */
116
+ export function getCurrentTheme() {
117
+ return currentTheme;
118
+ }
119
+ /**
120
+ * Get chalk function for a color name
121
+ * Useful for applying colors dynamically
122
+ */
123
+ export function getChalkColor(colorName) {
124
+ return activeColors[colorName];
125
+ }
126
+ /**
127
+ * Check if we should use inverted colors (light mode)
128
+ * Useful for components that need to explicitly set backgrounds
129
+ */
130
+ export function isLightMode() {
131
+ return currentTheme === "light";
132
+ }
133
+ /**
134
+ * Force set theme mode directly without detection
135
+ * Used for live preview in theme selector
136
+ */
137
+ export function setThemeMode(mode) {
138
+ currentTheme = mode;
139
+ activeColors = mode === "light" ? lightColors : darkColors;
140
+ }
141
+ /**
142
+ * Sanitize width values to prevent Yoga WASM crashes
143
+ * Ensures width is a valid, finite number within safe bounds
144
+ *
145
+ * @param width - The width value to sanitize
146
+ * @param min - Minimum allowed width (default: 1)
147
+ * @param max - Maximum allowed width (default: 100)
148
+ * @returns A safe width value guaranteed to be within [min, max]
149
+ */
150
+ export function sanitizeWidth(width, min = 1, max = 100) {
151
+ // Check for NaN, Infinity, or other invalid numbers
152
+ if (!Number.isFinite(width) || width < min) {
153
+ return min;
154
+ }
155
+ return Math.min(width, max);
156
+ }