@runloop/rl-cli 1.2.0 → 1.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 +28 -8
- package/dist/commands/blueprint/list.js +97 -28
- package/dist/commands/blueprint/prune.js +7 -19
- package/dist/commands/devbox/create.js +3 -0
- package/dist/commands/devbox/list.js +44 -65
- package/dist/commands/menu.js +2 -1
- package/dist/commands/network-policy/create.js +27 -0
- package/dist/commands/network-policy/delete.js +21 -0
- package/dist/commands/network-policy/get.js +15 -0
- package/dist/commands/network-policy/list.js +494 -0
- package/dist/commands/object/list.js +516 -24
- package/dist/commands/snapshot/list.js +90 -29
- package/dist/components/Banner.js +109 -8
- package/dist/components/ConfirmationPrompt.js +45 -0
- package/dist/components/DevboxActionsMenu.js +42 -6
- package/dist/components/DevboxCard.js +1 -1
- package/dist/components/DevboxCreatePage.js +95 -81
- package/dist/components/DevboxDetailPage.js +218 -272
- package/dist/components/LogsViewer.js +8 -1
- package/dist/components/MainMenu.js +35 -4
- package/dist/components/NavigationTips.js +24 -0
- package/dist/components/NetworkPolicyCreatePage.js +264 -0
- package/dist/components/OperationsMenu.js +9 -1
- package/dist/components/ResourceActionsMenu.js +5 -1
- package/dist/components/ResourceDetailPage.js +204 -0
- package/dist/components/ResourceListView.js +19 -2
- package/dist/components/StatusBadge.js +2 -2
- package/dist/components/Table.js +6 -8
- package/dist/components/form/FormActionButton.js +7 -0
- package/dist/components/form/FormField.js +7 -0
- package/dist/components/form/FormListManager.js +112 -0
- package/dist/components/form/FormSelect.js +34 -0
- package/dist/components/form/FormTextInput.js +8 -0
- package/dist/components/form/index.js +8 -0
- package/dist/hooks/useViewportHeight.js +38 -20
- package/dist/router/Router.js +23 -1
- package/dist/screens/BlueprintDetailScreen.js +337 -0
- package/dist/screens/MenuScreen.js +6 -0
- package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
- package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
- package/dist/screens/NetworkPolicyListScreen.js +7 -0
- package/dist/screens/ObjectDetailScreen.js +377 -0
- package/dist/screens/ObjectListScreen.js +7 -0
- package/dist/screens/SnapshotDetailScreen.js +208 -0
- package/dist/services/blueprintService.js +30 -11
- package/dist/services/networkPolicyService.js +108 -0
- package/dist/services/objectService.js +101 -0
- package/dist/services/snapshotService.js +39 -3
- package/dist/store/blueprintStore.js +4 -10
- package/dist/store/index.js +1 -0
- package/dist/store/networkPolicyStore.js +83 -0
- package/dist/store/objectStore.js +92 -0
- package/dist/store/snapshotStore.js +4 -8
- package/dist/utils/commands.js +47 -0
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { SpinnerComponent } from "../../components/Spinner.js";
|
|
|
8
8
|
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
9
9
|
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
10
10
|
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
11
|
+
import { NavigationTips } from "../../components/NavigationTips.js";
|
|
11
12
|
import { Table, createTextColumn } from "../../components/Table.js";
|
|
12
13
|
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
13
14
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
@@ -18,6 +19,7 @@ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
|
18
19
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
19
20
|
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
20
21
|
import { useNavigation } from "../../store/navigationStore.js";
|
|
22
|
+
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
21
23
|
const DEFAULT_PAGE_SIZE = 10;
|
|
22
24
|
const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
23
25
|
const { exit: inkExit } = useApp();
|
|
@@ -32,6 +34,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
32
34
|
const [operationError, setOperationError] = React.useState(null);
|
|
33
35
|
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
34
36
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
37
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
35
38
|
// Calculate overhead for viewport height
|
|
36
39
|
const overhead = 13;
|
|
37
40
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
@@ -40,11 +43,16 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
40
43
|
});
|
|
41
44
|
const PAGE_SIZE = viewportHeight;
|
|
42
45
|
// All width constants
|
|
46
|
+
const fixedWidth = 6; // border + padding
|
|
43
47
|
const idWidth = 25;
|
|
44
|
-
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
|
|
45
48
|
const devboxWidth = 15;
|
|
46
49
|
const timeWidth = 20;
|
|
47
50
|
const showDevboxIdColumn = terminalWidth >= 100 && !devboxId;
|
|
51
|
+
// Name width uses remaining space after fixed columns
|
|
52
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
53
|
+
const optionalWidth = showDevboxIdColumn ? devboxWidth : 0;
|
|
54
|
+
const remainingWidth = terminalWidth - baseWidth - optionalWidth;
|
|
55
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
48
56
|
// Fetch function for pagination hook
|
|
49
57
|
const fetchPage = React.useCallback(async (params) => {
|
|
50
58
|
const client = getClient();
|
|
@@ -81,16 +89,25 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
81
89
|
return result;
|
|
82
90
|
}, [devboxId]);
|
|
83
91
|
// Use the shared pagination hook
|
|
84
|
-
const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
92
|
+
const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
85
93
|
fetchPage,
|
|
86
94
|
pageSize: PAGE_SIZE,
|
|
87
95
|
getItemId: (snapshot) => snapshot.id,
|
|
88
96
|
pollInterval: 2000,
|
|
89
|
-
pollingEnabled: !showPopup &&
|
|
97
|
+
pollingEnabled: !showPopup &&
|
|
98
|
+
!executingOperation &&
|
|
99
|
+
!showCreateDevbox &&
|
|
100
|
+
!showDeleteConfirm,
|
|
90
101
|
deps: [devboxId, PAGE_SIZE],
|
|
91
102
|
});
|
|
92
103
|
// Operations for snapshots
|
|
93
104
|
const operations = React.useMemo(() => [
|
|
105
|
+
{
|
|
106
|
+
key: "view_details",
|
|
107
|
+
label: "View Details",
|
|
108
|
+
color: colors.primary,
|
|
109
|
+
icon: figures.pointer,
|
|
110
|
+
},
|
|
94
111
|
{
|
|
95
112
|
key: "create_devbox",
|
|
96
113
|
label: "Create Devbox from Snapshot",
|
|
@@ -142,14 +159,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
142
159
|
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
143
160
|
const startIndex = currentPage * PAGE_SIZE;
|
|
144
161
|
const endIndex = startIndex + snapshots.length;
|
|
145
|
-
const executeOperation = async () => {
|
|
162
|
+
const executeOperation = async (snapshot, operationKey) => {
|
|
146
163
|
const client = getClient();
|
|
147
|
-
const snapshot = selectedSnapshot;
|
|
148
164
|
if (!snapshot)
|
|
149
165
|
return;
|
|
150
166
|
try {
|
|
151
167
|
setOperationLoading(true);
|
|
152
|
-
switch (
|
|
168
|
+
switch (operationKey) {
|
|
153
169
|
case "delete":
|
|
154
170
|
await client.devboxes.deleteDiskSnapshot(snapshot.id);
|
|
155
171
|
setOperationResult(`Snapshot ${snapshot.id} deleted successfully`);
|
|
@@ -167,10 +183,16 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
167
183
|
// Handle operation result display
|
|
168
184
|
if (operationResult || operationError) {
|
|
169
185
|
if (input === "q" || key.escape || key.return) {
|
|
186
|
+
const wasDelete = executingOperation === "delete";
|
|
187
|
+
const hadError = operationError !== null;
|
|
170
188
|
setOperationResult(null);
|
|
171
189
|
setOperationError(null);
|
|
172
190
|
setExecutingOperation(null);
|
|
173
191
|
setSelectedSnapshot(null);
|
|
192
|
+
// Refresh the list after delete to show updated data
|
|
193
|
+
if (wasDelete && !hadError) {
|
|
194
|
+
setTimeout(() => refresh(), 0);
|
|
195
|
+
}
|
|
174
196
|
}
|
|
175
197
|
return;
|
|
176
198
|
}
|
|
@@ -189,17 +211,34 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
189
211
|
else if (key.return) {
|
|
190
212
|
setShowPopup(false);
|
|
191
213
|
const operationKey = operations[selectedOperation].key;
|
|
192
|
-
if (operationKey === "
|
|
214
|
+
if (operationKey === "view_details") {
|
|
215
|
+
navigate("snapshot-detail", {
|
|
216
|
+
snapshotId: selectedSnapshotItem.id,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else if (operationKey === "create_devbox") {
|
|
193
220
|
setSelectedSnapshot(selectedSnapshotItem);
|
|
194
221
|
setShowCreateDevbox(true);
|
|
195
222
|
}
|
|
223
|
+
else if (operationKey === "delete") {
|
|
224
|
+
// Show delete confirmation
|
|
225
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
226
|
+
setShowDeleteConfirm(true);
|
|
227
|
+
}
|
|
196
228
|
else {
|
|
197
229
|
setSelectedSnapshot(selectedSnapshotItem);
|
|
198
230
|
setExecutingOperation(operationKey);
|
|
199
|
-
// Execute immediately
|
|
200
|
-
|
|
231
|
+
// Execute immediately with values passed directly
|
|
232
|
+
executeOperation(selectedSnapshotItem, operationKey);
|
|
201
233
|
}
|
|
202
234
|
}
|
|
235
|
+
else if (input === "v" && selectedSnapshotItem) {
|
|
236
|
+
// View details hotkey
|
|
237
|
+
setShowPopup(false);
|
|
238
|
+
navigate("snapshot-detail", {
|
|
239
|
+
snapshotId: selectedSnapshotItem.id,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
203
242
|
else if (key.escape || input === "q") {
|
|
204
243
|
setShowPopup(false);
|
|
205
244
|
setSelectedOperation(0);
|
|
@@ -211,11 +250,10 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
211
250
|
setShowCreateDevbox(true);
|
|
212
251
|
}
|
|
213
252
|
else if (input === "d") {
|
|
214
|
-
// Delete hotkey
|
|
253
|
+
// Delete hotkey - show confirmation
|
|
215
254
|
setShowPopup(false);
|
|
216
255
|
setSelectedSnapshot(selectedSnapshotItem);
|
|
217
|
-
|
|
218
|
-
setTimeout(() => executeOperation(), 0);
|
|
256
|
+
setShowDeleteConfirm(true);
|
|
219
257
|
}
|
|
220
258
|
return;
|
|
221
259
|
}
|
|
@@ -241,6 +279,12 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
241
279
|
prevPage();
|
|
242
280
|
setSelectedIndex(0);
|
|
243
281
|
}
|
|
282
|
+
else if (key.return && selectedSnapshotItem) {
|
|
283
|
+
// Enter key navigates to detail view
|
|
284
|
+
navigate("snapshot-detail", {
|
|
285
|
+
snapshotId: selectedSnapshotItem.id,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
244
288
|
else if (input === "a" && selectedSnapshotItem) {
|
|
245
289
|
setShowPopup(true);
|
|
246
290
|
setSelectedOperation(0);
|
|
@@ -267,7 +311,22 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
267
311
|
label: selectedSnapshot?.name || selectedSnapshot?.id || "Snapshot",
|
|
268
312
|
},
|
|
269
313
|
{ label: operationLabel, active: true },
|
|
270
|
-
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(
|
|
314
|
+
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Continue" }] })] }));
|
|
315
|
+
}
|
|
316
|
+
// Delete confirmation
|
|
317
|
+
if (showDeleteConfirm && selectedSnapshot) {
|
|
318
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Snapshot", message: `Are you sure you want to delete "${selectedSnapshot.name || selectedSnapshot.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
|
|
319
|
+
{ label: "Snapshots" },
|
|
320
|
+
{ label: selectedSnapshot.name || selectedSnapshot.id },
|
|
321
|
+
{ label: "Delete", active: true },
|
|
322
|
+
], onConfirm: () => {
|
|
323
|
+
setShowDeleteConfirm(false);
|
|
324
|
+
setExecutingOperation("delete");
|
|
325
|
+
executeOperation(selectedSnapshot, "delete");
|
|
326
|
+
}, onCancel: () => {
|
|
327
|
+
setShowDeleteConfirm(false);
|
|
328
|
+
setSelectedSnapshot(null);
|
|
329
|
+
} }));
|
|
271
330
|
}
|
|
272
331
|
// Operation loading state
|
|
273
332
|
if (operationLoading && selectedSnapshot) {
|
|
@@ -311,30 +370,32 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
311
370
|
: []),
|
|
312
371
|
] }), _jsx(ErrorMessage, { message: "Failed to list snapshots", error: error })] }));
|
|
313
372
|
}
|
|
314
|
-
// Empty state
|
|
315
|
-
if (snapshots.length === 0) {
|
|
316
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
317
|
-
{ label: "Snapshots", active: !devboxId },
|
|
318
|
-
...(devboxId
|
|
319
|
-
? [{ label: `Devbox: ${devboxId}`, active: true }]
|
|
320
|
-
: []),
|
|
321
|
-
] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsxs(Text, { color: colors.primary, bold: true, children: ["rli snapshot create ", "<devbox-id>"] })] })] }));
|
|
322
|
-
}
|
|
323
373
|
// Main list view
|
|
324
374
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
325
375
|
{ label: "Snapshots", active: !devboxId },
|
|
326
376
|
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
327
|
-
] }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
|
|
377
|
+
] }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No snapshots found. Try: rli snapshot create", " ", "<devbox-id>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
|
|
328
378
|
key: op.key,
|
|
329
379
|
label: op.label,
|
|
330
380
|
color: op.color,
|
|
331
381
|
icon: op.icon,
|
|
332
|
-
shortcut: op.key === "
|
|
333
|
-
? "
|
|
334
|
-
: op.key === "
|
|
335
|
-
? "
|
|
336
|
-
: ""
|
|
337
|
-
|
|
382
|
+
shortcut: op.key === "view_details"
|
|
383
|
+
? "v"
|
|
384
|
+
: op.key === "create_devbox"
|
|
385
|
+
? "c"
|
|
386
|
+
: op.key === "delete"
|
|
387
|
+
? "d"
|
|
388
|
+
: "",
|
|
389
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
390
|
+
{
|
|
391
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
392
|
+
label: "Page",
|
|
393
|
+
condition: hasMore || hasPrev,
|
|
394
|
+
},
|
|
395
|
+
{ key: "Enter", label: "Details" },
|
|
396
|
+
{ key: "a", label: "Actions" },
|
|
397
|
+
{ key: "Esc", label: "Back" },
|
|
398
|
+
] })] }));
|
|
338
399
|
};
|
|
339
400
|
// Export the UI component for use in the main menu
|
|
340
401
|
export { ListSnapshotsUI };
|
|
@@ -1,14 +1,115 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { Box } from "ink";
|
|
2
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
4
|
import BigText from "ink-big-text";
|
|
5
5
|
import Gradient from "ink-gradient";
|
|
6
6
|
import { isLightMode } from "../utils/theme.js";
|
|
7
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
8
|
+
// Dramatic shades of green shimmer - wide range
|
|
9
|
+
const DARK_SHIMMER_COLORS = [
|
|
10
|
+
"#024A38", // Very very dark emerald
|
|
11
|
+
"#035544", //
|
|
12
|
+
"#036050", //
|
|
13
|
+
"#046C54", //
|
|
14
|
+
"#04765C", //
|
|
15
|
+
"#058164", //
|
|
16
|
+
"#058C6D", //
|
|
17
|
+
"#059669", // Deep emerald
|
|
18
|
+
"#08A076", //
|
|
19
|
+
"#0BA67B", //
|
|
20
|
+
"#0EAE84", //
|
|
21
|
+
"#10B981", // Runloop success green
|
|
22
|
+
"#14C793", // Lighter emerald
|
|
23
|
+
"#1CD7A7", //
|
|
24
|
+
"#24E0B5", //
|
|
25
|
+
"#30EAC0", //
|
|
26
|
+
"#40F5CC", // Very bright emerald
|
|
27
|
+
"#30EAC0", //
|
|
28
|
+
"#24E0B5", //
|
|
29
|
+
"#1CD7A7", //
|
|
30
|
+
"#14C793", // Lighter emerald
|
|
31
|
+
"#10B981", // Runloop success green
|
|
32
|
+
"#0EAE84", //
|
|
33
|
+
"#0BA67B", //
|
|
34
|
+
"#08A076", //
|
|
35
|
+
"#059669", // Deep emerald
|
|
36
|
+
"#058C6D", //
|
|
37
|
+
"#058164", //
|
|
38
|
+
"#04765C", //
|
|
39
|
+
"#046C54", //
|
|
40
|
+
"#036050", //
|
|
41
|
+
"#035544", //
|
|
42
|
+
];
|
|
43
|
+
const LIGHT_SHIMMER_COLORS = [
|
|
44
|
+
"#034D3A", // Very very deep emerald
|
|
45
|
+
"#045540", //
|
|
46
|
+
"#055D46", //
|
|
47
|
+
"#065F46", //
|
|
48
|
+
"#046A50", //
|
|
49
|
+
"#047857", // Deep emerald
|
|
50
|
+
"#058360", //
|
|
51
|
+
"#058C68", //
|
|
52
|
+
"#059669", // Runloop light success green
|
|
53
|
+
"#08A076", //
|
|
54
|
+
"#0BA67B", //
|
|
55
|
+
"#0EAE84", //
|
|
56
|
+
"#10B981", // Medium emerald
|
|
57
|
+
"#14C793", // Lighter emerald
|
|
58
|
+
"#18D29F", //
|
|
59
|
+
"#1CDCA9", //
|
|
60
|
+
"#20E5B3", //
|
|
61
|
+
"#1CDCA9", //
|
|
62
|
+
"#18D29F", //
|
|
63
|
+
"#14C793", // Lighter emerald
|
|
64
|
+
"#10B981", // Medium emerald
|
|
65
|
+
"#0EAE84", //
|
|
66
|
+
"#0BA67B", //
|
|
67
|
+
"#08A076", //
|
|
68
|
+
"#059669", // Runloop light success green
|
|
69
|
+
"#058C68", //
|
|
70
|
+
"#058360", //
|
|
71
|
+
"#047857", // Deep emerald
|
|
72
|
+
"#046A50", //
|
|
73
|
+
"#065F46", //
|
|
74
|
+
"#055D46", //
|
|
75
|
+
"#045540", //
|
|
76
|
+
];
|
|
77
|
+
// Pre-compute all rotated color frames at module load time
|
|
78
|
+
const precomputeFrames = (colors) => {
|
|
79
|
+
return colors.map((_, i) => [...colors.slice(i), ...colors.slice(0, i)]);
|
|
80
|
+
};
|
|
81
|
+
// Use every 2nd color to reduce frame count and minimize flickering
|
|
82
|
+
const DARK_FRAMES = precomputeFrames(DARK_SHIMMER_COLORS.filter((_, i) => i % 2 === 0));
|
|
83
|
+
const LIGHT_FRAMES = precomputeFrames(LIGHT_SHIMMER_COLORS.filter((_, i) => i % 2 === 0));
|
|
84
|
+
// Minimum width to show the full BigText banner (simple3d font needs ~80 chars for "RUNLOOP.ai")
|
|
85
|
+
const MIN_WIDTH_FOR_BIG_BANNER = 90;
|
|
86
|
+
// Animation interval in ms
|
|
87
|
+
const SHIMMER_INTERVAL = 400;
|
|
7
88
|
export const Banner = React.memo(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
89
|
+
const [frameIndex, setFrameIndex] = useState(0);
|
|
90
|
+
const frames = isLightMode() ? LIGHT_FRAMES : DARK_FRAMES;
|
|
91
|
+
const { terminalWidth } = useViewportHeight();
|
|
92
|
+
const timeoutRef = useRef(null);
|
|
93
|
+
// Determine if we should show compact mode
|
|
94
|
+
const isCompact = terminalWidth < MIN_WIDTH_FOR_BIG_BANNER;
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const tick = () => {
|
|
97
|
+
setFrameIndex((prev) => (prev + 1) % frames.length);
|
|
98
|
+
timeoutRef.current = setTimeout(tick, SHIMMER_INTERVAL);
|
|
99
|
+
};
|
|
100
|
+
timeoutRef.current = setTimeout(tick, SHIMMER_INTERVAL);
|
|
101
|
+
return () => {
|
|
102
|
+
if (timeoutRef.current) {
|
|
103
|
+
clearTimeout(timeoutRef.current);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}, [frames.length]);
|
|
107
|
+
// Use pre-computed frame - no array operations during render
|
|
108
|
+
const currentColors = frames[frameIndex];
|
|
109
|
+
// Compact banner for narrow terminals
|
|
110
|
+
if (isCompact) {
|
|
111
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", paddingX: 1, children: _jsx(Gradient, { colors: currentColors, children: _jsx(Text, { bold: true, children: "\u25C6 RUNLOOP.ai" }) }) }));
|
|
112
|
+
}
|
|
113
|
+
// Full banner for wide terminals
|
|
114
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", paddingX: 1, children: _jsx(Gradient, { colors: currentColors, children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
|
|
14
115
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ConfirmationPrompt - Reusable confirmation dialog for destructive actions
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { Header } from "./Header.js";
|
|
9
|
+
import { Breadcrumb } from "./Breadcrumb.js";
|
|
10
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
11
|
+
import { colors } from "../utils/theme.js";
|
|
12
|
+
export const ConfirmationPrompt = ({ title, message, details, breadcrumbItems, onConfirm, onCancel, confirmLabel = "Yes, delete", cancelLabel = "No, cancel", confirmColor = colors.error, }) => {
|
|
13
|
+
// Default to "No" (index 1)
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = React.useState(1);
|
|
15
|
+
useInput((input, key) => {
|
|
16
|
+
if (key.upArrow || key.leftArrow) {
|
|
17
|
+
setSelectedIndex(0);
|
|
18
|
+
}
|
|
19
|
+
else if (key.downArrow || key.rightArrow) {
|
|
20
|
+
setSelectedIndex(1);
|
|
21
|
+
}
|
|
22
|
+
else if (key.return) {
|
|
23
|
+
if (selectedIndex === 0) {
|
|
24
|
+
onConfirm();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
onCancel();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else if (key.escape || input === "q") {
|
|
31
|
+
onCancel();
|
|
32
|
+
}
|
|
33
|
+
else if (input === "y" || input === "Y") {
|
|
34
|
+
onConfirm();
|
|
35
|
+
}
|
|
36
|
+
else if (input === "n" || input === "N") {
|
|
37
|
+
onCancel();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return (_jsxs(_Fragment, { children: [breadcrumbItems && _jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Header, { title: title }), _jsxs(Box, { flexDirection: "column", marginTop: 1, paddingX: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.warning, children: [figures.warning, " ", message] }) }), details && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: details }) })), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: selectedIndex === 0 ? confirmColor : colors.textDim, children: [selectedIndex === 0 ? figures.pointer : " ", " "] }), _jsx(Text, { color: selectedIndex === 0 ? confirmColor : colors.textDim, bold: selectedIndex === 0, children: confirmLabel }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[y]"] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: selectedIndex === 1 ? colors.success : colors.textDim, children: [selectedIndex === 1 ? figures.pointer : " ", " "] }), _jsx(Text, { color: selectedIndex === 1 ? colors.success : colors.textDim, bold: selectedIndex === 1, children: cancelLabel }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[n]"] })] })] }), _jsx(NavigationTips, { showArrows: true, arrowLabel: "Select", marginTop: 1, paddingX: 0, tips: [
|
|
41
|
+
{ key: "Enter", label: "Confirm" },
|
|
42
|
+
{ key: "y/n", label: "Quick select" },
|
|
43
|
+
{ key: "Esc", label: "Cancel" },
|
|
44
|
+
] })] })] }));
|
|
45
|
+
};
|
|
@@ -8,6 +8,8 @@ import { SpinnerComponent } from "./Spinner.js";
|
|
|
8
8
|
import { ErrorMessage } from "./ErrorMessage.js";
|
|
9
9
|
import { SuccessMessage } from "./SuccessMessage.js";
|
|
10
10
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
12
|
+
import { ConfirmationPrompt } from "./ConfirmationPrompt.js";
|
|
11
13
|
import { colors } from "../utils/theme.js";
|
|
12
14
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
13
15
|
import { useNavigation } from "../store/navigationStore.js";
|
|
@@ -27,6 +29,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
27
29
|
const [operationError, setOperationError] = React.useState(null);
|
|
28
30
|
const [execScroll, setExecScroll] = React.useState(0);
|
|
29
31
|
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
32
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
30
33
|
// Calculate viewport for exec output:
|
|
31
34
|
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
32
35
|
// - Command header (border + 2 content + border + marginBottom): 5 lines
|
|
@@ -144,15 +147,22 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
144
147
|
return op.key === "logs" || op.key === "delete";
|
|
145
148
|
})
|
|
146
149
|
: allOperations;
|
|
147
|
-
// Auto-execute operations that don't need input
|
|
150
|
+
// Auto-execute operations that don't need input (except delete which needs confirmation)
|
|
148
151
|
React.useEffect(() => {
|
|
149
|
-
const autoExecuteOps = ["
|
|
152
|
+
const autoExecuteOps = ["ssh", "logs", "suspend", "resume"];
|
|
150
153
|
if (executingOperation &&
|
|
151
154
|
autoExecuteOps.includes(executingOperation) &&
|
|
152
155
|
!loading &&
|
|
153
156
|
devbox) {
|
|
154
157
|
executeOperation();
|
|
155
158
|
}
|
|
159
|
+
// Show confirmation for delete
|
|
160
|
+
if (executingOperation === "delete" &&
|
|
161
|
+
!loading &&
|
|
162
|
+
devbox &&
|
|
163
|
+
!showDeleteConfirm) {
|
|
164
|
+
setShowDeleteConfirm(true);
|
|
165
|
+
}
|
|
156
166
|
}, [executingOperation]);
|
|
157
167
|
// Handle Ctrl+C to exit
|
|
158
168
|
useExitOnCtrlC();
|
|
@@ -415,6 +425,21 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
415
425
|
}
|
|
416
426
|
};
|
|
417
427
|
const operationLabel = operations.find((o) => o.key === executingOperation)?.label || "Operation";
|
|
428
|
+
// Show delete confirmation
|
|
429
|
+
if (showDeleteConfirm) {
|
|
430
|
+
return (_jsx(ConfirmationPrompt, { title: "Shutdown Devbox", message: `Are you sure you want to shutdown "${devbox.name || devbox.id}"?`, details: "The devbox will be terminated and all unsaved data will be lost.", breadcrumbItems: [
|
|
431
|
+
...breadcrumbItems.slice(0, -1),
|
|
432
|
+
{ label: devbox.name || devbox.id },
|
|
433
|
+
{ label: "Shutdown", active: true },
|
|
434
|
+
], confirmLabel: "Yes, shutdown", onConfirm: () => {
|
|
435
|
+
setShowDeleteConfirm(false);
|
|
436
|
+
executeOperation();
|
|
437
|
+
}, onCancel: () => {
|
|
438
|
+
setShowDeleteConfirm(false);
|
|
439
|
+
setExecutingOperation(null);
|
|
440
|
+
onBack();
|
|
441
|
+
} }));
|
|
442
|
+
}
|
|
418
443
|
// Operation result display
|
|
419
444
|
if (operationResult || operationError) {
|
|
420
445
|
// Check for custom exec rendering
|
|
@@ -445,7 +470,12 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
445
470
|
const isStderr = actualIndex >= stdoutLines.length;
|
|
446
471
|
const lineColor = isStderr ? colors.error : colors.text;
|
|
447
472
|
return (_jsx(Box, { children: _jsx(Text, { color: lineColor, children: line }) }, index));
|
|
448
|
-
})] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] }), hasLess && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] })), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(
|
|
473
|
+
})] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", allLines.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "lines"] }), allLines.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Viewing ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, allLines.length), " of", " ", allLines.length] }), hasLess && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] })), hasMore && (_jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }))] })), stdout && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.success, dimColor: true, children: ["stdout: ", stdoutLines.length, " lines"] })] })), stderr && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.error, dimColor: true, children: ["stderr: ", stderrLines.length, " lines"] })] })), 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: [
|
|
474
|
+
{ key: "g", label: "Top" },
|
|
475
|
+
{ key: "G", label: "Bottom" },
|
|
476
|
+
{ key: "c", label: "Copy" },
|
|
477
|
+
{ key: "Enter/q/esc", label: "Back" },
|
|
478
|
+
] })] }));
|
|
449
479
|
}
|
|
450
480
|
// Check for custom logs rendering
|
|
451
481
|
if (operationResult &&
|
|
@@ -470,7 +500,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
470
500
|
}
|
|
471
501
|
}, title: "Logs" }));
|
|
472
502
|
}
|
|
473
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(
|
|
503
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Continue" }] })] }));
|
|
474
504
|
}
|
|
475
505
|
// Operation input mode
|
|
476
506
|
if (executingOperation && devbox) {
|
|
@@ -514,14 +544,20 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
514
544
|
? "/path/to/file"
|
|
515
545
|
: executingOperation === "tunnel"
|
|
516
546
|
? "8080"
|
|
517
|
-
: "my-snapshot" }) }), _jsx(
|
|
547
|
+
: "my-snapshot" }) }), _jsx(NavigationTips, { tips: [
|
|
548
|
+
{ key: "Enter", label: "Execute" },
|
|
549
|
+
{ key: "q/esc", label: "Cancel" },
|
|
550
|
+
] })] })] }));
|
|
518
551
|
}
|
|
519
552
|
// Operations selection mode - only show if not skipping
|
|
520
553
|
if (!skipOperationsMenu || !executingOperation) {
|
|
521
554
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _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) => {
|
|
522
555
|
const isSelected = index === selectedOperation;
|
|
523
556
|
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));
|
|
524
|
-
}) })] }), _jsx(
|
|
557
|
+
}) })] }), _jsx(NavigationTips, { showArrows: true, paddingX: 0, tips: [
|
|
558
|
+
{ key: "Enter", label: "Select" },
|
|
559
|
+
{ key: "q", label: "Back" },
|
|
560
|
+
] })] }));
|
|
525
561
|
}
|
|
526
562
|
// If skipOperationsMenu is true and executingOperation is set, show loading while it executes
|
|
527
563
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(SpinnerComponent, { message: "Loading..." })] }));
|
|
@@ -14,7 +14,7 @@ export const DevboxCard = ({ id, name, status, createdAt, }) => {
|
|
|
14
14
|
case "suspended":
|
|
15
15
|
return { icon: figures.circle, color: colors.textDim };
|
|
16
16
|
case "failed":
|
|
17
|
-
return { icon: figures.
|
|
17
|
+
return { icon: figures.warning, color: colors.error };
|
|
18
18
|
default:
|
|
19
19
|
return { icon: figures.circle, color: colors.textDim };
|
|
20
20
|
}
|