@runloop/rl-cli 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/dist/commands/blueprint/from-dockerfile.js +182 -0
- package/dist/commands/blueprint/list.js +97 -28
- package/dist/commands/blueprint/prune.js +7 -19
- package/dist/commands/devbox/create.js +3 -0
- package/dist/commands/devbox/list.js +44 -65
- package/dist/commands/menu.js +2 -1
- package/dist/commands/network-policy/create.js +27 -0
- package/dist/commands/network-policy/delete.js +21 -0
- package/dist/commands/network-policy/get.js +15 -0
- package/dist/commands/network-policy/list.js +494 -0
- package/dist/commands/object/list.js +516 -24
- package/dist/commands/snapshot/list.js +90 -29
- package/dist/components/Banner.js +109 -8
- package/dist/components/ConfirmationPrompt.js +45 -0
- package/dist/components/DevboxActionsMenu.js +42 -6
- package/dist/components/DevboxCard.js +1 -1
- package/dist/components/DevboxCreatePage.js +174 -168
- package/dist/components/DevboxDetailPage.js +218 -272
- package/dist/components/LogsViewer.js +8 -1
- package/dist/components/MainMenu.js +35 -4
- package/dist/components/NavigationTips.js +24 -0
- package/dist/components/NetworkPolicyCreatePage.js +263 -0
- package/dist/components/OperationsMenu.js +9 -1
- package/dist/components/ResourceActionsMenu.js +5 -1
- package/dist/components/ResourceDetailPage.js +204 -0
- package/dist/components/ResourceListView.js +19 -2
- package/dist/components/StatusBadge.js +2 -2
- package/dist/components/Table.js +6 -8
- package/dist/components/form/FormActionButton.js +7 -0
- package/dist/components/form/FormField.js +7 -0
- package/dist/components/form/FormListManager.js +112 -0
- package/dist/components/form/FormSelect.js +34 -0
- package/dist/components/form/FormTextInput.js +8 -0
- package/dist/components/form/index.js +8 -0
- package/dist/hooks/useViewportHeight.js +38 -20
- package/dist/router/Router.js +23 -1
- package/dist/screens/BlueprintDetailScreen.js +355 -0
- package/dist/screens/DevboxDetailScreen.js +4 -4
- package/dist/screens/MenuScreen.js +6 -0
- package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
- package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
- package/dist/screens/NetworkPolicyListScreen.js +7 -0
- package/dist/screens/ObjectDetailScreen.js +377 -0
- package/dist/screens/ObjectListScreen.js +7 -0
- package/dist/screens/SnapshotDetailScreen.js +208 -0
- package/dist/services/blueprintService.js +30 -11
- package/dist/services/networkPolicyService.js +108 -0
- package/dist/services/objectService.js +101 -0
- package/dist/services/snapshotService.js +39 -3
- package/dist/store/blueprintStore.js +4 -10
- package/dist/store/index.js +1 -0
- package/dist/store/networkPolicyStore.js +83 -0
- package/dist/store/objectStore.js +92 -0
- package/dist/store/snapshotStore.js +4 -8
- package/dist/utils/commands.js +65 -0
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@ import { SpinnerComponent } from "../../components/Spinner.js";
|
|
|
8
8
|
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
9
9
|
import { getStatusDisplay } from "../../components/StatusBadge.js";
|
|
10
10
|
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
11
|
+
import { NavigationTips } from "../../components/NavigationTips.js";
|
|
11
12
|
import { Table, createTextColumn } from "../../components/Table.js";
|
|
12
13
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
13
14
|
import { output, outputError } from "../../utils/output.js";
|
|
@@ -104,46 +105,24 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
104
105
|
const sourceWidth = 26;
|
|
105
106
|
// ID is always full width (25 chars for dbx_31CYd5LLFbBxst8mqnUjO format)
|
|
106
107
|
const idWidth = 26;
|
|
107
|
-
// Responsive layout
|
|
108
|
-
|
|
109
|
-
const
|
|
108
|
+
// Responsive layout - hide less important columns on smaller screens
|
|
109
|
+
// Priority (most to least important): ID, Name, Status, Created, Source, Capabilities
|
|
110
|
+
const showCapabilities = terminalWidth >= 160;
|
|
111
|
+
const showSource = terminalWidth >= 135;
|
|
112
|
+
const showCreated = terminalWidth >= 100;
|
|
110
113
|
// CRITICAL: Absolute maximum column widths to prevent Yoga crashes
|
|
111
114
|
const ABSOLUTE_MAX_NAME_WIDTH = 80;
|
|
112
115
|
// Name width is flexible and uses remaining space
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
12;
|
|
124
|
-
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(15, remainingWidth));
|
|
125
|
-
}
|
|
126
|
-
else if (terminalWidth >= 110) {
|
|
127
|
-
const remainingWidth = terminalWidth -
|
|
128
|
-
fixedWidth -
|
|
129
|
-
statusIconWidth -
|
|
130
|
-
idWidth -
|
|
131
|
-
statusTextWidth -
|
|
132
|
-
timeWidth -
|
|
133
|
-
sourceWidth -
|
|
134
|
-
10;
|
|
135
|
-
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(12, remainingWidth));
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
const remainingWidth = terminalWidth -
|
|
139
|
-
fixedWidth -
|
|
140
|
-
statusIconWidth -
|
|
141
|
-
idWidth -
|
|
142
|
-
statusTextWidth -
|
|
143
|
-
timeWidth -
|
|
144
|
-
10;
|
|
145
|
-
nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(8, remainingWidth));
|
|
146
|
-
}
|
|
116
|
+
// Only subtract widths of columns that are actually shown
|
|
117
|
+
const baseWidth = fixedWidth +
|
|
118
|
+
statusIconWidth +
|
|
119
|
+
idWidth +
|
|
120
|
+
statusTextWidth +
|
|
121
|
+
(showCreated ? timeWidth : 0) +
|
|
122
|
+
6; // border + padding
|
|
123
|
+
const optionalWidth = (showSource ? sourceWidth : 0) + (showCapabilities ? capabilitiesWidth : 0);
|
|
124
|
+
const remainingWidth = terminalWidth - baseWidth - optionalWidth;
|
|
125
|
+
const nameWidth = Math.min(ABSOLUTE_MAX_NAME_WIDTH, Math.max(15, remainingWidth));
|
|
147
126
|
// Build responsive column list (memoized to prevent recreating on every render)
|
|
148
127
|
const tableColumns = React.useMemo(() => {
|
|
149
128
|
const ABSOLUTE_MAX_NAME = 80;
|
|
@@ -156,22 +135,9 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
156
135
|
width: statusIconWidth,
|
|
157
136
|
render: (devbox, _index, isSelected) => {
|
|
158
137
|
const statusDisplay = getStatusDisplay(devbox?.status);
|
|
159
|
-
|
|
160
|
-
? colors.info
|
|
161
|
-
: statusDisplay.color;
|
|
162
|
-
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
138
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
163
139
|
},
|
|
164
140
|
},
|
|
165
|
-
createTextColumn("name", "Name", (devbox) => {
|
|
166
|
-
const name = String(devbox?.name || "");
|
|
167
|
-
const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
|
|
168
|
-
return name.length > safeMax
|
|
169
|
-
? name.substring(0, Math.max(1, safeMax - 3)) + "..."
|
|
170
|
-
: name;
|
|
171
|
-
}, {
|
|
172
|
-
width: Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME),
|
|
173
|
-
dimColor: false,
|
|
174
|
-
}),
|
|
175
141
|
createTextColumn("id", "ID", (devbox) => {
|
|
176
142
|
const id = String(devbox?.id || "");
|
|
177
143
|
const safeMax = Math.min(idWidth || 26, ABSOLUTE_MAX_ID);
|
|
@@ -184,6 +150,16 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
184
150
|
dimColor: false,
|
|
185
151
|
bold: false,
|
|
186
152
|
}),
|
|
153
|
+
createTextColumn("name", "Name", (devbox) => {
|
|
154
|
+
const name = String(devbox?.name || "");
|
|
155
|
+
const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
|
|
156
|
+
return name.length > safeMax
|
|
157
|
+
? name.substring(0, Math.max(1, safeMax - 3)) + "..."
|
|
158
|
+
: name;
|
|
159
|
+
}, {
|
|
160
|
+
width: Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME),
|
|
161
|
+
dimColor: false,
|
|
162
|
+
}),
|
|
187
163
|
// Status text column with color matching the icon
|
|
188
164
|
{
|
|
189
165
|
key: "status",
|
|
@@ -191,13 +167,10 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
191
167
|
width: statusTextWidth,
|
|
192
168
|
render: (devbox, _index, isSelected) => {
|
|
193
169
|
const statusDisplay = getStatusDisplay(devbox?.status);
|
|
194
|
-
const statusColor = statusDisplay.color === colors.textDim
|
|
195
|
-
? colors.info
|
|
196
|
-
: statusDisplay.color;
|
|
197
170
|
const safeWidth = Math.max(1, statusTextWidth);
|
|
198
171
|
const truncated = statusDisplay.text.slice(0, safeWidth);
|
|
199
172
|
const padded = truncated.padEnd(safeWidth, " ");
|
|
200
|
-
return (_jsx(Text, { color: isSelected ? "white" :
|
|
173
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
201
174
|
},
|
|
202
175
|
},
|
|
203
176
|
createTextColumn("created", "Created", (devbox) => {
|
|
@@ -208,18 +181,11 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
208
181
|
width: timeWidth,
|
|
209
182
|
color: colors.textDim,
|
|
210
183
|
dimColor: false,
|
|
184
|
+
visible: showCreated,
|
|
211
185
|
}),
|
|
212
186
|
];
|
|
213
187
|
if (showSource) {
|
|
214
|
-
columns.push(createTextColumn("source", "Source", (devbox) => {
|
|
215
|
-
if (devbox?.blueprint_id) {
|
|
216
|
-
const bpId = String(devbox.blueprint_id);
|
|
217
|
-
const truncated = bpId.slice(0, 16);
|
|
218
|
-
const text = `${truncated}`;
|
|
219
|
-
return text.length > 30 ? text.substring(0, 27) + "..." : text;
|
|
220
|
-
}
|
|
221
|
-
return "-";
|
|
222
|
-
}, {
|
|
188
|
+
columns.push(createTextColumn("source", "Source", (devbox) => devbox?.blueprint_id || "-", {
|
|
223
189
|
width: sourceWidth,
|
|
224
190
|
color: colors.textDim,
|
|
225
191
|
dimColor: false,
|
|
@@ -243,6 +209,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
243
209
|
idWidth,
|
|
244
210
|
statusTextWidth,
|
|
245
211
|
timeWidth,
|
|
212
|
+
showCreated,
|
|
246
213
|
showSource,
|
|
247
214
|
sourceWidth,
|
|
248
215
|
showCapabilities,
|
|
@@ -511,7 +478,19 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
511
478
|
setSelectedIndex(0);
|
|
512
479
|
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] })), !searchMode && submittedSearchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: submittedSearchQuery.length > 50
|
|
513
480
|
? submittedSearchQuery.substring(0, 50) + "..."
|
|
514
|
-
: submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })),
|
|
481
|
+
: submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
482
|
+
{
|
|
483
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
484
|
+
label: "Page",
|
|
485
|
+
condition: hasMore || hasPrev,
|
|
486
|
+
},
|
|
487
|
+
{ key: "Enter", label: "Details" },
|
|
488
|
+
{ key: "a", label: "Actions" },
|
|
489
|
+
{ key: "c", label: "Create" },
|
|
490
|
+
{ key: "o", label: "Open in Browser", condition: !!selectedDevbox },
|
|
491
|
+
{ key: "/", label: "Search" },
|
|
492
|
+
{ key: "Esc", label: "Back" },
|
|
493
|
+
] })] }));
|
|
515
494
|
};
|
|
516
495
|
// Export the UI component for use in the main menu
|
|
517
496
|
export { ListDevboxesUI };
|
package/dist/commands/menu.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
-
import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, } from "../utils/screen.js";
|
|
3
|
+
import { enterAlternateScreenBuffer, exitAlternateScreenBuffer, clearScreen, } from "../utils/screen.js";
|
|
4
4
|
import { processUtils } from "../utils/processUtils.js";
|
|
5
5
|
import { Router } from "../router/Router.js";
|
|
6
6
|
import { NavigationProvider } from "../store/navigationStore.js";
|
|
@@ -14,6 +14,7 @@ function App({ initialScreen = "menu", focusDevboxId, }) {
|
|
|
14
14
|
}
|
|
15
15
|
export async function runMainMenu(initialScreen = "menu", focusDevboxId) {
|
|
16
16
|
enterAlternateScreenBuffer();
|
|
17
|
+
clearScreen(); // Ensure cursor is at top-left before Ink renders
|
|
17
18
|
try {
|
|
18
19
|
const { waitUntilExit } = render(_jsx(App, { initialScreen: initialScreen, focusDevboxId: focusDevboxId }, `app-${initialScreen}-${focusDevboxId}`), {
|
|
19
20
|
patchConsole: false,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create network policy command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
export async function createNetworkPolicy(options) {
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
const policy = await client.networkPolicies.create({
|
|
10
|
+
name: options.name,
|
|
11
|
+
description: options.description,
|
|
12
|
+
allow_all: options.allowAll ?? false,
|
|
13
|
+
allow_devbox_to_devbox: options.allowDevboxToDevbox ?? false,
|
|
14
|
+
allowed_hostnames: options.allowedHostnames ?? [],
|
|
15
|
+
});
|
|
16
|
+
// Default: just output the ID for easy scripting
|
|
17
|
+
if (!options.output || options.output === "text") {
|
|
18
|
+
console.log(policy.id);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
output(policy, { format: options.output, defaultFormat: "json" });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
outputError("Failed to create network policy", error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delete network policy command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
export async function deleteNetworkPolicy(id, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
await client.networkPolicies.delete(id);
|
|
10
|
+
// Default: just output the ID for easy scripting
|
|
11
|
+
if (!options.output || options.output === "text") {
|
|
12
|
+
console.log(id);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
output({ id, status: "deleted" }, { format: options.output, defaultFormat: "json" });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
outputError("Failed to delete network policy", error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get network policy details command
|
|
3
|
+
*/
|
|
4
|
+
import { getClient } from "../../utils/client.js";
|
|
5
|
+
import { output, outputError } from "../../utils/output.js";
|
|
6
|
+
export async function getNetworkPolicy(options) {
|
|
7
|
+
try {
|
|
8
|
+
const client = getClient();
|
|
9
|
+
const policy = await client.networkPolicies.retrieve(options.id);
|
|
10
|
+
output(policy, { format: options.output, defaultFormat: "json" });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
outputError("Failed to get network policy", error);
|
|
14
|
+
}
|
|
15
|
+
}
|