@runloop/rl-cli 0.0.3 → 0.1.1

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 (73) hide show
  1. package/README.md +64 -29
  2. package/dist/cli.js +401 -92
  3. package/dist/commands/auth.js +12 -11
  4. package/dist/commands/blueprint/create.js +108 -0
  5. package/dist/commands/blueprint/get.js +37 -0
  6. package/dist/commands/blueprint/list.js +293 -225
  7. package/dist/commands/blueprint/logs.js +40 -0
  8. package/dist/commands/blueprint/preview.js +45 -0
  9. package/dist/commands/devbox/create.js +10 -9
  10. package/dist/commands/devbox/delete.js +8 -8
  11. package/dist/commands/devbox/download.js +49 -0
  12. package/dist/commands/devbox/exec.js +23 -13
  13. package/dist/commands/devbox/execAsync.js +43 -0
  14. package/dist/commands/devbox/get.js +37 -0
  15. package/dist/commands/devbox/getAsync.js +37 -0
  16. package/dist/commands/devbox/list.js +328 -190
  17. package/dist/commands/devbox/logs.js +40 -0
  18. package/dist/commands/devbox/read.js +49 -0
  19. package/dist/commands/devbox/resume.js +37 -0
  20. package/dist/commands/devbox/rsync.js +118 -0
  21. package/dist/commands/devbox/scp.js +122 -0
  22. package/dist/commands/devbox/shutdown.js +37 -0
  23. package/dist/commands/devbox/ssh.js +104 -0
  24. package/dist/commands/devbox/suspend.js +37 -0
  25. package/dist/commands/devbox/tunnel.js +120 -0
  26. package/dist/commands/devbox/upload.js +10 -10
  27. package/dist/commands/devbox/write.js +51 -0
  28. package/dist/commands/mcp-http.js +37 -0
  29. package/dist/commands/mcp-install.js +120 -0
  30. package/dist/commands/mcp.js +30 -0
  31. package/dist/commands/menu.js +20 -20
  32. package/dist/commands/object/delete.js +37 -0
  33. package/dist/commands/object/download.js +88 -0
  34. package/dist/commands/object/get.js +37 -0
  35. package/dist/commands/object/list.js +112 -0
  36. package/dist/commands/object/upload.js +130 -0
  37. package/dist/commands/snapshot/create.js +12 -11
  38. package/dist/commands/snapshot/delete.js +8 -8
  39. package/dist/commands/snapshot/list.js +56 -97
  40. package/dist/commands/snapshot/status.js +37 -0
  41. package/dist/components/ActionsPopup.js +16 -13
  42. package/dist/components/Banner.js +4 -4
  43. package/dist/components/Breadcrumb.js +55 -5
  44. package/dist/components/DetailView.js +7 -4
  45. package/dist/components/DevboxActionsMenu.js +315 -178
  46. package/dist/components/DevboxCard.js +15 -14
  47. package/dist/components/DevboxCreatePage.js +147 -113
  48. package/dist/components/DevboxDetailPage.js +180 -102
  49. package/dist/components/ErrorMessage.js +5 -4
  50. package/dist/components/Header.js +4 -3
  51. package/dist/components/MainMenu.js +34 -33
  52. package/dist/components/MetadataDisplay.js +17 -9
  53. package/dist/components/OperationsMenu.js +6 -5
  54. package/dist/components/ResourceActionsMenu.js +117 -0
  55. package/dist/components/ResourceListView.js +213 -0
  56. package/dist/components/Spinner.js +5 -4
  57. package/dist/components/StatusBadge.js +81 -31
  58. package/dist/components/SuccessMessage.js +4 -3
  59. package/dist/components/Table.example.js +53 -23
  60. package/dist/components/Table.js +19 -11
  61. package/dist/hooks/useCursorPagination.js +125 -0
  62. package/dist/mcp/server-http.js +416 -0
  63. package/dist/mcp/server.js +397 -0
  64. package/dist/utils/CommandExecutor.js +16 -12
  65. package/dist/utils/client.js +7 -7
  66. package/dist/utils/config.js +130 -4
  67. package/dist/utils/interactiveCommand.js +2 -2
  68. package/dist/utils/output.js +17 -17
  69. package/dist/utils/ssh.js +160 -0
  70. package/dist/utils/sshSession.js +16 -12
  71. package/dist/utils/theme.js +22 -0
  72. package/dist/utils/url.js +4 -4
  73. package/package.json +29 -4
