@runloop/rl-cli 0.0.2 → 0.1.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 +64 -29
- package/dist/cli.js +420 -76
- package/dist/commands/auth.js +12 -10
- package/dist/commands/blueprint/create.js +108 -0
- package/dist/commands/blueprint/get.js +37 -0
- package/dist/commands/blueprint/list.js +303 -224
- 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 +390 -205
- 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 +70 -0
- 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 +59 -91
- package/dist/commands/snapshot/status.js +37 -0
- package/dist/components/ActionsPopup.js +16 -13
- package/dist/components/Banner.js +5 -8
- package/dist/components/Breadcrumb.js +6 -6
- package/dist/components/DetailView.js +7 -4
- package/dist/components/DevboxActionsMenu.js +347 -189
- package/dist/components/DevboxCard.js +15 -14
- package/dist/components/DevboxCreatePage.js +147 -113
- package/dist/components/DevboxDetailPage.js +182 -103
- package/dist/components/ErrorMessage.js +5 -4
- package/dist/components/Header.js +4 -3
- package/dist/components/MainMenu.js +72 -0
- 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 +22 -6
- package/dist/utils/client.js +20 -3
- package/dist/utils/config.js +40 -4
- package/dist/utils/interactiveCommand.js +14 -0
- package/dist/utils/output.js +17 -17
- package/dist/utils/ssh.js +160 -0
- package/dist/utils/sshSession.js +29 -0
- package/dist/utils/theme.js +22 -0
- package/dist/utils/url.js +39 -0
- package/package.json +29 -4
|
@@ -1,150 +1,105 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from
|
|
3
|
-
import { Box, Text, useInput, useStdout
|
|
4
|
-
import TextInput from
|
|
5
|
-
import figures from
|
|
6
|
-
import { getClient } from
|
|
7
|
-
import { Header } from
|
|
8
|
-
import { SpinnerComponent } from
|
|
9
|
-
import { ErrorMessage } from
|
|
10
|
-
import { SuccessMessage } from
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { getClient } from "../../utils/client.js";
|
|
7
|
+
import { Header } from "../../components/Header.js";
|
|
8
|
+
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
9
|
+
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
10
|
+
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
11
|
+
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
12
|
+
import { createTextColumn, Table } from "../../components/Table.js";
|
|
13
|
+
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
14
|
+
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
15
|
+
import { createExecutor } from "../../utils/CommandExecutor.js";
|
|
16
|
+
import { getBlueprintUrl } from "../../utils/url.js";
|
|
17
|
+
import { colors } from "../../utils/theme.js";
|
|
18
|
+
import { getStatusDisplay } from "../../components/StatusBadge.js";
|
|
19
|
+
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
|
|
17
20
|
const PAGE_SIZE = 10;
|
|
18
21
|
const MAX_FETCH = 100;
|
|
19
|
-
|
|
20
|
-
const formatTimeAgo = (timestamp) => {
|
|
21
|
-
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
22
|
-
if (seconds < 60)
|
|
23
|
-
return `${seconds}s ago`;
|
|
24
|
-
const minutes = Math.floor(seconds / 60);
|
|
25
|
-
if (minutes < 60)
|
|
26
|
-
return `${minutes}m ago`;
|
|
27
|
-
const hours = Math.floor(minutes / 60);
|
|
28
|
-
if (hours < 24)
|
|
29
|
-
return `${hours}h ago`;
|
|
30
|
-
const days = Math.floor(hours / 24);
|
|
31
|
-
if (days < 30)
|
|
32
|
-
return `${days}d ago`;
|
|
33
|
-
const months = Math.floor(days / 30);
|
|
34
|
-
if (months < 12)
|
|
35
|
-
return `${months}mo ago`;
|
|
36
|
-
const years = Math.floor(months / 12);
|
|
37
|
-
return `${years}y ago`;
|
|
38
|
-
};
|
|
39
|
-
const ListBlueprintsUI = () => {
|
|
22
|
+
const ListBlueprintsUI = ({ onBack, onExit }) => {
|
|
40
23
|
const { stdout } = useStdout();
|
|
41
|
-
const
|
|
42
|
-
const [loading, setLoading] = React.useState(true);
|
|
43
|
-
const [blueprints, setBlueprints] = React.useState([]);
|
|
44
|
-
const [error, setError] = React.useState(null);
|
|
45
|
-
const [currentPage, setCurrentPage] = React.useState(0);
|
|
46
|
-
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
47
|
-
const [showDetails, setShowDetails] = React.useState(false);
|
|
24
|
+
const [selectedBlueprint, setSelectedBlueprint] = React.useState(null);
|
|
48
25
|
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
49
26
|
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
50
|
-
const [operationInput, setOperationInput] = React.useState(
|
|
27
|
+
const [operationInput, setOperationInput] = React.useState("");
|
|
51
28
|
const [operationResult, setOperationResult] = React.useState(null);
|
|
52
29
|
const [operationError, setOperationError] = React.useState(null);
|
|
30
|
+
const [loading, setLoading] = React.useState(false);
|
|
31
|
+
const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
|
|
32
|
+
// List view state - moved to top to ensure hooks are called in same order
|
|
33
|
+
const [blueprints, setBlueprints] = React.useState([]);
|
|
34
|
+
const [listError, setListError] = React.useState(null);
|
|
35
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
36
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
37
|
+
const [showActions, setShowActions] = React.useState(false);
|
|
38
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
53
39
|
// Calculate responsive column widths
|
|
54
40
|
const terminalWidth = stdout?.columns || 120;
|
|
55
41
|
const showDescription = terminalWidth >= 120;
|
|
56
|
-
const
|
|
42
|
+
const statusIconWidth = 2;
|
|
43
|
+
const statusTextWidth = 10;
|
|
57
44
|
const idWidth = 25;
|
|
58
45
|
const nameWidth = terminalWidth >= 120 ? 30 : 25;
|
|
59
46
|
const descriptionWidth = 40;
|
|
60
47
|
const timeWidth = 20;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
48
|
+
// Helper function to generate operations based on selected blueprint
|
|
49
|
+
const getOperationsForBlueprint = (blueprint) => {
|
|
50
|
+
const operations = [];
|
|
51
|
+
// Only show create devbox option if blueprint is successfully built
|
|
52
|
+
if (blueprint &&
|
|
53
|
+
(blueprint.status === "build_complete" ||
|
|
54
|
+
blueprint.status === "building_complete")) {
|
|
55
|
+
operations.push({
|
|
56
|
+
key: "create_devbox",
|
|
57
|
+
label: "Create Devbox from Blueprint",
|
|
58
|
+
color: colors.success,
|
|
59
|
+
icon: figures.play,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Always show delete option
|
|
63
|
+
operations.push({
|
|
64
|
+
key: "delete",
|
|
65
|
+
label: "Delete Blueprint",
|
|
66
|
+
color: colors.error,
|
|
75
67
|
icon: figures.cross,
|
|
76
|
-
}
|
|
77
|
-
|
|
68
|
+
});
|
|
69
|
+
return operations;
|
|
70
|
+
};
|
|
71
|
+
// Fetch blueprints - moved to top to ensure hooks are called in same order
|
|
78
72
|
React.useEffect(() => {
|
|
79
|
-
const
|
|
73
|
+
const fetchBlueprints = async () => {
|
|
80
74
|
try {
|
|
75
|
+
setLoading(true);
|
|
81
76
|
const client = getClient();
|
|
82
77
|
const allBlueprints = [];
|
|
83
78
|
let count = 0;
|
|
84
79
|
for await (const blueprint of client.blueprints.list()) {
|
|
85
80
|
allBlueprints.push(blueprint);
|
|
86
81
|
count++;
|
|
87
|
-
if (count >= MAX_FETCH)
|
|
82
|
+
if (count >= MAX_FETCH)
|
|
88
83
|
break;
|
|
89
|
-
}
|
|
90
84
|
}
|
|
91
85
|
setBlueprints(allBlueprints);
|
|
92
86
|
}
|
|
93
87
|
catch (err) {
|
|
94
|
-
|
|
88
|
+
setListError(err);
|
|
95
89
|
}
|
|
96
90
|
finally {
|
|
97
91
|
setLoading(false);
|
|
98
92
|
}
|
|
99
93
|
};
|
|
100
|
-
|
|
94
|
+
fetchBlueprints();
|
|
101
95
|
}, []);
|
|
102
|
-
//
|
|
103
|
-
const prevShowDetailsRef = React.useRef(showDetails);
|
|
104
|
-
React.useEffect(() => {
|
|
105
|
-
if (showDetails && !prevShowDetailsRef.current) {
|
|
106
|
-
console.clear();
|
|
107
|
-
}
|
|
108
|
-
prevShowDetailsRef.current = showDetails;
|
|
109
|
-
}, [showDetails]);
|
|
110
|
-
// Auto-execute operations that don't need input
|
|
111
|
-
React.useEffect(() => {
|
|
112
|
-
if (executingOperation === 'delete' && !loading && selectedBlueprint) {
|
|
113
|
-
executeOperation();
|
|
114
|
-
}
|
|
115
|
-
}, [executingOperation]);
|
|
116
|
-
const executeOperation = async () => {
|
|
117
|
-
const client = getClient();
|
|
118
|
-
const blueprint = selectedBlueprint;
|
|
119
|
-
try {
|
|
120
|
-
setLoading(true);
|
|
121
|
-
switch (executingOperation) {
|
|
122
|
-
case 'create_devbox':
|
|
123
|
-
const devbox = await client.devboxes.create({
|
|
124
|
-
blueprint_id: blueprint.id,
|
|
125
|
-
name: operationInput || undefined,
|
|
126
|
-
});
|
|
127
|
-
setOperationResult(`Devbox created successfully!\n\n` +
|
|
128
|
-
`ID: ${devbox.id}\n` +
|
|
129
|
-
`Name: ${devbox.name || '(none)'}\n` +
|
|
130
|
-
`Status: ${devbox.status}\n\n` +
|
|
131
|
-
`Use 'rln devbox list' to view all devboxes.`);
|
|
132
|
-
break;
|
|
133
|
-
case 'delete':
|
|
134
|
-
await client.blueprints.delete(blueprint.id);
|
|
135
|
-
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
136
|
-
break;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
140
|
-
setOperationError(err);
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
setLoading(false);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
96
|
+
// Handle input for all views - combined into single hook
|
|
146
97
|
useInput((input, key) => {
|
|
147
|
-
|
|
98
|
+
// Handle Ctrl+C to force exit
|
|
99
|
+
if (key.ctrl && input === "c") {
|
|
100
|
+
process.stdout.write("\x1b[?1049l"); // Exit alternate screen
|
|
101
|
+
process.exit(130);
|
|
102
|
+
}
|
|
148
103
|
// Handle operation input mode
|
|
149
104
|
if (executingOperation && !operationResult && !operationError) {
|
|
150
105
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
@@ -152,95 +107,130 @@ const ListBlueprintsUI = () => {
|
|
|
152
107
|
if (key.return) {
|
|
153
108
|
executeOperation();
|
|
154
109
|
}
|
|
155
|
-
else if (input ===
|
|
110
|
+
else if (input === "q" || key.escape) {
|
|
156
111
|
console.clear();
|
|
157
112
|
setExecutingOperation(null);
|
|
158
|
-
setOperationInput(
|
|
113
|
+
setOperationInput("");
|
|
159
114
|
}
|
|
160
115
|
}
|
|
161
116
|
return;
|
|
162
117
|
}
|
|
163
118
|
// Handle operation result display
|
|
164
119
|
if (operationResult || operationError) {
|
|
165
|
-
if (input ===
|
|
120
|
+
if (input === "q" || key.escape || key.return) {
|
|
166
121
|
console.clear();
|
|
167
122
|
setOperationResult(null);
|
|
168
123
|
setOperationError(null);
|
|
169
124
|
setExecutingOperation(null);
|
|
170
|
-
setOperationInput(
|
|
125
|
+
setOperationInput("");
|
|
171
126
|
}
|
|
172
127
|
return;
|
|
173
128
|
}
|
|
174
|
-
// Handle
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
else if (input === 'o' && selectedBlueprint) {
|
|
182
|
-
// Open in browser
|
|
183
|
-
const url = `https://platform.runloop.ai/blueprints/${selectedBlueprint.id}`;
|
|
184
|
-
const openBrowser = async () => {
|
|
185
|
-
const { exec } = await import('child_process');
|
|
186
|
-
const platform = process.platform;
|
|
187
|
-
let openCommand;
|
|
188
|
-
if (platform === 'darwin') {
|
|
189
|
-
openCommand = `open "${url}"`;
|
|
190
|
-
}
|
|
191
|
-
else if (platform === 'win32') {
|
|
192
|
-
openCommand = `start "${url}"`;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
openCommand = `xdg-open "${url}"`;
|
|
196
|
-
}
|
|
197
|
-
exec(openCommand);
|
|
198
|
-
};
|
|
199
|
-
openBrowser();
|
|
200
|
-
}
|
|
201
|
-
else if (key.upArrow && selectedOperation > 0) {
|
|
129
|
+
// Handle create devbox view
|
|
130
|
+
if (showCreateDevbox) {
|
|
131
|
+
return; // Let DevboxCreatePage handle its own input
|
|
132
|
+
}
|
|
133
|
+
// Handle actions popup overlay: consume keys and prevent table nav
|
|
134
|
+
if (showPopup) {
|
|
135
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
202
136
|
setSelectedOperation(selectedOperation - 1);
|
|
203
137
|
}
|
|
204
|
-
else if (key.downArrow &&
|
|
138
|
+
else if (key.downArrow &&
|
|
139
|
+
selectedOperation < allOperations.length - 1) {
|
|
205
140
|
setSelectedOperation(selectedOperation + 1);
|
|
206
141
|
}
|
|
207
142
|
else if (key.return) {
|
|
208
143
|
console.clear();
|
|
209
|
-
|
|
210
|
-
|
|
144
|
+
setShowPopup(false);
|
|
145
|
+
const operationKey = allOperations[selectedOperation].key;
|
|
146
|
+
if (operationKey === "create_devbox") {
|
|
147
|
+
// Go directly to create devbox screen
|
|
148
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
149
|
+
setShowCreateDevbox(true);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
// Execute other operations normally
|
|
153
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
154
|
+
setExecutingOperation(operationKey);
|
|
155
|
+
executeOperation();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (key.escape || input === "q") {
|
|
159
|
+
console.clear();
|
|
160
|
+
setShowPopup(false);
|
|
161
|
+
setSelectedOperation(0);
|
|
162
|
+
}
|
|
163
|
+
else if (input === "c") {
|
|
164
|
+
// Create devbox hotkey - only if blueprint is complete
|
|
165
|
+
if (selectedBlueprintItem &&
|
|
166
|
+
(selectedBlueprintItem.status === "build_complete" ||
|
|
167
|
+
selectedBlueprintItem.status === "building_complete")) {
|
|
168
|
+
console.clear();
|
|
169
|
+
setShowPopup(false);
|
|
170
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
171
|
+
setShowCreateDevbox(true);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (input === "d") {
|
|
175
|
+
// Delete hotkey
|
|
176
|
+
const deleteIndex = allOperations.findIndex((op) => op.key === "delete");
|
|
177
|
+
if (deleteIndex >= 0) {
|
|
178
|
+
console.clear();
|
|
179
|
+
setShowPopup(false);
|
|
180
|
+
setSelectedBlueprint(selectedBlueprintItem);
|
|
181
|
+
setExecutingOperation("delete");
|
|
182
|
+
executeOperation();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return; // prevent falling through to list nav
|
|
186
|
+
}
|
|
187
|
+
// Handle actions view
|
|
188
|
+
if (showActions) {
|
|
189
|
+
if (input === "q" || key.escape) {
|
|
190
|
+
console.clear();
|
|
191
|
+
setShowActions(false);
|
|
192
|
+
setSelectedOperation(0);
|
|
211
193
|
}
|
|
212
194
|
return;
|
|
213
195
|
}
|
|
214
|
-
// Handle list view
|
|
196
|
+
// Handle list navigation (default view)
|
|
197
|
+
const pageSize = PAGE_SIZE;
|
|
198
|
+
const totalPages = Math.ceil(blueprints.length / pageSize);
|
|
199
|
+
const startIndex = currentPage * pageSize;
|
|
200
|
+
const endIndex = Math.min(startIndex + pageSize, blueprints.length);
|
|
201
|
+
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
202
|
+
const pageBlueprints = currentBlueprints.length;
|
|
215
203
|
if (key.upArrow && selectedIndex > 0) {
|
|
216
204
|
setSelectedIndex(selectedIndex - 1);
|
|
217
205
|
}
|
|
218
206
|
else if (key.downArrow && selectedIndex < pageBlueprints - 1) {
|
|
219
207
|
setSelectedIndex(selectedIndex + 1);
|
|
220
208
|
}
|
|
221
|
-
else if ((input ===
|
|
209
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
210
|
+
currentPage < totalPages - 1) {
|
|
222
211
|
setCurrentPage(currentPage + 1);
|
|
223
212
|
setSelectedIndex(0);
|
|
224
213
|
}
|
|
225
|
-
else if ((input ===
|
|
214
|
+
else if ((input === "p" || key.leftArrow) && currentPage > 0) {
|
|
226
215
|
setCurrentPage(currentPage - 1);
|
|
227
216
|
setSelectedIndex(0);
|
|
228
217
|
}
|
|
229
|
-
else if (
|
|
218
|
+
else if (input === "a") {
|
|
230
219
|
console.clear();
|
|
231
|
-
|
|
220
|
+
setShowPopup(true);
|
|
221
|
+
setSelectedOperation(0);
|
|
232
222
|
}
|
|
233
|
-
else if (input ===
|
|
223
|
+
else if (input === "o" && currentBlueprints[selectedIndex]) {
|
|
234
224
|
// Open in browser
|
|
235
|
-
const url =
|
|
225
|
+
const url = getBlueprintUrl(currentBlueprints[selectedIndex].id);
|
|
236
226
|
const openBrowser = async () => {
|
|
237
|
-
const { exec } = await import(
|
|
227
|
+
const { exec } = await import("child_process");
|
|
238
228
|
const platform = process.platform;
|
|
239
229
|
let openCommand;
|
|
240
|
-
if (platform ===
|
|
230
|
+
if (platform === "darwin") {
|
|
241
231
|
openCommand = `open "${url}"`;
|
|
242
232
|
}
|
|
243
|
-
else if (platform ===
|
|
233
|
+
else if (platform === "win32") {
|
|
244
234
|
openCommand = `start "${url}"`;
|
|
245
235
|
}
|
|
246
236
|
else {
|
|
@@ -250,25 +240,65 @@ const ListBlueprintsUI = () => {
|
|
|
250
240
|
};
|
|
251
241
|
openBrowser();
|
|
252
242
|
}
|
|
253
|
-
else if (
|
|
254
|
-
|
|
243
|
+
else if (key.escape) {
|
|
244
|
+
if (onBack) {
|
|
245
|
+
onBack();
|
|
246
|
+
}
|
|
247
|
+
else if (onExit) {
|
|
248
|
+
onExit();
|
|
249
|
+
}
|
|
255
250
|
}
|
|
256
251
|
});
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
252
|
+
// Pagination computed early to allow hooks before any returns
|
|
253
|
+
const pageSize = PAGE_SIZE;
|
|
254
|
+
const totalPages = Math.ceil(blueprints.length / pageSize);
|
|
255
|
+
const startIndex = currentPage * pageSize;
|
|
256
|
+
const endIndex = Math.min(startIndex + pageSize, blueprints.length);
|
|
260
257
|
const currentBlueprints = blueprints.slice(startIndex, endIndex);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
// Ensure selected index is within bounds - place before any returns
|
|
259
|
+
React.useEffect(() => {
|
|
260
|
+
if (currentBlueprints.length > 0 &&
|
|
261
|
+
selectedIndex >= currentBlueprints.length) {
|
|
262
|
+
setSelectedIndex(Math.max(0, currentBlueprints.length - 1));
|
|
263
|
+
}
|
|
264
|
+
}, [currentBlueprints.length, selectedIndex]);
|
|
265
|
+
const selectedBlueprintItem = currentBlueprints[selectedIndex];
|
|
266
|
+
// Generate operations based on selected blueprint status
|
|
267
|
+
const allOperations = getOperationsForBlueprint(selectedBlueprintItem);
|
|
268
|
+
const executeOperation = async () => {
|
|
269
|
+
const client = getClient();
|
|
270
|
+
const blueprint = selectedBlueprint;
|
|
271
|
+
if (!blueprint)
|
|
272
|
+
return;
|
|
273
|
+
try {
|
|
274
|
+
setLoading(true);
|
|
275
|
+
switch (executingOperation) {
|
|
276
|
+
case "create_devbox":
|
|
277
|
+
// Navigate to create devbox screen with blueprint pre-filled
|
|
278
|
+
setShowCreateDevbox(true);
|
|
279
|
+
setExecutingOperation(null);
|
|
280
|
+
setLoading(false);
|
|
281
|
+
return;
|
|
282
|
+
case "delete":
|
|
283
|
+
await client.blueprints.delete(blueprint.id);
|
|
284
|
+
setOperationResult(`Blueprint ${blueprint.id} deleted successfully`);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
setOperationError(err);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
setLoading(false);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
265
295
|
// Filter operations based on blueprint status
|
|
266
296
|
const operations = selectedBlueprint
|
|
267
297
|
? allOperations.filter((op) => {
|
|
268
298
|
const status = selectedBlueprint.status;
|
|
269
299
|
// Only allow creating devbox if build is complete
|
|
270
|
-
if (op.key ===
|
|
271
|
-
return status ===
|
|
300
|
+
if (op.key === "create_devbox") {
|
|
301
|
+
return status === "build_complete";
|
|
272
302
|
}
|
|
273
303
|
// Allow delete for any status
|
|
274
304
|
return true;
|
|
@@ -276,92 +306,141 @@ const ListBlueprintsUI = () => {
|
|
|
276
306
|
: allOperations;
|
|
277
307
|
// Operation result display
|
|
278
308
|
if (operationResult || operationError) {
|
|
279
|
-
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
309
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
310
|
+
"Operation";
|
|
280
311
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
281
|
-
{ label:
|
|
312
|
+
{ label: "Blueprints" },
|
|
282
313
|
{
|
|
283
|
-
label: selectedBlueprint?.name || selectedBlueprint?.id ||
|
|
314
|
+
label: selectedBlueprint?.name || selectedBlueprint?.id || "Blueprint",
|
|
284
315
|
},
|
|
285
316
|
{ label: operationLabel, active: true },
|
|
286
|
-
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && _jsx(ErrorMessage, { message: "Operation failed", error: operationError }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color:
|
|
317
|
+
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter], [q], or [esc] to continue" }) })] }));
|
|
287
318
|
}
|
|
288
319
|
// Operation input mode
|
|
289
320
|
if (executingOperation && selectedBlueprint) {
|
|
290
321
|
const currentOp = allOperations.find((op) => op.key === executingOperation);
|
|
291
322
|
const needsInput = currentOp?.needsInput;
|
|
292
|
-
const operationLabel = currentOp?.label ||
|
|
323
|
+
const operationLabel = currentOp?.label || "Operation";
|
|
293
324
|
if (loading) {
|
|
294
325
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
295
|
-
{ label:
|
|
326
|
+
{ label: "Blueprints" },
|
|
296
327
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
297
328
|
{ label: operationLabel, active: true },
|
|
298
329
|
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: "Please wait..." })] }));
|
|
299
330
|
}
|
|
300
331
|
if (!needsInput) {
|
|
301
332
|
const messages = {
|
|
302
|
-
delete:
|
|
333
|
+
delete: "Deleting blueprint...",
|
|
303
334
|
};
|
|
304
335
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
305
|
-
{ label:
|
|
336
|
+
{ label: "Blueprints" },
|
|
306
337
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
307
338
|
{ label: operationLabel, active: true },
|
|
308
|
-
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] ||
|
|
339
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
309
340
|
}
|
|
310
341
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
311
|
-
{ label:
|
|
342
|
+
{ label: "Blueprints" },
|
|
312
343
|
{ label: selectedBlueprint.name || selectedBlueprint.id },
|
|
313
344
|
{ label: operationLabel, active: true },
|
|
314
|
-
] }), _jsx(Header, { title: operationLabel }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color:
|
|
345
|
+
] }), _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(Box, { marginTop: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: "Press [Enter] to execute \u2022 [q or esc] Cancel" }) })] })] }));
|
|
315
346
|
}
|
|
316
|
-
//
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (direction === 'up' && selectedOperation > 0) {
|
|
331
|
-
setSelectedOperation(selectedOperation - 1);
|
|
332
|
-
}
|
|
333
|
-
else if (direction === 'down' && selectedOperation < operations.length - 1) {
|
|
334
|
-
setSelectedOperation(selectedOperation + 1);
|
|
335
|
-
}
|
|
336
|
-
}, onSelect: (op) => {
|
|
337
|
-
console.clear();
|
|
338
|
-
setExecutingOperation(op.key);
|
|
339
|
-
}, onBack: () => {
|
|
340
|
-
console.clear();
|
|
341
|
-
setShowDetails(false);
|
|
342
|
-
setSelectedOperation(0);
|
|
343
|
-
}, additionalActions: [{ key: 'o', label: 'Browser', handler: () => { } }] })] }));
|
|
347
|
+
// Create devbox screen
|
|
348
|
+
if (showCreateDevbox && selectedBlueprint) {
|
|
349
|
+
return (_jsx(DevboxCreatePage, { onBack: () => {
|
|
350
|
+
setShowCreateDevbox(false);
|
|
351
|
+
setSelectedBlueprint(null);
|
|
352
|
+
}, onCreate: (devbox) => {
|
|
353
|
+
// Return to blueprint list after creation
|
|
354
|
+
setShowCreateDevbox(false);
|
|
355
|
+
setSelectedBlueprint(null);
|
|
356
|
+
}, initialBlueprintId: selectedBlueprint.id }));
|
|
357
|
+
}
|
|
358
|
+
// Loading state
|
|
359
|
+
if (loading) {
|
|
360
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SpinnerComponent, { message: "Loading blueprints..." })] }));
|
|
344
361
|
}
|
|
362
|
+
// Error state
|
|
363
|
+
if (listError) {
|
|
364
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
|
|
365
|
+
}
|
|
366
|
+
// Empty state
|
|
367
|
+
if (blueprints.length === 0) {
|
|
368
|
+
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" })] })] }));
|
|
369
|
+
}
|
|
370
|
+
// Pagination moved earlier
|
|
371
|
+
// Overlay: draw quick actions popup over the table (keep table visible)
|
|
345
372
|
// List view
|
|
346
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label:
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(Table, { data: currentBlueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${blueprints.length}]`, columns: [
|
|
374
|
+
{
|
|
375
|
+
key: "statusIcon",
|
|
376
|
+
label: "",
|
|
377
|
+
width: statusIconWidth,
|
|
378
|
+
render: (blueprint, index, isSelected) => {
|
|
379
|
+
const statusDisplay = getStatusDisplay(blueprint.status);
|
|
380
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
381
|
+
? colors.info
|
|
382
|
+
: statusDisplay.color;
|
|
383
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
key: "id",
|
|
388
|
+
label: "ID",
|
|
389
|
+
width: idWidth + 1,
|
|
390
|
+
render: (blueprint, index, isSelected) => {
|
|
391
|
+
const value = blueprint.id;
|
|
392
|
+
const width = idWidth + 1;
|
|
393
|
+
const truncated = value.slice(0, width - 1);
|
|
394
|
+
const padded = truncated.padEnd(width, " ");
|
|
395
|
+
return (_jsx(Text, { color: isSelected ? "white" : colors.textDim, bold: false, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
key: "statusText",
|
|
400
|
+
label: "Status",
|
|
401
|
+
width: statusTextWidth,
|
|
402
|
+
render: (blueprint, index, isSelected) => {
|
|
403
|
+
const statusDisplay = getStatusDisplay(blueprint.status);
|
|
404
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
405
|
+
? colors.info
|
|
406
|
+
: statusDisplay.color;
|
|
407
|
+
const truncated = statusDisplay.text.slice(0, statusTextWidth);
|
|
408
|
+
const padded = truncated.padEnd(statusTextWidth, " ");
|
|
409
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
createTextColumn("name", "Name", (blueprint) => blueprint.name || "(unnamed)", {
|
|
413
|
+
width: nameWidth,
|
|
414
|
+
}),
|
|
415
|
+
createTextColumn("description", "Description", (blueprint) => blueprint.dockerfile_setup?.description || "", {
|
|
416
|
+
width: descriptionWidth,
|
|
417
|
+
color: colors.textDim,
|
|
418
|
+
dimColor: false,
|
|
419
|
+
bold: false,
|
|
420
|
+
visible: showDescription,
|
|
421
|
+
}),
|
|
422
|
+
createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
|
|
423
|
+
? formatTimeAgo(blueprint.create_time_ms)
|
|
424
|
+
: "", {
|
|
425
|
+
width: timeWidth,
|
|
426
|
+
color: colors.textDim,
|
|
427
|
+
dimColor: false,
|
|
428
|
+
bold: false,
|
|
429
|
+
}),
|
|
430
|
+
] }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", blueprints.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _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 ", blueprints.length] })] }), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: -Math.min(allOperations.length + 10, PAGE_SIZE + 5), justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
|
|
431
|
+
key: op.key,
|
|
432
|
+
label: op.label,
|
|
433
|
+
color: op.color,
|
|
434
|
+
icon: op.icon,
|
|
435
|
+
shortcut: op.key === "create_devbox"
|
|
436
|
+
? "c"
|
|
437
|
+
: op.key === "delete"
|
|
438
|
+
? "d"
|
|
439
|
+
: "",
|
|
440
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate"] }), totalPages > 1 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 ", figures.arrowLeft, figures.arrowRight, " Page"] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [a] Actions"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [o] Browser"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [Esc] Back"] })] })] }));
|
|
364
441
|
};
|
|
442
|
+
// Export the UI component for use in the main menu
|
|
443
|
+
export { ListBlueprintsUI };
|
|
365
444
|
export async function listBlueprints(options = {}) {
|
|
366
445
|
const executor = createExecutor(options);
|
|
367
446
|
await executor.executeList(async () => {
|