@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.
Files changed (105) hide show
  1. package/README.md +54 -10
  2. package/dist/cli.js +79 -72
  3. package/dist/commands/auth.js +2 -2
  4. package/dist/commands/blueprint/create.js +31 -83
  5. package/dist/commands/blueprint/get.js +29 -34
  6. package/dist/commands/blueprint/list.js +278 -230
  7. package/dist/commands/blueprint/logs.js +133 -37
  8. package/dist/commands/config.js +118 -0
  9. package/dist/commands/devbox/create.js +120 -40
  10. package/dist/commands/devbox/delete.js +17 -33
  11. package/dist/commands/devbox/download.js +29 -43
  12. package/dist/commands/devbox/exec.js +22 -39
  13. package/dist/commands/devbox/execAsync.js +20 -37
  14. package/dist/commands/devbox/get.js +13 -35
  15. package/dist/commands/devbox/getAsync.js +12 -34
  16. package/dist/commands/devbox/list.js +241 -402
  17. package/dist/commands/devbox/logs.js +20 -38
  18. package/dist/commands/devbox/read.js +29 -43
  19. package/dist/commands/devbox/resume.js +13 -35
  20. package/dist/commands/devbox/rsync.js +26 -78
  21. package/dist/commands/devbox/scp.js +25 -79
  22. package/dist/commands/devbox/sendStdin.js +41 -0
  23. package/dist/commands/devbox/shutdown.js +13 -35
  24. package/dist/commands/devbox/ssh.js +46 -78
  25. package/dist/commands/devbox/suspend.js +13 -35
  26. package/dist/commands/devbox/tunnel.js +37 -88
  27. package/dist/commands/devbox/upload.js +28 -36
  28. package/dist/commands/devbox/write.js +29 -44
  29. package/dist/commands/mcp-http.js +6 -5
  30. package/dist/commands/mcp-install.js +12 -10
  31. package/dist/commands/mcp.js +5 -4
  32. package/dist/commands/menu.js +26 -67
  33. package/dist/commands/object/delete.js +12 -34
  34. package/dist/commands/object/download.js +26 -74
  35. package/dist/commands/object/get.js +12 -34
  36. package/dist/commands/object/list.js +15 -93
  37. package/dist/commands/object/upload.js +35 -96
  38. package/dist/commands/snapshot/create.js +23 -39
  39. package/dist/commands/snapshot/delete.js +17 -33
  40. package/dist/commands/snapshot/get.js +16 -0
  41. package/dist/commands/snapshot/list.js +309 -80
  42. package/dist/commands/snapshot/status.js +12 -34
  43. package/dist/components/ActionsPopup.js +64 -39
  44. package/dist/components/Banner.js +7 -1
  45. package/dist/components/Breadcrumb.js +11 -48
  46. package/dist/components/DevboxActionsMenu.js +117 -207
  47. package/dist/components/DevboxCreatePage.js +12 -7
  48. package/dist/components/DevboxDetailPage.js +76 -28
  49. package/dist/components/ErrorBoundary.js +29 -0
  50. package/dist/components/ErrorMessage.js +10 -2
  51. package/dist/components/Header.js +12 -4
  52. package/dist/components/InteractiveSpawn.js +104 -0
  53. package/dist/components/LogsViewer.js +169 -0
  54. package/dist/components/MainMenu.js +37 -33
  55. package/dist/components/MetadataDisplay.js +4 -4
  56. package/dist/components/OperationsMenu.js +1 -1
  57. package/dist/components/ResourceActionsMenu.js +4 -4
  58. package/dist/components/ResourceListView.js +46 -34
  59. package/dist/components/Spinner.js +7 -2
  60. package/dist/components/StatusBadge.js +1 -1
  61. package/dist/components/SuccessMessage.js +12 -2
  62. package/dist/components/Table.js +16 -6
  63. package/dist/components/UpdateNotification.js +56 -0
  64. package/dist/hooks/useCursorPagination.js +125 -85
  65. package/dist/hooks/useExitOnCtrlC.js +15 -0
  66. package/dist/hooks/useViewportHeight.js +47 -0
  67. package/dist/mcp/server-http.js +2 -1
  68. package/dist/mcp/server.js +71 -7
  69. package/dist/router/Router.js +70 -0
  70. package/dist/router/types.js +1 -0
  71. package/dist/screens/BlueprintListScreen.js +7 -0
  72. package/dist/screens/BlueprintLogsScreen.js +74 -0
  73. package/dist/screens/DevboxActionsScreen.js +25 -0
  74. package/dist/screens/DevboxCreateScreen.js +11 -0
  75. package/dist/screens/DevboxDetailScreen.js +60 -0
  76. package/dist/screens/DevboxListScreen.js +23 -0
  77. package/dist/screens/LogsSessionScreen.js +49 -0
  78. package/dist/screens/MenuScreen.js +23 -0
  79. package/dist/screens/SSHSessionScreen.js +55 -0
  80. package/dist/screens/SnapshotListScreen.js +7 -0
  81. package/dist/services/blueprintService.js +101 -0
  82. package/dist/services/devboxService.js +215 -0
  83. package/dist/services/snapshotService.js +81 -0
  84. package/dist/store/blueprintStore.js +89 -0
  85. package/dist/store/devboxStore.js +105 -0
  86. package/dist/store/index.js +7 -0
  87. package/dist/store/navigationStore.js +101 -0
  88. package/dist/store/snapshotStore.js +87 -0
  89. package/dist/utils/client.js +4 -2
  90. package/dist/utils/config.js +22 -111
  91. package/dist/utils/interactiveCommand.js +3 -2
  92. package/dist/utils/logFormatter.js +208 -0
  93. package/dist/utils/memoryMonitor.js +85 -0
  94. package/dist/utils/output.js +153 -61
  95. package/dist/utils/process.js +106 -0
  96. package/dist/utils/processUtils.js +135 -0
  97. package/dist/utils/screen.js +61 -0
  98. package/dist/utils/ssh.js +6 -3
  99. package/dist/utils/sshSession.js +5 -29
  100. package/dist/utils/terminalDetection.js +185 -0
  101. package/dist/utils/terminalSync.js +39 -0
  102. package/dist/utils/theme.js +162 -13
  103. package/dist/utils/versionCheck.js +53 -0
  104. package/dist/version.js +12 -0
  105. package/package.json +19 -17