@@ -0,0 +1,112 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React from "react";
3
+ import { Box, Text } from "ink";
4
+ import { getClient } from "../../utils/client.js";
5
+ import { Banner } from "../../components/Banner.js";
6
+ import { SpinnerComponent } from "../../components/Spinner.js";
7
+ import { ErrorMessage } from "../../components/ErrorMessage.js";
8
+ import { createExecutor } from "../../utils/CommandExecutor.js";
9
+ import { colors } from "../../utils/theme.js";
10
+ import { Table } from "../../components/Table.js";
11
+ const ListObjectsUI = ({ options }) => {
12
+ const [loading, setLoading] = React.useState(true);
13
+ const [result, setResult] = React.useState(null);
14
+ const [error, setError] = React.useState(null);
15
+ React.useEffect(() => {
16
+ const listObjects = async () => {
17
+ try {
18
+ const client = getClient();
19
+ const params = {};
20
+ if (options.limit)
21
+ params.limit = options.limit;
22
+ if (options.startingAfter)
23
+ params.startingAfter = options.startingAfter;
24
+ if (options.name)
25
+ params.name = options.name;
26
+ if (options.contentType)
27
+ params.contentType = options.contentType;
28
+ if (options.state)
29
+ params.state = options.state;
30
+ if (options.search)
31
+ params.search = options.search;
32
+ if (options.public)
33
+ params.isPublic = true;
34
+ const objects = options.public
35
+ ? await client.objects.listPublic(params)
36
+ : await client.objects.list(params);
37
+ setResult(objects);
38
+ }
39
+ catch (err) {
40
+ setError(err);
41
+ }
42
+ finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+ listObjects();
47
+ }, [options]);
48
+ const formatSize = (bytes) => {
49
+ if (bytes < 1024)
50
+ return `${bytes} B`;
51
+ if (bytes < 1024 * 1024)
52
+ return `${(bytes / 1024).toFixed(1)} KB`;
53
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
54
+ };
55
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Fetching objects..." }), result && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.primary, children: "Objects:" }), result.objects && result.objects.length > 0 ? (_jsx(Table, { data: result.objects, keyExtractor: (item) => item.id, columns: [
56
+ {
57
+ key: "id",
58
+ label: "ID",
59
+ width: 20,
60
+ render: (row) => (_jsx(Text, { color: colors.text, children: row.id })),
61
+ },
62
+ {
63
+ key: "name",
64
+ label: "Name",
65
+ width: 30,
66
+ render: (row) => (_jsx(Text, { color: colors.text, children: row.name })),
67
+ },
68
+ {
69
+ key: "type",
70
+ label: "Type",
71
+ width: 15,
72
+ render: (row) => (_jsx(Text, { color: colors.text, children: row.content_type })),
73
+ },
74
+ {
75
+ key: "state",
76
+ label: "State",
77
+ width: 15,
78
+ render: (row) => (_jsx(Text, { color: colors.text, children: row.state })),
79
+ },
80
+ {
81
+ key: "size",
82
+ label: "Size",
83
+ width: 10,
84
+ render: (row) => (_jsx(Text, { color: colors.text, children: row.size_bytes ? formatSize(row.size_bytes) : "N/A" })),
85
+ },
86
+ ] })) : (_jsx(Text, { color: colors.textDim, children: "No objects found" }))] })), error && _jsx(ErrorMessage, { message: "Failed to list objects", error: error })] }));
87
+ };
88
+ export async function listObjects(options) {
89
+ const executor = createExecutor({ output: options.output });
90
+ await executor.executeList(async () => {
91
+ const client = executor.getClient();
92
+ const params = {};
93
+ if (options.limit)
94
+ params.limit = options.limit;
95
+ if (options.startingAfter)
96
+ params.startingAfter = options.startingAfter;
97
+ if (options.name)
98
+ params.name = options.name;
99
+ if (options.contentType)
100
+ params.contentType = options.contentType;
101
+ if (options.state)
102
+ params.state = options.state;
103
+ if (options.search)
104
+ params.search = options.search;
105
+ if (options.public)
106
+ params.isPublic = true;
107
+ const objects = options.public
108
+ ? await client.objects.listPublic(params)
109
+ : await client.objects.list(params);
110
+ return objects.objects || [];
111
+ }, () => _jsx(ListObjectsUI, { options: options }), options.limit || 20);
112
+ }
@@ -0,0 +1,130 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ 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
+ import { readFile, stat } from "fs/promises";
10
+ import { extname } from "path";
11
+ const UploadObjectUI = ({ path, name, contentType }) => {
12
+ const [loading, setLoading] = React.useState(true);
13
+ const [result, setResult] = React.useState(null);
14
+ const [error, setError] = React.useState(null);
15
+ React.useEffect(() => {
16
+ const uploadObject = async () => {
17
+ try {
18
+ const client = getClient();
19
+ // Check if file exists
20
+ const stats = await stat(path);
21
+ const fileBuffer = await readFile(path);
22
+ // Auto-detect content type if not provided
23
+ let detectedContentType = contentType;
24
+ if (!detectedContentType) {
25
+ const ext = extname(path).toLowerCase();
26
+ const contentTypeMap = {
27
+ ".txt": "text",
28
+ ".html": "text",
29
+ ".css": "text",
30
+ ".js": "text",
31
+ ".json": "text",
32
+ ".yaml": "text",
33
+ ".yml": "text",
34
+ ".md": "text",
35
+ ".gz": "gzip",
36
+ ".tar": "tar",
37
+ ".tgz": "tgz",
38
+ ".tar.gz": "tgz",
39
+ };
40
+ detectedContentType = contentTypeMap[ext] || "unspecified";
41
+ }
42
+ // Step 1: Create the object
43
+ const createResponse = await client.objects.create({
44
+ name,
45
+ content_type: detectedContentType,
46
+ });
47
+ // Step 2: Upload the file
48
+ const uploadResponse = await fetch(createResponse.upload_url, {
49
+ method: "PUT",
50
+ body: fileBuffer,
51
+ headers: {
52
+ "Content-Length": fileBuffer.length.toString(),
53
+ },
54
+ });
55
+ if (!uploadResponse.ok) {
56
+ throw new Error(`Upload failed: HTTP ${uploadResponse.status}`);
57
+ }
58
+ // Step 3: Complete the upload
59
+ await client.objects.complete(createResponse.id);
60
+ setResult({
61
+ id: createResponse.id,
62
+ name,
63
+ contentType: detectedContentType,
64
+ size: stats.size,
65
+ });
66
+ }
67
+ catch (err) {
68
+ setError(err);
69
+ }
70
+ finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+ uploadObject();
75
+ }, [path, name, contentType]);
76
+ return (_jsxs(_Fragment, { children: [_jsx(Banner, {}), loading && _jsx(SpinnerComponent, { message: "Uploading object..." }), result && (_jsx(SuccessMessage, { message: "Object uploaded successfully", details: `ID: ${result.id}\nName: ${result.name}\nType: ${result.contentType}\nSize: ${result.size} bytes` })), error && (_jsx(ErrorMessage, { message: "Failed to upload object", error: error }))] }));
77
+ };
78
+ export async function uploadObject(options) {
79
+ const executor = createExecutor({ output: options.output });
80
+ await executor.executeAction(async () => {
81
+ const client = executor.getClient();
82
+ // Check if file exists
83
+ const stats = await stat(options.path);
84
+ const fileBuffer = await readFile(options.path);
85
+ // Auto-detect content type if not provided
86
+ let detectedContentType = options.contentType;
87
+ if (!detectedContentType) {
88
+ const ext = extname(options.path).toLowerCase();
89
+ const contentTypeMap = {
90
+ ".txt": "text",
91
+ ".html": "text",
92
+ ".css": "text",
93
+ ".js": "text",
94
+ ".json": "text",
95
+ ".yaml": "text",
96
+ ".yml": "text",
97
+ ".md": "text",
98
+ ".gz": "gzip",
99
+ ".tar": "tar",
100
+ ".tgz": "tgz",
101
+ ".tar.gz": "tgz",
102
+ };
103
+ detectedContentType = contentTypeMap[ext] || "unspecified";
104
+ }
105
+ // Step 1: Create the object
106
+ const createResponse = await client.objects.create({
107
+ name: options.name,
108
+ content_type: detectedContentType,
109
+ });
110
+ // Step 2: Upload the file
111
+ const uploadResponse = await fetch(createResponse.upload_url, {
112
+ method: "PUT",
113
+ body: fileBuffer,
114
+ headers: {
115
+ "Content-Length": fileBuffer.length.toString(),
116
+ },
117
+ });
118
+ if (!uploadResponse.ok) {
119
+ throw new Error(`Upload failed: HTTP ${uploadResponse.status}`);
120
+ }
121
+ // Step 3: Complete the upload
122
+ await client.objects.complete(createResponse.id);
123
+ return {
124
+ id: createResponse.id,
125
+ name: options.name,
126
+ contentType: detectedContentType,
127
+ size: stats.size,
128
+ };
129
+ }, () => (_jsx(UploadObjectUI, { path: options.path, name: options.name, contentType: options.contentType })));
130
+ }
@@ -1,14 +1,15 @@
1
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';
6
- 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';
2
+ import React from "react";
3
+ import { render, Box, Text } from "ink";
4
+ import Gradient from "ink-gradient";
5
+ import figures from "figures";
6
+ 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";
12
13
  const CreateSnapshotUI = ({ devboxId, name }) => {
13
14
  const [loading, setLoading] = React.useState(true);
14
15
  const [result, setResult] = React.useState(null);
@@ -31,7 +32,7 @@ const CreateSnapshotUI = ({ devboxId, name }) => {
31
32
  };
32
33
  create();
33
34
  }, []);
