@runloop/rl-cli 1.2.0 → 1.3.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 +28 -8
- 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 +95 -81
- 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 +264 -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 +337 -0
- 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 +47 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://github.com/runloopai/rl-cli/actions/workflows/ci.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A **TUI + CLI** for the [Runloop.ai](https://runloop.ai) platform. Use it as an **interactive TUI** (Terminal User Interface) with rich UI components, or as a **traditional CLI** for scripting and automation.
|
|
8
8
|
|
|
9
9
|
📖 **[Full Documentation](https://docs.runloop.ai/docs/tools/cli)**
|
|
10
10
|
|
|
@@ -15,10 +15,10 @@ An interactive CLI for interacting with the [Runloop.ai](https://runloop.ai) pla
|
|
|
15
15
|
## Quick Example
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
#
|
|
18
|
+
# TUI mode - launches an interactive terminal UI
|
|
19
19
|
rli
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# CLI mode - perfect for scripts and automation
|
|
22
22
|
rli devbox list # Outputs JSON/text
|
|
23
23
|
rli devbox create --name my-devbox
|
|
24
24
|
rli devbox exec <devbox-id> echo "Hello World"
|
|
@@ -27,10 +27,11 @@ rli devbox delete <devbox-id>
|
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
30
|
+
- 🖥️ **TUI mode** — Interactive terminal UI with menus, tables, and real-time updates
|
|
31
|
+
- 🎯 **CLI mode** — Traditional commands with text, JSON, and YAML output for scripting
|
|
30
32
|
- ⚡ Fast and responsive with pagination
|
|
31
33
|
- 📦 Manage devboxes, snapshots, and blueprints
|
|
32
|
-
- 🚀 Execute commands,
|
|
33
|
-
- 🎯 Traditional CLI with text, json, and yaml output modes.
|
|
34
|
+
- 🚀 Execute commands, SSH, view logs in devboxes
|
|
34
35
|
- 🤖 **Model Context Protocol (MCP) server for AI integration**
|
|
35
36
|
|
|
36
37
|
## Installation
|
|
@@ -53,13 +54,23 @@ Get your API key from [https://runloop.ai/settings](https://runloop.ai/settings)
|
|
|
53
54
|
|
|
54
55
|
## Usage
|
|
55
56
|
|
|
56
|
-
### Interactive
|
|
57
|
+
### TUI (Interactive Mode)
|
|
57
58
|
|
|
58
59
|
```bash
|
|
59
|
-
rli #
|
|
60
|
+
rli # Launch the interactive TUI
|
|
60
61
|
rli --help # See help information
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### CLI (Scripting Mode)
|
|
65
|
+
|
|
66
|
+
All commands support `--output` (`-o`) for format control:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
rli devbox list # Default text output
|
|
70
|
+
rli devbox list -o json # JSON output
|
|
71
|
+
rli devbox list -o yaml # YAML output
|
|
72
|
+
```
|
|
73
|
+
|
|
63
74
|
## Command Structure
|
|
64
75
|
|
|
65
76
|
The CLI is organized into command buckets:
|
|
@@ -119,6 +130,15 @@ rli object upload <path> # Upload a file as an object
|
|
|
119
130
|
rli object delete <id> # Delete an object (irreversible)
|
|
120
131
|
```
|
|
121
132
|
|
|
133
|
+
### Network-policy Commands (alias: `np`)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
rli network-policy list # List network policies
|
|
137
|
+
rli network-policy get <id> # Get network policy details
|
|
138
|
+
rli network-policy create # Create a new network policy
|
|
139
|
+
rli network-policy delete <id> # Delete a network policy
|
|
140
|
+
```
|
|
141
|
+
|
|
122
142
|
### Mcp Commands
|
|
123
143
|
|
|
124
144
|
```bash
|
|
@@ -160,7 +180,7 @@ rli mcp start --http --port 8080
|
|
|
160
180
|
|
|
161
181
|
## Theme Configuration
|
|
162
182
|
|
|
163
|
-
The
|
|
183
|
+
The TUI supports both light and dark terminal themes and will automatically select the appropriate theme.
|
|
164
184
|
|
|
165
185
|
## Development
|
|
166
186
|
|
|
@@ -9,6 +9,7 @@ import { SpinnerComponent } from "../../components/Spinner.js";
|
|
|
9
9
|
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
10
10
|
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
11
11
|
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
12
|
+
import { NavigationTips } from "../../components/NavigationTips.js";
|
|
12
13
|
import { createTextColumn, Table } from "../../components/Table.js";
|
|
13
14
|
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
14
15
|
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
@@ -21,6 +22,7 @@ import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
|
|
|
21
22
|
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
|
|
22
23
|
import { useCursorPagination } from "../../hooks/useCursorPagination.js";
|
|
23
24
|
import { useNavigation } from "../../store/navigationStore.js";
|
|
25
|
+
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
24
26
|
const DEFAULT_PAGE_SIZE = 10;
|
|
25
27
|
const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
26
28
|
const { exit: inkExit } = useApp();
|
|
@@ -33,6 +35,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
33
35
|
const [operationError, setOperationError] = React.useState(null);
|
|
34
36
|
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
35
37
|
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
38
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
36
39
|
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
37
40
|
const [showPopup, setShowPopup] = React.useState(false);
|
|
38
41
|
const { navigate } = useNavigation();
|
|
@@ -44,13 +47,18 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
44
47
|
});
|
|
45
48
|
const PAGE_SIZE = viewportHeight;
|
|
46
49
|
// All width constants
|
|
50
|
+
const fixedWidth = 6; // border + padding
|
|
47
51
|
const statusIconWidth = 2;
|
|
48
52
|
const statusTextWidth = 10;
|
|
49
53
|
const idWidth = 25;
|
|
50
|
-
const nameWidth = Math.max(15, terminalWidth >= 120 ? 30 : 25);
|
|
51
54
|
const descriptionWidth = 40;
|
|
52
55
|
const timeWidth = 20;
|
|
53
56
|
const showDescription = terminalWidth >= 120;
|
|
57
|
+
// Name width uses remaining space after fixed columns
|
|
58
|
+
const baseWidth = fixedWidth + statusIconWidth + statusTextWidth + idWidth + timeWidth;
|
|
59
|
+
const optionalWidth = showDescription ? descriptionWidth : 0;
|
|
60
|
+
const remainingWidth = terminalWidth - baseWidth - optionalWidth;
|
|
61
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
54
62
|
// Fetch function for pagination hook
|
|
55
63
|
const fetchPage = React.useCallback(async (params) => {
|
|
56
64
|
const client = getClient();
|
|
@@ -88,7 +96,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
88
96
|
pageSize: PAGE_SIZE,
|
|
89
97
|
getItemId: (blueprint) => blueprint.id,
|
|
90
98
|
pollInterval: 2000,
|
|
91
|
-
pollingEnabled: !showPopup &&
|
|
99
|
+
pollingEnabled: !showPopup &&
|
|
100
|
+
!showCreateDevbox &&
|
|
101
|
+
!executingOperation &&
|
|
102
|
+
!showDeleteConfirm,
|
|
92
103
|
deps: [PAGE_SIZE],
|
|
93
104
|
});
|
|
94
105
|
// Memoize columns array
|
|
@@ -99,10 +110,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
99
110
|
width: statusIconWidth,
|
|
100
111
|
render: (blueprint, _index, isSelected) => {
|
|
101
112
|
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
102
|
-
|
|
103
|
-
? colors.info
|
|
104
|
-
: statusDisplay.color;
|
|
105
|
-
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
113
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
106
114
|
},
|
|
107
115
|
},
|
|
108
116
|
{
|
|
@@ -123,13 +131,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
123
131
|
width: statusTextWidth,
|
|
124
132
|
render: (blueprint, _index, isSelected) => {
|
|
125
133
|
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
126
|
-
const statusColor = statusDisplay.color === colors.textDim
|
|
127
|
-
? colors.info
|
|
128
|
-
: statusDisplay.color;
|
|
129
134
|
const safeWidth = Math.max(1, statusTextWidth);
|
|
130
135
|
const truncated = statusDisplay.text.slice(0, safeWidth);
|
|
131
136
|
const padded = truncated.padEnd(safeWidth, " ");
|
|
132
|
-
return (_jsx(Text, { color: isSelected ? "white" :
|
|
137
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
133
138
|
},
|
|
134
139
|
},
|
|
135
140
|
createTextColumn("name", "Name", (blueprint) => blueprint.name || "", {
|
|
@@ -156,6 +161,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
156
161
|
// Helper function to generate operations based on selected blueprint
|
|
157
162
|
const getOperationsForBlueprint = (blueprint) => {
|
|
158
163
|
const operations = [];
|
|
164
|
+
// View Details is always first
|
|
165
|
+
operations.push({
|
|
166
|
+
key: "view_details",
|
|
167
|
+
label: "View Details",
|
|
168
|
+
color: colors.primary,
|
|
169
|
+
icon: figures.pointer,
|
|
170
|
+
});
|
|
159
171
|
// View Logs is always available
|
|
160
172
|
operations.push({
|
|
161
173
|
key: "view_logs",
|
|
@@ -213,6 +225,14 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
213
225
|
try {
|
|
214
226
|
setOperationLoading(true);
|
|
215
227
|
switch (operation) {
|
|
228
|
+
case "view_details":
|
|
229
|
+
// Navigate to the detail screen
|
|
230
|
+
setOperationLoading(false);
|
|
231
|
+
setExecutingOperation(null);
|
|
232
|
+
navigate("blueprint-detail", {
|
|
233
|
+
blueprintId: blueprint.id,
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
216
236
|
case "view_logs":
|
|
217
237
|
// Navigate to the logs screen
|
|
218
238
|
setOperationLoading(false);
|
|
@@ -295,16 +315,33 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
295
315
|
else if (key.return) {
|
|
296
316
|
setShowPopup(false);
|
|
297
317
|
const operationKey = allOperations[selectedOperation].key;
|
|
298
|
-
if (operationKey === "
|
|
318
|
+
if (operationKey === "view_details") {
|
|
319
|
+
navigate("blueprint-detail", {
|
|
320
|
+
blueprintId: selectedBlueprintItem.id,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else if (operationKey === "create_devbox") {
|
|
299
324
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
300
325
|
setShowCreateDevbox(true);
|
|
301
326
|
}
|
|
327
|
+
else if (operationKey === "delete") {
|
|
328
|
+
// Show delete confirmation
|
|
329
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
330
|
+
setShowDeleteConfirm(true);
|
|
331
|
+
}
|
|
302
332
|
else {
|
|
303
333
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
304
334
|
setExecutingOperation(operationKey);
|
|
305
335
|
executeOperation(selectedBlueprintItem, operationKey);
|
|
306
336
|
}
|
|
307
337
|
}
|
|
338
|
+
else if (input === "v" && selectedBlueprintItem) {
|
|
339
|
+
// View details hotkey
|
|
340
|
+
setShowPopup(false);
|
|
341
|
+
navigate("blueprint-detail", {
|
|
342
|
+
blueprintId: selectedBlueprintItem.id,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
308
345
|
else if (key.escape || input === "q") {
|
|
309
346
|
setShowPopup(false);
|
|
310
347
|
setSelectedOperation(0);
|
|
@@ -321,10 +358,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
321
358
|
else if (input === "d") {
|
|
322
359
|
const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
|
|
323
360
|
if (deleteIndex >= 0) {
|
|
361
|
+
// Show delete confirmation
|
|
324
362
|
setShowPopup(false);
|
|
325
363
|
setSelectedBlueprint(selectedBlueprintItem);
|
|
326
|
-
|
|
327
|
-
executeOperation(selectedBlueprintItem, "delete");
|
|
364
|
+
setShowDeleteConfirm(true);
|
|
328
365
|
}
|
|
329
366
|
}
|
|
330
367
|
else if (input === "l") {
|
|
@@ -360,6 +397,12 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
360
397
|
prevPage();
|
|
361
398
|
setSelectedIndex(0);
|
|
362
399
|
}
|
|
400
|
+
else if (key.return && selectedBlueprintItem) {
|
|
401
|
+
// Enter key navigates to detail view
|
|
402
|
+
navigate("blueprint-detail", {
|
|
403
|
+
blueprintId: selectedBlueprintItem.id,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
363
406
|
else if (input === "a") {
|
|
364
407
|
setShowPopup(true);
|
|
365
408
|
setSelectedOperation(0);
|
|
@@ -400,6 +443,21 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
400
443
|
}
|
|
401
444
|
}
|
|
402
445
|
});
|
|
446
|
+
// Delete confirmation
|
|
447
|
+
if (showDeleteConfirm && selectedBlueprint) {
|
|
448
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Blueprint", message: `Are you sure you want to delete "${selectedBlueprint.name || selectedBlueprint.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
|
|
449
|
+
{ label: "Blueprints" },
|
|
450
|
+
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
451
|
+
{ label: "Delete", active: true },
|
|
452
|
+
], onConfirm: () => {
|
|
453
|
+
setShowDeleteConfirm(false);
|
|
454
|
+
setExecutingOperation("delete");
|
|
455
|
+
executeOperation(selectedBlueprint, "delete");
|
|
456
|
+
}, onCancel: () => {
|
|
457
|
+
setShowDeleteConfirm(false);
|
|
458
|
+
setSelectedBlueprint(null);
|
|
459
|
+
} }));
|
|
460
|
+
}
|
|
403
461
|
// Operation result display
|
|
404
462
|
if (operationResult || operationError) {
|
|
405
463
|
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
@@ -410,7 +468,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
410
468
|
label: selectedBlueprint?.name || selectedBlueprint?.id || "Blueprint",
|
|
411
469
|
},
|
|
412
470
|
{ label: operationLabel, active: true },
|
|
413
|
-
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(
|
|
471
|
+
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Continue" }] })] }));
|
|
414
472
|
}
|
|
415
473
|
// Operation input mode
|
|
416
474
|
if (executingOperation && selectedBlueprint) {
|
|
@@ -435,7 +493,10 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
435
493
|
{ label: "Blueprints" },
|
|
436
494
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
437
495
|
{ label: operationLabel, active: true },
|
|
438
|
-
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp?.inputPrompt || "", " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp?.inputPlaceholder || "" }) }), _jsx(
|
|
496
|
+
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.primary, bold: true, children: selectedBlueprint.name || selectedBlueprint.id }) }), _jsx(Box, { children: _jsxs(Text, { color: colors.textDim, children: [currentOp?.inputPrompt || "", " "] }) }), _jsx(Box, { marginTop: 1, children: _jsx(TextInput, { value: operationInput, onChange: setOperationInput, placeholder: currentOp?.inputPlaceholder || "" }) }), _jsx(NavigationTips, { marginTop: 1, paddingX: 0, tips: [
|
|
497
|
+
{ key: "Enter", label: "Execute" },
|
|
498
|
+
{ key: "q/esc", label: "Cancel" },
|
|
499
|
+
] })] })] }));
|
|
439
500
|
}
|
|
440
501
|
// For operations that don't need input (like view_logs), fall through to list view
|
|
441
502
|
}
|
|
@@ -458,24 +519,32 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
|
|
|
458
519
|
if (listError) {
|
|
459
520
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
460
521
|
}
|
|
461
|
-
// Empty state
|
|
462
|
-
if (blueprints.length === 0) {
|
|
463
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.warning, children: figures.info }), _jsx(Text, { children: " No blueprints found. Try: " }), _jsx(Text, { color: colors.primary, bold: true, children: "rli blueprint create" })] })] }));
|
|
464
|
-
}
|
|
465
522
|
// List view
|
|
466
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns })), !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] })] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
|
|
523
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No blueprints found. Try: rli blueprint create"] }) })), !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] })] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
|
|
467
524
|
key: op.key,
|
|
468
525
|
label: op.label,
|
|
469
526
|
color: op.color,
|
|
470
527
|
icon: op.icon,
|
|
471
|
-
shortcut: op.key === "
|
|
472
|
-
? "
|
|
473
|
-
: op.key === "
|
|
474
|
-
? "
|
|
475
|
-
: op.key === "
|
|
476
|
-
? "
|
|
477
|
-
: ""
|
|
478
|
-
|
|
528
|
+
shortcut: op.key === "view_details"
|
|
529
|
+
? "v"
|
|
530
|
+
: op.key === "create_devbox"
|
|
531
|
+
? "c"
|
|
532
|
+
: op.key === "delete"
|
|
533
|
+
? "d"
|
|
534
|
+
: op.key === "view_logs"
|
|
535
|
+
? "l"
|
|
536
|
+
: "",
|
|
537
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
538
|
+
{
|
|
539
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
540
|
+
label: "Page",
|
|
541
|
+
condition: hasMore || hasPrev,
|
|
542
|
+
},
|
|
543
|
+
{ key: "Enter", label: "Details" },
|
|
544
|
+
{ key: "a", label: "Actions" },
|
|
545
|
+
{ key: "o", label: "Browser" },
|
|
546
|
+
{ key: "Esc", label: "Back" },
|
|
547
|
+
] })] }));
|
|
479
548
|
};
|
|
480
549
|
// Export the UI component for use in the main menu
|
|
481
550
|
export { ListBlueprintsUI };
|
|
@@ -42,9 +42,9 @@ async function fetchAllBlueprintsWithName(name) {
|
|
|
42
42
|
*/
|
|
43
43
|
function categorizeBlueprints(blueprints, keepCount) {
|
|
44
44
|
// Filter successful builds
|
|
45
|
-
const successful = blueprints.filter((b) => b.status === "build_complete"
|
|
46
|
-
// Filter failed builds
|
|
47
|
-
const failed = blueprints.filter((b) => b.status !== "build_complete"
|
|
45
|
+
const successful = blueprints.filter((b) => b.status === "build_complete");
|
|
46
|
+
// Filter failed/incomplete builds
|
|
47
|
+
const failed = blueprints.filter((b) => b.status !== "build_complete");
|
|
48
48
|
// Sort successful by create_time_ms descending (newest first)
|
|
49
49
|
successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
|
|
50
50
|
// Determine what to keep and delete
|
|
@@ -106,14 +106,8 @@ function displaySummary(name, result, isDryRun) {
|
|
|
106
106
|
else {
|
|
107
107
|
// Show all blueprints without summarizing
|
|
108
108
|
for (const blueprint of result.toDelete) {
|
|
109
|
-
const icon = blueprint.status === "build_complete"
|
|
110
|
-
|
|
111
|
-
? "✓"
|
|
112
|
-
: "✗";
|
|
113
|
-
const statusLabel = blueprint.status === "build_complete" ||
|
|
114
|
-
blueprint.status === "building_complete"
|
|
115
|
-
? "successful"
|
|
116
|
-
: "failed";
|
|
109
|
+
const icon = blueprint.status === "build_complete" ? "✓" : "⚠";
|
|
110
|
+
const statusLabel = blueprint.status === "build_complete" ? "successful" : "failed";
|
|
117
111
|
console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
|
|
118
112
|
}
|
|
119
113
|
}
|
|
@@ -127,14 +121,8 @@ function displayDeletedBlueprints(deleted) {
|
|
|
127
121
|
}
|
|
128
122
|
console.log("\nDeleted blueprints:");
|
|
129
123
|
for (const blueprint of deleted) {
|
|
130
|
-
const icon = blueprint.status === "build_complete"
|
|
131
|
-
|
|
132
|
-
? "✓"
|
|
133
|
-
: "✗";
|
|
134
|
-
const statusLabel = blueprint.status === "build_complete" ||
|
|
135
|
-
blueprint.status === "building_complete"
|
|
136
|
-
? "successful"
|
|
137
|
-
: "failed";
|
|
124
|
+
const icon = blueprint.status === "build_complete" ? "✓" : "⚠";
|
|
125
|
+
const statusLabel = blueprint.status === "build_complete" ? "successful" : "failed";
|
|
138
126
|
console.log(` ${icon} ${blueprint.id} - Created ${formatTimestamp(blueprint.create_time_ms)} (${statusLabel})`);
|
|
139
127
|
}
|
|
140
128
|
}
|
|
@@ -74,6 +74,9 @@ export async function createDevbox(options = {}) {
|
|
|
74
74
|
on_idle: options.idleAction,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
if (options.networkPolicy) {
|
|
78
|
+
launchParameters.network_policy_id = options.networkPolicy;
|
|
79
|
+
}
|
|
77
80
|
// Build create request
|
|
78
81
|
const createRequest = {
|
|
79
82
|
name: options.name || `devbox-${Date.now()}`,
|
|
@@ -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
|
+
}
|