@runloop/rl-cli 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -10
- package/dist/cli.js +79 -72
- package/dist/commands/auth.js +2 -2
- package/dist/commands/blueprint/create.js +31 -83
- package/dist/commands/blueprint/get.js +29 -34
- package/dist/commands/blueprint/list.js +278 -230
- package/dist/commands/blueprint/logs.js +133 -37
- package/dist/commands/config.js +118 -0
- package/dist/commands/devbox/create.js +120 -40
- package/dist/commands/devbox/delete.js +17 -33
- package/dist/commands/devbox/download.js +29 -43
- package/dist/commands/devbox/exec.js +22 -39
- package/dist/commands/devbox/execAsync.js +20 -37
- package/dist/commands/devbox/get.js +13 -35
- package/dist/commands/devbox/getAsync.js +12 -34
- package/dist/commands/devbox/list.js +241 -402
- package/dist/commands/devbox/logs.js +20 -38
- package/dist/commands/devbox/read.js +29 -43
- package/dist/commands/devbox/resume.js +13 -35
- package/dist/commands/devbox/rsync.js +26 -78
- package/dist/commands/devbox/scp.js +25 -79
- package/dist/commands/devbox/sendStdin.js +41 -0
- package/dist/commands/devbox/shutdown.js +13 -35
- package/dist/commands/devbox/ssh.js +46 -78
- package/dist/commands/devbox/suspend.js +13 -35
- package/dist/commands/devbox/tunnel.js +37 -88
- package/dist/commands/devbox/upload.js +28 -36
- package/dist/commands/devbox/write.js +29 -44
- package/dist/commands/mcp-http.js +6 -5
- package/dist/commands/mcp-install.js +12 -10
- package/dist/commands/mcp.js +5 -4
- package/dist/commands/menu.js +26 -67
- package/dist/commands/object/delete.js +12 -34
- package/dist/commands/object/download.js +26 -74
- package/dist/commands/object/get.js +12 -34
- package/dist/commands/object/list.js +15 -93
- package/dist/commands/object/upload.js +35 -96
- package/dist/commands/snapshot/create.js +23 -39
- package/dist/commands/snapshot/delete.js +17 -33
- package/dist/commands/snapshot/get.js +16 -0
- package/dist/commands/snapshot/list.js +309 -80
- package/dist/commands/snapshot/status.js +12 -34
- package/dist/components/ActionsPopup.js +64 -39
- package/dist/components/Banner.js +7 -1
- package/dist/components/Breadcrumb.js +11 -48
- package/dist/components/DevboxActionsMenu.js +117 -207
- package/dist/components/DevboxCreatePage.js +12 -7
- package/dist/components/DevboxDetailPage.js +76 -28
- package/dist/components/ErrorBoundary.js +29 -0
- package/dist/components/ErrorMessage.js +10 -2
- package/dist/components/Header.js +12 -4
- package/dist/components/InteractiveSpawn.js +104 -0
- package/dist/components/LogsViewer.js +169 -0
- package/dist/components/MainMenu.js +37 -33
- package/dist/components/MetadataDisplay.js +4 -4
- package/dist/components/OperationsMenu.js +1 -1
- package/dist/components/ResourceActionsMenu.js +4 -4
- package/dist/components/ResourceListView.js +46 -34
- package/dist/components/Spinner.js +7 -2
- package/dist/components/StatusBadge.js +1 -1
- package/dist/components/SuccessMessage.js +12 -2
- package/dist/components/Table.js +16 -6
- package/dist/components/UpdateNotification.js +56 -0
- package/dist/hooks/useCursorPagination.js +125 -85
- package/dist/hooks/useExitOnCtrlC.js +15 -0
- package/dist/hooks/useViewportHeight.js +47 -0
- package/dist/mcp/server-http.js +2 -1
- package/dist/mcp/server.js +71 -7
- package/dist/router/Router.js +70 -0
- package/dist/router/types.js +1 -0
- package/dist/screens/BlueprintListScreen.js +7 -0
- package/dist/screens/BlueprintLogsScreen.js +74 -0
- package/dist/screens/DevboxActionsScreen.js +25 -0
- package/dist/screens/DevboxCreateScreen.js +11 -0
- package/dist/screens/DevboxDetailScreen.js +60 -0
- package/dist/screens/DevboxListScreen.js +23 -0
- package/dist/screens/LogsSessionScreen.js +49 -0
- package/dist/screens/MenuScreen.js +23 -0
- package/dist/screens/SSHSessionScreen.js +55 -0
- package/dist/screens/SnapshotListScreen.js +7 -0
- package/dist/services/blueprintService.js +101 -0
- package/dist/services/devboxService.js +215 -0
- package/dist/services/snapshotService.js +81 -0
- package/dist/store/blueprintStore.js +89 -0
- package/dist/store/devboxStore.js +105 -0
- package/dist/store/index.js +7 -0
- package/dist/store/navigationStore.js +101 -0
- package/dist/store/snapshotStore.js +87 -0
- package/dist/utils/client.js +4 -2
- package/dist/utils/config.js +22 -111
- package/dist/utils/interactiveCommand.js +3 -2
- package/dist/utils/logFormatter.js +208 -0
- package/dist/utils/memoryMonitor.js +85 -0
- package/dist/utils/output.js +153 -61
- package/dist/utils/process.js +106 -0
- package/dist/utils/processUtils.js +135 -0
- package/dist/utils/screen.js +61 -0
- package/dist/utils/ssh.js +6 -3
- package/dist/utils/sshSession.js +5 -29
- package/dist/utils/terminalDetection.js +185 -0
- package/dist/utils/terminalSync.js +39 -0
- package/dist/utils/theme.js +162 -13
- package/dist/utils/versionCheck.js +53 -0
- package/dist/version.js +12 -0
- package/package.json +19 -17
|
@@ -1,41 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import Gradient from "ink-gradient";
|
|
5
|
-
import figures from "figures";
|
|
1
|
+
/**
|
|
2
|
+
* Create snapshot command
|
|
3
|
+
*/
|
|
6
4
|
import { getClient } from "../../utils/client.js";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
setError(err);
|
|
28
|
-
}
|
|
29
|
-
finally {
|
|
30
|
-
setLoading(false);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
create();
|
|
34
|
-
}, []);
|
|
35
|
-
return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), _jsx(Header, { title: "Create Snapshot", subtitle: "Taking a snapshot of your devbox..." }), loading && (_jsxs(_Fragment, { children: [_jsx(SpinnerComponent, { message: "Creating snapshot..." }), _jsxs(Box, { borderStyle: "round", borderColor: colors.info, paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: colors.primary, bold: true, children: [figures.info, " Configuration"] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, children: [figures.pointer, " Devbox ID:", " "] }), _jsx(Text, { color: colors.text, children: devboxId })] }), name && (_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, children: [figures.pointer, " Name: "] }), _jsx(Text, { color: colors.text, children: name })] }))] })] })] })), result && (_jsxs(_Fragment, { children: [_jsx(SuccessMessage, { message: "Snapshot created successfully!", details: `ID: ${result.id}\nName: ${result.name || "(unnamed)"}\nStatus: ${result.status}` }), _jsxs(Box, { borderStyle: "double", borderColor: colors.success, paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Gradient, { name: "summer", children: _jsxs(Text, { bold: true, children: [figures.star, " Next Steps"] }) }) }), _jsxs(Box, { flexDirection: "column", gap: 1, marginLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, children: [figures.tick, " View snapshots:", " "] }), _jsx(Text, { color: colors.primary, children: "rli snapshot list" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: colors.textDim, children: [figures.tick, " Create devbox from snapshot:", " "] }), _jsxs(Text, { color: colors.primary, children: ["rli devbox create -t ", result.id] })] })] })] })] })), error && (_jsx(ErrorMessage, { message: "Failed to create snapshot", error: error }))] }));
|
|
36
|
-
};
|
|
37
|
-
export async function createSnapshot(devboxId, options) {
|
|
38
|
-
console.clear();
|
|
39
|
-
const { waitUntilExit } = render(_jsx(CreateSnapshotUI, { devboxId: devboxId, name: options.name }));
|
|
40
|
-
await waitUntilExit();
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
export async function createSnapshot(devboxId, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
const snapshot = await client.devboxes.snapshotDisk(devboxId, {
|
|
10
|
+
...(options.name && { name: options.name }),
|
|
11
|
+
});
|
|
12
|
+
// Default: just output the ID for easy scripting
|
|
13
|
+
if (!options.output || options.output === "text") {
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
const snapshotId = snapshot.id || snapshot.snapshot_id;
|
|
16
|
+
console.log(snapshotId);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
output(snapshot, { format: options.output, defaultFormat: "json" });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
outputError("Failed to create snapshot", error);
|
|
24
|
+
}
|
|
41
25
|
}
|
|
@@ -1,37 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Delete snapshot 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 DeleteSnapshotUI = ({ id }) => {
|
|
10
|
-
const [loading, setLoading] = React.useState(true);
|
|
11
|
-
const [success, setSuccess] = React.useState(false);
|
|
12
|
-
const [error, setError] = React.useState(null);
|
|
13
|
-
React.useEffect(() => {
|
|
14
|
-
const deleteSnapshot = async () => {
|
|
15
|
-
try {
|
|
16
|
-
const client = getClient();
|
|
17
|
-
await client.devboxes.diskSnapshots.delete(id);
|
|
18
|
-
setSuccess(true);
|
|
19
|
-
}
|
|
20
|
-
catch (err) {
|
|
21
|
-
setError(err);
|
|
22
|
-
}
|
|
23
|
-
finally {
|
|
24
|
-
setLoading(false);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
deleteSnapshot();
|
|
28
|
-
}, []);
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsx(Header, { title: "Delete Snapshot", subtitle: `Deleting snapshot: ${id}` }), loading && _jsx(SpinnerComponent, { message: "Deleting snapshot..." }), success && (_jsx(SuccessMessage, { message: "Snapshot deleted successfully!", details: `ID: ${id}` })), error && (_jsx(ErrorMessage, { message: "Failed to delete snapshot", error: error }))] }));
|
|
30
|
-
};
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
31
6
|
export async function deleteSnapshot(id, options = {}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const client = executor.getClient();
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
35
9
|
await client.devboxes.diskSnapshots.delete(id);
|
|
36
|
-
|
|
10
|
+
// Default: just output the ID for easy scripting
|
|
11
|
+
if (!options.output || options.output === "text") {
|
|
12
|
+
console.log(id);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
output({ id, status: "deleted" }, { format: options.output, defaultFormat: "json" });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
outputError("Failed to delete snapshot", error);
|
|
20
|
+
}
|
|
37
21
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get snapshot details command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
export async function getSnapshot(options) {
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
// This is the way to get snapshot details
|
|
10
|
+
const snapshotDetails = await client.devboxes.diskSnapshots.queryStatus(options.id);
|
|
11
|
+
output(snapshotDetails, { format: options.output, defaultFormat: "json" });
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
outputError("Failed to get snapshot", error);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -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
|
}
|