34
- 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: "blue", paddingX: 3, paddingY: 1, marginY: 1, flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "cyan", bold: true, children: [figures.info, " Configuration"] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Devbox ID: "] }), _jsx(Text, { color: "white", children: devboxId })] }), name && (_jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.pointer, " Name: "] }), _jsx(Text, { color: "white", 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: "green", 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: "gray", children: [figures.tick, " View snapshots: "] }), _jsx(Text, { color: "cyan", children: "rln snapshot list" })] }), _jsxs(Box, { children: [_jsxs(Text, { color: "gray", children: [figures.tick, " Create devbox from snapshot: "] }), _jsxs(Text, { color: "cyan", children: ["rln devbox create -t ", result.id] })] })] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to create snapshot", error: error })] }));
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 }))] }));
35
36
  };
36
37
  export async function createSnapshot(devboxId, options) {
37
38
  console.clear();
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- 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';
2
+ import React from "react";
3
+ 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
9
  const DeleteSnapshotUI = ({ id }) => {
10
10
  const [loading, setLoading] = React.useState(true);
11
11
  const [success, setSuccess] = React.useState(false);
@@ -26,7 +26,7 @@ const DeleteSnapshotUI = ({ id }) => {
26
26
  };
27
27
  deleteSnapshot();
28
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 })] }));
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
30
  };
