@runloop/rl-cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text, useInput
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Header } from "./Header.js";
|
|
6
6
|
import { StatusBadge } from "./StatusBadge.js";
|
|
@@ -9,6 +9,9 @@ import { Breadcrumb } from "./Breadcrumb.js";
|
|
|
9
9
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
10
10
|
import { getDevboxUrl } from "../utils/url.js";
|
|
11
11
|
import { colors } from "../utils/theme.js";
|
|
12
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
13
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
|
+
import { getDevbox } from "../services/devboxService.js";
|
|
12
15
|
// Format time ago in a succinct way
|
|
13
16
|
const formatTimeAgo = (timestamp) => {
|
|
14
17
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
@@ -29,13 +32,53 @@ const formatTimeAgo = (timestamp) => {
|
|
|
29
32
|
const years = Math.floor(months / 12);
|
|
30
33
|
return `${years}y ago`;
|
|
31
34
|
};
|
|
32
|
-
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack,
|
|
33
|
-
const
|
|
35
|
+
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
36
|
+
const isMounted = React.useRef(true);
|
|
37
|
+
// Track mounted state
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
isMounted.current = true;
|
|
40
|
+
return () => {
|
|
41
|
+
isMounted.current = false;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
// Local state for devbox data (updated by polling)
|
|
45
|
+
const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
|
|
34
46
|
const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
|
|
35
47
|
const [detailScroll, setDetailScroll] = React.useState(0);
|
|
36
48
|
const [showActions, setShowActions] = React.useState(false);
|
|
37
49
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
38
|
-
|
|
50
|
+
// Background polling for devbox details
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
// Skip polling if showing actions, detailed info, or not mounted
|
|
53
|
+
if (showActions || showDetailedInfo)
|
|
54
|
+
return;
|
|
55
|
+
const interval = setInterval(async () => {
|
|
56
|
+
// Only poll when not in actions/detail mode and component is mounted
|
|
57
|
+
if (!showActions && !showDetailedInfo && isMounted.current) {
|
|
58
|
+
try {
|
|
59
|
+
const updatedDevbox = await getDevbox(initialDevbox.id);
|
|
60
|
+
// Only update if still mounted
|
|
61
|
+
if (isMounted.current) {
|
|
62
|
+
setCurrentDevbox(updatedDevbox);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Silently ignore polling errors to avoid disrupting user experience
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, 3000); // Poll every 3 seconds
|
|
70
|
+
return () => clearInterval(interval);
|
|
71
|
+
}, [initialDevbox.id, showActions, showDetailedInfo]);
|
|
72
|
+
// Calculate viewport for detailed info view:
|
|
73
|
+
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
74
|
+
// - Header (title + underline + marginBottom): 3 lines
|
|
75
|
+
// - Status box (content + marginBottom): 2 lines
|
|
76
|
+
// - Content box (marginTop + border + paddingY top/bottom + border + marginBottom): 6 lines
|
|
77
|
+
// - Help bar (marginTop + content): 2 lines
|
|
78
|
+
// - Safety buffer: 1 line
|
|
79
|
+
// Total: 18 lines
|
|
80
|
+
const detailViewport = useViewportHeight({ overhead: 18, minHeight: 10 });
|
|
81
|
+
const selectedDevbox = currentDevbox;
|
|
39
82
|
const allOperations = [
|
|
40
83
|
{
|
|
41
84
|
key: "logs",
|
|
@@ -123,14 +166,18 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
123
166
|
return op.key === "logs" || op.key === "delete";
|
|
124
167
|
})
|
|
125
168
|
: allOperations;
|
|
126
|
-
|
|
127
|
-
const formattedCreateTime = React.useMemo(() => selectedDevbox.create_time_ms
|
|
169
|
+
const formattedCreateTime = selectedDevbox.create_time_ms
|
|
128
170
|
? new Date(selectedDevbox.create_time_ms).toLocaleString()
|
|
129
|
-
: ""
|
|
130
|
-
const createTimeAgo =
|
|
171
|
+
: "";
|
|
172
|
+
const createTimeAgo = selectedDevbox.create_time_ms
|
|
131
173
|
? formatTimeAgo(selectedDevbox.create_time_ms)
|
|
132
|
-
: ""
|
|
174
|
+
: "";
|
|
175
|
+
// Handle Ctrl+C to exit
|
|
176
|
+
useExitOnCtrlC();
|
|
133
177
|
useInput((input, key) => {
|
|
178
|
+
// Don't process input if unmounting
|
|
179
|
+
if (!isMounted.current)
|
|
180
|
+
return;
|
|
134
181
|
// Skip input handling when in actions view
|
|
135
182
|
if (showActions) {
|
|
136
183
|
return;
|
|
@@ -161,7 +208,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
161
208
|
}
|
|
162
209
|
// Main view input handling
|
|
163
210
|
if (input === "q" || key.escape) {
|
|
164
|
-
console.clear();
|
|
165
211
|
onBack();
|
|
166
212
|
}
|
|
167
213
|
else if (input === "i") {
|
|
@@ -175,7 +221,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
175
221
|
setSelectedOperation(selectedOperation + 1);
|
|
176
222
|
}
|
|
177
223
|
else if (key.return || input === "a") {
|
|
178
|
-
console.clear();
|
|
179
224
|
setShowActions(true);
|
|
180
225
|
}
|
|
181
226
|
else if (input) {
|
|
@@ -183,7 +228,6 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
183
228
|
const matchedOpIndex = operations.findIndex((op) => op.shortcut === input);
|
|
184
229
|
if (matchedOpIndex !== -1) {
|
|
185
230
|
setSelectedOperation(matchedOpIndex);
|
|
186
|
-
console.clear();
|
|
187
231
|
setShowActions(true);
|
|
188
232
|
}
|
|
189
233
|
}
|
|
@@ -217,10 +261,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
217
261
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
218
262
|
// Core Information
|
|
219
263
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Devbox Details" }, "core-title"));
|
|
220
|
-
lines.push(_jsxs(Text, {
|
|
264
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.id] }, "core-id"));
|
|
221
265
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", selectedDevbox.name || "(none)"] }, "core-name"));
|
|
222
266
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(selectedDevbox.status)] }, "core-status"));
|
|
223
|
-
|
|
267
|
+
if (selectedDevbox.create_time_ms) {
|
|
268
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(selectedDevbox.create_time_ms).toLocaleString()] }, "core-created"));
|
|
269
|
+
}
|
|
224
270
|
if (selectedDevbox.end_time_ms) {
|
|
225
271
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(selectedDevbox.end_time_ms).toLocaleString()] }, "core-ended"));
|
|
226
272
|
}
|
|
@@ -263,9 +309,14 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
263
309
|
}
|
|
264
310
|
if (lp.launch_commands && lp.launch_commands.length > 0) {
|
|
265
311
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "launch-launch-cmds"));
|
|
266
|
-
lp.launch_commands.forEach((cmd, idx) => {
|
|
267
|
-
|
|
268
|
-
}
|
|
312
|
+
// lp.launch_commands.forEach((cmd: string, idx: number) => {
|
|
313
|
+
// lines.push(
|
|
314
|
+
// <Text key={`launch-cmd-${idx}`} dimColor>
|
|
315
|
+
// {" "}
|
|
316
|
+
// {figures.pointer} {cmd}
|
|
317
|
+
// </Text>,
|
|
318
|
+
// );
|
|
319
|
+
// });
|
|
269
320
|
}
|
|
270
321
|
if (lp.required_services && lp.required_services.length > 0) {
|
|
271
322
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Required Services: ", lp.required_services.join(", ")] }, "launch-services"));
|
|
@@ -285,10 +336,10 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
285
336
|
if (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) {
|
|
286
337
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
|
|
287
338
|
if (selectedDevbox.blueprint_id) {
|
|
288
|
-
lines.push(_jsxs(Text, {
|
|
339
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.blueprint_id] }, "source-bp"));
|
|
289
340
|
}
|
|
290
341
|
if (selectedDevbox.snapshot_id) {
|
|
291
|
-
lines.push(_jsxs(Text, {
|
|
342
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", selectedDevbox.snapshot_id] }, "source-snap"));
|
|
292
343
|
}
|
|
293
344
|
lines.push(_jsx(Text, { children: " " }, "source-space"));
|
|
294
345
|
}
|
|
@@ -297,7 +348,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
297
348
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Initiator" }, "init-title"));
|
|
298
349
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", selectedDevbox.initiator_type] }, "init-type"));
|
|
299
350
|
if (selectedDevbox.initiator_id) {
|
|
300
|
-
lines.push(_jsxs(Text, {
|
|
351
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", selectedDevbox.initiator_id] }, "init-id"));
|
|
301
352
|
}
|
|
302
353
|
lines.push(_jsx(Text, { children: " " }, "init-space"));
|
|
303
354
|
}
|
|
@@ -348,13 +399,12 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
348
399
|
}, breadcrumbItems: [
|
|
349
400
|
{ label: "Devboxes" },
|
|
350
401
|
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
351
|
-
], initialOperation: selectedOp?.key, skipOperationsMenu: true
|
|
402
|
+
], initialOperation: selectedOp?.key, skipOperationsMenu: true }));
|
|
352
403
|
}
|
|
353
404
|
// Detailed info mode - full screen
|
|
354
405
|
if (showDetailedInfo) {
|
|
355
406
|
const detailLines = buildDetailLines();
|
|
356
|
-
const
|
|
357
|
-
const viewportHeight = Math.max(10, terminalHeight - 12); // Reserve space for header/footer
|
|
407
|
+
const viewportHeight = detailViewport.viewportHeight;
|
|
358
408
|
const maxScroll = Math.max(0, detailLines.length - viewportHeight);
|
|
359
409
|
const actualScroll = Math.min(detailScroll, maxScroll);
|
|
360
410
|
const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
@@ -364,7 +414,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
364
414
|
{ label: "Devboxes" },
|
|
365
415
|
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
366
416
|
{ label: "Full Details", active: true },
|
|
367
|
-
] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.
|
|
417
|
+
] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.id })] }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: colors.border, paddingX: 2, paddingY: 1, children: _jsx(Box, { flexDirection: "column", children: visibleLines }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of", " ", detailLines.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 [q or esc] Back to Details"] })] })] }));
|
|
368
418
|
}
|
|
369
419
|
// Main detail view
|
|
370
420
|
const lp = selectedDevbox.launch_parameters;
|
|
@@ -374,7 +424,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
374
424
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
375
425
|
{ label: "Devboxes" },
|
|
376
426
|
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
377
|
-
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: colors.
|
|
427
|
+
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id }), _jsx(Text, { children: " " }), _jsx(StatusBadge, { status: selectedDevbox.status }), _jsxs(Text, { color: colors.idColor, children: [" \u2022 ", selectedDevbox.id] })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: formattedCreateTime }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", createTimeAgo, ")"] })] }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.success, dimColor: true, children: ["Uptime:", " ", uptime < 60
|
|
378
428
|
? `${uptime}m`
|
|
379
429
|
: `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }), lp?.keep_alive_time_seconds && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Keep-alive: ", Math.floor(lp.keep_alive_time_seconds / 60), "m"] }))] }))] }), _jsxs(Box, { flexDirection: "row", gap: 1, marginBottom: 1, children: [(lp?.resource_size_request ||
|
|
380
430
|
lp?.custom_cpu_cores ||
|
|
@@ -382,9 +432,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, onSSHRequest,
|
|
|
382
432
|
lp?.custom_disk_size ||
|
|
383
433
|
lp?.architecture) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Resources"] }), _jsxs(Text, { dimColor: true, children: [lp?.resource_size_request && `${lp.resource_size_request}`, lp?.architecture && ` • ${lp.architecture}`, lp?.custom_cpu_cores && ` • ${lp.custom_cpu_cores}VCPU`, lp?.custom_gb_memory && ` • ${lp.custom_gb_memory}GB RAM`, lp?.custom_disk_size && ` • ${lp.custom_disk_size}GB DISC`] })] })), hasCapabilities && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.info, bold: true, children: [figures.tick, " Capabilities"] }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
|
|
384
434
|
.filter((c) => c !== "unknown")
|
|
385
|
-
.join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), _jsxs(Text, { dimColor: true, children:
|
|
386
|
-
`BP: ${selectedDevbox.blueprint_id}`, selectedDevbox.snapshot_id &&
|
|
387
|
-
`Snap: ${selectedDevbox.snapshot_id}`] })] }))] }), selectedDevbox.metadata &&
|
|
435
|
+
.join(", ") })] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.circleFilled, " Source"] }), selectedDevbox.blueprint_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "BP: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.blueprint_id })] })), selectedDevbox.snapshot_id && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Snap: " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.snapshot_id })] }))] }))] }), selectedDevbox.metadata &&
|
|
388
436
|
Object.keys(selectedDevbox.metadata).length > 0 && (_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsx(MetadataDisplay, { metadata: selectedDevbox.metadata, showBorder: false }) })), selectedDevbox.failure_reason && (_jsxs(Box, { marginBottom: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " "] }), _jsx(Text, { color: colors.error, dimColor: true, children: selectedDevbox.failure_reason })] })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
389
437
|
const isSelected = index === selectedOperation;
|
|
390
438
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* ErrorBoundary to catch and handle React errors gracefully
|
|
7
|
+
* Particularly useful for catching Yoga WASM layout errors
|
|
8
|
+
*/
|
|
9
|
+
export class ErrorBoundary extends React.Component {
|
|
10
|
+
constructor(props) {
|
|
11
|
+
super(props);
|
|
12
|
+
this.state = { hasError: false };
|
|
13
|
+
}
|
|
14
|
+
static getDerivedStateFromError(error) {
|
|
15
|
+
return { hasError: true, error };
|
|
16
|
+
}
|
|
17
|
+
componentDidCatch(error, errorInfo) {
|
|
18
|
+
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
|
19
|
+
}
|
|
20
|
+
render() {
|
|
21
|
+
if (this.state.hasError) {
|
|
22
|
+
if (this.props.fallback) {
|
|
23
|
+
return this.props.fallback;
|
|
24
|
+
}
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: colors.error, bold: true, children: "\u26A0\uFE0F Rendering Error" }), _jsx(Text, { color: colors.textDim, children: this.state.error?.message || "An unexpected error occurred" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press Ctrl+C to exit" }) })] }));
|
|
26
|
+
}
|
|
27
|
+
return this.props.children;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -2,6 +2,14 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
export const ErrorMessage = ({ message, error
|
|
6
|
-
|
|
5
|
+
export const ErrorMessage = ({ message, error }) => {
|
|
6
|
+
// Limit message length to prevent Yoga layout engine errors
|
|
7
|
+
const MAX_LENGTH = 500;
|
|
8
|
+
const truncatedMessage = message.length > MAX_LENGTH
|
|
9
|
+
? message.substring(0, MAX_LENGTH) + "..."
|
|
10
|
+
: message;
|
|
11
|
+
const truncatedError = error?.message && error.message.length > MAX_LENGTH
|
|
12
|
+
? error.message.substring(0, MAX_LENGTH) + "..."
|
|
13
|
+
: error?.message;
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " ", truncatedMessage] }) }), truncatedError && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: truncatedError }) }))] }));
|
|
7
15
|
};
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
2
|
import { Box, Text } from "ink";
|
|
4
3
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
export const Header =
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export const Header = ({ title, subtitle }) => {
|
|
5
|
+
// Limit lengths to prevent Yoga layout engine errors
|
|
6
|
+
const MAX_TITLE_LENGTH = 100;
|
|
7
|
+
const MAX_SUBTITLE_LENGTH = 150;
|
|
8
|
+
const truncatedTitle = title.length > MAX_TITLE_LENGTH
|
|
9
|
+
? title.substring(0, MAX_TITLE_LENGTH) + "..."
|
|
10
|
+
: title;
|
|
11
|
+
const truncatedSubtitle = subtitle && subtitle.length > MAX_SUBTITLE_LENGTH
|
|
12
|
+
? subtitle.substring(0, MAX_SUBTITLE_LENGTH) + "..."
|
|
13
|
+
: subtitle;
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { bold: true, color: colors.accent3, children: ["\u258C", truncatedTitle] }), truncatedSubtitle && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: truncatedSubtitle })] }))] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: colors.accent3, children: "─".repeat(Math.max(0, Math.floor(Math.min(truncatedTitle.length + 1, MAX_TITLE_LENGTH + 1)))) }) })] }));
|
|
15
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InteractiveSpawn - Custom component for running interactive subprocesses
|
|
3
|
+
* Based on Ink's subprocess-output example pattern
|
|
4
|
+
* Handles proper TTY allocation for interactive commands like SSH
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { showCursor, clearScreen, enterAlternateScreenBuffer, } from "../utils/screen.js";
|
|
9
|
+
import { processUtils } from "../utils/processUtils.js";
|
|
10
|
+
/**
|
|
11
|
+
* Releases terminal control from Ink so a subprocess can take over.
|
|
12
|
+
* This directly manipulates stdin to bypass Ink's input handling.
|
|
13
|
+
*/
|
|
14
|
+
function releaseTerminal() {
|
|
15
|
+
// Pause stdin to stop Ink from reading input
|
|
16
|
+
process.stdin.pause();
|
|
17
|
+
// Disable raw mode so the subprocess can control terminal echo and line buffering
|
|
18
|
+
// SSH needs to set its own terminal modes
|
|
19
|
+
if (processUtils.stdin.isTTY && processUtils.stdin.setRawMode) {
|
|
20
|
+
processUtils.stdin.setRawMode(false);
|
|
21
|
+
}
|
|
22
|
+
// Reset terminal attributes (SGR reset) - clears any colors/styles Ink may have set
|
|
23
|
+
if (processUtils.stdout.isTTY) {
|
|
24
|
+
processUtils.stdout.write("\x1b[0m");
|
|
25
|
+
}
|
|
26
|
+
// Show cursor - Ink may have hidden it, and subprocesses expect it to be visible
|
|
27
|
+
showCursor();
|
|
28
|
+
// Flush stdout to ensure all pending writes are complete before handoff
|
|
29
|
+
if (processUtils.stdout.isTTY) {
|
|
30
|
+
processUtils.stdout.write("");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Restores terminal control to Ink after subprocess exits.
|
|
35
|
+
*/
|
|
36
|
+
function restoreTerminal() {
|
|
37
|
+
// Clear the screen to remove subprocess output before Ink renders
|
|
38
|
+
clearScreen();
|
|
39
|
+
// Re-enter alternate screen buffer for Ink's fullscreen UI
|
|
40
|
+
enterAlternateScreenBuffer();
|
|
41
|
+
// Re-enable raw mode for Ink's input handling
|
|
42
|
+
if (processUtils.stdin.isTTY && processUtils.stdin.setRawMode) {
|
|
43
|
+
processUtils.stdin.setRawMode(true);
|
|
44
|
+
}
|
|
45
|
+
// Resume stdin so Ink can read input again
|
|
46
|
+
process.stdin.resume();
|
|
47
|
+
}
|
|
48
|
+
export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
|
|
49
|
+
const processRef = React.useRef(null);
|
|
50
|
+
const hasSpawnedRef = React.useRef(false);
|
|
51
|
+
// Use a stable string representation of args for dependency comparison
|
|
52
|
+
const argsKey = React.useMemo(() => JSON.stringify(args), [args]);
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
// Only spawn once - prevent re-spawning if component re-renders
|
|
55
|
+
if (hasSpawnedRef.current) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
hasSpawnedRef.current = true;
|
|
59
|
+
// Release terminal from Ink's control
|
|
60
|
+
releaseTerminal();
|
|
61
|
+
// Use setImmediate to ensure terminal state is released without noticeable delay
|
|
62
|
+
// This is faster than setTimeout and ensures the event loop has processed the release
|
|
63
|
+
setImmediate(() => {
|
|
64
|
+
// Spawn the process with inherited stdio for proper TTY allocation
|
|
65
|
+
const child = spawn(command, args, {
|
|
66
|
+
stdio: "inherit", // This allows the process to use the terminal directly
|
|
67
|
+
shell: false,
|
|
68
|
+
});
|
|
69
|
+
processRef.current = child;
|
|
70
|
+
// Handle process exit
|
|
71
|
+
child.on("exit", (code, _signal) => {
|
|
72
|
+
processRef.current = null;
|
|
73
|
+
hasSpawnedRef.current = false;
|
|
74
|
+
// Restore terminal control to Ink
|
|
75
|
+
restoreTerminal();
|
|
76
|
+
if (onExit) {
|
|
77
|
+
onExit(code);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
// Handle spawn errors
|
|
81
|
+
child.on("error", (error) => {
|
|
82
|
+
processRef.current = null;
|
|
83
|
+
hasSpawnedRef.current = false;
|
|
84
|
+
// Restore terminal control to Ink
|
|
85
|
+
restoreTerminal();
|
|
86
|
+
if (onError) {
|
|
87
|
+
onError(error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
// Cleanup function - kill the process if component unmounts
|
|
92
|
+
return () => {
|
|
93
|
+
if (processRef.current && !processRef.current.killed) {
|
|
94
|
+
processRef.current.kill("SIGTERM");
|
|
95
|
+
}
|
|
96
|
+
// Restore terminal state on cleanup
|
|
97
|
+
restoreTerminal();
|
|
98
|
+
hasSpawnedRef.current = false;
|
|
99
|
+
};
|
|
100
|
+
}, [command, argsKey, onExit, onError]);
|
|
101
|
+
// This component doesn't render anything - it just manages the subprocess
|
|
102
|
+
// The subprocess output goes directly to the terminal via stdio: "inherit"
|
|
103
|
+
return null;
|
|
104
|
+
};
|
|
@@ -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
|
+
};
|