@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
|
@@ -1,55 +1,85 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from
|
|
3
|
-
import { Table, createTextColumn, createComponentColumn } from
|
|
4
|
-
import { StatusBadge } from
|
|
5
|
-
import figures from
|
|
6
|
-
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Table, createTextColumn, createComponentColumn } from "./Table.js";
|
|
4
|
+
import { StatusBadge } from "./StatusBadge.js";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { colors } from "../utils/theme.js";
|
|
7
|
+
function BlueprintsTable({ blueprints, selectedIndex, terminalWidth, }) {
|
|
7
8
|
// Responsive column widths
|
|
8
9
|
const showDescription = terminalWidth >= 120;
|
|
9
10
|
const showFullId = terminalWidth >= 80;
|
|
10
11
|
return (_jsx(Table, { data: blueprints, keyExtractor: (bp) => bp.id, selectedIndex: selectedIndex, columns: [
|
|
11
12
|
// Status badge column
|
|
12
|
-
createComponentColumn(
|
|
13
|
+
createComponentColumn("status", "Status", (bp) => _jsx(StatusBadge, { status: bp.status, showText: false }), { width: 2 }),
|
|
13
14
|
// ID column (responsive)
|
|
14
|
-
createTextColumn(
|
|
15
|
+
createTextColumn("id", "ID", (bp) => (showFullId ? bp.id : bp.id.slice(0, 13)), {
|
|
16
|
+
width: showFullId ? 25 : 15,
|
|
17
|
+
color: colors.textDim,
|
|
18
|
+
dimColor: true,
|
|
19
|
+
bold: false,
|
|
20
|
+
}),
|
|
15
21
|
// Name column
|
|
16
|
-
createTextColumn(
|
|
22
|
+
createTextColumn("name", "Name", (bp) => bp.name || "(unnamed)", { width: 30 }),
|
|
17
23
|
// Description column (optional)
|
|
18
|
-
createTextColumn(
|
|
24
|
+
createTextColumn("description", "Description", (bp) => bp.description || "", {
|
|
25
|
+
width: 40,
|
|
26
|
+
color: colors.textDim,
|
|
27
|
+
dimColor: true,
|
|
28
|
+
bold: false,
|
|
29
|
+
visible: showDescription,
|
|
30
|
+
}),
|
|
19
31
|
// Created time column
|
|
20
|
-
createTextColumn(
|
|
21
|
-
], emptyState: _jsx(Box, { children: _jsxs(Text, { color:
|
|
32
|
+
createTextColumn("created", "Created", (bp) => new Date(bp.created_at).toLocaleDateString(), { width: 15, color: colors.textDim, dimColor: true, bold: false }),
|
|
33
|
+
], emptyState: _jsx(Box, { children: _jsxs(Text, { color: colors.warning, children: [figures.info, " No blueprints found"] }) }) }));
|
|
22
34
|
}
|
|
23
|
-
function SnapshotsTable({ snapshots, selectedIndex, terminalWidth }) {
|
|
35
|
+
function SnapshotsTable({ snapshots, selectedIndex, terminalWidth, }) {
|
|
24
36
|
// Responsive column widths
|
|
25
37
|
const showSize = terminalWidth >= 100;
|
|
26
38
|
const showFullId = terminalWidth >= 80;
|
|
27
39
|
return (_jsx(Table, { data: snapshots, keyExtractor: (snap) => snap.id, selectedIndex: selectedIndex, columns: [
|
|
28
40
|
// Status badge column
|
|
29
|
-
createComponentColumn(
|
|
41
|
+
createComponentColumn("status", "Status", (snap) => _jsx(StatusBadge, { status: snap.status, showText: false }), { width: 2 }),
|
|
30
42
|
// ID column (responsive)
|
|
31
|
-
createTextColumn(
|
|
43
|
+
createTextColumn("id", "ID", (snap) => (showFullId ? snap.id : snap.id.slice(0, 13)), {
|
|
44
|
+
width: showFullId ? 25 : 15,
|
|
45
|
+
color: colors.textDim,
|
|
46
|
+
dimColor: true,
|
|
47
|
+
bold: false,
|
|
48
|
+
}),
|
|
32
49
|
// Name column
|
|
33
|
-
createTextColumn(
|
|
50
|
+
createTextColumn("name", "Name", (snap) => snap.name || "(unnamed)", { width: 25 }),
|
|
34
51
|
// Devbox ID column
|
|
35
|
-
createTextColumn(
|
|
52
|
+
createTextColumn("devbox", "Devbox", (snap) => snap.devbox_id.slice(0, 13), {
|
|
53
|
+
width: 15,
|
|
54
|
+
color: colors.primary,
|
|
55
|
+
dimColor: true,
|
|
56
|
+
bold: false,
|
|
57
|
+
}),
|
|
36
58
|
// Size column (optional)
|
|
37
|
-
createTextColumn(
|
|
59
|
+
createTextColumn("size", "Size", (snap) => (snap.size_gb ? `${snap.size_gb.toFixed(1)}GB` : ""), {
|
|
60
|
+
width: 10,
|
|
61
|
+
color: colors.warning,
|
|
62
|
+
dimColor: true,
|
|
63
|
+
bold: false,
|
|
64
|
+
visible: showSize,
|
|
65
|
+
}),
|
|
38
66
|
// Created time column
|
|
39
|
-
createTextColumn(
|
|
40
|
-
], emptyState: _jsx(Box, { children: _jsxs(Text, { color:
|
|
67
|
+
createTextColumn("created", "Created", (snap) => new Date(snap.created_at).toLocaleDateString(), { width: 15, color: colors.textDim, dimColor: true, bold: false }),
|
|
68
|
+
], emptyState: _jsx(Box, { children: _jsxs(Text, { color: colors.warning, children: [figures.info, " No snapshots found"] }) }) }));
|
|
41
69
|
}
|
|
42
70
|
// ============================================================================
|
|
43
71
|
// EXAMPLE 3: Custom Column with Complex Rendering
|
|
44
72
|
// ============================================================================
|
|
45
73
|
function CustomComplexColumn() {
|
|
46
74
|
const data = [
|
|
47
|
-
{ id:
|
|
48
|
-
{ id:
|
|
75
|
+
{ id: "1", name: "Item 1", tags: ["tag1", "tag2"] },
|
|
76
|
+
{ id: "2", name: "Item 2", tags: ["tag3"] },
|
|
49
77
|
];
|
|
50
78
|
return (_jsx(Table, { data: data, keyExtractor: (item) => item.id, selectedIndex: 0, columns: [
|
|
51
|
-
createTextColumn(
|
|
79
|
+
createTextColumn("name", "Name", (item) => item.name, {
|
|
80
|
+
width: 20,
|
|
81
|
+
}),
|
|
52
82
|
// Custom component column with complex rendering
|
|
53
|
-
createComponentColumn(
|
|
83
|
+
createComponentColumn("tags", "Tags", (item, index, isSelected) => (_jsx(Box, { width: 30, children: _jsx(Text, { color: isSelected ? colors.primary : colors.info, dimColor: true, children: item.tags.map((tag) => `[${tag}]`).join(" ") }) })), { width: 30 }),
|
|
54
84
|
] }));
|
|
55
85
|
}
|
package/dist/components/Table.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from
|
|
3
|
-
import { Box, Text } from
|
|
4
|
-
import figures from
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text } from "ink";
|
|
4
|
+
import figures from "figures";
|
|
5
|
+
import { colors } from "../utils/theme.js";
|
|
5
6
|
/**
|
|
6
7
|
* Reusable table component for displaying lists of data with optional selection
|
|
7
8
|
* Designed to be responsive and work across devboxes, blueprints, and snapshots
|
|
@@ -11,11 +12,11 @@ export function Table({ data, columns, selectedIndex = -1, showSelection = true,
|
|
|
11
12
|
return _jsx(_Fragment, { children: emptyState });
|
|
12
13
|
}
|
|
13
14
|
// Filter visible columns
|
|
14
|
-
const visibleColumns = columns.filter(col => col.visible !== false);
|
|
15
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color:
|
|
15
|
+
const visibleColumns = columns.filter((col) => col.visible !== false);
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: colors.primary, bold: true, children: ["\u256D\u2500 ", title, " ", "─".repeat(Math.max(0, 10)), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", borderStyle: title ? "single" : "round", borderColor: colors.border, paddingX: 1, children: [_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => (_jsx(Text, { bold: true, dimColor: true, children: column.label.slice(0, column.width).padEnd(column.width, " ") }, `header-${column.key}`)))] }), data.map((row, index) => {
|
|
16
17
|
const isSelected = index === selectedIndex;
|
|
17
18
|
const rowKey = keyExtractor(row);
|
|
18
|
-
return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ?
|
|
19
|
+
return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}`)))] }, rowKey));
|
|
19
20
|
})] })] }));
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
@@ -30,13 +31,20 @@ export function createTextColumn(key, label, getValue, options) {
|
|
|
30
31
|
render: (row, index, isSelected) => {
|
|
31
32
|
const value = getValue(row);
|
|
32
33
|
const width = options?.width || 20;
|
|
33
|
-
const color = options?.color || (isSelected ?
|
|
34
|
+
const color = options?.color || (isSelected ? colors.text : colors.text);
|
|
34
35
|
const bold = options?.bold !== undefined ? options.bold : isSelected;
|
|
35
36
|
const dimColor = options?.dimColor || false;
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
// Truncate and add ellipsis if text is too long
|
|
38
|
+
let truncated;
|
|
39
|
+
if (value.length > width) {
|
|
40
|
+
// Reserve space for ellipsis if truncating
|
|
41
|
+
truncated = value.slice(0, width - 1) + "…";
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
truncated = value;
|
|
45
|
+
}
|
|
46
|
+
const padded = truncated.padEnd(width, " ");
|
|
47
|
+
return (_jsx(Text, { color: isSelected ? colors.text : color, bold: bold, dimColor: dimColor, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
40
48
|
},
|
|
41
49
|
};
|
|
42
50
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export function useCursorPagination(config) {
|
|
3
|
+
const [items, setItems] = React.useState([]);
|
|
4
|
+
const [loading, setLoading] = React.useState(true);
|
|
5
|
+
const [error, setError] = React.useState(null);
|
|
6
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
7
|
+
const [totalCount, setTotalCount] = React.useState(0);
|
|
8
|
+
const [hasMore, setHasMore] = React.useState(false);
|
|
9
|
+
const [refreshing, setRefreshing] = React.useState(false);
|
|
10
|
+
// Cache for page data and cursors
|
|
11
|
+
const pageCache = React.useRef(new Map());
|
|
12
|
+
const lastIdCache = React.useRef(new Map());
|
|
13
|
+
const fetchData = React.useCallback(async (isInitialLoad = false) => {
|
|
14
|
+
try {
|
|
15
|
+
if (isInitialLoad) {
|
|
16
|
+
setRefreshing(true);
|
|
17
|
+
}
|
|
18
|
+
setLoading(true);
|
|
19
|
+
// Check cache first (skip on refresh)
|
|
20
|
+
if (!isInitialLoad && pageCache.current.has(currentPage)) {
|
|
21
|
+
setItems(pageCache.current.get(currentPage) || []);
|
|
22
|
+
setLoading(false);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const pageItems = [];
|
|
26
|
+
// Get starting_at cursor from previous page's last ID
|
|
27
|
+
const startingAt = currentPage > 0
|
|
28
|
+
? lastIdCache.current.get(currentPage - 1)
|
|
29
|
+
: undefined;
|
|
30
|
+
// Build query params
|
|
31
|
+
const queryParams = {
|
|
32
|
+
limit: config.pageSize,
|
|
33
|
+
...config.queryParams,
|
|
34
|
+
};
|
|
35
|
+
if (startingAt) {
|
|
36
|
+
queryParams.starting_at = startingAt;
|
|
37
|
+
}
|
|
38
|
+
// Fetch the page
|
|
39
|
+
const result = await config.fetchPage(queryParams);
|
|
40
|
+
// Extract items (handle both array response and paginated response)
|
|
41
|
+
const fetchedItems = Array.isArray(result) ? result : result.items;
|
|
42
|
+
pageItems.push(...fetchedItems.slice(0, config.pageSize));
|
|
43
|
+
// Update pagination metadata
|
|
44
|
+
if (!Array.isArray(result)) {
|
|
45
|
+
setTotalCount(result.total_count || pageItems.length);
|
|
46
|
+
setHasMore(result.has_more || false);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
setTotalCount(pageItems.length);
|
|
50
|
+
setHasMore(false);
|
|
51
|
+
}
|
|
52
|
+
// Cache the page data and last ID
|
|
53
|
+
if (pageItems.length > 0) {
|
|
54
|
+
pageCache.current.set(currentPage, pageItems);
|
|
55
|
+
lastIdCache.current.set(currentPage, config.getItemId(pageItems[pageItems.length - 1]));
|
|
56
|
+
}
|
|
57
|
+
// Update items for current page
|
|
58
|
+
setItems(pageItems);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
setError(err);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
setLoading(false);
|
|
65
|
+
if (isInitialLoad) {
|
|
66
|
+
setTimeout(() => setRefreshing(false), 300);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, [currentPage, config]);
|
|
70
|
+
// Initial load and page changes
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
fetchData(true);
|
|
73
|
+
}, [fetchData, currentPage]);
|
|
74
|
+
// Auto-refresh
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (!config.refreshInterval || config.refreshInterval <= 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const interval = setInterval(() => {
|
|
80
|
+
// Clear cache on refresh
|
|
81
|
+
pageCache.current.clear();
|
|
82
|
+
lastIdCache.current.clear();
|
|
83
|
+
fetchData(false);
|
|
84
|
+
}, config.refreshInterval);
|
|
85
|
+
return () => clearInterval(interval);
|
|
86
|
+
}, [config.refreshInterval, fetchData]);
|
|
87
|
+
const nextPage = React.useCallback(() => {
|
|
88
|
+
if (!loading && hasMore) {
|
|
89
|
+
setCurrentPage((prev) => prev + 1);
|
|
90
|
+
}
|
|
91
|
+
}, [loading, hasMore]);
|
|
92
|
+
const prevPage = React.useCallback(() => {
|
|
93
|
+
if (!loading && currentPage > 0) {
|
|
94
|
+
setCurrentPage((prev) => prev - 1);
|
|
95
|
+
}
|
|
96
|
+
}, [loading, currentPage]);
|
|
97
|
+
const goToPage = React.useCallback((page) => {
|
|
98
|
+
if (!loading && page >= 0) {
|
|
99
|
+
setCurrentPage(page);
|
|
100
|
+
}
|
|
101
|
+
}, [loading]);
|
|
102
|
+
const refresh = React.useCallback(() => {
|
|
103
|
+
pageCache.current.clear();
|
|
104
|
+
lastIdCache.current.clear();
|
|
105
|
+
fetchData(true);
|
|
106
|
+
}, [fetchData]);
|
|
107
|
+
const clearCache = React.useCallback(() => {
|
|
108
|
+
pageCache.current.clear();
|
|
109
|
+
lastIdCache.current.clear();
|
|
110
|
+
}, []);
|
|
111
|
+
return {
|
|
112
|
+
items,
|
|
113
|
+
loading,
|
|
114
|
+
error,
|
|
115
|
+
currentPage,
|
|
116
|
+
totalCount,
|
|
117
|
+
hasMore,
|
|
118
|
+
refreshing,
|
|
119
|
+
nextPage,
|
|
120
|
+
prevPage,
|
|
121
|
+
goToPage,
|
|
122
|
+
refresh,
|
|
123
|
+
clearCache,
|
|
124
|
+
};
|
|
125
|
+
}
|