31
31
  export async function deleteSnapshot(id, options = {}) {
32
32
  const executor = createExecutor(options);
@@ -1,55 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Box, Text, useInput, useStdout, useApp } from 'ink';
4
- import figures from 'figures';
5
- import { getClient } from '../../utils/client.js';
6
- import { SpinnerComponent } from '../../components/Spinner.js';
7
- import { ErrorMessage } from '../../components/ErrorMessage.js';
8
- import { StatusBadge } from '../../components/StatusBadge.js';
9
- import { Breadcrumb } from '../../components/Breadcrumb.js';
10
- import { Table, createTextColumn, createComponentColumn } from '../../components/Table.js';
11
- import { createExecutor } from '../../utils/CommandExecutor.js';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useStdout } from "ink";
3
+ 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";
7
+ import { colors } from "../../utils/theme.js";
12
8
  const PAGE_SIZE = 10;
13
9
  const MAX_FETCH = 100;
14
- // Format time ago
15
- const formatTimeAgo = (timestamp) => {
16
- const seconds = Math.floor((Date.now() - timestamp) / 1000);
17
- if (seconds < 60)
18
- return `${seconds}s ago`;
19
- const minutes = Math.floor(seconds / 60);
20
- if (minutes < 60)
21
- return `${minutes}m ago`;
22
- const hours = Math.floor(minutes / 60);
23
- if (hours < 24)
24
- return `${hours}h ago`;
25
- const days = Math.floor(hours / 24);
26
- if (days < 30)
27
- return `${days}d ago`;
28
- const months = Math.floor(days / 30);
29
- if (months < 12)
30
- return `${months}mo ago`;
31
- const years = Math.floor(months / 12);
32
- return `${years}y ago`;
33
- };
34
10
  const ListSnapshotsUI = ({ devboxId, onBack, onExit }) => {
35
11
  const { stdout } = useStdout();
36
- const { exit: inkExit } = useApp();
37
- const [loading, setLoading] = React.useState(true);
38
- const [snapshots, setSnapshots] = React.useState([]);
39
- const [error, setError] = React.useState(null);
40
- const [currentPage, setCurrentPage] = React.useState(0);
41
- const [selectedIndex, setSelectedIndex] = React.useState(0);
42
12
  // Calculate responsive column widths
43
13
  const terminalWidth = stdout?.columns || 120;
44
14
  const showDevboxId = terminalWidth >= 100 && !devboxId; // Hide devbox column if filtering by devbox
45
15
  const showFullId = terminalWidth >= 80;
16
+ const statusIconWidth = 2;
17
+ const statusTextWidth = 10;
46
18
  const idWidth = 25;
47
19
  const nameWidth = terminalWidth >= 120 ? 30 : 25;
48
20
  const devboxWidth = 15;
49
21
  const timeWidth = 20;
50
- React.useEffect(() => {
51
- const list = async () => {
52
- try {
22
+ return (_jsx(ResourceListView, { config: {
23
+ resourceName: "Snapshot",
24
+ resourceNamePlural: "Snapshots",
25
+ fetchResources: async () => {
53
26
  const client = getClient();
54
27
  const allSnapshots = [];
55
28
  let count = 0;
@@ -57,65 +30,51 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit }) => {
57
30
  for await (const snapshot of client.devboxes.listDiskSnapshots(params)) {
58
31
  allSnapshots.push(snapshot);
59
32
  count++;
60
- if (count >= MAX_FETCH) {
33
+ if (count >= MAX_FETCH)
61
34
  break;
62
- }
63
35
  }
64
- setSnapshots(allSnapshots);
65
- }
66
- catch (err) {
67
- setError(err);
68
- }
69
- finally {
70
- setLoading(false);
71
- }
72
- };
73
- list();
74
- }, [devboxId]);
75
- useInput((input, key) => {
76
- const pageSnapshots = currentSnapshots.length;
77
- if (key.upArrow && selectedIndex > 0) {
78
- setSelectedIndex(selectedIndex - 1);
79
- }
80
- else if (key.downArrow && selectedIndex < pageSnapshots - 1) {
81
- setSelectedIndex(selectedIndex + 1);
82
- }
83
- else if ((input === 'n' || key.rightArrow) && currentPage < totalPages - 1) {
84
- setCurrentPage(currentPage + 1);
85
- setSelectedIndex(0);
86
- }
87
- else if ((input === 'p' || key.leftArrow) && currentPage > 0) {
88
- setCurrentPage(currentPage - 1);
89
- setSelectedIndex(0);
90
- }
91
- else if (key.escape) {
92
- if (onBack) {
93
- onBack();
94
- }
95
- else if (onExit) {
96
- onExit();
97
- }
98
- else {
99
- inkExit();
100
- }
101
- }
102
- });
103
- const totalPages = Math.ceil(snapshots.length / PAGE_SIZE);
104
- const startIndex = currentPage * PAGE_SIZE;
105
- const endIndex = Math.min(startIndex + PAGE_SIZE, snapshots.length);
106
- const currentSnapshots = snapshots.slice(startIndex, endIndex);
107
- const ready = snapshots.filter((s) => s.status === 'ready').length;
108
- const pending = snapshots.filter((s) => s.status !== 'ready').length;
109
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
110
- { label: 'Snapshots', active: !devboxId },
111
- ...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
112
- ] }), loading && _jsx(SpinnerComponent, { message: "Loading snapshots..." }), !loading && !error && snapshots.length === 0 && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: figures.info }), _jsx(Text, { children: " No snapshots found. Try: " }), _jsx(Text, { color: "cyan", bold: true, children: "rln snapshot create <devbox-id>" })] })), !loading && !error && snapshots.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: "green", children: [figures.tick, " ", ready] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "yellow", children: [figures.ellipsis, " ", pending] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: [figures.hamburger, " ", snapshots.length, snapshots.length >= MAX_FETCH && '+'] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: " \u2022 " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["Page ", currentPage + 1, "/", totalPages] })] }))] }), _jsx(Table, { data: currentSnapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, columns: [
113
- createComponentColumn('status', 'Status', (snapshot) => _jsx(StatusBadge, { status: snapshot.status, showText: false }), { width: 2 }),
114
- createTextColumn('id', 'ID', (snapshot) => showFullId ? snapshot.id : snapshot.id.slice(0, 13), { width: showFullId ? idWidth : 15, color: 'gray', dimColor: true, bold: false }),
115
- createTextColumn('name', 'Name', (snapshot) => snapshot.name || '(unnamed)', { width: nameWidth }),
116
- createTextColumn('devbox', 'Devbox', (snapshot) => snapshot.devbox_id || '', { width: devboxWidth, color: 'cyan', dimColor: true, bold: false, visible: showDevboxId }),
117
- createTextColumn('created', 'Created', (snapshot) => snapshot.created_at ? formatTimeAgo(new Date(snapshot.created_at).getTime()) : '', { width: timeWidth, color: 'gray', dimColor: true, bold: false }),
118
- ] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022"] }), totalPages > 1 && (_jsxs(Text, { color: "gray", dimColor: true, children: [' ', figures.arrowLeft, figures.arrowRight, " Page \u2022"] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "[Esc] Back"] })] })] })), error && _jsx(ErrorMessage, { message: "Failed to list snapshots", error: error })] }));
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
+ } }));
119
78
  };
