@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
@@ -1,55 +1,85 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
- function BlueprintsTable({ blueprints, selectedIndex, terminalWidth }) {
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('status', 'Status', (bp) => _jsx(StatusBadge, { status: bp.status, showText: false }), { width: 2 }),
13
+ createComponentColumn("status", "Status", (bp) => _jsx(StatusBadge, { status: bp.status, showText: false }), { width: 2 }),
13
14
  // ID column (responsive)
14
- createTextColumn('id', 'ID', (bp) => showFullId ? bp.id : bp.id.slice(0, 13), { width: showFullId ? 25 : 15, color: 'gray', dimColor: true, bold: false }),
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('name', 'Name', (bp) => bp.name || '(unnamed)', { width: 30 }),
22
+ createTextColumn("name", "Name", (bp) => bp.name || "(unnamed)", { width: 30 }),
17
23
  // Description column (optional)
18
- createTextColumn('description', 'Description', (bp) => bp.description || '', { width: 40, color: 'gray', dimColor: true, bold: false, visible: showDescription }),
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('created', 'Created', (bp) => new Date(bp.created_at).toLocaleDateString(), { width: 15, color: 'gray', dimColor: true, bold: false }),
21
- ], emptyState: _jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [figures.info, " No blueprints found"] }) }) }));
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('status', 'Status', (snap) => _jsx(StatusBadge, { status: snap.status, showText: false }), { width: 2 }),
41
+ createComponentColumn("status", "Status", (snap) => _jsx(StatusBadge, { status: snap.status, showText: false }), { width: 2 }),
30
42
  // ID column (responsive)
31
- createTextColumn('id', 'ID', (snap) => showFullId ? snap.id : snap.id.slice(0, 13), { width: showFullId ? 25 : 15, color: 'gray', dimColor: true, bold: false }),
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('name', 'Name', (snap) => snap.name || '(unnamed)', { width: 25 }),
50
+ createTextColumn("name", "Name", (snap) => snap.name || "(unnamed)", { width: 25 }),
34
51
  // Devbox ID column
35
- createTextColumn('devbox', 'Devbox', (snap) => snap.devbox_id.slice(0, 13), { width: 15, color: 'cyan', dimColor: true, bold: false }),
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('size', 'Size', (snap) => snap.size_gb ? `${snap.size_gb.toFixed(1)}GB` : '', { width: 10, color: 'yellow', dimColor: true, bold: false, visible: showSize }),
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('created', 'Created', (snap) => new Date(snap.created_at).toLocaleDateString(), { width: 15, color: 'gray', dimColor: true, bold: false }),
40
- ], emptyState: _jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [figures.info, " No snapshots found"] }) }) }));
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: '1', name: 'Item 1', tags: ['tag1', 'tag2'] },
48
- { id: '2', name: 'Item 2', tags: ['tag3'] },
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('name', 'Name', (item) => item.name, { width: 20 }),
79
+ createTextColumn("name", "Name", (item) => item.name, {
80
+ width: 20,
81
+ }),
52
82
  // Custom component column with complex rendering
53
- createComponentColumn('tags', 'Tags', (item, index, isSelected) => (_jsx(Box, { width: 30, children: _jsx(Text, { color: isSelected ? 'cyan' : 'blue', dimColor: true, children: item.tags.map(tag => `[${tag}]`).join(' ') }) })), { width: 30 }),
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
  }
@@ -1,7 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Box, Text } from 'ink';
4
- import figures from 'figures';
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: "cyan", bold: true, children: ["\u256D\u2500 ", title, " ", ''.repeat(Math.max(0, 10)), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", borderStyle: title ? 'single' : 'round', borderColor: "gray", 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) => {
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 ? 'cyan' : 'gray', 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
+ 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 ? 'white' : 'white');
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
- // Pad the value to fill the full width
37
- const truncated = value.slice(0, width);
38
- const padded = truncated.padEnd(width, ' ');
39
- return (_jsx(Text, { color: isSelected ? 'white' : color, bold: bold, dimColor: !isSelected && dimColor, inverse: isSelected, children: padded }));
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
+ }