@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,90 +1,319 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
3
5
|
import { getClient } from "../../utils/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
6
|
+
import { Header } from "../../components/Header.js";
|
|
7
|
+
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
8
|
+
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
9
|
+
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
10
|
+
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
11
|
+
import { Table, createTextColumn } from "../../components/Table.js";
|
|
12
|
+
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
13
|
+
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
14
|
+
import { output, outputError } from "../../utils/output.js";
|
|
7
15
|
import { colors } from "../../utils/theme.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
17
|
+
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
18
|
+
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
19
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
20
|
+
const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
|
|
21
|
+
const { exit: inkExit } = useApp();
|
|
22
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
23
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
24
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
const [selectedSnapshot, setSelectedSnapshot] = React.useState(null);
|
|
27
|
+
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
28
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
29
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
30
|
+
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
31
|
+
// Calculate overhead for viewport height
|
|
32
|
+
const overhead = 13;
|
|
33
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
34
|
+
overhead,
|
|
35
|
+
minHeight: 5,
|
|
36
|
+
});
|
|
37
|
+
const PAGE_SIZE = viewportHeight;
|
|
38
|
+
// All width constants
|
|
18
39
|
const idWidth = 25;
|
|
19
|
-
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
40
|
+
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
|
|
20
41
|
const devboxWidth = 15;
|
|
21
42
|
const timeWidth = 20;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
43
|
+
const showDevboxIdColumn = terminalWidth >= 100 && !devboxId;
|
|
44
|
+
// Fetch function for pagination hook
|
|
45
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
46
|
+
const client = getClient();
|
|
47
|
+
const pageSnapshots = [];
|
|
48
|
+
// Build query params
|
|
49
|
+
const queryParams = {
|
|
50
|
+
limit: params.limit,
|
|
51
|
+
};
|
|
52
|
+
if (params.startingAt) {
|
|
53
|
+
queryParams.starting_after = params.startingAt;
|
|
54
|
+
}
|
|
55
|
+
if (devboxId) {
|
|
56
|
+
queryParams.devbox_id = devboxId;
|
|
57
|
+
}
|
|
58
|
+
// Fetch ONE page only
|
|
59
|
+
const page = (await client.devboxes.listDiskSnapshots(queryParams));
|
|
60
|
+
// Extract data and create defensive copies
|
|
61
|
+
if (page.snapshots && Array.isArray(page.snapshots)) {
|
|
62
|
+
page.snapshots.forEach((s) => {
|
|
63
|
+
pageSnapshots.push({
|
|
64
|
+
id: s.id,
|
|
65
|
+
name: s.name,
|
|
66
|
+
status: s.status,
|
|
67
|
+
create_time_ms: s.create_time_ms,
|
|
68
|
+
source_devbox_id: s.source_devbox_id,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const result = {
|
|
73
|
+
items: pageSnapshots,
|
|
74
|
+
hasMore: page.has_more || false,
|
|
75
|
+
totalCount: page.total_count || pageSnapshots.length,
|
|
76
|
+
};
|
|
77
|
+
return result;
|
|
78
|
+
}, [devboxId]);
|
|
79
|
+
// Use the shared pagination hook
|
|
80
|
+
const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
|
|
81
|
+
fetchPage,
|
|
82
|
+
pageSize: PAGE_SIZE,
|
|
83
|
+
getItemId: (snapshot) => snapshot.id,
|
|
84
|
+
pollInterval: 2000,
|
|
85
|
+
pollingEnabled: !showPopup && !executingOperation,
|
|
86
|
+
deps: [devboxId, PAGE_SIZE],
|
|
87
|
+
});
|
|
88
|
+
// Operations for snapshots
|
|
89
|
+
const operations = React.useMemo(() => [
|
|
90
|
+
{
|
|
91
|
+
key: "delete",
|
|
92
|
+
label: "Delete Snapshot",
|
|
93
|
+
color: colors.error,
|
|
94
|
+
icon: figures.cross,
|
|
95
|
+
},
|
|
96
|
+
], []);
|
|
97
|
+
// Build columns
|
|
98
|
+
const columns = React.useMemo(() => [
|
|
99
|
+
createTextColumn("id", "ID", (snapshot) => snapshot.id, {
|
|
100
|
+
width: idWidth + 1,
|
|
101
|
+
color: colors.idColor,
|
|
102
|
+
dimColor: false,
|
|
103
|
+
bold: false,
|
|
104
|
+
}),
|
|
105
|
+
createTextColumn("name", "Name", (snapshot) => snapshot.name || "", {
|
|
106
|
+
width: nameWidth,
|
|
107
|
+
}),
|
|
108
|
+
createTextColumn("devbox", "Devbox", (snapshot) => snapshot.source_devbox_id || "", {
|
|
109
|
+
width: devboxWidth,
|
|
110
|
+
color: colors.idColor,
|
|
111
|
+
dimColor: false,
|
|
112
|
+
bold: false,
|
|
113
|
+
visible: showDevboxIdColumn,
|
|
114
|
+
}),
|
|
115
|
+
createTextColumn("created", "Created", (snapshot) => snapshot.create_time_ms ? formatTimeAgo(snapshot.create_time_ms) : "", {
|
|
116
|
+
width: timeWidth,
|
|
117
|
+
color: colors.textDim,
|
|
118
|
+
dimColor: false,
|
|
119
|
+
bold: false,
|
|
120
|
+
}),
|
|
121
|
+
], [idWidth, nameWidth, devboxWidth, timeWidth, showDevboxIdColumn]);
|
|
122
|
+
// Handle Ctrl+C to exit
|
|
123
|
+
useExitOnCtrlC();
|
|
124
|
+
// Ensure selected index is within bounds
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
if (snapshots.length > 0 && selectedIndex >= snapshots.length) {
|
|
127
|
+
setSelectedIndex(Math.max(0, snapshots.length - 1));
|
|
128
|
+
}
|
|
129
|
+
}, [snapshots.length, selectedIndex]);
|
|
130
|
+
const selectedSnapshotItem = snapshots[selectedIndex];
|
|
131
|
+
// Calculate pagination info for display
|
|
132
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
133
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
134
|
+
const endIndex = startIndex + snapshots.length;
|
|
135
|
+
const executeOperation = async () => {
|
|
136
|
+
const client = getClient();
|
|
137
|
+
const snapshot = selectedSnapshot;
|
|
138
|
+
if (!snapshot)
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
setOperationLoading(true);
|
|
142
|
+
switch (executingOperation) {
|
|
143
|
+
case "delete":
|
|
144
|
+
await client.devboxes.deleteDiskSnapshot(snapshot.id);
|
|
145
|
+
setOperationResult(`Snapshot ${snapshot.id} deleted successfully`);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
setOperationError(err);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
setOperationLoading(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
useInput((input, key) => {
|
|
157
|
+
// Handle operation result display
|
|
158
|
+
if (operationResult || operationError) {
|
|
159
|
+
if (input === "q" || key.escape || key.return) {
|
|
160
|
+
setOperationResult(null);
|
|
161
|
+
setOperationError(null);
|
|
162
|
+
setExecutingOperation(null);
|
|
163
|
+
setSelectedSnapshot(null);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Handle popup navigation
|
|
168
|
+
if (showPopup) {
|
|
169
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
170
|
+
setSelectedOperation(selectedOperation - 1);
|
|
171
|
+
}
|
|
172
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
173
|
+
setSelectedOperation(selectedOperation + 1);
|
|
174
|
+
}
|
|
175
|
+
else if (key.return) {
|
|
176
|
+
setShowPopup(false);
|
|
177
|
+
const operationKey = operations[selectedOperation].key;
|
|
178
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
179
|
+
setExecutingOperation(operationKey);
|
|
180
|
+
// Execute immediately after state update
|
|
181
|
+
setTimeout(() => executeOperation(), 0);
|
|
182
|
+
}
|
|
183
|
+
else if (key.escape || input === "q") {
|
|
184
|
+
setShowPopup(false);
|
|
185
|
+
setSelectedOperation(0);
|
|
186
|
+
}
|
|
187
|
+
else if (input === "d") {
|
|
188
|
+
// Delete hotkey
|
|
189
|
+
setShowPopup(false);
|
|
190
|
+
setSelectedSnapshot(selectedSnapshotItem);
|
|
191
|
+
setExecutingOperation("delete");
|
|
192
|
+
setTimeout(() => executeOperation(), 0);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const pageSnapshots = snapshots.length;
|
|
197
|
+
// Handle list view navigation
|
|
198
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
199
|
+
setSelectedIndex(selectedIndex - 1);
|
|
200
|
+
}
|
|
201
|
+
else if (key.downArrow && selectedIndex < pageSnapshots - 1) {
|
|
202
|
+
setSelectedIndex(selectedIndex + 1);
|
|
203
|
+
}
|
|
204
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
205
|
+
!loading &&
|
|
206
|
+
!navigating &&
|
|
207
|
+
hasMore) {
|
|
208
|
+
nextPage();
|
|
209
|
+
setSelectedIndex(0);
|
|
210
|
+
}
|
|
211
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
212
|
+
!loading &&
|
|
213
|
+
!navigating &&
|
|
214
|
+
hasPrev) {
|
|
215
|
+
prevPage();
|
|
216
|
+
setSelectedIndex(0);
|
|
217
|
+
}
|
|
218
|
+
else if (input === "a" && selectedSnapshotItem) {
|
|
219
|
+
setShowPopup(true);
|
|
220
|
+
setSelectedOperation(0);
|
|
221
|
+
}
|
|
222
|
+
else if (key.escape) {
|
|
223
|
+
if (onBack) {
|
|
224
|
+
onBack();
|
|
225
|
+
}
|
|
226
|
+
else if (onExit) {
|
|
227
|
+
onExit();
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
inkExit();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Operation result display
|
|
235
|
+
if (operationResult || operationError) {
|
|
236
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
237
|
+
"Operation";
|
|
238
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
239
|
+
{ label: "Snapshots" },
|
|
240
|
+
{
|
|
241
|
+
label: selectedSnapshot?.name || selectedSnapshot?.id || "Snapshot",
|
|
242
|
+
},
|
|
243
|
+
{ label: operationLabel, active: true },
|
|
244
|
+
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
245
|
+
}
|
|
246
|
+
// Operation loading state
|
|
247
|
+
if (operationLoading && selectedSnapshot) {
|
|
248
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
249
|
+
"Operation";
|
|
250
|
+
const messages = {
|
|
251
|
+
delete: "Deleting snapshot...",
|
|
252
|
+
};
|
|
253
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
254
|
+
{ label: "Snapshots" },
|
|
255
|
+
{ label: selectedSnapshot.name || selectedSnapshot.id },
|
|
256
|
+
{ label: operationLabel, active: true },
|
|
257
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
258
|
+
}
|
|
259
|
+
// Loading state
|
|
260
|
+
if (loading && snapshots.length === 0) {
|
|
261
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
262
|
+
{ label: "Snapshots", active: !devboxId },
|
|
263
|
+
...(devboxId
|
|
264
|
+
? [{ label: `Devbox: ${devboxId}`, active: true }]
|
|
265
|
+
: []),
|
|
266
|
+
] }), _jsx(SpinnerComponent, { message: "Loading snapshots..." })] }));
|
|
267
|
+
}
|
|
268
|
+
// Error state
|
|
269
|
+
if (error) {
|
|
270
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
271
|
+
{ label: "Snapshots", active: !devboxId },
|
|
272
|
+
...(devboxId
|
|
273
|
+
? [{ label: `Devbox: ${devboxId}`, active: true }]
|
|
274
|
+
: []),
|
|
275
|
+
] }), _jsx(ErrorMessage, { message: "Failed to list snapshots", error: error })] }));
|
|
276
|
+
}
|
|
277
|
+
// Empty state
|
|
278
|
+
if (snapshots.length === 0) {
|
|
279
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
280
|
+
{ label: "Snapshots", active: !devboxId },
|
|
281
|
+
...(devboxId
|
|
282
|
+
? [{ label: `Devbox: ${devboxId}`, active: true }]
|
|
283
|
+
: []),
|
|
284
|
+
] }), _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>"] })] })] }));
|
|
285
|
+
}
|
|
286
|
+
// Main list view
|
|
287
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
288
|
+
{ label: "Snapshots", active: !devboxId },
|
|
289
|
+
...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
|
|
290
|
+
] }), !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) => ({
|
|
291
|
+
key: op.key,
|
|
292
|
+
label: op.label,
|
|
293
|
+
color: op.color,
|
|
294
|
+
icon: op.icon,
|
|
295
|
+
shortcut: op.key === "delete" ? "d" : "",
|
|
296
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), (hasMore || hasPrev) && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
78
297
|
};
|
|
79
298
|
// Export the UI component for use in the main menu
|
|
80
299
|
export { ListSnapshotsUI };
|
|
81
300
|
export async function listSnapshots(options) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
301
|
+
try {
|
|
302
|
+
const client = getClient();
|
|
303
|
+
// Build query params
|
|
304
|
+
const queryParams = {
|
|
305
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
306
|
+
};
|
|
307
|
+
if (options.devbox) {
|
|
308
|
+
queryParams.devbox_id = options.devbox;
|
|
309
|
+
}
|
|
310
|
+
// Fetch snapshots
|
|
311
|
+
const page = (await client.devboxes.listDiskSnapshots(queryParams));
|
|
312
|
+
// Extract snapshots array
|
|
313
|
+
const snapshots = page.snapshots || [];
|
|
314
|
+
output(snapshots, { format: options.output, defaultFormat: "json" });
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
outputError("Failed to list snapshots", error);
|
|
318
|
+
}
|
|
90
319
|
}
|
|
@@ -1,37 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Get snapshot status command
|
|
3
|
+
*/
|
|
3
4
|
import { getClient } from "../../utils/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
6
|
-
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
7
|
-
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
8
|
-
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
9
|
-
const SnapshotStatusUI = ({ snapshotId }) => {
|
|
10
|
-
const [loading, setLoading] = React.useState(true);
|
|
11
|
-
const [result, setResult] = React.useState(null);
|
|
12
|
-
const [error, setError] = React.useState(null);
|
|
13
|
-
React.useEffect(() => {
|
|
14
|
-
const getSnapshotStatus = async () => {
|
|
15
|
-
try {
|
|
16
|
-
const client = getClient();
|
|
17
|
-
const status = await client.devboxes.diskSnapshots.queryStatus(snapshotId);
|
|
18
|
-
setResult(status);
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
setError(err);
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
setLoading(false);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
getSnapshotStatus();
|
|
28
|
-
}, [snapshotId]);
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Getting snapshot status..." }), result && (_jsx(SuccessMessage, { message: "Snapshot status retrieved", details: `Snapshot ID: ${result.id}\nStatus: ${result.status}\nCreated: ${new Date(result.createdAt).toLocaleString()}` })), error && (_jsx(ErrorMessage, { message: "Failed to get snapshot status", error: error }))] }));
|
|
30
|
-
};
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
31
6
|
export async function getSnapshotStatus(options) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
}
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
const status = await client.devboxes.diskSnapshots.queryStatus(options.snapshotId);
|
|
10
|
+
output(status, { format: options.output, defaultFormat: "json" });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
outputError("Failed to get snapshot status", error);
|
|
14
|
+
}
|
|
37
15
|
}
|
|
@@ -2,44 +2,68 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
import { isLightMode } from "../utils/theme.js";
|
|
6
|
+
export const ActionsPopup = ({ devbox: _devbox, operations, selectedOperation, onClose: _onClose, }) => {
|
|
7
|
+
// Calculate max width needed for content (visible characters only)
|
|
8
|
+
// CRITICAL: Ensure all values are valid numbers to prevent Yoga crashes
|
|
9
|
+
const maxContentWidth = Math.max(...operations.map((op) => {
|
|
10
|
+
const lineText = `${figures.pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
|
|
11
|
+
const len = lineText.length;
|
|
12
|
+
return Number.isFinite(len) && len > 0 ? len : 0;
|
|
13
|
+
}), `${figures.play} Quick Actions`.length, `${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`.length, 40);
|
|
14
|
+
// Add horizontal padding to width (2 spaces on each side = 4 total)
|
|
15
|
+
// Plus 2 for border characters = 6 total extra
|
|
16
|
+
// CRITICAL: Validate all computed widths are positive integers
|
|
17
|
+
const contentWidth = Math.max(10, maxContentWidth + 4);
|
|
18
|
+
// Get background color chalk function - inverted for contrast
|
|
19
|
+
// In light mode (light terminal), use black background for popup
|
|
20
|
+
// In dark mode (dark terminal), use white background for popup
|
|
21
|
+
const bgColor = isLightMode() ? chalk.bgBlack : chalk.bgWhite;
|
|
22
|
+
const textColor = isLightMode() ? chalk.white : chalk.black;
|
|
23
|
+
// Helper to create background lines with proper padding including left/right margins
|
|
24
|
+
const createBgLine = (styledContent, plainContent) => {
|
|
25
|
+
const visibleLength = plainContent.length;
|
|
26
|
+
// CRITICAL: Validate repeat count is non-negative integer
|
|
27
|
+
const repeatCount = Math.max(0, Math.floor(maxContentWidth - visibleLength));
|
|
28
|
+
const rightPadding = " ".repeat(repeatCount);
|
|
29
|
+
// Apply background to left padding + content + right padding
|
|
30
|
+
return bgColor(" " + styledContent + rightPadding + " ");
|
|
15
31
|
};
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
// Create empty line with full background
|
|
33
|
+
// CRITICAL: Validate repeat count is positive integer
|
|
34
|
+
const emptyLine = bgColor(" ".repeat(Math.max(1, Math.floor(contentWidth))));
|
|
35
|
+
// Create border lines with background and integrated title
|
|
36
|
+
const title = `${figures.play} Quick Actions`;
|
|
37
|
+
// The content between ╭ and ╮ should be exactly contentWidth
|
|
38
|
+
// Format: "─ title ─────"
|
|
39
|
+
const titleWithSpaces = ` ${title} `;
|
|
40
|
+
const titleTotalLength = titleWithSpaces.length + 1; // +1 for leading dash
|
|
41
|
+
// CRITICAL: Validate repeat counts are non-negative integers
|
|
42
|
+
const remainingDashes = Math.max(0, Math.floor(contentWidth - titleTotalLength));
|
|
43
|
+
// Use theme primary color for borders to match theme
|
|
44
|
+
const borderColorFn = isLightMode() ? chalk.cyan : chalk.blue;
|
|
45
|
+
const borderTop = bgColor(borderColorFn("╭─" + titleWithSpaces + "─".repeat(remainingDashes) + "╮"));
|
|
46
|
+
// CRITICAL: Validate contentWidth is a positive integer
|
|
47
|
+
const borderBottom = bgColor(borderColorFn("╰" + "─".repeat(Math.max(1, Math.floor(contentWidth))) + "╯"));
|
|
48
|
+
const borderSide = (content) => {
|
|
49
|
+
return bgColor(borderColorFn("│") + content + borderColorFn("│"));
|
|
50
|
+
};
|
|
51
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), _jsx(Text, { children: borderSide(emptyLine) }), operations.map((op, index) => {
|
|
52
|
+
const isSelected = index === selectedOperation;
|
|
53
|
+
const pointer = isSelected ? figures.pointer : " ";
|
|
54
|
+
const lineText = `${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
|
|
55
|
+
let styledLine;
|
|
56
|
+
if (isSelected) {
|
|
57
|
+
// Selected: use operation-specific color for icon and label
|
|
58
|
+
const opColor = op.color;
|
|
59
|
+
const colorFn = chalk[opColor] || textColor;
|
|
60
|
+
styledLine = `${textColor(pointer)} ${colorFn(op.icon)} ${colorFn.bold(op.label)} ${textColor(`[${op.shortcut}]`)}`;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Unselected: gray/dim text for everything
|
|
64
|
+
const dimFn = isLightMode() ? chalk.gray : chalk.gray;
|
|
65
|
+
styledLine = `${dimFn(pointer)} ${dimFn(op.icon)} ${dimFn(op.label)} ${dimFn(`[${op.shortcut}]`)}`;
|
|
66
|
+
}
|
|
67
|
+
return (_jsx(Text, { children: borderSide(createBgLine(styledLine, lineText)) }, op.key));
|
|
68
|
+
}), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderSide(createBgLine(textColor(`${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`), `${figures.arrowUp}${figures.arrowDown} Nav • [Enter] • [Esc] Close`)) }), _jsx(Text, { children: borderSide(emptyLine) }), _jsx(Text, { children: borderBottom })] }) }));
|
|
45
69
|
};
|
|
@@ -2,57 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
|
|
6
|
-
// Version check component
|
|
7
|
-
const VersionCheck = () => {
|
|
8
|
-
const [updateAvailable, setUpdateAvailable] = React.useState(null);
|
|
9
|
-
const [isChecking, setIsChecking] = React.useState(true);
|
|
10
|
-
React.useEffect(() => {
|
|
11
|
-
const checkForUpdates = async () => {
|
|
12
|
-
try {
|
|
13
|
-
const currentVersion = process.env.npm_package_version || "0.0.1";
|
|
14
|
-
const response = await fetch("https://registry.npmjs.org/@runloop/rl-cli/latest");
|
|
15
|
-
if (response.ok) {
|
|
16
|
-
const data = await response.json();
|
|
17
|
-
const latestVersion = data.version;
|
|
18
|
-
if (latestVersion && latestVersion !== currentVersion) {
|
|
19
|
-
// Check if current version is older than latest
|
|
20
|
-
const compareVersions = (version1, version2) => {
|
|
21
|
-
const v1parts = version1.split('.').map(Number);
|
|
22
|
-
const v2parts = version2.split('.').map(Number);
|
|
23
|
-
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
24
|
-
const v1part = v1parts[i] || 0;
|
|
25
|
-
const v2part = v2parts[i] || 0;
|
|
26
|
-
if (v1part > v2part)
|
|
27
|
-
return 1;
|
|
28
|
-
if (v1part < v2part)
|
|
29
|
-
return -1;
|
|
30
|
-
}
|
|
31
|
-
return 0;
|
|
32
|
-
};
|
|
33
|
-
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
|
|
34
|
-
if (isUpdateAvailable) {
|
|
35
|
-
setUpdateAvailable(latestVersion);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
// Silently fail
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
setIsChecking(false);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
checkForUpdates();
|
|
48
|
-
}, []);
|
|
49
|
-
if (isChecking || !updateAvailable) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
return (_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: "\u2728" }), _jsxs(Text, { color: colors.text, bold: true, children: [" ", "Update available:", " "] }), _jsx(Text, { color: colors.textDim, dimColor: true, children: VERSION }), _jsxs(Text, { color: colors.primary, bold: true, children: [" ", "\u2192", " "] }), _jsx(Text, { color: colors.success, bold: true, children: updateAvailable }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Run:", " "] }), _jsx(Text, { color: colors.primary, bold: true, children: "npm install -g @runloop/rl-cli@latest" })] }));
|
|
53
|
-
};
|
|
54
|
-
export const Breadcrumb = React.memo(({ items, showVersionCheck = false }) => {
|
|
5
|
+
export const Breadcrumb = ({ items }) => {
|
|
55
6
|
const env = process.env.RUNLOOP_ENV?.toLowerCase();
|
|
56
7
|
const isDevEnvironment = env === "dev";
|
|
57
|
-
return (
|
|
58
|
-
|
|
8
|
+
return (_jsx(Box, { marginBottom: 1, paddingX: 1, paddingY: 0, children: _jsxs(Box, { borderStyle: "round", borderColor: colors.primary, paddingX: 2, paddingY: 0, children: [_jsx(Text, { color: colors.primary, bold: true, children: "rl" }), isDevEnvironment && (_jsxs(Text, { color: colors.error, bold: true, children: [" ", "(dev)"] })), _jsx(Text, { color: colors.textDim, children: " \u203A " }), items.map((item, index) => {
|
|
9
|
+
// Limit label length to prevent Yoga layout engine errors
|
|
10
|
+
const MAX_LABEL_LENGTH = 80;
|
|
11
|
+
const truncatedLabel = item.label.length > MAX_LABEL_LENGTH
|
|
12
|
+
? item.label.substring(0, MAX_LABEL_LENGTH) + "..."
|
|
13
|
+
: item.label;
|
|
14
|
+
return (_jsxs(React.Fragment, { children: [_jsx(Text, { color: item.active ? colors.primary : colors.textDim, children: truncatedLabel }), index < items.length - 1 && (_jsx(Text, { color: colors.textDim, children: " \u203A " }))] }, index));
|
|
15
|
+
})] }) }));
|
|
16
|
+
};
|