120
79
  // Export the UI component for use in the main menu
121
80
  export { ListSnapshotsUI };
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from "react";
3
+ 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
+ };
31
+ 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 }));
37
+ }
@@ -1,42 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
3
- import figures from 'figures';
4
- import chalk from 'chalk';
2
+ import { Box, Text } from "ink";
3
+ import figures from "figures";
4
+ import chalk from "chalk";
5
5
  export const ActionsPopup = ({ devbox, operations, selectedOperation, onClose, }) => {
6
6
  // Calculate the maximum width needed
7
- const maxLabelLength = Math.max(...operations.map(op => op.label.length));
7
+ const maxLabelLength = Math.max(...operations.map((op) => op.label.length));
8
8
  const contentWidth = maxLabelLength + 12; // Content + icon + pointer + shortcuts
9
9
  // Strip ANSI codes to get real length, then pad
10
- const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, '');
10
+ const stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, "");
11
11
  const bgLine = (content) => {
12
12
  const cleanLength = stripAnsi(content).length;
13
13
  const padding = Math.max(0, contentWidth - cleanLength);
14
- return chalk.bgBlack(content + ' '.repeat(padding));
14
+ return chalk.bgBlack(content + " ".repeat(padding));
15
15
  };
16
16
  // Render all lines with background
