@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.
- package/README.md +64 -29
- package/dist/cli.js +401 -92
- package/dist/commands/auth.js +12 -11
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +293 -225
- package/dist/commands/blueprint/logs.js +40 -0
- package/dist/commands/blueprint/preview.js +45 -0
- package/dist/commands/devbox/create.js +10 -9
- package/dist/commands/devbox/delete.js +8 -8
- package/dist/commands/devbox/download.js +49 -0
- package/dist/commands/devbox/exec.js +23 -13
- package/dist/commands/devbox/execAsync.js +43 -0
- package/dist/commands/devbox/get.js +37 -0
- package/dist/commands/devbox/getAsync.js +37 -0
- package/dist/commands/devbox/list.js +328 -190
- package/dist/commands/devbox/logs.js +40 -0
- package/dist/commands/devbox/read.js +49 -0
- package/dist/commands/devbox/resume.js +37 -0
- package/dist/commands/devbox/rsync.js +118 -0
- package/dist/commands/devbox/scp.js +122 -0
- package/dist/commands/devbox/shutdown.js +37 -0
- package/dist/commands/devbox/ssh.js +104 -0
- package/dist/commands/devbox/suspend.js +37 -0
- package/dist/commands/devbox/tunnel.js +120 -0
- package/dist/commands/devbox/upload.js +10 -10
- package/dist/commands/devbox/write.js +51 -0
- package/dist/commands/mcp-http.js +37 -0
- package/dist/commands/mcp-install.js +120 -0
- package/dist/commands/mcp.js +30 -0
- package/dist/commands/menu.js +20 -20
- package/dist/commands/object/delete.js +37 -0
- package/dist/commands/object/download.js +88 -0
- package/dist/commands/object/get.js +37 -0
- package/dist/commands/object/list.js +112 -0
- package/dist/commands/object/upload.js +130 -0
- package/dist/commands/snapshot/create.js +12 -11
- package/dist/commands/snapshot/delete.js +8 -8
- package/dist/commands/snapshot/list.js +56 -97
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +4 -4
- package/dist/components/Breadcrumb.js +55 -5
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +315 -178
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +180 -102
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +34 -33
- package/dist/components/MetadataDisplay.js +17 -9
- package/dist/components/OperationsMenu.js +6 -5
- package/dist/components/ResourceActionsMenu.js +117 -0
- package/dist/components/ResourceListView.js +213 -0
- package/dist/components/Spinner.js +5 -4
- package/dist/components/StatusBadge.js +81 -31
- package/dist/components/SuccessMessage.js +4 -3
- package/dist/components/Table.example.js +53 -23
- package/dist/components/Table.js +19 -11
- package/dist/hooks/useCursorPagination.js +125 -0
- package/dist/mcp/server-http.js +416 -0
- package/dist/mcp/server.js +397 -0
- package/dist/utils/CommandExecutor.js +16 -12
- package/dist/utils/client.js +7 -7
- package/dist/utils/config.js +130 -4
- package/dist/utils/interactiveCommand.js +2 -2
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +16 -12
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +4 -4
- 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
|
|
3
|
-
import { render, Box, Text } from
|
|
4
|
-
import Gradient from
|
|
5
|
-
import figures from
|
|
6
|
-
import { getClient } from
|
|
7
|
-
import { Header } from
|
|
8
|
-
import { Banner } from
|
|
9
|
-
import { SpinnerComponent } from
|
|
10
|
-
import { SuccessMessage } from
|
|
11
|
-
import { ErrorMessage } from
|
|
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:
|
|
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
|
|
3
|
-
import { getClient } from
|
|
4
|
-
import { Header } from
|
|
5
|
-
import { SpinnerComponent } from
|
|
6
|
-
import { SuccessMessage } from
|
|
7
|
-
import { ErrorMessage } from
|
|
8
|
-
import { createExecutor } from
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
3
|
-
import figures from
|
|
4
|
-
import chalk from
|
|
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 +
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
40
|
-
const borderBottom = chalk.cyan(
|
|
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(
|
|
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
|
|
3
|
-
import { Box } from
|
|
4
|
-
import BigText from
|
|
5
|
-
import Gradient from
|
|
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
|
});
|