@runloop/rl-cli 0.1.2 → 0.3.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 (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  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 +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Process utilities wrapper for testability.
3
+ *
4
+ * This module provides a mockable interface for process-related operations
5
+ * like exit, stdout/stderr writes, and terminal detection. In tests, you can
6
+ * replace these functions with mocks to avoid actual process termination
7
+ * and capture output.
8
+ *
9
+ * Usage in code:
10
+ * import { processUtils } from '../utils/processUtils.js';
11
+ * processUtils.exit(1);
12
+ * processUtils.stdout.write('Hello');
13
+ *
14
+ * Usage in tests:
15
+ * import { processUtils, resetProcessUtils } from '../utils/processUtils.js';
16
+ * processUtils.exit = jest.fn();
17
+ * // ... run tests ...
18
+ * resetProcessUtils(); // restore original behavior
19
+ */
20
+ // Store original references for reset
21
+ const originalExit = process.exit.bind(process);
22
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
23
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
24
+ const originalCwd = process.cwd.bind(process);
25
+ const originalOn = process.on.bind(process);
26
+ const originalOff = process.off.bind(process);
27
+ /**
28
+ * The main process utilities object.
29
+ * All properties are mutable for testing purposes.
30
+ */
31
+ export const processUtils = {
32
+ exit: originalExit,
33
+ stdout: {
34
+ write: (data) => originalStdoutWrite(data),
35
+ get isTTY() {
36
+ return process.stdout.isTTY ?? false;
37
+ },
38
+ },
39
+ stderr: {
40
+ write: (data) => originalStderrWrite(data),
41
+ get isTTY() {
42
+ return process.stderr.isTTY ?? false;
43
+ },
44
+ },
45
+ stdin: {
46
+ get isTTY() {
47
+ return process.stdin.isTTY ?? false;
48
+ },
49
+ setRawMode: process.stdin.setRawMode?.bind(process.stdin),
50
+ on: process.stdin.on.bind(process.stdin),
51
+ removeListener: process.stdin.removeListener.bind(process.stdin),
52
+ },
53
+ cwd: originalCwd,
54
+ on: originalOn,
55
+ off: originalOff,
56
+ get env() {
57
+ return process.env;
58
+ },
59
+ };
60
+ /**
61
+ * Reset all process utilities to their original implementations.
62
+ * Call this in test teardown to restore normal behavior.
63
+ */
64
+ export function resetProcessUtils() {
65
+ processUtils.exit = originalExit;
66
+ processUtils.stdout.write = (data) => originalStdoutWrite(data);
67
+ processUtils.stderr.write = (data) => originalStderrWrite(data);
68
+ processUtils.cwd = originalCwd;
69
+ processUtils.on = originalOn;
70
+ processUtils.off = originalOff;
71
+ }
72
+ /**
73
+ * Create a mock process utils for testing.
74
+ * Returns an object with jest mock functions.
75
+ */
76
+ export function createMockProcessUtils() {
77
+ const exitMock = (() => {
78
+ throw new Error("process.exit called");
79
+ });
80
+ return {
81
+ exit: exitMock,
82
+ stdout: {
83
+ write: () => true,
84
+ isTTY: false,
85
+ },
86
+ stderr: {
87
+ write: () => true,
88
+ isTTY: false,
89
+ },
90
+ stdin: {
91
+ isTTY: false,
92
+ setRawMode: () => { },
93
+ on: () => { },
94
+ removeListener: () => { },
95
+ },
96
+ cwd: () => "/mock/cwd",
97
+ on: () => { },
98
+ off: () => { },
99
+ env: {},
100
+ };
101
+ }
102
+ /**
103
+ * Install mock process utils for testing.
104
+ * Returns a cleanup function to restore originals.
105
+ */
106
+ export function installMockProcessUtils(mock) {
107
+ const backup = {
108
+ exit: processUtils.exit,
109
+ stdoutWrite: processUtils.stdout.write,
110
+ stderrWrite: processUtils.stderr.write,
111
+ cwd: processUtils.cwd,
112
+ on: processUtils.on,
113
+ off: processUtils.off,
114
+ };
115
+ if (mock.exit)
116
+ processUtils.exit = mock.exit;
117
+ if (mock.stdout?.write)
118
+ processUtils.stdout.write = mock.stdout.write;
119
+ if (mock.stderr?.write)
120
+ processUtils.stderr.write = mock.stderr.write;
121
+ if (mock.cwd)
122
+ processUtils.cwd = mock.cwd;
123
+ if (mock.on)
124
+ processUtils.on = mock.on;
125
+ if (mock.off)
126
+ processUtils.off = mock.off;
127
+ return () => {
128
+ processUtils.exit = backup.exit;
129
+ processUtils.stdout.write = backup.stdoutWrite;
130
+ processUtils.stderr.write = backup.stderrWrite;
131
+ processUtils.cwd = backup.cwd;
132
+ processUtils.on = backup.on;
133
+ processUtils.off = backup.off;
134
+ };
135
+ }
@@ -0,0 +1,61 @@
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
+ import { processUtils } from "./processUtils.js";
10
+ /**
11
+ * Enter the alternate screen buffer.
12
+ * This provides a fullscreen experience where content won't mix with
13
+ * previous terminal output. Like vim or top.
14
+ */
15
+ export function enterAlternateScreenBuffer() {
16
+ processUtils.stdout.write("\x1b[?1049h");
17
+ }
18
+ /**
19
+ * Exit the alternate screen buffer and restore the previous screen content.
20
+ * This returns the terminal to its original state before enterAlternateScreen() was called.
21
+ */
22
+ export function exitAlternateScreenBuffer() {
23
+ processUtils.stdout.write("\x1b[?1049l");
24
+ }
25
+ /**
26
+ * Clear the terminal screen.
27
+ * Uses ANSI escape sequences to clear the screen and move cursor to top-left.
28
+ */
29
+ export function clearScreen() {
30
+ // Clear entire screen and move cursor to top-left
31
+ processUtils.stdout.write("\x1b[2J\x1b[H");
32
+ }
33
+ /**
34
+ * Show the terminal cursor.
35
+ * Uses ANSI escape sequence to make the cursor visible.
36
+ */
37
+ export function showCursor() {
38
+ processUtils.stdout.write("\x1b[?25h");
39
+ }
40
+ /**
41
+ * Hide the terminal cursor.
42
+ * Uses ANSI escape sequence to make the cursor invisible.
43
+ */
44
+ export function hideCursor() {
45
+ processUtils.stdout.write("\x1b[?25l");
46
+ }
47
+ /**
48
+ * Reset terminal to a clean state.
49
+ * Exits alternate screen buffer, clears the screen, and resets cursor.
50
+ * Also resets terminal attributes to ensure clean state for subprocesses.
51
+ */
52
+ export function resetTerminal() {
53
+ exitAlternateScreenBuffer();
54
+ clearScreen();
55
+ // Reset terminal attributes (SGR reset)
56
+ processUtils.stdout.write("\x1b[0m");
57
+ // Move cursor to home position
58
+ processUtils.stdout.write("\x1b[H");
59
+ // Show cursor to ensure it's visible
60
+ showCursor();
61
+ }
package/dist/utils/ssh.js CHANGED
@@ -4,6 +4,7 @@ import { writeFile, mkdir, chmod } from "fs/promises";
4
4
  import { join } from "path";
5
5
  import { homedir } from "os";
6
6
  import { getClient } from "./client.js";
7
+ import { processUtils } from "./processUtils.js";
7
8
  const execAsync = promisify(exec);
8
9
  export function constructSSHConfig(options) {
9
10
  return `Host ${options.hostname}
@@ -91,7 +92,7 @@ export async function waitForReady(devboxId, timeoutSeconds = 180, pollIntervalS
91
92
  * Get SSH URL based on environment
92
93
  */
93
94
  export function getSSHUrl() {
94
- const env = process.env.RUNLOOP_ENV?.toLowerCase();
95
+ const env = processUtils.env.RUNLOOP_ENV?.toLowerCase();
95
96
  return env === "dev" ? "ssh.runloop.pro:443" : "ssh.runloop.ai:443";
96
97
  }
97
98
  /**
@@ -99,7 +100,9 @@ export function getSSHUrl() {
99
100
  */
100
101
  export function getProxyCommand() {
101
102
  const sshUrl = getSSHUrl();
102
- return `openssl s_client -quiet -verify_quiet -servername %h -connect ${sshUrl} 2>/dev/null`;
103
+ // macOS openssl doesn't support -verify_quiet, use compatible flags
104
+ // servername should be %h (target hostname) - SSH will replace %h with the actual hostname from the SSH command
105
+ return `openssl s_client -quiet -servername %h -connect ${sshUrl} 2>/dev/null`;
103
106
  }
104
107
  /**
105
108
  * Execute SSH command
@@ -126,7 +129,7 @@ export async function executeSSH(devboxId, user, keyfilePath, url, additionalArg
126
129
  }
127
130
  catch (error) {
128
131
  console.error("SSH command failed:", error);
129
- process.exit(1);
132
+ processUtils.exit(1);
130
133
  }
131
134
  }
132
135
  /**
@@ -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,185 @@
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
+ * Terminal responses can come in various formats:
22
+ * - OSC 11;rgb:RRRR/GGGG/BBBB (xterm-style, 4 hex digits per channel)
23
+ * - OSC 11;rgb:RR/GG/BB (short format, 2 hex digits per channel)
24
+ * - rgb:RRRR/GGGG/BBBB (without OSC prefix)
25
+ */
26
+ function parseRGBResponse(response) {
27
+ // Try multiple patterns to handle different terminal response formats
28
+ // Pattern 1: OSC 11;rgb:RRRR/GGGG/BBBB or 11;rgb:RRRR/GGGG/BBBB (xterm-style, 4 hex digits)
29
+ let match = response.match(/11;rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})/i);
30
+ if (match) {
31
+ // Take first 2 hex digits and convert to 0-255
32
+ const r = parseInt(match[1].substring(0, 2), 16);
33
+ const g = parseInt(match[2].substring(0, 2), 16);
34
+ const b = parseInt(match[3].substring(0, 2), 16);
35
+ return { r, g, b };
36
+ }
37
+ // Pattern 2: OSC 11;rgb:RR/GG/BB (short format, 2 hex digits)
38
+ match = response.match(/11;rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/i);
39
+ if (match) {
40
+ const r = parseInt(match[1], 16);
41
+ const g = parseInt(match[2], 16);
42
+ const b = parseInt(match[3], 16);
43
+ return { r, g, b };
44
+ }
45
+ // Pattern 3: rgb:RRRR/GGGG/BBBB (without OSC prefix, 4 hex digits)
46
+ match = response.match(/rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})/i);
47
+ if (match) {
48
+ const r = parseInt(match[1].substring(0, 2), 16);
49
+ const g = parseInt(match[2].substring(0, 2), 16);
50
+ const b = parseInt(match[3].substring(0, 2), 16);
51
+ return { r, g, b };
52
+ }
53
+ // Pattern 4: rgb:RR/GG/BB (without OSC prefix, 2 hex digits)
54
+ match = response.match(/rgb:([0-9a-f]{2})\/([0-9a-f]{2})\/([0-9a-f]{2})/i);
55
+ if (match) {
56
+ const r = parseInt(match[1], 16);
57
+ const g = parseInt(match[2], 16);
58
+ const b = parseInt(match[3], 16);
59
+ return { r, g, b };
60
+ }
61
+ // Pattern 5: Generic pattern for any hex values separated by /
62
+ match = response.match(/([0-9a-f]{2,4})\/([0-9a-f]{2,4})\/([0-9a-f]{2,4})/i);
63
+ if (match) {
64
+ const r = parseInt(match[1].substring(0, 2), 16);
65
+ const g = parseInt(match[2].substring(0, 2), 16);
66
+ const b = parseInt(match[3].substring(0, 2), 16);
67
+ return { r, g, b };
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Detect terminal theme by querying background color
73
+ * Returns 'light' or 'dark' based on background luminance, or null if detection fails
74
+ *
75
+ * NOTE: Theme detection runs automatically when theme preference is "auto".
76
+ * Users can disable it by setting RUNLOOP_DISABLE_THEME_DETECTION=1 to prevent
77
+ * any potential terminal flashing.
78
+ */
79
+ export async function detectTerminalTheme() {
80
+ // Skip detection in non-TTY environments
81
+ if (!stdin.isTTY || !stdout.isTTY) {
82
+ return null;
83
+ }
84
+ // Allow users to opt-out of theme detection if they experience flashing
85
+ if (process.env.RUNLOOP_DISABLE_THEME_DETECTION === "1") {
86
+ return null;
87
+ }
88
+ return new Promise((resolve) => {
89
+ let response = "";
90
+ let timeout;
91
+ let hasResolved = false;
92
+ const cleanup = () => {
93
+ try {
94
+ stdin.setRawMode(false);
95
+ stdin.pause();
96
+ stdin.removeListener("data", onData);
97
+ stdin.removeListener("readable", onReadable);
98
+ }
99
+ catch {
100
+ // Ignore errors during cleanup
101
+ }
102
+ if (timeout) {
103
+ clearTimeout(timeout);
104
+ }
105
+ };
106
+ const finish = (result) => {
107
+ if (hasResolved)
108
+ return;
109
+ hasResolved = true;
110
+ cleanup();
111
+ resolve(result);
112
+ };
113
+ const onData = (chunk) => {
114
+ if (hasResolved)
115
+ return;
116
+ const text = chunk.toString("utf8");
117
+ response += text;
118
+ // Check if we have a complete response
119
+ // Terminal responses typically end with ESC \ (ST) or BEL (\x07)
120
+ // Some terminals may also send the response without the OSC prefix
121
+ if (response.includes("\x1b\\") ||
122
+ response.includes("\x07") ||
123
+ response.includes("\x1b]")) {
124
+ const rgb = parseRGBResponse(response);
125
+ if (rgb) {
126
+ const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
127
+ // Threshold: luminance > 0.5 is considered light background
128
+ finish(luminance > 0.5 ? "light" : "dark");
129
+ return;
130
+ }
131
+ // If we got a response but couldn't parse it, check if it's complete
132
+ if (response.includes("\x1b\\") || response.includes("\x07")) {
133
+ finish(null);
134
+ return;
135
+ }
136
+ }
137
+ };
138
+ // Some terminals may send responses through the readable event instead
139
+ const onReadable = () => {
140
+ if (hasResolved)
141
+ return;
142
+ let chunk;
143
+ while ((chunk = stdin.read()) !== null) {
144
+ const text = chunk.toString("utf8");
145
+ response += text;
146
+ if (text.includes("\x1b\\") || text.includes("\x07")) {
147
+ const rgb = parseRGBResponse(response);
148
+ if (rgb) {
149
+ const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
150
+ finish(luminance > 0.5 ? "light" : "dark");
151
+ return;
152
+ }
153
+ finish(null);
154
+ return;
155
+ }
156
+ }
157
+ };
158
+ // Set timeout for terminals that don't support the query
159
+ timeout = setTimeout(() => {
160
+ finish(null);
161
+ }, 200); // Increased timeout to 200ms to give terminals more time to respond
162
+ try {
163
+ // Enable raw mode to capture escape sequences
164
+ stdin.setRawMode(true);
165
+ stdin.resume();
166
+ // Listen to both data and readable events
167
+ stdin.on("data", onData);
168
+ stdin.on("readable", onReadable);
169
+ // Query background color using OSC 11 sequence
170
+ // Format: ESC ] 11 ; ? ESC \
171
+ // Some terminals may need the BEL terminator instead
172
+ stdout.write("\x1b]11;?\x1b\\");
173
+ // Also try with BEL terminator as some terminals prefer it
174
+ // (but wait a bit to see if first one works)
175
+ setTimeout(() => {
176
+ if (!hasResolved) {
177
+ stdout.write("\x1b]11;?\x07");
178
+ }
179
+ }, 10);
180
+ }
181
+ catch {
182
+ finish(null);
183
+ }
184
+ });
185
+ }
@@ -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,170 @@
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
+ import chalk from "chalk";
8
+ // Dark mode color palette (default)
9
+ const darkColors = {
6
10
  // Primary brand colors
7
- primary: "cyan",
8
- secondary: "magenta",
11
+ primary: "#00D9FF", // Bright cyan
12
+ secondary: "#FF6EC7", // Vibrant magenta
9
13
  // Status colors
10
- success: "green",
11
- warning: "yellow",
12
- error: "red",
13
- info: "blue",
14
+ success: "#10B981", // Emerald green
15
+ warning: "#F59E0B", // Amber
16
+ error: "#EF4444", // Red
17
+ info: "#3B82F6", // Blue
14
18
  // UI colors
15
- text: "white",
16
- textDim: "gray",
17
- border: "gray",
19
+ text: "#FFFFFF", // White
20
+ textDim: "#9CA3AF", // Gray
21
+ border: "#6B7280", // Medium gray
22
+ background: "#000000", // Black
18
23
  // Accent colors for menu items and highlights
19
- accent1: "cyan",
20
- accent2: "magenta",
21
- accent3: "green",
24
+ accent1: "#00D9FF", // Same as primary
25
+ accent2: "#FF6EC7", // Same as secondary
26
+ accent3: "#10B981", // Same as success
27
+ // ID color for displaying resource IDs
28
+ idColor: "#60A5FA", // Muted blue for IDs
22
29
  };
30
+ // Light mode color palette
31
+ const lightColors = {
32
+ // Primary brand colors (brighter/darker for visibility on light backgrounds)
33
+ primary: "#2563EB", // Deep blue
34
+ secondary: "#C026D3", // Deep magenta
35
+ // Status colors
36
+ success: "#059669", // Deep green
37
+ warning: "#D97706", // Deep amber
38
+ error: "#DC2626", // Deep red
39
+ info: "#2563EB", // Deep blue
40
+ // UI colors
41
+ text: "#000000", // Black
42
+ textDim: "#4B5563", // Dark gray for better contrast on light backgrounds
43
+ border: "#9CA3AF", // Medium gray
44
+ background: "#FFFFFF", // White
45
+ // Accent colors for menu items and highlights
46
+ accent1: "#2563EB", // Same as primary
47
+ accent2: "#C026D3", // Same as secondary
48
+ accent3: "#059669", // Same as success
49
+ // ID color for displaying resource IDs
50
+ idColor: "#0284C7", // Deeper blue for IDs on light backgrounds
51
+ };
52
+ // Current active color palette (initialized by initializeTheme)
53
+ let activeColors = darkColors;
54
+ let currentTheme = "dark";
55
+ /**
56
+ * Get the current color palette
57
+ * This is the main export that components should use
58
+ */
59
+ export const colors = new Proxy({}, {
60
+ get(_target, prop) {
61
+ return activeColors[prop];
62
+ },
63
+ });
64
+ /**
65
+ * Initialize the theme system
66
+ * Must be called at CLI startup before rendering any UI
67
+ */
68
+ export async function initializeTheme() {
69
+ const preference = getThemePreference();
70
+ let detectedTheme = null;
71
+ // Auto-detect if preference is 'auto'
72
+ // Always detect on startup to support different terminal profiles
73
+ // (users may have different terminal profiles with different themes)
74
+ if (preference === "auto") {
75
+ try {
76
+ detectedTheme = await detectTerminalTheme();
77
+ // Cache the result for reference, but we always re-detect on startup
78
+ if (detectedTheme) {
79
+ setDetectedTheme(detectedTheme);
80
+ }
81
+ }
82
+ catch {
83
+ // Detection failed, fall back to cached value or dark mode
84
+ const cachedTheme = getDetectedTheme();
85
+ detectedTheme = cachedTheme || null;
86
+ }
87
+ }
88
+ // Determine final theme
89
+ if (preference === "light") {
90
+ currentTheme = "light";
91
+ activeColors = lightColors;
92
+ }
93
+ else if (preference === "dark") {
94
+ currentTheme = "dark";
95
+ activeColors = darkColors;
96
+ }
97
+ else if (detectedTheme) {
98
+ // Auto mode with successful detection
99
+ currentTheme = detectedTheme;
100
+ activeColors = detectedTheme === "light" ? lightColors : darkColors;
101
+ }
102
+ else {
103
+ // Auto mode with failed detection - default to dark
104
+ currentTheme = "dark";
105
+ activeColors = darkColors;
106
+ }
107
+ }
108
+ /**
109
+ * Get the current theme mode
110
+ */
111
+ export function getCurrentTheme() {
112
+ return currentTheme;
113
+ }
114
+ /**
115
+ * Get hex color value for a color name
116
+ * Useful for applying colors dynamically
117
+ */
118
+ export function getChalkColor(colorName) {
119
+ return activeColors[colorName];
120
+ }
121
+ /**
122
+ * Get chalk text color function for a color name
123
+ * Converts hex color to chalk function for text coloring
124
+ * @param colorName - Name of the color from the palette
125
+ * @returns Chalk function that can be used to color text
126
+ */
127
+ export function getChalkTextColor(colorName) {
128
+ const hexColor = activeColors[colorName];
129
+ return chalk.hex(hexColor);
130
+ }
131
+ /**
132
+ * Get chalk background color function for a color name
133
+ * Converts hex color to chalk function for background coloring
134
+ * @param colorName - Name of the color from the palette
135
+ * @returns Chalk function that can be used to color backgrounds
136
+ */
137
+ export function getChalkBgColor(colorName) {
138
+ const hexColor = activeColors[colorName];
139
+ return chalk.bgHex(hexColor);
140
+ }
141
+ /**
142
+ * Check if we should use inverted colors (light mode)
143
+ * Useful for components that need to explicitly set backgrounds
144
+ */
145
+ export function isLightMode() {
146
+ return currentTheme === "light";
147
+ }
148
+ /**
149
+ * Force set theme mode directly without detection
150
+ * Used for live preview in theme selector
151
+ */
152
+ export function setThemeMode(mode) {
153
+ currentTheme = mode;
154
+ activeColors = mode === "light" ? lightColors : darkColors;
155
+ }
156
+ /**
157
+ * Sanitize width values to prevent Yoga WASM crashes
158
+ * Ensures width is a valid, finite number within safe bounds
159
+ *
160
+ * @param width - The width value to sanitize
161
+ * @param min - Minimum allowed width (default: 1)
162
+ * @param max - Maximum allowed width (default: 100)
163
+ * @returns A safe width value guaranteed to be within [min, max]
164
+ */
165
+ export function sanitizeWidth(width, min = 1, max = 100) {
166
+ // Check for NaN, Infinity, or other invalid numbers
167
+ if (!Number.isFinite(width) || width < min) {
168
+ return min;
169
+ }
170
+ return Math.min(width, max);
171
+ }