@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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* LogsViewer - Shared component for viewing logs (devbox or blueprint)
|
|
4
|
+
* Extracted from DevboxActionsMenu for reuse
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import figures from "figures";
|
|
9
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
10
|
+
import { colors } from "../utils/theme.js";
|
|
11
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
12
|
+
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
13
|
+
export const LogsViewer = ({ logs, breadcrumbItems = [{ label: "Logs", active: true }], onBack, title = "Logs", }) => {
|
|
14
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
15
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
16
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
17
|
+
// Calculate viewport for logs output:
|
|
18
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
19
|
+
// - Log box borders: 2 lines
|
|
20
|
+
// - Stats bar (marginTop + content): 2 lines
|
|
21
|
+
// - Help bar (marginTop + content): 2 lines
|
|
22
|
+
// - Safety buffer: 1 line
|
|
23
|
+
// Total: 11 lines
|
|
24
|
+
const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
|
|
25
|
+
// Handle input for logs navigation
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (key.upArrow || input === "k") {
|
|
28
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
29
|
+
}
|
|
30
|
+
else if (key.downArrow || input === "j") {
|
|
31
|
+
setLogsScroll(logsScroll + 1);
|
|
32
|
+
}
|
|
33
|
+
else if (key.pageUp) {
|
|
34
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
35
|
+
}
|
|
36
|
+
else if (key.pageDown) {
|
|
37
|
+
setLogsScroll(logsScroll + 10);
|
|
38
|
+
}
|
|
39
|
+
else if (input === "g") {
|
|
40
|
+
setLogsScroll(0);
|
|
41
|
+
}
|
|
42
|
+
else if (input === "G") {
|
|
43
|
+
const maxScroll = Math.max(0, logs.length - logsViewport.viewportHeight);
|
|
44
|
+
setLogsScroll(maxScroll);
|
|
45
|
+
}
|
|
46
|
+
else if (input === "w") {
|
|
47
|
+
setLogsWrapMode(!logsWrapMode);
|
|
48
|
+
}
|
|
49
|
+
else if (input === "c") {
|
|
50
|
+
// Copy logs to clipboard using shared formatter
|
|
51
|
+
const logsText = logs
|
|
52
|
+
.map((log) => {
|
|
53
|
+
const parts = parseAnyLogEntry(log);
|
|
54
|
+
const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
|
|
55
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
56
|
+
const shell = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
57
|
+
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
58
|
+
})
|
|
59
|
+
.join("\n");
|
|
60
|
+
const copyToClipboard = async (text) => {
|
|
61
|
+
const { spawn } = await import("child_process");
|
|
62
|
+
const platform = process.platform;
|
|
63
|
+
let command;
|
|
64
|
+
let args;
|
|
65
|
+
if (platform === "darwin") {
|
|
66
|
+
command = "pbcopy";
|
|
67
|
+
args = [];
|
|
68
|
+
}
|
|
69
|
+
else if (platform === "win32") {
|
|
70
|
+
command = "clip";
|
|
71
|
+
args = [];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
command = "xclip";
|
|
75
|
+
args = ["-selection", "clipboard"];
|
|
76
|
+
}
|
|
77
|
+
const proc = spawn(command, args);
|
|
78
|
+
proc.stdin.write(text);
|
|
79
|
+
proc.stdin.end();
|
|
80
|
+
proc.on("exit", (code) => {
|
|
81
|
+
if (code === 0) {
|
|
82
|
+
setCopyStatus("Copied to clipboard!");
|
|
83
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
setCopyStatus("Failed to copy");
|
|
87
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
proc.on("error", () => {
|
|
91
|
+
setCopyStatus("Copy not supported");
|
|
92
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
copyToClipboard(logsText);
|
|
96
|
+
}
|
|
97
|
+
else if (input === "q" || key.escape || key.return) {
|
|
98
|
+
onBack();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const viewportHeight = Math.max(1, logsViewport.viewportHeight);
|
|
102
|
+
const terminalWidth = logsViewport.terminalWidth;
|
|
103
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
104
|
+
const actualScroll = Math.min(logsScroll, maxScroll);
|
|
105
|
+
const visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
106
|
+
const hasMore = actualScroll + viewportHeight < logs.length;
|
|
107
|
+
const hasLess = actualScroll > 0;
|
|
108
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, children: logs.length === 0 ? (_jsx(Text, { color: colors.textDim, dimColor: true, children: "No logs available" })) : (visibleLogs.map((log, index) => {
|
|
109
|
+
const parts = parseAnyLogEntry(log);
|
|
110
|
+
// Sanitize message: escape special chars to prevent layout breaks
|
|
111
|
+
const escapedMessage = parts.message
|
|
112
|
+
.replace(/\r\n/g, "\\n")
|
|
113
|
+
.replace(/\n/g, "\\n")
|
|
114
|
+
.replace(/\r/g, "\\r")
|
|
115
|
+
.replace(/\t/g, "\\t");
|
|
116
|
+
// Limit message length to prevent Yoga layout engine errors
|
|
117
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
118
|
+
const fullMessage = escapedMessage.length > MAX_MESSAGE_LENGTH
|
|
119
|
+
? escapedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
120
|
+
: escapedMessage;
|
|
121
|
+
const cmd = parts.cmd
|
|
122
|
+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
123
|
+
: "";
|
|
124
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
125
|
+
// Map color names to theme colors
|
|
126
|
+
const levelColorMap = {
|
|
127
|
+
red: colors.error,
|
|
128
|
+
yellow: colors.warning,
|
|
129
|
+
blue: colors.primary,
|
|
130
|
+
gray: colors.textDim,
|
|
131
|
+
};
|
|
132
|
+
const sourceColorMap = {
|
|
133
|
+
magenta: "#d33682",
|
|
134
|
+
cyan: colors.info,
|
|
135
|
+
green: colors.success,
|
|
136
|
+
yellow: colors.warning,
|
|
137
|
+
gray: colors.textDim,
|
|
138
|
+
white: colors.text,
|
|
139
|
+
};
|
|
140
|
+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
141
|
+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
142
|
+
if (logsWrapMode) {
|
|
143
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Calculate available width for message truncation
|
|
147
|
+
const timestampLen = parts.timestamp.length;
|
|
148
|
+
const levelLen = parts.level.length;
|
|
149
|
+
const sourceLen = parts.source.length + 2; // brackets
|
|
150
|
+
const shellLen = parts.shellName ? parts.shellName.length + 3 : 0;
|
|
151
|
+
const cmdLen = cmd.length;
|
|
152
|
+
const exitLen = exitCode.length;
|
|
153
|
+
const spacesLen = 5; // spaces between elements
|
|
154
|
+
const metadataWidth = timestampLen +
|
|
155
|
+
levelLen +
|
|
156
|
+
sourceLen +
|
|
157
|
+
shellLen +
|
|
158
|
+
cmdLen +
|
|
159
|
+
exitLen +
|
|
160
|
+
spacesLen;
|
|
161
|
+
const safeTerminalWidth = Math.max(80, terminalWidth);
|
|
162
|
+
const availableMessageWidth = Math.max(20, safeTerminalWidth - metadataWidth);
|
|
163
|
+
const truncatedMessage = fullMessage.length > availableMessageWidth
|
|
164
|
+
? fullMessage.substring(0, Math.max(1, availableMessageWidth - 3)) + "..."
|
|
165
|
+
: fullMessage;
|
|
166
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: truncatedMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }, index));
|
|
167
|
+
}
|
|
168
|
+
})) }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total logs"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, logs.length), " of", " ", logs.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [g] Top \u2022 [G] Bottom \u2022 [w] Toggle Wrap \u2022 [c] Copy \u2022 [Enter], [q], or [esc] Back"] }) })] }));
|
|
169
|
+
};
|
|
@@ -4,7 +4,7 @@ import { Box, Text, useInput, useApp } from "ink";
|
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Banner } from "./Banner.js";
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
-
import { VERSION } from "../
|
|
7
|
+
import { VERSION } from "../version.js";
|
|
8
8
|
import { colors } from "../utils/theme.js";
|
|
9
9
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
10
10
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
@@ -69,7 +69,7 @@ export const MainMenu = ({ onSelect }) => {
|
|
|
69
69
|
return (_jsxs(Box, { marginBottom: 0, children: [_jsx(Text, { color: isSelected ? item.color : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " }), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "- ", item.description] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", index + 1, "]"] })] }, item.key));
|
|
70
70
|
}) }), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) })] }));
|
|
71
71
|
}
|
|
72
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }] }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
72
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Breadcrumb, { items: [{ label: "Home", active: true }], showVersionCheck: true }), _jsx(Banner, {}), _jsx(Box, { flexDirection: "column", paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Cloud development environments for your team \u2022 v", VERSION] }) }) }), _jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, flexGrow: 1, children: [_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(Text, { color: colors.text, bold: true, children: "Select a resource:" }) }), menuItems.map((item, index) => {
|
|
73
73
|
const isSelected = index === selectedIndex;
|
|
74
74
|
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle: "single", borderColor: isSelected ? item.color : colors.border, marginTop: index === 0 ? 1 : 0, flexShrink: 0, children: [isSelected && (_jsxs(_Fragment, { children: [_jsx(Text, { color: item.color, bold: true, children: figures.pointer }), _jsx(Text, { children: " " })] })), _jsx(Text, { color: item.color, bold: true, children: item.icon }), _jsx(Text, { children: " " }), _jsx(Text, { color: isSelected ? item.color : colors.text, bold: isSelected, children: item.label }), _jsx(Text, { color: colors.textDim, children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: item.description }), _jsx(Text, { children: " " }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["[", index + 1, "]"] })] }, item.key));
|
|
75
75
|
})] }), _jsx(Box, { paddingX: 2, flexShrink: 0, children: _jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [1-3] Quick select \u2022 [Enter] Select \u2022 [Esc] Quit"] }) }) })] }));
|
|
@@ -87,15 +87,15 @@ export function ResourceListView({ config }) {
|
|
|
87
87
|
React.useEffect(() => {
|
|
88
88
|
fetchData(true);
|
|
89
89
|
}, [fetchData]);
|
|
90
|
-
// Auto-refresh
|
|
90
|
+
// Auto-refresh - STOP refreshing when there's an error to avoid flickering
|
|
91
91
|
React.useEffect(() => {
|
|
92
|
-
if (config.autoRefresh?.enabled) {
|
|
92
|
+
if (config.autoRefresh?.enabled && !error) {
|
|
93
93
|
const interval = setInterval(() => {
|
|
94
94
|
fetchData(false);
|
|
95
95
|
}, config.autoRefresh.interval || 3000);
|
|
96
96
|
return () => clearInterval(interval);
|
|
97
97
|
}
|
|
98
|
-
}, [config.autoRefresh, fetchData]);
|
|
98
|
+
}, [config.autoRefresh, fetchData, error]);
|
|
99
99
|
// Removed refresh icon animation to prevent constant re-renders and flashing
|
|
100
100
|
// Filter resources based on search query
|
|
101
101
|
const filteredResources = React.useMemo(() => {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { VERSION } from "../version.js";
|
|
6
|
+
/**
|
|
7
|
+
* Version check component that checks npm for updates and displays a notification
|
|
8
|
+
* Restored from git history and enhanced with better visual styling
|
|
9
|
+
*/
|
|
10
|
+
export const UpdateNotification = () => {
|
|
11
|
+
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
12
|
+
const [isChecking, setIsChecking] = React.useState(true);
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
const checkForUpdates = async () => {
|
|
15
|
+
try {
|
|
16
|
+
const currentVersion = VERSION;
|
|
17
|
+
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
18
|
+
if (response.ok) {
|
|
19
|
+
const data = (await response.json());
|
|
20
|
+
const latestVersion = data.version;
|
|
21
|
+
if (latestVersion && latestVersion !== currentVersion) {
|
|
22
|
+
// Check if current version is older than latest
|
|
23
|
+
const compareVersions = (version1, version2) => {
|
|
24
|
+
const v1parts = version1.split(".").map(Number);
|
|
25
|
+
const v2parts = version2.split(".").map(Number);
|
|
26
|
+
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
27
|
+
const v1part = v1parts[i] || 0;
|
|
28
|
+
const v2part = v2parts[i] || 0;
|
|
29
|
+
if (v1part > v2part)
|
|
30
|
+
return 1;
|
|
31
|
+
if (v1part < v2part)
|
|
32
|
+
return -1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
};
|
|
36
|
+
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
37
|
+
if (isUpdateAvailable) {
|
|
38
|
+
setUpdateAvailable(latestVersion);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Silently fail
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
setIsChecking(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
checkForUpdates();
|
|
51
|
+
}, []);
|
|
52
|
+
if (isChecking || !updateAvailable) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: colors.warning, paddingX: 1, paddingY: 0, marginTop: 0, children: [_jsx(Text, { color: colors.warning, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.warning, bold: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
|
|
56
|
+
};
|
|
@@ -119,9 +119,9 @@ export function useCursorPagination(config) {
|
|
|
119
119
|
// Fetch page 0
|
|
120
120
|
fetchPageData(0, true);
|
|
121
121
|
}, [depsKey, fetchPageData]);
|
|
122
|
-
// Polling effect
|
|
122
|
+
// Polling effect - STOP polling when there's an error to avoid flickering
|
|
123
123
|
React.useEffect(() => {
|
|
124
|
-
if (!pollInterval || pollInterval <= 0 || !pollingEnabled) {
|
|
124
|
+
if (!pollInterval || pollInterval <= 0 || !pollingEnabled || error) {
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
const timer = setInterval(() => {
|
|
@@ -130,7 +130,7 @@ export function useCursorPagination(config) {
|
|
|
130
130
|
}
|
|
131
131
|
}, pollInterval);
|
|
132
132
|
return () => clearInterval(timer);
|
|
133
|
-
}, [pollInterval, pollingEnabled, currentPage, fetchPageData]);
|
|
133
|
+
}, [pollInterval, pollingEnabled, currentPage, fetchPageData, error]);
|
|
134
134
|
// Navigation functions
|
|
135
135
|
const nextPage = React.useCallback(() => {
|
|
136
136
|
if (!loading && !navigating && hasMore) {
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { useInput } from "ink";
|
|
6
6
|
import { exitAlternateScreenBuffer } from "../utils/screen.js";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
8
|
export function useExitOnCtrlC() {
|
|
8
9
|
useInput((input, key) => {
|
|
9
10
|
if (key.ctrl && input === "c") {
|
|
10
11
|
exitAlternateScreenBuffer();
|
|
11
|
-
|
|
12
|
+
processUtils.exit(130); // Standard exit code for SIGINT
|
|
12
13
|
}
|
|
13
14
|
});
|
|
14
15
|
}
|
package/dist/mcp/server-http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { getClient } from "../utils/client.js";
|
|
6
6
|
import express from "express";
|
|
7
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
8
|
// Define available tools for the MCP server
|
|
8
9
|
const TOOLS = [
|
|
9
10
|
{
|
|
@@ -412,5 +413,5 @@ async function main() {
|
|
|
412
413
|
}
|
|
413
414
|
main().catch((error) => {
|
|
414
415
|
console.error("Fatal error in main():", error);
|
|
415
|
-
|
|
416
|
+
processUtils.exit(1);
|
|
416
417
|
});
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,7 +3,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import Runloop from "@runloop/api-client";
|
|
6
|
+
import { VERSION } from "@runloop/api-client/version.js";
|
|
6
7
|
import Conf from "conf";
|
|
8
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
7
9
|
let configInstance = null;
|
|
8
10
|
function getConfigInstance() {
|
|
9
11
|
if (!configInstance) {
|
|
@@ -43,7 +45,7 @@ function getBaseUrl() {
|
|
|
43
45
|
function getClient() {
|
|
44
46
|
const config = getConfig();
|
|
45
47
|
if (!config.apiKey) {
|
|
46
|
-
throw new Error("API key not configured. Please set RUNLOOP_API_KEY environment variable
|
|
48
|
+
throw new Error("API key not configured. Please set RUNLOOP_API_KEY environment variable.");
|
|
47
49
|
}
|
|
48
50
|
const baseURL = getBaseUrl();
|
|
49
51
|
return new Runloop({
|
|
@@ -51,6 +53,9 @@ function getClient() {
|
|
|
51
53
|
baseURL,
|
|
52
54
|
timeout: 10000, // 10 seconds instead of default 30 seconds
|
|
53
55
|
maxRetries: 2, // 2 retries instead of default 5 (only for retryable errors)
|
|
56
|
+
defaultHeaders: {
|
|
57
|
+
"User-Agent": `Runloop/JS ${VERSION} - CLI MCP`,
|
|
58
|
+
},
|
|
54
59
|
});
|
|
55
60
|
}
|
|
56
61
|
// Define available tools for the MCP server
|
|
@@ -452,5 +457,5 @@ async function main() {
|
|
|
452
457
|
main().catch((error) => {
|
|
453
458
|
console.error("[MCP] Fatal error in main():", error);
|
|
454
459
|
console.error("[MCP] Stack trace:", error instanceof Error ? error.stack : "N/A");
|
|
455
|
-
|
|
460
|
+
processUtils.exit(1);
|
|
456
461
|
});
|
package/dist/router/Router.js
CHANGED
|
@@ -16,6 +16,7 @@ import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
|
|
|
16
16
|
import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
|
|
17
17
|
import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
|
|
18
18
|
import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
|
|
19
|
+
import { BlueprintLogsScreen } from "../screens/BlueprintLogsScreen.js";
|
|
19
20
|
import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
|
|
20
21
|
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
21
22
|
/**
|
|
@@ -46,6 +47,7 @@ export function Router() {
|
|
|
46
47
|
break;
|
|
47
48
|
case "blueprint-list":
|
|
48
49
|
case "blueprint-detail":
|
|
50
|
+
case "blueprint-logs":
|
|
49
51
|
if (!currentScreen.startsWith("blueprint")) {
|
|
50
52
|
useBlueprintStore.getState().clearAll();
|
|
51
53
|
}
|
|
@@ -64,5 +66,5 @@ export function Router() {
|
|
|
64
66
|
// and mount new component, preventing race conditions during screen transitions.
|
|
65
67
|
// The key ensures React treats this as a completely new component tree.
|
|
66
68
|
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
67
|
-
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
69
|
+
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
68
70
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BlueprintLogsScreen - Screen for viewing blueprint build logs
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
8
|
+
import { LogsViewer } from "../components/LogsViewer.js";
|
|
9
|
+
import { Header } from "../components/Header.js";
|
|
10
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
11
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
12
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
13
|
+
import { getBlueprintLogs } from "../services/blueprintService.js";
|
|
14
|
+
import { colors } from "../utils/theme.js";
|
|
15
|
+
export function BlueprintLogsScreen({ blueprintId }) {
|
|
16
|
+
const { goBack, params } = useNavigation();
|
|
17
|
+
const [logs, setLogs] = React.useState([]);
|
|
18
|
+
const [loading, setLoading] = React.useState(true);
|
|
19
|
+
const [error, setError] = React.useState(null);
|
|
20
|
+
// Use blueprintId from props or params
|
|
21
|
+
const id = blueprintId || params.blueprintId;
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
if (!id) {
|
|
24
|
+
goBack();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const fetchLogs = async () => {
|
|
29
|
+
try {
|
|
30
|
+
setLoading(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
const blueprintLogs = await getBlueprintLogs(id);
|
|
33
|
+
if (!cancelled) {
|
|
34
|
+
setLogs(Array.isArray(blueprintLogs) ? blueprintLogs : []);
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
if (!cancelled) {
|
|
40
|
+
setError(err);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
fetchLogs();
|
|
46
|
+
return () => {
|
|
47
|
+
cancelled = true;
|
|
48
|
+
};
|
|
49
|
+
}, [id, goBack]);
|
|
50
|
+
if (!id) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Get blueprint name from params if available (for breadcrumb)
|
|
54
|
+
const blueprintName = params.blueprintName || id;
|
|
55
|
+
if (loading) {
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
57
|
+
{ label: "Blueprints" },
|
|
58
|
+
{ label: blueprintName },
|
|
59
|
+
{ label: "Logs", active: true },
|
|
60
|
+
] }), _jsx(Header, { title: "Loading Logs" }), _jsx(SpinnerComponent, { message: "Fetching blueprint logs..." })] }));
|
|
61
|
+
}
|
|
62
|
+
if (error) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
|
+
{ label: "Blueprints" },
|
|
65
|
+
{ label: blueprintName },
|
|
66
|
+
{ label: "Logs", active: true },
|
|
67
|
+
] }), _jsx(Header, { title: "Error" }), _jsx(ErrorMessage, { message: "Failed to load blueprint logs", error: error }), _jsx(Box, { marginTop: 1, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [q] or [esc] to go back" }) })] }));
|
|
68
|
+
}
|
|
69
|
+
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
70
|
+
{ label: "Blueprints" },
|
|
71
|
+
{ label: blueprintName },
|
|
72
|
+
{ label: "Logs", active: true },
|
|
73
|
+
], onBack: goBack, title: "Blueprint Build Logs" }));
|
|
74
|
+
}
|
|
@@ -72,34 +72,30 @@ export async function getBlueprint(id) {
|
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Get blueprint logs
|
|
75
|
+
* Returns the raw logs array from the API response
|
|
76
|
+
* Similar to getDevboxLogs - formatting is handled by logFormatter
|
|
75
77
|
*/
|
|
76
78
|
export async function getBlueprintLogs(id) {
|
|
77
79
|
const client = getClient();
|
|
78
80
|
const response = await client.blueprints.logs(id);
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const MAX_LEVEL_LENGTH = 20;
|
|
82
|
-
const logs = [];
|
|
81
|
+
// Return the logs array directly - formatting is handled by logFormatter
|
|
82
|
+
// Ensure timestamp_ms is present (API may return timestamp or timestamp_ms)
|
|
83
83
|
if (response.logs && Array.isArray(response.logs)) {
|
|
84
|
-
response.logs.
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
return response.logs.map((log) => {
|
|
85
|
+
// Normalize timestamp field to timestamp_ms if needed
|
|
86
|
+
// Create a new object to avoid mutating the original
|
|
87
|
+
const normalizedLog = { ...log };
|
|
88
|
+
if (normalizedLog.timestamp && !normalizedLog.timestamp_ms) {
|
|
89
|
+
// If timestamp is a number, use it directly; if it's a string, parse it
|
|
90
|
+
if (typeof normalizedLog.timestamp === "number") {
|
|
91
|
+
normalizedLog.timestamp_ms = normalizedLog.timestamp;
|
|
92
|
+
}
|
|
93
|
+
else if (typeof normalizedLog.timestamp === "string") {
|
|
94
|
+
normalizedLog.timestamp_ms = new Date(normalizedLog.timestamp).getTime();
|
|
95
|
+
}
|
|
89
96
|
}
|
|
90
|
-
|
|
91
|
-
.replace(/\r\n/g, "\\n")
|
|
92
|
-
.replace(/\n/g, "\\n")
|
|
93
|
-
.replace(/\r/g, "\\r")
|
|
94
|
-
.replace(/\t/g, "\\t");
|
|
95
|
-
logs.push({
|
|
96
|
-
timestamp: log.timestamp,
|
|
97
|
-
message,
|
|
98
|
-
level: log.level
|
|
99
|
-
? String(log.level).substring(0, MAX_LEVEL_LENGTH)
|
|
100
|
-
: undefined,
|
|
101
|
-
});
|
|
97
|
+
return normalizedLog;
|
|
102
98
|
});
|
|
103
99
|
}
|
|
104
|
-
return
|
|
100
|
+
return [];
|
|
105
101
|
}
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { getClient } from "./client.js";
|
|
7
7
|
import { shouldUseNonInteractiveOutput, outputList, outputResult, } from "./output.js";
|
|
8
|
-
import { enableSynchronousUpdates, disableSynchronousUpdates, } from "./terminalSync.js";
|
|
9
|
-
import { exitAlternateScreenBuffer, enterAlternateScreenBuffer, } from "./screen.js";
|
|
10
8
|
import YAML from "yaml";
|
|
11
9
|
export class CommandExecutor {
|
|
12
10
|
options;
|
|
@@ -34,16 +32,13 @@ export class CommandExecutor {
|
|
|
34
32
|
return;
|
|
35
33
|
}
|
|
36
34
|
// Interactive mode
|
|
37
|
-
// Enter alternate screen buffer
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
exitOnCtrlC: false,
|
|
42
|
-
});
|
|
35
|
+
// Enter alternate screen buffer
|
|
36
|
+
process.stdout.write("\x1b[?1049h");
|
|
37
|
+
console.clear();
|
|
38
|
+
const { waitUntilExit } = render(renderUI());
|
|
43
39
|
await waitUntilExit();
|
|
44
40
|
// Exit alternate screen buffer
|
|
45
|
-
|
|
46
|
-
exitAlternateScreenBuffer();
|
|
41
|
+
process.stdout.write("\x1b[?1049l");
|
|
47
42
|
}
|
|
48
43
|
/**
|
|
49
44
|
* Execute a create/action command with automatic format handling
|
|
@@ -60,17 +55,13 @@ export class CommandExecutor {
|
|
|
60
55
|
return;
|
|
61
56
|
}
|
|
62
57
|
// Interactive mode
|
|
63
|
-
// Enter alternate screen buffer
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const { waitUntilExit } = render(renderUI()
|
|
67
|
-
patchConsole: false,
|
|
68
|
-
exitOnCtrlC: false,
|
|
69
|
-
});
|
|
58
|
+
// Enter alternate screen buffer
|
|
59
|
+
process.stdout.write("\x1b[?1049h");
|
|
60
|
+
console.clear();
|
|
61
|
+
const { waitUntilExit } = render(renderUI());
|
|
70
62
|
await waitUntilExit();
|
|
71
63
|
// Exit alternate screen buffer
|
|
72
|
-
|
|
73
|
-
exitAlternateScreenBuffer();
|
|
64
|
+
process.stdout.write("\x1b[?1049l");
|
|
74
65
|
}
|
|
75
66
|
/**
|
|
76
67
|
* Execute a delete command with automatic format handling
|
|
@@ -88,50 +79,30 @@ export class CommandExecutor {
|
|
|
88
79
|
}
|
|
89
80
|
// Interactive mode
|
|
90
81
|
// Enter alternate screen buffer
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const { waitUntilExit } = render(renderUI(), {
|
|
94
|
-
patchConsole: false,
|
|
95
|
-
exitOnCtrlC: false,
|
|
96
|
-
});
|
|
82
|
+
process.stdout.write("\x1b[?1049h");
|
|
83
|
+
const { waitUntilExit } = render(renderUI());
|
|
97
84
|
await waitUntilExit();
|
|
98
85
|
// Exit alternate screen buffer
|
|
99
|
-
|
|
100
|
-
exitAlternateScreenBuffer();
|
|
86
|
+
process.stdout.write("\x1b[?1049l");
|
|
101
87
|
}
|
|
102
88
|
/**
|
|
103
89
|
* Fetch items from an async iterator with optional filtering and limits
|
|
104
|
-
* IMPORTANT: This method tries to access the page data directly first to avoid
|
|
105
|
-
* auto-pagination issues that can cause memory errors with large datasets.
|
|
106
90
|
*/
|
|
107
91
|
async fetchFromIterator(iterator, options = {}) {
|
|
108
92
|
const { filter, limit = 100 } = options;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (filter && !filter(item)) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
items.push(item);
|
|
123
|
-
count++;
|
|
124
|
-
if (count >= limit) {
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
93
|
+
const items = [];
|
|
94
|
+
let count = 0;
|
|
95
|
+
for await (const item of iterator) {
|
|
96
|
+
if (filter && !filter(item)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
items.push(item);
|
|
100
|
+
count++;
|
|
101
|
+
if (count >= limit) {
|
|
102
|
+
break;
|
|
127
103
|
}
|
|
128
104
|
}
|
|
129
|
-
|
|
130
|
-
if (filter) {
|
|
131
|
-
items = items.filter(filter);
|
|
132
|
-
}
|
|
133
|
-
// Apply limit
|
|
134
|
-
return items.slice(0, limit);
|
|
105
|
+
return items;
|
|
135
106
|
}
|
|
136
107
|
/**
|
|
137
108
|
* Handle errors consistently across all commands
|