@runloop/rl-cli 0.2.0 → 0.4.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 +5 -75
- package/dist/cli.js +24 -56
- package/dist/commands/auth.js +2 -1
- package/dist/commands/blueprint/list.js +68 -22
- package/dist/commands/blueprint/preview.js +38 -42
- package/dist/commands/config.js +3 -2
- package/dist/commands/devbox/ssh.js +2 -1
- package/dist/commands/devbox/tunnel.js +2 -1
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +9 -8
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +2 -1
- package/dist/components/ActionsPopup.js +18 -17
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +10 -9
- package/dist/components/DevboxActionsMenu.js +18 -180
- package/dist/components/InteractiveSpawn.js +24 -14
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +2 -2
- package/dist/components/ResourceListView.js +3 -3
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +3 -3
- package/dist/hooks/useExitOnCtrlC.js +2 -1
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +7 -2
- package/dist/router/Router.js +3 -1
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/services/blueprintService.js +18 -22
- package/dist/utils/CommandExecutor.js +24 -53
- package/dist/utils/client.js +5 -1
- package/dist/utils/config.js +2 -1
- package/dist/utils/logFormatter.js +47 -1
- package/dist/utils/output.js +4 -3
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +40 -2
- package/dist/utils/ssh.js +3 -2
- package/dist/utils/terminalDetection.js +120 -32
- package/dist/utils/theme.js +34 -19
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +4 -6
package/dist/utils/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Runloop from "@runloop/api-client";
|
|
2
|
+
import { VERSION } from "@runloop/api-client/version.js";
|
|
2
3
|
import { getConfig } from "./config.js";
|
|
3
4
|
/**
|
|
4
5
|
* Get the base URL based on RUNLOOP_ENV environment variable
|
|
@@ -18,11 +19,14 @@ function getBaseUrl() {
|
|
|
18
19
|
export function getClient() {
|
|
19
20
|
const config = getConfig();
|
|
20
21
|
if (!config.apiKey) {
|
|
21
|
-
throw new Error("API key not configured.
|
|
22
|
+
throw new Error("API key not configured. Set RUNLOOP_API_KEY environment variable.");
|
|
22
23
|
}
|
|
23
24
|
const baseURL = getBaseUrl();
|
|
24
25
|
return new Runloop({
|
|
25
26
|
bearerToken: config.apiKey,
|
|
26
27
|
baseURL,
|
|
28
|
+
defaultHeaders: {
|
|
29
|
+
"User-Agent": `Runloop/JS ${VERSION} - CLI`,
|
|
30
|
+
},
|
|
27
31
|
});
|
|
28
32
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import { existsSync, statSync, mkdirSync, writeFileSync } from "fs";
|
|
5
5
|
const config = new Conf({
|
|
6
6
|
projectName: "runloop-cli",
|
|
7
|
+
cwd: join(homedir(), ".runloop"),
|
|
7
8
|
});
|
|
8
9
|
export function getConfig() {
|
|
9
10
|
// Check environment variable first, then fall back to stored config
|
|
@@ -29,7 +30,7 @@ export function sshUrl() {
|
|
|
29
30
|
: "ssh.runloop.ai:443";
|
|
30
31
|
}
|
|
31
32
|
export function getCacheDir() {
|
|
32
|
-
return join(homedir(), ".
|
|
33
|
+
return join(homedir(), ".runloop", "rl-cli");
|
|
33
34
|
}
|
|
34
35
|
export function shouldCheckForUpdates() {
|
|
35
36
|
const cacheDir = getCacheDir();
|
|
@@ -83,7 +83,7 @@ export function getSourceInfo(source) {
|
|
|
83
83
|
return { abbrev, color: "white" };
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
|
-
* Parse a log entry into formatted parts (for use in Ink UI)
|
|
86
|
+
* Parse a devbox log entry into formatted parts (for use in Ink UI)
|
|
87
87
|
*/
|
|
88
88
|
export function parseLogEntry(log) {
|
|
89
89
|
const levelInfo = getLogLevelInfo(log.level);
|
|
@@ -101,6 +101,52 @@ export function parseLogEntry(log) {
|
|
|
101
101
|
exitCodeColor: log.exit_code === 0 ? "green" : "red",
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Parse a blueprint log entry into formatted parts (for use in Ink UI)
|
|
106
|
+
* Blueprint logs have a simpler structure (no source, shell_name, cmd, exit_code)
|
|
107
|
+
*/
|
|
108
|
+
export function parseBlueprintLogEntry(log) {
|
|
109
|
+
const levelInfo = getLogLevelInfo(log.level);
|
|
110
|
+
// Blueprint logs don't have a source, use "build" as default
|
|
111
|
+
const sourceInfo = getSourceInfo("build");
|
|
112
|
+
// Handle timestamp - may be timestamp_ms or timestamp
|
|
113
|
+
let timestampMs;
|
|
114
|
+
if (log.timestamp_ms !== undefined) {
|
|
115
|
+
timestampMs = log.timestamp_ms;
|
|
116
|
+
}
|
|
117
|
+
else if (log.timestamp !== undefined) {
|
|
118
|
+
const ts = log.timestamp;
|
|
119
|
+
timestampMs = typeof ts === "number" ? ts : new Date(ts).getTime();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Fallback to current time if no timestamp
|
|
123
|
+
timestampMs = Date.now();
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
timestamp: formatTimestamp(timestampMs),
|
|
127
|
+
level: levelInfo.name,
|
|
128
|
+
levelColor: levelInfo.color,
|
|
129
|
+
source: sourceInfo.abbrev,
|
|
130
|
+
sourceColor: sourceInfo.color,
|
|
131
|
+
shellName: null,
|
|
132
|
+
cmd: null,
|
|
133
|
+
message: log.message || "",
|
|
134
|
+
exitCode: null,
|
|
135
|
+
exitCodeColor: "gray",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Parse any log entry (devbox or blueprint) into formatted parts
|
|
140
|
+
*/
|
|
141
|
+
export function parseAnyLogEntry(log) {
|
|
142
|
+
// Check if it's a devbox log by looking for source field
|
|
143
|
+
if ("source" in log || "shell_name" in log || "cmd" in log) {
|
|
144
|
+
return parseLogEntry(log);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
return parseBlueprintLogEntry(log);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
104
150
|
/**
|
|
105
151
|
* Format a log entry as a string with chalk colors (for CLI output)
|
|
106
152
|
*/
|
package/dist/utils/output.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - outputError(message, error) - outputs error and exits
|
|
7
7
|
*/
|
|
8
8
|
import YAML from "yaml";
|
|
9
|
+
import { processUtils } from "./processUtils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Resolve the output format from options
|
|
11
12
|
*/
|
|
@@ -15,7 +16,7 @@ function resolveFormat(options) {
|
|
|
15
16
|
return format;
|
|
16
17
|
}
|
|
17
18
|
console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
|
|
18
|
-
|
|
19
|
+
processUtils.exit(1);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Format a value for text output (key-value pairs)
|
|
@@ -119,7 +120,7 @@ export function outputError(message, error) {
|
|
|
119
120
|
if (error && errorMessage !== message) {
|
|
120
121
|
console.error(` ${errorMessage}`);
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
processUtils.exit(1);
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
125
126
|
* Output a success message for action commands
|
|
@@ -202,5 +203,5 @@ export function validateOutputFormat(format) {
|
|
|
202
203
|
return "yaml";
|
|
203
204
|
}
|
|
204
205
|
console.error(`Unknown output format: ${format}. Valid options: text, json, yaml`);
|
|
205
|
-
|
|
206
|
+
processUtils.exit(1);
|
|
206
207
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process utilities wrapper for testability.
|
|
3
|
+
*
|
|
4
|
+
* This module provides wrappers around Node.js process functions
|
|
5
|
+
* that can be easily mocked in tests.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default implementation using real process
|
|
9
|
+
*/
|
|
10
|
+
const defaultProcess = {
|
|
11
|
+
exit: (code) => {
|
|
12
|
+
process.exit(code);
|
|
13
|
+
},
|
|
14
|
+
stdout: {
|
|
15
|
+
write: (data) => process.stdout.write(data),
|
|
16
|
+
get isTTY() {
|
|
17
|
+
return process.stdout.isTTY;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
stderr: {
|
|
21
|
+
write: (data) => process.stderr.write(data),
|
|
22
|
+
},
|
|
23
|
+
get env() {
|
|
24
|
+
return process.env;
|
|
25
|
+
},
|
|
26
|
+
cwd: () => process.cwd(),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Current process implementation - can be swapped for testing
|
|
30
|
+
*/
|
|
31
|
+
let currentProcess = defaultProcess;
|
|
32
|
+
/**
|
|
33
|
+
* Get the current process wrapper
|
|
34
|
+
*/
|
|
35
|
+
export function getProcess() {
|
|
36
|
+
return currentProcess;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set a custom process wrapper (for testing)
|
|
40
|
+
*/
|
|
41
|
+
export function setProcess(proc) {
|
|
42
|
+
currentProcess = proc;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reset to the default process wrapper
|
|
46
|
+
*/
|
|
47
|
+
export function resetProcess() {
|
|
48
|
+
currentProcess = defaultProcess;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Exit the process with an optional exit code
|
|
52
|
+
*/
|
|
53
|
+
export function exitProcess(code) {
|
|
54
|
+
return currentProcess.exit(code);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Write to stdout
|
|
58
|
+
*/
|
|
59
|
+
export function writeStdout(data) {
|
|
60
|
+
return currentProcess.stdout.write(data);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Write to stderr
|
|
64
|
+
*/
|
|
65
|
+
export function writeStderr(data) {
|
|
66
|
+
return currentProcess.stderr.write(data);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if stdout is a TTY
|
|
70
|
+
*/
|
|
71
|
+
export function isStdoutTTY() {
|
|
72
|
+
return !!currentProcess.stdout.isTTY;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get environment variable
|
|
76
|
+
*/
|
|
77
|
+
export function getEnv(key) {
|
|
78
|
+
return currentProcess.env[key];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get current working directory
|
|
82
|
+
*/
|
|
83
|
+
export function getCwd() {
|
|
84
|
+
return currentProcess.cwd();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Create a mock process for testing
|
|
88
|
+
*/
|
|
89
|
+
export function createMockProcess(overrides = {}) {
|
|
90
|
+
const exitFn = jest.fn();
|
|
91
|
+
const stdoutWriteFn = jest.fn().mockReturnValue(true);
|
|
92
|
+
const stderrWriteFn = jest.fn().mockReturnValue(true);
|
|
93
|
+
const cwdFn = jest.fn().mockReturnValue("/mock/cwd");
|
|
94
|
+
return {
|
|
95
|
+
exit: overrides.exit ?? exitFn,
|
|
96
|
+
stdout: overrides.stdout ?? {
|
|
97
|
+
write: stdoutWriteFn,
|
|
98
|
+
isTTY: true,
|
|
99
|
+
},
|
|
100
|
+
stderr: overrides.stderr ?? {
|
|
101
|
+
write: stderrWriteFn,
|
|
102
|
+
},
|
|
103
|
+
env: overrides.env ?? {},
|
|
104
|
+
cwd: overrides.cwd ?? cwdFn,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/utils/screen.js
CHANGED
|
@@ -6,18 +6,56 @@
|
|
|
6
6
|
* the current screen content and switches to a clean buffer. Upon exit,
|
|
7
7
|
* the original screen content is restored.
|
|
8
8
|
*/
|
|
9
|
+
import { processUtils } from "./processUtils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Enter the alternate screen buffer.
|
|
11
12
|
* This provides a fullscreen experience where content won't mix with
|
|
12
13
|
* previous terminal output. Like vim or top.
|
|
13
14
|
*/
|
|
14
15
|
export function enterAlternateScreenBuffer() {
|
|
15
|
-
|
|
16
|
+
processUtils.stdout.write("\x1b[?1049h");
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Exit the alternate screen buffer and restore the previous screen content.
|
|
19
20
|
* This returns the terminal to its original state before enterAlternateScreen() was called.
|
|
20
21
|
*/
|
|
21
22
|
export function exitAlternateScreenBuffer() {
|
|
22
|
-
|
|
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();
|
|
23
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
|
/**
|
|
@@ -128,7 +129,7 @@ export async function executeSSH(devboxId, user, keyfilePath, url, additionalArg
|
|
|
128
129
|
}
|
|
129
130
|
catch (error) {
|
|
130
131
|
console.error("SSH command failed:", error);
|
|
131
|
-
|
|
132
|
+
processUtils.exit(1);
|
|
132
133
|
}
|
|
133
134
|
}
|
|
134
135
|
/**
|
|
@@ -18,80 +18,168 @@ function getLuminance(r, g, b) {
|
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
20
|
* Parse RGB color from terminal response
|
|
21
|
-
*
|
|
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)
|
|
22
25
|
*/
|
|
23
26
|
function parseRGBResponse(response) {
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 };
|
|
28
60
|
}
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
|
34
70
|
}
|
|
35
71
|
/**
|
|
36
72
|
* Detect terminal theme by querying background color
|
|
37
73
|
* Returns 'light' or 'dark' based on background luminance, or null if detection fails
|
|
38
74
|
*
|
|
39
|
-
* NOTE:
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
42
78
|
*/
|
|
43
79
|
export async function detectTerminalTheme() {
|
|
44
80
|
// Skip detection in non-TTY environments
|
|
45
81
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
46
82
|
return null;
|
|
47
83
|
}
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
if (process.env.RUNLOOP_ENABLE_THEME_DETECTION !== "1") {
|
|
84
|
+
// Allow users to opt-out of theme detection if they experience flashing
|
|
85
|
+
if (process.env.RUNLOOP_DISABLE_THEME_DETECTION === "1") {
|
|
51
86
|
return null;
|
|
52
87
|
}
|
|
53
88
|
return new Promise((resolve) => {
|
|
54
89
|
let response = "";
|
|
55
90
|
let timeout;
|
|
91
|
+
let hasResolved = false;
|
|
56
92
|
const cleanup = () => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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);
|
|
61
112
|
};
|
|
62
113
|
const onData = (chunk) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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]")) {
|
|
67
124
|
const rgb = parseRGBResponse(response);
|
|
68
125
|
if (rgb) {
|
|
69
126
|
const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
|
|
70
127
|
// Threshold: luminance > 0.5 is considered light background
|
|
71
|
-
|
|
128
|
+
finish(luminance > 0.5 ? "light" : "dark");
|
|
129
|
+
return;
|
|
72
130
|
}
|
|
73
|
-
|
|
74
|
-
|
|
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;
|
|
75
155
|
}
|
|
76
156
|
}
|
|
77
157
|
};
|
|
78
158
|
// Set timeout for terminals that don't support the query
|
|
79
159
|
timeout = setTimeout(() => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}, 50); // 50ms timeout - quick to minimize any visual flashing
|
|
160
|
+
finish(null);
|
|
161
|
+
}, 200); // Increased timeout to 200ms to give terminals more time to respond
|
|
83
162
|
try {
|
|
84
163
|
// Enable raw mode to capture escape sequences
|
|
85
164
|
stdin.setRawMode(true);
|
|
86
165
|
stdin.resume();
|
|
166
|
+
// Listen to both data and readable events
|
|
87
167
|
stdin.on("data", onData);
|
|
168
|
+
stdin.on("readable", onReadable);
|
|
88
169
|
// Query background color using OSC 11 sequence
|
|
89
170
|
// Format: ESC ] 11 ; ? ESC \
|
|
171
|
+
// Some terminals may need the BEL terminator instead
|
|
90
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);
|
|
91
180
|
}
|
|
92
181
|
catch {
|
|
93
|
-
|
|
94
|
-
resolve(null);
|
|
182
|
+
finish(null);
|
|
95
183
|
}
|
|
96
184
|
});
|
|
97
185
|
}
|