17
17
  const lines = [
18
18
  bgLine(chalk.cyan.bold(` ${figures.play} Quick Actions`)),
19
- chalk.bgBlack(' '.repeat(contentWidth)),
19
+ chalk.bgBlack(" ".repeat(contentWidth)),
20
20
  ...operations.map((op, index) => {
21
21
  const isSelected = index === selectedOperation;
22
- const pointer = isSelected ? figures.pointer : ' ';
22
+ const pointer = isSelected ? figures.pointer : " ";
23
23
  const content = ` ${pointer} ${op.icon} ${op.label} [${op.shortcut}]`;
24
24
  let styled;
25
25
  if (isSelected) {
26
26
  const colorFn = chalk[op.color];
27
- styled = typeof colorFn === 'function' ? colorFn.bold(content) : chalk.white.bold(content);
27
+ styled =
28
+ typeof colorFn === "function"
29
+ ? colorFn.bold(content)
30
+ : chalk.white.bold(content);
28
31
  }
29
32
  else {
30
33
  styled = chalk.gray(content);
31
34
  }
32
35
  return bgLine(styled);
33
36
  }),
34
- chalk.bgBlack(' '.repeat(contentWidth)),
37
+ chalk.bgBlack(" ".repeat(contentWidth)),
35
38
  bgLine(chalk.gray.dim(` ${figures.arrowUp}${figures.arrowDown} Nav • [Enter]`)),
36
39
  bgLine(chalk.gray.dim(` [Esc] Close`)),
37
40
  ];
