@runloop/rl-cli 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/blueprint/list.js +30 -5
- package/dist/commands/devbox/create.js +18 -0
- package/dist/commands/devbox/list.js +30 -34
- package/dist/commands/devbox/tunnel.js +25 -0
- package/dist/commands/network-policy/list.js +30 -5
- package/dist/commands/object/list.js +30 -5
- package/dist/commands/snapshot/list.js +30 -5
- package/dist/components/DevboxActionsMenu.js +356 -50
- package/dist/components/ExecViewer.js +439 -0
- package/dist/components/LogsViewer.js +5 -2
- package/dist/components/ResourceDetailPage.js +2 -2
- package/dist/components/SearchBar.js +24 -0
- package/dist/components/StreamingLogsViewer.js +276 -0
- package/dist/hooks/useListSearch.js +54 -0
- package/dist/router/Router.js +3 -1
- package/dist/screens/DevboxExecScreen.js +51 -0
- package/dist/screens/SnapshotDetailScreen.js +20 -0
- package/dist/services/devboxService.js +42 -5
- package/dist/services/snapshotService.js +17 -4
- package/dist/utils/commands.js +2 -0
- package/dist/utils/output.js +8 -1
- package/package.json +3 -3
|
@@ -12,11 +12,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
|
|
|
12
12
|
import { Table, createTextColumn } from "../../components/Table.js";
|
|
13
13
|
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
14
14
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
15
|
+
import { SearchBar } from "../../components/SearchBar.js";
|
|
15
16
|
import { output, outputError } from "../../utils/output.js";
|
|
16
17
|
import { colors } from "../../utils/theme.js";
|
|
17
18
|
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
18
19
|
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
19
20
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
21
|
+
import { useListSearch } from "../../hooks/useListSearch.js";
|
|
20
22
|
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
21
23
|
import { useNavigation } from "../../store/navigationStore.js";
|
|
22
24
|
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
@@ -35,8 +37,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
35
37
|
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
36
38
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
37
39
|
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
40
|
+
// Search state
|
|
41
|
+
const search = useListSearch({
|
|
42
|
+
onSearchSubmit: () => setSelectedIndex(0),
|
|
43
|
+
onSearchClear: () => setSelectedIndex(0),
|
|
44
|
+
});
|
|
38
45
|
// Calculate overhead for viewport height
|
|
39
|
-
const overhead = 13;
|
|
46
|
+
const overhead = 13 + search.getSearchOverhead();
|
|
40
47
|
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
41
48
|
overhead,
|
|
42
49
|
minHeight: 5,
|
|
@@ -67,6 +74,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
67
74
|
if (devboxId) {
|
|
68
75
|
queryParams.devbox_id = devboxId;
|
|
69
76
|
}
|
|
77
|
+
if (search.submittedSearchQuery) {
|
|
78
|
+
queryParams.search = search.submittedSearchQuery;
|
|
79
|
+
}
|
|
70
80
|
// Fetch ONE page only
|
|
71
81
|
const page = (await client.devboxes.listDiskSnapshots(queryParams));
|
|
72
82
|
// Extract data and create defensive copies
|
|
@@ -87,7 +97,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
87
97
|
totalCount: page.total_count || pageSnapshots.length,
|
|
88
98
|
};
|
|
89
99
|
return result;
|
|
90
|
-
}, [devboxId]);
|
|
100
|
+
}, [devboxId, search.submittedSearchQuery]);
|
|
91
101
|
// Use the shared pagination hook
|
|
92
102
|
const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
93
103
|
fetchPage,
|
|
@@ -97,8 +107,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
97
107
|
pollingEnabled: !showPopup &&
|
|
98
108
|
!executingOperation &&
|
|
99
109
|
!showCreateDevbox &&
|
|
100
|
-
!showDeleteConfirm
|
|
101
|
-
|
|
110
|
+
!showDeleteConfirm &&
|
|
111
|
+
!search.searchMode,
|
|
112
|
+
deps: [devboxId, PAGE_SIZE, search.submittedSearchQuery],
|
|
102
113
|
});
|
|
103
114
|
// Operations for snapshots
|
|
104
115
|
const operations = React.useMemo(() => [
|
|
@@ -180,6 +191,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
180
191
|
}
|
|
181
192
|
};
|
|
182
193
|
useInput((input, key) => {
|
|
194
|
+
// Handle search mode input
|
|
195
|
+
if (search.searchMode) {
|
|
196
|
+
if (key.escape) {
|
|
197
|
+
search.cancelSearch();
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
183
201
|
// Handle operation result display
|
|
184
202
|
if (operationResult || operationError) {
|
|
185
203
|
if (input === "q" || key.escape || key.return) {
|
|
@@ -289,7 +307,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
289
307
|
setShowPopup(true);
|
|
290
308
|
setSelectedOperation(0);
|
|
291
309
|
}
|
|
310
|
+
else if (input === "/") {
|
|
311
|
+
search.enterSearchMode();
|
|
312
|
+
}
|
|
292
313
|
else if (key.escape) {
|
|
314
|
+
if (search.handleEscape()) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
293
317
|
if (onBack) {
|
|
294
318
|
onBack();
|
|
295
319
|
}
|
|
@@ -374,7 +398,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
374
398
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
375
399
|
{ label: "Snapshots", active: !devboxId },
|
|
376
400
|
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
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) => ({
|
|
401
|
+
] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search snapshots..." }), !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] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
|
|
378
402
|
key: op.key,
|
|
379
403
|
label: op.label,
|
|
380
404
|
color: op.color,
|
|
@@ -394,6 +418,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
|
394
418
|
},
|
|
395
419
|
{ key: "Enter", label: "Details" },
|
|
396
420
|
{ key: "a", label: "Actions" },
|
|
421
|
+
{ key: "/", label: "Search" },
|
|
397
422
|
{ key: "Esc", label: "Back" },
|
|
398
423
|
] })] }));
|
|
399
424
|
};
|
|
@@ -14,8 +14,8 @@ import { colors } from "../utils/theme.js";
|
|
|
14
14
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
15
15
|
import { useNavigation } from "../store/navigationStore.js";
|
|
16
16
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
17
|
+
import { suspendDevbox, resumeDevbox, shutdownDevbox, uploadFile, createSnapshot as createDevboxSnapshot, createTunnel, createSSHKey, } from "../services/devboxService.js";
|
|
18
|
+
import { StreamingLogsViewer } from "./StreamingLogsViewer.js";
|
|
19
19
|
export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
20
20
|
{ label: "Devboxes" },
|
|
21
21
|
{ label: devbox.name || devbox.id, active: true },
|
|
@@ -30,6 +30,17 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
30
30
|
const [execScroll, setExecScroll] = React.useState(0);
|
|
31
31
|
const [copyStatus, setCopyStatus] = React.useState(null);
|
|
32
32
|
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
33
|
+
// Snapshot form state
|
|
34
|
+
const [snapshotFormMode, setSnapshotFormMode] = React.useState(false);
|
|
35
|
+
const [snapshotName, setSnapshotName] = React.useState("");
|
|
36
|
+
const [snapshotCommitMessage, setSnapshotCommitMessage] = React.useState("");
|
|
37
|
+
const [snapshotMetadata, setSnapshotMetadata] = React.useState({});
|
|
38
|
+
const [snapshotFormField, setSnapshotFormField] = React.useState("name");
|
|
39
|
+
const [inSnapshotMetadataSection, setInSnapshotMetadataSection] = React.useState(false);
|
|
40
|
+
const [snapshotMetadataKey, setSnapshotMetadataKey] = React.useState("");
|
|
41
|
+
const [snapshotMetadataValue, setSnapshotMetadataValue] = React.useState("");
|
|
42
|
+
const [snapshotMetadataInputMode, setSnapshotMetadataInputMode] = React.useState(null);
|
|
43
|
+
const [selectedSnapshotMetadataIndex, setSelectedSnapshotMetadataIndex] = React.useState(0);
|
|
33
44
|
// Calculate viewport for exec output:
|
|
34
45
|
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
35
46
|
// - Command header (border + 2 content + border + marginBottom): 5 lines
|
|
@@ -163,14 +174,193 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
163
174
|
!showDeleteConfirm) {
|
|
164
175
|
setShowDeleteConfirm(true);
|
|
165
176
|
}
|
|
177
|
+
// Show snapshot form
|
|
178
|
+
if (executingOperation === "snapshot" &&
|
|
179
|
+
!loading &&
|
|
180
|
+
devbox &&
|
|
181
|
+
!snapshotFormMode &&
|
|
182
|
+
!operationResult &&
|
|
183
|
+
!operationError) {
|
|
184
|
+
setSnapshotFormMode(true);
|
|
185
|
+
setSnapshotFormField("name");
|
|
186
|
+
}
|
|
166
187
|
}, [executingOperation]);
|
|
167
188
|
// Handle Ctrl+C to exit
|
|
168
189
|
useExitOnCtrlC();
|
|
169
190
|
useInput((input, key) => {
|
|
170
|
-
// Handle
|
|
171
|
-
if (
|
|
191
|
+
// Handle snapshot metadata section input
|
|
192
|
+
if (snapshotFormMode && inSnapshotMetadataSection) {
|
|
193
|
+
const metadataKeys = Object.keys(snapshotMetadata);
|
|
194
|
+
const maxIndex = metadataKeys.length + 1;
|
|
195
|
+
// Handle input mode (typing key or value)
|
|
196
|
+
if (snapshotMetadataInputMode) {
|
|
197
|
+
if (snapshotMetadataInputMode === "key" &&
|
|
198
|
+
key.return &&
|
|
199
|
+
snapshotMetadataKey.trim()) {
|
|
200
|
+
setSnapshotMetadataInputMode("value");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
else if (snapshotMetadataInputMode === "value" && key.return) {
|
|
204
|
+
if (snapshotMetadataKey.trim() && snapshotMetadataValue.trim()) {
|
|
205
|
+
setSnapshotMetadata({
|
|
206
|
+
...snapshotMetadata,
|
|
207
|
+
[snapshotMetadataKey.trim()]: snapshotMetadataValue.trim(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
setSnapshotMetadataKey("");
|
|
211
|
+
setSnapshotMetadataValue("");
|
|
212
|
+
setSnapshotMetadataInputMode(null);
|
|
213
|
+
setSelectedSnapshotMetadataIndex(0);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
else if (key.escape) {
|
|
217
|
+
setSnapshotMetadataKey("");
|
|
218
|
+
setSnapshotMetadataValue("");
|
|
219
|
+
setSnapshotMetadataInputMode(null);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
else if (key.tab) {
|
|
223
|
+
setSnapshotMetadataInputMode(snapshotMetadataInputMode === "key" ? "value" : "key");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Navigation mode in metadata section
|
|
229
|
+
if (key.upArrow && selectedSnapshotMetadataIndex > 0) {
|
|
230
|
+
setSelectedSnapshotMetadataIndex(selectedSnapshotMetadataIndex - 1);
|
|
231
|
+
}
|
|
232
|
+
else if (key.downArrow && selectedSnapshotMetadataIndex < maxIndex) {
|
|
233
|
+
setSelectedSnapshotMetadataIndex(selectedSnapshotMetadataIndex + 1);
|
|
234
|
+
}
|
|
235
|
+
else if (key.return) {
|
|
236
|
+
if (selectedSnapshotMetadataIndex === 0) {
|
|
237
|
+
setSnapshotMetadataKey("");
|
|
238
|
+
setSnapshotMetadataValue("");
|
|
239
|
+
setSnapshotMetadataInputMode("key");
|
|
240
|
+
}
|
|
241
|
+
else if (selectedSnapshotMetadataIndex === maxIndex) {
|
|
242
|
+
setInSnapshotMetadataSection(false);
|
|
243
|
+
setSelectedSnapshotMetadataIndex(0);
|
|
244
|
+
setSnapshotMetadataKey("");
|
|
245
|
+
setSnapshotMetadataValue("");
|
|
246
|
+
setSnapshotMetadataInputMode(null);
|
|
247
|
+
}
|
|
248
|
+
else if (selectedSnapshotMetadataIndex >= 1 &&
|
|
249
|
+
selectedSnapshotMetadataIndex <= metadataKeys.length) {
|
|
250
|
+
const keyToEdit = metadataKeys[selectedSnapshotMetadataIndex - 1];
|
|
251
|
+
setSnapshotMetadataKey(keyToEdit || "");
|
|
252
|
+
setSnapshotMetadataValue(snapshotMetadata[keyToEdit] || "");
|
|
253
|
+
const newMetadata = { ...snapshotMetadata };
|
|
254
|
+
delete newMetadata[keyToEdit];
|
|
255
|
+
setSnapshotMetadata(newMetadata);
|
|
256
|
+
setSnapshotMetadataInputMode("key");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else if ((input === "d" || key.delete) &&
|
|
260
|
+
selectedSnapshotMetadataIndex >= 1 &&
|
|
261
|
+
selectedSnapshotMetadataIndex <= metadataKeys.length) {
|
|
262
|
+
const keyToDelete = metadataKeys[selectedSnapshotMetadataIndex - 1];
|
|
263
|
+
const newMetadata = { ...snapshotMetadata };
|
|
264
|
+
delete newMetadata[keyToDelete];
|
|
265
|
+
setSnapshotMetadata(newMetadata);
|
|
266
|
+
const newLength = Object.keys(newMetadata).length;
|
|
267
|
+
if (selectedSnapshotMetadataIndex > newLength) {
|
|
268
|
+
setSelectedSnapshotMetadataIndex(Math.max(0, newLength));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else if (key.escape || input === "q") {
|
|
272
|
+
setInSnapshotMetadataSection(false);
|
|
273
|
+
setSelectedSnapshotMetadataIndex(0);
|
|
274
|
+
setSnapshotMetadataKey("");
|
|
275
|
+
setSnapshotMetadataValue("");
|
|
276
|
+
setSnapshotMetadataInputMode(null);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Handle snapshot form mode (main form navigation)
|
|
281
|
+
if (snapshotFormMode && !inSnapshotMetadataSection) {
|
|
282
|
+
const snapshotFields = [
|
|
283
|
+
"name",
|
|
284
|
+
"commit_message",
|
|
285
|
+
"metadata",
|
|
286
|
+
"create",
|
|
287
|
+
];
|
|
288
|
+
const currentFieldIndex = snapshotFields.indexOf(snapshotFormField);
|
|
289
|
+
if (input === "q" || key.escape) {
|
|
290
|
+
// Cancel snapshot form
|
|
291
|
+
setSnapshotFormMode(false);
|
|
292
|
+
setSnapshotName("");
|
|
293
|
+
setSnapshotCommitMessage("");
|
|
294
|
+
setSnapshotMetadata({});
|
|
295
|
+
setSnapshotFormField("name");
|
|
296
|
+
setExecutingOperation(null);
|
|
297
|
+
if (skipOperationsMenu) {
|
|
298
|
+
onBack();
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Navigate between fields (only when not actively editing text fields)
|
|
303
|
+
if (snapshotFormField !== "name" &&
|
|
304
|
+
snapshotFormField !== "commit_message") {
|
|
305
|
+
if (key.upArrow && currentFieldIndex > 0) {
|
|
306
|
+
setSnapshotFormField(snapshotFields[currentFieldIndex - 1]);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (key.downArrow && currentFieldIndex < snapshotFields.length - 1) {
|
|
310
|
+
setSnapshotFormField(snapshotFields[currentFieldIndex + 1]);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Handle Enter key
|
|
315
|
+
if (key.return) {
|
|
316
|
+
if (snapshotFormField === "name") {
|
|
317
|
+
// Move to commit_message field
|
|
318
|
+
setSnapshotFormField("commit_message");
|
|
319
|
+
}
|
|
320
|
+
else if (snapshotFormField === "commit_message") {
|
|
321
|
+
// Move to metadata field
|
|
322
|
+
setSnapshotFormField("metadata");
|
|
323
|
+
}
|
|
324
|
+
else if (snapshotFormField === "metadata") {
|
|
325
|
+
// Enter metadata section
|
|
326
|
+
setInSnapshotMetadataSection(true);
|
|
327
|
+
setSelectedSnapshotMetadataIndex(0);
|
|
328
|
+
}
|
|
329
|
+
else if (snapshotFormField === "create") {
|
|
330
|
+
// Execute snapshot creation
|
|
331
|
+
executeOperation();
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Tab navigation (when not in text input fields)
|
|
336
|
+
if (key.tab &&
|
|
337
|
+
snapshotFormField !== "name" &&
|
|
338
|
+
snapshotFormField !== "commit_message") {
|
|
339
|
+
const nextIndex = key.shift
|
|
340
|
+
? Math.max(0, currentFieldIndex - 1)
|
|
341
|
+
: Math.min(snapshotFields.length - 1, currentFieldIndex + 1);
|
|
342
|
+
setSnapshotFormField(snapshotFields[nextIndex]);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Handle operation input mode (for exec, upload, tunnel)
|
|
348
|
+
if (executingOperation &&
|
|
349
|
+
!operationResult &&
|
|
350
|
+
!operationError &&
|
|
351
|
+
!snapshotFormMode) {
|
|
172
352
|
if (key.return && operationInput.trim()) {
|
|
173
|
-
|
|
353
|
+
// For exec, navigate to dedicated exec screen
|
|
354
|
+
if (executingOperation === "exec") {
|
|
355
|
+
navigate("devbox-exec", {
|
|
356
|
+
devboxId: devbox.id,
|
|
357
|
+
devboxName: devbox.name || devbox.id,
|
|
358
|
+
execCommand: operationInput,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
executeOperation();
|
|
363
|
+
}
|
|
174
364
|
}
|
|
175
365
|
else if (input === "q" || key.escape) {
|
|
176
366
|
setExecutingOperation(null);
|
|
@@ -196,6 +386,40 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
196
386
|
setExecutingOperation(null);
|
|
197
387
|
}
|
|
198
388
|
}
|
|
389
|
+
else if (input === "o" &&
|
|
390
|
+
operationResult &&
|
|
391
|
+
typeof operationResult === "object" &&
|
|
392
|
+
operationResult.__customRender === "tunnel") {
|
|
393
|
+
// Open tunnel URL in browser
|
|
394
|
+
const tunnelUrl = operationResult.__tunnelUrl;
|
|
395
|
+
if (tunnelUrl) {
|
|
396
|
+
const openBrowser = async () => {
|
|
397
|
+
const { exec } = await import("child_process");
|
|
398
|
+
const platform = process.platform;
|
|
399
|
+
let openCommand;
|
|
400
|
+
if (platform === "darwin") {
|
|
401
|
+
openCommand = `open "${tunnelUrl}"`;
|
|
402
|
+
}
|
|
403
|
+
else if (platform === "win32") {
|
|
404
|
+
openCommand = `start "${tunnelUrl}"`;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
openCommand = `xdg-open "${tunnelUrl}"`;
|
|
408
|
+
}
|
|
409
|
+
exec(openCommand, (error) => {
|
|
410
|
+
if (error) {
|
|
411
|
+
setCopyStatus("Could not open browser");
|
|
412
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
setCopyStatus("Opened in browser!");
|
|
416
|
+
setTimeout(() => setCopyStatus(null), 2000);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
openBrowser();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
199
423
|
else if ((key.upArrow || input === "k") &&
|
|
200
424
|
operationResult &&
|
|
201
425
|
typeof operationResult === "object" &&
|
|
@@ -238,6 +462,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
238
462
|
setExecScroll(maxScroll);
|
|
239
463
|
}
|
|
240
464
|
else if (input === "c" &&
|
|
465
|
+
!key.ctrl && // Ignore if Ctrl+C for quit
|
|
241
466
|
operationResult &&
|
|
242
467
|
typeof operationResult === "object" &&
|
|
243
468
|
operationResult.__customRender === "exec") {
|
|
@@ -316,19 +541,7 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
316
541
|
try {
|
|
317
542
|
setLoading(true);
|
|
318
543
|
switch (executingOperation) {
|
|
319
|
-
|
|
320
|
-
// Use service layer (already truncates output to prevent Yoga crashes)
|
|
321
|
-
const execResult = await execCommand(devbox.id, operationInput);
|
|
322
|
-
// Format exec result for custom rendering
|
|
323
|
-
const formattedExecResult = {
|
|
324
|
-
__customRender: "exec",
|
|
325
|
-
command: operationInput,
|
|
326
|
-
stdout: execResult.stdout || "",
|
|
327
|
-
stderr: execResult.stderr || "",
|
|
328
|
-
exitCode: execResult.exit_code ?? 0,
|
|
329
|
-
};
|
|
330
|
-
setOperationResult(formattedExecResult);
|
|
331
|
-
break;
|
|
544
|
+
// Note: "exec" is now handled by ExecViewer component directly
|
|
332
545
|
case "upload":
|
|
333
546
|
// Use service layer
|
|
334
547
|
const filename = operationInput.split("/").pop() || "file";
|
|
@@ -336,9 +549,28 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
336
549
|
setOperationResult(`File ${filename} uploaded successfully`);
|
|
337
550
|
break;
|
|
338
551
|
case "snapshot":
|
|
339
|
-
// Use service layer
|
|
340
|
-
const
|
|
552
|
+
// Use service layer with form data
|
|
553
|
+
const snapshotOptions = {};
|
|
554
|
+
if (snapshotName.trim()) {
|
|
555
|
+
snapshotOptions.name = snapshotName.trim();
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
snapshotOptions.name = `snapshot-${Date.now()}`;
|
|
559
|
+
}
|
|
560
|
+
if (snapshotCommitMessage.trim()) {
|
|
561
|
+
snapshotOptions.commit_message = snapshotCommitMessage.trim();
|
|
562
|
+
}
|
|
563
|
+
if (Object.keys(snapshotMetadata).length > 0) {
|
|
564
|
+
snapshotOptions.metadata = snapshotMetadata;
|
|
565
|
+
}
|
|
566
|
+
const snapshot = await createDevboxSnapshot(devbox.id, snapshotOptions);
|
|
341
567
|
setOperationResult(`Snapshot created: ${snapshot.id}`);
|
|
568
|
+
// Reset snapshot form state
|
|
569
|
+
setSnapshotFormMode(false);
|
|
570
|
+
setSnapshotName("");
|
|
571
|
+
setSnapshotCommitMessage("");
|
|
572
|
+
setSnapshotMetadata({});
|
|
573
|
+
setSnapshotFormField("name");
|
|
342
574
|
break;
|
|
343
575
|
case "ssh":
|
|
344
576
|
// Use service layer
|
|
@@ -372,19 +604,11 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
372
604
|
});
|
|
373
605
|
break;
|
|
374
606
|
case "logs":
|
|
375
|
-
//
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
else {
|
|
381
|
-
const logsResult = {
|
|
382
|
-
__customRender: "logs",
|
|
383
|
-
__logs: logs,
|
|
384
|
-
__totalCount: logs.length,
|
|
385
|
-
};
|
|
386
|
-
setOperationResult(logsResult);
|
|
387
|
-
}
|
|
607
|
+
// Set flag to show streaming logs viewer
|
|
608
|
+
const logsResult = {
|
|
609
|
+
__customRender: "logs",
|
|
610
|
+
};
|
|
611
|
+
setOperationResult(logsResult);
|
|
388
612
|
break;
|
|
389
613
|
case "tunnel":
|
|
390
614
|
// Use service layer
|
|
@@ -394,10 +618,13 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
394
618
|
}
|
|
395
619
|
else {
|
|
396
620
|
const tunnel = await createTunnel(devbox.id, port);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
621
|
+
// Store tunnel result with custom render type to enable "open in browser"
|
|
622
|
+
const tunnelResult = {
|
|
623
|
+
__customRender: "tunnel",
|
|
624
|
+
__tunnelUrl: tunnel.url,
|
|
625
|
+
__port: port,
|
|
626
|
+
};
|
|
627
|
+
setOperationResult(tunnelResult);
|
|
401
628
|
}
|
|
402
629
|
break;
|
|
403
630
|
case "suspend":
|
|
@@ -477,16 +704,15 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
477
704
|
{ key: "Enter/q/esc", label: "Back" },
|
|
478
705
|
] })] }));
|
|
479
706
|
}
|
|
480
|
-
// Check for custom logs rendering
|
|
707
|
+
// Check for custom logs rendering - use streaming logs viewer
|
|
481
708
|
if (operationResult &&
|
|
482
709
|
typeof operationResult === "object" &&
|
|
483
710
|
operationResult.__customRender === "logs") {
|
|
484
|
-
|
|
485
|
-
return (_jsx(LogsViewer, { logs: logs, breadcrumbItems: [
|
|
711
|
+
return (_jsx(StreamingLogsViewer, { devboxId: devbox.id, breadcrumbItems: [
|
|
486
712
|
...breadcrumbItems,
|
|
487
713
|
{ label: "Logs", active: true },
|
|
488
714
|
], onBack: () => {
|
|
489
|
-
// Clear
|
|
715
|
+
// Clear state
|
|
490
716
|
setOperationResult(null);
|
|
491
717
|
setOperationError(null);
|
|
492
718
|
setOperationInput("");
|
|
@@ -498,15 +724,93 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
498
724
|
else {
|
|
499
725
|
setExecutingOperation(null);
|
|
500
726
|
}
|
|
501
|
-
}
|
|
727
|
+
} }));
|
|
728
|
+
}
|
|
729
|
+
// Check for custom tunnel rendering
|
|
730
|
+
if (operationResult &&
|
|
731
|
+
typeof operationResult === "object" &&
|
|
732
|
+
operationResult.__customRender === "tunnel") {
|
|
733
|
+
const tunnelUrl = operationResult.__tunnelUrl || "";
|
|
734
|
+
const tunnelPort = operationResult.__port || "";
|
|
735
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: "Open Tunnel", active: true }] }), _jsx(Header, { title: "Tunnel Created" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.success, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.success, bold: true, children: [figures.tick, " Tunnel created successfully!"] }) }), _jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Port: " }), _jsx(Text, { color: colors.primary, bold: true, children: tunnelPort })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, children: "Public URL: " }) }), _jsx(Box, { children: _jsx(Text, { color: colors.info, bold: true, children: tunnelUrl }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["You can now access port ", tunnelPort, " on the devbox via this URL"] }) }), copyStatus && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.success, bold: true, children: copyStatus }) }))] }), _jsx(NavigationTips, { tips: [
|
|
736
|
+
{ key: "o", label: "Open in Browser" },
|
|
737
|
+
{ key: "Enter/q/esc", label: "Back" },
|
|
738
|
+
] })] }));
|
|
502
739
|
}
|
|
503
740
|
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" }] })] }));
|
|
504
741
|
}
|
|
742
|
+
// Snapshot form mode
|
|
743
|
+
if (snapshotFormMode && executingOperation === "snapshot" && devbox) {
|
|
744
|
+
if (loading) {
|
|
745
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
746
|
+
...breadcrumbItems,
|
|
747
|
+
{ label: "Create Snapshot", active: true },
|
|
748
|
+
] }), _jsx(Header, { title: "Creating Snapshot" }), _jsx(SpinnerComponent, { message: "Creating snapshot..." })] }));
|
|
749
|
+
}
|
|
750
|
+
const snapshotFields = [
|
|
751
|
+
{ key: "name", label: "Name (optional)" },
|
|
752
|
+
{ key: "metadata", label: "Metadata (optional)" },
|
|
753
|
+
{ key: "create", label: "Create Snapshot" },
|
|
754
|
+
];
|
|
755
|
+
const currentFieldIndex = snapshotFields.findIndex((f) => f.key === snapshotFormField);
|
|
756
|
+
// Expanded metadata section
|
|
757
|
+
if (inSnapshotMetadataSection) {
|
|
758
|
+
const metadataKeys = Object.keys(snapshotMetadata);
|
|
759
|
+
const maxIndex = metadataKeys.length + 1;
|
|
760
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
761
|
+
...breadcrumbItems,
|
|
762
|
+
{ label: "Create Snapshot", active: true },
|
|
763
|
+
] }), _jsx(Header, { title: "Create Snapshot - Metadata" }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " Manage Metadata"] }), snapshotMetadataInputMode && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: selectedSnapshotMetadataIndex === 0
|
|
764
|
+
? colors.success
|
|
765
|
+
: colors.warning, paddingX: 1, children: [_jsx(Text, { color: selectedSnapshotMetadataIndex === 0
|
|
766
|
+
? colors.success
|
|
767
|
+
: colors.warning, bold: true, children: selectedSnapshotMetadataIndex === 0
|
|
768
|
+
? "Adding New"
|
|
769
|
+
: "Editing" }), _jsx(Box, { children: snapshotMetadataInputMode === "key" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.primary, children: "Key: " }), _jsx(TextInput, { value: snapshotMetadataKey || "", onChange: setSnapshotMetadataKey, placeholder: "env" })] })) : (_jsxs(Text, { dimColor: true, children: ["Key: ", snapshotMetadataKey || ""] })) }), _jsx(Box, { children: snapshotMetadataInputMode === "value" ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.primary, children: "Value: " }), _jsx(TextInput, { value: snapshotMetadataValue || "", onChange: setSnapshotMetadataValue, placeholder: "production" })] })) : (_jsxs(Text, { dimColor: true, children: ["Value: ", snapshotMetadataValue || ""] })) })] })), !snapshotMetadataInputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedSnapshotMetadataIndex === 0
|
|
770
|
+
? colors.primary
|
|
771
|
+
: colors.textDim, children: [selectedSnapshotMetadataIndex === 0
|
|
772
|
+
? figures.pointer
|
|
773
|
+
: " ", " "] }), _jsx(Text, { color: selectedSnapshotMetadataIndex === 0
|
|
774
|
+
? colors.success
|
|
775
|
+
: colors.textDim, bold: selectedSnapshotMetadataIndex === 0, children: "+ Add new metadata" })] }), metadataKeys.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: metadataKeys.map((key, index) => {
|
|
776
|
+
const itemIndex = index + 1;
|
|
777
|
+
const isSelected = selectedSnapshotMetadataIndex === itemIndex;
|
|
778
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, bold: isSelected, children: [key, ": ", snapshotMetadata[key]] })] }, key));
|
|
779
|
+
}) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedSnapshotMetadataIndex === maxIndex
|
|
780
|
+
? colors.primary
|
|
781
|
+
: colors.textDim, children: [selectedSnapshotMetadataIndex === maxIndex
|
|
782
|
+
? figures.pointer
|
|
783
|
+
: " ", " "] }), _jsxs(Text, { color: selectedSnapshotMetadataIndex === maxIndex
|
|
784
|
+
? colors.success
|
|
785
|
+
: colors.textDim, bold: selectedSnapshotMetadataIndex === maxIndex, children: [figures.tick, " Done"] })] })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: snapshotMetadataInputMode
|
|
786
|
+
? `[Tab] Switch field • [Enter] ${snapshotMetadataInputMode === "key" ? "Next" : "Save"} • [esc] Cancel`
|
|
787
|
+
: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedSnapshotMetadataIndex === 0 ? "Add" : selectedSnapshotMetadataIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] })] }));
|
|
788
|
+
}
|
|
789
|
+
// Main snapshot form
|
|
790
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
791
|
+
...breadcrumbItems,
|
|
792
|
+
{ label: "Create Snapshot", active: true },
|
|
793
|
+
] }), _jsx(Header, { title: "Create Snapshot" }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: (() => {
|
|
794
|
+
const name = devbox.name || devbox.id;
|
|
795
|
+
return name.length > 100
|
|
796
|
+
? name.substring(0, 100) + "..."
|
|
797
|
+
: name;
|
|
798
|
+
})() }) }), _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: snapshotFormField === "name" ? colors.primary : colors.textDim, children: [snapshotFormField === "name" ? figures.pointer : " ", " Name:", " "] }), snapshotFormField === "name" ? (_jsx(TextInput, { value: snapshotName, onChange: setSnapshotName, placeholder: "my-snapshot (optional)" })) : (_jsx(Text, { color: colors.text, children: snapshotName || "(auto-generated)" }))] }), _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: snapshotFormField === "commit_message"
|
|
799
|
+
? colors.primary
|
|
800
|
+
: colors.textDim, children: [snapshotFormField === "commit_message" ? figures.pointer : " ", " ", "Commit Message:", " "] }), snapshotFormField === "commit_message" ? (_jsx(TextInput, { value: snapshotCommitMessage, onChange: setSnapshotCommitMessage, placeholder: "Describe this snapshot (optional)" })) : (_jsx(Text, { color: colors.text, children: snapshotCommitMessage || "(none)" }))] }), _jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: snapshotFormField === "metadata"
|
|
801
|
+
? colors.primary
|
|
802
|
+
: colors.textDim, children: [snapshotFormField === "metadata" ? figures.pointer : " ", " ", "Metadata:", " "] }), _jsxs(Text, { color: colors.text, children: [Object.keys(snapshotMetadata).length, " item(s)"] }), snapshotFormField === "metadata" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), Object.keys(snapshotMetadata).length > 0 && (_jsx(Box, { marginLeft: 4, flexDirection: "column", children: Object.entries(snapshotMetadata).map(([key, value]) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [key, ": ", value] }, key))) }))] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: snapshotFormField === "create" ? colors.success : colors.textDim, bold: snapshotFormField === "create", children: [snapshotFormField === "create" ? figures.pointer : " ", " ", figures.play, " Create Snapshot"] }) })] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
803
|
+
{
|
|
804
|
+
key: "Enter",
|
|
805
|
+
label: snapshotFormField === "create" ? "Create" : "Select",
|
|
806
|
+
},
|
|
807
|
+
{ key: "q/esc", label: "Cancel" },
|
|
808
|
+
] })] }));
|
|
809
|
+
}
|
|
505
810
|
// Operation input mode
|
|
506
811
|
if (executingOperation && devbox) {
|
|
507
812
|
const needsInput = executingOperation === "exec" ||
|
|
508
813
|
executingOperation === "upload" ||
|
|
509
|
-
executingOperation === "snapshot" ||
|
|
510
814
|
executingOperation === "tunnel";
|
|
511
815
|
if (loading) {
|
|
512
816
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
@@ -530,7 +834,6 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
530
834
|
const prompts = {
|
|
531
835
|
exec: "Command to execute:",
|
|
532
836
|
upload: "File path to upload:",
|
|
533
|
-
snapshot: "Snapshot name (optional):",
|
|
534
837
|
tunnel: "Port number to expose:",
|
|
535
838
|
};
|
|
536
839
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [...breadcrumbItems, { label: operationLabel, active: true }] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: (() => {
|
|
@@ -542,12 +845,15 @@ export const DevboxActionsMenu = ({ devbox, onBack, breadcrumbItems = [
|
|
|
542
845
|
? "ls -la"
|
|
543
846
|
: executingOperation === "upload"
|
|
544
847
|
? "/path/to/file"
|
|
545
|
-
: executingOperation === "
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
848
|
+
: "8080" }) }), _jsx(NavigationTips, { tips: executingOperation === "exec"
|
|
849
|
+
? [
|
|
850
|
+
{ key: "Enter", label: "Execute" },
|
|
851
|
+
{ key: "q/esc", label: "Cancel" },
|
|
852
|
+
]
|
|
853
|
+
: [
|
|
854
|
+
{ key: "Enter", label: "Execute" },
|
|
855
|
+
{ key: "q/esc", label: "Cancel" },
|
|
856
|
+
] })] })] }));
|
|
551
857
|
}
|
|
552
858
|
// Operations selection mode - only show if not skipping
|
|
553
859
|
if (!skipOperationsMenu || !executingOperation) {
|