@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.
- package/README.md +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- 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 =
|
|
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
|
-
|
|
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
|
-
|
|
132
|
+
processUtils.exit(1);
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
/**
|
package/dist/utils/sshSession.js
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/dist/utils/theme.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
8
|
-
secondary: "
|
|
11
|
+
primary: "#00D9FF", // Bright cyan
|
|
12
|
+
secondary: "#FF6EC7", // Vibrant magenta
|
|
9
13
|
// Status colors
|
|
10
|
-
success: "
|
|
11
|
-
warning: "
|
|
12
|
-
error: "
|
|
13
|
-
info: "
|
|
14
|
+
success: "#10B981", // Emerald green
|
|
15
|
+
warning: "#F59E0B", // Amber
|
|
16
|
+
error: "#EF4444", // Red
|
|
17
|
+
info: "#3B82F6", // Blue
|
|
14
18
|
// UI colors
|
|
15
|
-
text: "
|
|
16
|
-
textDim: "
|
|
17
|
-
border: "
|
|
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: "
|
|
20
|
-
accent2: "
|
|
21
|
-
accent3: "
|
|
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
|
+
}
|