@runloop/rl-cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/cli.js +73 -60
- package/dist/commands/auth.js +0 -1
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +215 -213
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/blueprint/preview.js +42 -38
- package/dist/commands/config.js +117 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +45 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +36 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-install.js +4 -3
- package/dist/commands/menu.js +24 -66
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +63 -39
- package/dist/components/Breadcrumb.js +10 -52
- package/dist/components/DevboxActionsMenu.js +182 -110
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +94 -0
- package/dist/components/MainMenu.js +36 -32
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +14 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server.js +65 -6
- package/dist/router/Router.js +68 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +105 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/CommandExecutor.js +53 -24
- package/dist/utils/client.js +0 -2
- package/dist/utils/config.js +20 -90
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +162 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +150 -59
- package/dist/utils/screen.js +23 -0
- package/dist/utils/ssh.js +3 -1
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +97 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +147 -13
- package/package.json +16 -13
|
@@ -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,94 @@
|
|
|
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 { exitAlternateScreenBuffer, enterAlternateScreenBuffer, } from "../utils/screen.js";
|
|
9
|
+
/**
|
|
10
|
+
* Releases terminal control from Ink so a subprocess can take over.
|
|
11
|
+
* This directly manipulates stdin to bypass Ink's input handling.
|
|
12
|
+
*/
|
|
13
|
+
function releaseTerminal() {
|
|
14
|
+
// Pause stdin to stop Ink from reading input
|
|
15
|
+
process.stdin.pause();
|
|
16
|
+
// Disable raw mode so the subprocess can control terminal echo and line buffering
|
|
17
|
+
// SSH needs to set its own terminal modes
|
|
18
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
19
|
+
process.stdin.setRawMode(false);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Restores terminal control to Ink after subprocess exits.
|
|
24
|
+
*/
|
|
25
|
+
function restoreTerminal() {
|
|
26
|
+
// Re-enable raw mode for Ink's input handling
|
|
27
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
28
|
+
process.stdin.setRawMode(true);
|
|
29
|
+
}
|
|
30
|
+
// Resume stdin so Ink can read input again
|
|
31
|
+
process.stdin.resume();
|
|
32
|
+
}
|
|
33
|
+
export const InteractiveSpawn = ({ command, args, onExit, onError, }) => {
|
|
34
|
+
const processRef = React.useRef(null);
|
|
35
|
+
const hasSpawnedRef = React.useRef(false);
|
|
36
|
+
// Use a stable string representation of args for dependency comparison
|
|
37
|
+
const argsKey = React.useMemo(() => JSON.stringify(args), [args]);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
// Only spawn once - prevent re-spawning if component re-renders
|
|
40
|
+
if (hasSpawnedRef.current) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
hasSpawnedRef.current = true;
|
|
44
|
+
// Exit alternate screen so subprocess gets a clean terminal
|
|
45
|
+
exitAlternateScreenBuffer();
|
|
46
|
+
// Release terminal from Ink's control
|
|
47
|
+
releaseTerminal();
|
|
48
|
+
// Small delay to ensure terminal state is fully released
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
// Spawn the process with inherited stdio for proper TTY allocation
|
|
51
|
+
const child = spawn(command, args, {
|
|
52
|
+
stdio: "inherit", // This allows the process to use the terminal directly
|
|
53
|
+
shell: false,
|
|
54
|
+
});
|
|
55
|
+
processRef.current = child;
|
|
56
|
+
// Handle process exit
|
|
57
|
+
child.on("exit", (code, _signal) => {
|
|
58
|
+
processRef.current = null;
|
|
59
|
+
hasSpawnedRef.current = false;
|
|
60
|
+
// Restore terminal control to Ink
|
|
61
|
+
restoreTerminal();
|
|
62
|
+
// Re-enter alternate screen after process exits
|
|
63
|
+
enterAlternateScreenBuffer();
|
|
64
|
+
if (onExit) {
|
|
65
|
+
onExit(code);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Handle spawn errors
|
|
69
|
+
child.on("error", (error) => {
|
|
70
|
+
processRef.current = null;
|
|
71
|
+
hasSpawnedRef.current = false;
|
|
72
|
+
// Restore terminal control to Ink
|
|
73
|
+
restoreTerminal();
|
|
74
|
+
// Re-enter alternate screen on error
|
|
75
|
+
enterAlternateScreenBuffer();
|
|
76
|
+
if (onError) {
|
|
77
|
+
onError(error);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}, 50);
|
|
81
|
+
// Cleanup function - kill the process if component unmounts
|
|
82
|
+
return () => {
|
|
83
|
+
if (processRef.current && !processRef.current.killed) {
|
|
84
|
+
processRef.current.kill("SIGTERM");
|
|
85
|
+
}
|
|
86
|
+
// Restore terminal state on cleanup
|
|
87
|
+
restoreTerminal();
|
|
88
|
+
hasSpawnedRef.current = false;
|
|
89
|
+
};
|
|
90
|
+
}, [command, argsKey, onExit, onError]);
|
|
91
|
+
// This component doesn't render anything - it just manages the subprocess
|
|
92
|
+
// The subprocess output goes directly to the terminal via stdio: "inherit"
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
@@ -6,34 +6,38 @@ import { Banner } from "./Banner.js";
|
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
7
|
import { VERSION } from "../cli.js";
|
|
8
8
|
import { colors } from "../utils/theme.js";
|
|
9
|
-
|
|
9
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
10
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
11
|
+
const menuItems = [
|
|
12
|
+
{
|
|
13
|
+
key: "devboxes",
|
|
14
|
+
label: "Devboxes",
|
|
15
|
+
description: "Manage cloud development environments",
|
|
16
|
+
icon: "◉",
|
|
17
|
+
color: colors.accent1,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
key: "blueprints",
|
|
21
|
+
label: "Blueprints",
|
|
22
|
+
description: "Create and manage devbox templates",
|
|
23
|
+
icon: "▣",
|
|
24
|
+
color: colors.accent2,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
key: "snapshots",
|
|
28
|
+
label: "Snapshots",
|
|
29
|
+
description: "Save and restore devbox states",
|
|
30
|
+
icon: "◈",
|
|
31
|
+
color: colors.accent3,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
export const MainMenu = ({ onSelect }) => {
|
|
10
35
|
const { exit } = useApp();
|
|
11
36
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
12
|
-
//
|
|
13
|
-
const terminalHeight =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
key: "devboxes",
|
|
17
|
-
label: "Devboxes",
|
|
18
|
-
description: "Manage cloud development environments",
|
|
19
|
-
icon: "◉",
|
|
20
|
-
color: colors.accent1,
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
key: "blueprints",
|
|
24
|
-
label: "Blueprints",
|
|
25
|
-
description: "Create and manage devbox templates",
|
|
26
|
-
icon: "▣",
|
|
27
|
-
color: colors.accent2,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: "snapshots",
|
|
31
|
-
label: "Snapshots",
|
|
32
|
-
description: "Save and restore devbox states",
|
|
33
|
-
icon: "◈",
|
|
34
|
-
color: colors.accent3,
|
|
35
|
-
},
|
|
36
|
-
], []);
|
|
37
|
+
// Use centralized viewport hook for consistent layout
|
|
38
|
+
const { terminalHeight } = useViewportHeight({ overhead: 0 });
|
|
39
|
+
// Handle Ctrl+C to exit
|
|
40
|
+
useExitOnCtrlC();
|
|
37
41
|
useInput((input, key) => {
|
|
38
42
|
if (key.upArrow && selectedIndex > 0) {
|
|
39
43
|
setSelectedIndex(selectedIndex - 1);
|
|
@@ -58,15 +62,15 @@ export const MainMenu = React.memo(({ onSelect }) => {
|
|
|
58
62
|
}
|
|
59
63
|
});
|
|
60
64
|
// Use compact layout if terminal height is less than 20 lines (memoized)
|
|
61
|
-
const useCompactLayout =
|
|
65
|
+
const useCompactLayout = terminalHeight < 20;
|
|
62
66
|
if (useCompactLayout) {
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
67
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { paddingX: 2, marginBottom: 1, children: [_jsx(Text, { color: colors.primary, bold: true, children: "RUNLOOP.ai" }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Cloud development environments \u2022 v", VERSION] })] }), _jsx(Box, { flexDirection: "column", paddingX: 2, children: menuItems.map((item, index) => {
|
|
64
68
|
const isSelected = index === selectedIndex;
|
|
65
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));
|
|
66
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"] }) })] }));
|
|
67
71
|
}
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
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) => {
|
|
69
73
|
const isSelected = index === selectedIndex;
|
|
70
|
-
return (_jsxs(Box, { paddingX: 2, paddingY: 0, borderStyle:
|
|
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));
|
|
71
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"] }) }) })] }));
|
|
72
|
-
}
|
|
76
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { Badge } from "@inkjs/ui";
|
|
4
3
|
import figures from "figures";
|
|
5
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
+
const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: ": " }), _jsx(Text, { color: color, children: value })] }));
|
|
6
6
|
// Generate color for each key based on hash
|
|
7
7
|
const getColorForKey = (key, index) => {
|
|
8
8
|
const colorList = [
|
|
@@ -20,10 +20,10 @@ export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = fal
|
|
|
20
20
|
if (entries.length === 0) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
-
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.
|
|
23
|
+
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 1, children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
|
|
24
24
|
const color = getColorForKey(key, index);
|
|
25
25
|
const isSelected = selectedKey === key;
|
|
26
|
-
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })),
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "row", alignItems: "center", children: [isSelected && (_jsxs(Text, { color: colors.primary, bold: true, children: [figures.pointer, " "] })), renderKeyValueBadge(key, value, isSelected ? colors.primary : color)] }, key));
|
|
27
27
|
})] }));
|
|
28
28
|
if (showBorder) {
|
|
29
29
|
return (_jsx(Box, { borderStyle: "round", borderColor: colors.accent3, paddingX: 2, paddingY: 1, flexDirection: "column", children: content }));
|
|
@@ -6,7 +6,7 @@ import { colors } from "../utils/theme.js";
|
|
|
6
6
|
* Reusable operations menu component for detail pages
|
|
7
7
|
* Displays a list of available operations with keyboard navigation
|
|
8
8
|
*/
|
|
9
|
-
export const OperationsMenu = ({ operations, selectedIndex, onNavigate, onSelect, onBack, additionalActions = [], }) => {
|
|
9
|
+
export const OperationsMenu = ({ operations, selectedIndex, onNavigate: _onNavigate, onSelect: _onSelect, onBack: _onBack, additionalActions = [], }) => {
|
|
10
10
|
return (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Operations"] }), _jsx(Box, { flexDirection: "column", children: operations.map((op, index) => {
|
|
11
11
|
const isSelected = index === selectedIndex;
|
|
12
12
|
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] })] }, op.key));
|
|
@@ -8,8 +8,8 @@ import { ActionsPopup } from "./ActionsPopup.js";
|
|
|
8
8
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
9
9
|
export const ResourceActionsMenu = (props) => {
|
|
10
10
|
if (props.resourceType === "devbox") {
|
|
11
|
-
const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu,
|
|
12
|
-
return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu
|
|
11
|
+
const { resource, onBack, breadcrumbItems, initialOperation, initialOperationIndex, skipOperationsMenu, } = props;
|
|
12
|
+
return (_jsx(DevboxActionsMenu, { devbox: resource, onBack: onBack, breadcrumbItems: breadcrumbItems, initialOperation: initialOperation, initialOperationIndex: initialOperationIndex, skipOperationsMenu: skipOperationsMenu }));
|
|
13
13
|
}
|
|
14
14
|
// Blueprint generic actions menu
|
|
15
15
|
const { resource, onBack, breadcrumbItems = [
|
|
@@ -101,10 +101,10 @@ export const ResourceActionsMenu = (props) => {
|
|
|
101
101
|
// Screens
|
|
102
102
|
if (operationResult || operationError) {
|
|
103
103
|
const label = operations.find((o) => o.key === executingOperation)?.label;
|
|
104
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: operationError ? colors.error : colors.success, children: operationError ? `${label} failed` : `${label} completed` }), !!operationResult && (_jsx(Text, { color: colors.textDim, dimColor: true, children: operationResult })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointerSmall, " Press [Enter] to go back"] })] })] }));
|
|
105
105
|
}
|
|
106
106
|
if (executingOperation && selectedOp?.needsInput) {
|
|
107
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems
|
|
107
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, children: [selectedOp.inputPrompt || "Input:", " "] }), _jsxs(Text, { children: [" ", operationInput] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" })] })] }));
|
|
108
108
|
}
|
|
109
109
|
// Operations menu
|
|
110
110
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: resource, operations: operations.map((op) => ({
|