@runloop/rl-cli 0.1.2 → 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.
- package/README.md +54 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -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 +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- 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 +63 -39
- package/dist/components/Breadcrumb.js +10 -48
- package/dist/components/DevboxActionsMenu.js +182 -110
- 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 +94 -0
- package/dist/components/MainMenu.js +36 -32
- 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/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -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 +105 -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/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
package/dist/utils/output.js
CHANGED
|
@@ -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
|
-
*
|
|
10
|
+
* Resolve the output format from options
|
|
7
11
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
*
|
|
21
|
+
* Format a value for text output (key-value pairs)
|
|
13
22
|
*/
|
|
14
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
184
|
+
* @deprecated Use output() instead
|
|
75
185
|
*/
|
|
76
186
|
export function outputList(items, options) {
|
|
77
187
|
if (shouldUseNonInteractiveOutput(options)) {
|
|
78
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
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,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
|
+
}
|
package/dist/utils/theme.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
8
|
-
secondary: "
|
|
10
|
+
primary: "#00D9FF", // Bright cyan
|
|
11
|
+
secondary: "#FF6EC7", // Vibrant magenta
|
|
9
12
|
// Status colors
|
|
10
|
-
success: "
|
|
11
|
-
warning: "
|
|
12
|
-
error: "
|
|
13
|
-
info: "
|
|
13
|
+
success: "#10B981", // Emerald green
|
|
14
|
+
warning: "#F59E0B", // Amber
|
|
15
|
+
error: "#EF4444", // Red
|
|
16
|
+
info: "#3B82F6", // Blue
|
|
14
17
|
// UI colors
|
|
15
|
-
text: "
|
|
16
|
-
textDim: "
|
|
17
|
-
border: "
|
|
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: "
|
|
20
|
-
accent2: "
|
|
21
|
-
accent3: "
|
|
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
|
+
}
|