38
41
  // Draw custom border with background to fill gaps
39
- const borderTop = chalk.cyan('' + ''.repeat(contentWidth) + '');
40
- const borderBottom = chalk.cyan('' + ''.repeat(contentWidth) + '');
41
- return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan(''), line, chalk.cyan('')] }, i))), _jsx(Text, { children: borderBottom })] }) }));
42
+ const borderTop = chalk.cyan("" + "".repeat(contentWidth) + "");
43
+ const borderBottom = chalk.cyan("" + "".repeat(contentWidth) + "");
44
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: borderTop }), lines.map((line, i) => (_jsxs(Text, { children: [chalk.cyan(""), line, chalk.cyan("")] }, i))), _jsx(Text, { children: borderBottom })] }) }));
42
45
  };
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Box } from 'ink';
4
- import BigText from 'ink-big-text';
5
- import Gradient from 'ink-gradient';
2
+ import React from "react";
3
+ import { Box } from "ink";
4
+ import BigText from "ink-big-text";
5
+ import Gradient from "ink-gradient";
6
6
  export const Banner = React.memo(() => {
7
7
  return (_jsx(Box, { flexDirection: "column", alignItems: "flex-start", children: _jsx(Gradient, { name: "vice", children: _jsx(BigText, { text: "RUNLOOP.ai", font: "simple3d" }) }) }));
8
8
  });