@@ -1,41 +1,25 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from "react";
3
- import { render, Box, Text } from "ink";
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 { Header } from "../../components/Header.js";
8
- import { Banner } from "../../components/Banner.js";
9
- import { SpinnerComponent } from "../../components/Spinner.js";
10
- import { SuccessMessage } from "../../components/SuccessMessage.js";
11
- import { ErrorMessage } from "../../components/ErrorMessage.js";
12
- import { colors } from "../../utils/theme.js";
13
- const CreateSnapshotUI = ({ devboxId, name }) => {
14
- const [loading, setLoading] = React.useState(true);
15
- const [result, setResult] = React.useState(null);
16
- const [error, setError] = React.useState(null);
17
- React.useEffect(() => {
18
- const create = async () => {
19
- try {
20
- const client = getClient();
21
- const snapshot = await client.devboxes.snapshotDisk(devboxId, {
22
- ...(name && { name }),
23
- });
24
- setResult(snapshot);
25
- }
26
- catch (err) {
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Delete snapshot command
3
+ */
3
4
  import { getClient } from "../../utils/client.js";
4
- import { Header } from "../../components/Header.js";
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
- const executor = createExecutor(options);
33
- await executor.executeDelete(async () => {
34
- const client = executor.getClient();
7
+ try {
8
+ const client = getClient();
35
9
  await client.devboxes.diskSnapshots.delete(id);
36
- }, id, () => _jsx(DeleteSnapshotUI, { id: id }));
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 { useStdout } from "ink";
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 { createTextColumn, } from "../../components/Table.js";
5
- import { ResourceListView, formatTimeAgo, } from "../../components/ResourceListView.js";
6
- import { createExecutor } from "../../utils/CommandExecutor.js";
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
- const PAGE_SIZE = 10;
9
- const MAX_FETCH = 100;
10
- const ListSnapshotsUI = ({ devboxId, onBack, onExit }) => {
11
- const { stdout } = useStdout();
12
- // Calculate responsive column widths
13
- const terminalWidth = stdout?.columns || 120;
14
- const showDevboxId = terminalWidth >= 100 && !devboxId; // Hide devbox column if filtering by devbox
15
- const showFullId = terminalWidth >= 80;
16
- const statusIconWidth = 2;
17
- const statusTextWidth = 10;
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
- return (_jsx(ResourceListView, { config: {
23
- resourceName: "Snapshot",
24
- resourceNamePlural: "Snapshots",
25
- fetchResources: async () => {
26
- const client = getClient();
27
- const allSnapshots = [];
28
- let count = 0;
29
- const params = devboxId ? { devbox_id: devboxId } : {};
30
- for await (const snapshot of client.devboxes.listDiskSnapshots(params)) {
31
- allSnapshots.push(snapshot);
32
- count++;
33
- if (count >= MAX_FETCH)
34
- break;
35
- }
36
- return allSnapshots;
37
- },
38
- columns: [
39
- createTextColumn("id", "ID", (snapshot) => snapshot.id, {
40
- width: idWidth,
41
- color: colors.textDim,
42
- dimColor: false,
43
- bold: false,
44
- }),
45
- createTextColumn("name", "Name", (snapshot) => snapshot.name || "(unnamed)", {
46
- width: nameWidth,
47
- }),
48
- createTextColumn("devbox", "Devbox", (snapshot) => snapshot.source_devbox_id || "", {
49
- width: devboxWidth,
50
- color: colors.primary,
51
- dimColor: false,
52
- bold: false,
53
- visible: showDevboxId,
54
- }),
55
- createTextColumn("created", "Created", (snapshot) => snapshot.create_time_ms
56
- ? formatTimeAgo(snapshot.create_time_ms)
57
- : "", {
58
- width: timeWidth,
59
- color: colors.textDim,
60
- dimColor: false,
61
- bold: false,
62
- }),
63
- ],
64
- keyExtractor: (snapshot) => snapshot.id,
65
- emptyState: {
66
- message: "No snapshots found. Try:",
67
- command: "rli snapshot create <devbox-id>",
68
- },
69
- pageSize: PAGE_SIZE,
70
- maxFetch: MAX_FETCH,
71
- onBack: onBack,
72
- onExit: onExit,
73
- breadcrumbItems: [
74
- { label: "Snapshots", active: !devboxId },
75
- ...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
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
- const executor = createExecutor(options);
83
- await executor.executeList(async () => {
84
- const client = executor.getClient();
85
- const params = options.devbox ? { devbox_id: options.devbox } : {};
86
- return executor.fetchFromIterator(client.devboxes.listDiskSnapshots(params), {
87
- limit: PAGE_SIZE,
88
- });
89
- }, () => _jsx(ListSnapshotsUI, { devboxId: options.devbox }), PAGE_SIZE);
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
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from "react";
1
+ /**
2
+ * Get snapshot status command
3
+ */
3
4
  import { getClient } from "../../utils/client.js";
4
- import { Banner } from "../../components/Banner.js";
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
- const executor = createExecutor({ output: options.outputFormat });
33
- await executor.executeAction(async () => {
34
- const client = executor.getClient();
35
- return client.devboxes.diskSnapshots.queryStatus(options.snapshotId);
36
- }, () => _jsx(SnapshotStatusUI, { snapshotId: options.snapshotId }));
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
  }