@runloop/rl-cli 1.6.0 → 1.7.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/dist/commands/devbox/create.js +18 -0
- package/dist/commands/devbox/tunnel.js +25 -0
- package/dist/components/DevboxActionsMenu.js +356 -50
- package/dist/components/ExecViewer.js +439 -0
- package/dist/components/LogsViewer.js +5 -2
- package/dist/components/ResourceDetailPage.js +2 -2
- package/dist/components/StreamingLogsViewer.js +276 -0
- package/dist/router/Router.js +3 -1
- package/dist/screens/DevboxExecScreen.js +51 -0
- package/dist/screens/SnapshotDetailScreen.js +20 -0
- package/dist/services/devboxService.js +42 -5
- package/dist/services/snapshotService.js +17 -4
- package/dist/utils/commands.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* StreamingLogsViewer - Live streaming logs viewer with auto-refresh
|
|
4
|
+
* Polls for new logs periodically and auto-scrolls when at bottom
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text, useInput } from "ink";
|
|
8
|
+
import Spinner from "ink-spinner";
|
|
9
|
+
import figures from "figures";
|
|
10
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
12
|
+
import { colors } from "../utils/theme.js";
|
|
13
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
14
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
15
|
+
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
16
|
+
import { getDevboxLogs } from "../services/devboxService.js";
|
|
17
|
+
export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Logs", active: true }], onBack, }) => {
|
|
18
|
+
const [logs, setLogs] = React.useState([]);
|
|
19
|
+
const [loading, setLoading] = React.useState(true);
|
|
20
|
+
const [error, setError] = React.useState(null);
|
|
21
|
+
const [logsWrapMode, setLogsWrapMode] = React.useState(false);
|
|
22
|
+
const [logsScroll, setLogsScroll] = React.useState(0);
|
|
23
|
+
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
24
|
+
const [autoScroll, setAutoScroll] = React.useState(true);
|
|
25
|
+
const [isPolling, setIsPolling] = React.useState(true);
|
|
26
|
+
// Refs for cleanup
|
|
27
|
+
const pollIntervalRef = React.useRef(null);
|
|
28
|
+
// Calculate viewport
|
|
29
|
+
const logsViewport = useViewportHeight({ overhead: 10, minHeight: 10 });
|
|
30
|
+
// Handle Ctrl+C
|
|
31
|
+
useExitOnCtrlC();
|
|
32
|
+
// Fetch logs function
|
|
33
|
+
const fetchLogs = React.useCallback(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const newLogs = await getDevboxLogs(devboxId);
|
|
36
|
+
setLogs(newLogs);
|
|
37
|
+
setError(null);
|
|
38
|
+
if (loading)
|
|
39
|
+
setLoading(false);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
setError(err.message);
|
|
43
|
+
if (loading)
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
}, [devboxId, loading]);
|
|
47
|
+
// Start polling on mount
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
// Initial fetch
|
|
50
|
+
fetchLogs();
|
|
51
|
+
// Poll every 2 seconds
|
|
52
|
+
pollIntervalRef.current = setInterval(() => {
|
|
53
|
+
if (isPolling) {
|
|
54
|
+
fetchLogs();
|
|
55
|
+
}
|
|
56
|
+
}, 2000);
|
|
57
|
+
return () => {
|
|
58
|
+
if (pollIntervalRef.current) {
|
|
59
|
+
clearInterval(pollIntervalRef.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, [fetchLogs, isPolling]);
|
|
63
|
+
// Calculate max scroll position
|
|
64
|
+
const getMaxScroll = () => {
|
|
65
|
+
if (logsWrapMode) {
|
|
66
|
+
return Math.max(0, logs.length - 1);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
return Math.max(0, logs.length - logsViewport.viewportHeight);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// Auto-scroll effect
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
if (autoScroll && logs.length > 0) {
|
|
75
|
+
const maxScroll = getMaxScroll();
|
|
76
|
+
setLogsScroll(maxScroll);
|
|
77
|
+
}
|
|
78
|
+
}, [logs.length, autoScroll, logsViewport.viewportHeight]);
|
|
79
|
+
// Handle input
|
|
80
|
+
useInput((input, key) => {
|
|
81
|
+
const maxScroll = getMaxScroll();
|
|
82
|
+
if (key.upArrow || input === "k") {
|
|
83
|
+
setLogsScroll(Math.max(0, logsScroll - 1));
|
|
84
|
+
setAutoScroll(false);
|
|
85
|
+
}
|
|
86
|
+
else if (key.downArrow || input === "j") {
|
|
87
|
+
const newScroll = Math.min(maxScroll, logsScroll + 1);
|
|
88
|
+
setLogsScroll(newScroll);
|
|
89
|
+
// Re-enable auto-scroll if we scroll to bottom
|
|
90
|
+
if (newScroll >= maxScroll) {
|
|
91
|
+
setAutoScroll(true);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (key.pageUp) {
|
|
95
|
+
setLogsScroll(Math.max(0, logsScroll - 10));
|
|
96
|
+
setAutoScroll(false);
|
|
97
|
+
}
|
|
98
|
+
else if (key.pageDown) {
|
|
99
|
+
const newScroll = Math.min(maxScroll, logsScroll + 10);
|
|
100
|
+
setLogsScroll(newScroll);
|
|
101
|
+
if (newScroll >= maxScroll) {
|
|
102
|
+
setAutoScroll(true);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (input === "g") {
|
|
106
|
+
setLogsScroll(0);
|
|
107
|
+
setAutoScroll(false);
|
|
108
|
+
}
|
|
109
|
+
else if (input === "G") {
|
|
110
|
+
setLogsScroll(maxScroll);
|
|
111
|
+
setAutoScroll(true);
|
|
112
|
+
}
|
|
113
|
+
else if (input === "w") {
|
|
114
|
+
setLogsWrapMode(!logsWrapMode);
|
|
115
|
+
}
|
|
116
|
+
else if (input === "p") {
|
|
117
|
+
// Toggle polling
|
|
118
|
+
setIsPolling(!isPolling);
|
|
119
|
+
}
|
|
120
|
+
else if (input === "c" && !key.ctrl) {
|
|
121
|
+
// Copy logs to clipboard (ignore if Ctrl+C for quit)
|
|
122
|
+
const logsText = logs
|
|
123
|
+
.map((log) => {
|
|
124
|
+
const parts = parseAnyLogEntry(log);
|
|
125
|
+
const cmd = parts.cmd ? `$ ${parts.cmd} ` : "";
|
|
126
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
127
|
+
const shell = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
128
|
+
return `${parts.timestamp} ${parts.level} [${parts.source}] ${shell}${cmd}${parts.message} ${exitCode}`.trim();
|
|
129
|
+
})
|
|
130
|
+
.join("\n");
|
|
131
|
+
const copyToClipboard = async (text) => {
|
|
132
|
+
const { spawn } = await import("child_process");
|
|
133
|
+
const platform = process.platform;
|
|
134
|
+
let command;
|
|
135
|
+
let args;
|
|
136
|
+
if (platform === "darwin") {
|
|
137
|
+
command = "pbcopy";
|
|
138
|
+
args = [];
|
|
139
|
+
}
|
|
140
|
+
else if (platform === "win32") {
|
|
141
|
+
command = "clip";
|
|
142
|
+
args = [];
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
command = "xclip";
|
|
146
|
+
args = ["-selection", "clipboard"];
|
|
147
|
+
}
|
|
148
|
+
const proc = spawn(command, args);
|
|
149
|
+
proc.stdin.write(text);
|
|
150
|
+
proc.stdin.end();
|
|
151
|
+
proc.on("exit", (code) => {
|
|
152
|
+
if (code === 0) {
|
|
153
|
+
setCopyStatus("Copied!");
|
|
154
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
setCopyStatus("Failed");
|
|
158
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
proc.on("error", () => {
|
|
162
|
+
setCopyStatus("Not supported");
|
|
163
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
copyToClipboard(logsText);
|
|
167
|
+
}
|
|
168
|
+
else if (input === "q" || key.escape || key.return) {
|
|
169
|
+
onBack();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const viewportHeight = Math.max(1, logsViewport.viewportHeight);
|
|
173
|
+
const terminalWidth = logsViewport.terminalWidth;
|
|
174
|
+
const boxChrome = 8;
|
|
175
|
+
const contentWidth = Math.max(40, terminalWidth - boxChrome);
|
|
176
|
+
// Helper to sanitize log message
|
|
177
|
+
const sanitizeMessage = (message) => {
|
|
178
|
+
const strippedAnsi = message.replace(
|
|
179
|
+
// eslint-disable-next-line no-control-regex
|
|
180
|
+
/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
|
|
181
|
+
return (strippedAnsi
|
|
182
|
+
.replace(/\r\n/g, " ")
|
|
183
|
+
.replace(/\n/g, " ")
|
|
184
|
+
.replace(/\r/g, " ")
|
|
185
|
+
.replace(/\t/g, " ")
|
|
186
|
+
// eslint-disable-next-line no-control-regex
|
|
187
|
+
.replace(/[\x00-\x1F]/g, ""));
|
|
188
|
+
};
|
|
189
|
+
// Calculate visible logs
|
|
190
|
+
let visibleLogs;
|
|
191
|
+
let actualScroll;
|
|
192
|
+
if (logsWrapMode) {
|
|
193
|
+
actualScroll = Math.min(logsScroll, Math.max(0, logs.length - 1));
|
|
194
|
+
visibleLogs = [];
|
|
195
|
+
let lineCount = 0;
|
|
196
|
+
for (let i = actualScroll; i < logs.length; i++) {
|
|
197
|
+
const parts = parseAnyLogEntry(logs[i]);
|
|
198
|
+
const sanitized = sanitizeMessage(parts.message);
|
|
199
|
+
const totalLength = parts.timestamp.length +
|
|
200
|
+
parts.level.length +
|
|
201
|
+
parts.source.length +
|
|
202
|
+
sanitized.length +
|
|
203
|
+
10;
|
|
204
|
+
const entryLines = Math.ceil(totalLength / contentWidth);
|
|
205
|
+
if (lineCount + entryLines > viewportHeight && visibleLogs.length > 0) {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
visibleLogs.push(logs[i]);
|
|
209
|
+
lineCount += entryLines;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const maxScroll = Math.max(0, logs.length - viewportHeight);
|
|
214
|
+
actualScroll = Math.min(logsScroll, maxScroll);
|
|
215
|
+
visibleLogs = logs.slice(actualScroll, actualScroll + viewportHeight);
|
|
216
|
+
}
|
|
217
|
+
const hasMore = actualScroll + visibleLogs.length < logs.length;
|
|
218
|
+
const hasLess = actualScroll > 0;
|
|
219
|
+
// Color maps
|
|
220
|
+
const levelColorMap = {
|
|
221
|
+
red: colors.error,
|
|
222
|
+
yellow: colors.warning,
|
|
223
|
+
blue: colors.primary,
|
|
224
|
+
gray: colors.textDim,
|
|
225
|
+
};
|
|
226
|
+
const sourceColorMap = {
|
|
227
|
+
magenta: "#d33682",
|
|
228
|
+
cyan: colors.info,
|
|
229
|
+
green: colors.success,
|
|
230
|
+
yellow: colors.warning,
|
|
231
|
+
gray: colors.textDim,
|
|
232
|
+
white: colors.text,
|
|
233
|
+
};
|
|
234
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, height: viewportHeight + 2, children: loading ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.info, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: colors.textDim, children: " Loading logs..." })] })) : error ? (_jsxs(Text, { color: colors.error, children: [figures.cross, " Error: ", error] })) : logs.length === 0 ? (_jsxs(Box, { children: [isPolling && (_jsxs(Text, { color: colors.info, children: [_jsx(Spinner, { type: "dots" }), " "] })), _jsx(Text, { color: colors.textDim, children: "Waiting for logs..." })] })) : (visibleLogs.map((log, index) => {
|
|
235
|
+
const parts = parseAnyLogEntry(log);
|
|
236
|
+
const sanitizedMessage = sanitizeMessage(parts.message);
|
|
237
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
238
|
+
const fullMessage = sanitizedMessage.length > MAX_MESSAGE_LENGTH
|
|
239
|
+
? sanitizedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
240
|
+
: sanitizedMessage;
|
|
241
|
+
const cmd = parts.cmd
|
|
242
|
+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
243
|
+
: "";
|
|
244
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
245
|
+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
246
|
+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
247
|
+
if (logsWrapMode) {
|
|
248
|
+
return (_jsx(Box, { width: contentWidth, flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", 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));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const shellPart = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
252
|
+
const exitPart = exitCode ? ` ${exitCode}` : "";
|
|
253
|
+
const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
|
|
254
|
+
const suffix = exitPart;
|
|
255
|
+
const availableForMessage = contentWidth - prefix.length - suffix.length;
|
|
256
|
+
let displayMessage;
|
|
257
|
+
if (availableForMessage <= 3) {
|
|
258
|
+
displayMessage = "";
|
|
259
|
+
}
|
|
260
|
+
else if (fullMessage.length <= availableForMessage) {
|
|
261
|
+
displayMessage = fullMessage;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
displayMessage =
|
|
265
|
+
fullMessage.substring(0, availableForMessage - 3) + "...";
|
|
266
|
+
}
|
|
267
|
+
return (_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", 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: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, index));
|
|
268
|
+
}
|
|
269
|
+
})) }), _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: [" ", "logs"] }), logs.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [actualScroll + 1, "-", Math.min(actualScroll + visibleLogs.length, 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", " "] }), isPolling ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.success, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: colors.success, children: " Live" })] })) : (_jsx(Text, { color: colors.textDim, children: "Paused" })), _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(NavigationTips, { showArrows: true, tips: [
|
|
270
|
+
{ key: "g/G", label: "Top/Bottom" },
|
|
271
|
+
{ key: "p", label: isPolling ? "Pause" : "Resume" },
|
|
272
|
+
{ key: "w", label: "Wrap" },
|
|
273
|
+
{ key: "c", label: "Copy" },
|
|
274
|
+
{ key: "q/esc", label: "Back" },
|
|
275
|
+
] })] }));
|
|
276
|
+
};
|
package/dist/router/Router.js
CHANGED
|
@@ -16,6 +16,7 @@ import { MenuScreen } from "../screens/MenuScreen.js";
|
|
|
16
16
|
import { DevboxListScreen } from "../screens/DevboxListScreen.js";
|
|
17
17
|
import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
|
|
18
18
|
import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
|
|
19
|
+
import { DevboxExecScreen } from "../screens/DevboxExecScreen.js";
|
|
19
20
|
import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
|
|
20
21
|
import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
|
|
21
22
|
import { BlueprintDetailScreen } from "../screens/BlueprintDetailScreen.js";
|
|
@@ -47,6 +48,7 @@ export function Router() {
|
|
|
47
48
|
case "devbox-list":
|
|
48
49
|
case "devbox-detail":
|
|
49
50
|
case "devbox-actions":
|
|
51
|
+
case "devbox-exec":
|
|
50
52
|
case "devbox-create":
|
|
51
53
|
// Clear devbox data when leaving devbox screens
|
|
52
54
|
// Keep cache if we're still in devbox context
|
|
@@ -88,5 +90,5 @@ export function Router() {
|
|
|
88
90
|
// and mount new component, preventing race conditions during screen transitions.
|
|
89
91
|
// The key ensures React treats this as a completely new component tree.
|
|
90
92
|
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
91
|
-
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(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
93
|
+
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-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
92
94
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DevboxExecScreen - Dedicated screen for command execution
|
|
4
|
+
* Route-based state ensures stability across terminal resizes
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Box, Text } from "ink";
|
|
8
|
+
import figures from "figures";
|
|
9
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
10
|
+
import { useDevboxStore } from "../store/devboxStore.js";
|
|
11
|
+
import { ExecViewer } from "../components/ExecViewer.js";
|
|
12
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
13
|
+
import { colors } from "../utils/theme.js";
|
|
14
|
+
export function DevboxExecScreen({ devboxId, execCommand, executionId, devboxName, returnScreen, returnParams, }) {
|
|
15
|
+
const { goBack, replace, params } = useNavigation();
|
|
16
|
+
const devboxes = useDevboxStore((state) => state.devboxes);
|
|
17
|
+
// Find devbox in store for display name
|
|
18
|
+
const devbox = devboxes.find((d) => d.id === devboxId);
|
|
19
|
+
const displayName = devboxName || devbox?.name || devboxId || "devbox";
|
|
20
|
+
// Validate required params
|
|
21
|
+
if (!devboxId || !execCommand) {
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Exec", active: true }] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Missing execution parameters. Returning..."] }) })] }));
|
|
23
|
+
}
|
|
24
|
+
// Build breadcrumbs
|
|
25
|
+
const breadcrumbItems = [
|
|
26
|
+
{ label: "Devboxes" },
|
|
27
|
+
{ label: displayName },
|
|
28
|
+
{ label: "Execute", active: true },
|
|
29
|
+
];
|
|
30
|
+
// Handle execution ID update - store in route params for persistence
|
|
31
|
+
const handleExecutionStart = React.useCallback((newExecutionId) => {
|
|
32
|
+
// Update route params to persist execution ID across re-renders
|
|
33
|
+
replace("devbox-exec", {
|
|
34
|
+
...params,
|
|
35
|
+
executionId: newExecutionId,
|
|
36
|
+
});
|
|
37
|
+
}, [replace, params]);
|
|
38
|
+
// Handle back navigation
|
|
39
|
+
const handleBack = React.useCallback(() => {
|
|
40
|
+
goBack();
|
|
41
|
+
}, [goBack]);
|
|
42
|
+
// Handle run another command - navigate to actions menu with exec pre-selected
|
|
43
|
+
const handleRunAnother = React.useCallback(() => {
|
|
44
|
+
// Replace current screen with actions menu, exec operation pre-selected
|
|
45
|
+
replace("devbox-actions", {
|
|
46
|
+
devboxId: devboxId,
|
|
47
|
+
operation: "exec",
|
|
48
|
+
});
|
|
49
|
+
}, [replace, devboxId]);
|
|
50
|
+
return (_jsx(ExecViewer, { devboxId: devboxId, command: execCommand, breadcrumbItems: breadcrumbItems, onBack: handleBack, onRunAnother: handleRunAnother, existingExecutionId: executionId, onExecutionStart: handleExecutionStart }));
|
|
51
|
+
}
|
|
@@ -96,6 +96,20 @@ export function SnapshotDetailScreen({ snapshotId, }) {
|
|
|
96
96
|
fields: basicFields,
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
+
// Commit message section (if present)
|
|
100
|
+
if (snapshot.commit_message) {
|
|
101
|
+
detailSections.push({
|
|
102
|
+
title: "Commit Message",
|
|
103
|
+
icon: figures.info,
|
|
104
|
+
color: colors.info,
|
|
105
|
+
fields: [
|
|
106
|
+
{
|
|
107
|
+
label: "",
|
|
108
|
+
value: snapshot.commit_message,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
99
113
|
// Metadata section
|
|
100
114
|
if (snapshot.metadata && Object.keys(snapshot.metadata).length > 0) {
|
|
101
115
|
const metadataFields = Object.entries(snapshot.metadata).map(([key, value]) => ({
|
|
@@ -172,6 +186,12 @@ export function SnapshotDetailScreen({ snapshotId, }) {
|
|
|
172
186
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Disk Size: ", sizeGB, " GB"] }, "core-size"));
|
|
173
187
|
}
|
|
174
188
|
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
189
|
+
// Commit Message
|
|
190
|
+
if (snap.commit_message) {
|
|
191
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Commit Message" }, "commit-title"));
|
|
192
|
+
lines.push(_jsxs(Text, { color: colors.info, children: [" ", snap.commit_message] }, "commit-msg"));
|
|
193
|
+
lines.push(_jsx(Text, { children: " " }, "commit-space"));
|
|
194
|
+
}
|
|
175
195
|
// Metadata
|
|
176
196
|
if (snap.metadata && Object.keys(snap.metadata).length > 0) {
|
|
177
197
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
|
|
@@ -146,12 +146,19 @@ export async function uploadFile(id, filepath, remotePath) {
|
|
|
146
146
|
path: remotePath,
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
|
-
|
|
150
|
-
* Create snapshot of devbox
|
|
151
|
-
*/
|
|
152
|
-
export async function createSnapshot(id, name) {
|
|
149
|
+
export async function createSnapshot(id, options) {
|
|
153
150
|
const client = getClient();
|
|
154
|
-
const
|
|
151
|
+
const params = {};
|
|
152
|
+
if (options?.name) {
|
|
153
|
+
params.name = options.name;
|
|
154
|
+
}
|
|
155
|
+
if (options?.metadata && Object.keys(options.metadata).length > 0) {
|
|
156
|
+
params.metadata = options.metadata;
|
|
157
|
+
}
|
|
158
|
+
if (options?.commit_message) {
|
|
159
|
+
params.commit_message = options.commit_message;
|
|
160
|
+
}
|
|
161
|
+
const snapshot = await client.devboxes.snapshotDisk(id, params);
|
|
155
162
|
return {
|
|
156
163
|
id: String(snapshot.id || "").substring(0, 100),
|
|
157
164
|
name: snapshot.name ? String(snapshot.name).substring(0, 200) : undefined,
|
|
@@ -213,3 +220,33 @@ export async function getDevboxLogs(id) {
|
|
|
213
220
|
// Return the logs array directly - formatting is handled by logFormatter
|
|
214
221
|
return response.logs || [];
|
|
215
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Execute command asynchronously in devbox
|
|
225
|
+
* Used for both sync and async modes to enable kill/leave-early functionality
|
|
226
|
+
*/
|
|
227
|
+
export async function execCommandAsync(id, command) {
|
|
228
|
+
const client = getClient();
|
|
229
|
+
const result = await client.devboxes.executions.executeAsync(id, { command });
|
|
230
|
+
// Extract execution ID from result
|
|
231
|
+
const executionId = result.execution_id || result.id || String(result);
|
|
232
|
+
return {
|
|
233
|
+
executionId: String(executionId).substring(0, 100),
|
|
234
|
+
status: result.status || "running",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get execution status and output
|
|
239
|
+
* Used for polling in sync mode and checking status
|
|
240
|
+
*/
|
|
241
|
+
export async function getExecution(devboxId, executionId) {
|
|
242
|
+
const client = getClient();
|
|
243
|
+
return client.devboxes.executions.retrieve(devboxId, executionId);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Kill a running execution
|
|
247
|
+
* Available in both sync and async modes
|
|
248
|
+
*/
|
|
249
|
+
export async function killExecution(devboxId, executionId) {
|
|
250
|
+
const client = getClient();
|
|
251
|
+
await client.devboxes.executions.kill(devboxId, executionId);
|
|
252
|
+
}
|
|
@@ -26,6 +26,7 @@ export async function listSnapshots(options) {
|
|
|
26
26
|
const MAX_ID_LENGTH = 100;
|
|
27
27
|
const MAX_NAME_LENGTH = 200;
|
|
28
28
|
const MAX_STATUS_LENGTH = 50;
|
|
29
|
+
const MAX_COMMIT_MSG_LENGTH = 1000;
|
|
29
30
|
// Status is constructed/available in API response but not in type definition
|
|
30
31
|
const snapshotView = s;
|
|
31
32
|
snapshots.push({
|
|
@@ -33,6 +34,9 @@ export async function listSnapshots(options) {
|
|
|
33
34
|
name: snapshotView.name
|
|
34
35
|
? String(snapshotView.name).substring(0, MAX_NAME_LENGTH)
|
|
35
36
|
: undefined,
|
|
37
|
+
commit_message: snapshotView.commit_message
|
|
38
|
+
? String(snapshotView.commit_message).substring(0, MAX_COMMIT_MSG_LENGTH)
|
|
39
|
+
: undefined,
|
|
36
40
|
create_time_ms: snapshotView.create_time_ms,
|
|
37
41
|
metadata: snapshotView.metadata || {},
|
|
38
42
|
source_devbox_id: String(snapshotView.source_devbox_id || "").substring(0, MAX_ID_LENGTH),
|
|
@@ -81,6 +85,7 @@ export async function getSnapshot(id) {
|
|
|
81
85
|
return {
|
|
82
86
|
id: snapshot.id,
|
|
83
87
|
name: snapshot.name || undefined,
|
|
88
|
+
commit_message: snapshot.commit_message || undefined,
|
|
84
89
|
create_time_ms: snapshot.create_time_ms,
|
|
85
90
|
metadata: snapshot.metadata || {},
|
|
86
91
|
source_devbox_id: snapshot.source_devbox_id || "",
|
|
@@ -92,11 +97,19 @@ export async function getSnapshot(id) {
|
|
|
92
97
|
/**
|
|
93
98
|
* Create a snapshot
|
|
94
99
|
*/
|
|
95
|
-
export async function createSnapshot(devboxId,
|
|
100
|
+
export async function createSnapshot(devboxId, options) {
|
|
96
101
|
const client = getClient();
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const params = {};
|
|
103
|
+
if (options?.name) {
|
|
104
|
+
params.name = options.name;
|
|
105
|
+
}
|
|
106
|
+
if (options?.commit_message) {
|
|
107
|
+
params.commit_message = options.commit_message;
|
|
108
|
+
}
|
|
109
|
+
if (options?.metadata && Object.keys(options.metadata).length > 0) {
|
|
110
|
+
params.metadata = options.metadata;
|
|
111
|
+
}
|
|
112
|
+
const snapshot = await client.devboxes.snapshotDisk(devboxId, params);
|
|
100
113
|
return {
|
|
101
114
|
id: snapshot.id,
|
|
102
115
|
name: snapshot.name || undefined,
|
package/dist/utils/commands.js
CHANGED
|
@@ -32,6 +32,7 @@ export function createProgram() {
|
|
|
32
32
|
.option("--entrypoint <command>", "Entrypoint command to run")
|
|
33
33
|
.option("--launch-commands <commands...>", "Initialization commands to run on startup")
|
|
34
34
|
.option("--env-vars <vars...>", "Environment variables (format: KEY=value)")
|
|
35
|
+
.option("--secrets <secrets...>", "Secrets to inject as environment variables (format: ENV_VAR=SECRET_NAME)")
|
|
35
36
|
.option("--code-mounts <mounts...>", "Code mount configurations (JSON format)")
|
|
36
37
|
.option("--idle-time <seconds>", "Idle time in seconds before idle action")
|
|
37
38
|
.option("--idle-action <action>", "Action on idle (shutdown, suspend)")
|
|
@@ -136,6 +137,7 @@ export function createProgram() {
|
|
|
136
137
|
devbox
|
|
137
138
|
.command("tunnel <id> <ports>")
|
|
138
139
|
.description("Create a port-forwarding tunnel to a devbox")
|
|
140
|
+
.option("--open", "Open the tunnel URL in browser automatically")
|
|
139
141
|
.option("-o, --output [format]", "Output format: text|json|yaml (default: text)")
|
|
140
142
|
.action(async (id, ports, options) => {
|
|
141
143
|
const { createTunnel } = await import("../commands/devbox/tunnel.js");
|